eodag 3.0.1__py3-none-any.whl → 3.1.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 (87) hide show
  1. eodag/api/core.py +174 -138
  2. eodag/api/product/_assets.py +44 -15
  3. eodag/api/product/_product.py +58 -47
  4. eodag/api/product/drivers/__init__.py +81 -4
  5. eodag/api/product/drivers/base.py +65 -4
  6. eodag/api/product/drivers/generic.py +65 -0
  7. eodag/api/product/drivers/sentinel1.py +97 -0
  8. eodag/api/product/drivers/sentinel2.py +95 -0
  9. eodag/api/product/metadata_mapping.py +117 -90
  10. eodag/api/search_result.py +13 -23
  11. eodag/cli.py +26 -5
  12. eodag/config.py +86 -92
  13. eodag/plugins/apis/base.py +1 -1
  14. eodag/plugins/apis/ecmwf.py +42 -22
  15. eodag/plugins/apis/usgs.py +17 -16
  16. eodag/plugins/authentication/aws_auth.py +16 -13
  17. eodag/plugins/authentication/base.py +5 -3
  18. eodag/plugins/authentication/header.py +3 -3
  19. eodag/plugins/authentication/keycloak.py +4 -4
  20. eodag/plugins/authentication/oauth.py +7 -3
  21. eodag/plugins/authentication/openid_connect.py +22 -16
  22. eodag/plugins/authentication/sas_auth.py +4 -4
  23. eodag/plugins/authentication/token.py +41 -10
  24. eodag/plugins/authentication/token_exchange.py +1 -1
  25. eodag/plugins/base.py +4 -4
  26. eodag/plugins/crunch/base.py +4 -4
  27. eodag/plugins/crunch/filter_date.py +4 -4
  28. eodag/plugins/crunch/filter_latest_intersect.py +6 -6
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
  30. eodag/plugins/crunch/filter_overlap.py +4 -4
  31. eodag/plugins/crunch/filter_property.py +6 -7
  32. eodag/plugins/download/aws.py +146 -87
  33. eodag/plugins/download/base.py +38 -56
  34. eodag/plugins/download/creodias_s3.py +29 -0
  35. eodag/plugins/download/http.py +173 -183
  36. eodag/plugins/download/s3rest.py +10 -11
  37. eodag/plugins/manager.py +10 -20
  38. eodag/plugins/search/__init__.py +6 -5
  39. eodag/plugins/search/base.py +90 -46
  40. eodag/plugins/search/build_search_result.py +1048 -361
  41. eodag/plugins/search/cop_marine.py +22 -12
  42. eodag/plugins/search/creodias_s3.py +9 -73
  43. eodag/plugins/search/csw.py +11 -11
  44. eodag/plugins/search/data_request_search.py +19 -18
  45. eodag/plugins/search/qssearch.py +99 -258
  46. eodag/plugins/search/stac_list_assets.py +85 -0
  47. eodag/plugins/search/static_stac_search.py +4 -4
  48. eodag/resources/ext_product_types.json +1 -1
  49. eodag/resources/product_types.yml +1134 -325
  50. eodag/resources/providers.yml +906 -2006
  51. eodag/resources/stac_api.yml +2 -2
  52. eodag/resources/user_conf_template.yml +10 -9
  53. eodag/rest/cache.py +2 -2
  54. eodag/rest/config.py +3 -3
  55. eodag/rest/core.py +112 -82
  56. eodag/rest/errors.py +5 -5
  57. eodag/rest/server.py +33 -14
  58. eodag/rest/stac.py +41 -38
  59. eodag/rest/types/collections_search.py +3 -3
  60. eodag/rest/types/eodag_search.py +29 -23
  61. eodag/rest/types/queryables.py +42 -31
  62. eodag/rest/types/stac_search.py +15 -25
  63. eodag/rest/utils/__init__.py +14 -21
  64. eodag/rest/utils/cql_evaluate.py +6 -6
  65. eodag/rest/utils/rfc3339.py +2 -2
  66. eodag/types/__init__.py +141 -32
  67. eodag/types/bbox.py +2 -2
  68. eodag/types/download_args.py +3 -3
  69. eodag/types/queryables.py +183 -72
  70. eodag/types/search_args.py +4 -4
  71. eodag/types/whoosh.py +127 -3
  72. eodag/utils/__init__.py +153 -51
  73. eodag/utils/exceptions.py +28 -21
  74. eodag/utils/import_system.py +2 -2
  75. eodag/utils/repr.py +65 -6
  76. eodag/utils/requests.py +13 -13
  77. eodag/utils/rest.py +2 -2
  78. eodag/utils/s3.py +231 -0
  79. eodag/utils/stac_reader.py +10 -10
  80. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
  81. eodag-3.1.0.dist-info/RECORD +113 -0
  82. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
  83. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
  84. eodag/utils/constraints.py +0 -244
  85. eodag-3.0.1.dist-info/RECORD +0 -109
  86. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
  87. {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
@@ -19,17 +19,7 @@
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- from typing import (
23
- TYPE_CHECKING,
24
- Annotated,
25
- Any,
26
- Dict,
27
- List,
28
- Literal,
29
- Optional,
30
- Tuple,
31
- Union,
32
- )
22
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, Optional, Union
33
23
 
34
24
  import geojson
35
25
  from pydantic import (
@@ -63,8 +53,8 @@ if TYPE_CHECKING:
63
53
  NumType = Union[float, int]
64
54
 
65
55
  BBox = Union[
66
- Tuple[NumType, NumType, NumType, NumType],
67
- Tuple[NumType, NumType, NumType, NumType, NumType, NumType],
56
+ tuple[NumType, NumType, NumType, NumType],
57
+ tuple[NumType, NumType, NumType, NumType, NumType, NumType],
68
58
  ]
69
59
 
70
60
  Geometry = Union[
@@ -106,8 +96,8 @@ class SearchPostRequest(BaseModel):
106
96
  model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
107
97
 
108
98
  provider: Optional[str] = None
109
- collections: Optional[List[str]] = None
110
- ids: Optional[List[str]] = None
99
+ collections: Optional[list[str]] = None
100
+ ids: Optional[list[str]] = None
111
101
  bbox: Optional[BBox] = None
112
102
  intersects: Optional[Geometry] = None
113
103
  datetime: Optional[str] = None
@@ -117,21 +107,21 @@ class SearchPostRequest(BaseModel):
117
107
  page: Optional[PositiveInt] = Field( # type: ignore
118
108
  default=None, description="Page number, must be a positive integer."
119
109
  )
120
- query: Optional[Dict[str, Any]] = None
121
- filter: Optional[Dict[str, Any]] = None
110
+ query: Optional[dict[str, Any]] = None
111
+ filter: Optional[dict[str, Any]] = None
122
112
  filter_lang: Optional[str] = Field(
123
113
  default=None,
124
114
  alias="filter-lang",
125
115
  description="The language used for filtering.",
126
116
  validate_default=True,
127
117
  )
128
- sortby: Optional[List[SortBy]] = None
118
+ sortby: Optional[list[SortBy]] = None
129
119
  crunch: Optional[str] = None
130
120
 
131
121
  @field_serializer("intersects")
132
122
  def serialize_intersects(
133
123
  self, intersects: Optional[Geometry]
134
- ) -> Optional[Dict[str, Any]]:
124
+ ) -> Optional[dict[str, Any]]:
135
125
  """Serialize intersects from shapely to a proper dict"""
136
126
  if intersects:
137
127
  return geojson.loads(geojson.dumps(intersects)) # type: ignore
@@ -150,7 +140,7 @@ class SearchPostRequest(BaseModel):
150
140
 
151
141
  @model_validator(mode="before")
152
142
  @classmethod
153
- def only_one_spatial(cls, values: Dict[str, Any]) -> Dict[str, Any]:
143
+ def only_one_spatial(cls, values: dict[str, Any]) -> dict[str, Any]:
154
144
  """Check bbox and intersects are not both supplied."""
155
145
  if "intersects" in values and "bbox" in values:
156
146
  raise ValueError("intersects and bbox parameters are mutually exclusive")
@@ -170,7 +160,7 @@ class SearchPostRequest(BaseModel):
170
160
 
171
161
  @field_validator("ids", "collections", mode="before")
172
162
  @classmethod
173
- def str_to_str_list(cls, v: Union[str, List[str]]) -> List[str]:
163
+ def str_to_str_list(cls, v: Union[str, list[str]]) -> list[str]:
174
164
  """Convert ids and collections strings to list of strings"""
175
165
  if isinstance(v, str):
176
166
  return [i.strip() for i in v.split(",")]
@@ -178,7 +168,7 @@ class SearchPostRequest(BaseModel):
178
168
 
179
169
  @field_validator("intersects", mode="before")
180
170
  @classmethod
181
- def validate_intersects(cls, v: Union[Dict[str, Any], Geometry]) -> Geometry:
171
+ def validate_intersects(cls, v: Union[dict[str, Any], Geometry]) -> Geometry:
182
172
  """Verify format of intersects"""
183
173
  if isinstance(v, BaseGeometry):
184
174
  return v
@@ -224,7 +214,7 @@ class SearchPostRequest(BaseModel):
224
214
  # Single date is interpreted as end date
225
215
  values = ["..", v]
226
216
 
227
- dates: List[str] = []
217
+ dates: list[str] = []
228
218
  for value in values:
229
219
  if value == ".." or value == "":
230
220
  dates.append("..")
@@ -267,13 +257,13 @@ class SearchPostRequest(BaseModel):
267
257
 
268
258
  def sortby2list(
269
259
  v: Optional[str],
270
- ) -> Optional[List[SortBy]]:
260
+ ) -> Optional[list[SortBy]]:
271
261
  """
272
262
  Convert sortby filter parameter GET syntax to POST syntax
273
263
  """
274
264
  if not v:
275
265
  return None
276
- sortby: List[SortBy] = []
266
+ sortby: list[SortBy] = []
277
267
  for sortby_param in v.split(","):
278
268
  sortby_param = sortby_param.strip()
279
269
  direction: Direction = "desc" if sortby_param.startswith("-") else "asc"
@@ -23,17 +23,7 @@ import logging
23
23
  import os
24
24
  from io import BufferedReader
25
25
  from shutil import make_archive, rmtree
26
- from typing import (
27
- TYPE_CHECKING,
28
- Any,
29
- Callable,
30
- Dict,
31
- Iterator,
32
- List,
33
- NamedTuple,
34
- Optional,
35
- Union,
36
- )
26
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, NamedTuple, Optional, Union
37
27
  from urllib.parse import unquote_plus, urlencode
38
28
 
39
29
  import orjson
@@ -55,12 +45,15 @@ __all__ = ["get_date", "get_datetime"]
55
45
 
56
46
  logger = logging.getLogger("eodag.rest.utils")
57
47
 
48
+ # Path of the liveness endpoint
49
+ LIVENESS_PROBE_PATH = "/_mgmt/ping"
50
+
58
51
 
59
52
  class Cruncher(NamedTuple):
60
53
  """Type hinted Cruncher namedTuple"""
61
54
 
62
55
  clazz: Callable[..., Any]
63
- config_params: List[str]
56
+ config_params: list[str]
64
57
 
65
58
 
66
59
  crunchers = {
@@ -87,19 +80,19 @@ def format_pydantic_error(e: pydanticValidationError) -> str:
87
80
 
88
81
  def is_dict_str_any(var: Any) -> bool:
89
82
  """Verify whether the variable is of type dict[str, Any]"""
90
- if isinstance(var, Dict):
83
+ if isinstance(var, dict):
91
84
  return all(isinstance(k, str) for k in var.keys()) # type: ignore
92
85
  return False
93
86
 
94
87
 
95
- def str2list(v: Optional[str]) -> Optional[List[str]]:
88
+ def str2list(v: Optional[str]) -> Optional[list[str]]:
96
89
  """Convert string to list base on , delimiter."""
97
90
  if v:
98
91
  return v.split(",")
99
92
  return None
100
93
 
101
94
 
102
- def str2json(k: str, v: Optional[str] = None) -> Optional[Dict[str, Any]]:
95
+ def str2json(k: str, v: Optional[str] = None) -> Optional[dict[str, Any]]:
103
96
  """decoding a URL parameter and then parsing it as JSON."""
104
97
  if not v:
105
98
  return None
@@ -109,25 +102,25 @@ def str2json(k: str, v: Optional[str] = None) -> Optional[Dict[str, Any]]:
109
102
  raise ValidationError(f"{k}: Incorrect JSON object") from e
110
103
 
111
104
 
112
- def flatten_list(nested_list: Union[Any, List[Any]]) -> List[Any]:
105
+ def flatten_list(nested_list: Union[Any, list[Any]]) -> list[Any]:
113
106
  """Flatten a nested list structure into a single list."""
114
107
  if not isinstance(nested_list, list):
115
108
  return [nested_list]
116
109
  else:
117
- flattened: List[Any] = []
110
+ flattened: list[Any] = []
118
111
  for element in nested_list:
119
112
  flattened.extend(flatten_list(element))
120
113
  return flattened
121
114
 
122
115
 
123
- def list_to_str_list(input_list: List[Any]) -> List[str]:
116
+ def list_to_str_list(input_list: list[Any]) -> list[str]:
124
117
  """Attempt to convert a list of any type to a list of strings."""
125
118
  try:
126
119
  # Try to convert each element to a string
127
120
  return [str(element) for element in input_list]
128
121
  except Exception as e:
129
122
  # Raise an exception if any element cannot be converted
130
- raise TypeError(f"Failed to convert to List[str]: {e}") from e
123
+ raise TypeError(f"Failed to convert to list[str]: {e}") from e
131
124
 
132
125
 
133
126
  def get_next_link(
@@ -135,7 +128,7 @@ def get_next_link(
135
128
  search_request: SearchPostRequest,
136
129
  total_results: Optional[int],
137
130
  items_per_page: int,
138
- ) -> Optional[Dict[str, Any]]:
131
+ ) -> Optional[dict[str, Any]]:
139
132
  """Generate next link URL and body"""
140
133
  body = search_request.model_dump(exclude_none=True)
141
134
  if "bbox" in body:
@@ -156,7 +149,7 @@ def get_next_link(
156
149
  params["page"] = str(page + 1)
157
150
  url += f"?{urlencode(params)}"
158
151
 
159
- next: Dict[str, Any] = {
152
+ next: dict[str, Any] = {
160
153
  "rel": "next",
161
154
  "href": url,
162
155
  "title": "Next page",
@@ -16,13 +16,13 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  from datetime import datetime as dt
19
- from typing import Any, Dict, List, Optional, Tuple, Union
19
+ from typing import Any, Optional, Union
20
20
 
21
21
  from pygeofilter import ast
22
22
  from pygeofilter.backends.evaluator import Evaluator, handle
23
23
  from pygeofilter.values import Geometry, Interval
24
24
 
25
- simpleNode = Union[ast.Attribute, str, int, complex, float, List[Any], Tuple[Any, ...]]
25
+ simpleNode = Union[ast.Attribute, str, int, complex, float, list[Any], tuple[Any, ...]]
26
26
 
27
27
 
28
28
  class EodagEvaluator(Evaluator):
@@ -36,7 +36,7 @@ class EodagEvaluator(Evaluator):
36
36
  return node
37
37
 
38
38
  @handle(Geometry)
39
- def spatial(self, node: Geometry) -> Dict[str, Any]:
39
+ def spatial(self, node: Geometry) -> dict[str, Any]:
40
40
  """handle geometry"""
41
41
  return node.geometry
42
42
 
@@ -46,7 +46,7 @@ class EodagEvaluator(Evaluator):
46
46
  return node.strftime("%Y-%m-%dT%H:%M:%SZ")
47
47
 
48
48
  @handle(Interval)
49
- def interval(self, _, *interval: Any) -> List[Any]:
49
+ def interval(self, _, *interval: Any) -> list[Any]:
50
50
  """handle datetime interval"""
51
51
  return list(interval)
52
52
 
@@ -60,7 +60,7 @@ class EodagEvaluator(Evaluator):
60
60
  )
61
61
  def predicate(
62
62
  self, node: ast.Predicate, lhs: Any, rhs: Any
63
- ) -> Optional[Dict[str, Any]]:
63
+ ) -> Optional[dict[str, Any]]:
64
64
  """
65
65
  Handle predicates
66
66
  Verify the property is first attribute in each predicate
@@ -114,6 +114,6 @@ class EodagEvaluator(Evaluator):
114
114
  return {lhs.name: list(rhs)}
115
115
 
116
116
  @handle(ast.And)
117
- def combination(self, _, lhs: Dict[str, str], rhs: Dict[str, str]):
117
+ def combination(self, _, lhs: dict[str, str], rhs: dict[str, str]):
118
118
  """handle combinations"""
119
119
  return {**lhs, **rhs}
@@ -16,14 +16,14 @@
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
18
  import datetime
19
- from typing import Optional, Tuple
19
+ from typing import Optional
20
20
 
21
21
  from eodag.utils.rest import rfc3339_str_to_datetime
22
22
 
23
23
 
24
24
  def str_to_interval(
25
25
  interval: Optional[str],
26
- ) -> Tuple[Optional[datetime.datetime], Optional[datetime.datetime]]:
26
+ ) -> tuple[Optional[datetime.datetime], Optional[datetime.datetime]]:
27
27
  """Extract a tuple of datetimes from an interval string.
28
28
 
29
29
  Interval strings are defined by
eodag/types/__init__.py CHANGED
@@ -16,16 +16,15 @@
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 (
22
23
  Annotated,
23
24
  Any,
24
- Dict,
25
- List,
26
25
  Literal,
27
26
  Optional,
28
- Tuple,
27
+ Type,
29
28
  TypedDict,
30
29
  Union,
31
30
  get_args,
@@ -33,15 +32,18 @@ from typing import (
33
32
  )
34
33
 
35
34
  from annotated_types import Gt, Lt
36
- from pydantic import Field
35
+ from pydantic import BaseModel, ConfigDict, Field, create_model
36
+ from pydantic.annotated_handlers import GetJsonSchemaHandler
37
37
  from pydantic.fields import FieldInfo
38
+ from pydantic.json_schema import JsonSchemaValue
39
+ from pydantic_core import CoreSchema
38
40
 
39
41
  from eodag.utils import copy_deepcopy
40
42
  from eodag.utils.exceptions import ValidationError
41
43
 
42
44
  # Types mapping from JSON Schema and OpenAPI 3.1.0 specifications to Python
43
45
  # See https://spec.openapis.org/oas/v3.1.0#data-types
44
- JSON_TYPES_MAPPING: Dict[str, type] = {
46
+ JSON_TYPES_MAPPING: dict[str, type] = {
45
47
  "boolean": bool,
46
48
  "integer": int,
47
49
  "number": float,
@@ -52,7 +54,7 @@ JSON_TYPES_MAPPING: Dict[str, type] = {
52
54
  }
53
55
 
54
56
 
55
- def json_type_to_python(json_type: Union[str, List[str]]) -> type:
57
+ def json_type_to_python(json_type: Union[str, list[str]]) -> type:
56
58
  """Get python type from json type https://spec.openapis.org/oas/v3.1.0#data-types
57
59
 
58
60
  >>> json_type_to_python("number")
@@ -69,9 +71,9 @@ def json_type_to_python(json_type: Union[str, List[str]]) -> type:
69
71
  return type(None)
70
72
 
71
73
 
72
- 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
+ def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> tuple[str, Any]:
75
+ """Checks if the value from an Annotated object is a minimum or maximum
76
+
75
77
  :param type_info: info from Annotated
76
78
  :return: "min" or "max"
77
79
  """
@@ -83,10 +85,10 @@ def _get_min_or_max(type_info: Union[Lt, Gt, Any]) -> Tuple[str, Any]:
83
85
 
84
86
 
85
87
  def _get_type_info_from_annotated(
86
- annotated_type: Annotated[type, Any]
87
- ) -> Dict[str, Any]:
88
- """
89
- retrieves type information from an annotated object
88
+ annotated_type: Annotated[type, Any],
89
+ ) -> dict[str, Any]:
90
+ """Retrieves type information from an annotated object
91
+
90
92
  :param annotated_type: annotated object
91
93
  :return: dict containing type and min/max if available
92
94
  """
@@ -107,7 +109,7 @@ def _get_type_info_from_annotated(
107
109
 
108
110
  def python_type_to_json(
109
111
  python_type: type,
110
- ) -> Optional[Union[str, List[Dict[str, Any]]]]:
112
+ ) -> Optional[Union[str, list[dict[str, Any]]]]:
111
113
  """Get json type from python https://spec.openapis.org/oas/v3.1.0#data-types
112
114
 
113
115
  >>> python_type_to_json(int)
@@ -118,7 +120,8 @@ def python_type_to_json(
118
120
  :param python_type: the python type
119
121
  :returns: the json type
120
122
  """
121
- if get_origin(python_type) is Union:
123
+ origin = get_origin(python_type)
124
+ if origin is Union:
122
125
  json_type = list()
123
126
  for single_python_type in get_args(python_type):
124
127
  type_data = {}
@@ -138,14 +141,16 @@ def python_type_to_json(
138
141
  return list(JSON_TYPES_MAPPING.keys())[
139
142
  list(JSON_TYPES_MAPPING.values()).index(python_type)
140
143
  ]
141
- elif get_origin(python_type) == Annotated:
144
+ elif origin is Annotated:
142
145
  return [_get_type_info_from_annotated(python_type)]
146
+ elif origin is list:
147
+ raise NotImplementedError("Never completed")
143
148
  else:
144
149
  return None
145
150
 
146
151
 
147
152
  def json_field_definition_to_python(
148
- json_field_definition: Dict[str, Any],
153
+ json_field_definition: dict[str, Any],
149
154
  default_value: Optional[Any] = None,
150
155
  required: Optional[bool] = False,
151
156
  ) -> Annotated[Any, FieldInfo]:
@@ -173,25 +178,48 @@ def json_field_definition_to_python(
173
178
  title=json_field_definition.get("title", None),
174
179
  description=json_field_definition.get("description", None),
175
180
  pattern=json_field_definition.get("pattern", None),
181
+ le=json_field_definition.get("maximum"),
182
+ ge=json_field_definition.get("minimum"),
176
183
  )
177
184
 
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
185
+ enum = json_field_definition.get("enum")
186
+
187
+ if python_type in (list, set):
188
+ items = json_field_definition.get("items", None)
189
+ if isinstance(items, list):
190
+ python_type = tuple[ # type: ignore
191
+ tuple(
192
+ json_field_definition_to_python(item, required=required)
193
+ for item in items
194
+ )
195
+ ]
196
+ elif isinstance(items, dict):
197
+ enum = items.get("enum")
198
+
199
+ if enum:
200
+ literal = Literal[tuple(sorted(enum))] # type: ignore
201
+ python_type = list[literal] if python_type in (list, set) else literal # type: ignore
182
202
 
183
203
  if "$ref" in json_field_definition:
184
204
  field_type_kwargs["json_schema_extra"] = {"$ref": json_field_definition["$ref"]}
185
205
 
186
- if not required or default_value:
187
- return Annotated[python_type, Field(default=default_value, **field_type_kwargs)]
188
- else:
189
- return Annotated[python_type, Field(..., **field_type_kwargs)]
206
+ metadata = [
207
+ python_type,
208
+ Field(
209
+ default_value if not required or default_value is not None else ...,
210
+ **field_type_kwargs,
211
+ ),
212
+ ]
213
+
214
+ if required:
215
+ metadata.append("json_schema_required")
216
+
217
+ return Annotated[tuple(metadata)]
190
218
 
191
219
 
192
220
  def python_field_definition_to_json(
193
- python_field_definition: Annotated[Any, FieldInfo]
194
- ) -> Dict[str, Any]:
221
+ python_field_definition: Annotated[Any, FieldInfo],
222
+ ) -> dict[str, Any]:
195
223
  """Get json field definition from python `typing.Annotated`
196
224
 
197
225
  >>> from pydantic import Field
@@ -212,7 +240,7 @@ def python_field_definition_to_json(
212
240
  "%s must be an instance of Annotated" % python_field_definition
213
241
  )
214
242
 
215
- json_field_definition: Dict[str, Any] = dict()
243
+ json_field_definition: dict[str, Any] = dict()
216
244
 
217
245
  python_field_args = get_args(python_field_definition)
218
246
 
@@ -252,6 +280,7 @@ def python_field_definition_to_json(
252
280
  json_field_definition["max"] = [
253
281
  row["max"] if "max" in row else None for row in field_type
254
282
  ]
283
+
255
284
  if "min" in json_field_definition and json_field_definition["min"].count(
256
285
  None
257
286
  ) == len(json_field_definition["min"]):
@@ -291,8 +320,8 @@ def python_field_definition_to_json(
291
320
 
292
321
 
293
322
  def model_fields_to_annotated(
294
- model_fields: Dict[str, FieldInfo]
295
- ) -> Dict[str, Annotated[Any, FieldInfo]]:
323
+ model_fields: dict[str, FieldInfo],
324
+ ) -> dict[str, Annotated[Any, FieldInfo]]:
296
325
  """Convert BaseModel.model_fields from FieldInfo to Annotated
297
326
 
298
327
  >>> from pydantic import create_model
@@ -306,7 +335,7 @@ def model_fields_to_annotated(
306
335
  :param model_fields: BaseModel.model_fields to convert
307
336
  :returns: Annotated tuple usable as create_model argument
308
337
  """
309
- annotated_model_fields = dict()
338
+ annotated_model_fields: dict[str, Annotated[Any, FieldInfo]] = dict()
310
339
  for param, field_info in model_fields.items():
311
340
  field_type = field_info.annotation or type(None)
312
341
  new_field_info = copy_deepcopy(field_info)
@@ -315,6 +344,77 @@ def model_fields_to_annotated(
315
344
  return annotated_model_fields
316
345
 
317
346
 
347
+ class BaseModelCustomJsonSchema(BaseModel):
348
+ """Base class for generated models with custom json schema."""
349
+
350
+ @classmethod
351
+ def __get_pydantic_json_schema__(
352
+ cls: Type[BaseModel], core_schema: CoreSchema, handler: GetJsonSchemaHandler
353
+ ) -> JsonSchemaValue:
354
+ """
355
+ Custom get json schema method to handle required fields with default values.
356
+ This is not supported by Pydantic by default.
357
+ It requires the field to be marked with the key "json_schema_required" in the metadata dict.
358
+ Example: Annotated[str, "json_schema_required"]
359
+ """
360
+ json_schema = handler.resolve_ref_schema(handler(core_schema))
361
+
362
+ json_schema["required"] = [
363
+ key
364
+ for key, field_info in cls.model_fields.items()
365
+ if "json_schema_required" in field_info.metadata
366
+ ]
367
+
368
+ return json_schema
369
+
370
+ model_config = ConfigDict(arbitrary_types_allowed=True)
371
+
372
+
373
+ def annotated_dict_to_model(
374
+ model_name: str, annotated_fields: dict[str, Annotated[Any, FieldInfo]]
375
+ ) -> BaseModel:
376
+ """Convert a dictionary of Annotated values to a Pydantic BaseModel.
377
+
378
+ >>> from pydantic import Field
379
+ >>> annotated_fields = {
380
+ ... "field1": Annotated[str, Field(description="A string field"), "json_schema_required"],
381
+ ... "field2": Annotated[int, Field(default=42, description="An integer field")],
382
+ ... }
383
+ >>> model = annotated_dict_to_model("TestModel", annotated_fields)
384
+ >>> json_schema = model.model_json_schema()
385
+ >>> json_schema["required"]
386
+ ['field1']
387
+ >>> json_schema["properties"]["field1"]
388
+ {'description': 'A string field', 'title': 'Field1', 'type': 'string'}
389
+ >>> json_schema["properties"]["field2"]
390
+ {'default': 42, 'description': 'An integer field', 'title': 'Field2', 'type': 'integer'}
391
+ >>> json_schema["title"]
392
+ 'TestModel'
393
+ >>> json_schema["type"]
394
+ 'object'
395
+
396
+ :param model_name: name of the model to be created
397
+ :param annotated_fields: dict containing the parameters and annotated values that should become
398
+ the properties of the model
399
+ :returns: pydantic model
400
+ """
401
+ fields = {}
402
+ for name, field in annotated_fields.items():
403
+ base_type, field_info, *metadata = get_args(field)
404
+ fields[name] = (
405
+ Annotated[tuple([base_type] + metadata)] if metadata else base_type,
406
+ field_info,
407
+ )
408
+
409
+ custom_model = create_model(
410
+ model_name,
411
+ __base__=BaseModelCustomJsonSchema,
412
+ **fields, # type: ignore
413
+ )
414
+
415
+ return custom_model
416
+
417
+
318
418
  class ProviderSortables(TypedDict):
319
419
  """A class representing sortable parameter(s) of a provider and the allowed
320
420
  maximum number of used sortable(s) in a search request with the provider
@@ -323,5 +423,14 @@ class ProviderSortables(TypedDict):
323
423
  :param max_sort_params: (optional) The allowed maximum number of sortable(s) in a search request with the provider
324
424
  """
325
425
 
326
- sortables: List[str]
426
+ sortables: list[str]
327
427
  max_sort_params: Annotated[Optional[int], Gt(0)]
428
+
429
+
430
+ class S3SessionKwargs(TypedDict, total=False):
431
+ """A class representing available keyword arguments to pass to :class:`boto3.session.Session` for authentication"""
432
+
433
+ aws_access_key_id: Optional[str]
434
+ aws_secret_access_key: Optional[str]
435
+ aws_session_token: Optional[str]
436
+ profile_name: Optional[str]
eodag/types/bbox.py CHANGED
@@ -15,14 +15,14 @@
15
15
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
16
  # See the License for the specific language governing permissions and
17
17
  # limitations under the License.
18
- from typing import Dict, List, Tuple, Union
18
+ from typing import Union
19
19
 
20
20
  from pydantic import BaseModel, ValidationInfo, field_validator
21
21
  from shapely.geometry.polygon import Polygon
22
22
 
23
23
  NumType = Union[float, int]
24
24
  BBoxArgs = Union[
25
- List[NumType], Tuple[NumType, NumType, NumType, NumType], Dict[str, NumType]
25
+ list[NumType], tuple[NumType, NumType, NumType, NumType], dict[str, NumType]
26
26
  ]
27
27
 
28
28
 
@@ -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 Optional, TypedDict, Union
21
21
 
22
22
 
23
23
  class DownloadConf(TypedDict, total=False):
@@ -33,8 +33,8 @@ 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
- dl_url_params: Dict[str, str]
38
+ dl_url_params: dict[str, str]
39
39
  delete_archive: bool
40
40
  asset: Optional[str]