evo-objects 0.4.3__tar.gz → 0.6.2__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.4.3 → evo_objects-0.6.2}/PKG-INFO +3 -3
- {evo_objects-0.4.3 → evo_objects-0.6.2}/pyproject.toml +2 -2
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/__init__.py +7 -1
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/_data.py +15 -2
- evo_objects-0.6.2/src/evo/objects/typed/_downhole.py +62 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/_grid.py +3 -2
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/_model.py +68 -21
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/attributes.py +78 -5
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/base.py +2 -25
- evo_objects-0.6.2/src/evo/objects/typed/downhole_collection.py +303 -0
- evo_objects-0.6.2/src/evo/objects/typed/downhole_intervals.py +246 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/spatial.py +2 -8
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/types.py +24 -41
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/utils/data.py +97 -5
- {evo_objects-0.4.3 → evo_objects-0.6.2}/.gitignore +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/LICENSE.md +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/_model_config.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/client/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/client/api_client.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/client/object_client.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/client/parse.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/data.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/api/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/api/data_api.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/api/metadata_api.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/api/objects_api.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/api/stages_api.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/endpoints/models.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/exceptions.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/io.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/parquet/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/parquet/loader.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/py.typed +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/_utils.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/block_model_ref.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/exceptions.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/pointset.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/regular_grid.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/regular_masked_grid.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/tensor_grid.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/typed/variogram.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/utils/__init__.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/utils/table_formats.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/utils/tables.py +0 -0
- {evo_objects-0.4.3 → evo_objects-0.6.2}/src/evo/objects/utils/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: evo-objects
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.2
|
|
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.
|
|
12
|
+
Requires-Dist: evo-sdk-common[jmespath]>=0.5.26
|
|
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.
|
|
4
|
+
version = "0.6.2"
|
|
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.
|
|
13
|
+
"evo-sdk-common[jmespath]>=0.5.26",
|
|
14
14
|
"pydantic>=2,<3",
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright ©
|
|
1
|
+
# Copyright © 2026 Bentley Systems, Incorporated
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -22,6 +22,8 @@ from .base import BaseObject, object_from_path, object_from_reference, object_fr
|
|
|
22
22
|
from .block_model_ref import (
|
|
23
23
|
BlockModel,
|
|
24
24
|
)
|
|
25
|
+
from .downhole_collection import DownholeCollection, DownholeCollectionData
|
|
26
|
+
from .downhole_intervals import DownholeIntervals, DownholeIntervalsData
|
|
25
27
|
from .pointset import (
|
|
26
28
|
Locations,
|
|
27
29
|
PointSet,
|
|
@@ -80,6 +82,10 @@ __all__ = [
|
|
|
80
82
|
"BoundingBox",
|
|
81
83
|
"CoordinateReferenceSystem",
|
|
82
84
|
"CubicStructure",
|
|
85
|
+
"DownholeCollection",
|
|
86
|
+
"DownholeCollectionData",
|
|
87
|
+
"DownholeIntervals",
|
|
88
|
+
"DownholeIntervalsData",
|
|
83
89
|
"Ellipsoid",
|
|
84
90
|
"EllipsoidRanges",
|
|
85
91
|
"EpsgCode",
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
# Copyright © 2026 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
|
+
|
|
1
12
|
from __future__ import annotations
|
|
2
13
|
|
|
3
14
|
from typing import Annotated, Any, ClassVar
|
|
@@ -111,7 +122,7 @@ class DataTableAndAttributes(SchemaModel):
|
|
|
111
122
|
if attr_df is not None:
|
|
112
123
|
await self.attributes.set_attributes(attr_df, fb=fb)
|
|
113
124
|
else:
|
|
114
|
-
|
|
125
|
+
self.attributes.clear()
|
|
115
126
|
|
|
116
127
|
def validate(self) -> None:
|
|
117
128
|
"""Validate that all attributes have the correct length."""
|
|
@@ -143,4 +154,6 @@ class DataTableAndAttributes(SchemaModel):
|
|
|
143
154
|
builder = SchemaBuilder(cls, context)
|
|
144
155
|
await builder.set_sub_model_value("_table", table_df)
|
|
145
156
|
await builder.set_sub_model_value("attributes", attr_df)
|
|
146
|
-
|
|
157
|
+
|
|
158
|
+
# As we specially handle the _table and attributes sub-models above, we skip them in the default data processing of build_from_data.
|
|
159
|
+
return await builder.build_from_data(data, skip_sub_models={"_table", "attributes"})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Copyright © 2026 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
|
+
"""Shared sub-models for downhole typed objects."""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Annotated, Any, ClassVar
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from evo.common.interfaces import IContext
|
|
21
|
+
from evo.objects.utils.table_formats import FLOAT_ARRAY_2, KnownTableFormat
|
|
22
|
+
|
|
23
|
+
from ._data import DataTable
|
|
24
|
+
from ._model import DataLocation, SchemaLocation, SchemaModel
|
|
25
|
+
from .attributes import Category
|
|
26
|
+
|
|
27
|
+
_HOLE_ID_COL = "hole_id"
|
|
28
|
+
_FROM_COL = "from"
|
|
29
|
+
_TO_COL = "to"
|
|
30
|
+
_DEPTH_COLS: list[str] = [_FROM_COL, _TO_COL]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HoleIdCategory(Category):
|
|
34
|
+
"""Categorical hole identifier column, shared by downhole typed objects."""
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
async def _data_to_schema(cls, data: pd.DataFrame, context: IContext) -> Any:
|
|
38
|
+
category_table = data[[_HOLE_ID_COL]].astype("category")
|
|
39
|
+
return await super()._data_to_schema(category_table, context=context)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DepthIntervalsTable(DataTable):
|
|
43
|
+
table_format: ClassVar[KnownTableFormat] = FLOAT_ARRAY_2
|
|
44
|
+
data_columns: ClassVar[list[str]] = _DEPTH_COLS
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
async def _data_to_schema(cls, data: pd.DataFrame, context: IContext) -> Any:
|
|
48
|
+
return await super()._data_to_schema(data[_DEPTH_COLS], context)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FromToModel(SchemaModel):
|
|
52
|
+
"""Schema model for the `from_to` component of a downhole intervals object."""
|
|
53
|
+
|
|
54
|
+
intervals: Annotated[DepthIntervalsTable, SchemaLocation("intervals.start_and_end"), DataLocation("intervals")]
|
|
55
|
+
unit: Annotated[str | None, SchemaLocation("unit"), DataLocation("depth_unit")]
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def _data_to_schema(cls, data: Any, context: IContext) -> Any:
|
|
59
|
+
result = await super()._data_to_schema(data, context)
|
|
60
|
+
if data.depth_unit is not None:
|
|
61
|
+
result["unit"] = data.depth_unit
|
|
62
|
+
return result
|
|
@@ -19,6 +19,7 @@ from typing import Annotated
|
|
|
19
19
|
from uuid import UUID
|
|
20
20
|
|
|
21
21
|
import pandas as pd
|
|
22
|
+
from pydantic import AliasChoices, Field
|
|
22
23
|
|
|
23
24
|
from evo.common import IFeedback
|
|
24
25
|
from evo.common.utils import NoFeedback
|
|
@@ -198,8 +199,8 @@ class BlockModelGeometry:
|
|
|
198
199
|
|
|
199
200
|
model_type: str
|
|
200
201
|
origin: Point3
|
|
201
|
-
n_blocks: Size3i
|
|
202
|
-
block_size: Size3d
|
|
202
|
+
n_blocks: Annotated[Size3i, Field(validation_alias=AliasChoices("n_blocks", "n_parent_blocks"))]
|
|
203
|
+
block_size: Annotated[Size3d, Field(validation_alias=AliasChoices("block_size", "parent_block_size"))]
|
|
203
204
|
rotation: Rotation | None = None
|
|
204
205
|
|
|
205
206
|
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import copy
|
|
15
15
|
from collections.abc import Sequence
|
|
16
16
|
from dataclasses import dataclass, field
|
|
17
|
-
from typing import Annotated, Any, Callable, Generic, TypeVar, get_args, get_origin, get_type_hints, overload
|
|
17
|
+
from typing import Annotated, Any, Callable, ClassVar, Generic, TypeVar, get_args, get_origin, get_type_hints, overload
|
|
18
18
|
|
|
19
19
|
from pydantic import TypeAdapter
|
|
20
20
|
|
|
@@ -86,11 +86,8 @@ class SubModelMetadata:
|
|
|
86
86
|
"""The field name in the data class for this sub-model."""
|
|
87
87
|
|
|
88
88
|
|
|
89
|
-
class
|
|
90
|
-
"""
|
|
91
|
-
|
|
92
|
-
This can be used on either typed objects classes or dataset classes.
|
|
93
|
-
"""
|
|
89
|
+
class BaseSchemaProperty(Generic[_T]):
|
|
90
|
+
"""Base descriptor for reading data from a Geoscience Object schema."""
|
|
94
91
|
|
|
95
92
|
def __init__(
|
|
96
93
|
self,
|
|
@@ -111,7 +108,7 @@ class SchemaProperty(Generic[_T]):
|
|
|
111
108
|
return self._type_adapter.dump_python(value)
|
|
112
109
|
|
|
113
110
|
@overload
|
|
114
|
-
def __get__(self, instance: None, owner: type[SchemaModel]) ->
|
|
111
|
+
def __get__(self, instance: None, owner: type[SchemaModel]) -> BaseSchemaProperty[_T]: ...
|
|
115
112
|
|
|
116
113
|
@overload
|
|
117
114
|
def __get__(self, instance: SchemaModel, owner: type[SchemaModel]) -> _T: ...
|
|
@@ -128,6 +125,13 @@ class SchemaProperty(Generic[_T]):
|
|
|
128
125
|
# Use TypeAdapter to validate and apply defaults from Field annotations
|
|
129
126
|
return self._type_adapter.validate_python(value)
|
|
130
127
|
|
|
128
|
+
|
|
129
|
+
class SchemaProperty(BaseSchemaProperty[_T]):
|
|
130
|
+
"""Descriptor for mutable data within a Geoscience Object schema.
|
|
131
|
+
|
|
132
|
+
This can be used on either typed objects classes or dataset classes.
|
|
133
|
+
"""
|
|
134
|
+
|
|
131
135
|
def __set__(self, instance: SchemaModel, value: Any) -> None:
|
|
132
136
|
_set_property_value(self, instance._document, value)
|
|
133
137
|
|
|
@@ -140,7 +144,19 @@ class SchemaProperty(Generic[_T]):
|
|
|
140
144
|
_set_property_value(self, document, value)
|
|
141
145
|
|
|
142
146
|
|
|
143
|
-
|
|
147
|
+
class SchemaConstantProperty(BaseSchemaProperty[_T]):
|
|
148
|
+
"""A SchemaProperty that always writes a fixed value to the schema."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, jmespath_expr: str, type_adapter: TypeAdapter[_T], value: _T) -> None:
|
|
151
|
+
super().__init__(jmespath_expr=jmespath_expr, type_adapter=type_adapter)
|
|
152
|
+
self._value = value
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def value(self) -> _T:
|
|
156
|
+
return self._value
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _set_property_value(schema_property: BaseSchemaProperty, document: dict[str, Any], value: Any) -> None:
|
|
144
160
|
"""Set the value of a SchemaProperty by name.
|
|
145
161
|
|
|
146
162
|
:param property_name: The name of the property to set.
|
|
@@ -177,6 +193,26 @@ class SchemaBuilder:
|
|
|
177
193
|
else:
|
|
178
194
|
self.document.update(sub_document)
|
|
179
195
|
|
|
196
|
+
async def build_from_data(self, data: Any, skip_sub_models: set[str] | None = None) -> dict[str, Any]:
|
|
197
|
+
"""Build the schema document by applying all properties and sub-models."""
|
|
198
|
+
for key, prop in self._properties.items():
|
|
199
|
+
if isinstance(prop, SchemaConstantProperty):
|
|
200
|
+
self.set_property(key, prop.value)
|
|
201
|
+
else:
|
|
202
|
+
value = getattr(data, key, None)
|
|
203
|
+
self.set_property(key, value)
|
|
204
|
+
|
|
205
|
+
for name, metadata in self._sub_models.items():
|
|
206
|
+
if skip_sub_models and name in skip_sub_models:
|
|
207
|
+
continue
|
|
208
|
+
if metadata.data_field:
|
|
209
|
+
sub_data = getattr(data, metadata.data_field, None)
|
|
210
|
+
else:
|
|
211
|
+
sub_data = data
|
|
212
|
+
await self.set_sub_model_value(name, sub_data)
|
|
213
|
+
|
|
214
|
+
return self.document
|
|
215
|
+
|
|
180
216
|
|
|
181
217
|
def _get_base_type(annotation: Any) -> tuple[Any, SchemaLocation | None, DataLocation | None]:
|
|
182
218
|
"""Extract the base type and SchemaLocation from an annotation.
|
|
@@ -208,14 +244,14 @@ class SchemaModel:
|
|
|
208
244
|
automatically created for nested SchemaModel/SchemaList fields.
|
|
209
245
|
"""
|
|
210
246
|
|
|
211
|
-
_schema_properties: dict[str,
|
|
247
|
+
_schema_properties: dict[str, BaseSchemaProperty[Any]] = {}
|
|
212
248
|
_sub_models: dict[str, SubModelMetadata] = {}
|
|
213
249
|
|
|
214
250
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
215
251
|
super().__init_subclass__(**kwargs)
|
|
216
252
|
|
|
217
253
|
# Initialize with inherited values (copy to avoid mutating parent)
|
|
218
|
-
schema_properties: dict[str,
|
|
254
|
+
schema_properties: dict[str, BaseSchemaProperty[Any]] = {}
|
|
219
255
|
sub_models: dict[str, SubModelMetadata] = {}
|
|
220
256
|
for base in cls.__mro__[1:]:
|
|
221
257
|
if issubclass(base, SchemaModel):
|
|
@@ -232,8 +268,28 @@ class SchemaModel:
|
|
|
232
268
|
cls._sub_models = sub_models
|
|
233
269
|
return
|
|
234
270
|
|
|
235
|
-
# Process the resolved annotations
|
|
271
|
+
# Process the resolved annotations, to produce the final set of schema properties and sub-models for this class
|
|
236
272
|
for field_name, annotation in hints.items():
|
|
273
|
+
# Handle ClassVar fields
|
|
274
|
+
if get_origin(annotation) is ClassVar:
|
|
275
|
+
inner_args = get_args(annotation)
|
|
276
|
+
inner_type = inner_args[0]
|
|
277
|
+
base_type, schema_location, _ = _get_base_type(inner_type)
|
|
278
|
+
# Skip class variables without a SchemaLocation, e.g. data_columns: ClassVar[list[str]] = [],
|
|
279
|
+
if schema_location is None:
|
|
280
|
+
continue
|
|
281
|
+
|
|
282
|
+
# Treat such remaining ClassVar fields as constants, which also have a corresponding value in the schema
|
|
283
|
+
type_adapter = TypeAdapter(base_type)
|
|
284
|
+
prop = SchemaConstantProperty(
|
|
285
|
+
jmespath_expr=schema_location.jmespath_expr,
|
|
286
|
+
type_adapter=type_adapter,
|
|
287
|
+
value=getattr(cls, field_name),
|
|
288
|
+
)
|
|
289
|
+
setattr(cls, field_name, prop)
|
|
290
|
+
schema_properties[field_name] = prop
|
|
291
|
+
continue
|
|
292
|
+
|
|
237
293
|
base_type, schema_location, data_location = _get_base_type(annotation)
|
|
238
294
|
|
|
239
295
|
# Skip fields without a SchemaLocation
|
|
@@ -348,16 +404,7 @@ class SchemaModel:
|
|
|
348
404
|
:return: The dictionary representation of the data.
|
|
349
405
|
"""
|
|
350
406
|
builder = SchemaBuilder(cls, context)
|
|
351
|
-
|
|
352
|
-
value = getattr(data, key, None)
|
|
353
|
-
builder.set_property(key, value)
|
|
354
|
-
for name, metadata in cls._sub_models.items():
|
|
355
|
-
if metadata.data_field:
|
|
356
|
-
sub_data = getattr(data, metadata.data_field, None)
|
|
357
|
-
else:
|
|
358
|
-
sub_data = data
|
|
359
|
-
await builder.set_sub_model_value(name, sub_data)
|
|
360
|
-
return builder.document
|
|
407
|
+
return await builder.build_from_data(data)
|
|
361
408
|
|
|
362
409
|
def search(self, expression: str) -> Any:
|
|
363
410
|
"""Search the model using a JMESPath expression.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright ©
|
|
1
|
+
# Copyright © 2026 Bentley Systems, Incorporated
|
|
2
2
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
3
3
|
# you may not use this file except in compliance with the License.
|
|
4
4
|
# You may obtain a copy of the License at
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
+
import typing
|
|
14
15
|
import uuid
|
|
16
|
+
from dataclasses import dataclass
|
|
15
17
|
from typing import TYPE_CHECKING, Annotated, Any
|
|
16
18
|
from uuid import UUID
|
|
17
19
|
|
|
@@ -23,6 +25,7 @@ from evo.common.utils import NoFeedback, iter_with_fb
|
|
|
23
25
|
from evo.objects import DownloadedObject
|
|
24
26
|
from evo.objects.utils.table_formats import (
|
|
25
27
|
BOOL_ARRAY_1,
|
|
28
|
+
DATE_TIME_ARRAY,
|
|
26
29
|
FLOAT_ARRAY_1,
|
|
27
30
|
INTEGER_ARRAY_1_INT32,
|
|
28
31
|
INTEGER_ARRAY_1_INT64,
|
|
@@ -67,6 +70,8 @@ def _infer_attribute_type_from_series(series: pd.Series) -> str:
|
|
|
67
70
|
return "category"
|
|
68
71
|
elif pd.api.types.is_string_dtype(series):
|
|
69
72
|
return "string"
|
|
73
|
+
elif pd.api.types.infer_dtype(series, skipna=True) in ["date", "datetime", "datetime64"]:
|
|
74
|
+
return "date_time"
|
|
70
75
|
else:
|
|
71
76
|
raise UnSupportedDataTypeError(f"Unsupported dtype for attribute: {series.dtype}")
|
|
72
77
|
|
|
@@ -76,9 +81,35 @@ _attribute_table_formats = {
|
|
|
76
81
|
"integer": [INTEGER_ARRAY_1_INT32, INTEGER_ARRAY_1_INT64],
|
|
77
82
|
"bool": [BOOL_ARRAY_1],
|
|
78
83
|
"string": [STRING_ARRAY],
|
|
84
|
+
"date_time": [DATE_TIME_ARRAY],
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
|
|
88
|
+
@dataclass
|
|
89
|
+
class AttributeDescription:
|
|
90
|
+
discipline: str = ""
|
|
91
|
+
type: str = ""
|
|
92
|
+
unit: str | None = None
|
|
93
|
+
scale: str | None = None
|
|
94
|
+
extensions: dict[str, typing.Any] | None = None
|
|
95
|
+
tags: dict[str, str] | None = None
|
|
96
|
+
|
|
97
|
+
def to_schema(self):
|
|
98
|
+
result = {
|
|
99
|
+
"discipline": self.discipline,
|
|
100
|
+
"type": self.type,
|
|
101
|
+
}
|
|
102
|
+
if self.unit:
|
|
103
|
+
result["unit"] = self.unit
|
|
104
|
+
if self.scale:
|
|
105
|
+
result["scale"] = self.scale
|
|
106
|
+
if self.extensions:
|
|
107
|
+
result["extensions"] = self.extensions
|
|
108
|
+
if self.tags:
|
|
109
|
+
result["tags"] = self.tags
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
82
113
|
class Attribute(SchemaModel):
|
|
83
114
|
"""A Geoscience Object Attribute"""
|
|
84
115
|
|
|
@@ -158,7 +189,7 @@ class Attribute(SchemaModel):
|
|
|
158
189
|
table_formats = _attribute_table_formats.get(attribute_type)
|
|
159
190
|
attr_doc["values"] = await data_client.upload_dataframe(df, table_format=table_formats, fb=fb)
|
|
160
191
|
|
|
161
|
-
if attribute_type in ["scalar", "integer", "category"]:
|
|
192
|
+
if attribute_type in ["scalar", "integer", "category", "date_time"]:
|
|
162
193
|
attr_doc["nan_description"] = {"values": []}
|
|
163
194
|
|
|
164
195
|
|
|
@@ -203,7 +234,15 @@ class PendingAttribute:
|
|
|
203
234
|
|
|
204
235
|
|
|
205
236
|
class Attributes(SchemaList[Attribute]):
|
|
206
|
-
"""A collection of Geoscience Object Attributes
|
|
237
|
+
"""A collection of Geoscience Object Attributes
|
|
238
|
+
|
|
239
|
+
The data source is a pandas DataFrame. If the DataFrame has attribute descriptions attached
|
|
240
|
+
to it, then they will be used to populate the schema's "attribute_description" fields. The
|
|
241
|
+
attribute descriptions are attached to the DataFrame's `attrs` attribute.
|
|
242
|
+
|
|
243
|
+
>>> df.attrs
|
|
244
|
+
{'attribute_description': {<column names>: <AttributeDescription>}, ...}
|
|
245
|
+
"""
|
|
207
246
|
|
|
208
247
|
_schema_path: str | None = None
|
|
209
248
|
"""The full JMESPath to this attributes list within the parent object schema."""
|
|
@@ -263,7 +302,7 @@ class Attributes(SchemaList[Attribute]):
|
|
|
263
302
|
|
|
264
303
|
for col in df.columns:
|
|
265
304
|
series = df[col]
|
|
266
|
-
attribute_type = _infer_attribute_type_from_series(
|
|
305
|
+
attribute_type = _infer_attribute_type_from_series(df[col])
|
|
267
306
|
|
|
268
307
|
attr_doc: dict[str, Any] = {
|
|
269
308
|
"name": str(col),
|
|
@@ -271,9 +310,19 @@ class Attributes(SchemaList[Attribute]):
|
|
|
271
310
|
"attribute_type": attribute_type,
|
|
272
311
|
}
|
|
273
312
|
|
|
274
|
-
|
|
313
|
+
if attribute_type == "date_time":
|
|
314
|
+
series = pd.to_datetime(series, utc=True).dt.as_unit("us")
|
|
315
|
+
|
|
316
|
+
col_df = pd.DataFrame({col: series})
|
|
275
317
|
await Attribute._upload_attribute_values(attr_doc, col_df, attribute_type, data_client)
|
|
276
318
|
|
|
319
|
+
attr_desc: AttributeDescription | None = df.attrs.get("attribute_descriptions", {}).get(col)
|
|
320
|
+
if attr_desc is not None:
|
|
321
|
+
if not isinstance(attr_desc, AttributeDescription):
|
|
322
|
+
raise TypeError("attribute description must be a AttributeDescription.")
|
|
323
|
+
if attr_desc.unit is not None:
|
|
324
|
+
attr_doc["attribute_description"] = attr_desc.to_schema()
|
|
325
|
+
|
|
277
326
|
attributes_list.append(attr_doc)
|
|
278
327
|
|
|
279
328
|
async def to_dataframe(self, *keys: str, fb: IFeedback = NoFeedback) -> pd.DataFrame:
|
|
@@ -528,3 +577,27 @@ class BlockModelAttributes:
|
|
|
528
577
|
def __repr__(self) -> str:
|
|
529
578
|
names = [attr.name for attr in self._attributes]
|
|
530
579
|
return f"BlockModelAttributes({names})"
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class Category(SchemaModel):
|
|
583
|
+
_data: Annotated[str, SchemaLocation("values.data")]
|
|
584
|
+
_length: Annotated[int, SchemaLocation("values.length")]
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def length(self):
|
|
588
|
+
return self._length
|
|
589
|
+
|
|
590
|
+
@classmethod
|
|
591
|
+
async def _data_to_schema(cls, category_table, context: IContext) -> Any:
|
|
592
|
+
data_client = get_data_client(context)
|
|
593
|
+
return await data_client.upload_category_dataframe(category_table)
|
|
594
|
+
|
|
595
|
+
async def to_dataframe(self, fb: IFeedback = NoFeedback) -> pd.DataFrame:
|
|
596
|
+
"""Load a DataFrame containing values for this table.
|
|
597
|
+
|
|
598
|
+
:param fb: Optional feedback object to report download progress.
|
|
599
|
+
:return: The loaded DataFrame with values for this table.
|
|
600
|
+
"""
|
|
601
|
+
if self._context.is_data_modified(self._data):
|
|
602
|
+
raise DataLoaderError("Data was modified since the object was downloaded")
|
|
603
|
+
return await self._obj.download_category_dataframe(self.as_dict(), fb=fb)
|
|
@@ -227,31 +227,8 @@ class _BaseObject(SchemaModel):
|
|
|
227
227
|
"sub_classification and creation_schema_version must be defined by the subclass"
|
|
228
228
|
)
|
|
229
229
|
schema_id = ObjectSchema("objects", cls.sub_classification, cls.creation_schema_version)
|
|
230
|
-
result
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
# Handle schema properties
|
|
235
|
-
for key, prop in cls._schema_properties.items():
|
|
236
|
-
value = getattr(data, key, None)
|
|
237
|
-
if value is not None:
|
|
238
|
-
prop.apply_to(result, value)
|
|
239
|
-
|
|
240
|
-
# Handle annotation-based sub-models
|
|
241
|
-
for name, metadata in cls._sub_models.items():
|
|
242
|
-
if metadata.data_field:
|
|
243
|
-
sub_data = getattr(data, metadata.data_field, None)
|
|
244
|
-
else:
|
|
245
|
-
sub_data = data
|
|
246
|
-
if sub_data is not None:
|
|
247
|
-
from ._utils import assign_jmespath_value
|
|
248
|
-
|
|
249
|
-
sub_document = await metadata.model_type._data_to_schema(sub_data, context)
|
|
250
|
-
if metadata.jmespath_expr:
|
|
251
|
-
assign_jmespath_value(result, metadata.jmespath_expr, sub_document)
|
|
252
|
-
else:
|
|
253
|
-
result.update(sub_document)
|
|
254
|
-
|
|
230
|
+
result = await super()._data_to_schema(data, context)
|
|
231
|
+
result["schema"] = str(schema_id)
|
|
255
232
|
return result
|
|
256
233
|
|
|
257
234
|
@classmethod
|