spanner-graph-notebook 1.1.2__tar.gz → 1.1.4__tar.gz

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 (91) hide show
  1. {spanner_graph_notebook-1.1.2/spanner_graph_notebook.egg-info → spanner_graph_notebook-1.1.4}/PKG-INFO +3 -2
  2. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/app.js +5 -0
  3. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/graph-server.js +27 -20
  4. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/node.js +14 -1
  5. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/spanner-config.js +3 -2
  6. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/spanner-store.js +7 -2
  7. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-forcegraph.js +3 -2
  8. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-sidebar.js +3 -1
  9. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/graph-server.test.ts +17 -13
  10. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/node.test.ts +28 -0
  11. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/spanner-config.test.ts +69 -0
  12. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/spanner-store.test.ts +82 -0
  13. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/setup.py +1 -1
  14. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4/spanner_graph_notebook.egg-info}/PKG-INFO +3 -2
  15. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/conversion.py +13 -0
  16. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_entities.py +15 -2
  17. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_server.py +117 -108
  18. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/magics.py +28 -17
  19. spanner_graph_notebook-1.1.4/tests/conversion_test.py +231 -0
  20. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/graph_entities_test.py +41 -22
  21. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/graph_server_test.py +53 -36
  22. spanner_graph_notebook-1.1.4/tests/node_expansion_test.py +165 -0
  23. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/server_test.py +35 -3
  24. spanner_graph_notebook-1.1.2/tests/conversion_test.py +0 -80
  25. spanner_graph_notebook-1.1.2/tests/node_expansion_test.py +0 -106
  26. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/CONTRIBUTING.md +0 -0
  27. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/LICENSE +0 -0
  28. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/MANIFEST.in +0 -0
  29. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/README.md +0 -0
  30. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/authentication.png +0 -0
  31. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/colab_usage.png +0 -0
  32. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/full_viz.png +0 -0
  33. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/hero.png +0 -0
  34. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/hero_with_properties.png +0 -0
  35. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/jupyter-spin-up.png +0 -0
  36. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/jupyter.gif +0 -0
  37. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/load_ext.png +0 -0
  38. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/mock_data_result.png +0 -0
  39. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/new_notebook.png +0 -0
  40. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/notebook_package_load.png +0 -0
  41. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/query_graph.png +0 -0
  42. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/assets/sample_jupyter.png +0 -0
  43. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/babel.config.js +0 -0
  44. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/jest-puppeteer.config.js +0 -0
  45. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/jest.config.ts +0 -0
  46. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/package.json +0 -0
  47. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/edge.js +0 -0
  48. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/graph-object.js +0 -0
  49. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/models/schema.js +0 -0
  50. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-menu.js +0 -0
  51. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/src/visualization/spanner-table.js +0 -0
  52. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/dev.html +0 -0
  53. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/graph-bg.svg +0 -0
  54. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/index.html +0 -0
  55. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/static/test.html +0 -0
  56. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/jest.setup.ts +0 -0
  57. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/mock-data.json +0 -0
  58. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/serve-content.js +0 -0
  59. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/edge.test.ts +0 -0
  60. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/graph-object.test.ts +0 -0
  61. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/models/schema.test.ts +0 -0
  62. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-menu.test.ts +0 -0
  63. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-sidebar.test.ts +0 -0
  64. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/unit/visualization/spanner-table.test.ts +0 -0
  65. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/app.test.ts +0 -0
  66. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/jest-visual.setup.ts +0 -0
  67. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-forcegraph.test.ts +0 -0
  68. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-menu.test.ts +0 -0
  69. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-sidebar.test.ts +0 -0
  70. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tests/visual/visualization/spanner-table.test.ts +0 -0
  71. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/frontend/tsconfig.json +0 -0
  72. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/sample.ipynb +0 -0
  73. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/setup.cfg +0 -0
  74. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/SOURCES.txt +0 -0
  75. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/dependency_links.txt +0 -0
  76. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/requires.txt +0 -0
  77. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graph_notebook.egg-info/top_level.txt +0 -0
  78. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/__init__.py +0 -0
  79. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/database.py +0 -0
  80. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/dev_util/__init__.py +0 -0
  81. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/dev_util/serve_dev.py +0 -0
  82. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_mock_data.csv +0 -0
  83. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_mock_schema.json +0 -0
  84. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/graph_visualization.py +0 -0
  85. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/spanner_graphs/schema_manager.py +0 -0
  86. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/__init__.py +0 -0
  87. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/database_test.py +0 -0
  88. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/magics_test.py +0 -0
  89. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/sample_notebook_test.py +0 -0
  90. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/schema_manager_test.py +0 -0
  91. {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.4}/tests/test_notebook.json +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: spanner-graph-notebook
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: Visually query Spanner Graph data in notebooks.
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -15,6 +15,7 @@ Requires-Dist: portpicker
15
15
  Requires-Dist: pydata-google-auth
