destiny_sdk 0.7.5__tar.gz → 0.8.0__tar.gz
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-0.7.5 → destiny_sdk-0.8.0}/PKG-INFO +2 -5
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/pyproject.toml +5 -5
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/client.py +5 -7
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/enhancements.py +95 -1
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/identifiers.py +59 -20
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_enhancements.py +75 -1
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_identifiers.py +53 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/uv.lock +7 -7
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/.gitignore +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/LICENSE +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/README.md +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/__init__.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/auth.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/core.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/imports.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/labs/__init__.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/labs/references.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/parsers/__init__.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/parsers/eppi_parser.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/parsers/exceptions.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/py.typed +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/references.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/robots.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/search.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/src/destiny_sdk/visibility.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/__init__.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/conftest.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/labs/test_references.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/parsers/test_eppi_parser.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_auth.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_client.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/destiny_references.jsonl +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/eppi_import.jsonl +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/eppi_import_with_annotations.jsonl +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/eppi_import_with_raw.jsonl +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/eppi_report.json +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_references.py +0 -0
- {destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_robots.py +0 -0
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: destiny_sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -6,7 +6,10 @@ requires = ["hatchling"]
|
|
|
6
6
|
dev = [
|
|
7
7
|
"mypy>=1.15.0,<2",
|
|
8
8
|
"pre-commit>=4.2.0,<5",
|
|
9
|
+
"pytest-asyncio>=1.0.0,<2",
|
|
9
10
|
"pytest-env>=1.1.5,<2",
|
|
11
|
+
"pytest-httpx>=0.35.0,<0.36",
|
|
12
|
+
"pytest>=8.4.0,<9",
|
|
10
13
|
"ruff>=0.11.5,<0.12",
|
|
11
14
|
"uvloop>=0.21.0,<0.22",
|
|
12
15
|
]
|
|
@@ -25,17 +28,14 @@ dependencies = [
|
|
|
25
28
|
"httpx>=0.28.1,<0.29",
|
|
26
29
|
"msal>=1.34.0",
|
|
27
30
|
"pydantic>=2.11.3,<3",
|
|
28
|
-
"pytest-asyncio>=1.0.0,<2",
|
|
29
|
-
"pytest-httpx>=0.35.0,<0.36",
|
|
30
|
-
"pytest>=8.4.0,<9",
|
|
31
31
|
"python-jose>=3.4.0,<4",
|
|
32
32
|
]
|
|
33
33
|
description = "A software development kit (sdk) to support interaction with the DESTINY repository"
|
|
34
34
|
license = "Apache-2.0"
|
|
35
35
|
name = "destiny_sdk"
|
|
36
36
|
readme = "README.md"
|
|
37
|
-
requires-python = "
|
|
38
|
-
version = "0.
|
|
37
|
+
requires-python = ">=3.12, <4"
|
|
38
|
+
version = "0.8.0"
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
labs = []
|
|
@@ -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
|
|
@@ -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
|
|
@@ -50,6 +50,27 @@ class AuthorPosition(StrEnum):
|
|
|
50
50
|
"""The last author."""
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
class PublicationVenueType(StrEnum):
|
|
54
|
+
"""
|
|
55
|
+
Type of publication venue.
|
|
56
|
+
|
|
57
|
+
Aligns with OpenAlex source types.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
JOURNAL = auto()
|
|
61
|
+
"""A journal publication."""
|
|
62
|
+
REPOSITORY = auto()
|
|
63
|
+
"""A repository (includes preprint servers like arXiv, bioRxiv)."""
|
|
64
|
+
CONFERENCE = auto()
|
|
65
|
+
"""A conference proceeding."""
|
|
66
|
+
EBOOK_PLATFORM = auto()
|
|
67
|
+
"""An ebook platform."""
|
|
68
|
+
BOOK_SERIES = auto()
|
|
69
|
+
"""A book series."""
|
|
70
|
+
OTHER = auto()
|
|
71
|
+
"""Other venue type."""
|
|
72
|
+
|
|
73
|
+
|
|
53
74
|
class Authorship(BaseModel):
|
|
54
75
|
"""
|
|
55
76
|
Represents a single author and their association with a reference.
|
|
@@ -70,6 +91,71 @@ class Authorship(BaseModel):
|
|
|
70
91
|
)
|
|
71
92
|
|
|
72
93
|
|
|
94
|
+
class Pagination(BaseModel):
|
|
95
|
+
"""
|
|
96
|
+
Pagination information for journal articles.
|
|
97
|
+
|
|
98
|
+
Maps to OpenAlex's work.biblio object. All fields are strings to match
|
|
99
|
+
OpenAlex's format, which may include non-numeric values like "Spring" or "A1".
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
volume: str | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
description="The volume number of the journal/publication.",
|
|
105
|
+
)
|
|
106
|
+
issue: str | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description="The issue number of the journal/publication.",
|
|
109
|
+
)
|
|
110
|
+
first_page: str | None = Field(
|
|
111
|
+
default=None,
|
|
112
|
+
description="The first page number of the reference in the publication.",
|
|
113
|
+
)
|
|
114
|
+
last_page: str | None = Field(
|
|
115
|
+
default=None,
|
|
116
|
+
description="The last page number of the reference in the publication.",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("volume", "issue", "first_page", "last_page", mode="before")
|
|
120
|
+
@classmethod
|
|
121
|
+
def normalize_pagination_string(cls, value: str | None) -> str | None:
|
|
122
|
+
"""Normalize pagination strings: NBSP to space, strip, empty to None."""
|
|
123
|
+
if isinstance(value, str):
|
|
124
|
+
# Replace NBSP with space, then strip
|
|
125
|
+
value = value.replace("\u00a0", " ").strip()
|
|
126
|
+
return value if value else None
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class PublicationVenue(BaseModel):
|
|
131
|
+
"""A publication venue (journal, repository, conference, etc.)."""
|
|
132
|
+
|
|
133
|
+
display_name: str | None = Field(
|
|
134
|
+
default=None,
|
|
135
|
+
description=(
|
|
136
|
+
"The display name of the venue (journal name, repository name, etc.)"
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
venue_type: PublicationVenueType | None = Field(
|
|
140
|
+
default=None,
|
|
141
|
+
description="The type of venue: journal, repository, book, conference, etc.",
|
|
142
|
+
)
|
|
143
|
+
issn: list[str] | None = Field(
|
|
144
|
+
default=None,
|
|
145
|
+
description="List of ISSNs associated with this venue (print and electronic)",
|
|
146
|
+
)
|
|
147
|
+
issn_l: str | None = Field(
|
|
148
|
+
default=None,
|
|
149
|
+
description=(
|
|
150
|
+
"The linking ISSN - a canonical ISSN for the venue across format changes"
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
host_organization_name: str | None = Field(
|
|
154
|
+
default=None,
|
|
155
|
+
description="Display name of the host organization (publisher)",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
73
159
|
class BibliographicMetadataEnhancement(BaseModel):
|
|
74
160
|
"""
|
|
75
161
|
An enhancement which is made up of bibliographic metadata.
|
|
@@ -111,6 +197,14 @@ other works have cited this work
|
|
|
111
197
|
description="The name of the entity which published the version of record.",
|
|
112
198
|
)
|
|
113
199
|
title: str | None = Field(default=None, description="The title of the reference.")
|
|
200
|
+
pagination: Pagination | None = Field(
|
|
201
|
+
default=None,
|
|
202
|
+
description="Pagination info (volume, issue, pages).",
|
|
203
|
+
)
|
|
204
|
+
publication_venue: PublicationVenue | None = Field(
|
|
205
|
+
default=None,
|
|
206
|
+
description="Publication venue information (journal, repository, etc.).",
|
|
207
|
+
)
|
|
114
208
|
|
|
115
209
|
@property
|
|
116
210
|
def fingerprint(self) -> str:
|
|
@@ -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."
|
|
@@ -8,7 +8,13 @@ from pydantic import ValidationError
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def test_bibliographic_metadata_enhancement_valid():
|
|
11
|
-
# Create valid bibliographic content
|
|
11
|
+
# Create valid bibliographic content with pagination
|
|
12
|
+
pagination = destiny_sdk.enhancements.Pagination(
|
|
13
|
+
volume="42",
|
|
14
|
+
issue="7",
|
|
15
|
+
first_page="495",
|
|
16
|
+
last_page="512",
|
|
17
|
+
)
|
|
12
18
|
bibliographic = destiny_sdk.enhancements.BibliographicMetadataEnhancement(
|
|
13
19
|
enhancement_type=destiny_sdk.enhancements.EnhancementType.BIBLIOGRAPHIC,
|
|
14
20
|
authorship=[],
|
|
@@ -19,6 +25,7 @@ def test_bibliographic_metadata_enhancement_valid():
|
|
|
19
25
|
publication_year=2020,
|
|
20
26
|
publisher="Test Publisher",
|
|
21
27
|
title="Test Title",
|
|
28
|
+
pagination=pagination,
|
|
22
29
|
)
|
|
23
30
|
enhancement = destiny_sdk.enhancements.Enhancement(
|
|
24
31
|
id=uuid.uuid4(),
|
|
@@ -33,6 +40,48 @@ def test_bibliographic_metadata_enhancement_valid():
|
|
|
33
40
|
enhancement.content.enhancement_type
|
|
34
41
|
== destiny_sdk.enhancements.EnhancementType.BIBLIOGRAPHIC
|
|
35
42
|
)
|
|
43
|
+
assert enhancement.content.pagination.volume == "42"
|
|
44
|
+
assert enhancement.content.pagination.issue == "7"
|
|
45
|
+
assert enhancement.content.pagination.first_page == "495"
|
|
46
|
+
assert enhancement.content.pagination.last_page == "512"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_bibliographic_metadata_enhancement_non_numeric_pagination_fields():
|
|
50
|
+
"""Test that non-numeric pagination fields are accepted (per OpenAlex spec)."""
|
|
51
|
+
bibliographic = destiny_sdk.enhancements.BibliographicMetadataEnhancement(
|
|
52
|
+
title="Test Title",
|
|
53
|
+
pagination=destiny_sdk.enhancements.Pagination(
|
|
54
|
+
volume="Spring",
|
|
55
|
+
issue="Special Issue",
|
|
56
|
+
first_page="A1",
|
|
57
|
+
last_page="A15",
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
assert bibliographic.pagination.volume == "Spring"
|
|
61
|
+
assert bibliographic.pagination.issue == "Special Issue"
|
|
62
|
+
assert bibliographic.pagination.first_page == "A1"
|
|
63
|
+
assert bibliographic.pagination.last_page == "A15"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_bibliographic_metadata_enhancement_with_publication_venue():
|
|
67
|
+
"""Test BibliographicMetadataEnhancement with publication_venue field."""
|
|
68
|
+
venue = destiny_sdk.enhancements.PublicationVenue(
|
|
69
|
+
display_name="Science",
|
|
70
|
+
venue_type=destiny_sdk.enhancements.PublicationVenueType.JOURNAL,
|
|
71
|
+
issn=["0036-8075"],
|
|
72
|
+
issn_l="0036-8075",
|
|
73
|
+
host_organization_name="AAAS",
|
|
74
|
+
)
|
|
75
|
+
bibliographic = destiny_sdk.enhancements.BibliographicMetadataEnhancement(
|
|
76
|
+
title="Test Article",
|
|
77
|
+
publication_venue=venue,
|
|
78
|
+
)
|
|
79
|
+
assert bibliographic.publication_venue is not None
|
|
80
|
+
assert bibliographic.publication_venue.display_name == "Science"
|
|
81
|
+
assert (
|
|
82
|
+
bibliographic.publication_venue.venue_type
|
|
83
|
+
== destiny_sdk.enhancements.PublicationVenueType.JOURNAL
|
|
84
|
+
)
|
|
36
85
|
|
|
37
86
|
|
|
38
87
|
def test_abstract_content_enhancement_valid():
|
|
@@ -224,3 +273,28 @@ def test_association_enhancement_invalid_identifier_type_errors():
|
|
|
224
273
|
],
|
|
225
274
|
association_type=destiny_sdk.enhancements.ReferenceAssociationType.CITES,
|
|
226
275
|
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_pagination_empty_string_to_none():
|
|
279
|
+
"""Test that empty pagination strings are converted to None."""
|
|
280
|
+
pagination = destiny_sdk.enhancements.Pagination(
|
|
281
|
+
volume="", issue=" ", first_page=None, last_page="42"
|
|
282
|
+
)
|
|
283
|
+
assert pagination.volume is None
|
|
284
|
+
assert pagination.issue is None
|
|
285
|
+
assert pagination.first_page is None
|
|
286
|
+
assert pagination.last_page == "42"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_pagination_nbsp_normalized():
|
|
290
|
+
"""Test that NBSP (U+00A0) is replaced with space and stripped."""
|
|
291
|
+
pagination = destiny_sdk.enhancements.Pagination(
|
|
292
|
+
volume="\u00a042\u00a0", # NBSP padding
|
|
293
|
+
issue="7\u00a0", # Trailing NBSP
|
|
294
|
+
first_page="\u00a0", # Only NBSP -> should become None
|
|
295
|
+
last_page="100",
|
|
296
|
+
)
|
|
297
|
+
assert pagination.volume == "42"
|
|
298
|
+
assert pagination.issue == "7"
|
|
299
|
+
assert pagination.first_page is None
|
|
300
|
+
assert pagination.last_page == "100"
|
|
@@ -30,6 +30,59 @@ def test_doi_url_removed():
|
|
|
30
30
|
assert obj.identifier == "10.1000/xyz123"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
@pytest.mark.parametrize(
|
|
34
|
+
"doi",
|
|
35
|
+
[
|
|
36
|
+
# DataCite GLIS characters: = ~ * $
|
|
37
|
+
# Crossref/legacy: #
|
|
38
|
+
"10.18730/9WQ$D", # Dollar sign
|
|
39
|
+
"10.18730/9WQ*D", # Asterisk
|
|
40
|
+
"10.18730/9WQ~D", # Tilde
|
|
41
|
+
"10.18730/9WQ=D", # Equals
|
|
42
|
+
"10.18730/9WQ#D", # Hash
|
|
43
|
+
"10.18730/9WQ$~*=#D", # Multiple special characters
|
|
44
|
+
# Latin Extended characters (accented letters)
|
|
45
|
+
"10.1000/journalÉdition", # É (U+00C9)
|
|
46
|
+
"10.1000/café", # é (U+00E9)
|
|
47
|
+
"10.1000/naïve", # ï (U+00EF)
|
|
48
|
+
"10.1000/Müller", # ü (U+00FC)
|
|
49
|
+
"10.1000/señor", # ñ (U+00F1)
|
|
50
|
+
],
|
|
51
|
+
)
|
|
52
|
+
def test_valid_doi_with_special_characters(doi: str):
|
|
53
|
+
"""Test that DOIs with DataCite GLIS and Latin Extended characters are valid."""
|
|
54
|
+
obj = destiny_sdk.identifiers.DOIIdentifier(
|
|
55
|
+
identifier_type=destiny_sdk.identifiers.ExternalIdentifierType.DOI,
|
|
56
|
+
identifier=doi,
|
|
57
|
+
)
|
|
58
|
+
assert obj.identifier == doi
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize(
|
|
62
|
+
("doi_input", "expected"),
|
|
63
|
+
[
|
|
64
|
+
# Unicode hyphen → ASCII hyphen
|
|
65
|
+
("10.1000/abc\u2010def", "10.1000/abc-def"),
|
|
66
|
+
# NBSP stripped (leading/trailing)
|
|
67
|
+
("\u00a010.1000/xyz123", "10.1000/xyz123"),
|
|
68
|
+
("10.1000/xyz123\u00a0", "10.1000/xyz123"),
|
|
69
|
+
# Case-insensitive URL prefix stripping
|
|
70
|
+
("HTTP://DOI.ORG/10.1000/xyz123", "10.1000/xyz123"),
|
|
71
|
+
("HTTPS://DOI.ORG/10.1000/xyz123", "10.1000/xyz123"),
|
|
72
|
+
("https://DX.DOI.ORG/10.1000/xyz123", "10.1000/xyz123"),
|
|
73
|
+
("DOI:10.1000/xyz123", "10.1000/xyz123"),
|
|
74
|
+
("doi: 10.1000/xyz123", "10.1000/xyz123"),
|
|
75
|
+
],
|
|
76
|
+
)
|
|
77
|
+
def test_doi_canonicalization(doi_input: str, expected: str):
|
|
78
|
+
"""Test DOI canonicalization: Unicode normalization and URL prefix stripping."""
|
|
79
|
+
obj = destiny_sdk.identifiers.DOIIdentifier(
|
|
80
|
+
identifier_type=destiny_sdk.identifiers.ExternalIdentifierType.DOI,
|
|
81
|
+
identifier=doi_input,
|
|
82
|
+
)
|
|
83
|
+
assert obj.identifier == expected
|
|
84
|
+
|
|
85
|
+
|
|
33
86
|
def test_valid_eric_identifier():
|
|
34
87
|
obj = destiny_sdk.identifiers.ERICIdentifier(
|
|
35
88
|
identifier_type=destiny_sdk.identifiers.ExternalIdentifierType.ERIC,
|
|
@@ -233,7 +233,7 @@ wheels = [
|
|
|
233
233
|
|
|
234
234
|
[[package]]
|
|
235
235
|
name = "destiny-sdk"
|
|
236
|
-
version = "0.
|
|
236
|
+
version = "0.8.0"
|
|
237
237
|
source = { editable = "." }
|
|
238
238
|
dependencies = [
|
|
239
239
|
{ name = "cachetools" },
|
|
@@ -241,9 +241,6 @@ dependencies = [
|
|
|
241
241
|
{ name = "httpx" },
|
|
242
242
|
{ name = "msal" },
|
|
243
243
|
{ name = "pydantic" },
|
|
244
|
-
{ name = "pytest" },
|
|
245
|
-
{ name = "pytest-asyncio" },
|
|
246
|
-
{ name = "pytest-httpx" },
|
|
247
244
|
{ name = "python-jose" },
|
|
248
245
|
]
|
|
249
246
|
|
|
@@ -251,7 +248,10 @@ dependencies = [
|
|
|
251
248
|
dev = [
|
|
252
249
|
{ name = "mypy" },
|
|
253
250
|
{ name = "pre-commit" },
|
|
251
|
+
{ name = "pytest" },
|
|
252
|
+
{ name = "pytest-asyncio" },
|
|
254
253
|
{ name = "pytest-env" },
|
|
254
|
+
{ name = "pytest-httpx" },
|
|
255
255
|
{ name = "ruff" },
|
|
256
256
|
{ name = "uvloop" },
|
|
257
257
|
]
|
|
@@ -263,9 +263,6 @@ requires-dist = [
|
|
|
263
263
|
{ name = "httpx", specifier = ">=0.28.1,<0.29" },
|
|
264
264
|
{ name = "msal", specifier = ">=1.34.0" },
|
|
265
265
|
{ name = "pydantic", specifier = ">=2.11.3,<3" },
|
|
266
|
-
{ name = "pytest", specifier = ">=8.4.0,<9" },
|
|
267
|
-
{ name = "pytest-asyncio", specifier = ">=1.0.0,<2" },
|
|
268
|
-
{ name = "pytest-httpx", specifier = ">=0.35.0,<0.36" },
|
|
269
266
|
{ name = "python-jose", specifier = ">=3.4.0,<4" },
|
|
270
267
|
]
|
|
271
268
|
provides-extras = ["labs"]
|
|
@@ -274,7 +271,10 @@ provides-extras = ["labs"]
|
|
|
274
271
|
dev = [
|
|
275
272
|
{ name = "mypy", specifier = ">=1.15.0,<2" },
|
|
276
273
|
{ name = "pre-commit", specifier = ">=4.2.0,<5" },
|
|
274
|
+
{ name = "pytest", specifier = ">=8.4.0,<9" },
|
|
275
|
+
{ name = "pytest-asyncio", specifier = ">=1.0.0,<2" },
|
|
277
276
|
{ name = "pytest-env", specifier = ">=1.1.5,<2" },
|
|
277
|
+
{ name = "pytest-httpx", specifier = ">=0.35.0,<0.36" },
|
|
278
278
|
{ name = "ruff", specifier = ">=0.11.5,<0.12" },
|
|
279
279
|
{ name = "uvloop", specifier = ">=0.21.0,<0.22" },
|
|
280
280
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{destiny_sdk-0.7.5 → destiny_sdk-0.8.0}/tests/unit/test_data/eppi_import_with_annotations.jsonl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|