kleinkram 0.36.2__py3-none-any.whl → 0.36.2.dev20241118065826__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kleinkram might be problematic. Click here for more details.
- kleinkram/__init__.py +6 -0
- kleinkram/__main__.py +6 -0
- kleinkram/_version.py +6 -0
- kleinkram/api/__init__.py +0 -0
- kleinkram/api/client.py +65 -0
- kleinkram/api/file_transfer.py +328 -0
- kleinkram/api/routes.py +460 -0
- kleinkram/app.py +180 -0
- kleinkram/auth.py +96 -0
- kleinkram/commands/__init__.py +1 -0
- kleinkram/commands/download.py +103 -0
- kleinkram/commands/endpoint.py +62 -0
- kleinkram/commands/list.py +93 -0
- kleinkram/commands/mission.py +57 -0
- kleinkram/commands/project.py +24 -0
- kleinkram/commands/upload.py +138 -0
- kleinkram/commands/verify.py +117 -0
- kleinkram/config.py +171 -0
- kleinkram/consts.py +8 -1
- kleinkram/core.py +14 -0
- kleinkram/enums.py +10 -0
- kleinkram/errors.py +59 -0
- kleinkram/main.py +6 -484
- kleinkram/models.py +186 -0
- kleinkram/utils.py +179 -0
- {kleinkram-0.36.2.dist-info/licenses → kleinkram-0.36.2.dev20241118065826.dist-info}/LICENSE +1 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/METADATA +113 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/RECORD +33 -0
- {kleinkram-0.36.2.dist-info → kleinkram-0.36.2.dev20241118065826.dist-info}/WHEEL +2 -1
- kleinkram-0.36.2.dev20241118065826.dist-info/entry_points.txt +2 -0
- kleinkram-0.36.2.dev20241118065826.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_utils.py +153 -0
- kleinkram/api_client.py +0 -63
- kleinkram/auth/auth.py +0 -160
- kleinkram/endpoint/endpoint.py +0 -58
- kleinkram/error_handling.py +0 -177
- kleinkram/file/file.py +0 -144
- kleinkram/helper.py +0 -272
- kleinkram/mission/mission.py +0 -310
- kleinkram/project/project.py +0 -138
- kleinkram/queue/queue.py +0 -8
- kleinkram/tag/tag.py +0 -71
- kleinkram/topic/topic.py +0 -55
- kleinkram/user/user.py +0 -75
- kleinkram-0.36.2.dist-info/METADATA +0 -25
- kleinkram-0.36.2.dist-info/RECORD +0 -20
- kleinkram-0.36.2.dist-info/entry_points.txt +0 -2
kleinkram/auth/auth.py
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import urllib.parse
|
|
4
|
-
import webbrowser
|
|
5
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing_extensions import Optional
|
|
8
|
-
|
|
9
|
-
import typer
|
|
10
|
-
from typing_extensions import Annotated
|
|
11
|
-
|
|
12
|
-
from kleinkram.consts import API_URL
|
|
13
|
-
|
|
14
|
-
TOKEN_FILE = Path(os.path.expanduser("~/.kleinkram.json"))
|
|
15
|
-
REFRESH_TOKEN = "refreshtoken"
|
|
16
|
-
AUTH_TOKEN = "authtoken"
|
|
17
|
-
CLI_KEY = "clikey"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class TokenFile:
|
|
21
|
-
def __init__(self):
|
|
22
|
-
try:
|
|
23
|
-
with TOKEN_FILE.open("r") as token_file:
|
|
24
|
-
content = json.load(token_file)
|
|
25
|
-
self.endpoint = content["endpoint"]
|
|
26
|
-
self.tokens = content["tokens"]
|
|
27
|
-
except FileNotFoundError:
|
|
28
|
-
self.tokens = {}
|
|
29
|
-
self.endpoint = API_URL
|
|
30
|
-
except json.JSONDecodeError:
|
|
31
|
-
print("Token file is corrupted. Please run 'login' command again.")
|
|
32
|
-
raise
|
|
33
|
-
|
|
34
|
-
def isCliToken(self):
|
|
35
|
-
return CLI_KEY in self.tokens[self.endpoint]
|
|
36
|
-
|
|
37
|
-
def getAuthToken(self):
|
|
38
|
-
return self.tokens[self.endpoint][AUTH_TOKEN]
|
|
39
|
-
|
|
40
|
-
def getRefreshToken(self):
|
|
41
|
-
return self.tokens[self.endpoint][REFRESH_TOKEN]
|
|
42
|
-
|
|
43
|
-
def getCLIToken(self):
|
|
44
|
-
return self.tokens[self.endpoint][CLI_KEY]
|
|
45
|
-
|
|
46
|
-
def writeToFile(self):
|
|
47
|
-
res = {
|
|
48
|
-
"endpoint": self.endpoint,
|
|
49
|
-
"tokens": self.tokens,
|
|
50
|
-
}
|
|
51
|
-
with TOKEN_FILE.open("w") as token_file:
|
|
52
|
-
json.dump(res, token_file)
|
|
53
|
-
|
|
54
|
-
def saveTokens(self, tokens):
|
|
55
|
-
self.tokens[self.endpoint] = tokens
|
|
56
|
-
self.writeToFile()
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
60
|
-
def do_GET(self):
|
|
61
|
-
if self.path.startswith("/cli/callback"):
|
|
62
|
-
query = urllib.parse.urlparse(self.path).query
|
|
63
|
-
params = urllib.parse.parse_qs(query)
|
|
64
|
-
self.server.tokens = {
|
|
65
|
-
AUTH_TOKEN: params.get(AUTH_TOKEN)[0],
|
|
66
|
-
REFRESH_TOKEN: params.get(REFRESH_TOKEN)[0],
|
|
67
|
-
}
|
|
68
|
-
self.send_response(200)
|
|
69
|
-
self.send_header("Content-type", "text/html")
|
|
70
|
-
self.end_headers()
|
|
71
|
-
self.wfile.write(b"Authentication successful. You can close this window.")
|
|
72
|
-
return
|
|
73
|
-
print("here")
|
|
74
|
-
|
|
75
|
-
def log_message(self, format, *args):
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def get_auth_tokens():
|
|
80
|
-
server_address = ("", 8000)
|
|
81
|
-
httpd = HTTPServer(server_address, OAuthCallbackHandler)
|
|
82
|
-
httpd.handle_request()
|
|
83
|
-
return httpd.tokens
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def logout():
|
|
87
|
-
"""
|
|
88
|
-
Logout from the currently set endpoint.
|
|
89
|
-
"""
|
|
90
|
-
tokenfile = TokenFile()
|
|
91
|
-
tokenfile.tokens[tokenfile.endpoint] = {}
|
|
92
|
-
tokenfile.writeToFile()
|
|
93
|
-
print("Logged out.")
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def login(
|
|
97
|
-
key: Optional[str] = typer.Option(None, help="CLI Key", hidden=True),
|
|
98
|
-
open_browser: Optional[bool] = typer.Option(
|
|
99
|
-
True, help="Open browser for authentication"
|
|
100
|
-
),
|
|
101
|
-
):
|
|
102
|
-
"""
|
|
103
|
-
Login into the currently set endpoint.\n
|
|
104
|
-
By default, it will open the browser for authentication. On machines without a browser, you can manually open the URL provided and paste the tokens back.
|
|
105
|
-
"""
|
|
106
|
-
tokenfile = TokenFile()
|
|
107
|
-
if key:
|
|
108
|
-
token = {}
|
|
109
|
-
token[CLI_KEY] = key
|
|
110
|
-
tokenfile.saveTokens(token)
|
|
111
|
-
|
|
112
|
-
else:
|
|
113
|
-
url = tokenfile.endpoint + "/auth/google?state=cli"
|
|
114
|
-
|
|
115
|
-
has_browser = True
|
|
116
|
-
try:
|
|
117
|
-
browser_available = webbrowser.get()
|
|
118
|
-
if not browser_available:
|
|
119
|
-
raise Exception("No web browser available.")
|
|
120
|
-
except Exception as e:
|
|
121
|
-
has_browser = False
|
|
122
|
-
|
|
123
|
-
if has_browser and open_browser:
|
|
124
|
-
webbrowser.open(url)
|
|
125
|
-
auth_tokens = get_auth_tokens()
|
|
126
|
-
|
|
127
|
-
if not auth_tokens:
|
|
128
|
-
raise Exception("Failed to get authentication tokens.")
|
|
129
|
-
|
|
130
|
-
tokenfile.saveTokens(auth_tokens)
|
|
131
|
-
print("Authentication complete. Tokens saved to ~/.kleinkram.json.")
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
print(
|
|
135
|
-
f"Please open the following URL manually in your browser to authenticate: {url + '-no-redirect'}"
|
|
136
|
-
)
|
|
137
|
-
print("Enter the authentication token provided after logging in:")
|
|
138
|
-
manual_auth_token = input("Authentication Token: ")
|
|
139
|
-
manual_refresh_token = input("Refresh Token: ")
|
|
140
|
-
if manual_auth_token:
|
|
141
|
-
tokenfile.saveTokens(
|
|
142
|
-
{AUTH_TOKEN: manual_auth_token, REFRESH_TOKEN: manual_refresh_token}
|
|
143
|
-
)
|
|
144
|
-
print("Authentication complete. Tokens saved to tokens.json.")
|
|
145
|
-
else:
|
|
146
|
-
raise ValueError("Failed to get authentication tokens.")
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def setCliKey(key: Annotated[str, typer.Argument(help="CLI Key")]):
|
|
150
|
-
"""
|
|
151
|
-
Set the CLI key (Actions Only)
|
|
152
|
-
|
|
153
|
-
Same as login with the --key option.
|
|
154
|
-
Should never be used by the user, only in docker containers launched from within Kleinkram.
|
|
155
|
-
"""
|
|
156
|
-
tokenfile = TokenFile()
|
|
157
|
-
if not tokenfile.endpoint in tokenfile.tokens:
|
|
158
|
-
tokenfile.tokens[tokenfile.endpoint] = {}
|
|
159
|
-
tokenfile.tokens[tokenfile.endpoint][CLI_KEY] = key
|
|
160
|
-
tokenfile.writeToFile()
|
kleinkram/endpoint/endpoint.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
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.\n\nThe endpoint is used to determine the API server to connect to"
|
|
8
|
-
"(default is the API server of https://datasets.leggedrobotics.com).",
|
|
9
|
-
no_args_is_help=True,
|
|
10
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@endpoint.command("set")
|
|
15
|
-
def set_endpoint(endpoint: str = typer.Argument(None, help="API endpoint to use")):
|
|
16
|
-
"""
|
|
17
|
-
Set the current endpoint
|
|
18
|
-
|
|
19
|
-
Use this command to switch between different API endpoints.\n
|
|
20
|
-
Standard endpoints are:\n
|
|
21
|
-
- http://localhost:3000\n
|
|
22
|
-
- https://api.datasets.leggedrobotics.com\n
|
|
23
|
-
- https://api.datasets.dev.leggedrobotics.com
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
if not endpoint:
|
|
27
|
-
raise ValueError("No endpoint provided.")
|
|
28
|
-
|
|
29
|
-
tokenfile = TokenFile()
|
|
30
|
-
tokenfile.endpoint = endpoint
|
|
31
|
-
tokenfile.writeToFile()
|
|
32
|
-
|
|
33
|
-
print()
|
|
34
|
-
print("Endpoint set to: " + endpoint)
|
|
35
|
-
if tokenfile.endpoint not in tokenfile.tokens:
|
|
36
|
-
print(
|
|
37
|
-
"Not authenticated on this endpoint, please execute 'klein login' to authenticate."
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@endpoint.command("get")
|
|
42
|
-
def get_endpoints():
|
|
43
|
-
"""
|
|
44
|
-
Get the current endpoint
|
|
45
|
-
|
|
46
|
-
Also displays all endpoints with saved tokens.
|
|
47
|
-
"""
|
|
48
|
-
tokenfile = TokenFile()
|
|
49
|
-
print("Current: " + tokenfile.endpoint)
|
|
50
|
-
print()
|
|
51
|
-
|
|
52
|
-
if not tokenfile.tokens:
|
|
53
|
-
print("No saved tokens found.")
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
print("Saved Tokens found for:")
|
|
57
|
-
for _endpoint, _ in tokenfile.tokens.items():
|
|
58
|
-
print("- " + _endpoint)
|
kleinkram/error_handling.py
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import typing
|
|
3
|
-
|
|
4
|
-
import typer
|
|
5
|
-
from httpx import HTTPStatusError, ReadError, RemoteProtocolError
|
|
6
|
-
from rich.console import Console
|
|
7
|
-
from rich.panel import Panel
|
|
8
|
-
from typer import Typer
|
|
9
|
-
|
|
10
|
-
from kleinkram.api_client import NotAuthenticatedException
|
|
11
|
-
|
|
12
|
-
ExceptionType = "typing.Type[Exception]"
|
|
13
|
-
ErrorHandlingCallback = typing.Callable[[Exception], int]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class AbortException(Exception):
|
|
17
|
-
|
|
18
|
-
def __init__(self, message: str):
|
|
19
|
-
self.message = message
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class AccessDeniedException(Exception):
|
|
23
|
-
|
|
24
|
-
def __init__(self, message: str, api_error: str):
|
|
25
|
-
self.message = message
|
|
26
|
-
self.api_error = api_error
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def not_yet_implemented_handler(e: Exception):
|
|
30
|
-
console = Console(file=sys.stderr)
|
|
31
|
-
default_msg = "This feature is not yet implemented. Please check for updates or use the web interface."
|
|
32
|
-
panel = Panel(
|
|
33
|
-
f"{default_msg}",
|
|
34
|
-
title="Not Yet Implemented",
|
|
35
|
-
style="yellow",
|
|
36
|
-
padding=(1, 2),
|
|
37
|
-
highlight=True,
|
|
38
|
-
)
|
|
39
|
-
print()
|
|
40
|
-
console.print(panel)
|
|
41
|
-
print()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def not_authenticated_handler(e: NotAuthenticatedException):
|
|
45
|
-
console = Console(file=sys.stderr)
|
|
46
|
-
panel = Panel(
|
|
47
|
-
f"{e.message}\n » Please run 'klein login' to authenticate.",
|
|
48
|
-
title="Not Authenticated",
|
|
49
|
-
style="yellow",
|
|
50
|
-
padding=(1, 2),
|
|
51
|
-
highlight=True,
|
|
52
|
-
)
|
|
53
|
-
print()
|
|
54
|
-
console.print(panel)
|
|
55
|
-
print()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def access_denied_handler(e: AccessDeniedException):
|
|
59
|
-
console = Console(file=sys.stderr)
|
|
60
|
-
panel = Panel(
|
|
61
|
-
f"{e.message}\n » API Response: {e.api_error}",
|
|
62
|
-
title="Access Denied",
|
|
63
|
-
style="red",
|
|
64
|
-
padding=(1, 2),
|
|
65
|
-
highlight=True,
|
|
66
|
-
)
|
|
67
|
-
print()
|
|
68
|
-
console.print(panel)
|
|
69
|
-
print()
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def value_error_handler(e: Exception):
|
|
73
|
-
console = Console(file=sys.stderr)
|
|
74
|
-
panel = Panel(
|
|
75
|
-
str(e),
|
|
76
|
-
title="Invalid Argument",
|
|
77
|
-
style="red",
|
|
78
|
-
padding=(1, 2),
|
|
79
|
-
highlight=True,
|
|
80
|
-
)
|
|
81
|
-
print()
|
|
82
|
-
console.print(panel)
|
|
83
|
-
print()
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def http_status_error_handler(e: HTTPStatusError):
|
|
87
|
-
console = Console(file=sys.stderr)
|
|
88
|
-
panel = Panel(
|
|
89
|
-
f"An HTTP error occurred: {e}\n\n » Please report this error to the developers.",
|
|
90
|
-
title="HTTP Status Error",
|
|
91
|
-
style="red",
|
|
92
|
-
padding=(1, 2),
|
|
93
|
-
highlight=True,
|
|
94
|
-
)
|
|
95
|
-
print()
|
|
96
|
-
console.print(panel)
|
|
97
|
-
print()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def remote_down_handler(e: Exception):
|
|
101
|
-
console = Console(file=sys.stderr)
|
|
102
|
-
panel = Panel(
|
|
103
|
-
f"An error occurred while communicating with the remote server: {e}\n"
|
|
104
|
-
f"\n » The server may be down or unreachable; please try again.",
|
|
105
|
-
title="Remote Protocol Error",
|
|
106
|
-
style="yellow",
|
|
107
|
-
padding=(1, 2),
|
|
108
|
-
highlight=True,
|
|
109
|
-
)
|
|
110
|
-
print()
|
|
111
|
-
console.print(panel)
|
|
112
|
-
print()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def abort_handler(e: AbortException):
|
|
116
|
-
console = Console(file=sys.stderr)
|
|
117
|
-
panel = Panel(
|
|
118
|
-
f"{e.message}",
|
|
119
|
-
title="Command Aborted",
|
|
120
|
-
style="yellow",
|
|
121
|
-
padding=(1, 2),
|
|
122
|
-
highlight=True,
|
|
123
|
-
)
|
|
124
|
-
print()
|
|
125
|
-
console.print(panel)
|
|
126
|
-
print()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
class ErrorHandledTyper(Typer):
|
|
130
|
-
error_handlers: typing.Dict[ExceptionType, ErrorHandlingCallback] = {
|
|
131
|
-
NotAuthenticatedException: not_authenticated_handler,
|
|
132
|
-
AccessDeniedException: access_denied_handler,
|
|
133
|
-
HTTPStatusError: http_status_error_handler,
|
|
134
|
-
NotImplementedError: not_yet_implemented_handler,
|
|
135
|
-
ValueError: value_error_handler,
|
|
136
|
-
RemoteProtocolError: remote_down_handler,
|
|
137
|
-
ReadError: remote_down_handler,
|
|
138
|
-
AbortException: abort_handler,
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
def __init__(self, *args, **kwargs):
|
|
142
|
-
super().__init__(*args, **kwargs)
|
|
143
|
-
|
|
144
|
-
def error_handler(self, exc: ExceptionType):
|
|
145
|
-
def decorator(f: ErrorHandlingCallback):
|
|
146
|
-
self.error_handlers[exc] = f
|
|
147
|
-
return f
|
|
148
|
-
|
|
149
|
-
return decorator
|
|
150
|
-
|
|
151
|
-
def __call__(self, *args, **kwargs):
|
|
152
|
-
try:
|
|
153
|
-
super(ErrorHandledTyper, self).__call__(*args, **kwargs)
|
|
154
|
-
|
|
155
|
-
except Exception as e:
|
|
156
|
-
|
|
157
|
-
# exit with error code 1 if no error handler is defined
|
|
158
|
-
if type(e) not in self.error_handlers:
|
|
159
|
-
typer.secho(
|
|
160
|
-
f"An unhanded error of type {type(e).__name__} occurred.",
|
|
161
|
-
fg=typer.colors.RED,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
typer.secho(
|
|
165
|
-
" » Please report this error to the developers.",
|
|
166
|
-
fg=typer.colors.RED,
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
typer.secho(f"\n\n{e}:", fg=typer.colors.RED)
|
|
170
|
-
console = Console()
|
|
171
|
-
console.print_exception(show_locals=True)
|
|
172
|
-
|
|
173
|
-
else:
|
|
174
|
-
self.error_handlers[type(e)](e)
|
|
175
|
-
|
|
176
|
-
# exit with error code 1
|
|
177
|
-
exit(1)
|
kleinkram/file/file.py
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from typing_extensions import Optional, Annotated, List
|
|
3
|
-
|
|
4
|
-
import httpx
|
|
5
|
-
import requests
|
|
6
|
-
import typer
|
|
7
|
-
|
|
8
|
-
from kleinkram.api_client import AuthenticatedClient
|
|
9
|
-
from kleinkram.error_handling import AccessDeniedException
|
|
10
|
-
|
|
11
|
-
file = typer.Typer(
|
|
12
|
-
name="file",
|
|
13
|
-
help="File operations",
|
|
14
|
-
no_args_is_help=True,
|
|
15
|
-
context_settings={"help_option_names": ["-h", "--help"]},
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@file.command("download")
|
|
20
|
-
def download_file(
|
|
21
|
-
file_uuid: Annotated[List[str], typer.Option(help="UUIDs of the files")],
|
|
22
|
-
local_path: Annotated[
|
|
23
|
-
str,
|
|
24
|
-
typer.Option(
|
|
25
|
-
prompt=True,
|
|
26
|
-
help="Local path to save the file",
|
|
27
|
-
),
|
|
28
|
-
],
|
|
29
|
-
):
|
|
30
|
-
"""
|
|
31
|
-
Download files by UUIDs to a local path.\n
|
|
32
|
-
Examples:\n
|
|
33
|
-
klein file download --file-uuid="9d5a9..." --file-uuid="9833f..." --local-path="~/Downloads" \n
|
|
34
|
-
klein file download --file-uuid="9d5a9..." --local-path="~/Downloads/example.bag"
|
|
35
|
-
|
|
36
|
-
"""
|
|
37
|
-
client = AuthenticatedClient()
|
|
38
|
-
url = f"/file/download"
|
|
39
|
-
|
|
40
|
-
fixed_local_path = os.path.expanduser(local_path)
|
|
41
|
-
|
|
42
|
-
isDir = os.path.isdir(fixed_local_path)
|
|
43
|
-
chunk_size = 1024 * 100 # 100 KB chunks, adjust size if needed
|
|
44
|
-
|
|
45
|
-
for file in file_uuid:
|
|
46
|
-
response = client.get(
|
|
47
|
-
url,
|
|
48
|
-
params={"uuid": file, "expires": True},
|
|
49
|
-
)
|
|
50
|
-
if response.status_code >= 400:
|
|
51
|
-
raise AccessDeniedException(
|
|
52
|
-
f"Failed to download file: {response.json()['message']}",
|
|
53
|
-
"Status Code: " + str(response.status_code),
|
|
54
|
-
)
|
|
55
|
-
download_url = response.text
|
|
56
|
-
if isDir:
|
|
57
|
-
filename = download_url.split("/")[6].split("?")[0] # Trust me bro
|
|
58
|
-
filepath = os.path.join(fixed_local_path, filename)
|
|
59
|
-
elif not isDir and len(file_uuid) == 1:
|
|
60
|
-
filepath = fixed_local_path
|
|
61
|
-
else:
|
|
62
|
-
raise ValueError("Multiple files can only be downloaded to a directory")
|
|
63
|
-
if os.path.exists(filepath):
|
|
64
|
-
raise FileExistsError(f"File already exists: {filepath}")
|
|
65
|
-
print(f"Downloading to: {filepath}")
|
|
66
|
-
filestream = requests.get(download_url, stream=True)
|
|
67
|
-
with open(filepath, "wb") as f:
|
|
68
|
-
for chunk in filestream.iter_content(chunk_size=chunk_size):
|
|
69
|
-
if chunk: # Filter out keep-alive new chunks
|
|
70
|
-
f.write(chunk)
|
|
71
|
-
print(f"Completed")
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
@file.command("list")
|
|
75
|
-
def list_files(
|
|
76
|
-
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
77
|
-
mission: Optional[str] = typer.Option(None, help="Name of Mission"),
|
|
78
|
-
topics: Optional[str] = typer.Option(None, help="Comma separated list of topics"),
|
|
79
|
-
tags: Optional[str] = typer.Option(
|
|
80
|
-
None, help="Comma separated list of tagtype:tagvalue pairs"
|
|
81
|
-
),
|
|
82
|
-
):
|
|
83
|
-
"""
|
|
84
|
-
List all files with optional filters for project, mission, or topics.
|
|
85
|
-
|
|
86
|
-
Can list files of a project, mission, or with specific topics (Logical AND).
|
|
87
|
-
Examples:\n
|
|
88
|
-
- 'klein filelist'\n
|
|
89
|
-
- 'klein file list --project "Project_1"'\n
|
|
90
|
-
- 'klein file list --mission "Mission_1"'\n
|
|
91
|
-
- 'klein file list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw"'\n
|
|
92
|
-
- 'klein file list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw" --mission "Mission A"'
|
|
93
|
-
"""
|
|
94
|
-
try:
|
|
95
|
-
url = f"/file/filteredByNames"
|
|
96
|
-
params = {}
|
|
97
|
-
if project:
|
|
98
|
-
params["projectName"] = project
|
|
99
|
-
if mission:
|
|
100
|
-
params["missionName"] = mission
|
|
101
|
-
if topics:
|
|
102
|
-
params["topics"] = topics
|
|
103
|
-
if tags:
|
|
104
|
-
params["tags"] = {}
|
|
105
|
-
for tag in tags.split(","):
|
|
106
|
-
tagtype, tagvalue = tag.split("§")
|
|
107
|
-
params["tags"][tagtype] = tagvalue
|
|
108
|
-
|
|
109
|
-
client = AuthenticatedClient()
|
|
110
|
-
response = client.get(
|
|
111
|
-
url,
|
|
112
|
-
params=params,
|
|
113
|
-
)
|
|
114
|
-
if response.status_code >= 400:
|
|
115
|
-
raise AccessDeniedException(
|
|
116
|
-
f"Failed to fetch files: {response.json()['message']} ({response.status_code})",
|
|
117
|
-
"Access Denied",
|
|
118
|
-
)
|
|
119
|
-
data = response.json()
|
|
120
|
-
missions_by_project_uuid = {}
|
|
121
|
-
files_by_mission_uuid = {}
|
|
122
|
-
for file in data:
|
|
123
|
-
mission_uuid = file["mission"]["uuid"]
|
|
124
|
-
project_uuid = file["mission"]["project"]["uuid"]
|
|
125
|
-
if project_uuid not in missions_by_project_uuid:
|
|
126
|
-
missions_by_project_uuid[project_uuid] = []
|
|
127
|
-
if mission_uuid not in missions_by_project_uuid[project_uuid]:
|
|
128
|
-
missions_by_project_uuid[project_uuid].append(mission_uuid)
|
|
129
|
-
if mission_uuid not in files_by_mission_uuid:
|
|
130
|
-
files_by_mission_uuid[mission_uuid] = []
|
|
131
|
-
files_by_mission_uuid[mission_uuid].append(file)
|
|
132
|
-
|
|
133
|
-
print("Files by mission & Project:")
|
|
134
|
-
for project_uuid, missions in missions_by_project_uuid.items():
|
|
135
|
-
first_file = files_by_mission_uuid[missions[0]][0]
|
|
136
|
-
print(f"* {first_file['mission']['project']['name']}")
|
|
137
|
-
for mission in missions:
|
|
138
|
-
print(f" - {files_by_mission_uuid[mission][0]['mission']['name']}")
|
|
139
|
-
for file in files_by_mission_uuid[mission]:
|
|
140
|
-
print(f" - '{file['filename']}'")
|
|
141
|
-
|
|
142
|
-
except httpx.HTTPError as e:
|
|
143
|
-
print(f"Failed to fetch missions: {e}")
|
|
144
|
-
raise e
|