eodag 2.12.0__py3-none-any.whl → 3.0.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.
Files changed (93) hide show
  1. eodag/__init__.py +6 -8
  2. eodag/api/core.py +654 -538
  3. eodag/api/product/__init__.py +12 -2
  4. eodag/api/product/_assets.py +59 -16
  5. eodag/api/product/_product.py +100 -93
  6. eodag/api/product/drivers/__init__.py +7 -2
  7. eodag/api/product/drivers/base.py +0 -3
  8. eodag/api/product/metadata_mapping.py +192 -96
  9. eodag/api/search_result.py +69 -10
  10. eodag/cli.py +55 -25
  11. eodag/config.py +391 -116
  12. eodag/plugins/apis/base.py +11 -165
  13. eodag/plugins/apis/ecmwf.py +36 -25
  14. eodag/plugins/apis/usgs.py +80 -35
  15. eodag/plugins/authentication/aws_auth.py +13 -4
  16. eodag/plugins/authentication/base.py +10 -1
  17. eodag/plugins/authentication/generic.py +2 -2
  18. eodag/plugins/authentication/header.py +31 -6
  19. eodag/plugins/authentication/keycloak.py +17 -84
  20. eodag/plugins/authentication/oauth.py +3 -3
  21. eodag/plugins/authentication/openid_connect.py +268 -49
  22. eodag/plugins/authentication/qsauth.py +4 -1
  23. eodag/plugins/authentication/sas_auth.py +9 -2
  24. eodag/plugins/authentication/token.py +98 -47
  25. eodag/plugins/authentication/token_exchange.py +122 -0
  26. eodag/plugins/crunch/base.py +3 -1
  27. eodag/plugins/crunch/filter_date.py +3 -9
  28. eodag/plugins/crunch/filter_latest_intersect.py +0 -3
  29. eodag/plugins/crunch/filter_latest_tpl_name.py +1 -4
  30. eodag/plugins/crunch/filter_overlap.py +4 -8
  31. eodag/plugins/crunch/filter_property.py +5 -11
  32. eodag/plugins/download/aws.py +149 -185
  33. eodag/plugins/download/base.py +88 -97
  34. eodag/plugins/download/creodias_s3.py +1 -1
  35. eodag/plugins/download/http.py +638 -310
  36. eodag/plugins/download/s3rest.py +47 -45
  37. eodag/plugins/manager.py +228 -88
  38. eodag/plugins/search/__init__.py +36 -0
  39. eodag/plugins/search/base.py +239 -30
  40. eodag/plugins/search/build_search_result.py +382 -37
  41. eodag/plugins/search/cop_marine.py +441 -0
  42. eodag/plugins/search/creodias_s3.py +25 -20
  43. eodag/plugins/search/csw.py +5 -7
  44. eodag/plugins/search/data_request_search.py +61 -30
  45. eodag/plugins/search/qssearch.py +713 -255
  46. eodag/plugins/search/static_stac_search.py +106 -40
  47. eodag/resources/ext_product_types.json +1 -1
  48. eodag/resources/product_types.yml +1921 -34
  49. eodag/resources/providers.yml +4091 -3655
  50. eodag/resources/stac.yml +50 -216
  51. eodag/resources/stac_api.yml +71 -25
  52. eodag/resources/stac_provider.yml +5 -0
  53. eodag/resources/user_conf_template.yml +89 -32
  54. eodag/rest/__init__.py +6 -0
  55. eodag/rest/cache.py +70 -0
  56. eodag/rest/config.py +68 -0
  57. eodag/rest/constants.py +26 -0
  58. eodag/rest/core.py +735 -0
  59. eodag/rest/errors.py +178 -0
  60. eodag/rest/server.py +264 -431
  61. eodag/rest/stac.py +442 -836
  62. eodag/rest/types/collections_search.py +44 -0
  63. eodag/rest/types/eodag_search.py +238 -47
  64. eodag/rest/types/queryables.py +164 -0
  65. eodag/rest/types/stac_search.py +273 -0
  66. eodag/rest/utils/__init__.py +216 -0
  67. eodag/rest/utils/cql_evaluate.py +119 -0
  68. eodag/rest/utils/rfc3339.py +64 -0
  69. eodag/types/__init__.py +106 -10
  70. eodag/types/bbox.py +15 -14
  71. eodag/types/download_args.py +40 -0
  72. eodag/types/search_args.py +57 -7
  73. eodag/types/whoosh.py +79 -0
  74. eodag/utils/__init__.py +110 -91
  75. eodag/utils/constraints.py +37 -45
  76. eodag/utils/exceptions.py +39 -22
  77. eodag/utils/import_system.py +0 -4
  78. eodag/utils/logging.py +37 -80
  79. eodag/utils/notebook.py +4 -4
  80. eodag/utils/repr.py +113 -0
  81. eodag/utils/requests.py +128 -0
  82. eodag/utils/rest.py +100 -0
  83. eodag/utils/stac_reader.py +93 -21
  84. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/METADATA +88 -53
  85. eodag-3.0.0.dist-info/RECORD +109 -0
  86. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/WHEEL +1 -1
  87. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/entry_points.txt +7 -5
  88. eodag/plugins/apis/cds.py +0 -540
  89. eodag/rest/types/stac_queryables.py +0 -134
  90. eodag/rest/utils.py +0 -1133
  91. eodag-2.12.0.dist-info/RECORD +0 -94
  92. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/LICENSE +0 -0
  93. {eodag-2.12.0.dist-info → eodag-3.0.0.dist-info}/top_level.txt +0 -0
