Skip to content

Commit

Permalink
Add support and tests for legacy mosaic tile routes (#234)
Browse files Browse the repository at this point in the history
* Add support and tests for legacy mosaic tile routes

The tile route prefixes were reordered in recent versions of
titiler.pgstac. Both versions of the routes are supported in PC and
tests have been added to confirm.
  • Loading branch information
mmcfarland authored Jul 18, 2024
1 parent 66fa8fe commit d2b46c9
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 43 deletions.
18 changes: 12 additions & 6 deletions deployment/bin/lib
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ function disable_shared_access_keys() {
--resource-group ${SAK_RESOURCE_GROUP} \
--allow-shared-key-access false \
--subscription ${ARM_SUBSCRIPTION_ID} \
--output none
--output none \
--only-show-errors

if [ $? -ne 0 ]; then
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
Expand All @@ -170,7 +171,8 @@ function enable_shared_access_keys() {
--resource-group ${SAK_RESOURCE_GROUP} \
--allow-shared-key-access true \
--subscription ${ARM_SUBSCRIPTION_ID} \
--output none
--output none \
--only-show-errors
done

sleep 10
Expand All @@ -185,7 +187,8 @@ function add_ip_to_firewalls() {
-n "${KEY_VAULT_NAME}" \
--ip-address "$cidr" \
--subscription "${ARM_SUBSCRIPTION_ID}" \
--output none
--output none \
--only-show-errors

# Also add the IP to the terraform state storage account
for FW_STORAGE_ACCOUNT in "${!FW_STORAGE_ACCOUNTS[@]}"; do
Expand All @@ -196,7 +199,8 @@ function add_ip_to_firewalls() {
-n "${FW_STORAGE_ACCOUNT}" \
--ip-address "$cidr" \
--subscription "${ARM_SUBSCRIPTION_ID}" \
--output none
--output none \
--only-show-errors
done

sleep 10
Expand All @@ -211,7 +215,8 @@ function remove_ip_from_firewalls() {
-n ${KEY_VAULT_NAME} \
--ip-address $cidr \
--subscription ${ARM_SUBSCRIPTION_ID} \
--output none
--output none \
--only-show-errors

for FW_STORAGE_ACCOUNT in "${!FW_STORAGE_ACCOUNTS[@]}"; do
FW_RESOURCE_GROUP=${FW_STORAGE_ACCOUNTS[$FW_STORAGE_ACCOUNT]}
Expand All @@ -221,6 +226,7 @@ function remove_ip_from_firewalls() {
-n ${FW_STORAGE_ACCOUNT} \
--ip-address $cidr \
--subscription ${ARM_SUBSCRIPTION_ID} \
--output none
--output none \
--only-show-errors
done
}
5 changes: 4 additions & 1 deletion pccommon/pccommon/config/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CamelModel(BaseModel):
# https://docs.pydantic.dev/latest/api/config/#pydantic.alias_generators.to_camel
"alias_generator": camelize,
"populate_by_name": True,
"use_enum_values": True,
}


Expand Down Expand Up @@ -267,7 +268,9 @@ class RenderOptions(CamelModel):

name: str
description: Optional[str] = None
type: Optional[RenderOptionType] = Field(default=RenderOptionType.raster_tile)
type: Optional[RenderOptionType] = Field(
default=RenderOptionType.raster_tile, validate_default=True
)
options: Optional[str]
vector_options: Optional[VectorTileOptions] = None
min_zoom: int
Expand Down
21 changes: 21 additions & 0 deletions pctiler/pctiler/endpoints/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import logging
from typing import Callable

import fastapi
import starlette

logger = logging.getLogger(__name__)


def get_endpoint_function(
router: fastapi.APIRouter, path: str, method: str
) -> Callable:
for route in router.routes:
match, _ = route.matches({"type": "http", "path": path, "method": method})
if match == starlette.routing.Match.FULL:
# The abstract BaseRoute doesn't have a `.endpoint` attribute,
# but all of its subclasses do.
return route.endpoint # type: ignore [attr-defined]

logger.warning(f"Could not find endpoint. method={method} path={path}")
raise fastapi.HTTPException(detail="Internal system error", status_code=500)
20 changes: 3 additions & 17 deletions pctiler/pctiler/endpoints/item.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import logging
from typing import Annotated, Callable, Optional
from typing import Annotated, Optional
from urllib.parse import quote_plus, urljoin

import fastapi
import pystac
import starlette
from fastapi import Body, Depends, HTTPException, Query, Request, Response
from fastapi import Body, Depends, Query, Request, Response
from fastapi.templating import Jinja2Templates
from geojson_pydantic.features import Feature
from html_sanitizer.sanitizer import Sanitizer
Expand All @@ -18,6 +17,7 @@
from pccommon.config import get_render_config
from pctiler.colormaps import PCColorMapParams
from pctiler.config import get_settings
from pctiler.endpoints.dependencies import get_endpoint_function
from pctiler.reader import ItemSTACReader, ReaderParams

try:
Expand Down Expand Up @@ -158,17 +158,3 @@ def geojson_crop( # type: ignore
env=env,
)
return result


def get_endpoint_function(
router: fastapi.APIRouter, path: str, method: str
) -> Callable:
for route in router.routes:
match, _ = route.matches({"type": "http", "path": path, "method": method})
if match == starlette.routing.Match.FULL:
# The abstract BaseRoute doesn't have a `.endpoint` attribute,
# but all of its subclasses do.
return route.endpoint # type: ignore [attr-defined]

logger.warning(f"Could not find endpoint. method={method} path={path}")
raise HTTPException(detail="Internal system error", status_code=500)
98 changes: 95 additions & 3 deletions pctiler/pctiler/endpoints/pg_mosaic.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from dataclasses import dataclass, field
from typing import List, Optional
from typing import Annotated, List, Literal, Optional

from fastapi import FastAPI, Query, Request
from fastapi import APIRouter, Depends, FastAPI, Query, Request, Response
from fastapi.responses import ORJSONResponse
from psycopg_pool import ConnectionPool
from pydantic import Field
from titiler.core import dependencies
from titiler.pgstac.dependencies import SearchIdParams
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.factory import MosaicTilerFactory

from pccommon.config import get_collection_config
from pccommon.config.collections import MosaicInfo
from pctiler.colormaps import PCColorMapParams
from pctiler.config import get_settings
from pctiler.endpoints.dependencies import get_endpoint_function
from pctiler.reader import PGSTACBackend, ReaderParams


Expand Down Expand Up @@ -75,3 +80,90 @@ def mosaic_info(
by_alias=True, exclude_unset=True
),
)


legacy_mosaic_router = APIRouter()


@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
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{z}/{x}/{y}@{scale}x", **img_endpoint_params
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{z}/{x}/{y}@{scale}x.{format}", **img_endpoint_params
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{tileMatrixSetId}/{z}/{x}/{y}", **img_endpoint_params
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{tileMatrixSetId}/{z}/{x}/{y}.{format}",
**img_endpoint_params,
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x",
**img_endpoint_params,
)
@legacy_mosaic_router.get(
"/tiles/{search_id}/{tileMatrixSetId}/{z}/{x}/{y}@{scale}x.{format}",
**img_endpoint_params,
)
def tile_routes( # type: ignore
request: Request,
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,
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
] = None,
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}",
method=request.method,
)
result = endpoint(
search_id=search_id,
tile=tile,
tileMatrixSetId=tileMatrixSetId,
scale=scale,
format=format,
tile_params=tile_params,
layer_params=layer_params,
dataset_params=dataset_params,
pixel_selection=pixel_selection,
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
5 changes: 5 additions & 0 deletions pctiler/pctiler/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator:
prefix=settings.mosaic_endpoint_prefix + "/{search_id}",
tags=["PgSTAC Mosaic endpoints"],
)
app.include_router(
pg_mosaic.legacy_mosaic_router,
prefix=settings.mosaic_endpoint_prefix,
tags=["PgSTAC Mosaic endpoints"],
)
pg_mosaic.add_collection_mosaic_info_route(
app,
prefix=settings.mosaic_endpoint_prefix,
Expand Down
14 changes: 9 additions & 5 deletions pctiler/pctiler/middleware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
from collections import OrderedDict

from starlette.datastructures import MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
Expand All @@ -23,11 +22,16 @@ async def send_with_searchid(message: Message) -> None:
elif message_type == "http.response.body":
# Rewrite id to searchid for backwards compatibility, keep key order
body = json.loads(message["body"])
ordered_body = OrderedDict()
ordered_body["searchid"] = body.get("id")
ordered_body.update(body)
body["searchid"] = body.get("id")

resp_body = json.dumps(ordered_body, ensure_ascii=False).encode("utf-8")
updated_links = []
for link in body.get("links", []):
link["href"] = link["href"].replace("/{tileMatrixSetId}", "")
updated_links.append(link)

body["links"] = updated_links

resp_body = json.dumps(body, ensure_ascii=False).encode("utf-8")
message["body"] = resp_body

# Update the content-length header on the start message
Expand Down
Loading

0 comments on commit d2b46c9

Please sign in to comment.