eodag 3.0.0b2__py3-none-any.whl → 3.0.1__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 (84) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +295 -287
  3. eodag/api/product/__init__.py +10 -4
  4. eodag/api/product/_assets.py +2 -14
  5. eodag/api/product/_product.py +16 -30
  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 +12 -31
  9. eodag/api/search_result.py +33 -12
  10. eodag/cli.py +35 -19
  11. eodag/config.py +455 -155
  12. eodag/plugins/apis/base.py +13 -7
  13. eodag/plugins/apis/ecmwf.py +16 -7
  14. eodag/plugins/apis/usgs.py +68 -16
  15. eodag/plugins/authentication/aws_auth.py +25 -7
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +14 -3
  18. eodag/plugins/authentication/header.py +12 -4
  19. eodag/plugins/authentication/keycloak.py +41 -22
  20. eodag/plugins/authentication/oauth.py +11 -1
  21. eodag/plugins/authentication/openid_connect.py +183 -167
  22. eodag/plugins/authentication/qsauth.py +12 -4
  23. eodag/plugins/authentication/sas_auth.py +19 -2
  24. eodag/plugins/authentication/token.py +59 -11
  25. eodag/plugins/authentication/token_exchange.py +19 -19
  26. eodag/plugins/crunch/base.py +7 -2
  27. eodag/plugins/crunch/filter_date.py +8 -11
  28. eodag/plugins/crunch/filter_latest_intersect.py +5 -7
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +2 -5
  30. eodag/plugins/crunch/filter_overlap.py +9 -15
  31. eodag/plugins/crunch/filter_property.py +9 -14
  32. eodag/plugins/download/aws.py +84 -99
  33. eodag/plugins/download/base.py +36 -77
  34. eodag/plugins/download/creodias_s3.py +11 -2
  35. eodag/plugins/download/http.py +134 -109
  36. eodag/plugins/download/s3rest.py +37 -43
  37. eodag/plugins/manager.py +173 -41
  38. eodag/plugins/search/__init__.py +9 -9
  39. eodag/plugins/search/base.py +35 -35
  40. eodag/plugins/search/build_search_result.py +55 -64
  41. eodag/plugins/search/cop_marine.py +113 -32
  42. eodag/plugins/search/creodias_s3.py +20 -8
  43. eodag/plugins/search/csw.py +41 -1
  44. eodag/plugins/search/data_request_search.py +119 -14
  45. eodag/plugins/search/qssearch.py +619 -197
  46. eodag/plugins/search/static_stac_search.py +25 -23
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +211 -56
  49. eodag/resources/providers.yml +1762 -1809
  50. eodag/resources/stac.yml +3 -163
  51. eodag/resources/user_conf_template.yml +134 -119
  52. eodag/rest/config.py +1 -2
  53. eodag/rest/constants.py +0 -1
  54. eodag/rest/core.py +70 -92
  55. eodag/rest/errors.py +181 -0
  56. eodag/rest/server.py +24 -330
  57. eodag/rest/stac.py +105 -630
  58. eodag/rest/types/eodag_search.py +17 -15
  59. eodag/rest/types/queryables.py +5 -14
  60. eodag/rest/types/stac_search.py +18 -13
  61. eodag/rest/utils/rfc3339.py +0 -1
  62. eodag/types/__init__.py +24 -6
  63. eodag/types/download_args.py +14 -5
  64. eodag/types/queryables.py +1 -2
  65. eodag/types/search_args.py +10 -11
  66. eodag/types/whoosh.py +0 -2
  67. eodag/utils/__init__.py +97 -136
  68. eodag/utils/constraints.py +0 -8
  69. eodag/utils/exceptions.py +23 -9
  70. eodag/utils/import_system.py +0 -4
  71. eodag/utils/logging.py +37 -80
  72. eodag/utils/notebook.py +4 -4
  73. eodag/utils/requests.py +13 -23
  74. eodag/utils/rest.py +0 -4
  75. eodag/utils/stac_reader.py +3 -15
  76. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/METADATA +41 -24
  77. eodag-3.0.1.dist-info/RECORD +109 -0
  78. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/WHEEL +1 -1
  79. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/entry_points.txt +1 -0
  80. eodag/resources/constraints/climate-dt.json +0 -13
  81. eodag/resources/constraints/extremes-dt.json +0 -8
  82. eodag-3.0.0b2.dist-info/RECORD +0 -110
  83. {eodag-3.0.0b2.dist-info → eodag-3.0.1.dist-info}/LICENSE +0 -0
  84. {eodag-3.0.0b2.dist-info → eodag-3.0.1.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,11 @@ 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
64
  from eodag.rest.utils import format_pydantic_error, str2json, str2list
66
65
  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,
78
- )
79
66
 
