kleinkram 0.0.116__py3-none-any.whl → 0.0.118__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/auth.py +82 -16
- kleinkram/main.py +106 -38
- {kleinkram-0.0.116.dist-info → kleinkram-0.0.118.dist-info}/METADATA +1 -1
- kleinkram-0.0.118.dist-info/RECORD +10 -0
- kleinkram-0.0.116.dist-info/RECORD +0 -10
- {kleinkram-0.0.116.dist-info → kleinkram-0.0.118.dist-info}/WHEEL +0 -0
- {kleinkram-0.0.116.dist-info → kleinkram-0.0.118.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.0.116.dist-info → kleinkram-0.0.118.dist-info}/licenses/LICENSE +0 -0
kleinkram/auth.py
CHANGED
|
@@ -3,6 +3,8 @@ import os
|
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import webbrowser
|
|
5
5
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
6
8
|
from typing_extensions import Annotated
|
|
7
9
|
|
|
8
10
|
from pathlib import Path
|
|
@@ -72,6 +74,7 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
|
72
74
|
self.end_headers()
|
|
73
75
|
self.wfile.write(b"Authentication successful. You can close this window.")
|
|
74
76
|
return
|
|
77
|
+
print("here")
|
|
75
78
|
|
|
76
79
|
def log_message(self, format, *args):
|
|
77
80
|
pass
|
|
@@ -90,8 +93,8 @@ class AuthenticatedClient(httpx.Client):
|
|
|
90
93
|
try:
|
|
91
94
|
self.tokenfile = TokenFile()
|
|
92
95
|
self._load_cookies()
|
|
93
|
-
except:
|
|
94
|
-
print("
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"{self.tokenfile.endpoint} is not authenticated. Please run 'klein login'.")
|
|
95
98
|
|
|
96
99
|
def _load_cookies(self):
|
|
97
100
|
if self.tokenfile.isCliToken():
|
|
@@ -107,7 +110,7 @@ class AuthenticatedClient(httpx.Client):
|
|
|
107
110
|
if not refresh_token:
|
|
108
111
|
print("No refresh token found. Please login again.")
|
|
109
112
|
raise Exception("No refresh token found.")
|
|
110
|
-
self.cookies.set(REFRESH_TOKEN, refresh_token,)
|
|
113
|
+
self.cookies.set(REFRESH_TOKEN, refresh_token, )
|
|
111
114
|
response = self.post(
|
|
112
115
|
"/auth/refresh-token",
|
|
113
116
|
)
|
|
@@ -122,7 +125,7 @@ class AuthenticatedClient(httpx.Client):
|
|
|
122
125
|
method, self.tokenfile.endpoint + url, *args, **kwargs
|
|
123
126
|
)
|
|
124
127
|
if (
|
|
125
|
-
|
|
128
|
+
url == "/auth/refresh-token"
|
|
126
129
|
) and response.status_code == 401:
|
|
127
130
|
print("Refresh token expired. Please login again.")
|
|
128
131
|
response.status_code = 403
|
|
@@ -137,32 +140,95 @@ class AuthenticatedClient(httpx.Client):
|
|
|
137
140
|
client = AuthenticatedClient()
|
|
138
141
|
|
|
139
142
|
|
|
140
|
-
def login(
|
|
143
|
+
def login(
|
|
144
|
+
key: Optional[str] = typer.Option(None, help="CLI Key", hidden=True),
|
|
145
|
+
open_browser: Optional[bool] = typer.Option(True, help="Open browser for authentication"),
|
|
146
|
+
):
|
|
147
|
+
"""
|
|
148
|
+
Login into the currently set endpoint.\n
|
|
149
|
+
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.
|
|
150
|
+
"""
|
|
141
151
|
tokenfile = TokenFile()
|
|
142
152
|
if key:
|
|
143
153
|
tokenfile.saveTokens(key)
|
|
144
154
|
else:
|
|
145
|
-
|
|
146
|
-
webbrowser.open(tokenfile.endpoint + "/auth/google?state=cli")
|
|
155
|
+
url = tokenfile.endpoint + "/auth/google?state=cli"
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
157
|
+
has_browser = True
|
|
158
|
+
try:
|
|
159
|
+
browser_available = webbrowser.get()
|
|
160
|
+
if not browser_available:
|
|
161
|
+
raise Exception("No web browser available.")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
has_browser = False
|
|
150
164
|
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
if has_browser and open_browser:
|
|
166
|
+
webbrowser.open(url)
|
|
167
|
+
auth_tokens = get_auth_tokens()
|
|
154
168
|
|
|
155
|
-
|
|
169
|
+
if not auth_tokens:
|
|
170
|
+
print("Failed to get authentication tokens.")
|
|
171
|
+
return
|
|
156
172
|
|
|
157
|
-
|
|
173
|
+
tokenfile.saveTokens(auth_tokens)
|
|
174
|
+
print("Authentication complete. Tokens saved to ~/.kleinkram.json.")
|
|
158
175
|
|
|
176
|
+
return
|
|
159
177
|
|
|
160
|
-
|
|
178
|
+
print(f"Please open the following URL manually in your browser to authenticate: {url + '-no-redirect'}")
|
|
179
|
+
print("Enter the authentication token provided after logging in:")
|
|
180
|
+
manual_auth_token = input("Authentication Token: ")
|
|
181
|
+
manual_refresh_token = input("Refresh Token: ")
|
|
182
|
+
if manual_auth_token:
|
|
183
|
+
tokenfile.saveTokens({AUTH_TOKEN: manual_auth_token, REFRESH_TOKEN: manual_refresh_token})
|
|
184
|
+
print("Authentication complete. Tokens saved to tokens.json.")
|
|
185
|
+
else:
|
|
186
|
+
print("No authentication token provided.")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def setEndpoint(
|
|
191
|
+
endpoint: Optional[str] = typer.Argument(None, help="API endpoint to use")
|
|
192
|
+
):
|
|
193
|
+
"""
|
|
194
|
+
Set the current endpoint
|
|
195
|
+
|
|
196
|
+
Use this command to switch between different API endpoints.\n
|
|
197
|
+
Standard endpoints are:\n
|
|
198
|
+
- http://localhost:3000\n
|
|
199
|
+
- https://api.datasets.leggedrobotics.com\n
|
|
200
|
+
- https://api.datasets.dev.leggedrobotics.com
|
|
201
|
+
"""
|
|
161
202
|
tokenfile = TokenFile()
|
|
162
203
|
tokenfile.endpoint = endpoint
|
|
163
204
|
tokenfile.writeToFile()
|
|
205
|
+
print("Endpoint set to: " + endpoint)
|
|
206
|
+
if tokenfile.endpoint not in tokenfile.tokens:
|
|
207
|
+
print("No tokens found for this endpoint.")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def endpoint():
|
|
211
|
+
"""
|
|
212
|
+
Get the current endpoint
|
|
213
|
+
|
|
214
|
+
Also displays all endpoints with saved tokens.
|
|
215
|
+
"""
|
|
216
|
+
tokenfile = TokenFile()
|
|
217
|
+
print("Current: " + tokenfile.endpoint)
|
|
218
|
+
print("Saved Tokens found for:")
|
|
219
|
+
for _endpoint, _ in tokenfile.tokens.items():
|
|
220
|
+
print("- " + _endpoint)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def setCliKey(
|
|
224
|
+
key: Annotated[str, typer.Argument(help="CLI Key")]
|
|
225
|
+
):
|
|
226
|
+
"""
|
|
227
|
+
Set the CLI key (Actions Only)
|
|
164
228
|
|
|
165
|
-
|
|
229
|
+
Same as login with the --key option.
|
|
230
|
+
Should never be used by the user, only in docker containers launched from within Kleinkram.
|
|
231
|
+
"""
|
|
166
232
|
tokenfile = TokenFile()
|
|
167
233
|
if not tokenfile.endpoint in tokenfile.tokens:
|
|
168
234
|
tokenfile.tokens[tokenfile.endpoint] = {}
|
kleinkram/main.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from datetime import datetime, timedelta
|
|
2
3
|
import os
|
|
3
4
|
|
|
@@ -10,17 +11,17 @@ from rich.table import Table
|
|
|
10
11
|
|
|
11
12
|
from .helper import uploadFiles, expand_and_match
|
|
12
13
|
|
|
13
|
-
from .auth import login, client, endpoint, setCliKey
|
|
14
|
+
from .auth import login, client, endpoint, setCliKey, setEndpoint
|
|
14
15
|
|
|
15
16
|
app = typer.Typer()
|
|
16
|
-
projects = typer.Typer(name="projects")
|
|
17
|
-
missions = typer.Typer(name="missions")
|
|
18
|
-
files = typer.Typer(name="files")
|
|
19
|
-
topics = typer.Typer(name="topics")
|
|
20
|
-
queue = typer.Typer(name="queue")
|
|
21
|
-
user = typer.Typer(name="users")
|
|
22
|
-
tagtypes = typer.Typer(name="tagtypes")
|
|
23
|
-
tag = typer.Typer(name="tag")
|
|
17
|
+
projects = typer.Typer(name="projects", help="Project operations")
|
|
18
|
+
missions = typer.Typer(name="missions", help="Mission operations")
|
|
19
|
+
files = typer.Typer(name="files", help="File operations")
|
|
20
|
+
topics = typer.Typer(name="topics", help="Topic operations")
|
|
21
|
+
queue = typer.Typer(name="queue", help="Queue operations")
|
|
22
|
+
user = typer.Typer(name="users", help="User operations")
|
|
23
|
+
tagtypes = typer.Typer(name="tagtypes", help="TagType operations")
|
|
24
|
+
tag = typer.Typer(name="tag", help="Tag operations")
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
app.add_typer(projects)
|
|
@@ -33,27 +34,39 @@ app.add_typer(tagtypes)
|
|
|
33
34
|
app.add_typer(tag)
|
|
34
35
|
app.command()(login)
|
|
35
36
|
app.command()(endpoint)
|
|
36
|
-
app.command()(
|
|
37
|
+
app.command()(setEndpoint)
|
|
38
|
+
app.command(hidden=True)(setCliKey)
|
|
37
39
|
|
|
38
40
|
|
|
39
41
|
@files.command("list")
|
|
40
42
|
def list_files(
|
|
41
|
-
project:
|
|
42
|
-
mission:
|
|
43
|
-
topics:
|
|
43
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
44
|
+
mission: Optional[str] = typer.Option(None, help="Name of Mission"),
|
|
45
|
+
topics: Optional[List[str]] = typer.Option(None, help="Comma separated list of topics")
|
|
44
46
|
):
|
|
45
47
|
"""
|
|
46
48
|
List all files with optional filters for project, mission, or topics.
|
|
49
|
+
|
|
50
|
+
Can list files of a project, mission, or with specific topics (Logical AND).
|
|
51
|
+
Examples:\n
|
|
52
|
+
- 'klein files list'\n
|
|
53
|
+
- 'klein files list --project "Project 1"'\n
|
|
54
|
+
- 'klein files list --mission "Mission 1"'\n
|
|
55
|
+
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw"'\n
|
|
56
|
+
- 'klein files list --topics "/elevation_mapping/semantic_map,/elevation_mapping/elevation_map_raw" --mission "Mission A"'
|
|
47
57
|
"""
|
|
48
58
|
try:
|
|
49
59
|
url = f"/file/filteredByNames"
|
|
60
|
+
params = {}
|
|
61
|
+
if project:
|
|
62
|
+
params["projectName"] = project
|
|
63
|
+
if mission:
|
|
64
|
+
params["missionName"] = mission
|
|
65
|
+
if topics:
|
|
66
|
+
params["topics"] = topics
|
|
50
67
|
response = client.get(
|
|
51
68
|
url,
|
|
52
|
-
params=
|
|
53
|
-
"projectName": project,
|
|
54
|
-
"missionName": mission,
|
|
55
|
-
"topics": topics,
|
|
56
|
-
},
|
|
69
|
+
params=params,
|
|
57
70
|
)
|
|
58
71
|
response.raise_for_status()
|
|
59
72
|
data = response.json()
|
|
@@ -102,11 +115,12 @@ def list_projects():
|
|
|
102
115
|
|
|
103
116
|
@missions.command("list")
|
|
104
117
|
def list_missions(
|
|
105
|
-
project:
|
|
106
|
-
verbose:
|
|
118
|
+
project: Optional[str] = typer.Option(None, help="Name of Project"),
|
|
119
|
+
verbose: Optional[bool] = typer.Option(False, help="Outputs a table with more information"),
|
|
107
120
|
):
|
|
108
121
|
"""
|
|
109
122
|
List all missions with optional filter for project.
|
|
123
|
+
|
|
110
124
|
"""
|
|
111
125
|
try:
|
|
112
126
|
url = "/mission"
|
|
@@ -150,26 +164,47 @@ def list_missions(
|
|
|
150
164
|
@missions.command("byUUID")
|
|
151
165
|
def mission_by_uuid(
|
|
152
166
|
uuid: Annotated[str, typer.Argument()],
|
|
167
|
+
json: Optional[bool] = typer.Option(False, help="Output as JSON"),
|
|
153
168
|
):
|
|
169
|
+
"""
|
|
170
|
+
Get mission name, project name, creator and table of its files given a Mission UUID
|
|
171
|
+
|
|
172
|
+
Use the JSON flag to output the full JSON response instead.
|
|
173
|
+
|
|
174
|
+
Can be run with API Key or with login.
|
|
175
|
+
"""
|
|
154
176
|
try:
|
|
155
177
|
url = "/mission/byUUID"
|
|
156
178
|
response = client.get(url, params={"uuid": uuid})
|
|
157
179
|
response.raise_for_status()
|
|
158
180
|
data = response.json()
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
if json:
|
|
182
|
+
print(data)
|
|
183
|
+
else:
|
|
184
|
+
print(f"mission: {data['name']}")
|
|
185
|
+
print(f"Creator: {data['creator']['name']}")
|
|
186
|
+
print("Project: " + data["project"]["name"])
|
|
187
|
+
table = Table("Filename", "Size", "date")
|
|
188
|
+
for file in data["files"]:
|
|
189
|
+
table.add_row(file["filename"], f"{file['size']}", file["date"])
|
|
190
|
+
print(table)
|
|
164
191
|
except httpx.HTTPError as e:
|
|
165
192
|
print(f"Failed to fetch missions: {e}")
|
|
166
193
|
|
|
167
194
|
|
|
168
195
|
@topics.command("list")
|
|
169
196
|
def topics(
|
|
170
|
-
file: Annotated[str, typer.Option()]
|
|
171
|
-
full: Annotated[bool, typer.Option()] = False,
|
|
197
|
+
file: Annotated[str, typer.Option(help="Name of File")],
|
|
198
|
+
full: Annotated[bool, typer.Option(help="As a table with additional parameters")] = False,
|
|
199
|
+
# Todo add mission / project as optional argument as filenames are not unique
|
|
172
200
|
):
|
|
201
|
+
"""
|
|
202
|
+
List topics for a file
|
|
203
|
+
|
|
204
|
+
Only makes sense with MCAP files as we don't associate topics with BAGs.
|
|
205
|
+
"""
|
|
206
|
+
if file.endswith(".bag"):
|
|
207
|
+
print("BAG files generally do not have topics")
|
|
173
208
|
try:
|
|
174
209
|
url = "/file/byName"
|
|
175
210
|
response = client.get(url, params={"name": file})
|
|
@@ -195,11 +230,19 @@ def topics(
|
|
|
195
230
|
|
|
196
231
|
|
|
197
232
|
@projects.command("create")
|
|
198
|
-
def create_project(
|
|
233
|
+
def create_project(
|
|
234
|
+
name: Annotated[str, typer.Option(help="Name of Project")],
|
|
235
|
+
description: Annotated[str, typer.Option(help="Description of Project")],
|
|
236
|
+
):
|
|
237
|
+
"""
|
|
238
|
+
Create a new project
|
|
239
|
+
"""
|
|
199
240
|
try:
|
|
200
241
|
url = "/project/create"
|
|
201
|
-
response = client.post(url, json={"name": name})
|
|
202
|
-
response.
|
|
242
|
+
response = client.post(url, json={"name": name, "description": description, "requiredTags": []}) # TODO: Add required tags as option
|
|
243
|
+
if response.status_code >= 400:
|
|
244
|
+
print(f"Failed to create project: {response.json()["message"]}")
|
|
245
|
+
return
|
|
203
246
|
print("Project created")
|
|
204
247
|
|
|
205
248
|
except httpx.HTTPError as e:
|
|
@@ -208,12 +251,23 @@ def create_project(name: Annotated[str, typer.Option()]):
|
|
|
208
251
|
|
|
209
252
|
@app.command("upload")
|
|
210
253
|
def upload(
|
|
211
|
-
path: Annotated[str, typer.Option(prompt=True)],
|
|
212
|
-
project: Annotated[str, typer.Option(prompt=True)],
|
|
213
|
-
mission: Annotated[str, typer.Option(prompt=True)],
|
|
254
|
+
path: Annotated[str, typer.Option(prompt=True, help="Path to files to upload, Regex supported")],
|
|
255
|
+
project: Annotated[str, typer.Option(prompt=True, help="Name of Project")],
|
|
256
|
+
mission: Annotated[str, typer.Option(prompt=True, help="Name of Mission to create")],
|
|
214
257
|
):
|
|
258
|
+
"""
|
|
259
|
+
Upload files matching the path to a mission in a project.
|
|
260
|
+
|
|
261
|
+
The mission name must be unique within the project and not yet created.\n
|
|
262
|
+
Examples:\n
|
|
263
|
+
- 'klein upload --path "~/data/**/*.bag" --project "Project 1" --mission "Mission 1"'\n
|
|
264
|
+
|
|
265
|
+
"""
|
|
215
266
|
files = expand_and_match(path)
|
|
216
267
|
filenames = list(map(lambda x: x.split("/")[-1], filter(lambda x: not os.path.isdir(x),files)))
|
|
268
|
+
if not filenames:
|
|
269
|
+
print("No files found")
|
|
270
|
+
return
|
|
217
271
|
filepaths = {}
|
|
218
272
|
for path in files:
|
|
219
273
|
if not os.path.isdir(path):
|
|
@@ -245,7 +299,7 @@ def upload(
|
|
|
245
299
|
|
|
246
300
|
create_mission_url = "/mission/create"
|
|
247
301
|
new_mission = client.post(
|
|
248
|
-
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"]}
|
|
302
|
+
create_mission_url, json={"name": mission, "projectUUID": project_json["uuid"], "tags": []}
|
|
249
303
|
)
|
|
250
304
|
new_mission.raise_for_status()
|
|
251
305
|
new_mission_data = new_mission.json()
|
|
@@ -320,7 +374,7 @@ def clear_queue():
|
|
|
320
374
|
print("Operation cancelled.")
|
|
321
375
|
|
|
322
376
|
|
|
323
|
-
@app.command("wipe")
|
|
377
|
+
@app.command("wipe", hidden=True)
|
|
324
378
|
def wipe():
|
|
325
379
|
"""Wipe all data"""
|
|
326
380
|
# Prompt the user for confirmation
|
|
@@ -360,8 +414,13 @@ def wipe():
|
|
|
360
414
|
print("Operation cancelled.")
|
|
361
415
|
|
|
362
416
|
|
|
363
|
-
@app.command("claim")
|
|
417
|
+
@app.command("claim", hidden=True)
|
|
364
418
|
def claim():
|
|
419
|
+
"""
|
|
420
|
+
Claim admin rights as the first user
|
|
421
|
+
|
|
422
|
+
Only works if no other user has claimed admin rights before.
|
|
423
|
+
"""
|
|
365
424
|
response = client.post("/user/claimAdmin")
|
|
366
425
|
response.raise_for_status()
|
|
367
426
|
print("Admin claimed.")
|
|
@@ -369,6 +428,7 @@ def claim():
|
|
|
369
428
|
|
|
370
429
|
@user.command("list")
|
|
371
430
|
def users():
|
|
431
|
+
"""List all users"""
|
|
372
432
|
response = client.get("/user/all")
|
|
373
433
|
response.raise_for_status()
|
|
374
434
|
data = response.json()
|
|
@@ -380,6 +440,7 @@ def users():
|
|
|
380
440
|
|
|
381
441
|
@user.command("info")
|
|
382
442
|
def user_info():
|
|
443
|
+
"""Get logged in user info"""
|
|
383
444
|
response = client.get("/user/me")
|
|
384
445
|
response.raise_for_status()
|
|
385
446
|
data = response.json()
|
|
@@ -388,6 +449,7 @@ def user_info():
|
|
|
388
449
|
|
|
389
450
|
@user.command("promote")
|
|
390
451
|
def promote(email: Annotated[str, typer.Option()]):
|
|
452
|
+
"""Promote another user to admin"""
|
|
391
453
|
response = client.post("/user/promote", json={"email": email})
|
|
392
454
|
response.raise_for_status()
|
|
393
455
|
print("User promoted.")
|
|
@@ -395,6 +457,7 @@ def promote(email: Annotated[str, typer.Option()]):
|
|
|
395
457
|
|
|
396
458
|
@user.command("demote")
|
|
397
459
|
def demote(email: Annotated[str, typer.Option()]):
|
|
460
|
+
"""Demote another user from admin"""
|
|
398
461
|
response = client.post("/user/demote", json={"email": email})
|
|
399
462
|
response.raise_for_status()
|
|
400
463
|
print("User demoted.")
|
|
@@ -404,6 +467,7 @@ def demote(email: Annotated[str, typer.Option()]):
|
|
|
404
467
|
def download(
|
|
405
468
|
missionuuid: Annotated[str, typer.Argument()],
|
|
406
469
|
):
|
|
470
|
+
"""Download file"""
|
|
407
471
|
try:
|
|
408
472
|
response = client.get("/file/downloadWithToken", params={"uuid": missionuuid})
|
|
409
473
|
response.raise_for_status()
|
|
@@ -417,6 +481,7 @@ def addTag(
|
|
|
417
481
|
tagtypeuuid: Annotated[str, typer.Argument()],
|
|
418
482
|
value: Annotated[str, typer.Argument()],
|
|
419
483
|
):
|
|
484
|
+
"""Tag a mission"""
|
|
420
485
|
try:
|
|
421
486
|
response = client.post("/tag/addTag", json={"mission": missionuuid, "tagType": tagtypeuuid, "value": value})
|
|
422
487
|
if response.status_code < 400:
|
|
@@ -426,13 +491,15 @@ def addTag(
|
|
|
426
491
|
print("Failed to tag mission")
|
|
427
492
|
except httpx.HTTPError as e:
|
|
428
493
|
print(e)
|
|
429
|
-
print("Failed to tag mission"
|
|
430
|
-
)
|
|
494
|
+
print("Failed to tag mission")
|
|
495
|
+
sys.exit(1)
|
|
496
|
+
|
|
431
497
|
|
|
432
498
|
@tagtypes.command('list')
|
|
433
499
|
def tagTypes(
|
|
434
500
|
verbose: Annotated[bool, typer.Option()] = False,
|
|
435
501
|
):
|
|
502
|
+
"""List all tagtypes"""
|
|
436
503
|
try:
|
|
437
504
|
response = client.get("/tag/all")
|
|
438
505
|
response.raise_for_status()
|
|
@@ -453,6 +520,7 @@ def tagTypes(
|
|
|
453
520
|
def deleteTag(
|
|
454
521
|
taguuid: Annotated[str, typer.Argument()],
|
|
455
522
|
):
|
|
523
|
+
"""Delete a tag"""
|
|
456
524
|
try:
|
|
457
525
|
response = client.delete("/tag/deleteTag", params={"uuid": taguuid})
|
|
458
526
|
if response.status_code < 400:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
kleinkram/auth.py,sha256=s6Lek1vLRs2cXSwfGmEWuuzmszMhaTAJbh9icxWPSkQ,7628
|
|
3
|
+
kleinkram/consts.py,sha256=8kCOeSdMqEyB89hT55w5yd1fksUwbj52a2nYkZTZI_0,88
|
|
4
|
+
kleinkram/helper.py,sha256=mP6AohHWU8kckAi1Oevgs0gfGPH1Qzkp0f7jiQF9u5I,2198
|
|
5
|
+
kleinkram/main.py,sha256=O1VOT8YFOc6YcyLhd723iOScvO1ILUG2LW1ggkUZVJY,18071
|
|
6
|
+
kleinkram-0.0.118.dist-info/METADATA,sha256=8eKAgwE0t4W6ecNaQ4r3eOnzgbCa-6ArE_aetX_otHk,733
|
|
7
|
+
kleinkram-0.0.118.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
+
kleinkram-0.0.118.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
9
|
+
kleinkram-0.0.118.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
10
|
+
kleinkram-0.0.118.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
kleinkram/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
kleinkram/auth.py,sha256=0pu0RQQrtTFoW07Rgni7FAMJS3e0BZ9Pz9QrFPsbfxg,5343
|
|
3
|
-
kleinkram/consts.py,sha256=8kCOeSdMqEyB89hT55w5yd1fksUwbj52a2nYkZTZI_0,88
|
|
4
|
-
kleinkram/helper.py,sha256=mP6AohHWU8kckAi1Oevgs0gfGPH1Qzkp0f7jiQF9u5I,2198
|
|
5
|
-
kleinkram/main.py,sha256=dS0gOy_KRqrzbX5xynROi6cnbDb_9Aww2ER4U4bpfU4,15106
|
|
6
|
-
kleinkram-0.0.116.dist-info/METADATA,sha256=ANiDjnZ0f-h4P6d6pUmUaeBDOroCDfojKMpDL_3d5Yk,733
|
|
7
|
-
kleinkram-0.0.116.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
-
kleinkram-0.0.116.dist-info/entry_points.txt,sha256=RHXtRzcreVHImatgjhQwZQ6GdJThElYjHEWcR1BPXUI,45
|
|
9
|
-
kleinkram-0.0.116.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
10
|
-
kleinkram-0.0.116.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|