destiny_sdk 0.7.4__py3-none-any.whl → 0.7.6__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.
- destiny_sdk/client.py +19 -7
- destiny_sdk/enhancements.py +41 -1
- destiny_sdk/identifiers.py +59 -20
- {destiny_sdk-0.7.4.dist-info → destiny_sdk-0.7.6.dist-info}/METADATA +2 -5
- {destiny_sdk-0.7.4.dist-info → destiny_sdk-0.7.6.dist-info}/RECORD +7 -7
- {destiny_sdk-0.7.4.dist-info → destiny_sdk-0.7.6.dist-info}/WHEEL +0 -0
- {destiny_sdk-0.7.4.dist-info → destiny_sdk-0.7.6.dist-info}/licenses/LICENSE +0 -0
destiny_sdk/client.py
CHANGED
|
@@ -213,8 +213,8 @@ class OAuthMiddleware(httpx.Auth):
|
|
|
213
213
|
|
|
214
214
|
auth = OAuthMiddleware(
|
|
215
215
|
azure_client_id="client-id",
|
|
216
|
-
azure_application_id="
|
|
217
|
-
|
|
216
|
+
azure_application_id="application-id",
|
|
217
|
+
azure_login_url="login-url",
|
|
218
218
|
)
|
|
219
219
|
|
|
220
220
|
**Confidential Client Application (client credentials)**
|
|
@@ -260,11 +260,9 @@ class OAuthMiddleware(httpx.Auth):
|
|
|
260
260
|
"""
|
|
261
261
|
Initialize the auth middleware.
|
|
262
262
|
|
|
263
|
-
:param
|
|
264
|
-
:type
|
|
265
|
-
:param
|
|
266
|
-
:type client_id: str
|
|
267
|
-
:param application_id: The application ID for the Destiny API.
|
|
263
|
+
:param azure_client_id: The OAuth2 client ID.
|
|
264
|
+
:type azure_client_id: str
|
|
265
|
+
:param azure_application_id: The application ID for the Destiny API.
|
|
268
266
|
:type application_id: str
|
|
269
267
|
:param azure_login_url: The Azure login URL.
|
|
270
268
|
:type azure_login_url: str
|
|
@@ -479,6 +477,7 @@ class OAuthClient:
|
|
|
479
477
|
self,
|
|
480
478
|
base_url: HttpUrl | str,
|
|
481
479
|
auth: httpx.Auth | None = None,
|
|
480
|
+
timeout: int = 10,
|
|
482
481
|
) -> None:
|
|
483
482
|
"""
|
|
484
483
|
Initialize the client.
|
|
@@ -490,6 +489,8 @@ class OAuthClient:
|
|
|
490
489
|
instance of ``OAuthMiddleware``, unless you need to create a custom auth
|
|
491
490
|
class.
|
|
492
491
|
:type auth: httpx.Auth | None
|
|
492
|
+
:param timeout: The timeout for requests, in seconds. Defaults to 10 seconds.
|
|
493
|
+
:type timeout: int
|
|
493
494
|
"""
|
|
494
495
|
self._client = httpx.Client(
|
|
495
496
|
base_url=str(base_url).removesuffix("/").removesuffix("/v1") + "/v1",
|
|
@@ -497,6 +498,7 @@ class OAuthClient:
|
|
|
497
498
|
"Content-Type": "application/json",
|
|
498
499
|
"User-Agent": user_agent,
|
|
499
500
|
},
|
|
501
|
+
timeout=timeout,
|
|
500
502
|
)
|
|
501
503
|
|
|
502
504
|
if auth:
|
|
@@ -529,6 +531,7 @@ class OAuthClient:
|
|
|
529
531
|
annotations: list[str | AnnotationFilter] | None = None,
|
|
530
532
|
sort: str | None = None,
|
|
531
533
|
page: int = 1,
|
|
534
|
+
timeout: int | None = None,
|
|
532
535
|
) -> ReferenceSearchResult:
|
|
533
536
|
"""
|
|
534
537
|
Send a search request to the Destiny Repository API.
|
|
@@ -547,6 +550,9 @@ class OAuthClient:
|
|
|
547
550
|
:type sort: str | None
|
|
548
551
|
:param page: The page number of results to retrieve.
|
|
549
552
|
:type page: int
|
|
553
|
+
:param timeout: The timeout for the request, in seconds. If provided, this will override
|
|
554
|
+
the client timeout.
|
|
555
|
+
:type timeout: int | None
|
|
550
556
|
:return: The response from the API.
|
|
551
557
|
:rtype: libs.sdk.src.destiny_sdk.references.ReferenceSearchResult
|
|
552
558
|
""" # noqa: E501
|
|
@@ -562,6 +568,7 @@ class OAuthClient:
|
|
|
562
568
|
response = self._client.get(
|
|
563
569
|
"/references/search/",
|
|
564
570
|
params=params,
|
|
571
|
+
timeout=timeout or httpx.USE_CLIENT_DEFAULT,
|
|
565
572
|
)
|
|
566
573
|
self._raise_for_status(response)
|
|
567
574
|
return ReferenceSearchResult.model_validate(response.json())
|
|
@@ -569,6 +576,7 @@ class OAuthClient:
|
|
|
569
576
|
def lookup(
|
|
570
577
|
self,
|
|
571
578
|
identifiers: list[str | IdentifierLookup],
|
|
579
|
+
timeout: int | None = None,
|
|
572
580
|
) -> list[Reference]:
|
|
573
581
|
"""
|
|
574
582
|
Lookup references by identifiers.
|
|
@@ -577,6 +585,9 @@ class OAuthClient:
|
|
|
577
585
|
|
|
578
586
|
:param identifiers: The identifiers to look up.
|
|
579
587
|
:type identifiers: list[str | libs.sdk.src.destiny_sdk.identifiers.IdentifierLookup]
|
|
588
|
+
:param timeout: The timeout for the request, in seconds. If provided, this will override
|
|
589
|
+
the client timeout.
|
|
590
|
+
:type timeout: int | None
|
|
580
591
|
:return: The list of references matching the identifiers.
|
|
581
592
|
:rtype: list[libs.sdk.src.destiny_sdk.references.Reference]
|
|
582
593
|
""" # noqa: E501
|
|
@@ -585,6 +596,7 @@ class OAuthClient:
|
|
|
585
596
|
params={
|
|
586
597
|
"identifier": ",".join([str(identifier) for identifier in identifiers])
|
|
587
598
|
},
|
|
599
|
+
timeout=timeout or httpx.USE_CLIENT_DEFAULT,
|
|
588
600
|
)
|
|
589
601
|
self._raise_for_status(response)
|
|
590
602
|
return TypeAdapter(list[Reference]).validate_python(response.json())
|
destiny_sdk/enhancements.py
CHANGED
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
from enum import StrEnum, auto
|
|
6
6
|
from typing import Annotated, Any, Literal, Self
|
|
7
7
|
|
|
8
|
-
from pydantic import UUID4, BaseModel, Field, HttpUrl, model_validator
|
|
8
|
+
from pydantic import UUID4, BaseModel, Field, HttpUrl, field_validator, model_validator
|
|
9
9
|
|
|
10
10
|
from destiny_sdk.core import _JsonlFileInputMixIn
|
|
11
11
|
from destiny_sdk.identifiers import Identifier
|
|
@@ -70,6 +70,42 @@ class Authorship(BaseModel):
|
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
class Pagination(BaseModel):
|
|
74
|
+
"""
|
|
75
|
+
Pagination information for journal articles.
|
|
76
|
+
|
|
77
|
+
Maps to OpenAlex's work.biblio object. All fields are strings to match
|
|
78
|
+
OpenAlex's format, which may include non-numeric values like "Spring" or "A1".
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
volume: str | None = Field(
|
|
82
|
+
default=None,
|
|
83
|
+
description="The volume number of the journal/publication.",
|
|
84
|
+
)
|
|
85
|
+
issue: str | None = Field(
|
|
86
|
+
default=None,
|
|
87
|
+
description="The issue number of the journal/publication.",
|
|
88
|
+
)
|
|
89
|
+
first_page: str | None = Field(
|
|
90
|
+
default=None,
|
|
91
|
+
description="The first page number of the reference in the publication.",
|
|
92
|
+
)
|
|
93
|
+
last_page: str | None = Field(
|
|
94
|
+
default=None,
|
|
95
|
+
description="The last page number of the reference in the publication.",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
@field_validator("volume", "issue", "first_page", "last_page", mode="before")
|
|
99
|
+
@classmethod
|
|
100
|
+
def normalize_pagination_string(cls, value: str | None) -> str | None:
|
|
101
|
+
"""Normalize pagination strings: NBSP to space, strip, empty to None."""
|
|
102
|
+
if isinstance(value, str):
|
|
103
|
+
# Replace NBSP with space, then strip
|
|
104
|
+
value = value.replace("\u00a0", " ").strip()
|
|
105
|
+
return value if value else None
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
73
109
|
class BibliographicMetadataEnhancement(BaseModel):
|
|
74
110
|
"""
|
|
75
111
|
An enhancement which is made up of bibliographic metadata.
|
|
@@ -111,6 +147,10 @@ other works have cited this work
|
|
|
111
147
|
description="The name of the entity which published the version of record.",
|
|
112
148
|
)
|
|
113
149
|
title: str | None = Field(default=None, description="The title of the reference.")
|
|
150
|
+
pagination: Pagination | None = Field(
|
|
151
|
+
default=None,
|
|
152
|
+
description="Pagination info (volume, issue, pages).",
|
|
153
|
+
)
|
|
114
154
|
|
|
115
155
|
@property
|
|
116
156
|
def fingerprint(self) -> str:
|
destiny_sdk/identifiers.py
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"""Identifier classes for the Destiny SDK."""
|
|
2
2
|
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
3
5
|
import uuid
|
|
4
6
|
from enum import StrEnum, auto
|
|
5
7
|
from typing import Annotated, Literal, Self
|
|
6
8
|
|
|
7
|
-
from pydantic import UUID4, BaseModel, Field, TypeAdapter, field_validator
|
|
9
|
+
from pydantic import UUID4, BaseModel, Field, PositiveInt, TypeAdapter, field_validator
|
|
10
|
+
|
|
11
|
+
# Case-insensitive patterns for DOI URL prefix stripping
|
|
12
|
+
_DOI_URL_PREFIX_RE = re.compile(r"^(?:https?://)?(?:dx\.)?doi\.org/", re.IGNORECASE)
|
|
13
|
+
_DOI_SCHEME_RE = re.compile(r"^doi:\s*", re.IGNORECASE)
|
|
14
|
+
|
|
15
|
+
# Translation table for DOI canonicalization (extensible for future characters)
|
|
16
|
+
_DOI_CHAR_TRANSLATION = str.maketrans(
|
|
17
|
+
{
|
|
18
|
+
"\u00a0": " ", # NBSP -> space
|
|
19
|
+
"\u2010": "-", # Unicode hyphen -> ASCII hyphen
|
|
20
|
+
}
|
|
21
|
+
)
|
|
8
22
|
|
|
9
23
|
|
|
10
24
|
class ExternalIdentifierType(StrEnum):
|
|
@@ -24,7 +38,7 @@ class ExternalIdentifierType(StrEnum):
|
|
|
24
38
|
PM_ID = auto()
|
|
25
39
|
"""A PubMed ID which is a unique identifier for a document in PubMed."""
|
|
26
40
|
PRO_QUEST = auto()
|
|
27
|
-
"""A ProQuest ID which is a
|
|
41
|
+
"""A ProQuest ID which is a unique identifier for a document in ProQuest."""
|
|
28
42
|
OPEN_ALEX = auto()
|
|
29
43
|
"""An OpenAlex ID which is a unique identifier for a document in OpenAlex."""
|
|
30
44
|
OTHER = auto()
|
|
@@ -35,8 +49,13 @@ class DOIIdentifier(BaseModel):
|
|
|
35
49
|
"""An external identifier representing a DOI."""
|
|
36
50
|
|
|
37
51
|
identifier: str = Field(
|
|
38
|
-
description=
|
|
39
|
-
|
|
52
|
+
description=(
|
|
53
|
+
"The DOI of the reference. "
|
|
54
|
+
"Format: '10.<registrant>/<suffix>' where registrant is 4-9 digits "
|
|
55
|
+
"and suffix is any non-whitespace characters. "
|
|
56
|
+
"Examples: 10.1000/journal.pone.0001, 10.18730/9WQ$D, 10.1000/édition"
|
|
57
|
+
),
|
|
58
|
+
pattern=r"^10\.\d{4,9}/\S+$",
|
|
40
59
|
)
|
|
41
60
|
identifier_type: Literal[ExternalIdentifierType.DOI] = Field(
|
|
42
61
|
ExternalIdentifierType.DOI, description="The type of identifier used."
|
|
@@ -44,23 +63,33 @@ class DOIIdentifier(BaseModel):
|
|
|
44
63
|
|
|
45
64
|
@field_validator("identifier", mode="before")
|
|
46
65
|
@classmethod
|
|
47
|
-
def
|
|
48
|
-
"""
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
66
|
+
def canonicalize_doi(cls, value: str) -> str:
|
|
67
|
+
"""
|
|
68
|
+
Canonicalize DOI: strip URL prefixes and normalize Unicode.
|
|
69
|
+
|
|
70
|
+
- NFC Unicode normalization
|
|
71
|
+
- Translate special characters (NBSP, Unicode hyphen) via translation table
|
|
72
|
+
- Case-insensitive URL/scheme prefix stripping
|
|
73
|
+
"""
|
|
74
|
+
# NFC normalization first
|
|
75
|
+
value = unicodedata.normalize("NFC", str(value))
|
|
76
|
+
# Translate special characters and strip
|
|
77
|
+
value = value.translate(_DOI_CHAR_TRANSLATION).strip()
|
|
78
|
+
# Strip URL prefixes (case-insensitive)
|
|
79
|
+
value = _DOI_URL_PREFIX_RE.sub("", value)
|
|
80
|
+
value = _DOI_SCHEME_RE.sub("", value)
|
|
81
|
+
return value.strip()
|
|
57
82
|
|
|
58
83
|
|
|
59
84
|
class ProQuestIdentifier(BaseModel):
|
|
60
85
|
"""An external identifier representing a ProQuest ID."""
|
|
61
86
|
|
|
62
87
|
identifier: str = Field(
|
|
63
|
-
description=
|
|
88
|
+
description=(
|
|
89
|
+
"The ProQuest ID of the reference. "
|
|
90
|
+
"Format: numeric digits only. Example: 12345678"
|
|
91
|
+
),
|
|
92
|
+
pattern=r"[0-9]+$",
|
|
64
93
|
)
|
|
65
94
|
identifier_type: Literal[ExternalIdentifierType.PRO_QUEST] = Field(
|
|
66
95
|
ExternalIdentifierType.PRO_QUEST, description="The type of identifier used."
|
|
@@ -84,12 +113,16 @@ class ERICIdentifier(BaseModel):
|
|
|
84
113
|
"""
|
|
85
114
|
An external identifier representing an ERIC Number.
|
|
86
115
|
|
|
87
|
-
An ERIC Number is defined as a
|
|
88
|
-
|
|
116
|
+
An ERIC Number is defined as a unique identifying number preceded by
|
|
117
|
+
ED (for a non-journal document) or EJ (for a journal article).
|
|
89
118
|
"""
|
|
90
119
|
|
|
91
120
|
identifier: str = Field(
|
|
92
|
-
description=
|
|
121
|
+
description=(
|
|
122
|
+
"The ERIC Number. "
|
|
123
|
+
"Format: 'ED' or 'EJ' followed by digits. Example: ED123456 or EJ789012"
|
|
124
|
+
),
|
|
125
|
+
pattern=r"E[D|J][0-9]+$",
|
|
93
126
|
)
|
|
94
127
|
identifier_type: Literal[ExternalIdentifierType.ERIC] = Field(
|
|
95
128
|
ExternalIdentifierType.ERIC, description="The type of identifier used."
|
|
@@ -112,7 +145,9 @@ class ERICIdentifier(BaseModel):
|
|
|
112
145
|
class PubMedIdentifier(BaseModel):
|
|
113
146
|
"""An external identifier representing a PubMed ID."""
|
|
114
147
|
|
|
115
|
-
identifier:
|
|
148
|
+
identifier: PositiveInt = Field(
|
|
149
|
+
description="The PubMed ID (PMID) of the reference. Example: 12345678",
|
|
150
|
+
)
|
|
116
151
|
identifier_type: Literal[ExternalIdentifierType.PM_ID] = Field(
|
|
117
152
|
ExternalIdentifierType.PM_ID, description="The type of identifier used."
|
|
118
153
|
)
|
|
@@ -122,7 +157,11 @@ class OpenAlexIdentifier(BaseModel):
|
|
|
122
157
|
"""An external identifier representing an OpenAlex ID."""
|
|
123
158
|
|
|
124
159
|
identifier: str = Field(
|
|
125
|
-
description=
|
|
160
|
+
description=(
|
|
161
|
+
"The OpenAlex ID of the reference. "
|
|
162
|
+
"Format: 'W' followed by digits. Example: W2741809807"
|
|
163
|
+
),
|
|
164
|
+
pattern=r"^W\d+$",
|
|
126
165
|
)
|
|
127
166
|
identifier_type: Literal[ExternalIdentifierType.OPEN_ALEX] = Field(
|
|
128
167
|
ExternalIdentifierType.OPEN_ALEX, description="The type of identifier used."
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: destiny_sdk
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.6
|
|
4
4
|
Summary: A software development kit (sdk) to support interaction with the DESTINY repository
|
|
5
5
|
Author-email: Adam Hamilton <adam@futureevidence.org>, Andrew Harvey <andrew@futureevidence.org>, Daniel Breves <daniel@futureevidence.org>, Jack Walmisley <jack@futureevidence.org>, Tim Repke <tim.repke@pik-potsdam.de>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
License-File: LICENSE
|
|
8
|
-
Requires-Python:
|
|
8
|
+
Requires-Python: <4,>=3.12
|
|
9
9
|
Requires-Dist: cachetools<6,>=5.5.2
|
|
10
10
|
Requires-Dist: fastapi<0.116,>=0.115.12
|
|
11
11
|
Requires-Dist: httpx<0.29,>=0.28.1
|
|
12
12
|
Requires-Dist: msal>=1.34.0
|
|
13
13
|
Requires-Dist: pydantic<3,>=2.11.3
|
|
14
|
-
Requires-Dist: pytest-asyncio<2,>=1.0.0
|
|
15
|
-
Requires-Dist: pytest-httpx<0.36,>=0.35.0
|
|
16
|
-
Requires-Dist: pytest<9,>=8.4.0
|
|
17
14
|
Requires-Dist: python-jose<4,>=3.4.0
|
|
18
15
|
Provides-Extra: labs
|
|
19
16
|
Description-Content-Type: text/markdown
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
destiny_sdk/__init__.py,sha256=NdSlsPQyDF3TW30_JzbvYMRBRA9iT677iTRWWCMdYOA,382
|
|
2
2
|
destiny_sdk/auth.py,sha256=bY72ywZEcG_67YBd9PrwgWTXkCf58rhLvVEXrtXbWtA,6247
|
|
3
|
-
destiny_sdk/client.py,sha256=
|
|
3
|
+
destiny_sdk/client.py,sha256=Hrsw_ofThHTy-WkmDYO2C0DLwXx4_4dEBEROXSl00dk,21925
|
|
4
4
|
destiny_sdk/core.py,sha256=E0Wotu9psggK1JRJxbvx3Jc7WEGE6zaz2R2awvRrLz8,2023
|
|
5
|
-
destiny_sdk/enhancements.py,sha256=
|
|
6
|
-
destiny_sdk/identifiers.py,sha256=
|
|
5
|
+
destiny_sdk/enhancements.py,sha256=oqkDu7slpQxJYBYO_xbZ8-wWmAmm5tMjYRD9NFCRY-o,17123
|
|
6
|
+
destiny_sdk/identifiers.py,sha256=0D2FTR0SO_WJNfWgXVg8M1T0VmtvHGxdIsWSiuQim7U,11215
|
|
7
7
|
destiny_sdk/imports.py,sha256=b-rh-dt3NsyLGxqmVzIzKaHiXhbw-3wtAaBN-ZW-i1E,5940
|
|
8
8
|
destiny_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
destiny_sdk/references.py,sha256=3Y8gBMTSyZY35S3pB1bnVHMai9RRiGeoGZysNvSo7kk,2553
|
|
@@ -15,7 +15,7 @@ destiny_sdk/labs/references.py,sha256=iZisRgGZ5c7X7uTFoe6Q0AwwFMa4yJbIoPUVv_hvOi
|
|
|
15
15
|
destiny_sdk/parsers/__init__.py,sha256=d5gS--bXla_0I7e_9wTBnGWMXt2U8b-_ndeprTPe1hk,149
|
|
16
16
|
destiny_sdk/parsers/eppi_parser.py,sha256=_1xnAT0F0o1HKpMWOGQbVS3VPOrhPqyzHDWR3CosWwk,9484
|
|
17
17
|
destiny_sdk/parsers/exceptions.py,sha256=0Sc_M4j560Nqh4SjeP_YrgOUVagdIwWwRz24E6YlZ1k,573
|
|
18
|
-
destiny_sdk-0.7.
|
|
19
|
-
destiny_sdk-0.7.
|
|
20
|
-
destiny_sdk-0.7.
|
|
21
|
-
destiny_sdk-0.7.
|
|
18
|
+
destiny_sdk-0.7.6.dist-info/METADATA,sha256=F1diRhYTbom-cL9sROdIiItvf4v4ACm0h7rLAgP18hs,2574
|
|
19
|
+
destiny_sdk-0.7.6.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
20
|
+
destiny_sdk-0.7.6.dist-info/licenses/LICENSE,sha256=6QURU4gvvTjVZ5rfp5amZ6FtFvcpPhAGUjxF5WSZAHI,9138
|
|
21
|
+
destiny_sdk-0.7.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|