kleinkram 0.32.3.dev20241023090650__py3-none-any.whl → 0.33.0.dev20241024121528__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/error_handling.py +27 -1
- kleinkram/helper.py +58 -41
- kleinkram/main.py +155 -89
- kleinkram/project/project.py +79 -13
- {kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/METADATA +1 -1
- {kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/RECORD +9 -9
- {kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/WHEEL +0 -0
- {kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/licenses/LICENSE +0 -0
kleinkram/error_handling.py
CHANGED
|
@@ -13,6 +13,12 @@ ExceptionType = "typing.Type[Exception]"
|
|
|
13
13
|
ErrorHandlingCallback = typing.Callable[[Exception], int]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class AbortException(Exception):
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str):
|
|
19
|
+
self.message = message
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class AccessDeniedException(Exception):
|
|
17
23
|
|
|
18
24
|
def __init__(self, message: str, api_error: str):
|
|
@@ -105,6 +111,20 @@ def remote_down_handler(e: Exception):
|
|
|
105
111
|
print()
|
|
106
112
|
|
|
107
113
|
|
|
114
|
+
def abort_handler(e: AbortException):
|
|
115
|
+
console = Console(file=sys.stderr)
|
|
116
|
+
panel = Panel(
|
|
117
|
+
f"{e.message}",
|
|
118
|
+
title="Command Aborted",
|
|
119
|
+
style="yellow",
|
|
120
|
+
padding=(1, 2),
|
|
121
|
+
highlight=True,
|
|
122
|
+
)
|
|
123
|
+
print()
|
|
124
|
+
console.print(panel)
|
|
125
|
+
print()
|
|
126
|
+
|
|
127
|
+
|
|
108
128
|
class ErrorHandledTyper(Typer):
|
|
109
129
|
error_handlers: typing.Dict[ExceptionType, ErrorHandlingCallback] = {
|
|
110
130
|
NotAuthenticatedException: not_authenticated_handler,
|
|
@@ -114,6 +134,7 @@ class ErrorHandledTyper(Typer):
|
|
|
114
134
|
ValueError: value_error_handler,
|
|
115
135
|
RemoteProtocolError: remote_down_handler,
|
|
116
136
|
ReadError: remote_down_handler,
|
|
137
|
+
AbortException: abort_handler,
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
def __init__(self, *args, **kwargs):
|
|
@@ -138,11 +159,16 @@ class ErrorHandledTyper(Typer):
|
|
|
138
159
|
f"An unhanded error of type {type(e).__name__} occurred.",
|
|
139
160
|
fg=typer.colors.RED,
|
|
140
161
|
)
|
|
141
|
-
|
|
162
|
+
|
|
142
163
|
typer.secho(
|
|
143
164
|
" » Please report this error to the developers.",
|
|
144
165
|
fg=typer.colors.RED,
|
|
145
166
|
)
|
|
167
|
+
|
|
168
|
+
typer.secho(f"\n\n{e}:", fg=typer.colors.RED)
|
|
169
|
+
console = Console()
|
|
170
|
+
console.print_exception(show_locals=True)
|
|
171
|
+
|
|
146
172
|
else:
|
|
147
173
|
self.error_handlers[type(e)](e)
|
|
148
174
|
|
kleinkram/helper.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import glob
|
|
2
2
|
import os
|
|
3
3
|
import queue
|
|
4
|
+
import sys
|
|
4
5
|
import threading
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from functools import partial
|
|
7
8
|
|
|
8
|
-
import typer
|
|
9
|
-
from botocore.config import Config
|
|
10
|
-
from botocore.utils import calculate_md5
|
|
11
|
-
from typing_extensions import Dict, List
|
|
12
9
|
import boto3
|
|
13
|
-
|
|
14
10
|
import tqdm
|
|
11
|
+
import typer
|
|
15
12
|
from boto3.s3.transfer import TransferConfig
|
|
16
|
-
from botocore.
|
|
13
|
+
from botocore.config import Config
|
|
14
|
+
from botocore.utils import calculate_md5
|
|
17
15
|
from rich import print
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from typing_extensions import Dict
|
|
18
18
|
|
|
19
19
|
from kleinkram.api_client import AuthenticatedClient
|
|
20
20
|
|
|
@@ -106,84 +106,101 @@ def expand_and_match(path_pattern):
|
|
|
106
106
|
return file_list
|
|
107
107
|
|
|
108
108
|
|
|
109
|
-
def uploadFiles(
|
|
109
|
+
def uploadFiles(
|
|
110
|
+
files_with_access: Dict[str, object], paths: Dict[str, str], nrThreads: int
|
|
111
|
+
):
|
|
110
112
|
client = AuthenticatedClient()
|
|
111
113
|
|
|
112
|
-
session = boto3.Session(
|
|
113
|
-
aws_access_key_id=credentials["accessKey"],
|
|
114
|
-
aws_secret_access_key=credentials["secretKey"],
|
|
115
|
-
aws_session_token=credentials["sessionToken"],
|
|
116
|
-
)
|
|
117
114
|
api_endpoint = client.tokenfile.endpoint
|
|
118
115
|
if api_endpoint == "http://localhost:3000":
|
|
119
116
|
minio_endpoint = "http://localhost:9000"
|
|
120
117
|
else:
|
|
121
118
|
minio_endpoint = api_endpoint.replace("api", "minio")
|
|
122
119
|
|
|
123
|
-
config = Config(retries={"max_attempts": 10, "mode": "standard"})
|
|
124
|
-
s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
|
|
125
|
-
|
|
126
120
|
_queue = queue.Queue()
|
|
127
|
-
for
|
|
128
|
-
_queue.put(
|
|
121
|
+
for file_with_access in files_with_access:
|
|
122
|
+
_queue.put((file_with_access, str(paths[file_with_access["fileName"]])))
|
|
123
|
+
|
|
129
124
|
threads = []
|
|
130
|
-
|
|
131
|
-
failed_uploads = []
|
|
125
|
+
transfer_callback = TransferCallback()
|
|
132
126
|
|
|
133
127
|
for i in range(nrThreads):
|
|
134
128
|
thread = threading.Thread(
|
|
135
|
-
target=uploadFile,
|
|
129
|
+
target=uploadFile,
|
|
130
|
+
args=(_queue, minio_endpoint, transfer_callback),
|
|
136
131
|
)
|
|
137
132
|
thread.start()
|
|
138
133
|
threads.append(thread)
|
|
139
134
|
for thread in threads:
|
|
140
135
|
thread.join()
|
|
141
|
-
if len(failed_uploads) > 0:
|
|
142
|
-
print("Failed to upload the following files:")
|
|
143
|
-
for file in failed_uploads:
|
|
144
|
-
print(file)
|
|
145
136
|
|
|
146
137
|
|
|
147
138
|
def uploadFile(
|
|
148
139
|
_queue: queue.Queue,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
failed_uploads: List[str],
|
|
140
|
+
minio_endpoint: str,
|
|
141
|
+
transfer_callback: TransferCallback,
|
|
152
142
|
):
|
|
143
|
+
config = Config(retries={"max_attempts": 10, "mode": "standard"})
|
|
144
|
+
|
|
153
145
|
while True:
|
|
154
146
|
try:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
file_with_access, filepath = _queue.get(timeout=3)
|
|
148
|
+
|
|
149
|
+
if "error" in file_with_access and (
|
|
150
|
+
file_with_access["error"] is not None or file_with_access["error"] != ""
|
|
151
|
+
):
|
|
152
|
+
console = Console(file=sys.stderr, style="red")
|
|
153
|
+
console.print(
|
|
154
|
+
f"Error uploading file: {file_with_access['fileName']} ({filepath}): {file_with_access['error']}"
|
|
155
|
+
)
|
|
156
|
+
_queue.task_done()
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
access_key = file_with_access["accessCredentials"]["accessKey"]
|
|
160
|
+
secret_key = file_with_access["accessCredentials"]["secretKey"]
|
|
161
|
+
session_token = file_with_access["accessCredentials"]["sessionToken"]
|
|
162
|
+
|
|
163
|
+
session = boto3.Session(
|
|
164
|
+
aws_access_key_id=access_key,
|
|
165
|
+
aws_secret_access_key=secret_key,
|
|
166
|
+
aws_session_token=session_token,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
|
|
170
|
+
|
|
171
|
+
fileu_uid = file_with_access["fileUUID"]
|
|
172
|
+
bucket = file_with_access["bucket"]
|
|
173
|
+
|
|
174
|
+
transfer_config = TransferConfig(
|
|
175
|
+
multipart_chunksize=10 * 1024 * 1024,
|
|
176
|
+
max_concurrency=5,
|
|
162
177
|
)
|
|
163
178
|
with open(filepath, "rb") as f:
|
|
164
179
|
md5_checksum = calculate_md5(f)
|
|
165
180
|
file_size = os.path.getsize(filepath)
|
|
166
|
-
|
|
167
|
-
callback_function = create_transfer_callback(
|
|
181
|
+
transfer_callback.add_file(filepath, file_size)
|
|
182
|
+
callback_function = create_transfer_callback(
|
|
183
|
+
transfer_callback, filepath
|
|
184
|
+
)
|
|
168
185
|
s3.Bucket(bucket).upload_file(
|
|
169
186
|
filepath,
|
|
170
|
-
|
|
171
|
-
Config=
|
|
187
|
+
fileu_uid,
|
|
188
|
+
Config=transfer_config,
|
|
172
189
|
Callback=callback_function,
|
|
173
190
|
)
|
|
174
191
|
|
|
175
192
|
client = AuthenticatedClient()
|
|
176
193
|
res = client.post(
|
|
177
194
|
"/queue/confirmUpload",
|
|
178
|
-
json={"uuid":
|
|
195
|
+
json={"uuid": fileu_uid, "md5": md5_checksum},
|
|
179
196
|
)
|
|
180
197
|
res.raise_for_status()
|
|
181
198
|
_queue.task_done()
|
|
182
199
|
except queue.Empty:
|
|
183
200
|
break
|
|
184
201
|
except Exception as e:
|
|
185
|
-
print(
|
|
186
|
-
|
|
202
|
+
print("Error uploading file: " + filepath)
|
|
203
|
+
print(e)
|
|
187
204
|
_queue.task_done()
|
|
188
205
|
|
|
189
206
|
|
kleinkram/main.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import importlib.metadata
|
|
1
2
|
import os
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
4
|
from enum import Enum
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
6
|
-
import importlib.metadata
|
|
7
7
|
import typer
|
|
8
8
|
from rich import print
|
|
9
9
|
from rich.table import Table
|
|
@@ -14,7 +14,10 @@ from typing_extensions import Annotated, List, Optional
|
|
|
14
14
|
from kleinkram.api_client import AuthenticatedClient
|
|
15
15
|
from kleinkram.auth.auth import login, setCliKey, logout
|
|
16
16
|
from kleinkram.endpoint.endpoint import endpoint
|
|
17
|
-
from kleinkram.error_handling import
|
|
17
|
+
from kleinkram.error_handling import (
|
|
18
|
+
ErrorHandledTyper,
|
|
19
|
+
AccessDeniedException,
|
|
20
|
+
)
|
|
18
21
|
from kleinkram.file.file import file
|
|
19
22
|
from kleinkram.mission.mission import missionCommands
|
|
20
23
|
from kleinkram.project.project import project
|
|
@@ -73,6 +76,9 @@ app = ErrorHandledTyper(
|
|
|
73
76
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
74
77
|
no_args_is_help=True,
|
|
75
78
|
cls=OrderCommands,
|
|
79
|
+
help=f"Kleinkram CLI\n\nThe Kleinkram CLI is a command line interface for Kleinkram. "
|
|
80
|
+
f"For a list of available commands, run 'klein --help' or visit "
|
|
81
|
+
f"https://docs.datasets.leggedrobotics.com/usage/cli/cli-getting-started.html for more information.",
|
|
76
82
|
)
|
|
77
83
|
|
|
78
84
|
|
|
@@ -110,20 +116,30 @@ def download():
|
|
|
110
116
|
raise NotImplementedError("Not implemented yet.")
|
|
111
117
|
|
|
112
118
|
|
|
113
|
-
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands)
|
|
119
|
+
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands, no_args_is_help=True)
|
|
114
120
|
def upload(
|
|
115
121
|
path: Annotated[
|
|
116
122
|
List[str],
|
|
117
|
-
typer.Option(
|
|
118
|
-
],
|
|
119
|
-
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
120
|
-
mission: Annotated[
|
|
121
|
-
str, typer.Option(prompt=True, help="Name of Mission to create")
|
|
123
|
+
typer.Option(help="Path to files to upload, Regex supported"),
|
|
122
124
|
],
|
|
125
|
+
project: Annotated[str, typer.Option(help="Name of Project")],
|
|
126
|
+
mission: Annotated[str, typer.Option(help="Name of Mission to create")],
|
|
123
127
|
tags: Annotated[
|
|
124
128
|
Optional[List[str]],
|
|
125
|
-
typer.Option(
|
|
129
|
+
typer.Option(help="Tags to add to the mission"),
|
|
126
130
|
] = None,
|
|
131
|
+
fix_filenames: Annotated[
|
|
132
|
+
bool,
|
|
133
|
+
typer.Option(help="Automatically fix filenames such that they are valid"),
|
|
134
|
+
] = False,
|
|
135
|
+
create_project: Annotated[
|
|
136
|
+
bool,
|
|
137
|
+
typer.Option(help="Allows adding files to an existing mission"),
|
|
138
|
+
] = False,
|
|
139
|
+
create_mission: Annotated[
|
|
140
|
+
bool,
|
|
141
|
+
typer.Option(help="Allows adding files to an existing mission"),
|
|
142
|
+
] = False,
|
|
127
143
|
):
|
|
128
144
|
"""
|
|
129
145
|
Upload files matching the path to a mission in a project.
|
|
@@ -137,107 +153,157 @@ def upload(
|
|
|
137
153
|
files = []
|
|
138
154
|
for p in path:
|
|
139
155
|
files.extend(expand_and_match(p))
|
|
140
|
-
filenames = list(
|
|
141
|
-
map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
|
|
142
|
-
)
|
|
143
|
-
if not filenames:
|
|
144
|
-
raise ValueError("No files found matching the given path.")
|
|
145
156
|
|
|
146
157
|
print(
|
|
147
158
|
f"Uploading the following files to mission '{mission}' in project '{project}':"
|
|
148
159
|
)
|
|
149
|
-
|
|
160
|
+
filename_filepaths_map = {}
|
|
150
161
|
for path in files:
|
|
151
162
|
if not os.path.isdir(path):
|
|
152
|
-
filepaths[path.split("/")[-1]] = path
|
|
153
|
-
typer.secho(f" - {path}", fg=typer.colors.RESET)
|
|
154
163
|
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
filename = path.split("/")[-1]
|
|
165
|
+
filename_without_extension, extension = os.path.splitext(filename)
|
|
166
|
+
if fix_filenames:
|
|
167
|
+
|
|
168
|
+
# replace all non-alphanumeric characters with underscores
|
|
169
|
+
filename_without_extension = "".join(
|
|
170
|
+
char if char.isalnum() else "_"
|
|
171
|
+
for char in filename_without_extension
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# trim filename to 40 characters
|
|
175
|
+
filename_without_extension = filename_without_extension[:40]
|
|
176
|
+
filename = f"{filename_without_extension}{extension}"
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
not filename.replace(".", "")
|
|
180
|
+
.replace("_", "")
|
|
181
|
+
.replace("-", "")
|
|
182
|
+
.isalnum()
|
|
183
|
+
):
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Filename '{filename}' is not valid. It must only contain alphanumeric characters, underscores and "
|
|
186
|
+
f"hyphens. Consider using the '--fix-filenames' option to automatically fix the filenames."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if not 3 <= len(filename_without_extension) <= 40:
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Filename '{filename}' is not valid. It must be between 3 and 40 characters long. Consider using "
|
|
192
|
+
f"the '--fix-filenames' option to automatically fix the filenames."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
filename_filepaths_map[filename] = path
|
|
196
|
+
typer.secho(f" - {filename}", fg=typer.colors.RESET)
|
|
197
|
+
print("\n\n")
|
|
198
|
+
|
|
199
|
+
filenames = list(filename_filepaths_map.keys())
|
|
200
|
+
|
|
201
|
+
if not filenames:
|
|
202
|
+
raise ValueError("No files found matching the given path.")
|
|
157
203
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
204
|
+
# validate filenames
|
|
205
|
+
if len(filenames) != len(set(filenames)):
|
|
206
|
+
raise ValueError(
|
|
207
|
+
"Filenames must be unique. Please check the files you are trying to upload. This can happen if you have "
|
|
208
|
+
"multiple files with the same name in different directories or use the '--fix-filenames' option."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
client = AuthenticatedClient()
|
|
212
|
+
|
|
213
|
+
get_project_url = "/project/byName"
|
|
214
|
+
project_response = client.get(get_project_url, params={"name": project})
|
|
215
|
+
if project_response.status_code >= 400:
|
|
216
|
+
if not create_project:
|
|
161
217
|
raise AccessDeniedException(
|
|
162
218
|
f"The project '{project}' does not exist or you do not have access to it.\n"
|
|
163
|
-
f"Consider using the following command to create a project: 'klein project create'
|
|
219
|
+
f"Consider using the following command to create a project: 'klein project create' "
|
|
220
|
+
f"or consider passing the flag '--create-project' to create the project automatically.",
|
|
164
221
|
f"{project_response.json()['message']} ({project_response.status_code})",
|
|
165
222
|
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"Access Denied",
|
|
223
|
+
else:
|
|
224
|
+
print(f"Project '{project}' does not exist. Creating it now.")
|
|
225
|
+
create_project_url = "/project/create"
|
|
226
|
+
project_response = client.post(
|
|
227
|
+
create_project_url,
|
|
228
|
+
json={
|
|
229
|
+
"name": project,
|
|
230
|
+
"description": "Autogenerated by klein CLI",
|
|
231
|
+
"requiredTags": [],
|
|
232
|
+
},
|
|
177
233
|
)
|
|
234
|
+
if project_response.status_code >= 400:
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Failed to create project. Status Code: "
|
|
237
|
+
f"{str(project_response.status_code)}\n"
|
|
238
|
+
f"{project_response.json()['message'][0]}"
|
|
239
|
+
)
|
|
240
|
+
print("Project created successfully.")
|
|
241
|
+
|
|
242
|
+
project_json = project_response.json()
|
|
243
|
+
if not project_json["uuid"]:
|
|
244
|
+
print(f"Project not found: '{project}'")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
can_upload = canUploadMission(client, project_json["uuid"])
|
|
248
|
+
if not can_upload:
|
|
249
|
+
raise AccessDeniedException(
|
|
250
|
+
f"You do not have the required permissions to upload to project '{project}'\n",
|
|
251
|
+
"Access Denied",
|
|
252
|
+
)
|
|
178
253
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
254
|
+
if not tags:
|
|
255
|
+
tags = []
|
|
256
|
+
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
182
257
|
|
|
183
|
-
|
|
258
|
+
required_tags = (
|
|
259
|
+
project_json["requiredTags"] if "requiredTags" in project_json else []
|
|
260
|
+
)
|
|
261
|
+
promptForTags(tags_dict, required_tags)
|
|
184
262
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
|
|
190
|
-
|
|
263
|
+
get_mission_url = "/mission/byName"
|
|
264
|
+
mission_response = client.get(get_mission_url, params={"name": mission})
|
|
265
|
+
if mission_response.status_code >= 400:
|
|
266
|
+
if not create_mission:
|
|
267
|
+
raise AccessDeniedException(
|
|
268
|
+
f"The mission '{mission}' does not exist or you do not have access to it.\n"
|
|
269
|
+
f"Consider using the following command to create a mission: 'klein mission create' "
|
|
270
|
+
f"or consider passing the flag '--create-mission' to create the mission automatically.",
|
|
271
|
+
f"{mission_response.json()['message']} ({mission_response.status_code})",
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
print(f"Mission '{mission}' does not exist. Creating it now.")
|
|
275
|
+
create_mission_url = "/mission/create"
|
|
276
|
+
mission_response = client.post(
|
|
277
|
+
create_mission_url,
|
|
278
|
+
json={
|
|
279
|
+
"name": mission,
|
|
280
|
+
"projectUUID": project_json["uuid"],
|
|
281
|
+
"tags": tags_dict,
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
if mission_response.status_code >= 400:
|
|
191
285
|
raise ValueError(
|
|
192
|
-
f"
|
|
193
|
-
f"
|
|
286
|
+
f"Failed to create mission. Status Code: "
|
|
287
|
+
f"{str(mission_response.status_code)}\n"
|
|
288
|
+
f"{mission_response.json()['message'][0]}"
|
|
194
289
|
)
|
|
195
|
-
raise Exception(f"Something failed, should not happen")
|
|
196
|
-
|
|
197
|
-
create_mission_url = "/mission/create"
|
|
198
|
-
new_mission = client.post(
|
|
199
|
-
create_mission_url,
|
|
200
|
-
json={
|
|
201
|
-
"name": mission,
|
|
202
|
-
"projectUUID": project_json["uuid"],
|
|
203
|
-
"tags": tags_dict,
|
|
204
|
-
},
|
|
205
|
-
)
|
|
206
|
-
if new_mission.status_code >= 400:
|
|
207
|
-
raise ValueError(
|
|
208
|
-
"Failed to create mission. Status Code: "
|
|
209
|
-
+ str(new_mission.status_code)
|
|
210
|
-
+ "\n"
|
|
211
|
-
+ new_mission.json()["message"]
|
|
212
|
-
)
|
|
213
|
-
new_mission_data = new_mission.json()
|
|
214
290
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
291
|
+
mission_json = mission_response.json()
|
|
292
|
+
|
|
293
|
+
get_temporary_credentials = "/file/temporaryAccess"
|
|
294
|
+
response = client.post(
|
|
295
|
+
get_temporary_credentials,
|
|
296
|
+
json={"filenames": filenames, "missionUUID": mission_json["uuid"]},
|
|
297
|
+
)
|
|
298
|
+
if response.status_code >= 400:
|
|
299
|
+
raise ValueError(
|
|
300
|
+
"Failed to get temporary credentials. Status Code: "
|
|
301
|
+
+ str(response.status_code)
|
|
302
|
+
+ "\n"
|
|
303
|
+
+ response.json()["message"][0]
|
|
219
304
|
)
|
|
220
|
-
if response_2.status_code >= 400:
|
|
221
|
-
raise ValueError(
|
|
222
|
-
"Failed to get temporary credentials. Status Code: "
|
|
223
|
-
+ str(response_2.status_code)
|
|
224
|
-
+ "\n"
|
|
225
|
-
+ response_2.json()["message"]
|
|
226
|
-
)
|
|
227
|
-
temp_credentials = response_2.json()
|
|
228
|
-
credential = temp_credentials["credentials"]
|
|
229
|
-
confirmed_files = temp_credentials["files"]
|
|
230
|
-
for _file in filenames:
|
|
231
|
-
if not _file in confirmed_files.keys():
|
|
232
|
-
raise Exception(
|
|
233
|
-
"Could not upload File '" + _file + "'. Is the filename unique? "
|
|
234
|
-
)
|
|
235
|
-
confirmed_files[_file]["filepath"] = filepaths[_file]
|
|
236
|
-
if len(confirmed_files.keys()) > 0:
|
|
237
|
-
uploadFiles(confirmed_files, credential, 4)
|
|
238
305
|
|
|
239
|
-
|
|
240
|
-
print(e)
|
|
306
|
+
uploadFiles(response.json(), filename_filepaths_map, 4)
|
|
241
307
|
|
|
242
308
|
|
|
243
309
|
@queue.command("list")
|
kleinkram/project/project.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing_extensions import Annotated
|
|
2
|
-
|
|
3
1
|
import httpx
|
|
4
2
|
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from typing_extensions import Annotated
|
|
5
6
|
|
|
6
7
|
from kleinkram.api_client import AuthenticatedClient
|
|
7
8
|
|
|
@@ -13,22 +14,87 @@ project = typer.Typer(
|
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
@project.command("list")
|
|
17
|
+
@project.command("list", help="List all projects")
|
|
17
18
|
def list_projects():
|
|
18
19
|
"""
|
|
19
20
|
List all projects.
|
|
20
21
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
projects = response.json()[0]
|
|
26
|
-
print("Projects:")
|
|
27
|
-
for _project in projects:
|
|
28
|
-
print(f"- {_project['name']}")
|
|
22
|
+
client = AuthenticatedClient()
|
|
23
|
+
response = client.get("/project/filtered")
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
projects = response.json()[0]
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
stdout_console = Console(stderr=False)
|
|
28
|
+
stderr_console = Console(stderr=True)
|
|
29
|
+
stderr_console.print(f"\nfound {len(projects)} projects with the following UUIDs:")
|
|
30
|
+
|
|
31
|
+
# print the uuids to stdout for simple piping
|
|
32
|
+
for p in projects:
|
|
33
|
+
stderr_console.print(" - ", end="")
|
|
34
|
+
stdout_console.print(p["uuid"])
|
|
35
|
+
stderr_console.print("\n")
|
|
36
|
+
|
|
37
|
+
# Print a summary table using rich to stderr
|
|
38
|
+
table = Table(title="Projects", expand=True)
|
|
39
|
+
table.add_column("Project UUID", width=10)
|
|
40
|
+
table.add_column("Project Name", width=12)
|
|
41
|
+
table.add_column("Description")
|
|
42
|
+
for p in projects:
|
|
43
|
+
table.add_row(p["uuid"], p["name"], p["description"])
|
|
44
|
+
|
|
45
|
+
stderr_console.print(table)
|
|
46
|
+
stderr_console.print("\n")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@project.command("details", help="Get details of a project")
|
|
50
|
+
def project_details(
|
|
51
|
+
project_uuid: Annotated[
|
|
52
|
+
str, typer.Argument(help="UUID of the project to get details of")
|
|
53
|
+
]
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Get details of a project
|
|
57
|
+
"""
|
|
58
|
+
client = AuthenticatedClient()
|
|
59
|
+
response = client.get(f"/project/one?uuid={project_uuid}")
|
|
60
|
+
response.raise_for_status()
|
|
61
|
+
project = response.json()
|
|
62
|
+
|
|
63
|
+
stdout_console = Console(stderr=False)
|
|
64
|
+
stderr_console = Console(stderr=True)
|
|
65
|
+
stderr_console.print(
|
|
66
|
+
f"\nDetails of project with UUID {project_uuid}:", highlight=False
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Print the details to stderr using rich
|
|
70
|
+
table = Table(title="Project Details", expand=True)
|
|
71
|
+
table.add_column("Key", width=16)
|
|
72
|
+
table.add_column("Value")
|
|
73
|
+
for key, value in project.items():
|
|
74
|
+
|
|
75
|
+
access_name_map = {0: "READ", 10: "CREATE", 20: "WRITE", 30: "DELETE"}
|
|
76
|
+
|
|
77
|
+
if key == "project_accesses":
|
|
78
|
+
value = ", ".join(
|
|
79
|
+
[
|
|
80
|
+
f"'{access['accessGroup']['name']}' ({access_name_map[access['rights']]})"
|
|
81
|
+
for access in value
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if key == "missions":
|
|
86
|
+
value = ", ".join([f"'{mission['name']}'" for mission in value])
|
|
87
|
+
|
|
88
|
+
if key == "creator":
|
|
89
|
+
value = value["name"]
|
|
90
|
+
|
|
91
|
+
table.add_row(key, f"{value}")
|
|
92
|
+
|
|
93
|
+
stderr_console.print(table)
|
|
94
|
+
stderr_console.print("\nList of missions:")
|
|
95
|
+
for mission in project["missions"]:
|
|
96
|
+
stderr_console.print(" - ", end="")
|
|
97
|
+
stdout_console.print(mission["uuid"])
|
|
32
98
|
|
|
33
99
|
|
|
34
100
|
@project.command("create")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: kleinkram
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.0.dev20241024121528
|
|
4
4
|
Summary: A CLI for the ETH project kleinkram
|
|
5
5
|
Project-URL: Homepage, https://github.com/leggedrobotics/kleinkram
|
|
6
6
|
Project-URL: Issues, https://github.com/leggedrobotics/kleinkram/issues
|
{kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/RECORD
RENAMED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
kleinkram/api_client.py,sha256=1GPsM-XFbPYEKP7RfWmzMTwxRqnVh4wtHVuW25KT8kA,2264
|
|
3
3
|
kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
|
|
4
|
-
kleinkram/error_handling.py,sha256
|
|
5
|
-
kleinkram/helper.py,sha256=
|
|
6
|
-
kleinkram/main.py,sha256=
|
|
4
|
+
kleinkram/error_handling.py,sha256=-Ai5xB_aGHDhzOhZyCJWijarSgU0_QJJrrvTFCAK6qI,4644
|
|
5
|
+
kleinkram/helper.py,sha256=mhRCztnkJ-Y9udpRKUY92ymVEZTlwzDrfLZkRupDqys,8953
|
|
6
|
+
kleinkram/main.py,sha256=cgDTfFQW91wlHZYg4_MmlERcFXDd1z--lUP6bGu8YnM,12536
|
|
7
7
|
kleinkram/auth/auth.py,sha256=w3-TsxWxURzLQ3_p43zgV4Rlh4dVL_WqI6HG2aes-b4,4991
|
|
8
8
|
kleinkram/endpoint/endpoint.py,sha256=uez5UrAnP7L5rVHUysA9tFkN3dB3dG1Ojt9g3w-UWuQ,1441
|
|
9
9
|
kleinkram/file/file.py,sha256=gLCZDHHgQWq25OmeG-lwkIh4aRZaLK12xxLkbhZ_m-g,5390
|
|
10
10
|
kleinkram/mission/mission.py,sha256=KI_r-DbaXr8uKi9rnSopj-G1N4Nq_ELEBn4mPJXMQzQ,8861
|
|
11
|
-
kleinkram/project/project.py,sha256=
|
|
11
|
+
kleinkram/project/project.py,sha256=le85GN9RgrqJeAL5mS-PhowFDjv-HBCYhgkKeFAUcGs,3780
|
|
12
12
|
kleinkram/queue/queue.py,sha256=MaLBjAu8asi9BkPvbbT-5AobCcpy3ex5rxM1kHpRINA,181
|
|
13
13
|
kleinkram/tag/tag.py,sha256=JSHbDPVfsvP34MuQhw__DPQk-Bah5G9BgwYsj_K_JGc,1805
|
|
14
14
|
kleinkram/topic/topic.py,sha256=IaXhrIHcJ3FSIr0WC-N7u9fkz-lAvSBgQklTX67t0Yc,1641
|
|
15
15
|
kleinkram/user/user.py,sha256=hDrbWeFPPnh2sswDd445SwcIFGyAbfXXWpYq1VqrK0g,1379
|
|
16
|
-
kleinkram-0.
|
|
17
|
-
kleinkram-0.
|
|
18
|
-
kleinkram-0.
|
|
19
|
-
kleinkram-0.
|
|
20
|
-
kleinkram-0.
|
|
16
|
+
kleinkram-0.33.0.dev20241024121528.dist-info/METADATA,sha256=_68yPVnug6xFs04N2uqmSzeGCBnTB13ziNb_4ma-sMA,845
|
|
17
|
+
kleinkram-0.33.0.dev20241024121528.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
18
|
+
kleinkram-0.33.0.dev20241024121528.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
19
|
+
kleinkram-0.33.0.dev20241024121528.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
20
|
+
kleinkram-0.33.0.dev20241024121528.dist-info/RECORD,,
|
{kleinkram-0.32.3.dev20241023090650.dist-info → kleinkram-0.33.0.dev20241024121528.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|