datacosmos 0.0.15__tar.gz → 0.0.17__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.
Files changed (67) hide show
  1. {datacosmos-0.0.15 → datacosmos-0.0.17}/PKG-INFO +1 -1
  2. datacosmos-0.0.17/datacosmos/auth/base_authenticator.py +62 -0
  3. datacosmos-0.0.17/datacosmos/auth/local_authenticator.py +72 -0
  4. datacosmos-0.0.17/datacosmos/auth/m2m_authenticator.py +63 -0
  5. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/datacosmos_client.py +69 -86
  6. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos.egg-info/PKG-INFO +1 -1
  7. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos.egg-info/SOURCES.txt +3 -0
  8. {datacosmos-0.0.15 → datacosmos-0.0.17}/pyproject.toml +1 -1
  9. {datacosmos-0.0.15 → datacosmos-0.0.17}/LICENSE.md +0 -0
  10. {datacosmos-0.0.15 → datacosmos-0.0.17}/README.md +0 -0
  11. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/__init__.py +0 -0
  12. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/auth/__init__.py +0 -0
  13. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/auth/local_token_fetcher.py +0 -0
  14. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/auth/token.py +0 -0
  15. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/__init__.py +0 -0
  16. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/auth/__init__.py +0 -0
  17. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/auth/factory.py +0 -0
  18. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/config.py +0 -0
  19. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/constants.py +0 -0
  20. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/loaders/yaml_source.py +0 -0
  21. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/__init__.py +0 -0
  22. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/authentication_config.py +0 -0
  23. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
  24. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/m2m_authentication_config.py +0 -0
  25. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/no_authentication_config.py +0 -0
  26. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/config/models/url.py +0 -0
  27. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/exceptions/__init__.py +0 -0
  28. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/exceptions/datacosmos_exception.py +0 -0
  29. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/__init__.py +0 -0
  30. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/collection/__init__.py +0 -0
  31. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/collection/collection_client.py +0 -0
  32. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/collection/models/__init__.py +0 -0
  33. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/collection/models/collection_update.py +0 -0
  34. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/constants/__init__.py +0 -0
  35. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
  36. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/enums/__init__.py +0 -0
  37. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/enums/processing_level.py +0 -0
  38. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/enums/product_type.py +0 -0
  39. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/enums/season.py +0 -0
  40. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/__init__.py +0 -0
  41. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/item_client.py +0 -0
  42. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/__init__.py +0 -0
  43. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/asset.py +0 -0
  44. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
  45. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/datacosmos_item.py +0 -0
  46. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/eo_band.py +0 -0
  47. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/item_update.py +0 -0
  48. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/item/models/raster_band.py +0 -0
  49. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/stac_client.py +0 -0
  50. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/__init__.py +0 -0
  51. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
  52. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
  53. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/storage_base.py +0 -0
  54. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/storage_client.py +0 -0
  55. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/stac/storage/uploader.py +0 -0
  56. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/__init__.py +0 -0
  57. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/http_response/__init__.py +0 -0
  58. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/http_response/check_api_response.py +0 -0
  59. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/http_response/models/__init__.py +0 -0
  60. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  61. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  62. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos/utils/url.py +0 -0
  63. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos.egg-info/dependency_links.txt +0 -0
  64. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos.egg-info/requires.txt +0 -0
  65. {datacosmos-0.0.15 → datacosmos-0.0.17}/datacosmos.egg-info/top_level.txt +0 -0
  66. {datacosmos-0.0.15 → datacosmos-0.0.17}/setup.cfg +0 -0
  67. {datacosmos-0.0.15 → datacosmos-0.0.17}/tests/test_pass.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.15
3
+ Version: 0.0.17
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
@@ -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,12 +1,12 @@
1
1
  """Client to interact with the Datacosmos API with authentication and request handling."""
2
+ from __future__ import annotations
2
3
 
4
+ import logging
3
5
  import threading
4
6
  from datetime import datetime, timedelta, timezone
5
- from pathlib import Path
6
- from typing import Any, Optional
7
+ from typing import Any, Callable, List, Optional
7
8
 
8
9
  import requests
9
- from oauthlib.oauth2 import BackendApplicationClient
10
10
  from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
11
11
  from requests_oauthlib import OAuth2Session
