kleinkram 0.36.2.dev20241107120238__py3-none-any.whl → 0.36.2.dev20241118065826__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 kleinkram might be problematic. Click here for more details.
- kleinkram/__init__.py +6 -0
- kleinkram/__main__.py +6 -0
- kleinkram/_version.py +6 -0
- kleinkram/api/__init__.py +0 -0
- kleinkram/api/client.py +65 -0
- kleinkram/api/file_transfer.py +328 -0
- kleinkram/api/routes.py +460 -0
- kleinkram/app.py +180 -0
- kleinkram/auth.py +96 -0
- kleinkram/commands/__init__.py +1 -0
- kleinkram/commands/download.py +103 -0
- kleinkram/commands/endpoint.py +62 -0
- kleinkram/commands/list.py +93 -0
- kleinkram/commands/mission.py +57 -0
- kleinkram/commands/project.py +24 -0
- kleinkram/commands/upload.py +138 -0
- kleinkram/commands/verify.py +117 -0
- kleinkram/config.py +171 -0
- kleinkram/consts.py +8 -1
- kleinkram/core.py +14 -0
- kleinkram/enums.py +10 -0
- kleinkram/errors.py +59 -0
- kleinkram/main.py +6 -484
- kleinkram/models.py +186 -0
- kleinkram/utils.py +179 -0
- {kleinkram-0.36.2.dev20241107120238.dist-info/licenses → kleinkram-0.36.2.dev20241118065826.dist-info}/LICENSE +1 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/METADATA +113 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/RECORD +33 -0
- {kleinkram-0.36.2.dev20241107120238.dist-info → kleinkram-0.36.2.dev20241118065826.dist-info}/WHEEL +2 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/entry_points.txt +2 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_utils.py +153 -0
- kleinkram/api_client.py +0 -63
- kleinkram/auth/auth.py +0 -160
- kleinkram/endpoint/endpoint.py +0 -58
- kleinkram/error_handling.py +0 -177
- kleinkram/file/file.py +0 -144
- kleinkram/helper.py +0 -272
- kleinkram/mission/mission.py +0 -310
- kleinkram/project/project.py +0 -138
- kleinkram/queue/queue.py +0 -8
- kleinkram/tag/tag.py +0 -71
- kleinkram/topic/topic.py +0 -55
- kleinkram/user/user.py +0 -75
- kleinkram-0.36.2.dev20241107120238.dist-info/METADATA +0 -25
- kleinkram-0.36.2.dev20241107120238.dist-info/RECORD +0 -20
- kleinkram-0.36.2.dev20241107120238.dist-info/entry_points.txt +0 -2
kleinkram/helper.py
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import glob
|
|
2
|
-
import os
|
|
3
|
-
import queue
|
|
4
|
-
import re
|
|
5
|
-
import sys
|
|
6
|
-
import threading
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from functools import partial
|
|
9
|
-
|
|
10
|
-
import boto3
|
|
11
|
-
import tqdm
|
|
12
|
-
import typer
|
|
13
|
-
from boto3.s3.transfer import TransferConfig
|
|
14
|
-
from botocore.config import Config
|
|
15
|
-
from botocore.utils import calculate_md5
|
|
16
|
-
from rich import print
|
|
17
|
-
from rich.console import Console
|
|
18
|
-
from typing_extensions import Dict
|
|
19
|
-
|
|
20
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class TransferCallback:
|
|
24
|
-
"""
|
|
25
|
-
Handle callbacks from the transfer manager.
|
|
26
|
-
|
|
27
|
-
The transfer manager periodically calls the __call__ method throughout
|
|
28
|
-
the upload process so that it can take action, such as displaying progress
|
|
29
|
-
to the user and collecting data about the transfer.
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
def __init__(self):
|
|
33
|
-
"""
|
|
34
|
-
Initialize the TransferCallback.
|
|
35
|
-
|
|
36
|
-
This initializes an empty dictionary to hold progress bars for each file.
|
|
37
|
-
"""
|
|
38
|
-
self._lock = threading.Lock()
|
|
39
|
-
self.file_progress = {}
|
|
40
|
-
|
|
41
|
-
def add_file(self, file_id, target_size):
|
|
42
|
-
"""
|
|
43
|
-
Add a new file to track.
|
|
44
|
-
|
|
45
|
-
:param file_id: A unique identifier for the file (e.g., file name or ID).
|
|
46
|
-
:param target_size: The total size of the file being transferred.
|
|
47
|
-
"""
|
|
48
|
-
with self._lock:
|
|
49
|
-
tqdm_instance = tqdm.tqdm(
|
|
50
|
-
total=target_size,
|
|
51
|
-
unit="B",
|
|
52
|
-
unit_scale=True,
|
|
53
|
-
desc=f"Uploading {file_id}",
|
|
54
|
-
)
|
|
55
|
-
self.file_progress[file_id] = {
|
|
56
|
-
"tqdm": tqdm_instance,
|
|
57
|
-
"total_transferred": 0,
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
def __call__(self, file_id, bytes_transferred):
|
|
61
|
-
"""
|
|
62
|
-
The callback method that is called by the transfer manager.
|
|
63
|
-
|
|
64
|
-
Display progress during file transfer and collect per-thread transfer
|
|
65
|
-
data. This method can be called by multiple threads, so shared instance
|
|
66
|
-
data is protected by a thread lock.
|
|
67
|
-
|
|
68
|
-
:param file_id: The identifier of the file being transferred.
|
|
69
|
-
:param bytes_transferred: The number of bytes transferred in this call.
|
|
70
|
-
"""
|
|
71
|
-
with self._lock:
|
|
72
|
-
if file_id in self.file_progress:
|
|
73
|
-
progress = self.file_progress[file_id]
|
|
74
|
-
progress["total_transferred"] += bytes_transferred
|
|
75
|
-
|
|
76
|
-
# Update tqdm progress bar
|
|
77
|
-
progress["tqdm"].update(bytes_transferred)
|
|
78
|
-
|
|
79
|
-
def close(self):
|
|
80
|
-
"""Close all tqdm progress bars."""
|
|
81
|
-
with self._lock:
|
|
82
|
-
for progress in self.file_progress.values():
|
|
83
|
-
progress["tqdm"].close()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def create_transfer_callback(callback_instance, file_id):
|
|
87
|
-
"""
|
|
88
|
-
Factory function to create a partial function for TransferCallback.
|
|
89
|
-
:param callback_instance: Instance of TransferCallback.
|
|
90
|
-
:param file_id: The unique identifier for the file.
|
|
91
|
-
:return: A callable that can be passed as a callback to boto3's upload_file method.
|
|
92
|
-
"""
|
|
93
|
-
return partial(callback_instance.__call__, file_id)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def expand_and_match(path_pattern):
|
|
97
|
-
expanded_path = os.path.expanduser(path_pattern)
|
|
98
|
-
expanded_path = os.path.expandvars(expanded_path)
|
|
99
|
-
|
|
100
|
-
normalized_path = os.path.normpath(expanded_path)
|
|
101
|
-
|
|
102
|
-
if "**" in normalized_path:
|
|
103
|
-
file_list = glob.glob(normalized_path, recursive=True)
|
|
104
|
-
else:
|
|
105
|
-
file_list = glob.glob(normalized_path)
|
|
106
|
-
|
|
107
|
-
return file_list
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def uploadFiles(
|
|
111
|
-
files_with_access: Dict[str, object], paths: Dict[str, str], nrThreads: int
|
|
112
|
-
):
|
|
113
|
-
client = AuthenticatedClient()
|
|
114
|
-
|
|
115
|
-
api_endpoint = client.tokenfile.endpoint
|
|
116
|
-
if api_endpoint == "http://localhost:3000":
|
|
117
|
-
minio_endpoint = "http://localhost:9000"
|
|
118
|
-
else:
|
|
119
|
-
minio_endpoint = api_endpoint.replace("api", "minio")
|
|
120
|
-
|
|
121
|
-
_queue = queue.Queue()
|
|
122
|
-
for file_with_access in files_with_access:
|
|
123
|
-
_queue.put((file_with_access, str(paths[file_with_access["fileName"]])))
|
|
124
|
-
|
|
125
|
-
threads = []
|
|
126
|
-
transfer_callback = TransferCallback()
|
|
127
|
-
|
|
128
|
-
for i in range(nrThreads):
|
|
129
|
-
thread = threading.Thread(
|
|
130
|
-
target=uploadFile,
|
|
131
|
-
args=(_queue, minio_endpoint, transfer_callback),
|
|
132
|
-
)
|
|
133
|
-
thread.start()
|
|
134
|
-
threads.append(thread)
|
|
135
|
-
for thread in threads:
|
|
136
|
-
thread.join()
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def uploadFile(
|
|
140
|
-
_queue: queue.Queue,
|
|
141
|
-
minio_endpoint: str,
|
|
142
|
-
transfer_callback: TransferCallback,
|
|
143
|
-
):
|
|
144
|
-
config = Config(retries={"max_attempts": 10, "mode": "standard"})
|
|
145
|
-
|
|
146
|
-
while True:
|
|
147
|
-
try:
|
|
148
|
-
file_with_access, filepath = _queue.get(timeout=3)
|
|
149
|
-
|
|
150
|
-
if "error" in file_with_access and (
|
|
151
|
-
file_with_access["error"] is not None or file_with_access["error"] != ""
|
|
152
|
-
):
|
|
153
|
-
console = Console(file=sys.stderr, style="red", highlight=False)
|
|
154
|
-
console.print(
|
|
155
|
-
f"Error uploading file: {file_with_access['fileName']} ({filepath}): {file_with_access['error']}"
|
|
156
|
-
)
|
|
157
|
-
_queue.task_done()
|
|
158
|
-
continue
|
|
159
|
-
|
|
160
|
-
access_key = file_with_access["accessCredentials"]["accessKey"]
|
|
161
|
-
secret_key = file_with_access["accessCredentials"]["secretKey"]
|
|
162
|
-
session_token = file_with_access["accessCredentials"]["sessionToken"]
|
|
163
|
-
|
|
164
|
-
session = boto3.Session(
|
|
165
|
-
aws_access_key_id=access_key,
|
|
166
|
-
aws_secret_access_key=secret_key,
|
|
167
|
-
aws_session_token=session_token,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
|
|
171
|
-
|
|
172
|
-
fileu_uid = file_with_access["fileUUID"]
|
|
173
|
-
bucket = file_with_access["bucket"]
|
|
174
|
-
|
|
175
|
-
transfer_config = TransferConfig(
|
|
176
|
-
multipart_chunksize=10 * 1024 * 1024,
|
|
177
|
-
max_concurrency=5,
|
|
178
|
-
)
|
|
179
|
-
with open(filepath, "rb") as f:
|
|
180
|
-
md5_checksum = calculate_md5(f)
|
|
181
|
-
file_size = os.path.getsize(filepath)
|
|
182
|
-
transfer_callback.add_file(filepath, file_size)
|
|
183
|
-
callback_function = create_transfer_callback(
|
|
184
|
-
transfer_callback, filepath
|
|
185
|
-
)
|
|
186
|
-
s3.Bucket(bucket).upload_file(
|
|
187
|
-
filepath,
|
|
188
|
-
fileu_uid,
|
|
189
|
-
Config=transfer_config,
|
|
190
|
-
Callback=callback_function,
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
client = AuthenticatedClient()
|
|
194
|
-
res = client.post(
|
|
195
|
-
"/queue/confirmUpload",
|
|
196
|
-
json={"uuid": fileu_uid, "md5": md5_checksum},
|
|
197
|
-
)
|
|
198
|
-
res.raise_for_status()
|
|
199
|
-
_queue.task_done()
|
|
200
|
-
except queue.Empty:
|
|
201
|
-
break
|
|
202
|
-
except Exception as e:
|
|
203
|
-
print("Error uploading file: " + filepath)
|
|
204
|
-
print(e)
|
|
205
|
-
_queue.task_done()
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def canUploadMission(client: AuthenticatedClient, project_uuid: str):
|
|
209
|
-
permissions = client.get("/user/permissions")
|
|
210
|
-
permissions.raise_for_status()
|
|
211
|
-
permissions_json = permissions.json()
|
|
212
|
-
for_project = filter(
|
|
213
|
-
lambda x: x["uuid"] == project_uuid, permissions_json["projects"]
|
|
214
|
-
)
|
|
215
|
-
max_for_project = max(map(lambda x: x["access"], for_project))
|
|
216
|
-
return max_for_project >= 10
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def promptForTags(setTags: Dict[str, str], requiredTags: Dict[str, str]):
|
|
220
|
-
for required_tag in requiredTags:
|
|
221
|
-
if required_tag["name"] not in setTags:
|
|
222
|
-
while True:
|
|
223
|
-
if required_tag["datatype"] in ["LOCATION", "STRING", "LINK"]:
|
|
224
|
-
tag_value = typer.prompt(
|
|
225
|
-
"Provide value for required tag " + required_tag["name"]
|
|
226
|
-
)
|
|
227
|
-
if tag_value != "":
|
|
228
|
-
break
|
|
229
|
-
elif required_tag["datatype"] == "BOOLEAN":
|
|
230
|
-
tag_value = typer.confirm(
|
|
231
|
-
"Provide (y/N) for required tag " + required_tag["name"]
|
|
232
|
-
)
|
|
233
|
-
break
|
|
234
|
-
elif required_tag["datatype"] == "NUMBER":
|
|
235
|
-
tag_value = typer.prompt(
|
|
236
|
-
"Provide number for required tag " + required_tag["name"]
|
|
237
|
-
)
|
|
238
|
-
try:
|
|
239
|
-
tag_value = float(tag_value)
|
|
240
|
-
break
|
|
241
|
-
except ValueError:
|
|
242
|
-
typer.echo("Invalid number format. Please provide a number.")
|
|
243
|
-
elif required_tag["datatype"] == "DATE":
|
|
244
|
-
tag_value = typer.prompt(
|
|
245
|
-
"Provide date for required tag " + required_tag["name"]
|
|
246
|
-
)
|
|
247
|
-
try:
|
|
248
|
-
tag_value = datetime.strptime(tag_value, "%Y-%m-%d %H:%M:%S")
|
|
249
|
-
break
|
|
250
|
-
except ValueError:
|
|
251
|
-
print("Invalid date format. Please use 'YYYY-MM-DD HH:MM:SS'")
|
|
252
|
-
|
|
253
|
-
setTags[required_tag["uuid"]] = tag_value
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def is_valid_UUIDv4(uuid: str) -> bool:
|
|
257
|
-
has_correct_length = len(uuid) == 36
|
|
258
|
-
|
|
259
|
-
# is UUID4
|
|
260
|
-
uuid_regex = (
|
|
261
|
-
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
262
|
-
)
|
|
263
|
-
is_valid_uuid = re.match(uuid_regex, uuid)
|
|
264
|
-
|
|
265
|
-
return has_correct_length and is_valid_uuid
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if __name__ == "__main__":
|
|
269
|
-
res = expand_and_match(
|
|
270
|
-
"~/Downloads/dodo_mission_2024_02_08-20240408T074313Z-003/**.bag"
|
|
271
|
-
)
|
|
272
|
-
print(res)
|
kleinkram/mission/mission.py
DELETED
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
|
|
4
|
-
import httpx
|
|
5
|
-
import requests
|
|
6
|
-
import typer
|
|
7
|
-
from rich.console import Console
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
from tqdm import tqdm
|
|
10
|
-
from typing_extensions import Annotated, Optional, List
|
|
11
|
-
|
|
12
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
13
|
-
from kleinkram.error_handling import AccessDeniedException
|
|
14
|
-
from kleinkram.helper import expand_and_match, uploadFiles
|
|
15
|
-
|
|
16
|
-
missionCommands = typer.Typer(
|
|
17
|
-
name="mission",
|
|
18
|
-
help="Mission operations",
|
|
19
|
-
no_args_is_help=True,
|
|
20
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@missionCommands.command("tag")
|
|
25
|
-
def addTag(
|
|
26
|
-
mission_uuid: Annotated[str, typer.Argument()],
|
|
27
|
-
tagtype_uuid: Annotated[str, typer.Argument()],
|
|
28
|
-
value: Annotated[str, typer.Argument()],
|
|
29
|
-
):
|
|
30
|
-
"""Tag a mission"""
|
|
31
|
-
try:
|
|
32
|
-
client = AuthenticatedClient()
|
|
33
|
-
response = client.post(
|
|
34
|
-
"/tag/addTag",
|
|
35
|
-
json={"mission": mission_uuid, "tagType": tagtype_uuid, "value": value},
|
|
36
|
-
)
|
|
37
|
-
if response.status_code < 400:
|
|
38
|
-
print("Tagged mission")
|
|
39
|
-
else:
|
|
40
|
-
print(response.json())
|
|
41
|
-
print("Failed to tag mission")
|
|
42
|
-
raise Exception("Failed to tag mission")
|
|
43
|
-
except httpx.HTTPError as e:
|
|
44
|
-
print(e)
|
|
45
|
-
print("Failed to tag mission")
|
|
46
|
-
raise e
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
@missionCommands.command("list")
|
|
50
|
-
def list_missions(
|
|
51
|
-
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
52
|
-
table: Optional[bool] = typer.Option(
|
|
53
|
-
True, help="Outputs a table with more information"
|
|
54
|
-
),
|
|
55
|
-
):
|
|
56
|
-
"""
|
|
57
|
-
List all missions with optional filter for project.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
url = "/mission"
|
|
61
|
-
params = {}
|
|
62
|
-
if project:
|
|
63
|
-
url += f"/filteredByProjectName"
|
|
64
|
-
params["projectName"] = project
|
|
65
|
-
else:
|
|
66
|
-
url += "/all"
|
|
67
|
-
|
|
68
|
-
client = AuthenticatedClient()
|
|
69
|
-
|
|
70
|
-
try:
|
|
71
|
-
|
|
72
|
-
response = client.get(url, params=params)
|
|
73
|
-
response.raise_for_status()
|
|
74
|
-
|
|
75
|
-
except httpx.HTTPError:
|
|
76
|
-
|
|
77
|
-
raise AccessDeniedException(
|
|
78
|
-
f"Failed to fetch mission."
|
|
79
|
-
f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
|
|
80
|
-
f"{response.json()['message']} ({response.status_code})",
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
data = response.json()
|
|
84
|
-
missions_by_project_uuid = {}
|
|
85
|
-
for mission in data:
|
|
86
|
-
project_uuid = mission["project"]["uuid"]
|
|
87
|
-
if project_uuid not in missions_by_project_uuid:
|
|
88
|
-
missions_by_project_uuid[project_uuid] = []
|
|
89
|
-
missions_by_project_uuid[project_uuid].append(mission)
|
|
90
|
-
|
|
91
|
-
if len(missions_by_project_uuid.items()) == 0:
|
|
92
|
-
print(f"No missions found for project '{project}'. Does it exist?")
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
print("missions by Project:")
|
|
96
|
-
if not table:
|
|
97
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
98
|
-
print(f"* {missions_by_project_uuid[project_uuid][0]['project']['name']}")
|
|
99
|
-
for mission in missions:
|
|
100
|
-
print(f" - {mission['name']}")
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
table = Table(
|
|
104
|
-
"project",
|
|
105
|
-
"name",
|
|
106
|
-
"UUID",
|
|
107
|
-
"creator",
|
|
108
|
-
"createdAt",
|
|
109
|
-
title="Missions",
|
|
110
|
-
expand=True,
|
|
111
|
-
)
|
|
112
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
113
|
-
for mission in missions:
|
|
114
|
-
table.add_row(
|
|
115
|
-
mission["project"]["name"],
|
|
116
|
-
mission["name"],
|
|
117
|
-
mission["uuid"],
|
|
118
|
-
mission["creator"]["name"],
|
|
119
|
-
mission["createdAt"],
|
|
120
|
-
)
|
|
121
|
-
console = Console()
|
|
122
|
-
console.print(table)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@missionCommands.command("byUUID")
|
|
126
|
-
def mission_by_uuid(
|
|
127
|
-
uuid: Annotated[str, typer.Argument()],
|
|
128
|
-
json: Optional[bool] = typer.Option(False, help="Output as JSON"),
|
|
129
|
-
):
|
|
130
|
-
"""
|
|
131
|
-
Get mission name, project name, creator and table of its files given a Mission UUID
|
|
132
|
-
|
|
133
|
-
Use the JSON flag to output the full JSON response instead.
|
|
134
|
-
|
|
135
|
-
Can be run with API Key or with login.
|
|
136
|
-
"""
|
|
137
|
-
url = "/mission/one"
|
|
138
|
-
client = AuthenticatedClient()
|
|
139
|
-
response = client.get(url, params={"uuid": uuid})
|
|
140
|
-
|
|
141
|
-
try:
|
|
142
|
-
response.raise_for_status()
|
|
143
|
-
except httpx.HTTPError:
|
|
144
|
-
raise AccessDeniedException(
|
|
145
|
-
f"Failed to fetch mission. "
|
|
146
|
-
f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
|
|
147
|
-
f"{response.json()['message']} ({response.status_code})",
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
data = response.json()
|
|
151
|
-
|
|
152
|
-
if json:
|
|
153
|
-
print(data)
|
|
154
|
-
return
|
|
155
|
-
print(f"mission: {data['name']}")
|
|
156
|
-
print(f"Creator: {data['creator']['name']}")
|
|
157
|
-
print("Project: " + data["project"]["name"])
|
|
158
|
-
table = Table("Filename", "Size", "date")
|
|
159
|
-
|
|
160
|
-
if "files" not in data:
|
|
161
|
-
print("No files found for mission.")
|
|
162
|
-
return
|
|
163
|
-
|
|
164
|
-
for file in data["files"]:
|
|
165
|
-
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
166
|
-
console = Console()
|
|
167
|
-
console.print(table)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@missionCommands.command("download")
|
|
171
|
-
def download(
|
|
172
|
-
mission_uuid: Annotated[
|
|
173
|
-
List[str], typer.Option(help="UUIDs of Mission to download")
|
|
174
|
-
],
|
|
175
|
-
local_path: Annotated[str, typer.Option()],
|
|
176
|
-
pattern: Optional[str] = typer.Option(
|
|
177
|
-
None,
|
|
178
|
-
help="Simple pattern to match the filename against. Allowed are alphanumeric characters,"
|
|
179
|
-
" '_', '-', '.' and '*' as wildcard.",
|
|
180
|
-
),
|
|
181
|
-
):
|
|
182
|
-
"""
|
|
183
|
-
|
|
184
|
-
Downloads all files of a mission to a local path.
|
|
185
|
-
The local path must be an empty directory.
|
|
186
|
-
|
|
187
|
-
"""
|
|
188
|
-
|
|
189
|
-
if not os.path.isdir(local_path):
|
|
190
|
-
raise ValueError(f"Local path '{local_path}' is not a directory.")
|
|
191
|
-
if not os.listdir(local_path) == []:
|
|
192
|
-
|
|
193
|
-
full_local_path = os.path.abspath(local_path)
|
|
194
|
-
|
|
195
|
-
raise ValueError(
|
|
196
|
-
f"Local path '{full_local_path}' is not empty, it contains {len(os.listdir(local_path))} files. "
|
|
197
|
-
f"The local target directory must be empty."
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
client = AuthenticatedClient()
|
|
201
|
-
for single_mission_uuid in mission_uuid:
|
|
202
|
-
response = client.get("/mission/download", params={"uuid": single_mission_uuid})
|
|
203
|
-
try:
|
|
204
|
-
response.raise_for_status()
|
|
205
|
-
except httpx.HTTPError as e:
|
|
206
|
-
raise AccessDeniedException(
|
|
207
|
-
f"Failed to download file."
|
|
208
|
-
f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
|
|
209
|
-
f"{response.json()['message']} ({response.status_code})",
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
paths = response.json()
|
|
213
|
-
if len(paths) == 0:
|
|
214
|
-
continue
|
|
215
|
-
|
|
216
|
-
# validate search pattern
|
|
217
|
-
if pattern:
|
|
218
|
-
if not re.match(r"^[a-zA-Z0-9_\-.*]+$", pattern):
|
|
219
|
-
raise ValueError(
|
|
220
|
-
"Invalid pattern. Allowed are alphanumeric characters, '_', '-', '.' and '*' as wildcard."
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
regex = pattern.replace("*", ".*")
|
|
224
|
-
pattern = re.compile(regex)
|
|
225
|
-
|
|
226
|
-
print(f"Found {len(paths)} files in mission:")
|
|
227
|
-
paths = [
|
|
228
|
-
path for path in paths if not pattern or pattern.match(path["filename"])
|
|
229
|
-
]
|
|
230
|
-
|
|
231
|
-
if pattern:
|
|
232
|
-
print(
|
|
233
|
-
f" » filtered to {len(paths)} files matching pattern '{pattern.pattern}'."
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
print(f"Start downloading {len(paths)} files to '{local_path}':\n")
|
|
237
|
-
for path in paths:
|
|
238
|
-
|
|
239
|
-
filename = path["filename"]
|
|
240
|
-
|
|
241
|
-
response = requests.get(path["link"], stream=True) # Enable streaming mode
|
|
242
|
-
chunk_size = 1024 * 1024 * 10 # 10 MB chunks, adjust size if needed
|
|
243
|
-
|
|
244
|
-
# Open the file for writing in binary mode
|
|
245
|
-
with open(os.path.join(local_path, filename), "wb") as f:
|
|
246
|
-
for chunk in tqdm(
|
|
247
|
-
response.iter_content(chunk_size=chunk_size),
|
|
248
|
-
unit="MB",
|
|
249
|
-
desc=filename,
|
|
250
|
-
):
|
|
251
|
-
if chunk: # Filter out keep-alive new chunks
|
|
252
|
-
f.write(chunk)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
@missionCommands.command("upload")
|
|
256
|
-
def upload(
|
|
257
|
-
path: Annotated[
|
|
258
|
-
List[str],
|
|
259
|
-
typer.Option(prompt=True, help="Path to files to upload, Regex supported"),
|
|
260
|
-
],
|
|
261
|
-
mission_uuid: Annotated[
|
|
262
|
-
str, typer.Option(prompt=True, help="UUID of Mission to create")
|
|
263
|
-
],
|
|
264
|
-
):
|
|
265
|
-
"""
|
|
266
|
-
Upload files matching the path to a mission in a project.
|
|
267
|
-
|
|
268
|
-
The mission name must be unique within the project and already created.
|
|
269
|
-
Multiple paths can be given by using the option multiple times.\n
|
|
270
|
-
\n
|
|
271
|
-
Examples:\n
|
|
272
|
-
- 'klein upload --path "~/data/**/*.bag" --mission-uuid "2518cfc2-07f2-41a5-b74c-fdedb1b97f88" '\n
|
|
273
|
-
|
|
274
|
-
"""
|
|
275
|
-
files = []
|
|
276
|
-
for p in path:
|
|
277
|
-
files.extend(expand_and_match(p))
|
|
278
|
-
filenames = list(
|
|
279
|
-
map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
|
|
280
|
-
)
|
|
281
|
-
if not filenames:
|
|
282
|
-
raise ValueError("No files found matching the given path.")
|
|
283
|
-
|
|
284
|
-
print(f"Uploading the following files to mission '{mission_uuid}':")
|
|
285
|
-
filepaths = {}
|
|
286
|
-
for path in files:
|
|
287
|
-
if not os.path.isdir(path):
|
|
288
|
-
filepaths[path.split("/")[-1]] = path
|
|
289
|
-
typer.secho(f" - {path}", fg=typer.colors.RESET)
|
|
290
|
-
|
|
291
|
-
try:
|
|
292
|
-
client = AuthenticatedClient()
|
|
293
|
-
get_temporary_credentials = "/file/temporaryAccess"
|
|
294
|
-
response = client.post(
|
|
295
|
-
get_temporary_credentials,
|
|
296
|
-
json={"filenames": filenames, "missionUUID": mission_uuid},
|
|
297
|
-
)
|
|
298
|
-
if response.status_code >= 400:
|
|
299
|
-
raise ValueError(
|
|
300
|
-
"Failed to upload data. Status Code: "
|
|
301
|
-
+ str(response.status_code)
|
|
302
|
-
+ "\n"
|
|
303
|
-
+ response.json()["message"][0]
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
uploadFiles(response.json(), filepaths, 4)
|
|
307
|
-
except Exception as e:
|
|
308
|
-
print(e)
|
|
309
|
-
print("Failed to upload files")
|
|
310
|
-
raise e
|