Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: STAC Render Extension support #1038

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions docs/src/advanced/Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class FactoryExtension(metaclass=abc.ABCMeta):

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

#### stacRenderExtenstion
- Goal: adds a `/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
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 @@ -33,6 +33,7 @@
cogValidateExtension,
cogViewerExtension,
stacExtension,
stacRenderExtension,
stacViewerExtension,
)
from titiler.mosaic.errors import MOSAIC_STATUS_CODES
Expand Down Expand Up @@ -122,6 +123,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
router_prefix="/stac",
extensions=[
stacViewerExtension(),
stacRenderExtension(),
],
)

Expand Down
68 changes: 68 additions & 0 deletions src/titiler/core/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""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 rasterio.dtypes import dtype_ranges
from rio_tiler.colormap import apply_cmap
from rio_tiler.errors import InvalidDatatypeWarning
Expand Down Expand Up @@ -116,3 +119,52 @@ def render_image(
),
output_format.mediatype,
)


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))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice the addition of QueryParams here.
The reason is that request_params_to_args expects to see the result of this function.
An example that helped me catch it is rescale param - link to def.

rescale: Annotated[
    Optional[List[str]],
    Query(...)
]

If I pass things directly from STAC renders block, it would not pass validation, since it is defined as [float]
To mitigate this, I first serialize params to the state that fastapi would receive them, and then deserialize how it would do it.

)


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