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.
Files changed (80) hide show
  1. {datacosmos-0.0.17 → datacosmos-0.0.27}/PKG-INFO +2 -1
  2. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/local_authenticator.py +4 -4
  3. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/m2m_authenticator.py +7 -5
  4. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/auth/factory.py +27 -40
  5. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/config.py +13 -26
  6. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/datacosmos_client.py +16 -14
  7. datacosmos-0.0.27/datacosmos/exceptions/__init__.py +15 -0
  8. datacosmos-0.0.27/datacosmos/exceptions/authentication_error.py +8 -0
  9. datacosmos-0.0.17/datacosmos/exceptions/datacosmos_exception.py → datacosmos-0.0.27/datacosmos/exceptions/datacosmos_error.py +2 -2
  10. datacosmos-0.0.27/datacosmos/exceptions/http_error.py +8 -0
  11. datacosmos-0.0.27/datacosmos/exceptions/stac_validation_error.py +8 -0
  12. datacosmos-0.0.27/datacosmos/exceptions/upload_error.py +23 -0
  13. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/collection_client.py +6 -6
  14. datacosmos-0.0.27/datacosmos/stac/enums/processing_level.py +28 -0
  15. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/item_client.py +39 -35
  16. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/catalog_search_parameters.py +6 -5
  17. datacosmos-0.0.27/datacosmos/stac/item/models/datacosmos_item.py +182 -0
  18. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/item_update.py +4 -4
  19. datacosmos-0.0.27/datacosmos/stac/storage/dataclasses/upload_path.py +78 -0
  20. datacosmos-0.0.27/datacosmos/stac/storage/dataclasses/upload_result.py +20 -0
  21. datacosmos-0.0.27/datacosmos/stac/storage/downloader.py +119 -0
  22. datacosmos-0.0.27/datacosmos/stac/storage/storage_base.py +86 -0
  23. datacosmos-0.0.27/datacosmos/stac/storage/storage_client.py +64 -0
  24. datacosmos-0.0.27/datacosmos/stac/storage/uploader.py +236 -0
  25. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/check_api_response.py +6 -6
  26. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/PKG-INFO +2 -1
  27. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/SOURCES.txt +7 -1
  28. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/requires.txt +1 -0
  29. {datacosmos-0.0.17 → datacosmos-0.0.27}/pyproject.toml +3 -2
  30. datacosmos-0.0.17/datacosmos/exceptions/__init__.py +0 -1
  31. datacosmos-0.0.17/datacosmos/stac/enums/processing_level.py +0 -16
  32. datacosmos-0.0.17/datacosmos/stac/item/models/datacosmos_item.py +0 -55
  33. datacosmos-0.0.17/datacosmos/stac/storage/dataclasses/upload_path.py +0 -42
  34. datacosmos-0.0.17/datacosmos/stac/storage/storage_base.py +0 -40
  35. datacosmos-0.0.17/datacosmos/stac/storage/storage_client.py +0 -33
  36. datacosmos-0.0.17/datacosmos/stac/storage/uploader.py +0 -127
  37. {datacosmos-0.0.17 → datacosmos-0.0.27}/LICENSE.md +0 -0
  38. {datacosmos-0.0.17 → datacosmos-0.0.27}/README.md +0 -0
  39. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/__init__.py +0 -0
  40. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/__init__.py +0 -0
  41. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/base_authenticator.py +0 -0
  42. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/local_token_fetcher.py +0 -0
  43. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/auth/token.py +0 -0
  44. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/__init__.py +0 -0
  45. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/auth/__init__.py +0 -0
  46. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/constants.py +0 -0
  47. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/loaders/yaml_source.py +0 -0
  48. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/__init__.py +0 -0
  49. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/authentication_config.py +0 -0
  50. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
  51. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/m2m_authentication_config.py +0 -0
  52. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/no_authentication_config.py +0 -0
  53. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/config/models/url.py +0 -0
  54. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/__init__.py +0 -0
  55. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/__init__.py +0 -0
  56. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/models/__init__.py +0 -0
  57. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/collection/models/collection_update.py +0 -0
  58. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/constants/__init__.py +0 -0
  59. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
  60. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/__init__.py +0 -0
  61. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/product_type.py +0 -0
  62. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/enums/season.py +0 -0
  63. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/__init__.py +0 -0
  64. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/__init__.py +0 -0
  65. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/asset.py +0 -0
  66. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/eo_band.py +0 -0
  67. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/item/models/raster_band.py +0 -0
  68. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/stac_client.py +0 -0
  69. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/storage/__init__.py +0 -0
  70. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
  71. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/__init__.py +0 -0
  72. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/__init__.py +0 -0
  73. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/__init__.py +0 -0
  74. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  75. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  76. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos/utils/url.py +0 -0
  77. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/dependency_links.txt +0 -0
  78. {datacosmos-0.0.17 → datacosmos-0.0.27}/datacosmos.egg-info/top_level.txt +0 -0
  79. {datacosmos-0.0.17 → datacosmos-0.0.27}/setup.cfg +0 -0
  80. {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.17
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(
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 DatacosmosException(f"Local authentication failed: {e}") from e
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 DatacosmosException(f"Local token refresh failed: {e}") from e
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, HTTPError, RequestException, Timeout
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.datacosmos_exception import DatacosmosException
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 (HTTPError, ConnectionError, Timeout) as e:
55
- raise DatacosmosException(f"M2M authentication failed: {e}") from e
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 DatacosmosException(
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
- auth_type = _normalize_auth_type(raw.get("type") or DEFAULT_AUTH_TYPE)
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=raw.get("client_id"),
54
- authorization_endpoint=raw.get(
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=raw.get("token_endpoint", DEFAULT_LOCAL_TOKEN_ENDPOINT),
58
- redirect_port=raw.get("redirect_port", DEFAULT_LOCAL_REDIRECT_PORT),
59
- scopes=raw.get("scopes", DEFAULT_LOCAL_SCOPES),
60
- audience=raw.get("audience", DEFAULT_AUTH_AUDIENCE),
61
- cache_file=raw.get("cache_file", DEFAULT_LOCAL_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=raw.get("token_url", DEFAULT_AUTH_TOKEN_URL),
67
- audience=raw.get("audience", DEFAULT_AUTH_AUDIENCE),
68
- client_id=raw.get("client_id"),
69
- client_secret=raw.get("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 ValueError(
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 ValueError(
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
- stac: URL | None = None
35
- datacosmos_cloud_storage: URL | None = None
36
- datacosmos_public_cloud_storage: URL | None = None
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, HTTPError, RequestException, Timeout
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(f"Unsupported session type: {type(http_session)}")
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 DatacosmosException(f"Unsupported authentication type: {auth_type}")
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 DatacosmosException(
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
- DatacosmosException: For any HTTP or request-related errors.
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 HTTPError as e:
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 HTTPError as e:
231
- raise DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(
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
+ ]
@@ -0,0 +1,8 @@
1
+ """Exception raised for authentication failures."""
2
+ from datacosmos.exceptions.datacosmos_error import DatacosmosError
3
+
4
+
5
+ class AuthenticationError(DatacosmosError):
6
+ """Exception raised for authentication failures."""
7
+
8
+ pass
@@ -6,11 +6,11 @@ from requests import Response
6
6
  from requests.exceptions import RequestException
7
7
 
8
8
 
9
- class DatacosmosException(RequestException):
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 DatacosmosException.
13
+ """Initialize DatacosmosError.
14
14
 
15
15
  Args:
16
16
  message (str): The error message.
@@ -0,0 +1,8 @@
1
+ """Exception raised for HTTP request failures."""
2
+ from datacosmos.exceptions.datacosmos_error import DatacosmosError
3
+
4
+
5
+ class HTTPError(DatacosmosError):
6
+ """Exception raised for HTTP request failures that wraps status codes."""
7
+
8
+ pass
@@ -0,0 +1,8 @@
1
+ """Custom exception for STAC validation errors."""
2
+ from datacosmos.exceptions.datacosmos_error import DatacosmosError
3
+
4
+
5
+ class StacValidationError(DatacosmosError):
6
+ """Exception raised for errors in STAC item validation."""
7
+
8
+ pass
@@ -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.datacosmos_exception import DatacosmosException
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
- return {"collections": data}
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 DatacosmosException(
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.datacosmos_exception import DatacosmosException
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) -> Item:
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
- Item: The fetched STAC item.
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 Item.from_dict(response.json())
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
- if isinstance(item, Item):
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
- if isinstance(item, Item):
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 collection_id:
112
- raise ValueError("Cannot create item: no collection_id found on item")
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
- OCError: If the item is not found or deletion is forbidden.
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
- DatacosmosException: If pagination token extraction fails.
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 DatacosmosException(
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