12
12
  from tenacity import (
@@ -16,9 +16,17 @@ from tenacity import (
16
16
  wait_exponential,
17
17
  )
18
18
 
19
+ from datacosmos.auth.base_authenticator import BaseAuthenticator
20
+ from datacosmos.auth.local_authenticator import LocalAuthenticator
21
+ from datacosmos.auth.m2m_authenticator import M2MAuthenticator
19
22
  from datacosmos.config.config import Config
20
23
  from datacosmos.exceptions.datacosmos_exception import DatacosmosException
21
24
 
25
+ _log = logging.getLogger(__name__)
26
+
27
+ RequestHook = Callable[[str, str, Any, Any], None]
28
+ ResponseHook = Callable[[requests.Response], None]
29
+
22
30
 
23
31
  class DatacosmosClient:
24
32
  """Client to interact with the Datacosmos API with authentication and request handling."""
@@ -29,17 +37,24 @@ class DatacosmosClient:
29
37
  self,
30
38
  config: Optional[Config | Any] = None,
31
39
  http_session: Optional[requests.Session | OAuth2Session] = None,
40
+ request_hooks: Optional[List[RequestHook]] = None,
41
+ response_hooks: Optional[List[ResponseHook]] = None,
32
42
  ):
33
43
  """Initialize the DatacosmosClient.
34
44
 
35
45
  Args:
36
46
  config (Optional[Config]): Configuration object (only needed when SDK creates its own session).
37
47
  http_session (Optional[requests.Session]): Pre-authenticated session.
48
+ request_hooks (Optional[List[RequestHook]]): A list of functions to be called before each request.
49
+ response_hooks (Optional[List[ResponseHook]]): A list of functions to be called after each successful response.
38
50
  """
39
51
  self.config = self._coerce_config(config)
40
52
  self.token: Optional[str] = None
41
53
  self.token_expiry: Optional[datetime] = None
42
54
  self._refresh_lock = threading.Lock()
55
+ self._authenticator: Optional[BaseAuthenticator] = None
56
+ self._request_hooks = request_hooks or []
57
+ self._response_hooks = response_hooks or []
43
58
 
44
59
  if http_session is not None:
45
60
  self._init_with_injected_session(http_session)
@@ -107,79 +122,26 @@ class DatacosmosClient:
107
122
  try:
108
123
  return datetime.now(timezone.utc) + timedelta(seconds=int(expires_in))
109
124
  except (TypeError, ValueError):
110
- # Unknown/invalid expiry -> mark as unknown so refresh logic kicks in
111
125
  return None
112
126
  return None
113
127
 
114
- # --------------------------- auth/session ---------------------------
128
+ # --------------------------- auth/session (refactored) ---------------------------
115
129
 
116
130
  def _authenticate_and_initialize_client(self) -> requests.Session:
117
- auth = self.config.authentication
118
- auth_type = getattr(auth, "type", "m2m")
131
+ auth_type = getattr(self.config.authentication, "type", "m2m")
119
132
  if auth_type == "m2m":
120
- return self.__build_m2m_session()
121
- if auth_type == "local":
122
- return self.__build_local_session()
123
- raise DatacosmosException(f"Unsupported authentication type: {auth_type}")
124
-
125
- def __build_m2m_session(self) -> requests.Session:
126
- """Client Credentials (M2M) flow using requests-oauthlib."""
127
- auth = self.config.authentication
128
- try:
129
- client = BackendApplicationClient(client_id=auth.client_id)
130
- oauth_session = OAuth2Session(client=client)
131
- token_response = oauth_session.fetch_token(
132
- token_url=auth.token_url,
133
- client_id=auth.client_id,
134
- client_secret=auth.client_secret,
135
- audience=auth.audience,
136
- )
137
- self.token = token_response["access_token"]
138
- expires_at = token_response.get("expires_at")
139
- if isinstance(expires_at, (int, float)):
140
- self.token_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc)
141
- else:
142
- self.token_expiry = datetime.now(timezone.utc) + timedelta(
143
- seconds=int(token_response.get("expires_in", 3600))
144
- )
145
- http_client = requests.Session()
146
- http_client.headers.update({"Authorization": f"Bearer {self.token}"})
147
- return http_client
148
- except (HTTPError, ConnectionError, Timeout) as e:
149
- raise DatacosmosException(f"Authentication failed: {e}") from e
150
- except RequestException as e:
151
- raise DatacosmosException(
152
- f"Unexpected request failure during authentication: {e}"
153
- ) from e
133
+ self._authenticator = M2MAuthenticator(self.config)
134
+ elif auth_type == "local":
135
+ self._authenticator = LocalAuthenticator(self.config)
136
+ else:
137
+ raise DatacosmosException(f"Unsupported authentication type: {auth_type}")
154
138
 
