-
Notifications
You must be signed in to change notification settings - Fork 179
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
Changes from 1 commit
529d864
e20dfef
c63a482
3f10def
9904786
a7121b2
3e9995e
767c31d
46b13ee
8dbdcf6
2907235
9496130
dc0a57c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,12 +9,12 @@ | |
|
||
from attrs import define | ||
from fastapi import Depends, HTTPException, Path, Request | ||
from fastapi.dependencies.utils import get_dependant, request_params_to_args | ||
from pydantic import BaseModel | ||
from typing_extensions import Annotated | ||
|
||
from titiler.core.factory import FactoryExtension, MultiBaseTilerFactory | ||
from titiler.core.models.OGC import Link | ||
from titiler.core.utils import extract_query_params | ||
|
||
|
||
class RenderItem(BaseModel, extra="allow"): | ||
|
@@ -53,15 +53,12 @@ class stacRenderExtension(FactoryExtension): | |
def register(self, factory: MultiBaseTilerFactory): | ||
"""Register endpoint to the tiler factory.""" | ||
|
||
def _prepare_query_string(render: Dict, src_path: str) -> str: | ||
"""Prepare render related query params. | ||
|
||
Validates and filters query params that titiler can understand. | ||
If titiler does not support parameter, it will be ignored. | ||
""" | ||
def _validate_params(render: Dict) -> bool: | ||
"""Validate render related query params.""" | ||
# List of dependencies a `/tile` URL should validate | ||
# Note: Those dependencies should only require Query() inputs | ||
tile_dependencies = [ | ||
factory.reader_dependency, | ||
factory.layer_dependency, | ||
factory.dataset_dependency, | ||
# Image rendering Dependencies | ||
|
@@ -71,64 +68,65 @@ def _prepare_query_string(render: Dict, src_path: str) -> str: | |
factory.render_dependency, | ||
] | ||
|
||
query = {"url": src_path} | ||
for dependency in tile_dependencies: | ||
dep = get_dependant(path="", call=dependency) | ||
if dep.query_params: | ||
# call the dependency with the query-parameters values | ||
query_values, _errors = request_params_to_args( | ||
dep.query_params, render | ||
) | ||
_ = dependency(**query_values) | ||
query.update(query_values) | ||
return urlencode( | ||
{key: value for key, value in query.items() if value is not None}, | ||
doseq=True, | ||
) | ||
_values, errors = extract_query_params(tile_dependencies, render) | ||
|
||
return errors | ||
|
||
def _prepare_render_item( | ||
render_id: str, render: Dict, request: Request, src_path: str | ||
) -> Dict: | ||
"""Prepare single render item.""" | ||
query_string = _prepare_query_string(render, src_path) | ||
links = [ | ||
{ | ||
"href": factory.url_for( | ||
request, | ||
"Show STAC render", | ||
"STAC Renders metadata", | ||
render_id=render_id, | ||
) | ||
+ "?" | ||
+ urlencode({"url": src_path}), | ||
"rel": "self", | ||
"type": "application/json", | ||
"title": f"{render_id} render item", | ||
}, | ||
{ | ||
"href": factory.url_for( | ||
request, | ||
"tilejson", | ||
tileMatrixSetId="{tileMatrixSetId}", | ||
) | ||
+ "?" | ||
+ query_string, | ||
"rel": "tilesets-map", | ||
"title": f"tilejson file for {render_id}", | ||
"templated": True, | ||
}, | ||
{ | ||
"href": factory.url_for( | ||
request, | ||
"wmts", | ||
tileMatrixSetId="{tileMatrixSetId}", | ||
) | ||
+ "?" | ||
+ query_string, | ||
"rel": "tilesets-map", | ||
"title": f"WMTS service for {render_id}", | ||
"templated": True, | ||
}, | ||
"title": f"STAC Renders metadata for {render_id}", | ||
} | ||
] | ||
errors = _validate_params(render) | ||
|
||
if not errors: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
query_string = urlencode( | ||
{ | ||
"url": src_path, | ||
**render, | ||
}, | ||
doseq=True, | ||
) | ||
|
||
links += [ | ||
{ | ||
"href": factory.url_for( | ||
request, | ||
"tilejson", | ||
tileMatrixSetId="{tileMatrixSetId}", | ||
) | ||
+ "?" | ||
+ query_string, | ||
"rel": "tilesets-map", | ||
"title": f"tilejson file for {render_id}", | ||
"templated": True, | ||
}, | ||
{ | ||
"href": factory.url_for( | ||
request, | ||
"wmts", | ||
tileMatrixSetId="{tileMatrixSetId}", | ||
) | ||
+ "?" | ||
+ query_string, | ||
"rel": "tilesets-map", | ||
"title": f"WMTS service for {render_id}", | ||
"templated": True, | ||
}, | ||
] | ||
|
||
return { | ||
"params": render, | ||
|
@@ -139,11 +137,12 @@ def _prepare_render_item( | |
"/renders", | ||
response_model=RenderItemList, | ||
response_model_exclude_none=True, | ||
name="List STAC renders", | ||
name="List STAC Renders metadata", | ||
) | ||
def render_list(request: Request, src_path=Depends(factory.path_dependency)): | ||
alekzvik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
with factory.reader(src_path) as src: | ||
renders = src.item.properties.get("renders", {}) | ||
|
||
prepared_renders = { | ||
alekzvik marked this conversation as resolved.
Show resolved
Hide resolved
|
||
render_id: _prepare_render_item(render_id, render, request, src_path) | ||
for render_id, render in renders.items() | ||
|
@@ -155,7 +154,7 @@ def render_list(request: Request, src_path=Depends(factory.path_dependency)): | |
"href": str(request.url), | ||
"rel": "self", | ||
"type": "application/json", | ||
"title": "List Render Items", | ||
"title": "List STAC Renders metadata", | ||
}, | ||
], | ||
} | ||
|
@@ -164,7 +163,7 @@ def render_list(request: Request, src_path=Depends(factory.path_dependency)): | |
"/renders/{render_id}", | ||
response_model=RenderItemWithLinks, | ||
response_model_exclude_none=True, | ||
name="Show STAC render", | ||
name="STAC Renders metadata", | ||
) | ||
def render( | ||
request: Request, | ||
|
@@ -175,8 +174,10 @@ def render( | |
): | ||
with factory.reader(src_path) as src: | ||
renders = src.item.properties.get("renders", {}) | ||
if render_id not in renders: | ||
raise HTTPException(status_code=404, detail="Render not found") | ||
render = renders[render_id] | ||
|
||
return _prepare_render_item(render_id, render, request, src_path) | ||
if render_id not in renders: | ||
raise HTTPException(status_code=404, detail="Render not found") | ||
|
||
render = renders[render_id] | ||
|
||
return _prepare_render_item(render_id, render, request, src_path) |
There was a problem hiding this comment.
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.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.