eodag 2.12.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eodag/__init__.py +6 -8
- eodag/api/core.py +654 -538
- eodag/api/product/__init__.py +12 -2
- eodag/api/product/_assets.py +59 -16
- eodag/api/product/_product.py +100 -93
- eodag/api/product/drivers/__init__.py +7 -2
- eodag/api/product/drivers/base.py +0 -3
- eodag/api/product/metadata_mapping.py +192 -96
- eodag/api/search_result.py +69 -10
- eodag/cli.py +55 -25
- eodag/config.py +391 -116
- eodag/plugins/apis/base.py +11 -165
- eodag/plugins/apis/ecmwf.py +36 -25
- eodag/plugins/apis/usgs.py +80 -35
- eodag/plugins/authentication/aws_auth.py +13 -4
- eodag/plugins/authentication/base.py +10 -1
- eodag/plugins/authentication/generic.py +2 -2
- eodag/plugins/authentication/header.py +31 -6
- eodag/plugins/authentication/keycloak.py +17 -84
- eodag/plugins/authentication/oauth.py +3 -3
- eodag/plugins/authentication/openid_connect.py +268 -49
- eodag/plugins/authentication/qsauth.py +4 -1
- eodag/plugins/authentication/sas_auth.py +9 -2
- eodag/plugins/authentication/token.py +98 -47
- eodag/plugins/authentication/token_exchange.py +122 -0
- eodag/plugins/crunch/base.py +3 -1
- eodag/plugins/crunch/filter_date.py +3 -9
- eodag/plugins/crunch/filter_latest_intersect.py +0 -3
- eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
- eodag/plugins/crunch/filter_overlap.py +4 -8
- eodag/plugins/crunch/filter_property.py +5 -11
- eodag/plugins/download/aws.py +149 -185
- eodag/plugins/download/base.py +88 -97
- eodag/plugins/download/creodias_s3.py +1 -1
- eodag/plugins/download/http.py +638 -310
- eodag/plugins/download/s3rest.py +47 -45
- eodag/plugins/manager.py +228 -88
- eodag/plugins/search/__init__.py +36 -0
- eodag/plugins/search/base.py +239 -30
- eodag/plugins/search/build_search_result.py +382 -37
- eodag/plugins/search/cop_marine.py +441 -0
- eodag/plugins/search/creodias_s3.py +25 -20
- eodag/plugins/search/csw.py +5 -7
- eodag/plugins/search/data_request_search.py +61 -30
- eodag/plugins/search/qssearch.py +713 -255
- eodag/plugins/search/static_stac_search.py +106 -40
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1921 -34
- eodag/resources/providers.yml +4091 -3655
- eodag/resources/stac.yml +50 -216
- eodag/resources/stac_api.yml +71 -25
- eodag/resources/stac_provider.yml +5 -0
- eodag/resources/user_conf_template.yml +89 -32
- eodag/rest/__init__.py +6 -0
- eodag/rest/cache.py +70 -0
- eodag/rest/config.py +68 -0
- eodag/rest/constants.py +26 -0
- eodag/rest/core.py +735 -0
- eodag/rest/errors.py +178 -0
- eodag/rest/server.py +264 -431
- eodag/rest/stac.py +442 -836
- eodag/rest/types/collections_search.py +44 -0
- eodag/rest/types/eodag_search.py +238 -47
- eodag/rest/types/queryables.py +164 -0
- eodag/rest/types/stac_search.py +273 -0
- eodag/rest/utils/__init__.py +216 -0
- eodag/rest/utils/cql_evaluate.py +119 -0
- eodag/rest/utils/rfc3339.py +64 -0
- eodag/types/__init__.py +106 -10
- eodag/types/bbox.py +15 -14
- eodag/types/download_args.py +40 -0
- eodag/types/search_args.py +57 -7
- eodag/types/whoosh.py +79 -0
- eodag/utils/__init__.py +110 -91
- eodag/utils/constraints.py +37 -45
- eodag/utils/exceptions.py +39 -22
- eodag/utils/import_system.py +0 -4
- eodag/utils/logging.py +37 -80
- eodag/utils/notebook.py +4 -4
- eodag/utils/repr.py +113 -0
- eodag/utils/requests.py +128 -0
- eodag/utils/rest.py +100 -0
- eodag/utils/stac_reader.py +93 -21
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
- eodag-3.0.0.dist-info/RECORD +109 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
- eodag/plugins/apis/cds.py +0 -540
- eodag/rest/types/stac_queryables.py +0 -134
- eodag/rest/utils.py +0 -1133
- eodag-2.12.0.dist-info/RECORD +0 -94
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
- {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
eodag/rest/server.py
CHANGED
|
@@ -19,9 +19,10 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
-
import
|
|
22
|
+
import re
|
|
23
23
|
from contextlib import asynccontextmanager
|
|
24
24
|
from importlib.metadata import version
|
|
25
|
+
from json import JSONDecodeError
|
|
25
26
|
from typing import (
|
|
26
27
|
TYPE_CHECKING,
|
|
27
28
|
Any,
|
|
@@ -29,54 +30,45 @@ from typing import (
|
|
|
29
30
|
Awaitable,
|
|
30
31
|
Callable,
|
|
31
32
|
Dict,
|
|
32
|
-
List,
|
|
33
33
|
Optional,
|
|
34
|
-
Union,
|
|
35
34
|
)
|
|
36
35
|
|
|
37
36
|
from fastapi import APIRouter as FastAPIRouter
|
|
38
37
|
from fastapi import FastAPI, HTTPException, Request
|
|
39
|
-
from fastapi.
|
|
38
|
+
from fastapi.concurrency import run_in_threadpool
|
|
40
39
|
from fastapi.middleware.cors import CORSMiddleware
|
|
41
40
|
from fastapi.openapi.utils import get_openapi
|
|
42
|
-
from fastapi.responses import ORJSONResponse
|
|
43
|
-
from pydantic import
|
|
44
|
-
from
|
|
41
|
+
from fastapi.responses import ORJSONResponse
|
|
42
|
+
from pydantic import ValidationError as pydanticValidationError
|
|
43
|
+
from pygeofilter.backends.cql2_json import to_cql2
|
|
44
|
+
from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
|
|
45
45
|
|
|
46
46
|
from eodag.config import load_stac_api_config
|
|
47
|
-
from eodag.rest.
|
|
48
|
-
from eodag.rest.
|
|
49
|
-
|
|
47
|
+
from eodag.rest.cache import init_cache
|
|
48
|
+
from eodag.rest.core import (
|
|
49
|
+
all_collections,
|
|
50
|
+
download_stac_item,
|
|
50
51
|
eodag_api_init,
|
|
51
|
-
|
|
52
|
+
get_collection,
|
|
52
53
|
get_detailled_collections_list,
|
|
54
|
+
get_queryables,
|
|
53
55
|
get_stac_api_version,
|
|
54
56
|
get_stac_catalogs,
|
|
55
|
-
get_stac_collection_by_id,
|
|
56
|
-
get_stac_collections,
|
|
57
57
|
get_stac_conformance,
|
|
58
58
|
get_stac_extension_oseo,
|
|
59
|
-
get_stac_item_by_id,
|
|
60
59
|
search_stac_items,
|
|
61
60
|
)
|
|
62
|
-
from eodag.
|
|
63
|
-
from eodag.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
NoMatchingProductType,
|
|
68
|
-
NotAvailableError,
|
|
69
|
-
RequestError,
|
|
70
|
-
TimeOutError,
|
|
71
|
-
UnsupportedProductType,
|
|
72
|
-
UnsupportedProvider,
|
|
73
|
-
ValidationError,
|
|
74
|
-
)
|
|
61
|
+
from eodag.rest.errors import add_exception_handlers
|
|
62
|
+
from eodag.rest.types.queryables import QueryablesGetParams
|
|
63
|
+
from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
|
|
64
|
+
from eodag.rest.utils import format_pydantic_error, str2json, str2list
|
|
65
|
+
from eodag.utils import parse_header, update_nested_dict
|
|
75
66
|
|
|
76
67
|
if TYPE_CHECKING:
|
|
77
68
|
from fastapi.types import DecoratedCallable
|
|
78
69
|
from requests import Response
|
|
79
70
|
|
|
71
|
+
from starlette.responses import Response as StarletteResponse
|
|
80
72
|
|
|
81
73
|
logger = logging.getLogger("eodag.rest.server")
|
|
82
74
|
|
|
@@ -118,6 +110,7 @@ router = APIRouter()
|
|
|
118
110
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
119
111
|
"""API init and tear-down"""
|
|
120
112
|
eodag_api_init()
|
|
113
|
+
init_cache(app)
|
|
121
114
|
yield
|
|
122
115
|
|
|
123
116
|
|
|
@@ -127,14 +120,16 @@ app = FastAPI(lifespan=lifespan, title="EODAG", docs_url="/api.html")
|
|
|
127
120
|
stac_api_config = load_stac_api_config()
|
|
128
121
|
|
|
129
122
|
|
|
130
|
-
@router.
|
|
131
|
-
|
|
123
|
+
@router.api_route(
|
|
124
|
+
methods=["GET", "HEAD"], path="/api", tags=["Capabilities"], include_in_schema=False
|
|
125
|
+
)
|
|
126
|
+
async def eodag_openapi(request: Request) -> Dict[str, Any]:
|
|
132
127
|
"""Customized openapi"""
|
|
133
128
|
logger.debug("URL: /api")
|
|
134
129
|
if app.openapi_schema:
|
|
135
130
|
return app.openapi_schema
|
|
136
131
|
|
|
137
|
-
root_catalog = get_stac_catalogs(url=""
|
|
132
|
+
root_catalog = await get_stac_catalogs(request=request, url="")
|
|
138
133
|
stac_api_version = get_stac_api_version()
|
|
139
134
|
|
|
140
135
|
openapi_schema = get_openapi(
|
|
@@ -151,11 +146,11 @@ def eodag_openapi() -> Dict[str, Any]:
|
|
|
151
146
|
openapi_schema["components"] = stac_api_config["components"]
|
|
152
147
|
openapi_schema["tags"] = stac_api_config["tags"]
|
|
153
148
|
|
|
154
|
-
detailled_collections_list = get_detailled_collections_list(
|
|
149
|
+
detailled_collections_list = get_detailled_collections_list()
|
|
155
150
|
|
|
156
151
|
openapi_schema["info"]["description"] = (
|
|
157
152
|
root_catalog["description"]
|
|
158
|
-
+ " (stac-api-spec {})"
|
|
153
|
+
+ f" (stac-api-spec {stac_api_version})"
|
|
159
154
|
+ "<details><summary>Available collections / product types</summary>"
|
|
160
155
|
+ "".join(
|
|
161
156
|
[
|
|
@@ -183,6 +178,8 @@ app.add_middleware(
|
|
|
183
178
|
allow_headers=["*"],
|
|
184
179
|
)
|
|
185
180
|
|
|
181
|
+
add_exception_handlers(app)
|
|
182
|
+
|
|
186
183
|
|
|
187
184
|
@app.middleware("http")
|
|
188
185
|
async def forward_middleware(
|
|
@@ -195,8 +192,10 @@ async def forward_middleware(
|
|
|
195
192
|
|
|
196
193
|
if "forwarded" in request.headers:
|
|
197
194
|
header_forwarded = parse_header(request.headers["forwarded"])
|
|
198
|
-
forwarded_host = header_forwarded.get_param("host", None) or forwarded_host
|
|
199
|
-
forwarded_proto =
|
|
195
|
+
forwarded_host = str(header_forwarded.get_param("host", None)) or forwarded_host
|
|
196
|
+
forwarded_proto = (
|
|
197
|
+
str(header_forwarded.get_param("proto", None)) or forwarded_proto
|
|
198
|
+
)
|
|
200
199
|
|
|
201
200
|
request.state.url_root = f"{forwarded_proto or request.url.scheme}://{forwarded_host or request.url.netloc}"
|
|
202
201
|
request.state.url = f"{request.state.url_root}{request.url.path}"
|
|
@@ -205,251 +204,166 @@ async def forward_middleware(
|
|
|
205
204
|
return response
|
|
206
205
|
|
|
207
206
|
|
|
208
|
-
@
|
|
209
|
-
async def
|
|
210
|
-
request: Request, error: Exception
|
|
211
|
-
) -> ORJSONResponse:
|
|
212
|
-
"""Default errors handle"""
|
|
213
|
-
description = (
|
|
214
|
-
getattr(error, "description", None)
|
|
215
|
-
or getattr(error, "detail", None)
|
|
216
|
-
or str(error)
|
|
217
|
-
)
|
|
218
|
-
return ORJSONResponse(
|
|
219
|
-
status_code=error.status_code,
|
|
220
|
-
content={"description": description},
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
@app.exception_handler(NoMatchingProductType)
|
|
225
|
-
@app.exception_handler(UnsupportedProductType)
|
|
226
|
-
@app.exception_handler(UnsupportedProvider)
|
|
227
|
-
@app.exception_handler(ValidationError)
|
|
228
|
-
async def handle_invalid_usage(request: Request, error: Exception) -> ORJSONResponse:
|
|
229
|
-
"""Invalid usage [400] errors handle"""
|
|
230
|
-
logger.warning(traceback.format_exc())
|
|
231
|
-
return await default_exception_handler(
|
|
232
|
-
request,
|
|
233
|
-
HTTPException(
|
|
234
|
-
status_code=400,
|
|
235
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
236
|
-
),
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
@app.exception_handler(NotAvailableError)
|
|
241
|
-
async def handle_resource_not_found(
|
|
242
|
-
request: Request, error: Exception
|
|
243
|
-
) -> ORJSONResponse:
|
|
244
|
-
"""Not found [404] errors handle"""
|
|
245
|
-
return await default_exception_handler(
|
|
246
|
-
request,
|
|
247
|
-
HTTPException(
|
|
248
|
-
status_code=404,
|
|
249
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
250
|
-
),
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
@app.exception_handler(MisconfiguredError)
|
|
255
|
-
@app.exception_handler(AuthenticationError)
|
|
256
|
-
async def handle_auth_error(request: Request, error: Exception) -> ORJSONResponse:
|
|
257
|
-
"""AuthenticationError should be sent as internal server error to the client"""
|
|
258
|
-
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
259
|
-
return await default_exception_handler(
|
|
260
|
-
request,
|
|
261
|
-
HTTPException(
|
|
262
|
-
status_code=500,
|
|
263
|
-
detail="Internal server error: please contact the administrator",
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
@app.exception_handler(DownloadError)
|
|
269
|
-
@app.exception_handler(RequestError)
|
|
270
|
-
async def handle_server_error(request: Request, error: Exception) -> ORJSONResponse:
|
|
271
|
-
"""These errors should be sent as internal server error with details to the client"""
|
|
272
|
-
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
273
|
-
return await default_exception_handler(
|
|
274
|
-
request,
|
|
275
|
-
HTTPException(
|
|
276
|
-
status_code=500,
|
|
277
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
278
|
-
),
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
@app.exception_handler(TimeOutError)
|
|
283
|
-
async def handle_timeout(request: Request, error: Exception) -> ORJSONResponse:
|
|
284
|
-
"""Timeout [504] errors handle"""
|
|
285
|
-
logger.error(f"{type(error).__name__}: {str(error)}")
|
|
286
|
-
return await default_exception_handler(
|
|
287
|
-
request,
|
|
288
|
-
HTTPException(
|
|
289
|
-
status_code=504,
|
|
290
|
-
detail=f"{type(error).__name__}: {str(error)}",
|
|
291
|
-
),
|
|
292
|
-
)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
@router.get("/", tags=["Capabilities"])
|
|
296
|
-
def catalogs_root(request: Request) -> Any:
|
|
207
|
+
@router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
|
|
208
|
+
async def catalogs_root(request: Request) -> ORJSONResponse:
|
|
297
209
|
"""STAC catalogs root"""
|
|
298
|
-
logger.
|
|
210
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
299
211
|
|
|
300
|
-
response = get_stac_catalogs(
|
|
212
|
+
response = await get_stac_catalogs(
|
|
213
|
+
request=request,
|
|
301
214
|
url=request.state.url,
|
|
302
|
-
root=request.state.url_root,
|
|
303
|
-
catalogs=[],
|
|
304
215
|
provider=request.query_params.get("provider", None),
|
|
305
216
|
)
|
|
306
217
|
|
|
307
|
-
return
|
|
218
|
+
return ORJSONResponse(response)
|
|
308
219
|
|
|
309
220
|
|
|
310
|
-
@router.
|
|
311
|
-
def conformance() ->
|
|
221
|
+
@router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
|
|
222
|
+
def conformance(request: Request) -> ORJSONResponse:
|
|
312
223
|
"""STAC conformance"""
|
|
313
|
-
logger.
|
|
224
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
314
225
|
response = get_stac_conformance()
|
|
315
226
|
|
|
316
|
-
return
|
|
227
|
+
return ORJSONResponse(response)
|
|
317
228
|
|
|
318
229
|
|
|
319
|
-
@router.
|
|
320
|
-
|
|
230
|
+
@router.api_route(
|
|
231
|
+
methods=["GET", "HEAD"],
|
|
232
|
+
path="/extensions/oseo/json-schema/schema.json",
|
|
233
|
+
include_in_schema=False,
|
|
234
|
+
)
|
|
235
|
+
def stac_extension_oseo(request: Request) -> ORJSONResponse:
|
|
321
236
|
"""STAC OGC / OpenSearch extension for EO"""
|
|
322
|
-
logger.
|
|
237
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
323
238
|
response = get_stac_extension_oseo(url=request.state.url)
|
|
324
239
|
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
class SearchBody(BaseModel):
|
|
329
|
-
"""
|
|
330
|
-
class which describes the body of a search request
|
|
331
|
-
"""
|
|
332
|
-
|
|
333
|
-
provider: Optional[str] = None
|
|
334
|
-
collections: Union[List[str], str]
|
|
335
|
-
datetime: Optional[str] = None
|
|
336
|
-
bbox: Optional[List[Union[int, float]]] = None
|
|
337
|
-
intersects: Optional[Dict[str, Any]] = None
|
|
338
|
-
limit: Optional[int] = DEFAULT_ITEMS_PER_PAGE
|
|
339
|
-
page: Optional[int] = 1
|
|
340
|
-
query: Optional[Dict[str, Any]] = None
|
|
341
|
-
ids: Optional[List[str]] = None
|
|
240
|
+
return ORJSONResponse(response)
|
|
342
241
|
|
|
343
242
|
|
|
344
|
-
@router.
|
|
345
|
-
"
|
|
243
|
+
@router.api_route(
|
|
244
|
+
methods=["GET", "HEAD"],
|
|
245
|
+
path="/collections/{collection_id}/items/{item_id}/download",
|
|
346
246
|
tags=["Data"],
|
|
347
247
|
include_in_schema=False,
|
|
348
248
|
)
|
|
349
249
|
def stac_collections_item_download(
|
|
350
250
|
collection_id: str, item_id: str, request: Request
|
|
351
|
-
) ->
|
|
251
|
+
) -> StarletteResponse:
|
|
352
252
|
"""STAC collection item download"""
|
|
353
|
-
logger.
|
|
253
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
354
254
|
|
|
355
255
|
arguments = dict(request.query_params)
|
|
356
256
|
provider = arguments.pop("provider", None)
|
|
357
257
|
|
|
358
|
-
return
|
|
359
|
-
|
|
258
|
+
return download_stac_item(
|
|
259
|
+
request=request,
|
|
260
|
+
collection_id=collection_id,
|
|
261
|
+
item_id=item_id,
|
|
262
|
+
provider=provider,
|
|
263
|
+
**arguments,
|
|
360
264
|
)
|
|
361
265
|
|
|
362
266
|
|
|
363
|
-
@router.
|
|
364
|
-
"
|
|
267
|
+
@router.api_route(
|
|
268
|
+
methods=["GET", "HEAD"],
|
|
269
|
+
path="/collections/{collection_id}/items/{item_id}/download/{asset}",
|
|
365
270
|
tags=["Data"],
|
|
366
271
|
include_in_schema=False,
|
|
367
272
|
)
|
|
368
273
|
def stac_collections_item_download_asset(
|
|
369
|
-
collection_id, item_id,
|
|
274
|
+
collection_id: str, item_id: str, asset: str, request: Request
|
|
370
275
|
):
|
|
371
276
|
"""STAC collection item asset download"""
|
|
372
|
-
logger.
|
|
277
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
373
278
|
|
|
374
279
|
arguments = dict(request.query_params)
|
|
375
280
|
provider = arguments.pop("provider", None)
|
|
376
281
|
|
|
377
|
-
return
|
|
378
|
-
|
|
282
|
+
return download_stac_item(
|
|
283
|
+
request=request,
|
|
284
|
+
collection_id=collection_id,
|
|
379
285
|
item_id=item_id,
|
|
380
286
|
provider=provider,
|
|
381
|
-
asset=
|
|
382
|
-
**arguments,
|
|
287
|
+
asset=asset,
|
|
383
288
|
)
|
|
384
289
|
|
|
385
290
|
|
|
386
|
-
@router.
|
|
387
|
-
"
|
|
291
|
+
@router.api_route(
|
|
292
|
+
methods=["GET", "HEAD"],
|
|
293
|
+
path="/collections/{collection_id}/items/{item_id}",
|
|
388
294
|
tags=["Data"],
|
|
389
295
|
include_in_schema=False,
|
|
390
296
|
)
|
|
391
|
-
def stac_collections_item(
|
|
297
|
+
def stac_collections_item(
|
|
298
|
+
collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
|
|
299
|
+
) -> ORJSONResponse:
|
|
392
300
|
"""STAC collection item by id"""
|
|
393
|
-
logger.
|
|
394
|
-
url = request.state.url
|
|
395
|
-
url_root = request.state.url_root
|
|
301
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
396
302
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
response = get_stac_item_by_id(
|
|
401
|
-
url=url,
|
|
402
|
-
item_id=item_id,
|
|
403
|
-
root=url_root,
|
|
404
|
-
catalogs=[collection_id],
|
|
405
|
-
provider=provider,
|
|
406
|
-
**arguments,
|
|
303
|
+
search_request = SearchPostRequest(
|
|
304
|
+
provider=provider, ids=[item_id], collections=[collection_id], limit=1
|
|
407
305
|
)
|
|
408
306
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
307
|
+
item_collection = search_stac_items(request, search_request)
|
|
308
|
+
|
|
309
|
+
if not item_collection["features"]:
|
|
412
310
|
raise HTTPException(
|
|
413
311
|
status_code=404,
|
|
414
|
-
detail="
|
|
415
|
-
item_id, collection_id
|
|
416
|
-
),
|
|
312
|
+
detail=f"Item {item_id} in Collection {collection_id} does not exist.",
|
|
417
313
|
)
|
|
418
314
|
|
|
315
|
+
return ORJSONResponse(item_collection["features"][0])
|
|
316
|
+
|
|
419
317
|
|
|
420
|
-
@router.
|
|
421
|
-
"
|
|
318
|
+
@router.api_route(
|
|
319
|
+
methods=["GET", "HEAD"],
|
|
320
|
+
path="/collections/{collection_id}/items",
|
|
422
321
|
tags=["Data"],
|
|
423
322
|
include_in_schema=False,
|
|
424
323
|
)
|
|
425
|
-
def stac_collections_items(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
324
|
+
def stac_collections_items(
|
|
325
|
+
collection_id: str,
|
|
326
|
+
request: Request,
|
|
327
|
+
provider: Optional[str] = None,
|
|
328
|
+
bbox: Optional[str] = None,
|
|
329
|
+
datetime: Optional[str] = None,
|
|
330
|
+
limit: Optional[int] = None,
|
|
331
|
+
query: Optional[str] = None,
|
|
332
|
+
page: Optional[int] = None,
|
|
333
|
+
sortby: Optional[str] = None,
|
|
334
|
+
filter: Optional[str] = None,
|
|
335
|
+
filter_lang: Optional[str] = "cql2-text",
|
|
336
|
+
crunch: Optional[str] = None,
|
|
337
|
+
) -> ORJSONResponse:
|
|
338
|
+
"""Fetch collection's features"""
|
|
433
339
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
arguments=arguments,
|
|
437
|
-
root=url_root,
|
|
340
|
+
return get_search(
|
|
341
|
+
request=request,
|
|
438
342
|
provider=provider,
|
|
439
|
-
|
|
343
|
+
collections=collection_id,
|
|
344
|
+
bbox=bbox,
|
|
345
|
+
datetime=datetime,
|
|
346
|
+
limit=limit,
|
|
347
|
+
query=query,
|
|
348
|
+
page=page,
|
|
349
|
+
sortby=sortby,
|
|
350
|
+
filter=filter,
|
|
351
|
+
filter_lang=filter_lang,
|
|
352
|
+
crunch=crunch,
|
|
440
353
|
)
|
|
441
|
-
return jsonable_encoder(response)
|
|
442
354
|
|
|
443
355
|
|
|
444
|
-
@router.
|
|
445
|
-
"
|
|
356
|
+
@router.api_route(
|
|
357
|
+
methods=["GET", "HEAD"],
|
|
358
|
+
path="/collections/{collection_id}/queryables",
|
|
446
359
|
tags=["Capabilities"],
|
|
447
360
|
include_in_schema=False,
|
|
448
361
|
response_model_exclude_none=True,
|
|
449
362
|
)
|
|
450
|
-
def list_collection_queryables(
|
|
451
|
-
request: Request,
|
|
452
|
-
|
|
363
|
+
async def list_collection_queryables(
|
|
364
|
+
request: Request,
|
|
365
|
+
collection_id: str,
|
|
366
|
+
) -> ORJSONResponse:
|
|
453
367
|
"""Returns the list of queryable properties for a specific collection.
|
|
454
368
|
|
|
455
369
|
This endpoint provides a list of properties that can be used as filters when querying
|
|
@@ -457,221 +371,79 @@ def list_collection_queryables(
|
|
|
457
371
|
that can be filtered using comparison operators.
|
|
458
372
|
|
|
459
373
|
:param request: The incoming request object.
|
|
460
|
-
:type request: fastapi.Request
|
|
461
374
|
:param collection_id: The identifier of the collection for which to retrieve queryable properties.
|
|
462
|
-
:type collection_id: str
|
|
463
|
-
:param provider: (optional) The provider for which to retrieve additional properties.
|
|
464
|
-
:type provider: str
|
|
465
375
|
:returns: A json object containing the list of available queryable properties for the specified collection.
|
|
466
|
-
:rtype: Any
|
|
467
376
|
"""
|
|
468
|
-
logger.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
additional_params.pop("provider", None)
|
|
472
|
-
|
|
473
|
-
queryables = StacQueryables(q_id=request.state.url, additional_properties=False)
|
|
377
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
378
|
+
additional_params = dict(request.query_params)
|
|
379
|
+
provider = additional_params.pop("provider", None)
|
|
474
380
|
|
|
475
|
-
|
|
476
|
-
|
|
381
|
+
queryables = await get_queryables(
|
|
382
|
+
request,
|
|
383
|
+
QueryablesGetParams(collection=collection_id, **additional_params),
|
|
384
|
+
provider=provider,
|
|
477
385
|
)
|
|
478
|
-
for key, collection_queryable in collection_queryables.items():
|
|
479
|
-
queryables[key] = collection_queryable
|
|
480
|
-
queryables.properties.pop("collections")
|
|
481
386
|
|
|
482
|
-
return
|
|
387
|
+
return ORJSONResponse(queryables)
|
|
483
388
|
|
|
484
389
|
|
|
485
|
-
@router.
|
|
486
|
-
"
|
|
390
|
+
@router.api_route(
|
|
391
|
+
methods=["GET", "HEAD"],
|
|
392
|
+
path="/collections/{collection_id}",
|
|
487
393
|
tags=["Capabilities"],
|
|
488
394
|
include_in_schema=False,
|
|
489
395
|
)
|
|
490
|
-
def collection_by_id(
|
|
396
|
+
async def collection_by_id(
|
|
397
|
+
collection_id: str, request: Request, provider: Optional[str] = None
|
|
398
|
+
) -> ORJSONResponse:
|
|
491
399
|
"""STAC collection by id"""
|
|
492
|
-
logger.
|
|
493
|
-
url = request.state.url_root + "/collections"
|
|
494
|
-
url_root = request.state.url_root
|
|
495
|
-
|
|
496
|
-
arguments = dict(request.query_params)
|
|
497
|
-
provider = arguments.pop("provider", None)
|
|
400
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
498
401
|
|
|
499
|
-
response =
|
|
500
|
-
|
|
501
|
-
root=url_root,
|
|
402
|
+
response = await get_collection(
|
|
403
|
+
request=request,
|
|
502
404
|
collection_id=collection_id,
|
|
503
405
|
provider=provider,
|
|
504
406
|
)
|
|
505
407
|
|
|
506
|
-
return
|
|
408
|
+
return ORJSONResponse(response)
|
|
507
409
|
|
|
508
410
|
|
|
509
|
-
@router.
|
|
510
|
-
"
|
|
411
|
+
@router.api_route(
|
|
412
|
+
methods=["GET", "HEAD"],
|
|
413
|
+
path="/collections",
|
|
511
414
|
tags=["Capabilities"],
|
|
512
415
|
include_in_schema=False,
|
|
513
416
|
)
|
|
514
|
-
def collections(
|
|
417
|
+
async def collections(
|
|
418
|
+
request: Request,
|
|
419
|
+
provider: Optional[str] = None,
|
|
420
|
+
q: Optional[str] = None,
|
|
421
|
+
platform: Optional[str] = None,
|
|
422
|
+
instrument: Optional[str] = None,
|
|
423
|
+
constellation: Optional[str] = None,
|
|
424
|
+
datetime: Optional[str] = None,
|
|
425
|
+
) -> ORJSONResponse:
|
|
515
426
|
"""STAC collections
|
|
516
427
|
|
|
517
|
-
Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
|
|
428
|
+
Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
|
|
429
|
+
processingLevel
|
|
518
430
|
"""
|
|
519
|
-
logger.
|
|
520
|
-
url = request.state.url
|
|
521
|
-
url_root = request.state.url_root
|
|
522
|
-
|
|
523
|
-
arguments = dict(request.query_params)
|
|
524
|
-
provider = arguments.pop("provider", None)
|
|
525
|
-
|
|
526
|
-
response = get_stac_collections(
|
|
527
|
-
url=url,
|
|
528
|
-
root=url_root,
|
|
529
|
-
arguments=arguments,
|
|
530
|
-
provider=provider,
|
|
531
|
-
)
|
|
532
|
-
|
|
533
|
-
return jsonable_encoder(response)
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
@router.get(
|
|
537
|
-
"/catalogs/{catalogs:path}/items/{item_id}/download",
|
|
538
|
-
tags=["Data"],
|
|
539
|
-
include_in_schema=False,
|
|
540
|
-
)
|
|
541
|
-
def stac_catalogs_item_download(
|
|
542
|
-
catalogs: str, item_id: str, request: Request
|
|
543
|
-
) -> StreamingResponse:
|
|
544
|
-
"""STAC Catalog item download"""
|
|
545
|
-
logger.debug(f"URL: {request.url}")
|
|
546
|
-
|
|
547
|
-
arguments = dict(request.query_params)
|
|
548
|
-
provider = arguments.pop("provider", None)
|
|
549
|
-
|
|
550
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
551
|
-
|
|
552
|
-
return download_stac_item_by_id_stream(
|
|
553
|
-
catalogs=list_catalog, item_id=item_id, provider=provider, **arguments
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
@router.get(
|
|
558
|
-
"/catalogs/{catalogs:path}/items/{item_id}/download/{asset_filter}",
|
|
559
|
-
tags=["Data"],
|
|
560
|
-
include_in_schema=False,
|
|
561
|
-
)
|
|
562
|
-
def stac_catalogs_item_download_asset(
|
|
563
|
-
catalogs, item_id, asset_filter, request: Request
|
|
564
|
-
):
|
|
565
|
-
"""STAC Catalog item asset download"""
|
|
566
|
-
logger.debug(f"URL: {request.url}")
|
|
567
|
-
|
|
568
|
-
arguments = dict(request.query_params)
|
|
569
|
-
provider = arguments.pop("provider", None)
|
|
570
|
-
|
|
571
|
-
catalogs = catalogs.strip("/").split("/")
|
|
572
|
-
|
|
573
|
-
return download_stac_item_by_id_stream(
|
|
574
|
-
catalogs=catalogs,
|
|
575
|
-
item_id=item_id,
|
|
576
|
-
provider=provider,
|
|
577
|
-
asset=asset_filter,
|
|
578
|
-
**arguments,
|
|
579
|
-
)
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
@router.get(
|
|
583
|
-
"/catalogs/{catalogs:path}/items/{item_id}",
|
|
584
|
-
tags=["Data"],
|
|
585
|
-
include_in_schema=False,
|
|
586
|
-
)
|
|
587
|
-
def stac_catalogs_item(catalogs: str, item_id: str, request: Request):
|
|
588
|
-
"""Fetch catalog's single features."""
|
|
589
|
-
logger.debug(f"URL: {request.url}")
|
|
590
|
-
url = request.state.url
|
|
591
|
-
url_root = request.state.url_root
|
|
592
|
-
|
|
593
|
-
arguments = dict(request.query_params)
|
|
594
|
-
provider = arguments.pop("provider", None)
|
|
595
|
-
|
|
596
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
597
|
-
response = get_stac_item_by_id(
|
|
598
|
-
url=url,
|
|
599
|
-
item_id=item_id,
|
|
600
|
-
root=url_root,
|
|
601
|
-
catalogs=list_catalog,
|
|
602
|
-
provider=provider,
|
|
603
|
-
**arguments,
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
if response:
|
|
607
|
-
return jsonable_encoder(response)
|
|
608
|
-
else:
|
|
609
|
-
raise HTTPException(
|
|
610
|
-
status_code=404,
|
|
611
|
-
detail="No item found matching `{}` id in catalog `{}`".format(
|
|
612
|
-
item_id, catalogs
|
|
613
|
-
),
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
@router.get(
|
|
618
|
-
"/catalogs/{catalogs:path}/items",
|
|
619
|
-
tags=["Data"],
|
|
620
|
-
include_in_schema=False,
|
|
621
|
-
)
|
|
622
|
-
def stac_catalogs_items(catalogs: str, request: Request) -> Any:
|
|
623
|
-
"""Fetch catalog's features
|
|
624
|
-
'"""
|
|
625
|
-
logger.debug(f"URL: {request.url}")
|
|
626
|
-
url = request.state.url
|
|
627
|
-
url_root = request.state.url_root
|
|
628
|
-
|
|
629
|
-
arguments = dict(request.query_params)
|
|
630
|
-
provider = arguments.pop("provider", None)
|
|
631
|
-
|
|
632
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
633
|
-
|
|
634
|
-
response = search_stac_items(
|
|
635
|
-
url=url,
|
|
636
|
-
arguments=arguments,
|
|
637
|
-
root=url_root,
|
|
638
|
-
catalogs=list_catalog,
|
|
639
|
-
provider=provider,
|
|
640
|
-
)
|
|
641
|
-
return jsonable_encoder(response)
|
|
431
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
642
432
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
"/catalogs/{catalogs:path}",
|
|
646
|
-
tags=["Capabilities"],
|
|
647
|
-
include_in_schema=False,
|
|
648
|
-
)
|
|
649
|
-
def stac_catalogs(catalogs: str, request: Request) -> Any:
|
|
650
|
-
"""Describe the given catalog and list available sub-catalogs"""
|
|
651
|
-
logger.debug(f"URL: {request.url}")
|
|
652
|
-
url = request.state.url
|
|
653
|
-
url_root = request.state.url_root
|
|
654
|
-
|
|
655
|
-
arguments = dict(request.query_params)
|
|
656
|
-
provider = arguments.pop("provider", None)
|
|
657
|
-
|
|
658
|
-
list_catalog = catalogs.strip("/").split("/")
|
|
659
|
-
response = get_stac_catalogs(
|
|
660
|
-
url=url,
|
|
661
|
-
root=url_root,
|
|
662
|
-
catalogs=list_catalog,
|
|
663
|
-
provider=provider,
|
|
433
|
+
collections = await all_collections(
|
|
434
|
+
request, provider, q, platform, instrument, constellation, datetime
|
|
664
435
|
)
|
|
665
|
-
return
|
|
436
|
+
return ORJSONResponse(collections)
|
|
666
437
|
|
|
667
438
|
|
|
668
|
-
@router.
|
|
669
|
-
"
|
|
439
|
+
@router.api_route(
|
|
440
|
+
methods=["GET", "HEAD"],
|
|
441
|
+
path="/queryables",
|
|
670
442
|
tags=["Capabilities"],
|
|
671
443
|
response_model_exclude_none=True,
|
|
672
444
|
include_in_schema=False,
|
|
673
445
|
)
|
|
674
|
-
def list_queryables(request: Request
|
|
446
|
+
async def list_queryables(request: Request) -> ORJSONResponse:
|
|
675
447
|
"""Returns the list of terms available for use when writing filter expressions.
|
|
676
448
|
|
|
677
449
|
This endpoint provides a list of terms that can be used as filters when querying
|
|
@@ -679,62 +451,123 @@ def list_queryables(request: Request, provider: Optional[str] = None) -> Any:
|
|
|
679
451
|
operators.
|
|
680
452
|
|
|
681
453
|
:param request: The incoming request object.
|
|
682
|
-
:type request: fastapi.Request
|
|
683
454
|
:returns: A json object containing the list of available queryable terms.
|
|
684
|
-
:rtype: Any
|
|
685
455
|
"""
|
|
686
|
-
logger.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
queryables.properties.update(
|
|
693
|
-
fetch_collection_queryable_properties(None, provider, **additional_params)
|
|
694
|
-
)
|
|
456
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
457
|
+
additional_params = dict(request.query_params.items())
|
|
458
|
+
provider = additional_params.pop("provider", None)
|
|
459
|
+
queryables = await get_queryables(
|
|
460
|
+
request, QueryablesGetParams(**additional_params), provider=provider
|
|
461
|
+
)
|
|
695
462
|
|
|
696
|
-
return
|
|
463
|
+
return ORJSONResponse(queryables)
|
|
697
464
|
|
|
698
465
|
|
|
699
|
-
@router.
|
|
700
|
-
"
|
|
466
|
+
@router.api_route(
|
|
467
|
+
methods=["GET", "HEAD"],
|
|
468
|
+
path="/search",
|
|
701
469
|
tags=["STAC"],
|
|
702
470
|
include_in_schema=False,
|
|
703
471
|
)
|
|
704
|
-
|
|
705
|
-
|
|
472
|
+
def get_search(
|
|
473
|
+
request: Request,
|
|
474
|
+
provider: Optional[str] = None,
|
|
475
|
+
collections: Optional[str] = None,
|
|
476
|
+
ids: Optional[str] = None,
|
|
477
|
+
bbox: Optional[str] = None,
|
|
478
|
+
datetime: Optional[str] = None,
|
|
479
|
+
intersects: Optional[str] = None,
|
|
480
|
+
limit: Optional[int] = None,
|
|
481
|
+
query: Optional[str] = None,
|
|
482
|
+
page: Optional[int] = None,
|
|
483
|
+
sortby: Optional[str] = None,
|
|
484
|
+
filter: Optional[str] = None, # pylint: disable=redefined-builtin
|
|
485
|
+
filter_lang: Optional[str] = "cql2-text",
|
|
486
|
+
crunch: Optional[str] = None,
|
|
487
|
+
) -> ORJSONResponse:
|
|
488
|
+
"""Handler for GET /search"""
|
|
489
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
490
|
+
|
|
491
|
+
query_params = str(request.query_params)
|
|
492
|
+
|
|
493
|
+
# Kludgy fix because using factory does not allow alias for filter-lang
|
|
494
|
+
if filter_lang is None:
|
|
495
|
+
match = re.search(r"filter-lang=([a-z0-9-]+)", query_params, re.IGNORECASE)
|
|
496
|
+
if match:
|
|
497
|
+
filter_lang = match.group(1)
|
|
498
|
+
|
|
499
|
+
base_args = {
|
|
500
|
+
"provider": provider,
|
|
501
|
+
"collections": str2list(collections),
|
|
502
|
+
"ids": str2list(ids),
|
|
503
|
+
"datetime": datetime,
|
|
504
|
+
"bbox": str2list(bbox),
|
|
505
|
+
"intersects": str2json("intersects", intersects),
|
|
506
|
+
"limit": limit,
|
|
507
|
+
"query": str2json("query", query),
|
|
508
|
+
"page": page,
|
|
509
|
+
"sortby": sortby2list(sortby),
|
|
510
|
+
"crunch": crunch,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if filter:
|
|
514
|
+
if filter_lang == "cql2-text":
|
|
515
|
+
ast = parse_cql2_text(filter)
|
|
516
|
+
base_args["filter"] = str2json("filter", to_cql2(ast)) # type: ignore
|
|
517
|
+
base_args["filter-lang"] = "cql2-json"
|
|
518
|
+
elif filter_lang == "cql-json":
|
|
519
|
+
base_args["filter"] = str2json(filter)
|
|
520
|
+
|
|
521
|
+
clean = {k: v for k, v in base_args.items() if v is not None and v != []}
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
search_request = SearchPostRequest.model_validate(clean)
|
|
525
|
+
except pydanticValidationError as e:
|
|
526
|
+
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
527
|
+
|
|
528
|
+
response = search_stac_items(
|
|
529
|
+
request=request,
|
|
530
|
+
search_request=search_request,
|
|
531
|
+
)
|
|
532
|
+
return ORJSONResponse(content=response, media_type="application/json")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@router.api_route(
|
|
536
|
+
methods=["POST", "HEAD"],
|
|
537
|
+
path="/search",
|
|
706
538
|
tags=["STAC"],
|
|
707
539
|
include_in_schema=False,
|
|
708
540
|
)
|
|
709
|
-
def
|
|
710
|
-
|
|
711
|
-
)
|
|
712
|
-
"""STAC collections items"""
|
|
713
|
-
logger.debug(f"URL: {request.url}")
|
|
714
|
-
logger.debug(f"Body: {search_body}")
|
|
541
|
+
async def post_search(request: Request) -> ORJSONResponse:
|
|
542
|
+
"""STAC post search"""
|
|
543
|
+
logger.info(f"{request.method} {request.state.url}")
|
|
715
544
|
|
|
716
|
-
|
|
717
|
-
url_root = request.state.url_root
|
|
545
|
+
content_type = request.headers.get("Content-Type")
|
|
718
546
|
|
|
719
|
-
if
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
547
|
+
if content_type is None:
|
|
548
|
+
raise HTTPException(status_code=400, detail="No Content-Type provided")
|
|
549
|
+
if content_type != "application/json":
|
|
550
|
+
raise HTTPException(status_code=400, detail="Content-Type not supported")
|
|
723
551
|
|
|
724
|
-
|
|
725
|
-
|
|
552
|
+
try:
|
|
553
|
+
payload = await request.json()
|
|
554
|
+
except JSONDecodeError as e:
|
|
555
|
+
raise HTTPException(status_code=400, detail="Invalid JSON data") from e
|
|
726
556
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
557
|
+
try:
|
|
558
|
+
search_request = SearchPostRequest.model_validate(payload)
|
|
559
|
+
except pydanticValidationError as e:
|
|
560
|
+
raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
|
|
561
|
+
|
|
562
|
+
logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
|
|
563
|
+
|
|
564
|
+
response = await run_in_threadpool(
|
|
565
|
+
search_stac_items,
|
|
566
|
+
request,
|
|
567
|
+
search_request,
|
|
736
568
|
)
|
|
737
|
-
|
|
569
|
+
|
|
570
|
+
return ORJSONResponse(content=response, media_type="application/json")
|
|
738
571
|
|
|
739
572
|
|
|
740
573
|
app.include_router(router)
|