Skip to content

Commit 39da62f

Browse files
authored
🎉 [automated-actions-cli] output format (#94)
* 📌 upgrade dependencies and pin click * ✨ [automated-actions-cli] output format * ⏪ revert default environment setting * 🚨 add types-pyyaml * ✅ formatter tests * 🚨 fix linter * update base images
1 parent 11d811f commit 39da62f

File tree

18 files changed

+489
-392
lines changed

18 files changed

+489
-392
lines changed

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# Base image with defaults for all stages
3-
FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c AS base
3+
FROM registry.access.redhat.com/ubi9-minimal@sha256:21ed5a01130d3a77eb4a26a80de70a4433860255b394e95dc2c26817de1da061 AS base
44

55
COPY LICENSE /licenses/
66

@@ -25,7 +25,7 @@ WORKDIR ${APP_ROOT}/src
2525
#
2626
# Builder image
2727
#
28-
FROM registry.access.redhat.com/ubi9/python-312@sha256:306e4320c559d67c60874e3bb85c9f84afc84b66c350b3d4afe0abd65201d6e6 AS builder
28+
FROM registry.access.redhat.com/ubi9/python-312@sha256:aa2a3c086013ce259af562ddc64c5aa55471c42a2a8a731f185767e86a94283b AS builder
2929
COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc1514a93369bfce6930eb06c8d576e58a4 /uv /bin/uv
3030
ENV \
3131
# use venv from ubi image
@@ -52,7 +52,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc151
5252

5353
COPY Makefile ./
5454
COPY --from=builder /opt/app-root /opt/app-root
55-
RUN uv sync --frozen
55+
RUN uv sync --frozen --verbose
5656
RUN make test
5757

5858

Dockerfile.cli

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM registry.access.redhat.com/ubi9/python-312@sha256:306e4320c559d67c60874e3bb85c9f84afc84b66c350b3d4afe0abd65201d6e6 AS base
1+
FROM registry.access.redhat.com/ubi9/python-312@sha256:aa2a3c086013ce259af562ddc64c5aa55471c42a2a8a731f185767e86a94283b AS base
22
COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc1514a93369bfce6930eb06c8d576e58a4 /uv /bin/uv
33

44
COPY LICENSE /licenses/

Dockerfile.client

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM registry.access.redhat.com/ubi9/python-312@sha256:306e4320c559d67c60874e3bb85c9f84afc84b66c350b3d4afe0abd65201d6e6 AS base
1+
FROM registry.access.redhat.com/ubi9/python-312@sha256:aa2a3c086013ce259af562ddc64c5aa55471c42a2a8a731f185767e86a94283b AS base
22
COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc1514a93369bfce6930eb06c8d576e58a4 /uv /bin/uv
33

44
COPY LICENSE /licenses/

Dockerfile.integration_tests

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# Base image with defaults for all stages
3-
FROM registry.access.redhat.com/ubi9/ubi-minimal@sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c AS base
3+
FROM registry.access.redhat.com/ubi9-minimal@sha256:21ed5a01130d3a77eb4a26a80de70a4433860255b394e95dc2c26817de1da061 AS base
44
COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc1514a93369bfce6930eb06c8d576e58a4 /uv /bin/uv
55

66
COPY LICENSE /licenses/
@@ -25,7 +25,7 @@ WORKDIR ${APP_ROOT}/src
2525
#
2626
# Builder image
2727
#
28-
FROM registry.access.redhat.com/ubi9/python-312@sha256:306e4320c559d67c60874e3bb85c9f84afc84b66c350b3d4afe0abd65201d6e6 AS builder
28+
FROM registry.access.redhat.com/ubi9/python-312@sha256:aa2a3c086013ce259af562ddc64c5aa55471c42a2a8a731f185767e86a94283b AS builder
2929
COPY --from=ghcr.io/astral-sh/uv:0.7.3@sha256:87a04222b228501907f487b338ca6fc1514a93369bfce6930eb06c8d576e58a4 /uv /bin/uv
3030

3131
ENV \

Dockerfile.opa

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM registry.access.redhat.com/ubi9-minimal@sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c AS base
1+
FROM registry.access.redhat.com/ubi9-minimal@sha256:21ed5a01130d3a77eb4a26a80de70a4433860255b394e95dc2c26817de1da061 AS base
22
COPY --from=openpolicyagent/opa:1.4.2-static@sha256:3c995dc8a59f6ddfd92eb7404d2f7ff9fe71cd025d9251199957a8a6afbfd76e /opa /opa
33

44
ENV PATH=${PATH}:/

packages/automated_actions_cli/automated_actions_cli/cli.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from rich.console import Console
1919

2020
from automated_actions_cli.config import config
21+
from automated_actions_cli.formatter import JsonFormatter, OutputFormat, YamlFormatter
2122
from automated_actions_cli.utils import (
2223
blend_text,
2324
kerberos_available,
@@ -82,12 +83,29 @@ def __exit__(self, *args: object, **kwargs: Any) -> None:
8283
def main(
8384
ctx: typer.Context,
8485
*,
85-
debug: Annotated[bool, typer.Option(help="Enable debug")] = False,
86-
screen_capture_file: Annotated[Path | None, typer.Option(writable=True)] = None,
86+
debug: Annotated[
87+
bool, typer.Option(help="Enable debug", envvar="AA_DEBUG")
88+
] = False,
89+
screen_capture_file: Annotated[
90+
Path | None,
91+
typer.Option(
92+
help="Capture screen recording as SVG",
93+
writable=True,
94+
envvar="AA_SCREEN_CAPTURE_FILE",
95+
),
96+
] = None,
8797
version: Annotated[ # noqa: ARG001
8898
bool | None, typer.Option(callback=version_callback, help="Display version")
8999
] = None,
90-
quiet: bool = typer.Option(default=False, help="Don't print anything"),
100+
quiet: Annotated[
101+
bool, typer.Option(help="Don't print anything", envvar="AA_QUIET")
102+
] = False,
103+
output: Annotated[
104+
OutputFormat, typer.Option(help="Output format", envvar="AA_OUTPUT")
105+
] = OutputFormat.yaml,
106+
color: Annotated[
107+
bool, typer.Option(help="Use colored output", envvar="AA_COLOR")
108+
] = True,
91109
) -> None:
92110
if "--help" in sys.argv:
93111
rich_print(
@@ -96,7 +114,7 @@ def main(
96114
# do not initialize the client and everything else if --help is passed
97115
return
98116

99-
if not quiet:
117+
if not quiet and not screen_capture_file:
100118
progress = progress_spinner(console=console)
101119
progress.start()
102120
progress.add_task(description="Processing...", total=None)
@@ -115,8 +133,7 @@ def main(
115133
token=token,
116134
raise_on_unexpected_status=True,
117135
follow_redirects=True,
118-
),
119-
"console": console,
136+
)
120137
}
121138

122139
elif kerberos_available():
@@ -129,28 +146,35 @@ def main(
129146
httpx_args={
130147
"auth": HTTPSPNEGOAuth(mutual_authentication=OPTIONAL),
131148
},
132-
),
133-
"console": console,
149+
)
134150
}
135151
else:
136152
logger.error(
137153
"No bearer token or Kerberos authentication available. Please set AA_TOKEN or install and configure Kerberos."
138154
)
139155
raise typer.Exit(1)
140156