eodag/utils/exceptions.py CHANGED
@@ -20,80 +20,97 @@ from __future__ import annotations
20
20
  from typing import TYPE_CHECKING
21
21
 
22
22
  if TYPE_CHECKING:
23
- from typing import Optional, Set, Tuple
23
+ from typing import Optional, Set
24
24
 
25
+ from typing_extensions import Annotated, Doc
25
26
 
26
- class ValidationError(Exception):
27
+
28
+ class EodagError(Exception):
29
+ """General EODAG error"""
30
+
31
+
32
+ class ValidationError(EodagError):
27
33
  """Error validating data"""
28
34
 
29
- def __init__(self, message: str) -> None:
35
+ def __init__(self, message: str, parameters: Set[str] = set()) -> None:
30
36
  self.message = message
37
+ self.parameters = parameters
31
38
 
32
39
 
33
- class PluginNotFoundError(Exception):
40
+ class PluginNotFoundError(EodagError):
34
41
  """Error when looking for a plugin class that was not defined"""
35
42
 
36
43
 
37
- class PluginImplementationError(Exception):
44
+ class PluginImplementationError(EodagError):
38
45
  """Error when a plugin does not behave as expected"""
39
46
 
40
47
 
41
- class MisconfiguredError(Exception):
48
+ class MisconfiguredError(EodagError):
42
49
  """An error indicating a Search Plugin that is not well configured"""
43
50
 
44
51
 
45
- class AddressNotFound(Exception):
52
+ class AddressNotFound(EodagError):
46
53
  """An error indicating the address of a subdataset was not found"""
47
54
 
48
55
 
49
- class UnsupportedProvider(Exception):
56
+ class UnsupportedProvider(EodagError):
50
57
  """An error indicating that eodag does not support a provider"""
51
58
 
52
59
 
53
- class UnsupportedProductType(Exception):
60
+ class UnsupportedProductType(EodagError):
54
61
  """An error indicating that eodag does not support a product type"""
55
62
 
56
63
  def __init__(self, product_type: str) -> None:
57
64
  self.product_type = product_type
58
65
 
59
66
 
60
- class UnsupportedDatasetAddressScheme(Exception):
67
+ class UnsupportedDatasetAddressScheme(EodagError):
61
68
  """An error indicating that eodag does not yet support an address scheme for
62
69
  accessing raster subdatasets"""
63
70
 
64
71
 
65
- class AuthenticationError(Exception):
72
+ class AuthenticationError(EodagError):
66
73
  """An error indicating that an authentication plugin did not succeeded
67
74
  authenticating a user"""
