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

@@ -0,0 +1,1005 @@
1
+ import json
2
+ import asyncio
3
+ from io import BytesIO
4
+ from typing import Dict, Any, Optional, Union
5
+
6
+ import requests
7
+ import aiohttp
8
+ import pandas as pd
9
+ import geopandas as gpd
10
+ import xarray as xr
11
+ from shapely.geometry import shape, mapping
12
+ from shapely.geometry.base import BaseGeometry as ShapelyGeometry
13
+
14
+ from .exceptions import APIError, ConfigurationError
15
+
16
+ class BaseClient:
17
+ def __init__(self, url: Optional[str] = None, key: Optional[str] = None,
18
+ auth_url: Optional[str] = "https://dev-au.terrak.io",
19
+ quiet: bool = False, config_file: Optional[str] = None,
20
+ verify: bool = True, timeout: int = 60):
21
+ self.quiet = quiet
22
+ self.verify = verify
23
+ self.timeout = timeout
24
+ self.auth_client = None
25
+ if auth_url:
26
+ from terrakio_core.auth import AuthClient
27
+ self.auth_client = AuthClient(
28
+ base_url=auth_url,
29
+ verify=verify,
30
+ timeout=timeout
31
+ )
32
+ self.url = url
33
+ self.key = key
34
+ if self.url is None or self.key is None:
35
+ from terrakio_core.config import read_config_file, DEFAULT_CONFIG_FILE
36
+ if config_file is None:
37
+ config_file = DEFAULT_CONFIG_FILE
38
+ try:
39
+ config = read_config_file(config_file)
40
+ if self.url is None:
41
+ self.url = config.get('url')
42
+ if self.key is None:
43
+ self.key = config.get('key')
44
+ except Exception as e:
45
+ raise ConfigurationError(
46
+ f"Failed to read configuration: {e}\n\n"
47
+ "To fix this issue:\n"
48
+ "1. Create a file at ~/.terrakioapirc with:\n"
49
+ "url: https://api.terrak.io\n"
50
+ "key: your-api-key\n\n"
51
+ "OR\n\n"
52
+ "2. Initialize the client with explicit parameters:\n"
53
+ "client = terrakio_api.Client(\n"
54
+ " url='https://api.terrak.io',\n"
55
+ " key='your-api-key'\n"
56
+ ")"
57
+ )
58
+ if not self.url:
59
+ raise ConfigurationError("Missing API URL in configuration")
60
+ if not self.key:
61
+ raise ConfigurationError("Missing API key in configuration")
62
+ self.url = self.url.rstrip('/')
63
+ if not self.quiet:
64
+ print(f"Using Terrakio API at: {self.url}")
65
+ self.session = requests.Session()
66
+ self.session.headers.update({
67
+ 'Content-Type': 'application/json',
68
+ 'x-api-key': self.key
69
+ })
70
+ self.user_management = None
71
+ self.dataset_management = None
72
+ self.mass_stats = None
73
+ self._aiohttp_session = None
74
+
75
+ @property
76
+ async def aiohttp_session(self):
77
+ if self._aiohttp_session is None or self._aiohttp_session.closed:
78
+ self._aiohttp_session = aiohttp.ClientSession(
79
+ headers={
80
+ 'Content-Type': 'application/json',
81
+ 'x-api-key': self.key
82
+ },
83
+ timeout=aiohttp.ClientTimeout(total=self.timeout)
84
+ )
85
+ return self._aiohttp_session
86
+
87
+ async def wcs_async(self, expr: str, feature: Union[Dict[str, Any], ShapelyGeometry],
88
+ in_crs: str = "epsg:4326", out_crs: str = "epsg:4326",
89
+ output: str = "csv", resolution: int = -1, **kwargs):
90
+ """
91
+ Asynchronous version of the wcs() method using aiohttp.
92
+
93
+ Args:
94
+ expr (str): The WCS expression to evaluate
95
+ feature (Union[Dict[str, Any], ShapelyGeometry]): The geographic feature
96
+ in_crs (str): Input coordinate reference system
97
+ out_crs (str): Output coordinate reference system
98
+ output (str): Output format ('csv' or 'netcdf')
99
+ resolution (int): Resolution parameter
100
+ **kwargs: Additional parameters to pass to the WCS request
101
+
102
+ Returns:
103
+ Union[pd.DataFrame, xr.Dataset, bytes]: The response data in the requested format
104
+ """
105
+ if hasattr(feature, 'is_valid'):
106
+ from shapely.geometry import mapping
107
+ feature = {
108
+ "type": "Feature",
109
+ "geometry": mapping(feature),
110
+ "properties": {}
111
+ }
112
+ self.validate_feature(feature)
113
+
114
+ payload = {
115
+ "feature": feature,
116
+ "in_crs": in_crs,
117
+ "out_crs": out_crs,
118
+ "output": output,
119
+ "resolution": resolution,
120
+ "expr": expr,
121
+ **kwargs
122
+ }
123
+
124
+ request_url = f"{self.url}/geoquery"
125
+
126
+ try:
127
+ # Get the shared aiohttp session
128
+ session = await self.aiohttp_session
129
+ async with session.post(request_url, json=payload, ssl=self.verify) as response:
130
+ if not response.ok:
131
+ error_msg = f"API request failed: {response.status} {response.reason}"
132
+ try:
133
+ error_data = await response.json()
134
+ if "detail" in error_data:
135
+ error_msg += f" - {error_data['detail']}"
136
+ except:
137
+ pass
138
+ raise APIError(error_msg)
139
+
140
+ content = await response.read()
141
+
142
+ if output.lower() == "csv":
143
+ import pandas as pd
144
+ df = pd.read_csv(BytesIO(content))
145
+ return df
146
+ elif output.lower() == "netcdf":
147
+ return xr.open_dataset(BytesIO(content))
148
+ else:
149
+ try:
150
+ return xr.open_dataset(BytesIO(content))
151
+ except ValueError:
152
+ import pandas as pd
153
+ try:
154
+ return pd.read_csv(BytesIO(content))
155
+ except:
156
+ return content
157
+
158
+ except aiohttp.ClientError as e:
159
+ raise APIError(f"Request failed: {str(e)}")
160
+ except Exception as e:
161
+ raise
162
+
163
+ async def close_async(self):
164
+ """Close the aiohttp session"""
165
+ if self._aiohttp_session and not self._aiohttp_session.closed:
166
+ await self._aiohttp_session.close()
167
+ self._aiohttp_session = None
168
+
169
+ async def __aenter__(self):
170
+ return self
171
+
172
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
173
+ await self.close_async()
174
+
175
+ def validate_feature(self, feature: Dict[str, Any]) -> None:
176
+ if hasattr(feature, 'is_valid'):
177
+ from shapely.geometry import mapping
178
+ feature = {
179
+ "type": "Feature",
180
+ "geometry": mapping(feature),
181
+ "properties": {}
182
+ }
183
+ if not isinstance(feature, dict):
184
+ raise ValueError("Feature must be a dictionary or a Shapely geometry")
185
+ if feature.get("type") != "Feature":
186
+ raise ValueError("GeoJSON object must be of type 'Feature'")
187
+ if "geometry" not in feature:
188
+ raise ValueError("Feature must contain a 'geometry' field")
189
+ if "properties" not in feature:
190
+ raise ValueError("Feature must contain a 'properties' field")
191
+ try:
192
+ geometry = shape(feature["geometry"])
193
+ except Exception as e:
194
+ raise ValueError(f"Invalid geometry format: {str(e)}")
195
+ if not geometry.is_valid:
196
+ raise ValueError(f"Invalid geometry: {geometry.is_valid_reason}")
197
+ geom_type = feature["geometry"]["type"]
198
+ if geom_type == "Point":
199
+ if len(feature["geometry"]["coordinates"]) != 2:
200
+ raise ValueError("Point must have exactly 2 coordinates")
201
+ elif geom_type == "Polygon":
202
+ if not geometry.is_simple:
203
+ raise ValueError("Polygon must be simple (not self-intersecting)")
204
+ if geometry.area == 0:
205
+ raise ValueError("Polygon must have non-zero area")
206
+ coords = feature["geometry"]["coordinates"][0]
207
+ if coords[0] != coords[-1]:
208
+ raise ValueError("Polygon must be closed (first and last points must match)")
209
+
210
+ def signup(self, email: str, password: str) -> Dict[str, Any]:
211
+ if not self.auth_client:
212
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
213
+ return self.auth_client.signup(email, password)
214
+
215
+ def login(self, email: str, password: str) -> Dict[str, str]:
216
+ if not self.auth_client:
217
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
218
+
219
+ try:
220
+ # First attempt to login
221
+ token_response = self.auth_client.login(email, password)
222
+
223
+ print("the token response is ", token_response)
224
+ # Only proceed with API key retrieval if login was successful
225
+ if token_response:
226
+ # After successful login, get the API key
227
+ api_key_response = self.view_api_key()
228
+ self.key = api_key_response
229
+
230
+ # Save email and API key to config file
231
+ import os
232
+ import json
233
+ config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
234
+ try:
235
+ config = {"EMAIL": email, "TERRAKIO_API_KEY": self.key}
236
+ if os.path.exists(config_path):
237
+ with open(config_path, 'r') as f:
238
+ config = json.load(f)
239
+ config["EMAIL"] = email
240
+ config["TERRAKIO_API_KEY"] = self.key
241
+
242
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
243
+ with open(config_path, 'w') as f:
244
+ json.dump(config, f, indent=4)
245
+
246
+ if not self.quiet:
247
+ print(f"Successfully authenticated as: {email}")
248
+ print(f"API key saved to {config_path}")
249
+ except Exception as e:
250
+ if not self.quiet:
251
+ print(f"Warning: Failed to update config file: {e}")
252
+
253
+ return {"token": token_response} if token_response else {"error": "Login failed"}
254
+ except Exception as e:
255
+ if not self.quiet:
256
+ print(f"Login failed: {str(e)}")
257
+ raise
258
+
259
+ def refresh_api_key(self) -> str:
260
+ if not self.auth_client:
261
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
262
+ if not self.auth_client.token:
263
+ raise ConfigurationError("Not authenticated. Call login() first.")
264
+ self.key = self.auth_client.refresh_api_key()
265
+ self.session.headers.update({'x-api-key': self.key})
266
+ import os
267
+ config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
268
+ try:
269
+ config = {"EMAIL": "", "TERRAKIO_API_KEY": ""}
270
+ if os.path.exists(config_path):
271
+ with open(config_path, 'r') as f:
272
+ config = json.load(f)
273
+ config["TERRAKIO_API_KEY"] = self.key
274
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
275
+ with open(config_path, 'w') as f:
276
+ json.dump(config, f, indent=4)
277
+ if not self.quiet:
278
+ print(f"API key generated successfully and updated in {config_path}")
279
+ except Exception as e:
280
+ if not self.quiet:
281
+ print(f"Warning: Failed to update config file: {e}")
282
+ return self.key
283
+
284
+ def view_api_key(self) -> str:
285
+ if not self.auth_client:
286
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
287
+ if not self.auth_client.token:
288
+ raise ConfigurationError("Not authenticated. Call login() first.")
289
+ self.key = self.auth_client.view_api_key()
290
+ self.session.headers.update({'x-api-key': self.key})
291
+ return self.key
292
+
293
+ def get_user_info(self) -> Dict[str, Any]:
294
+ if not self.auth_client:
295
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
296
+ if not self.auth_client.token:
297
+ raise ConfigurationError("Not authenticated. Call login() first.")
298
+ return self.auth_client.get_user_info()
299
+
300
+ def wcs(self, expr: str, feature: Union[Dict[str, Any], ShapelyGeometry], in_crs: str = "epsg:4326",
301
+ out_crs: str = "epsg:4326", output: str = "csv", resolution: int = -1,
302
+ **kwargs):
303
+ if hasattr(feature, 'is_valid'):
304
+ from shapely.geometry import mapping
305
+ feature = {
306
+ "type": "Feature",
307
+ "geometry": mapping(feature),
308
+ "properties": {}
309
+ }
310
+ self.validate_feature(feature)
311
+ payload = {
312
+ "feature": feature,
313
+ "in_crs": in_crs,
314
+ "out_crs": out_crs,
315
+ "output": output,
316
+ "resolution": resolution,
317
+ "expr": expr,
318
+ **kwargs
319
+ }
320
+ request_url = f"{self.url}/geoquery"
321
+ try:
322
+ response = self.session.post(request_url, json=payload, timeout=self.timeout, verify=self.verify)
323
+ if not response.ok:
324
+ error_msg = f"API request failed: {response.status_code} {response.reason}"
325
+ try:
326
+ error_data = response.json()
327
+ if "detail" in error_data:
328
+ error_msg += f" - {error_data['detail']}"
329
+ except:
330
+ pass
331
+ raise APIError(error_msg)
332
+ if output.lower() == "csv":
333
+ import pandas as pd
334
+ return pd.read_csv(BytesIO(response.content))
335
+ elif output.lower() == "netcdf":
336
+ return xr.open_dataset(BytesIO(response.content))
337
+ else:
338
+ try:
339
+ return xr.open_dataset(BytesIO(response.content))
340
+ except ValueError:
341
+ import pandas as pd
342
+ try:
343
+ return pd.read_csv(BytesIO(response.content))
344
+ except:
345
+ return response.content
346
+ except requests.RequestException as e:
347
+ raise APIError(f"Request failed: {str(e)}")
348
+
349
+ # Admin/protected methods
350
+ def _get_user_by_id(self, user_id: str):
351
+ if not self.user_management:
352
+ from terrakio_core.user_management import UserManagement
353
+ if not self.url or not self.key:
354
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
355
+ self.user_management = UserManagement(
356
+ api_url=self.url,
357
+ api_key=self.key,
358
+ verify=self.verify,
359
+ timeout=self.timeout
360
+ )
361
+ return self.user_management.get_user_by_id(user_id)
362
+
363
+ def _get_user_by_email(self, email: str):
364
+ if not self.user_management:
365
+ from terrakio_core.user_management import UserManagement
366
+ if not self.url or not self.key:
367
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
368
+ self.user_management = UserManagement(
369
+ api_url=self.url,
370
+ api_key=self.key,
371
+ verify=self.verify,
372
+ timeout=self.timeout
373
+ )
374
+ return self.user_management.get_user_by_email(email)
375
+
376
+ def _list_users(self, substring: str = None, uid: bool = False):
377
+ if not self.user_management:
378
+ from terrakio_core.user_management import UserManagement
379
+ if not self.url or not self.key:
380
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
381
+ self.user_management = UserManagement(
382
+ api_url=self.url,
383
+ api_key=self.key,
384
+ verify=self.verify,
385
+ timeout=self.timeout
386
+ )
387
+ return self.user_management.list_users(substring=substring, uid=uid)
388
+
389
+ def _edit_user(self, user_id: str, uid: str = None, email: str = None, role: str = None, apiKey: str = None, groups: list = None, quota: int = None):
390
+ if not self.user_management:
391
+ from terrakio_core.user_management import UserManagement
392
+ if not self.url or not self.key:
393
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
394
+ self.user_management = UserManagement(
395
+ api_url=self.url,
396
+ api_key=self.key,
397
+ verify=self.verify,
398
+ timeout=self.timeout
399
+ )
400
+ return self.user_management.edit_user(
401
+ user_id=user_id,
402
+ uid=uid,
403
+ email=email,
404
+ role=role,
405
+ apiKey=apiKey,
406
+ groups=groups,
407
+ quota=quota
408
+ )
409
+
410
+ def _reset_quota(self, email: str, quota: int = None):
411
+ if not self.user_management:
412
+ from terrakio_core.user_management import UserManagement
413
+ if not self.url or not self.key:
414
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
415
+ self.user_management = UserManagement(
416
+ api_url=self.url,
417
+ api_key=self.key,
418
+ verify=self.verify,
419
+ timeout=self.timeout
420
+ )
421
+ return self.user_management.reset_quota(email=email, quota=quota)
422
+
423
+ def _delete_user(self, uid: str):
424
+ if not self.user_management:
425
+ from terrakio_core.user_management import UserManagement
426
+ if not self.url or not self.key:
427
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
428
+ self.user_management = UserManagement(
429
+ api_url=self.url,
430
+ api_key=self.key,
431
+ verify=self.verify,
432
+ timeout=self.timeout
433
+ )
434
+ return self.user_management.delete_user(uid=uid)
435
+
436
+ # Dataset management protected methods
437
+ def _get_dataset(self, name: str, collection: str = "terrakio-datasets"):
438
+ if not self.dataset_management:
439
+ from terrakio_core.dataset_management import DatasetManagement
440
+ if not self.url or not self.key:
441
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
442
+ self.dataset_management = DatasetManagement(
443
+ api_url=self.url,
444
+ api_key=self.key,
445
+ verify=self.verify,
446
+ timeout=self.timeout
447
+ )
448
+ return self.dataset_management.get_dataset(name=name, collection=collection)
449
+
450
+ def _list_datasets(self, substring: str = None, collection: str = "terrakio-datasets"):
451
+ if not self.dataset_management:
452
+ from terrakio_core.dataset_management import DatasetManagement
453
+ if not self.url or not self.key:
454
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
455
+ self.dataset_management = DatasetManagement(
456
+ api_url=self.url,
457
+ api_key=self.key,
458
+ verify=self.verify,
459
+ timeout=self.timeout
460
+ )
461
+ return self.dataset_management.list_datasets(substring=substring, collection=collection)
462
+
463
+ def _create_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
464
+ if not self.dataset_management:
465
+ from terrakio_core.dataset_management import DatasetManagement
466
+ if not self.url or not self.key:
467
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
468
+ self.dataset_management = DatasetManagement(
469
+ api_url=self.url,
470
+ api_key=self.key,
471
+ verify=self.verify,
472
+ timeout=self.timeout
473
+ )
474
+ return self.dataset_management.create_dataset(name=name, collection=collection, **kwargs)
475
+
476
+ def _update_dataset(self, name: str, append: bool = True, collection: str = "terrakio-datasets", **kwargs):
477
+ if not self.dataset_management:
478
+ from terrakio_core.dataset_management import DatasetManagement
479
+ if not self.url or not self.key:
480
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
481
+ self.dataset_management = DatasetManagement(
482
+ api_url=self.url,
483
+ api_key=self.key,
484
+ verify=self.verify,
485
+ timeout=self.timeout
486
+ )
487
+ return self.dataset_management.update_dataset(name=name, append=append, collection=collection, **kwargs)
488
+
489
+ def _overwrite_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
490
+ if not self.dataset_management:
491
+ from terrakio_core.dataset_management import DatasetManagement
492
+ if not self.url or not self.key:
493
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
494
+ self.dataset_management = DatasetManagement(
495
+ api_url=self.url,
496
+ api_key=self.key,
497
+ verify=self.verify,
498
+ timeout=self.timeout
499
+ )
500
+ return self.dataset_management.overwrite_dataset(name=name, collection=collection, **kwargs)
501
+
502
+ def _delete_dataset(self, name: str, collection: str = "terrakio-datasets"):
503
+ if not self.dataset_management:
504
+ from terrakio_core.dataset_management import DatasetManagement
505
+ if not self.url or not self.key:
506
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
507
+ self.dataset_management = DatasetManagement(
508
+ api_url=self.url,
509
+ api_key=self.key,
510
+ verify=self.verify,
511
+ timeout=self.timeout
512
+ )
513
+ return self.dataset_management.delete_dataset(name=name, collection=collection)
514
+
515
+ def close(self):
516
+ """Close all client sessions"""
517
+ self.session.close()
518
+ if self.auth_client:
519
+ self.auth_client.session.close()
520
+ # Close aiohttp session if it exists
521
+ if self._aiohttp_session and not self._aiohttp_session.closed:
522
+ asyncio.run(self.close_async())
523
+
524
+ def __enter__(self):
525
+ return self
526
+
527
+ def __exit__(self, exc_type, exc_val, exc_tb):
528
+ self.close()
529
+
530
+ # Mass Stats methods
531
+ def upload_mass_stats(self, name, size, bucket, output, location=None, **kwargs):
532
+ if not self.mass_stats:
533
+ from terrakio_core.mass_stats import MassStats
534
+ if not self.url or not self.key:
535
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
536
+ self.mass_stats = MassStats(
537
+ base_url=self.url,
538
+ api_key=self.key,
539
+ verify=self.verify,
540
+ timeout=self.timeout
541
+ )
542
+ return self.mass_stats.upload_request(name, size, bucket, output, location, **kwargs)
543
+
544
+ def start_mass_stats_job(self, task_id):
545
+ if not self.mass_stats:
546
+ from terrakio_core.mass_stats import MassStats
547
+ if not self.url or not self.key:
548
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
549
+ self.mass_stats = MassStats(
550
+ base_url=self.url,
551
+ api_key=self.key,
552
+ verify=self.verify,
553
+ timeout=self.timeout
554
+ )
555
+ return self.mass_stats.start_job(task_id)
556
+
557
+ def get_mass_stats_task_id(self, name, stage, uid=None):
558
+ if not self.mass_stats:
559
+ from terrakio_core.mass_stats import MassStats
560
+ if not self.url or not self.key:
561
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
562
+ self.mass_stats = MassStats(
563
+ base_url=self.url,
564
+ api_key=self.key,
565
+ verify=self.verify,
566
+ timeout=self.timeout
567
+ )
568
+ return self.mass_stats.get_task_id(name, stage, uid)
569
+
570
+ def track_mass_stats_job(self, ids=None):
571
+ if not self.mass_stats:
572
+ from terrakio_core.mass_stats import MassStats
573
+ if not self.url or not self.key:
574
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
575
+ self.mass_stats = MassStats(
576
+ base_url=self.url,
577
+ api_key=self.key,
578
+ verify=self.verify,
579
+ timeout=self.timeout
580
+ )
581
+ return self.mass_stats.track_job(ids)
582
+
583
+ def get_mass_stats_history(self, limit=100):
584
+ if not self.mass_stats:
585
+ from terrakio_core.mass_stats import MassStats
586
+ if not self.url or not self.key:
587
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
588
+ self.mass_stats = MassStats(
589
+ base_url=self.url,
590
+ api_key=self.key,
591
+ verify=self.verify,
592
+ timeout=self.timeout
593
+ )
594
+ return self.mass_stats.get_history(limit)
595
+
596
+ def start_mass_stats_post_processing(self, process_name, data_name, output, consumer_path, overwrite=False):
597
+ if not self.mass_stats:
598
+ from terrakio_core.mass_stats import MassStats
599
+ if not self.url or not self.key:
600
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
601
+ self.mass_stats = MassStats(
602
+ base_url=self.url,
603
+ api_key=self.key,
604
+ verify=self.verify,
605
+ timeout=self.timeout
606
+ )
607
+ return self.mass_stats.start_post_processing(process_name, data_name, output, consumer_path, overwrite)
608
+
609
+ def download_mass_stats_results(self, id=None, force_loc=False, **kwargs):
610
+ if not self.mass_stats:
611
+ from terrakio_core.mass_stats import MassStats
612
+ if not self.url or not self.key:
613
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
614
+ self.mass_stats = MassStats(
615
+ base_url=self.url,
616
+ api_key=self.key,
617
+ verify=self.verify,
618
+ timeout=self.timeout
619
+ )
620
+ return self.mass_stats.download_results(id, force_loc, **kwargs)
621
+
622
+ def cancel_mass_stats_job(self, id):
623
+ if not self.mass_stats:
624
+ from terrakio_core.mass_stats import MassStats
625
+ if not self.url or not self.key:
626
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
627
+ self.mass_stats = MassStats(
628
+ base_url=self.url,
629
+ api_key=self.key,
630
+ verify=self.verify,
631
+ timeout=self.timeout
632
+ )
633
+ return self.mass_stats.cancel_job(id)
634
+
635
+ def cancel_all_mass_stats_jobs(self):
636
+ if not self.mass_stats:
637
+ from terrakio_core.mass_stats import MassStats
638
+ if not self.url or not self.key:
639
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
640
+ self.mass_stats = MassStats(
641
+ base_url=self.url,
642
+ api_key=self.key,
643
+ verify=self.verify,
644
+ timeout=self.timeout
645
+ )
646
+ return self.mass_stats.cancel_all_jobs()
647
+
648
+ def _create_pyramids(self, name, levels, config):
649
+ if not self.mass_stats:
650
+ from terrakio_core.mass_stats import MassStats
651
+ if not self.url or not self.key:
652
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
653
+ self.mass_stats = MassStats(
654
+ base_url=self.url,
655
+ api_key=self.key,
656
+ verify=self.verify,
657
+ timeout=self.timeout
658
+ )
659
+ return self.mass_stats.create_pyramids(name, levels, config)
660
+
661
+ def random_sample(self, name, **kwargs):
662
+ if not self.mass_stats:
663
+ from terrakio_core.mass_stats import MassStats
664
+ if not self.url or not self.key:
665
+ raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
666
+ self.mass_stats = MassStats(
667
+ base_url=self.url,
668
+ api_key=self.key,
669
+ verify=self.verify,
670
+ timeout=self.timeout
671
+ )
672
+ return self.mass_stats.random_sample(name, **kwargs)
673
+
674
+ async def zonal_stats_async(self, gdb, expr, conc=20, inplace=False, output="csv"):
675
+ """
676
+ Compute zonal statistics for all geometries in a GeoDataFrame using asyncio for concurrency.
677
+ """
678
+
679
+ # Process geometries in batches
680
+ all_results = []
681
+ row_indices = []
682
+
683
+ async def process_geometry(geom, index):
684
+ """Process a single geometry"""
685
+ try:
686
+ feature = {
687
+ "type": "Feature",
688
+ "geometry": mapping(geom),
689
+ "properties": {"index": index}
690
+ }
691
+ result = await self.wcs_async(expr=expr, feature=feature, output=output)
692
+ # Add original index to track which geometry this result belongs to
693
+ if isinstance(result, pd.DataFrame):
694
+ result['_geometry_index'] = index
695
+ return result
696
+ except Exception as e:
697
+ raise
698
+
699
+ async def process_batch(batch_indices):
700
+ """Process a batch of geometries concurrently using TaskGroup"""
701
+ try:
702
+ async with asyncio.TaskGroup() as tg:
703
+ tasks = []
704
+ for idx in batch_indices:
705
+ geom = gdb.geometry.iloc[idx]
706
+ task = tg.create_task(process_geometry(geom, idx))
707
+ tasks.append(task)
708
+
709
+ # Get results from completed tasks
710
+ results = []
711
+ for task in tasks:
712
+ try:
713
+ result = task.result()
714
+ results.append(result)
715
+ except Exception as e:
716
+ raise
717
+
718
+ return results
719
+ except* Exception as e:
720
+ # Get the actual exceptions from the tasks
721
+ for task in tasks:
722
+ if task.done() and task.exception():
723
+ raise task.exception()
724
+ raise
725
+
726
+ # Process in batches to control concurrency
727
+ for i in range(0, len(gdb), conc):
728
+ batch_indices = range(i, min(i + conc, len(gdb)))
729
+ try:
730
+ batch_results = await process_batch(batch_indices)
731
+ all_results.extend(batch_results)
732
+ row_indices.extend(batch_indices)
733
+ except Exception as e:
734
+ if hasattr(e, 'response'):
735
+ raise APIError(f"API request failed: {e.response.text}")
736
+ raise
737
+
738
+ if not all_results:
739
+ raise ValueError("No valid results were returned for any geometry")
740
+
741
+ # Combine all results
742
+ combined_df = pd.concat(all_results, ignore_index=True)
743
+
744
+ # Check if we have temporal results
745
+ has_time = 'time' in combined_df.columns
746
+
747
+ # Create a result GeoDataFrame
748
+ if has_time:
749
+ # For temporal data, we'll create a hierarchical index
750
+ # First make sure we have the geometry index and time columns
751
+ if '_geometry_index' not in combined_df.columns:
752
+ raise ValueError("Missing geometry index in results")
753
+
754
+ # Create hierarchical index on geometry_index and time
755
+ combined_df.set_index(['_geometry_index', 'time'], inplace=True)
756
+
757
+ # For each unique geometry index, we need the corresponding geometry
758
+ geometry_series = gdb.geometry.copy()
759
+
760
+ # Get columns that will become new attributes (exclude index/utility columns)
761
+ result_cols = combined_df.columns
762
+
763
+ # Create a new GeoDataFrame with multi-index
764
+ result_rows = []
765
+ geometries = []
766
+
767
+ # Iterate through the hierarchical index
768
+ for (geom_idx, time_val), row in combined_df.iterrows():
769
+ # Create a new row with geometry properties + result columns
770
+ new_row = {}
771
+
772
+ # Add original GeoDataFrame columns (except geometry)
773
+ for col in gdb.columns:
774
+ if col != 'geometry':
775
+ new_row[col] = gdb.loc[geom_idx, col]
776
+
777
+ # Add result columns
778
+ for col in result_cols:
779
+ new_row[col] = row[col]
780
+
781
+ result_rows.append(new_row)
782
+ geometries.append(gdb.geometry.iloc[geom_idx])
783
+
784
+ # Create a new GeoDataFrame with multi-index
785
+ multi_index = pd.MultiIndex.from_tuples(
786
+ combined_df.index.tolist(),
787
+ names=['geometry_index', 'time']
788
+ )
789
+
790
+ result_gdf = gpd.GeoDataFrame(
791
+ result_rows,
792
+ geometry=geometries,
793
+ index=multi_index
794
+ )
795
+
796
+ if inplace:
797
+ # Can't really do inplace with multi-temporal results as we're changing the structure
798
+ return result_gdf
799
+ else:
800
+ return result_gdf
801
+ else:
802
+ # Non-temporal data - just add new columns to the existing GeoDataFrame
803
+ result_gdf = gdb.copy() if not inplace else gdb
804
+
805
+ # Get column names from the results (excluding utility columns)
806
+ result_cols = [col for col in combined_df.columns if col not in ['_geometry_index']]
807
+
808
+ # Create a mapping from geometry index to result rows
809
+ geom_idx_to_row = {}
810
+ for idx, row in combined_df.iterrows():
811
+ geom_idx = int(row['_geometry_index'])
812
+ geom_idx_to_row[geom_idx] = row
813
+
814
+ # Add results as new columns to the GeoDataFrame
815
+ for col in result_cols:
816
+ # Initialize the column with None or appropriate default
817
+ if col not in result_gdf.columns:
818
+ result_gdf[col] = None
819
+
820
+ # Fill in values from results
821
+ for geom_idx, row in geom_idx_to_row.items():
822
+ result_gdf.loc[geom_idx, col] = row[col]
823
+
824
+ if inplace:
825
+ return None
826
+ else:
827
+ return result_gdf
828
+
829
+ def zonal_stats(self, gdb, expr, conc=20, inplace=False, output="csv"):
830
+ """
831
+ Compute zonal statistics for all geometries in a GeoDataFrame.
832
+
833
+ Args:
834
+ gdb (geopandas.GeoDataFrame): GeoDataFrame containing geometries
835
+ expr (str): Terrakio expression to evaluate, can include spatial aggregations
836
+ conc (int): Number of concurrent requests to make
837
+ inplace (bool): Whether to modify the input GeoDataFrame in place
838
+ output (str): Output format (csv or netcdf)
839
+
840
+ Returns:
841
+ geopandas.GeoDataFrame: GeoDataFrame with added columns for results, or None if inplace=True
842
+ """
843
+ import asyncio
844
+ result = asyncio.run(self.zonal_stats_async(gdb, expr, conc, inplace, output))
845
+ # Ensure aiohttp session is closed after running async code
846
+ try:
847
+ if self._aiohttp_session and not self._aiohttp_session.closed:
848
+ asyncio.run(self.close_async())
849
+ except RuntimeError:
850
+ # Event loop may already be closed, ignore
851
+ pass
852
+ return result
853
+
854
+ # Group access management protected methods
855
+ def _get_group_users_and_datasets(self, group_name: str):
856
+ if not hasattr(self, "group_access_management") or self.group_access_management is None:
857
+ from terrakio_core.group_access_management import GroupAccessManagement
858
+ if not self.url or not self.key:
859
+ raise ConfigurationError("Group access management client not initialized. Make sure API URL and key are set.")
860
+ self.group_access_management = GroupAccessManagement(
861
+ api_url=self.url,
862
+ api_key=self.key,
863
+ verify=self.verify,
864
+ timeout=self.timeout
865
+ )
866
+ return self.group_access_management.get_group_users_and_datasets(group_name)
867
+
868
+ def _add_group_to_dataset(self, dataset: str, group: str):
869
+ if not hasattr(self, "group_access_management") or self.group_access_management is None:
870
+ from terrakio_core.group_access_management import GroupAccessManagement
871
+ if not self.url or not self.key:
872
+ raise ConfigurationError("Group access management client not initialized. Make sure API URL and key are set.")
873
+ self.group_access_management = GroupAccessManagement(
874
+ api_url=self.url,
875
+ api_key=self.key,
876
+ verify=self.verify,
877
+ timeout=self.timeout
878
+ )
879
+ return self.group_access_management.add_group_to_dataset(dataset, group)
880
+
881
+ def _add_group_to_user(self, uid: str, group: str):
882
+ if not hasattr(self, "group_access_management") or self.group_access_management is None:
883
+ from terrakio_core.group_access_management import GroupAccessManagement
884
+ if not self.url or not self.key:
885
+ raise ConfigurationError("Group access management client not initialized. Make sure API URL and key are set.")
886
+ self.group_access_management = GroupAccessManagement(
887
+ api_url=self.url,
888
+ api_key=self.key,
889
+ verify=self.verify,
890
+ timeout=self.timeout
891
+ )
892
+ print("the uid is and the group is ", uid, group)
893
+ return self.group_access_management.add_group_to_user(uid, group)
894
+
895
+ def _delete_group_from_user(self, uid: str, group: str):
896
+ if not hasattr(self, "group_access_management") or self.group_access_management is None:
897
+ from terrakio_core.group_access_management import GroupAccessManagement
898
+ if not self.url or not self.key:
899
+ raise ConfigurationError("Group access management client not initialized. Make sure API URL and key are set.")
900
+ self.group_access_management = GroupAccessManagement(
901
+ api_url=self.url,
902
+ api_key=self.key,
903
+ verify=self.verify,
904
+ timeout=self.timeout
905
+ )
906
+ return self.group_access_management.delete_group_from_user(uid, group)
907
+
908
+ def _delete_group_from_dataset(self, dataset: str, group: str):
909
+ if not hasattr(self, "group_access_management") or self.group_access_management is None:
910
+ from terrakio_core.group_access_management import GroupAccessManagement
911
+ if not self.url or not self.key:
912
+ raise ConfigurationError("Group access management client not initialized. Make sure API URL and key are set.")
913
+ self.group_access_management = GroupAccessManagement(
914
+ api_url=self.url,
915
+ api_key=self.key,
916
+ verify=self.verify,
917
+ timeout=self.timeout
918
+ )
919
+ return self.group_access_management.delete_group_from_dataset(dataset, group)
920
+
921
+ # Space management protected methods
922
+ def _get_total_space_used(self):
923
+ if not hasattr(self, "space_management") or self.space_management is None:
924
+ from terrakio_core.space_management import SpaceManagement
925
+ if not self.url or not self.key:
926
+ raise ConfigurationError("Space management client not initialized. Make sure API URL and key are set.")
927
+ self.space_management = SpaceManagement(
928
+ api_url=self.url,
929
+ api_key=self.key,
930
+ verify=self.verify,
931
+ timeout=self.timeout
932
+ )
933
+ return self.space_management.get_total_space_used()
934
+
935
+ def _get_space_used_by_job(self, name: str, region: str = None):
936
+ if not hasattr(self, "space_management") or self.space_management is None:
937
+ from terrakio_core.space_management import SpaceManagement
938
+ if not self.url or not self.key:
939
+ raise ConfigurationError("Space management client not initialized. Make sure API URL and key are set.")
940
+ self.space_management = SpaceManagement(
941
+ api_url=self.url,
942
+ api_key=self.key,
943
+ verify=self.verify,
944
+ timeout=self.timeout
945
+ )
946
+ return self.space_management.get_space_used_by_job(name, region)
947
+
948
+ def _delete_user_job(self, name: str, region: str = None):
949
+ if not hasattr(self, "space_management") or self.space_management is None:
950
+ from terrakio_core.space_management import SpaceManagement
951
+ if not self.url or not self.key:
952
+ raise ConfigurationError("Space management client not initialized. Make sure API URL and key are set.")
953
+ self.space_management = SpaceManagement(
954
+ api_url=self.url,
955
+ api_key=self.key,
956
+ verify=self.verify,
957
+ timeout=self.timeout
958
+ )
959
+ return self.space_management.delete_user_job(name, region)
960
+
961
+ def _delete_data_in_path(self, path: str, region: str = None):
962
+ if not hasattr(self, "space_management") or self.space_management is None:
963
+ from terrakio_core.space_management import SpaceManagement
964
+ if not self.url or not self.key:
965
+ raise ConfigurationError("Space management client not initialized. Make sure API URL and key are set.")
966
+ self.space_management = SpaceManagement(
967
+ api_url=self.url,
968
+ api_key=self.key,
969
+ verify=self.verify,
970
+ timeout=self.timeout
971
+ )
972
+ return self.space_management.delete_data_in_path(path, region)
973
+
974
+ def train_model(self, model_name: str, training_data: dict) -> dict:
975
+ """
976
+ Train a model using the external model training API.
977
+
978
+ Args:
979
+ model_name (str): The name of the model to train.
980
+ training_data (dict): Dictionary containing training data parameters.
981
+
982
+ Returns:
983
+ dict: The response from the model training API.
984
+ """
985
+ endpoint = "https://modeltraining-573248941006.australia-southeast1.run.app/train_model"
986
+ payload = {
987
+ "model_name": model_name,
988
+ "training_data": training_data
989
+ }
990
+ try:
991
+ response = self.session.post(endpoint, json=payload, timeout=self.timeout, verify=self.verify)
992
+ if not response.ok:
993
+ error_msg = f"Model training request failed: {response.status_code} {response.reason}"
994
+ try:
995
+ error_data = response.json()
996
+ if "detail" in error_data:
997
+ error_msg += f" - {error_data['detail']}"
998
+ except Exception:
999
+ if response.text:
1000
+ error_msg += f" - {response.text}"
1001
+ raise APIError(error_msg)
1002
+ return response.json()
1003
+ except requests.RequestException as e:
1004
+ raise APIError(f"Model training request failed: {str(e)}")
1005
+