157+
printer = console.print if color else print
158+
match output:
159+
case OutputFormat.json:
160+
ctx.obj["formatter"] = JsonFormatter(printer=printer)
161+
case OutputFormat.yaml:
162+
ctx.obj["formatter"] = YamlFormatter(printer=printer)
163+
case _:
164+
raise ValueError("Invalid output format")
165+
141166
# enforce the user to login
142167
api_v1_me(client=ctx.obj["client"])
143168

144169
if screen_capture_file is not None:
170+
screen_capture_file = screen_capture_file.with_suffix(".svg")
145171
rich_print(f"Screen recording: {screen_capture_file}")
146172
# strip $0 and screen_capture_file option
147173
args = sys.argv[3:]
148174
console.print(f"$ automated-actions {' '.join(args)}")
149175
# title = command sub_command
150176
title = " ".join(args[0:2])
151-
atexit.register(
152-
console.save_svg, str(screen_capture_file.with_suffix(".svg")), title=title
153-
)
177+
atexit.register(console.save_svg, str(screen_capture_file), title=title)
154178

155179

156180
def initialize_client_actions() -> None:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
from collections.abc import Callable
3+
from enum import StrEnum
4+
5+
import yaml
6+
7+
8+
class OutputFormat(StrEnum):
9+
"""Output format for the command line interface."""
10+
11+
json = "json"
12+
yaml = "yaml"
13+
14+
15+
class JsonFormatter:
16+
def __init__(self, printer: Callable[[str], None], indent: int = 4) -> None:
17+
self._printer = printer
18+
self._indent = indent
19+
20+
def __call__(self, data: dict) -> None:
21+
"""Format the data as JSON."""
22+
self._printer(json.dumps(data, indent=self._indent, sort_keys=True))
23+
24+
25+
class YamlFormatter:
26+
def __init__(self, printer: Callable[[str], None], indent: int = 2) -> None:
27+
self._printer = printer
28+
self._indent = indent
29+
30+
def __call__(self, data: dict) -> None:
31+
"""Format the data as yaml."""
32+
self._printer(
33+
yaml.dump(data, indent=self._indent, sort_keys=True, explicit_start=True)
34+
)

