terrakio-core 0.4.97__py3-none-any.whl → 0.4.98.1b1__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.

@@ -1,27 +1,22 @@
1
- # Standard library imports
2
1
  import ast
3
2
  import json
4
3
  import textwrap
5
- import time
6
4
  from io import BytesIO
7
5
  from typing import Optional, Tuple
6
+
8
7
  import onnxruntime as ort
9
8
 
10
- # Internal imports
11
9
  from ..helper.decorators import require_api_key
12
10
 
13
- # Optional dependency flags
14
11
  TORCH_AVAILABLE = False
15
12
  SKL2ONNX_AVAILABLE = False
16
13
 
17
- # PyTorch imports
18
14
  try:
19
15
  import torch
20
16
  TORCH_AVAILABLE = True
21
17
  except ImportError:
22
18
  torch = None
23
19
 
24
- # Scikit-learn and ONNX conversion imports
25
20
  try:
26
21
  from sklearn.base import BaseEstimator
27
22
  from skl2onnx import convert_sklearn
@@ -36,124 +31,159 @@ class ModelManagement:
36
31
  def __init__(self, client):
37
32
  self._client = client
38
33
 
34
+ def _generate_test_request(self, expr: str, crs: str, resolution: float) -> dict:
35
+ """Generate test request using set polygon (Australia)"""
36
+ req = {
37
+ "feature": {
38
+ "type": "Feature",
39
+ "properties": {},
40
+ "geometry": {
41
+ "coordinates": [
42
+ [
43
+ [150.57846438251084, -29.535000759011766],
44
+ [150.57846438251084, -29.539538891448665],
45
+ [150.5845432181327, -29.539538891448665],
46
+ [150.5845432181327, -29.535000759011766],
47
+ [150.57846438251084, -29.535000759011766]
48
+ ]
49
+ ],
50
+ "type": "Polygon"
51
+ }
52
+ },
53
+ "in_crs": "epsg:4326",
54
+ "out_crs": crs,
55
+ "output": "csv",
56
+ "resolution": resolution,
57
+ "expr": expr,
58
+ "debug": True
59
+ }
60
+ return req
61
+
39
62
  @require_api_key
40
63
  async def generate_ai_dataset(
41
64
  self,
42
65
  name: str,
43
- aoi_geojson: str,
66
+ aoi: str,
44
67
  expression_x: str,
45
- filter_x_rate: float,
46
- filter_y_rate: float,
47
- samples: int,
48
- tile_size: int,
49
- expression_y: str = "skip",
50
68
  filter_x: str = "skip",
69
+ filter_x_rate: float = 1,
70
+ expression_y: str = "skip",
51
71
  filter_y: str = "skip",
52
- crs: str = "epsg:4326",
53
- res: float = 0.001,
54
- region: str = None,
55
- bucket: str = None,
72
+ filter_y_rate: float = 1,
73
+ samples: int = 1000,
74
+ tile_size: float = 256,
75
+ crs: str = "epsg:3577",
76
+ res: float = 10,
77
+ res_y: float = None,
78
+ skip_test: bool = False,
56
79
  start_year: int = None,
57
80
  end_year: int = None,
81
+ bucket: str = None,
82
+ server: str = None,
83
+ extra_filters: list[str] = None,
84
+ extra_filters_rate: list[float] = None,
85
+ extra_filters_res: list[float] = None
58
86
  ) -> dict:
