evo-objects 0.4.2__tar.gz → 0.6.1__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 (47) hide show
  1. {evo_objects-0.4.2 → evo_objects-0.6.1}/PKG-INFO +2 -2
  2. {evo_objects-0.4.2 → evo_objects-0.6.1}/pyproject.toml +2 -2
  3. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/__init__.py +7 -1
  4. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/_data.py +15 -2
  5. evo_objects-0.6.1/src/evo/objects/typed/_downhole.py +62 -0
  6. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/_model.py +68 -21
  7. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/attributes.py +78 -5
  8. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/base.py +2 -25
  9. evo_objects-0.6.1/src/evo/objects/typed/downhole_collection.py +303 -0
  10. evo_objects-0.6.1/src/evo/objects/typed/downhole_intervals.py +246 -0
  11. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/spatial.py +2 -8
  12. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/types.py +24 -41
  13. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/utils/data.py +3 -0
  14. {evo_objects-0.4.2 → evo_objects-0.6.1}/.gitignore +0 -0
  15. {evo_objects-0.4.2 → evo_objects-0.6.1}/LICENSE.md +0 -0
  16. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/__init__.py +0 -0
  17. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/_model_config.py +0 -0
  18. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/client/__init__.py +0 -0
  19. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/client/api_client.py +0 -0
  20. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/client/object_client.py +0 -0
  21. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/client/parse.py +0 -0
  22. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/data.py +0 -0
  23. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/__init__.py +0 -0
  24. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/api/__init__.py +0 -0
  25. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/api/data_api.py +0 -0
  26. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/api/metadata_api.py +0 -0
  27. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/api/objects_api.py +0 -0
  28. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/api/stages_api.py +0 -0
  29. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/endpoints/models.py +0 -0
  30. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/exceptions.py +0 -0
  31. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/io.py +0 -0
  32. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/parquet/__init__.py +0 -0
  33. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/parquet/loader.py +0 -0
  34. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/py.typed +0 -0
  35. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/_grid.py +0 -0
  36. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/_utils.py +0 -0
  37. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/block_model_ref.py +0 -0
  38. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/exceptions.py +0 -0
  39. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/pointset.py +0 -0
  40. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/regular_grid.py +0 -0
  41. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/regular_masked_grid.py +0 -0
  42. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/tensor_grid.py +0 -0
  43. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/typed/variogram.py +0 -0
  44. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/utils/__init__.py +0 -0
  45. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/utils/table_formats.py +0 -0
  46. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/utils/tables.py +0 -0
  47. {evo_objects-0.4.2 → evo_objects-0.6.1}/src/evo/objects/utils/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evo-objects
3
- Version: 0.4.2
3
+ Version: 0.6.1
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.8
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.2"
4
+ version = "0.6.1"
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.8",
13
+ "evo-sdk-common[jmespath]>=0.5.26",
14
14
  "pydantic>=2,<3",
15
15
  ]
16
16
 
@@ -1,4 +1,4 @@
1
- # Copyright © 2025 Bentley Systems, Incorporated
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
- await self.attributes.clear()
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
- return builder.document
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
@@ -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 SchemaProperty(Generic[_T]):
90
- """Descriptor for data within a Geoscience Object schema.
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]) -> SchemaProperty[_T]: ...
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
- def _set_property_value(schema_property: SchemaProperty, document: dict[str, Any], value: Any) -> None:
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, SchemaProperty[Any]] = {}
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, SchemaProperty[Any]] = {}
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
- for key in cls._schema_properties.keys():
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 © 2025 Bentley Systems, Incorporated
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(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
- col_df = df[[col]]
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: dict[str, Any] = {
231
- "schema": str(schema_id),
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