kleinkram 0.6.0.dev20240812074346__tar.gz → 0.6.0.dev20240812135206__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.

Files changed (25) hide show
  1. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/PKG-INFO +1 -1
  2. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/pyproject.toml +1 -1
  3. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/api_client.py +8 -17
  4. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/auth/auth.py +2 -40
  5. kleinkram-0.6.0.dev20240812135206/src/kleinkram/endpoint/endpoint.py +55 -0
  6. kleinkram-0.6.0.dev20240812135206/src/kleinkram/error_handling.py +149 -0
  7. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/file/file.py +4 -15
  8. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/main.py +64 -29
  9. kleinkram-0.6.0.dev20240812135206/src/kleinkram/mission/mission.py +192 -0
  10. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/project/project.py +1 -0
  11. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/tag/tag.py +30 -7
  12. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/topic/topic.py +1 -0
  13. kleinkram-0.6.0.dev20240812074346/src/kleinkram/mission/mission.py +0 -126
  14. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/.gitignore +0 -0
  15. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/LICENSE +0 -0
  16. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/README.md +0 -0
  17. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/deploy.sh +0 -0
  18. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/dev.sh +0 -0
  19. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/requirements.txt +0 -0
  20. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/klein.py +0 -0
  21. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/__init__.py +0 -0
  22. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/consts.py +0 -0
  23. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/helper.py +0 -0
  24. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/queue/queue.py +0 -0
  25. {kleinkram-0.6.0.dev20240812074346 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/user/user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: kleinkram
3
- Version: 0.6.0.dev20240812074346
3
+ Version: 0.6.0.dev20240812135206
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kleinkram"
7
- version = "0.6.0-dev20240812074346"
7
+ version = "0.6.0-dev20240812135206"
8
8
  description = "A CLI for the ETH project kleinkram"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.7"
@@ -1,10 +1,14 @@
1
1
  import httpx
2
- from rich.console import Console
3
- from rich.panel import Panel
4
2
 
5
3
  from kleinkram.auth.auth import TokenFile, CLI_KEY, AUTH_TOKEN, REFRESH_TOKEN
6
4
 
7
5
 
6
+ class NotAuthenticatedException(Exception):
7
+
8
+ def __init__(self, endpoint: str):
9
+ self.message = f"You are not authenticated on endpoint '{endpoint}'. Please run 'klein login' to authenticate."
10
+
11
+
8
12
  class AuthenticatedClient(httpx.Client):
9
13
  def __init__(self, *args, **kwargs):
10
14
  super().__init__(*args, **kwargs)
@@ -12,21 +16,8 @@ class AuthenticatedClient(httpx.Client):
12
16
  try:
13
17
  self.tokenfile = TokenFile()
14
18
  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()
19
+ except Exception:
20
+ raise NotAuthenticatedException(self.tokenfile.endpoint)
30
21
 
31
22
  def _load_cookies(self):
32
23
  if self.tokenfile.isCliToken():
@@ -11,8 +11,6 @@ from typing_extensions import Annotated
11
11
 
12
12
  from kleinkram.consts import API_URL
13
13
 
14
- app = typer.Typer()
15
-
16
14
  TOKEN_FILE = Path(os.path.expanduser("~/.kleinkram.json"))
17
15
  REFRESH_TOKEN = "refreshtoken"
18
16
  AUTH_TOKEN = "authtoken"
@@ -127,12 +125,10 @@ def login(
127
125
  auth_tokens = get_auth_tokens()
128
126
 
129
127
  if not auth_tokens:
130
- print("Failed to get authentication tokens.")
131
- return
128
+ raise Exception("Failed to get authentication tokens.")
132
129
 
133
130
  tokenfile.saveTokens(auth_tokens)
134
131
  print("Authentication complete. Tokens saved to ~/.kleinkram.json.")
135
-
136
132
  return
137
133
 
138
134
  print(
@@ -147,41 +143,7 @@ def login(
147
143
  )
148
144
  print("Authentication complete. Tokens saved to tokens.json.")
149
145
  else:
150
- print("No authentication token provided.")
151
- return
152
-
153
-
154
- def setEndpoint(
155
- endpoint: Optional[str] = typer.Argument(None, help="API endpoint to use")
156
- ):
157
- """
158
- Set the current endpoint
159
-
160
- Use this command to switch between different API endpoints.\n
161
- Standard endpoints are:\n
162
- - http://localhost:3000\n
163
- - https://api.datasets.leggedrobotics.com\n
164
- - https://api.datasets.dev.leggedrobotics.com
165
- """
166
- tokenfile = TokenFile()
167
- tokenfile.endpoint = endpoint
168
- tokenfile.writeToFile()
169
- print("Endpoint set to: " + endpoint)
170
- if tokenfile.endpoint not in tokenfile.tokens:
171
- print("No tokens found for this endpoint.")
172
-
173
-
174
- def endpoint():
175
- """
176
- Get the current endpoint
177
-
178
- Also displays all endpoints with saved tokens.
179
- """
180
- tokenfile = TokenFile()
181
- print("Current: " + tokenfile.endpoint)
182
- print("Saved Tokens found for:")
183
- for _endpoint, _ in tokenfile.tokens.items():
184
- print("- " + _endpoint)
146
+ raise ValueError("Failed to get authentication tokens.")
185
147
 
186
148
 
187
149
  def setCliKey(key: Annotated[str, typer.Argument(help="CLI Key")]):
@@ -0,0 +1,55 @@
1
+ import typer
2
+
3
+ from kleinkram.auth.auth import TokenFile
4
+
5
+ endpoint = typer.Typer(
6
+ name="endpoint",
7
+ help="Get Or Set the current endpoint",
8
+ no_args_is_help=True,
9
+ context_settings={"help_option_names": ["-h", "--help"]},
10
+ )
11
+
12
+
13
+ @endpoint.command("set")
14
+ def set_endpoint(endpoint: str = typer.Argument(None, help="API endpoint to use")):
15
+ """
16
+ Set the current endpoint
17
+
18
+ Use this command to switch between different API endpoints.\n
19
+ Standard endpoints are:\n
20
+ - http://localhost:3000\n
21
+ - https://api.datasets.leggedrobotics.com\n
22
+ - https://api.datasets.dev.leggedrobotics.com
23
+ """
24
+
25
+ if not endpoint:
26
+ raise ValueError("No endpoint provided.")
27
+
28
+ tokenfile = TokenFile()
29
+ tokenfile.endpoint = endpoint
30
+ tokenfile.writeToFile()
31
+
32
+ print()
33
+ print("Endpoint set to: " + endpoint)
34
+ if tokenfile.endpoint not in tokenfile.tokens:
35
+ print("No tokens found for this endpoint.")
36
+
37
+
38
+ @endpoint.command("get")
39
+ def get_endpoints():
40
+ """
41
+ Get the current endpoint
42
+
43
+ Also displays all endpoints with saved tokens.
44
+ """
45
+ tokenfile = TokenFile()
46
+ print("Current: " + tokenfile.endpoint)
47
+ print()
48
+
49
+ if not tokenfile.tokens:
50
+ print("No saved tokens found.")
51
+ return
52
+
53
+ print("Saved Tokens found for:")
54
+ for _endpoint, _ in tokenfile.tokens.items():
55
+ print("- " + _endpoint)
@@ -0,0 +1,149 @@
1
+ import typing
2
+
3
+ import typer
4
+ from httpx import HTTPStatusError, ReadError, RemoteProtocolError
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from typer import Typer
8
+
9
+ from kleinkram.api_client import NotAuthenticatedException
10
+
11
+ ExceptionType = "typing.Type[Exception]"
12
+ ErrorHandlingCallback = typing.Callable[[Exception], int]
13
+
14
+
15
+ class AccessDeniedException(Exception):
16
+
17
+ def __init__(self, message: str, api_error: str):
18
+ self.message = message
19
+ self.api_error = api_error
20
+
21
+
22
+ def not_yet_implemented_handler(e: Exception):
23
+ console = Console()
24
+ panel = Panel(
25
+ "This feature is not yet implemented, please check for updates.",
26
+ title="Not Yet Implemented",
27
+ style="yellow",
28
+ padding=(1, 2),
29
+ highlight=True,
30
+ )
31
+ print()
32
+ console.print(panel)
33
+ print()
34
+
35
+
36
+ def not_authenticated_handler(e: NotAuthenticatedException):
37
+ console = Console()
38
+ panel = Panel(
39
+ f"{e.message}\n » Please run 'klein login' to authenticate.",
40
+ title="Not Authenticated",
41
+ style="yellow",
42
+ padding=(1, 2),
43
+ highlight=True,
44
+ )
45
+ print()
46
+ console.print(panel)
47
+ print()
48
+
49
+
50
+ def access_denied_handler(e: AccessDeniedException):
51
+ console = Console()
52
+ panel = Panel(
53
+ f"{e.message}\n » API Response: {e.api_error}",
54
+ title="Access Denied",
55
+ style="red",
56
+ padding=(1, 2),
57
+ highlight=True,
58
+ )
59
+ print()
60
+ console.print(panel)
61
+ print()
62
+
63
+
64
+ def value_error_handler(e: Exception):
65
+ console = Console()
66
+ panel = Panel(
67
+ str(e),
68
+ title="Invalid Argument",
69
+ style="red",
70
+ padding=(1, 2),
71
+ highlight=True,
72
+ )
73
+ print()
74
+ console.print(panel)
75
+ print()
76
+
77
+
78
+ def http_status_error_handler(e: HTTPStatusError):
79
+ console = Console()
80
+ panel = Panel(
81
+ f"An HTTP error occurred: {e}\n\n » Please report this error to the developers.",
82
+ title="HTTP Status Error",
83
+ style="red",
84
+ padding=(1, 2),
85
+ highlight=True,
86
+ )
87
+ print()
88
+ console.print(panel)
89
+ print()
90
+
91
+
92
+ def remote_down_handler(e: Exception):
93
+ console = Console()
94
+ panel = Panel(
95
+ f"An error occurred while communicating with the remote server: {e}\n"
96
+ f"\n » The server may be down or unreachable; please try again.",
97
+ title="Remote Protocol Error",
98
+ style="yellow",
99
+ padding=(1, 2),
100
+ highlight=True,
101
+ )
102
+ print()
103
+ console.print(panel)
104
+ print()
105
+
106
+
107
+ class ErrorHandledTyper(Typer):
108
+ error_handlers: typing.Dict[ExceptionType, ErrorHandlingCallback] = {
109
+ NotAuthenticatedException: not_authenticated_handler,
110
+ AccessDeniedException: access_denied_handler,
111
+ HTTPStatusError: http_status_error_handler,
112
+ NotImplementedError: not_yet_implemented_handler,
113
+ ValueError: value_error_handler,
114
+ RemoteProtocolError: remote_down_handler,
115
+ ReadError: remote_down_handler,
116
+ }
117
+
118
+ def __init__(self, *args, **kwargs):
119
+ super().__init__(*args, **kwargs)
120
+
121
+ def error_handler(self, exc: ExceptionType):
122
+ def decorator(f: ErrorHandlingCallback):
123
+ self.error_handlers[exc] = f
124
+ return f
125
+
126
+ return decorator
127
+
128
+ def __call__(self, *args, **kwargs):
129
+ try:
130
+ super(ErrorHandledTyper, self).__call__(*args, **kwargs)
131
+
132
+ except Exception as e:
133
+
134
+ # exit with error code 1 if no error handler is defined
135
+ if type(e) not in self.error_handlers:
136
+ typer.secho(
137
+ f"An unhanded error of type {type(e).__name__} occurred.",
138
+ fg=typer.colors.RED,
139
+ )
140
+ typer.secho(str(e), fg=typer.colors.RED)
141
+ typer.secho(
142
+ " » Please report this error to the developers.",
143
+ fg=typer.colors.RED,
144
+ )
145
+ else:
146
+ self.error_handlers[type(e)](e)
147
+
148
+ # exit with error code 1
149
+ exit(1)
@@ -1,9 +1,12 @@
1
+ import os
1
2
  from typing import Optional, Annotated
2
3
 
3
4
  import httpx
5
+ import requests
4
6
  import typer
5
7
 
6
8
  from kleinkram.api_client import AuthenticatedClient
9
+ from kleinkram.error_handling import AccessDeniedException
7
10
 
8
11
  file = typer.Typer(
9
12
  name="file",
@@ -79,18 +82,4 @@ def list_files(
79
82
 
80
83
  except httpx.HTTPError as e:
81
84
  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
-
85
+ raise e
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
3
3
  from enum import Enum
4
4
 
5
5
  import httpx
6
+ import pkg_resources
6
7
  import typer
7
8
  from rich import print
8
9
  from rich.table import Table
@@ -11,7 +12,9 @@ from typer.models import Context
11
12
  from typing_extensions import Annotated
12
13
 
13
14
  from kleinkram.api_client import AuthenticatedClient
14
- from kleinkram.auth.auth import login, endpoint, setEndpoint, setCliKey, logout
15
+ from kleinkram.auth.auth import login, setCliKey, logout
16
+ from kleinkram.endpoint.endpoint import endpoint
17
+ from kleinkram.error_handling import ErrorHandledTyper, AccessDeniedException
15
18
  from kleinkram.file.file import file
16
19
  from kleinkram.mission.mission import mission
17
20
  from kleinkram.project.project import project
@@ -22,12 +25,20 @@ from kleinkram.user.user import user
22
25
  from .helper import uploadFiles, expand_and_match
23
26
 
24
27
 
25
- class Panel(str, Enum):
28
+ class CommandPanel(str, Enum):
26
29
  CoreCommands = "CORE COMMANDS"
27
30
  Commands = "COMMANDS"
28
31
  AdditionalCommands = "ADDITIONAL COMMANDS"
29
32
 
30
33
 
34
+ def version_callback(value: bool):
35
+ if value:
36
+ typer.echo(
37
+ f"CLI Version: {pkg_resources.get_distribution('kleinkram').version}"
38
+ )
39
+ raise typer.Exit()
40
+
41
+
31
42
  class OrderCommands(TyperGroup):
32
43
  """
33
44
 
@@ -36,7 +47,7 @@ class OrderCommands(TyperGroup):
36
47
  """
37
48
 
38
49
  def list_commands(self, _ctx: Context) -> list[str]:
39
- order = list(Panel)
50
+ order = list(CommandPanel)
40
51
  grouped_commands = {
41
52
  name: getattr(command, "rich_help_panel")
42
53
  for name, command in sorted(self.commands.items())
@@ -56,34 +67,48 @@ class OrderCommands(TyperGroup):
56
67
  ] + sorted(ungrouped_command_names)
57
68
 
58
69
 
59
- app = typer.Typer(
70
+ app = ErrorHandledTyper(
60
71
  context_settings={"help_option_names": ["-h", "--help"]},
61
72
  no_args_is_help=True,
62
73
  cls=OrderCommands,
63
74
  )
64
75
 
65
- app.add_typer(project, rich_help_panel=Panel.Commands)
66
- app.add_typer(mission, rich_help_panel=Panel.Commands)
67
76
 
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)
77
+ @app.callback()
78
+ def version(
79
+ version: bool = typer.Option(
80
+ None,
81
+ "--version",
82
+ "-v",
83
+ callback=version_callback,
84
+ is_eager=True,
85
+ help="Print the version and exit",
86
+ )
87
+ ):
88
+ pass
73
89
 
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)
90
+
91
+ app.add_typer(project, rich_help_panel=CommandPanel.Commands)
92
+ app.add_typer(mission, rich_help_panel=CommandPanel.Commands)
93
+
94
+ app.add_typer(topic, rich_help_panel=CommandPanel.Commands)
95
+ app.add_typer(file, rich_help_panel=CommandPanel.Commands)
96
+ app.add_typer(queue, rich_help_panel=CommandPanel.Commands)
97
+ app.add_typer(user, rich_help_panel=CommandPanel.Commands)
98
+ app.add_typer(tag, rich_help_panel=CommandPanel.Commands)
99
+ app.add_typer(endpoint, rich_help_panel=CommandPanel.AdditionalCommands)
100
+
101
+ app.command(rich_help_panel=CommandPanel.AdditionalCommands)(login)
102
+ app.command(rich_help_panel=CommandPanel.AdditionalCommands)(logout)
78
103
  app.command(hidden=True)(setCliKey)
79
104
 
80
105
 
81
- @app.command("download", rich_help_panel=Panel.CoreCommands)
106
+ @app.command("download", rich_help_panel=CommandPanel.CoreCommands)
82
107
  def download():
83
108
  raise NotImplementedError("Not implemented yet.")
84
109
 
85
110
 
86
- @app.command("upload", rich_help_panel=Panel.CoreCommands)
111
+ @app.command("upload", rich_help_panel=CommandPanel.CoreCommands)
87
112
  def upload(
88
113
  path: Annotated[
89
114
  str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")
@@ -106,24 +131,33 @@ def upload(
106
131
  map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
107
132
  )
108
133
  if not filenames:
109
- print("No files found")
110
- return
134
+ raise ValueError("No files found matching the given path.")
135
+
136
+ print(
137
+ f"Uploading the following files to mission '{mission}' in project '{project}':"
138
+ )
111
139
  filepaths = {}
112
140
  for path in files:
113
141
  if not os.path.isdir(path):
114
142
  filepaths[path.split("/")[-1]] = path
115
- print(f" - {path}")
143
+ typer.secho(f" - {path}", fg=typer.colors.RESET)
144
+
116
145
  try:
117
146
  client = AuthenticatedClient()
118
147
 
119
148
  get_project_url = "/project/byName"
120
149
  project_response = client.get(get_project_url, params={"name": project})
121
150
  if project_response.status_code >= 400:
122
- print(f"Failed to fetch project: {project_response.text}")
123
- return
151
+
152
+ raise AccessDeniedException(
153
+ f"The project '{project}' does not exist or you do not have access to it.\n"
154
+ f"Consider using the following command to create a project: 'klein project create'\n",
155
+ f"{project_response.json()['message']} ({project_response.status_code})",
156
+ )
157
+
124
158
  project_json = project_response.json()
125
159
  if not project_json["uuid"]:
126
- print(f"Project not found: {project}")
160
+ print(f"Project not found: '{project}'")
127
161
  return
128
162
 
129
163
  get_mission_url = "/mission/byName"
@@ -132,12 +166,11 @@ def upload(
132
166
  if mission_response.content:
133
167
  mission_json = mission_response.json()
134
168
  if mission_json["uuid"]:
135
- print(
136
- f"mission: {mission_json['uuid']} already exists. Delete it or select another name."
169
+ raise ValueError(
170
+ f"Mission {mission_json['name']} ({mission_json['uuid']}) already exists. Delete it or select "
171
+ f"another name."
137
172
  )
138
- return
139
- print(f"Something failed, should not happen")
140
- return
173
+ raise Exception(f"Something failed, should not happen")
141
174
 
142
175
  create_mission_url = "/mission/create"
143
176
  new_mission = client.post(
@@ -158,7 +191,9 @@ def upload(
158
191
  presigned_urls = response_2.json()
159
192
  for file in filenames:
160
193
  if not file in presigned_urls.keys():
161
- print("Could not upload File '" + file + "'. Is the filename unique? ")
194
+ raise Exception(
195
+ "Could not upload File '" + file + "'. Is the filename unique? "
196
+ )
162
197
  if len(presigned_urls) > 0:
163
198
  uploadFiles(presigned_urls, filepaths, 4)
164
199
 
@@ -0,0 +1,192 @@
1
+ import os
2
+ from typing import Annotated, Optional
3
+
4
+ import httpx
5
+ import requests
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.table import Table
9
+
10
+ from kleinkram.api_client import AuthenticatedClient
11
+ from kleinkram.error_handling import AccessDeniedException
12
+
13
+ mission = typer.Typer(
14
+ name="mission",
15
+ help="Mission operations",
16
+ no_args_is_help=True,
17
+ context_settings={"help_option_names": ["-h", "--help"]},
18
+ )
19
+
20
+
21
+ @mission.command("tag")
22
+ def addTag(
23
+ mission_uuid: Annotated[str, typer.Argument()],
24
+ tagtype_uuid: Annotated[str, typer.Argument()],
25
+ value: Annotated[str, typer.Argument()],
26
+ ):
27
+ """Tag a mission"""
28
+ try:
29
+ client = AuthenticatedClient()
30
+ response = client.post(
31
+ "/tag/addTag",
32
+ json={"mission": mission_uuid, "tagType": tagtype_uuid, "value": value},
33
+ )
34
+ if response.status_code < 400:
35
+ print("Tagged mission")
36
+ else:
37
+ print(response.json())
38
+ print("Failed to tag mission")
39
+ raise Exception("Failed to tag mission")
40
+ except httpx.HTTPError as e:
41
+ print(e)
42
+ print("Failed to tag mission")
43
+ raise e
44
+
45
+
46
+ @mission.command("list")
47
+ def list_missions(
48
+ project: Optional[str] = typer.Option(None, help="Name of Project"),
49
+ verbose: Optional[bool] = typer.Option(
50
+ False, help="Outputs a table with more information"
51
+ ),
52
+ ):
53
+ """
54
+ List all missions with optional filter for project.
55
+ """
56
+
57
+ url = "/mission"
58
+ if project:
59
+ url += f"/filteredByProjectName/{project}"
60
+ else:
61
+ url += "/all"
62
+
63
+ client = AuthenticatedClient()
64
+
65
+ try:
66
+
67
+ response = client.get(url)
68
+ response.raise_for_status()
69
+
70
+ except httpx.HTTPError:
71
+
72
+ raise AccessDeniedException(
73
+ f"Failed to fetch mission."
74
+ f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
75
+ f"{response.json()['message']} ({response.status_code})",
76
+ )
77
+
78
+ data = response.json()
79
+ missions_by_project_uuid = {}
80
+ for mission in data:
81
+ project_uuid = mission["project"]["uuid"]
82
+ if project_uuid not in missions_by_project_uuid:
83
+ missions_by_project_uuid[project_uuid] = []
84
+ missions_by_project_uuid[project_uuid].append(mission)
85
+
86
+ if len(missions_by_project_uuid.items()) == 0:
87
+ print(f"No missions found for project '{project}'. Does it exist?")
88
+ return
89
+
90
+ print("missions by Project:")
91
+ if not verbose:
92
+ for project_uuid, missions in missions_by_project_uuid.items():
93
+ print(f"* {missions_by_project_uuid[project_uuid][0]['project']['name']}")
94
+ for mission in missions:
95
+ print(f" - {mission['name']}")
96
+
97
+ else:
98
+ table = Table("UUID", "name", "project", "creator", "createdAt")
99
+ for project_uuid, missions in missions_by_project_uuid.items():
100
+ for mission in missions:
101
+ table.add_row(
102
+ mission["uuid"],
103
+ mission["name"],
104
+ mission["project"]["name"],
105
+ mission["creator"]["name"],
106
+ mission["createdAt"],
107
+ )
108
+ console = Console()
109
+ console.print(table)
110
+
111
+
112
+ @mission.command("byUUID")
113
+ def mission_by_uuid(
114
+ uuid: Annotated[str, typer.Argument()],
115
+ json: Optional[bool] = typer.Option(False, help="Output as JSON"),
116
+ ):
117
+ """
118
+ Get mission name, project name, creator and table of its files given a Mission UUID
119
+
120
+ Use the JSON flag to output the full JSON response instead.
121
+
122
+ Can be run with API Key or with login.
123
+ """
124
+ url = "/mission/byUUID"
125
+ client = AuthenticatedClient()
126
+ response = client.get(url, params={"uuid": uuid})
127
+
128
+ try:
129
+ response.raise_for_status()
130
+ except httpx.HTTPError:
131
+ raise AccessDeniedException(
132
+ f"Failed to fetch mission."
133
+ f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
134
+ f"{response.json()['message']} ({response.status_code})",
135
+ )
136
+
137
+ data = response.json()
138
+
139
+ if json:
140
+ print(data)
141
+ else:
142
+ print(f"mission: {data['name']}")
143
+ print(f"Creator: {data['creator']['name']}")
144
+ print("Project: " + data["project"]["name"])
145
+ table = Table("Filename", "Size", "date")
146
+ for file in data["files"]:
147
+ table.add_row(file["filename"], f"{file['size']}", file["date"])
148
+ console = Console()
149
+ console.print(table)
150
+
151
+
152
+ @mission.command("download")
153
+ def download(
154
+ mission_uuid: Annotated[str, typer.Argument()],
155
+ local_path: Annotated[str, typer.Argument()],
156
+ ):
157
+ """
158
+
159
+ Downloads all files of a mission to a local path.
160
+ The local path must be an empty directory.
161
+
162
+ """
163
+
164
+ if not os.path.isdir(local_path):
165
+ raise ValueError(f"Local path '{local_path}' is not a directory.")
166
+ if not os.listdir(local_path) == []:
167
+ raise ValueError(f"Local path '{local_path}' is not empty, but must be empty.")
168
+
169
+ client = AuthenticatedClient()
170
+ response = client.get("/file/downloadWithToken", params={"uuid": mission_uuid})
171
+
172
+ try:
173
+ response.raise_for_status()
174
+ except httpx.HTTPError as e:
175
+ raise AccessDeniedException(
176
+ f"Failed to download file."
177
+ f"Consider using the following command to list all missions: 'klein mission list --verbose'\n",
178
+ f"{response.json()['message']} ({response.status_code})",
179
+ )
180
+
181
+ paths = response.json()
182
+
183
+ print(f"Downloading files to {local_path}:")
184
+ for path in paths:
185
+
186
+ filename = path.split("/")[-1].split("?")[0]
187
+ print(f" - {filename}")
188
+
189
+ response = requests.get(path)
190
+ with open(os.path.join(local_path, filename), "wb") as f:
191
+ f.write(response.content)
192
+ print(f" Downloaded {filename}")
@@ -55,3 +55,4 @@ def create_project(
55
55
 
56
56
  except httpx.HTTPError as e:
57
57
  print(f"Failed to create project: {e}")
58
+ raise e
@@ -1,23 +1,38 @@
1
1
  from typing import Annotated
2
2
 
3
+ import httpx
3
4
  import typer
5
+ from rich.console import Console
4
6
  from rich.table import Table
5
7
 
6
8
  from kleinkram.api_client import AuthenticatedClient
7
9
 
8
- tag = typer.Typer(name="tag", help="Tag operations")
10
+ tag = typer.Typer(
11
+ name="tag",
12
+ help="Tag operations",
13
+ no_args_is_help=True,
14
+ context_settings={"help_option_names": ["-h", "--help"]},
15
+ )
9
16
 
10
17
 
11
18
  @tag.command("list-tag-types")
12
- def tagTypes(
19
+ def tag_types(
13
20
  verbose: Annotated[bool, typer.Option()] = False,
14
21
  ):
15
- """List all tagtypes"""
22
+ """
23
+ List all tagtypes
24
+ """
25
+
16
26
  try:
17
27
  client = AuthenticatedClient()
18
28
  response = client.get("/tag/all")
19
29
  response.raise_for_status()
20
30
  data = response.json()
31
+
32
+ if not data or len(data) == 0:
33
+ print("No tagtypes found")
34
+ return
35
+
21
36
  if verbose:
22
37
  table = Table("UUID", "Name", "Datatype")
23
38
  for tagtype in data:
@@ -26,16 +41,22 @@ def tagTypes(
26
41
  table = Table("Name", "Datatype")
27
42
  for tagtype in data:
28
43
  table.add_row(tagtype["name"], tagtype["datatype"])
29
- print(table)
44
+ console = Console()
45
+ console.print(table)
46
+
30
47
  except:
31
48
  print("Failed to fetch tagtypes")
49
+ raise Exception("Failed to fetch tagtypes")
32
50
 
33
51
 
34
52
  @tag.command("delete")
35
- def deleteTag(
53
+ def delete_tag(
36
54
  taguuid: Annotated[str, typer.Argument()],
37
55
  ):
38
- """Delete a tag"""
56
+ """
57
+ Delete a tag
58
+ """
59
+
39
60
  try:
40
61
  client = AuthenticatedClient()
41
62
  response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
@@ -44,5 +65,7 @@ def deleteTag(
44
65
  else:
45
66
  print(response)
46
67
  print("Failed to delete tag")
47
- except:
68
+ raise Exception("Failed to delete tag")
69
+ except httpx.HTTPError as e:
48
70
  print("Failed to delete tag")
71
+ raise Exception("Failed to delete tag")
@@ -52,3 +52,4 @@ def topics(
52
52
 
53
53
  except httpx.HTTPError as e:
54
54
  print(f"Failed")
55
+ raise e
@@ -1,126 +0,0 @@
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}")