59
87
  """
60
88
  Generate an AI dataset using specified parameters.
61
89
 
62
90
  Args:
63
- name (str): Name of the dataset to generate
64
- aoi_geojson (str): Path to GeoJSON file containing area of interest
65
- expression_x (str): Expression for X variable (e.g. "MSWX.air_temperature@(year=2021, month=1)")
66
- filter_x (str): Filter for X variable (e.g. "MSWX.air_temperature@(year=2021, month=1)")
67
- filter_x_rate (float): Filter rate for X variable (e.g. 0.5)
68
- expression_y (str): Expression for Y variable with {year} placeholder
69
- filter_y (str): Filter for Y variable (e.g. "MSWX.air_temperature@(year=2021, month=1)")
70
- filter_y_rate (float): Filter rate for Y variable (e.g. 0.5)
71
- samples (int): Number of samples to generate
72
- tile_size (int): Size of tiles in degrees
73
- crs (str, optional): Coordinate reference system. Defaults to "epsg:4326"
74
- res (float, optional): Resolution in degrees. Defaults to 0.001
75
- region (str, optional): Region code. Defaults to None
76
- bucket (str, optional): Bucket name. Defaults to None
77
- start_year (int, optional): Start year for data generation. Required if end_year provided
78
- end_year (int, optional): End year for data generation. Required if start_year provided
91
+ name (str): Name of the collection to create
92
+ aoi (str): Path to GeoJSON file containing area of interest
93
+ expression_x (str): Expression for X data (features)
94
+ filter_x (str): Filter expression for X data (default: "skip")
95
+ filter_x_rate (float): Filter rate for X data (default: 1)
96
+ expression_y (str): Expression for Y data (labels) (default: "skip")
97
+ filter_y (str): Filter expression for Y data (default: "skip")
98
+ filter_y_rate (float): Filter rate for Y data (default: 1)
99
+ samples (int): Number of samples to generate (default: 1000)
100
+ tile_size (float): Size of tiles in pixels (default: 256)
101
+ crs (str): Coordinate reference system (default: "epsg:3577")
102
+ res (float): Resolution for X data (default: 10)
103
+ res_y (float): Resolution for Y data, defaults to res if None
104
+ skip_test (bool): Skip expression validation test (default: False)
105
+ start_year (int): Start year for temporal filtering
106
+ end_year (int): End year for temporal filtering
107
+ bucket (str): Storage bucket name
108
+ server (str): Server to use for processing
109
+ extra_filters (list[str]): Additional filter expressions
110
+ extra_filters_rate (list[float]): Rates for additional filters
111
+ extra_filters_res (list[float]): Resolutions for additional filters
79
112
 
80
113
  Returns:
81
- dict: Response from the AI dataset generation API
114
+ dict: Response containing task_id and collection name
82
115
 
83
116
  Raises:
84
117
  APIError: If the API request fails
118
+ TypeError: If extra filters have mismatched rate and resolution lists
85
119
  """
86
- # Build config for expressions and filters
87
- config = {
88
- "expressions": [{"expr": expression_x, "res": res, "prefix": "x"}],
89
- "filters": []
90
- }
91
-
120
+ expressions = [{"expr": expression_x, "res": res, "prefix": "x"}]
121
+
122
+ res_y = res_y or res
123
+
92
124
  if expression_y != "skip":
93
- config["expressions"].append({"expr": expression_y, "res": res, "prefix": "y"})
94
-
125
+ expressions.append({"expr": expression_y, "res": res_y, "prefix": "y"})
126
+
127
+ filters = []
95
128
  if filter_x != "skip":
96
- config["filters"].append({"expr": filter_x, "res": res, "rate": filter_x_rate})
129
+ filters.append({"expr": filter_x, "res": res, "rate": filter_x_rate})
130
+
97
131
  if filter_y != "skip":
98
- config["filters"].append({"expr": filter_y, "res": res, "rate": filter_y_rate})
99
-
100
- # Replace year placeholders if start_year is provided
132
+ filters.append({"expr": filter_y, "res": res_y, "rate": filter_y_rate})
133
+
134
+ if extra_filters:
135
+ try:
136
+ extra_filters_combined = zip(extra_filters, extra_filters_res, extra_filters_rate, strict=True)
137
+ except TypeError:
138
+ raise TypeError("Extra filters must have matching rate and resolution.")
139
+
140
+ for expr, filter_res, rate in extra_filters_combined:
141
+ filters.append({"expr": expr, "res": filter_res, "rate": rate})
142
+
101
143
  if start_year is not None:
