cecil 0.0.21__py3-none-any.whl → 0.0.30__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/client.py CHANGED
@@ -1,16 +1,20 @@
1
1
  import os
2
- from typing import Dict, List
3
2
 
4
3
  import pandas as pd
5
4
  import requests
6
5
  import snowflake.connector
6
+ import xarray
7
+
7
8
  from pydantic import BaseModel
8
9
  from requests import auth
9
10
  from cryptography.hazmat.primitives import serialization
11
+ from typing import Dict, List, Optional
12
+ from warnings import warn
10
13
 
11
14
  from .errors import (
12
15
  Error,
13
16
  _handle_bad_request,
17
+ _handle_method_not_allowed,
14
18
  _handle_not_found,
15
19
  _handle_too_many_requests,
16
20
  _handle_unprocessable_entity,
@@ -31,8 +35,13 @@ from .models import (
31
35
  TransformationCreate,
32
36
  User,
33
37
  UserCreate,
38
+ DataRequestMetadata,
39
+ DataRequestParquetFiles,
40
+ DataRequestLoadXarray,
34
41
  )
35
42
  from .version import __version__
43
+ from .xarray import load_xarray
44
+ from .xarray import load_xarray_v2
36
45
 
37
46
 
38
47
  class Client:
@@ -43,9 +52,12 @@ class Client:
43
52
  )
44
53
  self._snowflake_user_creds = None
45
54
 
46
- def create_aoi(self, name: str, geometry: Dict) -> AOI:
55
+ def create_aoi(self, geometry: Dict, external_ref: Optional[str] = None) -> AOI:
47
56
  # TODO: validate geometry
48
- res = self._post(url="/v0/aois", model=AOICreate(name=name, geometry=geometry))
57
+ res = self._post(
58
+ url="/v0/aois",
59
+ model=AOICreate(geometry=geometry, external_ref=external_ref),
60
+ )
49
61
  return AOI(**res)
50
62
 
51
63
  def get_aoi(self, id: str) -> AOI:
@@ -56,10 +68,14 @@ class Client:
56
68
  res = self._get(url="/v0/aois")
57
69
  return [AOIRecord(**record) for record in res["records"]]
58
70
 
59
- def create_data_request(self, aoi_id: str, dataset_id: str) -> DataRequest:
71
+ def create_data_request(
72
+ self, aoi_id: str, dataset_id: str, external_ref: Optional[str] = None
73
+ ) -> DataRequest:
60
74
  res = self._post(
61
75
  url="/v0/data-requests",
62
- model=DataRequestCreate(aoi_id=aoi_id, dataset_id=dataset_id),
76
+ model=DataRequestCreate(
77
+ aoi_id=aoi_id, dataset_id=dataset_id, external_ref=external_ref
78
+ ),
63
79
  )
64
80
  return DataRequest(**res)
65
81
 
@@ -71,9 +87,32 @@ class Client:
71
87
  res = self._get(url="/v0/data-requests")
72
88
  return [DataRequest(**record) for record in res["records"]]
73
89
 
90
+ def load_xarray(self, data_request_id: str) -> xarray.Dataset:
91
+ res = self._get(url=f"/v0/data-requests/{data_request_id}/metadata")
92
+ metadata = DataRequestMetadata(**res)
93
+ return load_xarray(metadata)
94
+
95
+ def load_xarray_v2(self, data_request_id: str) -> xarray.Dataset:
96
+ res = self._get(url=f"/v0/data-requests/{data_request_id}/load-xarray")
97
+ load_xarray_info = DataRequestLoadXarray(**res)
98
+ return load_xarray_v2(load_xarray_info)
99
+
100
+ def load_dataframe(self, data_request_id: str) -> pd.DataFrame:
101
+ res = self._get(url=f"/v0/data-requests/{data_request_id}/parquet-files")
102
+ metadata = DataRequestParquetFiles(**res)
103
+ df = pd.concat((pd.read_parquet(f) for f in metadata.files))
104
+ return df[
105
+ [col for col in df.columns if col not in ("organisation_id", "created_at")]
106
+ ]
107
+
74
108
  def create_transformation(
75
109
  self, data_request_id: str, crs: str, spatial_resolution: float
76
110
  ) -> Transformation:
