kleinkram 0.37.0.dev20241113182530__py3-none-any.whl → 0.37.0.dev20241118070559__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.

Files changed (48) hide show
  1. kleinkram/__init__.py +6 -0
  2. kleinkram/__main__.py +6 -0
  3. kleinkram/_version.py +6 -0
  4. kleinkram/api/__init__.py +0 -0
  5. kleinkram/api/client.py +65 -0
  6. kleinkram/api/file_transfer.py +328 -0
  7. kleinkram/api/routes.py +460 -0
  8. kleinkram/app.py +180 -0
  9. kleinkram/auth.py +96 -0
  10. kleinkram/commands/__init__.py +1 -0
  11. kleinkram/commands/download.py +103 -0
  12. kleinkram/commands/endpoint.py +62 -0
  13. kleinkram/commands/list.py +93 -0
  14. kleinkram/commands/mission.py +57 -0
  15. kleinkram/commands/project.py +24 -0
  16. kleinkram/commands/upload.py +138 -0
  17. kleinkram/commands/verify.py +117 -0
  18. kleinkram/config.py +171 -0
  19. kleinkram/consts.py +8 -1
  20. kleinkram/core.py +14 -0
  21. kleinkram/enums.py +10 -0
  22. kleinkram/errors.py +59 -0
  23. kleinkram/main.py +6 -489
  24. kleinkram/models.py +186 -0
  25. kleinkram/utils.py +179 -0
  26. {kleinkram-0.37.0.dev20241113182530.dist-info/licenses → kleinkram-0.37.0.dev20241118070559.dist-info}/LICENSE +1 -1
  27. kleinkram-0.37.0.dev20241118070559.dist-info/METADATA +113 -0
  28. kleinkram-0.37.0.dev20241118070559.dist-info/RECORD +33 -0
  29. {kleinkram-0.37.0.dev20241113182530.dist-info → kleinkram-0.37.0.dev20241118070559.dist-info}/WHEEL +2 -1
  30. kleinkram-0.37.0.dev20241118070559.dist-info/entry_points.txt +2 -0
  31. kleinkram-0.37.0.dev20241118070559.dist-info/top_level.txt +2 -0
  32. tests/__init__.py +0 -0
  33. tests/test_utils.py +153 -0
  34. kleinkram/api_client.py +0 -63
  35. kleinkram/auth/auth.py +0 -160
  36. kleinkram/endpoint/endpoint.py +0 -58
  37. kleinkram/error_handling.py +0 -177
  38. kleinkram/file/file.py +0 -144
  39. kleinkram/helper.py +0 -272
  40. kleinkram/mission/mission.py +0 -310
  41. kleinkram/project/project.py +0 -138
  42. kleinkram/queue/queue.py +0 -8
  43. kleinkram/tag/tag.py +0 -71
  44. kleinkram/topic/topic.py +0 -55
  45. kleinkram/user/user.py +0 -75
  46. kleinkram-0.37.0.dev20241113182530.dist-info/METADATA +0 -24
  47. kleinkram-0.37.0.dev20241113182530.dist-info/RECORD +0 -20
  48. kleinkram-0.37.0.dev20241113182530.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()
@@ -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)
@@ -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