feat: add A-card operations frontend and backend foundation
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""Render the versioned A-card architecture YAML files to PNG and SVG.
|
||||
|
||||
Setup once:
|
||||
python3 -m pip install diagrams pyyaml
|
||||
|
||||
Usage:
|
||||
python3 docs/architecture/render_diagrams.py
|
||||
python3 docs/architecture/render_diagrams.py path/to/one.yaml
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from diagrams import Cluster, Diagram, Edge
|
||||
|
||||
|
||||
ICONS = {
|
||||
"users": "diagrams.onprem.client:Users",
|
||||
"nginx": "diagrams.onprem.network:Nginx",
|
||||
"server": "diagrams.onprem.compute:Server",
|
||||
"storage": "diagrams.generic.storage:Storage",
|
||||
"process": "diagrams.programming.flowchart:PredefinedProcess",
|
||||
"flowdb": "diagrams.programming.flowchart:Database",
|
||||
"fastapi": "diagrams.programming.framework:Fastapi",
|
||||
"react": "diagrams.programming.framework:React",
|
||||
"mysql": "diagrams.onprem.database:MySQL",
|
||||
}
|
||||
|
||||
EDGE_TYPES = {
|
||||
"flow": {"color": "#2f6fe0"},
|
||||
"write": {"color": "#3f9ad0"},
|
||||
"async": {"color": "#e0902f", "style": "dashed"},
|
||||
"obs": {"color": "#4f9d78", "style": "dashed"},
|
||||
"dep": {"color": "#8b6fc4", "style": "dashed"},
|
||||
"plain": {"color": "#9aa2ad"},
|
||||
}
|
||||
|
||||
|
||||
def resolve_icon(name: str) -> Any:
|
||||
module_path, class_name = ICONS[name].split(":")
|
||||
return getattr(importlib.import_module(module_path), class_name)
|
||||
|
||||
|
||||
def cluster_attr(style: dict[str, Any], font: str) -> dict[str, str]:
|
||||
return {
|
||||
"bgcolor": style.get("bg", "#F5F6F8"),
|
||||
"pencolor": style.get("pen", "#B8C0CC"),
|
||||
"style": "rounded,dashed" if style.get("dashed") else "rounded",
|
||||
"fontname": font,
|
||||
"fontsize": str(style.get("fontsize", 15)),
|
||||
"margin": str(style.get("margin", 18)),
|
||||
}
|
||||
|
||||
|
||||
def build_nodes(definitions: list[dict], registry: dict[str, Any]) -> None:
|
||||
for definition in definitions:
|
||||
label = str(definition.get("label", definition["id"])).replace("\\n", "\n")
|
||||
registry[definition["id"]] = resolve_icon(definition.get("icon", "server"))(label)
|
||||
|
||||
|
||||
def build_clusters(
|
||||
definitions: list[dict], registry: dict[str, Any], font: str
|
||||
) -> None:
|
||||
for definition in definitions:
|
||||
with Cluster(
|
||||
definition["name"],
|
||||
graph_attr=cluster_attr(definition.get("style", {}), font),
|
||||
):
|
||||
build_nodes(definition.get("nodes", []), registry)
|
||||
build_clusters(definition.get("clusters", []), registry, font)
|
||||
|
||||
|
||||
def draw_edges(definitions: list[dict], registry: dict[str, Any]) -> None:
|
||||
for definition in definitions:
|
||||
attributes = dict(
|
||||
EDGE_TYPES.get(definition.get("type", "plain"), EDGE_TYPES["plain"])
|
||||
)
|
||||
for key, output_key in (
|
||||
("label", "label"),
|
||||
("style", "style"),
|
||||
("width", "penwidth"),
|
||||
("color", "color"),
|
||||
("dir", "dir"),
|
||||
):
|
||||
if key in definition:
|
||||
value = definition[key]
|
||||
attributes[output_key] = (
|
||||
str(value).replace("\\n", "\n") if key == "label" else str(value)
|
||||
)
|
||||
if definition.get("free"):
|
||||
attributes["constraint"] = "false"
|
||||
registry[definition["from"]] >> Edge(**attributes) >> registry[definition["to"]]
|
||||
|
||||
|
||||
def render(path: Path) -> None:
|
||||
spec = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
theme = spec.get("theme", {})
|
||||
font = theme.get("font", "PingFang SC")
|
||||
graph_attr = {
|
||||
"fontname": font,
|
||||
"fontsize": str(theme.get("title_size", 28)),
|
||||
"bgcolor": "white",
|
||||
"pad": str(theme.get("pad", 0.7)),
|
||||
"splines": theme.get("splines", "ortho"),
|
||||
"nodesep": str(theme.get("nodesep", 0.75)),
|
||||
"ranksep": str(theme.get("ranksep", 1.0)),
|
||||
"compound": "true",
|
||||
"newrank": "true",
|
||||
"label": str(spec.get("title", "")).replace("\\n", "\n"),
|
||||
"labelloc": "t",
|
||||
}
|
||||
for optional in ("dpi", "size"):
|
||||
if theme.get(optional):
|
||||
graph_attr[optional] = str(theme[optional])
|
||||
|
||||
registry: dict[str, Any] = {}
|
||||
with Diagram(
|
||||
str(spec.get("title", path.stem)).replace("\\n", "\n"),
|
||||
filename=str(path.with_suffix("")),
|
||||
show=False,
|
||||
direction=spec.get("direction", "TB"),
|
||||
graph_attr=graph_attr,
|
||||
node_attr={"fontname": font, "fontsize": str(theme.get("node_size", 12))},
|
||||
edge_attr={"fontname": font, "fontsize": str(theme.get("edge_size", 10))},
|
||||
outformat=spec.get("formats", ["png", "svg"]),
|
||||
):
|
||||
build_nodes(spec.get("nodes", []), registry)
|
||||
build_clusters(spec.get("clusters", []), registry, font)
|
||||
draw_edges(spec.get("edges", []), registry)
|
||||
print(f"rendered {path.name}: {len(registry)} nodes")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("paths", nargs="*", type=Path)
|
||||
args = parser.parse_args()
|
||||
paths = args.paths or sorted(Path(__file__).parent.glob("*.yaml"))
|
||||
for path in paths:
|
||||
render(path.resolve())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user