Skip to content

Commit 4f7b8dd

Browse files
refactor and add more templates (#1227)
* refactor and add more templates * fix for tests * use colormap image in html preview * fix tilesets
1 parent 80fdcdd commit 4f7b8dd

33 files changed

Lines changed: 1185 additions & 363 deletions

CHANGES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616

1717
* add `description` in `ApiSettings`
1818

19+
### titiler.core
20+
21+
* delete `titiler.core.templating` submodule **breaking change**
22+
* move `create_html_response` function to `titiler.core.utils` submodule
23+
* move all HTML templates in `titiler/core/templates` directory **breaking change**
24+
* add HTML responses for tilesets, tilematrixsets, algorithms and colormaps endpoints
25+
* rename response model `ColorMapsList` -> `ColorMapList` and change it's attibutes to `colormaps` **breaking change**
26+
* add `templates` in the `BaseFactory` class definition
1927

2028
## 0.23.1 (2025-08-27)
2129

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

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
from logging import config as log_config
66
from typing import Annotated, Literal, Optional
77

8+
import jinja2
89
import rasterio
910
from fastapi import Depends, FastAPI, HTTPException, Query, Security
1011
from fastapi.security.api_key import APIKeyQuery
1112
from rio_tiler.io import Reader, STACReader
1213
from starlette.middleware.cors import CORSMiddleware
1314
from starlette.requests import Request
15+
from starlette.templating import Jinja2Templates
1416
from starlette_cramjam.middleware import CompressionMiddleware
1517

1618
from titiler.application import __version__ as titiler_version
@@ -31,8 +33,7 @@
3133
)
3234
from titiler.core.models.OGC import Conformance, Landing
3335
from titiler.core.resources.enums import MediaType
34-
from titiler.core.templating import create_html_response
35-
from titiler.core.utils import accept_media_type, update_openapi
36+
from titiler.core.utils import accept_media_type, create_html_response, update_openapi
3637
from titiler.extensions import (
3738
cogValidateExtension,
3839
cogViewerExtension,
@@ -48,9 +49,24 @@
4849
logging.getLogger("rasterio.session").setLevel(logging.ERROR)
4950
logging.getLogger("rio-tiler").setLevel(logging.ERROR)
5051

51-
5252
api_settings = ApiSettings()
5353

54+
# custom template directory
55+
templates_location = (
56+
[jinja2.FileSystemLoader(api_settings.template_directory)]
57+
if api_settings.template_directory
58+
else []
59+
)
60+
# default template directory
61+
templates_location.append(jinja2.PackageLoader("titiler.core", "templates"))
62+
63+
jinja2_env = jinja2.Environment(
64+
autoescape=jinja2.select_autoescape(["html", "xml"]),
65+
loader=jinja2.ChoiceLoader(templates_location),
66+
)
67+
titiler_templates = Jinja2Templates(env=jinja2_env)
68+
69+
5470
app_dependencies = []
5571
if api_settings.global_access_token:
5672
###############################################################################
@@ -111,6 +127,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
111127
stacExtension(),
112128
],
113129
enable_telemetry=api_settings.telemetry_enabled,
130+
templates=titiler_templates,
114131
)
115132

116133
app.include_router(
@@ -133,6 +150,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
133150
stacRenderExtension(),
134151
],
135152
enable_telemetry=api_settings.telemetry_enabled,
153+
templates=titiler_templates,
136154
)
137155

