datacosmos 0.0.14__tar.gz → 0.0.16__tar.gz

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.

Potentially problematic release.


This version of datacosmos might be problematic. Click here for more details.

Files changed (67) hide show
  1. {datacosmos-0.0.14 → datacosmos-0.0.16}/PKG-INFO +2 -1
  2. datacosmos-0.0.16/datacosmos/auth/base_authenticator.py +62 -0
  3. datacosmos-0.0.16/datacosmos/auth/local_authenticator.py +72 -0
  4. datacosmos-0.0.16/datacosmos/auth/m2m_authenticator.py +63 -0
  5. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/datacosmos_client.py +39 -94
  6. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/PKG-INFO +2 -1
  7. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/SOURCES.txt +3 -0
  8. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/requires.txt +1 -0
  9. {datacosmos-0.0.14 → datacosmos-0.0.16}/pyproject.toml +4 -3
  10. {datacosmos-0.0.14 → datacosmos-0.0.16}/LICENSE.md +0 -0
  11. {datacosmos-0.0.14 → datacosmos-0.0.16}/README.md +0 -0
  12. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/__init__.py +0 -0
  13. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/__init__.py +0 -0
  14. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/local_token_fetcher.py +0 -0
  15. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/token.py +0 -0
  16. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/__init__.py +0 -0
  17. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/auth/__init__.py +0 -0
  18. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/auth/factory.py +0 -0
  19. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/config.py +0 -0
  20. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/constants.py +0 -0
  21. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/loaders/yaml_source.py +0 -0
  22. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/__init__.py +0 -0
  23. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/authentication_config.py +0 -0
  24. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
  25. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/m2m_authentication_config.py +0 -0
  26. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/no_authentication_config.py +0 -0
  27. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/url.py +0 -0
  28. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/exceptions/__init__.py +0 -0
  29. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/exceptions/datacosmos_exception.py +0 -0
  30. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/__init__.py +0 -0
  31. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/__init__.py +0 -0
  32. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/collection_client.py +0 -0
  33. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/models/__init__.py +0 -0
  34. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/models/collection_update.py +0 -0
  35. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/constants/__init__.py +0 -0
  36. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
  37. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/__init__.py +0 -0
  38. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/processing_level.py +0 -0
  39. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/product_type.py +0 -0
  40. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/season.py +0 -0
  41. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/__init__.py +0 -0
  42. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/item_client.py +0 -0
  43. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/__init__.py +0 -0
  44. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/asset.py +0 -0
  45. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
  46. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/datacosmos_item.py +0 -0
  47. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/eo_band.py +0 -0
  48. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/item_update.py +0 -0
  49. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/raster_band.py +0 -0
  50. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/stac_client.py +0 -0
  51. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/__init__.py +0 -0
  52. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
  53. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
  54. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/storage_base.py +0 -0
  55. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/storage_client.py +0 -0
  56. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/uploader.py +0 -0
  57. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/__init__.py +0 -0
  58. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/__init__.py +0 -0
  59. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/check_api_response.py +0 -0
  60. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/__init__.py +0 -0
  61. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  62. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  63. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/url.py +0 -0
  64. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/dependency_links.txt +0 -0
  65. {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/top_level.txt +0 -0
  66. {datacosmos-0.0.14 → datacosmos-0.0.16}/setup.cfg +0 -0
  67. {datacosmos-0.0.14 → datacosmos-0.0.16}/tests/test_pass.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: A library for interacting with DataCosmos from Python code
5
5
  Author-email: Open Cosmos <support@open-cosmos.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -15,6 +15,7 @@ Requires-Dist: pydantic>=2
15
15
  Requires-Dist: pystac==1.12.1
16
16
  Requires-Dist: pyyaml==6.0.2
17
17
  Requires-Dist: structlog==24.4.0
18
+ Requires-Dist: tenacity>=8.2.3
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: black==22.3.0; extra == "dev"
20
21
  Requires-Dist: ruff==0.9.5; extra == "dev"
@@ -0,0 +1,62 @@
1
+ """Base authenticator class for DatacosmosClient."""
2
+ from __future__ import annotations
3
+
4
+ from abc import ABC, abstractmethod
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING, Optional
7
+
8
+ if TYPE_CHECKING:
9
+ import requests
10
+
11
+ from datacosmos.config.config import Config
12
+
13
+
14
+ class AuthResult:
15
+ """Authentication result object."""
16
+
17
+ def __init__(
18
+ self,
19
+ http_client: requests.Session,
20
+ token: Optional[str] = None,
21
+ token_expiry: Optional[datetime] = None,
22
+ ):
23
+ """Initializes an AuthResult object.
24
+
25
+ Args:
26
+ http_client (requests.Session): The HTTP client/session to use for requests.
27
+ token (Optional[str], optional): The authentication token, if available.
28
+ token_expiry (Optional[datetime], optional): The expiry time of the token, if available.
29
+ """
30
+ self.http_client = http_client
31
+ self.token = token
32
+ self.token_expiry = token_expiry
33
+
34
+
35
+ class BaseAuthenticator(ABC):
36
+ """Abstract base class for all authenticators."""
37
+
38
+ def __init__(self, config: Config):
39
+ """Initializes the BaseAuthenticator with the given configuration.
40
+
41
+ Args:
42
+ config (Config): The configuration object containing authentication settings.
43
+ """
44
+ self.config = config
45
+
46
+ @abstractmethod
47
+ def authenticate_and_build_session(self) -> AuthResult:
48
+ """Authenticates and builds a requests.Session object.
49
+
50
+ Returns:
51
+ AuthResult: An object containing the authenticated session, token, and token expiry.
52
+ """
53
+ ...
54
+
55
+ @abstractmethod
56
+ def refresh_token(self) -> AuthResult:
57
+ """Refreshes the authentication token.
58
+
59
+ Returns:
60
+ AuthResult: An object with the new token and expiry.
61
+ """
62
+ ...
@@ -0,0 +1,72 @@
1
+ """Local (interactive/cached) authenticator for DatacosmosClient."""
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import requests
7
+
8
+ from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
9
+ from datacosmos.auth.local_token_fetcher import LocalTokenFetcher
10
+ from datacosmos.exceptions.datacosmos_exception import DatacosmosException
11
+
12
+
13
+ class LocalAuthenticator(BaseAuthenticator):
14
+ """Handles authentication via the interactive local login flow."""
15
+
16
+ def __init__(self, config: Any):
17
+ """Initializes a LocalAuthenticator instance.
18
+
19
+ Args:
20
+ config (Any): Configuration object containing authentication settings.
21
+ """
22
+ super().__init__(config)
23
+ self._local_token_fetcher = self._init_fetcher()
24
+
25
+ def _init_fetcher(self) -> LocalTokenFetcher:
26
+ """Initializes the LocalTokenFetcher."""
27
+ auth = self.config.authentication
28
+ try:
29
+ return LocalTokenFetcher(
30
+ client_id=auth.client_id,
31
+ authorization_endpoint=auth.authorization_endpoint,
32
+ token_endpoint=auth.token_endpoint,
33
+ redirect_port=int(auth.redirect_port),
34
+ audience=auth.audience,
35
+ scopes=auth.scopes,
36
+ token_file=Path(auth.cache_file).expanduser(),
37
+ )
38
+ except Exception as e:
39
+ raise DatacosmosException(
40
+ f"Failed to initialize LocalTokenFetcher: {e}"
41
+ ) from e
42
+
43
+ def authenticate_and_build_session(self) -> AuthResult:
44
+ """Builds an authenticated session using the local token fetcher."""
45
+ try:
46
+ tok = self._local_token_fetcher.get_token()
47
+ token = tok.access_token
48
+ token_expiry = datetime.fromtimestamp(tok.expires_at, tz=timezone.utc)
49
+ http_client = requests.Session()
50
+ http_client.headers.update({"Authorization": f"Bearer {token}"})
51
+ return AuthResult(
52
+ http_client=http_client, token=token, token_expiry=token_expiry
53
+ )
54
+ except Exception as e:
55
+ raise DatacosmosException(f"Local authentication failed: {e}") from e
56
+
57
+ def refresh_token(self) -> AuthResult:
58
+ """Refreshes the local token non-interactively."""
59
+ try:
60
+ tok = self._local_token_fetcher.get_token()
61
+ token = tok.access_token
62
+ token_expiry = datetime.fromtimestamp(tok.expires_at, tz=timezone.utc)
63
+
64
+ # Create a new session with the new token
65
+ http_client = requests.Session()
66
+ http_client.headers.update({"Authorization": f"Bearer {token}"})
67
+
68
+ return AuthResult(
69
+ http_client=http_client, token=token, token_expiry=token_expiry
70
+ )
71
+ except Exception as e:
72
+ raise DatacosmosException(f"Local token refresh failed: {e}") from e
@@ -0,0 +1,63 @@
1
+ """M2M (Machine-to-Machine) authenticator for DatacosmosClient."""
2
+ from datetime import datetime, timedelta, timezone
3
+
4
+ import requests
5
+ from oauthlib.oauth2 import BackendApplicationClient
6
+ from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
7
+ from requests_oauthlib import OAuth2Session
8
+ from tenacity import (
9
+ retry,
10
+ retry_if_exception_type,
11
+ stop_after_attempt,
12
+ wait_exponential,
13
+ )
14
+
15
+ from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
16
+ from datacosmos.exceptions.datacosmos_exception import DatacosmosException
17
+
18
+
19
+ class M2MAuthenticator(BaseAuthenticator):
20
+ """Handles authentication using the Client Credentials (M2M) flow."""
21
+
22
+ @retry(
23
+ stop=stop_after_attempt(3),
24
+ wait=wait_exponential(multiplier=1, min=2, max=10),
25
+ retry=retry_if_exception_type((ConnectionError, Timeout)),
26
+ )
27
+ def authenticate_and_build_session(self) -> AuthResult:
28
+ """Builds an authenticated session using the M2M flow."""
29
+ auth = self.config.authentication
30
+ try:
31
+ client = BackendApplicationClient(client_id=auth.client_id)
32
+ oauth_session = OAuth2Session(client=client)
33
+ token_response = oauth_session.fetch_token(
34
+ token_url=auth.token_url,
35
+ client_id=auth.client_id,
36
+ client_secret=auth.client_secret,
37
+ audience=auth.audience,
38
+ )
39
+ token = token_response["access_token"]
40
+ expires_at = token_response.get("expires_at")
41
+ if isinstance(expires_at, (int, float)):
42
+ token_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc)
43
+ else:
44
+ expires_in = int(token_response.get("expires_in", 3600))
45
+ token_expiry = datetime.now(timezone.utc) + timedelta(
46
+ seconds=expires_in
47
+ )
48
+
49
+ http_client = requests.Session()
50
+ http_client.headers.update({"Authorization": f"Bearer {token}"})
51
+ return AuthResult(
52
+ http_client=http_client, token=token, token_expiry=token_expiry
53
+ )
54
+ except (HTTPError, ConnectionError, Timeout) as e:
55
+ raise DatacosmosException(f"M2M authentication failed: {e}") from e
56
+ except RequestException as e:
57
+ raise DatacosmosException(
58
+ f"Unexpected request failure during M2M authentication: {e}"
59
+ ) from e
60
+
61
+ def refresh_token(self) -> AuthResult:
62
+ """Refreshes the M2M token by re-running the authentication flow."""
63
+ return self.authenticate_and_build_session()
@@ -1,15 +1,23 @@
1
1
  """Client to interact with the Datacosmos API with authentication and request handling."""
