Coverage for src / graphable / cli / bare_cli.py: 95%
134 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-16 21:32 +0000
1from argparse import ArgumentParser
2from json import dumps
3from pathlib import Path
4from sys import exit
6from graphable.cli.commands.core import (
7 check_command,
8 checksum_command,
9 convert_command,
10 diff_command,
11 diff_visual_command,
12 info_command,
13 paths_command,
14 reduce_command,
15 render_command,
16 verify_command,
17 write_checksum_command,
18)
19from graphable.enums import Engine
22def run_bare():
23 parser = ArgumentParser(prog="graphable", description="Graphable CLI (Bare-bones)")
24 subparsers = parser.add_subparsers(dest="command", help="Commands")
26 # info
27 info_p = subparsers.add_parser("info", help="Get graph information")
28 info_p.add_argument("file", type=Path, help="Input graph file")
29 info_p.add_argument("-t", "--tag", help="Filter by tag")
30 info_p.add_argument("--upstream-of", help="Filter to ancestors of node")
31 info_p.add_argument("--downstream-of", help="Filter to descendants of node")
33 # check
34 check_p = subparsers.add_parser("check", help="Validate graph (cycles/consistency)")
35 check_p.add_argument("file", type=Path, help="Input graph file")
36 check_p.add_argument("-t", "--tag", help="Filter by tag")
37 check_p.add_argument("--upstream-of", help="Filter to ancestors of node")
38 check_p.add_argument("--downstream-of", help="Filter to descendants of node")
40 # reduce
41 reduce_p = subparsers.add_parser("reduce", help="Perform transitive reduction")
42 reduce_p.add_argument("input", type=Path, help="Input graph file")
43 reduce_p.add_argument("output", type=Path, help="Output graph file")
44 reduce_p.add_argument(
45 "--embed", action="store_true", help="Embed checksum in output"
46 )
47 reduce_p.add_argument("-t", "--tag", help="Filter by tag")
48 reduce_p.add_argument("--upstream-of", help="Filter to ancestors of node")
49 reduce_p.add_argument("--downstream-of", help="Filter to descendants of node")
51 # convert
52 convert_p = subparsers.add_parser("convert", help="Convert between formats")
53 convert_p.add_argument("input", type=Path, help="Input graph file")
54 convert_p.add_argument("output", type=Path, help="Output graph file")
55 convert_p.add_argument(
56 "--embed", action="store_true", help="Embed checksum in output"
57 )
58 convert_p.add_argument("-t", "--tag", help="Filter by tag")
59 convert_p.add_argument("--upstream-of", help="Filter to ancestors of node")
60 convert_p.add_argument("--downstream-of", help="Filter to descendants of node")
62 # render
63 render_p = subparsers.add_parser("render", help="Render graph as image")
64 render_p.add_argument("input", type=Path, help="Input graph file")
65 render_p.add_argument("output", type=Path, help="Output image file (.png, .svg)")
66 render_p.add_argument(
67 "-e",
68 "--engine",
69 choices=[e.value.lower() for e in Engine],
70 help="Rendering engine",
71 )
72 render_p.add_argument("-t", "--tag", help="Filter by tag")
73 render_p.add_argument("--upstream-of", help="Filter to ancestors of node")
74 render_p.add_argument("--downstream-of", help="Filter to descendants of node")
76 # checksum
77 checksum_p = subparsers.add_parser("checksum", help="Calculate graph checksum")
78 checksum_p.add_argument("file", type=Path, help="Graph file")
79 checksum_p.add_argument("-t", "--tag", help="Filter by tag")
80 checksum_p.add_argument("--upstream-of", help="Filter to ancestors of node")
81 checksum_p.add_argument("--downstream-of", help="Filter to descendants of node")
83 # verify
84 verify_p = subparsers.add_parser("verify", help="Verify graph checksum")
85 verify_p.add_argument("file", type=Path, help="Graph file")
86 verify_p.add_argument("--expected", help="Expected checksum (hex)")
87 verify_p.add_argument("-t", "--tag", help="Filter by tag")
88 verify_p.add_argument("--upstream-of", help="Filter to ancestors of node")
89 verify_p.add_argument("--downstream-of", help="Filter to descendants of node")
91 # write-checksum
92 wc_p = subparsers.add_parser(
93 "write-checksum", help="Write graph checksum to a file"
94 )
95 wc_p.add_argument("file", type=Path, help="Graph file")
96 wc_p.add_argument("output", type=Path, help="Output checksum file")
97 wc_p.add_argument("-t", "--tag", help="Filter by tag")
98 wc_p.add_argument("--upstream-of", help="Filter to ancestors of node")
99 wc_p.add_argument("--downstream-of", help="Filter to descendants of node")
101 # diff
102 diff_p = subparsers.add_parser("diff", help="Compare two graphs")
103 diff_p.add_argument("file1", type=Path, help="First graph file")
104 diff_p.add_argument("file2", type=Path, help="Second graph file")
105 diff_p.add_argument("-o", "--output", type=Path, help="Output file for visual diff")
106 diff_p.add_argument("-t", "--tag", help="Filter by tag")
108 # paths
109 paths_p = subparsers.add_parser("paths", help="Find all paths between two nodes")
110 paths_p.add_argument("file", type=Path, help="Graph file")
111 paths_p.add_argument("source", help="Source node reference")
112 paths_p.add_argument("target", help="Target node reference")
113 paths_p.add_argument("-t", "--tag", help="Filter by tag")
114 paths_p.add_argument("--upstream-of", help="Filter to ancestors of node")
115 paths_p.add_argument("--downstream-of", help="Filter to descendants of node")
117 # serve
118 serve_p = subparsers.add_parser("serve", help="Serve interactive visualization")
119 serve_p.add_argument("file", type=Path, help="Graph file to serve")
120 serve_p.add_argument("--port", type=int, default=8000, help="Port to serve on")
121 serve_p.add_argument("-t", "--tag", help="Filter by tag")
123 args = parser.parse_args()
125 if args.command == "info":
126 data = info_command(
127 args.file,
128 tag=args.tag,
129 upstream_of=args.upstream_of,
130 downstream_of=args.downstream_of,
131 )
132 print(f"Nodes: {data['nodes']}")
133 print(f"Edges: {data['edges']}")
134 print(f"Sources: {', '.join(data['sources'])}")
135 print(f"Sinks: {', '.join(data['sinks'])}")
136 if data.get("project_duration") is not None:
137 print(f"Project Duration: {data['project_duration']}")
138 print(f"Critical Path Length: {data['critical_path_length']}")
140 elif args.command == "check":
141 data = check_command(
142 args.file,
143 tag=args.tag,
144 upstream_of=args.upstream_of,
145 downstream_of=args.downstream_of,
146 )
147 if data["valid"]:
148 print("Graph is valid.")
149 else:
150 print(f"Graph is invalid: {data['error']}")
151 exit(1)
153 elif args.command == "reduce":
154 reduce_command(
155 args.input,
156 args.output,
157 embed_checksum=args.embed,
158 tag=args.tag,
159 upstream_of=args.upstream_of,
160 downstream_of=args.downstream_of,
161 )
162 print(f"Reduced graph saved to {args.output}")
164 elif args.command == "convert":
165 convert_command(
166 args.input,
167 args.output,
168 embed_checksum=args.embed,
169 tag=args.tag,
170 upstream_of=args.upstream_of,
171 downstream_of=args.downstream_of,
172 )
173 print(f"Converted {args.input} to {args.output}")
175 elif args.command == "render":
176 render_command(
177 args.input,
178 args.output,
179 engine=args.engine,
180 tag=args.tag,
181 upstream_of=args.upstream_of,
182 downstream_of=args.downstream_of,
183 )
184 print(f"Rendered {args.input} to {args.output}")
186 elif args.command == "checksum":
187 print(
188 checksum_command(
189 args.file,
190 tag=args.tag,
191 upstream_of=args.upstream_of,
192 downstream_of=args.downstream_of,
193 )
194 )
196 elif args.command == "verify":
197 data = verify_command(
198 args.file,
199 args.expected,
200 tag=args.tag,
201 upstream_of=args.upstream_of,
202 downstream_of=args.downstream_of,
203 )
204 if data["valid"] is True:
205 print("Checksum verified.")
206 elif data["valid"] is False:
207 print(f"Checksum mismatch! Actual: {data['actual']}")
208 exit(1)
209 else:
210 print(f"No checksum found to verify. Current: {data['actual']}")
212 elif args.command == "write-checksum":
213 write_checksum_command(
214 args.file,
215 args.output,
216 tag=args.tag,
217 upstream_of=args.upstream_of,
218 downstream_of=args.downstream_of,
219 )
220 print(f"Checksum written to {args.output}")
222 elif args.command == "diff":
223 if args.output:
224 diff_visual_command(args.file1, args.file2, args.output, tag=args.tag)
225 print(f"Visual diff saved to {args.output}")
226 else:
227 data = diff_command(args.file1, args.file2, tag=args.tag)
229 # Convert sets to lists for JSON serialization
230 serializable_data = {
231 k: list(v) if isinstance(v, set) else v for k, v in data.items()
232 }
233 # Special handling for edge tuples
234 for k in ["added_edges", "removed_edges", "modified_edges"]:
235 serializable_data[k] = [f"{u}->{v}" for u, v in data[k]]
237 print(dumps(serializable_data, indent=2))
239 elif args.command == "paths":
240 paths = paths_command(
241 args.file,
242 args.source,
243 args.target,
244 tag=args.tag,
245 upstream_of=args.upstream_of,
246 downstream_of=args.downstream_of,
247 )
248 if not paths:
249 print(f"No paths found from '{args.source}' to '{args.target}'.")
250 else:
251 print(f"Found {len(paths)} paths from '{args.source}' to '{args.target}':")
252 for i, path in enumerate(paths, 1):
253 print(f"{i}. {' -> '.join(path)}")
255 elif args.command == "serve":
256 print(f"Serving {args.file} on http://127.0.0.1:{args.port}")
257 # Note: serve_command needs update too
258 from graphable.cli.commands.serve import serve_command
260 serve_command(args.file, port=args.port, tag=args.tag)
262 else:
263 parser.print_help()
266if __name__ == "__main__":
267 run_bare()