destiny_sdk 0.5.1__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 (36) hide show
  1. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/.gitignore +5 -0
  2. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/PKG-INFO +10 -2
  3. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/README.md +7 -0
  4. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/pyproject.toml +5 -1
  5. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/__init__.py +2 -0
  6. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/core.py +14 -1
  7. destiny_sdk-0.6.0/src/destiny_sdk/labs/__init__.py +10 -0
  8. destiny_sdk-0.6.0/src/destiny_sdk/labs/references.py +154 -0
  9. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/references.py +9 -1
  10. destiny_sdk-0.6.0/src/destiny_sdk/search.py +52 -0
  11. destiny_sdk-0.6.0/tests/unit/labs/test_references.py +144 -0
  12. destiny_sdk-0.6.0/tests/unit/test_data/destiny_references.jsonl +10 -0
  13. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/uv.lock +2 -1
  14. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/LICENSE +0 -0
  15. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/auth.py +0 -0
  16. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/client.py +0 -0
  17. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/enhancements.py +0 -0
  18. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/identifiers.py +0 -0
  19. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/imports.py +0 -0
  20. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/__init__.py +0 -0
  21. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/parsers/eppi_parser.py +0 -0
  22. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/py.typed +0 -0
  23. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/robots.py +0 -0
  24. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/src/destiny_sdk/visibility.py +0 -0
  25. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/__init__.py +0 -0
  26. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/conftest.py +0 -0
  27. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/parsers/test_eppi_parser.py +0 -0
  28. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_auth.py +0 -0
  29. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_client.py +0 -0
  30. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import.jsonl +0 -0
  31. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_import_with_annotations.jsonl +0 -0
  32. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_data/eppi_report.json +0 -0
  33. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_enhancements.py +0 -0
  34. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_identifiers.py +0 -0
  35. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_references.py +0 -0
  36. {destiny_sdk-0.5.1 → destiny_sdk-0.6.0}/tests/unit/test_robots.py +0 -0
@@ -198,3 +198,8 @@ infra/app/*.py
198
198
  .certs
199
199
 
200
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.1
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.1"
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,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
@@ -0,0 +1,144 @@
1
+ """Tests for the experimental Reference class."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from destiny_sdk.labs.references import LabsReference
7
+ from destiny_sdk.references import Reference
8
+
9
+
10
+ def _read_references():
11
+ test_data_path = Path(__file__).parent.parent / "test_data/destiny_references.jsonl"
12
+ with test_data_path.open() as test_references_file:
13
+ for line in test_references_file:
14
+ yield LabsReference(reference=Reference.from_es(json.loads(line)))
15
+
16
+
17
+ def test_annotations():
18
+ """Test that the parse_data method returns the expected output."""
19
+
20
+ expected = [
21
+ (
22
+ {"scheme": "inclusion:destiny"},
23
+ [True, None, True, False, False, True, True, True, False, True],
24
+ ),
25
+ (
26
+ {"scheme": "inclusion:destiny", "label": "Included in DESTINY domain"},
27
+ [True, None, True, False, False, True, True, True, False, True],
28
+ ),
29
+ (
30
+ {
31
+ "scheme": "inclusion:destiny",
32
+ "label": "Included in DESTINY domain",
33
+ "expected_value": False,
34
+ },
35
+ [False, None, False, True, True, False, False, False, True, False],
36
+ ),
37
+ (
38
+ {
39
+ "scheme": "classifier:taxonomy:Context",
40
+ "label": "Coastal socioecological systems",
41
+ },
42
+ [None, None, None, None, None, False, False, False, False, False],
43
+ ),
44
+ (
45
+ {"scheme": "classifier:taxonomy:Context"},
46
+ [None, None, None, None, None, True, True, True, True, True],
47
+ ),
48
+ (
49
+ {"scheme": "classifier:taxonomy:Context", "expected_value": False},
50
+ [None, None, None, None, None, True, True, True, True, True],
51
+ ),
52
+ ]
53
+
54
+ for params, expected_values in expected:
55
+ for ref, expected_value in zip(
56
+ _read_references(), expected_values, strict=False
57
+ ):
58
+ assert expected_value == ref.has_bool_annotation(**params)
59
+
60
+
61
+ def test_titles():
62
+ """Test that the parse_data method returns the expected output."""
63
+ expected = [
64
+ "Makro Ekonomik Zihin Teori, Politika, Dinamikler",
65
+ "Analysis of parental beliefs and practices leading to excessive screen "
66
+ "time in early childhood",
67
+ "Exploring the link between A Body Shape Index and abdominal aortic "
68
+ "calcification in chronic kidney disease: a cross-sectional analysis from "
69
+ "2013–2014 National Health and Nutrition Examination Survey", # noqa: RUF001
70
+ "Microbiological Evaluation of Biodegradation Processes of Solid Waste in "
71
+ "Reclaimed Landfills",
72
+ "Wave-Structure Interactions of a Floating FPSO-Shaped Body",
73
+ "Function and public awareness of sustainable development and population "
74
+ "health projects in Montreal, Canada: a logic model and survey of the "
75
+ "Quartiers 21 Program",
76
+ "NGOs’ responses to the challenges faced by orphans and " # noqa: RUF001
77
+ "vulnerable children (OVC) in Chegutu, Zimbabwe",
78
+ "Climate change-related health hazards in daycare centers in Munich, Germany: "
79
+ "risk perception and adaptation measures",
80
+ None,
81
+ "Association of Placenta Praevia with Previous Cesarean Section in "
82
+ "Rajshahi Medical College Hospital",
83
+ ]
84
+ for ref, expected_value in zip(_read_references(), expected, strict=False):
85
+ assert expected_value == ref.title
86
+
87
+
88
+ def test_biblio():
89
+ """Test that the parse_data method returns the expected output."""
90
+ expected = {
91
+ "doi": [
92
+ "10.58830/ozgur.pub782",
93
+ None,
94
+ "10.1080/0886022x.2025.2517403",
95
+ "10.54740/ros.2025.028",
96
+ "10.31814/stce.huce2025-19(2)-11",
97
+ "10.1016/s0140-6736(14)61878-x",
98
+ "10.1111/issj.12473",
99
+ "10.1007/s10113-023-02136-w",
100
+ "10.3390/diseases11040157",
101
+ "10.36347/sasjs.2023.v09i10.007",
102
+ ],
103
+ "publication_year": [
104
+ 2025,
105
+ None,
106
+ 2025,
107
+ 2025,
108
+ 2025,
109
+ None,
110
+ 2023,
111
+ 2023,
112
+ 2023,
113
+ None,
114
+ ],
115
+ "openalex_id": [
116
+ "W4411634280",
117
+ "W4411634320",
118
+ "W4411634759",
119
+ "W4411698874",
120
+ "W4411698892",
121
+ "W1965074043",
122
+ "W4388230581",
123
+ "W4388231242",
124
+ "W4388231278",
125
+ "W4388232129",
126
+ ],
127
+ "pubmed_id": [
128
+ None,
129
+ 40562693,
130
+ 40562394,
131
+ None,
132
+ None,
133
+ None,
134
+ None,
135
+ None,
136
+ 37987268,
137
+ None,
138
+ ],
139
+ }
140
+ for prop, expected_values in expected.items():
141
+ for ref, expected_value in zip(
142
+ _read_references(), expected_values, strict=False
143
+ ):
144
+ assert expected_value == getattr(ref, prop)