packages/automated_actions_cli/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ requires-python = "~= 3.12.0"
1212
dependencies = [
1313
"appdirs==1.4.4",
1414
"automated-actions-client",
15+
# https://github.com/fastapi/typer/pull/1145
16+
"click<8.2.0",
1517
"httpx-gssapi==0.4",
1618
"pydantic-settings==2.9.1",
19+
"pyyaml==6.0.2",
1720
"rich==14.0.0",
1821
"typer==0.15.3",
1922
]
@@ -30,6 +33,7 @@ dev = [
3033
"mypy==1.15.0",
3134
"pytest==8.3.5",
3235
"pytest-cov==6.1.1",
36+
"types-pyyaml==6.0.12.20250402",
3337
]
3438

3539
[project.scripts]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from automated_actions_cli.formatter import JsonFormatter, YamlFormatter
2+
3+
4+
def test_formatter_json() -> None:
5+
def check_output(output: str) -> None:
6+
assert output.replace("\n", "") == '{"key": "value"}'
7+
8+
formatter = JsonFormatter(printer=check_output, indent=0)
9+
formatter({"key": "value"})
10+
11+
12+
def test_formatter_yaml() -> None:
13+
def check_output(output: str) -> None:
14+
assert (
15+
output
16+
== """---
17+
key: value\n"""
18+
)
19+
20+
formatter = YamlFormatter(printer=check_output, indent=0)
21+
formatter({"key": "value"})

packages/automated_actions_cli/tests/test_stub.py

Lines changed: 0 additions & 2 deletions
This file was deleted.

packages/automated_actions_client/automated_actions_client/api/v1/action_cancel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,5 @@ def action_cancel(
180180
action_id: Annotated[str, typer.Option(help="", show_default=False)],
181181
) -> None:
182182
result = sync(action_id=action_id, client=ctx.obj["client"])
183-
if "console" in ctx.obj:
184-
ctx.obj["console"].print(result)
183+
if "formatter" in ctx.obj and result:
184+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/automated_actions_client/api/v1/action_detail.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,5 @@ def action_detail(
180180
action_id: Annotated[str, typer.Option(help="", show_default=False)],
181181
) -> None:
182182
result = sync(action_id=action_id, client=ctx.obj["client"])
183-
if "console" in ctx.obj:
184-
ctx.obj["console"].print(result)
183+
if "formatter" in ctx.obj and result:
184+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/automated_actions_client/api/v1/action_list.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,5 @@ def action_list(
203203
] = ActionStatus.RUNNING,
204204
) -> None:
205205
result = sync(status=status, client=ctx.obj["client"])
206-
if "console" in ctx.obj:
207-
ctx.obj["console"].print(result)
206+
if "formatter" in ctx.obj and result:
207+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/automated_actions_client/api/v1/create_token.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,5 +200,5 @@ def create_token(
200200
),
201201
client=ctx.obj["client"],
202202
)
203-
if "console" in ctx.obj:
204-
ctx.obj["console"].print(result)
203+
if "formatter" in ctx.obj and result:
204+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/automated_actions_client/api/v1/me.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,5 @@ def me(
148148
ctx: typer.Context,
149149
) -> None:
150150
result = sync(client=ctx.obj["client"])
151-
if "console" in ctx.obj:
152-
ctx.obj["console"].print(result)
151+
if "formatter" in ctx.obj and result:
152+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/automated_actions_client/api/v1/openshift_workload_restart.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,5 +240,5 @@ def openshift_workload_restart(
240240
name=name,
241241
client=ctx.obj["client"],
242242
)
243-
if "console" in ctx.obj:
244-
ctx.obj["console"].print(result)
243+
if "formatter" in ctx.obj and result:
244+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)

packages/automated_actions_client/openapi_python_client_templates/typer_command.py.jinja

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ def {{endpoint.name | replace("-", "_") }}(
6464

6565
client=ctx.obj["client"]
6666
)
67-
if "console" in ctx.obj:
68-
ctx.obj["console"].print(result)
67+
if "formatter" in ctx.obj and result:
68+
ctx.obj["formatter"](result.to_dict() if hasattr(result, "to_dict") else result)
6969
{% endif %}

0 commit comments

Comments
 (0)