From ba5fcdb7f7bc5c617c366d791a25ddafb6f61c3d Mon Sep 17 00:00:00 2001 From: Michael Chin Date: Tue, 24 Sep 2024 20:07:24 -0700 Subject: [PATCH 1/2] Initial TinkerPop 4.0 support --- ChangeLog.md | 1 + .../configuration/generate_config.py | 15 ++--- src/graph_notebook/magics/graph_magic.py | 63 +++++++++++++++---- src/graph_notebook/neptune/client.py | 27 ++++++-- .../network/gremlin/GremlinNetwork.py | 57 +++++++++++------ ...and-Appearance-Customization-Gremlin.ipynb | 58 ++++++++++++++++- test/unit/configuration/test_configuration.py | 10 +-- .../test_configuration_from_main.py | 2 +- 8 files changed, 184 insertions(+), 49 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8f5b8874..2147803a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,6 +4,7 @@ Starting with v1.31.6, this file will contain a record of major features and upd ## Upcoming +- Add documentation for group keys in `%%graph_notebook_vis_options` ([Link to PR](https://github.com/aws/graph-notebook/pull/703)) - Enabled `--query-timeout` on `%%oc explain` for Neptune Analytics ([Link to PR](https://github.com/aws/graph-notebook/pull/701)) ## Release 4.6.0 (September 19, 2024) diff --git a/src/graph_notebook/configuration/generate_config.py b/src/graph_notebook/configuration/generate_config.py index 44270328..08a6fc6b 100644 --- a/src/graph_notebook/configuration/generate_config.py +++ b/src/graph_notebook/configuration/generate_config.py @@ -17,6 +17,7 @@ GRAPHBINARYV1, GREMLIN_SERIALIZERS_HTTP, GREMLIN_SERIALIZERS_WS, GREMLIN_SERIALIZERS_ALL, NEPTUNE_GREMLIN_SERIALIZERS_HTTP, DEFAULT_GREMLIN_WS_SERIALIZER, DEFAULT_GREMLIN_HTTP_SERIALIZER, + NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT, DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT, NEPTUNE_DB_SERVICE_NAME, NEPTUNE_ANALYTICS_SERVICE_NAME, normalize_service_name, normalize_protocol_name, normalize_serializer_class_name) @@ -93,16 +94,16 @@ def __init__(self, traversal_source: str = '', username: str = '', password: str print(f"Enforcing HTTP protocol.") connection_protocol = DEFAULT_HTTP_PROTOCOL # temporary restriction until GraphSON-typed and GraphBinary results are supported - if message_serializer not in NEPTUNE_GREMLIN_SERIALIZERS_HTTP: + if message_serializer not in NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT: if message_serializer not in GREMLIN_SERIALIZERS_ALL: if invalid_serializer_input: - print(f"Invalid serializer specified, defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER}. " - f"Valid serializers: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP}") + print(f"Invalid serializer specified, defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT}. " + f"Valid serializers: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT}") else: print(f"{message_serializer} is not currently supported for HTTP connections, " - f"defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER}. " - f"Please use one of: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP}") - message_serializer = DEFAULT_GREMLIN_HTTP_SERIALIZER + f"defaulting to {DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT}. " + f"Please use one of: {NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT}") + message_serializer = DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT else: if connection_protocol not in [DEFAULT_WS_PROTOCOL, DEFAULT_HTTP_PROTOCOL]: if invalid_protocol_input: @@ -342,7 +343,7 @@ def generate_default_config(): parser.add_argument("--gremlin_password", help="the password to use when creating Gremlin connections", default='') parser.add_argument("--gremlin_serializer", help="the serializer to use as the encoding format when creating Gremlin connections", - default=DEFAULT_GREMLIN_SERIALIZER) + default='') parser.add_argument("--gremlin_connection_protocol", help="the connection protocol to use for Gremlin connections", default='') diff --git a/src/graph_notebook/magics/graph_magic.py b/src/graph_notebook/magics/graph_magic.py index 6b779124..be25ddc2 100644 --- a/src/graph_notebook/magics/graph_magic.py +++ b/src/graph_notebook/magics/graph_magic.py @@ -54,8 +54,8 @@ SPARQL_EXPLAIN_MODES, OPENCYPHER_EXPLAIN_MODES, GREMLIN_EXPLAIN_MODES, \ OPENCYPHER_PLAN_CACHE_MODES, OPENCYPHER_DEFAULT_TIMEOUT, OPENCYPHER_STATUS_STATE_MODES, \ normalize_service_name, NEPTUNE_DB_SERVICE_NAME, NEPTUNE_ANALYTICS_SERVICE_NAME, GRAPH_PG_INFO_METRICS, \ - GREMLIN_PROTOCOL_FORMATS, DEFAULT_HTTP_PROTOCOL, DEFAULT_WS_PROTOCOL, \ - GREMLIN_SERIALIZERS_WS, GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP, normalize_protocol_name, generate_snapshot_name) + GREMLIN_PROTOCOL_FORMATS, DEFAULT_HTTP_PROTOCOL, DEFAULT_WS_PROTOCOL, GRAPHSONV4_UNTYPED, \ + GREMLIN_SERIALIZERS_WS, get_gremlin_serializer_mime, normalize_protocol_name, generate_snapshot_name) from graph_notebook.network import SPARQLNetwork from graph_notebook.network.gremlin.GremlinNetwork import parse_pattern_list_str, GremlinNetwork 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): f'If not specified, defaults to the value of the gremlin.connection_protocol field ' f'in %%graph_notebook_config. Please note that this option has no effect on the ' f'Profile and Explain modes, which must use HTTP.') + parser.add_argument('-qp', '--query-parameters', type=str, default='', + help='Parameter definitions to apply to the query. This option can accept a local variable ' + 'name, or a string representation of the map.') parser.add_argument('--explain-type', type=str.lower, default='dynamic', help=f'Explain mode to use when using the explain query mode. ' f'Accepted values: {GREMLIN_EXPLAIN_MODES}') @@ -1160,6 +1163,21 @@ def gremlin(self, line, cell, local_ns: dict = None): logger.debug(f'Arguments {args}') results_df = None + query_params = None + if args.query_parameters: + if args.query_parameters in local_ns: + query_params_input = local_ns[args.query_parameters] + else: + query_params_input = args.query_parameters + if isinstance(query_params_input, dict): + query_params = json.dumps(query_params_input) + else: + try: + query_params_dict = json.loads(query_params_input.replace("'", '"')) + query_params = json.dumps(query_params_dict) + except Exception as e: + print(f"Invalid query parameter input, ignoring.") + if args.no_scroll: gremlin_layout = UNRESTRICTED_LAYOUT gremlin_scrollY = True @@ -1184,8 +1202,13 @@ def gremlin(self, line, cell, local_ns: dict = None): if mode == QueryMode.EXPLAIN: try: + explain_args = {} + if args.explain_type: + explain_args['explain.mode'] = args.explain_type + if self.client.is_analytics_domain() and query_params: + explain_args['parameters'] = query_params res = self.client.gremlin_explain(cell, - args={'explain.mode': args.explain_type} if args.explain_type else {}) + args=explain_args) res.raise_for_status() except Exception as e: if self.client.is_analytics_domain(): @@ -1219,6 +1242,8 @@ def gremlin(self, line, cell, local_ns: dict = None): "profile.serializer": serializer, "profile.indexOps": args.profile_indexOps, "profile.debug": args.profile_debug} + if self.client.is_analytics_domain() and query_params: + profile_args['parameters'] = query_params try: profile_misc_args_dict = json.loads(args.profile_misc_args) profile_args.update(profile_misc_args_dict) @@ -1269,17 +1294,29 @@ def gremlin(self, line, cell, local_ns: dict = None): try: if connection_protocol == DEFAULT_HTTP_PROTOCOL: using_http = True + headers = {} message_serializer = self.graph_notebook_config.gremlin.message_serializer - message_serializer_mime = GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[message_serializer] - query_res_http = self.client.gremlin_http_query(cell, headers={ - 'Accept': message_serializer_mime}) + message_serializer_mime = get_gremlin_serializer_mime(message_serializer, DEFAULT_HTTP_PROTOCOL) + if message_serializer_mime != GRAPHSONV4_UNTYPED: + headers['Accept'] = message_serializer_mime + passed_params = query_params if self.client.is_analytics_domain() else None + query_res_http = self.client.gremlin_http_query(cell, + headers=headers, + query_params=passed_params) query_res_http.raise_for_status() try: query_res_http_json = query_res_http.json() except JSONDecodeError: query_res_fixed = repair_json(query_res_http.text) query_res_http_json = json.loads(query_res_fixed) - query_res = query_res_http_json['result']['data'] + if 'result' in query_res_http_json: + query_res = query_res_http_json['result']['data'] + else: + if 'reason' in query_res_http_json: + logger.debug('Query failed with internal error, see response.') + else: + logger.debug('Received unexpected response format, outputting as single entry.') + query_res = [query_res_http_json] else: query_res = self.client.gremlin_query(cell, transport_args=transport_args) except Exception as e: @@ -1317,7 +1354,7 @@ def gremlin(self, line, cell, local_ns: dict = None): ignore_groups=args.ignore_groups, using_http=using_http) - if using_http and 'path()' in cell and query_res: + if using_http and 'path()' in cell and query_res and isinstance(query_res, list): first_path = query_res[0] if isinstance(first_path, dict) and first_path.keys() == {'labels', 'objects'}: query_res_to_path_type = [] @@ -2844,8 +2881,8 @@ def seed(self, line, local_ns: dict = None): if self.client.is_analytics_domain(): model_options = SEED_MODEL_OPTIONS_PG - custom_language_options = SEED_LANGUAGE_OPTIONS_OC - samples_pg_language_options = SEED_LANGUAGE_OPTIONS_OC + custom_language_options = SEED_LANGUAGE_OPTIONS_PG + samples_pg_language_options = SEED_LANGUAGE_OPTIONS_PG else: model_options = SEED_MODEL_OPTIONS custom_language_options = SEED_LANGUAGE_OPTIONS @@ -3121,7 +3158,11 @@ def process_gremlin_query_line(query_line, line_index, q): logger.debug(f"Skipped blank query at line {line_index + 1} in seed file {q['name']}") return 0 try: - self.client.gremlin_query(query_line) + if self.client.is_neptune_domain() and self.client.is_analytics_domain() and \ + self.graph_notebook_config.gremlin.connection_protocol == DEFAULT_HTTP_PROTOCOL: + self.client.gremlin_http_query(query_line) + else: + self.client.gremlin_query(query_line) return 0 except GremlinServerError as gremlinEx: try: diff --git a/src/graph_notebook/neptune/client.py b/src/graph_notebook/neptune/client.py index c82d5c60..75cf0200 100644 --- a/src/graph_notebook/neptune/client.py +++ b/src/graph_notebook/neptune/client.py @@ -122,27 +122,34 @@ GRAPHSONV1 = 'GraphSONMessageSerializerGremlinV1' GRAPHSONV2 = 'GraphSONMessageSerializerV2' GRAPHSONV3 = 'GraphSONMessageSerializerV3' +GRAPHSONV4 = 'GraphSONMessageSerializerV4' GRAPHSONV1_UNTYPED = 'GraphSONUntypedMessageSerializerV1' GRAPHSONV2_UNTYPED = 'GraphSONUntypedMessageSerializerV2' GRAPHSONV3_UNTYPED = 'GraphSONUntypedMessageSerializerV3' +GRAPHSONV4_UNTYPED = 'GraphSONUntypedMessageSerializerV4' GRAPHBINARYV1 = 'GraphBinaryMessageSerializerV1' GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP = { GRAPHSONV1: 'application/vnd.gremlin-v1.0+json', GRAPHSONV2: 'application/vnd.gremlin-v2.0+json', GRAPHSONV3: 'application/vnd.gremlin-v3.0+json', + GRAPHSONV4: 'application/vnd.gremlin-v4.0+json', GRAPHSONV1_UNTYPED: 'application/vnd.gremlin-v1.0+json;types=false', GRAPHSONV2_UNTYPED: 'application/vnd.gremlin-v2.0+json;types=false', GRAPHSONV3_UNTYPED: 'application/vnd.gremlin-v3.0+json;types=false', + GRAPHSONV4_UNTYPED: 'application/vnd.gremlin-v4.0+json;types=false', GRAPHBINARYV1: 'application/vnd.graphbinary-v1.0' } GREMLIN_SERIALIZERS_WS = [GRAPHSONV2, GRAPHSONV3, GRAPHBINARYV1] GREMLIN_SERIALIZERS_HTTP = [GRAPHSONV1, GRAPHSONV1_UNTYPED, GRAPHSONV2_UNTYPED, GRAPHSONV3_UNTYPED] -GREMLIN_SERIALIZERS_ALL = GREMLIN_SERIALIZERS_WS + GREMLIN_SERIALIZERS_HTTP +GREMLIN_SERIALIZERS_HTTP_NEXT = [GRAPHSONV4, GRAPHSONV4_UNTYPED] +GREMLIN_SERIALIZERS_ALL = GREMLIN_SERIALIZERS_WS + GREMLIN_SERIALIZERS_HTTP + GREMLIN_SERIALIZERS_HTTP_NEXT NEPTUNE_GREMLIN_SERIALIZERS_HTTP = [GRAPHSONV1_UNTYPED, GRAPHSONV2_UNTYPED, GRAPHSONV3_UNTYPED] +NEPTUNE_GREMLIN_SERIALIZERS_HTTP_NEXT = NEPTUNE_GREMLIN_SERIALIZERS_HTTP + [GRAPHSONV4_UNTYPED] DEFAULT_GREMLIN_WS_SERIALIZER = GRAPHSONV3 DEFAULT_GREMLIN_HTTP_SERIALIZER = GRAPHSONV3_UNTYPED +DEFAULT_GREMLIN_HTTP_SERIALIZER_NEXT = GRAPHSONV4_UNTYPED DEFAULT_GREMLIN_SERIALIZER = GRAPHSONV3_UNTYPED DEFAULT_WS_PROTOCOL = "websockets" @@ -184,11 +191,14 @@ def get_gremlin_serializer_driver_class(serializer_str: str): return serializer.GraphSONSerializersV3d0() -def get_gremlin_serializer_mime(serializer_str: str): +def get_gremlin_serializer_mime(serializer_str: str, protocol: str = DEFAULT_GREMLIN_PROTOCOL): if serializer_str in GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP.keys(): return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[serializer_str] else: - return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[GRAPHSONV1_UNTYPED] + default_serializer_for_protocol = DEFAULT_GREMLIN_HTTP_SERIALIZER if protocol == DEFAULT_HTTP_PROTOCOL \ + else DEFAULT_GREMLIN_WS_SERIALIZER + print(f"Invalid serializer, defaulting to {default_serializer_for_protocol}") + return GREMLIN_SERIALIZERS_CLASS_TO_MIME_MAP[default_serializer_for_protocol] def normalize_protocol_name(protocol: str): @@ -218,8 +228,10 @@ def normalize_serializer_class_name(serializer: str): message_serializer += 'MessageSerializerGremlinV1' elif 'v2' in serializer_lower: message_serializer += 'MessageSerializerV2' - else: + elif 'v3' in serializer_lower: message_serializer += 'MessageSerializerV3' + else: + message_serializer += 'MessageSerializerV4' elif 'graphbinary' in serializer_lower: message_serializer = GRAPHBINARYV1 else: @@ -454,7 +466,7 @@ def gremlin_query(self, query, transport_args=None, bindings=None): c.close() raise e - def gremlin_http_query(self, query, headers=None) -> requests.Response: + def gremlin_http_query(self, query, headers=None, query_params: dict = None) -> requests.Response: if headers is None: headers = {} @@ -465,6 +477,8 @@ def gremlin_http_query(self, query, headers=None) -> requests.Response: data['query'] = query data['language'] = 'gremlin' headers['content-type'] = 'application/json' + if query_params: + data['parameters'] = str(query_params).replace("'", '"') else: uri = f'{self.get_uri(use_websocket=False, use_proxy=use_proxy)}/gremlin' data['gremlin'] = query @@ -499,6 +513,9 @@ def _gremlin_query_plan(self, query: str, plan_type: str, args: dict, ) -> reque data['query'] = query data['language'] = 'gremlin' headers['content-type'] = 'application/json' + if 'parameters' in args: + query_params = args.pop('parameters') + data['parameters'] = str(query_params).replace("'", '"') if plan_type == 'explain': # Remove explain.mode once HTTP is changed explain_mode = args.pop('explain.mode') diff --git a/src/graph_notebook/network/gremlin/GremlinNetwork.py b/src/graph_notebook/network/gremlin/GremlinNetwork.py index 73a8d553..d13cdbe7 100644 --- a/src/graph_notebook/network/gremlin/GremlinNetwork.py +++ b/src/graph_notebook/network/gremlin/GremlinNetwork.py @@ -84,6 +84,8 @@ def get_id(element): elif isinstance(element, dict): if T.id in element: element_id = element[T.id] + elif '~id' in element: + element_id = element['~id'] elif 'id' in element: element_id = element['id'] else: @@ -125,6 +127,8 @@ def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length= def get_dict_element_property_value(self, element, k, temp_label, custom_property): property_value = None + if isinstance(temp_label, list): + temp_label = str(temp_label).strip("[]'") if isinstance(custom_property, dict): try: if isinstance(custom_property[temp_label], tuple) and isinstance(element[k], list): @@ -311,10 +315,10 @@ def add_results(self, results, is_http=False): raise ValueError("results must be a list of paths") if is_http: - gremlin_id = 'id' + gremlin_ids = ['id', '~id'] gremlin_label = 'label' else: - gremlin_id = T.id + gremlin_ids = [T.id] gremlin_label = T.label for path_index, path in enumerate(results): @@ -325,10 +329,15 @@ def add_results(self, results, is_http=False): for i in range(len(path)): if isinstance(path[i], dict): is_elementmap = False - if gremlin_id in path[i] and gremlin_label in path[i]: + gremlin_id_in_path = False + for possible_id in gremlin_ids: + if possible_id in path[i]: + gremlin_id_in_path = True + break + if gremlin_id_in_path and gremlin_label in path[i]: for prop, value in path[i].items(): # ID and/or Label property keys could be renamed by a project() step - if isinstance(value, str) and prop not in [gremlin_id, gremlin_label]: + if isinstance(value, str) and prop not in gremlin_ids + [gremlin_label]: is_elementmap = True break elif isinstance(value, dict): @@ -346,8 +355,16 @@ def add_results(self, results, is_http=False): self.insert_path_element(path, i) else: self.insert_path_element(path, i) - elif isinstance(path, dict) and gremlin_id in path.keys() and gremlin_label in path.keys(): - self.insert_elementmap(path, index=path_index) + elif isinstance(path, dict): + gremlin_id_in_path_keys = False + for possible_id in gremlin_ids: + if possible_id in path.keys(): + gremlin_id_in_path_keys = True + break + if gremlin_id_in_path_keys and gremlin_label in path.keys(): + self.insert_elementmap(path, index=path_index) + else: + raise ValueError("all entries in results must be paths or elementMaps") else: raise ValueError("all entries in results must be paths or elementMaps") @@ -383,7 +400,7 @@ def add_vertex(self, v, path_index: int = -1): # T.label is set in order to handle the default case of grouping by label # when no explicit key is specified group = v.label - elif str(self.group_by_property) in [T_ID, 'id']: + elif str(self.group_by_property) in [T_ID, 'id', '~id']: group = v.id elif self.group_by_property == DEPTH_GRP_KEY: group = depth_group @@ -396,7 +413,7 @@ def add_vertex(self, v, path_index: int = -1): group = str(v) elif self.group_by_property[str(v.label)] in [T_LABEL, 'label']: group = v.label - elif self.group_by_property[str(v.label)] in [T_ID, 'id']: + elif self.group_by_property[str(v.label)] in [T_ID, 'id', '~id']: group = v.id elif self.group_by_property[str(v.label)] == DEPTH_GRP_KEY: group = depth_group @@ -449,9 +466,11 @@ def add_vertex(self, v, path_index: int = -1): # Since it is needed for checking for the vertex label's desired grouping behavior in group_by_property if T.label in v.keys() or 'label' in v.keys(): label_key = T.label if T.label in v.keys() else 'label' - title_plc = str(v[label_key]) + label_raw = v[label_key] + title_plc = str(label_raw) title, label = self.strip_and_truncate_label_and_title(title_plc, self.label_max_length) else: + label_raw = '' title_plc = '' group = DEFAULT_GRP @@ -459,7 +478,7 @@ def add_vertex(self, v, path_index: int = -1): group = str(v) group_is_set = True for k in v: - if str(k) in [T_ID, 'id']: + if str(k) in [T_ID, 'id', '~id']: node_id = str(v[k]) if isinstance(v[k], dict): @@ -499,7 +518,7 @@ def add_vertex(self, v, path_index: int = -1): group = str(v[k]) group_is_set = True if not display_is_set: - label_property_raw_value = self.get_dict_element_property_value(v, k, title_plc, + label_property_raw_value = self.get_dict_element_property_value(v, k, label_raw, self.display_property) if label_property_raw_value: label_full, label = self.strip_and_truncate_label_and_title(label_property_raw_value, @@ -508,7 +527,7 @@ def add_vertex(self, v, path_index: int = -1): title = label_full display_is_set = True if not tooltip_display_is_set: - tooltip_property_raw_value = self.get_dict_element_property_value(v, k, title_plc, + tooltip_property_raw_value = self.get_dict_element_property_value(v, k, label_raw, self.tooltip_property) if tooltip_property_raw_value: title, label_plc = self.strip_and_truncate_label_and_title(tooltip_property_raw_value, @@ -612,13 +631,14 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None): tooltip_display_is_set = False if T.label in edge.keys() or 'label' in edge.keys(): label_key = T.label if T.label in edge.keys() else 'label' - edge_title_plc = str(edge[label_key]) + edge_label_raw = edge[label_key] + edge_title_plc = str(edge_label_raw) edge_title, edge_label = self.strip_and_truncate_label_and_title(edge_title_plc, self.edge_label_max_length) else: - edge_title_plc = '' + edge_label_raw = '' for k in edge: - if str(k) in [T_ID, 'id']: + if str(k) in [T_ID, 'id', '~id']: edge_id = str(edge[k]) if isinstance(edge[k], dict): # Handle Direction properties, where the value is a map @@ -630,8 +650,8 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None): else: properties[k] = edge[k] - if self.edge_display_property not in [T_LABEL, 'label'] and not display_is_set: - label_property_raw_value = self.get_dict_element_property_value(edge, k, edge_title_plc, + if not display_is_set: + label_property_raw_value = self.get_dict_element_property_value(edge, k, edge_label_raw, self.edge_display_property) if label_property_raw_value: edge_label_full, edge_label = self.strip_and_truncate_label_and_title( @@ -639,8 +659,9 @@ def add_path_edge(self, edge, from_id='', to_id='', data=None): if not using_custom_tooltip: edge_title = edge_label_full display_is_set = True + if not tooltip_display_is_set: - tooltip_property_raw_value = self.get_dict_element_property_value(edge, k, edge_title_plc, + tooltip_property_raw_value = self.get_dict_element_property_value(edge, k, edge_label_raw, self.edge_tooltip_property) if tooltip_property_raw_value: edge_title, label_plc = self.strip_and_truncate_label_and_title(tooltip_property_raw_value, diff --git a/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb b/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb index 0d948141..f0198d86 100644 --- a/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb +++ b/src/graph_notebook/notebooks/01-Neptune-Database/02-Visualization/Grouping-and-Appearance-Customization-Gremlin.ipynb @@ -683,11 +683,65 @@ "\n", "To customize the appearance of node groups, we want to use the [groups](https://visjs.github.io/vis-network/docs/network/groups.html#) options. There is a nearly endless amount of customization you can make to the groups using the options provided, but we will demonstrate some of the most common ones in the next few sections.\n", "\n", + "
\n", + "
\n", + " Note on valueMap vs elementMap for group keys\n", + " \n", + "The exact value of the group keys that you use may vary, depending on the format specified inside the `by()` step.\n", + " \n", + "For the queries in the sections that follow, note that `by(valueMap())` is used. `valueMap` will return the property keys in the path in list form, for example:\n", + " \n", + "```\n", + "path[{'country': ['MX'], 'code': ['CZM'], 'longest': [10165], ...\n", + "```\n", + "\\\n", + "To match, the group keys in `%%graph_notebook_vis_options` must also follow the list format.\n", + " \n", + "```\n", + "{\n", + " \"groups\": {\n", + " \"['CA']\": {\"color\": \"red\"},\n", + " \"['MX']\": {\"color\": \"rgba(9, 104, 178, 1)\"}, \n", + " ...\n", + " }\n", + "}\n", + "```\n", + "\\\n", + "On the other hand, we may elect to use `elementMap` instead of `valueMap` in the `by` modulator. `elementMap` differs in that it always returns property keys alone, as their original type. It will also return the `label` and `id` token properties automatically; for `valueMap`, tokens have to be explicitly requests. \n", + " \n", + "For example, the same path generated with `elementMap` would look look like:\n", + "\n", + "```\n", + "path[{: '365', : 'airport', 'code': 'CZM', 'country': 'MX', ...\n", + "```\n", + "\\\n", + "Subsequently, we need to specify the group keys in `%%graph_notebook_vis_options` only as the base string value.\n", + " \n", + "```\n", + "{\n", + " \"groups\": {\n", + " \"CA\": {\"color\": \"red\"},\n", + " \"MX\": {\"color\": \"rgba(9, 104, 178, 1)\"}, \n", + " ...\n", + " }\n", + "}\n", + "```\n", + "\\\n", + "For more information on usage of `valueMap` and `elementMap`, please refer to Kelvin Lawrence's Practical Gremlin tutorials below.\n", + "\n", + "https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html#vm\n", + "\\\n", + "https://kelvinlawrence.net/book/Gremlin-Graph-Guide.html#element-map\n", + "\n", + "
\n", + "
\n", + "\n", + "\n", "### Specifying Group Colors\n", "\n", - "Specifying the colors of groups is probably one of the most common customizations performed. To accomplish this we specify the options using the `%%graph_notebook_vis_options` magic as shown below. For each of the associated group names we use the exact property value followed by the options you would like to use for that group.\n", + "Specifying the colors of groups is probably one of the most common customizations performed. To accomplish this we specify the options using the `%%graph_notebook_vis_options` magic as shown below. \n", "\n", - "**Note** Finding the exact property value for the group name can be accomplished by looking at the data returned in the Console tab.\n", + "For each of the associated group names, we use the exact property value followed by the options you would like to use for that group. Finding the exact property value for the group name can be accomplished by looking at the data returned in the Console tab.\n", "\n", "Run the next two cells to set the colors for our three groups to red for the airports in Canada, green for the airports in the US, and blue for the airports in Mexico. In the case of color, the values can be specified by name, RGBA value, or Hex value." ] diff --git a/test/unit/configuration/test_configuration.py b/test/unit/configuration/test_configuration.py index 4687d303..a472e85e 100644 --- a/test/unit/configuration/test_configuration.py +++ b/test/unit/configuration/test_configuration.py @@ -696,7 +696,7 @@ def test_configuration_gremlinsection_neptune_default_analytics(self): self.assertEqual(config.gremlin.traversal_source, 'g') self.assertEqual(config.gremlin.username, '') self.assertEqual(config.gremlin.password, '') - self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV3') + self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV4') self.assertEqual(config.gremlin.connection_protocol, DEFAULT_HTTP_PROTOCOL) def test_configuration_gremlinsection_neptune_analytics_override_ws_protocol(self): @@ -710,7 +710,7 @@ def test_configuration_gremlinsection_neptune_analytics_override_ws_protocol(sel self.assertEqual(config.gremlin.traversal_source, 'g') self.assertEqual(config.gremlin.username, '') self.assertEqual(config.gremlin.password, '') - self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV3') + self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV4') self.assertEqual(config.gremlin.connection_protocol, DEFAULT_HTTP_PROTOCOL) def test_configuration_gremlinsection_neptune_analytics_override_serializer(self): @@ -738,7 +738,7 @@ def test_configuration_gremlinsection_neptune_analytics_override_serializer_inva self.assertEqual(config.gremlin.traversal_source, 'g') self.assertEqual(config.gremlin.username, '') self.assertEqual(config.gremlin.password, '') - self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV3') + self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV4') self.assertEqual(config.gremlin.connection_protocol, DEFAULT_HTTP_PROTOCOL) def test_configuration_gremlinsection_neptune_analytics_override_serializer_not_graphson_untyped(self): @@ -752,7 +752,7 @@ def test_configuration_gremlinsection_neptune_analytics_override_serializer_not_ self.assertEqual(config.gremlin.traversal_source, 'g') self.assertEqual(config.gremlin.username, '') self.assertEqual(config.gremlin.password, '') - self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV3') + self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV4') self.assertEqual(config.gremlin.connection_protocol, DEFAULT_HTTP_PROTOCOL) def test_configuration_gremlinsection_neptune_analytics_override_http_protocol(self): @@ -766,7 +766,7 @@ def test_configuration_gremlinsection_neptune_analytics_override_http_protocol(s self.assertEqual(config.gremlin.traversal_source, 'g') self.assertEqual(config.gremlin.username, '') self.assertEqual(config.gremlin.password, '') - self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV3') + self.assertEqual(config.gremlin.message_serializer, 'GraphSONUntypedMessageSerializerV4') self.assertEqual(config.gremlin.connection_protocol, DEFAULT_HTTP_PROTOCOL) def test_configuration_gremlinsection_protocol_neptune_default_with_proxy(self): diff --git a/test/unit/configuration/test_configuration_from_main.py b/test/unit/configuration/test_configuration_from_main.py index 73dd2694..9276d9b2 100644 --- a/test/unit/configuration/test_configuration_from_main.py +++ b/test/unit/configuration/test_configuration_from_main.py @@ -205,7 +205,7 @@ def test_generate_configuration_main_gremlin_serializer_analytics(self): self.assertEqual(0, result) config = get_config(self.test_file_path) config_dict = config.to_dict() - self.assertEqual('GraphSONUntypedMessageSerializerV3', config_dict['gremlin']['message_serializer']) + self.assertEqual('GraphSONUntypedMessageSerializerV4', config_dict['gremlin']['message_serializer']) def test_generate_configuration_main_empty_args_custom(self): expected_config = Configuration(self.neptune_host_custom, self.port, neptune_hosts=self.custom_hosts_list) From a0eb1b692a7d6a60dcec31a26d54724ae6fce9ca Mon Sep 17 00:00:00 2001 From: Michael Chin Date: Tue, 1 Oct 2024 01:52:56 -0700 Subject: [PATCH 2/2] update changelog --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 2147803a..082eb03a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -4,7 +4,8 @@ Starting with v1.31.6, this file will contain a record of major features and upd ## Upcoming -- Add documentation for group keys in `%%graph_notebook_vis_options` ([Link to PR](https://github.com/aws/graph-notebook/pull/703)) +- Added experimental TinkerPop 4.0 support ([Link to PR](https://github.com/aws/graph-notebook/pull/704)) +- Added documentation for group keys in `%%graph_notebook_vis_options` ([Link to PR](https://github.com/aws/graph-notebook/pull/703)) - Enabled `--query-timeout` on `%%oc explain` for Neptune Analytics ([Link to PR](https://github.com/aws/graph-notebook/pull/701)) ## Release 4.6.0 (September 19, 2024)