pyalex 0.18__py3-none-any.whl → 0.20__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
@@ -7,6 +7,8 @@ except ImportError:
7
7
 
8
8
  from pyalex.api import Author
9
9
  from pyalex.api import Authors
10
+ from pyalex.api import Award
11
+ from pyalex.api import Awards
10
12
  from pyalex.api import Concept
11
13
  from pyalex.api import Concepts
12
14
  from pyalex.api import Domain
@@ -18,6 +20,8 @@ from pyalex.api import Funders
18
20
  from pyalex.api import Institution
19
21
  from pyalex.api import Institutions
20
22
  from pyalex.api import Journals
23
+ from pyalex.api import Keyword
24
+ from pyalex.api import Keywords
21
25
  from pyalex.api import OpenAlexResponseList
22
26
  from pyalex.api import People
23
27
  from pyalex.api import Publisher
@@ -35,6 +39,8 @@ from pyalex.api import config
35
39
  from pyalex.api import invert_abstract
36
40
 
37
41
  __all__ = [
42
+ "Award",
43
+ "Awards",
38
44
  "Works",
39
45
  "Work",
40
46
  "Authors",
@@ -57,6 +63,8 @@ __all__ = [
57
63
  "Subfield",
58
64
  "Topics",
59
65
  "Topic",
66
+ "Keywords",
67
+ "Keyword",
60
68
  "People",
61
69
  "Journals",
62
70
  "autocomplete",
pyalex/_version.py CHANGED
@@ -1,7 +1,14 @@
1
1
  # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
5
12
 
6
13
  TYPE_CHECKING = False
7
14
  if TYPE_CHECKING:
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
9
16
  from typing import Union
10
17
 
11
18
  VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
12
20
  else:
13
21
  VERSION_TUPLE = object
22
+ COMMIT_ID = object
14
23
 
15
24
  version: str
16
25
  __version__: str
17
26
  __version_tuple__: VERSION_TUPLE
18
27
  version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
19
30
 
20
- __version__ = version = '0.18'
21
- __version_tuple__ = version_tuple = (0, 18)
31
+ __version__ = version = '0.20'
32
+ __version_tuple__ = version_tuple = (0, 20)
33
+
34
+ __commit_id__ = commit_id = None
pyalex/api.py CHANGED
@@ -12,6 +12,8 @@ try:
12
12
  except ImportError:
13
13
  __version__ = "0.0.0"
14
14
 
15
+ logger = logging.getLogger("pyalex")
16
+
15
17
 
16
18
  class AlexConfig(dict):
17
19
  """Configuration class for OpenAlex API.
@@ -437,7 +439,9 @@ class BaseOpenAlex:
437
439
  "Object has no attribute 'filter_search'. Did you mean 'search_filter'?"
438
440
  )
439
441
 
440
- return getattr(self, key)
442
+ raise AttributeError(
443
+ f"'{self.__class__.__name__}' object has no attribute '{key}'"
444
+ )
441
445
 
442
446
  def __getitem__(self, record_id):
443
447
  if isinstance(record_id, list):
@@ -512,14 +516,20 @@ class BaseOpenAlex:
512
516
  if session is None:
513
517
  session = _get_requests_session()
514
518
 
519
+ logger.debug(f"Requesting URL: {url}")
520
+
515
521
  res = session.get(url, auth=OpenAlexAuth(config))
516
522
 
517
- if res.status_code == 403:
523
+ if res.status_code == 400:
518
524
  if (
519
525
  isinstance(res.json()["error"], str)
520
526
  and "query parameters" in res.json()["error"]
521
527
  ):
522
528
  raise QueryError(res.json()["message"])
529
+ if res.status_code == 401 and "API key" in res.json()["error"]:
530
+ raise QueryError(
531
+ f"{res.json()['error']}. Did you configure a valid API key?"
532
+ )
523
533
 
524
534
  res.raise_for_status()
525
535
  res_json = res.json()
@@ -626,7 +636,7 @@ class BaseOpenAlex:
626
636
  else:
627
637
  self.params[argument] = new_params
628
638
 
629
- logging.debug("Params updated:", self.params)
639
+ logger.debug(f"Params updated: {self.params}")
630
640
 
631
641
  def filter(self, **kwargs):
632
642
  """Add filter parameters to the API request.
@@ -864,9 +874,88 @@ class BaseOpenAlex:
864
874
  return resp_list
865
875
 
866
876
 
877
+ class BaseContent:
878
+ """Class representing content in OpenAlex."""
879
+
880
+ def __init__(self, key):
881
+ self.key = key
882
+
883
+ def __repr__(self):
884
+ return f"Content(key='{self.key}')"
885
+
886
+ @property
887
+ def url(self):
888
+ """Get the URL for the content.
889
+
890
+ Returns
891
+ -------
892
+ str
893
+ URL for the content.
894
+ """
895
+ return f"https://content.openalex.org/works/{self.key}"
896
+
897
+ def get(self):
898
+ """Get the content
899
+
900
+ Returns
901
+ -------
902
+ bytes
903
+ Content of the request.
904
+ """
905
+ content_url = f"https://content.openalex.org/works/{self.key}"
906
+
907
+ res = _get_requests_session().get(
908
+ content_url, auth=OpenAlexAuth(config), allow_redirects=True
909
+ )
910
+ res.raise_for_status()
911
+ return res.content
912
+
913
+ def download(self, filepath):
914
+ """Download the content to a file.
915
+
916
+ Parameters
917
+ ----------
918
+ filepath : str
919
+ Path to save the content.
920
+ """
921
+
922
+ with open(filepath, "wb") as f:
923
+ f.write(self.get())
924
+
925
+
867
926
  # The API
868
927
 
869
928
 
929
+ class PDF(BaseContent):
930
+ """Class representing a PDF content in OpenAlex."""
931
+
932
+ @property
933
+ def url(self):
934
+ """Get the URL for the content.
935
+
936
+ Returns
937
+ -------
938
+ str
939
+ URL for the content.
940
+ """
941
+ return f"https://content.openalex.org/works/{self.key}.pdf"
942
+
943
+
944
+ class TEI(BaseContent):
945
+ """Class representing a TEI content in OpenAlex."""
946
+
947
+ @property
948
+ def url(self):
949
+ """Get the URL for the content.
950
+
951
+ Returns
952
+ -------
953
+ str
954
+ URL for the content.
955
+ """
956
+ return f"https://content.openalex.org/works/{self.key}.grobid-xml"
957
+
958
+
870
959
  class Work(OpenAlexEntity):
871
960
  """Class representing a work entity in OpenAlex."""
872
961
 
@@ -908,6 +997,28 @@ class Work(OpenAlexEntity):
908
997
  else:
909
998
  return resp_list
910
999
 
1000
+ @property
1001
+ def pdf(self):
1002
+ """Get the PDF content for the work.
1003
+
1004
+ Returns
1005
+ -------
1006
+ PDF
1007
+ PDF content object.
1008
+ """
1009
+ return PDF(self["id"].split("/")[-1])
1010
+
1011
+ @property
1012
+ def tei(self):
1013
+ """Get the TEI content for the work.
1014
+
1015
+ Returns
1016
+ -------
1017
+ TEI
1018
+ TEI content object.
1019
+ """
1020
+ return TEI(self["id"].split("/")[-1])
1021
+
911
1022
 
912
1023
  class Works(BaseOpenAlex):
913
1024
  """Class representing a collection of work entities in OpenAlex."""
@@ -1023,6 +1134,30 @@ class Funders(BaseOpenAlex):
1023
1134
  resource_class = Funder
1024
1135
 
1025
1136
 
1137
+ class Award(OpenAlexEntity):
1138
+ """Class representing an award entity in OpenAlex."""
1139
+
1140
+ pass
1141
+
1142
+
1143
+ class Awards(BaseOpenAlex):
1144
+ """Class representing a collection of award entities in OpenAlex."""
1145
+
1146
+ resource_class = Award
1147
+
1148
+
1149
+ class Keyword(OpenAlexEntity):
1150
+ """Class representing a keyword entity in OpenAlex."""
1151
+
1152
+ pass
1153
+
1154
+
1155
+ class Keywords(BaseOpenAlex):
1156
+ """Class representing a collection of keyword entities in OpenAlex."""
1157
+
1158
+ resource_class = Keyword
1159
+
1160
+
1026
1161
  class Autocomplete(OpenAlexEntity):
1027
1162
  """Class representing an autocomplete entity in OpenAlex."""
1028
1163
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: pyalex
3
- Version: 0.18
3
+ Version: 0.20
4
4
  Summary: Python interface to the OpenAlex database
5
5
  Author-email: Jonathan de Bruin <jonathandebruinos@gmail.com>
6
6
  License: MIT
@@ -21,6 +21,8 @@ Requires-Dist: ruff; extra == "lint"
21
21
  Provides-Extra: test
22
22
  Requires-Dist: pytest; extra == "test"
23
23
  Requires-Dist: pytest-xdist; extra == "test"
24
+ Requires-Dist: dotenv; extra == "test"
25
+ Dynamic: license-file
24
26
 
25
27
  <p align="center">
26
28
  <img alt="PyAlex - a Python wrapper for OpenAlex" src="https://github.com/J535D165/pyalex/raw/main/pyalex_repocard.svg">
@@ -37,7 +39,18 @@ institutions, and more. OpenAlex offers a robust, open, and free [REST API](http
37
39
  PyAlex is a lightweight and thin Python interface to this API. PyAlex tries to
38
40
  stay as close as possible to the design of the original service.
39
41
 
40
- The following features of OpenAlex are currently supported by PyAlex:
42
+ The following entities of OpenAlex are currently supported by PyAlex:
43
+
44
+ - [x] Work
45
+ - [x] Author
46
+ - [x] Source
47
+ - [x] Institution
48
+ - [x] Concept
49
+ - [x] Topic
50
+ - [x] Publisher
51
+ - [x] Funder
52
+
53
+ Including the following functionality:
41
54
 
42
55
  - [x] Get single entities
43
56
  - [x] Filter entities
@@ -48,7 +61,7 @@ The following features of OpenAlex are currently supported by PyAlex:
48
61
  - [x] Sample
49
62
  - [x] Pagination
50
63
  - [x] Autocomplete endpoint
51
- - [x] N-grams
64
+ - [x] N-grams [Deprecated by OpenAlex]
52
65
  - [x] Authentication
53
66
 
54
67
  We aim to cover the entire API, and we are looking for help. We are welcoming Pull Requests.
@@ -57,6 +70,7 @@ We aim to cover the entire API, and we are looking for help. We are welcoming Pu
57
70
 
58
71
  - **Pipe operations** - PyAlex can handle multiple operations in a sequence. This allows the developer to write understandable queries. For examples, see [code snippets](#code-snippets).
59
72
  - **Plaintext abstracts** - OpenAlex [doesn't include plaintext abstracts](https://docs.openalex.org/api-entities/works/work-object#abstract_inverted_index) due to legal constraints. PyAlex can convert the inverted abstracts into [plaintext abstracts on the fly](#get-abstract).
73
+ - **Fetch content in PDF and TEI format** - Retrieve full-text content from OpenAlex in PDF or TEI XML formats. See [fetching content](#fetch-content-in-pdf-and-tei-format).
60
74
  - **Permissive license** - OpenAlex data is CC0 licensed :raised_hands:. PyAlex is published under the MIT license.
61
75
 
62
76
  ## Installation
@@ -72,36 +86,51 @@ pip install pyalex
72
86
  PyAlex offers support for all [Entity Objects](https://docs.openalex.org/api-entities/entities-overview): [Works](https://docs.openalex.org/api-entities/works), [Authors](https://docs.openalex.org/api-entities/authors), [Sources](https://docs.openalex.org/api-entities/sourcese), [Institutions](https://docs.openalex.org/api-entities/institutions), [Topics](https://docs.openalex.org/api-entities/topics), [Publishers](https://docs.openalex.org/api-entities/publishers), and [Funders](https://docs.openalex.org/api-entities/funders).
73
87
 
74
88
  ```python
75
- from pyalex import Works, Authors, Sources, Institutions, Topics, Publishers, Funders
89
+ from pyalex import (
90
+ Works,
91
+ Authors,
92
+ Sources,
93
+ Institutions,
94
+ Topics,
95
+ Keywords,
96
+ Publishers,
97
+ Funders,
98
+ Awards,
99
+ Concepts,
100
+ )
76
101
  ```
77
102
 
78
- ### The polite pool
103
+ ### Rate limits and authentication [Changed!]
79
104
 
80
- [The polite pool](https://docs.openalex.org/how-to-use-the-api/rate-limits-and-authentication#the-polite-pool) has much
81
- faster and more consistent response times. To get into the polite pool, you
82
- set your email:
105
+ **⚠️ API Key Required**: Starting February 13, 2026, an API key is **required** to use the OpenAlex API. API keys are free!
83
106
 
84
- ```python
85
- import pyalex
107
+ The OpenAlex API uses a credit-based rate limiting system. Different endpoint types consume different amounts of credits per request:
86
108
 
87
- pyalex.config.email = "mail@example.com"
88
- ```
109
+ - **Without API key**: 100 credits per day (testing/demos only)
110
+ - **With free API key**: 100,000 credits per day
111
+ - **Singleton requests** (e.g., `/works/W123`): Free (0 credits)
112
+ - **List requests** (e.g., `/works?filter=...`): 1 credit each
89
113
 
90
- ### Max retries
114
+ All users are limited to a maximum of 100 requests per second.
91
115
 
92
- By default, PyAlex will raise an error at the first failure when querying the OpenAlex API. You can set `max_retries` to a number higher than 0 to allow PyAlex to retry when an error occurs. `retry_backoff_factor` is related to the delay between two retry, and `retry_http_codes` are the HTTP error codes that should trigger a retry.
116
+ #### Get an API Key
117
+
118
+ 1. Create a free account at [openalex.org](https://openalex.org/)
119
+ 2. Go to [openalex.org/settings/api](https://openalex.org/settings/api) to get your API key
120
+ 3. Configure PyAlex with your key:
93
121
 
94
122
  ```python
95
- from pyalex import config
123
+ import pyalex
96
124
 
97
- config.max_retries = 0
98
- config.retry_backoff_factor = 0.1
99
- config.retry_http_codes = [429, 500, 503]
125
+ pyalex.config.api_key = "<YOUR_API_KEY>"
100
126
  ```
101
127
 
128
+ For more information, see the [OpenAlex Rate limits and authentication documentation](https://docs.openalex.org/how-to-use-the-api/rate-limits-and-authentication).
129
+
130
+
102
131
  ### Get single entity
103
132
 
104
- Get a single Work, Author, Source, Institution, Concept, Topic, Publisher or Funder from OpenAlex by the
133
+ Get a single Work, Author, Source, Institution, Concept, Topic, Publisher, Funders or Awards from OpenAlex by the
105
134
  OpenAlex ID, or by DOI or ROR.
106
135
 
107
136
  ```python
@@ -144,6 +173,8 @@ Publishers().random()
144
173
  Funders().random()
145
174
  ```
146
175
 
176
+ Check also [sample](#sample), which does support filters.
177
+
147
178
  #### Get abstract
148
179
 
149
180
  Only for Works. Request a work from the OpenAlex database:
@@ -164,6 +195,55 @@ w["abstract"]
164
195
 
165
196
  Please respect the legal constraints when using this feature.
166
197
 
198
+ #### Fetch content in PDF and TEI format
199
+
200
+ OpenAlex reference: [Get content](https://docs.openalex.org/how-to-use-the-api/get-content)
201
+
202
+ Only for Works. Retrieve the full-text content of a work in PDF or TEI (Text Encoding Initiative) XML format, if available.
203
+
204
+ ```python
205
+ from pyalex import Works
206
+
207
+ # Get a work
208
+ w = Works()["W4412002745"]
209
+
210
+ # Access the PDF content
211
+ pdf_content = w.pdf.get()
212
+
213
+ # Or access the TEI content
214
+ tei_content = w.tei.get()
215
+ ```
216
+
217
+ You can also download the content directly to a file:
218
+
219
+ ```python
220
+ from pyalex import Works
221
+
222
+ w = Works()["W4412002745"]
223
+
224
+ # Download PDF to a file
225
+ w.pdf.download("document.pdf")
226
+
227
+ # Download TEI to a file
228
+ w.tei.download("document.xml")
229
+ ```
230
+
231
+ You can also get the URL of the content without downloading it:
232
+
233
+ ```python
234
+ from pyalex import Works
235
+
236
+ w = Works()["W4412002745"]
237
+
238
+ # Get the URL of the PDF
239
+ pdf_url = w.pdf.url
240
+
241
+ # Get the URL of the TEI
242
+ tei_url = w.tei.url
243
+ ```
244
+
245
+ Note: Content availability depends on the publisher's open access policies and licensing agreements.
246
+
167
247
  ### Get lists of entities
168
248
 
169
249
  ```python
@@ -215,6 +295,17 @@ Works()
215
295
  .get()
216
296
  ```
217
297
 
298
+ #### Filter on a set of values
299
+ You can filter on a set of values, for example if you want all works from a list of DOI's:
300
+
301
+ ```python
302
+ Works()
303
+ .filter_or(doi=["10.1016/s0924-9338(99)80239-9", "10.1002/andp.19213690304"])
304
+ .get()
305
+ ```
306
+
307
+ You can use a maximum of 100 items in the set of values. Also note that OpenAlex allows a maximum URL length of 4096 characters. If you have a big list of identifiers you want to filter on you can run into this limit. It can be helpful to use the short form of the identifiers, so `W2001676859` instead of `https://openalex.org/W2001676859` and `10.1002/andp.19213690304` instead of `https://doi.org/10.1002/andp.19213690304`.
308
+
218
309
  #### Search entities
219
310
 
220
311
  OpenAlex reference: [The search parameter](https://docs.openalex.org/api-entities/works/search-works)
@@ -264,6 +355,14 @@ OpenAlex reference: [Sample entity lists](https://docs.openalex.org/how-to-use-t
264
355
  Works().sample(100, seed=535).get()
265
356
  ```
266
357
 
358
+ Get 10 random German-based institutions:
359
+
360
+ ```python
361
+ Institutions().filter(country_code="DE").sample(10).get()
362
+ ```
363
+
364
+ Check also [random](#get-random), which does not support filters.
365
+
267
366
  #### Logical expressions
268
367
 
269
368
  OpenAlex reference: [Logical expressions](https://docs.openalex.org/how-to-use-the-api/get-lists-of-entities/filter-entity-lists#logical-expressions)
@@ -393,10 +492,33 @@ with open(Path("works.json")) as f:
393
492
  works = [Work(w) for w in json.load(f)]
394
493
  ```
395
494
 
495
+ ## Standards
496
+
497
+ OpenAlex uses standard [ISO_3166-1_alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes.
498
+
396
499
  ## Code snippets
397
500
 
398
501
  A list of awesome use cases of the OpenAlex dataset.
399
502
 
503
+ ### Search author by name and affiliation
504
+
505
+ This requires searching for the affiliation first, retrieving the affiliation ID, and then searching for the author while filtering for the affiliation:
506
+
507
+ ```python
508
+ from pyalex import Authors, Institutions
509
+ import logging
510
+
511
+ # Search for the institution
512
+ insts = Institutions().search("MIT").get()
513
+ logging.info(f"{len(insts)} search results found for the institution")
514
+ inst_id = insts[0]["id"].replace("https://openalex.org/", "")
515
+
516
+ # Search for the author within the institution
517
+ auths = Authors().search("Daron Acemoglu").filter(affiliations={"institution":{"id": inst_id}}).get()
518
+ logging.info(f"{len(auths)} search results found for the author")
519
+ auth = auths[0]
520
+ ```
521
+
400
522
  ### Cited publications (works referenced by this paper, outgoing citations)
401
523
 
402
524
  ```python
@@ -423,6 +545,9 @@ from pyalex import Works
423
545
  Works().filter(author={"id": "A2887243803"}).get()
424
546
  ```
425
547
 
548
+ > [!WARNING]
549
+ > This gets only the first 25 works of the author. To get all of them, see the [paging section](#paging).
550
+
426
551
  ### Dataset publications in the global south
427
552
 
428
553
  ```python
@@ -449,16 +574,19 @@ Works() \
449
574
 
450
575
  ```
451
576
 
452
- ## Experimental
453
577
 
454
- ### Authentication
578
+ ## Troubleshooting
455
579
 
456
- OpenAlex experiments with authenticated requests at the moment. Authenticate your requests with
580
+ ### Max retries
581
+
582
+ By default, PyAlex will raise an error at the first failure when querying the OpenAlex API. You can set `max_retries` to a number higher than 0 to allow PyAlex to retry when an error occurs. `retry_backoff_factor` is related to the delay between two retry, and `retry_http_codes` are the HTTP error codes that should trigger a retry.
457
583
 
458
584
  ```python
459
- import pyalex
585
+ from pyalex import config
460
586
 
461
- pyalex.config.api_key = "<MY_KEY>"
587
+ config.max_retries = 0
588
+ config.retry_backoff_factor = 0.1
589
+ config.retry_http_codes = [429, 500, 503]
462
590
  ```
463
591
 
464
592
  ## Alternatives
@@ -0,0 +1,8 @@
1
+ pyalex/__init__.py,sha256=WlhyaRF8dXjCM7jzr9kiEcXMYLtD33lcXkZi-WNdcC0,1719
2
+ pyalex/_version.py,sha256=p-gqOONSPfi625BzXHIXfHuTTii-Zx-jV6poH7i3Jb8,701
3
+ pyalex/api.py,sha256=szLCHR3xTZrOaO45Fq1ShQk1nNdDB3OdgpIoyTXOyLY,30383
4
+ pyalex-0.20.dist-info/licenses/LICENSE,sha256=Mhf5MImRYP06a1EPVJCpkpTstOOEfGajN3T_Fz4izMg,1074
5
+ pyalex-0.20.dist-info/METADATA,sha256=YXv_LXlaV5vEF7qrge1Ln28pFx_IqVoMmixFcce5_mU,18133
6
+ pyalex-0.20.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
7
+ pyalex-0.20.dist-info/top_level.txt,sha256=D0An8hWy9e0xPhTaT6K-yuJKVeVV3bYGxZ6Y-v2WXSU,7
8
+ pyalex-0.20.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,8 +0,0 @@
1
- pyalex/__init__.py,sha256=upMXti6aJF6lz8J4EbdnQa13GhJzFGre7fnS_tj8NOw,1539
2
- pyalex/_version.py,sha256=nGXfO1OzooW799_WlgmJZ-ipD8H1pUPrej1lNW4Um9M,508
3
- pyalex/api.py,sha256=R5hDwrSlzmc3yia85DRF27DWJWpSw4b7DPqnUU_hGmw,27408
4
- pyalex-0.18.dist-info/LICENSE,sha256=Mhf5MImRYP06a1EPVJCpkpTstOOEfGajN3T_Fz4izMg,1074
5
- pyalex-0.18.dist-info/METADATA,sha256=GLWXRsSB6K9B5wAQpyHF1CCgJac9Sd6yvpcuZMHDWAg,14208
6
- pyalex-0.18.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
7
- pyalex-0.18.dist-info/top_level.txt,sha256=D0An8hWy9e0xPhTaT6K-yuJKVeVV3bYGxZ6Y-v2WXSU,7
8
- pyalex-0.18.dist-info/RECORD,,