datacosmos 0.0.17__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.17 → datacosmos-0.0.27}/PKG-INFO +2 -1
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/local_authenticator.py +4 -4
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/m2m_authenticator.py +7 -5
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/auth/factory.py +27 -40
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/config.py +13 -26
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/datacosmos_client.py +16 -14
- datacosmos-0.0.27/datacosmos/exceptions/__init__.py +15 -0
- datacosmos-0.0.27/datacosmos/exceptions/authentication_error.py +8 -0
- datacosmos-0.0.17/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.17 → 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.17 → datacosmos-0.0.27}/datacosmos/stac/item/item_client.py +39 -35
- {datacosmos-0.0.17 → 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.17 → 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.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/check_api_response.py +6 -6
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/PKG-INFO +2 -1
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/SOURCES.txt +7 -1
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/requires.txt +1 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/pyproject.toml +3 -2
- datacosmos-0.0.17/datacosmos/exceptions/__init__.py +0 -1
- datacosmos-0.0.17/datacosmos/stac/enums/processing_level.py +0 -16
- datacosmos-0.0.17/datacosmos/stac/item/models/datacosmos_item.py +0 -55
- datacosmos-0.0.17/datacosmos/stac/storage/dataclasses/upload_path.py +0 -42
- datacosmos-0.0.17/datacosmos/stac/storage/storage_base.py +0 -40
- datacosmos-0.0.17/datacosmos/stac/storage/storage_client.py +0 -33
- datacosmos-0.0.17/datacosmos/stac/storage/uploader.py +0 -127
- {datacosmos-0.0.17 → datacosmos-0.0.27}/LICENSE.md +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/README.md +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/base_authenticator.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/local_token_fetcher.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/token.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/auth/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/constants.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/loaders/yaml_source.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/m2m_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/no_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/url.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/models/collection_update.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/constants/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/product_type.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/season.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/asset.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/eo_band.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/raster_band.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/stac_client.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/storage/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/url.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/top_level.txt +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.27}/setup.cfg +0 -0
- {datacosmos-0.0.17 → 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
|
|
@@ -16,6 +16,7 @@ Requires-Dist: pystac==1.12.1
|
|
|
16
16
|
Requires-Dist: pyyaml==6.0.2
|
|
17
17
|
Requires-Dist: structlog==24.4.0
|
|
18
18
|
Requires-Dist: tenacity>=8.2.3
|
|
19
|
+
Requires-Dist: shapely>=1.8.0
|
|
19
20
|
Provides-Extra: dev
|
|
20
21
|
Requires-Dist: black==22.3.0; extra == "dev"
|
|
21
22
|
Requires-Dist: ruff==0.9.5; extra == "dev"
|
|
@@ -7,7 +7,7 @@ import requests
|
|
|
7
7
|
|
|
8
8
|
from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
|
|
9
9
|
from datacosmos.auth.local_token_fetcher import LocalTokenFetcher
|
|
10
|
-
from datacosmos.exceptions
|
|
10
|
+
from datacosmos.exceptions import AuthenticationError
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class LocalAuthenticator(BaseAuthenticator):
|
|
@@ -36,7 +36,7 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
36
36
|
token_file=Path(auth.cache_file).expanduser(),
|
|
37
37
|
)
|
|
38
38
|
except Exception as e:
|
|
39
|
-
raise
|
|
39
|
+
raise AuthenticationError(
|
|
40
40
|
f"Failed to initialize LocalTokenFetcher: {e}"
|
|
41
41
|
) from e
|
|
42
42
|
|
|
@@ -52,7 +52,7 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
52
52
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
53
53
|
)
|
|
54
54
|
except Exception as e:
|
|
55
|
-
raise
|
|
55
|
+
raise AuthenticationError(f"Local authentication failed: {e}") from e
|
|
56
56
|
|
|
57
57
|
def refresh_token(self) -> AuthResult:
|
|
58
58
|
"""Refreshes the local token non-interactively."""
|
|
@@ -69,4 +69,4 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
69
69
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
70
70
|
)
|
|
71
71
|
except Exception as e:
|
|
72
|
-
raise
|
|
72
|
+
raise AuthenticationError(f"Local token refresh failed: {e}") from e
|
|
@@ -3,7 +3,9 @@ from datetime import datetime, timedelta, timezone
|
|
|
3
3
|
|
|
4
4
|
import requests
|
|
5
5
|
from oauthlib.oauth2 import BackendApplicationClient
|
|
6
|
-
from requests.exceptions import ConnectionError
|
|
6
|
+
from requests.exceptions import ConnectionError
|
|
7
|
+
from requests.exceptions import HTTPError as RequestsHTTPError
|
|
8
|
+
from requests.exceptions import RequestException, Timeout
|
|
7
9
|
from requests_oauthlib import OAuth2Session
|
|
8
10
|
from tenacity import (
|
|
9
11
|
retry,
|
|
@@ -13,7 +15,7 @@ from tenacity import (
|
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
|
|
16
|
-
from datacosmos.exceptions
|
|
18
|
+
from datacosmos.exceptions import AuthenticationError
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class M2MAuthenticator(BaseAuthenticator):
|
|
@@ -51,10 +53,10 @@ class M2MAuthenticator(BaseAuthenticator):
|
|
|
51
53
|
return AuthResult(
|
|
52
54
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
53
55
|
)
|
|
54
|
-
except (
|
|
55
|
-
raise
|
|
56
|
+
except (RequestsHTTPError, ConnectionError, Timeout) as e:
|
|
57
|
+
raise AuthenticationError(f"M2M authentication failed: {e}") from e
|
|
56
58
|
except RequestException as e:
|
|
57
|
-
raise
|
|
59
|
+
raise AuthenticationError(
|
|
58
60
|
f"Unexpected request failure during M2M authentication: {e}"
|
|
59
61
|
) from e
|
|
60
62
|
|
|
@@ -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)
|
|
@@ -7,7 +7,9 @@ from datetime import datetime, timedelta, timezone
|
|
|
7
7
|
from typing import Any, Callable, List, Optional
|
|
8
8
|
|
|
9
9
|
import requests
|
|
10
|
-
from requests.exceptions import ConnectionError
|
|
10
|
+
from requests.exceptions import ConnectionError
|
|
11
|
+
from requests.exceptions import HTTPError as RequestsHTTPError
|
|
12
|
+
from requests.exceptions import RequestException, Timeout
|
|
11
13
|
from requests_oauthlib import OAuth2Session
|
|
12
14
|
from tenacity import (
|
|
13
15
|
retry,
|
|
@@ -20,7 +22,7 @@ from datacosmos.auth.base_authenticator import BaseAuthenticator
|
|
|
20
22
|
from datacosmos.auth.local_authenticator import LocalAuthenticator
|
|
21
23
|
from datacosmos.auth.m2m_authenticator import M2MAuthenticator
|
|
22
24
|
from datacosmos.config.config import Config
|
|
23
|
-
from datacosmos.exceptions
|
|
25
|
+
from datacosmos.exceptions import AuthenticationError, DatacosmosError, HTTPError
|
|
24
26
|
|
|
25
27
|
_log = logging.getLogger(__name__)
|
|
26
28
|
|
|
@@ -75,7 +77,7 @@ class DatacosmosClient:
|
|
|
75
77
|
try:
|
|
76
78
|
return Config.model_validate(cfg)
|
|
77
79
|
except Exception as e:
|
|
78
|
-
raise
|
|
80
|
+
raise AuthenticationError(
|
|
79
81
|
"Invalid config provided to DatacosmosClient"
|
|
80
82
|
) from e
|
|
81
83
|
|
|
@@ -88,7 +90,7 @@ class DatacosmosClient:
|
|
|
88
90
|
token_data = self._extract_token_data(http_session)
|
|
89
91
|
self.token = token_data.get("access_token")
|
|
90
92
|
if not self.token:
|
|
91
|
-
raise
|
|
93
|
+
raise AuthenticationError(
|
|
92
94
|
"Failed to extract access token from injected session"
|
|
93
95
|
)
|
|
94
96
|
self.token_expiry = self._compute_expiry(
|
|
@@ -103,11 +105,11 @@ class DatacosmosClient:
|
|
|
103
105
|
if isinstance(http_session, requests.Session):
|
|
104
106
|
auth_header = http_session.headers.get("Authorization", "")
|
|
105
107
|
if not auth_header.startswith("Bearer "):
|
|
106
|
-
raise
|
|
108
|
+
raise AuthenticationError(
|
|
107
109
|
"Injected requests.Session must include a 'Bearer' token in its headers"
|
|
108
110
|
)
|
|
109
111
|
return {"access_token": auth_header.split(" ", 1)[1]}
|
|
110
|
-
raise
|
|
112
|
+
raise AuthenticationError(f"Unsupported session type: {type(http_session)}")
|
|
111
113
|
|
|
112
114
|
def _compute_expiry(
|
|
113
115
|
self,
|
|
@@ -134,7 +136,7 @@ class DatacosmosClient:
|
|
|
134
136
|
elif auth_type == "local":
|
|
135
137
|
self._authenticator = LocalAuthenticator(self.config)
|
|
136
138
|
else:
|
|
137
|
-
raise
|
|
139
|
+
raise AuthenticationError(f"Unsupported authentication type: {auth_type}")
|
|
138
140
|
|
|
139
141
|
auth_result = self._authenticator.authenticate_and_build_session()
|
|
140
142
|
self.token = auth_result.token
|
|
@@ -166,7 +168,7 @@ class DatacosmosClient:
|
|
|
166
168
|
{"Authorization": f"Bearer {self.token}"}
|
|
167
169
|
)
|
|
168
170
|
else:
|
|
169
|
-
raise
|
|
171
|
+
raise AuthenticationError(
|
|
170
172
|
"Cannot refresh token, no authenticator initialized."
|
|
171
173
|
)
|
|
172
174
|
|
|
@@ -196,7 +198,7 @@ class DatacosmosClient:
|
|
|
196
198
|
requests.Response: The HTTP response.
|
|
197
199
|
|
|
198
200
|
Raises:
|
|
199
|
-
|
|
201
|
+
DatacosmosError: For any HTTP or request-related errors.
|
|
200
202
|
"""
|
|
201
203
|
self._refresh_token_if_needed()
|
|
202
204
|
|
|
@@ -219,7 +221,7 @@ class DatacosmosClient:
|
|
|
219
221
|
_log.error("Response hook failed.", exc_info=True)
|
|
220
222
|
|
|
221
223
|
return response
|
|
222
|
-
except
|
|
224
|
+
except RequestsHTTPError as e:
|
|
223
225
|
status = getattr(e.response, "status_code", None)
|
|
224
226
|
if status in (401, 403) and getattr(self, "_owns_session", False):
|
|
225
227
|
self._refresh_now()
|
|
@@ -227,17 +229,17 @@ class DatacosmosClient:
|
|
|
227
229
|
try:
|
|
228
230
|
retry_response.raise_for_status()
|
|
229
231
|
return retry_response
|
|
230
|
-
except
|
|
231
|
-
raise
|
|
232
|
+
except RequestsHTTPError as e:
|
|
233
|
+
raise HTTPError(
|
|
232
234
|
f"HTTP error during {method.upper()} request to {url} after refresh",
|
|
233
235
|
response=e.response,
|
|
234
236
|
) from e
|
|
235
|
-
raise
|
|
237
|
+
raise HTTPError(
|
|
236
238
|
f"HTTP error during {method.upper()} request to {url}",
|
|
237
239
|
response=getattr(e, "response", None),
|
|
238
240
|
) from e
|
|
239
241
|
except RequestException as e:
|
|
240
|
-
raise
|
|
242
|
+
raise DatacosmosError(
|
|
241
243
|
f"Unexpected request failure during {method.upper()} request to {url}: {e}"
|
|
242
244
|
) from e
|
|
243
245
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Exceptions for the datacosmos package."""
|
|
2
|
+
|
|
3
|
+
from .authentication_error import AuthenticationError
|
|
4
|
+
from .datacosmos_error import DatacosmosError
|
|
5
|
+
from .http_error import HTTPError
|
|
6
|
+
from .stac_validation_error import StacValidationError
|
|
7
|
+
from .upload_error import UploadError
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DatacosmosError",
|
|
11
|
+
"StacValidationError",
|
|
12
|
+
"AuthenticationError",
|
|
13
|
+
"HTTPError",
|
|
14
|
+
"UploadError",
|
|
15
|
+
]
|
|
@@ -6,11 +6,11 @@ from requests import Response
|
|
|
6
6
|
from requests.exceptions import RequestException
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
class
|
|
9
|
+
class DatacosmosError(RequestException):
|
|
10
10
|
"""Base exception class for all Datacosmos SDK exceptions."""
|
|
11
11
|
|
|
12
12
|
def __init__(self, message: str, response: Optional[Response] = None):
|
|
13
|
-
"""Initialize
|
|
13
|
+
"""Initialize DatacosmosError.
|
|
14
14
|
|
|
15
15
|
Args:
|
|
16
16
|
message (str): The error message.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Exception raised during asset upload operations."""
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UploadError(DatacosmosError):
|
|
8
|
+
"""Exception raised during asset upload operations, including asset context."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, asset_key: Optional[str] = None, **kwargs: Any):
|
|
11
|
+
"""Initialize UploadError.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
message (str): The error message.
|
|
15
|
+
asset_key (Optional[str]): The key of the asset that caused the failure.
|
|
16
|
+
"""
|
|
17
|
+
self.asset_key = asset_key
|
|
18
|
+
|
|
19
|
+
# Modify message to include context if available
|
|
20
|
+
if asset_key:
|
|
21
|
+
message = f"[{asset_key}] {message}"
|
|
22
|
+
|
|
23
|
+
super().__init__(message, **kwargs)
|
|
@@ -6,7 +6,7 @@ from pystac import Collection, Extent, SpatialExtent, TemporalExtent
|
|
|
6
6
|
from pystac.utils import str_to_datetime
|
|
7
7
|
|
|
8
8
|
from datacosmos.datacosmos_client import DatacosmosClient
|
|
9
|
-
from datacosmos.exceptions
|
|
9
|
+
from datacosmos.exceptions import HTTPError
|
|
10
10
|
from datacosmos.stac.collection.models.collection_update import CollectionUpdate
|
|
11
11
|
from datacosmos.utils.http_response.check_api_response import check_api_response
|
|
12
12
|
|
|
@@ -98,7 +98,10 @@ class CollectionClient:
|
|
|
98
98
|
data = response.json()
|
|
99
99
|
|
|
100
100
|
if isinstance(data, list):
|
|
101
|
-
|
|
101
|
+
raise HTTPError(
|
|
102
|
+
f"Unexpected API response format during collection fetch. Expected dictionary, got list: {data}",
|
|
103
|
+
response=response,
|
|
104
|
+
)
|
|
102
105
|
|
|
103
106
|
return data
|
|
104
107
|
|
|
@@ -147,7 +150,4 @@ class CollectionClient:
|
|
|
147
150
|
try:
|
|
148
151
|
return next_href.split("?")[1].split("=")[-1]
|
|
149
152
|
except (IndexError, AttributeError) as e:
|
|
150
|
-
raise
|
|
151
|
-
f"Failed to parse pagination token from {next_href}",
|
|
152
|
-
response=e.response,
|
|
153
|
-
) from e
|
|
153
|
+
raise HTTPError(f"Failed to parse pagination token from {next_href}") from e
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Level enum class."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CaseInsensitiveEnum(Enum):
|
|
7
|
+
"""An enum that can be initialized with case-insensitive strings."""
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def _missing_(cls, value: object):
|
|
11
|
+
if isinstance(value, str):
|
|
12
|
+
for member in cls:
|
|
13
|
+
if member.value.lower() == value.lower():
|
|
14
|
+
return member
|
|
15
|
+
return super()._missing_(value)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ProcessingLevel(CaseInsensitiveEnum):
|
|
19
|
+
"""Enum class for the processing levels of the data."""
|
|
20
|
+
|
|
21
|
+
RAW = "RAW"
|
|
22
|
+
L0 = "l0"
|
|
23
|
+
L1A = "l1A"
|
|
24
|
+
L2A = "l2A"
|
|
25
|
+
L1B = "l1B"
|
|
26
|
+
L1C = "l1C"
|
|
27
|
+
L1D = "l1D"
|
|
28
|
+
L3 = "l3"
|
|
@@ -8,7 +8,7 @@ from typing import Generator, Optional
|
|
|
8
8
|
from pystac import Item
|
|
9
9
|
|
|
10
10
|
from datacosmos.datacosmos_client import DatacosmosClient
|
|
11
|
-
from datacosmos.exceptions.
|
|
11
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
12
12
|
from datacosmos.stac.item.models.catalog_search_parameters import (
|
|
13
13
|
CatalogSearchParameters,
|
|
14
14
|
)
|
|
@@ -29,7 +29,7 @@ class ItemClient:
|
|
|
29
29
|
self.client = client
|
|
30
30
|
self.base_url = client.config.stac.as_domain_url()
|
|
31
31
|
|
|
32
|
-
def fetch_item(self, item_id: str, collection_id: str) ->
|
|
32
|
+
def fetch_item(self, item_id: str, collection_id: str) -> DatacosmosItem:
|
|
33
33
|
"""Fetch a single STAC item by ID.
|
|
34
34
|
|
|
35
35
|
Args:
|
|
@@ -37,12 +37,12 @@ class ItemClient:
|
|
|
37
37
|
collection_id (str): The ID of the collection containing the item.
|
|
38
38
|
|
|
39
39
|
Returns:
|
|
40
|
-
|
|
40
|
+
DatacosmosItem: The fetched STAC item.
|
|
41
41
|
"""
|
|
42
42
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}")
|
|
43
43
|
response = self.client.get(url)
|
|
44
44
|
check_api_response(response)
|
|
45
|
-
return
|
|
45
|
+
return DatacosmosItem.from_dict(response.json())
|
|
46
46
|
|
|
47
47
|
def search_items(
|
|
48
48
|
self, parameters: CatalogSearchParameters, project_id: str
|
|
@@ -74,15 +74,7 @@ class ItemClient:
|
|
|
74
74
|
ValueError: If the item has no collection set.
|
|
75
75
|
RequestError: If the API returns an error response.
|
|
76
76
|
"""
|
|
77
|
-
|
|
78
|
-
collection_id = item.collection_id or (
|
|
79
|
-
item.get_collection().id if item.get_collection() else None
|
|
80
|
-
)
|
|
81
|
-
else:
|
|
82
|
-
collection_id = item.collection
|
|
83
|
-
|
|
84
|
-
if not collection_id:
|
|
85
|
-
raise ValueError("Cannot create item: no collection_id found on item")
|
|
77
|
+
collection_id = self._get_collection_id(item, method="create")
|
|
86
78
|
|
|
87
79
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items")
|
|
88
80
|
item_json: dict = item.to_dict()
|
|
@@ -101,15 +93,10 @@ class ItemClient:
|
|
|
101
93
|
ValueError: If the item has no collection set.
|
|
102
94
|
RequestError: If the API returns an error response.
|
|
103
95
|
"""
|
|
104
|
-
|
|
105
|
-
collection_id = item.collection_id or (
|
|
106
|
-
item.get_collection().id if item.get_collection() else None
|
|
107
|
-
)
|
|
108
|
-
else:
|
|
109
|
-
collection_id = item.collection
|
|
96
|
+
collection_id = self._get_collection_id(item, method="add")
|
|
110
97
|
|
|
111
|
-
if not
|
|
112
|
-
raise ValueError("Cannot
|
|
98
|
+
if not item.id:
|
|
99
|
+
raise ValueError("Cannot add item: no item_id found on item")
|
|
113
100
|
|
|
114
101
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item.id}")
|
|
115
102
|
item_json: dict = item.to_dict()
|
|
@@ -150,24 +137,14 @@ class ItemClient:
|
|
|
150
137
|
collection_id (str): The ID of the collection containing the item.
|
|
151
138
|
|
|
152
139
|
Raises:
|
|
153
|
-
|
|
140
|
+
DatacosmosError: If the item is not found or deletion is forbidden.
|
|
154
141
|
"""
|
|
155
142
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item_id}")
|
|
156
143
|
response = self.client.delete(url)
|
|
157
144
|
check_api_response(response)
|
|
158
145
|
|
|
159
146
|
def _paginate_items(self, url: str, body: dict) -> Generator[Item, None, None]:
|
|
160
|
-
"""Handle pagination for the STAC search POST endpoint.
|
|
161
|
-
|
|
162
|
-
Fetches items one page at a time using the 'next' link.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
url (str): The base URL for the search endpoint.
|
|
166
|
-
body (dict): The request body containing search parameters.
|
|
167
|
-
|
|
168
|
-
Yields:
|
|
169
|
-
Item: Parsed STAC item.
|
|
170
|
-
"""
|
|
147
|
+
"""Handle pagination for the STAC search POST endpoint."""
|
|
171
148
|
params = {"limit": body.get("limit", 10)}
|
|
172
149
|
|
|
173
150
|
while True:
|
|
@@ -203,12 +180,39 @@ class ItemClient:
|
|
|
203
180
|
Optional[str]: The extracted token, or None if parsing fails.
|
|
204
181
|
|
|
205
182
|
Raises:
|
|
206
|
-
|
|
183
|
+
DatacosmosError: If pagination token extraction fails.
|
|
207
184
|
"""
|
|
208
185
|
try:
|
|
209
186
|
return next_href.split("?")[1].split("=")[-1]
|
|
210
187
|
except (IndexError, AttributeError) as e:
|
|
211
|
-
raise
|
|
188
|
+
raise DatacosmosError(
|
|
212
189
|
f"Failed to parse pagination token from {next_href}",
|
|
213
190
|
response=e.response,
|
|
214
191
|
) from e
|
|
192
|
+
|
|
193
|
+
def _get_collection_id(self, item: Item | DatacosmosItem, method: str) -> str:
|
|
194
|
+
"""Resolves the collection ID from an item.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
item: The STAC item.
|
|
198
|
+
method: The client method calling this helper ("create" or "add").
|
|
199
|
+
is_strict: Check if strict validation is to be done.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
The collection_id.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If collection ID cannot be resolved.
|
|
206
|
+
"""
|
|
207
|
+
if isinstance(item, Item):
|
|
208
|
+
collection_id = item.collection_id
|
|
209
|
+
else:
|
|
210
|
+
collection_id = item.collection
|
|
211
|
+
|
|
212
|
+
if not collection_id:
|
|
213
|
+
if method == "create":
|
|
214
|
+
raise ValueError("Cannot create item: no collection_id found on item")
|
|
215
|
+
else:
|
|
216
|
+
raise ValueError("Cannot add item: no collection_id found on item")
|
|
217
|
+
|
|
218
|
+
return collection_id
|