datacosmos 0.0.1__py3-none-any.whl → 0.0.3__py3-none-any.whl

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 (43) hide show
  1. config/__init__.py +5 -0
  2. config/config.py +195 -0
  3. config/models/__init__.py +1 -0
  4. config/models/m2m_authentication_config.py +23 -0
  5. config/models/url.py +35 -0
  6. datacosmos/exceptions/__init__.py +1 -0
  7. datacosmos/exceptions/datacosmos_exception.py +27 -0
  8. datacosmos/stac/__init__.py +5 -0
  9. datacosmos/stac/collection/__init__.py +4 -0
  10. datacosmos/stac/collection/collection_client.py +149 -0
  11. datacosmos/stac/collection/models/__init__.py +1 -0
  12. datacosmos/stac/collection/models/collection_update.py +46 -0
  13. datacosmos/stac/enums/__init__.py +1 -0
  14. datacosmos/stac/enums/level.py +15 -0
  15. datacosmos/stac/item/__init__.py +4 -0
  16. datacosmos/stac/item/item_client.py +186 -0
  17. datacosmos/stac/item/models/__init__.py +1 -0
  18. datacosmos/stac/item/models/asset.py +23 -0
  19. datacosmos/stac/item/models/datacosmos_item.py +55 -0
  20. datacosmos/stac/item/models/eo_band.py +15 -0
  21. datacosmos/stac/item/models/item_update.py +57 -0
  22. datacosmos/stac/item/models/raster_band.py +17 -0
  23. datacosmos/stac/item/models/search_parameters.py +58 -0
  24. datacosmos/stac/stac_client.py +12 -0
  25. datacosmos/uploader/__init__.py +1 -0
  26. datacosmos/uploader/dataclasses/__init__.py +1 -0
  27. datacosmos/uploader/dataclasses/upload_path.py +93 -0
  28. datacosmos/uploader/datacosmos_uploader.py +106 -0
  29. datacosmos/utils/__init__.py +1 -0
  30. datacosmos/utils/constants.py +16 -0
  31. datacosmos/utils/http_response/__init__.py +1 -0
  32. datacosmos/utils/http_response/check_api_response.py +34 -0
  33. datacosmos/utils/http_response/models/__init__.py +1 -0
  34. datacosmos/utils/http_response/models/datacosmos_error.py +26 -0
  35. datacosmos/utils/http_response/models/datacosmos_response.py +11 -0
  36. datacosmos/utils/missions.py +27 -0
  37. datacosmos/utils/url.py +60 -0
  38. {datacosmos-0.0.1.dist-info → datacosmos-0.0.3.dist-info}/METADATA +3 -2
  39. datacosmos-0.0.3.dist-info/RECORD +44 -0
  40. {datacosmos-0.0.1.dist-info → datacosmos-0.0.3.dist-info}/WHEEL +1 -1
  41. {datacosmos-0.0.1.dist-info → datacosmos-0.0.3.dist-info}/top_level.txt +1 -0
  42. datacosmos-0.0.1.dist-info/RECORD +0 -7
  43. {datacosmos-0.0.1.dist-info → datacosmos-0.0.3.dist-info/licenses}/LICENSE.md +0 -0
