datacosmos 0.0.2__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.

Files changed (51) hide show
  1. {datacosmos-0.0.2/datacosmos.egg-info → datacosmos-0.0.3}/PKG-INFO +3 -2
  2. datacosmos-0.0.3/README.md +209 -0
  3. {datacosmos-0.0.2 → datacosmos-0.0.3}/config/config.py +56 -28
  4. datacosmos-0.0.3/datacosmos/stac/enums/__init__.py +1 -0
  5. datacosmos-0.0.3/datacosmos/stac/enums/level.py +15 -0
  6. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/item/item_client.py +2 -1
  7. datacosmos-0.0.3/datacosmos/stac/item/models/asset.py +23 -0
  8. datacosmos-0.0.3/datacosmos/stac/item/models/datacosmos_item.py +55 -0
  9. datacosmos-0.0.3/datacosmos/stac/item/models/eo_band.py +15 -0
  10. datacosmos-0.0.3/datacosmos/stac/item/models/raster_band.py +17 -0
  11. datacosmos-0.0.3/datacosmos/uploader/__init__.py +1 -0
  12. datacosmos-0.0.3/datacosmos/uploader/dataclasses/__init__.py +1 -0
  13. datacosmos-0.0.3/datacosmos/uploader/dataclasses/upload_path.py +93 -0
  14. datacosmos-0.0.3/datacosmos/uploader/datacosmos_uploader.py +106 -0
  15. datacosmos-0.0.3/datacosmos/utils/constants.py +16 -0
  16. datacosmos-0.0.3/datacosmos/utils/missions.py +27 -0
  17. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/url.py +23 -0
  18. {datacosmos-0.0.2 → datacosmos-0.0.3/datacosmos.egg-info}/PKG-INFO +3 -2
  19. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos.egg-info/SOURCES.txt +12 -0
  20. {datacosmos-0.0.2 → datacosmos-0.0.3}/pyproject.toml +1 -1
  21. datacosmos-0.0.2/README.md +0 -257
  22. {datacosmos-0.0.2 → datacosmos-0.0.3}/LICENSE.md +0 -0
  23. {datacosmos-0.0.2 → datacosmos-0.0.3}/config/__init__.py +0 -0
  24. {datacosmos-0.0.2 → datacosmos-0.0.3}/config/models/__init__.py +0 -0
  25. {datacosmos-0.0.2 → datacosmos-0.0.3}/config/models/m2m_authentication_config.py +0 -0
  26. {datacosmos-0.0.2 → datacosmos-0.0.3}/config/models/url.py +0 -0
  27. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/__init__.py +0 -0
  28. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/datacosmos_client.py +0 -0
  29. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/exceptions/__init__.py +0 -0
  30. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/exceptions/datacosmos_exception.py +0 -0
  31. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/__init__.py +0 -0
  32. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/collection/__init__.py +0 -0
  33. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/collection/collection_client.py +0 -0
  34. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/collection/models/__init__.py +0 -0
  35. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/collection/models/collection_update.py +0 -0
  36. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/item/__init__.py +0 -0
  37. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/item/models/__init__.py +0 -0
  38. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/item/models/item_update.py +0 -0
  39. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/item/models/search_parameters.py +0 -0
  40. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/stac/stac_client.py +0 -0
  41. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/__init__.py +0 -0
  42. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/http_response/__init__.py +0 -0
  43. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/http_response/check_api_response.py +0 -0
  44. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/http_response/models/__init__.py +0 -0
  45. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/http_response/models/datacosmos_error.py +0 -0
  46. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos/utils/http_response/models/datacosmos_response.py +0 -0
  47. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos.egg-info/dependency_links.txt +0 -0
  48. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos.egg-info/requires.txt +0 -0
  49. {datacosmos-0.0.2 → datacosmos-0.0.3}/datacosmos.egg-info/top_level.txt +0 -0
  50. {datacosmos-0.0.2 → datacosmos-0.0.3}/setup.cfg +0 -0
  51. {datacosmos-0.0.2 → datacosmos-0.0.3}/tests/test_pass.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.2
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
+ ```
@@ -6,7 +6,7 @@ and supports environment variable-based overrides.
6
6
  """
7
7
 
8
8
  import os
9
- from typing import ClassVar, Optional
9
+ from typing import ClassVar, Literal, Optional
10
10
 
11
11
  import yaml
12
12
  from pydantic import field_validator
@@ -27,6 +27,9 @@ class Config(BaseSettings):
27
27
 
28
28
  authentication: Optional[M2MAuthenticationConfig] = None
29
29
  stac: Optional[URL] = None
30
+ datacosmos_cloud_storage: Optional[URL] = None
31
+ mission_id: int = 0
32
+ environment: Literal["local", "test", "prod"] = "test"
30
33
 
31
34
  DEFAULT_AUTH_TYPE: ClassVar[str] = "m2m"
32
35
  DEFAULT_AUTH_TOKEN_URL: ClassVar[str] = "https://login.open-cosmos.com/oauth/token"
@@ -77,7 +80,20 @@ class Config(BaseSettings):
77
80
  path=os.getenv("OC_STAC_PATH", "/api/data/v0/stac"),
78
81
  )
79
82
 
80
- return cls(authentication=authentication_config, stac=stac_config)
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
+ )
81
97
 
82
98
  @field_validator("authentication", mode="before")
83
99
  @classmethod
@@ -96,7 +112,7 @@ class Config(BaseSettings):
96
112
  ValueError: If authentication is missing or required fields are not set.
97
113
  """
98
114
  if not auth_data:
99
- cls.raise_missing_auth_error()
115
+ return cls.apply_auth_defaults(M2MAuthenticationConfig())
100
116
 
101
117
  auth = cls.parse_auth_config(auth_data)
102
118
  auth = cls.apply_auth_defaults(auth)
@@ -105,34 +121,24 @@ class Config(BaseSettings):
105
121
  return auth
106
122
 
107
123
  @staticmethod
108
- def raise_missing_auth_error():
109
- """Raise an error when authentication is missing."""
110
- raise ValueError(
111
- "M2M authentication is required. Provide it via:\n"
112
- "1. Explicit instantiation (Config(authentication=...))\n"
113
- "2. A YAML config file (config.yaml)\n"
114
- "3. Environment variables (OC_AUTH_CLIENT_ID, OC_AUTH_CLIENT_SECRET, etc.)"
115
- )
116
-
117
- @staticmethod
118
- def parse_auth_config(auth_data: dict) -> M2MAuthenticationConfig:
119
- """Convert dictionary input to M2MAuthenticationConfig object."""
120
- return (
121
- M2MAuthenticationConfig(**auth_data)
122
- if isinstance(auth_data, dict)
123
- else auth_data
124
- )
125
-
126
- @classmethod
127
- def apply_auth_defaults(
128
- cls, auth: M2MAuthenticationConfig
129
- ) -> M2MAuthenticationConfig:
124
+ def apply_auth_defaults(auth: M2MAuthenticationConfig) -> M2MAuthenticationConfig:
130
125
  """Apply default authentication values if they are missing."""
131
- auth.type = auth.type or cls.DEFAULT_AUTH_TYPE
132
- auth.token_url = auth.token_url or cls.DEFAULT_AUTH_TOKEN_URL
133
- auth.audience = auth.audience or cls.DEFAULT_AUTH_AUDIENCE
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
134
129
  return auth
135
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
+
136
142
  @staticmethod
137
143
  def check_required_auth_fields(auth: M2MAuthenticationConfig):
138
144
  """Ensure required fields (client_id, client_secret) are provided."""
@@ -165,3 +171,25 @@ class Config(BaseSettings):
165
171
  path="/api/data/v0/stac",
166
172
  )
167
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
+ """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"
@@ -9,6 +9,7 @@ from pystac import Item
9
9
 
10
10
  from datacosmos.datacosmos_client import DatacosmosClient
11
11
  from datacosmos.exceptions.datacosmos_exception import DatacosmosException
12
+ from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
12
13
  from datacosmos.stac.item.models.item_update import ItemUpdate
13
14
  from datacosmos.stac.item.models.search_parameters import SearchParameters
14
15
  from datacosmos.utils.http_response.check_api_response import check_api_response
@@ -71,7 +72,7 @@ class ItemClient:
71
72
  body = parameters.model_dump(by_alias=True, exclude_none=True)
72
73
  return self._paginate_items(url, body)
73
74
 
74
- def create_item(self, collection_id: str, item: Item) -> None:
75
+ def create_item(self, collection_id: str, item: Item | DatacosmosItem) -> None:
75
76
  """Create a new STAC item in a specified collection.
76
77
 
77
78
  Args:
@@ -0,0 +1,23 @@
1
+ """Model representing a datacosmos item asset."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from datacosmos.stac.item.models.eo_band import EoBand
6
+ from datacosmos.stac.item.models.raster_band import RasterBand
7
+
8
+
9
+ class Asset(BaseModel):
10
+ """Model representing a datacosmos item asset."""
11
+
12
+ href: str
13
+ title: str
14
+ description: str
15
+ type: str
16
+ roles: list[str] | None
17
+ eo_bands: list[EoBand] | None = Field(default=None, alias="eo:bands")
18
+ raster_bands: list[RasterBand] | None = Field(default=None, alias="raster:bands")
19
+
20
+ class Config:
21
+ """Pydantic configuration."""
22
+
23
+ populate_by_name = True
@@ -0,0 +1,55 @@
1
+ """Model representing a datacosmos item."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from datacosmos.stac.enums.level import Level
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) -> Level:
40
+ """Get the processing level of the Datacosmos item."""
41
+ return Level(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()
@@ -0,0 +1,15 @@
1
+ """Model representing an EO band."""
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class EoBand(BaseModel):
9
+ """Model representing an EO band."""
10
+
11
+ name: str
12
+ common_name: str
13
+ center_wavelength: float
14
+ full_width_half_max: float
15
+ solar_illumination: Optional[float] = None
@@ -0,0 +1,17 @@
1
+ """Model representing a raster band."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class RasterBand(BaseModel):
7
+ """Model representing a raster band."""
8
+
9
+ gain: float = Field(alias="scale")
10
+ bias: float = Field(alias="offset")
11
+ nodata: int
12
+ unit: str
13
+
14
+ class Config:
15
+ """Pydantic configuration."""
16
+
17
+ populate_by_name = True
@@ -0,0 +1 @@
1
+ """Uploader package for interacting with the Uploader API, providing upload functionalities to the datacosmos cloud storage."""
@@ -0,0 +1 @@
1
+ """Dataclasses for the uploader module."""
@@ -0,0 +1,93 @@
1
+ """Dataclass for retrieving the upload path of a file."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+
7
+ import structlog
8
+
9
+ from datacosmos.stac.enums.level import Level
10
+ from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
11
+ from datacosmos.utils.missions import get_mission_id
12
+
13
+ logger = structlog.get_logger()
14
+
15
+
16
+ @dataclass
17
+ class UploadPath:
18
+ """Dataclass for retrieving the upload path of a file."""
19
+
20
+ mission: str
21
+ level: Level
22
+ day: int
23
+ month: int
24
+ year: int
25
+ id: str
26
+ path: str
27
+
28
+ def __str__(self):
29
+ """Return a human-readable string representation of the Path."""
30
+ path = f"full/{self.mission.lower()}/{self.level.value.lower()}/{self.year:02}/{self.month:02}/{self.day:02}/{self.id}/{self.path}"
31
+ return path.removesuffix("/")
32
+
33
+ @classmethod
34
+ def from_item_path(
35
+ cls, item: DatacosmosItem, mission: str, item_path: str
36
+ ) -> "Path":
37
+ """Create a Path instance from a DatacosmosItem and a path."""
38
+ for asset in item.assets.values():
39
+ if mission == "":
40
+ mission = cls._get_mission_name(asset.href)
41
+ else:
42
+ break
43
+ dt = datetime.strptime(item.properties["datetime"], "%Y-%m-%dT%H:%M:%SZ")
44
+ path = UploadPath(
45
+ mission=mission,
46
+ level=Level(item.properties["processing:level"].lower()),
47
+ day=dt.day,
48
+ month=dt.month,
49
+ year=dt.year,
50
+ id=item.id,
51
+ path=item_path,
52
+ )
53
+ return cls(**path.__dict__)
54
+
55
+ @classmethod
56
+ def from_path(cls, path: str) -> "Path":
57
+ """Create a Path instance from a string path."""
58
+ parts = path.split("/")
59
+ if len(parts) < 7:
60
+ raise ValueError(f"Invalid path {path}")
61
+ return cls(
62
+ mission=parts[0],
63
+ level=Level(parts[1]),
64
+ day=int(parts[4]),
65
+ month=int(parts[3]),
66
+ year=int(parts[2]),
67
+ id=parts[5],
68
+ path="/".join(parts[6:]),
69
+ )
70
+
71
+ @classmethod
72
+ def _get_mission_name(cls, href: str) -> str:
73
+ mission = ""
74
+ # bruteforce mission name from asset path
75
+ # traverse the path and check if any part is a mission name (generates a mission id)
76
+ href_parts = href.split("/")
77
+ for idx, part in enumerate(href_parts):
78
+ try:
79
+ # when an id is found, then the mission name is valid
80
+ get_mission_id(
81
+ part, "test"
82
+ ) # using test as it is more wide and anything on prod should exists on test
83
+ except KeyError:
84
+ continue
85
+ # validate the mission name by checking if the path is correct
86
+ # using the same logic as the __str__ method
87
+ mission = part.lower()
88
+ h = "/".join(["full", *href_parts[idx:]])
89
+ p = UploadPath.from_path("/".join([mission, *href_parts[idx + 1 :]]))
90
+ if str(p) != h:
91
+ raise ValueError(f"Could not find mission name in asset path {href}")
92
+ break
93
+ return mission
@@ -0,0 +1,106 @@
1
+ """Module for uploading files to Datacosmos cloud storage and registering STAC items."""
2
+
3
+ from concurrent.futures import ThreadPoolExecutor
4
+ from pathlib import Path
5
+
6
+ from pydantic import TypeAdapter
7
+
8
+ from datacosmos.datacosmos_client import DatacosmosClient
9
+ from datacosmos.stac.item.item_client import ItemClient
10
+ from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
11
+ from datacosmos.uploader.dataclasses.upload_path import UploadPath
12
+ from datacosmos.utils.missions import get_mission_name
13
+
14
+
15
+ class DatacosmosUploader:
16
+ """Handles uploading files to Datacosmos storage and registering STAC items."""
17
+
18
+ def __init__(self, client: DatacosmosClient):
19
+ """Initialize the uploader with DatacosmosClient."""
20
+ mission_id = client.config.mission_id
21
+ environment = client.config.environment
22
+
23
+ self.datacosmos_client = client
24
+ self.item_client = ItemClient(client)
25
+ self.mission_name = (
26
+ get_mission_name(mission_id, environment) if mission_id != 0 else ""
27
+ )
28
+ self.base_url = client.config.datacosmos_cloud_storage.as_domain_url()
29
+
30
+ def upload_and_register_item(self, item_json_file_path: str) -> None:
31
+ """Uploads files to Datacosmos storage and registers a STAC item.
32
+
33
+ Args:
34
+ item_json_file_path (str): Path to the STAC item JSON file.
35
+ """
36
+ item = self._load_item(item_json_file_path)
37
+ collection_id, item_id = item.collection, item.id
38
+ dirname = str(Path(item_json_file_path).parent / Path(item_json_file_path).stem)
39
+
40
+ self._delete_existing_item(collection_id, item_id)
41
+ upload_path = self._get_upload_path(item)
42
+ self.upload_from_folder(dirname, upload_path)
43
+
44
+ self._update_item_assets(item)
45
+
46
+ self.item_client.create_item(collection_id, item)
47
+
48
+ def upload_file(self, src: str, dst: str) -> None:
49
+ """Uploads a single file to the specified destination path."""
50
+ url = self.base_url.with_suffix(str(dst))
51
+
52
+ with open(src, "rb") as f:
53
+ response = self.datacosmos_client.put(url, data=f)
54
+ response.raise_for_status()
55
+
56
+ def upload_from_folder(self, src: str, dst: UploadPath, workers: int = 4) -> None:
57
+ """Uploads all files from a folder to the destination path in parallel."""
58
+ if Path(dst.path).is_file():
59
+ raise ValueError(f"Destination path should not be a file path {dst}")
60
+
61
+ if Path(src).is_file():
62
+ raise ValueError(f"Source path should not be a file path {src}")
63
+
64
+ with ThreadPoolExecutor(max_workers=workers) as executor:
65
+ futures = []
66
+ for file in Path(src).rglob("*"):
67
+ if file.is_file():
68
+ dst = UploadPath(
69
+ mission=dst.mission,
70
+ level=dst.level,
71
+ day=dst.day,
72
+ month=dst.month,
73
+ year=dst.year,
74
+ id=dst.id,
75
+ path=str(file.relative_to(src)),
76
+ )
77
+ futures.append(executor.submit(self.upload_file, str(file), dst))
78
+ for future in futures:
79
+ future.result()
80
+
81
+ @staticmethod
82
+ def _load_item(item_json_file_path: str) -> DatacosmosItem:
83
+ """Loads and validates the STAC item from a JSON file."""
84
+ with open(item_json_file_path, "rb") as file:
85
+ data = file.read().decode("utf-8")
86
+ return TypeAdapter(DatacosmosItem).validate_json(data)
87
+
88
+ def _delete_existing_item(self, collection_id: str, item_id: str) -> None:
89
+ """Deletes an existing item if it already exists."""
90
+ try:
91
+ self.item_client.delete_item(item_id, collection_id)
92
+ except Exception: # nosec
93
+ pass # Ignore if item doesn't exist
94
+
95
+ def _get_upload_path(self, item: DatacosmosItem) -> str:
96
+ """Constructs the storage upload path based on the item and mission name."""
97
+ return UploadPath.from_item_path(item, self.mission_name, "")
98
+
99
+ def _update_item_assets(self, item: DatacosmosItem) -> None:
100
+ """Updates the item's assets with uploaded file URLs."""
101
+ for asset in item.assets.values():
102
+ try:
103
+ url = self.base_url
104
+ asset.href = url.with_base(asset.href) # type: ignore
105
+ except ValueError:
106
+ pass
@@ -0,0 +1,16 @@
1
+ """Package for storing constants."""
2
+
3
+ TEST_MISSION_NAMES = {
4
+ 55: "MENUT",
5
+ 56: "PHISAT-2",
6
+ 57: "HAMMER",
7
+ 63: "MANTIS",
8
+ 64: "PLATERO",
9
+ }
10
+ PROD_MISSION_NAMES = {
11
+ 23: "MENUT",
12
+ 29: "MANTIS",
13
+ 35: "PHISAT-2",
14
+ 37: "PLATERO",
15
+ 48: "HAMMER",
16
+ }
@@ -0,0 +1,27 @@
1
+ """Package for storing mission specific information."""
2
+
3
+ from datacosmos.utils.constants import PROD_MISSION_NAMES, TEST_MISSION_NAMES
4
+
5
+
6
+ def get_mission_name(mission: int, env: str) -> str:
7
+ """Get the mission name from the mission number."""
8
+ if env == "test" or env == "local":
9
+ return TEST_MISSION_NAMES[mission]
10
+ elif env == "prod":
11
+ return PROD_MISSION_NAMES[mission]
12
+ else:
13
+ raise ValueError(f"Unsupported environment: {env}")
14
+
15
+
16
+ def get_mission_id(mission_name: str, env: str) -> int:
17
+ """Get the mission number from the mission name."""
18
+ if env == "test" or env == "local":
19
+ return {v.upper(): k for k, v in TEST_MISSION_NAMES.items()}[
20
+ mission_name.upper()
21
+ ]
22
+ elif env == "prod":
23
+ return {v.upper(): k for k, v in PROD_MISSION_NAMES.items()}[
24
+ mission_name.upper()
25
+ ]
26
+ else:
27
+ raise ValueError(f"Unsupported environment: {env}")
@@ -35,3 +35,26 @@ class URL:
35
35
  """
36
36
  base = self.string()
37
37
  return f"{base.rstrip('/')}/{suffix.lstrip('/')}"
38
+
39
+ def with_base(self, url: str) -> str:
40
+ """Replaces the base of the url with the base stored in the URL object. (migrates url from one base to another).
41
+
42
+ Args:
43
+ url (str): url to migrate to the base of the URL object
44
+
45
+ Returns (str):
46
+ url with the base of the URL object
47
+ """
48
+ split_url = url.split("/")
49
+ if len(split_url) < 3 or url.find("://") == -1:
50
+ raise ValueError(f"URL '{url}' does not meet the minimum requirements")
51
+ # get the whole path
52
+ url_path = "/".join(split_url[3:])
53
+ # simple case, matching self.base at url
54
+ b = self.base.lstrip("/")
55
+ if (base_pos := url_path.find(b)) != -1:
56
+ # remove the base from the url
57
+ url_suffix = url_path[len(b) + base_pos :]
58
+ else:
59
+ url_suffix = url_path
60
+ return self.with_suffix(url_suffix)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: datacosmos
3
- Version: 0.0.2
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
@@ -21,12 +21,24 @@ datacosmos/stac/collection/__init__.py
21
21
  datacosmos/stac/collection/collection_client.py
22
22
  datacosmos/stac/collection/models/__init__.py
23
23
  datacosmos/stac/collection/models/collection_update.py
24
+ datacosmos/stac/enums/__init__.py
25
+ datacosmos/stac/enums/level.py
24
26
  datacosmos/stac/item/__init__.py
25
27
  datacosmos/stac/item/item_client.py
26
28
  datacosmos/stac/item/models/__init__.py
29
+ datacosmos/stac/item/models/asset.py
30
+ datacosmos/stac/item/models/datacosmos_item.py
31
+ datacosmos/stac/item/models/eo_band.py
27
32
  datacosmos/stac/item/models/item_update.py
33
+ datacosmos/stac/item/models/raster_band.py
28
34
  datacosmos/stac/item/models/search_parameters.py
35
+ datacosmos/uploader/__init__.py
36
+ datacosmos/uploader/datacosmos_uploader.py
37
+ datacosmos/uploader/dataclasses/__init__.py
38
+ datacosmos/uploader/dataclasses/upload_path.py
29
39
  datacosmos/utils/__init__.py
40
+ datacosmos/utils/constants.py
41
+ datacosmos/utils/missions.py
30
42
  datacosmos/utils/url.py
31
43
  datacosmos/utils/http_response/__init__.py
32
44
  datacosmos/utils/http_response/check_api_response.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "datacosmos"
7
- version = "0.0.2"
7
+ version = "0.0.3"
8
8
  authors = [
9
9
  { name="Open Cosmos", email="support@open-cosmos.com" },
10
10
  ]
@@ -1,257 +0,0 @@
1
- # DataCosmos SDK
2
-
3
- ## Overview
4
-
5
- The **DataCosmos SDK** allows Open Cosmos' customers to interact with the **DataCosmos APIs** for seamless 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 via PyPI
10
-
11
- The easiest way to install the SDK is via **pip**:
12
-
13
- ```sh
14
- pip install datacosmos=={version}
15
- ```
16
-
17
- ## Getting Started
18
-
19
- ### Initializing the Client
20
-
21
- The recommended way to initialize the SDK is by passing a `Config` object with authentication credentials:
22
-
23
- ```python
24
- from datacosmos.datacosmos_client import DatacosmosClient
25
- from config.config import Config
26
-
27
- config = Config(
28
- authentication={
29
- "client_id": "your_client_id",
30
- "client_secret": "your_client_secret",
31
- "token_url": "https://login.open-cosmos.com/oauth/token",
32
- "audience": "https://beeapp.open-cosmos.com"
33
- }
34
- )
35
- client = DatacosmosClient(config=config)
36
- ```
37
-
38
- Alternatively, the SDK can load configuration automatically from:
39
-
40
- - A YAML file (`config/config.yaml`)
41
- - Environment variables
42
-
43
- ### STAC Client
44
-
45
- The STACClient enables interaction with the STAC API, allowing for searching, retrieving, creating, updating, and deleting STAC items and collections.
46
-
47
- #### Initialize STACClient
48
-
49
- ```python
50
- from datacosmos.stac.stac_client import STACClient
51
-
52
- stac_client = STACClient(client)
53
- ```
54
-
55
- ### STACClient Methods
56
-
57
- #### 1. **Fetch a Collection**
58
-
59
- ```python
60
- from datacosmos.stac.stac_client import STACClient
61
- from datacosmos.datacosmos_client import DatacosmosClient
62
-
63
- datacosmos_client = DatacosmosClient()
64
- stac_client = STACClient(datacosmos_client)
65
-
66
- collection = stac_client.fetch_collection("test-collection")
67
- ```
68
-
69
- #### 2. **Fetch All Collections**
70
-
71
- ```python
72
- collections = list(stac_client.fetch_all_collections())
73
- ```
74
-
75
- #### 3. **Create a Collection**
76
-
77
- ```python
78
- from pystac import Collection
79
-
80
- new_collection = Collection(
81
- id="test-collection",
82
- title="Test Collection",
83
- description="This is a test collection",
84
- license="proprietary",
85
- extent={
86
- "spatial": {"bbox": [[-180, -90, 180, 90]]},
87
- "temporal": {"interval": [["2023-01-01T00:00:00Z", None]]},
88
- },
89
- )
90
-
91
- stac_client.create_collection(new_collection)
92
- ```
93
-
94
- #### 4. **Update a Collection**
95
-
96
- ```python
97
- from datacosmos.stac.collection.models.collection_update import CollectionUpdate
98
-
99
- update_data = CollectionUpdate(
100
- title="Updated Collection Title version 2",
101
- description="Updated description version 2",
102
- )
103
-
104
- stac_client.update_collection("test-collection", update_data)
105
- ```
106
-
107
- #### 5. **Delete a Collection**
108
-
109
- ```python
110
- collection_id = "test-collection"
111
- stac_client.delete_collection(collection_id)
112
- ```
113
-
114
- #### 1. **Search Items**
115
-
116
- ```python
117
- from datacosmos.stac.item.models.search_parameters import SearchParameters
118
-
119
- parameters = SearchParameters(collections=["example-collection"], limit=1)
120
- items = list(stac_client.search_items(parameters=parameters))
121
- ```
122
-
123
- #### 2. **Fetch a Single Item**
124
-
125
- ```python
126
- item = stac_client.fetch_item(item_id="example-item", collection_id="example-collection")
127
- ```
128
-
129
- #### 3. **Fetch All Items in a Collection**
130
-
131
- ```python
132
- items = stac_client.fetch_collection_items(collection_id="example-collection")
133
- ```
134
-
135
- #### 4. **Create a New STAC Item**
136
-
137
- ```python
138
- from datetime import datetime
139
- from pystac import Item, Asset
140
- from pystac.utils import str_to_datetime
141
-
142
- stac_item = Item(
143
- id="MENUT_000001418_20240211120920_20240211120932_new_release.tiff",
144
- geometry={
145
- "type": "Polygon",
146
- "coordinates": [
147
- [
148
- [-24.937406454761664, 64.5931773445667],
149
- [-19.6596824245997, 64.5931773445667],
150
- [-19.6596824245997, 63.117895100111724],
151
- [-24.937406454761664, 63.117895100111724],
152
- [-24.937406454761664, 64.5931773445667]
153
- ]
154
- ]
155
- },
156
- bbox=[
157
- -24.937406454761664,
158
- 63.117895100111724,
159
- -19.6596824245997,
160
- 64.5931773445667
161
- ],
162
- datetime=str_to_datetime("2024-02-11T12:09:32Z"),
163
- properties={"processing:level": "L0"},
164
- collection="menut-l0",
165
- )
166
-
167
- stac_item.add_asset(
168
- "thumbnail",
169
- Asset(
170
- href="https://test.app.open-cosmos.com/api/data/v0/storage/full/menut/l0/2024/02/11/MENUT_000001418_20240211120920_20240211120932.tiff/thumbnail.webp",
171
- media_type="image/webp",
172
- roles=["thumbnail"],
173
- title="Thumbnail",
174
- description="Thumbnail of the image"
175
- )
176
- )
177
-
178
- stac_client.create_item(collection_id="menutl-l0", item=stac_item)
179
- ```
180
-
181
- #### 5. **Update an Existing STAC Item**
182
-
183
- ```python
184
- from datacosmos.stac.item.models.item_update import ItemUpdate
185
- from pystac import Asset, Link
186
-
187
- update_payload = ItemUpdate(
188
- properties={
189
- "new_property": "updated_value",
190
- "datetime": "2024-11-10T14:58:00Z"
191
- },
192
- assets={
193
- "image": Asset(
194
- href="https://example.com/updated-image.tiff",
195
- media_type="image/tiff"
196
- )
197
- },
198
- links=[
199
- Link(rel="self", target="https://example.com/updated-image.tiff")
200
- ],
201
- geometry={
202
- "type": "Point",
203
- "coordinates": [10, 20]
204
- },
205
- bbox=[10.0, 20.0, 30.0, 40.0]
206
- )
207
-
208
- stac_client.update_item(item_id="new-item", collection_id="example-collection", update_data=update_payload)
209
- ```
210
-
211
- #### 6. **Delete an Item**
212
-
213
- ```python
214
- stac_client.delete_item(item_id="new-item", collection_id="example-collection")
215
- ```
216
-
217
- ## Configuration Options
218
-
219
- - **Recommended:** Instantiate `DatacosmosClient` with a `Config` object.
220
- - Alternatively, use **YAML files** (`config/config.yaml`).
221
- - Or, use **environment variables**.
222
-
223
- ## Contributing
224
-
225
- If you would like to contribute:
226
-
227
- 1. Fork the repository.
228
- 2. Create a feature branch.
229
- 3. Submit a pull request.
230
-
231
- ### Development Setup
232
-
233
- If you are developing the SDK, you can use `uv` for dependency management:
234
-
235
- ```sh
236
- pip install uv
237
- uv venv
238
- uv pip install -r pyproject.toml
239
- uv pip install -r pyproject.toml .[dev]
240
- source .venv/bin/activate
241
- ```
242
-
243
- Before making changes, ensure that:
244
-
245
- - The code is formatted using **Black** and **isort**.
246
- - Static analysis and linting are performed using **ruff** and **pydocstyle**.
247
- - Security checks are performed using **bandit**.
248
- - Tests are executed with **pytest**.
249
-
250
- ```sh
251
- black .
252
- isort .
253
- ruff check . --select C901
254
- pydocstyle .
255
- bandit -r -c pyproject.toml . --skip B105,B106,B101
256
- pytest
257
- ```
File without changes
File without changes