datacosmos 0.0.16__tar.gz → 0.0.18__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 (69) hide show
  1. {datacosmos-0.0.16 → datacosmos-0.0.18}/PKG-INFO +2 -1
  2. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/datacosmos_client.py +43 -2
  3. datacosmos-0.0.18/datacosmos/stac/enums/processing_level.py +28 -0
  4. datacosmos-0.0.18/datacosmos/stac/item/models/datacosmos_item.py +139 -0
  5. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos.egg-info/PKG-INFO +2 -1
  6. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos.egg-info/requires.txt +1 -0
  7. {datacosmos-0.0.16 → datacosmos-0.0.18}/pyproject.toml +3 -2
  8. datacosmos-0.0.16/datacosmos/stac/enums/processing_level.py +0 -16
  9. datacosmos-0.0.16/datacosmos/stac/item/models/datacosmos_item.py +0 -55
  10. {datacosmos-0.0.16 → datacosmos-0.0.18}/LICENSE.md +0 -0
  11. {datacosmos-0.0.16 → datacosmos-0.0.18}/README.md +0 -0
  12. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/__init__.py +0 -0
  13. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/__init__.py +0 -0
  14. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/base_authenticator.py +0 -0
  15. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/local_authenticator.py +0 -0
  16. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/local_token_fetcher.py +0 -0
  17. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/m2m_authenticator.py +0 -0
  18. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/auth/token.py +0 -0
  19. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/__init__.py +0 -0
  20. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/auth/__init__.py +0 -0
  21. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/auth/factory.py +0 -0
  22. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/config.py +0 -0
  23. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/constants.py +0 -0
  24. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/loaders/yaml_source.py +0 -0
  25. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/__init__.py +0 -0
  26. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/authentication_config.py +0 -0
  27. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/local_user_account_authentication_config.py +0 -0
  28. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/m2m_authentication_config.py +0 -0
  29. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/no_authentication_config.py +0 -0
  30. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/config/models/url.py +0 -0
  31. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/exceptions/__init__.py +0 -0
  32. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/exceptions/datacosmos_exception.py +0 -0
  33. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/__init__.py +0 -0
  34. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/collection/__init__.py +0 -0
  35. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/collection/collection_client.py +0 -0
  36. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/collection/models/__init__.py +0 -0
  37. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/collection/models/collection_update.py +0 -0
  38. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/constants/__init__.py +0 -0
  39. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/constants/satellite_name_mapping.py +0 -0
  40. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/enums/__init__.py +0 -0
  41. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/enums/product_type.py +0 -0
  42. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/enums/season.py +0 -0
  43. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/__init__.py +0 -0
  44. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/item_client.py +0 -0
  45. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/__init__.py +0 -0
  46. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/asset.py +0 -0
  47. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/catalog_search_parameters.py +0 -0
  48. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/eo_band.py +0 -0
  49. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/item_update.py +0 -0
  50. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/item/models/raster_band.py +0 -0
  51. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/stac_client.py +0 -0
  52. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/__init__.py +0 -0
  53. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/dataclasses/__init__.py +0 -0
  54. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/dataclasses/upload_path.py +0 -0
  55. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/storage_base.py +0 -0
  56. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/storage_client.py +0 -0
  57. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/stac/storage/uploader.py +0 -0
  58. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/__init__.py +0 -0
  59. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/http_response/__init__.py +0 -0
  60. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/http_response/check_api_response.py +0 -0
  61. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/http_response/models/__init__.py +0 -0
  62. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  63. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  64. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos/utils/url.py +0 -0
  65. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos.egg-info/SOURCES.txt +0 -0
  66. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos.egg-info/dependency_links.txt +0 -0
  67. {datacosmos-0.0.16 → datacosmos-0.0.18}/datacosmos.egg-info/top_level.txt +0 -0
  68. {datacosmos-0.0.16 → datacosmos-0.0.18}/setup.cfg +0 -0
  69. {datacosmos-0.0.16 → datacosmos-0.0.18}/tests/test_pass.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.16
3
+ Version: 0.0.18
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"
@@ -1,9 +1,10 @@
1
1
  """Client to interact with the Datacosmos API with authentication and request handling."""
2
2
  from __future__ import annotations
