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/app.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
4
6
|
from enum import Enum
|
|
5
|
-
from
|
|
6
|
-
from typing import Callable
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import List
|
|
8
9
|
from typing import Optional
|
|
9
|
-
from typing import Type
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
12
|
from click import Context
|
|
13
13
|
from kleinkram._version import __version__
|
|
14
14
|
from kleinkram.api.client import AuthenticatedClient
|
|
15
|
-
from kleinkram.api.routes import
|
|
16
|
-
from kleinkram.api.routes import
|
|
15
|
+
from kleinkram.api.routes import _claim_admin
|
|
16
|
+
from kleinkram.api.routes import _get_api_version
|
|
17
17
|
from kleinkram.auth import login_flow
|
|
18
18
|
from kleinkram.commands.download import download_typer
|
|
19
19
|
from kleinkram.commands.endpoint import endpoint_typer
|
|
@@ -24,11 +24,20 @@ from kleinkram.commands.upload import upload_typer
|
|
|
24
24
|
from kleinkram.commands.verify import verify_typer
|
|
25
25
|
from kleinkram.config import Config
|
|
26
26
|
from kleinkram.config import get_shared_state
|
|
27
|
+
from kleinkram.errors import ErrorHandledTyper
|
|
27
28
|
from kleinkram.errors import InvalidCLIVersion
|
|
29
|
+
from kleinkram.utils import format_traceback
|
|
28
30
|
from kleinkram.utils import get_supported_api_version
|
|
29
31
|
from rich.console import Console
|
|
30
32
|
from typer.core import TyperGroup
|
|
31
33
|
|
|
34
|
+
LOG_DIR = Path() / "logs"
|
|
35
|
+
LOG_FILE = LOG_DIR / f"{time.time_ns()}.log"
|
|
36
|
+
LOG_FORMAT = "%(asctime)s | %(name)s | %(levelname)s | %(message)s"
|
|
37
|
+
|
|
38
|
+
# setup default logging
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
32
41
|
|
|
33
42
|
CLI_HELP = """\
|
|
34
43
|
Kleinkram CLI
|
|
@@ -40,6 +49,14 @@ for more information.
|
|
|
40
49
|
"""
|
|
41
50
|
|
|
42
51
|
|
|
52
|
+
class LogLevel(str, Enum):
|
|
53
|
+
DEBUG = "DEBUG"
|
|
54
|
+
INFO = "INFO"
|
|
55
|
+
WARNING = "WARNING"
|
|
56
|
+
ERROR = "ERROR"
|
|
57
|
+
CRITICAL = "CRITICAL"
|
|
58
|
+
|
|
59
|
+
|
|
43
60
|
class CommandTypes(str, Enum):
|
|
44
61
|
AUTH = "Authentication Commands"
|
|
45
62
|
CORE = "Core Commands"
|
|
@@ -52,40 +69,6 @@ class OrderCommands(TyperGroup):
|
|
|
52
69
|
return list(self.commands)
|
|
53
70
|
|
|
54
71
|
|
|
55
|
-
ExceptionHandler = Callable[[Exception], int]
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class ErrorHandledTyper(typer.Typer):
|
|
59
|
-
"""\
|
|
60
|
-
error handlers that are last added will be used first
|
|
61
|
-
"""
|
|
62
|
-
|
|
63
|
-
_error_handlers: OrderedDict[Type[Exception], ExceptionHandler]
|
|
64
|
-
|
|
65
|
-
def error_handler(
|
|
66
|
-
self, exc: type[Exception]
|
|
67
|
-
) -> Callable[[ExceptionHandler], ExceptionHandler]:
|
|
68
|
-
def dec(func: ExceptionHandler) -> ExceptionHandler:
|
|
69
|
-
self._error_handlers[exc] = func
|
|
70
|
-
return func
|
|
71
|
-
|
|
72
|
-
return dec
|
|
73
|
-
|
|
74
|
-
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
75
|
-
super().__init__(*args, **kwargs)
|
|
76
|
-
self._error_handlers = OrderedDict()
|
|
77
|
-
|
|
78
|
-
def __call__(self, *args: Any, **kwargs: Any) -> int:
|
|
79
|
-
try:
|
|
80
|
-
return super().__call__(*args, **kwargs)
|
|
81
|
-
except Exception as e:
|
|
82
|
-
for tp, handler in reversed(self._error_handlers.items()):
|
|
83
|
-
if isinstance(e, tp):
|
|
84
|
-
exit_code = handler(e)
|
|
85
|
-
raise SystemExit(exit_code)
|
|
86
|
-
raise
|
|
87
|
-
|
|
88
|
-
|
|
89
72
|
app = ErrorHandledTyper(
|
|
90
73
|
cls=OrderCommands,
|
|
91
74
|
help=CLI_HELP,
|
|
@@ -93,17 +76,6 @@ app = ErrorHandledTyper(
|
|
|
93
76
|
no_args_is_help=True,
|
|
94
77
|
)
|
|
95
78
|
|
|
96
|
-
|
|
97
|
-
@app.error_handler(Exception)
|
|
98
|
-
def base_handler(exc: Exception) -> int:
|
|
99
|
-
if not get_shared_state().debug:
|
|
100
|
-
console = Console()
|
|
101
|
-
console.print(f"{type(exc).__name__}: {exc}", style="red")
|
|
102
|
-
return 1
|
|
103
|
-
|
|
104
|
-
raise exc
|
|
105
|
-
|
|
106
|
-
|
|
107
79
|
app.add_typer(download_typer, name="download", rich_help_panel=CommandTypes.CORE)
|
|
108
80
|
app.add_typer(upload_typer, name="upload", rich_help_panel=CommandTypes.CORE)
|
|
109
81
|
app.add_typer(verify_typer, name="verify", rich_help_panel=CommandTypes.CORE)
|
|
@@ -113,6 +85,16 @@ app.add_typer(mission_typer, name="mission", rich_help_panel=CommandTypes.CRUD)
|
|
|
113
85
|
app.add_typer(project_typer, name="project", rich_help_panel=CommandTypes.CRUD)
|
|
114
86
|
|
|
115
87
|
|
|
88
|
+
# attach error handler to app
|
|
89
|
+
@app.error_handler(Exception)
|
|
90
|
+
def base_handler(exc: Exception) -> int:
|
|
91
|
+
if not get_shared_state().debug:
|
|
92
|
+
Console(file=sys.stderr).print(f"{type(exc).__name__}: {exc}", style="red")
|
|
93
|
+
logger.error(format_traceback(exc))
|
|
94
|
+
return 1
|
|
95
|
+
raise exc
|
|
96
|
+
|
|
97
|
+
|
|
116
98
|
@app.command(rich_help_panel=CommandTypes.AUTH)
|
|
117
99
|
def login(
|
|
118
100
|
key: Optional[str] = typer.Option(None, help="CLI key"),
|
|
@@ -130,11 +112,11 @@ def logout(all: bool = typer.Option(False, help="logout on all enpoints")) -> No
|
|
|
130
112
|
@app.command(hidden=True)
|
|
131
113
|
def claim():
|
|
132
114
|
client = AuthenticatedClient()
|
|
133
|
-
|
|
115
|
+
_claim_admin(client)
|
|
134
116
|
print("admin rights claimed successfully.")
|
|
135
117
|
|
|
136
118
|
|
|
137
|
-
def
|
|
119
|
+
def _version_callback(value: bool) -> None:
|
|
138
120
|
if value:
|
|
139
121
|
typer.echo(__version__)
|
|
140
122
|
raise typer.Exit()
|
|
@@ -142,7 +124,7 @@ def _version_cb(value: bool) -> None:
|
|
|
142
124
|
|
|
143
125
|
def check_version_compatiblity() -> None:
|
|
144
126
|
cli_version = get_supported_api_version()
|
|
145
|
-
api_version =
|
|
127
|
+
api_version = _get_api_version()
|
|
146
128
|
api_vers_str = ".".join(map(str, api_version))
|
|
147
129
|
|
|
148
130
|
if cli_version[0] != api_version[0]:
|
|
@@ -151,11 +133,9 @@ def check_version_compatiblity() -> None:
|
|
|
151
133
|
)
|
|
152
134
|
|
|
153
135
|
if cli_version[1] != api_version[1]:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
style="red",
|
|
158
|
-
)
|
|
136
|
+
msg = f"CLI version {__version__} might not be compatible with API version {api_vers_str}"
|
|
137
|
+
Console(file=sys.stderr).print(msg, style="red")
|
|
138
|
+
logger.warning(msg)
|
|
159
139
|
|
|
160
140
|
|
|
161
141
|
@app.callback()
|
|
@@ -163,18 +143,35 @@ def cli(
|
|
|
163
143
|
verbose: bool = typer.Option(True, help="Enable verbose mode."),
|
|
164
144
|
debug: bool = typer.Option(False, help="Enable debug mode."),
|
|
165
145
|
version: Optional[bool] = typer.Option(
|
|
166
|
-
None, "--version", "-v", callback=
|
|
146
|
+
None, "--version", "-v", callback=_version_callback
|
|
167
147
|
),
|
|
148
|
+
log_level: Optional[LogLevel] = typer.Option(None, help="Set log level."),
|
|
168
149
|
):
|
|
169
150
|
_ = version # suppress unused variable warning
|
|
170
151
|
shared_state = get_shared_state()
|
|
171
152
|
shared_state.verbose = verbose
|
|
172
153
|
shared_state.debug = debug
|
|
173
154
|
|
|
155
|
+
if shared_state.debug:
|
|
156
|
+
log_level = LogLevel.DEBUG
|
|
157
|
+
|
|
158
|
+
if log_level is not None:
|
|
159
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
level = logging.getLevelName(log_level)
|
|
161
|
+
logging.basicConfig(level=level, filename=LOG_FILE, format=LOG_FORMAT)
|
|
162
|
+
else:
|
|
163
|
+
logging.disable(logging.CRITICAL)
|
|
164
|
+
|
|
165
|
+
logger.info(f"CLI version: {__version__}")
|
|
166
|
+
|
|
174
167
|
try:
|
|
175
168
|
check_version_compatiblity()
|
|
176
|
-
except InvalidCLIVersion:
|
|
169
|
+
except InvalidCLIVersion as e:
|
|
170
|
+
logger.error(format_traceback(e))
|
|
177
171
|
raise
|
|
178
172
|
except Exception:
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
err = ("failed to check version compatibility",)
|
|
174
|
+
Console(file=sys.stderr).print(
|
|
175
|
+
err, style="yellow" if shared_state.verbose else None
|
|
176
|
+
)
|
|
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)
|