cecil 0.0.28__tar.gz → 0.0.35__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.
- {cecil-0.0.28 → cecil-0.0.35}/.gitignore +1 -0
- cecil-0.0.35/CONTRIBUTING.md +23 -0
- cecil-0.0.35/PKG-INFO +24 -0
- cecil-0.0.35/README.md +5 -0
- {cecil-0.0.28 → cecil-0.0.35}/pyproject.toml +2 -2
- {cecil-0.0.28 → cecil-0.0.35}/src/cecil/client.py +123 -17
- {cecil-0.0.28 → cecil-0.0.35}/src/cecil/models.py +55 -4
- cecil-0.0.35/src/cecil/version.py +1 -0
- cecil-0.0.35/src/cecil/xarray.py +193 -0
- cecil-0.0.28/CONTRIBUTING.md +0 -21
- cecil-0.0.28/PKG-INFO +0 -122
- cecil-0.0.28/README.md +0 -103
- cecil-0.0.28/src/cecil/version.py +0 -1
- cecil-0.0.28/src/cecil/xarray.py +0 -74
- {cecil-0.0.28 → cecil-0.0.35}/.editorconfig +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/LICENSE.txt +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/Makefile +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/src/cecil/__init__.py +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/src/cecil/errors.py +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/tests/__init__.py +0 -0
- {cecil-0.0.28 → cecil-0.0.35}/tests/test_client.py +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## Development installation
|
|
2
|
+
|
|
3
|
+
Install packaging/distribution tools and linter:
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
pip install hatch twine black
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
From top-level repo directory, install the package in editable mode:
|
|
10
|
+
|
|
11
|
+
```shell
|
|
12
|
+
pip install -e .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Local edits to the package will immediately take effect.
|
|
16
|
+
|
|
17
|
+
Get the PyPI Test API Key from 1Password and add it to `~/.pypirc`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
[testpypi]
|
|
21
|
+
username = __token__
|
|
22
|
+
password = <PyPI Test API Key>
|
|
23
|
+
```
|
cecil-0.0.35/PKG-INFO
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cecil
|
|
3
|
+
Version: 0.0.35
|
|
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: dask==2025.11.0
|
|
13
|
+
Requires-Dist: pydantic<3.0.0,>=2.11.9
|
|
14
|
+
Requires-Dist: requests<3.0.0,>=2.32.5
|
|
15
|
+
Requires-Dist: rioxarray==0.19.0
|
|
16
|
+
Requires-Dist: snowflake-connector-python[pandas]<4.0.0,>=3.17.4
|
|
17
|
+
Requires-Dist: xarray==2025.11.0
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Cecil SDK
|
|
21
|
+
|
|
22
|
+
Please refer to the Cecil documentation:
|
|
23
|
+
|
|
24
|
+
https://docs.cecil.earth
|
cecil-0.0.35/README.md
ADDED
|
@@ -16,12 +16,12 @@ classifiers = [
|
|
|
16
16
|
"Operating System :: OS Independent",
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"dask==2025.
|
|
19
|
+
"dask==2025.11.0",
|
|
20
20
|
"pydantic>=2.11.9,<3.0.0",
|
|
21
21
|
"requests>=2.32.5,<3.0.0",
|
|
22
22
|
"rioxarray==0.19.0",
|
|
23
23
|
"snowflake-connector-python[pandas]>=3.17.4,<4.0.0",
|
|
24
|
-
"xarray==2025.
|
|
24
|
+
"xarray==2025.11.0"
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[tool.hatch.version]
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
from warnings import warn
|
|
2
4
|
|
|
3
5
|
import pandas as pd
|
|
4
6
|
import requests
|
|
5
7
|
import snowflake.connector
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
+
from cryptography.hazmat.primitives import serialization
|
|
8
9
|
from pydantic import BaseModel
|
|
9
10
|
from requests import auth
|
|
10
|
-
from cryptography.hazmat.primitives import serialization
|
|
11
|
-
from typing import Dict, List, Optional
|
|
12
|
-
from warnings import warn
|
|
13
11
|
|
|
12
|
+
import xarray
|
|
14
13
|
from .errors import (
|
|
15
14
|
Error,
|
|
16
15
|
_handle_bad_request,
|
|
@@ -35,11 +34,15 @@ from .models import (
|
|
|
35
34
|
TransformationCreate,
|
|
36
35
|
User,
|
|
37
36
|
UserCreate,
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
SubscriptionMetadata,
|
|
38
|
+
SubscriptionParquetFiles,
|
|
39
|
+
SubscriptionListFiles,
|
|
40
|
+
Subscription,
|
|
41
|
+
SubscriptionCreate,
|
|
40
42
|
)
|
|
41
43
|
from .version import __version__
|
|
42
44
|
from .xarray import load_xarray
|
|
45
|
+
from .xarray import load_xarray_v2
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
class Client:
|
|
@@ -69,6 +72,11 @@ class Client:
|
|
|
69
72
|
def create_data_request(
|
|
70
73
|
self, aoi_id: str, dataset_id: str, external_ref: Optional[str] = None
|
|
71
74
|
) -> DataRequest:
|
|
75
|
+
warn(
|
|
76
|
+
"create_data_request() is deprecated, use create_subscription() instead.",
|
|
77
|
+
DeprecationWarning,
|
|
78
|
+
stacklevel=2,
|
|
79
|
+
)
|
|
72
80
|
res = self._post(
|
|
73
81
|
url="/v0/data-requests",
|
|
74
82
|
model=DataRequestCreate(
|
|
@@ -78,22 +86,120 @@ class Client:
|
|
|
78
86
|
return DataRequest(**res)
|
|
79
87
|
|
|
80
88
|
def get_data_request(self, id: str) -> DataRequest:
|
|
89
|
+
warn(
|
|
90
|
+
"get_data_request() is deprecated, use get_subscription() instead.",
|
|
91
|
+
DeprecationWarning,
|
|
92
|
+
stacklevel=2,
|
|
93
|
+
)
|
|
81
94
|
res = self._get(url=f"/v0/data-requests/{id}")
|
|
82
95
|
return DataRequest(**res)
|
|
83
96
|
|
|
84
97
|
def list_data_requests(self) -> List[DataRequest]:
|
|
98
|
+
warn(
|
|
99
|
+
"list_data_requests() is deprecated, use list_subscriptions() instead.",
|
|
100
|
+
DeprecationWarning,
|
|
101
|
+
stacklevel=2,
|
|
102
|
+
)
|
|
85
103
|
res = self._get(url="/v0/data-requests")
|
|
86
104
|
return [DataRequest(**record) for record in res["records"]]
|
|
87
105
|
|
|
88
|
-
def
|
|
89
|
-
res = self._get(url=
|
|
90
|
-
|
|
91
|
-
return load_xarray(metadata)
|
|
106
|
+
def list_subscriptions(self) -> List[Subscription]:
|
|
107
|
+
res = self._get(url="/v0/subscriptions")
|
|
108
|
+
return [Subscription(**record) for record in res["records"]]
|
|
92
109
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
def create_subscription(
|
|
111
|
+
self, aoi_id: str, dataset_id: str, external_ref: Optional[str] = None
|
|
112
|
+
) -> Subscription:
|
|
113
|
+
res = self._post(
|
|
114
|
+
url="/v0/subscriptions",
|
|
115
|
+
model=SubscriptionCreate(
|
|
116
|
+
aoi_id=aoi_id, dataset_id=dataset_id, external_ref=external_ref
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return Subscription(**res)
|
|
121
|
+
|
|
122
|
+
def get_subscription(self, id: str) -> Subscription:
|
|
123
|
+
res = self._get(url=f"/v0/subscriptions/{id}")
|
|
124
|
+
return Subscription(**res)
|
|
125
|
+
|
|
126
|
+
def load_xarray(
|
|
127
|
+
self,
|
|
128
|
+
subscription_id: Optional[str] = None,
|
|
129
|
+
data_request_id: Optional[str] = None,
|
|
130
|
+
) -> xarray.Dataset:
|
|
131
|
+
if subscription_id is None and data_request_id is None:
|
|
132
|
+
raise TypeError("load_xarray() missing argument: 'subscription_id'")
|
|
133
|
+
|
|
134
|
+
if subscription_id is not None and data_request_id is not None:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"load_xarray() only accepts one argument but two were provided"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if data_request_id:
|
|
140
|
+
warn(
|
|
141
|
+
"data_request_id is deprecated, use subscription_id instead.",
|
|
142
|
+
DeprecationWarning,
|
|
143
|
+
stacklevel=2,
|
|
144
|
+
)
|
|
145
|
+
subscription_id = data_request_id
|
|
146
|
+
|
|
147
|
+
res = SubscriptionMetadata(
|
|
148
|
+
**self._get(url=f"/v0/subscriptions/{subscription_id}/metadata")
|
|
149
|
+
)
|
|
150
|
+
return load_xarray(res)
|
|
151
|
+
|
|
152
|
+
def _load_xarray_v2(
|
|
153
|
+
self,
|
|
154
|
+
subscription_id: Optional[str] = None,
|
|
155
|
+
data_request_id: Optional[str] = None,
|
|
156
|
+
) -> xarray.Dataset:
|
|
157
|
+
if subscription_id is None and data_request_id is None:
|
|
158
|
+
raise TypeError("load_xarray_v2() missing argument: 'subscription_id'")
|
|
159
|
+
|
|
160
|
+
if subscription_id is not None and data_request_id is not None:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"load_xarray_v2() only accepts one argument but two were provided"
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if data_request_id:
|
|
166
|
+
warn(
|
|
167
|
+
"data_request_id is deprecated, use subscription_id instead.",
|
|
168
|
+
DeprecationWarning,
|
|
169
|
+
stacklevel=2,
|
|
170
|
+
)
|
|
171
|
+
subscription_id = data_request_id
|
|
172
|
+
|
|
173
|
+
res = SubscriptionListFiles(
|
|
174
|
+
**self._get(url=f"/v0/subscriptions/{subscription_id}/files/tiff")
|
|
175
|
+
)
|
|
176
|
+
return load_xarray_v2(res)
|
|
177
|
+
|
|
178
|
+
def load_dataframe(
|
|
179
|
+
self,
|
|
180
|
+
subscription_id: Optional[str] = None,
|
|
181
|
+
data_request_id: Optional[str] = None,
|
|
182
|
+
) -> pd.DataFrame:
|
|
183
|
+
if subscription_id is None and data_request_id is None:
|
|
184
|
+
raise TypeError("load_dataframe missing argument: 'subscription_id'")
|
|
185
|
+
|
|
186
|
+
if subscription_id is not None and data_request_id is not None:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
"load_dataframe only accepts one argument but two were provided"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if data_request_id:
|
|
192
|
+
warn(
|
|
193
|
+
"data_request_id is deprecated, use subscription_id instead.",
|
|
194
|
+
DeprecationWarning,
|
|
195
|
+
stacklevel=2,
|
|
196
|
+
)
|
|
197
|
+
subscription_id = data_request_id
|
|
198
|
+
|
|
199
|
+
res = SubscriptionParquetFiles(
|
|
200
|
+
**self._get(url=f"/v0/subscriptions/{subscription_id}/parquet-files")
|
|
201
|
+
)
|
|
202
|
+
df = pd.concat((pd.read_parquet(f) for f in res.files))
|
|
97
203
|
return df[
|
|
98
204
|
[col for col in df.columns if col not in ("organisation_id", "created_at")]
|
|
99
205
|
]
|
|
@@ -202,12 +308,12 @@ class Client:
|
|
|
202
308
|
def update_organisation_settings(
|
|
203
309
|
self,
|
|
204
310
|
*,
|
|
205
|
-
|
|
311
|
+
monthly_subscription_limit,
|
|
206
312
|
) -> OrganisationSettings:
|
|
207
313
|
res = self._post(
|
|
208
314
|
url="/v0/organisation/settings",
|
|
209
315
|
model=OrganisationSettings(
|
|
210
|
-
|
|
316
|
+
monthly_subscription_limit=monthly_subscription_limit,
|
|
211
317
|
),
|
|
212
318
|
)
|
|
213
319
|
return OrganisationSettings(**res)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
from typing import Dict, Optional, List
|
|
3
3
|
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, SecretStr
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, SecretStr
|
|
5
5
|
from pydantic.alias_generators import to_camel
|
|
6
6
|
|
|
7
7
|
|
|
@@ -49,7 +49,9 @@ class DataRequestCreate(BaseModel):
|
|
|
49
49
|
|
|
50
50
|
class OrganisationSettings(BaseModel):
|
|
51
51
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
52
|
-
|
|
52
|
+
monthly_subscription_limit: Optional[int] = Field(
|
|
53
|
+
alias="monthlyDataRequestLimit",
|
|
54
|
+
)
|
|
53
55
|
|
|
54
56
|
|
|
55
57
|
class RecoverAPIKey(BaseModel):
|
|
@@ -126,7 +128,7 @@ class File(BaseModel):
|
|
|
126
128
|
bands: List[Band]
|
|
127
129
|
|
|
128
130
|
|
|
129
|
-
class
|
|
131
|
+
class SubscriptionMetadata(BaseModel):
|
|
130
132
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
131
133
|
provider_name: str
|
|
132
134
|
dataset_id: str
|
|
@@ -137,6 +139,55 @@ class DataRequestMetadata(BaseModel):
|
|
|
137
139
|
files: List[File]
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
class
|
|
142
|
+
class Bucket(BaseModel):
|
|
143
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
144
|
+
name: str
|
|
145
|
+
prefix: str
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class BucketCredentials(BaseModel):
|
|
149
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
150
|
+
access_key_id: str
|
|
151
|
+
secret_access_key: str
|
|
152
|
+
session_token: str
|
|
153
|
+
expiration: datetime.datetime
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class FileMapping(BaseModel):
|
|
157
|
+
type: str
|
|
158
|
+
bands: List
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class SubscriptionListFiles(BaseModel):
|
|
162
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
163
|
+
provider_name: str
|
|
164
|
+
dataset_id: str
|
|
165
|
+
dataset_name: str
|
|
166
|
+
aoi_id: str
|
|
167
|
+
data_request_id: str
|
|
168
|
+
bucket: Bucket
|
|
169
|
+
credentials: BucketCredentials
|
|
170
|
+
allowed_actions: List
|
|
171
|
+
file_mapping: Dict[str, FileMapping]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class SubscriptionParquetFiles(BaseModel):
|
|
141
175
|
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
142
176
|
files: List[str]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Subscription(BaseModel):
|
|
180
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
181
|
+
id: str
|
|
182
|
+
aoi_id: str
|
|
183
|
+
dataset_id: str
|
|
184
|
+
external_ref: Optional[str]
|
|
185
|
+
created_at: datetime.datetime
|
|
186
|
+
created_by: str
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SubscriptionCreate(BaseModel):
|
|
190
|
+
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
|
|
191
|
+
aoi_id: str
|
|
192
|
+
dataset_id: str
|
|
193
|
+
external_ref: Optional[str]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.35"
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
import boto3
|
|
6
|
+
import dask
|
|
7
|
+
import rasterio
|
|
8
|
+
import rasterio.session
|
|
9
|
+
import rioxarray
|
|
10
|
+
import xarray
|
|
11
|
+
|
|
12
|
+
from .models import SubscriptionMetadata, SubscriptionListFiles
|
|
13
|
+
|
|
14
|
+
# v1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def load_xarray(metadata: SubscriptionMetadata) -> xarray.Dataset:
|
|
18
|
+
data_vars = {}
|
|
19
|
+
|
|
20
|
+
for f in metadata.files:
|
|
21
|
+
try:
|
|
22
|
+
dataset = _retry_with_exponential_backoff(_load_file, 5, 1, 2, f.url)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise ValueError(f"failed to load file: {e}")
|
|
25
|
+
|
|
26
|
+
for b in f.bands:
|
|
27
|
+
band = dataset.sel(band=b.number, drop=True)
|
|
28
|
+
|
|
29
|
+
if b.time and b.time_pattern:
|
|
30
|
+
t = datetime.strptime(b.time, b.time_pattern)
|
|
31
|
+
band = band.expand_dims("time")
|
|
32
|
+
band = band.assign_coords(time=[t])
|
|
33
|
+
|
|
34
|
+
band.name = b.variable_name
|
|
35
|
+
|
|
36
|
+
if b.variable_name not in data_vars:
|
|
37
|
+
data_vars[b.variable_name] = []
|
|
38
|
+
|
|
39
|
+
data_vars[b.variable_name].append(band)
|
|
40
|
+
|
|
41
|
+
for variable_name, time_series in data_vars.items():
|
|
42
|
+
if "time" in time_series[0].dims:
|
|
43
|
+
data_vars[variable_name] = xarray.concat(
|
|
44
|
+
time_series, dim="time", join="exact"
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
data_vars[variable_name] = time_series[0]
|
|
48
|
+
|
|
49
|
+
return xarray.Dataset(
|
|
50
|
+
data_vars=data_vars,
|
|
51
|
+
attrs={
|
|
52
|
+
"provider_name": metadata.provider_name,
|
|
53
|
+
"dataset_name": metadata.dataset_name,
|
|
54
|
+
"dataset_id": metadata.dataset_id,
|
|
55
|
+
"aoi_id": metadata.aoi_id,
|
|
56
|
+
"subscription_id": metadata.data_request_id,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _retry_with_exponential_backoff(
|
|
62
|
+
func, retries, start_delay, multiplier, *args, **kwargs
|
|
63
|
+
):
|
|
64
|
+
delay = start_delay
|
|
65
|
+
for attempt in range(1, retries + 1):
|
|
66
|
+
try:
|
|
67
|
+
return func(*args, **kwargs)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
if attempt == retries:
|
|
70
|
+
raise e
|
|
71
|
+
time.sleep(delay)
|
|
72
|
+
delay *= multiplier
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_file(url: str):
|
|
77
|
+
return rioxarray.open_rasterio(
|
|
78
|
+
url,
|
|
79
|
+
chunks={"x": 2000, "y": 2000},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# v2
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def load_xarray_v2(res: SubscriptionListFiles) -> xarray.Dataset:
|
|
87
|
+
session = boto3.session.Session(
|
|
88
|
+
aws_access_key_id=res.credentials.access_key_id,
|
|
89
|
+
aws_secret_access_key=res.credentials.secret_access_key,
|
|
90
|
+
aws_session_token=res.credentials.session_token,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
keys = _list_keys_v2(session, res.bucket.name, res.bucket.prefix)
|
|
94
|
+
|
|
95
|
+
if not keys:
|
|
96
|
+
return xarray.Dataset()
|
|
97
|
+
|
|
98
|
+
timestamp_pattern = re.compile(r"\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}")
|
|
99
|
+
data_vars = {}
|
|
100
|
+
|
|
101
|
+
with rasterio.env.Env(
|
|
102
|
+
session=rasterio.session.AWSSession(session),
|
|
103
|
+
):
|
|
104
|
+
first_file = rioxarray.open_rasterio(
|
|
105
|
+
f"s3://{res.bucket.name}/{keys[0]}", chunks="auto"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
for key in keys:
|
|
109
|
+
filename = key.split("/")[-1]
|
|
110
|
+
|
|
111
|
+
file_info = res.file_mapping.get(filename)
|
|
112
|
+
if not file_info:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
timestamp_str = timestamp_pattern.search(key).group()
|
|
116
|
+
|
|
117
|
+
for band_num, var_name in enumerate(file_info.bands, start=1):
|
|
118
|
+
lazy_array = dask.array.from_delayed(
|
|
119
|
+
dask.delayed(_load_file_v2)(
|
|
120
|
+
session, f"s3://{res.bucket.name}/{key}", band_num
|
|
121
|
+
),
|
|
122
|
+
shape=(
|
|
123
|
+
first_file.rio.height,
|
|
124
|
+
first_file.rio.width,
|
|
125
|
+
),
|
|
126
|
+
dtype=file_info.type,
|
|
127
|
+
)
|
|
128
|
+
band_da = xarray.DataArray(
|
|
129
|
+
lazy_array,
|
|
130
|
+
dims=("y", "x"),
|
|
131
|
+
coords={
|
|
132
|
+
"y": first_file.y.values,
|
|
133
|
+
"x": first_file.x.values,
|
|
134
|
+
},
|
|
135
|
+
# attrs=first_file.attrs.copy() # TODO: is it the same for all files?
|
|
136
|
+
)
|
|
137
|
+
# band_da.encoding = first_file.encoding.copy() # TODO: is it the same for all files?
|
|
138
|
+
band_da.rio.write_crs(first_file.rio.crs, inplace=True)
|
|
139
|
+
band_da.rio.write_transform(first_file.rio.transform(), inplace=True)
|
|
140
|
+
|
|
141
|
+
band_da.name = var_name
|
|
142
|
+
|
|
143
|
+
# Dataset with time dimension
|
|
144
|
+
if timestamp_str != "0000/00/00/00/00/00":
|
|
145
|
+
t = datetime.strptime(timestamp_str, "%Y/%m/%d/%H/%M/%S")
|
|
146
|
+
band_da = band_da.expand_dims("time")
|
|
147
|
+
band_da = band_da.assign_coords(time=[t])
|
|
148
|
+
|
|
149
|
+
if var_name not in data_vars:
|
|
150
|
+
data_vars[var_name] = []
|
|
151
|
+
|
|
152
|
+
data_vars[var_name].append(band_da)
|
|
153
|
+
|
|
154
|
+
for var_name, time_series in data_vars.items():
|
|
155
|
+
if "time" in time_series[0].dims:
|
|
156
|
+
data_vars[var_name] = xarray.concat(time_series, dim="time", join="exact")
|
|
157
|
+
else:
|
|
158
|
+
data_vars[var_name] = time_series[0]
|
|
159
|
+
|
|
160
|
+
return xarray.Dataset(
|
|
161
|
+
data_vars=data_vars,
|
|
162
|
+
attrs={
|
|
163
|
+
"provider_name": res.provider_name,
|
|
164
|
+
"dataset_name": res.dataset_name,
|
|
165
|
+
"dataset_id": res.dataset_id,
|
|
166
|
+
"aoi_id": res.aoi_id,
|
|
167
|
+
"subscription_id": res.data_request_id,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _load_file_v2(aws_session: boto3.session.Session, url: str, band_num: int):
|
|
173
|
+
with rasterio.env.Env(
|
|
174
|
+
session=rasterio.session.AWSSession(aws_session),
|
|
175
|
+
):
|
|
176
|
+
with rasterio.open(url) as src:
|
|
177
|
+
return src.read(band_num)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _list_keys_v2(session: boto3.session.Session, bucket_name, prefix) -> list[str]:
|
|
181
|
+
s3_client = session.client("s3")
|
|
182
|
+
paginator = s3_client.get_paginator("list_objects_v2")
|
|
183
|
+
page_iterator = paginator.paginate(
|
|
184
|
+
Bucket=bucket_name,
|
|
185
|
+
Prefix=prefix,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
keys = []
|
|
189
|
+
for page in page_iterator:
|
|
190
|
+
for obj in page.get("Contents", []):
|
|
191
|
+
keys.append(obj["Key"])
|
|
192
|
+
|
|
193
|
+
return keys
|
cecil-0.0.28/CONTRIBUTING.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
## Development installation
|
|
2
|
-
|
|
3
|
-
Install packaging/distribution tools:
|
|
4
|
-
|
|
5
|
-
```shell
|
|
6
|
-
pip install hatch twine
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Install linter
|
|
10
|
-
|
|
11
|
-
```shell
|
|
12
|
-
pip install black
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
From top-level repo directory, install the package in editable mode:
|
|
16
|
-
|
|
17
|
-
```shell
|
|
18
|
-
pip install -e .
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Local edits to the package will immediately take effect.
|
cecil-0.0.28/PKG-INFO
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: cecil
|
|
3
|
-
Version: 0.0.28
|
|
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: dask==2025.9.1
|
|
13
|
-
Requires-Dist: pydantic<3.0.0,>=2.11.9
|
|
14
|
-
Requires-Dist: requests<3.0.0,>=2.32.5
|
|
15
|
-
Requires-Dist: rioxarray==0.19.0
|
|
16
|
-
Requires-Dist: snowflake-connector-python[pandas]<4.0.0,>=3.17.4
|
|
17
|
-
Requires-Dist: xarray==2025.6.1
|
|
18
|
-
Description-Content-Type: text/markdown
|
|
19
|
-
|
|
20
|
-
# Cecil SDK
|
|
21
|
-
|
|
22
|
-
[](https://pypi.org/project/cecil-sdk)
|
|
23
|
-
[](https://pypi.org/project/cecil-sdk)
|
|
24
|
-
|
|
25
|
-
-----
|
|
26
|
-
|
|
27
|
-
## Table of Contents
|
|
28
|
-
|
|
29
|
-
- [Installation](#installation)
|
|
30
|
-
- [Authentication](#authentication)
|
|
31
|
-
- [License](#license)
|
|
32
|
-
- [Examples](#examples)
|
|
33
|
-
|
|
34
|
-
## Installation
|
|
35
|
-
|
|
36
|
-
```shell
|
|
37
|
-
pip install cecil
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Authentication
|
|
41
|
-
|
|
42
|
-
Set `CECIL_API_KEY` environment variable to your Cecil API key.
|
|
43
|
-
|
|
44
|
-
## Examples
|
|
45
|
-
|
|
46
|
-
### Create an AOI and data request using the Cecil client
|
|
47
|
-
|
|
48
|
-
```python
|
|
49
|
-
import cecil
|
|
50
|
-
|
|
51
|
-
client = cecil.Client()
|
|
52
|
-
|
|
53
|
-
my_aoi = client.create_aoi(
|
|
54
|
-
name="My AOI",
|
|
55
|
-
geometry={
|
|
56
|
-
"type": "Polygon",
|
|
57
|
-
"coordinates": [
|
|
58
|
-
[
|
|
59
|
-
[145.410408835, -42.004083838],
|
|
60
|
-
[145.410408835, -42.004203978],
|
|
61
|
-
[145.410623191, -42.004203978],
|
|
62
|
-
[145.410623191, -42.004083838],
|
|
63
|
-
[145.410408835, -42.004083838],
|
|
64
|
-
]
|
|
65
|
-
],
|
|
66
|
-
},
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
# Get dataset ID from docs.cecil.earth -> Datasets
|
|
70
|
-
planet_forest_carbon_diligence_id = "c2dd4f55-56f6-4d05-aae3-ba7c1dcd812f"
|
|
71
|
-
|
|
72
|
-
my_data_request = client.create_data_request(
|
|
73
|
-
aoi_id=my_aoi.id,
|
|
74
|
-
dataset_id=planet_forest_carbon_diligence_id,
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
print(client.get_data_request(my_data_request.id))
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Create a transformation using the Cecil client
|
|
81
|
-
|
|
82
|
-
```python
|
|
83
|
-
my_transformation = client.create_transformation(
|
|
84
|
-
data_request_id=my_data_request.id,
|
|
85
|
-
crs="EPSG:4326",
|
|
86
|
-
spatial_resolution=0.005,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
print(client.get_transformation(my_transformation.id))
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Query data (once transformation is completed)
|
|
93
|
-
|
|
94
|
-
```python
|
|
95
|
-
df = client.query(f'''
|
|
96
|
-
SELECT *
|
|
97
|
-
FROM
|
|
98
|
-
planet.forest_carbon_diligence
|
|
99
|
-
WHERE
|
|
100
|
-
transformation_id = '{my_transformation.id}'
|
|
101
|
-
''')
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Other client methods:
|
|
105
|
-
|
|
106
|
-
```python
|
|
107
|
-
client.list_aois()
|
|
108
|
-
|
|
109
|
-
client.get_aoi(my_aoi.id)
|
|
110
|
-
|
|
111
|
-
client.list_data_requests()
|
|
112
|
-
|
|
113
|
-
client.get_data_request(my_data_request.id)
|
|
114
|
-
|
|
115
|
-
client.list_transformations()
|
|
116
|
-
|
|
117
|
-
client.get_transformation(my_transformation.id)
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## License
|
|
121
|
-
|
|
122
|
-
`cecil` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
cecil-0.0.28/README.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Cecil SDK
|
|
2
|
-
|
|
3
|
-
[](https://pypi.org/project/cecil-sdk)
|
|
4
|
-
[](https://pypi.org/project/cecil-sdk)
|
|
5
|
-
|
|
6
|
-
-----
|
|
7
|
-
|
|
8
|
-
## Table of Contents
|
|
9
|
-
|
|
10
|
-
- [Installation](#installation)
|
|
11
|
-
- [Authentication](#authentication)
|
|
12
|
-
- [License](#license)
|
|
13
|
-
- [Examples](#examples)
|
|
14
|
-
|
|
15
|
-
## Installation
|
|
16
|
-
|
|
17
|
-
```shell
|
|
18
|
-
pip install cecil
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Authentication
|
|
22
|
-
|
|
23
|
-
Set `CECIL_API_KEY` environment variable to your Cecil API key.
|
|
24
|
-
|
|
25
|
-
## Examples
|
|
26
|
-
|
|
27
|
-
### Create an AOI and data request using the Cecil client
|
|
28
|
-
|
|
29
|
-
```python
|
|
30
|
-
import cecil
|
|
31
|
-
|
|
32
|
-
client = cecil.Client()
|
|
33
|
-
|
|
34
|
-
my_aoi = client.create_aoi(
|
|
35
|
-
name="My AOI",
|
|
36
|
-
geometry={
|
|
37
|
-
"type": "Polygon",
|
|
38
|
-
"coordinates": [
|
|
39
|
-
[
|
|
40
|
-
[145.410408835, -42.004083838],
|
|
41
|
-
[145.410408835, -42.004203978],
|
|
42
|
-
[145.410623191, -42.004203978],
|
|
43
|
-
[145.410623191, -42.004083838],
|
|
44
|
-
[145.410408835, -42.004083838],
|
|
45
|
-
]
|
|
46
|
-
],
|
|
47
|
-
},
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Get dataset ID from docs.cecil.earth -> Datasets
|
|
51
|
-
planet_forest_carbon_diligence_id = "c2dd4f55-56f6-4d05-aae3-ba7c1dcd812f"
|
|
52
|
-
|
|
53
|
-
my_data_request = client.create_data_request(
|
|
54
|
-
aoi_id=my_aoi.id,
|
|
55
|
-
dataset_id=planet_forest_carbon_diligence_id,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
print(client.get_data_request(my_data_request.id))
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Create a transformation using the Cecil client
|
|
62
|
-
|
|
63
|
-
```python
|
|
64
|
-
my_transformation = client.create_transformation(
|
|
65
|
-
data_request_id=my_data_request.id,
|
|
66
|
-
crs="EPSG:4326",
|
|
67
|
-
spatial_resolution=0.005,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
print(client.get_transformation(my_transformation.id))
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Query data (once transformation is completed)
|
|
74
|
-
|
|
75
|
-
```python
|
|
76
|
-
df = client.query(f'''
|
|
77
|
-
SELECT *
|
|
78
|
-
FROM
|
|
79
|
-
planet.forest_carbon_diligence
|
|
80
|
-
WHERE
|
|
81
|
-
transformation_id = '{my_transformation.id}'
|
|
82
|
-
''')
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### Other client methods:
|
|
86
|
-
|
|
87
|
-
```python
|
|
88
|
-
client.list_aois()
|
|
89
|
-
|
|
90
|
-
client.get_aoi(my_aoi.id)
|
|
91
|
-
|
|
92
|
-
client.list_data_requests()
|
|
93
|
-
|
|
94
|
-
client.get_data_request(my_data_request.id)
|
|
95
|
-
|
|
96
|
-
client.list_transformations()
|
|
97
|
-
|
|
98
|
-
client.get_transformation(my_transformation.id)
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## License
|
|
102
|
-
|
|
103
|
-
`cecil` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.28"
|
cecil-0.0.28/src/cecil/xarray.py
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import rioxarray
|
|
3
|
-
import xarray
|
|
4
|
-
|
|
5
|
-
from datetime import datetime
|
|
6
|
-
|
|
7
|
-
from .errors import Error
|
|
8
|
-
from .models import DataRequestMetadata
|
|
9
|
-
|
|
10
|
-
os.environ["GDAL_NUM_THREADS"] = "1"
|
|
11
|
-
os.environ["GDAL_DISABLE_READDIR_ON_OPEN"] = "FALSE"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def align_pixel_grids(time_series):
|
|
15
|
-
# Use the first timestep as reference
|
|
16
|
-
reference_da = time_series[0]
|
|
17
|
-
aligned_series = [reference_da]
|
|
18
|
-
|
|
19
|
-
# Align all other timesteps to the reference grid
|
|
20
|
-
for i, da in enumerate(time_series[1:], 1):
|
|
21
|
-
try:
|
|
22
|
-
aligned_da = da.rio.reproject_match(reference_da)
|
|
23
|
-
aligned_series.append(aligned_da)
|
|
24
|
-
except Exception as e:
|
|
25
|
-
raise Error
|
|
26
|
-
continue
|
|
27
|
-
|
|
28
|
-
return aligned_series
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def load_xarray(metadata: DataRequestMetadata) -> xarray.Dataset:
|
|
32
|
-
data_vars = {}
|
|
33
|
-
|
|
34
|
-
for f in metadata.files:
|
|
35
|
-
dataset = rioxarray.open_rasterio(
|
|
36
|
-
f.url,
|
|
37
|
-
chunks={"x": 2000, "y": 2000},
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
for b in f.bands:
|
|
41
|
-
band = dataset.sel(band=b.number, drop=True)
|
|
42
|
-
|
|
43
|
-
if b.time and b.time_pattern:
|
|
44
|
-
time = datetime.strptime(b.time, b.time_pattern)
|
|
45
|
-
band = band.expand_dims("time")
|
|
46
|
-
band = band.assign_coords(time=[time])
|
|
47
|
-
|
|
48
|
-
band.name = b.variable_name
|
|
49
|
-
|
|
50
|
-
if b.variable_name not in data_vars:
|
|
51
|
-
data_vars[b.variable_name] = []
|
|
52
|
-
|
|
53
|
-
data_vars[b.variable_name].append(band)
|
|
54
|
-
|
|
55
|
-
for variable_name, time_series in data_vars.items():
|
|
56
|
-
if "time" in time_series[0].dims:
|
|
57
|
-
# time_series = align_pixel_grids(time_series)
|
|
58
|
-
data_vars[variable_name] = xarray.concat(
|
|
59
|
-
time_series, dim="time", join="exact"
|
|
60
|
-
)
|
|
61
|
-
else:
|
|
62
|
-
data_vars[variable_name] = time_series[0]
|
|
63
|
-
|
|
64
|
-
return xarray.Dataset(
|
|
65
|
-
data_vars=data_vars,
|
|
66
|
-
attrs={
|
|
67
|
-
"provider_name": metadata.provider_name,
|
|
68
|
-
"dataset_id": metadata.dataset_id,
|
|
69
|
-
"dataset_name": metadata.dataset_name,
|
|
70
|
-
"dataset_crs": metadata.dataset_crs,
|
|
71
|
-
"aoi_id": metadata.aoi_id,
|
|
72
|
-
"data_request_id": metadata.data_request_id,
|
|
73
|
-
},
|
|
74
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|