zabbix-cli-uio 3.1.1__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.
- zabbix_cli/__about__.py +5 -0
- zabbix_cli/__init__.py +5 -0
- zabbix_cli/__main__.py +6 -0
- zabbix_cli/_patches/__init__.py +10 -0
- zabbix_cli/_patches/click_repl.py +149 -0
- zabbix_cli/_patches/common.py +80 -0
- zabbix_cli/_patches/typer.py +317 -0
- zabbix_cli/_types.py +12 -0
- zabbix_cli/_v2_compat.py +102 -0
- zabbix_cli/app/__init__.py +32 -0
- zabbix_cli/app/app.py +232 -0
- zabbix_cli/app/plugins.py +180 -0
- zabbix_cli/auth.py +366 -0
- zabbix_cli/bulk.py +226 -0
- zabbix_cli/cache.py +86 -0
- zabbix_cli/commands/__init__.py +13 -0
- zabbix_cli/commands/cli.py +298 -0
- zabbix_cli/commands/common/__init__.py +0 -0
- zabbix_cli/commands/common/args.py +24 -0
- zabbix_cli/commands/export.py +688 -0
- zabbix_cli/commands/host.py +846 -0
- zabbix_cli/commands/hostgroup.py +494 -0
- zabbix_cli/commands/item.py +76 -0
- zabbix_cli/commands/macro.py +258 -0
- zabbix_cli/commands/maintenance.py +238 -0
- zabbix_cli/commands/problem.py +284 -0
- zabbix_cli/commands/proxy.py +658 -0
- zabbix_cli/commands/results/__init__.py +11 -0
- zabbix_cli/commands/results/cli.py +149 -0
- zabbix_cli/commands/results/export.py +53 -0
- zabbix_cli/commands/results/host.py +59 -0
- zabbix_cli/commands/results/hostgroup.py +177 -0
- zabbix_cli/commands/results/item.py +122 -0
- zabbix_cli/commands/results/macro.py +115 -0
- zabbix_cli/commands/results/maintenance.py +105 -0
- zabbix_cli/commands/results/problem.py +20 -0
- zabbix_cli/commands/results/proxy.py +184 -0
- zabbix_cli/commands/results/template.py +123 -0
- zabbix_cli/commands/results/templategroup.py +110 -0
- zabbix_cli/commands/results/user.py +236 -0
- zabbix_cli/commands/template.py +609 -0
- zabbix_cli/commands/templategroup.py +377 -0
- zabbix_cli/commands/user.py +861 -0
- zabbix_cli/config/__init__.py +0 -0
- zabbix_cli/config/__main__.py +9 -0
- zabbix_cli/config/constants.py +40 -0
- zabbix_cli/config/model.py +524 -0
- zabbix_cli/config/run.py +30 -0
- zabbix_cli/config/utils.py +128 -0
- zabbix_cli/dirs.py +80 -0
- zabbix_cli/exceptions.py +308 -0
- zabbix_cli/logs.py +171 -0
- zabbix_cli/main.py +256 -0
- zabbix_cli/models.py +302 -0
- zabbix_cli/output/__init__.py +1 -0
- zabbix_cli/output/console.py +201 -0
- zabbix_cli/output/formatting/__init__.py +1 -0
- zabbix_cli/output/formatting/bytes.py +11 -0
- zabbix_cli/output/formatting/constants.py +5 -0
- zabbix_cli/output/formatting/dates.py +38 -0
- zabbix_cli/output/formatting/grammar.py +51 -0
- zabbix_cli/output/formatting/path.py +15 -0
- zabbix_cli/output/prompts.py +385 -0
- zabbix_cli/output/render.py +155 -0
- zabbix_cli/output/style.py +207 -0
- zabbix_cli/py.typed +0 -0
- zabbix_cli/pyzabbix/__init__.py +3 -0
- zabbix_cli/pyzabbix/client.py +2509 -0
- zabbix_cli/pyzabbix/compat.py +134 -0
- zabbix_cli/pyzabbix/enums.py +527 -0
- zabbix_cli/pyzabbix/types.py +1285 -0
- zabbix_cli/pyzabbix/utils.py +36 -0
- zabbix_cli/scripts/__init__.py +1 -0
- zabbix_cli/scripts/bulk_execution.py +60 -0
- zabbix_cli/scripts/init.py +65 -0
- zabbix_cli/state.py +260 -0
- zabbix_cli/table.py +29 -0
- zabbix_cli/utils/__init__.py +3 -0
- zabbix_cli/utils/args.py +164 -0
- zabbix_cli/utils/commands.py +47 -0
- zabbix_cli/utils/fs.py +90 -0
- zabbix_cli/utils/rich.py +35 -0
- zabbix_cli/utils/utils.py +330 -0
- zabbix_cli_uio-3.1.1.dist-info/METADATA +404 -0
- zabbix_cli_uio-3.1.1.dist-info/RECORD +89 -0
- zabbix_cli_uio-3.1.1.dist-info/WHEEL +4 -0
- zabbix_cli_uio-3.1.1.dist-info/entry_points.txt +4 -0
- zabbix_cli_uio-3.1.1.dist-info/licenses/AUTHORS +32 -0
- zabbix_cli_uio-3.1.1.dist-info/licenses/LICENSE +674 -0
zabbix_cli/__about__.py
ADDED
zabbix_cli/__init__.py
ADDED
zabbix_cli/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from zabbix_cli._patches import typer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def patch_all() -> None:
|
|
7
|
+
"""Apply all patches to all modules."""
|
|
8
|
+
typer.patch()
|
|
9
|
+
# NOTE: we patch click_repl only when we actually launch the REPL
|
|
10
|
+
# See: zabbix_cli.main.start_repl
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
"""Patches for click_repl package."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing import Dict
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
import click_repl
|
|
15
|
+
from click.exceptions import Exit as ClickExit
|
|
16
|
+
from click_repl import ExitReplException
|
|
17
|
+
from click_repl import bootstrap_prompt
|
|
18
|
+
from click_repl import dispatch_repl_commands
|
|
19
|
+
from click_repl import handle_internal_commands
|
|
20
|
+
from prompt_toolkit.shortcuts import prompt
|
|
21
|
+
|
|
22
|
+
from zabbix_cli._patches.common import get_patcher
|
|
23
|
+
from zabbix_cli.exceptions import handle_exception
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from click.core import Context
|
|
27
|
+
|
|
28
|
+
from zabbix_cli.app import StatefulApp
|
|
29
|
+
|
|
30
|
+
patcher = get_patcher(f"click_repl version: {click_repl.__version__}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def repl( # noqa: C901
|
|
34
|
+
old_ctx: Context,
|
|
35
|
+
prompt_kwargs: Dict[str, Any] = None,
|
|
36
|
+
allow_system_commands: bool = True,
|
|
37
|
+
allow_internal_commands: bool = True,
|
|
38
|
+
app: Optional[StatefulApp] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Start an interactive shell. All subcommands are available in it.
|
|
41
|
+
|
|
42
|
+
:param old_ctx: The current Click context.
|
|
43
|
+
:param prompt_kwargs: Parameters passed to
|
|
44
|
+
:py:func:`prompt_toolkit.shortcuts.prompt`.
|
|
45
|
+
|
|
46
|
+
If stdin is not a TTY, no prompt will be printed, but only commands read
|
|
47
|
+
from stdin.
|
|
48
|
+
"""
|
|
49
|
+
# parent should be available, but we're not going to bother if not
|
|
50
|
+
group_ctx = old_ctx.parent or old_ctx
|
|
51
|
+
group = group_ctx.command
|
|
52
|
+
isatty = sys.stdin.isatty()
|
|
53
|
+
|
|
54
|
+
# Delete the REPL command from those available, as we don't want to allow
|
|
55
|
+
# nesting REPLs (note: pass `None` to `pop` as we don't want to error if
|
|
56
|
+
# REPL command already not present for some reason).
|
|
57
|
+
repl_command_name = old_ctx.command.name
|
|
58
|
+
if isinstance(group_ctx.command, click.CommandCollection):
|
|
59
|
+
available_commands = {
|
|
60
|
+
cmd_name: cmd_obj
|
|
61
|
+
for source in group_ctx.command.sources
|
|
62
|
+
for cmd_name, cmd_obj in source.commands.items()
|
|
63
|
+
}
|
|
64
|
+
else:
|
|
65
|
+
available_commands = group_ctx.command.commands
|
|
66
|
+
available_commands.pop(repl_command_name, None)
|
|
67
|
+
|
|
68
|
+
prompt_kwargs = bootstrap_prompt(prompt_kwargs, group)
|
|
69
|
+
|
|
70
|
+
if isatty:
|
|
71
|
+
|
|
72
|
+
def get_command():
|
|
73
|
+
return prompt(**prompt_kwargs)
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
get_command = sys.stdin.readline
|
|
77
|
+
|
|
78
|
+
while True:
|
|
79
|
+
try:
|
|
80
|
+
command = get_command()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
continue
|
|
83
|
+
except EOFError:
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
if not command:
|
|
87
|
+
if isatty:
|
|
88
|
+
continue
|
|
89
|
+
else:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
if allow_system_commands and dispatch_repl_commands(command):
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if allow_internal_commands:
|
|
96
|
+
try:
|
|
97
|
+
result = handle_internal_commands(command)
|
|
98
|
+
if isinstance(result, str):
|
|
99
|
+
click.echo(result)
|
|
100
|
+
continue
|
|
101
|
+
except ExitReplException:
|
|
102
|
+
break
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
args = shlex.split(command)
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
click.echo(f"{type(e).__name__}: {e}")
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
if app:
|
|
112
|
+
group = app.as_click_group()
|
|
113
|
+
with group.make_context(None, args, parent=group_ctx) as ctx:
|
|
114
|
+
group.invoke(ctx)
|
|
115
|
+
ctx.exit()
|
|
116
|
+
except click.ClickException as e:
|
|
117
|
+
e.show()
|
|
118
|
+
except ClickExit:
|
|
119
|
+
pass
|
|
120
|
+
except SystemExit:
|
|
121
|
+
pass
|
|
122
|
+
except ExitReplException:
|
|
123
|
+
break
|
|
124
|
+
# PATCH: Handle zabbix-cli exceptions
|
|
125
|
+
except Exception as e:
|
|
126
|
+
try:
|
|
127
|
+
handle_exception(e)
|
|
128
|
+
except SystemExit:
|
|
129
|
+
pass
|
|
130
|
+
# PATCH: Continue on keyboard interrupt
|
|
131
|
+
except KeyboardInterrupt:
|
|
132
|
+
from zabbix_cli.output.console import err_console
|
|
133
|
+
|
|
134
|
+
# User likely pressed Ctrl+C during a prompt or when a spinner
|
|
135
|
+
# was active. Ensure message is printed on a new line.
|
|
136
|
+
# TODO: determine if last char in terminal was newline somehow! Can we?
|
|
137
|
+
err_console.print("\n[red]Aborted.[/]")
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def patch_exception_handling() -> None:
|
|
142
|
+
"""Patch click_repl's exception handling to fall back on zabbix-cli exception handlers."""
|
|
143
|
+
with patcher("click_repl.repl"):
|
|
144
|
+
click_repl.repl = repl
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def patch() -> None:
|
|
148
|
+
"""Apply all patches."""
|
|
149
|
+
patch_exception_handling()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from typing import Type
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BasePatcher(ABC):
|
|
14
|
+
"""Context manager that logs and prints diagnostic info if an exception
|
|
15
|
+
occurs.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, description: str) -> None:
|
|
19
|
+
self.description = description
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def __package_info__(self) -> str:
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
def __enter__(self) -> BasePatcher:
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(
|
|
29
|
+
self,
|
|
30
|
+
exc_type: Optional[Type[BaseException]],
|
|
31
|
+
exc_val: Optional[BaseException],
|
|
32
|
+
exc_tb: Optional[TracebackType],
|
|
33
|
+
) -> bool:
|
|
34
|
+
if not exc_type:
|
|
35
|
+
return True
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
import rich
|
|
39
|
+
from rich.table import Table
|
|
40
|
+
|
|
41
|
+
from zabbix_cli.__about__ import __version__
|
|
42
|
+
|
|
43
|
+
# Rudimentary, but provides enough info to debug and fix the issue
|
|
44
|
+
console = rich.console.Console(stderr=True)
|
|
45
|
+
console.print_exception()
|
|
46
|
+
console.print()
|
|
47
|
+
table = Table(
|
|
48
|
+
title="Diagnostics",
|
|
49
|
+
show_header=False,
|
|
50
|
+
show_lines=False,
|
|
51
|
+
)
|
|
52
|
+
table.add_row(
|
|
53
|
+
"[b]Package [/]",
|
|
54
|
+
self.__package_info__(),
|
|
55
|
+
)
|
|
56
|
+
table.add_row(
|
|
57
|
+
"[b]zabbix-cli [/]",
|
|
58
|
+
__version__,
|
|
59
|
+
)
|
|
60
|
+
table.add_row(
|
|
61
|
+
"[b]Python [/]",
|
|
62
|
+
sys.version,
|
|
63
|
+
)
|
|
64
|
+
table.add_row(
|
|
65
|
+
"[b]Platform [/]",
|
|
66
|
+
sys.platform,
|
|
67
|
+
)
|
|
68
|
+
console.print(table)
|
|
69
|
+
console.print(f"[bold red]ERROR: Failed to patch {self.description}[/]")
|
|
70
|
+
raise SystemExit(1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_patcher(info: str) -> Type[BasePatcher]:
|
|
74
|
+
"""Returns a patcher for a given package."""
|
|
75
|
+
|
|
76
|
+
class Patcher(BasePatcher):
|
|
77
|
+
def __package_info__(self) -> str:
|
|
78
|
+
return info
|
|
79
|
+
|
|
80
|
+
return Patcher
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
"""Patching of Typer to extend functionality and change styling.
|
|
3
|
+
|
|
4
|
+
Will probably break for some version of Typer at some point.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import inspect
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import Callable
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
from typing import Type
|
|
18
|
+
from typing import Union
|
|
19
|
+
from typing import cast
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
import typer
|
|
24
|
+
from typer.main import lenient_issubclass
|
|
25
|
+
from typer.models import ParameterInfo
|
|
26
|
+
|
|
27
|
+
from zabbix_cli._patches.common import get_patcher
|
|
28
|
+
from zabbix_cli.pyzabbix.enums import APIStrEnum
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from typing import Dict
|
|
32
|
+
|
|
33
|
+
from rich.style import Style
|
|
34
|
+
|
|
35
|
+
patcher = get_patcher(f"Typer version: {typer.__version__}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def patch_help_text_style() -> None:
|
|
39
|
+
"""Remove dimming of help text.
|
|
40
|
+
|
|
41
|
+
https://github.com/tiangolo/typer/issues/437#issuecomment-1224149402
|
|
42
|
+
"""
|
|
43
|
+
with patcher("typer.rich_utils.STYLE_HELPTEXT"):
|
|
44
|
+
typer.rich_utils.STYLE_HELPTEXT = ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def patch_help_text_spacing() -> None:
|
|
48
|
+
"""Adds a single blank line between short and long help text of a command when using `--help`.
|
|
49
|
+
|
|
50
|
+
As of Typer 0.9.0, the short and long help text is printed without any
|
|
51
|
+
blank lines between them. This is bad for readability (IMO).
|
|
52
|
+
"""
|
|
53
|
+
from rich.console import group
|
|
54
|
+
from rich.markdown import Markdown
|
|
55
|
+
from rich.text import Text
|
|
56
|
+
from typer.rich_utils import DEPRECATED_STRING
|
|
57
|
+
from typer.rich_utils import MARKUP_MODE_MARKDOWN
|
|
58
|
+
from typer.rich_utils import MARKUP_MODE_RICH
|
|
59
|
+
from typer.rich_utils import STYLE_DEPRECATED
|
|
60
|
+
from typer.rich_utils import STYLE_HELPTEXT
|
|
61
|
+
from typer.rich_utils import STYLE_HELPTEXT_FIRST_LINE
|
|
62
|
+
from typer.rich_utils import MarkupMode
|
|
63
|
+
from typer.rich_utils import _make_rich_rext
|
|
64
|
+
|
|
65
|
+
@group()
|
|
66
|
+
def _get_help_text(
|
|
67
|
+
*,
|
|
68
|
+
obj: Union[click.Command, click.Group],
|
|
69
|
+
markup_mode: MarkupMode,
|
|
70
|
+
) -> Iterable[Union[Markdown, Text]]:
|
|
71
|
+
"""Build primary help text for a click command or group.
|
|
72
|
+
|
|
73
|
+
Returns the prose help text for a command or group, rendered either as a
|
|
74
|
+
Rich Text object or as Markdown.
|
|
75
|
+
If the command is marked as deprecated, the deprecated string will be prepended.
|
|
76
|
+
"""
|
|
77
|
+
# Prepend deprecated status
|
|
78
|
+
if obj.deprecated:
|
|
79
|
+
yield Text(DEPRECATED_STRING, style=STYLE_DEPRECATED)
|
|
80
|
+
|
|
81
|
+
# Fetch and dedent the help text
|
|
82
|
+
help_text = inspect.cleandoc(obj.help or "")
|
|
83
|
+
|
|
84
|
+
# Trim off anything that comes after \f on its own line
|
|
85
|
+
help_text = help_text.partition("\f")[0]
|
|
86
|
+
|
|
87
|
+
# Get the first paragraph
|
|
88
|
+
first_line = help_text.split("\n\n")[0]
|
|
89
|
+
# Remove single linebreaks
|
|
90
|
+
if markup_mode != MARKUP_MODE_MARKDOWN and not first_line.startswith("\b"):
|
|
91
|
+
first_line = first_line.replace("\n", " ")
|
|
92
|
+
yield _make_rich_rext(
|
|
93
|
+
text=first_line.strip(),
|
|
94
|
+
style=STYLE_HELPTEXT_FIRST_LINE,
|
|
95
|
+
markup_mode=markup_mode,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Get remaining lines, remove single line breaks and format as dim
|
|
99
|
+
remaining_paragraphs = help_text.split("\n\n")[1:]
|
|
100
|
+
if remaining_paragraphs:
|
|
101
|
+
if markup_mode != MARKUP_MODE_RICH:
|
|
102
|
+
# Remove single linebreaks
|
|
103
|
+
remaining_paragraphs = [
|
|
104
|
+
x.replace("\n", " ").strip()
|
|
105
|
+
if not x.startswith("\b")
|
|
106
|
+
else "{}\n".format(x.strip("\b\n"))
|
|
107
|
+
for x in remaining_paragraphs
|
|
108
|
+
]
|
|
109
|
+
# Join back together
|
|
110
|
+
remaining_lines = "\n".join(remaining_paragraphs)
|
|
111
|
+
else:
|
|
112
|
+
# Join with double linebreaks if markdown
|
|
113
|
+
remaining_lines = "\n\n".join(remaining_paragraphs)
|
|
114
|
+
yield _make_rich_rext(
|
|
115
|
+
text="\n",
|
|
116
|
+
style=STYLE_HELPTEXT,
|
|
117
|
+
markup_mode=markup_mode,
|
|
118
|
+
)
|
|
119
|
+
yield _make_rich_rext(
|
|
120
|
+
text=remaining_lines,
|
|
121
|
+
style=STYLE_HELPTEXT,
|
|
122
|
+
markup_mode=markup_mode,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
with patcher("typer.rich_utils._get_help_text"):
|
|
126
|
+
typer.rich_utils._get_help_text = _get_help_text
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def patch_generate_enum_convertor() -> None:
|
|
130
|
+
"""Patches enum value converter with an additional fallback to
|
|
131
|
+
instantiating the enum with the value directly.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
def generate_enum_convertor(enum: Type[Enum]) -> Callable[[Any], Any]:
|
|
135
|
+
lower_val_map = {str(val.value).lower(): val for val in enum}
|
|
136
|
+
|
|
137
|
+
def convertor(value: Any) -> Any:
|
|
138
|
+
if value is not None:
|
|
139
|
+
low = str(value).lower()
|
|
140
|
+
if low in lower_val_map:
|
|
141
|
+
key = lower_val_map[low]
|
|
142
|
+
return enum(key)
|
|
143
|
+
# Fall back to passing in the value as-is
|
|
144
|
+
try:
|
|
145
|
+
return enum(value)
|
|
146
|
+
except ValueError:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
return convertor
|
|
150
|
+
|
|
151
|
+
with patcher("typer.main.generate_enum_convertor"):
|
|
152
|
+
typer.main.generate_enum_convertor = generate_enum_convertor
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def patch_get_click_type() -> None:
|
|
156
|
+
"""Adds support for our custom `APIStrEnum` type.
|
|
157
|
+
|
|
158
|
+
Used in conjunction with our custom generate_enum_convertor to support
|
|
159
|
+
instantiating `APIStrEnum` with both the human-readable value and the API value
|
|
160
|
+
(e.g. `"Enabled"` and `0`).
|
|
161
|
+
|
|
162
|
+
Uses the `APIStrEnum.all_choices()` method to get the list of choices.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def get_click_type(
|
|
166
|
+
*, annotation: Any, parameter_info: ParameterInfo
|
|
167
|
+
) -> click.ParamType:
|
|
168
|
+
if parameter_info.click_type is not None:
|
|
169
|
+
return parameter_info.click_type
|
|
170
|
+
|
|
171
|
+
elif parameter_info.parser is not None:
|
|
172
|
+
return click.types.FuncParamType(parameter_info.parser)
|
|
173
|
+
|
|
174
|
+
elif annotation == str: # noqa: E721
|
|
175
|
+
return click.STRING
|
|
176
|
+
elif annotation == int: # noqa: E721
|
|
177
|
+
if parameter_info.min is not None or parameter_info.max is not None:
|
|
178
|
+
min_ = None
|
|
179
|
+
max_ = None
|
|
180
|
+
if parameter_info.min is not None:
|
|
181
|
+
min_ = int(parameter_info.min)
|
|
182
|
+
if parameter_info.max is not None:
|
|
183
|
+
max_ = int(parameter_info.max)
|
|
184
|
+
return click.IntRange(min=min_, max=max_, clamp=parameter_info.clamp)
|
|
185
|
+
else:
|
|
186
|
+
return click.INT
|
|
187
|
+
elif annotation == float: # noqa: E721
|
|
188
|
+
if parameter_info.min is not None or parameter_info.max is not None:
|
|
189
|
+
return click.FloatRange(
|
|
190
|
+
min=parameter_info.min,
|
|
191
|
+
max=parameter_info.max,
|
|
192
|
+
clamp=parameter_info.clamp,
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
return click.FLOAT
|
|
196
|
+
elif annotation == bool: # noqa: E721
|
|
197
|
+
return click.BOOL
|
|
198
|
+
elif annotation == UUID:
|
|
199
|
+
return click.UUID
|
|
200
|
+
elif annotation == datetime:
|
|
201
|
+
return click.DateTime(formats=parameter_info.formats)
|
|
202
|
+
elif (
|
|
203
|
+
annotation == Path
|
|
204
|
+
or parameter_info.allow_dash
|
|
205
|
+
or parameter_info.path_type
|
|
206
|
+
or parameter_info.resolve_path
|
|
207
|
+
):
|
|
208
|
+
return click.Path(
|
|
209
|
+
exists=parameter_info.exists,
|
|
210
|
+
file_okay=parameter_info.file_okay,
|
|
211
|
+
dir_okay=parameter_info.dir_okay,
|
|
212
|
+
writable=parameter_info.writable,
|
|
213
|
+
readable=parameter_info.readable,
|
|
214
|
+
resolve_path=parameter_info.resolve_path,
|
|
215
|
+
allow_dash=parameter_info.allow_dash,
|
|
216
|
+
path_type=parameter_info.path_type,
|
|
217
|
+
)
|
|
218
|
+
elif lenient_issubclass(annotation, typer.FileTextWrite):
|
|
219
|
+
return click.File(
|
|
220
|
+
mode=parameter_info.mode or "w",
|
|
221
|
+
encoding=parameter_info.encoding,
|
|
222
|
+
errors=parameter_info.errors,
|
|
223
|
+
lazy=parameter_info.lazy,
|
|
224
|
+
atomic=parameter_info.atomic,
|
|
225
|
+
)
|
|
226
|
+
elif lenient_issubclass(annotation, typer.FileText):
|
|
227
|
+
return click.File(
|
|
228
|
+
mode=parameter_info.mode or "r",
|
|
229
|
+
encoding=parameter_info.encoding,
|
|
230
|
+
errors=parameter_info.errors,
|
|
231
|
+
lazy=parameter_info.lazy,
|
|
232
|
+
atomic=parameter_info.atomic,
|
|
233
|
+
)
|
|
234
|
+
elif lenient_issubclass(annotation, typer.FileBinaryRead):
|
|
235
|
+
return click.File(
|
|
236
|
+
mode=parameter_info.mode or "rb",
|
|
237
|
+
encoding=parameter_info.encoding,
|
|
238
|
+
errors=parameter_info.errors,
|
|
239
|
+
lazy=parameter_info.lazy,
|
|
240
|
+
atomic=parameter_info.atomic,
|
|
241
|
+
)
|
|
242
|
+
elif lenient_issubclass(annotation, typer.FileBinaryWrite):
|
|
243
|
+
return click.File(
|
|
244
|
+
mode=parameter_info.mode or "wb",
|
|
245
|
+
encoding=parameter_info.encoding,
|
|
246
|
+
errors=parameter_info.errors,
|
|
247
|
+
lazy=parameter_info.lazy,
|
|
248
|
+
atomic=parameter_info.atomic,
|
|
249
|
+
)
|
|
250
|
+
# our patch for APIStrEnum
|
|
251
|
+
elif lenient_issubclass(annotation, APIStrEnum):
|
|
252
|
+
annotation = cast(Type[APIStrEnum], annotation)
|
|
253
|
+
return click.Choice(
|
|
254
|
+
annotation.all_choices(),
|
|
255
|
+
case_sensitive=parameter_info.case_sensitive,
|
|
256
|
+
)
|
|
257
|
+
elif lenient_issubclass(annotation, Enum):
|
|
258
|
+
return click.Choice(
|
|
259
|
+
[item.value for item in annotation],
|
|
260
|
+
case_sensitive=parameter_info.case_sensitive,
|
|
261
|
+
)
|
|
262
|
+
raise RuntimeError(f"Type not yet supported: {annotation}") # pragma no cover
|
|
263
|
+
|
|
264
|
+
"""Patch typer's get_click_type to support more types."""
|
|
265
|
+
with patcher("typer.main.get_click_type"):
|
|
266
|
+
typer.main.get_click_type = get_click_type
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def patch__get_rich_console() -> None:
|
|
270
|
+
from rich.console import Console
|
|
271
|
+
from typer.rich_utils import COLOR_SYSTEM
|
|
272
|
+
from typer.rich_utils import FORCE_TERMINAL
|
|
273
|
+
from typer.rich_utils import MAX_WIDTH
|
|
274
|
+
from typer.rich_utils import STYLE_METAVAR
|
|
275
|
+
from typer.rich_utils import STYLE_METAVAR_SEPARATOR
|
|
276
|
+
from typer.rich_utils import STYLE_NEGATIVE_OPTION
|
|
277
|
+
from typer.rich_utils import STYLE_NEGATIVE_SWITCH
|
|
278
|
+
from typer.rich_utils import STYLE_OPTION
|
|
279
|
+
from typer.rich_utils import STYLE_SWITCH
|
|
280
|
+
from typer.rich_utils import STYLE_USAGE
|
|
281
|
+
from typer.rich_utils import highlighter
|
|
282
|
+
|
|
283
|
+
from zabbix_cli.output.style import RICH_THEME
|
|
284
|
+
|
|
285
|
+
theme: Dict[str, Union[str, Style]] = RICH_THEME.styles.copy() # type: ignore[assignment]
|
|
286
|
+
theme.update(
|
|
287
|
+
{
|
|
288
|
+
"option": STYLE_OPTION,
|
|
289
|
+
"switch": STYLE_SWITCH,
|
|
290
|
+
"negative_option": STYLE_NEGATIVE_OPTION,
|
|
291
|
+
"negative_switch": STYLE_NEGATIVE_SWITCH,
|
|
292
|
+
"metavar": STYLE_METAVAR,
|
|
293
|
+
"metavar_sep": STYLE_METAVAR_SEPARATOR,
|
|
294
|
+
"usage": STYLE_USAGE,
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def _get_rich_console(stderr: bool = False) -> Console:
|
|
299
|
+
return Console(
|
|
300
|
+
theme=RICH_THEME,
|
|
301
|
+
highlighter=highlighter,
|
|
302
|
+
color_system=COLOR_SYSTEM,
|
|
303
|
+
force_terminal=FORCE_TERMINAL,
|
|
304
|
+
width=MAX_WIDTH,
|
|
305
|
+
stderr=stderr,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
typer.rich_utils._get_rich_console = _get_rich_console
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def patch() -> None:
|
|
312
|
+
"""Apply all patches."""
|
|
313
|
+
patch_help_text_style()
|
|
314
|
+
patch_help_text_spacing()
|
|
315
|
+
patch_generate_enum_convertor()
|
|
316
|
+
patch_get_click_type()
|
|
317
|
+
patch__get_rich_console()
|
zabbix_cli/_types.py
ADDED
zabbix_cli/_v2_compat.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Compatibility functions going from Zabbix-CLI v2 to v3.
|
|
2
|
+
|
|
3
|
+
The functions in this module are intended to ease the transition by
|
|
4
|
+
providing fallbacks to deprecated functionality in Zabbix-CLI v2.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
from typing import List
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from click.core import CommandCollection
|
|
19
|
+
from click.core import Group
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CONFIG_FILENAME = "zabbix-cli.conf"
|
|
23
|
+
CONFIG_FIXED_NAME = "zabbix-cli.fixed.conf"
|
|
24
|
+
|
|
25
|
+
# Config file locations
|
|
26
|
+
CONFIG_DEFAULT_DIR = "/usr/share/zabbix-cli"
|
|
27
|
+
CONFIG_SYSTEM_DIR = "/etc/zabbix-cli"
|
|
28
|
+
CONFIG_USER_DIR = os.path.expanduser("~/.zabbix-cli")
|
|
29
|
+
|
|
30
|
+
# Any item will overwrite values from the previous (NYI)
|
|
31
|
+
CONFIG_PRIORITY = tuple(
|
|
32
|
+
Path(os.path.join(d, f))
|
|
33
|
+
for d, f in (
|
|
34
|
+
(CONFIG_DEFAULT_DIR, CONFIG_FIXED_NAME),
|
|
35
|
+
(CONFIG_SYSTEM_DIR, CONFIG_FIXED_NAME),
|
|
36
|
+
(CONFIG_USER_DIR, CONFIG_FILENAME),
|
|
37
|
+
(CONFIG_SYSTEM_DIR, CONFIG_FILENAME),
|
|
38
|
+
(CONFIG_DEFAULT_DIR, CONFIG_FILENAME),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
AUTH_FILE = Path.home() / ".zabbix-cli_auth"
|
|
44
|
+
AUTH_TOKEN_FILE = Path.home() / ".zabbix-cli_auth_token"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_command_from_option(ctx: typer.Context, command: str) -> None:
|
|
48
|
+
"""Runs a command via old-style --command/-C option."""
|
|
49
|
+
from zabbix_cli.output.console import error
|
|
50
|
+
from zabbix_cli.output.console import exit_err
|
|
51
|
+
from zabbix_cli.output.console import warning
|
|
52
|
+
|
|
53
|
+
warning(
|
|
54
|
+
"The [i]--command/-C[/] option is deprecated and will be removed in a future release. "
|
|
55
|
+
"Invoke command directly instead."
|
|
56
|
+
)
|
|
57
|
+
if not isinstance(ctx.command, (CommandCollection, Group)):
|
|
58
|
+
exit_err( # TODO: find out if this could ever happen?
|
|
59
|
+
f"Cannot run command {command!r}. Ensure it is a valid command and try again."
|
|
60
|
+
)
|
|
61
|
+
cmd_obj = ctx.command.get_command(ctx, command)
|
|
62
|
+
if not cmd_obj:
|
|
63
|
+
exit_err(
|
|
64
|
+
f"Cannot run command {command!r}. Ensure it is a valid command and try again."
|
|
65
|
+
)
|
|
66
|
+
try:
|
|
67
|
+
ctx.invoke(cmd_obj, *ctx.args)
|
|
68
|
+
except typer.Exit:
|
|
69
|
+
pass
|
|
70
|
+
except Exception as e:
|
|
71
|
+
error(
|
|
72
|
+
f"Command {command!r} failed with error: {e}. Try re-running without --command."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def args_callback(
|
|
77
|
+
ctx: typer.Context, value: Optional[List[str]]
|
|
78
|
+
) -> Optional[List[str]]:
|
|
79
|
+
if ctx.resilient_parsing:
|
|
80
|
+
return # for auto-completion
|
|
81
|
+
if value:
|
|
82
|
+
from zabbix_cli.output.console import warning
|
|
83
|
+
|
|
84
|
+
warning(
|
|
85
|
+
f"Detected deprecated positional arguments {value}. Use options instead."
|
|
86
|
+
)
|
|
87
|
+
# NOTE: Must NEVER return None. The "fix" in Typer 0.10.0 for None defaults
|
|
88
|
+
# somehow broke the parsing of callback values by causing values returned by
|
|
89
|
+
# callbacks to be passed to the internal converter, which then fails
|
|
90
|
+
# because it expects a list but gets None.
|
|
91
|
+
# https://github.com/tiangolo/typer/pull/664
|
|
92
|
+
# https://github.com/tiangolo/typer/blob/142422a14ca4c6a8ad579e9bd0fd0728364d86e3/typer/main.py#L639
|
|
93
|
+
return value or []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
ARGS_POSITIONAL = typer.Argument(
|
|
97
|
+
None,
|
|
98
|
+
help="DEPRECATED: V2-style positional arguments.",
|
|
99
|
+
show_default=False,
|
|
100
|
+
hidden=True,
|
|
101
|
+
callback=args_callback,
|
|
102
|
+
)
|