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.

Files changed (71) hide show
  1. {datacosmos-0.0.17 → datacosmos-0.0.19}/PKG-INFO +2 -1
  2. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/local_authenticator.py +4 -6
  3. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/m2m_authenticator.py +3 -3
  4. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/datacosmos_client.py +11 -13
  5. datacosmos-0.0.19/datacosmos/exceptions/__init__.py +9 -0
  6. datacosmos-0.0.17/datacosmos/exceptions/datacosmos_exception.py → datacosmos-0.0.19/datacosmos/exceptions/datacosmos_error.py +2 -2
  7. datacosmos-0.0.19/datacosmos/exceptions/stac_validation_error.py +8 -0
  8. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/collection_client.py +2 -2
  9. datacosmos-0.0.19/datacosmos/stac/enums/processing_level.py +28 -0
  10. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/item_client.py +76 -20
  11. datacosmos-0.0.19/datacosmos/stac/item/models/datacosmos_item.py +147 -0
  12. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/check_api_response.py +6 -6
  13. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/PKG-INFO +2 -1
  14. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/SOURCES.txt +2 -1
  15. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/requires.txt +1 -0
  16. {datacosmos-0.0.17 → datacosmos-0.0.19}/pyproject.toml +3 -2
  17. datacosmos-0.0.17/datacosmos/exceptions/__init__.py +0 -1
  18. datacosmos-0.0.17/datacosmos/stac/enums/processing_level.py +0 -16
  19. datacosmos-0.0.17/datacosmos/stac/item/models/datacosmos_item.py +0 -55
  20. {datacosmos-0.0.17 → datacosmos-0.0.19}/LICENSE.md +0 -0
  21. {datacosmos-0.0.17 → datacosmos-0.0.19}/README.md +0 -0
  22. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/__init__.py +0 -0
  23. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/__init__.py +0 -0
  24. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/base_authenticator.py +0 -0
  25. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/local_token_fetcher.py +0 -0
  26. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/auth/token.py +0 -0
  27. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/__init__.py +0 -0
  28. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/auth/__init__.py +0 -0
  29. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/auth/factory.py +0 -0
  30. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/config.py +0 -0
  31. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/constants.py +0 -0
  32. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/loaders/yaml_source.py +0 -0
  33. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/__init__.py +0 -0
  34. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/authentication_config.py +0 -0
  35. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
  36. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/m2m_authentication_config.py +0 -0
  37. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/no_authentication_config.py +0 -0
  38. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/config/models/url.py +0 -0
  39. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/__init__.py +0 -0
  40. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/__init__.py +0 -0
  41. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/models/__init__.py +0 -0
  42. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/collection/models/collection_update.py +0 -0
  43. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/constants/__init__.py +0 -0
  44. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
  45. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/__init__.py +0 -0
  46. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/product_type.py +0 -0
  47. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/enums/season.py +0 -0
  48. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/__init__.py +0 -0
  49. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/__init__.py +0 -0
  50. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/asset.py +0 -0
  51. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
  52. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/eo_band.py +0 -0
  53. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/item_update.py +0 -0
  54. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/item/models/raster_band.py +0 -0
  55. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/stac_client.py +0 -0
  56. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/__init__.py +0 -0
  57. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
  58. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
  59. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/storage_base.py +0 -0
  60. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/storage_client.py +0 -0
  61. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/stac/storage/uploader.py +0 -0
  62. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/__init__.py +0 -0
  63. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/__init__.py +0 -0
  64. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/__init__.py +0 -0
  65. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  66. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  67. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos/utils/url.py +0 -0
  68. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/dependency_links.txt +0 -0
  69. {datacosmos-0.0.17 → datacosmos-0.0.19}/datacosmos.egg-info/top_level.txt +0 -0
  70. {datacosmos-0.0.17 → datacosmos-0.0.19}/setup.cfg +0 -0
  71. {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.17
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(
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 DatacosmosException(f"Local authentication failed: {e}") from e
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 DatacosmosException(f"Local token refresh failed: {e}") from e
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(f"M2M authentication failed: {e}") from e
55
+ raise DatacosmosError(f"M2M authentication failed: {e}") from e
56
56
  except RequestException as e:
57
- raise DatacosmosException(
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(f"Unsupported session type: {type(http_session)}")
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 DatacosmosException(f"Unsupported authentication type: {auth_type}")
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 DatacosmosException(
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
- DatacosmosException: For any HTTP or request-related errors.
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 DatacosmosException(
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 DatacosmosException(
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 DatacosmosException(
238
+ raise DatacosmosError(
241
239
  f"Unexpected request failure during {method.upper()} request to {url}: {e}"
242
240
  ) from e
243
241
 
@@ -0,0 +1,9 @@
1
+ """Exceptions for the datacosmos package."""
2
+
3
+ from .datacosmos_error import DatacosmosError
4
+ from .stac_validation_error import StacValidationError
5
+
6
+ __all__ = [
7
+ "DatacosmosError",
8
+ "StacValidationError",
9
+ ]
@@ -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
+ """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
@@ -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.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 DatacosmosException(
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.datacosmos_exception import DatacosmosException
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
- 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")
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
- 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
98
+ collection_id = self._get_validated_collection_id(item, method="add")
110
99
 
111
- if not collection_id:
112
- raise ValueError("Cannot create item: no collection_id found on item")
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
- DatacosmosException: If pagination token extraction fails.
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 DatacosmosException(
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)
@@ -1,20 +1,20 @@
1
- """Validates an API response and raises a DatacosmosException if an error occurs."""
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.datacosmos_exception import DatacosmosException
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 DatacosmosException if an error occurs.
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
- DatacosmosException: If the response status code indicates an error.
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 DatacosmosException(msg, response=response)
29
+ raise DatacosmosError(msg, response=response)
30
30
 
31
31
  except ValidationError:
32
- raise DatacosmosException(
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.17
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/datacosmos_exception.py
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
@@ -7,6 +7,7 @@ pystac==1.12.1
7
7
  pyyaml==6.0.2
8
8
  structlog==24.4.0
9
9
  tenacity>=8.2.3
10
+ shapely>=1.8.0
10
11
 
11
12
  [dev]
12
13
  black==22.3.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datacosmos"
7
- version = "0.0.17"
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