generated from coulomb/repo-seed
Establish Railiance Fabric graph model
This commit is contained in:
183
railiance_fabric/cli.py
Normal file
183
railiance_fabric/cli.py
Normal file
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from .graph import FabricGraph, build_graph
|
||||
from .validation import validate_roots
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="railiance-fabric",
|
||||
description="Load and validate Railiance Fabric declarations.",
|
||||
)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
validate = sub.add_parser(
|
||||
"validate",
|
||||
help="Validate one or more repo roots or declaration files.",
|
||||
)
|
||||
validate.add_argument(
|
||||
"paths",
|
||||
nargs="+",
|
||||
type=Path,
|
||||
help="Repo root, fabric directory, or declaration YAML file.",
|
||||
)
|
||||
validate.add_argument(
|
||||
"--warnings-as-errors",
|
||||
action="store_true",
|
||||
help="Exit non-zero when warnings are present.",
|
||||
)
|
||||
|
||||
providers = sub.add_parser("providers", help="List providers for a capability type or id.")
|
||||
providers.add_argument("capability", help="Capability type or capability id.")
|
||||
providers.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
consumers = sub.add_parser("consumers", help="List consumers of a capability or interface.")
|
||||
consumers.add_argument("target", help="Capability/interface type or declaration id.")
|
||||
consumers.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
dependency_path = sub.add_parser("dependency-path", help="Show dependency path for a service.")
|
||||
dependency_path.add_argument("service_id", help="Service declaration id.")
|
||||
dependency_path.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
unresolved = sub.add_parser("unresolved", help="Show missing or unresolved dependencies.")
|
||||
unresolved.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
blast = sub.add_parser("blast-radius", help="Show consumers affected by an interface change.")
|
||||
blast.add_argument("interface", help="Interface type or interface declaration id.")
|
||||
blast.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
|
||||
export = sub.add_parser("export", help="Export graph as JSON or Mermaid.")
|
||||
export.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
|
||||
export.add_argument("--format", choices=["json", "mermaid"], default="json")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if args.command == "validate":
|
||||
report = validate_roots(args.paths)
|
||||
for diagnostic in report.diagnostics:
|
||||
print(diagnostic.format())
|
||||
print(report.summary())
|
||||
if report.errors:
|
||||
return 1
|
||||
if args.warnings_as_errors and report.warnings:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if args.command == "providers":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
_print_providers(graph, args.capability)
|
||||
return 0
|
||||
|
||||
if args.command == "consumers":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
_print_consumers(graph, args.target)
|
||||
return 0
|
||||
|
||||
if args.command == "dependency-path":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
print("\n".join(graph.dependency_path_lines(args.service_id)))
|
||||
return 0
|
||||
|
||||
if args.command == "unresolved":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
_print_unresolved(graph)
|
||||
return 0
|
||||
|
||||
if args.command == "blast-radius":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
_print_consumers(graph, args.interface, matches=graph.blast_radius(args.interface))
|
||||
return 0
|
||||
|
||||
if args.command == "export":
|
||||
graph = _load_graph_or_exit(args.paths)
|
||||
print(graph.to_mermaid() if args.format == "mermaid" else graph.to_json())
|
||||
return 0
|
||||
|
||||
parser.error(f"unknown command {args.command!r}")
|
||||
return 2
|
||||
|
||||
def _load_graph_or_exit(paths: list[Path]) -> FabricGraph:
|
||||
graph = build_graph(paths)
|
||||
if graph.load_errors:
|
||||
for path, message in graph.load_errors:
|
||||
print(f"ERROR {path}: {message}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
return graph
|
||||
|
||||
|
||||
def _print_providers(graph: FabricGraph, capability: str) -> None:
|
||||
providers = graph.providers(capability)
|
||||
if not providers:
|
||||
print(f"no providers found for {capability}")
|
||||
return
|
||||
print("provider_id\tservice_id\tlifecycle\tenvironments\tinterfaces")
|
||||
for provider in providers:
|
||||
spec = provider.spec
|
||||
print(
|
||||
"\t".join(
|
||||
[
|
||||
provider.id,
|
||||
str(spec.get("service_id", "")),
|
||||
str(spec.get("lifecycle", "")),
|
||||
",".join(spec.get("environments", [])),
|
||||
",".join(spec.get("interface_ids", [])),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _print_consumers(
|
||||
graph: FabricGraph,
|
||||
target: str,
|
||||
matches: object | None = None,
|
||||
) -> None:
|
||||
consumer_matches = graph.consumers(target) if matches is None else list(matches)
|
||||
if not consumer_matches:
|
||||
print(f"no consumers found for {target}")
|
||||
return
|
||||
print("consumer_service_id\tdependency_id\trequires\tprovider_capability_id\tprovider_interface_id\tstatus")
|
||||
for match in consumer_matches:
|
||||
print(
|
||||
"\t".join(
|
||||
[
|
||||
match.consumer_service_id,
|
||||
match.dependency_id,
|
||||
match.required_capability_type,
|
||||
match.provider_capability_id,
|
||||
match.provider_interface_id,
|
||||
match.status,
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _print_unresolved(graph: FabricGraph) -> None:
|
||||
unresolved = graph.unresolved_dependencies()
|
||||
if not unresolved:
|
||||
print("no unresolved dependencies")
|
||||
return
|
||||
print("dependency_id\tconsumer_service_id\trequires")
|
||||
for dependency in unresolved:
|
||||
spec = dependency.spec
|
||||
requires = spec.get("requires", {})
|
||||
print(
|
||||
"\t".join(
|
||||
[
|
||||
dependency.id,
|
||||
str(spec.get("consumer_service_id", "")),
|
||||
str(requires.get("capability_id") or requires.get("capability_type", "")),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user