68
75
 
69
76
 
70
- class DownloadError(Exception):
77
+ class DownloadError(EodagError):
71
78
  """An error indicating something wrong with the download process"""
72
79
 
73
80
 
74
- class NotAvailableError(Exception):
81
+ class NotAvailableError(EodagError):
75
82
  """An error indicating that the product is not available for download"""
76
83
 
77
84
 
78
- class RequestError(Exception):
85
+ class RequestError(EodagError):
79
86
  """An error indicating that a request has failed. Usually eodag functions
80
87
  and methods should catch and skip this"""
81
88
 
82
- history: Set[Tuple[Exception, str]] = set()
89
+ status_code: Annotated[Optional[int], Doc("HTTP status code")] = None
90
+
91
+ @classmethod
92
+ def from_error(cls, error: Exception, msg: Optional[str] = None):
93
+ """Generate a RequestError from an Exception"""
94
+ status_code = getattr(error, "code", None)
95
+ text = getattr(error, "msg", None)
96
+
97
+ if response := getattr(error, "response", None):
98
+ status_code = response.status_code
99
+ text = response.text
100
+
101
+ text = text or str(error)
83
102
 
84
- def __str__(self):
85
- repr = super().__str__()
86
- for err_tuple in self.history:
87
- repr += f"\n- {str(err_tuple)}"
88
- return repr
103
+ e = cls(msg, text) if msg else cls(text)
104
+ e.status_code = status_code
105
+ return e
89
106
 
90
107
 
91
- class NoMatchingProductType(Exception):
108
+ class NoMatchingProductType(EodagError):
92
109
  """An error indicating that eodag was unable to derive a product type from a set
93
110
  of search parameters"""
94
111
 
95
112
 
96
- class STACOpenerError(Exception):
113
+ class STACOpenerError(EodagError):
97
114
  """An error indicating that a STAC file could not be opened"""
98
115
 
99
116
 
