kleinkram 0.6.0.dev20240812074426__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.
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/PKG-INFO +1 -1
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/pyproject.toml +1 -1
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/api_client.py +8 -17
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/auth/auth.py +2 -40
- kleinkram-0.6.0.dev20240812135206/src/kleinkram/endpoint/endpoint.py +55 -0
- kleinkram-0.6.0.dev20240812135206/src/kleinkram/error_handling.py +149 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/file/file.py +4 -14
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/main.py +64 -29
- kleinkram-0.6.0.dev20240812135206/src/kleinkram/mission/mission.py +192 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/project/project.py +1 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/tag/tag.py +30 -7
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/topic/topic.py +1 -0
- kleinkram-0.6.0.dev20240812074426/src/kleinkram/mission/mission.py +0 -126
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/.gitignore +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/LICENSE +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/README.md +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/deploy.sh +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/dev.sh +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/requirements.txt +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/klein.py +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/__init__.py +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/consts.py +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/helper.py +0 -0
- {kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/queue/queue.py +0 -0
- {kleinkram-0.6.0.dev20240812074426 → 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.
|
|
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
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/api_client.py
RENAMED
|
@@ -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
|
|
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():
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/auth/auth.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/file/file.py
RENAMED
|
@@ -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,17 +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")
|
|
85
|
+
raise e
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/main.py
RENAMED
|
@@ -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,
|
|
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
|
|
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(
|
|
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 =
|
|
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.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
app.
|
|
76
|
-
app.
|
|
77
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
136
|
-
f"
|
|
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
|
-
|
|
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
|
-
|
|
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}")
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/tag/tag.py
RENAMED
|
@@ -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(
|
|
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
|
|
19
|
+
def tag_types(
|
|
13
20
|
verbose: Annotated[bool, typer.Option()] = False,
|
|
14
21
|
):
|
|
15
|
-
"""
|
|
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
|
-
|
|
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
|
|
53
|
+
def delete_tag(
|
|
36
54
|
taguuid: Annotated[str, typer.Argument()],
|
|
37
55
|
):
|
|
38
|
-
"""
|
|
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
|
-
|
|
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")
|
|
@@ -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}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/__init__.py
RENAMED
|
File without changes
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/consts.py
RENAMED
|
File without changes
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/helper.py
RENAMED
|
File without changes
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/queue/queue.py
RENAMED
|
File without changes
|
{kleinkram-0.6.0.dev20240812074426 → kleinkram-0.6.0.dev20240812135206}/src/kleinkram/user/user.py
RENAMED
|
File without changes
|