evo-objects 0.3.1__tar.gz → 0.3.3__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. evo_objects-0.3.3/.gitignore +33 -0
  2. {evo_objects-0.3.1 → evo_objects-0.3.3}/PKG-INFO +2 -2
  3. {evo_objects-0.3.1 → evo_objects-0.3.3}/pyproject.toml +2 -2
  4. evo_objects-0.3.3/src/evo/objects/client/object_client.py +526 -0
  5. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/__init__.py +1 -1
  6. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/api/data_api.py +4 -3
  7. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/api/metadata_api.py +3 -2
  8. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/api/objects_api.py +87 -12
  9. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/api/stages_api.py +4 -3
  10. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/models.py +30 -27
  11. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/exceptions.py +9 -0
  12. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/parquet/__init__.py +3 -1
  13. evo_objects-0.3.3/src/evo/objects/parquet/types.py +95 -0
  14. evo_objects-0.3.1/.gitignore +0 -24
  15. evo_objects-0.3.1/src/evo/objects/client/object_client.py +0 -247
  16. evo_objects-0.3.1/src/evo/objects/parquet/types.py +0 -42
  17. {evo_objects-0.3.1 → evo_objects-0.3.3}/LICENSE.md +0 -0
  18. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/__init__.py +0 -0
  19. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/_model_config.py +0 -0
  20. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/client/__init__.py +0 -0
  21. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/client/api_client.py +0 -0
  22. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/client/parse.py +0 -0
  23. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/data.py +0 -0
  24. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/endpoints/api/__init__.py +0 -0
  25. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/io.py +0 -0
  26. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/parquet/loader.py +0 -0
  27. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/py.typed +0 -0
  28. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/utils/__init__.py +0 -0
  29. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/utils/data.py +0 -0
  30. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/utils/table_formats.py +0 -0
  31. {evo_objects-0.3.1 → evo_objects-0.3.3}/src/evo/objects/utils/tables.py +0 -0
@@ -0,0 +1,33 @@
1
+ # IDE Settings
2
+ .idea/
3
+ .project
4
+ .pydevproject
5
+ .vscode/
6
+
7
+ # Distribution / Packaging
8
+ *.egg*
9
+ *.whl
10
+ __pycache__/
11
+ site/
12
+ build/
13
+
14
+ # Reporting
15
+ .coverage
16
+
17
+ # Environmnents
18
+ venv/
19
+ venv*/
20
+ .venv/
21
+ .env
22
+ .tox/
23
+
24
+ # Jupyter Notebook checkpoints
25
+ .ipynb_checkpoints/
26
+
27
+ # Sample data directories (ignore all data except input folders)
28
+ samples/*/publish*/data/*
29
+ samples/*/download*/data/*
30
+ !samples/*/publish*/data/input/
31
+
32
+ # macOS invisible files
33
+ .DS_Store
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo-objects
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for using the Seequent Evo Geoscience Object API
5
5
  Project-URL: Source, https://github.com/SeequentEvo/evo-python-sdk
6
6
  Project-URL: Tracker, https://github.com/SeequentEvo/evo-python-sdk/issues
@@ -9,7 +9,7 @@ Project-URL: Documentation, https://developer.seequent.com/
9
9
  Author-email: Seequent <support@seequent.com>
10
10
  License-File: LICENSE.md
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: evo-sdk-common[jmespath]>=0.5.4
12
+ Requires-Dist: evo-sdk-common[jmespath]>=0.5.8
13
13
  Requires-Dist: pydantic<3,>=2
14
14
  Provides-Extra: aiohttp
15
15
  Requires-Dist: evo-sdk-common[aiohttp]; extra == 'aiohttp'
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "evo-objects"
3
3
  description = "Python SDK for using the Seequent Evo Geoscience Object API"
4
- version = "0.3.1"
4
+ version = "0.3.3"
5
5
  requires-python = ">=3.10"
6
6
  license-files = ["LICENSE.md"]
7
7
  dynamic = ["readme"]
@@ -10,7 +10,7 @@ authors = [
10
10
  ]
11
11
 
12
12
  dependencies = [
13
- "evo-sdk-common[jmespath]>=0.5.4",
13
+ "evo-sdk-common[jmespath]>=0.5.8",
14
14
  "pydantic>=2,<3",
15
15
  ]
