evo-objects 0.3.3__tar.gz → 0.4.0__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.
- {evo_objects-0.3.3 → evo_objects-0.4.0}/.gitignore +10 -1
- {evo_objects-0.3.3 → evo_objects-0.4.0}/PKG-INFO +3 -1
- {evo_objects-0.3.3 → evo_objects-0.4.0}/pyproject.toml +3 -2
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/client/api_client.py +16 -1
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/client/object_client.py +62 -27
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/parquet/__init__.py +1 -1
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/parquet/loader.py +1 -2
- evo_objects-0.4.0/src/evo/objects/typed/__init__.py +140 -0
- evo_objects-0.4.0/src/evo/objects/typed/_data.py +146 -0
- evo_objects-0.4.0/src/evo/objects/typed/_grid.py +259 -0
- evo_objects-0.4.0/src/evo/objects/typed/_model.py +438 -0
- evo_objects-0.4.0/src/evo/objects/typed/_utils.py +188 -0
- evo_objects-0.4.0/src/evo/objects/typed/attributes.py +530 -0
- evo_objects-0.4.0/src/evo/objects/typed/base.py +500 -0
- evo_objects-0.4.0/src/evo/objects/typed/block_model_ref.py +386 -0
- evo_objects-0.4.0/src/evo/objects/typed/exceptions.py +22 -0
- evo_objects-0.4.0/src/evo/objects/typed/pointset.py +141 -0
- evo_objects-0.4.0/src/evo/objects/typed/regular_grid.py +84 -0
- evo_objects-0.4.0/src/evo/objects/typed/regular_masked_grid.py +191 -0
- evo_objects-0.4.0/src/evo/objects/typed/spatial.py +76 -0
- evo_objects-0.4.0/src/evo/objects/typed/tensor_grid.py +162 -0
- evo_objects-0.4.0/src/evo/objects/typed/types.py +388 -0
- evo_objects-0.4.0/src/evo/objects/typed/variogram.py +663 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/utils/__init__.py +6 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/utils/data.py +140 -18
- evo_objects-0.4.0/src/evo/objects/utils/table_formats.py +221 -0
- {evo_objects-0.3.3/src/evo/objects/parquet → evo_objects-0.4.0/src/evo/objects/utils}/types.py +1 -0
- evo_objects-0.3.3/src/evo/objects/utils/table_formats.py +0 -158
- {evo_objects-0.3.3 → evo_objects-0.4.0}/LICENSE.md +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/__init__.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/_model_config.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/client/__init__.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/client/parse.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/data.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/__init__.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/api/__init__.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/api/data_api.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/api/metadata_api.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/api/objects_api.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/api/stages_api.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/endpoints/models.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/exceptions.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/io.py +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/py.typed +0 -0
- {evo_objects-0.3.3 → evo_objects-0.4.0}/src/evo/objects/utils/tables.py +0 -0
|
@@ -14,7 +14,7 @@ build/
|
|
|
14
14
|
# Reporting
|
|
15
15
|
.coverage
|
|
16
16
|
|
|
17
|
-
#
|
|
17
|
+
# Environments
|
|
18
18
|
venv/
|
|
19
19
|
venv*/
|
|
20
20
|
.venv/
|
|
@@ -31,3 +31,12 @@ samples/*/download*/data/*
|
|
|
31
31
|
|
|
32
32
|
# macOS invisible files
|
|
33
33
|
.DS_Store
|
|
34
|
+
|
|
35
|
+
# MkDocs
|
|
36
|
+
mkdocs/site/*
|
|
37
|
+
!mkdocs/site/packages/
|
|
38
|
+
!mkdocs/site/packages/**
|
|
39
|
+
!mkdocs/site/
|
|
40
|
+
|
|
41
|
+
# Other
|
|
42
|
+
drilling_campaign.xlsx
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
@@ -13,6 +13,8 @@ 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'
|
|
16
|
+
Provides-Extra: blockmodels
|
|
17
|
+
Requires-Dist: evo-blockmodels[utils]>=0.2.0; extra == 'blockmodels'
|
|
16
18
|
Provides-Extra: notebooks
|
|
17
19
|
Requires-Dist: evo-sdk-common[notebooks]; extra == 'notebooks'
|
|
18
20
|
Provides-Extra: utils
|
|
@@ -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.
|
|
4
|
+
version = "0.4.0"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
license-files = ["LICENSE.md"]
|
|
7
7
|
dynamic = ["readme"]
|
|
@@ -24,11 +24,12 @@ Documentation = "https://developer.seequent.com/"
|
|
|
24
24
|
aiohttp = ["evo-sdk-common[aiohttp]"]
|
|
25
25
|
notebooks = ["evo-sdk-common[notebooks]"]
|
|
26
26
|
utils = ["pyarrow", "pyarrow-stubs", "pandas", "numpy"]
|
|
27
|
+
blockmodels = ["evo-blockmodels[utils]>=0.2.0"]
|
|
27
28
|
|
|
28
29
|
[dependency-groups]
|
|
29
30
|
# Dev dependencies. The version is left unspecified so the latest is installed.
|
|
30
31
|
test = [
|
|
31
|
-
"evo-objects[aiohttp,utils]",
|
|
32
|
+
"evo-objects[aiohttp,utils,blockmodels]",
|
|
32
33
|
"pandas",
|
|
33
34
|
"parameterized==0.9.0",
|
|
34
35
|
"pytest",
|
|
@@ -15,7 +15,7 @@ from collections.abc import AsyncIterator, Sequence
|
|
|
15
15
|
from uuid import UUID
|
|
16
16
|
|
|
17
17
|
from evo import logging
|
|
18
|
-
from evo.common import APIConnector, BaseAPIClient, HealthCheckType, ICache, Page, ServiceHealth
|
|
18
|
+
from evo.common import APIConnector, BaseAPIClient, HealthCheckType, ICache, IContext, Page, ServiceHealth
|
|
19
19
|
from evo.common.data import EmptyResponse, Environment, OrderByOperatorEnum
|
|
20
20
|
from evo.common.utils import get_service_health, parse_order_by
|
|
21
21
|
|
|
@@ -52,6 +52,21 @@ class ObjectAPIClient(BaseAPIClient):
|
|
|
52
52
|
self._metadata_api = MetadataApi(connector=connector)
|
|
53
53
|
self._cache = cache
|
|
54
54
|
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_context(cls, context: IContext) -> ObjectAPIClient:
|
|
57
|
+
"""Create a ObjectAPIClient from the given context.
|
|
58
|
+
|
|
59
|
+
The context must have a hub_url, org_id, and workspace_id set.
|
|
60
|
+
|
|
61
|
+
:param context: The context to create the client from.
|
|
62
|
+
:return: A ObjectAPIClient instance.
|
|
63
|
+
"""
|
|
64
|
+
return cls(
|
|
65
|
+
environment=context.get_environment(),
|
|
66
|
+
connector=context.get_connector(),
|
|
67
|
+
cache=context.get_cache(),
|
|
68
|
+
)
|
|
69
|
+
|
|
55
70
|
async def get_service_health(self, check_type: HealthCheckType = HealthCheckType.FULL) -> ServiceHealth:
|
|
56
71
|
"""Get the health of the geoscience object service.
|
|
57
72
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import asyncio
|
|
14
15
|
import contextlib
|
|
15
16
|
from collections.abc import AsyncGenerator, Iterator, Sequence
|
|
16
17
|
from typing import Any, TypeVar
|
|
@@ -19,9 +20,9 @@ from uuid import UUID
|
|
|
19
20
|
from pydantic import ConfigDict, TypeAdapter
|
|
20
21
|
|
|
21
22
|
from evo import jmespath, logging
|
|
22
|
-
from evo.common import APIConnector, ICache, IFeedback
|
|
23
|
+
from evo.common import APIConnector, Environment, ICache, IContext, IFeedback
|
|
23
24
|
from evo.common.io.exceptions import DataNotFoundError
|
|
24
|
-
from evo.common.utils import NoFeedback,
|
|
25
|
+
from evo.common.utils import NoFeedback, split_feedback
|
|
25
26
|
|
|
26
27
|
from ..data import ObjectMetadata, ObjectReference, ObjectSchema
|
|
27
28
|
from ..endpoints import ObjectsApi, models
|
|
@@ -73,29 +74,12 @@ logger = logging.getLogger("object.client")
|
|
|
73
74
|
_T = TypeVar("_T")
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
"""
|
|
77
|
+
class DownloadedObject(IContext):
|
|
78
|
+
"""A downloaded geoscience object.
|
|
78
79
|
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
80
|
+
This class also implements the IContext interface, allowing it to be used
|
|
81
|
+
directly as a context for further API operations.
|
|
86
82
|
"""
|
|
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
83
|
|
|
100
84
|
def __init__(
|
|
101
85
|
self,
|
|
@@ -172,6 +156,25 @@ class DownloadedObject:
|
|
|
172
156
|
cache=cache,
|
|
173
157
|
)
|
|
174
158
|
|
|
159
|
+
@staticmethod
|
|
160
|
+
async def from_context(
|
|
161
|
+
context: IContext,
|
|
162
|
+
reference: ObjectReference | str,
|
|
163
|
+
) -> DownloadedObject:
|
|
164
|
+
"""Download a geoscience object from the service using a context.
|
|
165
|
+
|
|
166
|
+
:param context: The context providing the connector and cache.
|
|
167
|
+
:param reference: The reference to the object to download, or a URL as a string that can be parsed into
|
|
168
|
+
a reference.
|
|
169
|
+
|
|
170
|
+
:raises ValueError: If the reference is invalid, or if the connector base URL does not match the reference hub URL.
|
|
171
|
+
"""
|
|
172
|
+
return await DownloadedObject.from_reference(
|
|
173
|
+
connector=context.get_connector(),
|
|
174
|
+
reference=reference,
|
|
175
|
+
cache=context.get_cache(),
|
|
176
|
+
)
|
|
177
|
+
|
|
175
178
|
@property
|
|
176
179
|
def schema(self) -> ObjectSchema:
|
|
177
180
|
"""The schema of the object."""
|
|
@@ -182,6 +185,36 @@ class DownloadedObject:
|
|
|
182
185
|
"""The metadata of the object."""
|
|
183
186
|
return self._metadata
|
|
184
187
|
|
|
188
|
+
# IContext interface implementation
|
|
189
|
+
|
|
190
|
+
def get_environment(self) -> Environment:
|
|
191
|
+
"""Gets the Environment associated with this object.
|
|
192
|
+
|
|
193
|
+
:return: The Environment.
|
|
194
|
+
"""
|
|
195
|
+
return self._metadata.environment
|
|
196
|
+
|
|
197
|
+
def get_org_id(self) -> UUID:
|
|
198
|
+
"""Gets the organization ID associated with this object.
|
|
199
|
+
|
|
200
|
+
:return: The organization ID.
|
|
201
|
+
"""
|
|
202
|
+
return self._metadata.environment.org_id
|
|
203
|
+
|
|
204
|
+
def get_connector(self) -> APIConnector:
|
|
205
|
+
"""Gets the APIConnector associated with this object.
|
|
206
|
+
|
|
207
|
+
:return: The APIConnector.
|
|
208
|
+
"""
|
|
209
|
+
return self._connector
|
|
210
|
+
|
|
211
|
+
def get_cache(self) -> ICache | None:
|
|
212
|
+
"""Gets the ICache associated with this object, if any.
|
|
213
|
+
|
|
214
|
+
:return: The ICache, or None if no cache is associated.
|
|
215
|
+
"""
|
|
216
|
+
return self._cache
|
|
217
|
+
|
|
185
218
|
def as_dict(self) -> dict:
|
|
186
219
|
"""Get this object as a dictionary."""
|
|
187
220
|
return self._object.model_dump(mode="python", by_alias=True)
|
|
@@ -384,15 +417,17 @@ class DownloadedObject:
|
|
|
384
417
|
category_info["values"]["length"] * category_info["values"]["width"]
|
|
385
418
|
) # Total number of cells in values
|
|
386
419
|
t_size = category_info["table"]["length"] * 2 # Lookup tables always have 2 columns
|
|
387
|
-
|
|
420
|
+
values_fb, table_fb = split_feedback(fb, [v_size, t_size])
|
|
388
421
|
|
|
389
|
-
|
|
422
|
+
# Download both tables concurrently
|
|
423
|
+
values_table_coro = self.download_table(
|
|
390
424
|
category_info["values"],
|
|
391
425
|
nan_values=nan_values,
|
|
392
426
|
column_names=column_names,
|
|
393
|
-
fb=
|
|
427
|
+
fb=values_fb,
|
|
394
428
|
)
|
|
395
|
-
|
|
429
|
+
lookup_table_coro = self.download_table(category_info["table"], fb=table_fb)
|
|
430
|
+
values_table, lookup_table = await asyncio.gather(values_table_coro, lookup_table_coro)
|
|
396
431
|
|
|
397
432
|
arrays = []
|
|
398
433
|
for array in values_table.columns:
|
|
@@ -14,8 +14,8 @@ try:
|
|
|
14
14
|
except ImportError:
|
|
15
15
|
raise ImportError("The 'pyarrow' package is required to use ParquetLoader") from None
|
|
16
16
|
|
|
17
|
+
from ..utils.types import ArrayTableInfo, AttributeInfo, CategoryInfo, LookupTableInfo, TableInfo
|
|
17
18
|
from .loader import ParquetDownloader, ParquetLoader
|
|
18
|
-
from .types import ArrayTableInfo, AttributeInfo, CategoryInfo, LookupTableInfo, TableInfo
|
|
19
19
|
|
|
20
20
|
__all__ = [
|
|
21
21
|
"ArrayTableInfo",
|
|
@@ -27,8 +27,7 @@ from evo.common.io import BytesDestination, ChunkedIOManager, Download, HTTPSour
|
|
|
27
27
|
from evo.common.utils import NoFeedback
|
|
28
28
|
|
|
29
29
|
from ..exceptions import SchemaValidationError, TableFormatError
|
|
30
|
-
from ..utils import ArrowTableFormat, KnownTableFormat
|
|
31
|
-
from .types import TableInfo
|
|
30
|
+
from ..utils import ArrowTableFormat, KnownTableFormat, TableInfo
|
|
32
31
|
|
|
33
32
|
try:
|
|
34
33
|
import pandas as pd
|
|
@@ -0,0 +1,140 @@
|
|
|
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 ._grid import BlockModelData, BlockModelGeometry
|
|
13
|
+
from .attributes import (
|
|
14
|
+
Attribute,
|
|
15
|
+
Attributes,
|
|
16
|
+
BlockModelAttribute,
|
|
17
|
+
BlockModelAttributes,
|
|
18
|
+
BlockModelPendingAttribute,
|
|
19
|
+
PendingAttribute,
|
|
20
|
+
)
|
|
21
|
+
from .base import BaseObject, object_from_path, object_from_reference, object_from_uuid
|
|
22
|
+
from .block_model_ref import (
|
|
23
|
+
BlockModel,
|
|
24
|
+
)
|
|
25
|
+
from .pointset import (
|
|
26
|
+
Locations,
|
|
27
|
+
PointSet,
|
|
28
|
+
PointSetData,
|
|
29
|
+
)
|
|
30
|
+
from .regular_grid import (
|
|
31
|
+
Regular3DGrid,
|
|
32
|
+
Regular3DGridData,
|
|
33
|
+
)
|
|
34
|
+
from .regular_masked_grid import (
|
|
35
|
+
MaskedCells,
|
|
36
|
+
RegularMasked3DGrid,
|
|
37
|
+
RegularMasked3DGridData,
|
|
38
|
+
)
|
|
39
|
+
from .spatial import BaseSpatialObject
|
|
40
|
+
from .tensor_grid import (
|
|
41
|
+
Tensor3DGrid,
|
|
42
|
+
Tensor3DGridData,
|
|
43
|
+
)
|
|
44
|
+
from .types import (
|
|
45
|
+
BoundingBox,
|
|
46
|
+
CoordinateReferenceSystem,
|
|
47
|
+
Ellipsoid,
|
|
48
|
+
EllipsoidRanges,
|
|
49
|
+
EpsgCode,
|
|
50
|
+
Point3,
|
|
51
|
+
Rotation,
|
|
52
|
+
Size3d,
|
|
53
|
+
Size3i,
|
|
54
|
+
)
|
|
55
|
+
from .variogram import (
|
|
56
|
+
CubicStructure,
|
|
57
|
+
ExponentialStructure,
|
|
58
|
+
GaussianStructure,
|
|
59
|
+
GeneralisedCauchyStructure,
|
|
60
|
+
LinearStructure,
|
|
61
|
+
SphericalStructure,
|
|
62
|
+
SpheroidalStructure,
|
|
63
|
+
Variogram,
|
|
64
|
+
VariogramCurveData,
|
|
65
|
+
VariogramData,
|
|
66
|
+
VariogramStructure,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"Attribute",
|
|
71
|
+
"Attributes",
|
|
72
|
+
"BaseObject",
|
|
73
|
+
"BaseSpatialObject",
|
|
74
|
+
"BlockModel",
|
|
75
|
+
"BlockModelAttribute",
|
|
76
|
+
"BlockModelAttributes",
|
|
77
|
+
"BlockModelData",
|
|
78
|
+
"BlockModelGeometry",
|
|
79
|
+
"BlockModelPendingAttribute",
|
|
80
|
+
"BoundingBox",
|
|
81
|
+
"CoordinateReferenceSystem",
|
|
82
|
+
"CubicStructure",
|
|
83
|
+
"Ellipsoid",
|
|
84
|
+
"EllipsoidRanges",
|
|
85
|
+
"EpsgCode",
|
|
86
|
+
"ExponentialStructure",
|
|
87
|
+
"GaussianStructure",
|
|
88
|
+
"GeneralisedCauchyStructure",
|
|
89
|
+
"LinearStructure",
|
|
90
|
+
"Locations",
|
|
91
|
+
"MaskedCells",
|
|
92
|
+
"PendingAttribute",
|
|
93
|
+
"Point3",
|
|
94
|
+
"PointSet",
|
|
95
|
+
"PointSetData",
|
|
96
|
+
"Regular3DGrid",
|
|
97
|
+
"Regular3DGridData",
|
|
98
|
+
"RegularMasked3DGrid",
|
|
99
|
+
"RegularMasked3DGridData",
|
|
100
|
+
"Rotation",
|
|
101
|
+
"Size3d",
|
|
102
|
+
"Size3i",
|
|
103
|
+
"SphericalStructure",
|
|
104
|
+
"SpheroidalStructure",
|
|
105
|
+
"Tensor3DGrid",
|
|
106
|
+
"Tensor3DGridData",
|
|
107
|
+
"Variogram",
|
|
108
|
+
"VariogramCurveData",
|
|
109
|
+
"VariogramData",
|
|
110
|
+
"VariogramStructure",
|
|
111
|
+
"object_from_path",
|
|
112
|
+
"object_from_reference",
|
|
113
|
+
"object_from_uuid",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
# Conditionally export report types when evo-blockmodels is installed
|
|
117
|
+
try:
|
|
118
|
+
from evo.blockmodels.typed import ( # noqa: F401
|
|
119
|
+
Aggregation,
|
|
120
|
+
MassUnits,
|
|
121
|
+
RegularBlockModelData,
|
|
122
|
+
Report,
|
|
123
|
+
ReportCategorySpec,
|
|
124
|
+
ReportColumnSpec,
|
|
125
|
+
ReportResult,
|
|
126
|
+
ReportSpecificationData,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
__all__ += [
|
|
130
|
+
"Aggregation",
|
|
131
|
+
"MassUnits",
|
|
132
|
+
"RegularBlockModelData",
|
|
133
|
+
"Report",
|
|
134
|
+
"ReportCategorySpec",
|
|
135
|
+
"ReportColumnSpec",
|
|
136
|
+
"ReportResult",
|
|
137
|
+
"ReportSpecificationData",
|
|
138
|
+
]
|
|
139
|
+
except ImportError:
|
|
140
|
+
pass
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Any, ClassVar
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from evo.common import IFeedback
|
|
8
|
+
from evo.common.interfaces import IContext
|
|
9
|
+
from evo.common.utils import NoFeedback
|
|
10
|
+
from evo.objects.typed.attributes import Attributes
|
|
11
|
+
from evo.objects.utils.table_formats import KnownTableFormat
|
|
12
|
+
|
|
13
|
+
from ._model import SchemaBuilder, SchemaLocation, SchemaModel
|
|
14
|
+
from ._utils import get_data_client
|
|
15
|
+
from .exceptions import DataLoaderError, ObjectValidationError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DataTable(SchemaModel):
|
|
19
|
+
length: Annotated[int, SchemaLocation("length")]
|
|
20
|
+
_data: Annotated[str, SchemaLocation("data")]
|
|
21
|
+
|
|
22
|
+
table_format: ClassVar[KnownTableFormat | None] = None
|
|
23
|
+
data_columns: ClassVar[list[str]] = []
|
|
24
|
+
|
|
25
|
+
async def to_dataframe(self, fb: IFeedback = NoFeedback) -> pd.DataFrame:
|
|
26
|
+
"""Load a DataFrame containing values for this table.
|
|
27
|
+
|
|
28
|
+
:param fb: Optional feedback object to report download progress.
|
|
29
|
+
:return: The loaded DataFrame with values for this table.
|
|
30
|
+
"""
|
|
31
|
+
if self._context.is_data_modified(self._data):
|
|
32
|
+
raise DataLoaderError("Data was modified since the object was downloaded")
|
|
33
|
+
return await self._obj.download_dataframe(self.as_dict(), fb=fb, column_names=self.data_columns)
|
|
34
|
+
|
|
35
|
+
async def from_dataframe(self, df: pd.DataFrame, fb: IFeedback = NoFeedback) -> None:
|
|
36
|
+
"""Update the values of this table.
|
|
37
|
+
|
|
38
|
+
:param df: DataFrame containing the new values for this table.
|
|
39
|
+
:param fb: Optional feedback object to report upload progress.
|
|
40
|
+
"""
|
|
41
|
+
self._document.update(await self._data_to_schema(df, self._context, fb=fb))
|
|
42
|
+
|
|
43
|
+
# Mark the context as modified so loading data is not allowed
|
|
44
|
+
self._context.mark_modified(self._data)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
async def _data_to_schema(cls, data: Any, context: IContext, fb: IFeedback = NoFeedback) -> Any:
|
|
48
|
+
"""Upload a DataFrame and return the schema dictionary for the DataTable."""
|
|
49
|
+
if not isinstance(data, pd.DataFrame):
|
|
50
|
+
raise ObjectValidationError(f"Input data must be a pandas DataFrame, but got {type(data)}")
|
|
51
|
+
if list(data.columns) != cls.data_columns:
|
|
52
|
+
raise ObjectValidationError(
|
|
53
|
+
f"Input DataFrame must have columns {cls.data_columns}, but got {list(data.columns)}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
data_client = get_data_client(context)
|
|
57
|
+
return await data_client.upload_dataframe(data, table_format=cls.table_format, fb=fb)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class DataTableAndAttributes(SchemaModel):
|
|
61
|
+
"""A dataset representing a table of data along with associated attributes.
|
|
62
|
+
|
|
63
|
+
Subclasses should redefine the _table property to provide additional details about the data table like:
|
|
64
|
+
1. the location of it within the schema using SchemaLocation
|
|
65
|
+
2. the data columns that are expected in the table, which is done by creating a subclass of DataTable,
|
|
66
|
+
3. the table format used for storing the data, which can also be done by creating a subclass of DataTable.
|
|
67
|
+
|
|
68
|
+
e.g.,
|
|
69
|
+
class LocationTable(DataTable):
|
|
70
|
+
table_format: ClassVar[KnownTableFormat] = FLOAT_ARRAY_3
|
|
71
|
+
data_columns: ClassVar[list[str]] = ["x", "y", "z"]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Locations(DataTableAndAttributes):
|
|
75
|
+
_table: Annotated[LocationTable, SchemaLocation("coordinates")]
|
|
76
|
+
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
attributes: Annotated[Attributes, SchemaLocation("attributes")]
|
|
80
|
+
_table: DataTable
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def length(self) -> int:
|
|
84
|
+
"""The expected number of rows in the table and attributes."""
|
|
85
|
+
return self._table.length
|
|
86
|
+
|
|
87
|
+
async def to_dataframe(self, *keys: str, fb: IFeedback = NoFeedback) -> pd.DataFrame:
|
|
88
|
+
"""Load a DataFrame containing the values and attributes.
|
|
89
|
+
|
|
90
|
+
:param keys: Optional attribute keys to include. If not specified, all attributes are included.
|
|
91
|
+
:param fb: Optional feedback object to report download progress.
|
|
92
|
+
:return: DataFrame with data columns (e.g., X, Y, Z) and additional columns for attributes.
|
|
93
|
+
"""
|
|
94
|
+
table_df = await self._table.to_dataframe(fb=fb)
|
|
95
|
+
if self.attributes is not None and len(self.attributes) > 0:
|
|
96
|
+
attr_df = await self.attributes.to_dataframe(*keys, fb=fb)
|
|
97
|
+
combined_df = pd.concat([table_df, attr_df], axis=1)
|
|
98
|
+
return combined_df
|
|
99
|
+
else:
|
|
100
|
+
return table_df
|
|
101
|
+
|
|
102
|
+
async def from_dataframe(self, df: pd.DataFrame, fb: IFeedback = NoFeedback) -> None:
|
|
103
|
+
"""Set the table data and attributes from a DataFrame.
|
|
104
|
+
|
|
105
|
+
:param df: DataFrame containing data columns (e.g., X, Y, Z) and additional columns for attributes.
|
|
106
|
+
:param fb: Optional feedback object to report upload progress.
|
|
107
|
+
"""
|
|
108
|
+
table_df, attr_df = self._split_dataframe(df, self._table.data_columns)
|
|
109
|
+
|
|
110
|
+
await self._table.from_dataframe(table_df, fb=fb)
|
|
111
|
+
if attr_df is not None:
|
|
112
|
+
await self.attributes.set_attributes(attr_df, fb=fb)
|
|
113
|
+
else:
|
|
114
|
+
await self.attributes.clear()
|
|
115
|
+
|
|
116
|
+
def validate(self) -> None:
|
|
117
|
+
"""Validate that all attributes have the correct length."""
|
|
118
|
+
self.attributes.validate_lengths(self.length)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def _split_dataframe(cls, data: pd.DataFrame, data_columns: list[str]) -> tuple[pd.DataFrame, pd.DataFrame | None]:
|
|
122
|
+
"""Validate and split a DataFrame into table data and attribute data."""
|
|
123
|
+
|
|
124
|
+
missing = set(data_columns) - set(data.columns)
|
|
125
|
+
if missing:
|
|
126
|
+
raise ObjectValidationError(f"Input DataFrame must have {data_columns} columns. Missing: {missing}")
|
|
127
|
+
|
|
128
|
+
table_df = data[data_columns]
|
|
129
|
+
attr_cols = [col for col in data.columns if col not in data_columns]
|
|
130
|
+
attr_df = data[attr_cols] if attr_cols else None
|
|
131
|
+
return table_df, attr_df
|
|
132
|
+
|
|
133
|
+
@classmethod
|
|
134
|
+
async def _data_to_schema(cls, data: Any, context: IContext) -> Any:
|
|
135
|
+
if not isinstance(data, pd.DataFrame):
|
|
136
|
+
raise ObjectValidationError(f"Input data must be a pandas DataFrame, but got {type(data)}")
|
|
137
|
+
|
|
138
|
+
# Lookup the metadata of the _table sub-model, as sub-classes may redefine it
|
|
139
|
+
table_metadata = cls._sub_models["_table"]
|
|
140
|
+
table_type = table_metadata.model_type
|
|
141
|
+
table_df, attr_df = cls._split_dataframe(data, table_type.data_columns)
|
|
142
|
+
|
|
143
|
+
builder = SchemaBuilder(cls, context)
|
|
144
|
+
await builder.set_sub_model_value("_table", table_df)
|
|
145
|
+
await builder.set_sub_model_value("attributes", attr_df)
|
|
146
|
+
return builder.document
|