config/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Configuration package for the Datacosmos SDK.
2
+
3
+ This package includes modules for loading and managing authentication
4
+ configurations.
5
+ """
config/config.py ADDED
@@ -0,0 +1,195 @@
1
+ """Configuration module for the Datacosmos SDK.
2
+
3
+ Handles configuration management using Pydantic and Pydantic Settings.
4
+ It loads default values, allows overrides via YAML configuration files,
5
+ and supports environment variable-based overrides.
6
+ """
7
+
8
+ import os
9
+ from typing import ClassVar, Literal, Optional
10
+
11
+ import yaml
12
+ from pydantic import field_validator
13
+ from pydantic_settings import BaseSettings, SettingsConfigDict
14
+
15
+ from config.models.m2m_authentication_config import M2MAuthenticationConfig
16
+ from config.models.url import URL
17
+
18
+
19
+ class Config(BaseSettings):
20
+ """Centralized configuration for the Datacosmos SDK."""
21
+
22
+ model_config = SettingsConfigDict(
23
+ env_nested_delimiter="__",
24
+ nested_model_default_partial_update=True,
25
+ extra="allow",
26
+ )
27
+
28
+ authentication: Optional[M2MAuthenticationConfig] = None
29
+ stac: Optional[URL] = None
30
+ datacosmos_cloud_storage: Optional[URL] = None
31
+ mission_id: int = 0
32
+ environment: Literal["local", "test", "prod"] = "test"
33
+
34
+ DEFAULT_AUTH_TYPE: ClassVar[str] = "m2m"
35
+ DEFAULT_AUTH_TOKEN_URL: ClassVar[str] = "https://login.open-cosmos.com/oauth/token"
36
+ DEFAULT_AUTH_AUDIENCE: ClassVar[str] = "https://beeapp.open-cosmos.com"
37
+
38
+ @classmethod
39
+ def from_yaml(cls, file_path: str = "config/config.yaml") -> "Config":
40
+ """Load configuration from a YAML file and override defaults.
41
+
42
+ Args:
43
+ file_path (str): The path to the YAML configuration file.
44
+
45
+ Returns:
46
+ Config: An instance of the Config class with loaded settings.
47
+ """
48
+ config_data: dict = {}
49
+ if os.path.exists(file_path):
50
+ with open(file_path, "r") as f:
51
+ yaml_data = yaml.safe_load(f) or {}
52
+ # Remove empty values from YAML to avoid overwriting with `None`
53
+ config_data = {
54
+ key: value
55
+ for key, value in yaml_data.items()
56
+ if value not in [None, ""]
57
+ }
58
+
59
+ return cls(**config_data)
60
+
61
+ @classmethod
62
+ def from_env(cls) -> "Config":
63
+ """Load configuration from environment variables.
64
+
65
+ Returns:
66
+ Config: An instance of the Config class with settings loaded from environment variables.
67
+ """
68
+ authentication_config = M2MAuthenticationConfig(
69
+ type=os.getenv("OC_AUTH_TYPE", cls.DEFAULT_AUTH_TYPE),
70
+ client_id=os.getenv("OC_AUTH_CLIENT_ID"),
71
+ client_secret=os.getenv("OC_AUTH_CLIENT_SECRET"),
72
+ token_url=os.getenv("OC_AUTH_TOKEN_URL", cls.DEFAULT_AUTH_TOKEN_URL),
73
+ audience=os.getenv("OC_AUTH_AUDIENCE", cls.DEFAULT_AUTH_AUDIENCE),
74
+ )
75
+
76
+ stac_config = URL(
77
+ protocol=os.getenv("OC_STAC_PROTOCOL", "https"),
78
+ host=os.getenv("OC_STAC_HOST", "app.open-cosmos.com"),
79
+ port=int(os.getenv("OC_STAC_PORT", "443")),
80
+ path=os.getenv("OC_STAC_PATH", "/api/data/v0/stac"),
81
+ )
82
+
83
+ datacosmos_cloud_storage_config = URL(
84
+ protocol=os.getenv("DC_CLOUD_STORAGE_PROTOCOL", "https"),
85
+ host=os.getenv("DC_CLOUD_STORAGE_HOST", "app.open-cosmos.com"),
86
+ port=int(os.getenv("DC_CLOUD_STORAGE_PORT", "443")),
87
+ path=os.getenv("DC_CLOUD_STORAGE_PATH", "/api/data/v0/storage"),
88
+ )
89
+
90
+ return cls(
91
+ authentication=authentication_config,
92
+ stac=stac_config,
93
+ datacosmos_cloud_storage=datacosmos_cloud_storage_config,
94
+ mission_id=int(os.getenv("MISSION_ID", "0")),
95
+ environment=os.getenv("ENVIRONMENT", "test"),
96
+ )
97
+
98
+ @field_validator("authentication", mode="before")
99
+ @classmethod
100
+ def validate_authentication(
101
+ cls, auth_data: Optional[dict]
102
+ ) -> M2MAuthenticationConfig:
103
+ """Ensure authentication is provided and apply defaults.
104
+
105
+ Args:
106
+ auth_data (Optional[dict]): The authentication config as a dictionary.
107
+
108
+ Returns:
109
+ M2MAuthenticationConfig: The validated authentication configuration.
110
+
111
+ Raises:
112
+ ValueError: If authentication is missing or required fields are not set.
113
+ """
114
+ if not auth_data:
115
+ return cls.apply_auth_defaults(M2MAuthenticationConfig())
116
+
117
+ auth = cls.parse_auth_config(auth_data)
118
+ auth = cls.apply_auth_defaults(auth)
119
+
120
+ cls.check_required_auth_fields(auth)
121
+ return auth
122
+
123
+ @staticmethod
124
+ def apply_auth_defaults(auth: M2MAuthenticationConfig) -> M2MAuthenticationConfig:
125
+ """Apply default authentication values if they are missing."""
126
+ auth.type = auth.type or Config.DEFAULT_AUTH_TYPE
127
+ auth.token_url = auth.token_url or Config.DEFAULT_AUTH_TOKEN_URL
128
+ auth.audience = auth.audience or Config.DEFAULT_AUTH_AUDIENCE
129
+ return auth
130
+
131
+ @classmethod
132
+ def parse_auth_config(cls, auth_data: dict) -> M2MAuthenticationConfig:
133
+ """Parse authentication config from a dictionary."""
134
+ return M2MAuthenticationConfig(
135
+ type=auth_data.get("type", cls.DEFAULT_AUTH_TYPE),
136
+ token_url=auth_data.get("token_url", cls.DEFAULT_AUTH_TOKEN_URL),
137
+ audience=auth_data.get("audience", cls.DEFAULT_AUTH_AUDIENCE),
138
+ client_id=auth_data.get("client_id"),
139
+ client_secret=auth_data.get("client_secret"),
140
+ )
141
+
142
+ @staticmethod
143
+ def check_required_auth_fields(auth: M2MAuthenticationConfig):
144
+ """Ensure required fields (client_id, client_secret) are provided."""
145
+ missing_fields = [
146
+ field
147
+ for field in ("client_id", "client_secret")
148
+ if not getattr(auth, field)
149
+ ]
150
+ if missing_fields:
151
+ raise ValueError(
152
+ f"Missing required authentication fields: {', '.join(missing_fields)}"
153
+ )
154
+
155
+ @field_validator("stac", mode="before")
156
+ @classmethod
157
+ def validate_stac(cls, stac_config: Optional[URL]) -> URL:
158
+ """Ensure STAC configuration has a default if not explicitly set.
159
+
160
+ Args:
161
+ stac_config (Optional[URL]): The STAC config to validate.
162
+
163
+ Returns:
164
+ URL: The validated STAC configuration.
165
+ """
166
+ if stac_config is None:
167
+ return URL(
168
+ protocol="https",
169
+ host="app.open-cosmos.com",
170
+ port=443,
171
+ path="/api/data/v0/stac",
172
+ )
173
+ return stac_config
174
+
175
+ @field_validator("datacosmos_cloud_storage", mode="before")
176
+ @classmethod
177
+ def validate_datacosmos_cloud_storage(
178
+ cls, datacosmos_cloud_storage_config: Optional[URL]
179
+ ) -> URL:
180
+ """Ensure datacosmos cloud storage configuration has a default if not explicitly set.
181
+
182
+ Args:
183
+ datacosmos_cloud_storage_config (Optional[URL]): The datacosmos cloud storage config to validate.
184
+
185
+ Returns:
186
+ URL: The validated datacosmos cloud storage configuration.
187
+ """
188
+ if datacosmos_cloud_storage_config is None:
189
+ return URL(
190
+ protocol="https",
191
+ host="app.open-cosmos.com",
192
+ port=443,
193
+ path="/api/data/v0/storage",
194
+ )
195
+ return datacosmos_cloud_storage_config
@@ -0,0 +1 @@
1
+ """Models for configuration settings."""
@@ -0,0 +1,23 @@
1
+ """Module for configuring machine-to-machine (M2M) authentication.
2
+
3
+ Used when running scripts in the cluster that require automated authentication
4
+ without user interaction.
5
+ """
6
+
7
+ from typing import Literal
8
+
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class M2MAuthenticationConfig(BaseModel):
13
+ """Configuration for machine-to-machine authentication.
14
+
15
+ This is used when running scripts in the cluster that require authentication
16
+ with client credentials.
17
+ """
18
+
19
+ type: Literal["m2m"]
20
+ client_id: str
21
+ token_url: str
22
+ audience: str
23
+ client_secret: str
config/models/url.py ADDED
@@ -0,0 +1,35 @@
1
+ """Module defining a structured URL configuration model.
2
+
3
+ Ensures that URLs contain required components such as protocol, host,
4
+ port, and path.
5
+ """
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from datacosmos.utils.url import URL as DomainURL
10
+
11
+
12
+ class URL(BaseModel):
13
+ """Generic configuration model for a URL.
14
+
15
+ This class provides attributes to store URL components and a method
16
+ to convert them into a `DomainURL` instance.
17
+ """
18
+
19
+ protocol: str
20
+ host: str
21
+ port: int
22
+ path: str
23
+
24
+ def as_domain_url(self) -> DomainURL:
25
+ """Convert the URL instance to a `DomainURL` object.
26
+
27
+ Returns:
28
+ DomainURL: A domain-specific URL object.
29
+ """
30
+ return DomainURL(
31
+ protocol=self.protocol,
32
+ host=self.host,
33
+ port=self.port,
34
+ base=self.path,
35
+ )
@@ -0,0 +1 @@
1
+ """Exceptions for the datacosmos package."""
@@ -0,0 +1,27 @@
1
+ """Base exception class for all Datacosmos SDK exceptions."""
2
+
3
+ from typing import Optional
4
+
5
+ from requests import Response
6
+ from requests.exceptions import RequestException
7
+
8
+
9
+ class DatacosmosException(RequestException):
10
+ """Base exception class for all Datacosmos SDK exceptions."""
11
+
12
+ def __init__(self, message: str, response: Optional[Response] = None):
13
+ """Initialize DatacosmosException.
14
+
15
+ Args:
16
+ message (str): The error message.
17
+ response (Optional[Response]): The HTTP response object, if available.
18
+ """
19
+ self.response = response
20
+ self.status_code = response.status_code if response else None
21
+ self.details = response.text if response else None
22
+ full_message = (
23
+ f"{message} (Status: {self.status_code}, Details: {self.details})"
24
+ if response
25
+ else message
26
+ )
27
+ super().__init__(full_message)
@@ -0,0 +1,5 @@
1
+ """STAC package for interacting with the STAC API, providing query and fetch functionalities.
2
+
3
+ It enables interaction with STAC (SpatioTemporal Asset Catalog) services
4
+ using an authenticated Datacosmos client.
5
+ """
@@ -0,0 +1,4 @@
1
+ """STAC package for interacting with collections from the STAC API, providing query and fetch functionalities.
2
+
3
+ It enables interaction with collections from the STAC using an authenticated Datacosmos client.
4
+ """
@@ -0,0 +1,149 @@
1
+ """Handles operations related to STAC collections."""
2
+
3
+ from typing import Generator, Optional
4
+
5
+ from pystac import Collection, Extent, SpatialExtent, TemporalExtent
6
+ from pystac.utils import str_to_datetime
7
+
8
+ from datacosmos.datacosmos_client import DatacosmosClient
9
+ from datacosmos.stac.collection.models.collection_update import CollectionUpdate
10
+ from datacosmos.utils.http_response.check_api_response import check_api_response
11
+
12
+
13
+ class CollectionClient:
14
+ """Handles operations related to STAC collections."""
15
+
16
+ def __init__(self, client: DatacosmosClient):
17
+ """Initialize the CollectionClient with a DatacosmosClient."""
18
+ self.client = client
19
+ self.base_url = client.config.stac.as_domain_url()
20
+
21
+ def fetch_collection(self, collection_id: str) -> Collection:
22
+ """Fetch details of an existing STAC collection."""
23
+ url = self.base_url.with_suffix(f"/collections/{collection_id}")
24
+ response = self.client.get(url)
25
+ check_api_response(response)
26
+ return Collection.from_dict(response.json())
27
+
28
+ def create_collection(self, collection: Collection) -> None:
29
+ """Create a new STAC collection.
30
+
31
+ Args:
32
+ collection (Collection): The STAC collection to create.
33
+
34
+ Raises:
35
+ InvalidRequest: If the collection data is malformed.
36
+ """
37
+ if isinstance(collection.extent, dict):
38
+ spatial_data = collection.extent.get("spatial", {}).get("bbox", [[]])
39
+ temporal_data = collection.extent.get("temporal", {}).get("interval", [[]])
40
+
41
+ # Convert string timestamps to datetime objects
42
+ parsed_temporal = []
43
+ for interval in temporal_data:
44
+ start = str_to_datetime(interval[0]) if interval[0] else None
45
+ end = (
46
+ str_to_datetime(interval[1])
47
+ if len(interval) > 1 and interval[1]
48
+ else None
49
+ )
50
+ parsed_temporal.append([start, end])
51
+
52
+ collection.extent = Extent(
53
+ spatial=SpatialExtent(spatial_data),
54
+ temporal=TemporalExtent(parsed_temporal),
55
+ )
56
+
57
+ url = self.base_url.with_suffix("/collections")
58
+ response = self.client.post(url, json=collection.to_dict())
59
+ check_api_response(response)
60
+
61
+ def update_collection(
62
+ self, collection_id: str, update_data: CollectionUpdate
63
+ ) -> None:
64
+ """Update an existing STAC collection."""
65
+ url = self.base_url.with_suffix(f"/collections/{collection_id}")
66
+ response = self.client.patch(
67
+ url, json=update_data.model_dump(by_alias=True, exclude_none=True)
68
+ )
69
+ check_api_response(response)
70
+
71
+ def delete_collection(self, collection_id: str) -> None:
72
+ """Delete a STAC collection by its ID."""
73
+ url = self.base_url.with_suffix(f"/collections/{collection_id}")
74
+ response = self.client.delete(url)
75
+ check_api_response(response)
76
+
77
+ def fetch_all_collections(self) -> Generator[Collection, None, None]:
78
+ """Fetch all STAC collections with pagination support."""
79
+ url = self.base_url.with_suffix("/collections")
80
+ params = {"limit": 10}
81
+
82
+ while True:
83
+ data = self._fetch_collections_page(url, params)
84
+ yield from self._parse_collections(data)
85
+
86
+ next_cursor = self._get_next_pagination_cursor(data)
87
+ if not next_cursor:
88
+ break
89
+
90
+ params["cursor"] = next_cursor
91
+
92
+ def _fetch_collections_page(self, url: str, params: dict) -> dict:
93
+ """Fetch a single page of collections from the API."""
94
+ response = self.client.get(url, params=params)
95
+ check_api_response(response)
96
+
97
+ data = response.json()
98
+
99
+ if isinstance(data, list):
100
+ return {"collections": data}
101
+
102
+ return data
103
+
104
+ def _parse_collections(self, data: dict) -> Generator[Collection, None, None]:
105
+ """Convert API response data to STAC Collection objects, ensuring required fields exist."""
106
+ return (
107
+ Collection.from_dict(
108
+ {
109
+ **collection,
110
+ "type": collection.get("type", "Collection"),
111
+ "id": collection.get("id", ""),
112
+ "stac_version": collection.get("stac_version", "1.0.0"),
113
+ "extent": collection.get(
114
+ "extent",
115
+ {"spatial": {"bbox": []}, "temporal": {"interval": []}},
116
+ ),
117
+ "links": collection.get("links", []) or [],
118
+ "properties": collection.get("properties", {}),
119
+ }
120
+ )
121
+ for collection in data.get("collections", [])
122
+ if collection.get("type") == "Collection"
123
+ )
124
+
125
+ def _get_next_pagination_cursor(self, data: dict) -> Optional[str]:
126
+ """Extract the next pagination token from the response."""
127
+ next_href = self._get_next_link(data)
128
+ return self._extract_pagination_token(next_href) if next_href else None
129
+
130
+ def _get_next_link(self, data: dict) -> Optional[str]:
131
+ """Extract the next page link from the response."""
132
+ next_link = next(
133
+ (link for link in data.get("links", []) if link.get("rel") == "next"), None
134
+ )
135
+ return next_link.get("href", "") if next_link else None
136
+
137
+ def _extract_pagination_token(self, next_href: str) -> Optional[str]:
138
+ """Extract the pagination token from the next link URL.
139
+
140
+ Args:
141
+ next_href (str): The next page URL.
142
+
143
+ Returns:
144
+ Optional[str]: The extracted token, or None if parsing fails.
145
+ """
146
+ try:
147
+ return next_href.split("?")[1].split("=")[-1]
148
+ except (IndexError, AttributeError):
149
+ raise InvalidRequest(f"Failed to parse pagination token from {next_href}")
@@ -0,0 +1 @@
1
+ """Models for the Collection Client."""
@@ -0,0 +1,46 @@
1
+ """Represents a structured update model for STAC collections.
2
+
3
+ Allows partial updates where only the provided fields are modified.
4
+ """
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+ from pystac import Extent, Link, Provider, Summaries
9
+
10
+
11
+ class CollectionUpdate(BaseModel):
12
+ """Represents a structured update model for STAC collections.
13
+
14
+ Allows partial updates where only the provided fields are modified.
15
+ """
16
+
17
+ model_config = {"arbitrary_types_allowed": True}
18
+
19
+ title: Optional[str] = Field(None, description="Title of the STAC collection.")
20
+ description: Optional[str] = Field(
21
+ None, description="Description of the collection."
22
+ )
23
+ keywords: Optional[List[str]] = Field(
24
+ None, description="List of keywords associated with the collection."
25
+ )
26
+ license: Optional[str] = Field(None, description="Collection license information.")
27
+ providers: Optional[List[Provider]] = Field(
28
+ None, description="List of data providers."
29
+ )
30
+ extent: Optional[Extent] = Field(
31
+ None, description="Spatial and temporal extent of the collection."
32
+ )
33
+ summaries: Optional[Summaries] = Field(
34
+ None, description="Summaries for the collection."
35
+ )
36
+ links: Optional[List[Link]] = Field(
37
+ None, description="List of links associated with the collection."
38
+ )
39
+
40
+ def to_dict(self) -> Dict[str, Any]:
41
+ """Convert the model into a dictionary, excluding `None` values.
42
+
43
+ Returns:
44
+ Dict[str, Any]: Dictionary representation of the update payload.
45
+ """
46
+ return self.model_dump(by_alias=True, exclude_none=True)
@@ -0,0 +1 @@
1
+ """Enums for STAC."""
@@ -0,0 +1,15 @@
1
+ """Level enum class."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Level(Enum):
7
+ """Enum class for the processing levels of the data."""
8
+
9
+ L0 = "l0"
10
+ L1A = "l1a"
11
+ L2A = "l2a"
12
+ L1B = "l1b"
13
+ L1C = "l1c"
14
+ L1D = "l1d"
15
+ L3 = "l3"
@@ -0,0 +1,4 @@
1
+ """STAC package for interacting with items from the STAC API, providing query and fetch functionalities.
2
+
3
+ It enables interaction with items from the STAC using an authenticated Datacosmos client.
4
+ """