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.
Files changed (31) hide show
  1. biosero_data_services_sdk-0.1.1/PKG-INFO +46 -0
  2. biosero_data_services_sdk-0.1.1/README.md +31 -0
  3. biosero_data_services_sdk-0.1.1/pyproject.toml +61 -0
  4. biosero_data_services_sdk-0.1.1/setup.cfg +4 -0
  5. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/__init__.py +156 -0
  6. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/constants.py +17 -0
  7. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/events_sdk.py +100 -0
  8. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/exceptions.py +119 -0
  9. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/identities_sdk.py +70 -0
  10. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/locations_sdk.py +118 -0
  11. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/logger_config.py +131 -0
  12. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/__init__.py +28 -0
  13. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/base.py +25 -0
  14. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/container.py +247 -0
  15. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/event.py +185 -0
  16. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/identity.py +259 -0
  17. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/order.py +51 -0
  18. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/order_template.py +179 -0
  19. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/models/transportation.py +117 -0
  20. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/orders_sdk.py +90 -0
  21. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/py.typed +0 -0
  22. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/reports.py +210 -0
  23. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/sdk.py +291 -0
  24. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/sdk_base.py +342 -0
  25. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/transportation_sdk.py +101 -0
  26. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk/types.py +12 -0
  27. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/PKG-INFO +46 -0
  28. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/SOURCES.txt +29 -0
  29. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/dependency_links.txt +1 -0
  30. biosero_data_services_sdk-0.1.1/src/biosero_data_services_sdk.egg-info/requires.txt +4 -0
  31. 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
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
17
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
18
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
19
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
20
+ [![Actions status](https://github.com/lab-sync/biosero-data-services-sdk/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/lab-sync/biosero-data-services-sdk/actions)
21
+ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk)
22
+ [![PyPI Version](https://img.shields.io/pypi/v/biosero-data-services-sdk.svg)](https://pypi.org/project/biosero-data-services-sdk/)
23
+ [![Downloads](https://pepy.tech/badge/biosero-data-services-sdk)](https://pepy.tech/project/biosero-data-services-sdk)
24
+ [![Python Versions](https://img.shields.io/pypi/pyversions/biosero-data-services-sdk.svg)](https://pypi.org/project/biosero-data-services-sdk/)
25
+ [![Codecov](https://codecov.io/gh/lab-sync/biosero-data-services-sdk/branch/main/graph/badge.svg)](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
+ [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
2
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
3
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
4
+ [![Checked with pyright](https://microsoft.github.io/pyright/img/pyright_badge.svg)](https://microsoft.github.io/pyright/)
5
+ [![Actions status](https://github.com/lab-sync/biosero-data-services-sdk/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/lab-sync/biosero-data-services-sdk/actions)
6
+ [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/lab-sync/biosero-data-services-sdk)
7
+ [![PyPI Version](https://img.shields.io/pypi/v/biosero-data-services-sdk.svg)](https://pypi.org/project/biosero-data-services-sdk/)
8
+ [![Downloads](https://pepy.tech/badge/biosero-data-services-sdk)](https://pepy.tech/project/biosero-data-services-sdk)
9
+ [![Python Versions](https://img.shields.io/pypi/pyversions/biosero-data-services-sdk.svg)](https://pypi.org/project/biosero-data-services-sdk/)
10
+ [![Codecov](https://codecov.io/gh/lab-sync/biosero-data-services-sdk/branch/main/graph/badge.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )