terrakio-core 0.4.8__py3-none-any.whl → 0.4.94__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.

@@ -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):
@@ -34,11 +36,15 @@ class AsyncClient(BaseClient):
34
36
 
35
37
  async def _terrakio_request(self, method: str, endpoint: str, **kwargs):
36
38
  if self.session is None:
39
+ # To this:
37
40
  headers = {
38
- 'Content-Type': 'application/json',
39
- 'x-api-key': self.key,
41
+ 'x-api-key': self.key,
40
42
  'Authorization': self.token
41
43
  }
44
+
45
+ # Only add Content-Type if it's a JSON request
46
+ if 'json' in kwargs:
47
+ headers['Content-Type'] = 'application/json'
42
48
  clean_headers = {k: v for k, v in headers.items() if v is not None}
43
49
  async with aiohttp.ClientSession(headers=clean_headers, timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
44
50
  return await self._make_request_with_retry(session, method, endpoint, **kwargs)
@@ -220,6 +226,7 @@ class AsyncClient(BaseClient):
220
226
  ValueError: If concurrency is too high or if data exceeds memory limit without streaming
221
227
  APIError: If the API request fails
222
228
  """
229
+ # the sync client didn't pass the self here, so the client is now async
223
230
  return await _zonal_stats(
224
231
  client=self,
225
232
  gdf=gdf,
@@ -0,0 +1,132 @@
1
+ import asyncio
2
+ import os
3
+ import tempfile
4
+ import time
5
+ import uuid
6
+
7
+ from ..helper.tiles import tiles
8
+
9
+ async def create_dataset_file(
10
+ client,
11
+ aoi: str,
12
+ expression: str,
13
+ output: str,
14
+ download_path: str,
15
+ in_crs: str = "epsg:4326",
16
+ to_crs: str = "epsg:4326",
17
+ res: float = 0.0001,
18
+ region: str = None,
19
+ overwrite: bool = False,
20
+ skip_existing: bool = False,
21
+ non_interactive: bool = True,
22
+ name: str | None = None,
23
+ poll_interval: int = 30,
24
+ max_file_size_mb: int = 5120,
25
+ tile_size: int = 1024,
26
+ mask: bool = True
27
+ ) -> dict:
28
+
29
+ if not name:
30
+ name = f"file-gen-{uuid.uuid4().hex[:8]}"
31
+
32
+ body, reqs, groups = tiles(
33
+ name = name,
34
+ aoi = aoi,
35
+ expression = expression,
36
+ output = output,
37
+ tile_size = tile_size,
38
+ crs = in_crs,
39
+ res = res,
40
+ region = region,
41
+ to_crs = to_crs,
42
+ mask = mask,
43
+ overwrite = overwrite,
44
+ skip_existing = skip_existing,
45
+ non_interactive = non_interactive
46
+ )
47
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tempreq:
48
+ tempreq.write(reqs)
49
+ tempreqname = tempreq.name
50
+
51
+ task_id = await client.mass_stats.execute_job(
52
+ name=body["name"],
53
+ region=body["region"],
54
+ output=body["output"],
55
+ config = {},
56
+ overwrite=body["overwrite"],
57
+ skip_existing=body["skip_existing"],
58
+ request_json=tempreqname,
59
+ )
60
+
61
+ start_time = time.time()
62
+ status = None
63
+ client.logger.info(f"Tracking data generation job {task_id['task_id']}...")
64
+ while True:
65
+ try:
66
+ taskid = task_id['task_id']
67
+ trackinfo = await client.mass_stats.track_job([taskid])
68
+ status = trackinfo[taskid]['status']
69
+ if status == 'Completed':
70
+ client.logger.info('Data generated successfully!')
71
+ break
72
+ elif status in ['Failed', 'Cancelled', 'Error']:
73
+ raise RuntimeError(f"Job {taskid} failed with status: {status}")
74
+ else:
75
+ elapsed_time = time.time() - start_time
76
+ client.logger.info(f"Job status: {status} - Elapsed time: {elapsed_time:.1f}s")
77
+ await asyncio.sleep(poll_interval)
78
+
79
+
80
+ except KeyboardInterrupt:
81
+ client.logger.info(f"\nInterrupted! Job {taskid} is still running in the background.")
82
+ raise
83
+ except Exception as e:
84
+ client.logger.info(f"\nError tracking job: {e}")
85
+ raise
86
+
87
+ os.unlink(tempreqname)
88
+
89
+ combine_result = await client.mass_stats.combine_tiles(body["name"], body["overwrite"], body["output"], max_file_size_mb=max_file_size_mb)
90
+ combine_task_id = combine_result.get("task_id")
91
+
92
+ combine_start_time = time.time()
93
+ client.logger.info(f"Tracking file generation job {combine_task_id}...")
94
+ while True:
95
+ try:
96
+ trackinfo = await client.mass_stats.track_job([combine_task_id])
97
+ if body["output"] == "netcdf":
98
+ download_file_name = trackinfo[combine_task_id]['folder'] + '.nc'
99
+ elif body["output"] == "geotiff":
100
+ download_file_name = trackinfo[combine_task_id]['folder'] + '.tif'
101
+ bucket = trackinfo[combine_task_id]['bucket']
102
+ combine_status = trackinfo[combine_task_id]['status']
103
+ if combine_status == 'Completed':
104
+ client.logger.info('File/s generated successfully!')
105
+ break
106
+ elif combine_status in ['Failed', 'Cancelled', 'Error']:
107
+ raise RuntimeError(f"File generation job {combine_task_id} failed with status: {combine_status}")
108
+ else:
109
+ elapsed_time = time.time() - combine_start_time
110
+ client.logger.info(f"File generation job status: {combine_status} - Elapsed time: {elapsed_time:.1f}s")
111
+ time.sleep(poll_interval)
112
+ except KeyboardInterrupt:
113
+ client.logger.info(f"\nInterrupted! File generation job {combine_task_id} is still running in the background.")
114
+ raise
115
+ except Exception as e:
116
+ client.logger.info(f"\nError tracking file generation job: {e}")
117
+ raise
118
+
119
+ if download_path:
120
+ await client.mass_stats.download_file(
121
+ job_name=body["name"],
122
+ bucket=bucket,
123
+ file_type='processed',
124
+ folder='file-gen',
125
+ page_size=100,
126
+ output_path=download_path,
127
+ )
128
+ else:
129
+ path = f"{body['name']}/outputs/merged/{download_file_name}"
130
+ client.logger.info(f"Dataset file/s is available at {path}")
131
+
132
+ return {"generation_task_id": task_id, "combine_task_id": combine_task_id}
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+
3
+ import geopandas as gpd
4
+ from shapely.geometry import shape
5
+
6
+ from ..exceptions import APIError
7
+ from ..helper.bounded_taskgroup import BoundedTaskGroup
8
+
9
+ async def request_geoquery_list(
10
+ client,
11
+ quries: list[dict],
12
+ conc: int = 20,
13
+ ):
14
+ """
15
+ Execute multiple geo queries.
16
+
17
+ Args:
18
+ client: The Terrakio client instance
19
+ quries: List of dictionaries containing query parameters
20
+ conc: The concurrency level for the requests
21
+
22
+ Returns:
23
+ List of query results
24
+
25
+ Raises:
26
+ ValueError: If the queries list is empty
27
+ """
28
+ if not quries:
29
+ raise ValueError("Queries list cannot be empty")
30
+ if conc > 100:
31
+ raise ValueError("Concurrency (conc) is too high. Please set conc to 100 or less.")
32
+
33
+ for i, query in enumerate(quries):
34
+ if 'expr' not in query:
35
+ raise ValueError(f"Query at index {i} is missing the required 'expr' key")
36
+ if 'feature' not in query:
37
+ raise ValueError(f"Query at index {i} is missing the required 'feature' key")
38
+ if 'in_crs' not in query:
39
+ raise ValueError(f"Query at index {i} is missing the required 'in_crs' key")
40
+
41
+ completed_count = 0
42
+ lock = asyncio.Lock()
43
+ async def single_geo_query(query):
44
+ """
45
+ Execute multiple geo queries concurrently.
46
+
47
+ Args:
48
+ quries: List of dictionaries containing query parameters
49
+ """
50
+ total_number_of_requests = len(quries)
51
+ nonlocal completed_count
52
+ try:
53
+ result = await client.geoquery(**query)
54
+ if isinstance(result, dict) and result.get("error"):
55
+ error_msg = f"Request failed: {result.get('error_message', 'Unknown error')}"
56
+ if result.get('status_code'):
57
+ error_msg = f"Request failed with status {result['status_code']}: {result.get('error_message', 'Unknown error')}"
58
+ raise APIError(error_msg)
59
+ if isinstance(result, list):
60
+ result = result[0]
61
+ timestamp_number = result['request_count']
62
+ return timestamp_number
63
+ if not isinstance(result, xr.Dataset):
64
+ raise ValueError(f"Expected xarray Dataset, got {type(result)}")
65
+
66
+ async with lock:
67
+ completed_count += 1
68
+ if completed_count % max(1, total_number_of_requests // 10) == 0:
69
+ client.logger.info(f"Progress: {completed_count}/{total_number_of_requests} requests processed")
70
+ return result
71
+ except Exception as e:
72
+ async with lock:
73
+ completed_count += 1
74
+ raise
75
+
76
+ try:
77
+ async with BoundedTaskGroup(max_concurrency=conc) as tg:
78
+ tasks = [tg.create_task(single_geo_query(quries[idx])) for idx in range(len(quries))]
79
+ all_results = [task.result() for task in tasks]
80
+
81
+ except* Exception as eg:
82
+ for e in eg.exceptions:
83
+ if hasattr(e, 'response'):
84
+ raise APIError(f"API request failed: {e.response.text}")
85
+ raise
86
+ client.logger.info("All requests completed!")
87
+
88
+ if not all_results:
89
+ raise ValueError("No valid results were returned for any geometry")
90
+ if isinstance(all_results, list) and type(all_results[0]) == int:
91
+ return sum(all_results)/len(all_results)
92
+ else:
93
+ geometries = []
94
+ for query in quries:
95
+ feature = query['feature']
96
+ geometry = shape(feature['geometry'])
97
+ geometries.append(geometry)
98
+ result_gdf = gpd.GeoDataFrame({
99
+ 'geometry': geometries,
100
+ 'dataset': all_results
101
+ })
102
+ return result_gdf