kleinkram 0.4.0.dev20240808080202__py3-none-any.whl → 0.4.0.dev20240808144850__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/api_client.py +72 -0
- kleinkram/{auth.py → auth/auth.py} +15 -62
- kleinkram/file/file.py +109 -0
- kleinkram/helper.py +5 -5
- kleinkram/main.py +68 -369
- kleinkram/mission/mission.py +126 -0
- kleinkram/project/project.py +57 -0
- kleinkram/queue/queue.py +8 -0
- kleinkram/tag/tag.py +48 -0
- kleinkram/topic/topic.py +54 -0
- kleinkram/user/user.py +53 -0
- {kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/METADATA +1 -1
- kleinkram-0.4.0.dev20240808144850.dist-info/RECORD +18 -0
- kleinkram-0.4.0.dev20240808080202.dist-info/RECORD +0 -10
- {kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/WHEEL +0 -0
- {kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/licenses/LICENSE +0 -0
kleinkram/api_client.py
ADDED
|
@@ -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
|
|
@@ -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
|
|
kleinkram/file/file.py
ADDED
|
@@ -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.")
|
kleinkram/helper.py
CHANGED
|
@@ -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(
|
kleinkram/main.py
CHANGED
|
@@ -1,273 +1,89 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
from datetime import datetime, timedelta
|
|
3
1
|
import os
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from enum import Enum
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
import typer
|
|
7
|
-
from typing import List, Optional
|
|
8
|
-
from typing_extensions import Annotated
|
|
9
7
|
from rich import print
|
|
10
8
|
from rich.table import Table
|
|
9
|
+
from typer.core import TyperGroup
|
|
10
|
+
from typer.models import Context
|
|
11
|
+
from typing_extensions import Annotated
|
|
11
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
|
|
12
22
|
from .helper import uploadFiles, expand_and_match
|
|
13
23
|
|
|
14
|
-
from .auth import login, client, endpoint, setCliKey, setEndpoint
|
|
15
|
-
|
|
16
|
-
app = typer.Typer()
|
|
17
|
-
projects = typer.Typer(name="projects", help="Project operations")
|
|
18
|
-
missions = typer.Typer(name="missions", help="Mission operations")
|
|
19
|
-
files = typer.Typer(name="files", help="File operations")
|
|
20
|
-
topics = typer.Typer(name="topics", help="Topic operations")
|
|
21
|
-
queue = typer.Typer(name="queue", help="Status of files uploading")
|
|
22
|
-
user = typer.Typer(name="users", help="User operations")
|
|
23
|
-
tagtypes = typer.Typer(name="tagtypes", help="TagType operations")
|
|
24
|
-
tag = typer.Typer(name="tag", help="Tag operations")
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
app.add_typer(projects)
|
|
28
|
-
app.add_typer(missions)
|
|
29
|
-
app.add_typer(topics)
|
|
30
|
-
app.add_typer(files)
|
|
31
|
-
app.add_typer(queue)
|
|
32
|
-
app.add_typer(user)
|
|
33
|
-
app.add_typer(tagtypes)
|
|
34
|
-
app.add_typer(tag)
|
|
35
|
-
app.command()(login)
|
|
36
|
-
app.command()(endpoint)
|
|
37
|
-
app.command()(setEndpoint)
|
|
38
|
-
app.command(hidden=True)(setCliKey)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@files.command("list")
|
|
42
|
-
def list_files(
|
|
43
|
-
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
44
|
-
mission: Optional[str] = typer.Option(None, help="Name of Mission"),
|
|
45
|
-
topics: Optional[str] = typer.Option(None, help="Comma separated list of topics"),
|
|
46
|
-
tags: Optional[str] = typer.Option(
|
|
47
|
-
None, help="Comma separated list of tagtype:tagvalue pairs"
|
|
48
|
-
),
|
|
49
|
-
):
|
|
50
|
-
"""
|
|
51
|
-
List all files with optional filters for project, mission, or topics.
|
|
52
|
-
|
|
53
|
-
Can list files of a project, mission, or with specific topics (Logical AND).
|
|
54
|
-
Examples:\n
|
|
55
|
-
- 'klein files list'\n
|
|
56
|
-
- 'klein files list --project "Project 1"'\n
|
|
57
|
-
- 'klein files list --mission "Mission 1"'\n
|
|
58
|
-
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw"'\n
|
|
59
|
-
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw" --mission "Mission A"'
|
|
60
|
-
"""
|
|
61
|
-
try:
|
|
62
|
-
url = f"/file/filteredByNames"
|
|
63
|
-
params = {}
|
|
64
|
-
if project:
|
|
65
|
-
params["projectName"] = project
|
|
66
|
-
if mission:
|
|
67
|
-
params["missionName"] = mission
|
|
68
|
-
if topics:
|
|
69
|
-
params["topics"] = topics
|
|
70
|
-
if tags:
|
|
71
|
-
params["tags"] = {}
|
|
72
|
-
for tag in tags.split(","):
|
|
73
|
-
tagtype, tagvalue = tag.split("§")
|
|
74
|
-
params["tags"][tagtype] = tagvalue
|
|
75
|
-
response = client.get(
|
|
76
|
-
url,
|
|
77
|
-
params=params,
|
|
78
|
-
)
|
|
79
|
-
response.raise_for_status()
|
|
80
|
-
data = response.json()
|
|
81
|
-
missions_by_project_uuid = {}
|
|
82
|
-
files_by_mission_uuid = {}
|
|
83
|
-
for file in data:
|
|
84
|
-
mission_uuid = file["mission"]["uuid"]
|
|
85
|
-
project_uuid = file["mission"]["project"]["uuid"]
|
|
86
|
-
if project_uuid not in missions_by_project_uuid:
|
|
87
|
-
missions_by_project_uuid[project_uuid] = []
|
|
88
|
-
if mission_uuid not in missions_by_project_uuid[project_uuid]:
|
|
89
|
-
missions_by_project_uuid[project_uuid].append(mission_uuid)
|
|
90
|
-
if mission_uuid not in files_by_mission_uuid:
|
|
91
|
-
files_by_mission_uuid[mission_uuid] = []
|
|
92
|
-
files_by_mission_uuid[mission_uuid].append(file)
|
|
93
|
-
|
|
94
|
-
print("Files by mission & Project:")
|
|
95
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
96
|
-
first_file = files_by_mission_uuid[missions[0]][0]
|
|
97
|
-
print(f"* {first_file['mission']['project']['name']}")
|
|
98
|
-
for mission in missions:
|
|
99
|
-
print(f" - {files_by_mission_uuid[mission][0]['mission']['name']}")
|
|
100
|
-
for file in files_by_mission_uuid[mission]:
|
|
101
|
-
print(f" - '{file['filename']}'")
|
|
102
24
|
|
|
103
|
-
|
|
104
|
-
|
|
25
|
+
class Panel(str, Enum):
|
|
26
|
+
CoreCommands = "CORE COMMANDS"
|
|
27
|
+
Commands = "COMMANDS"
|
|
28
|
+
AdditionalCommands = "ADDITIONAL COMMANDS"
|
|
105
29
|
|
|
106
30
|
|
|
107
|
-
|
|
108
|
-
def list_projects():
|
|
109
|
-
"""
|
|
110
|
-
List all projects.
|
|
31
|
+
class OrderCommands(TyperGroup):
|
|
111
32
|
"""
|
|
112
|
-
try:
|
|
113
|
-
response = client.get("/project")
|
|
114
|
-
response.raise_for_status()
|
|
115
|
-
projects = response.json()
|
|
116
|
-
print("Projects:")
|
|
117
|
-
for project in projects:
|
|
118
|
-
print(f"- {project['name']}")
|
|
119
|
-
|
|
120
|
-
except httpx.HTTPError as e:
|
|
121
|
-
print(f"Failed to fetch projects: {e}")
|
|
122
|
-
|
|
123
33
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
127
|
-
verbose: Optional[bool] = typer.Option(
|
|
128
|
-
False, help="Outputs a table with more information"
|
|
129
|
-
),
|
|
130
|
-
):
|
|
131
|
-
"""
|
|
132
|
-
List all missions with optional filter for project.
|
|
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.
|
|
133
36
|
"""
|
|
134
|
-
try:
|
|
135
|
-
url = "/mission"
|
|
136
|
-
if project:
|
|
137
|
-
url += f"/filteredByProjectName/{project}"
|
|
138
|
-
else:
|
|
139
|
-
url += "/all"
|
|
140
|
-
response = client.get(url)
|
|
141
|
-
response.raise_for_status()
|
|
142
|
-
data = response.json()
|
|
143
|
-
missions_by_project_uuid = {}
|
|
144
|
-
for mission in data:
|
|
145
|
-
project_uuid = mission["project"]["uuid"]
|
|
146
|
-
if project_uuid not in missions_by_project_uuid:
|
|
147
|
-
missions_by_project_uuid[project_uuid] = []
|
|
148
|
-
missions_by_project_uuid[project_uuid].append(mission)
|
|
149
|
-
|
|
150
|
-
print("missions by Project:")
|
|
151
|
-
if not verbose:
|
|
152
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
153
|
-
print(
|
|
154
|
-
f"* {missions_by_project_uuid[project_uuid][0]['project']['name']}"
|
|
155
|
-
)
|
|
156
|
-
for mission in missions:
|
|
157
|
-
print(f" - {mission['name']}")
|
|
158
|
-
else:
|
|
159
|
-
table = Table("UUID", "name", "project", "creator", "createdAt")
|
|
160
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
161
|
-
for mission in missions:
|
|
162
|
-
table.add_row(
|
|
163
|
-
mission["uuid"],
|
|
164
|
-
mission["name"],
|
|
165
|
-
mission["project"]["name"],
|
|
166
|
-
mission["creator"]["name"],
|
|
167
|
-
mission["createdAt"],
|
|
168
|
-
)
|
|
169
|
-
print(table)
|
|
170
|
-
|
|
171
|
-
except httpx.HTTPError as e:
|
|
172
|
-
print(f"Failed to fetch missions: {e}")
|
|
173
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)
|
|
174
57
|
|
|
175
|
-
@missions.command("byUUID")
|
|
176
|
-
def mission_by_uuid(
|
|
177
|
-
uuid: Annotated[str, typer.Argument()],
|
|
178
|
-
json: Optional[bool] = typer.Option(False, help="Output as JSON"),
|
|
179
|
-
):
|
|
180
|
-
"""
|
|
181
|
-
Get mission name, project name, creator and table of its files given a Mission UUID
|
|
182
58
|
|
|
183
|
-
|
|
59
|
+
app = typer.Typer(
|
|
60
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
61
|
+
no_args_is_help=True,
|
|
62
|
+
cls=OrderCommands,
|
|
63
|
+
)
|
|
184
64
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
try:
|
|
188
|
-
url = "/mission/byUUID"
|
|
189
|
-
response = client.get(url, params={"uuid": uuid})
|
|
190
|
-
response.raise_for_status()
|
|
191
|
-
data = response.json()
|
|
192
|
-
if json:
|
|
193
|
-
print(data)
|
|
194
|
-
else:
|
|
195
|
-
print(f"mission: {data['name']}")
|
|
196
|
-
print(f"Creator: {data['creator']['name']}")
|
|
197
|
-
print("Project: " + data["project"]["name"])
|
|
198
|
-
table = Table("Filename", "Size", "date")
|
|
199
|
-
for file in data["files"]:
|
|
200
|
-
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
201
|
-
print(table)
|
|
202
|
-
except httpx.HTTPError as e:
|
|
203
|
-
print(f"Failed to fetch missions: {e}")
|
|
65
|
+
app.add_typer(project, rich_help_panel=Panel.Commands)
|
|
66
|
+
app.add_typer(mission, rich_help_panel=Panel.Commands)
|
|
204
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)
|
|
205
73
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
] = False,
|
|
212
|
-
# Todo add mission / project as optional argument as filenames are not unique or handle multiple files
|
|
213
|
-
):
|
|
214
|
-
"""
|
|
215
|
-
List topics for a file
|
|
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)
|
|
216
79
|
|
|
217
|
-
Only makes sense with MCAP files as we don't associate topics with BAGs as that would be redundant.
|
|
218
|
-
"""
|
|
219
|
-
if file.endswith(".bag"):
|
|
220
|
-
print("BAG files generally do not have topics")
|
|
221
|
-
try:
|
|
222
|
-
url = "/file/byName"
|
|
223
|
-
response = client.get(url, params={"name": file})
|
|
224
|
-
response.raise_for_status()
|
|
225
|
-
data = response.json()
|
|
226
|
-
if not full:
|
|
227
|
-
for topic in data["topics"]:
|
|
228
|
-
print(f" - {topic['name']}")
|
|
229
|
-
else:
|
|
230
|
-
table = Table("UUID", "name", "type", "nrMessages", "frequency")
|
|
231
|
-
for topic in data["topics"]:
|
|
232
|
-
table.add_row(
|
|
233
|
-
topic["uuid"],
|
|
234
|
-
topic["name"],
|
|
235
|
-
topic["type"],
|
|
236
|
-
topic["nrMessages"],
|
|
237
|
-
f"{topic['frequency']}",
|
|
238
|
-
)
|
|
239
|
-
print(table)
|
|
240
80
|
|
|
241
|
-
|
|
242
|
-
|
|
81
|
+
@app.command("download", rich_help_panel=Panel.CoreCommands)
|
|
82
|
+
def download():
|
|
83
|
+
raise NotImplementedError("Not implemented yet.")
|
|
243
84
|
|
|
244
85
|
|
|
245
|
-
@
|
|
246
|
-
def create_project(
|
|
247
|
-
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
248
|
-
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
249
|
-
):
|
|
250
|
-
"""
|
|
251
|
-
Create a new project
|
|
252
|
-
"""
|
|
253
|
-
# Todo add required tags as option.
|
|
254
|
-
try:
|
|
255
|
-
url = "/project/create"
|
|
256
|
-
response = client.post(
|
|
257
|
-
url, json={"name": name, "description": description, "requiredTags": []}
|
|
258
|
-
) # TODO: Add required tags as option
|
|
259
|
-
if response.status_code >= 400:
|
|
260
|
-
response_json = response.json()
|
|
261
|
-
response_text = response_json["message"]
|
|
262
|
-
print(f"Failed to create project: {response_text}")
|
|
263
|
-
return
|
|
264
|
-
print("Project created")
|
|
265
|
-
|
|
266
|
-
except httpx.HTTPError as e:
|
|
267
|
-
print(f"Failed to create project: {e}")
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
@app.command("upload")
|
|
86
|
+
@app.command("upload", rich_help_panel=Panel.CoreCommands)
|
|
271
87
|
def upload(
|
|
272
88
|
path: Annotated[
|
|
273
89
|
str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")
|
|
@@ -298,6 +114,8 @@ def upload(
|
|
|
298
114
|
filepaths[path.split("/")[-1]] = path
|
|
299
115
|
print(f" - {path}")
|
|
300
116
|
try:
|
|
117
|
+
client = AuthenticatedClient()
|
|
118
|
+
|
|
301
119
|
get_project_url = "/project/byName"
|
|
302
120
|
project_response = client.get(get_project_url, params={"name": project})
|
|
303
121
|
if project_response.status_code >= 400:
|
|
@@ -354,6 +172,7 @@ def clear_queue():
|
|
|
354
172
|
# Prompt the user for confirmation
|
|
355
173
|
confirmation = typer.prompt("Are you sure you want to clear the queue? (y/n)")
|
|
356
174
|
if confirmation.lower() == "y":
|
|
175
|
+
client = AuthenticatedClient()
|
|
357
176
|
response = client.delete("/queue/clear")
|
|
358
177
|
response.raise_for_status()
|
|
359
178
|
print("Queue cleared.")
|
|
@@ -367,6 +186,7 @@ def list_queue():
|
|
|
367
186
|
try:
|
|
368
187
|
url = "/queue/active"
|
|
369
188
|
startDate = datetime.now().date() - timedelta(days=1)
|
|
189
|
+
client = AuthenticatedClient()
|
|
370
190
|
response = client.get(url, params={"startDate": startDate})
|
|
371
191
|
response.raise_for_status()
|
|
372
192
|
data = response.json()
|
|
@@ -386,19 +206,6 @@ def list_queue():
|
|
|
386
206
|
print(e)
|
|
387
207
|
|
|
388
208
|
|
|
389
|
-
@files.command("clear")
|
|
390
|
-
def clear_queue():
|
|
391
|
-
"""Clear queue"""
|
|
392
|
-
# Prompt the user for confirmation
|
|
393
|
-
confirmation = typer.prompt("Are you sure you want to clear the Files? (y/n)")
|
|
394
|
-
if confirmation.lower() == "y":
|
|
395
|
-
response = client.delete("/file/clear")
|
|
396
|
-
response.raise_for_status()
|
|
397
|
-
print("Files cleared.")
|
|
398
|
-
else:
|
|
399
|
-
print("Operation cancelled.")
|
|
400
|
-
|
|
401
|
-
|
|
402
209
|
@app.command("wipe", hidden=True)
|
|
403
210
|
def wipe():
|
|
404
211
|
"""Wipe all data"""
|
|
@@ -412,6 +219,7 @@ def wipe():
|
|
|
412
219
|
print("Operation cancelled.")
|
|
413
220
|
return
|
|
414
221
|
|
|
222
|
+
client = AuthenticatedClient()
|
|
415
223
|
response_queue = client.delete("/queue/clear")
|
|
416
224
|
response_file = client.delete("/file/clear")
|
|
417
225
|
response_analysis = client.delete("/analysis/clear")
|
|
@@ -446,121 +254,12 @@ def claim():
|
|
|
446
254
|
|
|
447
255
|
Only works if no other user has claimed admin rights before.
|
|
448
256
|
"""
|
|
257
|
+
|
|
258
|
+
client = AuthenticatedClient()
|
|
449
259
|
response = client.post("/user/claimAdmin")
|
|
450
260
|
response.raise_for_status()
|
|
451
261
|
print("Admin claimed.")
|
|
452
262
|
|
|
453
263
|
|
|
454
|
-
@user.command("list")
|
|
455
|
-
def users():
|
|
456
|
-
"""List all users"""
|
|
457
|
-
response = client.get("/user/all")
|
|
458
|
-
response.raise_for_status()
|
|
459
|
-
data = response.json()
|
|
460
|
-
table = Table("Name", "Email", "Role", "googleId")
|
|
461
|
-
for user in data:
|
|
462
|
-
table.add_row(user["name"], user["email"], user["role"], user["googleId"])
|
|
463
|
-
print(table)
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
@user.command("info")
|
|
467
|
-
def user_info():
|
|
468
|
-
"""Get logged in user info"""
|
|
469
|
-
response = client.get("/user/me")
|
|
470
|
-
response.raise_for_status()
|
|
471
|
-
data = response.json()
|
|
472
|
-
print(data)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
@user.command("promote")
|
|
476
|
-
def promote(email: Annotated[str, typer.Option()]):
|
|
477
|
-
"""Promote another user to admin"""
|
|
478
|
-
response = client.post("/user/promote", json={"email": email})
|
|
479
|
-
response.raise_for_status()
|
|
480
|
-
print("User promoted.")
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
@user.command("demote")
|
|
484
|
-
def demote(email: Annotated[str, typer.Option()]):
|
|
485
|
-
"""Demote another user from admin"""
|
|
486
|
-
response = client.post("/user/demote", json={"email": email})
|
|
487
|
-
response.raise_for_status()
|
|
488
|
-
print("User demoted.")
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
@files.command("download")
|
|
492
|
-
def download(
|
|
493
|
-
missionuuid: Annotated[str, typer.Argument()],
|
|
494
|
-
):
|
|
495
|
-
"""Download file"""
|
|
496
|
-
try:
|
|
497
|
-
response = client.get("/file/downloadWithToken", params={"uuid": missionuuid})
|
|
498
|
-
response.raise_for_status()
|
|
499
|
-
print(response.json())
|
|
500
|
-
except:
|
|
501
|
-
print("Failed to download file")
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
@missions.command("tag")
|
|
505
|
-
def addTag(
|
|
506
|
-
missionuuid: Annotated[str, typer.Argument()],
|
|
507
|
-
tagtypeuuid: Annotated[str, typer.Argument()],
|
|
508
|
-
value: Annotated[str, typer.Argument()],
|
|
509
|
-
):
|
|
510
|
-
"""Tag a mission"""
|
|
511
|
-
try:
|
|
512
|
-
response = client.post(
|
|
513
|
-
"/tag/addTag",
|
|
514
|
-
json={"mission": missionuuid, "tagType": tagtypeuuid, "value": value},
|
|
515
|
-
)
|
|
516
|
-
if response.status_code < 400:
|
|
517
|
-
print("Tagged mission")
|
|
518
|
-
else:
|
|
519
|
-
print(response.json())
|
|
520
|
-
print("Failed to tag mission")
|
|
521
|
-
except httpx.HTTPError as e:
|
|
522
|
-
print(e)
|
|
523
|
-
print("Failed to tag mission")
|
|
524
|
-
sys.exit(1)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
@tagtypes.command("list")
|
|
528
|
-
def tagTypes(
|
|
529
|
-
verbose: Annotated[bool, typer.Option()] = False,
|
|
530
|
-
):
|
|
531
|
-
"""List all tagtypes"""
|
|
532
|
-
try:
|
|
533
|
-
response = client.get("/tag/all")
|
|
534
|
-
response.raise_for_status()
|
|
535
|
-
data = response.json()
|
|
536
|
-
if verbose:
|
|
537
|
-
table = Table("UUID", "Name", "Datatype")
|
|
538
|
-
for tagtype in data:
|
|
539
|
-
table.add_row(tagtype["uuid"], tagtype["name"], tagtype["datatype"])
|
|
540
|
-
else:
|
|
541
|
-
table = Table("Name", "Datatype")
|
|
542
|
-
for tagtype in data:
|
|
543
|
-
table.add_row(tagtype["name"], tagtype["datatype"])
|
|
544
|
-
print(table)
|
|
545
|
-
except:
|
|
546
|
-
print("Failed to fetch tagtypes")
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
@tag.command("delete")
|
|
550
|
-
def deleteTag(
|
|
551
|
-
taguuid: Annotated[str, typer.Argument()],
|
|
552
|
-
):
|
|
553
|
-
"""Delete a tag"""
|
|
554
|
-
try:
|
|
555
|
-
response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
|
|
556
|
-
if response.status_code < 400:
|
|
557
|
-
print("Deleted tag")
|
|
558
|
-
else:
|
|
559
|
-
print(response)
|
|
560
|
-
print("Failed to delete tag")
|
|
561
|
-
except:
|
|
562
|
-
print("Failed to delete tag")
|
|
563
|
-
|
|
564
|
-
|
|
565
264
|
if __name__ == "__main__":
|
|
566
265
|
app()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Annotated, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
10
|
+
|
|
11
|
+
mission = typer.Typer(
|
|
12
|
+
name="mission",
|
|
13
|
+
help="Mission operations",
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mission.command("tag")
|
|
20
|
+
def addTag(
|
|
21
|
+
missionuuid: Annotated[str, typer.Argument()],
|
|
22
|
+
tagtypeuuid: Annotated[str, typer.Argument()],
|
|
23
|
+
value: Annotated[str, typer.Argument()],
|
|
24
|
+
):
|
|
25
|
+
"""Tag a mission"""
|
|
26
|
+
try:
|
|
27
|
+
client = AuthenticatedClient()
|
|
28
|
+
response = client.post(
|
|
29
|
+
"/tag/addTag",
|
|
30
|
+
json={"mission": missionuuid, "tagType": tagtypeuuid, "value": value},
|
|
31
|
+
)
|
|
32
|
+
if response.status_code < 400:
|
|
33
|
+
print("Tagged mission")
|
|
34
|
+
else:
|
|
35
|
+
print(response.json())
|
|
36
|
+
print("Failed to tag mission")
|
|
37
|
+
except httpx.HTTPError as e:
|
|
38
|
+
print(e)
|
|
39
|
+
print("Failed to tag mission")
|
|
40
|
+
sys.exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mission.command("list")
|
|
44
|
+
def list_missions(
|
|
45
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
46
|
+
verbose: Optional[bool] = typer.Option(
|
|
47
|
+
False, help="Outputs a table with more information"
|
|
48
|
+
),
|
|
49
|
+
):
|
|
50
|
+
"""
|
|
51
|
+
List all missions with optional filter for project.
|
|
52
|
+
"""
|
|
53
|
+
try:
|
|
54
|
+
url = "/mission"
|
|
55
|
+
if project:
|
|
56
|
+
url += f"/filteredByProjectName/{project}"
|
|
57
|
+
else:
|
|
58
|
+
url += "/all"
|
|
59
|
+
client = AuthenticatedClient()
|
|
60
|
+
|
|
61
|
+
response = client.get(url)
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
data = response.json()
|
|
64
|
+
missions_by_project_uuid = {}
|
|
65
|
+
for mission in data:
|
|
66
|
+
project_uuid = mission["project"]["uuid"]
|
|
67
|
+
if project_uuid not in missions_by_project_uuid:
|
|
68
|
+
missions_by_project_uuid[project_uuid] = []
|
|
69
|
+
missions_by_project_uuid[project_uuid].append(mission)
|
|
70
|
+
|
|
71
|
+
print("missions by Project:")
|
|
72
|
+
if not verbose:
|
|
73
|
+
for project_uuid, missions in missions_by_project_uuid.items():
|
|
74
|
+
print(
|
|
75
|
+
f"* {missions_by_project_uuid[project_uuid][0]['project']['name']}"
|
|
76
|
+
)
|
|
77
|
+
for mission in missions:
|
|
78
|
+
print(f" - {mission['name']}")
|
|
79
|
+
else:
|
|
80
|
+
table = Table("UUID", "name", "project", "creator", "createdAt")
|
|
81
|
+
for project_uuid, missions in missions_by_project_uuid.items():
|
|
82
|
+
for mission in missions:
|
|
83
|
+
table.add_row(
|
|
84
|
+
mission["uuid"],
|
|
85
|
+
mission["name"],
|
|
86
|
+
mission["project"]["name"],
|
|
87
|
+
mission["creator"]["name"],
|
|
88
|
+
mission["createdAt"],
|
|
89
|
+
)
|
|
90
|
+
print(table)
|
|
91
|
+
|
|
92
|
+
except httpx.HTTPError as e:
|
|
93
|
+
print(f"Failed to fetch missions: {e}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mission.command("byUUID")
|
|
97
|
+
def mission_by_uuid(
|
|
98
|
+
uuid: Annotated[str, typer.Argument()],
|
|
99
|
+
json: Optional[bool] = typer.Option(False, help="Output as JSON"),
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Get mission name, project name, creator and table of its files given a Mission UUID
|
|
103
|
+
|
|
104
|
+
Use the JSON flag to output the full JSON response instead.
|
|
105
|
+
|
|
106
|
+
Can be run with API Key or with login.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
url = "/mission/byUUID"
|
|
110
|
+
client = AuthenticatedClient()
|
|
111
|
+
response = client.get(url, params={"uuid": uuid})
|
|
112
|
+
response.raise_for_status()
|
|
113
|
+
data = response.json()
|
|
114
|
+
if json:
|
|
115
|
+
print(data)
|
|
116
|
+
else:
|
|
117
|
+
print(f"mission: {data['name']}")
|
|
118
|
+
print(f"Creator: {data['creator']['name']}")
|
|
119
|
+
print("Project: " + data["project"]["name"])
|
|
120
|
+
table = Table("Filename", "Size", "date")
|
|
121
|
+
for file in data["files"]:
|
|
122
|
+
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
123
|
+
console = Console()
|
|
124
|
+
console.print(table)
|
|
125
|
+
except httpx.HTTPError as e:
|
|
126
|
+
print(f"Failed to fetch missions: {e}")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing 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")
|
|
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}")
|
kleinkram/queue/queue.py
ADDED
kleinkram/tag/tag.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
7
|
+
|
|
8
|
+
tag = typer.Typer(name="tag", help="Tag operations")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@tag.command("list-tag-types")
|
|
12
|
+
def tagTypes(
|
|
13
|
+
verbose: Annotated[bool, typer.Option()] = False,
|
|
14
|
+
):
|
|
15
|
+
"""List all tagtypes"""
|
|
16
|
+
try:
|
|
17
|
+
client = AuthenticatedClient()
|
|
18
|
+
response = client.get("/tag/all")
|
|
19
|
+
response.raise_for_status()
|
|
20
|
+
data = response.json()
|
|
21
|
+
if verbose:
|
|
22
|
+
table = Table("UUID", "Name", "Datatype")
|
|
23
|
+
for tagtype in data:
|
|
24
|
+
table.add_row(tagtype["uuid"], tagtype["name"], tagtype["datatype"])
|
|
25
|
+
else:
|
|
26
|
+
table = Table("Name", "Datatype")
|
|
27
|
+
for tagtype in data:
|
|
28
|
+
table.add_row(tagtype["name"], tagtype["datatype"])
|
|
29
|
+
print(table)
|
|
30
|
+
except:
|
|
31
|
+
print("Failed to fetch tagtypes")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@tag.command("delete")
|
|
35
|
+
def deleteTag(
|
|
36
|
+
taguuid: Annotated[str, typer.Argument()],
|
|
37
|
+
):
|
|
38
|
+
"""Delete a tag"""
|
|
39
|
+
try:
|
|
40
|
+
client = AuthenticatedClient()
|
|
41
|
+
response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
|
|
42
|
+
if response.status_code < 400:
|
|
43
|
+
print("Deleted tag")
|
|
44
|
+
else:
|
|
45
|
+
print(response)
|
|
46
|
+
print("Failed to delete tag")
|
|
47
|
+
except:
|
|
48
|
+
print("Failed to delete tag")
|
kleinkram/topic/topic.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import typer
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
8
|
+
|
|
9
|
+
topic = typer.Typer(
|
|
10
|
+
name="topic",
|
|
11
|
+
help="Topic operations",
|
|
12
|
+
no_args_is_help=True,
|
|
13
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@topic.command("list")
|
|
18
|
+
def topics(
|
|
19
|
+
file: Annotated[str, typer.Option(help="Name of File")],
|
|
20
|
+
full: Annotated[
|
|
21
|
+
bool, typer.Option(help="As a table with additional parameters")
|
|
22
|
+
] = False,
|
|
23
|
+
# Todo add mission / project as optional argument as filenames are not unique or handle multiple files
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
List topics for a file
|
|
27
|
+
|
|
28
|
+
Only makes sense with MCAP files as we don't associate topics with BAGs as that would be redundant.
|
|
29
|
+
"""
|
|
30
|
+
if file.endswith(".bag"):
|
|
31
|
+
print("BAG files generally do not have topics")
|
|
32
|
+
try:
|
|
33
|
+
url = "/file/byName"
|
|
34
|
+
client = AuthenticatedClient()
|
|
35
|
+
response = client.get(url, params={"name": file})
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
data = response.json()
|
|
38
|
+
if not full:
|
|
39
|
+
for topic in data["topics"]:
|
|
40
|
+
print(f" - {topic['name']}")
|
|
41
|
+
else:
|
|
42
|
+
table = Table("UUID", "name", "type", "nrMessages", "frequency")
|
|
43
|
+
for topic in data["topics"]:
|
|
44
|
+
table.add_row(
|
|
45
|
+
topic["uuid"],
|
|
46
|
+
topic["name"],
|
|
47
|
+
topic["type"],
|
|
48
|
+
topic["nrMessages"],
|
|
49
|
+
f"{topic['frequency']}",
|
|
50
|
+
)
|
|
51
|
+
print(table)
|
|
52
|
+
|
|
53
|
+
except httpx.HTTPError as e:
|
|
54
|
+
print(f"Failed")
|
kleinkram/user/user.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from kleinkram.api_client import AuthenticatedClient
|
|
7
|
+
|
|
8
|
+
user = typer.Typer(
|
|
9
|
+
name="users",
|
|
10
|
+
help="User operations",
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@user.command("list")
|
|
15
|
+
def users(ctx: typer.Context):
|
|
16
|
+
"""List all users"""
|
|
17
|
+
|
|
18
|
+
client = AuthenticatedClient()
|
|
19
|
+
response = client.get("/user/all")
|
|
20
|
+
response.raise_for_status()
|
|
21
|
+
data = response.json()
|
|
22
|
+
table = Table("Name", "Email", "Role", "googleId")
|
|
23
|
+
for user in data:
|
|
24
|
+
table.add_row(user["name"], user["email"], user["role"], user["googleId"])
|
|
25
|
+
print(table)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@user.command("info")
|
|
29
|
+
def user_info():
|
|
30
|
+
"""Get logged in user info"""
|
|
31
|
+
client = AuthenticatedClient()
|
|
32
|
+
response = client.get("/user/me")
|
|
33
|
+
response.raise_for_status()
|
|
34
|
+
data = response.json()
|
|
35
|
+
print(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@user.command("promote")
|
|
39
|
+
def promote(email: Annotated[str, typer.Option()]):
|
|
40
|
+
"""Promote another user to admin"""
|
|
41
|
+
client = AuthenticatedClient()
|
|
42
|
+
response = client.post("/user/promote", json={"email": email})
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
print("User promoted.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@user.command("demote")
|
|
48
|
+
def demote(email: Annotated[str, typer.Option()]):
|
|
49
|
+
"""Demote another user from admin"""
|
|
50
|
+
client = AuthenticatedClient()
|
|
51
|
+
response = client.post("/user/demote", json={"email": email})
|
|
52
|
+
response.raise_for_status()
|
|
53
|
+
print("User demoted.")
|
{kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: kleinkram
|
|
3
|
-
Version: 0.4.0.
|
|
3
|
+
Version: 0.4.0.dev20240808144850
|
|
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,18 @@
|
|
|
1
|
+
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
kleinkram/api_client.py,sha256=RVfHl5yAIYa-sYnyOoG6MATfmyhND5cp4yCbiGYpWDI,2494
|
|
3
|
+
kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
|
|
4
|
+
kleinkram/helper.py,sha256=9YUuCH0pfj7gK88XRtR0SD-cfdcbR-4g0DdAcUfwdm4,2334
|
|
5
|
+
kleinkram/main.py,sha256=HBPiTMyQ8ykBlkKmMDooVX2J30aaoQ-NPTF_qNE5Tmg,8883
|
|
6
|
+
kleinkram/auth/auth.py,sha256=vk2Qv7v7tb1GziVzVq5ZaiEk39wVDVRBENhUd4pxk1g,5928
|
|
7
|
+
kleinkram/file/file.py,sha256=ti-h72JdUs1UdYK7pElbQ-CpWiBQIEA-BhJ-8lgnDGc,3896
|
|
8
|
+
kleinkram/mission/mission.py,sha256=arKZbSE9bd7fwcT9fpoy3W1na6Vvn-bkyTV2gCLFLng,4041
|
|
9
|
+
kleinkram/project/project.py,sha256=p9fe4XQtGLTMDxHK7ofbe_8Sdv-aZ8I4KgseAn8RK20,1566
|
|
10
|
+
kleinkram/queue/queue.py,sha256=MaLBjAu8asi9BkPvbbT-5AobCcpy3ex5rxM1kHpRINA,181
|
|
11
|
+
kleinkram/tag/tag.py,sha256=0CN4XwcHmFNFc4n05o82bm2LRfm38NcN3U19vQ6Dqfc,1341
|
|
12
|
+
kleinkram/topic/topic.py,sha256=5_Vh38GkJ1eu1_hfIUtmjoxgaprDlbaglmuQmjFmpJc,1614
|
|
13
|
+
kleinkram/user/user.py,sha256=i_QfsctjhImvKKjuDPfOIyDr322SXgV-KxJo-a7qNZw,1368
|
|
14
|
+
kleinkram-0.4.0.dev20240808144850.dist-info/METADATA,sha256=TwAbpkNu5rjtGCu8GiBmO03a_ro7eQAm3VhfvnlBfjA,749
|
|
15
|
+
kleinkram-0.4.0.dev20240808144850.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
16
|
+
kleinkram-0.4.0.dev20240808144850.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
17
|
+
kleinkram-0.4.0.dev20240808144850.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
18
|
+
kleinkram-0.4.0.dev20240808144850.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kleinkram/auth.py,sha256=-XxVw3qLbWHT5Pv-yFyyxagu01RP8FlFLY-P274inYc,7739
|
|
3
|
-
kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
|
|
4
|
-
kleinkram/helper.py,sha256=5JbDBgaWjfenCHsRbNSdVZo7XfDaJ8VK_it7edi0ERU,2252
|
|
5
|
-
kleinkram/main.py,sha256=vhr75e0AXxdUz1nd9q-EwZbMk45uYmm8TxmHKLPAZBU,18655
|
|
6
|
-
kleinkram-0.4.0.dev20240808080202.dist-info/METADATA,sha256=bc9rWfYvtHRiyFtLxOT9mv3YcEGbwpEJOnKqtuVAzek,749
|
|
7
|
-
kleinkram-0.4.0.dev20240808080202.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
-
kleinkram-0.4.0.dev20240808080202.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
9
|
-
kleinkram-0.4.0.dev20240808080202.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
10
|
-
kleinkram-0.4.0.dev20240808080202.dist-info/RECORD,,
|
{kleinkram-0.4.0.dev20240808080202.dist-info → kleinkram-0.4.0.dev20240808144850.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|