terrakio-core 0.1.9__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.

File without changes
terrakio_core/auth.py ADDED
@@ -0,0 +1,237 @@
1
+ import requests
2
+ from typing import Optional, Dict, Any
3
+ from .exceptions import APIError, ConfigurationError
4
+
5
+ class AuthClient:
6
+ def __init__(self, base_url: str = "https://dev-au.terrak.io",
7
+ verify: bool = True, timeout: int = 60):
8
+ """
9
+ Initialize the Authentication Client for Terrakio API.
10
+
11
+ Args:
12
+ base_url: Authentication API base URL
13
+ verify: Verify SSL certificates
14
+ timeout: Request timeout in seconds
15
+ """
16
+ self.base_url = base_url.rstrip('/')
17
+ self.verify = verify
18
+ self.timeout = timeout
19
+ self.session = requests.Session()
20
+ self.session.headers.update({
21
+ 'Content-Type': 'application/json'
22
+ })
23
+ self.token = None
24
+ self.api_key = None
25
+
26
+ def signup(self, email: str, password: str) -> Dict[str, Any]:
27
+ """
28
+ Register a new user account.
29
+
30
+ Args:
31
+ email: User email address
32
+ password: User password
33
+
34
+ Returns:
35
+ API response data
36
+
37
+ Raises:
38
+ APIError: If signup fails
39
+ """
40
+ endpoint = f"{self.base_url}/users/signup"
41
+
42
+ payload = {
43
+ "email": email,
44
+ "password": password
45
+ }
46
+
47
+ try:
48
+ response = self.session.post(
49
+ endpoint,
50
+ json=payload,
51
+ verify=self.verify,
52
+ timeout=self.timeout
53
+ )
54
+
55
+ if not response.ok:
56
+ error_msg = f"Signup failed: {response.status_code} {response.reason}"
57
+ try:
58
+ error_data = response.json()
59
+ if "detail" in error_data:
60
+ error_msg += f" - {error_data['detail']}"
61
+ except:
62
+ pass
63
+ raise APIError(error_msg)
64
+
65
+ return response.json()
66
+ except requests.RequestException as e:
67
+ raise APIError(f"Signup request failed: {str(e)}")
68
+
69
+ def login(self, email: str, password: str) -> str:
70
+ """
71
+ Log in and obtain authentication token.
72
+
73
+ Args:
74
+ email: User email address
75
+ password: User password
76
+
77
+ Returns:
78
+ Authentication token
79
+
80
+ Raises:
81
+ APIError: If login fails
82
+ """
83
+ endpoint = f"{self.base_url}/users/login"
84
+
85
+ payload = {
86
+ "email": email,
87
+ "password": password
88
+ }
89
+
90
+ try:
91
+ response = self.session.post(
92
+ endpoint,
93
+ json=payload,
94
+ verify=self.verify,
95
+ timeout=self.timeout
96
+ )
97
+
98
+ if not response.ok:
99
+ error_msg = f"Login failed: {response.status_code} {response.reason}"
100
+ try:
101
+ error_data = response.json()
102
+ if "detail" in error_data:
103
+ error_msg += f" - {error_data['detail']}"
104
+ except:
105
+ pass
106
+ raise APIError(error_msg)
107
+
108
+ result = response.json()
109
+ self.token = result.get("token")
110
+
111
+ # Update session with authorization header
112
+ if self.token:
113
+ self.session.headers.update({
114
+ "Authorization": self.token
115
+ })
116
+
117
+ return self.token
118
+ except requests.RequestException as e:
119
+ raise APIError(f"Login request failed: {str(e)}")
120
+
121
+ def refresh_api_key(self) -> str:
122
+ """
123
+ Generate or refresh API key.
124
+
125
+ Returns:
126
+ API key
127
+
128
+ Raises:
129
+ ConfigurationError: If not authenticated
130
+ APIError: If refresh fails
131
+ """
132
+ if not self.token:
133
+ raise ConfigurationError("Not authenticated. Call login() first.")
134
+
135
+ endpoint = f"{self.base_url}/users/refresh_key"
136
+
137
+ try:
138
+ # Use session with updated headers from login
139
+ response = self.session.post(
140
+ endpoint,
141
+ verify=self.verify,
142
+ timeout=self.timeout
143
+ )
144
+
145
+ if not response.ok:
146
+ error_msg = f"API key generation failed: {response.status_code} {response.reason}"
147
+ try:
148
+ error_data = response.json()
149
+ if "detail" in error_data:
150
+ error_msg += f" - {error_data['detail']}"
151
+ except:
152
+ pass
153
+ raise APIError(error_msg)
154
+
155
+ result = response.json()
156
+ self.api_key = result.get("apiKey")
157
+ return self.api_key
158
+ except requests.RequestException as e:
159
+ raise APIError(f"API key refresh request failed: {str(e)}")
160
+
161
+ def view_api_key(self) -> str:
162
+ """
163
+ Retrieve current API key.
164
+
165
+ Returns:
166
+ API key
167
+
168
+ Raises:
169
+ ConfigurationError: If not authenticated
170
+ APIError: If retrieval fails
171
+ """
172
+ if not self.token:
173
+ raise ConfigurationError("Not authenticated. Call login() first.")
174
+
175
+ endpoint = f"{self.base_url}/users/key"
176
+
177
+ try:
178
+ # Use session with updated headers from login
179
+ response = self.session.get(
180
+ endpoint,
181
+ verify=self.verify,
182
+ timeout=self.timeout
183
+ )
184
+
185
+ if not response.ok:
186
+ error_msg = f"Failed to retrieve API key: {response.status_code} {response.reason}"
187
+ try:
188
+ error_data = response.json()
189
+ if "detail" in error_data:
190
+ error_msg += f" - {error_data['detail']}"
191
+ except:
192
+ pass
193
+ raise APIError(error_msg)
194
+
195
+ result = response.json()
196
+ self.api_key = result.get("apiKey")
197
+ return self.api_key
198
+ except requests.RequestException as e:
199
+ raise APIError(f"API key retrieval request failed: {str(e)}")
200
+
201
+ def get_user_info(self) -> Dict[str, Any]:
202
+ """
203
+ Retrieve the current user's information.
204
+
205
+ Returns:
206
+ User information data
207
+
208
+ Raises:
209
+ ConfigurationError: If not authenticated
210
+ APIError: If retrieval fails
211
+ """
212
+ if not self.token:
213
+ raise ConfigurationError("Not authenticated. Call login() first.")
214
+
215
+ endpoint = f"{self.base_url}/users/info"
216
+
217
+ try:
218
+ # Use session with updated headers from login
219
+ response = self.session.get(
220
+ endpoint,
221
+ verify=self.verify,
222
+ timeout=self.timeout
223
+ )
224
+
225
+ if not response.ok:
226
+ error_msg = f"Failed to retrieve user info: {response.status_code} {response.reason}"
227
+ try:
228
+ error_data = response.json()
229
+ if "detail" in error_data:
230
+ error_msg += f" - {error_data['detail']}"
231
+ except:
232
+ pass
233
+ raise APIError(error_msg)
234
+
235
+ return response.json()
236
+ except requests.RequestException as e:
237
+ raise APIError(f"User info retrieval request failed: {str(e)}")
@@ -0,0 +1,380 @@
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
+ from shapely.geometry import shape
7
+ from shapely.geometry.base import BaseGeometry as ShapelyGeometry
8
+ from .exceptions import APIError, ConfigurationError
9
+
10
+ class BaseClient:
11
+ def __init__(self, url: Optional[str] = None, key: Optional[str] = None,
12
+ auth_url: Optional[str] = "https://dev-au.terrak.io",
13
+ quiet: bool = False, config_file: Optional[str] = None,
14
+ verify: bool = True, timeout: int = 60):
15
+ self.quiet = quiet
16
+ self.verify = verify
17
+ self.timeout = timeout
18
+ self.auth_client = None
19
+ if auth_url:
20
+ from terrakio_core.auth import AuthClient
21
+ self.auth_client = AuthClient(
22
+ base_url=auth_url,
23
+ verify=verify,
24
+ timeout=timeout
25
+ )
26
+ self.url = url
27
+ self.key = key
28
+ if self.url is None or self.key is None:
29
+ from terrakio_core.config import read_config_file, DEFAULT_CONFIG_FILE
30
+ if config_file is None:
31
+ config_file = DEFAULT_CONFIG_FILE
32
+ try:
33
+ config = read_config_file(config_file)
34
+ if self.url is None:
35
+ self.url = config.get('url')
36
+ if self.key is None:
37
+ self.key = config.get('key')
38
+ except Exception as e:
39
+ raise ConfigurationError(
40
+ f"Failed to read configuration: {e}\n\n"
41
+ "To fix this issue:\n"
42
+ "1. Create a file at ~/.terrakioapirc with:\n"
43
+ "url: https://api.terrak.io\n"
44
+ "key: your-api-key\n\n"
45
+ "OR\n\n"
46
+ "2. Initialize the client with explicit parameters:\n"
47
+ "client = terrakio_api.Client(\n"
48
+ " url='https://api.terrak.io',\n"
49
+ " key='your-api-key'\n"
50
+ ")"
51
+ )
52
+ if not self.url:
53
+ raise ConfigurationError("Missing API URL in configuration")
54
+ if not self.key:
55
+ raise ConfigurationError("Missing API key in configuration")
56
+ self.url = self.url.rstrip('/')
57
+ if not self.quiet:
58
+ print(f"Using Terrakio API at: {self.url}")
59
+ self.session = requests.Session()
60
+ self.session.headers.update({
61
+ 'Content-Type': 'application/json',
62
+ 'x-api-key': self.key
63
+ })
64
+ self.user_management = None
65
+ self.dataset_management = None
66
+
67
+ def validate_feature(self, feature: Dict[str, Any]) -> None:
68
+ if hasattr(feature, 'is_valid'):
69
+ from shapely.geometry import mapping
70
+ feature = {
71
+ "type": "Feature",
72
+ "geometry": mapping(feature),
73
+ "properties": {}
74
+ }
75
+ if not isinstance(feature, dict):
76
+ raise ValueError("Feature must be a dictionary or a Shapely geometry")
77
+ if feature.get("type") != "Feature":
78
+ raise ValueError("GeoJSON object must be of type 'Feature'")
79
+ if "geometry" not in feature:
80
+ raise ValueError("Feature must contain a 'geometry' field")
81
+ if "properties" not in feature:
82
+ raise ValueError("Feature must contain a 'properties' field")
83
+ try:
84
+ geometry = shape(feature["geometry"])
85
+ except Exception as e:
86
+ raise ValueError(f"Invalid geometry format: {str(e)}")
87
+ if not geometry.is_valid:
88
+ raise ValueError(f"Invalid geometry: {geometry.is_valid_reason}")
89
+ geom_type = feature["geometry"]["type"]
90
+ if geom_type == "Point":
91
+ if len(feature["geometry"]["coordinates"]) != 2:
92
+ raise ValueError("Point must have exactly 2 coordinates")
93
+ elif geom_type == "Polygon":
94
+ if not geometry.is_simple:
95
+ raise ValueError("Polygon must be simple (not self-intersecting)")
96
+ if geometry.area == 0:
97
+ raise ValueError("Polygon must have non-zero area")
98
+ coords = feature["geometry"]["coordinates"][0]
99
+ if coords[0] != coords[-1]:
100
+ raise ValueError("Polygon must be closed (first and last points must match)")
101
+
102
+ def signup(self, email: str, password: str) -> Dict[str, Any]:
103
+ if not self.auth_client:
104
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
105
+ return self.auth_client.signup(email, password)
106
+
107
+ def login(self, email: str, password: str) -> str:
108
+ if not self.auth_client:
109
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
110
+ token = self.auth_client.login(email, password)
111
+ if not self.quiet:
112
+ print(f"Successfully authenticated as: {email}")
113
+ return token
114
+
115
+ def refresh_api_key(self) -> str:
116
+ if not self.auth_client:
117
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
118
+ if not self.auth_client.token:
119
+ raise ConfigurationError("Not authenticated. Call login() first.")
120
+ self.key = self.auth_client.refresh_api_key()
121
+ self.session.headers.update({'x-api-key': self.key})
122
+ import os
123
+ config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
124
+ try:
125
+ config = {"EMAIL": "", "TERRAKIO_API_KEY": ""}
126
+ if os.path.exists(config_path):
127
+ with open(config_path, 'r') as f:
128
+ config = json.load(f)
129
+ config["TERRAKIO_API_KEY"] = self.key
130
+ os.makedirs(os.path.dirname(config_path), exist_ok=True)
131
+ with open(config_path, 'w') as f:
132
+ json.dump(config, f, indent=4)
133
+ if not self.quiet:
134
+ print(f"API key generated successfully and updated in {config_path}")
135
+ except Exception as e:
136
+ if not self.quiet:
137
+ print(f"Warning: Failed to update config file: {e}")
138
+ return self.key
139
+
140
+ def view_api_key(self) -> str:
141
+ if not self.auth_client:
142
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
143
+ if not self.auth_client.token:
144
+ raise ConfigurationError("Not authenticated. Call login() first.")
145
+ self.key = self.auth_client.view_api_key()
146
+ self.session.headers.update({'x-api-key': self.key})
147
+ return self.key
148
+
149
+ def get_user_info(self) -> Dict[str, Any]:
150
+ if not self.auth_client:
151
+ raise ConfigurationError("Authentication client not initialized. Please provide auth_url during client initialization.")
152
+ if not self.auth_client.token:
153
+ raise ConfigurationError("Not authenticated. Call login() first.")
154
+ return self.auth_client.get_user_info()
155
+
156
+ def wcs(self, expr: str, feature: Union[Dict[str, Any], ShapelyGeometry], in_crs: str = "epsg:4326",
157
+ out_crs: str = "epsg:4326", output: str = "csv", resolution: int = -1,
158
+ **kwargs):
159
+ if hasattr(feature, 'is_valid'):
160
+ from shapely.geometry import mapping
161
+ feature = {
162
+ "type": "Feature",
163
+ "geometry": mapping(feature),
164
+ "properties": {}
165
+ }
166
+ self.validate_feature(feature)
167
+ payload = {
168
+ "feature": feature,
169
+ "in_crs": in_crs,
170
+ "out_crs": out_crs,
171
+ "output": output,
172
+ "resolution": resolution,
173
+ "expr": expr,
174
+ **kwargs
175
+ }
176
+ if not self.quiet:
177
+ print(f"Requesting data with expression: {expr}")
178
+ request_url = f"{self.url}/wcs"
179
+ try:
180
+ response = self.session.post(request_url, json=payload, timeout=self.timeout, verify=self.verify)
181
+ if not response.ok:
182
+ error_msg = f"API request failed: {response.status_code} {response.reason}"
183
+ try:
184
+ error_data = response.json()
185
+ if "detail" in error_data:
186
+ error_msg += f" - {error_data['detail']}"
187
+ except:
188
+ pass
189
+ raise APIError(error_msg)
190
+ if output.lower() == "csv":
191
+ import pandas as pd
192
+ return pd.read_csv(BytesIO(response.content))
193
+ elif output.lower() == "netcdf":
194
+ return xr.open_dataset(BytesIO(response.content))
195
+ else:
196
+ try:
197
+ return xr.open_dataset(BytesIO(response.content))
198
+ except ValueError:
199
+ import pandas as pd
200
+ try:
201
+ return pd.read_csv(BytesIO(response.content))
202
+ except:
203
+ return response.content
204
+ except requests.RequestException as e:
205
+ raise APIError(f"Request failed: {str(e)}")
206
+
207
+ # Admin/protected methods
208
+ def _get_user_by_id(self, user_id: str):
209
+ if not self.user_management:
210
+ from terrakio_core.user_management import UserManagement
211
+ if not self.url or not self.key:
212
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
213
+ self.user_management = UserManagement(
214
+ api_url=self.url,
215
+ api_key=self.key,
216
+ verify=self.verify,
217
+ timeout=self.timeout
218
+ )
219
+ return self.user_management.get_user_by_id(user_id)
220
+
221
+ def _get_user_by_email(self, email: str):
222
+ if not self.user_management:
223
+ from terrakio_core.user_management import UserManagement
224
+ if not self.url or not self.key:
225
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
226
+ self.user_management = UserManagement(
227
+ api_url=self.url,
228
+ api_key=self.key,
229
+ verify=self.verify,
230
+ timeout=self.timeout
231
+ )
232
+ return self.user_management.get_user_by_email(email)
233
+
234
+ def _list_users(self, substring: str = None, uid: bool = False):
235
+ if not self.user_management:
236
+ from terrakio_core.user_management import UserManagement
237
+ if not self.url or not self.key:
238
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
239
+ self.user_management = UserManagement(
240
+ api_url=self.url,
241
+ api_key=self.key,
242
+ verify=self.verify,
243
+ timeout=self.timeout
244
+ )
245
+ return self.user_management.list_users(substring=substring, uid=uid)
246
+
247
+ 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):
248
+ if not self.user_management:
249
+ from terrakio_core.user_management import UserManagement
250
+ if not self.url or not self.key:
251
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
252
+ self.user_management = UserManagement(
253
+ api_url=self.url,
254
+ api_key=self.key,
255
+ verify=self.verify,
256
+ timeout=self.timeout
257
+ )
258
+ return self.user_management.edit_user(
259
+ user_id=user_id,
260
+ uid=uid,
261
+ email=email,
262
+ role=role,
263
+ apiKey=apiKey,
264
+ groups=groups,
265
+ quota=quota
266
+ )
267
+
268
+ def _reset_quota(self, email: str, quota: int = None):
269
+ if not self.user_management:
270
+ from terrakio_core.user_management import UserManagement
271
+ if not self.url or not self.key:
272
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
273
+ self.user_management = UserManagement(
274
+ api_url=self.url,
275
+ api_key=self.key,
276
+ verify=self.verify,
277
+ timeout=self.timeout
278
+ )
279
+ return self.user_management.reset_quota(email=email, quota=quota)
280
+
281
+ def _delete_user(self, uid: str):
282
+ if not self.user_management:
283
+ from terrakio_core.user_management import UserManagement
284
+ if not self.url or not self.key:
285
+ raise ConfigurationError("User management client not initialized. Make sure API URL and key are set.")
286
+ self.user_management = UserManagement(
287
+ api_url=self.url,
288
+ api_key=self.key,
289
+ verify=self.verify,
290
+ timeout=self.timeout
291
+ )
292
+ return self.user_management.delete_user(uid=uid)
293
+
294
+ # Dataset management protected methods
295
+ def _get_dataset(self, name: str, collection: str = "terrakio-datasets"):
296
+ if not self.dataset_management:
297
+ from terrakio_core.dataset_management import DatasetManagement
298
+ if not self.url or not self.key:
299
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
300
+ self.dataset_management = DatasetManagement(
301
+ api_url=self.url,
302
+ api_key=self.key,
303
+ verify=self.verify,
304
+ timeout=self.timeout
305
+ )
306
+ return self.dataset_management.get_dataset(name=name, collection=collection)
307
+
308
+ def _list_datasets(self, substring: str = None, collection: str = "terrakio-datasets"):
309
+ if not self.dataset_management:
310
+ from terrakio_core.dataset_management import DatasetManagement
311
+ if not self.url or not self.key:
312
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
313
+ self.dataset_management = DatasetManagement(
314
+ api_url=self.url,
315
+ api_key=self.key,
316
+ verify=self.verify,
317
+ timeout=self.timeout
318
+ )
319
+ return self.dataset_management.list_datasets(substring=substring, collection=collection)
320
+
321
+ def _create_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
322
+ if not self.dataset_management:
323
+ from terrakio_core.dataset_management import DatasetManagement
324
+ if not self.url or not self.key:
325
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
326
+ self.dataset_management = DatasetManagement(
327
+ api_url=self.url,
328
+ api_key=self.key,
329
+ verify=self.verify,
330
+ timeout=self.timeout
331
+ )
332
+ return self.dataset_management.create_dataset(name=name, collection=collection, **kwargs)
333
+
334
+ def _update_dataset(self, name: str, append: bool = True, collection: str = "terrakio-datasets", **kwargs):
335
+ if not self.dataset_management:
336
+ from terrakio_core.dataset_management import DatasetManagement
337
+ if not self.url or not self.key:
338
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
339
+ self.dataset_management = DatasetManagement(
340
+ api_url=self.url,
341
+ api_key=self.key,
342
+ verify=self.verify,
343
+ timeout=self.timeout
344
+ )
345
+ return self.dataset_management.update_dataset(name=name, append=append, collection=collection, **kwargs)
346
+
347
+ def _overwrite_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs):
348
+ if not self.dataset_management:
349
+ from terrakio_core.dataset_management import DatasetManagement
350
+ if not self.url or not self.key:
351
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
352
+ self.dataset_management = DatasetManagement(
353
+ api_url=self.url,
354
+ api_key=self.key,
355
+ verify=self.verify,
356
+ timeout=self.timeout
357
+ )
358
+ return self.dataset_management.overwrite_dataset(name=name, collection=collection, **kwargs)
359
+
360
+ def _delete_dataset(self, name: str, collection: str = "terrakio-datasets"):
361
+ if not self.dataset_management:
362
+ from terrakio_core.dataset_management import DatasetManagement
363
+ if not self.url or not self.key:
364
+ raise ConfigurationError("Dataset management client not initialized. Make sure API URL and key are set.")
365
+ self.dataset_management = DatasetManagement(
366
+ api_url=self.url,
367
+ api_key=self.key,
368
+ verify=self.verify,
369
+ timeout=self.timeout
370
+ )
371
+ return self.dataset_management.delete_dataset(name=name, collection=collection)
372
+
373
+ def close(self):
374
+ self.session.close()
375
+ if self.auth_client:
376
+ self.auth_client.session.close()
377
+ def __enter__(self):
378
+ return self
379
+ def __exit__(self, exc_type, exc_val, exc_tb):
380
+ self.close()
@@ -0,0 +1,81 @@
1
+ import os
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Dict, Any, Optional
5
+
6
+ from .exceptions import ConfigurationError
7
+
8
+ # Default configuration file locations
9
+ DEFAULT_CONFIG_FILE = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
10
+ DEFAULT_API_URL = "https://api.terrak.io"
11
+
12
+ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE) -> Dict[str, Any]:
13
+ """
14
+ Read and parse the configuration file.
15
+
16
+ Args:
17
+ config_file: Path to the configuration file
18
+
19
+ Returns:
20
+ Dict[str, Any]: Configuration parameters
21
+
22
+ Raises:
23
+ ConfigurationError: If the configuration file can't be read or parsed
24
+ """
25
+ config_path = Path(os.path.expanduser(config_file))
26
+
27
+ if not config_path.exists():
28
+ raise ConfigurationError(
29
+ f"Configuration file not found: {config_file}\n"
30
+ f"Please create a file at {config_file} with the following format:\n"
31
+ '{\n "EMAIL": "your-email@example.com",\n "TERRAKIO_API_KEY": "your-api-key-here"\n}'
32
+ )
33
+
34
+ try:
35
+ with open(config_path, 'r') as f:
36
+ config_data = json.load(f)
37
+
38
+ # Convert the JSON config to our expected format
39
+ config = {
40
+ # Allow config to override default URL if provided
41
+ 'url': config_data.get('TERRAKIO_API_URL', DEFAULT_API_URL),
42
+ 'key': config_data.get('TERRAKIO_API_KEY')
43
+ }
44
+ return config
45
+
46
+ except Exception as e:
47
+ raise ConfigurationError(f"Failed to parse configuration file: {e}")
48
+
49
+ def create_default_config(email: str, api_key: str, api_url: Optional[str] = None, config_file: str = DEFAULT_CONFIG_FILE) -> None:
50
+ """
51
+ Create a default configuration file in JSON format.
52
+
53
+ Args:
54
+ email: User email
55
+ api_key: Terrakio API key
56
+ api_url: Optional API URL (if different from default)
57
+ config_file: Path to configuration file
58
+
59
+ Raises:
60
+ ConfigurationError: If the configuration file can't be created
61
+ """
62
+ config_path = Path(os.path.expanduser(config_file))
63
+
64
+ # Ensure directory exists
65
+ config_path.parent.mkdir(parents=True, exist_ok=True)
66
+
67
+ try:
68
+ config_data = {
69
+ "EMAIL": email,
70
+ "TERRAKIO_API_KEY": api_key
71
+ }
72
+
73
+ # Add API URL if provided
74
+ if api_url:
75
+ config_data["TERRAKIO_API_URL"] = api_url
76
+
77
+ with open(config_path, 'w') as f:
78
+ json.dump(config_data, f, indent=2)
79
+
80
+ except Exception as e:
81
+ raise ConfigurationError(f"Failed to create configuration file: {e}")
@@ -0,0 +1,235 @@
1
+ import requests
2
+ from typing import Dict, Any, List, Optional
3
+ from .exceptions import APIError
4
+
5
+ class DatasetManagement:
6
+ def __init__(self, api_url: str, api_key: str, verify: bool = True, timeout: int = 60):
7
+ """
8
+ Initialize the Dataset Management client.
9
+
10
+ Args:
11
+ api_url: API base URL
12
+ api_key: API key for authentication
13
+ verify: Verify SSL certificates
14
+ timeout: Request timeout in seconds
15
+ """
16
+ self.api_url = api_url.rstrip('/')
17
+ self.api_key = api_key
18
+ self.verify = verify
19
+ self.timeout = timeout
20
+ self.session = requests.Session()
21
+ self.session.headers.update({
22
+ 'x-api-key': self.api_key,
23
+ 'Content-Type': 'application/json'
24
+ })
25
+
26
+ def get_dataset(self, name: str, collection: str = "terrakio-datasets") -> Dict[str, Any]:
27
+ """
28
+ Retrieve dataset info by dataset name.
29
+
30
+ Args:
31
+ name: The name of the dataset (required)
32
+ collection: The dataset collection (default: 'terrakio-datasets')
33
+
34
+ Returns:
35
+ Dataset information as a dictionary
36
+
37
+ Raises:
38
+ APIError: If the API request fails
39
+ """
40
+ endpoint = f"{self.api_url}/datasets/{name}"
41
+ params = {"collection": collection} if collection else {}
42
+ try:
43
+ response = self.session.get(
44
+ endpoint,
45
+ params=params,
46
+ timeout=self.timeout,
47
+ verify=self.verify
48
+ )
49
+ if not response.ok:
50
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
51
+ return response.json()
52
+ except requests.RequestException as e:
53
+ raise APIError(f"Request failed: {str(e)}")
54
+
55
+ def list_datasets(self, substring: Optional[str] = None, collection: str = "terrakio-datasets") -> List[Dict[str, Any]]:
56
+ """
57
+ List datasets, optionally filtering by a substring and collection.
58
+
59
+ Args:
60
+ substring: Substring to filter by (optional)
61
+ collection: Dataset collection (default: 'terrakio-datasets')
62
+
63
+ Returns:
64
+ List of datasets matching the criteria
65
+
66
+ Raises:
67
+ APIError: If the API request fails
68
+ """
69
+ endpoint = f"{self.api_url}/datasets"
70
+ params = {"collection": collection}
71
+ if substring:
72
+ params["substring"] = substring
73
+ try:
74
+ response = self.session.get(
75
+ endpoint,
76
+ params=params,
77
+ timeout=self.timeout,
78
+ verify=self.verify
79
+ )
80
+ if not response.ok:
81
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
82
+ return response.json()
83
+ except requests.RequestException as e:
84
+ raise APIError(f"Request failed: {str(e)}")
85
+
86
+ def create_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs) -> Dict[str, Any]:
87
+ """
88
+ Create a new dataset.
89
+
90
+ Args:
91
+ name: Name of the dataset (required)
92
+ collection: Dataset collection (default: 'terrakio-datasets')
93
+ **kwargs: Additional dataset parameters including:
94
+ - products: List of products
95
+ - dates_iso8601: List of dates
96
+ - bucket: Storage bucket
97
+ - path: Storage path
98
+ - data_type: Data type
99
+ - no_data: No data value
100
+ - l_max: Maximum level
101
+ - y_size: Y size
102
+ - x_size: X size
103
+ - proj4: Projection string
104
+ - abstract: Dataset abstract
105
+ - geotransform: Geotransform parameters
106
+
107
+ Returns:
108
+ Created dataset information
109
+
110
+ Raises:
111
+ APIError: If the API request fails
112
+ """
113
+ endpoint = f"{self.api_url}/datasets"
114
+ params = {"collection": collection}
115
+ # Create payload with required name parameter
116
+ payload = {"name": name}
117
+
118
+ # Add optional parameters if provided
119
+ for param in ["products", "dates_iso8601", "bucket", "path", "data_type",
120
+ "no_data", "l_max", "y_size", "x_size", "proj4", "abstract", "geotransform"]:
121
+ if param in kwargs:
122
+ payload[param] = kwargs[param]
123
+
124
+ try:
125
+ response = self.session.post(
126
+ endpoint,
127
+ params=params,
128
+ json=payload,
129
+ timeout=self.timeout,
130
+ verify=self.verify
131
+ )
132
+
133
+ if not response.ok:
134
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
135
+ return response.json()
136
+ except requests.RequestException as e:
137
+ raise APIError(f"Request failed: {str(e)}")
138
+
139
+ def update_dataset(self, name: str, append: bool = True, collection: str = "terrakio-datasets", **kwargs) -> Dict[str, Any]:
140
+ """
141
+ Update a dataset. By default, values are appended unless append is set to False.
142
+
143
+ Args:
144
+ name: Name of the dataset (required)
145
+ append: Whether to append values (default: True)
146
+ collection: Dataset collection (default: 'terrakio-datasets')
147
+ **kwargs: Additional dataset parameters to update
148
+
149
+ Returns:
150
+ Updated dataset information
151
+
152
+ Raises:
153
+ APIError: If the API request fails
154
+ """
155
+ endpoint = f"{self.api_url}/datasets"
156
+ params = {"append": str(append).lower(), "collection": collection}
157
+ payload = {"name": name}
158
+ payload.update(kwargs)
159
+ try:
160
+ response = self.session.patch(
161
+ endpoint,
162
+ params=params,
163
+ json=payload,
164
+ timeout=self.timeout,
165
+ verify=self.verify
166
+ )
167
+ if not response.ok:
168
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
169
+ return response.json()
170
+ except requests.RequestException as e:
171
+ raise APIError(f"Request failed: {str(e)}")
172
+
173
+ def overwrite_dataset(self, name: str, collection: str = "terrakio-datasets", **kwargs) -> Dict[str, Any]:
174
+ """
175
+ Overwrite a dataset (replace all values).
176
+
177
+ Args:
178
+ name: Name of the dataset (required)
179
+ collection: Dataset collection (default: 'terrakio-datasets')
180
+ **kwargs: New dataset parameters
181
+
182
+ Returns:
183
+ Updated dataset information
184
+
185
+ Raises:
186
+ APIError: If the API request fails
187
+ """
188
+ endpoint = f"{self.api_url}/datasets"
189
+ params = {"collection": collection}
190
+ payload = {"name": name}
191
+ payload.update(kwargs)
192
+ try:
193
+ response = self.session.put(
194
+ endpoint,
195
+ params=params,
196
+ json=payload,
197
+ timeout=self.timeout,
198
+ verify=self.verify
199
+ )
200
+ if not response.ok:
201
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
202
+ return response.json()
203
+ except requests.RequestException as e:
204
+ raise APIError(f"Request failed: {str(e)}")
205
+
206
+ def delete_dataset(self, name: str, collection: str = "terrakio-datasets") -> Dict[str, Any]:
207
+ """
208
+ Delete a dataset by name.
209
+
210
+ Args:
211
+ name: The name of the dataset (required)
212
+ collection: Dataset collection (default: 'terrakio-datasets')
213
+
214
+ Returns:
215
+ API response as a dictionary
216
+
217
+ Raises:
218
+ APIError: If the API request fails
219
+ """
220
+ endpoint = f"{self.api_url}/datasets/{name}"
221
+ params = {"collection": collection}
222
+ try:
223
+ response = self.session.delete(
224
+ endpoint,
225
+ params=params,
226
+ timeout=self.timeout,
227
+ verify=self.verify
228
+ )
229
+ if response.status_code == 404:
230
+ return {"status": "error", "message": f"Dataset '{name}' does not exist in collection '{collection}'"}
231
+ if not response.ok:
232
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
233
+ return response.json()
234
+ except requests.RequestException as e:
235
+ raise APIError(f"Request failed: {str(e)}")
@@ -0,0 +1,18 @@
1
+ class APIError(Exception):
2
+ """Exception raised for errors in the API responses."""
3
+ pass
4
+
5
+
6
+ class ConfigurationError(Exception):
7
+ """Exception raised for errors in the configuration."""
8
+ pass
9
+
10
+
11
+ class DownloadError(Exception):
12
+ """Exception raised for errors during data download."""
13
+ pass
14
+
15
+
16
+ class ValidationError(Exception):
17
+ """Exception raised for invalid request parameters."""
18
+ pass
@@ -0,0 +1,227 @@
1
+ import requests
2
+ from typing import Dict, Any, List, Optional
3
+ from .exceptions import APIError, ConfigurationError
4
+
5
+ class UserManagement:
6
+ def __init__(self, api_url: str, api_key: str, verify: bool = True, timeout: int = 60):
7
+ """
8
+ Initialize the User Management client.
9
+
10
+ Args:
11
+ api_url: API base URL
12
+ api_key: API key for authentication
13
+ verify: Verify SSL certificates
14
+ timeout: Request timeout in seconds
15
+ """
16
+ self.api_url = api_url.rstrip('/')
17
+ self.api_key = api_key
18
+ self.verify = verify
19
+ self.timeout = timeout
20
+ self.session = requests.Session()
21
+ self.session.headers.update({
22
+ 'x-api-key': self.api_key,
23
+ 'Content-Type': 'application/json'
24
+ })
25
+
26
+ def get_user_by_id(self, user_id: str) -> Dict[str, Any]:
27
+ """
28
+ Retrieve user info by user ID.
29
+
30
+ Args:
31
+ user_id: User ID to retrieve
32
+
33
+ Returns:
34
+ User information as a dictionary
35
+
36
+ Raises:
37
+ APIError: If the API request fails
38
+ """
39
+ endpoint = f"{self.api_url}/admin/users/{user_id}"
40
+ try:
41
+ response = self.session.get(
42
+ endpoint,
43
+ timeout=self.timeout,
44
+ verify=self.verify
45
+ )
46
+ if not response.ok:
47
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
48
+ return response.json()
49
+ except requests.RequestException as e:
50
+ raise APIError(f"Request failed: {str(e)}")
51
+
52
+ def get_user_by_email(self, email: str) -> Dict[str, Any]:
53
+ """
54
+ Retrieve user info by email.
55
+
56
+ Args:
57
+ email: User email to retrieve
58
+
59
+ Returns:
60
+ User information as a dictionary
61
+
62
+ Raises:
63
+ APIError: If the API request fails
64
+ """
65
+
66
+ endpoint = f"{self.api_url}/admin/users/email/{email}"
67
+ try:
68
+ response = self.session.get(
69
+ endpoint,
70
+ timeout=self.timeout,
71
+ verify=self.verify
72
+ )
73
+ if not response.ok:
74
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
75
+ return response.json()
76
+ except requests.RequestException as e:
77
+ raise APIError(f"Request failed: {str(e)}")
78
+
79
+ def edit_user(
80
+ self,
81
+ user_id: str,
82
+ uid: Optional[str] = None,
83
+ email: Optional[str] = None,
84
+ role: Optional[str] = None,
85
+ apiKey: Optional[str] = None,
86
+ groups: Optional[List[str]] = None,
87
+ quota: Optional[int] = None
88
+ ) -> Dict[str, Any]:
89
+ """
90
+ Edit user info. Only provided fields will be updated.
91
+
92
+ Args:
93
+ user_id: User ID (required)
94
+ uid: New user ID (optional)
95
+ email: New user email (optional)
96
+ role: New user role (optional)
97
+ apiKey: New API key (optional)
98
+ groups: New list of groups (optional)
99
+ quota: New quota value (optional)
100
+
101
+ Returns:
102
+ Updated user information
103
+
104
+ Raises:
105
+ APIError: If the API request fails
106
+ """
107
+ endpoint = f"{self.api_url}/admin/users"
108
+ payload = {"uid": user_id}
109
+
110
+ if uid is not None:
111
+ payload["uid"] = uid
112
+ if email is not None:
113
+ payload["email"] = email
114
+ if role is not None:
115
+ payload["role"] = role
116
+ if apiKey is not None:
117
+ payload["apiKey"] = apiKey
118
+ if groups is not None:
119
+ payload["groups"] = groups
120
+ if quota is not None:
121
+ payload["quota"] = quota
122
+
123
+ try:
124
+ response = self.session.patch(
125
+ endpoint,
126
+ json=payload,
127
+ timeout=self.timeout,
128
+ verify=self.verify
129
+ )
130
+ if not response.ok:
131
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
132
+ return response.json()
133
+ except requests.RequestException as e:
134
+ raise APIError(f"Request failed: {str(e)}")
135
+
136
+ def list_users(self, substring: Optional[str] = None, uid: bool = False) -> List[Dict[str, Any]]:
137
+ """
138
+ List users, optionally filtering by a substring.
139
+
140
+ Args:
141
+ substring: Optional substring to filter users
142
+ uid: If True, includes the user ID in the response (default: False)
143
+
144
+ Returns:
145
+ List of users
146
+
147
+ Raises:
148
+ APIError: If the API request fails
149
+ """
150
+ # Use the base API URL instead of hardcoding
151
+ endpoint = f"{self.api_url}/admin/users"
152
+
153
+ params = {}
154
+ if substring:
155
+ params["substring"] = substring
156
+ if uid:
157
+ params["uid"] = "true"
158
+
159
+ try:
160
+ response = self.session.get(
161
+ endpoint,
162
+ params=params,
163
+ timeout=self.timeout,
164
+ verify=self.verify
165
+ )
166
+ if not response.ok:
167
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
168
+ return response.json()
169
+ except requests.RequestException as e:
170
+ raise APIError(f"Request failed: {str(e)}")
171
+
172
+ def reset_quota(self, email: str, quota: Optional[int] = None) -> Dict[str, Any]:
173
+ """
174
+ Reset the quota for a user by email.
175
+
176
+ Args:
177
+ email: The user's email (required)
178
+ quota: The new quota value (optional)
179
+
180
+ Returns:
181
+ API response as a dictionary
182
+
183
+ Raises:
184
+ APIError: If the API request fails
185
+ """
186
+ endpoint = f"{self.api_url}/admin/users/reset_quota/{email}"
187
+ payload = {"email": email}
188
+ if quota is not None:
189
+ payload["quota"] = quota
190
+ try:
191
+ response = self.session.patch(
192
+ endpoint,
193
+ json=payload,
194
+ timeout=self.timeout,
195
+ verify=self.verify
196
+ )
197
+ if not response.ok:
198
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
199
+ return response.json()
200
+ except requests.RequestException as e:
201
+ raise APIError(f"Request failed: {str(e)}")
202
+
203
+ def delete_user(self, uid: str) -> Dict[str, Any]:
204
+ """
205
+ Delete a user by UID.
206
+
207
+ Args:
208
+ uid: The user's UID (required)
209
+
210
+ Returns:
211
+ API response as a dictionary
212
+
213
+ Raises:
214
+ APIError: If the API request fails
215
+ """
216
+ endpoint = f"{self.api_url}/admin/users/{uid}"
217
+ try:
218
+ response = self.session.delete(
219
+ endpoint,
220
+ timeout=self.timeout,
221
+ verify=self.verify
222
+ )
223
+ if not response.ok:
224
+ raise APIError(f"API request failed: {response.status_code} {response.reason}")
225
+ return response.json()
226
+ except requests.RequestException as e:
227
+ raise APIError(f"Request failed: {str(e)}")
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: terrakio-core
3
+ Version: 0.1.9
4
+ Summary: Core components for Terrakio API clients
5
+ Home-page: https://github.com/HaizeaAnalytics/terrakio-python-api
6
+ Author: Yupeng Chao
7
+ Author-email: Yupeng Chao <yupeng@haizea.com.au>
8
+ Project-URL: Homepage, https://github.com/HaizeaAnalytics/terrakio-python-api
9
+ Project-URL: Bug Tracker, https://github.com/HaizeaAnalytics/terrakio-python-api/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Development Status :: 4 - Beta
18
+ Requires-Python: >=3.7
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: requests>=2.25.0
21
+ Requires-Dist: pyyaml>=5.1
22
+ Requires-Dist: xarray>=2023.1.0
23
+ Requires-Dist: shapely>=2.0.0
24
+ Dynamic: author
25
+ Dynamic: home-page
26
+ Dynamic: requires-python
27
+
28
+ # Terrakio Core
29
+
30
+ Core components for Terrakio API clients. This package provides the foundational classes and utilities used by both the regular and admin API clients.
31
+
32
+ This package is typically not used directly but is a dependency for:
33
+ - `terrakio-api`: Regular user client
34
+ - `terrakio-admin-api`: Administrative client
35
+
36
+ For documentation and usage examples, see the [main repository](https://github.com/HaizeaAnalytics/terrakio-python-api).
@@ -0,0 +1,11 @@
1
+ terrakio_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ terrakio_core/auth.py,sha256=Y3X5CcRUO7rAsrv995cSedZFKJAsW6ObDinYCbcQMpc,7605
3
+ terrakio_core/client.py,sha256=hjWxttPlrKfq5myHqWq2GT23pBGxRplm2FAY5j5YHCA,17441
4
+ terrakio_core/config.py,sha256=AwJ1VgR5K7N32XCU5k7_Dp1nIv_FYt8MBonq9yKlGzA,2658
5
+ terrakio_core/dataset_management.py,sha256=hhO35fwStS6HYFQdKP9wkr3DxHgjvpctmIU8UWH6w6U,8742
6
+ terrakio_core/exceptions.py,sha256=9S-I20-QiDRj1qgjFyYUwYM7BLic_bxurcDOIm2Fu_0,410
7
+ terrakio_core/user_management.py,sha256=Sl7wJOg1eUVUpcsgRjeknibYiIleLJk1VgJI7Mdpsss,7345
8
+ terrakio_core-0.1.9.dist-info/METADATA,sha256=AjBJ_avgfU8vz0ZPzHgQEJZMJboE0F3e1ggGRuw6djM,1488
9
+ terrakio_core-0.1.9.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
10
+ terrakio_core-0.1.9.dist-info/top_level.txt,sha256=5cBj6O7rNWyn97ND4YuvvXm0Crv4RxttT4JZvNdOG6Q,14
11
+ terrakio_core-0.1.9.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ terrakio_core