kleinkram 0.4.0__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.

@@ -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 pathlib import Path
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
- class AuthenticatedClient(httpx.Client):
91
- def __init__(self, *args, **kwargs):
92
- super().__init__(*args, **kwargs)
93
- try:
94
- self.tokenfile = TokenFile()
95
- self._load_cookies()
96
- except Exception as e:
97
- print(
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
- tokenfile.saveTokens(key)
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 glob
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 .auth import client
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
- except httpx.HTTPError as e:
104
- print(f"Failed to fetch missions: {e}")
25
+ class Panel(str, Enum):
26
+ CoreCommands = "CORE COMMANDS"
27
+ Commands = "COMMANDS"
28
+ AdditionalCommands = "ADDITIONAL COMMANDS"
105
29
 
106
30
 
107
- @projects.command("list")
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
- @missions.command("list")
125
- def list_missions(
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
- Use the JSON flag to output the full JSON response instead.
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
- Can be run with API Key or with login.
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
- @topics.command("list")
207
- def topics(
208
- file: Annotated[str, typer.Option(help="Name of File")],
209
- full: Annotated[
210
- bool, typer.Option(help="As a table with additional parameters")
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
- except httpx.HTTPError as e:
242
- print(f"Failed")
81
+ @app.command("download", rich_help_panel=Panel.CoreCommands)
82
+ def download():
83
+ raise NotImplementedError("Not implemented yet.")
243
84
 
244
85
 
245
- @projects.command("create")
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}")
@@ -0,0 +1,8 @@
1
+ import typer
2
+
3
+ queue = typer.Typer(
4
+ name="queue",
5
+ help="Status of files uploading",
6
+ no_args_is_help=True,
7
+ context_settings={"help_option_names": ["-h", "--help"]},
8
+ )
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")
@@ -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.")
@@ -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.dist-info/METADATA,sha256=Krv_M0yYS5AyrTtCd5U2e2qtLVV6ukPIHkOtKToow6o,731
7
- kleinkram-0.4.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
8
- kleinkram-0.4.0.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
9
- kleinkram-0.4.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
10
- kleinkram-0.4.0.dist-info/RECORD,,