2
+ from __future__ import annotations
2
3
 
3
4
  import threading
4
5
  from datetime import datetime, timedelta, timezone
5
- from pathlib import Path
6
6
  from typing import Any, Optional
7
7
 
8
8
  import requests
9
- from oauthlib.oauth2 import BackendApplicationClient
10
9
  from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
11
10
  from requests_oauthlib import OAuth2Session
12
-
11
+ from tenacity import (
12
+ retry,
13
+ retry_if_exception_type,
14
+ stop_after_attempt,
15
+ wait_exponential,
16
+ )
17
+
18
+ from datacosmos.auth.base_authenticator import BaseAuthenticator
19
+ from datacosmos.auth.local_authenticator import LocalAuthenticator
20
+ from datacosmos.auth.m2m_authenticator import M2MAuthenticator
13
21
  from datacosmos.config.config import Config
14
22
  from datacosmos.exceptions.datacosmos_exception import DatacosmosException
15
23
 
@@ -34,6 +42,7 @@ class DatacosmosClient:
34
42
  self.token: Optional[str] = None
35
43
  self.token_expiry: Optional[datetime] = None
36
44
  self._refresh_lock = threading.Lock()
45
+ self._authenticator: Optional[BaseAuthenticator] = None
37
46
 
38
47
  if http_session is not None:
