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