datacosmos 0.0.2__py3-none-any.whl → 0.0.4__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.
- config/config.py +56 -28
- datacosmos/stac/collection/collection_client.py +6 -2
- datacosmos/stac/collection/models/collection_update.py +1 -0
- datacosmos/stac/constants/__init__.py +1 -0
- datacosmos/stac/constants/satellite_name_mapping.py +20 -0
- datacosmos/stac/enums/__init__.py +1 -0
- datacosmos/stac/enums/processing_level.py +15 -0
- datacosmos/stac/enums/product_type.py +11 -0
- datacosmos/stac/enums/season.py +14 -0
- datacosmos/stac/item/item_client.py +11 -4
- datacosmos/stac/item/models/asset.py +23 -0
- datacosmos/stac/item/models/catalog_search_parameters.py +132 -0
- datacosmos/stac/item/models/datacosmos_item.py +55 -0
- datacosmos/stac/item/models/eo_band.py +15 -0
- datacosmos/stac/item/models/raster_band.py +17 -0
- datacosmos/uploader/__init__.py +1 -0
- datacosmos/uploader/dataclasses/__init__.py +1 -0
- datacosmos/uploader/dataclasses/upload_path.py +93 -0
- datacosmos/uploader/datacosmos_uploader.py +106 -0
- datacosmos/utils/constants.py +16 -0
- datacosmos/utils/missions.py +27 -0
- datacosmos/utils/url.py +23 -0
- {datacosmos-0.0.2.dist-info → datacosmos-0.0.4.dist-info}/METADATA +4 -2
- datacosmos-0.0.4.dist-info/RECORD +49 -0
- {datacosmos-0.0.2.dist-info → datacosmos-0.0.4.dist-info}/WHEEL +1 -1
- datacosmos-0.0.2.dist-info/RECORD +0 -32
- {datacosmos-0.0.2.dist-info → datacosmos-0.0.4.dist-info/licenses}/LICENSE.md +0 -0
- {datacosmos-0.0.2.dist-info → datacosmos-0.0.4.dist-info}/top_level.txt +0 -0
config/config.py
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
|
132
|
-
auth.token_url = auth.token_url or
|
|
133
|
-
auth.audience = auth.audience or
|
|
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
|
|
@@ -6,6 +6,7 @@ from pystac import Collection, Extent, SpatialExtent, TemporalExtent
|
|
|
6
6
|
from pystac.utils import str_to_datetime
|
|
7
7
|
|
|
8
8
|
from datacosmos.datacosmos_client import DatacosmosClient
|
|
9
|
+
from datacosmos.exceptions.datacosmos_exception import DatacosmosException
|
|
9
10
|
from datacosmos.stac.collection.models.collection_update import CollectionUpdate
|
|
10
11
|
from datacosmos.utils.http_response.check_api_response import check_api_response
|
|
11
12
|
|
|
@@ -145,5 +146,8 @@ class CollectionClient:
|
|
|
145
146
|
"""
|
|
146
147
|
try:
|
|
147
148
|
return next_href.split("?")[1].split("=")[-1]
|
|
148
|
-
except (IndexError, AttributeError):
|
|
149
|
-
raise
|
|
149
|
+
except (IndexError, AttributeError) as e:
|
|
150
|
+
raise DatacosmosException(
|
|
151
|
+
f"Failed to parse pagination token from {next_href}",
|
|
152
|
+
response=e.response,
|
|
153
|
+
) from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Constants for STAC."""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Satellite name mapping."""
|
|
2
|
+
|
|
3
|
+
SATELLITE_NAME_MAPPING = {
|
|
4
|
+
"GEOSAT-2": "2014-033D",
|
|
5
|
+
"SUPERVIEW-1-01": "2016-083A",
|
|
6
|
+
"SUPERVIEW-1-02": "2016-083B",
|
|
7
|
+
"SUPERVIEW-1-03": "2018-002A",
|
|
8
|
+
"SUPERVIEW-1-04": "2018-002B",
|
|
9
|
+
"MANTIS": "2023-174B",
|
|
10
|
+
"MENUT": "2023-001B",
|
|
11
|
+
"HAMMER": "2024-043BC",
|
|
12
|
+
"HAMMER-EM": "COSPAR-HAMMER-EM-TBD",
|
|
13
|
+
"Alisio": "2023-185M",
|
|
14
|
+
"Platero": "2023-174G",
|
|
15
|
+
"PHISAT-2": "2024-149C",
|
|
16
|
+
"PHISAT-2 EM": "COSPAR-PHISAT2-EM-TBD",
|
|
17
|
+
"Sentinel-2A": "2015-028A",
|
|
18
|
+
"Sentinel-2B": "2017-013A",
|
|
19
|
+
"Sentinel-2C": "2024-157A",
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Enums for STAC."""
|
|
@@ -9,6 +9,10 @@ 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.catalog_search_parameters import (
|
|
13
|
+
CatalogSearchParameters,
|
|
14
|
+
)
|
|
15
|
+
from datacosmos.stac.item.models.datacosmos_item import DatacosmosItem
|
|
12
16
|
from datacosmos.stac.item.models.item_update import ItemUpdate
|
|
13
17
|
from datacosmos.stac.item.models.search_parameters import SearchParameters
|
|
14
18
|
from datacosmos.utils.http_response.check_api_response import check_api_response
|
|
@@ -58,20 +62,23 @@ class ItemClient:
|
|
|
58
62
|
|
|
59
63
|
return self.search_items(parameters)
|
|
60
64
|
|
|
61
|
-
def search_items(
|
|
65
|
+
def search_items(
|
|
66
|
+
self, parameters: CatalogSearchParameters, project_id: str
|
|
67
|
+
) -> Generator[Item, None, None]:
|
|
62
68
|
"""Query the STAC catalog using the POST endpoint with filtering and pagination.
|
|
63
69
|
|
|
64
70
|
Args:
|
|
65
|
-
parameters (
|
|
71
|
+
parameters (CatalogSearchParameters): The search parameters.
|
|
66
72
|
|
|
67
73
|
Yields:
|
|
68
74
|
Item: Parsed STAC item.
|
|
69
75
|
"""
|
|
70
76
|
url = self.base_url.with_suffix("/search")
|
|
71
|
-
|
|
77
|
+
parameters_query = parameters.to_query()
|
|
78
|
+
body = {"project": project_id, "limit": 50, "query": parameters_query}
|
|
72
79
|
return self._paginate_items(url, body)
|
|
73
80
|
|
|
74
|
-
def create_item(self, collection_id: str, item: Item) -> None:
|
|
81
|
+
def create_item(self, collection_id: str, item: Item | DatacosmosItem) -> None:
|
|
75
82
|
"""Create a new STAC item in a specified collection.
|
|
76
83
|
|
|
77
84
|
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,132 @@
|
|
|
1
|
+
"""Query parameters for catalog search."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from typing import Any, List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from datacosmos.stac.constants.satellite_name_mapping import SATELLITE_NAME_MAPPING
|
|
9
|
+
from datacosmos.stac.enums.processing_level import ProcessingLevel
|
|
10
|
+
from datacosmos.stac.enums.product_type import ProductType
|
|
11
|
+
from datacosmos.stac.enums.season import Season
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CatalogSearchParameters(BaseModel):
|
|
15
|
+
"""Query parameters for catalog search."""
|
|
16
|
+
|
|
17
|
+
start_date: Optional[str] = None
|
|
18
|
+
end_date: Optional[str] = None
|
|
19
|
+
seasons: Optional[List[Season]] = None
|
|
20
|
+
satellite: Optional[List[str]] = None
|
|
21
|
+
product_type: Optional[List[ProductType]] = None
|
|
22
|
+
processing_level: Optional[List[ProcessingLevel]] = None
|
|
23
|
+
|
|
24
|
+
# --- Field Validators ---
|
|
25
|
+
|
|
26
|
+
@field_validator("seasons", mode="before")
|
|
27
|
+
@classmethod
|
|
28
|
+
def parse_seasons(cls, value):
|
|
29
|
+
"""Parses seasons values into a list of Season object."""
|
|
30
|
+
if value is None:
|
|
31
|
+
return None
|
|
32
|
+
return [Season(v) if not isinstance(v, Season) else v for v in value]
|
|
33
|
+
|
|
34
|
+
@field_validator("product_type", mode="before")
|
|
35
|
+
@classmethod
|
|
36
|
+
def parse_product_types(cls, value):
|
|
37
|
+
"""Parses product types values into a list of ProductType object."""
|
|
38
|
+
if value is None:
|
|
39
|
+
return None
|
|
40
|
+
return [ProductType(v) if not isinstance(v, ProductType) else v for v in value]
|
|
41
|
+
|
|
42
|
+
@field_validator("processing_level", mode="before")
|
|
43
|
+
@classmethod
|
|
44
|
+
def parse_processing_levels(cls, value):
|
|
45
|
+
"""Parses processing levels values into a list of ProcessingLevel object."""
|
|
46
|
+
if value is None:
|
|
47
|
+
return None
|
|
48
|
+
return [
|
|
49
|
+
ProcessingLevel(v) if not isinstance(v, ProcessingLevel) else v
|
|
50
|
+
for v in value
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
@field_validator("start_date", mode="before")
|
|
54
|
+
@classmethod
|
|
55
|
+
def parse_start_date(cls, value: Any) -> Optional[str]:
|
|
56
|
+
"""Validations on start_date."""
|
|
57
|
+
if value is None:
|
|
58
|
+
return None
|
|
59
|
+
try:
|
|
60
|
+
dt = datetime.strptime(value, "%m/%d/%Y")
|
|
61
|
+
if dt < datetime(2015, 5, 15):
|
|
62
|
+
raise ValueError("Date must be 5/15/2015 or later.")
|
|
63
|
+
return dt.isoformat() + "Z"
|
|
64
|
+
except ValueError:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
"Invalid start_date format. Use mm/dd/yyyy (e.g., 05/15/2024)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@field_validator("end_date", mode="before")
|
|
70
|
+
@classmethod
|
|
71
|
+
def parse_end_date(cls, value: Any) -> Optional[str]:
|
|
72
|
+
"""Validations on end_date."""
|
|
73
|
+
if value is None:
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
dt = datetime.strptime(value, "%m/%d/%Y")
|
|
77
|
+
if dt < datetime(2015, 5, 15):
|
|
78
|
+
raise ValueError("Date must be 5/15/2015 or later.")
|
|
79
|
+
dt = dt + timedelta(days=1) - timedelta(milliseconds=1)
|
|
80
|
+
return dt.isoformat() + "Z"
|
|
81
|
+
except ValueError:
|
|
82
|
+
raise ValueError(
|
|
83
|
+
"Invalid end_date format. Use mm/dd/yyyy (e.g., 05/15/2024)"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# --- Model Validator ---
|
|
87
|
+
|
|
88
|
+
@model_validator(mode="after")
|
|
89
|
+
def validate_date_range(self) -> "CatalogSearchParameters":
|
|
90
|
+
"""Checks if end_date is after the start_date."""
|
|
91
|
+
if self.start_date and self.end_date:
|
|
92
|
+
start_dt = datetime.fromisoformat(self.start_date.rstrip("Z"))
|
|
93
|
+
end_dt = datetime.fromisoformat(self.end_date.rstrip("Z"))
|
|
94
|
+
if start_dt > end_dt:
|
|
95
|
+
raise ValueError("end_date cannot be before start_date.")
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
# --- Query Mapper ---
|
|
99
|
+
|
|
100
|
+
def to_query(self) -> dict:
|
|
101
|
+
"""Map user-friendly input to STAC query structure."""
|
|
102
|
+
query = {}
|
|
103
|
+
|
|
104
|
+
if self.start_date or self.end_date:
|
|
105
|
+
query["datetime"] = {"gte": self.start_date, "lte": self.end_date}
|
|
106
|
+
|
|
107
|
+
if self.seasons:
|
|
108
|
+
query["opencosmos:season"] = {
|
|
109
|
+
"in": [seasons.value for seasons in self.seasons]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if self.product_type:
|
|
113
|
+
query["opencosmos:product_type"] = {
|
|
114
|
+
"in": [product_type.value for product_type in self.product_type]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if self.processing_level:
|
|
118
|
+
query["processing:level"] = {
|
|
119
|
+
"in": [
|
|
120
|
+
processing_level.value for processing_level in self.processing_level
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if self.satellite:
|
|
125
|
+
cospars = [
|
|
126
|
+
SATELLITE_NAME_MAPPING[ui]
|
|
127
|
+
for ui in self.satellite
|
|
128
|
+
if ui in SATELLITE_NAME_MAPPING
|
|
129
|
+
]
|
|
130
|
+
query["sat:platform_international_designator"] = {"in": cospars}
|
|
131
|
+
|
|
132
|
+
return query
|
|
@@ -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.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()
|
|
@@ -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}")
|
datacosmos/utils/url.py
CHANGED
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: datacosmos
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
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
|
|
@@ -13,6 +13,7 @@ Requires-Dist: oauthlib==3.2.0
|
|
|
13
13
|
Requires-Dist: requests-oauthlib==1.3.1
|
|
14
14
|
Requires-Dist: pydantic==2.10.6
|
|
15
15
|
Requires-Dist: pystac==1.12.1
|
|
16
|
+
Requires-Dist: pyyaml==6.0.2
|
|
16
17
|
Provides-Extra: dev
|
|
17
18
|
Requires-Dist: black==22.3.0; extra == "dev"
|
|
18
19
|
Requires-Dist: ruff==0.9.5; extra == "dev"
|
|
@@ -20,3 +21,4 @@ Requires-Dist: pytest==7.2.0; extra == "dev"
|
|
|
20
21
|
Requires-Dist: bandit[toml]==1.7.4; extra == "dev"
|
|
21
22
|
Requires-Dist: isort==5.11.4; extra == "dev"
|
|
22
23
|
Requires-Dist: pydocstyle==6.1.1; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
config/__init__.py,sha256=KCsaTb9-ZgFui1GM8wZFIPLJy0D0O8l8Z1Sv3NRD9UM,140
|
|
2
|
+
config/config.py,sha256=h5XwNSA6QFBCDyennyFDNMAmbQOdtg8DsFAvjHlSEx4,7233
|
|
3
|
+
config/models/__init__.py,sha256=r3lThPkyKjBjUZXRNscFzOrmn_-m_i9DvG3RePfCFYc,41
|
|
4
|
+
config/models/m2m_authentication_config.py,sha256=1eJ_9df7Twn5WeWPbqMuR63ZdxhnTpBtKzqAPMnzP_k,565
|
|
5
|
+
config/models/url.py,sha256=fwr2C06e_RDS8AWxOV_orVxMWhc57bzYoWSjFxQbkwg,835
|
|
6
|
+
datacosmos/__init__.py,sha256=dVHKpbz5FVtfoJAWHRdsUENG6H-vs4UrkuwnIvOGJr4,66
|
|
7
|
+
datacosmos/datacosmos_client.py,sha256=sivVYf45QEHTkUO62fnb1fnObKVmUngTR1Ga-ZRnoQE,4967
|
|
8
|
+
datacosmos/exceptions/__init__.py,sha256=Crz8W7mOvPUXYcfDVotvjUt_3HKawBpmJA_-uel9UJk,45
|
|
9
|
+
datacosmos/exceptions/datacosmos_exception.py,sha256=rKjJvQDvCEbxXWWccxB5GI_sth662bW8Yml0hX-vRw4,923
|
|
10
|
+
datacosmos/stac/__init__.py,sha256=B4x_Mr4X7TzQoYtRC-VzI4W-fEON5WUOaz8cWJbk3Fc,214
|
|
11
|
+
datacosmos/stac/stac_client.py,sha256=Cz_p96RmAgWX8t7Sye4OJRanQpCLihKStvfEw7IgYZc,472
|
|
12
|
+
datacosmos/stac/collection/__init__.py,sha256=VQMLnsU3sER5kh4YxHrHP7XCA3DG1y0n9yoSmvycOY0,212
|
|
13
|
+
datacosmos/stac/collection/collection_client.py,sha256=-Nn3yqL4mQS05YAMd0IUmv03hdHKYBtVG2_EqoaAQWc,6064
|
|
14
|
+
datacosmos/stac/collection/models/__init__.py,sha256=TQaihUS_CM9Eaekm4SbzFTNfv7BmabHv3Z-f37Py5Qs,40
|
|
15
|
+
datacosmos/stac/collection/models/collection_update.py,sha256=XC6-29nLz1VGWMxYAw7r1OuL8PdJ3b2oI-RPvnM-XXI,1657
|
|
16
|
+
datacosmos/stac/constants/__init__.py,sha256=dDRSsF7CKqNF44yIlNdE-PD1sp0Q5mhTEPT7hHIK7YE,26
|
|
17
|
+
datacosmos/stac/constants/satellite_name_mapping.py,sha256=EJqNdO9uW5B-sIeDF72AjnW7va5BM9mm4oNwijtl51w,575
|
|
18
|
+
datacosmos/stac/enums/__init__.py,sha256=GUEL2xGtdjsrszrxivs0X6daxkaZs2JsTu2JoBtsvB4,22
|
|
19
|
+
datacosmos/stac/enums/processing_level.py,sha256=5gHG-0kG5rCUxmXYwF3t94ALKk6zUqguOdyTL-jwgps,247
|
|
20
|
+
datacosmos/stac/enums/product_type.py,sha256=7lL0unJ1hxevW8Pepn9rmydUUWIORu2x4MEtp6rSFbA,196
|
|
21
|
+
datacosmos/stac/enums/season.py,sha256=QvUzXBYtPEfixhlbV0SAw2u_HK3tRFEnHKshJyIatdg,241
|
|
22
|
+
datacosmos/stac/item/__init__.py,sha256=lRuD_yp-JxoLqBA23q0XMkCNImf4T-X3BJnSw9u_3Yk,200
|
|
23
|
+
datacosmos/stac/item/item_client.py,sha256=ib848Pb2j6njvbx97vFFw7AWeKyBnBlK-05D3pFmIdU,7027
|
|
24
|
+
datacosmos/stac/item/models/__init__.py,sha256=bcOrOcIxGxGBrRVIyQVxSM3C3Xj_qzxIHgQeWo6f7Q8,34
|
|
25
|
+
datacosmos/stac/item/models/asset.py,sha256=mvg_fenYCGOTMGwXXpK2nyqBk5RMsUYxl6KhQTWW_b0,631
|
|
26
|
+
datacosmos/stac/item/models/catalog_search_parameters.py,sha256=VKMBnaTYjpn_zAM6wdk3P69ZoGBcqdzPcTGBlYPFvVk,4704
|
|
27
|
+
datacosmos/stac/item/models/datacosmos_item.py,sha256=AImz0GRxrpZfIETdzzNfaKX35wpr39Q4f4u0z6r8eys,1745
|
|
28
|
+
datacosmos/stac/item/models/eo_band.py,sha256=YC3Scn_wFhIo51pIVcJeuJienF7JGWoEv39JngDM6rI,309
|
|
29
|
+
datacosmos/stac/item/models/item_update.py,sha256=_CpjQn9SsfedfuxlHSiGeptqY4M-p15t9YX__mBRueI,2088
|
|
30
|
+
datacosmos/stac/item/models/raster_band.py,sha256=CoEVs-YyPE5Fse0He9DdOs4dGZpzfCsCuVzOcdXa_UM,354
|
|
31
|
+
datacosmos/stac/item/models/search_parameters.py,sha256=yMmcb-Tr2as8585MD5wuZLWcqzwtRRkj07WBkootVS0,2022
|
|
32
|
+
datacosmos/uploader/__init__.py,sha256=ZtfCVJ_pWKKh2F1r_NArnbG3_JtpcEiXcA_tmSwSKmQ,128
|
|
33
|
+
datacosmos/uploader/datacosmos_uploader.py,sha256=LUtBDvAjZI7AYxKnC9TZQDP4z6lV2aHusz92XqivFGw,4398
|
|
34
|
+
datacosmos/uploader/dataclasses/__init__.py,sha256=IjcyA8Vod-z1_Gi1FMZhK58Owman0foL25Hs0YtkYYs,43
|
|
35
|
+
datacosmos/uploader/dataclasses/upload_path.py,sha256=WPl9u-oB-ti07ssKNDjL4vRQXhlOmLCgjt8MxFGrf3A,3153
|
|
36
|
+
datacosmos/utils/__init__.py,sha256=XQbAnoqJrPpnSpEzAbjh84yqYWw8cBM8mNp8ynTG-54,50
|
|
37
|
+
datacosmos/utils/constants.py,sha256=f7pOqCpdXk7WFGoaTyuCpr65jb-TtfhoVGuYTz3_T6Y,272
|
|
38
|
+
datacosmos/utils/missions.py,sha256=7GOnrjxB8V11C_Jr3HHI4vpXifgkOSeirNjIDx17C58,940
|
|
39
|
+
datacosmos/utils/url.py,sha256=iQwZr6mYRoePqUZg-k3KQSV9o2wju5ZuCa5WS_GyJo4,2114
|
|
40
|
+
datacosmos/utils/http_response/__init__.py,sha256=BvOWwC5coYqq_kFn8gIw5m54TLpdfJKlW9vgRkfhXiA,33
|
|
41
|
+
datacosmos/utils/http_response/check_api_response.py,sha256=dKWW01jn2_lWV0xpOBABhEP42CFSsx9dP0iSxykbN54,1186
|
|
42
|
+
datacosmos/utils/http_response/models/__init__.py,sha256=Wj8YT6dqw7rAz_rctllxo5Or_vv8DwopvQvBzwCTvpw,45
|
|
43
|
+
datacosmos/utils/http_response/models/datacosmos_error.py,sha256=Uqi2uM98nJPeCbM7zngV6vHSk97jEAb_nkdDEeUjiQM,740
|
|
44
|
+
datacosmos/utils/http_response/models/datacosmos_response.py,sha256=oV4n-sue7K1wwiIQeHpxdNU8vxeqF3okVPE2rydw5W0,336
|
|
45
|
+
datacosmos-0.0.4.dist-info/licenses/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
|
|
46
|
+
datacosmos-0.0.4.dist-info/METADATA,sha256=8yM44qsv1vBDxutLlUte916Z6zgPVT0mnyR1Bz2drJ8,872
|
|
47
|
+
datacosmos-0.0.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
|
48
|
+
datacosmos-0.0.4.dist-info/top_level.txt,sha256=Iu5b533Fmdfz0rFKTnuBPjSUOQL2lEkTfHxsokP72s4,18
|
|
49
|
+
datacosmos-0.0.4.dist-info/RECORD,,
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
config/__init__.py,sha256=KCsaTb9-ZgFui1GM8wZFIPLJy0D0O8l8Z1Sv3NRD9UM,140
|
|
2
|
-
config/config.py,sha256=0M2wKmrcCJjte3UmLNQVags_qce7Id2ampBPqadzPJw,5908
|
|
3
|
-
config/models/__init__.py,sha256=r3lThPkyKjBjUZXRNscFzOrmn_-m_i9DvG3RePfCFYc,41
|
|
4
|
-
config/models/m2m_authentication_config.py,sha256=1eJ_9df7Twn5WeWPbqMuR63ZdxhnTpBtKzqAPMnzP_k,565
|
|
5
|
-
config/models/url.py,sha256=fwr2C06e_RDS8AWxOV_orVxMWhc57bzYoWSjFxQbkwg,835
|
|
6
|
-
datacosmos/__init__.py,sha256=dVHKpbz5FVtfoJAWHRdsUENG6H-vs4UrkuwnIvOGJr4,66
|
|
7
|
-
datacosmos/datacosmos_client.py,sha256=sivVYf45QEHTkUO62fnb1fnObKVmUngTR1Ga-ZRnoQE,4967
|
|
8
|
-
datacosmos/exceptions/__init__.py,sha256=Crz8W7mOvPUXYcfDVotvjUt_3HKawBpmJA_-uel9UJk,45
|
|
9
|
-
datacosmos/exceptions/datacosmos_exception.py,sha256=rKjJvQDvCEbxXWWccxB5GI_sth662bW8Yml0hX-vRw4,923
|
|
10
|
-
datacosmos/stac/__init__.py,sha256=B4x_Mr4X7TzQoYtRC-VzI4W-fEON5WUOaz8cWJbk3Fc,214
|
|
11
|
-
datacosmos/stac/stac_client.py,sha256=Cz_p96RmAgWX8t7Sye4OJRanQpCLihKStvfEw7IgYZc,472
|
|
12
|
-
datacosmos/stac/collection/__init__.py,sha256=VQMLnsU3sER5kh4YxHrHP7XCA3DG1y0n9yoSmvycOY0,212
|
|
13
|
-
datacosmos/stac/collection/collection_client.py,sha256=XTO2s309-cktJosvnwnFFXHDVmJc4vjvbEsZjpsCDmY,5904
|
|
14
|
-
datacosmos/stac/collection/models/__init__.py,sha256=TQaihUS_CM9Eaekm4SbzFTNfv7BmabHv3Z-f37Py5Qs,40
|
|
15
|
-
datacosmos/stac/collection/models/collection_update.py,sha256=Tqmfg4H4UQj5jsgy1dpKJCR59NSfWeiCSi9y8CY8-Cg,1656
|
|
16
|
-
datacosmos/stac/item/__init__.py,sha256=lRuD_yp-JxoLqBA23q0XMkCNImf4T-X3BJnSw9u_3Yk,200
|
|
17
|
-
datacosmos/stac/item/item_client.py,sha256=AYyRR92Wy-rDwc9wFoljb6lAgSCuWft4VT5muqGeyv8,6738
|
|
18
|
-
datacosmos/stac/item/models/__init__.py,sha256=bcOrOcIxGxGBrRVIyQVxSM3C3Xj_qzxIHgQeWo6f7Q8,34
|
|
19
|
-
datacosmos/stac/item/models/item_update.py,sha256=_CpjQn9SsfedfuxlHSiGeptqY4M-p15t9YX__mBRueI,2088
|
|
20
|
-
datacosmos/stac/item/models/search_parameters.py,sha256=yMmcb-Tr2as8585MD5wuZLWcqzwtRRkj07WBkootVS0,2022
|
|
21
|
-
datacosmos/utils/__init__.py,sha256=XQbAnoqJrPpnSpEzAbjh84yqYWw8cBM8mNp8ynTG-54,50
|
|
22
|
-
datacosmos/utils/url.py,sha256=luaGa6UqPIf0h_1u2z3CZ32YQXNl7nGV03lVW7mlRIM,1214
|
|
23
|
-
datacosmos/utils/http_response/__init__.py,sha256=BvOWwC5coYqq_kFn8gIw5m54TLpdfJKlW9vgRkfhXiA,33
|
|
24
|
-
datacosmos/utils/http_response/check_api_response.py,sha256=dKWW01jn2_lWV0xpOBABhEP42CFSsx9dP0iSxykbN54,1186
|
|
25
|
-
datacosmos/utils/http_response/models/__init__.py,sha256=Wj8YT6dqw7rAz_rctllxo5Or_vv8DwopvQvBzwCTvpw,45
|
|
26
|
-
datacosmos/utils/http_response/models/datacosmos_error.py,sha256=Uqi2uM98nJPeCbM7zngV6vHSk97jEAb_nkdDEeUjiQM,740
|
|
27
|
-
datacosmos/utils/http_response/models/datacosmos_response.py,sha256=oV4n-sue7K1wwiIQeHpxdNU8vxeqF3okVPE2rydw5W0,336
|
|
28
|
-
datacosmos-0.0.2.dist-info/LICENSE.md,sha256=vpbRI-UUbZVQfr3VG_CXt9HpRnL1b5kt8uTVbirxeyI,1486
|
|
29
|
-
datacosmos-0.0.2.dist-info/METADATA,sha256=SIuvO1SE647Q9OvrKYuj1VmzWFgKjErvrRxrKoNgcyk,821
|
|
30
|
-
datacosmos-0.0.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
31
|
-
datacosmos-0.0.2.dist-info/top_level.txt,sha256=Iu5b533Fmdfz0rFKTnuBPjSUOQL2lEkTfHxsokP72s4,18
|
|
32
|
-
datacosmos-0.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|