eodag 3.8.1__py3-none-any.whl → 3.9.1__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 +1 -1
- eodag/api/product/drivers/generic.py +5 -1
- eodag/api/product/metadata_mapping.py +132 -35
- eodag/cli.py +36 -4
- eodag/config.py +5 -2
- eodag/plugins/apis/ecmwf.py +3 -1
- eodag/plugins/apis/usgs.py +2 -1
- eodag/plugins/authentication/aws_auth.py +235 -37
- eodag/plugins/authentication/base.py +12 -2
- eodag/plugins/authentication/oauth.py +5 -0
- eodag/plugins/base.py +3 -2
- eodag/plugins/download/aws.py +44 -285
- eodag/plugins/download/base.py +3 -2
- eodag/plugins/download/creodias_s3.py +1 -38
- eodag/plugins/download/http.py +111 -103
- eodag/plugins/download/s3rest.py +3 -1
- eodag/plugins/manager.py +2 -1
- eodag/plugins/search/__init__.py +2 -1
- eodag/plugins/search/base.py +2 -1
- eodag/plugins/search/build_search_result.py +2 -2
- eodag/plugins/search/creodias_s3.py +9 -1
- eodag/plugins/search/qssearch.py +3 -1
- eodag/resources/ext_product_types.json +1 -1
- eodag/resources/product_types.yml +220 -30
- eodag/resources/providers.yml +633 -88
- eodag/resources/stac_provider.yml +5 -2
- eodag/resources/user_conf_template.yml +0 -5
- eodag/rest/core.py +8 -0
- eodag/rest/errors.py +9 -0
- eodag/rest/server.py +8 -0
- eodag/rest/stac.py +8 -0
- eodag/rest/utils/__init__.py +2 -4
- eodag/rest/utils/rfc3339.py +1 -1
- eodag/utils/__init__.py +69 -54
- eodag/utils/dates.py +204 -0
- eodag/utils/s3.py +187 -168
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/METADATA +4 -3
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/RECORD +42 -42
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/entry_points.txt +1 -1
- eodag/utils/rest.py +0 -100
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/WHEEL +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/licenses/LICENSE +0 -0
- {eodag-3.8.1.dist-info → eodag-3.9.1.dist-info}/top_level.txt +0 -0
|
@@ -33,6 +33,9 @@ search:
|
|
|
33
33
|
auto_discovery: true
|
|
34
34
|
metadata_pattern: '^[a-zA-Z0-9_:-]+$'
|
|
35
35
|
search_param: '{{{{"query":{{{{"{metadata}":{{{{"eq":"{{{metadata}}}" }}}} }}}} }}}}'
|
|
36
|
+
search_param_unparsed:
|
|
37
|
+
- filter
|
|
38
|
+
- query
|
|
36
39
|
metadata_path: '$.properties.*'
|
|
37
40
|
discover_product_types:
|
|
38
41
|
fetch_url: '{api_endpoint}/../collections'
|
|
@@ -99,7 +102,7 @@ search:
|
|
|
99
102
|
- '$.properties."sat:relative_orbit"'
|
|
100
103
|
orbitDirection:
|
|
101
104
|
- '{{"query":{{"sat:orbit_state":{{"eq":"{orbitDirection}"}}}}}}'
|
|
102
|
-
- '$.properties."sat:orbit_state"'
|
|
105
|
+
- '{$.properties."sat:orbit_state"#to_lower}'
|
|
103
106
|
cloudCover:
|
|
104
107
|
- '{{"query":{{"eo:cloud_cover":{{"lte":"{cloudCover}"}}}}}}'
|
|
105
108
|
- '$.properties."eo:cloud_cover"'
|
|
@@ -137,7 +140,7 @@ search:
|
|
|
137
140
|
- '$.properties."view:sun_elevation"'
|
|
138
141
|
polarizationChannels:
|
|
139
142
|
- '{{"query":{{"sar:polarizations":{{"eq":"{polarizationChannels}"}}}}}}'
|
|
140
|
-
- '$.properties.
|
|
143
|
+
- '{$.properties.sar:polarizations#csv_list(+)}'
|
|
141
144
|
dopplerFrequency:
|
|
142
145
|
- '{{"query":{{"sar:frequency_band":{{"eq":"{dopplerFrequency}"}}}}}}'
|
|
143
146
|
- '$.properties."sar:frequency_band"'
|
|
@@ -117,11 +117,6 @@ earth_search:
|
|
|
117
117
|
aws_profile:
|
|
118
118
|
download:
|
|
119
119
|
output_dir:
|
|
120
|
-
earth_search_cog:
|
|
121
|
-
priority: # Lower value means lower priority (Default: 0)
|
|
122
|
-
search: # Search parameters configuration
|
|
123
|
-
download:
|
|
124
|
-
output_dir:
|
|
125
120
|
earth_search_gcs:
|
|
126
121
|
priority: # Lower value means lower priority (Default: 0)
|
|
127
122
|
search: # Search parameters configuration
|
eodag/rest/core.py
CHANGED
|
@@ -21,6 +21,7 @@ import datetime
|
|
|
21
21
|
import logging
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
|
+
import warnings
|
|
24
25
|
from typing import TYPE_CHECKING, cast
|
|
25
26
|
from unittest.mock import Mock
|
|
26
27
|
from urllib.parse import urlencode
|
|
@@ -83,6 +84,13 @@ if TYPE_CHECKING:
|
|
|
83
84
|
from starlette.responses import Response
|
|
84
85
|
|
|
85
86
|
|
|
87
|
+
warnings.warn(
|
|
88
|
+
"The module `eodag.rest.core` is deprecated since v3.9.0 and will be removed in a future version. "
|
|
89
|
+
"The STAC server has moved to https://github.com/CS-SI/stac-fastapi-eodag",
|
|
90
|
+
category=DeprecationWarning,
|
|
91
|
+
stacklevel=2,
|
|
92
|
+
)
|
|
93
|
+
|
|
86
94
|
eodag_api = eodag.EODataAccessGateway()
|
|
87
95
|
|
|
88
96
|
logger = logging.getLogger("eodag.rest.core")
|
eodag/rest/errors.py
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
import logging
|
|
19
19
|
import re
|
|
20
|
+
import warnings
|
|
20
21
|
from typing import Union
|
|
21
22
|
|
|
22
23
|
from fastapi import FastAPI, Request
|
|
@@ -53,6 +54,14 @@ EODAG_DEFAULT_STATUS_CODES = {
|
|
|
53
54
|
ValidationError: status.HTTP_400_BAD_REQUEST,
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
warnings.warn(
|
|
58
|
+
"The module `eodag.rest.errors` is deprecated since v3.9.0 and will be removed in a future version. "
|
|
59
|
+
"The STAC server has moved to https://github.com/CS-SI/stac-fastapi-eodag",
|
|
60
|
+
category=DeprecationWarning,
|
|
61
|
+
stacklevel=2,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
56
65
|
logger = logging.getLogger("eodag.rest.server")
|
|
57
66
|
|
|
58
67
|
|
eodag/rest/server.py
CHANGED
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
22
|
import re
|
|
23
|
+
import warnings
|
|
23
24
|
from contextlib import asynccontextmanager
|
|
24
25
|
from importlib.metadata import version
|
|
25
26
|
from json import JSONDecodeError
|
|
@@ -67,6 +68,13 @@ if TYPE_CHECKING:
|
|
|
67
68
|
|
|
68
69
|
from starlette.responses import Response as StarletteResponse
|
|
69
70
|
|
|
71
|
+
warnings.warn(
|
|
72
|
+
"The module `eodag.rest.server` is deprecated since v3.9.0 and will be removed in a future version. "
|
|
73
|
+
"The STAC server has moved to https://github.com/CS-SI/stac-fastapi-eodag",
|
|
74
|
+
category=DeprecationWarning,
|
|
75
|
+
stacklevel=2,
|
|
76
|
+
)
|
|
77
|
+
|
|
70
78
|
logger = logging.getLogger("eodag.rest.server")
|
|
71
79
|
|
|
72
80
|
|
eodag/rest/stac.py
CHANGED
|
@@ -19,6 +19,7 @@ from __future__ import annotations
|
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
21
|
import os
|
|
22
|
+
import warnings
|
|
22
23
|
from collections import defaultdict
|
|
23
24
|
from datetime import datetime, timezone
|
|
24
25
|
from typing import TYPE_CHECKING, Any, Optional
|
|
@@ -68,6 +69,13 @@ if TYPE_CHECKING:
|
|
|
68
69
|
from eodag.api.search_result import SearchResult
|
|
69
70
|
|
|
70
71
|
|
|
72
|
+
warnings.warn(
|
|
73
|
+
"The module `eodag.rest.stac` is deprecated since v3.9.0 and will be removed in a future version. "
|
|
74
|
+
"The STAC server has moved to https://github.com/CS-SI/stac-fastapi-eodag",
|
|
75
|
+
category=DeprecationWarning,
|
|
76
|
+
stacklevel=2,
|
|
77
|
+
)
|
|
78
|
+
|
|
71
79
|
logger = logging.getLogger("eodag.rest.stac")
|
|
72
80
|
|
|
73
81
|
# fields not to put in item properties
|
eodag/rest/utils/__init__.py
CHANGED
|
@@ -34,8 +34,8 @@ from eodag.plugins.crunch.filter_latest_intersect import FilterLatestIntersect
|
|
|
34
34
|
from eodag.plugins.crunch.filter_latest_tpl_name import FilterLatestByName
|
|
35
35
|
from eodag.plugins.crunch.filter_overlap import FilterOverlap
|
|
36
36
|
from eodag.utils import StreamResponse
|
|
37
|
+
from eodag.utils.dates import get_date, get_datetime
|
|
37
38
|
from eodag.utils.exceptions import ValidationError
|
|
38
|
-
from eodag.utils.rest import get_date, get_datetime
|
|
39
39
|
|
|
40
40
|
if TYPE_CHECKING:
|
|
41
41
|
from eodag.rest.types.stac_search import SearchPostRequest
|
|
@@ -203,7 +203,5 @@ def file_to_stream(
|
|
|
203
203
|
filename = os.path.basename(filepath_to_stream)
|
|
204
204
|
return StreamResponse(
|
|
205
205
|
content=read_file_chunks_and_delete(open(filepath_to_stream, "rb")),
|
|
206
|
-
|
|
207
|
-
"content-disposition": f"attachment; filename={filename}",
|
|
208
|
-
},
|
|
206
|
+
filename=filename,
|
|
209
207
|
)
|
eodag/rest/utils/rfc3339.py
CHANGED
eodag/utils/__init__.py
CHANGED
|
@@ -24,7 +24,6 @@ this package should go here
|
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
26
|
import ast
|
|
27
|
-
import datetime
|
|
28
27
|
import errno
|
|
29
28
|
import functools
|
|
30
29
|
import hashlib
|
|
@@ -43,8 +42,7 @@ import unicodedata
|
|
|
43
42
|
import warnings
|
|
44
43
|
from collections import defaultdict
|
|
45
44
|
from copy import deepcopy as copy_deepcopy
|
|
46
|
-
from dataclasses import dataclass
|
|
47
|
-
from datetime import datetime as dt
|
|
45
|
+
from dataclasses import dataclass, field
|
|
48
46
|
from email.message import Message
|
|
49
47
|
from glob import glob
|
|
50
48
|
from importlib.metadata import metadata
|
|
@@ -75,8 +73,6 @@ import orjson
|
|
|
75
73
|
import shapefile
|
|
76
74
|
import shapely.wkt
|
|
77
75
|
import yaml
|
|
78
|
-
from dateutil.parser import isoparse
|
|
79
|
-
from dateutil.tz import UTC
|
|
80
76
|
from jsonpath_ng import jsonpath
|
|
81
77
|
from jsonpath_ng.ext import parse
|
|
82
78
|
from jsonpath_ng.jsonpath import Child, Fields, Index, Root, Slice
|
|
@@ -414,54 +410,6 @@ def maybe_generator(obj: Any) -> Iterator[Any]:
|
|
|
414
410
|
yield obj
|
|
415
411
|
|
|
416
412
|
|
|
417
|
-
def get_timestamp(date_time: str) -> float:
|
|
418
|
-
"""Return the Unix timestamp of an ISO8601 date/datetime in seconds.
|
|
419
|
-
|
|
420
|
-
If the datetime has no offset, it is assumed to be an UTC datetime.
|
|
421
|
-
|
|
422
|
-
:param date_time: The datetime string to return as timestamp
|
|
423
|
-
:returns: The timestamp corresponding to the ``date_time`` string in seconds
|
|
424
|
-
"""
|
|
425
|
-
dt = isoparse(date_time)
|
|
426
|
-
if not dt.tzinfo:
|
|
427
|
-
dt = dt.replace(tzinfo=UTC)
|
|
428
|
-
return dt.timestamp()
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def datetime_range(start: dt, end: dt) -> Iterator[dt]:
|
|
432
|
-
"""Generator function for all dates in-between ``start`` and ``end`` date."""
|
|
433
|
-
delta = end - start
|
|
434
|
-
for nday in range(delta.days + 1):
|
|
435
|
-
yield start + datetime.timedelta(days=nday)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def is_range_in_range(valid_range: str, check_range: str) -> bool:
|
|
439
|
-
"""Check if the check_range is completely within the valid_range.
|
|
440
|
-
|
|
441
|
-
This function checks if both the start and end dates of the check_range
|
|
442
|
-
are within the start and end dates of the valid_range.
|
|
443
|
-
|
|
444
|
-
:param valid_range: The valid date range in the format 'YYYY-MM-DD/YYYY-MM-DD'.
|
|
445
|
-
:param check_range: The date range to check in the format 'YYYY-MM-DD/YYYY-MM-DD'.
|
|
446
|
-
:returns: True if check_range is within valid_range, otherwise False.
|
|
447
|
-
"""
|
|
448
|
-
if "/" not in valid_range or "/" not in check_range:
|
|
449
|
-
return False
|
|
450
|
-
|
|
451
|
-
# Split the date ranges into start and end dates
|
|
452
|
-
start_valid, end_valid = valid_range.split("/")
|
|
453
|
-
start_check, end_check = check_range.split("/")
|
|
454
|
-
|
|
455
|
-
# Convert the strings to datetime objects using fromisoformat
|
|
456
|
-
start_valid_dt = datetime.datetime.fromisoformat(start_valid)
|
|
457
|
-
end_valid_dt = datetime.datetime.fromisoformat(end_valid)
|
|
458
|
-
start_check_dt = datetime.datetime.fromisoformat(start_check)
|
|
459
|
-
end_check_dt = datetime.datetime.fromisoformat(end_check)
|
|
460
|
-
|
|
461
|
-
# Check if check_range is within valid_range
|
|
462
|
-
return start_valid_dt <= start_check_dt and end_valid_dt >= end_check_dt
|
|
463
|
-
|
|
464
|
-
|
|
465
413
|
class DownloadedCallback:
|
|
466
414
|
"""Example class for callback after each download in :meth:`~eodag.api.core.EODataAccessGateway.download_all`"""
|
|
467
415
|
|
|
@@ -1457,9 +1405,76 @@ class StreamResponse:
|
|
|
1457
1405
|
"""Represents a streaming response"""
|
|
1458
1406
|
|
|
1459
1407
|
content: Iterable[bytes]
|
|
1460
|
-
|
|
1408
|
+
_filename: Optional[str] = field(default=None, repr=False, init=False)
|
|
1409
|
+
_size: Optional[int] = field(default=None, repr=False, init=False)
|
|
1410
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
1461
1411
|
media_type: Optional[str] = None
|
|
1462
1412
|
status_code: Optional[int] = None
|
|
1413
|
+
arcname: Optional[str] = None
|
|
1414
|
+
|
|
1415
|
+
def __init__(
|
|
1416
|
+
self,
|
|
1417
|
+
content: Iterable[bytes],
|
|
1418
|
+
filename: Optional[str] = None,
|
|
1419
|
+
size: Optional[int] = None,
|
|
1420
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
1421
|
+
media_type: Optional[str] = None,
|
|
1422
|
+
status_code: Optional[int] = None,
|
|
1423
|
+
arcname: Optional[str] = None,
|
|
1424
|
+
):
|
|
1425
|
+
self.content = content
|
|
1426
|
+
self.headers = dict(headers) if headers else {}
|
|
1427
|
+
self.media_type = media_type
|
|
1428
|
+
self.status_code = status_code
|
|
1429
|
+
self.arcname = arcname
|
|
1430
|
+
# use property setters to update headers
|
|
1431
|
+
self.filename = filename
|
|
1432
|
+
self.size = size
|
|
1433
|
+
|
|
1434
|
+
# filename handling
|
|
1435
|
+
@property
|
|
1436
|
+
def filename(self) -> Optional[str]:
|
|
1437
|
+
"""Get the filename for the streaming response.
|
|
1438
|
+
|
|
1439
|
+
:returns: The filename, or None if not set
|
|
1440
|
+
"""
|
|
1441
|
+
return self._filename
|
|
1442
|
+
|
|
1443
|
+
@filename.setter
|
|
1444
|
+
def filename(self, value: Optional[str]) -> None:
|
|
1445
|
+
"""Set the filename and update the content-disposition header accordingly.
|
|
1446
|
+
|
|
1447
|
+
:param value: The filename to set, or None to clear it
|
|
1448
|
+
"""
|
|
1449
|
+
self._filename = value
|
|
1450
|
+
if value:
|
|
1451
|
+
outputs_filename = os.path.basename(value)
|
|
1452
|
+
self.headers[
|
|
1453
|
+
"content-disposition"
|
|
1454
|
+
] = f'attachment; filename="{outputs_filename}"'
|
|
1455
|
+
elif "content-disposition" in self.headers:
|
|
1456
|
+
del self.headers["content-disposition"]
|
|
1457
|
+
|
|
1458
|
+
# size handling
|
|
1459
|
+
@property
|
|
1460
|
+
def size(self) -> Optional[int]:
|
|
1461
|
+
"""Get the content size for the streaming response.
|
|
1462
|
+
|
|
1463
|
+
:returns: The content size in bytes, or None if not set
|
|
1464
|
+
"""
|
|
1465
|
+
return self._size
|
|
1466
|
+
|
|
1467
|
+
@size.setter
|
|
1468
|
+
def size(self, value: Optional[int]) -> None:
|
|
1469
|
+
"""Set the content size and update the content-length header accordingly.
|
|
1470
|
+
|
|
1471
|
+
:param value: The content size in bytes, or None to clear it
|
|
1472
|
+
"""
|
|
1473
|
+
self._size = value
|
|
1474
|
+
if value is not None:
|
|
1475
|
+
self.headers["content-length"] = str(value)
|
|
1476
|
+
elif "content-length" in self.headers:
|
|
1477
|
+
del self.headers["content-length"]
|
|
1463
1478
|
|
|
1464
1479
|
|
|
1465
1480
|
def guess_file_type(file: str) -> Optional[str]:
|
eodag/utils/dates.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
"""eodag.rest.dates methods that must be importable without eodag[server] installeds"""
|
|
19
|
+
|
|
20
|
+
import datetime
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime as dt
|
|
23
|
+
from typing import Any, Iterator, Optional
|
|
24
|
+
|
|
25
|
+
import dateutil.parser
|
|
26
|
+
from dateutil import tz
|
|
27
|
+
from dateutil.parser import isoparse
|
|
28
|
+
from dateutil.tz import UTC
|
|
29
|
+
|
|
30
|
+
from eodag.utils.exceptions import ValidationError
|
|
31
|
+
|
|
32
|
+
RFC3339_PATTERN = (
|
|
33
|
+
r"^(\d{4})-(\d{2})-(\d{2})"
|
|
34
|
+
r"(?:T(\d{2}):(\d{2}):(\d{2})(\.\d+)?"
|
|
35
|
+
r"(Z|([+-])(\d{2}):(\d{2}))?)?$"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_timestamp(date_time: str) -> float:
|
|
40
|
+
"""Return the Unix timestamp of an ISO8601 date/datetime in seconds.
|
|
41
|
+
|
|
42
|
+
If the datetime has no offset, it is assumed to be an UTC datetime.
|
|
43
|
+
|
|
44
|
+
:param date_time: The datetime string to return as timestamp
|
|
45
|
+
:returns: The timestamp corresponding to the ``date_time`` string in seconds
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
>>> get_timestamp("2023-09-23T12:34:56Z") # doctest: +ELLIPSIS
|
|
49
|
+
1695472496.0
|
|
50
|
+
>>> get_timestamp("2023-09-23T12:34:56+02:00") # doctest: +ELLIPSIS
|
|
51
|
+
1695465296.0
|
|
52
|
+
>>> get_timestamp("2023-09-23") # doctest: +ELLIPSIS
|
|
53
|
+
1695427200.0
|
|
54
|
+
"""
|
|
55
|
+
dt = isoparse(date_time)
|
|
56
|
+
if not dt.tzinfo:
|
|
57
|
+
dt = dt.replace(tzinfo=UTC)
|
|
58
|
+
return dt.timestamp()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def datetime_range(start: dt, end: dt) -> Iterator[dt]:
|
|
62
|
+
"""Generator function for all dates in-between ``start`` and ``end`` date."""
|
|
63
|
+
delta = end - start
|
|
64
|
+
for nday in range(delta.days + 1):
|
|
65
|
+
yield start + datetime.timedelta(days=nday)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_range_in_range(valid_range: str, check_range: str) -> bool:
|
|
69
|
+
"""Check if the check_range is completely within the valid_range.
|
|
70
|
+
|
|
71
|
+
This function checks if both the start and end dates of the check_range
|
|
72
|
+
are within the start and end dates of the valid_range.
|
|
73
|
+
|
|
74
|
+
:param valid_range: The valid date range in the format 'YYYY-MM-DD/YYYY-MM-DD'.
|
|
75
|
+
:param check_range: The date range to check in the format 'YYYY-MM-DD/YYYY-MM-DD'.
|
|
76
|
+
:returns: True if check_range is within valid_range, otherwise False.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
>>> is_range_in_range("2023-01-01/2023-12-31", "2023-03-01/2023-03-31")
|
|
80
|
+
True
|
|
81
|
+
>>> is_range_in_range("2023-01-01/2023-12-31", "2022-12-01/2023-03-31")
|
|
82
|
+
False
|
|
83
|
+
>>> is_range_in_range("2023-01-01/2023-12-31", "2023-11-01/2024-01-01")
|
|
84
|
+
False
|
|
85
|
+
>>> is_range_in_range("2023-01-01/2023-12-31", "invalid-range")
|
|
86
|
+
False
|
|
87
|
+
>>> is_range_in_range("invalid-range", "2023-03-01/2023-03-31")
|
|
88
|
+
False
|
|
89
|
+
"""
|
|
90
|
+
if "/" not in valid_range or "/" not in check_range:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
# Split the date ranges into start and end dates
|
|
94
|
+
start_valid, end_valid = valid_range.split("/")
|
|
95
|
+
start_check, end_check = check_range.split("/")
|
|
96
|
+
|
|
97
|
+
# Convert the strings to datetime objects using fromisoformat
|
|
98
|
+
start_valid_dt = datetime.datetime.fromisoformat(start_valid)
|
|
99
|
+
end_valid_dt = datetime.datetime.fromisoformat(end_valid)
|
|
100
|
+
start_check_dt = datetime.datetime.fromisoformat(start_check)
|
|
101
|
+
end_check_dt = datetime.datetime.fromisoformat(end_check)
|
|
102
|
+
|
|
103
|
+
# Check if check_range is within valid_range
|
|
104
|
+
return start_valid_dt <= start_check_dt and end_valid_dt >= end_check_dt
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_datetime(arguments: dict[str, Any]) -> tuple[Optional[str], Optional[str]]:
|
|
108
|
+
"""Get start and end dates from a dict containing `/` separated dates in `datetime` item
|
|
109
|
+
|
|
110
|
+
:param arguments: dict containing a single date or `/` separated dates in `datetime` item
|
|
111
|
+
:returns: Start date and end date from datetime string (duplicate value if only one date as input)
|
|
112
|
+
|
|
113
|
+
Examples:
|
|
114
|
+
>>> get_datetime({"datetime": "2023-03-01/2023-03-31"})
|
|
115
|
+
('2023-03-01T00:00:00', '2023-03-31T00:00:00')
|
|
116
|
+
>>> get_datetime({"datetime": "2023-03-01"})
|
|
117
|
+
('2023-03-01T00:00:00', '2023-03-01T00:00:00')
|
|
118
|
+
>>> get_datetime({"datetime": "../2023-03-31"})
|
|
119
|
+
(None, '2023-03-31T00:00:00')
|
|
120
|
+
>>> get_datetime({"datetime": "2023-03-01/.."})
|
|
121
|
+
('2023-03-01T00:00:00', None)
|
|
122
|
+
>>> get_datetime({"dtstart": "2023-03-01", "dtend": "2023-03-31"})
|
|
123
|
+
('2023-03-01T00:00:00', '2023-03-31T00:00:00')
|
|
124
|
+
>>> get_datetime({})
|
|
125
|
+
(None, None)
|
|
126
|
+
"""
|
|
127
|
+
datetime_str = arguments.pop("datetime", None)
|
|
128
|
+
|
|
129
|
+
if datetime_str:
|
|
130
|
+
datetime_split = datetime_str.split("/")
|
|
131
|
+
if len(datetime_split) > 1:
|
|
132
|
+
dtstart = datetime_split[0] if datetime_split[0] != ".." else None
|
|
133
|
+
dtend = datetime_split[1] if datetime_split[1] != ".." else None
|
|
134
|
+
elif len(datetime_split) == 1:
|
|
135
|
+
# same time for start & end if only one is given
|
|
136
|
+
dtstart, dtend = datetime_split[0:1] * 2
|
|
137
|
+
else:
|
|
138
|
+
return None, None
|
|
139
|
+
|
|
140
|
+
return get_date(dtstart), get_date(dtend)
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
# return already set (dtstart, dtend) or None
|
|
144
|
+
dtstart = get_date(arguments.pop("dtstart", None))
|
|
145
|
+
dtend = get_date(arguments.pop("dtend", None))
|
|
146
|
+
return get_date(dtstart), get_date(dtend)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_date(date: Optional[str]) -> Optional[str]:
|
|
150
|
+
"""
|
|
151
|
+
Check if the input date can be parsed as a date
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
>>> from eodag.utils.exceptions import ValidationError
|
|
155
|
+
>>> get_date("2023-09-23")
|
|
156
|
+
'2023-09-23T00:00:00'
|
|
157
|
+
>>> get_date(None) is None
|
|
158
|
+
True
|
|
159
|
+
>>> get_date("invalid-date") # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
160
|
+
Traceback (most recent call last):
|
|
161
|
+
...
|
|
162
|
+
ValidationError
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
if not date:
|
|
166
|
+
return None
|
|
167
|
+
try:
|
|
168
|
+
return (
|
|
169
|
+
dateutil.parser.parse(date)
|
|
170
|
+
.replace(tzinfo=tz.UTC)
|
|
171
|
+
.isoformat()
|
|
172
|
+
.replace("+00:00", "")
|
|
173
|
+
)
|
|
174
|
+
except ValueError as e:
|
|
175
|
+
exc = ValidationError("invalid input date: %s" % e)
|
|
176
|
+
raise exc
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def rfc3339_str_to_datetime(s: str) -> datetime.datetime:
|
|
180
|
+
"""Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
|
|
181
|
+
|
|
182
|
+
:param s: The string to convert to :class:`datetime.datetime`
|
|
183
|
+
:returns: The datetime represented by the ISO8601 (RFC 3339) formatted string
|
|
184
|
+
:raises: :class:`ValidationError`
|
|
185
|
+
|
|
186
|
+
Examples:
|
|
187
|
+
>>> from eodag.utils.exceptions import ValidationError
|
|
188
|
+
>>> rfc3339_str_to_datetime("2023-09-23T12:34:56Z")
|
|
189
|
+
datetime.datetime(2023, 9, 23, 12, 34, 56, tzinfo=datetime.timezone.utc)
|
|
190
|
+
|
|
191
|
+
>>> rfc3339_str_to_datetime("invalid-date") # doctest: +IGNORE_EXCEPTION_DETAIL
|
|
192
|
+
Traceback (most recent call last):
|
|
193
|
+
...
|
|
194
|
+
ValidationError
|
|
195
|
+
"""
|
|
196
|
+
# Uppercase the string
|
|
197
|
+
s = s.upper()
|
|
198
|
+
|
|
199
|
+
# Match against RFC3339 regex.
|
|
200
|
+
result = re.match(RFC3339_PATTERN, s)
|
|
201
|
+
if not result:
|
|
202
|
+
raise ValidationError("Invalid RFC3339 datetime.")
|
|
203
|
+
|
|
204
|
+
return dateutil.parser.isoparse(s).replace(tzinfo=datetime.timezone.utc)
|