evo-objects 0.6.3__tar.gz → 0.6.5__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 (49) hide show
  1. {evo_objects-0.6.3 → evo_objects-0.6.5}/PKG-INFO +2 -2
  2. {evo_objects-0.6.3 → evo_objects-0.6.5}/pyproject.toml +2 -2
  3. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/data.py +2 -1
  4. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_grid.py +25 -7
  5. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/base.py +8 -2
  6. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/block_model_ref.py +8 -4
  7. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/regular_grid.py +1 -0
  8. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/regular_masked_grid.py +1 -0
  9. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/tensor_grid.py +1 -0
  10. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/types.py +50 -9
  11. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/variogram.py +12 -6
  12. {evo_objects-0.6.3 → evo_objects-0.6.5}/.gitignore +0 -0
  13. {evo_objects-0.6.3 → evo_objects-0.6.5}/LICENSE.md +0 -0
  14. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/__init__.py +0 -0
  15. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/_model_config.py +0 -0
  16. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/client/__init__.py +0 -0
  17. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/client/api_client.py +0 -0
  18. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/client/object_client.py +0 -0
  19. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/client/parse.py +0 -0
  20. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/__init__.py +0 -0
  21. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/api/__init__.py +0 -0
  22. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/api/data_api.py +0 -0
  23. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/api/metadata_api.py +0 -0
  24. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/api/objects_api.py +0 -0
  25. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/api/stages_api.py +0 -0
  26. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/endpoints/models.py +0 -0
  27. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/exceptions.py +0 -0
  28. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/io.py +0 -0
  29. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/parquet/__init__.py +0 -0
  30. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/parquet/loader.py +0 -0
  31. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/py.typed +0 -0
  32. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/__init__.py +0 -0
  33. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_data.py +0 -0
  34. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_downhole.py +0 -0
  35. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_model.py +0 -0
  36. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_prefetch.py +0 -0
  37. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/_utils.py +0 -0
  38. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/attributes.py +0 -0
  39. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/downhole_collection.py +0 -0
  40. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/downhole_intervals.py +0 -0
  41. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/exceptions.py +0 -0
  42. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/pointset.py +0 -0
  43. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/typed/spatial.py +0 -0
  44. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/utils/__init__.py +0 -0
  45. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/utils/data.py +0 -0
  46. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/utils/downhole.py +0 -0
  47. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/utils/table_formats.py +0 -0
  48. {evo_objects-0.6.3 → evo_objects-0.6.5}/src/evo/objects/utils/tables.py +0 -0
  49. {evo_objects-0.6.3 → evo_objects-0.6.5}/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
3
+ Version: 0.6.5
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.26
12
+ Requires-Dist: evo-sdk-common[jmespath]>=0.5.28
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.6.3"
4
+ version = "0.6.5"
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.26",
13
+ "evo-sdk-common[jmespath]>=0.5.28",
14
14
  "pydantic>=2,<3",
15
15
  ]
16
16
 
@@ -91,7 +91,8 @@ class ObjectReference(str):
91
91
  inst = str.__new__(cls, value)
92
92
 
93
93
  parsed = urlparse(value)
94
- if parsed.scheme != "https":
94
+ if not parsed.scheme:
95
+ # Valid hub URLs are always HTTPS, but it is sometimes useful to pass HTTP ones for testing, so these are allowed here too.
95
96
  raise ValueError("Reference must be a valid HTTPS URL")
96
97
 
97
98
  inst.hub_url = f"{parsed.scheme}://{parsed.netloc}/"
@@ -22,6 +22,7 @@ import pandas as pd
22
22
  from pydantic import AliasChoices, Field
23
23
 
24
24
  from evo.common import IFeedback
25
+ from evo.common.typed import FloatArrayLike3, IntArrayLike3
25
26
  from evo.common.utils import NoFeedback
26
27
 
27
28
  from ._model import DataLocation, SchemaLocation, SchemaModel
