terrakio-core 0.3.4__py3-none-any.whl → 0.3.6__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 +9 -0
- terrakio_core/async_client.py +304 -0
- terrakio_core/client.py +22 -1713
- terrakio_core/config.py +8 -15
- terrakio_core/convenience_functions/convenience_functions.py +296 -0
- terrakio_core/endpoints/auth.py +180 -0
- terrakio_core/endpoints/dataset_management.py +369 -0
- terrakio_core/endpoints/group_management.py +228 -0
- terrakio_core/endpoints/mass_stats.py +594 -0
- terrakio_core/endpoints/model_management.py +385 -0
- terrakio_core/endpoints/space_management.py +72 -0
- terrakio_core/endpoints/user_management.py +131 -0
- terrakio_core/helper/bounded_taskgroup.py +20 -0
- terrakio_core/helper/decorators.py +58 -0
- terrakio_core/{generation → helper}/tiles.py +1 -12
- terrakio_core/sync_client.py +370 -0
- {terrakio_core-0.3.4.dist-info → terrakio_core-0.3.6.dist-info}/METADATA +1 -1
- terrakio_core-0.3.6.dist-info/RECORD +21 -0
- terrakio_core/auth.py +0 -223
- terrakio_core/dataset_management.py +0 -287
- terrakio_core/decorators.py +0 -18
- terrakio_core/group_access_management.py +0 -232
- terrakio_core/mass_stats.py +0 -504
- terrakio_core/space_management.py +0 -101
- terrakio_core/user_management.py +0 -227
- terrakio_core-0.3.4.dist-info/RECORD +0 -16
- {terrakio_core-0.3.4.dist-info → terrakio_core-0.3.6.dist-info}/WHEEL +0 -0
- {terrakio_core-0.3.4.dist-info → terrakio_core-0.3.6.dist-info}/top_level.txt +0 -0
terrakio_core/config.py
CHANGED
|
@@ -2,21 +2,20 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Dict, Any, Optional
|
|
5
|
-
|
|
5
|
+
import logging
|
|
6
6
|
from .exceptions import ConfigurationError
|
|
7
7
|
|
|
8
8
|
# Default configuration file locations
|
|
9
9
|
DEFAULT_CONFIG_FILE = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
|
|
10
10
|
DEFAULT_API_URL = "https://api.terrak.io"
|
|
11
11
|
|
|
12
|
-
def read_config_file(config_file: str = DEFAULT_CONFIG_FILE,
|
|
12
|
+
def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, logger: logging.Logger = None) -> Dict[str, Any]:
|
|
13
13
|
"""
|
|
14
14
|
Read and parse the configuration file.
|
|
15
15
|
|
|
16
16
|
Args:
|
|
17
17
|
config_file: Path to the configuration file
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
logger: Logger object to log messages
|
|
20
19
|
Returns:
|
|
21
20
|
Dict[str, Any]: Configuration parameters with additional flags:
|
|
22
21
|
'is_logged_in': True if user is logged in
|
|
@@ -36,8 +35,7 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, quiet: bool = False
|
|
|
36
35
|
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
36
|
with open(config_path, 'w') as f:
|
|
38
37
|
json.dump({}, f)
|
|
39
|
-
|
|
40
|
-
print("No API key found. Please provide an API key to use this client.")
|
|
38
|
+
logger.info("No API key found. Please provide an API key to use this client.")
|
|
41
39
|
return {
|
|
42
40
|
'url': DEFAULT_API_URL,
|
|
43
41
|
'key': None,
|
|
@@ -54,8 +52,7 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, quiet: bool = False
|
|
|
54
52
|
# Read the config file data
|
|
55
53
|
# Check if config has an API key
|
|
56
54
|
if not config_data or 'TERRAKIO_API_KEY' not in config_data or not config_data.get('TERRAKIO_API_KEY'):
|
|
57
|
-
|
|
58
|
-
print("No API key found. Please provide an API key to use this client.")
|
|
55
|
+
logger.info("No API key found. Please provide an API key to use this client.")
|
|
59
56
|
return {
|
|
60
57
|
'url': DEFAULT_API_URL,
|
|
61
58
|
'key': None,
|
|
@@ -63,10 +60,7 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, quiet: bool = False
|
|
|
63
60
|
'user_email': None,
|
|
64
61
|
'token': config_data.get('PERSONAL_TOKEN')
|
|
65
62
|
}
|
|
66
|
-
|
|
67
|
-
# If we have config values, use them
|
|
68
|
-
if not quiet:
|
|
69
|
-
print(f"Currently logged in as: {config_data.get('EMAIL')}")
|
|
63
|
+
logger.info(f"Currently logged in as: {config_data.get('EMAIL')}")
|
|
70
64
|
# this meanb that we have already logged in to the tkio account
|
|
71
65
|
|
|
72
66
|
# Convert the JSON config to our expected format
|
|
@@ -82,9 +76,8 @@ def read_config_file(config_file: str = DEFAULT_CONFIG_FILE, quiet: bool = False
|
|
|
82
76
|
|
|
83
77
|
|
|
84
78
|
except Exception as e:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
print("No API key found. Please provide an API key to use this client.")
|
|
79
|
+
logger.info(f"Error reading config: {e}")
|
|
80
|
+
logger.info("No API key found. Please provide an API key to use this client.")
|
|
88
81
|
return {
|
|
89
82
|
'url': DEFAULT_API_URL,
|
|
90
83
|
'key': None,
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
import tempfile
|
|
4
|
+
import time
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from geopandas import GeoDataFrame
|
|
7
|
+
from shapely.geometry import mapping
|
|
8
|
+
from ..exceptions import APIError, ConfigurationError
|
|
9
|
+
from ..helper.bounded_taskgroup import BoundedTaskGroup
|
|
10
|
+
from ..helper.tiles import tiles
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
async def zonal_stats(
|
|
14
|
+
client,
|
|
15
|
+
gdf: GeoDataFrame,
|
|
16
|
+
expr: str,
|
|
17
|
+
conc: int = 20,
|
|
18
|
+
inplace: bool = False,
|
|
19
|
+
in_crs: str = "epsg:4326",
|
|
20
|
+
out_crs: str = "epsg:4326",
|
|
21
|
+
resolution: int = -1,
|
|
22
|
+
geom_fix: bool = False,
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Compute zonal statistics for all geometries in a GeoDataFrame.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
client: The AsyncClient instance
|
|
29
|
+
gdf (GeoDataFrame): GeoDataFrame containing geometries
|
|
30
|
+
expr (str): Terrakio expression to evaluate, can include spatial aggregations
|
|
31
|
+
conc (int): Number of concurrent requests to make
|
|
32
|
+
inplace (bool): Whether to modify the input GeoDataFrame in place
|
|
33
|
+
in_crs (str): Input coordinate reference system
|
|
34
|
+
out_crs (str): Output coordinate reference system
|
|
35
|
+
resolution (int): Resolution parameter
|
|
36
|
+
geom_fix (bool): Whether to fix the geometry (default False)
|
|
37
|
+
Returns:
|
|
38
|
+
geopandas.GeoDataFrame: GeoDataFrame with added columns for results, or None if inplace=True
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ValueError: If concurrency is too high
|
|
42
|
+
APIError: If the API request fails
|
|
43
|
+
"""
|
|
44
|
+
if conc > 100:
|
|
45
|
+
raise ValueError("Concurrency (conc) is too high. Please set conc to 100 or less.")
|
|
46
|
+
|
|
47
|
+
total_geometries = len(gdf)
|
|
48
|
+
|
|
49
|
+
client.logger.info(f"Processing {total_geometries} geometries with concurrency {conc}")
|
|
50
|
+
|
|
51
|
+
completed_count = 0
|
|
52
|
+
lock = asyncio.Lock()
|
|
53
|
+
|
|
54
|
+
async def process_geometry(geom, index):
|
|
55
|
+
"""Process a single geometry"""
|
|
56
|
+
nonlocal completed_count
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
feature = {
|
|
60
|
+
"type": "Feature",
|
|
61
|
+
"geometry": mapping(geom),
|
|
62
|
+
"properties": {"index": index}
|
|
63
|
+
}
|
|
64
|
+
result = await client.geoquery(expr=expr, feature=feature, output="csv",
|
|
65
|
+
in_crs=in_crs, out_crs=out_crs, resolution=resolution, geom_fix=geom_fix)
|
|
66
|
+
|
|
67
|
+
if isinstance(result, dict) and result.get("error"):
|
|
68
|
+
error_msg = f"Request {index} failed: {result.get('error_message', 'Unknown error')}"
|
|
69
|
+
if result.get('status_code'):
|
|
70
|
+
error_msg = f"Request {index} failed with status {result['status_code']}: {result.get('error_message', 'Unknown error')}"
|
|
71
|
+
raise APIError(error_msg)
|
|
72
|
+
|
|
73
|
+
if isinstance(result, pd.DataFrame):
|
|
74
|
+
result['_geometry_index'] = index
|
|
75
|
+
|
|
76
|
+
async with lock:
|
|
77
|
+
completed_count += 1
|
|
78
|
+
if completed_count % max(1, total_geometries // 10) == 0:
|
|
79
|
+
client.logger.info(f"Progress: {completed_count}/{total_geometries} geometries processed")
|
|
80
|
+
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
async with lock:
|
|
85
|
+
completed_count += 1
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
async with BoundedTaskGroup(max_concurrency=conc) as tg:
|
|
90
|
+
tasks = [
|
|
91
|
+
tg.create_task(process_geometry(gdf.geometry.iloc[idx], idx))
|
|
92
|
+
for idx in range(len(gdf))
|
|
93
|
+
]
|
|
94
|
+
all_results = [task.result() for task in tasks]
|
|
95
|
+
|
|
96
|
+
except* Exception as eg:
|
|
97
|
+
for e in eg.exceptions:
|
|
98
|
+
if hasattr(e, 'response'):
|
|
99
|
+
raise APIError(f"API request failed: {e.response.text}")
|
|
100
|
+
raise
|
|
101
|
+
|
|
102
|
+
client.logger.info("All requests completed! Processing results...")
|
|
103
|
+
|
|
104
|
+
if not all_results:
|
|
105
|
+
raise ValueError("No valid results were returned for any geometry")
|
|
106
|
+
|
|
107
|
+
combined_df = pd.concat(all_results, ignore_index=True)
|
|
108
|
+
|
|
109
|
+
has_time = 'time' in combined_df.columns
|
|
110
|
+
|
|
111
|
+
if has_time:
|
|
112
|
+
if '_geometry_index' not in combined_df.columns:
|
|
113
|
+
raise ValueError("Missing geometry index in results")
|
|
114
|
+
|
|
115
|
+
combined_df.set_index(['_geometry_index', 'time'], inplace=True)
|
|
116
|
+
|
|
117
|
+
result_cols = combined_df.columns
|
|
118
|
+
|
|
119
|
+
result_rows = []
|
|
120
|
+
geometries = []
|
|
121
|
+
|
|
122
|
+
for (geom_idx, time_val), row in combined_df.iterrows():
|
|
123
|
+
new_row = {}
|
|
124
|
+
|
|
125
|
+
for col in gdf.columns:
|
|
126
|
+
if col != 'geometry':
|
|
127
|
+
new_row[col] = gdf.loc[geom_idx, col]
|
|
128
|
+
|
|
129
|
+
for col in result_cols:
|
|
130
|
+
new_row[col] = row[col]
|
|
131
|
+
|
|
132
|
+
result_rows.append(new_row)
|
|
133
|
+
geometries.append(gdf.geometry.iloc[geom_idx])
|
|
134
|
+
|
|
135
|
+
multi_index = pd.MultiIndex.from_tuples(
|
|
136
|
+
combined_df.index.tolist(),
|
|
137
|
+
names=['geometry_index', 'time']
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result_gdf = GeoDataFrame(
|
|
141
|
+
result_rows,
|
|
142
|
+
geometry=geometries,
|
|
143
|
+
index=multi_index
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if inplace:
|
|
147
|
+
return result_gdf
|
|
148
|
+
else:
|
|
149
|
+
return result_gdf
|
|
150
|
+
else:
|
|
151
|
+
result_gdf = gdf.copy() if not inplace else gdf
|
|
152
|
+
|
|
153
|
+
result_cols = [col for col in combined_df.columns if col not in ['_geometry_index']]
|
|
154
|
+
|
|
155
|
+
geom_idx_to_row = {}
|
|
156
|
+
for idx, row in combined_df.iterrows():
|
|
157
|
+
geom_idx = int(row['_geometry_index'])
|
|
158
|
+
geom_idx_to_row[geom_idx] = row
|
|
159
|
+
|
|
160
|
+
for col in result_cols:
|
|
161
|
+
if col not in result_gdf.columns:
|
|
162
|
+
result_gdf[col] = None
|
|
163
|
+
|
|
164
|
+
for geom_idx, row in geom_idx_to_row.items():
|
|
165
|
+
result_gdf.loc[geom_idx, col] = row[col]
|
|
166
|
+
if inplace:
|
|
167
|
+
return None
|
|
168
|
+
else:
|
|
169
|
+
return result_gdf
|
|
170
|
+
|
|
171
|
+
async def create_dataset_file(
|
|
172
|
+
client,
|
|
173
|
+
aoi: str,
|
|
174
|
+
expression: str,
|
|
175
|
+
output: str,
|
|
176
|
+
in_crs: str = "epsg:4326",
|
|
177
|
+
res: float = 0.0001,
|
|
178
|
+
region: str = "aus",
|
|
179
|
+
to_crs: str = "epsg:4326",
|
|
180
|
+
overwrite: bool = True,
|
|
181
|
+
skip_existing: bool = False,
|
|
182
|
+
non_interactive: bool = True,
|
|
183
|
+
poll_interval: int = 30,
|
|
184
|
+
download_path: str = "/home/user/Downloads",
|
|
185
|
+
) -> dict:
|
|
186
|
+
|
|
187
|
+
name = f"tiles-{uuid.uuid4().hex[:8]}"
|
|
188
|
+
|
|
189
|
+
body, reqs, groups = tiles(
|
|
190
|
+
name = name,
|
|
191
|
+
aoi = aoi,
|
|
192
|
+
expression = expression,
|
|
193
|
+
output = output,
|
|
194
|
+
tile_size = 128,
|
|
195
|
+
crs = in_crs,
|
|
196
|
+
res = res,
|
|
197
|
+
region = region,
|
|
198
|
+
to_crs = to_crs,
|
|
199
|
+
fully_cover = True,
|
|
200
|
+
overwrite = overwrite,
|
|
201
|
+
skip_existing = skip_existing,
|
|
202
|
+
non_interactive = non_interactive
|
|
203
|
+
)
|
|
204
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tempreq:
|
|
205
|
+
tempreq.write(reqs)
|
|
206
|
+
tempreqname = tempreq.name
|
|
207
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tempmanifest:
|
|
208
|
+
tempmanifest.write(groups)
|
|
209
|
+
tempmanifestname = tempmanifest.name
|
|
210
|
+
|
|
211
|
+
task_id = await client.mass_stats.execute_job(
|
|
212
|
+
name=body["name"],
|
|
213
|
+
region=body["region"],
|
|
214
|
+
output=body["output"],
|
|
215
|
+
config = {},
|
|
216
|
+
overwrite=body["overwrite"],
|
|
217
|
+
skip_existing=body["skip_existing"],
|
|
218
|
+
request_json=tempreqname,
|
|
219
|
+
manifest_json=tempmanifestname,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
start_time = time.time()
|
|
223
|
+
status = None
|
|
224
|
+
|
|
225
|
+
while True:
|
|
226
|
+
try:
|
|
227
|
+
taskid = task_id['task_id']
|
|
228
|
+
trackinfo = await client.mass_stats.track_job([taskid])
|
|
229
|
+
client.logger.info("the trackinfo is: ", trackinfo)
|
|
230
|
+
status = trackinfo[taskid]['status']
|
|
231
|
+
|
|
232
|
+
if status == 'Completed':
|
|
233
|
+
client.logger.info('Tiles generated successfully!')
|
|
234
|
+
break
|
|
235
|
+
elif status in ['Failed', 'Cancelled', 'Error']:
|
|
236
|
+
raise RuntimeError(f"Job {taskid} failed with status: {status}")
|
|
237
|
+
else:
|
|
238
|
+
elapsed_time = time.time() - start_time
|
|
239
|
+
client.logger.info(f"Job status: {status} - Elapsed time: {elapsed_time:.1f}s", end='\r')
|
|
240
|
+
|
|
241
|
+
await asyncio.sleep(poll_interval)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
except KeyboardInterrupt:
|
|
245
|
+
client.logger.info(f"\nInterrupted! Job {taskid} is still running in the background.")
|
|
246
|
+
raise
|
|
247
|
+
except Exception as e:
|
|
248
|
+
client.logger.info(f"\nError tracking job: {e}")
|
|
249
|
+
raise
|
|
250
|
+
|
|
251
|
+
os.unlink(tempreqname)
|
|
252
|
+
os.unlink(tempmanifestname)
|
|
253
|
+
|
|
254
|
+
combine_result = await client.mass_stats.combine_tiles(body["name"], body["overwrite"], body["output"])
|
|
255
|
+
combine_task_id = combine_result.get("task_id")
|
|
256
|
+
|
|
257
|
+
combine_start_time = time.time()
|
|
258
|
+
while True:
|
|
259
|
+
try:
|
|
260
|
+
trackinfo = await client.mass_stats.track_job([combine_task_id])
|
|
261
|
+
client.logger.info('client create dataset file track info:', trackinfo)
|
|
262
|
+
if body["output"] == "netcdf":
|
|
263
|
+
download_file_name = trackinfo[combine_task_id]['folder'] + '.nc'
|
|
264
|
+
elif body["output"] == "geotiff":
|
|
265
|
+
download_file_name = trackinfo[combine_task_id]['folder'] + '.tif'
|
|
266
|
+
bucket = trackinfo[combine_task_id]['bucket']
|
|
267
|
+
combine_status = trackinfo[combine_task_id]['status']
|
|
268
|
+
if combine_status == 'Completed':
|
|
269
|
+
client.logger.info('Tiles combined successfully!')
|
|
270
|
+
break
|
|
271
|
+
elif combine_status in ['Failed', 'Cancelled', 'Error']:
|
|
272
|
+
raise RuntimeError(f"Combine job {combine_task_id} failed with status: {combine_status}")
|
|
273
|
+
else:
|
|
274
|
+
elapsed_time = time.time() - combine_start_time
|
|
275
|
+
client.logger.info(f"Combine job status: {combine_status} - Elapsed time: {elapsed_time:.1f}s", end='\r')
|
|
276
|
+
time.sleep(poll_interval)
|
|
277
|
+
except KeyboardInterrupt:
|
|
278
|
+
client.logger.info(f"\nInterrupted! Combine job {combine_task_id} is still running in the background.")
|
|
279
|
+
raise
|
|
280
|
+
except Exception as e:
|
|
281
|
+
client.logger.info(f"\nError tracking combine job: {e}")
|
|
282
|
+
raise
|
|
283
|
+
|
|
284
|
+
if download_path:
|
|
285
|
+
await client.mass_stats.download_file(
|
|
286
|
+
job_name=body["name"],
|
|
287
|
+
bucket=bucket,
|
|
288
|
+
file_type='processed',
|
|
289
|
+
page_size=10,
|
|
290
|
+
output_path=download_path,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
path = f"{body['name']}/outputs/merged/{download_file_name}"
|
|
294
|
+
client.logger.info(f"Combined file is available at {path}")
|
|
295
|
+
|
|
296
|
+
return {"generation_task_id": task_id, "combine_task_id": combine_task_id}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from typing import Dict, Any
|
|
4
|
+
from ..exceptions import APIError
|
|
5
|
+
from ..helper.decorators import require_token, require_api_key, require_auth
|
|
6
|
+
|
|
7
|
+
class AuthClient:
|
|
8
|
+
def __init__(self, client):
|
|
9
|
+
self._client = client
|
|
10
|
+
|
|
11
|
+
async def signup(self, email: str, password: str) -> Dict[str, str]:
|
|
12
|
+
"""
|
|
13
|
+
Signup a new user with email and password.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
email: User's email address
|
|
17
|
+
password: User's password
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dict containing the authentication token
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
APIError: If the signup request fails
|
|
24
|
+
"""
|
|
25
|
+
payload = {
|
|
26
|
+
"email": email,
|
|
27
|
+
"password": password
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
result = await self._client._terrakio_request("POST", "/users/signup", json=payload)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
self._client.logger.info(f"Signup failed: {str(e)}")
|
|
35
|
+
raise APIError(f"Signup request failed: {str(e)}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def login(self, email: str, password: str) -> Dict[str, str]:
|
|
39
|
+
"""
|
|
40
|
+
Login a user with email and password.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
email: User's email address
|
|
44
|
+
password: User's password
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dict containing the authentication token
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
APIError: If the login request fails
|
|
51
|
+
"""
|
|
52
|
+
payload = {
|
|
53
|
+
"email": email,
|
|
54
|
+
"password": password
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result = await self._client._terrakio_request("POST", "/users/login", json=payload)
|
|
59
|
+
token_response = result.get("token")
|
|
60
|
+
|
|
61
|
+
if token_response:
|
|
62
|
+
self._client.token = token_response
|
|
63
|
+
|
|
64
|
+
api_key_response = await self.view_api_key()
|
|
65
|
+
self._client.key = api_key_response
|
|
66
|
+
|
|
67
|
+
if not self._client.url:
|
|
68
|
+
self._client.url = "https://api.terrak.io"
|
|
69
|
+
|
|
70
|
+
self._save_config(email, token_response)
|
|
71
|
+
|
|
72
|
+
self._client.logger.info(f"Successfully authenticated as: {email}")
|
|
73
|
+
self._client.logger.info(f"Using Terrakio API at: {self._client.url}")
|
|
74
|
+
|
|
75
|
+
return {"token": token_response} if token_response else {"error": "Login failed"}
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
self._client.logger.info(f"Login failed: {str(e)}")
|
|
79
|
+
raise APIError(f"Login request failed: {str(e)}")
|
|
80
|
+
|
|
81
|
+
@require_token
|
|
82
|
+
async def view_api_key(self) -> str:
|
|
83
|
+
"""
|
|
84
|
+
View the current API key for the authenticated user.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str: The API key
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
APIError: If the API request fails
|
|
91
|
+
"""
|
|
92
|
+
result = await self._client._terrakio_request("GET", "/users/key")
|
|
93
|
+
api_key = result.get("apiKey")
|
|
94
|
+
return api_key
|
|
95
|
+
|
|
96
|
+
@require_api_key
|
|
97
|
+
@require_token
|
|
98
|
+
async def refresh_api_key(self) -> str:
|
|
99
|
+
"""
|
|
100
|
+
Refresh the API key for the authenticated user.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
str: The new API key
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
APIError: If the API request fails
|
|
107
|
+
"""
|
|
108
|
+
result = await self._client._terrakio_request("POST", "/users/refresh_key")
|
|
109
|
+
self._client.key = result.get("apiKey")
|
|
110
|
+
|
|
111
|
+
self._update_config_key()
|
|
112
|
+
|
|
113
|
+
return self._client.key
|
|
114
|
+
|
|
115
|
+
@require_api_key
|
|
116
|
+
def get_user_info(self) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
Get information about the authenticated user.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Dict[str, Any]: User information
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
APIError: If the API request fails
|
|
125
|
+
"""
|
|
126
|
+
return self._client._terrakio_request("GET", "/users/info")
|
|
127
|
+
|
|
128
|
+
def _save_config(self, email: str, token: str):
|
|
129
|
+
"""
|
|
130
|
+
Helper method to save config file.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
email: User's email address
|
|
134
|
+
token: Authentication token
|
|
135
|
+
"""
|
|
136
|
+
config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
config = {"EMAIL": email, "TERRAKIO_API_KEY": self._client.key}
|
|
140
|
+
if os.path.exists(config_path):
|
|
141
|
+
with open(config_path, 'r') as f:
|
|
142
|
+
config = json.load(f)
|
|
143
|
+
|
|
144
|
+
config["EMAIL"] = email
|
|
145
|
+
config["TERRAKIO_API_KEY"] = self._client.key
|
|
146
|
+
config["PERSONAL_TOKEN"] = token
|
|
147
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
148
|
+
|
|
149
|
+
with open(config_path, 'w') as f:
|
|
150
|
+
json.dump(config, f, indent=4)
|
|
151
|
+
|
|
152
|
+
self._client.logger.info(f"API key saved to {config_path}")
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
self._client.logger.info(f"Warning: Failed to update config file: {e}")
|
|
156
|
+
|
|
157
|
+
def _update_config_key(self):
|
|
158
|
+
"""
|
|
159
|
+
Helper method to update just the API key in config.
|
|
160
|
+
"""
|
|
161
|
+
config_path = os.path.join(os.environ.get("HOME", ""), ".tkio_config.json")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
config = {"EMAIL": "", "TERRAKIO_API_KEY": ""}
|
|
165
|
+
|
|
166
|
+
if os.path.exists(config_path):
|
|
167
|
+
with open(config_path, 'r') as f:
|
|
168
|
+
config = json.load(f)
|
|
169
|
+
|
|
170
|
+
config["TERRAKIO_API_KEY"] = self._client.key
|
|
171
|
+
|
|
172
|
+
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
|
173
|
+
|
|
174
|
+
with open(config_path, 'w') as f:
|
|
175
|
+
json.dump(config, f, indent=4)
|
|
176
|
+
|
|
177
|
+
self._client.logger.info(f"API key updated in {config_path}")
|
|
178
|
+
|
|
179
|
+
except Exception as e:
|
|
180
|
+
self._client.logger.info(f"Warning: Failed to update config file: {e}")
|