102
- expression_x = expression_x.replace("{year}", str(start_year))
103
- if expression_y != "skip":
104
- expression_y = expression_y.replace("{year}", str(start_year))
105
- if filter_x != "skip":
106
- filter_x = filter_x.replace("{year}", str(start_year))
107
- if filter_y != "skip":
108
- filter_y = filter_y.replace("{year}", str(start_year))
109
-
110
- # Load AOI GeoJSON
111
- with open(aoi_geojson, 'r') as f:
144
+ for expr_dict in expressions:
145
+ expr_dict["expr"] = expr_dict["expr"].replace("{year}", str(start_year))
146
+
147
+ for filter_dict in filters:
148
+ filter_dict["expr"] = filter_dict["expr"].replace("{year}", str(start_year))
149
+
150
+ # this is making request to the server that is being used when doing the initialization
151
+ if not skip_test:
152
+ for expr_dict in expressions:
153
+ test_request = self._generate_test_request(expr_dict["expr"], crs, -1)
154
+ await self._client._terrakio_request("POST", "geoquery", json=test_request)
155
+
156
+ for filter_dict in filters:
157
+ test_request = self._generate_test_request(filter_dict["expr"], crs, -1)
158
+ await self._client._terrakio_request("POST", "geoquery", json=test_request)
159
+
160
+ with open(aoi, 'r') as f:
112
161
  aoi_data = json.load(f)
