eodag 3.0.0b3__py3-none-any.whl → 3.1.0b1__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 (77) hide show
  1. eodag/api/core.py +292 -198
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +51 -14
  5. eodag/api/search_result.py +29 -3
  6. eodag/cli.py +57 -20
  7. eodag/config.py +413 -117
  8. eodag/plugins/apis/base.py +10 -4
  9. eodag/plugins/apis/ecmwf.py +49 -16
  10. eodag/plugins/apis/usgs.py +30 -7
  11. eodag/plugins/authentication/aws_auth.py +14 -5
  12. eodag/plugins/authentication/base.py +10 -1
  13. eodag/plugins/authentication/generic.py +14 -3
  14. eodag/plugins/authentication/header.py +12 -4
  15. eodag/plugins/authentication/keycloak.py +41 -22
  16. eodag/plugins/authentication/oauth.py +11 -1
  17. eodag/plugins/authentication/openid_connect.py +178 -163
  18. eodag/plugins/authentication/qsauth.py +12 -4
  19. eodag/plugins/authentication/sas_auth.py +19 -2
  20. eodag/plugins/authentication/token.py +93 -15
  21. eodag/plugins/authentication/token_exchange.py +19 -19
  22. eodag/plugins/crunch/base.py +4 -1
  23. eodag/plugins/crunch/filter_date.py +5 -2
  24. eodag/plugins/crunch/filter_latest_intersect.py +5 -4
  25. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  26. eodag/plugins/crunch/filter_overlap.py +5 -7
  27. eodag/plugins/crunch/filter_property.py +6 -6
  28. eodag/plugins/download/aws.py +50 -34
  29. eodag/plugins/download/base.py +41 -50
  30. eodag/plugins/download/creodias_s3.py +40 -2
  31. eodag/plugins/download/http.py +221 -195
  32. eodag/plugins/download/s3rest.py +25 -25
  33. eodag/plugins/manager.py +168 -23
  34. eodag/plugins/search/base.py +106 -39
  35. eodag/plugins/search/build_search_result.py +1065 -324
  36. eodag/plugins/search/cop_marine.py +112 -29
  37. eodag/plugins/search/creodias_s3.py +45 -24
  38. eodag/plugins/search/csw.py +41 -1
  39. eodag/plugins/search/data_request_search.py +109 -9
  40. eodag/plugins/search/qssearch.py +549 -257
  41. eodag/plugins/search/static_stac_search.py +20 -21
  42. eodag/resources/ext_product_types.json +1 -1
  43. eodag/resources/product_types.yml +577 -87
  44. eodag/resources/providers.yml +1619 -2776
  45. eodag/resources/stac.yml +3 -163
  46. eodag/resources/user_conf_template.yml +112 -97
  47. eodag/rest/config.py +1 -2
  48. eodag/rest/constants.py +0 -1
  49. eodag/rest/core.py +138 -98
  50. eodag/rest/errors.py +181 -0
  51. eodag/rest/server.py +55 -329
  52. eodag/rest/stac.py +93 -544
  53. eodag/rest/types/eodag_search.py +19 -8
  54. eodag/rest/types/queryables.py +6 -8
  55. eodag/rest/types/stac_search.py +11 -2
  56. eodag/rest/utils/__init__.py +3 -0
  57. eodag/types/__init__.py +71 -18
  58. eodag/types/download_args.py +3 -3
  59. eodag/types/queryables.py +180 -73
  60. eodag/types/search_args.py +3 -3
  61. eodag/types/whoosh.py +126 -0
  62. eodag/utils/__init__.py +147 -66
  63. eodag/utils/exceptions.py +47 -26
  64. eodag/utils/logging.py +37 -77
  65. eodag/utils/repr.py +65 -6
  66. eodag/utils/requests.py +11 -13
  67. eodag/utils/stac_reader.py +1 -1
  68. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/METADATA +80 -81
  69. eodag-3.1.0b1.dist-info/RECORD +108 -0
  70. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  71. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +4 -2
  72. eodag/resources/constraints/climate-dt.json +0 -13
  73. eodag/resources/constraints/extremes-dt.json +0 -8
  74. eodag/utils/constraints.py +0 -244
  75. eodag-3.0.0b3.dist-info/RECORD +0 -110
  76. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  77. {eodag-3.0.0b3.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/rest/server.py CHANGED
@@ -20,7 +20,6 @@ from __future__ import annotations
20
20
  import logging
21
21
  import os
22
22
  import re
23
- import traceback
24
23
  from contextlib import asynccontextmanager
25
24
  from importlib.metadata import version
26
25
  from json import JSONDecodeError
@@ -36,13 +35,13 @@ from typing import (
36
35
 
37
36
  from fastapi import APIRouter as FastAPIRouter
38
37
  from fastapi import FastAPI, HTTPException, Request
38
+ from fastapi.concurrency import run_in_threadpool
39
39
  from fastapi.middleware.cors import CORSMiddleware
40
40
  from fastapi.openapi.utils import get_openapi
41
41
  from fastapi.responses import ORJSONResponse
42
42
  from pydantic import ValidationError as pydanticValidationError
43
43
  from pygeofilter.backends.cql2_json import to_cql2
44
44
  from pygeofilter.parsers.cql2_text import parse as parse_cql2_text
45
- from starlette.exceptions import HTTPException as StarletteHTTPException
46
45
 
47
46
  from eodag.config import load_stac_api_config
48
47
  from eodag.rest.cache import init_cache
@@ -59,23 +58,16 @@ from eodag.rest.core import (
59
58
  get_stac_extension_oseo,
60
59
  search_stac_items,
61
60
  )
62
- from eodag.rest.types.eodag_search import EODAGSearch
61
+ from eodag.rest.errors import add_exception_handlers
63
62
  from eodag.rest.types.queryables import QueryablesGetParams
64
63
  from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
65
- from eodag.rest.utils import format_pydantic_error, str2json, str2list
66
- from eodag.utils import parse_header, update_nested_dict
67
- from eodag.utils.exceptions import (
68
- AuthenticationError,
69
- DownloadError,
70
- MisconfiguredError,
71
- NoMatchingProductType,
72
- NotAvailableError,
73
- RequestError,
74
- TimeOutError,
75
- UnsupportedProductType,
76
- UnsupportedProvider,
77
- ValidationError,
64
+ from eodag.rest.utils import (
65
+ LIVENESS_PROBE_PATH,
66
+ format_pydantic_error,
67
+ str2json,
68
+ str2list,
78
69
  )
70
+ from eodag.utils import parse_header, update_nested_dict
79
71
 
80
72
  if TYPE_CHECKING:
81
73
  from fastapi.types import DecoratedCallable
@@ -84,12 +76,6 @@ if TYPE_CHECKING:
84
76
  from starlette.responses import Response as StarletteResponse
85
77
 
86
78
  logger = logging.getLogger("eodag.rest.server")
87
- ERRORS_WITH_500_STATUS_CODE = {
88
- "MisconfiguredError",
89
- "AuthenticationError",
90
- "DownloadError",
91
- "RequestError",
92
- }
93
79
 
94
80
 
95
81
  class APIRouter(FastAPIRouter):
@@ -139,6 +125,17 @@ app = FastAPI(lifespan=lifespan, title="EODAG", docs_url="/api.html")
139
125
  stac_api_config = load_stac_api_config()
140
126
 
141
127
 
128
+ @router.api_route(
129
+ methods=["GET", "HEAD"],
130
+ path=LIVENESS_PROBE_PATH,
131
+ include_in_schema=False,
132
+ status_code=200,
133
+ )
134
+ async def liveness_probe(request: Request) -> Dict[str, bool]:
135
+ "Endpoint meant to be used as liveness probe by deployment platforms"
136
+ return {"success": True}
137
+
138
+
142
139
  @router.api_route(
143
140
  methods=["GET", "HEAD"], path="/api", tags=["Capabilities"], include_in_schema=False
144
141
  )
@@ -197,6 +194,8 @@ app.add_middleware(
197
194
  allow_headers=["*"],
198
195
  )
199
196
 
197
+ add_exception_handlers(app)
198
+
200
199
 
201
200
  @app.middleware("http")
202
201
  async def forward_middleware(
@@ -221,141 +220,10 @@ async def forward_middleware(
221
220
  return response
222
221
 
223
222
 
224
- @app.exception_handler(StarletteHTTPException)
225
- async def default_exception_handler(
226
- request: Request, error: HTTPException
227
- ) -> ORJSONResponse:
228
- """Default errors handle"""
229
- description = (
230
- getattr(error, "description", None)
231
- or getattr(error, "detail", None)
232
- or str(error)
233
- )
234
- return ORJSONResponse(
235
- status_code=error.status_code,
236
- content={"description": description},
237
- )
238
-
239
-
240
- @app.exception_handler(ValidationError)
241
- async def handle_invalid_usage_with_validation_error(
242
- request: Request, error: ValidationError
243
- ) -> ORJSONResponse:
244
- """Invalid usage [400] ValidationError handle"""
245
- if error.parameters:
246
- for error_param in error.parameters:
247
- stac_param = EODAGSearch.to_stac(error_param)
248
- error.message = error.message.replace(error_param, stac_param)
249
- logger.debug(traceback.format_exc())
250
- return await default_exception_handler(
251
- request,
252
- HTTPException(
253
- status_code=400,
254
- detail=f"{type(error).__name__}: {str(error.message)}",
255
- ),
256
- )
257
-
258
-
259
- @app.exception_handler(NoMatchingProductType)
260
- @app.exception_handler(UnsupportedProductType)
261
- @app.exception_handler(UnsupportedProvider)
262
- async def handle_invalid_usage(request: Request, error: Exception) -> ORJSONResponse:
263
- """Invalid usage [400] errors handle"""
264
- return await default_exception_handler(
265
- request,
266
- HTTPException(
267
- status_code=400,
268
- detail=f"{type(error).__name__}: {str(error)}",
269
- ),
270
- )
271
-
272
-
273
- @app.exception_handler(NotAvailableError)
274
- async def handle_resource_not_found(
275
- request: Request, error: Exception
276
- ) -> ORJSONResponse:
277
- """Not found [404] errors handle"""
278
- return await default_exception_handler(
279
- request,
280
- HTTPException(
281
- status_code=404,
282
- detail=f"{type(error).__name__}: {str(error)}",
283
- ),
284
- )
285
-
286
-
287
- @app.exception_handler(MisconfiguredError)
288
- @app.exception_handler(AuthenticationError)
289
- async def handle_auth_error(request: Request, error: Exception) -> ORJSONResponse:
290
- """These errors should be sent as internal server error to the client"""
291
- logger.error("%s: %s", type(error).__name__, str(error))
292
- return await default_exception_handler(
293
- request,
294
- HTTPException(
295
- status_code=500,
296
- detail="Internal server error: please contact the administrator",
297
- ),
298
- )
299
-
300
-
301
- @app.exception_handler(DownloadError)
302
- async def handle_download_error(request: Request, error: Exception) -> ORJSONResponse:
303
- """DownloadError should be sent as internal server error with details to the client"""
304
- logger.error(f"{type(error).__name__}: {str(error)}")
305
- return await default_exception_handler(
306
- request,
307
- HTTPException(
308
- status_code=500,
309
- detail=f"{type(error).__name__}: {str(error)}",
310
- ),
311
- )
312
-
313
-
314
- @app.exception_handler(RequestError)
315
- async def handle_request_error(request: Request, error: RequestError) -> ORJSONResponse:
316
- """RequestError should be sent as internal server error with details to the client"""
317
- if getattr(error, "history", None):
318
- error_history_tmp = list(error.history)
319
- for i, search_error in enumerate(error_history_tmp):
320
- if search_error[1].__class__.__name__ in ERRORS_WITH_500_STATUS_CODE:
321
- search_error[1].args = ("an internal error occured",)
322
- error_history_tmp[i] = search_error
323
- continue
324
- if getattr(error, "parameters", None):
325
- for error_param in error.parameters:
326
- stac_param = EODAGSearch.to_stac(error_param)
327
- search_error[1].args = (
328
- search_error[1].args[0].replace(error_param, stac_param),
329
- )
330
- error_history_tmp[i] = search_error
331
- error.history = set(error_history_tmp)
332
- logger.error(f"{type(error).__name__}: {str(error)}")
333
- return await default_exception_handler(
334
- request,
335
- HTTPException(
336
- status_code=500,
337
- detail=f"{type(error).__name__}: {str(error)}",
338
- ),
339
- )
340
-
341
-
342
- @app.exception_handler(TimeOutError)
343
- async def handle_timeout(request: Request, error: Exception) -> ORJSONResponse:
344
- """Timeout [504] errors handle"""
345
- logger.error(f"{type(error).__name__}: {str(error)}")
346
- return await default_exception_handler(
347
- request,
348
- HTTPException(
349
- status_code=504,
350
- detail=f"{type(error).__name__}: {str(error)}",
351
- ),
352
- )
353
-
354
-
355
223
  @router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
356
224
  async def catalogs_root(request: Request) -> ORJSONResponse:
357
225
  """STAC catalogs root"""
358
- logger.debug("URL: %s", request.url)
226
+ logger.info(f"{request.method} {request.state.url}")
359
227
 
360
228
  response = await get_stac_catalogs(
361
229
  request=request,
@@ -367,9 +235,9 @@ async def catalogs_root(request: Request) -> ORJSONResponse:
367
235
 
368
236
 
369
237
  @router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
370
- def conformance() -> ORJSONResponse:
238
+ def conformance(request: Request) -> ORJSONResponse:
371
239
  """STAC conformance"""
372
- logger.debug("URL: /conformance")
240
+ logger.info(f"{request.method} {request.state.url}")
373
241
  response = get_stac_conformance()
374
242
 
375
243
  return ORJSONResponse(response)
@@ -382,7 +250,7 @@ def conformance() -> ORJSONResponse:
382
250
  )
383
251
  def stac_extension_oseo(request: Request) -> ORJSONResponse:
384
252
  """STAC OGC / OpenSearch extension for EO"""
385
- logger.debug("URL: %s", request.url)
253
+ logger.info(f"{request.method} {request.state.url}")
386
254
  response = get_stac_extension_oseo(url=request.state.url)
387
255
 
388
256
  return ORJSONResponse(response)
@@ -398,14 +266,14 @@ def stac_collections_item_download(
398
266
  collection_id: str, item_id: str, request: Request
399
267
  ) -> StarletteResponse:
400
268
  """STAC collection item download"""
401
- logger.debug("URL: %s", request.url)
269
+ logger.info(f"{request.method} {request.state.url}")
402
270
 
403
271
  arguments = dict(request.query_params)
404
272
  provider = arguments.pop("provider", None)
405
273
 
406
274
  return download_stac_item(
407
275
  request=request,
408
- catalogs=[collection_id],
276
+ collection_id=collection_id,
409
277
  item_id=item_id,
410
278
  provider=provider,
411
279
  **arguments,
@@ -422,14 +290,14 @@ def stac_collections_item_download_asset(
422
290
  collection_id: str, item_id: str, asset: str, request: Request
423
291
  ):
424
292
  """STAC collection item asset download"""
425
- logger.debug("URL: %s", request.url)
293
+ logger.info(f"{request.method} {request.state.url}")
426
294
 
427
295
  arguments = dict(request.query_params)
428
296
  provider = arguments.pop("provider", None)
429
297
 
430
298
  return download_stac_item(
431
299
  request=request,
432
- catalogs=[collection_id],
300
+ collection_id=collection_id,
433
301
  item_id=item_id,
434
302
  provider=provider,
435
303
  asset=asset,
@@ -446,7 +314,7 @@ def stac_collections_item(
446
314
  collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
447
315
  ) -> ORJSONResponse:
448
316
  """STAC collection item by id"""
449
- logger.debug("URL: %s", request.url)
317
+ logger.info(f"{request.method} {request.state.url}")
450
318
 
451
319
  search_request = SearchPostRequest(
452
320
  provider=provider, ids=[item_id], collections=[collection_id], limit=1
@@ -522,14 +390,25 @@ async def list_collection_queryables(
522
390
  :param collection_id: The identifier of the collection for which to retrieve queryable properties.
523
391
  :returns: A json object containing the list of available queryable properties for the specified collection.
524
392
  """
525
- logger.debug(f"URL: {request.url}")
526
- additional_params = dict(request.query_params)
393
+ logger.info(f"{request.method} {request.state.url}")
394
+ # split by `,` to handle list of parameters
395
+ additional_params = {k: v.split(",") for k, v in dict(request.query_params).items()}
527
396
  provider = additional_params.pop("provider", None)
528
397
 
398
+ datetime = additional_params.pop("datetime", None)
399
+
529
400
  queryables = await get_queryables(
530
401
  request,
531
- QueryablesGetParams(collection=collection_id, **additional_params),
532
- provider=provider,
402
+ QueryablesGetParams.model_validate(
403
+ {
404
+ **additional_params,
405
+ **{
406
+ "collection": collection_id,
407
+ "datetime": datetime[0] if datetime else None,
408
+ },
409
+ }
410
+ ),
411
+ provider=provider[0] if provider else None,
533
412
  )
534
413
 
535
414
  return ORJSONResponse(queryables)
@@ -545,7 +424,7 @@ async def collection_by_id(
545
424
  collection_id: str, request: Request, provider: Optional[str] = None
546
425
  ) -> ORJSONResponse:
547
426
  """STAC collection by id"""
548
- logger.debug("URL: %s", request.url)
427
+ logger.info(f"{request.method} {request.state.url}")
549
428
 
550
429
  response = await get_collection(
551
430
  request=request,
@@ -576,7 +455,7 @@ async def collections(
576
455
  Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
577
456
  processingLevel
578
457
  """
579
- logger.debug("URL: %s", request.url)
458
+ logger.info(f"{request.method} {request.state.url}")
580
459
 
581
460
  collections = await all_collections(
582
461
  request, provider, q, platform, instrument, constellation, datetime
@@ -584,161 +463,6 @@ async def collections(
584
463
  return ORJSONResponse(collections)
585
464
 
586
465
 
587
- @router.api_route(
588
- methods=["GET", "HEAD"],
589
- path="/catalogs/{catalogs:path}/items/{item_id}/download",
590
- tags=["Data"],
591
- include_in_schema=False,
592
- )
593
- def stac_catalogs_item_download(
594
- catalogs: str, item_id: str, request: Request
595
- ) -> StarletteResponse:
596
- """STAC Catalog item download"""
597
- logger.debug("URL: %s", request.url)
598
-
599
- arguments = dict(request.query_params)
600
- provider = arguments.pop("provider", None)
601
-
602
- list_catalog = catalogs.strip("/").split("/")
603
-
604
- return download_stac_item(
605
- request=request,
606
- catalogs=list_catalog,
607
- item_id=item_id,
608
- provider=provider,
609
- **arguments,
610
- )
611
-
612
-
613
- @router.api_route(
614
- methods=["GET", "HEAD"],
615
- path="/catalogs/{catalogs:path}/items/{item_id}/download/{asset_filter}",
616
- tags=["Data"],
617
- include_in_schema=False,
618
- )
619
- def stac_catalogs_item_download_asset(
620
- catalogs: str, item_id: str, asset_filter: str, request: Request
621
- ):
622
- """STAC Catalog item asset download"""
623
- logger.debug("URL: %s", request.url)
624
-
625
- arguments = dict(request.query_params)
626
- provider = arguments.pop("provider", None)
627
-
628
- list_catalog = catalogs.strip("/").split("/")
629
-
630
- return download_stac_item(
631
- request,
632
- catalogs=list_catalog,
633
- item_id=item_id,
634
- provider=provider,
635
- asset=asset_filter,
636
- **arguments,
637
- )
638
-
639
-
640
- @router.api_route(
641
- methods=["GET", "HEAD"],
642
- path="/catalogs/{catalogs:path}/items/{item_id}",
643
- tags=["Data"],
644
- include_in_schema=False,
645
- )
646
- def stac_catalogs_item(
647
- catalogs: str, item_id: str, request: Request, provider: Optional[str] = None
648
- ):
649
- """Fetch catalog's single features."""
650
- logger.debug("URL: %s", request.url)
651
-
652
- list_catalog = catalogs.strip("/").split("/")
653
-
654
- search_request = SearchPostRequest(provider=provider, ids=[item_id], limit=1)
655
-
656
- item_collection = search_stac_items(request, search_request, catalogs=list_catalog)
657
-
658
- if not item_collection["features"]:
659
- raise HTTPException(
660
- status_code=404,
661
- detail=f"Item {item_id} in Catalog {catalogs} does not exist.",
662
- )
663
-
664
- return ORJSONResponse(item_collection["features"][0])
665
-
666
-
667
- @router.api_route(
668
- methods=["GET", "HEAD"],
669
- path="/catalogs/{catalogs:path}/items",
670
- tags=["Data"],
671
- include_in_schema=False,
672
- )
673
- def stac_catalogs_items(
674
- catalogs: str,
675
- request: Request,
676
- provider: Optional[str] = None,
677
- bbox: Optional[str] = None,
678
- datetime: Optional[str] = None,
679
- limit: Optional[int] = None,
680
- page: Optional[int] = None,
681
- sortby: Optional[str] = None,
682
- crunch: Optional[str] = None,
683
- ) -> ORJSONResponse:
684
- """Fetch catalog's features"""
685
- logger.debug("URL: %s", request.state.url)
686
-
687
- base_args = {
688
- "provider": provider,
689
- "datetime": datetime,
690
- "bbox": str2list(bbox),
691
- "limit": limit,
692
- "page": page,
693
- "sortby": sortby2list(sortby),
694
- "crunch": crunch,
695
- }
696
-
697
- clean = {k: v for k, v in base_args.items() if v is not None and v != []}
698
-
699
- list_catalog = catalogs.strip("/").split("/")
700
-
701
- try:
702
- search_request = SearchPostRequest.model_validate(clean)
703
- except pydanticValidationError as e:
704
- raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
705
-
706
- response = search_stac_items(
707
- request=request,
708
- search_request=search_request,
709
- catalogs=list_catalog,
710
- )
711
- return ORJSONResponse(response)
712
-
713
-
714
- @router.api_route(
715
- methods=["GET", "HEAD"],
716
- path="/catalogs/{catalogs:path}",
717
- tags=["Capabilities"],
718
- include_in_schema=False,
719
- )
720
- async def stac_catalogs(
721
- catalogs: str, request: Request, provider: Optional[str] = None
722
- ) -> ORJSONResponse:
723
- """Describe the given catalog and list available sub-catalogs"""
724
- logger.debug("URL: %s", request.url)
725
-
726
- if not catalogs:
727
- raise HTTPException(
728
- status_code=404,
729
- detail="Not found",
730
- )
731
-
732
- list_catalog = catalogs.strip("/").split("/")
733
- response = await get_stac_catalogs(
734
- request=request,
735
- url=request.state.url,
736
- catalogs=tuple(list_catalog),
737
- provider=provider,
738
- )
739
- return ORJSONResponse(response)
740
-
741
-
742
466
  @router.api_route(
743
467
  methods=["GET", "HEAD"],
744
468
  path="/queryables",
@@ -756,7 +480,7 @@ async def list_queryables(request: Request) -> ORJSONResponse:
756
480
  :param request: The incoming request object.
757
481
  :returns: A json object containing the list of available queryable terms.
758
482
  """
759
- logger.debug(f"URL: {request.url}")
483
+ logger.info(f"{request.method} {request.state.url}")
760
484
  additional_params = dict(request.query_params.items())
761
485
  provider = additional_params.pop("provider", None)
762
486
  queryables = await get_queryables(
@@ -789,7 +513,7 @@ def get_search(
789
513
  crunch: Optional[str] = None,
790
514
  ) -> ORJSONResponse:
791
515
  """Handler for GET /search"""
792
- logger.debug("URL: %s", request.state.url)
516
+ logger.info(f"{request.method} {request.state.url}")
793
517
 
794
518
  query_params = str(request.query_params)
795
519
 
@@ -843,7 +567,7 @@ def get_search(
843
567
  )
844
568
  async def post_search(request: Request) -> ORJSONResponse:
845
569
  """STAC post search"""
846
- logger.debug("URL: %s", request.url)
570
+ logger.info(f"{request.method} {request.state.url}")
847
571
 
848
572
  content_type = request.headers.get("Content-Type")
849
573
 
@@ -864,10 +588,12 @@ async def post_search(request: Request) -> ORJSONResponse:
864
588
 
865
589
  logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
866
590
 
867
- response = search_stac_items(
868
- request=request,
869
- search_request=search_request,
591
+ response = await run_in_threadpool(
592
+ search_stac_items,
593
+ request,
594
+ search_request,
870
595
  )
596
+
871
597
  return ORJSONResponse(content=response, media_type="application/json")
872
598
 
873
599