terrakio-core 0.3.9__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,
@@ -170,24 +101,7 @@ class SyncClient:
170
101
  geom_fix: bool = False,
171
102
  **kwargs
172
103
  ):
173
- """
174
- Compute WCS query for a single geometry (synchronous version).
175
-
176
- Args:
177
- expr (str): The WCS expression to evaluate
178
- feature (Union[Dict[str, Any], ShapelyGeometry]): The geographic feature
179
- in_crs (str): Input coordinate reference system
180
- out_crs (str): Output coordinate reference system
181
- resolution (int): Resolution parameter
182
- geom_fix (bool): Whether to fix the geometry (default False)
183
- **kwargs: Additional parameters to pass to the WCS request
184
-
185
- Returns:
186
- Union[pd.DataFrame, xr.Dataset, bytes]: The response data in the requested format
187
-
188
- Raises:
189
- APIError: If the API request fails
190
- """
104
+ """Compute WCS query for a single geometry (synchronous version)."""
191
105
  coro = self._async_client.geoquery(
192
106
  expr=expr,
193
107
  feature=feature,
@@ -199,21 +113,26 @@ class SyncClient:
199
113
  **kwargs
200
114
  )
201
115
  return self._run_async(coro)
202
-
116
+
203
117
  def zonal_stats(
204
- self,
205
- gdf: GeoDataFrame,
206
- expr: str,
207
- conc: int = 20,
208
- inplace: bool = False,
209
- in_crs: str = "epsg:4326",
210
- out_crs: str = "epsg:4326",
211
- resolution: int = -1,
212
- 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,
213
132
  ):
214
133
  """
215
134
  Compute zonal statistics for all geometries in a GeoDataFrame (synchronous version).
216
-
135
+
217
136
  Args:
218
137
  gdf (GeoDataFrame): GeoDataFrame containing geometries
219
138
  expr (str): Terrakio expression to evaluate, can include spatial aggregations
@@ -223,12 +142,20 @@ class SyncClient:
223
142
  out_crs (str): Output coordinate reference system
224
143
  resolution (int): Resolution parameter
225
144
  geom_fix (bool): Whether to fix the geometry (default False)
226
-
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
+
227
153
  Returns:
228
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.
229
156
 
230
157
  Raises:
231
- ValueError: If concurrency is too high
158
+ ValueError: If concurrency is too high or if data exceeds memory limit without streaming
232
159
  APIError: If the API request fails
233
160
  """
234
161
  coro = self._async_client.zonal_stats(
@@ -239,7 +166,12 @@ class SyncClient:
239
166
  in_crs=in_crs,
240
167
  out_crs=out_crs,
241
168
  resolution=resolution,
242
- 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
243
175
  )
244
176
  return self._run_async(coro)
245
177
 
@@ -258,30 +190,7 @@ class SyncClient:
258
190
  poll_interval: int = 30,
259
191
  download_path: str = "/home/user/Downloads",
260
192
  ) -> dict:
261
- """
262
- Create a dataset file using mass stats operations (synchronous version).
263
-
264
- Args:
265
- aoi (str): Area of interest
266
- expression (str): Terrakio expression to evaluate
267
- output (str): Output format
268
- in_crs (str): Input coordinate reference system (default "epsg:4326")
269
- res (float): Resolution (default 0.0001)
270
- region (str): Region (default "aus")
271
- to_crs (str): Target coordinate reference system (default "epsg:4326")
272
- overwrite (bool): Whether to overwrite existing files (default True)
273
- skip_existing (bool): Whether to skip existing files (default False)
274
- non_interactive (bool): Whether to run non-interactively (default True)
275
- poll_interval (int): Polling interval in seconds (default 30)
276
- download_path (str): Download path (default "/home/user/Downloads")
277
-
278
- Returns:
279
- dict: Dictionary containing generation_task_id and combine_task_id
280
-
281
- Raises:
282
- ConfigurationError: If mass stats client is not properly configured
283
- RuntimeError: If job fails
284
- """
193
+ """Create a dataset file using mass stats operations (synchronous version)."""
285
194
  coro = self._async_client.create_dataset_file(
286
195
  aoi=aoi,
287
196
  expression=expression,
@@ -297,11 +206,37 @@ class SyncClient:
297
206
  download_path=download_path,
298
207
  )
299
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())
300
237
 
301
238
  def close(self):
302
- """
303
- Close the underlying async client session (synchronous version).
304
- """
239
+ """Close the underlying async client session."""
305
240
  if not self._closed:
306
241
  async def close_async():
307
242
  await self._exit_context()
@@ -323,12 +258,10 @@ class SyncClient:
323
258
  try:
324
259
  self.close()
325
260
  except Exception:
326
- # Ignore errors during cleanup
327
261
  pass
328
262
 
329
263
  def __enter__(self):
330
264
  """Context manager entry."""
331
- # Ensure context is entered when used as context manager
332
265
  async def enter_async():
333
266
  await self._ensure_context()
334
267
 
@@ -348,21 +281,9 @@ class SyncClient:
348
281
  self.close()
349
282
 
350
283
  def __del__(self):
351
- """Destructor to ensure session is closed when object is garbage collected."""
284
+ """Destructor to ensure session is closed."""
352
285
  if not self._closed:
353
286
  try:
354
287
  self._cleanup()
355
288
  except Exception:
356
- # If we can't close gracefully, ignore the error during cleanup
357
- pass
358
-
359
- # Initialize endpoint managers (these can be overridden by subclasses)
360
- def _init_endpoints(self):
361
- """Initialize endpoint managers. Can be overridden by subclasses."""
362
- self.datasets = SyncWrapper(self._async_client.datasets, self)
363
- self.users = SyncWrapper(self._async_client.users, self)
364
- self.mass_stats = SyncWrapper(self._async_client.mass_stats, self)
365
- self.groups = SyncWrapper(self._async_client.groups, self)
366
- self.space = SyncWrapper(self._async_client.space, self)
367
- self.model = SyncWrapper(self._async_client.model, self)
368
- 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.9
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
@@ -1,21 +1,21 @@
1
- terrakio_core/__init__.py,sha256=k4gCdlDIDUWLAMA94FU5EsVDzxQJGgYGlx8y9QgD48M,248
2
- terrakio_core/async_client.py,sha256=7Ir_icai4AcAvaE-GsKj50xywbMAcSYigBAw-sQ-ZYA,12554
3
- terrakio_core/client.py,sha256=gVEQr2o00s1CU7xUZtKRKdxZ4_Z0ihbdLj7p0nDEBio,1046
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=glMNMFCtYTIJWg-926F0i1v4h75iI-KE5_1AyR8nH10,13547
7
- terrakio_core/convenience_functions/convenience_functions.py,sha256=4C7FR8l4BkaNQU5Q9w_A3fJ6Gf-dk5L3AmSVjKtbkuQ,10246
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.9.dist-info/METADATA,sha256=SSyRx4J6AiK2QDS827SMGTNM3kPg-9KTYJGz3phZ61k,1756
19
- terrakio_core-0.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- terrakio_core-0.3.9.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
21
- terrakio_core-0.3.9.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,,