terrakio-core 0.4.98__tar.gz → 0.4.98.1b3__tar.gz

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.

Files changed (28) hide show
  1. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/PKG-INFO +2 -1
  2. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/pyproject.toml +2 -1
  3. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/async_client.py +26 -169
  4. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/config.py +3 -44
  5. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/endpoints/auth.py +96 -47
  6. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/endpoints/dataset_management.py +120 -54
  7. terrakio_core-0.4.98.1b3/terrakio_core/endpoints/group_management.py +421 -0
  8. terrakio_core-0.4.98.1b3/terrakio_core/endpoints/mass_stats.py +835 -0
  9. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/endpoints/model_management.py +213 -109
  10. terrakio_core-0.4.98.1b3/terrakio_core/endpoints/user_management.py +216 -0
  11. terrakio_core-0.4.98.1b3/terrakio_core/exceptions.py +390 -0
  12. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/sync_client.py +9 -124
  13. terrakio_core-0.4.98/terrakio_core/endpoints/group_management.py +0 -228
  14. terrakio_core-0.4.98/terrakio_core/endpoints/mass_stats.py +0 -712
  15. terrakio_core-0.4.98/terrakio_core/endpoints/user_management.py +0 -131
  16. terrakio_core-0.4.98/terrakio_core/exceptions.py +0 -20
  17. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/.gitignore +0 -0
  18. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/README.md +0 -0
  19. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/__init__.py +0 -0
  20. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/accessors.py +0 -0
  21. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/client.py +0 -0
  22. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/convenience_functions/create_dataset_file.py +0 -0
  23. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/convenience_functions/geoquries.py +0 -0
  24. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/convenience_functions/zonal_stats.py +0 -0
  25. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/endpoints/space_management.py +0 -0
  26. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/helper/bounded_taskgroup.py +0 -0
  27. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/helper/decorators.py +0 -0
  28. {terrakio_core-0.4.98 → terrakio_core-0.4.98.1b3}/terrakio_core/helper/tiles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: terrakio-core
3
- Version: 0.4.98
3
+ Version: 0.4.98.1b3
4
4
  Summary: Core package for the terrakio-python-api
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: aiofiles>=24.1.0
@@ -14,6 +14,7 @@ Requires-Dist: onnxruntime>=1.22.1
14
14
  Requires-Dist: psutil>=7.0.0
15
15
  Requires-Dist: scipy>=1.16.1
16
16
  Requires-Dist: shapely>=2.1.1
17
+ Requires-Dist: typer>=0.19.2
17
18
  Requires-Dist: xarray>=2025.7.1
18
19
  Provides-Extra: ml
19
20
  Requires-Dist: scikit-learn>=1.7.1; extra == 'ml'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "terrakio-core"
3
- version = "0.4.98"
3
+ version = "0.4.98.1b3"
4
4
  description = "Core package for the terrakio-python-api"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -20,6 +20,7 @@ dependencies = [
20
20
  "shapely>=2.1.1",
21
21
  "xarray>=2025.7.1",
22
22
  "nest-asyncio>=1.6.0",
23
+ "typer>=0.19.2",
23
24
  ]
24
25
 
25
26
  [project.optional-dependencies]
@@ -9,7 +9,7 @@ from geopandas import GeoDataFrame
9
9
  from shapely.geometry.base import BaseGeometry as ShapelyGeometry
10
10
  from shapely.geometry import mapping
11
11
  from .client import BaseClient
12
- from .exceptions import APIError
12
+ from .exceptions import APIError, NetworkError
13
13
  from .endpoints.dataset_management import DatasetManagement
14
14
  from .endpoints.user_management import UserManagement
15
15
  from .endpoints.mass_stats import MassStats
@@ -36,13 +36,10 @@ class AsyncClient(BaseClient):
36
36
 
37
37
  async def _terrakio_request(self, method: str, endpoint: str, **kwargs):
38
38
  if self.session is None:
39
- # To this:
40
39
  headers = {
41
40
  'x-api-key': self.key,
42
41
  'Authorization': self.token
43
42
  }