3
3
 
4
+ import logging
4
5
  import threading
5
6
  from datetime import datetime, timedelta, timezone
6
- from typing import Any, Optional
7
+ from typing import Any, Callable, List, Optional
7
8
 
8
9
  import requests
9
10
  from requests.exceptions import ConnectionError, HTTPError, RequestException, Timeout
@@ -21,6 +22,11 @@ from datacosmos.auth.m2m_authenticator import M2MAuthenticator
21
22
  from datacosmos.config.config import Config
22
23
  from datacosmos.exceptions.datacosmos_exception import DatacosmosException
23
24
 
25
+ _log = logging.getLogger(__name__)
26
+
27
+ RequestHook = Callable[[str, str, Any, Any], None]
28
+ ResponseHook = Callable[[requests.Response], None]
29
+
24
30
 
25
31
  class DatacosmosClient:
26
32
  """Client to interact with the Datacosmos API with authentication and request handling."""
@@ -31,18 +37,24 @@ class DatacosmosClient:
31
37
  self,
32
38
  config: Optional[Config | Any] = None,
33
39
  http_session: Optional[requests.Session | OAuth2Session] = None,
40
+ request_hooks: Optional[List[RequestHook]] = None,
41
+ response_hooks: Optional[List[ResponseHook]] = None,
34
42
  ):
35
43
  """Initialize the DatacosmosClient.
36
44
 
37
45
  Args:
38
46
  config (Optional[Config]): Configuration object (only needed when SDK creates its own session).
39
47
  http_session (Optional[requests.Session]): Pre-authenticated session.
48
+ request_hooks (Optional[List[RequestHook]]): A list of functions to be called before each request.
49
+ response_hooks (Optional[List[ResponseHook]]): A list of functions to be called after each successful response.
40
50
  """
41
51
  self.config = self._coerce_config(config)
42
52
  self.token: Optional[str] = None
43
53
  self.token_expiry: Optional[datetime] = None
44
54
  self._refresh_lock = threading.Lock()
45
55
  self._authenticator: Optional[BaseAuthenticator] = None
56
+ self._request_hooks = request_hooks or []
57
+ self._response_hooks = response_hooks or []
46
58
 
47
59
  if http_session is not None:
48
60
  self._init_with_injected_session(http_session)
@@ -172,11 +184,40 @@ class DatacosmosClient:
172
184
  def request(
173
185
  self, method: str, url: str, *args: Any, **kwargs: Any
174
186
  ) -> requests.Response:
175
- """Send an HTTP request using the authenticated session (with auto-refresh and retries)."""
187
+ """Send an HTTP request using the authenticated session (with auto-refresh and retries).
188
+
189
+ Args:
190
+ method (str): The HTTP method (e.g., "GET", "POST").
191
+ url (str): The URL for the request.
192
+ *args: Positional arguments for requests.request().
193
+ **kwargs: Keyword arguments for requests.request().
194
+
195
+ Returns:
196
+ requests.Response: The HTTP response.
197
+
198
+ Raises:
199
+ DatacosmosException: For any HTTP or request-related errors.
200
+ """
176
201
  self._refresh_token_if_needed()
202
+
203
+ # Call pre-request hooks
204
+ for hook in self._request_hooks:
205
+ try:
206
+ hook(method, url, *args, **kwargs)
207
+ except Exception:
208
+ _log.error("Request hook failed.", exc_info=True)
209
+
177
210
  try:
178
211
  response = self._http_client.request(method, url, *args, **kwargs)
179
212
  response.raise_for_status()
213
+
214
+ # Call post-response hooks on success
215
+ for hook in self._response_hooks:
216
+ try:
217
+ hook(response)
218
+ except Exception:
219
+ _log.error("Response hook failed.", exc_info=True)
220
+
180
221
  return response
181
222
  except HTTPError as e:
182
223
  status = getattr(e.response, "status_code", None)
@@ -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"
@@ -0,0 +1,139 @@
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.datacosmos_exception import DatacosmosException
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 DatacosmosException(
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 DatacosmosException("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 DatacosmosException(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 DatacosmosException(
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 DatacosmosException(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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.16
3
+ Version: 0.0.18
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,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.16"
7
+ version = "0.0.18"
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,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