pyalex 0.15.1__py3-none-any.whl → 0.17__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyalex/__init__.py CHANGED
@@ -18,6 +18,7 @@ from pyalex.api import Funders
18
18
  from pyalex.api import Institution
19
19
  from pyalex.api import Institutions
20
20
  from pyalex.api import Journals
21
+ from pyalex.api import OpenAlexResponseList
21
22
  from pyalex.api import People
22
23
  from pyalex.api import Publisher
23
24
  from pyalex.api import Publishers
@@ -61,4 +62,5 @@ __all__ = [
61
62
  "autocomplete",
62
63
  "config",
63
64
  "invert_abstract",
65
+ "OpenAlexResponseList",
64
66
  ]
pyalex/_version.py CHANGED
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.15.1'
16
- __version_tuple__ = version_tuple = (0, 15, 1)
20
+ __version__ = version = '0.17'
21
+ __version_tuple__ = version_tuple = (0, 17)
pyalex/api.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
  import warnings
3
3
  from urllib.parse import quote_plus
4
+ from urllib.parse import urlunparse
4
5
 
5
6
  import requests
6
7
  from requests.auth import AuthBase
@@ -13,6 +14,26 @@ except ImportError:
13
14
 
14
15
 
15
16
  class AlexConfig(dict):
17
+ """Configuration class for OpenAlex API.
18
+
19
+ Attributes
20
+ ----------
21
+ email : str
22
+ Email address for API requests.
23
+ api_key : str
24
+ API key for authentication.
25
+ user_agent : str
26
+ User agent string for API requests.
27
+ openalex_url : str
28
+ Base URL for OpenAlex API.
29
+ max_retries : int
30
+ Maximum number of retries for API requests.
31
+ retry_backoff_factor : float
32
+ Backoff factor for retries.
33
+ retry_http_codes : list
34
+ List of HTTP status codes to retry on.
35
+ """
36
+
16
37
  def __getattr__(self, key):
17
38
  return super().__getitem__(key)
18
39
 
@@ -31,40 +52,129 @@ config = AlexConfig(
31
52
  )
32
53
 
33
54
 
55
+ class or_(dict):
56
+ """Logical OR expression class."""
57
+
58
+ pass
59
+
60
+
61
+ class _LogicalExpression:
62
+ """Base class for logical expressions.
63
+
64
+ Attributes
65
+ ----------
66
+ token : str
67
+ Token representing the logical operation.
68
+ value : any
69
+ Value to be used in the logical expression.
70
+ """
71
+
72
+ token = None
73
+
74
+ def __init__(self, value):
75
+ self.value = value
76
+
77
+ def __str__(self) -> str:
78
+ return f"{self.token}{self.value}"
79
+
80
+
81
+ class not_(_LogicalExpression):
82
+ """Logical NOT expression class."""
83
+
84
+ token = "!"
85
+
86
+
87
+ class gt_(_LogicalExpression):
88
+ """Logical greater than expression class."""
89
+
90
+ token = ">"
91
+
92
+
93
+ class lt_(_LogicalExpression):
94
+ """Logical less than expression class."""
95
+
96
+ token = "<"
97
+
98
+
34
99
  def _quote_oa_value(v):
35
100
  """Prepare a value for the OpenAlex API.
36
101
 
37
102
  Applies URL encoding to strings and converts booleans to lowercase strings.
103
+
104
+ Parameters
105
+ ----------
106
+ v : any
107
+ Value to be prepared.
108
+
109
+ Returns
110
+ -------
111
+ any
112
+ Prepared value.
38
113
  """
39
114
 
40
115
  # workaround for bug https://groups.google.com/u/1/g/openalex-users/c/t46RWnzZaXc
41
116
  if isinstance(v, bool):
42
117
  return str(v).lower()
43
118
 
119
+ if isinstance(v, _LogicalExpression) and isinstance(v.value, str):
120
+ v.value = quote_plus(v.value)
121
+ return v
122
+
44
123
  if isinstance(v, str):
45
124
  return quote_plus(v)
46
125
 
47
126
  return v
48
127
 
49
128
 
50
- def _flatten_kv(d, prefix=""):
129
+ def _flatten_kv(d, prefix=None, logical="+"):
130
+ """Flatten a dictionary into a key-value string for the OpenAlex API.
131
+
132
+ Parameters
133
+ ----------
134
+ d : dict
135
+ Dictionary to be flattened.
136
+ prefix : str, optional
137
+ Prefix for the keys.
138
+ logical : str, optional
139
+ Logical operator to join values.
140
+
141
+ Returns
142
+ -------
143
+ str
144
+ Flattened key-value string.
145
+ """
146
+ if prefix is None and not isinstance(d, dict):
147
+ raise ValueError("prefix should be set if d is not a dict")
148
+
51
149
  if isinstance(d, dict):
150
+ logical_subd = "|" if isinstance(d, or_) else logical
151
+
52
152
  t = []
53
153
  for k, v in d.items():
