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

@@ -61,11 +61,11 @@ class UserManagement:
61
61
  def edit_user(
62
62
  self,
63
63
  uid: str,
64
- email: Optional[str],
65
- role: Optional[str],
66
- apiKey: Optional[str],
67
- groups: Optional[List[str]],
68
- quota: Optional[int]
64
+ email: Optional[str] = None,
65
+ role: Optional[str] = None,
66
+ apiKey: Optional[str] = None,
67
+ groups: Optional[List[str]] = None,
68
+ quota: Optional[int] = None
69
69
  ) -> Dict[str, Any]:
70
70
  """
71
71
  Edit user info. Only provided fields will be updated.
@@ -1,164 +1,95 @@
1
1
  import asyncio
2
2
  import functools
3
+ import inspect
3
4
  from typing import Optional, Dict, Any, Union
4
5
  from geopandas import GeoDataFrame
5
6
  from shapely.geometry.base import BaseGeometry as ShapelyGeometry
6
7
  from .async_client import AsyncClient
7
- from typing import Dict
8
-
9
-
10
- def sync_wrapper(async_func):
11
- """
12
- Decorator to convert async functions to sync functions.
13
- """
14
- @functools.wraps(async_func)
15
- def wrapper(*args, **kwargs):
16
- # Get or create event loop
17
- try:
18
- loop = asyncio.get_running_loop()
19
- # If we're in an async context, we need to run in a new thread
20
- import concurrent.futures
21
- with concurrent.futures.ThreadPoolExecutor() as executor:
22
- future = executor.submit(asyncio.run, async_func(*args, **kwargs))
23
- return future.result()
24
- except RuntimeError:
25
- # No running loop, we can create a new one
26
- return asyncio.run(async_func(*args, **kwargs))
27
- return wrapper
28
8
 
29
9
 
30
10
  class SyncWrapper:
31
11
  """
32
- Generic synchronous wrapper for any async object.
33
- Automatically converts all async methods to sync using __getattr__.
12
+ Generic synchronous wrapper with __dir__ support for runtime autocomplete.
34
13
  """
35
14
 
36
15
  def __init__(self, async_obj, sync_client):
37
16
  self._async_obj = async_obj
38
17
  self._sync_client = sync_client
39
18
 
19
+ def __dir__(self):
20
+ """
21
+ Return list of attributes for autocomplete in interactive environments.
22
+ This enables autocomplete in Jupyter/iPython after instantiation.
23
+ """
24
+ # Get all public attributes from the wrapped async object
25
+ async_attrs = [attr for attr in dir(self._async_obj) if not attr.startswith('_')]
26
+
27
+ # Get all attributes from this wrapper instance
28
+ wrapper_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
29
+
30
+ # Combine and return unique attributes
31
+ return list(set(async_attrs + wrapper_attrs))
32
+
40
33
  def __getattr__(self, name):
41
34
  """
42
35
  Dynamically wrap any method call to convert async to sync.
43
36
  """
44
37
  attr = getattr(self._async_obj, name)
45
38
 
46
- # If it's a callable (method), wrap it
47
39
  if callable(attr):
40
+ @functools.wraps(attr)
48
41
  def sync_wrapper(*args, **kwargs):
49
42
  result = attr(*args, **kwargs)
50
- # If the result is a coroutine, run it synchronously
51
43
  if hasattr(result, '__await__'):
52
44
  return self._sync_client._run_async(result)
53
45
  return result
54
46
  return sync_wrapper
55
47
 
56
- # If it's not a callable (like a property), return as-is
57
48
  return attr
58
49
 
59
50
 
60
51
  class SyncClient:
61
52
  """
