eodag 3.0.1__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 (44) hide show
  1. eodag/api/core.py +116 -86
  2. eodag/api/product/_assets.py +6 -6
  3. eodag/api/product/_product.py +18 -18
  4. eodag/api/product/metadata_mapping.py +39 -11
  5. eodag/cli.py +22 -1
  6. eodag/config.py +14 -14
  7. eodag/plugins/apis/ecmwf.py +37 -14
  8. eodag/plugins/apis/usgs.py +5 -5
  9. eodag/plugins/authentication/openid_connect.py +2 -2
  10. eodag/plugins/authentication/token.py +37 -6
  11. eodag/plugins/crunch/filter_property.py +2 -3
  12. eodag/plugins/download/aws.py +11 -12
  13. eodag/plugins/download/base.py +30 -39
  14. eodag/plugins/download/creodias_s3.py +29 -0
  15. eodag/plugins/download/http.py +144 -152
  16. eodag/plugins/download/s3rest.py +5 -7
  17. eodag/plugins/search/base.py +73 -25
  18. eodag/plugins/search/build_search_result.py +1047 -310
  19. eodag/plugins/search/creodias_s3.py +25 -19
  20. eodag/plugins/search/data_request_search.py +1 -1
  21. eodag/plugins/search/qssearch.py +51 -139
  22. eodag/resources/ext_product_types.json +1 -1
  23. eodag/resources/product_types.yml +391 -32
  24. eodag/resources/providers.yml +678 -1744
  25. eodag/rest/core.py +92 -62
  26. eodag/rest/server.py +31 -4
  27. eodag/rest/types/eodag_search.py +6 -0
  28. eodag/rest/types/queryables.py +5 -6
  29. eodag/rest/utils/__init__.py +3 -0
  30. eodag/types/__init__.py +56 -15
  31. eodag/types/download_args.py +2 -2
  32. eodag/types/queryables.py +180 -72
  33. eodag/types/whoosh.py +126 -0
  34. eodag/utils/__init__.py +71 -10
  35. eodag/utils/exceptions.py +27 -20
  36. eodag/utils/repr.py +65 -6
  37. eodag/utils/requests.py +11 -11
  38. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/METADATA +76 -76
  39. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/RECORD +43 -44
  40. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/WHEEL +1 -1
  41. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/entry_points.txt +3 -2
  42. eodag/utils/constraints.py +0 -244
  43. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/LICENSE +0 -0
  44. {eodag-3.0.1.dist-info → eodag-3.1.0b1.dist-info}/top_level.txt +0 -0
eodag/rest/core.py CHANGED
@@ -33,7 +33,6 @@ from requests.models import Response as RequestsResponse
33
33
  import eodag
34
34
  from eodag import EOProduct
