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.
Files changed (38) hide show
  1. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/.gitignore +9 -0
  2. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/PKG-INFO +10 -2
  3. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/README.md +7 -0
  4. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/pyproject.toml +5 -1
  5. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/__init__.py +2 -0
  6. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/core.py +14 -1
  7. destiny_sdk-0.6.0/src/destiny_sdk/identifiers.py +192 -0
  8. destiny_sdk-0.6.0/src/destiny_sdk/labs/__init__.py +10 -0
  9. destiny_sdk-0.6.0/src/destiny_sdk/labs/references.py +154 -0
  10. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/references.py +9 -1
  11. destiny_sdk-0.6.0/src/destiny_sdk/search.py +52 -0
  12. destiny_sdk-0.6.0/tests/unit/labs/test_references.py +144 -0
  13. destiny_sdk-0.6.0/tests/unit/test_data/destiny_references.jsonl +10 -0
  14. destiny_sdk-0.6.0/tests/unit/test_identifiers.py +320 -0
  15. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/uv.lock +2 -1
  16. destiny_sdk-0.5.0/src/destiny_sdk/identifiers.py +0 -107
  17. destiny_sdk-0.5.0/tests/unit/test_identifiers.py +0 -104
  18. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/LICENSE +0 -0
  19. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/auth.py +0 -0
  20. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/client.py +0 -0
  21. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/enhancements.py +0 -0
  22. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/imports.py +0 -0
  23. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/__init__.py +0 -0
  24. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/eppi_parser.py +0 -0
  25. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/py.typed +0 -0
  26. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/robots.py +0 -0
  27. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/src/destiny_sdk/visibility.py +0 -0
  28. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/__init__.py +0 -0
  29. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/conftest.py +0 -0
  30. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/parsers/test_eppi_parser.py +0 -0
  31. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_auth.py +0 -0
  32. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_client.py +0 -0
  33. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import.jsonl +0 -0
  34. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import_with_annotations.jsonl +0 -0
  35. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_report.json +0 -0
  36. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_enhancements.py +0 -0
  37. {destiny_sdk-0.5.0 → destiny_sdk-0.6.0}/tests/unit/test_references.py +0 -0
  38. {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.5.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.5.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"]
@@ -8,6 +8,7 @@ from . import (
8
8
  imports,
9
9
  references,
10
10
  robots,
11
+ search,
11
12
  visibility,
12
13
  )
13
14
 
@@ -19,5 +20,6 @@ __all__ = [
19
20
  "imports",
20
21
  "references",
21
22
  "robots",
23
+ "search",
22
24
  "visibility",
23
25
  ]
@@ -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,10 @@
1
+ """
2
+ Experimental DESTINY SDK.
3
+
4
+ The DESTINY SDK-labs provides experimental features
5
+ for interacting with DESTINY repository.
6
+ """
7
+
8
+ from . import references
9
+
10
+ __all__ = ["references"]
@@ -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