62
- Synchronous wrapper around AsyncClient that converts all async methods to sync.
63
- Uses the AsyncClient as a context manager to properly handle session lifecycle.
53
+ Synchronous wrapper with __dir__ support for runtime autocomplete.
54
+ Works best in interactive environments like Jupyter/iPython.
64
55
  """
65
56
 
66
57
  def __init__(self, url: Optional[str] = None, api_key: Optional[str] = None, verbose: bool = False):
67
- """
68
- Initialize the synchronous client.
69
-
70
- Args:
71
- url (Optional[str]): The API base URL
72
- api_key (Optional[str]): The API key for authentication
73
- verbose (bool): Whether to enable verbose logging
74
- """
75
58
  self._async_client = AsyncClient(url=url, api_key=api_key, verbose=verbose)
76
59
  self._context_entered = False
77
60
  self._closed = False
78
61
 
79
62
  # Initialize endpoint managers
80
- self._init_endpoints()
63
+ self.datasets = SyncWrapper(self._async_client.datasets, self)
64
+ self.users = SyncWrapper(self._async_client.users, self)
65
+ self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
66
+ self.groups = SyncWrapper(self._async_client.groups, self)
67
+ self.space = SyncWrapper(self._async_client.space, self)
68
+ self.model = SyncWrapper(self._async_client.model, self)
69
+ self.auth = SyncWrapper(self._async_client.auth, self)
81
70
 
82
- # Register cleanup on exit
71
+ # Register cleanup
83
72
  import atexit
84
73
  atexit.register(self._cleanup)
85
-
86
- def __getattr__(self, name):
87
- """
88
- Delegate attribute access to the underlying async client.
89
- """
90
- if hasattr(self._async_client, name):
91
- attr = getattr(self._async_client, name)
92
-
93
- # If it's a callable (method), wrap it to run synchronously
94
- if callable(attr):
95
- def sync_method(*args, **kwargs):
96
- result = attr(*args, **kwargs)
97
- # If the result is a coroutine, run it synchronously
98
- if hasattr(result, '__await__'):
99
- return self._run_async(result)
100
- return result
101
- return sync_method
102
-
103
- # If it's not a callable (like a property), return as-is
104
- return attr
105
-
106
- # If the attribute doesn't exist, raise AttributeError
107
- raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")
108
-
109
- async def _ensure_context(self):
110
- """Ensure the async client context is entered."""
111
- if not self._context_entered and not self._closed:
112
- await self._async_client.__aenter__()
113
- self._context_entered = True
114
-
115
- async def _exit_context(self):
116
- """Exit the async client context."""
117
- if self._context_entered and not self._closed:
118
- await self._async_client.__aexit__(None, None, None)
119
- self._context_entered = False
120
74
 
121
- def _run_async(self, coro):
75
+ def __dir__(self):
122
76
  """
123
- Run an async coroutine and return the result synchronously.
124
- Ensures the AsyncClient context is properly managed.
77
+ Return list of attributes for autocomplete in interactive environments.
78
+ This includes all methods from the async client plus the endpoint managers.
125
79
  """
126
- async def run_with_context():
127
- await self._ensure_context()
128
- return await coro
80
+ # Get default attributes from this class
81
+ default_attrs = [attr for attr in object.__dir__(self) if not attr.startswith('_')]
129
82
 
130
- try:
131
- loop = asyncio.get_running_loop()
132
- # If we're already in an async context, run in a new thread
133
- import concurrent.futures
134
-
135
- with concurrent.futures.ThreadPoolExecutor() as executor:
136
- future = executor.submit(asyncio.run, run_with_context())
137
- return future.result()
138
- except RuntimeError:
139
- # No running loop, create a new one
140
- return asyncio.run(run_with_context())
141
-
142
- def _run_async_single_use(self, coro):
143
- """
144
- Run an async coroutine with its own context manager for one-off operations.
145
- This is useful when you don't want to maintain a persistent session.
146
- """
147
- async def run_with_own_context():
148
- async with self._async_client:
149
- return await coro
83
+ # Get all public methods from the async client
84
+ async_client_attrs = [attr for attr in dir(self._async_client) if not attr.startswith('_')]
150
85
 
151
- try:
152
- loop = asyncio.get_running_loop()
153
- # If we're already in an async context, run in a new thread
154
- import concurrent.futures
155
-
156
- with concurrent.futures.ThreadPoolExecutor() as executor:
157
- future = executor.submit(asyncio.run, run_with_own_context())
158
- return future.result()
159
- except RuntimeError:
160
- # No running loop, create a new one
161
- return asyncio.run(run_with_own_context())
86
+ # Add endpoint managers
87
+ endpoint_attrs = ['datasets', 'users', 'mass_stats', 'groups', 'space', 'model', 'auth']
88
+
89
+ # Combine all attributes
90
+ all_attrs = default_attrs + async_client_attrs + endpoint_attrs
91
+
92
+ return list(set(all_attrs))
162
93
 
163
94
  def geoquery(
164
95
  self,
@@ -166,56 +97,42 @@ class SyncClient:
166
97
  feature: Union[Dict[str, Any], ShapelyGeometry],
167
98
  in_crs: str = "epsg:4326",
168
99
  out_crs: str = "epsg:4326",
169
- output: str = "csv",
170
100
  resolution: int = -1,
171
101
  geom_fix: bool = False,
172
102
  **kwargs
173
103
  ):
174
- """
175
- Compute WCS query for a single geometry (synchronous version).
176
-
177
- Args:
178
- expr (str): The WCS expression to evaluate
179
- feature (Union[Dict[str, Any], ShapelyGeometry]): The geographic feature
180
- in_crs (str): Input coordinate reference system
181
- out_crs (str): Output coordinate reference system
182
- output (str): Output format ('csv' or 'netcdf')
183
- resolution (int): Resolution parameter
184
- geom_fix (bool): Whether to fix the geometry (default False)
185
- **kwargs: Additional parameters to pass to the WCS request
186
-
187
- Returns:
188
- Union[pd.DataFrame, xr.Dataset, bytes]: The response data in the requested format
189
-
190
- Raises:
191
- APIError: If the API request fails
192
- """
104
+ """Compute WCS query for a single geometry (synchronous version)."""
193
105
  coro = self._async_client.geoquery(
194
106
  expr=expr,
195
107
  feature=feature,
196
108
  in_crs=in_crs,
197
109
  out_crs=out_crs,
198
- output=output,
110
+ output="netcdf",
199
111
  resolution=resolution,
200
112
  geom_fix=geom_fix,
201
113
  **kwargs
202
114
  )
203
115
  return self._run_async(coro)
204
-
116
+
205
117
  def zonal_stats(
206
- self,
207
- gdf: GeoDataFrame,
208
- expr: str,
209
- conc: int = 20,
210
- inplace: bool = False,
211
- in_crs: str = "epsg:4326",
212
- out_crs: str = "epsg:4326",
213
- resolution: int = -1,
214
- geom_fix: bool = False,
118
+ self,
119
+ gdf: GeoDataFrame,
120
+ expr: str,
121
+ conc: int = 20,
122
+ inplace: bool = False,
123
+ in_crs: str = "epsg:4326",
124
+ out_crs: str = "epsg:4326",
125
+ resolution: int = -1,
126
+ 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,
215
132
  ):
216
133
  """