35
35
  from eodag.api.product.metadata_mapping import (
36
- DEFAULT_METADATA_MAPPING,
37
36
  NOT_AVAILABLE,
38
37
  OFFLINE_STATUS,
39
38
  ONLINE_STATUS,
@@ -54,11 +53,7 @@ from eodag.rest.constants import (
54
53
  from eodag.rest.errors import ResponseSearchError
55
54
  from eodag.rest.stac import StacCatalog, StacCollection, StacCommon, StacItem
56
55
  from eodag.rest.types.eodag_search import EODAGSearch
57
- from eodag.rest.types.queryables import (
58
- QueryablesGetParams,
59
- StacQueryableProperty,
60
- StacQueryables,
61
- )
56
+ from eodag.rest.types.queryables import QueryablesGetParams, StacQueryables
62
57
  from eodag.rest.types.stac_search import SearchPostRequest
63
58
  from eodag.rest.utils import (
64
59
  Cruncher,
@@ -193,28 +188,18 @@ def search_stac_items(
193
188
  results.number_matched = len(results)
194
189
  total = len(results)
195
190
 
196
- elif time_interval_overlap(eodag_args, catalog):
197
- criteria = {
198
- **catalog.search_args,
199
- **eodag_args.model_dump(exclude_none=True),
200
- }
191
+ else:
192
+ criteria = eodag_args.model_dump(exclude_none=True)
201
193
  # remove provider prefixes
202
- stac_extensions = stac_config["extensions"]
203
- keys_to_update = {}
204
- for key in criteria:
194
+ # quickfix for ecmwf fake extension to not impact items creation
195
+ stac_extensions = list(stac_config["extensions"].keys()) + ["ecmwf"]
196
+ for key in list(criteria):
205
197
  if ":" in key and key.split(":")[0] not in stac_extensions:
206
198
  new_key = key.split(":")[1]
207
- keys_to_update[key] = new_key
208
- for key, new_key in keys_to_update.items():
209
- criteria[new_key] = criteria[key]
210
- criteria.pop(key)
199
+ criteria[new_key] = criteria.pop(key)
211
200
 
212
201
  results = eodag_api.search(count=True, **criteria)
213
202
  total = results.number_matched or 0
214
- else:
215
- # return empty results
216
- results = SearchResult([], 0)
217
- total = 0
218
203
 
219
204
  if len(results) == 0 and results.errors:
220
205
  raise ResponseSearchError(results.errors)
@@ -342,13 +327,11 @@ def _order_and_update(
342
327
  if (
343
328
  product.properties.get("storageStatus") != ONLINE_STATUS
344
329
  and NOT_AVAILABLE in product.properties.get("orderStatusLink", "")
345
- and hasattr(product.downloader, "order_download")
330
+ and hasattr(product.downloader, "_order")
346
331
  ):
347
332
  # first order
348
333
  logger.debug("Order product")
349
- order_status_dict = product.downloader.order_download(
350
- product=product, auth=auth
351
- )
334
+ order_status_dict = product.downloader._order(product=product, auth=auth)
352
335
  query_args.update(order_status_dict or {})
353
336
 
354
337
  if (
@@ -359,11 +342,11 @@ def _order_and_update(
359
342
  product.properties["storageStatus"] = STAGING_STATUS
360
343
 
361
344
  if product.properties.get("storageStatus") == STAGING_STATUS and hasattr(
362
- product.downloader, "order_download_status"
345
+ product.downloader, "_order_status"
363
346
  ):
364
347
  # check order status if needed
365
348
  logger.debug("Checking product order status")
366
- product.downloader.order_download_status(product=product, auth=auth)
349
+ product.downloader._order_status(product=product, auth=auth)
367
350
 
368
351
  if product.properties.get("storageStatus") != ONLINE_STATUS:
369
352
  raise NotAvailableError("Product is not available yet")
@@ -417,7 +400,10 @@ async def all_collections(
417
400
  # # parse f-strings
418
401
  format_args = deepcopy(stac_config)
419
402
  format_args["collections"].update(
420
- {"url": stac_collection.url, "root": stac_collection.root}
403
+ {
404
+ "url": stac_collection.url,
405
+ "root": stac_collection.root,
406
+ }
421
407
  )
422
408
 
423
409
  collections["links"] = [
@@ -495,7 +481,10 @@ def time_interval_overlap(eodag_args: EODAGSearch, catalog: StacCatalog) -> bool
495
481
  # check if time filtering appears both in search arguments and catalog
496
482
  # (for catalogs built by date: i.e. `year/2020/month/05`)
497
483
  if not set(["start", "end"]) <= set(eodag_args.model_dump().keys()) or not set(
498
- ["start", "end"]
484
+ [
485
+ "start",
486
+ "end",
487
+ ]
499
488
  ) <= set(catalog.search_args.keys()):
500
489
  return True
501
490
 
@@ -591,51 +580,87 @@ async def get_queryables(
591
580
 
592
581
  async def _fetch() -> Dict[str, Any]:
593
582
  python_queryables = eodag_api.list_queryables(
594
- provider=provider, **params.model_dump(exclude_none=True, by_alias=True)
583
+ provider=provider,
584
+ fetch_providers=False,
585
+ **params.model_dump(exclude_none=True, by_alias=True),
595
586
  )
596
- python_queryables.pop("start")
597
- python_queryables.pop("end")
598
587
 
599
- # productType and id are already default in stac collection and id
600
- python_queryables.pop("productType", None)
601
- python_queryables.pop("id", None)
602
-
603
- stac_queryables: Dict[str, StacQueryableProperty] = deepcopy(
604
- StacQueryables.default_properties
588
+ python_queryables_json = python_queryables.get_model().model_json_schema(
589
+ by_alias=True
605
590
  )
591
+
592
+ properties: Dict[str, Any] = python_queryables_json["properties"]
593
+ required: List[str] = python_queryables_json.get("required") or []
594
+
595
+ # productType is either simply removed or replaced by collection later.
596
+ if "productType" in properties:
597
+ properties.pop("productType")
598
+ if "productType" in required:
599
+ required.remove("productType")
600
+
601
+ stac_properties: Dict[str, Any] = {}
602
+
606
603
  # get stac default properties to set prefixes
607
604
  stac_item_properties = list(stac_config["item"]["properties"].values())
608
- stac_item_properties.extend(list(stac_queryables.keys()))
609
- ignore = stac_config["metadata_ignore"]
610
- stac_item_properties.extend(ignore)
611
- default_mapping = DEFAULT_METADATA_MAPPING.keys()
612
- for param, queryable in python_queryables.items():
613
- if param in default_mapping and not any(
605
+ stac_item_properties.extend(stac_config["metadata_ignore"])
606
+ for param, queryable in properties.items():
607
+ # convert key to STAC format
608
+ if param in OSEO_METADATA_MAPPING.keys() and not any(
614
609
  param in str(prop) for prop in stac_item_properties
615
610
  ):
616
611
  param = f"oseo:{param}"
617
612
  stac_param = EODAGSearch.to_stac(param, stac_item_properties, provider)
618
- # only keep "datetime" queryable for dates
619
- if stac_param in stac_queryables or stac_param in (
620
- "start_datetime",
621
- "end_datetime",
622
- ):
623
- continue
624
613
 
625
- stac_queryables[
626
- stac_param
627
- ] = StacQueryableProperty.from_python_field_definition(
628
- stac_param, queryable
629
- )
614
+ queryable["title"] = stac_param.split(":")[-1]
615
+
616
+ # remove null default values
617
+ if not queryable.get("default"):
618
+ queryable.pop("default", None)
630
619
 
631
- if params.collection:
632
- stac_queryables.pop("collection")
620
+ stac_properties[stac_param] = queryable
621
+ required = list(map(lambda x: x.replace(param, stac_param), required))
622
+
623
+ # due to certain metadata mappings we might only get end_datetime but we can
624
+ # assume that start_datetime is also available
625
+ if (
626
+ "end_datetime" in stac_properties
627
+ and "start_datetime" not in stac_properties
628
+ ):
629
+ stac_properties["start_datetime"] = deepcopy(
630
+ stac_properties["end_datetime"]
631
+ )
632
+ stac_properties["start_datetime"]["title"] = "start_datetime"
633
+ # if we can search by start_datetime we can search by datetime
634
+ if "start_datetime" in stac_properties:
635
+ stac_properties["datetime"] = StacQueryables.possible_properties[
636
+ "datetime"
637
+ ].model_dump()
638
+
639
+ # format spatial extend properties to STAC format.
640
+ if "geometry" in stac_properties:
641
+ stac_properties["bbox"] = StacQueryables.possible_properties[
642
+ "bbox"
643
+ ].model_dump()
644
+ stac_properties["geometry"] = StacQueryables.possible_properties[
645
+ "geometry"
646
+ ].model_dump()
647
+
648
+ if not params.collection:
649
+ stac_properties["collection"] = StacQueryables.default_properties[
650
+ "collection"
651
+ ].model_dump()
652
+
653
+ additional_properties = python_queryables.additional_properties
654
+ description = "Queryable names for the EODAG STAC API Item Search filter. "
655
+ description += python_queryables.additional_information
633
656
 
634
657
  return StacQueryables(
635
658
  q_id=request.state.url,
636
- additional_properties=bool(not params.collection),
637
- properties=stac_queryables,
638
- ).model_dump(mode="json", by_alias=True)
659
+ additional_properties=additional_properties,
660
+ properties=stac_properties,
661
+ required=required or None,
662
+ description=description,
663
+ ).model_dump(mode="json", by_alias=True, exclude_none=True)
639
664
 
640
665
  hashed_queryables = hash(params.model_dump_json())
641
666
  return await cached(
@@ -701,11 +726,16 @@ def eodag_api_init() -> None:
701
726
  constellation: Union[str, List[str]] = ext_col.get("summaries", {}).get(
702
727
  "constellation"
703
728
  )
729
+ processing_level: Union[str, List[str]] = ext_col.get("summaries", {}).get(
730
+ "processing:level"
731
+ )
704
732
  # Check if platform or constellation are lists and join them into a string if they are
705
733
  if isinstance(platform, list):
706
734
  platform = ",".join(platform)
707
735
  if isinstance(constellation, list):
708
736
  constellation = ",".join(constellation)
737
+ if isinstance(processing_level, list):
738
+ processing_level = ",".join(processing_level)
709
739
 
710
740
  update_fields = {
711
741
  "title": ext_col.get("title"),
@@ -716,7 +746,7 @@ def eodag_api_init() -> None:
716
746
  ),
717
747
  "platform": constellation,
718
748
  "platformSerialIdentifier": platform,
719
- "processingLevel": ext_col.get("summaries", {}).get("processing:level"),
749
+ "processingLevel": processing_level,
720
750
  "license": ext_col["license"],
721
751
  "missionStartDate": ext_col["extent"]["temporal"]["interval"][0][0],
722
752
  "missionEndDate": ext_col["extent"]["temporal"]["interval"][-1][1],
eodag/rest/server.py CHANGED
@@ -61,7 +61,12 @@ from eodag.rest.core import (
61
61
  from eodag.rest.errors import add_exception_handlers
62
62
  from eodag.rest.types.queryables import QueryablesGetParams
63
63
  from eodag.rest.types.stac_search import SearchPostRequest, sortby2list
64
- from eodag.rest.utils import format_pydantic_error, str2json, str2list
64
+ from eodag.rest.utils import (
65
+ LIVENESS_PROBE_PATH,
66
+ format_pydantic_error,
67
+ str2json,
68
+ str2list,
69
+ )
65
70
  from eodag.utils import parse_header, update_nested_dict
66
71
 
67
72
  if TYPE_CHECKING:
@@ -120,6 +125,17 @@ app = FastAPI(lifespan=lifespan, title="EODAG", docs_url="/api.html")
120
125
  stac_api_config = load_stac_api_config()
121
126
 
122
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
+
123
139
  @router.api_route(
124
140
  methods=["GET", "HEAD"], path="/api", tags=["Capabilities"], include_in_schema=False
125
141
  )
@@ -375,13 +391,24 @@ async def list_collection_queryables(
375
391
  :returns: A json object containing the list of available queryable properties for the specified collection.
376
392
  """
377
393
  logger.info(f"{request.method} {request.state.url}")
378
- additional_params = dict(request.query_params)
394
+ # split by `,` to handle list of parameters
395
+ additional_params = {k: v.split(",") for k, v in dict(request.query_params).items()}
379
396
  provider = additional_params.pop("provider", None)
380
397
 
398
+ datetime = additional_params.pop("datetime", None)
399
+
381
400
  queryables = await get_queryables(
382
401
  request,
383
- QueryablesGetParams(collection=collection_id, **additional_params),
384
- 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,
385
412
  )
386
413
 
387
414
  return ORJSONResponse(queryables)
@@ -367,6 +367,12 @@ class EODAGSearch(BaseModel):
367
367
  provider: Optional[str] = None,
368
368
  ) -> str:
369
369
  """Get the alias of a field in a Pydantic model"""
370
+ # quick fix. TODO: refactor of EODAGSearch.
371
+ if field_name in ("productType", "id", "start_datetime", "end_datetime"):
372
+ return field_name
373
+ # another quick fix to handle different names of geometry
374
+ if field_name == "geometry":
375
+ field_name = "geom"
370
376
  field = cls.model_fields.get(field_name)
371
377
  if field is not None and field.alias is not None:
372
378
  return field.alias
@@ -131,14 +131,12 @@ class StacQueryables(BaseModel):
131
131
  default="Queryable names for the EODAG STAC API Item Search filter."
132
132
  )
133
133
  default_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
134
- "id": StacQueryableProperty(
135
- description="ID",
136
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id",
137
- ),
138
134
  "collection": StacQueryableProperty(
139
135
  description="Collection",
140
136
  ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
141
- ),
137
+ )
138
+ }
139
+ possible_properties: ClassVar[Dict[str, StacQueryableProperty]] = {
142
140
  "geometry": StacQueryableProperty(
143
141
  description="Geometry",
144
142
  ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
@@ -154,7 +152,8 @@ class StacQueryables(BaseModel):
154
152
  items={"type": "number"},
155
153
  ),
156
154
  }
157
- properties: Dict[str, StacQueryableProperty] = Field()
155
+ properties: Dict[str, Any] = Field()
156
+ required: Optional[List[str]] = Field(None)
158
157
  additional_properties: bool = Field(
159
158
  default=True, serialization_alias="additionalProperties"
160
159
  )
@@ -55,6 +55,9 @@ __all__ = ["get_date", "get_datetime"]
55
55
 
56
56
  logger = logging.getLogger("eodag.rest.utils")
57
57
 
58
+ # Path of the liveness endpoint
59
+ LIVENESS_PROBE_PATH = "/_mgmt/ping"
60
+
58
61
 
59
62
  class Cruncher(NamedTuple):
60
63
  """Type hinted Cruncher namedTuple"""
eodag/types/__init__.py CHANGED
@@ -16,6 +16,7 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  """EODAG types"""
19
+
19
20
  from __future__ import annotations
20
21
 
21
22
  from typing import (
@@ -33,7 +34,7 @@ from typing import (
33
34
  )
34
35
 
35
36
  from annotated_types import Gt, Lt
36
- from pydantic import Field
37
+ from pydantic import BaseModel, Field, create_model
37
38
  from pydantic.fields import FieldInfo
38
39
 
39
40
  from eodag.utils import copy_deepcopy
@@ -70,8 +71,8 @@ def json_type_to_python(json_type: Union[str, List[str]]) -> type:
70
71
 
71
72
 
72
73
  def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
73
- """
74
- checks if the value from an Annotated object is a minimum or maximum
74
+ """Checks if the value from an Annotated object is a minimum or maximum
75
+
75
76
  :param type_info: info from Annotated
76
77
  :return: "min" or "max"
77
78
  """
@@ -83,10 +84,10 @@ def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
83
84
 
84
85
 
85
86
  def _get_type_info_from_annotated(
86
- annotated_type: Annotated[type, Any]
87
+ annotated_type: Annotated[type, Any],
87
88
  ) -> Dict[str, Any]:
88
- """
89
- retrieves type information from an annotated object
89
+ """Retrieves type information from an annotated object
90
+
90
91
  :param annotated_type: annotated object
91
92
  :return: dict containing type and min/max if available
92
93
  """
@@ -118,7 +119,8 @@ def python_type_to_json(
118
119
  :param python_type: the python type
119
120
  :returns: the json type
120
121
  """
121
- if get_origin(python_type) is Union:
122
+ origin = get_origin(python_type)
123
+ if origin is Union:
122
124
  json_type = list()
123
125
  for single_python_type in get_args(python_type):
124
126
  type_data = {}
@@ -138,8 +140,10 @@ def python_type_to_json(
138
140
  return list(JSON_TYPES_MAPPING.keys())[
139
141
  list(JSON_TYPES_MAPPING.values()).index(python_type)
140
142
  ]
141
- elif get_origin(python_type) == Annotated:
143
+ elif origin is Annotated:
142
144
  return [_get_type_info_from_annotated(python_type)]
145
+ elif origin is list:
146
+ raise NotImplementedError("Never completed")
143
147
  else:
144
148
  return None
145
149
 
@@ -173,12 +177,27 @@ def json_field_definition_to_python(
173
177
  title=json_field_definition.get("title", None),
174
178
  description=json_field_definition.get("description", None),
175
179
  pattern=json_field_definition.get("pattern", None),
180
+ le=json_field_definition.get("maximum"),
181
+ ge=json_field_definition.get("minimum"),
176
182
  )
177
183
 
178
- if "enum" in json_field_definition and (
179
- isinstance(json_field_definition["enum"], (list, set))
180
- ):
181
- python_type = Literal[tuple(sorted(json_field_definition["enum"]))] # type: ignore
184
+ enum = json_field_definition.get("enum")
185
+
186
+ if python_type in (list, set):
187
+ items = json_field_definition.get("items", None)
188
+ if isinstance(items, list):
189
+ python_type = Tuple[ # type: ignore
190
+ tuple(
191
+ json_field_definition_to_python(item, required=required)
192
+ for item in items
193
+ )
194
+ ]
195
+ elif isinstance(items, dict):
196
+ enum = items.get("enum")
197
+
198
+ if enum:
199
+ literal = Literal[tuple(sorted(enum))] # type: ignore
200
+ python_type = List[literal] if python_type in (list, set) else literal # type: ignore
182
201
 
183
202
  if "$ref" in json_field_definition:
184
203
  field_type_kwargs["json_schema_extra"] = {"$ref": json_field_definition["$ref"]}
@@ -190,7 +209,7 @@ def json_field_definition_to_python(
190
209
 
191
210
 
192
211
  def python_field_definition_to_json(
193
- python_field_definition: Annotated[Any, FieldInfo]
212
+ python_field_definition: Annotated[Any, FieldInfo],
194
213
  ) -> Dict[str, Any]:
195
214
  """Get json field definition from python `typing.Annotated`
196
215
 
@@ -252,6 +271,7 @@ def python_field_definition_to_json(
252
271
  json_field_definition["max"] = [
253
272
  row["max"] if "max" in row else None for row in field_type
254
273
  ]
274
+
255
275
  if "min" in json_field_definition and json_field_definition["min"].count(
256
276
  None
257
277
  ) == len(json_field_definition["min"]):
@@ -291,7 +311,7 @@ def python_field_definition_to_json(
291
311
 
292
312
 
293
313
  def model_fields_to_annotated(
294
- model_fields: Dict[str, FieldInfo]
314
+ model_fields: Dict[str, FieldInfo],
295
315
  ) -> Dict[str, Annotated[Any, FieldInfo]]:
296
316
  """Convert BaseModel.model_fields from FieldInfo to Annotated
297
317
 
@@ -306,7 +326,7 @@ def model_fields_to_annotated(
306
326
  :param model_fields: BaseModel.model_fields to convert
307
327
  :returns: Annotated tuple usable as create_model argument
308
328
  """
309
- annotated_model_fields = dict()
329
+ annotated_model_fields: Dict[str, Annotated[Any, FieldInfo]] = dict()
310
330
  for param, field_info in model_fields.items():
311
331
  field_type = field_info.annotation or type(None)
312
332
  new_field_info = copy_deepcopy(field_info)
@@ -315,6 +335,27 @@ def model_fields_to_annotated(
315
335
  return annotated_model_fields
316
336
 
317
337
 
338
+ def annotated_dict_to_model(
339
+ model_name: str, annotated_fields: Dict[str, Annotated[Any, FieldInfo]]
340
+ ) -> BaseModel:
341
+ """Convert a dictionary of Annotated values to a Pydantic BaseModel.
342
+
343
+ :param model_name: name of the model to be created
344
+ :param annotated_fields: dict containing the parameters and annotated values that should become
345
+ the properties of the model
346
+ :returns: pydantic model
347
+ """
348
+ fields = {
349
+ name: (field.__args__[0], field.__metadata__[0])
350
+ for name, field in annotated_fields.items()
351
+ }
352
+ return create_model(
353
+ model_name,
354
+ **fields, # type: ignore
355
+ __config__={"arbitrary_types_allowed": True},
356
+ )
357
+
358
+
318
359
  class ProviderSortables(TypedDict):
319
360
  """A class representing sortable parameter(s) of a provider and the allowed
320
361
  maximum number of used sortable(s) in a search request with the provider
@@ -17,7 +17,7 @@
17
17
  # limitations under the License.
18
18
  from __future__ import annotations
19
19
 
20
- from typing import Dict, Optional, TypedDict
20
+ from typing import Dict, Optional, TypedDict, Union
21
21
 
22
22
 
23
23
  class DownloadConf(TypedDict, total=False):
@@ -33,7 +33,7 @@ class DownloadConf(TypedDict, total=False):
33
33
  """
34
34
 
35
35
  output_dir: str
36
- output_extension: str
36
+ output_extension: Union[str, None]
37
37
  extract: bool
38
38
  dl_url_params: Dict[str, str]
39
39
  delete_archive: bool