File: render.py

package info (click to toggle)
zabbix-cli 3.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,980 kB
  • sloc: python: 19,920; makefile: 5
file content (154 lines) | stat: -rw-r--r-- 4,935 bytes parent folder | download | duplicates (2)
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
from __future__ import annotations

import json
from contextlib import nullcontext
from typing import TYPE_CHECKING
from typing import Any

import typer

from zabbix_cli.output.console import console
from zabbix_cli.output.console import error
from zabbix_cli.output.console import success
from zabbix_cli.state import get_state

if TYPE_CHECKING:
    from pydantic import BaseModel

    from zabbix_cli.models import BaseResult
    from zabbix_cli.models import TableRenderable


def wrap_result(result: BaseModel) -> BaseResult:
    """Wraps a BaseModel instance in a Result object so that it receives
    `return_code`, `errors`, and `message` fields, with the original object
    is available as `result`.

    Does nothing if the function argument is already a BaseResult instance.
    """
    from zabbix_cli.models import BaseResult
    from zabbix_cli.models import Result

    if isinstance(result, BaseResult):
        return result
    # TODO: handle AggregateResult? (8 months later: What did I mean by this?)
    return Result(result=result)


def render_result(
    result: TableRenderable,
    ctx: typer.Context | None = None,
    **kwargs: Any,
) -> None:
    """Render the result of a command stdout or file.

    Parameters
    ----------
    result: TableRenderable,
        The result of a command. All commands produce a TableRenderable (BaseModel).
    ctx : typer.Context, optional
        The typer context from the command invocation, by default None
    **kwargs
        Additional keyword arguments to pass to the render function.
    """
    from zabbix_cli.config.constants import OutputFormat

    # Short form aliases
    state = get_state()
    fmt = state.config.app.output.format
    # paging = state.config.output.paging
    paging = False  # TODO: implement

    ctx_manager = console.pager() if paging else nullcontext()
    with ctx_manager:
        if fmt == OutputFormat.JSON:
            if state.config.app.legacy_json_format:
                render_json_legacy(result, ctx, **kwargs)
            else:
                render_json(result, ctx, **kwargs)
        elif fmt == OutputFormat.TABLE:
            render_table(result, ctx, **kwargs)
        # TODO: implement CSV
        else:
            raise ValueError(f"Unknown output format {fmt!r}.")


def render_table(
    result: TableRenderable, ctx: typer.Context | None = None, **kwargs: Any
) -> None:
    """Render the result of a command as a table if possible.
    If result contains a message, print success message instead.
    """
    # TODO: be able to print message _AND_ table
    # The Result/TableRenderable dichotomy is a bit of a mess
    from zabbix_cli.models import Result
    from zabbix_cli.models import ReturnCode

    if isinstance(result, Result) and result.message:
        if result.return_code == ReturnCode.ERROR:
            error(result.message)
        else:
            success(result.message)
    else:
        tbl = result.as_table()
        if not tbl.rows:
            if not result.empty_ok:
                console.print("No results found.")
        else:
            console.print(tbl)


def render_json(
    result: TableRenderable,
    ctx: typer.Context | None = None,
    **kwargs: Any,
) -> None:
    """Render the result of a command as JSON."""
    from zabbix_cli.models import ReturnCode

    result = wrap_result(result)
    o_json = result.model_dump_json(indent=2, by_alias=True)
    console.print_json(o_json, indent=2, sort_keys=False)
    if result.message:
        if result.return_code == ReturnCode.ERROR:
            error(result.message)
        else:
            success(result.message)


def render_json_legacy(
    result: TableRenderable,
    ctx: typer.Context | None = None,
    **kwargs: Any,
) -> None:
    """Render the result of a command as JSON (legacy V2 format).

    Result is always a dict with numeric string keys.

    Note:
    ----
    This function is very hacky, and will inevitably contain a number of band-aid
    fixes to enable 1:1 compatibility with the legacy V2 JSON format.
    We should try to move away from this format ASAP, so we can remove
    this function and all its hacks.
    """
    from zabbix_cli.models import Result

    # If we have a message, it should not be indexed
    # NOTE: do we have a more accurate heuristic for this?
    if isinstance(result, Result) and result.message:
        j = result.model_dump_json(indent=2)
    else:
        from zabbix_cli.models import AggregateResult

        jdict: dict[str, Any] = {}  # always a dict in legacy mode
        res = result.model_dump(mode="json", by_alias=True)
        if isinstance(result, AggregateResult):
            py_result = res.get("result", [])
        else:
            py_result = [res]

        for idx, item in enumerate(py_result):
            jdict[str(idx)] = item
        j = json.dumps(jdict, indent=2)
    console.print_json(j, indent=2, sort_keys=False)