diff --git a/src/claimed/components/geo/.python-version b/src/claimed/components/geo/.python-version new file mode 100644 index 00000000..6324d401 --- /dev/null +++ b/src/claimed/components/geo/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/src/claimed/components/geo/pyproject.toml b/src/claimed/components/geo/pyproject.toml new file mode 100644 index 00000000..ccfe6af7 --- /dev/null +++ b/src/claimed/components/geo/pyproject.toml @@ -0,0 +1,28 @@ +[project] +name = "stac-item-operator" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "gdal==3.11.5 ; sys_platform == 'linux'", + "gdal ; sys_platform == 'win32'", + "pystac>=1.14.3", + "shapely>=2.1.2", +] + +[tool.uv.sources] +gdal = [ + { index = "gdal-wheels", marker = "sys_platform == 'linux'" }, + { index = "geospatial_wheels", marker = "sys_platform == 'win32'" }, +] + +[[tool.uv.index]] +name = "geospatial_wheels" +url = "https://nathanjmcdougall.github.io/geospatial-wheels-index/" +explicit = true + +[[tool.uv.index]] +name = "gdal-wheels" +url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/simple" +explicit = true diff --git a/src/claimed/components/geo/src/__init__.py b/src/claimed/components/geo/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/claimed/components/geo/src/stac_item_operator.py b/src/claimed/components/geo/src/stac_item_operator.py new file mode 100644 index 00000000..caf7d7f7 --- /dev/null +++ b/src/claimed/components/geo/src/stac_item_operator.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python +# coding: utf-8 +""" +STAC Item Operator + +Converts a GeoTIFF embedding into a STAC Item JSON by populating a +provided template with spatial, temporal, and asset metadata extracted +directly from the file. +""" + +import copy +import json +import logging +import os +import re +from datetime import datetime, timezone + +from osgeo import gdal, osr + +# --------------------------------------------------------------------------- +# Sentinel-2 MGRS tile origin lookup (EPSG:32613 easting/northing, metres) +# --------------------------------------------------------------------------- +# Each entry: MGRS_TILE_ID → (epsg, ul_easting, ul_northing, pixel_size_m) +# Values sourced from the official ESA Sentinel-2 tiling grid. +# Extend this dict as additional tiles are encountered. +_S2_TILE_ORIGINS: dict[str, tuple[int, float, float, int]] = { + "T13TEF": (32613, 399960.0, 4000020.0, 10), +} + +log = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +# Filename pattern for embedding pixel windows: +# …_T_…__-_-_embedding… +_TILE_RE = re.compile(r'_(T[0-9]{2}[A-Z]{3})_') +_WINDOW_RE = re.compile(r'__(\d+)-(\d+)_(\d+)-(\d+)_embedding') + + +def _parse_spatial_from_filename(filename: str) -> tuple: + """ + Derive CRS, bounding box, and GeoJSON geometry from a Sentinel-2 + embedding filename when the TIFF has no embedded projection. + + The filename is expected to contain: + - An MGRS tile ID of the form ``_T_`` + (e.g. ``T13TEF``). + - A pixel-window suffix of the form + ``__-_-_embedding``. + + Coordinates are computed in the tile's native UTM CRS and then + reprojected to WGS 84 for the returned bbox and geometry, because + STAC requires geographic (lon/lat) coordinates in those fields. + + Parameters + ---------- + filename : str + Basename (or full path) of the embedding TIFF. + + Returns + ------- + tuple[list, dict, int] + ``(bbox_wgs84, geometry_wgs84, epsg)`` where *bbox_wgs84* is + ``[minx, miny, maxx, maxy]`` in WGS 84 degrees and *epsg* is + the integer EPSG code of the tile's UTM zone. + + Raises + ------ + ValueError + If the tile ID is not found in the lookup table, or if the + filename does not match the expected pattern. + """ + tile_m = _TILE_RE.search(filename) + win_m = _WINDOW_RE.search(filename) + if not tile_m or not win_m: + raise ValueError( + f"Cannot parse spatial metadata from filename '{filename}'. " + "Expected pattern: …_T_…__-_" + "-_embedding…" + ) + + tile_id = tile_m.group(1) + if tile_id not in _S2_TILE_ORIGINS: + raise ValueError( + f"Tile '{tile_id}' is not in the _S2_TILE_ORIGINS lookup table. " + "Add its (epsg, ul_easting, ul_northing, pixel_size_m) entry." + ) + + epsg, ul_x, ul_y, px = _S2_TILE_ORIGINS[tile_id] + col_start, col_end = int(win_m.group(1)), int(win_m.group(2)) + row_start, row_end = int(win_m.group(3)), int(win_m.group(4)) + + # UTM corners (y decreases southward from the upper-left origin) + utm_minx = ul_x + col_start * px + utm_maxx = ul_x + col_end * px + utm_maxy = ul_y - row_start * px + utm_miny = ul_y - row_end * px + + # Reproject the four corners to WGS 84 (STAC bbox must be lon/lat) + srs_utm = osr.SpatialReference() + srs_utm.ImportFromEPSG(epsg) + srs_wgs = osr.SpatialReference() + srs_wgs.ImportFromEPSG(4326) + srs_wgs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) + to_wgs = osr.CoordinateTransformation(srs_utm, srs_wgs) + + corners_utm = [ + (utm_minx, utm_miny), + (utm_maxx, utm_miny), + (utm_maxx, utm_maxy), + (utm_minx, utm_maxy), + ] + corners_wgs = [] + for cx, cy in corners_utm: + lon, lat, _ = to_wgs.TransformPoint(cx, cy) + corners_wgs.append((lon, lat)) + + lons = [c[0] for c in corners_wgs] + lats = [c[1] for c in corners_wgs] + bbox_wgs84 = [min(lons), min(lats), max(lons), max(lats)] + + ring = corners_wgs + [corners_wgs[0]] # close the ring + geometry_wgs84 = { + "type": "Polygon", + "coordinates": [[[lon, lat] for lon, lat in ring]], + } + + return bbox_wgs84, geometry_wgs84, epsg + + +def _compute_bbox_and_geometry( + gt: tuple, width: int, height: int +) -> tuple: + """ + Derive a bounding box and GeoJSON Polygon from a GDAL GeoTransform. + + Parameters + ---------- + gt : tuple + GDAL GeoTransform – (originX, pixelW, 0, originY, 0, pixelH). + width : int + Raster width in pixels. + height : int + Raster height in pixels. + + Returns + ------- + tuple[list, dict] + ``(bbox, geometry)`` where *bbox* is ``[minx, miny, maxx, maxy]`` + and *geometry* is a GeoJSON Polygon dict. + """ + minx = gt[0] + maxy = gt[3] + maxx = minx + gt[1] * width + miny = maxy + gt[5] * height # gt[5] is negative → miny < maxy + + bbox = [minx, miny, maxx, maxy] + geometry = { + "type": "Polygon", + "coordinates": [[ + [minx, miny], + [maxx, miny], + [maxx, maxy], + [minx, maxy], + [minx, miny], + ]], + } + return bbox, geometry + + +def _extract_tiff_metadata(tiff_path: str) -> dict: + """ + Extract spatial and band metadata from a GeoTIFF using GDAL. + + For standard georeferenced TIFFs the CRS, bbox, and geometry are read + directly from the file. For Sentinel-2 embedding TIFFs that carry no + embedded projection, they are derived from the MGRS tile ID and pixel + window encoded in the filename via :func:`_parse_spatial_from_filename`. + + Returns a dict with: + bbox – [minx, miny, maxx, maxy] in WGS 84 (lon/lat) + geometry – GeoJSON Polygon in WGS 84 + epsg – integer EPSG code of the tile's native UTM CRS + bands – number of raster bands + dtype – GDAL data-type name of band 1 + + Raises + ------ + ValueError + If GDAL cannot open the file, the CRS cannot be determined from + the file or filename, or the tile is not in the lookup table. + """ + ds = gdal.Open(tiff_path) + if ds is None: + raise ValueError(f"GDAL could not open '{tiff_path}'") + + try: + wkt = ds.GetProjection() + srs = osr.SpatialReference(wkt=wkt) + epsg = None + has_crs = bool(wkt) and (srs.IsProjected() or srs.IsGeographic()) + + if has_crs: + # Prefer authority code; fall back to None (still georeferenced). + code = srs.GetAuthorityCode(None) + if code: + epsg = int(code) + + # Always reproject to WGS 84 for STAC bbox/geometry. + srs_wgs = osr.SpatialReference() + srs_wgs.ImportFromEPSG(4326) + srs_wgs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) + srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) + + gt = ds.GetGeoTransform() + w, h = ds.RasterXSize, ds.RasterYSize + + # Compute corners in native CRS, then transform to WGS 84. + native_minx = gt[0] + native_maxy = gt[3] + native_maxx = native_minx + gt[1] * w + native_miny = native_maxy + gt[5] * h + + corners_native = [ + (native_minx, native_miny), + (native_maxx, native_miny), + (native_maxx, native_maxy), + (native_minx, native_maxy), + ] + + to_wgs = osr.CoordinateTransformation(srs, srs_wgs) + corners_wgs = [] + for cx, cy in corners_native: + lon, lat, _ = to_wgs.TransformPoint(cx, cy) + corners_wgs.append((lon, lat)) + + lons = [c[0] for c in corners_wgs] + lats = [c[1] for c in corners_wgs] + bbox = [min(lons), min(lats), max(lons), max(lats)] + ring = corners_wgs + [corners_wgs[0]] + geometry = { + "type": "Polygon", + "coordinates": [[[lon, lat] for lon, lat in ring]], + } + else: + # Embedding TIFF with no embedded CRS: derive from filename. + log.debug( + "No embedded CRS in '%s'; deriving spatial metadata from filename.", + tiff_path, + ) + bbox, geometry, epsg = _parse_spatial_from_filename( + os.path.basename(tiff_path) + ) + + bands = ds.RasterCount + dtype = gdal.GetDataTypeName(ds.GetRasterBand(1).DataType) + finally: + del ds + + return { + "bbox": bbox, + "geometry": geometry, + "epsg": epsg, + "bands": bands, + "dtype": dtype, + } + + +def _datetime_from_filename(filename: str) -> str: + """ + Try to parse an ISO-8601 datetime string from a filename such as + ``S2A_MSIL2A_20170102T175732_…``. + + Returns an ISO-8601 string (UTC) or the current UTC time if no date + is found. + """ + # Sentinel-2 style: YYYYMMDDTHHMMSS + m = re.search(r'(\d{8})T(\d{6})', filename) + if m: + date_str, time_str = m.group(1), m.group(2) + dt = datetime( + int(date_str[0:4]), int(date_str[4:6]), int(date_str[6:8]), + int(time_str[0:2]), int(time_str[2:4]), int(time_str[4:6]), + tzinfo=timezone.utc, + ) + return dt.isoformat() + + # Plain date YYYYMMDD + m = re.search(r'(\d{4})(\d{2})(\d{2})', filename) + if m: + dt = datetime(int(m.group(1)), int(m.group(2)), int(m.group(3)), + tzinfo=timezone.utc) + return dt.isoformat() + + return datetime.now(timezone.utc).isoformat() + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def run(tiff: str, stac_template: dict, cos_url: str) -> dict: + """ + Build a STAC Item JSON for a GeoTIFF embedding from a template. + + Parameters + ---------- + tiff : str + Absolute or relative path to the GeoTIFF embedding file. + stac_template : dict + A STAC Item JSON template (loaded from e.g. ``stac_examples/``). + The following fields are populated / overwritten from the tiff: + - ``id`` → filename stem (no extension) + - ``bbox`` → derived from geotransform (or filename) + - ``geometry`` → bounding-box polygon in WGS 84 + - ``properties.datetime`` → parsed from the filename + - ``properties.proj:epsg`` → EPSG code of the tile's native CRS + (omitted when the code cannot be identified) + - ``assets.embeddings.href`` → full COS/S3 URL (``cos_url/filename``) + + All other template fields are preserved unchanged. + + Returns + ------- + dict + The populated STAC Item as a plain Python dict (also written to disk). + + Side-effects + ------------ + Writes ``.json`` next to the tiff file (same directory). + """ + tiff = os.path.abspath(tiff) + meta = _extract_tiff_metadata(tiff) + + stem = os.path.splitext(os.path.basename(tiff))[0] + tiff_dir = os.path.dirname(tiff) + json_path = os.path.join(tiff_dir, f"{stem}.json") + + item = copy.deepcopy(stac_template) + + # Core STAC fields + item["id"] = stem + item["bbox"] = meta["bbox"] + item["geometry"] = meta["geometry"] + + # Properties – keep everything in the template, just update datetime + CRS + item.setdefault("properties", {}) + item["properties"]["datetime"] = _datetime_from_filename(stem) + if meta["epsg"] is not None: + item["properties"]["proj:epsg"] = meta["epsg"] + else: + item["properties"].pop("proj:epsg", None) + + # Asset href – full COS/S3 URL pointing to the TIFF in the bucket + item.setdefault("assets", {}).setdefault("embeddings", {}) + item["assets"]["embeddings"]["href"] = f"{cos_url}/{os.path.basename(tiff)}" + + # Persist + with open(json_path, "w", encoding="utf-8") as fh: + json.dump(item, fh, indent=4) + + log.info("STAC Item written to %s", json_path) + return item + + +# --------------------------------------------------------------------------- +# CLI entry-point (optional convenience) +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Create a STAC Item JSON for a TIFF embedding.") + parser.add_argument("tiff", help="Path to the GeoTIFF embedding.") + parser.add_argument("stac_template", help="Path to the STAC Item JSON template.") + parser.add_argument("cos_url", help="URL to the COS bucket where the STAC Item will be stored.") + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + with open(args.stac_template, encoding="utf-8") as fh: + template = json.load(fh) + + result = run(args.tiff, template, args.cos_url) + print(json.dumps(result, indent=4)) diff --git a/src/claimed/components/geo/stac/item_template.json b/src/claimed/components/geo/stac/item_template.json new file mode 100644 index 00000000..cbe32444 --- /dev/null +++ b/src/claimed/components/geo/stac/item_template.json @@ -0,0 +1,70 @@ +{ + "stac_version": "1.1.0", + "stac_extensions": [], + "type": "Feature", + "id":"<<>>", + "collection": null, + "links": [ + { + "rel": "self", + "href": "./terramind_item.json", + "type": "application/json" + }, + { + "rel": "emb:source-data", + "href": "<<>>", + "type": "application/json", + "title": "Sentinel-2 image from which this embedding was computed." + } + ], + "bbox": [ + -1, + -0, + 1, + 0 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -180, + -90 + ], + [ + 180, + -90 + ], + [ + 180, + 90 + ], + [ + -180, + 90 + ], + [ + -180, + -90 + ] + ] + ] + }, + "properties": { + "datetime": "<<>>", + "gsd": 10, + "title": "Terramind Embedding", + "description": "Terramind embedding from Sentinel-2-L2A imagery.", + "licensing": "CC-BY 4.0" + }, + "assets": { + "embeddings": { + "href": "<<>>", + "title": "AEF embeddings", + "type": "image/tiff", + "roles": [ + "embedding" + ] + } + } +} diff --git a/src/claimed/components/geo/uv.lock b/src/claimed/components/geo/uv.lock new file mode 100644 index 00000000..74c673e2 --- /dev/null +++ b/src/claimed/components/geo/uv.lock @@ -0,0 +1,147 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" +resolution-markers = [ + "sys_platform == 'linux'", + "sys_platform == 'win32'", + "sys_platform != 'linux' and sys_platform != 'win32'", +] + +[[package]] +name = "gdal" +version = "3.11.5" +source = { registry = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/simple" } +resolution-markers = [ + "sys_platform == 'linux'", +] +sdist = { url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/files/20c26a250b0cacfb541779292fcd65169293102c5b96e30fe60e1c4f4afce0b1/gdal-3.11.5.tar.gz", hash = "sha256:20c26a250b0cacfb541779292fcd65169293102c5b96e30fe60e1c4f4afce0b1" } +wheels = [ + { url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/files/5ce8f776b4bfaedfa17a7924cb411480d092db18e3ce7684ea923728f0f220b0/gdal-3.11.5-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:5ce8f776b4bfaedfa17a7924cb411480d092db18e3ce7684ea923728f0f220b0" }, + { url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/files/bd06e8d0948bfda39c3a31f5faed7307d1676885db5cd59cb078db030d1cd696/gdal-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bd06e8d0948bfda39c3a31f5faed7307d1676885db5cd59cb078db030d1cd696" }, + { url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/files/3bb47b267224633ad1c0f8ce1aa69f3509609a53772f61a9630d4e4c971626f6/gdal-3.11.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3bb47b267224633ad1c0f8ce1aa69f3509609a53772f61a9630d4e4c971626f6" }, + { url = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/files/29f3abcfde84c48a4d527962092d095c6c93f499f896f4c0cc4824d88517b728/gdal-3.11.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29f3abcfde84c48a4d527962092d095c6c93f499f896f4c0cc4824d88517b728" }, +] + +[[package]] +name = "gdal" +version = "3.12.2" +source = { registry = "https://nathanjmcdougall.github.io/geospatial-wheels-index/" } +resolution-markers = [ + "sys_platform == 'win32'", +] +wheels = [ + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314-win32.whl" }, + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314-win_amd64.whl" }, + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314-win_arm64.whl" }, + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314t-win32.whl" }, + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314t-win_amd64.whl" }, + { url = "https://github.com/cgohlke/geospatial-wheels/releases/download/v2026.2.26/gdal-3.12.2-cp314-cp314t-win_arm64.whl" }, +] + +[[package]] +name = "numpy" +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, +] + +[[package]] +name = "pystac" +version = "1.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/e6/efbc20dbc94ad7ed18fe11a4208103a509384ffcccd9bdc27953b725e686/pystac-1.14.3.tar.gz", hash = "sha256:24f92d6f301371859aa0abc1bbe7b1523a603e1184a6d139ecb323967c2c9bb3", size = 164205, upload-time = "2026-01-09T12:38:42.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/b4/a9430e72bfc3c458e1fcf8363890994e483052ab052ed93912be4e5b32c8/pystac-1.14.3-py3-none-any.whl", hash = "sha256:2f60005f521d541fb801428307098f223c14697b3faf4d2f0209afb6a43f39e5", size = 208506, upload-time = "2026-01-09T12:38:40.721Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stac-item-operator" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "gdal", version = "3.11.5", source = { registry = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/simple" }, marker = "sys_platform == 'linux'" }, + { name = "gdal", version = "3.12.2", source = { registry = "https://nathanjmcdougall.github.io/geospatial-wheels-index/" }, marker = "sys_platform == 'win32'" }, + { name = "pystac" }, + { name = "shapely" }, +] + +[package.metadata] +requires-dist = [ + { name = "gdal", marker = "sys_platform == 'linux'", specifier = "==3.11.5", index = "https://gitlab.com/api/v4/projects/61637378/packages/pypi/simple" }, + { name = "gdal", marker = "sys_platform == 'win32'", index = "https://nathanjmcdougall.github.io/geospatial-wheels-index/" }, + { name = "pystac", specifier = ">=1.14.3" }, + { name = "shapely", specifier = ">=2.1.2" }, +]