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.
- terrakio_core/__init__.py +0 -0
- terrakio_core/auth.py +237 -0
- terrakio_core/client.py +380 -0
- terrakio_core/config.py +81 -0
- terrakio_core/dataset_management.py +235 -0
- terrakio_core/exceptions.py +18 -0
- terrakio_core/user_management.py +227 -0
- terrakio_core-0.1.9.dist-info/METADATA +36 -0
- terrakio_core-0.1.9.dist-info/RECORD +11 -0
- terrakio_core-0.1.9.dist-info/WHEEL +5 -0
- terrakio_core-0.1.9.dist-info/top_level.txt +1 -0
|
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)}")
|
terrakio_core/client.py
ADDED
|
@@ -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()
|
terrakio_core/config.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
terrakio_core
|