eodag 3.10.1__py3-none-any.whl → 4.0.0a2__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 (75) hide show
  1. eodag/__init__.py +6 -1
  2. eodag/api/collection.py +353 -0
  3. eodag/api/core.py +606 -641
  4. eodag/api/product/__init__.py +3 -3
  5. eodag/api/product/_product.py +74 -56
  6. eodag/api/product/drivers/__init__.py +4 -46
  7. eodag/api/product/drivers/base.py +0 -28
  8. eodag/api/product/metadata_mapping.py +178 -216
  9. eodag/api/search_result.py +156 -15
  10. eodag/cli.py +83 -403
  11. eodag/config.py +81 -51
  12. eodag/plugins/apis/base.py +2 -2
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +55 -40
  15. eodag/plugins/authentication/base.py +1 -3
  16. eodag/plugins/crunch/filter_date.py +3 -3
  17. eodag/plugins/crunch/filter_latest_intersect.py +2 -2
  18. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -1
  19. eodag/plugins/download/aws.py +46 -42
  20. eodag/plugins/download/base.py +13 -14
  21. eodag/plugins/download/http.py +65 -65
  22. eodag/plugins/manager.py +28 -29
  23. eodag/plugins/search/__init__.py +6 -4
  24. eodag/plugins/search/base.py +131 -80
  25. eodag/plugins/search/build_search_result.py +245 -173
  26. eodag/plugins/search/cop_marine.py +87 -56
  27. eodag/plugins/search/csw.py +47 -37
  28. eodag/plugins/search/qssearch.py +653 -429
  29. eodag/plugins/search/stac_list_assets.py +1 -1
  30. eodag/plugins/search/static_stac_search.py +43 -44
  31. eodag/resources/{product_types.yml → collections.yml} +2594 -2453
  32. eodag/resources/ext_collections.json +1 -1
  33. eodag/resources/ext_product_types.json +1 -1
  34. eodag/resources/providers.yml +2706 -2733
  35. eodag/resources/stac_provider.yml +50 -92
  36. eodag/resources/user_conf_template.yml +9 -0
  37. eodag/types/__init__.py +2 -0
  38. eodag/types/queryables.py +70 -91
  39. eodag/types/search_args.py +1 -1
  40. eodag/utils/__init__.py +97 -21
  41. eodag/utils/dates.py +0 -12
  42. eodag/utils/exceptions.py +6 -6
  43. eodag/utils/free_text_search.py +3 -3
  44. eodag/utils/repr.py +2 -0
  45. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/METADATA +13 -99
  46. eodag-4.0.0a2.dist-info/RECORD +93 -0
  47. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/entry_points.txt +0 -4
  48. eodag/plugins/authentication/oauth.py +0 -60
  49. eodag/plugins/download/creodias_s3.py +0 -71
  50. eodag/plugins/download/s3rest.py +0 -351
  51. eodag/plugins/search/data_request_search.py +0 -565
  52. eodag/resources/stac.yml +0 -294
  53. eodag/resources/stac_api.yml +0 -2105
  54. eodag/rest/__init__.py +0 -24
  55. eodag/rest/cache.py +0 -70
  56. eodag/rest/config.py +0 -67
  57. eodag/rest/constants.py +0 -26
  58. eodag/rest/core.py +0 -764
  59. eodag/rest/errors.py +0 -210
  60. eodag/rest/server.py +0 -604
  61. eodag/rest/server.wsgi +0 -6
  62. eodag/rest/stac.py +0 -1032
  63. eodag/rest/templates/README +0 -1
  64. eodag/rest/types/__init__.py +0 -18
  65. eodag/rest/types/collections_search.py +0 -44
  66. eodag/rest/types/eodag_search.py +0 -386
  67. eodag/rest/types/queryables.py +0 -174
  68. eodag/rest/types/stac_search.py +0 -272
  69. eodag/rest/utils/__init__.py +0 -207
  70. eodag/rest/utils/cql_evaluate.py +0 -119
  71. eodag/rest/utils/rfc3339.py +0 -64
  72. eodag-3.10.1.dist-info/RECORD +0 -116
  73. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/WHEEL +0 -0
  74. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
  75. {eodag-3.10.1.dist-info → eodag-4.0.0a2.dist-info}/top_level.txt +0 -0