39
48
  self._init_with_injected_session(http_session)
@@ -101,79 +110,26 @@ class DatacosmosClient:
101
110
  try:
102
111
  return datetime.now(timezone.utc) + timedelta(seconds=int(expires_in))
103
112
  except (TypeError, ValueError):
104
- # Unknown/invalid expiry -> mark as unknown so refresh logic kicks in
105
113
  return None
106
114
  return None
107
115
 
108
- # --------------------------- auth/session ---------------------------
116
+ # --------------------------- auth/session (refactored) ---------------------------
109
117
 
110
118
  def _authenticate_and_initialize_client(self) -> requests.Session:
111
- auth = self.config.authentication
112
- auth_type = getattr(auth, "type", "m2m")
119
+ auth_type = getattr(self.config.authentication, "type", "m2m")
113
120
  if auth_type == "m2m":
114
- return self.__build_m2m_session()
115
- if auth_type == "local":
116
- return self.__build_local_session()
117
- raise DatacosmosException(f"Unsupported authentication type: {auth_type}")
118
-
119
- def __build_m2m_session(self) -> requests.Session:
120
- """Client Credentials (M2M) flow using requests-oauthlib."""
121
- auth = self.config.authentication
122
- try:
123
- client = BackendApplicationClient(client_id=auth.client_id)
124
- oauth_session = OAuth2Session(client=client)
125
- token_response = oauth_session.fetch_token(
126
- token_url=auth.token_url,
127
- client_id=auth.client_id,
128
- client_secret=auth.client_secret,
129
- audience=auth.audience,
130
- )
131
- self.token = token_response["access_token"]
132
- expires_at = token_response.get("expires_at")
133
- if isinstance(expires_at, (int, float)):
134
- self.token_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc)
135
- else:
136
- self.token_expiry = datetime.now(timezone.utc) + timedelta(
137
- seconds=int(token_response.get("expires_in", 3600))
138
- )
139
- http_client = requests.Session()
140
- http_client.headers.update({"Authorization": f"Bearer {self.token}"})
141
- return http_client
142
- except (HTTPError, ConnectionError, Timeout) as e:
143
- raise DatacosmosException(f"Authentication failed: {e}") from e
144
- except RequestException as e:
145
- raise DatacosmosException(
146
- f"Unexpected request failure during authentication: {e}"
147
- ) from e
148
-
149
- def __build_local_session(self) -> requests.Session:
150
- """Interactive local login via LocalTokenFetcher (cached + refresh)."""
151
- auth = self.config.authentication
152
- try:
153
- from datacosmos.auth.local_token_fetcher import LocalTokenFetcher
154
-
155
- fetcher = LocalTokenFetcher(
156
- client_id=auth.client_id,
157
- authorization_endpoint=auth.authorization_endpoint,
158
- token_endpoint=auth.token_endpoint,
159
- redirect_port=int(auth.redirect_port),
160
- audience=auth.audience,
161
- scopes=auth.scopes,
162
- token_file=Path(auth.cache_file).expanduser(),
163
- )
164
- tok = fetcher.get_token()
165
- except Exception as e:
166
- raise DatacosmosException(f"Local authentication failed: {e}") from e
121
+ self._authenticator = M2MAuthenticator(self.config)
122
+ elif auth_type == "local":
123
+ self._authenticator = LocalAuthenticator(self.config)
124
+ else:
125
+ raise DatacosmosException(f"Unsupported authentication type: {auth_type}")
167
126
 
