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

1from argparse import ArgumentParser 

2from json import dumps 

3from pathlib import Path 

4from sys import exit 

5 

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 

20 

21 

22def run_bare(): 

23 parser = ArgumentParser(prog="graphable", description="Graphable CLI (Bare-bones)") 

24 subparsers = parser.add_subparsers(dest="command", help="Commands") 

25 

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") 

32 

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") 

39 

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") 

50 

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") 

61 

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") 

75 

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") 

82 

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") 

90 

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") 

100 

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") 

107 

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") 

116 

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") 

122 

123 args = parser.parse_args() 

124 

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']}") 

139 

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) 

152 

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}") 

163 

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}") 

174 

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}") 

185 

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 ) 

195 

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']}") 

211 

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}") 

221 

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) 

228 

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]] 

236 

237 print(dumps(serializable_data, indent=2)) 

238 

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)}") 

254 

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 

259 

260 serve_command(args.file, port=args.port, tag=args.tag) 

261 

262 else: 

263 parser.print_help() 

264 

265 

266if __name__ == "__main__": 

267 run_bare()