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.
- eodag/api/core.py +174 -138
- eodag/api/product/_assets.py +44 -15
- eodag/api/product/_product.py +58 -47
- eodag/api/product/drivers/__init__.py +81 -4
- eodag/api/product/drivers/base.py +65 -4
- eodag/api/product/drivers/generic.py +65 -0
- eodag/api/product/drivers/sentinel1.py +97 -0
- eodag/api/product/drivers/sentinel2.py +95 -0
- eodag/api/product/metadata_mapping.py +117 -90
- eodag/api/search_result.py +13 -23
- eodag/cli.py +26 -5
- eodag/config.py +86 -92
- eodag/plugins/apis/base.py +1 -1
- eodag/plugins/apis/ecmwf.py +42 -22
- eodag/plugins/apis/usgs.py +17 -16
- eodag/plugins/authentication/aws_auth.py +16 -13
- eodag/plugins/authentication/base.py +5 -3
- eodag/plugins/authentication/header.py +3 -3
- eodag/plugins/authentication/keycloak.py +4 -4
- eodag/plugins/authentication/oauth.py +7 -3
- eodag/plugins/authentication/openid_connect.py +22 -16
- eodag/plugins/authentication/sas_auth.py +4 -4
- eodag/plugins/authentication/token.py +41 -10
- eodag/plugins/authentication/token_exchange.py +1 -1
- eodag/plugins/base.py +4 -4
- eodag/plugins/crunch/base.py +4 -4
- eodag/plugins/crunch/filter_date.py +4 -4
- eodag/plugins/crunch/filter_latest_intersect.py +6 -6
- eodag/plugins/crunch/filter_latest_tpl_name.py +7 -7
- eodag/plugins/crunch/filter_overlap.py +4 -4
- eodag/plugins/crunch/filter_property.py +6 -7
- eodag/plugins/download/aws.py +146 -87
- eodag/plugins/download/base.py +38 -56
- eodag/plugins/download/creodias_s3.py +29 -0
- eodag/plugins/download/http.py +173 -183
- eodag/plugins/download/s3rest.py +10 -11
- eodag/plugins/manager.py +10 -20
- eodag/plugins/search/__init__.py +6 -5
- eodag/plugins/search/base.py +90 -46
- eodag/plugins/search/build_search_result.py +1048 -361
- eodag/plugins/search/cop_marine.py +22 -12
- eodag/plugins/search/creodias_s3.py +9 -73
- eodag/plugins/search/csw.py +11 -11
- eodag/plugins/search/data_request_search.py +19 -18
- eodag/plugins/search/qssearch.py +99 -258
- eodag/plugins/search/stac_list_assets.py +85 -0
- eodag/plugins/search/static_stac_search.py +4 -4
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +1134 -325
- eodag/resources/providers.yml +906 -2006
- eodag/resources/stac_api.yml +2 -2
- eodag/resources/user_conf_template.yml +10 -9
- eodag/rest/cache.py +2 -2
- eodag/rest/config.py +3 -3
- eodag/rest/core.py +112 -82
- eodag/rest/errors.py +5 -5
- eodag/rest/server.py +33 -14
- eodag/rest/stac.py +41 -38
- eodag/rest/types/collections_search.py +3 -3
- eodag/rest/types/eodag_search.py +29 -23
- eodag/rest/types/queryables.py +42 -31
- eodag/rest/types/stac_search.py +15 -25
- eodag/rest/utils/__init__.py +14 -21
- eodag/rest/utils/cql_evaluate.py +6 -6
- eodag/rest/utils/rfc3339.py +2 -2
- eodag/types/__init__.py +141 -32
- eodag/types/bbox.py +2 -2
- eodag/types/download_args.py +3 -3
- eodag/types/queryables.py +183 -72
- eodag/types/search_args.py +4 -4
- eodag/types/whoosh.py +127 -3
- eodag/utils/__init__.py +153 -51
- eodag/utils/exceptions.py +28 -21
- eodag/utils/import_system.py +2 -2
- eodag/utils/repr.py +65 -6
- eodag/utils/requests.py +13 -13
- eodag/utils/rest.py +2 -2
- eodag/utils/s3.py +231 -0
- eodag/utils/stac_reader.py +10 -10
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/METADATA +77 -76
- eodag-3.1.0.dist-info/RECORD +113 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/WHEEL +1 -1
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/entry_points.txt +4 -2
- eodag/utils/constraints.py +0 -244
- eodag-3.0.1.dist-info/RECORD +0 -109
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/LICENSE +0 -0
- {eodag-3.0.1.dist-info → eodag-3.1.0.dist-info}/top_level.txt +0 -0
eodag/rest/types/stac_search.py
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
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[
|
|
110
|
-
ids: Optional[
|
|
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[
|
|
121
|
-
filter: Optional[
|
|
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[
|
|
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[
|
|
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:
|
|
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,
|
|
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[
|
|
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:
|
|
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[
|
|
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:
|
|
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"
|
eodag/rest/utils/__init__.py
CHANGED
|
@@ -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:
|
|
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,
|
|
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[
|
|
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[
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
|
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[
|
|
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:
|
|
152
|
+
next: dict[str, Any] = {
|
|
160
153
|
"rel": "next",
|
|
161
154
|
"href": url,
|
|
162
155
|
"title": "Next page",
|
eodag/rest/utils/cql_evaluate.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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) ->
|
|
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) ->
|
|
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[
|
|
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:
|
|
117
|
+
def combination(self, _, lhs: dict[str, str], rhs: dict[str, str]):
|
|
118
118
|
"""handle combinations"""
|
|
119
119
|
return {**lhs, **rhs}
|
eodag/rest/utils/rfc3339.py
CHANGED
|
@@ -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
|
|
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
|
-
) ->
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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]) ->
|
|
73
|
-
"""
|
|
74
|
-
|
|
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
|
-
) ->
|
|
88
|
-
"""
|
|
89
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
):
|
|
181
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
) ->
|
|
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:
|
|
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:
|
|
295
|
-
) ->
|
|
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:
|
|
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
|
|
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
|
-
|
|
25
|
+
list[NumType], tuple[NumType, NumType, NumType, NumType], dict[str, NumType]
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
|
eodag/types/download_args.py
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
from typing import
|
|
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:
|
|
38
|
+
dl_url_params: dict[str, str]
|
|
39
39
|
delete_archive: bool
|
|
40
40
|
asset: Optional[str]
|