138156
app.include_router(
@@ -149,6 +167,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
149167
mosaic = MosaicTilerFactory(
150168
router_prefix="/mosaicjson",
151169
enable_telemetry=api_settings.telemetry_enabled,
170+
templates=titiler_templates,
152171
)
153172
app.include_router(
154173
mosaic.router,
@@ -160,7 +179,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
160179

161180
###############################################################################
162181
# TileMatrixSets endpoints
163-
tms = TMSFactory()
182+
tms = TMSFactory(templates=titiler_templates)
164183
app.include_router(
165184
tms.router,
166185
tags=["Tiling Schemes"],
@@ -169,7 +188,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
169188

170189
###############################################################################
171190
# Algorithms endpoints
172-
algorithms = AlgorithmFactory()
191+
algorithms = AlgorithmFactory(templates=titiler_templates)
173192
app.include_router(
174193
algorithms.router,
175194
tags=["Algorithms"],
@@ -178,7 +197,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
178197

179198
###############################################################################
180199
# Colormaps endpoints
181-
cmaps = ColorMapFactory()
200+
cmaps = ColorMapFactory(templates=titiler_templates)
182201
app.include_router(
183202
cmaps.router,
184203
tags=["ColorMaps"],
@@ -365,6 +384,18 @@ def landing(
365384
"type": "application/json",
366385
"rel": "http://www.opengis.net/def/rel/ogc/1.0/tiling-schemes",
367386
},
387+
{
388+
"title": "List of Available Algorithms",
389+
"href": str(request.url_for("available_algorithms")),
390+
"type": "application/json",
391+
"rel": "data",
392+
},
393+
{
394+
"title": "List of Available ColorMaps",
395+
"href": str(request.url_for("available_colormaps")),
396+
"type": "application/json",
397+
"rel": "data",
398+
},
368399
{
369400
"title": "TiTiler Documentation (external link)",
370401
"href": "https://developmentseed.org/titiler/",
@@ -380,13 +411,13 @@ def landing(
380411
],
381412
}
382413

383-
output_type: Optional[MediaType]
384414
if f:
385415
output_type = MediaType[f]
386416
else:
387417
accepted_media = [MediaType.html, MediaType.json]
388-
output_type = accept_media_type(
389-
request.headers.get("accept", ""), accepted_media
418+
output_type = (
419+
accept_media_type(request.headers.get("accept", ""), accepted_media)
420+
or MediaType.json
390421
)
391422

392423
if output_type == MediaType.html:
@@ -395,6 +426,7 @@ def landing(
395426
data,
396427
title="TiTiler",
397428
template_name="landing",
429+
templates=titiler_templates,
398430
)
399431

400432
return data
@@ -433,13 +465,13 @@ def conformance(
433465
"""
434466
data = {"conformsTo": sorted(TITILER_CONFORMS_TO)}
435467

436-
output_type: Optional[MediaType]
437468
if f:
438469
output_type = MediaType[f]
439470
else:
440471
accepted_media = [MediaType.html, MediaType.json]
441-
output_type = accept_media_type(
442-
request.headers.get("accept", ""), accepted_media
472+
output_type = (
473+
accept_media_type(request.headers.get("accept", ""), accepted_media)
474+
or MediaType.json
443475
)
444476

445477
if output_type == MediaType.html:
@@ -448,6 +480,7 @@ def conformance(
448480
data,
449481
title="Conformance",
450482
template_name="conformance",
483+
templates=titiler_templates,
451484
)
452485

453486
return data

src/titiler/application/titiler/application/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ class ApiSettings(BaseSettings):
2727
root_path: str = ""
2828
debug: bool = False
2929

30+
template_directory: Optional[str] = None
31+
3032
disable_cog: bool = False
3133
disable_stac: bool = False
3234
disable_mosaic: bool = False

src/titiler/core/tests/test_factories.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1706,11 +1706,28 @@ def test_algorithm():
17061706

17071707
response = client.get("/algorithms")
17081708
assert response.status_code == 200
1709-
assert "hillshade" in response.json()
1709+
algo_ids = [algo["id"] for algo in response.json()["algorithms"]]
1710+
assert "hillshade" in algo_ids
1711+
1712+
response = client.get("/algorithms", params={"f": "html"})
1713+
assert response.status_code == 200
1714+
assert "text/html" in response.headers["content-type"]
1715+
1716+
response = client.get("/algorithms", headers={"Accept": "text/html"})
1717+
assert response.status_code == 200
1718+
assert "text/html" in response.headers["content-type"]
17101719

17111720
response = client.get("/algorithms/hillshade")
17121721
assert response.status_code == 200
17131722

1723+
response = client.get("/algorithms/hillshade", params={"f": "html"})
1724+
assert response.status_code == 200
1725+
assert "text/html" in response.headers["content-type"]
1726+
1727+
response = client.get("/algorithms/hillshade", headers={"Accept": "text/html"})
1728+
assert response.status_code == 200
1729+
assert "text/html" in response.headers["content-type"]
1730+
17141731

17151732
def test_path_param_in_prefix():
17161733
"""Test path params in prefix."""
@@ -1954,14 +1971,23 @@ def test_colormap_factory():
19541971

19551972
response = client.get("/colorMaps")
19561973
assert response.status_code == 200
1957-
assert "cust" in response.json()["colorMaps"]
1958-
assert "negative" in response.json()["colorMaps"]
1959-
assert "seq" in response.json()["colorMaps"]
1960-
assert "viridis" in response.json()["colorMaps"]
1974+
cmap_ids = [cm["id"] for cm in response.json()["colormaps"]]
1975+
assert "cust" in cmap_ids
1976+
assert "negative" in cmap_ids
1977+
assert "seq" in cmap_ids
1978+
assert "viridis" in cmap_ids
1979+
1980+
response = client.get("/colorMaps", headers={"Accept": "text/html"})
1981+
assert response.status_code == 200
1982+
assert "text/html" in response.headers["content-type"]
19611983

19621984
response = client.get("/colorMaps/viridis")
19631985
assert response.status_code == 200
19641986

1987+
response = client.get("/colorMaps/viridis", headers={"Accept": "text/html"})
1988+
assert response.status_code == 200
1989+
assert "text/html" in response.headers["content-type"]
1990+
19651991
response = client.get("/colorMaps/cust")
19661992
assert response.status_code == 200
19671993

@@ -1974,7 +2000,7 @@ def test_colormap_factory():
19742000
response = client.get("/colorMaps/yo")
19752001
assert response.status_code == 422
19762002

1977-
response = client.get("/colorMaps/viridis", params={"format": "png"})
2003+
response = client.get("/colorMaps/viridis", params={"f": "png"})
19782004
assert response.status_code == 200
19792005
meta = parse_img(response.content)
19802006
assert meta["dtype"] == "uint8"
@@ -1983,7 +2009,7 @@ def test_colormap_factory():
19832009
assert meta["height"] == 20
19842010

19852011
response = client.get(
1986-
"/colorMaps/viridis", params={"format": "png", "orientation": "vertical"}
2012+
"/colorMaps/viridis", params={"f": "png", "orientation": "vertical"}
19872013
)
19882014
assert response.status_code == 200
19892015
meta = parse_img(response.content)
@@ -1993,7 +2019,7 @@ def test_colormap_factory():
19932019
assert meta["height"] == 256
19942020

19952021
response = client.get(
1996-
"/colorMaps/viridis", params={"format": "png", "width": 1000, "height": 100}
2022+
"/colorMaps/viridis", params={"f": "png", "width": 1000, "height": 100}
19972023
)
19982024
assert response.status_code == 200
19992025
meta = parse_img(response.content)
@@ -2002,7 +2028,7 @@ def test_colormap_factory():
20022028
assert meta["width"] == 1000
20032029
assert meta["height"] == 100
20042030

2005-
response = client.get("/colorMaps/cust", params={"format": "png"})
2031+
response = client.get("/colorMaps/cust", params={"f": "png"})
20062032
assert response.status_code == 200
20072033
meta = parse_img(response.content)
20082034
assert meta["dtype"] == "uint8"
@@ -2011,7 +2037,7 @@ def test_colormap_factory():
20112037
assert meta["height"] == 20
20122038

20132039
response = client.get(
2014-
"/colorMaps/cust", params={"format": "png", "orientation": "vertical"}
2040+
"/colorMaps/cust", params={"f": "png", "orientation": "vertical"}
20152041
)
20162042
assert response.status_code == 200
20172043
meta = parse_img(response.content)
@@ -2020,7 +2046,7 @@ def test_colormap_factory():
20202046
assert meta["width"] == 20
20212047
assert meta["height"] == 256
20222048

2023-
response = client.get("/colorMaps/negative", params={"format": "png"})
2049+
response = client.get("/colorMaps/negative", params={"f": "png"})
20242050
assert response.status_code == 200
20252051
meta = parse_img(response.content)
20262052
assert meta["dtype"] == "uint8"
@@ -2029,7 +2055,7 @@ def test_colormap_factory():
20292055
assert meta["height"] == 20
20302056

20312057
response = client.get(
2032-
"/colorMaps/negative", params={"format": "png", "orientation": "vertical"}
2058+
"/colorMaps/negative", params={"f": "png", "orientation": "vertical"}
20332059
)
20342060
assert response.status_code == 200
20352061
meta = parse_img(response.content)
@@ -2038,7 +2064,7 @@ def test_colormap_factory():
20382064
assert meta["width"] == 20
20392065
assert meta["height"] == 256
20402066

2041-
response = client.get("/colorMaps/seq", params={"format": "png"})
2067+
response = client.get("/colorMaps/seq", params={"f": "png"})
20422068
assert response.status_code == 200
20432069
meta = parse_img(response.content)
20442070
assert meta["dtype"] == "uint8"
@@ -2047,7 +2073,7 @@ def test_colormap_factory():
20472073
assert meta["height"] == 20
20482074

20492075
response = client.get(
2050-
"/colorMaps/seq", params={"format": "png", "orientation": "vertical"}
2076+
"/colorMaps/seq", params={"f": "png", "orientation": "vertical"}
20512077
)
20522078
assert response.status_code == 200
20532079
meta = parse_img(response.content)

src/titiler/core/titiler/core/algorithm/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@
99
from pydantic import ValidationError
1010
from typing_extensions import Annotated
1111

12-
from titiler.core.algorithm.base import AlgorithmMetadata # noqa
13-
from titiler.core.algorithm.base import BaseAlgorithm
12+
from titiler.core.algorithm.base import ( # noqa
13+
AlgorithmMetadata,
14+
AlgorithmtList,
15+
BaseAlgorithm,
16+
)
1417
from titiler.core.algorithm.dem import Contours, HillShade, Slope, TerrainRGB, Terrarium
1518
from titiler.core.algorithm.index import NormalizedIndex
1619
from titiler.core.algorithm.math import _Max, _Mean, _Median, _Min, _Std, _Sum, _Var

src/titiler/core/titiler/core/algorithm/base.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Algorithm base class."""
22

33
import abc
4-
from typing import Dict, Optional, Sequence
4+
from typing import Dict, List, Optional, Sequence
55

66
from pydantic import BaseModel
77
from rio_tiler.models import ImageData
88

9+
from titiler.core.models.common import Link
10+
911

1012
class BaseAlgorithm(BaseModel, metaclass=abc.ABCMeta):
1113
"""Algorithm baseclass.
@@ -38,3 +40,17 @@ class AlgorithmMetadata(BaseModel):
3840
inputs: Dict
3941
outputs: Dict
4042
parameters: Dict
43+
44+
45+
class AlgorithmRef(BaseModel):
46+
"""AlgorithmRef model."""
47+
48+
id: str
49+
title: Optional[str] = None
50+
links: List[Link]
51+
52+
53+
class AlgorithmtList(BaseModel):
54+
"""AlgorithmList model."""
55+
56+
algorithms: List[AlgorithmRef]

0 commit comments

Comments
 (0)