eodag 2.12.1__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.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -168
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.1.dist-info/RECORD +0 -94
  92. {eodag-2.12.1.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.1.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 traceback
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.encoders import jsonable_encoder
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, StreamingResponse
43
- from pydantic import BaseModel
44
- from starlette.exceptions import HTTPException as StarletteHTTPException
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.types.stac_queryables import StacQueryables
48
- from eodag.rest.utils import (
49
- download_stac_item_by_id_stream,
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
- fetch_collection_queryable_properties,
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.utils import DEFAULT_ITEMS_PER_PAGE, parse_header, update_nested_dict
63
- from eodag.utils.exceptions import (
64
- AuthenticationError,
65
- DownloadError,
66
- MisconfiguredError,
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.get("/api", tags=["Capabilities"], include_in_schema=False)
131
- def eodag_openapi() -> Dict[str, Any]:
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="", fetch_providers=False)
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(fetch_providers=False)
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 {})".format(stac_api_version)
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 = header_forwarded.get_param("proto", None) or 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
- @app.exception_handler(StarletteHTTPException)
209
- async def default_exception_handler(
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.debug(f"URL: {request.url}")
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 jsonable_encoder(response)
218
+ return ORJSONResponse(response)
308
219
 
309
220
 
310
- @router.get("/conformance", tags=["Capabilities"])
311
- def conformance() -> Any:
221
+ @router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
222
+ def conformance(request: Request) -> ORJSONResponse:
312
223
  """STAC conformance"""
313
- logger.debug("URL: /conformance")
224
+ logger.info(f"{request.method} {request.state.url}")
314
225
  response = get_stac_conformance()
315
226
 
316
- return jsonable_encoder(response)
227
+ return ORJSONResponse(response)
317
228
 
318
229
 
319
- @router.get("/extensions/oseo/json-schema/schema.json", include_in_schema=False)
320
- def stac_extension_oseo(request: Request) -> Any:
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.debug(f"URL: {request.url}")
237
+ logger.info(f"{request.method} {request.state.url}")
323
238
  response = get_stac_extension_oseo(url=request.state.url)
324
239
 
325
- return jsonable_encoder(response)
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.get(
345
- "/collections/{collection_id}/items/{item_id}/download",
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
- ) -> StreamingResponse:
251
+ ) -> StarletteResponse:
352
252
  """STAC collection item download"""
353
- logger.debug(f"URL: {request.url}")
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 download_stac_item_by_id_stream(
359
- catalogs=[collection_id], item_id=item_id, provider=provider, **arguments
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.get(
364
- "/collections/{collection_id}/items/{item_id}/download/{asset_filter}",
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, asset_filter, request: Request
274
+ collection_id: str, item_id: str, asset: str, request: Request
370
275
  ):
371
276
  """STAC collection item asset download"""
372
- logger.debug(f"URL: {request.url}")
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 download_stac_item_by_id_stream(
378
- catalogs=[collection_id],
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=asset_filter,
382
- **arguments,
287
+ asset=asset,
383
288
  )
384
289
 
385
290
 
386
- @router.get(
387
- "/collections/{collection_id}/items/{item_id}",
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(collection_id: str, item_id: str, request: Request) -> Any:
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.debug(f"URL: {request.url}")
394
- url = request.state.url
395
- url_root = request.state.url_root
301
+ logger.info(f"{request.method} {request.state.url}")
396
302
 
397
- arguments = dict(request.query_params)
398
- provider = arguments.pop("provider", None)
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
- if response:
410
- return jsonable_encoder(response)
411
- else:
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="No item found matching `{}` id in collection `{}`".format(
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.get(
421
- "/collections/{collection_id}/items",
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(collection_id: str, request: Request) -> Any:
426
- """STAC collections items"""
427
- logger.debug(f"URL: {request.url}")
428
- url = request.state.url
429
- url_root = request.state.url_root
430
-
431
- arguments = dict(request.query_params)
432
- provider = arguments.pop("provider", None)
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
- response = search_stac_items(
435
- url=url,
436
- arguments=arguments,
437
- root=url_root,
340
+ return get_search(
341
+ request=request,
438
342
  provider=provider,
439
- catalogs=[collection_id],
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.get(
445
- "/collections/{collection_id}/queryables",
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, collection_id: str, provider: Optional[str] = None
452
- ) -> Any:
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.debug(f"URL: {request.url}")
469
- query_params = request.query_params.items()
470
- additional_params = dict(query_params)
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
- collection_queryables = fetch_collection_queryable_properties(
476
- collection_id, provider, **additional_params
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 jsonable_encoder(queryables)
387
+ return ORJSONResponse(queryables)
483
388
 
484
389
 
485
- @router.get(
486
- "/collections/{collection_id}",
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(collection_id: str, request: Request) -> Any:
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.debug(f"URL: {request.url}")
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 = get_stac_collection_by_id(
500
- url=url,
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 jsonable_encoder(response)
408
+ return ORJSONResponse(response)
507
409
 
508
410
 
509
- @router.get(
510
- "/collections",
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(request: Request) -> Any:
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, processingLevel
428
+ Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
429
+ processingLevel
518
430
  """
519
- logger.debug(f"URL: {request.url}")
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
- @router.get(
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 jsonable_encoder(response)
436
+ return ORJSONResponse(collections)
666
437
 
667
438
 
668
- @router.get(
669
- "/queryables",
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, provider: Optional[str] = None) -> Any:
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.debug(f"URL: {request.url}")
687
- query_params = request.query_params.items()
688
- additional_params = dict(query_params)
689
- additional_params.pop("provider", None)
690
- queryables = StacQueryables(q_id=request.state.url)
691
- if provider:
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 jsonable_encoder(queryables)
463
+ return ORJSONResponse(queryables)
697
464
 
698
465
 
699
- @router.get(
700
- "/search",
466
+ @router.api_route(
467
+ methods=["GET", "HEAD"],
468
+ path="/search",
701
469
  tags=["STAC"],
702
470
  include_in_schema=False,
703
471
  )
704
- @router.post(
705
- "/search",
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 stac_search(
710
- request: Request, search_body: Optional[SearchBody] = None
711
- ) -> ORJSONResponse:
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
- url = request.state.url
717
- url_root = request.state.url_root
545
+ content_type = request.headers.get("Content-Type")
718
546
 
719
- if search_body is None:
720
- body = {}
721
- else:
722
- body = vars(search_body)
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
- arguments = dict(request.query_params, **body)
725
- provider = arguments.pop("provider", None)
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
- response = search_stac_items(
728
- url=url,
729
- arguments=arguments,
730
- root=url_root,
731
- provider=provider,
732
- method=request.method,
733
- )
734
- resp = ORJSONResponse(
735
- content=response, status_code=200, media_type="application/json"
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
- return resp
569
+
570
+ return ORJSONResponse(content=response, media_type="application/json")
738
571
 
739
572
 
740
573
  app.include_router(router)