3
3
import json
4
4
import logging
5
5
from logging import config as log_config
6
+ from typing import Annotated , Literal , Optional
6
7
7
8
import jinja2
8
9
import rasterio
9
- from fastapi import Depends , FastAPI , HTTPException , Security
10
+ from fastapi import Depends , FastAPI , HTTPException , Query , Security
10
11
from fastapi .security .api_key import APIKeyQuery
11
12
from rio_tiler .io import Reader , STACReader
12
13
from starlette .middleware .cors import CORSMiddleware
13
14
from starlette .requests import Request
14
- from starlette .responses import HTMLResponse
15
15
from starlette .templating import Jinja2Templates
16
16
from starlette_cramjam .middleware import CompressionMiddleware
17
17
31
31
LowerCaseQueryStringMiddleware ,
32
32
TotalTimeMiddleware ,
33
33
)
34
+ from titiler .core .models .OGC import Conformance , Landing
35
+ from titiler .core .resources .enums import MediaType
36
+ from titiler .core .templating import create_html_response
37
+ from titiler .core .utils import accept_media_type , update_openapi
34
38
from titiler .extensions import (
35
39
cogValidateExtension ,
36
40
cogViewerExtension ,
@@ -97,6 +101,18 @@ def validate_access_token(access_token: str = Security(api_key_query)):
97
101
dependencies = app_dependencies ,
98
102
)
99
103
104
+ # Fix OpenAPI response header for OGC Common compatibility
105
+ update_openapi (app )
106
+
107
+ TITILER_CONFORMS_TO = {
108
+ "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/core" ,
109
+ "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/landing-page" ,
110
+ "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/oas30" ,
111
+ "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/html" ,
112
+ "http://www.opengis.net/spec/ogcapi-common-1/1.0/req/json" ,
113
+ }
114
+
115
+
100
116
###############################################################################
101
117
# Simple Dataset endpoints (e.g Cloud Optimized GeoTIFF)
102
118
if not api_settings .disable_cog :
@@ -116,6 +132,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
116
132
tags = ["Cloud Optimized GeoTIFF" ],
117
133
)
118
134
135
+ TITILER_CONFORMS_TO .update (cog .conforms_to )
119
136
120
137
###############################################################################
121
138
# STAC endpoints
@@ -135,6 +152,8 @@ def validate_access_token(access_token: str = Security(api_key_query)):
135
152
tags = ["SpatioTemporal Asset Catalog" ],
136
153
)
137
154
155
+ TITILER_CONFORMS_TO .update (stac .conforms_to )
156
+
138
157
###############################################################################
139
158
# Mosaic endpoints
140
159
if not api_settings .disable_mosaic :
@@ -145,13 +164,16 @@ def validate_access_token(access_token: str = Security(api_key_query)):
145
164
tags = ["MosaicJSON" ],
146
165
)
147
166
167
+ TITILER_CONFORMS_TO .update (mosaic .conforms_to )
168
+
148
169
###############################################################################
149
170
# TileMatrixSets endpoints
150
171
tms = TMSFactory ()
151
172
app .include_router (
152
173
tms .router ,
153
174
tags = ["Tiling Schemes" ],
154
175
)
176
+ TITILER_CONFORMS_TO .update (tms .conforms_to )
155
177
156
178
###############################################################################
157
179
# Algorithms endpoints
@@ -160,6 +182,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
160
182
algorithms .router ,
161
183
tags = ["Algorithms" ],
162
184
)
185
+ TITILER_CONFORMS_TO .update (algorithms .conforms_to )
163
186
164
187
###############################################################################
165
188
# Colormaps endpoints
@@ -168,6 +191,7 @@ def validate_access_token(access_token: str = Security(api_key_query)):
168
191
cmaps .router ,
169
192
tags = ["ColorMaps" ],
170
193
)
194
+ TITILER_CONFORMS_TO .update (cmaps .conforms_to )
171
195
172
196
173
197
add_exception_handlers (app , DEFAULT_STATUS_CODES )
@@ -289,30 +313,33 @@ def application_health_check():
289
313
}
290
314
291
315
292
- @app .get ("/" , response_class = HTMLResponse , include_in_schema = False )
293
- def landing (request : Request ):
316
+ @app .get (
317
+ "/" ,
318
+ response_model = Landing ,
319
+ response_model_exclude_none = True ,
320
+ responses = {
321
+ 200 : {
322
+ "content" : {
323
+ "text/html" : {},
324
+ "application/json" : {},
325
+ }
326
+ },
327
+ },
328
+ tags = ["OGC Common" ],
329
+ )
330
+ def landing (
331
+ request : Request ,
332
+ f : Annotated [
333
+ Optional [Literal ["html" , "json" ]],
334
+ Query (
335
+ description = "Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
336
+ ),
337
+ ] = None ,
338
+ ):
294
339
"""TiTiler landing page."""
295
- urlpath = request .url .path
296
- if root_path := request .scope .get ("root_path" ):
297
- urlpath = urlpath .removeprefix (root_path )
298
-
299
- crumbs = []
300
- baseurl = str (request .base_url ).rstrip ("/" )
301
-
302
- crumbpath = str (baseurl )
303
- if urlpath == "/" :
304
- urlpath = ""
305
-
306
- for crumb in urlpath .split ("/" ):
307
- crumbpath = crumbpath .rstrip ("/" )
308
- part = crumb
309
- if part is None or part == "" :
310
- part = "Home"
311
- crumbpath += f"/{ crumb } "
312
- crumbs .append ({"url" : crumbpath .rstrip ("/" ), "part" : part .capitalize ()})
313
-
314
340
data = {
315
- "title" : "titiler" ,
341
+ "title" : "TiTiler" ,
342
+ "description" : "A modern dynamic tile server built on top of FastAPI and Rasterio/GDAL." ,
316
343
"links" : [
317
344
{
318
345
"title" : "Landing page" ,
@@ -321,17 +348,23 @@ def landing(request: Request):
321
348
"rel" : "self" ,
322
349
},
323
350
{
324
- "title" : "the API definition (JSON)" ,
351
+ "title" : "The API definition (JSON)" ,
325
352
"href" : str (request .url_for ("openapi" )),
326
353
"type" : "application/vnd.oai.openapi+json;version=3.0" ,
327
354
"rel" : "service-desc" ,
328
355
},
329
356
{
330
- "title" : "the API documentation" ,
357
+ "title" : "The API documentation" ,
331
358
"href" : str (request .url_for ("swagger_ui_html" )),
332
359
"type" : "text/html" ,
333
360
"rel" : "service-doc" ,
334
361
},
362
+ {
363
+ "title" : "Conformance Declaration" ,
364
+ "href" : str (request .url_for ("conformance" )),
365
+ "type" : "text/html" ,
366
+ "rel" : "http://www.opengis.net/def/rel/ogc/1.0/conformance" ,
367
+ },
335
368
{
336
369
"title" : "TiTiler Documentation (external link)" ,
337
370
"href" : "https://developmentseed.org/titiler/" ,
@@ -347,16 +380,74 @@ def landing(request: Request):
347
380
],
348
381
}
349
382
350
- return templates .TemplateResponse (
351
- "index.html" ,
352
- {
353
- "request" : request ,
354
- "response" : data ,
355
- "template" : {
356
- "api_root" : baseurl ,
357
- "params" : request .query_params ,
358
- "title" : "TiTiler" ,
359
- },
360
- "crumbs" : crumbs ,
383
+ output_type : Optional [MediaType ]
384
+ if f :
385
+ output_type = MediaType [f ]
386
+ else :
387
+ accepted_media = [MediaType .html , MediaType .json ]
388
+ output_type = accept_media_type (
389
+ request .headers .get ("accept" , "" ), accepted_media
390
+ )
391
+
392
+ if output_type == MediaType .html :
393
+ return create_html_response (
394
+ request ,
395
+ data ,
396
+ title = "TiTiler" ,
397
+ template_name = "landing" ,
398
+ )
399
+
400
+ return data
401
+
402
+
403
+ @app .get (
404
+ "/conformance" ,
405
+ response_model = Conformance ,
406
+ response_model_exclude_none = True ,
407
+ responses = {
408
+ 200 : {
409
+ "content" : {
410
+ "text/html" : {},
411
+ "application/json" : {},
412
+ }
361
413
},
362
- )
414
+ },
415
+ tags = ["OGC Common" ],
416
+ )
417
+ def conformance (
418
+ request : Request ,
419
+ f : Annotated [
420
+ Optional [Literal ["html" , "json" ]],
421
+ Query (
422
+ description = "Response MediaType. Defaults to endpoint's default or value defined in `accept` header."
423
+ ),
424
+ ] = None ,
425
+ ):
426
+ """Conformance classes.
427
+
428
+ Called with `GET /conformance`.
429
+
430
+ Returns:
431
+ Conformance classes which the server conforms to.
432
+
433
+ """
434
+ data = {"conformsTo" : sorted (TITILER_CONFORMS_TO )}
435
+
436
+ output_type : Optional [MediaType ]
437
+ if f :
438
+ output_type = MediaType [f ]
439
+ else :
440
+ accepted_media = [MediaType .html , MediaType .json ]
441
+ output_type = accept_media_type (
442
+ request .headers .get ("accept" , "" ), accepted_media
443
+ )
444
+
445
+ if output_type == MediaType .html :
446
+ return create_html_response (
447
+ request ,
448
+ data ,
449
+ title = "Conformance" ,
450
+ template_name = "conformance" ,
451
+ )
452
+
453
+ return data
0 commit comments