terrakio-core 0.4.93__tar.gz → 0.4.95__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.

Potentially problematic release.


This version of terrakio-core might be problematic. Click here for more details.

Files changed (24) hide show
  1. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/PKG-INFO +2 -1
  2. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/pyproject.toml +5 -1
  3. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/__init__.py +1 -1
  4. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/async_client.py +4 -1
  5. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/convenience_functions/zonal_stats.py +75 -73
  6. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/mass_stats.py +54 -17
  7. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/.gitignore +0 -0
  8. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/README.md +0 -0
  9. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/accessors.py +0 -0
  10. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/client.py +0 -0
  11. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/config.py +0 -0
  12. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/convenience_functions/create_dataset_file.py +0 -0
  13. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/convenience_functions/geoquries.py +0 -0
  14. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/auth.py +0 -0
  15. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/dataset_management.py +0 -0
  16. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/group_management.py +0 -0
  17. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/model_management.py +0 -0
  18. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/space_management.py +0 -0
  19. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/endpoints/user_management.py +0 -0
  20. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/exceptions.py +0 -0
  21. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/helper/bounded_taskgroup.py +0 -0
  22. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/helper/decorators.py +0 -0
  23. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/helper/tiles.py +0 -0
  24. {terrakio_core-0.4.93 → terrakio_core-0.4.95}/terrakio_core/sync_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: terrakio-core
3
- Version: 0.4.93
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "terrakio-core"
3
- version = "0.4.93"
3
+ version = "0.4.95"
4
4
  description = "Core package for the terrakio-python-api"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -11,11 +11,15 @@ dependencies = [
11
11
  "h5netcdf>=1.6.3",
12
12
  "h5py>=3.14.0",
13
13
  "netcdf4>=1.7.2",
14
+ "h5py>=3.14.0",
15
+ "netcdf4>=1.7.2",
14
16
  "onnxruntime>=1.22.1",
15
17
  "psutil>=7.0.0",
16
18
  "scipy>=1.16.1",
19
+ "scipy>=1.16.1",
17
20
  "shapely>=2.1.1",
18
21
  "xarray>=2025.7.1",
22
+ "nest-asyncio>=1.6.0",
19
23
  ]
20
24
 
21
25
  [project.optional-dependencies]
@@ -5,7 +5,7 @@ Terrakio Core
5
5
  Core components for Terrakio API clients.
6
6
  """
7
7
 
8
- __version__ = "0.4.93"
8
+ __version__ = "0.4.95"
9
9
 
10
10
  from .async_client import AsyncClient
11
11
  from .sync_client import SyncClient as Client
@@ -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.convenience_functions import zonal_stats as _zonal_stats, create_dataset_file as _create_dataset_file, request_geoquery_list as _request_geoquery_list
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
- payload = {
61
- "job_name": job_info["name"],
62
- "file_type": "raw",
63
- "bucket": job_info["bucket"],
64
- }
65
- result = await self.client._terrakio_request("POST", "mass_stats/download_files", json=payload)
66
- download_urls = result["download_urls"][:n]
67
- datasets = []
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
- async with aiohttp.ClientSession() as session:
70
- for i, url in enumerate(download_urls):
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
- json_response = await self.client._terrakio_request(
89
- "POST", "mass_stats/download_json",
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
- json_data = await response.json()
97
- self.client.logger.info("Successfully downloaded geometry data")
98
-
99
- geometries = []
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 geometry data: HTTP {response.status}")
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
- self.client.logger.error(f"Error downloading geometry data: {e}")
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
- return "The zonal stats job(for preparing the data) has failed, please check the job status!"
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
- return "The zonal stats job(for preparing the data) is still runningm, please come back at a later time!"
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, request_json_path: str):
397
- with open(request_json_path, 'r') as file:
398
- request_data = json.load(file)
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(f"Request JSON file {request_json_path} should contain a list of dictionaries")
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, # Path to request JSON file
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
- with open(request_json, 'r') as file:
475
- request_data = json.load(file)
476
- if isinstance(request_data, list):
477
- size = len(request_data)
478
- else:
479
- raise ValueError(f"Request JSON file {request_json} should contain a list of dictionaries")
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
- requests_response = await self._upload_file(request_json, requests_url, use_gzip=True)
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:
File without changes