80
67
  if TYPE_CHECKING:
81
68
  from fastapi.types import DecoratedCallable
@@ -84,12 +71,6 @@ if TYPE_CHECKING:
84
71
  from starlette.responses import Response as StarletteResponse
85
72
 
86
73
  logger = logging.getLogger("eodag.rest.server")
87
- ERRORS_WITH_500_STATUS_CODE = {
88
- "MisconfiguredError",
89
- "AuthenticationError",
90
- "DownloadError",
91
- "RequestError",
92
- }
93
74
 
94
75
 
95
76
  class APIRouter(FastAPIRouter):
@@ -197,6 +178,8 @@ app.add_middleware(
197
178
  allow_headers=["*"],
198
179
  )
199
180
 
181
+ add_exception_handlers(app)
182
+
200
183
 
201
184
  @app.middleware("http")
202
185
  async def forward_middleware(
@@ -221,141 +204,10 @@ async def forward_middleware(
221
204
  return response
222
205
 
223
206
 
224
- @app.exception_handler(StarletteHTTPException)
225
- async def default_exception_handler(
226
- request: Request, error: Exception
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
207
  @router.api_route(methods=["GET", "HEAD"], path="/", tags=["Capabilities"])
356
208
  async def catalogs_root(request: Request) -> ORJSONResponse:
357
209
  """STAC catalogs root"""
358
- logger.debug("URL: %s", request.url)
210
+ logger.info(f"{request.method} {request.state.url}")
359
211
 
360
212
  response = await get_stac_catalogs(
361
213
  request=request,
@@ -367,9 +219,9 @@ async def catalogs_root(request: Request) -> ORJSONResponse:
367
219
 
368
220
 
369
221
  @router.api_route(methods=["GET", "HEAD"], path="/conformance", tags=["Capabilities"])
370
- def conformance() -> ORJSONResponse:
222
+ def conformance(request: Request) -> ORJSONResponse:
371
223
  """STAC conformance"""
372
- logger.debug("URL: /conformance")
224
+ logger.info(f"{request.method} {request.state.url}")
373
225
  response = get_stac_conformance()
374
226
 
375
227
  return ORJSONResponse(response)
@@ -382,7 +234,7 @@ def conformance() -> ORJSONResponse:
382
234
  )
383
235
  def stac_extension_oseo(request: Request) -> ORJSONResponse:
384
236
  """STAC OGC / OpenSearch extension for EO"""
385
- logger.debug("URL: %s", request.url)
237
+ logger.info(f"{request.method} {request.state.url}")
386
238
  response = get_stac_extension_oseo(url=request.state.url)
387
239
 
388
240
  return ORJSONResponse(response)
@@ -398,14 +250,14 @@ def stac_collections_item_download(
398
250
  collection_id: str, item_id: str, request: Request
399
251
  ) -> StarletteResponse:
400
252
  """STAC collection item download"""
401
- logger.debug("URL: %s", request.url)
253
+ logger.info(f"{request.method} {request.state.url}")
402
254
 
403
255
  arguments = dict(request.query_params)
404
256
  provider = arguments.pop("provider", None)
405
257
 
406
258
  return download_stac_item(
407
259
  request=request,
408
- catalogs=[collection_id],
260
+ collection_id=collection_id,
409
261
  item_id=item_id,
410
262
  provider=provider,
411
263
  **arguments,
@@ -422,14 +274,14 @@ def stac_collections_item_download_asset(
422
274
  collection_id: str, item_id: str, asset: str, request: Request
423
275
  ):
424
276
  """STAC collection item asset download"""
425
- logger.debug("URL: %s", request.url)
277
+ logger.info(f"{request.method} {request.state.url}")
426
278
 
427
279
  arguments = dict(request.query_params)
428
280
  provider = arguments.pop("provider", None)
429
281
 
430
282
  return download_stac_item(
431
283
  request=request,
432
- catalogs=[collection_id],
284
+ collection_id=collection_id,
433
285
  item_id=item_id,
434
286
  provider=provider,
435
287
  asset=asset,
@@ -446,7 +298,7 @@ def stac_collections_item(
446
298
  collection_id: str, item_id: str, request: Request, provider: Optional[str] = None
447
299
  ) -> ORJSONResponse:
448
300
  """STAC collection item by id"""
449
- logger.debug("URL: %s", request.url)
301
+ logger.info(f"{request.method} {request.state.url}")
450
302
 
451
303
  search_request = SearchPostRequest(
452
304
  provider=provider, ids=[item_id], collections=[collection_id], limit=1
@@ -519,13 +371,10 @@ async def list_collection_queryables(
519
371
  that can be filtered using comparison operators.
520
372
 
521
373
  :param request: The incoming request object.
522
- :type request: fastapi.Request
523
374
  :param collection_id: The identifier of the collection for which to retrieve queryable properties.
524
- :type collection_id: str
525
375
  :returns: A json object containing the list of available queryable properties for the specified collection.
526
- :rtype: Any
527
376
  """
528
- logger.debug(f"URL: {request.url}")
377
+ logger.info(f"{request.method} {request.state.url}")
529
378
  additional_params = dict(request.query_params)
530
379
  provider = additional_params.pop("provider", None)
531
380
 
@@ -548,7 +397,7 @@ async def collection_by_id(
548
397
  collection_id: str, request: Request, provider: Optional[str] = None
549
398
  ) -> ORJSONResponse:
550
399
  """STAC collection by id"""
551
- logger.debug("URL: %s", request.url)
400
+ logger.info(f"{request.method} {request.state.url}")
552
401
 
553
402
  response = await get_collection(
554
403
  request=request,
@@ -579,7 +428,7 @@ async def collections(
579
428
  Can be filtered using parameters: instrument, platform, platformSerialIdentifier, sensorType,
580
429
  processingLevel
581
430
  """
582
- logger.debug("URL: %s", request.url)
431
+ logger.info(f"{request.method} {request.state.url}")
583
432
 
584
433
  collections = await all_collections(
585
434
  request, provider, q, platform, instrument, constellation, datetime
@@ -587,161 +436,6 @@ async def collections(
587
436
  return ORJSONResponse(collections)
588
437
 
589
438
 
590
- @router.api_route(
591
- methods=["GET", "HEAD"],
592
- path="/catalogs/{catalogs:path}/items/{item_id}/download",
593
- tags=["Data"],
594
- include_in_schema=False,
595
- )
596
- def stac_catalogs_item_download(
597
- catalogs: str, item_id: str, request: Request
598
- ) -> StarletteResponse:
599
- """STAC Catalog item download"""
600
- logger.debug("URL: %s", request.url)
601
-
602
- arguments = dict(request.query_params)
603
- provider = arguments.pop("provider", None)
604
-
605
- list_catalog = catalogs.strip("/").split("/")
606
-
607
- return download_stac_item(
608
- request=request,
609
- catalogs=list_catalog,
610
- item_id=item_id,
611
- provider=provider,
612
- **arguments,
613
- )
614
-
615
-
616
- @router.api_route(
617
- methods=["GET", "HEAD"],
618
- path="/catalogs/{catalogs:path}/items/{item_id}/download/{asset_filter}",
619
- tags=["Data"],
620
- include_in_schema=False,
621
- )
622
- def stac_catalogs_item_download_asset(
623
- catalogs: str, item_id: str, asset_filter: str, request: Request
624
- ):
625
- """STAC Catalog item asset download"""
626
- logger.debug("URL: %s", request.url)
627
-
628
- arguments = dict(request.query_params)
629
- provider = arguments.pop("provider", None)
630
-
631
- list_catalog = catalogs.strip("/").split("/")
632
-
633
- return download_stac_item(
634
- request,
635
- catalogs=list_catalog,
636
- item_id=item_id,
637
- provider=provider,
638
- asset=asset_filter,
639
- **arguments,
640
- )
641
-
642
-
643
- @router.api_route(
644
- methods=["GET", "HEAD"],
645
- path="/catalogs/{catalogs:path}/items/{item_id}",
646
- tags=["Data"],
647
- include_in_schema=False,
648
- )
649
- def stac_catalogs_item(
650
- catalogs: str, item_id: str, request: Request, provider: Optional[str] = None
651
- ):
652
- """Fetch catalog's single features."""
653
- logger.debug("URL: %s", request.url)
654
-
655
- list_catalog = catalogs.strip("/").split("/")
656
-
657
- search_request = SearchPostRequest(provider=provider, ids=[item_id], limit=1)
658
-
659
- item_collection = search_stac_items(request, search_request, catalogs=list_catalog)
660
-
661
- if not item_collection["features"]:
662
- raise HTTPException(
663
- status_code=404,
664
- detail=f"Item {item_id} in Catalog {catalogs} does not exist.",
665
- )
666
-
667
- return ORJSONResponse(item_collection["features"][0])
668
-
669
-
670
- @router.api_route(
671
- methods=["GET", "HEAD"],
672
- path="/catalogs/{catalogs:path}/items",
673
- tags=["Data"],
674
- include_in_schema=False,
675
- )
676
- def stac_catalogs_items(
677
- catalogs: str,
678
- request: Request,
679
- provider: Optional[str] = None,
680
- bbox: Optional[str] = None,
681
- datetime: Optional[str] = None,
682
- limit: Optional[int] = None,
683
- page: Optional[int] = None,
684
- sortby: Optional[str] = None,
685
- crunch: Optional[str] = None,
686
- ) -> ORJSONResponse:
687
- """Fetch catalog's features"""
688
- logger.debug("URL: %s", request.state.url)
689
-
690
- base_args = {
691
- "provider": provider,
692
- "datetime": datetime,
693
- "bbox": str2list(bbox),
694
- "limit": limit,
695
- "page": page,
696
- "sortby": sortby2list(sortby),
697
- "crunch": crunch,
698
- }
699
-
700
- clean = {k: v for k, v in base_args.items() if v is not None and v != []}
701
-
702
- list_catalog = catalogs.strip("/").split("/")
703
-
704
- try:
705
- search_request = SearchPostRequest.model_validate(clean)
706
- except pydanticValidationError as e:
707
- raise HTTPException(status_code=400, detail=format_pydantic_error(e)) from e
708
-
709
- response = search_stac_items(
710
- request=request,
711
- search_request=search_request,
712
- catalogs=list_catalog,
713
- )
714
- return ORJSONResponse(response)
715
-
716
-
717
- @router.api_route(
718
- methods=["GET", "HEAD"],
719
- path="/catalogs/{catalogs:path}",
720
- tags=["Capabilities"],
721
- include_in_schema=False,
722
- )
723
- async def stac_catalogs(
724
- catalogs: str, request: Request, provider: Optional[str] = None
725
- ) -> ORJSONResponse:
726
- """Describe the given catalog and list available sub-catalogs"""
727
- logger.debug("URL: %s", request.url)
728
-
729
- if not catalogs:
730
- raise HTTPException(
731
- status_code=404,
732
- detail="Not found",
733
- )
734
-
735
- list_catalog = catalogs.strip("/").split("/")
736
- response = await get_stac_catalogs(
737
- request=request,
738
- url=request.state.url,
739
- catalogs=tuple(list_catalog),
740
- provider=provider,
741
- )
742
- return ORJSONResponse(response)
743
-
744
-
745
439
  @router.api_route(
746
440
  methods=["GET", "HEAD"],
747
441
  path="/queryables",
@@ -757,11 +451,9 @@ async def list_queryables(request: Request) -> ORJSONResponse:
757
451
  operators.
758
452
 
759
453
  :param request: The incoming request object.
760
- :type request: fastapi.Request
761
454
  :returns: A json object containing the list of available queryable terms.
762
- :rtype: Any
763
455
  """
764
- logger.debug(f"URL: {request.url}")
456
+ logger.info(f"{request.method} {request.state.url}")
765
457
  additional_params = dict(request.query_params.items())
766
458
  provider = additional_params.pop("provider", None)
767
459
  queryables = await get_queryables(
@@ -794,7 +486,7 @@ def get_search(
794
486
  crunch: Optional[str] = None,
795
487
  ) -> ORJSONResponse:
796
488
  """Handler for GET /search"""
797
- logger.debug("URL: %s", request.state.url)
489
+ logger.info(f"{request.method} {request.state.url}")
798
490
 
799
491
  query_params = str(request.query_params)
800
492
 
@@ -848,7 +540,7 @@ def get_search(
848
540
  )
849
541
  async def post_search(request: Request) -> ORJSONResponse:
850
542
  """STAC post search"""
851
- logger.debug("URL: %s", request.url)
543
+ logger.info(f"{request.method} {request.state.url}")
852
544
 
853
545
  content_type = request.headers.get("Content-Type")
854
546
 
@@ -869,10 +561,12 @@ async def post_search(request: Request) -> ORJSONResponse:
869
561
 
870
562
  logger.debug("Body: %s", search_request.model_dump(exclude_none=True))
871
563
 
872
- response = search_stac_items(
873
- request=request,
874
- search_request=search_request,
564
+ response = await run_in_threadpool(
565
+ search_stac_items,
566
+ request,
567
+ search_request,
875
568
  )
569
+
876
570
  return ORJSONResponse(content=response, media_type="application/json")
877
571
 
878
572