16
16
 
@@ -0,0 +1,526 @@
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 contextlib
15
+ from collections.abc import AsyncGenerator, Iterator, Sequence
16
+ from typing import Any, TypeVar
17
+ from uuid import UUID
18
+
19
+ from pydantic import ConfigDict, TypeAdapter
20
+
21
+ from evo import jmespath, logging
22
+ from evo.common import APIConnector, ICache, IFeedback
23
+ from evo.common.io.exceptions import DataNotFoundError
24
+ from evo.common.utils import NoFeedback, PartialFeedback
25
+
26
+ from ..data import ObjectMetadata, ObjectReference, ObjectSchema
27
+ from ..endpoints import ObjectsApi, models
28
+ from ..io import ObjectDataDownload
29
+ from . import parse
30
+
31
+ try:
32
+ import pyarrow as pa
33
+ import pyarrow.compute as pc
34
+
35
+ from ..parquet import (
36
+ AttributeInfo,
37
+ CategoryInfo,
38
+ ParquetDownloader,
39
+ ParquetLoader,
40
+ TableInfo,
41
+ )
42
+ except ImportError as e:
43
+ print(e)
44
+ _LOADER_AVAILABLE = False
45
+ else:
46
+ _LOADER_AVAILABLE = True
47
+
48
+ _TABLE_INFO_VALIDATOR: TypeAdapter[TableInfo] = TypeAdapter(TableInfo, config=ConfigDict(extra="ignore"))
49
+ _CATEGORY_INFO_VALIDATOR: TypeAdapter[CategoryInfo] = TypeAdapter(CategoryInfo)
50
+ _NAN_VALIDATOR: TypeAdapter[list[int] | list[float]] = TypeAdapter(
51
+ list[int] | list[float], config=ConfigDict(extra="ignore")
52
+ )
53
+ _ATTRIBUTE_VALIDATOR: TypeAdapter[AttributeInfo] = TypeAdapter(AttributeInfo)
54
+
55
+ try:
56
+ import pandas as pd
57
+ except ImportError:
58
+ _PD_AVAILABLE = False
59
+ else:
60
+ _PD_AVAILABLE = True
61
+
62
+ try:
63
+ import numpy as np
64
+ except ImportError:
65
+ _NP_AVAILABLE = False
66
+ else:
67
+ _NP_AVAILABLE = True
68
+
69
+ __all__ = ["DownloadedObject"]
70
+
71
+ logger = logging.getLogger("object.client")
72
+
73
+ _T = TypeVar("_T")
74
+
75
+
76
+ def _split_feedback(left: int, right: int) -> float:
77
+ """Helper to split feedback range into two parts based on left and right sizes.
78
+
79
+ :param left: Number of parts for the left side of the split.
80
+ :param right: Number of parts for the right side of the split.
81
+
82
+ :return: Proportion of feedback to allocate to the left side (between 0.0 and 1.0).
83
+ The right side will be the remainder (1.0 - proportion).
84
+
85
+ :raises ValueError: If left or right is negative.
86
+ """
87
+ if left < 0 or right < 0:
88
+ raise ValueError("Left and right sizes must be non-negative")
89
+ elif left >= 0 and right == 0:
90
+ return 1.0 # Left gets all feedback if right is zero
91
+ elif right > 0 and left == 0:
92
+ return 0.0 # Right gets all feedback if left is zero
93
+ else:
94
+ return left / (left + right) # Proportion of feedback for left
95
+
96
+
97
+ class DownloadedObject:
98
+ """A downloaded geoscience object."""
99
+
100
+ def __init__(
101
+ self,
102
+ object_: models.GeoscienceObject,
103
+ metadata: ObjectMetadata,
104
+ urls_by_name: dict[str, str],
105
+ connector: APIConnector,
106
+ cache: ICache | None = None,
107
+ ) -> None:
108
+ """
109
+ :param object_: The raw geoscience object model.
110
+ :param metadata: The parsed metadata for the object.
111
+ :param urls_by_name: A mapping of data names to their initial download URLs.
112
+ :param connector: The API connector to use for downloading data.
113
+ :param cache: An optional cache to use for data downloads.
114
+ """
115
+ self._object = object_
116
+ self._metadata = metadata
117
+ self._urls_by_name = urls_by_name
118
+ self._connector = connector
119
+ self._cache = cache
120
+
121
+ @staticmethod
122
+ async def from_reference(
123
+ connector: APIConnector,
124
+ reference: ObjectReference | str,
125
+ cache: ICache | None = None,
126
+ request_timeout: int | float | tuple[int | float, int | float] | None = None,
127
+ ) -> DownloadedObject:
128
+ """Download a geoscience object from the service, given an object reference.
129
+
130
+ :param connector: The API connector to use for downloading data.
131
+ :param reference: The reference to the object to download, or a URL as a string that can be parsed into
132
+ a reference.
133
+ :param cache: An optional cache to use for data downloads.
134
+ :param request_timeout: An optional timeout to use for API requests. See evo.common.APIConnector for details.
135
+
136
+ :raises ValueError: If the reference is invalid, or if the connector base URL does not match the reference hub URL.
137
+ """
138
+ ref = ObjectReference(reference) # Parse the reference if it's a string
139
+
140
+ if connector.base_url != ref.hub_url:
141
+ raise ValueError(
142
+ f"The connector base URL '{connector.base_url}' does not match the reference hub URL '{ref.hub_url}'"
143
+ )
144
+
145
+ api = ObjectsApi(connector)
146
+
147
+ request_kwargs = dict(
148
+ org_id=str(ref.org_id),
149
+ workspace_id=str(ref.workspace_id),
150
+ version=ref.version_id,
151
+ additional_headers={"Accept-Encoding": "gzip"},
152
+ request_timeout=request_timeout,
153
+ )
154
+
155
+ if ref.object_id is not None and ref.object_path is not None:
156
+ raise ValueError("Only one of object_id or object_path should be provided")
157
+
158
+ if ref.object_id is not None:
159
+ response = await api.get_object_by_id(object_id=ref.object_id, **request_kwargs)
160
+ elif ref.object_path is not None:
161
+ response = await api.get_object(objects_path=ref.object_path, **request_kwargs)
162
+ else:
163
+ raise ValueError("Either object_id or object_path must be provided")
164
+
165
+ metadata = parse.object_metadata(response, ref.environment)
166
+ urls_by_name = {getattr(link, "name", link.id): link.download_url for link in response.links.data}
167
+ return DownloadedObject(
168
+ object_=response.object,
169
+ metadata=metadata,
170
+ urls_by_name=urls_by_name,
171
+ connector=connector,
172
+ cache=cache,
173
+ )
174
+
175
+ @property
176
+ def schema(self) -> ObjectSchema:
177
+ """The schema of the object."""
178
+ return self._metadata.schema_id
179
+
180
+ @property
181
+ def metadata(self) -> ObjectMetadata:
182
+ """The metadata of the object."""
183
+ return self._metadata
184
+
185
+ def as_dict(self) -> dict:
186
+ """Get this object as a dictionary."""
187
+ return self._object.model_dump(mode="python", by_alias=True)
188
+
189
+ def search(self, expression: str) -> Any:
190
+ """Search the object metadata using a JMESPath expression.
191
+
192
+ :param expression: The JMESPath expression to use for the search.
193
+
194
+ :return: The result of the search.
195
+ """
196
+ return jmespath.search(expression, self.as_dict())
197
+
198
+ def prepare_data_download(self, data_identifiers: Sequence[str | UUID]) -> Iterator[ObjectDataDownload]:
199
+ """Prepare to download multiple data files from the geoscience object service, for this object.
200
+
201
+ Any data IDs that are not associated with the requested object will raise a DataNotFoundError.
202
+
203
+ :param data_identifiers: A list of sha256 digests or UUIDs for the data to be downloaded.
204
+
205
+ :return: An iterator of data download contexts that can be used to download the data.
206
+
207
+ :raises DataNotFoundError: If any requested data ID is not associated with this object.
208
+ """
209
+ try:
210
+ filtered_urls_by_name = {str(name): self._urls_by_name[str(name)] for name in data_identifiers}
211
+ except KeyError as exc:
212
+ raise DataNotFoundError(f"Unable to find the requested data: {exc.args[0]}") from exc
213
+ for ctx in ObjectDataDownload._create_multiple(
214
+ connector=self._connector, metadata=self._metadata, urls_by_name=filtered_urls_by_name
215
+ ):
216
+ yield ctx
217
+
218
+ async def update(
219
+ self,
220
+ object_dict: dict,
221
+ check_for_conflict: bool = True,
222
+ request_timeout: int | float | tuple[int | float, int | float] | None = None,
223
+ ) -> DownloadedObject:
224
+ """Update the geoscience object on the geoscience object service. Returning a new DownloadedObject representing
225
+ the new version of the object.
226
+
227
+ This will create a new version of the object, that fully replaces the existing properties of the object with
228
+ those provided in `object_dict`.
229
+
230
+ Note, this will not update the "DownloadedObject" instance in-place - it will still represent the original
231
+ version of the object. You will need to download the updated version separately if you wish to work with it.
232
+
233
+ :param object_dict: The new properties of the object as a dictionary.
234
+ :param check_for_conflict: If True, and if a newer version of the object exists on the geoscience object
235
+ service, the update will fail with a ObjectModifiedError exception. If False, it will not check whether
236
+ there is a newer version, so will perform the update regardless.
237
+ :param request_timeout: An optional timeout to use for API requests. See evo.common.APIConnector for details.
238
+
239
+ :returns: The new version of the object as a DownloadedObject.
240
+ """
241
+
242
+ api = ObjectsApi(self._connector)
243
+
244
+ if "uuid" not in object_dict:
245
+ object_dict["uuid"] = self.metadata.id
246
+
247
+ model = models.UpdateGeoscienceObject.model_validate(object_dict)
248
+ if model.uuid != self.metadata.id:
249
+ raise ValueError("The object ID in the new object does not match the current object ID")
250
+
251
+ response = await api.update_objects_by_id(
252
+ object_id=str(self.metadata.id),
253
+ org_id=str(self.metadata.environment.org_id),
254
+ workspace_id=str(self.metadata.environment.workspace_id),
255
+ update_geoscience_object=model,
256
+ request_timeout=request_timeout,
257
+ if_match=self.metadata.version_id if check_for_conflict else None,
258
+ )
259
+ metadata = parse.object_metadata(response, self.metadata.environment)
260
+ urls_by_name = {getattr(link, "name", link.id): link.download_url for link in response.links.data}
261
+ return DownloadedObject(
262
+ object_=response.object,
263
+ metadata=metadata,
264
+ urls_by_name=urls_by_name,
265
+ connector=self._connector,
266
+ cache=self._cache,
267
+ )
268
+
269
+ if _LOADER_AVAILABLE:
270
+ # Optional support for loading Parquet data using PyArrow.
271
+
272
+ def _validate_typed_dict(self, value: _T | str, validator: TypeAdapter[_T]) -> _T:
273
+ if isinstance(value, str):
274
+ resolved = self.search(value)
275
+ # Implicitly unwrap single-element arrays for convenience
276
+ # This allows, using predicates like: attributes[?name=='my_attribute']
277
+ if isinstance(resolved, jmespath.JMESPathArrayProxy) and len(resolved) == 1:
278
+ resolved = resolved[0]
279
+ if isinstance(resolved, jmespath.JMESPathObjectProxy):
280
+ value = resolved.raw
281
+ else:
282
+ raise ValueError(f"Expected object, got {type(resolved)}")
283
+ return validator.validate_python(value)
284
+
285
+ def _validate_nan_values(self, nan_values: list[int] | list[float] | str | None) -> list[int] | list[float]:
286
+ if nan_values is None:
287
+ return []
288
+ if isinstance(nan_values, str):
289
+ resolved = self.search(nan_values)
290
+ if isinstance(resolved, jmespath.JMESPathArrayProxy) and len(resolved) == 1:
291
+ # Consider single-element arrays for unwrapping
292
+ # This allows, using predicates like: attributes[?name=='my_attribute']
293
+ child = resolved[0]
294
+ if not isinstance(child, (int, float)):
295
+ resolved = child
296
+ if isinstance(resolved, jmespath.JMESPathArrayProxy):
297
+ nan_values = resolved.raw
298
+ # Support passing nan_description structure too
299
+ elif isinstance(resolved, jmespath.JMESPathObjectProxy) and "values" in resolved.raw:
300
+ nan_values = resolved.raw["values"]
301
+ else:
302
+ raise ValueError(f"Expected list, got {type(resolved)}")
303
+ return _NAN_VALIDATOR.validate_python(nan_values)
304
+
305
+ @contextlib.asynccontextmanager
306
+ async def _with_parquet_loader(
307
+ self, table_info: TableInfo | str, fb: IFeedback
308
+ ) -> AsyncGenerator[ParquetLoader, None]:
309
+ """Download parquet data and get a ParquetLoader for the data referenced by the given
310
+ table info or data reference string.
311
+
312
+ :param table_info: The table info dict, JMESPath to table info within the object.
313
+ :param fb: An optional feedback instance to report download progress to.
314
+
315
+ :returns: A ParquetLoader that can be used to read the referenced data.
316
+ """
317
+ table_info = self._validate_typed_dict(table_info, _TABLE_INFO_VALIDATOR)
318
+ (download,) = self.prepare_data_download([table_info["data"]])
319
+ async with ParquetDownloader(download, self._connector.transport, self._cache).with_feedback(fb) as loader:
320
+ loader.validate_with_table_info(table_info)
321
+ yield loader
322
+
323
+ async def download_table(
324
+ self,
325
+ table_info: TableInfo | str,
326
+ fb: IFeedback = NoFeedback,
327
+ *,
328
+ nan_values: list[int] | list[float] | str | None = None,
329
+ column_names: Sequence[str] | None = None,
330
+ ) -> pa.Table:
331
+ """Download the data referenced by the given table info as a PyArrow Table.
332
+
333
+ :param table_info: The table info dict, ot JMESPath to table info within the object.
334
+ :param fb: An optional feedback instance to report download progress to.
335
+ :param nan_values: An optional list of values to treat as null. Can also be a JMESPath expression to the
336
+ list of nan values, or the nan_description structure.
337
+ :param column_names: An optional list of column names for the table, instead of those in the Parquet file.
338
+
339
+ :returns: A PyArrow Table containing the downloaded data.
340
+ """
341
+ async with self._with_parquet_loader(table_info, fb) as loader:
342
+ table = loader.load_as_table()
343
+
344
+ if column_names is None:
345
+ column_names = table.column_names
346
+
347
+ nan_values = self._validate_nan_values(nan_values)
348
+ if len(nan_values) == 0:
349
+ return table.rename_columns(column_names)
350
+
351
+ # Replace specified nan_values with nulls
352
+ arrays = []
353
+ for array in table.columns:
354
+ if isinstance(array, pa.ChunkedArray):
355
+ array = array.combine_chunks()
356
+ null_scalar = pa.scalar(None, type=array.type)
357
+ nan_value_array = pa.array(nan_values, type=array.type)
358
+ arrays.append(pc.replace_with_mask(array, pc.is_in(array, nan_value_array), null_scalar))
359
+ return pa.Table.from_arrays(arrays, names=column_names)
360
+
361
+ async def download_category_table(
362
+ self,
363
+ category_info: CategoryInfo | str,
364
+ *,
365
+ nan_values: list[int] | list[float] | str | None = None,
366
+ column_names: Sequence[str] | None = None,
367
+ fb: IFeedback = NoFeedback,
368
+ ) -> pa.Table:
369
+ """Download the data referenced by the given category info as a PyArrow Table.
370
+
371
+ The arrays into the table will be DictionaryArrays constructed from the values and lookup tables.
372
+
373
+ :param category_info: The category info dict, or JMESPath to the category info within the object.
374
+ :param nan_values: An optional list of values to treat as null. Can also be a JMESPath expression to
375
+ nan_description structure.
376
+ :param column_names: An optional list of column names for the table, instead of those in the Parquet file.
377
+ :param fb: An optional feedback instance to report download progress to.
378
+
379
+ :returns: A PyArrow Table containing the downloaded data.
380
+ """
381
+ category_info = self._validate_typed_dict(category_info, _CATEGORY_INFO_VALIDATOR)
382
+
383
+ v_size = (
384
+ category_info["values"]["length"] * category_info["values"]["width"]
385
+ ) # Total number of cells in values
386
+ t_size = category_info["table"]["length"] * 2 # Lookup tables always have 2 columns
387
+ split = _split_feedback(v_size, t_size)
388
+
389
+ values_table = await self.download_table(
390
+ category_info["values"],
391
+ nan_values=nan_values,
392
+ column_names=column_names,
393
+ fb=PartialFeedback(fb, start=0, end=split),
394
+ )
395
+ lookup_table = await self.download_table(category_info["table"], fb=PartialFeedback(fb, start=split, end=1))
396
+
397
+ arrays = []
398
+ for array in values_table.columns:
399
+ indices = pc.index_in(array, lookup_table[0])
400
+ arrays.append(pa.DictionaryArray.from_arrays(indices, lookup_table[1]))
401
+ return pa.Table.from_arrays(arrays, names=values_table.column_names)
402
+
403
+ async def download_attribute_table(
404
+ self,
405
+ attribute: AttributeInfo | str,
406
+ fb: IFeedback = NoFeedback,
407
+ ) -> pa.Table:
408
+ """Download the data referenced by the given attribute as a PyArrow Table.
409
+
410
+ :param attribute: The attribute info dict, or JMESPath to the attribute info within the object.
411
+ :param fb: An optional feedback instance to report download progress to.
412
+
413
+ :returns: A PyArrow Table containing the downloaded data.
414
+ """
415
+ attribute = self._validate_typed_dict(attribute, _ATTRIBUTE_VALIDATOR)
416
+
417
+ if "table" in attribute:
418
+ table = await self.download_category_table(
419
+ attribute,
420
+ nan_values=attribute["nan_description"]["values"] if "nan_description" in attribute else None,
421
+ fb=fb,
422
+ )
423
+ else:
424
+ table = await self.download_table(
425
+ attribute["values"],
426
+ nan_values=attribute["nan_description"]["values"] if "nan_description" in attribute else None,
427
+ fb=fb,
428
+ )
429
+ if len(table.column_names) == 1:
430
+ table = table.rename_columns([attribute["name"]])
431
+ else:
432
+ table = table.rename_columns([f"{attribute['name']}_{i}" for i in range(len(table.column_names))])
433
+ return table
434
+
435
+ if _PD_AVAILABLE:
436
+ # Optional support for loading data as Pandas DataFrames. Requires parquet support via PyArrow as well.
437
+
438
+ async def download_dataframe(
439
+ self,
440
+ table_info: TableInfo | str,
441
+ fb: IFeedback = NoFeedback,
442
+ *,
443
+ nan_values: list[int] | list[float] | str | None = None,
444
+ column_names: Sequence[str] | None = None,
445
+ ) -> pd.DataFrame:
446
+ """Download the data referenced by the given table info as a Pandas DataFrame.
447
+
448
+ :param table_info: The table info dict, JMESPath to table info within the object.
449
+ :param fb: An optional feedback instance to report download progress to.
450
+ :param nan_values: An optional list of values to treat as null. Can also be a JMESPath expression to
451
+ nan_description structure.
452
+ :param column_names: An optional list of column names for the table, instead of those from the Parquet file.
453
+
454
+ :returns: A Pandas DataFrame containing the downloaded data.
455
+ """
456
+ table = await self.download_table(table_info, fb=fb, nan_values=nan_values, column_names=column_names)
457
+ return table.to_pandas()
458
+
459
+ async def download_category_dataframe(
460
+ self,
461
+ category_info: CategoryInfo | str,
462
+ fb: IFeedback = NoFeedback,
463
+ *,
464
+ nan_values: list[int] | list[float] | str | None = None,
465
+ column_names: Sequence[str] | None = None,
466
+ ) -> pd.DataFrame:
467
+ """Download the data referenced by the given category info as a Pandas DataFrame.
468
+
469
+ :param category_info: The category info dict, or JMESPath to the category info within the object.
470
+ :param nan_values: An optional list of values to treat as null. Can also be a JMESPath expression to
471
+ nan_description structure.
472
+ :param column_names: An optional list of column names for the table, instead of those from the Parquet file.
473
+ :param fb: An optional feedback instance to report download progress to.
474
+
475
+ :returns: A Pandas DataFrame containing the downloaded data.
476
+ """
477
+ table = await self.download_category_table(
478
+ category_info, fb=fb, nan_values=nan_values, column_names=column_names
479
+ )
480
+ return table.to_pandas()
481
+
482
+ async def download_attribute_dataframe(
483
+ self,
484
+ attribute: AttributeInfo | str,
485
+ fb: IFeedback = NoFeedback,
486
+ ) -> pd.DataFrame:
487
+ """Download the data referenced by the given attribute as a Pandas DataFrame.
488
+
489
+ :param attribute: The attribute info dict, or JMESPath to the attribute within the object.
490
+ :param fb: An optional feedback instance to report download progress to.
491
+
492
+ :returns: A Pandas DataFrame containing the downloaded data.
493
+ """
494
+ attribute = self._validate_typed_dict(attribute, _ATTRIBUTE_VALIDATOR)
495
+
496
+ if "table" in attribute:
497
+ df = await self.download_category_dataframe(
498
+ attribute,
499
+ nan_values=attribute["nan_description"]["values"] if "nan_description" in attribute else None,
500
+ fb=fb,
501
+ )
502
+ else:
503
+ df = await self.download_dataframe(
504
+ attribute["values"],
505
+ nan_values=attribute["nan_description"]["values"] if "nan_description" in attribute else None,
506
+ fb=fb,
507
+ )
508
+ if len(df.columns) == 1:
509
+ df.columns = [attribute["name"]]
510
+ else:
511
+ df.columns = [f"{attribute['name']}_{i}" for i in range(len(df.columns))]
512
+ return df
513
+
514
+ if _NP_AVAILABLE:
515
+ # Optional support for loading data as NumPy arrays. Requires parquet support via PyArrow as well.
516
+
517
+ async def download_array(self, table_info: TableInfo | str, fb: IFeedback = NoFeedback) -> np.ndarray:
518
+ """Download the data referenced by the given table info as a NumPy array.
519
+
520
+ :param table_info: The table info dict, JMESPath to table info within the object.
521
+ :param fb: An optional feedback instance to report download progress to.
522
+
523
+ :returns: A NumPy array containing the downloaded data.
524
+ """
525
+ async with self._with_parquet_loader(table_info, fb) as loader:
526
+ return loader.load_as_array()
@@ -21,7 +21,7 @@ For more information on using the Geoscience Object API, see the [Geoscience Obj
21
21
 
22
22
 
23
23
  This code is generated from the OpenAPI specification for Geoscience Object API.
24
- API version: 1.14.0
24
+ API version: 1.21.0
25
25
  """
26
26
 
27
27
  # Import endpoint apis.
@@ -21,11 +21,12 @@ For more information on using the Geoscience Object API, see the [Geoscience Obj
21
21
 
22
22
 
23
23
  This code is generated from the OpenAPI specification for Geoscience Object API.
24
- API version: 1.14.0
24
+ API version: 1.21.0
25
25
  """
26
26
 
27
27
  from evo.common.connector import APIConnector
28
- from evo.common.data import EmptyResponse, RequestMethod # noqa: F401
28
+ from evo.common.data import RequestMethod
29
+ from evo.common.utils import get_header_metadata
29
30
 
30
31
  from ..models import * # noqa: F403
31
32
 
@@ -93,7 +94,7 @@ class DataApi:
93
94
  _header_params = {
94
95
  "Content-Type": "application/json",
95
96
  "Accept": "application/json",
96
- }
97
+ } | get_header_metadata(__name__)
97
98
  if additional_headers is not None:
98
99
  _header_params.update(additional_headers)
99
100
 
@@ -21,11 +21,12 @@ For more information on using the Geoscience Object API, see the [Geoscience Obj
21
21
 
22
22
 
23
23
  This code is generated from the OpenAPI specification for Geoscience Object API.
24
- API version: 1.14.0
24
+ API version: 1.21.0
25
25
  """
26
26
 
27
27
  from evo.common.connector import APIConnector
28
28
  from evo.common.data import EmptyResponse, RequestMethod
29
+ from evo.common.utils import get_header_metadata
29
30
 
30
31
  from ..models import * # noqa: F403
31
32
 
@@ -105,7 +106,7 @@ class MetadataApi:
105
106
  # Prepare the header parameters.
106
107
  _header_params = {
107
108
  "Content-Type": "application/json",
108
- }
109
+ } | get_header_metadata(__name__)
109
110
  if additional_headers is not None:
110
111
  _header_params.update(additional_headers)
111
112