@@ -46,23 +47,35 @@ class Base3DGridData(BaseSpatialObjectData):
46
47
  """Base class for all 3D grid data.
47
48
 
48
49
  Contains the common properties shared by all grid types: origin, size, rotation, and cell_data.
50
+ Origin and size accept their named types or three values in a list, tuple, or 1D array.
51
+ All inputs, including named types, are validated and normalized.
49
52
  """
50
53
 
51
- origin: Point3
52
- size: Size3i
54
+ origin: Point3 | FloatArrayLike3
55
+ size: Size3i | IntArrayLike3
53
56
  cell_data: pd.DataFrame | None = None
54
57
  rotation: Rotation | None = None
55
58
 
59
+ def __post_init__(self) -> None:
60
+ object.__setattr__(self, "origin", Point3.from_array_like(self.origin))
61
+ object.__setattr__(self, "size", Size3i.from_array_like(self.size))
62
+
56
63
 
57
64
  @dataclass(kw_only=True, frozen=True)
58
65
  class BaseRegular3DGridData(Base3DGridData):
59
66
  """Base class for regular 3D grid data (both masked and non-masked).
60
67
 
61
68
  Contains the common properties shared by Regular3DGridData and RegularMasked3DGridData.
62
- Adds cell_size to the base grid properties.
69
+ Adds cell_size to the base grid properties; it accepts a Size3d or three
70
+ values in a list, tuple, or 1D array.
71
+ All inputs, including Size3d instances, are validated and normalized.
63
72
  """
64
73
 
65
- cell_size: Size3d
74
+ cell_size: Size3d | FloatArrayLike3
75
+
76
+ def __post_init__(self) -> None:
77
+ super().__post_init__()
78
+ object.__setattr__(self, "cell_size", Size3d.from_array_like(self.cell_size))
66
79
 
67
80
  def compute_bounding_box(self) -> BoundingBox:
68
81
  return BoundingBox.from_regular_grid(self.origin, self.size, self.cell_size, self.rotation)
@@ -223,15 +236,20 @@ class RegularBlockModelData:
223
236
  """
224
237
 
225
238
  name: str
226
- origin: Point3
227
- n_blocks: Size3i
228
- block_size: Size3d
239
+ origin: Point3 | FloatArrayLike3
240
+ n_blocks: Size3i | IntArrayLike3
241
+ block_size: Size3d | FloatArrayLike3
229
242
  cell_data: pd.DataFrame | None = None
230
243
  description: str | None = None
231
244
  coordinate_reference_system: str | None = None
232
245
  size_unit_id: str | None = None
233
246
  units: dict[str, str] = field(default_factory=dict)
234
247
 
248
+ def __post_init__(self) -> None:
249
+ object.__setattr__(self, "origin", Point3.from_array_like(self.origin))
250
+ object.__setattr__(self, "n_blocks", Size3i.from_array_like(self.n_blocks))
251
+ object.__setattr__(self, "block_size", Size3d.from_array_like(self.block_size))
252
+
235
253
 
236
254
  @dataclass(frozen=True, kw_only=True)
237
255
  class BlockModelData(BaseSpatialObjectData):
@@ -362,12 +362,18 @@ class _BaseObject(SchemaModel):
362
362
 
363
363
  :return: A new instance with refreshed data.
364
364
 
365
- Example:
365
+ Example::
366
+
366
367
  >>> # After a remote operation modifies the object...
367
368
  >>> obj = await obj.refresh()
368
369
  >>> obj.attributes # Now shows the latest attributes
369
370
  """
370
- return await self.from_reference(self._api_context, self.metadata.url)
371
+ # metadata.url pins the current version_id, which would just re-download the same version.
372
+ latest_reference = ObjectReference.new(
373
+ environment=self.metadata.environment,
374
+ object_id=self.metadata.id,
375
+ )
376
+ return await self.from_reference(self._api_context, latest_reference)
371
377
 