44
-
45
- # Only add Content-Type if it's a JSON request
46
43
  if 'json' in kwargs:
47
44
  headers['Content-Type'] = 'application/json'
48
45
  clean_headers = {k: v for k, v in headers.items() if v is not None}
@@ -57,20 +54,17 @@ class AsyncClient(BaseClient):
57
54
  for attempt in range(self.retry + 1):
58
55
  try:
59
56
  async with session.request(method, url, **kwargs) as response:
57
+ content = await response.text()
58
+
60
59
  if not response.ok and self._should_retry(response.status, attempt):
61
60
  self.logger.info(f"Request failed (attempt {attempt+1}/{self.retry+1}): {response.status}. Retrying...")
62
61
  continue
63
- if not response.ok:
64
- error_msg = f"API request failed: {response.status} {response.reason}"
65
- try:
66
- error_data = await response.json()
67
- if "detail" in error_data:
68
- error_msg += f" - {error_data['detail']}"
69
- except:
70
- pass
71
- raise APIError(error_msg, status_code=response.status)
72
- return await self._parse_response(response)
73
-
62
+ if response.ok:
63
+ data = await self._parse_response(response)
64
+ return data, response.status
65
+ else:
66
+ error_data = await response.json()
67
+ return error_data, response.status
74
68
  except aiohttp.ClientError as e:
75
69
  last_exception = e
76
70
  if attempt < self.retry:
@@ -78,8 +72,8 @@ class AsyncClient(BaseClient):
78
72
  continue
79
73
  else:
80
74
  break
81
-
82
- raise APIError(f"Networking error, request failed after {self.retry+1} attempts: {last_exception}", status_code=None)
75
+
76
+ raise NetworkError(f"Network failure after {self.retry+1} attempts: {last_exception}")
83
77
 
84
78
  def _should_retry(self, status_code: int, attempt: int) -> bool:
85
79
  """Determine if the request should be retried based on status code."""
@@ -193,158 +187,21 @@ class AsyncClient(BaseClient):
193
187
 
194
188
  return result
195
189
 
