diff --git a/pccommon/pccommon/middleware.py b/pccommon/pccommon/middleware.py index b91a3c10..f29fc840 100644 --- a/pccommon/pccommon/middleware.py +++ b/pccommon/pccommon/middleware.py @@ -84,6 +84,8 @@ def add_timeout(app: FastAPI, timeout_seconds: float) -> None: depends=depends, path=route.path_format ), ) + # TODO: `get_body_field` was updated after fastapi==0.112.4 + # https://github.com/fastapi/fastapi/blob/999eeb6c76ff37f94612dd140ce8091932f56c54/fastapi/dependencies/utils.py#L830-L832 # noqa: E501 route.body_field = get_body_field( dependant=route.dependant, name=route.unique_id ) diff --git a/pccommon/pccommon/redis.py b/pccommon/pccommon/redis.py index d166af59..01ac6680 100644 --- a/pccommon/pccommon/redis.py +++ b/pccommon/pccommon/redis.py @@ -234,7 +234,7 @@ def rate_limit( """ def _decorator( - fn: Callable[..., Coroutine[Any, Any, T]] + fn: Callable[..., Coroutine[Any, Any, T]], ) -> Callable[..., Coroutine[Any, Any, T]]: async def _wrapper(*args: Any, **kwargs: Any) -> T: request: Optional[Request] = kwargs.get("request") @@ -320,7 +320,7 @@ def back_pressure( """ def _decorator( - fn: Callable[..., Coroutine[Any, Any, T]] + fn: Callable[..., Coroutine[Any, Any, T]], ) -> Callable[..., Coroutine[Any, Any, T]]: async def _wrapper(*args: Any, **kwargs: Any) -> T: request: Optional[Request] = kwargs.get("request") diff --git a/pccommon/pyproject.toml b/pccommon/pyproject.toml index ef2c2a02..a9e0a0d8 100644 --- a/pccommon/pyproject.toml +++ b/pccommon/pyproject.toml @@ -13,7 +13,7 @@ dependencies = [ "azure-identity>=1.16.1", "azure-storage-blob>=12.20.0", "cachetools~=5.3", - "fastapi-slim>=0.111.0", + "fastapi==0.112.3", "html-sanitizer>=2.4.4", "idna>=3.7.0", "lxml_html_clean>=0.1.0", diff --git a/pccommon/requirements.txt b/pccommon/requirements.txt index 0584ae14..99dd5ef1 100644 --- a/pccommon/requirements.txt +++ b/pccommon/requirements.txt @@ -6,79 +6,79 @@ # annotated-types==0.7.0 # via pydantic -anyio==4.4.0 +anyio==4.9.0 # via starlette -async-timeout==4.0.3 +async-timeout==5.0.1 # via redis -azure-core==1.30.2 +azure-core==1.34.0 # via # azure-data-tables # azure-identity # azure-storage-blob # opencensus-ext-azure -azure-data-tables==12.5.0 +azure-data-tables==12.7.0 # via pccommon (pccommon/pyproject.toml) -azure-identity==1.16.1 +azure-identity==1.23.0 # via # opencensus-ext-azure # pccommon (pccommon/pyproject.toml) -azure-storage-blob==12.20.0 +azure-storage-blob==12.25.1 # via pccommon (pccommon/pyproject.toml) -beautifulsoup4==4.12.3 +beautifulsoup4==4.13.4 # via html-sanitizer -cachetools==5.3.3 +cachetools==5.5.2 # via # google-auth # pccommon (pccommon/pyproject.toml) -certifi==2024.7.4 +certifi==2025.6.15 # via requests -cffi==1.16.0 +cffi==1.17.1 # via cryptography -charset-normalizer==3.3.2 +charset-normalizer==3.4.2 # via requests -cryptography==42.0.8 +cryptography==45.0.4 # via # azure-identity # azure-storage-blob # msal # pyjwt -exceptiongroup==1.2.1 +exceptiongroup==1.3.0 # via anyio -fastapi-slim==0.111.0 +fastapi==0.112.3 # via pccommon (pccommon/pyproject.toml) -google-api-core==2.19.0 +google-api-core==2.25.1 # via opencensus -google-auth==2.30.0 +google-auth==2.40.3 # via google-api-core -googleapis-common-protos==1.63.1 +googleapis-common-protos==1.70.0 # via google-api-core -html-sanitizer==2.4.4 +html-sanitizer==2.5.0 # via pccommon (pccommon/pyproject.toml) -idna==3.7 +idna==3.10 # via # anyio # pccommon (pccommon/pyproject.toml) # requests # yarl -isodate==0.6.1 +isodate==0.7.2 # via # azure-data-tables # azure-storage-blob -lxml==5.2.2 +lxml==5.4.0 # via # html-sanitizer # lxml-html-clean -lxml-html-clean==0.1.0 +lxml-html-clean==0.4.2 # via # html-sanitizer # pccommon (pccommon/pyproject.toml) -msal==1.28.1 +msal==1.32.3 # via # azure-identity # msal-extensions -msal-extensions==1.1.0 +msal-extensions==1.3.1 # via azure-identity -multidict==6.0.5 +multidict==6.5.0 # via yarl opencensus==0.11.4 # via @@ -86,87 +86,93 @@ opencensus==0.11.4 # opencensus-ext-logging opencensus-context==0.1.3 # via opencensus -opencensus-ext-azure==1.1.13 +opencensus-ext-azure==1.1.15 # via pccommon (pccommon/pyproject.toml) opencensus-ext-logging==0.1.1 # via pccommon (pccommon/pyproject.toml) -orjson==3.10.5 +orjson==3.10.18 # via pccommon (pccommon/pyproject.toml) -packaging==24.1 - # via msal-extensions -portalocker==2.8.2 - # via msal-extensions -proto-plus==1.23.0 +propcache==0.3.2 + # via yarl +proto-plus==1.26.1 # via google-api-core -protobuf==4.25.3 +protobuf==6.31.1 # via # google-api-core # googleapis-common-protos # proto-plus -psutil==5.9.8 +psutil==7.0.0 # via opencensus-ext-azure -pyasn1==0.6.0 +pyasn1==0.6.1 # via # pyasn1-modules # rsa -pyasn1-modules==0.4.0 +pyasn1-modules==0.4.2 # via google-auth pycparser==2.22 # via cffi -pydantic==2.7.4 +pydantic==2.11.7 # via - # fastapi-slim + # fastapi # pccommon (pccommon/pyproject.toml) # pydantic-settings -pydantic-core==2.18.4 +pydantic-core==2.33.2 # via pydantic -pydantic-settings==2.3.3 +pydantic-settings==2.9.1 # via pccommon (pccommon/pyproject.toml) -pyhumps==3.5.3 +pyhumps==3.8.0 # via pccommon (pccommon/pyproject.toml) -pyjwt[crypto]==2.8.0 +pyjwt[crypto]==2.10.1 # via msal -python-dotenv==1.0.1 +python-dotenv==1.1.0 # via pydantic-settings -redis==4.6.0 +redis==6.2.0 # via pccommon (pccommon/pyproject.toml) -requests==2.32.3 +requests==2.32.4 # via # azure-core # google-api-core # msal # opencensus-ext-azure # pccommon (pccommon/pyproject.toml) -rsa==4.9 +rsa==4.9.1 # via google-auth -six==1.16.0 +six==1.17.0 # via # azure-core - # isodate # opencensus sniffio==1.3.1 # via anyio -soupsieve==2.5 +soupsieve==2.7 # via beautifulsoup4 -starlette==0.37.2 +starlette==0.38.6 # via - # fastapi-slim + # fastapi # pccommon (pccommon/pyproject.toml) -types-cachetools==4.2.9 +types-cachetools==6.0.0.20250525 # via pccommon (pccommon/pyproject.toml) -typing-extensions==4.12.2 +typing-extensions==4.14.0 # via # anyio # azure-core # azure-data-tables + # azure-identity # azure-storage-blob - # fastapi-slim + # beautifulsoup4 + # exceptiongroup + # fastapi + # multidict # pydantic # pydantic-core # starlette -urllib3==2.2.2 + # typing-inspection +typing-inspection==0.4.1 + # via + # pydantic + # pydantic-settings +urllib3==2.4.0 # via # pccommon (pccommon/pyproject.toml) # requests -yarl==1.9.4 +yarl==1.20.1 # via azure-data-tables diff --git a/pcfuncs/tests/ipban/test_ipban.py b/pcfuncs/tests/ipban/test_ipban.py index f5c69f8c..6a17e0d6 100644 --- a/pcfuncs/tests/ipban/test_ipban.py +++ b/pcfuncs/tests/ipban/test_ipban.py @@ -107,7 +107,7 @@ def integration_clients( @pytest.mark.integration def test_update_banned_ip_integration( - integration_clients: Tuple[LogsQueryClient, TableClient] + integration_clients: Tuple[LogsQueryClient, TableClient], ) -> None: logger.info(f"Test id: {TEST_ID} - integration test is running") logs_query_client, table_client = integration_clients diff --git a/pctiler/pctiler/endpoints/item.py b/pctiler/pctiler/endpoints/item.py index 9af89b8a..7fd27bc5 100644 --- a/pctiler/pctiler/endpoints/item.py +++ b/pctiler/pctiler/endpoints/item.py @@ -4,14 +4,17 @@ from urllib.parse import quote_plus, urljoin import fastapi +import jinja2 import pystac -from fastapi import Body, Depends, Query, Request, Response +from fastapi import Body, Depends, Path, Query, Request, Response from fastapi.templating import Jinja2Templates from geojson_pydantic.features import Feature from html_sanitizer.sanitizer import Sanitizer +from pydantic import Field from starlette.responses import HTMLResponse from titiler.core.dependencies import CoordCRSParams, DstCRSParams from titiler.core.factory import MultiBaseTilerFactory, img_endpoint_params +from titiler.core.models.mapbox import TileJSON from titiler.core.resources.enums import ImageType from titiler.pgstac.dependencies import get_stac_item @@ -22,12 +25,6 @@ from pctiler.endpoints.dependencies import get_endpoint_function from pctiler.reader import ItemSTACReader, ReaderParams -try: - from importlib.resources import files as resources_files # type: ignore -except ImportError: - # Try backported to PY<39 `importlib_resources`. - from importlib_resources import files as resources_files # type: ignore - logger = logging.getLogger(__name__) @@ -79,11 +76,10 @@ async def _fetch() -> dict: return pystac.Item.from_dict(_item) -# TODO: mypy fails in python 3.9, we need to find a proper way to do this -templates = Jinja2Templates( - directory=str(resources_files(__package__) / "templates") # type: ignore +jinja2_env = jinja2.Environment( + loader=jinja2.ChoiceLoader([jinja2.PackageLoader(__package__, "templates")]) ) - +templates = Jinja2Templates(env=jinja2_env) pc_tile_factory = MultiBaseTilerFactory( reader=ItemSTACReader, @@ -136,16 +132,23 @@ def map( ) +prefix = pc_tile_factory.operation_prefix + + +# crop/feature endpoint compat with titiler<0.15 (`/crop` was renamed `/feature`) @pc_tile_factory.router.post( r"/crop", + operation_id=f"{prefix}postDataForGeoJSONCrop", **img_endpoint_params, ) @pc_tile_factory.router.post( r"/crop.{format}", + operation_id=f"{prefix}postDataForGeoJSONWithFormatCrop", **img_endpoint_params, ) @pc_tile_factory.router.post( r"/crop/{width}x{height}.{format}", + operation_id=f"{prefix}postDataForGeoJSONWithSizesAndFormatCrop", **img_endpoint_params, ) def geojson_crop( # type: ignore @@ -155,7 +158,9 @@ def geojson_crop( # type: ignore ], format: Annotated[ ImageType, - "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", # noqa: E501,F722 + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." # noqa: F722,E501 + ), ] = None, # type: ignore[assignment] src_path=Depends(pc_tile_factory.path_dependency), coord_crs=Depends(CoordCRSParams), @@ -164,14 +169,6 @@ def geojson_crop( # type: ignore dataset_params=Depends(pc_tile_factory.dataset_dependency), image_params=Depends(pc_tile_factory.img_part_dependency), post_process=Depends(pc_tile_factory.process_dependency), - rescale=Depends(pc_tile_factory.rescale_dependency), - color_formula: Annotated[ - Optional[str], - Query( - title="Color Formula", # noqa: F722 - description="rio-color formula (info: https://github.com/mapbox/rio-color)", # noqa: E501,F722 - ), - ] = None, colormap=Depends(pc_tile_factory.colormap_dependency), render_params=Depends(pc_tile_factory.render_dependency), reader_params=Depends(pc_tile_factory.reader_dependency), @@ -191,11 +188,167 @@ def geojson_crop( # type: ignore dataset_params=dataset_params, image_params=image_params, post_process=post_process, - rescale=rescale, - color_formula=color_formula, colormap=colormap, render_params=render_params, reader_params=reader_params, env=env, ) return result + + +# /tiles endpoint compat with titiler<0.15, Optional `tileMatrixSetId` +@pc_tile_factory.router.get( + "/tiles/{z}/{x}/{y}", + operation_id=f"{prefix}getWebMercatorQuadTile", + **img_endpoint_params, +) +@pc_tile_factory.router.get( + "/tiles/{z}/{x}/{y}.{format}", + operation_id=f"{prefix}getWebMercatorQuadTileWithFormat", + **img_endpoint_params, +) +@pc_tile_factory.router.get( + "/tiles/{z}/{x}/{y}@{scale}x", + operation_id=f"{prefix}getWebMercatorQuadTileWithScale", + **img_endpoint_params, +) +@pc_tile_factory.router.get( + "/tiles/{z}/{x}/{y}@{scale}x.{format}", + operation_id=f"{prefix}getWebMercatorQuadTileWithFormatAndScale", + **img_endpoint_params, +) +def tile_compat( # type: ignore + request: fastapi.Request, + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", # noqa: F722,E501 + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", # noqa: F722,E501 + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", # noqa: F722,E501 + ), + ], + scale: Annotated[ + int, + Field( + gt=0, + le=4, + description="Tile size scale. 1=256x256, 2=512x512...", # noqa: F722,E501 + ), + ] = 1, + format: Annotated[ + Optional[ImageType], + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." # noqa: F722,E501 + ), + ] = None, + src_path=Depends(pc_tile_factory.path_dependency), + reader_params=Depends(pc_tile_factory.reader_dependency), + tile_params=Depends(pc_tile_factory.tile_dependency), + layer_params=Depends(pc_tile_factory.layer_dependency), + dataset_params=Depends(pc_tile_factory.dataset_dependency), + post_process=Depends(pc_tile_factory.process_dependency), + colormap=Depends(pc_tile_factory.colormap_dependency), + render_params=Depends(pc_tile_factory.render_dependency), + env=Depends(pc_tile_factory.environment_dependency), +) -> Response: + """tiles endpoints compat.""" + endpoint = get_endpoint_function( + pc_tile_factory.router, + path="/tiles/{tileMatrixSetId}/{z}/{x}/{y}", + method=request.method, + ) + result = endpoint( + z=z, + x=x, + y=y, + tileMatrixSetId="WebMercatorQuad", + scale=scale, + format=format, + src_path=src_path, + reader_params=reader_params, + tile_params=tile_params, + layer_params=layer_params, + dataset_params=dataset_params, + post_process=post_process, + colormap=colormap, + render_params=render_params, + env=env, + ) + return result + + +# /tilejson.json endpoint compat with titiler<0.15, Optional `tileMatrixSetId` +@pc_tile_factory.router.get( + "/tilejson.json", + response_model=TileJSON, + responses={200: {"description": "Return a tilejson"}}, + response_model_exclude_none=True, + operation_id=f"{pc_tile_factory.operation_prefix}getWebMercatorQuadTileJSON", +) +def tilejson_compat( # type: ignore + request: fastapi.Request, + tile_format: Annotated[ + Optional[ImageType], + Query( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." # noqa: F722,E501 + ), + ] = None, + tile_scale: Annotated[ + int, + Query( + gt=0, + lt=4, + description="Tile size scale. 1=256x256, 2=512x512...", # noqa: E501,F722 + ), + ] = 1, + minzoom: Annotated[ + Optional[int], + Query(description="Overwrite default minzoom."), # noqa: F722 + ] = None, + maxzoom: Annotated[ + Optional[int], + Query(description="Overwrite default maxzoom."), # noqa: F722 + ] = None, + src_path=Depends(pc_tile_factory.path_dependency), + reader_params=Depends(pc_tile_factory.reader_dependency), + tile_params=Depends(pc_tile_factory.tile_dependency), + layer_params=Depends(pc_tile_factory.layer_dependency), + dataset_params=Depends(pc_tile_factory.dataset_dependency), + post_process=Depends(pc_tile_factory.process_dependency), + colormap=Depends(pc_tile_factory.colormap_dependency), + render_params=Depends(pc_tile_factory.render_dependency), + env=Depends(pc_tile_factory.environment_dependency), +) -> Response: + """tilejson endpoint compat.""" + endpoint = get_endpoint_function( + pc_tile_factory.router, + path="/{tileMatrixSetId}/tilejson.json", + method=request.method, + ) + result = endpoint( + tileMatrixSetId="WebMercatorQuad", + tile_format=tile_format, + tile_scale=tile_scale, + minzoom=minzoom, + maxzoom=maxzoom, + src_path=src_path, + reader_params=reader_params, + tile_params=tile_params, + layer_params=layer_params, + dataset_params=dataset_params, + post_process=post_process, + colormap=colormap, + render_params=render_params, + env=env, + ) + return result diff --git a/pctiler/pctiler/endpoints/legend.py b/pctiler/pctiler/endpoints/legend.py index fed317e4..6c4959d8 100644 --- a/pctiler/pctiler/endpoints/legend.py +++ b/pctiler/pctiler/endpoints/legend.py @@ -70,7 +70,7 @@ async def get_classmap_legend( keys = list(classmap.keys()) # type: ignore trimmed_keys = keys[trim_start : len(keys) - trim_end] - trimmed_map = {k: classmap[k] for k in trimmed_keys} + trimmed_map = {k: classmap[k] for k in trimmed_keys} # type: ignore return JSONResponse(content=trimmed_map) @@ -155,7 +155,7 @@ def make_colormap(name: str, trim_start: int, length: int) -> ListedColormap: if len(cm) > 256 or max(cm) >= 256: raise Exception("Cannot make a colormap for discrete colormap") - colors = make_lut(cm) + colors = make_lut(cm) # type: ignore colors = colors[trim_start : length + 1] # rescale to 0-1 diff --git a/pctiler/pctiler/endpoints/pg_mosaic.py b/pctiler/pctiler/endpoints/pg_mosaic.py index f8e692fb..b0d5d44d 100644 --- a/pctiler/pctiler/endpoints/pg_mosaic.py +++ b/pctiler/pctiler/endpoints/pg_mosaic.py @@ -1,15 +1,14 @@ from dataclasses import dataclass, field from typing import Annotated, List, Literal, Optional -from fastapi import APIRouter, Depends, FastAPI, Query, Request, Response +from fastapi import APIRouter, Depends, FastAPI, Path, Query, Request, Response from fastapi.responses import ORJSONResponse from psycopg_pool import ConnectionPool from pydantic import Field from titiler.core import dependencies -from titiler.core.dependencies import ColorFormulaParams from titiler.core.factory import img_endpoint_params from titiler.core.resources.enums import ImageType -from titiler.pgstac.dependencies import SearchIdParams, TmsTileParams +from titiler.pgstac.dependencies import SearchIdParams from titiler.pgstac.extensions import searchInfoExtension from titiler.pgstac.factory import MosaicTilerFactory @@ -40,7 +39,7 @@ def __init__(self, request: Request): pgstac_mosaic_factory = MosaicTilerFactory( - reader=PGSTACBackend, + backend=PGSTACBackend, path_dependency=SearchIdParams, colormap_dependency=PCColorMapParams, layer_dependency=AssetsBidxExprParams, @@ -87,6 +86,8 @@ def mosaic_info( legacy_mosaic_router = APIRouter() +# Compat with titiler-pgstac<0.3.0, +# (`/tiles/{search_id}/...` was renamed `/{search_id}/tiles/...`) @legacy_mosaic_router.get("/tiles/{search_id}/{z}/{x}/{y}", **img_endpoint_params) @legacy_mosaic_router.get( "/tiles/{search_id}/{z}/{x}/{y}.{format}", **img_endpoint_params @@ -114,58 +115,75 @@ def mosaic_info( ) def tile_routes( # type: ignore request: Request, + z: Annotated[ + int, + Path( + description="Identifier (Z) selecting one of the scales defined in the TileMatrixSet and representing the scaleDenominator the tile.", # noqa: F722,E501 + ), + ], + x: Annotated[ + int, + Path( + description="Column (X) index of the tile on the selected TileMatrix. It cannot exceed the MatrixHeight-1 for the selected TileMatrix.", # noqa: F722,E501 + ), + ], + y: Annotated[ + int, + Path( + description="Row (Y) index of the tile on the selected TileMatrix. It cannot exceed the MatrixWidth-1 for the selected TileMatrix.", # noqa: F722,E501 + ), + ], search_id=Depends(pgstac_mosaic_factory.path_dependency), - tile=Depends(TmsTileParams), tileMatrixSetId: Annotated[ # type: ignore Literal[tuple(pgstac_mosaic_factory.supported_tms.list())], - f"Identifier selecting one of the TileMatrixSetId supported (default: '{pgstac_mosaic_factory.default_tms}')", # noqa: E501,F722 - ] = pgstac_mosaic_factory.default_tms, + "Identifier selecting one of the TileMatrixSetId supported (default: 'WebMercatorQuad')", # noqa: E501,F722 + ] = "WebMercatorQuad", scale: Annotated[ # type: ignore Optional[Annotated[int, Field(gt=0, le=4)]], "Tile size scale. 1=256x256, 2=512x512...", # noqa: E501,F722 ] = None, format: Annotated[ Optional[ImageType], - "Default will be automatically defined if the output image needs a mask (png) or not (jpeg).", # noqa: E501,F722 + Field( + description="Default will be automatically defined if the output image needs a mask (png) or not (jpeg)." # noqa: F722,E501 + ), ] = None, + backend_params=Depends(pgstac_mosaic_factory.backend_dependency), + reader_params=Depends(pgstac_mosaic_factory.reader_dependency), + assets_accessor_params=Depends(pgstac_mosaic_factory.assets_accessor_dependency), layer_params=Depends(pgstac_mosaic_factory.layer_dependency), dataset_params=Depends(pgstac_mosaic_factory.dataset_dependency), pixel_selection=Depends(pgstac_mosaic_factory.pixel_selection_dependency), tile_params=Depends(pgstac_mosaic_factory.tile_dependency), post_process=Depends(pgstac_mosaic_factory.process_dependency), - rescale=Depends(pgstac_mosaic_factory.rescale_dependency), - color_formula=Depends(ColorFormulaParams), colormap=Depends(pgstac_mosaic_factory.colormap_dependency), render_params=Depends(pgstac_mosaic_factory.render_dependency), - pgstac_params=Depends(pgstac_mosaic_factory.pgstac_dependency), - backend_params=Depends(pgstac_mosaic_factory.backend_dependency), - reader_params=Depends(pgstac_mosaic_factory.reader_dependency), env=Depends(pgstac_mosaic_factory.environment_dependency), ) -> Response: """Create map tile.""" endpoint = get_endpoint_function( pgstac_mosaic_factory.router, - path="/tiles/{z}/{x}/{y}", + path="/tiles/{tileMatrixSetId}/{z}/{x}/{y}", method=request.method, ) result = endpoint( search_id=search_id, - tile=tile, + z=z, + x=x, + y=y, tileMatrixSetId=tileMatrixSetId, scale=scale, format=format, - tile_params=tile_params, + backend_params=backend_params, + reader_params=reader_params, + assets_accessor_params=assets_accessor_params, layer_params=layer_params, dataset_params=dataset_params, pixel_selection=pixel_selection, + tile_params=tile_params, post_process=post_process, - rescale=rescale, - color_formula=color_formula, colormap=colormap, render_params=render_params, - pgstac_params=pgstac_params, - backend_params=backend_params, - reader_params=reader_params, env=env, ) return result diff --git a/pctiler/pctiler/main.py b/pctiler/pctiler/main.py index f82ff17d..d5f769c8 100755 --- a/pctiler/pctiler/main.py +++ b/pctiler/pctiler/main.py @@ -92,12 +92,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator: pg_mosaic.pgstac_mosaic_factory.dataset_dependency, pg_mosaic.pgstac_mosaic_factory.pixel_selection_dependency, pg_mosaic.pgstac_mosaic_factory.process_dependency, - pg_mosaic.pgstac_mosaic_factory.rescale_dependency, - pg_mosaic.pgstac_mosaic_factory.colormap_dependency, pg_mosaic.pgstac_mosaic_factory.render_dependency, + pg_mosaic.pgstac_mosaic_factory.assets_accessor_dependency, pg_mosaic.pgstac_mosaic_factory.reader_dependency, pg_mosaic.pgstac_mosaic_factory.backend_dependency, - pg_mosaic.pgstac_mosaic_factory.pgstac_dependency, ], tags=["PgSTAC Mosaic endpoints"], ) diff --git a/pctiler/pctiler/reader.py b/pctiler/pctiler/reader.py index 9313aad1..7858278f 100644 --- a/pctiler/pctiler/reader.py +++ b/pctiler/pctiler/reader.py @@ -1,5 +1,6 @@ import logging import time +import warnings from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Tuple, Type @@ -10,13 +11,14 @@ from fastapi import HTTPException from geojson_pydantic import Polygon from rio_tiler.errors import InvalidAssetName, MissingAssets, TileOutsideBounds +from rio_tiler.io.stac import STAC_ALTERNATE_KEY from rio_tiler.models import ImageData from rio_tiler.mosaic import mosaic_reader from rio_tiler.types import AssetInfo from starlette.requests import Request from titiler.core.dependencies import DefaultDependency -from titiler.pgstac import mosaic as pgstac_mosaic -from titiler.pgstac.reader import PgSTACReader +from titiler.pgstac import backend as pgstac_mosaic +from titiler.pgstac.reader import PgSTACReader, SimpleSTACReader from titiler.pgstac.settings import CacheSettings from pccommon.cdn import BlobCDN @@ -48,22 +50,77 @@ class ItemSTACReader(PgSTACReader): request: Optional[Request] = attr.ib(default=None) def _get_asset_info(self, asset: str) -> AssetInfo: - """return asset's url.""" - info = super()._get_asset_info(asset) - asset_url = BlobCDN.transform_if_available(info["url"]) + """Validate asset names and return asset's info. + + Args: + asset (str): STAC asset name. + + Returns: + AssetInfo: STAC asset info. + + """ + asset, vrt_options = self._parse_vrt_asset(asset) + if asset not in self.assets: + raise InvalidAssetName( + f"'{asset}' is not valid, should be one of {self.assets}" + ) + + asset_info = self.item.assets[asset] + extras = asset_info.extra_fields + + info = AssetInfo( + url=asset_info.get_absolute_href() or asset_info.href, + metadata=extras if not vrt_options else None, + ) - if self.input.collection_id: - render_config = get_render_config(self.input.collection_id) + if STAC_ALTERNATE_KEY and extras.get("alternate"): + if alternate := extras["alternate"].get(STAC_ALTERNATE_KEY): + info["url"] = alternate["href"] + + asset_url = BlobCDN.transform_if_available(info["url"]) + if self.item.collection_id: + render_config = get_render_config(self.item.collection_id) if render_config and render_config.requires_token: asset_url = pc.sign(asset_url) info["url"] = asset_url + + if asset_info.media_type: + info["media_type"] = asset_info.media_type + + # https://github.com/stac-extensions/file + if head := extras.get("file:header_size"): + info["env"] = {"GDAL_INGESTED_BYTES_AT_OPEN": head} + + # https://github.com/stac-extensions/raster + if extras.get("raster:bands") and not vrt_options: + bands = extras.get("raster:bands") + stats = [ + (b["statistics"]["minimum"], b["statistics"]["maximum"]) + for b in bands + if {"minimum", "maximum"}.issubset(b.get("statistics", {})) + ] + # check that stats data are all double and make warning if not + if ( + stats + and all(isinstance(v, (int, float)) for stat in stats for v in stat) + and len(stats) == len(bands) + ): + info["dataset_statistics"] = stats + else: + warnings.warn( + "Some statistics data in STAC are invalid, they will be ignored." + ) + + if vrt_options: + info["url"] = f"vrt://{info['url']}?{vrt_options}" + return info @attr.s -class MosaicSTACReader(pgstac_mosaic.CustomSTACReader): - """Custom version of titiler.pgstac.mosaic.CustomSTACReader).""" +class MosaicSTACReader(SimpleSTACReader): + """Custom version of titiler.pgstac.reader.SimpleSTACReader.""" # We make request an optional attribute to avoid re-writing # the whole list of attribute @@ -79,24 +136,45 @@ def _get_asset_info(self, asset: str) -> AssetInfo: str: STAC asset href. """ + asset, vrt_options = self._parse_vrt_asset(asset) if asset not in self.assets: - raise InvalidAssetName(f"{asset} is not valid") + raise InvalidAssetName( + f"{asset} is not valid. Should be one of {self.assets}" + ) - asset_url = BlobCDN.transform_if_available(self.input["assets"][asset]["href"]) + asset_info = self.input["assets"][asset] + info = AssetInfo( + url=asset_info["href"], + env={}, + ) - collection = self.input.get("collection", None) - if collection: + asset_url = BlobCDN.transform_if_available(info["url"]) + if collection := self.input.get("collection", None): render_config = get_render_config(collection) if render_config and render_config.requires_token: asset_url = pc.sign(asset_url) - info = AssetInfo(url=asset_url) - if "file:header_size" in self.input["assets"][asset]: - info["env"] = { - "GDAL_INGESTED_BYTES_AT_OPEN": self.input["assets"][asset][ - "file:header_size" - ] - } + info["url"] = asset_url + + if media_type := asset_info.get("type"): + info["media_type"] = media_type + + if header_size := asset_info.get("file:header_size"): + info["env"].update( # type: ignore + {"GDAL_INGESTED_BYTES_AT_OPEN": header_size} + ) + + if bands := asset_info.get("raster:bands"): + stats = [ + (b["statistics"]["minimum"], b["statistics"]["maximum"]) + for b in bands + if {"minimum", "maximum"}.issubset(b.get("statistics", {})) + ] + if len(stats) == len(bands): + info["dataset_statistics"] = stats + + if vrt_options: + info["url"] = f"vrt://{info['url']}?{vrt_options}" return info @@ -200,7 +278,7 @@ def _reader( ) as src_dst: return src_dst.tile(x, y, z, **kwargs) - tile = mosaic_reader( + img, used_assets = mosaic_reader( mosaic_assets, _reader, tile_x, @@ -223,4 +301,4 @@ def _reader( ), ) - return tile + return img, [x["id"] for x in used_assets] diff --git a/pctiler/pctiler/version.py b/pctiler/pctiler/version.py index 3dc1f76b..d3ec452c 100644 --- a/pctiler/pctiler/version.py +++ b/pctiler/pctiler/version.py @@ -1 +1 @@ -__version__ = "0.1.0" +__version__ = "0.2.0" diff --git a/pctiler/pyproject.toml b/pctiler/pyproject.toml index a776bf18..b981de50 100644 --- a/pctiler/pyproject.toml +++ b/pctiler/pyproject.toml @@ -7,12 +7,11 @@ name = "pctiler" dynamic = ["version"] description = "Planetary Computer API - Tiler." license = { text = "MIT" } -requires-python = ">=3.7" +requires-python = ">=3.9" dependencies = [ - "fastapi-slim==0.111.0", - "geojson-pydantic==1.1.0", + "fastapi==0.112.3", + "geojson-pydantic==2.0.0", "idna>=3.7.0", - "importlib_resources>=1.1.0;python_version<'3.9'", "jinja2==3.1.5", "matplotlib==3.9.0", "orjson==3.10.4", @@ -21,11 +20,11 @@ dependencies = [ "psycopg[binary,pool]", "pydantic>=2.7,<2.8", "pystac==1.10.1", - "rasterio==1.3.10", + "rasterio==1.4.3", "requests==2.32.3", - "titiler.core==0.18.3", - "titiler.mosaic==0.18.3", - "titiler.pgstac==1.3.0", + "titiler.core==0.22.1", + "titiler.mosaic==0.22.1", + "titiler.pgstac==1.8.0", ] [project.optional-dependencies] diff --git a/pctiler/requirements-dev.txt b/pctiler/requirements-dev.txt index a96f8449..e3a792c5 100644 --- a/pctiler/requirements-dev.txt +++ b/pctiler/requirements-dev.txt @@ -8,98 +8,111 @@ affine==2.4.0 # via rasterio annotated-types==0.7.0 # via pydantic -anyio==4.3.0 +anyio==4.9.0 # via # httpx # starlette -attrs==23.2.0 +attrs==25.3.0 # via # cogeo-mosaic + # jsonschema # morecantile # rasterio + # referencing # rio-tiler -cachetools==5.3.3 +cachetools==6.1.0 # via # cogeo-mosaic # rio-tiler -certifi==2024.7.4 +certifi==2025.6.15 # via # httpcore # httpx # pyproj # rasterio # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.2 # via requests -click==8.1.7 +click==8.1.8 # via # click-plugins # cligj + # cogeo-mosaic # planetary-computer # rasterio click-plugins==1.1.1 - # via rasterio + # via + # cogeo-mosaic + # rasterio cligj==0.7.2 - # via rasterio -cogeo-mosaic==7.1.0 + # via + # cogeo-mosaic + # rasterio +cogeo-mosaic==8.2.0 # via titiler-mosaic -color-operations==0.1.3 +color-operations==0.2.0 # via rio-tiler -contourpy==1.2.1 +contourpy==1.3.0 # via matplotlib cycler==0.12.1 # via matplotlib -exceptiongroup==1.2.0 +exceptiongroup==1.3.0 # via anyio -fastapi-slim==0.111.0 +fastapi==0.112.3 # via # pctiler (pctiler/pyproject.toml) # titiler-core -fonttools==4.53.0 +fonttools==4.58.4 # via matplotlib -geojson-pydantic==1.1.0 +geojson-pydantic==2.0.0 # via # pctiler (pctiler/pyproject.toml) # titiler-core - # titiler-pgstac -h11==0.14.0 +h11==0.16.0 # via httpcore -httpcore==1.0.5 +httpcore==1.0.9 # via httpx -httpx==0.27.0 +httpx==0.28.1 # via # cogeo-mosaic # rio-tiler -idna==3.7 +idna==3.10 # via # anyio # httpx # pctiler (pctiler/pyproject.toml) # requests -importlib-metadata==7.1.0 - # via rasterio -importlib-resources==6.4.0 +importlib-metadata==8.7.0 + # via + # cogeo-mosaic + # rasterio +importlib-resources==6.5.2 # via matplotlib jinja2==3.1.5 # via # pctiler (pctiler/pyproject.toml) # titiler-core -kiwisolver==1.4.5 +jsonschema==4.24.0 + # via pystac +jsonschema-specifications==2025.4.1 + # via jsonschema +kiwisolver==1.4.7 # via matplotlib -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 matplotlib==3.9.0 # via pctiler (pctiler/pyproject.toml) -morecantile==5.3.0 +morecantile==6.2.0 # via # cogeo-mosaic # rio-tiler # supermorecado # titiler-core -numexpr==2.9.0 +numexpr==2.10.2 # via rio-tiler -numpy==1.26.4 +numpy==2.0.2 # via + # cogeo-mosaic # color-operations # contourpy # matplotlib @@ -107,11 +120,10 @@ numpy==1.26.4 # rasterio # rio-tiler # shapely - # snuggs # titiler-core orjson==3.10.4 # via pctiler (pctiler/pyproject.toml) -packaging==24.1 +packaging==25.0 # via # matplotlib # planetary-computer @@ -121,16 +133,16 @@ pillow==10.3.0 # pctiler (pctiler/pyproject.toml) planetary-computer==1.0.0 # via pctiler (pctiler/pyproject.toml) -psycopg[binary,pool]==3.1.18 +psycopg[binary,pool]==3.2.9 # via pctiler (pctiler/pyproject.toml) -psycopg-binary==3.1.18 +psycopg-binary==3.2.9 # via psycopg -psycopg-pool==3.2.1 +psycopg-pool==3.2.6 # via psycopg pydantic==2.7.4 # via # cogeo-mosaic - # fastapi-slim + # fastapi # geojson-pydantic # morecantile # pctiler (pctiler/pyproject.toml) @@ -141,98 +153,105 @@ pydantic==2.7.4 # titiler-pgstac pydantic-core==2.18.4 # via pydantic -pydantic-settings==2.3.3 +pydantic-settings==2.9.1 # via # cogeo-mosaic # titiler-pgstac -pyparsing==3.1.2 +pyparsing==3.2.3 # via # matplotlib - # snuggs + # rasterio pyproj==3.6.1 # via morecantile -pystac==1.10.1 +pystac[validation]==1.10.1 # via # pctiler (pctiler/pyproject.toml) # planetary-computer # pystac-client # rio-tiler -pystac-client==0.6.1 +pystac-client==0.8.3 # via planetary-computer python-dateutil==2.9.0.post0 # via # matplotlib # pystac # pystac-client -python-dotenv==1.0.1 +python-dotenv==1.1.0 # via # planetary-computer # pydantic-settings -pytz==2024.1 +pytz==2025.2 # via planetary-computer -rasterio==1.3.10 +rasterio==1.4.3 # via # cogeo-mosaic # pctiler (pctiler/pyproject.toml) # rio-tiler # supermorecado # titiler-core +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications requests==2.32.3 # via # pctiler (pctiler/pyproject.toml) # planetary-computer # pystac-client -rio-tiler==6.6.1 +rio-tiler==7.8.1 # via # cogeo-mosaic # titiler-core -shapely==2.0.3 +rpds-py==0.25.1 + # via + # jsonschema + # referencing +shapely==2.0.7 # via cogeo-mosaic -simplejson==3.19.2 +simplejson==3.20.1 # via titiler-core -six==1.16.0 +six==1.17.0 # via python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx -snuggs==1.4.7 - # via rasterio -starlette==0.37.2 - # via fastapi-slim + # via anyio +starlette==0.38.6 + # via fastapi supermorecado==0.1.2 # via cogeo-mosaic -titiler-core==0.18.3 +titiler-core==0.22.1 # via # pctiler (pctiler/pyproject.toml) # titiler-mosaic # titiler-pgstac -titiler-mosaic==0.18.3 +titiler-mosaic==0.22.1 # via # pctiler (pctiler/pyproject.toml) # titiler-pgstac -titiler-pgstac==1.3.0 +titiler-pgstac==1.8.0 # via pctiler (pctiler/pyproject.toml) -types-requests==2.31.0.6 +types-requests==2.32.4.20250611 # via pctiler (pctiler/pyproject.toml) -types-urllib3==1.26.25.14 - # via types-requests -typing-extensions==4.10.0 +typing-extensions==4.14.0 # via # anyio - # fastapi-slim + # exceptiongroup + # fastapi # psycopg # psycopg-pool # pydantic # pydantic-core + # referencing + # rio-tiler # starlette # titiler-core -urllib3==1.26.18 - # via requests -zipp==3.19.2 + # typing-inspection +typing-inspection==0.4.1 + # via pydantic-settings +urllib3==2.4.0 + # via + # requests + # types-requests +zipp==3.23.0 # via # importlib-metadata # importlib-resources - -# The following packages are considered to be unsafe in a requirements file: -# setuptools diff --git a/pctiler/requirements-server.txt b/pctiler/requirements-server.txt index bb396935..3869e759 100644 --- a/pctiler/requirements-server.txt +++ b/pctiler/requirements-server.txt @@ -8,104 +8,117 @@ affine==2.4.0 # via rasterio annotated-types==0.7.0 # via pydantic -anyio==3.7.1 +anyio==4.9.0 # via # httpx # starlette # watchfiles -attrs==23.2.0 +attrs==25.3.0 # via # cogeo-mosaic + # jsonschema # morecantile # rasterio + # referencing # rio-tiler -cachetools==5.3.3 +cachetools==6.1.0 # via # cogeo-mosaic # rio-tiler -certifi==2024.7.4 +certifi==2025.6.15 # via # httpcore # httpx # pyproj # rasterio # requests -charset-normalizer==3.3.2 +charset-normalizer==3.4.2 # via requests -click==8.1.7 +click==8.1.8 # via # click-plugins # cligj + # cogeo-mosaic # planetary-computer # rasterio # uvicorn click-plugins==1.1.1 - # via rasterio + # via + # cogeo-mosaic + # rasterio cligj==0.7.2 - # via rasterio -cogeo-mosaic==7.1.0 + # via + # cogeo-mosaic + # rasterio +cogeo-mosaic==8.2.0 # via titiler-mosaic -color-operations==0.1.3 +color-operations==0.2.0 # via rio-tiler -contourpy==1.2.1 +contourpy==1.3.0 # via matplotlib cycler==0.12.1 # via matplotlib -exceptiongroup==1.2.0 +exceptiongroup==1.3.0 # via anyio -fastapi-slim==0.111.0 +fastapi==0.112.3 # via # pctiler (pctiler/pyproject.toml) # titiler-core -fonttools==4.53.0 +fonttools==4.58.4 # via matplotlib -geojson-pydantic==1.1.0 +geojson-pydantic==2.0.0 # via # pctiler (pctiler/pyproject.toml) # titiler-core - # titiler-pgstac -h11==0.14.0 +h11==0.16.0 # via # httpcore # uvicorn -httpcore==1.0.4 +httpcore==1.0.9 # via httpx -httptools==0.6.1 +httptools==0.6.4 # via uvicorn -httpx==0.27.0 +httpx==0.28.1 # via # cogeo-mosaic # rio-tiler -idna==3.7 +idna==3.10 # via # anyio # httpx # pctiler (pctiler/pyproject.toml) # requests -importlib-metadata==7.1.0 - # via rasterio -importlib-resources==6.4.0 +importlib-metadata==8.7.0 + # via + # cogeo-mosaic + # rasterio +importlib-resources==6.5.2 # via matplotlib jinja2==3.1.5 # via # pctiler (pctiler/pyproject.toml) # titiler-core -kiwisolver==1.4.5 +jsonschema==4.24.0 + # via pystac +jsonschema-specifications==2025.4.1 + # via jsonschema +kiwisolver==1.4.7 # via matplotlib -markupsafe==2.1.5 +markupsafe==3.0.2 # via jinja2 matplotlib==3.9.0 # via pctiler (pctiler/pyproject.toml) -morecantile==5.3.0 +morecantile==6.2.0 # via # cogeo-mosaic # rio-tiler # supermorecado # titiler-core -numexpr==2.9.0 +numexpr==2.10.2 # via rio-tiler -numpy==1.26.4 +numpy==2.0.2 # via + # cogeo-mosaic # color-operations # contourpy # matplotlib @@ -113,11 +126,10 @@ numpy==1.26.4 # rasterio # rio-tiler # shapely - # snuggs # titiler-core orjson==3.10.4 # via pctiler (pctiler/pyproject.toml) -packaging==24.1 +packaging==25.0 # via # matplotlib # planetary-computer @@ -127,16 +139,16 @@ pillow==10.3.0 # pctiler (pctiler/pyproject.toml) planetary-computer==1.0.0 # via pctiler (pctiler/pyproject.toml) -psycopg[binary,pool]==3.1.18 +psycopg[binary,pool]==3.2.9 # via pctiler (pctiler/pyproject.toml) -psycopg-binary==3.1.18 +psycopg-binary==3.2.9 # via psycopg -psycopg-pool==3.2.1 +psycopg-pool==3.2.6 # via psycopg pydantic==2.7.4 # via # cogeo-mosaic - # fastapi-slim + # fastapi # geojson-pydantic # morecantile # pctiler (pctiler/pyproject.toml) @@ -147,105 +159,113 @@ pydantic==2.7.4 # titiler-pgstac pydantic-core==2.18.4 # via pydantic -pydantic-settings==2.3.3 +pydantic-settings==2.9.1 # via # cogeo-mosaic # titiler-pgstac -pyparsing==3.1.2 +pyparsing==3.2.3 # via # matplotlib - # snuggs + # rasterio pyproj==3.6.1 # via morecantile -pystac==1.10.1 +pystac[validation]==1.10.1 # via # pctiler (pctiler/pyproject.toml) # planetary-computer # pystac-client # rio-tiler -pystac-client==0.6.1 +pystac-client==0.8.3 # via planetary-computer python-dateutil==2.9.0.post0 # via # matplotlib # pystac # pystac-client -python-dotenv==1.0.1 +python-dotenv==1.1.0 # via # planetary-computer # pydantic-settings # uvicorn -pytz==2024.1 +pytz==2025.2 # via planetary-computer -pyyaml==6.0.1 +pyyaml==6.0.2 # via uvicorn -rasterio==1.3.10 +rasterio==1.4.3 # via # cogeo-mosaic # pctiler (pctiler/pyproject.toml) # rio-tiler # supermorecado # titiler-core +referencing==0.36.2 + # via + # jsonschema + # jsonschema-specifications requests==2.32.3 # via # pctiler (pctiler/pyproject.toml) # planetary-computer # pystac-client -rio-tiler==6.6.1 +rio-tiler==7.8.1 # via # cogeo-mosaic # titiler-core -shapely==2.0.3 +rpds-py==0.25.1 + # via + # jsonschema + # referencing +shapely==2.0.7 # via cogeo-mosaic -simplejson==3.19.2 +simplejson==3.20.1 # via titiler-core -six==1.16.0 +six==1.17.0 # via python-dateutil sniffio==1.3.1 - # via - # anyio - # httpx -snuggs==1.4.7 - # via rasterio -starlette==0.37.2 - # via fastapi-slim + # via anyio +starlette==0.38.6 + # via fastapi supermorecado==0.1.2 # via cogeo-mosaic -titiler-core==0.18.3 +titiler-core==0.22.1 # via # pctiler (pctiler/pyproject.toml) # titiler-mosaic # titiler-pgstac -titiler-mosaic==0.18.3 +titiler-mosaic==0.22.1 # via # pctiler (pctiler/pyproject.toml) # titiler-pgstac -titiler-pgstac==1.3.0 +titiler-pgstac==1.8.0 # via pctiler (pctiler/pyproject.toml) -typing-extensions==4.10.0 +typing-extensions==4.14.0 # via - # fastapi-slim + # anyio + # exceptiongroup + # fastapi # psycopg # psycopg-pool # pydantic # pydantic-core + # referencing + # rio-tiler # starlette # titiler-core + # typing-inspection # uvicorn -urllib3==1.26.19 +typing-inspection==0.4.1 + # via pydantic-settings +urllib3==2.4.0 # via requests uvicorn[standard]==0.30.1 # via pctiler (pctiler/pyproject.toml) -uvloop==0.19.0 +uvloop==0.21.0 # via uvicorn -watchfiles==0.22.0 +watchfiles==1.1.0 # via uvicorn -websockets==12.0 +websockets==15.0.1 # via uvicorn -zipp==3.19.2 +zipp==3.23.0 # via # importlib-metadata # importlib-resources - -# The following packages are considered to be unsafe in a requirements file: -# setuptools