biosero-data-services-sdk 0.1.1__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.
- biosero_data_services_sdk-0.1.1/PKG-INFO +46 -0
- biosero_data_services_sdk-0.1.1/README.md +31 -0
- biosero_data_services_sdk-0.1.1/pyproject.toml +61 -0
- biosero_data_services_sdk-0.1.1/setup.cfg +4 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/__init__.py +156 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/constants.py +17 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/events_sdk.py +100 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/exceptions.py +119 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/identities_sdk.py +70 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/locations_sdk.py +118 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/logger_config.py +131 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/__init__.py +28 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/base.py +25 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/container.py +247 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/event.py +185 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/identity.py +259 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/order.py +51 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/order_template.py +179 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/transportation.py +117 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/orders_sdk.py +90 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/py.typed +0 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/reports.py +210 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/sdk.py +291 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/sdk_base.py +342 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/transportation_sdk.py +101 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/types.py +12 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/PKG-INFO +46 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/SOURCES.txt +29 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/dependency_links.txt +1 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/requires.txt +4 -0
- biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: biosero-data-services-sdk
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: SDK for interacting with the Biosero Data Services API
|
|
5
|
+
Author: Eli Fine
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Requires-Python: >=3.12.7
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: pydantic>=2.11.7
|
|
12
|
+
Requires-Dist: structlog>=25.3.0
|
|
13
|
+
Requires-Dist: nested-diff>=1.8.0
|
|
14
|
+
Requires-Dist: requests>=2.32.4
|
|
15
|
+
|
|
16
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
17
|
+
[](https://github.com/astral-sh/ruff)
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
[](https://microsoft.github.io/pyright/)
|
|
20
|
+
[](https://github.com/lab-sync/biosero-data-services-sdk/actions)
|
|
21
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk)
|
|
22
|
+
[](https://pypi.org/project/biosero-data-services-sdk/)
|
|
23
|
+
[](https://pepy.tech/project/biosero-data-services-sdk)
|
|
24
|
+
[](https://pypi.org/project/biosero-data-services-sdk/)
|
|
25
|
+
[](https://codecov.io/gh/lab-sync/biosero-data-services-sdk)
|
|
26
|
+
|
|
27
|
+
# Usage
|
|
28
|
+
Documentation is hosted on [ReadTheDocs](https://biosero-data-services-sdk.readthedocs.io/en/latest/?badge=latest).
|
|
29
|
+
|
|
30
|
+
# Development
|
|
31
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
32
|
+
|
|
33
|
+
To publish a new version of the repository, you can run the `Publish` workflow manually and publish to the staging registry from any branch, and you can check the 'Publish to Primary' option when on `main` to publish to the primary registry and create a git tag.
|
|
34
|
+
|
|
35
|
+
Open a connection to the EC2 instance hosting GBG Data Services in AWS:
|
|
36
|
+
```bash
|
|
37
|
+
aws ssm start-session --target i-0e5fd6d0bcdfd3c03 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8105"],"localPortNumber":["8105"]}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
When running the unit test suite, `pytest-recording` library is used. If it gives you errors about a cassette missing, the parameter to invoke a live HTTP request is `--record-mode=once` (add on to the end of the `pytest` command)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Updating from the template
|
|
45
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
46
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[](https://github.com/pre-commit/pre-commit)
|
|
2
|
+
[](https://github.com/astral-sh/ruff)
|
|
3
|
+
[](https://github.com/astral-sh/uv)
|
|
4
|
+
[](https://microsoft.github.io/pyright/)
|
|
5
|
+
[](https://github.com/lab-sync/biosero-data-services-sdk/actions)
|
|
6
|
+
[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk)
|
|
7
|
+
[](https://pypi.org/project/biosero-data-services-sdk/)
|
|
8
|
+
[](https://pepy.tech/project/biosero-data-services-sdk)
|
|
9
|
+
[](https://pypi.org/project/biosero-data-services-sdk/)
|
|
10
|
+
[](https://codecov.io/gh/lab-sync/biosero-data-services-sdk)
|
|
11
|
+
|
|
12
|
+
# Usage
|
|
13
|
+
Documentation is hosted on [ReadTheDocs](https://biosero-data-services-sdk.readthedocs.io/en/latest/?badge=latest).
|
|
14
|
+
|
|
15
|
+
# Development
|
|
16
|
+
This project has a dev container. If you already have VS Code and Docker installed, you can click the badge above or [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
|
|
17
|
+
|
|
18
|
+
To publish a new version of the repository, you can run the `Publish` workflow manually and publish to the staging registry from any branch, and you can check the 'Publish to Primary' option when on `main` to publish to the primary registry and create a git tag.
|
|
19
|
+
|
|
20
|
+
Open a connection to the EC2 instance hosting GBG Data Services in AWS:
|
|
21
|
+
```bash
|
|
22
|
+
aws ssm start-session --target i-0e5fd6d0bcdfd3c03 --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["8105"],"localPortNumber":["8105"]}'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
When running the unit test suite, `pytest-recording` library is used. If it gives you errors about a cassette missing, the parameter to invoke a live HTTP request is `--record-mode=once` (add on to the end of the `pytest` command)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Updating from the template
|
|
30
|
+
This repository uses a copier template. To pull in the latest updates from the template, use the command:
|
|
31
|
+
`copier update --trust --conflict rej --defaults`
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "biosero-data-services-sdk"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "SDK for interacting with the Biosero Data Services API"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Eli Fine"},
|
|
7
|
+
|
|
8
|
+
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12.7"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"pydantic>=2.11.7",
|
|
21
|
+
"structlog>=25.3.0",
|
|
22
|
+
"nested-diff>=1.8.0",
|
|
23
|
+
"requests>=2.32.4",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
# Specific to this repository
|
|
30
|
+
|
|
31
|
+
"vcrpy>=7.0.0",
|
|
32
|
+
"pytest-recording>=0.13.4",
|
|
33
|
+
"time-machine>=2.17.0",
|
|
34
|
+
"beautifulsoup4>=4.13.4",
|
|
35
|
+
"syrupy>=4.9.1",
|
|
36
|
+
"pytest-mock>=3.14.0",
|
|
37
|
+
|
|
38
|
+
# Managed by upstream template
|
|
39
|
+
"pyright>=1.1.403",
|
|
40
|
+
"pytest>=8.4.1",
|
|
41
|
+
"pytest-cov>=6.2.1",
|
|
42
|
+
"pytest-randomly>=3.16.0",
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
license-files = [] # kludge until this bug is fixed https://github.com/pypa/setuptools/issues/4759
|
|
49
|
+
|
|
50
|
+
[tool.uv]
|
|
51
|
+
package = true
|
|
52
|
+
|
|
53
|
+
[[tool.uv.index]]
|
|
54
|
+
name = "pypi"
|
|
55
|
+
url = "https://pypi.org/simple/"
|
|
56
|
+
publish-url = "https://upload.pypi.org/legacy/"
|
|
57
|
+
|
|
58
|
+
[[tool.uv.index]]
|
|
59
|
+
name = "testpypi"
|
|
60
|
+
url = "https://test.pypi.org/simple/"
|
|
61
|
+
publish-url = "https://test.pypi.org/legacy/"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from . import sdk
|
|
2
|
+
from .constants import BIOSERO_CONTAINER_IDENTITY_TYPE_ID
|
|
3
|
+
from .constants import BIOSERO_DATA_SERVICES_HOSTNAME_ENVVAR_NAME
|
|
4
|
+
from .constants import BIOSERO_MATERIAL_IDENTITY_TYPE_ID
|
|
5
|
+
from .constants import BIOSERO_RESOURCE_IDENTITY_TYPE_ID
|
|
6
|
+
from .constants import BIOSERO_SAMPLE_IDENTITY_TYPE_ID
|
|
7
|
+
from .constants import BIOSERO_TRANSFER_STATION_IDENTITY_TYPE_ID
|
|
8
|
+
from .constants import DATA_STORE_ARBITRARY_JSON_ID_BASE
|
|
9
|
+
from .constants import DATA_STORE_GENERIC_STATE_ID_BASE
|
|
10
|
+
from .constants import DATA_STORE_ROOT_ID_BASE
|
|
11
|
+
from .constants import DATA_STORE_TENANT_SUFFIX_ENVVAR_NAME
|
|
12
|
+
from .constants import DATA_STORE_TENANT_SUFFIX_LENGTH
|
|
13
|
+
from .constants import DEFAULT_DATA_STORE_TENANT_SUFFIX
|
|
14
|
+
from .constants import IDENTITY_ROOT_ID
|
|
15
|
+
from .constants import WASTE_LOCATION_ID
|
|
16
|
+
from .exceptions import DataRecordOverflowError
|
|
17
|
+
from .exceptions import DataServicesApiError
|
|
18
|
+
from .exceptions import DataStoreArbitraryJsonIdentityMissingError
|
|
19
|
+
from .exceptions import DataStoreRootIdentityMissingError
|
|
20
|
+
from .exceptions import EventDoesNotExistError
|
|
21
|
+
from .exceptions import IdentityAlreadyExistsError
|
|
22
|
+
from .exceptions import IdentityDoesNotExistError
|
|
23
|
+
from .exceptions import IdentityIsNotContainerError
|
|
24
|
+
from .exceptions import IdentityIsNotSubstanceError
|
|
25
|
+
from .exceptions import IdentityIsNotTransferStationError
|
|
26
|
+
from .exceptions import InvalidEventQueryParametersError
|
|
27
|
+
from .exceptions import NothingToScanProvidedError
|
|
28
|
+
from .exceptions import OrderDoesNotExistError
|
|
29
|
+
from .exceptions import OrderParameterNotInTemplateError
|
|
30
|
+
from .exceptions import OrderTemplateAlreadyExistsError
|
|
31
|
+
from .exceptions import OrderTemplateDoesNotExistError
|
|
32
|
+
from .exceptions import SeeminglyInfiniteLocationPathError
|
|
33
|
+
from .exceptions import TransportationRequestDoesNotExistError
|
|
34
|
+
from .logger_config import configure_logging
|
|
35
|
+
from .models import BAD_ORDER_STATUSES
|
|
36
|
+
from .models import LIQUID_TRANSFER_EVENT_TOPIC
|
|
37
|
+
from .models import LOCATION_CHANGED_EVENT_TOPIC
|
|
38
|
+
from .models import DataServicesContainer
|
|
39
|
+
from .models import DataServicesEvent
|
|
40
|
+
from .models import DataServicesIdentity
|
|
41
|
+
from .models import DataServicesIdentityProperty
|
|
42
|
+
from .models import DataServicesIdentityPropertyQuery
|
|
43
|
+
from .models import DataServicesLocation
|
|
44
|
+
from .models import DataServicesMaterialInContainer
|
|
45
|
+
from .models import DataServicesModel
|
|
46
|
+
from .models import DataServicesSampleInContainer
|
|
47
|
+
from .models import DataServicesSubstanceInContainer
|
|
48
|
+
from .models import DataServicesTransportationRequest
|
|
49
|
+
from .models import IdentityPropertyNotFoundError
|
|
50
|
+
from .models import IdentityPropertyValueType
|
|
51
|
+
from .models import LiquidTransferEvent
|
|
52
|
+
from .models import LocationChangedEvent
|
|
53
|
+
from .models import NullContainerIdError
|
|
54
|
+
from .models import NullIdentityPropertyValueError
|
|
55
|
+
from .models import Order
|
|
56
|
+
from .models import OrderParameter
|
|
57
|
+
from .models import OrderStatus
|
|
58
|
+
from .models import OrderTemplate
|
|
59
|
+
from .models import OrderTemplateScript
|
|
60
|
+
from .models import OrderTemplateWorkflow
|
|
61
|
+
from .models import TransportationRequestStatus
|
|
62
|
+
from .models import VolumeAmount
|
|
63
|
+
from .reports import DataServicesInconsistenciesReport
|
|
64
|
+
from .reports import EventUploadReport
|
|
65
|
+
from .reports import IdentitiesMergeReport
|
|
66
|
+
from .reports import IdentityDiff
|
|
67
|
+
from .reports import MissingIdentitiesAtLocationReport
|
|
68
|
+
from .reports import MissingIdentitiesAtLocationWithChildrenReport
|
|
69
|
+
from .sdk import DataServicesSdk
|
|
70
|
+
from .sdk import clear_items_in_waste_cache
|
|
71
|
+
from .sdk import get_items_in_waste_cache
|
|
72
|
+
from .sdk_base import DataStoreStateBase
|
|
73
|
+
from .types import EventId
|
|
74
|
+
from .types import IdentityId
|
|
75
|
+
from .types import IdentityName
|
|
76
|
+
from .types import LocationPath
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"BAD_ORDER_STATUSES",
|
|
80
|
+
"BIOSERO_CONTAINER_IDENTITY_TYPE_ID",
|
|
81
|
+
"BIOSERO_DATA_SERVICES_HOSTNAME_ENVVAR_NAME",
|
|
82
|
+
"BIOSERO_MATERIAL_IDENTITY_TYPE_ID",
|
|
83
|
+
"BIOSERO_RESOURCE_IDENTITY_TYPE_ID",
|
|
84
|
+
"BIOSERO_SAMPLE_IDENTITY_TYPE_ID",
|
|
85
|
+
"BIOSERO_TRANSFER_STATION_IDENTITY_TYPE_ID",
|
|
86
|
+
"DATA_STORE_ARBITRARY_JSON_ID_BASE",
|
|
87
|
+
"DATA_STORE_GENERIC_STATE_ID_BASE",
|
|
88
|
+
"DATA_STORE_ROOT_ID_BASE",
|
|
89
|
+
"DATA_STORE_TENANT_SUFFIX_ENVVAR_NAME",
|
|
90
|
+
"DATA_STORE_TENANT_SUFFIX_LENGTH",
|
|
91
|
+
"DEFAULT_DATA_STORE_TENANT_SUFFIX",
|
|
92
|
+
"IDENTITY_ROOT_ID",
|
|
93
|
+
"LIQUID_TRANSFER_EVENT_TOPIC",
|
|
94
|
+
"LOCATION_CHANGED_EVENT_TOPIC",
|
|
95
|
+
"WASTE_LOCATION_ID",
|
|
96
|
+
"DataRecordOverflowError",
|
|
97
|
+
"DataServicesApiError",
|
|
98
|
+
"DataServicesContainer",
|
|
99
|
+
"DataServicesEvent",
|
|
100
|
+
"DataServicesIdentity",
|
|
101
|
+
"DataServicesIdentityProperty",
|
|
102
|
+
"DataServicesIdentityPropertyQuery",
|
|
103
|
+
"DataServicesInconsistenciesReport",
|
|
104
|
+
"DataServicesLocation",
|
|
105
|
+
"DataServicesMaterialInContainer",
|
|
106
|
+
"DataServicesModel",
|
|
107
|
+
"DataServicesSampleInContainer",
|
|
108
|
+
"DataServicesSdk",
|
|
109
|
+
"DataServicesSubstanceInContainer",
|
|
110
|
+
"DataServicesTransportationRequest",
|
|
111
|
+
"DataStoreArbitraryJsonIdentityMissingError",
|
|
112
|
+
"DataStoreArbitraryJsonIdentityMissingError",
|
|
113
|
+
"DataStoreRootIdentityMissingError",
|
|
114
|
+
"DataStoreStateBase",
|
|
115
|
+
"EventDoesNotExistError",
|
|
116
|
+
"EventId",
|
|
117
|
+
"EventUploadReport",
|
|
118
|
+
"IdentitiesMergeReport",
|
|
119
|
+
"IdentityAlreadyExistsError",
|
|
120
|
+
"IdentityDiff",
|
|
121
|
+
"IdentityDoesNotExistError",
|
|
122
|
+
"IdentityId",
|
|
123
|
+
"IdentityIsNotContainerError",
|
|
124
|
+
"IdentityIsNotSubstanceError",
|
|
125
|
+
"IdentityIsNotTransferStationError",
|
|
126
|
+
"IdentityName",
|
|
127
|
+
"IdentityPropertyNotFoundError",
|
|
128
|
+
"IdentityPropertyValueType",
|
|
129
|
+
"InvalidEventQueryParametersError",
|
|
130
|
+
"LiquidTransferEvent",
|
|
131
|
+
"LocationChangedEvent",
|
|
132
|
+
"LocationPath",
|
|
133
|
+
"MissingIdentitiesAtLocationReport",
|
|
134
|
+
"MissingIdentitiesAtLocationWithChildrenReport",
|
|
135
|
+
"NothingToScanProvidedError",
|
|
136
|
+
"NullContainerIdError",
|
|
137
|
+
"NullIdentityPropertyValueError",
|
|
138
|
+
"Order",
|
|
139
|
+
"OrderDoesNotExistError",
|
|
140
|
+
"OrderParameter",
|
|
141
|
+
"OrderParameterNotInTemplateError",
|
|
142
|
+
"OrderStatus",
|
|
143
|
+
"OrderTemplate",
|
|
144
|
+
"OrderTemplateAlreadyExistsError",
|
|
145
|
+
"OrderTemplateDoesNotExistError",
|
|
146
|
+
"OrderTemplateScript",
|
|
147
|
+
"OrderTemplateWorkflow",
|
|
148
|
+
"SeeminglyInfiniteLocationPathError",
|
|
149
|
+
"TransportationRequestDoesNotExistError",
|
|
150
|
+
"TransportationRequestStatus",
|
|
151
|
+
"VolumeAmount",
|
|
152
|
+
"clear_items_in_waste_cache",
|
|
153
|
+
"configure_logging",
|
|
154
|
+
"get_items_in_waste_cache",
|
|
155
|
+
"sdk",
|
|
156
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# IDs created by BioSero within DataServices itself
|
|
2
|
+
IDENTITY_ROOT_ID = "7818C3CC-51AC-42FE-B38F-A9342857A859"
|
|
3
|
+
BIOSERO_MATERIAL_IDENTITY_TYPE_ID = "885C27BC-8121-4ABC-8FAD-A9DD08B26B09"
|
|
4
|
+
BIOSERO_SAMPLE_IDENTITY_TYPE_ID = "A1A3E1FA-1016-4E68-89A9-EDFE98E4F419"
|
|
5
|
+
BIOSERO_RESOURCE_IDENTITY_TYPE_ID = "C6E397A9-447E-41B0-B7B7-0459249F3C69"
|
|
6
|
+
BIOSERO_CONTAINER_IDENTITY_TYPE_ID = "AE62812F-A108-47E0-8B96-2B0CA7617CE2"
|
|
7
|
+
BIOSERO_TRANSFER_STATION_IDENTITY_TYPE_ID = "97E22FF1-0393-4B3D-96C2-45343919AC92"
|
|
8
|
+
# Arbitrarily created IDs
|
|
9
|
+
WASTE_LOCATION_ID = "Waste"
|
|
10
|
+
DATA_STORE_ROOT_ID_BASE = "CFC74038-9FC8-474B-A9A2-909"
|
|
11
|
+
DATA_STORE_ARBITRARY_JSON_ID_BASE = "A52E078A-A300-49F3-96F3-7E8"
|
|
12
|
+
DATA_STORE_GENERIC_STATE_ID_BASE = "9B2A01A1-BFAE-4479-B4F1-C2F"
|
|
13
|
+
DEFAULT_DATA_STORE_TENANT_SUFFIX = "unittes"
|
|
14
|
+
DATA_STORE_TENANT_SUFFIX_ENVVAR_NAME = "DATA_SERVICES_DATA_STORE_TENANT_SUFFIX"
|
|
15
|
+
DATA_STORE_TENANT_SUFFIX_LENGTH = 7 # keep it consistent so the overall ID is the same length as UUID
|
|
16
|
+
|
|
17
|
+
BIOSERO_DATA_SERVICES_HOSTNAME_ENVVAR_NAME = "BIOSERO_DATA_SERVICES_HOSTNAME"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import JsonValue
|
|
9
|
+
|
|
10
|
+
from .exceptions import DataServicesApiError
|
|
11
|
+
from .exceptions import EventDoesNotExistError
|
|
12
|
+
from .exceptions import InvalidEventQueryParametersError
|
|
13
|
+
from .models import DataServicesEvent
|
|
14
|
+
from .reports import EventUploadReport
|
|
15
|
+
from .sdk_base import SdkBase
|
|
16
|
+
from .types import EventId
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EventsSdk(SdkBase):
|
|
22
|
+
def publish_event(self, event: DataServicesEvent, *, explicitly_set_id: bool = False) -> EventId:
|
|
23
|
+
# Although the Swagger documentation says that the `data` field is nullable, we seem to get errors trying to publish an event with no data...unclear if we should refactor to assume it's non-nullable in all cases or just leave as-is
|
|
24
|
+
|
|
25
|
+
payload = event.to_api_request()
|
|
26
|
+
# Although the Swagger documentation says that the `createdDateUtc` field can be supplied, it doesn't seem to be respected when supplied
|
|
27
|
+
_ = payload.pop("createdDateUtc", None)
|
|
28
|
+
if not explicitly_set_id:
|
|
29
|
+
_ = payload.pop("eventId", None)
|
|
30
|
+
|
|
31
|
+
response = self._post_request(query="EventService", payload=payload)
|
|
32
|
+
assert isinstance(response, str), f"Expected a string, got {type(response)} with value {response}"
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
def get_events(
|
|
36
|
+
self, *, start: datetime.datetime | None = None, end: datetime.datetime | None = None, topic: str | None = None
|
|
37
|
+
) -> list[DataServicesEvent]:
|
|
38
|
+
payload: dict[str, JsonValue] = {}
|
|
39
|
+
if start is not None:
|
|
40
|
+
payload["start"] = start.isoformat()
|
|
41
|
+
if end is not None:
|
|
42
|
+
payload["end"] = end.isoformat()
|
|
43
|
+
if topic is not None:
|
|
44
|
+
payload["topic"] = topic
|
|
45
|
+
if not payload:
|
|
46
|
+
raise InvalidEventQueryParametersError
|
|
47
|
+
response = self._post_request(query="QueryService/Events", payload=payload)
|
|
48
|
+
assert isinstance(response, list), f"Expected a list, got {type(response)} with value {response}"
|
|
49
|
+
event_list: list[DataServicesEvent] = []
|
|
50
|
+
for item in response:
|
|
51
|
+
assert isinstance(item, dict), f"Expected a dict, got {type(item)} with value {item}"
|
|
52
|
+
event = DataServicesEvent.from_api_response(item)
|
|
53
|
+
event_list.append(event)
|
|
54
|
+
return event_list
|
|
55
|
+
|
|
56
|
+
def get_event_by_id(self, event_id: str) -> DataServicesEvent:
|
|
57
|
+
try:
|
|
58
|
+
response = self._get_query(f"events/{event_id}", api_version=3)
|
|
59
|
+
except DataServicesApiError as e:
|
|
60
|
+
if e.status_code == requests.codes.not_found and "Event with EventId" in e.body:
|
|
61
|
+
raise EventDoesNotExistError(event_id) from e
|
|
62
|
+
raise # pragma: nocover # not sure how to test this defensive statement
|
|
63
|
+
assert isinstance(response, dict), f"Expected a dict, got {type(response)} with value {response}"
|
|
64
|
+
return DataServicesEvent.from_api_response(response)
|
|
65
|
+
|
|
66
|
+
def upload_events_from_file(self, savepoint_dir: Path, *, preview: bool = False) -> EventUploadReport:
|
|
67
|
+
all_events_json_files = sorted(savepoint_dir.rglob("*events*.json"))
|
|
68
|
+
report = EventUploadReport()
|
|
69
|
+
for json_file in all_events_json_files:
|
|
70
|
+
with json_file.open() as f:
|
|
71
|
+
json_data = json.load(f)
|
|
72
|
+
assert isinstance(json_data, list), f"Expected list but got type {type(json_data)} for file {json_file}"
|
|
73
|
+
num_events_in_file = len(json_data) # pyright: ignore[reportUnknownArgumentType] # we're loading in JSON and then asserting about it
|
|
74
|
+
for idx, event_json in enumerate(json_data): # pyright: ignore[reportUnknownArgumentType,reportUnknownVariableType] # we're loading in JSON and then asserting about it
|
|
75
|
+
event_json = cast(dict[str, JsonValue], event_json)
|
|
76
|
+
assert isinstance(event_json, dict), f"Expected dict but got type {type(event_json)} for {event_json}"
|
|
77
|
+
event_in_file = DataServicesEvent.from_api_response(event_json)
|
|
78
|
+
logger.info(
|
|
79
|
+
f"Processing event {event_in_file.id} from file {json_file} for upload. {idx + 1}/{num_events_in_file} in file"
|
|
80
|
+
)
|
|
81
|
+
matching_existing_event: DataServicesEvent | None = None
|
|
82
|
+
try:
|
|
83
|
+
matching_existing_event = self.get_event_by_id(event_in_file.id)
|
|
84
|
+
except EventDoesNotExistError:
|
|
85
|
+
matching_existing_event = None
|
|
86
|
+
|
|
87
|
+
if matching_existing_event is None:
|
|
88
|
+
if not preview:
|
|
89
|
+
_ = self.publish_event(event_in_file, explicitly_set_id=True)
|
|
90
|
+
report.created.append(event_in_file.id)
|
|
91
|
+
continue
|
|
92
|
+
fields_to_exclude = {"created_at", "original_full_json_string"}
|
|
93
|
+
existing_event_fields = matching_existing_event.model_dump(exclude=fields_to_exclude)
|
|
94
|
+
event_in_file_fields = event_in_file.model_dump(exclude=fields_to_exclude)
|
|
95
|
+
if existing_event_fields == event_in_file_fields:
|
|
96
|
+
report.untouched.append(event_in_file.id)
|
|
97
|
+
continue
|
|
98
|
+
report.conflicts.append(event_in_file.id)
|
|
99
|
+
|
|
100
|
+
return report
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from .models import DataServicesIdentity
|
|
2
|
+
from .types import RequestWithOffset
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DataServicesApiError(Exception):
|
|
6
|
+
def __init__(self, *, url: str, status_code: int, reason: str, body: str) -> None:
|
|
7
|
+
self.url = url
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.reason = reason
|
|
10
|
+
self.body = body
|
|
11
|
+
super().__init__(f"Error querying {url}. Status code: {status_code}. Reason: {reason}. Body: {body}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SeeminglyInfiniteLocationPathError(Exception):
|
|
15
|
+
def __init__(self, *, original_id: str, location_path: list[str]):
|
|
16
|
+
super().__init__(
|
|
17
|
+
f"The location path for {original_id} seems to be infinite. This is likely due to a circular reference in the location hierarchy. Please check the location data for circular references. {location_path}"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IdentityIsNotSubstanceError(Exception):
|
|
22
|
+
def __init__(self, info: str | DataServicesIdentity) -> None:
|
|
23
|
+
super().__init__(
|
|
24
|
+
f"The provided identity {info} is not a Substance. It must be a direct child of Sample or Material identities for Data Services to be able to handle it."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IdentityIsNotContainerError(Exception):
|
|
29
|
+
def __init__(self, info: str | DataServicesIdentity) -> None:
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"The provided identity {info} is not a Container. It must be within the hierarchy of Container definitions."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IdentityIsNotTransferStationError(Exception):
|
|
36
|
+
def __init__(self, info: str | DataServicesIdentity) -> None:
|
|
37
|
+
super().__init__(
|
|
38
|
+
f"The provided identity {info} is not a Transfer Station. It must be within the hierarchy of Transfer Station definitions."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OrderParameterNotInTemplateError(Exception):
|
|
43
|
+
def __init__(self, *, parameter_name: str, template_name: str, template_parameters: list[str]):
|
|
44
|
+
super().__init__(
|
|
45
|
+
f"Order parameter '{parameter_name}' is not in the order template '{template_name}'. Available parameters: {', '.join(template_parameters)}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class IdentityDoesNotExistError(Exception):
|
|
50
|
+
def __init__(self, *, id: str, extra_message: str = "") -> None:
|
|
51
|
+
super().__init__(
|
|
52
|
+
f"Identity with identifier '{id}' does not exist" + (f"\n{extra_message}" if extra_message else "")
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OrderDoesNotExistError(Exception):
|
|
57
|
+
def __init__(self, id: str):
|
|
58
|
+
super().__init__(f"Order with ID {id} does not exist.")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class OrderTemplateDoesNotExistError(Exception):
|
|
62
|
+
def __init__(self, name: str):
|
|
63
|
+
super().__init__(f"Order template '{name}' does not exist.")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class OrderTemplateAlreadyExistsError(Exception):
|
|
67
|
+
def __init__(self, name: str):
|
|
68
|
+
super().__init__(f"Order template '{name}' already exists.")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TransportationRequestDoesNotExistError(Exception):
|
|
72
|
+
def __init__(self, request_id: str):
|
|
73
|
+
super().__init__(f"Transportation request with id {request_id} does not exist.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IdentityAlreadyExistsError(Exception):
|
|
77
|
+
def __init__(self, id: str):
|
|
78
|
+
super().__init__(f"Identity with identifier '{id}' already exists.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class EventDoesNotExistError(Exception):
|
|
82
|
+
def __init__(self, event_id: str):
|
|
83
|
+
super().__init__(f"Event with ID {event_id} does not exist.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InvalidEventQueryParametersError(Exception):
|
|
87
|
+
def __init__(self):
|
|
88
|
+
super().__init__("At least one of the query parameters must be provided.")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DataStoreRootIdentityMissingError(IdentityDoesNotExistError):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class DataStoreArbitraryJsonIdentityMissingError(IdentityDoesNotExistError):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class NothingToScanProvidedError(Exception):
|
|
100
|
+
def __init__(self):
|
|
101
|
+
super().__init__("Nothing to check for inconsistencies was passed in as an argument.")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DataRecordOverflowError(Exception):
|
|
105
|
+
def __init__(self, *, max_limit: int, request: RequestWithOffset):
|
|
106
|
+
request_info = self._get_request_info(request)
|
|
107
|
+
super().__init__(
|
|
108
|
+
f"Exceeded maximum limit of {max_limit} items while fetching data from DataServices API using {request_info}."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _get_request_info(self, request: RequestWithOffset) -> str:
|
|
112
|
+
"""Extract meaningful information from the request, especially if it's a partial function."""
|
|
113
|
+
if hasattr(request, "func") and hasattr(request, "keywords"): # It's likely a partial
|
|
114
|
+
func_name = request.func.__name__ if hasattr(request.func, "__name__") else str(request.func) # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType] # this is tested and it works
|
|
115
|
+
keywords = ", ".join(f"{k}={v!r}" for k, v in request.keywords.items()) # pyright: ignore[reportUnknownMemberType,reportAttributeAccessIssue,reportUnknownVariableType] # this is tested and it works
|
|
116
|
+
return f"partial({func_name}, {keywords})"
|
|
117
|
+
raise NotImplementedError(
|
|
118
|
+
f"Not sure how we got here, but this should always be called with a partial. The request was {request!r}"
|
|
119
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from .exceptions import IdentityDoesNotExistError
|
|
4
|
+
from .models import DataServicesIdentity
|
|
5
|
+
from .sdk_base import SdkBase
|
|
6
|
+
from .types import IdentityId
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class IdentitiesSdk(SdkBase):
|
|
12
|
+
def get_child_identities(
|
|
13
|
+
self, parent_id: str, *, recursive: bool = False, confirm_parent_id_exists: bool = True
|
|
14
|
+
) -> list[DataServicesIdentity]:
|
|
15
|
+
# The API for ChildIdentities does not actually check if the parent exists...it will just silently return an empty list. That's unexpected API behavior, so we check it ourselves, which will raise an error if it doesn't exist
|
|
16
|
+
if confirm_parent_id_exists:
|
|
17
|
+
_ = self.get_identity_by_id(parent_id)
|
|
18
|
+
limit = 999999 # arbitrarily large number
|
|
19
|
+
identity_list: list[DataServicesIdentity] = []
|
|
20
|
+
response = self._get_query(f"QueryService/ChildIdentities?parentTypeId={parent_id}&limit={limit}")
|
|
21
|
+
assert isinstance(response, list), f"Expected a list, got {type(response)} with value {response}"
|
|
22
|
+
|
|
23
|
+
for item in response:
|
|
24
|
+
assert isinstance(item, dict), f"Expected a dict, got {type(item)} with value {item}"
|
|
25
|
+
identity = DataServicesIdentity.from_api_response(item)
|
|
26
|
+
identity_list.append(identity)
|
|
27
|
+
if recursive:
|
|
28
|
+
child_identities = self.get_child_identities(
|
|
29
|
+
identity.id, recursive=True, confirm_parent_id_exists=False
|
|
30
|
+
)
|
|
31
|
+
identity_list.extend(child_identities)
|
|
32
|
+
return identity_list
|
|
33
|
+
|
|
34
|
+
def is_identity_name_a_child_of(self, *, name: str, parent_id: str) -> bool:
|
|
35
|
+
identities = self._get_query(f"identities?name={name}&typeIdentifier={parent_id}&limit=1", api_version=3)
|
|
36
|
+
assert isinstance(identities, list), f"Expected a list, got {type(identities)} with value {identities}"
|
|
37
|
+
return len(identities) > 0
|
|
38
|
+
|
|
39
|
+
def is_identity_a_descendent_of(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
identity_id: str,
|
|
43
|
+
ancestor_id: str,
|
|
44
|
+
is_being_called_recursively: bool = False,
|
|
45
|
+
identities_cache: dict[IdentityId, DataServicesIdentity] | None = None,
|
|
46
|
+
) -> bool:
|
|
47
|
+
if identities_cache is None:
|
|
48
|
+
identities_cache = {}
|
|
49
|
+
if identity_id in identities_cache:
|
|
50
|
+
identity = identities_cache[identity_id]
|
|
51
|
+
else:
|
|
52
|
+
try:
|
|
53
|
+
identity = self.get_identity_by_id(identity_id)
|
|
54
|
+
except IdentityDoesNotExistError:
|
|
55
|
+
if not is_being_called_recursively:
|
|
56
|
+
raise
|
|
57
|
+
return False
|
|
58
|
+
identities_cache[identity_id] = identity
|
|
59
|
+
if identity.parent_id == ancestor_id:
|
|
60
|
+
return True
|
|
61
|
+
if identity.parent_id == "":
|
|
62
|
+
return False
|
|
63
|
+
if identity.parent_id is None:
|
|
64
|
+
return False
|
|
65
|
+
return self.is_identity_a_descendent_of(
|
|
66
|
+
identity_id=identity.parent_id,
|
|
67
|
+
ancestor_id=ancestor_id,
|
|
68
|
+
is_being_called_recursively=True,
|
|
69
|
+
identities_cache=identities_cache,
|
|
70
|
+
)
|