196
- async def zonal_stats(
197
- self,
198
- gdf: GeoDataFrame,
199
- expr: str,
200
- conc: int = 20,
201
- in_crs: str = "epsg:4326",
202
- out_crs: str = "epsg:4326",
203
- resolution: int = -1,
204
- geom_fix: bool = False,
205
- mass_stats: bool = False,
206
- id_column: Optional[str] = None,
207
- ):
208
- """
209
- Compute zonal statistics for all geometries in a GeoDataFrame.
210
-
211
- Args:
212
- gdf (GeoDataFrame): GeoDataFrame containing geometries
213
- expr (str): Terrakio expression to evaluate, can include spatial aggregations
214
- conc (int): Number of concurrent requests to make
215
- in_crs (str): Input coordinate reference system
216
- out_crs (str): Output coordinate reference system
217
- resolution (int): Resolution parameter
218
- geom_fix (bool): Whether to fix the geometry (default False)
219
- mass_stats (bool): Whether to use mass stats for processing (default False)
220
- id_column (Optional[str]): Name of the ID column to use (default None)
221
-
222
- Returns:
223
- geopandas.GeoDataFrame: GeoDataFrame with added columns for results
224
-
225
- Raises:
226
- ValueError: If concurrency is too high or if data exceeds memory limit without streaming
227
- APIError: If the API request fails
228
- """
229
- # the sync client didn't pass the self here, so the client is now async
230
- return await _zonal_stats(
231
- client=self,
232
- gdf=gdf,
233
- expr=expr,
234
- conc=conc,
235
- in_crs=in_crs,
236
- out_crs=out_crs,
237
- resolution=resolution,
238
- geom_fix=geom_fix,
239
- mass_stats=mass_stats,
240
- id_column=id_column,
241
- )
242
-
243
- async def create_dataset_file(
244
- self,
245
- name: str,
246
- aoi: str,
247
- expression: str,
248
- output: str,
249
- in_crs: str = "epsg:4326",
250
- res: float = 0.0001,
251
- region: str = "aus",
252
- to_crs: str = "epsg:4326",
253
- overwrite: bool = True,
254
- skip_existing: bool = False,
255
- non_interactive: bool = True,
256
- poll_interval: int = 30,
257
- download_path: str = "/home/user/Downloads",
258
- mask = True,
259
- max_file_size_mb: int = 5120, # Default to 5GB
260
- tile_size: int = 1024,
261
- ) -> dict:
262
- """
263
- Create a dataset file using mass stats operations.
264
-
265
- Args:
266
- aoi (str): Area of interest
267
- expression (str): Terrakio expression to evaluate
268
- output (str): Output format
269
- in_crs (str): Input coordinate reference system (default "epsg:4326")
270
- res (float): Resolution (default 0.0001)
271
- region (str): Region (default "aus")
272
- to_crs (str): Target coordinate reference system (default "epsg:4326")
273
- overwrite (bool): Whether to overwrite existing files (default True)
274
- skip_existing (bool): Whether to skip existing files (default False)
275
- non_interactive (bool): Whether to run non-interactively (default True)
276
- poll_interval (int): Polling interval in seconds (default 30)
277
- download_path (str): Download path (default "/home/user/Downloads")
278
-
279
- Returns:
280
- dict: Dictionary containing generation_task_id and combine_task_id
281
-
282
- Raises:
283
- ConfigurationError: If mass stats client is not properly configured
284
- RuntimeError: If job fails
285
- """
286
- return await _create_dataset_file(
287
- client=self,
288
- aoi=aoi,
289
- expression=expression,
290
- output=output,
291
- in_crs=in_crs,
292
- res=res,
293
- region=region,
294
- to_crs=to_crs,
295
- overwrite=overwrite,
296
- skip_existing=skip_existing,
297
- non_interactive=non_interactive,
298
- poll_interval=poll_interval,
299
- download_path=download_path,
300
- name=name,
301
- mask=mask,
302
- max_file_size_mb=max_file_size_mb,
303
- tile_size=tile_size
304
- )
305
-
306
- async def geo_queries(
307
- self,
308
- queries: list[dict],
309
- conc: int = 20,
310
- ):
311
- """
312
- Execute multiple geo queries concurrently.
313
-
314
- Args:
315
- queries (list[dict]): List of dictionaries containing query parameters.
316
- Each query must have 'expr', 'feature', and 'in_crs' keys.
317
- conc (int): Number of concurrent requests to make (default 20, max 100)
318
-
319
- Returns:
320
- Union[float, geopandas.GeoDataFrame]:
321
- - float: Average of all results if results are integers
322
- - GeoDataFrame: GeoDataFrame with geometry and dataset columns if results are xarray datasets
323
-
324
- Raises:
325
- ValueError: If queries list is empty, concurrency is too high, or queries are malformed
326
- APIError: If the API request fails
327
-
328
- Example:
329
- queries = [
330
- {
331
- 'expr': 'WCF.wcf',
332
- 'feature': {'type': 'Feature', 'geometry': {...}, 'properties': {}},
333
- 'in_crs': 'epsg:4326'
334
- },
335
- {
336
- 'expr': 'NDVI.ndvi',
337
- 'feature': {'type': 'Feature', 'geometry': {...}, 'properties': {}},
338
- 'in_crs': 'epsg:4326'
339
- }
340
- ]
341
- result = await client.geo_queries(queries)
342
- """
343
- return await _request_geoquery_list(
344
- client=self,
345
- quries=queries, # Note: keeping original parameter name for compatibility
346
- conc=conc,
347
- )
190
+ async def zonal_stats(self, *args, **kwargs):
191
+ """Proxy to convenience zonal_stats with full argument passthrough."""
192
+ return await _zonal_stats(self, *args, **kwargs)
193
+
194
+ async def create_dataset_file(self, *args, **kwargs) -> dict:
195
+ """Proxy to convenience create_dataset_file with full argument passthrough."""
196
+ kwargs.setdefault('download_path', "/home/user/Downloads")
197
+ kwargs.setdefault('region', "aus")
198
+ return await _create_dataset_file(self, *args, **kwargs)
199
+
200
+ async def geo_queries(self, *args, **kwargs):
201
+ """Proxy to convenience request_geoquery_list with full argument passthrough."""
202
+ if 'queries' in kwargs:
203
+ kwargs['quries'] = kwargs.pop('queries')
204
+ return await _request_geoquery_list(self, *args, **kwargs)
348
205
 
