Skip to content

Commit a15dc8f

Browse files
add xarray DatasetMetadataExtension (#1118)
* add xarray DatasetMetadataExtension * add /dataset prefix and refactor * add tests and deprecate VariablesExtension * revert and follow xpublish
1 parent 0f3e3c7 commit a15dc8f

7 files changed

Lines changed: 292 additions & 13 deletions

File tree

CHANGES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
### titiler.xarray
9292

9393
* change `get_variable.drop_dim` parameter type from `str` to `List[str]` **breaking change**
94+
* deprecate `VariablesExtension` extension
95+
* add `DatasetMetadataExtension` extension (`/dataset/keys`, `/dataset/` and `/dataset/dict` endpoints)
9496

9597
## 0.21.1 (2025-01-29)
9698

src/titiler/core/tests/test_utils.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""Test utils."""
22

3+
import pytest
4+
35
from titiler.core.dependencies import BidxParams
4-
from titiler.core.utils import deserialize_query_params, get_dependency_query_params
6+
from titiler.core.resources.enums import MediaType
7+
from titiler.core.utils import (
8+
accept_media_type,
9+
deserialize_query_params,
10+
get_dependency_query_params,
11+
)
512

613

714
def test_get_dependency_params():
@@ -65,3 +72,47 @@ def test_deserialize_query_params():
6572
)
6673
assert res == BidxParams(indexes=[1])
6774
assert not err
75+
76+
77+
@pytest.mark.parametrize(
78+
"media,accept,expected",
79+
[
80+
([MediaType.html], "text/html, application/json;q=0.8", MediaType.html),
81+
(
82+
[MediaType.html, MediaType.json],
83+
"application/json, text/html;q=0.8",
84+
MediaType.json,
85+
),
86+
([MediaType.xml], "application/json, text/html;q=0.8", None),
87+
([MediaType.json], "", None),
88+
(
89+
[MediaType.json, MediaType.html],
90+
"application/json;q=1.0, text/html;q=0.8",
91+
MediaType.json,
92+
),
93+
(
94+
[MediaType.json, MediaType.html],
95+
"application/json;q=1.0, text/html;q=1.0",
96+
MediaType.json,
97+
),
98+
(
99+
[MediaType.html, MediaType.json],
100+
"application/json;q=1.0, text/html;q=1.0",
101+
MediaType.html,
102+
),
103+
([MediaType.html, MediaType.json], "*;q=1.0", MediaType.html),
104+
(
105+
[MediaType.json, MediaType.html],
106+
"application/json;q=aaa, text/html",
107+
MediaType.html,
108+
),
109+
(
110+
[MediaType.json, MediaType.html],
111+
"application/json;q=0.0, text/html",
112+
MediaType.html,
113+
),
114+
],
115+
)
116+
def test_accept_media_type(media, accept, expected):
117+
"""test MetadataOutputType dependency."""
118+
assert accept_media_type(accept, media) == expected

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

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from rio_tiler.types import BBox, ColorMapType, IntervalTuple
1616
from rio_tiler.utils import linear_rescale, render
1717

18-
from titiler.core.resources.enums import ImageType
18+
from titiler.core.resources.enums import ImageType, MediaType
1919

2020

2121
def rescale_array(
@@ -183,3 +183,50 @@ def extract_query_params(dependencies, params) -> Tuple[ValidParams, Errors]:
183183
values.update(dep_values)
184184
errors += dep_errors
185185
return values, errors
186+
187+
188+
def accept_media_type(accept: str, mediatypes: List[MediaType]) -> Optional[MediaType]:
189+
"""Return MediaType based on accept header and available mediatype.
190+
191+
Links:
192+
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
193+
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
194+
195+
"""
196+
accept_values = {}
197+
for m in accept.replace(" ", "").split(","):
198+
values = m.split(";")
199+
if len(values) == 1:
200+
name = values[0]
201+
quality = 1.0
202+
else:
203+
name = values[0]
204+
groups = dict([param.split("=") for param in values[1:]]) # type: ignore
205+
try:
206+
q = groups.get("q")
207+
quality = float(q) if q else 1.0
208+
except ValueError:
209+
quality = 0
210+
211+
# if quality is 0 we ignore encoding
212+
if quality:
213+
accept_values[name] = quality
214+
215+
# Create Preference matrix
216+
media_preference = {
217+
v: [n for (n, q) in accept_values.items() if q == v]
218+
for v in sorted(set(accept_values.values()), reverse=True)
219+
}
220+
221+
# Loop through available compression and encoding preference
222+
for _, pref in media_preference.items():
223+
for media in mediatypes:
224+
if media.value in pref:
225+
return media
226+
227+
# If no specified encoding is supported but "*" is accepted,
228+
# take one of the available compressions.
229+
if "*" in accept_values and mediatypes:
230+
return mediatypes[0]
231+
232+
return None
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# /// script
2+
# dependencies = [
3+
# "titiler.xarray[full]",
4+
# "starlette_cramjam",
5+
# "uvicorn",
6+
# ]
7+
# ///
8+
"""Example of Application."""
9+
10+
from fastapi import FastAPI
11+
from starlette.middleware.cors import CORSMiddleware
12+
from starlette_cramjam.middleware import CompressionMiddleware
13+
14+
from titiler.core.errors import DEFAULT_STATUS_CODES, add_exception_handlers
15+
from titiler.core.factory import AlgorithmFactory, ColorMapFactory, TMSFactory
16+
from titiler.core.middleware import CacheControlMiddleware
17+
from titiler.xarray.extensions import DatasetMetadataExtension
18+
from titiler.xarray.factory import TilerFactory
19+
20+
app = FastAPI(
21+
title="TiTiler with support of Multidimensional dataset",
22+
openapi_url="/api",
23+
docs_url="/api.html",
24+
version="0.1.0",
25+
)
26+
27+
28+
md = TilerFactory(
29+
router_prefix="/md",
30+
extensions=[
31+
DatasetMetadataExtension(),
32+
],
33+
)
34+
app.include_router(md.router, prefix="/md", tags=["Multi Dimensional"])
35+
36+
# TileMatrixSets endpoints
37+
app.include_router(TMSFactory().router, tags=["Tiling Schemes"])
38+
39+
###############################################################################
40+
# Algorithms endpoints
41+
app.include_router(
42+
AlgorithmFactory().router,
43+
tags=["Algorithms"],
44+
)
45+
46+
# Colormaps endpoints
47+
app.include_router(
48+
ColorMapFactory().router,
49+
tags=["ColorMaps"],
50+
)
51+
52+
add_exception_handlers(app, DEFAULT_STATUS_CODES)
53+
54+
# Set all CORS enabled origins
55+
app.add_middleware(
56+
CORSMiddleware,
57+
allow_origins="*",
58+
allow_credentials=True,
59+
allow_methods=["GET"],
60+
allow_headers=["*"],
61+
)
62+
63+
app.add_middleware(
64+
CompressionMiddleware,
65+
minimum_size=0,
66+
exclude_mediatype={
67+
"image/jpeg",
68+
"image/jpg",
69+
"image/png",
70+
"image/jp2",
71+
"image/webp",
72+
},
73+
compression_level=6,
74+
)
75+
76+
app.add_middleware(
77+
CacheControlMiddleware,
78+
cachecontrol="public, max-age=3600",
79+
exclude_path={r"/healthz"},
80+
)
81+
82+
83+
if __name__ == "__main__":
84+
import uvicorn
85+
86+
uvicorn.run(app=app, host="127.0.0.1", port=8080, log_level="info")

src/titiler/xarray/tests/test_dependencies.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""test dependencies."""
22

3+
from typing import Annotated
4+
35
from fastapi import Depends, FastAPI, Path
46
from starlette.testclient import TestClient
5-
from typing_extensions import Annotated
67

78
from titiler.xarray import dependencies
89

src/titiler/xarray/tests/test_factory.py

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from rasterio.io import MemoryFile
88
from starlette.testclient import TestClient
99

10-
from titiler.xarray.extensions import VariablesExtension
10+
from titiler.xarray.extensions import DatasetMetadataExtension, VariablesExtension
1111
from titiler.xarray.factory import TilerFactory
1212

1313
prefix = os.path.join(os.path.dirname(__file__), "fixtures")
@@ -19,16 +19,26 @@
1919
zarr_pyramid = os.path.join(prefix, "pyramid.zarr")
2020

2121

22+
def test_deprecated_extension():
23+
"""Test factory with options."""
24+
"""Test TilerFactory class."""
25+
with pytest.warns(DeprecationWarning):
26+
md = TilerFactory(extensions=[VariablesExtension()])
27+
assert len(md.router.routes) == 20
28+
29+
2230
def test_tiler_factory():
2331
"""Test factory with options."""
2432
"""Test TilerFactory class."""
2533
md = TilerFactory(
26-
add_viewer=False, add_part=False, extensions=[VariablesExtension()]
34+
add_viewer=False,
35+
add_part=False,
36+
extensions=[DatasetMetadataExtension()],
2737
)
28-
assert len(md.router.routes) == 14
38+
assert len(md.router.routes) == 16
2939

30-
md = TilerFactory(router_prefix="/md", extensions=[VariablesExtension()])
31-
assert len(md.router.routes) == 20
40+
md = TilerFactory(router_prefix="/md", extensions=[DatasetMetadataExtension()])
41+
assert len(md.router.routes) == 22
3242

3343
app = FastAPI()
3444
app.include_router(md.router, prefix="/md")
@@ -44,8 +54,8 @@ def test_tiler_factory():
4454
@pytest.fixture
4555
def app():
4656
"""App fixture."""
47-
md = TilerFactory(router_prefix="/md", extensions=[VariablesExtension()])
48-
assert len(md.router.routes) == 20
57+
md = TilerFactory(router_prefix="/md", extensions=[DatasetMetadataExtension()])
58+
assert len(md.router.routes) == 22
4959

5060
app = FastAPI()
5161
app.include_router(md.router, prefix="/md")
@@ -57,13 +67,22 @@ def app():
5767
"filename",
5868
[dataset_2d_nc, dataset_3d_nc, dataset_3d_zarr],
5969
)
60-
def test_variable(filename, app):
61-
"""Test /variables endpoint."""
62-
resp = app.get("/md/variables", params={"url": filename})
70+
def test_dataset_extension(filename, app):
71+
"""Test /dataset endpoints."""
72+
resp = app.get("/md/dataset/keys", params={"url": filename})
6373
assert resp.status_code == 200
6474
assert resp.headers["content-type"] == "application/json"
6575
assert resp.json() == ["dataset"]
6676

77+
resp = app.get("/md/dataset/dict", params={"url": filename})
78+
assert resp.status_code == 200
79+
assert resp.headers["content-type"] == "application/json"
80+
assert resp.json()["data_vars"]["dataset"]
81+
82+
resp = app.get("/md/dataset/", params={"url": filename})
83+
assert resp.status_code == 200
84+
assert "text/html" in resp.headers["content-type"]
85+
6786

6887
@pytest.mark.parametrize(
6988
"filename",

0 commit comments

Comments
 (0)