datacosmos 0.0.1__tar.gz → 0.0.3__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.1 → datacosmos-0.0.3}/PKG-INFO +3 -2
- datacosmos-0.0.3/README.md +209 -0
- datacosmos-0.0.3/config/__init__.py +5 -0
- datacosmos-0.0.3/config/config.py +195 -0
- datacosmos-0.0.3/config/models/__init__.py +1 -0
- datacosmos-0.0.3/config/models/m2m_authentication_config.py +23 -0
- datacosmos-0.0.3/config/models/url.py +35 -0
- datacosmos-0.0.3/datacosmos/exceptions/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/exceptions/datacosmos_exception.py +27 -0
- datacosmos-0.0.3/datacosmos/stac/__init__.py +5 -0
- datacosmos-0.0.3/datacosmos/stac/collection/__init__.py +4 -0
- datacosmos-0.0.3/datacosmos/stac/collection/collection_client.py +149 -0
- datacosmos-0.0.3/datacosmos/stac/collection/models/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/stac/collection/models/collection_update.py +46 -0
- datacosmos-0.0.3/datacosmos/stac/enums/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/stac/enums/level.py +15 -0
- datacosmos-0.0.3/datacosmos/stac/item/__init__.py +4 -0
- datacosmos-0.0.3/datacosmos/stac/item/item_client.py +186 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/asset.py +23 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/datacosmos_item.py +55 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/eo_band.py +15 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/item_update.py +57 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/raster_band.py +17 -0
- datacosmos-0.0.3/datacosmos/stac/item/models/search_parameters.py +58 -0
- datacosmos-0.0.3/datacosmos/stac/stac_client.py +12 -0
- datacosmos-0.0.3/datacosmos/uploader/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/uploader/dataclasses/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/uploader/dataclasses/upload_path.py +93 -0
- datacosmos-0.0.3/datacosmos/uploader/datacosmos_uploader.py +106 -0
- datacosmos-0.0.3/datacosmos/utils/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/utils/constants.py +16 -0
- datacosmos-0.0.3/datacosmos/utils/http_response/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/utils/http_response/check_api_response.py +34 -0
- datacosmos-0.0.3/datacosmos/utils/http_response/models/__init__.py +1 -0
- datacosmos-0.0.3/datacosmos/utils/http_response/models/datacosmos_error.py +26 -0
- datacosmos-0.0.3/datacosmos/utils/http_response/models/datacosmos_response.py +11 -0
- datacosmos-0.0.3/datacosmos/utils/missions.py +27 -0
- datacosmos-0.0.3/datacosmos/utils/url.py +60 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos.egg-info/PKG-INFO +3 -2
- datacosmos-0.0.3/datacosmos.egg-info/SOURCES.txt +48 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos.egg-info/top_level.txt +1 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/pyproject.toml +3 -3
- datacosmos-0.0.1/README.md +0 -239
- datacosmos-0.0.1/datacosmos.egg-info/SOURCES.txt +0 -11
- {datacosmos-0.0.1 → datacosmos-0.0.3}/LICENSE.md +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos/__init__.py +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos/datacosmos_client.py +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos.egg-info/dependency_links.txt +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/datacosmos.egg-info/requires.txt +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/setup.cfg +0 -0
- {datacosmos-0.0.1 → datacosmos-0.0.3}/tests/test_pass.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: datacosmos
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
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
|
|
@@ -20,3 +20,4 @@ Requires-Dist: pytest==7.2.0; extra == "dev"
|
|
|
20
20
|
Requires-Dist: bandit[toml]==1.7.4; extra == "dev"
|
|
21
21
|
Requires-Dist: isort==5.11.4; extra == "dev"
|
|
22
22
|
Requires-Dist: pydocstyle==6.1.1; extra == "dev"
|
|
23
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# DataCosmos SDK
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The **DataCosmos SDK** enables Open Cosmos customers to interact with the **DataCosmos APIs** for efficient data management and retrieval. It provides authentication handling, HTTP request utilities, and a client for interacting with the **STAC API** (SpatioTemporal Asset Catalog).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Install the SDK using **pip**:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pip install datacosmos=={version}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Initializing the Client
|
|
16
|
+
|
|
17
|
+
To start using the SDK, initialize the client. The easiest way to do this is by loading the configuration from a YAML file. Alternatively, you can manually instantiate the Config object or use environment variables.
|
|
18
|
+
|
|
19
|
+
### Default Initialization (Recommended)
|
|
20
|
+
|
|
21
|
+
By default, the client loads configuration from a YAML file (`config/config.yaml`).
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from datacosmos.datacosmos_client import DatacosmosClient
|
|
25
|
+
|
|
26
|
+
client = DatacosmosClient()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Loading from YAML (Recommended)
|
|
30
|
+
|
|
31
|
+
Create a YAML file (`config/config.yaml`) with the following content:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
authentication:
|
|
35
|
+
client_id: {client_id}
|
|
36
|
+
client_secret: {client_secret}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The client will automatically read this file when initialized.
|
|
40
|
+
|
|
41
|
+
### Loading from Environment Variables
|
|
42
|
+
|
|
43
|
+
Set the following environment variables:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
export OC_AUTH_CLIENT_ID={client_id}
|
|
47
|
+
export OC_AUTH_CLIENT_SECRET={client_secret}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The client will automatically read these values when initialized.
|
|
51
|
+
|
|
52
|
+
### Manual Instantiation
|
|
53
|
+
|
|
54
|
+
If manually instantiating `Config`, default values are now applied where possible.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from config.config import Config
|
|
58
|
+
from config.models.m2m_authentication_config import M2MAuthenticationConfig
|
|
59
|
+
from config.models.url import URL
|
|
60
|
+
|
|
61
|
+
config = Config(
|
|
62
|
+
authentication=M2MAuthenticationConfig(
|
|
63
|
+
client_id="your-client-id",
|
|
64
|
+
client_secret="your-client-secret"
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
client = DatacosmosClient(config=config)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Configuration Options and Defaults
|
|
72
|
+
|
|
73
|
+
| Setting | Default Value | Override Method |
|
|
74
|
+
|------------------------------|-------------------------------------------------|----------------|
|
|
75
|
+
| `authentication.type` | `m2m` | YAML / ENV |
|
|
76
|
+
| `authentication.client_id` | _Required in manual instantiation_ | YAML / ENV |
|
|
77
|
+
| `authentication.client_secret` | _Required in manual instantiation_ | YAML / ENV |
|
|
78
|
+
| `stac.protocol` | `https` | YAML / ENV |
|
|
79
|
+
| `stac.host` | `app.open-cosmos.com` | YAML / ENV |
|
|
80
|
+
| `stac.port` | `443` | YAML / ENV |
|
|
81
|
+
| `stac.path` | `/api/data/v0/stac` | YAML / ENV |
|
|
82
|
+
| `datacosmos_cloud_storage.protocol` | `https` | YAML / ENV |
|
|
83
|
+
| `datacosmos_cloud_storage.host` | `app.open-cosmos.com` | YAML / ENV |
|
|
84
|
+
| `datacosmos_cloud_storage.port` | `443` | YAML / ENV |
|
|
85
|
+
| `datacosmos_cloud_storage.path` | `/api/data/v0/storage` | YAML / ENV |
|
|
86
|
+
| `mission_id` | `0` | YAML / ENV |
|
|
87
|
+
| `environment` | `test` | YAML / ENV |
|
|
88
|
+
|
|
89
|
+
## STAC Client
|
|
90
|
+
|
|
91
|
+
The `STACClient` enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items and collections.
|
|
92
|
+
|
|
93
|
+
### Initialize STACClient
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from datacosmos.datacosmos_client import DatacosmosClient
|
|
97
|
+
from datacosmos.stac.stac_client import STACClient
|
|
98
|
+
|
|
99
|
+
client = DatacosmosClient()
|
|
100
|
+
stac_client = STACClient(client)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### STACClient Methods
|
|
104
|
+
|
|
105
|
+
#### 1. Fetch a Collection
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
collection = stac_client.fetch_collection("test-collection")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### 2. Fetch All Collections
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
collections = list(stac_client.fetch_all_collections())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### 3. Create a Collection
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from pystac import Collection
|
|
121
|
+
|
|
122
|
+
new_collection = Collection(
|
|
123
|
+
id="test-collection",
|
|
124
|
+
title="Test Collection",
|
|
125
|
+
description="This is a test collection",
|
|
126
|
+
license="proprietary",
|
|
127
|
+
extent={
|
|
128
|
+
"spatial": {"bbox": [[-180, -90, 180, 90]]},
|
|
129
|
+
"temporal": {"interval": [["2023-01-01T00:00:00Z", None]]},
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
stac_client.create_collection(new_collection)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
#### 4. Update a Collection
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from datacosmos.stac.collection.models.collection_update import CollectionUpdate
|
|
140
|
+
|
|
141
|
+
update_data = CollectionUpdate(
|
|
142
|
+
title="Updated Collection Title",
|
|
143
|
+
description="Updated description",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
stac_client.update_collection("test-collection", update_data)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
#### 5. Delete a Collection
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
stac_client.delete_collection("test-collection")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Uploading Files and Registering STAC Items
|
|
156
|
+
|
|
157
|
+
You can use the `DatacosmosUploader` class to upload files to the DataCosmos cloud storage and register a STAC item.
|
|
158
|
+
|
|
159
|
+
#### Upload Files and Register STAC Item
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from datacosmos.uploader.datacosmos_uploader import DatacosmosUploader
|
|
163
|
+
|
|
164
|
+
uploader = DatacosmosUploader(client)
|
|
165
|
+
item_json_file_path = "/path/to/stac_item.json"
|
|
166
|
+
uploader.upload_and_register_item(item_json_file_path)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Error Handling
|
|
170
|
+
|
|
171
|
+
Use `try-except` blocks to handle API errors gracefully:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
try:
|
|
175
|
+
data = client.get_data("dataset_id")
|
|
176
|
+
print(data)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"An error occurred: {e}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
To contribute:
|
|
184
|
+
|
|
185
|
+
1. Fork the repository.
|
|
186
|
+
2. Create a feature branch.
|
|
187
|
+
3. Submit a pull request.
|
|
188
|
+
|
|
189
|
+
### Development Setup
|
|
190
|
+
|
|
191
|
+
Use `uv` for dependency management:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
pip install uv
|
|
195
|
+
uv venv
|
|
196
|
+
uv pip install -r pyproject.toml .[dev]
|
|
197
|
+
source .venv/bin/activate
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Before making changes, run:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
black .
|
|
204
|
+
isort .
|
|
205
|
+
ruff check .
|
|
206
|
+
pydocstyle .
|
|
207
|
+
bandit -r -c pyproject.toml .
|
|
208
|
+
pytest
|
|
209
|
+
```
|
|
@@ -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
|
|
@@ -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,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."""
|