349
206
  async def __aenter__(self):
350
207
  if self._session is None:
@@ -7,7 +7,7 @@ from .exceptions import ConfigurationError
7
7
 
8
8
  # Default configuration file locations
9
9
  DEFAULT_CONFIG_FILE = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
10
- DEFAULT_API_URL = "https://api.terrak.io"
10
+ DEFAULT_API_URL = "https://dev-au.terrak.io"
11
11
 
12
12
  def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Logger = None) -> Dict[str, Any]:
13
13
  """
@@ -27,14 +27,8 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Log
27
27
  file if one doesn't exist and returns appropriate status flags.
28
28
  """
29
29
  config_path = Path(os.path.expanduser(config_file))
30
- # the first circumstance is that the config file does not exist
31
- # that we need to login before using any of the functions
32
- # Check if config file exists
30
+
33
31
  if not config_path.exists():
34
- # Create an empty config file
35
- config_path.parent.mkdir(parents=True, exist_ok=True)
36
- with open(config_path, 'w') as f:
37
- json.dump({}, f)
38
32
  logger.info("No API key found. Please provide an API key to use this client.")
39
33
  return {
40
34
  'url': DEFAULT_API_URL,
@@ -45,12 +39,9 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Log
45
39
  }
46
40
 
47
41
  try:
48
- # Read the config file
49
42
  with open(config_path, 'r') as f:
50
43
  config_data = json.load(f)
51
44
 
52
- # Read the config file data
53
- # Check if config has an API key
54
45
  if not config_data or 'TERRAKIO_API_KEY' not in config_data or not config_data.get('TERRAKIO_API_KEY'):
55
46
  logger.info("No API key found. Please provide an API key to use this client.")
56
47
  return {
@@ -61,11 +52,8 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Log
61
52
  'token': config_data.get('PERSONAL_TOKEN')
62
53
  }
63
54
  logger.info(f"Currently logged in as: {config_data.get('EMAIL')}")
64
- # this meanb that we have already logged in to the tkio account
65
55
 
66
- # Convert the JSON config to our expected format
67
56
  config = {
68
- # Always use the default URL, not from config file
69
57
  'url': DEFAULT_API_URL,
70
58
  'key': config_data.get('TERRAKIO_API_KEY'),
71
59
  'is_logged_in': True,
@@ -84,33 +72,4 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Log
84
72
  'is_logged_in': False,
85
73
  'user_email': None,
86
74
  'token': None
87
- }
88
-
89
- def create_default_config(email: str, api_key: str, config_file: str = DEFAULT_CONFIG_FILE) -> None:
90
- """
91
- Create a default configuration file in JSON format.
92
-
93
- Args:
94
- email: User email
95
- api_key: Terrakio API key
96
- config_file: Path to configuration file
97
-
98
- Raises:
99
- ConfigurationError: If the configuration file can't be created
100
- """
101
- config_path = Path(os.path.expanduser(config_file))
102
-
103
- # Ensure directory exists
104
- config_path.parent.mkdir(parents=True, exist_ok=True)
105
-
106
- try:
107
- config_data = {
108
- "EMAIL": email,
109
- "TERRAKIO_API_KEY": api_key
110
- }
111
-
112
- with open(config_path, 'w') as f:
113
- json.dump(config_data, f, indent=2)
114
-
115
- except Exception as e:
116
- raise ConfigurationError(f"Failed to create configuration file: {e}")
75
+ }
@@ -1,8 +1,21 @@
1
- import os
2
1
  import json
