dataform-dependency-visualizer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Script to generate individual SVG diagrams for each table in a schema
4
+ Shows immediate dependencies and dependents for each table
5
+ """
6
+ import re
7
+ from pathlib import Path
8
+ import sys
9
+
10
+ def parse_dependencies_report(report_path):
11
+ """Parse the dependencies_report.txt file"""
12
+ tables = {}
13
+ current_table = None
14
+
15
+ # Try different encodings
16
+ encodings = ['utf-8', 'utf-16', 'cp1252', 'latin-1']
17
+ content = None
18
+
19
+ for encoding in encodings:
20
+ try:
21
+ with open(report_path, 'r', encoding=encoding) as f:
22
+ content = f.read()
23
+ break
24
+ except (UnicodeDecodeError, UnicodeError):
25
+ continue
26
+
27
+ if content is None:
28
+ raise ValueError("Could not decode file with any supported encoding")
29
+
30
+ for line in content.split('\n'):
31
+ line = line.rstrip()
32
+
33
+ # Match table definition line
34
+ table_match = re.match(r'^Table: (.+?) \((\w+)\)$', line)
35
+ if table_match:
36
+ table_name = table_match.group(1)
37
+ table_type = table_match.group(2)
38
+ current_table = table_name
39
+ tables[current_table] = {
40
+ 'type': table_type,
41
+ 'dependencies': [],
42
+ 'dependents': []
43
+ }
44
+ continue
45
+
46
+ # Match dependency line
47
+ if current_table and '<-' in line:
48
+ dep = line.strip().replace('<- ', '').strip()
49
+ if dep:
50
+ tables[current_table]['dependencies'].append(dep)
51
+
52
+ # Match dependent line
53
+ if current_table and '->' in line:
54
+ dep = line.strip().replace('-> ', '').strip()
55
+ if dep:
56
+ tables[current_table]['dependents'].append(dep)
57
+
58
+ return tables
59
+
60
+ def generate_table_svg(table_name, table_info, all_tables, output_dir):
61
+ """Generate SVG for a single table showing its immediate neighbors"""
62
+
63
+ # Create safe filename
64
+ safe_name = table_name.replace('.', '_').replace('-', '_')
65
+ svg_file = output_dir / f"{safe_name}.svg"
66
+
67
+ # Generate SVG directly
68
+ generate_svg_manual(table_name, table_info, all_tables, svg_file)
69
+ return True, svg_file
70
+
71
+
72
+ def generate_svg_manual(table_name, table_info, all_tables, svg_file):
73
+ """Generate SVG manually without Graphviz dependency"""
74
+
75
+ # Layout parameters
76
+ node_width = 200
77
+ node_height = 60
78
+ h_spacing = 250
79
+ v_spacing = 80
80
+ margin = 50
81
+
82
+ # Organize nodes into columns
83
+ dependencies = table_info['dependencies']
84
+ dependents = table_info['dependents']
85
+
86
+ max_left = len(dependencies)
87
+ max_right = len(dependents)
88
+ max_vertical = max(max_left, 1, max_right)
89
+
90
+ # Calculate canvas size
91
+ num_cols = 1 + (1 if dependencies else 0) + (1 if dependents else 0)
92
+ canvas_width = margin * 2 + node_width * num_cols + h_spacing * (num_cols - 1)
93
+ canvas_height = margin * 2 + node_height * max_vertical + v_spacing * (max_vertical - 1)
94
+
95
+ # Node positions
96
+ nodes = {}
97
+
98
+ # Center column - main table
99
+ center_x = margin + node_width // 2
100
+ if dependencies:
101
+ center_x += node_width + h_spacing
102
+ center_y = canvas_height // 2
103
+
104
+ nodes[table_name] = {
105
+ 'x': center_x,
106
+ 'y': center_y,
107
+ 'color': '#ffeb3b',
108
+ 'border': '#f57f17',
109
+ 'type': table_info['type']
110
+ }
111
+
112
+ # Left column - dependencies
113
+ if dependencies:
114
+ left_x = margin + node_width // 2
115
+ start_y = center_y - ((len(dependencies) - 1) * (node_height + v_spacing)) // 2
116
+ for i, dep in enumerate(dependencies):
117
+ dep_type = all_tables.get(dep, {}).get('type', 'unknown')
118
+ color = {
119
+ 'table': '#e1f5ff',
120
+ 'view': '#fff3e0',
121
+ 'operations': '#f3e5f5'
122
+ }.get(dep_type, '#f5f5f5')
123
+ nodes[dep] = {
124
+ 'x': left_x,
125
+ 'y': start_y + i * (node_height + v_spacing),
126
+ 'color': color,
127
+ 'border': '#666',
128
+ 'type': dep_type
129
+ }
130
+
131
+ # Right column - dependents
132
+ if dependents:
133
+ right_x = center_x + node_width // 2 + h_spacing + node_width // 2
134
+ start_y = center_y - ((len(dependents) - 1) * (node_height + v_spacing)) // 2
135
+ for i, dept in enumerate(dependents):
136
+ dept_type = all_tables.get(dept, {}).get('type', 'unknown')
137
+ color = {
138
+ 'table': '#e1f5ff',
139
+ 'view': '#fff3e0',
140
+ 'operations': '#f3e5f5'
141
+ }.get(dept_type, '#f5f5f5')
142
+ nodes[dept] = {
143
+ 'x': right_x,
144
+ 'y': start_y + i * (node_height + v_spacing),
145
+ 'color': color,
146
+ 'border': '#666',
147
+ 'type': dept_type
148
+ }
149
+
150
+ # Generate SVG
151
+ svg_lines = [
152
+ '<?xml version="1.0" encoding="UTF-8"?>',
153
+ f'<svg width="{canvas_width}" height="{canvas_height}" xmlns="http://www.w3.org/2000/svg">',
154
+ ' <defs>',
155
+ ' <marker id="arrowhead" markerWidth="6" markerHeight="6" refX="5" refY="3" orient="auto">',
156
+ ' <polygon points="0 0, 6 3, 0 6" fill="#000" />',
157
+ ' </marker>',
158
+ ' </defs>',
159
+ ' <style>',
160
+ ' .node-text { font-family: Arial, sans-serif; font-size: 12px; fill: #333; }',
161
+ ' .type-badge { font-family: Arial, sans-serif; font-size: 10px; fill: #666; }',
162
+ ' </style>',
163
+ ]
164
+
165
+ # Draw edges first (so they appear behind nodes) with orthogonal routing
166
+ for dep in dependencies:
167
+ if dep in nodes:
168
+ x1 = nodes[dep]['x'] + node_width // 2
169
+ y1 = nodes[dep]['y']
170
+ x2 = nodes[table_name]['x'] - node_width // 2
171
+ y2 = nodes[table_name]['y']
172
+ # Orthogonal path: horizontal then vertical
173
+ mid_x = (x1 + x2) // 2
174
+ svg_lines.append(f' <path d="M {x1} {y1} L {mid_x} {y1} L {mid_x} {y2} L {x2} {y2}" stroke="#000" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)" />')
175
+
176
+ for dept in dependents:
177
+ if dept in nodes:
178
+ x1 = nodes[table_name]['x'] + node_width // 2
179
+ y1 = nodes[table_name]['y']
180
+ x2 = nodes[dept]['x'] - node_width // 2
181
+ y2 = nodes[dept]['y']
182
+ # Orthogonal path: horizontal then vertical
183
+ mid_x = (x1 + x2) // 2
184
+ svg_lines.append(f' <path d="M {x1} {y1} L {mid_x} {y1} L {mid_x} {y2} L {x2} {y2}" stroke="#000" stroke-width="1.5" fill="none" marker-end="url(#arrowhead)" />')
185
+
186
+ # Draw nodes
187
+ for node_name, pos in nodes.items():
188
+ x = pos['x'] - node_width // 2
189
+ y = pos['y'] - node_height // 2
190
+
191
+ # Node rectangle
192
+ stroke_width = 3 if node_name == table_name else 1
193
+ svg_lines.append(f' <rect x="{x}" y="{y}" width="{node_width}" height="{node_height}" '
194
+ f'fill="{pos["color"]}" stroke="{pos["border"]}" stroke-width="{stroke_width}" rx="5" />')
195
+
196
+ # Schema name at top
197
+ schema_name = node_name.split('.')[0] if '.' in node_name else ''
198
+ if schema_name:
199
+ schema_y = pos['y'] - 20
200
+ svg_lines.append(f' <text x="{pos["x"]}" y="{schema_y}" text-anchor="middle" class="type-badge" fill="#999">{schema_name}</text>')
201
+
202
+ # Node text - wrap into 2 lines if needed
203
+ display_name = node_name.split('.')[-1] if '.' in node_name else node_name
204
+
205
+ # Split into 2 lines if longer than 20 characters
206
+ if len(display_name) > 20:
207
+ # Find a good break point (underscore, or middle)
208
+ mid = len(display_name) // 2
209
+ break_point = display_name.rfind('_', 0, mid + 5)
210
+ if break_point == -1 or break_point < mid - 5:
211
+ break_point = mid
212
+
213
+ line1 = display_name[:break_point]
214
+ line2 = display_name[break_point:].lstrip('_')
215
+
216
+ text_y1 = pos['y'] - 8 if schema_name else pos['y'] - 13
217
+ text_y2 = pos['y'] + 6 if schema_name else pos['y'] + 1
218
+
219
+ svg_lines.append(f' <text x="{pos["x"]}" y="{text_y1}" text-anchor="middle" class="node-text">{line1}</text>')
220
+ svg_lines.append(f' <text x="{pos["x"]}" y="{text_y2}" text-anchor="middle" class="node-text">{line2}</text>')
221
+ else:
222
+ text_y = pos['y'] if schema_name else pos['y'] - 5
223
+ svg_lines.append(f' <text x="{pos["x"]}" y="{text_y}" text-anchor="middle" class="node-text">{display_name}</text>')
224
+
225
+ # Type badge
226
+ badge_y = pos['y'] + 20 if schema_name else pos['y'] + 15
227
+ svg_lines.append(f' <text x="{pos["x"]}" y="{badge_y}" text-anchor="middle" class="type-badge">{pos["type"]}</text>')
228
+
229
+ svg_lines.append('</svg>')
230
+
231
+ with open(svg_file, 'w', encoding='utf-8') as f:
232
+ f.write('\n'.join(svg_lines))
233
+
234
+ def generate_index_html(tables, schema, output_dir):
235
+ """Generate an index.html to view all SVGs"""
236
+
237
+ html_lines = [
238
+ '<!DOCTYPE html>',
239
+ '<html>',
240
+ '<head>',
241
+ ' <meta charset="utf-8">',
242
+ f' <title>{schema} - Dependency Diagrams</title>',
243
+ ' <style>',
244
+ ' body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }',
245
+ ' h1 { color: #333; }',
246
+ ' .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }',
247
+ ' .card { background: white; border: 1px solid #ddd; border-radius: 8px; padding: 15px; cursor: pointer; transition: transform 0.2s; }',
248
+ ' .card:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.1); }',
249
+ ' .card h3 { margin: 0 0 10px 0; color: #1976d2; }',
250
+ ' .badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 5px; }',
251
+ ' .badge.table { background: #e1f5ff; color: #01579b; }',
252
+ ' .badge.view { background: #fff3e0; color: #e65100; }',
253
+ ' .badge.operations { background: #f3e5f5; color: #4a148c; }',
254
+ ' .stats { color: #666; font-size: 14px; margin-top: 10px; }',
255
+ ' .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; }',
256
+ ' .modal-content { position: relative; margin: 2% auto; width: 90%; max-width: 1200px; height: 90%; background: white; border-radius: 8px; overflow: auto; }',
257
+ ' .close { position: absolute; top: 10px; right: 20px; font-size: 30px; cursor: pointer; color: #666; z-index: 1001; }',
258
+ ' .modal img { width: 100%; height: auto; }',
259
+ ' </style>',
260
+ '</head>',
261
+ '<body>',
262
+ f' <h1>{schema} - Table Dependencies</h1>',
263
+ f' <p>Total tables: {len(tables)}</p>',
264
+ ' <div class="grid">',
265
+ ]
266
+
267
+ for table_name, info in sorted(tables.items()):
268
+ safe_name = table_name.replace('.', '_').replace('-', '_')
269
+ short_name = table_name.split('.')[-1] if '.' in table_name else table_name
270
+
271
+ html_lines.extend([
272
+ ' <div class="card" onclick="showDiagram(\'' + safe_name + '.svg\', \'' + table_name + '\')">',
273
+ f' <h3>{short_name}</h3>',
274
+ f' <span class="badge {info["type"]}">{info["type"]}</span>',
275
+ f' <div class="stats">',
276
+ f' ← {len(info["dependencies"])} dependencies<br>',
277
+ f' → {len(info["dependents"])} dependents',
278
+ f' </div>',
279
+ ' </div>',
280
+ ])
281
+
282
+ html_lines.extend([
283
+ ' </div>',
284
+ ' <div id="modal" class="modal" onclick="closeModal()">',
285
+ ' <span class="close">&times;</span>',
286
+ ' <div class="modal-content" onclick="event.stopPropagation()">',
287
+ ' <h2 id="modalTitle" style="padding: 20px;"></h2>',
288
+ ' <img id="modalImage" src="" alt="Dependency diagram">',
289
+ ' </div>',
290
+ ' </div>',
291
+ ' <script>',
292
+ ' function showDiagram(file, title) {',
293
+ ' document.getElementById("modalImage").src = file;',
294
+ ' document.getElementById("modalTitle").textContent = title;',
295
+ ' document.getElementById("modal").style.display = "block";',
296
+ ' }',
297
+ ' function closeModal() {',
298
+ ' document.getElementById("modal").style.display = "none";',
299
+ ' }',
300
+ ' document.addEventListener("keydown", function(e) {',
301
+ ' if (e.key === "Escape") closeModal();',
302
+ ' });',
303
+ ' </script>',
304
+ '</body>',
305
+ '</html>',
306
+ ])
307
+
308
+ index_file = output_dir / 'index.html'
309
+ with open(index_file, 'w', encoding='utf-8') as f:
310
+ f.write('\n'.join(html_lines))
311
+
312
+ return index_file
313
+
314
+ def main():
315
+ report_path = Path('output/dependencies_report.txt')
316
+ if not report_path.exists():
317
+ report_path = Path('dependencies_report.txt')
318
+
319
+ if not report_path.exists():
320
+ print("Error: dependencies_report.txt not found")
321
+ print("Run: python utility_check_dependencies.py > dependencies_report.txt")
322
+ return
323
+
324
+ # Get schema from command line
325
+ if len(sys.argv) < 2:
326
+ print("Usage: python split_dependencies_svg.py <schema_name>")
327
+ print("Example: python split_dependencies_svg.py dashboard_wwim")
328
+ return
329
+
330
+ schema = sys.argv[1]
331
+
332
+ print(f"Parsing dependencies report...")
333
+ all_tables = parse_dependencies_report(report_path)
334
+
335
+ # Filter to schema
336
+ schema_tables = {k: v for k, v in all_tables.items() if k.startswith(schema + '.')}
337
+
338
+ if not schema_tables:
339
+ print(f"No tables found for schema: {schema}")
340
+ print(f"Available schemas:")
341
+ schemas = set(k.split('.')[0] for k in all_tables.keys() if '.' in k)
342
+ for s in sorted(schemas):
343
+ count = sum(1 for k in all_tables.keys() if k.startswith(s + '.'))
344
+ print(f" - {s} ({count} tables)")
345
+ return
346
+
347
+ print(f"Found {len(schema_tables)} tables in schema '{schema}'")
348
+
349
+ # Create output directory
350
+ base_output = Path('output')
351
+ base_output.mkdir(exist_ok=True)
352
+ output_dir = base_output / f'dependencies_{schema}'
353
+ output_dir.mkdir(exist_ok=True)
354
+ print(f"Output directory: {output_dir}")
355
+
356
+ # Generate SVG for each table
357
+ success_count = 0
358
+
359
+ for table_name, table_info in schema_tables.items():
360
+ print(f"Generating: {table_name}...", end=' ')
361
+ success, result = generate_table_svg(table_name, table_info, all_tables, output_dir)
362
+
363
+ if success:
364
+ print("OK")
365
+ success_count += 1
366
+ else:
367
+ print(f"FAILED: {result}")
368
+
369
+ print(f"\nGenerated {success_count}/{len(schema_tables)} SVG files")
370
+
371
+ # Generate index.html
372
+ index_file = generate_index_html(schema_tables, schema, output_dir)
373
+ print(f"Index file created: {index_file}")
374
+ print(f"\nOpening in browser...")
375
+
376
+ # Open index.html
377
+ import subprocess
378
+ subprocess.run(['start', str(index_file)], shell=True)
379
+
380
+ if __name__ == "__main__":
381
+ main()
@@ -0,0 +1,140 @@
1
+ """
2
+ Main visualizer class
3
+ """
4
+ from pathlib import Path
5
+ from typing import Optional, List
6
+ from .parser import parse_dependencies_report
7
+ from .svg_generator import generate_table_svg, generate_svg_manual
8
+ from .master_index import collect_all_svgs, generate_master_index
9
+
10
+
11
+ class DependencyVisualizer:
12
+ """Main class for generating dependency visualizations"""
13
+
14
+ def __init__(self, report_path: str = "dependencies_report.txt"):
15
+ """
16
+ Initialize visualizer
17
+
18
+ Args:
19
+ report_path: Path to dependencies report file
20
+ """
21
+ self.report_path = Path(report_path)
22
+ self.tables = None
23
+
24
+ def load_report(self):
25
+ """Load and parse the dependencies report"""
26
+ if self.tables is None:
27
+ self.tables = parse_dependencies_report(str(self.report_path))
28
+ return self.tables
29
+
30
+ def generate_schema_svgs(
31
+ self,
32
+ schema: str,
33
+ output_dir: str = "output",
34
+ exclude_patterns: Optional[List[str]] = None
35
+ ) -> int:
36
+ """
37
+ Generate SVG diagrams for all tables in a schema
38
+
39
+ Args:
40
+ schema: Schema name to generate
41
+ output_dir: Base output directory
42
+ exclude_patterns: List of schema patterns to exclude (e.g., ['refined_*'])
43
+
44
+ Returns:
45
+ Number of SVGs generated
46
+ """
47
+ self.load_report()
48
+
49
+ # Filter to schema
50
+ schema_tables = {
51
+ k: v for k, v in self.tables.items()
52
+ if k.startswith(schema + '.')
53
+ }
54
+
55
+ if not schema_tables:
56
+ raise ValueError(f"No tables found for schema: {schema}")
57
+
58
+ # Create output directory
59
+ base_output = Path(output_dir)
60
+ base_output.mkdir(exist_ok=True)
61
+ schema_output = base_output / f'dependencies_{schema}'
62
+ schema_output.mkdir(exist_ok=True)
63
+
64
+ # Generate SVGs
65
+ count = 0
66
+ for table_name, table_info in schema_tables.items():
67
+ safe_name = table_name.replace('.', '_').replace('-', '_')
68
+ svg_file = schema_output / f"{safe_name}.svg"
69
+
70
+ generate_svg_manual(table_name, table_info, self.tables, svg_file)
71
+ count += 1
72
+
73
+ return count
74
+
75
+ def generate_all_schemas(
76
+ self,
77
+ output_dir: str = "output",
78
+ exclude_patterns: Optional[List[str]] = None
79
+ ) -> dict:
80
+ """
81
+ Generate SVG diagrams for all schemas
82
+
83
+ Args:
84
+ output_dir: Base output directory
85
+ exclude_patterns: List of schema patterns to exclude (default: ['refined_*'])
86
+
87
+ Returns:
88
+ Dictionary mapping schema names to number of tables generated
89
+ """
90
+ if exclude_patterns is None:
91
+ exclude_patterns = ['refined_*']
92
+
93
+ self.load_report()
94
+
95
+ # Get all unique schemas
96
+ schemas = set()
97
+ for table_name in self.tables.keys():
98
+ if '.' in table_name:
99
+ schema = table_name.split('.')[0]
100
+ # Check exclusion patterns
101
+ excluded = False
102
+ for pattern in exclude_patterns:
103
+ if pattern.endswith('*'):
104
+ if schema.startswith(pattern[:-1]):
105
+ excluded = True
106
+ break
107
+ if not excluded:
108
+ schemas.add(schema)
109
+
110
+ # Generate for each schema
111
+ results = {}
112
+ for schema in sorted(schemas):
113
+ count = self.generate_schema_svgs(schema, output_dir)
114
+ results[schema] = count
115
+
116
+ return results
117
+
118
+ def generate_master_index(self, output_dir: str = "output") -> Path:
119
+ """
120
+ Generate master index.html to view all diagrams
121
+
122
+ Args:
123
+ output_dir: Output directory containing schema folders
124
+
125
+ Returns:
126
+ Path to generated index file
127
+ """
128
+ output_path = Path(output_dir)
129
+ schemas = collect_all_svgs()
130
+
131
+ if not schemas:
132
+ raise ValueError("No dependency folders found. Generate SVGs first.")
133
+
134
+ html_content = generate_master_index(schemas)
135
+
136
+ output_file = output_path / 'dependencies_master_index.html'
137
+ with open(output_file, 'w', encoding='utf-8') as f:
138
+ f.write(html_content)
139
+
140
+ return output_file