149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
"""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()
|