kleinkram 0.43.2.dev20250331124109__py3-none-any.whl → 0.58.0.dev20260110152317__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.
- kleinkram/api/client.py +6 -18
- kleinkram/api/deser.py +152 -1
- kleinkram/api/file_transfer.py +202 -101
- kleinkram/api/pagination.py +11 -2
- kleinkram/api/query.py +10 -10
- kleinkram/api/routes.py +192 -59
- kleinkram/auth.py +108 -7
- kleinkram/cli/_action.py +131 -0
- kleinkram/cli/_download.py +8 -19
- kleinkram/cli/_endpoint.py +2 -4
- kleinkram/cli/_file.py +6 -18
- kleinkram/cli/_file_validator.py +125 -0
- kleinkram/cli/_list.py +5 -15
- kleinkram/cli/_mission.py +24 -28
- kleinkram/cli/_project.py +10 -26
- kleinkram/cli/_run.py +220 -0
- kleinkram/cli/_upload.py +58 -26
- kleinkram/cli/_verify.py +59 -16
- kleinkram/cli/app.py +56 -17
- kleinkram/cli/error_handling.py +1 -3
- kleinkram/config.py +6 -21
- kleinkram/core.py +53 -43
- kleinkram/errors.py +12 -0
- kleinkram/models.py +51 -1
- kleinkram/printing.py +229 -18
- kleinkram/utils.py +10 -24
- kleinkram/wrappers.py +54 -30
- {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/METADATA +6 -4
- kleinkram-0.58.0.dev20260110152317.dist-info/RECORD +53 -0
- {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/WHEEL +1 -1
- {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/top_level.txt +0 -1
- {testing → tests}/backend_fixtures.py +27 -3
- tests/conftest.py +1 -1
- tests/generate_test_data.py +314 -0
- tests/test_config.py +2 -6
- tests/test_core.py +11 -31
- tests/test_end_to_end.py +3 -5
- tests/test_fixtures.py +3 -5
- tests/test_printing.py +9 -11
- tests/test_utils.py +1 -3
- tests/test_wrappers.py +9 -27
- kleinkram-0.43.2.dev20250331124109.dist-info/RECORD +0 -50
- testing/__init__.py +0 -0
- {kleinkram-0.43.2.dev20250331124109.dist-info → kleinkram-0.58.0.dev20260110152317.dist-info}/entry_points.txt +0 -0
kleinkram/cli/_action.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
import kleinkram.api.routes
|
|
11
|
+
from kleinkram.api.client import AuthenticatedClient
|
|
12
|
+
from kleinkram.api.query import MissionQuery
|
|
13
|
+
from kleinkram.api.query import ProjectQuery
|
|
14
|
+
from kleinkram.config import get_shared_state
|
|
15
|
+
from kleinkram.printing import print_action_templates_table
|
|
16
|
+
from kleinkram.printing import print_run_info
|
|
17
|
+
from kleinkram.utils import is_valid_uuid4
|
|
18
|
+
from kleinkram.utils import split_args
|
|
19
|
+
|
|
20
|
+
HELP = """\
|
|
21
|
+
Launch kleinkram actions from predefined templates.
|
|
22
|
+
|
|
23
|
+
You can list available action templates, launch new actions on specific missions, and optionally
|
|
24
|
+
follow their logs in real-time.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
action_typer = typer.Typer(
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
30
|
+
help=HELP,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
LIST_HELP = "Lists action templates (definitions). To list individual runs, use `klein run list`."
|
|
34
|
+
GET_HELP = "Get details for a specific action template."
|
|
35
|
+
RUN_HELP = "Launch a new action from a template."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@action_typer.command(help=LIST_HELP, name="list")
|
|
39
|
+
def list_actions() -> None:
|
|
40
|
+
client = AuthenticatedClient()
|
|
41
|
+
templates = list(kleinkram.api.routes.get_action_templates(client))
|
|
42
|
+
|
|
43
|
+
if not templates:
|
|
44
|
+
typer.echo("No action templates found.")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
print_action_templates_table(templates, pprint=get_shared_state().verbose)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@action_typer.command(help=RUN_HELP)
|
|
51
|
+
def run(
|
|
52
|
+
template_name: str = typer.Argument(..., help="Name or ID of the template to run."),
|
|
53
|
+
mission: str = typer.Option(..., "--mission", "-m", help="Mission ID or name to run the action on."),
|
|
54
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID or name (to scope mission)."),
|
|
55
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Follow the logs of the action run."),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Submits an action to run on a specific mission and optionally follows its logs.
|
|
59
|
+
"""
|
|
60
|
+
client = AuthenticatedClient()
|
|
61
|
+
pprint = get_shared_state().verbose
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
project_ids, project_patterns = split_args([project] if project else [])
|
|
65
|
+
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
66
|
+
|
|
67
|
+
mission_ids, mission_patterns = split_args([mission])
|
|
68
|
+
mission_query = MissionQuery(
|
|
69
|
+
ids=mission_ids,
|
|
70
|
+
patterns=mission_patterns,
|
|
71
|
+
project_query=project_query,
|
|
72
|
+
)
|
|
73
|
+
mission_obj = kleinkram.api.routes.get_mission(client, mission_query)
|
|
74
|
+
mission_uuid = mission_obj.id
|
|
75
|
+
except kleinkram.errors.MissionNotFound:
|
|
76
|
+
typer.secho(f"Error: Mission '{mission}' not found.", fg=typer.colors.RED)
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
except kleinkram.errors.InvalidMissionQuery:
|
|
79
|
+
typer.secho(
|
|
80
|
+
"Error: Mission query is ambiguous. Try specifying a project with -p.",
|
|
81
|
+
fg=typer.colors.RED,
|
|
82
|
+
)
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
typer.secho(f"Error resolving mission: {e}", fg=typer.colors.RED)
|
|
86
|
+
raise typer.Exit(code=1)
|
|
87
|
+
|
|
88
|
+
# 2. Resolve Template to UUID
|
|
89
|
+
try:
|
|
90
|
+
if is_valid_uuid4(template_name):
|
|
91
|
+
template_uuid = UUID(template_name)
|
|
92
|
+
else:
|
|
93
|
+
templates = kleinkram.api.routes.get_action_templates(client)
|
|
94
|
+
found_template = next((t for t in templates if t.name == template_name), None)
|
|
95
|
+
|
|
96
|
+
if not found_template:
|
|
97
|
+
typer.secho(
|
|
98
|
+
f"Error: Action template '{template_name}' not found.",
|
|
99
|
+
fg=typer.colors.RED,
|
|
100
|
+
)
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
template_uuid = found_template.uuid
|
|
103
|
+
except Exception as e:
|
|
104
|
+
typer.secho(f"Error resolving template: {e}", fg=typer.colors.RED)
|
|
105
|
+
raise typer.Exit(code=1)
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
action_uuid_str = kleinkram.api.routes.submit_action(client, mission_uuid, template_uuid)
|
|
109
|
+
typer.secho(f"Action submitted. Run ID: {action_uuid_str}", fg=typer.colors.GREEN)
|
|
110
|
+
|
|
111
|
+
except httpx.HTTPStatusError as e:
|
|
112
|
+
typer.secho(f"Error submitting action: {e.response.text}", fg=typer.colors.RED)
|
|
113
|
+
raise typer.Exit(code=1)
|
|
114
|
+
except (KeyError, Exception) as e:
|
|
115
|
+
typer.secho(f"An unexpected error occurred: {e}", fg=typer.colors.RED)
|
|
116
|
+
raise typer.Exit(code=1)
|
|
117
|
+
|
|
118
|
+
if follow:
|
|
119
|
+
exit_code = kleinkram.printing.follow_run_logs(client, action_uuid_str)
|
|
120
|
+
if exit_code != 0:
|
|
121
|
+
raise typer.Exit(code=exit_code)
|
|
122
|
+
|
|
123
|
+
elif pprint:
|
|
124
|
+
# Not following, but in verbose mode. Show run info.
|
|
125
|
+
try:
|
|
126
|
+
time.sleep(0.5) # Give API a moment
|
|
127
|
+
run_details = kleinkram.api.routes.get_run(client, action_uuid_str)
|
|
128
|
+
kleinkram.printing.print_run_info(run_details, pprint=True)
|
|
129
|
+
except Exception:
|
|
130
|
+
# Non-critical, we already printed the ID.
|
|
131
|
+
pass
|
kleinkram/cli/_download.py
CHANGED
|
@@ -22,28 +22,19 @@ Download files from kleinkram.
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
download_typer = typer.Typer(
|
|
26
|
-
name="download", no_args_is_help=True, invoke_without_command=True, help=HELP
|
|
27
|
-
)
|
|
25
|
+
download_typer = typer.Typer(name="download", no_args_is_help=True, invoke_without_command=True, help=HELP)
|
|
28
26
|
|
|
29
27
|
|
|
30
28
|
@download_typer.callback()
|
|
31
29
|
def download(
|
|
32
|
-
files: Optional[List[str]] = typer.Argument(
|
|
33
|
-
|
|
34
|
-
),
|
|
35
|
-
projects: Optional[List[str]] = typer.Option(
|
|
36
|
-
None, "--project", "-p", help="project names, ids or patterns"
|
|
37
|
-
),
|
|
38
|
-
missions: Optional[List[str]] = typer.Option(
|
|
39
|
-
None, "--mission", "-m", help="mission names, ids or patterns"
|
|
40
|
-
),
|
|
30
|
+
files: Optional[List[str]] = typer.Argument(None, help="file names, ids or patterns"),
|
|
31
|
+
projects: Optional[List[str]] = typer.Option(None, "--project", "-p", help="project names, ids or patterns"),
|
|
32
|
+
missions: Optional[List[str]] = typer.Option(None, "--mission", "-m", help="mission names, ids or patterns"),
|
|
41
33
|
dest: str = typer.Option(prompt="destination", help="local path to save the files"),
|
|
42
|
-
nested: bool = typer.Option(
|
|
43
|
-
False, help="save files in nested directories, project-name/mission-name"
|
|
44
|
-
),
|
|
34
|
+
nested: bool = typer.Option(False, help="save files in nested directories, project-name/mission-name"),
|
|
45
35
|
overwrite: bool = typer.Option(
|
|
46
|
-
False,
|
|
36
|
+
False,
|
|
37
|
+
help="overwrite files if they already exist and don't match the file size or file hash",
|
|
47
38
|
),
|
|
48
39
|
) -> None:
|
|
49
40
|
# create destionation directory
|
|
@@ -63,9 +54,7 @@ def download(
|
|
|
63
54
|
ids=mission_ids,
|
|
64
55
|
project_query=project_query,
|
|
65
56
|
)
|
|
66
|
-
file_query = FileQuery(
|
|
67
|
-
patterns=file_patterns, ids=file_ids, mission_query=mission_query
|
|
68
|
-
)
|
|
57
|
+
file_query = FileQuery(patterns=file_patterns, ids=file_ids, mission_query=mission_query)
|
|
69
58
|
|
|
70
59
|
kleinkram.core.download(
|
|
71
60
|
client=AuthenticatedClient(),
|
kleinkram/cli/_endpoint.py
CHANGED
|
@@ -18,7 +18,7 @@ from kleinkram.config import get_config
|
|
|
18
18
|
from kleinkram.config import select_endpoint
|
|
19
19
|
|
|
20
20
|
HELP = """\
|
|
21
|
-
|
|
21
|
+
Switch between different Kleinkram hosting.
|
|
22
22
|
|
|
23
23
|
The endpoint is used to determine the API server to connect to\
|
|
24
24
|
(default is the API server of https://datasets.leggedrobotics.com).
|
|
@@ -50,9 +50,7 @@ def endpoint(
|
|
|
50
50
|
console.print(f"Endpoint {name} not found.\n", style="red")
|
|
51
51
|
console.print(endpoint_table(config))
|
|
52
52
|
elif not (name and api and s3):
|
|
53
|
-
raise typer.BadParameter(
|
|
54
|
-
"to add a new endpoint you must specify the api and s3 endpoints"
|
|
55
|
-
)
|
|
53
|
+
raise typer.BadParameter("to add a new endpoint you must specify the api and s3 endpoints")
|
|
56
54
|
else:
|
|
57
55
|
new_endpoint = Endpoint(name, api, s3)
|
|
58
56
|
add_endpoint(config, new_endpoint)
|
kleinkram/cli/_file.py
CHANGED
|
@@ -19,19 +19,13 @@ INFO_HELP = "get information about a file"
|
|
|
19
19
|
DELETE_HELP = "delete a file"
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
file_typer = typer.Typer(
|
|
23
|
-
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
24
|
-
)
|
|
22
|
+
file_typer = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
25
23
|
|
|
26
24
|
|
|
27
25
|
@file_typer.command(help=INFO_HELP)
|
|
28
26
|
def info(
|
|
29
|
-
project: Optional[str] = typer.Option(
|
|
30
|
-
|
|
31
|
-
),
|
|
32
|
-
mission: Optional[str] = typer.Option(
|
|
33
|
-
None, "--mission", "-m", help="mission id or name"
|
|
34
|
-
),
|
|
27
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
28
|
+
mission: Optional[str] = typer.Option(None, "--mission", "-m", help="mission id or name"),
|
|
35
29
|
file: str = typer.Option(..., "--file", "-f", help="file id or name"),
|
|
36
30
|
) -> None:
|
|
37
31
|
project_ids, project_patterns = split_args([project] if project else [])
|
|
@@ -58,16 +52,10 @@ def info(
|
|
|
58
52
|
|
|
59
53
|
@file_typer.command(help=DELETE_HELP)
|
|
60
54
|
def delete(
|
|
61
|
-
project: Optional[str] = typer.Option(
|
|
62
|
-
|
|
63
|
-
),
|
|
64
|
-
mission: Optional[str] = typer.Option(
|
|
65
|
-
None, "--mission", "-m", help="mission id or name"
|
|
66
|
-
),
|
|
55
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
56
|
+
mission: Optional[str] = typer.Option(None, "--mission", "-m", help="mission id or name"),
|
|
67
57
|
file: str = typer.Option(..., "--file", "-f", help="file id or name"),
|
|
68
|
-
confirm: bool = typer.Option(
|
|
69
|
-
False, "--confirm", "-y", "--yes", help="confirm deletion"
|
|
70
|
-
),
|
|
58
|
+
confirm: bool = typer.Option(False, "--confirm", "-y", "--yes", help="confirm deletion"),
|
|
71
59
|
) -> None:
|
|
72
60
|
if not confirm:
|
|
73
61
|
typer.confirm(f"delete {project} {mission}", abort=True)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from kleinkram.config import get_shared_state
|
|
11
|
+
from kleinkram.errors import DatatypeNotSupported
|
|
12
|
+
from kleinkram.errors import FileNameNotSupported
|
|
13
|
+
from kleinkram.utils import EXPERIMENTAL_FILE_TYPES
|
|
14
|
+
from kleinkram.utils import SUPPORT_FILE_TYPES
|
|
15
|
+
from kleinkram.utils import check_filename_is_sanatized
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FileValidator:
|
|
20
|
+
"""Encapsulates all file validation logic based on CLI flags."""
|
|
21
|
+
|
|
22
|
+
skip: bool
|
|
23
|
+
experimental_datatypes: bool
|
|
24
|
+
|
|
25
|
+
# Stores (file, reason) for skipped files
|
|
26
|
+
skipped_files: List[tuple[Path, str]] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def filter_files(self, file_paths: List[Path]) -> List[Path]:
|
|
29
|
+
"""
|
|
30
|
+
Validates a list of file paths.
|
|
31
|
+
|
|
32
|
+
- Populates `self.skipped_files` with invalid files.
|
|
33
|
+
- Raises an exception on the first invalid file if `self.skip` is False.
|
|
34
|
+
- Returns a list of valid files to verify.
|
|
35
|
+
"""
|
|
36
|
+
files_to_verify = []
|
|
37
|
+
for file in file_paths:
|
|
38
|
+
try:
|
|
39
|
+
self._validate_path(file)
|
|
40
|
+
files_to_verify.append(file)
|
|
41
|
+
except (
|
|
42
|
+
FileNotFoundError,
|
|
43
|
+
IsADirectoryError,
|
|
44
|
+
DatatypeNotSupported,
|
|
45
|
+
FileNameNotSupported,
|
|
46
|
+
) as e:
|
|
47
|
+
if self.skip:
|
|
48
|
+
self.skipped_files.append((file, str(e)))
|
|
49
|
+
else:
|
|
50
|
+
# Re-raise the exception to be caught by Typer
|
|
51
|
+
self._raise_with_skip_hint(e)
|
|
52
|
+
return files_to_verify
|
|
53
|
+
|
|
54
|
+
def _validate_path(self, file: Path) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Runs a single file through all validation checks.
|
|
57
|
+
Raises an error if any check fails.
|
|
58
|
+
"""
|
|
59
|
+
# 0. Check for existence
|
|
60
|
+
if not file.exists():
|
|
61
|
+
raise FileNotFoundError(f"File not found: {file}")
|
|
62
|
+
|
|
63
|
+
# 1. Check for directories
|
|
64
|
+
if file.is_dir():
|
|
65
|
+
raise IsADirectoryError(f"{file} is a directory")
|
|
66
|
+
|
|
67
|
+
file_suffix = file.suffix.lower()
|
|
68
|
+
is_experimental = file_suffix in EXPERIMENTAL_FILE_TYPES
|
|
69
|
+
is_standard = file_suffix in SUPPORT_FILE_TYPES
|
|
70
|
+
|
|
71
|
+
# 2. Check if the datatype is known at all
|
|
72
|
+
if not is_standard and not is_experimental:
|
|
73
|
+
raise DatatypeNotSupported(f"Unsupported file type '{file_suffix}' on file {file}")
|
|
74
|
+
|
|
75
|
+
# 3. Check if an experimental datatype is allowed
|
|
76
|
+
if is_experimental and not self.experimental_datatypes:
|
|
77
|
+
raise DatatypeNotSupported(f"Experimental datatype '{file_suffix}' not enabled for file {file}")
|
|
78
|
+
|
|
79
|
+
# 4. Check filename
|
|
80
|
+
is_bad_name = not check_filename_is_sanatized(file.stem)
|
|
81
|
+
if is_bad_name:
|
|
82
|
+
raise FileNameNotSupported(f"Badly formed filename for file {file}")
|
|
83
|
+
|
|
84
|
+
def _raise_with_skip_hint(self, e: Exception) -> None:
|
|
85
|
+
"""Re-raises a validation error with a hint to use --skip."""
|
|
86
|
+
base_message = str(e)
|
|
87
|
+
hint = "Use --skip to ignore."
|
|
88
|
+
|
|
89
|
+
if isinstance(e, IsADirectoryError):
|
|
90
|
+
hint = "Use --skip to ignore directories."
|
|
91
|
+
elif isinstance(e, DatatypeNotSupported):
|
|
92
|
+
if "Experimental" in base_message:
|
|
93
|
+
hint = "Use --experimental-datatypes to allow or --skip to ignore."
|
|
94
|
+
elif isinstance(e, FileNameNotSupported):
|
|
95
|
+
hint = "Use --skip to ignore." # No --fix-filenames hint here
|
|
96
|
+
|
|
97
|
+
# Raise a new exception to the same type to preserve the error type
|
|
98
|
+
raise type(e)(f"{base_message}. {hint}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _report_skipped_files(skipped_files: List[tuple[Path, str]]) -> None:
|
|
102
|
+
"""Prints a formatted report of all skipped files."""
|
|
103
|
+
if skipped_files:
|
|
104
|
+
debug = get_shared_state().debug
|
|
105
|
+
|
|
106
|
+
if debug:
|
|
107
|
+
# Full report
|
|
108
|
+
typer.echo(
|
|
109
|
+
typer.style(
|
|
110
|
+
f"--- Skipped {len(skipped_files)} path(s) ---",
|
|
111
|
+
fg=typer.colors.YELLOW,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
for file, reason in skipped_files:
|
|
115
|
+
typer.echo(f"Skipped: {file} (Reason: {reason})")
|
|
116
|
+
typer.echo("---------------------------\n")
|
|
117
|
+
else:
|
|
118
|
+
# Summary report
|
|
119
|
+
typer.echo(
|
|
120
|
+
typer.style(
|
|
121
|
+
f"Skipped {len(skipped_files)} path(s) due to errors. Use `klein --debug [...]` for details.",
|
|
122
|
+
fg=typer.colors.YELLOW,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
typer.echo("")
|
kleinkram/cli/_list.py
CHANGED
|
@@ -23,9 +23,7 @@ List projects, missions, or files.
|
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
list_typer = typer.Typer(
|
|
27
|
-
name="list", invoke_without_command=True, help=HELP, no_args_is_help=True
|
|
28
|
-
)
|
|
26
|
+
list_typer = typer.Typer(name="list", invoke_without_command=True, help=HELP, no_args_is_help=True)
|
|
29
27
|
|
|
30
28
|
|
|
31
29
|
@list_typer.command()
|
|
@@ -34,12 +32,8 @@ def files(
|
|
|
34
32
|
None,
|
|
35
33
|
help="file names, ids or patterns",
|
|
36
34
|
),
|
|
37
|
-
projects: Optional[List[str]] = typer.Option(
|
|
38
|
-
|
|
39
|
-
),
|
|
40
|
-
missions: Optional[List[str]] = typer.Option(
|
|
41
|
-
None, "--mission", "-m", help="mission name or id"
|
|
42
|
-
),
|
|
35
|
+
projects: Optional[List[str]] = typer.Option(None, "--project", "-p", help="project name or id"),
|
|
36
|
+
missions: Optional[List[str]] = typer.Option(None, "--mission", "-m", help="mission name or id"),
|
|
43
37
|
) -> None:
|
|
44
38
|
file_ids, file_patterns = split_args(files or [])
|
|
45
39
|
mission_ids, mission_patterns = split_args(missions or [])
|
|
@@ -51,9 +45,7 @@ def files(
|
|
|
51
45
|
ids=mission_ids,
|
|
52
46
|
patterns=mission_patterns,
|
|
53
47
|
)
|
|
54
|
-
file_query = FileQuery(
|
|
55
|
-
mission_query=mission_query, patterns=file_patterns, ids=file_ids
|
|
56
|
-
)
|
|
48
|
+
file_query = FileQuery(mission_query=mission_query, patterns=file_patterns, ids=file_ids)
|
|
57
49
|
|
|
58
50
|
client = AuthenticatedClient()
|
|
59
51
|
parsed_files = list(get_files(client, file_query=file_query))
|
|
@@ -62,9 +54,7 @@ def files(
|
|
|
62
54
|
|
|
63
55
|
@list_typer.command()
|
|
64
56
|
def missions(
|
|
65
|
-
projects: Optional[List[str]] = typer.Option(
|
|
66
|
-
None, "--project", "-p", help="project name or id"
|
|
67
|
-
),
|
|
57
|
+
projects: Optional[List[str]] = typer.Option(None, "--project", "-p", help="project name or id"),
|
|
68
58
|
missions: Optional[List[str]] = typer.Argument(None, help="mission names"),
|
|
69
59
|
) -> None:
|
|
70
60
|
mission_ids, mission_patterns = split_args(missions or [])
|
kleinkram/cli/_mission.py
CHANGED
|
@@ -13,6 +13,7 @@ from kleinkram.api.query import ProjectQuery
|
|
|
13
13
|
from kleinkram.api.routes import get_mission
|
|
14
14
|
from kleinkram.api.routes import get_project
|
|
15
15
|
from kleinkram.config import get_shared_state
|
|
16
|
+
from kleinkram.errors import InvalidMissionQuery
|
|
16
17
|
from kleinkram.printing import print_mission_info
|
|
17
18
|
from kleinkram.utils import load_metadata
|
|
18
19
|
from kleinkram.utils import split_args
|
|
@@ -25,18 +26,14 @@ NOT_IMPLEMENTED_YET = """\
|
|
|
25
26
|
Not implemented yet, open an issue if you want specific functionality
|
|
26
27
|
"""
|
|
27
28
|
|
|
28
|
-
mission_typer = typer.Typer(
|
|
29
|
-
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
30
|
-
)
|
|
29
|
+
mission_typer = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
31
30
|
|
|
32
31
|
|
|
33
32
|
@mission_typer.command(help=CREATE_HELP)
|
|
34
33
|
def create(
|
|
35
34
|
project: str = typer.Option(..., "--project", "-p", help="project id or name"),
|
|
36
35
|
mission_name: str = typer.Option(..., "--mission", "-m", help="mission name"),
|
|
37
|
-
metadata: Optional[str] = typer.Option(
|
|
38
|
-
None, help="path to metadata file (json or yaml)"
|
|
39
|
-
),
|
|
36
|
+
metadata: Optional[str] = typer.Option(None, help="path to metadata file (json or yaml)"),
|
|
40
37
|
ignore_missing_tags: bool = typer.Option(False, help="ignore mission tags"),
|
|
41
38
|
) -> None:
|
|
42
39
|
project_ids, project_patterns = split_args([project] if project else [])
|
|
@@ -45,13 +42,16 @@ def create(
|
|
|
45
42
|
metadata_dct = load_metadata(Path(metadata)) if metadata else {} # noqa
|
|
46
43
|
|
|
47
44
|
client = AuthenticatedClient()
|
|
48
|
-
|
|
45
|
+
project = get_project(client, project_query, exact_match=True)
|
|
46
|
+
project_id = project.id
|
|
47
|
+
project_required_tags = project.required_tags
|
|
49
48
|
mission_id = kleinkram.api.routes._create_mission(
|
|
50
49
|
client,
|
|
51
50
|
project_id,
|
|
52
51
|
mission_name,
|
|
53
52
|
metadata=metadata_dct,
|
|
54
53
|
ignore_missing_tags=ignore_missing_tags,
|
|
54
|
+
required_tags=project_required_tags,
|
|
55
55
|
)
|
|
56
56
|
|
|
57
57
|
mission_parsed = get_mission(client, MissionQuery(ids=[mission_id]))
|
|
@@ -60,9 +60,7 @@ def create(
|
|
|
60
60
|
|
|
61
61
|
@mission_typer.command(help=INFO_HELP)
|
|
62
62
|
def info(
|
|
63
|
-
project: Optional[str] = typer.Option(
|
|
64
|
-
None, "--project", "-p", help="project id or name"
|
|
65
|
-
),
|
|
63
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
66
64
|
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
67
65
|
) -> None:
|
|
68
66
|
mission_ids, mission_patterns = split_args([mission])
|
|
@@ -82,9 +80,7 @@ def info(
|
|
|
82
80
|
|
|
83
81
|
@mission_typer.command(help=UPDATE_HELP)
|
|
84
82
|
def update(
|
|
85
|
-
project: Optional[str] = typer.Option(
|
|
86
|
-
None, "--project", "-p", help="project id or name"
|
|
87
|
-
),
|
|
83
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
88
84
|
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
89
85
|
metadata: str = typer.Option(help="path to metadata file (json or yaml)"),
|
|
90
86
|
) -> None:
|
|
@@ -102,9 +98,7 @@ def update(
|
|
|
102
98
|
|
|
103
99
|
client = AuthenticatedClient()
|
|
104
100
|
mission_id = get_mission(client, mission_query).id
|
|
105
|
-
kleinkram.core.update_mission(
|
|
106
|
-
client=client, mission_id=mission_id, metadata=metadata_dct
|
|
107
|
-
)
|
|
101
|
+
kleinkram.core.update_mission(client=client, mission_id=mission_id, metadata=metadata_dct)
|
|
108
102
|
|
|
109
103
|
mission_parsed = get_mission(client, mission_query)
|
|
110
104
|
print_mission_info(mission_parsed, pprint=get_shared_state().verbose)
|
|
@@ -112,17 +106,10 @@ def update(
|
|
|
112
106
|
|
|
113
107
|
@mission_typer.command(help=DELETE_HELP)
|
|
114
108
|
def delete(
|
|
115
|
-
project: Optional[str] = typer.Option(
|
|
116
|
-
None, "--project", "-p", help="project id or name"
|
|
117
|
-
),
|
|
109
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
118
110
|
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
119
|
-
confirm: bool = typer.Option(
|
|
120
|
-
False, "--confirm", "-y", "--yes", help="confirm deletion"
|
|
121
|
-
),
|
|
111
|
+
confirm: bool = typer.Option(False, "--confirm", "-y", "--yes", help="confirm deletion"),
|
|
122
112
|
) -> None:
|
|
123
|
-
if not confirm:
|
|
124
|
-
typer.confirm(f"delete {project} {mission}", abort=True)
|
|
125
|
-
|
|
126
113
|
project_ids, project_patterns = split_args([project] if project else [])
|
|
127
114
|
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
128
115
|
|
|
@@ -132,17 +119,26 @@ def delete(
|
|
|
132
119
|
patterns=mission_patterns,
|
|
133
120
|
project_query=project_query,
|
|
134
121
|
)
|
|
122
|
+
if mission_patterns and not (project_patterns or project_ids):
|
|
123
|
+
raise InvalidMissionQuery(
|
|
124
|
+
"Mission query does not uniquely determine mission. "
|
|
125
|
+
"Project name or id must be specified when deleting by mission name"
|
|
126
|
+
)
|
|
135
127
|
|
|
136
128
|
client = AuthenticatedClient()
|
|
137
129
|
mission_parsed = get_mission(client, mission_query)
|
|
130
|
+
if not confirm:
|
|
131
|
+
if project:
|
|
132
|
+
typer.confirm(f"delete {project} {mission}", abort=True)
|
|
133
|
+
else:
|
|
134
|
+
typer.confirm(f"delete {mission_parsed.name} {mission}", abort=True)
|
|
135
|
+
|
|
138
136
|
kleinkram.core.delete_mission(client=client, mission_id=mission_parsed.id)
|
|
139
137
|
|
|
140
138
|
|
|
141
139
|
@mission_typer.command(help=NOT_IMPLEMENTED_YET)
|
|
142
140
|
def prune(
|
|
143
|
-
project: Optional[str] = typer.Option(
|
|
144
|
-
None, "--project", "-p", help="project id or name"
|
|
145
|
-
),
|
|
141
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="project id or name"),
|
|
146
142
|
mission: str = typer.Option(..., "--mission", "-m", help="mission id or name"),
|
|
147
143
|
) -> None:
|
|
148
144
|
"""\
|
kleinkram/cli/_project.py
CHANGED
|
@@ -13,9 +13,7 @@ from kleinkram.config import get_shared_state
|
|
|
13
13
|
from kleinkram.printing import print_project_info
|
|
14
14
|
from kleinkram.utils import split_args
|
|
15
15
|
|
|
16
|
-
project_typer = typer.Typer(
|
|
17
|
-
no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]}
|
|
18
|
-
)
|
|
16
|
+
project_typer = typer.Typer(no_args_is_help=True, context_settings={"help_option_names": ["-h", "--help"]})
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
NOT_IMPLEMENTED_YET = """\
|
|
@@ -31,9 +29,7 @@ DELETE_HELP = "delete a project"
|
|
|
31
29
|
@project_typer.command(help=CREATE_HELP)
|
|
32
30
|
def create(
|
|
33
31
|
project: str = typer.Option(..., "--project", "-p", help="project name"),
|
|
34
|
-
description: str = typer.Option(
|
|
35
|
-
..., "--description", "-d", help="project description"
|
|
36
|
-
),
|
|
32
|
+
description: str = typer.Option(..., "--description", "-d", help="project description"),
|
|
37
33
|
) -> None:
|
|
38
34
|
client = AuthenticatedClient()
|
|
39
35
|
project_id = kleinkram.api.routes._create_project(client, project, description)
|
|
@@ -43,9 +39,7 @@ def create(
|
|
|
43
39
|
|
|
44
40
|
|
|
45
41
|
@project_typer.command(help=INFO_HELP)
|
|
46
|
-
def info(
|
|
47
|
-
project: str = typer.Option(..., "--project", "-p", help="project id or name")
|
|
48
|
-
) -> None:
|
|
42
|
+
def info(project: str = typer.Option(..., "--project", "-p", help="project id or name")) -> None:
|
|
49
43
|
project_ids, project_patterns = split_args([project])
|
|
50
44
|
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
51
45
|
|
|
@@ -57,40 +51,30 @@ def info(
|
|
|
57
51
|
@project_typer.command(help=UPDATE_HELP)
|
|
58
52
|
def update(
|
|
59
53
|
project: str = typer.Option(..., "--project", "-p", help="project id or name"),
|
|
60
|
-
description: Optional[str] = typer.Option(
|
|
61
|
-
|
|
62
|
-
),
|
|
63
|
-
new_name: Optional[str] = typer.Option(
|
|
64
|
-
None, "--new-name", "-n", "--name", help="new project name"
|
|
65
|
-
),
|
|
54
|
+
description: Optional[str] = typer.Option(None, "--description", "-d", help="project description"),
|
|
55
|
+
new_name: Optional[str] = typer.Option(None, "--new-name", "-n", "--name", help="new project name"),
|
|
66
56
|
) -> None:
|
|
67
57
|
if description is None and new_name is None:
|
|
68
|
-
raise typer.BadParameter(
|
|
69
|
-
"nothing to update, provide --description or --new-name"
|
|
70
|
-
)
|
|
58
|
+
raise typer.BadParameter("nothing to update, provide --description or --new-name")
|
|
71
59
|
|
|
72
60
|
project_ids, project_patterns = split_args([project])
|
|
73
61
|
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
74
62
|
|
|
75
63
|
client = AuthenticatedClient()
|
|
76
|
-
project_id = get_project(client=client, query=project_query).id
|
|
77
|
-
kleinkram.core.update_project(
|
|
78
|
-
client=client, project_id=project_id, description=description, new_name=new_name
|
|
79
|
-
)
|
|
64
|
+
project_id = get_project(client=client, query=project_query, exact_match=True).id
|
|
65
|
+
kleinkram.core.update_project(client=client, project_id=project_id, description=description, new_name=new_name)
|
|
80
66
|
|
|
81
67
|
project_parsed = get_project(client, ProjectQuery(ids=[project_id]))
|
|
82
68
|
print_project_info(project_parsed, pprint=get_shared_state().verbose)
|
|
83
69
|
|
|
84
70
|
|
|
85
71
|
@project_typer.command(help=DELETE_HELP)
|
|
86
|
-
def delete(
|
|
87
|
-
project: str = typer.Option(..., "--project", "-p", help="project id or name")
|
|
88
|
-
) -> None:
|
|
72
|
+
def delete(project: str = typer.Option(..., "--project", "-p", help="project id or name")) -> None:
|
|
89
73
|
project_ids, project_patterns = split_args([project])
|
|
90
74
|
project_query = ProjectQuery(ids=project_ids, patterns=project_patterns)
|
|
91
75
|
|
|
92
76
|
client = AuthenticatedClient()
|
|
93
|
-
project_id = get_project(client=client, query=project_query).id
|
|
77
|
+
project_id = get_project(client=client, query=project_query, exact_match=True).id
|
|
94
78
|
kleinkram.core.delete_project(client=client, project_id=project_id)
|
|
95
79
|
|
|
96
80
|
|