cecil 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cecil might be problematic. Click here for more details.
- cecil/__init__.py +3 -0
- cecil/client.py +212 -0
- cecil/errors.py +85 -0
- cecil/models.py +122 -0
- cecil/version.py +1 -0
- cecil/xarray.py +131 -0
- cecil-0.1.3.dist-info/METADATA +25 -0
- cecil-0.1.3.dist-info/RECORD +10 -0
- cecil-0.1.3.dist-info/WHEEL +4 -0
- cecil-0.1.3.dist-info/licenses/LICENSE.txt +21 -0
cecil/__init__.py
ADDED
cecil/client.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from requests import auth
|
|
9
|
+
|
|
10
|
+
import xarray
|
|
11
|
+
from .errors import (
|
|
12
|
+
Error,
|
|
13
|
+
_handle_bad_request,
|
|
14
|
+
_handle_method_not_allowed,
|
|
15
|
+
_handle_not_found,
|
|
16
|
+
_handle_too_many_requests,
|
|
17
|
+
_handle_unprocessable_entity,
|
|
18
|
+
)
|
|
19
|
+
from .models import (
|
|
20
|
+
AOI,
|
|
21
|
+
AOICreate,
|
|
22
|
+
OrganisationSettings,
|
|
23
|
+
RecoverAPIKey,
|
|
24
|
+
RecoverAPIKeyRequest,
|
|
25
|
+
RotateAPIKey,
|
|
26
|
+
RotateAPIKeyRequest,
|
|
27
|
+
User,
|
|
28
|
+
UserCreate,
|
|
29
|
+
SubscriptionParquetFiles,
|
|
30
|
+
SubscriptionListFiles,
|
|
31
|
+
Subscription,
|
|
32
|
+
SubscriptionCreate,
|
|
33
|
+
)
|
|
34
|
+
from .version import __version__
|
|
35
|
+
from .xarray import load_xarray
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Client:
|
|
39
|
+
def __init__(self, env: str = None) -> None:
|
|
40
|
+
self._api_auth = None
|
|
41
|
+
self._base_url = (
|
|
42
|
+
"https://api.cecil.earth" if env is None else f"https://{env}.cecil.earth"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def create_aoi(self, geometry: Dict, external_ref: str = "") -> AOI:
|
|
46
|
+
# TODO: validate geometry
|
|
47
|
+
res = self._post(
|
|
48
|
+
url="/v0/aois",
|
|
49
|
+
model=AOICreate(geometry=geometry, external_ref=external_ref),
|
|
50
|
+
)
|
|
51
|
+
return AOI(**res)
|
|
52
|
+
|
|
53
|
+
def get_aoi(self, id: str) -> AOI:
|
|
54
|
+
res = self._get(url=f"/v0/aois/{id}")
|
|
55
|
+
return AOI(**res)
|
|
56
|
+
|
|
57
|
+
def list_aois(self) -> List[AOI]:
|
|
58
|
+
res = self._get(url="/v0/aois")
|
|
59
|
+
return [AOI(**record) for record in res["records"]]
|
|
60
|
+
|
|
61
|
+
def list_subscriptions(self) -> List[Subscription]:
|
|
62
|
+
res = self._get(url="/v0/subscriptions")
|
|
63
|
+
return [Subscription(**record) for record in res["records"]]
|
|
64
|
+
|
|
65
|
+
def create_subscription(
|
|
66
|
+
self, aoi_id: str, dataset_id: str, external_ref: str = ""
|
|
67
|
+
) -> Subscription:
|
|
68
|
+
res = self._post(
|
|
69
|
+
url="/v0/subscriptions",
|
|
70
|
+
model=SubscriptionCreate(
|
|
71
|
+
aoi_id=aoi_id, dataset_id=dataset_id, external_ref=external_ref
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return Subscription(**res)
|
|
76
|
+
|
|
77
|
+
def get_subscription(self, id: str) -> Subscription:
|
|
78
|
+
res = self._get(url=f"/v0/subscriptions/{id}")
|
|
79
|
+
return Subscription(**res)
|
|
80
|
+
|
|
81
|
+
def load_xarray(self, subscription_id: str) -> xarray.Dataset:
|
|
82
|
+
res = SubscriptionListFiles(
|
|
83
|
+
**self._get(url=f"/v0/subscriptions/{subscription_id}/files/tiff")
|
|
84
|
+
)
|
|
85
|
+
return load_xarray(res)
|
|
86
|
+
|
|
87
|
+
def load_dataframe(self, subscription_id: str) -> pd.DataFrame:
|
|
88
|
+
res = SubscriptionParquetFiles(
|
|
89
|
+
**self._get(url=f"/v0/subscriptions/{subscription_id}/parquet-files")
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if not res.files:
|
|
93
|
+
return pd.DataFrame()
|
|
94
|
+
|
|
95
|
+
return pd.concat((pd.read_parquet(f) for f in res.files))
|
|
96
|
+
|
|
97
|
+
def recover_api_key(self, email: str) -> RecoverAPIKey:
|
|
98
|
+
res = self._post(
|
|
99
|
+
url="/v0/api-key/recover",
|
|
100
|
+
model=RecoverAPIKeyRequest(email=email),
|
|
101
|
+
skip_auth=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return RecoverAPIKey(**res)
|
|
105
|
+
|
|
106
|
+
def rotate_api_key(self) -> RotateAPIKey:
|
|
107
|
+
res = self._post(url=f"/v0/api-key/rotate", model=RotateAPIKeyRequest())
|
|
108
|
+
|
|
109
|
+
return RotateAPIKey(**res)
|
|
110
|
+
|
|
111
|
+
def create_user(self, first_name: str, last_name: str, email: str) -> User:
|
|
112
|
+
res = self._post(
|
|
113
|
+
url="/v0/users",
|
|
114
|
+
model=UserCreate(
|
|
115
|
+
first_name=first_name,
|
|
116
|
+
last_name=last_name,
|
|
117
|
+
email=email,
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
return User(**res)
|
|
121
|
+
|
|
122
|
+
def get_user(self, id: str) -> User:
|
|
123
|
+
res = self._get(url=f"/v0/users/{id}")
|
|
124
|
+
return User(**res)
|
|
125
|
+
|
|
126
|
+
def list_users(self) -> List[User]:
|
|
127
|
+
res = self._get(url="/v0/users")
|
|
128
|
+
return [User(**record) for record in res["records"]]
|
|
129
|
+
|
|
130
|
+
def get_organisation_settings(self) -> OrganisationSettings:
|
|
131
|
+
res = self._get(url="/v0/organisation/settings")
|
|
132
|
+
return OrganisationSettings(**res)
|
|
133
|
+
|
|
134
|
+
def update_organisation_settings(
|
|
135
|
+
self,
|
|
136
|
+
*,
|
|
137
|
+
monthly_subscription_limit,
|
|
138
|
+
) -> OrganisationSettings:
|
|
139
|
+
res = self._post(
|
|
140
|
+
url="/v0/organisation/settings",
|
|
141
|
+
model=OrganisationSettings(
|
|
142
|
+
monthly_subscription_limit=monthly_subscription_limit,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
return OrganisationSettings(**res)
|
|
146
|
+
|
|
147
|
+
def _request(self, method: str, url: str, skip_auth=False, **kwargs) -> Dict:
|
|
148
|
+
|
|
149
|
+
if not skip_auth:
|
|
150
|
+
self._set_auth()
|
|
151
|
+
|
|
152
|
+
headers = {"cecil-python-sdk-version": __version__}
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
r = requests.request(
|
|
156
|
+
method=method,
|
|
157
|
+
url=self._base_url + url,
|
|
158
|
+
auth=self._api_auth,
|
|
159
|
+
headers=headers,
|
|
160
|
+
timeout=None,
|
|
161
|
+
**kwargs,
|
|
162
|
+
)
|
|
163
|
+
r.raise_for_status()
|
|
164
|
+
return r.json()
|
|
165
|
+
|
|
166
|
+
except requests.exceptions.ConnectionError:
|
|
167
|
+
raise Error("failed to connect to the Cecil Platform")
|
|
168
|
+
|
|
169
|
+
except requests.exceptions.HTTPError as err:
|
|
170
|
+
message = f"Request failed with status code {err.response.status_code}"
|
|
171
|
+
if err.response.text != "":
|
|
172
|
+
message += f": {err.response.text}"
|
|
173
|
+
|
|
174
|
+
match err.response.status_code:
|
|
175
|
+
case 400:
|
|
176
|
+
_handle_bad_request(err.response)
|
|
177
|
+
case 401:
|
|
178
|
+
raise Error("unauthorised")
|
|
179
|
+
case 404:
|
|
180
|
+
_handle_not_found(err.response)
|
|
181
|
+
case 405:
|
|
182
|
+
_handle_method_not_allowed(err.response)
|
|
183
|
+
case 422:
|
|
184
|
+
_handle_unprocessable_entity(err.response)
|
|
185
|
+
case 429:
|
|
186
|
+
_handle_too_many_requests(err.response)
|
|
187
|
+
case 500:
|
|
188
|
+
raise Error("internal server error")
|
|
189
|
+
case _:
|
|
190
|
+
raise Error(
|
|
191
|
+
f"request failed with code {err.response.status_code}",
|
|
192
|
+
err.response.text,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _get(self, url: str, **kwargs) -> Dict:
|
|
196
|
+
return self._request(method="get", url=url, **kwargs)
|
|
197
|
+
|
|
198
|
+
def _post(self, url: str, model: BaseModel, skip_auth=False, **kwargs) -> Dict:
|
|
199
|
+
return self._request(
|
|
200
|
+
method="post",
|
|
201
|
+
url=url,
|
|
202
|
+
json=model.model_dump(by_alias=True),
|
|
203
|
+
skip_auth=skip_auth,
|
|
204
|
+
**kwargs,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _set_auth(self) -> None:
|
|
208
|
+
try:
|
|
209
|
+
api_key = os.environ["CECIL_API_KEY"]
|
|
210
|
+
self._api_auth = auth.HTTPBasicAuth(username=api_key, password="")
|
|
211
|
+
except KeyError:
|
|
212
|
+
raise ValueError("environment variable CECIL_API_KEY not set") from None
|
cecil/errors.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Error(Exception):
|
|
5
|
+
def __init__(self, message: str, details=None):
|
|
6
|
+
self.message = message
|
|
7
|
+
self.details = details
|
|
8
|
+
|
|
9
|
+
if self.details is not None:
|
|
10
|
+
super().__init__(f"{self.message} \n{json.dumps(self.details, indent=2)}")
|
|
11
|
+
return
|
|
12
|
+
|
|
13
|
+
super().__init__(self.message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _format_json_key(value: str):
|
|
17
|
+
return "".join(["_" + i.lower() if i.isupper() else i for i in value]).lstrip("_")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_json(value: str):
|
|
21
|
+
try:
|
|
22
|
+
json.loads(value)
|
|
23
|
+
return True
|
|
24
|
+
except ValueError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _handle_bad_request(response):
|
|
29
|
+
if not _is_json(response.text):
|
|
30
|
+
raise Error("bad request")
|
|
31
|
+
|
|
32
|
+
details = {}
|
|
33
|
+
for key, value in response.json().items():
|
|
34
|
+
details[_format_json_key(key)] = value
|
|
35
|
+
|
|
36
|
+
raise Error("bad request", details)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _handle_method_not_allowed(response):
|
|
40
|
+
if not _is_json(response.text):
|
|
41
|
+
raise Error("method not allowed")
|
|
42
|
+
|
|
43
|
+
details = {}
|
|
44
|
+
for key, value in response.json().items():
|
|
45
|
+
details[_format_json_key(key)] = value
|
|
46
|
+
|
|
47
|
+
raise Error("method not allowed", details)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _handle_not_found(response):
|
|
51
|
+
if not _is_json(response.text):
|
|
52
|
+
raise Error("resource not found")
|
|
53
|
+
|
|
54
|
+
details = {}
|
|
55
|
+
for key, value in response.json().items():
|
|
56
|
+
details[_format_json_key(key)] = value
|
|
57
|
+
|
|
58
|
+
raise Error("resource not found", details)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _handle_too_many_requests(response):
|
|
62
|
+
if not _is_json(response.text):
|
|
63
|
+
raise Error("too many requests")
|
|
64
|
+
|
|
65
|
+
details = {}
|
|
66
|
+
|
|
67
|
+
for key, value in response.json().items():
|
|
68
|
+
details[_format_json_key(key)] = value
|
|
69
|
+
|
|
70
|
+
raise Error("too many requests", details)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _handle_unprocessable_entity(response):
|
|
74
|
+
if not _is_json(response.text):
|
|
75
|
+
raise Error(f"failed to process request")
|
|
76
|
+
|
|
77
|
+
res_body = response.json()
|
|
78
|
+
if "params" not in res_body:
|
|
79
|
+
raise Error(f"failed to process request")
|
|
80
|
+
|
|
81
|
+
details = {}
|
|
82
|
+
for key, value in res_body["params"].items():
|
|
83
|
+
details[_format_json_key(key)] = value
|
|
84
|
+
|
|
85
|
+
raise Error(f"failed to process request", details)
|
cecil/models.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from typing import Dict, Optional, List
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
from pydantic.alias_generators import to_camel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AOI(BaseModel):
|
|
9
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
10
|
+
id: str
|
|
11
|
+
external_ref: str
|
|
12
|
+
geometry: Optional[Dict] = None
|
|
13
|
+
hectares: float
|
|
14
|
+
created_at: datetime.datetime
|
|
15
|
+
created_by: str
|
|
16
|
+
|
|
17
|
+
class AOICreate(BaseModel):
|
|
18
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
19
|
+
geometry: Dict
|
|
20
|
+
external_ref: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrganisationSettings(BaseModel):
|
|
24
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
25
|
+
monthly_subscription_limit: Optional[int]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RecoverAPIKey(BaseModel):
|
|
29
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
30
|
+
message: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RecoverAPIKeyRequest(BaseModel):
|
|
34
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
35
|
+
email: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RotateAPIKey(BaseModel):
|
|
39
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
40
|
+
new_api_key: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RotateAPIKeyRequest(BaseModel):
|
|
44
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class User(BaseModel):
|
|
48
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
49
|
+
id: str
|
|
50
|
+
first_name: str
|
|
51
|
+
last_name: str
|
|
52
|
+
email: str
|
|
53
|
+
created_at: datetime.datetime
|
|
54
|
+
created_by: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class UserCreate(BaseModel):
|
|
58
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
59
|
+
first_name: str
|
|
60
|
+
last_name: str
|
|
61
|
+
email: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Bucket(BaseModel):
|
|
65
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
66
|
+
name: str
|
|
67
|
+
prefix: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BucketCredentials(BaseModel):
|
|
71
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
72
|
+
access_key_id: str
|
|
73
|
+
secret_access_key: str
|
|
74
|
+
session_token: str
|
|
75
|
+
region: str
|
|
76
|
+
expiration: datetime.datetime
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Band(BaseModel):
|
|
80
|
+
number: int
|
|
81
|
+
name: str
|
|
82
|
+
dtype: str
|
|
83
|
+
nodata: Optional[float | int] = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class File(BaseModel):
|
|
87
|
+
bands: List[Band]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class SubscriptionListFiles(BaseModel):
|
|
91
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
92
|
+
provider_name: str
|
|
93
|
+
dataset_id: str
|
|
94
|
+
dataset_name: str
|
|
95
|
+
aoi_id: str
|
|
96
|
+
subscription_id: str
|
|
97
|
+
bucket: Bucket
|
|
98
|
+
credentials: BucketCredentials
|
|
99
|
+
allowed_actions: List
|
|
100
|
+
file_mapping: Dict[str, File]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class SubscriptionParquetFiles(BaseModel):
|
|
104
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
105
|
+
files: List[str]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Subscription(BaseModel):
|
|
109
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
110
|
+
id: str
|
|
111
|
+
aoi_id: str
|
|
112
|
+
dataset_id: str
|
|
113
|
+
external_ref: str
|
|
114
|
+
created_at: datetime.datetime
|
|
115
|
+
created_by: str
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class SubscriptionCreate(BaseModel):
|
|
119
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
120
|
+
aoi_id: str
|
|
121
|
+
dataset_id: str
|
|
122
|
+
external_ref: str
|
cecil/version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
cecil/xarray.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
import dask
|
|
6
|
+
import rasterio
|
|
7
|
+
import rasterio.session
|
|
8
|
+
import rioxarray
|
|
9
|
+
import xarray
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .models import SubscriptionListFiles
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_xarray(res: SubscriptionListFiles) -> xarray.Dataset:
|
|
16
|
+
session = boto3.session.Session(
|
|
17
|
+
aws_access_key_id=res.credentials.access_key_id,
|
|
18
|
+
aws_secret_access_key=res.credentials.secret_access_key,
|
|
19
|
+
aws_session_token=res.credentials.session_token,
|
|
20
|
+
region_name=res.credentials.region,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
keys = _list_keys(session, res.bucket.name, res.bucket.prefix)
|
|
24
|
+
|
|
25
|
+
if not keys:
|
|
26
|
+
return xarray.Dataset()
|
|
27
|
+
|
|
28
|
+
timestamp_pattern = re.compile(r"\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}")
|
|
29
|
+
data_vars = {}
|
|
30
|
+
|
|
31
|
+
with rasterio.env.Env(
|
|
32
|
+
session=rasterio.session.AWSSession(session),
|
|
33
|
+
):
|
|
34
|
+
first_file = rioxarray.open_rasterio(
|
|
35
|
+
f"s3://{res.bucket.name}/{keys[0]}", chunks="auto"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for key in keys:
|
|
39
|
+
filename = key.split("/")[-1]
|
|
40
|
+
|
|
41
|
+
file_info = res.file_mapping.get(filename)
|
|
42
|
+
if not file_info:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
timestamp_str = timestamp_pattern.search(key).group()
|
|
46
|
+
|
|
47
|
+
for band_info in file_info.bands:
|
|
48
|
+
lazy_array = dask.array.from_delayed(
|
|
49
|
+
dask.delayed(_load_file)(
|
|
50
|
+
session, f"s3://{res.bucket.name}/{key}", band_info.number
|
|
51
|
+
),
|
|
52
|
+
shape=(
|
|
53
|
+
first_file.rio.height,
|
|
54
|
+
first_file.rio.width,
|
|
55
|
+
),
|
|
56
|
+
dtype=band_info.dtype,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
nodata = band_info.nodata if band_info.nodata is not None else np.nan
|
|
60
|
+
|
|
61
|
+
band_da = xarray.DataArray(
|
|
62
|
+
lazy_array,
|
|
63
|
+
dims=("y", "x"),
|
|
64
|
+
coords={
|
|
65
|
+
"y": first_file.y.values,
|
|
66
|
+
"x": first_file.x.values,
|
|
67
|
+
},
|
|
68
|
+
attrs={
|
|
69
|
+
"AREA_OR_POINT": first_file.attrs["AREA_OR_POINT"],
|
|
70
|
+
"_FillValue": np.dtype(band_info.dtype).type(nodata),
|
|
71
|
+
"scale_factor": first_file.attrs["scale_factor"],
|
|
72
|
+
"add_offset": first_file.attrs["add_offset"],
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
# band_da.encoding = first_file.encoding.copy() # TODO: is it the same for all files?
|
|
76
|
+
band_da.rio.write_crs(first_file.rio.crs, inplace=True)
|
|
77
|
+
band_da.rio.write_transform(first_file.rio.transform(), inplace=True)
|
|
78
|
+
|
|
79
|
+
band_da.name = band_info.name
|
|
80
|
+
|
|
81
|
+
# Dataset with time dimension
|
|
82
|
+
if timestamp_str != "0000/00/00/00/00/00":
|
|
83
|
+
t = datetime.strptime(timestamp_str, "%Y/%m/%d/%H/%M/%S")
|
|
84
|
+
band_da = band_da.expand_dims("time")
|
|
85
|
+
band_da = band_da.assign_coords(time=[t])
|
|
86
|
+
|
|
87
|
+
if band_info.name not in data_vars:
|
|
88
|
+
data_vars[band_info.name] = []
|
|
89
|
+
|
|
90
|
+
data_vars[band_info.name].append(band_da)
|
|
91
|
+
|
|
92
|
+
for var_name, time_series in data_vars.items():
|
|
93
|
+
if "time" in time_series[0].dims:
|
|
94
|
+
data_vars[var_name] = xarray.concat(time_series, dim="time", join="exact")
|
|
95
|
+
else:
|
|
96
|
+
data_vars[var_name] = time_series[0]
|
|
97
|
+
|
|
98
|
+
return xarray.Dataset(
|
|
99
|
+
data_vars=data_vars,
|
|
100
|
+
attrs={
|
|
101
|
+
"provider_name": res.provider_name,
|
|
102
|
+
"dataset_name": res.dataset_name,
|
|
103
|
+
"dataset_id": res.dataset_id,
|
|
104
|
+
"aoi_id": res.aoi_id,
|
|
105
|
+
"subscription_id": res.subscription_id,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _load_file(aws_session: boto3.session.Session, url: str, band_num: int):
|
|
111
|
+
with rasterio.env.Env(
|
|
112
|
+
session=rasterio.session.AWSSession(aws_session),
|
|
113
|
+
):
|
|
114
|
+
with rasterio.open(url) as src:
|
|
115
|
+
return src.read(band_num)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _list_keys(session: boto3.session.Session, bucket_name, prefix) -> list[str]:
|
|
119
|
+
s3_client = session.client("s3")
|
|
120
|
+
paginator = s3_client.get_paginator("list_objects_v2")
|
|
121
|
+
page_iterator = paginator.paginate(
|
|
122
|
+
Bucket=bucket_name,
|
|
123
|
+
Prefix=prefix,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
keys = []
|
|
127
|
+
for page in page_iterator:
|
|
128
|
+
for obj in page.get("Contents", []):
|
|
129
|
+
keys.append(obj["Key"])
|
|
130
|
+
|
|
131
|
+
return keys
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cecil
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Python SDK for Cecil Earth
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE.txt
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Requires-Dist: boto3<2.0.0,>=1.42.0
|
|
13
|
+
Requires-Dist: dask>=2025.9.1
|
|
14
|
+
Requires-Dist: pyarrow<23.0.0,>=22.0.0
|
|
15
|
+
Requires-Dist: pydantic<3.0.0,>=2.11.9
|
|
16
|
+
Requires-Dist: requests<3.0.0,>=2.32.5
|
|
17
|
+
Requires-Dist: rioxarray<1.0.0,>=0.19.0
|
|
18
|
+
Requires-Dist: xarray>=2025.6.1
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Cecil SDK
|
|
22
|
+
|
|
23
|
+
Please refer to the Cecil documentation:
|
|
24
|
+
|
|
25
|
+
https://docs.cecil.earth
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cecil/__init__.py,sha256=AEcRl73BDSAQe6W0d1PDD87IEcumARtREl7dCVa_YQY,86
|
|
2
|
+
cecil/client.py,sha256=qv4Pm3EF3Q-TXBkcD_njYjrCipvgNYhA_tYyut_p5rs,6593
|
|
3
|
+
cecil/errors.py,sha256=EnyYvFfU_JWYTTRax58bdwOndri2f-HzbqyzxtoV8uo,2100
|
|
4
|
+
cecil/models.py,sha256=KpeIjkrQ_9L_84F8wA837pn0tpBopDRT-9jRuBnTAHA,3053
|
|
5
|
+
cecil/version.py,sha256=XEqb2aiIn8fzGE68Mph4ck1FtQqsR_am0wRWvrYPffQ,22
|
|
6
|
+
cecil/xarray.py,sha256=OIYL8PPOA7ZxiJZA6eQLhJi9WvZmdokf6u-_BfPwwiE,4200
|
|
7
|
+
cecil-0.1.3.dist-info/METADATA,sha256=AYJWa9PwH5osacVpBaMH4XXLjWviaij68P5j9c-m_0A,724
|
|
8
|
+
cecil-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
+
cecil-0.1.3.dist-info/licenses/LICENSE.txt,sha256=mUexcmfYx3bG1VIzAdQTOf_NzStYw6-QkKVdUY_d4i4,1066
|
|
10
|
+
cecil-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 cecilearth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|