destiny_sdk 0.2.0.post0__tar.gz → 0.2.2__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.2.2/.gitignore +199 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/PKG-INFO +21 -26
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/README.md +7 -7
- destiny_sdk-0.2.2/pyproject.toml +43 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/enhancements.py +2 -2
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/identifiers.py +10 -7
- destiny_sdk-0.2.2/src/destiny_sdk/parsers/__init__.py +5 -0
- destiny_sdk-0.2.2/src/destiny_sdk/parsers/eppi_parser.py +173 -0
- destiny_sdk-0.2.2/tests/unit/__init__.py +0 -0
- destiny_sdk-0.2.2/tests/unit/conftest.py +15 -0
- destiny_sdk-0.2.2/tests/unit/parsers/test_eppi_parser.py +47 -0
- destiny_sdk-0.2.2/tests/unit/test_auth.py +195 -0
- destiny_sdk-0.2.2/tests/unit/test_client.py +66 -0
- destiny_sdk-0.2.2/tests/unit/test_data/eppi_import.jsonl +4 -0
- destiny_sdk-0.2.2/tests/unit/test_data/eppi_import_with_annotations.jsonl +4 -0
- destiny_sdk-0.2.2/tests/unit/test_data/eppi_report.json +41 -0
- destiny_sdk-0.2.2/tests/unit/test_enhancements.py +126 -0
- destiny_sdk-0.2.2/tests/unit/test_identifiers.py +104 -0
- destiny_sdk-0.2.2/tests/unit/test_references.py +85 -0
- destiny_sdk-0.2.2/tests/unit/test_robots.py +61 -0
- destiny_sdk-0.2.2/uv.lock +591 -0
- destiny_sdk-0.2.0.post0/pyproject.toml +0 -41
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/LICENSE +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/__init__.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/auth.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/client.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/core.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/imports.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/py.typed +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/references.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/robots.py +0 -0
- {destiny_sdk-0.2.0.post0 → destiny_sdk-0.2.2}/src/destiny_sdk/visibility.py +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
docs/html/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
#uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
#poetry.lock
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
#pdm.lock
|
|
114
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
115
|
+
# in version control.
|
|
116
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
117
|
+
.pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
122
|
+
__pypackages__/
|
|
123
|
+
|
|
124
|
+
# Celery stuff
|
|
125
|
+
celerybeat-schedule
|
|
126
|
+
celerybeat.pid
|
|
127
|
+
|
|
128
|
+
# SageMath parsed files
|
|
129
|
+
*.sage.py
|
|
130
|
+
|
|
131
|
+
# Environments
|
|
132
|
+
.env
|
|
133
|
+
.venv
|
|
134
|
+
env/
|
|
135
|
+
venv/
|
|
136
|
+
ENV/
|
|
137
|
+
env.bak/
|
|
138
|
+
venv.bak/
|
|
139
|
+
cli/.env.*
|
|
140
|
+
!/cli/.env.example
|
|
141
|
+
|
|
142
|
+
# Spyder project settings
|
|
143
|
+
.spyderproject
|
|
144
|
+
.spyproject
|
|
145
|
+
|
|
146
|
+
# Rope project settings
|
|
147
|
+
.ropeproject
|
|
148
|
+
|
|
149
|
+
# mkdocs documentation
|
|
150
|
+
/site
|
|
151
|
+
|
|
152
|
+
# mypy
|
|
153
|
+
.mypy_cache/
|
|
154
|
+
.dmypy.json
|
|
155
|
+
dmypy.json
|
|
156
|
+
|
|
157
|
+
# ruff
|
|
158
|
+
.ruff_cache/
|
|
159
|
+
|
|
160
|
+
# Pyre type checker
|
|
161
|
+
.pyre/
|
|
162
|
+
|
|
163
|
+
# pytype static type analyzer
|
|
164
|
+
.pytype/
|
|
165
|
+
|
|
166
|
+
# Cython debug symbols
|
|
167
|
+
cython_debug/
|
|
168
|
+
|
|
169
|
+
# PyCharm
|
|
170
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
171
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
172
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
173
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
174
|
+
#.idea/
|
|
175
|
+
|
|
176
|
+
# PyPI configuration file
|
|
177
|
+
.pypirc
|
|
178
|
+
|
|
179
|
+
# Terraform
|
|
180
|
+
.terraform/
|
|
181
|
+
|
|
182
|
+
# VS Code Workspace settings
|
|
183
|
+
*.code-workspace
|
|
184
|
+
.vscode/
|
|
185
|
+
|
|
186
|
+
# Mac Finder config
|
|
187
|
+
.DS_Store
|
|
188
|
+
|
|
189
|
+
# MinIO presigned URLs
|
|
190
|
+
.minio/presigned_urls.json
|
|
191
|
+
|
|
192
|
+
# delete-me working files
|
|
193
|
+
tmp-scripts.sh
|
|
194
|
+
infra/app/*.py
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
.certs
|
|
198
|
+
|
|
199
|
+
libs/fake_data/*.jsonl
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: destiny_sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A software development kit (sdk) to support interaction with the DESTINY repository
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Requires-Python:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Requires-Dist:
|
|
14
|
-
Requires-Dist:
|
|
15
|
-
Requires-Dist:
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist: pytest (>=8.4.0,<9.0.0)
|
|
18
|
-
Requires-Dist: pytest-asyncio (>=1.0.0,<2.0.0)
|
|
19
|
-
Requires-Dist: pytest-httpx (>=0.35.0,<0.36.0)
|
|
20
|
-
Requires-Dist: python-jose (>=3.4.0,<4.0.0)
|
|
5
|
+
Author-email: Adam Hamilton <adam@futureevidence.org>, Andrew Harvey <andrew@futureevidence.org>, Daniel Breves <daniel@futureevidence.org>, Jack Walmisley <jack@futureevidence.org>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: ~=3.12
|
|
9
|
+
Requires-Dist: cachetools<6,>=5.5.2
|
|
10
|
+
Requires-Dist: fastapi<0.116,>=0.115.12
|
|
11
|
+
Requires-Dist: httpx<0.29,>=0.28.1
|
|
12
|
+
Requires-Dist: pydantic<3,>=2.11.3
|
|
13
|
+
Requires-Dist: pytest-asyncio<2,>=1.0.0
|
|
14
|
+
Requires-Dist: pytest-httpx<0.36,>=0.35.0
|
|
15
|
+
Requires-Dist: pytest<9,>=8.4.0
|
|
16
|
+
Requires-Dist: python-jose<4,>=3.4.0
|
|
21
17
|
Description-Content-Type: text/markdown
|
|
22
18
|
|
|
23
19
|
# DESTINY SDK
|
|
@@ -35,7 +31,7 @@ pip install destiny-sdk
|
|
|
35
31
|
```
|
|
36
32
|
|
|
37
33
|
```sh
|
|
38
|
-
|
|
34
|
+
uv add destiny-sdk
|
|
39
35
|
```
|
|
40
36
|
|
|
41
37
|
## Development
|
|
@@ -43,21 +39,21 @@ poetry add destiny-sdk
|
|
|
43
39
|
### Dependencies
|
|
44
40
|
|
|
45
41
|
```sh
|
|
46
|
-
|
|
42
|
+
uv install
|
|
47
43
|
```
|
|
48
44
|
|
|
49
45
|
### Tests
|
|
50
46
|
|
|
51
47
|
```sh
|
|
52
|
-
|
|
48
|
+
uv run pytest
|
|
53
49
|
```
|
|
54
50
|
|
|
55
51
|
### Installing as an editable package of another project
|
|
56
52
|
|
|
57
|
-
Run the following command in the root folder of the other project (assuming
|
|
53
|
+
Run the following command in the root folder of the other project (assuming uv as a packaging framework). Pip also has an `--editable` option that you can use.
|
|
58
54
|
|
|
59
55
|
```sh
|
|
60
|
-
|
|
56
|
+
uv add --editable ./PATH/TO/sdk/
|
|
61
57
|
```
|
|
62
58
|
|
|
63
59
|
or replace the dependency in `pyproject.toml` with
|
|
@@ -71,8 +67,8 @@ destiny-sdk = {path = "./PATH/TO/sdk/", develop = true}
|
|
|
71
67
|
If you want to use a local build of the sdk `z.whl`, do
|
|
72
68
|
|
|
73
69
|
```sh
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
uv build
|
|
71
|
+
uv add ./PATH/TO/WHEEL.whl
|
|
76
72
|
```
|
|
77
73
|
|
|
78
74
|
### Publishing
|
|
@@ -88,4 +84,3 @@ Given a version number `MAJOR.MINOR.PATCH`, increment the:
|
|
|
88
84
|
- `MAJOR` version when you make incompatible API change
|
|
89
85
|
- `MINOR` version when you add functionality in a backward compatible manner
|
|
90
86
|
- `PATCH` version when you make backward compatible bug fixes
|
|
91
|
-
|
|
@@ -13,7 +13,7 @@ pip install destiny-sdk
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
|
|
16
|
+
uv add destiny-sdk
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Development
|
|
@@ -21,21 +21,21 @@ poetry add destiny-sdk
|
|
|
21
21
|
### Dependencies
|
|
22
22
|
|
|
23
23
|
```sh
|
|
24
|
-
|
|
24
|
+
uv install
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
### Tests
|
|
28
28
|
|
|
29
29
|
```sh
|
|
30
|
-
|
|
30
|
+
uv run pytest
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
### Installing as an editable package of another project
|
|
34
34
|
|
|
35
|
-
Run the following command in the root folder of the other project (assuming
|
|
35
|
+
Run the following command in the root folder of the other project (assuming uv as a packaging framework). Pip also has an `--editable` option that you can use.
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
|
|
38
|
+
uv add --editable ./PATH/TO/sdk/
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
or replace the dependency in `pyproject.toml` with
|
|
@@ -49,8 +49,8 @@ destiny-sdk = {path = "./PATH/TO/sdk/", develop = true}
|
|
|
49
49
|
If you want to use a local build of the sdk `z.whl`, do
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
uv build
|
|
53
|
+
uv add ./PATH/TO/WHEEL.whl
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
### Publishing
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "hatchling.build"
|
|
3
|
+
requires = ["hatchling"]
|
|
4
|
+
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
dev = [
|
|
7
|
+
"mypy>=1.15.0,<2",
|
|
8
|
+
"pre-commit>=4.2.0,<5",
|
|
9
|
+
"pytest-env>=1.1.5,<2",
|
|
10
|
+
"ruff>=0.11.5,<0.12",
|
|
11
|
+
"uvloop>=0.21.0,<0.22",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project]
|
|
15
|
+
authors = [
|
|
16
|
+
{email = "adam@futureevidence.org", name = "Adam Hamilton"},
|
|
17
|
+
{email = "andrew@futureevidence.org", name = "Andrew Harvey"},
|
|
18
|
+
{email = "daniel@futureevidence.org", name = "Daniel Breves"},
|
|
19
|
+
{email = "jack@futureevidence.org", name = "Jack Walmisley"},
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"cachetools>=5.5.2,<6",
|
|
23
|
+
"fastapi>=0.115.12,<0.116",
|
|
24
|
+
"httpx>=0.28.1,<0.29",
|
|
25
|
+
"pydantic>=2.11.3,<3",
|
|
26
|
+
"pytest-asyncio>=1.0.0,<2",
|
|
27
|
+
"pytest-httpx>=0.35.0,<0.36",
|
|
28
|
+
"pytest>=8.4.0,<9",
|
|
29
|
+
"python-jose>=3.4.0,<4",
|
|
30
|
+
]
|
|
31
|
+
description = "A software development kit (sdk) to support interaction with the DESTINY repository"
|
|
32
|
+
license = "Apache-2.0"
|
|
33
|
+
name = "destiny_sdk"
|
|
34
|
+
readme = "README.md"
|
|
35
|
+
requires-python = "~=3.12"
|
|
36
|
+
version = "0.2.2"
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
addopts = ["--color=yes", "--import-mode=importlib", "--verbose"]
|
|
40
|
+
env = ["ENV=test"]
|
|
41
|
+
norecursedirs = ["tests/e2e"]
|
|
42
|
+
pythonpath = "."
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -58,7 +58,7 @@ class Authorship(BaseModel):
|
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
60
|
display_name: str = Field(description="The display name of the author.")
|
|
61
|
-
orcid: str = Field(description="The ORCid of the author.")
|
|
61
|
+
orcid: str | None = Field(default=None, description="The ORCid of the author.")
|
|
62
62
|
position: AuthorPosition = Field(
|
|
63
63
|
description="The position of the author within the list of authors."
|
|
64
64
|
)
|
|
@@ -105,7 +105,7 @@ other works have cited this work
|
|
|
105
105
|
|
|
106
106
|
class AbstractProcessType(StrEnum):
|
|
107
107
|
"""
|
|
108
|
-
The process used to
|
|
108
|
+
The process used to acquire the abstract.
|
|
109
109
|
|
|
110
110
|
**Allowed values**:
|
|
111
111
|
- `uninverted`
|
|
@@ -27,13 +27,6 @@ class ExternalIdentifierType(StrEnum):
|
|
|
27
27
|
OTHER = auto()
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def remove_doi_url(value: str) -> str:
|
|
31
|
-
"""Remove the URL part of the DOI if it exists."""
|
|
32
|
-
return (
|
|
33
|
-
value.removeprefix("http://doi.org/").removeprefix("https://doi.org/").strip()
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
30
|
class DOIIdentifier(BaseModel):
|
|
38
31
|
"""An external identifier representing a DOI."""
|
|
39
32
|
|
|
@@ -75,6 +68,16 @@ class OpenAlexIdentifier(BaseModel):
|
|
|
75
68
|
ExternalIdentifierType.OPEN_ALEX, description="The type of identifier used."
|
|
76
69
|
)
|
|
77
70
|
|
|
71
|
+
@field_validator("identifier", mode="before")
|
|
72
|
+
@classmethod
|
|
73
|
+
def remove_open_alex_url(cls, value: str) -> str:
|
|
74
|
+
"""Remove the OpenAlex URL if it exists."""
|
|
75
|
+
return (
|
|
76
|
+
value.removeprefix("http://openalex.org/")
|
|
77
|
+
.removeprefix("https://openalex.org/")
|
|
78
|
+
.strip()
|
|
79
|
+
)
|
|
80
|
+
|
|
78
81
|
|
|
79
82
|
class OtherIdentifier(BaseModel):
|
|
80
83
|
"""An external identifier not otherwise defined by the repository."""
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Parser for a EPPI JSON export file."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from destiny_sdk.enhancements import (
|
|
6
|
+
AbstractContentEnhancement,
|
|
7
|
+
AbstractProcessType,
|
|
8
|
+
AnnotationEnhancement,
|
|
9
|
+
AnnotationType,
|
|
10
|
+
AuthorPosition,
|
|
11
|
+
Authorship,
|
|
12
|
+
BibliographicMetadataEnhancement,
|
|
13
|
+
BooleanAnnotation,
|
|
14
|
+
EnhancementContent,
|
|
15
|
+
EnhancementFileInput,
|
|
16
|
+
)
|
|
17
|
+
from destiny_sdk.identifiers import (
|
|
18
|
+
DOIIdentifier,
|
|
19
|
+
ExternalIdentifier,
|
|
20
|
+
ExternalIdentifierType,
|
|
21
|
+
)
|
|
22
|
+
from destiny_sdk.references import ReferenceFileInput
|
|
23
|
+
from destiny_sdk.visibility import Visibility
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EPPIParser:
|
|
27
|
+
"""
|
|
28
|
+
Parser for an EPPI JSON export file.
|
|
29
|
+
|
|
30
|
+
See example here: https://eppi.ioe.ac.uk/cms/Portals/35/Maps/Examples/example_orignal.json
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
version = "1.0"
|
|
34
|
+
|
|
35
|
+
def __init__(self, tags: list[str] | None = None) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Initialize the EPPIParser with optional tags.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tags (list[str] | None): Optional list of tags to annotate references.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
self.tags = tags or []
|
|
44
|
+
self.parser_source = f"destiny_sdk.eppi_parser@{self.version}"
|
|
45
|
+
|
|
46
|
+
def _parse_identifiers(
|
|
47
|
+
self, ref_to_import: dict[str, Any]
|
|
48
|
+
) -> list[ExternalIdentifier]:
|
|
49
|
+
identifiers = []
|
|
50
|
+
if doi := ref_to_import.get("DOI"):
|
|
51
|
+
identifiers.append(
|
|
52
|
+
DOIIdentifier(
|
|
53
|
+
identifier=doi,
|
|
54
|
+
identifier_type=ExternalIdentifierType.DOI,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
return identifiers
|
|
58
|
+
|
|
59
|
+
def _parse_abstract_enhancement(
|
|
60
|
+
self, ref_to_import: dict[str, Any]
|
|
61
|
+
) -> EnhancementContent | None:
|
|
62
|
+
if abstract := ref_to_import.get("Abstract"):
|
|
63
|
+
return AbstractContentEnhancement(
|
|
64
|
+
process=AbstractProcessType.OTHER,
|
|
65
|
+
abstract=abstract,
|
|
66
|
+
)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def _parse_bibliographic_enhancement(
|
|
70
|
+
self, ref_to_import: dict[str, Any]
|
|
71
|
+
) -> EnhancementContent | None:
|
|
72
|
+
title = ref_to_import.get("Title")
|
|
73
|
+
publication_year = (
|
|
74
|
+
int(year)
|
|
75
|
+
if (year := ref_to_import.get("Year")) and year.isdigit()
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
publisher = ref_to_import.get("Publisher")
|
|
79
|
+
authors_string = ref_to_import.get("Authors")
|
|
80
|
+
|
|
81
|
+
authorships = []
|
|
82
|
+
if authors_string:
|
|
83
|
+
authors = [
|
|
84
|
+
author.strip() for author in authors_string.split(";") if author.strip()
|
|
85
|
+
]
|
|
86
|
+
for i, author_name in enumerate(authors):
|
|
87
|
+
position = AuthorPosition.MIDDLE
|
|
88
|
+
if i == 0:
|
|
89
|
+
position = AuthorPosition.FIRST
|
|
90
|
+
if i == len(authors) - 1 and i > 0:
|
|
91
|
+
position = AuthorPosition.LAST
|
|
92
|
+
|
|
93
|
+
authorships.append(
|
|
94
|
+
Authorship(
|
|
95
|
+
display_name=author_name,
|
|
96
|
+
position=position,
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if not title and not publication_year and not publisher and not authorships:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
return BibliographicMetadataEnhancement(
|
|
104
|
+
title=title,
|
|
105
|
+
publication_year=publication_year,
|
|
106
|
+
publisher=publisher,
|
|
107
|
+
authorship=authorships if authorships else None,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def _create_annotation_enhancement(self) -> EnhancementContent | None:
|
|
111
|
+
if not self.tags:
|
|
112
|
+
return None
|
|
113
|
+
annotations = [
|
|
114
|
+
BooleanAnnotation(
|
|
115
|
+
annotation_type=AnnotationType.BOOLEAN,
|
|
116
|
+
scheme=self.parser_source,
|
|
117
|
+
label=tag,
|
|
118
|
+
value=True,
|
|
119
|
+
)
|
|
120
|
+
for tag in self.tags
|
|
121
|
+
]
|
|
122
|
+
return AnnotationEnhancement(
|
|
123
|
+
annotations=annotations,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def parse_data(
|
|
127
|
+
self, data: dict, source: str | None = None, robot_version: str | None = None
|
|
128
|
+
) -> list[ReferenceFileInput]:
|
|
129
|
+
"""
|
|
130
|
+
Parse an EPPI JSON export dict and return a list of ReferenceFileInput objects.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
data (dict): Parsed EPPI JSON export data.
|
|
134
|
+
source (str | None): Optional source string for deduplication/provenance.
|
|
135
|
+
robot_version (str | None): Optional robot version string for provenance.
|
|
136
|
+
Defaults to parser version.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
list[ReferenceFileInput]: List of parsed references from the data.
|
|
140
|
+
|
|
141
|
+
"""
|
|
142
|
+
parser_source = source if source is not None else self.parser_source
|
|
143
|
+
references = []
|
|
144
|
+
for ref_to_import in data.get("References", []):
|
|
145
|
+
enhancement_contents = [
|
|
146
|
+
content
|
|
147
|
+
for content in [
|
|
148
|
+
self._parse_abstract_enhancement(ref_to_import),
|
|
149
|
+
self._parse_bibliographic_enhancement(ref_to_import),
|
|
150
|
+
self._create_annotation_enhancement(),
|
|
151
|
+
]
|
|
152
|
+
if content
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
enhancements = [
|
|
156
|
+
EnhancementFileInput(
|
|
157
|
+
source=parser_source,
|
|
158
|
+
visibility=Visibility.PUBLIC,
|
|
159
|
+
content=content,
|
|
160
|
+
enhancement_type=content.enhancement_type,
|
|
161
|
+
robot_version=robot_version,
|
|
162
|
+
)
|
|
163
|
+
for content in enhancement_contents
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
references.append(
|
|
167
|
+
ReferenceFileInput(
|
|
168
|
+
visibility=Visibility.PUBLIC,
|
|
169
|
+
identifiers=self._parse_identifiers(ref_to_import),
|
|
170
|
+
enhancements=enhancements,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
return references
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
MIGRATION_TASK: asyncio.Task | None = None
|
|
8
|
+
|
|
9
|
+
logging.getLogger("asyncio").setLevel("DEBUG")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
13
|
+
def anyio_backend() -> tuple[str, dict[str, Any]]:
|
|
14
|
+
"""Specify the anyio backend for async tests."""
|
|
15
|
+
return "asyncio", {"use_uvloop": True}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Tests for the EPPI parser."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from destiny_sdk.parsers.eppi_parser import EPPIParser
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_parse_data():
|
|
9
|
+
"""Test that the parse_data method returns the expected output."""
|
|
10
|
+
test_data_path = Path(__file__).parent.parent / "test_data"
|
|
11
|
+
input_path = test_data_path / "eppi_report.json"
|
|
12
|
+
output_path = test_data_path / "eppi_import.jsonl"
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
parser = EPPIParser()
|
|
17
|
+
with input_path.open() as f:
|
|
18
|
+
data = json.load(f)
|
|
19
|
+
references = parser.parse_data(data, robot_version="test-robot-version")
|
|
20
|
+
|
|
21
|
+
with output_path.open() as f:
|
|
22
|
+
expected_output = f.read()
|
|
23
|
+
|
|
24
|
+
actual_output = "".join([ref.to_jsonl() + "\n" for ref in references])
|
|
25
|
+
|
|
26
|
+
assert actual_output == expected_output
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_parse_data_with_annotations():
|
|
30
|
+
"""Test that the parse_data method returns the output with annotations."""
|
|
31
|
+
test_data_path = Path(__file__).parent.parent / "test_data"
|
|
32
|
+
input_path = test_data_path / "eppi_report.json"
|
|
33
|
+
output_path = test_data_path / "eppi_import_with_annotations.jsonl"
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
|
|
37
|
+
parser = EPPIParser(tags=["test-tag", "another-tag"])
|
|
38
|
+
with input_path.open() as f:
|
|
39
|
+
data = json.load(f)
|
|
40
|
+
references = parser.parse_data(data, robot_version="test-robot-version")
|
|
41
|
+
|
|
42
|
+
with output_path.open() as f:
|
|
43
|
+
expected_output = f.read()
|
|
44
|
+
|
|
45
|
+
actual_output = "".join([ref.to_jsonl() + "\n" for ref in references])
|
|
46
|
+
|
|
47
|
+
assert actual_output == expected_output
|