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.

Files changed (57) hide show
  1. kleinkram/__init__.py +33 -2
  2. kleinkram/api/client.py +21 -16
  3. kleinkram/api/deser.py +165 -0
  4. kleinkram/api/file_transfer.py +13 -24
  5. kleinkram/api/pagination.py +56 -0
  6. kleinkram/api/query.py +111 -0
  7. kleinkram/api/routes.py +270 -97
  8. kleinkram/auth.py +21 -20
  9. kleinkram/cli/__init__.py +0 -0
  10. kleinkram/{commands/download.py → cli/_download.py} +18 -44
  11. kleinkram/cli/_endpoint.py +58 -0
  12. kleinkram/{commands/list.py → cli/_list.py} +25 -38
  13. kleinkram/cli/_mission.py +153 -0
  14. kleinkram/cli/_project.py +99 -0
  15. kleinkram/cli/_upload.py +84 -0
  16. kleinkram/cli/_verify.py +56 -0
  17. kleinkram/{app.py → cli/app.py} +50 -22
  18. kleinkram/cli/error_handling.py +44 -0
  19. kleinkram/config.py +141 -107
  20. kleinkram/core.py +251 -3
  21. kleinkram/errors.py +13 -45
  22. kleinkram/main.py +1 -1
  23. kleinkram/models.py +48 -149
  24. kleinkram/printing.py +325 -0
  25. kleinkram/py.typed +0 -0
  26. kleinkram/types.py +9 -0
  27. kleinkram/utils.py +82 -27
  28. kleinkram/wrappers.py +401 -0
  29. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/METADATA +3 -3
  30. kleinkram-0.38.1.dev20250113080249.dist-info/RECORD +48 -0
  31. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/WHEEL +1 -1
  32. {kleinkram-0.38.1.dev20241125112529.dist-info → kleinkram-0.38.1.dev20250113080249.dist-info}/top_level.txt +1 -0
  33. testing/__init__.py +0 -0
  34. testing/backend_fixtures.py +69 -0
  35. tests/conftest.py +7 -0
  36. tests/test_config.py +115 -0
  37. tests/test_core.py +165 -0
  38. tests/test_end_to_end.py +29 -39
  39. tests/test_fixtures.py +29 -0
  40. tests/test_printing.py +62 -0
  41. tests/test_query.py +138 -0
  42. tests/test_utils.py +34 -24
  43. tests/test_wrappers.py +71 -0
  44. kleinkram/api/parsing.py +0 -86
  45. kleinkram/commands/__init__.py +0 -1
  46. kleinkram/commands/endpoint.py +0 -62
  47. kleinkram/commands/mission.py +0 -69
  48. kleinkram/commands/project.py +0 -24
  49. kleinkram/commands/upload.py +0 -164
  50. kleinkram/commands/verify.py +0 -142
  51. kleinkram/consts.py +0 -8
  52. kleinkram/enums.py +0 -10
  53. kleinkram/resources.py +0 -158
  54. kleinkram-0.38.1.dev20241125112529.dist-info/LICENSE +0 -674
  55. kleinkram-0.38.1.dev20241125112529.dist-info/RECORD +0 -37
  56. tests/test_resources.py +0 -137
  57. {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.parsing import _parse_file
12
- from kleinkram.api.parsing import _parse_mission
13
- from kleinkram.api.parsing import _parse_project
14
- from kleinkram.config import Config
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.models import DataType
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
- if resp.status_code in (403, 404):
72
- return []
74
+ FILE_ENDPOINT = "/file/many"
75
+ MISSION_ENDPOINT = "/mission/many"
76
+ PROJECT_ENDPOINT = "/project/many"
73
77
 
74
- resp.raise_for_status()
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
- return missions
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 _get_files_by_mission(client: AuthenticatedClient, mission: Mission) -> List[File]:
86
- params = {"uuid": str(mission.id), "take": MAX_PAGINATION}
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
- if resp.status_code in (403, 404):
91
- return []
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
- data = resp.json()
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
- return files
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 _get_mission_id_by_name(
105
- client: AuthenticatedClient, mission_name, project_id: UUID
106
- ) -> Optional[UUID]:
107
- params = {"name": mission_name, "projectUUID": str(project_id)}
108
- resp = client.get(MISSION_BY_NAME, params=params)
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
- # TODO: handle other status codes
114
- resp.raise_for_status()
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
- return UUID(data["uuid"], version=4)
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 _get_mission_id_by_name(client, mission_name, project_id) is not None:
139
- raise MissionExists(f"Mission with name: `{mission_name}` already exists")
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(MISSION_CREATE, json=payload)
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 _get_tag_type_by_name(
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[TagType]:
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
- tag_type = TagType(
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
- tag_type = _get_tag_type_by_name(client, key)
190
-
191
- if tag_type is None:
192
- print(f"tag: {key} not found")
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 _update_mission_metadata(
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(MISSION_UPDATE_METADATA, json=payload)
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 = 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"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: ")
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
- creds = Credentials(auth_token=auth_token, refresh_token=refresh_token)
36
- config.save_credentials(creds)
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 = Config(overwrite=True)
82
-
84
+ config = get_config()
83
85
  # use cli key login
84
86
  if key is not None:
85
- creds = Credentials(cli_key=key)
86
- config.save_credentials(creds)
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=url)
93
+ _browser_auth(url=oauth_url)
92
94
  else:
93
- headless_url = f"{url}-no-redirect"
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.file_transfer import download_files
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 spec
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
- project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
64
- mission_spec = MissionSpec(
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
- project_spec=project_spec,
64
+ project_query=project_query,
68
65
  )
69
- file_spec = FileSpec(
70
- patterns=file_patterns, ids=file_ids, mission_spec=mission_spec
66
+ file_query = FileQuery(
67
+ patterns=file_patterns, ids=file_ids, mission_query=mission_query
71
68
  )
72
69
 
73
- client = AuthenticatedClient()
74
- parsed_files = get_files_by_spec(client, file_spec)
75
-
76
- if get_shared_state().verbose:
77
- table = files_to_table(parsed_files, title="downloading files...")
78
- Console().print(table)
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
  )