168
- self.token = tok.access_token
169
- self.token_expiry = datetime.fromtimestamp(tok.expires_at, tz=timezone.utc)
127
+ auth_result = self._authenticator.authenticate_and_build_session()
128
+ self.token = auth_result.token
129
+ self.token_expiry = auth_result.token_expiry
130
+ return auth_result.http_client
170
131
 
171
- http_client = requests.Session()
172
- http_client.headers.update({"Authorization": f"Bearer {self.token}"})
173
- self._local_token_fetcher = fetcher
174
- return http_client
175
-
176
- # --------------------------- refresh logic ---------------------------
132
+ # --------------------------- refresh logic (refactored) ---------------------------
177
133
 
178
134
  def _needs_refresh(self) -> bool:
179
135
  if not getattr(self, "_owns_session", False):
@@ -185,29 +141,22 @@ class DatacosmosClient:
185
141
  )
186
142
 
187
143
  def _refresh_now(self) -> None:
188
- """Force refresh.
189
-
190
- In case of local auth it uses LocalTokenFetcher (non-interactive refresh/cached token).
191
- In case of m2m auth it re-runs client-credentials flow.
192
- """
144
+ """Force refresh using the delegated authenticator."""
193
145
  with self._refresh_lock:
194
146
  if not self._needs_refresh():
