sql-glider 0.1.23__py3-none-any.whl → 0.1.24__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.
- {sql_glider-0.1.23.dist-info → sql_glider-0.1.24.dist-info}/METADATA +52 -2
- {sql_glider-0.1.23.dist-info → sql_glider-0.1.24.dist-info}/RECORD +8 -8
- sqlglider/_version.py +2 -2
- sqlglider/cli.py +37 -13
- sqlglider/graph/diagram_formatters.py +330 -2
- {sql_glider-0.1.23.dist-info → sql_glider-0.1.24.dist-info}/WHEEL +0 -0
- {sql_glider-0.1.23.dist-info → sql_glider-0.1.24.dist-info}/entry_points.txt +0 -0
- {sql_glider-0.1.23.dist-info → sql_glider-0.1.24.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-glider
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.24
|
|
4
4
|
Summary: SQL Utility Toolkit for better understanding, use, and governance of your queries in a native environment.
|
|
5
5
|
Project-URL: Homepage, https://github.com/rycowhi/sql-glider/
|
|
6
6
|
Project-URL: Repository, https://github.com/rycowhi/sql-glider/
|
|
@@ -28,6 +28,8 @@ Requires-Dist: sqlglot[rs]>=25.0.0
|
|
|
28
28
|
Requires-Dist: typer>=0.9.0
|
|
29
29
|
Provides-Extra: databricks
|
|
30
30
|
Requires-Dist: databricks-sdk>=0.20.0; extra == 'databricks'
|
|
31
|
+
Provides-Extra: plotly
|
|
32
|
+
Requires-Dist: plotly>=5.0.0; extra == 'plotly'
|
|
31
33
|
Description-Content-Type: text/markdown
|
|
32
34
|
|
|
33
35
|
# SQL Glider
|
|
@@ -420,12 +422,33 @@ uv run sqlglider graph query graph.json --upstream orders.customer_id -f mermaid
|
|
|
420
422
|
# Query with DOT (Graphviz) diagram output
|
|
421
423
|
uv run sqlglider graph query graph.json --downstream customers.id -f dot
|
|
422
424
|
|
|
425
|
+
# Query with Plotly JSON output (for interactive visualization)
|
|
426
|
+
uv run sqlglider graph query graph.json --upstream orders.customer_id -f plotly
|
|
427
|
+
|
|
423
428
|
# Visualize entire graph as a diagram
|
|
424
429
|
uv run sqlglider graph visualize graph.json # Mermaid (default)
|
|
425
430
|
uv run sqlglider graph visualize graph.json -f dot # DOT/Graphviz
|
|
431
|
+
uv run sqlglider graph visualize graph.json -f plotly # Plotly JSON
|
|
426
432
|
uv run sqlglider graph visualize graph.json -o lineage.mmd # Save to file
|
|
427
433
|
```
|
|
428
434
|
|
|
435
|
+
> **Note:** Plotly output requires an optional dependency. Install with: `pip install sql-glider[plotly]`
|
|
436
|
+
|
|
437
|
+
The Plotly JSON output can be loaded into Plotly/Dash applications for interactive visualization:
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
import plotly.io as pio
|
|
441
|
+
from dash import Dash, dcc, html
|
|
442
|
+
|
|
443
|
+
# Load the JSON output
|
|
444
|
+
with open("lineage.json") as f:
|
|
445
|
+
fig = pio.from_json(f.read())
|
|
446
|
+
|
|
447
|
+
# Use in a Dash app
|
|
448
|
+
app = Dash(__name__)
|
|
449
|
+
app.layout = html.Div([dcc.Graph(figure=fig)])
|
|
450
|
+
```
|
|
451
|
+
|
|
429
452
|
**Example Upstream Query Output:**
|
|
430
453
|
```
|
|
431
454
|
Sources for 'order_totals.total'
|
|
@@ -711,12 +734,24 @@ Arguments:
|
|
|
711
734
|
Options:
|
|
712
735
|
--upstream, -u Find source columns for this column [optional]
|
|
713
736
|
--downstream, -d Find affected columns for this source [optional]
|
|
714
|
-
--output-format, -f Output format: 'text', 'json', or '
|
|
737
|
+
--output-format, -f Output format: 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', 'dot', or 'plotly' [default: text]
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
sqlglider graph visualize <graph_file> [OPTIONS]
|
|
742
|
+
|
|
743
|
+
Arguments:
|
|
744
|
+
graph_file Path to graph JSON file [required]
|
|
745
|
+
|
|
746
|
+
Options:
|
|
747
|
+
--output-format, -f Diagram format: 'mermaid', 'mermaid-markdown', 'dot', or 'plotly' [default: mermaid]
|
|
748
|
+
--output-file, -o Write diagram to file instead of stdout [optional]
|
|
715
749
|
```
|
|
716
750
|
|
|
717
751
|
**Notes:**
|
|
718
752
|
- `--upstream` and `--downstream` are mutually exclusive. Use one or the other.
|
|
719
753
|
- Graph queries are case-insensitive for column matching.
|
|
754
|
+
- Plotly output requires optional dependency: `pip install sql-glider[plotly]`
|
|
720
755
|
|
|
721
756
|
## Output Formats
|
|
722
757
|
|
|
@@ -921,6 +956,21 @@ UV_PUBLISH_TOKEN=pypi-...
|
|
|
921
956
|
- **pydantic:** Data validation and serialization
|
|
922
957
|
- **rustworkx:** High-performance graph library for cross-file lineage analysis
|
|
923
958
|
|
|
959
|
+
### Optional Dependencies
|
|
960
|
+
|
|
961
|
+
Install optional features with extras:
|
|
962
|
+
|
|
963
|
+
```bash
|
|
964
|
+
# Databricks catalog integration
|
|
965
|
+
pip install sql-glider[databricks]
|
|
966
|
+
|
|
967
|
+
# Plotly interactive visualization
|
|
968
|
+
pip install sql-glider[plotly]
|
|
969
|
+
|
|
970
|
+
# Install multiple extras
|
|
971
|
+
pip install sql-glider[databricks,plotly]
|
|
972
|
+
```
|
|
973
|
+
|
|
924
974
|
## References
|
|
925
975
|
|
|
926
976
|
- [SQLGlot Documentation](https://sqlglot.com/)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
sqlglider/__init__.py,sha256=gDf7s52dMcX7JuCZ1SLawcB1vb3U0yJCohu9RQAATBY,125
|
|
2
|
-
sqlglider/_version.py,sha256=
|
|
3
|
-
sqlglider/cli.py,sha256=
|
|
2
|
+
sqlglider/_version.py,sha256=IV4a2R7tlzuACf6FAyPEbprLKNroeE-n_UPSKi1QJSc,706
|
|
3
|
+
sqlglider/cli.py,sha256=oML-M3PW8Cjddii4_Tx_tfYAxYh6Q-inU4eehx0gzC0,69570
|
|
4
4
|
sqlglider/global_models.py,sha256=2vyJXAuXOsXQpE-D3F0ejj7eR9z0nDWFjTkielhzM8k,356
|
|
5
5
|
sqlglider/catalog/__init__.py,sha256=2PqFPyzFXJ14FpSUcBmVK2L-a_ypWQHAbHFHxLDk_LE,814
|
|
6
6
|
sqlglider/catalog/base.py,sha256=R7htHC43InpH4uRjYk33dMYYji6oylHns7Ye_mgfjJE,3116
|
|
@@ -12,7 +12,7 @@ sqlglider/dissection/formatters.py,sha256=M7gsmTNljRIeLIRv4D0vHvqJVrTqWSpsg7vem8
|
|
|
12
12
|
sqlglider/dissection/models.py,sha256=RRD3RIteqbUBY6e-74skKDvMH3qeAUaqA2sFcrjP5GQ,3618
|
|
13
13
|
sqlglider/graph/__init__.py,sha256=4DDdrPM75CmeQWt7wHdBsjCm1s70BHGLYdijIbaUEKY,871
|
|
14
14
|
sqlglider/graph/builder.py,sha256=VNBdsDlkiaId3JGvr2G4h6OIFek_9zPsGMIYL9GpJlk,15796
|
|
15
|
-
sqlglider/graph/diagram_formatters.py,sha256=
|
|
15
|
+
sqlglider/graph/diagram_formatters.py,sha256=6EdNwU23k4CL3hSBtM1lgZ5Gyc95o7vUMRdQa-a1Fko,22021
|
|
16
16
|
sqlglider/graph/formatters.py,sha256=p85-WN9oPmEETsAtWSo1sIQELF36w85QoFEJyfBZGoM,4800
|
|
17
17
|
sqlglider/graph/merge.py,sha256=uUZlm4BN3S9gRL66Cc2mzhbtuh4SVAv2n4cN4eUEQBU,4077
|
|
18
18
|
sqlglider/graph/models.py,sha256=EYmjv_WzDSNp_WfhJ6H-qBIOkAcoNKS7GRUryfKrHuY,9330
|
|
@@ -32,8 +32,8 @@ sqlglider/utils/__init__.py,sha256=KGp9-UzKz_OFBOTFoSy-g-NXDZsvyWXG_9-1zcC6ePE,2
|
|
|
32
32
|
sqlglider/utils/config.py,sha256=qx5zE9pjLCCzHQDFVPLVd7LgJ-lghxUa2x-aZOAHByY,4962
|
|
33
33
|
sqlglider/utils/file_utils.py,sha256=5_ff28E0r1R7emZzsOnRuHd-7zIX6873eyr1SuPEr4E,1093
|
|
34
34
|
sqlglider/utils/schema.py,sha256=LiWrYDunXKJdoSlpKmIaIQ2hLSaIN1iQHqkXjMpGzRE,1883
|
|
35
|
-
sql_glider-0.1.
|
|
36
|
-
sql_glider-0.1.
|
|
37
|
-
sql_glider-0.1.
|
|
38
|
-
sql_glider-0.1.
|
|
39
|
-
sql_glider-0.1.
|
|
35
|
+
sql_glider-0.1.24.dist-info/METADATA,sha256=5giBXU7edyyoMBfbFVficYatA60-To2D_P3eMc6PdQQ,31197
|
|
36
|
+
sql_glider-0.1.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
sql_glider-0.1.24.dist-info/entry_points.txt,sha256=HDuakHqHS5C0HFKsMIxMYmDU7-BLBGrnIJcYaVRu-s0,251
|
|
38
|
+
sql_glider-0.1.24.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
39
|
+
sql_glider-0.1.24.dist-info/RECORD,,
|
sqlglider/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.24'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 24)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
sqlglider/cli.py
CHANGED
|
@@ -1642,7 +1642,7 @@ def graph_query(
|
|
|
1642
1642
|
"text",
|
|
1643
1643
|
"--output-format",
|
|
1644
1644
|
"-f",
|
|
1645
|
-
help="Output format: 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', or '
|
|
1645
|
+
help="Output format: 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', 'dot', or 'plotly'",
|
|
1646
1646
|
),
|
|
1647
1647
|
) -> None:
|
|
1648
1648
|
"""
|
|
@@ -1661,6 +1661,9 @@ def graph_query(
|
|
|
1661
1661
|
|
|
1662
1662
|
# CSV output
|
|
1663
1663
|
sqlglider graph query graph.json --downstream orders.order_id -f csv
|
|
1664
|
+
|
|
1665
|
+
# Plotly JSON output (for Dash/Plotly visualization)
|
|
1666
|
+
sqlglider graph query graph.json --upstream orders.total -f plotly
|
|
1664
1667
|
"""
|
|
1665
1668
|
from sqlglider.graph.query import GraphQuerier
|
|
1666
1669
|
|
|
@@ -1685,10 +1688,11 @@ def graph_query(
|
|
|
1685
1688
|
"mermaid",
|
|
1686
1689
|
"mermaid-markdown",
|
|
1687
1690
|
"dot",
|
|
1691
|
+
"plotly",
|
|
1688
1692
|
]:
|
|
1689
1693
|
err_console.print(
|
|
1690
1694
|
f"[red]Error:[/red] Invalid output format '{output_format}'. "
|
|
1691
|
-
"Use 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', or '
|
|
1695
|
+
"Use 'text', 'json', 'csv', 'mermaid', 'mermaid-markdown', 'dot', or 'plotly'."
|
|
1692
1696
|
)
|
|
1693
1697
|
raise typer.Exit(1)
|
|
1694
1698
|
|
|
@@ -1720,6 +1724,14 @@ def graph_query(
|
|
|
1720
1724
|
from sqlglider.graph.diagram_formatters import DotFormatter
|
|
1721
1725
|
|
|
1722
1726
|
print(DotFormatter.format_query_result(result))
|
|
1727
|
+
elif output_format == "plotly":
|
|
1728
|
+
from sqlglider.graph.diagram_formatters import PlotlyFormatter
|
|
1729
|
+
|
|
1730
|
+
print(PlotlyFormatter.format_query_result(result))
|
|
1731
|
+
|
|
1732
|
+
except ImportError as e:
|
|
1733
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
1734
|
+
raise typer.Exit(1)
|
|
1723
1735
|
|
|
1724
1736
|
except FileNotFoundError as e:
|
|
1725
1737
|
err_console.print(f"[red]Error:[/red] {e}")
|
|
@@ -1825,7 +1837,7 @@ def graph_visualize(
|
|
|
1825
1837
|
"mermaid",
|
|
1826
1838
|
"--output-format",
|
|
1827
1839
|
"-f",
|
|
1828
|
-
help="Diagram format: 'mermaid', 'mermaid-markdown', or '
|
|
1840
|
+
help="Diagram format: 'mermaid', 'mermaid-markdown', 'dot', or 'plotly'",
|
|
1829
1841
|
),
|
|
1830
1842
|
output_file: Optional[Path] = typer.Option(
|
|
1831
1843
|
None,
|
|
@@ -1837,8 +1849,8 @@ def graph_visualize(
|
|
|
1837
1849
|
"""
|
|
1838
1850
|
Visualize the entire lineage graph as a diagram.
|
|
1839
1851
|
|
|
1840
|
-
Generates Mermaid
|
|
1841
|
-
in the graph for visualization tools.
|
|
1852
|
+
Generates Mermaid, DOT (Graphviz), or Plotly JSON diagrams showing all
|
|
1853
|
+
nodes and edges in the graph for visualization tools.
|
|
1842
1854
|
|
|
1843
1855
|
Examples:
|
|
1844
1856
|
|
|
@@ -1853,18 +1865,16 @@ def graph_visualize(
|
|
|
1853
1865
|
|
|
1854
1866
|
# Render DOT to PNG with Graphviz
|
|
1855
1867
|
sqlglider graph visualize graph.json -f dot -o lineage.dot
|
|
1868
|
+
|
|
1869
|
+
# Generate Plotly JSON for Dash/Plotly apps
|
|
1870
|
+
sqlglider graph visualize graph.json -f plotly -o lineage.json
|
|
1856
1871
|
"""
|
|
1857
|
-
from sqlglider.graph.diagram_formatters import (
|
|
1858
|
-
DotFormatter,
|
|
1859
|
-
MermaidFormatter,
|
|
1860
|
-
MermaidMarkdownFormatter,
|
|
1861
|
-
)
|
|
1862
1872
|
from sqlglider.graph.serialization import load_graph
|
|
1863
1873
|
|
|
1864
|
-
if output_format not in ["mermaid", "mermaid-markdown", "dot"]:
|
|
1874
|
+
if output_format not in ["mermaid", "mermaid-markdown", "dot", "plotly"]:
|
|
1865
1875
|
err_console.print(
|
|
1866
1876
|
f"[red]Error:[/red] Invalid output format '{output_format}'. "
|
|
1867
|
-
"Use 'mermaid', 'mermaid-markdown', or '
|
|
1877
|
+
"Use 'mermaid', 'mermaid-markdown', 'dot', or 'plotly'."
|
|
1868
1878
|
)
|
|
1869
1879
|
raise typer.Exit(1)
|
|
1870
1880
|
|
|
@@ -1872,11 +1882,21 @@ def graph_visualize(
|
|
|
1872
1882
|
graph = load_graph(graph_file)
|
|
1873
1883
|
|
|
1874
1884
|
if output_format == "mermaid":
|
|
1885
|
+
from sqlglider.graph.diagram_formatters import MermaidFormatter
|
|
1886
|
+
|
|
1875
1887
|
diagram = MermaidFormatter.format_full_graph(graph)
|
|
1876
1888
|
elif output_format == "mermaid-markdown":
|
|
1889
|
+
from sqlglider.graph.diagram_formatters import MermaidMarkdownFormatter
|
|
1890
|
+
|
|
1877
1891
|
diagram = MermaidMarkdownFormatter.format_full_graph(graph)
|
|
1878
|
-
|
|
1892
|
+
elif output_format == "dot":
|
|
1893
|
+
from sqlglider.graph.diagram_formatters import DotFormatter
|
|
1894
|
+
|
|
1879
1895
|
diagram = DotFormatter.format_full_graph(graph)
|
|
1896
|
+
else: # plotly
|
|
1897
|
+
from sqlglider.graph.diagram_formatters import PlotlyFormatter
|
|
1898
|
+
|
|
1899
|
+
diagram = PlotlyFormatter.format_full_graph(graph)
|
|
1880
1900
|
|
|
1881
1901
|
if output_file:
|
|
1882
1902
|
output_file.write_text(diagram, encoding="utf-8")
|
|
@@ -1884,6 +1904,10 @@ def graph_visualize(
|
|
|
1884
1904
|
else:
|
|
1885
1905
|
print(diagram)
|
|
1886
1906
|
|
|
1907
|
+
except ImportError as e:
|
|
1908
|
+
err_console.print(f"[red]Error:[/red] {e}")
|
|
1909
|
+
raise typer.Exit(1)
|
|
1910
|
+
|
|
1887
1911
|
except FileNotFoundError as e:
|
|
1888
1912
|
err_console.print(f"[red]Error:[/red] {e}")
|
|
1889
1913
|
raise typer.Exit(1)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
"""Diagram formatters for lineage graphs (Mermaid
|
|
1
|
+
"""Diagram formatters for lineage graphs (Mermaid, DOT/Graphviz, and Plotly)."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import re
|
|
4
|
-
from
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Any, Set
|
|
5
7
|
|
|
6
8
|
from sqlglider.graph.models import LineageGraph
|
|
7
9
|
from sqlglider.graph.query import LineageQueryResult
|
|
@@ -328,3 +330,329 @@ class DotFormatter:
|
|
|
328
330
|
|
|
329
331
|
lines.append("}")
|
|
330
332
|
return "\n".join(lines)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _compute_layered_layout(
|
|
336
|
+
nodes: list[str],
|
|
337
|
+
edges: list[tuple[str, str]],
|
|
338
|
+
) -> dict[str, tuple[float, float]]:
|
|
339
|
+
"""Compute layered layout positions for nodes using topological ordering.
|
|
340
|
+
|
|
341
|
+
Positions nodes in layers from left to right based on their dependencies.
|
|
342
|
+
Nodes with no incoming edges are placed in layer 0, their dependents in
|
|
343
|
+
layer 1, etc.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
nodes: List of node identifiers
|
|
347
|
+
edges: List of (source, target) edge tuples
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Dictionary mapping node identifiers to (x, y) positions
|
|
351
|
+
"""
|
|
352
|
+
if not nodes:
|
|
353
|
+
return {}
|
|
354
|
+
|
|
355
|
+
# Build adjacency structures
|
|
356
|
+
incoming: dict[str, set[str]] = defaultdict(set)
|
|
357
|
+
outgoing: dict[str, set[str]] = defaultdict(set)
|
|
358
|
+
for src, tgt in edges:
|
|
359
|
+
outgoing[src].add(tgt)
|
|
360
|
+
incoming[tgt].add(src)
|
|
361
|
+
|
|
362
|
+
# Assign layers via modified Kahn's algorithm
|
|
363
|
+
layers: dict[str, int] = {}
|
|
364
|
+
node_set = set(nodes)
|
|
365
|
+
|
|
366
|
+
# Start with nodes that have no incoming edges (roots)
|
|
367
|
+
current_layer = [n for n in nodes if not incoming[n]]
|
|
368
|
+
if not current_layer:
|
|
369
|
+
# Cycle detected or all nodes have incoming edges, use first node
|
|
370
|
+
current_layer = [nodes[0]]
|
|
371
|
+
|
|
372
|
+
layer_num = 0
|
|
373
|
+
while current_layer:
|
|
374
|
+
next_layer = []
|
|
375
|
+
for node in current_layer:
|
|
376
|
+
if node not in layers:
|
|
377
|
+
layers[node] = layer_num
|
|
378
|
+
for child in outgoing[node]:
|
|
379
|
+
if child in node_set and child not in layers:
|
|
380
|
+
# Check if all parents are assigned
|
|
381
|
+
if all(p in layers for p in incoming[child]):
|
|
382
|
+
next_layer.append(child)
|
|
383
|
+
layer_num += 1
|
|
384
|
+
current_layer = next_layer
|
|
385
|
+
|
|
386
|
+
# Assign any remaining unvisited nodes to the last layer
|
|
387
|
+
for node in nodes:
|
|
388
|
+
if node not in layers:
|
|
389
|
+
layers[node] = layer_num
|
|
390
|
+
|
|
391
|
+
# Group nodes by layer for vertical positioning
|
|
392
|
+
layer_groups: dict[int, list[str]] = defaultdict(list)
|
|
393
|
+
for node, layer in layers.items():
|
|
394
|
+
layer_groups[layer].append(node)
|
|
395
|
+
|
|
396
|
+
# Compute positions: x based on layer, y spread vertically within layer
|
|
397
|
+
positions: dict[str, tuple[float, float]] = {}
|
|
398
|
+
max_layer = max(layers.values()) if layers else 0
|
|
399
|
+
x_spacing = 1.0 if max_layer == 0 else 1.0
|
|
400
|
+
|
|
401
|
+
for layer, layer_nodes in layer_groups.items():
|
|
402
|
+
x = layer * x_spacing
|
|
403
|
+
n = len(layer_nodes)
|
|
404
|
+
for i, node in enumerate(sorted(layer_nodes)):
|
|
405
|
+
# Center nodes vertically, spread them out
|
|
406
|
+
y = (i - (n - 1) / 2) * 0.5
|
|
407
|
+
positions[node] = (x, y)
|
|
408
|
+
|
|
409
|
+
return positions
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
class PlotlyFormatter:
|
|
413
|
+
"""Format lineage graphs as Plotly JSON figure specifications.
|
|
414
|
+
|
|
415
|
+
Generates JSON that can be loaded into Plotly/Dash applications using
|
|
416
|
+
plotly.io.from_json() or directly into dcc.Graph components.
|
|
417
|
+
|
|
418
|
+
Requires the 'plotly' optional dependency: pip install sql-glider[plotly]
|
|
419
|
+
"""
|
|
420
|
+
|
|
421
|
+
@staticmethod
|
|
422
|
+
def _check_plotly_available() -> None:
|
|
423
|
+
"""Check if plotly is installed, raise ImportError if not."""
|
|
424
|
+
try:
|
|
425
|
+
import plotly # noqa: F401
|
|
426
|
+
except ImportError:
|
|
427
|
+
raise ImportError(
|
|
428
|
+
"Plotly is required for this output format. "
|
|
429
|
+
"Install it with: pip install sql-glider[plotly]"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def format_full_graph(graph: LineageGraph) -> str:
|
|
434
|
+
"""Format complete lineage graph as a Plotly JSON figure.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
graph: LineageGraph with all nodes and edges
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
JSON string representing a Plotly figure specification
|
|
441
|
+
"""
|
|
442
|
+
PlotlyFormatter._check_plotly_available()
|
|
443
|
+
|
|
444
|
+
node_ids = [n.identifier for n in graph.nodes]
|
|
445
|
+
edge_tuples = [(e.source_node, e.target_node) for e in graph.edges]
|
|
446
|
+
|
|
447
|
+
if not node_ids:
|
|
448
|
+
# Empty graph
|
|
449
|
+
figure: dict[str, Any] = {
|
|
450
|
+
"data": [],
|
|
451
|
+
"layout": {
|
|
452
|
+
"title": {"text": "Lineage Graph"},
|
|
453
|
+
"showlegend": False,
|
|
454
|
+
"xaxis": {"visible": False},
|
|
455
|
+
"yaxis": {"visible": False},
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
return json.dumps(figure, indent=2)
|
|
459
|
+
|
|
460
|
+
positions = _compute_layered_layout(node_ids, edge_tuples)
|
|
461
|
+
|
|
462
|
+
# Build edge traces (one trace per edge for simplicity)
|
|
463
|
+
edge_traces: list[dict[str, Any]] = []
|
|
464
|
+
for src, tgt in edge_tuples:
|
|
465
|
+
if src in positions and tgt in positions:
|
|
466
|
+
x0, y0 = positions[src]
|
|
467
|
+
x1, y1 = positions[tgt]
|
|
468
|
+
edge_traces.append(
|
|
469
|
+
{
|
|
470
|
+
"type": "scatter",
|
|
471
|
+
"x": [x0, x1, None],
|
|
472
|
+
"y": [y0, y1, None],
|
|
473
|
+
"mode": "lines",
|
|
474
|
+
"line": {"width": 1, "color": "#888"},
|
|
475
|
+
"hoverinfo": "none",
|
|
476
|
+
"showlegend": False,
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Build node trace
|
|
481
|
+
node_x = [positions[n][0] for n in node_ids if n in positions]
|
|
482
|
+
node_y = [positions[n][1] for n in node_ids if n in positions]
|
|
483
|
+
node_text = [n for n in node_ids if n in positions]
|
|
484
|
+
|
|
485
|
+
node_trace: dict[str, Any] = {
|
|
486
|
+
"type": "scatter",
|
|
487
|
+
"x": node_x,
|
|
488
|
+
"y": node_y,
|
|
489
|
+
"mode": "markers+text",
|
|
490
|
+
"text": node_text,
|
|
491
|
+
"textposition": "top center",
|
|
492
|
+
"hoverinfo": "text",
|
|
493
|
+
"marker": {
|
|
494
|
+
"size": 20,
|
|
495
|
+
"color": "#6495ED",
|
|
496
|
+
"line": {"width": 2, "color": "#4169E1"},
|
|
497
|
+
},
|
|
498
|
+
"showlegend": False,
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
figure = {
|
|
502
|
+
"data": edge_traces + [node_trace],
|
|
503
|
+
"layout": {
|
|
504
|
+
"title": {"text": "Lineage Graph"},
|
|
505
|
+
"showlegend": False,
|
|
506
|
+
"hovermode": "closest",
|
|
507
|
+
"xaxis": {"visible": False},
|
|
508
|
+
"yaxis": {"visible": False},
|
|
509
|
+
"margin": {"l": 40, "r": 40, "t": 60, "b": 40},
|
|
510
|
+
},
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return json.dumps(figure, indent=2)
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
def format_query_result(result: LineageQueryResult) -> str:
|
|
517
|
+
"""Format query result as a Plotly JSON figure with styling.
|
|
518
|
+
|
|
519
|
+
The queried column is highlighted in amber, root nodes in teal,
|
|
520
|
+
and leaf nodes in violet, matching the Mermaid/DOT color scheme.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
result: LineageQueryResult from upstream/downstream query
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
JSON string representing a Plotly figure specification
|
|
527
|
+
"""
|
|
528
|
+
PlotlyFormatter._check_plotly_available()
|
|
529
|
+
|
|
530
|
+
all_nodes = _collect_query_nodes(result)
|
|
531
|
+
edges = _collect_query_edges(result)
|
|
532
|
+
|
|
533
|
+
if not all_nodes:
|
|
534
|
+
# Should not happen, but handle gracefully
|
|
535
|
+
figure: dict[str, Any] = {
|
|
536
|
+
"data": [],
|
|
537
|
+
"layout": {
|
|
538
|
+
"title": {"text": f"Lineage: {result.query_column}"},
|
|
539
|
+
"showlegend": False,
|
|
540
|
+
"xaxis": {"visible": False},
|
|
541
|
+
"yaxis": {"visible": False},
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
return json.dumps(figure, indent=2)
|
|
545
|
+
|
|
546
|
+
node_list = sorted(all_nodes)
|
|
547
|
+
edge_list = list(edges)
|
|
548
|
+
positions = _compute_layered_layout(node_list, edge_list)
|
|
549
|
+
|
|
550
|
+
# Build styling lookup
|
|
551
|
+
root_ids: set[str] = set()
|
|
552
|
+
leaf_ids: set[str] = set()
|
|
553
|
+
for node in result.related_columns:
|
|
554
|
+
if node.is_root:
|
|
555
|
+
root_ids.add(node.identifier)
|
|
556
|
+
if node.is_leaf:
|
|
557
|
+
leaf_ids.add(node.identifier)
|
|
558
|
+
|
|
559
|
+
# Determine node colors
|
|
560
|
+
node_colors: list[str] = []
|
|
561
|
+
for node in node_list:
|
|
562
|
+
if node == result.query_column:
|
|
563
|
+
node_colors.append(QUERIED_FILL)
|
|
564
|
+
elif node in root_ids:
|
|
565
|
+
node_colors.append(ROOT_FILL)
|
|
566
|
+
elif node in leaf_ids:
|
|
567
|
+
node_colors.append(LEAF_FILL)
|
|
568
|
+
else:
|
|
569
|
+
node_colors.append("#6495ED") # Default blue
|
|
570
|
+
|
|
571
|
+
# Build edge traces
|
|
572
|
+
edge_traces: list[dict[str, Any]] = []
|
|
573
|
+
for src, tgt in sorted(edges):
|
|
574
|
+
if src in positions and tgt in positions:
|
|
575
|
+
x0, y0 = positions[src]
|
|
576
|
+
x1, y1 = positions[tgt]
|
|
577
|
+
edge_traces.append(
|
|
578
|
+
{
|
|
579
|
+
"type": "scatter",
|
|
580
|
+
"x": [x0, x1, None],
|
|
581
|
+
"y": [y0, y1, None],
|
|
582
|
+
"mode": "lines",
|
|
583
|
+
"line": {"width": 1.5, "color": "#888"},
|
|
584
|
+
"hoverinfo": "none",
|
|
585
|
+
"showlegend": False,
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
# Build node trace
|
|
590
|
+
node_x = [positions[n][0] for n in node_list if n in positions]
|
|
591
|
+
node_y = [positions[n][1] for n in node_list if n in positions]
|
|
592
|
+
|
|
593
|
+
node_trace: dict[str, Any] = {
|
|
594
|
+
"type": "scatter",
|
|
595
|
+
"x": node_x,
|
|
596
|
+
"y": node_y,
|
|
597
|
+
"mode": "markers+text",
|
|
598
|
+
"text": node_list,
|
|
599
|
+
"textposition": "top center",
|
|
600
|
+
"hoverinfo": "text",
|
|
601
|
+
"marker": {
|
|
602
|
+
"size": 20,
|
|
603
|
+
"color": node_colors,
|
|
604
|
+
"line": {"width": 2, "color": "#333"},
|
|
605
|
+
},
|
|
606
|
+
"showlegend": False,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
# Build legend traces (invisible markers for legend display)
|
|
610
|
+
legend_traces: list[dict[str, Any]] = [
|
|
611
|
+
{
|
|
612
|
+
"type": "scatter",
|
|
613
|
+
"x": [None],
|
|
614
|
+
"y": [None],
|
|
615
|
+
"mode": "markers",
|
|
616
|
+
"marker": {"size": 12, "color": QUERIED_FILL},
|
|
617
|
+
"name": "Queried Column",
|
|
618
|
+
"showlegend": True,
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
"type": "scatter",
|
|
622
|
+
"x": [None],
|
|
623
|
+
"y": [None],
|
|
624
|
+
"mode": "markers",
|
|
625
|
+
"marker": {"size": 12, "color": ROOT_FILL},
|
|
626
|
+
"name": "Root (no upstream)",
|
|
627
|
+
"showlegend": True,
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"type": "scatter",
|
|
631
|
+
"x": [None],
|
|
632
|
+
"y": [None],
|
|
633
|
+
"mode": "markers",
|
|
634
|
+
"marker": {"size": 12, "color": LEAF_FILL},
|
|
635
|
+
"name": "Leaf (no downstream)",
|
|
636
|
+
"showlegend": True,
|
|
637
|
+
},
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
direction_label = (
|
|
641
|
+
"Upstream" if result.direction == "upstream" else "Downstream"
|
|
642
|
+
)
|
|
643
|
+
title = f"{direction_label} Lineage: {result.query_column}"
|
|
644
|
+
|
|
645
|
+
figure = {
|
|
646
|
+
"data": edge_traces + [node_trace] + legend_traces,
|
|
647
|
+
"layout": {
|
|
648
|
+
"title": {"text": title},
|
|
649
|
+
"showlegend": True,
|
|
650
|
+
"legend": {"x": 1, "y": 1, "xanchor": "right"},
|
|
651
|
+
"hovermode": "closest",
|
|
652
|
+
"xaxis": {"visible": False},
|
|
653
|
+
"yaxis": {"visible": False},
|
|
654
|
+
"margin": {"l": 40, "r": 40, "t": 60, "b": 40},
|
|
655
|
+
},
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return json.dumps(figure, indent=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|