16
16
  Dynamic: description
17
17
  Dynamic: description-content-type
18
+ Dynamic: license-file
18
19
  Dynamic: requires-dist
19
20
  Dynamic: summary
20
21
 
@@ -164,6 +164,11 @@ class SpannerApp {
164
164
  return;
165
165
  }
166
166
 
167
+ if (data.error) {
168
+ this.graph.showErrorStateForNode(node, error);
169
+ return;
170
+ }
171
+
167
172
  const newData = this.store.appendGraphData(data.response.nodes, data.response.edges)
168
173
  if (newData) {
169
174
  // Show success message before appending data
@@ -13,8 +13,17 @@
13
13
  * limitations under the License.
14
14
  */
15
15
 
16
+ function isColab() {
17
+ return typeof google !== 'undefined'
18
+ }
19
+
16
20
  class GraphServer {
17
21
  isFetching = false;
22
+
23
+ /**
24
+ * Port number to connect to graph server. In colab environments, this is always undefined
25
+ * and not used (we use colab callbacks instead).
26
+ */
18
27
  port = 8195;
19
28
 
20
29
  endpoints = {
@@ -58,17 +67,21 @@ class GraphServer {
58
67
  }
59
68
 
60
69
  constructor(port, params) {
61
- let numericalPort = port;
62
- if (typeof numericalPort !== 'number') {
63
- numericalPort = Number.parseInt(numericalPort);
70
+ if (isColab()) {
71
+ this.port = undefined;
72
+ } else {
73
+ let numericalPort = port;
74
+ if (typeof numericalPort !== 'number') {
75
+ numericalPort = Number.parseInt(numericalPort);
64
76
 
65
- if (isNaN(numericalPort)) {
66
- console.error('Graph Server was not given a numerical port', {port});
67
- return;
77
+ if (isNaN(numericalPort)) {
78
+ console.error('Graph Server was not given a numerical port', {port});
79
+ return;
80
+ }
68
81
  }
69
- }
70
82
 
71
- this.port = numericalPort;
83
+ this.port = numericalPort;
84
+ }
72
85
  this.params = params
73
86
  }
74
87
 
@@ -109,10 +122,7 @@ class GraphServer {
109
122
  }
110
123
  }
111
124
 
112
- const {project, instance, database, graph} = JSON.parse(this.params);
113
-
114
125
  const request = {
115
- project, instance, database, graph,
116
126
  uid: node.uid,
117
127
  node_labels: node.labels,
118
128
  node_properties: validProperties,
@@ -125,21 +135,18 @@ class GraphServer {
125
135
 
126
136
  this.isFetching = true;
127
137
 
128
- if (typeof google !== 'undefined') {
138
+ if (isColab()) {
129
139
  return google.colab.kernel.invokeFunction('graph_visualization.NodeExpansion', [request, this.params])
130
140
  .then(result => result.data['application/json'])
131
141
  .finally(() => this.isFetching = false);
132
142
  }
133
143
 
134
- // For non-Colab environment, combine params and request
135
- const fullRequest = {
136
- ...JSON.parse(this.params),
137
- ...request
138
- };
139
-
140
144
  return fetch(this.buildRoute(this.endpoints.postNodeExpansion), {
141
145
  method: 'POST',
142
- body: JSON.stringify(fullRequest)
146
+ body: JSON.stringify({
147
+ params: this.params,
148
+ request
149
+ })
143
150
  })
144
151
  .then(response => {
145
152
  if (!response.ok) {
@@ -161,7 +168,7 @@ class GraphServer {
161
168
 
162
169
  this.isFetching = true;
163
170
 
164
- if (typeof google !== 'undefined') {
171
+ if (isColab()) {
165
172
  return google.colab.kernel.invokeFunction('graph_visualization.Query', [], request)
166
173
  .then(result => result.data['application/json'])
167
174
  .finally(() => this.isFetching = false);
@@ -39,6 +39,13 @@ class Node extends GraphObject {
39
39
  */
40
40
  identifiers = [];
41
41
 
42
+ /**
43
+ * Denotes if the node was created in the absence of a node from the query response.
44
+ * This may happen when a query only returns edges. The edge has UIDs for a source/destination node,
45
+ * but no nodes were returned from the response.
46
+ * @type {boolean}
47
+ */
48
+ intermediate = false;
42
49
 
43
50
  /**
44
51
  * The following properties will be set by ForceGraph.
@@ -58,6 +65,7 @@ class Node extends GraphObject {
58
65
  * @property {Object} key_property_names
59
66
  * @property {string} color
60
67
  * @property {string} identifier
68
+ * @property {bool} intermediate
61
69
  */
62
70
 
63
71
  /**
@@ -65,11 +73,12 @@ class Node extends GraphObject {
65
73
  * @param {NodeData} params
66
74
  */
67
75
  constructor(params) {
68
- const { labels, title, properties, value, key_property_names, identifier } = params;
76
+ const { labels, title, properties, value, key_property_names, identifier, intermediate } = params;
69
77
  super({ labels, title, properties, key_property_names, identifier });
70
78
 
71
79
  this.value = value;
72
80
  this.instantiated = true;
81
+ this.intermediate = intermediate || false;
73
82
 
74
83
  // Parse the human-readable unique identifiers that
75
84
  // distinguishes a node from its peers
@@ -81,6 +90,10 @@ class Node extends GraphObject {
81
90
  }
82
91
  }
83
92
  }
93
+
94
+ isIntermediateNode() {
95
+ return this.intermediate;
96
+ }
84
97
  }
85
98
 
86
99
  if (typeof module !== 'undefined' && module.exports) {
@@ -207,7 +207,7 @@ class GraphConfig {
207
207
 
208
208
  for (const uid of Object.keys(this.nodes)) {
209
209
  const node = this.nodes[uid];
210
- if (!node || !(node instanceof Node)) {
210
+ if (!node || !(node instanceof Node) || node.isIntermediateNode()) {
211
211
  continue;
212
212
  }
213
213
 
@@ -411,7 +411,8 @@ class GraphConfig {
411
411
  const newEdges = this.parseEdges(edgesData);
412
412
 
413
413
  for (const uid of Object.keys(newNodes)) {
414
- if (!this.nodes[uid]) {
414
+ const existingNode = this.nodes[uid];
415
+ if (!(existingNode instanceof Node) || existingNode.isIntermediateNode()) {
415
416
  this.nodes[uid] = newNodes[uid];
416
417
  }
417
418
  }
@@ -450,11 +450,15 @@ class GraphStore {
450
450
  * @returns {string} The color for the node based on its label.
451
451
  */
452
452
  getColorForNodeByLabel(node) {
453
- const defaultColor = 'rgb(100, 100, 100)';
453
+ const defaultColor = 'rgb(100, 100, 100)'
454
454
  if (!node || !(node instanceof Node)) {
455
455
  return defaultColor;
456
456
  }
457
457
 
458
+ if (node.isIntermediateNode()) {
459
+ return 'rgb(128, 134, 139)';
460
+ }
461
+
458
462
  const nodeColor = this.config.nodeColors[node.getLabels()];
459
463
  if (nodeColor) {
460
464
  return nodeColor;
@@ -489,7 +493,8 @@ class GraphStore {
489
493
  const newNodes = [];
490
494
  if (Array.isArray(nodesData)) {
491
495
  for (const nodeData of nodesData) {
492
- if (this.config.nodes[nodeData.identifier]) {
496
+ const existingNode = this.config.nodes[nodeData.identifier];
497
+ if (existingNode && !existingNode.isIntermediateNode()) {
493
498
  continue;
494
499
  }
495
500
 
@@ -1086,6 +1086,7 @@ class GraphVisualization {
1086
1086
  }
1087
1087
 
1088
1088
  const showLabel =
1089
+ !node.isIntermediateNode() && (
1089
1090
  // The user has chosen to view labels
1090
1091
  this.store.config.showLabels ||
1091
1092
  // Always show labels in Schema view
@@ -1096,7 +1097,7 @@ class GraphVisualization {
1096
1097
  this.selectedNodeNeighbors.includes(node) ||
1097
1098
  this.focusedNodeNeighbors.includes(node) ||
1098
1099
  this.focusedEdgeNeighbors.includes(node) ||
1099
- this.selectedEdgeNeighbors.includes(node);
1100
+ this.selectedEdgeNeighbors.includes(node));
1100
1101
 
1101
1102
  // Init label
1102
1103
  ctx.save();
@@ -1438,7 +1439,7 @@ class GraphVisualization {
1438
1439
  */
1439
1440
  _showMouseContextMenu(node, event) {
1440
1441
  // Don't show context menu in schema mode
1441
- if (this.store.config.viewMode === GraphConfig.ViewModes.SCHEMA) {
1442
+ if (this.store.config.viewMode === GraphConfig.ViewModes.SCHEMA || node.isIntermediateNode()) {
1442
1443
  return;
1443
1444
  }
1444
1445
 
@@ -277,7 +277,9 @@ class SidebarConstructor {
277
277
  if (this.store.config.selectedGraphObject) {
278
278
  if (this.store.config.selectedGraphObject instanceof Node) {
279
279
  this.properties();
280
- this.expandNode();
280
+ if (!this.store.config.selectedGraphObject.isIntermediateNode()) {
281
+ this.expandNode();
282
+ }
281
283
  this.neighbors();
282
284
  } else {
283
285
  this.neighbors();
@@ -148,19 +148,23 @@ describe('GraphServer', () => {
148
148
  {
149
149
  method: 'POST',
150
150
  body: JSON.stringify({
151
- project: 'test-project',
152
- instance: 'test-instance',
153
- database: 'test-database',
154
- graph: 'test-graph',
155
- mock: false,
156
- uid: 'node1',
157
- node_labels: ['TestLabel1'],
158
- node_properties: [{
159
- key: 'name',
160
- value: 'my-node-name',
161
- type: 'STRING'
162
- }],
163
- direction: 'OUTGOING'
151
+ params: JSON.stringify({
152
+ project: 'test-project',
153
+ instance: 'test-instance',
154
+ database: 'test-database',
155
+ graph: 'test-graph',
156
+ mock: false
157
+ }),
158
+ request: {
159
+ uid: 'node1',
160
+ node_labels: ['TestLabel1'],
161
+ node_properties: [{
162
+ key: 'name',
163
+ value: 'my-node-name',
164
+ type: 'STRING'
165
+ }],
166
+ direction: 'OUTGOING'
167
+ }
164
168
  })
165
169
  }
166
170
  );
@@ -57,4 +57,32 @@ describe('Node', () => {
57
57
  expect(graphNode.identifiers).toEqual(['foo', 'boolean', 'zero']);
58
58
  });
59
59
  });
60
+
61
+ describe('Intermediate Nodes', () => {
62
+ it('should create a non-intermediate node by default', () => {
63
+ const graphNode = new GraphNode(validNodeData);
64
+ expect(graphNode.intermediate).toBe(false);
65
+ expect(graphNode.isIntermediateNode()).toBe(false);
66
+ });
67
+
68
+ it('should create an intermediate node when specified', () => {
69
+ const intermediateNodeData = {
70
+ ...validNodeData,
71
+ intermediate: true
72
+ };
73
+ const graphNode = new GraphNode(intermediateNodeData);
74
+ expect(graphNode.intermediate).toBe(true);
75
+ expect(graphNode.isIntermediateNode()).toBe(true);
76
+ });
77
+
78
+ it('should handle undefined intermediate flag', () => {
79
+ const nodeData = {
80
+ ...validNodeData,
81
+ intermediate: undefined
82
+ };
83
+ const graphNode = new GraphNode(nodeData);
84
+ expect(graphNode.intermediate).toBe(false);
85
+ expect(graphNode.isIntermediateNode()).toBe(false);
86
+ });
87
+ });
60
88
  });
@@ -306,4 +306,73 @@ describe('GraphConfig', () => {
306
306
  expect(mockConfig.nodeCount).toBe(2);
307
307
  });
308
308
  });
309
+
310
+ describe('Intermediate Node Handling', () => {
311
+ test('should exclude intermediate nodes from color assignment', () => {
312
+ const mockConfig = new GraphConfig({
313
+ nodesData: [
314
+ { identifier: "1", labels: ['Person'], properties: {} },
315
+ { identifier: "2", labels: ['Movie'], properties: {}, intermediate: true }
316
+ ],
317
+ edgesData: [],
318
+ colorPalette: ['#FF0000'],
319
+ colorScheme: GraphConfig.ColorScheme.LABEL,
320
+ rowsData: [],
321
+ schemaData: {},
322
+ queryResult: {}
323
+ });
324
+
325
+ // Should only assign color to non-intermediate node
326
+ expect(Object.keys(mockConfig.nodeColors).length).toBe(1);
327
+ expect(mockConfig.nodeColors['Person']).toBeDefined();
328
+ });
329
+
330
+ test('should allow replacing intermediate nodes with real nodes', () => {
331
+ const mockConfig = new GraphConfig({
332
+ nodesData: [
333
+ { identifier: "1", labels: ['Person'], properties: {}, intermediate: true }
334
+ ],
335
+ edgesData: [],
336
+ colorPalette: ['#FF0000'],
337
+ colorScheme: GraphConfig.ColorScheme.LABEL,
338
+ rowsData: [],
339
+ schemaData: {},
340
+ queryResult: {}
341
+ });
342
+
343
+ // Append a real node with the same identifier
344
+ mockConfig.appendGraphData([
345
+ { identifier: "1", labels: ['Person'], properties: { name: 'John' } }
346
+ ], []);
347
+
348
+ const node = mockConfig.nodes["1"];
349
+ expect(node).toBeDefined();
350
+ expect(node.isIntermediateNode()).toBe(false);
351
+ expect(node.properties.name).toBe('John');
352
+ });
353
+
354
+ test('should not replace real nodes with intermediate nodes', () => {
355
+ const mockConfig = new GraphConfig({
356
+ nodesData: [
357
+ { identifier: "1", labels: ['Person'], properties: { name: 'John' } }
358
+ ],
359
+ edgesData: [],
360
+ colorPalette: ['#FF0000'],
361
+ colorScheme: GraphConfig.ColorScheme.LABEL,
362
+ rowsData: [],
363
+ schemaData: {},
364
+ queryResult: {}
365
+ });
366
+
367
+ // Try to append an intermediate node with the same identifier
368
+ mockConfig.appendGraphData([
369
+ { identifier: "1", labels: ['Person'], properties: {}, intermediate: true }
370
+ ], []);
371
+
372
+ const node = mockConfig.nodes["1"];
373
+ expect(node).toBeDefined();
374
+ expect(node.isIntermediateNode()).toBe(false);
375
+ expect(node.properties.name).toBe('John');
376
+ });
377
+ });
309
378
  });
@@ -925,4 +925,86 @@ describe('GraphStore', () => {
925
925
  expect(store.getEdgeTypesOfNodeSorted(undefined)).toEqual([]);
926
926
  expect(store.getEdgeTypesOfNodeSorted({} as any)).toEqual([]);
927
927
  });
928
+
929
+ describe('Intermediate Node Handling', () => {
930
+ test('should return correct color for intermediate nodes', () => {
931
+ const intermediateNode = new GraphNode({
932
+ identifier: "1",
933
+ labels: ['Person'],
934
+ properties: {},
935
+ intermediate: true
936
+ });
937
+
938
+ const color = store.getColorForNodeByLabel(intermediateNode);
939
+ expect(color).toBe('rgb(128, 134, 139)');
940
+ });
941
+
942
+ test('should return correct color for non-intermediate nodes', () => {
943
+ const regularNode = new GraphNode({
944
+ identifier: "1",
945
+ labels: ['Person'],
946
+ properties: {}
947
+ });
948
+
949
+ // Set up the node color explicitly for the test
950
+ store.config.nodeColors['Person'] = '#FF0000';
951
+
952
+ const color = store.getColorForNodeByLabel(regularNode);
953
+ expect(color).toBe('#FF0000'); // Now we expect the color we explicitly set
954
+ });
955
+
956
+ test('should handle node expansion requests for intermediate nodes', () => {
957
+ const intermediateNode = new GraphNode({
958
+ identifier: "1",
959
+ labels: ['Person'],
960
+ properties: {},
961
+ intermediate: true
962
+ });
963
+
964
+ // Mock the event listener
965
+ const mockCallback = jest.fn();
966
+ store.addEventListener(GraphStore.EventTypes.NODE_EXPANSION_REQUEST, mockCallback);
967
+
968
+ // Request expansion
969
+ store.requestNodeExpansion(intermediateNode, 'OUTGOING', 'CONNECTS_TO');
970
+
971
+ // Verify the callback was called with correct parameters
972
+ expect(mockCallback).toHaveBeenCalledWith(
973
+ intermediateNode,
974
+ 'OUTGOING',
975
+ 'CONNECTS_TO',
976
+ expect.any(Array),
977
+ expect.any(Object)
978
+ );
979
+ });
980
+
981
+ test('should handle node expansion requests for non-intermediate nodes', () => {
982
+ const regularNode = new GraphNode({
983
+ identifier: "1",
984
+ labels: ['Person'],
985
+ properties: { name: 'John' }
986
+ });
987
+
988
+ // Mock the event listener
989
+ const mockCallback = jest.fn();
990
+ store.addEventListener(GraphStore.EventTypes.NODE_EXPANSION_REQUEST, mockCallback);
991
+
992
+ // Request expansion
993
+ store.requestNodeExpansion(regularNode, 'OUTGOING', 'CONNECTS_TO');
994
+
995
+ // Verify the callback was called with correct parameters
996
+ expect(mockCallback).toHaveBeenCalledWith(
997
+ regularNode,
998
+ 'OUTGOING',
999
+ 'CONNECTS_TO',
1000
+ expect.arrayContaining([
1001
+ expect.objectContaining({
1002
+ key: 'name',
1003
+ value: 'John'
1004
+ })
1005
+ ]),
1006
+ expect.any(Object)
1007
+ );
1008
+ });
1009
+ });
928
1010
  });
@@ -32,7 +32,7 @@ with open("README.md", "r") as fh:
32
32
 
33
33
  setup(
34
34
  name="spanner-graph-notebook",
35
- version="v1.1.2",
35
+ version="v1.1.4",
36
36
  packages=find_packages(),
37
37
  install_requires=[
38
38
  "networkx", "numpy", "google-cloud-spanner", "ipython",
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: spanner-graph-notebook
3
- Version: 1.1.2
3
+ Version: 1.1.4
4
4
  Summary: Visually query Spanner Graph data in notebooks.
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -15,6 +15,7 @@ Requires-Dist: portpicker
15
15
  Requires-Dist: pydata-google-auth
16
16
  Dynamic: description
17
17
  Dynamic: description-content-type
18
+ Dynamic: license-file
18
19
  Dynamic: requires-dist
19
20
  Dynamic: summary
20
21
 
@@ -76,4 +76,17 @@ def get_nodes_edges(data: Dict[str, List[Any]], fields: List[StructType.Field],
76
76
  edge = Edge.from_json(item)
77
77
  edges.append(edge)
78
78
 
79
+ # Create placeholder nodes for nodes that were not returned
80
+ # from the query but are identified in the edges
81
+ missing_node_identifiers = set()
82
+ for edge in edges:
83
+ if edge.source not in node_identifiers:
84
+ missing_node_identifiers.add(edge.source)
85
+ if edge.destination not in node_identifiers:
86
+ missing_node_identifiers.add(edge.destination)
87
+
88
+ for identifier in missing_node_identifiers:
89
+ nodes.append(Node.make_intermediate(identifier))
90
+ node_identifiers.add(identifier)
91
+
79
92
  return nodes, edges
@@ -146,14 +146,16 @@ class Node:
146
146
  identifier (str): The unique identifier for the node.
147
147
  labels (List[str]): The labels associated with the node.
148
148
  properties (Dict[str, Any]): The properties of the node.
149
+ intermediate (bool): Determines if this node was generated outside of the query response
149
150
  """
150
151
 
151
152
  def __init__(self, identifier: str, labels: List[str],
152
- properties: Dict[str, Any]):
153
+ properties: Dict[str, Any], intermediate = False):
153
154
  self.identifier = identifier
154
155
  self.labels = labels
155
156
  self.key_property_names = []
156
157
  self.properties = properties
158
+ self.intermediate = intermediate
157
159
 
158
160
  def __repr__(self):
159
161
  return (f"Node(identifier={self.identifier}, "
@@ -165,9 +167,20 @@ class Node:
165
167
  "identifier": self.identifier,
166
168
  "labels": self.labels,
167
169
  "properties": self.properties,
168
- "key_property_names": self.key_property_names
170
+ "key_property_names": self.key_property_names,
171
+ "intermediate": self.intermediate
169
172
  }
170
173
 
174
+ @staticmethod
175
+ def make_intermediate(identifier: str):
176
+ """Create a Node instance in the absence of a node from the query response"""
177
+ return Node(
178
+ identifier=identifier,
179
+ labels=["Intermediate"],
180
+ properties={"note": "This node represents a referenced entity that wasn't returned in the query results."},
181
+ intermediate=True
182
+ )
183
+
171
184
  @classmethod
172
185
  def from_json(cls, data: Dict[str, Any]) -> Node:
173
186
  """Create a Node instance from a JSON object."""