372
378
  def search(self, expression: str) -> Any:
373
379
  """Search the object metadata using a JMESPath expression.
@@ -192,7 +192,8 @@ class BlockModel(BaseSpatialObject):
192
192
  :param fb: Optional feedback interface for progress reporting.
193
193
  :return: DataFrame containing the block model data with user-friendly column names.
194
194
 
195
- Example:
195
+ Example::
196
+
196
197
  >>> df = await block_model.to_dataframe()
197
198
  >>> df.head()
198
199
  """
@@ -207,7 +208,8 @@ class BlockModel(BaseSpatialObject):
207
208
 
208
209
  :return: A new BlockModel instance with refreshed data.
209
210
 
210
- Example:
211
+ Example::
212
+
211
213
  >>> # After running kriging that adds attributes...
212
214
  >>> block_model = await block_model.refresh()
213
215
  >>> block_model.attributes # Now shows the new attributes
@@ -335,7 +337,8 @@ class BlockModel(BaseSpatialObject):
335
337
  :param fb: Optional feedback interface for progress reporting.
336
338
  :return: The updated BlockModel instance (refreshed from server).
337
339
 
338
- Example:
340
+ Example::
341
+
339
342
  >>> from evo.blockmodels import Units
340
343
  >>> block_model = await block_model.set_attribute_units({
341
344
  ... "Au": Units.GRAMS_PER_TONNE,
@@ -361,7 +364,8 @@ class BlockModel(BaseSpatialObject):
361
364
  :param fb: Optional feedback interface for progress reporting.
362
365
  :return: A Report instance representing the created report.
363
366
 
364
- Example:
367
+ Example::
368
+
365
369
  >>> from evo.blockmodels.typed import ReportSpecificationData, ReportColumnSpec, ReportCategorySpec
366
370
  >>> report = await block_model.create_report(ReportSpecificationData(
367
371
  ... name="Gold Resource Report",
@@ -36,6 +36,7 @@ class Regular3DGridData(BaseRegular3DGridData):
36
36
  vertex_data: pd.DataFrame | None = None
37
37
 
38
38
  def __post_init__(self):
39
+ super().__post_init__()
39
40
  if self.cell_data is not None and self.cell_data.shape[0] != self.size.total_size:
40
41
  raise ObjectValidationError(
41
42
  f"The number of rows in the cell_data dataframe ({self.cell_data.shape[0]}) does not match the number of cells in the grid ({self.size.nx * self.size.ny * self.size.nz})."
@@ -43,6 +43,7 @@ class RegularMasked3DGridData(BaseRegular3DGridData):
43
43
  mask: np.ndarray
44
44
 
45
45
  def __post_init__(self):
46
+ super().__post_init__()
46
47
  if self.mask.shape[0] != self.size.total_size:
47
48
  raise ObjectValidationError(
48
49
  f"The number of rows in the mask ({self.mask.shape[0]}) does not match the number of cells in the grid ({self.size.nx * self.size.ny * self.size.nz})."
@@ -69,6 +69,7 @@ class Tensor3DGridData(Base3DGridData):
69
69
  vertex_data: pd.DataFrame | None = None
70
70
 
71
71
  def __post_init__(self):
72
+ super().__post_init__()
72
73
  # Validate cell size array lengths
73
74
  if self.cell_sizes_x.shape[0] != self.size.nx:
74
75
  raise ObjectValidationError(
@@ -12,6 +12,7 @@
12
12
  from __future__ import annotations
13
13
 
14
14
  from dataclasses import dataclass
15
+ from numbers import Real
15
16
  from typing import Annotated, Any, overload
16
17
 
17
18
  import numpy as np
@@ -20,7 +21,7 @@ import pydantic
20
21
 
21
22
  # Import basic geometry types from evo.common and re-export
22
23
  from evo.common.crs import EpsgCode, parse_crs
23
- from evo.common.typed import Point3, Size3d, Size3i
24
+ from evo.common.typed import FloatArrayLike3, Point3, Size3d, Size3i, _validate_array_like
24
25
 
25
26
  __all__ = [
26
27
  "BoundingBox",
@@ -204,12 +205,26 @@ class BoundingBox:
204
205
 
205
206
  @dataclass(frozen=True)
206
207
  class Rotation:
207
- """A rotation defined by dip azimuth, dip, and pitch angles."""
208
+ """A clockwise intrinsic ZXZ Euler rotation in degrees.
209
+
210
+ Dip azimuth [0, 360], dip [0, 180], and pitch [0, 360] rotate about
211
+ successive local Z, X, and Z axes, respectively. For column vectors,
212
+ the rotation matrix is Rz(dip azimuth) @ Rx(dip) @ Rz(pitch).
213
+ """
208
214
 
209
215
  dip_azimuth: float
210
216
  dip: float
211
217
  pitch: float
212
218
 
219
+ def __post_init__(self) -> None:
220
+ for name, value, lower, upper in (
221
+ ("dip_azimuth", self.dip_azimuth, 0, 360),
222
+ ("dip", self.dip, 0, 180),
223
+ ("pitch", self.pitch, 0, 360),
224
+ ):
225
+ if not isinstance(value, Real) or isinstance(value, bool) or not lower <= value <= upper:
226
+ raise ValueError(f"{name} must be a finite angle between {lower} and {upper} degrees (inclusive)")
227
+
213
228
  def as_rotation_matrix(self) -> np.ndarray:
214
229
  """Convert the rotation to a rotation matrix.
215
230
 
@@ -246,6 +261,12 @@ class Rotation:
246
261
  # Combined intrinsic rotations: dip_azimuth -> dip -> pitch
247
262
  return dip_azimuth_rotation_matrix @ dip_rotation_matrix @ pitch_rotation_matrix
248
263
 
264
+ @classmethod
265
+ def from_array_like(cls, value: Rotation | FloatArrayLike3) -> Rotation:
266
+ if isinstance(value, Rotation):
267
+ value = (value.dip_azimuth, value.dip, value.pitch)
268
+ return cls(*_validate_array_like(value, kind=Real, positive=False))
269
+
249
270
 
250
271
  @dataclass
251
272
  class EllipsoidRanges:
@@ -256,9 +277,9 @@ class EllipsoidRanges:
256
277
  minor: float
257
278
 
258
279
  def __init__(self, major: float, semi_major: float, minor: float):
259
- self.major = major
260
- self.semi_major = semi_major
261
- self.minor = minor
280
+ self.major, self.semi_major, self.minor = _validate_array_like(
281
+ (major, semi_major, minor), kind=Real, positive=True
282
+ )
262
283
 
263
284
  def to_dict(self) -> dict[str, Any]:
264
285
  return {"major": self.major, "semi_major": self.semi_major, "minor": self.minor}
@@ -270,17 +291,37 @@ class EllipsoidRanges:
270
291
  minor=self.minor * factor,
271
292
  )
272
293
 
294
+ @classmethod
295
+ def from_array_like(cls, value: EllipsoidRanges | FloatArrayLike3) -> EllipsoidRanges:
296
+ if isinstance(value, EllipsoidRanges):
297
+ value = (value.major, value.semi_major, value.minor)
298
+ return cls(*_validate_array_like(value, kind=Real, positive=True))
299
+
273
300
 
274
301
  @dataclass
275
302
  class Ellipsoid:
276
- """An ellipsoid defining a spatial region."""
303
+ """An ellipsoid defining a spatial region.
304
+
305
+ Ranges are ordered major, semi-major, minor. Rotation angles are in
306
+ degrees and ordered dip azimuth [0, 360], dip [0, 180], pitch [0, 360].
307
+ They specify clockwise intrinsic rotations about the successive Z, X,
308
+ and Z axes, respectively.
309
+
310
+ Ranges accept an EllipsoidRanges instance or a three-value list, tuple,
311
+ or 1D NumPy array. Rotation accepts a Rotation instance or the same
312
+ three-value sequence forms; omitting it applies no rotation.
313
+ """
277
314
 
278
315
  ranges: EllipsoidRanges
279
316
  _rotation: Rotation | None = None
280
317
 
281
- def __init__(self, ranges: EllipsoidRanges, rotation: Rotation | None = None):
282
- self.ranges = ranges
283
- self._rotation = rotation
318
+ def __init__(
319
+ self,
320
+ ranges: EllipsoidRanges | FloatArrayLike3,
321
+ rotation: Rotation | FloatArrayLike3 | None = None,
322
+ ):
323
+ self.ranges = EllipsoidRanges.from_array_like(ranges)
324
+ self._rotation = Rotation.from_array_like(rotation) if rotation is not None else None
284
325
 
285
326
  @property
286
327
  def rotation(self) -> Rotation:
@@ -70,7 +70,8 @@ class VariogramStructure:
70
70
  - Creating search ellipsoids via scaled()
71
71
  - Kriging search neighborhoods
72
72
 
73
- Example:
73
+ Example::
74
+
74
75
  >>> # Get ellipsoid from variogram structure
75
76
  >>> var_ell = variogram.structures[0].to_ellipsoid()
76
77
  >>>
@@ -277,7 +278,8 @@ class VariogramData(BaseObjectData):
277
278
  - `modelling_space`: Set to "data" for original units or "normalscore" for gaussian space
278
279
  - `data_variance`: Should match the sill value for non-normalized data
279
280
 
280
- Example using typed structures (recommended):
281
+ Example using typed structures (recommended)::
282
+
281
283
  >>> data = VariogramData(
282
284
  ... name="My Variogram",
283
285
  ... sill=1.0,
@@ -407,7 +409,8 @@ class Variogram(BaseObject):
407
409
  Returns:
408
410
  Ellipsoid configured with the structure's anisotropy ranges and rotation.
409
411
 
410
- Example:
412
+ Example::
413
+
411
414
  >>> # Get ellipsoid from structure with largest range (default)
412
415
  >>> var_ell = variogram.get_ellipsoid()
413
416
  >>>
@@ -475,7 +478,8 @@ class Variogram(BaseObject):
475
478
  Returns:
476
479
  Tuple of (major_curve, semi_major_curve, minor_curve) as VariogramCurveData.
477
480
 
478
- Example with Plotly:
481
+ Example with Plotly::
482
+
479
483
  >>> major, semi_maj, minor = variogram.get_principal_directions()
480
484
  >>> import plotly.graph_objects as go
481
485
  >>> fig = go.Figure()
@@ -549,7 +553,8 @@ class Variogram(BaseObject):
549
553
  Returns:
550
554
  Tuple of (distance, semivariance) as numpy arrays, suitable for plotting.
551
555
 
552
- Example with Plotly:
556
+ Example with Plotly::
557
+
553
558
  >>> distance, semivariance = variogram.get_direction(azimuth=45, dip=30)
554
559
  >>> import plotly.graph_objects as go
555
560
  >>> fig = go.Figure()
@@ -557,7 +562,8 @@ class Variogram(BaseObject):
557
562
  >>> fig.update_layout(xaxis_title='Distance', yaxis_title='Semivariance')
558
563
  >>> fig.show()
559
564
 
560
- Example with Matplotlib:
565
+ Example with Matplotlib::
566
+
561
567
  >>> distance, semivariance = variogram.get_direction(azimuth=0, dip=0)
562
568
  >>> import matplotlib.pyplot as plt
563
569
  >>> plt.plot(distance, semivariance)
File without changes
File without changes