├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── MANIFEST ├── MANIFEST.in ├── README.md ├── README.txt ├── pyaltmetric └── __init__.py ├── setup.py └── tests ├── __init__.py ├── fixtures ├── .DS_Store ├── average.json ├── empty.json ├── full.json └── wrong.txt └── test_article.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | #Altmetric API Key 56 | altmetric_api_key.txt 57 | 58 | #PyCharm Configs 59 | .idea/ 60 | .DS_Store 61 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | v0.1.0, <07/09/2014> -- Initial release. 2 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Center for Open Science 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | CHANGES.txt 3 | LICENSE.txt 4 | README.txt 5 | requirements.txt 6 | setup.py 7 | altmetricwrapper/__init__.py 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include docs *.txt 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | =========== 2 | PyAltmetric 3 | =========== 4 | 5 | PyAltmetric provides an easy python wrapper for the Altmetric API. 6 | 7 | Typical usage often looks like this: 8 | 9 | from pyaltmetric import Altmetric 10 | 11 | Create an Altmetric object: 12 | 13 | altmetric_object = Altmetric() 14 | 15 | Creating an Altmetric object caches api version number and key. The 16 | object is then used to make calls to the Altmetric API. From here you can 17 | easily create article objects filled with Altmetric information simply by 18 | giving an id for the article you want information on: 19 | 20 | article1 = altmetric_object.article_from_doi("doi_of_article") 21 | 22 | There are various id's to choose from including: bibcode, pmid, and arxiv id. 23 | 24 | After you have an article you can easily extract altmetric information 25 | using the built in attributes such as: 26 | 27 | article1.title 28 | article1.abstract 29 | article1.score 30 | 31 | If you want to gather multiple article metrics at once you can use the 32 | articles_from_timeframe method. This will give you articles with internet 33 | mentions within a given time period (past 3 days, past month, ect): 34 | 35 | articles = altmetric_object.articles_from_timeframe("1 day") 36 | 37 | It returns an article gerator that is easily iterable. 38 | 39 | If you already have a json from the Altmetric API you can create 40 | articles directly. 41 | 42 | First you need to import article: 43 | 44 | from pyaltmetric import Article 45 | 46 | Then you can simply create articles. There are three ways to do so. 47 | 48 | 1. Supplying a properly constructed dictionary stright into the constructor: 49 | 50 | article2 = Article(json_dict) 51 | 52 | 2. Supplying the filename of a properly formatted json and using the class method from_json_file: 53 | 54 | article3 = Article.from_json_file(file) 55 | 56 | 3. Supplying an alreay open file of properly formatted json and using the class method from_json_file: 57 | 58 | article3 = Article.from_json_file("filename.json") 59 | 60 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | =========== 2 | PyAltmetric 3 | =========== 4 | 5 | PyAltmetric provides an easy python wrapper for the Altmetric API. 6 | 7 | Typical usage often looks like this:: 8 | 9 | from pyaltmetric import Altmetric 10 | 11 | Create an Altmetric object:: 12 | 13 | altmetric_object = Altmetric() 14 | 15 | Creating an Altmetric object caches api version number and key. The 16 | object is then used to make calls to the Altmetric API. From here you can 17 | easily create article objects filled with Altmetric information simply by 18 | giving an id for the article you want information on:: 19 | 20 | article1 = altmetric_object.article_from_doi("doi_of_article") 21 | 22 | There are various id's to choose from including: bibcode, pmid, and arxiv id. 23 | 24 | After you have an article you can easily extract altmetric information 25 | using the built in attributes such as:: 26 | 27 | article1.title 28 | article1.abstract 29 | article1.score 30 | 31 | If you want to gather multiple article metrics at once you can use the 32 | articles_from_timeframe method. This will give you articles with internet 33 | mentions within a given time period (past 3 days, past month, ect):: 34 | 35 | articles = altmetric_object.articles_from_timeframe("1 day") 36 | 37 | It returns an article gerator which can easily be iterated through. 38 | 39 | If you already have a json from the Altmetric API you can create an 40 | article directly. 41 | 42 | First you need to import article:: 43 | 44 | from pyaltmetric import Article 45 | 46 | Then you can simply create atricles. There are three ways to do so: 47 | 48 | 1. Using supplying a properly constructed dictionary stright into the 49 | constructor. 50 | ::article2 = Article(json_dict) 51 | 2. Supplying the filename of a properly formatted json and using the 52 | class method from_json_file. 53 | ::article3 = Article.from_json_file(file) 54 | 3. Supplying an alreay open file of properly formatted json and using the 55 | class method from_json_file. 56 | ::article3 = Article.from_json_file("filename.json") 57 | 58 | -------------------------------------------------------------------------------- /pyaltmetric/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyAltmetric 3 | This is a python wrapper for the Altmetric API. 4 | 5 | For more information on the Altmetric API visit http://api.altmetric.com/. 6 | 7 | Some pieces of this library were inspired by or derived from the altmetric api 8 | wrapper altmetric.py which is licensed under the MIT open source license. 9 | 10 | If you display Altmetric data please attribute Altmetric somewhere on your 11 | page. 12 | """ 13 | 14 | import requests 15 | import datetime 16 | import warnings 17 | import json 18 | 19 | class AltmetricException(Exception): 20 | """Base class for any pyaltmetric error.""" 21 | pass 22 | 23 | class JSONParseException(AltmetricException): 24 | """ 25 | Failed to turn HTTP Response into JSON. 26 | Site is probably in the wrong format. 27 | """ 28 | pass 29 | 30 | class AltmetricHTTPException(AltmetricException): 31 | """A query argument or setting was formatted incorrectly.""" 32 | def __init__(self, status_code): 33 | response_codes = { 34 | 403:"You are not authorized for this call.", 35 | 420:"Rate Limit Reached", 36 | 502:"API is down.", 37 | } 38 | super(AltmetricHTTPException, self).__init__( 39 | response_codes.get(status_code, status_code) 40 | ) 41 | 42 | class IncorrectInput(AltmetricException): 43 | """Informing the user that their query is incorrect.""" 44 | def __init__(self, msg): 45 | super(IncorrectInput, self).__init__(msg) 46 | 47 | class Altmetric(object): 48 | def __init__(self, api_key = None, api_version = 'v1'): 49 | """Cache API key and version.""" 50 | self._api_version = api_version 51 | if self._api_version != 'v1': 52 | warnings.warn("This wrapper has only been tested with API v1." 53 | "If you try another version it will probably break.") 54 | 55 | self._api_url = "http://api.altmetric.com/%s/" % self.api_version 56 | 57 | self._api_key = {} 58 | if api_key: 59 | self._api_key = {'key': api_key} 60 | 61 | #Make articles 62 | def article_from_doi(self, doi): 63 | """Create an Article object using DOI.""" 64 | raw_json = self._get_altmetrics('doi', doi) 65 | return self._create_article(raw_json) 66 | 67 | def article_from_pmid(self, pmid): 68 | """Create an Article object using PMID.""" 69 | raw_json = self._get_altmetrics('pmid', pmid) 70 | return self._create_article(raw_json) 71 | 72 | def article_from_altmetric(self, altmetric_id): 73 | """Create an Article object using Altmetric ID.""" 74 | warnings.warn("Altmetric ID's are subject to change.") 75 | raw_json = self._get_altmetrics('id', altmetric_id) 76 | return self._create_article(raw_json) 77 | 78 | def article_from_ads(self, ads_bibcode): 79 | """Create an Article object using ADS Bibcode.""" 80 | raw_json = self._get_altmetrics('ads', ads_bibcode) 81 | return self._create_article(raw_json) 82 | 83 | def article_from_arxiv(self, arxiv_id): 84 | """Create an Article object using arXiv ID.""" 85 | raw_json = self._get_altmetrics('arxiv', arxiv_id) 86 | return self._create_article(raw_json) 87 | 88 | def articles_from_timeframe(self, timeframe, page = 1, num_results = 100, 89 | doi_prefix = None, nlmid = None, subjects = None, cited_in = None): 90 | 91 | """ 92 | Return articles with mentions within a certain timeframe keyword 93 | arguments can further limit the search. 94 | 95 | :param timeframe: Argument for past x days/months/years. In format: 96 | 1d, 1m, 1y... 97 | :param page: Integer. Which page of results you are on. 98 | :param num_results: 1-100. Number of results per page. 99 | :param doi_prefix: Limits results to those with this doi prefix. 100 | :param nlmid: List of journal NLM IDs. 101 | :param subjects: List of slugified journal subjects, accepts NLM 102 | subject ontology term(s). 103 | :param cited_in: Options of facebook, blogs, linkedin, video, 104 | pinterest, gplus,twitter, reddit, news, f1000, rh, qna, 105 | forum, peerreview. 106 | """ 107 | 108 | timeframe = self._check_timeframe(timeframe) 109 | 110 | while(1): 111 | raw_json = self._get_altmetrics('citations', timeframe, 112 | page = page, num_results = num_results, 113 | doi_prefix = doi_prefix, nlmid = nlmid, 114 | subjects = subjects, cited_in = cited_in) 115 | page += 1 116 | if not raw_json: 117 | break 118 | for result in raw_json.get('results', []): 119 | yield self._create_article(result) 120 | 121 | def _get_altmetrics(self, method, *args, **kwargs): 122 | """ 123 | Request information from Altmetric. Return a dictionary. 124 | """ 125 | request_url = self.api_url + method + "/" + "/".join([a for a in args]) 126 | params = kwargs or {} 127 | params.update(self.api_key) 128 | response = requests.get(request_url, params = params) 129 | if response.status_code == 200: 130 | try: 131 | return response.json() 132 | except ValueError as e: 133 | raise JSONParseException(e.message) 134 | elif response.status_code in (404, 400): 135 | return {} 136 | else: 137 | raise AltmetricHTTPException(response.status_code) 138 | 139 | def _create_article(self, json): 140 | """Return an article object.""" 141 | try: 142 | return Article(json) 143 | except AttributeError: 144 | return None 145 | 146 | def _check_timeframe(self, timeframe): 147 | if len(timeframe) > 2: 148 | if timeframe == 'all time': 149 | timeframe = 'at' 150 | else: 151 | timeframe = timeframe[0]+timeframe[2] 152 | 153 | if timeframe not in [ 154 | 'at','1d','2d','3d','4d','5d','6d','1w','1m','3m','6m','1y']: 155 | 156 | raise IncorrectInput("Invalid timeframe entered.") 157 | 158 | return timeframe 159 | 160 | 161 | @property 162 | def api_version(self): 163 | return self._api_version 164 | 165 | @property 166 | def api_url(self): 167 | return self._api_url 168 | 169 | @property 170 | def api_key(self): 171 | return self._api_key 172 | 173 | 174 | class Article(): 175 | def __init__(self, raw_dict): 176 | """ 177 | Create an article object. Get raw dictionary from 178 | Altmetrics JSON. Parse dictionary into attributes. 179 | """ 180 | if raw_dict: 181 | self._raw = raw_dict 182 | self._parse_raw() 183 | else: 184 | raise AttributeError 185 | 186 | @classmethod 187 | def from_json_file(cls, filename): 188 | """Return article from filename or path.""" 189 | try: 190 | with open(filename) as fi: 191 | raw = json.load(fi) 192 | obj = Article(raw) 193 | return obj 194 | except ValueError as e: 195 | raise JSONParseException(e.message) 196 | 197 | @classmethod 198 | def from_json(cls, file_): 199 | """Return an article from file.""" 200 | try: 201 | raw = json.load(file_) 202 | obj = Article(raw) 203 | return obj 204 | except ValueError as e: 205 | raise JSONParseException(e.message) 206 | 207 | def _parse_raw(self): 208 | """Extract all attributes from raw dictionary.""" 209 | #Article Info 210 | self._title = self._raw.get('title') 211 | self._abstract = self._raw.get('abstract') 212 | self._abstract_source = self._raw.get('abstract_source') 213 | self._journal = self._raw.get('journal') 214 | self._subjects = self._raw.get('subjects', []) 215 | self._added_on = self._convert_to_datetime(self._raw.get('added_on')) 216 | self._published_on = self._convert_to_datetime( 217 | self._raw.get('published_on')) 218 | self._url = self._raw.get('url') 219 | self._is_open_access = self._raw.get('is_oa') 220 | 221 | self._scopus_subjects = self._raw.get('scopus_subjects', []) 222 | self._publisher_subjects = self._parse_publisher_subjects\ 223 | (self._raw.get('publisher_subjects',[])) 224 | 225 | self._taglines = self._raw.get('tq', []) 226 | 227 | #Various ID's 228 | self._doi = self._raw.get('doi') 229 | self._nlmid = self._raw.get('nlmid') 230 | self._pmid = self._raw.get('pmid') 231 | self._altmetric_id = str(self._raw.get('altmetric_id', "")) 232 | self._arxiv_id = self._raw.get('arxiv_id') 233 | self._ads_id = self._raw.get('ads_id') 234 | self._issns = self._raw.get('issns', []) 235 | 236 | #Altmetrics 237 | self._score = self._raw.get('score') 238 | self._score_history = self._parse_score_history( 239 | self._raw.get('history', {})) 240 | self._score_context = self._parse_score_context( 241 | self._raw.get('context', {})) 242 | self._last_updated = self._convert_to_datetime( 243 | self._raw.get('last_updated')) 244 | self._schema = self._raw.get('schema')#schema for what? 245 | self._cited_by_facebook_walls_count = self._raw.get( 246 | 'cited_by_fbwalls_count') 247 | self._cited_by_redits_count = self._raw.get('cited_by_rdts_count') 248 | self._cited_by_tweeters_count = self._raw.get( 249 | 'cited_by_tweeters_count') 250 | self._cited_by_google_plus_count = self._raw.get( 251 | 'cited_by_gplus_count') 252 | self._cited_by_msm_count = self._raw.get('cited_by_msm_count') 253 | self._cited_by_delicious_count = self._raw.get('cited_by_delicious_count') 254 | self._cited_by_qs_count = self._raw.get('cited_by_qs_count') 255 | self._cited_by_posts_count = self._raw.get('cited_by_posts_count') 256 | self._cited_by_accounts_count = ( 257 | self._raw.get('cited_by_accounts_count') 258 | or self._raw.get('by_accounts_count') 259 | ) 260 | 261 | self._cited_by_forums_count = self._raw.get('cited_by_forums_count') 262 | self._cited_by_peer_review_sites_count = self._raw.get( 263 | 'cited_by_peer_review_sites_count') 264 | self._cited_by_feeds_count = self._raw.get('cited_by_feeds_count') 265 | self._cited_by_videos_count = self._raw.get('cited_by_videos_count') 266 | 267 | 268 | self._cohorts = self._raw.get('cohorts', {}) 269 | 270 | self._readers_count = self._raw.get('readers_count') 271 | self._readers = self._raw.get('readers', {}) 272 | 273 | self._altmetric_details_url = self._raw.get('details_url',) 274 | 275 | self._altmetric_images = self._raw.get('images', {}) 276 | 277 | def _parse_score_history(self, history): 278 | """Make the score_history dictionary a little more readable.""" 279 | new_dictionary = {} 280 | if history: 281 | change = {'d':'day','m':'month','w':'week','y':'year'} 282 | for item in history: 283 | if item == 'at': 284 | date = "all time" 285 | else: 286 | if item[0] == '1': 287 | date = "past " + change[item[1]] 288 | else: 289 | date = "past " + item[0]+ " " + change[item[1]]+"s" 290 | new_dictionary[date] = history[item] 291 | return new_dictionary 292 | 293 | def _convert_to_datetime(self, unix_time): 294 | """Convert UNIX timestamp to a datetime object.""" 295 | if isinstance(unix_time, int): 296 | return datetime.datetime.fromtimestamp(unix_time) 297 | 298 | def _parse_publisher_subjects(self, subjects): 299 | """ 300 | Turns the publisher_subjects list of dictionaries into a list of 301 | subjects. 302 | """ 303 | new_subjects = [] 304 | if subjects: 305 | for item in subjects: 306 | new_subjects.append(item['name']) 307 | return new_subjects 308 | 309 | def _parse_score_context(self, context): 310 | """ 311 | Change the names of the dictionaries in context to make more sense. 312 | """ 313 | new_context = {} 314 | if context: 315 | new_context['all'] = context.get( 316 | 'all', {}) 317 | new_context['journal age'] = context.get( 318 | 'similar_age_journal_3m', {}) 319 | new_context['context age'] = context.get( 320 | 'similar_age_3m', {}) 321 | new_context['journal'] = context.get('journal', {}) 322 | return new_context 323 | 324 | def __repr__(self): 325 | return self.title[:12].encode('UTF-8') 326 | 327 | def __str__(self): 328 | string = u"" 329 | for item in self._raw: 330 | string += unicode(item) + u": " + unicode(self._raw[item]) + u'\n' 331 | return unicode(string).encode('UTF-8') 332 | 333 | #Basic info 334 | @property 335 | def raw_dictionary(self): 336 | return self._raw 337 | 338 | @property 339 | def title(self): 340 | return self._title 341 | 342 | @property 343 | def abstract(self): 344 | return self._abstract 345 | 346 | @property 347 | def abstract_source(self): 348 | return self._abstract_source 349 | 350 | @property 351 | def journal(self): 352 | return self._journal 353 | 354 | @property 355 | def subjects(self): 356 | """Return a list of realted subjects""" 357 | return self._subjects 358 | 359 | @property 360 | def scopus_subjects(self): 361 | """Return a list of Scopus subjects""" 362 | return self._scopus_subjects 363 | 364 | @property 365 | def publisher_subjects(self): 366 | """Return a list of related subjects.""" 367 | return self._publisher_subjects 368 | 369 | @property 370 | def added_on(self): 371 | return self._added_on 372 | 373 | @property 374 | def published_on(self): 375 | return self._published_on 376 | 377 | @property 378 | def url(self): 379 | return self._url 380 | 381 | @property 382 | def is_open_access(self): 383 | return self._is_open_access 384 | 385 | @property 386 | def taglines(self): 387 | """Return a list of related phrases""" 388 | return self._taglines 389 | 390 | #Various ID's 391 | @property 392 | def doi(self): 393 | return self._doi 394 | 395 | @property 396 | def nlmid(self): 397 | return self._nlmid 398 | 399 | @property 400 | def pmid(self): 401 | return self._pmid 402 | 403 | @property 404 | def altmetric_id(self): 405 | return self._altmetric_id 406 | 407 | @property 408 | def arxiv_id(self): 409 | return self._arxiv_id 410 | 411 | @property 412 | def ads_id(self): 413 | return self._ads_id 414 | 415 | @property 416 | def issns(self): 417 | """A list of issns.""" 418 | return self._issns 419 | 420 | #Altmetrics 421 | @property 422 | def score(self): 423 | return self._score 424 | 425 | @property 426 | def score_history(self): 427 | """ 428 | Return dictionry of Altmetric scores for time periods 429 | such as 'past day', 'past 3 days', 'past month', 'past year', 430 | and 'all time' looking only at that time period. 431 | """ 432 | return self._score_history 433 | 434 | @property 435 | def last_updated(self): 436 | """Return when the Altmetrics were last updated.""" 437 | return self._last_updated 438 | 439 | @property 440 | def score_context(self): 441 | """ 442 | Return a dictionary that allows you to compare an article's popularity 443 | to articles of a 'similar age'(published within 6 weeks on either 444 | side), articles in journals of a 'similar age', and other articles in 445 | the same 'journal'. 446 | """ 447 | return self._score_context 448 | 449 | 450 | #Cited by 451 | #Returns count of unique authors for posts cited on various medias. 452 | 453 | @property 454 | def cited_by_facebook_walls_count(self): 455 | """ 456 | Return number of posts made on public facebook walls mentioning chosen 457 | article. 458 | """ 459 | return self._cited_by_facebook_walls_count 460 | 461 | @property 462 | def cited_by_redits_count(self): 463 | return self._cited_by_redits_count 464 | @property 465 | def cited_by_tweeters_count(self): 466 | return self._cited_by_tweeters_count 467 | 468 | @property 469 | def cited_by_google_plus_count(self): 470 | return self._cited_by_google_plus_count 471 | 472 | @property 473 | def cited_by_msm_count(self): 474 | """Return number of citations from articles in science news outlets.""" 475 | return self._cited_by_msm_count 476 | 477 | @property 478 | def cited_by_delicious_count(self): 479 | return self._cited_by_delicious_count 480 | 481 | @property 482 | def cited_by_qs_count(self): 483 | """ 484 | Return number of citations from questions, answers or comments on Stack 485 | Exchange sites (inc. Biostar). 486 | """ 487 | return self._cited_by_qs_count 488 | 489 | @property 490 | def cited_by_posts_count(self): 491 | return self._cited_by_posts_count 492 | 493 | @property 494 | def cited_by_forums_count(self): 495 | return self._cited_by_forums_count 496 | 497 | @property 498 | def cited_by_feeds_count(self): 499 | return self._cited_by_feeds_count 500 | 501 | @property 502 | def cited_by_peer_review_sites_count(self): 503 | return self._cited_by_peer_review_sites_count 504 | 505 | @property 506 | def cited_by_accounts_count(self): 507 | return self._cited_by_accounts_count 508 | 509 | @property 510 | def cited_by_videos_count(self): 511 | return self._cited_by_videos_count 512 | 513 | 514 | @property 515 | def readers_count(self): 516 | return self._readers_count 517 | 518 | @property 519 | def readers(self): 520 | """ 521 | Return a dictionary that contains information about the numbers of 522 | readers on various reference manager websites. The website name is the 523 | key and the number of readers is the value. 524 | Ex. {'mendeley': 11, , 'citeulike': 0, 'connotea' : 4} 525 | """ 526 | return self._readers 527 | 528 | @property 529 | def cohorts(self): 530 | """ 531 | Return a dictionary with the number of people mentioning this article 532 | who are members of the public (pub), practitioners (doc), research 533 | scientists (sci) or science communicators (com) 534 | (This is an experimental Altmetric feature). 535 | """ 536 | return self._cohorts 537 | 538 | @property 539 | def schema(self): 540 | return self._schema 541 | 542 | @property 543 | def altmetric_details_url(self): 544 | return self._altmetric_details_url 545 | 546 | @property 547 | def altmetric_images(self): 548 | """ 549 | Return a dictionary of the Altmetric score image in 550 | 'small', 'medium', and 'large'. 551 | """ 552 | return self._altmetric_images -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='PyAltmetric', 5 | version='0.1.0', 6 | author='Lauren Revere', 7 | author_email='lauren.revere@gmail.com', 8 | packages=['pyaltmetric'], 9 | license='LICENSE.txt', 10 | description='A python wrapper for the Altmetric API.', 11 | long_description=open('README.txt').read(), 12 | install_requires=['requests>=2.20.0'], 13 | ) 14 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CenterForOpenScience/PyAltmetric/24ff6d23e16d8b036f58e507c2b8fcb4d483e44b/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CenterForOpenScience/PyAltmetric/24ff6d23e16d8b036f58e507c2b8fcb4d483e44b/tests/fixtures/.DS_Store -------------------------------------------------------------------------------- /tests/fixtures/average.json: -------------------------------------------------------------------------------- 1 | {"title":"Climate change will hit genetic diversity","doi":"10.1038/news.2011.490","nlmid":"0410462","tq":["Researchers find that when climate change threatens the existence of species, it threatens genetic diversity, too","Climate change and genetic diversity (NatureNews)","#Climate change will hit genetic diversity as well as biodiversity #conservation #sayyesaus","Climate change will hit genetic diversity: Probable loss of 'cryptic' variation a challenge for conservationists."],"altmetric_jid":"4f6fa50a3cf058f610003160","issns":["0028-0836","1476-4687"],"journal":"Nature News","cohorts":{"pub":131,"sci":34,"doc":3,"com":8},"altmetric_id":241939,"schema":"1.5.4","is_oa":false,"cited_by_fbwalls_count":5,"cited_by_feeds_count":3,"cited_by_msm_count":1,"cited_by_posts_count":196,"cited_by_rdts_count":1,"cited_by_tweeters_count":176,"cited_by_accounts_count":186,"context":{"all":{"count":2162451,"mean":4.7684323457198,"rank":4979,"pct":99,"higher_than":2157568},"journal":{"count":25073,"mean":38.781049218251,"rank":1332,"pct":94,"higher_than":23741},"similar_age_3m":{"count":54719,"mean":4.231628860704,"rank":98,"pct":99,"higher_than":54621},"similar_age_journal_3m":{"count":869,"mean":41.477870967742,"rank":41,"pct":95,"higher_than":828}},"score":164.966,"history":{"at":164.966,"1d":0,"2d":0,"3d":0,"4d":0,"5d":0,"6d":0,"1w":0,"1m":0,"3m":0,"6m":0,"1y":0},"url":"http://dx.doi.org/10.1038/news.2011.490","added_on":1313946657,"published_on":1313881200,"subjects":["science"],"scopus_subjects":["General"],"last_updated":1334237127,"readers_count":1,"readers":{"mendeley":1,"connotea":0,"citeulike":0},"images":{"small":"https://altmetric-badges.a.ssl.fastly.net/?size=64&score=165&types=mbttttrf","medium":"https://altmetric-badges.a.ssl.fastly.net/?size=100&score=165&types=mbttttrf","large":"https://altmetric-badges.a.ssl.fastly.net/?size=180&score=165&types=mbttttrf"},"details_url":"http://www.altmetric.com/details.php?citation_id=241939"} -------------------------------------------------------------------------------- /tests/fixtures/empty.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tests/fixtures/full.json: -------------------------------------------------------------------------------- 1 | {"title":"Climate change will hit genetic diversity","doi":"10.1038/news.2011.490","nlmid":"0410462","tq":["Researchers find that when climate change threatens the existence of species, it threatens genetic diversity, too","Climate change and genetic diversity (NatureNews)","#Climate change will hit genetic diversity as well as biodiversity #conservation #sayyesaus","Climate change will hit genetic diversity: Probable loss of 'cryptic' variation a challenge for conservationists."],"altmetric_jid":"4f6fa50a3cf058f610003160","issns":["0028-0836","1476-4687"],"journal":"Nature News","cohorts":{"pub":131,"sci":34,"doc":3,"com":8},"altmetric_id":241939,"schema":"1.5.4","is_oa":true,"cited_by_fbwalls_count":5,"cited_by_feeds_count":3,"cited_by_msm_count":1,"cited_by_posts_count":196,"cited_by_rdts_count":1,"cited_by_tweeters_count":176,"cited_by_accounts_count":186,"context":{"all":{"count":2162451,"mean":4.7684323457198,"rank":4979,"pct":99,"higher_than":2157568},"journal":{"count":25073,"mean":38.781049218251,"rank":1332,"pct":94,"higher_than":23741},"similar_age_3m":{"count":54719,"mean":4.231628860704,"rank":98,"pct":99,"higher_than":54621},"similar_age_journal_3m":{"count":869,"mean":41.477870967742,"rank":41,"pct":95,"higher_than":828}},"score":164.966,"history":{"at":164.966,"1d":0,"2d":0,"3d":0,"4d":0,"5d":0,"6d":0,"1w":0,"1m":0,"3m":0,"6m":0,"1y":0},"url":"http://dx.doi.org/10.1038/news.2011.490","added_on":1313946657,"published_on":1313881200,"subjects":["science"],"scopus_subjects":["General"],"last_updated":1334237127,"readers_count":1,"readers":{"mendeley":1,"connotea":0,"citeulike":0},"images":{"small":"https://altmetric-badges.a.ssl.fastly.net/?size=64&score=165&types=mbttttrf","medium":"https://altmetric-badges.a.ssl.fastly.net/?size=100&score=165&types=mbttttrf","large":"https://altmetric-badges.a.ssl.fastly.net/?size=180&score=165&types=mbttttrf"},"details_url":"http://www.altmetric.com/details.php?citation_id=241939","abstract":"To determine whether the author's 20.9 lb (9.5 kg) carbon frame bicycle reduced commuting time compared with his 29.75 lb (13.5 kg) steel frame bicycle.","abstract_source":"pubmed", "schema":"1.5.4","publisher_subjects":[{"name":"Public Health And Health Services","scheme":"era"}],"cited_by_forums_count":6,"cited_by_gplus_count":3, "pmid":"21148220","arxiv_id":"1108.2455","ads_id":"2011arxiv1108.2455l","cited_by_videos_count":1, "cited_by_peer_review_sites_count":1, "cited_by_delicious_count":1, "cited_by_qs_count":1} -------------------------------------------------------------------------------- /tests/fixtures/wrong.txt: -------------------------------------------------------------------------------- 1 | This is a test file. 2 | Articles cannot be created from text files. -------------------------------------------------------------------------------- /tests/test_article.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from pyaltmetric import * 3 | import json 4 | import datetime 5 | 6 | class TestArticle(TestCase): 7 | def setUp(self): 8 | with open('tests/fixtures/full.json') as raw_json: 9 | raw_dict = json.load(raw_json) 10 | self.art = Article(raw_dict) 11 | 12 | def test__parse_raw_empty(self): 13 | #An empty dictionary would never actually be passed to this 14 | self.art._raw = {} 15 | self.art._parse_raw() 16 | 17 | self.assertFalse(self.art.title) 18 | self.assertFalse(self.art.abstract) 19 | self.assertFalse(self.art.abstract_source) 20 | self.assertFalse(self.art.journal) 21 | self.assertFalse(self.art.subjects) 22 | self.assertIsInstance(self.art.subjects, list) 23 | self.assertFalse(self.art.added_on) 24 | self.assertFalse(self.art.published_on) 25 | self.assertFalse(self.art.url) 26 | self.assertFalse(self.art.is_open_access) 27 | self.assertFalse(self.art.scopus_subjects) 28 | self.assertIsInstance(self.art.scopus_subjects, list) 29 | self.assertFalse(self.art.publisher_subjects) 30 | self.assertIsInstance(self.art.publisher_subjects, list) 31 | self.assertFalse(self.art.taglines) 32 | self.assertIsInstance(self.art.taglines, list) 33 | 34 | self.assertFalse(self.art.doi) 35 | self.assertFalse(self.art.nlmid) 36 | self.assertFalse(self.art.pmid) 37 | self.assertFalse(self.art.altmetric_id) 38 | self.assertFalse(self.art.arxiv_id) 39 | self.assertFalse(self.art.ads_id) 40 | self.assertFalse(self.art.issns) 41 | self.assertIsInstance(self.art.issns, list) 42 | 43 | self.assertFalse(self.art.score) 44 | self.assertFalse(self.art.score_history) 45 | self.assertIsInstance(self.art.score_history, dict) 46 | self.assertFalse(self.art.score_context) 47 | self.assertIsInstance(self.art.score_context, dict) 48 | self.assertFalse(self.art.last_updated) 49 | self.assertFalse(self.art.schema) 50 | self.assertFalse(self.art.cited_by_facebook_walls_count) 51 | self.assertFalse(self.art.cited_by_redits_count) 52 | self.assertFalse(self.art.cited_by_tweeters_count) 53 | self.assertFalse(self.art.cited_by_google_plus_count) 54 | self.assertFalse(self.art.cited_by_msm_count) 55 | self.assertFalse(self.art.cited_by_delicious_count) 56 | self.assertFalse(self.art.cited_by_qs_count) 57 | self.assertFalse(self.art.cited_by_posts_count) 58 | self.assertFalse(self.art.cited_by_accounts_count) 59 | self.assertFalse(self.art.cited_by_forums_count) 60 | self.assertFalse(self.art.cited_by_peer_review_sites_count) 61 | self.assertFalse(self.art.cited_by_feeds_count) 62 | self.assertFalse(self.art.cited_by_videos_count) 63 | self.assertFalse(self.art.cohorts) 64 | self.assertFalse(self.art.readers_count) 65 | self.assertFalse(self.art.readers) 66 | self.assertIsInstance(self.art.readers, dict) 67 | self.assertFalse(self.art.altmetric_details_url) 68 | self.assertFalse(self.art.altmetric_images) 69 | self.assertIsInstance(self.art.altmetric_images, dict) 70 | 71 | 72 | def test__parse_raw_average(self): 73 | with open('tests/fixtures/average.json') as raw_json: 74 | raw_dict = json.load(raw_json) 75 | self.art._raw = raw_dict 76 | self.art._parse_raw() 77 | 78 | self.assertTrue(self.art.title) 79 | self.assertFalse(self.art.abstract) 80 | self.assertFalse(self.art.abstract_source) 81 | self.assertTrue(self.art.journal) 82 | self.assertTrue(self.art.subjects) 83 | self.assertIsInstance(self.art.subjects, list) 84 | 85 | self.assertTrue(self.art.added_on) 86 | self.assertIsInstance(self.art.added_on, datetime.datetime) 87 | 88 | self.assertTrue(self.art.published_on) 89 | self.assertIsInstance(self.art.published_on, datetime.datetime) 90 | 91 | self.assertTrue(self.art.url) 92 | self.assertFalse(self.art.is_open_access) 93 | self.assertIsInstance(self.art.is_open_access, bool) 94 | 95 | self.assertTrue(self.art.scopus_subjects) 96 | self.assertIsInstance(self.art.scopus_subjects, list) 97 | 98 | self.assertFalse(self.art.publisher_subjects) 99 | self.assertTrue(self.art.taglines) 100 | self.assertIsInstance(self.art.taglines, list) 101 | 102 | self.assertTrue(self.art.doi) 103 | self.assertTrue(self.art.nlmid) 104 | self.assertFalse(self.art.pmid) 105 | self.assertTrue(self.art.altmetric_id) 106 | self.assertFalse(self.art.arxiv_id) 107 | self.assertFalse(self.art.ads_id) 108 | self.assertTrue(self.art.issns) 109 | self.assertIsInstance(self.art.taglines, list) 110 | 111 | self.assertTrue(self.art.score) 112 | self.assertIsInstance(self.art.score, float) 113 | 114 | self.assertTrue(self.art.score_history) 115 | self.assertIsInstance(self.art.score_history, dict) 116 | 117 | self.assertTrue(self.art.score_context) 118 | self.assertIsInstance(self.art.score_context, dict) 119 | 120 | self.assertTrue(self.art.last_updated) 121 | self.assertIsInstance(self.art.last_updated, datetime.datetime) 122 | 123 | self.assertTrue(self.art.schema) 124 | self.assertTrue(self.art.cited_by_facebook_walls_count) 125 | self.assertIsInstance(self.art.cited_by_facebook_walls_count, int) 126 | 127 | self.assertTrue(self.art.cited_by_redits_count) 128 | self.assertIsInstance(self.art.cited_by_redits_count, int) 129 | 130 | self.assertTrue(self.art.cited_by_tweeters_count) 131 | self.assertIsInstance(self.art.cited_by_tweeters_count, int) 132 | 133 | self.assertFalse(self.art.cited_by_google_plus_count) 134 | 135 | self.assertTrue(self.art.cited_by_msm_count) 136 | self.assertIsInstance(self.art.cited_by_msm_count, int) 137 | 138 | self.assertFalse(self.art.cited_by_delicious_count) 139 | self.assertFalse(self.art.cited_by_qs_count) 140 | 141 | self.assertTrue(self.art.cited_by_posts_count) 142 | self.assertIsInstance(self.art.cited_by_posts_count, int) 143 | 144 | self.assertTrue(self.art.cited_by_accounts_count) 145 | self.assertIsInstance(self.art.cited_by_accounts_count, int) 146 | 147 | self.assertFalse(self.art.cited_by_forums_count) 148 | 149 | self.assertFalse(self.art.cited_by_peer_review_sites_count) 150 | 151 | self.assertTrue(self.art.cited_by_feeds_count) 152 | self.assertIsInstance(self.art.cited_by_feeds_count, int) 153 | 154 | self.assertFalse(self.art.cited_by_videos_count) 155 | 156 | self.assertTrue(self.art.cohorts) 157 | self.assertIsInstance(self.art.cohorts, dict) 158 | 159 | self.assertTrue(self.art.readers_count) 160 | self.assertIsInstance(self.art.readers_count, int) 161 | 162 | self.assertTrue(self.art.readers) 163 | self.assertIsInstance(self.art.readers, dict) 164 | 165 | self.assertTrue(self.art.altmetric_details_url) 166 | self.assertTrue(self.art.altmetric_images) 167 | self.assertIsInstance(self.art.altmetric_images, dict) 168 | 169 | def test__parse_raw_full(self): 170 | self.assertTrue(self.art.title) 171 | self.assertTrue(self.art.abstract) 172 | self.assertTrue(self.art.abstract_source) 173 | self.assertTrue(self.art.journal) 174 | self.assertTrue(self.art.subjects) 175 | self.assertTrue(self.art.added_on) 176 | self.assertTrue(self.art.published_on) 177 | self.assertTrue(self.art.url) 178 | self.assertTrue(self.art.is_open_access) 179 | self.assertTrue(self.art.scopus_subjects) 180 | self.assertTrue(self.art.publisher_subjects) 181 | self.assertTrue(self.art.taglines) 182 | 183 | self.assertEquals("10.1038/news.2011.490", self.art.doi) 184 | self.assertEquals("0410462", self.art.nlmid) 185 | self.assertEquals("21148220", self.art.pmid) 186 | self.assertEquals("241939", self.art.altmetric_id) 187 | self.assertEquals("1108.2455", self.art.arxiv_id) 188 | self.assertEquals("2011arxiv1108.2455l", self.art.ads_id) 189 | 190 | self.assertTrue(self.art.issns) 191 | self.assertTrue(self.art.score) 192 | self.assertTrue(self.art.score_history) 193 | self.assertTrue(self.art.score_context) 194 | self.assertTrue(self.art.last_updated) 195 | self.assertTrue(self.art.schema) 196 | 197 | self.assertEquals(5, self.art.cited_by_facebook_walls_count) 198 | self.assertEquals(1, self.art.cited_by_redits_count) 199 | self.assertEquals(176, self.art.cited_by_tweeters_count) 200 | self.assertEquals(3, self.art.cited_by_google_plus_count) 201 | self.assertEquals(1, self.art.cited_by_msm_count) 202 | self.assertEquals(1, self.art.cited_by_delicious_count) 203 | self.assertEquals(1, self.art.cited_by_qs_count) 204 | self.assertEquals(196, self.art.cited_by_posts_count) 205 | self.assertEquals(186, self.art.cited_by_accounts_count) 206 | self.assertEquals(6, self.art.cited_by_forums_count) 207 | self.assertEquals(1, self.art.cited_by_peer_review_sites_count) 208 | self.assertEquals(3, self.art.cited_by_feeds_count) 209 | self.assertEquals(1, self.art.cited_by_videos_count) 210 | 211 | 212 | self.assertTrue(self.art.cohorts) 213 | self.assertTrue(self.art.readers_count) 214 | self.assertTrue(self.art.readers) 215 | self.assertTrue(self.art.altmetric_details_url) 216 | self.assertTrue(self.art.altmetric_images) 217 | 218 | def test_from_json_file_empty(self): 219 | self.assertRaises(AttributeError, Article.from_json_file,'tests/fixtures/empty.json') 220 | 221 | def test_from_json_file_wrong(self): 222 | self.assertRaises(JSONParseException, Article.from_json_file,'tests/fixtures/wrong.txt') 223 | 224 | def test_from_json_file_correct(self): 225 | a = Article.from_json_file('tests/fixtures/full.json') 226 | self.assertIsInstance(a, Article) 227 | 228 | def test_from_file_empty(self): 229 | with open('tests/fixtures/empty.json') as raw_json: 230 | self.assertRaises(AttributeError, Article.from_json,raw_json) 231 | 232 | def test_from_file_wrong(self): 233 | with open('tests/fixtures/wrong.txt') as raw_json: 234 | self.assertRaises(JSONParseException, Article.from_json, raw_json) 235 | 236 | def test_from_file_correct(self): 237 | with open('tests/fixtures/full.json') as raw_json: 238 | a = Article.from_json(raw_json) 239 | self.assertIsInstance(a, Article) 240 | 241 | def test__parse_score_history_average(self): 242 | old_history = self.art.raw_dictionary.get("history") 243 | correct_history = { 244 | "all time":164.966, "past day":0,"past 2 days":0,"past 3 days":0, 245 | "past 4 days":0,"past 5 days":0,"past 6 days":0,"past week":0, 246 | "past month":0,"past 3 months":0,"past 6 months":0,"past year":0 247 | } 248 | new_history = self.art._parse_score_history(old_history) 249 | for key in correct_history: 250 | self.assertEquals(correct_history[key], new_history.get(key)) 251 | 252 | def test__parse_score_history_empty(self): 253 | self.assertEqual(self.art._parse_score_history({}), {}) 254 | 255 | def test__convert_to_datetime_average(self): 256 | time = self.art._convert_to_datetime(0) 257 | self.assertIsInstance(time, datetime.datetime) 258 | 259 | def test__convert_to_datetime_empty(self): 260 | time = self.art._convert_to_datetime(None) 261 | self.assertFalse(time) 262 | 263 | def test__parse_publisher_subjects_average(self): 264 | old_subjects = self.art.raw_dictionary.get("publisher_subjects") 265 | correct_subjects = ["Public Health And Health Services"] 266 | new_subjects = self.art._parse_publisher_subjects(old_subjects) 267 | self.assertEquals(correct_subjects, new_subjects) 268 | 269 | def test__parse_publisher_subjects_empty(self): 270 | self.assertEquals(self.art._parse_publisher_subjects({}),[]) 271 | 272 | def test__parse_score_context_average(self): 273 | old_context = self.art.raw_dictionary.get("context") 274 | correct_context = { 275 | "all": 276 | {"count":2162451,"mean":4.7684323457198,"rank":4979,"pct":99, 277 | "higher_than":2157568}, 278 | "journal": 279 | {"count":25073,"mean":38.781049218251,"rank":1332,"pct":94, 280 | "higher_than":23741}, 281 | "context age": 282 | {"count":54719,"mean":4.231628860704,"rank":98,"pct":99, 283 | "higher_than":54621}, 284 | "journal age": 285 | {"count":869,"mean":41.477870967742,"rank":41,"pct":95, 286 | "higher_than":828} 287 | } 288 | 289 | new_context = self.art._parse_score_context(old_context) 290 | 291 | for key in correct_context: 292 | self.assertEquals(correct_context[key], new_context.get(key)) 293 | 294 | def test__parse_score_context_empty(self): 295 | self.assertEquals(self.art._parse_score_context({}),{}) --------------------------------------------------------------------------------