cgcardona / muse public
attributes.py python
115 lines 3.4 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """muse attributes — read and validate the .museattributes configuration file.
2
3 A plumbing command that parses the ``.museattributes`` file in the current
4 repository root and displays the resulting rules in a human-readable table
5 (or machine-readable JSON).
6
7 Usage::
8
9 muse attributes [--json]
10
11 Exit codes:
12 - ``0``: parsed successfully (or file not found — that is not an error).
13 - ``1``: file is present but contains no valid rules.
14 - ``3``: internal error.
15 """
16 from __future__ import annotations
17
18 import json
19 import logging
20
21 import typer
22 from typing_extensions import Annotated
23
24 from maestro.muse_cli._repo import require_repo
25 from maestro.muse_cli.errors import ExitCode
26 from maestro.services.muse_attributes import (
27 MuseAttribute,
28 load_attributes,
29 )
30
31 logger = logging.getLogger(__name__)
32
33 app = typer.Typer(no_args_is_help=False)
34
35
36 # ---------------------------------------------------------------------------
37 # Output formatters
38 # ---------------------------------------------------------------------------
39
40
41 def _format_attributes(attributes: list[MuseAttribute], *, as_json: bool) -> str:
42 """Render parsed attributes as a table or JSON."""
43 if as_json:
44 payload = [
45 {
46 "track_pattern": a.track_pattern,
47 "dimension": a.dimension,
48 "strategy": a.strategy.value,
49 }
50 for a in attributes
51 ]
52 return json.dumps(payload, indent=2)
53
54 if not attributes:
55 return "No rules defined. Create a .museattributes file in the repository root."
56
57 col_widths = (
58 max(len(a.track_pattern) for a in attributes),
59 max(len(a.dimension) for a in attributes),
60 max(len(a.strategy.value) for a in attributes),
61 )
62 col_widths = (
63 max(col_widths[0], len("Track Pattern")),
64 max(col_widths[1], len("Dimension")),
65 max(col_widths[2], len("Strategy")),
66 )
67
68 sep = " "
69 header = sep.join(
70 label.ljust(w)
71 for label, w in zip(
72 ("Track Pattern", "Dimension", "Strategy"), col_widths, strict=True
73 )
74 )
75 divider = sep.join("-" * w for w in col_widths)
76 rows = [
77 sep.join(
78 cell.ljust(w)
79 for cell, w in zip(
80 (a.track_pattern, a.dimension, a.strategy.value), col_widths, strict=True
81 )
82 )
83 for a in attributes
84 ]
85 lines = [f".museattributes — {len(attributes)} rule(s)", "", header, divider, *rows]
86 return "\n".join(lines)
87
88
89 # ---------------------------------------------------------------------------
90 # Command
91 # ---------------------------------------------------------------------------
92
93
94 @app.callback(invoke_without_command=True)
95 def attributes_show(
96 as_json: Annotated[
97 bool,
98 typer.Option("--json", help="Emit machine-readable JSON output."),
99 ] = False,
100 ) -> None:
101 """Read and display the .museattributes merge-strategy configuration."""
102 root = require_repo()
103
104 try:
105 rules = load_attributes(root)
106 output = _format_attributes(rules, as_json=as_json)
107 typer.echo(output)
108 if not rules:
109 raise typer.Exit(code=ExitCode.USER_ERROR)
110 except typer.Exit:
111 raise
112 except Exception as exc:
113 typer.echo(f"❌ muse attributes failed: {exc}")
114 logger.error("❌ muse attributes error: %s", exc, exc_info=True)
115 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)