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.
- eodag/__init__.py +6 -1
- eodag/api/collection.py +354 -0
- eodag/api/core.py +324 -303
- eodag/api/product/_product.py +15 -29
- eodag/api/product/drivers/__init__.py +2 -42
- eodag/api/product/drivers/base.py +0 -11
- eodag/api/product/metadata_mapping.py +34 -5
- eodag/api/search_result.py +144 -9
- eodag/cli.py +18 -15
- eodag/config.py +37 -3
- eodag/plugins/apis/ecmwf.py +16 -4
- eodag/plugins/apis/usgs.py +18 -7
- eodag/plugins/crunch/filter_latest_intersect.py +1 -0
- eodag/plugins/crunch/filter_overlap.py +3 -7
- eodag/plugins/search/__init__.py +3 -0
- eodag/plugins/search/base.py +6 -6
- eodag/plugins/search/build_search_result.py +157 -56
- eodag/plugins/search/cop_marine.py +48 -8
- eodag/plugins/search/csw.py +18 -8
- eodag/plugins/search/qssearch.py +331 -88
- eodag/plugins/search/static_stac_search.py +11 -12
- eodag/resources/collections.yml +610 -348
- eodag/resources/ext_collections.json +1 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/providers.yml +334 -62
- eodag/resources/stac_provider.yml +4 -2
- eodag/resources/user_conf_template.yml +9 -0
- eodag/types/__init__.py +2 -0
- eodag/types/queryables.py +16 -0
- eodag/utils/__init__.py +47 -2
- eodag/utils/repr.py +2 -0
- {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/METADATA +4 -2
- {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/RECORD +37 -36
- {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/WHEEL +0 -0
- {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/entry_points.txt +0 -0
- {eodag-4.0.0a1.dist-info → eodag-4.0.0a3.dist-info}/licenses/LICENSE +0 -0
- {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__ = [
|
|
38
|
+
__all__ = [
|
|
39
|
+
"EODataAccessGateway",
|
|
40
|
+
"EOProduct",
|
|
41
|
+
"SearchResult",
|
|
42
|
+
"setup_logging",
|
|
43
|
+
]
|
eodag/api/collection.py
ADDED
|
@@ -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__} ({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} 
|
|
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
|
+
)
|