3
- from typing import Dict, Any
4
- from ..exceptions import APIError
5
- from ..helper.decorators import require_token, require_api_key, require_auth
2
+ import os
3
+ from typing import Any, Dict
4
+
5
+ from ..exceptions import (
6
+ APIKeyError,
7
+ AuthenticationExpireError,
8
+ InvalidUsernamePasswordError,
9
+ LoginError,
10
+ QuotaError,
11
+ RefreshAPIKeyError,
12
+ ResetPasswordError,
13
+ SignupError,
14
+ UserInfoError,
15
+ InvalidEmailFormatError,
16
+ EmailAlreadyExistsError,
17
+ )
18
+ from ..helper.decorators import require_api_key, require_auth, require_token
6
19
 
7
20
  class AuthClient:
8
21
  def __init__(self, client):
@@ -20,22 +33,23 @@ class AuthClient:
20
33
  Dict containing the authentication token
21
34
 
22
35
  Raises:
23
- APIError: If the signup request fails
36
+ SignupError: If the signup request fails
24
37
  """
25
38
  payload = {
26
39
  "email": email,
27
40
  "password": password
28
41
  }
42
+ response, status = await self._client._terrakio_request("POST", "/users/signup", json=payload)
43
+ if status != 200:
44
+ if status == 422:
45
+ raise InvalidEmailFormatError(f"Invalid email format: {response}", status_code=status)
46
+ elif status == 409:
47
+ raise EmailAlreadyExistsError(f"Email already exists: {response}", status_code=status)
48
+ raise SignupError(f"Signup request failed: {response}", status_code=status)
49
+ else:
50
+ return response
29
51
 
30
-
31
- try:
32
- result = await self._client._terrakio_request("POST", "/users/signup", json=payload)
33
- except Exception as e:
34
- self._client.logger.info(f"Signup failed: {str(e)}")
35
- raise APIError(f"Signup request failed: {str(e)}")
36
-
37
-
38
- async def login(self, email: str, password: str) -> Dict[str, str]:
52
+ async def login(self, email: str, password: str) -> None:
39
53
  """
40
54
  Login a user with email and password.
41
55
 
@@ -44,7 +58,7 @@ class AuthClient:
44
58
  password: User's password
45
59
 
46
60
  Returns:
47
- Dict containing the authentication token
61
+ None
48
62
 
49
63
  Raises:
50
64
  APIError: If the login request fails
@@ -53,10 +67,14 @@ class AuthClient:
53
67
  "email": email,
54
68
  "password": password
55
69
  }
56
-
57
- try:
58
- result = await self._client._terrakio_request("POST", "/users/login", json=payload)
59
- token_response = result.get("token")
70
+ response, status = await self._client._terrakio_request("POST", "/users/login", json=payload)
71
+ if status != 200:
72
+ if status == 401:
73
+ raise InvalidUsernamePasswordError(f"Invalid username or password: {response}", status_code=status)
74
+ else:
75
+ raise LoginError(f"Login request failed: {response}", status_code=status)
76
+ else:
77
+ token_response = response.get("token")
60
78
 
61
79
  if token_response:
62
80
  self._client.token = token_response
@@ -72,11 +90,6 @@ class AuthClient:
72
90
  self._client.logger.info(f"Successfully authenticated as: {email}")
73
91
  self._client.logger.info(f"Using Terrakio API at: {self._client.url}")
74
92
 
75
- return {"token": token_response} if token_response else {"error": "Login failed"}
76
-
77
- except Exception as e:
78
- self._client.logger.info(f"Login failed: {str(e)}")
79
- raise APIError(f"Login request failed: {str(e)}")
80
93
 
81
94
  @require_token
82
95
  async def view_api_key(self) -> str:
@@ -87,11 +100,18 @@ class AuthClient:
87
100
  str: The API key
88
101
 
89
102
  Raises:
