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.
- dataform_dependency_visualizer-0.1.0.dist-info/METADATA +170 -0
- dataform_dependency_visualizer-0.1.0.dist-info/RECORD +13 -0
- dataform_dependency_visualizer-0.1.0.dist-info/WHEEL +5 -0
- dataform_dependency_visualizer-0.1.0.dist-info/entry_points.txt +2 -0
- dataform_dependency_visualizer-0.1.0.dist-info/licenses/LICENSE +21 -0
- dataform_dependency_visualizer-0.1.0.dist-info/top_level.txt +1 -0
- dataform_viz/__init__.py +14 -0
- dataform_viz/cli.py +159 -0
- dataform_viz/dataform_check.py +175 -0
- dataform_viz/master_index.py +278 -0
- dataform_viz/parser.py +69 -0
- dataform_viz/svg_generator.py +381 -0
- dataform_viz/visualizer.py +140 -0
|
@@ -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">×</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
|