spanner-graph-notebook 1.1.2__tar.gz → 1.1.3__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.
- {spanner_graph_notebook-1.1.2/spanner_graph_notebook.egg-info → spanner_graph_notebook-1.1.3}/PKG-INFO +1 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/app.js +5 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/graph-server.js +4 -10
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/graph-server.test.ts +17 -13
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/setup.py +1 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3/spanner_graph_notebook.egg-info}/PKG-INFO +1 -1
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_server.py +115 -106
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/magics.py +28 -17
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/graph_server_test.py +53 -36
- spanner_graph_notebook-1.1.3/tests/node_expansion_test.py +165 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/server_test.py +35 -3
- spanner_graph_notebook-1.1.2/tests/node_expansion_test.py +0 -106
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/CONTRIBUTING.md +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/LICENSE +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/MANIFEST.in +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/README.md +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/authentication.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/colab_usage.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/full_viz.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/hero.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/hero_with_properties.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/jupyter-spin-up.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/jupyter.gif +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/load_ext.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/mock_data_result.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/new_notebook.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/notebook_package_load.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/query_graph.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/sample_jupyter.png +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/babel.config.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/jest-puppeteer.config.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/jest.config.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/package.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/models/edge.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/models/graph-object.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/models/node.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/models/schema.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/spanner-config.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/spanner-store.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/visualization/spanner-forcegraph.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/visualization/spanner-menu.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/visualization/spanner-sidebar.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/visualization/spanner-table.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/static/dev.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/static/graph-bg.svg +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/static/index.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/static/test.html +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/jest.setup.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/mock-data.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/serve-content.js +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/models/edge.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/models/graph-object.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/models/node.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/models/schema.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/spanner-config.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/spanner-store.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/visualization/spanner-menu.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/visualization/spanner-sidebar.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/unit/visualization/spanner-table.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/app.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/jest-visual.setup.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/visualization/spanner-forcegraph.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/visualization/spanner-menu.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/visualization/spanner-sidebar.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/visualization/spanner-table.test.ts +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tsconfig.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/sample.ipynb +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/setup.cfg +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graph_notebook.egg-info/SOURCES.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graph_notebook.egg-info/dependency_links.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graph_notebook.egg-info/requires.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graph_notebook.egg-info/top_level.txt +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/conversion.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/database.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/dev_util/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/dev_util/serve_dev.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_entities.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_mock_data.csv +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_mock_schema.json +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_visualization.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/schema_manager.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/__init__.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/conversion_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/database_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/graph_entities_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/magics_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/sample_notebook_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/schema_manager_test.py +0 -0
- {spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/tests/test_notebook.json +0 -0
|
@@ -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
|
|
@@ -109,10 +109,7 @@ class GraphServer {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
const {project, instance, database, graph} = JSON.parse(this.params);
|
|
113
|
-
|
|
114
112
|
const request = {
|
|
115
|
-
project, instance, database, graph,
|
|
116
113
|
uid: node.uid,
|
|
117
114
|
node_labels: node.labels,
|
|
118
115
|
node_properties: validProperties,
|
|
@@ -131,15 +128,12 @@ class GraphServer {
|
|
|
131
128
|
.finally(() => this.isFetching = false);
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
// For non-Colab environment, combine params and request
|
|
135
|
-
const fullRequest = {
|
|
136
|
-
...JSON.parse(this.params),
|
|
137
|
-
...request
|
|
138
|
-
};
|
|
139
|
-
|
|
140
131
|
return fetch(this.buildRoute(this.endpoints.postNodeExpansion), {
|
|
141
132
|
method: 'POST',
|
|
142
|
-
body: JSON.stringify(
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
params: this.params,
|
|
135
|
+
request
|
|
136
|
+
})
|
|
143
137
|
})
|
|
144
138
|
.then(response => {
|
|
145
139
|
if (!response.ok) {
|
|
@@ -148,19 +148,23 @@ describe('GraphServer', () => {
|
|
|
148
148
|
{
|
|
149
149
|
method: 'POST',
|
|
150
150
|
body: JSON.stringify({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
);
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_server.py
RENAMED
|
@@ -42,6 +42,17 @@ PROPERTY_TYPE_MAP = {
|
|
|
42
42
|
'TIMESTAMP': TypeCode.TIMESTAMP
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
class NodePropertyForDataExploration:
|
|
46
|
+
def __init__(self, key: str, value: Union[str, int, float, bool], type: TypeCode):
|
|
47
|
+
self.key = key
|
|
48
|
+
self.value = value
|
|
49
|
+
self.type = type
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EdgeDirection(Enum):
|
|
53
|
+
INCOMING = "INCOMING"
|
|
54
|
+
OUTGOING = "OUTGOING"
|
|
55
|
+
|
|
45
56
|
def validate_property_type(property_type: str) -> TypeCode:
|
|
46
57
|
"""
|
|
47
58
|
Validates and converts a property type string to a Spanner TypeCode.
|
|
@@ -68,26 +79,95 @@ def validate_property_type(property_type: str) -> TypeCode:
|
|
|
68
79
|
|
|
69
80
|
return PROPERTY_TYPE_MAP[property_type]
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
def validate_node_expansion_request(data) -> (list[NodePropertyForDataExploration], EdgeDirection):
|
|
83
|
+
required_fields = ["project", "instance", "database", "graph", "uid", "node_labels", "direction"]
|
|
84
|
+
missing_fields = [field for field in required_fields if data.get(field) is None]
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
if missing_fields:
|
|
87
|
+
raise ValueError(f"Missing required fields: {', '.join(missing_fields)}")
|
|
88
|
+
|
|
89
|
+
node_labels = data.get("node_labels")
|
|
90
|
+
node_properties = data.get("node_properties", [])
|
|
91
|
+
|
|
92
|
+
# Validate node_labels
|
|
93
|
+
if not isinstance(node_labels, list):
|
|
94
|
+
raise ValueError("node_labels must be an array")
|
|
95
|
+
|
|
96
|
+
for label in node_labels:
|
|
97
|
+
if not isinstance(label, str):
|
|
98
|
+
raise ValueError("Each node label must be a string")
|
|
99
|
+
|
|
100
|
+
# Validate node_properties
|
|
101
|
+
if not isinstance(node_properties, list):
|
|
102
|
+
raise ValueError("node_properties must be an array")
|
|
103
|
+
|
|
104
|
+
validated_properties: list[NodePropertyForDataExploration] = []
|
|
105
|
+
for idx, prop in enumerate(node_properties):
|
|
106
|
+
if not isinstance(prop, dict):
|
|
107
|
+
raise ValueError(f"Property at index {idx} must be an object")
|
|
108
|
+
|
|
109
|
+
if not all(field in prop for field in ["key", "value", "type"]):
|
|
110
|
+
raise ValueError(f"Property at index {idx} is missing required fields (key, value, type)")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
prop_type_str = prop["type"]
|
|
114
|
+
if isinstance(prop_type_str, str):
|
|
115
|
+
prop_type = validate_property_type(prop_type_str)
|
|
116
|
+
|
|
117
|
+
value = prop["value"]
|
|
118
|
+
if prop_type in (TypeCode.INT64, TypeCode.NUMERIC):
|
|
119
|
+
if not (isinstance(value, int) or (isinstance(value, str) and value.isdigit())):
|
|
120
|
+
raise ValueError(f"Property '{prop['key']}' value must be a number for type {prop_type_str}")
|
|
121
|
+
elif prop_type in (TypeCode.FLOAT32, TypeCode.FLOAT64):
|
|
122
|
+
try:
|
|
123
|
+
float(value)
|
|
124
|
+
except (ValueError, TypeError):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Property '{prop['key']}' value must be a valid float for type {prop_type_str}")
|
|
127
|
+
elif prop_type == TypeCode.BOOL:
|
|
128
|
+
if not isinstance(value, bool) and not (isinstance(value, str) and value.lower() in ["true", "false"]):
|
|
129
|
+
raise ValueError(f"Property '{prop['key']}' value must be a boolean for type {prop_type_str}")
|
|
130
|
+
|
|
131
|
+
validated_properties.append(NodePropertyForDataExploration(
|
|
132
|
+
key=prop["key"],
|
|
133
|
+
value=prop["value"],
|
|
134
|
+
type=prop_type
|
|
135
|
+
))
|
|
136
|
+
else:
|
|
137
|
+
raise ValueError(f"Property type at index {idx} must be a string")
|
|
138
|
+
except ValueError as e:
|
|
139
|
+
raise ValueError(f"Invalid property type in property at index {idx}: {str(e)}")
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
direction = EdgeDirection(data.get("direction"))
|
|
143
|
+
except ValueError:
|
|
144
|
+
raise ValueError(f"Invalid direction: must be INCOMING or OUTGOING, got \"{data.get('direction')}\"")
|
|
145
|
+
|
|
146
|
+
return validated_properties, direction
|
|
80
147
|
|
|
81
148
|
def execute_node_expansion(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
149
|
+
params_str: str,
|
|
150
|
+
request: dict) -> dict:
|
|
151
|
+
"""Execute a node expansion query to find connected nodes and edges.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
params_str: A JSON string containing connection parameters (project, instance, database, graph, mock).
|
|
155
|
+
request: A dictionary containing node expansion request details (uid, node_labels, node_properties, direction, edge_label).
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
dict: A dictionary containing the query response with nodes and edges.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
params = json.loads(params_str)
|
|
162
|
+
node_properties, direction = validate_node_expansion_request(params | request)
|
|
163
|
+
|
|
164
|
+
project = params.get("project")
|
|
165
|
+
instance = params.get("instance")
|
|
166
|
+
database = params.get("database")
|
|
167
|
+
graph = params.get("graph")
|
|
168
|
+
uid = request.get("uid")
|
|
169
|
+
node_labels = request.get("node_labels")
|
|
170
|
+
edge_label = request.get("edge_label")
|
|
91
171
|
|
|
92
172
|
edge = "e" if not edge_label else f"e:{edge_label}"
|
|
93
173
|
|
|
@@ -232,6 +312,8 @@ class GraphServerHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
232
312
|
self.do_json_response(data)
|
|
233
313
|
|
|
234
314
|
def do_error_response(self, message):
|
|
315
|
+
if isinstance(message, Exception):
|
|
316
|
+
message = str(message)
|
|
235
317
|
self.do_json_response({'error': message})
|
|
236
318
|
|
|
237
319
|
def parse_post_data(self):
|
|
@@ -259,99 +341,26 @@ class GraphServerHandler(http.server.SimpleHTTPRequestHandler):
|
|
|
259
341
|
self.do_data_response(response)
|
|
260
342
|
|
|
261
343
|
def handle_post_node_expansion(self):
|
|
262
|
-
|
|
263
|
-
required_fields = ["project", "instance", "database", "graph", "uid", "node_labels", "direction"]
|
|
264
|
-
missing_fields = [field for field in required_fields if data.get(field) is None]
|
|
265
|
-
|
|
266
|
-
if missing_fields:
|
|
267
|
-
self.do_error_response(f"Missing required fields: {', '.join(missing_fields)}")
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
project = data.get("project")
|
|
271
|
-
instance = data.get("instance")
|
|
272
|
-
database = data.get("database")
|
|
273
|
-
graph = data.get("graph")
|
|
274
|
-
uid = data.get("uid")
|
|
275
|
-
node_labels = data.get("node_labels")
|
|
276
|
-
node_properties = data.get("node_properties", [])
|
|
277
|
-
edge_label = data.get("edge_label")
|
|
278
|
-
|
|
279
|
-
# Validate node_labels
|
|
280
|
-
if not isinstance(node_labels, list):
|
|
281
|
-
self.do_error_response("node_labels must be an array")
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
for label in node_labels:
|
|
285
|
-
if not isinstance(label, str):
|
|
286
|
-
self.do_error_response("Each node label must be a string")
|
|
287
|
-
return
|
|
288
|
-
|
|
289
|
-
# Validate node_properties
|
|
290
|
-
if not isinstance(node_properties, list):
|
|
291
|
-
self.do_error_response("node_properties must be an array")
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
validated_properties: list[NodeProperty] = []
|
|
295
|
-
for idx, prop in enumerate(node_properties):
|
|
296
|
-
if not isinstance(prop, dict):
|
|
297
|
-
self.do_error_response(f"Property at index {idx} must be an object")
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
if not all(field in prop for field in ["key", "value", "type"]):
|
|
301
|
-
self.do_error_response(f"Property at index {idx} is missing required fields (key, value, type)")
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
try:
|
|
305
|
-
prop_type_str = prop["type"]
|
|
306
|
-
if isinstance(prop_type_str, str):
|
|
307
|
-
prop_type = validate_property_type(prop_type_str)
|
|
308
|
-
|
|
309
|
-
value = prop["value"]
|
|
310
|
-
if prop_type in (TypeCode.INT64, TypeCode.NUMERIC):
|
|
311
|
-
if not (isinstance(value, int) or (isinstance(value, str) and value.isdigit())):
|
|
312
|
-
self.do_error_response(f"Property '{prop['key']}' value must be a number for type {prop_type_str}")
|
|
313
|
-
return
|
|
314
|
-
elif prop_type in (TypeCode.FLOAT32, TypeCode.FLOAT64):
|
|
315
|
-
try:
|
|
316
|
-
float(value)
|
|
317
|
-
except (ValueError, TypeError):
|
|
318
|
-
self.do_error_response(f"Property '{prop['key']}' value must be a valid float for type {prop_type_str}")
|
|
319
|
-
return
|
|
320
|
-
elif prop_type == TypeCode.BOOL:
|
|
321
|
-
if not isinstance(value, bool) and not (isinstance(value, str) and value.lower() in ["true", "false"]):
|
|
322
|
-
self.do_error_response(f"Property '{prop['key']}' value must be a boolean for type {prop_type_str}")
|
|
323
|
-
return
|
|
324
|
-
|
|
325
|
-
validated_properties.append(NodeProperty(
|
|
326
|
-
key=prop["key"],
|
|
327
|
-
value=prop["value"],
|
|
328
|
-
type=prop_type
|
|
329
|
-
))
|
|
330
|
-
else:
|
|
331
|
-
self.do_error_response(f"Property type at index {idx} must be a string")
|
|
332
|
-
return
|
|
333
|
-
except ValueError as e:
|
|
334
|
-
self.do_error_response(f"Invalid property type in property at index {idx}: {str(e)}")
|
|
335
|
-
return
|
|
344
|
+
"""Handle POST requests for node expansion.
|
|
336
345
|
|
|
346
|
+
Expects a JSON payload with:
|
|
347
|
+
- params: A JSON string containing connection parameters (project, instance, database, graph)
|
|
348
|
+
- request: A dictionary with node details (uid, node_labels, node_properties, direction, edge_label)
|
|
349
|
+
"""
|
|
337
350
|
try:
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
data = self.parse_post_data()
|
|
352
|
+
|
|
353
|
+
# Execute node expansion with:
|
|
354
|
+
# - params_str: JSON string with connection parameters (project, instance, database, graph)
|
|
355
|
+
# - request: Dict with node details (uid, node_labels, node_properties, direction, edge_label)
|
|
356
|
+
self.do_data_response(execute_node_expansion(
|
|
357
|
+
params_str=data.get("params"),
|
|
358
|
+
request=data.get("request")
|
|
359
|
+
))
|
|
360
|
+
except BaseException as e:
|
|
361
|
+
self.do_error_response(e)
|
|
341
362
|
return
|
|
342
363
|
|
|
343
|
-
self.do_data_response(execute_node_expansion(
|
|
344
|
-
project=project,
|
|
345
|
-
instance=instance,
|
|
346
|
-
database=database,
|
|
347
|
-
node_labels=node_labels,
|
|
348
|
-
node_properties=validated_properties,
|
|
349
|
-
graph=graph,
|
|
350
|
-
uid=uid,
|
|
351
|
-
direction=direction,
|
|
352
|
-
edge_label=edge_label,
|
|
353
|
-
))
|
|
354
|
-
|
|
355
364
|
def do_GET(self):
|
|
356
365
|
if self.path == GraphServer.endpoints["get_ping"]:
|
|
357
366
|
self.handle_get_ping()
|
|
@@ -35,8 +35,8 @@ from jinja2 import Template
|
|
|
35
35
|
|
|
36
36
|
from spanner_graphs.database import get_database_instance
|
|
37
37
|
from spanner_graphs.graph_server import (
|
|
38
|
-
GraphServer, execute_query, execute_node_expansion,
|
|
39
|
-
|
|
38
|
+
GraphServer, execute_query, execute_node_expansion,
|
|
39
|
+
validate_node_expansion_request
|
|
40
40
|
)
|
|
41
41
|
from spanner_graphs.graph_visualization import generate_visualization_html
|
|
42
42
|
|
|
@@ -92,21 +92,32 @@ def receive_query_request(query: str, params: str):
|
|
|
92
92
|
query=query,
|
|
93
93
|
mock=params_dict["mock"]))
|
|
94
94
|
|
|
95
|
-
def receive_node_expansion_request(request: dict,
|
|
96
|
-
"""Handle node expansion requests in Google Colab environment
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
def receive_node_expansion_request(request: dict, params_str: str):
|
|
96
|
+
"""Handle node expansion requests in Google Colab environment
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
request: A dictionary containing node expansion details including:
|
|
100
|
+
- uid: str - Unique identifier of the node to expand
|
|
101
|
+
- node_labels: List[str] - Labels of the node
|
|
102
|
+
- node_properties: List[Dict] - Properties of the node with key, value, and type
|
|
103
|
+
- direction: str - Direction of expansion ("INCOMING" or "OUTGOING")
|
|
104
|
+
- edge_label: Optional[str] - Label of edges to filter by
|
|
105
|
+
params_str: A JSON string containing connection parameters:
|
|
106
|
+
- project: str - GCP project ID
|
|
107
|
+
- instance: str - Spanner instance ID
|
|
108
|
+
- database: str - Spanner database ID
|
|
109
|
+
- graph: str - Graph name
|
|
110
|
+
- mock: bool - Whether to use mock data
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
JSON: A JSON-serialized response containing either:
|
|
114
|
+
- The query results with nodes and edges
|
|
115
|
+
- An error message if the request failed
|
|
116
|
+
"""
|
|
117
|
+
try:
|
|
118
|
+
return JSON(execute_node_expansion(params_str, request))
|
|
119
|
+
except BaseException as e:
|
|
120
|
+
return JSON({"error": e})
|
|
110
121
|
|
|
111
122
|
@magics_class
|
|
112
123
|
class NetworkVisualizationMagics(Magics):
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from unittest.mock import patch, MagicMock
|
|
3
3
|
from google.cloud.spanner_v1 import TypeCode
|
|
4
|
+
import json
|
|
4
5
|
|
|
5
6
|
from spanner_graphs.graph_server import (
|
|
6
7
|
validate_property_type,
|
|
7
8
|
execute_node_expansion,
|
|
8
|
-
EdgeDirection,
|
|
9
|
-
PROPERTY_TYPE_MAP
|
|
10
9
|
)
|
|
11
10
|
|
|
12
11
|
class TestPropertyTypeHandling(unittest.TestCase):
|
|
@@ -64,36 +63,42 @@ class TestPropertyTypeHandling(unittest.TestCase):
|
|
|
64
63
|
|
|
65
64
|
test_cases = [
|
|
66
65
|
# Numeric types (unquoted)
|
|
67
|
-
(
|
|
68
|
-
(
|
|
69
|
-
(
|
|
70
|
-
(
|
|
66
|
+
("INT64", "123", "123"),
|
|
67
|
+
("NUMERIC", "123", "123"),
|
|
68
|
+
("FLOAT32", "123.45", "123.45"),
|
|
69
|
+
("FLOAT64", "123.45", "123.45"),
|
|
71
70
|
# Boolean (unquoted)
|
|
72
|
-
(
|
|
71
|
+
("BOOL", "true", "true"),
|
|
73
72
|
# String types (quoted)
|
|
74
|
-
(
|
|
75
|
-
(
|
|
76
|
-
(
|
|
77
|
-
(
|
|
78
|
-
(
|
|
73
|
+
("STRING", "hello", '"hello"'),
|
|
74
|
+
("DATE", "2024-03-14", '"2024-03-14"'),
|
|
75
|
+
("TIMESTAMP", "2024-03-14T12:00:00Z", '"2024-03-14T12:00:00Z"'),
|
|
76
|
+
("BYTES", "base64data", '"base64data"'),
|
|
77
|
+
("ENUM", "ENUM_VALUE", '"ENUM_VALUE"'),
|
|
79
78
|
]
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
params = json.dumps({
|
|
82
81
|
"project": "test-project",
|
|
83
82
|
"instance": "test-instance",
|
|
84
83
|
"database": "test-database",
|
|
85
84
|
"graph": "test-graph",
|
|
86
|
-
|
|
87
|
-
"node_key_property_name": "test_property",
|
|
88
|
-
"direction": EdgeDirection.OUTGOING,
|
|
89
|
-
}
|
|
85
|
+
})
|
|
90
86
|
|
|
91
|
-
for
|
|
92
|
-
with self.subTest(type=
|
|
87
|
+
for type_str, value, expected_format in test_cases:
|
|
88
|
+
with self.subTest(type=type_str, value=value):
|
|
89
|
+
# Create a property dictionary
|
|
90
|
+
prop_dict = {"key": "test_property", "value": value, "type": type_str}
|
|
91
|
+
|
|
92
|
+
request = {
|
|
93
|
+
"uid": "test-uid",
|
|
94
|
+
"node_labels": ["Person"],
|
|
95
|
+
"node_properties": [prop_dict],
|
|
96
|
+
"direction": "OUTGOING"
|
|
97
|
+
}
|
|
98
|
+
|
|
93
99
|
execute_node_expansion(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
property_type=type_code
|
|
100
|
+
params_str=params,
|
|
101
|
+
request=request
|
|
97
102
|
)
|
|
98
103
|
|
|
99
104
|
# Extract the actual formatted value from the query
|
|
@@ -102,32 +107,44 @@ class TestPropertyTypeHandling(unittest.TestCase):
|
|
|
102
107
|
|
|
103
108
|
# Find the WHERE clause in the query and extract the value
|
|
104
109
|
where_line = [line for line in query.split('\n') if 'WHERE' in line][0]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
expected_pattern = f"n.test_property={expected_format}"
|
|
111
|
+
self.assertIn(expected_pattern, where_line,
|
|
112
|
+
f"Expected property value pattern {expected_pattern} not found in WHERE clause for type {type_str}")
|
|
113
|
+
|
|
108
114
|
@patch('spanner_graphs.graph_server.execute_query')
|
|
109
115
|
def test_property_value_formatting_no_type(self, mock_execute_query):
|
|
110
116
|
"""Test that property values are quoted when no type is provided."""
|
|
111
117
|
mock_execute_query.return_value = {"response": {"nodes": [], "edges": []}}
|
|
112
118
|
|
|
119
|
+
# Create a property dictionary with string type (since null type is not allowed)
|
|
120
|
+
prop_dict = {"key": "test_property", "value": "test_value", "type": "STRING"}
|
|
121
|
+
|
|
122
|
+
params = json.dumps({
|
|
123
|
+
"project": "test-project",
|
|
124
|
+
"instance": "test-instance",
|
|
125
|
+
"database": "test-database",
|
|
126
|
+
"graph": "test-graph",
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
request = {
|
|
130
|
+
"uid": "test-uid",
|
|
131
|
+
"node_labels": ["Person"],
|
|
132
|
+
"node_properties": [prop_dict],
|
|
133
|
+
"direction": "OUTGOING"
|
|
134
|
+
}
|
|
135
|
+
|
|
113
136
|
execute_node_expansion(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
database="test-database",
|
|
117
|
-
graph="test-graph",
|
|
118
|
-
uid="test-uid",
|
|
119
|
-
node_key_property_name="test_property",
|
|
120
|
-
node_key_property_value="test_value",
|
|
121
|
-
direction=EdgeDirection.OUTGOING,
|
|
122
|
-
property_type=None
|
|
137
|
+
params_str=params,
|
|
138
|
+
request=request
|
|
123
139
|
)
|
|
124
140
|
|
|
125
141
|
# Extract the actual formatted value from the query
|
|
126
142
|
last_call = mock_execute_query.call_args[0]
|
|
127
143
|
query = last_call[3]
|
|
128
144
|
where_line = [line for line in query.split('\n') if 'WHERE' in line][0]
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
expected_pattern = 'n.test_property="test_value"'
|
|
146
|
+
self.assertIn(expected_pattern, where_line,
|
|
147
|
+
"Property value should be quoted when string type is provided")
|
|
131
148
|
|
|
132
149
|
if __name__ == '__main__':
|
|
133
150
|
unittest.main()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch, MagicMock
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from spanner_graphs.magics import receive_node_expansion_request
|
|
6
|
+
from spanner_graphs.graph_server import EdgeDirection, TypeCode
|
|
7
|
+
|
|
8
|
+
class TestNodeExpansion(unittest.TestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
self.sample_request = {
|
|
11
|
+
"uid": "node-123",
|
|
12
|
+
"node_key_property_name": "id",
|
|
13
|
+
"node_key_property_value": "123",
|
|
14
|
+
"node_key_property_type": "INT64",
|
|
15
|
+
"direction": "OUTGOING",
|
|
16
|
+
"edge_label": "CONNECTS_TO"
|
|
17
|
+
}
|
|
18
|
+
self.sample_params = json.dumps({
|
|
19
|
+
"project": "test-project",
|
|
20
|
+
"instance": "test-instance",
|
|
21
|
+
"database": "test-database",
|
|
22
|
+
"graph": "test_graph",
|
|
23
|
+
"mock": False
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
@patch('spanner_graphs.magics.validate_node_expansion_request')
|
|
27
|
+
@patch('spanner_graphs.magics.execute_node_expansion')
|
|
28
|
+
def test_receive_node_expansion_request(self, mock_execute, mock_validate):
|
|
29
|
+
"""Test that the receive_node_expansion_request function correctly processes requests."""
|
|
30
|
+
# Setup mock return values
|
|
31
|
+
mock_validate.return_value = ([], EdgeDirection.OUTGOING)
|
|
32
|
+
mock_execute.return_value = {
|
|
33
|
+
"response": {
|
|
34
|
+
"nodes": [],
|
|
35
|
+
"edges": []
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Create request and params objects
|
|
40
|
+
request = {
|
|
41
|
+
"uid": "node-123",
|
|
42
|
+
"node_labels": ["Person"],
|
|
43
|
+
"node_properties": [
|
|
44
|
+
{"key": "id", "value": "123", "type": "INT64"}
|
|
45
|
+
],
|
|
46
|
+
"direction": "OUTGOING",
|
|
47
|
+
"edge_label": "CONNECTS_TO"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
params = json.dumps({
|
|
51
|
+
"project": "test-project",
|
|
52
|
+
"instance": "test-instance",
|
|
53
|
+
"database": "test-database",
|
|
54
|
+
"graph": "test_graph",
|
|
55
|
+
"mock": False
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# Call the function
|
|
59
|
+
result = receive_node_expansion_request(request, params)
|
|
60
|
+
|
|
61
|
+
# Verify execute_node_expansion was called with correct parameters
|
|
62
|
+
mock_execute.assert_called_once_with(params, request)
|
|
63
|
+
|
|
64
|
+
# Verify the result is wrapped in JSON
|
|
65
|
+
self.assertEqual(result.data, mock_execute.return_value)
|
|
66
|
+
|
|
67
|
+
@patch('spanner_graphs.magics.validate_node_expansion_request')
|
|
68
|
+
@patch('spanner_graphs.magics.execute_node_expansion')
|
|
69
|
+
def test_receive_node_expansion_request_without_edge_label(self, mock_execute, mock_validate):
|
|
70
|
+
"""Test that the receive_node_expansion_request function correctly handles missing edge_label."""
|
|
71
|
+
# Setup mock return values
|
|
72
|
+
mock_validate.return_value = ([], EdgeDirection.OUTGOING)
|
|
73
|
+
mock_execute.return_value = {
|
|
74
|
+
"response": {
|
|
75
|
+
"nodes": [],
|
|
76
|
+
"edges": []
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Create request without edge_label and params objects
|
|
81
|
+
request = {
|
|
82
|
+
"uid": "node-123",
|
|
83
|
+
"node_labels": ["Person"],
|
|
84
|
+
"node_properties": [
|
|
85
|
+
{"key": "id", "value": "123", "type": "INT64"}
|
|
86
|
+
],
|
|
87
|
+
"direction": "OUTGOING"
|
|
88
|
+
# No edge_label
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
params = json.dumps({
|
|
92
|
+
"project": "test-project",
|
|
93
|
+
"instance": "test-instance",
|
|
94
|
+
"database": "test-database",
|
|
95
|
+
"graph": "test_graph",
|
|
96
|
+
"mock": False
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# Call the function
|
|
100
|
+
result = receive_node_expansion_request(request, params)
|
|
101
|
+
|
|
102
|
+
# Verify execute_node_expansion was called with correct parameters
|
|
103
|
+
mock_execute.assert_called_once_with(params, request)
|
|
104
|
+
|
|
105
|
+
# Verify the result is wrapped in JSON
|
|
106
|
+
self.assertEqual(result.data, mock_execute.return_value)
|
|
107
|
+
|
|
108
|
+
@patch('spanner_graphs.magics.validate_node_expansion_request')
|
|
109
|
+
def test_invalid_property_type(self, mock_validate):
|
|
110
|
+
"""Test that invalid property types are correctly caught."""
|
|
111
|
+
# Set up the mock to raise a ValueError when called with invalid data
|
|
112
|
+
mock_validate.side_effect = ValueError("Invalid property type")
|
|
113
|
+
|
|
114
|
+
# Create a request with invalid property type
|
|
115
|
+
request = {
|
|
116
|
+
"uid": "node-123",
|
|
117
|
+
"node_labels": ["Person"],
|
|
118
|
+
"node_properties": [
|
|
119
|
+
{"key": "id", "value": "123", "type": "INVALID_TYPE"}
|
|
120
|
+
],
|
|
121
|
+
"direction": "OUTGOING"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
params = json.dumps({
|
|
125
|
+
"project": "test-project",
|
|
126
|
+
"instance": "test-instance",
|
|
127
|
+
"database": "test-database",
|
|
128
|
+
"graph": "test_graph",
|
|
129
|
+
"mock": False
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
# Call the function and verify it returns an error response
|
|
133
|
+
result = receive_node_expansion_request(request, params)
|
|
134
|
+
self.assertIn("error", result.data)
|
|
135
|
+
|
|
136
|
+
@patch('spanner_graphs.magics.validate_node_expansion_request')
|
|
137
|
+
def test_invalid_direction(self, mock_validate):
|
|
138
|
+
"""Test that invalid directions are correctly caught."""
|
|
139
|
+
# Set up the mock to raise a ValueError when called with invalid data
|
|
140
|
+
mock_validate.side_effect = ValueError("Invalid direction")
|
|
141
|
+
|
|
142
|
+
# Create a request with invalid direction
|
|
143
|
+
request = {
|
|
144
|
+
"uid": "node-123",
|
|
145
|
+
"node_labels": ["Person"],
|
|
146
|
+
"node_properties": [
|
|
147
|
+
{"key": "id", "value": "123", "type": "INT64"}
|
|
148
|
+
],
|
|
149
|
+
"direction": "INVALID_DIRECTION"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
params = json.dumps({
|
|
153
|
+
"project": "test-project",
|
|
154
|
+
"instance": "test-instance",
|
|
155
|
+
"database": "test-database",
|
|
156
|
+
"graph": "test_graph",
|
|
157
|
+
"mock": False
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
# Call the function and verify it returns an error response
|
|
161
|
+
result = receive_node_expansion_request(request, params)
|
|
162
|
+
self.assertIn("error", result.data)
|
|
163
|
+
|
|
164
|
+
if __name__ == '__main__':
|
|
165
|
+
unittest.main()
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import unittest
|
|
16
16
|
import requests
|
|
17
|
+
import json
|
|
17
18
|
from spanner_graphs.graph_server import GraphServer
|
|
18
19
|
|
|
19
20
|
class TestSpannerServer(unittest.TestCase):
|
|
@@ -39,13 +40,17 @@ class TestSpannerServer(unittest.TestCase):
|
|
|
39
40
|
# Build the request URL
|
|
40
41
|
route = GraphServer.build_route(GraphServer.endpoints["post_query"])
|
|
41
42
|
|
|
42
|
-
# Create request data
|
|
43
|
-
|
|
43
|
+
# Create request data with the new structure
|
|
44
|
+
params = json.dumps({
|
|
44
45
|
"project": "test-project",
|
|
45
46
|
"instance": "test-instance",
|
|
46
47
|
"database": "test-database",
|
|
47
|
-
"query": "GRAPH TestGraph MATCH (n) RETURN n",
|
|
48
48
|
"mock": True
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
request_data = {
|
|
52
|
+
"params": params,
|
|
53
|
+
"query": "GRAPH TestGraph MATCH (n) RETURN n"
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
# Send POST request
|
|
@@ -82,5 +87,32 @@ class TestSpannerServer(unittest.TestCase):
|
|
|
82
87
|
self.assertIn("source_node_identifier", edge)
|
|
83
88
|
self.assertIn("destination_node_identifier", edge)
|
|
84
89
|
|
|
90
|
+
def test_node_expansion_error_handling(self):
|
|
91
|
+
"""Test that errors in node expansion are properly handled and returned."""
|
|
92
|
+
# Build the request URL
|
|
93
|
+
route = GraphServer.build_route(GraphServer.endpoints["post_node_expansion"])
|
|
94
|
+
|
|
95
|
+
# Create request data with invalid fields to trigger validation error
|
|
96
|
+
request_data = {
|
|
97
|
+
"project": "test-project",
|
|
98
|
+
"instance": "test-instance",
|
|
99
|
+
"database": "test-database",
|
|
100
|
+
"graph": "test-graph",
|
|
101
|
+
"uid": "test-uid",
|
|
102
|
+
# Missing required node_labels field
|
|
103
|
+
"direction": "INVALID_DIRECTION" # Invalid direction
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Send POST request
|
|
107
|
+
response = requests.post(route, json=request_data)
|
|
108
|
+
|
|
109
|
+
# Verify response
|
|
110
|
+
self.assertEqual(response.status_code, 200) # Server still returns 200 but with error data
|
|
111
|
+
response_data = response.json()
|
|
112
|
+
|
|
113
|
+
# Check error presence
|
|
114
|
+
self.assertIn("error", response_data)
|
|
115
|
+
self.assertIsNotNone(response_data["error"])
|
|
116
|
+
|
|
85
117
|
if __name__ == '__main__':
|
|
86
118
|
unittest.main()
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import patch, MagicMock
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
from spanner_graphs.magics import receive_node_expansion_request
|
|
6
|
-
from spanner_graphs.graph_server import EdgeDirection, TypeCode
|
|
7
|
-
|
|
8
|
-
class TestNodeExpansion(unittest.TestCase):
|
|
9
|
-
def setUp(self):
|
|
10
|
-
self.sample_request = {
|
|
11
|
-
"uid": "node-123",
|
|
12
|
-
"node_key_property_name": "id",
|
|
13
|
-
"node_key_property_value": "123",
|
|
14
|
-
"node_key_property_type": "INT64",
|
|
15
|
-
"direction": "OUTGOING",
|
|
16
|
-
"edge_label": "CONNECTS_TO"
|
|
17
|
-
}
|
|
18
|
-
self.sample_params = json.dumps({
|
|
19
|
-
"project": "test-project",
|
|
20
|
-
"instance": "test-instance",
|
|
21
|
-
"database": "test-database",
|
|
22
|
-
"graph": "test_graph",
|
|
23
|
-
"mock": False
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
@patch('spanner_graphs.magics.execute_node_expansion')
|
|
27
|
-
def test_receive_node_expansion_request(self, mock_execute):
|
|
28
|
-
# Setup mock return value
|
|
29
|
-
mock_execute.return_value = {
|
|
30
|
-
"response": {
|
|
31
|
-
"nodes": [],
|
|
32
|
-
"edges": []
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
# Call the function
|
|
37
|
-
result = receive_node_expansion_request(self.sample_request, self.sample_params)
|
|
38
|
-
|
|
39
|
-
# Verify execute_node_expansion was called with correct parameters
|
|
40
|
-
mock_execute.assert_called_once_with(
|
|
41
|
-
project="test-project",
|
|
42
|
-
instance="test-instance",
|
|
43
|
-
database="test-database",
|
|
44
|
-
graph="test_graph",
|
|
45
|
-
uid="node-123",
|
|
46
|
-
node_key_property_name="id",
|
|
47
|
-
node_key_property_value="123",
|
|
48
|
-
direction=EdgeDirection.OUTGOING,
|
|
49
|
-
edge_label="CONNECTS_TO",
|
|
50
|
-
property_type=TypeCode.INT64
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Verify the result is wrapped in JSON
|
|
54
|
-
self.assertEqual(result.data, mock_execute.return_value)
|
|
55
|
-
|
|
56
|
-
@patch('spanner_graphs.magics.execute_node_expansion')
|
|
57
|
-
def test_receive_node_expansion_request_without_edge_label(self, mock_execute):
|
|
58
|
-
# Remove edge_label from request
|
|
59
|
-
request = dict(self.sample_request)
|
|
60
|
-
del request["edge_label"]
|
|
61
|
-
|
|
62
|
-
# Setup mock return value
|
|
63
|
-
mock_execute.return_value = {
|
|
64
|
-
"response": {
|
|
65
|
-
"nodes": [],
|
|
66
|
-
"edges": []
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
# Call the function
|
|
71
|
-
result = receive_node_expansion_request(request, self.sample_params)
|
|
72
|
-
|
|
73
|
-
# Verify execute_node_expansion was called with correct parameters
|
|
74
|
-
mock_execute.assert_called_once_with(
|
|
75
|
-
project="test-project",
|
|
76
|
-
instance="test-instance",
|
|
77
|
-
database="test-database",
|
|
78
|
-
graph="test_graph",
|
|
79
|
-
uid="node-123",
|
|
80
|
-
node_key_property_name="id",
|
|
81
|
-
node_key_property_value="123",
|
|
82
|
-
direction=EdgeDirection.OUTGOING,
|
|
83
|
-
edge_label=None,
|
|
84
|
-
property_type=TypeCode.INT64
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
def test_invalid_property_type(self):
|
|
88
|
-
# Modify request to have invalid property type
|
|
89
|
-
request = dict(self.sample_request)
|
|
90
|
-
request["node_key_property_type"] = "INVALID_TYPE"
|
|
91
|
-
|
|
92
|
-
# Verify it raises ValueError
|
|
93
|
-
with self.assertRaises(ValueError):
|
|
94
|
-
receive_node_expansion_request(request, self.sample_params)
|
|
95
|
-
|
|
96
|
-
def test_invalid_direction(self):
|
|
97
|
-
# Modify request to have invalid direction
|
|
98
|
-
request = dict(self.sample_request)
|
|
99
|
-
request["direction"] = "INVALID_DIRECTION"
|
|
100
|
-
|
|
101
|
-
# Verify it raises ValueError
|
|
102
|
-
with self.assertRaises(ValueError):
|
|
103
|
-
receive_node_expansion_request(request, self.sample_params)
|
|
104
|
-
|
|
105
|
-
if __name__ == '__main__':
|
|
106
|
-
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/hero_with_properties.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/assets/notebook_package_load.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/jest-puppeteer.config.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/models/graph-object.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/src/spanner-config.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/serve-content.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/frontend/tests/visual/app.test.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/dev_util/__init__.py
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/dev_util/serve_dev.py
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_entities.py
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_mock_data.csv
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_mock_schema.json
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/graph_visualization.py
RENAMED
|
File without changes
|
{spanner_graph_notebook-1.1.2 → spanner_graph_notebook-1.1.3}/spanner_graphs/schema_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|