eodag 4.0.0a1__py3-none-any.whl → 4.0.0a3__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 (37) hide show
  1. eodag/__init__.py +6 -1
  2. eodag/api/collection.py +354 -0
  3. eodag/api/core.py +324 -303
  4. eodag/api/product/_product.py +15 -29
  5. eodag/api/product/drivers/__init__.py +2 -42
  6. eodag/api/product/drivers/base.py +0 -11
  7. eodag/api/product/metadata_mapping.py +34 -5
  8. eodag/api/search_result.py +144 -9
  9. eodag/cli.py +18 -15
  10. eodag/config.py +37 -3
  11. eodag/plugins/apis/ecmwf.py +16 -4
  12. eodag/plugins/apis/usgs.py +18 -7
  13. eodag/plugins/crunch/filter_latest_intersect.py +1 -0
  14. eodag/plugins/crunch/filter_overlap.py +3 -7
  15. eodag/plugins/search/__init__.py +3 -0
  16. eodag/plugins/search/base.py +6 -6
  17. eodag/plugins/search/build_search_result.py +157 -56
  18. eodag/plugins/search/cop_marine.py +48 -8
  19. eodag/plugins/search/csw.py +18 -8
  20. eodag/plugins/search/qssearch.py +331 -88
  21. eodag/plugins/search/static_stac_search.py +11 -12
  22. eodag/resources/collections.yml +610 -348
  23. eodag/resources/ext_collections.json +1 -1
  24. eodag/resources/ext_product_types.json +1 -1
  25. eodag/resources/providers.yml +334 -62
  26. eodag/resources/stac_provider.yml +4 -2
  27. eodag/resources/user_conf_template.yml +9 -0
  28. eodag/types/__init__.py +2 -0
  29. eodag/types/queryables.py +16 -0
  30. eodag/utils/__init__.py +47 -2
  31. eodag/utils/repr.py +2 -0
  32. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/METADATA +4 -2
  33. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/RECORD +37 -36
  34. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/WHEEL +0 -0
  35. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/entry_points.txt +0 -0
  36. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/licenses/LICENSE +0 -0
  37. {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/top_level.txt +0 -0
eodag/__init__.py CHANGED
@@ -35,4 +35,9 @@ except PackageNotFoundError: # pragma: no cover
35
35
  __version__ = "0.0.0"
36
36
 
37
37
  # exportable content
38
- __all__ = ["EODataAccessGateway", "EOProduct", "SearchResult", "setup_logging"]
38
+ __all__ = [
39
+ "EODataAccessGateway",
40
+ "EOProduct",
41
+ "SearchResult",
42
+ "setup_logging",
43
+ ]
@@ -0,0 +1,354 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2025, 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
+ import logging
21
+ import re
22
+ from collections import UserDict, UserList
23
+ from typing import TYPE_CHECKING, Any, Optional
24
+
25
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
26
+ from pydantic import ValidationError as PydanticValidationError
27
+ from pydantic import model_validator
28
+ from pydantic_core import ErrorDetails, InitErrorDetails, PydanticCustomError
29
+ from stac_pydantic.collection import Extent, Provider, SpatialExtent, TimeInterval
30
+ from stac_pydantic.links import Links
31
+
32
+ from eodag.utils.env import is_env_var_true
33
+ from eodag.utils.exceptions import ValidationError
34
+ from eodag.utils.repr import dict_to_html_table
35
+
36
+ if TYPE_CHECKING:
37
+ from pydantic import ModelWrapValidatorHandler
38
+ from typing_extensions import Self
39
+
40
+ from eodag.api.core import EODataAccessGateway
41
+ from eodag.api.search_result import SearchResult
42
+ from eodag.types.queryables import QueryablesDict
43
+
44
+ logger = logging.getLogger("eodag.api.collection")
45
+
46
+ RFC3339_PATTERN = (
47
+ r"^(\d{4})-(\d{2})-(\d{2})"
48
+ r"(?:T(\d{2}):(\d{2}):(\d{2})(\.\d+)?"
49
+ r"(Z|([+-])(\d{2}):(\d{2}))?)?$"
50
+ )
51
+
52
+
53
+ class Collection(BaseModel):
54
+ """A class representing a collection.
55
+
56
+ A Collection object is used to describe a group of related :class:`~eodag.api.product._product.EOProduct` objects.
57
+ """
58
+
59
+ id: str = Field()
60
+ title: Optional[str] = Field(default=None)
61
+ description: Optional[str] = Field(default=None)
62
+ extent: Extent = Field(
63
+ default=Extent(
64
+ spatial=SpatialExtent(bbox=[[-180.0, -90.0, 180.0, 90.0]]), # type: ignore
65
+ temporal=TimeInterval(interval=[[None, None]]),
66
+ ),
67
+ description=(
68
+ "The temporal extent of the collection, following the STAC specification for extent definition (e.g. "
69
+ '{"spatial": {"bbox": [[-180.0, -90.0, 180.0, 90.0]]}, '
70
+ '"temporal": {"interval": [["2024-06-10T12:00:00Z", None]]}}'
71
+ "), with date/time strings in RFC 3339 format"
72
+ ),
73
+ )
74
+ keywords: Optional[list[str]] = Field(default=None)
75
+ license: Optional[str] = Field(default=None)
76
+ links: Optional[Links] = Field(default=None)
77
+ providers: Optional[list[Provider]] = Field(default=None)
78
+
79
+ # summaries
80
+ constellation: Optional[str] = Field(default=None)
81
+ instruments: Optional[list[str]] = Field(default=None)
82
+ platform: Optional[str] = Field(default=None)
83
+ processing_level: Optional[str] = Field(default=None, alias="processing:level")
84
+ sci_doi: Optional[str] = Field(default=None, alias="sci:doi")
85
+ eodag_sensor_type: Optional[str] = Field(default=None, alias="eodag:sensor_type")
86
+
87
+ # eodag-specific attribute
88
+ alias: Optional[str] = Field(
89
+ default=None,
90
+ description="An alias given by a user to use his customized id intead of the internal id of EODAG",
91
+ repr=False,
92
+ )
93
+
94
+ # Private property to store the eodag internal id value. Not part of the model schema.
95
+ _id: str = PrivateAttr()
96
+ _dag: Optional[EODataAccessGateway] = PrivateAttr(default=None)
97
+
98
+ model_config = ConfigDict(
99
+ extra="forbid", validate_by_name=True, serialize_by_alias=True
100
+ )
101
+
102
+ def model_post_init(self, context: Any) -> None:
103
+ """Post-initialization method to set internal attributes."""
104
+ self._id = self.id
105
+
106
+ @classmethod
107
+ def create_with_dag(cls, dag: EODataAccessGateway, **kwargs) -> Collection:
108
+ """Create a Collection with a EODataAccessGateway instance.
109
+
110
+ :param dag: The gateway instance to use to search products and to list queryables of the collection instance
111
+ :param kwargs: The collection attributes
112
+ """
113
+ instance = cls(**kwargs)
114
+ instance._dag = dag
115
+ return instance
116
+
117
+ @classmethod
118
+ def get_collection_mtd_from_alias(cls, value: str) -> str:
119
+ """Get collection metadata from alias
120
+
121
+ >>> Collection.get_collection_mtd_from_alias('processing:level')
122
+ 'processing_level'
123
+ """
124
+ alias_map = {
125
+ field_info.alias: name
126
+ for name, field_info in cls.model_fields.items()
127
+ if field_info.alias
128
+ }
129
+ return alias_map.get(value, value)
130
+
131
+ @model_validator(mode="after")
132
+ def set_id_from_alias(self) -> Self:
133
+ """if an alias exists, use it to update id attribute"""
134
+ if self.alias is not None:
135
+ self._id = self.id
136
+ self.id = self.alias
137
+ return self
138
+
139
+ @model_validator(mode="wrap")
140
+ @classmethod
141
+ def validate_collection(
142
+ cls, values: dict[str, Any] | Self, handler: ModelWrapValidatorHandler[Self]
143
+ ) -> Self:
144
+ """Allow to create a collection instance with bad formatted attributes (except "id").
145
+ Set incorrectly formatted attributes to None and ignore extra attributes.
146
+ Log a warning about validation errors if EODAG_VALIDATE_COLLECTIONS is set to True.
147
+ """
148
+ errors: list[ErrorDetails] = []
149
+ continue_validation: bool = True
150
+
151
+ # iterate over each step of validation where error(s) raise(s)
152
+ while continue_validation:
153
+ try:
154
+ handler(values)
155
+ except PydanticValidationError as e:
156
+ tmp_errors = e.errors()
157
+ # raise an error if the id is invalid
158
+ if any(error["loc"][0] == "id" for error in tmp_errors):
159
+ raise ValidationError.from_error(e) from e
160
+
161
+ # convert values to dict if it is a model instance
162
+ values_dict = values if isinstance(values, dict) else values.__dict__
163
+
164
+ # set incorrectly formatted attribute(s) to None and ignore its extra attribute(s)
165
+ for error in tmp_errors:
166
+ wrong_param = error["loc"][0]
167
+ if not isinstance(wrong_param, str):
168
+ continue
169
+ if (
170
+ cls.get_collection_mtd_from_alias(wrong_param)
171
+ not in cls.model_fields
172
+ ):
173
+ del values_dict[wrong_param]
174
+ else:
175
+ values_dict[wrong_param] = cls.model_fields[
176
+ cls.get_collection_mtd_from_alias(wrong_param)
177
+ ].get_default()
178
+
179
+ errors.extend(tmp_errors)
180
+ else:
181
+ continue_validation = False
182
+
183
+ # log a warning if there were validation errors and the env var is set to True
184
+ if errors and is_env_var_true("EODAG_VALIDATE_COLLECTIONS"):
185
+ # log all errors at once
186
+ error_title = f"collection {values_dict['id']}"
187
+ init_errors: list[InitErrorDetails] = [
188
+ InitErrorDetails(
189
+ type=PydanticCustomError(error["type"], error["msg"]),
190
+ loc=error["loc"],
191
+ input=error["input"],
192
+ )
193
+ for error in errors
194
+ ]
195
+ pydantic_error = PydanticValidationError.from_exception_data(
196
+ title=error_title, line_errors=init_errors
197
+ )
198
+ logger.warning(pydantic_error)
199
+
200
+ # Create a fresh instance with the cleaned values
201
+ return handler(values)
202
+
203
+ def __str__(self) -> str:
204
+ return f'{type(self).__name__}("{self.id}")'
205
+
206
+ def __repr_str__(self, join_str: str) -> str:
207
+ return join_str.join(
208
+ repr(v) if a is None else f"{a}={v!r}" for a, v in self.__repr_args__() if v
209
+ )
210
+
211
+ def _repr_html_(self, embedded: bool = False) -> str:
212
+ thead = (
213
+ f"""<thead><tr><td style='text-align: left; color: grey;'>
214
+ {type(self).__name__}("<span style='color: black'>{self.id}</span>")</td></tr></thead>
215
+ """
216
+ if not embedded
217
+ else ""
218
+ )
219
+ tr_style = "style='background-color: transparent;'" if embedded else ""
220
+ col_html_table = dict_to_html_table(
221
+ self.model_dump(exclude={"alias"}), depth=1, brackets=False
222
+ )
223
+
224
+ return (
225
+ f"<table>{thead}<tbody>"
226
+ f"<tr {tr_style}><td style='text-align: left;'>"
227
+ f"{col_html_table}</td></tr>"
228
+ "</tbody></table>"
229
+ )
230
+
231
+ def _ensure_dag(self) -> EODataAccessGateway:
232
+ if self._dag is None:
233
+ raise RuntimeError(
234
+ f"Collection '{self.id}' needs EODataAccessGateway to perform this operation. "
235
+ "Create with: Collection.create_with_dag(dag, id='...')"
236
+ )
237
+ return self._dag
238
+
239
+ def search(self, **kwargs: Any) -> SearchResult:
240
+ """Look for products of this collection matching criteria using the `dag` attribute of the instance.
241
+
242
+ :param kwargs: Some other criteria that will be used to do the search,
243
+ using parameters compatible with the provider
244
+
245
+ :returns: A collection of EO products matching the criteria.
246
+ :raises: :class:`~eodag.utils.exceptions.ValidationError`: If the `collection` argument is set in `kwargs`,
247
+ since it is already defined by the instance
248
+ """
249
+ dag = self._ensure_dag()
250
+ collection_search_arg = "collection"
251
+ if collection_search_arg in kwargs:
252
+ raise ValidationError(
253
+ f"{collection_search_arg} should not be set in kwargs since a collection instance is used",
254
+ {collection_search_arg},
255
+ )
256
+
257
+ return dag.search(collection=self.id, **kwargs)
258
+
259
+ def list_queryables(self, **kwargs: Any) -> QueryablesDict:
260
+ """Fetch the queryable properties for this collection using the `dag` attribute of the instance.
261
+
262
+ :param kwargs: additional filters for queryables
263
+
264
+ :returns: A :class:`~eodag.api.product.queryables.QuerybalesDict` containing the EODAG queryable
265
+ properties, associating parameters to their annotated type, and a additional_properties attribute
266
+ :raises: :class:`~eodag.utils.exceptions.ValidationError`: If the `collection` argument is set in `kwargs`,
267
+ since it is already defined by the instance
268
+ """
269
+ dag = self._ensure_dag()
270
+ collection_search_arg = "collection"
271
+ if collection_search_arg in kwargs:
272
+ raise ValidationError(
273
+ f"{collection_search_arg} should not be set in kwargs since a collection instance is used",
274
+ {collection_search_arg},
275
+ )
276
+
277
+ return dag.list_queryables(collection=self.id, **kwargs)
278
+
279
+
280
+ class CollectionsDict(UserDict[str, Collection]):
281
+ """A UserDict object which values are :class:`~eodag.api.collection.Collection` objects, keyed by provider id.
282
+
283
+ :param collections: A list of collections
284
+
285
+ :cvar data: List of collections
286
+ """
287
+
288
+ def __init__(
289
+ self,
290
+ collections: list[Collection],
291
+ ) -> None:
292
+ super().__init__()
293
+
294
+ self.data = {col._id: col for col in collections}
295
+
296
+ def __str__(self) -> str:
297
+ return "{" + ", ".join(f'"{col}": {col_f}' for col, col_f in self.items()) + "}"
298
+
299
+ def __repr__(self) -> str:
300
+ return str(self)
301
+
302
+
303
+ class CollectionsList(UserList[Collection]):
304
+ """An object representing a collection of :class:`~eodag.api.collection.Collection`.
305
+
306
+ :param collections: A list of collections
307
+
308
+ :cvar data: List of collections
309
+ """
310
+
311
+ def __init__(
312
+ self,
313
+ collections: list[Collection],
314
+ ) -> None:
315
+ super().__init__(collections)
316
+
317
+ def __str__(self) -> str:
318
+ return f"{type(self).__name__}([{', '.join(str(col) for col in self)}])"
319
+
320
+ def __repr__(self) -> str:
321
+ return str(self)
322
+
323
+ def _repr_html_(self, embedded: bool = False) -> str:
324
+ # mock "thead" tag by reproduicing its style to make "details" and "summary" tags work properly
325
+ mock_thead = (
326
+ f"""<details class='foldable'>
327
+ <summary style='text-align: left; color: grey; font-size: 12px;'>
328
+ {type(self).__name__}&ensp;({len(self)})
329
+ </summary>
330
+ """
331
+ if not embedded
332
+ else ""
333
+ )
334
+ tr_style = "style='background-color: transparent;'" if embedded else ""
335
+
336
+ return (
337
+ f"{mock_thead}<table><tbody>"
338
+ + "".join(
339
+ [
340
+ f"""<tr {tr_style}><td style='text-align: left;'>
341
+ <details>
342
+ <summary style='color: grey; font-family: monospace;'>
343
+ {i}&ensp;
344
+ {type(col).__name__}("<span style='color: black'>{col.id}</span>")
345
+ </summary>
346
+ {re.sub(r"(<thead>.*|.*</thead>)", "", col._repr_html_())}
347
+ </details>
348
+ </td></tr>
349
+ """
350
+ for i, col in enumerate(self)
351
+ ]
352
+ )
353
+ + "</tbody></table></details>"
354
+ )