kleinkram 0.32.3__py3-none-any.whl → 0.33.0__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/endpoint/endpoint.py +2 -1
- kleinkram/error_handling.py +29 -2
- kleinkram/helper.py +71 -41
- kleinkram/main.py +191 -94
- kleinkram/mission/mission.py +56 -17
- kleinkram/project/project.py +97 -17
- {kleinkram-0.32.3.dist-info → kleinkram-0.33.0.dist-info}/METADATA +1 -1
- kleinkram-0.33.0.dist-info/RECORD +20 -0
- kleinkram-0.32.3.dist-info/RECORD +0 -20
- {kleinkram-0.32.3.dist-info → kleinkram-0.33.0.dist-info}/WHEEL +0 -0
- {kleinkram-0.32.3.dist-info → kleinkram-0.33.0.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.32.3.dist-info → kleinkram-0.33.0.dist-info}/licenses/LICENSE +0 -0
kleinkram/endpoint/endpoint.py
CHANGED
|
@@ -4,7 +4,8 @@ from kleinkram.auth.auth import TokenFile
|
|
|
4
4
|
|
|
5
5
|
endpoint = typer.Typer(
|
|
6
6
|
name="endpoint",
|
|
7
|
-
help="Get Or Set the current 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).",
|
|
8
9
|
no_args_is_help=True,
|
|
9
10
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
10
11
|
)
|
kleinkram/error_handling.py
CHANGED
|
@@ -13,6 +13,12 @@ ExceptionType = "typing.Type[Exception]"
|
|
|
13
13
|
ErrorHandlingCallback = typing.Callable[[Exception], int]
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
class AbortException(Exception):
|
|
17
|
+
|
|
18
|
+
def __init__(self, message: str):
|
|
19
|
+
self.message = message
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class AccessDeniedException(Exception):
|
|
17
23
|
|
|
18
24
|
def __init__(self, message: str, api_error: str):
|
|
@@ -22,8 +28,9 @@ class AccessDeniedException(Exception):
|
|
|
22
28
|
|
|
23
29
|
def not_yet_implemented_handler(e: Exception):
|
|
24
30
|
console = Console(file=sys.stderr)
|
|
31
|
+
default_msg = "This feature is not yet implemented. Please check for updates or use the web interface."
|
|
25
32
|
panel = Panel(
|
|
26
|
-
"
|
|
33
|
+
f"{default_msg}",
|
|
27
34
|
title="Not Yet Implemented",
|
|
28
35
|
style="yellow",
|
|
29
36
|
padding=(1, 2),
|
|
@@ -105,6 +112,20 @@ def remote_down_handler(e: Exception):
|
|
|
105
112
|
print()
|
|
106
113
|
|
|
107
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
|
+
|
|
108
129
|
class ErrorHandledTyper(Typer):
|
|
109
130
|
error_handlers: typing.Dict[ExceptionType, ErrorHandlingCallback] = {
|
|
110
131
|
NotAuthenticatedException: not_authenticated_handler,
|
|
@@ -114,6 +135,7 @@ class ErrorHandledTyper(Typer):
|
|
|
114
135
|
ValueError: value_error_handler,
|
|
115
136
|
RemoteProtocolError: remote_down_handler,
|
|
116
137
|
ReadError: remote_down_handler,
|
|
138
|
+
AbortException: abort_handler,
|
|
117
139
|
}
|
|
118
140
|
|
|
119
141
|
def __init__(self, *args, **kwargs):
|
|
@@ -138,11 +160,16 @@ class ErrorHandledTyper(Typer):
|
|
|
138
160
|
f"An unhanded error of type {type(e).__name__} occurred.",
|
|
139
161
|
fg=typer.colors.RED,
|
|
140
162
|
)
|
|
141
|
-
|
|
163
|
+
|
|
142
164
|
typer.secho(
|
|
143
165
|
" » Please report this error to the developers.",
|
|
144
166
|
fg=typer.colors.RED,
|
|
145
167
|
)
|
|
168
|
+
|
|
169
|
+
typer.secho(f"\n\n{e}:", fg=typer.colors.RED)
|
|
170
|
+
console = Console()
|
|
171
|
+
console.print_exception(show_locals=True)
|
|
172
|
+
|
|
146
173
|
else:
|
|
147
174
|
self.error_handlers[type(e)](e)
|
|
148
175
|
|
kleinkram/helper.py
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import glob
|
|
2
2
|
import os
|
|
3
3
|
import queue
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
4
6
|
import threading
|
|
5
7
|
from datetime import datetime
|
|
6
8
|
from functools import partial
|
|
7
9
|
|
|
8
|
-
import typer
|
|
9
|
-
from botocore.config import Config
|
|
10
|
-
from botocore.utils import calculate_md5
|
|
11
|
-
from typing_extensions import Dict, List
|
|
12
10
|
import boto3
|
|
13
|
-
|
|
14
11
|
import tqdm
|
|
12
|
+
import typer
|
|
15
13
|
from boto3.s3.transfer import TransferConfig
|
|
16
|
-
from botocore.
|
|
14
|
+
from botocore.config import Config
|
|
15
|
+
from botocore.utils import calculate_md5
|
|
17
16
|
from rich import print
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from typing_extensions import Dict
|
|
18
19
|
|
|
19
20
|
from kleinkram.api_client import AuthenticatedClient
|
|
20
21
|
|
|
@@ -106,84 +107,101 @@ def expand_and_match(path_pattern):
|
|
|
106
107
|
return file_list
|
|
107
108
|
|
|
108
109
|
|
|
109
|
-
def uploadFiles(
|
|
110
|
+
def uploadFiles(
|
|
111
|
+
files_with_access: Dict[str, object], paths: Dict[str, str], nrThreads: int
|
|
112
|
+
):
|
|
110
113
|
client = AuthenticatedClient()
|
|
111
114
|
|
|
112
|
-
session = boto3.Session(
|
|
113
|
-
aws_access_key_id=credentials["accessKey"],
|
|
114
|
-
aws_secret_access_key=credentials["secretKey"],
|
|
115
|
-
aws_session_token=credentials["sessionToken"],
|
|
116
|
-
)
|
|
117
115
|
api_endpoint = client.tokenfile.endpoint
|
|
118
116
|
if api_endpoint == "http://localhost:3000":
|
|
119
117
|
minio_endpoint = "http://localhost:9000"
|
|
120
118
|
else:
|
|
121
119
|
minio_endpoint = api_endpoint.replace("api", "minio")
|
|
122
120
|
|
|
123
|
-
config = Config(retries={"max_attempts": 10, "mode": "standard"})
|
|
124
|
-
s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
|
|
125
|
-
|
|
126
121
|
_queue = queue.Queue()
|
|
127
|
-
for
|
|
128
|
-
_queue.put(
|
|
122
|
+
for file_with_access in files_with_access:
|
|
123
|
+
_queue.put((file_with_access, str(paths[file_with_access["fileName"]])))
|
|
124
|
+
|
|
129
125
|
threads = []
|
|
130
|
-
|
|
131
|
-
failed_uploads = []
|
|
126
|
+
transfer_callback = TransferCallback()
|
|
132
127
|
|
|
133
128
|
for i in range(nrThreads):
|
|
134
129
|
thread = threading.Thread(
|
|
135
|
-
target=uploadFile,
|
|
130
|
+
target=uploadFile,
|
|
131
|
+
args=(_queue, minio_endpoint, transfer_callback),
|
|
136
132
|
)
|
|
137
133
|
thread.start()
|
|
138
134
|
threads.append(thread)
|
|
139
135
|
for thread in threads:
|
|
140
136
|
thread.join()
|
|
141
|
-
if len(failed_uploads) > 0:
|
|
142
|
-
print("Failed to upload the following files:")
|
|
143
|
-
for file in failed_uploads:
|
|
144
|
-
print(file)
|
|
145
137
|
|
|
146
138
|
|
|
147
139
|
def uploadFile(
|
|
148
140
|
_queue: queue.Queue,
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
failed_uploads: List[str],
|
|
141
|
+
minio_endpoint: str,
|
|
142
|
+
transfer_callback: TransferCallback,
|
|
152
143
|
):
|
|
144
|
+
config = Config(retries={"max_attempts": 10, "mode": "standard"})
|
|
145
|
+
|
|
153
146
|
while True:
|
|
154
147
|
try:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
file_with_access, filepath = _queue.get(timeout=3)
|
|
149
|
+
|
|
150
|
+
if "error" in file_with_access and (
|
|
151
|
+
file_with_access["error"] is not None or file_with_access["error"] != ""
|
|
152
|
+
):
|
|
153
|
+
console = Console(file=sys.stderr, style="red", highlight=False)
|
|
154
|
+
console.print(
|
|
155
|
+
f"Error uploading file: {file_with_access['fileName']} ({filepath}): {file_with_access['error']}"
|
|
156
|
+
)
|
|
157
|
+
_queue.task_done()
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
access_key = file_with_access["accessCredentials"]["accessKey"]
|
|
161
|
+
secret_key = file_with_access["accessCredentials"]["secretKey"]
|
|
162
|
+
session_token = file_with_access["accessCredentials"]["sessionToken"]
|
|
163
|
+
|
|
164
|
+
session = boto3.Session(
|
|
165
|
+
aws_access_key_id=access_key,
|
|
166
|
+
aws_secret_access_key=secret_key,
|
|
167
|
+
aws_session_token=session_token,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
s3 = session.resource("s3", endpoint_url=minio_endpoint, config=config)
|
|
171
|
+
|
|
172
|
+
fileu_uid = file_with_access["fileUUID"]
|
|
173
|
+
bucket = file_with_access["bucket"]
|
|
174
|
+
|
|
175
|
+
transfer_config = TransferConfig(
|
|
176
|
+
multipart_chunksize=10 * 1024 * 1024,
|
|
177
|
+
max_concurrency=5,
|
|
162
178
|
)
|
|
163
179
|
with open(filepath, "rb") as f:
|
|
164
180
|
md5_checksum = calculate_md5(f)
|
|
165
181
|
file_size = os.path.getsize(filepath)
|
|
166
|
-
|
|
167
|
-
callback_function = create_transfer_callback(
|
|
182
|
+
transfer_callback.add_file(filepath, file_size)
|
|
183
|
+
callback_function = create_transfer_callback(
|
|
184
|
+
transfer_callback, filepath
|
|
185
|
+
)
|
|
168
186
|
s3.Bucket(bucket).upload_file(
|
|
169
187
|
filepath,
|
|
170
|
-
|
|
171
|
-
Config=
|
|
188
|
+
fileu_uid,
|
|
189
|
+
Config=transfer_config,
|
|
172
190
|
Callback=callback_function,
|
|
173
191
|
)
|
|
174
192
|
|
|
175
193
|
client = AuthenticatedClient()
|
|
176
194
|
res = client.post(
|
|
177
195
|
"/queue/confirmUpload",
|
|
178
|
-
json={"uuid":
|
|
196
|
+
json={"uuid": fileu_uid, "md5": md5_checksum},
|
|
179
197
|
)
|
|
180
198
|
res.raise_for_status()
|
|
181
199
|
_queue.task_done()
|
|
182
200
|
except queue.Empty:
|
|
183
201
|
break
|
|
184
202
|
except Exception as e:
|
|
185
|
-
print(
|
|
186
|
-
|
|
203
|
+
print("Error uploading file: " + filepath)
|
|
204
|
+
print(e)
|
|
187
205
|
_queue.task_done()
|
|
188
206
|
|
|
189
207
|
|
|
@@ -235,6 +253,18 @@ def promptForTags(setTags: Dict[str, str], requiredTags: Dict[str, str]):
|
|
|
235
253
|
setTags[required_tag["uuid"]] = tag_value
|
|
236
254
|
|
|
237
255
|
|
|
256
|
+
def is_valid_UUIDv4(uuid: str) -> bool:
|
|
257
|
+
has_correct_length = len(uuid) == 36
|
|
258
|
+
|
|
259
|
+
# is UUID4
|
|
260
|
+
uuid_regex = (
|
|
261
|
+
"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
262
|
+
)
|
|
263
|
+
is_valid_uuid = re.match(uuid_regex, uuid)
|
|
264
|
+
|
|
265
|
+
return has_correct_length and is_valid_uuid
|
|
266
|
+
|
|
267
|
+
|
|
238
268
|
if __name__ == "__main__":
|
|
239
269
|
res = expand_and_match(
|
|
240
270
|
"~/Downloads/dodo_mission_2024_02_08-20240408T074313Z-003/**.bag"
|
kleinkram/main.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import importlib.metadata
|
|
1
2
|
import os
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
4
|
from enum import Enum
|
|
4
5
|
|
|
5
6
|
import httpx
|
|
6
|
-
import importlib.metadata
|
|
7
7
|
import typer
|
|
8
8
|
from rich import print
|
|
9
9
|
from rich.table import Table
|
|
@@ -14,7 +14,10 @@ from typing_extensions import Annotated, List, Optional
|
|
|
14
14
|
from kleinkram.api_client import AuthenticatedClient
|
|
15
15
|
from kleinkram.auth.auth import login, setCliKey, logout
|
|
16
16
|
from kleinkram.endpoint.endpoint import endpoint
|
|
17
|
-
from kleinkram.error_handling import
|
|
17
|
+
from kleinkram.error_handling import (
|
|
18
|
+
ErrorHandledTyper,
|
|
19
|
+
AccessDeniedException,
|
|
20
|
+
)
|
|
18
21
|
from kleinkram.file.file import file
|
|
19
22
|
from kleinkram.mission.mission import missionCommands
|
|
20
23
|
from kleinkram.project.project import project
|
|
@@ -22,7 +25,13 @@ from kleinkram.queue.queue import queue
|
|
|
22
25
|
from kleinkram.tag.tag import tag
|
|
23
26
|
from kleinkram.topic.topic import topic
|
|
24
27
|
from kleinkram.user.user import user
|
|
25
|
-
from .helper import
|
|
28
|
+
from .helper import (
|
|
29
|
+
uploadFiles,
|
|
30
|
+
expand_and_match,
|
|
31
|
+
canUploadMission,
|
|
32
|
+
promptForTags,
|
|
33
|
+
is_valid_UUIDv4,
|
|
34
|
+
)
|
|
26
35
|
|
|
27
36
|
|
|
28
37
|
class CommandPanel(str, Enum):
|
|
@@ -73,6 +82,9 @@ app = ErrorHandledTyper(
|
|
|
73
82
|
context_settings={"help_option_names": ["-h", "--help"]},
|
|
74
83
|
no_args_is_help=True,
|
|
75
84
|
cls=OrderCommands,
|
|
85
|
+
help=f"Kleinkram CLI\n\nThe Kleinkram CLI is a command line interface for Kleinkram. "
|
|
86
|
+
f"For a list of available commands, run 'klein --help' or visit "
|
|
87
|
+
f"https://docs.datasets.leggedrobotics.com/usage/cli/cli-getting-started.html for more information.",
|
|
76
88
|
)
|
|
77
89
|
|
|
78
90
|
|
|
@@ -107,23 +119,35 @@ app.command(hidden=True)(setCliKey)
|
|
|
107
119
|
|
|
108
120
|
@app.command("download", rich_help_panel=CommandPanel.CoreCommands)
|
|
109
121
|
def download():
|
|
110
|
-
|
|
122
|
+
print(
|
|
123
|
+
"Not implemented yet. Consider using the 'klein file download' or 'klein mission download' commands."
|
|
124
|
+
)
|
|
111
125
|
|
|
112
126
|
|
|
113
|
-
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands)
|
|
127
|
+
@app.command("upload", rich_help_panel=CommandPanel.CoreCommands, no_args_is_help=True)
|
|
114
128
|
def upload(
|
|
115
129
|
path: Annotated[
|
|
116
130
|
List[str],
|
|
117
|
-
typer.Option(
|
|
118
|
-
],
|
|
119
|
-
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
120
|
-
mission: Annotated[
|
|
121
|
-
str, typer.Option(prompt=True, help="Name of Mission to create")
|
|
131
|
+
typer.Option(help="Path to files to upload, Regex supported"),
|
|
122
132
|
],
|
|
133
|
+
project: Annotated[str, typer.Option(help="Name or UUID of a Project")],
|
|
134
|
+
mission: Annotated[str, typer.Option(help="Name of UUID Mission to create")],
|
|
123
135
|
tags: Annotated[
|
|
124
136
|
Optional[List[str]],
|
|
125
|
-
typer.Option(
|
|
137
|
+
typer.Option(help="Tags to add to the mission"),
|
|
126
138
|
] = None,
|
|
139
|
+
fix_filenames: Annotated[
|
|
140
|
+
bool,
|
|
141
|
+
typer.Option(help="Automatically fix filenames such that they are valid"),
|
|
142
|
+
] = False,
|
|
143
|
+
create_project: Annotated[
|
|
144
|
+
bool,
|
|
145
|
+
typer.Option(help="Allows adding files to an existing mission"),
|
|
146
|
+
] = False,
|
|
147
|
+
create_mission: Annotated[
|
|
148
|
+
bool,
|
|
149
|
+
typer.Option(help="Allows adding files to an existing mission"),
|
|
150
|
+
] = False,
|
|
127
151
|
):
|
|
128
152
|
"""
|
|
129
153
|
Upload files matching the path to a mission in a project.
|
|
@@ -134,110 +158,183 @@ def upload(
|
|
|
134
158
|
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1" --tags "0700946d-1d6a-4520-b263-0e177f49c35b:LEE-H" --tags "1565118d-593c-4517-8c2d-9658452d9319:Dodo"'\n
|
|
135
159
|
|
|
136
160
|
"""
|
|
137
|
-
files = []
|
|
138
|
-
for p in path:
|
|
139
|
-
files.extend(expand_and_match(p))
|
|
140
|
-
filenames = list(
|
|
141
|
-
map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x), files))
|
|
142
|
-
)
|
|
143
|
-
if not filenames:
|
|
144
|
-
raise ValueError("No files found matching the given path.")
|
|
145
161
|
|
|
146
|
-
|
|
147
|
-
f"Uploading the following files to mission '{mission}' in project '{project}':"
|
|
148
|
-
)
|
|
149
|
-
filepaths = {}
|
|
150
|
-
for path in files:
|
|
151
|
-
if not os.path.isdir(path):
|
|
152
|
-
filepaths[path.split("/")[-1]] = path
|
|
153
|
-
typer.secho(f" - {path}", fg=typer.colors.RESET)
|
|
154
|
-
|
|
155
|
-
try:
|
|
156
|
-
client = AuthenticatedClient()
|
|
162
|
+
client = AuthenticatedClient()
|
|
157
163
|
|
|
164
|
+
##############################
|
|
165
|
+
# Check if project exists
|
|
166
|
+
##############################
|
|
167
|
+
if is_valid_UUIDv4(project):
|
|
168
|
+
get_project_url = "/project/one"
|
|
169
|
+
project_response = client.get(get_project_url, params={"uuid": project})
|
|
170
|
+
else:
|
|
158
171
|
get_project_url = "/project/byName"
|
|
159
172
|
project_response = client.get(get_project_url, params={"name": project})
|
|
160
|
-
|
|
173
|
+
|
|
174
|
+
if project_response.status_code >= 400:
|
|
175
|
+
if not create_project and not is_valid_UUIDv4(project):
|
|
161
176
|
raise AccessDeniedException(
|
|
162
177
|
f"The project '{project}' does not exist or you do not have access to it.\n"
|
|
163
|
-
f"Consider using the following command to create a project: 'klein project create'
|
|
178
|
+
f"Consider using the following command to create a project: 'klein project create' "
|
|
179
|
+
f"or consider passing the flag '--create-project' to create the project automatically.",
|
|
164
180
|
f"{project_response.json()['message']} ({project_response.status_code})",
|
|
165
181
|
)
|
|
182
|
+
elif is_valid_UUIDv4(project):
|
|
183
|
+
raise ValueError(
|
|
184
|
+
f"Project '{project}' does not exist. UUIDs cannot be used to create projects.\n"
|
|
185
|
+
f"Please provide a valid project name or consider creating the project using the"
|
|
186
|
+
f" following command: 'klein project create'"
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
print(f"Project '{project}' does not exist. Creating it now.")
|
|
190
|
+
create_project_url = "/project/create"
|
|
191
|
+
project_response = client.post(
|
|
192
|
+
create_project_url,
|
|
193
|
+
json={
|
|
194
|
+
"name": project,
|
|
195
|
+
"description": "Autogenerated by klein CLI",
|
|
196
|
+
"requiredTags": [],
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
if project_response.status_code >= 400:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"Failed to create project. Status Code: "
|
|
202
|
+
f"{str(project_response.status_code)}\n"
|
|
203
|
+
f"{project_response.json()['message'][0]}"
|
|
204
|
+
)
|
|
205
|
+
print("Project created successfully.")
|
|
206
|
+
|
|
207
|
+
project_json = project_response.json()
|
|
208
|
+
if not project_json["uuid"]:
|
|
209
|
+
print(f"Project not found: '{project}'")
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
can_upload = canUploadMission(client, project_json["uuid"])
|
|
213
|
+
if not can_upload:
|
|
214
|
+
raise AccessDeniedException(
|
|
215
|
+
f"You do not have the required permissions to upload to project '{project}'\n",
|
|
216
|
+
"Access Denied",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
if not tags:
|
|
220
|
+
tags = []
|
|
221
|
+
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
166
222
|
|
|
167
|
-
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
223
|
+
required_tags = (
|
|
224
|
+
project_json["requiredTags"] if "requiredTags" in project_json else []
|
|
225
|
+
)
|
|
226
|
+
promptForTags(tags_dict, required_tags)
|
|
227
|
+
|
|
228
|
+
##############################
|
|
229
|
+
# Check if mission exists
|
|
230
|
+
##############################
|
|
231
|
+
if is_valid_UUIDv4(mission):
|
|
232
|
+
get_mission_url = "/mission/one"
|
|
233
|
+
mission_response = client.get(get_mission_url, params={"uuid": mission})
|
|
234
|
+
else:
|
|
235
|
+
get_mission_url = "/mission/byName"
|
|
236
|
+
mission_response = client.get(get_mission_url, params={"name": mission})
|
|
171
237
|
|
|
172
|
-
|
|
173
|
-
if not
|
|
238
|
+
if mission_response.status_code >= 400:
|
|
239
|
+
if not create_mission:
|
|
174
240
|
raise AccessDeniedException(
|
|
175
|
-
f"
|
|
176
|
-
"
|
|
241
|
+
f"The mission '{mission}' does not exist or you do not have access to it.\n"
|
|
242
|
+
f"Consider using the following command to create a mission: 'klein mission create' "
|
|
243
|
+
f"or consider passing the flag '--create-mission' to create the mission automatically.",
|
|
244
|
+
f"{mission_response.json()['message']} ({mission_response.status_code})",
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
print(f"Mission '{mission}' does not exist. Creating it now.")
|
|
248
|
+
create_mission_url = "/mission/create"
|
|
249
|
+
mission_response = client.post(
|
|
250
|
+
create_mission_url,
|
|
251
|
+
json={
|
|
252
|
+
"name": mission,
|
|
253
|
+
"projectUUID": project_json["uuid"],
|
|
254
|
+
"tags": tags_dict,
|
|
255
|
+
},
|
|
177
256
|
)
|
|
257
|
+
if mission_response.status_code >= 400:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"Failed to create mission. Status Code: "
|
|
260
|
+
f"{str(mission_response.status_code)}\n"
|
|
261
|
+
f"{mission_response.json()['message'][0]}"
|
|
262
|
+
)
|
|
178
263
|
|
|
179
|
-
|
|
180
|
-
tags = []
|
|
181
|
-
tags_dict = {item.split(":")[0]: item.split(":")[1] for item in tags}
|
|
264
|
+
mission_json = mission_response.json()
|
|
182
265
|
|
|
183
|
-
|
|
266
|
+
files = []
|
|
267
|
+
for p in path:
|
|
268
|
+
files.extend(expand_and_match(p))
|
|
184
269
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
270
|
+
print(
|
|
271
|
+
f"Uploading the following files to mission '{mission_json['name']}' in project '{project_json['name']}':"
|
|
272
|
+
)
|
|
273
|
+
filename_filepaths_map = {}
|
|
274
|
+
for path in files:
|
|
275
|
+
if not os.path.isdir(path):
|
|
276
|
+
|
|
277
|
+
filename = path.split("/")[-1]
|
|
278
|
+
filename_without_extension, extension = os.path.splitext(filename)
|
|
279
|
+
if fix_filenames:
|
|
280
|
+
|
|
281
|
+
# replace all non-alphanumeric characters with underscores
|
|
282
|
+
filename_without_extension = "".join(
|
|
283
|
+
char if char.isalnum() else "_"
|
|
284
|
+
for char in filename_without_extension
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# trim filename to 40 characters
|
|
288
|
+
filename_without_extension = filename_without_extension[:40]
|
|
289
|
+
filename = f"{filename_without_extension}{extension}"
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
not filename.replace(".", "")
|
|
293
|
+
.replace("_", "")
|
|
294
|
+
.replace("-", "")
|
|
295
|
+
.isalnum()
|
|
296
|
+
):
|
|
191
297
|
raise ValueError(
|
|
192
|
-
f"
|
|
193
|
-
f"
|
|
298
|
+
f"Filename '{filename}' is not valid. It must only contain alphanumeric characters, underscores and "
|
|
299
|
+
f"hyphens. Consider using the '--fix-filenames' option to automatically fix the filenames."
|
|
194
300
|
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
301
|
+
|
|
302
|
+
if not 3 <= len(filename_without_extension) <= 40:
|
|
303
|
+
raise ValueError(
|
|
304
|
+
f"Filename '{filename}' is not valid. It must be between 3 and 40 characters long. Consider using "
|
|
305
|
+
f"the '--fix-filenames' option to automatically fix the filenames."
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
filename_filepaths_map[filename] = path
|
|
309
|
+
typer.secho(f" - {filename}", fg=typer.colors.RESET)
|
|
310
|
+
print("\n\n")
|
|
311
|
+
|
|
312
|
+
filenames = list(filename_filepaths_map.keys())
|
|
313
|
+
|
|
314
|
+
if not filenames:
|
|
315
|
+
raise ValueError("No files found matching the given path.")
|
|
316
|
+
|
|
317
|
+
# validate filenames
|
|
318
|
+
if len(filenames) != len(set(filenames)):
|
|
319
|
+
raise ValueError(
|
|
320
|
+
"Filenames must be unique. Please check the files you are trying to upload. This can happen if you have "
|
|
321
|
+
"multiple files with the same name in different directories or use the '--fix-filenames' option."
|
|
205
322
|
)
|
|
206
|
-
if new_mission.status_code >= 400:
|
|
207
|
-
raise ValueError(
|
|
208
|
-
"Failed to create mission. Status Code: "
|
|
209
|
-
+ str(new_mission.status_code)
|
|
210
|
-
+ "\n"
|
|
211
|
-
+ new_mission.json()["message"]
|
|
212
|
-
)
|
|
213
|
-
new_mission_data = new_mission.json()
|
|
214
323
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
324
|
+
get_temporary_credentials = "/file/temporaryAccess"
|
|
325
|
+
response = client.post(
|
|
326
|
+
get_temporary_credentials,
|
|
327
|
+
json={"filenames": filenames, "missionUUID": mission_json["uuid"]},
|
|
328
|
+
)
|
|
329
|
+
if response.status_code >= 400:
|
|
330
|
+
raise ValueError(
|
|
331
|
+
"Failed to get temporary credentials. Status Code: "
|
|
332
|
+
+ str(response.status_code)
|
|
333
|
+
+ "\n"
|
|
334
|
+
+ response.json()["message"][0]
|
|
219
335
|
)
|
|
220
|
-
if response_2.status_code >= 400:
|
|
221
|
-
raise ValueError(
|
|
222
|
-
"Failed to get temporary credentials. Status Code: "
|
|
223
|
-
+ str(response_2.status_code)
|
|
224
|
-
+ "\n"
|
|
225
|
-
+ response_2.json()["message"]
|
|
226
|
-
)
|
|
227
|
-
temp_credentials = response_2.json()
|
|
228
|
-
credential = temp_credentials["credentials"]
|
|
229
|
-
confirmed_files = temp_credentials["files"]
|
|
230
|
-
for _file in filenames:
|
|
231
|
-
if not _file in confirmed_files.keys():
|
|
232
|
-
raise Exception(
|
|
233
|
-
"Could not upload File '" + _file + "'. Is the filename unique? "
|
|
234
|
-
)
|
|
235
|
-
confirmed_files[_file]["filepath"] = filepaths[_file]
|
|
236
|
-
if len(confirmed_files.keys()) > 0:
|
|
237
|
-
uploadFiles(confirmed_files, credential, 4)
|
|
238
336
|
|
|
239
|
-
|
|
240
|
-
print(e)
|
|
337
|
+
uploadFiles(response.json(), filename_filepaths_map, 4)
|
|
241
338
|
|
|
242
339
|
|
|
243
340
|
@queue.command("list")
|
kleinkram/mission/mission.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
|
-
from botocore.utils import calculate_md5
|
|
4
|
-
from typing_extensions import Annotated, Optional, List
|
|
2
|
+
import re
|
|
5
3
|
|
|
6
4
|
import httpx
|
|
7
5
|
import requests
|
|
8
6
|
import typer
|
|
9
7
|
from rich.console import Console
|
|
10
8
|
from rich.table import Table
|
|
9
|
+
from tqdm import tqdm
|
|
10
|
+
from typing_extensions import Annotated, Optional, List
|
|
11
11
|
|
|
12
12
|
from kleinkram.api_client import AuthenticatedClient
|
|
13
13
|
from kleinkram.error_handling import AccessDeniedException
|
|
@@ -143,15 +143,20 @@ def mission_by_uuid(
|
|
|
143
143
|
|
|
144
144
|
if json:
|
|
145
145
|
print(data)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
return
|
|
147
|
+
print(f"mission: {data['name']}")
|
|
148
|
+
print(f"Creator: {data['creator']['name']}")
|
|
149
|
+
print("Project: " + data["project"]["name"])
|
|
150
|
+
table = Table("Filename", "Size", "date")
|
|
151
|
+
|
|
152
|
+
if "files" not in data:
|
|
153
|
+
print("No files found for mission.")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
for file in data["files"]:
|
|
157
|
+
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
158
|
+
console = Console()
|
|
159
|
+
console.print(table)
|
|
155
160
|
|
|
156
161
|
|
|
157
162
|
@missionCommands.command("download")
|
|
@@ -160,6 +165,11 @@ def download(
|
|
|
160
165
|
List[str], typer.Option(help="UUIDs of Mission to download")
|
|
161
166
|
],
|
|
162
167
|
local_path: Annotated[str, typer.Option()],
|
|
168
|
+
pattern: Optional[str] = typer.Option(
|
|
169
|
+
None,
|
|
170
|
+
help="Simple pattern to match the filename against. Allowed are alphanumeric characters,"
|
|
171
|
+
" '_', '-', '.' and '*' as wildcard.",
|
|
172
|
+
),
|
|
163
173
|
):
|
|
164
174
|
"""
|
|
165
175
|
|
|
@@ -171,7 +181,13 @@ def download(
|
|
|
171
181
|
if not os.path.isdir(local_path):
|
|
172
182
|
raise ValueError(f"Local path '{local_path}' is not a directory.")
|
|
173
183
|
if not os.listdir(local_path) == []:
|
|
174
|
-
|
|
184
|
+
|
|
185
|
+
full_local_path = os.path.abspath(local_path)
|
|
186
|
+
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Local path '{full_local_path}' is not empty, it contains {len(os.listdir(local_path))} files. "
|
|
189
|
+
f"The local target directory must be empty."
|
|
190
|
+
)
|
|
175
191
|
|
|
176
192
|
client = AuthenticatedClient()
|
|
177
193
|
for single_mission_uuid in mission_uuid:
|
|
@@ -189,20 +205,43 @@ def download(
|
|
|
189
205
|
if len(paths) == 0:
|
|
190
206
|
continue
|
|
191
207
|
|
|
208
|
+
# validate search pattern
|
|
209
|
+
if pattern:
|
|
210
|
+
if not re.match(r"^[a-zA-Z0-9_\-.*]+$", pattern):
|
|
211
|
+
raise ValueError(
|
|
212
|
+
"Invalid pattern. Allowed are alphanumeric characters, '_', '-', '.' and '*' as wildcard."
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
regex = pattern.replace("*", ".*")
|
|
216
|
+
pattern = re.compile(regex)
|
|
217
|
+
|
|
218
|
+
print(f"Found {len(paths)} files in mission:")
|
|
219
|
+
paths = [
|
|
220
|
+
path for path in paths if not pattern or pattern.match(path["filename"])
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
if pattern:
|
|
224
|
+
print(
|
|
225
|
+
f" » filtered to {len(paths)} files matching pattern '{pattern.pattern}'."
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
print(f"Start downloading {len(paths)} files to '{local_path}':\n")
|
|
192
229
|
for path in paths:
|
|
193
230
|
|
|
194
231
|
filename = path["filename"]
|
|
195
|
-
print(f" - {filename}")
|
|
196
232
|
|
|
197
233
|
response = requests.get(path["link"], stream=True) # Enable streaming mode
|
|
198
|
-
chunk_size = 1024 *
|
|
234
|
+
chunk_size = 1024 * 1024 * 10 # 10 MB chunks, adjust size if needed
|
|
199
235
|
|
|
200
236
|
# Open the file for writing in binary mode
|
|
201
237
|
with open(os.path.join(local_path, filename), "wb") as f:
|
|
202
|
-
for chunk in
|
|
238
|
+
for chunk in tqdm(
|
|
239
|
+
response.iter_content(chunk_size=chunk_size),
|
|
240
|
+
unit="MB",
|
|
241
|
+
desc=filename,
|
|
242
|
+
):
|
|
203
243
|
if chunk: # Filter out keep-alive new chunks
|
|
204
244
|
f.write(chunk)
|
|
205
|
-
print(f" Downloaded {filename}")
|
|
206
245
|
|
|
207
246
|
|
|
208
247
|
@missionCommands.command("upload")
|
kleinkram/project/project.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
from typing_extensions import Annotated
|
|
2
|
-
|
|
3
1
|
import httpx
|
|
4
2
|
import typer
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
from typing_extensions import Annotated
|
|
5
6
|
|
|
6
7
|
from kleinkram.api_client import AuthenticatedClient
|
|
7
8
|
|
|
@@ -13,25 +14,94 @@ project = typer.Typer(
|
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
@project.command("list")
|
|
17
|
+
@project.command("list", help="List all projects")
|
|
17
18
|
def list_projects():
|
|
18
19
|
"""
|
|
19
20
|
List all projects.
|
|
20
21
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
projects = response.json()[0]
|
|
26
|
-
print("Projects:")
|
|
27
|
-
for _project in projects:
|
|
28
|
-
print(f"- {_project['name']}")
|
|
22
|
+
client = AuthenticatedClient()
|
|
23
|
+
response = client.get("/project/filtered")
|
|
24
|
+
response.raise_for_status()
|
|
25
|
+
projects = response.json()[0]
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
print(
|
|
27
|
+
if len(projects) == 0:
|
|
28
|
+
print("No projects found. Create a new project using 'klein project create'")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
stdout_console = Console(stderr=False)
|
|
32
|
+
stderr_console = Console(stderr=True)
|
|
33
|
+
stderr_console.print(f"\nfound {len(projects)} projects with the following UUIDs:")
|
|
32
34
|
|
|
35
|
+
# print the uuids to stdout for simple piping
|
|
36
|
+
for p in projects:
|
|
37
|
+
stderr_console.print(" - ", end="")
|
|
38
|
+
stdout_console.print(p["uuid"])
|
|
39
|
+
stderr_console.print("\n")
|
|
40
|
+
|
|
41
|
+
# Print a summary table using rich to stderr
|
|
42
|
+
table = Table(title="Projects", expand=True)
|
|
43
|
+
table.add_column("Project UUID", width=10)
|
|
44
|
+
table.add_column("Project Name", width=12)
|
|
45
|
+
table.add_column("Description")
|
|
46
|
+
for p in projects:
|
|
47
|
+
table.add_row(p["uuid"], p["name"], p["description"])
|
|
48
|
+
|
|
49
|
+
stderr_console.print(table)
|
|
50
|
+
stderr_console.print("\n")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@project.command("details", help="Get details of a project", no_args_is_help=True)
|
|
54
|
+
def project_details(
|
|
55
|
+
project_uuid: Annotated[
|
|
56
|
+
str, typer.Argument(help="UUID of the project to get details of")
|
|
57
|
+
]
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Get details of a project
|
|
61
|
+
"""
|
|
62
|
+
client = AuthenticatedClient()
|
|
63
|
+
response = client.get(f"/project/one?uuid={project_uuid}")
|
|
64
|
+
response.raise_for_status()
|
|
65
|
+
project = response.json()
|
|
33
66
|
|
|
34
|
-
|
|
67
|
+
stdout_console = Console(stderr=False)
|
|
68
|
+
stderr_console = Console(stderr=True)
|
|
69
|
+
stderr_console.print(
|
|
70
|
+
f"\nDetails of project with UUID {project_uuid}:", highlight=False
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Print the details to stderr using rich
|
|
74
|
+
table = Table(title="Project Details", expand=True)
|
|
75
|
+
table.add_column("Key", width=16)
|
|
76
|
+
table.add_column("Value")
|
|
77
|
+
for key, value in project.items():
|
|
78
|
+
|
|
79
|
+
access_name_map = {0: "READ", 10: "CREATE", 20: "WRITE", 30: "DELETE"}
|
|
80
|
+
|
|
81
|
+
if key == "project_accesses":
|
|
82
|
+
value = ", ".join(
|
|
83
|
+
[
|
|
84
|
+
f"'{access['accessGroup']['name']}' ({access_name_map[access['rights']]})"
|
|
85
|
+
for access in value
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if key == "missions":
|
|
90
|
+
value = ", ".join([f"'{mission['name']}'" for mission in value])
|
|
91
|
+
|
|
92
|
+
if key == "creator":
|
|
93
|
+
value = value["name"]
|
|
94
|
+
|
|
95
|
+
table.add_row(key, f"{value}")
|
|
96
|
+
|
|
97
|
+
stderr_console.print(table)
|
|
98
|
+
stderr_console.print("\nList of missions:")
|
|
99
|
+
for mission in project["missions"]:
|
|
100
|
+
stderr_console.print(" - ", end="")
|
|
101
|
+
stdout_console.print(mission["uuid"])
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@project.command("create", no_args_is_help=True, help="Create a new project")
|
|
35
105
|
def create_project(
|
|
36
106
|
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
37
107
|
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
@@ -49,10 +119,20 @@ def create_project(
|
|
|
49
119
|
if response.status_code >= 400:
|
|
50
120
|
response_json = response.json()
|
|
51
121
|
response_text = response_json["message"]
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
122
|
+
raise ValueError(f"Failed to create project\n » {response_text}!")
|
|
123
|
+
|
|
124
|
+
stderr_console = Console(stderr=True)
|
|
125
|
+
stderr_console.print(f"Project '{name}' created successfully.")
|
|
126
|
+
|
|
127
|
+
stdout_console = Console(stderr=False)
|
|
128
|
+
stderr_console.print("\nProject UUID:\n - ", end="")
|
|
129
|
+
stdout_console.print(response.json()["uuid"])
|
|
55
130
|
|
|
56
131
|
except httpx.HTTPError as e:
|
|
57
132
|
print(f"Failed to create project: {e}")
|
|
58
133
|
raise e
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@project.command("delete", help="Delete a project")
|
|
137
|
+
def delete_project():
|
|
138
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
kleinkram/api_client.py,sha256=1GPsM-XFbPYEKP7RfWmzMTwxRqnVh4wtHVuW25KT8kA,2264
|
|
3
|
+
kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
|
|
4
|
+
kleinkram/error_handling.py,sha256=vvNtXSOnXs4b-aYEUIA0GVQWXLRW5I5FtNE1PZPHEPc,4704
|
|
5
|
+
kleinkram/helper.py,sha256=ELvs-p7zpJUP_rtTIVpcUCTz0D4WegoUZQ11UqUrUkg,9288
|
|
6
|
+
kleinkram/main.py,sha256=Ew4KViP1GkqUZYRtFbZ3HRxlU1TMGnfWJq8dBr0Y8_A,13615
|
|
7
|
+
kleinkram/auth/auth.py,sha256=w3-TsxWxURzLQ3_p43zgV4Rlh4dVL_WqI6HG2aes-b4,4991
|
|
8
|
+
kleinkram/endpoint/endpoint.py,sha256=WmHUH10_OSZUMrovh2yBeth9dBcn0yE7PxnnD4yPW-Y,1582
|
|
9
|
+
kleinkram/file/file.py,sha256=gLCZDHHgQWq25OmeG-lwkIh4aRZaLK12xxLkbhZ_m-g,5390
|
|
10
|
+
kleinkram/mission/mission.py,sha256=SkTjauagLamwZaQTWTeGmXAJ9YgUrBdh_3EbjYNZfZM,10082
|
|
11
|
+
kleinkram/project/project.py,sha256=4BOT3iOZZnMrZuALzBxQ5r-8dC8eZkel1OjxUFzK0w4,4336
|
|
12
|
+
kleinkram/queue/queue.py,sha256=MaLBjAu8asi9BkPvbbT-5AobCcpy3ex5rxM1kHpRINA,181
|
|
13
|
+
kleinkram/tag/tag.py,sha256=JSHbDPVfsvP34MuQhw__DPQk-Bah5G9BgwYsj_K_JGc,1805
|
|
14
|
+
kleinkram/topic/topic.py,sha256=IaXhrIHcJ3FSIr0WC-N7u9fkz-lAvSBgQklTX67t0Yc,1641
|
|
15
|
+
kleinkram/user/user.py,sha256=hDrbWeFPPnh2sswDd445SwcIFGyAbfXXWpYq1VqrK0g,1379
|
|
16
|
+
kleinkram-0.33.0.dist-info/METADATA,sha256=nDTs7GkZ_xPVbqSWR4XEd50hDjR7aHt-2MA21aFRDHg,827
|
|
17
|
+
kleinkram-0.33.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
18
|
+
kleinkram-0.33.0.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
19
|
+
kleinkram-0.33.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
20
|
+
kleinkram-0.33.0.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kleinkram/api_client.py,sha256=1GPsM-XFbPYEKP7RfWmzMTwxRqnVh4wtHVuW25KT8kA,2264
|
|
3
|
-
kleinkram/consts.py,sha256=pm_6OuQcO-tYcRhwauTtyRRsuYY0y0yb6EGuIl49LnI,50
|
|
4
|
-
kleinkram/error_handling.py,sha256=JKwQVKLu5VPkhpQbiJsCkhAgdNuzNuDbxlS85qTlvMU,4116
|
|
5
|
-
kleinkram/helper.py,sha256=tBxZj4PzII0YHfHtrDhagKrb4gTdNYa31t4E3DRDK0c,8372
|
|
6
|
-
kleinkram/main.py,sha256=9u_msMlPhphfaL92GNrm-cRUeSHxc_mOFPeXLDzv-ok,9642
|
|
7
|
-
kleinkram/auth/auth.py,sha256=w3-TsxWxURzLQ3_p43zgV4Rlh4dVL_WqI6HG2aes-b4,4991
|
|
8
|
-
kleinkram/endpoint/endpoint.py,sha256=uez5UrAnP7L5rVHUysA9tFkN3dB3dG1Ojt9g3w-UWuQ,1441
|
|
9
|
-
kleinkram/file/file.py,sha256=gLCZDHHgQWq25OmeG-lwkIh4aRZaLK12xxLkbhZ_m-g,5390
|
|
10
|
-
kleinkram/mission/mission.py,sha256=KI_r-DbaXr8uKi9rnSopj-G1N4Nq_ELEBn4mPJXMQzQ,8861
|
|
11
|
-
kleinkram/project/project.py,sha256=yDygz9JJ4Td5VsoCoCLm36HccRyd7jl65Hq05uxEGts,1602
|
|
12
|
-
kleinkram/queue/queue.py,sha256=MaLBjAu8asi9BkPvbbT-5AobCcpy3ex5rxM1kHpRINA,181
|
|
13
|
-
kleinkram/tag/tag.py,sha256=JSHbDPVfsvP34MuQhw__DPQk-Bah5G9BgwYsj_K_JGc,1805
|
|
14
|
-
kleinkram/topic/topic.py,sha256=IaXhrIHcJ3FSIr0WC-N7u9fkz-lAvSBgQklTX67t0Yc,1641
|
|
15
|
-
kleinkram/user/user.py,sha256=hDrbWeFPPnh2sswDd445SwcIFGyAbfXXWpYq1VqrK0g,1379
|
|
16
|
-
kleinkram-0.32.3.dist-info/METADATA,sha256=MHsu3qNlPHz0rDvWNx1vpssVxm6YJrXp1WGCDG4ydfQ,827
|
|
17
|
-
kleinkram-0.32.3.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
18
|
-
kleinkram-0.32.3.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
19
|
-
kleinkram-0.32.3.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
20
|
-
kleinkram-0.32.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|