195
147
  return
196
148
 
197
- auth_type = getattr(self.config.authentication, "type", "m2m")
198
- if auth_type == "local" and hasattr(self, "_local_token_fetcher"):
199
- tok = self._local_token_fetcher.get_token()
200
- self.token = tok.access_token
201
- self.token_expiry = datetime.fromtimestamp(
202
- tok.expires_at, tz=timezone.utc
203
- )
149
+ if self._authenticator:
150
+ auth_result = self._authenticator.refresh_token()
151
+ self.token = auth_result.token
152
+ self.token_expiry = auth_result.token_expiry
204
153
  self._http_client.headers.update(
205
154
  {"Authorization": f"Bearer {self.token}"}
206
155
  )
207
- return
208
-
209
- # default/m2m:
210
- self._http_client = self.__build_m2m_session()
156
+ else:
157
+ raise DatacosmosException(
158
+ "Cannot refresh token, no authenticator initialized."
159
+ )
211
160
 
212
161
  def _refresh_token_if_needed(self) -> None:
213
162
  if self._needs_refresh():
@@ -215,10 +164,15 @@ class DatacosmosClient:
215
164
 
216
165
  # --------------------------- request API ---------------------------
217
166
 
167
+ @retry(
168
+ stop=stop_after_attempt(5),
169
+ wait=wait_exponential(multiplier=1, min=2, max=20),
170
+ retry=retry_if_exception_type((ConnectionError, Timeout)),
171
+ )
218
172
  def request(
219
173
  self, method: str, url: str, *args: Any, **kwargs: Any
220
174
  ) -> requests.Response:
