kleinkram 0.38.1.dev20241125112529__py3-none-any.whl → 0.38.1.dev20250113080249__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/__init__.py +33 -2
- kleinkram/api/client.py +21 -16
- kleinkram/api/deser.py +165 -0
- kleinkram/api/file_transfer.py +13 -24
- kleinkram/api/pagination.py +56 -0
- kleinkram/api/query.py +111 -0
- kleinkram/api/routes.py +270 -97
- kleinkram/auth.py +21 -20
- kleinkram/cli/__init__.py +0 -0
- kleinkram/{commands/download.py → cli/_download.py} +18 -44
- kleinkram/cli/_endpoint.py +58 -0
- kleinkram/{commands/list.py → cli/_list.py} +25 -38
- kleinkram/cli/_mission.py +153 -0
- kleinkram/cli/_project.py +99 -0
- kleinkram/cli/_upload.py +84 -0
- kleinkram/cli/_verify.py +56 -0
- kleinkram/{app.py → cli/app.py} +50 -22
- kleinkram/cli/error_handling.py +44 -0
- kleinkram/config.py +141 -107
- kleinkram/core.py +251 -3
- kleinkram/errors.py +13 -45
- kleinkram/main.py +1 -1
- kleinkram/models.py +48 -149
- kleinkram/printing.py +325 -0
- kleinkram/py.typed +0 -0
- kleinkram/types.py +9 -0
- kleinkram/utils.py +82 -27
- kleinkram/wrappers.py +401 -0
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/METADATA +3 -3
- kleinkram-0.38.1.dev20250113080249.dist-info/RECORD +48 -0
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/top_level.txt +1 -0
- testing/__init__.py +0 -0
- testing/backend_fixtures.py +69 -0
- tests/conftest.py +7 -0
- tests/test_config.py +115 -0
- tests/test_core.py +165 -0
- tests/test_end_to_end.py +29 -39
- tests/test_fixtures.py +29 -0
- tests/test_printing.py +62 -0
- tests/test_query.py +138 -0
- tests/test_utils.py +34 -24
- tests/test_wrappers.py +71 -0
- kleinkram/api/parsing.py +0 -86
- kleinkram/commands/__init__.py +0 -1
- kleinkram/commands/endpoint.py +0 -62
- kleinkram/commands/mission.py +0 -69
- kleinkram/commands/project.py +0 -24
- kleinkram/commands/upload.py +0 -164
- kleinkram/commands/verify.py +0 -142
- kleinkram/consts.py +0 -8
- kleinkram/enums.py +0 -10
- kleinkram/resources.py +0 -158
- kleinkram-0.38.1.dev20241125112529.dist-info/LICENSE +0 -674
- kleinkram-0.38.1.dev20241125112529.dist-info/RECORD +0 -37
- tests/test_resources.py +0 -137
- {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/entry_points.txt +0 -0
kleinkram/api/routes.py
CHANGED
|
@@ -1,121 +1,234 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any
|
|
3
6
|
from typing import Dict
|
|
7
|
+
from typing import Generator
|
|
4
8
|
from typing import List
|
|
5
9
|
from typing import Optional
|
|
10
|
+
from typing import Sequence
|
|
6
11
|
from typing import Tuple
|
|
7
12
|
from uuid import UUID
|
|
8
13
|
|
|
9
14
|
import httpx
|
|
15
|
+
|
|
16
|
+
import kleinkram.errors
|
|
10
17
|
from kleinkram.api.client import AuthenticatedClient
|
|
11
|
-
from kleinkram.api.
|
|
12
|
-
from kleinkram.api.
|
|
13
|
-
from kleinkram.api.
|
|
14
|
-
from kleinkram.
|
|
18
|
+
from kleinkram.api.deser import FileObject
|
|
19
|
+
from kleinkram.api.deser import MissionObject
|
|
20
|
+
from kleinkram.api.deser import ProjectObject
|
|
21
|
+
from kleinkram.api.deser import _parse_file
|
|
22
|
+
from kleinkram.api.deser import _parse_mission
|
|
23
|
+
from kleinkram.api.deser import _parse_project
|
|
24
|
+
from kleinkram.api.pagination import paginated_request
|
|
25
|
+
from kleinkram.api.query import FileQuery
|
|
26
|
+
from kleinkram.api.query import MissionQuery
|
|
27
|
+
from kleinkram.api.query import ProjectQuery
|
|
28
|
+
from kleinkram.api.query import file_query_is_unique
|
|
29
|
+
from kleinkram.api.query import mission_query_is_unique
|
|
30
|
+
from kleinkram.api.query import project_query_is_unique
|
|
31
|
+
from kleinkram.config import get_config
|
|
15
32
|
from kleinkram.errors import AccessDenied
|
|
33
|
+
from kleinkram.errors import InvalidFileQuery
|
|
34
|
+
from kleinkram.errors import InvalidMissionMetadata
|
|
35
|
+
from kleinkram.errors import InvalidMissionQuery
|
|
36
|
+
from kleinkram.errors import InvalidProjectQuery
|
|
16
37
|
from kleinkram.errors import MissionExists
|
|
17
38
|
from kleinkram.errors import MissionNotFound
|
|
18
|
-
from kleinkram.
|
|
39
|
+
from kleinkram.errors import ProjectExists
|
|
40
|
+
from kleinkram.errors import ProjectNotFound
|
|
19
41
|
from kleinkram.models import File
|
|
20
42
|
from kleinkram.models import Mission
|
|
21
43
|
from kleinkram.models import Project
|
|
22
|
-
from kleinkram.models import TagType
|
|
23
44
|
from kleinkram.utils import is_valid_uuid4
|
|
24
45
|
|
|
25
46
|
__all__ = [
|
|
26
|
-
"_get_projects",
|
|
27
|
-
"_get_missions_by_project",
|
|
28
|
-
"_get_files_by_mission",
|
|
29
|
-
"_create_mission",
|
|
30
|
-
"_update_mission_metadata",
|
|
31
47
|
"_get_api_version",
|
|
32
48
|
"_claim_admin",
|
|
49
|
+
"_create_mission",
|
|
50
|
+
"_create_project",
|
|
51
|
+
"_update_mission",
|
|
52
|
+
"_update_project",
|
|
53
|
+
"_delete_files",
|
|
54
|
+
"_delete_mission",
|
|
55
|
+
"_delete_project",
|
|
56
|
+
"get_files",
|
|
57
|
+
"get_missions",
|
|
58
|
+
"get_projects",
|
|
59
|
+
"get_project",
|
|
60
|
+
"get_mission",
|
|
61
|
+
"get_file",
|
|
33
62
|
]
|
|
34
63
|
|
|
35
64
|
|
|
36
|
-
MAX_PAGINATION = 10_000
|
|
37
|
-
|
|
38
65
|
CLAIM_ADMIN = "/user/claimAdmin"
|
|
39
|
-
PROJECT_ALL = "/project/filtered"
|
|
40
|
-
MISSIONS_BY_PROJECT = "/mission/filtered"
|
|
41
|
-
MISSION_BY_NAME = "/mission/byName"
|
|
42
|
-
MISSION_CREATE = "/mission/create"
|
|
43
|
-
MISSION_UPDATE_METADATA = "/mission/tags"
|
|
44
|
-
FILE_OF_MISSION = "/file/ofMission"
|
|
45
|
-
TAG_TYPE_BY_NAME = "/tag/filtered"
|
|
46
66
|
GET_STATUS = "/user/me"
|
|
47
67
|
|
|
68
|
+
UPDATE_PROJECT = "/project"
|
|
69
|
+
UPDATE_MISSION = "/mission/tags" # TODO: just metadata for now
|
|
70
|
+
CREATE_MISSION = "/mission/create"
|
|
71
|
+
CREATE_PROJECT = "/project/create"
|
|
48
72
|
|
|
49
|
-
def _get_projects(client: AuthenticatedClient) -> list[Project]:
|
|
50
|
-
resp = client.get(PROJECT_ALL)
|
|
51
|
-
|
|
52
|
-
if resp.status_code in (403, 404):
|
|
53
|
-
return []
|
|
54
|
-
|
|
55
|
-
resp.raise_for_status()
|
|
56
|
-
|
|
57
|
-
ret = []
|
|
58
|
-
for pr in resp.json()[0]:
|
|
59
|
-
ret.append(_parse_project(pr))
|
|
60
|
-
|
|
61
|
-
return ret
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _get_missions_by_project(
|
|
65
|
-
client: AuthenticatedClient, project: Project
|
|
66
|
-
) -> List[Mission]:
|
|
67
|
-
params = {"uuid": str(project.id), "take": MAX_PAGINATION}
|
|
68
|
-
|
|
69
|
-
resp = client.get(MISSIONS_BY_PROJECT, params=params)
|
|
70
73
|
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
FILE_ENDPOINT = "/file/many"
|
|
75
|
+
MISSION_ENDPOINT = "/mission/many"
|
|
76
|
+
PROJECT_ENDPOINT = "/project/many"
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
data = resp.json()
|
|
77
|
-
missions = []
|
|
78
|
+
MISSION_BY_NAME = "/mission/byName"
|
|
79
|
+
TAG_TYPE_BY_NAME = "/tag/filtered"
|
|
78
80
|
|
|
79
|
-
for mission in data[0]:
|
|
80
|
-
missions.append(_parse_mission(mission, project))
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
class Params(str, Enum):
|
|
83
|
+
FILE_PATTERNS = "filePatterns"
|
|
84
|
+
FILE_IDS = "fileUuids"
|
|
85
|
+
MISSION_PATTERNS = "missionPatterns"
|
|
86
|
+
MISSION_IDS = "missionUuids"
|
|
87
|
+
PROJECT_PATTERNS = "projectPatterns"
|
|
88
|
+
PROJECT_IDS = "projectUuids"
|
|
83
89
|
|
|
84
90
|
|
|
85
|
-
def
|
|
86
|
-
|
|
91
|
+
def _handle_list_params(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
92
|
+
"""
|
|
93
|
+
json dumps lists
|
|
94
|
+
"""
|
|
95
|
+
new_params = {}
|
|
96
|
+
for k, v in params.items():
|
|
97
|
+
if not isinstance(v, list):
|
|
98
|
+
new_params[k] = v
|
|
99
|
+
else:
|
|
100
|
+
new_params[k] = json.dumps(v)
|
|
101
|
+
return new_params
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _project_query_to_params(
|
|
105
|
+
project_query: ProjectQuery,
|
|
106
|
+
) -> Dict[str, List[str]]:
|
|
107
|
+
params = {}
|
|
108
|
+
if project_query.patterns:
|
|
109
|
+
params[Params.PROJECT_PATTERNS.value] = project_query.patterns
|
|
110
|
+
if project_query.ids:
|
|
111
|
+
params[Params.PROJECT_IDS.value] = list(map(str, project_query.ids))
|
|
112
|
+
params = _handle_list_params(params)
|
|
113
|
+
return params
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _mission_query_to_params(mission_query: MissionQuery) -> Dict[str, List[str]]:
|
|
117
|
+
params = _project_query_to_params(mission_query.project_query)
|
|
118
|
+
if mission_query.patterns:
|
|
119
|
+
params[Params.MISSION_PATTERNS.value] = mission_query.patterns
|
|
120
|
+
if mission_query.ids:
|
|
121
|
+
params[Params.MISSION_IDS.value] = list(map(str, mission_query.ids))
|
|
122
|
+
params = _handle_list_params(params)
|
|
123
|
+
return params
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _file_query_to_params(file_query: FileQuery) -> Dict[str, List[str]]:
|
|
127
|
+
params = _mission_query_to_params(file_query.mission_query)
|
|
128
|
+
if file_query.patterns:
|
|
129
|
+
params[Params.FILE_PATTERNS.value] = list(file_query.patterns)
|
|
130
|
+
if file_query.ids:
|
|
131
|
+
params[Params.FILE_IDS.value] = list(map(str, file_query.ids))
|
|
132
|
+
params = _handle_list_params(params)
|
|
133
|
+
return params
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_files(
|
|
137
|
+
client: AuthenticatedClient,
|
|
138
|
+
file_query: FileQuery,
|
|
139
|
+
max_entries: Optional[int] = None,
|
|
140
|
+
) -> Generator[File, None, None]:
|
|
141
|
+
params = _file_query_to_params(file_query)
|
|
142
|
+
response_stream = paginated_request(
|
|
143
|
+
client, FILE_ENDPOINT, params=params, max_entries=max_entries
|
|
144
|
+
)
|
|
145
|
+
yield from map(lambda f: _parse_file(FileObject(f)), response_stream)
|
|
87
146
|
|
|
88
|
-
resp = client.get(FILE_OF_MISSION, params=params)
|
|
89
147
|
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
def get_missions(
|
|
149
|
+
client: AuthenticatedClient,
|
|
150
|
+
mission_query: MissionQuery,
|
|
151
|
+
max_entries: Optional[int] = None,
|
|
152
|
+
) -> Generator[Mission, None, None]:
|
|
153
|
+
params = _mission_query_to_params(mission_query)
|
|
154
|
+
response_stream = paginated_request(
|
|
155
|
+
client, MISSION_ENDPOINT, params=params, max_entries=max_entries
|
|
156
|
+
)
|
|
157
|
+
yield from map(lambda m: _parse_mission(MissionObject(m)), response_stream)
|
|
92
158
|
|
|
93
|
-
resp.raise_for_status()
|
|
94
159
|
|
|
95
|
-
|
|
160
|
+
def get_projects(
|
|
161
|
+
client: AuthenticatedClient,
|
|
162
|
+
project_query: ProjectQuery,
|
|
163
|
+
max_entries: Optional[int] = None,
|
|
164
|
+
) -> Generator[Project, None, None]:
|
|
165
|
+
params = _project_query_to_params(project_query)
|
|
166
|
+
response_stream = paginated_request(
|
|
167
|
+
client, PROJECT_ENDPOINT, params=params, max_entries=max_entries
|
|
168
|
+
)
|
|
169
|
+
yield from map(lambda p: _parse_project(ProjectObject(p)), response_stream)
|
|
96
170
|
|
|
97
|
-
files = []
|
|
98
|
-
for file in data[0]:
|
|
99
|
-
files.append(_parse_file(file, mission))
|
|
100
171
|
|
|
101
|
-
|
|
172
|
+
def get_project(client: AuthenticatedClient, query: ProjectQuery) -> Project:
|
|
173
|
+
"""\
|
|
174
|
+
get a unique project by specifying a project spec
|
|
175
|
+
"""
|
|
176
|
+
if not project_query_is_unique(query):
|
|
177
|
+
raise InvalidProjectQuery(
|
|
178
|
+
f"Project query does not uniquely determine project: {query}"
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
return next(get_projects(client, query))
|
|
182
|
+
except StopIteration:
|
|
183
|
+
raise ProjectNotFound(f"Project not found: {query}")
|
|
102
184
|
|
|
103
185
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
186
|
+
def get_mission(client: AuthenticatedClient, query: MissionQuery) -> Mission:
|
|
187
|
+
"""\
|
|
188
|
+
get a unique mission by specifying a mission query
|
|
189
|
+
"""
|
|
190
|
+
if not mission_query_is_unique(query):
|
|
191
|
+
raise InvalidMissionQuery(
|
|
192
|
+
f"Mission query does not uniquely determine mission: {query}"
|
|
193
|
+
)
|
|
194
|
+
try:
|
|
195
|
+
return next(get_missions(client, query))
|
|
196
|
+
except StopIteration:
|
|
197
|
+
raise MissionNotFound(f"Mission not found: {query}")
|
|
109
198
|
|
|
110
|
-
if resp.status_code in (403, 404):
|
|
111
|
-
return None
|
|
112
199
|
|
|
113
|
-
|
|
114
|
-
|
|
200
|
+
def get_file(client: AuthenticatedClient, query: FileQuery) -> File:
|
|
201
|
+
"""\
|
|
202
|
+
get a unique file by specifying a file query
|
|
203
|
+
"""
|
|
204
|
+
if not file_query_is_unique(query):
|
|
205
|
+
raise InvalidFileQuery(f"File query does not uniquely determine file: {query}")
|
|
206
|
+
try:
|
|
207
|
+
return next(get_files(client, query))
|
|
208
|
+
except StopIteration:
|
|
209
|
+
raise kleinkram.errors.FileNotFound(f"File not found: {query}")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _mission_name_is_available(
|
|
213
|
+
client: AuthenticatedClient, mission_name: str, project_id: UUID
|
|
214
|
+
) -> bool:
|
|
215
|
+
mission_query = MissionQuery(
|
|
216
|
+
patterns=[mission_name], project_query=ProjectQuery(ids=[project_id])
|
|
217
|
+
)
|
|
218
|
+
try:
|
|
219
|
+
_ = get_mission(client, mission_query)
|
|
220
|
+
except MissionNotFound:
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
115
223
|
|
|
116
|
-
data = resp.json()
|
|
117
224
|
|
|
118
|
-
|
|
225
|
+
def _project_name_is_available(client: AuthenticatedClient, project_name: str) -> bool:
|
|
226
|
+
project_query = ProjectQuery(patterns=[project_name])
|
|
227
|
+
try:
|
|
228
|
+
_ = get_project(client, project_query)
|
|
229
|
+
except ProjectNotFound:
|
|
230
|
+
return True
|
|
231
|
+
return False
|
|
119
232
|
|
|
120
233
|
|
|
121
234
|
def _create_mission(
|
|
@@ -135,8 +248,11 @@ def _create_mission(
|
|
|
135
248
|
if metadata is None:
|
|
136
249
|
metadata = {}
|
|
137
250
|
|
|
138
|
-
if
|
|
139
|
-
raise MissionExists(
|
|
251
|
+
if not _mission_name_is_available(client, mission_name, project_id):
|
|
252
|
+
raise MissionExists(
|
|
253
|
+
f"Mission with name: `{mission_name}` already exists"
|
|
254
|
+
f" in project: {project_id}"
|
|
255
|
+
)
|
|
140
256
|
|
|
141
257
|
if is_valid_uuid4(mission_name):
|
|
142
258
|
raise ValueError(
|
|
@@ -154,15 +270,29 @@ def _create_mission(
|
|
|
154
270
|
"ignoreTags": ignore_missing_tags,
|
|
155
271
|
}
|
|
156
272
|
|
|
157
|
-
resp = client.post(
|
|
273
|
+
resp = client.post(CREATE_MISSION, json=payload)
|
|
158
274
|
resp.raise_for_status()
|
|
159
275
|
|
|
160
276
|
return UUID(resp.json()["uuid"], version=4)
|
|
161
277
|
|
|
162
278
|
|
|
163
|
-
def
|
|
279
|
+
def _create_project(
|
|
280
|
+
client: AuthenticatedClient, project_name: str, description: str
|
|
281
|
+
) -> UUID:
|
|
282
|
+
if not _project_name_is_available(client, project_name):
|
|
283
|
+
raise ProjectExists(f"Project with name: `{project_name}` already exists")
|
|
284
|
+
|
|
285
|
+
# TODO: check name and description are valid
|
|
286
|
+
payload = {"name": project_name, "description": description}
|
|
287
|
+
resp = client.post(CREATE_PROJECT, json=payload)
|
|
288
|
+
resp.raise_for_status()
|
|
289
|
+
|
|
290
|
+
return UUID(resp.json()["uuid"], version=4)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _get_metadata_type_id_by_name(
|
|
164
294
|
client: AuthenticatedClient, tag_name: str
|
|
165
|
-
) -> Optional[
|
|
295
|
+
) -> Optional[UUID]:
|
|
166
296
|
resp = client.get(TAG_TYPE_BY_NAME, params={"name": tag_name, "take": 1})
|
|
167
297
|
|
|
168
298
|
if resp.status_code in (403, 404):
|
|
@@ -171,33 +301,24 @@ def _get_tag_type_by_name(
|
|
|
171
301
|
resp.raise_for_status()
|
|
172
302
|
|
|
173
303
|
data = resp.json()[0]
|
|
174
|
-
|
|
175
|
-
name=data["name"],
|
|
176
|
-
id=UUID(data["uuid"], version=4),
|
|
177
|
-
data_type=DataType(data["datatype"]),
|
|
178
|
-
description=data["description"],
|
|
179
|
-
)
|
|
180
|
-
return tag_type
|
|
304
|
+
return UUID(data["uuid"], version=4)
|
|
181
305
|
|
|
182
306
|
|
|
183
307
|
def _get_tags_map(
|
|
184
308
|
client: AuthenticatedClient, metadata: Dict[str, str]
|
|
185
309
|
) -> Dict[UUID, str]:
|
|
186
310
|
# TODO: this needs a better endpoint
|
|
311
|
+
# why are we using metadata type ids as keys???
|
|
187
312
|
ret = {}
|
|
188
313
|
for key, val in metadata.items():
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
continue
|
|
194
|
-
|
|
195
|
-
ret[tag_type.id] = val
|
|
196
|
-
|
|
314
|
+
metadata_type_id = _get_metadata_type_id_by_name(client, key)
|
|
315
|
+
if metadata_type_id is None:
|
|
316
|
+
raise InvalidMissionMetadata(f"metadata field: {key} does not exist")
|
|
317
|
+
ret[metadata_type_id] = val
|
|
197
318
|
return ret
|
|
198
319
|
|
|
199
320
|
|
|
200
|
-
def
|
|
321
|
+
def _update_mission(
|
|
201
322
|
client: AuthenticatedClient, mission_id: UUID, *, metadata: Dict[str, str]
|
|
202
323
|
) -> None:
|
|
203
324
|
tags_dct = _get_tags_map(client, metadata)
|
|
@@ -205,22 +326,40 @@ def _update_mission_metadata(
|
|
|
205
326
|
"missionUUID": str(mission_id),
|
|
206
327
|
"tags": {str(k): v for k, v in tags_dct.items()},
|
|
207
328
|
}
|
|
208
|
-
resp = client.post(
|
|
329
|
+
resp = client.post(UPDATE_MISSION, json=payload)
|
|
209
330
|
|
|
210
331
|
if resp.status_code == 404:
|
|
211
332
|
raise MissionNotFound
|
|
212
|
-
|
|
213
333
|
if resp.status_code == 403:
|
|
214
334
|
raise AccessDenied(f"cannot update mission: {mission_id}")
|
|
215
335
|
|
|
216
336
|
resp.raise_for_status()
|
|
217
337
|
|
|
218
338
|
|
|
339
|
+
def _update_project(
|
|
340
|
+
client: AuthenticatedClient,
|
|
341
|
+
project_id: UUID,
|
|
342
|
+
*,
|
|
343
|
+
description: Optional[str] = None,
|
|
344
|
+
new_name: Optional[str] = None,
|
|
345
|
+
) -> None:
|
|
346
|
+
if description is None and new_name is None:
|
|
347
|
+
raise ValueError("either description or new_name must be provided")
|
|
348
|
+
|
|
349
|
+
body = {}
|
|
350
|
+
if description is not None:
|
|
351
|
+
body["description"] = description
|
|
352
|
+
if new_name is not None:
|
|
353
|
+
body["name"] = new_name
|
|
354
|
+
resp = client.put(f"{UPDATE_PROJECT}/{project_id}", json=body)
|
|
355
|
+
resp.raise_for_status()
|
|
356
|
+
|
|
357
|
+
|
|
219
358
|
def _get_api_version() -> Tuple[int, int, int]:
|
|
220
|
-
config =
|
|
359
|
+
config = get_config()
|
|
221
360
|
client = httpx.Client()
|
|
222
361
|
|
|
223
|
-
resp = client.get(f"{config.endpoint}{GET_STATUS}")
|
|
362
|
+
resp = client.get(f"{config.endpoint.api}{GET_STATUS}")
|
|
224
363
|
vers = resp.headers["kleinkram-version"].split(".")
|
|
225
364
|
|
|
226
365
|
return tuple(map(int, vers)) # type: ignore
|
|
@@ -233,3 +372,37 @@ def _claim_admin(client: AuthenticatedClient) -> None:
|
|
|
233
372
|
response = client.post(CLAIM_ADMIN)
|
|
234
373
|
response.raise_for_status()
|
|
235
374
|
return
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
FILE_DELETE_MANY = "/file/deleteMultiple"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _delete_files(
|
|
381
|
+
client: AuthenticatedClient, file_ids: Sequence[UUID], mission_id: UUID
|
|
382
|
+
) -> None:
|
|
383
|
+
payload = {
|
|
384
|
+
"uuids": [str(file_id) for file_id in file_ids],
|
|
385
|
+
"missionUUID": str(mission_id),
|
|
386
|
+
}
|
|
387
|
+
resp = client.post(FILE_DELETE_MANY, json=payload)
|
|
388
|
+
resp.raise_for_status()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
MISSION_DELETE_ONE = "/mission/{}"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _delete_mission(client: AuthenticatedClient, mission_id: UUID) -> None:
|
|
395
|
+
resp = client.delete(MISSION_DELETE_ONE.format(mission_id))
|
|
396
|
+
|
|
397
|
+
# 409 is returned if the mission has files
|
|
398
|
+
# 403 is returned if the mission does not exist / user cant delete
|
|
399
|
+
|
|
400
|
+
resp.raise_for_status()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
PROJECT_DELETE_ONE = "/project/{}"
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _delete_project(client: AuthenticatedClient, project_id: UUID) -> None:
|
|
407
|
+
resp = client.delete(PROJECT_DELETE_ONE.format(project_id))
|
|
408
|
+
resp.raise_for_status()
|
kleinkram/auth.py
CHANGED
|
@@ -7,9 +7,10 @@ from http.server import BaseHTTPRequestHandler
|
|
|
7
7
|
from http.server import HTTPServer
|
|
8
8
|
from typing import Optional
|
|
9
9
|
|
|
10
|
-
from kleinkram.config import Config
|
|
11
10
|
from kleinkram.config import CONFIG_PATH
|
|
12
11
|
from kleinkram.config import Credentials
|
|
12
|
+
from kleinkram.config import get_config
|
|
13
|
+
from kleinkram.config import save_config
|
|
13
14
|
|
|
14
15
|
CLI_CALLBACK_ENDPOINT = "/cli/callback"
|
|
15
16
|
OAUTH_SLUG = "/auth/google?state=cli"
|
|
@@ -24,16 +25,18 @@ def _has_browser() -> bool:
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
def _headless_auth(*, url: str) -> None:
|
|
27
|
-
config = Config()
|
|
28
28
|
|
|
29
|
-
print(f"
|
|
30
|
-
print("
|
|
31
|
-
auth_token = getpass("
|
|
32
|
-
refresh_token = getpass("
|
|
29
|
+
print(f"please open the following URL manually to authenticate: {url}")
|
|
30
|
+
print("enter the authentication token provided after logging in:")
|
|
31
|
+
auth_token = getpass("authentication token: ")
|
|
32
|
+
refresh_token = getpass("refresh token: ")
|
|
33
33
|
|
|
34
34
|
if auth_token and refresh_token:
|
|
35
|
-
|
|
36
|
-
config.
|
|
35
|
+
config = get_config()
|
|
36
|
+
config.credentials = Credentials(
|
|
37
|
+
auth_token=auth_token, refresh_token=refresh_token
|
|
38
|
+
)
|
|
39
|
+
save_config(config)
|
|
37
40
|
print(f"Authentication complete. Tokens saved to {CONFIG_PATH}.")
|
|
38
41
|
else:
|
|
39
42
|
raise ValueError("Please provided tokens.")
|
|
@@ -50,12 +53,12 @@ class OAuthCallbackHandler(BaseHTTPRequestHandler):
|
|
|
50
53
|
auth_token=params.get("authtoken")[0], # type: ignore
|
|
51
54
|
refresh_token=params.get("refreshtoken")[0], # type: ignore
|
|
52
55
|
)
|
|
56
|
+
config = get_config()
|
|
57
|
+
config.credentials = creds
|
|
58
|
+
save_config(config)
|
|
53
59
|
except Exception:
|
|
54
60
|
raise RuntimeError("Failed to fetch authentication tokens.")
|
|
55
61
|
|
|
56
|
-
config = Config()
|
|
57
|
-
config.save_credentials(creds)
|
|
58
|
-
|
|
59
62
|
self.send_response(200)
|
|
60
63
|
self.send_header("Content-type", "text/html")
|
|
61
64
|
self.end_headers()
|
|
@@ -78,17 +81,15 @@ def _browser_auth(*, url: str) -> None:
|
|
|
78
81
|
|
|
79
82
|
|
|
80
83
|
def login_flow(*, key: Optional[str] = None, headless: bool = False) -> None:
|
|
81
|
-
config =
|
|
82
|
-
|
|
84
|
+
config = get_config()
|
|
83
85
|
# use cli key login
|
|
84
86
|
if key is not None:
|
|
85
|
-
|
|
86
|
-
config
|
|
87
|
-
|
|
88
|
-
url = f"{config.endpoint}{OAUTH_SLUG}"
|
|
87
|
+
config.credentials = Credentials(cli_key=key)
|
|
88
|
+
save_config(config)
|
|
89
|
+
return
|
|
89
90
|
|
|
91
|
+
oauth_url = f"{config.endpoint.api}{OAUTH_SLUG}"
|
|
90
92
|
if not headless and _has_browser():
|
|
91
|
-
_browser_auth(url=
|
|
93
|
+
_browser_auth(url=oauth_url)
|
|
92
94
|
else:
|
|
93
|
-
|
|
94
|
-
_headless_auth(url=headless_url)
|
|
95
|
+
_headless_auth(url=f"{oauth_url}-no-redirect")
|
|
File without changes
|
|
@@ -6,17 +6,14 @@ from typing import List
|
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
|
+
|
|
10
|
+
import kleinkram.core
|
|
9
11
|
from kleinkram.api.client import AuthenticatedClient
|
|
10
|
-
from kleinkram.api.
|
|
12
|
+
from kleinkram.api.query import FileQuery
|
|
13
|
+
from kleinkram.api.query import MissionQuery
|
|
14
|
+
from kleinkram.api.query import ProjectQuery
|
|
11
15
|
from kleinkram.config import get_shared_state
|
|
12
|
-
from kleinkram.models import files_to_table
|
|
13
|
-
from kleinkram.resources import FileSpec
|
|
14
|
-
from kleinkram.resources import get_files_by_spec
|
|
15
|
-
from kleinkram.resources import MissionSpec
|
|
16
|
-
from kleinkram.resources import ProjectSpec
|
|
17
16
|
from kleinkram.utils import split_args
|
|
18
|
-
from rich.console import Console
|
|
19
|
-
|
|
20
17
|
|
|
21
18
|
logger = logging.getLogger(__name__)
|
|
22
19
|
|
|
@@ -55,49 +52,26 @@ def download(
|
|
|
55
52
|
typer.confirm(f"Destination {dest_dir} does not exist. Create it?", abort=True)
|
|
56
53
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
57
54
|
|
|
58
|
-
# get file
|
|
55
|
+
# get file query
|
|
59
56
|
file_ids, file_patterns = split_args(files or [])
|
|
60
57
|
mission_ids, mission_patterns = split_args(missions or [])
|
|
61
58
|
project_ids, project_patterns = split_args(projects or [])
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
project_query = ProjectQuery(patterns=project_patterns, ids=project_ids)
|
|
61
|
+
mission_query = MissionQuery(
|
|
65
62
|
patterns=mission_patterns,
|
|
66
63
|
ids=mission_ids,
|
|
67
|
-
|
|
64
|
+
project_query=project_query,
|
|
68
65
|
)
|
|
69
|
-
|
|
70
|
-
patterns=file_patterns, ids=file_ids,
|
|
66
|
+
file_query = FileQuery(
|
|
67
|
+
patterns=file_patterns, ids=file_ids, mission_query=mission_query
|
|
71
68
|
)
|
|
72
69
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# get paths to files map
|
|
81
|
-
if (
|
|
82
|
-
len(set([(file.project_id, file.mission_id) for file in parsed_files])) > 1
|
|
83
|
-
and not nested
|
|
84
|
-
):
|
|
85
|
-
raise ValueError(
|
|
86
|
-
"files from multiple missions were selected, consider using `--nested`"
|
|
87
|
-
)
|
|
88
|
-
elif not nested:
|
|
89
|
-
# flat structure
|
|
90
|
-
paths_to_files = {dest_dir / file.name: file for file in parsed_files}
|
|
91
|
-
else:
|
|
92
|
-
# allow for nested directories
|
|
93
|
-
paths_to_files = {}
|
|
94
|
-
for file in parsed_files:
|
|
95
|
-
paths_to_files[
|
|
96
|
-
dest_dir / file.project_name / file.mission_name / file.name
|
|
97
|
-
] = file
|
|
98
|
-
|
|
99
|
-
# download files
|
|
100
|
-
logger.info(f"downloading {paths_to_files} files to {dest_dir}")
|
|
101
|
-
download_files(
|
|
102
|
-
client, paths_to_files, verbose=get_shared_state().verbose, overwrite=overwrite
|
|
70
|
+
kleinkram.core.download(
|
|
71
|
+
client=AuthenticatedClient(),
|
|
72
|
+
query=file_query,
|
|
73
|
+
base_dir=dest_dir,
|
|
74
|
+
nested=nested,
|
|
75
|
+
overwrite=overwrite,
|
|
76
|
+
verbose=get_shared_state().verbose,
|
|
103
77
|
)
|