datacosmos 0.0.17__tar.gz → 0.0.19__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.17 → datacosmos-0.0.19}/PKG-INFO +2 -1
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/local_authenticator.py +4 -6
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/m2m_authenticator.py +3 -3
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/datacosmos_client.py +11 -13
- datacosmos-0.0.19/datacosmos/exceptions/__init__.py +9 -0
- datacosmos-0.0.17/datacosmos/exceptions/datacosmos_exception.py → datacosmos-0.0.19/datacosmos/exceptions/datacosmos_error.py +2 -2
- datacosmos-0.0.19/datacosmos/exceptions/stac_validation_error.py +8 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/collection_client.py +2 -2
- datacosmos-0.0.19/datacosmos/stac/enums/processing_level.py +28 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/item_client.py +76 -20
- datacosmos-0.0.19/datacosmos/stac/item/models/datacosmos_item.py +147 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/check_api_response.py +6 -6
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/PKG-INFO +2 -1
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/SOURCES.txt +2 -1
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/requires.txt +1 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/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-0.0.19}/LICENSE.md +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/README.md +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/base_authenticator.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/local_token_fetcher.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/token.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/auth/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/auth/factory.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/constants.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/loaders/yaml_source.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/m2m_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/no_authentication_config.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/url.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/models/collection_update.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/constants/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/product_type.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/season.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/asset.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/eo_band.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/item_update.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/raster_band.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/stac_client.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/storage_base.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/storage_client.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/uploader.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/__init__.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/url.py +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/top_level.txt +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/setup.cfg +0 -0
- {datacosmos-0.0.17 → datacosmos-0.0.19}/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.19
|
|
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.datacosmos_error import DatacosmosError
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class LocalAuthenticator(BaseAuthenticator):
|
|
@@ -36,9 +36,7 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
36
36
|
token_file=Path(auth.cache_file).expanduser(),
|
|
37
37
|
)
|
|
38
38
|
except Exception as e:
|
|
39
|
-
raise
|
|
40
|
-
f"Failed to initialize LocalTokenFetcher: {e}"
|
|
41
|
-
) from e
|
|
39
|
+
raise DatacosmosError(f"Failed to initialize LocalTokenFetcher: {e}") from e
|
|
42
40
|
|
|
43
41
|
def authenticate_and_build_session(self) -> AuthResult:
|
|
44
42
|
"""Builds an authenticated session using the local token fetcher."""
|
|
@@ -52,7 +50,7 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
52
50
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
53
51
|
)
|
|
54
52
|
except Exception as e:
|
|
55
|
-
raise
|
|
53
|
+
raise DatacosmosError(f"Local authentication failed: {e}") from e
|
|
56
54
|
|
|
57
55
|
def refresh_token(self) -> AuthResult:
|
|
58
56
|
"""Refreshes the local token non-interactively."""
|
|
@@ -69,4 +67,4 @@ class LocalAuthenticator(BaseAuthenticator):
|
|
|
69
67
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
70
68
|
)
|
|
71
69
|
except Exception as e:
|
|
72
|
-
raise
|
|
70
|
+
raise DatacosmosError(f"Local token refresh failed: {e}") from e
|
|
@@ -13,7 +13,7 @@ from tenacity import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
from datacosmos.auth.base_authenticator import AuthResult, BaseAuthenticator
|
|
16
|
-
from datacosmos.exceptions.
|
|
16
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class M2MAuthenticator(BaseAuthenticator):
|
|
@@ -52,9 +52,9 @@ class M2MAuthenticator(BaseAuthenticator):
|
|
|
52
52
|
http_client=http_client, token=token, token_expiry=token_expiry
|
|
53
53
|
)
|
|
54
54
|
except (HTTPError, ConnectionError, Timeout) as e:
|
|
55
|
-
raise
|
|
55
|
+
raise DatacosmosError(f"M2M authentication failed: {e}") from e
|
|
56
56
|
except RequestException as e:
|
|
57
|
-
raise
|
|
57
|
+
raise DatacosmosError(
|
|
58
58
|
f"Unexpected request failure during M2M authentication: {e}"
|
|
59
59
|
) from e
|
|
60
60
|
|
|
@@ -20,7 +20,7 @@ from datacosmos.auth.base_authenticator import BaseAuthenticator
|
|
|
20
20
|
from datacosmos.auth.local_authenticator import LocalAuthenticator
|
|
21
21
|
from datacosmos.auth.m2m_authenticator import M2MAuthenticator
|
|
22
22
|
from datacosmos.config.config import Config
|
|
23
|
-
from datacosmos.exceptions.
|
|
23
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
24
24
|
|
|
25
25
|
_log = logging.getLogger(__name__)
|
|
26
26
|
|
|
@@ -75,9 +75,7 @@ class DatacosmosClient:
|
|
|
75
75
|
try:
|
|
76
76
|
return Config.model_validate(cfg)
|
|
77
77
|
except Exception as e:
|
|
78
|
-
raise
|
|
79
|
-
"Invalid config provided to DatacosmosClient"
|
|
80
|
-
) from e
|
|
78
|
+
raise DatacosmosError("Invalid config provided to DatacosmosClient") from e
|
|
81
79
|
|
|
82
80
|
def _init_with_injected_session(
|
|
83
81
|
self, http_session: requests.Session | OAuth2Session
|
|
@@ -88,7 +86,7 @@ class DatacosmosClient:
|
|
|
88
86
|
token_data = self._extract_token_data(http_session)
|
|
89
87
|
self.token = token_data.get("access_token")
|
|
90
88
|
if not self.token:
|
|
91
|
-
raise
|
|
89
|
+
raise DatacosmosError(
|
|
92
90
|
"Failed to extract access token from injected session"
|
|
93
91
|
)
|
|
94
92
|
self.token_expiry = self._compute_expiry(
|
|
@@ -103,11 +101,11 @@ class DatacosmosClient:
|
|
|
103
101
|
if isinstance(http_session, requests.Session):
|
|
104
102
|
auth_header = http_session.headers.get("Authorization", "")
|
|
105
103
|
if not auth_header.startswith("Bearer "):
|
|
106
|
-
raise
|
|
104
|
+
raise DatacosmosError(
|
|
107
105
|
"Injected requests.Session must include a 'Bearer' token in its headers"
|
|
108
106
|
)
|
|
109
107
|
return {"access_token": auth_header.split(" ", 1)[1]}
|
|
110
|
-
raise
|
|
108
|
+
raise DatacosmosError(f"Unsupported session type: {type(http_session)}")
|
|
111
109
|
|
|
112
110
|
def _compute_expiry(
|
|
113
111
|
self,
|
|
@@ -134,7 +132,7 @@ class DatacosmosClient:
|
|
|
134
132
|
elif auth_type == "local":
|
|
135
133
|
self._authenticator = LocalAuthenticator(self.config)
|
|
136
134
|
else:
|
|
137
|
-
raise
|
|
135
|
+
raise DatacosmosError(f"Unsupported authentication type: {auth_type}")
|
|
138
136
|
|
|
139
137
|
auth_result = self._authenticator.authenticate_and_build_session()
|
|
140
138
|
self.token = auth_result.token
|
|
@@ -166,7 +164,7 @@ class DatacosmosClient:
|
|
|
166
164
|
{"Authorization": f"Bearer {self.token}"}
|
|
167
165
|
)
|
|
168
166
|
else:
|
|
169
|
-
raise
|
|
167
|
+
raise DatacosmosError(
|
|
170
168
|
"Cannot refresh token, no authenticator initialized."
|
|
171
169
|
)
|
|
172
170
|
|
|
@@ -196,7 +194,7 @@ class DatacosmosClient:
|
|
|
196
194
|
requests.Response: The HTTP response.
|
|
197
195
|
|
|
198
196
|
Raises:
|
|
199
|
-
|
|
197
|
+
DatacosmosError: For any HTTP or request-related errors.
|
|
200
198
|
"""
|
|
201
199
|
self._refresh_token_if_needed()
|
|
202
200
|
|
|
@@ -228,16 +226,16 @@ class DatacosmosClient:
|
|
|
228
226
|
retry_response.raise_for_status()
|
|
229
227
|
return retry_response
|
|
230
228
|
except HTTPError as e:
|
|
231
|
-
raise
|
|
229
|
+
raise DatacosmosError(
|
|
232
230
|
f"HTTP error during {method.upper()} request to {url} after refresh",
|
|
233
231
|
response=e.response,
|
|
234
232
|
) from e
|
|
235
|
-
raise
|
|
233
|
+
raise DatacosmosError(
|
|
236
234
|
f"HTTP error during {method.upper()} request to {url}",
|
|
237
235
|
response=getattr(e, "response", None),
|
|
238
236
|
) from e
|
|
239
237
|
except RequestException as e:
|
|
240
|
-
raise
|
|
238
|
+
raise DatacosmosError(
|
|
241
239
|
f"Unexpected request failure during {method.upper()} request to {url}: {e}"
|
|
242
240
|
) from e
|
|
243
241
|
|
|
@@ -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.
|
|
@@ -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.datacosmos_error import DatacosmosError
|
|
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
|
|
|
@@ -147,7 +147,7 @@ class CollectionClient:
|
|
|
147
147
|
try:
|
|
148
148
|
return next_href.split("?")[1].split("=")[-1]
|
|
149
149
|
except (IndexError, AttributeError) as e:
|
|
150
|
-
raise
|
|
150
|
+
raise DatacosmosError(
|
|
151
151
|
f"Failed to parse pagination token from {next_href}",
|
|
152
152
|
response=e.response,
|
|
153
153
|
) 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 import DatacosmosError, StacValidationError
|
|
12
12
|
from datacosmos.stac.item.models.catalog_search_parameters import (
|
|
13
13
|
CatalogSearchParameters,
|
|
14
14
|
)
|
|
@@ -72,17 +72,10 @@ class ItemClient:
|
|
|
72
72
|
|
|
73
73
|
Raises:
|
|
74
74
|
ValueError: If the item has no collection set.
|
|
75
|
+
StacValidationError: If the collection ID in the links doesn't match the item's collection field.
|
|
75
76
|
RequestError: If the API returns an error response.
|
|
76
77
|
"""
|
|
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")
|
|
78
|
+
collection_id = self._get_validated_collection_id(item, method="create")
|
|
86
79
|
|
|
87
80
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items")
|
|
88
81
|
item_json: dict = item.to_dict()
|
|
@@ -99,17 +92,13 @@ class ItemClient:
|
|
|
99
92
|
|
|
100
93
|
Raises:
|
|
101
94
|
ValueError: If the item has no collection set.
|
|
95
|
+
StacValidationError: If the collection ID in the links doesn't match the item's collection field.
|
|
102
96
|
RequestError: If the API returns an error response.
|
|
103
97
|
"""
|
|
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
|
|
98
|
+
collection_id = self._get_validated_collection_id(item, method="add")
|
|
110
99
|
|
|
111
|
-
if not
|
|
112
|
-
raise ValueError("Cannot
|
|
100
|
+
if not item.id:
|
|
101
|
+
raise ValueError("Cannot add item: no item_id found on item")
|
|
113
102
|
|
|
114
103
|
url = self.base_url.with_suffix(f"/collections/{collection_id}/items/{item.id}")
|
|
115
104
|
item_json: dict = item.to_dict()
|
|
@@ -203,12 +192,79 @@ class ItemClient:
|
|
|
203
192
|
Optional[str]: The extracted token, or None if parsing fails.
|
|
204
193
|
|
|
205
194
|
Raises:
|
|
206
|
-
|
|
195
|
+
DatacosmosError: If pagination token extraction fails.
|
|
207
196
|
"""
|
|
208
197
|
try:
|
|
209
198
|
return next_href.split("?")[1].split("=")[-1]
|
|
210
199
|
except (IndexError, AttributeError) as e:
|
|
211
|
-
raise
|
|
200
|
+
raise DatacosmosError(
|
|
212
201
|
f"Failed to parse pagination token from {next_href}",
|
|
213
202
|
response=e.response,
|
|
214
203
|
) from e
|
|
204
|
+
|
|
205
|
+
def _get_validated_collection_id(
|
|
206
|
+
self, item: Item | DatacosmosItem, method: str
|
|
207
|
+
) -> str:
|
|
208
|
+
"""Resolves and validates the collection ID from an item, checking for link consistency.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
item: The STAC item.
|
|
212
|
+
method: The client method calling this helper ("create" or "add").
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The validated collection_id.
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
ValueError: If collection ID cannot be resolved.
|
|
219
|
+
StacValidationError: If the collection ID and parent link are inconsistent.
|
|
220
|
+
"""
|
|
221
|
+
if isinstance(item, Item):
|
|
222
|
+
collection_id = item.collection_id or (
|
|
223
|
+
item.get_collection().id if item.get_collection() else None
|
|
224
|
+
)
|
|
225
|
+
if collection_id and not self._is_collection_link_consistent_pystac(
|
|
226
|
+
item, collection_id
|
|
227
|
+
):
|
|
228
|
+
raise StacValidationError(
|
|
229
|
+
"Parent link in pystac.Item does not match its collection_id."
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
collection_id = item.collection
|
|
233
|
+
if collection_id and not self._is_collection_link_consistent_datacosmos(
|
|
234
|
+
item
|
|
235
|
+
):
|
|
236
|
+
raise StacValidationError(
|
|
237
|
+
"Parent link in DatacosmosItem does not match its collection."
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not collection_id:
|
|
241
|
+
if method == "create":
|
|
242
|
+
raise ValueError("Cannot create item: no collection_id found on item")
|
|
243
|
+
else:
|
|
244
|
+
raise ValueError("Cannot add item: no collection_id found on item")
|
|
245
|
+
|
|
246
|
+
return collection_id
|
|
247
|
+
|
|
248
|
+
def _is_collection_link_consistent_pystac(
|
|
249
|
+
self, item: Item, collection_id: str
|
|
250
|
+
) -> bool:
|
|
251
|
+
"""Helper to check if the parent link matches the pystac item's collection_id."""
|
|
252
|
+
parent_link = next(
|
|
253
|
+
(link for link in item.get_links("parent") if link.rel == "parent"), None
|
|
254
|
+
)
|
|
255
|
+
if not parent_link:
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
link_collection_id = parent_link.get_href().rstrip("/").split("/")[-1]
|
|
259
|
+
return link_collection_id == collection_id
|
|
260
|
+
|
|
261
|
+
def _is_collection_link_consistent_datacosmos(self, item: DatacosmosItem) -> bool:
|
|
262
|
+
"""Helper to check if the parent link matches the datacosmos item's collection field."""
|
|
263
|
+
if not item.collection:
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
for link in item.links:
|
|
267
|
+
if link.get("rel") == "parent":
|
|
268
|
+
link_collection_id = link.get("href", "").rstrip("/").split("/")[-1]
|
|
269
|
+
return link_collection_id == item.collection
|
|
270
|
+
return True
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Model representing a datacosmos item."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
8
|
+
from shapely.errors import ShapelyError
|
|
9
|
+
from shapely.geometry import Polygon, shape
|
|
10
|
+
|
|
11
|
+
from datacosmos.exceptions.stac_validation_error import StacValidationError
|
|
12
|
+
from datacosmos.stac.enums.processing_level import ProcessingLevel
|
|
13
|
+
from datacosmos.stac.item.models.asset import Asset
|
|
14
|
+
|
|
15
|
+
_REQUIRED_DATACOSMOS_PROPERTIES = [
|
|
16
|
+
"datetime",
|
|
17
|
+
"processing:level",
|
|
18
|
+
"sat:platform_international_designator",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DatacosmosItem(BaseModel):
|
|
23
|
+
"""Model representing a flexible Datacosmos STAC item with mandatory business fields."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="allow")
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
type: str
|
|
29
|
+
geometry: dict[str, Any]
|
|
30
|
+
bbox: list[float]
|
|
31
|
+
properties: dict[str, Any]
|
|
32
|
+
|
|
33
|
+
links: list[dict[str, Any]]
|
|
34
|
+
assets: dict[str, Asset]
|
|
35
|
+
|
|
36
|
+
stac_version: str | None = None
|
|
37
|
+
stac_extensions: list[str] | None = None
|
|
38
|
+
collection: str | None = None
|
|
39
|
+
|
|
40
|
+
@field_validator("properties", mode="before")
|
|
41
|
+
@classmethod
|
|
42
|
+
def validate_datacosmos_properties(
|
|
43
|
+
cls, properties_data: dict[str, Any]
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Validates that Datacosmos-specific properties exist."""
|
|
46
|
+
missing_keys = [
|
|
47
|
+
key for key in _REQUIRED_DATACOSMOS_PROPERTIES if key not in properties_data
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
if missing_keys:
|
|
51
|
+
raise StacValidationError(
|
|
52
|
+
f"Datacosmos-specific properties are missing: {', '.join(missing_keys)}."
|
|
53
|
+
)
|
|
54
|
+
return properties_data
|
|
55
|
+
|
|
56
|
+
@field_validator("geometry", mode="before")
|
|
57
|
+
@classmethod
|
|
58
|
+
def validate_geometry_is_polygon(
|
|
59
|
+
cls, geometry_data: dict[str, Any]
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""Validates that the geometry is a Polygon with coordinates and correct winding order."""
|
|
62
|
+
if geometry_data.get("type") != "Polygon" or not geometry_data.get(
|
|
63
|
+
"coordinates"
|
|
64
|
+
):
|
|
65
|
+
raise StacValidationError("Geometry must be a Polygon with coordinates.")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Use shape() for robust GeoJSON parsing and validation
|
|
69
|
+
polygon = shape(geometry_data)
|
|
70
|
+
|
|
71
|
+
if not polygon.is_valid:
|
|
72
|
+
raise ValueError(f"Polygon geometry is invalid: {polygon.geom_type}")
|
|
73
|
+
|
|
74
|
+
# right-hand rule validation:
|
|
75
|
+
# The right-hand rule means exterior ring must be counter-clockwise (CCW).
|
|
76
|
+
# Shapely's Polygon stores the exterior as CCW if the input is valid.
|
|
77
|
+
if not polygon.exterior.is_ccw:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Polygon winding order violates GeoJSON Right-Hand Rule (Exterior ring is clockwise)."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
except (KeyError, ShapelyError, ValueError) as e:
|
|
83
|
+
raise StacValidationError(f"Invalid geometry data: {e}") from e
|
|
84
|
+
|
|
85
|
+
return geometry_data
|
|
86
|
+
|
|
87
|
+
@model_validator(mode="after")
|
|
88
|
+
def validate_bbox_vs_geometry(self) -> "DatacosmosItem":
|
|
89
|
+
"""Validates that the bbox tightly encloses the geometry."""
|
|
90
|
+
if self.geometry and self.bbox:
|
|
91
|
+
try:
|
|
92
|
+
geom_shape = shape(self.geometry)
|
|
93
|
+
true_bbox = list(geom_shape.bounds)
|
|
94
|
+
|
|
95
|
+
# Check for floating point equality within a tolerance
|
|
96
|
+
if not all(
|
|
97
|
+
math.isclose(a, b, rel_tol=1e-9)
|
|
98
|
+
for a, b in zip(self.bbox, true_bbox)
|
|
99
|
+
):
|
|
100
|
+
raise StacValidationError(
|
|
101
|
+
"Provided bbox does not match geometry bounds."
|
|
102
|
+
)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
# Catch any errors from Shapely or the comparison
|
|
105
|
+
raise StacValidationError(f"Invalid bbox or geometry: {e}") from e
|
|
106
|
+
return self
|
|
107
|
+
|
|
108
|
+
def get_property(self, key: str) -> Any | None:
|
|
109
|
+
"""Get a property value from the Datacosmos item."""
|
|
110
|
+
return self.properties.get(key)
|
|
111
|
+
|
|
112
|
+
def get_asset(self, key: str) -> Asset | None:
|
|
113
|
+
"""Get an asset from the Datacosmos item."""
|
|
114
|
+
return self.assets.get(key)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def datetime(self) -> datetime:
|
|
118
|
+
"""Get the datetime of the Datacosmos item."""
|
|
119
|
+
return datetime.strptime(self.properties["datetime"], "%Y-%m-%dT%H:%M:%SZ")
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def level(self) -> ProcessingLevel:
|
|
123
|
+
"""Get the processing level of the Datacosmos item."""
|
|
124
|
+
return ProcessingLevel(self.properties["processing:level"].lower())
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def sat_int_designator(self) -> str:
|
|
128
|
+
"""Get the satellite international designator of the Datacosmos item."""
|
|
129
|
+
return self.properties["sat:platform_international_designator"]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def polygon(self) -> Polygon:
|
|
133
|
+
"""Returns the polygon of the item."""
|
|
134
|
+
coordinates = self.geometry["coordinates"][0]
|
|
135
|
+
return Polygon(coordinates)
|
|
136
|
+
|
|
137
|
+
def to_dict(self) -> dict:
|
|
138
|
+
"""Converts the DatacosmosItem instance to a dictionary."""
|
|
139
|
+
return self.model_dump()
|
|
140
|
+
|
|
141
|
+
def has_self_link(self) -> bool:
|
|
142
|
+
"""Checks if the item has a 'self' link."""
|
|
143
|
+
return any(link.get("rel") == "self" for link in self.links)
|
|
144
|
+
|
|
145
|
+
def has_parent_link(self) -> bool:
|
|
146
|
+
"""Checks if the item has a 'parent' link."""
|
|
147
|
+
return any(link.get("rel") == "parent" for link in self.links)
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/check_api_response.py
RENAMED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
"""Validates an API response and raises a
|
|
1
|
+
"""Validates an API response and raises a DatacosmosError if an error occurs."""
|
|
2
2
|
|
|
3
3
|
from pydantic import ValidationError
|
|
4
4
|
from requests import Response
|
|
5
5
|
|
|
6
|
-
from datacosmos.exceptions.
|
|
6
|
+
from datacosmos.exceptions.datacosmos_error import DatacosmosError
|
|
7
7
|
from datacosmos.utils.http_response.models.datacosmos_response import DatacosmosResponse
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def check_api_response(response: Response) -> None:
|
|
11
|
-
"""Validates an API response and raises a
|
|
11
|
+
"""Validates an API response and raises a DatacosmosError if an error occurs.
|
|
12
12
|
|
|
13
13
|
Args:
|
|
14
14
|
resp (requests.Response): The response object.
|
|
15
15
|
|
|
16
16
|
Raises:
|
|
17
|
-
|
|
17
|
+
DatacosmosError: If the response status code indicates an error.
|
|
18
18
|
"""
|
|
19
19
|
if 200 <= response.status_code < 400:
|
|
20
20
|
return
|
|
@@ -26,9 +26,9 @@ def check_api_response(response: Response) -> None:
|
|
|
26
26
|
msg = "\n * " + "\n * ".join(
|
|
27
27
|
error.human_readable() for error in response.errors
|
|
28
28
|
)
|
|
29
|
-
raise
|
|
29
|
+
raise DatacosmosError(msg, response=response)
|
|
30
30
|
|
|
31
31
|
except ValidationError:
|
|
32
|
-
raise
|
|
32
|
+
raise DatacosmosError(
|
|
33
33
|
f"HTTP {response.status_code}: {response.text}", response=response
|
|
34
34
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datacosmos
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.19
|
|
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"
|
|
@@ -27,7 +27,8 @@ datacosmos/config/models/m2m_authentication_config.py
|
|
|
27
27
|
datacosmos/config/models/no_authentication_config.py
|
|
28
28
|
datacosmos/config/models/url.py
|
|
29
29
|
datacosmos/exceptions/__init__.py
|
|
30
|
-
datacosmos/exceptions/
|
|
30
|
+
datacosmos/exceptions/datacosmos_error.py
|
|
31
|
+
datacosmos/exceptions/stac_validation_error.py
|
|
31
32
|
datacosmos/stac/__init__.py
|
|
32
33
|
datacosmos/stac/stac_client.py
|
|
33
34
|
datacosmos/stac/collection/__init__.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.19"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name="Open Cosmos", email="support@open-cosmos.com" },
|
|
10
10
|
]
|
|
@@ -23,7 +23,8 @@ dependencies = [
|
|
|
23
23
|
"pystac==1.12.1",
|
|
24
24
|
"pyyaml==6.0.2",
|
|
25
25
|
"structlog==24.4.0",
|
|
26
|
-
"tenacity>=8.2.3"
|
|
26
|
+
"tenacity>=8.2.3",
|
|
27
|
+
"shapely>=1.8.0"
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Exceptions for the datacosmos package."""
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"""Level enum class."""
|
|
2
|
-
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class ProcessingLevel(Enum):
|
|
7
|
-
"""Enum class for the processing levels of the data."""
|
|
8
|
-
|
|
9
|
-
RAW = "RAW"
|
|
10
|
-
L0 = "l0"
|
|
11
|
-
L1A = "l1A"
|
|
12
|
-
L2A = "l2A"
|
|
13
|
-
L1B = "l1B"
|
|
14
|
-
L1C = "l1C"
|
|
15
|
-
L1D = "l1D"
|
|
16
|
-
L3 = "l3"
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
"""Model representing a datacosmos item."""
|
|
2
|
-
|
|
3
|
-
from datetime import datetime
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
from datacosmos.stac.enums.processing_level import ProcessingLevel
|
|
8
|
-
from datacosmos.stac.item.models.asset import Asset
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class DatacosmosItem(BaseModel):
|
|
12
|
-
"""Model representing a datacosmos item."""
|
|
13
|
-
|
|
14
|
-
id: str
|
|
15
|
-
type: str
|
|
16
|
-
stac_version: str
|
|
17
|
-
stac_extensions: list | None
|
|
18
|
-
geometry: dict
|
|
19
|
-
properties: dict
|
|
20
|
-
links: list
|
|
21
|
-
assets: dict[str, Asset]
|
|
22
|
-
collection: str
|
|
23
|
-
bbox: tuple[float, float, float, float]
|
|
24
|
-
|
|
25
|
-
def get_property(self, key: str) -> str | None:
|
|
26
|
-
"""Get a property value from the Datacosmos item."""
|
|
27
|
-
return self.properties.get(key)
|
|
28
|
-
|
|
29
|
-
def get_asset(self, key: str) -> Asset | None:
|
|
30
|
-
"""Get an asset from the Datacosmos item."""
|
|
31
|
-
return self.assets.get(key)
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def datetime(self) -> datetime:
|
|
35
|
-
"""Get the datetime of the Datacosmos item."""
|
|
36
|
-
return datetime.strptime(self.properties["datetime"], "%Y-%m-%dT%H:%M:%SZ")
|
|
37
|
-
|
|
38
|
-
@property
|
|
39
|
-
def level(self) -> ProcessingLevel:
|
|
40
|
-
"""Get the processing level of the Datacosmos item."""
|
|
41
|
-
return ProcessingLevel(self.properties["processing:level"].lower())
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def sat_int_designator(self) -> str:
|
|
45
|
-
"""Get the satellite international designator of the Datacosmos item."""
|
|
46
|
-
property = self.get_property("sat:platform_international_designator")
|
|
47
|
-
if property is None:
|
|
48
|
-
raise ValueError(
|
|
49
|
-
"sat:platform_international_designator is missing in STAC item"
|
|
50
|
-
)
|
|
51
|
-
return property
|
|
52
|
-
|
|
53
|
-
def to_dict(self) -> dict:
|
|
54
|
-
"""Converts the DatacosmosItem instance to a dictionary."""
|
|
55
|
-
return self.model_dump()
|
|
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
|
|
File without changes
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/m2m_authentication_config.py
RENAMED
|
File without changes
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/no_authentication_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/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
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/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.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/datacosmos_error.py
RENAMED
|
File without changes
|
{datacosmos-0.0.17 → datacosmos-0.0.19}/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
|