terrakio-core 0.4.97__py3-none-any.whl → 0.4.98.1b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of terrakio-core might be problematic. Click here for more details.
- terrakio_core/__init__.py +1 -1
- terrakio_core/async_client.py +26 -169
- terrakio_core/config.py +3 -44
- terrakio_core/convenience_functions/zonal_stats.py +86 -33
- terrakio_core/endpoints/auth.py +96 -47
- terrakio_core/endpoints/dataset_management.py +120 -54
- terrakio_core/endpoints/group_management.py +269 -76
- terrakio_core/endpoints/mass_stats.py +704 -581
- terrakio_core/endpoints/model_management.py +213 -109
- terrakio_core/endpoints/user_management.py +106 -21
- terrakio_core/exceptions.py +371 -1
- terrakio_core/sync_client.py +9 -124
- {terrakio_core-0.4.97.dist-info → terrakio_core-0.4.98.1b1.dist-info}/METADATA +2 -1
- terrakio_core-0.4.98.1b1.dist-info/RECORD +23 -0
- terrakio_core-0.4.97.dist-info/RECORD +0 -23
- {terrakio_core-0.4.97.dist-info → terrakio_core-0.4.98.1b1.dist-info}/WHEEL +0 -0
|
@@ -1,712 +1,835 @@
|
|
|
1
|
-
from typing import Dict, Any, Optional
|
|
2
|
-
import json
|
|
3
|
-
import gzip
|
|
4
1
|
import os
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
from
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
2
|
+
import time
|
|
3
|
+
import typer
|
|
4
|
+
from typing import Dict, Any, Optional, List
|
|
5
|
+
from dateutil import parser
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.progress import Progress, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn
|
|
9
|
+
|
|
10
|
+
from ..exceptions import (
|
|
11
|
+
CreateCollectionError,
|
|
12
|
+
GetCollectionError,
|
|
13
|
+
ListCollectionsError,
|
|
14
|
+
CollectionNotFoundError,
|
|
15
|
+
CollectionAlreadyExistsError,
|
|
16
|
+
InvalidCollectionTypeError,
|
|
17
|
+
DeleteCollectionError,
|
|
18
|
+
ListTasksError,
|
|
19
|
+
UploadRequestsError,
|
|
20
|
+
UploadArtifactsError,
|
|
21
|
+
GetTaskError,
|
|
22
|
+
TaskNotFoundError,
|
|
23
|
+
DownloadFilesError,
|
|
24
|
+
CancelTaskError,
|
|
25
|
+
CancelCollectionTasksError,
|
|
26
|
+
CancelAllTasksError,
|
|
27
|
+
)
|
|
28
|
+
from ..helper.decorators import require_api_key
|
|
29
|
+
|
|
30
|
+
import aiohttp # Make sure this is imported at the top
|
|
31
|
+
|
|
17
32
|
|
|
18
33
|
class MassStats:
|
|
19
34
|
def __init__(self, client):
|
|
20
35
|
self._client = client
|
|
36
|
+
self.console = Console()
|
|
37
|
+
|
|
38
|
+
async def track_progress(self, task_id):
|
|
39
|
+
task_info = await self.get_task(task_id=task_id)
|
|
40
|
+
number_of_jobs = task_info["task"]["total"]
|
|
41
|
+
start_time = parser.parse(task_info["task"]["createdAt"])
|
|
42
|
+
|
|
43
|
+
self.console.print(f"[bold cyan]Tracking task: {task_id}[/bold cyan]")
|
|
44
|
+
|
|
45
|
+
completed_jobs_info = []
|
|
46
|
+
|
|
47
|
+
def get_job_description(job_info, include_status=False):
|
|
48
|
+
if not job_info:
|
|
49
|
+
return "No job info"
|
|
50
|
+
|
|
51
|
+
service = job_info.get("service", "Unknown service")
|
|
52
|
+
desc = service
|
|
53
|
+
|
|
54
|
+
if include_status:
|
|
55
|
+
status = job_info.get("status", "unknown")
|
|
56
|
+
desc += f" - {status}"
|
|
57
|
+
|
|
58
|
+
return desc
|
|
59
|
+
|
|
60
|
+
progress = Progress(
|
|
61
|
+
TextColumn("[progress.description]{task.description}"),
|
|
62
|
+
BarColumn(),
|
|
63
|
+
TaskProgressColumn(),
|
|
64
|
+
TimeElapsedColumn(),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
with progress:
|
|
68
|
+
last_completed_count = 0
|
|
69
|
+
current_job_task = None
|
|
70
|
+
current_job_description = None
|
|
71
|
+
|
|
72
|
+
while len(completed_jobs_info) < number_of_jobs:
|
|
73
|
+
task_info = await self.get_task(task_id=task_id)
|
|
74
|
+
completed_number = task_info["task"]["completed"]
|
|
75
|
+
current_job_info = task_info["currentJob"]
|
|
76
|
+
|
|
77
|
+
if completed_number > last_completed_count:
|
|
78
|
+
if current_job_task is not None:
|
|
79
|
+
completed_description = current_job_description.replace(" - pending", "").replace(" - running", "").replace(" - waiting", "")
|
|
80
|
+
completed_description += " - completed"
|
|
81
|
+
|
|
82
|
+
progress.update(
|
|
83
|
+
current_job_task,
|
|
84
|
+
description=f"[{last_completed_count + 1}/{number_of_jobs}] {completed_description}",
|
|
85
|
+
completed=100
|
|
86
|
+
)
|
|
87
|
+
completed_jobs_info.append({
|
|
88
|
+
"task": current_job_task,
|
|
89
|
+
"description": completed_description,
|
|
90
|
+
"job_number": last_completed_count + 1
|
|
91
|
+
})
|
|
92
|
+
current_job_task = None
|
|
93
|
+
current_job_description = None
|
|
94
|
+
|
|
95
|
+
last_completed_count = completed_number
|
|
96
|
+
|
|
97
|
+
if current_job_info:
|
|
98
|
+
status = current_job_info["status"]
|
|
99
|
+
current_job_description = get_job_description(current_job_info, include_status=True)
|
|
100
|
+
|
|
101
|
+
total_value = current_job_info.get("total", 0)
|
|
102
|
+
completed_value = current_job_info.get("completed", 0)
|
|
103
|
+
|
|
104
|
+
if total_value == -9999:
|
|
105
|
+
percent = 0
|
|
106
|
+
elif total_value > 0:
|
|
107
|
+
percent = int(completed_value / total_value * 100)
|
|
108
|
+
else:
|
|
109
|
+
percent = 0
|
|
110
|
+
|
|
111
|
+
if current_job_task is None:
|
|
112
|
+
current_job_task = progress.add_task(
|
|
113
|
+
f"[{completed_number + 1}/{number_of_jobs}] {current_job_description}",
|
|
114
|
+
total=100,
|
|
115
|
+
start_time=start_time
|
|
116
|
+
)
|
|
117
|
+
else:
|
|
118
|
+
progress.update(
|
|
119
|
+
current_job_task,
|
|
120
|
+
description=f"[{completed_number + 1}/{number_of_jobs}] {current_job_description}",
|
|
121
|
+
completed=percent
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if status == "Error":
|
|
125
|
+
self.console.print("[bold red]Error![/bold red]")
|
|
126
|
+
raise typer.Exit(code=1)
|
|
127
|
+
if status == "Cancelled":
|
|
128
|
+
self.console.print("[bold orange]Cancelled![/bold orange]")
|
|
129
|
+
raise typer.Exit(code=1)
|
|
130
|
+
elif status == "Completed":
|
|
131
|
+
completed_description = current_job_description.replace(" - pending", "").replace(" - running", "").replace(" - waiting", "")
|
|
132
|
+
completed_description += " - completed"
|
|
133
|
+
progress.update(
|
|
134
|
+
current_job_task,
|
|
135
|
+
description=f"[{completed_number + 1}/{number_of_jobs}] {completed_description}",
|
|
136
|
+
completed=100
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if completed_number == number_of_jobs and current_job_info is None:
|
|
140
|
+
if current_job_task is not None:
|
|
141
|
+
completed_description = current_job_description.replace(" - pending", "").replace(" - running", "").replace(" - waiting", "")
|
|
142
|
+
completed_description += " - completed"
|
|
143
|
+
progress.update(
|
|
144
|
+
current_job_task,
|
|
145
|
+
description=f"[{number_of_jobs}/{number_of_jobs}] {completed_description}",
|
|
146
|
+
completed=100
|
|
147
|
+
)
|
|
148
|
+
completed_jobs_info.append({
|
|
149
|
+
"task": current_job_task,
|
|
150
|
+
"description": completed_description,
|
|
151
|
+
"job_number": number_of_jobs
|
|
152
|
+
})
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
time.sleep(10)
|
|
156
|
+
|
|
157
|
+
self.console.print(f"[bold green]All {number_of_jobs} jobs finished![/bold green]")
|
|
21
158
|
|
|
22
159
|
@require_api_key
|
|
23
|
-
async def
|
|
24
|
-
self,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
config: Dict[str, Any],
|
|
30
|
-
region: str = None,
|
|
31
|
-
overwrite: bool = False,
|
|
32
|
-
skip_existing: bool = False,
|
|
33
|
-
location: Optional[str] = None,
|
|
34
|
-
force_loc: Optional[bool] = None,
|
|
35
|
-
server: Optional[str] = "dev-au.terrak.io",
|
|
160
|
+
async def create_collection(
|
|
161
|
+
self,
|
|
162
|
+
collection: str,
|
|
163
|
+
bucket: Optional[str] = None,
|
|
164
|
+
location: Optional[str] = None,
|
|
165
|
+
collection_type: str = "basic"
|
|
36
166
|
) -> Dict[str, Any]:
|
|
37
167
|
"""
|
|
38
|
-
|
|
168
|
+
Create a collection for the current user.
|
|
39
169
|
|
|
40
170
|
Args:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
overwrite: Whether to overwrite the job
|
|
47
|
-
skip_existing: Whether to skip existing jobs
|
|
48
|
-
location: The location of the job
|
|
49
|
-
force_loc: Whether to force the location
|
|
50
|
-
server: The server to use
|
|
51
|
-
|
|
171
|
+
collection: The name of the collection (required)
|
|
172
|
+
bucket: The bucket to use (optional, admin only)
|
|
173
|
+
location: The location to use (optional, admin only)
|
|
174
|
+
collection_type: The type of collection to create (optional, defaults to "basic")
|
|
175
|
+
|
|
52
176
|
Returns:
|
|
53
|
-
API response as a dictionary
|
|
54
|
-
|
|
177
|
+
API response as a dictionary containing the collection id
|
|
178
|
+
|
|
55
179
|
Raises:
|
|
56
|
-
|
|
180
|
+
CollectionAlreadyExistsError: If the collection already exists
|
|
181
|
+
InvalidCollectionTypeError: If the collection type is invalid
|
|
182
|
+
CreateCollectionError: If the API request fails due to unknown reasons
|
|
57
183
|
"""
|
|
58
|
-
# we don't actually need the region function inside the request, the endpoint will fix that for us
|
|
59
184
|
payload = {
|
|
60
|
-
"
|
|
61
|
-
"size": size,
|
|
62
|
-
"sample": sample,
|
|
63
|
-
"output": output,
|
|
64
|
-
"config": config,
|
|
65
|
-
"overwrite": overwrite,
|
|
66
|
-
"skip_existing": skip_existing,
|
|
67
|
-
"server": server,
|
|
68
|
-
"region": region
|
|
69
|
-
}
|
|
70
|
-
payload_mapping = {
|
|
71
|
-
"location": location,
|
|
72
|
-
"force_loc": force_loc
|
|
185
|
+
"collection_type": collection_type
|
|
73
186
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
187
|
+
|
|
188
|
+
if bucket is not None:
|
|
189
|
+
payload["bucket"] = bucket
|
|
190
|
+
|
|
191
|
+
if location is not None:
|
|
192
|
+
payload["location"] = location
|
|
193
|
+
|
|
194
|
+
response, status = await self._client._terrakio_request("POST", f"collections/{collection}", json=payload)
|
|
195
|
+
|
|
196
|
+
if status != 200:
|
|
197
|
+
if status == 400:
|
|
198
|
+
raise CollectionAlreadyExistsError(f"Collection {collection} already exists", status_code=status)
|
|
199
|
+
if status == 422:
|
|
200
|
+
raise InvalidCollectionTypeError(f"Invalid collection type: {collection_type}", status_code=status)
|
|
201
|
+
raise CreateCollectionError(f"Create collection failed with status {status}", status_code=status)
|
|
202
|
+
|
|
203
|
+
return response
|
|
78
204
|
|
|
79
205
|
@require_api_key
|
|
80
|
-
async def
|
|
206
|
+
async def delete_collection(
|
|
207
|
+
self,
|
|
208
|
+
collection: str,
|
|
209
|
+
full: Optional[bool] = False,
|
|
210
|
+
outputs: Optional[list] = [],
|
|
211
|
+
data: Optional[bool] = False
|
|
212
|
+
) -> Dict[str, Any]:
|
|
81
213
|
"""
|
|
82
|
-
|
|
214
|
+
Delete a collection by name.
|
|
83
215
|
|
|
84
216
|
Args:
|
|
85
|
-
|
|
86
|
-
|
|
217
|
+
collection: The name of the collection to delete (required)
|
|
218
|
+
full: Delete the full collection (optional, defaults to False)
|
|
219
|
+
outputs: Specific output folders to delete (optional, defaults to empty list)
|
|
220
|
+
data: Whether to delete raw data (xdata folder) (optional, defaults to False)
|
|
221
|
+
|
|
87
222
|
Returns:
|
|
88
|
-
API response as a dictionary
|
|
89
|
-
|
|
223
|
+
API response as a dictionary confirming deletion
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
CollectionNotFoundError: If the collection is not found
|
|
227
|
+
DeleteCollectionError: If the API request fails due to unknown reasons
|
|
90
228
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
229
|
+
payload = {
|
|
230
|
+
"full": full,
|
|
231
|
+
"outputs": outputs,
|
|
232
|
+
"data": data
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
response, status = await self._client._terrakio_request("DELETE", f"collections/{collection}", json=payload)
|
|
236
|
+
|
|
237
|
+
if status != 200:
|
|
238
|
+
if status == 404:
|
|
239
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
240
|
+
raise DeleteCollectionError(f"Delete collection failed with status {status}", status_code=status)
|
|
241
|
+
|
|
242
|
+
return response
|
|
243
|
+
|
|
93
244
|
@require_api_key
|
|
94
|
-
def
|
|
245
|
+
async def get_collection(self, collection: str) -> Dict[str, Any]:
|
|
95
246
|
"""
|
|
96
|
-
Get
|
|
247
|
+
Get a collection by name.
|
|
97
248
|
|
|
98
249
|
Args:
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
uid: The user ID of the job
|
|
102
|
-
|
|
250
|
+
collection: The name of the collection to retrieve(required)
|
|
251
|
+
|
|
103
252
|
Returns:
|
|
104
|
-
API response as a dictionary
|
|
253
|
+
API response as a dictionary containing collection information
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
CollectionNotFoundError: If the collection is not found
|
|
257
|
+
GetCollectionError: If the API request fails due to unknown reasons
|
|
258
|
+
"""
|
|
259
|
+
response, status = await self._client._terrakio_request("GET", f"collections/{collection}")
|
|
105
260
|
|
|
261
|
+
if status != 200:
|
|
262
|
+
if status == 404:
|
|
263
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
264
|
+
raise GetCollectionError(f"Get collection failed with status {status}", status_code=status)
|
|
265
|
+
|
|
266
|
+
return response
|
|
267
|
+
|
|
268
|
+
@require_api_key
|
|
269
|
+
async def list_collections(
|
|
270
|
+
self,
|
|
271
|
+
collection_type: Optional[str] = None,
|
|
272
|
+
limit: Optional[int] = 10,
|
|
273
|
+
page: Optional[int] = 0
|
|
274
|
+
) -> List[Dict[str, Any]]:
|
|
275
|
+
"""
|
|
276
|
+
List collections for the current user.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
collection_type: Filter by collection type (optional)
|
|
280
|
+
limit: Number of collections to return (optional, defaults to 10)
|
|
281
|
+
page: Page number (optional, defaults to 0)
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
API response as a list of dictionaries containing collection information
|
|
285
|
+
|
|
106
286
|
Raises:
|
|
107
|
-
|
|
287
|
+
ListCollectionsError: If the API request fails due to unknown reasons
|
|
108
288
|
"""
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
289
|
+
params = {}
|
|
290
|
+
|
|
291
|
+
if collection_type is not None:
|
|
292
|
+
params["collection_type"] = collection_type
|
|
293
|
+
|
|
294
|
+
if limit is not None:
|
|
295
|
+
params["limit"] = limit
|
|
296
|
+
|
|
297
|
+
if page is not None:
|
|
298
|
+
params["page"] = page
|
|
299
|
+
|
|
300
|
+
response, status = await self._client._terrakio_request("GET", "collections", params=params)
|
|
301
|
+
if status != 200:
|
|
302
|
+
raise ListCollectionsError(f"List collections failed with status {status}", status_code=status)
|
|
303
|
+
|
|
304
|
+
return response
|
|
305
|
+
|
|
114
306
|
@require_api_key
|
|
115
|
-
async def
|
|
307
|
+
async def list_tasks(
|
|
308
|
+
self,
|
|
309
|
+
limit: Optional[int] = 10,
|
|
310
|
+
page: Optional[int] = 0
|
|
311
|
+
) -> List[Dict[str, Any]]:
|
|
116
312
|
"""
|
|
117
|
-
|
|
313
|
+
List tasks for the current user.
|
|
118
314
|
|
|
119
315
|
Args:
|
|
120
|
-
|
|
316
|
+
limit: Number of tasks to return (optional, defaults to 10)
|
|
317
|
+
page: Page number (optional, defaults to 0)
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
API response as a list of dictionaries containing task information
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ListTasksError: If the API request fails due to unknown reasons
|
|
324
|
+
"""
|
|
325
|
+
params = {
|
|
326
|
+
"limit": limit,
|
|
327
|
+
"page": page
|
|
328
|
+
}
|
|
329
|
+
response, status = await self._client._terrakio_request("GET", "tasks", params=params)
|
|
330
|
+
|
|
331
|
+
if status != 200:
|
|
332
|
+
raise ListTasksError(f"List tasks failed with status {status}", status_code=status)
|
|
121
333
|
|
|
334
|
+
return response
|
|
335
|
+
|
|
336
|
+
@require_api_key
|
|
337
|
+
async def upload_requests(
|
|
338
|
+
self,
|
|
339
|
+
collection: str
|
|
340
|
+
) -> Dict[str, Any]:
|
|
341
|
+
"""
|
|
342
|
+
Retrieve signed url to upload requests for a collection.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
collection: Name of collection
|
|
346
|
+
|
|
122
347
|
Returns:
|
|
123
|
-
API response as a dictionary
|
|
348
|
+
API response as a dictionary containing the upload URL
|
|
124
349
|
|
|
125
350
|
Raises:
|
|
126
|
-
|
|
351
|
+
CollectionNotFoundError: If the collection is not found
|
|
352
|
+
UploadRequestsError: If the API request fails due to unknown reasons
|
|
127
353
|
"""
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
354
|
+
response, status = await self._client._terrakio_request("GET", f"collections/{collection}/upload/requests")
|
|
355
|
+
|
|
356
|
+
if status != 200:
|
|
357
|
+
if status == 404:
|
|
358
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
359
|
+
raise UploadRequestsError(f"Upload requests failed with status {status}", status_code=status)
|
|
360
|
+
|
|
361
|
+
return response
|
|
362
|
+
|
|
131
363
|
@require_api_key
|
|
132
|
-
def
|
|
364
|
+
async def upload_artifacts(
|
|
365
|
+
self,
|
|
366
|
+
collection: str,
|
|
367
|
+
file_type: str,
|
|
368
|
+
compressed: Optional[bool] = True
|
|
369
|
+
) -> Dict[str, Any]:
|
|
133
370
|
"""
|
|
134
|
-
|
|
371
|
+
Retrieve signed url to upload artifact file to a collection.
|
|
135
372
|
|
|
136
373
|
Args:
|
|
137
|
-
|
|
138
|
-
|
|
374
|
+
collection: Name of collection
|
|
375
|
+
file_type: The extension of the file
|
|
376
|
+
compressed: Whether to compress the file using gzip or not (defaults to True)
|
|
377
|
+
|
|
139
378
|
Returns:
|
|
140
|
-
API response as a dictionary
|
|
379
|
+
API response as a dictionary containing the upload URL
|
|
141
380
|
|
|
142
381
|
Raises:
|
|
143
|
-
|
|
382
|
+
CollectionNotFoundError: If the collection is not found
|
|
383
|
+
UploadArtifactsError: If the API request fails due to unknown reasons
|
|
144
384
|
"""
|
|
145
|
-
params = {
|
|
146
|
-
|
|
147
|
-
|
|
385
|
+
params = {
|
|
386
|
+
"file_type": file_type,
|
|
387
|
+
"compressed": str(compressed).lower(),
|
|
388
|
+
}
|
|
148
389
|
|
|
149
|
-
|
|
150
|
-
|
|
390
|
+
response, status = await self._client._terrakio_request("GET", f"collections/{collection}/upload", params=params)
|
|
391
|
+
|
|
392
|
+
if status != 200:
|
|
393
|
+
if status == 404:
|
|
394
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
395
|
+
raise UploadArtifactsError(f"Upload artifacts failed with status {status}", status_code=status)
|
|
396
|
+
|
|
397
|
+
return response
|
|
398
|
+
|
|
399
|
+
@require_api_key
|
|
400
|
+
async def get_task(
|
|
151
401
|
self,
|
|
152
|
-
|
|
153
|
-
data_name: str,
|
|
154
|
-
output: str,
|
|
155
|
-
consumer: str,
|
|
156
|
-
overwrite: bool = False
|
|
402
|
+
task_id: str
|
|
157
403
|
) -> Dict[str, Any]:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
404
|
+
"""
|
|
405
|
+
Get task information by task ID.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
task_id: ID of task to track
|
|
161
409
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
410
|
+
Returns:
|
|
411
|
+
API response as a dictionary containing task information
|
|
412
|
+
|
|
413
|
+
Raises:
|
|
414
|
+
TaskNotFoundError: If the task is not found
|
|
415
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
416
|
+
"""
|
|
417
|
+
response, status = await self._client._terrakio_request("GET", f"tasks/info/{task_id}")
|
|
418
|
+
|
|
419
|
+
if status != 200:
|
|
420
|
+
if status == 404:
|
|
421
|
+
raise TaskNotFoundError(f"Task {task_id} not found", status_code=status)
|
|
422
|
+
raise GetTaskError(f"Get task failed with status {status}", status_code=status)
|
|
168
423
|
|
|
169
|
-
return
|
|
170
|
-
"POST",
|
|
171
|
-
"mass_stats/post_process",
|
|
172
|
-
data=data,
|
|
173
|
-
)
|
|
424
|
+
return response
|
|
174
425
|
|
|
175
|
-
@require_api_key
|
|
176
|
-
async def
|
|
426
|
+
@require_api_key
|
|
427
|
+
async def generate_data(
|
|
177
428
|
self,
|
|
178
|
-
|
|
429
|
+
collection: str,
|
|
179
430
|
output: str,
|
|
180
|
-
|
|
181
|
-
|
|
431
|
+
skip_existing: Optional[bool] = True,
|
|
432
|
+
force_loc: Optional[bool] = None,
|
|
433
|
+
server: Optional[str] = None
|
|
182
434
|
) -> Dict[str, Any]:
|
|
183
|
-
|
|
184
|
-
data
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
435
|
+
"""
|
|
436
|
+
Generate data for a collection.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
collection: Name of collection
|
|
440
|
+
output: Output type (str)
|
|
441
|
+
force_loc: Write data directly to the cloud under this folder
|
|
442
|
+
skip_existing: Skip existing data
|
|
443
|
+
server: Server to use
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
API response as a dictionary containing task information
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
CollectionNotFoundError: If the collection is not found
|
|
450
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
451
|
+
"""
|
|
452
|
+
payload = {"output": output, "skip_existing": skip_existing}
|
|
453
|
+
|
|
454
|
+
if force_loc is not None:
|
|
455
|
+
payload["force_loc"] = force_loc
|
|
456
|
+
if server is not None:
|
|
457
|
+
payload["server"] = server
|
|
458
|
+
|
|
459
|
+
response, status = await self._client._terrakio_request("POST", f"collections/{collection}/generate_data", json=payload)
|
|
460
|
+
|
|
461
|
+
if status != 200:
|
|
462
|
+
if status == 404:
|
|
463
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
464
|
+
raise GetTaskError(f"Generate data failed with status {status}", status_code=status)
|
|
465
|
+
|
|
466
|
+
return response
|
|
189
467
|
|
|
190
|
-
return await self._client._terrakio_request(
|
|
191
|
-
"POST",
|
|
192
|
-
"mass_stats/transform",
|
|
193
|
-
data=data,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
468
|
@require_api_key
|
|
197
|
-
def
|
|
469
|
+
async def training_samples(
|
|
198
470
|
self,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
471
|
+
collection: str,
|
|
472
|
+
expressions: list[str],
|
|
473
|
+
filters: list[str],
|
|
474
|
+
aoi: dict,
|
|
475
|
+
samples: int,
|
|
476
|
+
crs: str,
|
|
477
|
+
tile_size: int,
|
|
478
|
+
res: float,
|
|
479
|
+
output: str,
|
|
480
|
+
year_range: Optional[list[int]] = None,
|
|
481
|
+
server: Optional[str] = None
|
|
205
482
|
) -> Dict[str, Any]:
|
|
206
483
|
"""
|
|
207
|
-
|
|
484
|
+
Generate training samples for a collection.
|
|
208
485
|
|
|
209
486
|
Args:
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
487
|
+
collection: Name of collection
|
|
488
|
+
expressions: List of expressions for each sample
|
|
489
|
+
filters: Expressions to filter sample areas
|
|
490
|
+
aoi: AOI to sample from (geojson dict)
|
|
491
|
+
samples: Number of samples to generate
|
|
492
|
+
crs: CRS of AOI
|
|
493
|
+
tile_size: Pixel width and height of samples
|
|
494
|
+
res: Resolution of samples
|
|
495
|
+
output: Sample output type
|
|
496
|
+
year_range: Optional year range filter
|
|
497
|
+
server: Server to use
|
|
216
498
|
|
|
217
499
|
Returns:
|
|
218
|
-
API response as a dictionary
|
|
500
|
+
API response as a dictionary containing task information
|
|
219
501
|
|
|
220
502
|
Raises:
|
|
221
|
-
|
|
222
|
-
|
|
503
|
+
CollectionNotFoundError: If the collection is not found
|
|
504
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
223
505
|
"""
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
506
|
+
payload = {
|
|
507
|
+
"expressions": expressions,
|
|
508
|
+
"filters": filters,
|
|
509
|
+
"aoi": aoi,
|
|
510
|
+
"samples": samples,
|
|
511
|
+
"crs": crs,
|
|
512
|
+
"tile_size": tile_size,
|
|
513
|
+
"res": res,
|
|
514
|
+
"output": output
|
|
515
|
+
}
|
|
229
516
|
|
|
230
|
-
if
|
|
231
|
-
|
|
232
|
-
|
|
517
|
+
if year_range is not None:
|
|
518
|
+
payload["year_range"] = year_range
|
|
519
|
+
if server is not None:
|
|
520
|
+
payload["server"] = server
|
|
233
521
|
|
|
234
|
-
|
|
522
|
+
response, status = await self._client._terrakio_request("POST", f"collections/{collection}/training_samples", json=payload)
|
|
523
|
+
|
|
524
|
+
if status != 200:
|
|
525
|
+
if status == 404:
|
|
526
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
527
|
+
raise GetTaskError(f"Training sample failed with status {status}", status_code=status)
|
|
235
528
|
|
|
236
|
-
|
|
237
|
-
params["id"] = id
|
|
238
|
-
if force_loc is True:
|
|
239
|
-
params["force_loc"] = force_loc
|
|
240
|
-
params["bucket"] = bucket
|
|
241
|
-
params["location"] = location
|
|
242
|
-
params["output"] = output
|
|
243
|
-
|
|
244
|
-
return self._client._terrakio_request("GET", "mass_stats/download", params=params)
|
|
529
|
+
return response
|
|
245
530
|
|
|
246
|
-
@require_api_key
|
|
247
|
-
async def
|
|
248
|
-
|
|
249
|
-
|
|
531
|
+
# @require_api_key
|
|
532
|
+
# async def post_processing(
|
|
533
|
+
# self,
|
|
534
|
+
# collection: str,
|
|
535
|
+
# folder: str,
|
|
536
|
+
# consumer: str
|
|
537
|
+
# ) -> Dict[str, Any]:
|
|
538
|
+
# """
|
|
539
|
+
# Run post processing for a collection.
|
|
540
|
+
|
|
541
|
+
# Args:
|
|
542
|
+
# collection: Name of collection
|
|
543
|
+
# folder: Folder to store output
|
|
544
|
+
# consumer: Post processing script
|
|
545
|
+
|
|
546
|
+
# Returns:
|
|
547
|
+
# API response as a dictionary containing task information
|
|
548
|
+
|
|
549
|
+
# Raises:
|
|
550
|
+
# CollectionNotFoundError: If the collection is not found
|
|
551
|
+
# GetTaskError: If the API request fails due to unknown reasons
|
|
552
|
+
# """
|
|
553
|
+
# # payload = {
|
|
554
|
+
# # "folder": folder,
|
|
555
|
+
# # "consumer": consumer
|
|
556
|
+
# # }
|
|
557
|
+
# # we have the consumer as a string, we need to read in the file and then pass in the content
|
|
558
|
+
# with open(consumer, 'rb') as f:
|
|
559
|
+
# files = {
|
|
560
|
+
# 'consumer': ('consumer.py', f.read(), 'text/plain')
|
|
561
|
+
# }
|
|
562
|
+
# data = {
|
|
563
|
+
# 'folder': folder
|
|
564
|
+
# }
|
|
250
565
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
raise ValueError(f"Invalid JSON in file {file_path}: {e}")
|
|
566
|
+
# # response, status = await self._client._terrakio_request("POST", f"collections/{collection}/post_process", json=payload)
|
|
567
|
+
# response, status = await self._client._terrakio_request(
|
|
568
|
+
# "POST",
|
|
569
|
+
# f"collections/{collection}/post_process",
|
|
570
|
+
# files=files,
|
|
571
|
+
# data=data
|
|
572
|
+
# )
|
|
573
|
+
# if status != 200:
|
|
574
|
+
# if status == 404:
|
|
575
|
+
# raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
576
|
+
# raise GetTaskError(f"Post processing failed with status {status}", status_code=status)
|
|
263
577
|
|
|
264
|
-
|
|
578
|
+
# return response
|
|
579
|
+
|
|
265
580
|
|
|
266
581
|
@require_api_key
|
|
267
|
-
async def
|
|
582
|
+
async def post_processing(
|
|
583
|
+
self,
|
|
584
|
+
collection: str,
|
|
585
|
+
folder: str,
|
|
586
|
+
consumer: str
|
|
587
|
+
) -> Dict[str, Any]:
|
|
268
588
|
"""
|
|
269
|
-
|
|
270
|
-
|
|
589
|
+
Run post processing for a collection.
|
|
590
|
+
|
|
271
591
|
Args:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
592
|
+
collection: Name of collection
|
|
593
|
+
folder: Folder to store output
|
|
594
|
+
consumer: Path to post processing script
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
API response as a dictionary containing task information
|
|
598
|
+
|
|
599
|
+
Raises:
|
|
600
|
+
CollectionNotFoundError: If the collection is not found
|
|
601
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
275
602
|
"""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
'
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
603
|
+
# Read file and build multipart form data
|
|
604
|
+
with open(consumer, 'rb') as f:
|
|
605
|
+
form = aiohttp.FormData()
|
|
606
|
+
form.add_field('folder', folder) # Add text field
|
|
607
|
+
form.add_field(
|
|
608
|
+
'consumer', # Field name
|
|
609
|
+
f.read(), # File content
|
|
610
|
+
filename='consumer.py', # Filename
|
|
611
|
+
content_type='text/x-python' # MIME type
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# Send using data= with FormData object (NOT files=)
|
|
615
|
+
response, status = await self._client._terrakio_request(
|
|
616
|
+
"POST",
|
|
617
|
+
f"collections/{collection}/post_process",
|
|
618
|
+
data=form # ✅ Pass FormData as data
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if status != 200:
|
|
622
|
+
if status == 404:
|
|
623
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
624
|
+
raise GetTaskError(f"Post processing failed with status {status}", status_code=status)
|
|
292
625
|
|
|
293
|
-
response = await self._client._regular_request("PUT", url, data=body, headers=headers)
|
|
294
626
|
return response
|
|
295
|
-
|
|
627
|
+
|
|
296
628
|
@require_api_key
|
|
297
|
-
async def
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
""
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
Args:
|
|
309
|
-
job_name: Name of the job
|
|
310
|
-
download_all: Whether to download all raw files from the job
|
|
311
|
-
file_type: either 'raw' or 'processed'
|
|
312
|
-
current_page: Current page number for pagination
|
|
313
|
-
page_size: Number of file per page for download
|
|
314
|
-
output_path: Path where the file should be saved
|
|
315
|
-
|
|
316
|
-
Returns:
|
|
317
|
-
str: Path to the downloaded file
|
|
629
|
+
async def zonal_stats(
|
|
630
|
+
self,
|
|
631
|
+
collection: str,
|
|
632
|
+
id_property: str,
|
|
633
|
+
column_name: str,
|
|
634
|
+
expr: str,
|
|
635
|
+
resolution: Optional[int] = 1,
|
|
636
|
+
in_crs: Optional[str] = "epsg:4326",
|
|
637
|
+
out_crs: Optional[str] = "epsg:4326"
|
|
638
|
+
) -> Dict[str, Any]:
|
|
318
639
|
"""
|
|
640
|
+
Run zonal stats over uploaded geojson collection.
|
|
319
641
|
|
|
642
|
+
Args:
|
|
643
|
+
collection: Name of collection
|
|
644
|
+
id_property: Property key in geojson to use as id
|
|
645
|
+
column_name: Name of new column to add
|
|
646
|
+
expr: Terrak.io expression to evaluate
|
|
647
|
+
resolution: Resolution of request (optional, defaults to 1)
|
|
648
|
+
in_crs: CRS of geojson (optional, defaults to "epsg:4326")
|
|
649
|
+
out_crs: Desired output CRS (optional, defaults to "epsg:4326")
|
|
320
650
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if file_type == "raw" and page_size is None:
|
|
325
|
-
raise ValueError("page_size is required to define pagination size when downloading raw files.")
|
|
651
|
+
Returns:
|
|
652
|
+
API response as a dictionary containing task information
|
|
326
653
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
654
|
+
Raises:
|
|
655
|
+
CollectionNotFoundError: If the collection is not found
|
|
656
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
657
|
+
"""
|
|
658
|
+
payload = {
|
|
659
|
+
"id_property": id_property,
|
|
660
|
+
"column_name": column_name,
|
|
661
|
+
"expr": expr,
|
|
662
|
+
"resolution": resolution,
|
|
663
|
+
"in_crs": in_crs,
|
|
664
|
+
"out_crs": out_crs
|
|
332
665
|
}
|
|
666
|
+
|
|
667
|
+
response, status = await self._client._terrakio_request("POST", f"collections/{collection}/zonal_stats", json=payload)
|
|
333
668
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
subpath = Path(path_parts[-1])
|
|
347
|
-
file_save_path = output_dir / subpath
|
|
348
|
-
file_save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
349
|
-
self._client.logger.info(f"Downloading file to {file_save_path} ({i+1}/{len(download_urls)})")
|
|
350
|
-
|
|
351
|
-
async with session.get(url) as resp:
|
|
352
|
-
resp.raise_for_status()
|
|
353
|
-
import aiofiles
|
|
354
|
-
async with aiofiles.open(file_save_path, 'wb') as file:
|
|
355
|
-
async for chunk in resp.content.iter_chunked(1048576): # 1 MB
|
|
356
|
-
if chunk:
|
|
357
|
-
await file.write(chunk)
|
|
358
|
-
|
|
359
|
-
if not os.path.exists(file_save_path):
|
|
360
|
-
raise Exception(f"File was not written to {file_save_path}")
|
|
361
|
-
|
|
362
|
-
file_size = os.path.getsize(file_save_path)
|
|
363
|
-
self._client.logger.info(f"File downloaded successfully to {file_save_path} (size: {file_size / (1024 * 1024):.4f} mb)")
|
|
364
|
-
output_files.append(str(file_save_path))
|
|
365
|
-
|
|
366
|
-
try:
|
|
367
|
-
page = 1
|
|
368
|
-
total_files = None
|
|
369
|
-
downloaded_files = 0
|
|
370
|
-
async with aiohttp.ClientSession() as session:
|
|
371
|
-
while True:
|
|
372
|
-
params = {
|
|
373
|
-
"page": page,
|
|
374
|
-
"page_size": page_size
|
|
375
|
-
}
|
|
376
|
-
response = await self._client._terrakio_request("POST", "mass_stats/download_files", json=request_body, params=params)
|
|
377
|
-
data = response
|
|
378
|
-
|
|
379
|
-
download_urls = data.get('download_urls')
|
|
380
|
-
if not download_urls:
|
|
381
|
-
break
|
|
382
|
-
await download_urls_batch(download_urls, session)
|
|
383
|
-
if total_files is None:
|
|
384
|
-
total_files = data.get('subdir_total_files')
|
|
385
|
-
downloaded_files += len(download_urls)
|
|
386
|
-
if total_files is not None and downloaded_files >= total_files:
|
|
387
|
-
break
|
|
388
|
-
if len(download_urls) < page_size:
|
|
389
|
-
break # Last page
|
|
390
|
-
page += 1
|
|
391
|
-
return output_files
|
|
392
|
-
except Exception as e:
|
|
393
|
-
raise Exception(f"Error in download process: {e}")
|
|
394
|
-
|
|
395
|
-
def validate_request(self, request_json: Union[str, List[Dict]]):
|
|
396
|
-
# Handle both file path and direct JSON data
|
|
397
|
-
if isinstance(request_json, str):
|
|
398
|
-
# It's a file path
|
|
399
|
-
with open(request_json, 'r') as file:
|
|
400
|
-
request_data = json.load(file)
|
|
401
|
-
elif isinstance(request_json, list):
|
|
402
|
-
# It's already JSON data
|
|
403
|
-
request_data = request_json
|
|
404
|
-
else:
|
|
405
|
-
raise ValueError("request_json must be either a file path (str) or JSON data (list)")
|
|
406
|
-
|
|
407
|
-
# Rest of validation logic stays exactly the same
|
|
408
|
-
if not isinstance(request_data, list):
|
|
409
|
-
raise ValueError("Request JSON should contain a list of dictionaries")
|
|
410
|
-
|
|
411
|
-
for i, request in enumerate(request_data):
|
|
412
|
-
if not isinstance(request, dict):
|
|
413
|
-
raise ValueError(f"Request {i} should be a dictionary")
|
|
414
|
-
required_keys = ["request", "group", "file"]
|
|
415
|
-
for key in required_keys:
|
|
416
|
-
if key not in request:
|
|
417
|
-
raise ValueError(f"Request {i} should contain {key}")
|
|
418
|
-
try:
|
|
419
|
-
str(request["group"])
|
|
420
|
-
except ValueError:
|
|
421
|
-
ValueError("Group must be string or convertible to string")
|
|
422
|
-
if not isinstance(request["request"], dict):
|
|
423
|
-
raise ValueError("Request must be a dictionary")
|
|
424
|
-
if not isinstance(request["file"], (str, int, list)):
|
|
425
|
-
raise ValueError("'file' must be a string or a list of strings")
|
|
426
|
-
if i == 3:
|
|
427
|
-
break
|
|
428
|
-
|
|
429
|
-
async def execute_job(
|
|
430
|
-
self,
|
|
431
|
-
name: str,
|
|
432
|
-
output: str,
|
|
433
|
-
config: Dict[str, Any],
|
|
434
|
-
request_json: Union[str, List[Dict]], # ← Accept both file path OR data
|
|
435
|
-
region: str = None,
|
|
436
|
-
overwrite: bool = False,
|
|
437
|
-
skip_existing: bool = False,
|
|
438
|
-
location: str = None,
|
|
439
|
-
force_loc: bool = None,
|
|
440
|
-
server: str = None
|
|
669
|
+
if status != 200:
|
|
670
|
+
if status == 404:
|
|
671
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
672
|
+
raise GetTaskError(f"Zonal stats failed with status {status}", status_code=status)
|
|
673
|
+
|
|
674
|
+
return response
|
|
675
|
+
|
|
676
|
+
@require_api_key
|
|
677
|
+
async def zonal_stats_transform(
|
|
678
|
+
self,
|
|
679
|
+
collection: str,
|
|
680
|
+
consumer: str
|
|
441
681
|
) -> Dict[str, Any]:
|
|
442
682
|
"""
|
|
443
|
-
|
|
444
|
-
|
|
683
|
+
Transform raw data in collection. Creates a new collection.
|
|
684
|
+
|
|
445
685
|
Args:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
request_json: Path to the request JSON file
|
|
450
|
-
overwrite: Whether to overwrite the job
|
|
451
|
-
skip_existing: Whether to skip existing jobs
|
|
452
|
-
location: The location of the job
|
|
453
|
-
force_loc: Whether to force the location
|
|
454
|
-
server: The server to use
|
|
455
|
-
|
|
686
|
+
collection: Name of collection
|
|
687
|
+
consumer: Post processing script (file path or script content)
|
|
688
|
+
|
|
456
689
|
Returns:
|
|
457
|
-
API response as a dictionary
|
|
458
|
-
|
|
690
|
+
API response as a dictionary containing task information
|
|
691
|
+
|
|
459
692
|
Raises:
|
|
460
|
-
|
|
693
|
+
CollectionNotFoundError: If the collection is not found
|
|
694
|
+
GetTaskError: If the API request fails due to unknown reasons
|
|
461
695
|
"""
|
|
696
|
+
if os.path.isfile(consumer):
|
|
697
|
+
with open(consumer, 'r') as f:
|
|
698
|
+
script_content = f.read()
|
|
699
|
+
else:
|
|
700
|
+
script_content = consumer
|
|
701
|
+
|
|
702
|
+
files = {
|
|
703
|
+
'consumer': ('script.py', script_content, 'text/plain')
|
|
704
|
+
}
|
|
462
705
|
|
|
463
|
-
|
|
464
|
-
""
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
for item in request_data:
|
|
469
|
-
if not isinstance(item, dict):
|
|
470
|
-
raise ValueError("Each item in request JSON should be a dictionary")
|
|
471
|
-
|
|
472
|
-
if 'group' not in item:
|
|
473
|
-
raise ValueError("Each item should have a 'group' field")
|
|
474
|
-
|
|
475
|
-
group = item['group']
|
|
476
|
-
if group not in seen_groups:
|
|
477
|
-
groups.append(group)
|
|
478
|
-
seen_groups.add(group)
|
|
479
|
-
|
|
480
|
-
return groups
|
|
481
|
-
|
|
482
|
-
# # Load and validate request JSON
|
|
483
|
-
# try:
|
|
484
|
-
# with open(request_json, 'r') as file:
|
|
485
|
-
# request_data = json.load(file)
|
|
486
|
-
# if isinstance(request_data, list):
|
|
487
|
-
# size = len(request_data)
|
|
488
|
-
# else:
|
|
489
|
-
# raise ValueError(f"Request JSON file {request_json} should contain a list of dictionaries")
|
|
490
|
-
# except FileNotFoundError as e:
|
|
491
|
-
# return e
|
|
492
|
-
# except json.JSONDecodeError as e:
|
|
493
|
-
# return e
|
|
494
|
-
try:
|
|
495
|
-
if isinstance(request_json, str):
|
|
496
|
-
# It's a file path
|
|
497
|
-
with open(request_json, 'r') as file:
|
|
498
|
-
request_data = json.load(file)
|
|
499
|
-
elif isinstance(request_json, list):
|
|
500
|
-
# It's already JSON data
|
|
501
|
-
request_data = request_json
|
|
502
|
-
else:
|
|
503
|
-
raise ValueError("request_json must be either a file path (str) or JSON data (list)")
|
|
504
|
-
|
|
505
|
-
if isinstance(request_data, list):
|
|
506
|
-
size = len(request_data)
|
|
507
|
-
else:
|
|
508
|
-
raise ValueError("Request JSON should contain a list of dictionaries")
|
|
509
|
-
except FileNotFoundError as e:
|
|
510
|
-
return e
|
|
511
|
-
except json.JSONDecodeError as e:
|
|
512
|
-
return e
|
|
513
|
-
|
|
514
|
-
# Generate manifest from request data (kept in memory)
|
|
515
|
-
try:
|
|
516
|
-
manifest_groups = extract_manifest_from_request(request_data)
|
|
517
|
-
except Exception as e:
|
|
518
|
-
raise ValueError(f"Error extracting manifest from request JSON: {e}")
|
|
519
|
-
|
|
520
|
-
# Extract the first expression
|
|
521
|
-
first_request = request_data[0] # Changed from data[0] to request_data[0]
|
|
522
|
-
first_expression = first_request["request"]["expr"]
|
|
523
|
-
|
|
524
|
-
# Get upload URLs
|
|
525
|
-
upload_result = await self._upload_request(
|
|
526
|
-
name=name,
|
|
527
|
-
size=size,
|
|
528
|
-
region=region,
|
|
529
|
-
sample = first_expression,
|
|
530
|
-
output=output,
|
|
531
|
-
config=config,
|
|
532
|
-
location=location,
|
|
533
|
-
force_loc=force_loc,
|
|
534
|
-
overwrite=overwrite,
|
|
535
|
-
server=server,
|
|
536
|
-
skip_existing=skip_existing
|
|
706
|
+
response, status = await self._client._terrakio_request(
|
|
707
|
+
"POST",
|
|
708
|
+
f"collections/{collection}/transform",
|
|
709
|
+
files=files
|
|
537
710
|
)
|
|
711
|
+
|
|
712
|
+
if status != 200:
|
|
713
|
+
if status == 404:
|
|
714
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
715
|
+
raise GetTaskError(f"Transform failed with status {status}", status_code=status)
|
|
538
716
|
|
|
539
|
-
|
|
540
|
-
manifest_url = upload_result.get('manifest_url')
|
|
541
|
-
|
|
542
|
-
if not requests_url:
|
|
543
|
-
raise ValueError("No requests_url returned from server for request JSON upload")
|
|
544
|
-
|
|
545
|
-
# Upload request JSON file
|
|
546
|
-
try:
|
|
547
|
-
self.validate_request(request_json)
|
|
548
|
-
|
|
549
|
-
if isinstance(request_json, str):
|
|
550
|
-
# File path - use existing _upload_file method
|
|
551
|
-
requests_response = await self._upload_file(request_json, requests_url, use_gzip=True)
|
|
552
|
-
else:
|
|
553
|
-
# JSON data - use _upload_json_data method
|
|
554
|
-
requests_response = await self._upload_json_data(request_json, requests_url, use_gzip=True)
|
|
555
|
-
|
|
556
|
-
if requests_response.status not in [200, 201, 204]:
|
|
557
|
-
# ... rest stays the same
|
|
558
|
-
self._client.logger.error(f"Requests upload error: {requests_response.text()}")
|
|
559
|
-
raise Exception(f"Failed to upload request JSON: {requests_response.text()}")
|
|
560
|
-
except Exception as e:
|
|
561
|
-
raise Exception(f"Error uploading request JSON file {request_json}: {e}")
|
|
562
|
-
|
|
563
|
-
if not manifest_url:
|
|
564
|
-
raise ValueError("No manifest_url returned from server for manifest JSON upload")
|
|
565
|
-
|
|
566
|
-
# Upload manifest JSON data directly (no temporary file needed)
|
|
567
|
-
try:
|
|
568
|
-
manifest_response = await self._upload_json_data(manifest_groups, manifest_url, use_gzip=False)
|
|
569
|
-
if manifest_response.status not in [200, 201, 204]:
|
|
570
|
-
self._client.logger.error(f"Manifest upload error: {manifest_response.text()}")
|
|
571
|
-
raise Exception(f"Failed to upload manifest JSON: {manifest_response.text()}")
|
|
572
|
-
except Exception as e:
|
|
573
|
-
raise Exception(f"Error uploading manifest JSON: {e}")
|
|
574
|
-
|
|
575
|
-
# Start the job
|
|
576
|
-
start_job_task_id = await self.start_job(upload_result.get("id"))
|
|
577
|
-
return start_job_task_id
|
|
717
|
+
return response
|
|
578
718
|
|
|
579
719
|
@require_api_key
|
|
580
|
-
def
|
|
720
|
+
async def download_files(
|
|
721
|
+
self,
|
|
722
|
+
collection: str,
|
|
723
|
+
file_type: str,
|
|
724
|
+
page: Optional[int] = 0,
|
|
725
|
+
page_size: Optional[int] = 100,
|
|
726
|
+
folder: Optional[str] = None
|
|
727
|
+
) -> Dict[str, Any]:
|
|
581
728
|
"""
|
|
582
|
-
|
|
583
|
-
|
|
729
|
+
Get list of signed urls to download files in collection.
|
|
730
|
+
|
|
584
731
|
Args:
|
|
585
|
-
|
|
586
|
-
|
|
732
|
+
collection: Name of collection
|
|
733
|
+
file_type: Whether to return raw or processed (after post processing) files
|
|
734
|
+
page: Page number (optional, defaults to 0)
|
|
735
|
+
page_size: Number of files to return per page (optional, defaults to 100)
|
|
736
|
+
folder: If processed file type, which folder to download files from (optional)
|
|
737
|
+
|
|
587
738
|
Returns:
|
|
588
|
-
API response as a dictionary
|
|
589
|
-
|
|
739
|
+
API response as a dictionary containing list of download URLs
|
|
740
|
+
|
|
590
741
|
Raises:
|
|
591
|
-
|
|
742
|
+
CollectionNotFoundError: If the collection is not found
|
|
743
|
+
DownloadFilesError: If the API request fails due to unknown reasons
|
|
592
744
|
"""
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
@require_api_key
|
|
596
|
-
def cancel_all_jobs(self) -> Dict[str, Any]:
|
|
597
|
-
"""
|
|
598
|
-
Cancel all mass stats jobs.
|
|
745
|
+
params = {"file_type": file_type}
|
|
599
746
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
747
|
+
if page is not None:
|
|
748
|
+
params["page"] = page
|
|
749
|
+
if page_size is not None:
|
|
750
|
+
params["page_size"] = page_size
|
|
751
|
+
if folder is not None:
|
|
752
|
+
params["folder"] = folder
|
|
753
|
+
|
|
754
|
+
response, status = await self._client._terrakio_request("GET", f"collections/{collection}/download", params=params)
|
|
755
|
+
|
|
756
|
+
if status != 200:
|
|
757
|
+
if status == 404:
|
|
758
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
759
|
+
raise DownloadFilesError(f"Download files failed with status {status}", status_code=status)
|
|
760
|
+
|
|
761
|
+
return response
|
|
762
|
+
|
|
608
763
|
@require_api_key
|
|
609
|
-
async def
|
|
764
|
+
async def cancel_task(
|
|
610
765
|
self,
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
aoi: dict,
|
|
614
|
-
samples: int,
|
|
615
|
-
crs: str,
|
|
616
|
-
tile_size: int,
|
|
617
|
-
res: float,
|
|
618
|
-
output: str,
|
|
619
|
-
year_range: list[int] = None,
|
|
620
|
-
overwrite: bool = False,
|
|
621
|
-
server: str = None,
|
|
622
|
-
bucket: str = None
|
|
623
|
-
) -> Dict[str, Any]:
|
|
766
|
+
task_id: str
|
|
767
|
+
):
|
|
624
768
|
"""
|
|
625
|
-
|
|
769
|
+
Cancel a task by task ID.
|
|
626
770
|
|
|
627
771
|
Args:
|
|
628
|
-
|
|
629
|
-
config: The config of the job
|
|
630
|
-
aoi: The AOI of the job
|
|
631
|
-
samples: The number of samples to take
|
|
632
|
-
crs: The CRS of the job
|
|
633
|
-
tile_size: The tile size of the job
|
|
634
|
-
res: The resolution of the job
|
|
635
|
-
output: The output of the job
|
|
636
|
-
year_range: The year range of the job
|
|
637
|
-
overwrite: Whether to overwrite the job
|
|
638
|
-
server: The server to use
|
|
639
|
-
bucket: The bucket to use
|
|
772
|
+
task_id: ID of task to cancel
|
|
640
773
|
|
|
641
774
|
Returns:
|
|
642
|
-
API response as a dictionary
|
|
643
|
-
|
|
644
|
-
Raises:
|
|
645
|
-
|
|
775
|
+
API response as a dictionary containing task information
|
|
776
|
+
|
|
777
|
+
Raises:
|
|
778
|
+
TaskNotFoundError: If the task is not found
|
|
779
|
+
CancelTaskError: If the API request fails due to unknown reasons
|
|
646
780
|
"""
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
"
|
|
652
|
-
"crs": crs,
|
|
653
|
-
"tile_size": tile_size,
|
|
654
|
-
"res": res,
|
|
655
|
-
"output": output,
|
|
656
|
-
"overwrite": str(overwrite).lower(),
|
|
657
|
-
}
|
|
658
|
-
payload_mapping = {
|
|
659
|
-
"year_range": year_range,
|
|
660
|
-
"server": server,
|
|
661
|
-
"bucket": bucket,
|
|
662
|
-
}
|
|
663
|
-
for key, value in payload_mapping.items():
|
|
664
|
-
if value is not None:
|
|
665
|
-
payload[key] = value
|
|
666
|
-
return await self._client._terrakio_request("POST", "random_sample", json=payload)
|
|
781
|
+
response, status = await self._client._terrakio_request("POST", f"tasks/cancel/{task_id}")
|
|
782
|
+
if status != 200:
|
|
783
|
+
if status == 404:
|
|
784
|
+
raise TaskNotFoundError(f"Task {task_id} not found", status_code=status)
|
|
785
|
+
raise CancelTaskError(f"Cancel task failed with status {status}", status_code=status)
|
|
667
786
|
|
|
787
|
+
return response
|
|
668
788
|
|
|
669
789
|
@require_api_key
|
|
670
|
-
def
|
|
790
|
+
async def cancel_collection_tasks(
|
|
791
|
+
self,
|
|
792
|
+
collection: str
|
|
793
|
+
):
|
|
671
794
|
"""
|
|
672
|
-
|
|
795
|
+
Cancel all tasks for a collection.
|
|
673
796
|
|
|
674
797
|
Args:
|
|
675
|
-
|
|
676
|
-
levels: The levels of the pyramids
|
|
677
|
-
config: The config of the job
|
|
798
|
+
collection: Name of collection
|
|
678
799
|
|
|
679
800
|
Returns:
|
|
680
|
-
API response as a dictionary
|
|
801
|
+
API response as a dictionary containing task information for the collection
|
|
802
|
+
|
|
803
|
+
Raises:
|
|
804
|
+
CollectionNotFoundError: If the collection is not found
|
|
805
|
+
CancelCollectionTasksError: If the API request fails due to unknown reasons
|
|
681
806
|
"""
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
807
|
+
|
|
808
|
+
response, status = await self._client._terrakio_request("POST", f"collections/{collection}/cancel")
|
|
809
|
+
if status != 200:
|
|
810
|
+
if status == 404:
|
|
811
|
+
raise CollectionNotFoundError(f"Collection {collection} not found", status_code=status)
|
|
812
|
+
raise CancelCollectionTasksError(f"Cancel collection tasks failed with status {status}", status_code=status)
|
|
688
813
|
|
|
814
|
+
return response
|
|
815
|
+
|
|
689
816
|
@require_api_key
|
|
690
|
-
async def
|
|
817
|
+
async def cancel_all_tasks(
|
|
818
|
+
self
|
|
819
|
+
):
|
|
691
820
|
"""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
Args:
|
|
695
|
-
data_name: The name of the dataset
|
|
696
|
-
overwrite: Whether to overwrite the dataset
|
|
697
|
-
output: The output of the dataset
|
|
821
|
+
Cancel all tasks for the current user.
|
|
698
822
|
|
|
699
823
|
Returns:
|
|
700
|
-
API response as a dictionary
|
|
824
|
+
API response as a dictionary containing task information for all tasks
|
|
701
825
|
|
|
702
826
|
Raises:
|
|
703
|
-
|
|
827
|
+
CancelAllTasksError: If the API request fails due to unknown reasons
|
|
704
828
|
"""
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return await self._client._terrakio_request("POST", "mass_stats/combine_tiles", json=payload)
|
|
829
|
+
|
|
830
|
+
response, status = await self._client._terrakio_request("POST", "tasks/cancel")
|
|
831
|
+
|
|
832
|
+
if status != 200:
|
|
833
|
+
raise CancelAllTasksError(f"Cancel all tasks failed with status {status}", status_code=status)
|
|
834
|
+
|
|
835
|
+
return response
|