Skip to content

Commit 72a8df9

Browse files
alekzvikemmanuelmathotvincentsarago
authored
feat: STAC Render Extension support (#1038)
* 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]>
1 parent 5158f9d commit 72a8df9

File tree

10 files changed

+712
-2
lines changed

10 files changed

+712
-2
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
### titiler.extensions
2727

2828
* use `factory.render_func` as render function in `wmsExtension` endpoints
29+
* 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)
2930

3031
### Misc
3132

docs/src/advanced/Extensions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ class FactoryExtension(metaclass=abc.ABCMeta):
5555

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

58+
#### stacRenderExtenstion
59+
60+
- 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
61+
5862
## How To
5963

6064
### Use extensions

src/titiler/application/tests/routes/test_stac.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""test /COG endpoints."""
1+
"""test /stac endpoints."""
22

33
from typing import Dict
44
from unittest.mock import patch

src/titiler/application/titiler/application/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
cogValidateExtension,
3535
cogViewerExtension,
3636
stacExtension,
37+
stacRenderExtension,
3738
stacViewerExtension,
3839
)
3940
from titiler.mosaic.errors import MOSAIC_STATUS_CODES
@@ -123,6 +124,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
123124
router_prefix="/stac",
124125
extensions=[
125126
stacViewerExtension(),
127+
stacRenderExtension(),
126128
],
127129
)
128130

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""Test utils."""
2+
3+
from titiler.core.dependencies import BidxParams
4+
from titiler.core.utils import deserialize_query_params, get_dependency_query_params
5+
6+
7+
def test_get_dependency_params():
8+
"""Test dependency filtering from query params."""
9+
10+
# invalid
11+
values, err = get_dependency_query_params(
12+
dependency=BidxParams, params={"bidx": ["invalid type"]}
13+
)
14+
assert values == {}
15+
assert err
16+
assert err == [
17+
{
18+
"input": "invalid type",
19+
"loc": (
20+
"query",
21+
"bidx",
22+
0,
23+
),
24+
"msg": "Input should be a valid integer, unable to parse string as an integer",
25+
"type": "int_parsing",
26+
},
27+
]
28+
29+
# not in dep
30+
values, err = get_dependency_query_params(
31+
dependency=BidxParams, params={"not_in_dep": "no error, no value"}
32+
)
33+
assert values == {"indexes": None}
34+
assert not err
35+
36+
# valid
37+
values, err = get_dependency_query_params(
38+
dependency=BidxParams, params={"bidx": [1, 2, 3]}
39+
)
40+
assert values == {"indexes": [1, 2, 3]}
41+
assert not err
42+
43+
# valid and not in dep
44+
values, err = get_dependency_query_params(
45+
dependency=BidxParams,
46+
params={"bidx": [1, 2, 3], "other param": "to be filtered out"},
47+
)
48+
assert values == {"indexes": [1, 2, 3]}
49+
assert not err
50+
51+
52+
def test_deserialize_query_params():
53+
"""Test deserialize_query_params."""
54+
# invalid
55+
res, err = deserialize_query_params(
56+
dependency=BidxParams, params={"bidx": ["invalid type"]}
57+
)
58+
print(res)
59+
assert res == BidxParams(indexes=None)
60+
assert err
61+
62+
# valid
63+
res, err = deserialize_query_params(
64+
dependency=BidxParams, params={"not_in_dep": "no error, no value", "bidx": [1]}
65+
)
66+
assert res == BidxParams(indexes=[1])
67+
assert not err

src/titiler/core/titiler/core/utils.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
"""titiler.core utilities."""
22

33
import warnings
4-
from typing import Any, Optional, Sequence, Tuple, Union
4+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
5+
from urllib.parse import urlencode
56

67
import numpy
8+
from fastapi.datastructures import QueryParams
9+
from fastapi.dependencies.utils import get_dependant, request_params_to_args
710
from geojson_pydantic.geometries import MultiPolygon, Polygon
811
from rasterio.dtypes import dtype_ranges
912
from rio_tiler.colormap import apply_cmap
@@ -131,3 +134,52 @@ def bounds_to_geometry(bounds: BBox) -> Union[Polygon, MultiPolygon]:
131134
coordinates=[pl.coordinates, pr.coordinates],
132135
)
133136
return Polygon.from_bounds(*bounds)
137+
138+
139+
T = TypeVar("T")
140+
141+
ValidParams = Dict[str, Any]
142+
Errors = List[Any]
143+
144+
145+
def get_dependency_query_params(
146+
dependency: Callable,
147+
params: Dict,
148+
) -> Tuple[ValidParams, Errors]:
149+
"""Check QueryParams for Query dependency.
150+
151+
1. `get_dependant` is used to get the query-parameters required by the `callable`
152+
2. we use `request_params_to_args` to construct arguments needed to call the `callable`
153+
3. we call the `callable` and catch any errors
154+
155+
Important: We assume the `callable` in not a co-routine.
156+
"""
157+
dep = get_dependant(path="", call=dependency)
158+
return request_params_to_args(
159+
dep.query_params, QueryParams(urlencode(params, doseq=True))
160+
)
161+
162+
163+
def deserialize_query_params(
164+
dependency: Callable[..., T], params: Dict
165+
) -> Tuple[T, Errors]:
166+
"""Deserialize QueryParams for given dependency.
167+
168+
Parse params as query params and deserialize with dependency.
169+
170+
Important: We assume the `callable` in not a co-routine.
171+
"""
172+
values, errors = get_dependency_query_params(dependency, params)
173+
return dependency(**values), errors
174+
175+
176+
def extract_query_params(dependencies, params) -> Tuple[ValidParams, Errors]:
177+
"""Extract query params given list of dependencies."""
178+
values = {}
179+
errors = []
180+
for dep in dependencies:
181+
dep_values, dep_errors = deserialize_query_params(dep, params)
182+
if dep_values:
183+
values.update(dep_values)
184+
errors += dep_errors
185+
return values, errors

0 commit comments

Comments
 (0)