From 43b94e6e741e9c930101aa88683bdde633acc915 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Mon, 28 Sep 2026 10:11:13 -0500 Subject: [PATCH 1/5] Upgrade raster runtime to TiTiler 3.2 --- cdk/handlers/raster_handler.py | 37 +- cdk/runtimes/eoapi/raster/README.md | 28 +- .../eoapi/raster/eoapi/raster/factory.py | 1144 ++++++----------- .../eoapi/raster/eoapi/raster/main.py | 51 +- .../eoapi/raster/eoapi/raster/models.py | 32 +- .../eoapi/raster/eoapi/raster/settings.py | 8 +- cdk/runtimes/eoapi/raster/pyproject.toml | 8 +- .../eoapi/raster/tests/test_runtime.py | 218 ++++ cdk/runtimes/eoapi/raster/uv.lock | 244 +++- docker-compose.yml | 10 +- 10 files changed, 901 insertions(+), 879 deletions(-) create mode 100644 cdk/runtimes/eoapi/raster/tests/test_runtime.py diff --git a/cdk/handlers/raster_handler.py b/cdk/handlers/raster_handler.py index ba4ccee..cd96f5f 100644 --- a/cdk/handlers/raster_handler.py +++ b/cdk/handlers/raster_handler.py @@ -11,9 +11,21 @@ from eoapi.raster.main import app from eoapi.raster.utils import get_secret_dict from fastapi import Request -from fastapi.routing import APIRoute from mangum import Mangum -from snapshot_restore_py import register_after_restore, register_before_snapshot + +try: + from snapshot_restore_py import register_after_restore, register_before_snapshot +except ImportError: + + def register_before_snapshot(func): + """Leave snapshot hooks inert outside the Lambda SnapStart runtime.""" + return func + + def register_after_restore(func): + """Leave snapshot hooks inert outside the Lambda SnapStart runtime.""" + return func + + from titiler.pgstac.db import connect_to_db from titiler.pgstac.settings import PostgresSettings @@ -28,11 +40,11 @@ secret = get_secret_dict(pgstac_secret_arn) pg_settings = PostgresSettings( - postgres_host=pgbouncer_host or secret["host"], - postgres_dbname=secret["dbname"], - postgres_user=secret["username"], - postgres_pass=secret["password"], - postgres_port=secret["port"], + pghost=pgbouncer_host or secret["host"], + pgdatabase=secret["dbname"], + pguser=secret["username"], + pgpassword=secret["password"], + pgport=secret["port"], ) @@ -57,12 +69,11 @@ async def startup_event() -> None: templates_start = time.monotonic() logger.info("FastAPI startup: Building route templates") app.state.path_templates = {} - for route in app.routes: - if isinstance(route, APIRoute): - # replace : with _ to make it regexable - route_path = route.path.replace(":", "__") - pattern = re.sub(r"{([^}]+)}", r"(?P<\1>[^/]+)", route_path) - app.state.path_templates[re.compile(f"^{pattern}$")] = route_path + for route_path in app.openapi()["paths"]: + # Replace : with _ to make it regexable, and template params with groups. + route_path = route_path.replace(":", "__") + pattern = re.sub(r"{([^}]+)}", r"(?P<\1>[^/]+)", route_path) + app.state.path_templates[re.compile(f"^{pattern}$")] = route_path logger.info( "FastAPI startup: Route templates built in " diff --git a/cdk/runtimes/eoapi/raster/README.md b/cdk/runtimes/eoapi/raster/README.md index 6779ca2..2d3fd67 100644 --- a/cdk/runtimes/eoapi/raster/README.md +++ b/cdk/runtimes/eoapi/raster/README.md @@ -1,3 +1,27 @@ -# eoapi.raster +# Raster runtime -![raster docs](https://user-images.githubusercontent.com/10407788/151455911-c455a043-3313-4c26-b980-042cb80787a3.png) +The runtime targets Python 3.12 and uses TiTiler pgSTAC 3.2.0 with TiTiler core, mosaic, and extensions 2.3.0. Dependencies are locked in `uv.lock`. + +## Route migration + +pgSTAC tile endpoints now use an explicit tile matrix set. For example: + +- `/searches/{search_id}/tiles/WebMercatorQuad/{z}/{x}/{y}` +- `/searches/{search_id}/WebMercatorQuad/tilejson.json` +- `/searches/{search_id}/WebMercatorQuad/map.html` + +The same route families are available under `/collections/{collection_id}`. Legacy no-TMS tile, TileJSON, and map-viewer paths redirect with HTTP 307 to their `WebMercatorQuad` equivalents. The old `/mosaic` prefix still redirects other routes to `/searches`; `/mosaic/register` and `/mosaic/list` map to `/searches/register` and `/searches/` respectively. Info, point, WMTS, registration, statistics, and tileset-list routes do not get a tile matrix segment injected. + +The custom `/mosaics` creation and resource API remains available. Its native tile routes are TMS-qualified, for example `/mosaics/{mosaic_id}/tiles/WebMercatorQuad/{z}/{x}/{y}`. + +For local Compose, raster database settings use the TiTiler pgSTAC `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGHOST`, and `PGPORT` variables. Other services retain their own environment names. + +## Focused checks + +From this directory, run: + +```sh +MOSAIC_BACKEND=dynamodb:// MOSAIC_HOST=us-west-2/test-table \ + uv run --locked --extra lambda --extra psycopg-binary --with pytest \ + pytest -q tests/test_runtime.py +``` diff --git a/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py b/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py index 9385949..c5d4b48 100644 --- a/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py +++ b/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py @@ -1,662 +1,63 @@ -"""Custom factory classes for titiler-pgstac""" +"""Custom MosaicJSON creation and resource routes.""" import asyncio import logging import os +import re import uuid -from asyncio import wait_for from functools import partial -from typing import Annotated, Optional -from urllib.parse import urlencode - -import morecantile -import rasterio -from attrs import define -from cogeo_mosaic.backends import DynamoDBBackend -from cogeo_mosaic.errors import MosaicError +from typing import Annotated + +from cogeo_mosaic.backends import DynamoDBBackend, MosaicBackend +from cogeo_mosaic.errors import MosaicError, MosaicExistsError from cogeo_mosaic.mosaic import MosaicJSON -from fastapi import Depends, Header, HTTPException, Path, Query -from pydantic import Field +from fastapi import ( + APIRouter, + Depends, + Header, + HTTPException, + Path, + Request, + Response, + status, +) +from fastapi.responses import RedirectResponse from pystac_client import Client from rio_tiler.constants import MAX_THREADS from rio_tiler.io import Reader -from starlette import status -from starlette.requests import Request -from starlette.responses import Response -from titiler.core.factory import img_endpoint_params -from titiler.core.models.mapbox import TileJSON -from titiler.core.resources.enums import ImageType, MediaType, OptionalHeader -from titiler.core.resources.responses import JSONResponse, XMLResponse -from titiler.mosaic import factory -from titiler.mosaic.models.responses import Point from eoapi.raster.models import ( Link, MosaicEntity, StacApiQueryRequestBody, - StoreException, TooManyResultsException, - UnsupportedOperationException, UrisRequestBody, ) from eoapi.raster.settings import MosaicSettings +logger = logging.getLogger(__name__) mosaic_config = MosaicSettings() +router = APIRouter() -@define(kw_only=True) -class MosaicTilerFactory(factory.MosaicTilerFactory): - """Custom MosaicTiler Factory.""" - - logger = logging.getLogger(__name__) - - def register_routes(self): # noqa - """This Method register routes to the router.""" - - # Register default endpoints from titiler MosaicTilerFactory - # super().register_routes() - - # Register Custom endpoints - # with dynamodb backend, the tiles field for this is always empty - # https://github.com/developmentseed/cogeo-mosaic/issues/175 - @self.router.get( - "/mosaics/{mosaic_id}", - response_model=MosaicEntity, - responses={ - status.HTTP_200_OK: { - "description": "Return a Mosaic resource for the given ID." - }, - status.HTTP_404_NOT_FOUND: { - "description": "Mosaic resource for the given ID does not exist." - }, - }, - ) - async def get_mosaic( - request: Request, - mosaic_id: str = Path(..., description="MosaicId"), - ) -> MosaicEntity: - self_uri = self.url_for(request, "get_mosaic", mosaic_id=mosaic_id) - if await retrieve(mosaic_id): - return mk_mosaic_entity(mosaic_id=mosaic_id, self_uri=self_uri) - - raise HTTPException( - status.HTTP_404_NOT_FOUND, - "Error: mosaic with given ID does not exist.", - ) - - @self.router.get( - "/mosaics/{mosaic_id}/mosaicjson", - response_model=MosaicJSON, - responses={ - 200: { - "description": "Return a MosaicJSON definition for the given ID." - }, - 404: { - "description": "Mosaic resource for the given ID does not exist." - }, - }, - ) - async def get_mosaic_mosaicjson( - mosaic_id: str = Path(..., description="MosaicId"), - ) -> MosaicJSON: - if m := await retrieve(mosaic_id, include_tiles=True): - return m - raise HTTPException( - status.HTTP_404_NOT_FOUND, - "Error: mosaic with given ID does not exist.", - ) - - @self.router.post( - "/mosaics", - status_code=status.HTTP_201_CREATED, - responses={ - status.HTTP_201_CREATED: {"description": "Created a new mosaic"}, - status.HTTP_409_CONFLICT: { - "description": "Conflict while trying to create mosaic" - }, - status.HTTP_500_INTERNAL_SERVER_ERROR: { - "description": "Mosaic could not be created" - }, - }, - response_model=MosaicEntity, - openapi_extra={ - "requestBody": { - "content": { - "": { - "schema": MosaicJSON.model_json_schema( - ref_template="#/components/schemas" - ) - }, - "application/json": { - "schema": MosaicJSON.model_json_schema( - ref_template="#/components/schemas" - ) - }, - "application/json; charset=utf-8": { - "schema": MosaicJSON.model_json_schema( - ref_template="#/components/schemas" - ) - }, - "application/vnd.titiler.mosaicjson+json": { - "schema": MosaicJSON.model_json_schema( - ref_template="#/components/schemas" - ) - }, - "application/vnd.titiler.urls+json": { - "schema": UrisRequestBody.model_json_schema( - ref_template="#/components/schemas" - ) - }, - "application/vnd.titiler.stac-api-query+json": { - "schema": StacApiQueryRequestBody.model_json_schema( - ref_template="#/components/schemas" - ) - }, - }, - "required": True, - }, - }, - ) - async def post_mosaics( - request: Request, - response: Response, - mosaicjson: MosaicJSON = Depends(populate_mosaicjson), - ) -> MosaicEntity: - """Create a MosaicJSON""" - mosaic_id = str(uuid.uuid4()) - - # duplicate IDs are unlikely to exist, but handle it just to be safe - try: - await store(mosaic_id, mosaicjson, overwrite=False) - - except StoreException as e: - raise HTTPException( - status.HTTP_409_CONFLICT, - "Error: mosaic with given ID already exists", - ) from e - - except Exception as e: - logging.error(f"could not save mosaic: {e}") - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - "Error: could not save mosaic", - ) from e - - self_uri = self.url_for(request, "get_mosaic", mosaic_id=mosaic_id) - response.headers["Location"] = self_uri - - return mk_mosaic_entity(mosaic_id, self_uri) - - # derived from cogeo.xyz - @self.router.get( - "/mosaics/{mosaic_id}/tilejson.json", - response_model=TileJSON, - responses={ - 200: {"description": "Return a tilejson for the given ID."}, - 404: { - "description": "Mosaic resource for the given ID does not exist." - }, - }, - response_model_exclude_none=True, - ) - async def tilejson( - request: Request, - mosaic_id: str = Path(..., description="MosaicId"), - tile_format: ImageType | None = Query( - None, description="Output image type. Default is auto." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: int | None = Query(None, description="Overwrite default minzoom."), - maxzoom: int | None = Query(None, description="Overwrite default maxzoom."), - layer_params=Depends(self.layer_dependency), - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - post_process=Depends(self.process_dependency), # noqa - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - backend_params=Depends(self.backend_dependency), # noqa - reader_params=Depends(self.reader_dependency), # noqa - env=Depends(self.environment_dependency), # noqa - ): - """Return TileJSON document for a MosaicJSON.""" - kwargs = { - "mosaic_id": mosaic_id, - "z": "{z}", - "x": "{x}", - "y": "{y}", - "scale": tile_scale, - } - if tile_format: - kwargs["format"] = tile_format.value - tiles_url = self.url_for(request, "tile", **kwargs) - - qs_key_to_remove = [ - "tile_format", - "tile_scale", - "minzoom", - "maxzoom", - ] - qs = [ - (key, value) - for (key, value) in request.query_params._list - if key.lower() not in qs_key_to_remove - ] - if qs: - tiles_url += f"?{urlencode(qs)}" - - if mosaicjson := await retrieve(mosaic_id): - center = list(mosaicjson.center) - if minzoom is not None: - center[-1] = minzoom - return { - "bounds": mosaicjson.bounds, - "center": tuple(center), - "minzoom": minzoom if minzoom is not None else mosaicjson.minzoom, - "maxzoom": maxzoom if maxzoom is not None else mosaicjson.maxzoom, - "name": mosaic_id, - "tiles": [tiles_url], - } - - raise HTTPException( - status.HTTP_404_NOT_FOUND, - "Error: mosaic with given ID does not exist.", - ) - - # derived from cogeo-xyz - @self.router.get( - "/mosaics/{mosaic_id}/tiles/{z}/{x}/{y}", **img_endpoint_params - ) - @self.router.get( - "/mosaics/{mosaic_id}/tiles/{z}/{x}/{y}.{format}", **img_endpoint_params - ) - @self.router.get( - "/mosaics/{mosaic_id}/tiles/{z}/{x}/{y}@{scale}x", **img_endpoint_params - ) - @self.router.get( - "/mosaics/{mosaic_id}/tiles/{z}/{x}/{y}@{scale}x.{format}", - **img_endpoint_params, - ) - def tile( - mosaic_id: str = Path(..., description="MosaicId"), - z: int = Path(..., ge=0, le=30, description="Mercator tiles's zoom level"), - x: int = Path(..., description="Mercator tiles's column"), - y: int = Path(..., description="Mercator tiles's row"), - scale: Annotated[ - int, - Field( - gt=0, le=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - ] = 1, - format: Annotated[ - ImageType, - ( - "Default will be automatically defined if the output image " - "needs a mask (png) or not (jpeg)." - ), - ] = None, - backend_params=Depends(self.backend_dependency), - reader_params=Depends(self.reader_dependency), - layer_params=Depends(self.layer_dependency), - dataset_params=Depends(self.dataset_dependency), - pixel_selection=Depends(self.pixel_selection_dependency), - tile_params=Depends(self.tile_dependency), - post_process=Depends(self.process_dependency), - colormap=Depends(self.colormap_dependency), - render_params=Depends(self.render_dependency), - env=Depends(self.environment_dependency), - ): - """Create map tile from a mosaic.""" - threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) - - mosaic_uri = mk_src_path(mosaic_id) - tms = self.supported_tms.get("WebMercatorQuad") - with ( - rasterio.Env(**env), - self.backend( - mosaic_uri, - tms=tms, - reader=self.dataset_reader, - reader_options=reader_params.as_dict(), - **backend_params.as_dict(), - ) as src_dst, - ): - image, assets = src_dst.tile( - x, - y, - z, - pixel_selection=pixel_selection, - tilesize=scale * 256, - threads=threads, - **tile_params.as_dict(), - **layer_params.as_dict(), - **dataset_params.as_dict(), - ) - - if post_process: - image = post_process(image) - - content, media_type = self.render_func( - image, - output_format=format, - colormap=colormap, - **render_params.as_dict(), - ) - - headers: dict[str, str] = {} - if OptionalHeader.x_assets in self.optional_headers: - headers["X-Assets"] = ",".join(assets) - - return Response(content, media_type=media_type, headers=headers) - - @self.router.get( - "/mosaics/{mosaic_id}/WMTSCapabilities.xml", response_class=XMLResponse - ) - def wmts( - request: Request, - mosaic_id: str = Path(..., description="MosaicId"), - tile_format: ImageType = Query( - ImageType.png, description="Output image type. Default is png." - ), - tile_scale: int = Query( - 1, gt=0, lt=4, description="Tile size scale. 1=256x256, 2=512x512..." - ), - minzoom: int | None = Query(None, description="Overwrite default minzoom."), - maxzoom: int | None = Query(None, description="Overwrite default maxzoom."), - layer_params=Depends(self.layer_dependency), # noqa - dataset_params=Depends(self.dataset_dependency), # noqa - pixel_selection=Depends(self.pixel_selection_dependency), # noqa - buffer: Optional[float] = Query( # noqa - None, - gt=0, - title="Tile buffer.", - description=( - "Buffer on each side of the given tile. It must be a multiple " - "of `0.5`. Output **tilesize** will be expanded to " - "`tilesize + 2 * tile_buffer` (e.g 0.5 = 257x257, " - "1.0 = 258x258)." - ), - ), - post_process=Depends(self.process_dependency), # noqa - colormap=Depends(self.colormap_dependency), # noqa - render_params=Depends(self.render_dependency), # noqa - backend_params=Depends(self.backend_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), - ): - """OGC WMTS endpoint.""" - route_params = { - "mosaic_id": mosaic_id, - "z": "{TileMatrix}", - "x": "{TileCol}", - "y": "{TileRow}", - "scale": tile_scale, - "format": tile_format.value, - } - tiles_url = self.url_for(request, "tile", **route_params) - - qs_key_to_remove = [ - "tile_format", - "tile_scale", - "minzoom", - "maxzoom", - "service", - "request", - ] - qs = [ - (key, value) - for (key, value) in request.query_params._list - if key.lower() not in qs_key_to_remove - ] - if qs: - tiles_url += f"?{urlencode(qs)}" - - mosaic_uri = mk_src_path(mosaic_id) - with ( - rasterio.Env(**env), - self.backend( - mosaic_uri, - reader=self.dataset_reader, - reader_options=reader_params.as_dict(), - **backend_params.as_dict(), - ) as src_dst, - ): - bounds = src_dst.bounds - minzoom = minzoom if minzoom is not None else src_dst.minzoom - maxzoom = maxzoom if maxzoom is not None else src_dst.maxzoom - - tms = morecantile.tms.get("WebMercatorQuad") - - tileMatrix = [] - for zoom in range(minzoom, maxzoom + 1): # type: ignore - matrix = tms.matrix(zoom) - top_left_corner = f"{matrix.pointOfOrigin[0]} {matrix.pointOfOrigin[1]}" - tm = f""" - - {matrix.id} - {matrix.scaleDenominator} - {top_left_corner} - {matrix.tileWidth} - {matrix.tileHeight} - {matrix.matrixWidth} - {matrix.matrixHeight} - """ - tileMatrix.append(tm) - - return self.templates.TemplateResponse( - "wmts.xml", - { - "request": request, - "tiles_endpoint": tiles_url, - "bounds": bounds, - "tileMatrix": tileMatrix, - "tms": tms, - "title": "Mosaic", - "layer_name": mosaic_id, - "media_type": tile_format.mediatype, - }, - media_type=MediaType.xml.value, - ) - - @self.router.get( - "/mosaics/{mosaic_id}/point/{lon},{lat}", - response_model=Point, - response_class=JSONResponse, - responses={200: {"description": "Return a value for a point"}}, - ) - def point( - mosaic_id: str = Path(..., description="MosaicId"), - lon: float = Path(..., description="Longitude"), - lat: float = Path(..., description="Latitude"), - # https://github.com/developmentseed/titiler/blob/17cdff2f0ddf08dbd9a47c2140b13c4bbcc30b6d/src/titiler/mosaic/titiler/mosaic/factory.py#L698-L727 - # coord_crs=Depends(CoordCRSParams), added in titiler>=0.12 - layer_params=Depends(self.layer_dependency), - dataset_params=Depends(self.dataset_dependency), - backend_params=Depends(self.backend_dependency), - reader_params=Depends(self.reader_dependency), - env=Depends(self.environment_dependency), - ): - """Get Point value for a Mosaic.""" - threads = int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)) - - mosaic_uri = mk_src_path(mosaic_id) - with ( - rasterio.Env(**env), - self.backend( - mosaic_uri, - reader=self.dataset_reader, - reader_options=reader_params.as_dict(), - **backend_params.as_dict(), - ) as src_dst, - ): - values = src_dst.point( - lon, - lat, - threads=threads, - **layer_params.as_dict(), - **dataset_params.as_dict(), - ) - - return { - "coordinates": [lon, lat], - "values": [ - (src, pts.data.tolist(), pts.band_names) for src, pts in values - ], - } - - async def store( - mosaic_id: str, - mosaicjson: MosaicJSON, - overwrite: bool, - ) -> None: - try: - existing = await retrieve(mosaic_id) - except Exception: - existing = False - - if not overwrite and existing: - raise StoreException("Attempting to create already existing mosaic") - if overwrite and not existing: - raise StoreException("Attempting to update non-existant mosaic") - - mosaic_uri = mk_src_path(mosaic_id) - - try: - await wait_for( - asyncio.get_running_loop().run_in_executor( - None, # executor - mosaic_write, # func - mosaic_uri, - mosaicjson, - overwrite, - ), - 20, - ) - except TimeoutError as e: - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - "Error: timeout storing mosaic in datastore", - ) from e - - def mosaic_write( - mosaic_uri: str, - mosaicjson: MosaicJSON, - overwrite: bool, - ) -> None: - with self.backend(mosaic_uri, mosaic_def=mosaicjson) as mosaic: - mosaic.write(overwrite=overwrite) - - async def retrieve( - mosaic_id: str, include_tiles: bool = False - ) -> MosaicJSON | None: - mosaic_uri = mk_src_path(mosaic_id) - - try: - return await wait_for( - asyncio.get_running_loop().run_in_executor( - None, # executor - read_mosaicjson_sync, # func - mosaic_uri, - include_tiles, - ), - 20, - ) - except TimeoutError as e: - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - "Error: timeout retrieving mosaic from datastore.", - ) from e - except MosaicError: - return None - - def read_mosaicjson_sync(mosaic_uri: str, include_tiles: bool) -> MosaicJSON: - with self.backend( - mosaic_uri, - reader=self.dataset_reader, - # TODO - # **self.backend_options, # Replaced with an endpoint dependency - ) as mosaic: - mosaicjson = mosaic.mosaic_def - if include_tiles and isinstance(mosaic, DynamoDBBackend): - keys = (mosaic._fetch_dynamodb(qk) for qk in mosaic._quadkeys) - mosaicjson.tiles = {x["quadkey"]: x["assets"] for x in keys} - return mosaicjson - - async def delete(mosaic_id: str) -> None: - mosaic_uri = mk_src_path(mosaic_id) - - try: - await wait_for( - asyncio.get_running_loop().run_in_executor( - None, - delete_mosaicjson_sync, - mosaic_uri, # executor # func - ), - 20, - ) - except TimeoutError as e: - raise HTTPException( - status.HTTP_500_INTERNAL_SERVER_ERROR, - "Error: timeout deleting mosaic.", - ) from e - - return - - def delete_mosaicjson_sync(mosaic_uri: str) -> None: - with self.backend( - mosaic_uri, - reader=self.dataset_reader, - # TODO - # **self.backend_options, # Replaced with an endpoint dependency - ) as mosaic: - if isinstance(mosaic, DynamoDBBackend): - mosaic.delete() # delete is only supported by DynamoDB - else: - raise UnsupportedOperationException("Delete is not supported") - - -# todo: make this safer in case visual doesn't exist -# how to handle others? -# support for selection by role? -def asset_href(feature: dict, asset_name: str) -> str: - """Get asset url.""" - if href := feature.get("assets", {}).get(asset_name, {}).get("href"): - return href - raise Exception(f"Asset with name '{asset_name}' could not be found.") - - -def mk_src_path(mosaic_id: str) -> str: - """Return Mosaic Path.""" - if mosaic_config.backend == "dynamodb://": - return f"{mosaic_config.backend}{mosaic_config.host}:{mosaic_id}" - return ( - f"{mosaic_config.backend}{mosaic_config.host}/{mosaic_id}{mosaic_config.format}" - ) - - -async def mosaicjson_from_urls(urisrb: UrisRequestBody) -> MosaicJSON: - """Create MosaicJSON from a list of COG URLs.""" - if len(urisrb.urls) > MAX_ITEMS: +async def mosaicjson_from_urls(request: UrisRequestBody) -> MosaicJSON: + """Create a MosaicJSON from COG URLs.""" + if len(request.urls) > MAX_ITEMS: raise HTTPException( status.HTTP_400_BAD_REQUEST, f"Error: a maximum of {MAX_ITEMS} URLs can be mosaiced.", ) try: - mosaicjson = await wait_for( - asyncio.get_running_loop().run_in_executor( - None, # executor - lambda: MosaicJSON.from_urls( - urls=urisrb.urls, - minzoom=urisrb.minzoom, - maxzoom=urisrb.maxzoom, - max_threads=int( - os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS) - ), # todo - ), + mosaicjson = await asyncio.wait_for( + asyncio.to_thread( + MosaicJSON.from_urls, + urls=request.urls, + minzoom=request.minzoom, + maxzoom=request.maxzoom, + max_threads=int(os.getenv("MOSAIC_CONCURRENCY", MAX_THREADS)), ), - 20, + timeout=20, ) except TimeoutError as e: raise HTTPException( @@ -670,17 +71,18 @@ async def mosaicjson_from_urls(urisrb: UrisRequestBody) -> MosaicJSON: "Error: could not extract mosaic data", ) - mosaicjson.name = urisrb.name - mosaicjson.description = urisrb.description - mosaicjson.attribution = urisrb.attribution - mosaicjson.version = urisrb if urisrb.version else "0.0.1" - + mosaicjson.name = request.name + mosaicjson.description = request.description + mosaicjson.attribution = request.attribution + mosaicjson.version = request.version or "0.0.1" return mosaicjson -async def mosaicjson_from_stac_api_query(req: StacApiQueryRequestBody) -> MosaicJSON: - """Create a mosaic from a STAC-API search request.""" - if not req.stac_api_root: +async def mosaicjson_from_stac_api_query( + request: StacApiQueryRequestBody, +) -> MosaicJSON: + """Create a MosaicJSON from a STAC API search request.""" + if not request.stac_api_root: raise HTTPException( status.HTTP_400_BAD_REQUEST, "Error: stac_api_root field must be non-empty.", @@ -688,21 +90,14 @@ async def mosaicjson_from_stac_api_query(req: StacApiQueryRequestBody) -> Mosaic try: try: - features = await wait_for( - asyncio.get_running_loop().run_in_executor( - None, - execute_stac_search, - req, # executor # func - ), - 30, + features = await asyncio.wait_for( + asyncio.to_thread(execute_stac_search, request), timeout=30 ) - except TimeoutError as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, "Error: timeout executing STAC API search.", ) from e - except TooManyResultsException as e: raise HTTPException( status.HTTP_400_BAD_REQUEST, @@ -716,23 +111,19 @@ async def mosaicjson_from_stac_api_query(req: StacApiQueryRequestBody) -> Mosaic ) try: - mosaicjson = await wait_for( - asyncio.get_running_loop().run_in_executor( - None, + mosaicjson = await asyncio.wait_for( + asyncio.to_thread( extract_mosaicjson_from_features, features, - req.asset_name if req.asset_name else "visual", + request.asset_name or "visual", ), - 60, # todo: how much time should/can it take? + timeout=60, ) - except TimeoutError as e: raise HTTPException( status.HTTP_500_INTERNAL_SERVER_ERROR, - ( - "Error: timeout reading a COG asset and generating " - "MosaicJSON definition" - ), + "Error: timeout reading a COG asset and generating " + "MosaicJSON definition", ) from e if mosaicjson is None: @@ -741,16 +132,13 @@ async def mosaicjson_from_stac_api_query(req: StacApiQueryRequestBody) -> Mosaic "Error: could not extract mosaic data", ) - mosaicjson.name = req.name - mosaicjson.description = req.description - mosaicjson.attribution = req.attribution - mosaicjson.version = req if req.version else "0.0.1" - + mosaicjson.name = request.name + mosaicjson.description = request.description + mosaicjson.attribution = request.attribution + mosaicjson.version = request.version or "0.0.1" return mosaicjson - - except HTTPException as e: - raise e - + except HTTPException: + raise except Exception as e: raise HTTPException(status.HTTP_500_INTERNAL_SERVER_ERROR, f"Error: {e}") from e @@ -758,109 +146,140 @@ async def mosaicjson_from_stac_api_query(req: StacApiQueryRequestBody) -> Mosaic MAX_ITEMS = 100 -def execute_stac_search(mosaic_request: StacApiQueryRequestBody) -> list[dict]: - """Send Search request to the stac-api.""" +def execute_stac_search(request: StacApiQueryRequestBody) -> list[dict]: + """Run a bounded STAC API search for mosaic source items.""" try: - search_result = Client.open(mosaic_request.stac_api_root).search( - # **mosaic_request.dict(), ?? this feel a little unsafe - ids=mosaic_request.ids, - collections=mosaic_request.collections, - datetime=mosaic_request.datetime, - bbox=mosaic_request.bbox, - intersects=mosaic_request.intersects, - query=mosaic_request.query, + result = Client.open(request.stac_api_root).search( + ids=request.ids, + collections=request.collections, + datetime=request.datetime, + bbox=request.bbox, + intersects=request.intersects, + query=request.query, max_items=MAX_ITEMS, - limit=mosaic_request.limit if mosaic_request.limit else 100, - # setting limit >500 causes an error https://github.com/stac-utils/pystac-client/issues/56 + limit=request.limit or MAX_ITEMS, ) - matched = search_result.matched() + matched = result.matched() if matched > MAX_ITEMS: raise TooManyResultsException( f"too many results: {matched} Items matched, " f"but only a maximum of {MAX_ITEMS} are allowed." ) - - return search_result.items_as_collection().to_dict()["features"] - - except TooManyResultsException as e: - raise e - + return result.items_as_collection().to_dict()["features"] + except TooManyResultsException: + raise except Exception as e: raise Exception(f"STAC Search error: {e}") from e -# assumes all assets are uniform. get the min and max zoom from the first. +def asset_href(feature: dict, asset_name: str) -> str: + """Return the named asset href from a STAC Item.""" + if href := feature.get("assets", {}).get(asset_name, {}).get("href"): + return href + raise ValueError(f"Asset with name '{asset_name}' could not be found.") + + def extract_mosaicjson_from_features( features: list[dict], asset_name: str ) -> MosaicJSON | None: - """Get COG Min/Max Zoom from STAC items and create a MosaicJSON.""" - if features: - try: - with Reader(asset_href(features[0], asset_name)) as cog: - info = cog.info() - - return MosaicJSON.from_features( - features, - minzoom=info.minzoom, - maxzoom=info.maxzoom, - accessor=partial(asset_href, asset_name=asset_name), - ) - - # when Item geometry is a MultiPolygon (instead of Polygon), supermercado raises - # handle error "local variable 'x' referenced before assignment" - # supermercado/burntiles.py ", line 38, in _feature_extrema - # as this method only handles Polygon, LineString, and Point :grimace: - # https://github.com/mapbox/supermercado/issues/47 - except UnboundLocalError as e: - raise Exception( - "STAC Items likely have MultiPolygon geometry, and only " - "Polygon is supported." - ) from e - - except Exception as e: - raise Exception(f"Error extracting mosaic data from results: {e}") from e - - else: + """Build a MosaicJSON from STAC Items using their named COG asset.""" + if not features: return None + try: + with Reader(asset_href(features[0], asset_name)) as cog: + info = cog.info() + return MosaicJSON.from_features( + features, + minzoom=info.minzoom, + maxzoom=info.maxzoom, + accessor=partial(asset_href, asset_name=asset_name), + ) + except UnboundLocalError as e: + raise ValueError( + "STAC Items likely have MultiPolygon geometry; only Polygon is supported." + ) from e + except Exception as e: + raise ValueError(f"Error extracting mosaic data from results: {e}") from e + async def populate_mosaicjson( request: Request, - content_type: str | None = Header(None), + content_type: Annotated[str | None, Header()] = None, ) -> MosaicJSON: - """Post MosaicJSON dependency.""" - body_json = await request.json() - - # Case 1: we received a MosaicJSON document - if ( - not content_type - or content_type == "application/json" - or content_type == "application/json; charset=utf-8" - or content_type == "application/vnd.titiler.mosaicjson+json" + """Parse the supported MosaicJSON creation request bodies.""" + body = await request.json() + + if content_type in ( + None, + "application/json", + "application/json; charset=utf-8", + "application/vnd.titiler.mosaicjson+json", ): - mosaicjson = MosaicJSON(**body_json) + return MosaicJSON.model_validate(body) + if content_type == "application/vnd.titiler.urls+json": + return await mosaicjson_from_urls(UrisRequestBody.model_validate(body)) + if content_type == "application/vnd.titiler.stac-api-query+json": + return await mosaicjson_from_stac_api_query( + StacApiQueryRequestBody.model_validate(body) + ) + raise HTTPException( + status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, + "Error: media in Content-Type header is not supported.", + ) - # Case 2: we received a list of URLs - elif content_type == "application/vnd.titiler.urls+json": - mosaicjson = await mosaicjson_from_urls(UrisRequestBody(**body_json)) - # Case 3: we received a stac-api search request - elif content_type == "application/vnd.titiler.stac-api-query+json": - mosaicjson = await mosaicjson_from_stac_api_query( - StacApiQueryRequestBody(**body_json) - ) +def mk_src_path(mosaic_id: str) -> str: + """Build the configured cogeo-mosaic storage URI for a mosaic ID.""" + if mosaic_config.backend == "dynamodb://": + return f"{mosaic_config.backend}{mosaic_config.host}:{mosaic_id}" + return ( + f"{mosaic_config.backend}{mosaic_config.host}/{mosaic_id}{mosaic_config.format}" + ) - else: - raise HTTPException( - status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, - "Error: media in Content-Type header is not supported.", - ) - return mosaicjson +def mosaic_path(mosaic_id: Annotated[str, Path()]) -> str: + """Resolve a resource ID to its configured storage URI.""" + return mk_src_path(mosaic_id) + + +def mosaic_write(mosaic_id: str, mosaicjson: MosaicJSON) -> None: + """Write a MosaicJSON to the configured backend.""" + with MosaicBackend(mk_src_path(mosaic_id), mosaic_def=mosaicjson) as backend: + backend.write(overwrite=False) + + +def read_mosaicjson(mosaic_id: str, include_tiles: bool = False) -> MosaicJSON: + """Read a MosaicJSON, restoring DynamoDB tile rows when requested.""" + with MosaicBackend(mk_src_path(mosaic_id)) as backend: + mosaicjson = backend.mosaic_def + if include_tiles and isinstance(backend, DynamoDBBackend): + mosaicjson.tiles = { + item["quadkey"]: item["assets"] + for item in (backend._fetch_dynamodb(key) for key in backend._quadkeys) + } + return mosaicjson + + +async def retrieve_mosaic( + mosaic_id: str, include_tiles: bool = False +) -> MosaicJSON | None: + """Read a stored MosaicJSON, returning None when it does not exist.""" + try: + return await asyncio.wait_for( + asyncio.to_thread(read_mosaicjson, mosaic_id, include_tiles), timeout=20 + ) + except TimeoutError as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + "Error: timeout retrieving mosaic from datastore.", + ) from e + except MosaicError: + return None -def mk_mosaic_entity(mosaic_id, self_uri): - """Mosaic Links response.""" +def mk_mosaic_entity(mosaic_id: str, self_uri: str) -> MosaicEntity: + """Build the custom Mosaic resource and its native TMS-qualified links.""" return MosaicEntity( id=mosaic_id, links=[ @@ -873,13 +292,13 @@ def mk_mosaic_entity(mosaic_id, self_uri): ), Link( rel="tilejson", - href=f"{self_uri}/tilejson.json", + href=f"{self_uri}/WebMercatorQuad/tilejson.json", type="application/json", title="TileJSON", ), Link( rel="tiles", - href=f"{self_uri}/tiles/{{z}}/{{x}}/{{y}}", + href=f"{self_uri}/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}", type="application/json", title="Tiles", ), @@ -891,3 +310,244 @@ def mk_mosaic_entity(mosaic_id, self_uri): ), ], ) + + +@router.get("/mosaics/{mosaic_id}", name="get_mosaic") +async def get_mosaic(request: Request, mosaic_id: str) -> MosaicEntity: + """Return the custom Mosaic resource for a stored ID.""" + self_uri = str(request.url_for("get_mosaic", mosaic_id=mosaic_id)) + if await retrieve_mosaic(mosaic_id): + return mk_mosaic_entity(mosaic_id, self_uri) + raise HTTPException( + status.HTTP_404_NOT_FOUND, "Error: mosaic with given ID does not exist." + ) + + +@router.get("/mosaics/{mosaic_id}/mosaicjson") +async def get_mosaicjson(mosaic_id: str) -> MosaicJSON: + """Return the stored MosaicJSON definition.""" + if mosaicjson := await retrieve_mosaic(mosaic_id, include_tiles=True): + return mosaicjson + raise HTTPException( + status.HTTP_404_NOT_FOUND, "Error: mosaic with given ID does not exist." + ) + + +@router.post( + "/mosaics", + status_code=status.HTTP_201_CREATED, + openapi_extra={ + "requestBody": { + "content": { + "": {"schema": MosaicJSON.model_json_schema()}, + "application/json": {"schema": MosaicJSON.model_json_schema()}, + "application/json; charset=utf-8": { + "schema": MosaicJSON.model_json_schema() + }, + "application/vnd.titiler.mosaicjson+json": { + "schema": MosaicJSON.model_json_schema() + }, + "application/vnd.titiler.urls+json": { + "schema": UrisRequestBody.model_json_schema() + }, + "application/vnd.titiler.stac-api-query+json": { + "schema": StacApiQueryRequestBody.model_json_schema() + }, + }, + "required": True, + } + }, +) +async def post_mosaics( + request: Request, + response: Response, + mosaicjson: Annotated[MosaicJSON, Depends(populate_mosaicjson)], +) -> MosaicEntity: + """Create and store a MosaicJSON using the custom API.""" + mosaic_id = str(uuid.uuid4()) + try: + await asyncio.wait_for( + asyncio.to_thread(mosaic_write, mosaic_id, mosaicjson), 20 + ) + except TimeoutError as e: + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, + "Error: timeout storing mosaic in datastore", + ) from e + except MosaicExistsError as e: + raise HTTPException( + status.HTTP_409_CONFLICT, + "Error: mosaic with given ID already exists", + ) from e + except Exception as e: + logger.exception("Could not save mosaic") + raise HTTPException( + status.HTTP_500_INTERNAL_SERVER_ERROR, "Error: could not save mosaic" + ) from e + + self_uri = str(request.url_for("get_mosaic", mosaic_id=mosaic_id)) + response.headers["Location"] = self_uri + return mk_mosaic_entity(mosaic_id, self_uri) + + +def _redirect(request: Request, path: str) -> RedirectResponse: + """Redirect while preserving the original query string and method.""" + query = request.scope["query_string"].decode("latin-1") + return RedirectResponse(f"{path}?{query}" if query else path, status_code=307) + + +def redirect_collection_compatibility(request: Request) -> RedirectResponse | None: + """Redirect only legacy collection paths missing their tile matrix set.""" + path = request.url.path + if match := re.fullmatch( + r"/collections/([^/]+)/tiles/(\d+)/(\d+)/(\d+(?:@\d+x)?(?:\.[\w-]+)?)(/assets)?", + path, + ): + collection_id, z, x, y, assets = match.groups() + target = f"/collections/{collection_id}/tiles/WebMercatorQuad/{z}/{x}/{y}" + return _redirect(request, target + (assets or "")) + + if match := re.fullmatch(r"/collections/([^/]+)/(tilejson\.json|map\.html)", path): + collection_id, endpoint = match.groups() + return _redirect( + request, f"/collections/{collection_id}/WebMercatorQuad/{endpoint}" + ) + return None + + +def _redirect_search_id(search_id: str | None, _collection_id: str | None) -> str: + """Return the native pgSTAC search path for a legacy route.""" + return f"/searches/{search_id}" + + +@router.api_route( + "/mosaic/{search_id}/tiles/{z}/{x}/{y}", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tiles( + request: Request, + z: str, + x: str, + y: str, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy no-TMS tile paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}") + + +@router.api_route( + "/mosaic/{search_id}/tiles/{z}/{x}/{y}.{format}", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tiles_format( + request: Request, + z: str, + x: str, + y: str, + format: str, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy formatted tile paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}.{format}") + + +@router.api_route( + "/mosaic/{search_id}/tiles/{z}/{x}/{y}@{scale}x", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tiles_scale( + request: Request, + z: str, + x: str, + y: str, + scale: str, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy scaled tile paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}@{scale}x") + + +@router.api_route( + "/mosaic/{search_id}/tiles/{z}/{x}/{y}@{scale}x.{format}", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tiles_scale_format( + request: Request, + z: str, + x: str, + y: str, + scale: str, + format: str, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy scaled formatted tiles to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect( + request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}@{scale}x.{format}" + ) + + +@router.api_route( + "/mosaic/{search_id}/tiles/{z}/{x}/{y}/assets", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tile_assets( + request: Request, + z: str, + x: str, + y: str, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy tile asset paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}/assets") + + +@router.api_route( + "/mosaic/{search_id}/tilejson.json", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_tilejson( + request: Request, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy TileJSON paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/WebMercatorQuad/tilejson.json") + + +@router.api_route( + "/mosaic/{search_id}/map.html", methods=["GET", "HEAD"], include_in_schema=False +) +async def redirect_map( + request: Request, + search_id: str | None = None, + collection_id: str | None = None, +): + """Redirect legacy viewer paths to native WebMercatorQuad routes.""" + target = _redirect_search_id(search_id, collection_id) + return _redirect(request, f"{target}/WebMercatorQuad/map.html") + + +@router.api_route( + "/mosaic/{subpath:path}", methods=["GET", "POST"], include_in_schema=False +) +async def redirect_to_searches(request: Request, subpath: str): + """Keep the old pgSTAC route prefix working for non-TMS endpoints.""" + new_path = "/searches/" if subpath == "list" else f"/searches/{subpath}" + return _redirect(request, new_path) diff --git a/cdk/runtimes/eoapi/raster/eoapi/raster/main.py b/cdk/runtimes/eoapi/raster/eoapi/raster/main.py index 77d6045..9ae15f8 100644 --- a/cdk/runtimes/eoapi/raster/eoapi/raster/main.py +++ b/cdk/runtimes/eoapi/raster/eoapi/raster/main.py @@ -2,8 +2,8 @@ Handler for AWS Lambda. """ -from fastapi import APIRouter, Request -from fastapi.responses import RedirectResponse +from cogeo_mosaic.backends import MosaicBackend +from fastapi import Request from rio_tiler.io import STACReader from titiler.core.factory import MultiBaseTilerFactory, TilerFactory from titiler.extensions import ( @@ -11,9 +11,18 @@ cogViewerExtension, stacViewerExtension, ) +from titiler.mosaic.extensions.mosaicjson import MosaicJSONExtension +from titiler.mosaic.extensions.wmts import wmtsExtension +from titiler.mosaic.factory import MosaicTilerFactory from titiler.pgstac.main import app # noqa: E402 -from eoapi.raster.factory import MosaicTilerFactory +from eoapi.raster.factory import ( + mosaic_path, + redirect_collection_compatibility, +) +from eoapi.raster.factory import ( + router as custom_mosaic_router, +) ######################################## # Include the /cog router @@ -51,26 +60,22 @@ ) ############################################################# -# Include the /mosaics router (for legacy mosaicjson support) -############################################################# -mosaic = MosaicTilerFactory() -app.include_router(mosaic.router, tags=["MosaicJSON"]) - -######################################## -# Redirect /mosaic requests to /searches -######################################## -redirect_router = APIRouter() - - -@redirect_router.api_route( - "/mosaic/{subpath:path}", methods=["GET", "POST"], status_code=307 +# Include native MosaicJSON tiling routes and the custom storage resource API. +mosaic = MosaicTilerFactory( + backend=MosaicBackend, + path_dependency=mosaic_path, + router_prefix="/mosaics/{mosaic_id}", + add_statistics=True, + add_part=True, + extensions=[MosaicJSONExtension(), wmtsExtension()], ) -async def redirect_to_searches(request: Request): - new_path = request.url.path.replace("/mosaic", "/searches", 1) - query_string = request.url.query - if query_string: - new_path = f"{new_path}?{query_string}" - return RedirectResponse(url=new_path) +app.include_router(mosaic.router, prefix="/mosaics/{mosaic_id}", tags=["MosaicJSON"]) +app.include_router(custom_mosaic_router) -app.include_router(redirect_router) +@app.middleware("http") +async def redirect_legacy_collection_routes(request: Request, call_next): + """Redirect old collection paths before native pgSTAC dependencies run.""" + if response := redirect_collection_compatibility(request): + return response + return await call_next(request) diff --git a/cdk/runtimes/eoapi/raster/eoapi/raster/models.py b/cdk/runtimes/eoapi/raster/eoapi/raster/models.py index 7c59a6c..733771a 100644 --- a/cdk/runtimes/eoapi/raster/eoapi/raster/models.py +++ b/cdk/runtimes/eoapi/raster/eoapi/raster/models.py @@ -6,14 +6,6 @@ from stac_pydantic.api import Search -def to_camel(snake_str: str) -> str: - """ - Converts snake_case_string to camelCaseString - """ - first, *others = snake_str.split("_") - return "".join([first.lower(), *map(str.title, others)]) - - # Link and Links derived from models in https://github.com/stac-utils/stac-pydantic class Link(BaseModel): """Link Relation""" @@ -60,6 +52,8 @@ def validate_datetime(cls, v): overrides default validation due to issue https://github.com/stac-utils/stac-pydantic/issues/78 """ # Single date is interpreted as end date + if v is None: + return v values = v.split("/") if "/" in v else ["..", v] dates = [] for value in values: @@ -89,24 +83,4 @@ class UrisRequestBody(BaseModel): class TooManyResultsException(Exception): - """exception when there are too many STAC API results to generate a mosaicjson""" - - def __init__(self, message): - """init""" - self.message = message - - -class StoreException(Exception): - """exception when there is a problem storing the mosaicjson in the datastore""" - - def __init__(self, message): - """init""" - self.message = message - - -class UnsupportedOperationException(Exception): - """exception for unsupported operation""" - - def __init__(self, message): - """init""" - self.message = message + """A STAC search exceeded the mosaic creation result limit.""" diff --git a/cdk/runtimes/eoapi/raster/eoapi/raster/settings.py b/cdk/runtimes/eoapi/raster/eoapi/raster/settings.py index ec5d0e1..08ecb5c 100644 --- a/cdk/runtimes/eoapi/raster/eoapi/raster/settings.py +++ b/cdk/runtimes/eoapi/raster/eoapi/raster/settings.py @@ -1,6 +1,6 @@ """settings for titiler-pgstac runtime""" -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict class MosaicSettings(BaseSettings): @@ -10,8 +10,4 @@ class MosaicSettings(BaseSettings): host: str | None format: str = ".json.gz" # format will be ignored for dynamodb backend - class Config: - """model config""" - - env_prefix = "MOSAIC_" - env_file = ".env" + model_config = SettingsConfigDict(env_prefix="MOSAIC_", env_file=".env") diff --git a/cdk/runtimes/eoapi/raster/pyproject.toml b/cdk/runtimes/eoapi/raster/pyproject.toml index 2364604..8eb0686 100644 --- a/cdk/runtimes/eoapi/raster/pyproject.toml +++ b/cdk/runtimes/eoapi/raster/pyproject.toml @@ -16,13 +16,15 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler.pgstac==1.9.0", - "titiler.extensions[cogeo]>=0.24,<0.25", + "titiler-pgstac==3.2.0", + "titiler-extensions[cogeo]==2.3.0", "pystac-client==0.9.0", "stac-pydantic>=3.1.3", - "importlib_resources>=1.1.0;python_version<'3.9'", "numpy>=2.3.3", "numexpr>=2.13.1", + "cogeo-mosaic>=9.2,<10", + "titiler-core==2.3.0", + "titiler-mosaic==2.3.0", ] [project.optional-dependencies] diff --git a/cdk/runtimes/eoapi/raster/tests/test_runtime.py b/cdk/runtimes/eoapi/raster/tests/test_runtime.py new file mode 100644 index 0000000..e675d34 --- /dev/null +++ b/cdk/runtimes/eoapi/raster/tests/test_runtime.py @@ -0,0 +1,218 @@ +"""Focused behavior tests for the raster runtime.""" + +import asyncio +import importlib +import json +import os +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace + +import numpy as np +import pytest +import rasterio +from fastapi.testclient import TestClient +from rasterio.transform import from_bounds + +os.environ.setdefault("MOSAIC_BACKEND", "dynamodb://") +os.environ.setdefault("MOSAIC_HOST", "us-west-2/test-table") + +from eoapi.raster.factory import mosaic_config +from eoapi.raster.main import app + + +@pytest.mark.parametrize( + ("method", "path", "location"), + [ + ( + "GET", + "/mosaic/search-1/tiles/1/2/3.png?x=a%2Fb&x=two&collection_id=ignored", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3.png?x=a%2Fb&x=two&collection_id=ignored", + ), + ( + "GET", + "/mosaic/search-1/tiles/1/2/3@2x.webp", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3@2x.webp", + ), + ( + "GET", + "/mosaic/search-1/tiles/1/2/3/assets", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3/assets", + ), + ( + "GET", + "/mosaic/search-1/tilejson.json", + "/searches/search-1/WebMercatorQuad/tilejson.json", + ), + ( + "GET", + "/mosaic/search-1/map.html", + "/searches/search-1/WebMercatorQuad/map.html", + ), + ( + "GET", + "/collections/c-1/tiles/1/2/3.png", + "/collections/c-1/tiles/WebMercatorQuad/1/2/3.png", + ), + ( + "GET", + "/collections/c-1/tiles/1/2/3@2x.webp", + "/collections/c-1/tiles/WebMercatorQuad/1/2/3@2x.webp", + ), + ( + "GET", + "/collections/c-1/tiles/1/2/3/assets", + "/collections/c-1/tiles/WebMercatorQuad/1/2/3/assets", + ), + ( + "GET", + "/collections/c-1/tilejson.json", + "/collections/c-1/WebMercatorQuad/tilejson.json", + ), + ( + "GET", + "/collections/c-1/map.html", + "/collections/c-1/WebMercatorQuad/map.html", + ), + ("POST", "/mosaic/register?token=a%2Fb", "/searches/register?token=a%2Fb"), + ("GET", "/mosaic/list", "/searches/"), + ("GET", "/mosaic/search-1/info", "/searches/search-1/info"), + ( + "GET", + "/mosaic/search-1/WMTSCapabilities.xml", + "/searches/search-1/WMTSCapabilities.xml", + ), + ("GET", "/mosaic/search-1/statistics", "/searches/search-1/statistics"), + ], +) +def test_legacy_redirects_preserve_methods_and_queries(method, path, location): + """Legacy no-TMS routes redirect to native paths without changing scope.""" + response = TestClient(app).request(method, path, follow_redirects=False) + + assert response.status_code == 307 + assert response.headers["location"] == location + + +def test_canonical_collection_paths_are_not_compatibility_redirects(): + """Native paths and unrelated collections endpoints never get rewritten.""" + client = TestClient(app, raise_server_exceptions=False) + + response = client.get( + "/collections/c-1/tiles/WebMercatorQuad/1/2/3.png", + follow_redirects=False, + ) + + # No pgSTAC database is configured in this focused test; a native handler is + # reached and fails there, rather than being redirected by compatibility code. + assert response.status_code == 500 + assert "location" not in response.headers + + +def test_native_routes_are_exposed_without_custom_route_overrides(): + """The upgraded pgSTAC and Mosaic factories expose their native routes.""" + paths = app.openapi()["paths"] + + assert "/searches/{search_id}/tiles/{tileMatrixSetId}/{z}/{x}/{y}" in paths + assert "/searches/{search_id}/{tileMatrixSetId}/tilejson.json" in paths + assert "/searches/{search_id}/{tileMatrixSetId}/map.html" in paths + assert "/searches/{search_id}/statistics" in paths + assert "/searches/{search_id}/feature" in paths + assert "/mosaics/{mosaic_id}/tiles/{tileMatrixSetId}/{z}/{x}/{y}" in paths + assert "/mosaics/{mosaic_id}/WMTSCapabilities.xml" in paths + assert "/mosaic/{search_id}/tiles/{z}/{x}/{y}" not in paths + + +def test_custom_mosaic_creation_storage_and_native_tile_endpoint(tmp_path, monkeypatch): + """Custom URL creation stores a mosaic usable by the native tile route.""" + monkeypatch.setattr(mosaic_config, "backend", "") + monkeypatch.setattr(mosaic_config, "host", f"{tmp_path}/") + monkeypatch.setattr(mosaic_config, "format", ".json") + + cog_path = tmp_path / "tiny.tif" + with rasterio.open( + cog_path, + "w", + driver="GTiff", + height=16, + width=16, + count=1, + dtype="uint8", + crs="EPSG:4326", + transform=from_bounds(-180, -85, 180, 85, 16, 16), + ) as dataset: + dataset.write(np.full((1, 16, 16), 100, dtype="uint8")) + + client = TestClient(app) + response = client.post( + "/mosaics", + headers={"Content-Type": "application/vnd.titiler.urls+json"}, + json={"urls": [str(cog_path)], "minzoom": 0, "maxzoom": 0}, + ) + + assert response.status_code == 201 + mosaic_id = response.json()["id"] + assert response.headers["location"].endswith(f"/mosaics/{mosaic_id}") + assert client.get(f"/mosaics/{mosaic_id}").status_code == 200 + assert client.get(f"/mosaics/{mosaic_id}/mosaicjson").status_code == 200 + + entity = client.get(f"/mosaics/{mosaic_id}").json() + assert any( + link["href"].endswith( + f"/mosaics/{mosaic_id}/tiles/WebMercatorQuad/{{z}}/{{x}}/{{y}}" + ) + for link in entity["links"] + ) + tile = client.get(f"/mosaics/{mosaic_id}/tiles/WebMercatorQuad/0/0/0.png") + assert tile.status_code == 200 + assert tile.headers["content-type"] == "image/png" + + +def test_lambda_startup_uses_new_pg_settings_and_handles_optional_snapstart( + monkeypatch, +): + """The Lambda module imports and builds routes with TiTiler 3.2 settings.""" + + class SecretsClient: + def get_secret_value(self, **kwargs): + return { + "SecretString": json.dumps( + { + "host": "database", + "dbname": "postgis", + "username": "raster", + "password": "secret", + "port": 5432, + } + ) + } + + fake_boto3 = ModuleType("boto3") + fake_boto3.session = SimpleNamespace( + Session=lambda: SimpleNamespace(client=lambda **kwargs: SecretsClient()) + ) + monkeypatch.syspath_prepend(str(Path(__file__).resolve().parents[5])) + monkeypatch.setitem(sys.modules, "boto3", fake_boto3) + monkeypatch.setattr(app, "middleware_stack", None) + monkeypatch.setenv("PGSTAC_SECRET_ARN", "arn:local:test") + monkeypatch.delenv("AWS_EXECUTION_ENV", raising=False) + + module = importlib.import_module("cdk.handlers.raster_handler") + assert module.pg_settings.pghost == "database" + assert module.pg_settings.pgdatabase == "postgis" + assert module.pg_settings.pguser == "raster" + assert module.pg_settings.pgpassword == "secret" + assert module.pg_settings.pgport == 5432 + + connections = [] + + async def connect_to_db(app, settings): + connections.append(settings) + + monkeypatch.setattr(module, "connect_to_db", connect_to_db) + asyncio.run(module.startup_event()) + + assert connections == [module.pg_settings] + assert any( + path == "/searches/{search_id}/tiles/{tileMatrixSetId}/{z}/{x}/{y}" + for path in module.app.state.path_templates.values() + ) diff --git a/cdk/runtimes/eoapi/raster/uv.lock b/cdk/runtimes/eoapi/raster/uv.lock index 4cb7235..3557caf 100644 --- a/cdk/runtimes/eoapi/raster/uv.lock +++ b/cdk/runtimes/eoapi/raster/uv.lock @@ -150,7 +150,7 @@ wheels = [ [[package]] name = "cogeo-mosaic" -version = "8.2.0" +version = "9.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -158,7 +158,7 @@ dependencies = [ { name = "click" }, { name = "click-plugins" }, { name = "cligj" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "morecantile" }, { name = "numpy" }, { name = "pydantic" }, @@ -168,9 +168,9 @@ dependencies = [ { name = "shapely" }, { name = "supermorecado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/dd/64a58399a87d21828ebda01b28f0b66a962ca71380ff10111269834ef499/cogeo_mosaic-8.2.0.tar.gz", hash = "sha256:5dfc79dde66e8563959d9896124209c12a97edb611d87f255550c14840818bfa", size = 33948, upload-time = "2025-05-06T08:42:39.753Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a5/87c3354b4072a9f403ff6dc325aa595acfee9d289e5ee7bd28a90ab8c691/cogeo_mosaic-9.2.0.tar.gz", hash = "sha256:8694c3104063662d1ae79fca139eed38d591417af318b9dd89855419e76e2440", size = 27972, upload-time = "2026-06-18T04:57:39.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/e0/2a8446c97c8dc190cd627eaecd476b5b3a872398c359e32e8316ec1a4d62/cogeo_mosaic-8.2.0-py3-none-any.whl", hash = "sha256:b943cd21e9a8b68135553c5484fb88dd065a23e24349430a39eb5e6487cf11e5", size = 40477, upload-time = "2025-05-06T08:42:38.64Z" }, + { url = "https://files.pythonhosted.org/packages/07/a1/22a770179653f84558b16929c9722a58e81d444d81969e1429fb73c52667/cogeo_mosaic-9.2.0-py3-none-any.whl", hash = "sha256:5b53e28f9fcc52ee804be5a400f5a68dfd82c82f710d9d55a48a9053b33ea51b", size = 39937, upload-time = "2026-06-18T04:57:38.264Z" }, ] [[package]] @@ -205,15 +205,108 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cql2" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/dd/1e71fa93b7c8c5c33546c503ea0e941db5e3f78de645d2712c4b1d4cde07/cql2-0.6.0.tar.gz", hash = "sha256:b6512cabdc9eeae75fff8473ccc58252cb2c5847b539a8f7d2af57134117e877", size = 246382, upload-time = "2026-08-13T12:10:36.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/80/929f77b3a8c772db0fec977811eda62796f4c617bce41345887004f8bf3e/cql2-0.6.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:db01194e6a92ab3e20da93bcd6a6ecea66309d748b05072fa035311599c156fe", size = 3505190, upload-time = "2026-08-13T12:10:28.747Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6e/a02a126936f080b9fe4d02ec05aaef0f3e442c6d3c1dffd43cb1d7a57ec0/cql2-0.6.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2264e500e13a2449201a8ab1ac8c56a74d87f3a539715d415f0a48af827ff27e", size = 3505834, upload-time = "2026-08-13T12:10:27.397Z" }, + { url = "https://files.pythonhosted.org/packages/54/10/3ef66e98dd2a9595a7aabb556ecb06aab4091e9bc369a056d22625cfd6b8/cql2-0.6.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78c151d54f50280a930fd3c424d3c6f3592e8da06baf02699020e665ffea9c5e", size = 3928862, upload-time = "2026-08-13T12:10:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f7/d219c01d41a2a70fcb95f0e0ef37cc52ea9a6f1dff244b815cfb3ba230b9/cql2-0.6.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a35639bc7acb4f16e745227c8a3324b4d679f9fce938872927c5349ae1845e", size = 3789906, upload-time = "2026-08-13T12:10:20.158Z" }, + { url = "https://files.pythonhosted.org/packages/00/15/9713c5996419a09cb7b1d11c228bab09b6de2f7cce20b218a7c3b00f3ed2/cql2-0.6.0-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258e3832b72642967d9cf5d4afa6eaf37ebe63ea59f9df1e417fa9ab17b79f72", size = 4230511, upload-time = "2026-08-13T12:10:24.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/8de6b40d829b238953f2569c539fb024b92ade158e07b4d031272844b578/cql2-0.6.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d8f81a5d8b60bc0a304f8de4b039fa7baa6249d81ce98edf7f93fdf7d6f320", size = 5461848, upload-time = "2026-08-13T12:10:21.666Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ce/762c2ee2b4d96db0bec1328a271f91097edfb419bad2e0ea234ce555b59d/cql2-0.6.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba1d3d3ca56510844e6fe52d2cac8af6263c71c0c0763152a1565c43e5511675", size = 3831285, upload-time = "2026-08-13T12:10:23.011Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c7/0969ffe232d06502121e90f9753a0805165457fff63160985651ac075f8e/cql2-0.6.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:420a7bb7f5faa63d2181e07d76041ccf0a360c274ee01af03798253e90c63f62", size = 3919117, upload-time = "2026-08-13T12:10:25.996Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ca/a8a54e22002dfe7c3faa141e862842cdca92bcadb934857d381856c7852a/cql2-0.6.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5bc9e6224eadf6c4cf9f35b0f36ba164dbd0f3604a2f8f9c487de158ed3ba4c9", size = 4103810, upload-time = "2026-08-13T12:10:30.025Z" }, + { url = "https://files.pythonhosted.org/packages/33/9c/2a212c300b0fb9572e8feb4c48445971cfbb76c90718e12d00bd077a39ac/cql2-0.6.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:555ee716e96f40d8e5a7f28db8aaed469efbef6bc12cdb31c469df477d6e77ce", size = 4066284, upload-time = "2026-08-13T12:10:31.427Z" }, + { url = "https://files.pythonhosted.org/packages/ae/67/3644d04fc305c767cb55f3727e2bea10a44d8c1ecab4811f672318bf04b7/cql2-0.6.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:0f888faa2748e26620292f39f33364a17c485ea712c7fecdbb25bdb177dc11be", size = 4264067, upload-time = "2026-08-13T12:10:33.437Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f1/9ebc2d8d52995a899e85a3c7ec6a29da609562093f5e9e2f32749d11d8f8/cql2-0.6.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aea8d89850345dea99811c1b63ab1fe6ebd6755da5eeee4811b9554afbdf2f2a", size = 4236509, upload-time = "2026-08-13T12:10:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/27/f0/51032e6a66221ed67d74c17933633f16df7403f64509b0fe527b9581e9c6/cql2-0.6.0-cp310-abi3-win32.whl", hash = "sha256:8de7cfb144d1484a90c10b13f012c36a54e15c141a1abc897e1d62fea0930c3b", size = 2759592, upload-time = "2026-08-13T12:10:38.785Z" }, + { url = "https://files.pythonhosted.org/packages/a8/4e/5c5d37825485532931e6291f622116f95a5260e10d8b4439a2f1c686454e/cql2-0.6.0-cp310-abi3-win_amd64.whl", hash = "sha256:224f0f5afb7eedde51986f75f018dfc1561c8bd47a3cbefb45be81b7b94b4208", size = 3042298, upload-time = "2026-08-13T12:10:37.489Z" }, +] + +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/0d/7c84c913a5fae85b773a9dcf8874390f9d68ba0fcc6630efa7ff1541b950/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dba5c14b8b4f73ea1e65720f5a3fe4280c1d27761238378be8274135c60bbc6e", size = 3553368, upload-time = "2025-07-27T21:22:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/2b/cc/4f6d185d8a744776f53035e72831ff8eefc2354f46ab836f4bd3c4f6c138/cramjam-2.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11eb40722b3fcf3e6890fba46c711bf60f8dc26360a24876c85e52d76c33b25b", size = 1860014, upload-time = "2025-07-27T21:22:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a8/626c76263085c6d5ded0e71823b411e9522bfc93ba6cc59855a5869296e7/cramjam-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeb26e2898994b6e8319f19a4d37c481512acdcc6d30e1b5ecc9d8ec57e835cb", size = 1693512, upload-time = "2025-07-27T21:22:30.999Z" }, + { url = "https://files.pythonhosted.org/packages/e9/52/0851a16a62447532e30ba95a80e638926fdea869a34b4b5b9d0a020083ba/cramjam-2.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f8d82081ed7d8fe52c982bd1f06e4c7631a73fe1fb6d4b3b3f2404f87dc40fe", size = 2025285, upload-time = "2025-07-27T21:22:32.954Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/122e444f59dbc216451d8e3d8282c9665dc79eaf822f5f1470066be1b695/cramjam-2.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:092a3ec26e0a679305018380e4f652eae1b6dfe3fc3b154ee76aa6b92221a17c", size = 1761327, upload-time = "2025-07-27T21:22:34.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/bc/3a0189aef1af2b29632c039c19a7a1b752bc21a4053582a5464183a0ad3d/cramjam-2.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:529d6d667c65fd105d10bd83d1cd3f9869f8fd6c66efac9415c1812281196a92", size = 1854075, upload-time = "2025-07-27T21:22:36.157Z" }, + { url = "https://files.pythonhosted.org/packages/2e/80/8a6343b13778ce52d94bb8d5365a30c3aa951276b1857201fe79d7e2ad25/cramjam-2.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:555eb9c90c450e0f76e27d9ff064e64a8b8c6478ab1a5594c91b7bc5c82fd9f0", size = 2032710, upload-time = "2025-07-27T21:22:38.17Z" }, + { url = "https://files.pythonhosted.org/packages/df/6b/cd1778a207c29eda10791e3dfa018b588001928086e179fc71254793c625/cramjam-2.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5edf4c9e32493035b514cf2ba0c969d81ccb31de63bd05490cc8bfe3b431674e", size = 2068353, upload-time = "2025-07-27T21:22:39.615Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f0/5c2a5cd5711032f3b191ca50cb786c17689b4a9255f9f768866e6c9f04d9/cramjam-2.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2fe41f48c4d58d923803383b0737f048918b5a0d10390de9628bb6272b107", size = 1978104, upload-time = "2025-07-27T21:22:41.106Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8b/b363a5fb2c3347504fe9a64f8d0f1e276844f0e532aa7162c061cd1ffee4/cramjam-2.11.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca14cf1cabdb0b77d606db1bb9e9ca593b1dbd421fcaf251ec9a5431ec449f3", size = 2030779, upload-time = "2025-07-27T21:22:42.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7b/d83dad46adb6c988a74361f81ad9c5c22642be53ad88616a19baedd06243/cramjam-2.11.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:309e95bf898829476bccf4fd2c358ec00e7ff73a12f95a3cdeeba4bb1d3683d5", size = 2155297, upload-time = "2025-07-27T21:22:44.6Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/60d9be4cb33d8740a4aa94c7513f2ef3c4eba4fd13536f086facbafade71/cramjam-2.11.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:86dca35d2f15ef22922411496c220f3c9e315d5512f316fe417461971cc1648d", size = 2169255, upload-time = "2025-07-27T21:22:46.534Z" }, + { url = "https://files.pythonhosted.org/packages/11/b0/4a595f01a243aec8ad272b160b161c44351190c35d98d7787919d962e9e5/cramjam-2.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:193c6488bd2f514cbc0bef5c18fad61a5f9c8d059dd56edf773b3b37f0e85496", size = 2155651, upload-time = "2025-07-27T21:22:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/38/47/7776659aaa677046b77f527106e53ddd47373416d8fcdb1e1a881ec5dc06/cramjam-2.11.0-cp312-cp312-win32.whl", hash = "sha256:514e2c008a8b4fa823122ca3ecab896eac41d9aa0f5fc881bd6264486c204e32", size = 1603568, upload-time = "2025-07-27T21:22:50.084Z" }, + { url = "https://files.pythonhosted.org/packages/75/b1/d53002729cfd94c5844ddfaf1233c86d29f2dbfc1b764a6562c41c044199/cramjam-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:53fed080476d5f6ad7505883ec5d1ec28ba36c2273db3b3e92d7224fe5e463db", size = 1709287, upload-time = "2025-07-27T21:22:51.534Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/406c5dc0f8e82385519d8c299c40fd6a56d97eca3fcd6f5da8dad48de75b/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2c289729cc1c04e88bafa48b51082fb462b0a57dbc96494eab2be9b14dca62af", size = 3553330, upload-time = "2025-07-27T21:22:53.124Z" }, + { url = "https://files.pythonhosted.org/packages/00/ad/4186884083d6e4125b285903e17841827ab0d6d0cffc86216d27ed91e91d/cramjam-2.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:045201ee17147e36cf43d8ae2fa4b4836944ac672df5874579b81cf6d40f1a1f", size = 1859756, upload-time = "2025-07-27T21:22:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/54/01/91b485cf76a7efef638151e8a7d35784dae2c4ff221b1aec2c083e4b106d/cramjam-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:619cd195d74c9e1d2a3ad78d63451d35379c84bd851aec552811e30842e1c67a", size = 1693609, upload-time = "2025-07-27T21:22:56.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/d0c80d279b2976870fc7d10f15dcb90a3c10c06566c6964b37c152694974/cramjam-2.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6eb3ae5ab72edb2ed68bdc0f5710f0a6cad7fd778a610ec2c31ee15e32d3921e", size = 2024912, upload-time = "2025-07-27T21:22:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/d6/70/88f2a5cb904281ed5d3c111b8f7d5366639817a5470f059bcd26833fc870/cramjam-2.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7da3f4b19e3078f9635f132d31b0a8196accb2576e3213ddd7a77f93317c20", size = 1760715, upload-time = "2025-07-27T21:22:59.528Z" }, + { url = "https://files.pythonhosted.org/packages/b2/06/cf5b02081132537d28964fb385fcef9ed9f8a017dd7d8c59d317e53ba50d/cramjam-2.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:57286b289cd557ac76c24479d8ecfb6c3d5b854cce54ccc7671f9a2f5e2a2708", size = 1853782, upload-time = "2025-07-27T21:23:01.07Z" }, + { url = "https://files.pythonhosted.org/packages/57/27/63525087ed40a53d1867021b9c4858b80cc86274ffe7225deed067d88d92/cramjam-2.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28952fbbf8b32c0cb7fa4be9bcccfca734bf0d0989f4b509dc7f2f70ba79ae06", size = 2032354, upload-time = "2025-07-27T21:23:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ef/dbba082c6ebfb6410da4dd39a64e654d7194fcfd4567f85991a83fa4ec32/cramjam-2.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78ed2e4099812a438b545dfbca1928ec825e743cd253bc820372d6ef8c3adff4", size = 2068007, upload-time = "2025-07-27T21:23:04.526Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/d902b9358a46a086938feae83b2251720e030f06e46006f4c1fc0ac9da20/cramjam-2.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9aecd5c3845d415bd6c9957c93de8d93097e269137c2ecb0e5a5256374bdc8", size = 1977485, upload-time = "2025-07-27T21:23:06.058Z" }, + { url = "https://files.pythonhosted.org/packages/e8/03/982f54553244b0afcbdb2ad2065d460f0ab05a72a96896a969a1ca136a1e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:362fcf4d6f5e1242a4540812455f5a594949190f6fbc04f2ffbfd7ae0266d788", size = 2030447, upload-time = "2025-07-27T21:23:07.679Z" }, + { url = "https://files.pythonhosted.org/packages/74/5f/748e54cdb665ec098ec519e23caacc65fc5ae58718183b071e33fc1c45b4/cramjam-2.11.0-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:13240b3dea41b1174456cb9426843b085dc1a2bdcecd9ee2d8f65ac5703374b0", size = 2154949, upload-time = "2025-07-27T21:23:09.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/81/c4e6cb06ed69db0dc81f9a8b1dc74995ebd4351e7a1877143f7031ff2700/cramjam-2.11.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:c54eed83726269594b9086d827decc7d2015696e31b99bf9b69b12d9063584fe", size = 2168925, upload-time = "2025-07-27T21:23:10.976Z" }, + { url = "https://files.pythonhosted.org/packages/13/5b/966365523ce8290a08e163e3b489626c5adacdff2b3da9da1b0823dfb14e/cramjam-2.11.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f8195006fdd0fc0a85b19df3d64a3ef8a240e483ae1dfc7ac6a4316019eb5df2", size = 2154950, upload-time = "2025-07-27T21:23:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/7f8eb5c534b72b32c6eb79d74585bfee44a9a5647a14040bb65c31c2572d/cramjam-2.11.0-cp313-cp313-win32.whl", hash = "sha256:ccf30e3fe6d770a803dcdf3bb863fa44ba5dc2664d4610ba2746a3c73599f2e4", size = 1603199, upload-time = "2025-07-27T21:23:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/37/05/47b5e0bf7c41a3b1cdd3b7c2147f880c93226a6bef1f5d85183040cbdece/cramjam-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ee36348a204f0a68b03400f4736224e9f61d1c6a1582d7f875c1ca56f0254268", size = 1708924, upload-time = "2025-07-27T21:23:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, +] + [[package]] name = "eoapi-raster" source = { editable = "." } dependencies = [ + { name = "cogeo-mosaic" }, { name = "numexpr" }, { name = "numpy" }, { name = "pystac-client" }, { name = "stac-pydantic" }, + { name = "titiler-core" }, { name = "titiler-extensions", extra = ["cogeo"] }, + { name = "titiler-mosaic" }, { name = "titiler-pgstac" }, ] @@ -233,7 +326,7 @@ psycopg-c = [ [package.metadata] requires-dist = [ - { name = "importlib-resources", marker = "python_full_version < '3.9'", specifier = ">=1.1.0" }, + { name = "cogeo-mosaic", specifier = ">=9.2,<10" }, { name = "mangum", marker = "extra == 'lambda'", specifier = ">=0.19.0" }, { name = "numexpr", specifier = ">=2.13.1" }, { name = "numpy", specifier = ">=2.3.3" }, @@ -242,14 +335,16 @@ requires-dist = [ { name = "psycopg", extras = ["pool"], marker = "extra == 'psycopg'" }, { name = "pystac-client", specifier = "==0.9.0" }, { name = "stac-pydantic", specifier = ">=3.1.3" }, - { name = "titiler-extensions", extras = ["cogeo"], specifier = ">=0.24,<0.25" }, - { name = "titiler-pgstac", specifier = "==1.9.0" }, + { name = "titiler-core", specifier = "==2.3.0" }, + { name = "titiler-extensions", extras = ["cogeo"], specifier = "==2.3.0" }, + { name = "titiler-mosaic", specifier = "==2.3.0" }, + { name = "titiler-pgstac", specifier = "==3.2.0" }, ] provides-extras = ["lambda", "psycopg", "psycopg-binary", "psycopg-c"] [[package]] name = "fastapi" -version = "0.138.1" +version = "0.141.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -258,9 +353,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/c9/5e8defe249899c0dc900643695fc07829a67fc88b4ff2cdb03fcbdbf5a4b/fastapi-0.138.1.tar.gz", hash = "sha256:96e3702dce09ee0dce48856135620d3d865ca684a79fe7513fd7b13a12f82862", size = 419646, upload-time = "2026-06-25T15:40:42.115Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/a9/69a6924f645eb4dd8cd625bf255b3625990eb3e14e073438a53c405dcd3e/fastapi-0.138.1-py3-none-any.whl", hash = "sha256:b994cae7ba8b82c976a728b544244de31333fa5f7d261f9a1dffe526444cae23", size = 129182, upload-time = "2026-06-25T15:40:40.771Z" }, + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, ] [[package]] @@ -285,40 +380,51 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/a4568248771ce81957bfb7cc600264a40fbcda092391ee1c415c50be4bea/httpcore2-2.13.1-py3-none-any.whl", hash = "sha256:e1e05d4f25f7d7d496bfb96748f6f4b67657b03da069b3a68c36069f3db73d0a", size = 83423, upload-time = "2026-09-23T07:47:19.365Z" }, ] [[package]] -name = "httpx" -version = "0.28.1" +name = "httpx2" +version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "sys_platform == 'emscripten'" }, { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/44/474bef2a0e9d90f1715d32cb98b0738695ca17ba324095fb2497ed7fbd59/httpx2-2.13.1.tar.gz", hash = "sha256:e48744a19e3af5ee48313d0ce5fe941d5422fae5705ea922a4aabf94d7800dfa", size = 100405, upload-time = "2026-09-23T07:47:23.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9c/6fe8931fd9f381042a9e4c7d5a7b4cbf7016b252bec0c99a49fce42c3326/httpx2-2.13.1-py3-none-any.whl", hash = "sha256:6dff50fabc270ee5fd25d845d0b078ed20564579744d6d962850975996d2f9a4", size = 95597, upload-time = "2026-09-23T07:47:20.995Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, ] [[package]] name = "idna" -version = "3.15" +version = "3.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, ] [[package]] @@ -437,16 +543,17 @@ wheels = [ [[package]] name = "morecantile" -version = "6.2.0" +version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, + { name = "click" }, { name = "pydantic" }, { name = "pyproj" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/72/2d0e1f1e936538004581f792f8a2377831761fd12e4ed0a665abf768fc60/morecantile-6.2.0.tar.gz", hash = "sha256:65c7150ea68bbe16ee6f75f3f171ac1ae51ab26e7a77c92a768048f40f916412", size = 46317, upload-time = "2024-12-19T15:35:47.233Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/46/20ec1fad75ed4fc4b20c86550507f868366154040e6db1d5dd9ca6538b6f/morecantile-7.1.0.tar.gz", hash = "sha256:7faf41f8b3c364a322fe96d44f43b6ba55dc0ba7454222cebc3b695ffec4bc97", size = 43148, upload-time = "2026-09-16T15:47:55.806Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/09/6c/6ca6ed6b93c9879e6a804515169faefcd99e02114ef113598de9b71d27be/morecantile-6.2.0-py3-none-any.whl", hash = "sha256:a3cc8f85c6afcddb6c2ec933ad692557f96e89689730dbbd4350bdcf6ac52be0", size = 49473, upload-time = "2024-12-19T15:35:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/90cb898ee87d0a5c89c60a6a95978c70bceab4180abc8855de9771df09e1/morecantile-7.1.0-py3-none-any.whl", hash = "sha256:1a6fd28a22467a6ec3fef1dfc4659d892a48a3c7e5e4acdceff68c6cc7956b0b", size = 50901, upload-time = "2026-09-16T15:47:54.369Z" }, ] [[package]] @@ -905,7 +1012,7 @@ wheels = [ [[package]] name = "rio-cogeo" -version = "5.4.2" +version = "7.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -913,31 +1020,31 @@ dependencies = [ { name = "pydantic" }, { name = "rasterio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d8/ac/94a3a6a4f4ec1d78b48fa9895f40c4e33e8ef10fa69137fdf7b4b70a937b/rio_cogeo-5.4.2.tar.gz", hash = "sha256:a6d48dfd6c4ade9f2b67e381958cc5939fa5de53b184c56e13b0b009d33ae84c", size = 21219, upload-time = "2025-06-27T08:00:33.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/de/e81e743e304a46da2f89540dae0f15a793b9c62fcad2a1229687bb8a3b38/rio_cogeo-7.0.3.tar.gz", hash = "sha256:1e0104c49908d3175c5647b8ee2c5ed9f86cca84afe88af0ef623f28d85a1850", size = 19305, upload-time = "2026-09-24T08:00:52.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/03/e6e4e10465802eb839a4799dac1a73d2dffcb3c978a8999b5d94953a40a6/rio_cogeo-5.4.2-py3-none-any.whl", hash = "sha256:6f11a11f4bc59added4c10a946b91c4f4ec186db0163039417cce784e56c3ebe", size = 21106, upload-time = "2025-06-27T08:00:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/65/b610e10335c2d40170864486e12d88eb5888222c58b4bf17d73c780c1841/rio_cogeo-7.0.3-py3-none-any.whl", hash = "sha256:63b33767251002e9f3692772aa88282cd465c1412834d9462a2e30d1c7e77472", size = 21863, upload-time = "2026-09-24T08:00:51.292Z" }, ] [[package]] name = "rio-tiler" -version = "7.9.0" +version = "9.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, { name = "cachetools" }, { name = "color-operations" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "morecantile" }, { name = "numexpr" }, { name = "numpy" }, { name = "pydantic" }, { name = "pystac" }, { name = "rasterio" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/a8/b2c1c3b4504538040782d3d1b282b4f4f5666149432eb3f4b85a5ea231e1/rio_tiler-7.9.0.tar.gz", hash = "sha256:2587c8ae4162e5dffce2fa2fb4779cffedb2006691687189f2b3644c96748b57", size = 175702, upload-time = "2025-10-07T12:23:49.195Z" } +sdist = { url = "https://files.pythonhosted.org/packages/64/0a/a9ea688aa83777a89d1eb1b2d5a3c9b5e05b0e0d2db4b7206e9140f671f7/rio_tiler-9.4.7.tar.gz", hash = "sha256:f456e9df5cf81ba7cc7901ff709391dc78afff0d07c93959b9fe35bab36cf62d", size = 213061, upload-time = "2026-09-28T08:49:18.437Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/29/54/4cd91363d54c6820954273e471c9b54c109a625e98e71fd2d6ed13c0fd66/rio_tiler-7.9.0-py3-none-any.whl", hash = "sha256:03b46df03a656a39e42812c1e30fd1639e7caf61bc1c280b3e4ced3665e0c02c", size = 269850, upload-time = "2025-10-07T12:23:47.893Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d5/f12d62a287068619288f268a355b9d3e8158b5aeed94eb050300ab774819/rio_tiler-9.4.7-py3-none-any.whl", hash = "sha256:eed09bd5f74fd1e321e4502fd56053eaa86ece250693b3ba4c53194da84535d3", size = 316733, upload-time = "2026-09-28T08:49:16.644Z" }, ] [[package]] @@ -1127,16 +1234,16 @@ wheels = [ [[package]] name = "stac-pydantic" -version = "3.1.3" +version = "3.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "geojson-pydantic" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/4b/71c3d83a8f2c990bd16baae0b95d996a50a92c9d4449952ea5099ce5f187/stac_pydantic-3.1.3.tar.gz", hash = "sha256:40d3ed7604e4520ad53c16b9d0c9ba80d398269a11b0ee090cbabd73b6b198ec", size = 20714, upload-time = "2024-10-14T12:46:05.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/40/efef25a9a0f3d36b1264fbdd11f136bca7d3d069dc6dbd5c77c432ed20ff/stac_pydantic-3.6.0.tar.gz", hash = "sha256:29aa0b98e5223e0b0172663575a6df25f4dd359c577a2b6bfafdaa4cad18a917", size = 16287, upload-time = "2026-06-26T16:13:08.855Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9c/db0d0771aeb1ffc4a1315ece564f5665a91516b90919d1149ed9645b84ce/stac_pydantic-3.1.3-py3-none-any.whl", hash = "sha256:c984f9ee573459041429801a3f7e5af42c94cc9b4e466ab06f59e99ee8e5b826", size = 23338, upload-time = "2024-10-14T12:46:04.037Z" }, + { url = "https://files.pythonhosted.org/packages/17/43/46bf5ba8595c563213a76e652d63839686b6819378b921a09d040a7faf8b/stac_pydantic-3.6.0-py3-none-any.whl", hash = "sha256:7e5ca441639274fca12d299e2a9d58c327153940f775729924e200d7103e2d5f", size = 25505, upload-time = "2026-06-26T16:13:07.955Z" }, ] [[package]] @@ -1152,22 +1259,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "starlette-cramjam" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/83/79d2060127c8158849f375a21adf4116639d4829b84bbca598514a2ca871/starlette_cramjam-0.7.0.tar.gz", hash = "sha256:38a70bea50d2fce91eb43acbddd544c1c7d4b5de4b26a482472d7dc142024edd", size = 7614, upload-time = "2026-04-27T08:39:30.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1f/7611cb364b60e69f50afd6b5a5dc336c24450d6a1ed87431d944700b834c/starlette_cramjam-0.7.0-py3-none-any.whl", hash = "sha256:a0754bd582aa5cdd7d2e9982ea0bd9c78521583099590971e9cf0482fe26b8a4", size = 8060, upload-time = "2026-04-27T08:39:28.676Z" }, +] + [[package]] name = "supermorecado" -version = "0.1.2" +version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "morecantile" }, { name = "rasterio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/cc/97693e7c34e56a9f3c869ff10a962e14d38c48b2c56436f0483ee77c3cc4/supermorecado-0.1.2.tar.gz", hash = "sha256:b51664d2eb12326e657a9d80c6849857c42ef3876545515ef988e266e6cc9654", size = 12171, upload-time = "2023-09-11T14:40:15.737Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/36/bb66411c216d6c1c1c08686ec526fdf6d62e268cc283d5966168d8f08da9/supermorecado-0.2.0.tar.gz", hash = "sha256:e64f5667d7f00117486d04876aa58622b38f356b19e2c61ae29332f2cabf50e1", size = 9528, upload-time = "2026-04-03T14:16:52.377Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/25/909292b8a012282d3b535e51c2626095f0ff0cfea796da637b34512fb9f6/supermorecado-0.1.2-py3-none-any.whl", hash = "sha256:5116b9ff8c8aa0b0da235cb5962449dc878515c8155adf1440268c3cf271bed6", size = 14486, upload-time = "2023-09-11T14:40:13.349Z" }, + { url = "https://files.pythonhosted.org/packages/49/91/0c82bea0f2d9875a302d4d06c0109b86b2b3e5eec0dff23fadfb15fd219a/supermorecado-0.2.0-py3-none-any.whl", hash = "sha256:247f721854af8b991d81faa554fd93e971c6d5395146ab1b9b746f1a51862f29", size = 15067, upload-time = "2026-04-03T14:16:50.866Z" }, ] [[package]] name = "titiler-core" -version = "0.24.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastapi" }, @@ -1179,23 +1299,23 @@ dependencies = [ { name = "rasterio" }, { name = "rio-tiler" }, { name = "simplejson" }, - { name = "typing-extensions" }, + { name = "starlette" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/3a/1e89d31c3ba35a09f734e1249d0548d780f2039cf64575c0d5a59bef2738/titiler_core-0.24.0.tar.gz", hash = "sha256:60a717fc5a5da03183e8c8bd28bc470af0ad26a57a61792b6cc43650fe769699", size = 70880, upload-time = "2025-09-23T15:17:10.552Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/38/bb342612976c9c6c5767ef113d7498cd058b0966b12dbcc95c108ef6c509/titiler_core-2.3.0.tar.gz", hash = "sha256:936c25ebd7d4c1991ae238c4b58c4dc349aa0c72637af9dd002d2cdbf613adc4", size = 70398, upload-time = "2026-09-15T16:28:07.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/54/2ed0b2321e1a66207d4a419abffe485140f59ee869625834c67c255d4b65/titiler_core-0.24.0-py3-none-any.whl", hash = "sha256:a51473b0cd1df6694de9134346ea044a990cf0a9717428bc2d27c705a7a5db57", size = 87421, upload-time = "2025-09-23T15:17:09.546Z" }, + { url = "https://files.pythonhosted.org/packages/57/b5/e68e1cc8032c468ca6b9f9d45d71c858e3485f60f206aef51c7e4831c12a/titiler_core-2.3.0-py3-none-any.whl", hash = "sha256:d968e151df48df82862d48c119b123f5569957961a315afb5ac804d9fd4c41cd", size = 88833, upload-time = "2026-09-15T16:28:01.247Z" }, ] [[package]] name = "titiler-extensions" -version = "0.24.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "titiler-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/34/75a4aac8a94358cd46aa2cc86bc4b5c09c1ce2a3c9777e8829d3f9bc25b6/titiler_extensions-0.24.0.tar.gz", hash = "sha256:be95627e46b74aec12357a9a75867f81c7ccab9ef6b5336c5a2d15754ddd2f50", size = 29203, upload-time = "2025-09-23T15:17:26.584Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/e3/c577d14fce0d04cbc3377f83fe38db89d0b85d85f2273b09d3ac8f7b06dc/titiler_extensions-2.3.0.tar.gz", hash = "sha256:098959a366eb0f26d6f52a7d3889ade5b1445ccf8e899b5c73ea842978370cbb", size = 32301, upload-time = "2026-09-15T16:28:08.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5c/cfca5b3f058b354c9a00a18e09b26127e255204e21025cb472de04352b46/titiler_extensions-0.24.0-py3-none-any.whl", hash = "sha256:e69128167204b03bcb13f8175f9230fb5f0e3b500d79969c9cbd2db9336fe7f5", size = 33767, upload-time = "2025-09-23T15:17:25.683Z" }, + { url = "https://files.pythonhosted.org/packages/b0/50/5f80d78c0da1b928c547c179a2cdbc9f8177ee8d10b2cf6f006a37b69f9c/titiler_extensions-2.3.0-py3-none-any.whl", hash = "sha256:b31d5f2b173e7a42ae5d800552c46a3490580c0c2061ca041bc31108e21f0e02", size = 39031, upload-time = "2026-09-15T16:28:02.673Z" }, ] [package.optional-dependencies] @@ -1205,30 +1325,42 @@ cogeo = [ [[package]] name = "titiler-mosaic" -version = "0.24.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cogeo-mosaic" }, { name = "titiler-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/f0/52a3771b605c3889459e08c3d279e6ed76c86bee5c2f1d91bfa6c2395497/titiler_mosaic-0.24.0.tar.gz", hash = "sha256:6b029e2f0917e896ce773972e0d72a31d0a684d35356157c242883b9b548a240", size = 10831, upload-time = "2025-09-23T15:17:19.169Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/c7/5b93629d2018cde44f5bb011c1bcc58b81f0ddfbfa64b2d541cdadae3749/titiler_mosaic-2.3.0.tar.gz", hash = "sha256:8159a22a6a39bef6dc3b5a4af293092dc29ff6a3f7dd8eda8e0ffd794f8a91bf", size = 15462, upload-time = "2026-09-15T16:28:09.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/8a/c7c18ff2bb8640087c751bc87c762ad449a8883614937a3aafb63f122613/titiler_mosaic-0.24.0-py3-none-any.whl", hash = "sha256:5f647d3adf20264554550b92179876d6981b7e5aa7db4ff0a5dc6f59a75c1462", size = 11578, upload-time = "2025-09-23T15:17:18.015Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/53a1871d33d3ba9e355b6bcee9ba5b988c620e848db88c5477b5a433d7ca/titiler_mosaic-2.3.0-py3-none-any.whl", hash = "sha256:de44b5b70e09b5dff9a85222b65e26ac7d0f8d5ad30855a0f82500d6d09b3104", size = 18119, upload-time = "2026-09-15T16:28:03.957Z" }, ] [[package]] name = "titiler-pgstac" -version = "1.9.0" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cql2" }, { name = "pydantic" }, { name = "pydantic-settings" }, + { name = "stac-pydantic" }, + { name = "starlette-cramjam" }, { name = "titiler-core" }, + { name = "titiler-extensions" }, { name = "titiler-mosaic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/bd9aa5b6f7e79f00d30c3a1ba8238786d7ecc70c49e850217cbdff1244e2/titiler_pgstac-1.9.0.tar.gz", hash = "sha256:90e055dca09a255b128867e8a8795f9f58fcdc4258856b167a4275c54caf6452", size = 41750, upload-time = "2025-09-23T23:44:24.333Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/c7/153d96d13276f24ed639913576bbafad2a739311068ac6c48af17a515aa1/titiler_pgstac-3.2.0.tar.gz", hash = "sha256:6b4105e3fac1702d6f39a6ab44b773b78aeba110c101f4745465f0cf5aead582", size = 38452, upload-time = "2026-09-16T06:37:28.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/21/58d5339f9be77f29e4c07bf541f3d2f88e53c627a9b2136a196b7cd5fa66/titiler_pgstac-3.2.0-py3-none-any.whl", hash = "sha256:abe173539100f9699fcc8d9b6dd874d924a1a61fb1c34d305259a0570a7c4564", size = 45353, upload-time = "2026-09-16T06:37:27.118Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/ed/35694de7c40865663231449006f13260fd7a0bc4745672bae1c97455ed55/titiler_pgstac-1.9.0-py3-none-any.whl", hash = "sha256:527cd78978dc63eaa77567a1e4e1165b1141c524b3c9729922747352abb25199", size = 46589, upload-time = "2025-09-23T23:44:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] [[package]] diff --git a/docker-compose.yml b/docker-compose.yml index 636d6da..2069a4f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,11 +63,11 @@ services: environment: PYTHONUNBUFFERED: "1" PYTHONPATH: /asset - POSTGRES_USER: username - POSTGRES_PASS: password - POSTGRES_DBNAME: postgis - POSTGRES_HOST: database - POSTGRES_PORT: "5432" + PGUSER: username + PGPASSWORD: password + PGDATABASE: postgis + PGHOST: database + PGPORT: "5432" DB_MIN_CONN_SIZE: "1" DB_MAX_CONN_SIZE: "10" CPL_TMPDIR: /tmp From 93a7eb431cb23f8eb8876eecb364e1d99d3974bc Mon Sep 17 00:00:00 2001 From: hrodmn Date: Mon, 28 Sep 2026 10:36:52 -0500 Subject: [PATCH 2/5] Fix legacy TiTiler route redirects --- cdk/runtimes/eoapi/raster/README.md | 2 +- .../eoapi/raster/eoapi/raster/factory.py | 123 ++++++++++++++---- .../eoapi/raster/tests/test_runtime.py | 75 +++++++++-- 3 files changed, 166 insertions(+), 34 deletions(-) diff --git a/cdk/runtimes/eoapi/raster/README.md b/cdk/runtimes/eoapi/raster/README.md index 2d3fd67..f14e8f1 100644 --- a/cdk/runtimes/eoapi/raster/README.md +++ b/cdk/runtimes/eoapi/raster/README.md @@ -10,7 +10,7 @@ pgSTAC tile endpoints now use an explicit tile matrix set. For example: - `/searches/{search_id}/WebMercatorQuad/tilejson.json` - `/searches/{search_id}/WebMercatorQuad/map.html` -The same route families are available under `/collections/{collection_id}`. Legacy no-TMS tile, TileJSON, and map-viewer paths redirect with HTTP 307 to their `WebMercatorQuad` equivalents. The old `/mosaic` prefix still redirects other routes to `/searches`; `/mosaic/register` and `/mosaic/list` map to `/searches/register` and `/searches/` respectively. Info, point, WMTS, registration, statistics, and tileset-list routes do not get a tile matrix segment injected. +The same route families are available under `/collections/{collection_id}`. Legacy no-TMS tile, TileJSON, and map-viewer paths redirect with HTTP 307 to their `WebMercatorQuad` equivalents. Legacy `@{scale}x` tiles become unscaled tile paths with `tilesize=256*scale`; an explicit `tilesize` wins. Legacy TileJSON `tile_scale` becomes `tilesize` (including the old default scale of 1). TMS-qualified WMTS capability paths redirect to the native unqualified endpoint, which advertises all configured TMSs; the released WMTS extension has no parameter to limit the document to one TMS. The old `/mosaic` prefix still redirects other routes to `/searches`; `/mosaic/register` and `/mosaic/list` map to `/searches/register` and `/searches/` respectively. Info, point, WMTS, registration, statistics, and tileset-list routes do not get a tile matrix segment injected. The custom `/mosaics` creation and resource API remains available. Its native tile routes are TMS-qualified, for example `/mosaics/{mosaic_id}/tiles/WebMercatorQuad/{z}/{x}/{y}`. diff --git a/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py b/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py index c5d4b48..02abb02 100644 --- a/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py +++ b/cdk/runtimes/eoapi/raster/eoapi/raster/factory.py @@ -7,6 +7,7 @@ import uuid from functools import partial from typing import Annotated +from urllib.parse import unquote_plus from cogeo_mosaic.backends import DynamoDBBackend, MosaicBackend from cogeo_mosaic.errors import MosaicError, MosaicExistsError @@ -390,9 +391,26 @@ async def post_mosaics( return mk_mosaic_entity(mosaic_id, self_uri) -def _redirect(request: Request, path: str) -> RedirectResponse: - """Redirect while preserving the original query string and method.""" - query = request.scope["query_string"].decode("latin-1") +def _redirect( + request: Request, + path: str, + *, + tilesize: int | None = None, + remove_tile_scale: bool = False, +) -> RedirectResponse: + """Redirect while preserving unrelated query parameters and the method.""" + parts = request.scope["query_string"].decode("latin-1").split("&") + keys = [unquote_plus(part.partition("=")[0]) for part in parts if part] + has_tilesize = "tilesize" in keys + if remove_tile_scale: + parts = [ + part + for part in parts + if unquote_plus(part.partition("=")[0]) != "tile_scale" + ] + if tilesize is not None and not has_tilesize: + parts.append(f"tilesize={tilesize}") + query = "&".join(part for part in parts if part) return RedirectResponse(f"{path}?{query}" if query else path, status_code=307) @@ -400,36 +418,61 @@ def redirect_collection_compatibility(request: Request) -> RedirectResponse | No """Redirect only legacy collection paths missing their tile matrix set.""" path = request.url.path if match := re.fullmatch( - r"/collections/([^/]+)/tiles/(\d+)/(\d+)/(\d+(?:@\d+x)?(?:\.[\w-]+)?)(/assets)?", + r"/collections/([^/]+)/tiles/(\d+)/(\d+)/(\d+)(?:@(\d+)x)?(\.[\w-]+)?(/assets)?", path, ): - collection_id, z, x, y, assets = match.groups() - target = f"/collections/{collection_id}/tiles/WebMercatorQuad/{z}/{x}/{y}" - return _redirect(request, target + (assets or "")) + collection_id, z, x, y, scale, extension, assets = match.groups() + target = ( + f"/collections/{collection_id}/tiles/WebMercatorQuad/" + f"{z}/{x}/{y}{extension or ''}" + ) + tilesize = 256 * int(scale) if scale and not assets else None + return _redirect(request, target + (assets or ""), tilesize=tilesize) + + if match := re.fullmatch( + r"/collections/([^/]+)/([^/]+)/WMTSCapabilities\.xml", path + ): + collection_id, _ = match.groups() + return _redirect(request, f"/collections/{collection_id}/WMTSCapabilities.xml") if match := re.fullmatch(r"/collections/([^/]+)/(tilejson\.json|map\.html)", path): collection_id, endpoint = match.groups() + if endpoint == "tilejson.json": + try: + tile_scale = int(request.query_params.get("tile_scale", "1")) + except ValueError as e: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) from e + if not 0 < tile_scale < 4: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) + return _redirect( + request, + f"/collections/{collection_id}/WebMercatorQuad/{endpoint}", + tilesize=256 * tile_scale, + remove_tile_scale=True, + ) return _redirect( request, f"/collections/{collection_id}/WebMercatorQuad/{endpoint}" ) return None -def _redirect_search_id(search_id: str | None, _collection_id: str | None) -> str: +def _redirect_search_id( + search_id: str | None, _collection_id: str | None = None +) -> str: """Return the native pgSTAC search path for a legacy route.""" return f"/searches/{search_id}" @router.api_route( - "/mosaic/{search_id}/tiles/{z}/{x}/{y}", + "/mosaic/{search_id}/tiles/{z:int}/{x:int}/{y:int}", methods=["GET", "HEAD"], include_in_schema=False, ) async def redirect_tiles( request: Request, - z: str, - x: str, - y: str, + z: int, + x: int, + y: int, search_id: str | None = None, collection_id: str | None = None, ): @@ -439,15 +482,15 @@ async def redirect_tiles( @router.api_route( - "/mosaic/{search_id}/tiles/{z}/{x}/{y}.{format}", + "/mosaic/{search_id}/tiles/{z:int}/{x:int}/{y:int}.{format}", methods=["GET", "HEAD"], include_in_schema=False, ) async def redirect_tiles_format( request: Request, - z: str, - x: str, - y: str, + z: int, + x: int, + y: int, format: str, search_id: str | None = None, collection_id: str | None = None, @@ -467,13 +510,17 @@ async def redirect_tiles_scale( z: str, x: str, y: str, - scale: str, + scale: int, search_id: str | None = None, collection_id: str | None = None, ): - """Redirect legacy scaled tile paths to native WebMercatorQuad routes.""" + """Redirect scaled paths to native tilesize query parameters.""" target = _redirect_search_id(search_id, collection_id) - return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}@{scale}x") + return _redirect( + request, + f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}", + tilesize=256 * scale, + ) @router.api_route( @@ -486,15 +533,17 @@ async def redirect_tiles_scale_format( z: str, x: str, y: str, - scale: str, + scale: int, format: str, search_id: str | None = None, collection_id: str | None = None, ): - """Redirect legacy scaled formatted tiles to native WebMercatorQuad routes.""" + """Redirect scaled formatted paths to native tilesize query parameters.""" target = _redirect_search_id(search_id, collection_id) return _redirect( - request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}@{scale}x.{format}" + request, + f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}.{format}", + tilesize=256 * scale, ) @@ -516,6 +565,21 @@ async def redirect_tile_assets( return _redirect(request, f"{target}/tiles/WebMercatorQuad/{z}/{x}/{y}/assets") +@router.api_route( + "/mosaic/{search_id}/{tile_matrix_set_id}/WMTSCapabilities.xml", + methods=["GET", "HEAD"], + include_in_schema=False, +) +async def redirect_wmts( + request: Request, + search_id: str, + tile_matrix_set_id: str, +): + """Redirect the legacy TMS-specific WMTS path to the native endpoint.""" + target = _redirect_search_id(search_id) + return _redirect(request, f"{target}/WMTSCapabilities.xml") + + @router.api_route( "/mosaic/{search_id}/tilejson.json", methods=["GET", "HEAD"], @@ -526,9 +590,20 @@ async def redirect_tilejson( search_id: str | None = None, collection_id: str | None = None, ): - """Redirect legacy TileJSON paths to native WebMercatorQuad routes.""" + """Redirect legacy TileJSON scale options to native tilesize parameters.""" + try: + tile_scale = int(request.query_params.get("tile_scale", "1")) + except ValueError as e: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) from e + if not 0 < tile_scale < 4: + raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY) target = _redirect_search_id(search_id, collection_id) - return _redirect(request, f"{target}/WebMercatorQuad/tilejson.json") + return _redirect( + request, + f"{target}/WebMercatorQuad/tilejson.json", + tilesize=256 * tile_scale, + remove_tile_scale=True, + ) @router.api_route( diff --git a/cdk/runtimes/eoapi/raster/tests/test_runtime.py b/cdk/runtimes/eoapi/raster/tests/test_runtime.py index e675d34..5e96453 100644 --- a/cdk/runtimes/eoapi/raster/tests/test_runtime.py +++ b/cdk/runtimes/eoapi/raster/tests/test_runtime.py @@ -32,17 +32,27 @@ ( "GET", "/mosaic/search-1/tiles/1/2/3@2x.webp", - "/searches/search-1/tiles/WebMercatorQuad/1/2/3@2x.webp", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3.webp?tilesize=512", ), ( "GET", - "/mosaic/search-1/tiles/1/2/3/assets", - "/searches/search-1/tiles/WebMercatorQuad/1/2/3/assets", + "/mosaic/search-1/tiles/1/2/3@3x.png?tilesize=640&other=a%2Fb", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3.png?tilesize=640&other=a%2Fb", ), ( "GET", "/mosaic/search-1/tilejson.json", - "/searches/search-1/WebMercatorQuad/tilejson.json", + "/searches/search-1/WebMercatorQuad/tilejson.json?tilesize=256", + ), + ( + "GET", + "/mosaic/search-1/tilejson.json?tile_scale=3&tilesize=768&other=a%2Fb", + "/searches/search-1/WebMercatorQuad/tilejson.json?tilesize=768&other=a%2Fb", + ), + ( + "GET", + "/mosaic/search-1/tiles/1/2/3/assets", + "/searches/search-1/tiles/WebMercatorQuad/1/2/3/assets", ), ( "GET", @@ -57,7 +67,7 @@ ( "GET", "/collections/c-1/tiles/1/2/3@2x.webp", - "/collections/c-1/tiles/WebMercatorQuad/1/2/3@2x.webp", + "/collections/c-1/tiles/WebMercatorQuad/1/2/3.webp?tilesize=512", ), ( "GET", @@ -66,8 +76,8 @@ ), ( "GET", - "/collections/c-1/tilejson.json", - "/collections/c-1/WebMercatorQuad/tilejson.json", + "/collections/c-1/tilejson.json?tile_scale=2&token=a%2Fb", + "/collections/c-1/WebMercatorQuad/tilejson.json?token=a%2Fb&tilesize=512", ), ( "GET", @@ -79,10 +89,15 @@ ("GET", "/mosaic/search-1/info", "/searches/search-1/info"), ( "GET", - "/mosaic/search-1/WMTSCapabilities.xml", - "/searches/search-1/WMTSCapabilities.xml", + "/mosaic/search-1/WebMercatorQuad/WMTSCapabilities.xml?x=a%2Fb", + "/searches/search-1/WMTSCapabilities.xml?x=a%2Fb", ), ("GET", "/mosaic/search-1/statistics", "/searches/search-1/statistics"), + ( + "GET", + "/collections/c-1/WebMercatorQuad/WMTSCapabilities.xml", + "/collections/c-1/WMTSCapabilities.xml", + ), ], ) def test_legacy_redirects_preserve_methods_and_queries(method, path, location): @@ -93,6 +108,48 @@ def test_legacy_redirects_preserve_methods_and_queries(method, path, location): assert response.headers["location"] == location +@pytest.mark.parametrize( + ("legacy_path", "native_path", "expected_status"), + [ + ( + "/mosaic/s-1/tiles/1/2/3@2x.png", + "/searches/s-1/tiles/WebMercatorQuad/1/2/3.png", + None, + ), + ( + "/mosaic/s-1/WebMercatorQuad/WMTSCapabilities.xml", + "/searches/s-1/WMTSCapabilities.xml", + None, + ), + ( + "/collections/c-1/WebMercatorQuad/WMTSCapabilities.xml", + "/collections/c-1/WMTSCapabilities.xml", + None, + ), + ( + "/collections/c-1/map.html", + "/collections/c-1/WebMercatorQuad/map.html", + None, + ), + ], +) +def test_legacy_redirects_resolve_through_native_routes( + legacy_path, native_path, expected_status +): + """Redirects reach registered 2.3 routes, not merely plausible URLs.""" + response = TestClient(app, raise_server_exceptions=False).get( + legacy_path, follow_redirects=True + ) + + assert response.history[0].status_code == 307 + assert response.url.path == native_path + if expected_status is None: + # pgSTAC needs a database; a 404 would indicate a bad native route. + assert response.status_code != 404 + else: + assert response.status_code == expected_status + + def test_canonical_collection_paths_are_not_compatibility_redirects(): """Native paths and unrelated collections endpoints never get rewritten.""" client = TestClient(app, raise_server_exceptions=False) From ec082141ad79355de9b98ebebf8e89499fb4d2b3 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Mon, 28 Sep 2026 10:43:25 -0500 Subject: [PATCH 3/5] Clarify raster route compatibility scope --- cdk/runtimes/eoapi/raster/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdk/runtimes/eoapi/raster/README.md b/cdk/runtimes/eoapi/raster/README.md index f14e8f1..9a04f37 100644 --- a/cdk/runtimes/eoapi/raster/README.md +++ b/cdk/runtimes/eoapi/raster/README.md @@ -10,9 +10,9 @@ pgSTAC tile endpoints now use an explicit tile matrix set. For example: - `/searches/{search_id}/WebMercatorQuad/tilejson.json` - `/searches/{search_id}/WebMercatorQuad/map.html` -The same route families are available under `/collections/{collection_id}`. Legacy no-TMS tile, TileJSON, and map-viewer paths redirect with HTTP 307 to their `WebMercatorQuad` equivalents. Legacy `@{scale}x` tiles become unscaled tile paths with `tilesize=256*scale`; an explicit `tilesize` wins. Legacy TileJSON `tile_scale` becomes `tilesize` (including the old default scale of 1). TMS-qualified WMTS capability paths redirect to the native unqualified endpoint, which advertises all configured TMSs; the released WMTS extension has no parameter to limit the document to one TMS. The old `/mosaic` prefix still redirects other routes to `/searches`; `/mosaic/register` and `/mosaic/list` map to `/searches/register` and `/searches/` respectively. Info, point, WMTS, registration, statistics, and tileset-list routes do not get a tile matrix segment injected. +The same route families are available under `/collections/{collection_id}`. Compatibility redirects are intentionally limited to the documented missing-TMS `/mosaic` and collection tile, TileJSON, and map-viewer aliases, plus the implemented TMS-qualified WMTS capability aliases and the existing `/mosaic`-to-`/searches` prefix remapping. These redirects use HTTP 307. Legacy `@{scale}x` tiles become unscaled tile paths with `tilesize=256*scale`; an explicit `tilesize` wins. Legacy TileJSON `tile_scale` becomes `tilesize` (including the old default scale of 1). TMS-qualified WMTS capability paths redirect to the native unqualified endpoint, which advertises all configured TMSs; the released WMTS extension has no parameter to limit the document to one TMS. This is not full compatibility for every old `/searches` endpoint or TMS-qualified scaling path or query. -The custom `/mosaics` creation and resource API remains available. Its native tile routes are TMS-qualified, for example `/mosaics/{mosaic_id}/tiles/WebMercatorQuad/{z}/{x}/{y}`. +The custom `/mosaics` creation and resource API remains available, but plural `/mosaics` URLs are not compatibility-redirected. Its native tile routes are TMS-qualified, for example `/mosaics/{mosaic_id}/tiles/WebMercatorQuad/{z}/{x}/{y}`. For local Compose, raster database settings use the TiTiler pgSTAC `PGUSER`, `PGPASSWORD`, `PGDATABASE`, `PGHOST`, and `PGPORT` variables. Other services retain their own environment names. From 53c2cb59cd609e0877df33b21b705068a7c69e20 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 29 Sep 2026 05:31:38 -0500 Subject: [PATCH 4/5] chore(deps): allow a range of versions for titiler-pgstac --- cdk/runtimes/eoapi/raster/pyproject.toml | 4 ++-- cdk/runtimes/eoapi/raster/uv.lock | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cdk/runtimes/eoapi/raster/pyproject.toml b/cdk/runtimes/eoapi/raster/pyproject.toml index 8eb0686..f374d0e 100644 --- a/cdk/runtimes/eoapi/raster/pyproject.toml +++ b/cdk/runtimes/eoapi/raster/pyproject.toml @@ -16,8 +16,8 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "titiler-pgstac==3.2.0", - "titiler-extensions[cogeo]==2.3.0", + "titiler-pgstac>=3.2.0,<4", + "titiler-extensions[cogeo]>=2.3.0,<3", "pystac-client==0.9.0", "stac-pydantic>=3.1.3", "numpy>=2.3.3", diff --git a/cdk/runtimes/eoapi/raster/uv.lock b/cdk/runtimes/eoapi/raster/uv.lock index 3557caf..dc10518 100644 --- a/cdk/runtimes/eoapi/raster/uv.lock +++ b/cdk/runtimes/eoapi/raster/uv.lock @@ -336,9 +336,9 @@ requires-dist = [ { name = "pystac-client", specifier = "==0.9.0" }, { name = "stac-pydantic", specifier = ">=3.1.3" }, { name = "titiler-core", specifier = "==2.3.0" }, - { name = "titiler-extensions", extras = ["cogeo"], specifier = "==2.3.0" }, + { name = "titiler-extensions", extras = ["cogeo"], specifier = ">=2.3.0,<3" }, { name = "titiler-mosaic", specifier = "==2.3.0" }, - { name = "titiler-pgstac", specifier = "==3.2.0" }, + { name = "titiler-pgstac", specifier = ">=3.2.0,<4" }, ] provides-extras = ["lambda", "psycopg", "psycopg-binary", "psycopg-c"] From 183b8d8f865b9efe9203b847fdfec58472d6a365 Mon Sep 17 00:00:00 2001 From: hrodmn Date: Tue, 29 Sep 2026 05:31:55 -0500 Subject: [PATCH 5/5] chore: add dependabot entries for eoapi runtimes --- .github/dependabot.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 54b6b12..10db7d6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,6 +22,28 @@ updates: cooldown: default-days: 30 + - package-ecosystem: uv + directory: /cdk/runtimes/eoapi/raster + schedule: + interval: monthly + groups: + python-dependencies: + patterns: + - "*" + cooldown: + default-days: 30 + + - package-ecosystem: uv + directory: /cdk/runtimes/eoapi/stac + schedule: + interval: monthly + groups: + python-dependencies: + patterns: + - "*" + cooldown: + default-days: 30 + - package-ecosystem: pre-commit directory: / schedule: