spanner-graph-notebook 1.0.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.
Files changed (40) hide show
  1. assets/authentication.png +0 -0
  2. assets/full_viz.png +0 -0
  3. assets/hero.png +0 -0
  4. assets/hero_with_properties.png +0 -0
  5. assets/jupyter-spin-up.png +0 -0
  6. assets/load_ext.png +0 -0
  7. assets/mock_data_result.png +0 -0
  8. assets/new_notebook.png +0 -0
  9. assets/notebook_package_load.png +0 -0
  10. assets/query_graph.png +0 -0
  11. assets/sample_jupyter.png +0 -0
  12. spanner_graph_notebook-1.0.0.dist-info/LICENSE +202 -0
  13. spanner_graph_notebook-1.0.0.dist-info/METADATA +169 -0
  14. spanner_graph_notebook-1.0.0.dist-info/RECORD +40 -0
  15. spanner_graph_notebook-1.0.0.dist-info/WHEEL +5 -0
  16. spanner_graph_notebook-1.0.0.dist-info/top_level.txt +2 -0
  17. spanner_graphs/__init__.py +19 -0
  18. spanner_graphs/conversion.py +299 -0
  19. spanner_graphs/database.py +199 -0
  20. spanner_graphs/graph_entities.py +405 -0
  21. spanner_graphs/graph_mock_data.csv +51 -0
  22. spanner_graphs/graph_mock_schema.json +681 -0
  23. spanner_graphs/magics.py +211 -0
  24. spanner_graphs/schema_manager.py +60 -0
  25. templates/assets/images/graph-bg.svg +4 -0
  26. templates/spanner-graph/models/edge.js +77 -0
  27. templates/spanner-graph/models/graph-object.js +64 -0
  28. templates/spanner-graph/models/node.js +77 -0
  29. templates/spanner-graph/models/schema.js +327 -0
  30. templates/spanner-graph/spanner-config.js +304 -0
  31. templates/spanner-graph/spanner-store.js +382 -0
  32. templates/spanner-graph/visualization/spanner-forcegraph.js +1380 -0
  33. templates/spanner-graph/visualization/spanner-sidebar.js +904 -0
  34. templates/template-spannergraph.html +210 -0
  35. tests/__init__.py +13 -0
  36. tests/conversion_test.py +163 -0
  37. tests/database_test.py +62 -0
  38. tests/graph_entities_test.py +124 -0
  39. tests/schema_manager_test.py +115 -0
  40. tests/test_notebook.json +23 -0
@@ -0,0 +1,211 @@
1
+ # Copyright 2024 Google LLC
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Magic class for our visualization"""
16
+
17
+ import argparse
18
+ import base64
19
+ from enum import Enum, auto
20
+ import json
21
+ import os
22
+ import sys
23
+ from IPython.core.magic import Magics, magics_class, cell_magic
24
+ from IPython.display import display, clear_output, IFrame
25
+ from networkx import DiGraph
26
+ import ipywidgets as widgets
27
+ from ipywidgets import interact
28
+ from jinja2 import Template
29
+
30
+ from spanner_graphs.conversion import (columns_to_native_numpy,
31
+ prepare_data_for_graphing, SizeMode)
32
+
33
+ from spanner_graphs.database import SpannerDatabase, MockSpannerDatabase
34
+
35
+
36
+ def _load_file(path: list[str]) -> str:
37
+ file_path = os.path.sep.join(path)
38
+ if not os.path.exists(file_path):
39
+ raise FileNotFoundError(f"Template file not found: {file_path}")
40
+
41
+ with open(file_path, 'r') as file:
42
+ content = file.read()
43
+
44
+ return content
45
+
46
+ def _load_image(path: list[str]) -> str:
47
+ file_path = os.path.sep.join(path)
48
+ if not os.path.exists(file_path):
49
+ print("image does not exist")
50
+ return ''
51
+
52
+ if file_path.lower().endswith('.svg'):
53
+ with open(file_path, 'r') as file:
54
+ svg = file.read()
55
+ return base64.b64encode(svg.encode('utf-8')).decode('utf-8')
56
+ else:
57
+ with open(file_path, 'rb') as file:
58
+ return base64.b64decode(file.read()).decode('utf-8')
59
+
60
+ def _generate_html(graph: DiGraph, rows, schema):
61
+ if not isinstance(rows, list):
62
+ rows = []
63
+
64
+ # Get the directory of the current file (magics.py)
65
+ current_dir = os.path.dirname(os.path.abspath(__file__))
66
+
67
+ # Go up directories until we find the 'templates' folder
68
+ search_dir = current_dir
69
+ while 'templates' not in os.listdir(search_dir):
70
+ parent = os.path.dirname(search_dir)
71
+ if parent == search_dir: # We've reached the root directory after I updated
72
+ raise FileNotFoundError("Could not find 'templates' directory")
73
+ search_dir = parent
74
+
75
+ # Retrieve the javascript content
76
+ template_content = _load_file([search_dir, 'templates', 'template-spannergraph.html'])
77
+ schema_content = _load_file([search_dir, 'templates', 'spanner-graph', 'models', 'schema.js'])
78
+ graph_object_content = _load_file([search_dir, 'templates', 'spanner-graph', 'models', 'graph-object.js'])
79
+ node_content = _load_file([search_dir, 'templates', 'spanner-graph', 'models', 'node.js'])
80
+ edge_content = _load_file([search_dir, 'templates', 'spanner-graph', 'models', 'edge.js'])
81
+ config_content = _load_file([search_dir, 'templates', 'spanner-graph', 'spanner-config.js'])
82
+ store_content = _load_file([search_dir, 'templates', 'spanner-graph', 'spanner-store.js'])
83
+ graph_content = _load_file([search_dir, 'templates', 'spanner-graph', 'visualization', 'spanner-forcegraph.js'])
84
+ sidebar_content = _load_file([search_dir, 'templates', 'spanner-graph', 'visualization', 'spanner-sidebar.js'])
85
+
86
+ # Retrieve image content
87
+ graph_background_image = _load_image([search_dir, "templates", "assets", "images", "graph-bg.svg"])
88
+
89
+ # Create a Jinja2 template
90
+ template = Template(template_content)
91
+
92
+ nodes = []
93
+ for (node_id, node) in graph.nodes(data=True):
94
+ nodes.append(node)
95
+
96
+ edges = []
97
+ for (from_id, to_id, edge) in graph.edges(data=True):
98
+ edges.append(edge)
99
+
100
+ # Render the template with the graph data and JavaScript content
101
+ html_content = template.render(
102
+ graph_background_image=graph_background_image,
103
+ template_content=template_content,
104
+ schema_content=schema_content,
105
+ graph_object_content=graph_object_content,
106
+ node_content=node_content,
107
+ edge_content=edge_content,
108
+ config_content=config_content,
109
+ graph_content=graph_content,
110
+ store_content=store_content,
111
+ sidebar_content=sidebar_content,
112
+ nodes=nodes,
113
+ edges=edges,
114
+ rows=rows,
115
+ schema=schema
116
+ )
117
+
118
+ return html_content
119
+
120
+
121
+ def _parse_element_display(element_rep: str) -> dict[str, str]:
122
+ """Helper function to parse element display fields into a dict."""
123
+ if not element_rep:
124
+ return {}
125
+ res = {
126
+ e.strip().split(":")[0].lower(): e.strip().split(":")[1]
127
+ for e in element_rep.strip().split(",")
128
+ }
129
+ return res
130
+
131
+
132
+ @magics_class
133
+ class NetworkVisualizationMagics(Magics):
134
+ """Network visualizer with Networkx"""
135
+
136
+ def __init__(self, shell):
137
+ super().__init__(shell)
138
+ self.database = None
139
+ self.limit = 5
140
+ self.args = None
141
+ self.cell = None
142
+
143
+ def visualize(self, limit):
144
+ """Helper function to create and display the visualization"""
145
+ query_result, fields, rows, schema_json = self.database.execute_query(self.cell, limit)
146
+ d, ignored_columns = columns_to_native_numpy(query_result, fields)
147
+
148
+
149
+ graph: DiGraph = prepare_data_for_graphing(
150
+ incoming=d,
151
+ schema_json=schema_json)
152
+
153
+ if len(ignored_columns) > 0:
154
+ print(f"Some returned fields are not graph "
155
+ f"element JSON type, so they are not "
156
+ f"visualized below: {', '.join(ignored_columns)}")
157
+
158
+ # Generate the HTML content
159
+ html_content = _generate_html(graph, rows, schema_json)
160
+
161
+ # Encode the HTML content
162
+ encoded_content = base64.b64encode(html_content.encode()).decode()
163
+ data_uri = f"data:text/html;base64,{encoded_content}"
164
+
165
+ display(IFrame(src=data_uri, width="100%", height="700px"))
166
+
167
+ @cell_magic
168
+ def spanner_graph(self, line: str, cell: str):
169
+ """spanner_graph function"""
170
+ parser = argparse.ArgumentParser(
171
+ description="Visualize network from Spanner database")
172
+ parser.add_argument("--project", help="GCP project ID")
173
+ parser.add_argument("--instance",
174
+ help="Spanner instance ID")
175
+ parser.add_argument("--database",
176
+ help="Spanner database ID")
177
+ parser.add_argument("--mock",
178
+ action="store_true",
179
+ help="Use mock database")
180
+
181
+ args = parser.parse_args(line.split())
182
+ if not args.mock:
183
+ if not (args.project and args.instance and args.database):
184
+ raise ValueError(
185
+ "Please provide `--project`, `--instance`, "
186
+ "and `--database` values for your query.")
187
+
188
+ try:
189
+ self.args = parser.parse_args(line.split())
190
+ self.cell = cell
191
+
192
+ if self.args.mock:
193
+ self.database = MockSpannerDatabase()
194
+ else:
195
+ self.database = SpannerDatabase(self.args.project,
196
+ self.args.instance,
197
+ self.args.database)
198
+
199
+ clear_output(wait=True)
200
+ self.visualize(self.limit)
201
+ except ValueError as e:
202
+ print(f"Error: {e}")
203
+ print("Usage: %%spanner_graph_viz --project PROJECT_ID "
204
+ "--instance INSTANCE_ID --database DATABASE_ID "
205
+ "[--mock] ")
206
+ print(" SELECT ... you query here ...")
207
+
208
+
209
+ def load_ipython_extension(ipython):
210
+ """Registration function"""
211
+ ipython.register_magics(NetworkVisualizationMagics)
@@ -0,0 +1,60 @@
1
+ # Copyright 2024 Google LLC
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Dict, List, Set
18
+ from .graph_entities import Node
19
+
20
+ class SchemaManager:
21
+ def __init__(self, schema_json: dict = None):
22
+ self.schema_dict = schema_json or {}
23
+ self.node_label_to_property_names = self._build_node_mappings()
24
+ self.unique_node_labels = self._find_unique_node_labels()
25
+
26
+ def _find_unique_node_labels(self) -> Set[str]:
27
+ label_count = {}
28
+ for node_table in self.schema_dict.get('nodeTables', []):
29
+ labelNames = node_table.get('labelNames', [])
30
+ if len(labelNames) == 1:
31
+ label = labelNames[0]
32
+ label_count[label] = label_count.get(label, 0) + 1
33
+ return { label for label, count in label_count.items() if count == 1}
34
+
35
+ def _build_node_mappings(self) -> Dict[str, List[str]]:
36
+ node_label_to_property_names = {}
37
+ for node_table in self.schema_dict.get('nodeTables', []):
38
+ labelNames = node_table.get('labelNames', [])
39
+ if len(labelNames) != 1:
40
+ continue
41
+
42
+ keyColumns = node_table.get('keyColumns', [])
43
+ propertyDefinitions = node_table.get('propertyDefinitions', [])
44
+ label = labelNames[0]
45
+ key_property_names = []
46
+ for keyColumn in keyColumns:
47
+ for prop in propertyDefinitions:
48
+ if prop.get('valueExpressionSql', '') == keyColumn:
49
+ key_property_names.append(prop.get('propertyDeclarationName'))
50
+ break
51
+ node_label_to_property_names[label] = key_property_names
52
+
53
+ return node_label_to_property_names
54
+
55
+ def get_key_property_names(self, obj: Node) -> List[str]:
56
+ if not isinstance(obj, Node):
57
+ raise TypeError("node expected")
58
+ if len(obj.labels) == 1 and obj.labels[0] in self.unique_node_labels:
59
+ return self.node_label_to_property_names.get(obj.labels[0], [])
60
+ return []
@@ -0,0 +1,4 @@
1
+ <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="10" height="10" fill="#FBFDFF"/>
3
+ <rect x="4" y="4" width="2" height="2" fill="#E8EAED"/>
4
+ </svg>
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+
16
+ class Edge extends GraphObject {
17
+ /**
18
+ * The identifier of the node this edge is directed to.
19
+ * @type {Node}
20
+ */
21
+ target;
22
+
23
+ /**
24
+ * The identifier of the node this edge originates from.
25
+ * @type {Node}
26
+ */
27
+ source;
28
+
29
+ /**
30
+ * @typedef {Object} EdgeData - The label shown in the sidebar or graph.
31
+ * @property {string} label
32
+ * @property {string|Object} properties - An optional property:value map.
33
+ * @property {Object} key_property_names
34
+ * @property {string} color
35
+ * @property {number} from
36
+ * @property {number} to
37
+ * @property {number} id
38
+ */
39
+
40
+ /**
41
+ * An edge is the line that connects two Nodes.
42
+ *
43
+ * @param {Object} params
44
+ * @param {string} params.to - The identifier of the node this edge is directed to.
45
+ * @param {string} params.from - The identifier of the node this edge originates from.
46
+ * @param {string} params.label - The label for the edge.
47
+ * @param {string|Object} params.title - The optional property:value map for the edge.
48
+ * @extends GraphObject
49
+ */
50
+ constructor({ to, from, label, properties, title }) {
51
+ super({ label, title, properties });
52
+
53
+ if (!this.isNumber(to) || !this.isNumber(from)) {
54
+ this.instantiated = false;
55
+ console.log('Failed to instantiate edge', { reason: '"to" and "from" are not numbers', to, from, label, title });
56
+ return;
57
+ }
58
+
59
+ /**
60
+ * preserve ID from getting
61
+ * overwritten by ForceGraph
62
+ */
63
+ this.to = to;
64
+ this.from = from;
65
+
66
+ this.source = from;
67
+ this.target = to;
68
+
69
+ this.instantiated = true;
70
+ }
71
+
72
+ isNumber(value) {
73
+ return Number.isFinite(Number(value));
74
+ }
75
+ }
76
+
77
+ window[namespace].Edge = Edge;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+
16
+ class GraphObject {
17
+ /**
18
+ * The label of the Graph Object.
19
+ * @type {string}
20
+ */
21
+ label;
22
+
23
+ /**
24
+ * A map of properties and their values describing the Graph Ebject.
25
+ * @type {{[key: string]: string}}
26
+ */
27
+ properties = {};
28
+
29
+ /**
30
+ * A boolean indicating if the Graph Object object has been instantiated.
31
+ * @type {boolean}
32
+ */
33
+ instantiated = false;
34
+
35
+ /**
36
+ * The key property names for the graph element determines what keys in the properties
37
+ * are to be displayed.
38
+ * @type {string[]}
39
+ */
40
+ key_property_names = [];
41
+
42
+ /**
43
+ * The reason for the instantiation error.
44
+ * @type {string}
45
+ */
46
+ instantiationErrorReason;
47
+
48
+
49
+ /**
50
+ * An object that renders on the graph.
51
+ *
52
+ * @param {Object} params
53
+ * @param {string} params.label - The label for the object.
54
+ * @param {Object} params.properties - The optional property:value map for the object.
55
+ */
56
+ constructor({ label, properties, key_property_names }) {
57
+ this.label = label;
58
+ this.properties = properties;
59
+ this.key_property_names = key_property_names;
60
+ this.instantiated = true;
61
+ }
62
+ }
63
+
64
+ window[namespace].GraphObject = GraphObject;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ */
15
+
16
+ /**
17
+ * Represents a graph node.
18
+ * @class
19
+ */
20
+ class Node extends GraphObject {
21
+ /**
22
+ * Arbitrary value
23
+ * @type {number}
24
+ */
25
+ value;
26
+
27
+ /**
28
+ * The numeric value associated with the node.
29
+ * @type {number}
30
+ */
31
+ id;
32
+
33
+ /**
34
+ * The numeric value associated with the neighborhood.
35
+ * This may be used by the visualization implementation for clustering.
36
+ * @type {number}
37
+ */
38
+ neighborhood = 0;
39
+
40
+ color = '#ec0001'
41
+
42
+ /**
43
+ * @typedef {Object} NodeData - The label shown in the sidebar or graph.
44
+ * @property {string} label
45
+ * @property {Object} properties - An optional property:value map.
46
+ * @property {Object} key_property_names
47
+ * @property {string} color
48
+ * @property {number} id
49
+ */
50
+
51
+ /**
52
+ * A node on the graph
53
+ *
54
+ * @param {Object} params
55
+ * @param {string} params.label - The label for the edge.
56
+ * @param {string|Object} params.title - The optional property:value map for the edge.
57
+ * @param {string} params.color - The color of the edge
58
+ * @extends GraphObject
59
+ */
60
+ constructor({ label, title, properties, value, id, neighborhood, color, key_property_names }) {
61
+ super({ label, title, properties, key_property_names });
62
+
63
+ if (typeof id != 'number') {
64
+ this.instantiationErrorReason = "Node does not have an ID";
65
+ console.error(this.instantiationErrorReason, { label, title, value, id });
66
+ return;
67
+ }
68
+
69
+ this.id = id;
70
+ this.value = value;
71
+ this.instantiated = true;
72
+ this.neighborhood = typeof neighborhood === 'number' ? neighborhood : 0;
73
+ this.color = color;
74
+ }
75
+ }
76
+
77
+ window[namespace].Node = Node;