terrakio-core 0.2.2__py3-none-any.whl → 0.2.3__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/client.py DELETED
@@ -1,973 +0,0 @@
1
- import requests
2
- import xarray as xr
3
- from io import BytesIO
4
- from typing import Dict, Any, Optional, Union
5
- import json
6
- import aiohttp
7
- import asyncio
8
- from shapely.geometry import shape
9
- from shapely.geometry.base import BaseGeometry as ShapelyGeometry
10
- from .exceptions import APIError, ConfigurationError
11
-
12
- class BaseClient:
13
- def __init__(self, url: Optional[str] = None, key: Optional[str] = None,
14
- auth_url: Optional[str] = "https://dev-au.terrak.io",
15
- quiet: bool = False, config_file: Optional[str] = None,
16
- verify: bool = True, timeout: int = 60):
17
- self.quiet = quiet
18
- self.verify = verify
19
- self.timeout = timeout
20
- self.auth_client = None
21
- if auth_url:
22
- from terrakio_core.auth import AuthClient
23
- self.auth_client = AuthClient(
24
- base_url=auth_url,
25
- verify=verify,
26
- timeout=timeout
27
- )
28
- self.url = url
29
- self.key = key
30
- if self.url is None or self.key is None:
31
- from terrakio_core.config import read_config_file, DEFAULT_CONFIG_FILE
32
- if config_file is None:
33
- config_file = DEFAULT_CONFIG_FILE
34
- try:
35
- config = read_config_file(config_file)
36
- if self.url is None:
37
- self.url = config.get('url')
38
- if self.key is None:
39
- self.key = config.get('key')
40
- except Exception as e:
41
- raise ConfigurationError(
42
- f"Failed to read configuration: {e}\n\n"
43
- "To fix this issue:\n"
44
- "1. Create a file at ~/.terrakioapirc with:\n"
45
- "url: https://api.terrak.io\n"
46
- "key: your-api-key\n\n"
47
- "OR\n\n"
48
- "2. Initialize the client with explicit parameters:\n"
49
- "client = terrakio_api.Client(\n"
50
- " url='https://api.terrak.io',\n"
51
- " key='your-api-key'\n"
52
- ")"
53
- )
54
- if not self.url:
55
- raise ConfigurationError("Missing API URL in configuration")
56
- if not self.key:
57
- raise ConfigurationError("Missing API key in configuration")
58
- self.url = self.url.rstrip('/')
59
- if not self.quiet:
60
- print(f"Using Terrakio API at: {self.url}")
61
- self.session = requests.Session()
62
- self.session.headers.update({
63
- 'Content-Type': 'application/json',
64
- 'x-api-key': self.key
65
- })
66
- self.user_management = None
67
- self.dataset_management = None
68
- self.mass_stats = None
69
- self._aiohttp_session = None
70
-
71
- @property
72
- async def aiohttp_session(self):
73
- if self._aiohttp_session is None or self._aiohttp_session.closed:
74
- self._aiohttp_session = aiohttp.ClientSession(
75
- headers={
76
- 'Content-Type': 'application/json',
77
- 'x-api-key': self.key
78
- },
79
- timeout=aiohttp.ClientTimeout(total=self.timeout)
80
- )
81
- return self._aiohttp_session
82
-
83
- async def wcs_async(self, expr: str, feature: Union[Dict[str, Any], ShapelyGeometry],
84
- in_crs: str = "epsg:4326", out_crs: str = "epsg:4326",
85
- output: str = "csv", resolution: int = -1, **kwargs):
86
- """
87
- Asynchronous version of the wcs() method using aiohttp.
88
-
89
- Args:
90
- expr (str): The WCS expression to evaluate
91
- feature (Union[Dict[str, Any], ShapelyGeometry]): The geographic feature
92
- in_crs (str): Input coordinate reference system
93
- out_crs (str): Output coordinate reference system
94
- output (str): Output format ('csv' or 'netcdf')
95
- resolution (int): Resolution parameter
96
- **kwargs: Additional parameters to pass to the WCS request
97
-
98
- Returns:
99
- Union[pd.DataFrame, xr.Dataset, bytes]: The response data in the requested format
100
- """
101
- if hasattr(feature, 'is_valid'):
102
- from shapely.geometry import mapping
103
- feature = {
104
- "type": "Feature",
105
- "geometry": mapping(feature),
106
- "properties": {}
107
- }
108
- self.validate_feature(feature)
109
-
110
- payload = {
111
- "feature": feature,
112
- "in_crs": in_crs,
113
- "out_crs": out_crs,
114
- "output": output,
115
- "resolution": resolution,
116
- "expr": expr,
117
- **kwargs
118
- }
119
-
120
- request_url = f"{self.url}/geoquery"
121
-
122
- try:
123
- # Get the shared aiohttp session
124
- session = await self.aiohttp_session
125
- async with session.post(request_url, json=payload, ssl=self.verify) as response:
126
- if not response.ok:
127
- error_msg = f"API request failed: {response.status} {response.reason}"
128
- try:
129
- error_data = await response.json()
130
- if "detail" in error_data:
131
- error_msg += f" - {error_data['detail']}"
132
- except:
133
- pass
134
- raise APIError(error_msg)
135
-
136
- content = await response.read()
137
-
138
- if output.lower() == "csv":
139
- import pandas as pd
140
- df = pd.read_csv(BytesIO(content))
141
- return df
142
- elif output.lower() == "netcdf":
143
- return xr.open_dataset(BytesIO(content))
144
- else:
145
- try:
146
- return xr.open_dataset(BytesIO(content))
147
- except ValueError:
148
- import pandas as pd
149
- try:
150
- return pd.read_csv(BytesIO(content))
151
- except:
152
- return content
153
-
154
- except aiohttp.ClientError as e:
155
- raise APIError(f"Request failed: {str(e)}")
156
- except Exception as e:
157
- raise
158
-
159
- async def close_async(self):
160
- """Close the aiohttp session"""
161
- if self._aiohttp_session and not self._aiohttp_session.closed:
162
- await self._aiohttp_session.close()
163
- self._aiohttp_session = None
164
-
165
- async def __aenter__(self):
166
- return self
167
-
168
- async def __aexit__(self, exc_type, exc_val, exc_tb):
169
- await self.close_async()
170
-
171
- def validate_feature(self, feature: Dict[str, Any]) -> None:
172
- if hasattr(feature, 'is_valid'):
173
- from shapely.geometry import mapping
174
- feature = {
175
- "type": "Feature",
176
- "geometry": mapping(feature),
177
- "properties": {}
178
- }
179
- if not isinstance(feature, dict):
180
- raise ValueError("Feature must be a dictionary or a Shapely geometry")
181
- if feature.get("type") != "Feature":
182
- raise ValueError("GeoJSON object must be of type 'Feature'")
183
- if "geometry" not in feature:
184
- raise ValueError("Feature must contain a 'geometry' field")
185
- if "properties" not in feature:
186
- raise ValueError("Feature must contain a 'properties' field")
187
- try:
188
- geometry = shape(feature["geometry"])
189
- except Exception as e:
190
- raise ValueError(f"Invalid geometry format: {str(e)}")
191
- if not geometry.is_valid:
192
- raise ValueError(f"Invalid geometry: {geometry.is_valid_reason}")
193
- geom_type = feature["geometry"]["type"]
194
- if geom_type == "Point":
195
- if len(feature["geometry"]["coordinates"]) != 2:
196
- raise ValueError("Point must have exactly 2 coordinates")
197
- elif geom_type == "Polygon":
198
- if not geometry.is_simple:
199
- raise ValueError("Polygon must be simple (not self-intersecting)")
200
- if geometry.area == 0:
201
- raise ValueError("Polygon must have non-zero area")
202
- coords = feature["geometry"]["coordinates"][0]
203
- if coords[0] != coords[-1]:
204
- raise ValueError("Polygon must be closed (first and last points must match)")
205
-
206
- def signup(self, email: str, password: str) -> Dict[str, Any]:
207
- if not self.auth_client:
208
- raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
209
- return self.auth_client.signup(email, password)
210
-
211
- def login(self, email: str, password: str) -> Dict[str, str]:
212
- if not self.auth_client:
213
- raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
214
-
215
- try:
216
- # First attempt to login
217
- token_response = self.auth_client.login(email, password)
218
-
219
- print("the token response is ", token_response)
220
- # Only proceed with API key retrieval if login was successful
221
- if token_response:
222
- # After successful login, get the API key
223
- api_key_response = self.view_api_key()
224
- self.key = api_key_response
225
-
226
- # Save email and API key to config file
227
- import os
228
- import json
229
- config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
230
- try:
231
- config = {"EMAIL": email, "TERRAKIO_API_KEY": self.key}
232
- if os.path.exists(config_path):
233
- with open(config_path, 'r') as f:
234
- config = json.load(f)
235
- config["EMAIL"] = email
236
- config["TERRAKIO_API_KEY"] = self.key
237
-
238
- os.makedirs(os.path.dirname(config_path), exist_ok=True)
239
- with open(config_path, 'w') as f:
240
- json.dump(config, f, indent=4)
241
-
242
- if not self.quiet:
243
- print(f"Successfully authenticated as: {email}")
244
- print(f"API key saved to {config_path}")
245
- except Exception as e:
246
- if not self.quiet:
247
- print(f"Warning: Failed to update config file: {e}")
248
-
249
- return {"token": token_response} if token_response else {"error": "Login failed"}
250
- except Exception as e:
251
- if not self.quiet:
252
- print(f"Login failed: {str(e)}")
253
- raise
254
-
255
- def refresh_api_key(self) -> str:
256
- if not self.auth_client:
257
- raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
258
- if not self.auth_client.token:
259
- raise ConfigurationError("Not authenticated. Call login() first.")
260
- self.key = self.auth_client.refresh_api_key()
261
- self.session.headers.update({'x-api-key': self.key})
262
- import os
263
- config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
264
- try:
265
- config = {"EMAIL": "", "TERRAKIO_API_KEY": ""}
266
- if os.path.exists(config_path):
267
- with open(config_path, 'r') as f:
268
- config = json.load(f)
269
- config["TERRAKIO_API_KEY"] = self.key
270
- os.makedirs(os.path.dirname(config_path), exist_ok=True)
271
- with open(config_path, 'w') as f:
272
- json.dump(config, f, indent=4)
273
- if not self.quiet:
274
- print(f"API key generated successfully and updated in {config_path}")
275
- except Exception as e:
276
- if not self.quiet:
277
- print(f"Warning: Failed to update config file: {e}")
278
- return self.key
279
-
280
- def view_api_key(self) -> str:
281
- if not self.auth_client:
282
- raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
283
- if not self.auth_client.token:
284
- raise ConfigurationError("Not authenticated. Call login() first.")
285
- self.key = self.auth_client.view_api_key()
286
- self.session.headers.update({'x-api-key': self.key})
287
- return self.key
288
-
289
- def get_user_info(self) -> Dict[str, Any]:
290
- if not self.auth_client:
291
- raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
292
- if not self.auth_client.token:
293
- raise ConfigurationError("Not authenticated. Call login() first.")
294
- return self.auth_client.get_user_info()
295
-
296
- def wcs(self, expr: str, feature: Union[Dict[str, Any], ShapelyGeometry], in_crs: str = "epsg:4326",
297
- out_crs: str = "epsg:4326", output: str = "csv", resolution: int = -1,
298
- **kwargs):
299
- if hasattr(feature, 'is_valid'):
300
- from shapely.geometry import mapping
301
- feature = {
302
- "type": "Feature",
303
- "geometry": mapping(feature),
304
- "properties": {}
305
- }
306
- self.validate_feature(feature)
307
- payload = {
308
- "feature": feature,
309
- "in_crs": in_crs,
310
- "out_crs": out_crs,
311
- "output": output,
312
- "resolution": resolution,
313
- "expr": expr,
314
- **kwargs
315
- }
316
- request_url = f"{self.url}/geoquery"
317
- try:
318
- response = self.session.post(request_url, json=payload, timeout=self.timeout, verify=self.verify)
319
- if not response.ok:
320
- error_msg = f"API request failed: {response.status_code} {response.reason}"
321
- try:
322
- error_data = response.json()
323
- if "detail" in error_data:
324
- error_msg += f" - {error_data['detail']}"
325
- except:
326
- pass
327
- raise APIError(error_msg)
328
- if output.lower() == "csv":
329
- import pandas as pd
330
- return pd.read_csv(BytesIO(response.content))
331
- elif output.lower() == "netcdf":
332
- return xr.open_dataset(BytesIO(response.content))
333
- else:
334
- try:
335
- return xr.open_dataset(BytesIO(response.content))
336
- except ValueError:
337
- import pandas as pd
338
- try:
339
- return pd.read_csv(BytesIO(response.content))
340
- except:
341
- return response.content
342
- except requests.RequestException as e:
343
- raise APIError(f"Request failed: {str(e)}")
344
-
345
- # Admin/protected methods
346
- def _get_user_by_id(self, user_id: str):
347
- if not self.user_management:
348
- from terrakio_core.user_management import UserManagement
349
- if not self.url or not self.key:
350
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
351
- self.user_management = UserManagement(
352
- api_url=self.url,
353
- api_key=self.key,
354
- verify=self.verify,
355
- timeout=self.timeout
356
- )
357
- return self.user_management.get_user_by_id(user_id)
358
-
359
- def _get_user_by_email(self, email: str):
360
- if not self.user_management:
361
- from terrakio_core.user_management import UserManagement
362
- if not self.url or not self.key:
363
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
364
- self.user_management = UserManagement(
365
- api_url=self.url,
366
- api_key=self.key,
367
- verify=self.verify,
368
- timeout=self.timeout
369
- )
370
- return self.user_management.get_user_by_email(email)
371
-
372
- def _list_users(self, substring: str = None, uid: bool = False):
373
- if not self.user_management:
374
- from terrakio_core.user_management import UserManagement
375
- if not self.url or not self.key:
376
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
377
- self.user_management = UserManagement(
378
- api_url=self.url,
379
- api_key=self.key,
380
- verify=self.verify,
381
- timeout=self.timeout
382
- )
383
- return self.user_management.list_users(substring=substring, uid=uid)
384
-
385
- 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):
386
- if not self.user_management:
387
- from terrakio_core.user_management import UserManagement
388
- if not self.url or not self.key:
389
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
390
- self.user_management = UserManagement(
391
- api_url=self.url,
392
- api_key=self.key,
393
- verify=self.verify,
394
- timeout=self.timeout
395
- )
396
- return self.user_management.edit_user(
397
- user_id=user_id,
398
- uid=uid,
399
- email=email,
400
- role=role,
401
- apiKey=apiKey,
402
- groups=groups,
403
- quota=quota
404
- )
405
-
406
- def _reset_quota(self, email: str, quota: int = None):
407
- if not self.user_management:
408
- from terrakio_core.user_management import UserManagement
409
- if not self.url or not self.key:
410
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
411
- self.user_management = UserManagement(
412
- api_url=self.url,
413
- api_key=self.key,
414
- verify=self.verify,
415
- timeout=self.timeout
416
- )
417
- return self.user_management.reset_quota(email=email, quota=quota)
418
-
419
- def _delete_user(self, uid: str):
420
- if not self.user_management:
421
- from terrakio_core.user_management import UserManagement
422
- if not self.url or not self.key:
423
- raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
424
- self.user_management = UserManagement(
425
- api_url=self.url,
426
- api_key=self.key,
427
- verify=self.verify,
428
- timeout=self.timeout
429
- )
430
- return self.user_management.delete_user(uid=uid)
431
-
432
- # Dataset management protected methods
433
- def _get_dataset(self, name: str, collection: str = "terrakio-datasets"):
434
- if not self.dataset_management:
435
- from terrakio_core.dataset_management import DatasetManagement
436
- if not self.url or not self.key:
437
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
438
- self.dataset_management = DatasetManagement(
439
- api_url=self.url,
440
- api_key=self.key,
441
- verify=self.verify,
442
- timeout=self.timeout
443
- )
444
- return self.dataset_management.get_dataset(name=name, collection=collection)
445
-
446
- def _list_datasets(self, substring: str = None, collection: str = "terrakio-datasets"):
447
- if not self.dataset_management:
448
- from terrakio_core.dataset_management import DatasetManagement
449
- if not self.url or not self.key:
450
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
451
- self.dataset_management = DatasetManagement(
452
- api_url=self.url,
453
- api_key=self.key,
454
- verify=self.verify,
455
- timeout=self.timeout
456
- )
457
- return self.dataset_management.list_datasets(substring=substring, collection=collection)
458
-
459
- def _create_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
460
- if not self.dataset_management:
461
- from terrakio_core.dataset_management import DatasetManagement
462
- if not self.url or not self.key:
463
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
464
- self.dataset_management = DatasetManagement(
465
- api_url=self.url,
466
- api_key=self.key,
467
- verify=self.verify,
468
- timeout=self.timeout
469
- )
470
- return self.dataset_management.create_dataset(name=name, collection=collection, **kwargs)
471
-
472
- def _update_dataset(self, name: str, append: bool = True, collection: str = "terrakio-datasets", **kwargs):
473
- if not self.dataset_management:
474
- from terrakio_core.dataset_management import DatasetManagement
475
- if not self.url or not self.key:
476
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
477
- self.dataset_management = DatasetManagement(
478
- api_url=self.url,
479
- api_key=self.key,
480
- verify=self.verify,
481
- timeout=self.timeout
482
- )
483
- return self.dataset_management.update_dataset(name=name, append=append, collection=collection, **kwargs)
484
-
485
- def _overwrite_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
486
- if not self.dataset_management:
487
- from terrakio_core.dataset_management import DatasetManagement
488
- if not self.url or not self.key:
489
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
490
- self.dataset_management = DatasetManagement(
491
- api_url=self.url,
492
- api_key=self.key,
493
- verify=self.verify,
494
- timeout=self.timeout
495
- )
496
- return self.dataset_management.overwrite_dataset(name=name, collection=collection, **kwargs)
497
-
498
- def _delete_dataset(self, name: str, collection: str = "terrakio-datasets"):
499
- if not self.dataset_management:
500
- from terrakio_core.dataset_management import DatasetManagement
501
- if not self.url or not self.key:
502
- raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
503
- self.dataset_management = DatasetManagement(
504
- api_url=self.url,
505
- api_key=self.key,
506
- verify=self.verify,
507
- timeout=self.timeout
508
- )
509
- return self.dataset_management.delete_dataset(name=name, collection=collection)
510
-
511
- def close(self):
512
- """Close all client sessions"""
513
- self.session.close()
514
- if self.auth_client:
515
- self.auth_client.session.close()
516
- # Close aiohttp session if it exists
517
- if self._aiohttp_session and not self._aiohttp_session.closed:
518
- asyncio.run(self.close_async())
519
-
520
- def __enter__(self):
521
- return self
522
-
523
- def __exit__(self, exc_type, exc_val, exc_tb):
524
- self.close()
525
-
526
- # Mass Stats methods
527
- def upload_mass_stats(self, name, size, bucket, output, location=None, **kwargs):
528
- if not self.mass_stats:
529
- from terrakio_core.mass_stats import MassStats
530
- if not self.url or not self.key:
531
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
532
- self.mass_stats = MassStats(
533
- base_url=self.url,
534
- api_key=self.key,
535
- verify=self.verify,
536
- timeout=self.timeout
537
- )
538
- return self.mass_stats.upload_request(name, size, bucket, output, location, **kwargs)
539
-
540
- def start_mass_stats_job(self, task_id):
541
- if not self.mass_stats:
542
- from terrakio_core.mass_stats import MassStats
543
- if not self.url or not self.key:
544
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
545
- self.mass_stats = MassStats(
546
- base_url=self.url,
547
- api_key=self.key,
548
- verify=self.verify,
549
- timeout=self.timeout
550
- )
551
- return self.mass_stats.start_job(task_id)
552
-
553
- def get_mass_stats_task_id(self, name, stage, uid=None):
554
- if not self.mass_stats:
555
- from terrakio_core.mass_stats import MassStats
556
- if not self.url or not self.key:
557
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
558
- self.mass_stats = MassStats(
559
- base_url=self.url,
560
- api_key=self.key,
561
- verify=self.verify,
562
- timeout=self.timeout
563
- )
564
- return self.mass_stats.get_task_id(name, stage, uid)
565
-
566
- def track_mass_stats_job(self, ids=None):
567
- if not self.mass_stats:
568
- from terrakio_core.mass_stats import MassStats
569
- if not self.url or not self.key:
570
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
571
- self.mass_stats = MassStats(
572
- base_url=self.url,
573
- api_key=self.key,
574
- verify=self.verify,
575
- timeout=self.timeout
576
- )
577
- return self.mass_stats.track_job(ids)
578
-
579
- def get_mass_stats_history(self, limit=100):
580
- if not self.mass_stats:
581
- from terrakio_core.mass_stats import MassStats
582
- if not self.url or not self.key:
583
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
584
- self.mass_stats = MassStats(
585
- base_url=self.url,
586
- api_key=self.key,
587
- verify=self.verify,
588
- timeout=self.timeout
589
- )
590
- return self.mass_stats.get_history(limit)
591
-
592
- def start_mass_stats_post_processing(self, process_name, data_name, output, consumer_path, overwrite=False):
593
- if not self.mass_stats:
594
- from terrakio_core.mass_stats import MassStats
595
- if not self.url or not self.key:
596
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
597
- self.mass_stats = MassStats(
598
- base_url=self.url,
599
- api_key=self.key,
600
- verify=self.verify,
601
- timeout=self.timeout
602
- )
603
- return self.mass_stats.start_post_processing(process_name, data_name, output, consumer_path, overwrite)
604
-
605
- def download_mass_stats_results(self, id=None, force_loc=False, **kwargs):
606
- if not self.mass_stats:
607
- from terrakio_core.mass_stats import MassStats
608
- if not self.url or not self.key:
609
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
610
- self.mass_stats = MassStats(
611
- base_url=self.url,
612
- api_key=self.key,
613
- verify=self.verify,
614
- timeout=self.timeout
615
- )
616
- return self.mass_stats.download_results(id, force_loc, **kwargs)
617
-
618
- def cancel_mass_stats_job(self, id):
619
- if not self.mass_stats:
620
- from terrakio_core.mass_stats import MassStats
621
- if not self.url or not self.key:
622
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
623
- self.mass_stats = MassStats(
624
- base_url=self.url,
625
- api_key=self.key,
626
- verify=self.verify,
627
- timeout=self.timeout
628
- )
629
- return self.mass_stats.cancel_job(id)
630
-
631
- def cancel_all_mass_stats_jobs(self):
632
- if not self.mass_stats:
633
- from terrakio_core.mass_stats import MassStats
634
- if not self.url or not self.key:
635
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
636
- self.mass_stats = MassStats(
637
- base_url=self.url,
638
- api_key=self.key,
639
- verify=self.verify,
640
- timeout=self.timeout
641
- )
642
- return self.mass_stats.cancel_all_jobs()
643
-
644
- def _create_pyramids(self, name, levels, config):
645
- if not self.mass_stats:
646
- from terrakio_core.mass_stats import MassStats
647
- if not self.url or not self.key:
648
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
649
- self.mass_stats = MassStats(
650
- base_url=self.url,
651
- api_key=self.key,
652
- verify=self.verify,
653
- timeout=self.timeout
654
- )
655
- return self.mass_stats.create_pyramids(name, levels, config)
656
-
657
- def random_sample(self, name, **kwargs):
658
- if not self.mass_stats:
659
- from terrakio_core.mass_stats import MassStats
660
- if not self.url or not self.key:
661
- raise ConfigurationError("Mass Stats client not initialized. Make sure API URL and key are set.")
662
- self.mass_stats = MassStats(
663
- base_url=self.url,
664
- api_key=self.key,
665
- verify=self.verify,
666
- timeout=self.timeout
667
- )
668
- return self.mass_stats.random_sample(name, **kwargs)
669
-
670
- async def zonal_stats_async(self, gdb, expr, conc=20, inplace=False, output="csv"):
671
- """
672
- Compute zonal statistics for all geometries in a GeoDataFrame using asyncio for concurrency.
673
- """
674
- import asyncio
675
- import pandas as pd
676
- import geopandas as gpd
677
- from shapely.geometry import mapping
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
-