Skip to content

Commit 7f02355

Browse files
authored
Experimental TinkerPop 4.0 support (#704)
* Initial TinkerPop 4.0 support * update changelog
1 parent cf196f3 commit 7f02355

File tree

7 files changed

+129
-48
lines changed

7 files changed

+129
-48
lines changed

ChangeLog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Starting with v1.31.6, this file will contain a record of major features and upd
44

55
## Upcoming
66

7-
- Add documentation for group keys in `%%graph_notebook_vis_options` ([Link to PR](https://github.com/aws/graph-notebook/pull/703))
7+
- Added experimental TinkerPop 4.0 support ([Link to PR](https://github.com/aws/graph-notebook/pull/704))
8+
- Added documentation for group keys in `%%graph_notebook_vis_options` ([Link to PR](https://github.com/aws/graph-notebook/pull/703))
89
- Enabled `--query-timeout` on `%%oc explain` for Neptune Analytics ([Link to PR](https://github.com/aws/graph-notebook/pull/701))
910

1011
## Release 4.6.0 (September 19, 2024)

src/graph_notebook/configuration/generate_config.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
GRAPHBINARYV1, GREMLIN_SERIALIZERS_HTTP, GREMLIN_SERIALIZERS_WS,
1818
GREMLIN_SERIALIZERS_ALL, NEPTUNE_GREMLIN_SERIALIZERS_HTTP,
1919
DEFAULT_GREMLIN_WS_SERIALIZER, DEFAULT_GREMLIN_HTTP_SERIALIZER,
20+
NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT, DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT,
2021
NEPTUNE_DB_SERVICE_NAME, NEPTUNE_ANALYTICS_SERVICE_NAME,
2122
normalize_service_name, normalize_protocol_name,
2223
normalize_serializer_class_name)
@@ -93,16 +94,16 @@ def __init__(self, traversal_source: str = '', username: str = '', password: str
9394
print(f"Enforcing HTTP protocol.")
9495
connection_protocol = DEFAULT_HTTP_PROTOCOL
9596
# temporary restriction until GraphSON-typed and GraphBinary results are supported
96-
if message_serializer not in NEPTUNE_GREMLIN_SERIALIZERS_HTTP:
97+
if message_serializer not in NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT:
9798
if message_serializer not in GREMLIN_SERIALIZERS_ALL:
9899
if invalid_serializer_input:
99-
print(f"Invalid serializer specified, defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER}. "
100-
f"Valid serializers: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP}")
100+
print(f"Invalid serializer specified, defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT}. "
101+
f"Valid serializers: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT}")
101102
else:
102103
print(f"{message_serializer} is not currently supported for HTTP connections, "
103-
f"defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER}. "
104-
f"Please use one of: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP}")
105-
message_serializer = DEFAULT_GREMLIN_HTTP_SERIALIZER
104+
f"defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT}. "
105+
f"Please use one of: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT}")
106+
message_serializer = DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT
106107
else:
107108
if connection_protocol not in [DEFAULT_WS_PROTOCOL, DEFAULT_HTTP_PROTOCOL]:
108109
if invalid_protocol_input:
@@ -342,7 +343,7 @@ def generate_default_config():
342343
parser.add_argument("--gremlin_password", help="the password to use when creating Gremlin connections", default='')
343344
parser.add_argument("--gremlin_serializer",
344345
help="the serializer to use as the encoding format when creating Gremlin connections",
345-
default=DEFAULT_GREMLIN_SERIALIZER)
346+
default='')
346347
parser.add_argument("--gremlin_connection_protocol",
347348
help="the connection protocol to use for Gremlin connections",
348349
default='')

src/graph_notebook/magics/graph_magic.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@
5454
SPARQL_EXPLAIN_MODES, OPENCYPHER_EXPLAIN_MODES, GREMLIN_EXPLAIN_MODES, \
5555
OPENCYPHER_PLAN_CACHE_MODES, OPENCYPHER_DEFAULT_TIMEOUT, OPENCYPHER_STATUS_STATE_MODES, \
5656
normalize_service_name, NEPTUNE_DB_SERVICE_NAME, NEPTUNE_ANALYTICS_SERVICE_NAME, GRAPH_PG_INFO_METRICS, \
57-
GREMLIN_PROTOCOL_FORMATS, DEFAULT_HTTP_PROTOCOL, DEFAULT_WS_PROTOCOL, \
58-
GREMLIN_SERIALIZERS_WS, GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP, normalize_protocol_name, generate_snapshot_name)
57+
GREMLIN_PROTOCOL_FORMATS, DEFAULT_HTTP_PROTOCOL, DEFAULT_WS_PROTOCOL, GRAPHSONV4_UNTYPED, \
58+
GREMLIN_SERIALIZERS_WS, get_gremlin_serializer_mime, normalize_protocol_name, generate_snapshot_name)
5959
from graph_notebook.network import SPARQLNetwork
6060
from graph_notebook.network.gremlin.GremlinNetwork import parse_pattern_list_str, GremlinNetwork
6161
from graph_notebook.visualization.rows_and_columns import sparql_get_rows_and_columns, opencypher_get_rows_and_columns
@@ -1091,6 +1091,9 @@ def gremlin(self, line, cell, local_ns: dict = None):
10911091
f'If not specified, defaults to the value of the gremlin.connection_protocol field '
10921092
f'in %%graph_notebook_config. Please note that this option has no effect on the '
10931093
f'Profile and Explain modes, which must use HTTP.')
1094+
parser.add_argument('-qp', '--query-parameters', type=str, default='',
1095+
help='Parameter definitions to apply to the query. This option can accept a local variable '
1096+
'name, or a string representation of the map.')
10941097
parser.add_argument('--explain-type', type=str.lower, default='dynamic',
10951098
help=f'Explain mode to use when using the explain query mode. '
10961099
f'Accepted values: {GREMLIN_EXPLAIN_MODES}')
@@ -1160,6 +1163,21 @@ def gremlin(self, line, cell, local_ns: dict = None):
11601163
logger.debug(f'Arguments {args}')
11611164
results_df = None
11621165

1166+
query_params = None
1167+
if args.query_parameters:
1168+
if args.query_parameters in local_ns:
1169+
query_params_input = local_ns[args.query_parameters]
1170+
else:
1171+
query_params_input = args.query_parameters
1172+
if isinstance(query_params_input, dict):
1173+
query_params = json.dumps(query_params_input)
1174+
else:
1175+
try:
1176+
query_params_dict = json.loads(query_params_input.replace("'", '"'))
1177+
query_params = json.dumps(query_params_dict)
1178+
except Exception as e:
1179+
print(f"Invalid query parameter input, ignoring.")
1180+
11631181
if args.no_scroll:
11641182
gremlin_layout = UNRESTRICTED_LAYOUT
11651183
gremlin_scrollY = True
@@ -1184,8 +1202,13 @@ def gremlin(self, line, cell, local_ns: dict = None):
11841202

11851203
if mode == QueryMode.EXPLAIN:
11861204
try:
1205+
explain_args = {}
1206+
if args.explain_type:
1207+
explain_args['explain.mode'] = args.explain_type
1208+
if self.client.is_analytics_domain() and query_params:
1209+
explain_args['parameters'] = query_params
11871210
res = self.client.gremlin_explain(cell,
1188-
args={'explain.mode': args.explain_type} if args.explain_type else {})
1211+
args=explain_args)
11891212
res.raise_for_status()
11901213
except Exception as e:
11911214
if self.client.is_analytics_domain():
@@ -1219,6 +1242,8 @@ def gremlin(self, line, cell, local_ns: dict = None):
12191242
"profile.serializer": serializer,
12201243
"profile.indexOps": args.profile_indexOps,
12211244
"profile.debug": args.profile_debug}
1245+
if self.client.is_analytics_domain() and query_params:
1246+
profile_args['parameters'] = query_params
12221247
try:
12231248
profile_misc_args_dict = json.loads(args.profile_misc_args)
12241249
profile_args.update(profile_misc_args_dict)
@@ -1269,17 +1294,29 @@ def gremlin(self, line, cell, local_ns: dict = None):
12691294
try:
12701295
if connection_protocol == DEFAULT_HTTP_PROTOCOL:
12711296
using_http = True
1297+
headers = {}
12721298
message_serializer = self.graph_notebook_config.gremlin.message_serializer
1273-
message_serializer_mime = GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[message_serializer]
1274-
query_res_http = self.client.gremlin_http_query(cell, headers={
1275-
'Accept': message_serializer_mime})
1299+
message_serializer_mime = get_gremlin_serializer_mime(message_serializer, DEFAULT_HTTP_PROTOCOL)
1300+
if message_serializer_mime != GRAPHSONV4_UNTYPED:
1301+
headers['Accept'] = message_serializer_mime
1302+
passed_params = query_params if self.client.is_analytics_domain() else None
1303+
query_res_http = self.client.gremlin_http_query(cell,
1304+
headers=headers,
1305+
query_params=passed_params)
12761306
query_res_http.raise_for_status()
12771307
try:
12781308
query_res_http_json = query_res_http.json()
12791309
except JSONDecodeError:
12801310
query_res_fixed = repair_json(query_res_http.text)
12811311
query_res_http_json = json.loads(query_res_fixed)
1282-
query_res = query_res_http_json['result']['data']
1312+
if 'result' in query_res_http_json:
1313+
query_res = query_res_http_json['result']['data']
1314+
else:
1315+
if 'reason' in query_res_http_json:
1316+
logger.debug('Query failed with internal error, see response.')
1317+
else:
1318+
logger.debug('Received unexpected response format, outputting as single entry.')
1319+
query_res = [query_res_http_json]
12831320
else:
12841321
query_res = self.client.gremlin_query(cell, transport_args=transport_args)
12851322
except Exception as e:
@@ -1317,7 +1354,7 @@ def gremlin(self, line, cell, local_ns: dict = None):
13171354
ignore_groups=args.ignore_groups,
13181355
using_http=using_http)
13191356

1320-
if using_http and 'path()' in cell and query_res:
1357+
if using_http and 'path()' in cell and query_res and isinstance(query_res, list):
13211358
first_path = query_res[0]
13221359
if isinstance(first_path, dict) and first_path.keys() == {'labels', 'objects'}:
13231360
query_res_to_path_type = []
@@ -2844,8 +2881,8 @@ def seed(self, line, local_ns: dict = None):
28442881

28452882
if self.client.is_analytics_domain():
28462883
model_options = SEED_MODEL_OPTIONS_PG
2847-
custom_language_options = SEED_LANGUAGE_OPTIONS_OC
2848-
samples_pg_language_options = SEED_LANGUAGE_OPTIONS_OC
2884+
custom_language_options = SEED_LANGUAGE_OPTIONS_PG
2885+
samples_pg_language_options = SEED_LANGUAGE_OPTIONS_PG
28492886
else:
28502887
model_options = SEED_MODEL_OPTIONS
28512888
custom_language_options = SEED_LANGUAGE_OPTIONS
@@ -3121,7 +3158,11 @@ def process_gremlin_query_line(query_line, line_index, q):
31213158
logger.debug(f"Skipped blank query at line {line_index + 1} in seed file {q['name']}")
31223159
return 0
31233160
try:
3124-
self.client.gremlin_query(query_line)
3161+
if self.client.is_neptune_domain() and self.client.is_analytics_domain() and \
3162+
self.graph_notebook_config.gremlin.connection_protocol == DEFAULT_HTTP_PROTOCOL:
3163+
self.client.gremlin_http_query(query_line)
3164+
else:
3165+
self.client.gremlin_query(query_line)
31253166
return 0
31263167
except GremlinServerError as gremlinEx:
31273168
try:

src/graph_notebook/neptune/client.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,27 +122,34 @@
122122
GRAPHSONV1 = 'GraphSONMessageSerializerGremlinV1'
123123
GRAPHSONV2 = 'GraphSONMessageSerializerV2'
124124
GRAPHSONV3 = 'GraphSONMessageSerializerV3'
125+
GRAPHSONV4 = 'GraphSONMessageSerializerV4'
125126
GRAPHSONV1_UNTYPED = 'GraphSONUntypedMessageSerializerV1'
126127
GRAPHSONV2_UNTYPED = 'GraphSONUntypedMessageSerializerV2'
127128
GRAPHSONV3_UNTYPED = 'GraphSONUntypedMessageSerializerV3'
129+
GRAPHSONV4_UNTYPED = 'GraphSONUntypedMessageSerializerV4'
128130
GRAPHBINARYV1 = 'GraphBinaryMessageSerializerV1'
129131

130132
GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP = {
131133
GRAPHSONV1: 'application/vnd.gremlin-v1.0+json',
132134
GRAPHSONV2: 'application/vnd.gremlin-v2.0+json',
133135
GRAPHSONV3: 'application/vnd.gremlin-v3.0+json',
136+
GRAPHSONV4: 'application/vnd.gremlin-v4.0+json',
134137
GRAPHSONV1_UNTYPED: 'application/vnd.gremlin-v1.0+json;types=false',
135138
GRAPHSONV2_UNTYPED: 'application/vnd.gremlin-v2.0+json;types=false',
136139
GRAPHSONV3_UNTYPED: 'application/vnd.gremlin-v3.0+json;types=false',
140+
GRAPHSONV4_UNTYPED: 'application/vnd.gremlin-v4.0+json;types=false',
137141
GRAPHBINARYV1: 'application/vnd.graphbinary-v1.0'
138142
}
139143

140144
GREMLIN_SERIALIZERS_WS = [GRAPHSONV2, GRAPHSONV3, GRAPHBINARYV1]
141145
GREMLIN_SERIALIZERS_HTTP = [GRAPHSONV1, GRAPHSONV1_UNTYPED, GRAPHSONV2_UNTYPED, GRAPHSONV3_UNTYPED]
142-
GREMLIN_SERIALIZERS_ALL = GREMLIN_SERIALIZERS_WS + GREMLIN_SERIALIZERS_HTTP
146+
GREMLIN_SERIALIZERS_HTTP_NEXT = [GRAPHSONV4, GRAPHSONV4_UNTYPED]
147+
GREMLIN_SERIALIZERS_ALL = GREMLIN_SERIALIZERS_WS + GREMLIN_SERIALIZERS_HTTP + GREMLIN_SERIALIZERS_HTTP_NEXT
143148
NEPTUNE_GREMLIN_SERIALIZERS_HTTP = [GRAPHSONV1_UNTYPED, GRAPHSONV2_UNTYPED, GRAPHSONV3_UNTYPED]
149+
NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT = NEPTUNE_GREMLIN_SERIALIZERS_HTTP + [GRAPHSONV4_UNTYPED]
144150
DEFAULT_GREMLIN_WS_SERIALIZER = GRAPHSONV3
145151
DEFAULT_GREMLIN_HTTP_SERIALIZER = GRAPHSONV3_UNTYPED
152+
DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT = GRAPHSONV4_UNTYPED
146153
DEFAULT_GREMLIN_SERIALIZER = GRAPHSONV3_UNTYPED
147154

148155
DEFAULT_WS_PROTOCOL = "websockets"
@@ -184,11 +191,14 @@ def get_gremlin_serializer_driver_class(serializer_str: str):
184191
return serializer.GraphSONSerializersV3d0()
185192

186193

187-
def get_gremlin_serializer_mime(serializer_str: str):
194+
def get_gremlin_serializer_mime(serializer_str: str, protocol: str = DEFAULT_GREMLIN_PROTOCOL):
188195
if serializer_str in GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP.keys():
189196
return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[serializer_str]
190197
else:
191-
return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[GRAPHSONV1_UNTYPED]
198+
default_serializer_for_protocol = DEFAULT_GREMLIN_HTTP_SERIALIZER if protocol == DEFAULT_HTTP_PROTOCOL \
199+
else DEFAULT_GREMLIN_WS_SERIALIZER
200+
print(f"Invalid serializer, defaulting to {default_serializer_for_protocol}")
201+
return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[default_serializer_for_protocol]
192202

193203

194204
def normalize_protocol_name(protocol: str):
@@ -218,8 +228,10 @@ def normalize_serializer_class_name(serializer: str):
218228
message_serializer += 'MessageSerializerGremlinV1'
219229
elif 'v2' in serializer_lower:
220230
message_serializer += 'MessageSerializerV2'
221-
else:
231+
elif 'v3' in serializer_lower:
222232
message_serializer += 'MessageSerializerV3'
233+
else:
234+
message_serializer += 'MessageSerializerV4'
223235
elif 'graphbinary' in serializer_lower:
224236
message_serializer = GRAPHBINARYV1
225237
else:
@@ -454,7 +466,7 @@ def gremlin_query(self, query, transport_args=None, bindings=None):
454466
c.close()
455467
raise e
456468

457-
def gremlin_http_query(self, query, headers=None) -> requests.Response:
469+
def gremlin_http_query(self, query, headers=None, query_params: dict = None) -> requests.Response:
458470
if headers is None:
459471
headers = {}
460472

@@ -465,6 +477,8 @@ def gremlin_http_query(self, query, headers=None) -> requests.Response:
465477
data['query'] = query
466478
data['language'] = 'gremlin'
467479
headers['content-type'] = 'application/json'
480+
if query_params:
481+
data['parameters'] = str(query_params).replace("'", '"')
468482
else:
469483
uri = f'{self.get_uri(use_websocket=False, use_proxy=use_proxy)}/gremlin'
470484
data['gremlin'] = query
@@ -499,6 +513,9 @@ def _gremlin_query_plan(self, query: str, plan_type: str, args: dict, ) -> reque
499513
data['query'] = query
500514
data['language'] = 'gremlin'
501515
headers['content-type'] = 'application/json'
516+
if 'parameters' in args:
517+
query_params = args.pop('parameters')
518+
data['parameters'] = str(query_params).replace("'", '"')
502519
if plan_type == 'explain':
503520
# Remove explain.mode once HTTP is changed
504521
explain_mode = args.pop('explain.mode')

0 commit comments

Comments
 (0)