Skip to content

Commit 7fb4171

Browse files
committed
feat(cli): add build/validate/graph commands with flows auto-discovery
1 parent a707911 commit 7fb4171

1 file changed

Lines changed: 137 additions & 0 deletions

File tree

src/fastapi_cloudflow/cli.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import importlib
2+
from pathlib import Path
3+
4+
import typer
5+
6+
from fastapi_cloudflow.codegen.workflows import emit_workflow_yaml
7+
from fastapi_cloudflow.core import get_workflows
8+
9+
app = typer.Typer(help="FastAPI CloudFlow CLI")
10+
11+
12+
def _import_app(app_spec: str | None) -> None:
13+
if not app_spec:
14+
return
15+
module_name, _, _attr = app_spec.partition(":")
16+
importlib.import_module(module_name)
17+
18+
19+
def _discover_flow_modules(flows_path: Path) -> list[str]:
20+
modules: list[str] = []
21+
root = Path.cwd()
22+
if not flows_path.exists():
23+
return modules
24+
for py in flows_path.rglob("*.py"):
25+
if py.name == "__init__.py":
26+
continue
27+
try:
28+
rel = py.relative_to(root)
29+
except ValueError:
30+
rel = py
31+
mod = ".".join(rel.with_suffix("").parts)
32+
modules.append(mod)
33+
return modules
34+
35+
36+
@app.command()
37+
def build(
38+
module: list[str] | None = None,
39+
out: Path = Path("build/workflows"),
40+
base_url: str | None = None,
41+
app_spec: str | None = None,
42+
flows_path: Path = Path("app/flows"),
43+
):
44+
module = module or []
45+
if module:
46+
for m in module:
47+
__import__(m)
48+
elif app_spec:
49+
# App is responsible for importing/registering flows; avoid double-imports
50+
_import_app(app_spec)
51+
else:
52+
# No app specified; import flows from the provided path
53+
for m in _discover_flow_modules(flows_path):
54+
__import__(m)
55+
workflows = get_workflows()
56+
out.mkdir(parents=True, exist_ok=True)
57+
for wf in workflows:
58+
emit_workflow_yaml(wf, out, base_url_expr=f'"{base_url}"' if base_url else None)
59+
print(f"wrote {out / (wf.name + '.yaml')}")
60+
61+
62+
@app.command()
63+
def validate(
64+
module: list[str] | None = None,
65+
app_spec: str | None = None,
66+
flows_path: Path = Path("app/flows"),
67+
):
68+
module = module or []
69+
if module:
70+
for m in module:
71+
__import__(m)
72+
elif app_spec:
73+
_import_app(app_spec)
74+
else:
75+
for m in _discover_flow_modules(flows_path):
76+
__import__(m)
77+
workflows = get_workflows()
78+
for wf in workflows:
79+
if not wf.nodes:
80+
raise typer.Exit(code=1)
81+
print("Validation OK")
82+
83+
84+
@app.command()
85+
def graph(
86+
module: list[str] | None = None,
87+
out: Path | None = None,
88+
app_spec: str | None = None,
89+
flows_path: Path = Path("app/flows"),
90+
per_workflow: bool = False,
91+
):
92+
module = module or []
93+
if module:
94+
for m in module:
95+
__import__(m)
96+
elif app_spec:
97+
_import_app(app_spec)
98+
else:
99+
for m in _discover_flow_modules(flows_path):
100+
__import__(m)
101+
workflows = get_workflows()
102+
103+
if per_workflow:
104+
# Write one Mermaid file per workflow into the provided directory
105+
if out is None:
106+
out = Path("build/graphs")
107+
out.mkdir(parents=True, exist_ok=True)
108+
for wf in workflows:
109+
lines: list[str] = ["graph TD"]
110+
# Keep subgraph for consistency/labeling even when single workflow
111+
lines.append(f" subgraph {wf.name}")
112+
for node in wf.nodes:
113+
lines.append(f" {node.name}[{node.name}]")
114+
for a, b in zip(wf.nodes, wf.nodes[1:], strict=False):
115+
lines.append(f" {a.name} --> {b.name}")
116+
lines.append(" end")
117+
content = "\n".join(lines) + "\n"
118+
target = out / f"{wf.name}.mmd"
119+
target.write_text(content, encoding="utf-8")
120+
print(f"wrote {target}")
121+
else:
122+
lines: list[str] = ["graph TD"]
123+
for wf in workflows:
124+
lines.append(f" subgraph {wf.name}")
125+
for node in wf.nodes:
126+
lines.append(f" {node.name}[{node.name}]")
127+
for a, b in zip(wf.nodes, wf.nodes[1:], strict=False):
128+
lines.append(f" {a.name} --> {b.name}")
129+
lines.append(" end")
130+
131+
content = "\n".join(lines) + "\n"
132+
if out is None:
133+
print(content)
134+
else:
135+
out.parent.mkdir(parents=True, exist_ok=True)
136+
out.write_text(content, encoding="utf-8")
137+
print(f"wrote {out}")

0 commit comments

Comments
 (0)