From 0aedd95c99dfc8a26b9c964468c3906bd5bf1fcb Mon Sep 17 00:00:00 2001 From: hrodmn Date: Fri, 28 Aug 2026 13:10:17 -0500 Subject: [PATCH 1/8] feat: add DPS provenance to generated STAC items --- README.md | 9 ++ .../src/dps_stac_item_generator/item.py | 57 ++++++-- .../runtime/tests/test_item.py | 128 +++++++++++++++--- .../runtime/tests/test_item_gen_handler.py | 2 +- 4 files changed, 164 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 96c307c..7cdf21a 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,15 @@ User STAC catalog configuration: - `USER_STAC_CATALOG_TRANSACTIONS_AUTH_MODE=basic` enables catalog write routes and selects the supported auth mode. Catalog write routes require catalogs to stay enabled. - `USER_STAC_CATALOG_TRANSACTIONS_AUTH_SECRET_ARN` can point at an existing auth secret. +## DPS-generated STAC items + +The DPS item generator assigns unregistered items to collections named +`{username}__{algorithm_name}__{algorithm_version}`. Authorized user-supplied +collection IDs are preserved. Generated items include the filterable +`maap-dps:algorithm_name`, `maap-dps:algorithm_version`, `maap-dps:username`, +and `maap-dps:tag` properties, the MAAP DPS STAC extension, and a `via` link to +the source `.met.json` file. + Collection-only STAC transactions can still be enabled with: - `USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE=basic` diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py index e135307..2826f2e 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py @@ -2,7 +2,7 @@ import json import logging import re -from typing import Any, Dict, Generator, Optional, Union +from typing import Any, Generator, Optional, Union from urllib.parse import urlparse import obstore @@ -16,7 +16,10 @@ logger = logging.getLogger() logger.setLevel(logging.INFO) -COLLECTION_ID_FORMAT = "{username}__{algorithm_name}__{algorithm_version}__{tag}" +COLLECTION_ID_FORMAT = "{username}__{algorithm_name}__{algorithm_version}" +DPS_STAC_EXTENSION = ( + "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json" +) class ObstoreStacIO(DefaultStacIO): @@ -63,18 +66,24 @@ def get_dps_output_prefix(s3_key) -> Optional[str]: return None -def load_met_json(bucket: str, job_output_prefix: str) -> Optional[Dict[str, str]]: - """Load the .met.json file that gets uploaded with DPS job outputs""" +def load_met_json( + bucket: str, job_output_prefix: str +) -> tuple[dict[str, str], str] | None: + """Load DPS metadata and return its discovered object key.""" store = from_url(f"s3://{bucket}/{job_output_prefix}") stream = obstore.list(store, chunk_size=10) for list_result in stream: for result in list_result: - if result["path"].endswith("met.json"): - return json.loads( - obstore.get(store, result["path"]) - .bytes() - .to_bytes() - .decode("utf-8") + met_json_key = result["path"] + if met_json_key.endswith("met.json"): + return ( + json.loads( + obstore.get(store, met_json_key) + .bytes() + .to_bytes() + .decode("utf-8") + ), + met_json_key, ) @@ -129,16 +138,18 @@ def get_stac_items( s3_key_parsed = urlparse(catalog_json_key) - job_metadata = load_met_json(s3_key_parsed.netloc, job_output_prefix) - if not job_metadata: + met_json = load_met_json(s3_key_parsed.netloc, job_output_prefix) + if not met_json: raise ValueError( f"could not locate the .met.json file with the DPS job outputs in {job_output_prefix}" ) + job_metadata, met_json_key = met_json deterministic_collection_id = slugify( COLLECTION_ID_FORMAT.format(**job_metadata), regex_pattern=r"[/\?#%& ]+" ) username = job_metadata.get("username", "") + met_json_href = f"s3://{s3_key_parsed.netloc}/{met_json_key.lstrip('/')}" catalog = pystac.Catalog.from_file(catalog_json_key) catalog.make_all_asset_hrefs_absolute() @@ -156,4 +167,26 @@ def get_stac_items( else: item_dict["collection"] = deterministic_collection_id + item_dict.setdefault("properties", {}).update( + { + "maap-dps:algorithm_name": job_metadata["algorithm_name"], + "maap-dps:algorithm_version": job_metadata["algorithm_version"], + "maap-dps:username": job_metadata["username"], + "maap-dps:tag": job_metadata["tag"], + } + ) + item_dict["stac_extensions"] = list( + dict.fromkeys( + (item_dict.get("stac_extensions") or []) + [DPS_STAC_EXTENSION] + ) + ) + links = item_dict.setdefault("links", []) + if not any( + link.get("rel") == "via" and link.get("href") == met_json_href + for link in links + ): + links.append( + {"rel": "via", "href": met_json_href, "type": "application/json"} + ) + yield Item(**item_dict) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py index fff54a9..02b0dac 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py @@ -2,7 +2,7 @@ import pystac import pytest -from dps_stac_item_generator.item import get_stac_items, is_authorized +from dps_stac_item_generator.item import get_stac_items, is_authorized, load_met_json from stac_pydantic.item import Item @@ -45,6 +45,39 @@ def test_multiple_patterns_first_match_wins(self): class TestGetStacItems: """Test cases for get_stac_items function.""" + def test_load_met_json_returns_discovered_key(self): + store = MagicMock() + met_json_object = MagicMock() + met_json_object.bytes.return_value.to_bytes.return_value.decode.return_value = ( + '{"algorithm_name": "awesome-algo"}' + ) + with ( + patch("dps_stac_item_generator.item.from_url", return_value=store), + patch( + "dps_stac_item_generator.item.obstore.list", + return_value=[ + [ + {"path": "2023/01/15/10/30/45/123456/catalog.json"}, + {"path": "2023/01/15/10/30/45/123456/job.met.json"}, + ] + ], + ) as mock_list, + patch( + "dps_stac_item_generator.item.obstore.get", + return_value=met_json_object, + ) as mock_get, + ): + result = load_met_json("test-bucket", "2023/01/15/10/30/45/123456/") + + assert result == ( + {"algorithm_name": "awesome-algo"}, + "2023/01/15/10/30/45/123456/job.met.json", + ) + mock_list.assert_called_once_with(store, chunk_size=10) + mock_get.assert_called_once_with( + store, "2023/01/15/10/30/45/123456/job.met.json" + ) + @pytest.fixture def mock_catalog(self): """Create a mock STAC catalog with items.""" @@ -66,7 +99,11 @@ def mock_catalog(self): "bbox": [-180, -90, 180, 90], "links": [], "assets": {}, - "stac_extensions": [], + "stac_extensions": [ + "https://example.com/existing-extension.json", + "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", + "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", + ], } item2 = MagicMock() item2.to_dict.return_value = { @@ -84,7 +121,7 @@ def mock_catalog(self): "bbox": [-180, -90, 180, 90], "links": [], "assets": {}, - "stac_extensions": [], + "stac_extensions": ["https://example.com/existing-extension.json"], } catalog.get_all_items.return_value = [item1, item2] catalog.make_all_asset_hrefs_absolute.return_value = None @@ -104,7 +141,8 @@ def mock_job_metadata(self): def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): """Test successful generation of STAC items from catalog.""" catalog_s3_key = "s3://test-bucket/2023/01/15/10/30/45/123456/catalog.json" - expected_collection_id = "superman__awesome-algo__0.1__test" + expected_collection_id = "superman__awesome-algo__0.1" + expected_met_json_href = "s3://test-bucket/2023/01/15/10/30/45/123456/.met.json" with ( patch( @@ -113,7 +151,10 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list(get_stac_items(catalog_s3_key)) @@ -123,6 +164,22 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): for item in items: assert isinstance(item, Item) assert item.collection == expected_collection_id + assert item.properties.model_dump() == { + "datetime": item.properties.datetime, + "maap-dps:algorithm_name": "awesome-algo", + "maap-dps:algorithm_version": "0.1", + "maap-dps:username": "superman", + "maap-dps:tag": "test", + } + assert [str(extension) for extension in item.stac_extensions] == [ + "https://example.com/existing-extension.json", + "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", + ] + assert { + "rel": "via", + "href": expected_met_json_href, + "type": "application/json", + } in item.model_dump()["links"] mock_catalog.make_all_asset_hrefs_absolute.assert_called_once() mock_catalog.get_all_items.assert_called_once() @@ -140,7 +197,10 @@ def test_get_stac_items_invalid_s3_key_format( ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): with pytest.raises( @@ -175,7 +235,10 @@ def test_get_stac_items_load_met_json_called_correctly( ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ) as mock_load_met, ): list(get_stac_items(catalog_s3_key)) @@ -198,7 +261,10 @@ def test_get_stac_items_empty_catalog(self, mock_job_metadata): ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list(get_stac_items(catalog_s3_key)) @@ -218,7 +284,10 @@ def test_get_stac_items_catalog_loading_failure(self, mock_job_metadata): ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): with pytest.raises(Exception, match="Failed to load catalog"): @@ -235,7 +304,10 @@ def test_get_stac_items_generator_behavior(self, mock_catalog, mock_job_metadata ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items_generator = get_stac_items(catalog_s3_key) @@ -256,7 +328,10 @@ def test_get_stac_items_invalid_catalog_json(self, mock_job_metadata): with ( patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), patch( "dps_stac_item_generator.item.pystac.Catalog.from_file", @@ -273,7 +348,7 @@ def test_santitize_collection_id(self, mock_catalog, mock_job_metadata): catalog_s3_key = "s3://test-bucket/2023/01/15/10/30/45/123456/catalog.json" mock_job_metadata["username"] = "user/name" mock_job_metadata["algorithm_name"] = "algo?name" - expected_collection_id = "user-name__algo-name__0.1__test" + expected_collection_id = "user-name__algo-name__0.1" with ( patch( @@ -282,7 +357,10 @@ def test_santitize_collection_id(self, mock_catalog, mock_job_metadata): ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list(get_stac_items(catalog_s3_key)) @@ -302,7 +380,10 @@ def test_authorized_collection_id_preserved(self, mock_catalog, mock_job_metadat ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list( @@ -316,7 +397,7 @@ def test_unauthorized_collection_id_replaced(self, mock_catalog, mock_job_metada """Items get the deterministic ID when the user is not authorized.""" catalog_s3_key = "s3://test-bucket/2023/01/15/10/30/45/123456/catalog.json" registry = {"test-collection": ["other-user"]} - expected_collection_id = "superman__awesome-algo__0.1__test" + expected_collection_id = "superman__awesome-algo__0.1" with ( patch( @@ -325,7 +406,10 @@ def test_unauthorized_collection_id_replaced(self, mock_catalog, mock_job_metada ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list( @@ -347,7 +431,10 @@ def test_wildcard_registry_pattern(self, mock_catalog, mock_job_metadata): ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list( @@ -362,7 +449,7 @@ def test_empty_registry_uses_deterministic_id( ): """An empty registry results in the deterministic collection ID for all items.""" catalog_s3_key = "s3://test-bucket/2023/01/15/10/30/45/123456/catalog.json" - expected_collection_id = "superman__awesome-algo__0.1__test" + expected_collection_id = "superman__awesome-algo__0.1" with ( patch( @@ -371,7 +458,10 @@ def test_empty_registry_uses_deterministic_id( ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=mock_job_metadata, + return_value=( + mock_job_metadata, + "2023/01/15/10/30/45/123456/.met.json", + ), ), ): items = list(get_stac_items(catalog_s3_key, collection_id_registry={})) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py index 857f9b2..ab17c5a 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py @@ -619,7 +619,7 @@ def test_handler_registry_preserves_authorized_collection_id( ), patch( "dps_stac_item_generator.item.load_met_json", - return_value=job_metadata, + return_value=(job_metadata, "2023/01/15/10/30/45/123456/.met.json"), ), ): result = item_gen_handler.handler(event, mock_context) From a58f71846d449fac24f837949a2fb5be0fbb4ff7 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Fri, 28 Aug 2026 14:21:05 -0500 Subject: [PATCH 2/8] fix: store DPS metadata as a STAC asset --- README.md | 4 ++-- .../src/dps_stac_item_generator/item.py | 19 ++++++++++-------- .../runtime/tests/test_item.py | 20 +++++++++++++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7cdf21a..5884028 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,8 @@ The DPS item generator assigns unregistered items to collections named `{username}__{algorithm_name}__{algorithm_version}`. Authorized user-supplied collection IDs are preserved. Generated items include the filterable `maap-dps:algorithm_name`, `maap-dps:algorithm_version`, `maap-dps:username`, -and `maap-dps:tag` properties, the MAAP DPS STAC extension, and a `via` link to -the source `.met.json` file. +and `maap-dps:tag` properties, the MAAP DPS STAC extension, and a `dps-metadata` +asset containing the source `.met.json` file. Collection-only STAC transactions can still be enabled with: diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py index 2826f2e..6b5f102 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py @@ -180,13 +180,16 @@ def get_stac_items( (item_dict.get("stac_extensions") or []) + [DPS_STAC_EXTENSION] ) ) - links = item_dict.setdefault("links", []) - if not any( - link.get("rel") == "via" and link.get("href") == met_json_href - for link in links - ): - links.append( - {"rel": "via", "href": met_json_href, "type": "application/json"} - ) + item_dict.setdefault("assets", {})["dps-metadata"] = { + "href": met_json_href, + "type": "application/json", + "roles": ["metadata"], + "title": "DPS job metadata", + } + item_dict["links"] = [ + link + for link in item_dict.get("links", []) + if not (link.get("rel") == "via" and link.get("href") == met_json_href) + ] yield Item(**item_dict) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py index 02b0dac..0e9650e 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py @@ -97,7 +97,13 @@ def mock_catalog(self): ], }, "bbox": [-180, -90, 180, 90], - "links": [], + "links": [ + { + "rel": "via", + "href": "s3://test-bucket/2023/01/15/10/30/45/123456/.met.json", + "type": "application/json", + } + ], "assets": {}, "stac_extensions": [ "https://example.com/existing-extension.json", @@ -175,11 +181,17 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): "https://example.com/existing-extension.json", "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", ] - assert { - "rel": "via", + assert item.model_dump()["assets"]["dps-metadata"] == { "href": expected_met_json_href, "type": "application/json", - } in item.model_dump()["links"] + "roles": ["metadata"], + "title": "DPS job metadata", + } + assert not any( + link.get("rel") == "via" + and link.get("href") == expected_met_json_href + for link in item.model_dump()["links"] + ) mock_catalog.make_all_asset_hrefs_absolute.assert_called_once() mock_catalog.get_all_items.assert_called_once() From 5c1d6fdb9ab8c6124e67c7a41238694374f8ab7d Mon Sep 17 00:00:00 2001 From: hrodmn Date: Fri, 28 Aug 2026 14:40:00 -0500 Subject: [PATCH 3/8] refactor: mutate pystac items before conversion --- README.md | 4 +- .../src/dps_stac_item_generator/item.py | 41 ++++---- .../runtime/tests/test_item.py | 94 ++++++++++--------- .../runtime/tests/test_item_gen_handler.py | 23 ++--- 4 files changed, 87 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 5884028..5fd8912 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ The DPS item generator assigns unregistered items to collections named collection IDs are preserved. Generated items include the filterable `maap-dps:algorithm_name`, `maap-dps:algorithm_version`, `maap-dps:username`, and `maap-dps:tag` properties, the MAAP DPS STAC extension, and a `dps-metadata` -asset containing the source `.met.json` file. +asset containing the source `.met.json` file. The generator also overwrites the +STAC Common Metadata `created` property with the UTC publication time shared by +all Items generated from that catalog. Collection-only STAC transactions can still be enabled with: diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py index 6b5f102..38722e7 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py @@ -2,13 +2,14 @@ import json import logging import re +from datetime import datetime, timezone from typing import Any, Generator, Optional, Union from urllib.parse import urlparse import obstore import pystac from obstore.store import from_url -from pystac import Link +from pystac import Asset, Link from pystac.stac_io import DefaultStacIO, StacIO from slugify import slugify from stac_pydantic.item import Item @@ -150,13 +151,13 @@ def get_stac_items( ) username = job_metadata.get("username", "") met_json_href = f"s3://{s3_key_parsed.netloc}/{met_json_key.lstrip('/')}" + processing_time = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") catalog = pystac.Catalog.from_file(catalog_json_key) catalog.make_all_asset_hrefs_absolute() for item in catalog.get_all_items(): - item_dict = item.to_dict() - item_collection_id = item_dict.get("collection") + item_collection_id = item.collection_id if item_collection_id and is_authorized(username, item_collection_id, registry): logger.info( @@ -165,31 +166,33 @@ def get_stac_items( username, ) else: - item_dict["collection"] = deterministic_collection_id + item.collection_id = deterministic_collection_id - item_dict.setdefault("properties", {}).update( + item.properties.update( { "maap-dps:algorithm_name": job_metadata["algorithm_name"], "maap-dps:algorithm_version": job_metadata["algorithm_version"], "maap-dps:username": job_metadata["username"], "maap-dps:tag": job_metadata["tag"], + "created": processing_time, } ) - item_dict["stac_extensions"] = list( - dict.fromkeys( - (item_dict.get("stac_extensions") or []) + [DPS_STAC_EXTENSION] - ) + item.stac_extensions[:] = list(dict.fromkeys(item.stac_extensions)) + if DPS_STAC_EXTENSION not in item.stac_extensions: + item.stac_extensions.append(DPS_STAC_EXTENSION) + item.add_asset( + "dps-metadata", + Asset( + href=met_json_href, + media_type="application/json", + roles=["metadata"], + title="DPS job metadata", + ), ) - item_dict.setdefault("assets", {})["dps-metadata"] = { - "href": met_json_href, - "type": "application/json", - "roles": ["metadata"], - "title": "DPS job metadata", - } - item_dict["links"] = [ + item.links = [ link - for link in item_dict.get("links", []) - if not (link.get("rel") == "via" and link.get("href") == met_json_href) + for link in item.links + if not (link.rel == "via" and link.href == met_json_href) ] - yield Item(**item_dict) + yield Item(**item.to_dict()) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py index 0e9650e..90f635b 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py @@ -1,3 +1,5 @@ +from datetime import datetime as DateTime +from datetime import timezone from unittest.mock import MagicMock, patch import pystac @@ -83,52 +85,47 @@ def mock_catalog(self): """Create a mock STAC catalog with items.""" catalog = MagicMock(spec=pystac.Catalog) - item1 = MagicMock() - item1.to_dict.return_value = { - "type": "Feature", - "stac_version": "1.0.0", - "id": "item1", - "collection": "test-collection", - "properties": {"datetime": "2023-01-01T00:00:00Z"}, - "geometry": { - "type": "Polygon", - "coordinates": [ - [[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]] - ], - }, - "bbox": [-180, -90, 180, 90], - "links": [ - { - "rel": "via", - "href": "s3://test-bucket/2023/01/15/10/30/45/123456/.met.json", - "type": "application/json", - } - ], - "assets": {}, - "stac_extensions": [ + geometry = { + "type": "Polygon", + "coordinates": [[[-180, -90], [180, -90], [180, 90], [-180, -90]]], + } + item1 = pystac.Item( + id="item1", + geometry=geometry, + bbox=[-180, -90, 180, 90], + datetime=DateTime(2023, 1, 1, tzinfo=timezone.utc), + properties={"created": "2000-01-01T00:00:00Z"}, + collection="test-collection", + stac_extensions=[ "https://example.com/existing-extension.json", "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", ], - } - item2 = MagicMock() - item2.to_dict.return_value = { - "type": "Feature", - "stac_version": "1.0.0", - "id": "item2", - "collection": "test-collection", - "properties": {"datetime": "2023-01-02T00:00:00Z"}, - "geometry": { - "type": "Polygon", - "coordinates": [ - [[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]] - ], - }, - "bbox": [-180, -90, 180, 90], - "links": [], - "assets": {}, - "stac_extensions": ["https://example.com/existing-extension.json"], - } + ) + item1.add_link( + pystac.Link( + "via", + "s3://test-bucket/2023/01/15/10/30/45/123456/.met.json", + media_type="application/json", + ) + ) + item2 = pystac.Item( + id="item2", + geometry=geometry, + bbox=[-180, -90, 180, 90], + datetime=DateTime(2023, 1, 2, tzinfo=timezone.utc), + properties={}, + collection="test-collection", + stac_extensions=["https://example.com/existing-extension.json"], + ) + item2.add_asset( + "existing-data", + pystac.Asset( + "s3://test-bucket/2023/01/15/10/30/45/123456/data.tif", + media_type="image/tiff", + roles=["data"], + ), + ) catalog.get_all_items.return_value = [item1, item2] catalog.make_all_asset_hrefs_absolute.return_value = None @@ -149,6 +146,8 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): catalog_s3_key = "s3://test-bucket/2023/01/15/10/30/45/123456/catalog.json" expected_collection_id = "superman__awesome-algo__0.1" expected_met_json_href = "s3://test-bucket/2023/01/15/10/30/45/123456/.met.json" + processing_time = DateTime(2024, 1, 2, 3, 4, 5, tzinfo=timezone.utc) + expected_created = "2024-01-02T03:04:05Z" with ( patch( @@ -162,7 +161,9 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): "2023/01/15/10/30/45/123456/.met.json", ), ), + patch("dps_stac_item_generator.item.datetime") as mock_datetime, ): + mock_datetime.now.return_value = processing_time items = list(get_stac_items(catalog_s3_key)) assert len(items) == 2 @@ -176,7 +177,9 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): "maap-dps:algorithm_version": "0.1", "maap-dps:username": "superman", "maap-dps:tag": "test", + "created": item.properties.created, } + assert item.properties.created == processing_time assert [str(extension) for extension in item.stac_extensions] == [ "https://example.com/existing-extension.json", "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", @@ -187,12 +190,19 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): "roles": ["metadata"], "title": "DPS job metadata", } + if item.id == "item2": + assert "existing-data" in item.model_dump()["assets"] assert not any( link.get("rel") == "via" and link.get("href") == expected_met_json_href for link in item.model_dump()["links"] ) + mock_datetime.now.assert_called_once_with(timezone.utc) + assert all( + item.properties["created"] == expected_created + for item in mock_catalog.get_all_items.return_value + ) mock_catalog.make_all_asset_hrefs_absolute.assert_called_once() mock_catalog.get_all_items.assert_called_once() diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py index ab17c5a..f991e09 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py @@ -1,5 +1,7 @@ import json import logging +from datetime import datetime +from datetime import timezone import os from unittest.mock import MagicMock, patch @@ -585,24 +587,19 @@ def test_handler_registry_preserves_authorized_collection_id( mock_catalog = MagicMock(spec=pystac.Catalog) mock_catalog.make_all_asset_hrefs_absolute.return_value = None - pystac_item = MagicMock() - pystac_item.to_dict.return_value = { - "type": "Feature", - "stac_version": "1.0.0", - "id": "test-item", - "collection": "my-custom-collection", - "properties": {"datetime": "2023-01-01T00:00:00Z"}, - "geometry": { + pystac_item = pystac.Item( + id="test-item", + geometry={ "type": "Polygon", "coordinates": [ [[-180, -90], [180, -90], [180, 90], [-180, 90], [-180, -90]] ], }, - "bbox": [-180, -90, 180, 90], - "links": [], - "assets": {}, - "stac_extensions": [], - } + bbox=[-180, -90, 180, 90], + datetime=datetime(2023, 1, 1, tzinfo=timezone.utc), + properties={}, + collection="my-custom-collection", + ) mock_catalog.get_all_items.return_value = [pystac_item] job_metadata = { From d01150eb291c11abf8e5f5109d675a907833ac19 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 8 Sep 2026 10:05:50 -0500 Subject: [PATCH 4/8] chore: use pystac.extensions.maap_dps --- .../runtime/pyproject.toml | 6 +- .../src/dps_stac_item_generator/item.py | 23 +- .../DpsStacItemGenerator/runtime/uv.lock | 514 ++++++++++++++---- 3 files changed, 434 insertions(+), 109 deletions(-) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml b/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml index f8ae3c4..572c178 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml +++ b/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml @@ -9,9 +9,10 @@ requires-python = ">=3.12" dependencies = [ "obstore>=0.7.0", "pydantic>=2.11.0", - "pystac[validation]>=1.13.0", "stac-pydantic>=3.2.0", "python-slugify==8.0.4", + "pystac-ext-maap-dps", + "pystac[validation]>=1.15.2", ] [dependency-groups] @@ -29,3 +30,6 @@ build-backend = "hatchling.build" [tool.pytest.ini_options] addopts = "-vv --ignore=cdk.out --no-header --tb=native" pythonpath = "." + +[tool.uv.sources] +pystac-ext-maap-dps = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git", rev = "851bd53cec57775f12199dcea94b5800c3623923" } diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py index 38722e7..0bd6e5b 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py @@ -10,6 +10,7 @@ import pystac from obstore.store import from_url from pystac import Asset, Link +from pystac.extensions.maap_dps import MaapDpsExtension from pystac.stac_io import DefaultStacIO, StacIO from slugify import slugify from stac_pydantic.item import Item @@ -18,10 +19,6 @@ logger.setLevel(logging.INFO) COLLECTION_ID_FORMAT = "{username}__{algorithm_name}__{algorithm_version}" -DPS_STAC_EXTENSION = ( - "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json" -) - class ObstoreStacIO(DefaultStacIO): def read_text(self, source: Union[str, Link], *args: Any, **kwargs: Any) -> str: @@ -168,18 +165,14 @@ def get_stac_items( else: item.collection_id = deterministic_collection_id - item.properties.update( - { - "maap-dps:algorithm_name": job_metadata["algorithm_name"], - "maap-dps:algorithm_version": job_metadata["algorithm_version"], - "maap-dps:username": job_metadata["username"], - "maap-dps:tag": job_metadata["tag"], - "created": processing_time, - } - ) item.stac_extensions[:] = list(dict.fromkeys(item.stac_extensions)) - if DPS_STAC_EXTENSION not in item.stac_extensions: - item.stac_extensions.append(DPS_STAC_EXTENSION) + MaapDpsExtension.ext(item, add_if_missing=True).apply( + algorithm_name=job_metadata["algorithm_name"], + algorithm_version=job_metadata["algorithm_version"], + username=job_metadata["username"], + tag=job_metadata["tag"], + ) + item.properties["created"] = processing_time item.add_asset( "dps-metadata", Asset( diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock b/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock index edae1ec..5946a9a 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock +++ b/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock @@ -27,11 +27,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -100,6 +100,7 @@ dependencies = [ { name = "obstore" }, { name = "pydantic" }, { name = "pystac", extra = ["validation"] }, + { name = "pystac-ext-maap-dps" }, { name = "python-slugify" }, { name = "stac-pydantic" }, ] @@ -116,7 +117,8 @@ dev = [ requires-dist = [ { name = "obstore", specifier = ">=0.7.0" }, { name = "pydantic", specifier = ">=2.11.0" }, - { name = "pystac", extras = ["validation"], specifier = ">=1.13.0" }, + { name = "pystac", extras = ["validation"], specifier = ">=1.15.2" }, + { name = "pystac-ext-maap-dps", git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=851bd53cec57775f12199dcea94b5800c3623923" }, { name = "python-slugify", specifier = "==8.0.4" }, { name = "stac-pydantic", specifier = ">=3.2.0" }, ] @@ -207,7 +209,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -215,9 +217,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [[package]] @@ -388,14 +390,36 @@ wheels = [ [[package]] name = "pystac" -version = "1.14.1" +version = "1.15.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/bf/e0d6f143b878a16f2117f24ba73f19a482d081d691bc086a9354b6e0ef24/pystac-1.14.1.tar.gz", hash = "sha256:4def289ab2168d67492ed0b5a3bd738d3dfa42390a50563776bfd1558af38d53", size = 163434, upload-time = "2025-09-18T15:13:49.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/01/eb465e19137b36ba683417e982907aa9c7df1fb0b968e1424e5d678ba0dc/pystac-1.14.1-py3-none-any.whl", hash = "sha256:19d73306d8fb94fbd66b7945ee5510e3574c8d48462f86e1e91e3f257b79722b", size = 207710, upload-time = "2025-09-18T15:13:47.189Z" }, + { name = "pystac-core" }, + { name = "pystac-ext-classification" }, + { name = "pystac-ext-datacube" }, + { name = "pystac-ext-eo" }, + { name = "pystac-ext-file" }, + { name = "pystac-ext-grid" }, + { name = "pystac-ext-item-assets" }, + { name = "pystac-ext-label" }, + { name = "pystac-ext-mgrs" }, + { name = "pystac-ext-mlm" }, + { name = "pystac-ext-pointcloud" }, + { name = "pystac-ext-projection" }, + { name = "pystac-ext-raster" }, + { name = "pystac-ext-render" }, + { name = "pystac-ext-sar" }, + { name = "pystac-ext-sat" }, + { name = "pystac-ext-scientific" }, + { name = "pystac-ext-storage" }, + { name = "pystac-ext-table" }, + { name = "pystac-ext-timestamps" }, + { name = "pystac-ext-version" }, + { name = "pystac-ext-view" }, + { name = "pystac-ext-xarray-assets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/38/300a9bc5bf30748c7bc768afd8564e0b6958810c8ca0c50f1c0847676a40/pystac-1.15.2.tar.gz", hash = "sha256:4c9a3b2352cad7eb3c226145251c65cbe5c1b04b25d4265b30c015dddf5f391d", size = 7595, upload-time = "2026-07-27T15:47:43.842Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/54/81ca429d8ee0fa1ff78ad3102a612cfb8224aae4ba42ac625c726b13f81c/pystac-1.15.2-py3-none-any.whl", hash = "sha256:b006833253d7e5acd04d877bb7504e14aa1dc98dd0f0ab12f893595030dda2b3", size = 5694, upload-time = "2026-07-27T15:47:42.768Z" }, ] [package.optional-dependencies] @@ -403,6 +427,295 @@ validation = [ { name = "jsonschema" }, ] +[[package]] +name = "pystac-core" +version = "1.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/67/892aaa463188a259b9538d9d49f420d761ba8fcb8464bf0d8b2cb0a2ef56/pystac_core-1.15.2.tar.gz", hash = "sha256:3720485dd06334e6536afe8f2b8bb0def5c65437170e0251b3417abb89d6b3f3", size = 93857, upload-time = "2026-07-27T15:47:53.04Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/d6/3c65df53ac66532e4451917ed63dcc7c260db30eed0d5c5f1782fcc4128e/pystac_core-1.15.2-py3-none-any.whl", hash = "sha256:96e4acf5eb08ba8be543729b59c3072103bffa7b90283c7497cb25fbff70e6cf", size = 117305, upload-time = "2026-07-27T15:47:51.866Z" }, +] + +[[package]] +name = "pystac-ext-classification" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, + { name = "pystac-ext-raster" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/45/f17a8ac5ab0e184ea5d7bf8b90410bb915ec1ef7598714d657267114491c/pystac_ext_classification-2.0.1.tar.gz", hash = "sha256:be271440ef6f676e0d48e3238100e29c4f6a1a92626a1353c4728c4f8ea0fabe", size = 21404, upload-time = "2026-08-10T15:54:27.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/6e/a2bc64146ac5afb5a72815288ffb811a4ada5124b49a96fa5bce39dd76aa/pystac_ext_classification-2.0.1-py3-none-any.whl", hash = "sha256:966939c42314487d40ba37824eb5c4575cffc841fdda3f8da6f61be1fde39f17", size = 7356, upload-time = "2026-08-10T15:54:26.502Z" }, +] + +[[package]] +name = "pystac-ext-datacube" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/c5/e1f085cd97de803187277dd8d5ea6f029ec1b25388d523e4f0bd0ffc14a1/pystac_ext_datacube-2.2.1.tar.gz", hash = "sha256:80f7ecbc82dee5d713f2c6ad4f87cd01a7559c1f77fc9e51d2ff9bd06c4db5ae", size = 22256, upload-time = "2026-08-10T15:56:09.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/65/971e8cbe32b3783b7872075822697b6a9899d34963813834e26b41d98fd2/pystac_ext_datacube-2.2.1-py3-none-any.whl", hash = "sha256:e88876860aa96b002786139547faa6acc760424f90051141322668c91e78c56d", size = 7116, upload-time = "2026-08-10T15:56:08.529Z" }, +] + +[[package]] +name = "pystac-ext-eo" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, + { name = "pystac-ext-projection" }, + { name = "pystac-ext-view" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/fb/696063acebb9b3e5af0a560678a36a31f5d4ab62de3d78440088ba46ba24/pystac_ext_eo-1.1.1.tar.gz", hash = "sha256:83a94d974cf57aa4b21b1039b8fd527f0a5e07d9f3534aa3fa5752f59c694301", size = 21365, upload-time = "2026-08-10T15:54:06.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/46/2e067dcc7eb4a2ec5ee088e666e9eef8cfeadd3f291b39dfa4ac6fcfd7cc/pystac_ext_eo-1.1.1-py3-none-any.whl", hash = "sha256:053e3a5fa7e6bd6cb4e1a97f0aaab944bfff554e2d6264c5f81d9b1593d7582c", size = 7653, upload-time = "2026-08-10T15:54:05.329Z" }, +] + +[[package]] +name = "pystac-ext-file" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/d44b7ac9377468eb62f1ac0053217edc01d87242e236b9c216c3deeee4d8/pystac_ext_file-2.1.1.tar.gz", hash = "sha256:b2b72eedc7c77dd3ff4efe2ff57956046cd1e66ab28d53df0b693e56c37d1c96", size = 13204, upload-time = "2026-08-10T15:55:02.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/36/3f36a36874cb080caa1b61b33f7e1e2d423ddef2a26c3238101397e17d21/pystac_ext_file-2.1.1-py3-none-any.whl", hash = "sha256:bee49e1768d585050c1fb278c328cd61808d4615cb69d4a24fa75eb387774753", size = 5763, upload-time = "2026-08-10T15:55:01.128Z" }, +] + +[[package]] +name = "pystac-ext-grid" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/26/9252ac31d8de5a93e7f884ba73a4a19b4885275f2d11e0cbbda736077308/pystac_ext_grid-1.1.1.tar.gz", hash = "sha256:00070bc4fd26aadc6b21a1f24250e33c1719773fff14487bd0c08b0b89f06438", size = 9294, upload-time = "2026-08-10T15:55:24.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/db/49eb396e89fbd5cd0f7f05c0c5f5f4219f13d4231d6714af59a907d90738/pystac_ext_grid-1.1.1-py3-none-any.whl", hash = "sha256:eaf1bb7439946c2dc82e2b54d75d795e29a4fb10276bc37c973fbd352d9f03ed", size = 3599, upload-time = "2026-08-10T15:55:23.419Z" }, +] + +[[package]] +name = "pystac-ext-item-assets" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/f3/66875a6aa034f0ec41e22ce3660a4c180eadf4116d728dfbfed746c042c4/pystac_ext_item_assets-1.0.1.tar.gz", hash = "sha256:0a69d0c19e56ff481bbc5bf652639a99f084e30a1307a29b45aeedaf9a1611aa", size = 4099, upload-time = "2026-08-10T15:54:52.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/12/7331f1ba03359b390d7ae5940dd47cab58792d20c734d24daa9b50da199e/pystac_ext_item_assets-1.0.1-py3-none-any.whl", hash = "sha256:be8c014edc6eeacd6793880706826bedb38218fda652b019c00ee17c9ea2dfed", size = 3728, upload-time = "2026-08-10T15:54:51.295Z" }, +] + +[[package]] +name = "pystac-ext-label" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/df/fc8d52a20c5b6ee5dc1d08aca11d927219de3745632264a295c2bc1695f9/pystac_ext_label-1.0.2.tar.gz", hash = "sha256:92c195ba9049b1ba7ee23e1a2b53a00dc6060f2438fc51d398eebdf7d3c6a8ba", size = 8613, upload-time = "2026-08-10T15:54:32.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/11/9d9b4e9e8429949ea94ecb6f53778f2a26028793a27f6559f29fc0f88704/pystac_ext_label-1.0.2-py3-none-any.whl", hash = "sha256:36acaaf1f452693f02dd11602374a87855939bc39e6d979990f33a7f305d05ff", size = 8150, upload-time = "2026-08-10T15:54:31.645Z" }, +] + +[[package]] +name = "pystac-ext-maap-dps" +version = "0.1.0" +source = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=851bd53cec57775f12199dcea94b5800c3623923#851bd53cec57775f12199dcea94b5800c3623923" } +dependencies = [ + { name = "pystac-core" }, +] + +[[package]] +name = "pystac-ext-mgrs" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/3138c5cb98bcda995f1cdd8b9fe0569b788861a387ed37f0e21932eaa42a/pystac_ext_mgrs-1.0.1.tar.gz", hash = "sha256:ac96294b5b81a4df655e9162809c09c97cf1af21c1b4f146d9cd398caad39f44", size = 6827, upload-time = "2026-08-10T15:54:50.366Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/dd/af65ac94fb019eff59a3b726506872ef9376246368ef9e036bc4ba4b95f4/pystac_ext_mgrs-1.0.1-py3-none-any.whl", hash = "sha256:75ded1355764b26aa1031e812089befba1191906904e05350ae2a578f34485c8", size = 4127, upload-time = "2026-08-10T15:54:49.259Z" }, +] + +[[package]] +name = "pystac-ext-mlm" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, + { name = "pystac-ext-classification" }, + { name = "pystac-ext-raster" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/f0/6389add8da669c9c27fe65cd423ca2b003fe43222095d7b0397c35566130/pystac_ext_mlm-1.4.1.tar.gz", hash = "sha256:f27ee69816e6d076eec5bef1f9fc7778850d332afeccc598f787bf53c9439019", size = 36264, upload-time = "2026-08-10T15:55:10.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/e6/04827c118441fb9389e7c6652ef81088fc8c755e2a8705a392e21e3f5ffe/pystac_ext_mlm-1.4.1-py3-none-any.whl", hash = "sha256:64f74a485ce167093a699e60be1358562817d5c98d55f9e990021a9763600269", size = 13889, upload-time = "2026-08-10T15:55:08.957Z" }, +] + +[[package]] +name = "pystac-ext-pointcloud" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/75/51/3699ba67f6f8d1fad9664bfd8e45daad360d27f328438dc1ac1b2469e2b6/pystac_ext_pointcloud-1.0.1.tar.gz", hash = "sha256:995b9ab0d84d22a2495add7a709f97b5cd9685c180d6da9af29c87383919aa15", size = 12815, upload-time = "2026-08-10T15:55:37.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5f/108711e6892fe353ee82f467702a0e4db4e14f4836590ff5d21b82383654/pystac_ext_pointcloud-1.0.1-py3-none-any.whl", hash = "sha256:e32ee5e794b3dccc3ab1f2c3f8e11422da4dce882061123f66e9ddaa6ae6f009", size = 6467, upload-time = "2026-08-10T15:55:36.495Z" }, +] + +[[package]] +name = "pystac-ext-projection" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/cd/bcf41fd04bac5fc47bcb6a36aa367746effedf8aa2cf5601ca54692ccb5b/pystac_ext_projection-2.0.1.tar.gz", hash = "sha256:191bc730689ffeda7bdc94c5719f75ed951033f0b5e7a035a0af27213f33e085", size = 72321, upload-time = "2026-08-10T15:54:30.987Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/ad/a983aae95495fa23cdcbf9735642bd9ab5e6cf37fd41c4c2b2f979287a6e/pystac_ext_projection-2.0.1-py3-none-any.whl", hash = "sha256:2c81997ee09a3332fa2b2bf635f2343034d8f87e612d4ecc2bea072aa5733acc", size = 7410, upload-time = "2026-08-10T15:54:29.988Z" }, +] + +[[package]] +name = "pystac-ext-raster" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/e3/2a3856e4faab77bd30cd6060668712facbea561c316fc55b78351a1a373a/pystac_ext_raster-1.1.1.tar.gz", hash = "sha256:1249d34332cfd05634a31eb6f9d63d977cef4320d68876054bdbc1319ffc8b8a", size = 30921, upload-time = "2026-08-10T15:55:27.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/ce/cf54e3448f65c78dfa56ec2f00e9f3d76286a8411d8141a84328bba9fe25/pystac_ext_raster-1.1.1-py3-none-any.whl", hash = "sha256:69a634c98df2537a487155348d766de5038f545c560f45752844bd763fec2801", size = 6850, upload-time = "2026-08-10T15:55:26.652Z" }, +] + +[[package]] +name = "pystac-ext-render" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/ba/e8711c8b46719f4d8e74fd73c5ae66fa5cd0b79e0dc7770d00bc1d14adcf/pystac_ext_render-2.0.0.tar.gz", hash = "sha256:ed53926d0af556c98d5f6bbfd86b8d02d635df24b70607f37f386cea7713b4f7", size = 9611, upload-time = "2026-06-25T15:50:00.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/63/3162c536d3f3cacf627b7a74915dc90abb10cf3baa5ed9b28b3b92b5914c/pystac_ext_render-2.0.0-py3-none-any.whl", hash = "sha256:69c3a8307d9528b8ae5baadde76dae9e546c19c1f4210a57daf235f8e9d9e427", size = 5020, upload-time = "2026-06-25T15:49:59.974Z" }, +] + +[[package]] +name = "pystac-ext-sar" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/57/8ebb150bb69f441ba98280b147f0b4a7f9c9d0dbb54984c3e7109a83d158/pystac_ext_sar-1.0.1.tar.gz", hash = "sha256:3a1a24a7429d74570a3fb93c534095c18fb42e1b3c8e1c4cdb594091eb23ee0c", size = 11345, upload-time = "2026-08-10T15:55:04.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/c0/a3a64c3446aa4d41bd551b23127a8dbe45c5abff05b6cdab43edef5c54f8/pystac_ext_sar-1.0.1-py3-none-any.whl", hash = "sha256:58bb69f503250e5b3f77a60a0b5947d4d67440c36f25ebb34698698ded4a0e26", size = 6537, upload-time = "2026-08-10T15:55:03.94Z" }, +] + +[[package]] +name = "pystac-ext-sat" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/fb/93551098c26a4c3b599e5e14c92ed8b6148213ba779b93776288782164b9/pystac_ext_sat-1.0.1.tar.gz", hash = "sha256:aa7bce65192335d08b45b21f76cf66380626209c90b8ea5d817de82392c898e8", size = 10105, upload-time = "2026-08-10T15:55:01.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f9/d643fd0854f79c311becb0d7652e994870b796127d1a5ac651c0adec21ea/pystac_ext_sat-1.0.1-py3-none-any.whl", hash = "sha256:6f8e1381289f07d1994a65ae14f728f71ea39273efce063e506705c480f3e775", size = 4852, upload-time = "2026-08-10T15:55:00.359Z" }, +] + +[[package]] +name = "pystac-ext-scientific" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/89/f613d8e69cca4749dae2cf35fb2cd53729096beba3708284a91cc4b0a5d1/pystac_ext_scientific-1.0.1.tar.gz", hash = "sha256:8437804a8cd096c7ed766b11a05cddcdf1cb8994766d34e610eaec3fe1918001", size = 13011, upload-time = "2026-08-10T15:55:29.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/16/93d1114ce68565432abf66a79197a19e7b91345f1df9debdddebe3ea500c/pystac_ext_scientific-1.0.1-py3-none-any.whl", hash = "sha256:26b6c1b626db6218efd14e7e963d9873c48f68085c5f29693f0e5902a849bc90", size = 5478, upload-time = "2026-08-10T15:55:28.348Z" }, +] + +[[package]] +name = "pystac-ext-storage" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/1c/1496356b02684092620b291ed1346b6e4cba1033b4e9024091b2935ab92e/pystac_ext_storage-2.0.1.tar.gz", hash = "sha256:11e020767dcff6b4b8d7880d26d0f366368af922c825445a165b70c9fd302492", size = 13293, upload-time = "2026-08-10T15:55:23.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/5f/bca2c8feeee8d5eb817dee96e332f3817041444e3bb73f26f9eefbc4e1c3/pystac_ext_storage-2.0.1-py3-none-any.whl", hash = "sha256:3341b858e77bd317fde3aa147e974dfa82488a1a4c08e876058f28ec5b57c058", size = 7645, upload-time = "2026-08-10T15:55:21.989Z" }, +] + +[[package]] +name = "pystac-ext-table" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/45/4ed94fc528a8d0a28bfba3b1c1d4da0a5342c71ae3e311b25b7ba41b5745/pystac_ext_table-1.2.1.tar.gz", hash = "sha256:585bc36338b261dffa1a1b23e7d148e8788ed8a1e9bc71baabe8a566ba6271c3", size = 8217, upload-time = "2026-08-10T15:54:59.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/a4/56d56a017d8caa0be9b0296adbcd405397ec7f7b511d96a8408aea043f41/pystac_ext_table-1.2.1-py3-none-any.whl", hash = "sha256:60dd80deefb0b2f94ec8cf696c9cbfc8b6a530e769d0bbe50a055caa3de07e77", size = 4713, upload-time = "2026-08-10T15:54:57.139Z" }, +] + +[[package]] +name = "pystac-ext-timestamps" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/27/6d2fb5f443903d18e5d2c4c001a75d3c002a9b80f52addec4c25db8855a7/pystac_ext_timestamps-1.1.1.tar.gz", hash = "sha256:6c57bbaec88ddef129f479452ca7863c3d16dba12ecd7b700a7f9fb3eb6e5673", size = 8842, upload-time = "2026-08-10T15:54:51.498Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/d9/bf6107bec935f373ea7b42d04dfcb48905018d8e941c36de83f9e78f83df/pystac_ext_timestamps-1.1.1-py3-none-any.whl", hash = "sha256:f68e368edea169dc102241b24784d54600c8e9bc4d3571f6e3288ad29199be67", size = 4555, upload-time = "2026-08-10T15:54:50.455Z" }, +] + +[[package]] +name = "pystac-ext-version" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/cf/b04b9729e50b11f3a3e8db5a2ccfb027ac683add42cc105d040435ac9010/pystac_ext_version-1.2.1.tar.gz", hash = "sha256:7ae63cab8440ee7e2da19e2024ecd55a7affb022ba9384eefe5bd98ce85f21da", size = 12884, upload-time = "2026-08-10T15:55:46.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/a5/f9cb68b2d1f02cf51bf8092e1667e776cb4930407d40a9b635321db6547a/pystac_ext_version-1.2.1-py3-none-any.whl", hash = "sha256:895e66f72f31b34e63f1b3d39def05bb7e3be6906cff951d4b4da4c617bfcb41", size = 5778, upload-time = "2026-08-10T15:55:45.972Z" }, +] + +[[package]] +name = "pystac-ext-view" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/ff/ee1dd45140d3f4a837720f9051cab5442a2015e1a8ef7105b881b16232c6/pystac_ext_view-1.0.1.tar.gz", hash = "sha256:11b5ea7ebcd717a86ef256abe4270ab6c1ab701683d3b4fc0d104ea194d759e2", size = 9932, upload-time = "2026-08-10T15:55:49.763Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/60/ca15164332ddd65c4aba3c856fb0a43da5317e0134815b2d483a4d15ea53/pystac_ext_view-1.0.1-py3-none-any.whl", hash = "sha256:7d8dab8f638589c2b17496e17770c98759f8c20e36ebaab931af43cf25b5c504", size = 4761, upload-time = "2026-08-10T15:55:48.556Z" }, +] + +[[package]] +name = "pystac-ext-xarray-assets" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pystac-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/06/053bf90500067d27d7a1299a7f58cfe29656e3b8b91d3734b94f2121babd/pystac_ext_xarray_assets-1.0.1.tar.gz", hash = "sha256:7d75e79a2b8702d1f572eecdf8af6b29a1013789f6de5e688a5a3c9c14254de6", size = 6945, upload-time = "2026-08-10T15:55:31.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/a1/4efc99e95c551d63896e667537ee7f1cc0c0583488ac4c6a4ca3c7bd00fe/pystac_ext_xarray_assets-1.0.1-py3-none-any.whl", hash = "sha256:114631f3585aa18d73fa9cae8e256d8937d455af4d9fb4e84f5d0981d2ec3bc4", size = 3931, upload-time = "2026-08-10T15:55:30.065Z" }, +] + [[package]] name = "pytest" version = "9.0.3" @@ -457,97 +770,112 @@ wheels = [ [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, ] [[package]] name = "rpds-py" -version = "0.27.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" }, - { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" }, - { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" }, - { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" }, - { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" }, - { url = "https://files.pythonhosted.org/packages/cc/77/610aeee8d41e39080c7e14afa5387138e3c9fa9756ab893d09d99e7d8e98/rpds_py-0.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e4b9fcfbc021633863a37e92571d6f91851fa656f0180246e84cbd8b3f6b329b", size = 361741, upload-time = "2025-08-27T12:13:31.039Z" }, - { url = "https://files.pythonhosted.org/packages/3a/fc/c43765f201c6a1c60be2043cbdb664013def52460a4c7adace89d6682bf4/rpds_py-0.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1441811a96eadca93c517d08df75de45e5ffe68aa3089924f963c782c4b898cf", size = 345574, upload-time = "2025-08-27T12:13:32.902Z" }, - { url = "https://files.pythonhosted.org/packages/20/42/ee2b2ca114294cd9847d0ef9c26d2b0851b2e7e00bf14cc4c0b581df0fc3/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55266dafa22e672f5a4f65019015f90336ed31c6383bd53f5e7826d21a0e0b83", size = 385051, upload-time = "2025-08-27T12:13:34.228Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e8/1e430fe311e4799e02e2d1af7c765f024e95e17d651612425b226705f910/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78827d7ac08627ea2c8e02c9e5b41180ea5ea1f747e9db0915e3adf36b62dcf", size = 398395, upload-time = "2025-08-27T12:13:36.132Z" }, - { url = "https://files.pythonhosted.org/packages/82/95/9dc227d441ff2670651c27a739acb2535ccaf8b351a88d78c088965e5996/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae92443798a40a92dc5f0b01d8a7c93adde0c4dc965310a29ae7c64d72b9fad2", size = 524334, upload-time = "2025-08-27T12:13:37.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/01/a670c232f401d9ad461d9a332aa4080cd3cb1d1df18213dbd0d2a6a7ab51/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c46c9dd2403b66a2a3b9720ec4b74d4ab49d4fabf9f03dfdce2d42af913fe8d0", size = 407691, upload-time = "2025-08-27T12:13:38.94Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/0a14aebbaa26fe7fab4780c76f2239e76cc95a0090bdb25e31d95c492fcd/rpds_py-0.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2efe4eb1d01b7f5f1939f4ef30ecea6c6b3521eec451fb93191bf84b2a522418", size = 386868, upload-time = "2025-08-27T12:13:40.192Z" }, - { url = "https://files.pythonhosted.org/packages/3b/03/8c897fb8b5347ff6c1cc31239b9611c5bf79d78c984430887a353e1409a1/rpds_py-0.27.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:15d3b4d83582d10c601f481eca29c3f138d44c92187d197aff663a269197c02d", size = 405469, upload-time = "2025-08-27T12:13:41.496Z" }, - { url = "https://files.pythonhosted.org/packages/da/07/88c60edc2df74850d496d78a1fdcdc7b54360a7f610a4d50008309d41b94/rpds_py-0.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4ed2e16abbc982a169d30d1a420274a709949e2cbdef119fe2ec9d870b42f274", size = 422125, upload-time = "2025-08-27T12:13:42.802Z" }, - { url = "https://files.pythonhosted.org/packages/6b/86/5f4c707603e41b05f191a749984f390dabcbc467cf833769b47bf14ba04f/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a75f305c9b013289121ec0f1181931975df78738cdf650093e6b86d74aa7d8dd", size = 562341, upload-time = "2025-08-27T12:13:44.472Z" }, - { url = "https://files.pythonhosted.org/packages/b2/92/3c0cb2492094e3cd9baf9e49bbb7befeceb584ea0c1a8b5939dca4da12e5/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:67ce7620704745881a3d4b0ada80ab4d99df390838839921f99e63c474f82cf2", size = 592511, upload-time = "2025-08-27T12:13:45.898Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/82e64fbb0047c46a168faa28d0d45a7851cd0582f850b966811d30f67ad8/rpds_py-0.27.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d992ac10eb86d9b6f369647b6a3f412fc0075cfd5d799530e84d335e440a002", size = 557736, upload-time = "2025-08-27T12:13:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/3c863973d409210da7fb41958172c6b7dbe7fc34e04d3cc1f10bb85e979f/rpds_py-0.27.1-cp313-cp313-win32.whl", hash = "sha256:4f75e4bd8ab8db624e02c8e2fc4063021b58becdbe6df793a8111d9343aec1e3", size = 221462, upload-time = "2025-08-27T12:13:48.742Z" }, - { url = "https://files.pythonhosted.org/packages/ce/2c/5867b14a81dc217b56d95a9f2a40fdbc56a1ab0181b80132beeecbd4b2d6/rpds_py-0.27.1-cp313-cp313-win_amd64.whl", hash = "sha256:f9025faafc62ed0b75a53e541895ca272815bec18abe2249ff6501c8f2e12b83", size = 232034, upload-time = "2025-08-27T12:13:50.11Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/3958f3f018c01923823f1e47f1cc338e398814b92d83cd278364446fac66/rpds_py-0.27.1-cp313-cp313-win_arm64.whl", hash = "sha256:ed10dc32829e7d222b7d3b93136d25a406ba9788f6a7ebf6809092da1f4d279d", size = 222392, upload-time = "2025-08-27T12:13:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/01/76/1cdf1f91aed5c3a7bf2eba1f1c4e4d6f57832d73003919a20118870ea659/rpds_py-0.27.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:92022bbbad0d4426e616815b16bc4127f83c9a74940e1ccf3cfe0b387aba0228", size = 358355, upload-time = "2025-08-27T12:13:54.012Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6f/bf142541229374287604caf3bb2a4ae17f0a580798fd72d3b009b532db4e/rpds_py-0.27.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47162fdab9407ec3f160805ac3e154df042e577dd53341745fc7fb3f625e6d92", size = 342138, upload-time = "2025-08-27T12:13:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/1a/77/355b1c041d6be40886c44ff5e798b4e2769e497b790f0f7fd1e78d17e9a8/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb89bec23fddc489e5d78b550a7b773557c9ab58b7946154a10a6f7a214a48b2", size = 380247, upload-time = "2025-08-27T12:13:57.683Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a4/d9cef5c3946ea271ce2243c51481971cd6e34f21925af2783dd17b26e815/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e48af21883ded2b3e9eb48cb7880ad8598b31ab752ff3be6457001d78f416723", size = 390699, upload-time = "2025-08-27T12:13:59.137Z" }, - { url = "https://files.pythonhosted.org/packages/3a/06/005106a7b8c6c1a7e91b73169e49870f4af5256119d34a361ae5240a0c1d/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f5b7bd8e219ed50299e58551a410b64daafb5017d54bbe822e003856f06a802", size = 521852, upload-time = "2025-08-27T12:14:00.583Z" }, - { url = "https://files.pythonhosted.org/packages/e5/3e/50fb1dac0948e17a02eb05c24510a8fe12d5ce8561c6b7b7d1339ab7ab9c/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08f1e20bccf73b08d12d804d6e1c22ca5530e71659e6673bce31a6bb71c1e73f", size = 402582, upload-time = "2025-08-27T12:14:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/cb/b0/f4e224090dc5b0ec15f31a02d746ab24101dd430847c4d99123798661bfc/rpds_py-0.27.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dc5dceeaefcc96dc192e3a80bbe1d6c410c469e97bdd47494a7d930987f18b2", size = 384126, upload-time = "2025-08-27T12:14:03.437Z" }, - { url = "https://files.pythonhosted.org/packages/54/77/ac339d5f82b6afff1df8f0fe0d2145cc827992cb5f8eeb90fc9f31ef7a63/rpds_py-0.27.1-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d76f9cc8665acdc0c9177043746775aa7babbf479b5520b78ae4002d889f5c21", size = 399486, upload-time = "2025-08-27T12:14:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/d6/29/3e1c255eee6ac358c056a57d6d6869baa00a62fa32eea5ee0632039c50a3/rpds_py-0.27.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:134fae0e36022edad8290a6661edf40c023562964efea0cc0ec7f5d392d2aaef", size = 414832, upload-time = "2025-08-27T12:14:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/3f/db/6d498b844342deb3fa1d030598db93937a9964fcf5cb4da4feb5f17be34b/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb11a4f1b2b63337cfd3b4d110af778a59aae51c81d195768e353d8b52f88081", size = 557249, upload-time = "2025-08-27T12:14:08.37Z" }, - { url = "https://files.pythonhosted.org/packages/60/f3/690dd38e2310b6f68858a331399b4d6dbb9132c3e8ef8b4333b96caf403d/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:13e608ac9f50a0ed4faec0e90ece76ae33b34c0e8656e3dceb9a7db994c692cd", size = 587356, upload-time = "2025-08-27T12:14:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/86/e3/84507781cccd0145f35b1dc32c72675200c5ce8d5b30f813e49424ef68fc/rpds_py-0.27.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dd2135527aa40f061350c3f8f89da2644de26cd73e4de458e79606384f4f68e7", size = 555300, upload-time = "2025-08-27T12:14:11.783Z" }, - { url = "https://files.pythonhosted.org/packages/e5/ee/375469849e6b429b3516206b4580a79e9ef3eb12920ddbd4492b56eaacbe/rpds_py-0.27.1-cp313-cp313t-win32.whl", hash = "sha256:3020724ade63fe320a972e2ffd93b5623227e684315adce194941167fee02688", size = 216714, upload-time = "2025-08-27T12:14:13.629Z" }, - { url = "https://files.pythonhosted.org/packages/21/87/3fc94e47c9bd0742660e84706c311a860dcae4374cf4a03c477e23ce605a/rpds_py-0.27.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8ee50c3e41739886606388ba3ab3ee2aae9f35fb23f833091833255a31740797", size = 228943, upload-time = "2025-08-27T12:14:14.937Z" }, - { url = "https://files.pythonhosted.org/packages/70/36/b6e6066520a07cf029d385de869729a895917b411e777ab1cde878100a1d/rpds_py-0.27.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:acb9aafccaae278f449d9c713b64a9e68662e7799dbd5859e2c6b3c67b56d334", size = 362472, upload-time = "2025-08-27T12:14:16.333Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/b4646032e0dcec0df9c73a3bd52f63bc6c5f9cda992f06bd0e73fe3fbebd/rpds_py-0.27.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b7fb801aa7f845ddf601c49630deeeccde7ce10065561d92729bfe81bd21fb33", size = 345676, upload-time = "2025-08-27T12:14:17.764Z" }, - { url = "https://files.pythonhosted.org/packages/b0/16/2f1003ee5d0af4bcb13c0cf894957984c32a6751ed7206db2aee7379a55e/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe0dd05afb46597b9a2e11c351e5e4283c741237e7f617ffb3252780cca9336a", size = 385313, upload-time = "2025-08-27T12:14:19.829Z" }, - { url = "https://files.pythonhosted.org/packages/05/cd/7eb6dd7b232e7f2654d03fa07f1414d7dfc980e82ba71e40a7c46fd95484/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b6dfb0e058adb12d8b1d1b25f686e94ffa65d9995a5157afe99743bf7369d62b", size = 399080, upload-time = "2025-08-27T12:14:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/20/51/5829afd5000ec1cb60f304711f02572d619040aa3ec033d8226817d1e571/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed090ccd235f6fa8bb5861684567f0a83e04f52dfc2e5c05f2e4b1309fcf85e7", size = 523868, upload-time = "2025-08-27T12:14:23.485Z" }, - { url = "https://files.pythonhosted.org/packages/05/2c/30eebca20d5db95720ab4d2faec1b5e4c1025c473f703738c371241476a2/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf876e79763eecf3e7356f157540d6a093cef395b65514f17a356f62af6cc136", size = 408750, upload-time = "2025-08-27T12:14:24.924Z" }, - { url = "https://files.pythonhosted.org/packages/90/1a/cdb5083f043597c4d4276eae4e4c70c55ab5accec078da8611f24575a367/rpds_py-0.27.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ed005216a51b1d6e2b02a7bd31885fe317e45897de81d86dcce7d74618ffff", size = 387688, upload-time = "2025-08-27T12:14:27.537Z" }, - { url = "https://files.pythonhosted.org/packages/7c/92/cf786a15320e173f945d205ab31585cc43969743bb1a48b6888f7a2b0a2d/rpds_py-0.27.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ee4308f409a40e50593c7e3bb8cbe0b4d4c66d1674a316324f0c2f5383b486f9", size = 407225, upload-time = "2025-08-27T12:14:28.981Z" }, - { url = "https://files.pythonhosted.org/packages/33/5c/85ee16df5b65063ef26017bef33096557a4c83fbe56218ac7cd8c235f16d/rpds_py-0.27.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b08d152555acf1f455154d498ca855618c1378ec810646fcd7c76416ac6dc60", size = 423361, upload-time = "2025-08-27T12:14:30.469Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8e/1c2741307fcabd1a334ecf008e92c4f47bb6f848712cf15c923becfe82bb/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:dce51c828941973a5684d458214d3a36fcd28da3e1875d659388f4f9f12cc33e", size = 562493, upload-time = "2025-08-27T12:14:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/04/03/5159321baae9b2222442a70c1f988cbbd66b9be0675dd3936461269be360/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1476d6f29eb81aa4151c9a31219b03f1f798dc43d8af1250a870735516a1212", size = 592623, upload-time = "2025-08-27T12:14:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/ff/39/c09fd1ad28b85bc1d4554a8710233c9f4cefd03d7717a1b8fbfd171d1167/rpds_py-0.27.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3ce0cac322b0d69b63c9cdb895ee1b65805ec9ffad37639f291dd79467bee675", size = 558800, upload-time = "2025-08-27T12:14:35.436Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d6/99228e6bbcf4baa764b18258f519a9035131d91b538d4e0e294313462a98/rpds_py-0.27.1-cp314-cp314-win32.whl", hash = "sha256:dfbfac137d2a3d0725758cd141f878bf4329ba25e34979797c89474a89a8a3a3", size = 221943, upload-time = "2025-08-27T12:14:36.898Z" }, - { url = "https://files.pythonhosted.org/packages/be/07/c802bc6b8e95be83b79bdf23d1aa61d68324cb1006e245d6c58e959e314d/rpds_py-0.27.1-cp314-cp314-win_amd64.whl", hash = "sha256:a6e57b0abfe7cc513450fcf529eb486b6e4d3f8aee83e92eb5f1ef848218d456", size = 233739, upload-time = "2025-08-27T12:14:38.386Z" }, - { url = "https://files.pythonhosted.org/packages/c8/89/3e1b1c16d4c2d547c5717377a8df99aee8099ff050f87c45cb4d5fa70891/rpds_py-0.27.1-cp314-cp314-win_arm64.whl", hash = "sha256:faf8d146f3d476abfee026c4ae3bdd9ca14236ae4e4c310cbd1cf75ba33d24a3", size = 223120, upload-time = "2025-08-27T12:14:39.82Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/dc7931dc2fa4a6e46b2a4fa744a9fe5c548efd70e0ba74f40b39fa4a8c10/rpds_py-0.27.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:ba81d2b56b6d4911ce735aad0a1d4495e808b8ee4dc58715998741a26874e7c2", size = 358944, upload-time = "2025-08-27T12:14:41.199Z" }, - { url = "https://files.pythonhosted.org/packages/e6/22/4af76ac4e9f336bfb1a5f240d18a33c6b2fcaadb7472ac7680576512b49a/rpds_py-0.27.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:84f7d509870098de0e864cad0102711c1e24e9b1a50ee713b65928adb22269e4", size = 342283, upload-time = "2025-08-27T12:14:42.699Z" }, - { url = "https://files.pythonhosted.org/packages/1c/15/2a7c619b3c2272ea9feb9ade67a45c40b3eeb500d503ad4c28c395dc51b4/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e960fc78fecd1100539f14132425e1d5fe44ecb9239f8f27f079962021523e", size = 380320, upload-time = "2025-08-27T12:14:44.157Z" }, - { url = "https://files.pythonhosted.org/packages/a2/7d/4c6d243ba4a3057e994bb5bedd01b5c963c12fe38dde707a52acdb3849e7/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62f85b665cedab1a503747617393573995dac4600ff51869d69ad2f39eb5e817", size = 391760, upload-time = "2025-08-27T12:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/b4/71/b19401a909b83bcd67f90221330bc1ef11bc486fe4e04c24388d28a618ae/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fed467af29776f6556250c9ed85ea5a4dd121ab56a5f8b206e3e7a4c551e48ec", size = 522476, upload-time = "2025-08-27T12:14:47.364Z" }, - { url = "https://files.pythonhosted.org/packages/e4/44/1a3b9715c0455d2e2f0f6df5ee6d6f5afdc423d0773a8a682ed2b43c566c/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2729615f9d430af0ae6b36cf042cb55c0936408d543fb691e1a9e36648fd35a", size = 403418, upload-time = "2025-08-27T12:14:49.991Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4b/fb6c4f14984eb56673bc868a66536f53417ddb13ed44b391998100a06a96/rpds_py-0.27.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b207d881a9aef7ba753d69c123a35d96ca7cb808056998f6b9e8747321f03b8", size = 384771, upload-time = "2025-08-27T12:14:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/c0/56/d5265d2d28b7420d7b4d4d85cad8ef891760f5135102e60d5c970b976e41/rpds_py-0.27.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:639fd5efec029f99b79ae47e5d7e00ad8a773da899b6309f6786ecaf22948c48", size = 400022, upload-time = "2025-08-27T12:14:53.859Z" }, - { url = "https://files.pythonhosted.org/packages/8f/e9/9f5fc70164a569bdd6ed9046486c3568d6926e3a49bdefeeccfb18655875/rpds_py-0.27.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fecc80cb2a90e28af8a9b366edacf33d7a91cbfe4c2c4544ea1246e949cfebeb", size = 416787, upload-time = "2025-08-27T12:14:55.673Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/56dd03430ba491db943a81dcdef115a985aac5f44f565cd39a00c766d45c/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42a89282d711711d0a62d6f57d81aa43a1368686c45bc1c46b7f079d55692734", size = 557538, upload-time = "2025-08-27T12:14:57.245Z" }, - { url = "https://files.pythonhosted.org/packages/3f/36/92cc885a3129993b1d963a2a42ecf64e6a8e129d2c7cc980dbeba84e55fb/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:cf9931f14223de59551ab9d38ed18d92f14f055a5f78c1d8ad6493f735021bbb", size = 588512, upload-time = "2025-08-27T12:14:58.728Z" }, - { url = "https://files.pythonhosted.org/packages/dd/10/6b283707780a81919f71625351182b4f98932ac89a09023cb61865136244/rpds_py-0.27.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f39f58a27cc6e59f432b568ed8429c7e1641324fbe38131de852cd77b2d534b0", size = 555813, upload-time = "2025-08-27T12:15:00.334Z" }, - { url = "https://files.pythonhosted.org/packages/04/2e/30b5ea18c01379da6272a92825dd7e53dc9d15c88a19e97932d35d430ef7/rpds_py-0.27.1-cp314-cp314t-win32.whl", hash = "sha256:d5fa0ee122dc09e23607a28e6d7b150da16c662e66409bbe85230e4c85bb528a", size = 217385, upload-time = "2025-08-27T12:15:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/7d/97119da51cb1dd3f2f3c0805f155a3aa4a95fa44fe7d78ae15e69edf4f34/rpds_py-0.27.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6567d2bb951e21232c2f660c24cf3470bb96de56cdcb3f071a83feeaff8a2772", size = 230097, upload-time = "2025-08-27T12:15:03.961Z" }, +version = "2026.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, ] [[package]] From b7a7f38488a0bbc1e1244505165bb335af6fa4e5 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 8 Sep 2026 10:06:12 -0500 Subject: [PATCH 5/8] chore: add script to remove job tag from collection ids --- README.md | 12 ++ scripts/migrate_dps_collection_ids.py | 162 ++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100755 scripts/migrate_dps_collection_ids.py diff --git a/README.md b/README.md index 5fd8912..40be16b 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,18 @@ asset containing the source `.met.json` file. The generator also overwrites the STAC Common Metadata `created` property with the UTC publication time shared by all Items generated from that catalog. +To merge legacy tag-specific DPS collections into these tag-free IDs, preview +then apply the database migration: + +```bash +./scripts/migrate_dps_collection_ids.py --dry-run +./scripts/migrate_dps_collection_ids.py --apply +``` + +It recognizes four-part IDs (`username__algorithm__version__tag`), merges their +items into the corresponding three-part ID, and refuses to proceed if that +would create duplicate item IDs. + Collection-only STAC transactions can still be enabled with: - `USER_STAC_COLLECTION_TRANSACTIONS_AUTH_MODE=basic` diff --git a/scripts/migrate_dps_collection_ids.py b/scripts/migrate_dps_collection_ids.py new file mode 100755 index 0000000..32bd45e --- /dev/null +++ b/scripts/migrate_dps_collection_ids.py @@ -0,0 +1,162 @@ +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "psycopg[binary]>=3.2,<4", +# ] +# /// +"""Merge legacy DPS tag collections into tag-free collection IDs. + +By default (or with ``--dry-run``) this reports the changes. Pass ``--apply`` to make them. The script +uses the local compose database by default; use the Docker-network URL when +running from a container: +``postgresql://username:password@database:5432/postgis``. +""" + +from __future__ import annotations + +import argparse +import logging +import os +from collections import defaultdict +from typing import Any + +from psycopg import connect +from psycopg.rows import dict_row + +LOGGER = logging.getLogger(__name__) +DEFAULT_DATABASE_URL = "postgresql://username:password@127.0.0.1:5439/postgis" + + +def target_collection_id(collection_id: str) -> str | None: + """Return the tag-free ID for a four-part legacy DPS collection ID.""" + parts = collection_id.split("__") + if len(parts) != 4 or not all(parts): + return None + return "__".join(parts[:-1]) + + +def migration_plan(connection: Any) -> dict[str, list[str]]: + """Return legacy collection IDs grouped by their replacement ID.""" + with connection.cursor() as cursor: + cursor.execute("SELECT id FROM pgstac.collections ORDER BY id") + collection_ids = [row["id"] for row in cursor.fetchall()] + + plan: dict[str, list[str]] = defaultdict(list) + for collection_id in collection_ids: + if target_id := target_collection_id(collection_id): + plan[target_id].append(collection_id) + return dict(plan) + + +def conflicting_item_ids(connection: Any, plan: dict[str, list[str]]) -> list[str]: + """Return item-ID conflicts that would be created by the migration.""" + sources = [source for source_ids in plan.values() for source in source_ids] + if not sources: + return [] + + with connection.cursor() as cursor: + cursor.execute( + """ + SELECT target_id, id + FROM ( + SELECT COALESCE(mapping.target_id, items.collection) AS target_id, items.id + FROM pgstac.items + LEFT JOIN unnest(%s::text[], %s::text[]) AS mapping(source_id, target_id) + ON items.collection = mapping.source_id + WHERE items.collection = ANY(%s) + OR items.collection = ANY(%s) + ) AS migrated_items + GROUP BY target_id, id + HAVING count(*) > 1 + ORDER BY target_id, id + """, + ( + sources, + [target for target, source_ids in plan.items() for _ in source_ids], + sources, + list(plan), + ), + ) + return [f"{row['target_id']}/{row['id']}" for row in cursor.fetchall()] + + +def apply_migration(connection: Any, plan: dict[str, list[str]]) -> None: + """Create tag-free collections, move their items, and remove old collections.""" + with connection.cursor() as cursor: + for target_id, source_ids in plan.items(): + source_id = source_ids[0] + cursor.execute( + """ + INSERT INTO pgstac.collections (content) + SELECT jsonb_set(content, '{id}', to_jsonb(%s::text)) + FROM pgstac.collections + WHERE id = %s + ON CONFLICT (id) DO NOTHING + """, + (target_id, source_id), + ) + cursor.execute( + """ + INSERT INTO pgstac.items_staging_upsert (content) + SELECT jsonb_set(pgstac.format_item(items), '{collection}', to_jsonb(%s::text)) + FROM pgstac.items + WHERE collection = ANY(%s) + """, + (target_id, source_ids), + ) + cursor.execute( + "DELETE FROM pgstac.items WHERE collection = ANY(%s)", (source_ids,) + ) + cursor.execute( + "DELETE FROM pgstac.collections WHERE id = ANY(%s)", (source_ids,) + ) + + +def parse_args() -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--database-url", + default=os.environ.get("DATABASE_URL", DEFAULT_DATABASE_URL), + help="PostgreSQL URL; defaults to DATABASE_URL then the local compose database.", + ) + parser.add_argument("--apply", action="store_true", help="Perform the migration.") + parser.add_argument( + "--dry-run", action="store_true", help="Report changes without applying them." + ) + return parser.parse_args() + + +def main() -> None: + """Report or apply the DPS collection-ID migration.""" + args = parse_args() + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") + + with connect(args.database_url, row_factory=dict_row) as connection: + plan = migration_plan(connection) + if not plan: + LOGGER.info("No four-part legacy DPS collection IDs found.") + return + + for target_id, source_ids in plan.items(): + LOGGER.info("%s -> %s", ", ".join(source_ids), target_id) + + conflicts = conflicting_item_ids(connection, plan) + if conflicts: + raise SystemExit( + "Refusing to merge duplicate item IDs: " + ", ".join(conflicts) + ) + if args.dry_run or not args.apply: + LOGGER.info( + "Dry run. Re-run with --apply to migrate %d collection(s).", + sum(map(len, plan.values())), + ) + return + + apply_migration(connection, plan) + LOGGER.info("Migrated %d collection(s).", sum(map(len, plan.values()))) + + +if __name__ == "__main__": + main() From e76ce9b7eb3454b4f0e641ee00334ebad2922c3e Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 8 Sep 2026 10:17:40 -0500 Subject: [PATCH 6/8] chore: add git to Dockerfile --- cdk/constructs/DpsStacItemGenerator/runtime/Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/Dockerfile b/cdk/constructs/DpsStacItemGenerator/runtime/Dockerfile index 1adf485..edddd4e 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/Dockerfile +++ b/cdk/constructs/DpsStacItemGenerator/runtime/Dockerfile @@ -10,7 +10,9 @@ WORKDIR /asset COPY runtime/pyproject.toml pyproject.toml COPY runtime/src/dps_stac_item_generator/ dps_stac_item_generator/ -RUN uv export --no-dev --no-editable -o requirements.txt && \ +RUN dnf install -y git && \ + dnf clean all && \ + uv export --no-dev --no-editable -o requirements.txt && \ uv pip install --target /asset -r requirements.txt CMD ["dps_stac_item_generator.handler.handler"] From 3de911663bfa64b25b27c7a23730dfd67978cf84 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 15 Sep 2026 20:15:12 -0500 Subject: [PATCH 7/8] chore: updates for v0.1.0 of maap-dps-stac-extension --- README.md | 2 +- cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml | 2 +- .../runtime/src/dps_stac_item_generator/item.py | 3 ++- .../DpsStacItemGenerator/runtime/tests/test_item.py | 3 ++- .../runtime/tests/test_item_gen_handler.py | 3 +-- cdk/constructs/DpsStacItemGenerator/runtime/uv.lock | 4 ++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 40be16b..5555451 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ User STAC catalog configuration: The DPS item generator assigns unregistered items to collections named `{username}__{algorithm_name}__{algorithm_version}`. Authorized user-supplied collection IDs are preserved. Generated items include the filterable -`maap-dps:algorithm_name`, `maap-dps:algorithm_version`, `maap-dps:username`, +`maap-dps:algorithm_name`, `processing:version`, `maap-dps:username`, and `maap-dps:tag` properties, the MAAP DPS STAC extension, and a `dps-metadata` asset containing the source `.met.json` file. The generator also overwrites the STAC Common Metadata `created` property with the UTC publication time shared by diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml b/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml index 572c178..52b983b 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml +++ b/cdk/constructs/DpsStacItemGenerator/runtime/pyproject.toml @@ -32,4 +32,4 @@ addopts = "-vv --ignore=cdk.out --no-header --tb=native" pythonpath = "." [tool.uv.sources] -pystac-ext-maap-dps = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git", rev = "851bd53cec57775f12199dcea94b5800c3623923" } +pystac-ext-maap-dps = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git", rev = "v0.1.0" } diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py index 0bd6e5b..71e80c5 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/src/dps_stac_item_generator/item.py @@ -20,6 +20,7 @@ COLLECTION_ID_FORMAT = "{username}__{algorithm_name}__{algorithm_version}" + class ObstoreStacIO(DefaultStacIO): def read_text(self, source: Union[str, Link], *args: Any, **kwargs: Any) -> str: parsed = urlparse(str(source)) @@ -168,7 +169,7 @@ def get_stac_items( item.stac_extensions[:] = list(dict.fromkeys(item.stac_extensions)) MaapDpsExtension.ext(item, add_if_missing=True).apply( algorithm_name=job_metadata["algorithm_name"], - algorithm_version=job_metadata["algorithm_version"], + processing_version=job_metadata["algorithm_version"], username=job_metadata["username"], tag=job_metadata["tag"], ) diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py index 90f635b..6943f5a 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item.py @@ -174,7 +174,7 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): assert item.properties.model_dump() == { "datetime": item.properties.datetime, "maap-dps:algorithm_name": "awesome-algo", - "maap-dps:algorithm_version": "0.1", + "processing:version": "0.1", "maap-dps:username": "superman", "maap-dps:tag": "test", "created": item.properties.created, @@ -183,6 +183,7 @@ def test_get_stac_items_success(self, mock_catalog, mock_job_metadata): assert [str(extension) for extension in item.stac_extensions] == [ "https://example.com/existing-extension.json", "https://maap-project.github.io/maap-dps-stac-extension/v0.1.0/schema.json", + "https://stac-extensions.github.io/processing/v1.2.0/schema.json", ] assert item.model_dump()["assets"]["dps-metadata"] == { "href": expected_met_json_href, diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py index f991e09..af7d7bc 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py +++ b/cdk/constructs/DpsStacItemGenerator/runtime/tests/test_item_gen_handler.py @@ -1,8 +1,7 @@ import json import logging -from datetime import datetime -from datetime import timezone import os +from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pystac diff --git a/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock b/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock index 5946a9a..fde66eb 100644 --- a/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock +++ b/cdk/constructs/DpsStacItemGenerator/runtime/uv.lock @@ -118,7 +118,7 @@ requires-dist = [ { name = "obstore", specifier = ">=0.7.0" }, { name = "pydantic", specifier = ">=2.11.0" }, { name = "pystac", extras = ["validation"], specifier = ">=1.15.2" }, - { name = "pystac-ext-maap-dps", git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=851bd53cec57775f12199dcea94b5800c3623923" }, + { name = "pystac-ext-maap-dps", git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=v0.1.0" }, { name = "python-slugify", specifier = "==8.0.4" }, { name = "stac-pydantic", specifier = ">=3.2.0" }, ] @@ -529,7 +529,7 @@ wheels = [ [[package]] name = "pystac-ext-maap-dps" version = "0.1.0" -source = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=851bd53cec57775f12199dcea94b5800c3623923#851bd53cec57775f12199dcea94b5800c3623923" } +source = { git = "https://github.com/MAAP-Project/maap-dps-stac-extension.git?rev=v0.1.0#bd38dc03041c6246abff2d5dce66a9cd200f2674" } dependencies = [ { name = "pystac-core" }, ] From f6902e4b01bbb6613471f5bc9a687a0dc1fcac5c Mon Sep 17 00:00:00 2001 From: hrodmn Date: Wed, 16 Sep 2026 05:43:55 -0500 Subject: [PATCH 8/8] fix: skip migrating collections that would have conflicting item ids --- README.md | 131 ++++++++++++++++++++++- scripts/migrate_dps_collection_ids.py | 144 ++++++++++++++++++++++++-- 2 files changed, 264 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a7e1f7a..1945e08 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,11 @@ then apply the database migration: ``` It recognizes four-part IDs (`username__algorithm__version__tag`), merges their -items into the corresponding three-part ID, and refuses to proceed if that -would create duplicate item IDs. +items into the corresponding three-part ID, and adds the DPS metadata fields +from the legacy ID. Collections containing an item-ID collision after merging +are reported and left unchanged. For a deployed database, follow the +[RDS connection guide](#connect-to-rds-through-an-ssm-tunnel) below and the +RDS usage instructions in the migration script's docstring. Collection-only STAC transactions can still be enabled with: @@ -115,6 +118,130 @@ This has three consequences : 2. In addition, because these APIs _also_ sometimes need access to the internet, a NAT gateway must in addition be deployed in that VPC. 3. For direct, administrative connections to the database, one _must_ go through an instance placed in the same VPC as the database. +### Connect to RDS through an SSM tunnel + +For administrative database access, use the existing PgBouncer EC2 instance +as an SSM network relay and run your database client locally. Forward to the +**RDS endpoint**, not the PgBouncer service, to bypass connection pooling. RDS stays +private, and you do not need to install dependencies on the EC2 instance or +open inbound ports. + +You need the AWS CLI, `jq`, `curl`, and a PostgreSQL client such as `psql` +on your workstation. Also +[install the Session Manager plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html); +it is a separate installation from the AWS CLI. +Configure your AWS profile and region first. Your AWS +identity needs permission to read the stack resources, SSM parameter, and +database secret (including KMS decryption if applicable), and start sessions +using `AWS-StartPortForwardingSessionToRemoteHost`. The EC2 instance must be +SSM-managed with SSM Agent 3.1.1374.0 or later. + +#### Find the database secret + +Choose the deployment you want to connect to: + +| Database | Stack name | SSM parameter type | +| --- | --- | --- | +| User STAC (including DPS outputs) | `MAAP-STAC--userSTAC` | `internal` | +| Public STAC | `MAAP-STAC--pgSTAC` | `public` | + +The examples use userSTAC. Confirm your account and stage, then list the +secrets belonging to that CDK deployment: + +```bash +aws sts get-caller-identity +STAGE=test # change as appropriate +STACK="MAAP-STAC-${STAGE}-userSTAC" # userSTAC or pgSTAC + +aws cloudformation list-stack-resources \ + --stack-name "$STACK" \ + --query 'StackResourceSummaries[?ResourceType==`AWS::SecretsManager::Secret`].[LogicalResourceId,PhysicalResourceId]' \ + --output table +``` + +You can also find these under **CloudFormation → stack → Resources**. + +Select the database secret whose ID contains `pgstacdbbootstrappersecret`, not the +STAC HTTP basic-auth secret. CloudFormation gives you the secret's identifier; retrieve its value +from Secrets Manager. In the same terminal: + +```bash +SECRET_ID='' +DB_SECRET=$(aws secretsmanager get-secret-value \ + --secret-id "$SECRET_ID" --query SecretString --output text) + +export PGHOST=$(jq -er '.host' <<< "$DB_SECRET") +export PGDATABASE=$(jq -er '.dbname' <<< "$DB_SECRET") +export PGUSER=$(jq -er '.username' <<< "$DB_SECRET") +export PGPASSWORD=$(jq -er '.password' <<< "$DB_SECRET") +unset DB_SECRET +``` + +Check that these commands succeed and that `PGHOST` matches the selected RDS +endpoint. Do not print the secret or run these commands with shell tracing +(`set -x`) enabled. + +#### Start the tunnel + +In a second terminal with the same AWS profile and region, retrieve the RDS +endpoint from the same secret and start the session. Variables set in the first +terminal are not available in this terminal: + +```bash +STAGE=test # use the same stage as above +TYPE=internal # use public for the pgSTAC stack +SECRET_ID='' +RDS_HOST=$(aws secretsmanager get-secret-value \ + --secret-id "$SECRET_ID" --query SecretString --output text | jq -er '.host') +INSTANCE_ID=$(aws ssm get-parameter \ + --name "/maap-eoapi/$STAGE/$TYPE/pgbouncer-instance-id" \ + --query Parameter.Value --output text) + +aws ssm start-session \ + --target "$INSTANCE_ID" \ + --document-name AWS-StartPortForwardingSessionToRemoteHost \ + --parameters "{\"host\":[\"$RDS_HOST\"],\"portNumber\":[\"5432\"],\"localPortNumber\":[\"15432\"]}" +``` + +Leave this terminal open while you use the database. The EC2 host needs +network access to RDS on port 5432, as it does for normal PgBouncer traffic. + +#### Connect with a local client + +Back in the first terminal, download the +[AWS RDS CA bundle](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) +and configure TLS. `PGHOSTADDR` sends the connection through localhost while +`PGHOST` retains the RDS hostname for certificate verification: + +```bash +curl --fail --show-error --silent \ + https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ + --output /tmp/maap-rds-global-bundle.pem + +export PGHOSTADDR=127.0.0.1 +export PGPORT=15432 +export PGSSLMODE=verify-full +export PGSSLROOTCERT=/tmp/maap-rds-global-bundle.pem + +psql -c 'SELECT current_database(), current_user;' +psql +``` + +`psql` and other libpq-based clients, including psycopg, can use these `PG*` +environment variables. A client's explicit connection string can override +them; check the tool's connection options before running commands. + +Use `\q` to leave `psql`. When finished, clear the connection variables and +close the SSM session in the second terminal: + +```bash +unset PGPASSWORD PGHOST PGHOSTADDR PGPORT PGDATABASE PGUSER PGSSLMODE PGSSLROOTCERT +``` + +Before destructive operations, confirm the target database and ensure you have +a recoverable backup. For work expected to run for hours, prefer a durable +in-VPC execution environment over a workstation tunnel. + ## Ingestion The term "ingestion" refers to the process of cataloging data in the STAC catalog associated with this deployment. diff --git a/scripts/migrate_dps_collection_ids.py b/scripts/migrate_dps_collection_ids.py index 32bd45e..847b744 100755 --- a/scripts/migrate_dps_collection_ids.py +++ b/scripts/migrate_dps_collection_ids.py @@ -7,10 +7,25 @@ # /// """Merge legacy DPS tag collections into tag-free collection IDs. -By default (or with ``--dry-run``) this reports the changes. Pass ``--apply`` to make them. The script -uses the local compose database by default; use the Docker-network URL when -running from a container: +By default (or with ``--dry-run``) this reports the changes. Pass ``--apply`` to +make them. The script uses the local compose database by default; use the +Docker-network URL when running from a container: ``postgresql://username:password@database:5432/postgis``. + +For deployed RDS, follow the connection guide in +README.md#connect-to-rds-through-an-ssm-tunnel, selecting userSTAC (internal). +Keep the tunnel open and run these commands in the terminal with the PG* +environment variables configured (requires uv):: + + uv run --script scripts/migrate_dps_collection_ids.py --database-url "" --dry-run + uv run --script scripts/migrate_dps_collection_ids.py --database-url "" --apply + +The empty --database-url tells psycopg to use the PG* environment variables. +Omitting it uses DATABASE_URL or the local Compose default instead. +Before applying, review the dry-run plan, confirm a recoverable backup, pause +writers, and drain in-flight ingestion. The conflict check does not prevent +concurrent writes. Verify collection and item counts and check pgSTAC queued +work before resuming ingestion; the deployed stack enables use_queue. """ from __future__ import annotations @@ -49,6 +64,51 @@ def migration_plan(connection: Any) -> dict[str, list[str]]: return dict(plan) +def conflicting_source_collections( + connection: Any, plan: dict[str, list[str]] +) -> list[str]: + """Return legacy collections containing items that conflict after merging.""" + sources = [source for source_ids in plan.values() for source in source_ids] + if not sources: + return [] + + with connection.cursor() as cursor: + cursor.execute( + """ + WITH migrated_items AS ( + SELECT + COALESCE(mapping.target_id, items.collection) AS target_id, + items.collection AS source_id, + items.id + FROM pgstac.items + LEFT JOIN unnest(%s::text[], %s::text[]) + AS mapping(source_id, target_id) + ON items.collection = mapping.source_id + WHERE items.collection = ANY(%s) + OR items.collection = ANY(%s) + ), conflicts AS ( + SELECT target_id, id + FROM migrated_items + GROUP BY target_id, id + HAVING count(*) > 1 + ) + SELECT DISTINCT migrated_items.source_id + FROM migrated_items + JOIN conflicts USING (target_id, id) + WHERE migrated_items.source_id = ANY(%s) + ORDER BY migrated_items.source_id + """, + ( + sources, + [target for target, source_ids in plan.items() for _ in source_ids], + sources, + list(plan), + sources, + ), + ) + return [row["source_id"] for row in cursor.fetchall()] + + def conflicting_item_ids(connection: Any, plan: dict[str, list[str]]) -> list[str]: """Return item-ID conflicts that would be created by the migration.""" sources = [source for source_ids in plan.values() for source in source_ids] @@ -60,9 +120,12 @@ def conflicting_item_ids(connection: Any, plan: dict[str, list[str]]) -> list[st """ SELECT target_id, id FROM ( - SELECT COALESCE(mapping.target_id, items.collection) AS target_id, items.id + SELECT + COALESCE(mapping.target_id, items.collection) AS target_id, + items.id FROM pgstac.items - LEFT JOIN unnest(%s::text[], %s::text[]) AS mapping(source_id, target_id) + LEFT JOIN unnest(%s::text[], %s::text[]) + AS mapping(source_id, target_id) ON items.collection = mapping.source_id WHERE items.collection = ANY(%s) OR items.collection = ANY(%s) @@ -96,14 +159,55 @@ def apply_migration(connection: Any, plan: dict[str, list[str]]) -> None: """, (target_id, source_id), ) + source_parts = [source_id.split("__") for source_id in source_ids] cursor.execute( """ INSERT INTO pgstac.items_staging_upsert (content) - SELECT jsonb_set(pgstac.format_item(items), '{collection}', to_jsonb(%s::text)) + SELECT jsonb_set( + jsonb_set( + jsonb_set( + jsonb_set( + jsonb_set( + pgstac.format_item(items), + '{collection}', + to_jsonb(mapping.target_id) + ), + '{properties,maap-dps:algorithm_name}', + to_jsonb(mapping.algorithm_name) + ), + '{properties,processing:version}', + to_jsonb(mapping.algorithm_version) + ), + '{properties,maap-dps:username}', to_jsonb(mapping.username) + ), + '{properties,maap-dps:tag}', to_jsonb(mapping.tag) + ) FROM pgstac.items - WHERE collection = ANY(%s) + JOIN unnest( + %s::text[], + %s::text[], + %s::text[], + %s::text[], + %s::text[], + %s::text[] + ) AS mapping( + source_id, + target_id, + username, + algorithm_name, + algorithm_version, + tag + ) + ON items.collection = mapping.source_id """, - (target_id, source_ids), + ( + source_ids, + [target_id] * len(source_ids), + [parts[0] for parts in source_parts], + [parts[1] for parts in source_parts], + [parts[2] for parts in source_parts], + [parts[3] for parts in source_parts], + ), ) cursor.execute( "DELETE FROM pgstac.items WHERE collection = ANY(%s)", (source_ids,) @@ -119,7 +223,9 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--database-url", default=os.environ.get("DATABASE_URL", DEFAULT_DATABASE_URL), - help="PostgreSQL URL; defaults to DATABASE_URL then the local compose database.", + help=( + "PostgreSQL URL; defaults to DATABASE_URL then the local compose database." + ), ) parser.add_argument("--apply", action="store_true", help="Perform the migration.") parser.add_argument( @@ -139,6 +245,26 @@ def main() -> None: LOGGER.info("No four-part legacy DPS collection IDs found.") return + skipped_sources = conflicting_source_collections(connection, plan) + if skipped_sources: + LOGGER.warning( + "Skipping %d legacy collection(s) with duplicate item IDs: %s", + len(skipped_sources), + ", ".join(skipped_sources), + ) + skipped = set(skipped_sources) + plan = { + target_id: [ + source_id for source_id in source_ids if source_id not in skipped + ] + for target_id, source_ids in plan.items() + } + plan = { + target_id: source_ids + for target_id, source_ids in plan.items() + if source_ids + } + for target_id, source_ids in plan.items(): LOGGER.info("%s -> %s", ", ".join(source_ids), target_id)