datacosmos 0.0.14__tar.gz → 0.0.27__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.
- {datacosmos-0.0.14 → datacosmos-0.0.27}/PKG-INFO +3 -1
- datacosmos-0.0.27/datacosmos/auth/base_authenticator.py +62 -0
- datacosmos-0.0.27/datacosmos/auth/local_authenticator.py +72 -0
- datacosmos-0.0.27/datacosmos/auth/m2m_authenticator.py +65 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/auth/factory.py +27 -40
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/config.py +13 -26
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/datacosmos_client.py +94 -106
- datacosmos-0.0.27/datacosmos/exceptions/__init__.py +15 -0
- datacosmos-0.0.27/datacosmos/exceptions/authentication_error.py +8 -0
- datacosmos-0.0.14/datacosmos/exceptions/datacosmos_exception.py → datacosmos-0.0.27/datacosmos/exceptions/datacosmos_error.py +2 -2
- datacosmos-0.0.27/datacosmos/exceptions/http_error.py +8 -0
- datacosmos-0.0.27/datacosmos/exceptions/stac_validation_error.py +8 -0
- datacosmos-0.0.27/datacosmos/exceptions/upload_error.py +23 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/collection/collection_client.py +6 -6
- datacosmos-0.0.27/datacosmos/stac/enums/processing_level.py +28 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/item_client.py +39 -35
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/catalog_search_parameters.py +6 -5
- datacosmos-0.0.27/datacosmos/stac/item/models/datacosmos_item.py +182 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/item_update.py +4 -4
- datacosmos-0.0.27/datacosmos/stac/storage/dataclasses/upload_path.py +78 -0
- datacosmos-0.0.27/datacosmos/stac/storage/dataclasses/upload_result.py +20 -0
- datacosmos-0.0.27/datacosmos/stac/storage/downloader.py +119 -0
- datacosmos-0.0.27/datacosmos/stac/storage/storage_base.py +86 -0
- datacosmos-0.0.27/datacosmos/stac/storage/storage_client.py +64 -0
- datacosmos-0.0.27/datacosmos/stac/storage/uploader.py +236 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/http_response/check_api_response.py +6 -6
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos.egg-info/PKG-INFO +3 -1
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos.egg-info/SOURCES.txt +10 -1
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos.egg-info/requires.txt +2 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/pyproject.toml +5 -3
- datacosmos-0.0.14/datacosmos/exceptions/__init__.py +0 -1
- datacosmos-0.0.14/datacosmos/stac/enums/processing_level.py +0 -16
- datacosmos-0.0.14/datacosmos/stac/item/models/datacosmos_item.py +0 -55
- datacosmos-0.0.14/datacosmos/stac/storage/dataclasses/upload_path.py +0 -42
- datacosmos-0.0.14/datacosmos/stac/storage/storage_base.py +0 -40
- datacosmos-0.0.14/datacosmos/stac/storage/storage_client.py +0 -33
- datacosmos-0.0.14/datacosmos/stac/storage/uploader.py +0 -127
- {datacosmos-0.0.14 → datacosmos-0.0.27}/LICENSE.md +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/README.md +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/auth/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/auth/local_token_fetcher.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/auth/token.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/auth/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/constants.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/loaders/yaml_source.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/m2m_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/no_authentication_config.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/config/models/url.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/collection/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/collection/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/collection/models/collection_update.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/constants/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/enums/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/enums/product_type.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/enums/season.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/asset.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/eo_band.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/item/models/raster_band.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/stac_client.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/storage/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/http_response/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/__init__.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos/utils/url.py +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/datacosmos.egg-info/top_level.txt +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/setup.cfg +0 -0
- {datacosmos-0.0.14 → datacosmos-0.0.27}/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.27
|
|
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,8 @@ 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
|
|
19
|
+
Requires-Dist: shapely>=1.8.0
|
|
18
20
|
Provides-Extra: dev
|
|
19
21
|
Requires-Dist: black==22.3.0; extra == "dev"
|
|
20
22
|
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 import AuthenticationError
|
|
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 AuthenticationError(
|
|
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 AuthenticationError(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 AuthenticationError(f"Local token refresh failed: {e}") from e
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
|
7
|
+
from requests.exceptions import HTTPError as RequestsHTTPError
|
|
8
|
+
from requests.exceptions import RequestException, Timeout
|
|
9
|
+
from requests_oauthlib import OAuth2Session
|
|
10
|
+
from tenacity import (
|
|
11
|
+
retry,
|
|
12
|
+
retry_if_exception_type,
|
|
13
|
+
stop_after_attempt,
|
|
14
|
+
wait_exponential,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
|
|
18
|
+
from datacosmos.exceptions import AuthenticationError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class M2MAuthenticator(BaseAuthenticator):
|
|
22
|
+
"""Handles authentication using the Client Credentials (M2M) flow."""
|
|
23
|
+
|
|
24
|
+
@retry(
|
|
25
|
+
stop=stop_after_attempt(3),
|
|
26
|
+
wait=wait_exponential(multiplier=1, min=2, max=10),
|
|
27
|
+
retry=retry_if_exception_type((ConnectionError, Timeout)),
|
|
28
|
+
)
|
|
29
|
+
def authenticate_and_build_session(self) -> AuthResult:
|
|
30
|
+
"""Builds an authenticated session using the M2M flow."""
|
|
31
|
+
auth = self.config.authentication
|
|
32
|
+
try:
|
|
33
|
+
client = BackendApplicationClient(client_id=auth.client_id)
|
|
34
|
+
oauth_session = OAuth2Session(client=client)
|
|
35
|
+
token_response = oauth_session.fetch_token(
|
|
36
|
+
token_url=auth.token_url,
|
|
37
|
+
client_id=auth.client_id,
|
|
38
|
+
client_secret=auth.client_secret,
|
|
39
|
+
audience=auth.audience,
|
|
40
|
+
)
|
|
41
|
+
token = token_response["access_token"]
|
|
42
|
+
expires_at = token_response.get("expires_at")
|
|
43
|
+
if isinstance(expires_at, (int, float)):
|
|
44
|
+
token_expiry = datetime.fromtimestamp(expires_at, tz=timezone.utc)
|
|
45
|
+
else:
|
|
46
|
+
expires_in = int(token_response.get("expires_in", 3600))
|
|
47
|
+
token_expiry = datetime.now(timezone.utc) + timedelta(
|
|
48
|
+
seconds=expires_in
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
http_client = requests.Session()
|
|
52
|
+
http_client.headers.update({"Authorization": f"Bearer {token}"})
|
|
53
|
+
return AuthResult(
|
|
54
|
+
http_client=http_client, token=token, token_expiry=token_expiry
|
|
55
|
+
)
|
|
56
|
+
except (RequestsHTTPError, ConnectionError, Timeout) as e:
|
|
57
|
+
raise AuthenticationError(f"M2M authentication failed: {e}") from e
|
|
58
|
+
except RequestException as e:
|
|
59
|
+
raise AuthenticationError(
|
|
60
|
+
f"Unexpected request failure during M2M authentication: {e}"
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
def refresh_token(self) -> AuthResult:
|
|
64
|
+
"""Refreshes the M2M token by re-running the authentication flow."""
|
|
65
|
+
return self.authenticate_and_build_session()
|
|
@@ -5,11 +5,6 @@ This module normalizes the `authentication` config into a concrete model:
|
|
|
5
5
|
- `apply_auth_defaults` fills sensible defaults per auth type without inventing secrets.
|
|
6
6
|
- `check_required_auth_fields` enforces the minimum required inputs.
|
|
7
7
|
- `normalize_authentication` runs the whole pipeline.
|
|
8
|
-
|
|
9
|
-
Design notes:
|
|
10
|
-
- Auth models accept partial data (fields are Optional with None defaults).
|
|
11
|
-
- We DO NOT pass `None` explicitly when constructing models here.
|
|
12
|
-
- Required-ness is enforced centrally by `check_required_auth_fields`, not by model init.
|
|
13
8
|
"""
|
|
14
9
|
|
|
15
10
|
from typing import Optional, Union, cast
|
|
@@ -28,54 +23,51 @@ from datacosmos.config.models.local_user_account_authentication_config import (
|
|
|
28
23
|
LocalUserAccountAuthenticationConfig,
|
|
29
24
|
)
|
|
30
25
|
from datacosmos.config.models.m2m_authentication_config import M2MAuthenticationConfig
|
|
26
|
+
from datacosmos.exceptions import AuthenticationError
|
|
31
27
|
|
|
32
28
|
AuthModel = Union[M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig]
|
|
33
29
|
|
|
34
30
|
|
|
35
31
|
def parse_auth_config(raw: dict | AuthModel | None) -> Optional[AuthModel]:
|
|
36
|
-
"""Turn a raw dict (e.g., from YAML) into a concrete auth model.
|
|
37
|
-
|
|
38
|
-
- If `raw` is already an auth model (M2M or local), return it unchanged.
|
|
39
|
-
- If `raw` is a dict, choose/validate the type using `raw['type']`
|
|
40
|
-
(or DEFAULT_AUTH_TYPE), then construct the corresponding model.
|
|
41
|
-
For missing fields we *may* apply non-secret defaults (endpoints, etc.).
|
|
42
|
-
"""
|
|
43
|
-
if raw is None or isinstance(
|
|
44
|
-
raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)
|
|
45
|
-
):
|
|
32
|
+
"""Turn a raw dict (e.g., from YAML/env) into a concrete auth model."""
|
|
33
|
+
if isinstance(raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)):
|
|
46
34
|
return cast(Optional[AuthModel], raw)
|
|
47
35
|
|
|
48
|
-
|
|
36
|
+
if raw is None:
|
|
37
|
+
raw_data = {}
|
|
38
|
+
else:
|
|
39
|
+
raw_data = raw.copy()
|
|
40
|
+
|
|
41
|
+
if raw is None and not raw_data:
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
auth_type = _normalize_auth_type(raw_data.get("type") or DEFAULT_AUTH_TYPE)
|
|
49
45
|
|
|
50
46
|
if auth_type == "local":
|
|
51
47
|
return LocalUserAccountAuthenticationConfig(
|
|
52
48
|
type="local",
|
|
53
|
-
client_id=
|
|
54
|
-
authorization_endpoint=
|
|
49
|
+
client_id=raw_data.get("client_id"),
|
|
50
|
+
authorization_endpoint=raw_data.get(
|
|
55
51
|
"authorization_endpoint", DEFAULT_LOCAL_AUTHORIZATION_ENDPOINT
|
|
56
52
|
),
|
|
57
|
-
token_endpoint=
|
|
58
|
-
redirect_port=
|
|
59
|
-
scopes=
|
|
60
|
-
audience=
|
|
61
|
-
cache_file=
|
|
53
|
+
token_endpoint=raw_data.get("token_endpoint", DEFAULT_LOCAL_TOKEN_ENDPOINT),
|
|
54
|
+
redirect_port=raw_data.get("redirect_port", DEFAULT_LOCAL_REDIRECT_PORT),
|
|
55
|
+
scopes=raw_data.get("scopes", DEFAULT_LOCAL_SCOPES),
|
|
56
|
+
audience=raw_data.get("audience", DEFAULT_AUTH_AUDIENCE),
|
|
57
|
+
cache_file=raw_data.get("cache_file", DEFAULT_LOCAL_CACHE_FILE),
|
|
62
58
|
)
|
|
63
59
|
|
|
64
60
|
return M2MAuthenticationConfig(
|
|
65
61
|
type="m2m",
|
|
66
|
-
token_url=
|
|
67
|
-
audience=
|
|
68
|
-
client_id=
|
|
69
|
-
client_secret=
|
|
62
|
+
token_url=raw_data.get("token_url", DEFAULT_AUTH_TOKEN_URL),
|
|
63
|
+
audience=raw_data.get("audience", DEFAULT_AUTH_AUDIENCE),
|
|
64
|
+
client_id=raw_data.get("client_id"),
|
|
65
|
+
client_secret=raw_data.get("client_secret"),
|
|
70
66
|
)
|
|
71
67
|
|
|
72
68
|
|
|
73
69
|
def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
74
|
-
"""Fill in any missing defaults by type (non-secret values only).
|
|
75
|
-
|
|
76
|
-
If `auth` is None, construct a default "shell" based on DEFAULT_AUTH_TYPE,
|
|
77
|
-
without passing None for unknown credentials.
|
|
78
|
-
"""
|
|
70
|
+
"""Fill in any missing defaults by type (non-secret values only)."""
|
|
79
71
|
if auth is None:
|
|
80
72
|
default_type = _normalize_auth_type(DEFAULT_AUTH_TYPE)
|
|
81
73
|
if default_type == "local":
|
|
@@ -101,7 +93,6 @@ def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
|
101
93
|
auth.audience = auth.audience or DEFAULT_AUTH_AUDIENCE
|
|
102
94
|
return auth
|
|
103
95
|
|
|
104
|
-
# Local defaults (Pydantic already coerces types; only set when missing)
|
|
105
96
|
auth.type = auth.type or "local"
|
|
106
97
|
auth.authorization_endpoint = (
|
|
107
98
|
auth.authorization_endpoint or DEFAULT_LOCAL_AUTHORIZATION_ENDPOINT
|
|
@@ -116,22 +107,18 @@ def apply_auth_defaults(auth: AuthModel | None) -> AuthModel:
|
|
|
116
107
|
|
|
117
108
|
|
|
118
109
|
def check_required_auth_fields(auth: AuthModel) -> None:
|
|
119
|
-
"""Enforce required fields per auth type.
|
|
120
|
-
|
|
121
|
-
- m2m requires client_id and client_secret.
|
|
122
|
-
- local requires client_id.
|
|
123
|
-
"""
|
|
110
|
+
"""Enforce required fields per auth type."""
|
|
124
111
|
if isinstance(auth, M2MAuthenticationConfig):
|
|
125
112
|
missing = [f for f in ("client_id", "client_secret") if not getattr(auth, f)]
|
|
126
113
|
if missing:
|
|
127
|
-
raise
|
|
114
|
+
raise AuthenticationError(
|
|
128
115
|
f"Missing required authentication fields for m2m: {', '.join(missing)}"
|
|
129
116
|
)
|
|
130
117
|
return
|
|
131
118
|
|
|
132
119
|
if isinstance(auth, LocalUserAccountAuthenticationConfig):
|
|
133
120
|
if not auth.client_id:
|
|
134
|
-
raise
|
|
121
|
+
raise AuthenticationError(
|
|
135
122
|
"Missing required authentication field for local: client_id"
|
|
136
123
|
)
|
|
137
124
|
return
|
|
@@ -7,7 +7,7 @@ and supports environment variable-based overrides.
|
|
|
7
7
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from pydantic import field_validator
|
|
10
|
+
from pydantic import Field, field_validator
|
|
11
11
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
12
12
|
|
|
13
13
|
from datacosmos.config.auth.factory import normalize_authentication, parse_auth_config
|
|
@@ -18,6 +18,10 @@ from datacosmos.config.constants import (
|
|
|
18
18
|
)
|
|
19
19
|
from datacosmos.config.loaders.yaml_source import yaml_settings_source
|
|
20
20
|
from datacosmos.config.models.authentication_config import AuthenticationConfig
|
|
21
|
+
from datacosmos.config.models.local_user_account_authentication_config import (
|
|
22
|
+
LocalUserAccountAuthenticationConfig,
|
|
23
|
+
)
|
|
24
|
+
from datacosmos.config.models.m2m_authentication_config import M2MAuthenticationConfig
|
|
21
25
|
from datacosmos.config.models.url import URL
|
|
22
26
|
|
|
23
27
|
|
|
@@ -31,9 +35,14 @@ class Config(BaseSettings):
|
|
|
31
35
|
)
|
|
32
36
|
|
|
33
37
|
authentication: Optional[AuthenticationConfig] = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
stac: URL = Field(default_factory=lambda: URL(**DEFAULT_STAC))
|
|
40
|
+
datacosmos_cloud_storage: URL = Field(
|
|
41
|
+
default_factory=lambda: URL(**DEFAULT_STORAGE)
|
|
42
|
+
)
|
|
43
|
+
datacosmos_public_cloud_storage: URL = Field(
|
|
44
|
+
default_factory=lambda: URL(**DEFAULT_STORAGE)
|
|
45
|
+
)
|
|
37
46
|
|
|
38
47
|
@classmethod
|
|
39
48
|
def settings_customise_sources(cls, *args, **kwargs):
|
|
@@ -65,13 +74,6 @@ class Config(BaseSettings):
|
|
|
65
74
|
def _parse_authentication(cls, raw):
|
|
66
75
|
if raw is None:
|
|
67
76
|
return None
|
|
68
|
-
from datacosmos.config.models.local_user_account_authentication_config import (
|
|
69
|
-
LocalUserAccountAuthenticationConfig,
|
|
70
|
-
)
|
|
71
|
-
from datacosmos.config.models.m2m_authentication_config import (
|
|
72
|
-
M2MAuthenticationConfig,
|
|
73
|
-
)
|
|
74
|
-
|
|
75
77
|
if isinstance(
|
|
76
78
|
raw, (M2MAuthenticationConfig, LocalUserAccountAuthenticationConfig)
|
|
77
79
|
):
|
|
@@ -84,18 +86,3 @@ class Config(BaseSettings):
|
|
|
84
86
|
@classmethod
|
|
85
87
|
def _validate_authentication(cls, auth: Optional[AuthenticationConfig]):
|
|
86
88
|
return normalize_authentication(auth)
|
|
87
|
-
|
|
88
|
-
@field_validator("stac", mode="before")
|
|
89
|
-
@classmethod
|
|
90
|
-
def _default_stac(cls, value: URL | None) -> URL:
|
|
91
|
-
return value or URL(**DEFAULT_STAC)
|
|
92
|
-
|
|
93
|
-
@field_validator("datacosmos_cloud_storage", mode="before")
|
|
94
|
-
@classmethod
|
|
95
|
-
def _default_cloud_storage(cls, value: URL | None) -> URL:
|
|
96
|
-
return value or URL(**DEFAULT_STORAGE)
|
|
97
|
-
|
|
98
|
-
@field_validator("datacosmos_public_cloud_storage", mode="before")
|
|
99
|
-
@classmethod
|
|
100
|
-
def _default_public_cloud_storage(cls, value: URL | None) -> URL:
|
|
101
|
-
return value or URL(**DEFAULT_STORAGE)
|