111
+ warn(
112
+ "create_transformation() is deprecated, refer to https://github.com/cecilearth/examples",
113
+ DeprecationWarning,
114
+ stacklevel=2,
115
+ )
77
116
  res = self._post(
78
117
  url="/v0/transformations",
79
118
  model=TransformationCreate(
@@ -85,14 +124,32 @@ class Client:
85
124
  return Transformation(**res)
86
125
 
87
126
  def get_transformation(self, id: str) -> Transformation:
127
+ warn(
128
+ "get_transformation() is deprecated.",
129
+ DeprecationWarning,
130
+ stacklevel=2,
131
+ )
88
132
  res = self._get(url=f"/v0/transformations/{id}")
89
133
  return Transformation(**res)
90
134
 
91
135
  def list_transformations(self) -> List[Transformation]:
136
+ warn(
137
+ "list_transformations() is deprecated.",
138
+ DeprecationWarning,
139
+ stacklevel=2,
140
+ )
92
141
  res = self._get(url="/v0/transformations")
93
142
  return [Transformation(**record) for record in res["records"]]
94
143
 
95
144
  def query(self, sql: str) -> pd.DataFrame:
145
+ warn(
146
+ "query() is deprecated, use load_xarray() or load_dataframe() instead.",
147
+ DeprecationWarning,
148
+ stacklevel=2,
149
+ )
150
+ return self._query(sql)
151
+
152
+ def _query(self, sql: str) -> pd.DataFrame:
96
153
  if self._snowflake_user_creds is None:
97
154
  res = self._get(url="/v0/snowflake-user-credentials")
98
155
  self._snowflake_user_creds = SnowflakeUserCredentials(**res)
@@ -196,6 +253,8 @@ class Client:
196
253
  raise Error("unauthorised")
197
254
  case 404:
198
255
  _handle_not_found(err.response)
256
+ case 405:
257
+ _handle_method_not_allowed(err.response)
199
258
  case 422:
200
259
  _handle_unprocessable_entity(err.response)
201
260
  case 429:
cecil/errors.py CHANGED
@@ -36,6 +36,17 @@ def _handle_bad_request(response):
36
36
  raise Error("bad request", details)
37
37
 
38
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
+
39
50
  def _handle_not_found(response):
40
51
  if not _is_json(response.text):
41
52
  raise Error("resource not found")
cecil/models.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import datetime
2
- from typing import Dict, Optional
2
+ from typing import Dict, Optional, List
3
3
 
4
4
  from pydantic import BaseModel, ConfigDict, SecretStr
5
5
  from pydantic.alias_generators import to_camel
@@ -8,7 +8,7 @@ from pydantic.alias_generators import to_camel
8
8
  class AOI(BaseModel):
9
9
  model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
10
10
  id: str
11
- name: str
11
+ external_ref: Optional[str]
12
12
  geometry: Dict
13
13
  hectares: float
14
14
  created_at: datetime.datetime
@@ -18,7 +18,7 @@ class AOI(BaseModel):
18
18
  class AOIRecord(BaseModel):
19
19
  model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
20
20
  id: str
21
- name: str
21
+ external_ref: Optional[str]
22
22
  hectares: float
23
23
  created_at: datetime.datetime
24
24
  created_by: str
@@ -26,8 +26,8 @@ class AOIRecord(BaseModel):
26
26
 
27
27
  class AOICreate(BaseModel):
28
28
  model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
29
- name: str
30
29
  geometry: Dict
30
+ external_ref: Optional[str]
31
31
 
32
32
 
33
33
  class DataRequest(BaseModel):
@@ -35,6 +35,7 @@ class DataRequest(BaseModel):
35
35
  id: str
36
36
  aoi_id: str
37
37
  dataset_id: str
38
+ external_ref: Optional[str]
38
39
  created_at: datetime.datetime
39
40
  created_by: str
40
41
 
@@ -43,6 +44,7 @@ class DataRequestCreate(BaseModel):
43
44
  model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
44
45
  aoi_id: str
45
46
  dataset_id: str
47
+ external_ref: Optional[str]
46
48
 
47
49
 
48
50
  class OrganisationSettings(BaseModel):
@@ -108,3 +110,54 @@ class UserCreate(BaseModel):
108
110
  first_name: str
109
111
  last_name: str
110
112
  email: str
113
+
114
+
115
+ class Band(BaseModel):
116
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
117
+ variable_name: str
118
+ time: str
119
+ time_pattern: str
120
+ number: int
121
+
122
+
123
+ class File(BaseModel):
124
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
125
+ url: str
126
+ bands: List[Band]
127
+
128
+
129
+ class DataRequestMetadata(BaseModel):
130
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
131
+ provider_name: str
132
+ dataset_id: str
133
+ dataset_name: str
134
+ dataset_crs: str
135
+ aoi_id: str
136
+ data_request_id: str
137
+ files: List[File]
138
+
139
+
140
+ class Bucket(BaseModel):
141
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
142
+ name: str
143
+ prefix: str
144
+ access_key_id: str
145
+ secret_access_key: str
146
+ session_token: str
147
+ expiration: datetime.datetime
148
+
149
+
150
+ class DataRequestLoadXarray(BaseModel):
151
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
152
+ provider_name: str
153
+ dataset_id: str
154
+ dataset_name: str
155
+ dataset_crs: str
156
+ aoi_id: str
157
+ data_request_id: str
158
+ bucket: Bucket
159
+
160
+
161
+ class DataRequestParquetFiles(BaseModel):
162
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
163
+ files: List[str]
cecil/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.0.21"
1
+ __version__ = "0.0.30"
cecil/xarray.py ADDED
@@ -0,0 +1,165 @@
1
+ import os
2
+ import re
3
+ import time
4
+
5
+ import boto3
6
+ import rioxarray
7
+ import xarray
8
+
9
+ from datetime import datetime
10
+
11
+ from .errors import Error
12
+ from .models import DataRequestMetadata, DataRequestLoadXarray, Bucket
13
+
14
+ os.environ["GDAL_DISABLE_READDIR_ON_OPEN"] = "TRUE"
15
+
16
+
17
+ def align_pixel_grids(time_series):
18
+ # Use the first timestep as reference
19
+ reference_da = time_series[0]
20
+ aligned_series = [reference_da]
21
+
22
+ # Align all other timesteps to the reference grid
23
+ for i, da in enumerate(time_series[1:], 1):
24
+ try:
25
+ aligned_da = da.rio.reproject_match(reference_da)
26
+ aligned_series.append(aligned_da)
27
+ except Exception:
28
+ raise Error
29
+
30
+ return aligned_series
31
+
32
+
33
+ def retry_with_exponential_backoff(
34
+ func, retries, start_delay, multiplier, *args, **kwargs
35
+ ):
36
+ delay = start_delay
37
+ for attempt in range(1, retries + 1):
38
+ try:
39
+ return func(*args, **kwargs)
40
+ except Exception as e:
41
+ if attempt == retries:
42
+ raise e
43
+ time.sleep(delay)
44
+ delay *= multiplier
45
+ return None
46
+
47
+
48
+ def load_file(url: str):
49
+ return rioxarray.open_rasterio(
50
+ url,
51
+ chunks={"x": 2000, "y": 2000},
52
+ )
53
+
54
+
55
+ def load_xarray(metadata: DataRequestMetadata) -> xarray.Dataset:
56
+ data_vars = {}
57
+
58
+ for f in metadata.files:
59
+ try:
60
+ dataset = retry_with_exponential_backoff(load_file, 5, 1, 2, f.url)
61
+ except Exception as e:
62
+ raise ValueError(f"failed to load file: {e}")
63
+
64
+ for b in f.bands:
65
+ band = dataset.sel(band=b.number, drop=True)
66
+
67
+ if b.time and b.time_pattern:
68
+ t = datetime.strptime(b.time, b.time_pattern)
69
+ band = band.expand_dims("time")
70
+ band = band.assign_coords(time=[t])
71
+
72
+ band.name = b.variable_name
73
+
74
+ if b.variable_name not in data_vars:
75
+ data_vars[b.variable_name] = []
76
+
77
+ data_vars[b.variable_name].append(band)
78
+
79
+ for variable_name, time_series in data_vars.items():
80
+ if "time" in time_series[0].dims:
81
+ # time_series = align_pixel_grids(time_series)
82
+ data_vars[variable_name] = xarray.concat(
83
+ time_series, dim="time", join="exact"
84
+ )
85
+ else:
86
+ data_vars[variable_name] = time_series[0]
87
+
88
+ return xarray.Dataset(
89
+ data_vars=data_vars,
90
+ attrs={
91
+ "provider_name": metadata.provider_name,
92
+ "dataset_id": metadata.dataset_id,
93
+ "dataset_name": metadata.dataset_name,
94
+ "dataset_crs": metadata.dataset_crs,
95
+ "aoi_id": metadata.aoi_id,
96
+ "data_request_id": metadata.data_request_id,
97
+ },
98
+ )
99
+
100
+
101
+ def load_xarray_v2(load_xarray_info: DataRequestLoadXarray) -> xarray.Dataset:
102
+ data_vars = {}
103
+
104
+ keys = _get_xarray_keys(load_xarray_info.bucket)
105
+ for key in keys:
106
+ timestamp_pattern = re.compile(r"\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}")
107
+ timestamp_str = timestamp_pattern.search(key).group()
108
+
109
+ variable_name = key.split("/")[14]
110
+ filename = f"s3://{load_xarray_info.bucket.name}/{key}"
111
+ dataset = rioxarray.open_rasterio(filename, chunks={"x": 2000, "y": 2000})
112
+ band = dataset.sel(band=1, drop=True)
113
+ band.name = variable_name
114
+
115
+ # Dataset without time information
116
+ if timestamp_str != "0000/00/00/00/00/00":
117
+ time = datetime.strptime(timestamp_str, "%Y/%m/%d/%H/%M/%S")
118
+ band = band.expand_dims("time")
119
+ band = band.assign_coords(time=[time])
120
+
121
+ if variable_name not in data_vars:
122
+ data_vars[variable_name] = []
123
+
124
+ data_vars[variable_name].append(band)
125
+
126
+ for variable_name, time_series in data_vars.items():
127
+ if "time" in time_series[0].dims:
128
+ data_vars[variable_name] = xarray.concat(
129
+ time_series, dim="time", join="exact"
130
+ )
131
+ else:
132
+ data_vars[variable_name] = time_series[0]
133
+
134
+ return xarray.Dataset(
135
+ data_vars=data_vars,
136
+ attrs={
137
+ "provider_name": load_xarray_info.provider_name,
138
+ "dataset_id": load_xarray_info.dataset_id,
139
+ "dataset_name": load_xarray_info.dataset_name,
140
+ "dataset_crs": load_xarray_info.dataset_crs,
141
+ "aoi_id": load_xarray_info.aoi_id,
142
+ "data_request_id": load_xarray_info.data_request_id,
143
+ },
144
+ )
145
+
146
+
147
+ def _get_xarray_keys(bucket: Bucket) -> list[str]:
148
+ os.environ["AWS_ACCESS_KEY_ID"] = bucket.access_key_id
149
+ os.environ["AWS_SECRET_ACCESS_KEY"] = bucket.secret_access_key
150
+ os.environ["AWS_SESSION_TOKEN"] = bucket.session_token
151
+
152
+ s3_client = boto3.client("s3")
153
+
154
+ paginator = s3_client.get_paginator("list_objects_v2")
155
+ page_iterator = paginator.paginate(
156
+ Bucket=bucket.name,
157
+ Prefix=bucket.prefix,
158
+ )
159
+
160
+ keys = []
161
+ for page in page_iterator:
162
+ for obj in page.get("Contents", []):
163
+ keys.append(obj["Key"])
164
+
165
+ return keys
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cecil
3
- Version: 0.0.21
3
+ Version: 0.0.30
4
4
  Summary: Python SDK for Cecil Earth
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE.txt
@@ -8,10 +8,13 @@ Classifier: Development Status :: 4 - Beta
8
8
  Classifier: License :: OSI Approved :: MIT License
9
9
  Classifier: Operating System :: OS Independent
10
10
  Classifier: Programming Language :: Python :: 3
11
- Requires-Python: >=3.8
12
- Requires-Dist: pydantic
13
- Requires-Dist: requests
14
- Requires-Dist: snowflake-connector-python[pandas]
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
15
18
  Description-Content-Type: text/markdown
16
19
 
17
20
  # Cecil SDK
@@ -0,0 +1,10 @@
1
+ cecil/__init__.py,sha256=AEcRl73BDSAQe6W0d1PDD87IEcumARtREl7dCVa_YQY,86
2
+ cecil/client.py,sha256=mhe7l133-uy7hpRWjcb2s4DD4wsnxPqJLoEuBKMut5I,9501
3
+ cecil/errors.py,sha256=EnyYvFfU_JWYTTRax58bdwOndri2f-HzbqyzxtoV8uo,2100
4
+ cecil/models.py,sha256=lI4UulUv-J0Qh4zrm1UBuqS96CDewyL6sGWRP4AQEQs,4163
5
+ cecil/version.py,sha256=8ZeepqkW4DvpVeNm92mx0tIzgvVevS4NKWkTXXHuXNY,23
6
+ cecil/xarray.py,sha256=K3IRfTkdWAJVUK0LgZLjztpeqhC4QptkrdNg9WYoIVk,5024
7
+ cecil-0.0.30.dist-info/METADATA,sha256=-G1QZ40hNHT_Nz8p5G9PFpqZTTnZoYeW6eEslNdlUGw,2800
8
+ cecil-0.0.30.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
+ cecil-0.0.30.dist-info/licenses/LICENSE.txt,sha256=mUexcmfYx3bG1VIzAdQTOf_NzStYw6-QkKVdUY_d4i4,1066
10
+ cecil-0.0.30.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- cecil/__init__.py,sha256=AEcRl73BDSAQe6W0d1PDD87IEcumARtREl7dCVa_YQY,86
2
- cecil/client.py,sha256=TXcTdNUF8ER8gs8KQBzHIsoQPqDLj3npFeF0PWEi4EI,7430
3
- cecil/errors.py,sha256=ZNiSTYH2MgNZ7tNIgV07-Ge3KtmdncfzWiBi9yjURGs,1818
4
- cecil/models.py,sha256=GpW1pT9NBKS5y4Os0pW8UR3MO9kVJe1r8jOK-MjADLA,2799
5
- cecil/version.py,sha256=PsqtE_T084MVsMv47JyTQ3DK2CRZJ3Kd9Q_vnw02oZk,23
6
- cecil-0.0.21.dist-info/METADATA,sha256=2Q9DsNWUMtimtqNDdDqbssyFGT33DgWaVjrYHrkLmrs,2659
7
- cecil-0.0.21.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
- cecil-0.0.21.dist-info/licenses/LICENSE.txt,sha256=mUexcmfYx3bG1VIzAdQTOf_NzStYw6-QkKVdUY_d4i4,1066
9
- cecil-0.0.21.dist-info/RECORD,,
File without changes