1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
|
from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import NoReturn
from typing import Optional
import typer
from rich.console import Console
from zabbix_cli.logs import logger
from zabbix_cli.output.formatting.path import path_link
from zabbix_cli.output.style import RICH_THEME
from zabbix_cli.output.style import Icon
from zabbix_cli.state import get_state
if TYPE_CHECKING:
from rich.theme import Theme
from zabbix_cli.config.model import Config
# stdout console used to print results
console = Console(theme=RICH_THEME)
# stderr console used to print prompts, messages, etc.
err_console = Console(
stderr=True,
highlight=False,
soft_wrap=True,
theme=RICH_THEME,
)
def get_theme(name: str) -> Theme:
"""Get a Rich console theme by name."""
return RICH_THEME # other themes NYI
RESERVED_EXTRA_KEYS = (
"name",
"level",
"pathname",
"lineno",
"msg",
"args",
"exc_info",
"func",
"sinfo",
)
def get_extra_dict(**kwargs: Any) -> dict[str, Any]:
"""Format the extra dict for logging. Renames some keys to avoid
collisions with the default keys.
See: https://docs.python.org/3.11/library/logging.html#logging.LogRecord
"""
for k in list(kwargs): # iterate over copy while mutating
if k in RESERVED_EXTRA_KEYS:
kwargs[f"{k}_"] = kwargs.pop(k)
return kwargs
def debug_kv(key: str, value: Any) -> None:
"""Print and log a key value pair."""
msg = f"[bold]{key:<20}:[/bold] {value}"
from rich.markup import render
logger.debug(
render(msg).plain, extra=get_extra_dict(key=key, value=value), stacklevel=2
)
err_console.print(msg)
def debug(message: str, icon: str = "", *args: Any, **kwargs: Any) -> None:
"""Log with INFO level and print an informational message."""
logger.debug(message, extra=get_extra_dict(**kwargs), stacklevel=2)
err_console.print(message)
def info(message: str, icon: str = Icon.INFO, *args: Any, **kwargs: Any) -> None:
"""Log with INFO level and print an informational message."""
logger.info(message, extra=get_extra_dict(**kwargs), stacklevel=2)
err_console.print(f"[success]{icon}[/] {message}")
def success(message: str, icon: str = Icon.OK, **kwargs: Any) -> None:
"""Log with INFO level and print a success message."""
logger.info(message, extra=get_extra_dict(**kwargs), stacklevel=2)
err_console.print(f"[success]{icon}[/] {message}")
def warning(message: str, icon: str = Icon.WARNING, **kwargs: Any) -> None:
"""Log with WARNING level and optionally print a warning message."""
logger.warning(message, extra=get_extra_dict(**kwargs), stacklevel=2)
err_console.print(f"[warning]{icon} {message}[/]")
def error(
message: str,
icon: str = Icon.ERROR,
*,
exc_info: bool = False,
log: bool = True,
**kwargs: Any,
) -> None:
"""Log with ERROR level and print an error message."""
if log: # we can disable logging when the logger isn't set up yet
logger.error(
message, extra=get_extra_dict(**kwargs), exc_info=exc_info, stacklevel=2
)
err_console.print(f"[error]{icon} ERROR: {message}")
def print_help(ctx: typer.Context) -> None:
err_console.print(ctx.get_help())
def exit_ok(message: Optional[str] = None, code: int = 0, **kwargs: Any) -> NoReturn:
"""Logs a message with INFO level and exits with the given code (default: 0)
Parameters
----------
message : str
Message to print.
code : int, optional
Exit code, by default 0
**kwargs
Additional keyword arguments to pass to the extra dict.
"""
if message:
info(message, **kwargs)
raise SystemExit(code)
def exit_err(
message: str, code: int = 1, exception: Optional[Exception] = None, **kwargs: Any
) -> NoReturn:
"""Logs a message with ERROR level and exits with the given
code (default: 1).
Parameters
----------
message : str
Message to print.
code : int, optional
Exit code, by default 1
**kwargs
Additional keyword arguments to pass to the extra dict.
"""
state = get_state()
# Render JSON-formatted error message if output format is JSON
if state.config.app.output.format == "json":
from zabbix_cli.exceptions import get_cause_args
from zabbix_cli.models import Result
from zabbix_cli.models import ReturnCode
from zabbix_cli.output.render import render_json
errors = get_cause_args(exception)
render_json(
Result(message=message, return_code=ReturnCode.ERROR, errors=errors)
)
else:
error(message, **kwargs)
raise SystemExit(code)
def print_toml(toml_str: str) -> None:
"""Prints TOML to stdout using the default console."""
console.print(
toml_str,
markup=False, # TOML tables could be interpreted as rich markup
soft_wrap=True, # prevents mangling whitespace
)
def print_path(path: Path) -> None:
"""Prints a path to stdout using the default console."""
console.print(
path_link(path),
highlight=False,
soft_wrap=True,
)
def disable_color() -> None:
"""Disable color output in consoles."""
console._color_system = None # pyright: ignore[reportPrivateUsage]
err_console._color_system = None # pyright: ignore[reportPrivateUsage]
# HACK: set env var to disable color in Typer console
os.environ["NO_COLOR"] = "1"
def enable_color() -> None:
"""Enable color output in consoles if supported."""
console._color_system = console._detect_color_system() # pyright: ignore[reportPrivateUsage]
err_console._color_system = console._detect_color_system() # pyright: ignore[reportPrivateUsage]
os.unsetenv("NO_COLOR") # remove hack
def configure_console(config: Config) -> None:
"""Configure console output based on the application configuration."""
if not config.app.output.color:
disable_color()
|