├── .gitignore ├── LICENSE-MIT.txt ├── MANIFEST.in ├── README.rst ├── orcid ├── __init__.py ├── constants.py ├── exceptions.py ├── rest.py └── utils.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE-MIT.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2013 Scholrly, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A simple Python wrapper around the ORCID.org API. 2 | 3 | Example 4 | ======= 5 | 6 | Here's a quick snippet to get info on `John Wilbanks`_. :: 7 | 8 | >>> import orcid 9 | >>> #retrieve john's profile from his ORCID 10 | >>> john = orcid.get('0000-0002-4510-0385') 11 | >>> print john.family_name 12 | wilbanks 13 | 14 | What if you'd like to see an author's works or areas of interest? :: 15 | 16 | >>> print john.keywords 17 | [] 18 | >>> print john.publications 19 | [] 20 | 21 | Hm, let's try another author. :: 22 | 23 | >>> alfonso = orcid.get('0000-0001-8855-5569') 24 | >>> print alfonso.keywords 25 | [u'computer science', u' bioinformatics', u' computational biology'] 26 | >>> print alfonso.publications[0] 27 | 28 | 29 | 30 | Maybe you'd like to read about Mr. Wiener's contributions? :: 31 | 32 | >>> print alfonso.publications[0].url 33 | http://www.scopus.com/inward/record.url?eid=2-s2.0-67650513866&partnerID=MN8TOARS 34 | 35 | Searching 36 | ========= 37 | 38 | If you'd rather search for authors, try ORCID's search functionality :: 39 | 40 | >>> #do a simple author search for john 41 | >>> authors = orcid.search('john wilbanks') 42 | >>> print next(authors).family_name 43 | wilbanks 44 | 45 | You can also accomplish more complex queries using `Q` objects and fields :: 46 | 47 | >>> from orcid import Q 48 | >>> authors = orcid.search(Q('given-name','john') & Q('family-name', 'wilbanks')) 49 | >>> print next(authors).family_name 50 | wilbanks 51 | 52 | Enjoy! 53 | 54 | .. _John Wilbanks: http://en.wikipedia.org/wiki/John_Wilbanks -------------------------------------------------------------------------------- /orcid/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ('get', 'search', 'Q') 2 | 3 | from .rest import get, search 4 | from lucenequerybuilder import Q 5 | -------------------------------------------------------------------------------- /orcid/constants.py: -------------------------------------------------------------------------------- 1 | ORCID_PUBLIC_BASE_URL = 'http://pub.orcid.org/' 2 | ORCID_SANDBOX_BASE_URL = 'http://pub.orcid.org/' 3 | -------------------------------------------------------------------------------- /orcid/exceptions.py: -------------------------------------------------------------------------------- 1 | class ORCIDException(Exception): 2 | pass 3 | 4 | class NotFoundException(ORCIDException): 5 | pass 6 | -------------------------------------------------------------------------------- /orcid/rest.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from .constants import ORCID_PUBLIC_BASE_URL 4 | from .utils import dictmapper, MappingRule as to 5 | 6 | from .exceptions import NotFoundException 7 | 8 | BASE_HEADERS = {'Accept':'application/orcid+json'} 9 | 10 | BIO_PATH = ['orcid-profile','orcid-bio'] 11 | PERSONAL_DETAILS_PATH = BIO_PATH + ['personal-details'] 12 | 13 | def _parse_keywords(d): 14 | # XXX yes, splitting on commas is bad- but a bug in ORCID 15 | # (https://github.com/ORCID/ORCID-Parent/issues/27) makes this the best 16 | # way. will fix when they do 17 | if d is not None: 18 | return d.get('keyword',[{}])[0].get('value','').split(',') 19 | return [] 20 | 21 | WebsiteBase = dictmapper('WebsiteBase', { 22 | 'name':['url-name','value'], 23 | 'url':['url', 'value'] 24 | }) 25 | 26 | class Website(WebsiteBase): 27 | def __unicode__(self): 28 | return self.url 29 | 30 | def __repr__(self): 31 | return "<%s %s [%s]>" % (type(self).__name__, self.name, self.url) 32 | 33 | def _parse_researcher_urls(l): 34 | if l is not None: 35 | return [Website(d) for d in l] 36 | return [] 37 | 38 | CitationBase = dictmapper('CitationBase', { 39 | 'text':['citation'], 40 | 'type':['work-citation-type'] 41 | }) 42 | 43 | class Citation(CitationBase): 44 | def __unicode__(self): 45 | return self.text 46 | 47 | def __repr__(self): 48 | return '<%s [type: %s]>' % (type(self).__name__, self.type) 49 | 50 | ExternalIDBase = dictmapper('ExternalIDBase', { 51 | 'id':['work-external-identifier-id','value'], 52 | 'type':['work-external-identifier-type'] 53 | }) 54 | 55 | class ExternalID(ExternalIDBase): 56 | def __unicode__(self): 57 | return unicode(self.id) 58 | 59 | def __repr__(self): 60 | return '<%s %s:%s>' % (type(self).__name__, self.type, str(self.id)) 61 | 62 | PublicationBase = dictmapper('PublicationBase',{ 63 | 'title':['work-title','title','value'], 64 | 'subtitle':['work-title','subtitle','value'], 65 | 'url':['url','value'], 66 | 'citation':to(['citation'], lambda d: Citation(d) if d is not None else None), 67 | 'external_ids':to(['work-external-identifiers','work-external-identifier'], 68 | lambda l: map(ExternalID, l) if l is not None else None), 69 | }) 70 | 71 | class Publication(PublicationBase): 72 | def __repr__(self): 73 | return '<%s "%s">' % (type(self).__name__, self.title) 74 | 75 | WORKS_PATH = ['orcid-profile', 'orcid-activities','orcid-works',] 76 | 77 | def _parse_publications(l): 78 | if l is not None: 79 | return [Publication(d) for d in l] 80 | return [] 81 | 82 | Works = dictmapper('Works', { 83 | 'publications':to(WORKS_PATH + ['orcid-work'], _parse_publications), 84 | }) 85 | 86 | AuthorBase = dictmapper('AuthorBase', { 87 | 'orcid':['orcid-profile','orcid','value'], 88 | 'family_name':PERSONAL_DETAILS_PATH + ['family-name','value'], 89 | 'given_name':PERSONAL_DETAILS_PATH + ['given-names','value'], 90 | 'biography':BIO_PATH + ['biography',], 91 | 'keywords':to(BIO_PATH + ['keywords'], _parse_keywords), 92 | 'researcher_urls':to(BIO_PATH + ['researcher-urls','researcher-url'], 93 | _parse_researcher_urls), 94 | }) 95 | 96 | class Author(AuthorBase): 97 | _loaded_works = None 98 | 99 | def _load_works(self): 100 | resp = requests.get(ORCID_PUBLIC_BASE_URL + self.orcid 101 | + '/orcid-works', headers = BASE_HEADERS) 102 | self._loaded_works = Works(resp.json()) 103 | 104 | @property 105 | def publications(self): 106 | if self._loaded_works is None: 107 | self._load_works() 108 | return self._loaded_works.publications 109 | 110 | def __repr__(self): 111 | return "<%s %s %s, ORCID %s>" % (type(self).__name__, self.given_name, 112 | self.family_name, self.orcid) 113 | 114 | Citation = dictmapper('Citation', { 115 | 'citation':['citation'], 116 | 'citation_type':['work-citation-type'] 117 | }) 118 | 119 | def get(orcid_id): 120 | """ 121 | Get an author based on an ORCID identifier. 122 | """ 123 | resp = requests.get(ORCID_PUBLIC_BASE_URL + unicode(orcid_id), 124 | headers=BASE_HEADERS) 125 | json_body = resp.json() 126 | return Author(json_body) 127 | 128 | def search(query): 129 | resp = requests.get(ORCID_PUBLIC_BASE_URL + 'search/orcid-bio', 130 | params={'q':unicode(query)}, headers=BASE_HEADERS) 131 | json_body = resp.json() 132 | return (Author(res) for res in json_body.get('orcid-search-results', {})\ 133 | .get('orcid-search-result')) 134 | -------------------------------------------------------------------------------- /orcid/utils.py: -------------------------------------------------------------------------------- 1 | def dict_value_from_path(d, path): 2 | cur_dict = d 3 | for key in path[:-1]: 4 | cur_dict = cur_dict.get(key, {}) 5 | return cur_dict.get(path[-1], None) if cur_dict is not None else None 6 | 7 | def dictmapper(typename, mapping): 8 | """ 9 | A factory to create `namedtuple`-like classes from a field-to-dict-path 10 | mapping:: 11 | 12 | Person = dictmapper({'person':('person','name')}) 13 | example_dict = {'person':{'name':'John'}} 14 | john = Person(example_dict) 15 | assert john.name == 'John' 16 | 17 | If a function is specified as a mapping value instead of a dict "path", it 18 | will be run with the backing dict as its first argument. 19 | """ 20 | def init(self, d, *args, **kwargs): 21 | """ 22 | Initialize `dictmapper` classes with a dict to back getters. 23 | """ 24 | self._original_dict = d 25 | 26 | def getter_from_dict_path(path): 27 | if not callable(path) and len(path) < 1: 28 | raise ValueError('Dict paths should be iterables with at least one' 29 | ' key or callable objects that take one argument.') 30 | def getter(self): 31 | cur_dict = self._original_dict 32 | if callable(path): 33 | return path(cur_dict) 34 | return dict_value_from_path(cur_dict, path) 35 | return getter 36 | 37 | prop_mapping = dict((k, property(getter_from_dict_path(v))) 38 | for k, v in mapping.iteritems()) 39 | prop_mapping['__init__'] = init 40 | return type(typename, tuple(), prop_mapping) 41 | 42 | class MappingRule(object): 43 | def __init__(self, path, further_func = lambda x : x): 44 | self.path = path 45 | self.further_func = further_func 46 | 47 | def __call__(self, d): 48 | return self.further_func(dict_value_from_path(d, self.path)) 49 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==1.0.4 2 | lucene-querybuilder==0.1.6 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | def readme(): 4 | with open('README.rst') as f: 5 | return f.read() 6 | 7 | setup(name='orcid-python', 8 | version='0.1', 9 | description='A simple wrapper around the ORCID.org API.', 10 | long_description=readme(), 11 | classifiers=[ 12 | 'Development Status :: 3 - Alpha', 13 | 'License :: OSI Approved :: MIT License', 14 | ], 15 | url='https://github.com/scholrly/orcid-python', 16 | author='Matt Luongo', 17 | author_email='mhluongo@gmail.com', 18 | license='MIT', 19 | packages=['orcid'], 20 | install_requires=[ 21 | 'requests>=1.0.4', 22 | 'lucene-querybuilder>=0.1.6', 23 | ] 24 | ) 25 | --------------------------------------------------------------------------------