diff --git a/packages/evo-blockmodels/pyproject.toml b/packages/evo-blockmodels/pyproject.toml index ee2f5e3d..d95b4791 100644 --- a/packages/evo-blockmodels/pyproject.toml +++ b/packages/evo-blockmodels/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "evo-blockmodels" description = "Python SDK for using the Seequent Evo Geoscience Block Model API" -version = "0.6.0" +version = "0.6.1" requires-python = ">=3.10" license-files = ["LICENSE.md"] dynamic = ["readme"] diff --git a/packages/evo-blockmodels/src/evo/blockmodels/endpoints/models.py b/packages/evo-blockmodels/src/evo/blockmodels/endpoints/models.py index 515c186a..a0914eb9 100644 --- a/packages/evo-blockmodels/src/evo/blockmodels/endpoints/models.py +++ b/packages/evo-blockmodels/src/evo/blockmodels/endpoints/models.py @@ -944,14 +944,23 @@ class SizeOptionsRegular(CustomBaseModel): class UnitType(Enum): - LENGTH = "LENGTH" + UNKNOWN = "UNKNOWN" # For unit types introduced after this SDK version + MASS_PER_MASS = "MASS_PER_MASS" + MASS_PER_VOLUME = "MASS_PER_VOLUME" + DIMENSIONLESS = "GENERIC_DIMENSIONLESS" MASS = "MASS" + LENGTH = "LENGTH" VOLUME = "VOLUME" - VALUE = "VALUE" - MASS_PER_VOLUME = "MASS_PER_VOLUME" - MASS_PER_MASS = "MASS_PER_MASS" - VOLUME_PER_VOLUME = "VOLUME_PER_VOLUME" VALUE_PER_MASS = "VALUE_PER_MASS" + VALUE = "VALUE" + CATEGORY = "CATEGORY" + ANGLE = "ANGLE" + + @classmethod + def _missing_(cls, value: object) -> UnitType | None: + if isinstance(value, str): + return cls.UNKNOWN + return None class UpdateMetadataValues(CustomBaseModel): diff --git a/packages/evo-blockmodels/src/evo/blockmodels/typed/__init__.py b/packages/evo-blockmodels/src/evo/blockmodels/typed/__init__.py index 1c2ce6a5..c0bf284a 100644 --- a/packages/evo-blockmodels/src/evo/blockmodels/typed/__init__.py +++ b/packages/evo-blockmodels/src/evo/blockmodels/typed/__init__.py @@ -11,8 +11,6 @@ """Typed access for block models with pandas DataFrame support.""" -from evo.common.typed import BoundingBox, Point3, Size3d, Size3i - from .base import BaseTypedBlockModel from .regular_block_model import RegularBlockModel, RegularBlockModelData from .report import ( @@ -24,6 +22,7 @@ ReportResult, ReportSpecificationData, ) +from .types import BoundingBox, Point3, Size3d, Size3i from .units import UnitInfo, Units, UnitType, get_available_units __all__ = [ diff --git a/packages/evo-blockmodels/src/evo/blockmodels/typed/units.py b/packages/evo-blockmodels/src/evo/blockmodels/typed/units.py index bc55ef4f..ba3412d2 100644 --- a/packages/evo-blockmodels/src/evo/blockmodels/typed/units.py +++ b/packages/evo-blockmodels/src/evo/blockmodels/typed/units.py @@ -34,14 +34,23 @@ class UnitType(Enum): """Types of units supported by the Block Model Service.""" - LENGTH = "LENGTH" + UNKNOWN = "UNKNOWN" # For unit types introduced after this SDK version + MASS_PER_MASS = "MASS_PER_MASS" + MASS_PER_VOLUME = "MASS_PER_VOLUME" + DIMENSIONLESS = "GENERIC_DIMENSIONLESS" MASS = "MASS" + LENGTH = "LENGTH" VOLUME = "VOLUME" - VALUE = "VALUE" - MASS_PER_VOLUME = "MASS_PER_VOLUME" - MASS_PER_MASS = "MASS_PER_MASS" - VOLUME_PER_VOLUME = "VOLUME_PER_VOLUME" VALUE_PER_MASS = "VALUE_PER_MASS" + VALUE = "VALUE" + CATEGORY = "CATEGORY" + ANGLE = "ANGLE" + + @classmethod + def _missing_(cls, value: object) -> UnitType | None: + if isinstance(value, str): + return cls.UNKNOWN + return None @dataclass(frozen=True) @@ -64,7 +73,7 @@ class UnitInfo: """Conversion factor to the reference unit for this unit type.""" -class Units: +class Units(str, Enum): """Common unit IDs for block model attributes. These are the most commonly used unit IDs. For a complete list, @@ -86,60 +95,72 @@ class Units: await bm_ref.add_attribute(df, "metal_content", unit=Units.KILOS_PER_CUBIC_METRE) """ - # Length units - METRES = "m" - FEET = "ft" - CENTIMETRES = "cm" - - # Mass units - CARATS = "ct" - GRAMS = "g" - POUNDS = "lbm" - TROY_OUNCES = "ozm[troy]" - TONNES = "t" - KILOTONNES = "kt" - MEGATONNES = "Mt" - SHORT_TONS = "ton[US]" - THOUSAND_SHORT_TONS = "kton[US]" - MILLION_SHORT_TONS = "Mton[US]" - KILOGRAMS = "kg" - THOUSAND_POUNDS = "klbm" - MILLIGRAMS = "mg" - MICROGRAMS = "ug" - THOUSAND_CARATS = "1000 ct" - THOUSAND_TROY_OUNCES = "1000 ozm[troy]" - MILLION_TROY_OUNCES = "1000000 ozm[troy]" - MILLION_POUNDS = "Mlbm" - # Mass per mass (grade) units - PERCENT = "%[mass]" - PARTS_PER_MILLION = "ppm[mass]" + CARATS_PER_HUNDRED_TONNE = "0.01 ct/t" + CARATS_PER_TONNE = "ct/t" GRAMS_PER_TONNE = "g/t" + MICROGRAMS_PER_GRAM = "ug/g" + MICROGRAMS_PER_KILOGRAM = "ug/kg" MILLIGRAMS_PER_GRAM = "mg/g" MILLIGRAMS_PER_KILOGRAM = "mg/kg" - MICROGRAMS_PER_GRAM = "ug/g" - CARATS_PER_HUNDRED_TONNE = "0.01 ct/t" PARTS_PER_BILLION = "ppb[mass]" + PARTS_PER_MILLION = "ppm[mass]" + PERCENT = "%[mass]" TROY_OUNCES_PER_SHORT_TON = "oz t/ton[US]" - CARATS_PER_TONNE = "ct/t" - MICROGRAMS_PER_KILOGRAM = "ug/kg" # Mass per volume (density) units - KILOS_PER_CUBIC_METRE = "kg/m3" GRAMS_PER_CUBIC_CENTIMETRE = "g/cm3" + KILOS_PER_CUBIC_METRE = "kg/m3" POUNDS_PER_CUBIC_FOOT = "lbm/ft3" - TONNES_PER_CUBIC_METRE = "t/m3" SHORT_TON_PER_CUBIC_FOOT = "ton[US]/ft3" + TONNES_PER_CUBIC_METRE = "t/m3" - # Value units - DOLLARS_PER_TONNE = "$/t" - DOLLARS_PER_SHORT_TON = "$/ton[US]" - DOLLARS = "$" + # Dimensionless units + COUNT = "count" + RATIO = "ratio" + + # Mass units + CARATS = "ct" + GRAMS = "g" + KILOGRAMS = "kg" + KILOTONNES = "kt" + MEGATONNES = "Mt" + MICROGRAMS = "ug" + MILLIGRAMS = "mg" + MILLION_POUNDS = "Mlbm" + MILLION_SHORT_TONS = "Mton[US]" + MILLION_TROY_OUNCES = "1000000 ozm[troy]" + POUNDS = "lbm" + SHORT_TONS = "ton[US]" + THOUSAND_CARATS = "1000 ct" + THOUSAND_POUNDS = "klbm" + THOUSAND_SHORT_TONS = "kton[US]" + THOUSAND_TROY_OUNCES = "1000 ozm[troy]" + TONNES = "t" + TROY_OUNCES = "ozm[troy]" + + # Length units + CENTIMETRES = "cm" + FEET = "ft" + METRES = "m" # Volume units CUBIC_CENTIMETRES = "cm3" - CUBIC_METRES = "m3" CUBIC_FEET = "ft3" + CUBIC_METRES = "m3" + + # Value units + DOLLARS = "$" + DOLLARS_PER_SHORT_TON = "$/ton[US]" + DOLLARS_PER_TONNE = "$/t" + + # Category units + NUMERIC_CATEGORY = "numeric_category" + + # Angle units + DEGREES = "dega" + RADIANS = "rad" + REVOLUTIONS = "rev" async def get_available_units(context: IContext) -> list[UnitInfo]: diff --git a/packages/evo-blockmodels/tests/test_list_block_models.py b/packages/evo-blockmodels/tests/test_list_block_models.py index c211a7e7..cfe0e974 100644 --- a/packages/evo-blockmodels/tests/test_list_block_models.py +++ b/packages/evo-blockmodels/tests/test_list_block_models.py @@ -1,11 +1,13 @@ import json import uuid from datetime import datetime, timezone +from unittest import mock from uuid import uuid4 from evo.blockmodels import BlockModelAPIClient from evo.blockmodels.data import ListingVersion, RegularGridDefinition from evo.blockmodels.endpoints.models import MissingColumnPolicy +from evo.blockmodels.endpoints.models import UnitType as EndpointUnitType from evo.common import Environment from evo.common.test_tools import ( BASE_URL, @@ -160,6 +162,52 @@ async def test_list_versions_returns_versions(self) -> None: self.assertEqual(result[0].version_id, 2) self.assertEqual(result[1].version_id, 1) + async def test_list_versions_accepts_unknown_referenced_unit_type(self) -> None: + bm_id = uuid.uuid4() + version = self.make_version(1, str(uuid.uuid4())) + referenced_units = [ + { + "conversion_factor": 1.0, + "description": "A future unit", + "symbol": "future", + "unit_id": "future", + "unit_type": "FUTURE_UNIT_TYPE", + }, + ] + captured_units = [] + list_block_model_versions = self.client._versions_api.list_block_model_versions + + async def capture_referenced_units(*args, **kwargs): + response = await list_block_model_versions(*args, **kwargs) + captured_units.extend(response.referenced_units) + return response + + with ( + self.transport.set_http_response( + 200, + json.dumps( + { + "count": 1, + "limit": 100, + "offset": 0, + "results": [version], + "total": 1, + "referenced_units": referenced_units, + } + ), + headers={"Content-Type": "application/json"}, + ), + mock.patch.object( + self.client._versions_api, + "list_block_model_versions", + side_effect=capture_referenced_units, + ), + ): + result = await self.client.list_versions(bm_id) + + self.assertEqual(len(result), 1) + self.assertIs(captured_units[0].unit_type, EndpointUnitType.UNKNOWN) + async def test_list_versions_with_preview_sends_header(self) -> None: bm_id = uuid.uuid4() v1 = self.make_version(1, str(uuid.uuid4()))