Skip to content

Commit

Permalink
feat: STAC Render Extension support (#1038)
Browse files Browse the repository at this point in the history
* Render Extension
Render extension started during STAC render sprint in SatSummit Lisbon 2024.

- listing (or showing to please Vincent)

Please contribute to complete the feature to

- generate the final XYZ link for rendering following the rules in STAC extensions
- add a dedicated endpoint for render XYZ

* feat(stac): add render extenstions support

* remove unnecessary item.json file

* Rework the response structure to include links.

* rename renderExtension -> stacRenderExtension

* add tests for stacRenderExtension

* add test for extra param

* docs and docstrings

* fix typing in python 3.8

* Move out query params validation

* edits

* update changelog

---------

Co-authored-by: Emmanuel Mathot <[email protected]>
Co-authored-by: vincentsarago <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
1 parent 5158f9d commit 72a8df9
Show file tree
Hide file tree
Showing 10 changed files with 712 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
### titiler.extensions

* use `factory.render_func` as render function in `wmsExtension` endpoints
* add `stacRenderExtension` which adds two endpoints: `/renders` (lists all renders) and `/renders/<render_id>` (render metadata and links) (author @alekzvik, https://github.com/developmentseed/titiler/pull/1038)

### Misc

Expand Down
4 changes: 4 additions & 0 deletions docs/src/advanced/Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ class FactoryExtension(metaclass=abc.ABCMeta):

- Goal: adds a `/wms` endpoint to support OGC WMS specification (`GetCapabilities` and `GetMap`)

#### stacRenderExtenstion

- Goal: adds `/render` and `/render/{render_id}` endpoints which return the contents of [STAC render extension](https://github.com/stac-extensions/render) and links to tileset.json and WMTS service

## How To

### Use extensions
Expand Down
2 changes: 1 addition & 1 deletion src/titiler/application/tests/routes/test_stac.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""test /COG endpoints."""
"""test /stac endpoints."""

from typing import Dict
from unittest.mock import patch
Expand Down
2 changes: 2 additions & 0 deletions src/titiler/application/titiler/application/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
cogValidateExtension,
cogViewerExtension,
stacExtension,
stacRenderExtension,
stacViewerExtension,
)
from titiler.mosaic.errors import MOSAIC_STATUS_CODES
Expand Down Expand Up @@ -123,6 +124,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
router_prefix="/stac",
extensions=[
stacViewerExtension(),
stacRenderExtension(),
],
)

Expand Down
67 changes: 67 additions & 0 deletions src/titiler/core/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Test utils."""

from titiler.core.dependencies import BidxParams
from titiler.core.utils import deserialize_query_params, get_dependency_query_params


def test_get_dependency_params():
"""Test dependency filtering from query params."""

# invalid
values, err = get_dependency_query_params(
dependency=BidxParams, params={"bidx": ["invalid type"]}
)
assert values == {}
assert err
assert err == [
{
"input": "invalid type",
"loc": (
"query",
"bidx",
0,
),
"msg": "Input should be a valid integer, unable to parse string as an integer",
"type": "int_parsing",
},
]

# not in dep
values, err = get_dependency_query_params(
dependency=BidxParams, params={"not_in_dep": "no error, no value"}
)
assert values == {"indexes": None}
assert not err

# valid
values, err = get_dependency_query_params(
dependency=BidxParams, params={"bidx": [1, 2, 3]}
)
assert values == {"indexes": [1, 2, 3]}
assert not err

# valid and not in dep
values, err = get_dependency_query_params(
dependency=BidxParams,
params={"bidx": [1, 2, 3], "other param": "to be filtered out"},
)
assert values == {"indexes": [1, 2, 3]}
assert not err


def test_deserialize_query_params():
"""Test deserialize_query_params."""
# invalid
res, err = deserialize_query_params(
dependency=BidxParams, params={"bidx": ["invalid type"]}
)
print(res)
assert res == BidxParams(indexes=None)
assert err

# valid
res, err = deserialize_query_params(
dependency=BidxParams, params={"not_in_dep": "no error, no value", "bidx": [1]}
)
assert res == BidxParams(indexes=[1])
assert not err
54 changes: 53 additions & 1 deletion src/titiler/core/titiler/core/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""titiler.core utilities."""

import warnings
from typing import Any, Optional, Sequence, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
from urllib.parse import urlencode

import numpy
from fastapi.datastructures import QueryParams
from fastapi.dependencies.utils import get_dependant, request_params_to_args
from geojson_pydantic.geometries import MultiPolygon, Polygon
from rasterio.dtypes import dtype_ranges
from rio_tiler.colormap import apply_cmap
Expand Down Expand Up @@ -131,3 +134,52 @@ def bounds_to_geometry(bounds: BBox) -> Union[Polygon, MultiPolygon]:
coordinates=[pl.coordinates, pr.coordinates],
)
return Polygon.from_bounds(*bounds)


T = TypeVar("T")

ValidParams = Dict[str, Any]
Errors = List[Any]


def get_dependency_query_params(
dependency: Callable,
params: Dict,
) -> Tuple[ValidParams, Errors]:
"""Check QueryParams for Query dependency.
1. `get_dependant` is used to get the query-parameters required by the `callable`
2. we use `request_params_to_args` to construct arguments needed to call the `callable`
3. we call the `callable` and catch any errors
Important: We assume the `callable` in not a co-routine.
"""
dep = get_dependant(path="", call=dependency)
return request_params_to_args(
dep.query_params, QueryParams(urlencode(params, doseq=True))
)


def deserialize_query_params(
dependency: Callable[..., T], params: Dict
) -> Tuple[T, Errors]:
"""Deserialize QueryParams for given dependency.
Parse params as query params and deserialize with dependency.
Important: We assume the `callable` in not a co-routine.
"""
values, errors = get_dependency_query_params(dependency, params)
return dependency(**values), errors


def extract_query_params(dependencies, params) -> Tuple[ValidParams, Errors]:
"""Extract query params given list of dependencies."""
values = {}
errors = []
for dep in dependencies:
dep_values, dep_errors = deserialize_query_params(dep, params)
if dep_values:
values.update(dep_values)
errors += dep_errors
return values, errors
Loading

0 comments on commit 72a8df9

Please sign in to comment.