kleinkram 0.32.3__tar.gz → 0.33.0.dev20241024121528__tar.gz
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-0.32.3 → kleinkram-0.33.0.dev20241024121528}/PKG-INFO +1 -1
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/pyproject.toml +1 -1
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/requirements.txt +2 -1
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/error_handling.py +27 -1
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/helper.py +58 -41
- kleinkram-0.33.0.dev20241024121528/src/kleinkram/main.py +350 -0
- kleinkram-0.33.0.dev20241024121528/src/kleinkram/project/project.py +124 -0
- kleinkram-0.32.3/src/kleinkram/main.py +0 -284
- kleinkram-0.32.3/src/kleinkram/project/project.py +0 -58
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/.gitignore +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/LICENSE +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/README.md +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/deploy.sh +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/dev.sh +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/klein.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/__init__.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/api_client.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/auth/auth.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/consts.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/endpoint/endpoint.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/file/file.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/mission/mission.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/queue/queue.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/tag/tag.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/topic/topic.py +0 -0
- {kleinkram-0.32.3 → kleinkram-0.33.0.dev20241024121528}/src/kleinkram/user/user.py +0 -0
|
@@ -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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from typer.core import TyperGroup
|
|
11
|
+
from typer.models import Context
|
|
12
|
+
from typing_extensions import Annotated, List, Optional
|
|
13
|
+
|
|
14
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
15
|
+
from kleinkram.auth.auth import login, setCliKey, logout
|
|
16
|
+
from kleinkram.endpoint.endpoint import endpoint
|
|
17
|
+
from kleinkram.error_handling import (
|
|
18
|
+
ErrorHandledTyper,
|
|
19
|
+
AccessDeniedException,
|
|
20
|
+
)
|
|
21
|
+
from kleinkram.file.file import file
|
|
22
|
+
from kleinkram.mission.mission import missionCommands
|
|
23
|
+
from kleinkram.project.project import project
|
|
24
|
+
from kleinkram.queue.queue import queue
|
|
25
|
+
from kleinkram.tag.tag import tag
|
|
26
|
+
from kleinkram.topic.topic import topic
|
|
27
|
+
from kleinkram.user.user import user
|
|
28
|
+
from .helper import uploadFiles, expand_and_match, canUploadMission, promptForTags
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommandPanel(str, Enum):
|
|
32
|
+
CoreCommands = "CORE COMMANDS"
|
|
33
|
+
Commands = "COMMANDS"
|
|
34
|
+
AdditionalCommands = "ADDITIONAL COMMANDS"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def version_callback(value: bool):
|
|
38
|
+
if value:
|
|
39
|
+
try:
|
|
40
|
+
_version = importlib.metadata.version("kleinkram")
|
|
41
|
+
except importlib.metadata.PackageNotFoundError:
|
|
42
|
+
_version = "local"
|
|
43
|
+
typer.echo(f"CLI Version: {_version}")
|
|
44
|
+
raise typer.Exit()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OrderCommands(TyperGroup):
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
The following code snippet is taken from https://github.com/tiangolo/typer/discussions/855 (see comment
|
|
51
|
+
https://github.com/tiangolo/typer/discussions/855#discussioncomment-9824582) and adapted to our use case.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def list_commands(self, _ctx: Context) -> List[str]:
|
|
55
|
+
order = list(CommandPanel)
|
|
56
|
+
grouped_commands = {
|
|
57
|
+
name: getattr(command, "rich_help_panel")
|
|
58
|
+
for name, command in sorted(self.commands.items())
|
|
59
|
+
if getattr(command, "rich_help_panel") in order
|
|
60
|
+
}
|
|
61
|
+
ungrouped_command_names = [
|
|
62
|
+
command.name
|
|
63
|
+
for command in self.commands.values()
|
|
64
|
+
if command.name not in grouped_commands
|
|
65
|
+
]
|
|
66
|
+
return [
|
|
67
|
+
name
|
|
68
|
+
for name, command in sorted(
|
|
69
|
+
grouped_commands.items(),
|
|
70
|
+
key=lambda item: order.index(item[1]),
|
|
71
|
+
)
|
|
72
|
+
] + sorted(ungrouped_command_names)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
app = ErrorHandledTyper(
|
|
76
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
77
|
+
no_args_is_help=True,
|
|
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.",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.callback()
|
|
86
|
+
def version(
|
|
87
|
+
version: bool = typer.Option(
|
|
88
|
+
None,
|
|
89
|
+
"--version",
|
|
90
|
+
"-v",
|
|
91
|
+
callback=version_callback,
|
|
92
|
+
is_eager=True,
|
|
93
|
+
help="Print the version and exit",
|
|
94
|
+
)
|
|
95
|
+
):
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
app.add_typer(project, rich_help_panel=CommandPanel.Commands)
|
|
100
|
+
app.add_typer(missionCommands, rich_help_panel=CommandPanel.Commands)
|
|
101
|
+
|
|
102
|
+
app.add_typer(topic, rich_help_panel=CommandPanel.Commands)
|
|
103
|
+
app.add_typer(file, rich_help_panel=CommandPanel.Commands)
|
|
104
|
+
app.add_typer(queue, rich_help_panel=CommandPanel.Commands)
|
|
105
|
+
app.add_typer(user, rich_help_panel=CommandPanel.Commands)
|
|
106
|
+
app.add_typer(tag, rich_help_panel=CommandPanel.Commands)
|
|
107
|
+
app.add_typer(endpoint, rich_help_panel=CommandPanel.AdditionalCommands)
|
|
108
|
+
|
|
109
|
+
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(login)
|
|
110
|
+
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(logout)
|
|
111
|
+
app.command(hidden=True)(setCliKey)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command("download", rich_help_panel=CommandPanel.CoreCommands)
|
|
115
|
+
def download():
|
|
116
|
+
raise NotImplementedError("Not implemented yet.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands, no_args_is_help=True)
|
|
120
|
+
def upload(
|
|
121
|
+
path: Annotated[
|
|
122
|
+
List[str],
|
|
123
|
+
typer.Option(help="Path to files to upload, Regex supported"),
|
|
124
|
+
],
|
|
125
|
+
project: Annotated[str, typer.Option(help="Name of Project")],
|
|
126
|
+
mission: Annotated[str, typer.Option(help="Name of Mission to create")],
|
|
127
|
+
tags: Annotated[
|
|
128
|
+
Optional[List[str]],
|
|
129
|
+
typer.Option(help="Tags to add to the mission"),
|
|
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,
|
|
143
|
+
):
|
|
144
|
+
"""
|
|
145
|
+
Upload files matching the path to a mission in a project.
|
|
146
|
+
|
|
147
|
+
The mission name must be unique within the project and not yet created.\n
|
|
148
|
+
Multiple paths can be given by using the option multiple times.\n
|
|
149
|
+
Examples:\n
|
|
150
|
+
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1" --tags "0700946d-1d6a-4520-b263-0e177f49c35b:LEE-H" --tags "1565118d-593c-4517-8c2d-9658452d9319:Dodo"'\n
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
files = []
|
|
154
|
+
for p in path:
|
|
155
|
+
files.extend(expand_and_match(p))
|
|
156
|
+
|
|
157
|
+
print(
|
|
158
|
+
f"Uploading the following files to mission '{mission}' in project '{project}':"
|
|
159
|
+
)
|
|
160
|
+
filename_filepaths_map = {}
|
|
161
|
+
for path in files:
|
|
162
|
+
if not os.path.isdir(path):
|
|
163
|
+
|
|
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.")
|
|
203
|
+
|
|
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:
|
|
217
|
+
raise AccessDeniedException(
|
|
218
|
+
f"The project '{project}' does not exist or you do not have access to it.\n"
|
|
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.",
|
|
221
|
+
f"{project_response.json()['message']} ({project_response.status_code})",
|
|
222
|
+
)
|
|
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
|
+
},
|
|
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
|
+
)
|
|
253
|
+
|
|
254
|
+
if not tags:
|
|
255
|
+
tags = []
|
|
256
|
+
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
257
|
+
|
|
258
|
+
required_tags = (
|
|
259
|
+
project_json["requiredTags"] if "requiredTags" in project_json else []
|
|
260
|
+
)
|
|
261
|
+
promptForTags(tags_dict, required_tags)
|
|
262
|
+
|
|
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:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"Failed to create mission. Status Code: "
|
|
287
|
+
f"{str(mission_response.status_code)}\n"
|
|
288
|
+
f"{mission_response.json()['message'][0]}"
|
|
289
|
+
)
|
|
290
|
+
|
|
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]
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
uploadFiles(response.json(), filename_filepaths_map, 4)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@queue.command("list")
|
|
310
|
+
def list_queue():
|
|
311
|
+
"""List current Queue entities"""
|
|
312
|
+
try:
|
|
313
|
+
url = "/queue/active"
|
|
314
|
+
startDate = datetime.now().date() - timedelta(days=1)
|
|
315
|
+
client = AuthenticatedClient()
|
|
316
|
+
response = client.get(url, params={"startDate": startDate})
|
|
317
|
+
response.raise_for_status()
|
|
318
|
+
data = response.json()
|
|
319
|
+
table = Table("UUID", "filename", "mission", "state", "origin", "createdAt")
|
|
320
|
+
for topic in data:
|
|
321
|
+
table.add_row(
|
|
322
|
+
topic["uuid"],
|
|
323
|
+
topic["filename"],
|
|
324
|
+
topic["mission"]["name"],
|
|
325
|
+
topic["state"],
|
|
326
|
+
topic["location"],
|
|
327
|
+
topic["createdAt"],
|
|
328
|
+
)
|
|
329
|
+
print(table)
|
|
330
|
+
|
|
331
|
+
except httpx.HTTPError as e:
|
|
332
|
+
print(e)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@app.command("claim", hidden=True)
|
|
336
|
+
def claim():
|
|
337
|
+
"""
|
|
338
|
+
Claim admin rights as the first user
|
|
339
|
+
|
|
340
|
+
Only works if no other user has claimed admin rights before.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
client = AuthenticatedClient()
|
|
344
|
+
response = client.post("/user/claimAdmin")
|
|
345
|
+
response.raise_for_status()
|
|
346
|
+
print("Admin claimed.")
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
app()
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
8
|
+
|
|
9
|
+
project = typer.Typer(
|
|
10
|
+
name="project",
|
|
11
|
+
help="Project operations",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@project.command("list", help="List all projects")
|
|
18
|
+
def list_projects():
|
|
19
|
+
"""
|
|
20
|
+
List all projects.
|
|
21
|
+
"""
|
|
22
|
+
client = AuthenticatedClient()
|
|
23
|
+
response = client.get("/project/filtered")
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
projects = response.json()[0]
|
|
26
|
+
|
|
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"])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@project.command("create")
|
|
101
|
+
def create_project(
|
|
102
|
+
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
103
|
+
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
104
|
+
):
|
|
105
|
+
"""
|
|
106
|
+
Create a new project
|
|
107
|
+
"""
|
|
108
|
+
# Todo add required tags as option.
|
|
109
|
+
try:
|
|
110
|
+
url = "/project/create"
|
|
111
|
+
client = AuthenticatedClient()
|
|
112
|
+
response = client.post(
|
|
113
|
+
url, json={"name": name, "description": description, "requiredTags": []}
|
|
114
|
+
) # TODO: Add required tags as option
|
|
115
|
+
if response.status_code >= 400:
|
|
116
|
+
response_json = response.json()
|
|
117
|
+
response_text = response_json["message"]
|
|
118
|
+
print(f"Failed to create project: {response_text}")
|
|
119
|
+
return
|
|
120
|
+
print("Project created")
|
|
121
|
+
|
|
122
|
+
except httpx.HTTPError as e:
|
|
123
|
+
print(f"Failed to create project: {e}")
|
|
124
|
+
raise e
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
|
-
from enum import Enum
|
|
4
|
-
|
|
5
|
-
import httpx
|
|
6
|
-
import importlib.metadata
|
|
7
|
-
import typer
|
|
8
|
-
from rich import print
|
|
9
|
-
from rich.table import Table
|
|
10
|
-
from typer.core import TyperGroup
|
|
11
|
-
from typer.models import Context
|
|
12
|
-
from typing_extensions import Annotated, List, Optional
|
|
13
|
-
|
|
14
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
15
|
-
from kleinkram.auth.auth import login, setCliKey, logout
|
|
16
|
-
from kleinkram.endpoint.endpoint import endpoint
|
|
17
|
-
from kleinkram.error_handling import ErrorHandledTyper, AccessDeniedException
|
|
18
|
-
from kleinkram.file.file import file
|
|
19
|
-
from kleinkram.mission.mission import missionCommands
|
|
20
|
-
from kleinkram.project.project import project
|
|
21
|
-
from kleinkram.queue.queue import queue
|
|
22
|
-
from kleinkram.tag.tag import tag
|
|
23
|
-
from kleinkram.topic.topic import topic
|
|
24
|
-
from kleinkram.user.user import user
|
|
25
|
-
from .helper import uploadFiles, expand_and_match, canUploadMission, promptForTags
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class CommandPanel(str, Enum):
|
|
29
|
-
CoreCommands = "CORE COMMANDS"
|
|
30
|
-
Commands = "COMMANDS"
|
|
31
|
-
AdditionalCommands = "ADDITIONAL COMMANDS"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def version_callback(value: bool):
|
|
35
|
-
if value:
|
|
36
|
-
try:
|
|
37
|
-
_version = importlib.metadata.version("kleinkram")
|
|
38
|
-
except importlib.metadata.PackageNotFoundError:
|
|
39
|
-
_version = "local"
|
|
40
|
-
typer.echo(f"CLI Version: {_version}")
|
|
41
|
-
raise typer.Exit()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class OrderCommands(TyperGroup):
|
|
45
|
-
"""
|
|
46
|
-
|
|
47
|
-
The following code snippet is taken from https://github.com/tiangolo/typer/discussions/855 (see comment
|
|
48
|
-
https://github.com/tiangolo/typer/discussions/855#discussioncomment-9824582) and adapted to our use case.
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
def list_commands(self, _ctx: Context) -> List[str]:
|
|
52
|
-
order = list(CommandPanel)
|
|
53
|
-
grouped_commands = {
|
|
54
|
-
name: getattr(command, "rich_help_panel")
|
|
55
|
-
for name, command in sorted(self.commands.items())
|
|
56
|
-
if getattr(command, "rich_help_panel") in order
|
|
57
|
-
}
|
|
58
|
-
ungrouped_command_names = [
|
|
59
|
-
command.name
|
|
60
|
-
for command in self.commands.values()
|
|
61
|
-
if command.name not in grouped_commands
|
|
62
|
-
]
|
|
63
|
-
return [
|
|
64
|
-
name
|
|
65
|
-
for name, command in sorted(
|
|
66
|
-
grouped_commands.items(),
|
|
67
|
-
key=lambda item: order.index(item[1]),
|
|
68
|
-
)
|
|
69
|
-
] + sorted(ungrouped_command_names)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
app = ErrorHandledTyper(
|
|
73
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
74
|
-
no_args_is_help=True,
|
|
75
|
-
cls=OrderCommands,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@app.callback()
|
|
80
|
-
def version(
|
|
81
|
-
version: bool = typer.Option(
|
|
82
|
-
None,
|
|
83
|
-
"--version",
|
|
84
|
-
"-v",
|
|
85
|
-
callback=version_callback,
|
|
86
|
-
is_eager=True,
|
|
87
|
-
help="Print the version and exit",
|
|
88
|
-
)
|
|
89
|
-
):
|
|
90
|
-
pass
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
app.add_typer(project, rich_help_panel=CommandPanel.Commands)
|
|
94
|
-
app.add_typer(missionCommands, rich_help_panel=CommandPanel.Commands)
|
|
95
|
-
|
|
96
|
-
app.add_typer(topic, rich_help_panel=CommandPanel.Commands)
|
|
97
|
-
app.add_typer(file, rich_help_panel=CommandPanel.Commands)
|
|
98
|
-
app.add_typer(queue, rich_help_panel=CommandPanel.Commands)
|
|
99
|
-
app.add_typer(user, rich_help_panel=CommandPanel.Commands)
|
|
100
|
-
app.add_typer(tag, rich_help_panel=CommandPanel.Commands)
|
|
101
|
-
app.add_typer(endpoint, rich_help_panel=CommandPanel.AdditionalCommands)
|
|
102
|
-
|
|
103
|
-
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(login)
|
|
104
|
-
app.command(rich_help_panel=CommandPanel.AdditionalCommands)(logout)
|
|
105
|
-
app.command(hidden=True)(setCliKey)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@app.command("download", rich_help_panel=CommandPanel.CoreCommands)
|
|
109
|
-
def download():
|
|
110
|
-
raise NotImplementedError("Not implemented yet.")
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands)
|
|
114
|
-
def upload(
|
|
115
|
-
path: Annotated[
|
|
116
|
-
List[str],
|
|
117
|
-
typer.Option(prompt=True, help="Path to files to upload, Regex supported"),
|
|
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")
|
|
122
|
-
],
|
|
123
|
-
tags: Annotated[
|
|
124
|
-
Optional[List[str]],
|
|
125
|
-
typer.Option(prompt=False, help="Tags to add to the mission"),
|
|
126
|
-
] = None,
|
|
127
|
-
):
|
|
128
|
-
"""
|
|
129
|
-
Upload files matching the path to a mission in a project.
|
|
130
|
-
|
|
131
|
-
The mission name must be unique within the project and not yet created.\n
|
|
132
|
-
Multiple paths can be given by using the option multiple times.\n
|
|
133
|
-
Examples:\n
|
|
134
|
-
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1" --tags "0700946d-1d6a-4520-b263-0e177f49c35b:LEE-H" --tags "1565118d-593c-4517-8c2d-9658452d9319:Dodo"'\n
|
|
135
|
-
|
|
136
|
-
"""
|
|
137
|
-
files = []
|
|
138
|
-
for p in path:
|
|
139
|
-
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
|
-
|
|
146
|
-
print(
|
|
147
|
-
f"Uploading the following files to mission '{mission}' in project '{project}':"
|
|
148
|
-
)
|
|
149
|
-
filepaths = {}
|
|
150
|
-
for path in files:
|
|
151
|
-
if not os.path.isdir(path):
|
|
152
|
-
filepaths[path.split("/")[-1]] = path
|
|
153
|
-
typer.secho(f" - {path}", fg=typer.colors.RESET)
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
client = AuthenticatedClient()
|
|
157
|
-
|
|
158
|
-
get_project_url = "/project/byName"
|
|
159
|
-
project_response = client.get(get_project_url, params={"name": project})
|
|
160
|
-
if project_response.status_code >= 400:
|
|
161
|
-
raise AccessDeniedException(
|
|
162
|
-
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'\n",
|
|
164
|
-
f"{project_response.json()['message']} ({project_response.status_code})",
|
|
165
|
-
)
|
|
166
|
-
|
|
167
|
-
project_json = project_response.json()
|
|
168
|
-
if not project_json["uuid"]:
|
|
169
|
-
print(f"Project not found: '{project}'")
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
can_upload = canUploadMission(client, project_json["uuid"])
|
|
173
|
-
if not can_upload:
|
|
174
|
-
raise AccessDeniedException(
|
|
175
|
-
f"You do not have the required permissions to upload to project '{project}'\n",
|
|
176
|
-
"Access Denied",
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
if not tags:
|
|
180
|
-
tags = []
|
|
181
|
-
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
182
|
-
|
|
183
|
-
promptForTags(tags_dict, project_json["requiredTags"])
|
|
184
|
-
|
|
185
|
-
get_mission_url = "/mission/byName"
|
|
186
|
-
mission_response = client.get(get_mission_url, params={"name": mission})
|
|
187
|
-
mission_response.raise_for_status()
|
|
188
|
-
if mission_response.content:
|
|
189
|
-
mission_json = mission_response.json()
|
|
190
|
-
if mission_json["uuid"]:
|
|
191
|
-
raise ValueError(
|
|
192
|
-
f"Mission {mission_json['name']} ({mission_json['uuid']}) already exists. Delete it or select "
|
|
193
|
-
f"another name."
|
|
194
|
-
)
|
|
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
|
-
|
|
215
|
-
get_temporary_credentials = "/file/temporaryAccess"
|
|
216
|
-
response_2 = client.post(
|
|
217
|
-
get_temporary_credentials,
|
|
218
|
-
json={"filenames": filenames, "missionUUID": new_mission_data["uuid"]},
|
|
219
|
-
)
|
|
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
|
-
|
|
239
|
-
except httpx.HTTPError as e:
|
|
240
|
-
print(e)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
@queue.command("list")
|
|
244
|
-
def list_queue():
|
|
245
|
-
"""List current Queue entities"""
|
|
246
|
-
try:
|
|
247
|
-
url = "/queue/active"
|
|
248
|
-
startDate = datetime.now().date() - timedelta(days=1)
|
|
249
|
-
client = AuthenticatedClient()
|
|
250
|
-
response = client.get(url, params={"startDate": startDate})
|
|
251
|
-
response.raise_for_status()
|
|
252
|
-
data = response.json()
|
|
253
|
-
table = Table("UUID", "filename", "mission", "state", "origin", "createdAt")
|
|
254
|
-
for topic in data:
|
|
255
|
-
table.add_row(
|
|
256
|
-
topic["uuid"],
|
|
257
|
-
topic["filename"],
|
|
258
|
-
topic["mission"]["name"],
|
|
259
|
-
topic["state"],
|
|
260
|
-
topic["location"],
|
|
261
|
-
topic["createdAt"],
|
|
262
|
-
)
|
|
263
|
-
print(table)
|
|
264
|
-
|
|
265
|
-
except httpx.HTTPError as e:
|
|
266
|
-
print(e)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
@app.command("claim", hidden=True)
|
|
270
|
-
def claim():
|
|
271
|
-
"""
|
|
272
|
-
Claim admin rights as the first user
|
|
273
|
-
|
|
274
|
-
Only works if no other user has claimed admin rights before.
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
client = AuthenticatedClient()
|
|
278
|
-
response = client.post("/user/claimAdmin")
|
|
279
|
-
response.raise_for_status()
|
|
280
|
-
print("Admin claimed.")
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if __name__ == "__main__":
|
|
284
|
-
app()
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from typing_extensions import Annotated
|
|
2
|
-
|
|
3
|
-
import httpx
|
|
4
|
-
import typer
|
|
5
|
-
|
|
6
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
7
|
-
|
|
8
|
-
project = typer.Typer(
|
|
9
|
-
name="project",
|
|
10
|
-
help="Project operations",
|
|
11
|
-
no_args_is_help=True,
|
|
12
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@project.command("list")
|
|
17
|
-
def list_projects():
|
|
18
|
-
"""
|
|
19
|
-
List all projects.
|
|
20
|
-
"""
|
|
21
|
-
try:
|
|
22
|
-
client = AuthenticatedClient()
|
|
23
|
-
response = client.get("/project/filtered")
|
|
24
|
-
response.raise_for_status()
|
|
25
|
-
projects = response.json()[0]
|
|
26
|
-
print("Projects:")
|
|
27
|
-
for _project in projects:
|
|
28
|
-
print(f"- {_project['name']}")
|
|
29
|
-
|
|
30
|
-
except httpx.HTTPError as e:
|
|
31
|
-
print(f"Failed to fetch projects: {e}")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@project.command("create")
|
|
35
|
-
def create_project(
|
|
36
|
-
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
37
|
-
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
38
|
-
):
|
|
39
|
-
"""
|
|
40
|
-
Create a new project
|
|
41
|
-
"""
|
|
42
|
-
# Todo add required tags as option.
|
|
43
|
-
try:
|
|
44
|
-
url = "/project/create"
|
|
45
|
-
client = AuthenticatedClient()
|
|
46
|
-
response = client.post(
|
|
47
|
-
url, json={"name": name, "description": description, "requiredTags": []}
|
|
48
|
-
) # TODO: Add required tags as option
|
|
49
|
-
if response.status_code >= 400:
|
|
50
|
-
response_json = response.json()
|
|
51
|
-
response_text = response_json["message"]
|
|
52
|
-
print(f"Failed to create project: {response_text}")
|
|
53
|
-
return
|
|
54
|
-
print("Project created")
|
|
55
|
-
|
|
56
|
-
except httpx.HTTPError as e:
|
|
57
|
-
print(f"Failed to create project: {e}")
|
|
58
|
-
raise e
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|