Coverage for src / graphable / views / utils.py: 83%

48 statements  

« prev     ^ index     » next       coverage.py v7.13.3, created at 2026-02-16 21:32 +0000

1from shutil import which 

2from typing import Callable 

3 

4 

5def wrap_with_checksum(content: str, checksum: str, extension: str) -> str: 

6 """ 

7 Prepend a checksum as a comment to the content based on file extension. 

8 

9 Args: 

10 content: The original content string. 

11 checksum: The blake2b hash to embed. 

12 extension: The file extension (e.g., '.json', '.yaml'). 

13 

14 Returns: 

15 str: The content with the checksum comment prepended. 

16 """ 

17 prefix = f"blake2b: {checksum}" 

18 

19 ext = extension.lower() 

20 

21 if ext == ".graphml" or ext == ".html" or ext == ".xml": 

22 comment = f"<!-- {prefix} -->" 

23 elif ext in (".yaml", ".yml", ".toml", ".txt", ".ascii", ".csv"): 

24 comment = f"# {prefix}" 

25 elif ext in (".dot", ".gv", ".d2"): 

26 comment = f"// {prefix}" 

27 elif ext == ".mmd" or ext == ".mermaid": 

28 comment = f"%% {prefix}" 

29 elif ext == ".puml": 

30 comment = f"' {prefix}" 

31 elif ext == ".tex": 

32 comment = f"% {prefix}" 

33 elif ext == ".json": 

34 import json 

35 

36 try: 

37 # Parse the rendered JSON string back to a dict 

38 data = json.loads(content) 

39 # Wrap it in a higher-order dict 

40 wrapped_data = {"checksum": prefix, "graph": data} 

41 # Return as a formatted JSON string 

42 return json.dumps(wrapped_data, indent=2) 

43 except Exception: 

44 # Fallback if content isn't valid JSON for some reason 

45 return f"# {prefix}\n{content}" 

46 else: 

47 comment = f"# {prefix}" 

48 

49 return f"{comment}\n{content}" 

50 

51 

52def detect_engine() -> str: 

53 """ 

54 Detect the available visualization engine based on system PATH. 

55 Priority: Mermaid -> Graphviz -> D2 -> PlantUML. 

56 

57 Returns: 

58 str: The name of the detected engine. 

59 

60 Raises: 

61 RuntimeError: If no engine is found. 

62 """ 

63 engines = { 

64 "mermaid": "mmdc", 

65 "graphviz": "dot", 

66 "d2": "d2", 

67 "plantuml": "plantuml", 

68 } 

69 

70 for engine, executable in engines.items(): 

71 if which(executable): 

72 return engine 

73 

74 raise RuntimeError( 

75 "No rendering engine found on PATH. " 

76 "Please install one of: mermaid-cli, graphviz, d2, or plantuml." 

77 ) 

78 

79 

80def get_image_exporter(engine: str | None = None) -> Callable[..., None]: 

81 """ 

82 Get the appropriate image exporter based on the engine name or auto-detection. 

83 

84 Args: 

85 engine: Optional engine name (e.g., 'mermaid', 'graphviz'). 

86 

87 Returns: 

88 Callable: An export function that takes (graph, output_path, **kwargs). 

89 """ 

90 from ..enums import Engine 

91 

92 if engine is None: 

93 engine_val = detect_engine() 

94 else: 

95 # Normalize engine name 

96 engine_val = engine.value if isinstance(engine, Engine) else engine.lower() 

97 

98 if engine_val == Engine.MERMAID: 

99 from .mermaid import export_topology_mermaid_image as exporter 

100 elif engine_val == Engine.GRAPHVIZ: 

101 from .graphviz import export_topology_graphviz_image as exporter 

102 elif engine_val == Engine.D2: 

103 from .d2 import export_topology_d2_image as exporter 

104 elif engine_val == Engine.PLANTUML: 

105 from .plantuml import export_topology_plantuml_image as exporter 

106 else: 

107 raise ValueError(f"Unknown rendering engine: {engine_val}") 

108 

109 return exporter