terrakio-core 0.4.93__py3-none-any.whl → 0.4.95__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 terrakio-core might be problematic. Click here for more details.
- terrakio_core/__init__.py +1 -1
- terrakio_core/async_client.py +4 -1
- terrakio_core/convenience_functions/zonal_stats.py +75 -73
- terrakio_core/endpoints/mass_stats.py +54 -17
- {terrakio_core-0.4.93.dist-info → terrakio_core-0.4.95.dist-info}/METADATA +2 -1
- {terrakio_core-0.4.93.dist-info → terrakio_core-0.4.95.dist-info}/RECORD +7 -7
- {terrakio_core-0.4.93.dist-info → terrakio_core-0.4.95.dist-info}/WHEEL +0 -0
terrakio_core/__init__.py
CHANGED
terrakio_core/async_client.py
CHANGED
|
@@ -17,7 +17,9 @@ from .endpoints.group_management import GroupManagement
|
|
|
17
17
|
from .endpoints.space_management import SpaceManagement
|
|
18
18
|
from .endpoints.model_management import ModelManagement
|
|
19
19
|
from .endpoints.auth import AuthClient
|
|
20
|
-
from .convenience_functions.
|
|
20
|
+
from .convenience_functions.zonal_stats import zonal_stats as _zonal_stats
|
|
21
|
+
from .convenience_functions.geoquries import request_geoquery_list as _request_geoquery_list
|
|
22
|
+
from .convenience_functions.create_dataset_file import create_dataset_file as _create_dataset_file
|
|
21
23
|
|
|
22
24
|
class AsyncClient(BaseClient):
|
|
23
25
|
def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None, verbose: bool = False, session: Optional[aiohttp.ClientSession] = None):
|
|
@@ -224,6 +226,7 @@ class AsyncClient(BaseClient):
|
|
|
224
226
|
ValueError: If concurrency is too high or if data exceeds memory limit without streaming
|
|
225
227
|
APIError: If the API request fails
|
|
226
228
|
"""
|
|
229
|
+
# the sync client didn't pass the self here, so the client is now async
|
|
227
230
|
return await _zonal_stats(
|
|
228
231
|
client=self,
|
|
229
232
|
gdf=gdf,
|
|
@@ -56,88 +56,90 @@ class cloud_object(gpd.GeoDataFrame):
|
|
|
56
56
|
track_info = await self.client.mass_stats.track_job([self.job_id])
|
|
57
57
|
job_info = track_info[self.job_id]
|
|
58
58
|
status = job_info['status']
|
|
59
|
+
|
|
59
60
|
if status == "Completed":
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
payload = {
|
|
62
|
+
"job_name": job_info["name"],
|
|
63
|
+
"file_type": "raw",
|
|
64
|
+
"bucket": job_info["bucket"],
|
|
65
|
+
}
|
|
66
|
+
result = await self.client._terrakio_request("POST", "mass_stats/download_files", json=payload)
|
|
67
|
+
download_urls = result["download_urls"][:n]
|
|
68
|
+
datasets = []
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
self.client.logger.info(f"Downloading dataset {i+1}/{len(download_urls)}...")
|
|
73
|
-
async with session.get(url) as response:
|
|
74
|
-
if response.status == 200:
|
|
75
|
-
content = await response.read()
|
|
76
|
-
dataset = xr.open_dataset(BytesIO(content))
|
|
77
|
-
datasets.append(dataset)
|
|
78
|
-
self.client.logger.info(f"Successfully processed dataset {i+1}")
|
|
79
|
-
else:
|
|
80
|
-
self.client.logger.warning(f"Failed to download dataset {i+1}: HTTP {response.status}")
|
|
81
|
-
except Exception as e:
|
|
82
|
-
self.client.logger.error(f"Error downloading dataset {i+1}: {e}")
|
|
83
|
-
continue
|
|
84
|
-
if not datasets:
|
|
85
|
-
self.client.logger.warning("No datasets were successfully downloaded")
|
|
86
|
-
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
70
|
+
async with aiohttp.ClientSession() as session:
|
|
71
|
+
for i, url in enumerate(download_urls):
|
|
87
72
|
try:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
params={"job_name": job_info['name']}
|
|
91
|
-
)
|
|
92
|
-
json_url = json_response["download_url"]
|
|
93
|
-
|
|
94
|
-
async with session.get(json_url) as response:
|
|
73
|
+
self.client.logger.info(f"Downloading dataset {i+1}/{len(download_urls)}...")
|
|
74
|
+
async with session.get(url) as response:
|
|
95
75
|
if response.status == 200:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
max_geometries = min(n, len(json_data), len(datasets))
|
|
101
|
-
|
|
102
|
-
for i in range(max_geometries):
|
|
103
|
-
try:
|
|
104
|
-
geom_dict = json_data[i]["request"]["feature"]["geometry"]
|
|
105
|
-
shapely_geom = shape(geom_dict)
|
|
106
|
-
geometries.append(shapely_geom)
|
|
107
|
-
except (KeyError, ValueError) as e:
|
|
108
|
-
self.client.logger.warning(f"Error parsing geometry {i}: {e}")
|
|
109
|
-
continue
|
|
110
|
-
|
|
111
|
-
min_length = min(len(datasets), len(geometries))
|
|
112
|
-
if min_length == 0:
|
|
113
|
-
self.client.logger.warning("No matching datasets and geometries found")
|
|
114
|
-
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
115
|
-
|
|
116
|
-
gdf = gpd.GeoDataFrame({
|
|
117
|
-
'geometry': geometries[:min_length],
|
|
118
|
-
'dataset': datasets[:min_length]
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
self.client.logger.info(f"Created GeoDataFrame with {len(gdf)} rows")
|
|
122
|
-
try:
|
|
123
|
-
expanded_gdf = expand_on_variables_and_time(gdf)
|
|
124
|
-
return expanded_gdf
|
|
125
|
-
except NameError:
|
|
126
|
-
self.client.logger.warning("expand_on_variables_and_time function not found, returning raw GeoDataFrame")
|
|
127
|
-
return gdf
|
|
128
|
-
|
|
76
|
+
content = await response.read()
|
|
77
|
+
dataset = xr.open_dataset(BytesIO(content))
|
|
78
|
+
datasets.append(dataset)
|
|
79
|
+
self.client.logger.info(f"Successfully processed dataset {i+1}")
|
|
129
80
|
else:
|
|
130
|
-
self.client.logger.warning(f"Failed to download
|
|
131
|
-
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
132
|
-
|
|
81
|
+
self.client.logger.warning(f"Failed to download dataset {i+1}: HTTP {response.status}")
|
|
133
82
|
except Exception as e:
|
|
134
|
-
|
|
83
|
+
self.client.logger.error(f"Error downloading dataset {i+1}: {e}")
|
|
84
|
+
continue
|
|
85
|
+
if not datasets:
|
|
86
|
+
self.client.logger.warning("No datasets were successfully downloaded")
|
|
87
|
+
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
88
|
+
try:
|
|
89
|
+
json_response = await self.client._terrakio_request(
|
|
90
|
+
"POST", "mass_stats/download_json",
|
|
91
|
+
params={"job_name": job_info['name']}
|
|
92
|
+
)
|
|
93
|
+
json_url = json_response["download_url"]
|
|
94
|
+
|
|
95
|
+
async with session.get(json_url) as response:
|
|
96
|
+
if response.status == 200:
|
|
97
|
+
json_data = await response.json()
|
|
98
|
+
self.client.logger.info("Successfully downloaded geometry data")
|
|
99
|
+
|
|
100
|
+
geometries = []
|
|
101
|
+
max_geometries = min(n, len(json_data), len(datasets))
|
|
102
|
+
|
|
103
|
+
for i in range(max_geometries):
|
|
104
|
+
try:
|
|
105
|
+
geom_dict = json_data[i]["request"]["feature"]["geometry"]
|
|
106
|
+
shapely_geom = shape(geom_dict)
|
|
107
|
+
geometries.append(shapely_geom)
|
|
108
|
+
except (KeyError, ValueError) as e:
|
|
109
|
+
self.client.logger.warning(f"Error parsing geometry {i}: {e}")
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
min_length = min(len(datasets), len(geometries))
|
|
113
|
+
if min_length == 0:
|
|
114
|
+
self.client.logger.warning("No matching datasets and geometries found")
|
|
115
|
+
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
116
|
+
|
|
117
|
+
gdf = gpd.GeoDataFrame({
|
|
118
|
+
'geometry': geometries[:min_length],
|
|
119
|
+
'dataset': datasets[:min_length]
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
self.client.logger.info(f"Created GeoDataFrame with {len(gdf)} rows")
|
|
123
|
+
try:
|
|
124
|
+
expanded_gdf = expand_on_variables_and_time(gdf)
|
|
125
|
+
return expanded_gdf
|
|
126
|
+
except NameError:
|
|
127
|
+
self.client.logger.warning("expand_on_variables_and_time function not found, returning raw GeoDataFrame")
|
|
128
|
+
return gdf
|
|
129
|
+
|
|
130
|
+
else:
|
|
131
|
+
self.client.logger.warning(f"Failed to download geometry data: HTTP {response.status}")
|
|
135
132
|
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
self.client.logger.error(f"Error downloading geometry data: {e}")
|
|
136
|
+
return gpd.GeoDataFrame({'geometry': [], 'dataset': []})
|
|
137
|
+
|
|
136
138
|
elif status in ["Failed", "Cancelled", "Error"]:
|
|
137
|
-
|
|
139
|
+
raise RuntimeError(f"The zonal stats job (job_id: {self.job_id}) has failed, cancelled, or errored. Please check the job status!")
|
|
140
|
+
|
|
138
141
|
else:
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
raise RuntimeError(f"The zonal stats job (job_id: {self.job_id}) is still running. Please come back at a later time!")
|
|
141
143
|
|
|
142
144
|
def expand_on_time(gdf):
|
|
143
145
|
"""
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import gzip
|
|
4
4
|
import os
|
|
5
5
|
import weakref
|
|
6
|
+
import weakref
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from urllib.parse import urlparse
|
|
8
9
|
from ..helper.decorators import require_token, require_api_key, require_auth
|
|
@@ -13,7 +14,6 @@ import xarray as xr
|
|
|
13
14
|
from io import BytesIO
|
|
14
15
|
import geopandas as gpd
|
|
15
16
|
from shapely.geometry import shape
|
|
16
|
-
from ..convenience_functions.convenience_functions import expand_on_variables_and_time
|
|
17
17
|
|
|
18
18
|
class MassStats:
|
|
19
19
|
def __init__(self, client):
|
|
@@ -145,6 +145,7 @@ class MassStats:
|
|
|
145
145
|
params = {"limit": limit}
|
|
146
146
|
return self._client._terrakio_request("GET", "mass_stats/history", params=params)
|
|
147
147
|
|
|
148
|
+
|
|
148
149
|
@require_api_key
|
|
149
150
|
async def start_post_processing(
|
|
150
151
|
self,
|
|
@@ -174,7 +175,6 @@ class MassStats:
|
|
|
174
175
|
@require_api_key
|
|
175
176
|
async def zonal_stats_transform(
|
|
176
177
|
self,
|
|
177
|
-
process_name: str,
|
|
178
178
|
data_name: str,
|
|
179
179
|
output: str,
|
|
180
180
|
consumer: bytes,
|
|
@@ -182,7 +182,6 @@ class MassStats:
|
|
|
182
182
|
) -> Dict[str, Any]:
|
|
183
183
|
|
|
184
184
|
data = aiohttp.FormData()
|
|
185
|
-
data.add_field('process_name', process_name)
|
|
186
185
|
data.add_field('data_name', data_name)
|
|
187
186
|
data.add_field('output', output)
|
|
188
187
|
data.add_field('overwrite', str(overwrite).lower())
|
|
@@ -393,11 +392,22 @@ class MassStats:
|
|
|
393
392
|
except Exception as e:
|
|
394
393
|
raise Exception(f"Error in download process: {e}")
|
|
395
394
|
|
|
396
|
-
def validate_request(self,
|
|
397
|
-
|
|
398
|
-
|
|
395
|
+
def validate_request(self, request_json: Union[str, List[Dict]]):
|
|
396
|
+
# Handle both file path and direct JSON data
|
|
397
|
+
if isinstance(request_json, str):
|
|
398
|
+
# It's a file path
|
|
399
|
+
with open(request_json, 'r') as file:
|
|
400
|
+
request_data = json.load(file)
|
|
401
|
+
elif isinstance(request_json, list):
|
|
402
|
+
# It's already JSON data
|
|
403
|
+
request_data = request_json
|
|
404
|
+
else:
|
|
405
|
+
raise ValueError("request_json must be either a file path (str) or JSON data (list)")
|
|
406
|
+
|
|
407
|
+
# Rest of validation logic stays exactly the same
|
|
399
408
|
if not isinstance(request_data, list):
|
|
400
|
-
raise ValueError(
|
|
409
|
+
raise ValueError("Request JSON should contain a list of dictionaries")
|
|
410
|
+
|
|
401
411
|
for i, request in enumerate(request_data):
|
|
402
412
|
if not isinstance(request, dict):
|
|
403
413
|
raise ValueError(f"Request {i} should be a dictionary")
|
|
@@ -421,7 +431,7 @@ class MassStats:
|
|
|
421
431
|
name: str,
|
|
422
432
|
output: str,
|
|
423
433
|
config: Dict[str, Any],
|
|
424
|
-
request_json: str, #
|
|
434
|
+
request_json: Union[str, List[Dict]], # ← Accept both file path OR data
|
|
425
435
|
region: str = None,
|
|
426
436
|
overwrite: bool = False,
|
|
427
437
|
skip_existing: bool = False,
|
|
@@ -469,19 +479,38 @@ class MassStats:
|
|
|
469
479
|
|
|
470
480
|
return groups
|
|
471
481
|
|
|
472
|
-
# Load and validate request JSON
|
|
482
|
+
# # Load and validate request JSON
|
|
483
|
+
# try:
|
|
484
|
+
# with open(request_json, 'r') as file:
|
|
485
|
+
# request_data = json.load(file)
|
|
486
|
+
# if isinstance(request_data, list):
|
|
487
|
+
# size = len(request_data)
|
|
488
|
+
# else:
|
|
489
|
+
# raise ValueError(f"Request JSON file {request_json} should contain a list of dictionaries")
|
|
490
|
+
# except FileNotFoundError as e:
|
|
491
|
+
# return e
|
|
492
|
+
# except json.JSONDecodeError as e:
|
|
493
|
+
# return e
|
|
473
494
|
try:
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
495
|
+
if isinstance(request_json, str):
|
|
496
|
+
# It's a file path
|
|
497
|
+
with open(request_json, 'r') as file:
|
|
498
|
+
request_data = json.load(file)
|
|
499
|
+
elif isinstance(request_json, list):
|
|
500
|
+
# It's already JSON data
|
|
501
|
+
request_data = request_json
|
|
502
|
+
else:
|
|
503
|
+
raise ValueError("request_json must be either a file path (str) or JSON data (list)")
|
|
504
|
+
|
|
505
|
+
if isinstance(request_data, list):
|
|
506
|
+
size = len(request_data)
|
|
507
|
+
else:
|
|
508
|
+
raise ValueError("Request JSON should contain a list of dictionaries")
|
|
480
509
|
except FileNotFoundError as e:
|
|
481
510
|
return e
|
|
482
511
|
except json.JSONDecodeError as e:
|
|
483
512
|
return e
|
|
484
|
-
|
|
513
|
+
|
|
485
514
|
# Generate manifest from request data (kept in memory)
|
|
486
515
|
try:
|
|
487
516
|
manifest_groups = extract_manifest_from_request(request_data)
|
|
@@ -516,8 +545,16 @@ class MassStats:
|
|
|
516
545
|
# Upload request JSON file
|
|
517
546
|
try:
|
|
518
547
|
self.validate_request(request_json)
|
|
519
|
-
|
|
548
|
+
|
|
549
|
+
if isinstance(request_json, str):
|
|
550
|
+
# File path - use existing _upload_file method
|
|
551
|
+
requests_response = await self._upload_file(request_json, requests_url, use_gzip=True)
|
|
552
|
+
else:
|
|
553
|
+
# JSON data - use _upload_json_data method
|
|
554
|
+
requests_response = await self._upload_json_data(request_json, requests_url, use_gzip=True)
|
|
555
|
+
|
|
520
556
|
if requests_response.status not in [200, 201, 204]:
|
|
557
|
+
# ... rest stays the same
|
|
521
558
|
self._client.logger.error(f"Requests upload error: {requests_response.text()}")
|
|
522
559
|
raise Exception(f"Failed to upload request JSON: {requests_response.text()}")
|
|
523
560
|
except Exception as e:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: terrakio-core
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.95
|
|
4
4
|
Summary: Core package for the terrakio-python-api
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: aiofiles>=24.1.0
|
|
@@ -8,6 +8,7 @@ Requires-Dist: aiohttp>=3.12.15
|
|
|
8
8
|
Requires-Dist: geopandas>=1.1.1
|
|
9
9
|
Requires-Dist: h5netcdf>=1.6.3
|
|
10
10
|
Requires-Dist: h5py>=3.14.0
|
|
11
|
+
Requires-Dist: nest-asyncio>=1.6.0
|
|
11
12
|
Requires-Dist: netcdf4>=1.7.2
|
|
12
13
|
Requires-Dist: onnxruntime>=1.22.1
|
|
13
14
|
Requires-Dist: psutil>=7.0.0
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
terrakio_core/__init__.py,sha256=
|
|
1
|
+
terrakio_core/__init__.py,sha256=oQmSomwovopKB9FJwxuboCtt6Sd5BwiCuROUTTR92WI,274
|
|
2
2
|
terrakio_core/accessors.py,sha256=pVZ7qqkkiZdUN5DwqDFwWNHRMfzT9pLnDehI8yiUNVw,43595
|
|
3
|
-
terrakio_core/async_client.py,sha256=
|
|
3
|
+
terrakio_core/async_client.py,sha256=txdSsX3IwqtHlcS86u6N6vjV0-PIiermxNOIjEMQ3Yg,14950
|
|
4
4
|
terrakio_core/client.py,sha256=VXP7BtJWIfpPPZR7_yNdSTcGwNgTwhb7KorusqkQrzk,5603
|
|
5
5
|
terrakio_core/config.py,sha256=r8NARVYOca4AuM88VP_j-8wQxOk1s7VcRdyEdseBlLE,4193
|
|
6
6
|
terrakio_core/exceptions.py,sha256=4qnpOM1gOxsNIXDXY4qwY1d3I4Myhp7HBh7b2D0SVrU,529
|
|
7
7
|
terrakio_core/sync_client.py,sha256=jbG2sMnbR3QPvhAxQX2dBWeX_6f-Qx_MFSRLLpvfRh4,14604
|
|
8
8
|
terrakio_core/convenience_functions/create_dataset_file.py,sha256=RDTAQnKUigyczv3EKhKrs34VMDZDCgL4iz0bge1d9e4,4774
|
|
9
9
|
terrakio_core/convenience_functions/geoquries.py,sha256=zIgt4fDCBgOIUM_h7-a6brOG-Mi2C_bQdnqcSliTVDs,3766
|
|
10
|
-
terrakio_core/convenience_functions/zonal_stats.py,sha256=
|
|
10
|
+
terrakio_core/convenience_functions/zonal_stats.py,sha256=B0c95M5yqGE3uC2_Cad3FKXeaz97hVHdnaWSMdJdKsU,19496
|
|
11
11
|
terrakio_core/endpoints/auth.py,sha256=FdLsPScPIBo-Gxl6ZnE-46cp2molggAJtL72LssN3fg,6049
|
|
12
12
|
terrakio_core/endpoints/dataset_management.py,sha256=BUm8IIlW_Q45vDiQp16CiJGeSLheI8uWRVRQtMdhaNk,13161
|
|
13
13
|
terrakio_core/endpoints/group_management.py,sha256=VFl3jakjQa9OPi351D3DZvLU9M7fHdfjCzGhmyJsx3U,6309
|
|
14
|
-
terrakio_core/endpoints/mass_stats.py,sha256=
|
|
14
|
+
terrakio_core/endpoints/mass_stats.py,sha256=Vb6Tf8kKf5Hlch4ddsrQnfayfiK6z7NSjO8D0pop4p8,25699
|
|
15
15
|
terrakio_core/endpoints/model_management.py,sha256=LH_gHPrqYA-_45KWpDBRcFbwHgm-Kg0zk1ealy7P_C0,52379
|
|
16
16
|
terrakio_core/endpoints/space_management.py,sha256=YWb55nkJnFJGlALJ520DvurxDqVqwYtsvqQPWzxzhDs,2266
|
|
17
17
|
terrakio_core/endpoints/user_management.py,sha256=WlFr3EfK8iI6DfkpMuYLHZUPk2n7_DHHO6z1hndmZB4,3816
|
|
18
18
|
terrakio_core/helper/bounded_taskgroup.py,sha256=wiTH10jhKZgrsgrFUNG6gig8bFkUEPHkGRT2XY7Rgmo,677
|
|
19
19
|
terrakio_core/helper/decorators.py,sha256=L6om7wmWNgCei3Wy5U0aZ-70OzsCwclkjIf7SfQuhCg,2289
|
|
20
20
|
terrakio_core/helper/tiles.py,sha256=lcLCO6KiP05lCI9vngo3zCZJ6Z9C0pUxHSQS4H58EHc,2699
|
|
21
|
-
terrakio_core-0.4.
|
|
22
|
-
terrakio_core-0.4.
|
|
23
|
-
terrakio_core-0.4.
|
|
21
|
+
terrakio_core-0.4.95.dist-info/METADATA,sha256=gAjc5wDDg2a8vmkBVZWSK1QdtMTlaq9EGFY9qyQ16q4,1151
|
|
22
|
+
terrakio_core-0.4.95.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
terrakio_core-0.4.95.dist-info/RECORD,,
|
|
File without changes
|