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.
- {datacosmos-0.0.14 → datacosmos-0.0.16}/PKG-INFO +2 -1
- datacosmos-0.0.16/datacosmos/auth/base_authenticator.py +62 -0
- datacosmos-0.0.16/datacosmos/auth/local_authenticator.py +72 -0
- datacosmos-0.0.16/datacosmos/auth/m2m_authenticator.py +63 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/datacosmos_client.py +39 -94
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/PKG-INFO +2 -1
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/SOURCES.txt +3 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/requires.txt +1 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/pyproject.toml +4 -3
- {datacosmos-0.0.14 → datacosmos-0.0.16}/LICENSE.md +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/README.md +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/local_token_fetcher.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/auth/token.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/auth/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/auth/factory.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/constants.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/loaders/yaml_source.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/m2m_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/no_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/url.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/exceptions/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/exceptions/datacosmos_exception.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/collection_client.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/models/collection_update.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/constants/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/processing_level.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/product_type.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/enums/season.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/item_client.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/asset.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/datacosmos_item.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/eo_band.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/item_update.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/raster_band.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/stac_client.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/storage_base.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/storage_client.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/storage/uploader.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/check_api_response.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/url.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos.egg-info/top_level.txt +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.16}/setup.cfg +0 -0
- {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.
|
|
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
|
-
|
|
112
|
-
auth_type = getattr(auth, "type", "m2m")
|
|
119
|
+
auth_type = getattr(self.config.authentication, "type", "m2m")
|
|
113
120
|
if auth_type == "m2m":
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
169
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
self.
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "datacosmos"
|
|
7
|
-
version = "0.0.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/m2m_authentication_config.py
RENAMED
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/config/models/no_authentication_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/collection/models/collection_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/stac/item/models/catalog_search_parameters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/check_api_response.py
RENAMED
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_error.py
RENAMED
|
File without changes
|
{datacosmos-0.0.14 → datacosmos-0.0.16}/datacosmos/utils/http_response/models/datacosmos_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|