kleinkram 0.38.1.dev20241120100707__py3-none-any.whl → 0.38.1.dev20241212075157__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/api/client.py +31 -23
- kleinkram/api/file_transfer.py +323 -203
- kleinkram/api/parsing.py +86 -0
- kleinkram/api/routes.py +77 -311
- kleinkram/app.py +20 -54
- kleinkram/auth.py +0 -2
- kleinkram/commands/download.py +60 -60
- kleinkram/commands/list.py +53 -48
- kleinkram/commands/mission.py +25 -13
- kleinkram/commands/upload.py +79 -53
- kleinkram/commands/verify.py +58 -35
- kleinkram/config.py +2 -3
- kleinkram/errors.py +48 -31
- kleinkram/models.py +2 -2
- kleinkram/resources.py +158 -0
- kleinkram/utils.py +16 -47
- {kleinkram-0.38.1.dev20241120100707.dist-info → kleinkram-0.38.1.dev20241212075157.dist-info}/METADATA +5 -3
- kleinkram-0.38.1.dev20241212075157.dist-info/RECORD +37 -0
- {kleinkram-0.38.1.dev20241120100707.dist-info → kleinkram-0.38.1.dev20241212075157.dist-info}/WHEEL +1 -1
- tests/test_end_to_end.py +105 -0
- tests/test_resources.py +137 -0
- tests/test_utils.py +13 -59
- kleinkram-0.38.1.dev20241120100707.dist-info/RECORD +0 -33
- {kleinkram-0.38.1.dev20241120100707.dist-info → kleinkram-0.38.1.dev20241212075157.dist-info}/LICENSE +0 -0
- {kleinkram-0.38.1.dev20241120100707.dist-info → kleinkram-0.38.1.dev20241212075157.dist-info}/entry_points.txt +0 -0
- {kleinkram-0.38.1.dev20241120100707.dist-info → kleinkram-0.38.1.dev20241212075157.dist-info}/top_level.txt +0 -0
kleinkram/api/parsing.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from kleinkram.errors import ParsingError
|
|
9
|
+
from kleinkram.models import File
|
|
10
|
+
from kleinkram.models import FileState
|
|
11
|
+
from kleinkram.models import Mission
|
|
12
|
+
from kleinkram.models import Project
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"_parse_project",
|
|
16
|
+
"_parse_mission",
|
|
17
|
+
"_parse_file",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_project(project: Dict[str, Any]) -> Project:
|
|
22
|
+
try:
|
|
23
|
+
project_id = UUID(project["uuid"], version=4)
|
|
24
|
+
project_name = project["name"]
|
|
25
|
+
project_description = project["description"]
|
|
26
|
+
|
|
27
|
+
parsed = Project(
|
|
28
|
+
id=project_id, name=project_name, description=project_description
|
|
29
|
+
)
|
|
30
|
+
except Exception:
|
|
31
|
+
raise ParsingError(f"error parsing project: {project}")
|
|
32
|
+
return parsed
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_mission(
|
|
36
|
+
mission: Dict[str, Any], project: Optional[Project] = None
|
|
37
|
+
) -> Mission:
|
|
38
|
+
try:
|
|
39
|
+
mission_id = UUID(mission["uuid"], version=4)
|
|
40
|
+
mission_name = mission["name"]
|
|
41
|
+
|
|
42
|
+
project_id = (
|
|
43
|
+
project.id if project else UUID(mission["project"]["uuid"], version=4)
|
|
44
|
+
)
|
|
45
|
+
project_name = project.name if project else mission["project"]["name"]
|
|
46
|
+
|
|
47
|
+
parsed = Mission(
|
|
48
|
+
id=mission_id,
|
|
49
|
+
name=mission_name,
|
|
50
|
+
project_id=project_id,
|
|
51
|
+
project_name=project_name,
|
|
52
|
+
)
|
|
53
|
+
except Exception:
|
|
54
|
+
raise ParsingError(f"error parsing mission: {mission}")
|
|
55
|
+
return parsed
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _parse_file(file: Dict[str, Any], mission: Optional[Mission] = None) -> File:
|
|
59
|
+
try:
|
|
60
|
+
filename = file["filename"]
|
|
61
|
+
file_id = UUID(file["uuid"], version=4)
|
|
62
|
+
file_size = file["size"]
|
|
63
|
+
file_hash = file["hash"]
|
|
64
|
+
|
|
65
|
+
project_id = (
|
|
66
|
+
mission.project_id if mission else UUID(file["project"]["uuid"], version=4)
|
|
67
|
+
)
|
|
68
|
+
project_name = mission.project_name if mission else file["project"]["name"]
|
|
69
|
+
|
|
70
|
+
mission_id = mission.id if mission else UUID(file["mission"]["uuid"], version=4)
|
|
71
|
+
mission_name = mission.name if mission else file["mission"]["name"]
|
|
72
|
+
|
|
73
|
+
parsed = File(
|
|
74
|
+
id=file_id,
|
|
75
|
+
name=filename,
|
|
76
|
+
size=file_size,
|
|
77
|
+
hash=file_hash,
|
|
78
|
+
project_id=project_id,
|
|
79
|
+
project_name=project_name,
|
|
80
|
+
mission_id=mission_id,
|
|
81
|
+
mission_name=mission_name,
|
|
82
|
+
state=FileState(file["state"]),
|
|
83
|
+
)
|
|
84
|
+
except Exception:
|
|
85
|
+
raise ParsingError(f"error parsing file: {file}")
|
|
86
|
+
return parsed
|
kleinkram/api/routes.py
CHANGED
|
@@ -1,209 +1,129 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import logging
|
|
4
|
-
from typing import Any
|
|
5
|
-
from typing import cast
|
|
6
3
|
from typing import Dict
|
|
7
4
|
from typing import List
|
|
8
5
|
from typing import Optional
|
|
9
6
|
from typing import Tuple
|
|
10
|
-
from typing import Union
|
|
11
7
|
from uuid import UUID
|
|
12
8
|
|
|
13
9
|
import httpx
|
|
14
10
|
from kleinkram.api.client import AuthenticatedClient
|
|
11
|
+
from kleinkram.api.parsing import _parse_file
|
|
12
|
+
from kleinkram.api.parsing import _parse_mission
|
|
13
|
+
from kleinkram.api.parsing import _parse_project
|
|
15
14
|
from kleinkram.config import Config
|
|
16
|
-
from kleinkram.errors import
|
|
15
|
+
from kleinkram.errors import AccessDenied
|
|
17
16
|
from kleinkram.errors import MissionExists
|
|
18
|
-
from kleinkram.errors import
|
|
17
|
+
from kleinkram.errors import MissionNotFound
|
|
19
18
|
from kleinkram.models import DataType
|
|
20
19
|
from kleinkram.models import File
|
|
21
|
-
from kleinkram.models import FilesById
|
|
22
|
-
from kleinkram.models import FilesByMission
|
|
23
|
-
from kleinkram.models import FileState
|
|
24
20
|
from kleinkram.models import Mission
|
|
25
|
-
from kleinkram.models import MissionById
|
|
26
|
-
from kleinkram.models import MissionByName
|
|
27
21
|
from kleinkram.models import Project
|
|
28
22
|
from kleinkram.models import TagType
|
|
29
|
-
from kleinkram.utils import filtered_by_patterns
|
|
30
23
|
from kleinkram.utils import is_valid_uuid4
|
|
31
24
|
|
|
32
|
-
|
|
25
|
+
__all__ = [
|
|
26
|
+
"_get_projects",
|
|
27
|
+
"_get_missions_by_project",
|
|
28
|
+
"_get_files_by_mission",
|
|
29
|
+
"_create_mission",
|
|
30
|
+
"_update_mission_metadata",
|
|
31
|
+
"_get_api_version",
|
|
32
|
+
"_claim_admin",
|
|
33
|
+
]
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
MAX_PAGINATION = 10_000
|
|
36
37
|
|
|
37
|
-
TEMP_CREDS = "/file/temporaryAccess"
|
|
38
38
|
CLAIM_ADMIN = "/user/claimAdmin"
|
|
39
|
-
|
|
40
|
-
PROJECT_BY_NAME = "/project/byName"
|
|
41
|
-
PROJECT_BY_ID = "/project/one"
|
|
42
|
-
PROJECT_CREATE = "/project/create"
|
|
43
39
|
PROJECT_ALL = "/project/filtered"
|
|
44
|
-
|
|
40
|
+
MISSIONS_BY_PROJECT = "/mission/filtered"
|
|
45
41
|
MISSION_BY_NAME = "/mission/byName"
|
|
46
|
-
MISSION_BY_ID = "/mission/one"
|
|
47
42
|
MISSION_CREATE = "/mission/create"
|
|
48
|
-
MISSION_BY_PROJECT_NAME = "/mission/filteredByProjectName"
|
|
49
43
|
MISSION_UPDATE_METADATA = "/mission/tags"
|
|
50
|
-
|
|
51
|
-
ALL_USERS = "/user/all"
|
|
52
|
-
USER_INFO = "/user/me"
|
|
53
|
-
PROMOTE_USER = "/user/promote"
|
|
54
|
-
DEMOTE_USER = "/user/demote"
|
|
55
|
-
|
|
56
|
-
FILE_DOWNLOAD = "/file/download"
|
|
57
|
-
FILE_QUERY = "/file/filteredByNames"
|
|
58
|
-
FILE_ONE = "/file/one"
|
|
59
44
|
FILE_OF_MISSION = "/file/ofMission"
|
|
60
|
-
|
|
61
45
|
TAG_TYPE_BY_NAME = "/tag/filtered"
|
|
62
|
-
|
|
63
46
|
GET_STATUS = "/user/me"
|
|
64
47
|
|
|
65
48
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
the first user on the system could call this
|
|
69
|
-
"""
|
|
70
|
-
response = client.post(CLAIM_ADMIN)
|
|
71
|
-
response.raise_for_status()
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_project(
|
|
76
|
-
client: AuthenticatedClient, identifier: Union[str, UUID]
|
|
77
|
-
) -> Union[tuple[UUID, Dict[str, Any]], tuple[None, None]]:
|
|
78
|
-
|
|
79
|
-
if isinstance(identifier, UUID):
|
|
80
|
-
params = {"uuid": str(identifier)}
|
|
81
|
-
else:
|
|
82
|
-
params = {"name": identifier}
|
|
83
|
-
|
|
84
|
-
resp = client.get("/missions", params=params)
|
|
49
|
+
def _get_projects(client: AuthenticatedClient) -> list[Project]:
|
|
50
|
+
resp = client.get(PROJECT_ALL)
|
|
85
51
|
|
|
86
52
|
if resp.status_code in (403, 404):
|
|
87
|
-
return
|
|
53
|
+
return []
|
|
88
54
|
|
|
89
|
-
# TODO: handle other status codes
|
|
90
55
|
resp.raise_for_status()
|
|
91
56
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def create_project(
|
|
97
|
-
client: AuthenticatedClient,
|
|
98
|
-
project_name: str,
|
|
99
|
-
*,
|
|
100
|
-
description: str | None = None,
|
|
101
|
-
check_exists: bool = False,
|
|
102
|
-
) -> UUID:
|
|
103
|
-
"""\
|
|
104
|
-
creates a new mission with the given name and project_id
|
|
105
|
-
|
|
106
|
-
if check_exists is True, the function will return the existing mission_id,
|
|
107
|
-
otherwise if the mission already exists an error will be raised
|
|
108
|
-
"""
|
|
109
|
-
if description is None:
|
|
110
|
-
description = "autogenerated by CLI"
|
|
111
|
-
|
|
112
|
-
if check_exists:
|
|
113
|
-
project_id, _ = get_project(client, project_name)
|
|
114
|
-
if project_id is not None:
|
|
115
|
-
return project_id
|
|
116
|
-
|
|
117
|
-
if is_valid_uuid4(project_name):
|
|
118
|
-
raise ValueError(
|
|
119
|
-
f"Project name: `{project_name}` is a valid UUIDv4, "
|
|
120
|
-
"project names must not be valid UUIDv4's"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
resp = client.post(
|
|
124
|
-
MISSION_CREATE,
|
|
125
|
-
json={
|
|
126
|
-
"name": project_name,
|
|
127
|
-
"description": description,
|
|
128
|
-
"requiredTags": [],
|
|
129
|
-
},
|
|
130
|
-
)
|
|
57
|
+
ret = []
|
|
58
|
+
for pr in resp.json()[0]:
|
|
59
|
+
ret.append(_parse_project(pr))
|
|
131
60
|
|
|
132
|
-
|
|
133
|
-
raise ValueError(
|
|
134
|
-
f"Failed to create project. Status Code: "
|
|
135
|
-
f"{str(resp.status_code)}\n"
|
|
136
|
-
f"{resp.json()['message'][0]}"
|
|
137
|
-
)
|
|
61
|
+
return ret
|
|
138
62
|
|
|
139
|
-
return UUID(resp.json()["uuid"], version=4)
|
|
140
63
|
|
|
64
|
+
def _get_missions_by_project(
|
|
65
|
+
client: AuthenticatedClient, project: Project
|
|
66
|
+
) -> List[Mission]:
|
|
67
|
+
params = {"uuid": str(project.id), "take": MAX_PAGINATION}
|
|
141
68
|
|
|
142
|
-
|
|
143
|
-
client: AuthenticatedClient, mission_name, project_id: UUID
|
|
144
|
-
) -> Optional[UUID]:
|
|
145
|
-
params = {"name": mission_name, "projectUUID": str(project_id)}
|
|
146
|
-
resp = client.get(MISSION_BY_NAME, params=params)
|
|
69
|
+
resp = client.get(MISSIONS_BY_PROJECT, params=params)
|
|
147
70
|
|
|
148
71
|
if resp.status_code in (403, 404):
|
|
149
|
-
return
|
|
72
|
+
return []
|
|
150
73
|
|
|
151
|
-
# TODO: handle other status codes
|
|
152
74
|
resp.raise_for_status()
|
|
153
75
|
|
|
154
76
|
data = resp.json()
|
|
77
|
+
missions = []
|
|
78
|
+
|
|
79
|
+
for mission in data[0]:
|
|
80
|
+
missions.append(_parse_mission(mission, project))
|
|
81
|
+
|
|
82
|
+
return missions
|
|
155
83
|
|
|
156
|
-
return UUID(data["uuid"], version=4)
|
|
157
84
|
|
|
85
|
+
def _get_files_by_mission(client: AuthenticatedClient, mission: Mission) -> List[File]:
|
|
86
|
+
params = {"uuid": str(mission.id), "take": MAX_PAGINATION}
|
|
158
87
|
|
|
159
|
-
def get_mission_by_id(
|
|
160
|
-
client: AuthenticatedClient, mission_id: UUID
|
|
161
|
-
) -> Optional[Mission]:
|
|
162
|
-
params = {"uuid": str(mission_id), "take": MAX_PAGINATION}
|
|
163
88
|
resp = client.get(FILE_OF_MISSION, params=params)
|
|
164
89
|
|
|
165
90
|
if resp.status_code in (403, 404):
|
|
166
|
-
return
|
|
91
|
+
return []
|
|
167
92
|
|
|
168
93
|
resp.raise_for_status()
|
|
169
|
-
data = resp.json()[0]
|
|
170
|
-
files = [_parse_file(file) for file in data]
|
|
171
94
|
|
|
172
|
-
|
|
173
|
-
resp.raise_for_status()
|
|
95
|
+
data = resp.json()
|
|
174
96
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
name=mission_data["name"],
|
|
179
|
-
project_id=UUID(mission_data["project"]["uuid"], version=4),
|
|
180
|
-
project_name=mission_data["project"]["name"],
|
|
181
|
-
files=files,
|
|
182
|
-
)
|
|
97
|
+
files = []
|
|
98
|
+
for file in data[0]:
|
|
99
|
+
files.append(_parse_file(file, mission))
|
|
183
100
|
|
|
184
|
-
return
|
|
101
|
+
return files
|
|
185
102
|
|
|
186
103
|
|
|
187
|
-
def
|
|
188
|
-
client: AuthenticatedClient,
|
|
104
|
+
def _get_mission_id_by_name(
|
|
105
|
+
client: AuthenticatedClient, mission_name, project_id: UUID
|
|
189
106
|
) -> Optional[UUID]:
|
|
190
|
-
params = {"name":
|
|
191
|
-
resp = client.get(
|
|
107
|
+
params = {"name": mission_name, "projectUUID": str(project_id)}
|
|
108
|
+
resp = client.get(MISSION_BY_NAME, params=params)
|
|
192
109
|
|
|
193
110
|
if resp.status_code in (403, 404):
|
|
194
111
|
return None
|
|
195
112
|
|
|
113
|
+
# TODO: handle other status codes
|
|
196
114
|
resp.raise_for_status()
|
|
197
115
|
|
|
198
|
-
|
|
116
|
+
data = resp.json()
|
|
117
|
+
|
|
118
|
+
return UUID(data["uuid"], version=4)
|
|
199
119
|
|
|
200
120
|
|
|
201
|
-
def
|
|
121
|
+
def _create_mission(
|
|
202
122
|
client: AuthenticatedClient,
|
|
203
123
|
project_id: UUID,
|
|
204
124
|
mission_name: str,
|
|
205
125
|
*,
|
|
206
|
-
|
|
126
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
207
127
|
ignore_missing_tags: bool = False,
|
|
208
128
|
) -> UUID:
|
|
209
129
|
"""\
|
|
@@ -212,8 +132,10 @@ def create_mission(
|
|
|
212
132
|
if check_exists is True, the function will return the existing mission_id,
|
|
213
133
|
otherwise if the mission already exists an error will be raised
|
|
214
134
|
"""
|
|
135
|
+
if metadata is None:
|
|
136
|
+
metadata = {}
|
|
215
137
|
|
|
216
|
-
if
|
|
138
|
+
if _get_mission_id_by_name(client, mission_name, project_id) is not None:
|
|
217
139
|
raise MissionExists(f"Mission with name: `{mission_name}` already exists")
|
|
218
140
|
|
|
219
141
|
if is_valid_uuid4(mission_name):
|
|
@@ -222,10 +144,13 @@ def create_mission(
|
|
|
222
144
|
"mission names must not be valid UUIDv4's"
|
|
223
145
|
)
|
|
224
146
|
|
|
147
|
+
# we need to translate tag keys to tag type ids
|
|
148
|
+
tags = _get_tags_map(client, metadata)
|
|
149
|
+
|
|
225
150
|
payload = {
|
|
226
151
|
"name": mission_name,
|
|
227
152
|
"projectUUID": str(project_id),
|
|
228
|
-
"tags": {str(k): v for k, v in tags.items()}
|
|
153
|
+
"tags": {str(k): v for k, v in tags.items()},
|
|
229
154
|
"ignoreTags": ignore_missing_tags,
|
|
230
155
|
}
|
|
231
156
|
|
|
@@ -235,175 +160,7 @@ def create_mission(
|
|
|
235
160
|
return UUID(resp.json()["uuid"], version=4)
|
|
236
161
|
|
|
237
162
|
|
|
238
|
-
def
|
|
239
|
-
"""\
|
|
240
|
-
we need this to check if a user has the permissions to
|
|
241
|
-
create a mission in an existing project
|
|
242
|
-
"""
|
|
243
|
-
|
|
244
|
-
resp = client.get("/user/permissions")
|
|
245
|
-
resp.raise_for_status()
|
|
246
|
-
|
|
247
|
-
project_group: List[Dict[str, Union[str, int]]] = resp.json().get("projects", [])
|
|
248
|
-
filtered_by_id = filter(lambda x: x.get("uuid") == str(project_id), project_group)
|
|
249
|
-
|
|
250
|
-
# it is possilbe that a user has access to a project via multiple groups
|
|
251
|
-
# in this case we take the highest permission level
|
|
252
|
-
return cast(int, max(map(lambda x: x.get("access", 0), filtered_by_id)))
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def _parse_file(file: Dict[str, Any]) -> File:
|
|
256
|
-
project_id = UUID(file["mission"]["project"]["uuid"], version=4)
|
|
257
|
-
project_name = file["mission"]["project"]["name"]
|
|
258
|
-
|
|
259
|
-
mission_id = UUID(file["mission"]["uuid"], version=4)
|
|
260
|
-
mission_name = file["mission"]["name"]
|
|
261
|
-
|
|
262
|
-
filename = file["filename"]
|
|
263
|
-
file_id = UUID(file["uuid"], version=4)
|
|
264
|
-
file_size = file["size"]
|
|
265
|
-
file_hash = file["hash"]
|
|
266
|
-
|
|
267
|
-
parsed = File(
|
|
268
|
-
id=file_id,
|
|
269
|
-
name=filename,
|
|
270
|
-
size=file_size,
|
|
271
|
-
hash=file_hash,
|
|
272
|
-
project_id=project_id,
|
|
273
|
-
project_name=project_name,
|
|
274
|
-
mission_id=mission_id,
|
|
275
|
-
mission_name=mission_name,
|
|
276
|
-
state=FileState(file["state"]),
|
|
277
|
-
)
|
|
278
|
-
return parsed
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
def get_file(client: AuthenticatedClient, id: UUID) -> File:
|
|
282
|
-
resp = client.get(FILE_ONE, params={"uuid": str(id)})
|
|
283
|
-
resp.raise_for_status()
|
|
284
|
-
|
|
285
|
-
return _parse_file(resp.json())
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def get_files(
|
|
289
|
-
client: AuthenticatedClient,
|
|
290
|
-
name: Optional[str] = None,
|
|
291
|
-
project: Optional[str] = None,
|
|
292
|
-
mission: Optional[str] = None,
|
|
293
|
-
topics: Optional[List[str]] = None,
|
|
294
|
-
tags: Optional[Dict[str, str]] = None,
|
|
295
|
-
) -> List[File]:
|
|
296
|
-
# TODO: allow to search by id
|
|
297
|
-
|
|
298
|
-
params: Dict[str, Any] = {"take": MAX_PAGINATION}
|
|
299
|
-
if name is not None:
|
|
300
|
-
params["name"] = name
|
|
301
|
-
if project is not None:
|
|
302
|
-
params["projectName"] = project
|
|
303
|
-
if mission is not None:
|
|
304
|
-
params["missionName"] = mission
|
|
305
|
-
if topics:
|
|
306
|
-
params["topics"] = ",".join(topics)
|
|
307
|
-
if tags:
|
|
308
|
-
params["tags"] = tags
|
|
309
|
-
|
|
310
|
-
resp = client.get(FILE_QUERY, params=params)
|
|
311
|
-
resp.raise_for_status()
|
|
312
|
-
|
|
313
|
-
files = []
|
|
314
|
-
data = resp.json()
|
|
315
|
-
|
|
316
|
-
for file in data:
|
|
317
|
-
try:
|
|
318
|
-
parsed = _parse_file(file)
|
|
319
|
-
files.append(parsed)
|
|
320
|
-
except Exception:
|
|
321
|
-
print(f"Error parsing file: {file}")
|
|
322
|
-
return files
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def get_missions(
|
|
326
|
-
client: AuthenticatedClient,
|
|
327
|
-
project: Optional[str] = None,
|
|
328
|
-
tags: Optional[Dict[str, str]] = None,
|
|
329
|
-
) -> list[Mission]:
|
|
330
|
-
# TODO: use a better endpoint once this exists
|
|
331
|
-
matching_files = get_files(client, project=project, tags=tags)
|
|
332
|
-
|
|
333
|
-
ret = {}
|
|
334
|
-
for file in matching_files:
|
|
335
|
-
ret[file.mission_id] = Mission(
|
|
336
|
-
id=file.mission_id,
|
|
337
|
-
name=file.mission_name,
|
|
338
|
-
project_id=file.project_id,
|
|
339
|
-
project_name=file.project_name,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
return list(ret.values())
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def get_projects(client: AuthenticatedClient) -> list[Project]:
|
|
346
|
-
resp = client.get(PROJECT_ALL)
|
|
347
|
-
resp.raise_for_status()
|
|
348
|
-
|
|
349
|
-
ret = []
|
|
350
|
-
for pr in resp.json()[0]:
|
|
351
|
-
id = UUID(pr["uuid"], version=4)
|
|
352
|
-
name = pr["name"]
|
|
353
|
-
description = pr["description"]
|
|
354
|
-
ret.append(Project(id=id, name=name, description=description))
|
|
355
|
-
|
|
356
|
-
return ret
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def get_mission_by_spec(
|
|
360
|
-
client: AuthenticatedClient, spec: Union[MissionById, MissionByName]
|
|
361
|
-
) -> Optional[Mission]:
|
|
362
|
-
if isinstance(spec, MissionById):
|
|
363
|
-
return get_mission_by_id(client, spec.id)
|
|
364
|
-
|
|
365
|
-
if isinstance(spec.project, UUID):
|
|
366
|
-
project_id = spec.project
|
|
367
|
-
else:
|
|
368
|
-
project_id = get_project_id_by_name(client, spec.project)
|
|
369
|
-
if project_id is None:
|
|
370
|
-
return None
|
|
371
|
-
|
|
372
|
-
mission_id = get_mission_id_by_name(client, spec.name, project_id)
|
|
373
|
-
if mission_id is None:
|
|
374
|
-
return None
|
|
375
|
-
|
|
376
|
-
return get_mission_by_id(client, mission_id)
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def get_files_by_file_spec(
|
|
380
|
-
client: AuthenticatedClient, spec: Union[FilesByMission, FilesById]
|
|
381
|
-
) -> List[File]:
|
|
382
|
-
if isinstance(spec, FilesById):
|
|
383
|
-
return [get_file(client, file_id) for file_id in spec.ids]
|
|
384
|
-
|
|
385
|
-
parsed_mission = get_mission_by_spec(client, spec.mission)
|
|
386
|
-
if parsed_mission is None:
|
|
387
|
-
raise ValueError("mission not found")
|
|
388
|
-
|
|
389
|
-
if spec.files:
|
|
390
|
-
file_ids = [id_ for id_ in spec.files if isinstance(id_, UUID)]
|
|
391
|
-
file_names = filtered_by_patterns(
|
|
392
|
-
[file.name for file in parsed_mission.files],
|
|
393
|
-
[name for name in spec.files if isinstance(name, str)],
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
filtered = [
|
|
397
|
-
file
|
|
398
|
-
for file in parsed_mission.files
|
|
399
|
-
if file.id in file_ids or file.name in file_names
|
|
400
|
-
]
|
|
401
|
-
return filtered
|
|
402
|
-
|
|
403
|
-
return parsed_mission.files
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
def get_tag_type_by_name(
|
|
163
|
+
def _get_tag_type_by_name(
|
|
407
164
|
client: AuthenticatedClient, tag_name: str
|
|
408
165
|
) -> Optional[TagType]:
|
|
409
166
|
resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
|
|
@@ -423,13 +180,13 @@ def get_tag_type_by_name(
|
|
|
423
180
|
return tag_type
|
|
424
181
|
|
|
425
182
|
|
|
426
|
-
def
|
|
183
|
+
def _get_tags_map(
|
|
427
184
|
client: AuthenticatedClient, metadata: Dict[str, str]
|
|
428
185
|
) -> Dict[UUID, str]:
|
|
429
186
|
# TODO: this needs a better endpoint
|
|
430
187
|
ret = {}
|
|
431
188
|
for key, val in metadata.items():
|
|
432
|
-
tag_type =
|
|
189
|
+
tag_type = _get_tag_type_by_name(client, key)
|
|
433
190
|
|
|
434
191
|
if tag_type is None:
|
|
435
192
|
print(f"tag: {key} not found")
|
|
@@ -440,10 +197,10 @@ def get_tags_map(
|
|
|
440
197
|
return ret
|
|
441
198
|
|
|
442
199
|
|
|
443
|
-
def
|
|
444
|
-
client: AuthenticatedClient, mission_id: UUID, metadata: Dict[str, str]
|
|
200
|
+
def _update_mission_metadata(
|
|
201
|
+
client: AuthenticatedClient, mission_id: UUID, *, metadata: Dict[str, str]
|
|
445
202
|
) -> None:
|
|
446
|
-
tags_dct =
|
|
203
|
+
tags_dct = _get_tags_map(client, metadata)
|
|
447
204
|
payload = {
|
|
448
205
|
"missionUUID": str(mission_id),
|
|
449
206
|
"tags": {str(k): v for k, v in tags_dct.items()},
|
|
@@ -451,15 +208,15 @@ def update_mission_metadata(
|
|
|
451
208
|
resp = client.post(MISSION_UPDATE_METADATA, json=payload)
|
|
452
209
|
|
|
453
210
|
if resp.status_code == 404:
|
|
454
|
-
raise
|
|
211
|
+
raise MissionNotFound
|
|
455
212
|
|
|
456
213
|
if resp.status_code == 403:
|
|
457
|
-
raise
|
|
214
|
+
raise AccessDenied(f"cannot update mission: {mission_id}")
|
|
458
215
|
|
|
459
216
|
resp.raise_for_status()
|
|
460
217
|
|
|
461
218
|
|
|
462
|
-
def
|
|
219
|
+
def _get_api_version() -> Tuple[int, int, int]:
|
|
463
220
|
config = Config()
|
|
464
221
|
client = httpx.Client()
|
|
465
222
|
|
|
@@ -467,3 +224,12 @@ def get_api_version() -> Tuple[int, int, int]:
|
|
|
467
224
|
vers = resp.headers["kleinkram-version"].split(".")
|
|
468
225
|
|
|
469
226
|
return tuple(map(int, vers)) # type: ignore
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _claim_admin(client: AuthenticatedClient) -> None:
|
|
230
|
+
"""\
|
|
231
|
+
the first user on the system could call this
|
|
232
|
+
"""
|
|
233
|
+
response = client.post(CLAIM_ADMIN)
|
|
234
|
+
response.raise_for_status()
|
|
235
|
+
return
|