217
134
  Compute zonal statistics for all geometries in a GeoDataFrame (synchronous version).
218
-
135
+
219
136
  Args:
220
137
  gdf (GeoDataFrame): GeoDataFrame containing geometries
221
138
  expr (str): Terrakio expression to evaluate, can include spatial aggregations
@@ -225,12 +142,20 @@ class SyncClient:
225
142
  out_crs (str): Output coordinate reference system
226
143
  resolution (int): Resolution parameter
227
144
  geom_fix (bool): Whether to fix the geometry (default False)
228
-
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
+
229
153
  Returns:
230
154
  geopandas.GeoDataFrame: GeoDataFrame with added columns for results, or None if inplace=True
155
+ If stream_to_disk=True, large datasets are saved as NetCDF files with file paths stored.
231
156
 
232
157
  Raises:
233
- ValueError: If concurrency is too high
158
+ ValueError: If concurrency is too high or if data exceeds memory limit without streaming
234
159
  APIError: If the API request fails
235
160
  """
236
161
  coro = self._async_client.zonal_stats(
@@ -241,7 +166,12 @@ class SyncClient:
241
166
  in_crs=in_crs,
242
167
  out_crs=out_crs,
243
168
  resolution=resolution,
244
- geom_fix=geom_fix
169
+ 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
245
175
  )
246
176
  return self._run_async(coro)
247
177
 
@@ -260,30 +190,7 @@ class SyncClient:
260
190
  poll_interval: int = 30,
261
191
  download_path: str = "/home/user/Downloads",
262
192
  ) -> dict:
263
- """
264
- Create a dataset file using mass stats operations (synchronous version).
265
-
266
- Args:
267
- aoi (str): Area of interest
268
- expression (str): Terrakio expression to evaluate
269
- output (str): Output format
270
- in_crs (str): Input coordinate reference system (default "epsg:4326")
271
- res (float): Resolution (default 0.0001)
272
- region (str): Region (default "aus")
273
- to_crs (str): Target coordinate reference system (default "epsg:4326")
274
- overwrite (bool): Whether to overwrite existing files (default True)
275
- skip_existing (bool): Whether to skip existing files (default False)
276
- non_interactive (bool): Whether to run non-interactively (default True)
277
- poll_interval (int): Polling interval in seconds (default 30)
278
- download_path (str): Download path (default "/home/user/Downloads")
279
-
280
- Returns:
281
- dict: Dictionary containing generation_task_id and combine_task_id
282
-
283
- Raises:
284
- ConfigurationError: If mass stats client is not properly configured
285
- RuntimeError: If job fails
286
- """
193
+ """Create a dataset file using mass stats operations (synchronous version)."""
287
194
  coro = self._async_client.create_dataset_file(
288
195
  aoi=aoi,
289
196
  expression=expression,
@@ -299,11 +206,37 @@ class SyncClient:
299
206
  download_path=download_path,
300
207
  )
301
208
  return self._run_async(coro)
209
+
210
+ # Rest of the methods remain the same...
211
+ async def _ensure_context(self):
212
+ """Ensure the async client context is entered."""
213
+ if not self._context_entered and not self._closed:
214
+ await self._async_client.__aenter__()
215
+ self._context_entered = True
216
+
217
+ async def _exit_context(self):
218
+ """Exit the async client context."""
219
+ if self._context_entered and not self._closed:
220
+ await self._async_client.__aexit__(None, None, None)
221
+ self._context_entered = False
222
+
223
+ def _run_async(self, coro):
224
+ """Run an async coroutine and return the result synchronously."""
225
+ async def run_with_context():
226
+ await self._ensure_context()
227
+ return await coro
228
+
229
+ try:
230
+ loop = asyncio.get_running_loop()
231
+ import concurrent.futures
232
+ with concurrent.futures.ThreadPoolExecutor() as executor:
233
+ future = executor.submit(asyncio.run, run_with_context())
234
+ return future.result()
235
+ except RuntimeError:
236
+ return asyncio.run(run_with_context())
302
237
 
303
238
  def close(self):
304
- """
305
- Close the underlying async client session (synchronous version).
306
- """
239
+ """Close the underlying async client session."""
307
240
  if not self._closed:
308
241
  async def close_async():
309
242
  await self._exit_context()
@@ -325,12 +258,10 @@ class SyncClient:
325
258
  try:
326
259
  self.close()
327
260
  except Exception:
328
- # Ignore errors during cleanup
329
261
  pass
330
262
 
331
263
  def __enter__(self):
332
264
  """Context manager entry."""
333
- # Ensure context is entered when used as context manager
334
265
  async def enter_async():
335
266
  await self._ensure_context()
336
267
 
@@ -350,21 +281,9 @@ class SyncClient:
350
281
  self.close()
351
282
 
352
283
  def __del__(self):
353
- """Destructor to ensure session is closed when object is garbage collected."""
284
+ """Destructor to ensure session is closed."""
354
285
  if not self._closed:
355
286
  try:
356
287
  self._cleanup()
357
288
  except Exception:
358
- # If we can't close gracefully, ignore the error during cleanup
359
- pass
360
-
361
- # Initialize endpoint managers (these can be overridden by subclasses)
362
- def _init_endpoints(self):
363
- """Initialize endpoint managers. Can be overridden by subclasses."""
364
- self.datasets = SyncWrapper(self._async_client.datasets, self)
365
- self.users = SyncWrapper(self._async_client.users, self)
366
- self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
367
- self.groups = SyncWrapper(self._async_client.groups, self)
368
- self.space = SyncWrapper(self._async_client.space, self)
369
- self.model = SyncWrapper(self._async_client.model, self)
370
- self.auth = SyncWrapper(self._async_client.auth, self)
289
+ pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: terrakio-core
3
- Version: 0.3.8
3
+ Version: 0.4.0
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
@@ -22,6 +22,7 @@ Requires-Dist: xarray>=2023.1.0
22
22
  Requires-Dist: shapely>=2.0.0
23
23
  Requires-Dist: geopandas>=0.13.0
24
24
  Requires-Dist: google-cloud-storage>=2.0.0
25
+ Requires-Dist: scipy>=1.7.0
25
26
  Requires-Dist: nest_asyncio
26
27
  Provides-Extra: ml
27
28
  Requires-Dist: torch>=2.7.1; extra == "ml"
@@ -1,21 +1,21 @@
1
- terrakio_core/__init__.py,sha256=nUk_Q29ij_1R32AjR8Ygwy0_ID4-zdVkQexvBz7reM4,242
2
- terrakio_core/async_client.py,sha256=wyuIJGMAzfFwfM5BYgCTMuY0YAKNYJqRNB3IF5NpVow,12730
3
- terrakio_core/client.py,sha256=h8GW88g6RlGwNFW6MW48c_3BnaeT9nSd19LI1jCn1GU,1008
1
+ terrakio_core/__init__.py,sha256=-9YH041kcLF7sIqWy3V0rtFc7q14fVXx6JFK0U5Ey6s,248
2
+ terrakio_core/async_client.py,sha256=T0AEyhh5EwZ2_okRiHuXkLgCqovxl-fuQSejH7hXU0I,13801
3
+ terrakio_core/client.py,sha256=-tGffOKGMyuowsvBwaV7Wtc_EZSWuSwv26_I5FkUank,5446
4
4
  terrakio_core/config.py,sha256=r8NARVYOca4AuM88VP_j-8wQxOk1s7VcRdyEdseBlLE,4193
5
5
  terrakio_core/exceptions.py,sha256=4qnpOM1gOxsNIXDXY4qwY1d3I4Myhp7HBh7b2D0SVrU,529
6
- terrakio_core/sync_client.py,sha256=v1mcBtUaKWACqZgw8dTTVPMxUfKfiY0kjtBKzDwtGTU,13634
7
- terrakio_core/convenience_functions/convenience_functions.py,sha256=U7bLGwfBF-FUYc0nv49pAViPsBQ6LgPlV6c6b-zeKo8,10616
6
+ terrakio_core/sync_client.py,sha256=UpPn9rHp6x6otxj3QJ1Scnac4stgIpTtb__gmXszYCA,10787
7
+ terrakio_core/convenience_functions/convenience_functions.py,sha256=sBY2g7Vv3jakkuXnuFomXBWP0y6Q7q1K4ay3g4TxIoQ,21068
8
8
  terrakio_core/endpoints/auth.py,sha256=e_hdNE6JOGhRVlQMFdEoOmoMHp5EzK6CclOEnc_AmZw,5863
9
9
  terrakio_core/endpoints/dataset_management.py,sha256=BUm8IIlW_Q45vDiQp16CiJGeSLheI8uWRVRQtMdhaNk,13161
10
10
  terrakio_core/endpoints/group_management.py,sha256=VFl3jakjQa9OPi351D3DZvLU9M7fHdfjCzGhmyJsx3U,6309
11
- terrakio_core/endpoints/mass_stats.py,sha256=y1w3QLkDD0sKP1tBcFDqgLYLNxX94I-LYbNotaKhLYM,21356
12
- terrakio_core/endpoints/model_management.py,sha256=Q2bqsVfBILu-hZVw1tr5WjOR68qoYF6m326YJXgAOeo,33886
11
+ terrakio_core/endpoints/mass_stats.py,sha256=yhLCYRrdQPiWwJVCIPbzU5NV3xU5m62pxhYY1FucYjI,23130
12
+ terrakio_core/endpoints/model_management.py,sha256=uzyIHCRgyOwaQFConO0Ur6C0bnMdj4VDpyjiMG8R1Mc,42303
13
13
  terrakio_core/endpoints/space_management.py,sha256=YWb55nkJnFJGlALJ520DvurxDqVqwYtsvqQPWzxzhDs,2266
14
- terrakio_core/endpoints/user_management.py,sha256=x0JW6VET7eokngmkhZPukegxoJNR1X09BVehJt2nIdI,3781
14
+ terrakio_core/endpoints/user_management.py,sha256=WlFr3EfK8iI6DfkpMuYLHZUPk2n7_DHHO6z1hndmZB4,3816
15
15
  terrakio_core/helper/bounded_taskgroup.py,sha256=wiTH10jhKZgrsgrFUNG6gig8bFkUEPHkGRT2XY7Rgmo,677
16
16
  terrakio_core/helper/decorators.py,sha256=L6om7wmWNgCei3Wy5U0aZ-70OzsCwclkjIf7SfQuhCg,2289
17
17
  terrakio_core/helper/tiles.py,sha256=xNtp3oDD912PN_FQV5fb6uQYhwfHANuXyIcxoVCCfZU,2632
18
- terrakio_core-0.3.8.dist-info/METADATA,sha256=oZlZhEda5qq8myogGbxvlV0ZJExcEf5kMaYWEVES0BE,1728
19
- terrakio_core-0.3.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- terrakio_core-0.3.8.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
21
- terrakio_core-0.3.8.dist-info/RECORD,,
18
+ terrakio_core-0.4.0.dist-info/METADATA,sha256=ctBxSZybuLE-4Mwh0-FTVZKCCkfXZl-jTRfIqth5bKc,1756
19
+ terrakio_core-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ terrakio_core-0.4.0.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
21
+ terrakio_core-0.4.0.dist-info/RECORD,,