Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Starting with v1.31.6, this file will contain a record of major features and upd

## Upcoming

## Release 5.0.2 (Aug 19, 2025)
- Support edgeOnlyLoad param for load magic ([Link to PR](https://github.com/aws/graph-notebook/pull/750))
- Theme support for Graph Notebook Widgets ([Link to PR](https://github.com/aws/graph-notebook/pull/754))
- Fix expected output in graph query tutorials ([Link to PR](https://github.com/aws/graph-notebook/pull/755))

## Release 5.0.1 (May 19, 2025)
- Locked numba dependency to 0.60.0 to avoid numpy conflict ([Link to PR](https://github.com/aws/graph-notebook/pull/735))
- Fixed library target for nbclassic nbextension for graph_notebook_widget ([Link to PR](https://github.com/aws/graph-notebook/pull/739))
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ python3 -m build .

You should now be able to find the built distribution at

`./dist/graph_notebook-5.0.1-py3-none-any.whl`
`./dist/graph_notebook-5.0.2-py3-none-any.whl`

And use it by following the [installation](https://github.com/aws/graph-notebook#installation) steps, replacing

Expand All @@ -440,7 +440,7 @@ pip install graph-notebook
with

``` python
pip install ./dist/graph_notebook-5.0.1-py3-none-any.whl --force-reinstall
pip install ./dist/graph_notebook-5.0.2-py3-none-any.whl --force-reinstall
```

## Contributing Guidelines
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ build-backend = "hatchling.build"

[project]
name = "graph-notebook"
version = "5.0.1"
version = "5.0.2"
description = "Jupyter notebook extension to connect to graph databases"
readme = "README.md"
license = { file = "LICENSE" }
Expand Down
2 changes: 1 addition & 1 deletion src/graph_notebook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
SPDX-License-Identifier: Apache-2.0
"""

__version__ = '5.0.1'
__version__ = '5.0.2'
28 changes: 27 additions & 1 deletion src/graph_notebook/magics/graph_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -3764,7 +3764,33 @@ def handle_opencypher_query(self, line, cell, local_ns):
results_df, has_results = oc_results_df(res, res_format)
if has_results:
titles.append('Console')
# Need to eventually add code to parse and display a network for the bolt format here
# Create graph visualization for bolt response
try:
# Wrap bolt response in expected format
# Required because the graph visualizer need the data to be present in a certain format
transformed_res = {"results": res} if isinstance(res, list) else {"results": []}

gn = OCNetwork(group_by_property=args.group_by, display_property=args.display_property,
group_by_raw=args.group_by_raw,
group_by_depth=args.group_by_depth,
edge_display_property=args.edge_display_property,
tooltip_property=args.tooltip_property,
edge_tooltip_property=args.edge_tooltip_property,
label_max_length=args.label_max_length,
edge_label_max_length=args.rel_label_max_length,
ignore_groups=args.ignore_groups)
gn.add_results(transformed_res)
logger.debug(f'number of nodes is {len(gn.graph.nodes)}')
if len(gn.graph.nodes) > 0:
self.graph_notebook_vis_options['physics']['disablePhysicsAfterInitialSimulation'] \
= args.stop_physics
self.graph_notebook_vis_options['physics']['simulationDuration'] = args.simulation_duration
force_graph_output = Force(network=gn, options=self.graph_notebook_vis_options)
titles.append('Graph')
children.append(force_graph_output)
except (TypeError, ValueError) as network_creation_error:
logger.debug(f'Unable to create network from bolt result. Skipping from result set: {res}')
logger.debug(f'Error: {network_creation_error}')

if not args.silent:
if args.mode != 'explain':
Expand Down
29 changes: 28 additions & 1 deletion src/graph_notebook/neptune/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,34 @@ def opencyper_bolt(self, query: str, **kwargs):
with driver.session(database=self.neo4j_database) as session:
try:
res = session.run(query, kwargs)
data = res.data()
data = []
for record in res:
record_dict = {}
for key in record.keys():
value = record[key]
if hasattr(value, 'id') and (hasattr(value, 'labels') or hasattr(value, 'type')): # Node or Relationship
if hasattr(value, 'labels'): # Node
record_dict[key] = {
'~id': str(value.id),
'~entityType': 'node',
'~labels': list(value.labels),
'~properties': dict(value)
}
elif hasattr(value, 'type'): # Relationship

start_node = value.nodes[0] if value.nodes else None
end_node = value.nodes[1] if len(value.nodes) > 1 else None
record_dict[key] = {
'~id': str(value.id),
'~entityType': 'relationship',
'~start': str(start_node.id) if start_node else '',
'~end': str(end_node.id) if end_node else '',
'~type': value.type,
'~properties': dict(value)
}
else:
record_dict[key] = value
data.append(record_dict)
except AuthError:
print("Neo4J Bolt request failed with an authentication error. Please ensure that the 'neo4j' section "
"of your %graph_notebook_config contains the correct credentials and auth setting.")
Expand Down
3 changes: 1 addition & 2 deletions src/graph_notebook/network/opencypher/OCNetwork.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@

class OCNetwork(EventfulNetwork):
"""OCNetwork extends the EventfulNetwork class and uses the add_results method to parse any response that returns
nodes/relationships as part (or all) of the response. Currently this only works with HTTPS response format but in
the future, we will work to support Bolt based responses as well.
nodes/relationships as part (or all) of the response.
"""

def __init__(self, graph: MultiDiGraph = None, callbacks=None, label_max_length=DEFAULT_LABEL_MAX_LENGTH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"For these notebooks, we will be leveraging a dataset from the book [Graph Databases in Action](https://www.manning.com/books/graph-databases-in-action?a_aid=bechberger) from Manning Publications. \n",
"\n",
"\n",
"**Note** These notebooks do not cover data modeling or building a data loading pipeline. If you would like a more detailed description about how this dataset is constructed and the design of the data model came from then please read the book.\n",
"**Note** These notebooks do not cover data modeling or building a data loading pipeline. If you would like a more detailed description about how this dataset is constructed and where the design of the data model came from then please read the book.\n",
"\n",
"To get started, the first step is to load data into the cluster. Assuming the cluster is empty, this can be accomplished by running the cell below which will load our Dining By Friends data."
]
Expand All @@ -34,7 +34,7 @@
"\n",
"Throughout all the **Learning Gremlin on Neptune** notebooks, you will notice that each code block starts with either a `%` or `%%` command. These are called *workbench magic* commands, and are essentially shortcuts to specific Neptune APIs. For example:\n",
"\n",
"* `%%gremlin` - issues a Gremlin query to the Neptune endpoint usng WebSockets\n",
"* `%%gremlin` - issues a Gremlin query to the Neptune endpoint using WebSockets\n",
"* `%seed` - provides a convenient way to add sample data to your Neptune endpoint\n",
"* `%load` - generates a form that you can use to submit a bulk load request to Neptune\n",
"\n",
Expand Down Expand Up @@ -99,7 +99,7 @@
" \"restaurant\": {\n",
" \"color\": \"#ffe6cc\"\n",
" },\n",
" \"cusine\": {\n",
" \"cuisine\": {\n",
" \"color\": \"#fff2cc\"\n",
" }\n",
" }\n",
Expand Down Expand Up @@ -233,7 +233,7 @@
"\n",
"### Finding Nodes\n",
"\n",
"The simplest traversal you can do in Gremlin is to search for nodes. In Gremlin traversals, nodes are represented by `V()`. In our example, *review*, *restaurant*, *cuisine*, *person*, *state* and *city* as represented as nodes.\n",
"The simplest traversal you can do in Gremlin is to search for nodes. In Gremlin traversals, nodes are represented by `V()`. In our example, *review*, *restaurant*, *cuisine*, *person*, *state* and *city* are represented as nodes.\n",
"\n",
"Execute the query below to search for all nodes and return them, but limit the number returned to 10."
]
Expand Down Expand Up @@ -320,7 +320,7 @@
"id": "a3093ad2",
"metadata": {},
"source": [
"We can also do the same using the combination of `bothE()` and `otherV()`, instead of explicitly stating whether to travese outgoing or incoming edges."
"We can also do the same using the combination of `bothE()` and `otherV()`, instead of explicitly stating whether to traverse outgoing or incoming edges."
]
},
{
Expand Down Expand Up @@ -484,7 +484,7 @@
"metadata": {},
"source": [
"### Filtering Edge by Label\n",
"Another common item you to filter on is the type or label associated with an edge. As with nodes, you can use the `hasLabel()` step associated with an edge."
"Another common item to filter on is the type or label associated with an edge. As with nodes, you can use the `hasLabel()` step associated with an edge."
]
},
{
Expand Down Expand Up @@ -523,7 +523,7 @@
"%%gremlin -d $node_labels\n",
"g.V()\n",
".hasLabel('person')\n",
".where(out().hasLabel('person').count().is(gte(2))) // <-- filter only people who have at 2 or more friend connections\n",
".where(out().hasLabel('person').count().is(gte(2))) // <-- filter only people who have 2 or more friend connections\n",
".outE()\n",
" .hasLabel('friends')\n",
".inV()\n",
Expand All @@ -537,7 +537,7 @@
"id": "c5abc463",
"metadata": {},
"source": [
"What if we wanted to get a list of all the restaurants in order to find out which cuisine's they serve? After all, all this learning has made me hungry!"
"What if we wanted to get a list of all the restaurants in order to find out which cuisines they serve? After all, all this learning has made me hungry!"
]
},
{
Expand Down Expand Up @@ -597,7 +597,7 @@
"id": "8b998cd1",
"metadata": {},
"source": [
"Because there are no properties associated to any of our edges, running the following query won't return any records. However, you can use it to see how the same concept of filtering nodes based on properties can be applied to edge.s"
"Because there are no properties associated to any of our edges, running the following query won't return any records. However, you can use it to see how the same concept of filtering nodes based on properties can be applied to edges."
]
},
{
Expand Down Expand Up @@ -768,7 +768,7 @@
"id": "0452038d",
"metadata": {},
"source": [
"**Note** When using `by()` after a `select()` you must specify the same number of `by()` statements as there are variables in the `select()`. Failing to doing so, will cause Gremlin to re-use whichever by() statements have been specified, starting with the first one. Now, this may not always be a problem, as we can see in the next example:"
"**Note** When using `by()` after a `select()` you must specify the same number of `by()` statements as there are variables in the `select()`. Failing to do so, will cause Gremlin to re-use whichever by() statements have been specified, starting with the first one. Now, this may not always be a problem, as we can see in the next example:"
]
},
{
Expand Down Expand Up @@ -823,7 +823,7 @@
"id": "943bdceb",
"metadata": {},
"source": [
"Notice in the above query how we've combined `project()` and `select()` to provide us with the same results. This is because to we've needed to alias specific portions of the incoming traversal, e.g. the node representing *Dave*, and the nodes representing Dave's *friends*.\n",
"Notice in the above query how we've combined `project()` and `select()` to provide us with the same results. This is because we've needed to alias specific portions of the incoming traversal, e.g. the node representing *Dave*, and the nodes representing Dave's *friends*.\n",
"\n",
"If we were to run the following query, you'll notice something very odd happen with the results."
]
Expand Down Expand Up @@ -912,7 +912,7 @@
"\n",
"In addition to returning simple key-value pairs, we can construct more complex responses. This is a common requirement, especially when returning aggregations or when returning attributes from different variables in the matched patterns.\n",
"\n",
"These new projections are created by using the `by()` step modulator (which is discussed more in the Loops-Repeats notebook). As we're previous seen, for each traversal step, we write a `by()` step to apply to it. The example below shows how we can return a custom string with the statement \"*person* is friends with *person*\"."
"These new projections are created by using the `by()` step modulator (which is discussed more in the Loops-Repeats notebook). As we've previously seen, for each traversal step, we write a `by()` step to apply to it. The example below shows how we can return a custom string with the statement \"*person* is friends with *person*\"."
]
},
{
Expand Down Expand Up @@ -1038,7 +1038,7 @@
"\n",
"* Find all the friends of friends that do not have a connection to Dave\n",
"\n",
"The correct answer contains three results: \"Hank\", \"Denise\", \"Paras\""
"The correct answer contains two results: \"Denise\", \"Paras\""
]
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
" \"restaurant\": {\n",
" \"color\": \"#ffe6cc\"\n",
" },\n",
" \"cusine\": {\n",
" \"cuisine\": {\n",
" \"color\": \"#fff2cc\"\n",
" }\n",
" }\n",
Expand All @@ -76,7 +76,7 @@
"id": "e5bebb15",
"metadata": {},
"source": [
"We'll be using the `node_labels` variable to provide a nicer visualisation when running the queries in this notebook. To use it, we need to pass it along with the query itself, as follows:\n",
"We'll be using the `node_labels` variable to provide a nicer visualization when running the queries in this notebook. To use it, we need to pass it along with the query itself, as follows:\n",
"\n",
"`%%gremlin -d node_labels`\n",
"\n",
Expand Down Expand Up @@ -132,7 +132,7 @@
"\n",
"The `repeat()` step also supports two 'modulators'; `until()` and `emit()`, which can be both used before or after the `repeat()` step. Using the `until()` step before the `repeat()` is similar to the common [`while...do`](https://www.w3schools.com/java/java_while_loop.asp) programming paradigm, whereas using the `until()` _after_ the `repeat()` is similar to the [`do...while`](https://www.w3schools.com/cpp/cpp_do_while_loop.asp) concept.\n",
"\n",
"The `emit()` modulator works by returning the results of a traversal as it is executed, and can be useful when used in conjunction with other looping-limiting steps such as `times()`. An example of this is the query below where we want to limit the `repeat()` to two hops, however we also want to return paths which include only one hops."
"The `emit()` modulator works by returning the results of a traversal as it is executed, and can be useful when used in conjunction with other looping-limiting steps such as `times()`. An example of this is the query below where we want to limit the `repeat()` to two hops, however we also want to return paths which include only one hop."
]
},
{
Expand Down Expand Up @@ -446,7 +446,7 @@
"\n",
"When repeating a traversal in Gremlin using the `repeat()` it's common to come across a pattern whereby the path loops back on itself. This is called a cyclic path, and can lead to your Gremlin queries looping forever.\n",
"\n",
"To stop this from occurring, it's good practise to include the `simplePath()` step. This removes paths with repeated objects, thus ensuring cyclic paths are not traversed.\n",
"To stop this from occurring, it's good practice to include the `simplePath()` step. This removes paths with repeated objects, thus ensuring cyclic paths are not traversed.\n",
"\n",
"**Important**. The `simplePath()` filters for repeated object based on the previous step, such as `in()` or `out()`.\n",
"\n",
Expand Down Expand Up @@ -481,14 +481,14 @@
"metadata": {},
"source": [
"\n",
"### Visualising Results in a Neptune Notebook\n",
"### Visualizing Results in a Neptune Notebook\n",
"\n",
"A key part of using any graph database is being able to visualise the way the objects stored within it are connected to each other. We've already shown how to do this in previous examples, however it's important to understand which of the Gremlin steps support this type of functionality.\n",
"A key part of using any graph database is being able to visualize the way the objects stored within it are connected to each other. We've already shown how to do this in previous examples, however it's important to understand which of the Gremlin steps support this type of functionality.\n",
"\n",
"* `path()` - used to provide access to all nodes and edges within each unique path traversed\n",
"* `simplePath()` - used to ensure we don't repeat a traversal across an object we've already covered (this can lead to infinite looping if the model supports circular references)\n",
"\n",
"If you're running this in a Neptune Notebook, we can use the `path()` step we tell the notebook to automatically present a visualisation of the output of a query. The following query returns 10 paths visualising the connections between `person`, `city`, `restaurant` and `cuisine`. Run the following query, and a graphical visualisation will automatically appear."
"If you're running this in a Neptune Notebook, we can use the `path()` step we tell the notebook to automatically present a visualization of the output of a query. The following query returns 10 paths visualizing the connections between `person`, `city`, `restaurant` and `cuisine`. Run the following query, and a graphical visualization will automatically appear."
]
},
{
Expand Down Expand Up @@ -595,7 +595,7 @@
"* Find the friends of that person (i.e. traverse the `friends` edge)\n",
"* Return the friends `first_name`\n",
"\n",
"The correct answer is a three results: \"Hank\", \"Denise\", \"Paras\""
"The correct answer is three results: \"Hank\", \"Denise\", \"Paras\""
]
},
{
Expand Down Expand Up @@ -676,7 +676,7 @@
"source": [
"## Conclusion\n",
"\n",
"In this notebook, we explored writing looping and repeat queries in Gremlin. These queries are a powerful and common way to explore connected data to answer questions, especially those where the exact number of connection is unknown. \n",
"In this notebook, we explored writing looping and repeat queries in Gremlin. These queries are a powerful and common way to explore connected data to answer questions, especially those where the exact number of connections are unknown. \n",
"\n",
"In the next notebook we will take what we have learned in this notebook and extend it to demonstrate how to order, group, and aggregate values in queries."
]
Expand All @@ -703,4 +703,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}
Loading
Loading