From 590732a4a7059c9fce1e9b7c090c9d660e329538 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Thu, 30 Jul 2026 10:14:20 +0200 Subject: [PATCH 1/7] AssetCollection conversion --- tilebox-datasets/pyproject.toml | 1 + tilebox-datasets/tests/test_assets.py | 219 +++++++ .../tests/testdata/sentinel2_assets.binpb | 3 + .../tests/testdata/sentinel2_storage.binpb | 3 + tilebox-datasets/tilebox/datasets/__init__.py | 15 +- .../tilebox/datasets/assets/__init__.py | 3 + .../tilebox/datasets/assets/assets.py | 399 +++++++++++++ .../tilebox/datasets/assets/converters.py | 125 ++++ .../tilebox/datasets/assets/stac/__init__.py | 0 .../datasets/assets/stac/authentication.py | 78 +++ .../tilebox/datasets/assets/stac/metadata.py | 80 +++ .../tilebox/datasets/assets/stac/product.py | 18 + .../tilebox/datasets/assets/stac/sar.py | 29 + .../tilebox/datasets/assets/stac/satellite.py | 31 + .../tilebox/datasets/assets/stac/storage.py | 31 + .../tilebox/datasets/datapoints.py | 13 + .../datasets/stac/v1/authentication_pb2.py | 12 +- .../datasets/stac/v1/authentication_pb2.pyi | 12 +- .../datasets/datasets/stac/v1/storage_pb2.py | 27 +- .../datasets/datasets/stac/v1/storage_pb2.pyi | 18 +- tilebox-storage/pyproject.toml | 2 + tilebox-storage/tests/test_asset_client.py | 147 +++++ tilebox-storage/tests/test_geotiff.py | 47 ++ tilebox-storage/tilebox/storage/aio.py | 28 + tilebox-storage/tilebox/storage/client.py | 327 +++++++++++ tilebox-storage/tilebox/storage/geotiff.py | 100 ++++ uv.lock | 547 ++++++++++++------ 27 files changed, 2118 insertions(+), 197 deletions(-) create mode 100644 tilebox-datasets/tests/test_assets.py create mode 100644 tilebox-datasets/tests/testdata/sentinel2_assets.binpb create mode 100644 tilebox-datasets/tests/testdata/sentinel2_storage.binpb create mode 100644 tilebox-datasets/tilebox/datasets/assets/__init__.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/assets.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/converters.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/__init__.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/authentication.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/metadata.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/product.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/sar.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/satellite.py create mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/storage.py create mode 100644 tilebox-datasets/tilebox/datasets/datapoints.py create mode 100644 tilebox-storage/tests/test_asset_client.py create mode 100644 tilebox-storage/tests/test_geotiff.py create mode 100644 tilebox-storage/tilebox/storage/client.py create mode 100644 tilebox-storage/tilebox/storage/geotiff.py diff --git a/tilebox-datasets/pyproject.toml b/tilebox-datasets/pyproject.toml index 17ad365..8785e0b 100644 --- a/tilebox-datasets/pyproject.toml +++ b/tilebox-datasets/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "pandas>=2.1", "shapely>=2", "promise>=2.3", + "typing-extensions>=4.5", ] [dependency-groups] diff --git a/tilebox-datasets/tests/test_assets.py b/tilebox-datasets/tests/test_assets.py new file mode 100644 index 0000000..deee45e --- /dev/null +++ b/tilebox-datasets/tests/test_assets.py @@ -0,0 +1,219 @@ +from pathlib import Path + +import numpy as np +import pytest +import xarray as xr +from google.protobuf.struct_pb2 import Struct + +from tilebox.datasets import iter_datapoints +from tilebox.datasets.assets import AssetCollection +from tilebox.datasets.assets.stac.authentication import SignedURLFlow as SignedURLFlowModel +from tilebox.datasets.assets.stac.metadata import ElectroOpticalProperties, RasterProperties +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import RasterProperties as RasterPropertiesProto +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Asset, AssetAccessProfile, AssetLocation, Assets, Band +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import ( + Authentication, + AuthenticationFlow, + AuthenticationParameter, + SignedURLFlow, +) +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import ( + AuthenticationScheme as AuthenticationSchemeProto, +) +from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage, StorageScheme + +TESTDATA = Path(__file__).parent / "testdata" + + +def _scalar(**variables: object) -> xr.Dataset: + return xr.Dataset({name: xr.DataArray(np.array(value, dtype=object)) for name, value in variables.items()}) + + +def _read_real_fixture() -> tuple[Assets, Storage]: + """Read real Sentinel-2 metadata captured from the public Tilebox dataset. + + These raw protobuf messages are the first result returned on 2026-07-29 by + ``tilebox dataset query open_data.aws_earth.sentinel2 --last 10Y --limit 1``. + The datapoint's STAC ID is ``S2A_T33TWM_20161102T100149_L2A``. It has no + authentication metadata, so no synthetic Authentication fixture is stored. + """ + assets = Assets.FromString((TESTDATA / "sentinel2_assets.binpb").read_bytes()) + storage = Storage.FromString((TESTDATA / "sentinel2_storage.binpb").read_bytes()) + return assets, storage + + +def test_real_sentinel2_fixture_decodes_to_self_contained_assets() -> None: + assets_message, storage = _read_real_fixture() + assets = AssetCollection.from_datapoint(_scalar(catalog=assets_message, storage=storage)) + + assert len(assets) == 23 + red = assets["red"] + assert red.primary.href.endswith("/S2A_T33TWM_20161102T100149_L2A/B04.tif") + assert red.alternates["s3"].href == ( + "s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/33/T/WM/2016/11/S2A_T33TWM_20161102T100149_L2A/B04.tif" + ) + assert red.alternates["s3"].storage_schemes["earth-search"].key == "earth-search" + assert red.alternates["s3"].storage_schemes["earth-search"].region == "us-west-2" + assert red.media_type == "image/tiff; application=geotiff; profile=cloud-optimized" + assert red.roles == frozenset({"data", "reflectance"}) + assert red.raster == RasterProperties(sampling="unspecified", scale=0.0001, offset=-0.1, spatial_resolution=10) + assert red.bands[0].data_type == "uint16" + assert red.bands[0].nodata == 0 + assert red.bands[0].raster == red.raster + assert red.bands[0].electro_optical == ElectroOpticalProperties( + common_name="red", + center_wavelength=0.665, + full_width_half_max=0.038, + solar_illumination=None, + ) + + +def test_discovery_uses_descriptor_type_and_supports_overrides() -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="https://example.com/")], + assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + ) + datapoint = _scalar(not_named_assets=root, other=root) + with pytest.raises(ValueError, match=r"ambiguous datasets\.stac\.v1\.Assets"): + AssetCollection.from_datapoint(datapoint) + + decoded = AssetCollection.from_datapoint(datapoint, fields={"assets": "other"}) + assert decoded["data"].primary.href == "https://example.com/data.tif" + with pytest.raises(ValueError, match="not present"): + AssetCollection.from_datapoint(datapoint, fields={"assets": "missing"}) + + +def test_context_ambiguity_and_explicit_override() -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["main"])], + assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + ) + first = Storage(schemes={"main": StorageScheme(region="eu-west-1")}) + second = Storage(schemes={"main": StorageScheme(region="us-west-2")}) + datapoint = _scalar(root=root, first=first, second=second) + with pytest.raises(ValueError, match=r"ambiguous datasets\.stac\.v1\.Storage"): + AssetCollection.from_datapoint(datapoint) + decoded = AssetCollection.from_datapoint(datapoint, fields={"storage": "second"}) + assert decoded["data"].primary.storage_schemes["main"].region == "us-west-2" + + +def test_storage_and_authentication_extensions_use_immutable_mappings() -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["main"], auth_refs=["signed"])], + assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + ) + properties = Struct() + properties.update({"endpoint": "https://example.com", "options": ["one", "two"]}) + storage = Storage( + schemes={ + "main": StorageScheme( + known_type="KNOWN_STORAGE_TYPE_CUSTOM_S3", + bucket="bucket", + account="account", + additional_properties=properties.fields, + ) + } + ) + schema = Struct() + schema.update({"type": "string", "enum": ["one", "two"]}) + authentication = Authentication( + schemes={ + "signed": AuthenticationSchemeProto( + flows=[ + AuthenticationFlow( + key="authorizationCode", + signed_url=SignedURLFlow( + parameters={"token": AuthenticationParameter(required=True, schema=schema)} + ), + ) + ] + ) + } + ) + + location = AssetCollection.from_datapoint(_scalar(root=root, storage=storage, authentication=authentication))[ + "data" + ].primary + scheme = location.storage_schemes["main"] + assert scheme.bucket == "bucket" + assert scheme.account == "account" + assert scheme.additional_properties == {"endpoint": "https://example.com", "options": ("one", "two")} + flow = location.authentication_schemes["signed"].flows["authorizationCode"] + assert isinstance(flow, SignedURLFlowModel) + assert flow.parameters["token"].schema == {"type": "string", "enum": ("one", "two")} + with pytest.raises(TypeError): + scheme.additional_properties["new"] = True # type: ignore[index] + + +def test_alternate_href_absent_empty_and_nonempty_are_distinct() -> None: + root = Assets( + access_profiles=[ + AssetAccessProfile(base_href="https://primary/"), + AssetAccessProfile(alternate_key="inherited", base_href="https://inherited/"), + AssetAccessProfile(alternate_key="empty", base_href="https://empty/"), + AssetAccessProfile(alternate_key="other", base_href="https://other/"), + ], + assets=[ + Asset( + key="data", + primary=AssetLocation(access_profile_index=0, href="file.tif"), + alternates=[ + AssetLocation(access_profile_index=1), + AssetLocation(access_profile_index=2, href=""), + AssetLocation(access_profile_index=3, href="different.tif"), + ], + ) + ], + ) + decoded = AssetCollection.from_datapoint(_scalar(root=root))["data"] + assert decoded.alternates["inherited"].href == "https://inherited/file.tif" + assert decoded.alternates["empty"].href == "https://empty/" + assert decoded.alternates["other"].href == "https://other/different.tif" + + +def test_band_inheritance_is_field_by_field() -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="file:///")], + band_profiles=[Band(name="red", raster=RasterPropertiesProto(scale=2))], + assets=[ + Asset( + key="data", + primary=AssetLocation(access_profile_index=0, href="tmp/data.tif"), + band_profile_indices=[0], + data_type="DATA_TYPE_UINT16", + nodata=-1, + raster=RasterPropertiesProto(offset=3, spatial_resolution=10), + ) + ], + ) + band = AssetCollection.from_datapoint(_scalar(root=root))["data"].bands[0] + assert band.data_type == "uint16" + assert band.nodata == -1 + assert band.raster == RasterProperties(sampling="unspecified", scale=2, offset=3, spatial_resolution=10) + + +def test_missing_referenced_context_is_rejected() -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["missing"])], + assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + ) + with pytest.raises(ValueError, match="missing storage scheme 'missing'"): + AssetCollection.from_datapoint(_scalar(root=root)) + + +def test_iter_datapoints_and_multi_datapoint_error() -> None: + data = xr.Dataset({"value": ("time", [1, 2, 3])}) + assert [datapoint.value.item() for datapoint in iter_datapoints(data)] == [1, 2, 3] + with pytest.raises(ValueError, match=r"(?s)received 3.*dimension 'time'"): + AssetCollection.from_datapoint(data) + with pytest.raises(ValueError, match=r"'sample'.*not present"): + list(iter_datapoints(data, dimension="sample")) + + +def test_decoded_collections_are_deeply_immutable() -> None: + assets_message, storage = _read_real_fixture() + assets = AssetCollection.from_datapoint(_scalar(root=assets_message, storage=storage)) + with pytest.raises(TypeError): + assets._assets["new"] = assets["red"] # type: ignore[index] + with pytest.raises(TypeError): + assets["red"].alternates["new"] = assets["red"].primary # type: ignore[index] diff --git a/tilebox-datasets/tests/testdata/sentinel2_assets.binpb b/tilebox-datasets/tests/testdata/sentinel2_assets.binpb new file mode 100644 index 0000000..de8b566 --- /dev/null +++ b/tilebox-datasets/tests/testdata/sentinel2_assets.binpb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:746da8992974fd466b363c5cad42bfa01b19dd36c6c9525e278424a916a45370 +size 5076 diff --git a/tilebox-datasets/tests/testdata/sentinel2_storage.binpb b/tilebox-datasets/tests/testdata/sentinel2_storage.binpb new file mode 100644 index 0000000..9063965 --- /dev/null +++ b/tilebox-datasets/tests/testdata/sentinel2_storage.binpb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21063f8dbb149af03a0950d32d134972a8a253a817efd28abba533c9a3ebd536 +size 77 diff --git a/tilebox-datasets/tilebox/datasets/__init__.py b/tilebox-datasets/tilebox/datasets/__init__.py index 7ee27a0..dc0cae2 100644 --- a/tilebox-datasets/tilebox/datasets/__init__.py +++ b/tilebox-datasets/tilebox/datasets/__init__.py @@ -6,11 +6,20 @@ if TYPE_CHECKING: from tilebox.datasets.aio.timeseries import TimeseriesCollection, TimeseriesDataset + from tilebox.datasets.datapoints import iter_datapoints from tilebox.datasets.query import field from tilebox.datasets.sync.client import Client from tilebox.datasets.sync.dataset import CollectionClient, DatasetClient -__all__ = ["Client", "CollectionClient", "DatasetClient", "TimeseriesCollection", "TimeseriesDataset", "field"] +__all__ = [ + "Client", + "CollectionClient", + "DatasetClient", + "TimeseriesCollection", + "TimeseriesDataset", + "field", + "iter_datapoints", +] def __getattr__(name: str) -> Any: @@ -42,6 +51,10 @@ def __getattr__(name: str) -> Any: from tilebox.datasets.aio.timeseries import TimeseriesDataset # noqa: PLC0415 value = TimeseriesDataset + case "iter_datapoints": + from tilebox.datasets.datapoints import iter_datapoints # noqa: PLC0415 + + value = iter_datapoints case _: raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/tilebox-datasets/tilebox/datasets/assets/__init__.py b/tilebox-datasets/tilebox/datasets/assets/__init__.py new file mode 100644 index 0000000..b08b746 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/__init__.py @@ -0,0 +1,3 @@ +from tilebox.datasets.assets.assets import AssetCollection + +__all__ = ["AssetCollection"] diff --git a/tilebox-datasets/tilebox/datasets/assets/assets.py b/tilebox-datasets/tilebox/datasets/assets/assets.py new file mode 100644 index 0000000..7193e3c --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/assets.py @@ -0,0 +1,399 @@ +"""Immutable asset metadata decoded from Tilebox datapoints.""" + +from collections.abc import Iterator, Mapping +from dataclasses import dataclass, fields, replace +from types import MappingProxyType +from typing import Any, TypeVar, cast + +import numpy as np +import xarray as xr +from google.protobuf.message import Message +from typing_extensions import NotRequired, TypedDict + +from tilebox.datasets.assets.converters import _enum_name, _has_field, converters +from tilebox.datasets.assets.stac.authentication import AuthenticationScheme +from tilebox.datasets.assets.stac.metadata import ( + ClassificationClass, + ElectroOpticalProperties, + File, + Projection, + RasterProperties, + Statistics, + View, +) +from tilebox.datasets.assets.stac.product import ProductProperties +from tilebox.datasets.assets.stac.sar import SarProperties +from tilebox.datasets.assets.stac.satellite import SatelliteProperties +from tilebox.datasets.assets.stac.storage import StorageScheme +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Band as BandProto +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication +from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage + +_ASSETS_MESSAGE_NAME = Assets.DESCRIPTOR.full_name +_STORAGE_MESSAGE_NAME = Storage.DESCRIPTOR.full_name +_AUTHENTICATION_MESSAGE_NAME = Authentication.DESCRIPTOR.full_name +_EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({}) +_MessageT = TypeVar("_MessageT", bound=Message) + + +class AssetFieldOverrides(TypedDict): + """Optional xarray variable names for ambiguous asset metadata fields.""" + + assets: NotRequired[str] + storage: NotRequired[str] + authentication: NotRequired[str] + + +@dataclass(frozen=True, slots=True) +class Band: + """Band metadata inspired by the STAC asset and extension specifications.""" + + name: str | None = None + description: str | None = None + data_type: str = "unspecified" + nodata: float | None = None + unit: str | None = None + electro_optical: ElectroOpticalProperties | None = None + raster: RasterProperties | None = None + classes: tuple[ClassificationClass, ...] = () + sar: SarProperties | None = None + + +@dataclass(frozen=True, slots=True) +class AssetLocation: + """A resolved asset URL and its applicable access schemes.""" + + href: str + storage_schemes: Mapping[str, StorageScheme] = _EMPTY_MAPPING + authentication_schemes: Mapping[str, AuthenticationScheme] = _EMPTY_MAPPING + + +@dataclass(frozen=True, slots=True) +class Asset: + """An immutable asset inspired by the STAC Asset Object specification. + + See https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-object. + """ + + key: str + primary: AssetLocation + alternates: Mapping[str, AssetLocation] = _EMPTY_MAPPING + media_type: str | None = None + title: str | None = None + description: str | None = None + roles: frozenset[str] = frozenset() + gsd: float | None = None + bands: tuple[Band, ...] = () + data_type: str = "unspecified" + nodata: float | None = None + statistics: Statistics | None = None + unit: str | None = None + electro_optical: ElectroOpticalProperties | None = None + raster: RasterProperties | None = None + projection: Projection | None = None + view: View | None = None + classes: tuple[ClassificationClass, ...] = () + file: File | None = None + sar: SarProperties | None = None + satellite: SatelliteProperties | None = None + product: ProductProperties | None = None + + +@dataclass(frozen=True, slots=True) +class AssetCollection(Mapping[str, Asset]): + """Immutable assets belonging to exactly one dataset datapoint.""" + + _assets: Mapping[str, Asset] + + @classmethod + def from_datapoint( + cls, + datapoint: xr.Dataset, + *, + fields: AssetFieldOverrides | None = None, + ) -> "AssetCollection": + """Decode the assets attached to one Tilebox dataset datapoint. + + Args: + datapoint: A scalar :class:`xarray.Dataset`, typically one result selected + from :meth:`tilebox.datasets.Dataset.query`. For a query containing + multiple datapoints, iterate with + :func:`tilebox.datasets.iter_datapoints` first. + fields: Optional xarray variable names for the ``assets``, ``storage``, + and ``authentication`` protobuf messages. Fields are normally + discovered from their generated protobuf types; specify only the + entries needed to resolve ambiguous datasets. + + Returns: + An immutable, mapping-like collection keyed by asset name. + + Raises: + TypeError: If ``datapoint`` is not an xarray dataset. + ValueError: If the input is not scalar or its protobuf fields cannot be + discovered or decoded unambiguously. + """ + if not isinstance(datapoint, xr.Dataset): + raise TypeError("datapoint must be an xarray.Dataset") + if datapoint.sizes.get("time", 0) > 1: + dimension = "time" + size = datapoint.sizes[dimension] + raise ValueError( + f"AssetCollection.from_datapoint() expects one datapoint, but received {size} " + f"along dimension {dimension!r}.\n\n" + f"Select one explicitly:\n\n assets = AssetCollection.from_datapoint(data.isel({dimension}=0))\n\n" + "Or iterate over all datapoints:\n\n" + " for datapoint in iter_datapoints(data):\n" + " assets = AssetCollection.from_datapoint(datapoint)" + ) + overrides = fields or {} + root = _discover_xarray_message(datapoint, Assets, overrides.get("assets")) + if root is None: + raise ValueError(f"no populated {_ASSETS_MESSAGE_NAME} field found in datapoint") + storage = _discover_xarray_message(datapoint, Storage, overrides.get("storage")) + authentication = _discover_xarray_message(datapoint, Authentication, overrides.get("authentication")) + return cls(_decode_assets(root, storage, authentication)) + + def __getitem__(self, key: str) -> Asset: + return self._assets[key] + + def __iter__(self) -> Iterator[str]: + return iter(self._assets) + + def __len__(self) -> int: + return len(self._assets) + + +_MEDIA_TYPES = { + "geojson": "application/geo+json", + "json": "application/json", + "cloud_optimized_geotiff": "image/tiff; application=geotiff; profile=cloud-optimized", + "jpeg_2000": "image/jp2", + "jpeg": "image/jpeg", + "png": "image/png", + "application_xml": "application/xml", + "zip": "application/zip", + "directory": "inode/directory", + "geotiff": "image/tiff; application=geotiff", + "tiff": "image/tiff", + "hdf5": "application/x-hdf5", + "hdf": "application/x-hdf", + "netcdf": "application/x-netcdf", + "zarr_v2": "application/vnd+zarr", + "zarr_v3": "application/vnd+zarr", + "parquet": "application/vnd.apache.parquet", + "geopackage": "application/geopackage+sqlite3", + "copc": "application/vnd.laszip+copc", + "html": "text/html", + "text": "text/plain", + "text_xml": "text/xml", + "flatgeobuf": "application/vnd.flatgeobuf", + "pmtiles": "application/vnd.pmtiles", + "nitf": "image/nitf", + "octet_stream": "application/octet-stream", +} + + +def _convert_storage(key: str, message: Message) -> StorageScheme: + return replace(converters.convert(message), key=key) + + +def _convert_authentication(key: str, message: Message) -> AuthenticationScheme: + return replace(converters.convert(message), key=key) + + +def _resolve_refs(registry: Mapping[str, Any], refs: Any, kind: str) -> Mapping[str, Any]: + resolved = {} + for key in refs: + if key not in registry: + raise ValueError(f"asset references missing {kind} scheme {key!r}") from None + resolved[key] = registry[key] + return MappingProxyType(resolved) + + +def _location( + source: Any, + profiles: Any, + storage: Mapping[str, StorageScheme], + authentication: Mapping[str, AuthenticationScheme], + *, + fallback_href: str | None = None, +) -> tuple[str, AssetLocation]: + """Resolve a compact protobuf location through its shared access profile. + + Access profiles deduplicate URL prefixes and scheme references in protobuf. This + expands one profile index into a complete URL and keyed scheme mappings. Alternate + locations may inherit the primary location's relative href through ``fallback_href``. + """ + if not _has_field(source, "access_profile_index"): + raise ValueError("asset location is missing its access profile index") + index = source.access_profile_index + if index >= len(profiles): + raise ValueError(f"invalid asset access profile index: {index}") + profile = profiles[index] + suffix = source.href if _has_field(source, "href") else fallback_href + if suffix is None: + raise ValueError("asset location has no href and no primary href to inherit") + return profile.alternate_key, AssetLocation( + href=profile.base_href + suffix, + storage_schemes=_resolve_refs(storage, profile.storage_refs, "storage"), + authentication_schemes=_resolve_refs(authentication, profile.auth_refs, "authentication"), + ) + + +def _inherit(child: Any, parent: Any) -> Any: + """Fill unspecified band metadata from its asset-level defaults. + + STAC permits common metadata to be attached to the asset while individual bands + override only selected fields. This creates the effective immutable band model. + """ + if child is None: + return parent + if parent is None: + return child + values = {} + for item in fields(child): + value = getattr(child, item.name) + if value is None or value in ("unspecified", ()): + values[item.name] = getattr(parent, item.name) + return replace(child, **values) + + +def _decode_band(source: BandProto, asset: Asset) -> Band: + band = Band( + name=source.name if _has_field(source, "name") else None, + description=source.description if _has_field(source, "description") else None, + data_type=_enum_name(source.DESCRIPTOR.fields_by_name["data_type"], source.data_type), + nodata=source.nodata if _has_field(source, "nodata") else None, + unit=source.unit if _has_field(source, "unit") else None, + electro_optical=converters.convert(source.eo) if source.HasField("eo") else None, + raster=converters.convert(source.raster) if source.HasField("raster") else None, + classes=tuple(converters.convert(item) for item in source.classes), + sar=converters.convert(source.sar) if source.HasField("sar") else None, + ) + return replace( + band, + data_type=asset.data_type if band.data_type == "unspecified" else band.data_type, + nodata=asset.nodata if band.nodata is None else band.nodata, + unit=asset.unit if band.unit is None else band.unit, + electro_optical=_inherit(band.electro_optical, asset.electro_optical), + raster=_inherit(band.raster, asset.raster), + classes=asset.classes if not band.classes else band.classes, + sar=_inherit(band.sar, asset.sar), + ) + + +def _media_type(message: Message) -> str | None: + if _has_field(message, "custom"): + return message.custom # type: ignore[attr-defined] + known = _enum_name(message.DESCRIPTOR.fields_by_name["known"], message.known) # type: ignore[attr-defined] + return None if known == "unspecified" else _MEDIA_TYPES[known] + + +def _decode_assets( + root: Assets, storage_message: Storage | None, authentication_message: Authentication | None +) -> Mapping[str, Asset]: + storage = ( + {key: _convert_storage(key, value) for key, value in storage_message.schemes.items()} + if storage_message is not None + else {} + ) + authentication = ( + {key: _convert_authentication(key, value) for key, value in authentication_message.schemes.items()} + if authentication_message is not None + else {} + ) + result: dict[str, Asset] = {} + for source in root.assets: + if not source.HasField("primary"): + raise ValueError(f"asset {source.key!r} has no primary location") + primary_suffix = source.primary.href if _has_field(source.primary, "href") else None + _, primary = _location(source.primary, root.access_profiles, storage, authentication) + alternates: dict[str, AssetLocation] = {} + for alternate_source in source.alternates: + alternate_key, alternate = _location( + alternate_source, + root.access_profiles, + storage, + authentication, + fallback_href=primary_suffix, + ) + if not alternate_key: + raise ValueError(f"asset {source.key!r} has an alternate with an empty key") + if alternate_key in alternates: + raise ValueError(f"asset {source.key!r} has duplicate alternate key {alternate_key!r}") + alternates[alternate_key] = alternate + role_field = source.DESCRIPTOR.fields_by_name["roles"] + asset = Asset( + key=source.key, + primary=primary, + alternates=MappingProxyType(alternates), + media_type=_media_type(source.media_type) if source.HasField("media_type") else None, + title=source.title if _has_field(source, "title") else None, + description=source.description if _has_field(source, "description") else None, + roles=frozenset([*(_enum_name(role_field, role) for role in source.roles), *source.custom_roles]), + gsd=source.gsd if _has_field(source, "gsd") else None, + data_type=_enum_name(source.DESCRIPTOR.fields_by_name["data_type"], source.data_type), + nodata=source.nodata if _has_field(source, "nodata") else None, + statistics=converters.convert(source.statistics) if source.HasField("statistics") else None, + unit=source.unit if _has_field(source, "unit") else None, + electro_optical=converters.convert(source.eo) if source.HasField("eo") else None, + raster=converters.convert(source.raster) if source.HasField("raster") else None, + projection=converters.convert(source.projection) if source.HasField("projection") else None, + view=converters.convert(source.view) if source.HasField("view") else None, + classes=tuple(converters.convert(item) for item in source.classes), + file=converters.convert(source.file) if source.HasField("file") else None, + sar=converters.convert(source.sar) if source.HasField("sar") else None, + satellite=converters.convert(source.satellite) if source.HasField("satellite") else None, + product=converters.convert(source.product) if source.HasField("product") else None, + ) + bands = [] + for index in source.band_profile_indices: + if index >= len(root.band_profiles): + raise ValueError(f"invalid band profile index: {index}") + bands.append(_decode_band(root.band_profiles[index], asset)) + asset = replace(asset, bands=tuple(bands)) + if asset.key in result: + raise ValueError(f"duplicate asset key: {asset.key!r}") + result[asset.key] = asset + return MappingProxyType(result) + + +def _message_values(data: Any) -> list[Message]: + values = np.asarray(data.values, dtype=object).reshape(-1) + return [value for value in values if isinstance(value, Message)] + + +def _discover_xarray_message( + datapoint: xr.Dataset, + message_type: type[_MessageT], + variable_override: str | None, +) -> _MessageT | None: + """Find one protobuf message in xarray variables by generated class. + + ``variable_override`` selects an exact xarray variable when descriptor discovery + would be ambiguous. The descriptor full name is a fallback for messages created + from an equivalent dynamic descriptor pool, whose generated Python class differs. + """ + message_name = message_type.DESCRIPTOR.full_name + + def matches(value: Message) -> bool: + return isinstance(value, message_type) or value.DESCRIPTOR.full_name == message_name + + if variable_override is not None: + if variable_override not in datapoint.variables: + raise ValueError(f"context field {variable_override!r} is not present in the datapoint") + values = [value for value in _message_values(datapoint[variable_override]) if matches(value)] + if len(values) != 1: + raise ValueError(f"field {variable_override!r} does not contain exactly one {message_name} message") + return cast(_MessageT, values[0]) + candidates = [] + for name, data in datapoint.variables.items(): + values = [value for value in _message_values(data) if matches(value)] + if values: + if len(values) != 1: + raise ValueError(f"field {name!r} contains multiple {message_name} messages") + candidates.append((name, values[0])) + if len(candidates) > 1: + raise ValueError(f"ambiguous {message_name} fields: {', '.join(name for name, _ in candidates)}") + return cast(_MessageT, candidates[0][1]) if candidates else None diff --git a/tilebox-datasets/tilebox/datasets/assets/converters.py b/tilebox-datasets/tilebox/datasets/assets/converters.py new file mode 100644 index 0000000..9d9195c --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/converters.py @@ -0,0 +1,125 @@ +"""Decorator-based conversion of protobuf messages to immutable Python values.""" + +from collections.abc import Callable, Mapping +from dataclasses import dataclass, fields +from datetime import timezone +from types import MappingProxyType +from typing import Any + +from google.protobuf.descriptor import FieldDescriptor +from google.protobuf.json_format import MessageToDict +from google.protobuf.message import Message + + +@dataclass(frozen=True, slots=True) +class _Conversion: + python_type: type[Any] + rename_fields: Mapping[str, str] + field_converters: Mapping[str, Callable[[Any], Any]] + + +class ProtobufConverterRegistry: + """Registry mapping generated protobuf classes to immutable Python dataclasses.""" + + def __init__(self) -> None: + self._conversions: dict[type[Message], _Conversion] = {} + self._conversions_by_name: dict[str, _Conversion] = {} + + def register( + self, + protobuf_type: type[Message], + *, + rename_fields: Mapping[str, str] | None = None, + field_converters: Mapping[str, Callable[[Any], Any]] | None = None, + ) -> Any: + """Register a decorated dataclass as the conversion target for a protobuf type.""" + + def decorator(python_type: type[Any]) -> type[Any]: + conversion = _Conversion( + python_type, + MappingProxyType(dict(rename_fields or {})), + MappingProxyType(dict(field_converters or {})), + ) + self._conversions[protobuf_type] = conversion + self._conversions_by_name[protobuf_type.DESCRIPTOR.full_name] = conversion + return python_type + + return decorator + + def convert(self, message: Message) -> Any: # noqa: C901, PLR0912 + """Recursively convert a registered protobuf message.""" + full_name = message.DESCRIPTOR.full_name + if full_name == "google.protobuf.Timestamp": + return message.ToDatetime(tzinfo=timezone.utc) # type: ignore[attr-defined] + if full_name in {"google.protobuf.Struct", "google.protobuf.Value"}: + return _freeze(MessageToDict(message)) + + conversion = self._conversions.get(type(message)) + if conversion is None: + conversion = next( + ( + registered + for protobuf_type, registered in self._conversions.items() + if isinstance(message, protobuf_type) + ), + None, + ) + if conversion is None: + conversion = self._conversions_by_name.get(full_name) + if conversion is None: + raise TypeError(f"no Python converter is registered for {full_name}") + + python_fields = {item.name for item in fields(conversion.python_type)} + values: dict[str, Any] = {} + for field in message.DESCRIPTOR.fields: + name = conversion.rename_fields.get(field.name, field.name) + if name not in python_fields: + continue + value = getattr(message, field.name) + field_converter = conversion.field_converters.get(field.name) + if field_converter is not None: + values[name] = field_converter(value) + continue + if field.is_repeated: + if field.message_type and field.message_type.GetOptions().map_entry: + values[name] = MappingProxyType( + {key: self.convert(item) if isinstance(item, Message) else item for key, item in value.items()} + ) + elif field.type == FieldDescriptor.TYPE_ENUM: + values[name] = tuple(_enum_name(field, item) for item in value) + else: + values[name] = tuple(self.convert(item) if isinstance(item, Message) else item for item in value) + continue + if not _has_field(message, field.name) and field.type != FieldDescriptor.TYPE_ENUM: + values.setdefault(name, None) + elif field.type == FieldDescriptor.TYPE_MESSAGE: + values[name] = self.convert(value) + elif field.type == FieldDescriptor.TYPE_ENUM: + values[name] = _enum_name(field, value) + else: + values[name] = value + return conversion.python_type(**values) + + +def _has_field(message: Message, name: str) -> bool: + field = message.DESCRIPTOR.fields_by_name[name] + if field.has_presence: + return message.HasField(name) + return any(present is field for present, _ in message.ListFields()) + + +def _enum_name(field: Any, value: int) -> str: + name = field.enum_type.values_by_number[value].name + prefix = field.enum_type.values[0].name.removesuffix("UNSPECIFIED") + return name.removeprefix(prefix).lower() + + +def _freeze(value: Any) -> Any: + if isinstance(value, dict): + return MappingProxyType({key: _freeze(item) for key, item in value.items()}) + if isinstance(value, list): + return tuple(_freeze(item) for item in value) + return value + + +converters = ProtobufConverterRegistry() diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/__init__.py b/tilebox-datasets/tilebox/datasets/assets/stac/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py b/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py new file mode 100644 index 0000000..4fe0909 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py @@ -0,0 +1,78 @@ +"""Immutable models inspired by the STAC authentication extension. + +See https://github.com/stac-extensions/authentication. +""" + +from collections.abc import Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import AuthenticationParameter as ParameterProto +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import AuthenticationScheme as SchemeProto +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import OAuth2Flow as OAuth2FlowProto +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import SignedURLFlow as SignedURLFlowProto + + +@converters.register(OAuth2FlowProto) +@dataclass(frozen=True, slots=True) +class OAuth2Flow: + authorization_url: str | None = None + token_url: str | None = None + scopes: Mapping[str, str] = MappingProxyType({}) + refresh_url: str | None = None + + +@converters.register(ParameterProto) +@dataclass(frozen=True, slots=True) +class AuthenticationParameter: + location: str | None = None + required: bool | None = None + description: str | None = None + schema: Mapping[str, Any] = MappingProxyType({}) + + +@converters.register(SignedURLFlowProto) +@dataclass(frozen=True, slots=True) +class SignedURLFlow: + method: str | None = None + authorization_api: str | None = None + parameters: Mapping[str, AuthenticationParameter] = MappingProxyType({}) + response_field: str | None = None + + +def _convert_flows(flows: Any) -> Mapping[str, OAuth2Flow] | Mapping[str, SignedURLFlow]: + oauth2_flows: dict[str, OAuth2Flow] = {} + signed_url_flows: dict[str, SignedURLFlow] = {} + for flow in flows: + choices = [name for name in ("oauth2", "signed_url") if flow.HasField(name)] + if len(choices) != 1: + raise ValueError(f"authentication flow {flow.key!r} must define exactly one flow type") + if flow.key in oauth2_flows or flow.key in signed_url_flows: + raise ValueError(f"duplicate authentication flow key: {flow.key!r}") + converted = converters.convert(getattr(flow, choices[0])) + if isinstance(converted, OAuth2Flow): + oauth2_flows[flow.key] = converted + else: + signed_url_flows[flow.key] = converted + if oauth2_flows and signed_url_flows: + raise ValueError("authentication scheme cannot mix OAuth2 and signed URL flows") + return MappingProxyType(oauth2_flows) if oauth2_flows else MappingProxyType(signed_url_flows) + + +@converters.register( + SchemeProto, + rename_fields={"known_type": "type", "custom_type": "type"}, + field_converters={"flows": _convert_flows}, +) +@dataclass(frozen=True, slots=True) +class AuthenticationScheme: + key: str = "" + type: str | None = None + description: str | None = None + name: str | None = None + location: str | None = None + scheme: str | None = None + flows: Mapping[str, OAuth2Flow] | Mapping[str, SignedURLFlow] = MappingProxyType({}) + open_id_connect_url: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py b/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py new file mode 100644 index 0000000..b1af636 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py @@ -0,0 +1,80 @@ +"""Immutable models inspired by STAC asset, EO, raster, projection, view, classification, and file metadata specs. + +See https://github.com/radiantearth/stac-spec and https://stac-extensions.github.io/. +""" + +from dataclasses import dataclass + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import ( + ClassificationClass as ClassificationClassProto, +) +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import EOProperties as EOPropertiesProto +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import File as FileProto +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import Projection as ProjectionProto +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import RasterProperties as RasterPropertiesProto +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import View as ViewProto +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Statistics as StatisticsProto + + +@converters.register(StatisticsProto) +@dataclass(frozen=True, slots=True) +class Statistics: + minimum: float | None = None + maximum: float | None = None + + +@converters.register(EOPropertiesProto) +@dataclass(frozen=True, slots=True) +class ElectroOpticalProperties: + common_name: str = "unspecified" + center_wavelength: float | None = None + full_width_half_max: float | None = None + solar_illumination: float | None = None + + +@converters.register(RasterPropertiesProto) +@dataclass(frozen=True, slots=True) +class RasterProperties: + sampling: str = "unspecified" + scale: float | None = None + offset: float | None = None + spatial_resolution: float | None = None + + +@converters.register(ClassificationClassProto) +@dataclass(frozen=True, slots=True) +class ClassificationClass: + value: int | None = None + description: str | None = None + name: str | None = None + title: str | None = None + color_hint: str | None = None + nodata: bool | None = None + percentage: float | None = None + count: int | None = None + + +@converters.register(ProjectionProto) +@dataclass(frozen=True, slots=True) +class Projection: + # STAC permits 2D/3D bboxes and six- or nine-element affine transforms. + bbox: tuple[float, ...] = () + shape: tuple[int, ...] = () + transform: tuple[float, ...] = () + code: str | None = None + + +@converters.register(ViewProto) +@dataclass(frozen=True, slots=True) +class View: + incidence_angle: float | None = None + azimuth: float | None = None + + +@converters.register(FileProto) +@dataclass(frozen=True, slots=True) +class File: + checksum: bytes | None = None + size: int | None = None + local_path: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/product.py b/tilebox-datasets/tilebox/datasets/assets/stac/product.py new file mode 100644 index 0000000..93eda12 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/product.py @@ -0,0 +1,18 @@ +"""Immutable model inspired by the STAC product extension. + +See https://github.com/stac-extensions/product. +""" + +from dataclasses import dataclass + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.product_pb2 import ProductProperties as ProductPropertiesProto + + +@converters.register(ProductPropertiesProto) +@dataclass(frozen=True, slots=True) +class ProductProperties: + type: str | None = None + timeliness: str | None = None + timeliness_category: str | None = None + acquisition_type: str = "unspecified" diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/sar.py b/tilebox-datasets/tilebox/datasets/assets/stac/sar.py new file mode 100644 index 0000000..a01a1cf --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/sar.py @@ -0,0 +1,29 @@ +"""Immutable model inspired by the STAC SAR extension. + +See https://github.com/stac-extensions/sar. +""" + +from dataclasses import dataclass + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.sar_pb2 import SARProperties as SARPropertiesProto + + +@converters.register(SARPropertiesProto) +@dataclass(frozen=True, slots=True) +class SarProperties: + polarizations: tuple[str, ...] = () + instrument_mode: str | None = None + frequency_band: str = "unspecified" + center_frequency: float | None = None + bandwidth: float | None = None + resolution_range: float | None = None + resolution_azimuth: float | None = None + pixel_spacing_range: float | None = None + pixel_spacing_azimuth: float | None = None + looks_range: int | None = None + looks_azimuth: int | None = None + looks_equivalent_number: float | None = None + observation_direction: str = "unspecified" + relative_burst: int | None = None + beam_ids: tuple[str, ...] = () diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py b/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py new file mode 100644 index 0000000..619aee6 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py @@ -0,0 +1,31 @@ +"""Immutable models inspired by the STAC satellite extension. + +See https://github.com/stac-extensions/sat. +""" + +import datetime as dt +from dataclasses import dataclass + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.satellite_pb2 import SatelliteOrbitStateVector as OrbitVectorProto +from tilebox.datasets.datasets.stac.v1.satellite_pb2 import SatelliteProperties as SatellitePropertiesProto + + +@converters.register(OrbitVectorProto) +@dataclass(frozen=True, slots=True) +class SatelliteOrbitStateVector: + datetime: dt.datetime | None = None + values: tuple[float, ...] = () + + +@converters.register(SatellitePropertiesProto) +@dataclass(frozen=True, slots=True) +class SatelliteProperties: + platform_international_designator: str | None = None + orbit_state: str = "unspecified" + absolute_orbit: int | None = None + relative_orbit: int | None = None + orbit_cycle: int | None = None + orbit_state_vectors: tuple[SatelliteOrbitStateVector, ...] = () + anx_datetime: dt.datetime | None = None + acquisition_station: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/storage.py b/tilebox-datasets/tilebox/datasets/assets/stac/storage.py new file mode 100644 index 0000000..cf0ade3 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/assets/stac/storage.py @@ -0,0 +1,31 @@ +"""Immutable model inspired by the STAC storage extension. + +See https://github.com/stac-extensions/storage. +""" + +from collections.abc import Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + +from tilebox.datasets.assets.converters import converters +from tilebox.datasets.datasets.stac.v1.storage_pb2 import StorageScheme as StorageSchemeProto + + +@converters.register( + StorageSchemeProto, + rename_fields={"known_type": "type", "custom_type": "type"}, +) +@dataclass(frozen=True, slots=True) +class StorageScheme: + key: str = "" + type: str | None = None + platform: str | None = None + title: str | None = None + description: str | None = None + region: str | None = None + requester_pays: bool | None = None + storage_class: str | None = None + bucket: str | None = None + account: str | None = None + additional_properties: Mapping[str, Any] = MappingProxyType({}) diff --git a/tilebox-datasets/tilebox/datasets/datapoints.py b/tilebox-datasets/tilebox/datasets/datapoints.py new file mode 100644 index 0000000..d4c6112 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datapoints.py @@ -0,0 +1,13 @@ +"""Utilities for working with xarray datapoints returned by dataset queries.""" + +from collections.abc import Iterator + +import xarray as xr + + +def iter_datapoints(data: xr.Dataset, *, dimension: str = "time") -> Iterator[xr.Dataset]: + """Yield scalar datapoints from an xarray query result.""" + if dimension not in data.sizes: + raise ValueError(f"datapoint dimension {dimension!r} is not present in the dataset") + for index in range(data.sizes[dimension]): + yield data.isel({dimension: index}) diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.py index 49212bb..2d7a6ca 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.py @@ -25,7 +25,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%datasets/stac/v1/authentication.proto\x12\x10\x64\x61tasets.stac.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xbd\x01\n\x0e\x41uthentication\x12G\n\x07schemes\x18\x01 \x03(\x0b\x32-.datasets.stac.v1.Authentication.SchemesEntryR\x07schemes\x1a\x62\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12<\n\x05value\x18\x02 \x01(\x0b\x32&.datasets.stac.v1.AuthenticationSchemeR\x05value:\x02\x38\x01\"\x81\x03\n\x14\x41uthenticationScheme\x12O\n\nknown_type\x18\x01 \x01(\x0e\x32).datasets.stac.v1.KnownAuthenticationTypeB\x05\xaa\x01\x02\x08\x01R\tknownType\x12&\n\x0b\x63ustom_type\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ncustomType\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x19\n\x04name\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\x1b\n\x08location\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x02in\x12\x1d\n\x06scheme\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x06scheme\x12:\n\x05\x66lows\x18\x07 \x03(\x0b\x32$.datasets.stac.v1.AuthenticationFlowR\x05\x66lows\x12\x34\n\x13open_id_connect_url\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x10openIdConnectUrl\"\x9c\x01\n\x12\x41uthenticationFlow\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x34\n\x06oauth2\x18\x02 \x01(\x0b\x32\x1c.datasets.stac.v1.OAuth2FlowR\x06oauth2\x12>\n\nsigned_url\x18\x03 \x01(\x0b\x32\x1f.datasets.stac.v1.SignedURLFlowR\tsignedUrl\"\x89\x02\n\nOAuth2Flow\x12\x32\n\x11\x61uthorization_url\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x10\x61uthorizationUrl\x12\"\n\ttoken_url\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x08tokenUrl\x12@\n\x06scopes\x18\x03 \x03(\x0b\x32(.datasets.stac.v1.OAuth2Flow.ScopesEntryR\x06scopes\x12&\n\x0brefresh_url\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\nrefreshUrl\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xbd\x02\n\rSignedURLFlow\x12\x16\n\x06method\x18\x01 \x01(\tR\x06method\x12+\n\x11\x61uthorization_api\x18\x02 \x01(\tR\x10\x61uthorizationApi\x12O\n\nparameters\x18\x03 \x03(\x0b\x32/.datasets.stac.v1.SignedURLFlow.ParametersEntryR\nparameters\x12,\n\x0eresponse_field\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\rresponseField\x1ah\n\x0fParametersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12?\n\x05value\x18\x02 \x01(\x0b\x32).datasets.stac.v1.AuthenticationParameterR\x05value:\x02\x38\x01\"\xb1\x01\n\x17\x41uthenticationParameter\x12\x14\n\x08location\x18\x01 \x01(\tR\x02in\x12!\n\x08required\x18\x02 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\x08required\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x34\n\x06schema\x18\x04 \x01(\x0b\x32\x1c.datasets.stac.v1.JSONSchemaR\x06schema\"^\n\nJSONSchema\x12/\n\x06object\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructR\x06object\x12\x1f\n\x07\x62oolean\x18\x02 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\x07\x62oolean*\xb0\x02\n\x17KnownAuthenticationType\x12)\n%KNOWN_AUTHENTICATION_TYPE_UNSPECIFIED\x10\x00\x12\"\n\x1eKNOWN_AUTHENTICATION_TYPE_HTTP\x10\x01\x12 \n\x1cKNOWN_AUTHENTICATION_TYPE_S3\x10\x02\x12(\n$KNOWN_AUTHENTICATION_TYPE_SIGNED_URL\x10\x03\x12$\n KNOWN_AUTHENTICATION_TYPE_OAUTH2\x10\x04\x12%\n!KNOWN_AUTHENTICATION_TYPE_API_KEY\x10\x05\x12-\n)KNOWN_AUTHENTICATION_TYPE_OPEN_ID_CONNECT\x10\x06\x42\x92\x01\n\x14\x63om.datasets.stac.v1B\x13\x41uthenticationProtoP\x01\xa2\x02\x03\x44SX\xaa\x02\x10\x44\x61tasets.Stac.V1\xca\x02\x10\x44\x61tasets\\Stac\\V1\xe2\x02\x1c\x44\x61tasets\\Stac\\V1\\GPBMetadata\xea\x02\x12\x44\x61tasets::Stac::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%datasets/stac/v1/authentication.proto\x12\x10\x64\x61tasets.stac.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xbd\x01\n\x0e\x41uthentication\x12G\n\x07schemes\x18\x01 \x03(\x0b\x32-.datasets.stac.v1.Authentication.SchemesEntryR\x07schemes\x1a\x62\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12<\n\x05value\x18\x02 \x01(\x0b\x32&.datasets.stac.v1.AuthenticationSchemeR\x05value:\x02\x38\x01\"\x81\x03\n\x14\x41uthenticationScheme\x12O\n\nknown_type\x18\x01 \x01(\x0e\x32).datasets.stac.v1.KnownAuthenticationTypeB\x05\xaa\x01\x02\x08\x01R\tknownType\x12&\n\x0b\x63ustom_type\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ncustomType\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x19\n\x04name\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\x1b\n\x08location\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x02in\x12\x1d\n\x06scheme\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x06scheme\x12:\n\x05\x66lows\x18\x07 \x03(\x0b\x32$.datasets.stac.v1.AuthenticationFlowR\x05\x66lows\x12\x34\n\x13open_id_connect_url\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x10openIdConnectUrl\"\x9c\x01\n\x12\x41uthenticationFlow\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x34\n\x06oauth2\x18\x02 \x01(\x0b\x32\x1c.datasets.stac.v1.OAuth2FlowR\x06oauth2\x12>\n\nsigned_url\x18\x03 \x01(\x0b\x32\x1f.datasets.stac.v1.SignedURLFlowR\tsignedUrl\"\x89\x02\n\nOAuth2Flow\x12\x32\n\x11\x61uthorization_url\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x10\x61uthorizationUrl\x12\"\n\ttoken_url\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x08tokenUrl\x12@\n\x06scopes\x18\x03 \x03(\x0b\x32(.datasets.stac.v1.OAuth2Flow.ScopesEntryR\x06scopes\x12&\n\x0brefresh_url\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\nrefreshUrl\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xbd\x02\n\rSignedURLFlow\x12\x16\n\x06method\x18\x01 \x01(\tR\x06method\x12+\n\x11\x61uthorization_api\x18\x02 \x01(\tR\x10\x61uthorizationApi\x12O\n\nparameters\x18\x03 \x03(\x0b\x32/.datasets.stac.v1.SignedURLFlow.ParametersEntryR\nparameters\x12,\n\x0eresponse_field\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\rresponseField\x1ah\n\x0fParametersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12?\n\x05value\x18\x02 \x01(\x0b\x32).datasets.stac.v1.AuthenticationParameterR\x05value:\x02\x38\x01\"\xac\x01\n\x17\x41uthenticationParameter\x12\x14\n\x08location\x18\x01 \x01(\tR\x02in\x12!\n\x08required\x18\x02 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\x08required\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12/\n\x06schema\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x06schema*\xb0\x02\n\x17KnownAuthenticationType\x12)\n%KNOWN_AUTHENTICATION_TYPE_UNSPECIFIED\x10\x00\x12\"\n\x1eKNOWN_AUTHENTICATION_TYPE_HTTP\x10\x01\x12 \n\x1cKNOWN_AUTHENTICATION_TYPE_S3\x10\x02\x12(\n$KNOWN_AUTHENTICATION_TYPE_SIGNED_URL\x10\x03\x12$\n KNOWN_AUTHENTICATION_TYPE_OAUTH2\x10\x04\x12%\n!KNOWN_AUTHENTICATION_TYPE_API_KEY\x10\x05\x12-\n)KNOWN_AUTHENTICATION_TYPE_OPEN_ID_CONNECT\x10\x06\x42\x92\x01\n\x14\x63om.datasets.stac.v1B\x13\x41uthenticationProtoP\x01\xa2\x02\x03\x44SX\xaa\x02\x10\x44\x61tasets.Stac.V1\xca\x02\x10\x44\x61tasets\\Stac\\V1\xe2\x02\x1c\x44\x61tasets\\Stac\\V1\\GPBMetadata\xea\x02\x12\x44\x61tasets::Stac::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -65,10 +65,8 @@ _globals['_AUTHENTICATIONPARAMETER'].fields_by_name['required']._serialized_options = b'\252\001\002\010\001' _globals['_AUTHENTICATIONPARAMETER'].fields_by_name['description']._loaded_options = None _globals['_AUTHENTICATIONPARAMETER'].fields_by_name['description']._serialized_options = b'\252\001\002\010\001' - _globals['_JSONSCHEMA'].fields_by_name['boolean']._loaded_options = None - _globals['_JSONSCHEMA'].fields_by_name['boolean']._serialized_options = b'\252\001\002\010\001' - _globals['_KNOWNAUTHENTICATIONTYPE']._serialized_start=1693 - _globals['_KNOWNAUTHENTICATIONTYPE']._serialized_end=1997 + _globals['_KNOWNAUTHENTICATIONTYPE']._serialized_start=1592 + _globals['_KNOWNAUTHENTICATIONTYPE']._serialized_end=1896 _globals['_AUTHENTICATION']._serialized_start=90 _globals['_AUTHENTICATION']._serialized_end=279 _globals['_AUTHENTICATION_SCHEMESENTRY']._serialized_start=181 @@ -86,7 +84,5 @@ _globals['_SIGNEDURLFLOW_PARAMETERSENTRY']._serialized_start=1310 _globals['_SIGNEDURLFLOW_PARAMETERSENTRY']._serialized_end=1414 _globals['_AUTHENTICATIONPARAMETER']._serialized_start=1417 - _globals['_AUTHENTICATIONPARAMETER']._serialized_end=1594 - _globals['_JSONSCHEMA']._serialized_start=1596 - _globals['_JSONSCHEMA']._serialized_end=1690 + _globals['_AUTHENTICATIONPARAMETER']._serialized_end=1589 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.pyi b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.pyi index 9dae2c5..e8e04bd 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.pyi +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb2.pyi @@ -115,13 +115,5 @@ class AuthenticationParameter(_message.Message): location: str required: bool description: str - schema: JSONSchema - def __init__(self, location: _Optional[str] = ..., required: bool = ..., description: _Optional[str] = ..., schema: _Optional[_Union[JSONSchema, _Mapping]] = ...) -> None: ... - -class JSONSchema(_message.Message): - __slots__ = ("object", "boolean") - OBJECT_FIELD_NUMBER: _ClassVar[int] - BOOLEAN_FIELD_NUMBER: _ClassVar[int] - object: _struct_pb2.Struct - boolean: bool - def __init__(self, object: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ..., boolean: bool = ...) -> None: ... + schema: _struct_pb2.Struct + def __init__(self, location: _Optional[str] = ..., required: bool = ..., description: _Optional[str] = ..., schema: _Optional[_Union[_struct_pb2.Struct, _Mapping]] = ...) -> None: ... diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.py index 8d8aa7e..2b2b7e9 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.py @@ -22,9 +22,10 @@ _sym_db = _symbol_database.Default() +from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x64\x61tasets/stac/v1/storage.proto\x12\x10\x64\x61tasets.stac.v1\"\xa8\x01\n\x07Storage\x12@\n\x07schemes\x18\x01 \x03(\x0b\x32&.datasets.stac.v1.Storage.SchemesEntryR\x07schemes\x1a[\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32\x1f.datasets.stac.v1.StorageSchemeR\x05value:\x02\x38\x01\"\xdc\x02\n\rStorageScheme\x12H\n\nknown_type\x18\x01 \x01(\x0e\x32\".datasets.stac.v1.KnownStorageTypeB\x05\xaa\x01\x02\x08\x01R\tknownType\x12&\n\x0b\x63ustom_type\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ncustomType\x12\x1a\n\x08platform\x18\x03 \x01(\tR\x08platform\x12\x1b\n\x05title\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12\'\n\x0b\x64\x65scription\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x1d\n\x06region\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x06region\x12,\n\x0erequester_pays\x18\x07 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\rrequesterPays\x12*\n\rstorage_class\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0cstorageClass*\xcc\x01\n\x10KnownStorageType\x12\"\n\x1eKNOWN_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19KNOWN_STORAGE_TYPE_AWS_S3\x10\x01\x12 \n\x1cKNOWN_STORAGE_TYPE_CUSTOM_S3\x10\x02\x12&\n\"KNOWN_STORAGE_TYPE_MICROSOFT_AZURE\x10\x03\x12+\n\'KNOWN_STORAGE_TYPE_GOOGLE_CLOUD_STORAGE\x10\x04\x42\x8b\x01\n\x14\x63om.datasets.stac.v1B\x0cStorageProtoP\x01\xa2\x02\x03\x44SX\xaa\x02\x10\x44\x61tasets.Stac.V1\xca\x02\x10\x44\x61tasets\\Stac\\V1\xe2\x02\x1c\x44\x61tasets\\Stac\\V1\\GPBMetadata\xea\x02\x12\x44\x61tasets::Stac::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1e\x64\x61tasets/stac/v1/storage.proto\x12\x10\x64\x61tasets.stac.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xa8\x01\n\x07Storage\x12@\n\x07schemes\x18\x01 \x03(\x0b\x32&.datasets.stac.v1.Storage.SchemesEntryR\x07schemes\x1a[\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32\x1f.datasets.stac.v1.StorageSchemeR\x05value:\x02\x38\x01\"\xed\x04\n\rStorageScheme\x12H\n\nknown_type\x18\x01 \x01(\x0e\x32\".datasets.stac.v1.KnownStorageTypeB\x05\xaa\x01\x02\x08\x01R\tknownType\x12&\n\x0b\x63ustom_type\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ncustomType\x12\x1a\n\x08platform\x18\x03 \x01(\tR\x08platform\x12\x1b\n\x05title\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12\'\n\x0b\x64\x65scription\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x1d\n\x06region\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x06region\x12,\n\x0erequester_pays\x18\x07 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\rrequesterPays\x12*\n\rstorage_class\x18\x08 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0cstorageClass\x12\x1d\n\x06\x62ucket\x18\t \x01(\tB\x05\xaa\x01\x02\x08\x01R\x06\x62ucket\x12\x1f\n\x07\x61\x63\x63ount\x18\n \x01(\tB\x05\xaa\x01\x02\x08\x01R\x07\x61\x63\x63ount\x12n\n\x15\x61\x64\x64itional_properties\x18\x0b \x03(\x0b\x32\x39.datasets.stac.v1.StorageScheme.AdditionalPropertiesEntryR\x14\x61\x64\x64itionalProperties\x1a_\n\x19\x41\x64\x64itionalPropertiesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.ValueR\x05value:\x02\x38\x01*\xcc\x01\n\x10KnownStorageType\x12\"\n\x1eKNOWN_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19KNOWN_STORAGE_TYPE_AWS_S3\x10\x01\x12 \n\x1cKNOWN_STORAGE_TYPE_CUSTOM_S3\x10\x02\x12&\n\"KNOWN_STORAGE_TYPE_MICROSOFT_AZURE\x10\x03\x12+\n\'KNOWN_STORAGE_TYPE_GOOGLE_CLOUD_STORAGE\x10\x04\x42\x8b\x01\n\x14\x63om.datasets.stac.v1B\x0cStorageProtoP\x01\xa2\x02\x03\x44SX\xaa\x02\x10\x44\x61tasets.Stac.V1\xca\x02\x10\x44\x61tasets\\Stac\\V1\xe2\x02\x1c\x44\x61tasets\\Stac\\V1\\GPBMetadata\xea\x02\x12\x44\x61tasets::Stac::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -34,6 +35,8 @@ _globals['DESCRIPTOR']._serialized_options = b'\n\024com.datasets.stac.v1B\014StorageProtoP\001\242\002\003DSX\252\002\020Datasets.Stac.V1\312\002\020Datasets\\Stac\\V1\342\002\034Datasets\\Stac\\V1\\GPBMetadata\352\002\022Datasets::Stac::V1\222\003\002\010\002' _globals['_STORAGE_SCHEMESENTRY']._loaded_options = None _globals['_STORAGE_SCHEMESENTRY']._serialized_options = b'8\001' + _globals['_STORAGESCHEME_ADDITIONALPROPERTIESENTRY']._loaded_options = None + _globals['_STORAGESCHEME_ADDITIONALPROPERTIESENTRY']._serialized_options = b'8\001' _globals['_STORAGESCHEME'].fields_by_name['known_type']._loaded_options = None _globals['_STORAGESCHEME'].fields_by_name['known_type']._serialized_options = b'\252\001\002\010\001' _globals['_STORAGESCHEME'].fields_by_name['custom_type']._loaded_options = None @@ -48,12 +51,18 @@ _globals['_STORAGESCHEME'].fields_by_name['requester_pays']._serialized_options = b'\252\001\002\010\001' _globals['_STORAGESCHEME'].fields_by_name['storage_class']._loaded_options = None _globals['_STORAGESCHEME'].fields_by_name['storage_class']._serialized_options = b'\252\001\002\010\001' - _globals['_KNOWNSTORAGETYPE']._serialized_start=575 - _globals['_KNOWNSTORAGETYPE']._serialized_end=779 - _globals['_STORAGE']._serialized_start=53 - _globals['_STORAGE']._serialized_end=221 - _globals['_STORAGE_SCHEMESENTRY']._serialized_start=130 - _globals['_STORAGE_SCHEMESENTRY']._serialized_end=221 - _globals['_STORAGESCHEME']._serialized_start=224 - _globals['_STORAGESCHEME']._serialized_end=572 + _globals['_STORAGESCHEME'].fields_by_name['bucket']._loaded_options = None + _globals['_STORAGESCHEME'].fields_by_name['bucket']._serialized_options = b'\252\001\002\010\001' + _globals['_STORAGESCHEME'].fields_by_name['account']._loaded_options = None + _globals['_STORAGESCHEME'].fields_by_name['account']._serialized_options = b'\252\001\002\010\001' + _globals['_KNOWNSTORAGETYPE']._serialized_start=878 + _globals['_KNOWNSTORAGETYPE']._serialized_end=1082 + _globals['_STORAGE']._serialized_start=83 + _globals['_STORAGE']._serialized_end=251 + _globals['_STORAGE_SCHEMESENTRY']._serialized_start=160 + _globals['_STORAGE_SCHEMESENTRY']._serialized_end=251 + _globals['_STORAGESCHEME']._serialized_start=254 + _globals['_STORAGESCHEME']._serialized_end=875 + _globals['_STORAGESCHEME_ADDITIONALPROPERTIESENTRY']._serialized_start=780 + _globals['_STORAGESCHEME_ADDITIONALPROPERTIESENTRY']._serialized_end=875 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.pyi b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.pyi index 6fa550c..50c38bd 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.pyi +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb2.pyi @@ -1,3 +1,4 @@ +from google.protobuf import struct_pb2 as _struct_pb2 from google.protobuf.internal import containers as _containers from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper from google.protobuf import descriptor as _descriptor @@ -34,7 +35,14 @@ class Storage(_message.Message): def __init__(self, schemes: _Optional[_Mapping[str, StorageScheme]] = ...) -> None: ... class StorageScheme(_message.Message): - __slots__ = ("known_type", "custom_type", "platform", "title", "description", "region", "requester_pays", "storage_class") + __slots__ = ("known_type", "custom_type", "platform", "title", "description", "region", "requester_pays", "storage_class", "bucket", "account", "additional_properties") + class AdditionalPropertiesEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: _struct_pb2.Value + def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[_struct_pb2.Value, _Mapping]] = ...) -> None: ... KNOWN_TYPE_FIELD_NUMBER: _ClassVar[int] CUSTOM_TYPE_FIELD_NUMBER: _ClassVar[int] PLATFORM_FIELD_NUMBER: _ClassVar[int] @@ -43,6 +51,9 @@ class StorageScheme(_message.Message): REGION_FIELD_NUMBER: _ClassVar[int] REQUESTER_PAYS_FIELD_NUMBER: _ClassVar[int] STORAGE_CLASS_FIELD_NUMBER: _ClassVar[int] + BUCKET_FIELD_NUMBER: _ClassVar[int] + ACCOUNT_FIELD_NUMBER: _ClassVar[int] + ADDITIONAL_PROPERTIES_FIELD_NUMBER: _ClassVar[int] known_type: KnownStorageType custom_type: str platform: str @@ -51,4 +62,7 @@ class StorageScheme(_message.Message): region: str requester_pays: bool storage_class: str - def __init__(self, known_type: _Optional[_Union[KnownStorageType, str]] = ..., custom_type: _Optional[str] = ..., platform: _Optional[str] = ..., title: _Optional[str] = ..., description: _Optional[str] = ..., region: _Optional[str] = ..., requester_pays: bool = ..., storage_class: _Optional[str] = ...) -> None: ... + bucket: str + account: str + additional_properties: _containers.MessageMap[str, _struct_pb2.Value] + def __init__(self, known_type: _Optional[_Union[KnownStorageType, str]] = ..., custom_type: _Optional[str] = ..., platform: _Optional[str] = ..., title: _Optional[str] = ..., description: _Optional[str] = ..., region: _Optional[str] = ..., requester_pays: bool = ..., storage_class: _Optional[str] = ..., bucket: _Optional[str] = ..., account: _Optional[str] = ..., additional_properties: _Optional[_Mapping[str, _struct_pb2.Value]] = ...) -> None: ... diff --git a/tilebox-storage/pyproject.toml b/tilebox-storage/pyproject.toml index cf96e22..abddcd9 100644 --- a/tilebox-storage/pyproject.toml +++ b/tilebox-storage/pyproject.toml @@ -28,6 +28,8 @@ dependencies = [ "shapely>=2", "obstore>=0.8.0", "boto3>=1.37.0", # required for the obstore Boto3CredentialProvider + "async-geotiff; python_version >= '3.11'", + "pyproj>=3.4", ] [dependency-groups] diff --git a/tilebox-storage/tests/test_asset_client.py b/tilebox-storage/tests/test_asset_client.py new file mode 100644 index 0000000..ca5e73b --- /dev/null +++ b/tilebox-storage/tests/test_asset_client.py @@ -0,0 +1,147 @@ +import subprocess +import sys +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from types import MappingProxyType +from typing import Any +from unittest.mock import AsyncMock, patch + +import pytest +from obstore.store import LocalStore + +from tilebox.datasets.assets.assets import Asset, AssetLocation +from tilebox.datasets.assets.stac.authentication import AuthenticationScheme +from tilebox.datasets.assets.stac.storage import StorageScheme +from tilebox.storage.aio import AssetAccessPolicy, Client + + +def _asset(location: AssetLocation, **kwargs: Any) -> Asset: + return Asset(key="data", primary=location, **kwargs) + + +def test_new_aio_api_does_not_introduce_root_client_or_eager_geotiff_import() -> None: + code = ( + "import sys\n" + "import tilebox.storage as storage\n" + "assert not hasattr(storage, 'Client')\n" + "from tilebox.storage.aio import Client, download, iter_bytes, open_geotiff, read_bytes\n" + "assert Client and download and iter_bytes and open_geotiff and read_bytes\n" + "assert 'async_geotiff' not in sys.modules\n" + ) + subprocess.run([sys.executable, "-c", code], check=True) # noqa: S603 + + +def test_resolve_prefers_s3_and_reuses_store() -> None: + scheme = StorageScheme(key="earth-search", type="aws_s3", region="us-west-2", requester_pays=False) + schemes = MappingProxyType({scheme.key: scheme}) + primary = AssetLocation("https://example.com/data.tif", schemes) + alternate = AssetLocation("s3://bucket/path/data.tif", schemes) + asset = _asset(primary, alternates=MappingProxyType({"s3": alternate})) + client = Client() + + with patch("obstore.store.S3Store") as store_class: + store = object() + store_class.return_value = store + first = client.resolve(asset) + second = client.resolve(asset) + + assert first.location is alternate + assert first.store is second.store is store + assert first.path == "path/data.tif" + store_class.assert_called_once_with( + "bucket", + region="us-west-2", + request_payer=False, + skip_signature=True, + ) + + +def test_concurrent_resolution_constructs_one_store() -> None: + asset = _asset(AssetLocation("s3://bucket/data.tif")) + client = Client() + with patch("obstore.store.S3Store") as store_class: + store_class.return_value = object() + with ThreadPoolExecutor(max_workers=8) as executor: + stores = list(executor.map(lambda _: client.resolve(asset).store, range(32))) + assert len({id(store) for store in stores}) == 1 + store_class.assert_called_once() + + +def test_resolution_rejection_details_and_client_policy() -> None: + auth = AuthenticationScheme(key="oauth", type="oauth2") + first = StorageScheme(key="first", type="aws_s3") + second = StorageScheme(key="second", type="aws_s3") + asset = _asset( + AssetLocation( + "s3://bucket/data.tif", + MappingProxyType({first.key: first, second.key: second}), + MappingProxyType({auth.key: auth}), + ) + ) + client = Client() + + with pytest.raises(ValueError, match=r"primary.*multiple storage schemes"): + client.resolve(asset) + authenticated = _asset( + AssetLocation("s3://bucket/data.tif", authentication_schemes=MappingProxyType({auth.key: auth})) + ) + with pytest.raises(ValueError, match=r"authentication scheme 'oauth'.*unsupported"): + client.resolve(authenticated) + + primary = AssetLocation("https://example.com/data.tif") + alternate = AssetLocation("s3://bucket/data.tif") + location = ( + Client(policy=AssetAccessPolicy(preferred_schemes=("https", "s3"))) + .resolve(_asset(primary, alternates=MappingProxyType({"s3": alternate}))) + .location + ) + assert location is primary + + +@pytest.mark.asyncio +async def test_read_stream_and_atomic_download(tmp_path: Path) -> None: + source = tmp_path / "source.bin" + source.write_bytes(b"asset contents") + asset = _asset(AssetLocation(source.as_uri())) + client = Client() + + assert await client.read_bytes(asset) == b"asset contents" + with pytest.raises(ValueError, match="max_bytes=5"): + await client.read_bytes(asset, max_bytes=5) + assert b"".join([chunk async for chunk in client.iter_bytes(asset)]) == b"asset contents" + + destination = tmp_path / "nested" / "destination.bin" + assert await client.download(asset, destination) == destination + assert destination.read_bytes() == b"asset contents" + with pytest.raises(FileExistsError): + await client.download(asset, destination) + source.write_bytes(b"replacement") + await client.download(asset, destination, overwrite=True) + assert destination.read_bytes() == b"replacement" + assert list(destination.parent.glob("*.tmp")) == [] + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="async-geotiff requires Python 3.11 or newer") +async def test_open_geotiff_forwards_resolved_store_path_and_options(tmp_path: Path) -> None: + path = tmp_path / "data.tif" + path.touch() + asset = _asset(AssetLocation(path.as_uri()), media_type="image/tiff; application=geotiff") + opened = object() + with patch("async_geotiff.GeoTIFF.open", new=AsyncMock(return_value=opened)) as open_mock: + result = await Client().open_geotiff(asset, prefetch=1024, multiplier=3) + + assert result is opened + args, kwargs = open_mock.call_args + assert args == (str(path).lstrip("/"),) + assert isinstance(kwargs["store"], LocalStore) + assert kwargs["prefetch"] == 1024 + assert kwargs["multiplier"] == 3 + + +@pytest.mark.asyncio +@pytest.mark.skipif(sys.version_info < (3, 11), reason="async-geotiff requires Python 3.11 or newer") +async def test_open_geotiff_rejects_incompatible_media_type() -> None: + asset = _asset(AssetLocation("file:///tmp/data.json"), media_type="application/json") + with pytest.raises(ValueError, match="not compatible"): + await Client().open_geotiff(asset) diff --git a/tilebox-storage/tests/test_geotiff.py b/tilebox-storage/tests/test_geotiff.py new file mode 100644 index 0000000..d16ce74 --- /dev/null +++ b/tilebox-storage/tests/test_geotiff.py @@ -0,0 +1,47 @@ +from types import SimpleNamespace +from typing import cast + +import pytest + +async_geotiff = pytest.importorskip("async_geotiff") +from affine import Affine # noqa: E402 + +Window = async_geotiff.Window +GeoTIFF = async_geotiff.GeoTIFF + +from tilebox.storage.geotiff import window_from_bounds # noqa: E402 + + +def _geotiff() -> GeoTIFF: + return cast( + GeoTIFF, + SimpleNamespace( + crs="EPSG:4326", + transform=Affine(1, 0, 0, 0, -1, 10), + width=10, + height=10, + ), + ) + + +def test_window_from_projected_bounds_rounds_outward() -> None: + window = window_from_bounds(_geotiff(), (1.2, 2.2, 4.1, 7.8), crs="EPSG:4326") + assert window == Window(col_off=1, row_off=2, width=4, height=6) + + +def test_window_clips_partial_overlap() -> None: + assert window_from_bounds(_geotiff(), (-3, 8, 3, 12), crs="EPSG:4326") == Window(0, 0, 3, 2) + + +def test_window_requires_full_containment() -> None: + with pytest.raises(ValueError, match="not fully contained"): + window_from_bounds(_geotiff(), (-1, 2, 3, 5), crs="EPSG:4326", require_fully_contained=True) + + +def test_window_rejects_empty_malformed_and_antimeridian_bounds() -> None: + with pytest.raises(ValueError, match="do not intersect"): + window_from_bounds(_geotiff(), (20, 20, 21, 21), crs="EPSG:4326") + with pytest.raises(ValueError, match="bottom"): + window_from_bounds(_geotiff(), (1, 4, 2, 3), crs="EPSG:3857") + with pytest.raises(ValueError, match=r"split.*antimeridian"): + window_from_bounds(_geotiff(), (170, -10, -170, 10), crs="EPSG:4326") diff --git a/tilebox-storage/tilebox/storage/aio.py b/tilebox-storage/tilebox/storage/aio.py index c2c5741..265c0f8 100644 --- a/tilebox-storage/tilebox/storage/aio.py +++ b/tilebox-storage/tilebox/storage/aio.py @@ -18,6 +18,15 @@ from _tilebox.grpc.aio.producer_consumer import async_producer_consumer from _tilebox.grpc.aio.syncify import Syncifiable +from tilebox.storage.client import ( + AssetAccessPolicy, + Client, + ResolvedAsset, + download, + iter_bytes, + open_geotiff, + read_bytes, +) from tilebox.storage.granule import ( ASFStorageGranule, CopernicusStorageGranule, @@ -27,6 +36,25 @@ _is_copernicus_odata_url, ) +__all__ = [ + "ASFStorageClient", + "AssetAccessPolicy", + "CachingStorageClient", + "Client", + "CopernicusStorageClient", + "LocalFileSystemStorageClient", + "ResolvedAsset", + "StorageClient", + "USGSLandsatStorageClient", + "UmbraStorageClient", + "download", + "download_objects", + "iter_bytes", + "list_object_paths", + "open_geotiff", + "read_bytes", +] + if TYPE_CHECKING: import niquests import xarray as xr diff --git a/tilebox-storage/tilebox/storage/client.py b/tilebox-storage/tilebox/storage/client.py new file mode 100644 index 0000000..39d1085 --- /dev/null +++ b/tilebox-storage/tilebox/storage/client.py @@ -0,0 +1,327 @@ +"""Asset-oriented asynchronous storage access.""" + +import os +import sys +import threading +import uuid +from collections.abc import AsyncIterator, Mapping +from dataclasses import dataclass +from os import PathLike +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import unquote, urlsplit + +if TYPE_CHECKING: + from async_geotiff import GeoTIFF + from obstore.store import ObjectStore + + from tilebox.datasets.assets.assets import Asset, AssetLocation + from tilebox.datasets.assets.stac.authentication import AuthenticationScheme + from tilebox.datasets.assets.stac.storage import StorageScheme + + +@dataclass(frozen=True, slots=True) +class AssetAccessPolicy: + """Policy controlling deterministic asset-location selection.""" + + preferred_schemes: tuple[str, ...] = ("file", "s3", "gs", "az", "https", "http") + + +@dataclass(frozen=True, slots=True) +class ResolvedAsset: + """A resolved asset location ready for object-level access.""" + + asset: "Asset" + location: "AssetLocation" + href: str + store: "ObjectStore" + path: str + storage_scheme: "StorageScheme | None" + authentication_scheme: "AuthenticationScheme | None" + + +class Client: + """Resolve and access dataset assets using reusable object stores.""" + + def __init__(self, *, policy: AssetAccessPolicy | None = None) -> None: + """Create an asset storage client. + + Args: + policy: Location-selection policy. By default local, S3, Google Cloud, + Azure, and HTTP locations are preferred in that order. + """ + self.policy = policy or AssetAccessPolicy() + self._stores: dict[tuple[Any, ...], ObjectStore] = {} + self._stores_lock = threading.RLock() + + def resolve(self, asset: "Asset") -> ResolvedAsset: + """Resolve an asset without making a network request. + + Args: + asset: Asset whose best supported location should be selected. + + Returns: + The selected location, object store, object path, and access metadata. + + Raises: + ValueError: If no location can be accessed by this client. + """ + candidates = [("primary", asset.primary), *asset.alternates.items()] + rank = {scheme: index for index, scheme in enumerate(self.policy.preferred_schemes)} + candidates.sort(key=lambda item: rank.get(_uri_scheme(item[1].href), len(rank))) + + rejected = [] + for label, location in candidates: + try: + storage = _select_scheme(location.storage_schemes, "storage") + authentication = _select_scheme(location.authentication_schemes, "authentication") + store, path = self._store_for(location.href, storage, authentication) + except (TypeError, ValueError) as error: + rejected.append(f"{label} ({location.href!r}): {error}") + continue + return ResolvedAsset(asset, location, location.href, store, path, storage, authentication) + details = "; ".join(rejected) or "asset has no locations" + raise ValueError(f"unable to resolve asset {asset.key!r}: {details}") + + def _store_for( # noqa: C901, PLR0912 + self, + href: str, + storage: "StorageScheme | None", + authentication: "AuthenticationScheme | None", + ) -> "tuple[ObjectStore, str]": + from obstore.store import AzureStore, GCSStore, HTTPStore, LocalStore, S3Store # noqa: PLC0415 + + parsed = urlsplit(href) + scheme = _uri_scheme(href) + if authentication is not None and authentication.type != "s3": + raise ValueError(f"authentication scheme {authentication.key!r} ({authentication.type!r}) is unsupported") + + if scheme == "file": + identity = ("file", "/") + constructor = lambda: LocalStore("/") # noqa: E731 + path = unquote(parsed.path if parsed.scheme else href).lstrip("/") + elif scheme == "s3": + if storage is not None and storage.type not in {"aws_s3", "custom_s3", "unspecified", None}: + raise ValueError(f"storage scheme {storage.key!r} is incompatible with s3") + authenticated = authentication is not None + options = ( + storage.region if storage else None, + storage.requester_pays if storage else None, + not authenticated, + storage.platform if storage and storage.type == "custom_s3" else None, + ) + identity = ("s3", parsed.netloc, *options, repr(authentication)) + constructor = lambda: S3Store( # noqa: E731 + parsed.netloc, + **{ + key: value + for key, value in { + "region": options[0], + "request_payer": options[1], + "skip_signature": options[2], + "endpoint": options[3], + }.items() + if value is not None + }, + ) + path = parsed.path.lstrip("/") + elif scheme in {"gs", "gcs"}: + if storage is not None and storage.type not in {"google_cloud_storage", "unspecified", None}: + raise ValueError(f"storage scheme {storage.key!r} is incompatible with Google Cloud Storage") + anonymous = authentication is None + identity = ("gs", parsed.netloc, anonymous, repr(authentication)) + constructor = lambda: GCSStore(parsed.netloc, skip_signature=anonymous) # noqa: E731 + path = parsed.path.lstrip("/") + elif scheme in {"az", "azure"}: + segments = parsed.path.lstrip("/").split("/", 1) + if not parsed.netloc or not segments[0]: + raise ValueError("Azure href must have the form az://account/container/path") + container = segments[0] + path = segments[1] if len(segments) == 2 else "" + identity = ("az", parsed.netloc, container, repr(authentication)) + constructor = lambda: AzureStore(container, account_name=parsed.netloc) # noqa: E731 + elif scheme in {"http", "https"}: + if authentication is not None: + raise ValueError("authenticated HTTP locations are not supported yet") + origin = f"{scheme}://{parsed.netloc}" + identity = (scheme, origin) + constructor = lambda: HTTPStore(origin) # noqa: E731 + path = parsed.path.lstrip("/") + if parsed.query: + path = f"{path}?{parsed.query}" + else: + raise ValueError(f"unsupported URI scheme {scheme!r}") + + with self._stores_lock: + store = self._stores.get(identity) + if store is None: + store = constructor() + self._stores[identity] = store + return store, path + + async def read_bytes( + self, + asset: "Asset", + *, + max_bytes: int | None = None, + ) -> bytes: + """Read an entire asset into memory. + + Args: + asset: Asset to resolve and read. + max_bytes: Optional maximum accepted object size. The request is rejected + before reading when the provider reports a larger size, and checked + again after reading when no reliable size was available beforehand. + + Returns: + The complete object contents as Python bytes. + """ + import obstore # noqa: PLC0415 + + resolved = self.resolve(asset) + result = await obstore.get_async(resolved.store, resolved.path) + size = result.meta.get("size") + if max_bytes is not None and size is not None and size > max_bytes: + raise ValueError(f"asset size {size} exceeds max_bytes={max_bytes}") + data = bytes(await result.bytes_async()) + if max_bytes is not None and len(data) > max_bytes: + raise ValueError(f"asset size {len(data)} exceeds max_bytes={max_bytes}") + return data + + async def iter_bytes( + self, + asset: "Asset", + ) -> AsyncIterator[bytes]: + """Stream an asset as provider-dependent byte chunks. + + Args: + asset: Asset to resolve and stream. + + Yields: + Byte chunks whose sizes are chosen by the storage provider. + """ + import obstore # noqa: PLC0415 + + resolved = self.resolve(asset) + result = await obstore.get_async(resolved.store, resolved.path) + async for chunk in result: + yield bytes(chunk) + + async def download( + self, + asset: "Asset", + destination: str | PathLike[str], + *, + overwrite: bool = False, + ) -> Path: + """Atomically download an asset to an exact local file path. + + Args: + asset: Asset to resolve and download. + destination: Exact destination file path. Parent directories are created. + overwrite: Replace an existing destination when true; otherwise raise + :class:`FileExistsError`. + + Returns: + The destination path. + """ + destination = Path(destination) + if destination.exists() and not overwrite: + raise FileExistsError(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = destination.with_name(f".{destination.name}.{uuid.uuid4().hex}.tmp") + try: + with temporary.open("xb") as output: + async for chunk in self.iter_bytes(asset): + output.write(chunk) + if overwrite: + temporary.replace(destination) + else: + os.link(temporary, destination) + finally: + temporary.unlink(missing_ok=True) + return destination + + async def open_geotiff( + self, + asset: "Asset", + *, + prefetch: int = 32768, + multiplier: float = 2.0, + ) -> "GeoTIFF": + """Open a GeoTIFF without reading pixel data. + + Args: + asset: GeoTIFF asset to resolve and open. + prefetch: Number of bytes initially fetched to parse TIFF metadata. + multiplier: Growth factor for subsequent metadata range requests. + + Returns: + An async-geotiff ``GeoTIFF`` using the resolved object store. + """ + media_type = (asset.media_type or "").lower() + if media_type and not media_type.startswith(("image/tiff", "image/geotiff", "application/geotiff")): + raise ValueError(f"asset media type {asset.media_type!r} is not compatible with GeoTIFF") + try: + from async_geotiff import GeoTIFF # noqa: PLC0415 + except ImportError: + if sys.version_info < (3, 11): + raise ImportError( + "open_geotiff is unavailable on Python 3.10 because async-geotiff requires Python 3.11 or newer" + ) from None + raise ImportError("async-geotiff is required by tilebox-storage but could not be imported") from None + resolved = self.resolve(asset) + return await GeoTIFF.open( + resolved.path, + store=resolved.store, + prefetch=prefetch, + multiplier=multiplier, + ) + + +def _uri_scheme(href: str) -> str: + return urlsplit(href).scheme.lower() or "file" + + +def _select_scheme(schemes: Mapping[str, Any], kind: str) -> Any | None: + if len(schemes) > 1: + keys = ", ".join(repr(key) for key in schemes) + raise ValueError(f"multiple {kind} schemes are applicable ({keys})") + return next(iter(schemes.values()), None) + + +_default_client = Client() + + +async def read_bytes( + asset: "Asset", + *, + max_bytes: int | None = None, +) -> bytes: + """Read an entire asset using the shared default client.""" + return await _default_client.read_bytes(asset, max_bytes=max_bytes) + + +def iter_bytes(asset: "Asset") -> AsyncIterator[bytes]: + """Stream an asset using the shared default client.""" + return _default_client.iter_bytes(asset) + + +async def download( + asset: "Asset", + destination: str | PathLike[str], + *, + overwrite: bool = False, +) -> Path: + """Download an asset using the shared default client.""" + return await _default_client.download(asset, destination, overwrite=overwrite) + + +async def open_geotiff( + asset: "Asset", + *, + prefetch: int = 32768, + multiplier: float = 2.0, +) -> "GeoTIFF": + """Open a GeoTIFF using the shared default client.""" + return await _default_client.open_geotiff(asset, prefetch=prefetch, multiplier=multiplier) diff --git a/tilebox-storage/tilebox/storage/geotiff.py b/tilebox-storage/tilebox/storage/geotiff.py new file mode 100644 index 0000000..40d5b5a --- /dev/null +++ b/tilebox-storage/tilebox/storage/geotiff.py @@ -0,0 +1,100 @@ +"""GeoTIFF coordinate helpers.""" + +import math +import sys + +from pyproj import CRS, Proj, Transformer + +try: + from async_geotiff import GeoTIFF, Window +except ImportError: + if sys.version_info < (3, 11): + raise ImportError( + "tilebox.storage.geotiff is unavailable on Python 3.10 because async-geotiff requires Python 3.11 or newer" + ) from None + raise ImportError("async-geotiff is required by tilebox-storage but could not be imported") from None + +Bounds = tuple[float, float, float, float] +BoundsCRS = str | int | CRS | Proj + + +def window_from_bounds( # noqa: C901 + geotiff: GeoTIFF, + bounds: Bounds, + *, + crs: BoundsCRS, + require_fully_contained: bool = False, +) -> Window: + """Convert geographic bounds to an outward-rounded GeoTIFF pixel window. + + This helper is useful before calling async-geotiff window-reading methods. It + transforms ``bounds`` from ``crs`` into the image CRS, maps all four corners + through the inverse image transform, rounds outward so intersecting pixels are + retained, and clips the result to the image dimensions. + + Args: + geotiff: Open async-geotiff image whose CRS, affine transform, width, and + height define the output pixel coordinate space. + bounds: Four coordinates in explicit ``(left, bottom, right, top)`` order, + also known as ``(min_x, min_y, max_x, max_y)``. + crs: CRS of ``bounds`` as an EPSG integer, user-input string, ``CRS``, or + ``Proj`` instance. + require_fully_contained: Reject bounds extending beyond the image instead of + clipping them to the image dimensions. + + Returns: + An async-geotiff ``Window`` containing every pixel touched by the bounds. + + Raises: + ValueError: If bounds are malformed, cross the EPSG:4326 antimeridian, cannot + be transformed, do not intersect the image, or violate full containment. + """ + try: + valid_bounds = len(bounds) == 4 and all(math.isfinite(value) for value in bounds) + except TypeError: + valid_bounds = False + if not valid_bounds: + raise ValueError("bounds must contain four finite values in (left, bottom, right, top) order") + left, bottom, right, top = bounds + try: + source_crs = CRS.from_user_input(crs) + except Exception as error: + raise ValueError(f"invalid bounds CRS: {crs!r}") from error + if left > right: + if source_crs.to_epsg() == 4326: + raise ValueError("antimeridian-crossing bounds are not supported; split the bounds at the antimeridian") + raise ValueError("bounds left must be less than or equal to right") + if bottom > top: + raise ValueError("bounds bottom must be less than or equal to top") + try: + transformed = Transformer.from_crs(source_crs, geotiff.crs, always_xy=True).transform_bounds( + *bounds, + densify_pts=21, + ) + except Exception as error: + raise ValueError("bounds could not be transformed into the GeoTIFF CRS") from error + if not all(math.isfinite(value) for value in transformed): + raise ValueError("bounds could not be transformed into finite GeoTIFF coordinates") + left, bottom, right, top = transformed + try: + inverse = ~geotiff.transform + pixels = [inverse * (x, y) for x in (left, right) for y in (bottom, top)] + except Exception as error: + raise ValueError("GeoTIFF transform is not invertible") from error + col_start = math.floor(min(point[0] for point in pixels)) + col_stop = math.ceil(max(point[0] for point in pixels)) + row_start = math.floor(min(point[1] for point in pixels)) + row_stop = math.ceil(max(point[1] for point in pixels)) + outside = col_start < 0 or row_start < 0 or col_stop > geotiff.width or row_stop > geotiff.height + if require_fully_contained and outside: + raise ValueError("Requested bounds are not fully contained in the GeoTIFF") + col_start, row_start = max(0, col_start), max(0, row_start) + col_stop, row_stop = min(geotiff.width, col_stop), min(geotiff.height, row_stop) + if col_start >= col_stop or row_start >= row_stop: + raise ValueError("Requested bounds do not intersect the GeoTIFF") + return Window( + col_off=col_start, + row_off=row_start, + width=col_stop - col_start, + height=row_stop - row_start, + ) diff --git a/uv.lock b/uv.lock index ea5d705..99db024 100644 --- a/uv.lock +++ b/uv.lock @@ -26,6 +26,15 @@ members = [ "tilebox-workflows", ] +[[package]] +name = "affine" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/98/d2f0bb06385069e799fc7d2870d9e078cfa0fa396dc8a2b81227d0da08b9/affine-2.4.0.tar.gz", hash = "sha256:a24d818d6a836c131976d22f8c27b8d3ca32d0af64c1d8d29deb7bafa4da1eea", size = 17132, upload-time = "2023-01-19T23:44:30.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, +] + [[package]] name = "aiofile" version = "3.9.0" @@ -90,6 +99,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/2b/04b8a15f3a1c77bc79ddf5c73875327f34b4fa75982df2b76e45e402d364/asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933", size = 28702, upload-time = "2026-07-12T03:31:47.542Z" }, ] +[[package]] +name = "async-geotiff" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "affine" }, + { name = "async-tiff" }, + { name = "defusedxml" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pyproj", version = "3.7.2", source = { registry = "https://pypi.org/simple" } }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/51/3ccc7adc768b2e704917c06382ef013401b87e0e0433033580e781d3d647/async_geotiff-0.5.1.tar.gz", hash = "sha256:673e7694a6d8a39a863215e9b1b1761b2b54e2555217c65f53d0042e8eda3e30", size = 26691, upload-time = "2026-05-18T18:13:54.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/b9/c348b19ab3845e177f08f51965db4b70ecd0563b114dbc5dcc4971b20cf9/async_geotiff-0.5.1-py3-none-any.whl", hash = "sha256:68a30a2f131f2dca95fb256c497afd25a8238098be37e068dfa96150a10a381c", size = 34031, upload-time = "2026-05-18T18:13:52.172Z" }, +] + +[[package]] +name = "async-tiff" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "obspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/81/6b8198cbf294ab6f50c7b8fd704b773af595c5f40e738358d40c218df236/async_tiff-0.7.2.tar.gz", hash = "sha256:b2eb1f2671de4e331d1eaef1fd5bb3670e1f26ec7465115be488e60349f2dbce", size = 5249708, upload-time = "2026-05-06T19:03:00.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e2/3f975c66494986edd9c90d5fe8acb1d77e7354b56fa6f0b5d28da8f73516/async_tiff-0.7.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7a5006bfcfccda9a7fe3b2178701dbd04e64eb7fea74c952c277d350c255af18", size = 4054423, upload-time = "2026-05-06T19:02:11.206Z" }, + { url = "https://files.pythonhosted.org/packages/9d/80/0620c7da5465e1e5edbb4b70cd2abaedafd931691cb00c3fa6cb2a419dbb/async_tiff-0.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c41fe8198001decfe66ce0d059711b175494915484b85a9bb3c8f2a98ab24350", size = 3765742, upload-time = "2026-05-06T19:02:13.922Z" }, + { url = "https://files.pythonhosted.org/packages/c3/b5/cca60420f6b17d6ae6f284bbfcda3507433256d18951bd6a0b89022dcee7/async_tiff-0.7.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:0be9c8dd1cc08e7dbeeb04ed9e1bf2c0b96376c46fd13c3070cee92c5ae461de", size = 4026449, upload-time = "2026-05-06T19:02:15.806Z" }, + { url = "https://files.pythonhosted.org/packages/73/cf/44caed1bf95954b3cb44b478ad4d42dca639bb50eb9d52c48d9f087f9e71/async_tiff-0.7.2-cp310-cp310-manylinux_2_28_i686.whl", hash = "sha256:04c47f376ce9de160915d26a4e8b123572e3c01e74bee36b31cf2bfdff35c91a", size = 4105640, upload-time = "2026-05-06T19:02:18.063Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b4/37ad37984dc07d7288d7513752aec5d727ee6257748a11e4a0e4de414e54/async_tiff-0.7.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:243804a56a9b2d7af2e50cab6fdca443a829ab6b678c0e6c8ff78610c0098fdd", size = 4270610, upload-time = "2026-05-06T19:02:20.134Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f8/8a31b5fc877f2d6c6aa9d0a1de9be7dad94426992e473d7baa77b89c9555/async_tiff-0.7.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc6dd9459a63bf2fa55fe074bdb940d2889bc2467c528d56fb9bfba57a1be1", size = 4132026, upload-time = "2026-05-06T19:02:22.01Z" }, + { url = "https://files.pythonhosted.org/packages/7c/90/80b3cab03f0f38ae1d458cfff180930610b2ab27ae63f22146d438301b4b/async_tiff-0.7.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d4f74d3ec990791b08af6992f0c16b64fea938805f8e26a4931d1ffd2d3d8f60", size = 4408974, upload-time = "2026-05-06T19:02:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/40/3e/3d0483ae66ce94c35a23a9982025783f7986183cc561756cdac4cd0a32f6/async_tiff-0.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca3f521d7f3b9303424a141685396044003669f5acfbe10e58aceb10320a7e85", size = 4032660, upload-time = "2026-05-06T19:02:26.358Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8b/1591e69991987b43ccd7faecb843bd601dfb08a35395770c1562aaca73da/async_tiff-0.7.2-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:028e9d629dbe8d830581d8e46b92081a656953db020b424615af3c6e51ed9249", size = 4056302, upload-time = "2026-05-06T19:02:28.816Z" }, + { url = "https://files.pythonhosted.org/packages/c5/58/fc2451333d389da41e8505a8edcc711fcacfb8f038c67a1629d7e87fbdc0/async_tiff-0.7.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:97d77fcad25824321645c3b83da060b1ece827f30e76fe64db74b4df97e5da1d", size = 3764126, upload-time = "2026-05-06T19:02:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/90/76/342bdaff66caa7b4eb6d224bdf8b5f2815b888c665dd51f9b8f83d45316f/async_tiff-0.7.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:62859389a21dc512f5c37fe17729eeeafb66c445374fcf888c1aa2c64bc79ff7", size = 4023878, upload-time = "2026-05-06T19:02:33.374Z" }, + { url = "https://files.pythonhosted.org/packages/79/ec/2f16e32670e9ade6aba6cc0634cc65f232e8f42cdd46e6a883592d8b59ce/async_tiff-0.7.2-cp311-abi3-manylinux_2_28_i686.whl", hash = "sha256:1fb75527b4405107af587e55f4d34e50e8f2f8fe70bfac615e551fbe156cfe11", size = 4106939, upload-time = "2026-05-06T19:02:35.493Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8f/c8a7c48fd966e13ee7390984d43318f71f0eab76889108b3a9621b6ef62a/async_tiff-0.7.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:84ca597d8a6354d7b1ae46d2c41432e0dad36ffd4ad54f45aa29d11857506e9b", size = 4276382, upload-time = "2026-05-06T19:02:37.424Z" }, + { url = "https://files.pythonhosted.org/packages/aa/26/8382fab4dd930c4d96d3a3d243fb0f90750e472ae2a6bbc2c82343df6167/async_tiff-0.7.2-cp311-abi3-musllinux_1_2_i686.whl", hash = "sha256:ed2ca1842f47714a23623125bb864b858db400f9b7af4cae41dfc04f798f2557", size = 4133568, upload-time = "2026-05-06T19:02:39.651Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/b889e197f51c82a9470cb294b81c4713725e646ffb6716d078d4004b9569/async_tiff-0.7.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0cb8ec113b5d33ad020556527ae54f2cb86f7e955fedac718779a4b12404688f", size = 4411500, upload-time = "2026-05-06T19:02:41.473Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/e106fcee3a8cd98d57c8842ba4ff2839fea6ca736970f0899b3162298fc9/async_tiff-0.7.2-cp311-abi3-win_amd64.whl", hash = "sha256:296647adac8b05ae1bacc69feea2e42ce17220d34df1bc419be0daa94364ebd5", size = 4036094, upload-time = "2026-05-06T19:02:43.421Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5f/83b491bbedf430098aec5c9b57d6a723674297a608817c8fb1dddb8f85a4/async_tiff-0.7.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e75dc8f806f60fe738cce85d5f03a014472db48ed4369b5ba03b185396d09772", size = 4055398, upload-time = "2026-05-06T19:02:45.728Z" }, + { url = "https://files.pythonhosted.org/packages/ad/18/509a3541851d014c8a7f4f52032593f8247a28fb7d7ad96e38526222b217/async_tiff-0.7.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:49473e043083a532dd7f1210363e626f964e65be9ce68ed986a2645005ae0f0c", size = 3765618, upload-time = "2026-05-06T19:02:47.564Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/1bb5fa40d4bdbac4b73e0b52094fd45d77fada866ebe8789c126793a9cc4/async_tiff-0.7.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:43b1399633fa5839a705c43b3f01dc9d70de5dd422098130da34a904eb0f5600", size = 4026176, upload-time = "2026-05-06T19:02:49.704Z" }, + { url = "https://files.pythonhosted.org/packages/e0/11/e5919f111e3dd051803c3fc528a0014bd0a25c83688f758de32a75feba03/async_tiff-0.7.2-pp311-pypy311_pp73-manylinux_2_28_i686.whl", hash = "sha256:1b6ab725e061c232e4f0a74cd2aa72024a4459f521deeedc807a82a1408b61e0", size = 4105321, upload-time = "2026-05-06T19:02:51.931Z" }, + { url = "https://files.pythonhosted.org/packages/70/5e/36b4e1aa54be7ecd44f1845ba041f3fb8182c7825639d2e1e051c59bbad1/async_tiff-0.7.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ed74fc8a7368140d1ff56094513ee9e90ef150d83846710e9436e990d4df056b", size = 4271421, upload-time = "2026-05-06T19:02:53.798Z" }, + { url = "https://files.pythonhosted.org/packages/3d/de/15c656564e7a77524b29fc15b825d9b6f37d4f3345c69261ad854ef6803a/async_tiff-0.7.2-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:3df1bf90c3fa40acc5eef42ca9d927467d4b34d5d1281d9779ec77396bd5a964", size = 4131834, upload-time = "2026-05-06T19:02:55.966Z" }, + { url = "https://files.pythonhosted.org/packages/cb/14/f25529fe0de6d97dfc5e6d292b2334f597aa40276b67e19cbe419d0a0e7b/async_tiff-0.7.2-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1609c254845ab6ddb2db02466fa0fdad5a7410fc24eb652e4ea44891216dd2d1", size = 4409385, upload-time = "2026-05-06T19:02:58.364Z" }, +] + [[package]] name = "backports-asyncio-runner" version = "1.2.0" @@ -101,30 +161,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.56" +version = "1.43.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/05/23e1aa8c9e4b0399a61e7fd65c4f9cc0625121f24760e37471f776404abb/boto3-1.43.56.tar.gz", hash = "sha256:57c90df9fb026f2e6ae22530861198130203733c5c9ec4e5cca3a4037f5a8db4", size = 112673, upload-time = "2026-07-24T19:31:48.606Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/c8/ef9de1d7413da3adcdb6363258ba6b5cc703593409d8c1957825b20a69d3/boto3-1.43.59.tar.gz", hash = "sha256:4e9b14f89adc1a533c89312e86d8e00455a6f15d398796d92f9191b06e56b401", size = 112653, upload-time = "2026-07-29T19:33:25.703Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/57/3a960c9f581c00f2a591901b46e035ff79ab3956d16607f12306b3b8d483/boto3-1.43.56-py3-none-any.whl", hash = "sha256:feb699d4ab241ef5c1b80bb58277be2aaad365cd4b672d7817e0bc59ee45131b", size = 140026, upload-time = "2026-07-24T19:31:47.155Z" }, + { url = "https://files.pythonhosted.org/packages/2b/10/c5999e72b020012f2e0ccccf2a15632329edd34cb95b02b1ccfb1712ec08/boto3-1.43.59-py3-none-any.whl", hash = "sha256:58b9635deebf075c1c3d76df78df08eb2979c2a74283194676783a0bff3b4557", size = 140024, upload-time = "2026-07-29T19:33:23.751Z" }, ] [[package]] name = "boto3-stubs" -version = "1.43.56" +version = "1.43.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fb/bc/1f2beda73136f950fc8b5596633803a82b482d793a4a85bdde926bc818b9/boto3_stubs-1.43.56.tar.gz", hash = "sha256:fe3d8049eb74dd7756dadd42f5a73b0f85ec3ed15f75b6e583f893f4563a0d2f", size = 103223, upload-time = "2026-07-24T20:10:25.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/7d/daf6dbd541106b6a62753cda2a0ae566a20f3c251644beb68b6836ae2586/boto3_stubs-1.43.59.tar.gz", hash = "sha256:158858b7733a123a536f9d9665c99d6514528426e35480b95de2684a98183181", size = 103230, upload-time = "2026-07-29T19:55:25.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/62/2ba9e5d2378be5b3e1cfc8b444ee9a1b5d826493feb15df15cf47eae5c69/boto3_stubs-1.43.56-py3-none-any.whl", hash = "sha256:8d3529082b3330e8718986a6a05cc0cd8319f8fddd10104eb7624e1c94b20fd2", size = 70962, upload-time = "2026-07-24T20:10:21.149Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/b619a497acf73a0cc5dc640eea8e147271a07095a07c9bb094ba6df318d3/boto3_stubs-1.43.59-py3-none-any.whl", hash = "sha256:599d8a6166240dd7a5c5103c43bb267e8596641ce1350502fb65cc6ed72c9c58", size = 70963, upload-time = "2026-07-29T19:55:21.52Z" }, ] [package.optional-dependencies] @@ -140,16 +200,16 @@ essential = [ [[package]] name = "botocore" -version = "1.43.56" +version = "1.43.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/cc/7f84a5d3071fe878380e9f610ab36ca87b8cbbc4aa81ba2727f90e1f3ea3/botocore-1.43.56.tar.gz", hash = "sha256:6c01f85f0ff9863076f4c761e74ee3aa96c5ccc1ad09fc1efd62ef8f2d22bf57", size = 15733117, upload-time = "2026-07-24T19:31:38.125Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/37/3712a70796583570a5a2e426163e13762ba5ec615a73e966ad18e5933954/botocore-1.43.59.tar.gz", hash = "sha256:8016da69ecc1d705249a8ef13548d3c95eec87ac1cd26133a8bdfa73ca175be0", size = 15788291, upload-time = "2026-07-29T19:33:14.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/cd/86fe9e659e9699f62f8dd5ecd8c6725474334b23cab8aa71d82b5f56f1a4/botocore-1.43.56-py3-none-any.whl", hash = "sha256:aafc741f1b10f6fd63253eaf6ea029680c1ff436d87e1b8969d62aefa0c76976", size = 15418773, upload-time = "2026-07-24T19:31:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/f6/cd/62d749f824b25c152144665f7c5eb8b5ca8be967a87e0c63577b7d4501ae/botocore-1.43.59-py3-none-any.whl", hash = "sha256:21393c35d23b19d7ba95cc4156b59f4013f80696d667997e1abd9d4e29651708", size = 15471171, upload-time = "2026-07-29T19:33:10.864Z" }, ] [[package]] @@ -718,6 +778,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" }, ] +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -961,75 +1030,75 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.161.6" +version = "6.163.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/2a/f082741a0aa60e2729c8afed2f0c8717aa74648b1ca15ff553516548d262/hypothesis-6.161.6.tar.gz", hash = "sha256:e276f5705cb929ff059785db8fd3a5074c9081529944833a0a4b4f6b9b9303dd", size = 487491, upload-time = "2026-07-27T05:33:24.859Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/59/0b6670cdeea0de95d9b80a22877e8ac9fdc78d48f92fc4bd774f14528017/hypothesis-6.161.6-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a7933cf957fa95ed311be348beb6ebab9a80e4fbcd665bdfdfc82d28703967d8", size = 767824, upload-time = "2026-07-27T05:32:09.953Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/b74f9035406917eebf0bbac4fd075478457fd63e9a610cda7ac5c41ae24f/hypothesis-6.161.6-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:39564c86891a731872d5a2fffa76788ee24cf57dc713d8d140382487f4afc4f4", size = 763367, upload-time = "2026-07-27T05:32:07.164Z" }, - { url = "https://files.pythonhosted.org/packages/94/76/9b406c3c0fc4596ce46d87e5f7e9dc0751550af3425287472e1decff024b/hypothesis-6.161.6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:428951eb9e755d5d061fe110313cd6c625184cb8b1cbe2cd781b117aaf02d3a7", size = 1092657, upload-time = "2026-07-27T05:32:04.657Z" }, - { url = "https://files.pythonhosted.org/packages/5e/96/d10f48cc40b9a6091d460c9a7597f6e2e28a7eab1e794feffdd5d5ee5df1/hypothesis-6.161.6-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c0eee4d6609b0245fb6343c600794387d780d41f298b2120869930adb10b22bf", size = 1121225, upload-time = "2026-07-27T05:32:39.111Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6f/d79b488b8e5f25f01a66ae91d3bb7d50df988eddb5a9cfd34656f946888f/hypothesis-6.161.6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bedf9019cac5272b0c08542e56a124841121368d084cb6f840d0d97fd2dd73c0", size = 1142152, upload-time = "2026-07-27T05:32:24.288Z" }, - { url = "https://files.pythonhosted.org/packages/50/ed/fe23766d0f884582e8b4e55cfd58ac5560f4df11ca8b8daaa2ac271060e8/hypothesis-6.161.6-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:f94c1189c8b4e42a3fa28e89df4fa5b5dfe9289e7aa50381443e459ac003d8ea", size = 1097466, upload-time = "2026-07-27T05:32:15.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/6a/f05dc0b558a05cc5da691a54a75870232425e16b57ec7e7f095387af9668/hypothesis-6.161.6-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ce10197c57f7efd7bf49509d24a4154f2a0e01a9758b57cef1523e289c4858d", size = 1134196, upload-time = "2026-07-27T05:32:44.109Z" }, - { url = "https://files.pythonhosted.org/packages/3f/b0/f43c5f503b5c73776428a37e90c90b5b0a607f275a18b2822ce52618ffdf/hypothesis-6.161.6-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c40f0aed8e8037a5ea4fa9486ea98c0ec3a007419bd7b3dbe14712a3632e028b", size = 1266502, upload-time = "2026-07-27T05:32:42.527Z" }, - { url = "https://files.pythonhosted.org/packages/f4/2a/bb563f9718ac5bf98ef6dbb566fb1d5fae9fd23829ed320fc4c84c4b5985/hypothesis-6.161.6-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fc96ba21133e95a01ab56486b91f83f1e5609ffe55f17618d732cbb29dcb8863", size = 1394242, upload-time = "2026-07-27T05:32:13.435Z" }, - { url = "https://files.pythonhosted.org/packages/f7/26/6dd5a85e9d3556074ebb3988f5bddb3bf52532b2a2bd10ea2a8421a0f852/hypothesis-6.161.6-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:4fad1cb8f49b453890dd5768e14c79e9f6cc8079f1f474e3b9027956985ab2c9", size = 1267017, upload-time = "2026-07-27T05:32:11.746Z" }, - { url = "https://files.pythonhosted.org/packages/26/34/a6cde6774c815bcc98648a17f1ed461345212bcee4492d8c6b0ee72282cf/hypothesis-6.161.6-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7478ba09770f49c160ffbd22b4bbf7d886ac309653fa7116b5edd65e53536b8d", size = 1309114, upload-time = "2026-07-27T05:32:25.889Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b2/4178fe71ee42c383ab669f345e1841a7b880a8fe84e498e78cffdaac87d3/hypothesis-6.161.6-cp310-abi3-win32.whl", hash = "sha256:7e4cc1073644a1b468f46252cc4f135eb51c59d79dbc08acfcd6b4ac290a9ffc", size = 653673, upload-time = "2026-07-27T05:32:01.864Z" }, - { url = "https://files.pythonhosted.org/packages/48/fe/2d91b2e7e06e9737450d7e3fcf09d7efa0c66e57767babc0016ed56cd174/hypothesis-6.161.6-cp310-abi3-win_amd64.whl", hash = "sha256:4a40098bc9c863c2013efb7f9b02f22721e971f795305f24934874a3135e77ef", size = 659826, upload-time = "2026-07-27T05:32:16.609Z" }, - { url = "https://files.pythonhosted.org/packages/a2/67/a0fb537be61560bf26d4faecc9450eb65b54e2dd97b78dcba27d715725a0/hypothesis-6.161.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ce243a3caa26284247eee61992e0e07f85a466fc6f5ac8e72992ba910612dbc2", size = 768537, upload-time = "2026-07-27T05:32:56.462Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a0/73d0229035534c5d27486309bca91513e302072593f9dda82595b40a6c16/hypothesis-6.161.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:635f0286e7fc771e58d82d38280dcedf2d0cd41f353f49cd74be04367683b9d6", size = 764266, upload-time = "2026-07-27T05:32:49.712Z" }, - { url = "https://files.pythonhosted.org/packages/32/79/4374d2948c568f1594b0e7f42d0d69e516aeac53f89560525ae895e3a21d/hypothesis-6.161.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b199468cb6a1afe74bfad8c0f0580ced611341052df478e7d6db1128bce4b07f", size = 1093131, upload-time = "2026-07-27T05:31:56.382Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ce/b5f849f4fe433eb55c8d6195fa566973c489b0deec06f5f37a9f20cba868/hypothesis-6.161.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d585eb08b1319776093cde6a7bc30ce7ee9675001b105e6f4e99f2850ed4620", size = 1142671, upload-time = "2026-07-27T05:32:21.293Z" }, - { url = "https://files.pythonhosted.org/packages/16/5a/c575a65a61639fcbaabbf31f26826cb7cc0228653a1d1c06382784e2761e/hypothesis-6.161.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:42c70d23e59dc4748cb2f8aee131f979bf16e9612b2ee48f7e8fb51c70f4a948", size = 1267068, upload-time = "2026-07-27T05:33:04.635Z" }, - { url = "https://files.pythonhosted.org/packages/78/9d/86bdcb868a742376ef602997947efd3c7774f9d760d1f8f3654363f6c652/hypothesis-6.161.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fbed7de4873833d4a51fbe437dc3edc5f2a76c48666485432233864a530c549", size = 1309437, upload-time = "2026-07-27T05:31:57.653Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f9/ac1993ca99c61505f895877f46d6fea233acb0cbb79ab9324491ac278613/hypothesis-6.161.6-cp310-cp310-win_amd64.whl", hash = "sha256:e8c9de18a5dc42bef1b7c3c65e4f72a5a9c3604a438c721d25c802ec2ded06ac", size = 659673, upload-time = "2026-07-27T05:33:06.487Z" }, - { url = "https://files.pythonhosted.org/packages/0a/78/78d6743e3a4c425412986e1c2595a1fb6f3024cfa61afdd77385d357ee12/hypothesis-6.161.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:959a0db6f42fe01faf393646876ce43f736fb3c0162dbfafd4f4de21bd4aa6c8", size = 768298, upload-time = "2026-07-27T05:32:59.534Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b6/5f05dbc91523f4e6f8b0da75960318faf8287b38298af2b62f1bc5772fcb/hypothesis-6.161.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:081a740171bf5bb3cacf2ee2f895803a3d0e6a9709722e0681ed919c724aaf6c", size = 764079, upload-time = "2026-07-27T05:32:22.777Z" }, - { url = "https://files.pythonhosted.org/packages/e0/43/c9027d8029162069afec7fbdef240b6770f7ed7e086a771ba2b8a5c103d6/hypothesis-6.161.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:359eb0d8bd7c6d2a63ac765fc0b6e0d73bfad9a06aef98ad0f7b2619c17ee8fe", size = 1092970, upload-time = "2026-07-27T05:32:29.116Z" }, - { url = "https://files.pythonhosted.org/packages/04/86/67bf5f444896334dae1b3d52203f573864f2cf7c55400a33b5da3b51e589/hypothesis-6.161.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bbd0a83fc79cb317230bc9122755035a64505071d1e0599e9dfd6371e4ebdb3", size = 1142431, upload-time = "2026-07-27T05:32:18.082Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a8/b86778a6945c69c067a46329d1680aecf3c0eb85e00be82444319e2500bd/hypothesis-6.161.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b76dd9bfca6ac3baed9996b20a12c84058f7c17e5802636939c13ed9b3664f4d", size = 1266744, upload-time = "2026-07-27T05:31:46.283Z" }, - { url = "https://files.pythonhosted.org/packages/d8/b3/3b8a6eb0e78a52efc902a4927d7207d2f9478b70658ad4e38f861d1441df/hypothesis-6.161.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2af997b980ddad5cb8a6d13bd48fcae35f4211e339ff5bb48abe2754d75918b8", size = 1309396, upload-time = "2026-07-27T05:32:53.204Z" }, - { url = "https://files.pythonhosted.org/packages/ee/3d/89a5dc081cdbb6966aa14c8320d74bf2fdce6ac52f27a5c62c2645c762dd/hypothesis-6.161.6-cp311-cp311-win_amd64.whl", hash = "sha256:b9321091368ff8e7955f2178b9df3bf2f1e4dd8d360c173a9414c9d27ed697f4", size = 659521, upload-time = "2026-07-27T05:31:55.117Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/bafca1b91c225d7905d6d2d20882f79591925288c5ca0c6aed4c23e0c421/hypothesis-6.161.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:96c325113320f3fc50ed889a20114b9e317145a66c19544dbd071ba173328690", size = 769412, upload-time = "2026-07-27T05:32:03.332Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/89c48ddfb0bf79cd47ec94d30673ebed554c946816f407bd3c79321e2397/hypothesis-6.161.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ff4e786eb37ef81dd6f78f0238ec787217dbcdb00ccc565c14eee22c676677ba", size = 760979, upload-time = "2026-07-27T05:32:35.558Z" }, - { url = "https://files.pythonhosted.org/packages/00/23/f2fee896448d5d480d3de7d94f8aad2c1c8a0961575d054547f7f81d681d/hypothesis-6.161.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36bd6e336f4a0b259311860306674e83175b56650bd1a54aaa6bb1c878accf03", size = 1091422, upload-time = "2026-07-27T05:31:59.292Z" }, - { url = "https://files.pythonhosted.org/packages/97/92/1f5ac80a195a14837a041ef7232a9760fca52720edea27f543e17a38c9eb/hypothesis-6.161.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce64858eaef339a487c4e7ac1cb8898412a36d06a7872bc44af239a7ab5c2ffd", size = 1141456, upload-time = "2026-07-27T05:33:11.802Z" }, - { url = "https://files.pythonhosted.org/packages/98/38/6c4025549e071e315ab97175a159b5e261afec1cca7d9d4f9cb1c32ef120/hypothesis-6.161.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0323be2fca0754dff630fe1d719d5876c93c5e3be9678ae6fe8557bf689b5868", size = 1264180, upload-time = "2026-07-27T05:31:50.92Z" }, - { url = "https://files.pythonhosted.org/packages/80/37/b31de9ba2fc908529b18a1e386d8167a39883fcab938107b3ee2de02872b/hypothesis-6.161.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18fff0ca1d5dded13c56f430d377bc6585327bb4b987fcf8e37676633569b48c", size = 1308460, upload-time = "2026-07-27T05:32:45.915Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b3/795860dc3817f4cf0bbbcd4ecfba307a77dcc0e85e2d601765364f4989bb/hypothesis-6.161.6-cp312-cp312-win_amd64.whl", hash = "sha256:16da70ffe8507307bd8cea7f2ecdcb7bda1d1980412213ad55571044ba53300f", size = 656950, upload-time = "2026-07-27T05:32:05.965Z" }, - { url = "https://files.pythonhosted.org/packages/09/65/143d649a444037afec0b52aac0b85c16ac0ca99c454f6f8bcbe7202180f1/hypothesis-6.161.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:53e279433bba79a30ec299d281572ca1533309986ed7e1992f9b1c300cbc9173", size = 769306, upload-time = "2026-07-27T05:31:52.397Z" }, - { url = "https://files.pythonhosted.org/packages/6a/cf/7ec64020028051d76160d1e23ec4e2d00ca7772bb387c0a85dc399f38b5b/hypothesis-6.161.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:17bec7179accef79e7bfaed5c68e312a8b440aaf8423e9a67289f8804517ec32", size = 760943, upload-time = "2026-07-27T05:32:57.945Z" }, - { url = "https://files.pythonhosted.org/packages/de/27/2a79199aa9714f786ad9a522dfffb022e8571339fe392f0af5337f40e4f1/hypothesis-6.161.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:badae92f9fe82a882d0e33f48079fdab760fb1c16ac0be282234c0eb89de9f18", size = 1091338, upload-time = "2026-07-27T05:33:22.896Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/5db65f078b00472f69df015c4ebefd5c78da70e633b0563a1ccec39443fa/hypothesis-6.161.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54632d679dc00079ee4aa458131ed58776abb98d96ef27919777ac06b363944", size = 1141284, upload-time = "2026-07-27T05:32:30.926Z" }, - { url = "https://files.pythonhosted.org/packages/78/e6/05f320f26ff4dd3994e2afed775d0399b348999d554b142ccfaa72ff1d60/hypothesis-6.161.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fdad7dfa87d62ffbbeb89197cfd734285275576f8d8106bcd72696446202c0fc", size = 1264201, upload-time = "2026-07-27T05:31:48.378Z" }, - { url = "https://files.pythonhosted.org/packages/51/9b/7826008f4e0baff43da6f38d897c5a8c6e16dafed4f12cee52ead0e2f936/hypothesis-6.161.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a06a6d62efec24bd196ff47dc4dd95c0714e23e870b7f9e2ef2f4429525760ee", size = 1308183, upload-time = "2026-07-27T05:32:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/44/47/0e2a6179a3c2173acf15de897f6b144d0e22657fbbf38e561a7f0f236ce5/hypothesis-6.161.6-cp313-cp313-win_amd64.whl", hash = "sha256:662221ad5961cf746626ca024b1b52474f2debf892bbac2af978aad4f5e2bfbf", size = 656916, upload-time = "2026-07-27T05:32:34.077Z" }, - { url = "https://files.pythonhosted.org/packages/74/32/1b7c11d7b293928078aba04a363f6d51c530d89b470c146b1040ae2f94aa/hypothesis-6.161.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:ecf98cf996b82ca46efa3eebe151c6721c1109df50d34aeb73ad8477d7b85363", size = 769519, upload-time = "2026-07-27T05:32:40.75Z" }, - { url = "https://files.pythonhosted.org/packages/e8/c4/fcaac023585da73fca407932bd11afeaeb0bfd1da174da8ec4225761c8e5/hypothesis-6.161.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc11a56f2b4931ecf25ce578c535c9f6448ed60f112dce9eae09fb0b0d655db1", size = 761082, upload-time = "2026-07-27T05:33:17.442Z" }, - { url = "https://files.pythonhosted.org/packages/ef/41/563549e4f82c360973a38909018bb4b8e46465bb00d4b5b686509df01d8d/hypothesis-6.161.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c2af82180b08fa843b699db7f2b7d0ba9b62fd1e21fb8dd646bb96a16fd352", size = 1091840, upload-time = "2026-07-27T05:32:47.818Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2a/46b5f0a3b46959ced231ff78ea941483f2f571473d6f249c6438967510df/hypothesis-6.161.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:332e8a7b99fa69a2675a726f5136e68545b9e84e0ff744679b14accf62ed2b5d", size = 1141464, upload-time = "2026-07-27T05:33:10.127Z" }, - { url = "https://files.pythonhosted.org/packages/33/25/77c3052c0fcedd2a19bffd7b2b44ea93dac73bd62f1d8b03420d3ac55ee2/hypothesis-6.161.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:160f49f1b7e54e50a3ab67e9772b15df5085ec0f8e624e9acaf66dbaf8422f6e", size = 1264629, upload-time = "2026-07-27T05:33:20.984Z" }, - { url = "https://files.pythonhosted.org/packages/7c/8a/06a54d1be5468089c9688a6f7e3f3e9c65eb24d0046896d9646cfa4ebce9/hypothesis-6.161.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7712075d545604a728007ea085fa10acd785713d6e54c8702dbc0b5bed6d2175", size = 1308485, upload-time = "2026-07-27T05:33:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/8e4688ee36c16502aa09b3d2d771f7413b9d9124448c3483b996228fa110/hypothesis-6.161.6-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:8da289ff744e6225feb222f6d8ece08d77324cbabe2357ab7067632c0c06a359", size = 601010, upload-time = "2026-07-27T05:33:13.62Z" }, - { url = "https://files.pythonhosted.org/packages/2d/2f/1f8451e2f1c18f34a5fad71610887eb81451d09658555c2de1470689ca47/hypothesis-6.161.6-cp314-cp314-win_amd64.whl", hash = "sha256:61ae780388292510ea303f0d79154a4623e8e0c00c49ba981329ec111800bd7c", size = 656829, upload-time = "2026-07-27T05:33:15.325Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/9da7f06a693f0fb0c51c51e843c37210492a03c330699f000d007f505452/hypothesis-6.161.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:6fc0cd5490f69202a8608aa3c3085078c598117049672c6173c4eb8cdfff8b26", size = 768102, upload-time = "2026-07-27T05:31:53.708Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/c26463bbacbe3caa1567a3121be328ee2ae16a91b5cded6f79edbe3e192c/hypothesis-6.161.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e692d51542664b3d431fe835ada7d37630764e7542150bfa987346b03b3b00a1", size = 759547, upload-time = "2026-07-27T05:31:49.746Z" }, - { url = "https://files.pythonhosted.org/packages/46/06/4c926dc8bc4d7c6df7a0321a291dcfcc214665f34a9a073165e9fdbadf75/hypothesis-6.161.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:225b51e0ea315cb70eaee0e0d6506bc5452f1b3f08f48c48953dd2afea006491", size = 1090434, upload-time = "2026-07-27T05:33:02.965Z" }, - { url = "https://files.pythonhosted.org/packages/1c/3c/cc7d2f675b7c44179ba3d343b5fa6b07627365bdc2f0d8c014be5448b85a/hypothesis-6.161.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efac2635c2a690b2afe84e94fceed8c4d3c219fa806340e73fc8f26233dc801", size = 1140375, upload-time = "2026-07-27T05:32:19.866Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f4/662cb8b5ae2a5f92c12892f3654a2c59ce73c25adbacddc6876336c33876/hypothesis-6.161.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac13ee5963380f63f9ffe2de47b7b489a09f8a1e2701e9c816476ff52405ed71", size = 1262843, upload-time = "2026-07-27T05:32:27.546Z" }, - { url = "https://files.pythonhosted.org/packages/7b/49/ee44c0aec2ae4f2e3024c44893b6fa2761cacd4d3b08a3223ea1a1ba8779/hypothesis-6.161.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47ff47cff1fc4b704b805c255bc7c3c64e2c3f7eebdc1bc4c4155df8621cbab3", size = 1307243, upload-time = "2026-07-27T05:32:00.696Z" }, - { url = "https://files.pythonhosted.org/packages/96/4b/725d3eef7be39fb2957eab63ded9c2d338b3ffb52140ec77c449f193fa41/hypothesis-6.161.6-cp314-cp314t-win_amd64.whl", hash = "sha256:981fa76521dd82a05e8749182404f675d3e0ad0b84566c558c8c45d7a8fb70fc", size = 656980, upload-time = "2026-07-27T05:33:01.104Z" }, - { url = "https://files.pythonhosted.org/packages/79/9d/ab53e1be2074ccf42bea03eb97a8ed7e50da392a7ab841e294d9894877b5/hypothesis-6.161.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a890919b3c93aab5d7fbc945d25e16100bd3cf58acfba54664f4f7b2023f4c1", size = 769226, upload-time = "2026-07-27T05:32:08.384Z" }, - { url = "https://files.pythonhosted.org/packages/6b/75/8a08176ef2a44d012490e31315cdad34884c5f157d65e38da9336ca43284/hypothesis-6.161.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f8e3d3c0820b8a71a4804f92e3d2667db2d013c6285e467ac0e098927243e14a", size = 765073, upload-time = "2026-07-27T05:33:19.285Z" }, - { url = "https://files.pythonhosted.org/packages/ae/19/38c24c5011a3825ee27124e742c681ade72cfdbb4500c0afa1e9e1fc485b/hypothesis-6.161.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5eb2a65d803da165ca5faa01ca07d2c47aa41e0a4de7d14b3c74f478f38241a", size = 1093943, upload-time = "2026-07-27T05:32:51.38Z" }, - { url = "https://files.pythonhosted.org/packages/25/72/918c964e22d94c78fc0594a06d8077a469e28dcc2f5ede45a703df72f36d/hypothesis-6.161.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2a58004ed367961bfce1703bee591221d94e28ad8ac15a57f0eecabbe659ac", size = 1143728, upload-time = "2026-07-27T05:32:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/dd/0c/395637e3bcb374f1b513000d2de2690236acbeb2baf33b4b8446155072a5/hypothesis-6.161.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:91bccc94514b865604079a2f5187f02a8e6e855c78ae2127b7fe2e7929a47949", size = 660621, upload-time = "2026-07-27T05:32:54.84Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/61/08/4cbfa0327e9df00f57fc67f91847add2f0dd6c23408935273b095ee9a9f1/hypothesis-6.163.0.tar.gz", hash = "sha256:520480d4bd3a17557616c25923640953e360332c89d012fffcebd69857e674a9", size = 490145, upload-time = "2026-07-28T07:16:46.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/b1/3eea7a422de342bd0095a9672c3e03fe0466e4561a55499a1e01b4a9f098/hypothesis-6.163.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:331906cb029b6b360b8ebac3ec00c3cfa720037fe2efb294a503a1979c9a9a8f", size = 769898, upload-time = "2026-07-28T07:15:19.323Z" }, + { url = "https://files.pythonhosted.org/packages/bc/24/45b5c948c76c16ecf1e4ad1ca4a4a3fce55b3317ee172bc8230ff2956ae1/hypothesis-6.163.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b4ad2134405d5345434c22dea96bbc12c85abcfc3c253a8063dbc9ff01164555", size = 765438, upload-time = "2026-07-28T07:16:04.69Z" }, + { url = "https://files.pythonhosted.org/packages/0f/72/7725039a75b3679dc445a169026b11860a0e67a600c4ffed49a42040a000/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50073f8e63c1e7d3403899755657a990d8bba7b5b5bff66b1c56796d4969bb28", size = 1094699, upload-time = "2026-07-28T07:15:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/dd/fa/bcfa3879f303a302ec6e5f4d35b583924867a0821b42fa275f440f3b8dab/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c5d1e6bad47edf6fb1d7406cf6d67314ac08325c63a49550d782a4596ea302b", size = 1123315, upload-time = "2026-07-28T07:16:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/e7fe2f0658db5182bb1d4b266d17f8f81cf3db82eeba27677ddfea13ac09/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ec3b709508ccd835d8ded1db025b7800618f2289a22a6bfd4927da5f4eb33c", size = 1144220, upload-time = "2026-07-28T07:15:20.617Z" }, + { url = "https://files.pythonhosted.org/packages/90/14/c26f93a4693bfd83d7ba14b7043d986458026f6635d1b157bc2f1c56a59e/hypothesis-6.163.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:7cb3d927360fe73f9a06d646e6082237142ee39c24679c7133d22bf06dd03b45", size = 1099527, upload-time = "2026-07-28T07:16:17.955Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f5/4c1dde28d2e18e169dd6c471ef4bcf12abf3c6a3f4a1744bdabdbdf411ba/hypothesis-6.163.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f3cceb4720a39127622fbf3bcebe1775b894372c53b5edddfdef10bbdeef9ec", size = 1136272, upload-time = "2026-07-28T07:16:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5b/54153509ed42cc17e1b65efe34a0c781f42fb04c902dc5f25486c537ec88/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59f5fdb8addb44c17520a60d50542d9db6ceba577bbf54efefa9c10ee20be140", size = 1268518, upload-time = "2026-07-28T07:15:42.991Z" }, + { url = "https://files.pythonhosted.org/packages/81/86/f86f0d15d91b9cbff3546c1b8891950b2b08c0527e7494133fb2180e1b8b/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe222f50a1898e87a1e7323ab35f9e956278efabe4dd55a1342808206d05ad", size = 1396357, upload-time = "2026-07-28T07:15:26.696Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/c096b6b272f15e17e8e65c6ccfb2e1d167456ff5bbd9db33dca3185199f6/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:56ed585baab75cb98462c57ca88bbdc6a9d935a14118dd572fb476c3ecec2a06", size = 1269128, upload-time = "2026-07-28T07:16:11.902Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9f/0807445874b3083a22c9a14a0ffc31bd0060ef3b408ce4cb31c20779cdf8/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2849c23b2e0fe2eef4c1ec336b01eac7ad7397c49fca43c264f59ec1e6046eac", size = 1311189, upload-time = "2026-07-28T07:16:08.545Z" }, + { url = "https://files.pythonhosted.org/packages/d6/de/3319aa8fcffd1641defc1c5eea1ad646a33d1b62528ef487a89752bc8079/hypothesis-6.163.0-cp310-abi3-win32.whl", hash = "sha256:b2ddcdaf6691101e06dc4a5add7b8c8fdf1e68daba599255a281f3f3550d3331", size = 655743, upload-time = "2026-07-28T07:16:30.966Z" }, + { url = "https://files.pythonhosted.org/packages/8e/48/36bc72910451e6e88b75e59a6ddbf0db34ff61a3b11c9439801dfd5fec20/hypothesis-6.163.0-cp310-abi3-win_amd64.whl", hash = "sha256:4ab0dadc09c537d4ac57e564039dfe7daf09c98375306d54bfc0fd6c218efcca", size = 661902, upload-time = "2026-07-28T07:16:34.407Z" }, + { url = "https://files.pythonhosted.org/packages/fe/4d/f20c3dfbe8cd897e1b33eda6f3107e15e451ef09e7f7a6749878f9d6f09d/hypothesis-6.163.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:67d1593941ede41052b4a35ec25b50d0e280358c7674ef7812d520010e7e8bdf", size = 770601, upload-time = "2026-07-28T07:16:20.056Z" }, + { url = "https://files.pythonhosted.org/packages/29/aa/0c42c477575ed62170df7548f70c85f53cbfafeaf9ee501d995c885d27d1/hypothesis-6.163.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee47c2cb1be03a052ebd3549dad07f636a98b3ccfd7acbe5e17b3b7da0ab9e37", size = 766343, upload-time = "2026-07-28T07:15:13.794Z" }, + { url = "https://files.pythonhosted.org/packages/34/02/1edec861e6837d2d78f8a4ed87dbbb6844dc1c7197209cb8ab1a76cae830/hypothesis-6.163.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3091b28de83c5116e0ccd9a4bcb28ef61d2aace5df91093bb22434fd2350c", size = 1095207, upload-time = "2026-07-28T07:15:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8f/ecbd7356432d4bdeac33cb7285fd905f19d425adb9df10d18e45e733699e/hypothesis-6.163.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae7305ae20fddeea09df317b920c45d3e20bfedbdb041f4db6ca5267c458189", size = 1144733, upload-time = "2026-07-28T07:16:03.147Z" }, + { url = "https://files.pythonhosted.org/packages/23/43/cfc48bb8a33e060c3822cd8b58125e8fd5f4fa9d2eb250eafc50d62eeced/hypothesis-6.163.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9be37b7ddf0af9e3f9112cd133afc34e78a56da1f96db5f2b4fc289fe1c4d1c3", size = 1269169, upload-time = "2026-07-28T07:15:35.737Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2b/62dd7a3589e7d2b38521889f7173c588c966eb7a27d70c61cfa2f4edb0dc/hypothesis-6.163.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58be45d1737bf8c2e10cf29505c0f10f8a23d61bc82e4339182a6c8251cbc2d9", size = 1311512, upload-time = "2026-07-28T07:15:05.833Z" }, + { url = "https://files.pythonhosted.org/packages/7f/94/95d315013c4bea4d244e77cb10cb7097d3fa0fba851ef7a746daf4629e3b/hypothesis-6.163.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2a20e9835d3c4b293a709ee6ef769bcb18c6ed4ef337a9e251c1a9496d5e8be", size = 661788, upload-time = "2026-07-28T07:16:23.768Z" }, + { url = "https://files.pythonhosted.org/packages/63/80/796ac61dddb3ede550ea127e28e027a57a4ea481581c0bb27701fefd655a/hypothesis-6.163.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:213527755f0fc2b1f3721e73fd60023e2752a48f914e3e2df8d35111956ae5c8", size = 770366, upload-time = "2026-07-28T07:15:22.003Z" }, + { url = "https://files.pythonhosted.org/packages/eb/87/53924f322922bcfc05e40c79d432978333827b335fa9efa5fd1e8cbf3c90/hypothesis-6.163.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca1b48bde68c528a79dec2a2859e05035802e5b1c9c3579f388c9de6ed6d0148", size = 766150, upload-time = "2026-07-28T07:15:16.151Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fb/b62480e6510052139d7b2a0219d4ae8bd52ccad0ed74db15dd61330a6962/hypothesis-6.163.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3db868a943c814cc557104712d43bf609adfe5ea9f708f38377d366b4855f8", size = 1095046, upload-time = "2026-07-28T07:15:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d7/fa8aac7b47f41d0fe30fa270c5026f37a0c0c60d7b9a1a93dcc8fcfc50ce/hypothesis-6.163.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4159a1c2560e10de51b1c14956e277eb1b37526c9abef9e87c1e531760486448", size = 1144516, upload-time = "2026-07-28T07:16:32.662Z" }, + { url = "https://files.pythonhosted.org/packages/4f/26/c543c76d8a8b8f58f2d7adf0cb42e4928be3464e95f4fa9d7221b42ea9ce/hypothesis-6.163.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffdda3006a383a48f71a23b4f2b3fae3fe1b09af67925d885985f7ec34d66bcb", size = 1268886, upload-time = "2026-07-28T07:16:36.102Z" }, + { url = "https://files.pythonhosted.org/packages/d9/54/3613ef980cfa60f5c6bfbc533989d6c67b6b5f4e9e638fc6d010a5dd852f/hypothesis-6.163.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3b6cee2afe6c67b31a4a64b63a876e0b020befdc61daabea80f7a0e14f19203a", size = 1311472, upload-time = "2026-07-28T07:16:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/af/0c/cbaebc49fd5807a4b4286dea6e60d5330951e43115d002371bdfc99e70f8/hypothesis-6.163.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a933aca9ebf9daf951d07cf01200c94c321b6ee0b42cc7b67675c9686d914c2", size = 661580, upload-time = "2026-07-28T07:15:32.939Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2c/74e989557efc429b28282cbe754c17fc74495467a72a1c94b5eb734fe374/hypothesis-6.163.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a57352efa938889ea9992667a5014c0fc870d03945de71918574d1cf28276378", size = 771445, upload-time = "2026-07-28T07:16:44.2Z" }, + { url = "https://files.pythonhosted.org/packages/fd/08/3ed2089d8cbeae125ea92879e82b99ea3a8b676c837710019249ccab379f/hypothesis-6.163.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a0c396244c13805edcb73ff467c4c8178ccefc41c4ef5ed00a68e612fd773e9", size = 763070, upload-time = "2026-07-28T07:15:34.318Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/4e85a11f15e8ebd730f3b3e4a5d83653f7da482a4e81fa36958951d89b4a/hypothesis-6.163.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28a6cc1c25a6cc9b6ec079eaabd32ac769994831ecddd57123ce43c9056dcf34", size = 1093500, upload-time = "2026-07-28T07:15:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/1f/29/c4790d2a5e6f48e6be5f868124a0d3c7e1d0102e2ca50503a0718e51289c/hypothesis-6.163.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd312b15044b1c1a0920a5827a830559b2d1fa380851cedf509f8b835309c5b9", size = 1143541, upload-time = "2026-07-28T07:15:44.393Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/01b72694e758449e9b530b72ecaf5f3625c80a3968de0d12ee6e784de22e/hypothesis-6.163.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b839dfd1342bb50570cb0c66b80322307cdb468abf14faf5df4dab022bc1b9ce", size = 1266326, upload-time = "2026-07-28T07:15:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a3/b3997add991e2fc6123b3c8dde3631f9f633db67ba23f5b2567736b50b9e/hypothesis-6.163.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4f5be1482189c7b0a1dcac269fffe97a7d18cc04ac9a9a4d6613212dd87f38b", size = 1310536, upload-time = "2026-07-28T07:15:17.798Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fe/36f576185d63ee0b4d94ed9564415de08baed4937e785b3695c8dd665c9c/hypothesis-6.163.0-cp312-cp312-win_amd64.whl", hash = "sha256:7ca7b20bf38d51e15f7808b0239791c4792b1709ce0c63093acaff56a09c31e6", size = 659021, upload-time = "2026-07-28T07:15:53.143Z" }, + { url = "https://files.pythonhosted.org/packages/58/69/c474a3fa1c33d9a6e059e820221275d9c60eb10085f6164212c291856157/hypothesis-6.163.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a16ebce774755a7a652bd44c62101dc914372ed1a98935969624848c9627b4a4", size = 771335, upload-time = "2026-07-28T07:15:14.988Z" }, + { url = "https://files.pythonhosted.org/packages/54/27/8951688de58314780ba0af5bf2675709009dcc1fb568d244f2a03c4de8e9/hypothesis-6.163.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40dfab6fe6a02a80abef81aebf88e53cd529e3f2f6ba3486b674a67b1f4a3512", size = 763020, upload-time = "2026-07-28T07:15:25.218Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/9eb7451400507f4b5c972c81c2bc57d0009597ea37295519a9645963a50e/hypothesis-6.163.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f28ad27193c1fbcfb52ef2ee63d2b721563525089e80962b4268b306dac45507", size = 1093415, upload-time = "2026-07-28T07:16:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/24/cc/c12c780676c7a4a4051d05e7b278a94bf1bb496ef31882881160f16866c9/hypothesis-6.163.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aac96db8a6c7ee43aba2ee0d3c43893da1fb7c38ed54790c1be2b6d8fd87b96", size = 1143356, upload-time = "2026-07-28T07:16:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c1/61c5ebdb77a3f259803e60a12f56de35f8b6d641a6219f798dcfa69dece3/hypothesis-6.163.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9d23f0f3a14bb6e6f99c793d340196dba4af95ba25bfcab624d1794f540f5e27", size = 1266375, upload-time = "2026-07-28T07:15:11.353Z" }, + { url = "https://files.pythonhosted.org/packages/a2/53/f3a89b4d21d89098d1dec749632aa0fece04f5d17a9e7e91c7f10596d55a/hypothesis-6.163.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b123b4995a7612f1130e2b2362c9a5d0568df887bf7e7bdb45c23af8cd5423c9", size = 1310257, upload-time = "2026-07-28T07:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/06/e7/88e71c4cec0df68aa7a2fb251083c544f30697bd90fb4b1ed19de493ab81/hypothesis-6.163.0-cp313-cp313-win_amd64.whl", hash = "sha256:b268211e625cd550e361fc387bf1db5deb1e9cae0ce4041116f0a0aafeef7c06", size = 658983, upload-time = "2026-07-28T07:16:10.289Z" }, + { url = "https://files.pythonhosted.org/packages/71/df/e3b2f0419cebcc86bba96a357d7ef37790538ed6b09f3183ae01f1fc3d23/hypothesis-6.163.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9105c66ea8dbc108adc42058bb7b65bd953f53ee178bf63bf9ebb0cded6c8c96", size = 771564, upload-time = "2026-07-28T07:15:28.017Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f8/a6f75e61ecd983029f8463bf007498155c4ed33114e6931e7aa3fbaf651e/hypothesis-6.163.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e165f6cc2075059b7c95dac1612bfb25494f72d90f56880e84c288b089f8a896", size = 763164, upload-time = "2026-07-28T07:15:29.973Z" }, + { url = "https://files.pythonhosted.org/packages/5e/07/5193812567f6ca46c1f0cd02dabfd47c85d7c9e3287b8a870fc8150ee462/hypothesis-6.163.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cba5202f74e7e4cdb676d86f26e8cc1b4fdc88f7f58ba73c8ac45b6b22f3070", size = 1093916, upload-time = "2026-07-28T07:16:25.626Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0c/9d61f0e306499d734dc63ebe374b01c5f50be7072fd5e76eed25cc0b86ac/hypothesis-6.163.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7f706df6839dcc53f20833f2933cbcd126fd2fdee7c312e053de49df4b64e44", size = 1143547, upload-time = "2026-07-28T07:15:07.311Z" }, + { url = "https://files.pythonhosted.org/packages/60/b4/2a9eb04c9847ddf7e6b30bba8bc27b7efe5511d368335b6a41657b3f02dd/hypothesis-6.163.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:487ab8ec2f01a225d6a1e2ceadc5290cde2c691952bd2e7f76199cf82e06fb25", size = 1266755, upload-time = "2026-07-28T07:15:41.457Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f3/8d1903fbfcbf48b90bb5590826821afc9b4fc494391ab1fdfab9df23e928/hypothesis-6.163.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ae63dec6d1d467b7f4737455f81a7a82f14a41c14510937fcfbc726a085b5f8", size = 1310560, upload-time = "2026-07-28T07:15:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/47/2b/1a8c0457b44775d0aad369f21cbae8026b772a71aa917d1b956b7349b2a4/hypothesis-6.163.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:31dc46c48aa53c3ec92d03120978ca7f19b9cf96d195ed3fc93503f1433c94a6", size = 603067, upload-time = "2026-07-28T07:16:06.697Z" }, + { url = "https://files.pythonhosted.org/packages/10/56/a9cd947064043035457dec0124ac2437ca72acf358b30cf203a229ad831b/hypothesis-6.163.0-cp314-cp314-win_amd64.whl", hash = "sha256:320b076bf6436f971f1c73ee651e60001226d1b4e341f2c4a1ca87248261ca03", size = 658931, upload-time = "2026-07-28T07:15:37.174Z" }, + { url = "https://files.pythonhosted.org/packages/77/37/2d16317fda0ecd915cb094be9a9e8911106e693ed03a4d680de314711854/hypothesis-6.163.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ab34c61d9249f1a8129cb4276062c04e3e47b5be8de6446e7c7fe11362d6fe43", size = 770142, upload-time = "2026-07-28T07:16:21.827Z" }, + { url = "https://files.pythonhosted.org/packages/3f/65/d80a9bfb7868f2c6c072a684993548391c56e0afa1972f79ee1fe168d91b/hypothesis-6.163.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f2f1b67a48da86d3e41c9445367b49a49f7efdb60fc8b5e3593f05e6afb2efbe", size = 761694, upload-time = "2026-07-28T07:15:31.36Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/c1f2515c638d2637300bb2fd6af129319eae75cdf2ed7804644535f64fb2/hypothesis-6.163.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c084749c115ea7918cf7efa144682783da17eec70d1276689182b871126e715", size = 1092511, upload-time = "2026-07-28T07:15:51.503Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f8/b3cd946308b5a09e52b075792610da310c0b7972bb1e8aa673400b86540c/hypothesis-6.163.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e72e8d5818e5ef8cd6a2191c386e3fd1a6d9e3739cf97289b4d9b5dbc8e38d", size = 1142425, upload-time = "2026-07-28T07:16:15.626Z" }, + { url = "https://files.pythonhosted.org/packages/85/96/b122859e6f7335b54aff759f573a813158d2249488450a75e76462ddaf61/hypothesis-6.163.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5a3ac6c62d49f7fe518dfe7fa924fa03aac839993702207802b0e45f9e1b0dab", size = 1264946, upload-time = "2026-07-28T07:15:23.848Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e0226e8c904b8b4788eb52dacd303c91acbd260904abf64e8f9bad03c88a/hypothesis-6.163.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e568a3d766b7ba8df00e0c33efc4c6530cde14fbc72daabe4824eed211ed7596", size = 1309320, upload-time = "2026-07-28T07:16:29.218Z" }, + { url = "https://files.pythonhosted.org/packages/8c/48/e8bd29fed17c9608524b6a39db4a27b6eec7ce85bda78bba3ee0deebd80e/hypothesis-6.163.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8f22fb8218ba6a452bf9000fc656e1ed57625d17cc8a3871a0fcea3b1b69ebf", size = 659064, upload-time = "2026-07-28T07:16:00.075Z" }, + { url = "https://files.pythonhosted.org/packages/39/db/d4e877b8639bbebedeff4a0511f5fc459c06a31d79a79bf75584eddda8da/hypothesis-6.163.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:002a9709345892279fb0e81b5a05b72d08cfe81f937339827be0d588607ca9b0", size = 771252, upload-time = "2026-07-28T07:15:08.694Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/3492490e997e5c8c9244a56728a623ca817577af3462fca5d1def060a066/hypothesis-6.163.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:52f16840add2eb02c2416f3b83cec4f527b6c19699f2d31eff4859233c715526", size = 767161, upload-time = "2026-07-28T07:15:54.972Z" }, + { url = "https://files.pythonhosted.org/packages/51/73/37a4d4a6f3f0789fb2fddc851da957075fbe223706d1148ccc4dda825171/hypothesis-6.163.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fc895691a2420595506eb17f3a104f2fa9039f013c0770a6cc2743ccaf6fed", size = 1096016, upload-time = "2026-07-28T07:15:09.821Z" }, + { url = "https://files.pythonhosted.org/packages/48/46/20bc7801f8b539334dc0439c28753632a078c96d6732eccf1bd4880644d2/hypothesis-6.163.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef8954e37c80e0c46e6161eef1c72c71059b95250e620a77bd646f6c7a52a2d", size = 1145797, upload-time = "2026-07-28T07:15:39.864Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/4d5c8feb78ed86e698d4e0665d3179eb657ae66d3606ffe096d8055aaa82/hypothesis-6.163.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0838a28e9943d5b834ebae59b02adda76e2cd1e65caa808104c72102052057d", size = 662696, upload-time = "2026-07-28T07:15:38.519Z" }, ] [[package]] @@ -1492,14 +1561,14 @@ wheels = [ [[package]] name = "mypy-boto3-ec2" -version = "1.43.46" +version = "1.43.59" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/73/2a3aaec26a3c4f6d8caffe7ebaa25cd3c46004941e91c751d616ab6768dd/mypy_boto3_ec2-1.43.46.tar.gz", hash = "sha256:5c324fa431e5c36ab460918cf5232c05dfd6c36256880b2afb9c6e9626d9089b", size = 452022, upload-time = "2026-07-10T20:13:54.016Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/12/bc304295cf4bbb06a68ad57e7c1eba1354e67325bf331fee6e01a88b4c3c/mypy_boto3_ec2-1.43.59.tar.gz", hash = "sha256:70093dd275e06c0d58912f4342bfa833fcc91f5fb5a7957983f9e51be168de2d", size = 453094, upload-time = "2026-07-29T19:55:15.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/71/23bd81381d8138cf364efa80b269399de429c715960f6e329367766e0866/mypy_boto3_ec2-1.43.46-py3-none-any.whl", hash = "sha256:a52334d21bb2dd9e89890c6a121206f4ce70dc7bb3d3ba892165b66438549bbe", size = 440540, upload-time = "2026-07-10T20:13:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/57/67/7d516efde4645c0864dc561e367142a47d2ed5d8cea66ca3f70232509e64/mypy_boto3_ec2-1.43.59-py3-none-any.whl", hash = "sha256:9a5d0d2a4d91d353e2c5d9eb8c577d1d3145f84a21b5c7a882aac23c8ef161c5", size = 441548, upload-time = "2026-07-29T19:55:11.486Z" }, ] [[package]] @@ -1561,16 +1630,16 @@ wheels = [ [[package]] name = "niquests" -version = "3.20.1" +version = "3.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "charset-normalizer" }, { name = "urllib3-future" }, { name = "wassima", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f2/6f/c2310e0c0d09ef1c22d235ef82d2d5d16f6b04b3a3702ebe1bc8d1dfbd6c/niquests-3.20.1.tar.gz", hash = "sha256:55951812cf997963bfef8e4f797ff0460ca571e53ec184039b010e12152c0e41", size = 1039594, upload-time = "2026-07-09T06:18:39.488Z" } +sdist = { url = "https://files.pythonhosted.org/packages/03/1c/836ad34fb18fac781722019100e2bffd81eb9a0011d7a91c434a7094460d/niquests-3.21.0.tar.gz", hash = "sha256:5b7d10a05f4c7ed08cede0af74f492ae7a8a5a71291833d029e23365fc3ea80a", size = 1070211, upload-time = "2026-07-29T10:26:24.993Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/25/8c5412b047ab545ac59b59712a840e2d1ce91759aec1afdf47b85c12c748/niquests-3.20.1-py3-none-any.whl", hash = "sha256:71062880739c94d67aead1f8b7972baad3236b97a55a490493729789d1639fc4", size = 213889, upload-time = "2026-07-09T06:18:37.969Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/c08403f905f9aaffc74b483fbb0ad4cc372d1da22cccb4ca771a3e65a185/niquests-3.21.0-py3-none-any.whl", hash = "sha256:dbea441b9e9d5d755e02caa2b57b94cdb97b23d754ead21b80a4ac18ef66805f", size = 230382, upload-time = "2026-07-29T10:26:23.194Z" }, ] [[package]] @@ -1784,6 +1853,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, ] +[[package]] +name = "obspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/94/7a9ad6927cac6ec7680e11772fb692145a05a93bafd80b84f6f0ef12f4e7/obspec-0.1.0.tar.gz", hash = "sha256:b189781a53f82ef8d6abf0c9e77fd4c46ac9f244d5a91eb35ee61c2e2b204a4a", size = 117254, upload-time = "2025-06-25T05:24:00.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/69/96feeac84ce0b871567225c78515f3b557c023e72ed9b4f1833f3662bd6b/obspec-0.1.0-py3-none-any.whl", hash = "sha256:307f0fa2c2998b324ecf0eed6a2a89049a4c40c9b1fa2b5e1af28f0ee72136b3", size = 15231, upload-time = "2025-06-25T05:23:58.735Z" }, +] + [[package]] name = "obstore" version = "0.11.0" @@ -2121,11 +2202,11 @@ wheels = [ [[package]] name = "pip" -version = "26.1.2" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/91/47e7d486260f618783899587af63ccf7980fb60245c3e63dd4571c6b57ad/pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605", size = 1840799, upload-time = "2026-05-31T17:33:58.56Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/96/e6f8e9d9d7b9cc4457092712a7e919c3186aa2c2fa9ffed2c5d29cc947e8/pip-26.2.tar.gz", hash = "sha256:2d8542afcc84cdd8e846c2b36b2861fad1da376dd98f8e7113e9108a3c331690", size = 1848845, upload-time = "2026-07-29T21:57:56.407Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/95/6b5cb3461ea5673ba0995989746db58eb18b91b54dbf331e72f569540946/pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab", size = 1813144, upload-time = "2026-05-31T17:33:56.772Z" }, + { url = "https://files.pythonhosted.org/packages/62/36/a3aed958d60531cb442b7ab4596cda7b3621cfb916f8ae1d6769795c7dc1/pip-26.2-py3-none-any.whl", hash = "sha256:931c303696af6fa3417112103b1cad26890e5a07eccb5b99783700e33f2b8aad", size = 1816475, upload-time = "2026-07-29T21:57:54.763Z" }, ] [[package]] @@ -2391,6 +2472,131 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyproj" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/10/a8480ea27ea4bbe896c168808854d00f2a9b49f95c0319ddcbba693c8a90/pyproj-3.7.1.tar.gz", hash = "sha256:60d72facd7b6b79853f19744779abcd3f804c4e0d4fa8815469db20c9f640a47", size = 226339, upload-time = "2025-02-16T04:28:46.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/a3/c4cd4bba5b336075f145fe784fcaf4ef56ffbc979833303303e7a659dda2/pyproj-3.7.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:bf09dbeb333c34e9c546364e7df1ff40474f9fddf9e70657ecb0e4f670ff0b0e", size = 6262524, upload-time = "2025-02-16T04:27:19.725Z" }, + { url = "https://files.pythonhosted.org/packages/40/45/4fdf18f4cc1995f1992771d2a51cf186a9d7a8ec973c9693f8453850c707/pyproj-3.7.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:6575b2e53cc9e3e461ad6f0692a5564b96e7782c28631c7771c668770915e169", size = 4665102, upload-time = "2025-02-16T04:27:24.428Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d2/360eb127380106cee83569954ae696b88a891c804d7a93abe3fbc15f5976/pyproj-3.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8cb516ee35ed57789b46b96080edf4e503fdb62dbb2e3c6581e0d6c83fca014b", size = 9432667, upload-time = "2025-02-16T04:27:27.04Z" }, + { url = "https://files.pythonhosted.org/packages/76/a5/c6e11b9a99ce146741fb4d184d5c468446c6d6015b183cae82ac822a6cfa/pyproj-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e47c4e93b88d99dd118875ee3ca0171932444cdc0b52d493371b5d98d0f30ee", size = 9259185, upload-time = "2025-02-16T04:27:30.35Z" }, + { url = "https://files.pythonhosted.org/packages/41/56/a3c15c42145797a99363fa0fdb4e9805dccb8b4a76a6d7b2cdf36ebcc2a1/pyproj-3.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3e8d276caeae34fcbe4813855d0d97b9b825bab8d7a8b86d859c24a6213a5a0d", size = 10469103, upload-time = "2025-02-16T04:27:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/ef/73/c9194c2802fefe2a4fd4230bdd5ab083e7604e93c64d0356fa49c363bad6/pyproj-3.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f173f851ee75e54acdaa053382b6825b400cb2085663a9bb073728a59c60aebb", size = 10401391, upload-time = "2025-02-16T04:27:36.051Z" }, + { url = "https://files.pythonhosted.org/packages/c5/1d/ce8bb5b9251b04d7c22d63619bb3db3d2397f79000a9ae05b3fd86a5837e/pyproj-3.7.1-cp310-cp310-win32.whl", hash = "sha256:f550281ed6e5ea88fcf04a7c6154e246d5714be495c50c9e8e6b12d3fb63e158", size = 5869997, upload-time = "2025-02-16T04:27:38.302Z" }, + { url = "https://files.pythonhosted.org/packages/09/6a/ca145467fd2e5b21e3d5b8c2b9645dcfb3b68f08b62417699a1f5689008e/pyproj-3.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:3537668992a709a2e7f068069192138618c00d0ba113572fdd5ee5ffde8222f3", size = 6278581, upload-time = "2025-02-16T04:27:41.051Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/63670fc527e664068b70b7cab599aa38b7420dd009bdc29ea257e7f3dfb3/pyproj-3.7.1-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:a94e26c1a4950cea40116775588a2ca7cf56f1f434ff54ee35a84718f3841a3d", size = 6264315, upload-time = "2025-02-16T04:27:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/25/9d/cbaf82cfb290d1f1fa42feb9ba9464013bb3891e40c4199f8072112e4589/pyproj-3.7.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:263b54ba5004b6b957d55757d846fc5081bc02980caa0279c4fc95fa0fff6067", size = 4666267, upload-time = "2025-02-16T04:27:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/79/53/24f9f9b8918c0550f3ff49ad5de4cf3f0688c9f91ff191476db8979146fe/pyproj-3.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6d6a2ccd5607cd15ef990c51e6f2dd27ec0a741e72069c387088bba3aab60fa", size = 9680510, upload-time = "2025-02-16T04:27:49.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ac/12fab74a908d40b63174dc704587febd0729414804bbfd873cabe504ff2d/pyproj-3.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5dcf24ede53d8abab7d8a77f69ff1936c6a8843ef4fcc574646e4be66e5739", size = 9493619, upload-time = "2025-02-16T04:27:52.65Z" }, + { url = "https://files.pythonhosted.org/packages/c4/45/26311d6437135da2153a178125db5dfb6abce831ce04d10ec207eabac70a/pyproj-3.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c2e7449840a44ce860d8bea2c6c1c4bc63fa07cba801dcce581d14dcb031a02", size = 10709755, upload-time = "2025-02-16T04:27:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/99/52/4ecd0986f27d0e6c8ee3a7bc5c63da15acd30ac23034f871325b297e61fd/pyproj-3.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0829865c1d3a3543f918b3919dc601eea572d6091c0dd175e1a054db9c109274", size = 10642970, upload-time = "2025-02-16T04:27:58.343Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a5/d3bfc018fc92195a000d1d28acc1f3f1df15ff9f09ece68f45a2636c0134/pyproj-3.7.1-cp311-cp311-win32.whl", hash = "sha256:6181960b4b812e82e588407fe5c9c68ada267c3b084db078f248db5d7f45d18a", size = 5868295, upload-time = "2025-02-16T04:28:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/92/39/ef6f06a5b223dbea308cfcbb7a0f72e7b506aef1850e061b2c73b0818715/pyproj-3.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ad0ff443a785d84e2b380869fdd82e6bfc11eba6057d25b4409a9bbfa867970", size = 6279871, upload-time = "2025-02-16T04:28:04.988Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c9/876d4345b8d17f37ac59ebd39f8fa52fc6a6a9891a420f72d050edb6b899/pyproj-3.7.1-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:2781029d90df7f8d431e29562a3f2d8eafdf233c4010d6fc0381858dc7373217", size = 6264087, upload-time = "2025-02-16T04:28:09.036Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/5f8691f8c90e7f402cc80a6276eb19d2ec1faa150d5ae2dd9c7b0a254da8/pyproj-3.7.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d61bf8ab04c73c1da08eedaf21a103b72fa5b0a9b854762905f65ff8b375d394", size = 4669628, upload-time = "2025-02-16T04:28:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/42/ec/16475bbb79c1c68845c0a0d9c60c4fb31e61b8a2a20bc18b1a81e81c7f68/pyproj-3.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04abc517a8555d1b05fcee768db3280143fe42ec39fdd926a2feef31631a1f2f", size = 9721415, upload-time = "2025-02-16T04:28:13.342Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/448f05b15e318bd6bea9a32cfaf11e886c4ae61fa3eee6e09ed5c3b74bb2/pyproj-3.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084c0a475688f934d386c2ab3b6ce03398a473cd48adfda70d9ab8f87f2394a0", size = 9556447, upload-time = "2025-02-16T04:28:15.818Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ae/bd15fe8d8bd914ead6d60bca7f895a4e6f8ef7e3928295134ff9a7dad14c/pyproj-3.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a20727a23b1e49c7dc7fe3c3df8e56a8a7acdade80ac2f5cca29d7ca5564c145", size = 10758317, upload-time = "2025-02-16T04:28:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d9/5ccefb8bca925f44256b188a91c31238cae29ab6ee7f53661ecc04616146/pyproj-3.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bf84d766646f1ebd706d883755df4370aaf02b48187cedaa7e4239f16bc8213d", size = 10771259, upload-time = "2025-02-16T04:28:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/31dedff9c35fa703162f922eeb0baa6c44a3288469a5fd88d209e2892f9e/pyproj-3.7.1-cp312-cp312-win32.whl", hash = "sha256:5f0da2711364d7cb9f115b52289d4a9b61e8bca0da57f44a3a9d6fc9bdeb7274", size = 5859914, upload-time = "2025-02-16T04:28:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/3e/47/c6ab03d6564a7c937590cff81a2742b5990f096cce7c1a622d325be340ee/pyproj-3.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:aee664a9d806612af30a19dba49e55a7a78ebfec3e9d198f6a6176e1d140ec98", size = 6273196, upload-time = "2025-02-16T04:28:25.227Z" }, + { url = "https://files.pythonhosted.org/packages/ef/01/984828464c9960036c602753fc0f21f24f0aa9043c18fa3f2f2b66a86340/pyproj-3.7.1-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:5f8d02ef4431dee414d1753d13fa82a21a2f61494737b5f642ea668d76164d6d", size = 6253062, upload-time = "2025-02-16T04:28:27.861Z" }, + { url = "https://files.pythonhosted.org/packages/68/65/6ecdcdc829811a2c160cdfe2f068a009fc572fd4349664f758ccb0853a7c/pyproj-3.7.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0b853ae99bda66cbe24b4ccfe26d70601d84375940a47f553413d9df570065e0", size = 4660548, upload-time = "2025-02-16T04:28:29.526Z" }, + { url = "https://files.pythonhosted.org/packages/67/da/dda94c4490803679230ba4c17a12f151b307a0d58e8110820405ca2d98db/pyproj-3.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83db380c52087f9e9bdd8a527943b2e7324f275881125e39475c4f9277bdeec4", size = 9662464, upload-time = "2025-02-16T04:28:31.437Z" }, + { url = "https://files.pythonhosted.org/packages/6f/57/f61b7d22c91ae1d12ee00ac4c0038714e774ebcd851b9133e5f4f930dd40/pyproj-3.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b35ed213892e211a3ce2bea002aa1183e1a2a9b79e51bb3c6b15549a831ae528", size = 9497461, upload-time = "2025-02-16T04:28:33.848Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f6/932128236f79d2ac7d39fe1a19667fdf7155d9a81d31fb9472a7a497790f/pyproj-3.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a8b15b0463d1303bab113d1a6af2860a0d79013c3a66fcc5475ce26ef717fd4f", size = 10708869, upload-time = "2025-02-16T04:28:37.34Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0d/07ac7712994454a254c383c0d08aff9916a2851e6512d59da8dc369b1b02/pyproj-3.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:87229e42b75e89f4dad6459200f92988c5998dfb093c7c631fb48524c86cd5dc", size = 10729260, upload-time = "2025-02-16T04:28:40.639Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/9c604bc72c37ba69b867b6df724d6a5af6789e8c375022c952f65b2af558/pyproj-3.7.1-cp313-cp313-win32.whl", hash = "sha256:d666c3a3faaf3b1d7fc4a544059c4eab9d06f84a604b070b7aa2f318e227798e", size = 5855462, upload-time = "2025-02-16T04:28:42.827Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/68a2b7f5fb6400c64aad82d72bcc4bc531775e62eedff993a77c780defd0/pyproj-3.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:d3caac7473be22b6d6e102dde6c46de73b96bc98334e577dfaee9886f102ea2e", size = 6266573, upload-time = "2025-02-16T04:28:44.727Z" }, +] + +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/bd/f205552cd1713b08f93b09e39a3ec99edef0b3ebbbca67b486fdf1abe2de/pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5", size = 6227022, upload-time = "2025-08-14T12:03:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/75/4c/9a937e659b8b418ab573c6d340d27e68716928953273e0837e7922fcac34/pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a", size = 4625810, upload-time = "2025-08-14T12:03:53.808Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/a9f41e814dc4d1dc54e95b2ccaf0b3ebe3eb18b1740df05fe334724c3d89/pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25", size = 9638694, upload-time = "2025-08-14T12:03:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ab/9bdb4a6216b712a1f9aab1c0fcbee5d3726f34a366f29c3e8c08a78d6b70/pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a", size = 9493977, upload-time = "2025-08-14T12:03:57.937Z" }, + { url = "https://files.pythonhosted.org/packages/c9/db/2db75b1b6190f1137b1c4e8ef6a22e1c338e46320f6329bfac819143e063/pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc", size = 10841151, upload-time = "2025-08-14T12:04:00.271Z" }, + { url = "https://files.pythonhosted.org/packages/89/f7/989643394ba23a286e9b7b3f09981496172f9e0d4512457ffea7dc47ffc7/pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5", size = 10751585, upload-time = "2025-08-14T12:04:02.228Z" }, + { url = "https://files.pythonhosted.org/packages/53/6d/ad928fe975a6c14a093c92e6a319ca18f479f3336bb353a740bdba335681/pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a", size = 5908533, upload-time = "2025-08-14T12:04:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/79/e0/b95584605cec9ed50b7ebaf7975d1c4ddeec5a86b7a20554ed8b60042bd7/pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433", size = 6320742, upload-time = "2025-08-14T12:04:06.357Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4d/536e8f93bca808175c2d0a5ac9fdf69b960d8ab6b14f25030dccb07464d7/pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71", size = 6245772, upload-time = "2025-08-14T12:04:08.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + [[package]] name = "pyproject-hooks" version = "1.2.0" @@ -2860,6 +3066,7 @@ dependencies = [ { name = "shapely" }, { name = "tilebox-grpc" }, { name = "tqdm" }, + { name = "typing-extensions" }, { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "xarray", version = "2026.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] @@ -2882,6 +3089,7 @@ requires-dist = [ { name = "shapely", specifier = ">=2" }, { name = "tilebox-grpc", editable = "tilebox-grpc" }, { name = "tqdm", specifier = ">=4.65" }, + { name = "typing-extensions", specifier = ">=4.5" }, { name = "xarray", specifier = ">=2023.11" }, ] @@ -2983,10 +3191,13 @@ source = { editable = "tilebox-storage" } dependencies = [ { name = "aiofile", version = "3.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "aiofile", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "async-geotiff", marker = "python_full_version >= '3.11'" }, { name = "boto3" }, { name = "folium" }, { name = "niquests" }, { name = "obstore" }, + { name = "pyproj", version = "3.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pyproj", version = "3.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "shapely" }, { name = "tilebox-datasets" }, ] @@ -3003,10 +3214,12 @@ dev = [ [package.metadata] requires-dist = [ { name = "aiofile", specifier = ">=3.8" }, + { name = "async-geotiff", marker = "python_full_version >= '3.11'" }, { name = "boto3", specifier = ">=1.37.0" }, { name = "folium", specifier = ">=0.15" }, { name = "niquests", specifier = ">=3.18" }, { name = "obstore", specifier = ">=0.8.0" }, + { name = "pyproj", specifier = ">=3.4" }, { name = "shapely", specifier = ">=2" }, { name = "tilebox-datasets", editable = "tilebox-datasets" }, ] @@ -3134,14 +3347,14 @@ wheels = [ [[package]] name = "tqdm" -version = "4.69.1" +version = "4.70.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dd/84/da0e5038228fa34dfd77c5026b173ed035d2a3ba31f1077590c013de2bff/tqdm-4.69.1.tar.gz", hash = "sha256:2be21080a0ce17e902c2f1baeb6a74bf551b67bbdfa4bc0109fad471d0b4cb0d", size = 793046, upload-time = "2026-07-24T14:22:02.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/50/5817619a0fca56aff06383dbfde7ae017b3ca383915b3f1e4713164273cf/tqdm-4.69.1-py3-none-any.whl", hash = "sha256:0a654b96f7a2660cceb615b56f307ec2bef96c515409014a429a561981ab52b4", size = 675452, upload-time = "2026-07-24T14:22:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, ] [[package]] @@ -3295,88 +3508,88 @@ wheels = [ [[package]] name = "wrapt" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/a4/282c8e64300a59fc834518a54bf0afabb4ff9218b5fa76958b450459a844/wrapt-2.2.2.tar.gz", hash = "sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302", size = 129068, upload-time = "2026-06-20T23:49:44.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/8b/59781d0fe7b0adfbea37f600857de4be68921e454aeecf1a11bda35cdccc/wrapt-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:055e6fcfaa28e58c6a8c247d48b92be9d56f818b7068aa4f22b15b3343a09931", size = 80556, upload-time = "2026-06-20T23:47:28.473Z" }, - { url = "https://files.pythonhosted.org/packages/94/dc/66c61aca927230c9cf97a3cb005c803971a1076ff9f7d61085d035c20085/wrapt-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8374eb6b1a58809211e84ff835a182bb17ab2807a5bfef23204c8cff38178a00", size = 81648, upload-time = "2026-06-20T23:47:30.504Z" }, - { url = "https://files.pythonhosted.org/packages/23/1b/545eee1c18f3af4cf140bb5822b6ef81ebe569df0a63ac109973103a30a5/wrapt-2.2.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:656593bb3f5529f03d27af4136c4d7b11990e470bcbc6fefa5ef218695bece55", size = 152956, upload-time = "2026-06-20T23:47:31.867Z" }, - { url = "https://files.pythonhosted.org/packages/44/a7/6f42a3d03e44dc612a5dcff324e7366075a7857f0be2d49a8cb8a68279b8/wrapt-2.2.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfb00cb7bb22099e2f64b7340fb96113639aa7260c0972af3797ace2297b936c", size = 154771, upload-time = "2026-06-20T23:47:33.352Z" }, - { url = "https://files.pythonhosted.org/packages/bf/55/4d76175aaa97523c38f1d28f79d18ab41a1b116814158a818bc0eba00571/wrapt-2.2.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e7f10ee0bd53673bfd52b67cbce83336fe6cad90d2377b03baf66491d2bbfb91", size = 149460, upload-time = "2026-06-20T23:47:34.712Z" }, - { url = "https://files.pythonhosted.org/packages/84/9b/12e23264d8f4735e8483262f95c5a6b03c3665fd2a84bdf99a45b6a2f4ec/wrapt-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4402f57c5f0d0579599858ffbdd9bf4e3f0972f51096f2bd6cc7dab6b76ee49e", size = 153648, upload-time = "2026-06-20T23:47:36.092Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a3/bcd5ec37289dcd85ecd4d15395a6a6063d60bc45ff94a9d77814e1e54d64/wrapt-2.2.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3a4eb7964ff4643d333c84f880bcf554652b2a1050aebc54ae696327f61acfaf", size = 148502, upload-time = "2026-06-20T23:47:37.623Z" }, - { url = "https://files.pythonhosted.org/packages/f2/be/716d708f607fa70f8a6eb47dff8ee945d5278dfc89ffeeff33039d052e63/wrapt-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e542b7c5af91e2123a8aabf19894319d5ec4268d2a9ffd2f239386133fc47746", size = 152238, upload-time = "2026-06-20T23:47:39.118Z" }, - { url = "https://files.pythonhosted.org/packages/b5/c0/1a48e7e54501274f5d906f18372221b13183b0afbb5b8bb4c7ca0392c0b4/wrapt-2.2.2-cp310-cp310-win32.whl", hash = "sha256:6e7e45b43d3c774d244fe7264378f5a3f0f383bc55a54a9866434e524540110f", size = 77278, upload-time = "2026-06-20T23:47:40.476Z" }, - { url = "https://files.pythonhosted.org/packages/b0/82/9cd69a1af288fbdedf01a10e3c8a0b6890b08c7f3f96d36a213699dbcd94/wrapt-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:955f1d6e72a352e478de8d8b503abe301c5e139a141b62eb0923bd694995025f", size = 80131, upload-time = "2026-06-20T23:47:41.785Z" }, - { url = "https://files.pythonhosted.org/packages/7f/73/8db7e27daef37ae70a53ea62bef7fe80cc51a8b5e9e9181a8be6eb9a999c/wrapt-2.2.2-cp310-cp310-win_arm64.whl", hash = "sha256:b89d8d73c82db2bb7e6090b3afd7973f980d24e905cc34394eab60b884b3bf67", size = 79615, upload-time = "2026-06-20T23:47:43.109Z" }, - { url = "https://files.pythonhosted.org/packages/27/15/0c2d55168707465abfc41f33c0b23d792a5fa9b65c26983606940900a120/wrapt-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f1a2ff355ece6a111ca7a20dc86df6659c9205d3fcee674ca34f2a2854fd4e73", size = 80782, upload-time = "2026-06-20T23:47:44.367Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b5/5c0b093eb48f8a062ef6267d3cb36e9bb1b88440181f6545a383c60efdf8/wrapt-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55b9a899e6fff5444f229d30aa6e9ac92d2216d9d60f33c771b5d76a760d5f8e", size = 81678, upload-time = "2026-06-20T23:47:45.857Z" }, - { url = "https://files.pythonhosted.org/packages/34/f3/de70937472dd3e8a4e6811192f9c6075efdffd4a2cd9b4596bf160f89668/wrapt-2.2.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a2d78c363f97d8bd718ee40432c66395685e9e98528ccaa423c3355d1715a26d", size = 159671, upload-time = "2026-06-20T23:47:47.345Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/40aed2330e7f02ecf74386ffcfef9ccb7108c6a430f15b6a252b663b1bed/wrapt-2.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d619e1eed9bd4f6ed9f24cd61971aa086fa86505289628d464bcf8a2c2e3f328", size = 160785, upload-time = "2026-06-20T23:47:48.759Z" }, - { url = "https://files.pythonhosted.org/packages/45/04/aa5309beed5344b00220ae6b3b24055852192656194c27947bee1736306a/wrapt-2.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:518b0c5e323511ec56a38894802ddd5e1222626484e68efe63f201854ad788e5", size = 153699, upload-time = "2026-06-20T23:47:50.177Z" }, - { url = "https://files.pythonhosted.org/packages/01/df/2def7e99d1fe87eea413f95f671924cdddcb08823b1ffd212748dfa6d062/wrapt-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4bccea5cdecffa9dd70e343741f0e41e0a16619313d04b72f78bb525162ebcd0", size = 159695, upload-time = "2026-06-20T23:47:51.602Z" }, - { url = "https://files.pythonhosted.org/packages/c7/f6/a906d01a2ce12157bad2404957b3e2140da354b8a70b2fa48bbf282871c0/wrapt-2.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:209112cafd963710a05d199aae431d79a28bc76eb8e6d1bbbb8ad24340722cae", size = 152813, upload-time = "2026-06-20T23:47:53.03Z" }, - { url = "https://files.pythonhosted.org/packages/02/49/bc0086292d239575b4c08f4cf8a4079fa58abbad58ec23abf84833a283ed/wrapt-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5a5290e4bf2f332fc29ce72ffb9a2fff678aaac047e2e9f5f7165cd7792e099", size = 158809, upload-time = "2026-06-20T23:47:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/55/83/8fbd034de1f3e907edaa18786d5dd8f6932874edee0826c7cecb5cab03a1/wrapt-2.2.2-cp311-cp311-win32.whl", hash = "sha256:5499236ad1dc116012e2a5dd943f3f31af12fce452128e2bbcbd55a7d3d4d14c", size = 77414, upload-time = "2026-06-20T23:47:55.882Z" }, - { url = "https://files.pythonhosted.org/packages/7e/9c/23695baa331c6de4e874c3d78b8e0bed92e1d2a274e665b29858f6841672/wrapt-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:8636809939152be6ae20a6cef0fed9fe60f411b47847d0426a826884b469e971", size = 80368, upload-time = "2026-06-20T23:47:57.237Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/40cefc342bf89b234a4490d741290fce781774b831aefb39c25471da96c9/wrapt-2.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:5d0a142f7af07caeb5e5da87493162a7b8efa19ba919e550a746f7446e13fb30", size = 79489, upload-time = "2026-06-20T23:47:58.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/85/180b40628b23772692a0c76e8030114e1c0ae068470ed531919f0a5f2a4a/wrapt-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8417fd3c674d3c8023d080292d29301531a12daf8bd938dd419710dd2f464f2b", size = 81484, upload-time = "2026-06-20T23:47:59.924Z" }, - { url = "https://files.pythonhosted.org/packages/94/f2/21c90f2a16689702e2aaff45795b11018dff2c9b1242bac10d225483f676/wrapt-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7070c7472582e31af3dfc2622b2381a0df7435110a9388ed8db5ffbce67efb", size = 82151, upload-time = "2026-06-20T23:48:01.303Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b3/7e6e9fcf4fe7e1b69a49fe6cc5a44e8224bab6283c5233c97e132f14908e/wrapt-2.2.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2e096c9d39a59b35b63c9aacfbbbec2088ff51ff1fc31051acc60a07f42f273a", size = 169828, upload-time = "2026-06-20T23:48:02.719Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/894f132d857ed5a9904d937baf368badcbe5ea9e436e2f1930fe21c9f1f0/wrapt-2.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d1a6050405bf334be33bf66296f113563622972a34900ae6fa60fd283a1a900", size = 171544, upload-time = "2026-06-20T23:48:04.266Z" }, - { url = "https://files.pythonhosted.org/packages/29/de/3c833e03725b477e9ea34028224dd21a48781830101e4e036f77e8b6b102/wrapt-2.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10adb01371408c6de504a6658b9886480f1a4919a83752748a387a504a21df79", size = 160663, upload-time = "2026-06-20T23:48:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/be/27edce350b24e3054d9d047f65f16d4c4d4c1f3f31c4278a1f8a95c723c8/wrapt-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3442eee2a5798f9b451f1b2cd7518ce8b7e28a2a364696c414460a0e295c012a", size = 169387, upload-time = "2026-06-20T23:48:07.243Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c4/9fd9679af8bf38e146652c7f47b6b352c3e5795b4ad1c0b7f94e15ac2aa7/wrapt-2.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6c99012a22f735a85eed7c4b86a3e99c30fdd57d9e115b2b45f796264b58d0bf", size = 158849, upload-time = "2026-06-20T23:48:08.91Z" }, - { url = "https://files.pythonhosted.org/packages/bc/c2/aa6c0c2206803068c6859dabe01f8c84c43744da93d4c67b8946d21655ee/wrapt-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b686cfc008776a3952d6213cb296ed7f45d782a8453936406faa89eac0835ab", size = 168147, upload-time = "2026-06-20T23:48:10.374Z" }, - { url = "https://files.pythonhosted.org/packages/42/63/3eb25da41049d20ae18fcab2dd8b056e02387c4bfa626cbdfb7c3b872e4f/wrapt-2.2.2-cp312-cp312-win32.whl", hash = "sha256:ef2cce266b5b0b07e19fa82e59673b81142b7a3607c8ed1254113d048ed668da", size = 77734, upload-time = "2026-06-20T23:48:11.769Z" }, - { url = "https://files.pythonhosted.org/packages/da/09/0390e008a305360948fa9ce69507d041ac12cb2ee5d28e34467e2ee79391/wrapt-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:abf8c20a2d72ee69e16328b3c91342c446e723bfe48bfcc4dded3b9722ac027f", size = 80585, upload-time = "2026-06-20T23:48:13.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b3/84c445c66969f2d3457276b183a48c91097d59bbef9af6c075366b0f8c36/wrapt-2.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:c6c64c5d02578bc4c4bca4f0aef1504de933c1d5b4ac2710b9131111459506c8", size = 79553, upload-time = "2026-06-20T23:48:14.5Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/f32f4b22c6511173c11d9e541ab4e7d8467a0f1b3455acaf784115d31ff8/wrapt-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69", size = 81296, upload-time = "2026-06-20T23:48:15.881Z" }, - { url = "https://files.pythonhosted.org/packages/72/06/4d117d5d77a9344776c0248b24dae3d3dd2f58e5f765fa08cf887072e719/wrapt-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617", size = 81841, upload-time = "2026-06-20T23:48:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/15/ff/63ad96f98eb58a742b1a20d80f21da88924405910149950b912368150468/wrapt-2.2.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e", size = 167882, upload-time = "2026-06-20T23:48:18.764Z" }, - { url = "https://files.pythonhosted.org/packages/20/1f/8bb62d8933df7acf3247194e6e9fc68edf9d2fa203252c89c94b319dd472/wrapt-2.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d", size = 167411, upload-time = "2026-06-20T23:48:20.315Z" }, - { url = "https://files.pythonhosted.org/packages/17/09/8789dcb09ee1de715727db7521aabbb68ffa68dfade3a49468440cfced49/wrapt-2.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b", size = 158607, upload-time = "2026-06-20T23:48:21.728Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/66e02562d53ee67d841f175e38e3c993c2d78a3e104c576cad61c028b43c/wrapt-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c", size = 166367, upload-time = "2026-06-20T23:48:23.177Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a3/832ac4e41222fb263b3042d42c2f08d305db7d0f0c9b1d3a271a9eede8f6/wrapt-2.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f", size = 157176, upload-time = "2026-06-20T23:48:24.711Z" }, - { url = "https://files.pythonhosted.org/packages/b7/01/1bd5e4d2df9c0178989ac8da9186543465388588ee2ef153e2591accebef/wrapt-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94", size = 167025, upload-time = "2026-06-20T23:48:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/1c/69/583ed25291ab53e1ec117135fb1c33425e2f46d2bc8f29c17f7a94cf4274/wrapt-2.2.2-cp313-cp313-win32.whl", hash = "sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d", size = 77605, upload-time = "2026-06-20T23:48:27.643Z" }, - { url = "https://files.pythonhosted.org/packages/29/68/e69fc6d06e1523c68e0d00f95c9aed1158ce9908ee41603f7f2eae3d5db6/wrapt-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4", size = 80508, upload-time = "2026-06-20T23:48:29.013Z" }, - { url = "https://files.pythonhosted.org/packages/55/21/fe7a393d9e5dc0923bed8f5d857e9dcff210f1fa0888c02cc8f3ffaa55aa/wrapt-2.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac", size = 79565, upload-time = "2026-06-20T23:48:30.429Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e5/c120d13bf5091164f68c3c1657e84f16f57e71d978421b626393ac5bd7eb/wrapt-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5", size = 83264, upload-time = "2026-06-20T23:48:31.807Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/d4a1eb97e0e286625bdf21bc7f702637f9607787ffbbdb5ec14d50c79dbf/wrapt-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec", size = 83791, upload-time = "2026-06-20T23:48:33.482Z" }, - { url = "https://files.pythonhosted.org/packages/18/1e/f060df47755e87b57684cee7bfc1362b204df55fac96ffebc0631b697b79/wrapt-2.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9", size = 203399, upload-time = "2026-06-20T23:48:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/c4/de/2316a757a1abb6453700b79d83e532146dcef2611348282d4d8889792161/wrapt-2.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c", size = 210461, upload-time = "2026-06-20T23:48:36.569Z" }, - { url = "https://files.pythonhosted.org/packages/ed/29/d1160785ae18ca2495a6d82a21154103d74f656c9fd457fb35f6b11b965a/wrapt-2.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194", size = 195313, upload-time = "2026-06-20T23:48:38.175Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2d/7caa9598ae61a9cf0989cc501739cbeeb7d650ab3193cca1407b9af0c6ab/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066", size = 206116, upload-time = "2026-06-20T23:48:39.804Z" }, - { url = "https://files.pythonhosted.org/packages/ac/02/281ea1088b8650d865f311b35cf86fd21df89128e2909714f1161e01c9d0/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20", size = 192668, upload-time = "2026-06-20T23:48:41.346Z" }, - { url = "https://files.pythonhosted.org/packages/be/7d/976e2d5b4b5c5babda40974edd54d0a5585cb60132ed86b46f4b80239b16/wrapt-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af", size = 198891, upload-time = "2026-06-20T23:48:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/59/b7/e47651797c097f75a37e2ce86dcf04048ff576f3a674f7c558df7b5e9622/wrapt-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9", size = 78537, upload-time = "2026-06-20T23:48:44.509Z" }, - { url = "https://files.pythonhosted.org/packages/d1/6f/9fa5d59fb06d890defb5a8f727ce6a14d2932c8760153f96956628559fee/wrapt-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28", size = 82005, upload-time = "2026-06-20T23:48:46.391Z" }, - { url = "https://files.pythonhosted.org/packages/15/80/4c7bd9873d1f9f7d138d93556b500469dbe24f42710b877519c2b9eb380d/wrapt-2.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0", size = 80762, upload-time = "2026-06-20T23:48:47.964Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/7fd9c3f83b2c74cbfc572a0b88aa37431e04bd8aed70d2c0efd3464206de/wrapt-2.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745", size = 81341, upload-time = "2026-06-20T23:48:49.39Z" }, - { url = "https://files.pythonhosted.org/packages/4b/68/1bfa43100dd90d4ef74a05897b86275cf57e1313ca14aae2545bc9f872c9/wrapt-2.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00", size = 81921, upload-time = "2026-06-20T23:48:50.986Z" }, - { url = "https://files.pythonhosted.org/packages/74/eb/df7b7f0b631dbbc750f39be27d8b55f65777d8ac86da80e12be41a644c4b/wrapt-2.2.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc", size = 167713, upload-time = "2026-06-20T23:48:52.598Z" }, - { url = "https://files.pythonhosted.org/packages/4d/9a/d1bd36f6d088c8e652a9383cabbd49af30b8c576302a7eccddbab6963e3f/wrapt-2.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95", size = 166779, upload-time = "2026-06-20T23:48:54.33Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ae/24ffacd4187fac2740a1972093929e836dea092d42c87d728cd98fee11a6/wrapt-2.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33", size = 158407, upload-time = "2026-06-20T23:48:55.944Z" }, - { url = "https://files.pythonhosted.org/packages/a3/ed/974427668249a356051e8d67d47fa54ef6c777f0fcf3bae9d292c047d4b6/wrapt-2.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab", size = 166594, upload-time = "2026-06-20T23:48:57.617Z" }, - { url = "https://files.pythonhosted.org/packages/fb/5f/e1d7c6e4523f78db2fbd7826babd0348da1d5e0834c4f918b9ab5757dfae/wrapt-2.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663", size = 157068, upload-time = "2026-06-20T23:48:59.171Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c1/7ebd1027f00700c0b0233b20aceef2b4784294ed64971424c4a78e069e34/wrapt-2.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d", size = 166470, upload-time = "2026-06-20T23:49:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/99/eb/974e471a6a978b8180186b8a9dc5ae3361ce269a967190b709b8ce17abfb/wrapt-2.2.2-cp314-cp314-win32.whl", hash = "sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56", size = 78062, upload-time = "2026-06-20T23:49:02.327Z" }, - { url = "https://files.pythonhosted.org/packages/49/ec/e1281156cdc7a66693838ad7a0865ad641c74abd337a957d668b575aaffb/wrapt-2.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406", size = 80832, upload-time = "2026-06-20T23:49:03.837Z" }, - { url = "https://files.pythonhosted.org/packages/45/7d/1b6b5ddd94005a2dac97a4490c9838f3154977850d633abcb65b30089437/wrapt-2.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c", size = 80029, upload-time = "2026-06-20T23:49:05.237Z" }, - { url = "https://files.pythonhosted.org/packages/b0/33/9ebcf8aafe91c601127cbd93708c16aa8f688f34a10bf004046803ecdc4f/wrapt-2.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a", size = 83357, upload-time = "2026-06-20T23:49:06.632Z" }, - { url = "https://files.pythonhosted.org/packages/39/38/ec45b635153327b52e52732a0ea980e5f00b7efba65f9e018828f1e69daa/wrapt-2.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063", size = 83794, upload-time = "2026-06-20T23:49:08.098Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ea/1a89e6d3b7a83c3affe5c09cde77792c947e63e4bc85ad84cd5bb9abb0d8/wrapt-2.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f", size = 203362, upload-time = "2026-06-20T23:49:09.811Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/3b58763d9863b5a73771c0d97110f9595d248db454009e07e1535ee905a4/wrapt-2.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81", size = 210449, upload-time = "2026-06-20T23:49:11.521Z" }, - { url = "https://files.pythonhosted.org/packages/2d/6f/17fd9e053103d8be148d20d5d7505facc72d5fe1f9127973904ceaed79cf/wrapt-2.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c", size = 195349, upload-time = "2026-06-20T23:49:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/ef/04/d0d1ccaaa12cb7dccf28a23f0279a608ba498f71e81d949d5ed54bcfd5c1/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff", size = 206099, upload-time = "2026-06-20T23:49:15.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/b3/e8aa07b619890a2aa6cde1931b1887abb08820721b564a5f80b7ca3f3aa0/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5", size = 192728, upload-time = "2026-06-20T23:49:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/1819fb50f0d3c9bd758d8a83b56f1b470dee8b5b8eac8702b7c137cea9d4/wrapt-2.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c", size = 198842, upload-time = "2026-06-20T23:49:18.504Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/e88313f16a99930b899ef970d91c281544a470749a359decad994483bbda/wrapt-2.2.2-cp314-cp314t-win32.whl", hash = "sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca", size = 79059, upload-time = "2026-06-20T23:49:20.107Z" }, - { url = "https://files.pythonhosted.org/packages/a0/4f/ac12fda57a55068a094ec42851fb0a40e8489d8941863d517452de62e507/wrapt-2.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77", size = 82462, upload-time = "2026-06-20T23:49:21.631Z" }, - { url = "https://files.pythonhosted.org/packages/48/a7/df732dac86d9b2027c56bd163dbc883e037b16c3469614752e148d219c61/wrapt-2.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347", size = 81182, upload-time = "2026-06-20T23:49:23.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d2/6317eb6d4554855bbf12d61857774af34747bf88a42c19bf306de67e2fa3/wrapt-2.2.2-py3-none-any.whl", hash = "sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048", size = 61460, upload-time = "2026-06-20T23:49:42.966Z" }, +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/31/5822ce37ca8820c2ed35a498c67c8b37960b9cee2ba437fd32849d0a234c/wrapt-2.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0bb2797048db0956348cb3058c33bc4184614f13231389cfbccc16a5d32780a7", size = 81191, upload-time = "2026-07-28T06:04:04.858Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5a/3c6117938be98754578ab83f5a40d7d0ea2cd2c487dc5cd6027ee7228229/wrapt-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce9f398f868d2b3b27aa2ea4de79645ef9077aeeac8dfc2814b0d542c6a2b87f", size = 82255, upload-time = "2026-07-28T06:04:07.151Z" }, + { url = "https://files.pythonhosted.org/packages/a5/0f/94ae724c5087eb6054c0d63febd7094947dcf302fe058e2e0488102a872b/wrapt-2.3.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad71df7a04dd3497e9302e81f4a7c91bd401ea0e15a9df9029527900f94bee43", size = 155228, upload-time = "2026-07-28T06:04:08.272Z" }, + { url = "https://files.pythonhosted.org/packages/6c/21/1f780bba935dcf697c0c59de9be3a559bbb8e31a53ca3f25422023738432/wrapt-2.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc82c2ccc8e234c844f5303d9f2984b346dcdd53e94823ce8420d2c75b4b9023", size = 157073, upload-time = "2026-07-28T06:04:09.459Z" }, + { url = "https://files.pythonhosted.org/packages/73/31/6c7799d7b6431fcd7e1b83245fb45258a2d2c3a2187fbaecb83572a72d7a/wrapt-2.3.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6e19531ae33c508cea7d84a7edfda01fa86e51b8d1a93a77712c55e6e469152", size = 151594, upload-time = "2026-07-28T06:04:10.784Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/42d670dbfafd49076c6eb2b7d67633d7e1c968e39bfb11a135acb6fac67b/wrapt-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df4ce31150bcd5d9f36f816aac3010ab4f4bf8672ac1d3b0ac7d539ec61c7c02", size = 156069, upload-time = "2026-07-28T06:04:12.316Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d6/c66b4ba4eda49257c84d5c2df26118280f09ca7905aee20d0064db778d13/wrapt-2.3.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e2e692bc0d63f881cf7006730a56bd4e0c2fab5dc318466942805d692b166276", size = 150930, upload-time = "2026-07-28T06:04:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f2/1a3b949c0322fb27396eafd1044328c1cb0400e0b32105d75a3cd03096e7/wrapt-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c8388ba7faf5dbf9ee106bb70d66f257629b1bd98091123e19e8a4553a319199", size = 154525, upload-time = "2026-07-28T06:04:14.698Z" }, + { url = "https://files.pythonhosted.org/packages/12/65/147563a3dfa6e830c857b93b530ebd8c0cd9d540e5914aec8f9b12880c02/wrapt-2.3.0-cp310-cp310-win32.whl", hash = "sha256:e045ff75d7d94900fc32896ed93c45ce2d2cac28c9dead582ff9a5a49d446e35", size = 77879, upload-time = "2026-07-28T06:04:16.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/eb/921405b4dc55d4f8be4c700ef120539fdd75d5fdb50d83bd257171ee18e0/wrapt-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:b4fc96b159af0a3e0faa72475a69d66292bea72a5bed1e1aca1bffbddc3cb2b0", size = 80733, upload-time = "2026-07-28T06:04:17.43Z" }, + { url = "https://files.pythonhosted.org/packages/b6/13/75947450c5bb57795fa86384721cd52c5c4deb0879022f309501a8a85d44/wrapt-2.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:1236fa25173ca964c97422470482e9011b9e3c7ed0d75798b40b3da3b0e0e760", size = 80199, upload-time = "2026-07-28T06:04:18.761Z" }, + { url = "https://files.pythonhosted.org/packages/00/b8/9182e4c618a847be0baccb68e4602b070d0fa22c782cf058f4bc66b32709/wrapt-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ab559e1b2551d23d54db2a0001c6d73bad022a254639561c5f6c382a9d6c2fe", size = 81427, upload-time = "2026-07-28T06:04:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/613cefd9c5977366b1587e61c0b428176d382e6d75b454084c5e58503042/wrapt-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bff9a671bc00709cab5a7f745c592b5671873449db0ee2a569af994f16b29a4d", size = 82360, upload-time = "2026-07-28T06:04:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/71/71/4cd2151a236f44a6e2dd4ed8011838d7ba0be3d656c8bafdfc65a2ed1917/wrapt-2.3.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc648a335d7e01adb3640b25f02fd0ea05886cf04d0af7f4ee902bc7b5e466e8", size = 161700, upload-time = "2026-07-28T06:04:22.723Z" }, + { url = "https://files.pythonhosted.org/packages/49/2c/bc508fee75eb2919ed69769800b09968e4aab16897f909a23f39c81e323f/wrapt-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0077f3d65541925fa83002f967b22ad6550d24813ac64cb905f717194128d9c", size = 162922, upload-time = "2026-07-28T06:04:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e5/04f34d38e66d857dfc2fc4088d60e70c0e422467822defa49b2b4a26e17b/wrapt-2.3.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9790ea25190a4e0fe4cdf4eeb868e9d75f8a024a70a5b6bf9c348a3a2b72e731", size = 156125, upload-time = "2026-07-28T06:04:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/23/41/c35940ea1c423f129ebe4361db853bc80d4def6326242e1206fa15bf94f4/wrapt-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:816877aa749253149f9ecfd2635d4d948ecfa338e1a0311d187b1acb1bb8a3eb", size = 162039, upload-time = "2026-07-28T06:04:27.154Z" }, + { url = "https://files.pythonhosted.org/packages/0e/60/9bda34c3d7d182aa703fe35339ae0ed4c4dad5e5c587f93890143e1f87fb/wrapt-2.3.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d1c2c1b808600d2ea808e6360910a60ed5f409a4011655e10f9164ba0a414a6", size = 155110, upload-time = "2026-07-28T06:04:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ba/60bfd9b1a751f4fcb2d603668fc272d651ccdd339a56acf8c40ad21a0293/wrapt-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5ba1e5e08ddc46130e9682b2c249f2d1dd39bda9106ed4bd401b7519f18f41bd", size = 161089, upload-time = "2026-07-28T06:04:29.959Z" }, + { url = "https://files.pythonhosted.org/packages/0f/32/2bd358c6f4f1305c813479d1e9ba746bebdd794f4a20107ab2b3ee0cbd45/wrapt-2.3.0-cp311-cp311-win32.whl", hash = "sha256:45c9279b373d15649dfa2c2077cb3408ea1a6d3125afbdab9d6b809a66f68e14", size = 78030, upload-time = "2026-07-28T06:04:31.241Z" }, + { url = "https://files.pythonhosted.org/packages/4a/62/ecc969b13b141fef89b888c9760821cb01a86ac8fc953911592c8e1e1522/wrapt-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:195b1842b4122fb54e3cd3dd5b2b4aa49302a5a61da901df0481f5c97aedde84", size = 80944, upload-time = "2026-07-28T06:04:32.655Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3d/9278ada8a2b3f24372b630361e84e9a7de7abc3784634860c26d1c37785a/wrapt-2.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:6db604ef0c67bdb2042ecdfd7b7f037cf09733557ca42360d1018285634f7b98", size = 80074, upload-time = "2026-07-28T06:04:33.811Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4a/d17a0fad1bf1c5f2c887ff71fef75654141b0880bff71d157d955b5bec3a/wrapt-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a45ffae742ce91a16e11cb6c7cd71e7f9994f3cbd283b962ab093f5c6dcf525", size = 82139, upload-time = "2026-07-28T06:04:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/51b92daaf6defb57f4dc56bdcce985400f75c6984a03ca5e78ccac717028/wrapt-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:69e477046f2237ef0bc6547544ee73008dc764ca26eff44f09e976d221b34d5d", size = 82723, upload-time = "2026-07-28T06:04:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, + { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, + { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, + { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, + { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, + { url = "https://files.pythonhosted.org/packages/37/a5/5dc590e863a419930d988f8b7ca3e75a6befcfb10b6003b3a152f3d5f732/wrapt-2.3.0-cp312-cp312-win32.whl", hash = "sha256:fb8e2e6704a1e0b1b989546c69e2688371ef4a07fa5f61bde3eb6211186f5ac1", size = 78323, upload-time = "2026-07-28T06:04:45.484Z" }, + { url = "https://files.pythonhosted.org/packages/51/f9/4a6925a07951df56394f7e6ebe14f69f1c5ef9d87aa63e0839acf15aa63a/wrapt-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cdc021cb0b62471d6aac7f2bd92f3b4658073775f9ee7fcd325c511129e7bcc8", size = 81180, upload-time = "2026-07-28T06:04:47.021Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4f/8b5de0395b2a72216751d41c9861df6facaeb611b619d8810ed2b3b23eb2/wrapt-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:67bfe2485f50368c3fcd2275fc1fd100e350d601e0058921a7c82678a465aeab", size = 80155, upload-time = "2026-07-28T06:04:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6e/0f88a072483e76b881e3fdcd6b6ffb4a5791002514fe541e72b1b73c859a/wrapt-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d3fb71e65b001adfc42684522eeccd9c21d8ba679945abc993439567b66e59f", size = 81960, upload-time = "2026-07-28T06:04:49.622Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ff/b7e2776e7c294075eb712cc9ef573d1b818f393006d09787262b8fc871c4/wrapt-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:51a7a4181c1295774812271fbcd7c909df372bc25579d4ed9eb875caaf0ae86f", size = 82435, upload-time = "2026-07-28T06:04:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, + { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, + { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, + { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d4/354e1725e35a73b2af4fa70a3e024c7a5d1bf1802dfb862dcb668aae0253/wrapt-2.3.0-cp313-cp313-win32.whl", hash = "sha256:10461884b3014fbfc8eb7d09a93c5f246363e6711d9d881f95eb8c27fdef049f", size = 78241, upload-time = "2026-07-28T06:05:00.507Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7e/34c87fa2174848dfee820322aaa318bab08913998ccecc8d2f57b4ad4639/wrapt-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac870cc97b73bb00ac353329e9559a4bebc47c4c86792ed9b23b58c15b6ad838", size = 81113, upload-time = "2026-07-28T06:05:01.839Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/fcc9a530579e008c9478bb565a6cdfbfd33536660f069c8b91a6607c5050/wrapt-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:a65e8db2b4e90c2e7ade931086351c98ef420bf7a94ee08c95ac8a3cbbc43579", size = 80182, upload-time = "2026-07-28T06:05:03.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/50/3864848b95b28ef73e17551fc8dccbff2628a834f52cf26a57f9c419fb83/wrapt-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:fd1f2f557dd3491fe75905e578f4db967393d40d1a8f468edc4d40ac7f2d5944", size = 83921, upload-time = "2026-07-28T06:05:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/3b/4c/3d1921a60c3e8c71c540ff136e6a47a1fbccf7f671e818394889f7871d9c/wrapt-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9f5d2aec29dfc76c37e23897dee92766a3fd4f3bff3ae7fc9c6b4bf37d8c1360", size = 84412, upload-time = "2026-07-28T06:05:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, + { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, + { url = "https://files.pythonhosted.org/packages/13/9b/4fc042ceb757866dd4a5fc057b3b736f2b360d3703ce9f830d83dc9226e0/wrapt-2.3.0-cp313-cp313t-win32.whl", hash = "sha256:7ebb274aba688b043429eb1500ff8a76ce0cb8ac0812ca3e301f06247b8722b3", size = 79178, upload-time = "2026-07-28T06:05:16.469Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ff/b94878f8eed809ca042685276bcea9f24e8c2ca7c9653bb80bbb920a68a5/wrapt-2.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c4bded758ad6f03b965830944a2f0bc5b2eb3767fe5a7310134315d1a6610e98", size = 82634, upload-time = "2026-07-28T06:05:18.026Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/663e1de5332a71685a729754312d327d4cada767c36e1c5a2db4c8de49e6/wrapt-2.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:d2cc64539da63e39ffb9c7ede849b6e8ddaaf7b3876b5cfb04efd85a5f3f4eb6", size = 81387, upload-time = "2026-07-28T06:05:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/58/10/b073beaea89bc0d3670a75ff51139430a54b6af7ba7796507730634536dd/wrapt-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea52a0d0f08c584943d5764be0e84efa912c8da23c23e1e285ff2f5641c18fcc", size = 81978, upload-time = "2026-07-28T06:05:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/b3/31/0916d9cebf848ed3f1a0c1888faee421747df77331e4db2bc527a9a85988/wrapt-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd85b0aa88efdb189d6ae2f35f4526943a8f091c38599c9c31478241c819e6a1", size = 82518, upload-time = "2026-07-28T06:05:22.562Z" }, + { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, + { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/016104650d4e572fa91506eb396b3dd8efbccc9284fdc1c9479c3d21db28/wrapt-2.3.0-cp314-cp314-win32.whl", hash = "sha256:2935d5454b3f179a29b12cf390ee47246740ba2c3a7545b1b46ba31a5f2a4a0b", size = 78700, upload-time = "2026-07-28T06:05:33.391Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/6fdc20a9f2ca304748b3f0819cbf377d55260562777bf0b615431bc3c181/wrapt-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:cc2cea812e5cb179a796b766747e7d3b21088760d8deb95676d482b8c8e6fa7d", size = 81422, upload-time = "2026-07-28T06:05:34.774Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a4/9cbd53bf05746bea2c392af39cb052427a8ec95cbd494d930733d8f44681/wrapt-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:22cc5c0a717bd4da87018ae0bffd4c19c6fb679d3ff357216ba566ab26c76cab", size = 80639, upload-time = "2026-07-28T06:05:36.228Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/6c5e4a0f66ea0d2b2dd267e8dd05a0014eea56840b3c8595d40b0a5d1f91/wrapt-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a6b5984cd65dd639546f0eb4b8eacf1c31cb2fe9fb5c27bffe240987cdb2cf84", size = 84030, upload-time = "2026-07-28T06:05:37.714Z" }, + { url = "https://files.pythonhosted.org/packages/6a/eb/a1aedf03283bc9cbf8a1783995ddc54e3c5a86878f19002d2c428494f4c5/wrapt-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c88abcf53daef80e01a75c7530e727fa6e2c1888fe83e3dcdba4c96216a1f5c7", size = 84419, upload-time = "2026-07-28T06:05:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, + { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, + { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, + { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/cd/25/7860927edba06b758b8852a6f02e832be715563c67a6795d94350bc81099/wrapt-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:629d73378082c00a8173031f9fb30a3ac6abbc894a5bfdfae71fabc60642d501", size = 79685, upload-time = "2026-07-28T06:05:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0f/270bafe92fde3b069a39bc01e39ee79340895b335640df861d43d2a51885/wrapt-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:42869085687f0aefd57c0f636c3f9354f8ffb321a8ba9cb52d19beb796e561c5", size = 83104, upload-time = "2026-07-28T06:05:52.405Z" }, + { url = "https://files.pythonhosted.org/packages/55/b3/af176d79a8515a8a720eccdad9a96f6e31a30abf2865430c8c42adf2fd13/wrapt-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b1e5aa486e269b00ed35e64771c7d0ab8096cfd2643405ca8cd60ebedc099a51", size = 81774, upload-time = "2026-07-28T06:05:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, ] [[package]] From e37d741f412c91284c2aa2fe53c8565927f083ce Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Thu, 30 Jul 2026 10:54:14 +0200 Subject: [PATCH 2/7] Generate protobuf-py STAC messages --- buf.gen.datasets-bufpy.yaml | 13 + tilebox-datasets/pyproject.toml | 1 + .../tilebox/datasets/datasets/__init__.py | 1 + .../datasets/datasets/stac/__init__.py | 1 + .../datasets/datasets/stac/v1/__init__.py | 1 + .../datasets/stac/v1/asset_metadata_pb.py | 491 +++++++++++ .../datasets/datasets/stac/v1/asset_pb.py | 763 ++++++++++++++++++ .../datasets/stac/v1/authentication_pb.py | 410 ++++++++++ .../datasets/datasets/stac/v1/core_pb.py | 451 +++++++++++ .../datasets/stac/v1/processing_pb.py | 60 ++ .../datasets/datasets/stac/v1/product_pb.py | 113 +++ .../datasets/datasets/stac/v1/sar_pb.py | 307 +++++++ .../datasets/datasets/stac/v1/satellite_pb.py | 186 +++++ .../datasets/datasets/stac/v1/storage_pb.py | 214 +++++ tilebox-grpc/pyproject.toml | 3 +- tools/generate_protobuf.py | 1 + uv.lock | 80 +- 17 files changed, 3056 insertions(+), 40 deletions(-) create mode 100644 buf.gen.datasets-bufpy.yaml create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/__init__.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/core_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/processing_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/product_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/sar_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/satellite_pb.py create mode 100644 tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb.py diff --git a/buf.gen.datasets-bufpy.yaml b/buf.gen.datasets-bufpy.yaml new file mode 100644 index 0000000..f63bb4c --- /dev/null +++ b/buf.gen.datasets-bufpy.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://www.schemastore.org/buf.gen.json +version: v2 +plugins: + - remote: buf.build/bufbuild/py:v0.2.0 + out: tilebox-datasets/tilebox/datasets + opt: init_files=false +inputs: + - module: buf.build/tilebox/api + paths: + - "datasets/stac/v1" + # Keep this experimental package validation-blind, matching the existing generated clients. + exclude_types: + - "buf.validate.**" diff --git a/tilebox-datasets/pyproject.toml b/tilebox-datasets/pyproject.toml index 8785e0b..e66c444 100644 --- a/tilebox-datasets/pyproject.toml +++ b/tilebox-datasets/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "shapely>=2", "promise>=2.3", "typing-extensions>=4.5", + "protobuf-py>=0.1.1", ] [dependency-groups] diff --git a/tilebox-datasets/tilebox/datasets/datasets/__init__.py b/tilebox-datasets/tilebox/datasets/datasets/__init__.py index e69de29..9d48db4 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/__init__.py +++ b/tilebox-datasets/tilebox/datasets/datasets/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/__init__.py b/tilebox-datasets/tilebox/datasets/datasets/stac/__init__.py index e69de29..9d48db4 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/__init__.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/__init__.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/__init__.py new file mode 100644 index 0000000..9d48db4 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py new file mode 100644 index 0000000..55589d1 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py @@ -0,0 +1,491 @@ +# Generated from datasets/stac/v1/asset_metadata.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_EOPropertiesFields: TypeAlias = Literal["common_name", "center_wavelength", "full_width_half_max", "solar_illumination"] + +class EOProperties(Message[_EOPropertiesFields]): + """ + EOProperties contains the EO 2.0 fields that are useful at asset or band level. + eo:cloud_cover and eo:snow_cover are intentionally omitted: they are typically Item properties and should become + dedicated top-level Tilebox dataset fields instead of being embedded in Assets. + + ```proto + message datasets.stac.v1.EOProperties + ``` + + Attributes: + common_name: + ```proto + datasets.stac.v1.EOCommonName common_name = 1; + ``` + center_wavelength: + Spectral values are expressed in micrometres. + + ```proto + optional double center_wavelength = 2; + ``` + full_width_half_max: + ```proto + optional double full_width_half_max = 3; + ``` + solar_illumination: + ```proto + optional double solar_illumination = 4; + ``` + """ + + __slots__ = ("common_name", "center_wavelength", "full_width_half_max", "solar_illumination") + + if TYPE_CHECKING: + + def __init__( + self, + *, + common_name: EOCommonName | None = None, + center_wavelength: float | None = None, + full_width_half_max: float | None = None, + solar_illumination: float | None = None, + ) -> None: + pass + + common_name: EOCommonName + center_wavelength: float + full_width_half_max: float + solar_illumination: float + +_RasterPropertiesFields: TypeAlias = Literal["sampling", "scale", "offset", "spatial_resolution"] + +class RasterProperties(Message[_RasterPropertiesFields]): + """ + RasterProperties contains the Raster 2.0 fields used by the supported Items. + + ```proto + message datasets.stac.v1.RasterProperties + ``` + + Attributes: + sampling: + ```proto + datasets.stac.v1.RasterSampling sampling = 1; + ``` + scale: + ```proto + optional double scale = 2; + ``` + offset: + ```proto + optional double offset = 3; + ``` + spatial_resolution: + Spatial resolution is expressed in metres. + + ```proto + optional double spatial_resolution = 4; + ``` + """ + + __slots__ = ("sampling", "scale", "offset", "spatial_resolution") + + if TYPE_CHECKING: + + def __init__( + self, + *, + sampling: RasterSampling | None = None, + scale: float | None = None, + offset: float | None = None, + spatial_resolution: float | None = None, + ) -> None: + pass + + sampling: RasterSampling + scale: float + offset: float + spatial_resolution: float + +_ClassificationClassFields: TypeAlias = Literal["value", "description", "name", "title", "color_hint", "nodata", "percentage", "count"] + +class ClassificationClass(Message[_ClassificationClassFields]): + """ + ClassificationClass contains all Classification 2.0 class fields. Bit-field definitions are intentionally deferred. + + ```proto + message datasets.stac.v1.ClassificationClass + ``` + + Attributes: + value: + ```proto + optional int64 value = 1; + ``` + description: + ```proto + optional string description = 2; + ``` + name: + ```proto + optional string name = 3; + ``` + title: + ```proto + optional string title = 4; + ``` + color_hint: + ```proto + optional string color_hint = 5; + ``` + nodata: + ```proto + optional bool nodata = 6; + ``` + percentage: + ```proto + optional double percentage = 7; + ``` + count: + ```proto + optional uint64 count = 8; + ``` + """ + + __slots__ = ("value", "description", "name", "title", "color_hint", "nodata", "percentage", "count") + + if TYPE_CHECKING: + + def __init__( + self, + *, + value: int | None = None, + description: str | None = None, + name: str | None = None, + title: str | None = None, + color_hint: str | None = None, + nodata: bool | None = None, + percentage: float | None = None, + count: int | None = None, + ) -> None: + pass + + value: int + description: str + name: str + title: str + color_hint: str + nodata: bool + percentage: float + count: int + +_ProjectionFields: TypeAlias = Literal["code", "bbox", "shape", "transform"] + +class Projection(Message[_ProjectionFields]): + """ + Projection contains the Projection 2.0 fields used by the supported Items. + + ```proto + message datasets.stac.v1.Projection + ``` + + Attributes: + code: + Explicit presence distinguishes a missing code from a value. A STAC JSON null is treated as missing. + + ```proto + optional string code = 1; + ``` + bbox: + Four values describe a 2D bbox and six values describe a 3D bbox. + + ```proto + repeated double bbox = 2 [packed = true]; + ``` + shape: + Exactly two integers in Y, X order. + + ```proto + repeated int64 shape = 3 [packed = true]; + ``` + transform: + Six values describe a 2D affine transform and nine values describe a 3D transform. + + ```proto + repeated double transform = 4 [packed = true]; + ``` + """ + + __slots__ = ("code", "bbox", "shape", "transform") + + if TYPE_CHECKING: + + def __init__( + self, + *, + code: str | None = None, + bbox: list[float] | None = None, + shape: list[int] | None = None, + transform: list[float] | None = None, + ) -> None: + pass + + code: str + bbox: list[float] + shape: list[int] + transform: list[float] + +_ViewFields: TypeAlias = Literal["incidence_angle", "azimuth"] + +class View(Message[_ViewFields]): + """ + View contains the View 1.1 fields used by the supported Items. + + ```proto + message datasets.stac.v1.View + ``` + + Attributes: + incidence_angle: + ```proto + optional double incidence_angle = 1; + ``` + azimuth: + ```proto + optional double azimuth = 2; + ``` + """ + + __slots__ = ("incidence_angle", "azimuth") + + if TYPE_CHECKING: + + def __init__( + self, + *, + incidence_angle: float | None = None, + azimuth: float | None = None, + ) -> None: + pass + + incidence_angle: float + azimuth: float + +_FileFields: TypeAlias = Literal["checksum", "size", "local_path"] + +class File(Message[_FileFields]): + """ + File contains the File 2.1 fields used by the supported Items. + + ```proto + message datasets.stac.v1.File + ``` + + Attributes: + checksum: + Decoded binary multihash. STAC JSON represents this as lowercase hexadecimal. + + ```proto + optional bytes checksum = 1; + ``` + size: + ```proto + optional uint64 size = 2; + ``` + local_path: + ```proto + optional string local_path = 3; + ``` + """ + + __slots__ = ("checksum", "size", "local_path") + + if TYPE_CHECKING: + + def __init__( + self, + *, + checksum: bytes | None = None, + size: int | None = None, + local_path: str | None = None, + ) -> None: + pass + + checksum: bytes + size: int + local_path: str + +class EOCommonName(Enum): + """ + EOCommonName is the closed EO 2.0 common-name vocabulary. + + ```proto + enum datasets.stac.v1.EOCommonName + ``` + + Attributes: + UNSPECIFIED: + ```proto + EO_COMMON_NAME_UNSPECIFIED = 0 + ``` + PAN: + ```proto + EO_COMMON_NAME_PAN = 1 + ``` + COASTAL: + ```proto + EO_COMMON_NAME_COASTAL = 2 + ``` + BLUE: + ```proto + EO_COMMON_NAME_BLUE = 3 + ``` + GREEN: + ```proto + EO_COMMON_NAME_GREEN = 4 + ``` + GREEN05: + ```proto + EO_COMMON_NAME_GREEN05 = 5 + ``` + YELLOW: + ```proto + EO_COMMON_NAME_YELLOW = 6 + ``` + RED: + ```proto + EO_COMMON_NAME_RED = 7 + ``` + REDEDGE: + ```proto + EO_COMMON_NAME_REDEDGE = 8 + ``` + REDEDGE071: + ```proto + EO_COMMON_NAME_REDEDGE071 = 9 + ``` + REDEDGE075: + ```proto + EO_COMMON_NAME_REDEDGE075 = 10 + ``` + REDEDGE078: + ```proto + EO_COMMON_NAME_REDEDGE078 = 11 + ``` + NIR: + ```proto + EO_COMMON_NAME_NIR = 12 + ``` + NIR08: + ```proto + EO_COMMON_NAME_NIR08 = 13 + ``` + NIR09: + ```proto + EO_COMMON_NAME_NIR09 = 14 + ``` + CIRRUS: + ```proto + EO_COMMON_NAME_CIRRUS = 15 + ``` + SWIR16: + ```proto + EO_COMMON_NAME_SWIR16 = 16 + ``` + SWIR22: + ```proto + EO_COMMON_NAME_SWIR22 = 17 + ``` + LWIR: + ```proto + EO_COMMON_NAME_LWIR = 18 + ``` + LWIR11: + ```proto + EO_COMMON_NAME_LWIR11 = 19 + ``` + LWIR12: + ```proto + EO_COMMON_NAME_LWIR12 = 20 + ``` + """ + + UNSPECIFIED = 0 + PAN = 1 + COASTAL = 2 + BLUE = 3 + GREEN = 4 + GREEN05 = 5 + YELLOW = 6 + RED = 7 + REDEDGE = 8 + REDEDGE071 = 9 + REDEDGE075 = 10 + REDEDGE078 = 11 + NIR = 12 + NIR08 = 13 + NIR09 = 14 + CIRRUS = 15 + SWIR16 = 16 + SWIR22 = 17 + LWIR = 18 + LWIR11 = 19 + LWIR12 = 20 + +class RasterSampling(Enum): + """ + RasterSampling contains the closed Raster 2.0 raster:sampling vocabulary. + + ```proto + enum datasets.stac.v1.RasterSampling + ``` + + Attributes: + UNSPECIFIED: + ```proto + RASTER_SAMPLING_UNSPECIFIED = 0 + ``` + AREA: + ```proto + RASTER_SAMPLING_AREA = 1 + ``` + POINT: + ```proto + RASTER_SAMPLING_POINT = 2 + ``` + """ + + UNSPECIFIED = 0 + AREA = 1 + POINT = 2 + + +_DESC = file_desc( + b'\n%datasets/stac/v1/asset_metadata.proto\x12\x10datasets.stac.v1"\xef\x01\n\x0cEOProperties\x12?\n\x0bcommon_name\x18\x01 \x01(\x0e2\x1e.datasets.stac.v1.EOCommonNameR\ncommonName\x122\n\x11center_wavelength\x18\x02 \x01(\x01R\x10centerWavelengthB\x05\xaa\x01\x02\x08\x01\x124\n\x13full_width_half_max\x18\x03 \x01(\x01R\x10fullWidthHalfMaxB\x05\xaa\x01\x02\x08\x01\x124\n\x12solar_illumination\x18\x04 \x01(\x01R\x11solarIlluminationB\x05\xaa\x01\x02\x08\x01"\xc2\x01\n\x10RasterProperties\x12<\n\x08sampling\x18\x01 \x01(\x0e2 .datasets.stac.v1.RasterSamplingR\x08sampling\x12\x1b\n\x05scale\x18\x02 \x01(\x01R\x05scaleB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06offset\x18\x03 \x01(\x01R\x06offsetB\x05\xaa\x01\x02\x08\x01\x124\n\x12spatial_resolution\x18\x04 \x01(\x01R\x11spatialResolutionB\x05\xaa\x01\x02\x08\x01"\x9c\x02\n\x13ClassificationClass\x12\x1b\n\x05value\x18\x01 \x01(\x03R\x05valueB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x02 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x04name\x18\x03 \x01(\tR\x04nameB\x05\xaa\x01\x02\x08\x01\x12\x1b\n\x05title\x18\x04 \x01(\tR\x05titleB\x05\xaa\x01\x02\x08\x01\x12$\n\ncolor_hint\x18\x05 \x01(\tR\tcolorHintB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06nodata\x18\x06 \x01(\x08R\x06nodataB\x05\xaa\x01\x02\x08\x01\x12%\n\npercentage\x18\x07 \x01(\x01R\npercentageB\x05\xaa\x01\x02\x08\x01\x12\x1b\n\x05count\x18\x08 \x01(\x04R\x05countB\x05\xaa\x01\x02\x08\x01"o\n\nProjection\x12\x19\n\x04code\x18\x01 \x01(\tR\x04codeB\x05\xaa\x01\x02\x08\x01\x12\x12\n\x04bbox\x18\x02 \x03(\x01R\x04bbox\x12\x14\n\x05shape\x18\x03 \x03(\x03R\x05shape\x12\x1c\n\ttransform\x18\x04 \x03(\x01R\ttransform"W\n\x04View\x12.\n\x0fincidence_angle\x18\x01 \x01(\x01R\x0eincidenceAngleB\x05\xaa\x01\x02\x08\x01\x12\x1f\n\x07azimuth\x18\x02 \x01(\x01R\x07azimuthB\x05\xaa\x01\x02\x08\x01"j\n\x04File\x12!\n\x08checksum\x18\x01 \x01(\x0cR\x08checksumB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x04size\x18\x02 \x01(\x04R\x04sizeB\x05\xaa\x01\x02\x08\x01\x12$\n\nlocal_path\x18\x03 \x01(\tR\tlocalPathB\x05\xaa\x01\x02\x08\x01*\xc9\x04\n\x0cEOCommonName\x12\x1e\n\x1aEO_COMMON_NAME_UNSPECIFIED\x10\x00\x12\x16\n\x12EO_COMMON_NAME_PAN\x10\x01\x12\x1a\n\x16EO_COMMON_NAME_COASTAL\x10\x02\x12\x17\n\x13EO_COMMON_NAME_BLUE\x10\x03\x12\x18\n\x14EO_COMMON_NAME_GREEN\x10\x04\x12\x1a\n\x16EO_COMMON_NAME_GREEN05\x10\x05\x12\x19\n\x15EO_COMMON_NAME_YELLOW\x10\x06\x12\x16\n\x12EO_COMMON_NAME_RED\x10\x07\x12\x1a\n\x16EO_COMMON_NAME_REDEDGE\x10\x08\x12\x1d\n\x19EO_COMMON_NAME_REDEDGE071\x10\t\x12\x1d\n\x19EO_COMMON_NAME_REDEDGE075\x10\n\x12\x1d\n\x19EO_COMMON_NAME_REDEDGE078\x10\x0b\x12\x16\n\x12EO_COMMON_NAME_NIR\x10\x0c\x12\x18\n\x14EO_COMMON_NAME_NIR08\x10\r\x12\x18\n\x14EO_COMMON_NAME_NIR09\x10\x0e\x12\x19\n\x15EO_COMMON_NAME_CIRRUS\x10\x0f\x12\x19\n\x15EO_COMMON_NAME_SWIR16\x10\x10\x12\x19\n\x15EO_COMMON_NAME_SWIR22\x10\x11\x12\x17\n\x13EO_COMMON_NAME_LWIR\x10\x12\x12\x19\n\x15EO_COMMON_NAME_LWIR11\x10\x13\x12\x19\n\x15EO_COMMON_NAME_LWIR12\x10\x14*f\n\x0eRasterSampling\x12\x1f\n\x1bRASTER_SAMPLING_UNSPECIFIED\x10\x00\x12\x18\n\x14RASTER_SAMPLING_AREA\x10\x01\x12\x19\n\x15RASTER_SAMPLING_POINT\x10\x02B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [], + { + "EOProperties": EOProperties, + "RasterProperties": RasterProperties, + "ClassificationClass": ClassificationClass, + "Projection": Projection, + "View": View, + "File": File, + "EOCommonName": EOCommonName, + "RasterSampling": RasterSampling, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/asset_metadata.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py new file mode 100644 index 0000000..b0ae8d0 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py @@ -0,0 +1,763 @@ +# Generated from datasets/stac/v1/asset.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +from . import asset_metadata_pb, core_pb, product_pb, sar_pb, satellite_pb + +if TYPE_CHECKING: + from protobuf import DescFile + + from .asset_metadata_pb import ClassificationClass, EOProperties, File, Projection, RasterProperties, View + from .core_pb import MediaType + from .product_pb import ProductProperties + from .sar_pb import SARProperties + from .satellite_pb import SatelliteProperties + + +_StatisticsFields: TypeAlias = Literal["minimum", "maximum"] + +class Statistics(Message[_StatisticsFields]): + """ + Statistics contains the statistics fields used by the supported Items. + + ```proto + message datasets.stac.v1.Statistics + ``` + + Attributes: + minimum: + ```proto + optional double minimum = 1; + ``` + maximum: + ```proto + optional double maximum = 2; + ``` + """ + + __slots__ = ("minimum", "maximum") + + if TYPE_CHECKING: + + def __init__( + self, + *, + minimum: float | None = None, + maximum: float | None = None, + ) -> None: + pass + + minimum: float + maximum: float + +_AssetAccessProfileFields: TypeAlias = Literal["alternate_key", "default_alternate_name", "base_href", "storage_refs", "auth_refs"] + +class AssetAccessProfile(Message[_AssetAccessProfileFields]): + """ + AssetAccessProfile deduplicates access metadata shared by asset locations in one STAC Item. + + ```proto + message datasets.stac.v1.AssetAccessProfile + ``` + + Attributes: + alternate_key: + Alternate Assets map key, such as `s3` or `https`, when used for an alternate location. + + ```proto + string alternate_key = 1; + ``` + default_alternate_name: + Default alternate:name for locations using this profile. + + ```proto + optional string default_alternate_name = 2; + ``` + base_href: + Item-specific common URI prefix used to compress hrefs. A location's full href is the byte concatenation of + base_href and its href suffix; URI reference resolution and path normalization are not applied. + + ```proto + string base_href = 3; + ``` + storage_refs: + Keys into the item-level Storage.schemes registry. + + ```proto + repeated string storage_refs = 4; + ``` + auth_refs: + Keys into the item-level Authentication.schemes registry. + + ```proto + repeated string auth_refs = 5; + ``` + """ + + __slots__ = ("alternate_key", "default_alternate_name", "base_href", "storage_refs", "auth_refs") + + if TYPE_CHECKING: + + def __init__( + self, + *, + alternate_key: str = "", + default_alternate_name: str | None = None, + base_href: str = "", + storage_refs: list[str] | None = None, + auth_refs: list[str] | None = None, + ) -> None: + pass + + alternate_key: str + default_alternate_name: str + base_href: str + storage_refs: list[str] + auth_refs: list[str] + +_AssetLocationFields: TypeAlias = Literal["access_profile_index", "href", "alternate_name"] + +class AssetLocation(Message[_AssetLocationFields]): + """ + AssetLocation selects an access profile and supplies a relative or absolute href. + + ```proto + message datasets.stac.v1.AssetLocation + ``` + + Attributes: + access_profile_index: + Required index into Assets.access_profiles. Zero is a valid index. + + ```proto + optional uint32 access_profile_index = 1; + ``` + href: + Href suffix for the selected profile. This must be present on primary locations. When absent on an alternate, + it reuses the primary location's href suffix verbatim before applying the alternate profile's base_href. + A present empty string represents the selected profile's base_href exactly. + + ```proto + optional string href = 2; + ``` + alternate_name: + Per-location override for the access profile's default alternate:name. + + ```proto + optional string alternate_name = 3; + ``` + """ + + __slots__ = ("access_profile_index", "href", "alternate_name") + + if TYPE_CHECKING: + + def __init__( + self, + *, + access_profile_index: int | None = None, + href: str | None = None, + alternate_name: str | None = None, + ) -> None: + pass + + access_profile_index: int + href: str + alternate_name: str + +_BandFields: TypeAlias = Literal["name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar"] + +class Band(Message[_BandFields]): + """ + Band is a reusable STAC 1.1 band profile containing common, EO, Raster, and Classification fields. Fields present + here override the corresponding inheritable Asset fields; ingestion should omit values lifted to the Asset. + + ```proto + message datasets.stac.v1.Band + ``` + + Attributes: + name: + ```proto + optional string name = 1; + ``` + description: + ```proto + optional string description = 2; + ``` + data_type: + ```proto + datasets.stac.v1.DataType data_type = 3; + ``` + nodata: + ```proto + optional double nodata = 4; + ``` + unit: + ```proto + optional string unit = 5; + ``` + eo: + ```proto + optional datasets.stac.v1.EOProperties eo = 6; + ``` + raster: + ```proto + optional datasets.stac.v1.RasterProperties raster = 7; + ``` + classes: + ```proto + repeated datasets.stac.v1.ClassificationClass classes = 8; + ``` + sar: + SAR 1.3.2 permits frequency, center-frequency, and bandwidth metadata on Band Objects. + + ```proto + optional datasets.stac.v1.SARProperties sar = 9; + ``` + """ + + __slots__ = ("name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar") + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str | None = None, + description: str | None = None, + data_type: DataType | None = None, + nodata: float | None = None, + unit: str | None = None, + eo: EOProperties | None = None, + raster: RasterProperties | None = None, + classes: list[ClassificationClass] | None = None, + sar: SARProperties | None = None, + ) -> None: + pass + + name: str + description: str + data_type: DataType + nodata: float + unit: str + eo: EOProperties | None + raster: RasterProperties | None + classes: list[ClassificationClass] + sar: SARProperties | None + +_AssetFields: TypeAlias = Literal["key", "primary", "alternates", "media_type", "title", "description", "roles", "custom_roles", "gsd", "band_profile_indices", "data_type", "nodata", "statistics", "unit", "eo", "raster", "projection", "view", "classes", "file", "sar", "satellite", "product"] + +class Asset(Message[_AssetFields]): + """ + Asset contains the canonical STAC 1.1 Asset Object fields used by the supported Items. Inheritable metadata applies + to every band unless a Band overrides it. Ingestion should lift values shared by all bands to this level. + + ```proto + message datasets.stac.v1.Asset + ``` + + Attributes: + key: + Key from the STAC assets object. + + ```proto + string key = 1; + ``` + primary: + ```proto + optional datasets.stac.v1.AssetLocation primary = 2; + ``` + alternates: + ```proto + repeated datasets.stac.v1.AssetLocation alternates = 3; + ``` + media_type: + ```proto + optional datasets.stac.v1.MediaType media_type = 4; + ``` + title: + ```proto + optional string title = 5; + ``` + description: + ```proto + optional string description = 6; + ``` + roles: + STAC role order is not semantically meaningful. Known roles are stored compactly; all other values remain strings. + + ```proto + repeated datasets.stac.v1.KnownAssetRole roles = 7 [packed = true]; + ``` + custom_roles: + ```proto + repeated string custom_roles = 8; + ``` + gsd: + ```proto + optional double gsd = 9; + ``` + band_profile_indices: + Ordered indices into Assets.band_profiles. References deliberately have no per-use overrides: metadata differences + require distinct profiles, which keeps reconstruction to direct lookup plus normal STAC Asset-to-Band inheritance. + + ```proto + repeated uint32 band_profile_indices = 10 [packed = true]; + ``` + data_type: + ```proto + datasets.stac.v1.DataType data_type = 11; + ``` + nodata: + ```proto + optional double nodata = 12; + ``` + statistics: + ```proto + optional datasets.stac.v1.Statistics statistics = 13; + ``` + unit: + ```proto + optional string unit = 14; + ``` + eo: + ```proto + optional datasets.stac.v1.EOProperties eo = 15; + ``` + raster: + ```proto + optional datasets.stac.v1.RasterProperties raster = 16; + ``` + projection: + ```proto + optional datasets.stac.v1.Projection projection = 17; + ``` + view: + ```proto + optional datasets.stac.v1.View view = 18; + ``` + classes: + ```proto + repeated datasets.stac.v1.ClassificationClass classes = 19; + ``` + file: + ```proto + optional datasets.stac.v1.File file = 20; + ``` + sar: + ```proto + optional datasets.stac.v1.SARProperties sar = 21; + ``` + satellite: + ```proto + optional datasets.stac.v1.SatelliteProperties satellite = 22; + ``` + product: + ```proto + optional datasets.stac.v1.ProductProperties product = 23; + ``` + """ + + __slots__ = ("key", "primary", "alternates", "media_type", "title", "description", "roles", "custom_roles", "gsd", "band_profile_indices", "data_type", "nodata", "statistics", "unit", "eo", "raster", "projection", "view", "classes", "file", "sar", "satellite", "product") + + if TYPE_CHECKING: + + def __init__( + self, + *, + key: str = "", + primary: AssetLocation | None = None, + alternates: list[AssetLocation] | None = None, + media_type: MediaType | None = None, + title: str | None = None, + description: str | None = None, + roles: list[KnownAssetRole] | None = None, + custom_roles: list[str] | None = None, + gsd: float | None = None, + band_profile_indices: list[int] | None = None, + data_type: DataType | None = None, + nodata: float | None = None, + statistics: Statistics | None = None, + unit: str | None = None, + eo: EOProperties | None = None, + raster: RasterProperties | None = None, + projection: Projection | None = None, + view: View | None = None, + classes: list[ClassificationClass] | None = None, + file: File | None = None, + sar: SARProperties | None = None, + satellite: SatelliteProperties | None = None, + product: ProductProperties | None = None, + ) -> None: + pass + + key: str + primary: AssetLocation | None + alternates: list[AssetLocation] + media_type: MediaType | None + title: str + description: str + roles: list[KnownAssetRole] + custom_roles: list[str] + gsd: float + band_profile_indices: list[int] + data_type: DataType + nodata: float + statistics: Statistics | None + unit: str + eo: EOProperties | None + raster: RasterProperties | None + projection: Projection | None + view: View | None + classes: list[ClassificationClass] + file: File | None + sar: SARProperties | None + satellite: SatelliteProperties | None + product: ProductProperties | None + +_AssetsFields: TypeAlias = Literal["access_profiles", "band_profiles", "assets"] + +class Assets(Message[_AssetsFields]): + """ + Assets contains the shared profile tables and assets of one STAC Item. Asset locations reference access_profiles by + index, and assets reference band_profiles by ordered indices. Both tables deduplicate repeated metadata; every index + must reference an existing profile. Band references intentionally cannot override profiles: use a distinct profile + for every unique Band. Use one access profile with an empty base_href and refs for uncompressed absolute hrefs. + Empty repeated fields are intentionally canonicalized: protobuf does not distinguish an absent JSON array from []. + + ```proto + message datasets.stac.v1.Assets + ``` + + Attributes: + access_profiles: + ```proto + repeated datasets.stac.v1.AssetAccessProfile access_profiles = 1; + ``` + band_profiles: + ```proto + repeated datasets.stac.v1.Band band_profiles = 2; + ``` + assets: + ```proto + repeated datasets.stac.v1.Asset assets = 3; + ``` + """ + + __slots__ = ("access_profiles", "band_profiles", "assets") + + if TYPE_CHECKING: + + def __init__( + self, + *, + access_profiles: list[AssetAccessProfile] | None = None, + band_profiles: list[Band] | None = None, + assets: list[Asset] | None = None, + ) -> None: + pass + + access_profiles: list[AssetAccessProfile] + band_profiles: list[Band] + assets: list[Asset] + +class KnownAssetRole(Enum): + """ + KnownAssetRole contains the STAC 1.1, EO 2.0, and SAR 1.3.2 recommended asset roles. STAC asset roles are open; + values not listed here are stored in Asset.custom_roles. + + ```proto + enum datasets.stac.v1.KnownAssetRole + ``` + + Attributes: + UNSPECIFIED: + ```proto + KNOWN_ASSET_ROLE_UNSPECIFIED = 0 + ``` + DATA: + STAC 1.1 recommended asset roles occupy values 1-99. + https://github.com/radiantearth/stac-spec/blob/v1.1.0/best-practices.md#list-of-asset-roles + + ```proto + KNOWN_ASSET_ROLE_DATA = 1 + ``` + METADATA: + ```proto + KNOWN_ASSET_ROLE_METADATA = 2 + ``` + THUMBNAIL: + ```proto + KNOWN_ASSET_ROLE_THUMBNAIL = 3 + ``` + OVERVIEW: + ```proto + KNOWN_ASSET_ROLE_OVERVIEW = 4 + ``` + VISUAL: + ```proto + KNOWN_ASSET_ROLE_VISUAL = 5 + ``` + DATE: + ```proto + KNOWN_ASSET_ROLE_DATE = 6 + ``` + GRAPHIC: + ```proto + KNOWN_ASSET_ROLE_GRAPHIC = 7 + ``` + DATA_MASK: + ```proto + KNOWN_ASSET_ROLE_DATA_MASK = 8 + ``` + SNOW_ICE: + ```proto + KNOWN_ASSET_ROLE_SNOW_ICE = 9 + ``` + LAND_WATER: + ```proto + KNOWN_ASSET_ROLE_LAND_WATER = 10 + ``` + WATER_MASK: + ```proto + KNOWN_ASSET_ROLE_WATER_MASK = 11 + ``` + ISO_19115: + ```proto + KNOWN_ASSET_ROLE_ISO_19115 = 12 + ``` + REFLECTANCE: + EO 2.0 best-practice asset roles occupy values 100-199. + https://github.com/stac-extensions/eo/blob/v2.0.0/README.md#best-practices + + ```proto + KNOWN_ASSET_ROLE_REFLECTANCE = 100 + ``` + TEMPERATURE: + ```proto + KNOWN_ASSET_ROLE_TEMPERATURE = 101 + ``` + SATURATION: + ```proto + KNOWN_ASSET_ROLE_SATURATION = 102 + ``` + CLOUD: + ```proto + KNOWN_ASSET_ROLE_CLOUD = 103 + ``` + CLOUD_SHADOW: + ```proto + KNOWN_ASSET_ROLE_CLOUD_SHADOW = 104 + ``` + LOCAL_INCIDENCE_ANGLE: + SAR 1.3.2 best-practice asset roles occupy values 200-299. + https://github.com/stac-extensions/sar/blob/v1.3.2/README.md#best-practices + + ```proto + KNOWN_ASSET_ROLE_LOCAL_INCIDENCE_ANGLE = 200 + ``` + ELLIPSOID_INCIDENCE_ANGLE: + ```proto + KNOWN_ASSET_ROLE_ELLIPSOID_INCIDENCE_ANGLE = 201 + ``` + NOISE_POWER: + ```proto + KNOWN_ASSET_ROLE_NOISE_POWER = 202 + ``` + AMPLITUDE: + ```proto + KNOWN_ASSET_ROLE_AMPLITUDE = 203 + ``` + MAGNITUDE: + ```proto + KNOWN_ASSET_ROLE_MAGNITUDE = 204 + ``` + SIGMA0: + ```proto + KNOWN_ASSET_ROLE_SIGMA0 = 205 + ``` + BETA0: + ```proto + KNOWN_ASSET_ROLE_BETA0 = 206 + ``` + GAMMA0: + ```proto + KNOWN_ASSET_ROLE_GAMMA0 = 207 + ``` + DATE_OFFSET: + ```proto + KNOWN_ASSET_ROLE_DATE_OFFSET = 208 + ``` + COVMAT: + ```proto + KNOWN_ASSET_ROLE_COVMAT = 209 + ``` + PRD: + ```proto + KNOWN_ASSET_ROLE_PRD = 210 + ``` + """ + + UNSPECIFIED = 0 + DATA = 1 + METADATA = 2 + THUMBNAIL = 3 + OVERVIEW = 4 + VISUAL = 5 + DATE = 6 + GRAPHIC = 7 + DATA_MASK = 8 + SNOW_ICE = 9 + LAND_WATER = 10 + WATER_MASK = 11 + ISO_19115 = 12 + REFLECTANCE = 100 + TEMPERATURE = 101 + SATURATION = 102 + CLOUD = 103 + CLOUD_SHADOW = 104 + LOCAL_INCIDENCE_ANGLE = 200 + ELLIPSOID_INCIDENCE_ANGLE = 201 + NOISE_POWER = 202 + AMPLITUDE = 203 + MAGNITUDE = 204 + SIGMA0 = 205 + BETA0 = 206 + GAMMA0 = 207 + DATE_OFFSET = 208 + COVMAT = 209 + PRD = 210 + +class DataType(Enum): + """ + DataType contains the complete closed STAC 1.1 data_type vocabulary. + + ```proto + enum datasets.stac.v1.DataType + ``` + + Attributes: + UNSPECIFIED: + ```proto + DATA_TYPE_UNSPECIFIED = 0 + ``` + INT8: + ```proto + DATA_TYPE_INT8 = 1 + ``` + INT16: + ```proto + DATA_TYPE_INT16 = 2 + ``` + INT32: + ```proto + DATA_TYPE_INT32 = 3 + ``` + INT64: + ```proto + DATA_TYPE_INT64 = 4 + ``` + UINT8: + ```proto + DATA_TYPE_UINT8 = 5 + ``` + UINT16: + ```proto + DATA_TYPE_UINT16 = 6 + ``` + UINT32: + ```proto + DATA_TYPE_UINT32 = 7 + ``` + UINT64: + ```proto + DATA_TYPE_UINT64 = 8 + ``` + FLOAT16: + ```proto + DATA_TYPE_FLOAT16 = 9 + ``` + FLOAT32: + ```proto + DATA_TYPE_FLOAT32 = 10 + ``` + FLOAT64: + ```proto + DATA_TYPE_FLOAT64 = 11 + ``` + CINT16: + ```proto + DATA_TYPE_CINT16 = 12 + ``` + CINT32: + ```proto + DATA_TYPE_CINT32 = 13 + ``` + CFLOAT32: + ```proto + DATA_TYPE_CFLOAT32 = 14 + ``` + CFLOAT64: + ```proto + DATA_TYPE_CFLOAT64 = 15 + ``` + OTHER: + ```proto + DATA_TYPE_OTHER = 16 + ``` + """ + + UNSPECIFIED = 0 + INT8 = 1 + INT16 = 2 + INT32 = 3 + INT64 = 4 + UINT8 = 5 + UINT16 = 6 + UINT32 = 7 + UINT64 = 8 + FLOAT16 = 9 + FLOAT32 = 10 + FLOAT64 = 11 + CINT16 = 12 + CINT32 = 13 + CFLOAT32 = 14 + CFLOAT64 = 15 + OTHER = 16 + + +_DESC = file_desc( + b'\n\x1cdatasets/stac/v1/asset.proto\x12\x10datasets.stac.v1\x1a%datasets/stac/v1/asset_metadata.proto\x1a\x1bdatasets/stac/v1/core.proto\x1a\x1edatasets/stac/v1/product.proto\x1a\x1adatasets/stac/v1/sar.proto\x1a datasets/stac/v1/satellite.proto"N\n\nStatistics\x12\x1f\n\x07minimum\x18\x01 \x01(\x01R\x07minimumB\x05\xaa\x01\x02\x08\x01\x12\x1f\n\x07maximum\x18\x02 \x01(\x01R\x07maximumB\x05\xaa\x01\x02\x08\x01"\xd3\x01\n\x12AssetAccessProfile\x12#\n\ralternate_key\x18\x01 \x01(\tR\x0calternateKey\x12;\n\x16default_alternate_name\x18\x02 \x01(\tR\x14defaultAlternateNameB\x05\xaa\x01\x02\x08\x01\x12\x1b\n\tbase_href\x18\x03 \x01(\tR\x08baseHref\x12!\n\x0cstorage_refs\x18\x04 \x03(\tR\x0bstorageRefs\x12\x1b\n\tauth_refs\x18\x05 \x03(\tR\x08authRefs"\x91\x01\n\rAssetLocation\x127\n\x14access_profile_index\x18\x01 \x01(\rR\x12accessProfileIndexB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x04href\x18\x02 \x01(\tR\x04hrefB\x05\xaa\x01\x02\x08\x01\x12,\n\x0ealternate_name\x18\x03 \x01(\tR\ralternateNameB\x05\xaa\x01\x02\x08\x01"\x9d\x03\n\x04Band\x12\x19\n\x04name\x18\x01 \x01(\tR\x04nameB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x02 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x127\n\tdata_type\x18\x03 \x01(\x0e2\x1a.datasets.stac.v1.DataTypeR\x08dataType\x12\x1d\n\x06nodata\x18\x04 \x01(\x01R\x06nodataB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x04unit\x18\x05 \x01(\tR\x04unitB\x05\xaa\x01\x02\x08\x01\x12.\n\x02eo\x18\x06 \x01(\x0b2\x1e.datasets.stac.v1.EOPropertiesR\x02eo\x12:\n\x06raster\x18\x07 \x01(\x0b2".datasets.stac.v1.RasterPropertiesR\x06raster\x12?\n\x07classes\x18\x08 \x03(\x0b2%.datasets.stac.v1.ClassificationClassR\x07classes\x121\n\x03sar\x18\t \x01(\x0b2\x1f.datasets.stac.v1.SARPropertiesR\x03sar"\xe8\x08\n\x05Asset\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x129\n\x07primary\x18\x02 \x01(\x0b2\x1f.datasets.stac.v1.AssetLocationR\x07primary\x12?\n\nalternates\x18\x03 \x03(\x0b2\x1f.datasets.stac.v1.AssetLocationR\nalternates\x12:\n\nmedia_type\x18\x04 \x01(\x0b2\x1b.datasets.stac.v1.MediaTypeR\tmediaType\x12\x1b\n\x05title\x18\x05 \x01(\tR\x05titleB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x06 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x126\n\x05roles\x18\x07 \x03(\x0e2 .datasets.stac.v1.KnownAssetRoleR\x05roles\x12!\n\x0ccustom_roles\x18\x08 \x03(\tR\x0bcustomRoles\x12\x17\n\x03gsd\x18\t \x01(\x01R\x03gsdB\x05\xaa\x01\x02\x08\x01\x120\n\x14band_profile_indices\x18\n \x03(\rR\x12bandProfileIndices\x127\n\tdata_type\x18\x0b \x01(\x0e2\x1a.datasets.stac.v1.DataTypeR\x08dataType\x12\x1d\n\x06nodata\x18\x0c \x01(\x01R\x06nodataB\x05\xaa\x01\x02\x08\x01\x12<\n\nstatistics\x18\r \x01(\x0b2\x1c.datasets.stac.v1.StatisticsR\nstatistics\x12\x19\n\x04unit\x18\x0e \x01(\tR\x04unitB\x05\xaa\x01\x02\x08\x01\x12.\n\x02eo\x18\x0f \x01(\x0b2\x1e.datasets.stac.v1.EOPropertiesR\x02eo\x12:\n\x06raster\x18\x10 \x01(\x0b2".datasets.stac.v1.RasterPropertiesR\x06raster\x12<\n\nprojection\x18\x11 \x01(\x0b2\x1c.datasets.stac.v1.ProjectionR\nprojection\x12*\n\x04view\x18\x12 \x01(\x0b2\x16.datasets.stac.v1.ViewR\x04view\x12?\n\x07classes\x18\x13 \x03(\x0b2%.datasets.stac.v1.ClassificationClassR\x07classes\x12*\n\x04file\x18\x14 \x01(\x0b2\x16.datasets.stac.v1.FileR\x04file\x121\n\x03sar\x18\x15 \x01(\x0b2\x1f.datasets.stac.v1.SARPropertiesR\x03sar\x12C\n\tsatellite\x18\x16 \x01(\x0b2%.datasets.stac.v1.SatellitePropertiesR\tsatellite\x12=\n\x07product\x18\x17 \x01(\x0b2#.datasets.stac.v1.ProductPropertiesR\x07product"\xc5\x01\n\x06Assets\x12M\n\x0faccess_profiles\x18\x01 \x03(\x0b2$.datasets.stac.v1.AssetAccessProfileR\x0eaccessProfiles\x12;\n\rband_profiles\x18\x02 \x03(\x0b2\x16.datasets.stac.v1.BandR\x0cbandProfiles\x12/\n\x06assets\x18\x03 \x03(\x0b2\x17.datasets.stac.v1.AssetR\x06assets*\xbe\x07\n\x0eKnownAssetRole\x12 \n\x1cKNOWN_ASSET_ROLE_UNSPECIFIED\x10\x00\x12\x19\n\x15KNOWN_ASSET_ROLE_DATA\x10\x01\x12\x1d\n\x19KNOWN_ASSET_ROLE_METADATA\x10\x02\x12\x1e\n\x1aKNOWN_ASSET_ROLE_THUMBNAIL\x10\x03\x12\x1d\n\x19KNOWN_ASSET_ROLE_OVERVIEW\x10\x04\x12\x1b\n\x17KNOWN_ASSET_ROLE_VISUAL\x10\x05\x12\x19\n\x15KNOWN_ASSET_ROLE_DATE\x10\x06\x12\x1c\n\x18KNOWN_ASSET_ROLE_GRAPHIC\x10\x07\x12\x1e\n\x1aKNOWN_ASSET_ROLE_DATA_MASK\x10\x08\x12\x1d\n\x19KNOWN_ASSET_ROLE_SNOW_ICE\x10\t\x12\x1f\n\x1bKNOWN_ASSET_ROLE_LAND_WATER\x10\n\x12\x1f\n\x1bKNOWN_ASSET_ROLE_WATER_MASK\x10\x0b\x12\x1e\n\x1aKNOWN_ASSET_ROLE_ISO_19115\x10\x0c\x12 \n\x1cKNOWN_ASSET_ROLE_REFLECTANCE\x10d\x12 \n\x1cKNOWN_ASSET_ROLE_TEMPERATURE\x10e\x12\x1f\n\x1bKNOWN_ASSET_ROLE_SATURATION\x10f\x12\x1a\n\x16KNOWN_ASSET_ROLE_CLOUD\x10g\x12!\n\x1dKNOWN_ASSET_ROLE_CLOUD_SHADOW\x10h\x12+\n&KNOWN_ASSET_ROLE_LOCAL_INCIDENCE_ANGLE\x10\xc8\x01\x12/\n*KNOWN_ASSET_ROLE_ELLIPSOID_INCIDENCE_ANGLE\x10\xc9\x01\x12!\n\x1cKNOWN_ASSET_ROLE_NOISE_POWER\x10\xca\x01\x12\x1f\n\x1aKNOWN_ASSET_ROLE_AMPLITUDE\x10\xcb\x01\x12\x1f\n\x1aKNOWN_ASSET_ROLE_MAGNITUDE\x10\xcc\x01\x12\x1c\n\x17KNOWN_ASSET_ROLE_SIGMA0\x10\xcd\x01\x12\x1b\n\x16KNOWN_ASSET_ROLE_BETA0\x10\xce\x01\x12\x1c\n\x17KNOWN_ASSET_ROLE_GAMMA0\x10\xcf\x01\x12!\n\x1cKNOWN_ASSET_ROLE_DATE_OFFSET\x10\xd0\x01\x12\x1c\n\x17KNOWN_ASSET_ROLE_COVMAT\x10\xd1\x01\x12\x19\n\x14KNOWN_ASSET_ROLE_PRD\x10\xd2\x01*\x85\x03\n\x08DataType\x12\x19\n\x15DATA_TYPE_UNSPECIFIED\x10\x00\x12\x12\n\x0eDATA_TYPE_INT8\x10\x01\x12\x13\n\x0fDATA_TYPE_INT16\x10\x02\x12\x13\n\x0fDATA_TYPE_INT32\x10\x03\x12\x13\n\x0fDATA_TYPE_INT64\x10\x04\x12\x13\n\x0fDATA_TYPE_UINT8\x10\x05\x12\x14\n\x10DATA_TYPE_UINT16\x10\x06\x12\x14\n\x10DATA_TYPE_UINT32\x10\x07\x12\x14\n\x10DATA_TYPE_UINT64\x10\x08\x12\x15\n\x11DATA_TYPE_FLOAT16\x10\t\x12\x15\n\x11DATA_TYPE_FLOAT32\x10\n\x12\x15\n\x11DATA_TYPE_FLOAT64\x10\x0b\x12\x14\n\x10DATA_TYPE_CINT16\x10\x0c\x12\x14\n\x10DATA_TYPE_CINT32\x10\r\x12\x16\n\x12DATA_TYPE_CFLOAT32\x10\x0e\x12\x16\n\x12DATA_TYPE_CFLOAT64\x10\x0f\x12\x13\n\x0fDATA_TYPE_OTHER\x10\x10B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [ + asset_metadata_pb.desc(), + core_pb.desc(), + product_pb.desc(), + sar_pb.desc(), + satellite_pb.desc(), + ], + { + "Statistics": Statistics, + "AssetAccessProfile": AssetAccessProfile, + "AssetLocation": AssetLocation, + "Band": Band, + "Asset": Asset, + "Assets": Assets, + "KnownAssetRole": KnownAssetRole, + "DataType": DataType, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/asset.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb.py new file mode 100644 index 0000000..2e72fc9 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/authentication_pb.py @@ -0,0 +1,410 @@ +# Generated from datasets/stac/v1/authentication.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc +from protobuf.wkt import struct_pb + +if TYPE_CHECKING: + from protobuf import DescFile + from protobuf.wkt import Struct + + +_AuthenticationFields: TypeAlias = Literal["schemes"] + +class Authentication(Message[_AuthenticationFields]): + """ + Authentication stores the Authentication 1.1 auth:schemes registry. + + ```proto + message datasets.stac.v1.Authentication + ``` + + Attributes: + schemes: + ```proto + map schemes = 1; + ``` + """ + + __slots__ = ("schemes",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + schemes: dict[str, AuthenticationScheme] | None = None, + ) -> None: + pass + + schemes: dict[str, AuthenticationScheme] + +_AuthenticationSchemeFields: TypeAlias = Literal["known_type", "custom_type", "description", "name", "location", "scheme", "flows", "open_id_connect_url"] + +class AuthenticationScheme(Message[_AuthenticationSchemeFields]): + """ + AuthenticationScheme describes one reusable authentication mechanism. + + ```proto + message datasets.stac.v1.AuthenticationScheme + ``` + + Attributes: + known_type: + Exactly one compact known type or exact custom Authentication 1.1 type string is set. + + ```proto + optional datasets.stac.v1.KnownAuthenticationType known_type = 1; + ``` + custom_type: + ```proto + optional string custom_type = 2; + ``` + description: + ```proto + optional string description = 3; + ``` + name: + API key parameter name. Required for apiKey authentication. + + ```proto + optional string name = 4; + ``` + location: + API key parameter location, conventionally `query`, `header`, or `cookie`. The vocabulary is open in the extension. + + ```proto + optional string location = 5 [json_name = "in"]; + ``` + scheme: + HTTP Authorization scheme, such as `basic` or `bearer`. The vocabulary is open in the extension. + + ```proto + optional string scheme = 6; + ``` + flows: + OAuth2 or signed URL flows. Each key corresponds to one entry in the STAC flows map. + + ```proto + repeated datasets.stac.v1.AuthenticationFlow flows = 7; + ``` + open_id_connect_url: + OpenID Connect discovery URL. Required when known_type is OPEN_ID_CONNECT. + + ```proto + optional string open_id_connect_url = 8; + ``` + """ + + __slots__ = ("known_type", "custom_type", "description", "name", "location", "scheme", "flows", "open_id_connect_url") + + if TYPE_CHECKING: + + def __init__( + self, + *, + known_type: KnownAuthenticationType | None = None, + custom_type: str | None = None, + description: str | None = None, + name: str | None = None, + location: str | None = None, + scheme: str | None = None, + flows: list[AuthenticationFlow] | None = None, + open_id_connect_url: str | None = None, + ) -> None: + pass + + known_type: KnownAuthenticationType + custom_type: str + description: str + name: str + location: str + scheme: str + flows: list[AuthenticationFlow] + open_id_connect_url: str + +_AuthenticationFlowFields: TypeAlias = Literal["key", "oauth2", "signed_url"] + +class AuthenticationFlow(Message[_AuthenticationFlowFields]): + """ + AuthenticationFlow models one keyed entry in an Authentication Scheme Object's flows map. Exactly one flow value is + set without using a protobuf oneof, keeping generated client APIs straightforward. + + ```proto + message datasets.stac.v1.AuthenticationFlow + ``` + + Attributes: + key: + Flow map key. OAuth2 defines authorizationCode, implicit, password, and clientCredentials; custom keys are allowed. + + ```proto + string key = 1; + ``` + oauth2: + ```proto + optional datasets.stac.v1.OAuth2Flow oauth2 = 2; + ``` + signed_url: + ```proto + optional datasets.stac.v1.SignedURLFlow signed_url = 3; + ``` + """ + + __slots__ = ("key", "oauth2", "signed_url") + + if TYPE_CHECKING: + + def __init__( + self, + *, + key: str = "", + oauth2: OAuth2Flow | None = None, + signed_url: SignedURLFlow | None = None, + ) -> None: + pass + + key: str + oauth2: OAuth2Flow | None + signed_url: SignedURLFlow | None + +_OAuth2FlowFields: TypeAlias = Literal["authorization_url", "token_url", "scopes", "refresh_url"] + +class OAuth2Flow(Message[_OAuth2FlowFields]): + """ + OAuth2Flow models an Authentication 1.1 OAuth2 Flow Object. + + ```proto + message datasets.stac.v1.OAuth2Flow + ``` + + Attributes: + authorization_url: + ```proto + optional string authorization_url = 1; + ``` + token_url: + ```proto + optional string token_url = 2; + ``` + scopes: + STAC requires scopes, but permits an empty map. Always emit an empty object when no scopes are available. + + ```proto + map scopes = 3; + ``` + refresh_url: + ```proto + optional string refresh_url = 4; + ``` + """ + + __slots__ = ("authorization_url", "token_url", "scopes", "refresh_url") + + if TYPE_CHECKING: + + def __init__( + self, + *, + authorization_url: str | None = None, + token_url: str | None = None, + scopes: dict[str, str] | None = None, + refresh_url: str | None = None, + ) -> None: + pass + + authorization_url: str + token_url: str + scopes: dict[str, str] + refresh_url: str + +_SignedURLFlowFields: TypeAlias = Literal["method", "authorization_api", "parameters", "response_field"] + +class SignedURLFlow(Message[_SignedURLFlowFields]): + """ + SignedURLFlow models an Authentication 1.1 Signed URL Object. + + ```proto + message datasets.stac.v1.SignedURLFlow + ``` + + Attributes: + method: + ```proto + string method = 1; + ``` + authorization_api: + ```proto + string authorization_api = 2; + ``` + parameters: + ```proto + map parameters = 3; + ``` + response_field: + ```proto + optional string response_field = 4; + ``` + """ + + __slots__ = ("method", "authorization_api", "parameters", "response_field") + + if TYPE_CHECKING: + + def __init__( + self, + *, + method: str = "", + authorization_api: str = "", + parameters: dict[str, AuthenticationParameter] | None = None, + response_field: str | None = None, + ) -> None: + pass + + method: str + authorization_api: str + parameters: dict[str, AuthenticationParameter] + response_field: str + +_AuthenticationParameterFields: TypeAlias = Literal["location", "required", "description", "schema"] + +class AuthenticationParameter(Message[_AuthenticationParameterFields]): + """ + AuthenticationParameter models a request parameter for a signed URL authorization API. + + ```proto + message datasets.stac.v1.AuthenticationParameter + ``` + + Attributes: + location: + Parameter location, conventionally `query`, `header`, or `body`. The vocabulary is open in the extension. + + ```proto + string location = 1 [json_name = "in"]; + ``` + required: + ```proto + optional bool required = 2; + ``` + description: + ```proto + optional string description = 3; + ``` + schema: + JSON Schema draft-07 object. Struct preserves arbitrary keywords, nested schemas, and custom keywords. + + ```proto + optional google.protobuf.Struct schema = 4; + ``` + """ + + __slots__ = ("location", "required", "description", "schema") + + if TYPE_CHECKING: + + def __init__( + self, + *, + location: str = "", + required: bool | None = None, + description: str | None = None, + schema: Struct | None = None, + ) -> None: + pass + + location: str + required: bool + description: str + schema: Struct | None + +class KnownAuthenticationType(Enum): + """ + KnownAuthenticationType is Tilebox's compact dictionary for the standard Authentication 1.1 scheme types, in the + same order as the extension specification. Authentication 1.1 has an open type vocabulary; all other values remain + available through AuthenticationScheme.custom_type. + + ```proto + enum datasets.stac.v1.KnownAuthenticationType + ``` + + Attributes: + UNSPECIFIED: + ```proto + KNOWN_AUTHENTICATION_TYPE_UNSPECIFIED = 0 + ``` + HTTP: + http + + ```proto + KNOWN_AUTHENTICATION_TYPE_HTTP = 1 + ``` + S3: + s3 + + ```proto + KNOWN_AUTHENTICATION_TYPE_S3 = 2 + ``` + SIGNED_URL: + signedUrl + + ```proto + KNOWN_AUTHENTICATION_TYPE_SIGNED_URL = 3 + ``` + OAUTH2: + oauth2 + + ```proto + KNOWN_AUTHENTICATION_TYPE_OAUTH2 = 4 + ``` + API_KEY: + apiKey + + ```proto + KNOWN_AUTHENTICATION_TYPE_API_KEY = 5 + ``` + OPEN_ID_CONNECT: + openIdConnect + + ```proto + KNOWN_AUTHENTICATION_TYPE_OPEN_ID_CONNECT = 6 + ``` + """ + + UNSPECIFIED = 0 + HTTP = 1 + S3 = 2 + SIGNED_URL = 3 + OAUTH2 = 4 + API_KEY = 5 + OPEN_ID_CONNECT = 6 + + +_DESC = file_desc( + b'\n%datasets/stac/v1/authentication.proto\x12\x10datasets.stac.v1\x1a\x1cgoogle/protobuf/struct.proto"\xbd\x01\n\x0eAuthentication\x12G\n\x07schemes\x18\x01 \x03(\x0b2-.datasets.stac.v1.Authentication.SchemesEntryR\x07schemes\x1ab\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12<\n\x05value\x18\x02 \x01(\x0b2&.datasets.stac.v1.AuthenticationSchemeR\x05value:\x028\x01"\x81\x03\n\x14AuthenticationScheme\x12O\n\nknown_type\x18\x01 \x01(\x0e2).datasets.stac.v1.KnownAuthenticationTypeR\tknownTypeB\x05\xaa\x01\x02\x08\x01\x12&\n\x0bcustom_type\x18\x02 \x01(\tR\ncustomTypeB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x03 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x04name\x18\x04 \x01(\tR\x04nameB\x05\xaa\x01\x02\x08\x01\x12\x1b\n\x08location\x18\x05 \x01(\tR\x02inB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06scheme\x18\x06 \x01(\tR\x06schemeB\x05\xaa\x01\x02\x08\x01\x12:\n\x05flows\x18\x07 \x03(\x0b2$.datasets.stac.v1.AuthenticationFlowR\x05flows\x124\n\x13open_id_connect_url\x18\x08 \x01(\tR\x10openIdConnectUrlB\x05\xaa\x01\x02\x08\x01"\x9c\x01\n\x12AuthenticationFlow\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x124\n\x06oauth2\x18\x02 \x01(\x0b2\x1c.datasets.stac.v1.OAuth2FlowR\x06oauth2\x12>\n\nsigned_url\x18\x03 \x01(\x0b2\x1f.datasets.stac.v1.SignedURLFlowR\tsignedUrl"\x89\x02\n\nOAuth2Flow\x122\n\x11authorization_url\x18\x01 \x01(\tR\x10authorizationUrlB\x05\xaa\x01\x02\x08\x01\x12"\n\ttoken_url\x18\x02 \x01(\tR\x08tokenUrlB\x05\xaa\x01\x02\x08\x01\x12@\n\x06scopes\x18\x03 \x03(\x0b2(.datasets.stac.v1.OAuth2Flow.ScopesEntryR\x06scopes\x12&\n\x0brefresh_url\x18\x04 \x01(\tR\nrefreshUrlB\x05\xaa\x01\x02\x08\x01\x1a9\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x028\x01"\xbd\x02\n\rSignedURLFlow\x12\x16\n\x06method\x18\x01 \x01(\tR\x06method\x12+\n\x11authorization_api\x18\x02 \x01(\tR\x10authorizationApi\x12O\n\nparameters\x18\x03 \x03(\x0b2/.datasets.stac.v1.SignedURLFlow.ParametersEntryR\nparameters\x12,\n\x0eresponse_field\x18\x04 \x01(\tR\rresponseFieldB\x05\xaa\x01\x02\x08\x01\x1ah\n\x0fParametersEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12?\n\x05value\x18\x02 \x01(\x0b2).datasets.stac.v1.AuthenticationParameterR\x05value:\x028\x01"\xac\x01\n\x17AuthenticationParameter\x12\x14\n\x08location\x18\x01 \x01(\tR\x02in\x12!\n\x08required\x18\x02 \x01(\x08R\x08requiredB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x03 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x12/\n\x06schema\x18\x04 \x01(\x0b2\x17.google.protobuf.StructR\x06schema*\xb0\x02\n\x17KnownAuthenticationType\x12)\n%KNOWN_AUTHENTICATION_TYPE_UNSPECIFIED\x10\x00\x12"\n\x1eKNOWN_AUTHENTICATION_TYPE_HTTP\x10\x01\x12 \n\x1cKNOWN_AUTHENTICATION_TYPE_S3\x10\x02\x12(\n$KNOWN_AUTHENTICATION_TYPE_SIGNED_URL\x10\x03\x12$\n KNOWN_AUTHENTICATION_TYPE_OAUTH2\x10\x04\x12%\n!KNOWN_AUTHENTICATION_TYPE_API_KEY\x10\x05\x12-\n)KNOWN_AUTHENTICATION_TYPE_OPEN_ID_CONNECT\x10\x06B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [ + struct_pb.desc(), + ], + { + "Authentication": Authentication, + "AuthenticationScheme": AuthenticationScheme, + "AuthenticationFlow": AuthenticationFlow, + "OAuth2Flow": OAuth2Flow, + "SignedURLFlow": SignedURLFlow, + "AuthenticationParameter": AuthenticationParameter, + "KnownAuthenticationType": KnownAuthenticationType, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/authentication.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/core_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/core_pb.py new file mode 100644 index 0000000..777faae --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/core_pb.py @@ -0,0 +1,451 @@ +# Generated from datasets/stac/v1/core.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_MediaTypeFields: TypeAlias = Literal["known", "custom"] + +class MediaType(Message[_MediaTypeFields]): + """ + MediaType stores a common media type compactly while retaining STAC's open media-type vocabulary. + + ```proto + message datasets.stac.v1.MediaType + ``` + + Attributes: + known: + The exact media type represented by either the compact dictionary or a custom string. Exactly one is set. + + ```proto + optional datasets.stac.v1.KnownMediaType known = 1; + ``` + custom: + ```proto + optional string custom = 2; + ``` + """ + + __slots__ = ("known", "custom") + + if TYPE_CHECKING: + + def __init__( + self, + *, + known: KnownMediaType | None = None, + custom: str | None = None, + ) -> None: + pass + + known: KnownMediaType + custom: str + +_ProviderFields: TypeAlias = Literal["name", "description", "roles", "url"] + +class Provider(Message[_ProviderFields]): + """ + Provider is reusable as the element type of a flattened STAC properties.providers dataset field. + + ```proto + message datasets.stac.v1.Provider + ``` + + Attributes: + name: + ```proto + optional string name = 1; + ``` + description: + ```proto + optional string description = 2; + ``` + roles: + ```proto + repeated datasets.stac.v1.ProviderRole roles = 3 [packed = true]; + ``` + url: + ```proto + optional string url = 4; + ``` + """ + + __slots__ = ("name", "description", "roles", "url") + + if TYPE_CHECKING: + + def __init__( + self, + *, + name: str | None = None, + description: str | None = None, + roles: list[ProviderRole] | None = None, + url: str | None = None, + ) -> None: + pass + + name: str + description: str + roles: list[ProviderRole] + url: str + +_LinkFields: TypeAlias = Literal["href", "rel", "media_type", "title", "storage_refs", "auth_refs"] + +class Link(Message[_LinkFields]): + """ + Link contains the STAC 1.1 Link Object fields used by the supported Items. + + ```proto + message datasets.stac.v1.Link + ``` + + Attributes: + href: + ```proto + string href = 1; + ``` + rel: + ```proto + string rel = 2; + ``` + media_type: + ```proto + optional datasets.stac.v1.MediaType media_type = 3; + ``` + title: + ```proto + optional string title = 4; + ``` + storage_refs: + Keys into the item-level Storage.schemes registry. + + ```proto + repeated string storage_refs = 5; + ``` + auth_refs: + Keys into the item-level Authentication.schemes registry. + + ```proto + repeated string auth_refs = 6; + ``` + """ + + __slots__ = ("href", "rel", "media_type", "title", "storage_refs", "auth_refs") + + if TYPE_CHECKING: + + def __init__( + self, + *, + href: str = "", + rel: str = "", + media_type: MediaType | None = None, + title: str | None = None, + storage_refs: list[str] | None = None, + auth_refs: list[str] | None = None, + ) -> None: + pass + + href: str + rel: str + media_type: MediaType | None + title: str + storage_refs: list[str] + auth_refs: list[str] + +_LinksFields: TypeAlias = Literal["links"] + +class Links(Message[_LinksFields]): + """ + Links contains the ordered Link Objects of a STAC entity. + + ```proto + message datasets.stac.v1.Links + ``` + + Attributes: + links: + ```proto + repeated datasets.stac.v1.Link links = 1; + ``` + """ + + __slots__ = ("links",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + links: list[Link] | None = None, + ) -> None: + pass + + links: list[Link] + +class KnownMediaType(Enum): + """ + KnownMediaType is a compact dictionary of media types commonly used by STAC assets and links. + + ```proto + enum datasets.stac.v1.KnownMediaType + ``` + + Attributes: + UNSPECIFIED: + ```proto + KNOWN_MEDIA_TYPE_UNSPECIFIED = 0 + ``` + GEOJSON: + application/geo+json + + ```proto + KNOWN_MEDIA_TYPE_GEOJSON = 1 + ``` + JSON: + application/json + + ```proto + KNOWN_MEDIA_TYPE_JSON = 2 + ``` + CLOUD_OPTIMIZED_GEOTIFF: + image/tiff; application=geotiff; profile=cloud-optimized + + ```proto + KNOWN_MEDIA_TYPE_CLOUD_OPTIMIZED_GEOTIFF = 3 + ``` + JPEG_2000: + image/jp2 + + ```proto + KNOWN_MEDIA_TYPE_JPEG_2000 = 4 + ``` + JPEG: + image/jpeg + + ```proto + KNOWN_MEDIA_TYPE_JPEG = 5 + ``` + PNG: + image/png + + ```proto + KNOWN_MEDIA_TYPE_PNG = 6 + ``` + APPLICATION_XML: + application/xml + + ```proto + KNOWN_MEDIA_TYPE_APPLICATION_XML = 7 + ``` + ZIP: + application/zip + + ```proto + KNOWN_MEDIA_TYPE_ZIP = 8 + ``` + DIRECTORY: + application/x-directory + + ```proto + KNOWN_MEDIA_TYPE_DIRECTORY = 9 + ``` + GEOTIFF: + image/tiff; application=geotiff + + ```proto + KNOWN_MEDIA_TYPE_GEOTIFF = 10 + ``` + TIFF: + image/tiff + + ```proto + KNOWN_MEDIA_TYPE_TIFF = 11 + ``` + HDF5: + application/x-hdf5 + + ```proto + KNOWN_MEDIA_TYPE_HDF5 = 12 + ``` + HDF: + application/x-hdf + + ```proto + KNOWN_MEDIA_TYPE_HDF = 13 + ``` + NETCDF: + application/netcdf + + ```proto + KNOWN_MEDIA_TYPE_NETCDF = 14 + ``` + ZARR_V2: + application/vnd.zarr; version=2 + + ```proto + KNOWN_MEDIA_TYPE_ZARR_V2 = 15 + ``` + ZARR_V3: + application/vnd.zarr; version=3 + + ```proto + KNOWN_MEDIA_TYPE_ZARR_V3 = 16 + ``` + PARQUET: + application/vnd.apache.parquet + + ```proto + KNOWN_MEDIA_TYPE_PARQUET = 17 + ``` + GEOPACKAGE: + application/geopackage+sqlite3 + + ```proto + KNOWN_MEDIA_TYPE_GEOPACKAGE = 18 + ``` + COPC: + application/vnd.laszip+copc + + ```proto + KNOWN_MEDIA_TYPE_COPC = 19 + ``` + HTML: + text/html + + ```proto + KNOWN_MEDIA_TYPE_HTML = 20 + ``` + TEXT: + text/plain + + ```proto + KNOWN_MEDIA_TYPE_TEXT = 21 + ``` + TEXT_XML: + text/xml + + ```proto + KNOWN_MEDIA_TYPE_TEXT_XML = 22 + ``` + FLATGEOBUF: + application/vnd.flatgeobuf + + ```proto + KNOWN_MEDIA_TYPE_FLATGEOBUF = 23 + ``` + PMTILES: + application/vnd.pmtiles + + ```proto + KNOWN_MEDIA_TYPE_PMTILES = 24 + ``` + NITF: + application/vnd.nitf + + ```proto + KNOWN_MEDIA_TYPE_NITF = 25 + ``` + OCTET_STREAM: + application/octet-stream + + ```proto + KNOWN_MEDIA_TYPE_OCTET_STREAM = 26 + ``` + """ + + UNSPECIFIED = 0 + GEOJSON = 1 + JSON = 2 + CLOUD_OPTIMIZED_GEOTIFF = 3 + JPEG_2000 = 4 + JPEG = 5 + PNG = 6 + APPLICATION_XML = 7 + ZIP = 8 + DIRECTORY = 9 + GEOTIFF = 10 + TIFF = 11 + HDF5 = 12 + HDF = 13 + NETCDF = 14 + ZARR_V2 = 15 + ZARR_V3 = 16 + PARQUET = 17 + GEOPACKAGE = 18 + COPC = 19 + HTML = 20 + TEXT = 21 + TEXT_XML = 22 + FLATGEOBUF = 23 + PMTILES = 24 + NITF = 25 + OCTET_STREAM = 26 + +class ProviderRole(Enum): + """ + ProviderRole contains the closed STAC 1.1 Provider role vocabulary. + + ```proto + enum datasets.stac.v1.ProviderRole + ``` + + Attributes: + UNSPECIFIED: + ```proto + PROVIDER_ROLE_UNSPECIFIED = 0 + ``` + PRODUCER: + ```proto + PROVIDER_ROLE_PRODUCER = 1 + ``` + LICENSOR: + ```proto + PROVIDER_ROLE_LICENSOR = 2 + ``` + PROCESSOR: + ```proto + PROVIDER_ROLE_PROCESSOR = 3 + ``` + HOST: + ```proto + PROVIDER_ROLE_HOST = 4 + ``` + """ + + UNSPECIFIED = 0 + PRODUCER = 1 + LICENSOR = 2 + PROCESSOR = 3 + HOST = 4 + + +_DESC = file_desc( + b'\n\x1bdatasets/stac/v1/core.proto\x12\x10datasets.stac.v1"i\n\tMediaType\x12=\n\x05known\x18\x01 \x01(\x0e2 .datasets.stac.v1.KnownMediaTypeR\x05knownB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06custom\x18\x02 \x01(\tR\x06customB\x05\xaa\x01\x02\x08\x01"\x9d\x01\n\x08Provider\x12\x19\n\x04name\x18\x01 \x01(\tR\x04nameB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x02 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x124\n\x05roles\x18\x03 \x03(\x0e2\x1e.datasets.stac.v1.ProviderRoleR\x05roles\x12\x17\n\x03url\x18\x04 \x01(\tR\x03urlB\x05\xaa\x01\x02\x08\x01"\xc5\x01\n\x04Link\x12\x12\n\x04href\x18\x01 \x01(\tR\x04href\x12\x10\n\x03rel\x18\x02 \x01(\tR\x03rel\x12:\n\nmedia_type\x18\x03 \x01(\x0b2\x1b.datasets.stac.v1.MediaTypeR\tmediaType\x12\x1b\n\x05title\x18\x04 \x01(\tR\x05titleB\x05\xaa\x01\x02\x08\x01\x12!\n\x0cstorage_refs\x18\x05 \x03(\tR\x0bstorageRefs\x12\x1b\n\tauth_refs\x18\x06 \x03(\tR\x08authRefs"5\n\x05Links\x12,\n\x05links\x18\x01 \x03(\x0b2\x16.datasets.stac.v1.LinkR\x05links*\xc1\x06\n\x0eKnownMediaType\x12 \n\x1cKNOWN_MEDIA_TYPE_UNSPECIFIED\x10\x00\x12\x1c\n\x18KNOWN_MEDIA_TYPE_GEOJSON\x10\x01\x12\x19\n\x15KNOWN_MEDIA_TYPE_JSON\x10\x02\x12,\n(KNOWN_MEDIA_TYPE_CLOUD_OPTIMIZED_GEOTIFF\x10\x03\x12\x1e\n\x1aKNOWN_MEDIA_TYPE_JPEG_2000\x10\x04\x12\x19\n\x15KNOWN_MEDIA_TYPE_JPEG\x10\x05\x12\x18\n\x14KNOWN_MEDIA_TYPE_PNG\x10\x06\x12$\n KNOWN_MEDIA_TYPE_APPLICATION_XML\x10\x07\x12\x18\n\x14KNOWN_MEDIA_TYPE_ZIP\x10\x08\x12\x1e\n\x1aKNOWN_MEDIA_TYPE_DIRECTORY\x10\t\x12\x1c\n\x18KNOWN_MEDIA_TYPE_GEOTIFF\x10\n\x12\x19\n\x15KNOWN_MEDIA_TYPE_TIFF\x10\x0b\x12\x19\n\x15KNOWN_MEDIA_TYPE_HDF5\x10\x0c\x12\x18\n\x14KNOWN_MEDIA_TYPE_HDF\x10\r\x12\x1b\n\x17KNOWN_MEDIA_TYPE_NETCDF\x10\x0e\x12\x1c\n\x18KNOWN_MEDIA_TYPE_ZARR_V2\x10\x0f\x12\x1c\n\x18KNOWN_MEDIA_TYPE_ZARR_V3\x10\x10\x12\x1c\n\x18KNOWN_MEDIA_TYPE_PARQUET\x10\x11\x12\x1f\n\x1bKNOWN_MEDIA_TYPE_GEOPACKAGE\x10\x12\x12\x19\n\x15KNOWN_MEDIA_TYPE_COPC\x10\x13\x12\x19\n\x15KNOWN_MEDIA_TYPE_HTML\x10\x14\x12\x19\n\x15KNOWN_MEDIA_TYPE_TEXT\x10\x15\x12\x1d\n\x19KNOWN_MEDIA_TYPE_TEXT_XML\x10\x16\x12\x1f\n\x1bKNOWN_MEDIA_TYPE_FLATGEOBUF\x10\x17\x12\x1c\n\x18KNOWN_MEDIA_TYPE_PMTILES\x10\x18\x12\x19\n\x15KNOWN_MEDIA_TYPE_NITF\x10\x19\x12!\n\x1dKNOWN_MEDIA_TYPE_OCTET_STREAM\x10\x1a*\x9a\x01\n\x0cProviderRole\x12\x1d\n\x19PROVIDER_ROLE_UNSPECIFIED\x10\x00\x12\x1a\n\x16PROVIDER_ROLE_PRODUCER\x10\x01\x12\x1a\n\x16PROVIDER_ROLE_LICENSOR\x10\x02\x12\x1b\n\x17PROVIDER_ROLE_PROCESSOR\x10\x03\x12\x16\n\x12PROVIDER_ROLE_HOST\x10\x04B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [], + { + "MediaType": MediaType, + "Provider": Provider, + "Link": Link, + "Links": Links, + "KnownMediaType": KnownMediaType, + "ProviderRole": ProviderRole, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/core.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/processing_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/processing_pb.py new file mode 100644 index 0000000..40d11bb --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/processing_pb.py @@ -0,0 +1,60 @@ +# Generated from datasets/stac/v1/processing.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_ProcessingSoftwareFields: TypeAlias = Literal["versions"] + +class ProcessingSoftware(Message[_ProcessingSoftwareFields]): + """ + ProcessingSoftware maps each application or library name to its version, tag, commit, or equivalent identifier. + + ```proto + message datasets.stac.v1.ProcessingSoftware + ``` + + Attributes: + versions: + ```proto + map versions = 1; + ``` + """ + + __slots__ = ("versions",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + versions: dict[str, str] | None = None, + ) -> None: + pass + + versions: dict[str, str] + + +_DESC = file_desc( + b'\n!datasets/stac/v1/processing.proto\x12\x10datasets.stac.v1"\xa1\x01\n\x12ProcessingSoftware\x12N\n\x08versions\x18\x01 \x03(\x0b22.datasets.stac.v1.ProcessingSoftware.VersionsEntryR\x08versions\x1a;\n\rVersionsEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [], + { + "ProcessingSoftware": ProcessingSoftware, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/processing.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/product_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/product_pb.py new file mode 100644 index 0000000..b9d37a7 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/product_pb.py @@ -0,0 +1,113 @@ +# Generated from datasets/stac/v1/product.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_ProductPropertiesFields: TypeAlias = Literal["type", "timeliness", "timeliness_category", "acquisition_type"] + +class ProductProperties(Message[_ProductPropertiesFields]): + """ + ProductProperties contains all Product 1.0 fields. In particular, type is an open string rather than an enum. + + ```proto + message datasets.stac.v1.ProductProperties + ``` + + Attributes: + type: + ```proto + optional string type = 1; + ``` + timeliness: + ISO 8601 duration. This remains a string because calendar durations cannot always be represented by protobuf Duration. + + ```proto + optional string timeliness = 2; + ``` + timeliness_category: + ```proto + optional string timeliness_category = 3; + ``` + acquisition_type: + ```proto + datasets.stac.v1.ProductAcquisitionType acquisition_type = 4; + ``` + """ + + __slots__ = ("type", "timeliness", "timeliness_category", "acquisition_type") + + if TYPE_CHECKING: + + def __init__( + self, + *, + type: str | None = None, + timeliness: str | None = None, + timeliness_category: str | None = None, + acquisition_type: ProductAcquisitionType | None = None, + ) -> None: + pass + + type: str + timeliness: str + timeliness_category: str + acquisition_type: ProductAcquisitionType + +class ProductAcquisitionType(Enum): + """ + ProductAcquisitionType contains the closed Product 1.0 product:acquisition_type vocabulary. + + ```proto + enum datasets.stac.v1.ProductAcquisitionType + ``` + + Attributes: + UNSPECIFIED: + ```proto + PRODUCT_ACQUISITION_TYPE_UNSPECIFIED = 0 + ``` + NOMINAL: + ```proto + PRODUCT_ACQUISITION_TYPE_NOMINAL = 1 + ``` + CALIBRATION: + ```proto + PRODUCT_ACQUISITION_TYPE_CALIBRATION = 2 + ``` + OTHER: + ```proto + PRODUCT_ACQUISITION_TYPE_OTHER = 3 + ``` + """ + + UNSPECIFIED = 0 + NOMINAL = 1 + CALIBRATION = 2 + OTHER = 3 + + +_DESC = file_desc( + b'\n\x1edatasets/stac/v1/product.proto\x12\x10datasets.stac.v1"\xe2\x01\n\x11ProductProperties\x12\x19\n\x04type\x18\x01 \x01(\tR\x04typeB\x05\xaa\x01\x02\x08\x01\x12%\n\ntimeliness\x18\x02 \x01(\tR\ntimelinessB\x05\xaa\x01\x02\x08\x01\x126\n\x13timeliness_category\x18\x03 \x01(\tR\x12timelinessCategoryB\x05\xaa\x01\x02\x08\x01\x12S\n\x10acquisition_type\x18\x04 \x01(\x0e2(.datasets.stac.v1.ProductAcquisitionTypeR\x0facquisitionType*\xb6\x01\n\x16ProductAcquisitionType\x12(\n$PRODUCT_ACQUISITION_TYPE_UNSPECIFIED\x10\x00\x12$\n PRODUCT_ACQUISITION_TYPE_NOMINAL\x10\x01\x12(\n$PRODUCT_ACQUISITION_TYPE_CALIBRATION\x10\x02\x12"\n\x1ePRODUCT_ACQUISITION_TYPE_OTHER\x10\x03B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [], + { + "ProductProperties": ProductProperties, + "ProductAcquisitionType": ProductAcquisitionType, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/product.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/sar_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/sar_pb.py new file mode 100644 index 0000000..c71fe03 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/sar_pb.py @@ -0,0 +1,307 @@ +# Generated from datasets/stac/v1/sar.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc + +if TYPE_CHECKING: + from protobuf import DescFile + + +_SARPropertiesFields: TypeAlias = Literal["polarizations", "instrument_mode", "frequency_band", "center_frequency", "bandwidth", "resolution_range", "resolution_azimuth", "pixel_spacing_range", "pixel_spacing_azimuth", "looks_range", "looks_azimuth", "looks_equivalent_number", "observation_direction", "relative_burst", "beam_ids"] + +class SARProperties(Message[_SARPropertiesFields]): + """ + SARProperties contains the SAR 1.3.2 fields for an Asset. Item-property fields remain flattened in generated dataset + schemas and may reuse these enum types. Deprecated sar:product_type is normalized to Product 1.0. + + ```proto + message datasets.stac.v1.SARProperties + ``` + + Attributes: + polarizations: + ```proto + repeated datasets.stac.v1.SARPolarization polarizations = 1 [packed = true]; + ``` + instrument_mode: + Open provider- and mission-specific vocabulary. Deprecated by SAR 1.3.2 but retained for source compatibility. + + ```proto + optional string instrument_mode = 2; + ``` + frequency_band: + ```proto + datasets.stac.v1.SARFrequencyBand frequency_band = 3; + ``` + center_frequency: + Center frequency in gigahertz. + + ```proto + optional double center_frequency = 4; + ``` + bandwidth: + Range bandwidth in gigahertz. + + ```proto + optional double bandwidth = 5; + ``` + resolution_range: + Resolution and pixel spacing values are in metres. + + ```proto + optional double resolution_range = 6; + ``` + resolution_azimuth: + ```proto + optional double resolution_azimuth = 7; + ``` + pixel_spacing_range: + ```proto + optional double pixel_spacing_range = 8; + ``` + pixel_spacing_azimuth: + ```proto + optional double pixel_spacing_azimuth = 9; + ``` + looks_range: + ```proto + optional uint64 looks_range = 10; + ``` + looks_azimuth: + ```proto + optional uint64 looks_azimuth = 11; + ``` + looks_equivalent_number: + ```proto + optional double looks_equivalent_number = 12; + ``` + observation_direction: + ```proto + datasets.stac.v1.SARObservationDirection observation_direction = 13; + ``` + relative_burst: + ```proto + optional uint64 relative_burst = 14; + ``` + beam_ids: + ```proto + repeated string beam_ids = 15; + ``` + """ + + __slots__ = ("polarizations", "instrument_mode", "frequency_band", "center_frequency", "bandwidth", "resolution_range", "resolution_azimuth", "pixel_spacing_range", "pixel_spacing_azimuth", "looks_range", "looks_azimuth", "looks_equivalent_number", "observation_direction", "relative_burst", "beam_ids") + + if TYPE_CHECKING: + + def __init__( + self, + *, + polarizations: list[SARPolarization] | None = None, + instrument_mode: str | None = None, + frequency_band: SARFrequencyBand | None = None, + center_frequency: float | None = None, + bandwidth: float | None = None, + resolution_range: float | None = None, + resolution_azimuth: float | None = None, + pixel_spacing_range: float | None = None, + pixel_spacing_azimuth: float | None = None, + looks_range: int | None = None, + looks_azimuth: int | None = None, + looks_equivalent_number: float | None = None, + observation_direction: SARObservationDirection | None = None, + relative_burst: int | None = None, + beam_ids: list[str] | None = None, + ) -> None: + pass + + polarizations: list[SARPolarization] + instrument_mode: str + frequency_band: SARFrequencyBand + center_frequency: float + bandwidth: float + resolution_range: float + resolution_azimuth: float + pixel_spacing_range: float + pixel_spacing_azimuth: float + looks_range: int + looks_azimuth: int + looks_equivalent_number: float + observation_direction: SARObservationDirection + relative_burst: int + beam_ids: list[str] + +class SARFrequencyBand(Enum): + """ + SARFrequencyBand contains the closed SAR 1.3.2 sar:frequency_band vocabulary. + + ```proto + enum datasets.stac.v1.SARFrequencyBand + ``` + + Attributes: + UNSPECIFIED: + ```proto + SAR_FREQUENCY_BAND_UNSPECIFIED = 0 + ``` + P: + ```proto + SAR_FREQUENCY_BAND_P = 1 + ``` + L: + ```proto + SAR_FREQUENCY_BAND_L = 2 + ``` + S: + ```proto + SAR_FREQUENCY_BAND_S = 3 + ``` + C: + ```proto + SAR_FREQUENCY_BAND_C = 4 + ``` + X: + ```proto + SAR_FREQUENCY_BAND_X = 5 + ``` + KU: + ```proto + SAR_FREQUENCY_BAND_KU = 6 + ``` + K: + ```proto + SAR_FREQUENCY_BAND_K = 7 + ``` + KA: + ```proto + SAR_FREQUENCY_BAND_KA = 8 + ``` + """ + + UNSPECIFIED = 0 + P = 1 + L = 2 + S = 3 + C = 4 + X = 5 + KU = 6 + K = 7 + KA = 8 + +class SARPolarization(Enum): + """ + SARPolarization contains the closed SAR 1.3.2 sar:polarizations element vocabulary. + + ```proto + enum datasets.stac.v1.SARPolarization + ``` + + Attributes: + UNSPECIFIED: + ```proto + SAR_POLARIZATION_UNSPECIFIED = 0 + ``` + HH: + ```proto + SAR_POLARIZATION_HH = 1 + ``` + VV: + ```proto + SAR_POLARIZATION_VV = 2 + ``` + HV: + ```proto + SAR_POLARIZATION_HV = 3 + ``` + VH: + ```proto + SAR_POLARIZATION_VH = 4 + ``` + LH: + ```proto + SAR_POLARIZATION_LH = 5 + ``` + LV: + ```proto + SAR_POLARIZATION_LV = 6 + ``` + RH: + ```proto + SAR_POLARIZATION_RH = 7 + ``` + RV: + ```proto + SAR_POLARIZATION_RV = 8 + ``` + CH: + ```proto + SAR_POLARIZATION_CH = 9 + ``` + CV: + ```proto + SAR_POLARIZATION_CV = 10 + ``` + """ + + UNSPECIFIED = 0 + HH = 1 + VV = 2 + HV = 3 + VH = 4 + LH = 5 + LV = 6 + RH = 7 + RV = 8 + CH = 9 + CV = 10 + +class SARObservationDirection(Enum): + """ + SARObservationDirection contains the closed SAR 1.3.2 sar:observation_direction vocabulary. + + ```proto + enum datasets.stac.v1.SARObservationDirection + ``` + + Attributes: + UNSPECIFIED: + ```proto + SAR_OBSERVATION_DIRECTION_UNSPECIFIED = 0 + ``` + LEFT: + ```proto + SAR_OBSERVATION_DIRECTION_LEFT = 1 + ``` + RIGHT: + ```proto + SAR_OBSERVATION_DIRECTION_RIGHT = 2 + ``` + """ + + UNSPECIFIED = 0 + LEFT = 1 + RIGHT = 2 + + +_DESC = file_desc( + b'\n\x1adatasets/stac/v1/sar.proto\x12\x10datasets.stac.v1"\xc0\x06\n\rSARProperties\x12G\n\rpolarizations\x18\x01 \x03(\x0e2!.datasets.stac.v1.SARPolarizationR\rpolarizations\x12.\n\x0finstrument_mode\x18\x02 \x01(\tR\x0einstrumentModeB\x05\xaa\x01\x02\x08\x01\x12I\n\x0efrequency_band\x18\x03 \x01(\x0e2".datasets.stac.v1.SARFrequencyBandR\rfrequencyBand\x120\n\x10center_frequency\x18\x04 \x01(\x01R\x0fcenterFrequencyB\x05\xaa\x01\x02\x08\x01\x12#\n\tbandwidth\x18\x05 \x01(\x01R\tbandwidthB\x05\xaa\x01\x02\x08\x01\x120\n\x10resolution_range\x18\x06 \x01(\x01R\x0fresolutionRangeB\x05\xaa\x01\x02\x08\x01\x124\n\x12resolution_azimuth\x18\x07 \x01(\x01R\x11resolutionAzimuthB\x05\xaa\x01\x02\x08\x01\x125\n\x13pixel_spacing_range\x18\x08 \x01(\x01R\x11pixelSpacingRangeB\x05\xaa\x01\x02\x08\x01\x129\n\x15pixel_spacing_azimuth\x18\t \x01(\x01R\x13pixelSpacingAzimuthB\x05\xaa\x01\x02\x08\x01\x12&\n\x0blooks_range\x18\n \x01(\x04R\nlooksRangeB\x05\xaa\x01\x02\x08\x01\x12*\n\rlooks_azimuth\x18\x0b \x01(\x04R\x0clooksAzimuthB\x05\xaa\x01\x02\x08\x01\x12=\n\x17looks_equivalent_number\x18\x0c \x01(\x01R\x15looksEquivalentNumberB\x05\xaa\x01\x02\x08\x01\x12^\n\x15observation_direction\x18\r \x01(\x0e2).datasets.stac.v1.SARObservationDirectionR\x14observationDirection\x12,\n\x0erelative_burst\x18\x0e \x01(\x04R\rrelativeBurstB\x05\xaa\x01\x02\x08\x01\x12\x19\n\x08beam_ids\x18\x0f \x03(\tR\x07beamIds*\x88\x02\n\x10SARFrequencyBand\x12"\n\x1eSAR_FREQUENCY_BAND_UNSPECIFIED\x10\x00\x12\x18\n\x14SAR_FREQUENCY_BAND_P\x10\x01\x12\x18\n\x14SAR_FREQUENCY_BAND_L\x10\x02\x12\x18\n\x14SAR_FREQUENCY_BAND_S\x10\x03\x12\x18\n\x14SAR_FREQUENCY_BAND_C\x10\x04\x12\x18\n\x14SAR_FREQUENCY_BAND_X\x10\x05\x12\x19\n\x15SAR_FREQUENCY_BAND_KU\x10\x06\x12\x18\n\x14SAR_FREQUENCY_BAND_K\x10\x07\x12\x19\n\x15SAR_FREQUENCY_BAND_KA\x10\x08*\xad\x02\n\x0fSARPolarization\x12 \n\x1cSAR_POLARIZATION_UNSPECIFIED\x10\x00\x12\x17\n\x13SAR_POLARIZATION_HH\x10\x01\x12\x17\n\x13SAR_POLARIZATION_VV\x10\x02\x12\x17\n\x13SAR_POLARIZATION_HV\x10\x03\x12\x17\n\x13SAR_POLARIZATION_VH\x10\x04\x12\x17\n\x13SAR_POLARIZATION_LH\x10\x05\x12\x17\n\x13SAR_POLARIZATION_LV\x10\x06\x12\x17\n\x13SAR_POLARIZATION_RH\x10\x07\x12\x17\n\x13SAR_POLARIZATION_RV\x10\x08\x12\x17\n\x13SAR_POLARIZATION_CH\x10\t\x12\x17\n\x13SAR_POLARIZATION_CV\x10\n*\x8d\x01\n\x17SARObservationDirection\x12)\n%SAR_OBSERVATION_DIRECTION_UNSPECIFIED\x10\x00\x12"\n\x1eSAR_OBSERVATION_DIRECTION_LEFT\x10\x01\x12#\n\x1fSAR_OBSERVATION_DIRECTION_RIGHT\x10\x02B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [], + { + "SARProperties": SARProperties, + "SARFrequencyBand": SARFrequencyBand, + "SARPolarization": SARPolarization, + "SARObservationDirection": SARObservationDirection, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/sar.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/satellite_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/satellite_pb.py new file mode 100644 index 0000000..e07df68 --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/satellite_pb.py @@ -0,0 +1,186 @@ +# Generated from datasets/stac/v1/satellite.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc +from protobuf.wkt import timestamp_pb + +if TYPE_CHECKING: + from protobuf import DescFile + from protobuf.wkt import Timestamp + + +_SatelliteOrbitStateVectorFields: TypeAlias = Literal["datetime", "values"] + +class SatelliteOrbitStateVector(Message[_SatelliteOrbitStateVectorFields]): + """ + SatelliteOrbitStateVector models one entry in the Satellite 1.2 sat:orbit_state_vectors map. + + ```proto + message datasets.stac.v1.SatelliteOrbitStateVector + ``` + + Attributes: + datetime: + ```proto + optional google.protobuf.Timestamp datetime = 1; + ``` + values: + ECI position x/y/z in metres, velocity x/y/z in metres per second, and optional acceleration x/y/z in metres per + second squared. The order matches the STAC JSON array. + + ```proto + repeated double values = 2 [packed = true]; + ``` + """ + + __slots__ = ("datetime", "values") + + if TYPE_CHECKING: + + def __init__( + self, + *, + datetime: Timestamp | None = None, + values: list[float] | None = None, + ) -> None: + pass + + datetime: Timestamp | None + values: list[float] + +_SatellitePropertiesFields: TypeAlias = Literal["platform_international_designator", "orbit_state", "absolute_orbit", "relative_orbit", "orbit_cycle", "orbit_state_vectors", "anx_datetime", "acquisition_station"] + +class SatelliteProperties(Message[_SatellitePropertiesFields]): + """ + SatelliteProperties contains all Satellite 1.2 fields for an Asset. Item-property fields remain flattened in generated + dataset schemas and may reuse SatelliteOrbitState and SatelliteOrbitStateVector. + + ```proto + message datasets.stac.v1.SatelliteProperties + ``` + + Attributes: + platform_international_designator: + ```proto + optional string platform_international_designator = 1; + ``` + orbit_state: + ```proto + datasets.stac.v1.SatelliteOrbitState orbit_state = 2; + ``` + absolute_orbit: + ```proto + optional uint64 absolute_orbit = 3; + ``` + relative_orbit: + ```proto + optional uint64 relative_orbit = 4; + ``` + orbit_cycle: + ```proto + optional uint64 orbit_cycle = 5; + ``` + orbit_state_vectors: + ```proto + repeated datasets.stac.v1.SatelliteOrbitStateVector orbit_state_vectors = 6; + ``` + anx_datetime: + ```proto + optional google.protobuf.Timestamp anx_datetime = 7; + ``` + acquisition_station: + ```proto + optional string acquisition_station = 8; + ``` + """ + + __slots__ = ("platform_international_designator", "orbit_state", "absolute_orbit", "relative_orbit", "orbit_cycle", "orbit_state_vectors", "anx_datetime", "acquisition_station") + + if TYPE_CHECKING: + + def __init__( + self, + *, + platform_international_designator: str | None = None, + orbit_state: SatelliteOrbitState | None = None, + absolute_orbit: int | None = None, + relative_orbit: int | None = None, + orbit_cycle: int | None = None, + orbit_state_vectors: list[SatelliteOrbitStateVector] | None = None, + anx_datetime: Timestamp | None = None, + acquisition_station: str | None = None, + ) -> None: + pass + + platform_international_designator: str + orbit_state: SatelliteOrbitState + absolute_orbit: int + relative_orbit: int + orbit_cycle: int + orbit_state_vectors: list[SatelliteOrbitStateVector] + anx_datetime: Timestamp | None + acquisition_station: str + +class SatelliteOrbitState(Enum): + """ + SatelliteOrbitState contains the closed Satellite 1.2 sat:orbit_state vocabulary. It describes the acquisition's + orbit direction or class; it is distinct from the timestamped position and velocity samples in orbit_state_vectors. + + ```proto + enum datasets.stac.v1.SatelliteOrbitState + ``` + + Attributes: + UNSPECIFIED: + ```proto + SATELLITE_ORBIT_STATE_UNSPECIFIED = 0 + ``` + ASCENDING: + ```proto + SATELLITE_ORBIT_STATE_ASCENDING = 1 + ``` + DESCENDING: + ```proto + SATELLITE_ORBIT_STATE_DESCENDING = 2 + ``` + GEOSTATIONARY: + ```proto + SATELLITE_ORBIT_STATE_GEOSTATIONARY = 3 + ``` + CROSSING: + ```proto + SATELLITE_ORBIT_STATE_CROSSING = 4 + ``` + """ + + UNSPECIFIED = 0 + ASCENDING = 1 + DESCENDING = 2 + GEOSTATIONARY = 3 + CROSSING = 4 + + +_DESC = file_desc( + b'\n datasets/stac/v1/satellite.proto\x12\x10datasets.stac.v1\x1a\x1fgoogle/protobuf/timestamp.proto"k\n\x19SatelliteOrbitStateVector\x126\n\x08datetime\x18\x01 \x01(\x0b2\x1a.google.protobuf.TimestampR\x08datetime\x12\x16\n\x06values\x18\x02 \x03(\x01R\x06values"\x88\x04\n\x13SatelliteProperties\x12Q\n!platform_international_designator\x18\x01 \x01(\tR\x1fplatformInternationalDesignatorB\x05\xaa\x01\x02\x08\x01\x12F\n\x0borbit_state\x18\x02 \x01(\x0e2%.datasets.stac.v1.SatelliteOrbitStateR\norbitState\x12,\n\x0eabsolute_orbit\x18\x03 \x01(\x04R\rabsoluteOrbitB\x05\xaa\x01\x02\x08\x01\x12,\n\x0erelative_orbit\x18\x04 \x01(\x04R\rrelativeOrbitB\x05\xaa\x01\x02\x08\x01\x12&\n\x0borbit_cycle\x18\x05 \x01(\x04R\norbitCycleB\x05\xaa\x01\x02\x08\x01\x12[\n\x13orbit_state_vectors\x18\x06 \x03(\x0b2+.datasets.stac.v1.SatelliteOrbitStateVectorR\x11orbitStateVectors\x12=\n\x0canx_datetime\x18\x07 \x01(\x0b2\x1a.google.protobuf.TimestampR\x0banxDatetime\x126\n\x13acquisition_station\x18\x08 \x01(\tR\x12acquisitionStationB\x05\xaa\x01\x02\x08\x01*\xd4\x01\n\x13SatelliteOrbitState\x12%\n!SATELLITE_ORBIT_STATE_UNSPECIFIED\x10\x00\x12#\n\x1fSATELLITE_ORBIT_STATE_ASCENDING\x10\x01\x12$\n SATELLITE_ORBIT_STATE_DESCENDING\x10\x02\x12\'\n#SATELLITE_ORBIT_STATE_GEOSTATIONARY\x10\x03\x12"\n\x1eSATELLITE_ORBIT_STATE_CROSSING\x10\x04B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [ + timestamp_pb.desc(), + ], + { + "SatelliteOrbitStateVector": SatelliteOrbitStateVector, + "SatelliteProperties": SatelliteProperties, + "SatelliteOrbitState": SatelliteOrbitState, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/satellite.proto`.""" + return _DESC diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb.py new file mode 100644 index 0000000..e6052fc --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/storage_pb.py @@ -0,0 +1,214 @@ +# Generated from datasets/stac/v1/storage.proto. DO NOT EDIT. +# Generated by protoc-gen-py v0.2.0 with parameter "init_files=false". +# ruff: noqa: PGH004 +# ruff: noqa +# fmt: off + +from __future__ import annotations + +from typing import Literal, TYPE_CHECKING, TypeAlias + +from protobuf import Enum, Message +from protobuf._codegen import file_desc +from protobuf.wkt import struct_pb + +if TYPE_CHECKING: + from protobuf import DescFile + from protobuf.wkt import Value + + +_StorageFields: TypeAlias = Literal["schemes"] + +class Storage(Message[_StorageFields]): + """ + Storage stores the Storage 2.0 storage:schemes registry. + + ```proto + message datasets.stac.v1.Storage + ``` + + Attributes: + schemes: + ```proto + map schemes = 1; + ``` + """ + + __slots__ = ("schemes",) + + if TYPE_CHECKING: + + def __init__( + self, + *, + schemes: dict[str, StorageScheme] | None = None, + ) -> None: + pass + + schemes: dict[str, StorageScheme] + +_StorageSchemeFields: TypeAlias = Literal["known_type", "custom_type", "platform", "title", "description", "region", "requester_pays", "storage_class", "bucket", "account", "additional_properties"] + +class StorageScheme(Message[_StorageSchemeFields]): + """ + StorageScheme describes a storage provider independently of concrete asset hrefs. + + ```proto + message datasets.stac.v1.StorageScheme + ``` + + Attributes: + known_type: + Exactly one compact known type or exact custom Storage 2.0 type string is set. + + ```proto + optional datasets.stac.v1.KnownStorageType known_type = 1; + ``` + custom_type: + ```proto + optional string custom_type = 2; + ``` + platform: + Provider endpoint/API URI or URI template, not an asset href prefix. + + ```proto + string platform = 3; + ``` + title: + ```proto + optional string title = 4; + ``` + description: + ```proto + optional string description = 5; + ``` + region: + ```proto + optional string region = 6; + ``` + requester_pays: + ```proto + optional bool requester_pays = 7; + ``` + storage_class: + Maps the provider-specific `class` property used by the Copernicus example. + + ```proto + optional string storage_class = 8; + ``` + bucket: + Bucket variable used by the standard AWS S3 platform template and by provider-specific templates when applicable. + + ```proto + optional string bucket = 9; + ``` + account: + Account variable used by the standard Microsoft Azure platform template. + + ```proto + optional string account = 10; + ``` + additional_properties: + Additional provider properties and URI-template variables. Canonical STAC conversion flattens these entries into + the Storage Scheme Object and must reject collisions with the typed properties above. + + ```proto + map additional_properties = 11; + ``` + """ + + __slots__ = ("known_type", "custom_type", "platform", "title", "description", "region", "requester_pays", "storage_class", "bucket", "account", "additional_properties") + + if TYPE_CHECKING: + + def __init__( + self, + *, + known_type: KnownStorageType | None = None, + custom_type: str | None = None, + platform: str = "", + title: str | None = None, + description: str | None = None, + region: str | None = None, + requester_pays: bool | None = None, + storage_class: str | None = None, + bucket: str | None = None, + account: str | None = None, + additional_properties: dict[str, Value] | None = None, + ) -> None: + pass + + known_type: KnownStorageType + custom_type: str + platform: str + title: str + description: str + region: str + requester_pays: bool + storage_class: str + bucket: str + account: str + additional_properties: dict[str, Value] + +class KnownStorageType(Enum): + """ + KnownStorageType is Tilebox's compact dictionary for common open Storage 2.0 type strings. + + ```proto + enum datasets.stac.v1.KnownStorageType + ``` + + Attributes: + UNSPECIFIED: + ```proto + KNOWN_STORAGE_TYPE_UNSPECIFIED = 0 + ``` + AWS_S3: + aws-s3 + + ```proto + KNOWN_STORAGE_TYPE_AWS_S3 = 1 + ``` + CUSTOM_S3: + custom-s3 + + ```proto + KNOWN_STORAGE_TYPE_CUSTOM_S3 = 2 + ``` + MICROSOFT_AZURE: + ms-azure + + ```proto + KNOWN_STORAGE_TYPE_MICROSOFT_AZURE = 3 + ``` + GOOGLE_CLOUD_STORAGE: + Tilebox convention exported as the custom Storage type string `google-cloud-storage`. + + ```proto + KNOWN_STORAGE_TYPE_GOOGLE_CLOUD_STORAGE = 4 + ``` + """ + + UNSPECIFIED = 0 + AWS_S3 = 1 + CUSTOM_S3 = 2 + MICROSOFT_AZURE = 3 + GOOGLE_CLOUD_STORAGE = 4 + + +_DESC = file_desc( + b'\n\x1edatasets/stac/v1/storage.proto\x12\x10datasets.stac.v1\x1a\x1cgoogle/protobuf/struct.proto"\xa8\x01\n\x07Storage\x12@\n\x07schemes\x18\x01 \x03(\x0b2&.datasets.stac.v1.Storage.SchemesEntryR\x07schemes\x1a[\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x125\n\x05value\x18\x02 \x01(\x0b2\x1f.datasets.stac.v1.StorageSchemeR\x05value:\x028\x01"\xed\x04\n\rStorageScheme\x12H\n\nknown_type\x18\x01 \x01(\x0e2".datasets.stac.v1.KnownStorageTypeR\tknownTypeB\x05\xaa\x01\x02\x08\x01\x12&\n\x0bcustom_type\x18\x02 \x01(\tR\ncustomTypeB\x05\xaa\x01\x02\x08\x01\x12\x1a\n\x08platform\x18\x03 \x01(\tR\x08platform\x12\x1b\n\x05title\x18\x04 \x01(\tR\x05titleB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x05 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06region\x18\x06 \x01(\tR\x06regionB\x05\xaa\x01\x02\x08\x01\x12,\n\x0erequester_pays\x18\x07 \x01(\x08R\rrequesterPaysB\x05\xaa\x01\x02\x08\x01\x12*\n\rstorage_class\x18\x08 \x01(\tR\x0cstorageClassB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06bucket\x18\t \x01(\tR\x06bucketB\x05\xaa\x01\x02\x08\x01\x12\x1f\n\x07account\x18\n \x01(\tR\x07accountB\x05\xaa\x01\x02\x08\x01\x12n\n\x15additional_properties\x18\x0b \x03(\x0b29.datasets.stac.v1.StorageScheme.AdditionalPropertiesEntryR\x14additionalProperties\x1a_\n\x19AdditionalPropertiesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b2\x16.google.protobuf.ValueR\x05value:\x028\x01*\xcc\x01\n\x10KnownStorageType\x12"\n\x1eKNOWN_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\x1d\n\x19KNOWN_STORAGE_TYPE_AWS_S3\x10\x01\x12 \n\x1cKNOWN_STORAGE_TYPE_CUSTOM_S3\x10\x02\x12&\n"KNOWN_STORAGE_TYPE_MICROSOFT_AZURE\x10\x03\x12+\n\'KNOWN_STORAGE_TYPE_GOOGLE_CLOUD_STORAGE\x10\x04B\x05\x92\x03\x02\x08\x02b\x08editionsp\xe8\x07', + [ + struct_pb.desc(), + ], + { + "Storage": Storage, + "StorageScheme": StorageScheme, + "KnownStorageType": KnownStorageType, + }, +) + + +def desc() -> DescFile: + """Returns the descriptor for the file `datasets/stac/v1/storage.proto`.""" + return _DESC diff --git a/tilebox-grpc/pyproject.toml b/tilebox-grpc/pyproject.toml index 3d153d6..e658a4e 100644 --- a/tilebox-grpc/pyproject.toml +++ b/tilebox-grpc/pyproject.toml @@ -26,7 +26,8 @@ dependencies = [ # grpcio 1.66 also has this bug on MacOS still: https://github.com/grpc/grpc/issues/37642 "grpcio>=1.70", "grpcio-status>=1.70", - "connectrpc>=0.10.1", + # connectrpc 0.11 switches from Google's protobuf runtime to protobuf-py + "connectrpc>=0.10.1,<0.11.0", "pyqwest>=0.6.2", "protobuf>=6", # for the libraries below we specify a minimum, tested to be working version diff --git a/tools/generate_protobuf.py b/tools/generate_protobuf.py index ab53d99..4ab9215 100755 --- a/tools/generate_protobuf.py +++ b/tools/generate_protobuf.py @@ -27,6 +27,7 @@ def main() -> None: # client-side validation, and validation-blind messages avoid global descriptor conflicts. print("Running buf generate") # noqa: T201 os.system("buf generate --template buf.gen.datasets.yaml") # noqa: S605, S607 + os.system("buf generate --template buf.gen.datasets-bufpy.yaml") # noqa: S605, S607 os.system("buf generate --template buf.gen.workflows.yaml") # noqa: S605, S607 package_mapping = { diff --git a/uv.lock b/uv.lock index 99db024..e97b7c0 100644 --- a/uv.lock +++ b/uv.lock @@ -560,15 +560,15 @@ wheels = [ [[package]] name = "connectrpc" -version = "0.11.1" +version = "0.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf-py" }, + { name = "protobuf" }, { name = "pyqwest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/b5/63e14ab9d4d4cc58818562db4120a35fa7454e3035633da41bdc6b712abd/connectrpc-0.11.1.tar.gz", hash = "sha256:18277f7838847b4271ca38d40c7d2387b5a2ea6a29f240689c19e1ec84aaff66", size = 46222, upload-time = "2026-07-15T06:33:31.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/11/adb3ab104282202000b1c98213c70c7068d76917d9f17c83b818e3d212c1/connectrpc-0.10.1.tar.gz", hash = "sha256:eaf093a4f0d1b9c854c71839083ed5242a551100e99cf52e3cc5e35663f2a5ea", size = 47043, upload-time = "2026-05-29T02:19:57.2Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/ac/aa647675812cd075f2cb9acb00d62f4f2cbcbc6736049a62342b7371ce5b/connectrpc-0.11.1-py3-none-any.whl", hash = "sha256:8a52e2e92a485fa9681c1101a79a5ebeb31807e3ea3d5aabd41f484a6398bc7b", size = 64991, upload-time = "2026-07-15T06:33:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/cf/67/49a6a2d1d0ee04b4d96410d12c751a44437a9bdf4991d7b88fa5c231c0d2/connectrpc-0.10.1-py3-none-any.whl", hash = "sha256:6e965625bbc4b185532bd98e4919528bea6d1c24619b6dba416d0dfafc10e344", size = 64120, upload-time = "2026-05-29T02:19:55.991Z" }, ] [[package]] @@ -2292,49 +2292,49 @@ wheels = [ [[package]] name = "protobuf-py" -version = "0.1.1" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf-py-ext", marker = "(platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/ed/02fd902d9c51b7ff53dfc9a745eb11490722edfd30073af889e171f07b8e/protobuf_py-0.1.1.tar.gz", hash = "sha256:6bd08ac4d8f1661965bbe2685429d79043704cdd1ee720a7a89617331742240b", size = 133525, upload-time = "2026-06-24T19:02:15.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/56/b93a2ed9588779cd19dff9d62a611a7852987db195a0c9e6ca6d4a66682f/protobuf_py-0.2.0.tar.gz", hash = "sha256:e4f2e08c8c99a0d652f20c7e52d9290e04e14f4f086fc16a83ce728bd509f0ba", size = 148832, upload-time = "2026-07-14T05:15:24.122Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/00/1b3775aca1c70e3007e06ef5996f6bb9b3a32341eb0cce3ffb6effad8dec/protobuf_py-0.1.1-py3-none-any.whl", hash = "sha256:efc4f50f275ed6dae10a1f30bb81ad1a75368557b3ff22a532b7a472050368f1", size = 181656, upload-time = "2026-06-24T19:01:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/74/78/f86aba47416d66fab4f522bca7916920b94e5e5cf73e79ea49c6a5e5c8ab/protobuf_py-0.2.0-py3-none-any.whl", hash = "sha256:6811340f5a803c4e23575d9ee47c00c1385e18d93964b8cbacb0fa86eb3378f3", size = 199903, upload-time = "2026-07-14T05:14:46.744Z" }, ] [[package]] name = "protobuf-py-ext" -version = "0.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/05/6dc9ccff1e8159eb9a144e6d3c4acfd2211cd4fcd20c34fe9155d17d6a7f/protobuf_py_ext-0.1.1.tar.gz", hash = "sha256:e85bfdfdb3ed50634db8ccc7429dd9286520109489c735463971a418707b4fef", size = 31912, upload-time = "2026-06-24T19:02:16.321Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/70/2b5d62a60a2e0d88ecde1ae98db3132bcd2672fb39c8d581b82b34ac0bd8/protobuf_py_ext-0.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:310039e03cb15181781a0b78017419f6d4ee302e988c3c70b87f1facdf05532d", size = 306008, upload-time = "2026-06-24T19:01:31.399Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d6/73c06bb4cac2e04c0adc154965fe8b6520224bd737fd7e73bba405056321/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ec8348d1ba045f79b2fedd14e40cca36f2a41b52f2c4fdf55a60c58add2353", size = 314769, upload-time = "2026-06-24T19:01:32.89Z" }, - { url = "https://files.pythonhosted.org/packages/18/c5/e4e6bc6096b66d1c82639a1b501147f16ee65d32567304b987d002e2c666/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182798e4861aba72d05855bd06febe4926aa7265e6f444a1b8af5252beee4f7e", size = 328122, upload-time = "2026-06-24T19:01:34.394Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4b/1d792a40d0f0a0f914f1dfa8bb5e9573ca0ecd5fe5cecf80d77193212abd/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9eb25c3a329c0551cc86b209a5e5d8ecb8d834b9924a3aa019377853a703b6d3", size = 492338, upload-time = "2026-06-24T19:01:36.103Z" }, - { url = "https://files.pythonhosted.org/packages/52/51/5ad329223c905e5c530cae38ae24cde3584d8ab7e09457f2a01afce80e60/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aaecbde82bef10c7c40578cbb61b7a19896bf7fa450972050a3bb302acb7d5d6", size = 541578, upload-time = "2026-06-24T19:01:37.481Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c7/01bd8a8bfe2df4b07aafac12ccfb7b7ebe2303f65296a9e02fcafa28ff3e/protobuf_py_ext-0.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:6b0c615c48e95acc53cf33e9310eeaff8b30d2d7555bf93e7bca8fb4f40e9a5c", size = 251319, upload-time = "2026-06-24T19:01:38.946Z" }, - { url = "https://files.pythonhosted.org/packages/24/dc/914065538b5db54b6a920b5af38c1ef142252ea1eea711820435fa259fac/protobuf_py_ext-0.1.1-cp310-abi3-win_arm64.whl", hash = "sha256:72956cd0af5dee24b41c6f5ba5e42622d17e6d555002b5efc1634e27a1446de2", size = 241635, upload-time = "2026-06-24T19:01:40.301Z" }, - { url = "https://files.pythonhosted.org/packages/13/a5/0b5d73cab815fd50615cb87a02e04e8332f9e27380c890aba1459cf7e919/protobuf_py_ext-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f0cd58620f415a3534d195358338f4999a774e12510f91c592b38d13d37388", size = 304903, upload-time = "2026-06-24T19:01:41.796Z" }, - { url = "https://files.pythonhosted.org/packages/37/a9/14c120b9ac36a0e11bb05e482fa6ae322de583a8e1068e4859035c289947/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21572764f625d829604fc4d83635f533f35775f11c6da142345b3f3c8d64bd09", size = 316148, upload-time = "2026-06-24T19:01:43.247Z" }, - { url = "https://files.pythonhosted.org/packages/35/54/7c00a1a9783c3ba74f6b2f5d2f049f09037bbd489c465c09a34f32fb611b/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f14f00c2678bfbe7ad46f057b9f9938c1677bcf39e405e163e272c6f9814b8", size = 326458, upload-time = "2026-06-24T19:01:44.655Z" }, - { url = "https://files.pythonhosted.org/packages/0d/0e/81fa50c0d6c4d664e5be6d0a3c4f2c7edfef87ab12d0bac764abca1f2955/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1610865622e2e27568277ca63d8d2d23dcc55eaa767398865c21539ddf4ce24d", size = 493596, upload-time = "2026-06-24T19:01:46.344Z" }, - { url = "https://files.pythonhosted.org/packages/56/19/3183cf6e4de62846c20549654a1d573dae51ca06aaf7fed06accdfab74f6/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8995efba9476e1ea18ef9873306871dba697308994bc2766558fec3387acc3de", size = 539656, upload-time = "2026-06-24T19:01:48.21Z" }, - { url = "https://files.pythonhosted.org/packages/a4/6c/09841f3dbe7c3d4b3f2779f8f2832ac23fb96ac8ab71674e91af732cf9ff/protobuf_py_ext-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba61d49dace8f874361583030a7c48139b42eb37c9ffbb1e7e8a227a51576f44", size = 305017, upload-time = "2026-06-24T19:01:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/52/43/e450b5e202f6715274acc9acd9f9769908cbdeab861a53c798fcbd467d35/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a9c2f026096ca4c595c89a297067ef371e241d3ff8e1f6d7c779aa8419cdca7", size = 316215, upload-time = "2026-06-24T19:01:51.249Z" }, - { url = "https://files.pythonhosted.org/packages/97/5c/23e79b35f1b5755b9bdc0c0c9b8cd8abababaef1a1639608d8a96bb61a9d/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf78707a040b9294e5e1ec4a1875f0046acfe52e92150cea27de4e9fc9db39bb", size = 326419, upload-time = "2026-06-24T19:01:52.93Z" }, - { url = "https://files.pythonhosted.org/packages/e8/de/5493de0ec12920a3b1dd72608ce0c1e8cdb7e0eb9e87accaecc5216df29e/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4373845cbb85bdade3ef5368cc2f4b5f80bf173383afc1ab063f95644e5599", size = 493734, upload-time = "2026-06-24T19:01:54.301Z" }, - { url = "https://files.pythonhosted.org/packages/98/89/31da55b414e6332aad11d858e9a86bd36fbb324f52fa3fc6d8f1c57840a9/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a4cc478eef7a2acc1daaebcde479a3ac2396d47e6bdc7e776ca4c4147ba8b4c", size = 539703, upload-time = "2026-06-24T19:01:55.707Z" }, - { url = "https://files.pythonhosted.org/packages/1c/71/39f231838ef06476d46ac40dd814894277a732a3688c0a0c994850b3b62f/protobuf_py_ext-0.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:359dccdc1c3eafed2a913c570bb082b8df848a1d27d548ce8385f246a7d68be2", size = 302145, upload-time = "2026-06-24T19:01:57.116Z" }, - { url = "https://files.pythonhosted.org/packages/87/f1/01c81ff5f420600366a0e6a613ce2af20834534d2b3d7523f4f3b4ddb54f/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c38b4a10a306366443273ee03ca554537a0965bf05b7ada8e7dbdee04cc93e", size = 314541, upload-time = "2026-06-24T19:01:58.745Z" }, - { url = "https://files.pythonhosted.org/packages/86/d8/1cbf5a0298ceddc0cdbb28bf1ecaf8bd3cff54ed4ced6a1392d5226172fc/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79eee3bcbb289d6ea114eb8fe3a1469c5b59bf53e207238469f567d9c53ba56f", size = 325092, upload-time = "2026-06-24T19:02:00.382Z" }, - { url = "https://files.pythonhosted.org/packages/af/23/8b1694616044cbfec2d18d4c6fffb03e05089c8bdb5970c6bafd60cc7fd5/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10992141a8282a71ac3e3530d1a489efb27618d84000b9a47918cf70e5816d9b", size = 491684, upload-time = "2026-06-24T19:02:01.862Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/99997a7a6cf6989c944d9183c5deb6a045c27460add0316c7b34763891b3/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4411ffe0e06a774b83c5c71c546ce097640a25f596c45f95f53d3e3148e3f22d", size = 538048, upload-time = "2026-06-24T19:02:03.362Z" }, - { url = "https://files.pythonhosted.org/packages/15/3c/9e99ecbb68e1d5b46016d821eb1cbba9a91e6b50e71e75faf0c1e6189f0d/protobuf_py_ext-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55a17d80ea419501ff221a6627523f38a431bb33e6aa3de81ae3a7f271c49c75", size = 296337, upload-time = "2026-06-24T19:02:04.787Z" }, - { url = "https://files.pythonhosted.org/packages/af/f2/305338a28225fb54b28d6c3f5948109b9088b329a2b6f77ca610c2bdcd34/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbb518b5638403ceaa08ac4fc7dac626f45ed9b856b3517de3906cf3de4d632", size = 308961, upload-time = "2026-06-24T19:02:06.185Z" }, - { url = "https://files.pythonhosted.org/packages/60/f9/416103c93677ff2ea407704ea64fa6de9e700dc030c48360a182d91ce373/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4adc65ab6a5e4885c67fc808bcacb83d755d05b566d312a0e10c2a873f2ad4", size = 321895, upload-time = "2026-06-24T19:02:07.714Z" }, - { url = "https://files.pythonhosted.org/packages/5a/83/eb3e81bb2f83834b7deff4cee2d56eeb1adcbc847492a56b7f910ab257db/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e97f45c49676efacfb8e95bfcb1a002bc337e618a6780be1e55df2ba5ebd2f1e", size = 486091, upload-time = "2026-06-24T19:02:09.243Z" }, - { url = "https://files.pythonhosted.org/packages/99/96/bd88fca38556b3105e4dd41d6a176e31dcc583fe979e830aeedd2f8c20ee/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:53a6b3590f6aa7f97b8ed60f62fef9b096babfeae82f7283fd3a4c405827d4f8", size = 534582, upload-time = "2026-06-24T19:02:11.227Z" }, +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/8a/45b769a5475f2c95696907265074c3c8113e091a7afaa27e8b4ca7ce0a95/protobuf_py_ext-0.2.0.tar.gz", hash = "sha256:b4a79dc7c341ac05a47081a58b0692ea5ec4530aa9a08c94da0c9023d988080e", size = 31488, upload-time = "2026-07-14T05:15:25.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/98/197802e24a0b1bc138d9a32f9522998d8465bd02b996ae178af8f948d8e2/protobuf_py_ext-0.2.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3a6e1b8d653a26a67b1bc17bb0efda237bc8b9f1179efc415de2549d536e1ae2", size = 304882, upload-time = "2026-07-14T05:14:48.378Z" }, + { url = "https://files.pythonhosted.org/packages/12/7e/db5ace17eef81afafdda3a710e9e1eb376cb2984e2e51cb1b56e9cd49440/protobuf_py_ext-0.2.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7640e2e7cf53d4d8aaa3063659d2ee2be52687e66fd4087039c700b11ba837bf", size = 312969, upload-time = "2026-07-14T05:14:49.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7e/81b63b4de45f3a91b3fa6a6d209d325f9c3c69289dc81c75f3305fadd9ef/protobuf_py_ext-0.2.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:959acb10bd84fabfbbe09d10f3c45a8cb59fce803f97a6af0805dc2a93f739d3", size = 327164, upload-time = "2026-07-14T05:14:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a5/5d9c39d154e6a554c7f6b5a42d370ef4211d369572229681a582e194b6da/protobuf_py_ext-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d2ace3421a6c4ac7a413b84de2910086a54af37ca46aafc4899d4935a2bd5b9", size = 490567, upload-time = "2026-07-14T05:14:52.392Z" }, + { url = "https://files.pythonhosted.org/packages/85/7f/65d2083373d5a19a2900d90dfdcc960968f493b6fceb1dd612d5a537336f/protobuf_py_ext-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:722b69b1993effa4c03ea0e90916273388fa7e2f3077faef8863c99d6ca36da2", size = 540066, upload-time = "2026-07-14T05:14:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/f8/eb/92530ff853c5601e8f9ca65f125fd9832266f5c9a4dff6b2a47dfae8a788/protobuf_py_ext-0.2.0-cp310-abi3-win_amd64.whl", hash = "sha256:cba5e1dfba68ecd69cedfaf8761aa7fb408fa4723f5b3e341208b13f3ebdebb1", size = 250251, upload-time = "2026-07-14T05:14:55.175Z" }, + { url = "https://files.pythonhosted.org/packages/fc/18/c9e603abecf90fa8c08f619e12c018707ae8ce8ee34a30102cda1963656f/protobuf_py_ext-0.2.0-cp310-abi3-win_arm64.whl", hash = "sha256:3cafe6fd772e661f6b593223412795026abb5a14e7c645823a27f01d53b3af4b", size = 240701, upload-time = "2026-07-14T05:14:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/63/2d/66603f517b15073d0638be557cb64e12ca3ab4a7e1996de9c4d8afa20a8d/protobuf_py_ext-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e37bb6880e7065c29760fd969414d56aa117c5a707abaa11e98164b7e962452", size = 303802, upload-time = "2026-07-14T05:14:57.565Z" }, + { url = "https://files.pythonhosted.org/packages/01/cc/bfb5b23a94f2447d314fc5cd933dffa981df0e789cfc5c0280fc502190a3/protobuf_py_ext-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f1b91efb7bddea459d70ba86001720e29859a788eaaa8bde2cd8c71ccae1ec4", size = 314159, upload-time = "2026-07-14T05:14:58.735Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ee/9624cc545c6f558ff158732aea9eea6e5f0cb56b919d45a5769582e4dedb/protobuf_py_ext-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69392ef99bfd5b2598466e2cde82d90fc4077924c9a9b24e651aa093fb0f763", size = 325915, upload-time = "2026-07-14T05:15:00.11Z" }, + { url = "https://files.pythonhosted.org/packages/15/55/a962e054435d2cdcb3effa55ec385b8959d588d9d12978ccf53b33d23c6b/protobuf_py_ext-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1cc3cdab302ed310aeed4a4586f0600b458291430ea95d2eb6d1d05d4c6f592", size = 491459, upload-time = "2026-07-14T05:15:01.252Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/7f2701ee6d28c821cfd9a9b123b69d7ecfc6dad067c00a1f6f86e49c9cbe/protobuf_py_ext-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6325c5676a09dd8f129d2d72b5c1f3a97aaf172f5db016abefd5eaf4fc6f53eb", size = 538696, upload-time = "2026-07-14T05:15:02.594Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c4/7e6bf96c5fac84a557036d185c7fbd0f561e89e4459e130f867796333a7b/protobuf_py_ext-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c70440ad22fa38fda6a73987c0fb84ba37252fb23c87ddf2fc8d935bb32a127b", size = 303874, upload-time = "2026-07-14T05:15:03.928Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5c/7acb810f41b5821eee0249b5820ae3a5a6335251b3b70336048d36d0ba1a/protobuf_py_ext-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86a7d7e047c2e3b764cf288cad7578b1be9fac90476a68d22c52112248d94fe6", size = 314215, upload-time = "2026-07-14T05:15:05.039Z" }, + { url = "https://files.pythonhosted.org/packages/03/ea/eb328fd99817e0308cb3d4669c0d553186650c45098ade52aee0520f1293/protobuf_py_ext-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e997e59916d3ab7206c68a858a61eeb2c9bdebb1aad0eccddf5b4c78be2d8cc6", size = 326016, upload-time = "2026-07-14T05:15:06.15Z" }, + { url = "https://files.pythonhosted.org/packages/56/23/a3d58915611515523c0098958e7cd2abb8b9491c37821baf875a9ae901f0/protobuf_py_ext-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12a1ccec44f29060d496d608711825e73f1649bb98ee50d974b769ae2a205d51", size = 491556, upload-time = "2026-07-14T05:15:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/f8/01/3e0f533d540033f61bb088cbc3fad0a245b9d0de93585af6c74fd38996f0/protobuf_py_ext-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f4bc97fd4d50e9029ce9aff57f9af211a4e893a68603180b23cdc20e2f1ffcbd", size = 538726, upload-time = "2026-07-14T05:15:08.75Z" }, + { url = "https://files.pythonhosted.org/packages/4d/f2/47efc784f3753a6b0d726b26b4ebe3c5f3d94ba84a525543432e2aa14fd7/protobuf_py_ext-0.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:35f316c6732f63f4002e6441294da3f730d43624cd179b9cfae3d0ffabbc972a", size = 301017, upload-time = "2026-07-14T05:15:09.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fe/ec47d9fde881176cdd729fcd95032233372d05f8061b4727c5c758637a83/protobuf_py_ext-0.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e126dceb0a83a7146e91520abf648e281a8137f9a4ac86baee358e24721e537d", size = 312792, upload-time = "2026-07-14T05:15:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/548045a14f3ae298cf2f996c952061d58a1dc5eef231786954ad53d2f578/protobuf_py_ext-0.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d5abee03e1031cc905d9443bb0b9d14d6fa8d36c7f64ff0c555ec883d5afef", size = 324495, upload-time = "2026-07-14T05:15:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f1/be/92fd8698a1cb033e2851cf45ab69815aff362a6577cd301c27fed00e9388/protobuf_py_ext-0.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70243a1f803037acdce7022040818cbc55d3348bdc3903ffb1d4b18fecbf18ff", size = 489849, upload-time = "2026-07-14T05:15:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/2a/3b/ee34c785c0cddad7666b6d1d0a1c2524e122c1cc96e1855cb905ceca325b/protobuf_py_ext-0.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f4982b1abf24a30a6414ebbb6001171abcbf8f5b945d4a2ca273d132be879732", size = 536823, upload-time = "2026-07-14T05:15:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/15/5a/032bb471ae761cb4b7021ae54cda3587cdc0fca3fcef396cf27a2f85a8a7/protobuf_py_ext-0.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba51b6298cda70cdabd88a6cc0f998e5d74856254db5bcf009eb22c5248eb0ef", size = 295335, upload-time = "2026-07-14T05:15:16.592Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/082fe80c66ba7b87ceb71c31faebe8989015cb0d7feee2006fba00f1c194/protobuf_py_ext-0.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:626d0982819b316529729223b52676049e0912bd4a75696059317037bf241b0c", size = 306898, upload-time = "2026-07-14T05:15:17.929Z" }, + { url = "https://files.pythonhosted.org/packages/c2/41/364b0f19165d8476ec1a5c121ed0cb2579c952f5872c4d4eeb3df534a22c/protobuf_py_ext-0.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd6fef4b16b564396f96e58663ac077c42d686968c4ce5771c2333cf50e6fbb", size = 321085, upload-time = "2026-07-14T05:15:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/d445897080aca64f36f212d3725a57da2e46983ce517657d831b2542be29/protobuf_py_ext-0.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ded28f377c32b0dc51b134924a54a553fbcaaeb87eaa68f2a80973169ca06b01", size = 484260, upload-time = "2026-07-14T05:15:20.49Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/f41f884487187cd812687782681c31acdf007b00df85927db6e998b81c97/protobuf_py_ext-0.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d303366fdb084d0532e243909b96bbb35fbfd8bb960728489d9729396794711f", size = 533528, upload-time = "2026-07-14T05:15:21.77Z" }, ] [[package]] @@ -3063,6 +3063,7 @@ dependencies = [ { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "promise" }, + { name = "protobuf-py" }, { name = "shapely" }, { name = "tilebox-grpc" }, { name = "tqdm" }, @@ -3086,6 +3087,7 @@ requires-dist = [ { name = "numpy", specifier = ">=1.24" }, { name = "pandas", specifier = ">=2.1" }, { name = "promise", specifier = ">=2.3" }, + { name = "protobuf-py", specifier = ">=0.1.1" }, { name = "shapely", specifier = ">=2" }, { name = "tilebox-grpc", editable = "tilebox-grpc" }, { name = "tqdm", specifier = ">=4.65" }, @@ -3125,7 +3127,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4" }, - { name = "connectrpc", specifier = ">=0.10.1" }, + { name = "connectrpc", specifier = ">=0.10.1,<0.11.0" }, { name = "grpcio", specifier = ">=1.70" }, { name = "grpcio-status", specifier = ">=1.70" }, { name = "lz4", specifier = ">=4" }, From d4e8d61c51c0ea1261fbdaf830692f3c7ee4c71e Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Thu, 30 Jul 2026 11:13:26 +0200 Subject: [PATCH 3/7] Convert asset messages to protobuf-py messages --- .../test_protobuf_xarray.py | 99 ++++++++++++++----- .../protobuf_conversion/field_types.py | 49 +++++++++ 2 files changed, 123 insertions(+), 25 deletions(-) diff --git a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py index fd5896e..0e6dfb4 100644 --- a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py +++ b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py @@ -14,8 +14,19 @@ from tests.data.datapoint import example_datapoints from tests.example_dataset.example_dataset_pb2 import ExampleDatapoint -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets +from tilebox.datasets.datasets.stac.v1.asset_pb import Assets +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Asset as AssetPB2 +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsPB2 +from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationPB2 +from tilebox.datasets.datasets.stac.v1.core_pb import Provider +from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderPB2 +from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware +from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 +from tilebox.datasets.datasets.stac.v1.storage_pb import Storage +from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StoragePB2 from tilebox.datasets.protobuf_conversion.protobuf_xarray import MessageToXarrayConverter +from tilebox.datasets.protobuf_conversion.to_protobuf import to_messages from tilebox.datasets.query.time_interval import timestamp_to_datetime, us_to_datetime @@ -103,43 +114,81 @@ def test_convert_datapoint(datapoint: ExampleDatapoint) -> None: # noqa: PLR091 assert isinstance(dataset.some_repeated_geometry[i].item(), Polygon | MultiPolygon) -def test_convert_unknown_message_fields_as_objects() -> None: +def test_convert_stac_messages_to_protobuf_py() -> None: + message_types = { + "assets": ("datasets.stac.v1.Assets", AssetsPB2(assets=[AssetPB2(key="preview")]), Assets), + "authentication": ("datasets.stac.v1.Authentication", AuthenticationPB2(), Authentication), + "provider": ("datasets.stac.v1.Provider", ProviderPB2(name="Tilebox"), Provider), + "processing_software": ( + "datasets.stac.v1.ProcessingSoftware", + ProcessingSoftwarePB2(versions={"processor": "1.0"}), + ProcessingSoftware, + ), + "storage": ("datasets.stac.v1.Storage", StoragePB2(), Storage), + } file_descriptor = descriptor_pb2.FileDescriptorProto( name="tests/protobuf_conversion/stac_datapoint.proto", package="tests.protobuf_conversion", - dependency=["datasets/stac/v1/asset.proto"], + dependency=[ + "datasets/stac/v1/asset.proto", + "datasets/stac/v1/authentication.proto", + "datasets/stac/v1/core.proto", + "datasets/stac/v1/processing.proto", + "datasets/stac/v1/storage.proto", + ], ) message_descriptor = file_descriptor.message_type.add(name="StacDatapoint") - message_descriptor.field.add( - name="assets", - number=1, - label=descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL, - type=descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE, - type_name=".datasets.stac.v1.Assets", - ) - message_descriptor.field.add( - name="related_assets", - number=2, - label=descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED, - type=descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE, - type_name=".datasets.stac.v1.Assets", - ) + for index, (field_name, (message_name, _, _)) in enumerate(message_types.items()): + message_descriptor.field.add( + name=field_name, + number=index * 2 + 1, + label=descriptor_pb2.FieldDescriptorProto.LABEL_OPTIONAL, + type=descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE, + type_name=f".{message_name}", + ) + message_descriptor.field.add( + name=f"related_{field_name}", + number=index * 2 + 2, + label=descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED, + type=descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE, + type_name=f".{message_name}", + ) descriptor = Default().AddSerializedFile(file_descriptor.SerializeToString()) message_type = GetMessageClass(descriptor.message_types_by_name["StacDatapoint"]) - assets = Assets() - related_assets = Assets() - messages = [message_type(), message_type(assets=assets, related_assets=[related_assets])] + values = {} + for field_name, (_, source_value, _) in message_types.items(): + values[field_name] = source_value + values[f"related_{field_name}"] = [source_value] + messages = [message_type(), message_type(**values)] converter = MessageToXarrayConverter() converter.convert_all(messages) dataset = converter.finalize("time") - assert dataset.assets.dtype == object - assert dataset.assets[0].item() is None - assert dataset.assets[1].item() == assets - assert dataset.related_assets.dtype == object - assert dataset.related_assets[1, 0].item() == related_assets + for field_name, (_, source_value, target_type) in message_types.items(): + assert dataset[field_name].dtype == object + assert dataset[field_name][0].item() is None + converted_value = dataset[field_name][1].item() + assert isinstance(converted_value, target_type) + assert converted_value.to_binary() == source_value.SerializeToString() + + repeated_field_name = f"related_{field_name}" + assert dataset[repeated_field_name].dtype == object + converted_repeated_value = dataset[repeated_field_name][1, 0].item() + assert isinstance(converted_repeated_value, target_type) + assert converted_repeated_value.to_binary() == source_value.SerializeToString() + + roundtripped = to_messages(dataset, message_type) + assert roundtripped == messages + + for repeated_container in (list, tuple): + input_data = {} + for field_name, (_, source_value, target_type) in message_types.items(): + target_value = target_type.from_binary(source_value.SerializeToString()) + input_data[field_name] = [target_value] + input_data[f"related_{field_name}"] = [repeated_container([target_value])] + assert to_messages(input_data, message_type) == [message_type(**values)] @given(lists(example_datapoints(generated_fields=True, missing_fields=True), min_size=5, max_size=30)) diff --git a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py index 649bc54..4fb9c5a 100644 --- a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py +++ b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py @@ -12,8 +12,19 @@ from numpy import dtypes as npdtypes from pandas.core.tools.datetimes import DatetimeScalar, to_datetime from pandas.core.tools.timedeltas import to_timedelta +from protobuf import Message as ProtobufPyMessage from shapely import from_wkb +from tilebox.datasets.datasets.stac.v1.asset_pb import Assets +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsPB2 +from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationPB2 +from tilebox.datasets.datasets.stac.v1.core_pb import Provider +from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderPB2 +from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware +from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 +from tilebox.datasets.datasets.stac.v1.storage_pb import Storage +from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StoragePB2 from tilebox.datasets.datasets.v1.well_known_types_pb2 import UUID as UUIDMessage # noqa: N811 from tilebox.datasets.datasets.v1.well_known_types_pb2 import Geometry, LatLon, LatLonAlt, Quaternion, Vec3 @@ -67,6 +78,25 @@ def to_proto(self, value: Any) -> ProtoFieldValue | None: return value +class ProtobufPyMessageField(ProtobufFieldType): + def __init__(self, source_type: type[Message], target_type: type[ProtobufPyMessage[Any]]) -> None: + super().__init__(object) + self._source_type = source_type + self._target_type = target_type + + def from_proto(self, value: ProtoFieldValue) -> ProtobufPyMessage[Any]: + if not isinstance(value, self._source_type): + raise TypeError(f"Expected {self._source_type.__name__} message but got {type(value)}") + return self._target_type.from_binary(value.SerializeToString()) + + def to_proto(self, value: ProtobufPyMessage[Any]) -> Message | None: + if is_missing(value): + return None + if not isinstance(value, self._target_type): + raise TypeError(f"Expected {self._target_type.__name__} message but got {type(value)}") + return self._source_type.FromString(value.to_binary()) + + class BoolField(ProtobufFieldType): def __init__(self) -> None: super().__init__(bool) @@ -240,9 +270,28 @@ def to_proto(self, value: tuple[float, float, float]) -> LatLonAlt | None: FieldDescriptor.TYPE_BYTES: object, # use object to allow for variable length bytes } + +class AssetsRepr(Assets): + """ + Override the default __repr__ and __str__ methods for the Assets protobuf message to provide a more + concise representation, especially when the messages are contained in larger xarray.Dataset objects. + """ + + def __repr__(self) -> str: + return f"[{', '.join(a.key for a in self.assets)}]" + + def __str__(self) -> str: + return f"{len(self.assets)} assets" + + _MESSAGE_NAMES_TO_FIELDS = { "google.protobuf.Timestamp": TimestampField(), "google.protobuf.Duration": TimeDeltaField(), + "datasets.stac.v1.Assets": ProtobufPyMessageField(AssetsPB2, AssetsRepr), + "datasets.stac.v1.Authentication": ProtobufPyMessageField(AuthenticationPB2, Authentication), + "datasets.stac.v1.Provider": ProtobufPyMessageField(ProviderPB2, Provider), + "datasets.stac.v1.ProcessingSoftware": ProtobufPyMessageField(ProcessingSoftwarePB2, ProcessingSoftware), + "datasets.stac.v1.Storage": ProtobufPyMessageField(StoragePB2, Storage), "datasets.v1.UUID": UUIDField(), "datasets.v1.Geometry": GeometryField(), "datasets.v1.Vec3": Vec3Field(), From 91f97d13940e5d3f130cc3c9987d722b5dc9576b Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Thu, 30 Jul 2026 11:56:35 +0200 Subject: [PATCH 4/7] Use protobuf-py messages in AssetCollection --- .../test_protobuf_xarray.py | 13 +- tilebox-datasets/tests/test_assets.py | 97 ++-- .../tilebox/datasets/assets/assets.py | 463 +++++++++++------- .../tilebox/datasets/assets/converters.py | 125 ----- .../tilebox/datasets/assets/stac/__init__.py | 0 .../datasets/assets/stac/authentication.py | 78 --- .../tilebox/datasets/assets/stac/metadata.py | 80 --- .../tilebox/datasets/assets/stac/product.py | 18 - .../tilebox/datasets/assets/stac/sar.py | 29 -- .../tilebox/datasets/assets/stac/satellite.py | 31 -- .../tilebox/datasets/assets/stac/storage.py | 31 -- .../protobuf_conversion/field_types.py | 4 +- tilebox-storage/tests/test_asset_client.py | 22 +- tilebox-storage/tilebox/storage/client.py | 61 ++- 14 files changed, 404 insertions(+), 648 deletions(-) delete mode 100644 tilebox-datasets/tilebox/datasets/assets/converters.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/__init__.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/authentication.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/metadata.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/product.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/sar.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/satellite.py delete mode 100644 tilebox-datasets/tilebox/datasets/assets/stac/storage.py diff --git a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py index 0e6dfb4..25f01fa 100644 --- a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py +++ b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py @@ -19,12 +19,15 @@ from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsPB2 from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationPB2 -from tilebox.datasets.datasets.stac.v1.core_pb import Provider +from tilebox.datasets.datasets.stac.v1.core_pb import Links, Provider +from tilebox.datasets.datasets.stac.v1.core_pb2 import Link as LinkPB2 +from tilebox.datasets.datasets.stac.v1.core_pb2 import Links as LinksPB2 from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderPB2 from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 from tilebox.datasets.datasets.stac.v1.storage_pb import Storage from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StoragePB2 +from tilebox.datasets.protobuf_conversion.field_types import AssetsRepr from tilebox.datasets.protobuf_conversion.protobuf_xarray import MessageToXarrayConverter from tilebox.datasets.protobuf_conversion.to_protobuf import to_messages from tilebox.datasets.query.time_interval import timestamp_to_datetime, us_to_datetime @@ -116,8 +119,9 @@ def test_convert_datapoint(datapoint: ExampleDatapoint) -> None: # noqa: PLR091 def test_convert_stac_messages_to_protobuf_py() -> None: message_types = { - "assets": ("datasets.stac.v1.Assets", AssetsPB2(assets=[AssetPB2(key="preview")]), Assets), + "assets": ("datasets.stac.v1.Assets", AssetsPB2(assets=[AssetPB2(key="preview")]), AssetsRepr), "authentication": ("datasets.stac.v1.Authentication", AuthenticationPB2(), Authentication), + "links": ("datasets.stac.v1.Links", LinksPB2(links=[LinkPB2(href="https://example.com")]), Links), "provider": ("datasets.stac.v1.Provider", ProviderPB2(name="Tilebox"), Provider), "processing_software": ( "datasets.stac.v1.ProcessingSoftware", @@ -179,6 +183,11 @@ def test_convert_stac_messages_to_protobuf_py() -> None: assert isinstance(converted_repeated_value, target_type) assert converted_repeated_value.to_binary() == source_value.SerializeToString() + converted_assets = dataset["assets"][1].item() + assert isinstance(converted_assets, Assets) + assert repr(converted_assets) == "[preview]" + assert str(converted_assets) == "1 assets" + roundtripped = to_messages(dataset, message_type) assert roundtripped == messages diff --git a/tilebox-datasets/tests/test_assets.py b/tilebox-datasets/tests/test_assets.py index deee45e..fb29d50 100644 --- a/tilebox-datasets/tests/test_assets.py +++ b/tilebox-datasets/tests/test_assets.py @@ -3,24 +3,33 @@ import numpy as np import pytest import xarray as xr -from google.protobuf.struct_pb2 import Struct from tilebox.datasets import iter_datapoints from tilebox.datasets.assets import AssetCollection -from tilebox.datasets.assets.stac.authentication import SignedURLFlow as SignedURLFlowModel -from tilebox.datasets.assets.stac.metadata import ElectroOpticalProperties, RasterProperties -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import RasterProperties as RasterPropertiesProto -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Asset, AssetAccessProfile, AssetLocation, Assets, Band -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import ( +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb import ( + EOCommonName, + EOProperties, + RasterProperties, +) +from tilebox.datasets.datasets.stac.v1.asset_pb import ( + Asset, + AssetAccessProfile, + AssetLocation, + Assets, + Band, + DataType, + KnownAssetRole, +) +from tilebox.datasets.datasets.stac.v1.authentication_pb import ( Authentication, AuthenticationFlow, AuthenticationParameter, SignedURLFlow, ) -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import ( +from tilebox.datasets.datasets.stac.v1.authentication_pb import ( AuthenticationScheme as AuthenticationSchemeProto, ) -from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage, StorageScheme +from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, Storage, StorageScheme TESTDATA = Path(__file__).parent / "testdata" @@ -37,8 +46,8 @@ def _read_real_fixture() -> tuple[Assets, Storage]: The datapoint's STAC ID is ``S2A_T33TWM_20161102T100149_L2A``. It has no authentication metadata, so no synthetic Authentication fixture is stored. """ - assets = Assets.FromString((TESTDATA / "sentinel2_assets.binpb").read_bytes()) - storage = Storage.FromString((TESTDATA / "sentinel2_storage.binpb").read_bytes()) + assets = Assets.from_binary((TESTDATA / "sentinel2_assets.binpb").read_bytes()) + storage = Storage.from_binary((TESTDATA / "sentinel2_storage.binpb").read_bytes()) return assets, storage @@ -52,19 +61,17 @@ def test_real_sentinel2_fixture_decodes_to_self_contained_assets() -> None: assert red.alternates["s3"].href == ( "s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/33/T/WM/2016/11/S2A_T33TWM_20161102T100149_L2A/B04.tif" ) - assert red.alternates["s3"].storage_schemes["earth-search"].key == "earth-search" assert red.alternates["s3"].storage_schemes["earth-search"].region == "us-west-2" assert red.media_type == "image/tiff; application=geotiff; profile=cloud-optimized" - assert red.roles == frozenset({"data", "reflectance"}) - assert red.raster == RasterProperties(sampling="unspecified", scale=0.0001, offset=-0.1, spatial_resolution=10) - assert red.bands[0].data_type == "uint16" + assert red.roles == frozenset({KnownAssetRole.DATA, KnownAssetRole.REFLECTANCE}) + assert red.raster == RasterProperties(scale=0.0001, offset=-0.1, spatial_resolution=10) + assert red.bands[0].data_type == DataType.UINT16 assert red.bands[0].nodata == 0 assert red.bands[0].raster == red.raster - assert red.bands[0].electro_optical == ElectroOpticalProperties( - common_name="red", + assert red.bands[0].eo == EOProperties( + common_name=EOCommonName.RED, center_wavelength=0.665, full_width_half_max=0.038, - solar_illumination=None, ) @@ -97,52 +104,34 @@ def test_context_ambiguity_and_explicit_override() -> None: assert decoded["data"].primary.storage_schemes["main"].region == "us-west-2" -def test_storage_and_authentication_extensions_use_immutable_mappings() -> None: +def test_locations_reuse_generated_storage_and_authentication_messages() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["main"], auth_refs=["signed"])], assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], ) - properties = Struct() - properties.update({"endpoint": "https://example.com", "options": ["one", "two"]}) - storage = Storage( - schemes={ - "main": StorageScheme( - known_type="KNOWN_STORAGE_TYPE_CUSTOM_S3", - bucket="bucket", - account="account", - additional_properties=properties.fields, - ) - } + scheme_message = StorageScheme( + known_type=KnownStorageType.CUSTOM_S3, + bucket="bucket", + account="account", ) - schema = Struct() - schema.update({"type": "string", "enum": ["one", "two"]}) - authentication = Authentication( - schemes={ - "signed": AuthenticationSchemeProto( - flows=[ - AuthenticationFlow( - key="authorizationCode", - signed_url=SignedURLFlow( - parameters={"token": AuthenticationParameter(required=True, schema=schema)} - ), - ) - ] - ) - } + storage = Storage(schemes={"main": scheme_message}) + signed_url = SignedURLFlow(parameters={"token": AuthenticationParameter(required=True)}) + authentication_scheme = AuthenticationSchemeProto( + flows=[AuthenticationFlow(key="authorizationCode", signed_url=signed_url)] ) + authentication = Authentication(schemes={"signed": authentication_scheme}) location = AssetCollection.from_datapoint(_scalar(root=root, storage=storage, authentication=authentication))[ "data" ].primary scheme = location.storage_schemes["main"] + assert scheme is scheme_message assert scheme.bucket == "bucket" assert scheme.account == "account" - assert scheme.additional_properties == {"endpoint": "https://example.com", "options": ("one", "two")} - flow = location.authentication_schemes["signed"].flows["authorizationCode"] - assert isinstance(flow, SignedURLFlowModel) - assert flow.parameters["token"].schema == {"type": "string", "enum": ("one", "two")} + assert location.authentication_schemes["signed"] is authentication_scheme + assert location.authentication_schemes["signed"].flows[0].signed_url == signed_url with pytest.raises(TypeError): - scheme.additional_properties["new"] = True # type: ignore[index] + location.storage_schemes["new"] = scheme # type: ignore[index] def test_alternate_href_absent_empty_and_nonempty_are_distinct() -> None: @@ -174,22 +163,22 @@ def test_alternate_href_absent_empty_and_nonempty_are_distinct() -> None: def test_band_inheritance_is_field_by_field() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="file:///")], - band_profiles=[Band(name="red", raster=RasterPropertiesProto(scale=2))], + band_profiles=[Band(name="red", raster=RasterProperties(scale=2))], assets=[ Asset( key="data", primary=AssetLocation(access_profile_index=0, href="tmp/data.tif"), band_profile_indices=[0], - data_type="DATA_TYPE_UINT16", + data_type=DataType.UINT16, nodata=-1, - raster=RasterPropertiesProto(offset=3, spatial_resolution=10), + raster=RasterProperties(offset=3, spatial_resolution=10), ) ], ) band = AssetCollection.from_datapoint(_scalar(root=root))["data"].bands[0] - assert band.data_type == "uint16" + assert band.data_type == DataType.UINT16 assert band.nodata == -1 - assert band.raster == RasterProperties(sampling="unspecified", scale=2, offset=3, spatial_resolution=10) + assert band.raster == RasterProperties(scale=2, offset=3, spatial_resolution=10) def test_missing_referenced_context_is_rejected() -> None: diff --git a/tilebox-datasets/tilebox/datasets/assets/assets.py b/tilebox-datasets/tilebox/datasets/assets/assets.py index 7193e3c..f6c6d8a 100644 --- a/tilebox-datasets/tilebox/datasets/assets/assets.py +++ b/tilebox-datasets/tilebox/datasets/assets/assets.py @@ -1,40 +1,82 @@ -"""Immutable asset metadata decoded from Tilebox datapoints.""" +"""Resolved asset metadata for Tilebox datapoints.""" from collections.abc import Iterator, Mapping -from dataclasses import dataclass, fields, replace +from dataclasses import dataclass, replace from types import MappingProxyType from typing import Any, TypeVar, cast import numpy as np import xarray as xr -from google.protobuf.message import Message +from protobuf import Message from typing_extensions import NotRequired, TypedDict -from tilebox.datasets.assets.converters import _enum_name, _has_field, converters -from tilebox.datasets.assets.stac.authentication import AuthenticationScheme -from tilebox.datasets.assets.stac.metadata import ( +from tilebox.datasets.datasets.stac.v1.asset_metadata_pb import ( ClassificationClass, - ElectroOpticalProperties, + EOCommonName, + EOProperties, File, Projection, RasterProperties, - Statistics, + RasterSampling, View, ) -from tilebox.datasets.assets.stac.product import ProductProperties -from tilebox.datasets.assets.stac.sar import SarProperties -from tilebox.datasets.assets.stac.satellite import SatelliteProperties -from tilebox.datasets.assets.stac.storage import StorageScheme -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Band as BandProto -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication -from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage - -_ASSETS_MESSAGE_NAME = Assets.DESCRIPTOR.full_name -_STORAGE_MESSAGE_NAME = Storage.DESCRIPTOR.full_name -_AUTHENTICATION_MESSAGE_NAME = Authentication.DESCRIPTOR.full_name +from tilebox.datasets.datasets.stac.v1.asset_pb import ( + AssetAccessProfile, + Assets, + DataType, + KnownAssetRole, + Statistics, +) +from tilebox.datasets.datasets.stac.v1.asset_pb import ( + AssetLocation as ProtoAssetLocation, +) +from tilebox.datasets.datasets.stac.v1.asset_pb import ( + Band as ProtoBand, +) +from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication, AuthenticationScheme +from tilebox.datasets.datasets.stac.v1.core_pb import KnownMediaType, MediaType +from tilebox.datasets.datasets.stac.v1.product_pb import ProductProperties +from tilebox.datasets.datasets.stac.v1.sar_pb import ( + SARFrequencyBand, + SARObservationDirection, + SARProperties, +) +from tilebox.datasets.datasets.stac.v1.satellite_pb import SatelliteProperties +from tilebox.datasets.datasets.stac.v1.storage_pb import Storage, StorageScheme + +_ASSETS_MESSAGE_NAME = "datasets.stac.v1.Assets" +_STORAGE_MESSAGE_NAME = "datasets.stac.v1.Storage" +_AUTHENTICATION_MESSAGE_NAME = "datasets.stac.v1.Authentication" _EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({}) -_MessageT = TypeVar("_MessageT", bound=Message) +_MessageT = TypeVar("_MessageT", bound=Message[Any]) +_MEDIA_TYPES = { + KnownMediaType.GEOJSON: "application/geo+json", + KnownMediaType.JSON: "application/json", + KnownMediaType.CLOUD_OPTIMIZED_GEOTIFF: "image/tiff; application=geotiff; profile=cloud-optimized", + KnownMediaType.JPEG_2000: "image/jp2", + KnownMediaType.JPEG: "image/jpeg", + KnownMediaType.PNG: "image/png", + KnownMediaType.APPLICATION_XML: "application/xml", + KnownMediaType.ZIP: "application/zip", + KnownMediaType.DIRECTORY: "application/x-directory", + KnownMediaType.GEOTIFF: "image/tiff; application=geotiff", + KnownMediaType.TIFF: "image/tiff", + KnownMediaType.HDF5: "application/x-hdf5", + KnownMediaType.HDF: "application/x-hdf", + KnownMediaType.NETCDF: "application/netcdf", + KnownMediaType.ZARR_V2: "application/vnd.zarr; version=2", + KnownMediaType.ZARR_V3: "application/vnd.zarr; version=3", + KnownMediaType.PARQUET: "application/vnd.apache.parquet", + KnownMediaType.GEOPACKAGE: "application/geopackage+sqlite3", + KnownMediaType.COPC: "application/vnd.laszip+copc", + KnownMediaType.HTML: "text/html", + KnownMediaType.TEXT: "text/plain", + KnownMediaType.TEXT_XML: "text/xml", + KnownMediaType.FLATGEOBUF: "application/vnd.flatgeobuf", + KnownMediaType.PMTILES: "application/vnd.pmtiles", + KnownMediaType.NITF: "application/vnd.nitf", + KnownMediaType.OCTET_STREAM: "application/octet-stream", +} class AssetFieldOverrides(TypedDict): @@ -47,22 +89,22 @@ class AssetFieldOverrides(TypedDict): @dataclass(frozen=True, slots=True) class Band: - """Band metadata inspired by the STAC asset and extension specifications.""" + """Resolved band metadata with asset-level defaults inherited.""" name: str | None = None description: str | None = None - data_type: str = "unspecified" + data_type: DataType = DataType.UNSPECIFIED nodata: float | None = None unit: str | None = None - electro_optical: ElectroOpticalProperties | None = None + eo: EOProperties | None = None raster: RasterProperties | None = None classes: tuple[ClassificationClass, ...] = () - sar: SarProperties | None = None + sar: SARProperties | None = None @dataclass(frozen=True, slots=True) class AssetLocation: - """A resolved asset URL and its applicable access schemes.""" + """A resolved asset URL and its applicable generated access schemes.""" href: str storage_schemes: Mapping[str, StorageScheme] = _EMPTY_MAPPING @@ -71,7 +113,7 @@ class AssetLocation: @dataclass(frozen=True, slots=True) class Asset: - """An immutable asset inspired by the STAC Asset Object specification. + """A resolved asset inspired by the STAC Asset Object specification. See https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-object. """ @@ -82,27 +124,27 @@ class Asset: media_type: str | None = None title: str | None = None description: str | None = None - roles: frozenset[str] = frozenset() + roles: frozenset[KnownAssetRole | str] = frozenset() gsd: float | None = None bands: tuple[Band, ...] = () - data_type: str = "unspecified" + data_type: DataType = DataType.UNSPECIFIED nodata: float | None = None statistics: Statistics | None = None unit: str | None = None - electro_optical: ElectroOpticalProperties | None = None + eo: EOProperties | None = None raster: RasterProperties | None = None projection: Projection | None = None view: View | None = None classes: tuple[ClassificationClass, ...] = () file: File | None = None - sar: SarProperties | None = None + sar: SARProperties | None = None satellite: SatelliteProperties | None = None product: ProductProperties | None = None @dataclass(frozen=True, slots=True) class AssetCollection(Mapping[str, Asset]): - """Immutable assets belonging to exactly one dataset datapoint.""" + """Resolved assets belonging to exactly one dataset datapoint.""" _assets: Mapping[str, Asset] @@ -113,7 +155,7 @@ def from_datapoint( *, fields: AssetFieldOverrides | None = None, ) -> "AssetCollection": - """Decode the assets attached to one Tilebox dataset datapoint. + """Resolve the assets attached to one Tilebox dataset datapoint. Args: datapoint: A scalar :class:`xarray.Dataset`, typically one result selected @@ -121,9 +163,9 @@ def from_datapoint( multiple datapoints, iterate with :func:`tilebox.datasets.iter_datapoints` first. fields: Optional xarray variable names for the ``assets``, ``storage``, - and ``authentication`` protobuf messages. Fields are normally - discovered from their generated protobuf types; specify only the - entries needed to resolve ambiguous datasets. + and ``authentication`` protobuf messages. Fields are normally found + from their concrete protobuf-py classes; specify only entries needed + to resolve ambiguous datasets. Returns: An immutable, mapping-like collection keyed by asset name. @@ -131,7 +173,7 @@ def from_datapoint( Raises: TypeError: If ``datapoint`` is not an xarray dataset. ValueError: If the input is not scalar or its protobuf fields cannot be - discovered or decoded unambiguously. + discovered or resolved unambiguously. """ if not isinstance(datapoint, xr.Dataset): raise TypeError("datapoint must be an xarray.Dataset") @@ -147,12 +189,17 @@ def from_datapoint( " assets = AssetCollection.from_datapoint(datapoint)" ) overrides = fields or {} - root = _discover_xarray_message(datapoint, Assets, overrides.get("assets")) + root = _discover_xarray_message(datapoint, Assets, _ASSETS_MESSAGE_NAME, overrides.get("assets")) if root is None: raise ValueError(f"no populated {_ASSETS_MESSAGE_NAME} field found in datapoint") - storage = _discover_xarray_message(datapoint, Storage, overrides.get("storage")) - authentication = _discover_xarray_message(datapoint, Authentication, overrides.get("authentication")) - return cls(_decode_assets(root, storage, authentication)) + storage = _discover_xarray_message(datapoint, Storage, _STORAGE_MESSAGE_NAME, overrides.get("storage")) + authentication = _discover_xarray_message( + datapoint, + Authentication, + _AUTHENTICATION_MESSAGE_NAME, + overrides.get("authentication"), + ) + return cls(_resolve_assets(root, storage, authentication)) def __getitem__(self, key: str) -> Asset: return self._assets[key] @@ -164,45 +211,11 @@ def __len__(self) -> int: return len(self._assets) -_MEDIA_TYPES = { - "geojson": "application/geo+json", - "json": "application/json", - "cloud_optimized_geotiff": "image/tiff; application=geotiff; profile=cloud-optimized", - "jpeg_2000": "image/jp2", - "jpeg": "image/jpeg", - "png": "image/png", - "application_xml": "application/xml", - "zip": "application/zip", - "directory": "inode/directory", - "geotiff": "image/tiff; application=geotiff", - "tiff": "image/tiff", - "hdf5": "application/x-hdf5", - "hdf": "application/x-hdf", - "netcdf": "application/x-netcdf", - "zarr_v2": "application/vnd+zarr", - "zarr_v3": "application/vnd+zarr", - "parquet": "application/vnd.apache.parquet", - "geopackage": "application/geopackage+sqlite3", - "copc": "application/vnd.laszip+copc", - "html": "text/html", - "text": "text/plain", - "text_xml": "text/xml", - "flatgeobuf": "application/vnd.flatgeobuf", - "pmtiles": "application/vnd.pmtiles", - "nitf": "image/nitf", - "octet_stream": "application/octet-stream", -} - - -def _convert_storage(key: str, message: Message) -> StorageScheme: - return replace(converters.convert(message), key=key) - - -def _convert_authentication(key: str, message: Message) -> AuthenticationScheme: - return replace(converters.convert(message), key=key) - - -def _resolve_refs(registry: Mapping[str, Any], refs: Any, kind: str) -> Mapping[str, Any]: +def _resolve_refs( + registry: Mapping[str, _MessageT], + refs: list[str], + kind: str, +) -> Mapping[str, _MessageT]: resolved = {} for key in refs: if key not in registry: @@ -212,26 +225,21 @@ def _resolve_refs(registry: Mapping[str, Any], refs: Any, kind: str) -> Mapping[ def _location( - source: Any, - profiles: Any, + source: ProtoAssetLocation, + profiles: list[AssetAccessProfile], storage: Mapping[str, StorageScheme], authentication: Mapping[str, AuthenticationScheme], *, fallback_href: str | None = None, ) -> tuple[str, AssetLocation]: - """Resolve a compact protobuf location through its shared access profile. - - Access profiles deduplicate URL prefixes and scheme references in protobuf. This - expands one profile index into a complete URL and keyed scheme mappings. Alternate - locations may inherit the primary location's relative href through ``fallback_href``. - """ - if not _has_field(source, "access_profile_index"): + """Expand an access-profile reference into a URL and access metadata.""" + if not source.has_field("access_profile_index"): raise ValueError("asset location is missing its access profile index") index = source.access_profile_index if index >= len(profiles): raise ValueError(f"invalid asset access profile index: {index}") profile = profiles[index] - suffix = source.href if _has_field(source, "href") else fallback_href + suffix = source.href if source.has_field("href") else fallback_href if suffix is None: raise ValueError("asset location has no href and no primary href to inherit") return profile.alternate_key, AssetLocation( @@ -241,73 +249,186 @@ def _location( ) -def _inherit(child: Any, parent: Any) -> Any: - """Fill unspecified band metadata from its asset-level defaults. +def _inherit_eo(child: EOProperties | None, parent: EOProperties | None) -> EOProperties | None: + if child is None: + return parent + if parent is None: + return child + return EOProperties( + common_name=child.common_name if child.common_name != EOCommonName.UNSPECIFIED else parent.common_name, + center_wavelength=( + child.center_wavelength + if child.has_field("center_wavelength") + else parent.center_wavelength + if parent.has_field("center_wavelength") + else None + ), + full_width_half_max=( + child.full_width_half_max + if child.has_field("full_width_half_max") + else parent.full_width_half_max + if parent.has_field("full_width_half_max") + else None + ), + solar_illumination=( + child.solar_illumination + if child.has_field("solar_illumination") + else parent.solar_illumination + if parent.has_field("solar_illumination") + else None + ), + ) + + +def _media_type(source: MediaType | None) -> str | None: + if source is None: + return None + if source.has_field("custom"): + return source.custom + return _MEDIA_TYPES.get(source.known) - STAC permits common metadata to be attached to the asset while individual bands - override only selected fields. This creates the effective immutable band model. - """ + +def _inherit_raster(child: RasterProperties | None, parent: RasterProperties | None) -> RasterProperties | None: if child is None: return parent if parent is None: return child - values = {} - for item in fields(child): - value = getattr(child, item.name) - if value is None or value in ("unspecified", ()): - values[item.name] = getattr(parent, item.name) - return replace(child, **values) - - -def _decode_band(source: BandProto, asset: Asset) -> Band: - band = Band( - name=source.name if _has_field(source, "name") else None, - description=source.description if _has_field(source, "description") else None, - data_type=_enum_name(source.DESCRIPTOR.fields_by_name["data_type"], source.data_type), - nodata=source.nodata if _has_field(source, "nodata") else None, - unit=source.unit if _has_field(source, "unit") else None, - electro_optical=converters.convert(source.eo) if source.HasField("eo") else None, - raster=converters.convert(source.raster) if source.HasField("raster") else None, - classes=tuple(converters.convert(item) for item in source.classes), - sar=converters.convert(source.sar) if source.HasField("sar") else None, + return RasterProperties( + sampling=child.sampling if child.sampling != RasterSampling.UNSPECIFIED else parent.sampling, + scale=child.scale if child.has_field("scale") else parent.scale if parent.has_field("scale") else None, + offset=child.offset if child.has_field("offset") else parent.offset if parent.has_field("offset") else None, + spatial_resolution=( + child.spatial_resolution + if child.has_field("spatial_resolution") + else parent.spatial_resolution + if parent.has_field("spatial_resolution") + else None + ), ) - return replace( - band, - data_type=asset.data_type if band.data_type == "unspecified" else band.data_type, - nodata=asset.nodata if band.nodata is None else band.nodata, - unit=asset.unit if band.unit is None else band.unit, - electro_optical=_inherit(band.electro_optical, asset.electro_optical), - raster=_inherit(band.raster, asset.raster), - classes=asset.classes if not band.classes else band.classes, - sar=_inherit(band.sar, asset.sar), + + +def _inherit_sar(child: SARProperties | None, parent: SARProperties | None) -> SARProperties | None: + if child is None: + return parent + if parent is None: + return child + return SARProperties( + polarizations=child.polarizations or parent.polarizations, + instrument_mode=( + child.instrument_mode + if child.has_field("instrument_mode") + else parent.instrument_mode + if parent.has_field("instrument_mode") + else None + ), + frequency_band=( + child.frequency_band if child.frequency_band != SARFrequencyBand.UNSPECIFIED else parent.frequency_band + ), + center_frequency=( + child.center_frequency + if child.has_field("center_frequency") + else parent.center_frequency + if parent.has_field("center_frequency") + else None + ), + bandwidth=( + child.bandwidth + if child.has_field("bandwidth") + else parent.bandwidth + if parent.has_field("bandwidth") + else None + ), + resolution_range=( + child.resolution_range + if child.has_field("resolution_range") + else parent.resolution_range + if parent.has_field("resolution_range") + else None + ), + resolution_azimuth=( + child.resolution_azimuth + if child.has_field("resolution_azimuth") + else parent.resolution_azimuth + if parent.has_field("resolution_azimuth") + else None + ), + pixel_spacing_range=( + child.pixel_spacing_range + if child.has_field("pixel_spacing_range") + else parent.pixel_spacing_range + if parent.has_field("pixel_spacing_range") + else None + ), + pixel_spacing_azimuth=( + child.pixel_spacing_azimuth + if child.has_field("pixel_spacing_azimuth") + else parent.pixel_spacing_azimuth + if parent.has_field("pixel_spacing_azimuth") + else None + ), + looks_range=( + child.looks_range + if child.has_field("looks_range") + else parent.looks_range + if parent.has_field("looks_range") + else None + ), + looks_azimuth=( + child.looks_azimuth + if child.has_field("looks_azimuth") + else parent.looks_azimuth + if parent.has_field("looks_azimuth") + else None + ), + looks_equivalent_number=( + child.looks_equivalent_number + if child.has_field("looks_equivalent_number") + else parent.looks_equivalent_number + if parent.has_field("looks_equivalent_number") + else None + ), + observation_direction=( + child.observation_direction + if child.observation_direction != SARObservationDirection.UNSPECIFIED + else parent.observation_direction + ), + relative_burst=( + child.relative_burst + if child.has_field("relative_burst") + else parent.relative_burst + if parent.has_field("relative_burst") + else None + ), + beam_ids=child.beam_ids or parent.beam_ids, ) -def _media_type(message: Message) -> str | None: - if _has_field(message, "custom"): - return message.custom # type: ignore[attr-defined] - known = _enum_name(message.DESCRIPTOR.fields_by_name["known"], message.known) # type: ignore[attr-defined] - return None if known == "unspecified" else _MEDIA_TYPES[known] +def _resolve_band(source: ProtoBand, asset: Asset) -> Band: + return Band( + name=source.name if source.has_field("name") else None, + description=source.description if source.has_field("description") else None, + data_type=source.data_type if source.data_type != DataType.UNSPECIFIED else asset.data_type, + nodata=source.nodata if source.has_field("nodata") else asset.nodata, + unit=source.unit if source.has_field("unit") else asset.unit, + eo=_inherit_eo(source.eo, asset.eo), + raster=_inherit_raster(source.raster, asset.raster), + classes=tuple(source.classes or asset.classes), + sar=_inherit_sar(source.sar, asset.sar), + ) -def _decode_assets( - root: Assets, storage_message: Storage | None, authentication_message: Authentication | None +def _resolve_assets( + root: Assets, + storage_message: Storage | None, + authentication_message: Authentication | None, ) -> Mapping[str, Asset]: - storage = ( - {key: _convert_storage(key, value) for key, value in storage_message.schemes.items()} - if storage_message is not None - else {} - ) - authentication = ( - {key: _convert_authentication(key, value) for key, value in authentication_message.schemes.items()} - if authentication_message is not None - else {} - ) + storage = storage_message.schemes if storage_message is not None else {} + authentication = authentication_message.schemes if authentication_message is not None else {} result: dict[str, Asset] = {} for source in root.assets: - if not source.HasField("primary"): + if source.primary is None: raise ValueError(f"asset {source.key!r} has no primary location") - primary_suffix = source.primary.href if _has_field(source.primary, "href") else None + primary_suffix = source.primary.href if source.primary.has_field("href") else None _, primary = _location(source.primary, root.access_profiles, storage, authentication) alternates: dict[str, AssetLocation] = {} for alternate_source in source.alternates: @@ -323,35 +444,34 @@ def _decode_assets( if alternate_key in alternates: raise ValueError(f"asset {source.key!r} has duplicate alternate key {alternate_key!r}") alternates[alternate_key] = alternate - role_field = source.DESCRIPTOR.fields_by_name["roles"] asset = Asset( key=source.key, primary=primary, alternates=MappingProxyType(alternates), - media_type=_media_type(source.media_type) if source.HasField("media_type") else None, - title=source.title if _has_field(source, "title") else None, - description=source.description if _has_field(source, "description") else None, - roles=frozenset([*(_enum_name(role_field, role) for role in source.roles), *source.custom_roles]), - gsd=source.gsd if _has_field(source, "gsd") else None, - data_type=_enum_name(source.DESCRIPTOR.fields_by_name["data_type"], source.data_type), - nodata=source.nodata if _has_field(source, "nodata") else None, - statistics=converters.convert(source.statistics) if source.HasField("statistics") else None, - unit=source.unit if _has_field(source, "unit") else None, - electro_optical=converters.convert(source.eo) if source.HasField("eo") else None, - raster=converters.convert(source.raster) if source.HasField("raster") else None, - projection=converters.convert(source.projection) if source.HasField("projection") else None, - view=converters.convert(source.view) if source.HasField("view") else None, - classes=tuple(converters.convert(item) for item in source.classes), - file=converters.convert(source.file) if source.HasField("file") else None, - sar=converters.convert(source.sar) if source.HasField("sar") else None, - satellite=converters.convert(source.satellite) if source.HasField("satellite") else None, - product=converters.convert(source.product) if source.HasField("product") else None, + media_type=_media_type(source.media_type), + title=source.title if source.has_field("title") else None, + description=source.description if source.has_field("description") else None, + roles=frozenset([*source.roles, *source.custom_roles]), + gsd=source.gsd if source.has_field("gsd") else None, + data_type=source.data_type, + nodata=source.nodata if source.has_field("nodata") else None, + statistics=source.statistics, + unit=source.unit if source.has_field("unit") else None, + eo=source.eo, + raster=source.raster, + projection=source.projection, + view=source.view, + classes=tuple(source.classes), + file=source.file, + sar=source.sar, + satellite=source.satellite, + product=source.product, ) bands = [] for index in source.band_profile_indices: if index >= len(root.band_profiles): raise ValueError(f"invalid band profile index: {index}") - bands.append(_decode_band(root.band_profiles[index], asset)) + bands.append(_resolve_band(root.band_profiles[index], asset)) asset = replace(asset, bands=tuple(bands)) if asset.key in result: raise ValueError(f"duplicate asset key: {asset.key!r}") @@ -359,7 +479,7 @@ def _decode_assets( return MappingProxyType(result) -def _message_values(data: Any) -> list[Message]: +def _message_values(data: Any) -> list[Message[Any]]: values = np.asarray(data.values, dtype=object).reshape(-1) return [value for value in values if isinstance(value, Message)] @@ -367,29 +487,20 @@ def _message_values(data: Any) -> list[Message]: def _discover_xarray_message( datapoint: xr.Dataset, message_type: type[_MessageT], + message_name: str, variable_override: str | None, ) -> _MessageT | None: - """Find one protobuf message in xarray variables by generated class. - - ``variable_override`` selects an exact xarray variable when descriptor discovery - would be ambiguous. The descriptor full name is a fallback for messages created - from an equivalent dynamic descriptor pool, whose generated Python class differs. - """ - message_name = message_type.DESCRIPTOR.full_name - - def matches(value: Message) -> bool: - return isinstance(value, message_type) or value.DESCRIPTOR.full_name == message_name - + """Find one concrete protobuf-py message among the xarray variables.""" if variable_override is not None: if variable_override not in datapoint.variables: raise ValueError(f"context field {variable_override!r} is not present in the datapoint") - values = [value for value in _message_values(datapoint[variable_override]) if matches(value)] + values = [value for value in _message_values(datapoint[variable_override]) if isinstance(value, message_type)] if len(values) != 1: raise ValueError(f"field {variable_override!r} does not contain exactly one {message_name} message") return cast(_MessageT, values[0]) candidates = [] for name, data in datapoint.variables.items(): - values = [value for value in _message_values(data) if matches(value)] + values = [value for value in _message_values(data) if isinstance(value, message_type)] if values: if len(values) != 1: raise ValueError(f"field {name!r} contains multiple {message_name} messages") diff --git a/tilebox-datasets/tilebox/datasets/assets/converters.py b/tilebox-datasets/tilebox/datasets/assets/converters.py deleted file mode 100644 index 9d9195c..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/converters.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Decorator-based conversion of protobuf messages to immutable Python values.""" - -from collections.abc import Callable, Mapping -from dataclasses import dataclass, fields -from datetime import timezone -from types import MappingProxyType -from typing import Any - -from google.protobuf.descriptor import FieldDescriptor -from google.protobuf.json_format import MessageToDict -from google.protobuf.message import Message - - -@dataclass(frozen=True, slots=True) -class _Conversion: - python_type: type[Any] - rename_fields: Mapping[str, str] - field_converters: Mapping[str, Callable[[Any], Any]] - - -class ProtobufConverterRegistry: - """Registry mapping generated protobuf classes to immutable Python dataclasses.""" - - def __init__(self) -> None: - self._conversions: dict[type[Message], _Conversion] = {} - self._conversions_by_name: dict[str, _Conversion] = {} - - def register( - self, - protobuf_type: type[Message], - *, - rename_fields: Mapping[str, str] | None = None, - field_converters: Mapping[str, Callable[[Any], Any]] | None = None, - ) -> Any: - """Register a decorated dataclass as the conversion target for a protobuf type.""" - - def decorator(python_type: type[Any]) -> type[Any]: - conversion = _Conversion( - python_type, - MappingProxyType(dict(rename_fields or {})), - MappingProxyType(dict(field_converters or {})), - ) - self._conversions[protobuf_type] = conversion - self._conversions_by_name[protobuf_type.DESCRIPTOR.full_name] = conversion - return python_type - - return decorator - - def convert(self, message: Message) -> Any: # noqa: C901, PLR0912 - """Recursively convert a registered protobuf message.""" - full_name = message.DESCRIPTOR.full_name - if full_name == "google.protobuf.Timestamp": - return message.ToDatetime(tzinfo=timezone.utc) # type: ignore[attr-defined] - if full_name in {"google.protobuf.Struct", "google.protobuf.Value"}: - return _freeze(MessageToDict(message)) - - conversion = self._conversions.get(type(message)) - if conversion is None: - conversion = next( - ( - registered - for protobuf_type, registered in self._conversions.items() - if isinstance(message, protobuf_type) - ), - None, - ) - if conversion is None: - conversion = self._conversions_by_name.get(full_name) - if conversion is None: - raise TypeError(f"no Python converter is registered for {full_name}") - - python_fields = {item.name for item in fields(conversion.python_type)} - values: dict[str, Any] = {} - for field in message.DESCRIPTOR.fields: - name = conversion.rename_fields.get(field.name, field.name) - if name not in python_fields: - continue - value = getattr(message, field.name) - field_converter = conversion.field_converters.get(field.name) - if field_converter is not None: - values[name] = field_converter(value) - continue - if field.is_repeated: - if field.message_type and field.message_type.GetOptions().map_entry: - values[name] = MappingProxyType( - {key: self.convert(item) if isinstance(item, Message) else item for key, item in value.items()} - ) - elif field.type == FieldDescriptor.TYPE_ENUM: - values[name] = tuple(_enum_name(field, item) for item in value) - else: - values[name] = tuple(self.convert(item) if isinstance(item, Message) else item for item in value) - continue - if not _has_field(message, field.name) and field.type != FieldDescriptor.TYPE_ENUM: - values.setdefault(name, None) - elif field.type == FieldDescriptor.TYPE_MESSAGE: - values[name] = self.convert(value) - elif field.type == FieldDescriptor.TYPE_ENUM: - values[name] = _enum_name(field, value) - else: - values[name] = value - return conversion.python_type(**values) - - -def _has_field(message: Message, name: str) -> bool: - field = message.DESCRIPTOR.fields_by_name[name] - if field.has_presence: - return message.HasField(name) - return any(present is field for present, _ in message.ListFields()) - - -def _enum_name(field: Any, value: int) -> str: - name = field.enum_type.values_by_number[value].name - prefix = field.enum_type.values[0].name.removesuffix("UNSPECIFIED") - return name.removeprefix(prefix).lower() - - -def _freeze(value: Any) -> Any: - if isinstance(value, dict): - return MappingProxyType({key: _freeze(item) for key, item in value.items()}) - if isinstance(value, list): - return tuple(_freeze(item) for item in value) - return value - - -converters = ProtobufConverterRegistry() diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/__init__.py b/tilebox-datasets/tilebox/datasets/assets/stac/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py b/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py deleted file mode 100644 index 4fe0909..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/authentication.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Immutable models inspired by the STAC authentication extension. - -See https://github.com/stac-extensions/authentication. -""" - -from collections.abc import Mapping -from dataclasses import dataclass -from types import MappingProxyType -from typing import Any - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import AuthenticationParameter as ParameterProto -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import AuthenticationScheme as SchemeProto -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import OAuth2Flow as OAuth2FlowProto -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import SignedURLFlow as SignedURLFlowProto - - -@converters.register(OAuth2FlowProto) -@dataclass(frozen=True, slots=True) -class OAuth2Flow: - authorization_url: str | None = None - token_url: str | None = None - scopes: Mapping[str, str] = MappingProxyType({}) - refresh_url: str | None = None - - -@converters.register(ParameterProto) -@dataclass(frozen=True, slots=True) -class AuthenticationParameter: - location: str | None = None - required: bool | None = None - description: str | None = None - schema: Mapping[str, Any] = MappingProxyType({}) - - -@converters.register(SignedURLFlowProto) -@dataclass(frozen=True, slots=True) -class SignedURLFlow: - method: str | None = None - authorization_api: str | None = None - parameters: Mapping[str, AuthenticationParameter] = MappingProxyType({}) - response_field: str | None = None - - -def _convert_flows(flows: Any) -> Mapping[str, OAuth2Flow] | Mapping[str, SignedURLFlow]: - oauth2_flows: dict[str, OAuth2Flow] = {} - signed_url_flows: dict[str, SignedURLFlow] = {} - for flow in flows: - choices = [name for name in ("oauth2", "signed_url") if flow.HasField(name)] - if len(choices) != 1: - raise ValueError(f"authentication flow {flow.key!r} must define exactly one flow type") - if flow.key in oauth2_flows or flow.key in signed_url_flows: - raise ValueError(f"duplicate authentication flow key: {flow.key!r}") - converted = converters.convert(getattr(flow, choices[0])) - if isinstance(converted, OAuth2Flow): - oauth2_flows[flow.key] = converted - else: - signed_url_flows[flow.key] = converted - if oauth2_flows and signed_url_flows: - raise ValueError("authentication scheme cannot mix OAuth2 and signed URL flows") - return MappingProxyType(oauth2_flows) if oauth2_flows else MappingProxyType(signed_url_flows) - - -@converters.register( - SchemeProto, - rename_fields={"known_type": "type", "custom_type": "type"}, - field_converters={"flows": _convert_flows}, -) -@dataclass(frozen=True, slots=True) -class AuthenticationScheme: - key: str = "" - type: str | None = None - description: str | None = None - name: str | None = None - location: str | None = None - scheme: str | None = None - flows: Mapping[str, OAuth2Flow] | Mapping[str, SignedURLFlow] = MappingProxyType({}) - open_id_connect_url: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py b/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py deleted file mode 100644 index b1af636..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/metadata.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Immutable models inspired by STAC asset, EO, raster, projection, view, classification, and file metadata specs. - -See https://github.com/radiantearth/stac-spec and https://stac-extensions.github.io/. -""" - -from dataclasses import dataclass - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import ( - ClassificationClass as ClassificationClassProto, -) -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import EOProperties as EOPropertiesProto -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import File as FileProto -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import Projection as ProjectionProto -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import RasterProperties as RasterPropertiesProto -from tilebox.datasets.datasets.stac.v1.asset_metadata_pb2 import View as ViewProto -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Statistics as StatisticsProto - - -@converters.register(StatisticsProto) -@dataclass(frozen=True, slots=True) -class Statistics: - minimum: float | None = None - maximum: float | None = None - - -@converters.register(EOPropertiesProto) -@dataclass(frozen=True, slots=True) -class ElectroOpticalProperties: - common_name: str = "unspecified" - center_wavelength: float | None = None - full_width_half_max: float | None = None - solar_illumination: float | None = None - - -@converters.register(RasterPropertiesProto) -@dataclass(frozen=True, slots=True) -class RasterProperties: - sampling: str = "unspecified" - scale: float | None = None - offset: float | None = None - spatial_resolution: float | None = None - - -@converters.register(ClassificationClassProto) -@dataclass(frozen=True, slots=True) -class ClassificationClass: - value: int | None = None - description: str | None = None - name: str | None = None - title: str | None = None - color_hint: str | None = None - nodata: bool | None = None - percentage: float | None = None - count: int | None = None - - -@converters.register(ProjectionProto) -@dataclass(frozen=True, slots=True) -class Projection: - # STAC permits 2D/3D bboxes and six- or nine-element affine transforms. - bbox: tuple[float, ...] = () - shape: tuple[int, ...] = () - transform: tuple[float, ...] = () - code: str | None = None - - -@converters.register(ViewProto) -@dataclass(frozen=True, slots=True) -class View: - incidence_angle: float | None = None - azimuth: float | None = None - - -@converters.register(FileProto) -@dataclass(frozen=True, slots=True) -class File: - checksum: bytes | None = None - size: int | None = None - local_path: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/product.py b/tilebox-datasets/tilebox/datasets/assets/stac/product.py deleted file mode 100644 index 93eda12..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/product.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Immutable model inspired by the STAC product extension. - -See https://github.com/stac-extensions/product. -""" - -from dataclasses import dataclass - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.product_pb2 import ProductProperties as ProductPropertiesProto - - -@converters.register(ProductPropertiesProto) -@dataclass(frozen=True, slots=True) -class ProductProperties: - type: str | None = None - timeliness: str | None = None - timeliness_category: str | None = None - acquisition_type: str = "unspecified" diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/sar.py b/tilebox-datasets/tilebox/datasets/assets/stac/sar.py deleted file mode 100644 index a01a1cf..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/sar.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Immutable model inspired by the STAC SAR extension. - -See https://github.com/stac-extensions/sar. -""" - -from dataclasses import dataclass - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.sar_pb2 import SARProperties as SARPropertiesProto - - -@converters.register(SARPropertiesProto) -@dataclass(frozen=True, slots=True) -class SarProperties: - polarizations: tuple[str, ...] = () - instrument_mode: str | None = None - frequency_band: str = "unspecified" - center_frequency: float | None = None - bandwidth: float | None = None - resolution_range: float | None = None - resolution_azimuth: float | None = None - pixel_spacing_range: float | None = None - pixel_spacing_azimuth: float | None = None - looks_range: int | None = None - looks_azimuth: int | None = None - looks_equivalent_number: float | None = None - observation_direction: str = "unspecified" - relative_burst: int | None = None - beam_ids: tuple[str, ...] = () diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py b/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py deleted file mode 100644 index 619aee6..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/satellite.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Immutable models inspired by the STAC satellite extension. - -See https://github.com/stac-extensions/sat. -""" - -import datetime as dt -from dataclasses import dataclass - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.satellite_pb2 import SatelliteOrbitStateVector as OrbitVectorProto -from tilebox.datasets.datasets.stac.v1.satellite_pb2 import SatelliteProperties as SatellitePropertiesProto - - -@converters.register(OrbitVectorProto) -@dataclass(frozen=True, slots=True) -class SatelliteOrbitStateVector: - datetime: dt.datetime | None = None - values: tuple[float, ...] = () - - -@converters.register(SatellitePropertiesProto) -@dataclass(frozen=True, slots=True) -class SatelliteProperties: - platform_international_designator: str | None = None - orbit_state: str = "unspecified" - absolute_orbit: int | None = None - relative_orbit: int | None = None - orbit_cycle: int | None = None - orbit_state_vectors: tuple[SatelliteOrbitStateVector, ...] = () - anx_datetime: dt.datetime | None = None - acquisition_station: str | None = None diff --git a/tilebox-datasets/tilebox/datasets/assets/stac/storage.py b/tilebox-datasets/tilebox/datasets/assets/stac/storage.py deleted file mode 100644 index cf0ade3..0000000 --- a/tilebox-datasets/tilebox/datasets/assets/stac/storage.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Immutable model inspired by the STAC storage extension. - -See https://github.com/stac-extensions/storage. -""" - -from collections.abc import Mapping -from dataclasses import dataclass -from types import MappingProxyType -from typing import Any - -from tilebox.datasets.assets.converters import converters -from tilebox.datasets.datasets.stac.v1.storage_pb2 import StorageScheme as StorageSchemeProto - - -@converters.register( - StorageSchemeProto, - rename_fields={"known_type": "type", "custom_type": "type"}, -) -@dataclass(frozen=True, slots=True) -class StorageScheme: - key: str = "" - type: str | None = None - platform: str | None = None - title: str | None = None - description: str | None = None - region: str | None = None - requester_pays: bool | None = None - storage_class: str | None = None - bucket: str | None = None - account: str | None = None - additional_properties: Mapping[str, Any] = MappingProxyType({}) diff --git a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py index 4fb9c5a..02349eb 100644 --- a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py +++ b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py @@ -19,7 +19,8 @@ from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsPB2 from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationPB2 -from tilebox.datasets.datasets.stac.v1.core_pb import Provider +from tilebox.datasets.datasets.stac.v1.core_pb import Links, Provider +from tilebox.datasets.datasets.stac.v1.core_pb2 import Links as LinksPB2 from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderPB2 from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 @@ -289,6 +290,7 @@ def __str__(self) -> str: "google.protobuf.Duration": TimeDeltaField(), "datasets.stac.v1.Assets": ProtobufPyMessageField(AssetsPB2, AssetsRepr), "datasets.stac.v1.Authentication": ProtobufPyMessageField(AuthenticationPB2, Authentication), + "datasets.stac.v1.Links": ProtobufPyMessageField(LinksPB2, Links), "datasets.stac.v1.Provider": ProtobufPyMessageField(ProviderPB2, Provider), "datasets.stac.v1.ProcessingSoftware": ProtobufPyMessageField(ProcessingSoftwarePB2, ProcessingSoftware), "datasets.stac.v1.Storage": ProtobufPyMessageField(StoragePB2, Storage), diff --git a/tilebox-storage/tests/test_asset_client.py b/tilebox-storage/tests/test_asset_client.py index ca5e73b..3c34e4c 100644 --- a/tilebox-storage/tests/test_asset_client.py +++ b/tilebox-storage/tests/test_asset_client.py @@ -10,8 +10,8 @@ from obstore.store import LocalStore from tilebox.datasets.assets.assets import Asset, AssetLocation -from tilebox.datasets.assets.stac.authentication import AuthenticationScheme -from tilebox.datasets.assets.stac.storage import StorageScheme +from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme, KnownAuthenticationType +from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, StorageScheme from tilebox.storage.aio import AssetAccessPolicy, Client @@ -32,8 +32,8 @@ def test_new_aio_api_does_not_introduce_root_client_or_eager_geotiff_import() -> def test_resolve_prefers_s3_and_reuses_store() -> None: - scheme = StorageScheme(key="earth-search", type="aws_s3", region="us-west-2", requester_pays=False) - schemes = MappingProxyType({scheme.key: scheme}) + scheme = StorageScheme(known_type=KnownStorageType.AWS_S3, region="us-west-2", requester_pays=False) + schemes = MappingProxyType({"earth-search": scheme}) primary = AssetLocation("https://example.com/data.tif", schemes) alternate = AssetLocation("s3://bucket/path/data.tif", schemes) asset = _asset(primary, alternates=MappingProxyType({"s3": alternate})) @@ -68,14 +68,14 @@ def test_concurrent_resolution_constructs_one_store() -> None: def test_resolution_rejection_details_and_client_policy() -> None: - auth = AuthenticationScheme(key="oauth", type="oauth2") - first = StorageScheme(key="first", type="aws_s3") - second = StorageScheme(key="second", type="aws_s3") + auth = AuthenticationScheme(known_type=KnownAuthenticationType.OAUTH2) + first = StorageScheme(known_type=KnownStorageType.AWS_S3) + second = StorageScheme(known_type=KnownStorageType.AWS_S3) asset = _asset( AssetLocation( "s3://bucket/data.tif", - MappingProxyType({first.key: first, second.key: second}), - MappingProxyType({auth.key: auth}), + MappingProxyType({"first": first, "second": second}), + MappingProxyType({"oauth": auth}), ) ) client = Client() @@ -83,9 +83,9 @@ def test_resolution_rejection_details_and_client_policy() -> None: with pytest.raises(ValueError, match=r"primary.*multiple storage schemes"): client.resolve(asset) authenticated = _asset( - AssetLocation("s3://bucket/data.tif", authentication_schemes=MappingProxyType({auth.key: auth})) + AssetLocation("s3://bucket/data.tif", authentication_schemes=MappingProxyType({"oauth": auth})) ) - with pytest.raises(ValueError, match=r"authentication scheme 'oauth'.*unsupported"): + with pytest.raises(ValueError, match=r"authentication type 'oauth2'.*unsupported"): client.resolve(authenticated) primary = AssetLocation("https://example.com/data.tif") diff --git a/tilebox-storage/tilebox/storage/client.py b/tilebox-storage/tilebox/storage/client.py index 39d1085..215537f 100644 --- a/tilebox-storage/tilebox/storage/client.py +++ b/tilebox-storage/tilebox/storage/client.py @@ -16,8 +16,8 @@ from obstore.store import ObjectStore from tilebox.datasets.assets.assets import Asset, AssetLocation - from tilebox.datasets.assets.stac.authentication import AuthenticationScheme - from tilebox.datasets.assets.stac.storage import StorageScheme + from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme + from tilebox.datasets.datasets.stac.v1.storage_pb import StorageScheme @dataclass(frozen=True, slots=True) @@ -83,7 +83,7 @@ def resolve(self, asset: "Asset") -> ResolvedAsset: details = "; ".join(rejected) or "asset has no locations" raise ValueError(f"unable to resolve asset {asset.key!r}: {details}") - def _store_for( # noqa: C901, PLR0912 + def _store_for( # noqa: C901, PLR0912, PLR0915 self, href: str, storage: "StorageScheme | None", @@ -93,22 +93,24 @@ def _store_for( # noqa: C901, PLR0912 parsed = urlsplit(href) scheme = _uri_scheme(href) - if authentication is not None and authentication.type != "s3": - raise ValueError(f"authentication scheme {authentication.key!r} ({authentication.type!r}) is unsupported") + authentication_type = _authentication_type(authentication) + if authentication is not None and authentication_type != "s3": + raise ValueError(f"authentication type {authentication_type!r} is unsupported") if scheme == "file": identity = ("file", "/") constructor = lambda: LocalStore("/") # noqa: E731 path = unquote(parsed.path if parsed.scheme else href).lstrip("/") elif scheme == "s3": - if storage is not None and storage.type not in {"aws_s3", "custom_s3", "unspecified", None}: - raise ValueError(f"storage scheme {storage.key!r} is incompatible with s3") + storage_type = _storage_type(storage) + if storage is not None and storage_type not in {"aws_s3", "custom_s3", "unspecified"}: + raise ValueError(f"storage type {storage_type!r} is incompatible with s3") authenticated = authentication is not None options = ( - storage.region if storage else None, - storage.requester_pays if storage else None, + storage.region if storage and storage.has_field("region") else None, + storage.requester_pays if storage and storage.has_field("requester_pays") else None, not authenticated, - storage.platform if storage and storage.type == "custom_s3" else None, + storage.platform if storage and storage_type == "custom_s3" and storage.platform else None, ) identity = ("s3", parsed.netloc, *options, repr(authentication)) constructor = lambda: S3Store( # noqa: E731 @@ -126,8 +128,9 @@ def _store_for( # noqa: C901, PLR0912 ) path = parsed.path.lstrip("/") elif scheme in {"gs", "gcs"}: - if storage is not None and storage.type not in {"google_cloud_storage", "unspecified", None}: - raise ValueError(f"storage scheme {storage.key!r} is incompatible with Google Cloud Storage") + storage_type = _storage_type(storage) + if storage is not None and storage_type not in {"google_cloud_storage", "unspecified"}: + raise ValueError(f"storage type {storage_type!r} is incompatible with Google Cloud Storage") anonymous = authentication is None identity = ("gs", parsed.netloc, anonymous, repr(authentication)) constructor = lambda: GCSStore(parsed.netloc, skip_signature=anonymous) # noqa: E731 @@ -283,6 +286,40 @@ def _uri_scheme(href: str) -> str: return urlsplit(href).scheme.lower() or "file" +def _storage_type(storage: "StorageScheme | None") -> str | None: + if storage is None: + return None + if storage.has_field("custom_type"): + return storage.custom_type + from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType # noqa: PLC0415 + + return { + KnownStorageType.UNSPECIFIED: "unspecified", + KnownStorageType.AWS_S3: "aws_s3", + KnownStorageType.CUSTOM_S3: "custom_s3", + KnownStorageType.MICROSOFT_AZURE: "microsoft_azure", + KnownStorageType.GOOGLE_CLOUD_STORAGE: "google_cloud_storage", + }[storage.known_type] + + +def _authentication_type(authentication: "AuthenticationScheme | None") -> str | None: + if authentication is None: + return None + if authentication.has_field("custom_type"): + return authentication.custom_type + from tilebox.datasets.datasets.stac.v1.authentication_pb import KnownAuthenticationType # noqa: PLC0415 + + return { + KnownAuthenticationType.UNSPECIFIED: "unspecified", + KnownAuthenticationType.HTTP: "http", + KnownAuthenticationType.S3: "s3", + KnownAuthenticationType.SIGNED_URL: "signed_url", + KnownAuthenticationType.OAUTH2: "oauth2", + KnownAuthenticationType.API_KEY: "api_key", + KnownAuthenticationType.OPEN_ID_CONNECT: "open_id_connect", + }[authentication.known_type] + + def _select_scheme(schemes: Mapping[str, Any], kind: str) -> Any | None: if len(schemes) > 1: keys = ", ".join(repr(key) for key in schemes) From cfb0e2b00ffb4f9b4911a01539639ecdf85965e7 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Thu, 30 Jul 2026 12:09:54 +0200 Subject: [PATCH 5/7] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a7320..6e92e41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `tilebox-datasets`: Added `AssetCollection.from_datapoint()` for resolving STAC assets and their generated metadata, + storage, and authentication messages from queried datapoints. +- `tilebox-storage`: Added async asset resolution, reading, downloading, and GeoTIFF access backed by object stores. + ## [0.57.0] - 2026-07-27 ### Added From b3ff24d8ed54b78a4fe8c10cecd9ecc6a53a9908 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Fri, 31 Jul 2026 15:46:17 +0200 Subject: [PATCH 6/7] Asset ingestion API --- CHANGELOG.md | 10 +- prek.toml | 2 +- pyproject.toml | 1 + tilebox-datasets/tests/data/test_datasets.py | 24 + .../test_protobuf_xarray.py | 19 +- .../protobuf_conversion/test_to_protobuf.py | 76 ++ tilebox-datasets/tests/test_assets.py | 443 ++++++++- .../tilebox/datasets/aio/client.py | 3 +- .../tilebox/datasets/aio/dataset.py | 4 +- .../tilebox/datasets/assets/__init__.py | 12 +- .../tilebox/datasets/assets/assets.py | 868 ++++++++++++++++-- .../tilebox/datasets/data/datasets.py | 11 +- .../protobuf_conversion/field_types.py | 69 +- .../protobuf_conversion/to_protobuf.py | 301 ++++-- tilebox-datasets/tilebox/datasets/schema.py | 68 ++ .../tilebox/datasets/sync/client.py | 3 +- .../tilebox/datasets/sync/dataset.py | 4 +- uv.lock | 299 +++--- 18 files changed, 1839 insertions(+), 378 deletions(-) create mode 100644 tilebox-datasets/tilebox/datasets/schema.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e92e41..84e4dd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.58.0] - 2026-07-31 + ### Added - `tilebox-datasets`: Added `AssetCollection.from_datapoint()` for resolving STAC assets and their generated metadata, storage, and authentication messages from queried datapoints. +- `tilebox-datasets`: Added `Asset`, `Band`, and `AssetLocation` authoring types together with + `AssetCollection.from_assets()` and `AssetCollection.to_fields()` for compiling optimized STAC asset metadata for + ingestion, plus common string-valued `MediaType` constants. +- `tilebox-datasets`: Added public schema field types for creating datasets containing supported STAC Assets, Storage, + Authentication, Links, Provider, and ProcessingSoftware messages, with aliases for Geometry and UUID fields. - `tilebox-storage`: Added async asset resolution, reading, downloading, and GeoTIFF access backed by object stores. ## [0.57.0] - 2026-07-27 @@ -435,7 +442,8 @@ the first client that does not cache data (since it's already on the local file - Released under the [MIT](https://opensource.org/license/mit) license. - Released packages: `tilebox-datasets`, `tilebox-workflows`, `tilebox-storage`, `tilebox-grpc` -[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.57.0...HEAD +[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.58.0...HEAD +[0.58.0]: https://github.com/tilebox/tilebox-python/compare/v0.57.0...v0.58.0 [0.57.0]: https://github.com/tilebox/tilebox-python/compare/v0.56.0...v0.57.0 [0.56.0]: https://github.com/tilebox/tilebox-python/compare/v0.55.1...v0.56.0 [0.55.1]: https://github.com/tilebox/tilebox-python/compare/v0.55.0...v0.55.1 diff --git a/prek.toml b/prek.toml index 1d6a559..39ad949 100644 --- a/prek.toml +++ b/prek.toml @@ -19,7 +19,7 @@ hooks = [ [[repos]] repo = "https://github.com/charliermarsh/ruff-pre-commit" -rev = "v0.16.0" +rev = "v0.16.1" hooks = [ { id = "ruff-check", diff --git a/pyproject.toml b/pyproject.toml index 1068e66..c028ba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,4 +119,5 @@ exclude = [ "**/*_pb2.py", "**/*_pb2.pyi", "**/*pb2_grpc.py", + "**/*pb.py", ] diff --git a/tilebox-datasets/tests/data/test_datasets.py b/tilebox-datasets/tests/data/test_datasets.py index 64171e4..acb9c2b 100644 --- a/tilebox-datasets/tests/data/test_datasets.py +++ b/tilebox-datasets/tests/data/test_datasets.py @@ -2,6 +2,7 @@ from uuid import uuid4 import pytest +from google.protobuf.descriptor_pb2 import FieldDescriptorProto from hypothesis import given from tests.data.datasets import ( @@ -26,6 +27,7 @@ FieldRole, ListDatasetsResponse, ) +from tilebox.datasets.schema import Assets, Authentication, Links, ProcessingSoftware, Provider, Storage from tilebox.datasets.service import TileboxDatasetService @@ -61,6 +63,28 @@ def test_field_from_dict(field_dict: FieldDict) -> None: ] +@pytest.mark.parametrize( + ("message_type", "message_name"), + [ + (Assets, "datasets.stac.v1.Assets"), + (Authentication, "datasets.stac.v1.Authentication"), + (Links, "datasets.stac.v1.Links"), + (Provider, "datasets.stac.v1.Provider"), + (ProcessingSoftware, "datasets.stac.v1.ProcessingSoftware"), + (Storage, "datasets.stac.v1.Storage"), + ], +) +def test_message_types_can_define_dataset_fields(message_type: type, message_name: str) -> None: + scalar = Field.from_dict({"name": "metadata", "type": message_type}) + repeated = Field.from_dict({"name": "metadata", "type": list[message_type]}) # type: ignore[typeddict-item,valid-type] + + assert scalar.descriptor.type == FieldDescriptorProto.TYPE_MESSAGE + assert scalar.descriptor.type_name == f".{message_name}" + assert scalar.descriptor.label == FieldDescriptorProto.LABEL_OPTIONAL + assert repeated.descriptor.type_name == f".{message_name}" + assert repeated.descriptor.label == FieldDescriptorProto.LABEL_REPEATED + + @given(fields()) def test_fields_to_message_and_back(field: Field) -> None: assert Field.from_message(field.to_message()) == field diff --git a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py index 25f01fa..9a1a243 100644 --- a/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py +++ b/tilebox-datasets/tests/protobuf_conversion/test_protobuf_xarray.py @@ -27,7 +27,7 @@ from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 from tilebox.datasets.datasets.stac.v1.storage_pb import Storage from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StoragePB2 -from tilebox.datasets.protobuf_conversion.field_types import AssetsRepr +from tilebox.datasets.protobuf_conversion.field_types import _AssetsDisplay from tilebox.datasets.protobuf_conversion.protobuf_xarray import MessageToXarrayConverter from tilebox.datasets.protobuf_conversion.to_protobuf import to_messages from tilebox.datasets.query.time_interval import timestamp_to_datetime, us_to_datetime @@ -119,7 +119,7 @@ def test_convert_datapoint(datapoint: ExampleDatapoint) -> None: # noqa: PLR091 def test_convert_stac_messages_to_protobuf_py() -> None: message_types = { - "assets": ("datasets.stac.v1.Assets", AssetsPB2(assets=[AssetPB2(key="preview")]), AssetsRepr), + "assets": ("datasets.stac.v1.Assets", AssetsPB2(assets=[AssetPB2(key="preview")]), _AssetsDisplay), "authentication": ("datasets.stac.v1.Authentication", AuthenticationPB2(), Authentication), "links": ("datasets.stac.v1.Links", LinksPB2(links=[LinkPB2(href="https://example.com")]), Links), "provider": ("datasets.stac.v1.Provider", ProviderPB2(name="Tilebox"), Provider), @@ -176,6 +176,10 @@ def test_convert_stac_messages_to_protobuf_py() -> None: converted_value = dataset[field_name][1].item() assert isinstance(converted_value, target_type) assert converted_value.to_binary() == source_value.SerializeToString() + if field_name == "assets": + assert isinstance(converted_value, Assets) + assert str(converted_value) == "1 assets" + assert repr(converted_value) == "[preview]" repeated_field_name = f"related_{field_name}" assert dataset[repeated_field_name].dtype == object @@ -193,11 +197,20 @@ def test_convert_stac_messages_to_protobuf_py() -> None: for repeated_container in (list, tuple): input_data = {} + record = {} for field_name, (_, source_value, target_type) in message_types.items(): - target_value = target_type.from_binary(source_value.SerializeToString()) + value_type = Assets if field_name == "assets" else target_type + target_value = value_type.from_binary(source_value.SerializeToString()) input_data[field_name] = [target_value] input_data[f"related_{field_name}"] = [repeated_container([target_value])] + record[field_name] = target_value + record[f"related_{field_name}"] = repeated_container([target_value]) assert to_messages(input_data, message_type) == [message_type(**values)] + assert to_messages([record], message_type) == [message_type(**values)] + + html = dataset._repr_html_() + assert "preview" in html + assert "access_profiles" not in html @given(lists(example_datapoints(generated_fields=True, missing_fields=True), min_size=5, max_size=30)) diff --git a/tilebox-datasets/tests/protobuf_conversion/test_to_protobuf.py b/tilebox-datasets/tests/protobuf_conversion/test_to_protobuf.py index 1c62b05..fe7e314 100644 --- a/tilebox-datasets/tests/protobuf_conversion/test_to_protobuf.py +++ b/tilebox-datasets/tests/protobuf_conversion/test_to_protobuf.py @@ -1,4 +1,8 @@ +from datetime import datetime, timezone + +import numpy as np import pandas as pd +import pytest import xarray as xr from hypothesis import given from hypothesis.strategies import lists @@ -39,3 +43,75 @@ def test_pandas_to_protobuf_messages(datapoints: list[pd.DataFrame]) -> None: dataframe = pd.concat(datapoints) converted = to_messages(dataframe, ExampleDatapoint) assert len(converted) == len(datapoints) + + +def test_record_oriented_data_preserves_absent_fields_and_filters_missing_values() -> None: + time = datetime(2026, 7, 31, tzinfo=timezone.utc) + converted = to_messages( + [ + { + "time": time, + "some_bool": True, + "some_int": 4, + "some_repeated_int": [1, None, np.nan, 2], + }, + {"time": time, "some_bool": np.nan}, + ], + ExampleDatapoint, + required_fields=["time"], + ) + + assert converted[0].some_bool is True + assert converted[0].some_int == 4 + assert converted[0].some_repeated_int == [1, 2] + assert converted[1].some_bool is False + assert converted[1].some_int == 0 + + +def test_record_oriented_data_requires_every_record_to_have_required_fields() -> None: + with pytest.raises(ValueError, match=r"Record 1: Missing required field.*time"): + to_messages( + [{"time": datetime(2026, 7, 31, tzinfo=timezone.utc)}, {"some_int": 1}], + ExampleDatapoint, + required_fields=["time"], + ) + + +def test_record_oriented_data_rejects_required_values_that_convert_to_unset() -> None: + with pytest.raises(ValueError, match="Record 0: Field 'some_identifier': Invalid value for required field"): + to_messages( + [{"some_identifier": ""}], + ExampleDatapoint, + required_fields=["some_identifier"], + ) + + +def test_dataframe_missing_values_leave_optional_fields_unset() -> None: + time = datetime(2026, 7, 31, tzinfo=timezone.utc) + dataframe = pd.DataFrame([{"time": time, "some_bool": True}, {"time": time}]) + + converted = to_messages(dataframe, ExampleDatapoint, required_fields=["time"]) + + assert converted[0].some_bool is True + assert converted[1].some_bool is False + + +def test_iterable_of_column_tuples_is_rejected_as_invalid_records() -> None: + with pytest.raises(TypeError, match="record 0 is tuple"): + to_messages([("time", [datetime(2026, 7, 31, tzinfo=timezone.utc)])], ExampleDatapoint) # type: ignore[arg-type] + + +def test_ignored_columns_do_not_participate_in_shape_validation() -> None: + converted = to_messages( + {"time": [datetime(2026, 7, 31, tzinfo=timezone.utc)], "id": []}, + ExampleDatapoint, + required_fields=["time"], + ignore_fields=["id"], + ) + + assert len(converted) == 1 + + +def test_conversion_errors_include_record_and_field_context() -> None: + with pytest.raises(TypeError, match="Record 0: Field 'some_repeated_int': Expected an iterable"): + to_messages([{"some_repeated_int": 1}], ExampleDatapoint) diff --git a/tilebox-datasets/tests/test_assets.py b/tilebox-datasets/tests/test_assets.py index fb29d50..421ec10 100644 --- a/tilebox-datasets/tests/test_assets.py +++ b/tilebox-datasets/tests/test_assets.py @@ -5,30 +5,47 @@ import xarray as xr from tilebox.datasets import iter_datapoints -from tilebox.datasets.assets import AssetCollection +from tilebox.datasets.assets import ( + Asset, + AssetCollection, + AssetLocation, + Band, + MediaType, +) from tilebox.datasets.datasets.stac.v1.asset_metadata_pb import ( EOCommonName, EOProperties, RasterProperties, ) from tilebox.datasets.datasets.stac.v1.asset_pb import ( - Asset, + Asset as ProtoAsset, +) +from tilebox.datasets.datasets.stac.v1.asset_pb import ( AssetAccessProfile, - AssetLocation, Assets, - Band, DataType, KnownAssetRole, ) +from tilebox.datasets.datasets.stac.v1.asset_pb import AssetLocation as ProtoAssetLocation +from tilebox.datasets.datasets.stac.v1.asset_pb import Band as ProtoBand from tilebox.datasets.datasets.stac.v1.authentication_pb import ( Authentication, AuthenticationFlow, AuthenticationParameter, + OAuth2Flow, SignedURLFlow, ) from tilebox.datasets.datasets.stac.v1.authentication_pb import ( AuthenticationScheme as AuthenticationSchemeProto, ) +from tilebox.datasets.datasets.stac.v1.core_pb import KnownMediaType +from tilebox.datasets.datasets.stac.v1.core_pb import MediaType as ProtoMediaType +from tilebox.datasets.datasets.stac.v1.sar_pb import ( + SARFrequencyBand, + SARObservationDirection, + SARPolarization, + SARProperties, +) from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, Storage, StorageScheme TESTDATA = Path(__file__).parent / "testdata" @@ -75,10 +92,395 @@ def test_real_sentinel2_fixture_decodes_to_self_contained_assets() -> None: ) +def test_real_sentinel2_fixture_recompiles_to_identical_optimized_fields() -> None: + """The public AWS Earth Search fixture retains the Go compiler's canonical layout.""" + assets_message, storage = _read_real_fixture() + decoded = AssetCollection.from_datapoint(_scalar(assets=assets_message, storage=storage)) + + fields = AssetCollection.from_assets(decoded.values()).to_fields() + + assert fields["assets"].to_binary() == assets_message.to_binary() + assert fields["storage"].to_binary() == storage.to_binary() + assert len(fields["assets"].access_profiles) == 2 + assert len(fields["assets"].band_profiles) == 12 + + +def test_asset_authoring_compiles_profiles_registries_and_custom_field_names() -> None: + scheme = StorageScheme(known_type=KnownStorageType.AWS_S3, region="eu-central-1") + authentication_scheme = AuthenticationSchemeProto(name="signed") + assets = AssetCollection.from_assets( + [ + Asset( + key="red", + primary=AssetLocation( + "https://example.com/item/red.tif", + storage_schemes={"main": scheme}, + authentication_schemes={"signed": authentication_scheme}, + ), + alternates={ + "s3": AssetLocation( + "s3://bucket/item/red.tif", + alternate_name="Amazon S3", + storage_schemes={"main": scheme}, + authentication_schemes={"signed": authentication_scheme}, + ) + }, + media_type="image/tiff; application=geotiff; profile=cloud-optimized", + roles=frozenset({"data", "custom-role"}), + ), + Asset( + key="green", + primary=AssetLocation( + "https://example.com/item/green.tif", + storage_schemes={"main": scheme}, + authentication_schemes={"signed": authentication_scheme}, + ), + alternates={ + "s3": AssetLocation( + "s3://bucket/item/green.tif", + alternate_name="Amazon S3", + storage_schemes={"main": scheme}, + authentication_schemes={"signed": authentication_scheme}, + ) + }, + ), + ] + ) + + fields = assets.to_fields(fields={"assets": "catalog", "storage": "store", "authentication": "auth"}) + root = fields["catalog"] + assert isinstance(root, Assets) + assert root.access_profiles == [ + AssetAccessProfile(base_href="https://example.com/item/", storage_refs=["main"], auth_refs=["signed"]), + AssetAccessProfile( + alternate_key="s3", + default_alternate_name="Amazon S3", + base_href="s3://bucket/item/", + storage_refs=["main"], + auth_refs=["signed"], + ), + ] + assert all(not alternate.has_field("href") for asset in root.assets for alternate in asset.alternates) + assert fields["store"] == Storage(schemes={"main": scheme}) + assert fields["auth"] == Authentication(schemes={"signed": authentication_scheme}) + compiled_red = next(asset for asset in root.assets if asset.key == "red") + assert assets["red"].roles == frozenset({KnownAssetRole.DATA, "custom-role"}) + assert compiled_red.roles == [KnownAssetRole.DATA] + assert compiled_red.custom_roles == ["custom-role"] + decoded = AssetCollection.from_datapoint( + _scalar(catalog=root, store=fields["store"], auth=fields["auth"]), + fields={"assets": "catalog", "storage": "store", "authentication": "auth"}, + ) + assert decoded == assets + assert decoded["red"] == assets["red"] + assert decoded["green"] == assets["green"] + + +def test_media_type_has_string_enum_semantics_and_compact_encoding() -> None: + assert isinstance(MediaType.CLOUD_OPTIMIZED_GEOTIFF, str) + assert str(MediaType.CLOUD_OPTIMIZED_GEOTIFF) == ("image/tiff; application=geotiff; profile=cloud-optimized") + + assets = AssetCollection.from_assets( + [ + Asset( + key="enum", + primary=AssetLocation("https://example.com/enum.tif"), + media_type=MediaType.CLOUD_OPTIMIZED_GEOTIFF, + ), + Asset( + key="known-string", + primary=AssetLocation("https://example.com/known-string.json"), + media_type="application/json", + ), + Asset( + key="custom", + primary=AssetLocation("https://example.com/custom.bin"), + media_type="application/x-example", + ), + ] + ) + + compiled = assets.to_fields()["assets"] + compiled_by_key = {asset.key: asset for asset in compiled.assets} + assert compiled_by_key["enum"].media_type == ProtoMediaType(known=KnownMediaType.CLOUD_OPTIMIZED_GEOTIFF) + assert compiled_by_key["known-string"].media_type == ProtoMediaType(known=KnownMediaType.JSON) + assert compiled_by_key["custom"].media_type == ProtoMediaType(custom="application/x-example") + + decoded = AssetCollection.from_datapoint(_scalar(assets=compiled)) + assert decoded["enum"].media_type is (MediaType.CLOUD_OPTIMIZED_GEOTIFF) + assert decoded["known-string"].media_type is MediaType.JSON + assert decoded["custom"].media_type == "application/x-example" + + +def test_primary_and_alternate_names_round_trip() -> None: + assets = AssetCollection.from_assets( + [ + Asset( + key="red", + primary=AssetLocation("https://example.com/red.tif", alternate_name="HTTPS"), + alternates={ + "s3": AssetLocation("s3://bucket/red.tif", alternate_name="Amazon S3"), + }, + ), + Asset( + key="green", + primary=AssetLocation("https://example.com/green.tif", alternate_name="HTTPS"), + alternates={ + "s3": AssetLocation("s3://bucket/green.tif", alternate_name="Amazon S3"), + }, + ), + ] + ) + + compiled = assets.to_fields()["assets"] + profiles = {profile.alternate_key: profile for profile in compiled.access_profiles} + + assert profiles[""].default_alternate_name == "HTTPS" + assert profiles["s3"].default_alternate_name == "Amazon S3" + assert all(asset.primary is not None and not asset.primary.has_field("alternate_name") for asset in compiled.assets) + assert AssetCollection.from_datapoint(_scalar(assets=compiled)) == assets + + names_with_no_common_default = AssetCollection.from_assets( + [ + Asset( + key="red", + primary=AssetLocation("https://example.com/red.tif", alternate_name="HTTPS"), + alternates={"s3": AssetLocation("s3://bucket/red.tif", alternate_name="Amazon S3")}, + ), + Asset( + key="green", + primary=AssetLocation("https://example.com/green.tif"), + alternates={"s3": AssetLocation("s3://bucket/green.tif", alternate_name="S3 mirror")}, + ), + ] + ) + + compiled = names_with_no_common_default.to_fields()["assets"] + assert all(not profile.has_field("default_alternate_name") for profile in compiled.access_profiles) + + decoded = AssetCollection.from_datapoint(_scalar(assets=compiled)) + assert decoded["red"].primary.alternate_name == "HTTPS" + assert decoded["green"].primary.alternate_name is None + assert decoded["red"].alternates["s3"].alternate_name == "Amazon S3" + assert decoded["green"].alternates["s3"].alternate_name == "S3 mirror" + + +def test_field_names_are_validated_at_runtime() -> None: + assets = AssetCollection.from_assets([Asset(key="data", primary=AssetLocation("https://example.com/data.tif"))]) + datapoint = _scalar(assets=assets.to_fields()["assets"]) + + for fields, error in ( + ({"unknown": "value"}, "unknown asset field names"), + ({"assets": ""}, "cannot be empty"), + ({"storage": "assets"}, "must be distinct"), + ): + with pytest.raises(ValueError, match=error): + assets.to_fields(fields=fields) # type: ignore[arg-type] + with pytest.raises(ValueError, match=error): + AssetCollection.from_datapoint(datapoint, fields=fields) # type: ignore[arg-type] + + with pytest.raises(TypeError, match="must be a string"): + assets.to_fields(fields={"assets": 1}) # type: ignore[typeddict-item] + + +def test_band_compilation_lifts_metadata_and_interns_profiles() -> None: + band = Band( + name="red", + data_type=DataType.UINT16, + nodata=0, + eo=EOProperties(common_name=EOCommonName.RED, center_wavelength=0.665), + raster=RasterProperties(scale=0, offset=0, spatial_resolution=10), + ) + assets = AssetCollection.from_assets( + Asset( + key=key, + primary=AssetLocation(f"https://example.com/{key}.tif"), + bands=(band,), + ) + for key in ("red-a", "red-b") + ) + + root = assets.to_fields()["assets"] + + assert len(root.band_profiles) == 1 + assert [asset.band_profile_indices for asset in root.assets] == [[0], [0]] + assert all(asset.data_type == DataType.UINT16 for asset in root.assets) + assert all(asset.has_field("nodata") and asset.nodata == 0 for asset in root.assets) + assert all(asset.raster == RasterProperties(scale=0, offset=0, spatial_resolution=10) for asset in root.assets) + assert all(asset.eo is None for asset in root.assets) + assert root.band_profiles[0].eo == band.eo + decoded = AssetCollection.from_datapoint(_scalar(assets=root)) + assert decoded == assets + assert decoded["red-a"] == assets["red-a"] + assert decoded["red-b"] == assets["red-b"] + + +def test_sar_metadata_is_lifted_field_by_field_and_round_trips() -> None: + def sar_properties(resolution_range: float | None = None) -> SARProperties: + return SARProperties( + polarizations=[SARPolarization.VV], + instrument_mode="IW", + frequency_band=SARFrequencyBand.C, + center_frequency=5.405, + resolution_range=resolution_range, + looks_range=0, + observation_direction=SARObservationDirection.RIGHT, + beam_ids=["beam"], + ) + + assets = AssetCollection.from_assets( + [ + Asset( + key="data", + primary=AssetLocation("https://example.com/data.tif"), + bands=( + Band(name="vv-near", sar=sar_properties(10)), + Band(name="vv-far", sar=sar_properties(20)), + ), + ) + ] + ) + + root = assets.to_fields()["assets"] + compiled = root.assets[0] + + assert compiled.sar == sar_properties() + sar_profiles = [profile.sar for profile in root.band_profiles] + assert all(profile is not None for profile in sar_profiles) + assert [profile.resolution_range for profile in sar_profiles if profile is not None] == [20, 10] + assert all(profile.has_field("resolution_range") for profile in sar_profiles if profile is not None) + assert all(not profile.has_field("center_frequency") for profile in sar_profiles if profile is not None) + assert AssetCollection.from_datapoint(_scalar(assets=root)) == assets + + +def test_asset_authoring_validation_and_registry_merge() -> None: + location = AssetLocation("https://example.com/data.tif") + with pytest.raises(ValueError, match="asset key cannot be empty"): + AssetCollection.from_assets([Asset(key="", primary=location)]) + with pytest.raises(ValueError, match="duplicate asset key"): + AssetCollection.from_assets([Asset(key="data", primary=location), Asset(key="data", primary=location)]) + + scheme = StorageScheme(region="eu-west-1") + assets = AssetCollection.from_assets( + [Asset(key="data", primary=AssetLocation(location.href, storage_schemes={"main": scheme}))] + ) + fields = assets.to_fields(storage=Storage(schemes={"link-only": StorageScheme(region="us-west-2")})) + assert set(fields["storage"].schemes) == {"main", "link-only"} + with pytest.raises(ValueError, match="conflicting scheme"): + assets.to_fields(storage=Storage(schemes={"main": StorageScheme(region="different")})) + with pytest.raises(ValueError, match="field names must be distinct"): + assets.to_fields(fields={"storage": "assets"}) + + first_auth = AuthenticationSchemeProto(flows=[AuthenticationFlow(oauth2=OAuth2Flow(scopes={"a": "A", "b": "B"}))]) + equal_auth = AuthenticationSchemeProto(flows=[AuthenticationFlow(oauth2=OAuth2Flow(scopes={"b": "B", "a": "A"}))]) + assets_with_auth = AssetCollection.from_assets( + [ + Asset( + key="data", + primary=AssetLocation(location.href, authentication_schemes={"oauth": first_auth}), + ) + ] + ) + assert ( + assets_with_auth.to_fields(authentication=Authentication(schemes={"oauth": equal_auth}))[ + "authentication" + ].schemes["oauth"] + == equal_auth + ) + + +def test_nan_and_empty_band_extensions_round_trip_semantically() -> None: + assets = AssetCollection.from_assets( + [ + Asset( + key="data", + primary=AssetLocation("https://example.com/data.tif"), + bands=(Band(name="band", nodata=float("nan"), eo=EOProperties()),), + ) + ] + ) + + packed = assets.to_fields()["assets"] + reparsed = Assets.from_binary(packed.to_binary()) + decoded = AssetCollection.from_datapoint(_scalar(assets=reparsed)) + + assert packed.assets[0].has_field("nodata") + assert not packed.band_profiles[0].has_field("nodata") + assert decoded == assets + + +@pytest.mark.parametrize( + ("root", "error"), + [ + ( + Assets( + access_profiles=[AssetAccessProfile(base_href="https://example.com/")], + assets=[ProtoAsset(primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], + ), + "asset key cannot be empty", + ), + ( + Assets( + access_profiles=[AssetAccessProfile(alternate_key="s3", base_href="s3://bucket/")], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], + ), + "primary location uses alternate profile", + ), + ( + Assets( + access_profiles=[AssetAccessProfile()], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href=""))], + ), + "primary location href cannot be empty", + ), + ( + Assets( + access_profiles=[AssetAccessProfile(base_href="https://example.com/")], + assets=[ + ProtoAsset( + key="data", + primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"), + roles=[KnownAssetRole.UNSPECIFIED], + ) + ], + ), + "invalid role", + ), + ], +) +def test_malformed_assets_messages_are_rejected(root: Assets, error: str) -> None: + with pytest.raises(ValueError, match=error): + AssetCollection.from_datapoint(_scalar(assets=root)) + + +@pytest.mark.parametrize( + "media_type", + [ + ProtoMediaType(), + ProtoMediaType(known=KnownMediaType.JSON, custom="application/custom"), + ProtoMediaType(known=KnownMediaType.UNSPECIFIED), + ProtoMediaType(custom=""), + ], +) +def test_malformed_media_types_are_rejected(media_type: ProtoMediaType) -> None: + root = Assets( + access_profiles=[AssetAccessProfile(base_href="https://example.com/")], + assets=[ + ProtoAsset( + key="data", + primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"), + media_type=media_type, + ) + ], + ) + with pytest.raises(ValueError, match="media type"): + AssetCollection.from_datapoint(_scalar(assets=root)) + + def test_discovery_uses_descriptor_type_and_supports_overrides() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="https://example.com/")], - assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], ) datapoint = _scalar(not_named_assets=root, other=root) with pytest.raises(ValueError, match=r"ambiguous datasets\.stac\.v1\.Assets"): @@ -93,7 +495,7 @@ def test_discovery_uses_descriptor_type_and_supports_overrides() -> None: def test_context_ambiguity_and_explicit_override() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["main"])], - assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], ) first = Storage(schemes={"main": StorageScheme(region="eu-west-1")}) second = Storage(schemes={"main": StorageScheme(region="us-west-2")}) @@ -107,7 +509,7 @@ def test_context_ambiguity_and_explicit_override() -> None: def test_locations_reuse_generated_storage_and_authentication_messages() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["main"], auth_refs=["signed"])], - assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], ) scheme_message = StorageScheme( known_type=KnownStorageType.CUSTOM_S3, @@ -143,13 +545,13 @@ def test_alternate_href_absent_empty_and_nonempty_are_distinct() -> None: AssetAccessProfile(alternate_key="other", base_href="https://other/"), ], assets=[ - Asset( + ProtoAsset( key="data", - primary=AssetLocation(access_profile_index=0, href="file.tif"), + primary=ProtoAssetLocation(access_profile_index=0, href="file.tif"), alternates=[ - AssetLocation(access_profile_index=1), - AssetLocation(access_profile_index=2, href=""), - AssetLocation(access_profile_index=3, href="different.tif"), + ProtoAssetLocation(access_profile_index=1), + ProtoAssetLocation(access_profile_index=2, href=""), + ProtoAssetLocation(access_profile_index=3, href="different.tif"), ], ) ], @@ -163,11 +565,11 @@ def test_alternate_href_absent_empty_and_nonempty_are_distinct() -> None: def test_band_inheritance_is_field_by_field() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="file:///")], - band_profiles=[Band(name="red", raster=RasterProperties(scale=2))], + band_profiles=[ProtoBand(name="red", raster=RasterProperties(scale=2))], assets=[ - Asset( + ProtoAsset( key="data", - primary=AssetLocation(access_profile_index=0, href="tmp/data.tif"), + primary=ProtoAssetLocation(access_profile_index=0, href="tmp/data.tif"), band_profile_indices=[0], data_type=DataType.UINT16, nodata=-1, @@ -184,7 +586,7 @@ def test_band_inheritance_is_field_by_field() -> None: def test_missing_referenced_context_is_rejected() -> None: root = Assets( access_profiles=[AssetAccessProfile(base_href="s3://bucket/", storage_refs=["missing"])], - assets=[Asset(key="data", primary=AssetLocation(access_profile_index=0, href="data.tif"))], + assets=[ProtoAsset(key="data", primary=ProtoAssetLocation(access_profile_index=0, href="data.tif"))], ) with pytest.raises(ValueError, match="missing storage scheme 'missing'"): AssetCollection.from_datapoint(_scalar(root=root)) @@ -197,12 +599,3 @@ def test_iter_datapoints_and_multi_datapoint_error() -> None: AssetCollection.from_datapoint(data) with pytest.raises(ValueError, match=r"'sample'.*not present"): list(iter_datapoints(data, dimension="sample")) - - -def test_decoded_collections_are_deeply_immutable() -> None: - assets_message, storage = _read_real_fixture() - assets = AssetCollection.from_datapoint(_scalar(root=assets_message, storage=storage)) - with pytest.raises(TypeError): - assets._assets["new"] = assets["red"] # type: ignore[index] - with pytest.raises(TypeError): - assets["red"].alternates["new"] = assets["red"].primary # type: ignore[index] diff --git a/tilebox-datasets/tilebox/datasets/aio/client.py b/tilebox-datasets/tilebox/datasets/aio/client.py index c899efc..a263b4d 100644 --- a/tilebox-datasets/tilebox/datasets/aio/client.py +++ b/tilebox-datasets/tilebox/datasets/aio/client.py @@ -121,7 +121,8 @@ async def create_or_update_dataset( Args: kind: The kind of the dataset. code_name: The code name of the dataset. - fields: The custom fields of the dataset. + fields: The custom fields of the dataset. Supported generated + message field types are available from :mod:`tilebox.datasets.schema`. name: The name of the dataset. Defaults to the code name. Returns: diff --git a/tilebox-datasets/tilebox/datasets/aio/dataset.py b/tilebox-datasets/tilebox/datasets/aio/dataset.py index 83be95f..88d8c97 100644 --- a/tilebox-datasets/tilebox/datasets/aio/dataset.py +++ b/tilebox-datasets/tilebox/datasets/aio/dataset.py @@ -529,8 +529,8 @@ async def ingest( data: The data to ingest. Supported data types are: - xr.Dataset: Ingest a dataset such as it is returned by the output of `collection.load()` - pd.DataFrame: Ingest a pandas DataFrame, mapping the column names to the dataset fields - - Iterable, dict or nd-array: Ingest any object that can be converted to a pandas DataFrame, - equivalent to `ingest(pd.DataFrame(data))` + - Mapping: Ingest column-oriented fields, with one collection of values per field name + - Iterable of mappings: Ingest record-oriented data, with one mapping per datapoint allow_existing: Whether to allow existing datapoints. Datapoints will only be overwritten if all of their fields are exactly equal to already existing datapoints. Tilebox will never create duplicate datapoints, but will raise an error if the datapoint already exists. Setting this to diff --git a/tilebox-datasets/tilebox/datasets/assets/__init__.py b/tilebox-datasets/tilebox/datasets/assets/__init__.py index b08b746..95622b0 100644 --- a/tilebox-datasets/tilebox/datasets/assets/__init__.py +++ b/tilebox-datasets/tilebox/datasets/assets/__init__.py @@ -1,3 +1,11 @@ -from tilebox.datasets.assets.assets import AssetCollection +from tilebox.datasets.assets.assets import ( + Asset, + AssetCollection, + AssetFieldNames, + AssetFields, + AssetLocation, + Band, + MediaType, +) -__all__ = ["AssetCollection"] +__all__ = ["Asset", "AssetCollection", "AssetFieldNames", "AssetFields", "AssetLocation", "Band", "MediaType"] diff --git a/tilebox-datasets/tilebox/datasets/assets/assets.py b/tilebox-datasets/tilebox/datasets/assets/assets.py index f6c6d8a..8ec1cf0 100644 --- a/tilebox-datasets/tilebox/datasets/assets/assets.py +++ b/tilebox-datasets/tilebox/datasets/assets/assets.py @@ -1,9 +1,12 @@ -"""Resolved asset metadata for Tilebox datapoints.""" +"""Semantic asset metadata authoring, compilation, and resolution.""" -from collections.abc import Iterator, Mapping -from dataclasses import dataclass, replace +from collections.abc import Iterable, Iterator, Mapping +from dataclasses import dataclass, is_dataclass, replace +from dataclasses import fields as dataclass_fields +from enum import Enum +from math import isnan from types import MappingProxyType -from typing import Any, TypeVar, cast +from typing import Any, Literal, TypeVar, cast, overload import numpy as np import xarray as xr @@ -20,6 +23,9 @@ RasterSampling, View, ) +from tilebox.datasets.datasets.stac.v1.asset_pb import ( + Asset as ProtoAsset, +) from tilebox.datasets.datasets.stac.v1.asset_pb import ( AssetAccessProfile, Assets, @@ -34,7 +40,8 @@ Band as ProtoBand, ) from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication, AuthenticationScheme -from tilebox.datasets.datasets.stac.v1.core_pb import KnownMediaType, MediaType +from tilebox.datasets.datasets.stac.v1.core_pb import KnownMediaType +from tilebox.datasets.datasets.stac.v1.core_pb import MediaType as ProtoMediaType from tilebox.datasets.datasets.stac.v1.product_pb import ProductProperties from tilebox.datasets.datasets.stac.v1.sar_pb import ( SARFrequencyBand, @@ -48,48 +55,127 @@ _STORAGE_MESSAGE_NAME = "datasets.stac.v1.Storage" _AUTHENTICATION_MESSAGE_NAME = "datasets.stac.v1.Authentication" _EMPTY_MAPPING: Mapping[str, Any] = MappingProxyType({}) +_NO_COMMON = object() _MessageT = TypeVar("_MessageT", bound=Message[Any]) -_MEDIA_TYPES = { - KnownMediaType.GEOJSON: "application/geo+json", - KnownMediaType.JSON: "application/json", - KnownMediaType.CLOUD_OPTIMIZED_GEOTIFF: "image/tiff; application=geotiff; profile=cloud-optimized", - KnownMediaType.JPEG_2000: "image/jp2", - KnownMediaType.JPEG: "image/jpeg", - KnownMediaType.PNG: "image/png", - KnownMediaType.APPLICATION_XML: "application/xml", - KnownMediaType.ZIP: "application/zip", - KnownMediaType.DIRECTORY: "application/x-directory", - KnownMediaType.GEOTIFF: "image/tiff; application=geotiff", - KnownMediaType.TIFF: "image/tiff", - KnownMediaType.HDF5: "application/x-hdf5", - KnownMediaType.HDF: "application/x-hdf", - KnownMediaType.NETCDF: "application/netcdf", - KnownMediaType.ZARR_V2: "application/vnd.zarr; version=2", - KnownMediaType.ZARR_V3: "application/vnd.zarr; version=3", - KnownMediaType.PARQUET: "application/vnd.apache.parquet", - KnownMediaType.GEOPACKAGE: "application/geopackage+sqlite3", - KnownMediaType.COPC: "application/vnd.laszip+copc", - KnownMediaType.HTML: "text/html", - KnownMediaType.TEXT: "text/plain", - KnownMediaType.TEXT_XML: "text/xml", - KnownMediaType.FLATGEOBUF: "application/vnd.flatgeobuf", - KnownMediaType.PMTILES: "application/vnd.pmtiles", - KnownMediaType.NITF: "application/vnd.nitf", - KnownMediaType.OCTET_STREAM: "application/octet-stream", + + +# Replace this compatibility definition with enum.StrEnum once Python 3.10 support is dropped. +class MediaType(str, Enum): + """Common media types used by STAC assets and links.""" + + GEOJSON = "application/geo+json" + JSON = "application/json" + CLOUD_OPTIMIZED_GEOTIFF = "image/tiff; application=geotiff; profile=cloud-optimized" + JPEG_2000 = "image/jp2" + JPEG = "image/jpeg" + PNG = "image/png" + APPLICATION_XML = "application/xml" + ZIP = "application/zip" + DIRECTORY = "application/x-directory" + GEOTIFF = "image/tiff; application=geotiff" + TIFF = "image/tiff" + HDF5 = "application/x-hdf5" + HDF = "application/x-hdf" + NETCDF = "application/netcdf" + ZARR_V2 = "application/vnd.zarr; version=2" + ZARR_V3 = "application/vnd.zarr; version=3" + PARQUET = "application/vnd.apache.parquet" + GEOPACKAGE = "application/geopackage+sqlite3" + COPC = "application/vnd.laszip+copc" + HTML = "text/html" + TEXT = "text/plain" + TEXT_XML = "text/xml" + FLATGEOBUF = "application/vnd.flatgeobuf" + PMTILES = "application/vnd.pmtiles" + NITF = "application/vnd.nitf" + OCTET_STREAM = "application/octet-stream" + + def __str__(self) -> str: + """Return the media-type value, matching enum.StrEnum semantics.""" + return self.value + + +_MEDIA_TYPES = {known: MediaType[known.name] for known in KnownMediaType if known != KnownMediaType.UNSPECIFIED} +_KNOWN_ROLES = { + role.name.lower().replace("_", "-"): role for role in KnownAssetRole if role != KnownAssetRole.UNSPECIFIED +} +_ASSET_FIELD_DEFAULTS = {"assets": "assets", "storage": "storage", "authentication": "authentication"} +_SAR_FIELDS = ( + "polarizations", + "instrument_mode", + "frequency_band", + "center_frequency", + "bandwidth", + "resolution_range", + "resolution_azimuth", + "pixel_spacing_range", + "pixel_spacing_azimuth", + "looks_range", + "looks_azimuth", + "looks_equivalent_number", + "observation_direction", + "relative_burst", + "beam_ids", +) +_SAR_ENUM_DEFAULTS = { + "frequency_band": SARFrequencyBand.UNSPECIFIED, + "observation_direction": SARObservationDirection.UNSPECIFIED, } +_RegistryAttribute = Literal["storage_schemes", "authentication_schemes"] -class AssetFieldOverrides(TypedDict): - """Optional xarray variable names for ambiguous asset metadata fields.""" +@dataclass(frozen=True, slots=True) +class _AccessProfileSpec: + """Canonical values used to build and reference one access profile.""" + + alternate_key: str + default_alternate_name: str | None + base_href: str + storage_refs: tuple[str, ...] + authentication_refs: tuple[str, ...] + + +class AssetFieldNames(TypedDict): + """Dataset field names used to read or write asset metadata. + + Attributes: + assets: Field containing the generated :class:`Assets` message. + storage: Field containing the generated :class:`Storage` registry. + authentication: Field containing the generated :class:`Authentication` + registry. + """ assets: NotRequired[str] storage: NotRequired[str] authentication: NotRequired[str] +class AssetFields(TypedDict): + """Compiled fields produced with the default asset field names.""" + + assets: Assets + storage: NotRequired[Storage] + authentication: NotRequired[Authentication] + + @dataclass(frozen=True, slots=True) class Band: - """Resolved band metadata with asset-level defaults inherited.""" + """Semantic metadata for one asset band. + + Asset-level defaults are inherited when an :class:`AssetCollection` is + constructed. + + Attributes: + name: Band name. + description: Human-readable band description. + data_type: Raster data type, or ``UNSPECIFIED`` to inherit it. + nodata: Nodata value, or ``None`` to inherit it. + unit: Unit name, or ``None`` to inherit it. + eo: Generated Electro-Optical metadata. + raster: Generated Raster extension metadata. + classes: Generated Classification extension classes. + sar: Generated Synthetic-Aperture Radar metadata. + """ name: str | None = None description: str | None = None @@ -104,24 +190,58 @@ class Band: @dataclass(frozen=True, slots=True) class AssetLocation: - """A resolved asset URL and its applicable generated access schemes.""" + """An asset URL and its applicable access schemes. + + Attributes: + href: Fully resolved asset href. + storage_schemes: Storage registry entries used by this location, keyed + by their exact STAC registry keys. + authentication_schemes: Authentication registry entries used by this + location, keyed by their exact STAC registry keys. + alternate_name: STAC alternate-assets display name. + """ href: str storage_schemes: Mapping[str, StorageScheme] = _EMPTY_MAPPING authentication_schemes: Mapping[str, AuthenticationScheme] = _EMPTY_MAPPING + alternate_name: str | None = None @dataclass(frozen=True, slots=True) class Asset: - """A resolved asset inspired by the STAC Asset Object specification. + """A semantic asset inspired by the STAC Asset Object specification. See https://github.com/radiantearth/stac-spec/blob/master/item-spec/item-spec.md#asset-object. + + Attributes: + key: Key in the STAC assets object. + primary: Primary asset location. + alternates: Alternate locations keyed by their STAC alternate-assets key. + media_type: Exact media type string. + title: Optional title. + description: Optional description. + roles: Known or custom STAC asset roles. + gsd: Ground sample distance in metres. + bands: Ordered, resolved band metadata. + data_type: Asset-level raster data type. + nodata: Asset-level nodata value. + statistics: Generated Raster statistics. + unit: Asset-level unit. + eo: Generated Electro-Optical metadata. + raster: Generated Raster extension metadata. + projection: Generated Projection extension metadata. + view: Generated View extension metadata. + classes: Generated Classification extension classes. + file: Generated File extension metadata. + sar: Generated Synthetic-Aperture Radar metadata. + satellite: Generated Satellite extension metadata. + product: Generated Product extension metadata. """ key: str primary: AssetLocation alternates: Mapping[str, AssetLocation] = _EMPTY_MAPPING - media_type: str | None = None + media_type: MediaType | str | None = None title: str | None = None description: str | None = None roles: frozenset[KnownAssetRole | str] = frozenset() @@ -143,8 +263,14 @@ class Asset: @dataclass(frozen=True, slots=True) -class AssetCollection(Mapping[str, Asset]): - """Resolved assets belonging to exactly one dataset datapoint.""" +class AssetCollection(Mapping[str, Asset]): # noqa: PLW1641 - mutable mappings make the value unhashable + """Semantic assets belonging to exactly one dataset datapoint. + + Use :meth:`from_assets` to construct and validate a semantic collection, + :meth:`to_fields` to compile it into optimized ingestion fields, and + :meth:`from_datapoint` to resolve optimized fields back into a semantic + collection. + """ _assets: Mapping[str, Asset] @@ -153,7 +279,7 @@ def from_datapoint( cls, datapoint: xr.Dataset, *, - fields: AssetFieldOverrides | None = None, + fields: AssetFieldNames | None = None, ) -> "AssetCollection": """Resolve the assets attached to one Tilebox dataset datapoint. @@ -168,12 +294,13 @@ def from_datapoint( to resolve ambiguous datasets. Returns: - An immutable, mapping-like collection keyed by asset name. + A read-only, mapping-like collection keyed by asset name. Raises: - TypeError: If ``datapoint`` is not an xarray dataset. + TypeError: If ``datapoint`` is not an xarray dataset or a configured + field name is not a string. ValueError: If the input is not scalar or its protobuf fields cannot be - discovered or resolved unambiguously. + discovered, validated, or resolved unambiguously. """ if not isinstance(datapoint, xr.Dataset): raise TypeError("datapoint must be an xarray.Dataset") @@ -188,7 +315,7 @@ def from_datapoint( " for datapoint in iter_datapoints(data):\n" " assets = AssetCollection.from_datapoint(datapoint)" ) - overrides = fields or {} + overrides = _validate_field_names(fields) root = _discover_xarray_message(datapoint, Assets, _ASSETS_MESSAGE_NAME, overrides.get("assets")) if root is None: raise ValueError(f"no populated {_ASSETS_MESSAGE_NAME} field found in datapoint") @@ -201,6 +328,96 @@ def from_datapoint( ) return cls(_resolve_assets(root, storage, authentication)) + @classmethod + def from_assets(cls, assets: Iterable[Asset]) -> "AssetCollection": + """Construct, validate, and normalize a semantic asset collection. + + Known role strings are canonicalized, Band values inherit Asset + defaults, empty Band extension messages are normalized to absence, and + metadata shared by all Bands is lifted onto the Asset. Locations, + registries, and resulting Band profiles are validated for compilation. + + Args: + assets: Assets to materialize, validate, and normalize. Keys must be + nonempty and unique. + + Returns: + A read-only, mapping-like collection keyed by each asset's key. + + Raises: + TypeError: If an Asset, location, Band, or role has an invalid type. + ValueError: If keys, locations, roles, media types, registries, or + Band metadata cannot be represented by the ingestion format. + """ + materialized = list(assets) + result: dict[str, Asset] = {} + for asset in materialized: + if not isinstance(asset, Asset): + raise TypeError(f"assets must contain Asset instances, got {type(asset).__name__}") + _validate_authored_asset(asset) + if asset.key in result: + raise ValueError(f"duplicate asset key: {asset.key!r}") + normalized = _normalize_asset(asset) + for band in normalized.bands: + if not _compile_band_profile(band, normalized).to_binary(): + raise ValueError(f"asset {asset.key!r} has a band with no metadata after inheritance") + result[asset.key] = normalized + locations = [location for asset in result.values() for location in (asset.primary, *asset.alternates.values())] + _merge_registry({}, locations, "storage_schemes") + _merge_registry({}, locations, "authentication_schemes") + return cls(MappingProxyType(result)) + + @overload + def to_fields( + self, + *, + fields: None = None, + storage: Storage | None = None, + authentication: Authentication | None = None, + ) -> AssetFields: ... + + @overload + def to_fields( + self, + *, + fields: AssetFieldNames, + storage: Storage | None = None, + authentication: Authentication | None = None, + ) -> dict[str, Assets | Storage | Authentication]: ... + + def to_fields( + self, + *, + fields: AssetFieldNames | None = None, + storage: Storage | None = None, + authentication: Authentication | None = None, + ) -> AssetFields | dict[str, Assets | Storage | Authentication]: + """Compile the semantic collection into optimized ingestion fields. + + Args: + fields: Optional output names for assets, storage, and authentication. + storage: Additional storage registry entries to retain, including + entries referenced only by other STAC fields such as Links. + authentication: Additional authentication registry entries to retain, + including entries referenced only by other STAC fields such as Links. + + Returns: + A field-name mapping containing ``Assets`` and nonempty registries. + + Raises: + TypeError: If a configured field name is not a string. + ValueError: If names collide, schemes conflict, or metadata is invalid. + """ + overrides = _validate_field_names(fields) + names = {**_ASSET_FIELD_DEFAULTS, **overrides} + root, storage_schemes, auth_schemes = _compile_assets(self.values(), storage, authentication) + result: dict[str, Assets | Storage | Authentication] = {names["assets"]: root} + if storage_schemes: + result[names["storage"]] = Storage(schemes=storage_schemes) + if auth_schemes: + result[names["authentication"]] = Authentication(schemes=auth_schemes) + return cast("AssetFields", result) if fields is None else result + def __getitem__(self, key: str) -> Asset: return self._assets[key] @@ -210,6 +427,21 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self._assets) + def __eq__(self, other: object) -> bool: + """Compare collections by resolved asset semantics, not profile placement. + + Args: + other: Object to compare with this collection. + + Returns: + Whether both collections contain the same resolved asset metadata. + """ + if not isinstance(other, AssetCollection): + return NotImplemented + return self.keys() == other.keys() and all( + _semantic_equal(_asset_semantics(asset), _asset_semantics(other[key])) for key, asset in self.items() + ) + def _resolve_refs( registry: Mapping[str, _MessageT], @@ -224,6 +456,50 @@ def _resolve_refs( return MappingProxyType(resolved) +def _validate_field_names(fields: AssetFieldNames | None) -> dict[str, str]: + """Validate runtime field-name overrides and return a plain snapshot.""" + if fields is None: + return {} + unknown = set(fields).difference(_ASSET_FIELD_DEFAULTS) + if unknown: + raise ValueError(f"unknown asset field names: {', '.join(sorted(unknown))}") + overrides = dict(fields) + for logical_name, physical_name in overrides.items(): + if not isinstance(physical_name, str): + raise TypeError(f"field name for {logical_name!r} must be a string") + if not physical_name: + raise ValueError(f"field name for {logical_name!r} cannot be empty") + resolved = {**_ASSET_FIELD_DEFAULTS, **overrides} + if len(set(resolved.values())) != len(resolved): + raise ValueError("asset, storage, and authentication field names must be distinct") + return overrides + + +def _validate_authored_asset(asset: Asset) -> None: + """Validate semantic invariants required by compilation and resolution.""" + if not asset.key: + raise ValueError("asset key cannot be empty") + _validate_authored_location(asset.primary, asset.key) + for alternate_key, location in asset.alternates.items(): + if not alternate_key: + raise ValueError(f"asset {asset.key!r} has an alternate with an empty key") + _validate_authored_location(location, asset.key, alternate_key) + if asset.media_type == "": + raise ValueError(f"asset {asset.key!r} media type cannot be empty") + if any(not isinstance(band, Band) for band in asset.bands): + raise TypeError(f"asset {asset.key!r} bands must contain Band instances") + _canonical_roles(asset.roles, asset.key) + + +def _validate_authored_location(location: object, asset_key: str, alternate_key: str | None = None) -> None: + """Validate the type and href of one authored location.""" + label = "primary" if alternate_key is None else f"alternate {alternate_key!r}" + if not isinstance(location, AssetLocation): + raise TypeError(f"asset {asset_key!r} {label} must be an AssetLocation") + if not location.href: + raise ValueError(f"asset {asset_key!r} {label} location href cannot be empty") + + def _location( source: ProtoAssetLocation, profiles: list[AssetAccessProfile], @@ -244,12 +520,470 @@ def _location( raise ValueError("asset location has no href and no primary href to inherit") return profile.alternate_key, AssetLocation( href=profile.base_href + suffix, + alternate_name=( + source.alternate_name + if source.has_field("alternate_name") + else profile.default_alternate_name + if profile.has_field("default_alternate_name") + else None + ), storage_schemes=_resolve_refs(storage, profile.storage_refs, "storage"), authentication_schemes=_resolve_refs(authentication, profile.auth_refs, "authentication"), ) +def _merge_registry( + base: Mapping[str, _MessageT], locations: Iterable[AssetLocation], attribute: _RegistryAttribute +) -> dict[str, _MessageT]: + """Merge equal registry entries and reject conflicting uses of one key.""" + result = dict(base) + for location in locations: + for key, scheme in getattr(location, attribute).items(): + previous = result.get(key) + if previous is not None and previous != scheme: + raise ValueError(f"conflicting scheme for key {key!r}") + if previous is None: + result[key] = scheme + return result + + +def _semantic_equal(left: Any, right: Any) -> bool: + """Compare semantic values recursively, treating NaN values as equal.""" + if isinstance(left, Message) and isinstance(right, Message): + return left == right + if isinstance(left, float) and isinstance(right, float) and isnan(left) and isnan(right): + return True + if isinstance(left, Mapping) and isinstance(right, Mapping): + return left.keys() == right.keys() and all(_semantic_equal(value, right[key]) for key, value in left.items()) + if isinstance(left, (tuple, list)) and isinstance(right, (tuple, list)): + return len(left) == len(right) and all(_semantic_equal(a, b) for a, b in zip(left, right, strict=True)) + if is_dataclass(left) and is_dataclass(right) and type(left) is type(right): + return all( + _semantic_equal(getattr(left, field.name), getattr(right, field.name)) for field in dataclass_fields(left) + ) + return left == right + + +def _common(values: Iterable[Any]) -> Any: + materialized = list(values) + if not materialized: + return _NO_COMMON + return materialized[0] if all(_semantic_equal(materialized[0], value) for value in materialized[1:]) else _NO_COMMON + + +def _message_value(message: Message[Any] | None, field: str, unspecified: Any = None) -> Any: + if message is None: + return unspecified + value = getattr(message, field) + if isinstance(value, list): + return tuple(value) + if unspecified is not None and value == unspecified: + return unspecified + return value if message.has_field(field) else unspecified + + +def _safe_common_href_prefix(hrefs: Iterable[str]) -> str: + """Return a byte-preserving common href prefix ending at a path boundary.""" + values = list(hrefs) + prefix = values[0] + for value in values[1:]: + mismatch = next( + (index for index, pair in enumerate(zip(prefix, value, strict=False)) if pair[0] != pair[1]), + min(len(prefix), len(value)), + ) + prefix = prefix[:mismatch] + boundary = prefix.rfind("/") + return prefix[: boundary + 1] if boundary >= 0 else "" + + +def _encode_media_type(value: MediaType | str | None) -> ProtoMediaType | None: + """Encode an exact media-type string using the compact known dictionary.""" + if value is None: + return None + for known, text in _MEDIA_TYPES.items(): + if value == text: + return ProtoMediaType(known=known) + return ProtoMediaType(custom=value) + + +def _canonical_roles(roles: Iterable[KnownAssetRole | str], asset_key: str) -> frozenset[KnownAssetRole | str]: + """Canonicalize known role strings and validate all authored role values.""" + canonical: set[KnownAssetRole | str] = set() + for role in roles: + if isinstance(role, KnownAssetRole): + if role == KnownAssetRole.UNSPECIFIED: + raise ValueError(f"asset {asset_key!r} has an invalid role") + canonical.add(role) + elif isinstance(role, str): + if not role: + raise ValueError(f"asset {asset_key!r} has an invalid role") + canonical.add(_KNOWN_ROLES.get(role, role)) + else: + raise TypeError(f"asset {asset_key!r} has a role of unsupported type {type(role).__name__}") + return frozenset(canonical) + + +def _compile_assets( + authored: Iterable[Asset], + storage: Storage | None, + authentication: Authentication | None, +) -> tuple[Assets, dict[str, StorageScheme], dict[str, AuthenticationScheme]]: + """Compile semantic assets into compact generated ingestion messages. + + Compilation lifts shared effective Band metadata onto each Asset, removes + inherited values from Band profiles, interns and deterministically orders + equal Band and access profiles, compresses hrefs at URI path boundaries, + and merges referenced storage and authentication registries. + + Args: + authored: Semantic assets to compile. + storage: Additional storage entries, including entries used by Links. + authentication: Additional authentication entries, including entries + used by Links. + + Returns: + The optimized Assets message and merged storage and authentication + registries. + """ + assets = sorted((_normalize_asset(asset) for asset in authored), key=lambda asset: asset.key) + locations = [location for asset in assets for location in (asset.primary, *asset.alternates.values())] + storage_schemes = _merge_registry(storage.schemes if storage is not None else {}, locations, "storage_schemes") + auth_schemes = _merge_registry( + authentication.schemes if authentication is not None else {}, locations, "authentication_schemes" + ) + access_profiles, location_specs, indices = _compile_access_profiles(assets) + unsorted_band_profiles: dict[bytes, ProtoBand] = {} + asset_band_keys: dict[str, list[bytes]] = {} + proto_assets: list[ProtoAsset] = [] + for asset in assets: + band_keys = [] + for band in asset.bands: + profile = _compile_band_profile(band, asset) + key = profile.to_binary() + if not key: + raise ValueError(f"asset {asset.key!r} has a band with no metadata after inheritance") + unsorted_band_profiles.setdefault(key, profile) + band_keys.append(key) + asset_band_keys[asset.key] = band_keys + roles = _canonical_roles(asset.roles, asset.key) + known_roles = {role for role in roles if isinstance(role, KnownAssetRole)} + known_roles_list = sorted(known_roles, key=lambda role: role.value) + custom_roles = sorted(role for role in roles if isinstance(role, str)) + primary_spec = location_specs[(asset.key, "")] + primary_suffix = asset.primary.href.removeprefix(primary_spec.base_href) + primary = ProtoAssetLocation( + access_profile_index=indices[primary_spec], + href=primary_suffix, + alternate_name=None + if asset.primary.alternate_name == primary_spec.default_alternate_name + else asset.primary.alternate_name, + ) + alternates = [] + for key, location in sorted(asset.alternates.items()): + spec = location_specs[(asset.key, key)] + suffix = location.href.removeprefix(spec.base_href) + alternates.append( + ProtoAssetLocation( + access_profile_index=indices[spec], + href=None if suffix == primary_suffix else suffix, + alternate_name=None + if location.alternate_name == spec.default_alternate_name + else location.alternate_name, + ) + ) + proto_assets.append( + ProtoAsset( + key=asset.key, + primary=primary, + alternates=alternates, + media_type=_encode_media_type(asset.media_type), + title=asset.title, + description=asset.description, + roles=known_roles_list, + custom_roles=custom_roles, + gsd=asset.gsd, + data_type=asset.data_type, + nodata=asset.nodata, + statistics=asset.statistics, + unit=asset.unit, + eo=asset.eo, + raster=asset.raster, + projection=asset.projection, + view=asset.view, + classes=list(asset.classes), + file=asset.file, + sar=asset.sar, + satellite=asset.satellite, + product=asset.product, + ) + ) + band_profiles = sorted(unsorted_band_profiles.values(), key=_band_profile_sort_key) + band_indices = {profile.to_binary(): index for index, profile in enumerate(band_profiles)} + for proto_asset in proto_assets: + proto_asset.band_profile_indices.extend(band_indices[key] for key in asset_band_keys[proto_asset.key]) + return ( + Assets(access_profiles=access_profiles, band_profiles=band_profiles, assets=proto_assets), + storage_schemes, + auth_schemes, + ) + + +def _compile_access_profiles( + assets: list[Asset], +) -> tuple[ + list[AssetAccessProfile], + dict[tuple[str, str], _AccessProfileSpec], + dict[_AccessProfileSpec, int], +]: + """Build deterministic, href-compressing profiles for asset locations.""" + groups: dict[ + tuple[str, tuple[str, ...], tuple[str, ...]], + list[tuple[str, AssetLocation]], + ] = {} + for asset in assets: + if "" in asset.alternates: + raise ValueError(f"asset {asset.key!r} has an alternate with an empty key") + ordered = [("", asset.primary), *sorted(asset.alternates.items())] + for alternate_key, location in ordered: + if not location.href: + raise ValueError(f"asset {asset.key!r} location href cannot be empty") + group = ( + alternate_key, + tuple(sorted(location.storage_schemes)), + tuple(sorted(location.authentication_schemes)), + ) + groups.setdefault(group, []).append((asset.key, location)) + profile_specs = [] + location_specs: dict[tuple[str, str], _AccessProfileSpec] = {} + for (alternate_key, storage_refs, auth_refs), grouped_locations in groups.items(): + base_href = _safe_common_href_prefix(location.href for _, location in grouped_locations) + alternate_names = [location.alternate_name for _, location in grouped_locations] + default_name = ( + alternate_names[0] + if alternate_names and alternate_names[0] is not None and len(set(alternate_names)) == 1 + else None + ) + spec = _AccessProfileSpec(alternate_key, default_name, base_href, storage_refs, auth_refs) + profile_specs.append(spec) + for asset_key, _ in grouped_locations: + location_specs[(asset_key, alternate_key)] = spec + sorted_specs = sorted( + profile_specs, + key=lambda item: ( + bool(item.alternate_key), + item.alternate_key, + item.storage_refs, + item.authentication_refs, + item.base_href, + item.default_alternate_name is not None, + item.default_alternate_name or "", + ), + ) + indices = {spec: index for index, spec in enumerate(sorted_specs)} + access_profiles = [ + AssetAccessProfile( + alternate_key=spec.alternate_key, + default_alternate_name=spec.default_alternate_name, + base_href=spec.base_href, + storage_refs=list(spec.storage_refs), + auth_refs=list(spec.authentication_refs), + ) + for spec in sorted_specs + ] + return access_profiles, location_specs, indices + + +def _compile_band_profile(band: Band, asset: Asset) -> ProtoBand: + """Compile one effective Band into its sparse, inheriting profile.""" + return ProtoBand( + name=band.name, + description=band.description, + data_type=None if band.data_type == asset.data_type else band.data_type, + nodata=None if _semantic_equal(band.nodata, asset.nodata) else band.nodata, + unit=None if band.unit == asset.unit else band.unit, + eo=_sparse_eo(band.eo, asset.eo), + raster=_sparse_raster(band.raster, asset.raster), + classes=[] if _semantic_equal(band.classes, asset.classes) else list(band.classes), + sar=_sparse_sar(band.sar, asset.sar), + ) + + +def _effective_band(band: Band, asset: Asset) -> Band: + """Resolve one Band by applying all inheritable Asset defaults.""" + return Band( + name=band.name, + description=band.description, + data_type=band.data_type if band.data_type != DataType.UNSPECIFIED else asset.data_type, + nodata=band.nodata if band.nodata is not None else asset.nodata, + unit=band.unit if band.unit is not None else asset.unit, + eo=_inherit_eo(band.eo, asset.eo), + raster=_inherit_raster(band.raster, asset.raster), + classes=band.classes or asset.classes, + sar=_inherit_sar(band.sar, asset.sar), + ) + + +def _normalize_asset(asset: Asset) -> Asset: + """Return the canonical semantic form produced again by resolution. + + Band values are first resolved against authored Asset defaults. Values that + are effectively common to every Band are then lifted onto the Asset using + the same rules as protobuf compilation. EO identity deliberately remains + Band-scoped unless it was already authored on the Asset. + """ + roles = _canonical_roles(asset.roles, asset.key) + if not asset.bands: + return replace(asset, roles=roles) + bands = tuple(_normalize_band_extensions(_effective_band(band, asset)) for band in asset.bands) + common_data_type = _common(band.data_type for band in bands) + common_nodata = _common(band.nodata for band in bands) + common_unit = _common(band.unit for band in bands) + common_classes = _common(band.classes for band in bands) + return replace( + asset, + roles=roles, + bands=bands, + data_type=asset.data_type if common_data_type is _NO_COMMON else common_data_type, + nodata=asset.nodata if common_nodata is _NO_COMMON else common_nodata, + unit=asset.unit if common_unit is _NO_COMMON else common_unit, + raster=_lift_raster(bands, asset.raster), + classes=asset.classes if common_classes is _NO_COMMON else common_classes, + sar=_lift_sar(bands, asset.sar), + ) + + +def _asset_semantics(asset: Asset) -> Asset: + """Normalize an Asset for profile-placement-independent equality.""" + return _normalize_asset(asset) + + +def _normalize_band_extensions(band: Band) -> Band: + """Canonicalize present but empty generated Band metadata to absence.""" + return replace( + band, + eo=band.eo if band.eo is not None and band.eo.to_binary() else None, + raster=band.raster if band.raster is not None and band.raster.to_binary() else None, + sar=band.sar if band.sar is not None and band.sar.to_binary() else None, + ) + + +def _lift_raster(bands: tuple[Band, ...], parent: RasterProperties | None) -> RasterProperties | None: + """Lift Raster fields effectively shared by every Band onto the Asset. + + This is the parent-building half of compilation. ``_sparse_raster`` then + removes those inherited values from each child profile; ``_inherit_raster`` + performs the inverse reconstruction while resolving queried data. + """ + if not bands: + return parent + common_sampling = _common(_message_value(band.raster, "sampling", RasterSampling.UNSPECIFIED) for band in bands) + sampling = ( + _message_value(parent, "sampling", RasterSampling.UNSPECIFIED) + if common_sampling is _NO_COMMON + else common_sampling + ) + values = {} + for field in ("scale", "offset", "spatial_resolution"): + common = _common(_message_value(band.raster, field) for band in bands) + values[field] = _message_value(parent, field) if common is _NO_COMMON else common + result = RasterProperties(sampling=sampling, **values) + return result if result.to_binary() else None + + +def _lift_sar(bands: tuple[Band, ...], parent: SARProperties | None) -> SARProperties | None: + """Lift SAR fields effectively shared by every Band onto the Asset. + + This complements ``_sparse_sar`` during compilation. ``_inherit_sar`` is + the inverse operation used when resolving Band profiles. + """ + if not bands: + return parent + values = {} + for field in _SAR_FIELDS: + default = _SAR_ENUM_DEFAULTS.get(field) + common = _common(_message_value(band.sar, field, default) for band in bands) + values[field] = _message_value(parent, field, default) if common is _NO_COMMON else common + values["polarizations"] = list(values["polarizations"] or ()) + values["beam_ids"] = list(values["beam_ids"] or ()) + result = SARProperties(**values) + return result if result.to_binary() else None + + +def _sparse_eo(child: EOProperties | None, parent: EOProperties | None) -> EOProperties | None: + """Remove EO fields inherited from an already-authored Asset EO value.""" + if child is None: + return None + result = EOProperties( + common_name=None if parent is not None and child.common_name == parent.common_name else child.common_name, + center_wavelength=None + if _semantic_equal(_message_value(child, "center_wavelength"), _message_value(parent, "center_wavelength")) + else _message_value(child, "center_wavelength"), + full_width_half_max=None + if _semantic_equal(_message_value(child, "full_width_half_max"), _message_value(parent, "full_width_half_max")) + else _message_value(child, "full_width_half_max"), + solar_illumination=None + if _semantic_equal(_message_value(child, "solar_illumination"), _message_value(parent, "solar_illumination")) + else _message_value(child, "solar_illumination"), + ) + return result if result.to_binary() else None + + +def _sparse_raster(child: RasterProperties | None, parent: RasterProperties | None) -> RasterProperties | None: + """Remove Raster fields inherited from the canonical Asset value. + + This complements ``_lift_raster`` during compilation; ``_inherit_raster`` + reconstructs the effective value during resolution. + """ + if child is None: + return None + result = RasterProperties( + sampling=None if parent is not None and child.sampling == parent.sampling else child.sampling, + scale=None + if _semantic_equal(_message_value(child, "scale"), _message_value(parent, "scale")) + else _message_value(child, "scale"), + offset=None + if _semantic_equal(_message_value(child, "offset"), _message_value(parent, "offset")) + else _message_value(child, "offset"), + spatial_resolution=None + if _semantic_equal(_message_value(child, "spatial_resolution"), _message_value(parent, "spatial_resolution")) + else _message_value(child, "spatial_resolution"), + ) + return result if result.to_binary() else None + + +def _sparse_sar(child: SARProperties | None, parent: SARProperties | None) -> SARProperties | None: + """Remove SAR fields inherited from the canonical Asset value. + + This complements ``_lift_sar`` during compilation; ``_inherit_sar`` + reconstructs the effective value during resolution. + """ + if child is None: + return None + values = {} + for field in _SAR_FIELDS: + default = _SAR_ENUM_DEFAULTS.get(field) + value = _message_value(child, field, default) + values[field] = None if _semantic_equal(value, _message_value(parent, field, default)) else value + values["polarizations"] = list(values["polarizations"] or ()) + values["beam_ids"] = list(values["beam_ids"] or ()) + result = SARProperties(**values) + return result if result.to_binary() else None + + +def _band_profile_sort_key(profile: ProtoBand) -> tuple[Any, ...]: + """Return the canonical ordering key for an interned Band profile.""" + center = _message_value(profile.eo, "center_wavelength") + return ( + 0 if center is not None else 1, + center if center is not None else 0, + 0 if profile.has_field("name") else 1, + profile.name if profile.has_field("name") else "", + profile.to_binary(), + ) + + def _inherit_eo(child: EOProperties | None, parent: EOProperties | None) -> EOProperties | None: + """Resolve effective EO fields from a sparse child and its Asset parent.""" if child is None: return parent if parent is None: @@ -280,15 +1014,24 @@ def _inherit_eo(child: EOProperties | None, parent: EOProperties | None) -> EOPr ) -def _media_type(source: MediaType | None) -> str | None: +def _media_type(source: ProtoMediaType | None) -> MediaType | str | None: if source is None: return None - if source.has_field("custom"): + has_known = source.has_field("known") + has_custom = source.has_field("custom") + if has_known == has_custom: + raise ValueError("asset media type must contain exactly one known or custom value") + if has_custom: + if not source.custom: + raise ValueError("asset custom media type cannot be empty") return source.custom - return _MEDIA_TYPES.get(source.known) + if source.known not in _MEDIA_TYPES: + raise ValueError(f"asset has invalid known media type {source.known!r}") + return _MEDIA_TYPES[source.known] def _inherit_raster(child: RasterProperties | None, parent: RasterProperties | None) -> RasterProperties | None: + """Reconstruct effective Raster fields from sparse child and parent values.""" if child is None: return parent if parent is None: @@ -308,6 +1051,7 @@ def _inherit_raster(child: RasterProperties | None, parent: RasterProperties | N def _inherit_sar(child: SARProperties | None, parent: SARProperties | None) -> SARProperties | None: + """Reconstruct effective SAR fields from sparse child and parent values.""" if child is None: return parent if parent is None: @@ -417,6 +1161,23 @@ def _resolve_band(source: ProtoBand, asset: Asset) -> Band: ) +def _resolve_primary_location( + source: ProtoAsset, + profiles: list[AssetAccessProfile], + storage: Mapping[str, StorageScheme], + authentication: Mapping[str, AuthenticationScheme], +) -> tuple[AssetLocation, str | None]: + if source.primary is None: + raise ValueError(f"asset {source.key!r} has no primary location") + primary_suffix = source.primary.href if source.primary.has_field("href") else None + primary_key, primary = _location(source.primary, profiles, storage, authentication) + if primary_key: + raise ValueError(f"asset {source.key!r} primary location uses alternate profile {primary_key!r}") + if not primary.href: + raise ValueError(f"asset {source.key!r} primary location href cannot be empty") + return primary, primary_suffix + + def _resolve_assets( root: Assets, storage_message: Storage | None, @@ -426,10 +1187,9 @@ def _resolve_assets( authentication = authentication_message.schemes if authentication_message is not None else {} result: dict[str, Asset] = {} for source in root.assets: - if source.primary is None: - raise ValueError(f"asset {source.key!r} has no primary location") - primary_suffix = source.primary.href if source.primary.has_field("href") else None - _, primary = _location(source.primary, root.access_profiles, storage, authentication) + if not source.key: + raise ValueError("asset key cannot be empty") + primary, primary_suffix = _resolve_primary_location(source, root.access_profiles, storage, authentication) alternates: dict[str, AssetLocation] = {} for alternate_source in source.alternates: alternate_key, alternate = _location( @@ -441,6 +1201,8 @@ def _resolve_assets( ) if not alternate_key: raise ValueError(f"asset {source.key!r} has an alternate with an empty key") + if not alternate.href: + raise ValueError(f"asset {source.key!r} alternate {alternate_key!r} href cannot be empty") if alternate_key in alternates: raise ValueError(f"asset {source.key!r} has duplicate alternate key {alternate_key!r}") alternates[alternate_key] = alternate @@ -451,7 +1213,7 @@ def _resolve_assets( media_type=_media_type(source.media_type), title=source.title if source.has_field("title") else None, description=source.description if source.has_field("description") else None, - roles=frozenset([*source.roles, *source.custom_roles]), + roles=_canonical_roles([*source.roles, *source.custom_roles], source.key), gsd=source.gsd if source.has_field("gsd") else None, data_type=source.data_type, nodata=source.nodata if source.has_field("nodata") else None, diff --git a/tilebox-datasets/tilebox/datasets/data/datasets.py b/tilebox-datasets/tilebox/datasets/data/datasets.py index a0c43e8..356de47 100644 --- a/tilebox-datasets/tilebox/datasets/data/datasets.py +++ b/tilebox-datasets/tilebox/datasets/data/datasets.py @@ -3,15 +3,14 @@ from datetime import datetime, timedelta from enum import Enum from typing import Literal, TypeAlias, TypedDict, get_args, get_origin -from uuid import UUID import numpy as np from google.protobuf import duration_pb2, timestamp_pb2 from google.protobuf.descriptor_pb2 import FieldDescriptorProto, FileDescriptorSet -from shapely import Geometry from typing_extensions import NotRequired, Required from tilebox.datasets.datasets.v1 import core_pb2, dataset_type_pb2, datasets_pb2, well_known_types_pb2 +from tilebox.datasets.schema import UUID, Geometry, MessageFieldType, _wire_message_type from tilebox.datasets.uuid import uuid_message_to_optional_uuid, uuid_message_to_uuid, uuid_to_uuid_message @@ -100,6 +99,7 @@ class FieldDict(TypedDict): | type[list[UUID]] | type[Geometry] | type[list[Geometry]] + | MessageFieldType ] description: NotRequired[str] example_value: NotRequired[str] @@ -146,7 +146,12 @@ def from_dict(cls, field: FieldDict) -> "Field": label = FieldDescriptorProto.Label.LABEL_OPTIONAL inner_type = field["type"] - (field_type, field_type_name) = _TYPE_INFO[inner_type] + wire_message_type = _wire_message_type(inner_type) + if wire_message_type is not None: + field_type = FieldDescriptorProto.TYPE_MESSAGE + field_type_name = f".{wire_message_type.DESCRIPTOR.full_name}" + else: + (field_type, field_type_name) = _TYPE_INFO[inner_type] return cls( descriptor=FieldDescriptorProto( diff --git a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py index 02349eb..250349d 100644 --- a/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py +++ b/tilebox-datasets/tilebox/datasets/protobuf_conversion/field_types.py @@ -15,19 +15,9 @@ from protobuf import Message as ProtobufPyMessage from shapely import from_wkb -from tilebox.datasets.datasets.stac.v1.asset_pb import Assets -from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsPB2 -from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication -from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationPB2 -from tilebox.datasets.datasets.stac.v1.core_pb import Links, Provider -from tilebox.datasets.datasets.stac.v1.core_pb2 import Links as LinksPB2 -from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderPB2 -from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware -from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwarePB2 -from tilebox.datasets.datasets.stac.v1.storage_pb import Storage -from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StoragePB2 from tilebox.datasets.datasets.v1.well_known_types_pb2 import UUID as UUIDMessage # noqa: N811 from tilebox.datasets.datasets.v1.well_known_types_pb2 import Geometry, LatLon, LatLonAlt, Quaternion, Vec3 +from tilebox.datasets.schema import _MESSAGE_FIELD_TYPES, Assets ScalarProtoFieldValue = Message | float | str | bool | bytes @@ -80,22 +70,38 @@ def to_proto(self, value: Any) -> ProtoFieldValue | None: class ProtobufPyMessageField(ProtobufFieldType): - def __init__(self, source_type: type[Message], target_type: type[ProtobufPyMessage[Any]]) -> None: + """Convert between protobuf wire messages and protobuf-py values.""" + + def __init__( + self, + wire_type: type[Message], + value_type: type[ProtobufPyMessage[Any]], + *, + read_type: type[ProtobufPyMessage[Any]] | None = None, + ) -> None: + """Initialize a protobuf-py field converter. + + Args: + wire_type: Google protobuf message used on the ingestion wire. + value_type: Public protobuf-py message accepted during ingestion. + read_type: Optional subtype returned when decoding query results. + """ super().__init__(object) - self._source_type = source_type - self._target_type = target_type + self._wire_type = wire_type + self._value_type = value_type + self._read_type = read_type or value_type def from_proto(self, value: ProtoFieldValue) -> ProtobufPyMessage[Any]: - if not isinstance(value, self._source_type): - raise TypeError(f"Expected {self._source_type.__name__} message but got {type(value)}") - return self._target_type.from_binary(value.SerializeToString()) + if not isinstance(value, self._wire_type): + raise TypeError(f"Expected {self._wire_type.__name__} message but got {type(value)}") + return self._read_type.from_binary(value.SerializeToString()) def to_proto(self, value: ProtobufPyMessage[Any]) -> Message | None: if is_missing(value): return None - if not isinstance(value, self._target_type): - raise TypeError(f"Expected {self._target_type.__name__} message but got {type(value)}") - return self._source_type.FromString(value.to_binary()) + if not isinstance(value, self._value_type): + raise TypeError(f"Expected {self._value_type.__name__} message but got {type(value)}") + return self._wire_type.FromString(value.to_binary()) class BoolField(ProtobufFieldType): @@ -272,11 +278,8 @@ def to_proto(self, value: tuple[float, float, float]) -> LatLonAlt | None: } -class AssetsRepr(Assets): - """ - Override the default __repr__ and __str__ methods for the Assets protobuf message to provide a more - concise representation, especially when the messages are contained in larger xarray.Dataset objects. - """ +class _AssetsDisplay(Assets): + """Assets value with concise text representations, including in xarray displays.""" def __repr__(self) -> str: return f"[{', '.join(a.key for a in self.assets)}]" @@ -285,15 +288,17 @@ def __str__(self) -> str: return f"{len(self.assets)} assets" -_MESSAGE_NAMES_TO_FIELDS = { +_MESSAGE_NAMES_TO_FIELDS: dict[str, ProtobufFieldType] = { "google.protobuf.Timestamp": TimestampField(), "google.protobuf.Duration": TimeDeltaField(), - "datasets.stac.v1.Assets": ProtobufPyMessageField(AssetsPB2, AssetsRepr), - "datasets.stac.v1.Authentication": ProtobufPyMessageField(AuthenticationPB2, Authentication), - "datasets.stac.v1.Links": ProtobufPyMessageField(LinksPB2, Links), - "datasets.stac.v1.Provider": ProtobufPyMessageField(ProviderPB2, Provider), - "datasets.stac.v1.ProcessingSoftware": ProtobufPyMessageField(ProcessingSoftwarePB2, ProcessingSoftware), - "datasets.stac.v1.Storage": ProtobufPyMessageField(StoragePB2, Storage), + **{ + wire_type.DESCRIPTOR.full_name: ProtobufPyMessageField( + wire_type, + value_type, + read_type=_AssetsDisplay if value_type is Assets else None, + ) + for value_type, wire_type in _MESSAGE_FIELD_TYPES.items() + }, "datasets.v1.UUID": UUIDField(), "datasets.v1.Geometry": GeometryField(), "datasets.v1.Vec3": Vec3Field(), diff --git a/tilebox-datasets/tilebox/datasets/protobuf_conversion/to_protobuf.py b/tilebox-datasets/tilebox/datasets/protobuf_conversion/to_protobuf.py index df4a48c..1326a91 100644 --- a/tilebox-datasets/tilebox/datasets/protobuf_conversion/to_protobuf.py +++ b/tilebox-datasets/tilebox/datasets/protobuf_conversion/to_protobuf.py @@ -1,139 +1,235 @@ -from collections import defaultdict -from collections.abc import Collection, Iterable, Iterator, Mapping -from typing import Any, cast +from collections.abc import Collection, Iterable, Iterator, Mapping, Sequence +from typing import Any, TypeVar, cast from uuid import UUID import numpy as np import pandas as pd import xarray as xr -from google.protobuf.descriptor import Descriptor +from google.protobuf.descriptor import Descriptor, FieldDescriptor from google.protobuf.message import Message from tilebox.datasets.protobuf_conversion.field_types import ( ProtobufFieldType, ProtoFieldValue, + ScalarProtoFieldValue, infer_field_type, is_missing, ) -IngestionData = Mapping[str, Collection[Any]] | Iterable[tuple[str, Collection[Any]]] | pd.DataFrame | xr.Dataset +Record = Mapping[str, Any] +Column = Sequence[Any] | np.ndarray | pd.Series +ColumnarData = Mapping[str, Column] +IngestionData = ColumnarData | Iterable[Record] | pd.DataFrame | xr.Dataset DatapointIDs = pd.DataFrame | pd.Series | xr.Dataset | xr.DataArray | np.ndarray | Collection[UUID] | Collection[str] +_MessageT = TypeVar("_MessageT", bound=Message) -def to_messages( # noqa: C901, PLR0912 +def to_messages( data: IngestionData, - message_type: type[Message], + message_type: type[_MessageT], required_fields: list[str] | None = None, ignore_fields: list[str] | None = None, -) -> list[Message]: - if not isinstance(data, xr.Dataset) and not isinstance(data, pd.DataFrame): - try: - data = pd.DataFrame(data) - except (TypeError, ValueError) as err: - raise ValueError(f"Invalid ingestion data. Failed to convert data to a pandas.DataFrame(): {err}") from None - - if required_fields is None: - required_fields = [] +) -> list[_MessageT]: + """Convert supported ingestion inputs into protobuf messages. - for required_field in required_fields: - if required_field not in data: - raise ValueError(f"Missing required field {required_field}") + Mappings are interpreted as column-oriented data. Iterable inputs are + interpreted as records, with one mapping per datapoint. DataFrames and + xarray Datasets are adapted to records without passing values through a + second tabular representation. + Missing optional values and absent record keys leave the protobuf field + unset. Required fields must be present and non-missing in every record. + """ + required = set(required_fields or []) ignore = set(ignore_fields or []) - field_descriptors_by_name = cast(Descriptor, message_type.DESCRIPTOR).fields_by_name + records = _iter_records(data, field_descriptors_by_name, ignore) + return _records_to_messages(records, message_type, field_descriptors_by_name, required, ignore) - # let's validate our fields, to make sure that they are all known fields for the given protobuf message - # and that they are all lists of the same length - field_lengths = defaultdict(list) - fields: dict[str, pd.Series | np.ndarray | list[ProtoFieldValue]] = {} - field_names = [str(field) for field in data] +def marshal_messages(messages: list[Message]) -> list[bytes]: + return [m.SerializeToString(deterministic=True) for m in messages] + + +def _iter_records( + data: IngestionData, + descriptors: Mapping[str, FieldDescriptor], + ignore: set[str], +) -> Iterator[Record]: + """Adapt each supported input representation to an iterator of records.""" if isinstance(data, xr.Dataset): - # list(dataset) only returns the variables, not the coords, so for xarray we need to add the coords as well - # but not all coords, we only care abou time for now - field_names.extend(list(set(map(str, data.coords)) & {"time"})) + yield from _xarray_records(data, descriptors, ignore) + return + if isinstance(data, pd.DataFrame): + yield from _dataframe_records(data, descriptors, ignore) + return + if isinstance(data, Mapping): + yield from _columnar_records(cast(ColumnarData, data), descriptors, ignore) + return + for index, record in enumerate(data): + if not isinstance(record, Mapping): + raise TypeError( + f"Record-oriented ingestion data must contain mappings, but record {index} is {type(record).__name__}" + ) + try: + _validate_field_names(record, descriptors, ignore) + except (TypeError, ValueError) as error: + raise type(error)(f"Record {index}: {error}") from error + yield record + + +def _dataframe_records( + data: pd.DataFrame, + descriptors: Mapping[str, FieldDescriptor], + ignore: set[str], +) -> Iterator[Record]: + """Yield DataFrame rows positionally without coercing object values.""" + all_field_names = list(data.columns) + field_names = _validate_field_names(data.columns, descriptors, ignore) + if len(set(field_names)) != len(field_names): + raise ValueError("Ingestion DataFrame contains duplicate field names") + positions = [all_field_names.index(field_name) for field_name in field_names] + for values in data.itertuples(index=False, name=None): + yield {field_name: values[position] for field_name, position in zip(field_names, positions, strict=True)} + + +def _xarray_records( + data: xr.Dataset, + descriptors: Mapping[str, FieldDescriptor], + ignore: set[str], +) -> Iterator[Record]: + """Yield xarray datapoints while removing padding from repeated fields.""" + field_names = [*data.data_vars] + if "time" in data.coords and "time" not in field_names: + field_names.append("time") + validated_names = _validate_field_names(field_names, descriptors, ignore) + + columns: dict[str, Column] = {} + for field_name in validated_names: + values = data[field_name].to_numpy() + descriptor = descriptors.get(field_name) + if descriptor is not None and descriptor.is_repeated: + field_type = infer_field_type(descriptor) + values = trim_trailing_fill_values(values, field_type.fill_value) + columns[field_name] = values + yield from _transpose_columns(columns, validated_names) + + +def _columnar_records( + data: ColumnarData, + descriptors: Mapping[str, FieldDescriptor], + ignore: set[str], +) -> Iterator[Record]: + """Transpose column-oriented data into records after checking its shape.""" + field_names = _validate_field_names(data, descriptors, ignore) + yield from _transpose_columns(data, field_names) + + +def _transpose_columns(data: ColumnarData, field_names: list[str]) -> Iterator[Record]: + """Validate and positionally transpose selected columns into records.""" + columns: list[Column] = [] + lengths: dict[int, list[str]] = {} + for field_name in field_names: + values = data[field_name] + if isinstance(values, str | bytes | bytearray) or not isinstance(values, Sequence | np.ndarray | pd.Series): + raise TypeError(f"Column {field_name!r} must be an ordered sequence of values") + columns.append(values) + lengths.setdefault(len(values), []).append(field_name) + + if len(lengths) > 1: + details = "\n".join(f"- {length}: {', '.join(names)}" for length, names in lengths.items()) + raise ValueError(f"Inconsistent number of datapoints:\n{details}") + for row in zip(*columns, strict=True): + yield dict(zip(field_names, row, strict=True)) + + +def _validate_field_names( + field_names: Iterable[object], + descriptors: Mapping[str, FieldDescriptor], + ignore: set[str], +) -> list[str]: + """Validate dataset field names and return the fields to ingest.""" + result: list[str] = [] for field_name in field_names: + if not isinstance(field_name, str): + raise TypeError(f"Dataset field names must be strings, got {type(field_name).__name__}") if field_name in ignore: continue - - values = data[field_name] # this works for pandas series and xarray data arrays - if not isinstance(values, pd.Series | xr.DataArray | np.ndarray): - raise TypeError( - f"expected a list, pandas.Series or xarray.DataArray of elements for field {field_name}, got {type(values)} instead" - ) - - if field_name not in field_descriptors_by_name: - raise ValueError( - f"{field_name} is not a valid field of dataset type {message_type.__name__}. " - f"Expected one of {', '.join(field_descriptors_by_name.keys())}" - ) - - if isinstance(values, xr.DataArray): - values = values.to_numpy() - - if len(values) == 0: # empty list, we treat it as not set + if field_name not in descriptors: + raise ValueError(f"{field_name} is not a valid dataset field. Expected one of {', '.join(descriptors)}") + result.append(field_name) + return result + + +def _records_to_messages( + records: Iterable[Record], + message_type: type[_MessageT], + descriptors: Mapping[str, FieldDescriptor], + required: set[str], + ignore: set[str], +) -> list[_MessageT]: + """Convert records and add the record index to conversion errors.""" + return [ + _record_to_message_with_context(indexed_record, message_type, descriptors, required, ignore) + for indexed_record in enumerate(records) + ] + + +def _record_to_message_with_context( + indexed_record: tuple[int, Record], + message_type: type[_MessageT], + descriptors: Mapping[str, FieldDescriptor], + required: set[str], + ignore: set[str], +) -> _MessageT: + """Convert one record and add its index to conversion errors.""" + index, record = indexed_record + try: + return _record_to_message(record, message_type, descriptors, required, ignore) + except (TypeError, ValueError) as error: + raise type(error)(f"Record {index}: {error}") from error + + +def _record_to_message( + record: Record, + message_type: type[_MessageT], + descriptors: Mapping[str, FieldDescriptor], + required: set[str], + ignore: set[str], +) -> _MessageT: + """Convert one field-name-validated record into a protobuf message.""" + missing_required = [name for name in required if name not in record or is_missing(record[name])] + if missing_required: + raise ValueError(f"Missing required field(s): {', '.join(sorted(missing_required))}") + + values: dict[str, ProtoFieldValue] = {} + for field_name, value in record.items(): + if field_name in ignore or is_missing(value): continue - - field_lengths[len(values)].append(field_name) # to validate all fields have the same length - - descriptor = field_descriptors_by_name[field_name] + descriptor = descriptors[field_name] field_type = infer_field_type(descriptor) + try: + if descriptor.is_repeated: + converted = _convert_repeated_value(value, field_type) + else: + converted = field_type.to_proto(value) + except (TypeError, ValueError) as error: + raise type(error)(f"Field {field_name!r}: {error}") from error + if converted is None: + if field_name in required: + raise ValueError(f"Field {field_name!r}: Invalid value for required field") + continue + values[field_name] = converted + return message_type(**values) - if descriptor.is_repeated: - values = convert_repeated_values_to_proto(values, field_type) - else: - values = convert_values_to_proto(values, field_type, filter_none=False) - - fields[field_name] = values - - # now convert every datapoint to a protobuf message - if len(field_lengths) == 0: # early return, no actual data to convert - return [] - - if len(field_lengths) > 1: # some fields have different number of elements than others - msg = [f"- {n}: {', '.join(names)}" for n, names in field_lengths.items()] - newline = "\n" # since we can't use it in f-strings due to old python version support - raise ValueError(f"Inconsistent number of datapoints: {newline}{newline.join(msg)}") - - return [message_type(**datapoint) for datapoint in columnar_to_row_based(fields)] - - -def marshal_messages(messages: list[Message]) -> list[bytes]: - return [m.SerializeToString(deterministic=True) for m in messages] - - -def columnar_to_row_based( - data: dict[str, pd.Series | np.ndarray | list[ProtoFieldValue]], -) -> Iterator[dict[str, Any]]: - if len(data) == 0: - return - - n_datapoints = len(next(iter(data.values()))) - for i in range(n_datapoints): - datapoint = {} - for name, values in data.items(): - datapoint[name] = values[i] - yield datapoint - - -def convert_values_to_proto( - values: np.ndarray | pd.Series, field_type: ProtobufFieldType, filter_none: bool = False -) -> list[ProtoFieldValue]: - if filter_none: - return [field_type.to_proto(value) for value in values if not is_missing(value)] - return [field_type.to_proto(value) for value in values] - - -def convert_repeated_values_to_proto( - values: np.ndarray | pd.Series | list[np.ndarray], field_type: ProtobufFieldType -) -> list[ProtoFieldValue]: - if isinstance(values, np.ndarray): # it was an xarray, with potentially padded fill values at the end - values = trim_trailing_fill_values(values, field_type.fill_value) - # since repeated fields can have different lengths between datapoints, we can filter out None values here - return [convert_values_to_proto(repeated_values, field_type, filter_none=True) for repeated_values in values] # ty: ignore[invalid-return-type] +def _convert_repeated_value(value: Any, field_type: ProtobufFieldType) -> list[ScalarProtoFieldValue]: + """Convert one repeated field, dropping missing/unrepresentable elements.""" + if isinstance(value, str | bytes | bytearray | Mapping) or not isinstance(value, Iterable): + raise TypeError("Expected an iterable of values") + converted = (field_type.to_proto(element) for element in value if not is_missing(element)) + return [cast(ScalarProtoFieldValue, element) for element in converted if element is not None] def trim_trailing_fill_values(values: np.ndarray, fill_value: Any) -> list[np.ndarray]: @@ -161,7 +257,8 @@ def trim_trailing_fill_values(values: np.ndarray, fill_value: Any) -> list[np.nd if is_fill_value.ndim == 3: # nested messages that have a third dimension is_fill_value = is_fill_value.all(-1) - assert is_fill_value.ndim == 2, f"Expected a 2D array of fill values, got {is_fill_value.ndim}D" + if is_fill_value.ndim != 2: + raise ValueError(f"Expected a 2D array of fill values, got {is_fill_value.ndim}D") if np.all(is_fill_value): # we only got fill values, which only makes sense if they really are encoded in the protobuf diff --git a/tilebox-datasets/tilebox/datasets/schema.py b/tilebox-datasets/tilebox/datasets/schema.py new file mode 100644 index 0000000..64ea0fe --- /dev/null +++ b/tilebox-datasets/tilebox/datasets/schema.py @@ -0,0 +1,68 @@ +"""Public Python field types supported in Tilebox dataset schemas. + +Use these classes as the ``type`` in a field passed to +``Client.create_or_update_dataset()``, for example +``{"name": "assets", "type": Assets}``. +""" + +from typing import Any, TypeAlias, cast +from uuid import UUID + +from google.protobuf.message import Message as WireMessage +from protobuf import Message +from shapely import Geometry + +from tilebox.datasets.datasets.stac.v1.asset_pb import Assets +from tilebox.datasets.datasets.stac.v1.asset_pb2 import Assets as AssetsWire +from tilebox.datasets.datasets.stac.v1.authentication_pb import Authentication +from tilebox.datasets.datasets.stac.v1.authentication_pb2 import Authentication as AuthenticationWire +from tilebox.datasets.datasets.stac.v1.core_pb import Links, Provider +from tilebox.datasets.datasets.stac.v1.core_pb2 import Links as LinksWire +from tilebox.datasets.datasets.stac.v1.core_pb2 import Provider as ProviderWire +from tilebox.datasets.datasets.stac.v1.processing_pb import ProcessingSoftware +from tilebox.datasets.datasets.stac.v1.processing_pb2 import ProcessingSoftware as ProcessingSoftwareWire +from tilebox.datasets.datasets.stac.v1.storage_pb import Storage +from tilebox.datasets.datasets.stac.v1.storage_pb2 import Storage as StorageWire + +MessageFieldType: TypeAlias = ( + type[Assets] + | type[list[Assets]] + | type[Authentication] + | type[list[Authentication]] + | type[Links] + | type[list[Links]] + | type[Provider] + | type[list[Provider]] + | type[ProcessingSoftware] + | type[list[ProcessingSoftware]] + | type[Storage] + | type[list[Storage]] +) +"""A generated message class supported as a scalar or repeated dataset field.""" + +_MESSAGE_FIELD_TYPES: dict[type[Message[Any]], type[WireMessage]] = { + Assets: AssetsWire, + Authentication: AuthenticationWire, + Links: LinksWire, + Provider: ProviderWire, + ProcessingSoftware: ProcessingSoftwareWire, + Storage: StorageWire, +} + + +def _wire_message_type(value_type: object) -> type[WireMessage] | None: + """Return the wire class for a supported public message class.""" + return _MESSAGE_FIELD_TYPES.get(cast("type[Message[Any]]", value_type)) + + +__all__ = [ + "UUID", + "Assets", + "Authentication", + "Geometry", + "Links", + "MessageFieldType", + "ProcessingSoftware", + "Provider", + "Storage", +] diff --git a/tilebox-datasets/tilebox/datasets/sync/client.py b/tilebox-datasets/tilebox/datasets/sync/client.py index 647c3ce..6fe404a 100644 --- a/tilebox-datasets/tilebox/datasets/sync/client.py +++ b/tilebox-datasets/tilebox/datasets/sync/client.py @@ -123,7 +123,8 @@ def create_or_update_dataset( Args: kind: The kind of the dataset. code_name: The code name of the dataset. - fields: The custom fields of the dataset. + fields: The custom fields of the dataset. Supported generated + message field types are available from :mod:`tilebox.datasets.schema`. name: The name of the dataset. Defaults to the code name. Returns: diff --git a/tilebox-datasets/tilebox/datasets/sync/dataset.py b/tilebox-datasets/tilebox/datasets/sync/dataset.py index ea86271..3bc2a03 100644 --- a/tilebox-datasets/tilebox/datasets/sync/dataset.py +++ b/tilebox-datasets/tilebox/datasets/sync/dataset.py @@ -521,8 +521,8 @@ def ingest( data: The data to ingest. Supported data types are: - xr.Dataset: Ingest a dataset such as it is returned by the output of `collection.load()` - pd.DataFrame: Ingest a pandas DataFrame, mapping the column names to the dataset fields - - Iterable, dict or nd-array: Ingest any object that can be converted to a pandas DataFrame, - equivalent to `ingest(pd.DataFrame(data))` + - Mapping: Ingest column-oriented fields, with one collection of values per field name + - Iterable of mappings: Ingest record-oriented data, with one mapping per datapoint allow_existing: Whether to allow existing datapoints. Datapoints will only be overwritten if all of their fields are exactly equal to already existing datapoints. Tilebox will never create duplicate datapoints, but will raise an error if the datapoint already exists. Setting this to diff --git a/uv.lock b/uv.lock index e97b7c0..87f1f6b 100644 --- a/uv.lock +++ b/uv.lock @@ -161,30 +161,30 @@ wheels = [ [[package]] name = "boto3" -version = "1.43.59" +version = "1.43.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/c8/ef9de1d7413da3adcdb6363258ba6b5cc703593409d8c1957825b20a69d3/boto3-1.43.59.tar.gz", hash = "sha256:4e9b14f89adc1a533c89312e86d8e00455a6f15d398796d92f9191b06e56b401", size = 112653, upload-time = "2026-07-29T19:33:25.703Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/70/64c16a00866976fac63f0b672bba5303060bc29814c20200f2012f650574/boto3-1.43.61.tar.gz", hash = "sha256:a818a2ed37ff555c8c710f9c308a5407ef325c43f4162d7f5ce961e20440ca18", size = 112671, upload-time = "2026-07-31T04:17:12.624Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/10/c5999e72b020012f2e0ccccf2a15632329edd34cb95b02b1ccfb1712ec08/boto3-1.43.59-py3-none-any.whl", hash = "sha256:58b9635deebf075c1c3d76df78df08eb2979c2a74283194676783a0bff3b4557", size = 140024, upload-time = "2026-07-29T19:33:23.751Z" }, + { url = "https://files.pythonhosted.org/packages/64/b6/414bc692bd4d8e1f22336024026de31d7f2bfd2ba67b794c77e2181fa76a/boto3-1.43.61-py3-none-any.whl", hash = "sha256:67f1544ef97caddd6d0ebd613010981e3990ac843bc010cc202a88b489b182a0", size = 140023, upload-time = "2026-07-31T04:17:10.542Z" }, ] [[package]] name = "boto3-stubs" -version = "1.43.59" +version = "1.43.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cf/7d/daf6dbd541106b6a62753cda2a0ae566a20f3c251644beb68b6836ae2586/boto3_stubs-1.43.59.tar.gz", hash = "sha256:158858b7733a123a536f9d9665c99d6514528426e35480b95de2684a98183181", size = 103230, upload-time = "2026-07-29T19:55:25.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/32/71d4c33b578e08dbef35d975fb62ce53b77efa5ea594aab06fd6dca6b2a5/boto3_stubs-1.43.61.tar.gz", hash = "sha256:c07479fd99ddcfd46077b5d3dbf1897e90295ad663d59960e1210460a3ae6818", size = 103434, upload-time = "2026-07-31T05:25:25.286Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/61/b619a497acf73a0cc5dc640eea8e147271a07095a07c9bb094ba6df318d3/boto3_stubs-1.43.59-py3-none-any.whl", hash = "sha256:599d8a6166240dd7a5c5103c43bb267e8596641ce1350502fb65cc6ed72c9c58", size = 70963, upload-time = "2026-07-29T19:55:21.52Z" }, + { url = "https://files.pythonhosted.org/packages/c5/05/55768541fec6161b0806159f77f6957e294c4bd79f21bf87bdfa0e8aa8ae/boto3_stubs-1.43.61-py3-none-any.whl", hash = "sha256:770e4f23e6d14070d24c7e007b01fca48857c301ec7b23a021e34e2efd6ffa66", size = 71074, upload-time = "2026-07-31T05:25:15.925Z" }, ] [package.optional-dependencies] @@ -200,16 +200,16 @@ essential = [ [[package]] name = "botocore" -version = "1.43.59" +version = "1.43.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/37/3712a70796583570a5a2e426163e13762ba5ec615a73e966ad18e5933954/botocore-1.43.59.tar.gz", hash = "sha256:8016da69ecc1d705249a8ef13548d3c95eec87ac1cd26133a8bdfa73ca175be0", size = 15788291, upload-time = "2026-07-29T19:33:14.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/9d/be2fdd9f9723734d019d9f3f9f1e8dfe5fb55ab26909c963652cfbdf1761/botocore-1.43.61.tar.gz", hash = "sha256:23a9e8578b08bfb83953d32a08b304bb2cb7a7e2607a44b212c1a39862c2024e", size = 15798287, upload-time = "2026-07-31T04:17:01.494Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/cd/62d749f824b25c152144665f7c5eb8b5ca8be967a87e0c63577b7d4501ae/botocore-1.43.59-py3-none-any.whl", hash = "sha256:21393c35d23b19d7ba95cc4156b59f4013f80696d667997e1abd9d4e29651708", size = 15471171, upload-time = "2026-07-29T19:33:10.864Z" }, + { url = "https://files.pythonhosted.org/packages/0c/90/a95b9c70a26d95ac6875e356986a82639f3c11b39b750502bd10be4926a2/botocore-1.43.61-py3-none-any.whl", hash = "sha256:167c8626661561958a8b5d2066e478113c5789a79e6fbc711b249f2c3d5bca5f", size = 15485538, upload-time = "2026-07-31T04:16:57.579Z" }, ] [[package]] @@ -1030,75 +1030,75 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.163.0" +version = "6.164.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/08/4cbfa0327e9df00f57fc67f91847add2f0dd6c23408935273b095ee9a9f1/hypothesis-6.163.0.tar.gz", hash = "sha256:520480d4bd3a17557616c25923640953e360332c89d012fffcebd69857e674a9", size = 490145, upload-time = "2026-07-28T07:16:46.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b1/3eea7a422de342bd0095a9672c3e03fe0466e4561a55499a1e01b4a9f098/hypothesis-6.163.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:331906cb029b6b360b8ebac3ec00c3cfa720037fe2efb294a503a1979c9a9a8f", size = 769898, upload-time = "2026-07-28T07:15:19.323Z" }, - { url = "https://files.pythonhosted.org/packages/bc/24/45b5c948c76c16ecf1e4ad1ca4a4a3fce55b3317ee172bc8230ff2956ae1/hypothesis-6.163.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b4ad2134405d5345434c22dea96bbc12c85abcfc3c253a8063dbc9ff01164555", size = 765438, upload-time = "2026-07-28T07:16:04.69Z" }, - { url = "https://files.pythonhosted.org/packages/0f/72/7725039a75b3679dc445a169026b11860a0e67a600c4ffed49a42040a000/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50073f8e63c1e7d3403899755657a990d8bba7b5b5bff66b1c56796d4969bb28", size = 1094699, upload-time = "2026-07-28T07:15:56.668Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fa/bcfa3879f303a302ec6e5f4d35b583924867a0821b42fa275f440f3b8dab/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c5d1e6bad47edf6fb1d7406cf6d67314ac08325c63a49550d782a4596ea302b", size = 1123315, upload-time = "2026-07-28T07:16:40.381Z" }, - { url = "https://files.pythonhosted.org/packages/5f/7f/e7fe2f0658db5182bb1d4b266d17f8f81cf3db82eeba27677ddfea13ac09/hypothesis-6.163.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ec3b709508ccd835d8ded1db025b7800618f2289a22a6bfd4927da5f4eb33c", size = 1144220, upload-time = "2026-07-28T07:15:20.617Z" }, - { url = "https://files.pythonhosted.org/packages/90/14/c26f93a4693bfd83d7ba14b7043d986458026f6635d1b157bc2f1c56a59e/hypothesis-6.163.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:7cb3d927360fe73f9a06d646e6082237142ee39c24679c7133d22bf06dd03b45", size = 1099527, upload-time = "2026-07-28T07:16:17.955Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f5/4c1dde28d2e18e169dd6c471ef4bcf12abf3c6a3f4a1744bdabdbdf411ba/hypothesis-6.163.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f3cceb4720a39127622fbf3bcebe1775b894372c53b5edddfdef10bbdeef9ec", size = 1136272, upload-time = "2026-07-28T07:16:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/54153509ed42cc17e1b65efe34a0c781f42fb04c902dc5f25486c537ec88/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59f5fdb8addb44c17520a60d50542d9db6ceba577bbf54efefa9c10ee20be140", size = 1268518, upload-time = "2026-07-28T07:15:42.991Z" }, - { url = "https://files.pythonhosted.org/packages/81/86/f86f0d15d91b9cbff3546c1b8891950b2b08c0527e7494133fb2180e1b8b/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe222f50a1898e87a1e7323ab35f9e956278efabe4dd55a1342808206d05ad", size = 1396357, upload-time = "2026-07-28T07:15:26.696Z" }, - { url = "https://files.pythonhosted.org/packages/46/3a/c096b6b272f15e17e8e65c6ccfb2e1d167456ff5bbd9db33dca3185199f6/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:56ed585baab75cb98462c57ca88bbdc6a9d935a14118dd572fb476c3ecec2a06", size = 1269128, upload-time = "2026-07-28T07:16:11.902Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9f/0807445874b3083a22c9a14a0ffc31bd0060ef3b408ce4cb31c20779cdf8/hypothesis-6.163.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2849c23b2e0fe2eef4c1ec336b01eac7ad7397c49fca43c264f59ec1e6046eac", size = 1311189, upload-time = "2026-07-28T07:16:08.545Z" }, - { url = "https://files.pythonhosted.org/packages/d6/de/3319aa8fcffd1641defc1c5eea1ad646a33d1b62528ef487a89752bc8079/hypothesis-6.163.0-cp310-abi3-win32.whl", hash = "sha256:b2ddcdaf6691101e06dc4a5add7b8c8fdf1e68daba599255a281f3f3550d3331", size = 655743, upload-time = "2026-07-28T07:16:30.966Z" }, - { url = "https://files.pythonhosted.org/packages/8e/48/36bc72910451e6e88b75e59a6ddbf0db34ff61a3b11c9439801dfd5fec20/hypothesis-6.163.0-cp310-abi3-win_amd64.whl", hash = "sha256:4ab0dadc09c537d4ac57e564039dfe7daf09c98375306d54bfc0fd6c218efcca", size = 661902, upload-time = "2026-07-28T07:16:34.407Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4d/f20c3dfbe8cd897e1b33eda6f3107e15e451ef09e7f7a6749878f9d6f09d/hypothesis-6.163.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:67d1593941ede41052b4a35ec25b50d0e280358c7674ef7812d520010e7e8bdf", size = 770601, upload-time = "2026-07-28T07:16:20.056Z" }, - { url = "https://files.pythonhosted.org/packages/29/aa/0c42c477575ed62170df7548f70c85f53cbfafeaf9ee501d995c885d27d1/hypothesis-6.163.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee47c2cb1be03a052ebd3549dad07f636a98b3ccfd7acbe5e17b3b7da0ab9e37", size = 766343, upload-time = "2026-07-28T07:15:13.794Z" }, - { url = "https://files.pythonhosted.org/packages/34/02/1edec861e6837d2d78f8a4ed87dbbb6844dc1c7197209cb8ab1a76cae830/hypothesis-6.163.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00d3091b28de83c5116e0ccd9a4bcb28ef61d2aace5df91093bb22434fd2350c", size = 1095207, upload-time = "2026-07-28T07:15:47.452Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8f/ecbd7356432d4bdeac33cb7285fd905f19d425adb9df10d18e45e733699e/hypothesis-6.163.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae7305ae20fddeea09df317b920c45d3e20bfedbdb041f4db6ca5267c458189", size = 1144733, upload-time = "2026-07-28T07:16:03.147Z" }, - { url = "https://files.pythonhosted.org/packages/23/43/cfc48bb8a33e060c3822cd8b58125e8fd5f4fa9d2eb250eafc50d62eeced/hypothesis-6.163.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9be37b7ddf0af9e3f9112cd133afc34e78a56da1f96db5f2b4fc289fe1c4d1c3", size = 1269169, upload-time = "2026-07-28T07:15:35.737Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2b/62dd7a3589e7d2b38521889f7173c588c966eb7a27d70c61cfa2f4edb0dc/hypothesis-6.163.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58be45d1737bf8c2e10cf29505c0f10f8a23d61bc82e4339182a6c8251cbc2d9", size = 1311512, upload-time = "2026-07-28T07:15:05.833Z" }, - { url = "https://files.pythonhosted.org/packages/7f/94/95d315013c4bea4d244e77cb10cb7097d3fa0fba851ef7a746daf4629e3b/hypothesis-6.163.0-cp310-cp310-win_amd64.whl", hash = "sha256:a2a20e9835d3c4b293a709ee6ef769bcb18c6ed4ef337a9e251c1a9496d5e8be", size = 661788, upload-time = "2026-07-28T07:16:23.768Z" }, - { url = "https://files.pythonhosted.org/packages/63/80/796ac61dddb3ede550ea127e28e027a57a4ea481581c0bb27701fefd655a/hypothesis-6.163.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:213527755f0fc2b1f3721e73fd60023e2752a48f914e3e2df8d35111956ae5c8", size = 770366, upload-time = "2026-07-28T07:15:22.003Z" }, - { url = "https://files.pythonhosted.org/packages/eb/87/53924f322922bcfc05e40c79d432978333827b335fa9efa5fd1e8cbf3c90/hypothesis-6.163.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca1b48bde68c528a79dec2a2859e05035802e5b1c9c3579f388c9de6ed6d0148", size = 766150, upload-time = "2026-07-28T07:15:16.151Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fb/b62480e6510052139d7b2a0219d4ae8bd52ccad0ed74db15dd61330a6962/hypothesis-6.163.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a3db868a943c814cc557104712d43bf609adfe5ea9f708f38377d366b4855f8", size = 1095046, upload-time = "2026-07-28T07:15:45.796Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/fa8aac7b47f41d0fe30fa270c5026f37a0c0c60d7b9a1a93dcc8fcfc50ce/hypothesis-6.163.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4159a1c2560e10de51b1c14956e277eb1b37526c9abef9e87c1e531760486448", size = 1144516, upload-time = "2026-07-28T07:16:32.662Z" }, - { url = "https://files.pythonhosted.org/packages/4f/26/c543c76d8a8b8f58f2d7adf0cb42e4928be3464e95f4fa9d7221b42ea9ce/hypothesis-6.163.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffdda3006a383a48f71a23b4f2b3fae3fe1b09af67925d885985f7ec34d66bcb", size = 1268886, upload-time = "2026-07-28T07:16:36.102Z" }, - { url = "https://files.pythonhosted.org/packages/d9/54/3613ef980cfa60f5c6bfbc533989d6c67b6b5f4e9e638fc6d010a5dd852f/hypothesis-6.163.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3b6cee2afe6c67b31a4a64b63a876e0b020befdc61daabea80f7a0e14f19203a", size = 1311472, upload-time = "2026-07-28T07:16:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/af/0c/cbaebc49fd5807a4b4286dea6e60d5330951e43115d002371bdfc99e70f8/hypothesis-6.163.0-cp311-cp311-win_amd64.whl", hash = "sha256:0a933aca9ebf9daf951d07cf01200c94c321b6ee0b42cc7b67675c9686d914c2", size = 661580, upload-time = "2026-07-28T07:15:32.939Z" }, - { url = "https://files.pythonhosted.org/packages/8f/2c/74e989557efc429b28282cbe754c17fc74495467a72a1c94b5eb734fe374/hypothesis-6.163.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a57352efa938889ea9992667a5014c0fc870d03945de71918574d1cf28276378", size = 771445, upload-time = "2026-07-28T07:16:44.2Z" }, - { url = "https://files.pythonhosted.org/packages/fd/08/3ed2089d8cbeae125ea92879e82b99ea3a8b676c837710019249ccab379f/hypothesis-6.163.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a0c396244c13805edcb73ff467c4c8178ccefc41c4ef5ed00a68e612fd773e9", size = 763070, upload-time = "2026-07-28T07:15:34.318Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/4e85a11f15e8ebd730f3b3e4a5d83653f7da482a4e81fa36958951d89b4a/hypothesis-6.163.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28a6cc1c25a6cc9b6ec079eaabd32ac769994831ecddd57123ce43c9056dcf34", size = 1093500, upload-time = "2026-07-28T07:15:49.539Z" }, - { url = "https://files.pythonhosted.org/packages/1f/29/c4790d2a5e6f48e6be5f868124a0d3c7e1d0102e2ca50503a0718e51289c/hypothesis-6.163.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd312b15044b1c1a0920a5827a830559b2d1fa380851cedf509f8b835309c5b9", size = 1143541, upload-time = "2026-07-28T07:15:44.393Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/01b72694e758449e9b530b72ecaf5f3625c80a3968de0d12ee6e784de22e/hypothesis-6.163.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b839dfd1342bb50570cb0c66b80322307cdb468abf14faf5df4dab022bc1b9ce", size = 1266326, upload-time = "2026-07-28T07:15:12.604Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a3/b3997add991e2fc6123b3c8dde3631f9f633db67ba23f5b2567736b50b9e/hypothesis-6.163.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4f5be1482189c7b0a1dcac269fffe97a7d18cc04ac9a9a4d6613212dd87f38b", size = 1310536, upload-time = "2026-07-28T07:15:17.798Z" }, - { url = "https://files.pythonhosted.org/packages/5d/fe/36f576185d63ee0b4d94ed9564415de08baed4937e785b3695c8dd665c9c/hypothesis-6.163.0-cp312-cp312-win_amd64.whl", hash = "sha256:7ca7b20bf38d51e15f7808b0239791c4792b1709ce0c63093acaff56a09c31e6", size = 659021, upload-time = "2026-07-28T07:15:53.143Z" }, - { url = "https://files.pythonhosted.org/packages/58/69/c474a3fa1c33d9a6e059e820221275d9c60eb10085f6164212c291856157/hypothesis-6.163.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a16ebce774755a7a652bd44c62101dc914372ed1a98935969624848c9627b4a4", size = 771335, upload-time = "2026-07-28T07:15:14.988Z" }, - { url = "https://files.pythonhosted.org/packages/54/27/8951688de58314780ba0af5bf2675709009dcc1fb568d244f2a03c4de8e9/hypothesis-6.163.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40dfab6fe6a02a80abef81aebf88e53cd529e3f2f6ba3486b674a67b1f4a3512", size = 763020, upload-time = "2026-07-28T07:15:25.218Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d7/9eb7451400507f4b5c972c81c2bc57d0009597ea37295519a9645963a50e/hypothesis-6.163.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f28ad27193c1fbcfb52ef2ee63d2b721563525089e80962b4268b306dac45507", size = 1093415, upload-time = "2026-07-28T07:16:27.408Z" }, - { url = "https://files.pythonhosted.org/packages/24/cc/c12c780676c7a4a4051d05e7b278a94bf1bb496ef31882881160f16866c9/hypothesis-6.163.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aac96db8a6c7ee43aba2ee0d3c43893da1fb7c38ed54790c1be2b6d8fd87b96", size = 1143356, upload-time = "2026-07-28T07:16:01.571Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c1/61c5ebdb77a3f259803e60a12f56de35f8b6d641a6219f798dcfa69dece3/hypothesis-6.163.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9d23f0f3a14bb6e6f99c793d340196dba4af95ba25bfcab624d1794f540f5e27", size = 1266375, upload-time = "2026-07-28T07:15:11.353Z" }, - { url = "https://files.pythonhosted.org/packages/a2/53/f3a89b4d21d89098d1dec749632aa0fece04f5d17a9e7e91c7f10596d55a/hypothesis-6.163.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b123b4995a7612f1130e2b2362c9a5d0568df887bf7e7bdb45c23af8cd5423c9", size = 1310257, upload-time = "2026-07-28T07:16:42.213Z" }, - { url = "https://files.pythonhosted.org/packages/06/e7/88e71c4cec0df68aa7a2fb251083c544f30697bd90fb4b1ed19de493ab81/hypothesis-6.163.0-cp313-cp313-win_amd64.whl", hash = "sha256:b268211e625cd550e361fc387bf1db5deb1e9cae0ce4041116f0a0aafeef7c06", size = 658983, upload-time = "2026-07-28T07:16:10.289Z" }, - { url = "https://files.pythonhosted.org/packages/71/df/e3b2f0419cebcc86bba96a357d7ef37790538ed6b09f3183ae01f1fc3d23/hypothesis-6.163.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9105c66ea8dbc108adc42058bb7b65bd953f53ee178bf63bf9ebb0cded6c8c96", size = 771564, upload-time = "2026-07-28T07:15:28.017Z" }, - { url = "https://files.pythonhosted.org/packages/f1/f8/a6f75e61ecd983029f8463bf007498155c4ed33114e6931e7aa3fbaf651e/hypothesis-6.163.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e165f6cc2075059b7c95dac1612bfb25494f72d90f56880e84c288b089f8a896", size = 763164, upload-time = "2026-07-28T07:15:29.973Z" }, - { url = "https://files.pythonhosted.org/packages/5e/07/5193812567f6ca46c1f0cd02dabfd47c85d7c9e3287b8a870fc8150ee462/hypothesis-6.163.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cba5202f74e7e4cdb676d86f26e8cc1b4fdc88f7f58ba73c8ac45b6b22f3070", size = 1093916, upload-time = "2026-07-28T07:16:25.626Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9d61f0e306499d734dc63ebe374b01c5f50be7072fd5e76eed25cc0b86ac/hypothesis-6.163.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7f706df6839dcc53f20833f2933cbcd126fd2fdee7c312e053de49df4b64e44", size = 1143547, upload-time = "2026-07-28T07:15:07.311Z" }, - { url = "https://files.pythonhosted.org/packages/60/b4/2a9eb04c9847ddf7e6b30bba8bc27b7efe5511d368335b6a41657b3f02dd/hypothesis-6.163.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:487ab8ec2f01a225d6a1e2ceadc5290cde2c691952bd2e7f76199cf82e06fb25", size = 1266755, upload-time = "2026-07-28T07:15:41.457Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f3/8d1903fbfcbf48b90bb5590826821afc9b4fc494391ab1fdfab9df23e928/hypothesis-6.163.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ae63dec6d1d467b7f4737455f81a7a82f14a41c14510937fcfbc726a085b5f8", size = 1310560, upload-time = "2026-07-28T07:15:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/47/2b/1a8c0457b44775d0aad369f21cbae8026b772a71aa917d1b956b7349b2a4/hypothesis-6.163.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:31dc46c48aa53c3ec92d03120978ca7f19b9cf96d195ed3fc93503f1433c94a6", size = 603067, upload-time = "2026-07-28T07:16:06.697Z" }, - { url = "https://files.pythonhosted.org/packages/10/56/a9cd947064043035457dec0124ac2437ca72acf358b30cf203a229ad831b/hypothesis-6.163.0-cp314-cp314-win_amd64.whl", hash = "sha256:320b076bf6436f971f1c73ee651e60001226d1b4e341f2c4a1ca87248261ca03", size = 658931, upload-time = "2026-07-28T07:15:37.174Z" }, - { url = "https://files.pythonhosted.org/packages/77/37/2d16317fda0ecd915cb094be9a9e8911106e693ed03a4d680de314711854/hypothesis-6.163.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ab34c61d9249f1a8129cb4276062c04e3e47b5be8de6446e7c7fe11362d6fe43", size = 770142, upload-time = "2026-07-28T07:16:21.827Z" }, - { url = "https://files.pythonhosted.org/packages/3f/65/d80a9bfb7868f2c6c072a684993548391c56e0afa1972f79ee1fe168d91b/hypothesis-6.163.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f2f1b67a48da86d3e41c9445367b49a49f7efdb60fc8b5e3593f05e6afb2efbe", size = 761694, upload-time = "2026-07-28T07:15:31.36Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/c1f2515c638d2637300bb2fd6af129319eae75cdf2ed7804644535f64fb2/hypothesis-6.163.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c084749c115ea7918cf7efa144682783da17eec70d1276689182b871126e715", size = 1092511, upload-time = "2026-07-28T07:15:51.503Z" }, - { url = "https://files.pythonhosted.org/packages/cd/f8/b3cd946308b5a09e52b075792610da310c0b7972bb1e8aa673400b86540c/hypothesis-6.163.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e72e8d5818e5ef8cd6a2191c386e3fd1a6d9e3739cf97289b4d9b5dbc8e38d", size = 1142425, upload-time = "2026-07-28T07:16:15.626Z" }, - { url = "https://files.pythonhosted.org/packages/85/96/b122859e6f7335b54aff759f573a813158d2249488450a75e76462ddaf61/hypothesis-6.163.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5a3ac6c62d49f7fe518dfe7fa924fa03aac839993702207802b0e45f9e1b0dab", size = 1264946, upload-time = "2026-07-28T07:15:23.848Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e0226e8c904b8b4788eb52dacd303c91acbd260904abf64e8f9bad03c88a/hypothesis-6.163.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e568a3d766b7ba8df00e0c33efc4c6530cde14fbc72daabe4824eed211ed7596", size = 1309320, upload-time = "2026-07-28T07:16:29.218Z" }, - { url = "https://files.pythonhosted.org/packages/8c/48/e8bd29fed17c9608524b6a39db4a27b6eec7ce85bda78bba3ee0deebd80e/hypothesis-6.163.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8f22fb8218ba6a452bf9000fc656e1ed57625d17cc8a3871a0fcea3b1b69ebf", size = 659064, upload-time = "2026-07-28T07:16:00.075Z" }, - { url = "https://files.pythonhosted.org/packages/39/db/d4e877b8639bbebedeff4a0511f5fc459c06a31d79a79bf75584eddda8da/hypothesis-6.163.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:002a9709345892279fb0e81b5a05b72d08cfe81f937339827be0d588607ca9b0", size = 771252, upload-time = "2026-07-28T07:15:08.694Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/3492490e997e5c8c9244a56728a623ca817577af3462fca5d1def060a066/hypothesis-6.163.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:52f16840add2eb02c2416f3b83cec4f527b6c19699f2d31eff4859233c715526", size = 767161, upload-time = "2026-07-28T07:15:54.972Z" }, - { url = "https://files.pythonhosted.org/packages/51/73/37a4d4a6f3f0789fb2fddc851da957075fbe223706d1148ccc4dda825171/hypothesis-6.163.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34fc895691a2420595506eb17f3a104f2fa9039f013c0770a6cc2743ccaf6fed", size = 1096016, upload-time = "2026-07-28T07:15:09.821Z" }, - { url = "https://files.pythonhosted.org/packages/48/46/20bc7801f8b539334dc0439c28753632a078c96d6732eccf1bd4880644d2/hypothesis-6.163.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ef8954e37c80e0c46e6161eef1c72c71059b95250e620a77bd646f6c7a52a2d", size = 1145797, upload-time = "2026-07-28T07:15:39.864Z" }, - { url = "https://files.pythonhosted.org/packages/94/c2/4d5c8feb78ed86e698d4e0665d3179eb657ae66d3606ffe096d8055aaa82/hypothesis-6.163.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0838a28e9943d5b834ebae59b02adda76e2cd1e65caa808104c72102052057d", size = 662696, upload-time = "2026-07-28T07:15:38.519Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7a/ac/7b76103bd74d8457e4de0c6a6c3a26ac6327016438bde125e0a3de83a5b8/hypothesis-6.164.0.tar.gz", hash = "sha256:5d63d263d8c71b571638c18d9591f6e34b836c60a12469e9d9105c1c785f00f1", size = 492022, upload-time = "2026-07-30T12:39:49.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/fe/d5b75a55892b33e72945f82efc71f645d29c0bfdb9f00727f7535a52edcc/hypothesis-6.164.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:14b861ac3353f8643b82a3ba76b8a0a54d2a06160c32b9a1f64a8ab41b179089", size = 771561, upload-time = "2026-07-30T12:39:00.404Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b0/2f01e9efc7267446bad0e2a68f7472daa174a72553d213b16aefe44b2bda/hypothesis-6.164.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:3d8c8bb00a4b86ae90b9ad41f3e1c99d016ec3e64c0ff9d676a4bb7be4f56948", size = 767079, upload-time = "2026-07-30T12:39:23.123Z" }, + { url = "https://files.pythonhosted.org/packages/c3/26/d7bcd26b58e1df2bd39116b924b2a72676215d9650e68cbff9a629c3ce30/hypothesis-6.164.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e80e3ba8eaf37664eaa0f2625cef120b330b128a7df570210cf8be4f5ae65aaa", size = 1096364, upload-time = "2026-07-30T12:38:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/9d/17/99fe7ea866935da83444c3ef7885a14fc7349d96ff61c6faebd37ef4edf2/hypothesis-6.164.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8cdf70f821e2d2f3a0bccaab29830aea8aefb63a77806e7e91246fb65a10c8d3", size = 1124963, upload-time = "2026-07-30T12:39:13.1Z" }, + { url = "https://files.pythonhosted.org/packages/38/e8/df08be6296cbc1271d44e81f8ff9dcd6267a07552fb768e0fdc166e93d40/hypothesis-6.164.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc3743e22b3cffa7267b4bc74d03628606e4a115495728e986a7be220987315", size = 1145886, upload-time = "2026-07-30T12:39:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/4e/72/d5cf6fbfac40891d4281f630e16a6eb217ff56f97e350a06e0fd9322aa6a/hypothesis-6.164.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:730f09d4afcd8a918b3d589bfb6421e3b41c057aa57652a773ef4f512cc60836", size = 1101181, upload-time = "2026-07-30T12:39:05.194Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ff/7ceb002329febffb678b65835ca6e9479a916325d088aadb0210d07f8252/hypothesis-6.164.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9651cb48cb5a995295b442138d15d381547b935dcb0066fca7148a7955347400", size = 1137970, upload-time = "2026-07-30T12:39:16.076Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8f/c12c697b73ca9ca24d8a913879e3e0a9db86479754c7221554247c701565/hypothesis-6.164.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:51d161d2655dd86143b370c577267b5b7b4c2e8fcb8a3f22c1a787572aad707c", size = 1270184, upload-time = "2026-07-30T12:38:54.436Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2f/93f1c850c794fc9c80f5e61b3b20652126b865e6f57b348ae530446aadc7/hypothesis-6.164.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e8a250552390128b57e3afe55035ce2c2cb1f6f0919817657854244f071bc5be", size = 1397987, upload-time = "2026-07-30T12:38:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/bab2546325e15e87c8518dfbca263c81dbc35d566c516d66c9da98a38b77/hypothesis-6.164.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:570cd51944e1cc3443847d8afa3d17fcf8aac475a1f744c9e7318a5ad7ef5c9f", size = 1270755, upload-time = "2026-07-30T12:38:51.571Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4e/ea97dd39678a42dc5a24e3e2a64d3b950fad9fb1dcce8d7be5afb52a0335/hypothesis-6.164.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3a423e543055b3de5af7a7624c4285422541658367211fa293a3a57dd0ad01ba", size = 1312888, upload-time = "2026-07-30T12:38:30.847Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/a6f2d5b12b23d65f16eb398750e430065f9d1f40f4418569e3b87ef58d23/hypothesis-6.164.0-cp310-abi3-win32.whl", hash = "sha256:f5e51490b2ce64c66138f24477d83c71b6224ab0ef65700da10187c464b54e94", size = 657401, upload-time = "2026-07-30T12:39:11.581Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d3/c5ee410daa594cac2d3fe1fbe5473f2390e35f4369e168a817e43341ce2f/hypothesis-6.164.0-cp310-abi3-win_amd64.whl", hash = "sha256:c9059dfbb039342b6590bbce207f90e0f9a80fdf45a404c68c2d3e598be78ab3", size = 663566, upload-time = "2026-07-30T12:39:30.27Z" }, + { url = "https://files.pythonhosted.org/packages/02/43/08818e5df46965d122bbb7945fdd07ff25d150dd6fda2649e5e786e072c9/hypothesis-6.164.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:dfa1bc85784616a95a73df72a0af616ed230b10390a1c904c9097ba8706ce6bc", size = 772278, upload-time = "2026-07-30T12:38:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e0/deb80f6031a2f7f9e492e14d5db8028c45de018cbd161e9d327c4c2607c4/hypothesis-6.164.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:232efdf023a9f18918199745128adcd0396f9176469ffd82c380f8175b446066", size = 767976, upload-time = "2026-07-30T12:38:32.026Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0a/a6235b947529ea01793a64c810c956cfca52a6aacde1545dd814fd737f64/hypothesis-6.164.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:360583bca38dbb14438901e27c7d9259ed35023fde301ab4f16515408ce345f4", size = 1096868, upload-time = "2026-07-30T12:39:47.351Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1e/8d5e6d63abd800af91da4c797bc45fea8be2d73da302f2f79368b39a6083/hypothesis-6.164.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8cd46b24f0f99396d64998126406e2f2fd1f92fe3478007ccc8c51672370759", size = 1146396, upload-time = "2026-07-30T12:39:17.752Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cd/157945fa5ffe993dcac40bbf8a2b86f0372ae590cdc7e2b2489429b3c0f7/hypothesis-6.164.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:15f4ab9a69c0707dcd2e9320fabb396cdc1094ab8c1b6dadad04d0f73c18509f", size = 1270822, upload-time = "2026-07-30T12:39:21.126Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d9/b80a73241bc7549a42beec1903b4226a2758bb559195e5d8dca1d56ceedf/hypothesis-6.164.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b8168a4ea415b3df962e91ded89c81079618879f70be7a6ed16c95cf19d5e0c4", size = 1313094, upload-time = "2026-07-30T12:39:31.895Z" }, + { url = "https://files.pythonhosted.org/packages/32/22/18ec1050d301103e2831daa8f1d01ea6480a4dec0f1c51a17827f23aad4c/hypothesis-6.164.0-cp310-cp310-win_amd64.whl", hash = "sha256:64be21b68bcb8f31e76975d6366eab5f544c5c712f69759c4cf596f34965ef36", size = 663413, upload-time = "2026-07-30T12:39:24.792Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/eb86342b486f6392e884f3974ff144ae978e4646787250199f11b9d0931b/hypothesis-6.164.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e0cffc2228f9185e02eb48cfbd8e30ddf9968ccaac55ab1fb70cbda9aad97507", size = 772036, upload-time = "2026-07-30T12:38:22.166Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/15415bd08b9ae221381ae01d6f453055744112b730c4a670b1642fce4b83/hypothesis-6.164.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f19befdb5e2d8ebe42edd0d2150681a97f599af478ed4e5c86f43762d068b760", size = 767811, upload-time = "2026-07-30T12:39:19.419Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/59624a5c3194c2cabe0ceeb0d14294b093d8e2d712720bc09320ff73d72a/hypothesis-6.164.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7115077451e893cb2b96cc30066fc1033ee0788f2d85557fc8479f26b4e6eb", size = 1096709, upload-time = "2026-07-30T12:39:08.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/3a/09d6c06bb24e6099bdfa7bf74d0f22fbb050f02709563d744d705e44f24a/hypothesis-6.164.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa97ba4c59014260c07c35fb3447c3c47eb2faedcffcdd76a9a7967fcac4208", size = 1146181, upload-time = "2026-07-30T12:39:06.668Z" }, + { url = "https://files.pythonhosted.org/packages/59/11/27390692c2529fed8ebd337127b42299f0f5670f66decedb3cc4c1721d3e/hypothesis-6.164.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:900bb366b2be38c01753345cfdec3b7f30b1b274b9ffa0b0c0bfb3fd085278db", size = 1270536, upload-time = "2026-07-30T12:39:03.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/2a/7021041460601e2711dbb9dc8c5efbefbbbeffb56b52407ae674d02666e5/hypothesis-6.164.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:def37fdb4d4edb9b423e5ae0625f36074063a6ba82a7d3149dd74f59a291ed3b", size = 1313128, upload-time = "2026-07-30T12:38:44.311Z" }, + { url = "https://files.pythonhosted.org/packages/30/1f/40cf7209663d4a1d760ead952c58dfba7aca168455dfd436b4c1764d935c/hypothesis-6.164.0-cp311-cp311-win_amd64.whl", hash = "sha256:14633b36b646e9a2a611834ac0a26cde245440469708f59668327eb54f202692", size = 663260, upload-time = "2026-07-30T12:39:38.707Z" }, + { url = "https://files.pythonhosted.org/packages/90/91/4942fe3f2f08b920368ed5a2937346259e843e382205513b4a0e70d2de9d/hypothesis-6.164.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6bc3373fe550cf4d7cadb94ceaeb91e431e1418a96b7baa330487366eaa67d3c", size = 773152, upload-time = "2026-07-30T12:38:33.328Z" }, + { url = "https://files.pythonhosted.org/packages/eb/df/e66d052386a2b6c3e2f3eab32a02d7de3c9c59cd21d5dd58c08ecfa715f0/hypothesis-6.164.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2780297ca68929b153eff7effb2ebe67e9487d2fd9f49fa961007f8f2d236c9e", size = 764713, upload-time = "2026-07-30T12:38:48.59Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d5/5a50d14b8f04809e973c4dea884b367fef3663ff253c1205fa9e96229ef9/hypothesis-6.164.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b400bb4eb5a4a1e19cd5af3cc63817909e6b54b4603e04022bdba46860913d7", size = 1095160, upload-time = "2026-07-30T12:38:58.925Z" }, + { url = "https://files.pythonhosted.org/packages/58/01/781b19ce4382ec239c4dc6ec3bd9f195e69e5570f2814bbf04b5781ecb18/hypothesis-6.164.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fca6632933fc506dd96926d9383483e4c0066c7ff62c748d059a3276da761e7", size = 1145199, upload-time = "2026-07-30T12:39:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/e9/64/30e016863515ca01c1c738b05dd50491353d3ccae6432362e56e0c15d0da/hypothesis-6.164.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9e1f6e89e5ec34735b727f3ce41d12e7f3b8efc162c91c8a225e10b54b504b4", size = 1267980, upload-time = "2026-07-30T12:38:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/84/23/17eb8d67d59ecd3a820c905fbdf514e371dd7d01631e62a304cdd5793abe/hypothesis-6.164.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:51b0f967f608707b24ed37a298174ae6eec7899bfe3f271d1c3062c39ad66c06", size = 1312181, upload-time = "2026-07-30T12:38:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/42/69/cff9f3cd9524252adda7c8e0e129dfc176e72f64fdf0bf1552d1ea43d78d/hypothesis-6.164.0-cp312-cp312-win_amd64.whl", hash = "sha256:5770df7d518bf867a9379e9081abd9e44db1d15473430e26a0946438c08c5926", size = 660690, upload-time = "2026-07-30T12:38:28.107Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/729697380a22dc2ce8feae3c64b08bf3bd3c27e99c3706cb9bdac40c6fc8/hypothesis-6.164.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:29e7cb48974cb9fd87602e20625c890385793c6b56c18a957085a9c291f56ef8", size = 773046, upload-time = "2026-07-30T12:39:40.473Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/72374f02d90dfda198afd8aac6b1e7d1184506f97e62ebcf3d2c1e5bf761/hypothesis-6.164.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1ff8c3819345be8dd15ee6588ee9383869a54c9a3d2232cce5e26b456424135d", size = 764659, upload-time = "2026-07-30T12:38:55.896Z" }, + { url = "https://files.pythonhosted.org/packages/6e/75/fb26388915d71e5949b98ccd0c9d95edcbe6b45d0370f177d43633d81ae2/hypothesis-6.164.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33e88be13fac3ff7cb789a0b4cc43d99fb297db085f529fbb363188141c7d5bf", size = 1095078, upload-time = "2026-07-30T12:38:34.677Z" }, + { url = "https://files.pythonhosted.org/packages/be/63/f6da6e39667d39a1e44c5df82fbe6cff070c29aaffa9beb62a5322e7d8ae/hypothesis-6.164.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2e296d03a77355ce2e1c32e85a636b555edf0ddaaef277f98f1b84fe38a4595", size = 1145015, upload-time = "2026-07-30T12:39:26.487Z" }, + { url = "https://files.pythonhosted.org/packages/88/c7/55ba09727da3d9a60628c50e31e6083a36f403cb230f5e1a7bd1749a5c39/hypothesis-6.164.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53698a1b246714539dd0ecc2d556cde613d74e9f7385ec4109e0651ab2d382d6", size = 1268027, upload-time = "2026-07-30T12:38:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/4789cade332f799b0e8f2f7ea0fe2aae6157a85e60f74497e316dd17a7e3/hypothesis-6.164.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:004c92c4b869f8e258f0641101b7743cae8420436f4465383f681c086ef95c9d", size = 1311895, upload-time = "2026-07-30T12:39:14.621Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/18d85e624f8631aec42daa8a2f07c6edcedb7385b2c0f375ba8a30cbd065/hypothesis-6.164.0-cp313-cp313-win_amd64.whl", hash = "sha256:4878f81fa92a580d3e16b53e64e01a9d9fe1dca5973783558493a003138dbd36", size = 660656, upload-time = "2026-07-30T12:38:37.696Z" }, + { url = "https://files.pythonhosted.org/packages/c7/06/3c144d427799c7c72befb0bb3b199d419a89b96e1002fd8f0cc94c84ffb7/hypothesis-6.164.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9110010bdf6deb3ba9134f8ce8b683e8bb0fba108a351045c96d60c410eb6963", size = 773254, upload-time = "2026-07-30T12:38:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/74/2d/b61a10d9e70df04aa7e8f34efef8e4afe364e8995c59f894e1c35b428214/hypothesis-6.164.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df103e5d32b47d574c6e857d45361e2cba5a198d6dae4e4ee1bd248b3a2cbfa", size = 764786, upload-time = "2026-07-30T12:38:24.464Z" }, + { url = "https://files.pythonhosted.org/packages/99/68/7f80ac7bdffe78686135311c919534be411d4565c2a5ba38fd389880c553/hypothesis-6.164.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abec95020960c0ed08e5be318d2bcdde79f2c6fc7785e368a9389d31d3e802a", size = 1095578, upload-time = "2026-07-30T12:38:57.422Z" }, + { url = "https://files.pythonhosted.org/packages/45/f9/97dcbac776bcf33cb4241b52111527821f707b60a84d03d0ea670b09a134/hypothesis-6.164.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b106756cc9abd50ab1632541ea7b7223792d877a084726aa0304237d758181e", size = 1145207, upload-time = "2026-07-30T12:38:23.387Z" }, + { url = "https://files.pythonhosted.org/packages/a4/df/68184b6f71540435c895cf35ad1d67a3634a887c597ab38d3372c0d20186/hypothesis-6.164.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:11c4aab2ae6757fc4bc3bbf009487e24fd3490365817bbf40b9ec85a7e02fabb", size = 1268357, upload-time = "2026-07-30T12:38:52.946Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/6a6851dc8af89a5c0418937d38456417b2a1fc9db15c992b9cb43d53a7a3/hypothesis-6.164.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4713edecbc0969557ca135769a36d1e524c8e3b7a2b271de48d98fa29f681bf6", size = 1312183, upload-time = "2026-07-30T12:39:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/19/83adeb1f8f045bd8a1ab9822d0c3db28b337d37fff01d809fcd6e3ea70f8/hypothesis-6.164.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:e6882d316c390d33c55ec8f1675f35ab238d7c0473ccf8d235c69eaef6c621b9", size = 604771, upload-time = "2026-07-30T12:39:33.579Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/fcb48ebfbccdc5b695de175b9d1d344b3688782150f0603124bb70c0891b/hypothesis-6.164.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c3357633b38bca8c927fd90d02b39a0a3f35f24cdbcfb2fb1dcf69a3f63bd85", size = 660570, upload-time = "2026-07-30T12:39:43.898Z" }, + { url = "https://files.pythonhosted.org/packages/42/61/5857da7db0435fa69df658a9eafba62eb8a1319454005ce2a0d97f6f9e4d/hypothesis-6.164.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:53152cb549f52d661c47768d0d12a192ef26a7a9758a7f13b8ec41e8e63d6325", size = 771839, upload-time = "2026-07-30T12:38:26.842Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/22292a9dab1c544362d1759244132c7d71a9d9d5eda5d454ec735fba6bd3/hypothesis-6.164.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cee7898ad84b63da6506ae48483bb36f319a25ea4c2b1d2df47d021cc4080c24", size = 763363, upload-time = "2026-07-30T12:38:20.042Z" }, + { url = "https://files.pythonhosted.org/packages/e8/29/cc0c6e9a065a32f93fe52dde746232f007d2cabf619d4e7b1b37bd34c424/hypothesis-6.164.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0def33f0d236e54144a5218997e4492925144d4615f25fdbb4ac8e47b7b709e6", size = 1094171, upload-time = "2026-07-30T12:39:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/a7/59/37040d0776a29d4bc6d0ca9a50ca2755200007e4a8ddc27b010115b69c85/hypothesis-6.164.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:471fd80d70f2df606b1320276168bc2c6007a586124a1d81628264ccb9266f68", size = 1144089, upload-time = "2026-07-30T12:38:43.024Z" }, + { url = "https://files.pythonhosted.org/packages/fa/10/5235ed3c090a2f12fa15cc1d08e5a36cfa31bc0607c45199b0806e930ab4/hypothesis-6.164.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2eb285756aee62890fd08d6e97cf77651dfe7c093ceac094df52120a7a8dbe68", size = 1266595, upload-time = "2026-07-30T12:39:36.979Z" }, + { url = "https://files.pythonhosted.org/packages/7f/97/ffc4cee4dfdffe658e839d5f4df72ae3fa7bfea9401550b475d9700e0ee2/hypothesis-6.164.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7c5215b5568968c35c6e124e5a4a8068f80419d6171414ddf735b49e1df1ab59", size = 1310998, upload-time = "2026-07-30T12:38:45.788Z" }, + { url = "https://files.pythonhosted.org/packages/dd/08/681d4a272cd2812151581c3328e41a80a34e420d676e419a25b4b9dc2291/hypothesis-6.164.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a845e59fae87bb47a6fb84e0d5adb5679b3b55042fc3f8791da91486103cfbf0", size = 660724, upload-time = "2026-07-30T12:38:40.341Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/7f41c25a4aa977ddeb79b61df2dd70ab19986356a344ea2b4cf1fc6a85d2/hypothesis-6.164.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:eb43a07578e04c5a66d86b5a9dd6e5eb81280a8c20b14ff456dd57936fecde15", size = 772964, upload-time = "2026-07-30T12:38:41.559Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/d09a3b2af2a817e9c91769ac04ea1083f25e177b54311e8389d2d5cb2bf4/hypothesis-6.164.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c46cd09811f28c286821863565cf07679294221c40aab3c7a593685fb9c725ed", size = 768787, upload-time = "2026-07-30T12:38:29.533Z" }, + { url = "https://files.pythonhosted.org/packages/60/91/f073c582c8746efae8b7c2218129d335f13f98cd59d70c04a1ac03baa8e1/hypothesis-6.164.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c209968acd4d6992c6b8d659f27d160d1368656796781a6fe46471dd9383e4", size = 1097680, upload-time = "2026-07-30T12:39:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/45/6e/fb1a4e43975b811b40eadd55505fa58d04604e8951dfdcad53903913c8bf/hypothesis-6.164.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a28c8c86a6d3dfb4471687e7b274b6159241a1e56cbe88203521bce635c6f084", size = 1147457, upload-time = "2026-07-30T12:39:01.894Z" }, + { url = "https://files.pythonhosted.org/packages/32/47/340074ec647f799fdc1b17b2b857e8d0ac0192459980717b7d45910133c8/hypothesis-6.164.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04d2bd698fb58ec697f020ebe7b1f8779fc5bf0c910f3dc934c78542790563fa", size = 664358, upload-time = "2026-07-30T12:38:16.722Z" }, ] [[package]] @@ -1158,7 +1158,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.15.0" +version = "9.16.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -1176,7 +1176,6 @@ resolution-markers = [ ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, { name = "ipython-pygments-lexers" }, { name = "jedi" }, { name = "matplotlib-inline" }, @@ -1188,9 +1187,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/49/04360f83b4d110195751b4171b75dc1cd7b97ba122b18da34b5828172d59/ipython-9.16.0.tar.gz", hash = "sha256:d2f92587b1ef51d84f934dffe05fabb9255f0038ed0a21426f2ea761e39ad09a", size = 4515375, upload-time = "2026-07-31T08:02:51.977Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/3a/948263ca3b9d65bb2b1b0c521b3a49fad5d59ada58724bd87d2bd5ff3f36/ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e", size = 630895, upload-time = "2026-06-26T11:03:33.809Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/d30656b9eb33b8ed4e421ca55c13c7fff412086f0405bbe53c39a7ee4a3b/ipython-9.16.0-py3-none-any.whl", hash = "sha256:3d02b96de2a59074d153b1ac1c3865de738df114e430e879e6e5ef100a4d470c", size = 625973, upload-time = "2026-07-31T08:02:50.114Z" }, ] [[package]] @@ -1212,7 +1211,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "comm" }, { name = "ipython", version = "8.39.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "9.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyterlab-widgets" }, { name = "traitlets" }, { name = "widgetsnbextension" }, @@ -1573,14 +1572,14 @@ wheels = [ [[package]] name = "mypy-boto3-lambda" -version = "1.43.48" +version = "1.43.60" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/93/aba6c64a33426150f69c0dda676815294a3fd4af8d5d249d721ad9c2c24b/mypy_boto3_lambda-1.43.48.tar.gz", hash = "sha256:f783d46cf0d001eba713b02ce98a3956ff90f4a02dfe986c0dd7bac32034cb07", size = 52630, upload-time = "2026-07-14T20:32:34.727Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/77/7cebb2f0b88680cdf91eaa1c33fb88e0cbd27b4387fd352a8d7bcbc4c68d/mypy_boto3_lambda-1.43.60.tar.gz", hash = "sha256:c0f7e6d984c0fc5120966786915ef3012e9ccc9d382de412e0b71f309667851c", size = 52628, upload-time = "2026-07-30T20:11:07.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/41/e01c5b0f8a9c2ef9b549140e9ba6642b837311f11b62d80fcc8e983a6268/mypy_boto3_lambda-1.43.48-py3-none-any.whl", hash = "sha256:6461afa3125717f3cc7e358313c7c8d1ece2d07f73e5be3ab1d0524732015492", size = 61118, upload-time = "2026-07-14T20:32:32.697Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ab/712f39d92c602931176661e00259e037f439a7982df11f3ce05ea4d01069/mypy_boto3_lambda-1.43.60-py3-none-any.whl", hash = "sha256:bc097cdcb8d4a87e2685551bbaf39e583f1b603b38a6b45c784df83847819971", size = 61105, upload-time = "2026-07-30T20:11:02.711Z" }, ] [[package]] @@ -2608,55 +2607,55 @@ wheels = [ [[package]] name = "pyqwest" -version = "0.7.0" +version = "0.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/05/8576f0eeb44d3fe14c70e2bafa77ae6930c5e6f42b93c568719ea4456715/pyqwest-0.7.0.tar.gz", hash = "sha256:ac65f2243f3e814e7f4aad3f2fcfe78f89aad2de2a825eaaabf4c102f1937843", size = 457991, upload-time = "2026-07-19T05:20:06.61Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/49/7f26a2c2a3e8bbab4862bcbf4f54b706d9524fa9705cc0127553b81ed161/pyqwest-0.7.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7b2339f3b55ae179e0cb55bbd5792ae1c954d31f80de7b4dde0e95b03576b6be", size = 5159904, upload-time = "2026-07-19T05:18:57.757Z" }, - { url = "https://files.pythonhosted.org/packages/20/f1/4aba1976566936ca873e1a726a0d9bdf8195a0dc4e4f78122f6050328566/pyqwest-0.7.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2159c7e2eaf2563cdadfda17314e2b1747c30603979bf73db8daef311247db82", size = 5044476, upload-time = "2026-07-19T05:18:59.473Z" }, - { url = "https://files.pythonhosted.org/packages/fe/06/45908e5cda7fa28ba60df5ed2091056893c394c3217a796bc6577c4bd294/pyqwest-0.7.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a826f1b492a7497f469c8467bd51faf582733654bb2bc9fcdd4fa502f3e6ca1", size = 5560021, upload-time = "2026-07-19T05:19:01.097Z" }, - { url = "https://files.pythonhosted.org/packages/f2/3d/603f5c9446e8c13a4970b816246d7928e895408a7ed1778d98de3e25ea43/pyqwest-0.7.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da43c7e86bee9e74ff474d4829cd398fb9220ff0d84d633094ea6797fdfe03a1", size = 5506130, upload-time = "2026-07-19T05:19:02.812Z" }, - { url = "https://files.pythonhosted.org/packages/33/6d/2984aa02f4d0296dddb5df159e1af6c6e0ddc0b507d592f13d6ccd0b3162/pyqwest-0.7.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d1c71327c323a19dc90a0dafb1d68fb13f4f775a2498a26bf95f17165e64da9f", size = 5719961, upload-time = "2026-07-19T05:19:04.213Z" }, - { url = "https://files.pythonhosted.org/packages/19/06/e30c0d31eea17cabf99ef90c7c02e14cc94ca7d59793d397de9359148693/pyqwest-0.7.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c002e0fc31a96f1c47986299710f49ed4551e4a912a7cdb69aba6fd94db6d544", size = 5908279, upload-time = "2026-07-19T05:19:05.595Z" }, - { url = "https://files.pythonhosted.org/packages/eb/db/a1368faae1cbd094a0d179b76de69b8d1908bd45898d3bcd29ac98622072/pyqwest-0.7.0-cp310-abi3-win_amd64.whl", hash = "sha256:847e4468b5379a219b91a13dd3c92dd3b7b3d9f59af23e4c6776e663594cb241", size = 4755104, upload-time = "2026-07-19T05:19:07.772Z" }, - { url = "https://files.pythonhosted.org/packages/b3/38/c493e85ebd17d3a5c56eb53fbea36a1a6f396b999b38173ffde513576eb1/pyqwest-0.7.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4988fa1c368072886dce48f52fcbabe9f25784c6e189a9a6f2b42f6e9f383973", size = 5172962, upload-time = "2026-07-19T05:19:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/d6/68/1b340fdc7735b5682d308cabc2d65ea76c03a6cc2c30072a0c14c24f716c/pyqwest-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dfb61138c802c7317d840a274fa24f9d878301f693268e9186a9896452017a71", size = 5032604, upload-time = "2026-07-19T05:19:10.884Z" }, - { url = "https://files.pythonhosted.org/packages/4d/52/eecd858568ef6586cbdc3c419a9a0b1e8f891e2968f0f6b2a9fa9bdaac19/pyqwest-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6fdc0601590bdc547007828627082f0dc0e445e92d900dccb227e51898d7243", size = 5558302, upload-time = "2026-07-19T05:19:12.348Z" }, - { url = "https://files.pythonhosted.org/packages/c0/8f/b67d0056b18a9f99a1c9253cb983d5c4361e3e102d729df6623b71d6d939/pyqwest-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f82c971810695f5fd7962859a2469616c83b7aca6664d8f147287ee5ff430cd", size = 5501483, upload-time = "2026-07-19T05:19:13.892Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a6/69b87c4c80ced01645a143679458895dcb5c0e5ca0b3d4a43d979939cce6/pyqwest-0.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9dc2f405803b94525ab030c01cdac7093bcc1a5da6802de3345a868a0f51b3da", size = 5717334, upload-time = "2026-07-19T05:19:15.541Z" }, - { url = "https://files.pythonhosted.org/packages/26/7b/5891b72b9193b692b50ecdc4e26e7e3687f9763f5f263646bc8f997f5b62/pyqwest-0.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:537f71f97a533fa355d02c15ed9f0cc05fb8915996cb921caa87fe7310b457ee", size = 5902447, upload-time = "2026-07-19T05:19:17.273Z" }, - { url = "https://files.pythonhosted.org/packages/6a/80/1b280618af3f9d36081449d153efc5620f0eea93ac169574fc69208d2b91/pyqwest-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:196aa73fb7eee4b6c052d6f4172a4321904a7208331f4322ccee3ee7d0adbc78", size = 4750469, upload-time = "2026-07-19T05:19:18.916Z" }, - { url = "https://files.pythonhosted.org/packages/c1/9c/64c8df83a152804ac3074fb879c1229c3aca55a12dc33dc73c72e93405e6/pyqwest-0.7.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:72d001892ed570df1dcc489ef8b0a03bc7abd4fbdca10625c358a87e66b226ad", size = 5171308, upload-time = "2026-07-19T05:19:20.332Z" }, - { url = "https://files.pythonhosted.org/packages/14/d1/46e629541a3f7231dd978fa9939e6ddcd075238880395bcfb16544769165/pyqwest-0.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:03da0797bac5c2eb1a40f02afccd4b5aad5ad0d375bb993df2f5e0c692fc0c6c", size = 5032449, upload-time = "2026-07-19T05:19:21.967Z" }, - { url = "https://files.pythonhosted.org/packages/44/3c/132194c747696e41ad5745c51587bd20057d800b6a64fb901ce2ac990149/pyqwest-0.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:837e18aef4490eed25b4194d49f16732ccf1abf2cdd0845e75d75d2a4428b98c", size = 5556847, upload-time = "2026-07-19T05:19:23.589Z" }, - { url = "https://files.pythonhosted.org/packages/e6/5d/6726a70e89c60bf3cd30d5d918eff68dc1d1ceb9c911331d7fb57977bfc3/pyqwest-0.7.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:418e8c3a67ca6226bfb6147b8668203c74d9c872657117086f4c264a674d7980", size = 5500333, upload-time = "2026-07-19T05:19:25.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/3f/329df64d3e8778bc311a22e432280cd317619c3f9b7e414f375127bb90b0/pyqwest-0.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31176abbbcbe6d03740967b5c0241deaa60b328792644cb2e317a34d6b076433", size = 5717248, upload-time = "2026-07-19T05:19:26.963Z" }, - { url = "https://files.pythonhosted.org/packages/2e/93/6670afb97c6ce7b5ba27981e023d9edc7413e509bbe18afebb729834d9e6/pyqwest-0.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e21102f9c13234a0066d42bb5429dc5c311d7fd99401878837d9675a7f6e03b9", size = 5902814, upload-time = "2026-07-19T05:19:28.637Z" }, - { url = "https://files.pythonhosted.org/packages/51/d3/bf9440a03c97f408743313215d8c7e9875b0b2b0d2ef97f5fbb5066cd57d/pyqwest-0.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:ca19e96b5e902a6d35e18cee7f5e90612fca6ab169378d42242cdcb638578cee", size = 4750510, upload-time = "2026-07-19T05:19:30.587Z" }, - { url = "https://files.pythonhosted.org/packages/dc/25/8b0919bf504309982857e564e3035ffd799d7aaa23457eb351485573e5ee/pyqwest-0.7.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4e4e79187c3e4ebd07d663d8dc7a7cba865c72020332e41807426a7e3526fda0", size = 5177972, upload-time = "2026-07-19T05:19:32.051Z" }, - { url = "https://files.pythonhosted.org/packages/52/12/d652213fe336c52b75fb4df710f5cff08f8a1dcc9e385c9120f18476f5c9/pyqwest-0.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc2c569481ade1bc0b89b07daf1b49b20a1337a53207ffe0ef325260f509404", size = 5032608, upload-time = "2026-07-19T05:19:33.772Z" }, - { url = "https://files.pythonhosted.org/packages/82/b0/406c91563140b87ade5c5935410997b26449b8eb2f91511bc52741ae38fb/pyqwest-0.7.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef1bfbdca9ec9c8a7b223268f5ef8d45694da7226929454b0cb40f2e3d1ddd5", size = 5558937, upload-time = "2026-07-19T05:19:35.174Z" }, - { url = "https://files.pythonhosted.org/packages/7f/17/a0a09936b5dd5d9d7517289aee3683bbf6554b0853bce5280ead9da27336/pyqwest-0.7.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c2f6a6830becfa1c5bb94aa169ad854da5749a95a440aee759932d5965c213", size = 5500603, upload-time = "2026-07-19T05:19:37.023Z" }, - { url = "https://files.pythonhosted.org/packages/00/34/ccec52ca9f14fbe11292fadbe89c771353e0f46d58aa88d8e93d6e018117/pyqwest-0.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0d11128ccc382f64fcdf92d3bce6be7191489c1b3a9a3f0dbdf89e55451638de", size = 5719784, upload-time = "2026-07-19T05:19:38.649Z" }, - { url = "https://files.pythonhosted.org/packages/16/cc/58dca466c236e497cbb2e75b97a2489225d21f5063d932ab7d1723294112/pyqwest-0.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5832373c7bcccfbc3bb79c44b592e0871631139a2ef5aef771b8fe2b7bd4301d", size = 5901396, upload-time = "2026-07-19T05:19:40.402Z" }, - { url = "https://files.pythonhosted.org/packages/06/a0/983ea3b859828bef8372896e9c407f62bf426b84526d0b0cf9fd9abce411/pyqwest-0.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:7330070c4497e564007985716ac347cb9a7500eba5c9610500653a2ce4cfe86e", size = 4751036, upload-time = "2026-07-19T05:19:42.159Z" }, - { url = "https://files.pythonhosted.org/packages/92/1d/e2fe3dfe6d87989017412f394abc45435a3af2526e998c83cf836937b23c/pyqwest-0.7.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:02fd606a35be7803a770071f642a375e55df4c2025e49b10b859430f424c4492", size = 5165801, upload-time = "2026-07-19T05:19:43.82Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fe/90126ac71fe09c729f36c166cb71b5148dd8a80e47f6f232bf71494604a2/pyqwest-0.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a807835c6a0777f0cc57c321c78c7bf162f6354698844d78db6e03ba9edd83dc", size = 5025128, upload-time = "2026-07-19T05:19:45.391Z" }, - { url = "https://files.pythonhosted.org/packages/e5/15/ee84ce834705d613c0b113fb4cf9d274011cfcc6c6ededb52d92a38a2367/pyqwest-0.7.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86d84871cb0a572700dece3a6c45c294721f655f3d0b85477333209b487ecef1", size = 5551017, upload-time = "2026-07-19T05:19:47.046Z" }, - { url = "https://files.pythonhosted.org/packages/41/ad/18b4540fb276f9b540018efe024aca0801acfe4209417d4faed85bccdb04/pyqwest-0.7.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:005ddd2a777d30ca7ce4de12df1336760e14d46394ab56299a9e81626d78b991", size = 5493032, upload-time = "2026-07-19T05:19:49.038Z" }, - { url = "https://files.pythonhosted.org/packages/6c/91/f25477eb80c375378a876cf2bac80c55ec9ea36b94bd7d4b2b2931860c85/pyqwest-0.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca02089249c292ab9c148fa86c06927701897090226a13e392a6ec53312380ea", size = 5712503, upload-time = "2026-07-19T05:19:50.542Z" }, - { url = "https://files.pythonhosted.org/packages/f4/63/c5be988c55c69c87de950e3dabab322f82a78dc7c2ebdf89af32626bd473/pyqwest-0.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:da3bc117c1380e9577994da15b8236f7c3bb400111d6be4b57a9b2e4f30c9a79", size = 5893782, upload-time = "2026-07-19T05:19:52.286Z" }, - { url = "https://files.pythonhosted.org/packages/82/a7/96144e6db9a49eb5e42734562ac5d387c2b78fe1142674d3a284ce188ef7/pyqwest-0.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a599ddac7ded32ed62d15ca90bc9c77652ba34b225cf17a404821cec92a189fa", size = 4744187, upload-time = "2026-07-19T05:19:53.921Z" }, - { url = "https://files.pythonhosted.org/packages/ed/42/26cf547b2e43078b4663e717715a289f31fc873d65d45cf354f364faa744/pyqwest-0.7.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bebc51e3c9c6339d81964c6e60516a5f5c9fe793c82da57ef7c2d87a55741829", size = 5170766, upload-time = "2026-07-19T05:19:55.472Z" }, - { url = "https://files.pythonhosted.org/packages/dc/bc/1aac42f34bd3e5ac4c13a2c267dba6bda4bdea594ff96c7851dff930c8f1/pyqwest-0.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:37623b492aea7ccd3b6cfe3179110643d28c2cdc83c765aac3d207b4321995d2", size = 5053361, upload-time = "2026-07-19T05:19:56.973Z" }, - { url = "https://files.pythonhosted.org/packages/0d/59/e3d8cfd2c7be0e12773ee4f767efc03be50ccdd28c6155d0b176bf1d8a9a/pyqwest-0.7.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02ff96a7746de208a4b8c78abe780c413d66576146a4ec64efd348b0c8a8328c", size = 5573900, upload-time = "2026-07-19T05:19:58.536Z" }, - { url = "https://files.pythonhosted.org/packages/39/a4/7dd4d548be6d54b195d737164dc67c39d9c5df40aa76a873fe1d2d9b4426/pyqwest-0.7.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2e9bfa81198db7023202962a94920986a9a3dd12fe2f9d310f0cfcdddc092c", size = 5510692, upload-time = "2026-07-19T05:20:00.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/13/c070baf81da8653803328b632baddb5647b66cc7c6353c58f024ca82277c/pyqwest-0.7.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e1548708dbeb29a60db199c70b9e93733d6b334e7efa1dfa5e3846a4f890db6d", size = 5735516, upload-time = "2026-07-19T05:20:01.487Z" }, - { url = "https://files.pythonhosted.org/packages/84/46/e0f2a02ebf504db6bdf4bf343f0d3362eccf8dd5a325382fab1305fa7022/pyqwest-0.7.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4bb3cf0975ee829b6c76e1c42f01863122ce71b1a952e507e260a9d29eb9b183", size = 5916743, upload-time = "2026-07-19T05:20:03.132Z" }, - { url = "https://files.pythonhosted.org/packages/07/7c/4a35df79a142e5fcb5f46fce2d1d70dd72325a65db4139bbba4fbed0d9b9/pyqwest-0.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2ea1ce4d630c92cb2cc6b3bf91ada0053b051dfd5c0662d89957ebd829cfdcce", size = 4759967, upload-time = "2026-07-19T05:20:04.837Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/25/305acffe35817ac5dd3659f06bf5c7522cf2d6b1cbfb6e71dfd4e7cf28b0/pyqwest-0.8.0.tar.gz", hash = "sha256:138a6a01f5e3bdd92abed412f35bf122b5a7c41cadaffe9c8b3ba0dc2dc5d822", size = 469648, upload-time = "2026-07-31T03:33:42.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/38/76280a079b0f84dcc05c14eba16b6cd39bef9e8aa0a51e916abf71fea637/pyqwest-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2976d8fc93b7b4fdb9bfc7ad231e6584d81b9777ad88c8256e02cfa8b690364d", size = 5216354, upload-time = "2026-07-31T03:32:16.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/afde50b6814e6c053237fb6182f3ffcbe6562fdbfd0c44b6b6589cb37ea7/pyqwest-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:afe8a86b28ef637b85d2239758f91ef3acdb01bd569fb8abcb4ec70b368fd2d7", size = 5095245, upload-time = "2026-07-31T03:32:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/d9/18/0b0d177754b337afaf9b7d7b0de476ae250bab3bbf105568211fa83da2d6/pyqwest-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d948f99dc80e1d4879bbfb4ff073e1d7566dc307d483f6857907b9b634631d", size = 5614817, upload-time = "2026-07-31T03:32:21.005Z" }, + { url = "https://files.pythonhosted.org/packages/dc/12/d5b2e201ca3c374174644fbf201329ff6635ebabef0fcc49701f8d168984/pyqwest-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bcd1edaa837e136af6982c31c17100a16b6e4918e7237a34229928754edae35", size = 5557336, upload-time = "2026-07-31T03:32:22.725Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/d5ba098c323d7f5eb9cb9ebf075060d14c3364bf176d9815655065462bc7/pyqwest-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c25504a5fe4d72f43b17253c9e1dc4cda426e9fa0a72474c4555455f63706c1", size = 5775102, upload-time = "2026-07-31T03:32:24.766Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e5/1b1bae527e4de93b9eddbf15a78f1ce5198aece5c169478e385364e0f1f1/pyqwest-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:513fe7649d727cccb5d6e04999e91fed5f61f5e20a8556a2f96d3d1aeea02001", size = 5963251, upload-time = "2026-07-31T03:32:26.792Z" }, + { url = "https://files.pythonhosted.org/packages/44/14/a9d883fc59df1f34fe42764277ca26de11d74dd3aa3d9f9f4a35d7414f37/pyqwest-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:7896646bd426db0cbd7a5d4ebd0dacd59f1a2c3cdc12fb2667234e9504e33b03", size = 4834356, upload-time = "2026-07-31T03:32:28.921Z" }, + { url = "https://files.pythonhosted.org/packages/2e/49/c0640e19718c4533590bcaba647ad3145f793c66c125cb62c399e580b1a6/pyqwest-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:c4970ad95d4d51aa07980687d07e1f4003b68bc1fa1e46c9202f1b008c95662e", size = 5232611, upload-time = "2026-07-31T03:32:30.778Z" }, + { url = "https://files.pythonhosted.org/packages/50/85/25c440e1dc31320afa9de8a3ab43b0bc6f5ef5090e43fac350b5de4db623/pyqwest-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:63f341cf695b9c8b9b184a0f1c67f6e578b833502f1f21a9359f031b5b77a627", size = 5089482, upload-time = "2026-07-31T03:32:32.817Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7c/a5c49001dc0685043512c5497a7820fe091d765f93a730ace81708ed8f5e/pyqwest-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed43c61eebe85aa078ff922378aac9904c9d0be06c99da5826c865c28f8468dc", size = 5618803, upload-time = "2026-07-31T03:32:34.801Z" }, + { url = "https://files.pythonhosted.org/packages/39/af/6835aaeb451f629886bae0b924a864b73b906c84944be37fea43298c163a/pyqwest-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7988aeb1dce1ee0d3dbcf9d6c8d3a2a821d9c56402c95c057567363ed01116c0", size = 5556779, upload-time = "2026-07-31T03:32:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/ea/66/2f130d7dc3341112f1a620984d4af17cacf2a17085a1eaa98dfaa35da028/pyqwest-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37ebb0bfeaa5a6b5c6ebd1cfdbb3e4d89ac343e46be07a993e8af6473ee82b1b", size = 5779456, upload-time = "2026-07-31T03:32:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/14a30458a69e6f1b8e007ff6bd6fdf867cc28de45d046374f45087d31fd8/pyqwest-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4471266ff506d02f21350921abd0adbaa08d6b7db6d24408c2ed9635e187e263", size = 5962595, upload-time = "2026-07-31T03:32:41.02Z" }, + { url = "https://files.pythonhosted.org/packages/05/22/db95030afe6c5adc90b2864e194d8074175675a90703282286e3370ecd53/pyqwest-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:5b605cb927b5bbd0b1247a5439d90068fee84802003bd6609c48e985aeb15570", size = 4824467, upload-time = "2026-07-31T03:32:42.733Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/96900a0797bf6fcf281de0f8e9cd612e13df983cc5f9ce7aa2bd21d1ecab/pyqwest-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa840884e19a5777159d99c9bcc38b2f0f536484dfa0edbc4041a50134afc6e5", size = 5230908, upload-time = "2026-07-31T03:32:44.706Z" }, + { url = "https://files.pythonhosted.org/packages/ae/be/55f23ecbf28c247b856caec896c5ed301ccb9e4f96e3aaa193174182e032/pyqwest-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1aabb09561a75e16d38c83d341fa3ec1c5009a7f91f300f620493e885ac1fb25", size = 5088999, upload-time = "2026-07-31T03:32:46.632Z" }, + { url = "https://files.pythonhosted.org/packages/0d/32/0c05267a312102f8840e1e08350382f6f46be7304d84e6cb9b19317e107f/pyqwest-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6db97734f909d865115e45c213a983a3f0a0b67697fe2937779df92a07827aaa", size = 5618462, upload-time = "2026-07-31T03:32:48.71Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e9/c36af7b2b34364fe6abc0737f8ae47bed87377c87e2a035e0429a716477a/pyqwest-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44c266a84125017b09c89199d6355c6eeffaf381bf46670c49ec46353d9e5b0", size = 5555364, upload-time = "2026-07-31T03:32:50.588Z" }, + { url = "https://files.pythonhosted.org/packages/44/18/da3f1469ba6866a695c510a287debf30034d84cc93f8cab021bf930834fb/pyqwest-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:29cd8eca7aafa14b6ef164c4360bad0db155c68ac16bc907bbadc9a414ea4b5b", size = 5777961, upload-time = "2026-07-31T03:32:52.58Z" }, + { url = "https://files.pythonhosted.org/packages/bd/89/56c3543dd06d7ef19d54c4d698455ed9f32cc779b42b896d46149a6f73f1/pyqwest-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c2d8f4057b8bd3c540c4d0b596b8110e1d49d7a06be46de6dd0db864e2238b4b", size = 5962311, upload-time = "2026-07-31T03:32:54.747Z" }, + { url = "https://files.pythonhosted.org/packages/fc/95/82a428e536b1bc1fe655f22ade122f55c476c3d06ee6c8223fe32de124ba/pyqwest-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:568be49f10da2bdc97482093f713f36b6f3b7b4392977044c942b18b32eebceb", size = 4824529, upload-time = "2026-07-31T03:32:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e3/64b5bc3152c8201864b09d398843ed4a4fdf5e66eb951ca0d4a4eddc6f13/pyqwest-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8b33afc7289534488968b3ee95fc452724cb20c0b9dc64bd432f9eadd7f8e86b", size = 5232837, upload-time = "2026-07-31T03:32:58.857Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/071af07d1d74910a9efb5c6a9e93823534aac8f56f446b8984a42660ee58/pyqwest-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:51195116f455d027348a53a04af0463b6657ebc514a311c5952df0134de14ea8", size = 5087870, upload-time = "2026-07-31T03:33:00.971Z" }, + { url = "https://files.pythonhosted.org/packages/70/d9/75f20faab72384780946107de99f1f5a98b5486462861defe4050bad7b03/pyqwest-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd49b8955cf167efa828052c0a08a00fa29b45f712d60f75979187cf0a7342d", size = 5620684, upload-time = "2026-07-31T03:33:03.082Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/150e91e8d58ae22522b77de8a3a28295b484e7676ce689c33f2182db10f0/pyqwest-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dded7f50f96e53a978784db7ceab4a7e7ff7529fc94b804603e0a3cfd5915162", size = 5557460, upload-time = "2026-07-31T03:33:05.161Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/3b60ffa85a2d4ca1218d40fc00d60ff32dc0777a01e1299376ebdc76f48c/pyqwest-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c27059c54a5ead5922f4599e31b9c727cf0bf4343be813fc64695a4b7bf24dec", size = 5780272, upload-time = "2026-07-31T03:33:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/75/1c/ae5377a9cf7e0da2389b76d092a67cfd0e0ee58d6dd0ecc9eee2d1066a5c/pyqwest-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f0a713a3414f50123aab649bd2591dac0264b150778f10934a28c5e11d27f2", size = 5964492, upload-time = "2026-07-31T03:33:09.7Z" }, + { url = "https://files.pythonhosted.org/packages/f1/f0/43f650ea9c68c6a4251c6e0d5ae02f7aa4e7bd35d7ea6c5ebbb15f9af2d4/pyqwest-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:6b076960044609de8611961609b3ed00ed8dedf1504d957d7c3f354e64ed6ce9", size = 4822044, upload-time = "2026-07-31T03:33:11.863Z" }, + { url = "https://files.pythonhosted.org/packages/01/c6/208138925d0e25f1c4cf4d8b5949ed590ecc71098cb1968152627a0f91fe/pyqwest-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:778ce6f447d481913c787a2ee8d8623ac0b1fe673281c686f6b99e8689815857", size = 5217325, upload-time = "2026-07-31T03:33:13.932Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/8b8dd86ee0b96672c2338816957c285b71db08a90848db68c8b937c9c0db/pyqwest-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:19d58b0f0ca2d21832c26aa096954283b57404515bf196df5b2ec894111ccc1a", size = 5076014, upload-time = "2026-07-31T03:33:15.805Z" }, + { url = "https://files.pythonhosted.org/packages/44/7f/738077c5fd9229d61965faa946c2f54ddf90e7f910162fca2f07763107c8/pyqwest-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47de00287d20233782e391083fa197e89bb531b194131520dd9ec2641b57777", size = 5602382, upload-time = "2026-07-31T03:33:17.924Z" }, + { url = "https://files.pythonhosted.org/packages/88/df/b95b88e31593689d7b3d4a6e2f5550cd686a8648f50b70ec27a2bdbe41a3/pyqwest-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9445cccde16f4a1ef0409a42677ccbd8350cdde0a407bb3db6353c0f921fa812", size = 5547598, upload-time = "2026-07-31T03:33:19.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/26105b8c6209dc13b0339ed2e5d3425476643619595f4d9e74acec9279ed/pyqwest-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:18f9fa7543070d31a334970b979bdc58acfde4b163fae8f012dac9cc7122632f", size = 5765677, upload-time = "2026-07-31T03:33:21.931Z" }, + { url = "https://files.pythonhosted.org/packages/54/07/d14c467eddd3d89b561b6891df15af2ace5f20f92e1fc7dfc4d33068f627/pyqwest-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ace2147bd302c13997a0baec3eca5a2176aeb78ba1a019564bd78baa72554cf3", size = 5951710, upload-time = "2026-07-31T03:33:24.263Z" }, + { url = "https://files.pythonhosted.org/packages/da/91/fa6805b5d0a85ae8f1633b90c0a1c4c81e8ccc803e4c6ace68f04012a29b/pyqwest-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ed4b17453cbfe5bfaf9afb646fa4900621f958d9fb488b356b5a819d61b5cfe1", size = 4816517, upload-time = "2026-07-31T03:33:26.371Z" }, + { url = "https://files.pythonhosted.org/packages/55/d5/b047c6fcce7536018169d33e15ad927af9d8581f3f01725303cabaf099ac/pyqwest-0.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b93263e0fdc8c8e33bfcb55f1a678acf8cc2d6257d6ef4c4785f0fe32f62708d", size = 5217204, upload-time = "2026-07-31T03:33:28.445Z" }, + { url = "https://files.pythonhosted.org/packages/96/ee/ccdb687682c7b969fb2ce4a87e35e8bb99a75d82c6276c8c62f87de39cc0/pyqwest-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f7fb0e669bd0e5b024d27ed98799c2dc19fd0eb4cdde8b508e887bc0708fa51", size = 5096201, upload-time = "2026-07-31T03:33:30.364Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/7ef06ad3f5d2f23891e7ceea25132ff9d297c1a079fe420b05db13cd49c8/pyqwest-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b03c1159c0ae2b45870d3009c79942f70a01e7122810d0eb7a4273f90cd36410", size = 5620096, upload-time = "2026-07-31T03:33:32.3Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1a/ff28a9af929aec5726448b8ddceffa95c0f4be5fd18d16fe44d8f15e0847/pyqwest-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14423de4e06014bb8549405add86dca1e866b5cd9c335790fcff9eee9263ece3", size = 5561862, upload-time = "2026-07-31T03:33:34.523Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7c/303f32fabcf261afb3a118a95ecb8e68e5be060e9216b1d3808bf9fd1512/pyqwest-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:91793f0b180f38d0d5eec063b4f791104ee51cd6515791325e644b3b1a02f418", size = 5782390, upload-time = "2026-07-31T03:33:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/c7/40/556b7a947bb93ff33787c3d240c7b9d5f107f066663a681329cad4ce2b01/pyqwest-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1389bbc67290e848413b9900ab358efae3991a35dc6be5169f65e507e8d4d77c", size = 5970151, upload-time = "2026-07-31T03:33:38.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/72/a12b6109c3c274a5a680f96f4b6b108dba94958075116bca4d3d2ace2eab/pyqwest-0.8.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bf5caf0e25a26145e08643be9b65831c0a2c9bf6a19d1e6c9b1ad5a2c06cf3e9", size = 4825893, upload-time = "2026-07-31T03:33:40.887Z" }, ] [[package]] @@ -2906,27 +2905,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, - { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, - { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, - { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, - { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, - { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, - { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, - { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, - { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, - { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, - { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" }, + { url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" }, + { url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" }, + { url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" }, + { url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/fb22af2fd78a736e241fabf67e30ce1799a64244026377a49e133af90762/ruff-0.16.1-py3-none-win32.whl", hash = "sha256:d31765e131295b8445caf301e3e8a85b34d1b9b211b4109b7ba457888b051806", size = 10838258, upload-time = "2026-07-30T19:36:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/35/e57fd9fb5d423961df087a00b12d42c0a830288dc2f3b45ecca299158b4f/ruff-0.16.1-py3-none-win_amd64.whl", hash = "sha256:09b05e8b90c2cb06ad63464350e7a45e8e44a2dfe52072ebfba6666ca8d3f596", size = 11961111, upload-time = "2026-07-30T19:36:56.107Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/240ea004bf6dc4feb40e9832f2205a476a47dd5b8a3f8211a5fc5f95e20e/ruff-0.16.1-py3-none-win_arm64.whl", hash = "sha256:dbaadaac38c70239f056d306b7476f246b0bf000fa6b3876402acbf5b227eaf8", size = 11309414, upload-time = "2026-07-30T19:36:58.79Z" }, ] [[package]] @@ -3361,11 +3360,11 @@ wheels = [ [[package]] name = "traitlets" -version = "5.15.1" +version = "5.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/a9/a2584b8313b89f94869ddb3c4074617a691de1812a614d2d50e32ca5a7a6/traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722", size = 163344, upload-time = "2026-06-03T12:26:06.181Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/a1/d7e7d9f461575d8bb77e3c3bd78a6cdfdd2bb4a06bfbbb8a0e1f51ab7bc2/traitlets-5.16.0.tar.gz", hash = "sha256:7de0a3fabaf5971ff15c8905545f9febfa850309fb8e86e1b42bdb5b46b293ed", size = 165946, upload-time = "2026-07-31T12:23:49.785Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/01/bd/f8607e908605262e4926cbfd2560094bc5d04ef7f8aff1340e7fff503016/traitlets-5.16.0-py3-none-any.whl", hash = "sha256:94a9967ba45e89e837cf9934029c8d019bea9149cfffa115ed8c1900f679beba", size = 86093, upload-time = "2026-07-31T12:23:47.533Z" }, ] [[package]] From 3a2917ae779740435088cb46bd0de195ff450d96 Mon Sep 17 00:00:00 2001 From: Lukas Bindreiter Date: Fri, 31 Jul 2026 15:54:16 +0200 Subject: [PATCH 7/7] Fix tests --- tilebox-datasets/tilebox/datasets/assets/assets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tilebox-datasets/tilebox/datasets/assets/assets.py b/tilebox-datasets/tilebox/datasets/assets/assets.py index 8ec1cf0..d8f0926 100644 --- a/tilebox-datasets/tilebox/datasets/assets/assets.py +++ b/tilebox-datasets/tilebox/datasets/assets/assets.py @@ -2,6 +2,7 @@ from collections.abc import Iterable, Iterator, Mapping from dataclasses import dataclass, is_dataclass, replace +from dataclasses import field as dataclass_field from dataclasses import fields as dataclass_fields from enum import Enum from math import isnan @@ -202,8 +203,8 @@ class AssetLocation: """ href: str - storage_schemes: Mapping[str, StorageScheme] = _EMPTY_MAPPING - authentication_schemes: Mapping[str, AuthenticationScheme] = _EMPTY_MAPPING + storage_schemes: Mapping[str, StorageScheme] = dataclass_field(default_factory=lambda: _EMPTY_MAPPING) + authentication_schemes: Mapping[str, AuthenticationScheme] = dataclass_field(default_factory=lambda: _EMPTY_MAPPING) alternate_name: str | None = None @@ -240,7 +241,7 @@ class Asset: key: str primary: AssetLocation - alternates: Mapping[str, AssetLocation] = _EMPTY_MAPPING + alternates: Mapping[str, AssetLocation] = dataclass_field(default_factory=lambda: _EMPTY_MAPPING) media_type: MediaType | str | None = None title: str | None = None description: str | None = None