destiny_sdk 0.5.0__tar.gz → 0.6.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.5.0 → destiny_sdk-0.6.0}/.gitignore +9 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/PKG-INFO +10 -2
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/README.md +7 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/pyproject.toml +5 -1
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/__init__.py +2 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/core.py +14 -1
- destiny_sdk-0.6.0/src/destiny_sdk/identifiers.py +192 -0
- destiny_sdk-0.6.0/src/destiny_sdk/labs/__init__.py +10 -0
- destiny_sdk-0.6.0/src/destiny_sdk/labs/references.py +154 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/references.py +9 -1
- destiny_sdk-0.6.0/src/destiny_sdk/search.py +52 -0
- destiny_sdk-0.6.0/tests/unit/labs/test_references.py +144 -0
- destiny_sdk-0.6.0/tests/unit/test_data/destiny_references.jsonl +10 -0
- destiny_sdk-0.6.0/tests/unit/test_identifiers.py +320 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/uv.lock +2 -1
- destiny_sdk-0.5.0/src/destiny_sdk/identifiers.py +0 -107
- destiny_sdk-0.5.0/tests/unit/test_identifiers.py +0 -104
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/LICENSE +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/auth.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/client.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/enhancements.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/imports.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/__init__.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/eppi_parser.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/py.typed +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/robots.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/visibility.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/__init__.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/conftest.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/parsers/test_eppi_parser.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_auth.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_client.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import.jsonl +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import_with_annotations.jsonl +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_report.json +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_enhancements.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_references.py +0 -0
- {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_robots.py +0 -0
|
@@ -15,6 +15,7 @@ downloads/
|
|
|
15
15
|
eggs/
|
|
16
16
|
.eggs/
|
|
17
17
|
lib/
|
|
18
|
+
!/ui/lib/
|
|
18
19
|
lib64/
|
|
19
20
|
parts/
|
|
20
21
|
sdist/
|
|
@@ -186,6 +187,9 @@ cython_debug/
|
|
|
186
187
|
# Mac Finder config
|
|
187
188
|
.DS_Store
|
|
188
189
|
|
|
190
|
+
# Include everything in ui (has its own .gitignore)
|
|
191
|
+
!/ui/
|
|
192
|
+
|
|
189
193
|
# delete-me working files
|
|
190
194
|
tmp-scripts.sh
|
|
191
195
|
infra/app/*.py
|
|
@@ -194,3 +198,8 @@ infra/app/*.py
|
|
|
194
198
|
.certs
|
|
195
199
|
|
|
196
200
|
libs/fake_data/*.jsonl
|
|
201
|
+
|
|
202
|
+
.data
|
|
203
|
+
.env.*
|
|
204
|
+
!.env.example
|
|
205
|
+
.idea/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: destiny_sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A software development kit (sdk) to support interaction with the DESTINY repository
|
|
5
|
-
Author-email: Adam Hamilton <adam@futureevidence.org>, Andrew Harvey <andrew@futureevidence.org>, Daniel Breves <daniel@futureevidence.org>, Jack Walmisley <jack@futureevidence.org>
|
|
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
8
|
Requires-Python: ~=3.12
|
|
@@ -14,6 +14,7 @@ Requires-Dist: pytest-asyncio<2,>=1.0.0
|
|
|
14
14
|
Requires-Dist: pytest-httpx<0.36,>=0.35.0
|
|
15
15
|
Requires-Dist: pytest<9,>=8.4.0
|
|
16
16
|
Requires-Dist: python-jose<4,>=3.4.0
|
|
17
|
+
Provides-Extra: labs
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
18
19
|
|
|
19
20
|
# DESTINY SDK
|
|
@@ -34,6 +35,13 @@ pip install destiny-sdk
|
|
|
34
35
|
uv add destiny-sdk
|
|
35
36
|
```
|
|
36
37
|
|
|
38
|
+
Some labs functionality may require extra dependencies - these can be installed by:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
pip install destiny-sdk[labs]
|
|
42
|
+
uv add destiny-sdk --extra labs
|
|
43
|
+
```
|
|
44
|
+
|
|
37
45
|
## Development
|
|
38
46
|
|
|
39
47
|
### Dependencies
|
|
@@ -16,6 +16,13 @@ pip install destiny-sdk
|
|
|
16
16
|
uv add destiny-sdk
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
Some labs functionality may require extra dependencies - these can be installed by:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install destiny-sdk[labs]
|
|
23
|
+
uv add destiny-sdk --extra labs
|
|
24
|
+
```
|
|
25
|
+
|
|
19
26
|
## Development
|
|
20
27
|
|
|
21
28
|
### Dependencies
|
|
@@ -17,6 +17,7 @@ authors = [
|
|
|
17
17
|
{email = "andrew@futureevidence.org", name = "Andrew Harvey"},
|
|
18
18
|
{email = "daniel@futureevidence.org", name = "Daniel Breves"},
|
|
19
19
|
{email = "jack@futureevidence.org", name = "Jack Walmisley"},
|
|
20
|
+
{email = "tim.repke@pik-potsdam.de", name = "Tim Repke"},
|
|
20
21
|
]
|
|
21
22
|
dependencies = [
|
|
22
23
|
"cachetools>=5.5.2,<6",
|
|
@@ -33,7 +34,10 @@ license = "Apache-2.0"
|
|
|
33
34
|
name = "destiny_sdk"
|
|
34
35
|
readme = "README.md"
|
|
35
36
|
requires-python = "~=3.12"
|
|
36
|
-
version = "0.
|
|
37
|
+
version = "0.6.0"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
labs = []
|
|
37
41
|
|
|
38
42
|
[tool.pytest.ini_options]
|
|
39
43
|
addopts = ["--color=yes", "--import-mode=importlib", "--verbose"]
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Self
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from destiny_sdk.search import SearchResultPage, SearchResultTotal
|
|
6
8
|
|
|
7
9
|
# These are non-standard newline characters that are not escaped by model_dump_json().
|
|
8
10
|
# We want jsonl files to have empirical new lines so they can be streamed line by line.
|
|
@@ -47,3 +49,14 @@ class _JsonlFileInputMixIn(BaseModel):
|
|
|
47
49
|
:rtype: Self
|
|
48
50
|
"""
|
|
49
51
|
return cls.model_validate_json(jsonl)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SearchResultMixIn(BaseModel):
|
|
55
|
+
"""A mixin class for models that represent search results."""
|
|
56
|
+
|
|
57
|
+
total: SearchResultTotal = Field(
|
|
58
|
+
description="The total number of results matching the search criteria.",
|
|
59
|
+
)
|
|
60
|
+
page: SearchResultPage = Field(
|
|
61
|
+
description="Information about the page of results.",
|
|
62
|
+
)
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Identifier classes for the Destiny SDK."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from enum import StrEnum, auto
|
|
5
|
+
from typing import Annotated, Literal, Self
|
|
6
|
+
|
|
7
|
+
from pydantic import UUID4, BaseModel, Field, TypeAdapter, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ExternalIdentifierType(StrEnum):
|
|
11
|
+
"""
|
|
12
|
+
The type of identifier used to identify a reference.
|
|
13
|
+
|
|
14
|
+
This is used to identify the type of identifier used in the `ExternalIdentifier`
|
|
15
|
+
class.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
DOI = auto()
|
|
19
|
+
"""A DOI (Digital Object Identifier) which is a unique identifier for a document."""
|
|
20
|
+
PM_ID = auto()
|
|
21
|
+
"""A PubMed ID which is a unique identifier for a document in PubMed."""
|
|
22
|
+
OPEN_ALEX = auto()
|
|
23
|
+
"""An OpenAlex ID which is a unique identifier for a document in OpenAlex."""
|
|
24
|
+
OTHER = auto()
|
|
25
|
+
"""Any other identifier not defined. This should be used sparingly."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DOIIdentifier(BaseModel):
|
|
29
|
+
"""An external identifier representing a DOI."""
|
|
30
|
+
|
|
31
|
+
identifier: str = Field(
|
|
32
|
+
description="The DOI of the reference.",
|
|
33
|
+
pattern=r"^10\.\d{4,9}/[-._;()/:a-zA-Z0-9%<>\[\]+&]+$",
|
|
34
|
+
)
|
|
35
|
+
identifier_type: Literal[ExternalIdentifierType.DOI] = Field(
|
|
36
|
+
ExternalIdentifierType.DOI, description="The type of identifier used."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@field_validator("identifier", mode="before")
|
|
40
|
+
@classmethod
|
|
41
|
+
def remove_doi_url(cls, value: str) -> str:
|
|
42
|
+
"""Remove the URL part of the DOI if it exists."""
|
|
43
|
+
return (
|
|
44
|
+
value.removeprefix("http://doi.org/")
|
|
45
|
+
.removeprefix("https://doi.org/")
|
|
46
|
+
.strip()
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PubMedIdentifier(BaseModel):
|
|
51
|
+
"""An external identifier representing a PubMed ID."""
|
|
52
|
+
|
|
53
|
+
identifier: int = Field(description="The PubMed ID of the reference.")
|
|
54
|
+
identifier_type: Literal[ExternalIdentifierType.PM_ID] = Field(
|
|
55
|
+
ExternalIdentifierType.PM_ID, description="The type of identifier used."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class OpenAlexIdentifier(BaseModel):
|
|
60
|
+
"""An external identifier representing an OpenAlex ID."""
|
|
61
|
+
|
|
62
|
+
identifier: str = Field(
|
|
63
|
+
description="The OpenAlex ID of the reference.", pattern=r"^W\d+$"
|
|
64
|
+
)
|
|
65
|
+
identifier_type: Literal[ExternalIdentifierType.OPEN_ALEX] = Field(
|
|
66
|
+
ExternalIdentifierType.OPEN_ALEX, description="The type of identifier used."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@field_validator("identifier", mode="before")
|
|
70
|
+
@classmethod
|
|
71
|
+
def remove_open_alex_url(cls, value: str) -> str:
|
|
72
|
+
"""Remove the OpenAlex URL if it exists."""
|
|
73
|
+
return (
|
|
74
|
+
value.removeprefix("http://openalex.org/")
|
|
75
|
+
.removeprefix("https://openalex.org/")
|
|
76
|
+
.strip()
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class OtherIdentifier(BaseModel):
|
|
81
|
+
"""An external identifier not otherwise defined by the repository."""
|
|
82
|
+
|
|
83
|
+
identifier: str = Field(description="The identifier of the reference.")
|
|
84
|
+
identifier_type: Literal[ExternalIdentifierType.OTHER] = Field(
|
|
85
|
+
ExternalIdentifierType.OTHER, description="The type of identifier used."
|
|
86
|
+
)
|
|
87
|
+
other_identifier_name: str = Field(
|
|
88
|
+
description="The name of the undocumented identifier type."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
#: Union type for all external identifiers.
|
|
93
|
+
ExternalIdentifier = Annotated[
|
|
94
|
+
DOIIdentifier | PubMedIdentifier | OpenAlexIdentifier | OtherIdentifier,
|
|
95
|
+
Field(discriminator="identifier_type"),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
ExternalIdentifierAdapter: TypeAdapter[ExternalIdentifier] = TypeAdapter(
|
|
99
|
+
ExternalIdentifier
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LinkedExternalIdentifier(BaseModel):
|
|
104
|
+
"""An external identifier which identifies a reference."""
|
|
105
|
+
|
|
106
|
+
identifier: ExternalIdentifier = Field(
|
|
107
|
+
description="The identifier of the reference.",
|
|
108
|
+
discriminator="identifier_type",
|
|
109
|
+
)
|
|
110
|
+
reference_id: UUID4 = Field(
|
|
111
|
+
description="The ID of the reference this identifier identifies."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class IdentifierLookup(BaseModel):
|
|
116
|
+
"""An external identifier lookup."""
|
|
117
|
+
|
|
118
|
+
identifier: str = Field(description="The identifier value.")
|
|
119
|
+
identifier_type: ExternalIdentifierType | None = Field(
|
|
120
|
+
description="The type of identifier used. If not provided, it is assumed to"
|
|
121
|
+
" be a DESTINY identifier.",
|
|
122
|
+
)
|
|
123
|
+
other_identifier_name: str | None = Field(
|
|
124
|
+
default=None,
|
|
125
|
+
description="The name of the undocumented identifier type.",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def serialize(self) -> str:
|
|
129
|
+
"""Serialize the identifier lookup to a string."""
|
|
130
|
+
if self.identifier_type is None:
|
|
131
|
+
return self.identifier
|
|
132
|
+
if self.identifier_type == ExternalIdentifierType.OTHER:
|
|
133
|
+
return f"other:{self.other_identifier_name}:{self.identifier}"
|
|
134
|
+
return f"{self.identifier_type.value.lower()}:{self.identifier}"
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def parse(cls, identifier_lookup_string: str, delimiter: str = ":") -> Self:
|
|
138
|
+
"""Parse an identifier string into an IdentifierLookup."""
|
|
139
|
+
if delimiter not in identifier_lookup_string:
|
|
140
|
+
try:
|
|
141
|
+
UUID4(identifier_lookup_string)
|
|
142
|
+
except ValueError as exc:
|
|
143
|
+
msg = (
|
|
144
|
+
f"Invalid identifier lookup string: {identifier_lookup_string}. "
|
|
145
|
+
"Must be UUIDv4 if no identifier type is specified."
|
|
146
|
+
)
|
|
147
|
+
raise ValueError(msg) from exc
|
|
148
|
+
return cls(
|
|
149
|
+
identifier=identifier_lookup_string,
|
|
150
|
+
identifier_type=None,
|
|
151
|
+
)
|
|
152
|
+
identifier_type, identifier = identifier_lookup_string.split(delimiter, 1)
|
|
153
|
+
if identifier_type == ExternalIdentifierType.OTHER:
|
|
154
|
+
if delimiter not in identifier:
|
|
155
|
+
msg = (
|
|
156
|
+
f"Invalid identifier lookup string: {identifier_lookup_string}. "
|
|
157
|
+
"Other identifier type must include other identifier name."
|
|
158
|
+
)
|
|
159
|
+
raise ValueError(msg)
|
|
160
|
+
other_identifier_type, identifier = identifier.split(delimiter, 1)
|
|
161
|
+
return cls(
|
|
162
|
+
identifier=identifier,
|
|
163
|
+
identifier_type=ExternalIdentifierType.OTHER,
|
|
164
|
+
other_identifier_name=other_identifier_type,
|
|
165
|
+
)
|
|
166
|
+
if identifier_type not in ExternalIdentifierType:
|
|
167
|
+
msg = (
|
|
168
|
+
f"Invalid identifier lookup string: {identifier_lookup_string}. "
|
|
169
|
+
f"Unknown identifier type: {identifier_type}."
|
|
170
|
+
)
|
|
171
|
+
raise ValueError(msg)
|
|
172
|
+
return cls(
|
|
173
|
+
identifier=identifier,
|
|
174
|
+
identifier_type=ExternalIdentifierType(identifier_type),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_identifier(cls, identifier: ExternalIdentifier | UUID4) -> Self:
|
|
179
|
+
"""Create an IdentifierLookup from an ExternalIdentifier or UUID4."""
|
|
180
|
+
if isinstance(identifier, uuid.UUID):
|
|
181
|
+
return cls(identifier=str(identifier), identifier_type=None)
|
|
182
|
+
return cls(
|
|
183
|
+
identifier=str(identifier.identifier),
|
|
184
|
+
identifier_type=identifier.identifier_type,
|
|
185
|
+
other_identifier_name=getattr(identifier, "other_identifier_name", None),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def to_identifier(self) -> ExternalIdentifier | UUID4:
|
|
189
|
+
"""Convert into an ExternalIdentifier or UUID4 if it has no identifier_type."""
|
|
190
|
+
if self.identifier_type is None:
|
|
191
|
+
return UUID4(self.identifier)
|
|
192
|
+
return ExternalIdentifierAdapter.validate_python(self.model_dump())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extended Reference SDK.
|
|
3
|
+
|
|
4
|
+
Extended Reference class for the Destiny SDK
|
|
5
|
+
with added experimental convenience methods and properties.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Generator
|
|
9
|
+
from typing import cast
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from destiny_sdk.enhancements import (
|
|
14
|
+
Annotation,
|
|
15
|
+
AnnotationType,
|
|
16
|
+
BibliographicMetadataEnhancement,
|
|
17
|
+
EnhancementType,
|
|
18
|
+
)
|
|
19
|
+
from destiny_sdk.identifiers import ExternalIdentifierType
|
|
20
|
+
from destiny_sdk.references import Reference
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LabsReference(BaseModel):
|
|
24
|
+
"""Experimental presenter class for Reference with added convenience methods."""
|
|
25
|
+
|
|
26
|
+
reference: Reference = Field(
|
|
27
|
+
...,
|
|
28
|
+
description="The core Reference object",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _get_id(self, identifier_type: ExternalIdentifierType) -> str | int | None:
|
|
32
|
+
"""Fetch an identifier matching the given identifier_type."""
|
|
33
|
+
for identifier in self.reference.identifiers or []:
|
|
34
|
+
if identifier.identifier_type == identifier_type:
|
|
35
|
+
return identifier.identifier
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def openalex_id(self) -> str | None:
|
|
40
|
+
"""Return an OpenAlex ID for the reference."""
|
|
41
|
+
return cast(
|
|
42
|
+
str | None, self._get_id(identifier_type=ExternalIdentifierType.OPEN_ALEX)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def doi(self) -> str | None:
|
|
47
|
+
"""Return a DOI for the reference."""
|
|
48
|
+
return cast(
|
|
49
|
+
str | None, self._get_id(identifier_type=ExternalIdentifierType.DOI)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def pubmed_id(self) -> int | None:
|
|
54
|
+
"""Return a pubmed ID for the reference."""
|
|
55
|
+
return cast(
|
|
56
|
+
int | None, self._get_id(identifier_type=ExternalIdentifierType.PM_ID)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def abstract(self) -> str | None:
|
|
61
|
+
"""Return an abstract for the reference."""
|
|
62
|
+
for enhancement in self.reference.enhancements or []:
|
|
63
|
+
if enhancement.content.enhancement_type == EnhancementType.ABSTRACT:
|
|
64
|
+
return enhancement.content.abstract
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def publication_year(self) -> int | None:
|
|
69
|
+
"""Return a publication year for the reference."""
|
|
70
|
+
for meta in self.it_bibliographics():
|
|
71
|
+
if meta.publication_year is not None:
|
|
72
|
+
return meta.publication_year
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def title(self) -> str | None:
|
|
77
|
+
"""The title of the reference. If multiple are present, return first one."""
|
|
78
|
+
for meta in self.it_bibliographics():
|
|
79
|
+
if meta.title is not None:
|
|
80
|
+
return meta.title
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def it_bibliographics(
|
|
84
|
+
self,
|
|
85
|
+
) -> Generator[BibliographicMetadataEnhancement, None, None]:
|
|
86
|
+
"""Iterate bibliographic enhancements."""
|
|
87
|
+
for enhancement in self.reference.enhancements or []:
|
|
88
|
+
if enhancement.content.enhancement_type == EnhancementType.BIBLIOGRAPHIC:
|
|
89
|
+
yield enhancement.content
|
|
90
|
+
|
|
91
|
+
def it_annotations(
|
|
92
|
+
self,
|
|
93
|
+
source: str | None = None,
|
|
94
|
+
annotation_type: AnnotationType | None = None,
|
|
95
|
+
scheme: str | None = None,
|
|
96
|
+
label: str | None = None,
|
|
97
|
+
) -> Generator[Annotation, None, None]:
|
|
98
|
+
"""
|
|
99
|
+
Iterate annotation enhancements for the given filters.
|
|
100
|
+
|
|
101
|
+
:param source: Optional filter for Enhancement.source
|
|
102
|
+
:param annotation_type: Optional filter for
|
|
103
|
+
AnnotationEnhancement.annotation_type
|
|
104
|
+
:param scheme: Optional filter for Annotation.scheme
|
|
105
|
+
:param label: Optional filter for Annotation.label
|
|
106
|
+
"""
|
|
107
|
+
for enhancement in self.reference.enhancements or []:
|
|
108
|
+
if enhancement.content.enhancement_type == EnhancementType.ANNOTATION:
|
|
109
|
+
if source is not None and enhancement.source != source:
|
|
110
|
+
continue
|
|
111
|
+
for annotation in enhancement.content.annotations:
|
|
112
|
+
if (
|
|
113
|
+
annotation_type is not None
|
|
114
|
+
and annotation.annotation_type != annotation_type
|
|
115
|
+
):
|
|
116
|
+
continue
|
|
117
|
+
if scheme is not None and annotation.scheme != scheme:
|
|
118
|
+
continue
|
|
119
|
+
if label is not None and annotation.label != label:
|
|
120
|
+
continue
|
|
121
|
+
yield annotation
|
|
122
|
+
|
|
123
|
+
def has_bool_annotation(
|
|
124
|
+
self,
|
|
125
|
+
source: str | None = None,
|
|
126
|
+
scheme: str | None = None,
|
|
127
|
+
label: str | None = None,
|
|
128
|
+
expected_value: bool = True, # noqa: FBT001, FBT002
|
|
129
|
+
) -> bool | None:
|
|
130
|
+
"""
|
|
131
|
+
Check if a specific annotation exists and is true.
|
|
132
|
+
|
|
133
|
+
:param source: Optional filter for Enhancement.source
|
|
134
|
+
:param scheme: Optional filter for Annotation.scheme
|
|
135
|
+
:param label: Optional filter for Annotation.label
|
|
136
|
+
:param expected_value: Specify expected boolean annotation value
|
|
137
|
+
:return: Returns the boolean value for the first annotation matching
|
|
138
|
+
the filters or None if nothing is found.
|
|
139
|
+
"""
|
|
140
|
+
if scheme is None and label is None:
|
|
141
|
+
msg = "Please use at least one of the optional scheme or label filters."
|
|
142
|
+
raise AssertionError(msg)
|
|
143
|
+
|
|
144
|
+
found_annotation = False
|
|
145
|
+
for annotation in self.it_annotations(
|
|
146
|
+
source=source,
|
|
147
|
+
annotation_type=AnnotationType.BOOLEAN,
|
|
148
|
+
scheme=scheme,
|
|
149
|
+
label=label,
|
|
150
|
+
):
|
|
151
|
+
if annotation.value == expected_value:
|
|
152
|
+
return True
|
|
153
|
+
found_annotation = True
|
|
154
|
+
return False if found_annotation else None
|
|
@@ -4,7 +4,7 @@ from typing import Self
|
|
|
4
4
|
|
|
5
5
|
from pydantic import UUID4, BaseModel, Field, TypeAdapter
|
|
6
6
|
|
|
7
|
-
from destiny_sdk.core import _JsonlFileInputMixIn
|
|
7
|
+
from destiny_sdk.core import SearchResultMixIn, _JsonlFileInputMixIn
|
|
8
8
|
from destiny_sdk.enhancements import Enhancement, EnhancementFileInput
|
|
9
9
|
from destiny_sdk.identifiers import ExternalIdentifier
|
|
10
10
|
from destiny_sdk.visibility import Visibility
|
|
@@ -65,3 +65,11 @@ class ReferenceFileInput(_JsonlFileInputMixIn, BaseModel):
|
|
|
65
65
|
default=None,
|
|
66
66
|
description="A list of enhancements for the reference",
|
|
67
67
|
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ReferenceSearchResult(SearchResultMixIn, BaseModel):
|
|
71
|
+
"""A search result for references."""
|
|
72
|
+
|
|
73
|
+
references: list[Reference] = Field(
|
|
74
|
+
description="The references returned by the search.",
|
|
75
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Models for search queries and results."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SearchResultTotal(BaseModel):
|
|
7
|
+
"""Information about the total number of search results."""
|
|
8
|
+
|
|
9
|
+
count: int = Field(
|
|
10
|
+
description="The total number of results matching the search criteria.",
|
|
11
|
+
)
|
|
12
|
+
is_lower_bound: bool = Field(
|
|
13
|
+
description="Whether the count is a lower bound (true) or exact (false).",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SearchResultPage(BaseModel):
|
|
18
|
+
"""Information about the page of search results."""
|
|
19
|
+
|
|
20
|
+
count: int = Field(
|
|
21
|
+
description="The number of results on this page.",
|
|
22
|
+
)
|
|
23
|
+
number: int = Field(
|
|
24
|
+
description="The page number of results returned, indexed from 1.",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AnnotationFilter(BaseModel):
|
|
29
|
+
"""An annotation filter for search queries."""
|
|
30
|
+
|
|
31
|
+
scheme: str = Field(
|
|
32
|
+
description="The annotation scheme to filter by.",
|
|
33
|
+
)
|
|
34
|
+
label: str | None = Field(
|
|
35
|
+
None,
|
|
36
|
+
description="The annotation label to filter by.",
|
|
37
|
+
)
|
|
38
|
+
score: float | None = Field(
|
|
39
|
+
None,
|
|
40
|
+
description="The minimum score for the annotation filter.",
|
|
41
|
+
ge=0.0,
|
|
42
|
+
le=1.0,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def serialize(self) -> str:
|
|
46
|
+
"""Serialize the annotation filter to a string."""
|
|
47
|
+
annotation = self.scheme
|
|
48
|
+
if self.label:
|
|
49
|
+
annotation += f"/{self.label}"
|
|
50
|
+
if self.score is not None:
|
|
51
|
+
annotation += f"@{self.score}"
|
|
52
|
+
return annotation
|