@@ -1 +0,0 @@
1
- Empty templates directory kept for compatibility reasons
@@ -1,18 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Copyright 2023, CS GROUP - France, https://www.csgroup.eu/
3
- #
4
- # This file is part of EODAG project
5
- # https://www.github.com/CS-SI/EODAG
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- """EODAG rest.types package"""
@@ -1,44 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Copyright 2023, CS GROUP - France, https://www.csgroup.eu/
3
- #
4
- # This file is part of EODAG project
5
- # https://www.github.com/CS-SI/EODAG
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- from typing import Any, Optional
19
-
20
- from pydantic import (
21
- BaseModel,
22
- ConfigDict,
23
- Field,
24
- SerializerFunctionWrapHandler,
25
- model_serializer,
26
- )
27
-
28
- from eodag.rest.types.eodag_search import EODAGSearch
29
-
30
-
31
- class CollectionsSearchRequest(BaseModel):
32
- """Search args for GET collections"""
33
-
34
- model_config = ConfigDict(frozen=True)
35
-
36
- q: Optional[str] = Field(default=None, serialization_alias="free_text")
37
- platform: Optional[str] = Field(default=None)
38
- instrument: Optional[str] = Field(default=None)
39
- constellation: Optional[str] = Field(default=None)
40
-
41
- @model_serializer(mode="wrap")
42
- def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
43
- dumped: dict[str, Any] = handler(self)
44
- return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
@@ -1,386 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Copyright 2023, CS GROUP - France, https://www.csgroup.eu/
3
- #
4
- # This file is part of EODAG project
5
- # https://www.github.com/CS-SI/EODAG
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- from __future__ import annotations
19
-
20
- from typing import TYPE_CHECKING, Any, Optional, Union, cast
21
-
22
- from pydantic import (
23
- AliasChoices,
24
- AliasPath,
25
- BaseModel,
26
- ConfigDict,
27
- Field,
28
- ValidationError,
29
- ValidationInfo,
30
- field_validator,
31
- model_validator,
32
- )
33
- from pydantic.alias_generators import to_camel, to_snake
34
- from pydantic_core import InitErrorDetails, PydanticCustomError
35
- from pygeofilter.parsers.cql2_json import parse as parse_json
36
- from shapely.geometry import (
37
- GeometryCollection,
38
- LinearRing,
39
- LineString,
40
- MultiLineString,
41
- MultiPoint,
42
- MultiPolygon,
43
- Point,
44
- Polygon,
45
- )
46
-
47
- from eodag.rest.utils import flatten_list, is_dict_str_any, list_to_str_list
48
- from eodag.rest.utils.cql_evaluate import EodagEvaluator
49
- from eodag.utils import DEFAULT_ITEMS_PER_PAGE
50
-
51
- if TYPE_CHECKING:
52
- from typing_extensions import Self
53
-
54
- Geometry = Union[
55
- dict[str, Any],
56
- Point,
57
- MultiPoint,
58
- LineString,
59
- MultiLineString,
60
- Polygon,
61
- MultiPolygon,
62
- LinearRing,
63
- GeometryCollection,
64
- ]
65
-
66
-
67
- class EODAGSearch(BaseModel):
68
- """Model used to convert a STAC formated request to an EODAG formated one"""
69
-
70
- model_config = ConfigDict(
71
- extra="allow", populate_by_name=True, arbitrary_types_allowed=True
72
- )
73
-
74
- productType: Optional[str] = Field(None, alias="collections", validate_default=True)
75
- provider: Optional[str] = Field(None)
76
- ids: Optional[list[str]] = Field(None)
77
- id: Optional[list[str]] = Field(
78
- None, alias="ids"
79
- ) # TODO: remove when updating queryables
80
- geom: Optional[Geometry] = Field(None, alias="geometry")
81
- start: Optional[str] = Field(None, alias="start_datetime")
82
- end: Optional[str] = Field(None, alias="end_datetime")
83
- startTimeFromAscendingNode: Optional[str] = Field(
84
- None,
85
- alias="start_datetime",
86
- validation_alias=AliasChoices("start_datetime", "datetime"),
87
- )
88
- completionTimeFromAscendingNode: Optional[str] = Field(None, alias="end_datetime")
89
- publicationDate: Optional[str] = Field(None, alias="published")
90
- creationDate: Optional[str] = Field(None, alias="created")
91
- modificationDate: Optional[str] = Field(None, alias="updated")
92
- platformSerialIdentifier: Optional[str] = Field(None, alias="platform")
93
- instrument: Optional[str] = Field(None, alias="instruments")
94
- platform: Optional[str] = Field(None, alias="constellation")
95
- resolution: Optional[Union[int, str]] = Field(None, alias="gsd")
96
- cloudCover: Optional[int] = Field(None, alias="eo:cloud_cover")
97
- snowCover: Optional[int] = Field(None, alias="eo:snow_cover")
98
- processingLevel: Optional[str] = Field(None, alias="processing:level")
99
- orbitDirection: Optional[str] = Field(None, alias="sat:orbit_state")
100
- relativeOrbitNumber: Optional[int] = Field(None, alias="sat:relative_orbit")
101
- orbitNumber: Optional[int] = Field(None, alias="sat:absolute_orbit")
102
- # TODO: colision in property name. Need to handle "sar:product_type"
103
- sensorMode: Optional[str] = Field(None, alias="sar:instrument_mode")
104
- polarizationChannels: Optional[list[str]] = Field(None, alias="sar:polarizations")
105
- dopplerFrequency: Optional[str] = Field(None, alias="sar:frequency_band")
106
- doi: Optional[str] = Field(None, alias="sci:doi")
107
- illuminationElevationAngle: Optional[float] = Field(
108
- None, alias="view:sun_elevation"
109
- )
110
- illuminationAzimuthAngle: Optional[float] = Field(None, alias="view:sun_azimuth")
111
- page: Optional[int] = Field(1)
112
- items_per_page: int = Field(DEFAULT_ITEMS_PER_PAGE, alias="limit")
113
- sort_by: Optional[list[tuple[str, str]]] = Field(None, alias="sortby")
114
- raise_errors: bool = False
115
-
116
- _to_eodag_map: dict[str, str]
117
-
118
- @model_validator(mode="after")
119
- def remove_timeFromAscendingNode(self) -> Self: # pylint: disable=invalid-name
120
- """TimeFromAscendingNode are just used for translation and not for search"""
121
- self.startTimeFromAscendingNode = None # pylint: disable=invalid-name
122
- self.completionTimeFromAscendingNode = None # pylint: disable=invalid-name
123
- return self
124
-
125
- @model_validator(mode="after")
126
- def parse_extra_fields(self) -> Self:
127
- """process unknown and oseo EODAG custom extensions fields"""
128
- # Transform EODAG custom extensions OSEO and UNK.
129
- if not self.__pydantic_extra__:
130
- return self
131
-
132
- keys_to_update: dict[str, str] = {}
133
- for key in self.__pydantic_extra__.keys():
134
- if key.startswith("unk:"):
135
- keys_to_update[key] = key[len("unk:") :]
136
- elif key.startswith("oseo:"):
137
- keys_to_update[key] = key[len("oseo:") :]
138
-
139
- for old_key, new_key in keys_to_update.items():
140
- self.__pydantic_extra__[
141
- to_camel(to_snake(new_key))
142
- ] = self.__pydantic_extra__.pop(old_key)
143
-
144
- return self
145
-
146
- @model_validator(mode="before")
147
- @classmethod
148
- def remove_keys(cls, values: dict[str, Any]) -> dict[str, Any]:
149
- """Remove 'datetime', 'crunch', 'intersects', and 'bbox' keys"""
150
- for key in ["datetime", "crunch", "intersects", "bbox", "filter_lang"]:
151
- values.pop(key, None)
152
- return values
153
-
154
- @model_validator(mode="before")
155
- @classmethod
156
- def parse_collections(
157
- cls, values: dict[str, Any], info: ValidationInfo
158
- ) -> dict[str, Any]:
159
- """convert collections to productType"""
160
-
161
- if collections := values.pop("collections", None):
162
- if len(collections) > 1:
163
- raise ValueError("Only one collection is supported per search")
164
- values["productType"] = collections[0]
165
- else:
166
- if not getattr(info, "context", None) or not info.context.get( # type: ignore
167
- "isCatalog"
168
- ):
169
- raise ValueError("A collection is required")
170
-
171
- return values
172
-
173
- @model_validator(mode="before")
174
- @classmethod
175
- def parse_query(cls, values: dict[str, Any]) -> dict[str, Any]:
176
- """
177
- Convert a STAC query parameter filter with the "eq" operator to a dict.
178
- """
179
-
180
- def add_error(error_message: str, input: Any) -> None:
181
- errors.append(
182
- InitErrorDetails(
183
- type=PydanticCustomError("invalid_query", error_message), # type: ignore
184
- loc=("query",),
185
- input=input,
186
- )
187
- )
188
-
189
- query = values.pop("query", None)
190
- if not query:
191
- return values
192
-
193
- query_props: dict[str, Any] = {}
194
- errors: list[InitErrorDetails] = []
195
- for property_name, conditions in cast(dict[str, Any], query).items():
196
- # Remove the prefix "properties." if present
197
- prop = property_name.replace("properties.", "", 1)
198
-
199
- # Check if exactly one operator is specified per property
200
- if not is_dict_str_any(conditions) or len(conditions) != 1: # type: ignore
201
- add_error(
202
- "Exactly 1 operator must be specified per property",
203
- query[property_name],
204
- )
205
- continue
206
-
207
- # Retrieve the operator and its value
208
- operator, value = next(iter(cast(dict[str, Any], conditions).items()))
209
-
210
- # Validate the operator
211
- # only eq, in and lte are allowed
212
- # lte is only supported with eo:cloud_cover
213
- # eo:cloud_cover only accept lte operator
214
- if (
215
- operator not in ("eq", "lte", "in")
216
- or (operator == "lte" and prop != "eo:cloud_cover")
217
- or (prop == "eo:cloud_cover" and operator != "lte")
218
- ):
219
- add_error(
220
- f'operator "{operator}" is not supported for property "{prop}"',
221
- query[property_name],
222
- )
223
- continue
224
- if operator == "in" and not isinstance(value, list):
225
- add_error(
226
- f'operator "{operator}" requires a value of type list for property "{prop}"',
227
- query[property_name],
228
- )
229
- continue
230
-
231
- query_props[prop] = value
232
-
233
- if errors:
234
- raise ValidationError.from_exception_data(
235
- title=cls.__name__, line_errors=errors
236
- )
237
-
238
- return {**values, **query_props}
239
-
240
- @model_validator(mode="before")
241
- @classmethod
242
- def parse_cql(cls, values: dict[str, Any]) -> dict[str, Any]:
243
- """
244
- Process cql2 filter
245
- """
246
-
247
- def add_error(error_message: str) -> None:
248
- errors.append(
249
- InitErrorDetails(
250
- type=PydanticCustomError("invalid_filter", error_message), # type: ignore
251
- loc=("filter",),
252
- )
253
- )
254
-
255
- filter_ = values.pop("filter", None)
256
- if not filter_:
257
- return values
258
-
259
- errors: list[InitErrorDetails] = []
260
- try:
261
- parsing_result = EodagEvaluator().evaluate(parse_json(filter_)) # type: ignore
262
- except (ValueError, NotImplementedError) as e:
263
- add_error(str(e))
264
- raise ValidationError.from_exception_data(
265
- title=cls.__name__, line_errors=errors
266
- ) from e
267
-
268
- if not is_dict_str_any(parsing_result):
269
- add_error("The parsed filter is not a proper dictionary")
270
- raise ValidationError.from_exception_data(
271
- title=cls.__name__, line_errors=errors
272
- )
273
-
274
- cql_args: dict[str, Any] = cast(dict[str, Any], parsing_result)
275
-
276
- invalid_keys = {
277
- "collections": 'Use "collection" instead of "collections"',
278
- "ids": 'Use "id" instead of "ids"',
279
- }
280
- for k, m in invalid_keys.items():
281
- if k in cql_args:
282
- add_error(m)
283
-
284
- if errors:
285
- raise ValidationError.from_exception_data(
286
- title=cls.__name__, line_errors=errors
287
- )
288
-
289
- # convert collection to EODAG collections
290
- if col := cql_args.pop("collection", None):
291
- cql_args["collections"] = col if isinstance(col, list) else [col]
292
-
293
- # convert id to EODAG ids
294
- if id := cql_args.pop("id", None):
295
- cql_args["ids"] = id if isinstance(id, list) else [id]
296
-
297
- return {**values, **cql_args}
298
-
299
- @field_validator("instrument", mode="before")
300
- @classmethod
301
- def join_instruments(cls, v: Union[str, list[str]]) -> str:
302
- """convert instruments to instrument"""
303
- if isinstance(v, list):
304
- return ",".join(v)
305
- return v
306
-
307
- @field_validator("sort_by", mode="before")
308
- @classmethod
309
- def parse_sortby(
310
- cls,
311
- sortby_post_params: list[dict[str, str]],
312
- ) -> list[tuple[str, str]]:
313
- """
314
- Convert STAC POST sortby to EODAG sort_by
315
- """
316
- special_fields = {
317
- "start": "startTimeFromAscendingNode",
318
- "end": "completionTimeFromAscendingNode",
319
- }
320
- return [
321
- (
322
- special_fields.get(
323
- to_camel(to_snake(cls.to_eodag(param["field"]))),
324
- to_camel(to_snake(cls.to_eodag(param["field"]))),
325
- ),
326
- param["direction"],
327
- )
328
- for param in sortby_post_params
329
- ]
330
-
331
- @field_validator("start", "end")
332
- @classmethod
333
- def cleanup_dates(cls, v: str) -> str:
334
- """proper format dates"""
335
- if v.endswith("+00:00"):
336
- return v.replace("+00:00", "") + "Z"
337
- return v
338
-
339
- @classmethod
340
- def _create_to_eodag_map(cls) -> None:
341
- """Create mapping to convert fields from STAC to EODAG"""
342
- cls._to_eodag_map = {}
343
- for name, field_info in cls.model_fields.items():
344
- if field_info.validation_alias:
345
- if isinstance(field_info.validation_alias, (AliasChoices, AliasPath)):
346
- for a in list_to_str_list(
347
- flatten_list(field_info.validation_alias.convert_to_aliases())
348
- ):
349
- cls._to_eodag_map[a] = name
350
- else:
351
- cls._to_eodag_map[field_info.validation_alias] = name
352
- elif field_info.alias:
353
- cls._to_eodag_map[field_info.alias] = name
354
-
355
- @classmethod
356
- def to_eodag(cls, value: str) -> str:
357
- """Convert a STAC parameter to its matching EODAG name"""
358
- if not isinstance(cls._to_eodag_map, dict) or not cls._to_eodag_map:
359
- cls._create_to_eodag_map()
360
- return cls._to_eodag_map.get(value, value)
361
-
362
- @classmethod
363
- def to_stac(
364
- cls,
365
- field_name: str,
366
- stac_item_properties: Optional[list[str]] = None,
367
- provider: Optional[str] = None,
368
- ) -> str:
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"
376
- field = cls.model_fields.get(field_name)
377
- if field is not None and field.alias is not None:
378
- return field.alias
379
- if (
380
- provider
381
- and ":" not in field_name
382
- and stac_item_properties
383
- and field_name not in stac_item_properties
384
- ):
385
- return f"{provider}:{field_name}"
386
- return field_name
@@ -1,174 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- # Copyright 2023, CS GROUP - France, https://www.csgroup.eu/
3
- #
4
- # This file is part of EODAG project
5
- # https://www.github.com/CS-SI/EODAG
6
- #
7
- # Licensed under the Apache License, Version 2.0 (the "License");
8
- # you may not use this file except in compliance with the License.
9
- # You may obtain a copy of the License at
10
- #
11
- # http://www.apache.org/licenses/LICENSE-2.0
12
- #
13
- # Unless required by applicable law or agreed to in writing, software
14
- # distributed under the License is distributed on an "AS IS" BASIS,
15
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- # See the License for the specific language governing permissions and
17
- # limitations under the License.
18
- from __future__ import annotations
19
-
20
- from typing import TYPE_CHECKING, Annotated, Any, ClassVar, Optional, Union
21
-
22
- from pydantic import (
23
- BaseModel,
24
- ConfigDict,
25
- Field,
26
- SerializationInfo,
27
- SerializerFunctionWrapHandler,
28
- field_validator,
29
- model_serializer,
30
- model_validator,
31
- )
32
-
33
- from eodag.rest.types.eodag_search import EODAGSearch
34
- from eodag.rest.utils.rfc3339 import str_to_interval
35
- from eodag.types import python_field_definition_to_json
36
-
37
- if TYPE_CHECKING:
38
- from pydantic.fields import FieldInfo
39
- from typing_extensions import Self
40
-
41
-
42
- class QueryablesGetParams(BaseModel):
43
- """Store GET Queryables query params"""
44
-
45
- collection: Optional[str] = Field(default=None, serialization_alias="productType")
46
- datetime: Optional[str] = Field(default=None)
47
- start_datetime: Optional[str] = Field(default=None)
48
- end_datetime: Optional[str] = Field(default=None)
49
-
50
- model_config = ConfigDict(extra="allow", frozen=True)
51
-
52
- @model_serializer(mode="wrap")
53
- def _serialize(self, handler: SerializerFunctionWrapHandler) -> dict[str, Any]:
54
- dumped: dict[str, Any] = handler(self)
55
- return {EODAGSearch.to_eodag(k): v for k, v in dumped.items()}
56
-
57
- @field_validator("datetime", "start_datetime", "end_datetime", mode="before")
58
- def validate_datetime(cls, value: Any) -> Optional[str]:
59
- """datetime, start_datetime and end_datetime must be a string"""
60
- if isinstance(value, list):
61
- return value[0]
62
-
63
- return value
64
-
65
- @model_validator(mode="after")
66
- def compute_datetimes(self: Self) -> Self:
67
- """Start datetime must be a string"""
68
- if not self.datetime:
69
- return self
70
-
71
- start, end = str_to_interval(self.datetime)
72
-
73
- if not self.start_datetime and start:
74
- self.start_datetime = start.strftime("%Y-%m-%dT%H:%M:%SZ")
75
-
76
- if not self.end_datetime and end:
77
- self.end_datetime = end.strftime("%Y-%m-%dT%H:%M:%SZ")
78
-
79
- return self
80
-
81
-
82
- class StacQueryableProperty(BaseModel):
83
- """A class representing a queryable property.
84
-
85
- :param description: The description of the queryables property
86
- :param ref: (optional) A reference link to the schema of the property.
87
- :param type: (optional) possible types of the property
88
- """
89
-
90
- description: str
91
- ref: Optional[str] = Field(default=None, serialization_alias="$ref")
92
- type: Optional[Union[str, list[str]]] = None
93
- enum: Optional[list[Any]] = None
94
- value: Optional[Any] = None
95
- min: Optional[Union[int, list[Union[int, None]]]] = None
96
- max: Optional[Union[int, list[Union[int, None]]]] = None
97
- oneOf: Optional[list[Any]] = None
98
- items: Optional[Any] = None
99
-
100
- @classmethod
101
- def from_python_field_definition(
102
- cls, id: str, python_field_definition: Annotated[Any, FieldInfo]
103
- ) -> StacQueryableProperty:
104
- """Build Model from python_field_definition"""
105
- def_dict = python_field_definition_to_json(python_field_definition)
106
-
107
- if not def_dict.get("description"):
108
- def_dict["description"] = def_dict.get("title") or id
109
-
110
- return cls(**def_dict)
111
-
112
- @model_serializer(mode="wrap")
113
- def remove_none(
114
- self,
115
- handler: SerializerFunctionWrapHandler,
116
- _: SerializationInfo,
117
- ):
118
- """Remove none value property fields during serialization"""
119
- props: dict[str, Any] = handler(self)
120
- return {k: v for k, v in props.items() if v is not None}
121
-
122
-
123
- class StacQueryables(BaseModel):
124
- """A class representing queryable properties for the STAC API.
125
-
126
- :param json_schema: The URL of the JSON schema.
127
- :param q_id: (optional) The identifier of the queryables.
128
- :param q_type: The type of the object.
129
- :param title: The title of the queryables.
130
- :param description: The description of the queryables
131
- :param properties: A dictionary of queryable properties.
132
- :param additional_properties: Whether additional properties are allowed.
133
- """
134
-
135
- json_schema: str = Field(
136
- default="https://json-schema.org/draft/2019-09/schema",
137
- serialization_alias="$schema",
138
- )
139
- q_id: Optional[str] = Field(default=None, serialization_alias="$id")
140
- q_type: str = Field(default="object", serialization_alias="type")
141
- title: str = Field(default="Queryables for EODAG STAC API")
142
- description: str = Field(
143
- default="Queryable names for the EODAG STAC API Item Search filter."
144
- )
145
- default_properties: ClassVar[dict[str, StacQueryableProperty]] = {
146
- "collection": StacQueryableProperty(
147
- description="Collection",
148
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection",
149
- )
150
- }
151
- possible_properties: ClassVar[dict[str, StacQueryableProperty]] = {
152
- "geometry": StacQueryableProperty(
153
- description="Geometry",
154
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry",
155
- ),
156
- "datetime": StacQueryableProperty(
157
- description="Datetime - use parameters year, month, day, time instead if available",
158
- ref="https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime",
159
- ),
160
- "bbox": StacQueryableProperty(
161
- description="BBox",
162
- type="array",
163
- oneOf=[{"minItems": 4, "maxItems": 4}, {"minItems": 6, "maxItems": 6}],
164
- items={"type": "number"},
165
- ),
166
- }
167
- properties: dict[str, Any] = Field()
168
- required: Optional[list[str]] = Field(None)
169
- additional_properties: bool = Field(
170
- default=True, serialization_alias="additionalProperties"
171
- )
172
-
173
- def __contains__(self, name: str) -> bool:
174
- return name in self.properties