90
- APIError: If the API request fails
103
+ AuthenticationExpireError: If authentication expired
104
+ APIKeyError: If the API key request fails
91
105
  """
92
- result = await self._client._terrakio_request("GET", "/users/key")
93
- api_key = result.get("apiKey")
94
- return api_key
106
+ response, status = await self._client._terrakio_request("GET", "/users/key")
107
+ api_key = response.get("apiKey")
108
+ if status != 200:
109
+ if status == 400 and response.get("detail")["message"] == "Not authenticated":
110
+ raise AuthenticationExpireError(f"Authentication expired, please login again: {response}")
111
+ else:
112
+ raise APIKeyError(f"Error fetching API key: {response}", status_code=status)
113
+ else:
114
+ return api_key
95
115
 
96
116
  @require_api_key
97
117
  @require_token
@@ -103,17 +123,18 @@ class AuthClient:
103
123
  str: The new API key
104
124
 
105
125
  Raises:
106
- APIError: If the API request fails
126
+ RefreshAPIKeyError: If the API key refresh request fails
107
127
  """
108
- result = await self._client._terrakio_request("POST", "/users/refresh_key")
109
- self._client.key = result.get("apiKey")
110
-
111
- self._update_config_key()
112
-
113
- return self._client.key
128
+ response, status = await self._client._terrakio_request("POST", "/users/refresh_key")
129
+ self._client.key = response.get("apiKey")
130
+ if status != 200:
131
+ raise RefreshAPIKeyError(f"Error refreshing API key: {response}", status_code=status)
132
+ else:
133
+ self._update_config_key()
134
+ return self._client.key
114
135
 
115
136
  @require_api_key
116
- def get_user_info(self) -> Dict[str, Any]:
137
+ async def get_user_info(self) -> Dict[str, Any]:
117
138
  """
118
139
  Get information about the authenticated user.
119
140
 
@@ -121,9 +142,45 @@ class AuthClient:
121
142
  Dict[str, Any]: User information
122
143
 
123
144
  Raises:
124
- APIError: If the API request fails
145
+ AuthenticationExpireError: If authentication expired
146
+ UserInfoError: If the user info request fails
125
147
  """
126
- return self._client._terrakio_request("GET", "/users/info")
148
+ response, status = await self._client._terrakio_request("GET", "/users/info")
149
+ if status != 200:
150
+ if status == 400 and response.get("detail")["message"] == "Not authenticated":
151
+ raise AuthenticationExpireError(f"Authentication expired, please login again: {response}", status_code=status)
152
+ else:
153
+ raise UserInfoError(f"Error fetching user info: {response}", status_code=status)
154
+ else:
155
+ return response
156
+
157
+ @require_api_key
158
+ async def reset_password(self, email : str) -> Dict[str, Any]:
159
+ """
160
+ Reset the password for a user by email.
161
+ """
162
+ response, status = await self._client._terrakio_request("GET", f"/users/reset-password?email={email}")
163
+ if status != 200:
164
+ raise ResetPasswordError(f"Error resetting password: {response}", status_code=status)
165
+ else:
166
+ return response['message']
167
+
168
+ @require_api_key
169
+ async def get_user_quota(self):
170
+ """
171
+ Get the user's quota.
172
+
173
+ Returns:
174
+ Dict: User's quota
175
+
176
+ Raises:
177
+ QuotaError: If the quota request fails
178
+ """
179
+ response, status = await self._client._terrakio_request("GET", "/users/quota")
180
+ if status != 200:
181
+ raise QuotaError(f"Error fetching quota: {response}", status_code = status)
182
+ else:
183
+ return response
127
184
 
128
185
  def _save_config(self, email: str, token: str):
129
186
  """
@@ -145,7 +202,6 @@ class AuthClient:
145
202
  config["TERRAKIO_API_KEY"] = self._client.key
146
203
  config["PERSONAL_TOKEN"] = token
147
204
  os.makedirs(os.path.dirname(config_path), exist_ok=True)
148
-
149
205
  with open(config_path, 'w') as f:
150
206
  json.dump(config, f, indent=4)
151
207
 
@@ -177,11 +233,4 @@ class AuthClient:
177
233
  self._client.logger.info(f"API key updated in {config_path}")
178
234
 
179
235
  except Exception as e:
180
- self._client.logger.info(f"Warning: Failed to update config file: {e}")
181
-
182
-
183
- # we have four different circumstances:
184
- # same expression, different region
185
- # same expression, same region
186
- # different expression, same region
187
- # different expression, different region
236
+ self._client.logger.info(f"Warning: Failed to update config file: {e}")