54
- if isinstance(v, list):
55
- t.extend([f"{prefix}.{k}:{_quote_oa_value(i)}" for i in v])
56
- else:
57
- new_prefix = f"{prefix}.{k}" if prefix else f"{k}"
58
- x = _flatten_kv(v, prefix=new_prefix)
59
- t.append(x)
154
+ x = _flatten_kv(
155
+ v, prefix=f"{prefix}.{k}" if prefix else f"{k}", logical=logical_subd
156
+ )
157
+ t.append(x)
60
158
 
61
159
  return ",".join(t)
160
+ elif isinstance(d, list):
161
+ list_str = logical.join([f"{_quote_oa_value(i)}" for i in d])
162
+ return f"{prefix}:{list_str}"
62
163
  else:
63
164
  return f"{prefix}:{_quote_oa_value(d)}"
64
165
 
65
166
 
66
167
  def _params_merge(params, add_params):
67
- for k, _v in add_params.items():
168
+ """Merge additional parameters into existing parameters.
169
+
170
+ Parameters
171
+ ----------
172
+ params : dict
173
+ Existing parameters.
174
+ add_params : dict
175
+ Additional parameters to be merged.
176
+ """
177
+ for k in add_params.keys():
68
178
  if (
69
179
  k in params
70
180
  and isinstance(params[k], dict)
@@ -92,6 +202,13 @@ def _params_merge(params, add_params):
92
202
 
93
203
 
94
204
  def _get_requests_session():
205
+ """Create a Requests session with automatic retry.
206
+
207
+ Returns
208
+ -------
209
+ requests.Session
210
+ Requests session with retry configuration.
211
+ """
95
212
  # create an Requests Session with automatic retry:
96
213
  requests_session = requests.Session()
97
214
  retries = Retry(
@@ -108,20 +225,108 @@ def _get_requests_session():
108
225
 
109
226
 
110
227
  def invert_abstract(inv_index):
228
+ """Invert OpenAlex abstract index.
229
+
230
+ Parameters
231
+ ----------
232
+ inv_index : dict
233
+ Inverted index of the abstract.
234
+
235
+ Returns
236
+ -------
237
+ str
238
+ Inverted abstract.
239
+ """
111
240
  if inv_index is not None:
112
241
  l_inv = [(w, p) for w, pos in inv_index.items() for p in pos]
113
242
  return " ".join(map(lambda x: x[0], sorted(l_inv, key=lambda x: x[1])))
114
243
 
115
244
 
245
+ def _wrap_values_nested_dict(d, func):
246
+ """Apply a function to all values in a nested dictionary.
247
+
248
+ Parameters
249
+ ----------
250
+ d : dict
251
+ Nested dictionary.
252
+ func : function
253
+ Function to apply to the values.
254
+
255
+ Returns
256
+ -------
257
+ dict
258
+ Dictionary with the function applied to the values.
259
+ """
260
+ for k, v in d.items():
261
+ if isinstance(v, dict):
262
+ d[k] = _wrap_values_nested_dict(v, func)
263
+ elif isinstance(v, list):
264
+ d[k] = [func(i) for i in v]
265
+ else:
266
+ d[k] = func(v)
267
+
268
+ return d
269
+
270
+
116
271
  class QueryError(ValueError):
272
+ """Exception raised for errors in the query."""
273
+
117
274
  pass
118
275
 
119
276
 
120
277
  class OpenAlexEntity(dict):
278
+ """Base class for OpenAlex entities."""
279
+
121
280
  pass
122
281
 
123
282
 
283
+ class OpenAlexResponseList(list):
284
+ """A list of OpenAlexEntity objects with metadata.
285
+
286
+ Attributes:
287
+ meta: a dictionary with metadata about the results
288
+ resource_class: the class to use for each entity in the results
289
+
290
+ Arguments:
291
+ results: a list of OpenAlexEntity objects
292
+ meta: a dictionary with metadata about the results
293
+ resource_class: the class to use for each entity in the results
294
+
295
+ Returns:
296
+ a OpenAlexResponseList object
297
+ """
298
+
299
+ def __init__(self, results, meta=None, resource_class=OpenAlexEntity):
300
+ self.resource_class = resource_class
301
+ self.meta = meta
302
+
303
+ super().__init__([resource_class(ent) for ent in results])
304
+
305
+
124
306
  class Paginator:
307
+ """Paginator for OpenAlex API results.
308
+
309
+ Attributes
310
+ ----------
311
+ VALUE_CURSOR_START : str
312
+ Starting value for cursor pagination.
313
+ VALUE_NUMBER_START : int
314
+ Starting value for page pagination.
315
+
316
+ Parameters
317
+ ----------
318
+ endpoint_class : class
319
+ Class of the endpoint to paginate.
320
+ method : str, optional
321
+ Pagination method ('cursor' or 'page').
322
+ value : any, optional
323
+ Starting value for pagination.
324
+ per_page : int, optional
325
+ Number of results per page.
326
+ n_max : int, optional
327
+ Maximum number of results.
328
+ """
329
+
125
330
  VALUE_CURSOR_START = "*"
126
331
  VALUE_NUMBER_START = 1
127
332
 
@@ -157,32 +362,31 @@ class Paginator:
157
362
  else:
158
363
  raise ValueError()
159
364
 
160
- results, meta = self.endpoint_class.get(
161
- return_meta=True, per_page=self.per_page, **pagination_params
162
- )
365
+ r = self.endpoint_class.get(per_page=self.per_page, **pagination_params)
163
366
 
164
367
  if self.method == "cursor":
165
- self._next_value = meta["next_cursor"]
368
+ self._next_value = r.meta["next_cursor"]
166
369
 
167
370
  if self.method == "page":
168
- if len(results) > 0:
169
- self._next_value = meta["page"] + 1
371
+ if len(r) > 0:
372
+ self._next_value = r.meta["page"] + 1
170
373
  else:
171
374
  self._next_value = None
172
375
 
173
- self.n = self.n + len(results)
376
+ self.n = self.n + len(r)
174
377
 
175
- return results
378
+ return r
176
379
 
177
380
 
178
381
  class OpenAlexAuth(AuthBase):
179
- """OpenAlex auth class based on requests auth
180
-
181
- Includes the email, api_key and user-agent headers.
382
+ """OpenAlex auth class based on requests auth.
182
383
 
183
- arguments:
184
- config: an AlexConfig object
384
+ Includes the email, api_key, and user-agent headers.
185
385
 
386
+ Parameters
387
+ ----------
388
+ config : AlexConfig
389
+ Configuration object for OpenAlex API.
186
390
  """
187
391
 
188
392
  def __init__(self, config):
@@ -202,74 +406,102 @@ class OpenAlexAuth(AuthBase):
202
406
 
203
407
 
204
408
  class BaseOpenAlex:
205
- """Base class for OpenAlex objects."""
409
+ """Base class for OpenAlex objects.
410
+
411
+ Parameters
412
+ ----------
413
+ params : dict, optional
414
+ Parameters for the API request.
415
+ """
206
416
 
207
417
  def __init__(self, params=None):
208
418
  self.params = params
209
419
 
210
- def _get_multi_items(self, record_list):
211
- return self.filter(openalex_id="|".join(record_list)).get()
212
-
213
- def _full_collection_name(self):
214
- if self.params is not None and "q" in self.params.keys():
215
- return (
216
- f"{config.openalex_url}/autocomplete/{self.__class__.__name__.lower()}"
217
- )
218
- else:
219
- return f"{config.openalex_url}/{self.__class__.__name__.lower()}"
220
-
221
420
  def __getattr__(self, key):
222
421
  if key == "groupby":
223
422
  raise AttributeError(
224
- "Object has no attribute 'groupby'. " "Did you mean 'group_by'?"
423
+ "Object has no attribute 'groupby'. Did you mean 'group_by'?"
225
424
  )
226
425
 
227
426
  if key == "filter_search":
228
427
  raise AttributeError(
229
- "Object has no attribute 'filter_search'. "
230
- "Did you mean 'search_filter'?"
428
+ "Object has no attribute 'filter_search'. Did you mean 'search_filter'?"
231
429
  )
232
430
 
233
431
  return getattr(self, key)
234
432
 
235
433
  def __getitem__(self, record_id):
236
434
  if isinstance(record_id, list):
237
- return self._get_multi_items(record_id)
435
+ if len(record_id) > 100:
436
+ raise ValueError("OpenAlex does not support more than 100 ids")
238
437
 
239
- return self._get_from_url(
240
- f"{self._full_collection_name()}/{record_id}", return_meta=False
241
- )
438
+ return self.filter_or(openalex_id=record_id).get(per_page=len(record_id))
439
+ elif isinstance(record_id, str):
440
+ self.params = record_id
441
+ return self._get_from_url(self.url)
442
+ else:
443
+ raise ValueError("record_id should be a string or a list of strings")
444
+
445
+ def _url_query(self):
446
+ if isinstance(self.params, list):
447
+ return self.filter_or(openalex_id=self.params)
448
+ elif isinstance(self.params, dict):
449
+ l_params = []
450
+ for k, v in self.params.items():
451
+ if v is None:
452
+ pass
453
+ elif isinstance(v, list):
454
+ l_params.append(
455
+ "{}={}".format(k, ",".join(map(_quote_oa_value, v)))
456
+ )
457
+ elif k in ["filter", "sort"]:
458
+ l_params.append(f"{k}={_flatten_kv(v)}")
459
+ else:
460
+ l_params.append(f"{k}={_quote_oa_value(v)}")
461
+
462
+ if l_params:
463
+ return "&".join(l_params)
464
+
465
+ else:
466
+ return ""
242
467
 
243
468
  @property
244
469
  def url(self):
245
- if not self.params:
246
- return self._full_collection_name()
247
-
248
- l_params = []
249
- for k, v in self.params.items():
250
- if v is None:
251
- pass
252
- elif isinstance(v, list):
253
- l_params.append("{}={}".format(k, ",".join(map(_quote_oa_value, v))))
254
- elif k in ["filter", "sort"]:
255
- l_params.append(f"{k}={_flatten_kv(v)}")
256
- else:
257
- l_params.append(f"{k}={_quote_oa_value(v)}")
470
+ """Return the URL for the API request.
471
+
472
+ The URL doens't include the identification, authentication,
473
+ and pagination parameters.
258
474
 
259
- if l_params:
260
- return "{}?{}".format(self._full_collection_name(), "&".join(l_params))
261
475
 
262
- return self._full_collection_name()
476
+ Returns
477
+ -------
478
+ str
479
+ URL for the API request.
480
+ """
481
+ base_path = self.__class__.__name__.lower()
482
+
483
+ if isinstance(self.params, str):
484
+ path = f"{base_path}/{_quote_oa_value(self.params)}"
485
+ query = ""
486
+ else:
487
+ path = base_path
488
+ query = self._url_query()
489
+
490
+ return urlunparse(("https", "api.openalex.org", path, "", query, ""))
263
491
 
264
492
  def count(self):
265
- _, m = self.get(return_meta=True, per_page=1)
493
+ """Get the count of results.
266
494
 
267
- return m["count"]
495
+ Returns
496
+ -------
497
+ int
498
+ Count of results.
499
+ """
500
+ return self.get(per_page=1).meta["count"]
268
501
 
269
- def _get_from_url(self, url, return_meta=False):
502
+ def _get_from_url(self, url):
270
503
  res = _get_requests_session().get(url, auth=OpenAlexAuth(config))
271
504
 
272
- # handle query errors
273
505
  if res.status_code == 403:
274
506
  if (
275
507
  isinstance(res.json()["error"], str)
@@ -280,32 +512,61 @@ class BaseOpenAlex:
280
512
  res.raise_for_status()
281
513
  res_json = res.json()
282
514
 
283
- # group-by or results page
284
515
  if self.params and "group-by" in self.params:
285
- results = res_json["group_by"]
516
+ return OpenAlexResponseList(
517
+ res_json["group_by"], res_json["meta"], self.resource_class
518
+ )
286
519
  elif "results" in res_json:
287
- results = [self.resource_class(ent) for ent in res_json["results"]]
520
+ return OpenAlexResponseList(
521
+ res_json["results"], res_json["meta"], self.resource_class
522
+ )
288
523
  elif "id" in res_json:
289
- results = self.resource_class(res_json)
524
+ return self.resource_class(res_json)
290
525
  else:
291
526
  raise ValueError("Unknown response format")
292
527
 
293
- # return result and metadata
294
- if return_meta:
295
- return results, res_json["meta"]
296
- else:
297
- return results
298
-
299
528
  def get(self, return_meta=False, page=None, per_page=None, cursor=None):
300
529
  if per_page is not None and (per_page < 1 or per_page > 200):
301
530
  raise ValueError("per_page should be a number between 1 and 200.")
302
531
 
303
- self._add_params("per-page", per_page)
304
- self._add_params("page", page)
305
- self._add_params("cursor", cursor)
306
- return self._get_from_url(self.url, return_meta=return_meta)
532
+ if not isinstance(self.params, (str, list)):
533
+ self._add_params("per-page", per_page)
534
+ self._add_params("page", page)
535
+ self._add_params("cursor", cursor)
536
+
537
+ resp_list = self._get_from_url(self.url)
538
+
539
+ if return_meta:
540
+ warnings.warn(
541
+ "return_meta is deprecated, call .meta on the result",
542
+ DeprecationWarning,
543
+ stacklevel=2,
544
+ )
545
+ return resp_list, resp_list.meta
546
+ else:
547
+ return resp_list
307
548
 
308
549
  def paginate(self, method="cursor", page=1, per_page=None, cursor="*", n_max=10000):
550
+ """Paginate results from the API.
551
+
552
+ Parameters
553
+ ----------
554
+ method : str, optional
555
+ Pagination method ('cursor' or 'page').
556
+ page : int, optional
557
+ Page number for pagination.
558
+ per_page : int, optional
559
+ Number of results per page.
560
+ cursor : str, optional
561
+ Cursor for pagination.
562
+ n_max : int, optional
563
+ Maximum number of results.
564
+
565
+ Returns
566
+ -------
567
+ Paginator
568
+ Paginator object.
569
+ """
309
570
  if method == "cursor":
310
571
  if self.params.get("sample"):
311
572
  raise ValueError("method should be 'page' when using sample")
@@ -320,9 +581,30 @@ class BaseOpenAlex:
320
581
  )
321
582
 
322
583
  def random(self):
584
+ """Get a random result.
585
+
586
+ Returns
587
+ -------
588
+ OpenAlexEntity
589
+ Random result.
590
+ """
323
591
  return self.__getitem__("random")
324
592
 
325
- def _add_params(self, argument, new_params):
593
+ def _add_params(self, argument, new_params, raise_if_exists=False):
594
+ """Add parameters to the API request.
595
+
596
+ Parameters
597
+ ----------
598
+ argument : str
599
+ Parameter name.
600
+ new_params : any
601
+ Parameter value.
602
+ raise_if_exists : bool, optional
603
+ Whether to raise an error if the parameter already exists.
604
+ """
605
+ if raise_if_exists:
606
+ raise NotImplementedError("raise_if_exists is not implemented")
607
+
326
608
  if self.params is None:
327
609
  self.params = {argument: new_params}
328
610
  elif argument in self.params and isinstance(self.params[argument], dict):
@@ -333,44 +615,247 @@ class BaseOpenAlex:
333
615
  logging.debug("Params updated:", self.params)
334
616
 
335
617
  def filter(self, **kwargs):
618
+ """Add filter parameters to the API request.
619
+
620
+ Parameters
621
+ ----------
622
+ **kwargs : dict
623
+ Filter parameters.
624
+
625
+ Returns
626
+ -------
627
+ BaseOpenAlex
628
+ Updated object.
629
+ """
336
630
  self._add_params("filter", kwargs)
337
631
  return self
338
632
 
633
+ def filter_and(self, **kwargs):
634
+ """Add AND filter parameters to the API request.
635
+
636
+ Parameters
637
+ ----------
638
+ **kwargs : dict
639
+ Filter parameters.
640
+
641
+ Returns
642
+ -------
643
+ BaseOpenAlex
644
+ Updated object.
645
+ """
646
+ return self.filter(**kwargs)
647
+
648
+ def filter_or(self, **kwargs):
649
+ """Add OR filter parameters to the API request.
650
+
651
+ Parameters
652
+ ----------
653
+ **kwargs : dict
654
+ Filter parameters.
655
+
656
+ Returns
657
+ -------
658
+ BaseOpenAlex
659
+ Updated object.
660
+ """
661
+ self._add_params("filter", or_(kwargs), raise_if_exists=False)
662
+ return self
663
+
664
+ def filter_not(self, **kwargs):
665
+ """Add NOT filter parameters to the API request.
666
+
667
+ Parameters
668
+ ----------
669
+ **kwargs : dict
670
+ Filter parameters.
671
+
672
+ Returns
673
+ -------
674
+ BaseOpenAlex
675
+ Updated object.
676
+ """
677
+ self._add_params("filter", _wrap_values_nested_dict(kwargs, not_))
678
+ return self
679
+
680
+ def filter_gt(self, **kwargs):
681
+ """Add greater than filter parameters to the API request.
682
+
683
+ Parameters
684
+ ----------
685
+ **kwargs : dict
686
+ Filter parameters.
687
+
688
+ Returns
689
+ -------
690
+ BaseOpenAlex
691
+ Updated object.
692
+ """
693
+ self._add_params("filter", _wrap_values_nested_dict(kwargs, gt_))
694
+ return self
695
+
696
+ def filter_lt(self, **kwargs):
697
+ """Add less than filter parameters to the API request.
698
+
699
+ Parameters
700
+ ----------
701
+ **kwargs : dict
702
+ Filter parameters.
703
+
704
+ Returns
705
+ -------
706
+ BaseOpenAlex
707
+ Updated object.
708
+ """
709
+ self._add_params("filter", _wrap_values_nested_dict(kwargs, lt_))
710
+ return self
711
+
339
712
  def search_filter(self, **kwargs):
713
+ """Add search filter parameters to the API request.
714
+
715
+ Parameters
716
+ ----------
717
+ **kwargs : dict
718
+ Filter parameters.
719
+
720
+ Returns
721
+ -------
722
+ BaseOpenAlex
723
+ Updated object.
724
+ """
340
725
  self._add_params("filter", {f"{k}.search": v for k, v in kwargs.items()})
341
726
  return self
342
727
 
343
728
  def sort(self, **kwargs):
729
+ """Add sort parameters to the API request.
730
+
731
+ Parameters
732
+ ----------
733
+ **kwargs : dict
734
+ Sort parameters.
735
+
736
+ Returns
737
+ -------
738
+ BaseOpenAlex
739
+ Updated object.
740
+ """
344
741
  self._add_params("sort", kwargs)
345
742
  return self
346
743
 
347
744
  def group_by(self, group_key):
745
+ """Add group-by parameters to the API request.
746
+
747
+ Parameters
748
+ ----------
749
+ group_key : str
750
+ Group-by key.
751
+
752
+ Returns
753
+ -------
754
+ BaseOpenAlex
755
+ Updated object.
756
+ """
348
757
  self._add_params("group-by", group_key)
349
758
  return self
350
759
 
351
760
  def search(self, s):
761
+ """Add search parameters to the API request.
762
+
763
+ Parameters
764
+ ----------
765
+ s : str
766
+ Search string.
767
+
768
+ Returns
769
+ -------
770
+ BaseOpenAlex
771
+ Updated object.
772
+ """
352
773
  self._add_params("search", s)
353
774
  return self
354
775
 
355
776
  def sample(self, n, seed=None):
777
+ """Add sample parameters to the API request.
778
+
779
+ Parameters
780
+ ----------
781
+ n : int
782
+ Number of samples.
783
+ seed : int, optional
784
+ Seed for sampling.
785
+
786
+ Returns
787
+ -------
788
+ BaseOpenAlex
789
+ Updated object.
790
+ """
356
791
  self._add_params("sample", n)
357
792
  self._add_params("seed", seed)
358
793
  return self
359
794
 
360
795
  def select(self, s):
796
+ """Add select parameters to the API request.
797
+
798
+ Parameters
799
+ ----------
800
+ s : str
801
+ Select string.
802
+
803
+ Returns
804
+ -------
805
+ BaseOpenAlex
806
+ Updated object.
807
+ """
361
808
  self._add_params("select", s)
362
809
  return self
363
810
 
364
- def autocomplete(self, s, **kwargs):
365
- """autocomplete the string s, for a specific type of entity"""
811
+ def autocomplete(self, s, return_meta=False):
812
+ """Return the OpenAlex autocomplete results.
813
+
814
+ Parameters
815
+ ----------
816
+ s : str
817
+ String to autocomplete.
818
+ return_meta : bool, optional
819
+ Whether to return metadata.
820
+
821
+ Returns
822
+ -------
823
+ OpenAlexResponseList
824
+ List of autocomplete results.
825
+ """
826
+
366
827
  self._add_params("q", s)
367
- return self.get(**kwargs)
828
+
829
+ resp_list = self._get_from_url(
830
+ urlunparse(
831
+ (
832
+ "https",
833
+ "api.openalex.org",
834
+ f"autocomplete/{self.__class__.__name__.lower()}",
835
+ "",
836
+ self._url_query(),
837
+ "",
838
+ )
839
+ )
840
+ )
841
+
842
+ if return_meta:
843
+ warnings.warn(
844
+ "return_meta is deprecated, call .meta on the result",
845
+ DeprecationWarning,
846
+ stacklevel=2,
847
+ )
848
+ return resp_list, resp_list.meta
849
+ else:
850
+ return resp_list
368
851
 
369
852
 
370
853
  # The API
371
854
 
372
855
 
373
856
  class Work(OpenAlexEntity):
857
+ """Class representing a work entity in OpenAlex."""
858
+
374
859
  def __getitem__(self, key):
375
860
  if key == "abstract":
376
861
  return invert_abstract(self["abstract_inverted_index"])
@@ -378,6 +863,18 @@ class Work(OpenAlexEntity):
378
863
  return super().__getitem__(key)
379
864
 
380
865
  def ngrams(self, return_meta=False):
866
+ """Get n-grams for the work.
867
+
868
+ Parameters
869
+ ----------
870
+ return_meta : bool, optional
871
+ Whether to return metadata.
872
+
873
+ Returns
874
+ -------
875
+ OpenAlexResponseList
876
+ List of n-grams.
877
+ """
381
878
  openalex_id = self["id"].split("/")[-1]
382
879
  n_gram_url = f"{config.openalex_url}/works/{openalex_id}/ngrams"
383
880
 
@@ -385,105 +882,162 @@ class Work(OpenAlexEntity):
385
882
  res.raise_for_status()
386
883
  results = res.json()
387
884
 
388
- # return result and metadata
885
+ resp_list = OpenAlexResponseList(results["ngrams"], results["meta"])
886
+
389
887
  if return_meta:
390
- return results["ngrams"], results["meta"]
888
+ warnings.warn(
889
+ "return_meta is deprecated, call .meta on the result",
890
+ DeprecationWarning,
891
+ stacklevel=2,
892
+ )
893
+ return resp_list, resp_list.meta
391
894
  else:
392
- return results["ngrams"]
895
+ return resp_list
393
896
 
394
897
 
395
898
  class Works(BaseOpenAlex):
899
+ """Class representing a collection of work entities in OpenAlex."""
900
+
396
901
  resource_class = Work
397
902
 
398
903
 
399
904
  class Author(OpenAlexEntity):
905
+ """Class representing an author entity in OpenAlex."""
906
+
400
907
  pass
401
908
 
402
909
 
403
910
  class Authors(BaseOpenAlex):
911
+ """Class representing a collection of author entities in OpenAlex."""
912
+
404
913
  resource_class = Author
405
914
 
406
915
 
407
916
  class Source(OpenAlexEntity):
917
+ """Class representing a source entity in OpenAlex."""
918
+
408
919
  pass
409
920
 
410
921
 
411
922
  class Sources(BaseOpenAlex):
923
+ """Class representing a collection of source entities in OpenAlex."""
924
+
412
925
  resource_class = Source
413
926
 
414
927
 
415
928
  class Institution(OpenAlexEntity):
929
+ """Class representing an institution entity in OpenAlex."""
930
+
416
931
  pass
417
932
 
418
933
 
419
934
  class Institutions(BaseOpenAlex):
935
+ """Class representing a collection of institution entities in OpenAlex."""
936
+
420
937
  resource_class = Institution
421
938
 
422
939
 
423
940
  class Domain(OpenAlexEntity):
941
+ """Class representing a domain entity in OpenAlex."""
942
+
424
943
  pass
425
944
 
426
945
 
427
946
  class Domains(BaseOpenAlex):
947
+ """Class representing a collection of domain entities in OpenAlex."""
948
+
428
949
  resource_class = Domain
429
950
 
430
951
 
431
952
  class Field(OpenAlexEntity):
953
+ """Class representing a field entity in OpenAlex."""
954
+
432
955
  pass
433
956
 
434
957
 
435
958
  class Fields(BaseOpenAlex):
959
+ """Class representing a collection of field entities in OpenAlex."""
960
+
436
961
  resource_class = Field
437
962
 
438
963
 
439
964
  class Subfield(OpenAlexEntity):
965
+ """Class representing a subfield entity in OpenAlex."""
966
+
440
967
  pass
441
968
 
442
969
 
443
970
  class Subfields(BaseOpenAlex):
971
+ """Class representing a collection of subfield entities in OpenAlex."""
972
+
444
973
  resource_class = Subfield
445
974
 
446
975
 
447
976
  class Topic(OpenAlexEntity):
977
+ """Class representing a topic entity in OpenAlex."""
978
+
448
979
  pass
449
980
 
450
981
 
451
982
  class Topics(BaseOpenAlex):
983
+ """Class representing a collection of topic entities in OpenAlex."""
984
+
452
985
  resource_class = Topic
453
986
 
454
987
 
455
988
  class Publisher(OpenAlexEntity):
989
+ """Class representing a publisher entity in OpenAlex."""
990
+
456
991
  pass
457
992
 
458
993
 
459
994
  class Publishers(BaseOpenAlex):
995
+ """Class representing a collection of publisher entities in OpenAlex."""
996
+
460
997
  resource_class = Publisher
461
998
 
462
999
 
463
1000
  class Funder(OpenAlexEntity):
1001
+ """Class representing a funder entity in OpenAlex."""
1002
+
464
1003
  pass
465
1004
 
466
1005
 
467
1006
  class Funders(BaseOpenAlex):
1007
+ """Class representing a collection of funder entities in OpenAlex."""
1008
+
468
1009
  resource_class = Funder
469
1010
 
470
1011
 
471
1012
  class Autocomplete(OpenAlexEntity):
1013
+ """Class representing an autocomplete entity in OpenAlex."""
1014
+
472
1015
  pass
473
1016
 
474
1017
 
475
1018
  class autocompletes(BaseOpenAlex):
476
- """Class to autocomplete without being based on the type of entity"""
1019
+ """Class to autocomplete without being based on the type of entity."""
477
1020
 
478
1021
  resource_class = Autocomplete
479
1022
 
480
1023
  def __getitem__(self, key):
481
1024
  return self._get_from_url(
482
- f"{config.openalex_url}/autocomplete?q={key}", return_meta=False
1025
+ urlunparse(
1026
+ (
1027
+ "https",
1028
+ "api.openalex.org",
1029
+ "autocomplete",
1030
+ "",
1031
+ f"q={quote_plus(key)}",
1032
+ "",
1033
+ )
1034
+ )
483
1035
  )
484
1036
 
485
1037
 
486
1038
  class Concept(OpenAlexEntity):
1039
+ """Class representing a concept entity in OpenAlex."""
1040
+
487
1041
  def __init__(self, *args, **kwargs):
488
1042
  warnings.warn(
489
1043
  "Concept is deprecated by OpenAlex and replaced by topics.",
@@ -494,6 +1048,8 @@ class Concept(OpenAlexEntity):
494
1048
 
495
1049
 
496
1050
  class Concepts(BaseOpenAlex):
1051
+ """Class representing a collection of concept entities in OpenAlex."""
1052
+
497
1053
  resource_class = Concept
498
1054
 
499
1055
  def __init__(self, *args, **kwargs):
@@ -506,7 +1062,18 @@ class Concepts(BaseOpenAlex):
506
1062
 
507
1063
 
508
1064
  def autocomplete(s):
509
- """autocomplete with any type of entity"""
1065
+ """Autocomplete with any type of entity.
1066
+
1067
+ Parameters
1068
+ ----------
1069
+ s : str
1070
+ String to autocomplete.
1071
+
1072
+ Returns
1073
+ -------
1074
+ OpenAlexResponseList
1075
+ List of autocomplete results.
1076
+ """
510
1077
  return autocompletes()[s]
511
1078
 
512
1079
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: pyalex
3
- Version: 0.15.1
3
+ Version: 0.17
4
4
  Summary: Python interface to the OpenAlex database
5
5
  Author-email: Jonathan de Bruin <jonathandebruinos@gmail.com>
6
6
  License: MIT
@@ -17,10 +17,10 @@ License-File: LICENSE
17
17
  Requires-Dist: requests
18
18
  Requires-Dist: urllib3
19
19
  Provides-Extra: lint
20
- Requires-Dist: ruff ; extra == 'lint'
20
+ Requires-Dist: ruff; extra == "lint"
21
21
  Provides-Extra: test
22
- Requires-Dist: pytest ; extra == 'test'
23
- Requires-Dist: pytest-xdist ; extra == 'test'
22
+ Requires-Dist: pytest; extra == "test"
23
+ Requires-Dist: pytest-xdist; extra == "test"
24
24
 
25
25
  <p align="center">
26
26
  <img alt="PyAlex - a Python wrapper for OpenAlex" src="https://github.com/J535D165/pyalex/raw/main/pyalex_repocard.svg">
@@ -126,7 +126,7 @@ Works()["W2741809807"]["open_access"]
126
126
  The previous works also for Authors, Sources, Institutions, Concepts and Topics
127
127
 
128
128
  ```python
129
- Authors()["A2887243803"]
129
+ Authors()["A5027479191"]
130
130
  Authors()["https://orcid.org/0000-0002-4297-0502"] # same
131
131
  ```
132
132
 
@@ -139,7 +139,6 @@ Works().random()
139
139
  Authors().random()
140
140
  Sources().random()
141
141
  Institutions().random()
142
- Concepts().random()
143
142
  Topics().random()
144
143
  Publishers().random()
145
144
  Funders().random()
@@ -183,11 +182,11 @@ Works().count()
183
182
  For lists of entities, you can return the result as well as the metadata. By default, only the results are returned.
184
183
 
185
184
  ```python
186
- results, meta = Topics().get(return_meta=True)
185
+ topics = Topics().get()
187
186
  ```
188
187
 
189
188
  ```python
190
- print(meta)
189
+ print(topics.meta)
191
190
  {'count': 65073, 'db_response_time_ms': 16, 'page': 1, 'per_page': 25}
192
191
  ```
193
192
 
@@ -383,6 +382,10 @@ Works()["W2023271753"].ngrams()
383
382
  All results from PyAlex can be serialized. For example, save the results to a JSON file:
384
383
 
385
384
  ```python
385
+ import json
386
+ from pathlib import Path
387
+ from pyalex import Work
388
+
386
389
  with open(Path("works.json"), "w") as f:
387
390
  json.dump(Works().get(), f)
388
391
 
@@ -394,7 +397,7 @@ with open(Path("works.json")) as f:
394
397
 
395
398
  A list of awesome use cases of the OpenAlex dataset.
396
399
 
397
- ### Cited publications (referenced works)
400
+ ### Cited publications (works referenced by this paper, outgoing citations)
398
401
 
399
402
  ```python
400
403
  from pyalex import Works
@@ -405,6 +408,13 @@ w = Works()["W2741809807"]
405
408
  Works()[w["referenced_works"]]
406
409
  ```
407
410
 
411
+ ### Citing publications (other works that reference this paper, incoming citations)
412
+
413
+ ```python
414
+ from pyalex import Works
415
+ Works().filter(cites="W2741809807").get()
416
+ ```
417
+
408
418
  ### Get works of a single author
409
419
 
410
420
  ```python
@@ -463,6 +473,7 @@ R users can use the excellent [OpenAlexR](https://github.com/ropensci/openalexR)
463
473
 
464
474
  > This library is a community contribution. The authors of this Python library aren't affiliated with OpenAlex.
465
475
 
476
+ This library is maintained by [J535D165](https://github.com/J535D165) and [PeterLombaers](https://github.com/PeterLombaers).
466
477
  Feel free to reach out with questions, remarks, and suggestions. The
467
- [issue tracker](/issues) is a good starting point. You can also email me at
478
+ [issue tracker](/issues) is a good starting point. You can also reach out via
468
479
  [jonathandebruinos@gmail.com](mailto:jonathandebruinos@gmail.com).
@@ -0,0 +1,8 @@
1
+ pyalex/__init__.py,sha256=upMXti6aJF6lz8J4EbdnQa13GhJzFGre7fnS_tj8NOw,1539
2
+ pyalex/_version.py,sha256=t3d5dJC864lqQ-TUIs6gpWRp7YVH04dI08mpFt6wvR0,508
3
+ pyalex/api.py,sha256=05clKZlIcH7g7G5d7tELxrfUIyyzdPKwbRinZ1Pliy4,26783
4
+ pyalex-0.17.dist-info/LICENSE,sha256=Mhf5MImRYP06a1EPVJCpkpTstOOEfGajN3T_Fz4izMg,1074
5
+ pyalex-0.17.dist-info/METADATA,sha256=3K3i_n7uMyF91DfrwFI5t0X5jaYD8lD2HcySMNHcfbg,14208
6
+ pyalex-0.17.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
7
+ pyalex-0.17.dist-info/top_level.txt,sha256=D0An8hWy9e0xPhTaT6K-yuJKVeVV3bYGxZ6Y-v2WXSU,7
8
+ pyalex-0.17.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.1)
2
+ Generator: setuptools (76.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- pyalex/__init__.py,sha256=52XK8om6IVD1Yiq_HYOCR6PUY56sPRHutGM03NOrGMQ,1467
2
- pyalex/_version.py,sha256=po5_rvCFTU8xU9IC56wyK0-zfBgz_U4xX6CO2mv9Mzs,413
3
- pyalex/api.py,sha256=JTOx0P037IOhhYVAigQO5uPsd1HMQC-SM3wny0_52uU,13236
4
- pyalex-0.15.1.dist-info/LICENSE,sha256=Mhf5MImRYP06a1EPVJCpkpTstOOEfGajN3T_Fz4izMg,1074
5
- pyalex-0.15.1.dist-info/METADATA,sha256=JSa8cKcjZUZ-lDo28WyuFN1GHEEWUHWLvSLaKh3IP10,13859
6
- pyalex-0.15.1.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
7
- pyalex-0.15.1.dist-info/top_level.txt,sha256=D0An8hWy9e0xPhTaT6K-yuJKVeVV3bYGxZ6Y-v2WXSU,7
8
- pyalex-0.15.1.dist-info/RECORD,,