diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d503d7..3e1ebf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.66.0] - 2026-10-14 + +### Added + +- `tilebox-datasets`: Support STAC classification bitfields on assets and bands, including inheritance, + shared metadata lifting, and protobuf round trips. +- `tilebox-workflows`: Add workflow summaries and optional presentation metadata. +- `tilebox-storage`: Read Planetary Computer assets with automatic, lazily fetched Azure SAS credentials. + Reuse container-scoped credentials across assets and refresh them on demand without changing asset URLs. +- `tilebox-storage`: Support Azure `abfs` and `abfss` asset locations alongside `az`, `azure`, and Azure HTTPS locations. + +### Changed + +- Update generated protobuf code to the latest API. +- `tilebox-workflows`: Change workflow creation to `create(name, summary, description="")`, with a required summary. + Change updates to `update(workflow_or_slug, name=None, summary=None, description=None)`. + The positional order is now name, summary, description. For summary and description updates, + `None` leaves the value unchanged and `""` clears it. +- `tilebox-storage`: Require explicitly configured credential providers through `Client(s3_credentials=...)` + for S3-authenticated asset locations. + Asset locations without authentication metadata use unsigned cloud requests, including Azure. +- `tilebox-storage`: Reuse asset stores by backend, namespace, effective options, and credential identity, + rather than authentication descriptions or other unused metadata. +- `tilebox-storage`: Require obstore 0.11.1 or newer and add Tenacity for bounded asynchronous signing retries. + +### Fixed + +- `tilebox-storage`: Preserve HTTP query parameters in request URLs instead of encoding them into object keys. +- `tilebox-storage`: Filter unsupported storage, authentication, and signing-flow combinations before checking + for ambiguous asset access. Reject authentication schemes that are incompatible with the selected backend. + ## [0.65.0] - 2026-10-09 ### Added diff --git a/tilebox-datasets/tests/test_assets.py b/tilebox-datasets/tests/test_assets.py index 1131e81..b2ed728 100644 --- a/tilebox-datasets/tests/test_assets.py +++ b/tilebox-datasets/tests/test_assets.py @@ -13,6 +13,8 @@ MediaType, ) from tilebox.datasets.datasets.stac.v1.asset_metadata_pb import ( + ClassificationBitField, + ClassificationClass, EOCommonName, EOProperties, RasterProperties, @@ -28,6 +30,7 @@ ) 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.asset_pb2 import Assets as AssetsWire from tilebox.datasets.datasets.stac.v1.authentication_pb import ( Authentication, AuthenticationFlow, @@ -318,6 +321,60 @@ def test_band_compilation_lifts_metadata_and_interns_profiles() -> None: assert decoded["red-b"] == assets["red-b"] +@pytest.mark.parametrize("placement", ["asset", "inherited", "shared", "override", "distinct"]) +def test_classification_bitfields_round_trip(placement: str) -> None: + cloud = ClassificationBitField( + offset=0, + length=2, + name="cloud", + description="Cloud confidence", + roles=["cloud", "quality"], + classes=[ClassificationClass(value=0, name="clear"), ClassificationClass(value=3, name="high")], + ) + snow = ClassificationBitField( + offset=5, + length=1, + name="snow", + classes=[ClassificationClass(value=0, name="absent"), ClassificationClass(value=1, name="present")], + ) + defaults = (cloud, snow) + asset_bitfields = defaults if placement in {"asset", "inherited", "override"} else () + bands = { + "asset": (), + "inherited": (Band(name="qa"), Band(name="qa2")), + "shared": (Band(name="qa", bitfields=defaults), Band(name="qa2", bitfields=defaults)), + "override": (Band(name="qa"), Band(name="qa2", bitfields=(snow,))), + "distinct": (Band(name="qa", bitfields=(cloud,)), Band(name="qa", bitfields=(snow,))), + }[placement] + expected_bands = { + "asset": (), + "inherited": (defaults, defaults), + "shared": (defaults, defaults), + "override": (defaults, (snow,)), + "distinct": ((cloud,), (snow,)), + }[placement] + expected_asset = () if placement == "distinct" else defaults + authored = AssetCollection.from_assets( + [Asset(key="qa", primary=AssetLocation("https://example.com/qa.tif"), bitfields=asset_bitfields, bands=bands)] + ) + assert tuple(band.bitfields for band in authored["qa"].bands) == expected_bands + + compiled = authored.to_fields()["assets"] + assert tuple(compiled.assets[0].bitfields) == expected_asset + sparse_bitfields = [compiled.band_profiles[index].bitfields for index in compiled.assets[0].band_profile_indices] + assert sparse_bitfields == [list(bits) if bits != expected_asset else [] for bits in expected_bands] + + wire = AssetsWire.FromString(compiled.to_binary()) + if placement != "distinct": + assert wire.assets[0].bitfields[0].HasField("offset") + assert wire.assets[0].bitfields[0].offset == 0 + assert wire.assets[0].bitfields[0].classes[0].HasField("value") + decoded = AssetCollection.from_datapoint(_scalar(assets=Assets.from_binary(wire.SerializeToString()))) + assert decoded["qa"].bitfields == expected_asset + assert tuple(band.bitfields for band in decoded["qa"].bands) == expected_bands + assert decoded == authored + + def test_sar_metadata_is_lifted_field_by_field_and_round_trips() -> None: def sar_properties(resolution_range: float | None = None) -> SARProperties: return SARProperties( diff --git a/tilebox-datasets/tilebox/datasets/assets/assets.py b/tilebox-datasets/tilebox/datasets/assets/assets.py index c1304dc..9990428 100644 --- a/tilebox-datasets/tilebox/datasets/assets/assets.py +++ b/tilebox-datasets/tilebox/datasets/assets/assets.py @@ -14,6 +14,7 @@ from protobuf import Message from tilebox.datasets.datasets.stac.v1.asset_metadata_pb import ( + ClassificationBitField, ClassificationClass, EOCommonName, EOProperties, @@ -170,6 +171,7 @@ class Band: raster: Generated Raster extension metadata. classes: Generated Classification extension classes. sar: Generated Synthetic-Aperture Radar metadata. + bitfields: Generated Classification extension bitfields, or empty to inherit them. """ name: str | None = None @@ -181,6 +183,7 @@ class Band: raster: RasterProperties | None = None classes: tuple[ClassificationClass, ...] = () sar: SARProperties | None = None + bitfields: tuple[ClassificationBitField, ...] = () @dataclass(frozen=True, slots=True) @@ -231,6 +234,7 @@ class Asset: sar: Generated Synthetic-Aperture Radar metadata. satellite: Generated Satellite extension metadata. product: Generated Product extension metadata. + bitfields: Generated Classification extension bitfields. """ key: str @@ -255,6 +259,7 @@ class Asset: sar: SARProperties | None = None satellite: SatelliteProperties | None = None product: ProductProperties | None = None + bitfields: tuple[ClassificationBitField, ...] = () @dataclass(frozen=True, slots=True) @@ -711,6 +716,7 @@ def _compile_assets( sar=asset.sar, satellite=asset.satellite, product=asset.product, + bitfields=list(asset.bitfields), ) ) band_profiles = sorted(unsorted_band_profiles.values(), key=_band_profile_sort_key) @@ -801,6 +807,7 @@ def _compile_band_profile(band: Band, asset: Asset) -> ProtoBand: 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), + bitfields=[] if _semantic_equal(band.bitfields, asset.bitfields) else list(band.bitfields), ) @@ -816,6 +823,7 @@ def _effective_band(band: Band, asset: Asset) -> Band: raster=_inherit_raster(band.raster, asset.raster), classes=band.classes or asset.classes, sar=_inherit_sar(band.sar, asset.sar), + bitfields=band.bitfields or asset.bitfields, ) @@ -835,6 +843,7 @@ def _normalize_asset(asset: Asset) -> Asset: 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) + common_bitfields = _common(band.bitfields for band in bands) return replace( asset, roles=roles, @@ -845,6 +854,7 @@ def _normalize_asset(asset: Asset) -> Asset: raster=_lift_raster(bands, asset.raster), classes=asset.classes if common_classes is _NO_COMMON else common_classes, sar=_lift_sar(bands, asset.sar), + bitfields=asset.bitfields if common_bitfields is _NO_COMMON else common_bitfields, ) @@ -1154,6 +1164,7 @@ def _resolve_band(source: ProtoBand, asset: Asset) -> Band: raster=_inherit_raster(source.raster, asset.raster), classes=tuple(source.classes or asset.classes), sar=_inherit_sar(source.sar, asset.sar), + bitfields=tuple(source.bitfields or asset.bitfields), ) @@ -1224,6 +1235,7 @@ def _resolve_assets( sar=source.sar, satellite=source.satellite, product=source.product, + bitfields=tuple(source.bitfields), ) bands = [] for index in source.band_profile_indices: 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 index 55589d1..5c8520f 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb.py @@ -121,7 +121,7 @@ def __init__( class ClassificationClass(Message[_ClassificationClassFields]): """ - ClassificationClass contains all Classification 2.0 class fields. Bit-field definitions are intentionally deferred. + ClassificationClass contains all Classification 2.0 class fields. ```proto message datasets.stac.v1.ClassificationClass @@ -189,6 +189,67 @@ def __init__( percentage: float count: int +_ClassificationBitFieldFields: TypeAlias = Literal["offset", "length", "classes", "name", "description", "roles"] + +class ClassificationBitField(Message[_ClassificationBitFieldFields]): + """ + ClassificationBitField describes classes encoded in a contiguous range of bits, following Classification 2.0. + Offset zero denotes the least significant bit; class values apply after extracting this bit range. + + ```proto + message datasets.stac.v1.ClassificationBitField + ``` + + Attributes: + offset: + ```proto + optional uint32 offset = 1; + ``` + length: + ```proto + optional uint32 length = 2; + ``` + classes: + ```proto + repeated datasets.stac.v1.ClassificationClass classes = 3; + ``` + name: + ```proto + optional string name = 4; + ``` + description: + ```proto + optional string description = 5; + ``` + roles: + ```proto + repeated string roles = 6; + ``` + """ + + __slots__ = ("offset", "length", "classes", "name", "description", "roles") + + if TYPE_CHECKING: + + def __init__( + self, + *, + offset: int | None = None, + length: int | None = None, + classes: list[ClassificationClass] | None = None, + name: str | None = None, + description: str | None = None, + roles: list[str] | None = None, + ) -> None: + pass + + offset: int + length: int + classes: list[ClassificationClass] + name: str + description: str + roles: list[str] + _ProjectionFields: TypeAlias = Literal["code", "bbox", "shape", "transform"] class Projection(Message[_ProjectionFields]): @@ -471,12 +532,13 @@ class RasterSampling(Enum): _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', + 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"\xf1\x01\n\x16ClassificationBitField\x12\x1d\n\x06offset\x18\x01 \x01(\rR\x06offsetB\x05\xaa\x01\x02\x08\x01\x12\x1d\n\x06length\x18\x02 \x01(\rR\x06lengthB\x05\xaa\x01\x02\x08\x01\x12?\n\x07classes\x18\x03 \x03(\x0b2%.datasets.stac.v1.ClassificationClassR\x07classes\x12\x19\n\x04name\x18\x04 \x01(\tR\x04nameB\x05\xaa\x01\x02\x08\x01\x12\'\n\x0bdescription\x18\x05 \x01(\tR\x0bdescriptionB\x05\xaa\x01\x02\x08\x01\x12\x14\n\x05roles\x18\x06 \x03(\tR\x05roles"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, + "ClassificationBitField": ClassificationBitField, "Projection": Projection, "View": View, "File": File, diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.py index c3932b4..7e922c7 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n%datasets/stac/v1/asset_metadata.proto\x12\x10\x64\x61tasets.stac.v1\"\xef\x01\n\x0c\x45OProperties\x12?\n\x0b\x63ommon_name\x18\x01 \x01(\x0e\x32\x1e.datasets.stac.v1.EOCommonNameR\ncommonName\x12\x32\n\x11\x63\x65nter_wavelength\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x10\x63\x65nterWavelength\x12\x34\n\x13\x66ull_width_half_max\x18\x03 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x10\x66ullWidthHalfMax\x12\x34\n\x12solar_illumination\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x11solarIllumination\"\xc2\x01\n\x10RasterProperties\x12<\n\x08sampling\x18\x01 \x01(\x0e\x32 .datasets.stac.v1.RasterSamplingR\x08sampling\x12\x1b\n\x05scale\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x05scale\x12\x1d\n\x06offset\x18\x03 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06offset\x12\x34\n\x12spatial_resolution\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x11spatialResolution\"\x9c\x02\n\x13\x43lassificationClass\x12\x1b\n\x05value\x18\x01 \x01(\x03\x42\x05\xaa\x01\x02\x08\x01R\x05value\x12\'\n\x0b\x64\x65scription\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x19\n\x04name\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\x1b\n\x05title\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12$\n\ncolor_hint\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\tcolorHint\x12\x1d\n\x06nodata\x18\x06 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12%\n\npercentage\x18\x07 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\npercentage\x12\x1b\n\x05\x63ount\x18\x08 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x05\x63ount\"o\n\nProjection\x12\x19\n\x04\x63ode\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04\x63ode\x12\x12\n\x04\x62\x62ox\x18\x02 \x03(\x01R\x04\x62\x62ox\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(\x01\x42\x05\xaa\x01\x02\x08\x01R\x0eincidenceAngle\x12\x1f\n\x07\x61zimuth\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07\x61zimuth\"j\n\x04\x46ile\x12!\n\x08\x63hecksum\x18\x01 \x01(\x0c\x42\x05\xaa\x01\x02\x08\x01R\x08\x63hecksum\x12\x19\n\x04size\x18\x02 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x04size\x12$\n\nlocal_path\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\tlocalPath*\xc9\x04\n\x0c\x45OCommonName\x12\x1e\n\x1a\x45O_COMMON_NAME_UNSPECIFIED\x10\x00\x12\x16\n\x12\x45O_COMMON_NAME_PAN\x10\x01\x12\x1a\n\x16\x45O_COMMON_NAME_COASTAL\x10\x02\x12\x17\n\x13\x45O_COMMON_NAME_BLUE\x10\x03\x12\x18\n\x14\x45O_COMMON_NAME_GREEN\x10\x04\x12\x1a\n\x16\x45O_COMMON_NAME_GREEN05\x10\x05\x12\x19\n\x15\x45O_COMMON_NAME_YELLOW\x10\x06\x12\x16\n\x12\x45O_COMMON_NAME_RED\x10\x07\x12\x1a\n\x16\x45O_COMMON_NAME_REDEDGE\x10\x08\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE071\x10\t\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE075\x10\n\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE078\x10\x0b\x12\x16\n\x12\x45O_COMMON_NAME_NIR\x10\x0c\x12\x18\n\x14\x45O_COMMON_NAME_NIR08\x10\r\x12\x18\n\x14\x45O_COMMON_NAME_NIR09\x10\x0e\x12\x19\n\x15\x45O_COMMON_NAME_CIRRUS\x10\x0f\x12\x19\n\x15\x45O_COMMON_NAME_SWIR16\x10\x10\x12\x19\n\x15\x45O_COMMON_NAME_SWIR22\x10\x11\x12\x17\n\x13\x45O_COMMON_NAME_LWIR\x10\x12\x12\x19\n\x15\x45O_COMMON_NAME_LWIR11\x10\x13\x12\x19\n\x15\x45O_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\x02\x42\x91\x01\n\x14\x63om.datasets.stac.v1B\x12\x41ssetMetadataProtoP\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/asset_metadata.proto\x12\x10\x64\x61tasets.stac.v1\"\xef\x01\n\x0c\x45OProperties\x12?\n\x0b\x63ommon_name\x18\x01 \x01(\x0e\x32\x1e.datasets.stac.v1.EOCommonNameR\ncommonName\x12\x32\n\x11\x63\x65nter_wavelength\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x10\x63\x65nterWavelength\x12\x34\n\x13\x66ull_width_half_max\x18\x03 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x10\x66ullWidthHalfMax\x12\x34\n\x12solar_illumination\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x11solarIllumination\"\xc2\x01\n\x10RasterProperties\x12<\n\x08sampling\x18\x01 \x01(\x0e\x32 .datasets.stac.v1.RasterSamplingR\x08sampling\x12\x1b\n\x05scale\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x05scale\x12\x1d\n\x06offset\x18\x03 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06offset\x12\x34\n\x12spatial_resolution\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x11spatialResolution\"\x9c\x02\n\x13\x43lassificationClass\x12\x1b\n\x05value\x18\x01 \x01(\x03\x42\x05\xaa\x01\x02\x08\x01R\x05value\x12\'\n\x0b\x64\x65scription\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x19\n\x04name\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\x1b\n\x05title\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12$\n\ncolor_hint\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\tcolorHint\x12\x1d\n\x06nodata\x18\x06 \x01(\x08\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12%\n\npercentage\x18\x07 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\npercentage\x12\x1b\n\x05\x63ount\x18\x08 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x05\x63ount\"\xf1\x01\n\x16\x43lassificationBitField\x12\x1d\n\x06offset\x18\x01 \x01(\rB\x05\xaa\x01\x02\x08\x01R\x06offset\x12\x1d\n\x06length\x18\x02 \x01(\rB\x05\xaa\x01\x02\x08\x01R\x06length\x12?\n\x07\x63lasses\x18\x03 \x03(\x0b\x32%.datasets.stac.v1.ClassificationClassR\x07\x63lasses\x12\x19\n\x04name\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x14\n\x05roles\x18\x06 \x03(\tR\x05roles\"o\n\nProjection\x12\x19\n\x04\x63ode\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04\x63ode\x12\x12\n\x04\x62\x62ox\x18\x02 \x03(\x01R\x04\x62\x62ox\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(\x01\x42\x05\xaa\x01\x02\x08\x01R\x0eincidenceAngle\x12\x1f\n\x07\x61zimuth\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07\x61zimuth\"j\n\x04\x46ile\x12!\n\x08\x63hecksum\x18\x01 \x01(\x0c\x42\x05\xaa\x01\x02\x08\x01R\x08\x63hecksum\x12\x19\n\x04size\x18\x02 \x01(\x04\x42\x05\xaa\x01\x02\x08\x01R\x04size\x12$\n\nlocal_path\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\tlocalPath*\xc9\x04\n\x0c\x45OCommonName\x12\x1e\n\x1a\x45O_COMMON_NAME_UNSPECIFIED\x10\x00\x12\x16\n\x12\x45O_COMMON_NAME_PAN\x10\x01\x12\x1a\n\x16\x45O_COMMON_NAME_COASTAL\x10\x02\x12\x17\n\x13\x45O_COMMON_NAME_BLUE\x10\x03\x12\x18\n\x14\x45O_COMMON_NAME_GREEN\x10\x04\x12\x1a\n\x16\x45O_COMMON_NAME_GREEN05\x10\x05\x12\x19\n\x15\x45O_COMMON_NAME_YELLOW\x10\x06\x12\x16\n\x12\x45O_COMMON_NAME_RED\x10\x07\x12\x1a\n\x16\x45O_COMMON_NAME_REDEDGE\x10\x08\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE071\x10\t\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE075\x10\n\x12\x1d\n\x19\x45O_COMMON_NAME_REDEDGE078\x10\x0b\x12\x16\n\x12\x45O_COMMON_NAME_NIR\x10\x0c\x12\x18\n\x14\x45O_COMMON_NAME_NIR08\x10\r\x12\x18\n\x14\x45O_COMMON_NAME_NIR09\x10\x0e\x12\x19\n\x15\x45O_COMMON_NAME_CIRRUS\x10\x0f\x12\x19\n\x15\x45O_COMMON_NAME_SWIR16\x10\x10\x12\x19\n\x15\x45O_COMMON_NAME_SWIR22\x10\x11\x12\x17\n\x13\x45O_COMMON_NAME_LWIR\x10\x12\x12\x19\n\x15\x45O_COMMON_NAME_LWIR11\x10\x13\x12\x19\n\x15\x45O_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\x02\x42\x91\x01\n\x14\x63om.datasets.stac.v1B\x12\x41ssetMetadataProtoP\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) @@ -60,6 +60,14 @@ _globals['_CLASSIFICATIONCLASS'].fields_by_name['percentage']._serialized_options = b'\252\001\002\010\001' _globals['_CLASSIFICATIONCLASS'].fields_by_name['count']._loaded_options = None _globals['_CLASSIFICATIONCLASS'].fields_by_name['count']._serialized_options = b'\252\001\002\010\001' + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['offset']._loaded_options = None + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['offset']._serialized_options = b'\252\001\002\010\001' + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['length']._loaded_options = None + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['length']._serialized_options = b'\252\001\002\010\001' + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['name']._loaded_options = None + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['name']._serialized_options = b'\252\001\002\010\001' + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['description']._loaded_options = None + _globals['_CLASSIFICATIONBITFIELD'].fields_by_name['description']._serialized_options = b'\252\001\002\010\001' _globals['_PROJECTION'].fields_by_name['code']._loaded_options = None _globals['_PROJECTION'].fields_by_name['code']._serialized_options = b'\252\001\002\010\001' _globals['_VIEW'].fields_by_name['incidence_angle']._loaded_options = None @@ -72,20 +80,22 @@ _globals['_FILE'].fields_by_name['size']._serialized_options = b'\252\001\002\010\001' _globals['_FILE'].fields_by_name['local_path']._loaded_options = None _globals['_FILE'].fields_by_name['local_path']._serialized_options = b'\252\001\002\010\001' - _globals['_EOCOMMONNAME']._serialized_start=1096 - _globals['_EOCOMMONNAME']._serialized_end=1681 - _globals['_RASTERSAMPLING']._serialized_start=1683 - _globals['_RASTERSAMPLING']._serialized_end=1785 + _globals['_EOCOMMONNAME']._serialized_start=1340 + _globals['_EOCOMMONNAME']._serialized_end=1925 + _globals['_RASTERSAMPLING']._serialized_start=1927 + _globals['_RASTERSAMPLING']._serialized_end=2029 _globals['_EOPROPERTIES']._serialized_start=60 _globals['_EOPROPERTIES']._serialized_end=299 _globals['_RASTERPROPERTIES']._serialized_start=302 _globals['_RASTERPROPERTIES']._serialized_end=496 _globals['_CLASSIFICATIONCLASS']._serialized_start=499 _globals['_CLASSIFICATIONCLASS']._serialized_end=783 - _globals['_PROJECTION']._serialized_start=785 - _globals['_PROJECTION']._serialized_end=896 - _globals['_VIEW']._serialized_start=898 - _globals['_VIEW']._serialized_end=985 - _globals['_FILE']._serialized_start=987 - _globals['_FILE']._serialized_end=1093 + _globals['_CLASSIFICATIONBITFIELD']._serialized_start=786 + _globals['_CLASSIFICATIONBITFIELD']._serialized_end=1027 + _globals['_PROJECTION']._serialized_start=1029 + _globals['_PROJECTION']._serialized_end=1140 + _globals['_VIEW']._serialized_start=1142 + _globals['_VIEW']._serialized_end=1229 + _globals['_FILE']._serialized_start=1231 + _globals['_FILE']._serialized_end=1337 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.pyi b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.pyi index 50aebef..a9cb2b7 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.pyi +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_metadata_pb2.pyi @@ -2,7 +2,7 @@ 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 from google.protobuf import message as _message -from collections.abc import Iterable as _Iterable +from collections.abc import Iterable as _Iterable, Mapping as _Mapping from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union DESCRIPTOR: _descriptor.FileDescriptor @@ -105,6 +105,22 @@ class ClassificationClass(_message.Message): count: int def __init__(self, value: _Optional[int] = ..., description: _Optional[str] = ..., name: _Optional[str] = ..., title: _Optional[str] = ..., color_hint: _Optional[str] = ..., nodata: bool = ..., percentage: _Optional[float] = ..., count: _Optional[int] = ...) -> None: ... +class ClassificationBitField(_message.Message): + __slots__ = ("offset", "length", "classes", "name", "description", "roles") + OFFSET_FIELD_NUMBER: _ClassVar[int] + LENGTH_FIELD_NUMBER: _ClassVar[int] + CLASSES_FIELD_NUMBER: _ClassVar[int] + NAME_FIELD_NUMBER: _ClassVar[int] + DESCRIPTION_FIELD_NUMBER: _ClassVar[int] + ROLES_FIELD_NUMBER: _ClassVar[int] + offset: int + length: int + classes: _containers.RepeatedCompositeFieldContainer[ClassificationClass] + name: str + description: str + roles: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ..., classes: _Optional[_Iterable[_Union[ClassificationClass, _Mapping]]] = ..., name: _Optional[str] = ..., description: _Optional[str] = ..., roles: _Optional[_Iterable[str]] = ...) -> None: ... + class Projection(_message.Message): __slots__ = ("code", "bbox", "shape", "transform") CODE_FIELD_NUMBER: _ClassVar[int] diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py index b0ae8d0..a205168 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: from protobuf import DescFile - from .asset_metadata_pb import ClassificationClass, EOProperties, File, Projection, RasterProperties, View + from .asset_metadata_pb import ClassificationBitField, ClassificationClass, EOProperties, File, Projection, RasterProperties, View from .core_pb import MediaType from .product_pb import ProductProperties from .sar_pb import SARProperties @@ -174,7 +174,7 @@ def __init__( href: str alternate_name: str -_BandFields: TypeAlias = Literal["name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar"] +_BandFields: TypeAlias = Literal["name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar", "bitfields"] class Band(Message[_BandFields]): """ @@ -224,9 +224,13 @@ class Band(Message[_BandFields]): ```proto optional datasets.stac.v1.SARProperties sar = 9; ``` + bitfields: + ```proto + repeated datasets.stac.v1.ClassificationBitField bitfields = 10; + ``` """ - __slots__ = ("name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar") + __slots__ = ("name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar", "bitfields") if TYPE_CHECKING: @@ -242,6 +246,7 @@ def __init__( raster: RasterProperties | None = None, classes: list[ClassificationClass] | None = None, sar: SARProperties | None = None, + bitfields: list[ClassificationBitField] | None = None, ) -> None: pass @@ -254,8 +259,9 @@ def __init__( raster: RasterProperties | None classes: list[ClassificationClass] sar: SARProperties | None + bitfields: list[ClassificationBitField] -_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"] +_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", "bitfields"] class Asset(Message[_AssetFields]): """ @@ -366,9 +372,13 @@ class Asset(Message[_AssetFields]): ```proto optional datasets.stac.v1.ProductProperties product = 23; ``` + bitfields: + ```proto + repeated datasets.stac.v1.ClassificationBitField bitfields = 24; + ``` """ - __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") + __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", "bitfields") if TYPE_CHECKING: @@ -398,6 +408,7 @@ def __init__( sar: SARProperties | None = None, satellite: SatelliteProperties | None = None, product: ProductProperties | None = None, + bitfields: list[ClassificationBitField] | None = None, ) -> None: pass @@ -424,6 +435,7 @@ def __init__( sar: SARProperties | None satellite: SatelliteProperties | None product: ProductProperties | None + bitfields: list[ClassificationBitField] _AssetsFields: TypeAlias = Literal["access_profiles", "band_profiles", "assets"] @@ -737,7 +749,7 @@ class DataType(Enum): _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', + 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"\xe5\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\x12F\n\tbitfields\x18\n \x03(\x0b2(.datasets.stac.v1.ClassificationBitFieldR\tbitfields"\xb0\t\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\x12F\n\tbitfields\x18\x18 \x03(\x0b2(.datasets.stac.v1.ClassificationBitFieldR\tbitfields"\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(), diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.py b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.py index 9fbe09f..81b0be2 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.py +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.py @@ -29,7 +29,7 @@ from tilebox.datasets.datasets.stac.v1 import satellite_pb2 as datasets_dot_stac_dot_v1_dot_satellite__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1c\x64\x61tasets/stac/v1/asset.proto\x12\x10\x64\x61tasets.stac.v1\x1a%datasets/stac/v1/asset_metadata.proto\x1a\x1b\x64\x61tasets/stac/v1/core.proto\x1a\x1e\x64\x61tasets/stac/v1/product.proto\x1a\x1a\x64\x61tasets/stac/v1/sar.proto\x1a datasets/stac/v1/satellite.proto\"N\n\nStatistics\x12\x1f\n\x07minimum\x18\x01 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07minimum\x12\x1f\n\x07maximum\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07maximum\"\xd3\x01\n\x12\x41ssetAccessProfile\x12#\n\ralternate_key\x18\x01 \x01(\tR\x0c\x61lternateKey\x12;\n\x16\x64\x65\x66\x61ult_alternate_name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x14\x64\x65\x66\x61ultAlternateName\x12\x1b\n\tbase_href\x18\x03 \x01(\tR\x08\x62\x61seHref\x12!\n\x0cstorage_refs\x18\x04 \x03(\tR\x0bstorageRefs\x12\x1b\n\tauth_refs\x18\x05 \x03(\tR\x08\x61uthRefs\"\x91\x01\n\rAssetLocation\x12\x37\n\x14\x61\x63\x63\x65ss_profile_index\x18\x01 \x01(\rB\x05\xaa\x01\x02\x08\x01R\x12\x61\x63\x63\x65ssProfileIndex\x12\x19\n\x04href\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04href\x12,\n\x0e\x61lternate_name\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ralternateName\"\x9d\x03\n\x04\x42\x61nd\x12\x19\n\x04name\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x37\n\tdata_type\x18\x03 \x01(\x0e\x32\x1a.datasets.stac.v1.DataTypeR\x08\x64\x61taType\x12\x1d\n\x06nodata\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12\x19\n\x04unit\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04unit\x12.\n\x02\x65o\x18\x06 \x01(\x0b\x32\x1e.datasets.stac.v1.EOPropertiesR\x02\x65o\x12:\n\x06raster\x18\x07 \x01(\x0b\x32\".datasets.stac.v1.RasterPropertiesR\x06raster\x12?\n\x07\x63lasses\x18\x08 \x03(\x0b\x32%.datasets.stac.v1.ClassificationClassR\x07\x63lasses\x12\x31\n\x03sar\x18\t \x01(\x0b\x32\x1f.datasets.stac.v1.SARPropertiesR\x03sar\"\xe8\x08\n\x05\x41sset\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x39\n\x07primary\x18\x02 \x01(\x0b\x32\x1f.datasets.stac.v1.AssetLocationR\x07primary\x12?\n\nalternates\x18\x03 \x03(\x0b\x32\x1f.datasets.stac.v1.AssetLocationR\nalternates\x12:\n\nmedia_type\x18\x04 \x01(\x0b\x32\x1b.datasets.stac.v1.MediaTypeR\tmediaType\x12\x1b\n\x05title\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12\'\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x36\n\x05roles\x18\x07 \x03(\x0e\x32 .datasets.stac.v1.KnownAssetRoleR\x05roles\x12!\n\x0c\x63ustom_roles\x18\x08 \x03(\tR\x0b\x63ustomRoles\x12\x17\n\x03gsd\x18\t \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x03gsd\x12\x30\n\x14\x62\x61nd_profile_indices\x18\n \x03(\rR\x12\x62\x61ndProfileIndices\x12\x37\n\tdata_type\x18\x0b \x01(\x0e\x32\x1a.datasets.stac.v1.DataTypeR\x08\x64\x61taType\x12\x1d\n\x06nodata\x18\x0c \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12<\n\nstatistics\x18\r \x01(\x0b\x32\x1c.datasets.stac.v1.StatisticsR\nstatistics\x12\x19\n\x04unit\x18\x0e \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04unit\x12.\n\x02\x65o\x18\x0f \x01(\x0b\x32\x1e.datasets.stac.v1.EOPropertiesR\x02\x65o\x12:\n\x06raster\x18\x10 \x01(\x0b\x32\".datasets.stac.v1.RasterPropertiesR\x06raster\x12<\n\nprojection\x18\x11 \x01(\x0b\x32\x1c.datasets.stac.v1.ProjectionR\nprojection\x12*\n\x04view\x18\x12 \x01(\x0b\x32\x16.datasets.stac.v1.ViewR\x04view\x12?\n\x07\x63lasses\x18\x13 \x03(\x0b\x32%.datasets.stac.v1.ClassificationClassR\x07\x63lasses\x12*\n\x04\x66ile\x18\x14 \x01(\x0b\x32\x16.datasets.stac.v1.FileR\x04\x66ile\x12\x31\n\x03sar\x18\x15 \x01(\x0b\x32\x1f.datasets.stac.v1.SARPropertiesR\x03sar\x12\x43\n\tsatellite\x18\x16 \x01(\x0b\x32%.datasets.stac.v1.SatellitePropertiesR\tsatellite\x12=\n\x07product\x18\x17 \x01(\x0b\x32#.datasets.stac.v1.ProductPropertiesR\x07product\"\xc5\x01\n\x06\x41ssets\x12M\n\x0f\x61\x63\x63\x65ss_profiles\x18\x01 \x03(\x0b\x32$.datasets.stac.v1.AssetAccessProfileR\x0e\x61\x63\x63\x65ssProfiles\x12;\n\rband_profiles\x18\x02 \x03(\x0b\x32\x16.datasets.stac.v1.BandR\x0c\x62\x61ndProfiles\x12/\n\x06\x61ssets\x18\x03 \x03(\x0b\x32\x17.datasets.stac.v1.AssetR\x06\x61ssets*\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\x10\x64\x12 \n\x1cKNOWN_ASSET_ROLE_TEMPERATURE\x10\x65\x12\x1f\n\x1bKNOWN_ASSET_ROLE_SATURATION\x10\x66\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\x08\x44\x61taType\x12\x19\n\x15\x44\x41TA_TYPE_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x44\x41TA_TYPE_INT8\x10\x01\x12\x13\n\x0f\x44\x41TA_TYPE_INT16\x10\x02\x12\x13\n\x0f\x44\x41TA_TYPE_INT32\x10\x03\x12\x13\n\x0f\x44\x41TA_TYPE_INT64\x10\x04\x12\x13\n\x0f\x44\x41TA_TYPE_UINT8\x10\x05\x12\x14\n\x10\x44\x41TA_TYPE_UINT16\x10\x06\x12\x14\n\x10\x44\x41TA_TYPE_UINT32\x10\x07\x12\x14\n\x10\x44\x41TA_TYPE_UINT64\x10\x08\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT16\x10\t\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT32\x10\n\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT64\x10\x0b\x12\x14\n\x10\x44\x41TA_TYPE_CINT16\x10\x0c\x12\x14\n\x10\x44\x41TA_TYPE_CINT32\x10\r\x12\x16\n\x12\x44\x41TA_TYPE_CFLOAT32\x10\x0e\x12\x16\n\x12\x44\x41TA_TYPE_CFLOAT64\x10\x0f\x12\x13\n\x0f\x44\x41TA_TYPE_OTHER\x10\x10\x42\x89\x01\n\x14\x63om.datasets.stac.v1B\nAssetProtoP\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\x1c\x64\x61tasets/stac/v1/asset.proto\x12\x10\x64\x61tasets.stac.v1\x1a%datasets/stac/v1/asset_metadata.proto\x1a\x1b\x64\x61tasets/stac/v1/core.proto\x1a\x1e\x64\x61tasets/stac/v1/product.proto\x1a\x1a\x64\x61tasets/stac/v1/sar.proto\x1a datasets/stac/v1/satellite.proto\"N\n\nStatistics\x12\x1f\n\x07minimum\x18\x01 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07minimum\x12\x1f\n\x07maximum\x18\x02 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x07maximum\"\xd3\x01\n\x12\x41ssetAccessProfile\x12#\n\ralternate_key\x18\x01 \x01(\tR\x0c\x61lternateKey\x12;\n\x16\x64\x65\x66\x61ult_alternate_name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x14\x64\x65\x66\x61ultAlternateName\x12\x1b\n\tbase_href\x18\x03 \x01(\tR\x08\x62\x61seHref\x12!\n\x0cstorage_refs\x18\x04 \x03(\tR\x0bstorageRefs\x12\x1b\n\tauth_refs\x18\x05 \x03(\tR\x08\x61uthRefs\"\x91\x01\n\rAssetLocation\x12\x37\n\x14\x61\x63\x63\x65ss_profile_index\x18\x01 \x01(\rB\x05\xaa\x01\x02\x08\x01R\x12\x61\x63\x63\x65ssProfileIndex\x12\x19\n\x04href\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04href\x12,\n\x0e\x61lternate_name\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\ralternateName\"\xe5\x03\n\x04\x42\x61nd\x12\x19\n\x04name\x18\x01 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x37\n\tdata_type\x18\x03 \x01(\x0e\x32\x1a.datasets.stac.v1.DataTypeR\x08\x64\x61taType\x12\x1d\n\x06nodata\x18\x04 \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12\x19\n\x04unit\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04unit\x12.\n\x02\x65o\x18\x06 \x01(\x0b\x32\x1e.datasets.stac.v1.EOPropertiesR\x02\x65o\x12:\n\x06raster\x18\x07 \x01(\x0b\x32\".datasets.stac.v1.RasterPropertiesR\x06raster\x12?\n\x07\x63lasses\x18\x08 \x03(\x0b\x32%.datasets.stac.v1.ClassificationClassR\x07\x63lasses\x12\x31\n\x03sar\x18\t \x01(\x0b\x32\x1f.datasets.stac.v1.SARPropertiesR\x03sar\x12\x46\n\tbitfields\x18\n \x03(\x0b\x32(.datasets.stac.v1.ClassificationBitFieldR\tbitfields\"\xb0\t\n\x05\x41sset\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x39\n\x07primary\x18\x02 \x01(\x0b\x32\x1f.datasets.stac.v1.AssetLocationR\x07primary\x12?\n\nalternates\x18\x03 \x03(\x0b\x32\x1f.datasets.stac.v1.AssetLocationR\nalternates\x12:\n\nmedia_type\x18\x04 \x01(\x0b\x32\x1b.datasets.stac.v1.MediaTypeR\tmediaType\x12\x1b\n\x05title\x18\x05 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x05title\x12\'\n\x0b\x64\x65scription\x18\x06 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\x12\x36\n\x05roles\x18\x07 \x03(\x0e\x32 .datasets.stac.v1.KnownAssetRoleR\x05roles\x12!\n\x0c\x63ustom_roles\x18\x08 \x03(\tR\x0b\x63ustomRoles\x12\x17\n\x03gsd\x18\t \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x03gsd\x12\x30\n\x14\x62\x61nd_profile_indices\x18\n \x03(\rR\x12\x62\x61ndProfileIndices\x12\x37\n\tdata_type\x18\x0b \x01(\x0e\x32\x1a.datasets.stac.v1.DataTypeR\x08\x64\x61taType\x12\x1d\n\x06nodata\x18\x0c \x01(\x01\x42\x05\xaa\x01\x02\x08\x01R\x06nodata\x12<\n\nstatistics\x18\r \x01(\x0b\x32\x1c.datasets.stac.v1.StatisticsR\nstatistics\x12\x19\n\x04unit\x18\x0e \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04unit\x12.\n\x02\x65o\x18\x0f \x01(\x0b\x32\x1e.datasets.stac.v1.EOPropertiesR\x02\x65o\x12:\n\x06raster\x18\x10 \x01(\x0b\x32\".datasets.stac.v1.RasterPropertiesR\x06raster\x12<\n\nprojection\x18\x11 \x01(\x0b\x32\x1c.datasets.stac.v1.ProjectionR\nprojection\x12*\n\x04view\x18\x12 \x01(\x0b\x32\x16.datasets.stac.v1.ViewR\x04view\x12?\n\x07\x63lasses\x18\x13 \x03(\x0b\x32%.datasets.stac.v1.ClassificationClassR\x07\x63lasses\x12*\n\x04\x66ile\x18\x14 \x01(\x0b\x32\x16.datasets.stac.v1.FileR\x04\x66ile\x12\x31\n\x03sar\x18\x15 \x01(\x0b\x32\x1f.datasets.stac.v1.SARPropertiesR\x03sar\x12\x43\n\tsatellite\x18\x16 \x01(\x0b\x32%.datasets.stac.v1.SatellitePropertiesR\tsatellite\x12=\n\x07product\x18\x17 \x01(\x0b\x32#.datasets.stac.v1.ProductPropertiesR\x07product\x12\x46\n\tbitfields\x18\x18 \x03(\x0b\x32(.datasets.stac.v1.ClassificationBitFieldR\tbitfields\"\xc5\x01\n\x06\x41ssets\x12M\n\x0f\x61\x63\x63\x65ss_profiles\x18\x01 \x03(\x0b\x32$.datasets.stac.v1.AssetAccessProfileR\x0e\x61\x63\x63\x65ssProfiles\x12;\n\rband_profiles\x18\x02 \x03(\x0b\x32\x16.datasets.stac.v1.BandR\x0c\x62\x61ndProfiles\x12/\n\x06\x61ssets\x18\x03 \x03(\x0b\x32\x17.datasets.stac.v1.AssetR\x06\x61ssets*\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\x10\x64\x12 \n\x1cKNOWN_ASSET_ROLE_TEMPERATURE\x10\x65\x12\x1f\n\x1bKNOWN_ASSET_ROLE_SATURATION\x10\x66\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\x08\x44\x61taType\x12\x19\n\x15\x44\x41TA_TYPE_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x44\x41TA_TYPE_INT8\x10\x01\x12\x13\n\x0f\x44\x41TA_TYPE_INT16\x10\x02\x12\x13\n\x0f\x44\x41TA_TYPE_INT32\x10\x03\x12\x13\n\x0f\x44\x41TA_TYPE_INT64\x10\x04\x12\x13\n\x0f\x44\x41TA_TYPE_UINT8\x10\x05\x12\x14\n\x10\x44\x41TA_TYPE_UINT16\x10\x06\x12\x14\n\x10\x44\x41TA_TYPE_UINT32\x10\x07\x12\x14\n\x10\x44\x41TA_TYPE_UINT64\x10\x08\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT16\x10\t\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT32\x10\n\x12\x15\n\x11\x44\x41TA_TYPE_FLOAT64\x10\x0b\x12\x14\n\x10\x44\x41TA_TYPE_CINT16\x10\x0c\x12\x14\n\x10\x44\x41TA_TYPE_CINT32\x10\r\x12\x16\n\x12\x44\x41TA_TYPE_CFLOAT32\x10\x0e\x12\x16\n\x12\x44\x41TA_TYPE_CFLOAT64\x10\x0f\x12\x13\n\x0f\x44\x41TA_TYPE_OTHER\x10\x10\x42\x89\x01\n\x14\x63om.datasets.stac.v1B\nAssetProtoP\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) @@ -67,10 +67,10 @@ _globals['_ASSET'].fields_by_name['nodata']._serialized_options = b'\252\001\002\010\001' _globals['_ASSET'].fields_by_name['unit']._loaded_options = None _globals['_ASSET'].fields_by_name['unit']._serialized_options = b'\252\001\002\010\001' - _globals['_KNOWNASSETROLE']._serialized_start=2402 - _globals['_KNOWNASSETROLE']._serialized_end=3360 - _globals['_DATATYPE']._serialized_start=3363 - _globals['_DATATYPE']._serialized_end=3752 + _globals['_KNOWNASSETROLE']._serialized_start=2546 + _globals['_KNOWNASSETROLE']._serialized_end=3504 + _globals['_DATATYPE']._serialized_start=3507 + _globals['_DATATYPE']._serialized_end=3896 _globals['_STATISTICS']._serialized_start=212 _globals['_STATISTICS']._serialized_end=290 _globals['_ASSETACCESSPROFILE']._serialized_start=293 @@ -78,9 +78,9 @@ _globals['_ASSETLOCATION']._serialized_start=507 _globals['_ASSETLOCATION']._serialized_end=652 _globals['_BAND']._serialized_start=655 - _globals['_BAND']._serialized_end=1068 - _globals['_ASSET']._serialized_start=1071 - _globals['_ASSET']._serialized_end=2199 - _globals['_ASSETS']._serialized_start=2202 - _globals['_ASSETS']._serialized_end=2399 + _globals['_BAND']._serialized_end=1140 + _globals['_ASSET']._serialized_start=1143 + _globals['_ASSET']._serialized_end=2343 + _globals['_ASSETS']._serialized_start=2346 + _globals['_ASSETS']._serialized_end=2543 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.pyi b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.pyi index ef15113..0239bc6 100644 --- a/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.pyi +++ b/tilebox-datasets/tilebox/datasets/datasets/stac/v1/asset_pb2.pyi @@ -143,7 +143,7 @@ class AssetLocation(_message.Message): def __init__(self, access_profile_index: _Optional[int] = ..., href: _Optional[str] = ..., alternate_name: _Optional[str] = ...) -> None: ... class Band(_message.Message): - __slots__ = ("name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar") + __slots__ = ("name", "description", "data_type", "nodata", "unit", "eo", "raster", "classes", "sar", "bitfields") NAME_FIELD_NUMBER: _ClassVar[int] DESCRIPTION_FIELD_NUMBER: _ClassVar[int] DATA_TYPE_FIELD_NUMBER: _ClassVar[int] @@ -153,6 +153,7 @@ class Band(_message.Message): RASTER_FIELD_NUMBER: _ClassVar[int] CLASSES_FIELD_NUMBER: _ClassVar[int] SAR_FIELD_NUMBER: _ClassVar[int] + BITFIELDS_FIELD_NUMBER: _ClassVar[int] name: str description: str data_type: DataType @@ -162,10 +163,11 @@ class Band(_message.Message): raster: _asset_metadata_pb2.RasterProperties classes: _containers.RepeatedCompositeFieldContainer[_asset_metadata_pb2.ClassificationClass] sar: _sar_pb2.SARProperties - def __init__(self, name: _Optional[str] = ..., description: _Optional[str] = ..., data_type: _Optional[_Union[DataType, str]] = ..., nodata: _Optional[float] = ..., unit: _Optional[str] = ..., eo: _Optional[_Union[_asset_metadata_pb2.EOProperties, _Mapping]] = ..., raster: _Optional[_Union[_asset_metadata_pb2.RasterProperties, _Mapping]] = ..., classes: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationClass, _Mapping]]] = ..., sar: _Optional[_Union[_sar_pb2.SARProperties, _Mapping]] = ...) -> None: ... + bitfields: _containers.RepeatedCompositeFieldContainer[_asset_metadata_pb2.ClassificationBitField] + def __init__(self, name: _Optional[str] = ..., description: _Optional[str] = ..., data_type: _Optional[_Union[DataType, str]] = ..., nodata: _Optional[float] = ..., unit: _Optional[str] = ..., eo: _Optional[_Union[_asset_metadata_pb2.EOProperties, _Mapping]] = ..., raster: _Optional[_Union[_asset_metadata_pb2.RasterProperties, _Mapping]] = ..., classes: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationClass, _Mapping]]] = ..., sar: _Optional[_Union[_sar_pb2.SARProperties, _Mapping]] = ..., bitfields: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationBitField, _Mapping]]] = ...) -> None: ... class Asset(_message.Message): - __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") + __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", "bitfields") KEY_FIELD_NUMBER: _ClassVar[int] PRIMARY_FIELD_NUMBER: _ClassVar[int] ALTERNATES_FIELD_NUMBER: _ClassVar[int] @@ -189,6 +191,7 @@ class Asset(_message.Message): SAR_FIELD_NUMBER: _ClassVar[int] SATELLITE_FIELD_NUMBER: _ClassVar[int] PRODUCT_FIELD_NUMBER: _ClassVar[int] + BITFIELDS_FIELD_NUMBER: _ClassVar[int] key: str primary: AssetLocation alternates: _containers.RepeatedCompositeFieldContainer[AssetLocation] @@ -212,7 +215,8 @@ class Asset(_message.Message): sar: _sar_pb2.SARProperties satellite: _satellite_pb2.SatelliteProperties product: _product_pb2.ProductProperties - def __init__(self, key: _Optional[str] = ..., primary: _Optional[_Union[AssetLocation, _Mapping]] = ..., alternates: _Optional[_Iterable[_Union[AssetLocation, _Mapping]]] = ..., media_type: _Optional[_Union[_core_pb2.MediaType, _Mapping]] = ..., title: _Optional[str] = ..., description: _Optional[str] = ..., roles: _Optional[_Iterable[_Union[KnownAssetRole, str]]] = ..., custom_roles: _Optional[_Iterable[str]] = ..., gsd: _Optional[float] = ..., band_profile_indices: _Optional[_Iterable[int]] = ..., data_type: _Optional[_Union[DataType, str]] = ..., nodata: _Optional[float] = ..., statistics: _Optional[_Union[Statistics, _Mapping]] = ..., unit: _Optional[str] = ..., eo: _Optional[_Union[_asset_metadata_pb2.EOProperties, _Mapping]] = ..., raster: _Optional[_Union[_asset_metadata_pb2.RasterProperties, _Mapping]] = ..., projection: _Optional[_Union[_asset_metadata_pb2.Projection, _Mapping]] = ..., view: _Optional[_Union[_asset_metadata_pb2.View, _Mapping]] = ..., classes: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationClass, _Mapping]]] = ..., file: _Optional[_Union[_asset_metadata_pb2.File, _Mapping]] = ..., sar: _Optional[_Union[_sar_pb2.SARProperties, _Mapping]] = ..., satellite: _Optional[_Union[_satellite_pb2.SatelliteProperties, _Mapping]] = ..., product: _Optional[_Union[_product_pb2.ProductProperties, _Mapping]] = ...) -> None: ... + bitfields: _containers.RepeatedCompositeFieldContainer[_asset_metadata_pb2.ClassificationBitField] + def __init__(self, key: _Optional[str] = ..., primary: _Optional[_Union[AssetLocation, _Mapping]] = ..., alternates: _Optional[_Iterable[_Union[AssetLocation, _Mapping]]] = ..., media_type: _Optional[_Union[_core_pb2.MediaType, _Mapping]] = ..., title: _Optional[str] = ..., description: _Optional[str] = ..., roles: _Optional[_Iterable[_Union[KnownAssetRole, str]]] = ..., custom_roles: _Optional[_Iterable[str]] = ..., gsd: _Optional[float] = ..., band_profile_indices: _Optional[_Iterable[int]] = ..., data_type: _Optional[_Union[DataType, str]] = ..., nodata: _Optional[float] = ..., statistics: _Optional[_Union[Statistics, _Mapping]] = ..., unit: _Optional[str] = ..., eo: _Optional[_Union[_asset_metadata_pb2.EOProperties, _Mapping]] = ..., raster: _Optional[_Union[_asset_metadata_pb2.RasterProperties, _Mapping]] = ..., projection: _Optional[_Union[_asset_metadata_pb2.Projection, _Mapping]] = ..., view: _Optional[_Union[_asset_metadata_pb2.View, _Mapping]] = ..., classes: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationClass, _Mapping]]] = ..., file: _Optional[_Union[_asset_metadata_pb2.File, _Mapping]] = ..., sar: _Optional[_Union[_sar_pb2.SARProperties, _Mapping]] = ..., satellite: _Optional[_Union[_satellite_pb2.SatelliteProperties, _Mapping]] = ..., product: _Optional[_Union[_product_pb2.ProductProperties, _Mapping]] = ..., bitfields: _Optional[_Iterable[_Union[_asset_metadata_pb2.ClassificationBitField, _Mapping]]] = ...) -> None: ... class Assets(_message.Message): __slots__ = ("access_profiles", "band_profiles", "assets") diff --git a/tilebox-storage/README.md b/tilebox-storage/README.md index fd689ea..2c816a2 100644 --- a/tilebox-storage/README.md +++ b/tilebox-storage/README.md @@ -84,6 +84,86 @@ for content in downloaded_data.iterdir(): print(f" - {content.relative_to(downloaded_data)}") ``` +## Planetary Computer assets + +The asynchronous asset client reads Planetary Computer assets through the same API as public assets: + +```python +from tilebox.datasets.assets import AssetCollection +from tilebox.storage.aio import Client + +assets = AssetCollection.from_datapoint(datapoint) +storage = Client() + +resolved = storage.resolve(assets["red"]) # Local metadata validation only; no network requests. +geotiff = await storage.open_geotiff(assets["red"]) +metadata = await storage.read_bytes(assets["mtl.json"], max_bytes=1_000_000) +await storage.download(assets["red"], "data/red.tif") +``` + +The asset must declare the Planetary Computer `signedUrl` flow for +`https://planetarycomputer.microsoft.com/api/sas/v1/sign` (GET, an `href` query parameter, +and an `href` response field). HTTPS locations also need Microsoft Azure storage metadata. +The client checks the account and container against any supplied storage metadata. +Planetary Computer is the only built-in signing service; unknown APIs are rejected. + +The first read fetches a container-scoped SAS token automatically. Assets with the same account, +container, and credential binding share an object store. Descriptions and unused flows do not +split the cache. Obstore caches credentials and +refreshes them on demand near expiry. `resolved.href` stays unsigned; callers do not need to manage +tokens or close a session. Token requests use `niquests` and bounded async Tenacity retries. + +Use async obstore operations if accessing `resolved.store` directly. Download NetCDF and HDF assets +with `download()` before opening them locally; `open_geotiff()` only supports GeoTIFF assets. + +## Asset locations and credentials + +The asset client supports local paths and `file`, `s3`, `gs`/`gcs`, `az`/`azure`, `abfs`/`abfss`, +and HTTP(S) locations. Azure forms are: + +- `az://account/container/key` (also `azure`) +- `abfs://container@account.dfs.core.windows.net/key` (also `abfss`) +- `abfs://container/key` with the account in storage metadata (also `abfss`) +- `https://account.blob.core.windows.net/container/key` with Azure storage metadata + +These Azure forms share stores and signing credentials. A resolved `path` can be an object key +or dataset prefix; it is never a full URL. This permits multi-key readers such as a future +`open_zarr()` without putting the prefix into the store cache key. No Zarr reader is included yet. + +Locations without authentication metadata use unsigned cloud requests, even when credentials +are present in the environment. Obstore still reads environment configuration when constructing +stores; `skip_signature` disables request authentication, not environment configuration parsing. +Declared but unsupported authentication fails resolution rather than falling back to anonymous +access. The client tries supported alternate locations during resolution, not after read failures. + +S3 authentication requires an explicitly supplied provider, keyed by the location's authentication +scheme name. For example, this opts into boto3's credential discovery on the first read: + +```python +import asyncio +from obstore.auth.boto3 import Boto3CredentialProvider +from tilebox.storage.aio import Client + + +async def aws_credentials(): + def fetch(): + return Boto3CredentialProvider()() + + return await asyncio.to_thread(fetch) + + +storage = Client(s3_credentials={"aws": aws_credentials}) +``` + +HTTP query parameters remain part of the request URL and store identity, rather than being +encoded into the object key. They cannot be discarded: a parameter such as `versionId` may +select different data. Prefer normalized native locations in datapoints for cloud-specific +authentication; the client does not infer S3 or GCS bucket layouts from arbitrary HTTPS URLs. + +Resolution uses an internal registry of supported backends and authentication flows; there is no +public plugin registration API. Unsupported combinations are filtered before ambiguity is checked. +Two supported combinations or signing flows require the caller to disambiguate the metadata. + ## License Distributed under the MIT License (`The MIT License`). diff --git a/tilebox-storage/pyproject.toml b/tilebox-storage/pyproject.toml index f7eec51..1ef1066 100644 --- a/tilebox-storage/pyproject.toml +++ b/tilebox-storage/pyproject.toml @@ -23,8 +23,9 @@ requires-python = ">=3.11" dependencies = [ "tilebox-datasets>=0.56.0", "niquests>=3.18", + "tenacity>=8", "aiofile>=3.8", - "obstore>=0.8.0", + "obstore>=0.11.1", "boto3>=1.40.2", "async-geotiff", "pyproj>=3.4", diff --git a/tilebox-storage/tests/test_asset_client.py b/tilebox-storage/tests/test_asset_client.py index 3a3f84f..6d8cd4b 100644 --- a/tilebox-storage/tests/test_asset_client.py +++ b/tilebox-storage/tests/test_asset_client.py @@ -1,24 +1,137 @@ +import json import subprocess import sys +from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +from threading import Thread from types import MappingProxyType -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, patch +from urllib.parse import urlsplit +import numpy as np +import obstore import pytest -from obstore.store import LocalStore +import xarray as xr +from obstore.store import AzureStore, GCSStore, HTTPStore, LocalStore, S3Store +from tilebox.datasets.assets import AssetCollection from tilebox.datasets.assets.assets import Asset, AssetLocation -from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme, KnownAuthenticationType -from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, StorageScheme +from tilebox.datasets.datasets.stac.v1.asset_pb import Assets +from tilebox.datasets.datasets.stac.v1.authentication_pb import ( + Authentication, + AuthenticationScheme, + KnownAuthenticationType, +) +from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, Storage, StorageScheme from tilebox.storage.aio import AssetAccessPolicy, Client +if TYPE_CHECKING: + from obstore.store import S3Credential + def _asset(location: AssetLocation, **kwargs: Any) -> Asset: return Asset(key="data", primary=location, **kwargs) +@pytest.mark.parametrize( + ("sample", "store_type", "href", "options"), + [ + pytest.param( + "sentinel1", + S3Store, + "s3://sentinel-s1-l1c/GRD/2020/1/1/IW/DV/" + "S1B_IW_GRDH_1SDV_20200101T000510_20200101T000539_019616_02512B_B140/measurement/iw-vv.tiff", + {"region": "eu-central-1", "request_payer": "false"}, + id="aws-earth-sentinel1", + ), + pytest.param( + "sentinel2", + S3Store, + "s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/11/X/NM/2026/7/" + "S2B_T11XNM_20260701T000137_L2A/B04.tif", + {"region": "us-west-2", "request_payer": "false"}, + id="aws-earth-sentinel2", + ), + pytest.param( + "wyvern", + HTTPStore, + "https://wyvern-data.com/wyvern_dragonette-001_20240506T061223_fb944bb4_l2a/" + "wyvern_dragonette-001_20240506T061223_fb944bb4_l2a.tiff", + {}, + id="wyvern-dragonette", + ), + pytest.param( + "planet", + GCSStore, + "gs://open-cogs/planet-stac/tanager1-release2-core-imagery/" + "ortho_visual/20240925_185504_87_4001_ortho_visual.tif", + {}, + id="planet-tanager", + ), + pytest.param( + "satellogic", + S3Store, + "s3://satellogic-earthview/data/tif/zone=36N/region=362863_5817776/date=2022-07-01/" + "20220701_085711_SN18_36N_362863_5817776_TOA.tif", + {}, + id="satellogic-earth-view", + ), + pytest.param( + "planetary_computer", + AzureStore, + "https://modiseuwest.blob.core.windows.net/modis-061-cogs/MOD13Q1/35/10/2026193/" + "MOD13Q1.A2026193.h35v10.061.2026210034525_250m_16_days_NDVI.tif", + {"account_name": "modiseuwest", "container_name": "modis-061-cogs", "skip_signature": "false"}, + id="planetary-computer-modis", + ), + ], +) +def test_resolve_real_catalog_assets( + sample: str, store_type: type[S3Store | GCSStore | HTTPStore | AzureStore], href: str, options: dict[str, str] +) -> None: + """Resolve captured metadata through public APIs without reading source bytes. + + Captured on 2026-10-14 with ``tilebox dataset query `. + """ + fixture = json.loads((Path(__file__).parent / "testdata" / "asset_access.json").read_text())[sample] + messages = {"assets": Assets, "storage": Storage, "authentication": Authentication} + datapoint = xr.Dataset( + { + name: xr.DataArray(np.array(message.from_json(json.dumps(fixture[name])), dtype=object)) + for name, message in messages.items() + if name in fixture + } + ) + asset = next(iter(AssetCollection.from_datapoint(datapoint).values())) + resolved = Client().resolve(asset) + + assert isinstance(resolved.store, store_type) + assert resolved.href == href + expected = urlsplit(href) + expected_path = expected.path.lstrip("/") + if isinstance(resolved.store, AzureStore): + assert resolved.authentication_scheme is not None + assert resolved.authentication_scheme.known_type == KnownAuthenticationType.SIGNED_URL + assert callable(resolved.store.credential_provider) + expected_path = expected_path.removeprefix(f"{options['container_name']}/") + else: + assert resolved.authentication_scheme is None + assert resolved.path == expected_path + if isinstance(resolved.store, HTTPStore): + assert resolved.store.url == f"{expected.scheme}://{expected.netloc}/" + else: + if not isinstance(resolved.store, AzureStore): + assert resolved.store.config["bucket"] == expected.netloc + assert resolved.store.config["skip_signature"] == "true" + assert resolved.store.credential_provider is None + for key, value in options.items(): + assert resolved.store.config[key] == value + + def test_new_aio_api_does_not_introduce_root_client_or_eager_geotiff_import() -> None: code = ( "import sys\n" @@ -75,12 +188,11 @@ def test_resolution_rejection_details_and_client_policy() -> None: AssetLocation( "s3://bucket/data.tif", MappingProxyType({"first": first, "second": second}), - MappingProxyType({"oauth": auth}), ) ) client = Client() - with pytest.raises(ValueError, match=r"primary.*multiple storage schemes"): + with pytest.raises(ValueError, match=r"primary.*multiple supported storage/authentication"): client.resolve(asset) authenticated = _asset( AssetLocation("s3://bucket/data.tif", authentication_schemes=MappingProxyType({"oauth": auth})) @@ -98,6 +210,154 @@ def test_resolution_rejection_details_and_client_policy() -> None: assert location is primary +def test_filters_unsupported_combinations_before_checking_ambiguity() -> None: + compatible = StorageScheme(known_type=KnownStorageType.AWS_S3, region="us-west-2") + incompatible = StorageScheme(known_type=KnownStorageType.GOOGLE_CLOUD_STORAGE) + s3_auth = AuthenticationScheme(known_type=KnownAuthenticationType.S3) + unsupported_auth = AuthenticationScheme(known_type=KnownAuthenticationType.OAUTH2) + asset = _asset( + AssetLocation( + "s3://bucket/a%20b.tif", + {"wrong": incompatible, "correct": compatible}, + {"oauth": unsupported_auth, "aws": s3_auth}, + ) + ) + provider = AsyncMock(side_effect=AssertionError("resolve must not fetch credentials")) + client = Client(s3_credentials={"aws": provider}) + resolved = client.resolve(asset) + assert resolved.storage_scheme is compatible + assert resolved.authentication_scheme is s3_auth + assert resolved.path == "a b.tif" + + # A rejected preferred location still permits a supported alternate. + fallback = AssetLocation("https://example.com/a.tif") + assert Client().resolve(replace(asset, alternates={"http": fallback})).location is fallback + + +@pytest.mark.parametrize( + "href", ["file:///tmp/a", "gs://bucket/a", "az://account/container/a", "https://example.com/a"] +) +def test_s3_authentication_is_not_accepted_by_other_backends(href: str) -> None: + auth = AuthenticationScheme(known_type=KnownAuthenticationType.S3) + with pytest.raises(ValueError, match="unsupported"): + Client().resolve(_asset(AssetLocation(href, authentication_schemes={"aws": auth}))) + + +@pytest.fixture +def object_server() -> Iterator[tuple[str, list[dict[str, Any]]]]: + requests: list[dict[str, Any]] = [] + + class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + requests.append({"path": self.path, "headers": self.headers}) + body = b"0123456789" + byte_range = self.headers.get("Range") + self.send_response(206 if byte_range else 200) + if byte_range: + start, end = map(int, byte_range.removeprefix("bytes=").split("-")) + self.send_header("Content-Range", f"bytes {start}-{end}/{len(body)}") + body = body[start : end + 1] + self.send_header("Content-Length", str(len(body))) + self.send_header("ETag", '"test"') + self.send_header("Last-Modified", "Mon, 12 Oct 2026 12:00:00 GMT") + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}", requests + finally: + server.shutdown() + server.server_close() + thread.join() + + +@pytest.mark.asyncio +async def test_http_paths_queries_and_ranges(object_server: tuple[str, list[dict[str, Any]]]) -> None: + origin, requests = object_server + client = Client() + asset = _asset(AssetLocation(f"{origin}/folder/a%20b%25.tif?versionId=abc%2Fdef&download=1")) + assert await client.read_bytes(asset) == b"0123456789" + resolved = client.resolve(asset) + assert resolved.path == "folder/a b%.tif" + assert bytes(await obstore.get_range_async(resolved.store, resolved.path, start=2, end=6)) == b"2345" + assert [request["path"] for request in requests] == [ + "/folder/a%20b%25.tif?versionId=abc%2Fdef&download=1", + "/folder/a%20b%25.tif?versionId=abc%2Fdef&download=1", + ] + other_version = _asset(AssetLocation(f"{origin}/folder/a%20b%25.tif?versionId=other")) + assert client.resolve(other_version).store is not resolved.store + + +@pytest.mark.asyncio +async def test_cloud_authentication_is_explicit( + object_server: tuple[str, list[dict[str, Any]]], monkeypatch: pytest.MonkeyPatch +) -> None: + origin, requests = object_server + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "ambient-key") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "ambient-secret") + monkeypatch.setenv("AZURE_STORAGE_SAS_TOKEN", "sig=ambient&se=2099-01-01") + + def local_s3(bucket: str, **kwargs: Any) -> S3Store: + kwargs["aws_endpoint_url_s3"] = origin + return S3Store(bucket, client_options={"allow_http": True}, **kwargs) + + def local_azure(container: str, **kwargs: Any) -> AzureStore: + kwargs["endpoint"] = origin + return AzureStore(container, client_options={"allow_http": True}, **kwargs) + + def local_gcs(bucket: str, **kwargs: Any) -> GCSStore: + return GCSStore(bucket, base_url=origin, client_options={"allow_http": True}, **kwargs) + + monkeypatch.setattr(obstore.store, "S3Store", local_s3) + monkeypatch.setattr(obstore.store, "AzureStore", local_azure) + monkeypatch.setattr(obstore.store, "GCSStore", local_gcs) + client = Client() + for href in ("s3://bucket/a%20b", "gs://bucket/a%20b", "az://account/container/a%20b"): + assert await client.read_bytes(_asset(AssetLocation(href))) == b"0123456789" + assert all("Authorization" not in request["headers"] and "?" not in request["path"] for request in requests) + + auth = AuthenticationScheme(known_type=KnownAuthenticationType.S3) + storage = StorageScheme(known_type=KnownStorageType.AWS_S3, region="us-west-2", requester_pays=True) + asset = _asset(AssetLocation("s3://bucket/a%20b", {"aws": storage}, {"aws": auth})) + with pytest.raises(ValueError, match="explicitly configured credential provider"): + client.resolve(asset) + + async def credentials() -> "S3Credential": + return { + "access_key_id": "explicit-key", + "secret_access_key": "explicit-secret", + "token": "session", + "expires_at": None, + } + + async def other_credentials() -> "S3Credential": + return {"access_key_id": "other-key", "secret_access_key": "other-secret", "token": None, "expires_at": None} + + providers = {"aws": credentials, "alias": credentials, "other": other_credentials} + client = Client(s3_credentials=providers) + providers["aws"] = other_credentials # Existing clients retain their credential bindings. + resolved = client.resolve(asset) + anonymous = replace(asset, primary=replace(asset.primary, authentication_schemes={})) + other = replace(asset, primary=replace(asset.primary, authentication_schemes={"other": auth})) + alias = replace(asset, primary=replace(asset.primary, authentication_schemes={"alias": auth})) + assert client.resolve(anonymous).store is not resolved.store + assert client.resolve(other).store is not resolved.store + assert client.resolve(alias).store is resolved.store + assert await client.read_bytes(asset) == b"0123456789" + headers = requests[-1]["headers"] + assert headers["Authorization"].startswith("AWS4-HMAC-SHA256 Credential=explicit-key/") + assert headers["x-amz-security-token"] == "session" + assert headers["x-amz-request-payer"] == "requester" + assert await Client(s3_credentials=providers).read_bytes(asset) == b"0123456789" + assert requests[-1]["headers"]["Authorization"].startswith("AWS4-HMAC-SHA256 Credential=other-key/") + + @pytest.mark.asyncio async def test_read_stream_and_atomic_download(tmp_path: Path) -> None: source = tmp_path / "source.bin" diff --git a/tilebox-storage/tests/test_planetary_computer.py b/tilebox-storage/tests/test_planetary_computer.py new file mode 100644 index 0000000..a8efd5f --- /dev/null +++ b/tilebox-storage/tests/test_planetary_computer.py @@ -0,0 +1,301 @@ +import asyncio +import json +from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from dataclasses import replace +from datetime import UTC, datetime, timedelta +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread +from typing import Any +from unittest.mock import patch +from urllib.parse import parse_qs, urlsplit + +import niquests +import obstore +import pytest +from obstore.store import AzureStore, HTTPStore +from protobuf.wkt import Struct, Value + +from tilebox.datasets.assets.assets import Asset, AssetLocation +from tilebox.datasets.datasets.stac.v1.authentication_pb import ( + AuthenticationFlow, + AuthenticationParameter, + AuthenticationScheme, + KnownAuthenticationType, + SignedURLFlow, +) +from tilebox.datasets.datasets.stac.v1.storage_pb import KnownStorageType, StorageScheme +from tilebox.storage.aio import Client + + +def _asset(container: str = "modis-061-cogs", path: str = "scene/red%20band.tif") -> Asset: + return Asset( + key="red", + primary=AssetLocation( + f"https://modiseuwest.blob.core.windows.net/{container}/{path}", + storage_schemes={ + "arbitrary-storage-name": StorageScheme( + known_type=KnownStorageType.MICROSOFT_AZURE, + platform="https://{account}.blob.core.windows.net", + account="modiseuwest", + additional_properties={"container": Value.from_python(container)}, + ) + }, + authentication_schemes={ + "arbitrary-auth-name": AuthenticationScheme( + known_type=KnownAuthenticationType.SIGNED_URL, + flows=[ + AuthenticationFlow( + key="authorizationCode", + signed_url=SignedURLFlow( + method="GET", + authorization_api="https://planetarycomputer.microsoft.com/api/sas/v1/sign", + parameters={ + "href": AuthenticationParameter( + location="query", required=True, schema=Struct.from_python({"type": "string"}) + ) + }, + response_field="href", + ), + ) + ], + ) + }, + ), + ) + + +def test_resolution_is_offline_and_shares_stores_by_container() -> None: + client = Client() + asset = _asset() + with patch.object( + niquests.AsyncSession, "__init__", side_effect=AssertionError("resolution must not create a session") + ): + with ThreadPoolExecutor(max_workers=8) as executor: + resolved = list(executor.map(client.resolve, [asset, _asset(path="scene/blue.tif")] * 8)) + assert len({id(item.store) for item in resolved}) == 1 + assert isinstance(resolved[0].store, AzureStore) + assert resolved[0].href == asset.primary.href + assert resolved[0].path == "scene/red band.tif" + assert client.resolve(_asset(container="modis-061")).store is not resolved[0].store + other_auth = _asset() + other_auth.primary.authentication_schemes["arbitrary-auth-name"].description = "different configuration" + assert client.resolve(other_auth).store is resolved[0].store + public = Asset(key="preview", primary=AssetLocation(asset.primary.href)) + assert isinstance(client.resolve(public).store, HTTPStore) + anonymous_azure = replace(asset, primary=replace(asset.primary, authentication_schemes={})) + assert client.resolve(anonymous_azure).store is not resolved[0].store + + +@pytest.mark.parametrize( + ("field", "value"), + [ + ("authorization_api", "https://evil.example/sign?sig=secret"), + ("method", "POST"), + ("response_field", "token"), + ("parameters", {}), + ], +) +def test_rejects_unsupported_signing_flows(field: str, value: Any) -> None: + asset = _asset() + flow = asset.primary.authentication_schemes["arbitrary-auth-name"].flows[0].signed_url + assert flow is not None + setattr(flow, field, value) + with pytest.raises(ValueError, match="unsupported") as error: + Client().resolve(asset) + assert "secret" not in str(error.value) + + +def test_selects_registered_flow_without_requiring_annotation_details() -> None: + asset = _asset() + authentication = asset.primary.authentication_schemes["arbitrary-auth-name"] + flow = authentication.flows[0] + flow.key = "custom-flow-name" + assert flow.signed_url is not None + flow.signed_url.parameters = { + "href": AuthenticationParameter(location="query"), + "optional_hint": AuthenticationParameter(location="query", required=False), + } + authentication.flows.insert( + 0, + AuthenticationFlow(key="other-service", signed_url=SignedURLFlow(authorization_api="https://example.com/sign")), + ) + incompatible = AuthenticationFlow.from_binary(flow.to_binary()) + assert incompatible.signed_url is not None + incompatible.signed_url.method = "POST" + authentication.flows.insert(0, incompatible) + resolved = Client().resolve(asset) + assert isinstance(resolved.store, AzureStore) + assert resolved.path == "scene/red band.tif" + + authentication.flows.append(flow) + with pytest.raises(ValueError, match="multiple supported signed URL flows"): + Client().resolve(asset) + + +@pytest.mark.parametrize( + "href", + [ + "https://otheraccount.blob.core.windows.net/modis-061-cogs/data.tif", + "https://modiseuwest.blob.core.windows.net/modis-061/data.tif", + "https://modiseuwest.blob.core.windows.net.evil.example/modis-061-cogs/data.tif", + "http://modiseuwest.blob.core.windows.net/modis-061-cogs/data.tif", + "https://modiseuwest.blob.core.windows.net/modis-061-cogs/data.tif?sig=secret", + ], +) +def test_rejects_href_mismatches_without_leaking_queries(href: str) -> None: + original = _asset().primary + asset = Asset( + key="red", + primary=AssetLocation(href, original.storage_schemes, original.authentication_schemes), + ) + with pytest.raises(ValueError, match="unable to resolve") as error: + Client().resolve(asset) + assert "secret" not in str(error.value) + + +@pytest.fixture +def pc_server(monkeypatch: pytest.MonkeyPatch) -> Iterator[dict[str, Any]]: + """Exercise real niquests and obstore against a local token and blob service.""" + state: dict[str, Any] = {"tokens": 0, "responses": [], "ttl": 3600, "blob_queries": []} + + class Handler(BaseHTTPRequestHandler): + def do_GET(self) -> None: + parsed = urlsplit(self.path) + if parsed.path.startswith("/api/sas/v1/token/"): + state["tokens"] += 1 + expiry = (datetime.now(UTC) + timedelta(seconds=state["ttl"])).isoformat() + status, payload = ( + state["responses"].pop(0) + if state["responses"] + else (200, {"token": f"sig=token{state['tokens']}&se={expiry}", "msft:expiry": expiry}) + ) + data = json.dumps(payload).encode() + else: + query = parse_qs(parsed.query) + state["blob_queries"].append(query) + status = 200 if query.get("sig") else 403 + data = parsed.path.encode() + self.send_response(status) + self.send_header("Content-Length", str(len(data))) + self.send_header("ETag", '"test-etag"') + self.send_header("Last-Modified", "Mon, 12 Oct 2026 12:00:00 GMT") + self.end_headers() + self.wfile.write(data) + + def log_message(self, format: str, *args: Any) -> None: # noqa: A002 + pass + + server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + origin = f"http://127.0.0.1:{server.server_port}" + original_get = niquests.AsyncSession.get + + async def local_token_get(session: niquests.AsyncSession, url: str, **kwargs: Any) -> Any: + assert url.startswith("https://planetarycomputer.microsoft.com/api/sas/v1/token/modiseuwest/") + return await original_get(session, origin + urlsplit(url).path, **kwargs) + + def local_store(container: str, **kwargs: Any) -> AzureStore: + kwargs["endpoint"] = origin + return AzureStore(container, client_options={"allow_http": True}, **kwargs) + + monkeypatch.setattr(niquests.AsyncSession, "get", local_token_get) + monkeypatch.setattr(obstore.store, "AzureStore", local_store) + try: + yield state + finally: + server.shutdown() + server.server_close() + thread.join() + + +@pytest.mark.asyncio +async def test_reads_share_credentials_and_refresh_on_demand(pc_server: dict[str, Any], tmp_path: Path) -> None: + client = Client() + asset = _asset(path="scene/red.tif") + pc_server["ttl"] = 60 # Inside obstore's five-minute refresh window. + assert await client.read_bytes(asset) == b"/modis-061-cogs/scene/red.tif" + assert pc_server["tokens"] == 1 + await asyncio.sleep(0.15) # Beyond obstore's 100 ms minimum token reuse interval. + pc_server["ttl"] = 3600 + reads = await asyncio.gather(*(client.read_bytes(_asset(path=f"scene/band{i}.tif")) for i in range(8))) + assert reads == [f"/modis-061-cogs/scene/band{i}.tif".encode() for i in range(8)] + assert pc_server["tokens"] == 2 + assert all(query["sig"] == ["token2"] for query in pc_server["blob_queries"][1:]) + await asyncio.sleep(0.15) + destination = await client.download(asset, tmp_path / "red.tif") + assert destination.read_bytes() == b"/modis-061-cogs/scene/red.tif" + assert pc_server["tokens"] == 2 + assert await client.read_bytes(_asset(container="modis-061", path="original.hdf")) == b"/modis-061/original.hdf" + assert pc_server["tokens"] == 3 + + +@pytest.mark.asyncio +async def test_azure_uri_aliases_share_signing_for_objects_and_prefixes(pc_server: dict[str, Any]) -> None: + client = Client() + asset = _asset(path="scene/data.zarr") + original = client.resolve(asset) + for href in ( + "az://modiseuwest/modis-061-cogs/scene/data.zarr", + "azure://modiseuwest/modis-061-cogs/scene/data.zarr", + "abfs://modis-061-cogs@modiseuwest.dfs.core.windows.net/scene/data.zarr", + "abfss://modis-061-cogs@modiseuwest.dfs.core.windows.net/scene/data.zarr", + "abfs://modis-061-cogs/scene/data.zarr", + "abfss://modis-061-cogs/scene/data.zarr", + ): + resolved = client.resolve(replace(asset, primary=replace(asset.primary, href=href))) + assert resolved.store is original.store + assert resolved.path == "scene/data.zarr" + assert resolved.href == href + # A future Zarr reader can use this prefix with the same store for every key. + for key in ("zarr.json", "temperature/c/0/1"): + result = await obstore.get_async(original.store, f"{original.path}/{key}") + assert bytes(await result.bytes_async()) == f"/modis-061-cogs/scene/data.zarr/{key}".encode() + assert pc_server["tokens"] == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("status", [429, 503]) +async def test_retries_transient_token_failures(pc_server: dict[str, Any], status: int) -> None: + pc_server["responses"] = [(status, {"error": "secret"})] * 2 + assert await Client().read_bytes(_asset(path="data.json")) == b"/modis-061-cogs/data.json" + assert pc_server["tokens"] == 3 + + +@pytest.mark.asyncio +async def test_token_retries_are_bounded(pc_server: dict[str, Any]) -> None: + pc_server["responses"] = [(503, {"error": "secret"})] * 3 + with pytest.raises(obstore.exceptions.UnauthenticatedError, match="token request failed") as error: + await Client().read_bytes(_asset()) + assert "secret" not in str(error.value) + assert pc_server["tokens"] == 3 + assert pc_server["blob_queries"] == [] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("status", "payload"), + [ + (403, {"error": "secret"}), + (302, {"href": "https://evil.example/?sig=secret"}), + (200, {"token": "secret", "msft:expiry": "2099-01-01T00:00:00Z"}), + (200, {"token": "sig=secret&se=2099-01-01", "msft:expiry": "2099-01-01T00:00:00"}), + (200, {"token": "sig=secret&se=2000-01-01", "msft:expiry": "2000-01-01T00:00:00Z"}), + (200, {"token": "sig=secret&se=2099-01-01", "msft:expiry": "secret"}), + (200, None), + ], +) +async def test_invalid_credentials_fail_safely_and_are_not_cached( + pc_server: dict[str, Any], status: int, payload: Any +) -> None: + pc_server["responses"] = [(status, payload)] + client = Client() + with pytest.raises(obstore.exceptions.UnauthenticatedError, match="Planetary Computer") as error: + await client.read_bytes(_asset(path="data.json")) + assert "secret" not in str(error.value) + assert pc_server["tokens"] == 1 + assert pc_server["blob_queries"] == [] + assert await client.read_bytes(_asset(path="data.json")) == b"/modis-061-cogs/data.json" + assert pc_server["tokens"] == 2 diff --git a/tilebox-storage/tests/testdata/asset_access.json b/tilebox-storage/tests/testdata/asset_access.json new file mode 100644 index 0000000..db1a36b --- /dev/null +++ b/tilebox-storage/tests/testdata/asset_access.json @@ -0,0 +1,276 @@ +{ + "sentinel1": { + "dataset": "open_data.aws_earth.sentinel1", + "id": "016f5e6b-dce6-41d9-8844-0c904fc39c2f", + "stac_id": "S1B_IW_GRDH_1SDV_20200101T000510_20200101T000539_019616_02512B", + "assets": { + "access_profiles": [ + { + "base_href": "s3://sentinel-s1-l1c/GRD/2020/1/1/IW/DV/S1B_IW_GRDH_1SDV_20200101T000510_20200101T000539_019616_02512B_B140/", + "storage_refs": [ + "sentinel-1-products" + ] + }, + { + "alternate_key": "https", + "base_href": "https://sentinel-s1-l1c.s3.eu-central-1.amazonaws.com/GRD/2020/1/1/IW/DV/S1B_IW_GRDH_1SDV_20200101T000510_20200101T000539_019616_02512B_B140/", + "default_alternate_name": "HTTPS", + "storage_refs": [ + "sentinel-1-products" + ] + } + ], + "assets": [ + { + "key": "vv", + "primary": { + "access_profile_index": 0, + "href": "measurement/iw-vv.tiff" + }, + "alternates": [ + { + "access_profile_index": 1 + } + ] + } + ] + }, + "storage": { + "schemes": { + "earth-search": { + "bucket": "earthsearch-data", + "known_type": "KNOWN_STORAGE_TYPE_AWS_S3", + "platform": "https://{bucket}.s3.{region}.amazonaws.com", + "region": "us-west-2", + "requester_pays": false + }, + "sentinel-1-products": { + "bucket": "sentinel-s1-l1c", + "known_type": "KNOWN_STORAGE_TYPE_AWS_S3", + "platform": "https://{bucket}.s3.{region}.amazonaws.com", + "region": "eu-central-1", + "requester_pays": false + } + } + } + }, + "sentinel2": { + "dataset": "open_data.aws_earth.sentinel2", + "id": "019f1afb-4f9b-138d-74fb-579d4fbffdf0", + "stac_id": "S2B_T11XNM_20260701T000137_L2A", + "assets": { + "access_profiles": [ + { + "base_href": "https://e84-earth-search-sentinel-data.s3.us-west-2.amazonaws.com/sentinel-2-c1-l2a/11/X/NM/2026/7/S2B_T11XNM_20260701T000137_L2A/", + "storage_refs": [ + "earth-search" + ] + }, + { + "alternate_key": "s3", + "base_href": "s3://e84-earth-search-sentinel-data/sentinel-2-c1-l2a/11/X/NM/2026/7/S2B_T11XNM_20260701T000137_L2A/", + "default_alternate_name": "S3", + "storage_refs": [ + "earth-search" + ] + } + ], + "assets": [ + { + "key": "red", + "primary": { + "access_profile_index": 0, + "href": "B04.tif" + }, + "alternates": [ + { + "access_profile_index": 1 + } + ] + } + ] + }, + "storage": { + "schemes": { + "earth-search": { + "bucket": "e84-earth-search-sentinel-data", + "known_type": "KNOWN_STORAGE_TYPE_AWS_S3", + "platform": "https://{bucket}.s3.{region}.amazonaws.com", + "region": "us-west-2", + "requester_pays": false + } + } + } + }, + "wyvern": { + "dataset": "open_data.wyvern.dragonette", + "id": "018f4c88-35ba-0b81-dd1e-e9a684261f3d", + "stac_id": "wyvern_dragonette-001_20240506T061223_fb944bb4_l2a", + "assets": { + "access_profiles": [ + { + "base_href": "https://" + } + ], + "assets": [ + { + "key": "Cloud optimized GeoTiff", + "primary": { + "access_profile_index": 0, + "href": "wyvern-data.com/wyvern_dragonette-001_20240506T061223_fb944bb4_l2a/wyvern_dragonette-001_20240506T061223_fb944bb4_l2a.tiff" + } + } + ] + } + }, + "planet": { + "dataset": "open_data.planet.tanager", + "id": "01922a89-76a6-4259-ae30-cd2832c37028", + "stac_id": "20240925_185504_87_4001", + "assets": { + "access_profiles": [ + { + "base_href": "https://storage.googleapis.com/open-cogs/planet-stac/tanager1-release2-core-imagery/" + }, + { + "alternate_key": "gcs", + "base_href": "gs://open-cogs/planet-stac/tanager1-release2-core-imagery/", + "default_alternate_name": "GCS" + } + ], + "assets": [ + { + "key": "ortho_visual", + "primary": { + "access_profile_index": 0, + "href": "ortho_visual/20240925_185504_87_4001_ortho_visual.tif" + }, + "alternates": [ + { + "access_profile_index": 1 + } + ] + } + ] + } + }, + "satellogic": { + "dataset": "open_data.satellogic.earth_view", + "id": "0181b8fa-7863-1000-3e25-d0621e5fa326", + "stac_id": "20220701_085711_SN18_36N_362863_5817776", + "assets": { + "access_profiles": [ + { + "base_href": "https://satellogic-earthview.s3.us-west-2.amazonaws.com/data/" + }, + { + "alternate_key": "s3", + "base_href": "s3://satellogic-earthview/data/", + "default_alternate_name": "S3" + } + ], + "assets": [ + { + "key": "analytic", + "primary": { + "access_profile_index": 0, + "href": "tif/zone=36N/region=362863_5817776/date=2022-07-01/20220701_085711_SN18_36N_362863_5817776_TOA.tif" + }, + "alternates": [ + { + "access_profile_index": 1 + } + ] + } + ] + } + }, + "planetary_computer": { + "dataset": "tilebox.test_planetary_computer", + "id": "019f539f-a800-22b3-9e87-a25c51beeaf1", + "stac_id": "MOD13Q1.A2026193.h35v10.061.2026210034525", + "storage": { + "schemes": { + "azure_modiseuwest_modis-061": { + "account": "modiseuwest", + "additional_properties": { + "container": "modis-061" + }, + "known_type": "KNOWN_STORAGE_TYPE_MICROSOFT_AZURE", + "platform": "https://{account}.blob.core.windows.net", + "region": "westeurope" + }, + "azure_modiseuwest_modis-061-cogs": { + "account": "modiseuwest", + "additional_properties": { + "container": "modis-061-cogs" + }, + "known_type": "KNOWN_STORAGE_TYPE_MICROSOFT_AZURE", + "platform": "https://{account}.blob.core.windows.net", + "region": "westeurope" + } + } + }, + "authentication": { + "schemes": { + "planetary_computer": { + "flows": [ + { + "key": "authorizationCode", + "signed_url": { + "authorization_api": "https://planetarycomputer.microsoft.com/api/sas/v1/sign", + "method": "GET", + "parameters": { + "href": { + "description": "The complete unsigned href of the selected asset location.", + "location": "query", + "required": true, + "schema": { + "type": "string" + } + } + }, + "response_field": "href" + } + } + ], + "known_type": "KNOWN_AUTHENTICATION_TYPE_SIGNED_URL" + } + } + }, + "assets": { + "access_profiles": [ + { + "base_href": "https://planetarycomputer.microsoft.com/api/data/v1/item/" + }, + { + "auth_refs": [ + "planetary_computer" + ], + "base_href": "https://modiseuwest.blob.core.windows.net/modis-061/MOD13Q1/35/10/2026193/", + "storage_refs": [ + "azure_modiseuwest_modis-061" + ] + }, + { + "auth_refs": [ + "planetary_computer" + ], + "base_href": "https://modiseuwest.blob.core.windows.net/modis-061-cogs/MOD13Q1/35/10/2026193/", + "storage_refs": [ + "azure_modiseuwest_modis-061-cogs" + ] + } + ], + "assets": [ + { + "key": "250m_16_days_NDVI", + "primary": { + "access_profile_index": 2, + "href": "MOD13Q1.A2026193.h35v10.061.2026210034525_250m_16_days_NDVI.tif" + } + } + ] + } + } +} diff --git a/tilebox-storage/tilebox/storage/_authentication.py b/tilebox-storage/tilebox/storage/_authentication.py new file mode 100644 index 0000000..aa170f9 --- /dev/null +++ b/tilebox-storage/tilebox/storage/_authentication.py @@ -0,0 +1,53 @@ +"""Built-in authentication plugins; credentials are never inferred from a URI.""" + +from collections.abc import Mapping +from dataclasses import dataclass, field, replace +from typing import TYPE_CHECKING + +from tilebox.storage._registry import Registry, StoreSpec + +if TYPE_CHECKING: + from collections.abc import Coroutine + from typing import Any + + from obstore.store import S3Credential, S3CredentialProvider + + from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme + + +def register_authentication(registry: Registry, s3_credentials: Mapping[str, "S3CredentialProvider"]) -> None: + from tilebox.storage._planetary_computer import bind_credentials # noqa: PLC0415 + + for backend in ("file", "http", "s3", "gcs", "azure"): + registry.register_authentication(backend, None, _anonymous) + registry.register_authentication("s3", "s3", _S3Authentication(s3_credentials)) + registry.register_signer("azure", "https://planetarycomputer.microsoft.com/api/sas/v1/sign", bind_credentials) + + +def _anonymous( + spec: StoreSpec, path: str, _name: str, _authentication: "AuthenticationScheme | None" +) -> tuple[StoreSpec, str]: + return spec, path + + +@dataclass(frozen=True, eq=False) +class _S3Credentials: + # Identity equality supports even unhashable caller providers, without using secret values as keys. + provider: "S3CredentialProvider" = field(repr=False) + + def __call__(self) -> "S3Credential | Coroutine[Any, Any, S3Credential]": + return self.provider() + + +class _S3Authentication: + def __init__(self, providers: Mapping[str, "S3CredentialProvider"]) -> None: + bindings = {id(provider): _S3Credentials(provider) for provider in providers.values()} + self._credentials = {name: bindings[id(provider)] for name, provider in providers.items()} + + def __call__( + self, spec: StoreSpec, path: str, name: str, _authentication: "AuthenticationScheme | None" + ) -> tuple[StoreSpec, str]: + provider = self._credentials.get(name) + if provider is None: + raise ValueError("S3 authentication requires an explicitly configured credential provider") + return replace(spec, credential_provider=provider), path diff --git a/tilebox-storage/tilebox/storage/_backends.py b/tilebox-storage/tilebox/storage/_backends.py new file mode 100644 index 0000000..841566e --- /dev/null +++ b/tilebox-storage/tilebox/storage/_backends.py @@ -0,0 +1,167 @@ +"""Built-in namespace parsers and obstore factories.""" + +import re +from functools import partial +from pathlib import Path +from typing import TYPE_CHECKING, Any +from urllib.parse import SplitResult, unquote, urlsplit, urlunsplit + +from tilebox.storage._registry import Registry, StoreSpec + +if TYPE_CHECKING: + from obstore.store import ObjectStore + + from tilebox.datasets.datasets.stac.v1.storage_pb import StorageScheme + + +def register_backends(registry: Registry) -> None: + unspecified = (None, "unspecified") + registry.register_backend( + "file", uri_schemes=("file",), storage_types=unspecified, parse=_file, create=_local_store + ) + registry.register_backend( + "s3", + uri_schemes=("s3",), + storage_types=(*unspecified, "aws_s3", "custom_s3"), + parse=_s3, + create=partial(_cloud_store, "S3Store"), + ) + registry.register_backend( + "gcs", + uri_schemes=("gs", "gcs"), + storage_types=(*unspecified, "google_cloud_storage"), + parse=_gcs, + create=partial(_cloud_store, "GCSStore"), + ) + azure_factory = partial(_cloud_store, "AzureStore") + registry.register_backend( + "azure", + uri_schemes=("az", "azure", "abfs", "abfss"), + storage_types=(*unspecified, "microsoft_azure"), + parse=_azure, + create=azure_factory, + ) + registry.register_backend( + "azure", + uri_schemes=("https",), + storage_types=("microsoft_azure",), + parse=_azure, + create=azure_factory, + ) + registry.register_backend( + "http", + uri_schemes=("http", "https"), + storage_types=(*unspecified, "aws_s3", "custom_s3", "google_cloud_storage"), + parse=_http, + create=_http_store, + ) + + +def _unsigned_uri(href: str) -> SplitResult: + parsed = urlsplit(href) + if not parsed.netloc or parsed.query or parsed.fragment or parsed.password: + raise ValueError("expected an unsigned storage URI without query, fragment, or password") + return parsed + + +def _file(href: str, _storage: "StorageScheme | None") -> tuple[StoreSpec, str]: + parsed = urlsplit(href) + if parsed.scheme and (parsed.netloc not in {"", "localhost"} or parsed.query or parsed.fragment): + raise ValueError("expected a local file URI without query or fragment") + path = Path(unquote(parsed.path) if parsed.scheme else href).absolute() + return StoreSpec("file", path.anchor), str(path.relative_to(path.anchor)) + + +def _s3(href: str, storage: "StorageScheme | None") -> tuple[StoreSpec, str]: + parsed = _unsigned_uri(href) + if parsed.username: + raise ValueError("S3 URI must not contain credentials") + options: dict[str, str | bool] = {} + if storage is not None: + if storage.has_field("region"): + options["region"] = storage.region + if storage.has_field("requester_pays"): + options["request_payer"] = storage.requester_pays + if storage.known_type.name == "CUSTOM_S3": + endpoint = urlsplit(storage.platform) + if ( + endpoint.scheme != "https" + or not endpoint.netloc + or endpoint.username + or endpoint.query + or endpoint.fragment + ): + raise ValueError("custom S3 storage requires an HTTPS endpoint without credentials") + options["aws_endpoint_url_s3"] = storage.platform + return StoreSpec("s3", parsed.netloc, tuple(sorted(options.items()))), unquote(parsed.path.lstrip("/")) + + +def _gcs(href: str, _storage: "StorageScheme | None") -> tuple[StoreSpec, str]: + parsed = _unsigned_uri(href) + if parsed.username: + raise ValueError("GCS URI must not contain credentials") + return StoreSpec("gcs", parsed.netloc), unquote(parsed.path.lstrip("/")) + + +def _azure(href: str, storage: "StorageScheme | None") -> tuple[StoreSpec, str]: + parsed = _unsigned_uri(href) + account = storage.account if storage is not None else "" + if parsed.scheme in {"abfs", "abfss"}: + container = parsed.username or parsed.netloc + if parsed.username: + account = (parsed.hostname or "").removesuffix(".dfs.core.windows.net") + if parsed.netloc != f"{container}@{account}.dfs.core.windows.net": + raise ValueError("unsupported Azure DFS authority") + path = parsed.path.lstrip("/") + else: + if parsed.scheme == "https": + account = parsed.netloc.removesuffix(".blob.core.windows.net") + if parsed.netloc != f"{account}.blob.core.windows.net": + raise ValueError("expected an unsigned Azure href matching the account and container") + else: + account = parsed.netloc + container, _, path = parsed.path.lstrip("/").partition("/") + if not re.fullmatch(r"[a-z0-9]{3,24}", account) or not re.fullmatch(r"[a-z0-9][a-z0-9-]{1,61}[a-z0-9]", container): + raise ValueError("Azure storage requires a valid account and container") + origin = f"https://{account}.blob.core.windows.net" + if storage is not None: + declared_container = storage.additional_properties.get("container") + if ( + (storage.account and storage.account != account) + or (declared_container is not None and declared_container.to_python() != container) + or (storage.platform and storage.platform not in {origin, "https://{account}.blob.core.windows.net"}) + ): + raise ValueError("expected an unsigned Azure href matching the account and container") + options = (("account_name", account), ("endpoint", origin), ("use_emulator", False)) + return StoreSpec("azure", container, options), unquote(path) + + +def _http(href: str, _storage: "StorageScheme | None") -> tuple[StoreSpec, str]: + parsed = urlsplit(href) + if not parsed.netloc or parsed.username or parsed.fragment: + raise ValueError("HTTP locations require a host and no userinfo or fragment") + # Queries stay on the base URL. Obstore appends and escapes only object path segments. + origin = urlunsplit((parsed.scheme, parsed.netloc, "", parsed.query, "")) + return StoreSpec("http", origin), unquote(parsed.path.lstrip("/")) + + +def _cloud_store(class_name: str, spec: StoreSpec) -> "ObjectStore": + from obstore import store # noqa: PLC0415 + + options: dict[str, Any] = dict(spec.options) + options["skip_signature"] = spec.credential_provider is None + if spec.credential_provider is not None: + options["credential_provider"] = spec.credential_provider + return getattr(store, class_name)(spec.root, **options) + + +def _local_store(spec: StoreSpec) -> "ObjectStore": + from obstore.store import LocalStore # noqa: PLC0415 + + return LocalStore(spec.root) + + +def _http_store(spec: StoreSpec) -> "ObjectStore": + from obstore.store import HTTPStore # noqa: PLC0415 + + return HTTPStore(spec.root, client_options={"allow_http": spec.root.startswith("http://")}) diff --git a/tilebox-storage/tilebox/storage/_planetary_computer.py b/tilebox-storage/tilebox/storage/_planetary_computer.py new file mode 100644 index 0000000..b0f7b7f --- /dev/null +++ b/tilebox-storage/tilebox/storage/_planetary_computer.py @@ -0,0 +1,95 @@ +"""Planetary Computer metadata validation and lazy Azure SAS credentials.""" + +from dataclasses import dataclass, replace +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any +from urllib.parse import parse_qsl + +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential + +from tilebox.storage._registry import StoreSpec + +if TYPE_CHECKING: + from niquests import AsyncResponse, AsyncSession, Response + from obstore.store import AzureSASToken + + from tilebox.datasets.datasets.stac.v1.authentication_pb import SignedURLFlow + + +def bind_credentials(spec: StoreSpec, path: str, signed: "SignedURLFlow") -> tuple[StoreSpec, str]: + """Adapt the advertised signing flow to container-scoped Azure credentials.""" + if signed.method != "GET" or signed.response_field != "href": + raise ValueError("unsupported Planetary Computer signed URL flow") + parameter = signed.parameters.get("href") + if ( + parameter is None + or parameter.location != "query" + or any(parameter.required for name, parameter in signed.parameters.items() if name != "href") + ): + raise ValueError("unsupported Planetary Computer signing parameters") + + account = str(dict(spec.options)["account_name"]) + return replace(spec, credential_provider=_PlanetaryComputerCredentialProvider(account, spec.root)), path + + +@dataclass(frozen=True) +class _PlanetaryComputerCredentialProvider: + account: str + container: str + + async def __call__(self) -> "AzureSASToken": + from niquests import AsyncSession # noqa: PLC0415 + from niquests.exceptions import RequestException # noqa: PLC0415 + + # The session belongs to this fetch, not to the storage client's lifetime or event loop. + async with AsyncSession() as session: + session.trust_env = False # Never forward netrc credentials to the signing service. + try: + url = f"https://planetarycomputer.microsoft.com/api/sas/v1/token/{self.account}/{self.container}" + response = await _request_token(session, url) + except RequestException: + raise ValueError("Planetary Computer token request failed") from None + if response.status_code != 200: + raise ValueError("Planetary Computer token endpoint did not return HTTP 200") + try: + return _parse_credentials(response.json()) + except (KeyError, TypeError, ValueError): + # Neither response bodies nor HTTP/JSON errors are safe to include: they may contain SAS tokens. + raise ValueError("Planetary Computer returned invalid or expired SAS credentials") from None + + +def _is_transient(error: BaseException) -> bool: + from niquests.exceptions import ConnectionError as HTTPConnectionError # noqa: PLC0415 + from niquests.exceptions import HTTPError, Timeout # noqa: PLC0415 + + if isinstance(error, (HTTPConnectionError, Timeout)): + return True + if isinstance(error, HTTPError) and error.response is not None: + status = error.response.status_code or 0 + return status == 429 or 500 <= status < 600 + return False + + +@retry( + retry=retry_if_exception(_is_transient), + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=0.5), + reraise=True, +) +async def _request_token(session: "AsyncSession", url: str) -> "Response | AsyncResponse": + response = await session.get(url, timeout=30, allow_redirects=False) + response.raise_for_status() + return response + + +def _parse_credentials(payload: Any) -> "AzureSASToken": + token = payload["token"] + if not isinstance(token, str) or not token or any(char.isspace() for char in token): + raise ValueError("invalid SAS token") + fields = dict(parse_qsl(token.lstrip("?"), strict_parsing=True)) + if not fields.get("sig") or not fields.get("se"): + raise ValueError("invalid SAS token") + expires_at = datetime.fromisoformat(payload["msft:expiry"]) + if expires_at.tzinfo is None or expires_at <= datetime.now(UTC): + raise ValueError("invalid SAS expiry") + return {"sas_token": token, "expires_at": expires_at.astimezone(UTC)} diff --git a/tilebox-storage/tilebox/storage/_registry.py b/tilebox-storage/tilebox/storage/_registry.py new file mode 100644 index 0000000..1b62a5d --- /dev/null +++ b/tilebox-storage/tilebox/storage/_registry.py @@ -0,0 +1,193 @@ +"""Internal, per-client dispatch for asset storage and authentication. + +Plugins resolve metadata without I/O. Store construction and credential fetching +are separate: the client caches stores, and obstore refreshes their credentials. +""" + +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any +from urllib.parse import urlsplit + +if TYPE_CHECKING: + from obstore.store import ObjectStore, S3CredentialProvider + + from tilebox.datasets.assets.assets import AssetLocation + from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme, SignedURLFlow + from tilebox.datasets.datasets.stac.v1.storage_pb import StorageScheme + + +@dataclass(frozen=True, slots=True) +class StoreSpec: + """Store construction inputs, also used as the client's cache key. + + ``root`` is a bucket, container, filesystem root, or HTTP base URL. Options + must be immutable and sorted. Credential providers must be hashable: equality + means the same credential identity and scope, never the current token value. + Object keys and dataset prefixes belong outside this spec. + """ + + backend: str + root: str = field(repr=False) + options: tuple[tuple[str, str | bool], ...] = () + credential_provider: Callable[[], Any] | None = field(default=None, repr=False) + + +BackendParser = Callable[[str, "StorageScheme | None"], tuple[StoreSpec, str]] +StoreFactory = Callable[[StoreSpec], "ObjectStore"] +AuthenticationResolver = Callable[[StoreSpec, str, str, "AuthenticationScheme | None"], tuple[StoreSpec, str]] +SigningResolver = Callable[[StoreSpec, str, "SignedURLFlow"], tuple[StoreSpec, str]] + + +class Registry: + """An explicit set of supported backends, authentication types, and signers. + + Each client owns its built-in registrations. This is an implementation detail, + not a public plugin API. No plugins are discovered from catalog metadata. + """ + + def __init__(self) -> None: + self._backends: dict[tuple[str, str | None], BackendParser] = {} + self._factories: dict[str, StoreFactory] = {} + self._authentication: dict[tuple[str, str | None], AuthenticationResolver] = {} + self._signers: dict[tuple[str, str], SigningResolver] = {} + + @classmethod + def default(cls, *, s3_credentials: Mapping[str, "S3CredentialProvider"] | None = None) -> "Registry": + """Create built-ins, optionally binding S3 auth scheme names to providers. + + Providers are invoked on reads, not during resolution. Environment or + profile discovery must be implemented by an explicitly supplied provider. + """ + from tilebox.storage._authentication import register_authentication # noqa: PLC0415 + from tilebox.storage._backends import register_backends # noqa: PLC0415 + + registry = cls() + register_backends(registry) + register_authentication(registry, s3_credentials or {}) + return registry + + def register_backend( + self, + name: str, + *, + uri_schemes: tuple[str, ...], + storage_types: tuple[str | None, ...], + parse: BackendParser, + create: StoreFactory, + ) -> None: + """Register one backend's supported URI/storage combinations and factory. + + Parsers validate metadata and return a spec with ``backend=name`` plus a + decoded key or prefix. Raise ValueError with a credential-free explanation + for unsupported metadata. Neither parsing nor construction may fetch data. + """ + keys = [(uri, storage) for uri in uri_schemes for storage in storage_types] + if (name in self._factories and self._factories[name] != create) or any(key in self._backends for key in keys): + raise ValueError("backend registration overlaps an existing registration") + self._factories[name] = create + self._backends.update(dict.fromkeys(keys, parse)) + + def register_authentication( + self, backend: str, authentication_type: str | None, resolver: AuthenticationResolver + ) -> None: + """Register a compatible backend/auth pair; None means explicitly anonymous. + + Resolvers receive the spec, path, scheme name, and scheme metadata. They + return the authenticated spec and path, possibly for a different backend. + ``signed_url`` is reserved; use ``register_signer`` for signing adapters. + """ + if authentication_type == "signed_url": + raise ValueError("signed_url authentication is handled by the registry; use register_signer") + key = (backend, authentication_type) + if key in self._authentication: + raise ValueError("authentication registration overlaps an existing registration") + self._authentication[key] = resolver + + def register_signer(self, backend: str, authorization_api: str, resolver: SigningResolver) -> None: + """Register a trusted signed-URL adapter for one backend and exact API. + + Adapters receive the spec, path, and flow, and return a spec/path pair. + They validate the flow and bind credentials with their actual scope. A + per-object URL signer must not claim container-wide credentials. + """ + key = (backend, authorization_api) + if key in self._signers: + raise ValueError("signer registration overlaps an existing registration") + self._signers[key] = resolver + + def resolve( + self, location: "AssetLocation" + ) -> "tuple[StoreSpec, str, StorageScheme | None, AuthenticationScheme | None]": + """Select exactly one supported combination, without creating a store.""" + supported = [] + rejected = [] + uri = urlsplit(location.href).scheme.lower() or "file" + for storage in location.storage_schemes.values() or [None]: + parser = self._backends.get((uri, _storage_type(storage))) + if parser is None: + rejected.append("unsupported URI/storage combination") + continue + try: + spec, path = parser(location.href, storage) + except ValueError as error: + rejected.append(str(error)) + continue + for name, authentication in location.authentication_schemes.items() or [("", None)]: + kind = _authentication_type(authentication) + resolver = self._signed_url if kind == "signed_url" else self._authentication.get((spec.backend, kind)) + if resolver is None: + rejected.append(f"authentication type {kind!r} is unsupported for {spec.backend}") + continue + try: + authenticated, authenticated_path = resolver(spec, path, name, authentication) + except ValueError as error: + rejected.append(str(error)) + continue + supported.append((authenticated, authenticated_path, storage, authentication)) + if not supported: + raise ValueError("; ".join(dict.fromkeys(rejected))) + if len(supported) > 1: + raise ValueError("multiple supported storage/authentication combinations are applicable") + return supported[0] + + def create_store(self, spec: StoreSpec) -> "ObjectStore": + return self._factories[spec.backend](spec) + + def _signed_url( + self, spec: StoreSpec, path: str, _name: str, authentication: "AuthenticationScheme | None" + ) -> tuple[StoreSpec, str]: + assert authentication is not None + supported = [] + for flow in authentication.flows: + signed = flow.signed_url + if signed is None or flow.oauth2 is not None: + continue + resolver = self._signers.get((spec.backend, signed.authorization_api)) + if resolver is None: + continue + try: + supported.append(resolver(spec, path, signed)) + except ValueError: + continue + if not supported: + raise ValueError("unsupported signed URL flow for this backend") + if len(supported) > 1: + raise ValueError("multiple supported signed URL flows are applicable") + return supported[0] + + +def _storage_type(storage: "StorageScheme | None") -> str | None: + if storage is None: + return None + if storage.has_field("custom_type"): + return storage.custom_type + return storage.known_type.name.lower() + + +def _authentication_type(authentication: "AuthenticationScheme | None") -> str | None: + if authentication is None: + return None + if authentication.has_field("custom_type"): + return authentication.custom_type + return authentication.known_type.name.lower() diff --git a/tilebox-storage/tilebox/storage/client.py b/tilebox-storage/tilebox/storage/client.py index a025438..0cf2bda 100644 --- a/tilebox-storage/tilebox/storage/client.py +++ b/tilebox-storage/tilebox/storage/client.py @@ -7,12 +7,14 @@ from dataclasses import dataclass from os import PathLike from pathlib import Path -from typing import TYPE_CHECKING, Any -from urllib.parse import unquote, urlsplit +from typing import TYPE_CHECKING +from urllib.parse import urlsplit + +from tilebox.storage._registry import Registry, StoreSpec if TYPE_CHECKING: from async_geotiff import GeoTIFF - from obstore.store import ObjectStore + from obstore.store import ObjectStore, S3CredentialProvider from tilebox.datasets.assets.assets import Asset, AssetLocation from tilebox.datasets.datasets.stac.v1.authentication_pb import AuthenticationScheme @@ -23,7 +25,7 @@ class AssetAccessPolicy: """Policy controlling deterministic asset-location selection.""" - preferred_schemes: tuple[str, ...] = ("file", "s3", "gs", "az", "https", "http") + preferred_schemes: tuple[str, ...] = ("file", "s3", "gs", "gcs", "az", "azure", "abfs", "abfss", "https", "http") @dataclass(frozen=True, slots=True) @@ -32,25 +34,39 @@ class ResolvedAsset: asset: "Asset" location: "AssetLocation" - href: str store: "ObjectStore" path: str storage_scheme: "StorageScheme | None" authentication_scheme: "AuthenticationScheme | None" + @property + def href(self) -> str: + """The original selected location, never a refreshed signed URL.""" + return self.location.href + class Client: """Resolve and access dataset assets using reusable object stores.""" - def __init__(self, *, policy: AssetAccessPolicy | None = None) -> None: + def __init__( + self, + *, + policy: AssetAccessPolicy | None = None, + s3_credentials: Mapping[str, "S3CredentialProvider"] | 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. + s3_credentials: Explicit S3 credential providers keyed by the location's + authentication scheme name. The mapping is copied at construction; + providers are invoked on reads, not during resolution. Locations + without authentication metadata always use unsigned cloud requests. """ self.policy = policy or AssetAccessPolicy() - self._stores: dict[tuple[Any, ...], ObjectStore] = {} + self._registry = Registry.default(s3_credentials=s3_credentials) + self._stores: dict[StoreSpec, ObjectStore] = {} self._stores_lock = threading.RLock() def resolve(self, asset: "Asset") -> ResolvedAsset: @@ -72,94 +88,22 @@ def resolve(self, asset: "Asset") -> ResolvedAsset: 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) + spec, path, storage, authentication = self._registry.resolve(location) + store = self._store_for(spec) except (TypeError, ValueError) as error: - rejected.append(f"{label} ({location.href!r}): {error}") + rejected.append(f"{label}: {error}") continue - return ResolvedAsset(asset, location, location.href, store, path, storage, authentication) + return ResolvedAsset(asset, location, 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, PLR0915 - 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) - 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": - 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 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" and storage.platform 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"}: - 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 - 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}") - + def _store_for(self, spec: StoreSpec) -> "ObjectStore": with self._stores_lock: - store = self._stores.get(identity) + store = self._stores.get(spec) if store is None: - store = constructor() - self._stores[identity] = store - return store, path + store = self._registry.create_store(spec) + self._stores[spec] = store + return store async def read_bytes( self, @@ -279,47 +223,6 @@ 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) - raise ValueError(f"multiple {kind} schemes are applicable ({keys})") - return next(iter(schemes.values()), None) - - _default_client = Client() diff --git a/tilebox-workflows/tests/test_data.py b/tilebox-workflows/tests/test_data.py index e7e6e85..bc66297 100644 --- a/tilebox-workflows/tests/test_data.py +++ b/tilebox-workflows/tests/test_data.py @@ -48,9 +48,11 @@ TaskSubmissionGroup, TaskSubmissions, Workflow, + WorkflowPresentation, WorkflowRelease, ) from tilebox.workflows.formatting.job import JobWidget, RichDisplayJob +from tilebox.workflows.workflows.v1 import workflows_pb2 @given(task_identifiers()) @@ -165,6 +167,36 @@ def test_workflows_to_message_and_back(workflow: Workflow) -> None: assert Workflow.from_message(workflow.to_message()) == workflow +@pytest.mark.parametrize( + "presentation", + [ + None, + WorkflowPresentation(), + WorkflowPresentation( + image_url="https://example.com/preview.png", + image_alt="Flood extent", + imagery_type=["SAR", "optical"], + use_case=["flood monitoring"], + dataset_label=["Sentinel-1", "Sentinel-2"], + ), + ], +) +def test_workflow_summary_and_presentation(presentation: WorkflowPresentation | None) -> None: + message = workflows_pb2.Workflow( + slug="flood-extent", + name="Flood extent", + summary="Map flooded areas", + description="A longer description of the workflow.", + presentation=workflows_pb2.WorkflowPresentation(**vars(presentation)) if presentation is not None else None, + ) + workflow = Workflow.from_message(message) + assert workflow.summary == "Map flooded areas" + assert workflow.description == "A longer description of the workflow." + assert workflow.presentation == presentation + assert workflow.to_message() == message + assert workflow.to_message().HasField("presentation") == (presentation is not None) + + @given(task_submissions()) def test_task_submissions_to_message_and_back(sub_task: TaskSubmissions) -> None: assert TaskSubmissions.from_message(sub_task.to_message()) == sub_task diff --git a/tilebox-workflows/tests/workflows/test_client.py b/tilebox-workflows/tests/workflows/test_client.py index 84b47e2..3f99762 100644 --- a/tilebox-workflows/tests/workflows/test_client.py +++ b/tilebox-workflows/tests/workflows/test_client.py @@ -53,7 +53,7 @@ def __init__(self) -> None: def CreateWorkflow(self, req: CreateWorkflowRequest) -> WorkflowMessage: # noqa: N802 slug = req.name.lower().replace(" ", "-") - workflow = WorkflowMessage(slug=slug, name=req.name, description=req.description) + workflow = WorkflowMessage(slug=slug, name=req.name, description=req.description, summary=req.summary) self.workflows[workflow.slug] = workflow return workflow @@ -76,6 +76,8 @@ def UpdateWorkflow(self, req: UpdateWorkflowRequest) -> WorkflowMessage: # noqa workflow.name = req.name if req.HasField("description"): workflow.description = req.description + if req.HasField("summary"): + workflow.summary = req.summary self.workflows[workflow.slug] = workflow return workflow @@ -108,11 +110,12 @@ def test_create_workflow() -> None: service.service = MockWorkflowService() workflow_client = WorkflowClient(service) - workflow = workflow_client.create("Agentic Workflow", description="Description") + workflow = workflow_client.create("Agentic Workflow", "Short summary", "Description") assert workflow.slug == "agentic-workflow" assert workflow.name == "Agentic Workflow" assert workflow.description == "Description" + assert workflow.summary == "Short summary" def test_list_find_and_delete_workflows() -> None: @@ -120,8 +123,9 @@ def test_list_find_and_delete_workflows() -> None: mock_service = MockWorkflowService() service.service = mock_service workflow_client = WorkflowClient(service) - workflow = workflow_client.create("Agentic Workflow") + workflow = workflow_client.create("Agentic Workflow", "Short summary") + assert workflow.description == "" assert workflow_client.all() == [workflow] assert workflow_client.find(workflow.slug) == workflow @@ -135,9 +139,9 @@ def test_update_workflow() -> None: mock_service = MockWorkflowService() service.service = mock_service workflow_client = WorkflowClient(service) - workflow = workflow_client.create("Agentic Workflow", description="Description") + workflow = workflow_client.create("Agentic Workflow", "Short summary", "Description") - updated_workflow = workflow_client.update(workflow, name="Updated Workflow", description="Updated description") + updated_workflow = workflow_client.update(workflow, "Updated Workflow", "Updated summary", "Updated description") request = mock_service.update_requests[-1] assert request.workflow_slug == workflow.slug @@ -146,6 +150,7 @@ def test_update_workflow() -> None: assert request.name == "Updated Workflow" assert request.description == "Updated description" assert updated_workflow.name == "Updated Workflow" + assert updated_workflow.summary == "Updated summary" assert updated_workflow.description == "Updated description" @@ -154,19 +159,35 @@ def test_update_workflow_preserves_optional_presence() -> None: mock_service = MockWorkflowService() service.service = mock_service workflow_client = WorkflowClient(service) - workflow = workflow_client.create("Agentic Workflow", description="Description") + workflow = workflow_client.create("Agentic Workflow", description="Description", summary="Short summary") - workflow_client.update(workflow.slug) + unchanged = workflow_client.update(workflow.slug) + assert unchanged == workflow request = mock_service.update_requests[-1] assert not request.HasField("name") assert not request.HasField("description") + assert not request.HasField("summary") - updated_workflow = workflow_client.update(workflow.slug, description="") + unchanged = workflow_client.update(workflow.slug, name=None, summary=None, description=None) + assert unchanged == workflow + + updated_workflow = workflow_client.update(workflow.slug, summary=None, description="") request = mock_service.update_requests[-1] assert not request.HasField("name") + assert not request.HasField("summary") assert request.HasField("description") assert request.description == "" assert updated_workflow.description == "" + assert updated_workflow.summary == "Short summary" + + updated_workflow = workflow_client.update(workflow.slug, summary="Updated summary") + assert updated_workflow.summary == "Updated summary" + assert updated_workflow.description == "" + updated_workflow = workflow_client.update(workflow.slug, description="Restored description") + updated_workflow = workflow_client.update(workflow.slug, summary="", description=None) + assert updated_workflow.summary == "" + assert updated_workflow.description == "Restored description" + assert workflow_client.find(workflow.slug) == updated_workflow def test_unpublish_release() -> None: diff --git a/tilebox-workflows/tilebox/workflows/data.py b/tilebox-workflows/tilebox/workflows/data.py index c19dccf..cf00588 100644 --- a/tilebox-workflows/tilebox/workflows/data.py +++ b/tilebox-workflows/tilebox/workflows/data.py @@ -424,12 +424,42 @@ def to_message(self) -> workflows_pb2.ReleaseContent: ) +@dataclass(order=True, frozen=True) +class WorkflowPresentation: + image_url: str = "" + image_alt: str = "" + imagery_type: list[str] = field(default_factory=list) + use_case: list[str] = field(default_factory=list) + dataset_label: list[str] = field(default_factory=list) + + @classmethod + def from_message(cls, presentation: workflows_pb2.WorkflowPresentation) -> "WorkflowPresentation": + return cls( + image_url=presentation.image_url, + image_alt=presentation.image_alt, + imagery_type=list(presentation.imagery_type), + use_case=list(presentation.use_case), + dataset_label=list(presentation.dataset_label), + ) + + def to_message(self) -> workflows_pb2.WorkflowPresentation: + return workflows_pb2.WorkflowPresentation( + image_url=self.image_url, + image_alt=self.image_alt, + imagery_type=self.imagery_type, + use_case=self.use_case, + dataset_label=self.dataset_label, + ) + + @dataclass(order=True, frozen=True) class Workflow: slug: str name: str description: str releases: list["WorkflowRelease"] = field(default_factory=list) + summary: str = "" + presentation: WorkflowPresentation | None = field(default=None, compare=False) @classmethod def from_message(cls, workflow: workflows_pb2.Workflow) -> "Workflow": @@ -439,6 +469,10 @@ def from_message(cls, workflow: workflows_pb2.Workflow) -> "Workflow": name=workflow.name, description=workflow.description, releases=[WorkflowRelease.from_message(release) for release in workflow.releases], + summary=workflow.summary, + presentation=WorkflowPresentation.from_message(workflow.presentation) + if workflow.HasField("presentation") + else None, ) def to_message(self) -> workflows_pb2.Workflow: @@ -448,6 +482,8 @@ def to_message(self) -> workflows_pb2.Workflow: name=self.name, description=self.description, releases=[release.to_message() for release in self.releases], + summary=self.summary, + presentation=self.presentation.to_message() if self.presentation is not None else None, ) diff --git a/tilebox-workflows/tilebox/workflows/workflows/client.py b/tilebox-workflows/tilebox/workflows/workflows/client.py index ea0a7bf..26f0047 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/client.py +++ b/tilebox-workflows/tilebox/workflows/workflows/client.py @@ -18,17 +18,18 @@ def __init__(self, service: WorkflowService) -> None: """ self._service = service - def create(self, name: str, description: str = "") -> Workflow: + def create(self, name: str, summary: str, description: str = "") -> Workflow: """Create a new workflow with the given name. Args: name: The name of the workflow to create. - description: The description of the workflow to create. + summary: The short summary of the workflow to create. + description: The description of the workflow to create. Defaults to an empty string. Returns: The created workflow. """ - return self._service.create(name, description) + return self._service.create(name, summary, description) def all(self) -> list[Workflow]: """List all available workflows. @@ -53,6 +54,7 @@ def update( self, workflow_or_slug: WorkflowSlugLike, name: str | None = None, + summary: str | None = None, description: str | None = None, ) -> Workflow: """Update a workflow by slug. @@ -60,12 +62,13 @@ def update( Args: workflow_or_slug: The workflow or slug of the workflow to update. name: The new display name for the workflow. If not provided, the name is left unchanged. - description: The new description for the workflow. If not provided, the description is left unchanged. + summary: The new short summary. ``None`` leaves it unchanged; ``""`` clears it. + description: The new description. ``None`` leaves it unchanged; ``""`` clears it. Returns: The updated workflow. """ - return self._service.update(to_workflow_slug(workflow_or_slug), name, description) + return self._service.update(to_workflow_slug(workflow_or_slug), name, summary, description) def delete(self, workflow_or_slug: WorkflowSlugLike) -> None: """Delete a workflow by slug. diff --git a/tilebox-workflows/tilebox/workflows/workflows/service.py b/tilebox-workflows/tilebox/workflows/workflows/service.py index 2635122..afc9b81 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/service.py +++ b/tilebox-workflows/tilebox/workflows/workflows/service.py @@ -37,8 +37,8 @@ def __init__(self, channel: Channel | Any) -> None: with_pythonic_errors(WorkflowsServiceStub(channel)) if hasattr(channel, "unary_unary") else channel ) - def create(self, name: str, description: str = "") -> Workflow: - request = CreateWorkflowRequest(name=name, description=description) + def create(self, name: str, summary: str, description: str = "") -> Workflow: + request = CreateWorkflowRequest(name=name, summary=summary, description=description) return Workflow.from_message(self.service.CreateWorkflow(request)) def list_all(self) -> list[Workflow]: @@ -50,8 +50,16 @@ def get_by_slug(self, workflow_slug: str) -> Workflow: request = GetWorkflowRequest(workflow_slug=workflow_slug) return Workflow.from_message(self.service.GetWorkflow(request)) - def update(self, workflow_slug: str, name: str | None = None, description: str | None = None) -> Workflow: - request = UpdateWorkflowRequest(workflow_slug=workflow_slug, name=name, description=description) + def update( + self, + workflow_slug: str, + name: str | None = None, + summary: str | None = None, + description: str | None = None, + ) -> Workflow: + request = UpdateWorkflowRequest( + workflow_slug=workflow_slug, name=name, summary=summary, description=description + ) return Workflow.from_message(self.service.UpdateWorkflow(request)) def delete(self, workflow_slug: str) -> None: diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_connect.py b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_connect.py index 63b2cb2..71defb2 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_connect.py +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_connect.py @@ -50,6 +50,9 @@ async def delete_storage_subscription(self, request: tilebox_dot_v1_dot_id__pb2. async def list_storage_subscription_events(self, request: workflows_dot_v1_dot_storage__location__pb2.ListStorageSubscriptionEventsRequest, ctx: RequestContext) -> workflows_dot_v1_dot_storage__location__pb2.StorageSubscriptionEvents: raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + async def notify_object_created(self, request: workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, ctx: RequestContext) -> google_dot_protobuf_dot_empty__pb2.Empty: + raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + class StorageLocationServiceASGIApplication(ConnectASGIApplication[StorageLocationService]): def __init__(self, service: StorageLocationService | AsyncGenerator[StorageLocationService], *, interceptors: Iterable[Interceptor]=(), read_max_bytes: int | None = None, compressions: Iterable[Compression] | None = None, codecs: Iterable[Codec] | None = None) -> None: @@ -156,6 +159,16 @@ def __init__(self, service: StorageLocationService | AsyncGenerator[StorageLocat ), function=svc.list_storage_subscription_events, ), + "/workflows.v1.StorageLocationService/NotifyObjectCreated": Endpoint.unary( + method=MethodInfo( + name="NotifyObjectCreated", + service_name="workflows.v1.StorageLocationService", + input=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + output=google_dot_protobuf_dot_empty__pb2.Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=svc.notify_object_created, + ), }, interceptors=interceptors, read_max_bytes=read_max_bytes, @@ -370,6 +383,26 @@ async def list_storage_subscription_events( timeout_ms=timeout_ms, ) + async def notify_object_created( + self, + request: workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> google_dot_protobuf_dot_empty__pb2.Empty: + return await self.execute_unary( + request=request, + method=MethodInfo( + name="NotifyObjectCreated", + service_name="workflows.v1.StorageLocationService", + input=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + output=google_dot_protobuf_dot_empty__pb2.Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) + @@ -395,6 +428,8 @@ def delete_storage_subscription(self, request: tilebox_dot_v1_dot_id__pb2.ID, ct raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") def list_storage_subscription_events(self, request: workflows_dot_v1_dot_storage__location__pb2.ListStorageSubscriptionEventsRequest, ctx: RequestContext) -> workflows_dot_v1_dot_storage__location__pb2.StorageSubscriptionEvents: raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") + def notify_object_created(self, request: workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, ctx: RequestContext) -> google_dot_protobuf_dot_empty__pb2.Empty: + raise ConnectError(Code.UNIMPLEMENTED, "Not implemented") class StorageLocationServiceWSGIApplication(ConnectWSGIApplication): @@ -501,6 +536,16 @@ def __init__(self, service: StorageLocationServiceSync, interceptors: Iterable[I ), function=service.list_storage_subscription_events, ), + "/workflows.v1.StorageLocationService/NotifyObjectCreated": EndpointSync.unary( + method=MethodInfo( + name="NotifyObjectCreated", + service_name="workflows.v1.StorageLocationService", + input=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + output=google_dot_protobuf_dot_empty__pb2.Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + function=service.notify_object_created, + ), }, interceptors=interceptors, read_max_bytes=read_max_bytes, @@ -714,3 +759,23 @@ def list_storage_subscription_events( headers=headers, timeout_ms=timeout_ms, ) + + def notify_object_created( + self, + request: workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + *, + headers: Headers | Mapping[str, str] | None = None, + timeout_ms: int | None = None, + ) -> google_dot_protobuf_dot_empty__pb2.Empty: + return self.execute_unary( + request=request, + method=MethodInfo( + name="NotifyObjectCreated", + service_name="workflows.v1.StorageLocationService", + input=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest, + output=google_dot_protobuf_dot_empty__pb2.Empty, + idempotency_level=IdempotencyLevel.UNKNOWN, + ), + headers=headers, + timeout_ms=timeout_ms, + ) diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.py b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.py index b332b4b..67849d3 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.py +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.py @@ -29,7 +29,7 @@ from tilebox.datasets.tilebox.v1 import query_pb2 as tilebox_dot_v1_dot_query__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#workflows/v1/storage_location.proto\x12\x0cworkflows.v1\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\"\xde\x01\n\x0fStorageLocation\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1e\n\x08location\x18\x02 \x01(\tB\x02\x18\x01R\x08location\x12\x31\n\x04type\x18\x03 \x01(\x0e\x32\x19.workflows.v1.StorageTypeB\x02\x18\x01R\x04type\x12\x12\n\x04name\x18\x04 \x01(\tR\x04name\x12\x44\n\treference\x18\x05 \x01(\x0b\x32&.workflows.v1.StorageLocationReferenceR\treference\"\xc7\x02\n\x18StorageLocationReference\x12\x46\n\raws_s3_bucket\x18\x01 \x01(\x0b\x32\".workflows.v1.AWSS3BucketReferenceR\x0b\x61wsS3Bucket\x12?\n\ngcs_bucket\x18\x02 \x01(\x0b\x32 .workflows.v1.GCSBucketReferenceR\tgcsBucket\x12?\n\nazure_blob\x18\x03 \x01(\x0b\x32 .workflows.v1.AzureBlobReferenceR\tazureBlob\x12\x32\n\x05local\x18\x04 \x01(\x0b\x32\x1c.workflows.v1.LocalReferenceR\x05local\x12-\n\x04type\x18\x05 \x01(\x0e\x32\x19.workflows.v1.StorageTypeR\x04type\"F\n\x14\x41WSS3BucketReference\x12\x16\n\x06\x62ucket\x18\x01 \x01(\tR\x06\x62ucket\x12\x16\n\x06region\x18\x02 \x01(\tR\x06region\"g\n\x12GCSBucketReference\x12\x16\n\x06\x62ucket\x18\x01 \x01(\tR\x06\x62ucket\x12\x1d\n\nproject_id\x18\x02 \x01(\tR\tprojectId\x12\x1a\n\x08location\x18\x03 \x01(\tR\x08location\"\x89\x01\n\x12\x41zureBlobReference\x12=\n\x1bstorage_account_resource_id\x18\x01 \x01(\tR\x18storageAccountResourceId\x12\x1c\n\tcontainer\x18\x02 \x01(\tR\tcontainer\x12\x16\n\x06region\x18\x03 \x01(\tR\x06region\"$\n\x0eLocalReference\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\"\x8d\x01\n\x0cTriggeredJob\x12%\n\x06job_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x05jobId\x12\x33\n\rautomation_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x0c\x61utomationId\x12!\n\x0cglob_pattern\x18\x03 \x01(\tR\x0bglobPattern\"R\n\rTriggeredJobs\x12\x41\n\x0etriggered_jobs\x18\x01 \x03(\x0b\x32\x1a.workflows.v1.TriggeredJobR\rtriggeredJobs\"x\n\x1c\x43reateStorageLocationRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x44\n\treference\x18\x02 \x01(\x0b\x32&.workflows.v1.StorageLocationReferenceR\treference\"r\n\x1cUpdateStorageLocationRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\"_\n\x19\x41WSSNSStorageSubscription\x12\x1b\n\ttopic_arn\x18\x01 \x01(\tR\x08topicArn\x12%\n\x0emessage_format\x18\x02 \x01(\tR\rmessageFormat\"\x9a\x01\n\x1fGooglePubSubStorageSubscription\x12\"\n\x0csubscription\x18\x01 \x01(\tR\x0csubscription\x12\x32\n\x15service_account_email\x18\x02 \x01(\tR\x13serviceAccountEmail\x12\x1f\n\x08\x61udience\x18\x03 \x01(\tB\x03\xe0\x41\x03R\x08\x61udience\"\xbd\x01\n!AzureEventGridStorageSubscription\x12\x37\n\x15webhook_secret_header\x18\x01 \x01(\tB\x03\xe0\x41\x03R\x13webhookSecretHeader\x12*\n\x0ewebhook_secret\x18\x02 \x01(\tB\x03\xe0\x41\x03R\rwebhookSecret\x12\x33\n\x13webhook_secret_hash\x18\x03 \x01(\x0c\x42\x03\xe0\x41\x03R\x11webhookSecretHash\"\x87\x04\n\x13StorageSubscription\x12#\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDB\x03\xe0\x41\x03R\x02id\x12>\n\x13storage_location_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x39\n\x04type\x18\x03 \x01(\x0e\x32%.workflows.v1.StorageSubscriptionTypeR\x04type\x12\x1f\n\x08\x65ndpoint\x18\x04 \x01(\tB\x03\xe0\x41\x03R\x08\x65ndpoint\x12>\n\ncreated_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03R\tcreatedAt\x12@\n\x07\x61ws_sns\x18\x07 \x01(\x0b\x32\'.workflows.v1.AWSSNSStorageSubscriptionR\x06\x61wsSns\x12R\n\rgoogle_pubsub\x18\x08 \x01(\x0b\x32-.workflows.v1.GooglePubSubStorageSubscriptionR\x0cgooglePubsub\x12Y\n\x10\x61zure_event_grid\x18\t \x01(\x0b\x32/.workflows.v1.AzureEventGridStorageSubscriptionR\x0e\x61zureEventGrid\"\x8e\x03\n CreateStorageSubscriptionRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32%.workflows.v1.StorageSubscriptionTypeR\x04type\x12@\n\x07\x61ws_sns\x18\x03 \x01(\x0b\x32\'.workflows.v1.AWSSNSStorageSubscriptionR\x06\x61wsSns\x12R\n\rgoogle_pubsub\x18\x04 \x01(\x0b\x32-.workflows.v1.GooglePubSubStorageSubscriptionR\x0cgooglePubsub\x12Y\n\x10\x61zure_event_grid\x18\x05 \x01(\x0b\x32/.workflows.v1.AzureEventGridStorageSubscriptionR\x0e\x61zureEventGrid\"a\n\x1fListStorageSubscriptionsRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\"_\n\x14StorageSubscriptions\x12G\n\rsubscriptions\x18\x01 \x03(\x0b\x32!.workflows.v1.StorageSubscriptionR\rsubscriptions\"\x90\x03\n\x18StorageSubscriptionEvent\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1d\n\nobject_key\x18\x02 \x01(\tR\tobjectKey\x12\x32\n\x04type\x18\x03 \x01(\x0e\x32\x1e.workflows.v1.StorageEventTypeR\x04type\x12\x39\n\nevent_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\teventTime\x12;\n\x0breceived_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nreceivedAt\x12\x41\n\x0etriggered_jobs\x18\x06 \x03(\x0b\x32\x1a.workflows.v1.TriggeredJobR\rtriggeredJobs\x12\x46\n\x17storage_subscription_id\x18\x07 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x15storageSubscriptionId\"\x99\x01\n$ListStorageSubscriptionEventsRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x31\n\x04page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x97\x01\n\x19StorageSubscriptionEvents\x12>\n\x06\x65vents\x18\x01 \x03(\x0b\x32&.workflows.v1.StorageSubscriptionEventR\x06\x65vents\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"O\n\x10StorageLocations\x12;\n\tlocations\x18\x01 \x03(\x0b\x32\x1d.workflows.v1.StorageLocationR\tlocations\"\xa7\x01\n\x15TriggeredStorageEvent\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32\x1e.workflows.v1.StorageEventTypeR\x04type\x12\x1a\n\x08location\x18\x03 \x01(\tR\x08location*\x8f\x01\n\x0bStorageType\x12\x1c\n\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10STORAGE_TYPE_GCS\x10\x01\x12\x17\n\x13STORAGE_TYPE_AWS_S3\x10\x02\x12\x16\n\x12STORAGE_TYPE_LOCAL\x10\x03\x12\x1b\n\x17STORAGE_TYPE_AZURE_BLOB\x10\x04*\xf3\x01\n\x17StorageSubscriptionType\x12)\n%STORAGE_SUBSCRIPTION_TYPE_UNSPECIFIED\x10\x00\x12%\n!STORAGE_SUBSCRIPTION_TYPE_AWS_SNS\x10\x01\x12+\n\'STORAGE_SUBSCRIPTION_TYPE_GOOGLE_PUBSUB\x10\x02\x12.\n*STORAGE_SUBSCRIPTION_TYPE_AZURE_EVENT_GRID\x10\x03\x12)\n%STORAGE_SUBSCRIPTION_TYPE_TILEBOX_CLI\x10\x04*V\n\x10StorageEventType\x12\"\n\x1eSTORAGE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1aSTORAGE_EVENT_TYPE_CREATED\x10\x01\x32\xa5\x07\n\x16StorageLocationService\x12N\n\x14ListStorageLocations\x12\x16.google.protobuf.Empty\x1a\x1e.workflows.v1.StorageLocations\x12\x43\n\x12GetStorageLocation\x12\x0e.tilebox.v1.ID\x1a\x1d.workflows.v1.StorageLocation\x12\x62\n\x15\x43reateStorageLocation\x12*.workflows.v1.CreateStorageLocationRequest\x1a\x1d.workflows.v1.StorageLocation\x12\x62\n\x15UpdateStorageLocation\x12*.workflows.v1.UpdateStorageLocationRequest\x1a\x1d.workflows.v1.StorageLocation\x12?\n\x15\x44\x65leteStorageLocation\x12\x0e.tilebox.v1.ID\x1a\x16.google.protobuf.Empty\x12n\n\x19\x43reateStorageSubscription\x12..workflows.v1.CreateStorageSubscriptionRequest\x1a!.workflows.v1.StorageSubscription\x12m\n\x18ListStorageSubscriptions\x12-.workflows.v1.ListStorageSubscriptionsRequest\x1a\".workflows.v1.StorageSubscriptions\x12K\n\x16GetStorageSubscription\x12\x0e.tilebox.v1.ID\x1a!.workflows.v1.StorageSubscription\x12\x43\n\x19\x44\x65leteStorageSubscription\x12\x0e.tilebox.v1.ID\x1a\x16.google.protobuf.Empty\x12|\n\x1dListStorageSubscriptionEvents\x12\x32.workflows.v1.ListStorageSubscriptionEventsRequest\x1a\'.workflows.v1.StorageSubscriptionEventsB~\n\x10\x63om.workflows.v1B\x14StorageLocationProtoP\x01\xa2\x02\x03WXX\xaa\x02\x0cWorkflows.V1\xca\x02\x0cWorkflows\\V1\xe2\x02\x18Workflows\\V1\\GPBMetadata\xea\x02\rWorkflows::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n#workflows/v1/storage_location.proto\x12\x0cworkflows.v1\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\"\xde\x01\n\x0fStorageLocation\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1e\n\x08location\x18\x02 \x01(\tB\x02\x18\x01R\x08location\x12\x31\n\x04type\x18\x03 \x01(\x0e\x32\x19.workflows.v1.StorageTypeB\x02\x18\x01R\x04type\x12\x12\n\x04name\x18\x04 \x01(\tR\x04name\x12\x44\n\treference\x18\x05 \x01(\x0b\x32&.workflows.v1.StorageLocationReferenceR\treference\"\xc7\x02\n\x18StorageLocationReference\x12\x46\n\raws_s3_bucket\x18\x01 \x01(\x0b\x32\".workflows.v1.AWSS3BucketReferenceR\x0b\x61wsS3Bucket\x12?\n\ngcs_bucket\x18\x02 \x01(\x0b\x32 .workflows.v1.GCSBucketReferenceR\tgcsBucket\x12?\n\nazure_blob\x18\x03 \x01(\x0b\x32 .workflows.v1.AzureBlobReferenceR\tazureBlob\x12\x32\n\x05local\x18\x04 \x01(\x0b\x32\x1c.workflows.v1.LocalReferenceR\x05local\x12-\n\x04type\x18\x05 \x01(\x0e\x32\x19.workflows.v1.StorageTypeR\x04type\"F\n\x14\x41WSS3BucketReference\x12\x16\n\x06\x62ucket\x18\x01 \x01(\tR\x06\x62ucket\x12\x16\n\x06region\x18\x02 \x01(\tR\x06region\"g\n\x12GCSBucketReference\x12\x16\n\x06\x62ucket\x18\x01 \x01(\tR\x06\x62ucket\x12\x1d\n\nproject_id\x18\x02 \x01(\tR\tprojectId\x12\x1a\n\x08location\x18\x03 \x01(\tR\x08location\"\x89\x01\n\x12\x41zureBlobReference\x12=\n\x1bstorage_account_resource_id\x18\x01 \x01(\tR\x18storageAccountResourceId\x12\x1c\n\tcontainer\x18\x02 \x01(\tR\tcontainer\x12\x16\n\x06region\x18\x03 \x01(\tR\x06region\"$\n\x0eLocalReference\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\"\x8d\x01\n\x0cTriggeredJob\x12%\n\x06job_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x05jobId\x12\x33\n\rautomation_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x0c\x61utomationId\x12!\n\x0cglob_pattern\x18\x03 \x01(\tR\x0bglobPattern\"R\n\rTriggeredJobs\x12\x41\n\x0etriggered_jobs\x18\x01 \x03(\x0b\x32\x1a.workflows.v1.TriggeredJobR\rtriggeredJobs\"x\n\x1c\x43reateStorageLocationRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x44\n\treference\x18\x02 \x01(\x0b\x32&.workflows.v1.StorageLocationReferenceR\treference\"r\n\x1cUpdateStorageLocationRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\"_\n\x19\x41WSSNSStorageSubscription\x12\x1b\n\ttopic_arn\x18\x01 \x01(\tR\x08topicArn\x12%\n\x0emessage_format\x18\x02 \x01(\tR\rmessageFormat\"\x9a\x01\n\x1fGooglePubSubStorageSubscription\x12\"\n\x0csubscription\x18\x01 \x01(\tR\x0csubscription\x12\x32\n\x15service_account_email\x18\x02 \x01(\tR\x13serviceAccountEmail\x12\x1f\n\x08\x61udience\x18\x03 \x01(\tB\x03\xe0\x41\x03R\x08\x61udience\"\xbd\x01\n!AzureEventGridStorageSubscription\x12\x37\n\x15webhook_secret_header\x18\x01 \x01(\tB\x03\xe0\x41\x03R\x13webhookSecretHeader\x12*\n\x0ewebhook_secret\x18\x02 \x01(\tB\x03\xe0\x41\x03R\rwebhookSecret\x12\x33\n\x13webhook_secret_hash\x18\x03 \x01(\x0c\x42\x03\xe0\x41\x03R\x11webhookSecretHash\"\x87\x04\n\x13StorageSubscription\x12#\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDB\x03\xe0\x41\x03R\x02id\x12>\n\x13storage_location_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x39\n\x04type\x18\x03 \x01(\x0e\x32%.workflows.v1.StorageSubscriptionTypeR\x04type\x12\x1f\n\x08\x65ndpoint\x18\x04 \x01(\tB\x03\xe0\x41\x03R\x08\x65ndpoint\x12>\n\ncreated_at\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x03\xe0\x41\x03R\tcreatedAt\x12@\n\x07\x61ws_sns\x18\x07 \x01(\x0b\x32\'.workflows.v1.AWSSNSStorageSubscriptionR\x06\x61wsSns\x12R\n\rgoogle_pubsub\x18\x08 \x01(\x0b\x32-.workflows.v1.GooglePubSubStorageSubscriptionR\x0cgooglePubsub\x12Y\n\x10\x61zure_event_grid\x18\t \x01(\x0b\x32/.workflows.v1.AzureEventGridStorageSubscriptionR\x0e\x61zureEventGrid\"\x8e\x03\n CreateStorageSubscriptionRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32%.workflows.v1.StorageSubscriptionTypeR\x04type\x12@\n\x07\x61ws_sns\x18\x03 \x01(\x0b\x32\'.workflows.v1.AWSSNSStorageSubscriptionR\x06\x61wsSns\x12R\n\rgoogle_pubsub\x18\x04 \x01(\x0b\x32-.workflows.v1.GooglePubSubStorageSubscriptionR\x0cgooglePubsub\x12Y\n\x10\x61zure_event_grid\x18\x05 \x01(\x0b\x32/.workflows.v1.AzureEventGridStorageSubscriptionR\x0e\x61zureEventGrid\"a\n\x1fListStorageSubscriptionsRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\"\xc0\x01\n\x1aNotifyObjectCreatedRequest\x12\x46\n\x17storage_subscription_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x15storageSubscriptionId\x12\x1f\n\x0bobject_path\x18\x02 \x01(\tR\nobjectPath\x12\x39\n\nevent_time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\teventTime\"_\n\x14StorageSubscriptions\x12G\n\rsubscriptions\x18\x01 \x03(\x0b\x32!.workflows.v1.StorageSubscriptionR\rsubscriptions\"\x90\x03\n\x18StorageSubscriptionEvent\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x1d\n\nobject_key\x18\x02 \x01(\tR\tobjectKey\x12\x32\n\x04type\x18\x03 \x01(\x0e\x32\x1e.workflows.v1.StorageEventTypeR\x04type\x12\x39\n\nevent_time\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\teventTime\x12;\n\x0breceived_at\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\nreceivedAt\x12\x41\n\x0etriggered_jobs\x18\x06 \x03(\x0b\x32\x1a.workflows.v1.TriggeredJobR\rtriggeredJobs\x12\x46\n\x17storage_subscription_id\x18\x07 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x15storageSubscriptionId\"\x99\x01\n$ListStorageSubscriptionEventsRequest\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x31\n\x04page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x97\x01\n\x19StorageSubscriptionEvents\x12>\n\x06\x65vents\x18\x01 \x03(\x0b\x32&.workflows.v1.StorageSubscriptionEventR\x06\x65vents\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"O\n\x10StorageLocations\x12;\n\tlocations\x18\x01 \x03(\x0b\x32\x1d.workflows.v1.StorageLocationR\tlocations\"\xa7\x01\n\x15TriggeredStorageEvent\x12>\n\x13storage_location_id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x11storageLocationId\x12\x32\n\x04type\x18\x02 \x01(\x0e\x32\x1e.workflows.v1.StorageEventTypeR\x04type\x12\x1a\n\x08location\x18\x03 \x01(\tR\x08location*\x8f\x01\n\x0bStorageType\x12\x1c\n\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\x14\n\x10STORAGE_TYPE_GCS\x10\x01\x12\x17\n\x13STORAGE_TYPE_AWS_S3\x10\x02\x12\x16\n\x12STORAGE_TYPE_LOCAL\x10\x03\x12\x1b\n\x17STORAGE_TYPE_AZURE_BLOB\x10\x04*\xf3\x01\n\x17StorageSubscriptionType\x12)\n%STORAGE_SUBSCRIPTION_TYPE_UNSPECIFIED\x10\x00\x12%\n!STORAGE_SUBSCRIPTION_TYPE_AWS_SNS\x10\x01\x12+\n\'STORAGE_SUBSCRIPTION_TYPE_GOOGLE_PUBSUB\x10\x02\x12.\n*STORAGE_SUBSCRIPTION_TYPE_AZURE_EVENT_GRID\x10\x03\x12)\n%STORAGE_SUBSCRIPTION_TYPE_TILEBOX_CLI\x10\x04*V\n\x10StorageEventType\x12\"\n\x1eSTORAGE_EVENT_TYPE_UNSPECIFIED\x10\x00\x12\x1e\n\x1aSTORAGE_EVENT_TYPE_CREATED\x10\x01\x32\xfe\x07\n\x16StorageLocationService\x12N\n\x14ListStorageLocations\x12\x16.google.protobuf.Empty\x1a\x1e.workflows.v1.StorageLocations\x12\x43\n\x12GetStorageLocation\x12\x0e.tilebox.v1.ID\x1a\x1d.workflows.v1.StorageLocation\x12\x62\n\x15\x43reateStorageLocation\x12*.workflows.v1.CreateStorageLocationRequest\x1a\x1d.workflows.v1.StorageLocation\x12\x62\n\x15UpdateStorageLocation\x12*.workflows.v1.UpdateStorageLocationRequest\x1a\x1d.workflows.v1.StorageLocation\x12?\n\x15\x44\x65leteStorageLocation\x12\x0e.tilebox.v1.ID\x1a\x16.google.protobuf.Empty\x12n\n\x19\x43reateStorageSubscription\x12..workflows.v1.CreateStorageSubscriptionRequest\x1a!.workflows.v1.StorageSubscription\x12m\n\x18ListStorageSubscriptions\x12-.workflows.v1.ListStorageSubscriptionsRequest\x1a\".workflows.v1.StorageSubscriptions\x12K\n\x16GetStorageSubscription\x12\x0e.tilebox.v1.ID\x1a!.workflows.v1.StorageSubscription\x12\x43\n\x19\x44\x65leteStorageSubscription\x12\x0e.tilebox.v1.ID\x1a\x16.google.protobuf.Empty\x12|\n\x1dListStorageSubscriptionEvents\x12\x32.workflows.v1.ListStorageSubscriptionEventsRequest\x1a\'.workflows.v1.StorageSubscriptionEvents\x12W\n\x13NotifyObjectCreated\x12(.workflows.v1.NotifyObjectCreatedRequest\x1a\x16.google.protobuf.EmptyB~\n\x10\x63om.workflows.v1B\x14StorageLocationProtoP\x01\xa2\x02\x03WXX\xaa\x02\x0cWorkflows.V1\xca\x02\x0cWorkflows\\V1\xe2\x02\x18Workflows\\V1\\GPBMetadata\xea\x02\rWorkflows::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -59,12 +59,12 @@ _globals['_LISTSTORAGESUBSCRIPTIONEVENTSREQUEST'].fields_by_name['page']._serialized_options = b'\252\001\002\010\001' _globals['_STORAGESUBSCRIPTIONEVENTS'].fields_by_name['next_page']._loaded_options = None _globals['_STORAGESUBSCRIPTIONEVENTS'].fields_by_name['next_page']._serialized_options = b'\252\001\002\010\001' - _globals['_STORAGETYPE']._serialized_start=4099 - _globals['_STORAGETYPE']._serialized_end=4242 - _globals['_STORAGESUBSCRIPTIONTYPE']._serialized_start=4245 - _globals['_STORAGESUBSCRIPTIONTYPE']._serialized_end=4488 - _globals['_STORAGEEVENTTYPE']._serialized_start=4490 - _globals['_STORAGEEVENTTYPE']._serialized_end=4576 + _globals['_STORAGETYPE']._serialized_start=4294 + _globals['_STORAGETYPE']._serialized_end=4437 + _globals['_STORAGESUBSCRIPTIONTYPE']._serialized_start=4440 + _globals['_STORAGESUBSCRIPTIONTYPE']._serialized_end=4683 + _globals['_STORAGEEVENTTYPE']._serialized_start=4685 + _globals['_STORAGEEVENTTYPE']._serialized_end=4771 _globals['_STORAGELOCATION']._serialized_start=194 _globals['_STORAGELOCATION']._serialized_end=416 _globals['_STORAGELOCATIONREFERENCE']._serialized_start=419 @@ -97,18 +97,20 @@ _globals['_CREATESTORAGESUBSCRIPTIONREQUEST']._serialized_end=2936 _globals['_LISTSTORAGESUBSCRIPTIONSREQUEST']._serialized_start=2938 _globals['_LISTSTORAGESUBSCRIPTIONSREQUEST']._serialized_end=3035 - _globals['_STORAGESUBSCRIPTIONS']._serialized_start=3037 - _globals['_STORAGESUBSCRIPTIONS']._serialized_end=3132 - _globals['_STORAGESUBSCRIPTIONEVENT']._serialized_start=3135 - _globals['_STORAGESUBSCRIPTIONEVENT']._serialized_end=3535 - _globals['_LISTSTORAGESUBSCRIPTIONEVENTSREQUEST']._serialized_start=3538 - _globals['_LISTSTORAGESUBSCRIPTIONEVENTSREQUEST']._serialized_end=3691 - _globals['_STORAGESUBSCRIPTIONEVENTS']._serialized_start=3694 - _globals['_STORAGESUBSCRIPTIONEVENTS']._serialized_end=3845 - _globals['_STORAGELOCATIONS']._serialized_start=3847 - _globals['_STORAGELOCATIONS']._serialized_end=3926 - _globals['_TRIGGEREDSTORAGEEVENT']._serialized_start=3929 - _globals['_TRIGGEREDSTORAGEEVENT']._serialized_end=4096 - _globals['_STORAGELOCATIONSERVICE']._serialized_start=4579 - _globals['_STORAGELOCATIONSERVICE']._serialized_end=5512 + _globals['_NOTIFYOBJECTCREATEDREQUEST']._serialized_start=3038 + _globals['_NOTIFYOBJECTCREATEDREQUEST']._serialized_end=3230 + _globals['_STORAGESUBSCRIPTIONS']._serialized_start=3232 + _globals['_STORAGESUBSCRIPTIONS']._serialized_end=3327 + _globals['_STORAGESUBSCRIPTIONEVENT']._serialized_start=3330 + _globals['_STORAGESUBSCRIPTIONEVENT']._serialized_end=3730 + _globals['_LISTSTORAGESUBSCRIPTIONEVENTSREQUEST']._serialized_start=3733 + _globals['_LISTSTORAGESUBSCRIPTIONEVENTSREQUEST']._serialized_end=3886 + _globals['_STORAGESUBSCRIPTIONEVENTS']._serialized_start=3889 + _globals['_STORAGESUBSCRIPTIONEVENTS']._serialized_end=4040 + _globals['_STORAGELOCATIONS']._serialized_start=4042 + _globals['_STORAGELOCATIONS']._serialized_end=4121 + _globals['_TRIGGEREDSTORAGEEVENT']._serialized_start=4124 + _globals['_TRIGGEREDSTORAGEEVENT']._serialized_end=4291 + _globals['_STORAGELOCATIONSERVICE']._serialized_start=4774 + _globals['_STORAGELOCATIONSERVICE']._serialized_end=5796 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.pyi b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.pyi index 9ea9a90..255ab00 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.pyi +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2.pyi @@ -207,6 +207,16 @@ class ListStorageSubscriptionsRequest(_message.Message): storage_location_id: _id_pb2.ID def __init__(self, storage_location_id: _Optional[_Union[_id_pb2.ID, _Mapping]] = ...) -> None: ... +class NotifyObjectCreatedRequest(_message.Message): + __slots__ = ("storage_subscription_id", "object_path", "event_time") + STORAGE_SUBSCRIPTION_ID_FIELD_NUMBER: _ClassVar[int] + OBJECT_PATH_FIELD_NUMBER: _ClassVar[int] + EVENT_TIME_FIELD_NUMBER: _ClassVar[int] + storage_subscription_id: _id_pb2.ID + object_path: str + event_time: _timestamp_pb2.Timestamp + def __init__(self, storage_subscription_id: _Optional[_Union[_id_pb2.ID, _Mapping]] = ..., object_path: _Optional[str] = ..., event_time: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + class StorageSubscriptions(_message.Message): __slots__ = ("subscriptions",) SUBSCRIPTIONS_FIELD_NUMBER: _ClassVar[int] diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2_grpc.py b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2_grpc.py index 8ff71ea..3964e0d 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2_grpc.py +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/storage_location_pb2_grpc.py @@ -67,6 +67,11 @@ def __init__(self, channel): request_serializer=workflows_dot_v1_dot_storage__location__pb2.ListStorageSubscriptionEventsRequest.SerializeToString, response_deserializer=workflows_dot_v1_dot_storage__location__pb2.StorageSubscriptionEvents.FromString, _registered_method=True) + self.NotifyObjectCreated = channel.unary_unary( + '/workflows.v1.StorageLocationService/NotifyObjectCreated', + request_serializer=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest.SerializeToString, + response_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString, + _registered_method=True) class StorageLocationServiceServicer: @@ -143,6 +148,14 @@ def ListStorageSubscriptionEvents(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def NotifyObjectCreated(self, request, context): + """NotifyObjectCreated delivers a storage event of a local storage location. Typically called by the Tilebox CLI + during the `tilebox storage-location watch` command. The subscription must be of type TILEBOX_CLI. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_StorageLocationServiceServicer_to_server(servicer, server): rpc_method_handlers = { @@ -196,6 +209,11 @@ def add_StorageLocationServiceServicer_to_server(servicer, server): request_deserializer=workflows_dot_v1_dot_storage__location__pb2.ListStorageSubscriptionEventsRequest.FromString, response_serializer=workflows_dot_v1_dot_storage__location__pb2.StorageSubscriptionEvents.SerializeToString, ), + 'NotifyObjectCreated': grpc.unary_unary_rpc_method_handler( + servicer.NotifyObjectCreated, + request_deserializer=workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest.FromString, + response_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'workflows.v1.StorageLocationService', rpc_method_handlers) @@ -477,3 +495,30 @@ def ListStorageSubscriptionEvents(request, timeout, metadata, _registered_method=True) + + @staticmethod + def NotifyObjectCreated(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/workflows.v1.StorageLocationService/NotifyObjectCreated', + workflows_dot_v1_dot_storage__location__pb2.NotifyObjectCreatedRequest.SerializeToString, + google_dot_protobuf_dot_empty__pb2.Empty.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.py b/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.py index f09b11f..d994b78 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.py +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.py @@ -28,7 +28,7 @@ from tilebox.workflows.workflows.v1 import core_pb2 as workflows_dot_v1_dot_core__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cworkflows/v1/workflows.proto\x12\x0cworkflows.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\x1a\x17workflows/v1/core.proto\"\xcb\x01\n\x07\x43luster\x12\x12\n\x04slug\x18\x02 \x01(\tR\x04slug\x12!\n\x0c\x64isplay_name\x18\x03 \x01(\tR\x0b\x64isplayName\x12 \n\x0b\x64\x65scription\x18\x06 \x01(\tR\x0b\x64\x65scription\x12\x1c\n\tdeletable\x18\x04 \x01(\x08R\tdeletable\x12\x43\n\x11\x64\x65ployed_releases\x18\x05 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\x10\x64\x65ployedReleasesJ\x04\x08\x01\x10\x02\"`\n\x14\x43reateClusterRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04slug\x18\x03 \x01(\tR\x04slug\"6\n\x11GetClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\"}\n\x14UpdateClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\x12\x19\n\x04name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\"9\n\x14\x44\x65leteClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\"\x17\n\x15\x44\x65leteClusterResponse\"\x15\n\x13ListClustersRequest\"I\n\x14ListClustersResponse\x12\x31\n\x08\x63lusters\x18\x01 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"9\n\x12GetWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\"\x80\x01\n\x15UpdateWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12\x19\n\x04name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\"<\n\x15\x44\x65leteWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\"\x18\n\x16\x44\x65leteWorkflowResponse\"\xad\x01\n\x1dPublishWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12/\n\x0b\x61rtifact_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\nartifactId\x12\x36\n\x07\x63ontent\x18\x03 \x01(\x0b\x32\x1c.workflows.v1.ReleaseContentR\x07\x63ontent\"u\n\x1fUnpublishWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\"\"\n UnpublishWorkflowReleaseResponse\"I\n\x14ListWorkflowsRequest\x12\x31\n\x04page\x18\x01 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x89\x01\n\x15ListWorkflowsResponse\x12\x34\n\tworkflows\x18\x01 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\tworkflows\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"O\n\x1aListPublicWorkflowsRequest\x12\x31\n\x04page\x18\x01 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x8f\x01\n\x1bListPublicWorkflowsResponse\x12\x34\n\tworkflows\x18\x01 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\tworkflows\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"M\n\x15\x43reateWorkflowRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\"\x95\x01\n\x08Workflow\x12\x12\n\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\x39\n\x08releases\x18\x05 \x03(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x08releasesJ\x04\x08\x01\x10\x02\"\x8b\x02\n\x0fWorkflowRelease\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x32\n\x08\x61rtifact\x18\x02 \x01(\x0b\x32\x16.workflows.v1.ArtifactR\x08\x61rtifact\x12\x36\n\x07\x63ontent\x18\x03 \x01(\x0b\x32\x1c.workflows.v1.ReleaseContentR\x07\x63ontent\x12\x39\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x31\n\x08\x63lusters\x18\x05 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"B\n\x08\x41rtifact\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x16\n\x06\x64igest\x18\x02 \x01(\tR\x06\x64igest\"\xe9\x01\n\x0eReleaseContent\x12 \n\x0b\x66ingerprint\x18\x01 \x01(\tR\x0b\x66ingerprint\x12\x32\n\x05tasks\x18\x02 \x03(\x0b\x32\x1c.workflows.v1.TaskIdentifierR\x05tasks\x12(\n\x05\x66iles\x18\x03 \x03(\x0b\x32\x12.workflows.v1.PathR\x05\x66iles\x12,\n\x12runner_object_path\x18\x04 \x01(\tR\x10runnerObjectPath\x12)\n\x10\x63ommand_override\x18\x05 \x03(\tR\x0f\x63ommandOverride\"h\n\x04Path\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\tdirectory\x18\x02 \x01(\x08R\tdirectory\x12.\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x12.workflows.v1.PathR\x08\x63hildren\"\x97\x01\n\x1c\x44\x65ployWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\x12#\n\rcluster_slugs\x18\x03 \x03(\tR\x0c\x63lusterSlugs\"\x8b\x01\n\x1d\x44\x65ployWorkflowReleaseResponse\x12\x37\n\x07release\x18\x01 \x01(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x07release\x12\x31\n\x08\x63lusters\x18\x02 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"\x99\x01\n\x1eUndeployWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\x12#\n\rcluster_slugs\x18\x03 \x03(\tR\x0c\x63lusterSlugs\"\x8d\x01\n\x1fUndeployWorkflowReleaseResponse\x12\x37\n\x07release\x18\x01 \x01(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x07release\x12\x31\n\x08\x63lusters\x18\x02 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters2\xf6\n\n\x10WorkflowsService\x12J\n\rCreateCluster\x12\".workflows.v1.CreateClusterRequest\x1a\x15.workflows.v1.Cluster\x12\x44\n\nGetCluster\x12\x1f.workflows.v1.GetClusterRequest\x1a\x15.workflows.v1.Cluster\x12J\n\rUpdateCluster\x12\".workflows.v1.UpdateClusterRequest\x1a\x15.workflows.v1.Cluster\x12X\n\rDeleteCluster\x12\".workflows.v1.DeleteClusterRequest\x1a#.workflows.v1.DeleteClusterResponse\x12U\n\x0cListClusters\x12!.workflows.v1.ListClustersRequest\x1a\".workflows.v1.ListClustersResponse\x12M\n\x0e\x43reateWorkflow\x12#.workflows.v1.CreateWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12X\n\rListWorkflows\x12\".workflows.v1.ListWorkflowsRequest\x1a#.workflows.v1.ListWorkflowsResponse\x12j\n\x13ListPublicWorkflows\x12(.workflows.v1.ListPublicWorkflowsRequest\x1a).workflows.v1.ListPublicWorkflowsResponse\x12G\n\x0bGetWorkflow\x12 .workflows.v1.GetWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12M\n\x0eUpdateWorkflow\x12#.workflows.v1.UpdateWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12[\n\x0e\x44\x65leteWorkflow\x12#.workflows.v1.DeleteWorkflowRequest\x1a$.workflows.v1.DeleteWorkflowResponse\x12\x64\n\x16PublishWorkflowRelease\x12+.workflows.v1.PublishWorkflowReleaseRequest\x1a\x1d.workflows.v1.WorkflowRelease\x12y\n\x18UnpublishWorkflowRelease\x12-.workflows.v1.UnpublishWorkflowReleaseRequest\x1a..workflows.v1.UnpublishWorkflowReleaseResponse\x12p\n\x15\x44\x65ployWorkflowRelease\x12*.workflows.v1.DeployWorkflowReleaseRequest\x1a+.workflows.v1.DeployWorkflowReleaseResponse\x12v\n\x17UndeployWorkflowRelease\x12,.workflows.v1.UndeployWorkflowReleaseRequest\x1a-.workflows.v1.UndeployWorkflowReleaseResponseBx\n\x10\x63om.workflows.v1B\x0eWorkflowsProtoP\x01\xa2\x02\x03WXX\xaa\x02\x0cWorkflows.V1\xca\x02\x0cWorkflows\\V1\xe2\x02\x18Workflows\\V1\\GPBMetadata\xea\x02\rWorkflows::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1cworkflows/v1/workflows.proto\x12\x0cworkflows.v1\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x13tilebox/v1/id.proto\x1a\x16tilebox/v1/query.proto\x1a\x17workflows/v1/core.proto\"\xcb\x01\n\x07\x43luster\x12\x12\n\x04slug\x18\x02 \x01(\tR\x04slug\x12!\n\x0c\x64isplay_name\x18\x03 \x01(\tR\x0b\x64isplayName\x12 \n\x0b\x64\x65scription\x18\x06 \x01(\tR\x0b\x64\x65scription\x12\x1c\n\tdeletable\x18\x04 \x01(\x08R\tdeletable\x12\x43\n\x11\x64\x65ployed_releases\x18\x05 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\x10\x64\x65ployedReleasesJ\x04\x08\x01\x10\x02\"`\n\x14\x43reateClusterRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04slug\x18\x03 \x01(\tR\x04slug\"6\n\x11GetClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\"}\n\x14UpdateClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\x12\x19\n\x04name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\"9\n\x14\x44\x65leteClusterRequest\x12!\n\x0c\x63luster_slug\x18\x01 \x01(\tR\x0b\x63lusterSlug\"\x17\n\x15\x44\x65leteClusterResponse\"\x15\n\x13ListClustersRequest\"I\n\x14ListClustersResponse\x12\x31\n\x08\x63lusters\x18\x01 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"9\n\x12GetWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\"\xa1\x01\n\x15UpdateWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12\x19\n\x04name\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x04name\x12\x1f\n\x07summary\x18\x04 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x07summary\x12\'\n\x0b\x64\x65scription\x18\x03 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\"<\n\x15\x44\x65leteWorkflowRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\"\x18\n\x16\x44\x65leteWorkflowResponse\"\xad\x01\n\x1dPublishWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12/\n\x0b\x61rtifact_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\nartifactId\x12\x36\n\x07\x63ontent\x18\x03 \x01(\x0b\x32\x1c.workflows.v1.ReleaseContentR\x07\x63ontent\"u\n\x1fUnpublishWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\"\"\n UnpublishWorkflowReleaseResponse\"I\n\x14ListWorkflowsRequest\x12\x31\n\x04page\x18\x01 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x89\x01\n\x15ListWorkflowsResponse\x12\x34\n\tworkflows\x18\x01 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\tworkflows\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"O\n\x1aListPublicWorkflowsRequest\x12\x31\n\x04page\x18\x01 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x04page\"\x8f\x01\n\x1bListPublicWorkflowsResponse\x12\x34\n\tworkflows\x18\x01 \x03(\x0b\x32\x16.workflows.v1.WorkflowR\tworkflows\x12:\n\tnext_page\x18\x02 \x01(\x0b\x32\x16.tilebox.v1.PaginationB\x05\xaa\x01\x02\x08\x01R\x08nextPage\"n\n\x15\x43reateWorkflowRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n\x07summary\x18\x03 \x01(\tR\x07summary\x12\'\n\x0b\x64\x65scription\x18\x02 \x01(\tB\x05\xaa\x01\x02\x08\x01R\x0b\x64\x65scription\"\xf7\x01\n\x08Workflow\x12\x12\n\x04slug\x18\x02 \x01(\tR\x04slug\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12\x18\n\x07summary\x18\x06 \x01(\tR\x07summary\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\x39\n\x08releases\x18\x05 \x03(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x08releases\x12\x46\n\x0cpresentation\x18\x07 \x01(\x0b\x32\".workflows.v1.WorkflowPresentationR\x0cpresentationJ\x04\x08\x01\x10\x02\"\xb3\x01\n\x14WorkflowPresentation\x12\x1b\n\timage_url\x18\x01 \x01(\tR\x08imageUrl\x12\x1b\n\timage_alt\x18\x02 \x01(\tR\x08imageAlt\x12!\n\x0cimagery_type\x18\x03 \x03(\tR\x0bimageryType\x12\x19\n\x08use_case\x18\x04 \x03(\tR\x07useCase\x12#\n\rdataset_label\x18\x05 \x03(\tR\x0c\x64\x61tasetLabel\"\x8b\x02\n\x0fWorkflowRelease\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x32\n\x08\x61rtifact\x18\x02 \x01(\x0b\x32\x16.workflows.v1.ArtifactR\x08\x61rtifact\x12\x36\n\x07\x63ontent\x18\x03 \x01(\x0b\x32\x1c.workflows.v1.ReleaseContentR\x07\x63ontent\x12\x39\n\ncreated_at\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\tcreatedAt\x12\x31\n\x08\x63lusters\x18\x05 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"B\n\x08\x41rtifact\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x0e.tilebox.v1.IDR\x02id\x12\x16\n\x06\x64igest\x18\x02 \x01(\tR\x06\x64igest\"\xe9\x01\n\x0eReleaseContent\x12 \n\x0b\x66ingerprint\x18\x01 \x01(\tR\x0b\x66ingerprint\x12\x32\n\x05tasks\x18\x02 \x03(\x0b\x32\x1c.workflows.v1.TaskIdentifierR\x05tasks\x12(\n\x05\x66iles\x18\x03 \x03(\x0b\x32\x12.workflows.v1.PathR\x05\x66iles\x12,\n\x12runner_object_path\x18\x04 \x01(\tR\x10runnerObjectPath\x12)\n\x10\x63ommand_override\x18\x05 \x03(\tR\x0f\x63ommandOverride\"h\n\x04Path\x12\x12\n\x04path\x18\x01 \x01(\tR\x04path\x12\x1c\n\tdirectory\x18\x02 \x01(\x08R\tdirectory\x12.\n\x08\x63hildren\x18\x03 \x03(\x0b\x32\x12.workflows.v1.PathR\x08\x63hildren\"\x97\x01\n\x1c\x44\x65ployWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\x12#\n\rcluster_slugs\x18\x03 \x03(\tR\x0c\x63lusterSlugs\"\x8b\x01\n\x1d\x44\x65ployWorkflowReleaseResponse\x12\x37\n\x07release\x18\x01 \x01(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x07release\x12\x31\n\x08\x63lusters\x18\x02 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters\"\x99\x01\n\x1eUndeployWorkflowReleaseRequest\x12#\n\rworkflow_slug\x18\x01 \x01(\tR\x0cworkflowSlug\x12-\n\nrelease_id\x18\x02 \x01(\x0b\x32\x0e.tilebox.v1.IDR\treleaseId\x12#\n\rcluster_slugs\x18\x03 \x03(\tR\x0c\x63lusterSlugs\"\x8d\x01\n\x1fUndeployWorkflowReleaseResponse\x12\x37\n\x07release\x18\x01 \x01(\x0b\x32\x1d.workflows.v1.WorkflowReleaseR\x07release\x12\x31\n\x08\x63lusters\x18\x02 \x03(\x0b\x32\x15.workflows.v1.ClusterR\x08\x63lusters2\xf6\n\n\x10WorkflowsService\x12J\n\rCreateCluster\x12\".workflows.v1.CreateClusterRequest\x1a\x15.workflows.v1.Cluster\x12\x44\n\nGetCluster\x12\x1f.workflows.v1.GetClusterRequest\x1a\x15.workflows.v1.Cluster\x12J\n\rUpdateCluster\x12\".workflows.v1.UpdateClusterRequest\x1a\x15.workflows.v1.Cluster\x12X\n\rDeleteCluster\x12\".workflows.v1.DeleteClusterRequest\x1a#.workflows.v1.DeleteClusterResponse\x12U\n\x0cListClusters\x12!.workflows.v1.ListClustersRequest\x1a\".workflows.v1.ListClustersResponse\x12M\n\x0e\x43reateWorkflow\x12#.workflows.v1.CreateWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12X\n\rListWorkflows\x12\".workflows.v1.ListWorkflowsRequest\x1a#.workflows.v1.ListWorkflowsResponse\x12j\n\x13ListPublicWorkflows\x12(.workflows.v1.ListPublicWorkflowsRequest\x1a).workflows.v1.ListPublicWorkflowsResponse\x12G\n\x0bGetWorkflow\x12 .workflows.v1.GetWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12M\n\x0eUpdateWorkflow\x12#.workflows.v1.UpdateWorkflowRequest\x1a\x16.workflows.v1.Workflow\x12[\n\x0e\x44\x65leteWorkflow\x12#.workflows.v1.DeleteWorkflowRequest\x1a$.workflows.v1.DeleteWorkflowResponse\x12\x64\n\x16PublishWorkflowRelease\x12+.workflows.v1.PublishWorkflowReleaseRequest\x1a\x1d.workflows.v1.WorkflowRelease\x12y\n\x18UnpublishWorkflowRelease\x12-.workflows.v1.UnpublishWorkflowReleaseRequest\x1a..workflows.v1.UnpublishWorkflowReleaseResponse\x12p\n\x15\x44\x65ployWorkflowRelease\x12*.workflows.v1.DeployWorkflowReleaseRequest\x1a+.workflows.v1.DeployWorkflowReleaseResponse\x12v\n\x17UndeployWorkflowRelease\x12,.workflows.v1.UndeployWorkflowReleaseRequest\x1a-.workflows.v1.UndeployWorkflowReleaseResponseBx\n\x10\x63om.workflows.v1B\x0eWorkflowsProtoP\x01\xa2\x02\x03WXX\xaa\x02\x0cWorkflows.V1\xca\x02\x0cWorkflows\\V1\xe2\x02\x18Workflows\\V1\\GPBMetadata\xea\x02\rWorkflows::V1\x92\x03\x02\x08\x02\x62\x08\x65\x64itionsp\xe8\x07') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -42,6 +42,8 @@ _globals['_UPDATECLUSTERREQUEST'].fields_by_name['description']._serialized_options = b'\252\001\002\010\001' _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['name']._loaded_options = None _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['name']._serialized_options = b'\252\001\002\010\001' + _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['summary']._loaded_options = None + _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['summary']._serialized_options = b'\252\001\002\010\001' _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['description']._loaded_options = None _globals['_UPDATEWORKFLOWREQUEST'].fields_by_name['description']._serialized_options = b'\252\001\002\010\001' _globals['_LISTWORKFLOWSREQUEST'].fields_by_name['page']._loaded_options = None @@ -52,6 +54,8 @@ _globals['_LISTPUBLICWORKFLOWSREQUEST'].fields_by_name['page']._serialized_options = b'\252\001\002\010\001' _globals['_LISTPUBLICWORKFLOWSRESPONSE'].fields_by_name['next_page']._loaded_options = None _globals['_LISTPUBLICWORKFLOWSRESPONSE'].fields_by_name['next_page']._serialized_options = b'\252\001\002\010\001' + _globals['_CREATEWORKFLOWREQUEST'].fields_by_name['description']._loaded_options = None + _globals['_CREATEWORKFLOWREQUEST'].fields_by_name['description']._serialized_options = b'\252\001\002\010\001' _globals['_CLUSTER']._serialized_start=150 _globals['_CLUSTER']._serialized_end=353 _globals['_CREATECLUSTERREQUEST']._serialized_start=355 @@ -71,45 +75,47 @@ _globals['_GETWORKFLOWREQUEST']._serialized_start=818 _globals['_GETWORKFLOWREQUEST']._serialized_end=875 _globals['_UPDATEWORKFLOWREQUEST']._serialized_start=878 - _globals['_UPDATEWORKFLOWREQUEST']._serialized_end=1006 - _globals['_DELETEWORKFLOWREQUEST']._serialized_start=1008 - _globals['_DELETEWORKFLOWREQUEST']._serialized_end=1068 - _globals['_DELETEWORKFLOWRESPONSE']._serialized_start=1070 - _globals['_DELETEWORKFLOWRESPONSE']._serialized_end=1094 - _globals['_PUBLISHWORKFLOWRELEASEREQUEST']._serialized_start=1097 - _globals['_PUBLISHWORKFLOWRELEASEREQUEST']._serialized_end=1270 - _globals['_UNPUBLISHWORKFLOWRELEASEREQUEST']._serialized_start=1272 - _globals['_UNPUBLISHWORKFLOWRELEASEREQUEST']._serialized_end=1389 - _globals['_UNPUBLISHWORKFLOWRELEASERESPONSE']._serialized_start=1391 - _globals['_UNPUBLISHWORKFLOWRELEASERESPONSE']._serialized_end=1425 - _globals['_LISTWORKFLOWSREQUEST']._serialized_start=1427 - _globals['_LISTWORKFLOWSREQUEST']._serialized_end=1500 - _globals['_LISTWORKFLOWSRESPONSE']._serialized_start=1503 - _globals['_LISTWORKFLOWSRESPONSE']._serialized_end=1640 - _globals['_LISTPUBLICWORKFLOWSREQUEST']._serialized_start=1642 - _globals['_LISTPUBLICWORKFLOWSREQUEST']._serialized_end=1721 - _globals['_LISTPUBLICWORKFLOWSRESPONSE']._serialized_start=1724 - _globals['_LISTPUBLICWORKFLOWSRESPONSE']._serialized_end=1867 - _globals['_CREATEWORKFLOWREQUEST']._serialized_start=1869 - _globals['_CREATEWORKFLOWREQUEST']._serialized_end=1946 - _globals['_WORKFLOW']._serialized_start=1949 - _globals['_WORKFLOW']._serialized_end=2098 - _globals['_WORKFLOWRELEASE']._serialized_start=2101 - _globals['_WORKFLOWRELEASE']._serialized_end=2368 - _globals['_ARTIFACT']._serialized_start=2370 - _globals['_ARTIFACT']._serialized_end=2436 - _globals['_RELEASECONTENT']._serialized_start=2439 - _globals['_RELEASECONTENT']._serialized_end=2672 - _globals['_PATH']._serialized_start=2674 - _globals['_PATH']._serialized_end=2778 - _globals['_DEPLOYWORKFLOWRELEASEREQUEST']._serialized_start=2781 - _globals['_DEPLOYWORKFLOWRELEASEREQUEST']._serialized_end=2932 - _globals['_DEPLOYWORKFLOWRELEASERESPONSE']._serialized_start=2935 - _globals['_DEPLOYWORKFLOWRELEASERESPONSE']._serialized_end=3074 - _globals['_UNDEPLOYWORKFLOWRELEASEREQUEST']._serialized_start=3077 - _globals['_UNDEPLOYWORKFLOWRELEASEREQUEST']._serialized_end=3230 - _globals['_UNDEPLOYWORKFLOWRELEASERESPONSE']._serialized_start=3233 - _globals['_UNDEPLOYWORKFLOWRELEASERESPONSE']._serialized_end=3374 - _globals['_WORKFLOWSSERVICE']._serialized_start=3377 - _globals['_WORKFLOWSSERVICE']._serialized_end=4775 + _globals['_UPDATEWORKFLOWREQUEST']._serialized_end=1039 + _globals['_DELETEWORKFLOWREQUEST']._serialized_start=1041 + _globals['_DELETEWORKFLOWREQUEST']._serialized_end=1101 + _globals['_DELETEWORKFLOWRESPONSE']._serialized_start=1103 + _globals['_DELETEWORKFLOWRESPONSE']._serialized_end=1127 + _globals['_PUBLISHWORKFLOWRELEASEREQUEST']._serialized_start=1130 + _globals['_PUBLISHWORKFLOWRELEASEREQUEST']._serialized_end=1303 + _globals['_UNPUBLISHWORKFLOWRELEASEREQUEST']._serialized_start=1305 + _globals['_UNPUBLISHWORKFLOWRELEASEREQUEST']._serialized_end=1422 + _globals['_UNPUBLISHWORKFLOWRELEASERESPONSE']._serialized_start=1424 + _globals['_UNPUBLISHWORKFLOWRELEASERESPONSE']._serialized_end=1458 + _globals['_LISTWORKFLOWSREQUEST']._serialized_start=1460 + _globals['_LISTWORKFLOWSREQUEST']._serialized_end=1533 + _globals['_LISTWORKFLOWSRESPONSE']._serialized_start=1536 + _globals['_LISTWORKFLOWSRESPONSE']._serialized_end=1673 + _globals['_LISTPUBLICWORKFLOWSREQUEST']._serialized_start=1675 + _globals['_LISTPUBLICWORKFLOWSREQUEST']._serialized_end=1754 + _globals['_LISTPUBLICWORKFLOWSRESPONSE']._serialized_start=1757 + _globals['_LISTPUBLICWORKFLOWSRESPONSE']._serialized_end=1900 + _globals['_CREATEWORKFLOWREQUEST']._serialized_start=1902 + _globals['_CREATEWORKFLOWREQUEST']._serialized_end=2012 + _globals['_WORKFLOW']._serialized_start=2015 + _globals['_WORKFLOW']._serialized_end=2262 + _globals['_WORKFLOWPRESENTATION']._serialized_start=2265 + _globals['_WORKFLOWPRESENTATION']._serialized_end=2444 + _globals['_WORKFLOWRELEASE']._serialized_start=2447 + _globals['_WORKFLOWRELEASE']._serialized_end=2714 + _globals['_ARTIFACT']._serialized_start=2716 + _globals['_ARTIFACT']._serialized_end=2782 + _globals['_RELEASECONTENT']._serialized_start=2785 + _globals['_RELEASECONTENT']._serialized_end=3018 + _globals['_PATH']._serialized_start=3020 + _globals['_PATH']._serialized_end=3124 + _globals['_DEPLOYWORKFLOWRELEASEREQUEST']._serialized_start=3127 + _globals['_DEPLOYWORKFLOWRELEASEREQUEST']._serialized_end=3278 + _globals['_DEPLOYWORKFLOWRELEASERESPONSE']._serialized_start=3281 + _globals['_DEPLOYWORKFLOWRELEASERESPONSE']._serialized_end=3420 + _globals['_UNDEPLOYWORKFLOWRELEASEREQUEST']._serialized_start=3423 + _globals['_UNDEPLOYWORKFLOWRELEASEREQUEST']._serialized_end=3576 + _globals['_UNDEPLOYWORKFLOWRELEASERESPONSE']._serialized_start=3579 + _globals['_UNDEPLOYWORKFLOWRELEASERESPONSE']._serialized_end=3720 + _globals['_WORKFLOWSSERVICE']._serialized_start=3723 + _globals['_WORKFLOWSSERVICE']._serialized_end=5121 # @@protoc_insertion_point(module_scope) diff --git a/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.pyi b/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.pyi index 906b65f..59b154e 100644 --- a/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.pyi +++ b/tilebox-workflows/tilebox/workflows/workflows/v1/workflows_pb2.pyi @@ -77,14 +77,16 @@ class GetWorkflowRequest(_message.Message): def __init__(self, workflow_slug: _Optional[str] = ...) -> None: ... class UpdateWorkflowRequest(_message.Message): - __slots__ = ("workflow_slug", "name", "description") + __slots__ = ("workflow_slug", "name", "summary", "description") WORKFLOW_SLUG_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] + SUMMARY_FIELD_NUMBER: _ClassVar[int] DESCRIPTION_FIELD_NUMBER: _ClassVar[int] workflow_slug: str name: str + summary: str description: str - def __init__(self, workflow_slug: _Optional[str] = ..., name: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... + def __init__(self, workflow_slug: _Optional[str] = ..., name: _Optional[str] = ..., summary: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... class DeleteWorkflowRequest(_message.Message): __slots__ = ("workflow_slug",) @@ -147,24 +149,44 @@ class ListPublicWorkflowsResponse(_message.Message): def __init__(self, workflows: _Optional[_Iterable[_Union[Workflow, _Mapping]]] = ..., next_page: _Optional[_Union[_query_pb2.Pagination, _Mapping]] = ...) -> None: ... class CreateWorkflowRequest(_message.Message): - __slots__ = ("name", "description") + __slots__ = ("name", "summary", "description") NAME_FIELD_NUMBER: _ClassVar[int] + SUMMARY_FIELD_NUMBER: _ClassVar[int] DESCRIPTION_FIELD_NUMBER: _ClassVar[int] name: str + summary: str description: str - def __init__(self, name: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... + def __init__(self, name: _Optional[str] = ..., summary: _Optional[str] = ..., description: _Optional[str] = ...) -> None: ... class Workflow(_message.Message): - __slots__ = ("slug", "name", "description", "releases") + __slots__ = ("slug", "name", "summary", "description", "releases", "presentation") SLUG_FIELD_NUMBER: _ClassVar[int] NAME_FIELD_NUMBER: _ClassVar[int] + SUMMARY_FIELD_NUMBER: _ClassVar[int] DESCRIPTION_FIELD_NUMBER: _ClassVar[int] RELEASES_FIELD_NUMBER: _ClassVar[int] + PRESENTATION_FIELD_NUMBER: _ClassVar[int] slug: str name: str + summary: str description: str releases: _containers.RepeatedCompositeFieldContainer[WorkflowRelease] - def __init__(self, slug: _Optional[str] = ..., name: _Optional[str] = ..., description: _Optional[str] = ..., releases: _Optional[_Iterable[_Union[WorkflowRelease, _Mapping]]] = ...) -> None: ... + presentation: WorkflowPresentation + def __init__(self, slug: _Optional[str] = ..., name: _Optional[str] = ..., summary: _Optional[str] = ..., description: _Optional[str] = ..., releases: _Optional[_Iterable[_Union[WorkflowRelease, _Mapping]]] = ..., presentation: _Optional[_Union[WorkflowPresentation, _Mapping]] = ...) -> None: ... + +class WorkflowPresentation(_message.Message): + __slots__ = ("image_url", "image_alt", "imagery_type", "use_case", "dataset_label") + IMAGE_URL_FIELD_NUMBER: _ClassVar[int] + IMAGE_ALT_FIELD_NUMBER: _ClassVar[int] + IMAGERY_TYPE_FIELD_NUMBER: _ClassVar[int] + USE_CASE_FIELD_NUMBER: _ClassVar[int] + DATASET_LABEL_FIELD_NUMBER: _ClassVar[int] + image_url: str + image_alt: str + imagery_type: _containers.RepeatedScalarFieldContainer[str] + use_case: _containers.RepeatedScalarFieldContainer[str] + dataset_label: _containers.RepeatedScalarFieldContainer[str] + def __init__(self, image_url: _Optional[str] = ..., image_alt: _Optional[str] = ..., imagery_type: _Optional[_Iterable[str]] = ..., use_case: _Optional[_Iterable[str]] = ..., dataset_label: _Optional[_Iterable[str]] = ...) -> None: ... class WorkflowRelease(_message.Message): __slots__ = ("id", "artifact", "content", "created_at", "clusters") diff --git a/uv.lock b/uv.lock index 69e0dbd..f5c40a5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -3069,6 +3069,7 @@ dependencies = [ { name = "obstore" }, { name = "pyproj", version = "3.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "pyproj", version = "3.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "tenacity" }, { name = "tilebox-datasets" }, ] @@ -3087,8 +3088,9 @@ requires-dist = [ { name = "async-geotiff" }, { name = "boto3", specifier = ">=1.40.2" }, { name = "niquests", specifier = ">=3.18" }, - { name = "obstore", specifier = ">=0.8.0" }, + { name = "obstore", specifier = ">=0.11.1" }, { name = "pyproj", specifier = ">=3.4" }, + { name = "tenacity", specifier = ">=8" }, { name = "tilebox-datasets", editable = "tilebox-datasets" }, ]