cmdbox-cli 1.0.0__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.
- cmdbox/__init__.py +0 -0
- cmdbox/cli/__init__.py +0 -0
- cmdbox/cli/app.py +125 -0
- cmdbox/cli/commands/__init__.py +0 -0
- cmdbox/cli/commands/alias_fallback.py +102 -0
- cmdbox/cli/commands/command_crud.py +429 -0
- cmdbox/cli/commands/command_run.py +255 -0
- cmdbox/cli/commands/history.py +109 -0
- cmdbox/cli/commands/init.py +54 -0
- cmdbox/cli/commands/settings.py +62 -0
- cmdbox/cli/commands/tag_crud.py +277 -0
- cmdbox/cli/commands/variable_crud.py +349 -0
- cmdbox/cli/common/__init__.py +0 -0
- cmdbox/cli/common/errors.py +58 -0
- cmdbox/cli/common/update_fields.py +88 -0
- cmdbox/cli/completions/__init__.py +0 -0
- cmdbox/cli/completions/commands.py +26 -0
- cmdbox/cli/completions/fields.py +31 -0
- cmdbox/cli/completions/tags.py +24 -0
- cmdbox/cli/completions/variables.py +26 -0
- cmdbox/cli/handlers/__init__.py +0 -0
- cmdbox/cli/handlers/command_handlers.py +357 -0
- cmdbox/cli/handlers/common_handlers.py +15 -0
- cmdbox/cli/handlers/history_handlers.py +94 -0
- cmdbox/cli/handlers/init_handler.py +127 -0
- cmdbox/cli/handlers/run_handler.py +178 -0
- cmdbox/cli/handlers/settings_handler.py +59 -0
- cmdbox/cli/handlers/tag_handlers.py +220 -0
- cmdbox/cli/handlers/variable_handlers.py +272 -0
- cmdbox/cli/prompts/__init__.py +0 -0
- cmdbox/cli/prompts/completers.py +161 -0
- cmdbox/cli/prompts/prompts.py +108 -0
- cmdbox/cli/prompts/validators.py +46 -0
- cmdbox/cli/ui/__init__.py +0 -0
- cmdbox/cli/ui/console.py +31 -0
- cmdbox/cli/ui/editor.py +141 -0
- cmdbox/cli/ui/presenters/__init__.py +0 -0
- cmdbox/cli/ui/presenters/app_presenter.py +8 -0
- cmdbox/cli/ui/presenters/command_presenter.py +168 -0
- cmdbox/cli/ui/presenters/history_presenter.py +83 -0
- cmdbox/cli/ui/presenters/init_instructions.py +52 -0
- cmdbox/cli/ui/presenters/init_presenter.py +57 -0
- cmdbox/cli/ui/presenters/result_presenter.py +144 -0
- cmdbox/cli/ui/presenters/settings_presenter.py +130 -0
- cmdbox/cli/ui/presenters/tag_presenter.py +97 -0
- cmdbox/cli/ui/presenters/variable_presenter.py +103 -0
- cmdbox/cli/ui/primitives.py +410 -0
- cmdbox/cli/ui/theme.py +43 -0
- cmdbox/cli/ui/theme_builder.py +49 -0
- cmdbox/common/__init__.py +0 -0
- cmdbox/common/io.py +34 -0
- cmdbox/container.py +156 -0
- cmdbox/core/__init__.py +0 -0
- cmdbox/core/fields.py +48 -0
- cmdbox/core/paths.py +52 -0
- cmdbox/database.py +65 -0
- cmdbox/exceptions.py +10 -0
- cmdbox/init/__init__.py +0 -0
- cmdbox/init/detect.py +82 -0
- cmdbox/init/integrations/bash.sh +10 -0
- cmdbox/init/integrations/cmd.bat +14 -0
- cmdbox/init/integrations/fish.fish +11 -0
- cmdbox/init/integrations/powershell.ps1 +14 -0
- cmdbox/init/integrations/zsh.sh +10 -0
- cmdbox/init/io.py +68 -0
- cmdbox/init/specs.py +54 -0
- cmdbox/logging_setup/__init__.py +0 -0
- cmdbox/logging_setup/log_config.py +123 -0
- cmdbox/logging_setup/log_decorators.py +40 -0
- cmdbox/logging_setup/log_handlers.py +94 -0
- cmdbox/migrations/__init__.py +1 -0
- cmdbox/migrations/errors.py +10 -0
- cmdbox/migrations/runner.py +127 -0
- cmdbox/migrations/versions/__init__.py +0 -0
- cmdbox/models.py +165 -0
- cmdbox/repositories/__init__.py +0 -0
- cmdbox/repositories/base_repository.py +181 -0
- cmdbox/repositories/command_repository.py +391 -0
- cmdbox/repositories/errors.py +120 -0
- cmdbox/repositories/history_repository.py +155 -0
- cmdbox/repositories/results.py +37 -0
- cmdbox/repositories/tag_repository.py +91 -0
- cmdbox/repositories/validators.py +256 -0
- cmdbox/repositories/variable_repository.py +324 -0
- cmdbox/resolve/__init__.py +0 -0
- cmdbox/resolve/errors.py +65 -0
- cmdbox/resolve/lookup.py +137 -0
- cmdbox/resolve/resolver.py +402 -0
- cmdbox/resolve/type_defs.py +96 -0
- cmdbox/runtime/__init__.py +0 -0
- cmdbox/runtime/executor.py +454 -0
- cmdbox/runtime/results.py +25 -0
- cmdbox/runtime/shell.py +90 -0
- cmdbox/services/__init__.py +0 -0
- cmdbox/services/command_services.py +261 -0
- cmdbox/services/errors.py +37 -0
- cmdbox/services/field_selection.py +162 -0
- cmdbox/services/history_service.py +68 -0
- cmdbox/services/run_service.py +204 -0
- cmdbox/services/tag_services.py +134 -0
- cmdbox/services/variable_services.py +224 -0
- cmdbox/settings/__init__.py +0 -0
- cmdbox/settings/models.py +129 -0
- cmdbox/settings/settings_repository.py +36 -0
- cmdbox/settings/settings_service.py +144 -0
- cmdbox/version.py +1 -0
- cmdbox_cli-1.0.0.dist-info/METADATA +125 -0
- cmdbox_cli-1.0.0.dist-info/RECORD +112 -0
- cmdbox_cli-1.0.0.dist-info/WHEEL +5 -0
- cmdbox_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cmdbox_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- cmdbox_cli-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
8
|
+
from cmdbox.cli.ui.presenters.init_presenter import (
|
|
9
|
+
render_install_instructions,
|
|
10
|
+
render_install_success,
|
|
11
|
+
render_shell_output,
|
|
12
|
+
)
|
|
13
|
+
from cmdbox.init.detect import detect_shell
|
|
14
|
+
from cmdbox.init.io import load_integration_text, upsert_marked_block
|
|
15
|
+
from cmdbox.init.specs import SHELLS
|
|
16
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@log_action(__name__, "run_init_command")
|
|
20
|
+
def run_init_command(
|
|
21
|
+
*,
|
|
22
|
+
shell: str = None,
|
|
23
|
+
install: bool = False,
|
|
24
|
+
path: str = None,
|
|
25
|
+
get_console: Callable[[], ConsoleUI],
|
|
26
|
+
) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Executes the initialization command for shell integration, handling snippet output,
|
|
29
|
+
installation, and providing specific configurations based on the operating system shell.
|
|
30
|
+
|
|
31
|
+
This function manages different installation modes (e.g., updating profile files,
|
|
32
|
+
writing integration files, or providing wrapper hints) and facilitates smooth integration
|
|
33
|
+
of the application with the user's shell environment.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
shell (str): The shell name provided for integration. Must match one of the expected keys
|
|
37
|
+
in the `SHELLS` dictionary (case-insensitive).
|
|
38
|
+
install (bool): Specifies whether to install the integration snippet to the shell's
|
|
39
|
+
configuration file or output the snippet to stdout. Defaults to `False`.
|
|
40
|
+
path (str, optional): A custom path to the file to which the integration snippet
|
|
41
|
+
will be written when `install` is `True`. If not provided, defaults to the
|
|
42
|
+
path determined by the shell specification.
|
|
43
|
+
get_console (Callable[[], ConsoleUI]): A callable returning an instance of `ConsoleUI`
|
|
44
|
+
to handle console output and user feedback.
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
typer.BadParameter: If an invalid shell name is provided that does not exist in the
|
|
48
|
+
`SHELLS` dictionary.
|
|
49
|
+
"""
|
|
50
|
+
console = get_console()
|
|
51
|
+
|
|
52
|
+
if not shell:
|
|
53
|
+
shell = detect_shell()
|
|
54
|
+
|
|
55
|
+
shell_key = shell.lower()
|
|
56
|
+
if shell_key not in SHELLS:
|
|
57
|
+
raise typer.BadParameter(f"Invalid shell: {shell}")
|
|
58
|
+
|
|
59
|
+
spec = SHELLS[shell_key]
|
|
60
|
+
snippet = load_integration_text(spec.filename)
|
|
61
|
+
|
|
62
|
+
if not install:
|
|
63
|
+
title = f"Install instructions for {spec.name}"
|
|
64
|
+
console.print(
|
|
65
|
+
render_install_instructions(
|
|
66
|
+
snippet, shell=shell, title=title, include_help_text=shell != "cmd"
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
target = (
|
|
72
|
+
Path(path)
|
|
73
|
+
if path
|
|
74
|
+
else (spec.default_path_fn() if spec.default_path_fn else None)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if spec.install_mode == "profile_block":
|
|
78
|
+
if target is None:
|
|
79
|
+
raise typer.BadParameter(
|
|
80
|
+
f"No default install path available for shell: {spec.name}."
|
|
81
|
+
)
|
|
82
|
+
upsert_marked_block(target, snippet)
|
|
83
|
+
console.print(render_install_success())
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if spec.install_mode == "write_file":
|
|
87
|
+
if target is None:
|
|
88
|
+
raise typer.BadParameter(
|
|
89
|
+
f"No default install path available for shell: {spec.name}."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if target.exists():
|
|
93
|
+
backup = target.with_suffix(target.suffix + ".bak")
|
|
94
|
+
shutil.copy2(target, backup)
|
|
95
|
+
|
|
96
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
target.write_text(snippet, encoding="utf-8")
|
|
98
|
+
console.print(render_install_success())
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if spec.install_mode == "wrapper_hint":
|
|
102
|
+
title = "Unable to install snippet for cmd shell"
|
|
103
|
+
console.print(
|
|
104
|
+
render_install_instructions(
|
|
105
|
+
snippet, shell="cmd", title=title, include_help_text=False
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@log_action(__name__, "run_detect_shell")
|
|
112
|
+
def run_detect_shell(*, get_console: Callable[[], ConsoleUI]) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Detects the current shell being used and prints it to the console.
|
|
115
|
+
|
|
116
|
+
This function retrieves a `ConsoleUI` object using the provided `get_console`
|
|
117
|
+
callable. It then detects the current shell and sends a message with the name
|
|
118
|
+
of the detected shell to be displayed on the console.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
get_console (Callable[[], ConsoleUI]): A callable that, when invoked,
|
|
122
|
+
returns an instance of a `ConsoleUI` object used for displaying the
|
|
123
|
+
detected shell information.
|
|
124
|
+
"""
|
|
125
|
+
console = get_console()
|
|
126
|
+
shell = detect_shell()
|
|
127
|
+
console.print(render_shell_output(shell))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Callable
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cmdbox.cli.prompts.prompts import prompt_for_missing_var
|
|
8
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
9
|
+
from cmdbox.cli.ui.presenters.result_presenter import (
|
|
10
|
+
render_execution_result,
|
|
11
|
+
render_preview_result,
|
|
12
|
+
)
|
|
13
|
+
from cmdbox.runtime.executor import RunContext
|
|
14
|
+
from cmdbox.services.run_service import RunService
|
|
15
|
+
from cmdbox.settings.models import Settings
|
|
16
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
17
|
+
|
|
18
|
+
log = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=False)
|
|
22
|
+
class RawRunContext:
|
|
23
|
+
"""
|
|
24
|
+
This dataclass is effectively the same as the actual RunContext
|
|
25
|
+
except that it takes a different 'pre-parsed' version of the
|
|
26
|
+
env argument. This class takes the argument in a format that
|
|
27
|
+
can be supplied by the user, which must then be parsed into a
|
|
28
|
+
format that can be used by the RunContext.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
cwd: str | None = None
|
|
32
|
+
env: list[str] | str | None = None
|
|
33
|
+
capture: bool = False
|
|
34
|
+
shell: str | None = None
|
|
35
|
+
timeout: int | None = None
|
|
36
|
+
emit: bool = False
|
|
37
|
+
verbose: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@log_action(__name__, "run_run_command")
|
|
41
|
+
def run_run_command(
|
|
42
|
+
*,
|
|
43
|
+
alias: str,
|
|
44
|
+
runtime_vars: dict[str, str] | None = None,
|
|
45
|
+
run_ctx: RawRunContext | None = None,
|
|
46
|
+
get_run_service: Callable[[], RunService],
|
|
47
|
+
get_settings: Callable[[], Settings],
|
|
48
|
+
get_console: Callable[[], ConsoleUI],
|
|
49
|
+
):
|
|
50
|
+
if run_ctx and run_ctx.verbose is None:
|
|
51
|
+
settings = get_settings()
|
|
52
|
+
run_ctx.verbose = settings.execution_settings.default_verbose
|
|
53
|
+
|
|
54
|
+
run_service = get_run_service()
|
|
55
|
+
|
|
56
|
+
missing = run_service.collect_missing_vars(alias, runtime_vars=runtime_vars)
|
|
57
|
+
if missing:
|
|
58
|
+
for var_name in missing:
|
|
59
|
+
value = prompt_for_missing_var(var_name)
|
|
60
|
+
runtime_vars[var_name] = value
|
|
61
|
+
|
|
62
|
+
run_ctx = get_run_ctx(run_ctx) if run_ctx else RunContext()
|
|
63
|
+
ex_result = run_service.run(alias, ctx=run_ctx, runtime_vars=runtime_vars)
|
|
64
|
+
if run_ctx.verbose and not run_ctx.emit:
|
|
65
|
+
console = get_console()
|
|
66
|
+
console.print(render_execution_result(ex_result))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@log_action(__name__, "run_preview_command")
|
|
70
|
+
def run_preview_command(
|
|
71
|
+
*,
|
|
72
|
+
alias: str,
|
|
73
|
+
runtime_vars: dict[str, str] | None = None,
|
|
74
|
+
run_ctx: RawRunContext | None = None,
|
|
75
|
+
get_run_service: Callable[[], RunService],
|
|
76
|
+
get_settings: Callable[[], Settings],
|
|
77
|
+
get_console: Callable[[], ConsoleUI],
|
|
78
|
+
):
|
|
79
|
+
if run_ctx and run_ctx.verbose is None:
|
|
80
|
+
settings = get_settings()
|
|
81
|
+
run_ctx.verbose = settings.execution_settings.default_verbose
|
|
82
|
+
|
|
83
|
+
run_service = get_run_service()
|
|
84
|
+
|
|
85
|
+
missing = run_service.collect_missing_vars(alias, runtime_vars=runtime_vars)
|
|
86
|
+
if missing:
|
|
87
|
+
for var_name in missing:
|
|
88
|
+
value = prompt_for_missing_var(var_name)
|
|
89
|
+
runtime_vars[var_name] = value
|
|
90
|
+
|
|
91
|
+
run_ctx = get_run_ctx(run_ctx) if run_ctx else RunContext()
|
|
92
|
+
prev_result, effective_ctx = run_service.preview(
|
|
93
|
+
alias, runtime_vars=runtime_vars, ctx=run_ctx
|
|
94
|
+
)
|
|
95
|
+
rendered_result = render_preview_result(prev_result, ctx=effective_ctx)
|
|
96
|
+
console = get_console()
|
|
97
|
+
console.print(rendered_result)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_run_ctx(raw_run_ctx: RawRunContext | None) -> RunContext:
|
|
101
|
+
"""
|
|
102
|
+
Creates and returns a `RunContext` object based on the provided `raw_run_ctx`.
|
|
103
|
+
|
|
104
|
+
If the `raw_run_ctx` is not provided (i.e., is None), a default `RunContext` is
|
|
105
|
+
created and returned. If `raw_run_ctx` is provided, its properties are used to
|
|
106
|
+
initialize the `RunContext`, and its environment is parsed before being passed
|
|
107
|
+
to the new context.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
raw_run_ctx: An instance of `RawRunContext` or None. If provided, it must
|
|
111
|
+
contain context information such as current working directory, shell
|
|
112
|
+
configuration, and environment variables.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
RunContext: A `RunContext` object constructed using the provided
|
|
116
|
+
`raw_run_ctx` or with default values if no `raw_run_ctx` is provided.
|
|
117
|
+
"""
|
|
118
|
+
if raw_run_ctx is None:
|
|
119
|
+
return RunContext()
|
|
120
|
+
|
|
121
|
+
env = parse_env(raw_run_ctx.env)
|
|
122
|
+
return RunContext(
|
|
123
|
+
cwd=raw_run_ctx.cwd,
|
|
124
|
+
env=env,
|
|
125
|
+
capture=raw_run_ctx.capture,
|
|
126
|
+
shell=raw_run_ctx.shell,
|
|
127
|
+
timeout=raw_run_ctx.timeout,
|
|
128
|
+
emit=raw_run_ctx.emit,
|
|
129
|
+
verbose=raw_run_ctx.verbose,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def parse_env(env: list[str] | str | None) -> dict[str, str] | None:
|
|
134
|
+
"""
|
|
135
|
+
Parses a list, string, or None value into a dictionary where each key-value pair
|
|
136
|
+
represents an environment variable, with the key and value extracted from the
|
|
137
|
+
input format. If the input is None, returns None.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
env (list[str] | str | None): A list of strings, a comma-separated string,
|
|
141
|
+
or None, where each string is in the format "key=value".
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
dict[str, str] | None: A dictionary containing the parsed environment
|
|
145
|
+
variables as key-value pairs, or None if the input is None.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
typer.BadParameter: If an entry in the input does not follow the
|
|
149
|
+
"key=value" format.
|
|
150
|
+
"""
|
|
151
|
+
if env is None:
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
ret = {}
|
|
155
|
+
|
|
156
|
+
if isinstance(env, str):
|
|
157
|
+
env = env.split(",")
|
|
158
|
+
|
|
159
|
+
def split_pair(pair: str) -> tuple[str, str]:
|
|
160
|
+
if "=" not in pair:
|
|
161
|
+
log.error(f"Invalid environment variable format: {pair}")
|
|
162
|
+
raise typer.BadParameter(
|
|
163
|
+
"Invalid environment variable format. Each env must be in the format of key=value."
|
|
164
|
+
)
|
|
165
|
+
k, v = pair.split("=", maxsplit=1)
|
|
166
|
+
log.debug(f"Parsed env: {k}={v}")
|
|
167
|
+
return k, v
|
|
168
|
+
|
|
169
|
+
for pair in env:
|
|
170
|
+
if "," in pair:
|
|
171
|
+
for k, v in map(split_pair, pair.split(",")):
|
|
172
|
+
ret[k] = v
|
|
173
|
+
else:
|
|
174
|
+
k, v = split_pair(pair)
|
|
175
|
+
ret[k] = v
|
|
176
|
+
|
|
177
|
+
log.debug(f"Parsed env: {ret}")
|
|
178
|
+
return ret
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
7
|
+
from cmdbox.cli.ui.editor import edit_text_fullscreen, EditCanceled, edit_text_in_editor
|
|
8
|
+
from cmdbox.cli.ui.presenters.settings_presenter import render_settings_show
|
|
9
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
10
|
+
from cmdbox.settings.settings_service import SettingsService
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@log_action(__name__, "run_edit_settings")
|
|
16
|
+
def run_edit_settings(
|
|
17
|
+
external: bool = True,
|
|
18
|
+
*,
|
|
19
|
+
get_settings_service: Callable[[], SettingsService],
|
|
20
|
+
get_console: Callable[[], ConsoleUI],
|
|
21
|
+
):
|
|
22
|
+
settings_service = get_settings_service()
|
|
23
|
+
console = get_console()
|
|
24
|
+
|
|
25
|
+
def editor_fn(initial_text: str) -> str:
|
|
26
|
+
if external:
|
|
27
|
+
return edit_text_in_editor(
|
|
28
|
+
initial_text, suffix=".toml", title_hint="CmdBox Settings"
|
|
29
|
+
)
|
|
30
|
+
else:
|
|
31
|
+
return edit_text_fullscreen(initial_text, title="Edit Settings")
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
settings_service.edit(editor_fn)
|
|
35
|
+
console.success("Settings saved.")
|
|
36
|
+
except EditCanceled:
|
|
37
|
+
console.info("Settings not saved.")
|
|
38
|
+
raise typer.Exit(code=0)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
log.error("Error saving settings.", exc_info=True)
|
|
41
|
+
console.error(f"Error saving settings: {exc}")
|
|
42
|
+
raise typer.Exit(code=1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@log_action(__name__, "run_show_settings")
|
|
46
|
+
def run_show_settings(
|
|
47
|
+
fields: str | None = None,
|
|
48
|
+
*,
|
|
49
|
+
get_settings_service: Callable[[], SettingsService],
|
|
50
|
+
get_console: Callable[[], ConsoleUI],
|
|
51
|
+
):
|
|
52
|
+
parsed_fields = fields.split(",") if fields else None
|
|
53
|
+
|
|
54
|
+
settings_service = get_settings_service()
|
|
55
|
+
console = get_console()
|
|
56
|
+
|
|
57
|
+
settings = settings_service.get()
|
|
58
|
+
output = render_settings_show(settings, parsed_fields)
|
|
59
|
+
console.print(output)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Callable, Any, Sequence, Dict
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from cmdbox.cli.common.update_fields import (
|
|
7
|
+
merge_fields,
|
|
8
|
+
parse_set_pairs,
|
|
9
|
+
filter_allowed,
|
|
10
|
+
)
|
|
11
|
+
from cmdbox.cli.prompts.prompts import (
|
|
12
|
+
prompt_for_name,
|
|
13
|
+
prompt_for_description,
|
|
14
|
+
)
|
|
15
|
+
from cmdbox.cli.ui.presenters.tag_presenter import (
|
|
16
|
+
render_tag_created,
|
|
17
|
+
render_tag,
|
|
18
|
+
render_tag_list,
|
|
19
|
+
render_tag_updated,
|
|
20
|
+
render_tag_deleted,
|
|
21
|
+
)
|
|
22
|
+
from cmdbox.cli.prompts.validators import TagNameValidator
|
|
23
|
+
from cmdbox.cli.ui.console import ConsoleUI
|
|
24
|
+
from cmdbox.services.field_selection import FieldSelectionResolver
|
|
25
|
+
from cmdbox.services.tag_services import TagServices
|
|
26
|
+
from cmdbox.settings.models import Settings
|
|
27
|
+
from cmdbox.logging_setup.log_decorators import log_action
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class AddTagArgs:
|
|
32
|
+
name: Optional[str]
|
|
33
|
+
description: Optional[str]
|
|
34
|
+
interactive: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@log_action(__name__, "run_add_tag")
|
|
38
|
+
def run_add_tag(
|
|
39
|
+
*,
|
|
40
|
+
args: AddTagArgs,
|
|
41
|
+
get_tag_services: Callable[[], TagServices],
|
|
42
|
+
get_console: Callable[[], ConsoleUI],
|
|
43
|
+
) -> None:
|
|
44
|
+
name = args.name
|
|
45
|
+
description = args.description
|
|
46
|
+
|
|
47
|
+
if args.interactive or name is None:
|
|
48
|
+
name = prompt_for_name(TagNameValidator())
|
|
49
|
+
|
|
50
|
+
if args.interactive or description is None:
|
|
51
|
+
description = prompt_for_description()
|
|
52
|
+
|
|
53
|
+
tag_service = get_tag_services()
|
|
54
|
+
tag = tag_service.create_tag(name=name, description=description)
|
|
55
|
+
console = get_console()
|
|
56
|
+
console.print(render_tag_created(tag))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@log_action(__name__, "run_get_tag")
|
|
60
|
+
def run_get_tag(
|
|
61
|
+
*,
|
|
62
|
+
name: str,
|
|
63
|
+
get_tag_services: Callable[[], TagServices],
|
|
64
|
+
get_console: Callable[[], ConsoleUI],
|
|
65
|
+
) -> None:
|
|
66
|
+
console = get_console()
|
|
67
|
+
tag_service = get_tag_services()
|
|
68
|
+
tag = tag_service.get_tag(name)
|
|
69
|
+
console.print(render_tag(tag))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@log_action(__name__, "run_update_tag")
|
|
73
|
+
def run_update_tag(
|
|
74
|
+
*,
|
|
75
|
+
name: str,
|
|
76
|
+
description: Optional[str],
|
|
77
|
+
new_name: Optional[str],
|
|
78
|
+
set_pairs: Optional[Sequence[str]],
|
|
79
|
+
edit_mode: bool,
|
|
80
|
+
edit_fields: Optional[str],
|
|
81
|
+
get_tag_services: Callable[[], TagServices],
|
|
82
|
+
get_settings: Callable[[], Settings],
|
|
83
|
+
get_console: Callable[[], ConsoleUI],
|
|
84
|
+
) -> None:
|
|
85
|
+
allowed = {"name", "description"}
|
|
86
|
+
fields: Dict[str, Any] = {}
|
|
87
|
+
|
|
88
|
+
tag_service = get_tag_services()
|
|
89
|
+
tag = tag_service.get_tag(name)
|
|
90
|
+
console = get_console()
|
|
91
|
+
|
|
92
|
+
if edit_mode:
|
|
93
|
+
if any([description, new_name, set_pairs]):
|
|
94
|
+
raise typer.BadParameter(
|
|
95
|
+
"--edit cannot be combined with field options or --set."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
updated_fields: dict[str, Any] = {}
|
|
99
|
+
if edit_fields:
|
|
100
|
+
edit_fields = [x.strip() for x in edit_fields.split(",")]
|
|
101
|
+
|
|
102
|
+
field_aliases = get_settings().field_aliases.alias_mapping
|
|
103
|
+
|
|
104
|
+
def check_field_aliases(field: str) -> bool:
|
|
105
|
+
return (
|
|
106
|
+
edit_fields is None
|
|
107
|
+
or field in edit_fields
|
|
108
|
+
or any(x in edit_fields for x in field_aliases.get(field, []))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if check_field_aliases("name"):
|
|
112
|
+
updated_fields["name"] = prompt_for_name(
|
|
113
|
+
TagNameValidator(), default=tag.name
|
|
114
|
+
)
|
|
115
|
+
if check_field_aliases("description"):
|
|
116
|
+
updated_fields["description"] = prompt_for_description(
|
|
117
|
+
default=tag.description
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
fields = updated_fields
|
|
121
|
+
|
|
122
|
+
else:
|
|
123
|
+
if description is not None:
|
|
124
|
+
fields["description"] = description
|
|
125
|
+
if new_name is not None:
|
|
126
|
+
fields["name"] = new_name
|
|
127
|
+
|
|
128
|
+
fields = merge_fields(fields, parse_set_pairs(set_pairs))
|
|
129
|
+
fields = filter_allowed(fields, allowed)
|
|
130
|
+
|
|
131
|
+
if not fields:
|
|
132
|
+
raise typer.BadParameter("No fields specified to update.")
|
|
133
|
+
|
|
134
|
+
current = {
|
|
135
|
+
"name": tag.name,
|
|
136
|
+
"description": tag.description,
|
|
137
|
+
}
|
|
138
|
+
fields = {key: value for key, value in fields.items() if current.get(key) != value}
|
|
139
|
+
|
|
140
|
+
if not fields:
|
|
141
|
+
console.info("No changes detected.")
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
tag_service.update_tag(name, **fields)
|
|
145
|
+
|
|
146
|
+
updated_tag = tag_service.get_tag_by_id(tag.id)
|
|
147
|
+
console.print(render_tag_updated(updated_tag))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@log_action(__name__, "run_list_tags")
|
|
151
|
+
def run_list_tags(
|
|
152
|
+
*,
|
|
153
|
+
limit: int,
|
|
154
|
+
order_by: str,
|
|
155
|
+
fields: list[str] | None = None,
|
|
156
|
+
get_tag_services: Callable[[], TagServices],
|
|
157
|
+
get_settings: Callable[[], Settings],
|
|
158
|
+
get_console: Callable[[], ConsoleUI],
|
|
159
|
+
get_display_field_resolver: Callable[[], FieldSelectionResolver],
|
|
160
|
+
) -> None:
|
|
161
|
+
console = get_console()
|
|
162
|
+
tag_service = get_tag_services()
|
|
163
|
+
tags = tag_service.list_tags(limit=limit, order_by=order_by)
|
|
164
|
+
|
|
165
|
+
settings = get_settings()
|
|
166
|
+
fields = get_display_field_resolver().resolve(
|
|
167
|
+
fields,
|
|
168
|
+
default_fields=settings.default_fields.tag_output,
|
|
169
|
+
aliases=settings.field_aliases.alias_map,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
console.print(render_tag_list(tags, title="Tags", fields=fields))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@log_action(__name__, "run_search_tags")
|
|
176
|
+
def run_search_tags(
|
|
177
|
+
*,
|
|
178
|
+
term: str,
|
|
179
|
+
limit: int,
|
|
180
|
+
search_fields: list[str] | None = None,
|
|
181
|
+
fields: list[str] | None = None,
|
|
182
|
+
get_tag_services: Callable[[], TagServices],
|
|
183
|
+
get_settings: Callable[[], Settings],
|
|
184
|
+
get_console: Callable[[], ConsoleUI],
|
|
185
|
+
get_display_field_resolver: Callable[[], FieldSelectionResolver],
|
|
186
|
+
get_search_field_resolver: Callable[[], FieldSelectionResolver],
|
|
187
|
+
) -> None:
|
|
188
|
+
console = get_console()
|
|
189
|
+
tag_service = get_tag_services()
|
|
190
|
+
|
|
191
|
+
settings = get_settings()
|
|
192
|
+
output_fields = get_display_field_resolver().resolve(
|
|
193
|
+
fields,
|
|
194
|
+
default_fields=settings.default_fields.tag_output,
|
|
195
|
+
aliases=settings.field_aliases.alias_map,
|
|
196
|
+
)
|
|
197
|
+
search_fields = get_search_field_resolver().resolve(
|
|
198
|
+
search_fields,
|
|
199
|
+
default_fields=settings.default_fields.tag_search,
|
|
200
|
+
aliases=settings.field_aliases.alias_map,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
tags = tag_service.search(term, limit=limit, fields=search_fields)
|
|
204
|
+
console.print(render_tag_list(tags, title="Search Results", fields=output_fields))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@log_action(__name__, "run_delete_tag")
|
|
208
|
+
def run_delete_tag(
|
|
209
|
+
*,
|
|
210
|
+
name: str,
|
|
211
|
+
get_tag_services: Callable[[], TagServices],
|
|
212
|
+
get_console: Callable[[], ConsoleUI],
|
|
213
|
+
) -> None:
|
|
214
|
+
console = get_console()
|
|
215
|
+
tag_service = get_tag_services()
|
|
216
|
+
tag = tag_service.get_tag(name)
|
|
217
|
+
if tag_service.delete_tag(name):
|
|
218
|
+
console.print(render_tag_deleted(tag))
|
|
219
|
+
else:
|
|
220
|
+
console.error(f"Failed to delete tag '{name}'.")
|