221
- """Send an HTTP request using the authenticated session (with auto-refresh)."""
175
+ """Send an HTTP request using the authenticated session (with auto-refresh and retries)."""
222
176
  self._refresh_token_if_needed()
223
177
  try:
224
178
  response = self._http_client.request(method, url, *args, **kwargs)
@@ -227,7 +181,6 @@ class DatacosmosClient:
227
181
  except HTTPError as e:
228
182
  status = getattr(e.response, "status_code", None)
229
183
  if status in (401, 403) and getattr(self, "_owns_session", False):
230
- # token likely expired/invalid — refresh once and retry
231
184
  self._refresh_now()
232
185
  retry_response = self._http_client.request(method, url, *args, **kwargs)
233
186
  try:
@@ -242,14 +195,6 @@ class DatacosmosClient:
242
195
  f"HTTP error during {method.upper()} request to {url}",
243
196
  response=getattr(e, "response", None),
244
197
  ) from e
245
- except ConnectionError as e:
246
- raise DatacosmosException(
247
- f"Connection error during {method.upper()} request to {url}: {e}"
248
- ) from e
249
- except Timeout as e:
250
- raise DatacosmosException(
251
- f"Request timeout during {method.upper()} request to {url}: {e}"
252
- ) from e
253
198
  except RequestException as e:
254
199
  raise DatacosmosException(
255
200
  f"Unexpected request failure during {method.upper()} request to {url}: {e}"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.14
3
+ Version: 0.0.16
4
4
  Summary: A library for interacting with DataCosmos from Python code
5
5
  Author-email: Open Cosmos <support@open-cosmos.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -15,6 +15,7 @@ Requires-Dist: pydantic>=2
15
15
  Requires-Dist: pystac==1.12.1
16
16
  Requires-Dist: pyyaml==6.0.2
17
17
  Requires-Dist: structlog==24.4.0
18
+ Requires-Dist: tenacity>=8.2.3
18
19
  Provides-Extra: dev
19
20
  Requires-Dist: black==22.3.0; extra == "dev"
20
21
  Requires-Dist: ruff==0.9.5; extra == "dev"
@@ -9,7 +9,10 @@ datacosmos.egg-info/dependency_links.txt
9
9
  datacosmos.egg-info/requires.txt
10
10
  datacosmos.egg-info/top_level.txt
11
11
  datacosmos/auth/__init__.py
12
+ datacosmos/auth/base_authenticator.py
13
+ datacosmos/auth/local_authenticator.py
12
14
  datacosmos/auth/local_token_fetcher.py
15
+ datacosmos/auth/m2m_authenticator.py
13
16
  datacosmos/auth/token.py
14
17
  datacosmos/config/__init__.py
15
18
  datacosmos/config/config.py
@@ -6,6 +6,7 @@ pydantic>=2
6
6
  pystac==1.12.1
7
7
  pyyaml==6.0.2
8
8
  structlog==24.4.0
9
+ tenacity>=8.2.3
9
10
 
10
11
  [dev]
11
12
  black==22.3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datacosmos"
7
- version = "0.0.14"
7
+ version = "0.0.16"
8
8
  authors = [
9
9
  { name="Open Cosmos", email="support@open-cosmos.com" },
10
10
  ]
@@ -22,7 +22,8 @@ dependencies = [
22
22
  "pydantic>=2",
23
23
  "pystac==1.12.1",
24
24
  "pyyaml==6.0.2",
25
- "structlog==24.4.0"
25
+ "structlog==24.4.0",
26
+ "tenacity>=8.2.3"
26
27
  ]
27
28
 
28
29
  [project.optional-dependencies]
@@ -51,4 +52,4 @@ multi_line_output = 3
51
52
  include_trailing_comma = true
52
53
  force_grid_wrap = 0
53
54
  use_parentheses = true
54
- line_length = 88
55
+ line_length = 88
File without changes
File without changes
File without changes