evo-objects 0.6.2__tar.gz → 0.6.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.
- {evo_objects-0.6.2 → evo_objects-0.6.3}/PKG-INFO +37 -1
- {evo_objects-0.6.2 → evo_objects-0.6.3}/pyproject.toml +1 -1
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/__init__.py +12 -1
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/_model.py +18 -3
- evo_objects-0.6.3/src/evo/objects/typed/_prefetch.py +72 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/attributes.py +50 -8
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/base.py +15 -1
- evo_objects-0.6.3/src/evo/objects/typed/downhole_collection.py +589 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/utils/__init__.py +3 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/utils/data.py +10 -1
- evo_objects-0.6.3/src/evo/objects/utils/downhole.py +76 -0
- evo_objects-0.6.2/src/evo/objects/typed/downhole_collection.py +0 -303
- {evo_objects-0.6.2 → evo_objects-0.6.3}/.gitignore +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/LICENSE.md +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/__init__.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/_model_config.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/client/__init__.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/client/api_client.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/client/object_client.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/client/parse.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/data.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/__init__.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/api/__init__.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/api/data_api.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/api/metadata_api.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/api/objects_api.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/api/stages_api.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/endpoints/models.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/exceptions.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/io.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/parquet/__init__.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/parquet/loader.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/py.typed +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/_data.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/_downhole.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/_grid.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/_utils.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/block_model_ref.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/downhole_intervals.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/exceptions.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/pointset.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/regular_grid.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/regular_masked_grid.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/spatial.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/tensor_grid.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/types.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/typed/variogram.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/utils/table_formats.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/utils/tables.py +0 -0
- {evo_objects-0.6.2 → evo_objects-0.6.3}/src/evo/objects/utils/types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: evo-objects
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -95,6 +95,42 @@ You can also get a list of all objects. Internally, this recursively calls the `
|
|
|
95
95
|
|
|
96
96
|
Check out the other methods on the `ObjectAPIClient` for more details on how to upload and download objects, or get object versions.
|
|
97
97
|
|
|
98
|
+
### Typed downhole collections
|
|
99
|
+
|
|
100
|
+
`DownholeCollection` provides a DataFrame-based API for creating and reading downhole objects. A collection can contain
|
|
101
|
+
distance tables, interval tables, or both. `hole_index` is an integer key in the object's `hole_id` lookup table, not a
|
|
102
|
+
row position or a pandas categorical code. The creation helper uses dense zero-based keys in sorted hole-ID order unless
|
|
103
|
+
an explicit `hole_indices` mapping is supplied. Persisted objects may use arbitrary integer keys. Collection holes may
|
|
104
|
+
contain a subset or repeated chunks.
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
import pandas as pd
|
|
108
|
+
|
|
109
|
+
from evo.objects.typed import DownholeCollection, DownholeCollectionData
|
|
110
|
+
from evo.objects.typed.downhole_collection import IntervalCollection
|
|
111
|
+
from evo.objects.utils.downhole import hole_chunks_from_ids
|
|
112
|
+
|
|
113
|
+
intervals = pd.DataFrame({"from": [0.0], "to": [1.5], "lithology": ["sandstone"]})
|
|
114
|
+
collections = [
|
|
115
|
+
IntervalCollection(
|
|
116
|
+
name="geology",
|
|
117
|
+
holes=hole_chunks_from_ids(pd.Series(["DH-01"])),
|
|
118
|
+
table=intervals,
|
|
119
|
+
unit="m", # Collection coordinate units are set explicitly here.
|
|
120
|
+
)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Build DownholeCollectionData with matching path, location-hole chunks, and collar properties,
|
|
124
|
+
# then create it with: await DownholeCollection.create(context, data)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Use `await dhc.location.to_dataframe()` and `await dhc.location.path_to_dataframe()` to read collars and paths.
|
|
128
|
+
Distance and interval tables provide `to_dataframe()` and `to_dataframe_by_hole()`. Before reading a large object, call
|
|
129
|
+
`await dhc.prefetch_collections("geology")` to warm only the requested collection data (and location data by default).
|
|
130
|
+
Attribute descriptions for ordinary attributes round-trip through `DataFrame.attrs["attribute_descriptions"]`.
|
|
131
|
+
Collection coordinate units come only from `DistanceCollection.unit` or `IntervalCollection.unit`; unit metadata on the
|
|
132
|
+
distance or `from` column is ignored.
|
|
133
|
+
|
|
98
134
|
## Contributing
|
|
99
135
|
|
|
100
136
|
For instructions on contributing to the development of this library, please refer to the [evo-python-sdk documentation](https://github.com/seequentevo/evo-python-sdk).
|
|
@@ -12,17 +12,24 @@
|
|
|
12
12
|
from ._grid import BlockModelData, BlockModelGeometry
|
|
13
13
|
from .attributes import (
|
|
14
14
|
Attribute,
|
|
15
|
+
AttributeDescription,
|
|
15
16
|
Attributes,
|
|
16
17
|
BlockModelAttribute,
|
|
17
18
|
BlockModelAttributes,
|
|
18
19
|
BlockModelPendingAttribute,
|
|
19
20
|
PendingAttribute,
|
|
21
|
+
Unit,
|
|
20
22
|
)
|
|
21
23
|
from .base import BaseObject, object_from_path, object_from_reference, object_from_uuid
|
|
22
24
|
from .block_model_ref import (
|
|
23
25
|
BlockModel,
|
|
24
26
|
)
|
|
25
|
-
from .downhole_collection import
|
|
27
|
+
from .downhole_collection import (
|
|
28
|
+
DistanceCollection,
|
|
29
|
+
DownholeCollection,
|
|
30
|
+
DownholeCollectionData,
|
|
31
|
+
IntervalCollection,
|
|
32
|
+
)
|
|
26
33
|
from .downhole_intervals import DownholeIntervals, DownholeIntervalsData
|
|
27
34
|
from .pointset import (
|
|
28
35
|
Locations,
|
|
@@ -70,6 +77,7 @@ from .variogram import (
|
|
|
70
77
|
|
|
71
78
|
__all__ = [
|
|
72
79
|
"Attribute",
|
|
80
|
+
"AttributeDescription",
|
|
73
81
|
"Attributes",
|
|
74
82
|
"BaseObject",
|
|
75
83
|
"BaseSpatialObject",
|
|
@@ -82,6 +90,7 @@ __all__ = [
|
|
|
82
90
|
"BoundingBox",
|
|
83
91
|
"CoordinateReferenceSystem",
|
|
84
92
|
"CubicStructure",
|
|
93
|
+
"DistanceCollection",
|
|
85
94
|
"DownholeCollection",
|
|
86
95
|
"DownholeCollectionData",
|
|
87
96
|
"DownholeIntervals",
|
|
@@ -92,6 +101,7 @@ __all__ = [
|
|
|
92
101
|
"ExponentialStructure",
|
|
93
102
|
"GaussianStructure",
|
|
94
103
|
"GeneralisedCauchyStructure",
|
|
104
|
+
"IntervalCollection",
|
|
95
105
|
"LinearStructure",
|
|
96
106
|
"Locations",
|
|
97
107
|
"MaskedCells",
|
|
@@ -110,6 +120,7 @@ __all__ = [
|
|
|
110
120
|
"SpheroidalStructure",
|
|
111
121
|
"Tensor3DGrid",
|
|
112
122
|
"Tensor3DGridData",
|
|
123
|
+
"Unit",
|
|
113
124
|
"Variogram",
|
|
114
125
|
"VariogramCurveData",
|
|
115
126
|
"VariogramData",
|
|
@@ -450,15 +450,30 @@ class SchemaList(Sequence[_M]):
|
|
|
450
450
|
if get_origin(base) is SchemaList:
|
|
451
451
|
args = get_args(base)
|
|
452
452
|
if args:
|
|
453
|
-
|
|
453
|
+
item_type = args[0]
|
|
454
|
+
if isinstance(item_type, type):
|
|
455
|
+
cls._item_type = item_type
|
|
456
|
+
elif "_resolve_item_type" not in cls.__dict__ or "_data_to_schema" not in cls.__dict__:
|
|
457
|
+
raise TypeError(
|
|
458
|
+
"SchemaList with a non-class item type must override _resolve_item_type() and _data_to_schema()"
|
|
459
|
+
)
|
|
454
460
|
break
|
|
455
461
|
|
|
456
462
|
def __getitem__(self, index: int) -> _M:
|
|
457
|
-
|
|
463
|
+
document = self._document[index]
|
|
464
|
+
return self._resolve_item_type(document)(self._context, document)
|
|
458
465
|
|
|
459
466
|
def __iter__(self):
|
|
460
467
|
for item in self._document:
|
|
461
|
-
yield self.
|
|
468
|
+
yield self._resolve_item_type(item)(self._context, item)
|
|
469
|
+
|
|
470
|
+
@classmethod
|
|
471
|
+
def _resolve_item_type(cls, document: dict[str, Any]) -> type[_M]:
|
|
472
|
+
"""Resolve the schema model used for a document item.
|
|
473
|
+
|
|
474
|
+
Subclasses with heterogeneous document items can override this hook.
|
|
475
|
+
"""
|
|
476
|
+
return cls._item_type
|
|
462
477
|
|
|
463
478
|
def __len__(self) -> int:
|
|
464
479
|
return len(self._document)
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
"""Helpers for warming typed-object data in the local cache."""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from evo.common import IFeedback
|
|
21
|
+
from evo.common.utils import NoFeedback
|
|
22
|
+
from evo.objects import DownloadedObject
|
|
23
|
+
from evo.objects.io import _CACHE_SCOPE
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def collect_data_ids(documents: Any) -> list[str]:
|
|
27
|
+
"""Collect de-duplicated ``data`` references from document values."""
|
|
28
|
+
ids: list[str] = []
|
|
29
|
+
|
|
30
|
+
def visit(value: Any) -> None:
|
|
31
|
+
if isinstance(value, dict):
|
|
32
|
+
for key, nested in value.items():
|
|
33
|
+
if key == "data" and isinstance(nested, str) and nested not in ids:
|
|
34
|
+
ids.append(nested)
|
|
35
|
+
else:
|
|
36
|
+
visit(nested)
|
|
37
|
+
elif isinstance(value, (list, tuple)):
|
|
38
|
+
for nested in value:
|
|
39
|
+
visit(nested)
|
|
40
|
+
|
|
41
|
+
visit(documents)
|
|
42
|
+
return ids
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def prefetch_object_data(
|
|
46
|
+
obj: DownloadedObject,
|
|
47
|
+
*,
|
|
48
|
+
data_ids: Sequence[str] | None = None,
|
|
49
|
+
max_concurrent: int = 8,
|
|
50
|
+
fb: IFeedback = NoFeedback,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Warm cache entries referenced by an object, downloading each ID at most once."""
|
|
53
|
+
if max_concurrent < 1:
|
|
54
|
+
raise ValueError("max_concurrent must be at least 1")
|
|
55
|
+
cache = obj.get_cache()
|
|
56
|
+
if cache is None:
|
|
57
|
+
raise ValueError("prefetch requires an IContext with a cache")
|
|
58
|
+
identifiers = list(dict.fromkeys(data_ids if data_ids is not None else collect_data_ids(obj.as_dict())))
|
|
59
|
+
if not identifiers:
|
|
60
|
+
return
|
|
61
|
+
cache_location = cache.get_location(environment=obj.get_environment(), scope=_CACHE_SCOPE)
|
|
62
|
+
identifiers = [identifier for identifier in identifiers if not (cache_location / identifier).exists()]
|
|
63
|
+
if not identifiers:
|
|
64
|
+
return
|
|
65
|
+
contexts = list(obj.prepare_data_download(identifiers))
|
|
66
|
+
semaphore = asyncio.Semaphore(max_concurrent)
|
|
67
|
+
|
|
68
|
+
async def download(context: Any) -> None:
|
|
69
|
+
async with semaphore:
|
|
70
|
+
await context.download_to_cache(cache, obj.get_connector().transport, fb=fb)
|
|
71
|
+
|
|
72
|
+
await asyncio.gather(*(download(context) for context in contexts))
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
import typing
|
|
15
15
|
import uuid
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import TYPE_CHECKING, Annotated, Any
|
|
17
|
+
from typing import TYPE_CHECKING, Annotated, Any, Protocol, runtime_checkable
|
|
18
18
|
from uuid import UUID
|
|
19
19
|
|
|
20
20
|
import pandas as pd
|
|
@@ -41,11 +41,13 @@ if TYPE_CHECKING:
|
|
|
41
41
|
|
|
42
42
|
__all__ = [
|
|
43
43
|
"Attribute",
|
|
44
|
+
"AttributeDescription",
|
|
44
45
|
"Attributes",
|
|
45
46
|
"BlockModelAttribute",
|
|
46
47
|
"BlockModelAttributes",
|
|
47
48
|
"BlockModelPendingAttribute",
|
|
48
49
|
"PendingAttribute",
|
|
50
|
+
"Unit",
|
|
49
51
|
]
|
|
50
52
|
|
|
51
53
|
|
|
@@ -53,6 +55,13 @@ class UnSupportedDataTypeError(Exception):
|
|
|
53
55
|
"""An unsupported data type was encountered while processing data."""
|
|
54
56
|
|
|
55
57
|
|
|
58
|
+
@runtime_checkable
|
|
59
|
+
class Unit(Protocol):
|
|
60
|
+
"""A schema unit enum represented by its string value."""
|
|
61
|
+
|
|
62
|
+
value: str
|
|
63
|
+
|
|
64
|
+
|
|
56
65
|
def _infer_attribute_type_from_series(series: pd.Series) -> str:
|
|
57
66
|
"""Infer the attribute type from a Pandas Series.
|
|
58
67
|
|
|
@@ -89,13 +98,20 @@ _attribute_table_formats = {
|
|
|
89
98
|
class AttributeDescription:
|
|
90
99
|
discipline: str = ""
|
|
91
100
|
type: str = ""
|
|
92
|
-
unit: str | None = None
|
|
101
|
+
unit: str | Unit | None = None
|
|
93
102
|
scale: str | None = None
|
|
94
103
|
extensions: dict[str, typing.Any] | None = None
|
|
95
104
|
tags: dict[str, str] | None = None
|
|
96
105
|
|
|
97
|
-
def
|
|
98
|
-
|
|
106
|
+
def __post_init__(self) -> None:
|
|
107
|
+
if self.unit is None or isinstance(self.unit, str):
|
|
108
|
+
return
|
|
109
|
+
if not isinstance(self.unit, Unit) or not isinstance(self.unit.value, str):
|
|
110
|
+
raise TypeError("unit must be a str, a Unit with a string value, or None")
|
|
111
|
+
self.unit = self.unit.value
|
|
112
|
+
|
|
113
|
+
def to_schema(self) -> dict[str, Any]:
|
|
114
|
+
result: dict[str, Any] = {
|
|
99
115
|
"discipline": self.discipline,
|
|
100
116
|
"type": self.type,
|
|
101
117
|
}
|
|
@@ -117,6 +133,7 @@ class Attribute(SchemaModel):
|
|
|
117
133
|
_attribute_type: Annotated[str, SchemaLocation("attribute_type")]
|
|
118
134
|
_key: Annotated[str | None, SchemaLocation("key")]
|
|
119
135
|
_data: Annotated[str, SchemaLocation("values.data")]
|
|
136
|
+
_attribute_description: Annotated[dict[str, Any] | None, SchemaLocation("attribute_description")]
|
|
120
137
|
|
|
121
138
|
@property
|
|
122
139
|
def key(self) -> str:
|
|
@@ -132,6 +149,12 @@ class Attribute(SchemaModel):
|
|
|
132
149
|
"""The type of this attribute."""
|
|
133
150
|
return self._attribute_type
|
|
134
151
|
|
|
152
|
+
@property
|
|
153
|
+
def attribute_description(self) -> AttributeDescription | None:
|
|
154
|
+
"""Optional descriptive metadata associated with this attribute."""
|
|
155
|
+
raw = self._attribute_description
|
|
156
|
+
return AttributeDescription(**raw) if raw else None
|
|
157
|
+
|
|
135
158
|
@property
|
|
136
159
|
def exists(self) -> bool:
|
|
137
160
|
"""Whether this attribute exists on the object.
|
|
@@ -241,7 +264,7 @@ class Attributes(SchemaList[Attribute]):
|
|
|
241
264
|
attribute descriptions are attached to the DataFrame's `attrs` attribute.
|
|
242
265
|
|
|
243
266
|
>>> df.attrs
|
|
244
|
-
{'
|
|
267
|
+
{'attribute_descriptions': {<column names>: <AttributeDescription>}, ...}
|
|
245
268
|
"""
|
|
246
269
|
|
|
247
270
|
_schema_path: str | None = None
|
|
@@ -320,8 +343,8 @@ class Attributes(SchemaList[Attribute]):
|
|
|
320
343
|
if attr_desc is not None:
|
|
321
344
|
if not isinstance(attr_desc, AttributeDescription):
|
|
322
345
|
raise TypeError("attribute description must be a AttributeDescription.")
|
|
323
|
-
if attr_desc.
|
|
324
|
-
attr_doc["attribute_description"] =
|
|
346
|
+
if description := attr_desc.to_schema():
|
|
347
|
+
attr_doc["attribute_description"] = description
|
|
325
348
|
|
|
326
349
|
attributes_list.append(attr_doc)
|
|
327
350
|
|
|
@@ -337,7 +360,15 @@ class Attributes(SchemaList[Attribute]):
|
|
|
337
360
|
"""
|
|
338
361
|
attributes = [self[key] for key in keys] if keys else list(self)
|
|
339
362
|
parts = [await attribute.to_dataframe(fb=fb_part) for attribute, fb_part in iter_with_fb(attributes, fb)]
|
|
340
|
-
|
|
363
|
+
result = pd.concat(parts, axis=1) if len(parts) > 0 else pd.DataFrame()
|
|
364
|
+
descriptions = {
|
|
365
|
+
attribute.name: description
|
|
366
|
+
for attribute in attributes
|
|
367
|
+
if isinstance(attribute, Attribute) and (description := attribute.attribute_description) is not None
|
|
368
|
+
}
|
|
369
|
+
if descriptions:
|
|
370
|
+
result.attrs["attribute_descriptions"] = descriptions
|
|
371
|
+
return result
|
|
341
372
|
|
|
342
373
|
async def append_attribute(self, df: pd.DataFrame, fb: IFeedback = NoFeedback):
|
|
343
374
|
"""Add a new attribute to the object.
|
|
@@ -601,3 +632,14 @@ class Category(SchemaModel):
|
|
|
601
632
|
if self._context.is_data_modified(self._data):
|
|
602
633
|
raise DataLoaderError("Data was modified since the object was downloaded")
|
|
603
634
|
return await self._obj.download_category_dataframe(self.as_dict(), fb=fb)
|
|
635
|
+
|
|
636
|
+
async def to_indexed_dataframe(self, fb: IFeedback = NoFeedback) -> pd.DataFrame:
|
|
637
|
+
"""Load the persisted category lookup as ``[key, value]`` rows.
|
|
638
|
+
|
|
639
|
+
Pandas categorical codes are dense positional values and must not be used
|
|
640
|
+
as schema lookup keys. This method is intended for joins involving an
|
|
641
|
+
index column such as ``hole_index``.
|
|
642
|
+
"""
|
|
643
|
+
if self._context.is_data_modified(self._data):
|
|
644
|
+
raise DataLoaderError("Data was modified since the object was downloaded")
|
|
645
|
+
return await self._obj.download_dataframe(self.as_dict()["table"], fb=fb)
|
|
@@ -14,15 +14,18 @@ from __future__ import annotations
|
|
|
14
14
|
import copy
|
|
15
15
|
import sys
|
|
16
16
|
import weakref
|
|
17
|
+
from collections.abc import Sequence
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from typing import Annotated, Any, ClassVar
|
|
19
20
|
from uuid import UUID
|
|
20
21
|
|
|
21
22
|
from evo import jmespath
|
|
22
|
-
from evo.common import IContext, StaticContext
|
|
23
|
+
from evo.common import IContext, IFeedback, StaticContext
|
|
24
|
+
from evo.common.utils import NoFeedback
|
|
23
25
|
from evo.objects import DownloadedObject, ObjectMetadata, ObjectReference, ObjectSchema, SchemaVersion
|
|
24
26
|
|
|
25
27
|
from ._model import ModelContext, SchemaLocation, SchemaModel
|
|
28
|
+
from ._prefetch import prefetch_object_data
|
|
26
29
|
from ._utils import (
|
|
27
30
|
create_geoscience_object,
|
|
28
31
|
replace_geoscience_object,
|
|
@@ -416,6 +419,17 @@ class BaseObject(_BaseObject):
|
|
|
416
419
|
tags: Annotated[dict[str, str], SchemaLocation("tags")] = {}
|
|
417
420
|
extensions: Annotated[dict, SchemaLocation("extensions")] = {}
|
|
418
421
|
|
|
422
|
+
async def prefetch(
|
|
423
|
+
self,
|
|
424
|
+
*,
|
|
425
|
+
data_ids: Sequence[str] | None = None,
|
|
426
|
+
max_concurrent: int = 8,
|
|
427
|
+
fb: IFeedback = NoFeedback,
|
|
428
|
+
) -> None:
|
|
429
|
+
"""Warm cached data files referenced by this object."""
|
|
430
|
+
|
|
431
|
+
await prefetch_object_data(self._obj, data_ids=data_ids, max_concurrent=max_concurrent, fb=fb)
|
|
432
|
+
|
|
419
433
|
@classmethod
|
|
420
434
|
def create(
|
|
421
435
|
cls,
|