113
-
114
- task_response = await self._client.mass_stats.random_sample(
115
- name=name,
116
- config=config,
162
+
163
+ await self._client.mass_stats.create_collection(
164
+ collection=name,
165
+ bucket=bucket,
166
+ collection_type="basic"
167
+ )
168
+
169
+ task_id_dict = await self._client.mass_stats.training_samples(
170
+ collection=name,
171
+ expressions=expressions,
172
+ filters=filters,
117
173
  aoi=aoi_data,
118
174
  samples=samples,
119
175
  year_range=[start_year, end_year],
120
176
  crs=crs,
121
177
  tile_size=tile_size,
122
178
  res=res,
123
- region=region,
124
- output="netcdf",
125
- server=self._client.url,
126
- bucket=bucket,
127
- overwrite=True
179
+ output="nc",
180
+ server=server
128
181
  )
129
- task_id = task_response["task_id"]
130
-
131
- while True:
132
- result = await self._client.mass_stats.track_job(ids=[task_id])
133
- status = result[task_id]['status']
134
- completed = result[task_id].get('completed', 0)
135
- total = result[task_id].get('total', 1)
136
-
137
- progress = completed / total if total > 0 else 0
138
- bar_length = 50
139
- filled_length = int(bar_length * progress)
140
- bar = '█' * filled_length + '░' * (bar_length - filled_length)
141
- percentage = progress * 100
142
-
143
- self._client.logger.info(f"Job status: {status} [{bar}] {percentage:.1f}% ({completed}/{total})")
144
-
145
- if status == "Completed":
146
- self._client.logger.info("Job completed successfully!")
147
- break
148
- elif status == "Error":
149
- self._client.logger.info("Job encountered an error")
150
- raise Exception(f"Job {task_id} encountered an error")
151
-
152
- time.sleep(5)
182
+
183
+ task_id = task_id_dict["task_id"]
184
+
185
+ await self._client.mass_stats.track_progress(task_id)
153
186
 
154
- task_id = await self._client.mass_stats.start_job(task_id)
155
- return task_id
156
-
157
187
  @require_api_key
158
188
  async def _get_url_for_upload_model_and_script(self, expression: str, model_name: str, script_name: str) -> str:
159
189
  """
@@ -252,7 +282,7 @@ class ModelManagement:
252
282
  return bucket_name
253
283
 
254
284
  @require_api_key
255
- async def upload_and_deploy_model(self, model, virtual_dataset_name: str, virtual_product_name: str, input_expression: str, dates_iso8601: list, input_shape: Tuple[int, ...] = None, processing_script_path: Optional[str] = None, model_type: Optional[str] = None):
285
+ async def upload_and_deploy_model(self, model, virtual_dataset_name: str, virtual_product_name: str, input_expression: str, dates_iso8601: list, input_shape: Tuple[int, ...] = None, processing_script_path: Optional[str] = None, model_type: Optional[str] = None, padding: int = 0):
256
286
  """
257
287
  Upload a model to the bucket and deploy it.
258
288
  Args:
@@ -264,6 +294,7 @@ class ModelManagement:
264
294
  input_shape: Shape of input data for ONNX conversion (required for PyTorch models)
265
295
  processing_script_path: Path to the processing script, if not provided, no processing will be done
266
296
  model_type: The type of the model we want to upload
297
+ padding: Padding value for the dataset (default: 0)
267
298
 
268
299
  Raises:
269
300
  APIError: If the API request fails
@@ -278,12 +309,11 @@ class ModelManagement:
278
309
  uid = user_info["uid"]
279
310
  await self._client.datasets.create_dataset(
280
311
  name=virtual_dataset_name,
281
- collection="terrakio-datasets",
282
312
  products=[virtual_product_name],
283
- path=f"gs://{bucket_name}/{uid}/virtual_datasets/{virtual_dataset_name}/inference_scripts",
313
+ path=f"gs://{bucket_name}/{uid}/models/{virtual_dataset_name}/inference_scripts",
284
314
  input=input_expression,
285
315
  dates_iso8601=dates_iso8601,
286
- padding=0
316
+ padding=padding
287
317
  )
288
318
 
289
319
  @require_api_key
@@ -1088,15 +1118,32 @@ class ModelManagement:
1088
1118
  raise ValueError(f"Failed to convert scikit-learn model to ONNX: {str(e)}")
1089
1119
 
1090
1120
  @require_api_key
1091
- def train_model(
1092
- self,
1093
- model_name: str,
1094
- training_dataset: str,
1095
- task_type: str,
1096
- model_category: str,
1097
- architecture: str,
1098
- region: str,
1099
- hyperparameters: dict = None
1121
+ async def train_model(
1122
+ self,
1123
+ model_name: str,
1124
+ task_type: str,
1125
+ model_category: str,
1126
+ architecture: str,
1127
+ hyperparameters: dict = None,
1128
+ aoi: str = None,
1129
+ expression_x: str = None,
1130
+ filter_x: str = "skip",
1131
+ filter_x_rate: float = 1,
1132
+ expression_y: str = "skip",
1133
+ filter_y: str = "skip",
1134
+ filter_y_rate: float = 1,
1135
+ samples: int = 1000,
1136
+ tile_size: float = 256,
1137
+ crs: str = "epsg:3577",
1138
+ res: float = 10,
1139
+ res_y: float = None,
1140
+ skip_test: bool = False,
1141
+ start_year: int = None,
1142
+ end_year: int = None,
1143
+ server: str = None,
1144
+ extra_filters: list[str] = None,
1145
+ extra_filters_rate: list[float] = None,
1146
+ extra_filters_res: list[float] = None
1100
1147
  ) -> dict:
1101
1148
  """
1102
1149
  Train a model using the external model training API.
@@ -1107,7 +1154,6 @@ class ModelManagement:
1107
1154
  task_type (str): The type of ML task (e.g., regression, classification).
1108
1155
  model_category (str): The category of model (e.g., random_forest).
1109
1156
  architecture (str): The model architecture.
1110
- region (str): The region identifier.
1111
1157
  hyperparameters (dict, optional): Additional hyperparameters for training.
1112
1158
 
1113
1159
  Returns:
@@ -1116,13 +1162,71 @@ class ModelManagement:
1116
1162
  Raises:
1117
1163
  APIError: If the API request fails
1118
1164
  """
1165
+ expressions = [{"expr": expression_x, "res": res, "prefix": "x"}]
1166
+
1167
+ res_y = res_y or res
1168
+
1169
+ if expression_y != "skip":
1170
+ expressions.append({"expr": expression_y, "res": res_y, "prefix": "y"})
1171
+
1172
+ filters = []
1173
+ if filter_x != "skip":
1174
+ filters.append({"expr": filter_x, "res": res, "rate": filter_x_rate})
1175
+
1176
+ if filter_y != "skip":
1177
+ filters.append({"expr": filter_y, "res": res_y, "rate": filter_y_rate})
1178
+
1179
+ if extra_filters:
1180
+ try:
1181
+ extra_filters_combined = zip(extra_filters, extra_filters_res, extra_filters_rate, strict=True)
1182
+ except TypeError:
1183
+ raise TypeError("Extra filters must have matching rate and resolution.")
1184
+
1185
+ for expr, filter_res, rate in extra_filters_combined:
1186
+ filters.append({"expr": expr, "res": filter_res, "rate": rate})
1187
+
1188
+ if start_year is not None:
1189
+ for expr_dict in expressions:
1190
+ expr_dict["expr"] = expr_dict["expr"].replace("{year}", str(start_year))
1191
+
1192
+ for filter_dict in filters:
1193
+ filter_dict["expr"] = filter_dict["expr"].replace("{year}", str(start_year))
1194
+
1195
+ if not skip_test:
1196
+ for expr_dict in expressions:
1197
+ test_request = self._generate_test_request(expr_dict["expr"], crs, -1)
1198
+ await self._client._terrakio_request("POST", "geoquery", json=test_request)
1199
+
1200
+ for filter_dict in filters:
1201
+ test_request = self._generate_test_request(filter_dict["expr"], crs, -1)
1202
+ await self._client._terrakio_request("POST", "geoquery", json=test_request)
1203
+
1204
+ with open(aoi, 'r') as f:
1205
+ aoi_data = json.load(f)
1206
+
1207
+ await self._client.mass_stats.create_collection(
1208
+ collection=model_name,
1209
+ bucket="terrakio-mass-requests",
1210
+ collection_type="basic"
1211
+ )
1212
+
1119
1213
  payload = {
1120
1214
  "model_name": model_name,
1121
- "training_dataset": training_dataset,
1122
1215
  "task_type": task_type,
1123
1216
  "model_category": model_category,
1124
1217
  "architecture": architecture,
1125
- "region": region,
1126
- "hyperparameters": hyperparameters
1218
+ "hyperparameters": hyperparameters,
1219
+ "expressions": expressions,
1220
+ "filters": filters,
1221
+ "aoi": aoi_data,
1222
+ "samples": samples,
1223
+ "year_range": [start_year, end_year],
1224
+ "crs": crs,
1225
+ "tile_size": tile_size,
1226
+ "res": res,
1227
+ "server": server
1127
1228
  }
1128
- return self._client._terrakio_request("POST", "/train_model", json=payload)
1229
+
1230
+ task_id_dict, _ = await self._client._terrakio_request("POST", "models/train", json=payload)
1231
+
1232
+ await self._client.mass_stats.track_progress(task_id_dict["task_id"])
@@ -1,12 +1,13 @@
1
1
  from typing import Dict, Any, List, Optional
2
2
  from ..helper.decorators import require_token, require_api_key, require_auth
3
+ from ..exceptions import UserNotFoundError, GetUserByIdError, GetUserByEmailError, ListUsersError, EditUserError, ResetQuotaError, DeleteUserError, GetUsersByRoleError, RoleDoNotExistError, ChangeRoleError
3
4
 
4
5
  class UserManagement:
5
6
  def __init__(self, client):
6
7
  self._client = client
7
8
 
8
9
  @require_api_key
9
- def get_user_by_id(self, id: str) -> Dict[str, Any]:
10
+ async def get_user_by_id(self, id: str) -> Dict[str, Any]:
10
11
  """
11
12
  Get user by ID.
12
13
 
@@ -17,12 +18,19 @@ class UserManagement:
17
18
  User information
18
19
 
19
20
  Raises:
20
- APIError: If the API request fails
21
+ GetUserByIdError: If the API request fails
22
+ UserNotFoundError: If the user is not found
21
23
  """
22
- return self._client._terrakio_request("GET", f"admin/users/{id}")
23
-
24
+ response, status = await self._client._terrakio_request("GET", f"admin/users/{id}")
25
+ if status != 200:
26
+ if status == 404:
27
+ raise UserNotFoundError(f"User {id} not found.", status_code = status)
28
+ raise GetUserByIdError(f"Get user by id failed with status {status}", status_code = status)
29
+ else:
30
+ return response
31
+
24
32
  @require_api_key
25
- def get_user_by_email(self, email: str) -> Dict[str, Any]:
33
+ async def get_user_by_email(self, email: str) -> Dict[str, Any]:
26
34
  """
27
35
  Get user by email.
28
36
 
@@ -33,12 +41,19 @@ class UserManagement:
33
41
  User information
34
42
 
35
43
  Raises:
36
- APIError: If the API request fails
44
+ GetUserByEmailError: If the API request fails
45
+ UserNotFoundError: If the user is not found
37
46
  """
38
- return self._client._terrakio_request("GET", f"admin/users/email/{email}")
47
+ response, status = await self._client._terrakio_request("GET", f"admin/users/email/{email}")
48
+ if status != 200:
49
+ if status == 404:
50
+ raise UserNotFoundError(f"User {email} not found.", status_code = status)
51
+ raise GetUserByEmailError(f"Get user by email failed with status {status}", status_code = status)
52
+ else:
53
+ return response
39
54
 
40
55
  @require_api_key
41
- def list_users(self, substring: Optional[str] = None, uid: bool = False) -> List[Dict[str, Any]]:
56
+ async def list_users(self, substring: Optional[str] = None, uid: bool = False) -> List[Dict[str, Any]]:
42
57
  """
43
58
  List users, optionally filtering by a substring.
44
59
 
@@ -50,15 +65,19 @@ class UserManagement:
50
65
  List of users
51
66
 
52
67
  Raises:
53
- APIError: If the API request fails
68
+ ListUsersError: If the API request fails
54
69
  """
55
70
  params = {"uid": str(uid).lower()}
56
71
  if substring:
57
72
  params['substring'] = substring
58
- return self._client._terrakio_request("GET", "admin/users", params=params)
73
+ response, status = await self._client._terrakio_request("GET", "admin/users", params=params)
74
+ if status != 200:
75
+ raise ListUsersError(f"List users failed with status {status}", status_code = status)
76
+ else:
77
+ return response
59
78
 
60
79
  @require_api_key
61
- def edit_user(
80
+ async def edit_user(
62
81
  self,
63
82
  uid: str,
64
83
  email: Optional[str] = None,
@@ -82,7 +101,7 @@ class UserManagement:
82
101
  Updated user information
83
102
 
84
103
  Raises:
85
- APIError: If the API request fails
104
+ EditUserError: If the API request fails
86
105
  """
87
106
  payload = {"uid": uid}
88
107
  payload_mapping = {
@@ -95,10 +114,14 @@ class UserManagement:
95
114
  for key, value in payload_mapping.items():
96
115
  if value is not None:
97
116
  payload[key] = value
98
- return self._client._terrakio_request("PATCH", "admin/users", json=payload)
117
+ response, status = await self._client._terrakio_request("PATCH", "admin/users", json=payload)
118
+ if status != 200:
119
+ raise EditUserError(f"Edit user failed with status {status}", status_code = status)
120
+ else:
121
+ return response
99
122
 
100
123
  @require_api_key
101
- def reset_quota(self, email: str, quota: Optional[int] = None) -> Dict[str, Any]:
124
+ async def reset_quota(self, email: str, quota: Optional[int] = None) -> Dict[str, Any]:
102
125
  """
103
126
  Reset the quota for a user by email.
104
127
 
@@ -106,16 +129,20 @@ class UserManagement:
106
129
  email: The user's email (required)
107
130
  quota: The new quota value (optional)
108
131
 
109
- Returns:
110
- API response as a dictionary
132
+ Raises:
133
+ ResetQuotaError: If the API request fails
111
134
  """
112
135
  payload = {"email": email}
113
136
  if quota is not None:
114
137
  payload["quota"] = quota
115
- return self._client._terrakio_request("PATCH", f"admin/users/reset_quota/{email}", json=payload)
138
+ response, status = await self._client._terrakio_request("PATCH", f"admin/users/reset_quota/{email}", json=payload)
139
+ if status != 200:
140
+ raise ResetQuotaError(f"Reset quota failed with status {status}", status_code = status)
141
+ else:
142
+ return response
116
143
 
117
144
  @require_api_key
118
- def delete_user(self, uid: str) -> Dict[str, Any]:
145
+ async def delete_user(self, uid: str) -> Dict[str, Any]:
119
146
  """
120
147
  Delete a user by UID.
121
148
 
@@ -123,9 +150,67 @@ class UserManagement:
123
150
  uid: The user's UID (required)
124
151
 
125
152
  Returns:
126
- API response as a dictionary
153
+ Deleted user information
154
+
155
+ Raises:
156
+ DeleteUserError: If the API request fails
157
+ """
158
+ response, status = await self._client._terrakio_request("DELETE", f"admin/users/{uid}")
159
+ if status != 200:
160
+ raise DeleteUserError(f"Delete user failed with status {status}", status_code = status)
161
+ else:
162
+ return response
163
+
164
+ @require_api_key
165
+ async def get_users_by_role(self, role: str) -> Dict[str, Any]:
166
+ """
167
+ Get users by role.
168
+
169
+ Args:
170
+ role: The user role to filter by (required)
171
+
172
+ Returns:
173
+ Users with the specified role
174
+
175
+ Raises:
176
+ GetUsersByRoleError: If the API request fails
177
+ """
178
+ response, status = await self._client._terrakio_request("GET", f"admin/users/role?role={role}")
179
+ if status != 200:
180
+ if status == 422:
181
+ raise RoleDoNotExistError(f"Role {role} does not exist", status_code = status)
182
+ raise GetUsersByRoleError(f"Get users by role failed with status {status}", status_code = status)
183
+ else:
184
+ return response
185
+
186
+ @require_api_key
187
+ async def change_role(self, uid: str, role: str, reset_quota: Optional[bool] = None, limit: Optional[int] = None) -> Dict[str, Any]:
188
+ """
189
+ Change user role.
190
+
191
+ Args:
192
+ uid: The user's UID to change role for (required)
193
+ role: Role to apply (required)
194
+ reset_quota: Reset user's quota to new role limit (optional)
195
+ limit: Quota limit if role is custom (optional)
196
+
197
+ Returns:
198
+ Response from the role change operation
127
199
 
128
200
  Raises:
129
- APIError: If the API request fails
201
+ ChangeRoleError: If the API request fails
130
202
  """
131
- return self._client._terrakio_request("DELETE", f"admin/users/{uid}")
203
+ payload = {"uid": uid, "role": role}
204
+ if reset_quota is not None:
205
+ payload["reset_quota"] = reset_quota
206
+ if limit is not None:
207
+ payload["limit"] = limit
208
+ response, status = await self._client._terrakio_request("PATCH", "admin/users/change_role", json=payload)
209
+ if status != 200:
210
+ if status == 404:
211
+ raise UserNotFoundError(f"User {uid} not found", status_code = status)
212
+ elif status == 422:
213
+ raise RoleDoNotExistError(f"Role {role} does not exist", status_code = status)
214
+ raise ChangeRoleError(f"Change role failed with status {status}", status_code = status)
215
+ else:
216
+ return response