terrakio-core 0.4.3__py3-none-any.whl → 0.4.5__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 +3 -1
- terrakio_core/accessors.py +477 -0
- terrakio_core/async_client.py +67 -39
- terrakio_core/client.py +83 -84
- terrakio_core/convenience_functions/convenience_functions.py +316 -324
- terrakio_core/endpoints/auth.py +8 -1
- terrakio_core/endpoints/mass_stats.py +13 -9
- terrakio_core/endpoints/model_management.py +604 -948
- terrakio_core/sync_client.py +436 -44
- {terrakio_core-0.4.3.dist-info → terrakio_core-0.4.5.dist-info}/METADATA +2 -1
- terrakio_core-0.4.5.dist-info/RECORD +22 -0
- terrakio_core-0.4.3.dist-info/RECORD +0 -21
- {terrakio_core-0.4.3.dist-info → terrakio_core-0.4.5.dist-info}/WHEEL +0 -0
- {terrakio_core-0.4.3.dist-info → terrakio_core-0.4.5.dist-info}/top_level.txt +0 -0
terrakio_core/sync_client.py
CHANGED
|
@@ -1,6 +1,346 @@
|
|
|
1
|
+
# import asyncio
|
|
2
|
+
# import concurrent.futures
|
|
3
|
+
# import threading
|
|
4
|
+
# import functools
|
|
5
|
+
# import inspect
|
|
6
|
+
# from typing import Optional, Dict, Any, Union
|
|
7
|
+
# from geopandas import GeoDataFrame
|
|
8
|
+
# from shapely.geometry.base import BaseGeometry as ShapelyGeometry
|
|
9
|
+
# from .async_client import AsyncClient
|
|
10
|
+
# from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
# # # Add this after your other imports
|
|
13
|
+
# # if TYPE_CHECKING:
|
|
14
|
+
# # from .endpoints.dataset_management import DatasetManagement
|
|
15
|
+
# # from .endpoints.user_management import UserManagement
|
|
16
|
+
# # from .endpoints.mass_stats import MassStats
|
|
17
|
+
# # from .endpoints.group_management import GroupManagement
|
|
18
|
+
# # from .endpoints.space_management import SpaceManagement
|
|
19
|
+
# # from .endpoints.model_management import ModelManagement
|
|
20
|
+
# # from .endpoints.auth import AuthClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# class SyncWrapper:
|
|
24
|
+
# """Generic synchronous wrapper with __dir__ support for runtime autocomplete."""
|
|
25
|
+
|
|
26
|
+
# def __init__(self, async_obj, sync_client):
|
|
27
|
+
# self._async_obj = async_obj
|
|
28
|
+
# self._sync_client = sync_client
|
|
29
|
+
|
|
30
|
+
# def __dir__(self):
|
|
31
|
+
# """Return list of attributes for autocomplete in interactive environments."""
|
|
32
|
+
# async_attrs = [attr for attr in dir(self._async_obj) if not attr.startswith('_')]
|
|
33
|
+
# wrapper_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
|
|
34
|
+
# return list(set(async_attrs + wrapper_attrs))
|
|
35
|
+
|
|
36
|
+
# def __getattr__(self, name):
|
|
37
|
+
# """Dynamically wrap any method call to convert async to sync."""
|
|
38
|
+
# attr = getattr(self._async_obj, name)
|
|
39
|
+
|
|
40
|
+
# if callable(attr):
|
|
41
|
+
# @functools.wraps(attr)
|
|
42
|
+
# def sync_wrapper(*args, **kwargs):
|
|
43
|
+
# result = attr(*args, **kwargs)
|
|
44
|
+
# if hasattr(result, '__await__'):
|
|
45
|
+
# return self._sync_client._run_async(result)
|
|
46
|
+
# return result
|
|
47
|
+
# return sync_wrapper
|
|
48
|
+
|
|
49
|
+
# return attr
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# class SyncClient:
|
|
53
|
+
# """
|
|
54
|
+
# Thread-safe synchronous wrapper for AsyncClient.
|
|
55
|
+
# Uses a persistent event loop in a dedicated thread to avoid event loop conflicts.
|
|
56
|
+
# """
|
|
57
|
+
|
|
58
|
+
# # datasets: 'DatasetManagement'
|
|
59
|
+
# # users: 'UserManagement'
|
|
60
|
+
# # mass_stats: 'MassStats'
|
|
61
|
+
# # groups: 'GroupManagement'
|
|
62
|
+
# # space: 'SpaceManagement'
|
|
63
|
+
# # model: 'ModelManagement'
|
|
64
|
+
# # auth: 'AuthClient'
|
|
65
|
+
|
|
66
|
+
# def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None, verbose: bool = False):
|
|
67
|
+
# self._async_client = AsyncClient(url=url, api_key=api_key, verbose=verbose)
|
|
68
|
+
# self._context_entered = False
|
|
69
|
+
# self._closed = False
|
|
70
|
+
|
|
71
|
+
# # Thread and event loop management
|
|
72
|
+
# self._loop = None
|
|
73
|
+
# self._thread = None
|
|
74
|
+
# self._loop_ready = None
|
|
75
|
+
# self._loop_exception = None
|
|
76
|
+
|
|
77
|
+
# # Initialize endpoint managers
|
|
78
|
+
# print("we are here!!!!!!!!!!!!!!!!!")
|
|
79
|
+
# self.datasets = SyncWrapper(self._async_client.datasets, self)
|
|
80
|
+
# self.users = SyncWrapper(self._async_client.users, self)
|
|
81
|
+
# self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
|
|
82
|
+
# self.groups = SyncWrapper(self._async_client.groups, self)
|
|
83
|
+
# self.space = SyncWrapper(self._async_client.space, self)
|
|
84
|
+
# self.model = SyncWrapper(self._async_client.model, self)
|
|
85
|
+
# self.auth = SyncWrapper(self._async_client.auth, self)
|
|
86
|
+
|
|
87
|
+
# # Register cleanup
|
|
88
|
+
# import atexit
|
|
89
|
+
# atexit.register(self._cleanup)
|
|
90
|
+
|
|
91
|
+
# def _ensure_event_loop(self):
|
|
92
|
+
# """Ensure we have a persistent event loop in a dedicated thread."""
|
|
93
|
+
# if self._loop is None or self._loop.is_closed():
|
|
94
|
+
# self._loop_ready = threading.Event()
|
|
95
|
+
# self._loop_exception = None
|
|
96
|
+
|
|
97
|
+
# def run_loop():
|
|
98
|
+
# """Run the event loop in a dedicated thread."""
|
|
99
|
+
# try:
|
|
100
|
+
# # Create a new event loop for this thread
|
|
101
|
+
# self._loop = asyncio.new_event_loop()
|
|
102
|
+
# asyncio.set_event_loop(self._loop)
|
|
103
|
+
|
|
104
|
+
# # Signal that the loop is ready
|
|
105
|
+
# self._loop_ready.set()
|
|
106
|
+
|
|
107
|
+
# # Run the loop forever (until stopped)
|
|
108
|
+
# self._loop.run_forever()
|
|
109
|
+
# except Exception as e:
|
|
110
|
+
# self._loop_exception = e
|
|
111
|
+
# self._loop_ready.set()
|
|
112
|
+
# finally:
|
|
113
|
+
# # Clean up when the loop stops
|
|
114
|
+
# if self._loop and not self._loop.is_closed():
|
|
115
|
+
# self._loop.close()
|
|
116
|
+
|
|
117
|
+
# # Start the thread
|
|
118
|
+
# self._thread = threading.Thread(target=run_loop, daemon=True)
|
|
119
|
+
# self._thread.start()
|
|
120
|
+
|
|
121
|
+
# # Wait for the loop to be ready
|
|
122
|
+
# self._loop_ready.wait(timeout=10)
|
|
123
|
+
|
|
124
|
+
# if self._loop_exception:
|
|
125
|
+
# raise self._loop_exception
|
|
126
|
+
|
|
127
|
+
# if not self._loop_ready.is_set():
|
|
128
|
+
# raise RuntimeError("Event loop failed to start within timeout")
|
|
129
|
+
|
|
130
|
+
# def _run_async(self, coro):
|
|
131
|
+
# """
|
|
132
|
+
# Run async coroutine using persistent event loop.
|
|
133
|
+
# This is the core method that makes everything work.
|
|
134
|
+
# """
|
|
135
|
+
# # Ensure we have an event loop
|
|
136
|
+
# self._ensure_event_loop()
|
|
137
|
+
|
|
138
|
+
# if self._loop.is_closed():
|
|
139
|
+
# raise RuntimeError("Event loop is closed")
|
|
140
|
+
|
|
141
|
+
# # Create a future to get the result back from the event loop thread
|
|
142
|
+
# future = concurrent.futures.Future()
|
|
143
|
+
|
|
144
|
+
# async def run_with_context():
|
|
145
|
+
# """Run the coroutine with proper context management."""
|
|
146
|
+
# try:
|
|
147
|
+
# # Ensure the async client is properly initialized
|
|
148
|
+
# await self._ensure_context()
|
|
149
|
+
|
|
150
|
+
# # Run the actual coroutine
|
|
151
|
+
# result = await coro
|
|
152
|
+
|
|
153
|
+
# # Set the result on the future
|
|
154
|
+
# future.set_result(result)
|
|
155
|
+
# except Exception as e:
|
|
156
|
+
# # Set the exception on the future
|
|
157
|
+
# future.set_exception(e)
|
|
158
|
+
|
|
159
|
+
# # Schedule the coroutine on the persistent event loop
|
|
160
|
+
# self._loop.call_soon_threadsafe(
|
|
161
|
+
# lambda: asyncio.create_task(run_with_context())
|
|
162
|
+
# )
|
|
163
|
+
|
|
164
|
+
# # Wait for the result (with timeout to avoid hanging)
|
|
165
|
+
# try:
|
|
166
|
+
# return future.result(timeout=300) # 5 minute timeout
|
|
167
|
+
# except concurrent.futures.TimeoutError:
|
|
168
|
+
# raise RuntimeError("Async operation timed out after 5 minutes")
|
|
169
|
+
|
|
170
|
+
# async def _ensure_context(self):
|
|
171
|
+
# """Ensure the async client context is entered."""
|
|
172
|
+
# if not self._context_entered and not self._closed:
|
|
173
|
+
# await self._async_client.__aenter__()
|
|
174
|
+
# self._context_entered = True
|
|
175
|
+
|
|
176
|
+
# async def _exit_context(self):
|
|
177
|
+
# """Exit the async client context."""
|
|
178
|
+
# if self._context_entered and not self._closed:
|
|
179
|
+
# await self._async_client.__aexit__(None, None, None)
|
|
180
|
+
# self._context_entered = False
|
|
181
|
+
|
|
182
|
+
# def close(self):
|
|
183
|
+
# """Close the underlying async client session and stop the event loop."""
|
|
184
|
+
# if not self._closed:
|
|
185
|
+
# if self._loop and not self._loop.is_closed():
|
|
186
|
+
# # Schedule cleanup on the event loop
|
|
187
|
+
# future = concurrent.futures.Future()
|
|
188
|
+
|
|
189
|
+
# async def cleanup():
|
|
190
|
+
# """Clean up the async client."""
|
|
191
|
+
# try:
|
|
192
|
+
# await self._exit_context()
|
|
193
|
+
# future.set_result(None)
|
|
194
|
+
# except Exception as e:
|
|
195
|
+
# future.set_exception(e)
|
|
196
|
+
|
|
197
|
+
# # Run cleanup
|
|
198
|
+
# self._loop.call_soon_threadsafe(
|
|
199
|
+
# lambda: asyncio.create_task(cleanup())
|
|
200
|
+
# )
|
|
201
|
+
|
|
202
|
+
# # Wait for cleanup to complete
|
|
203
|
+
# try:
|
|
204
|
+
# future.result(timeout=10)
|
|
205
|
+
# except:
|
|
206
|
+
# pass # Ignore cleanup errors
|
|
207
|
+
|
|
208
|
+
# # Stop the event loop
|
|
209
|
+
# self._loop.call_soon_threadsafe(self._loop.stop)
|
|
210
|
+
|
|
211
|
+
# # Wait for thread to finish
|
|
212
|
+
# if self._thread and self._thread.is_alive():
|
|
213
|
+
# self._thread.join(timeout=5)
|
|
214
|
+
|
|
215
|
+
# self._closed = True
|
|
216
|
+
|
|
217
|
+
# def _cleanup(self):
|
|
218
|
+
# """Internal cleanup method called by atexit."""
|
|
219
|
+
# if not self._closed:
|
|
220
|
+
# try:
|
|
221
|
+
# self.close()
|
|
222
|
+
# except Exception:
|
|
223
|
+
# pass # Ignore cleanup errors
|
|
224
|
+
|
|
225
|
+
# def __dir__(self):
|
|
226
|
+
# """Return list of attributes for autocomplete in interactive environments."""
|
|
227
|
+
# default_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
|
|
228
|
+
# async_client_attrs = [attr for attr in dir(self._async_client) if not attr.startswith('_')]
|
|
229
|
+
# endpoint_attrs = ['datasets', 'users', 'mass_stats', 'groups', 'space', 'model', 'auth']
|
|
230
|
+
# all_attrs = default_attrs + async_client_attrs + endpoint_attrs
|
|
231
|
+
# return list(set(all_attrs))
|
|
232
|
+
|
|
233
|
+
# # Your existing methods (geoquery, zonal_stats, etc.)
|
|
234
|
+
# def geoquery(
|
|
235
|
+
# self,
|
|
236
|
+
# expr: str,
|
|
237
|
+
# feature: Union[Dict[str, Any], ShapelyGeometry],
|
|
238
|
+
# in_crs: str = "epsg:4326",
|
|
239
|
+
# out_crs: str = "epsg:4326",
|
|
240
|
+
# resolution: int = -1,
|
|
241
|
+
# geom_fix: bool = False,
|
|
242
|
+
# **kwargs
|
|
243
|
+
# ):
|
|
244
|
+
# """Compute WCS query for a single geometry (synchronous version)."""
|
|
245
|
+
# coro = self._async_client.geoquery(
|
|
246
|
+
# expr=expr,
|
|
247
|
+
# feature=feature,
|
|
248
|
+
# in_crs=in_crs,
|
|
249
|
+
# out_crs=out_crs,
|
|
250
|
+
# output="netcdf",
|
|
251
|
+
# resolution=resolution,
|
|
252
|
+
# geom_fix=geom_fix,
|
|
253
|
+
# **kwargs
|
|
254
|
+
# )
|
|
255
|
+
# return self._run_async(coro)
|
|
256
|
+
|
|
257
|
+
# def zonal_stats(
|
|
258
|
+
# self,
|
|
259
|
+
# gdf: GeoDataFrame,
|
|
260
|
+
# expr: str,
|
|
261
|
+
# conc: int = 20,
|
|
262
|
+
# inplace: bool = False,
|
|
263
|
+
# in_crs: str = "epsg:4326",
|
|
264
|
+
# out_crs: str = "epsg:4326",
|
|
265
|
+
# resolution: int = -1,
|
|
266
|
+
# geom_fix: bool = False,
|
|
267
|
+
# drop_nan: bool = False,
|
|
268
|
+
# spatial_reduction: str = None,
|
|
269
|
+
# temporal_reduction: str = None,
|
|
270
|
+
# max_memory_mb: int = 500,
|
|
271
|
+
# stream_to_disk: bool = False,
|
|
272
|
+
# ):
|
|
273
|
+
# """Compute zonal statistics for all geometries in a GeoDataFrame (synchronous version)."""
|
|
274
|
+
# coro = self._async_client.zonal_stats(
|
|
275
|
+
# gdf=gdf,
|
|
276
|
+
# expr=expr,
|
|
277
|
+
# conc=conc,
|
|
278
|
+
# inplace=inplace,
|
|
279
|
+
# in_crs=in_crs,
|
|
280
|
+
# out_crs=out_crs,
|
|
281
|
+
# resolution=resolution,
|
|
282
|
+
# geom_fix=geom_fix,
|
|
283
|
+
# drop_nan=drop_nan,
|
|
284
|
+
# spatial_reduction=spatial_reduction,
|
|
285
|
+
# temporal_reduction=temporal_reduction,
|
|
286
|
+
# max_memory_mb=max_memory_mb,
|
|
287
|
+
# stream_to_disk=stream_to_disk
|
|
288
|
+
# )
|
|
289
|
+
# return self._run_async(coro)
|
|
290
|
+
|
|
291
|
+
# def create_dataset_file(
|
|
292
|
+
# self,
|
|
293
|
+
# aoi: str,
|
|
294
|
+
# expression: str,
|
|
295
|
+
# output: str,
|
|
296
|
+
# in_crs: str = "epsg:4326",
|
|
297
|
+
# res: float = 0.0001,
|
|
298
|
+
# region: str = "aus",
|
|
299
|
+
# to_crs: str = "epsg:4326",
|
|
300
|
+
# overwrite: bool = True,
|
|
301
|
+
# skip_existing: bool = False,
|
|
302
|
+
# non_interactive: bool = True,
|
|
303
|
+
# poll_interval: int = 30,
|
|
304
|
+
# download_path: str = "/home/user/Downloads",
|
|
305
|
+
# ) -> dict:
|
|
306
|
+
# """Create a dataset file using mass stats operations (synchronous version)."""
|
|
307
|
+
# coro = self._async_client.create_dataset_file(
|
|
308
|
+
# aoi=aoi,
|
|
309
|
+
# expression=expression,
|
|
310
|
+
# output=output,
|
|
311
|
+
# in_crs=in_crs,
|
|
312
|
+
# res=res,
|
|
313
|
+
# region=region,
|
|
314
|
+
# to_crs=to_crs,
|
|
315
|
+
# overwrite=overwrite,
|
|
316
|
+
# skip_existing=skip_existing,
|
|
317
|
+
# non_interactive=non_interactive,
|
|
318
|
+
# poll_interval=poll_interval,
|
|
319
|
+
# download_path=download_path,
|
|
320
|
+
# )
|
|
321
|
+
# return self._run_async(coro)
|
|
322
|
+
|
|
323
|
+
# # Context manager support
|
|
324
|
+
# def __enter__(self):
|
|
325
|
+
# """Context manager entry."""
|
|
326
|
+
# return self
|
|
327
|
+
|
|
328
|
+
# def __exit__(self, exc_type, exc_val, exc_tb):
|
|
329
|
+
# """Context manager exit."""
|
|
330
|
+
# self.close()
|
|
331
|
+
|
|
332
|
+
# def __del__(self):
|
|
333
|
+
# """Destructor to ensure session is closed."""
|
|
334
|
+
# if not self._closed:
|
|
335
|
+
# try:
|
|
336
|
+
# self._cleanup()
|
|
337
|
+
# except Exception:
|
|
338
|
+
# pass
|
|
339
|
+
|
|
340
|
+
|
|
1
341
|
import asyncio
|
|
2
342
|
import functools
|
|
3
|
-
import
|
|
343
|
+
import concurrent.futures
|
|
4
344
|
from typing import Optional, Dict, Any, Union
|
|
5
345
|
from geopandas import GeoDataFrame
|
|
6
346
|
from shapely.geometry.base import BaseGeometry as ShapelyGeometry
|
|
@@ -21,13 +361,10 @@ class SyncWrapper:
|
|
|
21
361
|
Return list of attributes for autocomplete in interactive environments.
|
|
22
362
|
This enables autocomplete in Jupyter/iPython after instantiation.
|
|
23
363
|
"""
|
|
24
|
-
# Get all public attributes from the wrapped async object
|
|
25
364
|
async_attrs = [attr for attr in dir(self._async_obj) if not attr.startswith('_')]
|
|
26
365
|
|
|
27
|
-
# Get all attributes from this wrapper instance
|
|
28
366
|
wrapper_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
|
|
29
367
|
|
|
30
|
-
# Combine and return unique attributes
|
|
31
368
|
return list(set(async_attrs + wrapper_attrs))
|
|
32
369
|
|
|
33
370
|
def __getattr__(self, name):
|
|
@@ -59,7 +396,6 @@ class SyncClient:
|
|
|
59
396
|
self._context_entered = False
|
|
60
397
|
self._closed = False
|
|
61
398
|
|
|
62
|
-
# Initialize endpoint managers
|
|
63
399
|
self.datasets = SyncWrapper(self._async_client.datasets, self)
|
|
64
400
|
self.users = SyncWrapper(self._async_client.users, self)
|
|
65
401
|
self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
|
|
@@ -68,7 +404,6 @@ class SyncClient:
|
|
|
68
404
|
self.model = SyncWrapper(self._async_client.model, self)
|
|
69
405
|
self.auth = SyncWrapper(self._async_client.auth, self)
|
|
70
406
|
|
|
71
|
-
# Register cleanup
|
|
72
407
|
import atexit
|
|
73
408
|
atexit.register(self._cleanup)
|
|
74
409
|
|
|
@@ -77,16 +412,12 @@ class SyncClient:
|
|
|
77
412
|
Return list of attributes for autocomplete in interactive environments.
|
|
78
413
|
This includes all methods from the async client plus the endpoint managers.
|
|
79
414
|
"""
|
|
80
|
-
# Get default attributes from this class
|
|
81
415
|
default_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
|
|
82
416
|
|
|
83
|
-
# Get all public methods from the async client
|
|
84
417
|
async_client_attrs = [attr for attr in dir(self._async_client) if not attr.startswith('_')]
|
|
85
418
|
|
|
86
|
-
# Add endpoint managers
|
|
87
419
|
endpoint_attrs = ['datasets', 'users', 'mass_stats', 'groups', 'space', 'model', 'auth']
|
|
88
420
|
|
|
89
|
-
# Combine all attributes
|
|
90
421
|
all_attrs = default_attrs + async_client_attrs + endpoint_attrs
|
|
91
422
|
|
|
92
423
|
return list(set(all_attrs))
|
|
@@ -119,16 +450,10 @@ class SyncClient:
|
|
|
119
450
|
gdf: GeoDataFrame,
|
|
120
451
|
expr: str,
|
|
121
452
|
conc: int = 20,
|
|
122
|
-
inplace: bool = False,
|
|
123
453
|
in_crs: str = "epsg:4326",
|
|
124
454
|
out_crs: str = "epsg:4326",
|
|
125
455
|
resolution: int = -1,
|
|
126
456
|
geom_fix: bool = False,
|
|
127
|
-
drop_nan: bool = False,
|
|
128
|
-
spatial_reduction: str = None,
|
|
129
|
-
temporal_reduction: str = None,
|
|
130
|
-
max_memory_mb: int = 500,
|
|
131
|
-
stream_to_disk: bool = False,
|
|
132
457
|
):
|
|
133
458
|
"""
|
|
134
459
|
Compute zonal statistics for all geometries in a GeoDataFrame (synchronous version).
|
|
@@ -137,22 +462,13 @@ class SyncClient:
|
|
|
137
462
|
gdf (GeoDataFrame): GeoDataFrame containing geometries
|
|
138
463
|
expr (str): Terrakio expression to evaluate, can include spatial aggregations
|
|
139
464
|
conc (int): Number of concurrent requests to make
|
|
140
|
-
inplace (bool): Whether to modify the input GeoDataFrame in place
|
|
141
465
|
in_crs (str): Input coordinate reference system
|
|
142
466
|
out_crs (str): Output coordinate reference system
|
|
143
467
|
resolution (int): Resolution parameter
|
|
144
468
|
geom_fix (bool): Whether to fix the geometry (default False)
|
|
145
|
-
drop_nan (bool): Whether to drop NaN values from the results (default False)
|
|
146
|
-
spatial_reduction (str): Reduction operation for spatial dimensions (x, y).
|
|
147
|
-
Options: 'mean', 'median', 'min', 'max', 'std', 'var', 'sum', 'count'
|
|
148
|
-
temporal_reduction (str): Reduction operation for temporal dimension (time).
|
|
149
|
-
Options: 'mean', 'median', 'min', 'max', 'std', 'var', 'sum', 'count'
|
|
150
|
-
max_memory_mb (int): Maximum memory threshold in MB (default 500MB)
|
|
151
|
-
stream_to_disk (bool): Whether to stream datasets to disk as NetCDF files (default False)
|
|
152
469
|
|
|
153
470
|
Returns:
|
|
154
|
-
geopandas.GeoDataFrame: GeoDataFrame with added columns for results
|
|
155
|
-
If stream_to_disk=True, large datasets are saved as NetCDF files with file paths stored.
|
|
471
|
+
geopandas.GeoDataFrame: GeoDataFrame with added columns for results
|
|
156
472
|
|
|
157
473
|
Raises:
|
|
158
474
|
ValueError: If concurrency is too high or if data exceeds memory limit without streaming
|
|
@@ -162,16 +478,10 @@ class SyncClient:
|
|
|
162
478
|
gdf=gdf,
|
|
163
479
|
expr=expr,
|
|
164
480
|
conc=conc,
|
|
165
|
-
inplace=inplace,
|
|
166
481
|
in_crs=in_crs,
|
|
167
482
|
out_crs=out_crs,
|
|
168
483
|
resolution=resolution,
|
|
169
484
|
geom_fix=geom_fix,
|
|
170
|
-
drop_nan=drop_nan,
|
|
171
|
-
spatial_reduction=spatial_reduction,
|
|
172
|
-
temporal_reduction=temporal_reduction,
|
|
173
|
-
max_memory_mb=max_memory_mb,
|
|
174
|
-
stream_to_disk=stream_to_disk
|
|
175
485
|
)
|
|
176
486
|
return self._run_async(coro)
|
|
177
487
|
|
|
@@ -207,7 +517,49 @@ class SyncClient:
|
|
|
207
517
|
)
|
|
208
518
|
return self._run_async(coro)
|
|
209
519
|
|
|
210
|
-
|
|
520
|
+
def geo_queries(
|
|
521
|
+
self,
|
|
522
|
+
queries: list[dict],
|
|
523
|
+
conc: int = 20,
|
|
524
|
+
):
|
|
525
|
+
"""
|
|
526
|
+
Execute multiple geo queries concurrently (synchronous version).
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
queries (list[dict]): List of dictionaries containing query parameters.
|
|
530
|
+
Each query must have 'expr', 'feature', and 'in_crs' keys.
|
|
531
|
+
conc (int): Number of concurrent requests to make (default 20, max 100)
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
Union[float, geopandas.GeoDataFrame]:
|
|
535
|
+
- float: Average of all results if results are integers
|
|
536
|
+
- GeoDataFrame: GeoDataFrame with geometry and dataset columns if results are xarray datasets
|
|
537
|
+
|
|
538
|
+
Raises:
|
|
539
|
+
ValueError: If queries list is empty, concurrency is too high, or queries are malformed
|
|
540
|
+
APIError: If the API request fails
|
|
541
|
+
|
|
542
|
+
Example:
|
|
543
|
+
queries = [
|
|
544
|
+
{
|
|
545
|
+
'expr': 'WCF.wcf',
|
|
546
|
+
'feature': {'type': 'Feature', 'geometry': {...}, 'properties': {}},
|
|
547
|
+
'in_crs': 'epsg:4326'
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
'expr': 'NDVI.ndvi',
|
|
551
|
+
'feature': {'type': 'Feature', 'geometry': {...}, 'properties': {}},
|
|
552
|
+
'in_crs': 'epsg:4326'
|
|
553
|
+
}
|
|
554
|
+
]
|
|
555
|
+
result = client.geo_queries(queries)
|
|
556
|
+
"""
|
|
557
|
+
coro = self._async_client.geo_queries(
|
|
558
|
+
queries=queries,
|
|
559
|
+
conc=conc,
|
|
560
|
+
)
|
|
561
|
+
return self._run_async(coro)
|
|
562
|
+
|
|
211
563
|
async def _ensure_context(self):
|
|
212
564
|
"""Ensure the async client context is entered."""
|
|
213
565
|
if not self._context_entered and not self._closed:
|
|
@@ -221,18 +573,36 @@ class SyncClient:
|
|
|
221
573
|
self._context_entered = False
|
|
222
574
|
|
|
223
575
|
def _run_async(self, coro):
|
|
224
|
-
"""
|
|
576
|
+
"""
|
|
577
|
+
Run an async coroutine and return the result synchronously.
|
|
578
|
+
This version handles both Jupyter notebook environments and regular Python environments.
|
|
579
|
+
"""
|
|
225
580
|
async def run_with_context():
|
|
226
581
|
await self._ensure_context()
|
|
227
582
|
return await coro
|
|
228
583
|
|
|
229
584
|
try:
|
|
585
|
+
# Check if we're in a running event loop (like Jupyter)
|
|
230
586
|
loop = asyncio.get_running_loop()
|
|
231
|
-
|
|
587
|
+
|
|
588
|
+
# Method 1: Try using nest_asyncio if available
|
|
589
|
+
try:
|
|
590
|
+
import nest_asyncio
|
|
591
|
+
nest_asyncio.apply()
|
|
592
|
+
return asyncio.run(run_with_context())
|
|
593
|
+
except ImportError:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
# Method 2: Use ThreadPoolExecutor to run in a separate thread
|
|
597
|
+
def run_in_thread():
|
|
598
|
+
return asyncio.run(run_with_context())
|
|
599
|
+
|
|
232
600
|
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
233
|
-
future = executor.submit(
|
|
601
|
+
future = executor.submit(run_in_thread)
|
|
234
602
|
return future.result()
|
|
603
|
+
|
|
235
604
|
except RuntimeError:
|
|
605
|
+
# No running loop, safe to use asyncio.run()
|
|
236
606
|
return asyncio.run(run_with_context())
|
|
237
607
|
|
|
238
608
|
def close(self):
|
|
@@ -243,10 +613,21 @@ class SyncClient:
|
|
|
243
613
|
|
|
244
614
|
try:
|
|
245
615
|
loop = asyncio.get_running_loop()
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
616
|
+
|
|
617
|
+
# Try nest_asyncio first
|
|
618
|
+
try:
|
|
619
|
+
import nest_asyncio
|
|
620
|
+
nest_asyncio.apply()
|
|
621
|
+
asyncio.run(close_async())
|
|
622
|
+
except ImportError:
|
|
623
|
+
# Fall back to ThreadPoolExecutor
|
|
624
|
+
def run_in_thread():
|
|
625
|
+
return asyncio.run(close_async())
|
|
626
|
+
|
|
627
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
628
|
+
future = executor.submit(run_in_thread)
|
|
629
|
+
future.result()
|
|
630
|
+
|
|
250
631
|
except RuntimeError:
|
|
251
632
|
asyncio.run(close_async())
|
|
252
633
|
|
|
@@ -267,10 +648,21 @@ class SyncClient:
|
|
|
267
648
|
|
|
268
649
|
try:
|
|
269
650
|
loop = asyncio.get_running_loop()
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
651
|
+
|
|
652
|
+
# Try nest_asyncio first
|
|
653
|
+
try:
|
|
654
|
+
import nest_asyncio
|
|
655
|
+
nest_asyncio.apply()
|
|
656
|
+
asyncio.run(enter_async())
|
|
657
|
+
except ImportError:
|
|
658
|
+
# Fall back to ThreadPoolExecutor
|
|
659
|
+
def run_in_thread():
|
|
660
|
+
return asyncio.run(enter_async())
|
|
661
|
+
|
|
662
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
663
|
+
future = executor.submit(run_in_thread)
|
|
664
|
+
future.result()
|
|
665
|
+
|
|
274
666
|
except RuntimeError:
|
|
275
667
|
asyncio.run(enter_async())
|
|
276
668
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: terrakio-core
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Core components for Terrakio API clients
|
|
5
5
|
Author-email: Yupeng Chao <yupeng@haizea.com.au>
|
|
6
6
|
Project-URL: Homepage, https://github.com/HaizeaAnalytics/terrakio-python-api
|
|
@@ -24,6 +24,7 @@ Requires-Dist: geopandas>=0.13.0
|
|
|
24
24
|
Requires-Dist: google-cloud-storage>=2.0.0
|
|
25
25
|
Requires-Dist: scipy>=1.7.0
|
|
26
26
|
Requires-Dist: nest_asyncio
|
|
27
|
+
Requires-Dist: onnxruntime>=1.10.0
|
|
27
28
|
Provides-Extra: ml
|
|
28
29
|
Requires-Dist: torch>=2.7.1; extra == "ml"
|
|
29
30
|
Requires-Dist: scikit-learn>=1.7.0; extra == "ml"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
terrakio_core/__init__.py,sha256=snOTpM6-GxHvvDJ6hjlsWywgIago1JbE8QDfsWMRlPI,273
|
|
2
|
+
terrakio_core/accessors.py,sha256=qWLljU83YO7EUOefo_f6_P6ba6uiYMXwou0ihAHKBHQ,23706
|
|
3
|
+
terrakio_core/async_client.py,sha256=M7C6by-GCW9AvDRhIwpBG12fO5--NG5qrHdSC6Fjkrc,14082
|
|
4
|
+
terrakio_core/client.py,sha256=VXP7BtJWIfpPPZR7_yNdSTcGwNgTwhb7KorusqkQrzk,5603
|
|
5
|
+
terrakio_core/config.py,sha256=r8NARVYOca4AuM88VP_j-8wQxOk1s7VcRdyEdseBlLE,4193
|
|
6
|
+
terrakio_core/exceptions.py,sha256=4qnpOM1gOxsNIXDXY4qwY1d3I4Myhp7HBh7b2D0SVrU,529
|
|
7
|
+
terrakio_core/sync_client.py,sha256=wsVKjaIDZW4AC2-81RXseE9mFbXu90T5Bbwrgw37x6k,24864
|
|
8
|
+
terrakio_core/convenience_functions/convenience_functions.py,sha256=B7qbObjP4OuAUtrVf4Gi58c0q2EkTwOCOE568P0Q-EE,18607
|
|
9
|
+
terrakio_core/endpoints/auth.py,sha256=FdLsPScPIBo-Gxl6ZnE-46cp2molggAJtL72LssN3fg,6049
|
|
10
|
+
terrakio_core/endpoints/dataset_management.py,sha256=BUm8IIlW_Q45vDiQp16CiJGeSLheI8uWRVRQtMdhaNk,13161
|
|
11
|
+
terrakio_core/endpoints/group_management.py,sha256=VFl3jakjQa9OPi351D3DZvLU9M7fHdfjCzGhmyJsx3U,6309
|
|
12
|
+
terrakio_core/endpoints/mass_stats.py,sha256=6PSWWCpKLKMcFOoXfOAuROX8iSuG_dLyTYcw7gzhhZ4,23464
|
|
13
|
+
terrakio_core/endpoints/model_management.py,sha256=LH_gHPrqYA-_45KWpDBRcFbwHgm-Kg0zk1ealy7P_C0,52379
|
|
14
|
+
terrakio_core/endpoints/space_management.py,sha256=YWb55nkJnFJGlALJ520DvurxDqVqwYtsvqQPWzxzhDs,2266
|
|
15
|
+
terrakio_core/endpoints/user_management.py,sha256=WlFr3EfK8iI6DfkpMuYLHZUPk2n7_DHHO6z1hndmZB4,3816
|
|
16
|
+
terrakio_core/helper/bounded_taskgroup.py,sha256=wiTH10jhKZgrsgrFUNG6gig8bFkUEPHkGRT2XY7Rgmo,677
|
|
17
|
+
terrakio_core/helper/decorators.py,sha256=L6om7wmWNgCei3Wy5U0aZ-70OzsCwclkjIf7SfQuhCg,2289
|
|
18
|
+
terrakio_core/helper/tiles.py,sha256=xNtp3oDD912PN_FQV5fb6uQYhwfHANuXyIcxoVCCfZU,2632
|
|
19
|
+
terrakio_core-0.4.5.dist-info/METADATA,sha256=5_6bPROpM38AJ7NJDqFzORJFG2HJR-j_X0Rg4KwriUY,1791
|
|
20
|
+
terrakio_core-0.4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
21
|
+
terrakio_core-0.4.5.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
|
|
22
|
+
terrakio_core-0.4.5.dist-info/RECORD,,
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
terrakio_core/__init__.py,sha256=GDUYT7cudfxltFZZ-2_5xM4Pd7o-pkQx1O8K9HrE5f0,248
|
|
2
|
-
terrakio_core/async_client.py,sha256=ffLKMbzclHfyBZBJn4uR8nlMW5E-96PX-4O5EghaSPE,13805
|
|
3
|
-
terrakio_core/client.py,sha256=-tGffOKGMyuowsvBwaV7Wtc_EZSWuSwv26_I5FkUank,5446
|
|
4
|
-
terrakio_core/config.py,sha256=r8NARVYOca4AuM88VP_j-8wQxOk1s7VcRdyEdseBlLE,4193
|
|
5
|
-
terrakio_core/exceptions.py,sha256=4qnpOM1gOxsNIXDXY4qwY1d3I4Myhp7HBh7b2D0SVrU,529
|
|
6
|
-
terrakio_core/sync_client.py,sha256=UpPn9rHp6x6otxj3QJ1Scnac4stgIpTtb__gmXszYCA,10787
|
|
7
|
-
terrakio_core/convenience_functions/convenience_functions.py,sha256=sBY2g7Vv3jakkuXnuFomXBWP0y6Q7q1K4ay3g4TxIoQ,21068
|
|
8
|
-
terrakio_core/endpoints/auth.py,sha256=e_hdNE6JOGhRVlQMFdEoOmoMHp5EzK6CclOEnc_AmZw,5863
|
|
9
|
-
terrakio_core/endpoints/dataset_management.py,sha256=BUm8IIlW_Q45vDiQp16CiJGeSLheI8uWRVRQtMdhaNk,13161
|
|
10
|
-
terrakio_core/endpoints/group_management.py,sha256=VFl3jakjQa9OPi351D3DZvLU9M7fHdfjCzGhmyJsx3U,6309
|
|
11
|
-
terrakio_core/endpoints/mass_stats.py,sha256=IZEozQ9GyOmUhd7V8M66Bz2OWsyq-VKzOw_sj_i-dng,23154
|
|
12
|
-
terrakio_core/endpoints/model_management.py,sha256=PF-2f6mW_RtkPWSL-N56kbgLeB6Z4EhU2N2qFqPan7o,70365
|
|
13
|
-
terrakio_core/endpoints/space_management.py,sha256=YWb55nkJnFJGlALJ520DvurxDqVqwYtsvqQPWzxzhDs,2266
|
|
14
|
-
terrakio_core/endpoints/user_management.py,sha256=WlFr3EfK8iI6DfkpMuYLHZUPk2n7_DHHO6z1hndmZB4,3816
|
|
15
|
-
terrakio_core/helper/bounded_taskgroup.py,sha256=wiTH10jhKZgrsgrFUNG6gig8bFkUEPHkGRT2XY7Rgmo,677
|
|
16
|
-
terrakio_core/helper/decorators.py,sha256=L6om7wmWNgCei3Wy5U0aZ-70OzsCwclkjIf7SfQuhCg,2289
|
|
17
|
-
terrakio_core/helper/tiles.py,sha256=xNtp3oDD912PN_FQV5fb6uQYhwfHANuXyIcxoVCCfZU,2632
|
|
18
|
-
terrakio_core-0.4.3.dist-info/METADATA,sha256=201kTjM26SSTmtRPBohs9GlBsP_3U3tUw6ja2X0D6uM,1756
|
|
19
|
-
terrakio_core-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
-
terrakio_core-0.4.3.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
|
|
21
|
-
terrakio_core-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|