155
- def __build_local_session(self) -> requests.Session:
156
- """Interactive local login via LocalTokenFetcher (cached + refresh)."""
157
- auth = self.config.authentication
158
- try:
159
- from datacosmos.auth.local_token_fetcher import LocalTokenFetcher
160
-
161
- fetcher = LocalTokenFetcher(
162
- client_id=auth.client_id,
163
- authorization_endpoint=auth.authorization_endpoint,
164
- token_endpoint=auth.token_endpoint,
165
- redirect_port=int(auth.redirect_port),
166
- audience=auth.audience,
167
- scopes=auth.scopes,
168
- token_file=Path(auth.cache_file).expanduser(),
169
- )
170
- tok = fetcher.get_token()
171
- except Exception as e:
172
- raise DatacosmosException(f"Local authentication failed: {e}") from e
173
-
174
- self.token = tok.access_token
175
- self.token_expiry = datetime.fromtimestamp(tok.expires_at, tz=timezone.utc)
139
+ auth_result = self._authenticator.authenticate_and_build_session()
140
+ self.token = auth_result.token
141
+ self.token_expiry = auth_result.token_expiry
142
+ return auth_result.http_client
176
143
 
177
- http_client = requests.Session()
178
- http_client.headers.update({"Authorization": f"Bearer {self.token}"})
179
- self._local_token_fetcher = fetcher
180
- return http_client
181
-
182
- # --------------------------- refresh logic ---------------------------
144
+ # --------------------------- refresh logic (refactored) ---------------------------
183
145
 
184
146
  def _needs_refresh(self) -> bool:
185
147
  if not getattr(self, "_owns_session", False):
@@ -191,29 +153,22 @@ class DatacosmosClient:
191
153
  )
192
154
 
193
155
  def _refresh_now(self) -> None:
194
- """Force refresh.
195
-
196
- In case of local auth it uses LocalTokenFetcher (non-interactive refresh/cached token).
197
- In case of m2m auth it re-runs client-credentials flow.
198
- """
156
+ """Force refresh using the delegated authenticator."""
199
157
  with self._refresh_lock:
200
158
  if not self._needs_refresh():
201
159
  return
202
160
 
203
- auth_type = getattr(self.config.authentication, "type", "m2m")
204
- if auth_type == "local" and hasattr(self, "_local_token_fetcher"):
205
- tok = self._local_token_fetcher.get_token()
206
- self.token = tok.access_token
207
- self.token_expiry = datetime.fromtimestamp(
208
- tok.expires_at, tz=timezone.utc
209
- )
161
+ if self._authenticator:
162
+ auth_result = self._authenticator.refresh_token()
163
+ self.token = auth_result.token
164
+ self.token_expiry = auth_result.token_expiry
210
165
  self._http_client.headers.update(
211
166
  {"Authorization": f"Bearer {self.token}"}
212
167
  )
213
- return
214
-
215
- # default/m2m:
216
- self._http_client = self.__build_m2m_session()
168
+ else:
169
+ raise DatacosmosException(
170
+ "Cannot refresh token, no authenticator initialized."
171
+ )
217
172
 
218
173
  def _refresh_token_if_needed(self) -> None:
219
174
  if self._needs_refresh():
@@ -229,16 +184,44 @@ class DatacosmosClient:
229
184
  def request(
230
185
  self, method: str, url: str, *args: Any, **kwargs: Any
231
186
  ) -> requests.Response:
232
- """Send an HTTP request using the authenticated session (with auto-refresh and retries)."""
187
+ """Send an HTTP request using the authenticated session (with auto-refresh and retries).
188
+
189
+ Args:
190
+ method (str): The HTTP method (e.g., "GET", "POST").
191
+ url (str): The URL for the request.
192
+ *args: Positional arguments for requests.request().
193
+ **kwargs: Keyword arguments for requests.request().
194
+
195
+ Returns:
196
+ requests.Response: The HTTP response.
197
+
198
+ Raises:
199
+ DatacosmosException: For any HTTP or request-related errors.
200
+ """
233
201
  self._refresh_token_if_needed()
202
+
203
+ # Call pre-request hooks
204
+ for hook in self._request_hooks:
205
+ try:
206
+ hook(method, url, *args, **kwargs)
207
+ except Exception:
208
+ _log.error("Request hook failed.", exc_info=True)
209
+
234
210
  try:
235
211
  response = self._http_client.request(method, url, *args, **kwargs)
236
212
  response.raise_for_status()
213
+
214
+ # Call post-response hooks on success
215
+ for hook in self._response_hooks:
216
+ try:
217
+ hook(response)
218
+ except Exception:
219
+ _log.error("Response hook failed.", exc_info=True)
220
+
237
221
  return response
238
222
  except HTTPError as e:
239
223
  status = getattr(e.response, "status_code", None)
240
224
  if status in (401, 403) and getattr(self, "_owns_session", False):
241
- # token likely expired/invalid — refresh once and retry
242
225
  self._refresh_now()
243
226
  retry_response = self._http_client.request(method, url, *args, **kwargs)
244
227
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.15
3
+ Version: 0.0.17
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
@@ -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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datacosmos"
7
- version = "0.0.15"
7
+ version = "0.0.17"
8
8
  authors = [
9
9
  { name="Open Cosmos", email="support@open-cosmos.com" },
10
10
  ]
File without changes
File without changes
File without changes