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/app.py
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import os
|
|
5
4
|
import sys
|
|
6
5
|
import time
|
|
7
|
-
from collections import OrderedDict
|
|
8
6
|
from enum import Enum
|
|
9
7
|
from pathlib import Path
|
|
10
|
-
from typing import Any
|
|
11
|
-
from typing import Callable
|
|
12
8
|
from typing import List
|
|
13
9
|
from typing import Optional
|
|
14
|
-
from typing import Type
|
|
15
10
|
|
|
16
11
|
import typer
|
|
17
12
|
from click import Context
|
|
18
13
|
from kleinkram._version import __version__
|
|
19
14
|
from kleinkram.api.client import AuthenticatedClient
|
|
20
|
-
from kleinkram.api.routes import
|
|
21
|
-
from kleinkram.api.routes import
|
|
15
|
+
from kleinkram.api.routes import _claim_admin
|
|
16
|
+
from kleinkram.api.routes import _get_api_version
|
|
22
17
|
from kleinkram.auth import login_flow
|
|
23
18
|
from kleinkram.commands.download import download_typer
|
|
24
19
|
from kleinkram.commands.endpoint import endpoint_typer
|
|
@@ -29,6 +24,7 @@ from kleinkram.commands.upload import upload_typer
|
|
|
29
24
|
from kleinkram.commands.verify import verify_typer
|
|
30
25
|
from kleinkram.config import Config
|
|
31
26
|
from kleinkram.config import get_shared_state
|
|
27
|
+
from kleinkram.errors import ErrorHandledTyper
|
|
32
28
|
from kleinkram.errors import InvalidCLIVersion
|
|
33
29
|
from kleinkram.utils import format_traceback
|
|
34
30
|
from kleinkram.utils import get_supported_api_version
|
|
@@ -73,40 +69,6 @@ class OrderCommands(TyperGroup):
|
|
|
73
69
|
return list(self.commands)
|
|
74
70
|
|
|
75
71
|
|
|
76
|
-
ExceptionHandler = Callable[[Exception], int]
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class ErrorHandledTyper(typer.Typer):
|
|
80
|
-
"""\
|
|
81
|
-
error handlers that are last added will be used first
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
_error_handlers: OrderedDict[Type[Exception], ExceptionHandler]
|
|
85
|
-
|
|
86
|
-
def error_handler(
|
|
87
|
-
self, exc: type[Exception]
|
|
88
|
-
) -> Callable[[ExceptionHandler], ExceptionHandler]:
|
|
89
|
-
def dec(func: ExceptionHandler) -> ExceptionHandler:
|
|
90
|
-
self._error_handlers[exc] = func
|
|
91
|
-
return func
|
|
92
|
-
|
|
93
|
-
return dec
|
|
94
|
-
|
|
95
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
96
|
-
super().__init__(*args, **kwargs)
|
|
97
|
-
self._error_handlers = OrderedDict()
|
|
98
|
-
|
|
99
|
-
def __call__(self, *args: Any, **kwargs: Any) -> int:
|
|
100
|
-
try:
|
|
101
|
-
return super().__call__(*args, **kwargs)
|
|
102
|
-
except Exception as e:
|
|
103
|
-
for tp, handler in reversed(self._error_handlers.items()):
|
|
104
|
-
if isinstance(e, tp):
|
|
105
|
-
exit_code = handler(e)
|
|
106
|
-
raise SystemExit(exit_code)
|
|
107
|
-
raise
|
|
108
|
-
|
|
109
|
-
|
|
110
72
|
app = ErrorHandledTyper(
|
|
111
73
|
cls=OrderCommands,
|
|
112
74
|
help=CLI_HELP,
|
|
@@ -114,7 +76,16 @@ app = ErrorHandledTyper(
|
|
|
114
76
|
no_args_is_help=True,
|
|
115
77
|
)
|
|
116
78
|
|
|
79
|
+
app.add_typer(download_typer, name="download", rich_help_panel=CommandTypes.CORE)
|
|
80
|
+
app.add_typer(upload_typer, name="upload", rich_help_panel=CommandTypes.CORE)
|
|
81
|
+
app.add_typer(verify_typer, name="verify", rich_help_panel=CommandTypes.CORE)
|
|
82
|
+
app.add_typer(list_typer, name="list", rich_help_panel=CommandTypes.CORE)
|
|
83
|
+
app.add_typer(endpoint_typer, name="endpoint", rich_help_panel=CommandTypes.AUTH)
|
|
84
|
+
app.add_typer(mission_typer, name="mission", rich_help_panel=CommandTypes.CRUD)
|
|
85
|
+
app.add_typer(project_typer, name="project", rich_help_panel=CommandTypes.CRUD)
|
|
117
86
|
|
|
87
|
+
|
|
88
|
+
# attach error handler to app
|
|
118
89
|
@app.error_handler(Exception)
|
|
119
90
|
def base_handler(exc: Exception) -> int:
|
|
120
91
|
if not get_shared_state().debug:
|
|
@@ -124,15 +95,6 @@ def base_handler(exc: Exception) -> int:
|
|
|
124
95
|
raise exc
|
|
125
96
|
|
|
126
97
|
|
|
127
|
-
app.add_typer(download_typer, name="download", rich_help_panel=CommandTypes.CORE)
|
|
128
|
-
app.add_typer(upload_typer, name="upload", rich_help_panel=CommandTypes.CORE)
|
|
129
|
-
app.add_typer(verify_typer, name="verify", rich_help_panel=CommandTypes.CORE)
|
|
130
|
-
app.add_typer(list_typer, name="list", rich_help_panel=CommandTypes.CORE)
|
|
131
|
-
app.add_typer(endpoint_typer, name="endpoint", rich_help_panel=CommandTypes.AUTH)
|
|
132
|
-
app.add_typer(mission_typer, name="mission", rich_help_panel=CommandTypes.CRUD)
|
|
133
|
-
app.add_typer(project_typer, name="project", rich_help_panel=CommandTypes.CRUD)
|
|
134
|
-
|
|
135
|
-
|
|
136
98
|
@app.command(rich_help_panel=CommandTypes.AUTH)
|
|
137
99
|
def login(
|
|
138
100
|
key: Optional[str] = typer.Option(None, help="CLI key"),
|
|
@@ -150,7 +112,7 @@ def logout(all: bool = typer.Option(False, help="logout on all enpoints")) -> No
|
|
|
150
112
|
@app.command(hidden=True)
|
|
151
113
|
def claim():
|
|
152
114
|
client = AuthenticatedClient()
|
|
153
|
-
|
|
115
|
+
_claim_admin(client)
|
|
154
116
|
print("admin rights claimed successfully.")
|
|
155
117
|
|
|
156
118
|
|
|
@@ -162,7 +124,7 @@ def _version_callback(value: bool) -> None:
|
|
|
162
124
|
|
|
163
125
|
def check_version_compatiblity() -> None:
|
|
164
126
|
cli_version = get_supported_api_version()
|
|
165
|
-
api_version =
|
|
127
|
+
api_version = _get_api_version()
|
|
166
128
|
api_vers_str = ".".join(map(str, api_version))
|
|
167
129
|
|
|
168
130
|
if cli_version[0] != api_version[0]:
|
|
@@ -197,6 +159,8 @@ def cli(
|
|
|
197
159
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
198
160
|
level = logging.getLevelName(log_level)
|
|
199
161
|
logging.basicConfig(level=level, filename=LOG_FILE, format=LOG_FORMAT)
|
|
162
|
+
else:
|
|
163
|
+
logging.disable(logging.CRITICAL)
|
|
200
164
|
|
|
201
165
|
logger.info(f"CLI version: {__version__}")
|
|
202
166
|
|
|
@@ -206,6 +170,8 @@ def cli(
|
|
|
206
170
|
logger.error(format_traceback(e))
|
|
207
171
|
raise
|
|
208
172
|
except Exception:
|
|
209
|
-
err = "failed to check version compatibility"
|
|
210
|
-
Console(file=sys.stderr).print(
|
|
173
|
+
err = ("failed to check version compatibility",)
|
|
174
|
+
Console(file=sys.stderr).print(
|
|
175
|
+
err, style="yellow" if shared_state.verbose else None
|
|
176
|
+
)
|
|
211
177
|
logger.error(err)
|
kleinkram/auth.py
CHANGED
|
@@ -9,9 +9,7 @@ from typing import Optional
|
|
|
9
9
|
|
|
10
10
|
from kleinkram.config import Config
|
|
11
11
|
from kleinkram.config import CONFIG_PATH
|
|
12
|
-
from kleinkram.config import CorruptedConfigFile
|
|
13
12
|
from kleinkram.config import Credentials
|
|
14
|
-
from kleinkram.config import InvalidConfigFile
|
|
15
13
|
|
|
16
14
|
CLI_CALLBACK_ENDPOINT = "/cli/callback"
|
|
17
15
|
OAUTH_SLUG = "/auth/google?state=cli"
|
kleinkram/commands/download.py
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import logging
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import List
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
8
|
import typer
|
|
9
9
|
from kleinkram.api.client import AuthenticatedClient
|
|
10
|
-
from kleinkram.api.file_transfer import
|
|
11
|
-
from kleinkram.api.routes import get_files_by_file_spec
|
|
10
|
+
from kleinkram.api.file_transfer import download_files
|
|
12
11
|
from kleinkram.config import get_shared_state
|
|
13
|
-
from kleinkram.models import FILE_STATE_COLOR
|
|
14
12
|
from kleinkram.models import files_to_table
|
|
15
|
-
from kleinkram.
|
|
16
|
-
from kleinkram.
|
|
17
|
-
from kleinkram.
|
|
18
|
-
from kleinkram.
|
|
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
|
+
from kleinkram.utils import split_args
|
|
19
18
|
from rich.console import Console
|
|
20
19
|
|
|
21
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
22
23
|
HELP = """\
|
|
23
24
|
Download files from kleinkram.
|
|
24
25
|
"""
|
|
@@ -34,70 +35,69 @@ def download(
|
|
|
34
35
|
files: Optional[List[str]] = typer.Argument(
|
|
35
36
|
None, help="file names, ids or patterns"
|
|
36
37
|
),
|
|
37
|
-
|
|
38
|
-
None, "--project", "-p", help="project
|
|
38
|
+
projects: Optional[List[str]] = typer.Option(
|
|
39
|
+
None, "--project", "-p", help="project names, ids or patterns"
|
|
39
40
|
),
|
|
40
|
-
|
|
41
|
-
None, "--mission", "-m", help="mission
|
|
41
|
+
missions: Optional[List[str]] = typer.Option(
|
|
42
|
+
None, "--mission", "-m", help="mission names, ids or patterns"
|
|
42
43
|
),
|
|
43
44
|
dest: str = typer.Option(prompt="destination", help="local path to save the files"),
|
|
45
|
+
nested: bool = typer.Option(
|
|
46
|
+
False, help="save files in nested directories, project-name/mission-name"
|
|
47
|
+
),
|
|
48
|
+
overwrite: bool = typer.Option(
|
|
49
|
+
False, help="overwrite files if they already exist and don't match the filehash"
|
|
50
|
+
),
|
|
44
51
|
) -> None:
|
|
45
|
-
_files = [to_name_or_uuid(f) for f in files or []]
|
|
46
|
-
_project = to_name_or_uuid(project) if project else None
|
|
47
|
-
_mission = to_name_or_uuid(mission) if mission else None
|
|
48
|
-
|
|
49
52
|
# create destionation directory
|
|
50
53
|
dest_dir = Path(dest)
|
|
51
|
-
|
|
52
54
|
if not dest_dir.exists():
|
|
53
55
|
typer.confirm(f"Destination {dest_dir} does not exist. Create it?", abort=True)
|
|
54
|
-
|
|
55
56
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
# get file spec
|
|
59
|
+
file_ids, file_patterns = split_args(files or [])
|
|
60
|
+
mission_ids, mission_patterns = split_args(missions or [])
|
|
61
|
+
project_ids, project_patterns = split_args(projects or [])
|
|
62
|
+
|
|
63
|
+
project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
|
|
64
|
+
mission_spec = MissionSpec(
|
|
65
|
+
patterns=mission_patterns,
|
|
66
|
+
ids=mission_ids,
|
|
67
|
+
project_spec=project_spec,
|
|
68
|
+
)
|
|
69
|
+
file_spec = FileSpec(
|
|
70
|
+
patterns=file_patterns, ids=file_ids, mission_spec=mission_spec
|
|
71
|
+
)
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
raise ValueError(
|
|
64
|
-
"the files you are trying to download do not have unique names"
|
|
65
|
-
)
|
|
73
|
+
client = AuthenticatedClient()
|
|
74
|
+
parsed_files = get_files_by_spec(client, file_spec)
|
|
66
75
|
|
|
67
|
-
console = Console()
|
|
68
76
|
if get_shared_state().verbose:
|
|
69
77
|
table = files_to_table(parsed_files, title="downloading files...")
|
|
70
|
-
|
|
78
|
+
Console().print(table)
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
except FileExistsError:
|
|
97
|
-
local_hash = b64_md5(dest_dir / file.name)
|
|
98
|
-
if local_hash == file.hash:
|
|
99
|
-
print(f"{file.name} already exists in dest, skipping...")
|
|
100
|
-
else:
|
|
101
|
-
print(f"{file.name} already exists in dest, but has different hash!")
|
|
102
|
-
except Exception as e:
|
|
103
|
-
print(f"Error downloading file {file.name}: {repr(e)}")
|
|
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
|
|
103
|
+
)
|
kleinkram/commands/list.py
CHANGED
|
@@ -5,17 +5,18 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
from kleinkram.api.client import AuthenticatedClient
|
|
8
|
-
from kleinkram.api.routes import get_files_by_file_spec
|
|
9
|
-
from kleinkram.api.routes import get_missions
|
|
10
|
-
from kleinkram.api.routes import get_projects
|
|
11
8
|
from kleinkram.config import get_shared_state
|
|
12
9
|
from kleinkram.models import files_to_table
|
|
13
10
|
from kleinkram.models import missions_to_table
|
|
14
11
|
from kleinkram.models import projects_to_table
|
|
15
|
-
from kleinkram.
|
|
16
|
-
from kleinkram.
|
|
12
|
+
from kleinkram.resources import FileSpec
|
|
13
|
+
from kleinkram.resources import get_files_by_spec
|
|
14
|
+
from kleinkram.resources import get_missions_by_spec
|
|
15
|
+
from kleinkram.resources import get_projects_by_spec
|
|
16
|
+
from kleinkram.resources import MissionSpec
|
|
17
|
+
from kleinkram.resources import ProjectSpec
|
|
18
|
+
from kleinkram.utils import split_args
|
|
17
19
|
from rich.console import Console
|
|
18
|
-
from typer import BadParameter
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
HELP = """\
|
|
@@ -28,75 +29,79 @@ list_typer = typer.Typer(
|
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def _parse_metadata(raw: List[str]) -> dict:
|
|
32
|
-
ret = {}
|
|
33
|
-
for tag in raw:
|
|
34
|
-
if "=" not in tag:
|
|
35
|
-
raise BadParameter("tag must be formatted as `key=value`")
|
|
36
|
-
k, v = tag.split("=")
|
|
37
|
-
ret[k] = v
|
|
38
|
-
return ret
|
|
39
|
-
|
|
40
|
-
|
|
41
32
|
@list_typer.command()
|
|
42
33
|
def files(
|
|
43
34
|
files: Optional[List[str]] = typer.Argument(
|
|
44
|
-
None,
|
|
35
|
+
None,
|
|
36
|
+
help="file names, ids or patterns",
|
|
45
37
|
),
|
|
46
|
-
|
|
38
|
+
projects: Optional[List[str]] = typer.Option(
|
|
47
39
|
None, "--project", "-p", help="project name or id"
|
|
48
40
|
),
|
|
49
|
-
|
|
41
|
+
missions: Optional[List[str]] = typer.Option(
|
|
50
42
|
None, "--mission", "-m", help="mission name or id"
|
|
51
43
|
),
|
|
52
44
|
) -> None:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
file_ids, file_patterns = split_args(files or [])
|
|
46
|
+
mission_ids, mission_patterns = split_args(missions or [])
|
|
47
|
+
project_ids, project_patterns = split_args(projects or [])
|
|
48
|
+
|
|
49
|
+
project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
|
|
50
|
+
mission_spec = MissionSpec(
|
|
51
|
+
project_spec=project_spec,
|
|
52
|
+
ids=mission_ids,
|
|
53
|
+
patterns=mission_patterns,
|
|
54
|
+
)
|
|
55
|
+
file_spec = FileSpec(
|
|
56
|
+
mission_spec=mission_spec, patterns=file_patterns, ids=file_ids
|
|
57
|
+
)
|
|
58
58
|
|
|
59
59
|
client = AuthenticatedClient()
|
|
60
|
-
|
|
61
|
-
parsed_files = get_files_by_file_spec(client, file_spec)
|
|
60
|
+
parsed_files = get_files_by_spec(client, file_spec)
|
|
62
61
|
|
|
63
62
|
if get_shared_state().verbose:
|
|
64
|
-
|
|
65
|
-
console = Console()
|
|
66
|
-
console.print(table)
|
|
63
|
+
Console().print(files_to_table(parsed_files))
|
|
67
64
|
else:
|
|
68
65
|
for file in parsed_files:
|
|
69
66
|
print(file.id)
|
|
70
67
|
|
|
71
68
|
|
|
72
69
|
@list_typer.command()
|
|
73
|
-
def
|
|
70
|
+
def missions(
|
|
71
|
+
projects: Optional[List[str]] = typer.Option(
|
|
72
|
+
None, "--project", "-p", help="project name or id"
|
|
73
|
+
),
|
|
74
|
+
missions: Optional[List[str]] = typer.Argument(None, help="mission names"),
|
|
75
|
+
) -> None:
|
|
76
|
+
mission_ids, mission_patterns = split_args(missions or [])
|
|
77
|
+
project_ids, project_patterns = split_args(projects or [])
|
|
78
|
+
|
|
79
|
+
project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
|
|
80
|
+
mission_spec = MissionSpec(
|
|
81
|
+
ids=mission_ids,
|
|
82
|
+
patterns=mission_patterns,
|
|
83
|
+
project_spec=project_spec,
|
|
84
|
+
)
|
|
85
|
+
|
|
74
86
|
client = AuthenticatedClient()
|
|
75
|
-
|
|
87
|
+
parsed_missions = get_missions_by_spec(client, mission_spec)
|
|
76
88
|
|
|
77
89
|
if get_shared_state().verbose:
|
|
78
|
-
|
|
79
|
-
console = Console()
|
|
80
|
-
console.print(table)
|
|
81
|
-
else:
|
|
82
|
-
for project in projects:
|
|
83
|
-
print(project.id)
|
|
90
|
+
Console().print(missions_to_table(parsed_missions))
|
|
84
91
|
|
|
85
92
|
|
|
86
93
|
@list_typer.command()
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
metadata: Optional[List[str]] = typer.Argument(None, help="tag=value pairs"),
|
|
94
|
+
def projects(
|
|
95
|
+
projects: Optional[List[str]] = typer.Argument(None, help="project names"),
|
|
90
96
|
) -> None:
|
|
91
|
-
|
|
97
|
+
project_ids, project_patterns = split_args(projects or [])
|
|
98
|
+
project_spec = ProjectSpec(patterns=project_patterns, ids=project_ids)
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
client = AuthenticatedClient()
|
|
101
|
+
parsed_projects = get_projects_by_spec(client, project_spec)
|
|
95
102
|
|
|
96
103
|
if get_shared_state().verbose:
|
|
97
|
-
|
|
98
|
-
console = Console()
|
|
99
|
-
console.print(table)
|
|
104
|
+
Console().print(projects_to_table(parsed_projects))
|
|
100
105
|
else:
|
|
101
|
-
for
|
|
102
|
-
print(
|
|
106
|
+
for project in parsed_projects:
|
|
107
|
+
print(project.id)
|
kleinkram/commands/mission.py
CHANGED
|
@@ -5,12 +5,14 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
import typer
|
|
7
7
|
from kleinkram.api.client import AuthenticatedClient
|
|
8
|
-
from kleinkram.api.routes import
|
|
9
|
-
from kleinkram.
|
|
10
|
-
from kleinkram.
|
|
11
|
-
from kleinkram.
|
|
8
|
+
from kleinkram.api.routes import _update_mission_metadata
|
|
9
|
+
from kleinkram.errors import MissionNotFound
|
|
10
|
+
from kleinkram.resources import get_missions_by_spec
|
|
11
|
+
from kleinkram.resources import mission_spec_is_unique
|
|
12
|
+
from kleinkram.resources import MissionSpec
|
|
13
|
+
from kleinkram.resources import ProjectSpec
|
|
12
14
|
from kleinkram.utils import load_metadata
|
|
13
|
-
from kleinkram.utils import
|
|
15
|
+
from kleinkram.utils import split_args
|
|
14
16
|
|
|
15
17
|
mission_typer = typer.Typer(
|
|
16
18
|
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
@@ -32,19 +34,29 @@ def update(
|
|
|
32
34
|
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
33
35
|
metadata: str = typer.Option(help="path to metadata file (json or yaml)"),
|
|
34
36
|
) -> None:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
38
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
project_spec = ProjectSpec(ids=project_ids, patterns=project_patterns)
|
|
41
|
+
mission_spec = MissionSpec(
|
|
42
|
+
ids=mission_ids,
|
|
43
|
+
patterns=mission_patterns,
|
|
44
|
+
project_spec=project_spec,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if not mission_spec_is_unique(mission_spec):
|
|
48
|
+
raise ValueError(f"mission spec is not unique: {mission_spec}")
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
client = AuthenticatedClient()
|
|
51
|
+
missions = get_missions_by_spec(client, mission_spec)
|
|
42
52
|
|
|
43
|
-
if
|
|
44
|
-
raise
|
|
53
|
+
if not missions:
|
|
54
|
+
raise MissionNotFound(f"Mission {mission} does not exist")
|
|
55
|
+
elif len(missions) > 1:
|
|
56
|
+
raise RuntimeError(f"Multiple missions found: {missions}") # unreachable
|
|
45
57
|
|
|
46
58
|
metadata_dct = load_metadata(Path(metadata))
|
|
47
|
-
|
|
59
|
+
_update_mission_metadata(client, missions[0].id, metadata=metadata_dct)
|
|
48
60
|
|
|
49
61
|
|
|
50
62
|
@mission_typer.command(help=NOT_IMPLEMENTED_YET)
|