kleinkram 0.4.0.dev20240808080202__tar.gz → 0.5.0__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.4.0.dev20240808080202 → kleinkram-0.5.0}/PKG-INFO +1 -1
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/pyproject.toml +1 -1
- kleinkram-0.5.0/src/klein.py +9 -0
- kleinkram-0.5.0/src/kleinkram/api_client.py +72 -0
- {kleinkram-0.4.0.dev20240808080202/src/kleinkram → kleinkram-0.5.0/src/kleinkram/auth}/auth.py +15 -62
- kleinkram-0.5.0/src/kleinkram/file/file.py +109 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/src/kleinkram/helper.py +5 -5
- kleinkram-0.5.0/src/kleinkram/main.py +265 -0
- kleinkram-0.5.0/src/kleinkram/mission/mission.py +126 -0
- kleinkram-0.5.0/src/kleinkram/project/project.py +57 -0
- kleinkram-0.5.0/src/kleinkram/queue/queue.py +8 -0
- kleinkram-0.5.0/src/kleinkram/tag/tag.py +48 -0
- kleinkram-0.5.0/src/kleinkram/topic/topic.py +54 -0
- kleinkram-0.5.0/src/kleinkram/user/user.py +53 -0
- kleinkram-0.4.0.dev20240808080202/src/kleinkram/main.py +0 -566
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/.gitignore +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/LICENSE +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/README.md +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/deploy.sh +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/dev.sh +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/requirements.txt +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/src/kleinkram/__init__.py +0 -0
- {kleinkram-0.4.0.dev20240808080202 → kleinkram-0.5.0}/src/kleinkram/consts.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: kleinkram
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
|
|
5
|
+
from kleinkram.auth.auth import TokenFile, CLI_KEY, AUTH_TOKEN, REFRESH_TOKEN
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticatedClient(httpx.Client):
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
self.tokenfile = TokenFile()
|
|
14
|
+
self._load_cookies()
|
|
15
|
+
except Exception as e:
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
msg = f"You are not authenticated on endpoint '{self.tokenfile.endpoint}'. Please run 'klein login' to authenticate."
|
|
19
|
+
|
|
20
|
+
panel = Panel(
|
|
21
|
+
msg,
|
|
22
|
+
title="Not Authenticated",
|
|
23
|
+
style="yellow",
|
|
24
|
+
padding=(1, 2),
|
|
25
|
+
highlight=True,
|
|
26
|
+
)
|
|
27
|
+
print()
|
|
28
|
+
console.print(panel)
|
|
29
|
+
print()
|
|
30
|
+
|
|
31
|
+
def _load_cookies(self):
|
|
32
|
+
if self.tokenfile.isCliToken():
|
|
33
|
+
self.cookies.set(CLI_KEY, self.tokenfile.getCLIToken())
|
|
34
|
+
else:
|
|
35
|
+
self.cookies.set(AUTH_TOKEN, self.tokenfile.getAuthToken())
|
|
36
|
+
|
|
37
|
+
def refresh_token(self):
|
|
38
|
+
if self.tokenfile.isCliToken():
|
|
39
|
+
print("CLI key cannot be refreshed.")
|
|
40
|
+
return
|
|
41
|
+
refresh_token = self.tokenfile.getRefreshToken()
|
|
42
|
+
if not refresh_token:
|
|
43
|
+
print("No refresh token found. Please login again.")
|
|
44
|
+
raise Exception("No refresh token found.")
|
|
45
|
+
self.cookies.set(
|
|
46
|
+
REFRESH_TOKEN,
|
|
47
|
+
refresh_token,
|
|
48
|
+
)
|
|
49
|
+
response = self.post(
|
|
50
|
+
"/auth/refresh-token",
|
|
51
|
+
)
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
new_access_token = response.cookies.get(AUTH_TOKEN)
|
|
54
|
+
new_tokens = {AUTH_TOKEN: new_access_token, REFRESH_TOKEN: refresh_token}
|
|
55
|
+
self.tokenfile.saveTokens(new_tokens)
|
|
56
|
+
self.cookies.set(AUTH_TOKEN, new_access_token)
|
|
57
|
+
|
|
58
|
+
def request(self, method, url, *args, **kwargs):
|
|
59
|
+
response = super().request(
|
|
60
|
+
method, self.tokenfile.endpoint + url, *args, **kwargs
|
|
61
|
+
)
|
|
62
|
+
if (url == "/auth/refresh-token") and response.status_code == 401:
|
|
63
|
+
print("Refresh token expired. Please login again.")
|
|
64
|
+
response.status_code = 403
|
|
65
|
+
exit(1)
|
|
66
|
+
if response.status_code == 401:
|
|
67
|
+
print("Token expired, refreshing token...")
|
|
68
|
+
self.refresh_token()
|
|
69
|
+
response = super().request(
|
|
70
|
+
method, self.tokenfile.endpoint + url, *args, **kwargs
|
|
71
|
+
)
|
|
72
|
+
return response
|
{kleinkram-0.4.0.dev20240808080202/src/kleinkram → kleinkram-0.5.0/src/kleinkram/auth}/auth.py
RENAMED
|
@@ -3,17 +3,15 @@ import os
|
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import webbrowser
|
|
5
5
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
9
|
+
import typer
|
|
8
10
|
from typing_extensions import Annotated
|
|
9
11
|
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
import httpx
|
|
13
|
-
import typer
|
|
12
|
+
from kleinkram.consts import API_URL
|
|
14
13
|
|
|
15
14
|
app = typer.Typer()
|
|
16
|
-
from .consts import API_URL
|
|
17
15
|
|
|
18
16
|
TOKEN_FILE = Path(os.path.expanduser("~/.kleinkram.json"))
|
|
19
17
|
REFRESH_TOKEN = "refreshtoken"
|
|
@@ -87,62 +85,14 @@ def get_auth_tokens():
|
|
|
87
85
|
return httpd.tokens
|
|
88
86
|
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
f"{self.tokenfile.endpoint} is not authenticated. Please run 'klein login'."
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
def _load_cookies(self):
|
|
102
|
-
if self.tokenfile.isCliToken():
|
|
103
|
-
self.cookies.set(CLI_KEY, self.tokenfile.getCLIToken())
|
|
104
|
-
else:
|
|
105
|
-
self.cookies.set(AUTH_TOKEN, self.tokenfile.getAuthToken())
|
|
106
|
-
|
|
107
|
-
def refresh_token(self):
|
|
108
|
-
if self.tokenfile.isCliToken():
|
|
109
|
-
print("CLI key cannot be refreshed.")
|
|
110
|
-
return
|
|
111
|
-
refresh_token = self.tokenfile.getRefreshToken()
|
|
112
|
-
if not refresh_token:
|
|
113
|
-
print("No refresh token found. Please login again.")
|
|
114
|
-
raise Exception("No refresh token found.")
|
|
115
|
-
self.cookies.set(
|
|
116
|
-
REFRESH_TOKEN,
|
|
117
|
-
refresh_token,
|
|
118
|
-
)
|
|
119
|
-
response = self.post(
|
|
120
|
-
"/auth/refresh-token",
|
|
121
|
-
)
|
|
122
|
-
response.raise_for_status()
|
|
123
|
-
new_access_token = response.cookies.get(AUTH_TOKEN)
|
|
124
|
-
new_tokens = {AUTH_TOKEN: new_access_token, REFRESH_TOKEN: refresh_token}
|
|
125
|
-
self.tokenfile.saveTokens(new_tokens)
|
|
126
|
-
self.cookies.set(AUTH_TOKEN, new_access_token)
|
|
127
|
-
|
|
128
|
-
def request(self, method, url, *args, **kwargs):
|
|
129
|
-
response = super().request(
|
|
130
|
-
method, self.tokenfile.endpoint + url, *args, **kwargs
|
|
131
|
-
)
|
|
132
|
-
if (url == "/auth/refresh-token") and response.status_code == 401:
|
|
133
|
-
print("Refresh token expired. Please login again.")
|
|
134
|
-
response.status_code = 403
|
|
135
|
-
exit(1)
|
|
136
|
-
if response.status_code == 401:
|
|
137
|
-
print("Token expired, refreshing token...")
|
|
138
|
-
self.refresh_token()
|
|
139
|
-
response = super().request(
|
|
140
|
-
method, self.tokenfile.endpoint + url, *args, **kwargs
|
|
141
|
-
)
|
|
142
|
-
return response
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
client = AuthenticatedClient()
|
|
88
|
+
def logout():
|
|
89
|
+
"""
|
|
90
|
+
Logout from the currently set endpoint.
|
|
91
|
+
"""
|
|
92
|
+
tokenfile = TokenFile()
|
|
93
|
+
tokenfile.tokens[tokenfile.endpoint] = {}
|
|
94
|
+
tokenfile.writeToFile()
|
|
95
|
+
print("Logged out.")
|
|
146
96
|
|
|
147
97
|
|
|
148
98
|
def login(
|
|
@@ -157,7 +107,10 @@ def login(
|
|
|
157
107
|
"""
|
|
158
108
|
tokenfile = TokenFile()
|
|
159
109
|
if key:
|
|
160
|
-
|
|
110
|
+
token = {}
|
|
111
|
+
token[CLI_KEY] = key
|
|
112
|
+
tokenfile.saveTokens(token)
|
|
113
|
+
|
|
161
114
|
else:
|
|
162
115
|
url = tokenfile.endpoint + "/auth/google?state=cli"
|
|
163
116
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Optional, Annotated
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
7
|
+
|
|
8
|
+
file = typer.Typer(
|
|
9
|
+
name="file",
|
|
10
|
+
help="File operations",
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@file.command("list")
|
|
17
|
+
def list_files(
|
|
18
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
19
|
+
mission: Optional[str] = typer.Option(None, help="Name of Mission"),
|
|
20
|
+
topics: Optional[str] = typer.Option(None, help="Comma separated list of topics"),
|
|
21
|
+
tags: Optional[str] = typer.Option(
|
|
22
|
+
None, help="Comma separated list of tagtype:tagvalue pairs"
|
|
23
|
+
),
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
List all files with optional filters for project, mission, or topics.
|
|
27
|
+
|
|
28
|
+
Can list files of a project, mission, or with specific topics (Logical AND).
|
|
29
|
+
Examples:\n
|
|
30
|
+
- 'klein filelist'\n
|
|
31
|
+
- 'klein file list --project "Project 1"'\n
|
|
32
|
+
- 'klein file list --mission "Mission 1"'\n
|
|
33
|
+
- 'klein file list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw"'\n
|
|
34
|
+
- 'klein file list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw" --mission "Mission A"'
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
url = f"/file/filteredByNames"
|
|
38
|
+
params = {}
|
|
39
|
+
if project:
|
|
40
|
+
params["projectName"] = project
|
|
41
|
+
if mission:
|
|
42
|
+
params["missionName"] = mission
|
|
43
|
+
if topics:
|
|
44
|
+
params["topics"] = topics
|
|
45
|
+
if tags:
|
|
46
|
+
params["tags"] = {}
|
|
47
|
+
for tag in tags.split(","):
|
|
48
|
+
tagtype, tagvalue = tag.split("§")
|
|
49
|
+
params["tags"][tagtype] = tagvalue
|
|
50
|
+
|
|
51
|
+
client = AuthenticatedClient()
|
|
52
|
+
response = client.get(
|
|
53
|
+
url,
|
|
54
|
+
params=params,
|
|
55
|
+
)
|
|
56
|
+
response.raise_for_status()
|
|
57
|
+
data = response.json()
|
|
58
|
+
missions_by_project_uuid = {}
|
|
59
|
+
files_by_mission_uuid = {}
|
|
60
|
+
for file in data:
|
|
61
|
+
mission_uuid = file["mission"]["uuid"]
|
|
62
|
+
project_uuid = file["mission"]["project"]["uuid"]
|
|
63
|
+
if project_uuid not in missions_by_project_uuid:
|
|
64
|
+
missions_by_project_uuid[project_uuid] = []
|
|
65
|
+
if mission_uuid not in missions_by_project_uuid[project_uuid]:
|
|
66
|
+
missions_by_project_uuid[project_uuid].append(mission_uuid)
|
|
67
|
+
if mission_uuid not in files_by_mission_uuid:
|
|
68
|
+
files_by_mission_uuid[mission_uuid] = []
|
|
69
|
+
files_by_mission_uuid[mission_uuid].append(file)
|
|
70
|
+
|
|
71
|
+
print("Files by mission & Project:")
|
|
72
|
+
for project_uuid, missions in missions_by_project_uuid.items():
|
|
73
|
+
first_file = files_by_mission_uuid[missions[0]][0]
|
|
74
|
+
print(f"* {first_file['mission']['project']['name']}")
|
|
75
|
+
for mission in missions:
|
|
76
|
+
print(f" - {files_by_mission_uuid[mission][0]['mission']['name']}")
|
|
77
|
+
for file in files_by_mission_uuid[mission]:
|
|
78
|
+
print(f" - '{file['filename']}'")
|
|
79
|
+
|
|
80
|
+
except httpx.HTTPError as e:
|
|
81
|
+
print(f"Failed to fetch missions: {e}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@file.command("download")
|
|
85
|
+
def download(
|
|
86
|
+
missionuuid: Annotated[str, typer.Argument()],
|
|
87
|
+
):
|
|
88
|
+
"""Download file"""
|
|
89
|
+
try:
|
|
90
|
+
client = AuthenticatedClient()
|
|
91
|
+
response = client.get("/file/downloadWithToken", params={"uuid": missionuuid})
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
print(response.json())
|
|
94
|
+
except:
|
|
95
|
+
print("Failed to download file")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@file.command("clear")
|
|
99
|
+
def clear_queue():
|
|
100
|
+
"""Clear queue"""
|
|
101
|
+
# Prompt the user for confirmation
|
|
102
|
+
confirmation = typer.prompt("Are you sure you want to clear the Files? (y/n)")
|
|
103
|
+
if confirmation.lower() == "y":
|
|
104
|
+
client = AuthenticatedClient()
|
|
105
|
+
response = client.delete("/file/clear")
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
print("Files cleared.")
|
|
108
|
+
else:
|
|
109
|
+
print("Operation cancelled.")
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
+
import glob
|
|
1
2
|
import os
|
|
3
|
+
import queue
|
|
2
4
|
import threading
|
|
3
|
-
import
|
|
5
|
+
from typing import Dict
|
|
4
6
|
|
|
5
7
|
import httpx
|
|
6
8
|
import tqdm
|
|
7
9
|
from rich import print
|
|
8
|
-
import queue
|
|
9
|
-
|
|
10
|
-
from typing import Dict
|
|
11
10
|
|
|
12
|
-
from .
|
|
11
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
def expand_and_match(path_pattern):
|
|
@@ -54,6 +53,7 @@ def uploadFile(_queue: queue.Queue, paths: Dict[str, str], pbar: tqdm):
|
|
|
54
53
|
response = cli.put(url, content=f, headers=headers)
|
|
55
54
|
if response.status_code == 200:
|
|
56
55
|
pbar.update(100) # Update progress for each file
|
|
56
|
+
client = AuthenticatedClient()
|
|
57
57
|
client.post("/queue/confirmUpload", json={"uuid": uuid})
|
|
58
58
|
else:
|
|
59
59
|
print(
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import typer
|
|
7
|
+
from rich import print
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from typer.core import TyperGroup
|
|
10
|
+
from typer.models import Context
|
|
11
|
+
from typing_extensions import Annotated
|
|
12
|
+
|
|
13
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
14
|
+
from kleinkram.auth.auth import login, endpoint, setEndpoint, setCliKey, logout
|
|
15
|
+
from kleinkram.file.file import file
|
|
16
|
+
from kleinkram.mission.mission import mission
|
|
17
|
+
from kleinkram.project.project import project
|
|
18
|
+
from kleinkram.queue.queue import queue
|
|
19
|
+
from kleinkram.tag.tag import tag
|
|
20
|
+
from kleinkram.topic.topic import topic
|
|
21
|
+
from kleinkram.user.user import user
|
|
22
|
+
from .helper import uploadFiles, expand_and_match
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Panel(str, Enum):
|
|
26
|
+
CoreCommands = "CORE COMMANDS"
|
|
27
|
+
Commands = "COMMANDS"
|
|
28
|
+
AdditionalCommands = "ADDITIONAL COMMANDS"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OrderCommands(TyperGroup):
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
The following code snippet is taken from https://github.com/tiangolo/typer/discussions/855 (see comment
|
|
35
|
+
https://github.com/tiangolo/typer/discussions/855#discussioncomment-9824582) and adapted to our use case.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def list_commands(self, _ctx: Context) -> list[str]:
|
|
39
|
+
order = list(Panel)
|
|
40
|
+
grouped_commands = {
|
|
41
|
+
name: getattr(command, "rich_help_panel")
|
|
42
|
+
for name, command in sorted(self.commands.items())
|
|
43
|
+
if getattr(command, "rich_help_panel") in order
|
|
44
|
+
}
|
|
45
|
+
ungrouped_command_names = [
|
|
46
|
+
command.name
|
|
47
|
+
for command in self.commands.values()
|
|
48
|
+
if command.name not in grouped_commands
|
|
49
|
+
]
|
|
50
|
+
return [
|
|
51
|
+
name
|
|
52
|
+
for name, command in sorted(
|
|
53
|
+
grouped_commands.items(),
|
|
54
|
+
key=lambda item: order.index(item[1]),
|
|
55
|
+
)
|
|
56
|
+
] + sorted(ungrouped_command_names)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
app = typer.Typer(
|
|
60
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
61
|
+
no_args_is_help=True,
|
|
62
|
+
cls=OrderCommands,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
app.add_typer(project, rich_help_panel=Panel.Commands)
|
|
66
|
+
app.add_typer(mission, rich_help_panel=Panel.Commands)
|
|
67
|
+
|
|
68
|
+
app.add_typer(topic, rich_help_panel=Panel.Commands)
|
|
69
|
+
app.add_typer(file, rich_help_panel=Panel.Commands)
|
|
70
|
+
app.add_typer(queue, rich_help_panel=Panel.Commands)
|
|
71
|
+
app.add_typer(user, rich_help_panel=Panel.Commands)
|
|
72
|
+
app.add_typer(tag, rich_help_panel=Panel.Commands)
|
|
73
|
+
|
|
74
|
+
app.command(rich_help_panel=Panel.CoreCommands)(login)
|
|
75
|
+
app.command(rich_help_panel=Panel.CoreCommands)(logout)
|
|
76
|
+
app.command(rich_help_panel=Panel.AdditionalCommands)(endpoint)
|
|
77
|
+
app.command(rich_help_panel=Panel.AdditionalCommands)(setEndpoint)
|
|
78
|
+
app.command(hidden=True)(setCliKey)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.command("download", rich_help_panel=Panel.CoreCommands)
|
|
82
|
+
def download():
|
|
83
|
+
raise NotImplementedError("Not implemented yet.")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("upload", rich_help_panel=Panel.CoreCommands)
|
|
87
|
+
def upload(
|
|
88
|
+
path: Annotated[
|
|
89
|
+
str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")
|
|
90
|
+
],
|
|
91
|
+
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
92
|
+
mission: Annotated[
|
|
93
|
+
str, typer.Option(prompt=True, help="Name of Mission to create")
|
|
94
|
+
],
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Upload files matching the path to a mission in a project.
|
|
98
|
+
|
|
99
|
+
The mission name must be unique within the project and not yet created.\n
|
|
100
|
+
Examples:\n
|
|
101
|
+
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1"'\n
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
files = expand_and_match(path)
|
|
105
|
+
filenames = list(
|
|
106
|
+
map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
|
|
107
|
+
)
|
|
108
|
+
if not filenames:
|
|
109
|
+
print("No files found")
|
|
110
|
+
return
|
|
111
|
+
filepaths = {}
|
|
112
|
+
for path in files:
|
|
113
|
+
if not os.path.isdir(path):
|
|
114
|
+
filepaths[path.split("/")[-1]] = path
|
|
115
|
+
print(f" - {path}")
|
|
116
|
+
try:
|
|
117
|
+
client = AuthenticatedClient()
|
|
118
|
+
|
|
119
|
+
get_project_url = "/project/byName"
|
|
120
|
+
project_response = client.get(get_project_url, params={"name": project})
|
|
121
|
+
if project_response.status_code >= 400:
|
|
122
|
+
print(f"Failed to fetch project: {project_response.text}")
|
|
123
|
+
return
|
|
124
|
+
project_json = project_response.json()
|
|
125
|
+
if not project_json["uuid"]:
|
|
126
|
+
print(f"Project not found: {project}")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
get_mission_url = "/mission/byName"
|
|
130
|
+
mission_response = client.get(get_mission_url, params={"name": mission})
|
|
131
|
+
mission_response.raise_for_status()
|
|
132
|
+
if mission_response.content:
|
|
133
|
+
mission_json = mission_response.json()
|
|
134
|
+
if mission_json["uuid"]:
|
|
135
|
+
print(
|
|
136
|
+
f"mission: {mission_json['uuid']} already exists. Delete it or select another name."
|
|
137
|
+
)
|
|
138
|
+
return
|
|
139
|
+
print(f"Something failed, should not happen")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
create_mission_url = "/mission/create"
|
|
143
|
+
new_mission = client.post(
|
|
144
|
+
create_mission_url,
|
|
145
|
+
json={"name": mission, "projectUUID": project_json["uuid"], "tags": []},
|
|
146
|
+
)
|
|
147
|
+
new_mission.raise_for_status()
|
|
148
|
+
new_mission_data = new_mission.json()
|
|
149
|
+
print(f"Created mission: {new_mission_data['name']}")
|
|
150
|
+
|
|
151
|
+
get_presigned_url = "/queue/createPreSignedURLS"
|
|
152
|
+
|
|
153
|
+
response_2 = client.post(
|
|
154
|
+
get_presigned_url,
|
|
155
|
+
json={"filenames": filenames, "missionUUID": new_mission_data["uuid"]},
|
|
156
|
+
)
|
|
157
|
+
response_2.raise_for_status()
|
|
158
|
+
presigned_urls = response_2.json()
|
|
159
|
+
for file in filenames:
|
|
160
|
+
if not file in presigned_urls.keys():
|
|
161
|
+
print("Could not upload File '" + file + "'. Is the filename unique? ")
|
|
162
|
+
if len(presigned_urls) > 0:
|
|
163
|
+
uploadFiles(presigned_urls, filepaths, 4)
|
|
164
|
+
|
|
165
|
+
except httpx.HTTPError as e:
|
|
166
|
+
print(e)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@queue.command("clear")
|
|
170
|
+
def clear_queue():
|
|
171
|
+
"""Clear queue"""
|
|
172
|
+
# Prompt the user for confirmation
|
|
173
|
+
confirmation = typer.prompt("Are you sure you want to clear the queue? (y/n)")
|
|
174
|
+
if confirmation.lower() == "y":
|
|
175
|
+
client = AuthenticatedClient()
|
|
176
|
+
response = client.delete("/queue/clear")
|
|
177
|
+
response.raise_for_status()
|
|
178
|
+
print("Queue cleared.")
|
|
179
|
+
else:
|
|
180
|
+
print("Operation cancelled.")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@queue.command("list")
|
|
184
|
+
def list_queue():
|
|
185
|
+
"""List current Queue entities"""
|
|
186
|
+
try:
|
|
187
|
+
url = "/queue/active"
|
|
188
|
+
startDate = datetime.now().date() - timedelta(days=1)
|
|
189
|
+
client = AuthenticatedClient()
|
|
190
|
+
response = client.get(url, params={"startDate": startDate})
|
|
191
|
+
response.raise_for_status()
|
|
192
|
+
data = response.json()
|
|
193
|
+
table = Table("UUID", "filename", "mission", "state", "origin", "createdAt")
|
|
194
|
+
for topic in data:
|
|
195
|
+
table.add_row(
|
|
196
|
+
topic["uuid"],
|
|
197
|
+
topic["filename"],
|
|
198
|
+
topic["mission"]["name"],
|
|
199
|
+
topic["state"],
|
|
200
|
+
topic["location"],
|
|
201
|
+
topic["createdAt"],
|
|
202
|
+
)
|
|
203
|
+
print(table)
|
|
204
|
+
|
|
205
|
+
except httpx.HTTPError as e:
|
|
206
|
+
print(e)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command("wipe", hidden=True)
|
|
210
|
+
def wipe():
|
|
211
|
+
"""Wipe all data"""
|
|
212
|
+
# Prompt the user for confirmation
|
|
213
|
+
confirmation = typer.prompt("Are you sure you want to wipe all data? (y/n)")
|
|
214
|
+
if confirmation.lower() == "y":
|
|
215
|
+
second_confirmation = typer.prompt(
|
|
216
|
+
"This action is irreversible. Are you really sure? (y/n)"
|
|
217
|
+
)
|
|
218
|
+
if second_confirmation.lower() != "y":
|
|
219
|
+
print("Operation cancelled.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
client = AuthenticatedClient()
|
|
223
|
+
response_queue = client.delete("/queue/clear")
|
|
224
|
+
response_file = client.delete("/file/clear")
|
|
225
|
+
response_analysis = client.delete("/analysis/clear")
|
|
226
|
+
response_mission = client.delete("/mission/clear")
|
|
227
|
+
response_project = client.delete("/project/clear")
|
|
228
|
+
|
|
229
|
+
if response_queue.status_code >= 400:
|
|
230
|
+
print("Failed to clear queue.")
|
|
231
|
+
print(response_queue.text)
|
|
232
|
+
elif response_file.status_code >= 400:
|
|
233
|
+
print("Failed to clear files.")
|
|
234
|
+
print(response_file.text)
|
|
235
|
+
elif response_analysis.status_code >= 400:
|
|
236
|
+
print("Failed to clear analysis.")
|
|
237
|
+
print(response_analysis.text)
|
|
238
|
+
elif response_mission.status_code >= 400:
|
|
239
|
+
print("Failed to clear missions.")
|
|
240
|
+
print(response_mission.text)
|
|
241
|
+
elif response_project.status_code >= 400:
|
|
242
|
+
print("Failed to clear projects.")
|
|
243
|
+
print(response_project.text)
|
|
244
|
+
else:
|
|
245
|
+
print("Data wiped.")
|
|
246
|
+
else:
|
|
247
|
+
print("Operation cancelled.")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
@app.command("claim", hidden=True)
|
|
251
|
+
def claim():
|
|
252
|
+
"""
|
|
253
|
+
Claim admin rights as the first user
|
|
254
|
+
|
|
255
|
+
Only works if no other user has claimed admin rights before.
|
|
256
|
+
"""
|
|
257
|
+
|
|
258
|
+
client = AuthenticatedClient()
|
|
259
|
+
response = client.post("/user/claimAdmin")
|
|
260
|
+
response.raise_for_status()
|
|
261
|
+
print("Admin claimed.")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
app()
|