evo-objects 0.1.0__py3-none-any.whl
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.
- evo/objects/__init__.py +25 -0
- evo/objects/_model_config.py +20 -0
- evo/objects/client.py +437 -0
- evo/objects/data.py +121 -0
- evo/objects/endpoints/__init__.py +35 -0
- evo/objects/endpoints/api/__init__.py +15 -0
- evo/objects/endpoints/api/data_api.py +118 -0
- evo/objects/endpoints/api/metadata_api.py +129 -0
- evo/objects/endpoints/api/objects_api.py +893 -0
- evo/objects/endpoints/api/stages_api.py +106 -0
- evo/objects/endpoints/models.py +292 -0
- evo/objects/exceptions.py +67 -0
- evo/objects/io.py +218 -0
- evo/objects/utils/__init__.py +35 -0
- evo/objects/utils/_types.py +82 -0
- evo/objects/utils/data.py +250 -0
- evo/objects/utils/table_formats.py +159 -0
- evo/objects/utils/tables.py +392 -0
- evo_objects-0.1.0.dist-info/METADATA +63 -0
- evo_objects-0.1.0.dist-info/RECORD +23 -0
- evo_objects-0.1.0.dist-info/WHEEL +5 -0
- evo_objects-0.1.0.dist-info/licenses/LICENSE.md +190 -0
- evo_objects-0.1.0.dist-info/top_level.txt +1 -0
evo/objects/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from .client import DownloadedObject, ObjectAPIClient
|
|
13
|
+
from .data import ObjectMetadata, ObjectSchema, ObjectVersion, SchemaVersion
|
|
14
|
+
from .io import ObjectDataDownload, ObjectDataUpload
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DownloadedObject",
|
|
18
|
+
"ObjectAPIClient",
|
|
19
|
+
"ObjectDataDownload",
|
|
20
|
+
"ObjectDataUpload",
|
|
21
|
+
"ObjectMetadata",
|
|
22
|
+
"ObjectSchema",
|
|
23
|
+
"ObjectVersion",
|
|
24
|
+
"SchemaVersion",
|
|
25
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CustomBaseModel(BaseModel):
|
|
16
|
+
"""Custom base model for providing a global configuration to generated models."""
|
|
17
|
+
|
|
18
|
+
model_config = ConfigDict(
|
|
19
|
+
extra="allow",
|
|
20
|
+
)
|
evo/objects/client.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from collections.abc import AsyncIterator, Iterator, Sequence
|
|
15
|
+
from pathlib import PurePosixPath
|
|
16
|
+
from uuid import UUID
|
|
17
|
+
|
|
18
|
+
from evo import logging
|
|
19
|
+
from evo.common import APIConnector, BaseAPIClient, HealthCheckType, ICache, Page, ServiceHealth
|
|
20
|
+
from evo.common.data import Environment
|
|
21
|
+
from evo.common.io.exceptions import DataNotFoundError
|
|
22
|
+
from evo.common.utils import get_service_health
|
|
23
|
+
from evo.workspaces import ServiceUser
|
|
24
|
+
|
|
25
|
+
from .data import ObjectMetadata, ObjectSchema, ObjectVersion
|
|
26
|
+
from .endpoints import ObjectsApi
|
|
27
|
+
from .endpoints.models import (
|
|
28
|
+
GeoscienceObject,
|
|
29
|
+
GeoscienceObjectVersion,
|
|
30
|
+
GetObjectResponse,
|
|
31
|
+
ListedObject,
|
|
32
|
+
PostObjectResponse,
|
|
33
|
+
UpdateGeoscienceObject,
|
|
34
|
+
)
|
|
35
|
+
from .exceptions import ObjectUUIDError
|
|
36
|
+
from .io import ObjectDataDownload, ObjectDataUpload
|
|
37
|
+
from .utils import ObjectDataClient
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger("object.client")
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"DownloadedObject",
|
|
43
|
+
"ObjectAPIClient",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _version_from_listed_version(model: GeoscienceObjectVersion) -> ObjectVersion:
|
|
48
|
+
"""Create an ObjectVersion instance from a generated ListedObject model.
|
|
49
|
+
|
|
50
|
+
:param model: The model to create the ObjectVersion instance from.
|
|
51
|
+
|
|
52
|
+
:return: An ObjectVersion instance.
|
|
53
|
+
"""
|
|
54
|
+
created_by = None if model.created_by is None else ServiceUser.from_model(model.created_by) # type: ignore
|
|
55
|
+
return ObjectVersion(
|
|
56
|
+
version_id=model.version_id,
|
|
57
|
+
created_at=model.created_at,
|
|
58
|
+
created_by=created_by,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DownloadedObject:
|
|
63
|
+
"""A downloaded geoscience object."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self, object_: GeoscienceObject, metadata: ObjectMetadata, urls_by_name: dict[str, str], connector: APIConnector
|
|
67
|
+
) -> None:
|
|
68
|
+
self._object = object_
|
|
69
|
+
self._metadata = metadata
|
|
70
|
+
self._urls_by_name = urls_by_name
|
|
71
|
+
self._connector = connector
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def schema(self) -> ObjectSchema:
|
|
75
|
+
"""The schema of the object."""
|
|
76
|
+
return self._metadata.schema_id
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def metadata(self) -> ObjectMetadata:
|
|
80
|
+
"""The metadata of the object."""
|
|
81
|
+
return self._metadata
|
|
82
|
+
|
|
83
|
+
def as_dict(self) -> dict:
|
|
84
|
+
"""Get this object as a dictionary."""
|
|
85
|
+
return self._object.model_dump(mode="python", by_alias=True)
|
|
86
|
+
|
|
87
|
+
def prepare_data_download(self, data_identifiers: Sequence[str | UUID]) -> Iterator[ObjectDataDownload]:
|
|
88
|
+
"""Prepare to download multiple data files from the geoscience object service, for this object.
|
|
89
|
+
|
|
90
|
+
Any data IDs that are not associated with the requested object will raise a DataNotFoundError.
|
|
91
|
+
|
|
92
|
+
:param data_identifiers: A list of sha256 digests or UUIDs for the data to be downloaded.
|
|
93
|
+
|
|
94
|
+
:return: An iterator of data download contexts that can be used to download the data.
|
|
95
|
+
|
|
96
|
+
:raises DataNotFoundError: If any requested data ID is not associated with this object.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
filtered_urls_by_name = {str(name): self._urls_by_name[str(name)] for name in data_identifiers}
|
|
100
|
+
except KeyError as exc:
|
|
101
|
+
raise DataNotFoundError(f"Unable to find the requested data: {exc.args[0]}") from exc
|
|
102
|
+
for ctx in ObjectDataDownload._create_multiple(
|
|
103
|
+
connector=self._connector, metadata=self._metadata, urls_by_name=filtered_urls_by_name
|
|
104
|
+
):
|
|
105
|
+
yield ctx
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ObjectAPIClient(BaseAPIClient):
|
|
109
|
+
def __init__(self, environment: Environment, connector: APIConnector) -> None:
|
|
110
|
+
super().__init__(environment, connector)
|
|
111
|
+
self._objects_api = ObjectsApi(connector=connector)
|
|
112
|
+
|
|
113
|
+
async def get_service_health(self, check_type: HealthCheckType = HealthCheckType.FULL) -> ServiceHealth:
|
|
114
|
+
"""Get the health of the geoscience object service.
|
|
115
|
+
|
|
116
|
+
:param check_type: The type of health check to perform.
|
|
117
|
+
|
|
118
|
+
:return: A ServiceHealth object.
|
|
119
|
+
|
|
120
|
+
:raises EvoAPIException: If the API returns an unexpected status code.
|
|
121
|
+
:raises ClientValueError: If the response is not a valid service health check response.
|
|
122
|
+
"""
|
|
123
|
+
return await get_service_health(self._connector, "geoscience-object", check_type=check_type)
|
|
124
|
+
|
|
125
|
+
def _metadata_from_listed_object(self, model: ListedObject) -> ObjectMetadata:
|
|
126
|
+
"""Create an ObjectMetadata instance from a generated ListedObject model.
|
|
127
|
+
|
|
128
|
+
:param model: The model to create the ObjectMetadata instance from.
|
|
129
|
+
|
|
130
|
+
:return: An ObjectMetadata instance.
|
|
131
|
+
"""
|
|
132
|
+
created_by = None if model.created_by is None else ServiceUser.from_model(model.created_by)
|
|
133
|
+
modified_by = None if model.modified_by is None else ServiceUser.from_model(model.modified_by)
|
|
134
|
+
return ObjectMetadata(
|
|
135
|
+
environment=self._environment,
|
|
136
|
+
id=model.object_id,
|
|
137
|
+
name=model.name,
|
|
138
|
+
created_at=model.created_at,
|
|
139
|
+
created_by=created_by,
|
|
140
|
+
modified_at=model.modified_at,
|
|
141
|
+
modified_by=modified_by,
|
|
142
|
+
parent=model.path.rstrip("/"),
|
|
143
|
+
schema_id=ObjectSchema.from_id(model.schema_),
|
|
144
|
+
version_id=model.version_id,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def _metadata_from_endpoint_model(self, model: GetObjectResponse | PostObjectResponse) -> ObjectMetadata:
|
|
148
|
+
"""Create an ObjectMetadata instance from a generated GetObjectResponse or PostObjectResponse model.
|
|
149
|
+
|
|
150
|
+
:param model: The model to create the ObjectMetadata instance from.
|
|
151
|
+
|
|
152
|
+
:return: An ObjectMetadata instance.
|
|
153
|
+
"""
|
|
154
|
+
object_path = PurePosixPath(model.object_path)
|
|
155
|
+
created_by = None if model.created_by is None else ServiceUser.from_model(model.created_by)
|
|
156
|
+
modified_by = None if model.modified_by is None else ServiceUser.from_model(model.modified_by)
|
|
157
|
+
return ObjectMetadata(
|
|
158
|
+
environment=self._environment,
|
|
159
|
+
id=model.object_id,
|
|
160
|
+
name=object_path.name,
|
|
161
|
+
created_at=model.created_at,
|
|
162
|
+
created_by=created_by,
|
|
163
|
+
modified_at=model.modified_at,
|
|
164
|
+
modified_by=modified_by,
|
|
165
|
+
parent=str(object_path.parent),
|
|
166
|
+
schema_id=ObjectSchema.from_id(model.object.schema_),
|
|
167
|
+
version_id=model.version_id,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
async def list_objects(self, offset: int = 0, limit: int = 5000) -> Page[ObjectMetadata]:
|
|
171
|
+
"""List up to `limit` geoscience objects, starting at `offset`.
|
|
172
|
+
|
|
173
|
+
The geoscience objects will be the latest version of the object.
|
|
174
|
+
If there are no objects to list, the page will be empty.
|
|
175
|
+
|
|
176
|
+
:param offset: The number of objects to skip before listing.
|
|
177
|
+
:param limit: Max number of objects to list.
|
|
178
|
+
|
|
179
|
+
:return: A page of all objects from the query.
|
|
180
|
+
"""
|
|
181
|
+
assert limit > 0
|
|
182
|
+
assert offset >= 0
|
|
183
|
+
response = await self._objects_api.list_objects(
|
|
184
|
+
org_id=str(self._environment.org_id),
|
|
185
|
+
workspace_id=str(self._environment.workspace_id),
|
|
186
|
+
limit=limit,
|
|
187
|
+
offset=offset,
|
|
188
|
+
)
|
|
189
|
+
return Page(
|
|
190
|
+
offset=offset,
|
|
191
|
+
limit=limit,
|
|
192
|
+
total=response.total,
|
|
193
|
+
items=[self._metadata_from_listed_object(model) for model in response.objects],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
async def list_all_objects(self, limit_per_request: int = 5000) -> list[ObjectMetadata]:
|
|
197
|
+
"""List all geoscience objects in the workspace.
|
|
198
|
+
|
|
199
|
+
This method makes multiple calls to the `list_objects` endpoint until all objects have been listed.
|
|
200
|
+
|
|
201
|
+
:param limit_per_request: The maximum number of objects to list in one request.
|
|
202
|
+
|
|
203
|
+
:return: A list of all objects in the workspace.
|
|
204
|
+
"""
|
|
205
|
+
items = []
|
|
206
|
+
offset = 0
|
|
207
|
+
while True:
|
|
208
|
+
page = await self.list_objects(offset=offset, limit=limit_per_request)
|
|
209
|
+
items += page.items()
|
|
210
|
+
if page.is_last:
|
|
211
|
+
break
|
|
212
|
+
offset = page.next_offset
|
|
213
|
+
return items
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def _get_object_versions(response: GetObjectResponse) -> list[ObjectVersion]:
|
|
217
|
+
object_versions = [_version_from_listed_version(model) for model in response.versions]
|
|
218
|
+
return sorted(object_versions, key=lambda v: v.created_at, reverse=True)
|
|
219
|
+
|
|
220
|
+
async def list_versions_by_path(self, path: str) -> list[ObjectVersion]:
|
|
221
|
+
"""List all version for the given object.
|
|
222
|
+
|
|
223
|
+
:param path: The path to the geoscience object.
|
|
224
|
+
|
|
225
|
+
:return: A sorted list of object versions. The latest version is the first element of the list.
|
|
226
|
+
"""
|
|
227
|
+
response = await self._objects_api.get_object(
|
|
228
|
+
org_id=str(self._environment.org_id),
|
|
229
|
+
workspace_id=str(self._environment.workspace_id),
|
|
230
|
+
objects_path=path,
|
|
231
|
+
include_versions=True,
|
|
232
|
+
)
|
|
233
|
+
return self._get_object_versions(response)
|
|
234
|
+
|
|
235
|
+
async def list_versions_by_id(self, object_id: UUID) -> list[ObjectVersion]:
|
|
236
|
+
"""List all version for the given object.
|
|
237
|
+
|
|
238
|
+
:param object_id: The UUID of the geoscience object.
|
|
239
|
+
|
|
240
|
+
:return: A sorted list of object versions. The latest version is the first element of the list.
|
|
241
|
+
"""
|
|
242
|
+
response = await self._objects_api.get_object_by_id(
|
|
243
|
+
org_id=str(self._environment.org_id),
|
|
244
|
+
workspace_id=str(self._environment.workspace_id),
|
|
245
|
+
object_id=str(object_id),
|
|
246
|
+
include_versions=True,
|
|
247
|
+
)
|
|
248
|
+
return self._get_object_versions(response)
|
|
249
|
+
|
|
250
|
+
async def prepare_data_upload(self, data_identifiers: Sequence[str | UUID]) -> AsyncIterator[ObjectDataUpload]:
|
|
251
|
+
"""Prepare to upload multiple data files to the geoscience object service.
|
|
252
|
+
|
|
253
|
+
Referenced data that already exists in the workspace will be skipped.
|
|
254
|
+
|
|
255
|
+
:param data_identifiers: A list of sha256 digests or UUIDs for the data to be uploaded.
|
|
256
|
+
|
|
257
|
+
:return: An async iterator of data upload contexts that can be used to upload the data. Data identifiers
|
|
258
|
+
that already exist in the workspace will be skipped.
|
|
259
|
+
"""
|
|
260
|
+
async for ctx in ObjectDataUpload._create_multiple(
|
|
261
|
+
connector=self._connector,
|
|
262
|
+
environment=self._environment,
|
|
263
|
+
names=data_identifiers,
|
|
264
|
+
):
|
|
265
|
+
yield ctx
|
|
266
|
+
|
|
267
|
+
async def prepare_data_download(
|
|
268
|
+
self, object_id: UUID, version_id: str, data_identifiers: Sequence[str | UUID]
|
|
269
|
+
) -> AsyncIterator[ObjectDataDownload]:
|
|
270
|
+
"""Prepare to download multiple data files from the geoscience object service.
|
|
271
|
+
|
|
272
|
+
Any data IDs that are not associated with the requested object will raise a DataNotFoundError.
|
|
273
|
+
|
|
274
|
+
:param object_id: The ID of the object to download data from.
|
|
275
|
+
:param version_id: The version ID of the object to download data from.
|
|
276
|
+
:param data_identifiers: A list of sha256 digests or UUIDs for the data to be downloaded.
|
|
277
|
+
|
|
278
|
+
:return: An async iterator of data download contexts that can be used to download the data.
|
|
279
|
+
|
|
280
|
+
:raises DataNotFoundError: If any requested data ID is not associated with the referenced object.
|
|
281
|
+
"""
|
|
282
|
+
downloaded_object = await self.download_object_by_id(object_id, version=version_id)
|
|
283
|
+
for ctx in downloaded_object.prepare_data_download(data_identifiers):
|
|
284
|
+
yield ctx
|
|
285
|
+
|
|
286
|
+
def get_data_client(self, cache: ICache) -> ObjectDataClient:
|
|
287
|
+
"""Get a data client for the geoscience object service.
|
|
288
|
+
|
|
289
|
+
The data client provides a high-level interface for uploading and downloading data that is referenced in
|
|
290
|
+
geoscience objects, and caching the data locally. It depends on the optional dependency `pyarrow`, which is
|
|
291
|
+
not installed by default. This dependency can be installed with `pip install evo-objects[utils]`.
|
|
292
|
+
|
|
293
|
+
:param cache: The cache to use for data downloads.
|
|
294
|
+
|
|
295
|
+
:return: An ObjectDataClient instance.
|
|
296
|
+
|
|
297
|
+
:raises RuntimeError: If the `pyarrow` package is not installed.
|
|
298
|
+
"""
|
|
299
|
+
return ObjectDataClient(environment=self._environment, connector=self._connector, cache=cache)
|
|
300
|
+
|
|
301
|
+
async def create_geoscience_object(self, path: str, object_dict: dict) -> ObjectMetadata:
|
|
302
|
+
"""Upload a new geoscience object to the geoscience object service.
|
|
303
|
+
|
|
304
|
+
New geoscience objects must not have a UUID, so that one can be assigned by the Geoscience Object Service.
|
|
305
|
+
The `object_instance` that is passed in will be updated with the service-assigned UUID after it has been
|
|
306
|
+
uploaded, and the metadata of the created object will be returned.
|
|
307
|
+
|
|
308
|
+
:param path: The path to upload the object to.
|
|
309
|
+
:param object_dict: The geoscience object to be uploaded.
|
|
310
|
+
|
|
311
|
+
:return: The metadata of the uploaded object.
|
|
312
|
+
|
|
313
|
+
:raises ObjectUUIDError: If the provided object has a UUID.
|
|
314
|
+
"""
|
|
315
|
+
if object_dict.get("uuid") is not None:
|
|
316
|
+
raise ObjectUUIDError("Object has a UUID but new objects should have None")
|
|
317
|
+
object_for_upload = GeoscienceObject.model_validate(object_dict)
|
|
318
|
+
|
|
319
|
+
result = await self._objects_api.post_objects(
|
|
320
|
+
org_id=str(self._environment.org_id),
|
|
321
|
+
workspace_id=str(self._environment.workspace_id),
|
|
322
|
+
objects_path=path,
|
|
323
|
+
geoscience_object=object_for_upload,
|
|
324
|
+
)
|
|
325
|
+
object_dict["uuid"] = result.object_id
|
|
326
|
+
return self._metadata_from_endpoint_model(result)
|
|
327
|
+
|
|
328
|
+
async def move_geoscience_object(self, path: str, object_dict: dict) -> ObjectMetadata:
|
|
329
|
+
"""Move an existing geoscience object to a new path in the geoscience object service.
|
|
330
|
+
|
|
331
|
+
:param path: The new path to move the object to.
|
|
332
|
+
:param object_dict: The geoscience object to be moved.
|
|
333
|
+
|
|
334
|
+
:return: The metadata of the moved object.
|
|
335
|
+
|
|
336
|
+
:raises ObjectUUIDError: If the provided object does not have a UUID.
|
|
337
|
+
"""
|
|
338
|
+
if object_dict.get("uuid") is None:
|
|
339
|
+
raise ObjectUUIDError("Object does not have a UUID")
|
|
340
|
+
object_for_upload = GeoscienceObject.model_validate(object_dict)
|
|
341
|
+
|
|
342
|
+
result = await self._objects_api.post_objects(
|
|
343
|
+
org_id=str(self._environment.org_id),
|
|
344
|
+
workspace_id=str(self._environment.workspace_id),
|
|
345
|
+
objects_path=path,
|
|
346
|
+
geoscience_object=object_for_upload,
|
|
347
|
+
)
|
|
348
|
+
return self._metadata_from_endpoint_model(result)
|
|
349
|
+
|
|
350
|
+
async def update_geoscience_object(self, object_dict: dict) -> ObjectMetadata:
|
|
351
|
+
"""Update an existing geoscience object in the geoscience object service.
|
|
352
|
+
|
|
353
|
+
:param object_dict: The geoscience object to be updated.
|
|
354
|
+
|
|
355
|
+
:return: The metadata of the updated object.
|
|
356
|
+
|
|
357
|
+
:raises ObjectUUIDError: If the provided object does not have a UUID.
|
|
358
|
+
"""
|
|
359
|
+
if object_dict.get("uuid") is None:
|
|
360
|
+
raise ObjectUUIDError("Object does not have a UUID")
|
|
361
|
+
object_for_upload = UpdateGeoscienceObject.model_validate(object_dict)
|
|
362
|
+
|
|
363
|
+
result = await self._objects_api.update_objects_by_id(
|
|
364
|
+
object_id=str(object_for_upload.uuid),
|
|
365
|
+
org_id=str(self._environment.org_id),
|
|
366
|
+
workspace_id=str(self._environment.workspace_id),
|
|
367
|
+
update_geoscience_object=object_for_upload,
|
|
368
|
+
)
|
|
369
|
+
return self._metadata_from_endpoint_model(result)
|
|
370
|
+
|
|
371
|
+
def _downloaded_object_from_response(self, response: GetObjectResponse) -> DownloadedObject:
|
|
372
|
+
"""Parse object metadata and a geoscience object model instance from a get object response
|
|
373
|
+
|
|
374
|
+
:param response: The response from one of the get object endpoints.
|
|
375
|
+
|
|
376
|
+
:return: A tuple containing the object metadata and a data model of the requested geoscience object.
|
|
377
|
+
"""
|
|
378
|
+
metadata = self._metadata_from_endpoint_model(response)
|
|
379
|
+
urls_by_name = {getattr(link, "name", link.id): link.download_url for link in response.links.data}
|
|
380
|
+
return DownloadedObject(response.object, metadata, urls_by_name, self._connector)
|
|
381
|
+
|
|
382
|
+
async def download_object_by_path(self, path: str, version: str | None = None) -> DownloadedObject:
|
|
383
|
+
"""Download a geoscience object definition (by path).
|
|
384
|
+
|
|
385
|
+
:param path: The path to the geoscience object.
|
|
386
|
+
:param version: The version of the geoscience object to download. This will download the latest version by
|
|
387
|
+
default.
|
|
388
|
+
|
|
389
|
+
:return: A tuple containing the object metadata and a data model of the requested geoscience object.
|
|
390
|
+
"""
|
|
391
|
+
response = await self._objects_api.get_object(
|
|
392
|
+
org_id=str(self._environment.org_id),
|
|
393
|
+
workspace_id=str(self._environment.workspace_id),
|
|
394
|
+
objects_path=path,
|
|
395
|
+
version=version,
|
|
396
|
+
additional_headers={"Accept-Encoding": "gzip"},
|
|
397
|
+
)
|
|
398
|
+
return self._downloaded_object_from_response(response)
|
|
399
|
+
|
|
400
|
+
async def download_object_by_id(self, object_id: UUID, version: str | None = None) -> DownloadedObject:
|
|
401
|
+
"""Download a geoscience object definition (by UUID).
|
|
402
|
+
|
|
403
|
+
:param object_id: The uuid of the geoscience object.
|
|
404
|
+
:param version: The version of the geoscience object to download. This will download the latest version by
|
|
405
|
+
default.
|
|
406
|
+
|
|
407
|
+
:return: A tuple containing the object metadata and a data model of the requested geoscience object.
|
|
408
|
+
"""
|
|
409
|
+
response = await self._objects_api.get_object_by_id(
|
|
410
|
+
org_id=str(self._environment.org_id),
|
|
411
|
+
workspace_id=str(self._environment.workspace_id),
|
|
412
|
+
object_id=str(object_id),
|
|
413
|
+
version=version,
|
|
414
|
+
additional_headers={"Accept-Encoding": "gzip"},
|
|
415
|
+
)
|
|
416
|
+
return self._downloaded_object_from_response(response)
|
|
417
|
+
|
|
418
|
+
async def get_latest_object_versions(self, object_ids: list[UUID], batch_size: int = 500) -> dict[UUID, str]:
|
|
419
|
+
"""Get the latest version of each object by uuid.
|
|
420
|
+
|
|
421
|
+
:param object_ids: A list of object uuids.
|
|
422
|
+
:param batch_size: The maximum number of objects to check in one API call (max 500).
|
|
423
|
+
|
|
424
|
+
:return: A mapping of uuids to the latest version id.
|
|
425
|
+
"""
|
|
426
|
+
offset = 0
|
|
427
|
+
n_objects = len(object_ids)
|
|
428
|
+
latest_ids: dict[UUID, str] = {}
|
|
429
|
+
while batch_object_ids := object_ids[offset : min(offset + batch_size, n_objects)]:
|
|
430
|
+
offset += batch_size
|
|
431
|
+
response = await self._objects_api.list_object_version_ids(
|
|
432
|
+
org_id=str(self._environment.org_id),
|
|
433
|
+
workspace_id=str(self._environment.workspace_id),
|
|
434
|
+
request_body=[str(object_id) for object_id in batch_object_ids],
|
|
435
|
+
)
|
|
436
|
+
latest_ids.update({UUID(latest.object_id): latest.version_id for latest in response})
|
|
437
|
+
return latest_ids
|
evo/objects/data.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
from evo.common import ResourceMetadata
|
|
19
|
+
from evo.workspaces import ServiceUser
|
|
20
|
+
|
|
21
|
+
from .exceptions import SchemaIDFormatError
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ObjectMetadata",
|
|
25
|
+
"ObjectSchema",
|
|
26
|
+
"ObjectVersion",
|
|
27
|
+
"SchemaVersion",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, kw_only=True)
|
|
32
|
+
class ObjectMetadata(ResourceMetadata):
|
|
33
|
+
"""Metadata about a geoscience object."""
|
|
34
|
+
|
|
35
|
+
parent: str
|
|
36
|
+
"""The parent path of the object."""
|
|
37
|
+
|
|
38
|
+
schema_id: ObjectSchema
|
|
39
|
+
"""The geoscience object schema."""
|
|
40
|
+
|
|
41
|
+
version_id: str
|
|
42
|
+
"""An arbitrary identifier for the object version."""
|
|
43
|
+
|
|
44
|
+
modified_at: datetime
|
|
45
|
+
"""The date and time when the object was last modified."""
|
|
46
|
+
|
|
47
|
+
modified_by: ServiceUser | None
|
|
48
|
+
"""The user who last modified the object."""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def path(self) -> str:
|
|
52
|
+
"""The full path of the object, formed by joining the parent and name, separated by a slash ('/')."""
|
|
53
|
+
return f"{self.parent}/{self.name}"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def url(self) -> str:
|
|
57
|
+
"""The url of the object."""
|
|
58
|
+
return "{hub_url}/geoscience-object/orgs/{org_id}/workspaces/{workspace_id}/objects/{object_id}?version={version_id}".format(
|
|
59
|
+
hub_url=self.environment.hub_url.rstrip("/"),
|
|
60
|
+
org_id=self.environment.org_id,
|
|
61
|
+
workspace_id=self.environment.workspace_id,
|
|
62
|
+
object_id=self.id,
|
|
63
|
+
version_id=self.version_id,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True, kw_only=True)
|
|
68
|
+
class ObjectVersion:
|
|
69
|
+
"""Represents a version of an object."""
|
|
70
|
+
|
|
71
|
+
version_id: str
|
|
72
|
+
"""Used by the service to identify a unique resource version."""
|
|
73
|
+
|
|
74
|
+
created_at: datetime
|
|
75
|
+
"""A UTC timestamp representing when the version was uploaded to the service."""
|
|
76
|
+
|
|
77
|
+
created_by: ServiceUser | None
|
|
78
|
+
"""The user that uploaded the version."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True, order=True)
|
|
82
|
+
class SchemaVersion:
|
|
83
|
+
major: int
|
|
84
|
+
minor: int
|
|
85
|
+
patch: int
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def from_str(cls, string: str) -> SchemaVersion:
|
|
89
|
+
return cls(*[int(i) for i in string.split(".")])
|
|
90
|
+
|
|
91
|
+
def __str__(self) -> str:
|
|
92
|
+
return f"{self.major}.{self.minor}.{self.patch}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass(frozen=True)
|
|
96
|
+
class ObjectSchema:
|
|
97
|
+
root_classification: str
|
|
98
|
+
sub_classification: str
|
|
99
|
+
version: SchemaVersion
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def classification(self) -> str:
|
|
103
|
+
return f"{self.root_classification}/{self.sub_classification}"
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_id(cls, schema_id: str) -> ObjectSchema:
|
|
107
|
+
schema_components = re.match(
|
|
108
|
+
r"/(?P<root>[-\w]+)/(?P<sub>[-\w]+)/(?P<version>\d+\.\d+\.\d+)/(?P=sub)\.schema\.json",
|
|
109
|
+
schema_id,
|
|
110
|
+
)
|
|
111
|
+
if schema_components is None:
|
|
112
|
+
raise SchemaIDFormatError(f"Could not parse schema id: '{schema_id}'")
|
|
113
|
+
|
|
114
|
+
return cls(
|
|
115
|
+
root_classification=schema_components.group("root"),
|
|
116
|
+
sub_classification=schema_components.group("sub"),
|
|
117
|
+
version=SchemaVersion.from_str(schema_components.group("version")),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def __str__(self) -> str:
|
|
121
|
+
return f"/{self.classification}/{self.version}/{self.sub_classification}.schema.json"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
"""
|
|
12
|
+
Geoscience Object API
|
|
13
|
+
=============
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
The Geoscience Object API enables technological integrations at the object level. It enables users to access and utilise their data across all products through a common and accessible data structure.
|
|
17
|
+
|
|
18
|
+
A Geoscience Object is a data structure that represents a concrete geological, geotechnical, or geophysical concept. Geoscience Objects can be referenced by their UUID or by a user-defined object path.
|
|
19
|
+
|
|
20
|
+
For more information on using the Geoscience Object API, see the [Geoscience Object API overview](/docs/guides/objects), or the API references here.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
This code is generated from the OpenAPI specification for Geoscience Object API.
|
|
24
|
+
API version: 1.14.0
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Import endpoint apis.
|
|
28
|
+
from .api import DataApi, MetadataApi, ObjectsApi, StagesApi
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"DataApi",
|
|
32
|
+
"MetadataApi",
|
|
33
|
+
"ObjectsApi",
|
|
34
|
+
"StagesApi",
|
|
35
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Copyright © 2025 Bentley Systems, Incorporated
|
|
2
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
|
+
# you may not use this file except in compliance with the License.
|
|
4
|
+
# You may obtain a copy of the License at
|
|
5
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
7
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
8
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
9
|
+
# See the License for the specific language governing permissions and
|
|
10
|
+
# limitations under the License.
|
|
11
|
+
|
|
12
|
+
from .data_api import DataApi # noqa: F401
|
|
13
|
+
from .metadata_api import MetadataApi # noqa: F401
|
|
14
|
+
from .objects_api import ObjectsApi # noqa: F401
|
|
15
|
+
from .stages_api import StagesApi # noqa: F401
|