@@ -52,12 +52,9 @@ def import_all_modules(
52
52
  import_all_modules(base_package)
53
53
 
54
54
  :param base_package: The package from where we must import all the modules
55
- :type base_package: `module`
56
55
  :param depth: (optional) If `base_package` has sub packages, import all the modules recursively up to this level.
57
56
  Defaults to 1 (limits to the level of `base_package`)
58
- :type depth: int
59
57
  :param exclude: (optional) The sub packages and modules to ignore while importing. Empty by default
60
- :type exclude: tuple(str, ...)
61
58
 
62
59
  .. note::
63
60
  if `package` and `subpackage` have a module of the same name and this name is included in the exclude
@@ -88,7 +85,6 @@ def patch_owslib_requests(verify: bool = True) -> Generator[None, Any, None]:
88
85
  these functions in `owslib <https://geopython.github.io/OWSLib/>`_.
89
86
 
90
87
  :param verify: (optional) Whether to verify the use of https or not
91
- :type verify: bool
92
88
  """
93
89
  from owslib.util import requests
94
90
 
eodag/utils/logging.py CHANGED
@@ -32,95 +32,53 @@ def setup_logging(verbose: int, no_progress_bar: bool = False) -> None:
32
32
  * 1: no logging but still displays progress bars
33
33
  * 2: INFO level
34
34
  * 3: DEBUG level
35
- :type verbose: int
36
35
  :param no_progress_bar: (optional) Disable progress bars
37
- :type no_progress_bar: bool
38
36
  """
39
37
  global disable_tqdm
40
38
  disable_tqdm = no_progress_bar
41
39
 
40
+ if verbose > 3:
41
+ raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
42
+
42
43
  if verbose < 1:
43
44
  disable_tqdm = True
44
45
 
45
- if verbose <= 1:
46
- logging.config.dictConfig(
47
- {
48
- "version": 1,
49
- "disable_existing_loggers": False,
50
- "handlers": {
51
- "null": {"level": "DEBUG", "class": "logging.NullHandler"}
52
- },
53
- "loggers": {
54
- "eodag": {"handlers": ["null"], "propagate": True, "level": "INFO"}
55
- },
56
- }
57
- )
58
- elif verbose == 2:
59
- logging.config.dictConfig(
60
- {
61
- "version": 1,
62
- "disable_existing_loggers": False,
63
- "formatters": {
64
- "standard": {
65
- "format": "%(asctime)-15s %(name)-32s [%(levelname)-8s] %(message)s"
66
- }
67
- },
68
- "handlers": {
69
- "console": {
70
- "level": "DEBUG",
71
- "class": "logging.StreamHandler",
72
- "formatter": "standard",
73
- }
74
- },
75
- "loggers": {
76
- "eodag": {
77
- "handlers": ["console"],
78
- "propagate": True,
79
- "level": "INFO",
80
- },
81
- "sentinelsat": {
82
- "handlers": ["console"],
83
- "propagate": True,
84
- "level": "INFO",
85
- },
86
- },
87
- }
88
- )
89
- elif verbose == 3:
90
- logging.config.dictConfig(
91
- {
92
- "version": 1,
93
- "disable_existing_loggers": False,
94
- "formatters": {
95
- "verbose": {
96
- "format": (
97
- "%(asctime)-15s %(name)-32s [%(levelname)-8s] (tid=%(thread)d) %(message)s"
98
- )
99
- }
100
- },
101
- "handlers": {
102
- "console": {
103
- "level": "DEBUG",
104
- "class": "logging.StreamHandler",
105
- "formatter": "verbose",
106
- }
46
+ level = "DEBUG" if verbose == 3 else "INFO"
47
+
48
+ handlers = {
49
+ "console": {
50
+ "level": level,
51
+ "class": "logging.StreamHandler",
52
+ "formatter": "standard",
53
+ },
54
+ "null": {"level": level, "class": "logging.NullHandler"},
55
+ }
56
+ handler = "console" if verbose > 1 else "null"
57
+
58
+ logging.config.dictConfig(
59
+ {
60
+ "version": 1,
61
+ "disable_existing_loggers": False,
62
+ "formatters": {
63
+ "standard": {
64
+ "format": "%(asctime)-15s %(name)-32s [%(levelname)-8s] %(message)s"
65
+ }
66
+ },
67
+ "handlers": handlers,
68
+ "loggers": {
69
+ "eodag": {
70
+ "handlers": [handler],
71
+ "propagate": True,
72
+ "level": f"{level}",
107
73
  },
108
- "loggers": {
109
- "eodag": {
110
- "handlers": ["console"],
111
- "propagate": True,
112
- "level": "DEBUG",
113
- },
114
- "sentinelsat": {
115
- "handlers": ["console"],
116
- "propagate": True,
117
- "level": "DEBUG",
118
- },
74
+ "eodag-cube": {
75
+ "handlers": [handler],
76
+ "propagate": True,
77
+ "level": f"{level}",
119
78
  },
120
- }
121
- )
122
- else:
123
- raise ValueError("'verbose' must be one of: 0, 1, 2, 3")
79
+ },
80
+ }
81
+ )
124
82
 
125
83
 
126
84
  def get_logging_verbose() -> Optional[int]:
@@ -142,7 +100,6 @@ def get_logging_verbose() -> Optional[int]:
142
100
  3
143
101
 
144
102
  :returns: Verbose level in ``[0, 1, 2, 3]`` or None if not set
145
- :rtype: int or None
146
103
  """
147
104
  global disable_tqdm
148
105
  logger = logging.getLogger("eodag")
eodag/utils/notebook.py CHANGED
@@ -23,7 +23,7 @@ from typing import Any, Optional
23
23
  def check_ipython() -> bool:
24
24
  """Check if called from ipython"""
25
25
  try:
26
- __IPYTHON__
26
+ __IPYTHON__ # type: ignore[name-defined]
27
27
  return True
28
28
  except NameError:
29
29
  return False
@@ -32,7 +32,7 @@ def check_ipython() -> bool:
32
32
  def check_notebook() -> bool:
33
33
  """Check if called from a notebook"""
34
34
  try:
35
- shell = get_ipython().__class__.__name__
35
+ shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
36
36
  if shell == "ZMQInteractiveShell":
37
37
  return True # Jupyter notebook or qtconsole
38
38
  elif shell == "TerminalInteractiveShell":
@@ -69,7 +69,7 @@ class NotebookWidgets:
69
69
  if not self.is_notebook:
70
70
  return None
71
71
 
72
- self.html_box.data = html_value
72
+ setattr(self.html_box, "data", html_value)
73
73
 
74
74
  if not self.html_box_shown:
75
75
  self._html_handle = self.display(self.html_box, display_id=True)
@@ -83,5 +83,5 @@ class NotebookWidgets:
83
83
  if not self.is_notebook:
84
84
  return None
85
85
 
86
- self.html_box.data = ""
86
+ setattr(self.html_box, "data", "")
87
87
  self._update_display(self.html_box, display_id=self._html_handle.display_id)
eodag/utils/repr.py ADDED
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024, 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 collections.abc
21
+ from typing import Any, Optional
22
+ from urllib.parse import urlparse
23
+
24
+
25
+ def str_as_href(link: str) -> str:
26
+ """URL to html link"""
27
+ if urlparse(link).scheme in ("file", "http", "https", "s3"):
28
+ return f"<a href='{link}' target='_blank'>{link}</a>"
29
+ else:
30
+ return link
31
+
32
+
33
+ def html_table(input: Any, depth: Optional[int] = None) -> str:
34
+ """Transform input to HTML table"""
35
+ if isinstance(input, collections.abc.Mapping):
36
+ return dict_to_html_table(input, depth=depth)
37
+ elif isinstance(input, collections.abc.Sequence) and not isinstance(input, str):
38
+ return list_to_html_table(input, depth=depth)
39
+ elif isinstance(input, str):
40
+ return f"'{str_as_href(input)}'"
41
+ else:
42
+ return str_as_href(str(input))
43
+
44
+
45
+ def dict_to_html_table(
46
+ input_dict: collections.abc.Mapping,
47
+ depth: Optional[int] = None,
48
+ brackets: bool = True,
49
+ ) -> str:
50
+ """Transform input dict to HTML table"""
51
+ opening_bracket = "<span style='color: grey;'>{</span>" if brackets else ""
52
+ closing_bracket = "<span style='color: grey;'>}</span>" if brackets else ""
53
+ indent = "10px" if brackets else "0"
54
+
55
+ if depth is not None:
56
+ depth -= 1
57
+
58
+ if depth is None or depth >= 0:
59
+ return (
60
+ f"{opening_bracket}<table style='margin: 0;'>"
61
+ + "".join(
62
+ [
63
+ f"""<tr style='background-color: transparent;'>
64
+ <td style='padding: 5px 0 0 {indent}; text-align: left; color: grey; vertical-align:top;'>{k}:</td>
65
+ <td style='padding: 5px 0 0 10px; text-align: left;'>{
66
+ html_table(v, depth=depth)
67
+ },</td>
68
+ </tr>
69
+ """
70
+ for k, v in input_dict.items()
71
+ ]
72
+ )
73
+ + f"</table>{closing_bracket}"
74
+ )
75
+ else:
76
+ return (
77
+ f"{opening_bracket}"
78
+ + ", ".join(
79
+ [
80
+ f"""<span style='text-align: left;'>
81
+ '{k}': {html_table(v, depth=depth)}
82
+ </span>"""
83
+ for k, v in input_dict.items()
84
+ ]
85
+ )
86
+ + f"{closing_bracket}"
87
+ )
88
+
89
+
90
+ def list_to_html_table(
91
+ input_list: collections.abc.Sequence, depth: Optional[int] = None
92
+ ) -> str:
93
+ """Transform input list to HTML table"""
94
+ if depth is not None:
95
+ depth -= 1
96
+ separator = (
97
+ ",<br />"
98
+ if any(isinstance(v, collections.abc.Mapping) for v in input_list)
99
+ else ", "
100
+ )
101
+ return (
102
+ "<span style='color: grey;'>[</span>"
103
+ + separator.join(
104
+ [
105
+ f"""<span style='text-align: left;'>{
106
+ html_table(v, depth=depth)
107
+ }</span>
108
+ """
109
+ for v in input_list
110
+ ]
111
+ )
112
+ + "<span style='color: grey;'>]</span>"
113
+ )
@@ -0,0 +1,128 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2024, 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 os
22
+ from typing import Any, Optional, Tuple
23
+
24
+ import requests
25
+
26
+ from eodag.utils import HTTP_REQ_TIMEOUT, USER_AGENT, path_to_uri, uri_to_path
27
+ from eodag.utils.exceptions import RequestError, TimeOutError
28
+
29
+ logger = logging.getLogger("eodag.utils.requests")
30
+
31
+
32
+ def fetch_json(
33
+ file_url: str,
34
+ req_session: Optional[requests.Session] = None,
35
+ auth: Optional[requests.auth.AuthBase] = None,
36
+ timeout: float = HTTP_REQ_TIMEOUT,
37
+ ) -> Any:
38
+ """
39
+ Fetches http/distant or local json file
40
+
41
+ :param file_url: url from which the file can be fetched
42
+ :param req_session: (optional) requests session
43
+ :param auth: (optional) authenticated object if request needs authentication
44
+ :param timeout: (optional) authenticated object
45
+ :returns: json file content
46
+ """
47
+ if req_session is None:
48
+ req_session = requests.Session()
49
+ try:
50
+ if not file_url.lower().startswith("http"):
51
+ file_url = path_to_uri(os.path.abspath(file_url))
52
+ req_session.mount("file://", LocalFileAdapter())
53
+
54
+ headers = USER_AGENT
55
+ logger.debug(f"fetching {file_url}")
56
+ res = req_session.get(
57
+ file_url,
58
+ headers=headers,
59
+ auth=auth,
60
+ timeout=timeout,
61
+ )
62
+ res.raise_for_status()
63
+ except requests.exceptions.Timeout as exc:
64
+ raise TimeOutError(exc, timeout=HTTP_REQ_TIMEOUT) from exc
65
+ except requests.exceptions.RequestException as exc:
66
+ raise RequestError.from_error(exc, f"Unable to fetch {file_url}") from exc
67
+ else:
68
+ return res.json()
69
+
70
+
71
+ class LocalFileAdapter(requests.adapters.BaseAdapter):
72
+ """Protocol Adapter to allow Requests to GET file:// URLs inspired
73
+ by https://stackoverflow.com/questions/10123929/fetch-a-file-from-a-local-url-with-python-requests/27786580
74
+ `LocalFileAdapter` class available for the moment (on the 2024-04-22)
75
+ """
76
+
77
+ @staticmethod
78
+ def _chkpath(method: str, path: str) -> Tuple[int, str]:
79
+ """Return an HTTP status for the given filesystem path.
80
+
81
+ :param method: method of the request
82
+ :param path: path of the given file
83
+ :returns: HTTP status and its associated message
84
+ """
85
+ if method.lower() in ("put", "delete"):
86
+ return 501, "Not Implemented" # TODO
87
+ elif method.lower() not in ("get", "head"):
88
+ return 405, "Method Not Allowed"
89
+ elif os.path.isdir(path):
90
+ return 400, "Path Not A File"
91
+ elif not os.path.isfile(path):
92
+ return 404, "File Not Found"
93
+ elif not os.access(path, os.R_OK):
94
+ return 403, "Access Denied"
95
+ else:
96
+ return 200, "OK"
97
+
98
+ def send(
99
+ self, request: requests.PreparedRequest, *args: Any, **kwargs: Any
100
+ ) -> requests.Response:
101
+ """Wraps a file, described in request, in a Response object.
102
+
103
+ :param req: The PreparedRequest being "sent".
104
+ :param kwargs: (not used) additionnal arguments of the request
105
+ :returns: a Response object containing the file
106
+ """
107
+ response = requests.Response()
108
+
109
+ if request.method is None or request.url is None:
110
+ raise RequestError("Method or url of the request is missing")
111
+
112
+ path_url = uri_to_path(request.url)
113
+
114
+ response.status_code, response.reason = self._chkpath(request.method, path_url)
115
+ if response.status_code == 200 and request.method.lower() != "head":
116
+ try:
117
+ response.raw = open(path_url, "rb")
118
+ except (OSError, IOError) as err:
119
+ response.status_code = 500
120
+ response.reason = str(err)
121
+ response.url = request.url
122
+ response.request = request
123
+
124
+ return response
125
+
126
+ def close(self):
127
+ """Closes without cleaning up adapter specific items."""
128
+ pass
eodag/utils/rest.py ADDED
@@ -0,0 +1,100 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright 2018, 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.utils methods that must be importable without eodag[server] installed"""
19
+
20
+ from __future__ import annotations
21
+
22
+ import datetime
23
+ import re
24
+ from typing import Any, Dict, Optional, Tuple
25
+
26
+ import dateutil.parser
27
+ from dateutil import tz
28
+
29
+ from eodag.utils.exceptions import ValidationError
30
+
31
+ RFC3339_PATTERN = (
32
+ r"^(\d{4})-(\d{2})-(\d{2})"
33
+ r"(?:T(\d{2}):(\d{2}):(\d{2})(\.\d+)?"
34
+ r"(Z|([+-])(\d{2}):(\d{2}))?)?$"
35
+ )
36
+
37
+
38
+ def get_datetime(arguments: Dict[str, Any]) -> Tuple[Optional[str], Optional[str]]:
39
+ """Get start and end dates from a dict containing `/` separated dates in `datetime` item
40
+
41
+ :param arguments: dict containing a single date or `/` separated dates in `datetime` item
42
+ :returns: Start date and end date from datetime string (duplicate value if only one date as input)
43
+ """
44
+ datetime_str = arguments.pop("datetime", None)
45
+
46
+ if datetime_str:
47
+ datetime_split = datetime_str.split("/")
48
+ if len(datetime_split) > 1:
49
+ dtstart = datetime_split[0] if datetime_split[0] != ".." else None
50
+ dtend = datetime_split[1] if datetime_split[1] != ".." else None
51
+ elif len(datetime_split) == 1:
52
+ # same time for start & end if only one is given
53
+ dtstart, dtend = datetime_split[0:1] * 2
54
+ else:
55
+ return None, None
56
+
57
+ return get_date(dtstart), get_date(dtend)
58
+
59
+ else:
60
+ # return already set (dtstart, dtend) or None
61
+ dtstart = get_date(arguments.pop("dtstart", None))
62
+ dtend = get_date(arguments.pop("dtend", None))
63
+ return get_date(dtstart), get_date(dtend)
64
+
65
+
66
+ def get_date(date: Optional[str]) -> Optional[str]:
67
+ """Check if the input date can be parsed as a date"""
68
+
69
+ if not date:
70
+ return None
71
+ try:
72
+ return (
73
+ dateutil.parser.parse(date)
74
+ .replace(tzinfo=tz.UTC)
75
+ .isoformat()
76
+ .replace("+00:00", "")
77
+ )
78
+ except ValueError as e:
79
+ exc = ValidationError("invalid input date: %s" % e)
80
+ raise exc
81
+
82
+
83
+ def rfc3339_str_to_datetime(s: str) -> datetime.datetime:
84
+ """Convert a string conforming to RFC 3339 to a :class:`datetime.datetime`.
85
+
86
+ :param s: The string to convert to :class:`datetime.datetime`
87
+
88
+ :returns: The datetime represented by the ISO8601 (RFC 3339) formatted string
89
+
90
+ raises: :class:`ValidationError`
91
+ """
92
+ # Uppercase the string
93
+ s = s.upper()
94
+
95
+ # Match against RFC3339 regex.
96
+ result = re.match(RFC3339_PATTERN, s)
97
+ if not result:
98
+ raise ValidationError("Invalid RFC3339 datetime.")
99
+
100
+ return dateutil.parser.isoparse(s).replace(tzinfo=datetime.timezone.utc)