Skip to content

Commit

Permalink
added neo4j push and pull functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jkminder committed Mar 15, 2024
1 parent 1b1df7e commit f07c226
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 11 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ The library is built specifically for converting data into a [neo4j](https://neo


## Installation
If you have setup a private ssh key for your github, copy-paste the command below to install the latest version ([v1.1.0][latest_tag]):
If you have setup a private ssh key for your github, copy-paste the command below to install the latest version ([v1.2.0][latest_tag]):
```
pip install git+ssh://[email protected]/sg-dev/rel2graph@v1.1.0
pip install git+ssh://[email protected]/sg-dev/rel2graph@v1.2.0
```

If you don't have ssh set up, download the latest wheel [here][latest_wheel] and install the wheel with:
Expand Down Expand Up @@ -93,7 +93,7 @@ converter()
# Known issues
If you encounter a bug or an unexplainable behavior, please check the [known issues](https://github.com/sg-dev/rel2graph/labels/bug) list. If your issue is not found, submit a new one.

[latest_version]: v1.1.0
[latest_tag]: https://github.com/sg-dev/rel2graph/releases/tag/v1.1.0
[latest_wheel]: https://github.com/sg-dev/rel2graph/releases/download/v1.1.0/rel2graph-1.0.1-py3-none-any.whl
[latest_version]: v1.2.0
[latest_tag]: https://github.com/sg-dev/rel2graph/releases/tag/v1.2.0
[latest_wheel]: https://github.com/sg-dev/rel2graph/releases/download/v1.2.0/rel2graph-1.0.1-py3-none-any.whl
[wiki]: https://rel2graph.jkminder.ch/index.html
4 changes: 4 additions & 0 deletions docs/source/api/neo4j.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ These functions abstract complexity of interacting with Neo4j. Instead of writin

.. autofunction:: rel2graph.neo4j.merge

.. autofunction:: rel2graph.neo4j.push

.. autofunction:: rel2graph.neo4j.pull

.. autofunction:: rel2graph.neo4j.match_nodes

.. autofunction:: rel2graph.neo4j.match_relationships
Expand Down
4 changes: 2 additions & 2 deletions docs/source/neo4j.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ Neo4j Integration

The rel2graph library comes with a set of abstract classes that simplify the interaction with neo4j in python. They are derived from the now EOL library py2neo.
This includes python objects to represent |Node| and |Relationship| objects as well as a |Subgraph| object that can be used to represent a set of nodes and relationships.
|Node| and |Relationship| objects are themself a |Subgraph|. The two functions :py:func:`create <rel2graph.neo4j.create>` and :py:func:`merge <rel2graph.neo4j.create>` can be used to create or merge a |Subgraph| into a neo4j database given a neo4j session.
Further use the functions :py:func:`match_nodes <rel2graph.neo4j.match_nodes>` and :py:func:`match_relationships <rel2graph.neo4j.match_relationships>` to match elements in the graph and return a list of |Node| or |Relationship|.
|Node| and |Relationship| objects are themself a |Subgraph|. The functions :py:func:`create <rel2graph.neo4j.create>` and :py:func:`merge <rel2graph.neo4j.merge>` can be used to create or merge a |Subgraph| into a neo4j database given a neo4j session. To sync local
|Subgraph| objects with the database, use the :py:func:`push <rel2graph.neo4j.push>` and :py:func:`pull <rel2graph.neo4j.pull>` functions.Further use the functions :py:func:`match_nodes <rel2graph.neo4j.match_nodes>` and :py:func:`match_relationships <rel2graph.neo4j.match_relationships>` to match elements in the graph and return a list of |Node| or |Relationship|.
We refer to the :doc:`neo4j documentation <api/neo4j>` for more information.

.. |Subgraph| replace:: :py:class:`Subgraph <rel2graph.neo4j.Subgraph>`
Expand Down
20 changes: 20 additions & 0 deletions rel2graph/neo4j/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ def merge(graph: Subgraph, session: Session, primary_label=None, primary_key=Non
session.execute_write(graph.__db_merge__, primary_label=primary_label, primary_key=primary_key)


def push(graph: Subgraph, session: Session):
"""
Updates local graph elements with the database. The graph needs to be already in the database.
Args:
graph (Subgraph): The graph to create.
session (Session): The `session <https://neo4j.com/docs/api/python-driver/current/api.html#session>`_ to use.
"""
session.execute_write(graph.__db_push__)

def pull(graph: Subgraph, session: Session):
"""
Pulls remote changes to the graph to the local copy. The graph needs to be already in the database.
Args:
graph (Subgraph): The graph to create.
session (Session): The `session <https://neo4j.com/docs/api/python-driver/current/api.html#session>`_ to use.
"""
session.execute_write(graph.__db_pull__)


def match_nodes(session: Session, *labels: List[str], **properties: dict):
"""
Expand Down
59 changes: 58 additions & 1 deletion rel2graph/neo4j/graph_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
unwind_merge_nodes_query, \
unwind_create_relationships_query, \
unwind_merge_relationships_query, \
cypher_join
cypher_join, cypher_escape
from .encoder import encode_value, xstr, is_safe_key
from collections import OrderedDict

Expand Down Expand Up @@ -191,6 +191,7 @@ def __db_create__(self, tx):
for i, record in enumerate(records):
node = nodes[i]
node.identity = record[0]
node._remote_labels = frozenset(labels)
for r_type, relationships in rel_dict.items():
data = map(lambda r: [r.start_node.identity, dict(r), r.end_node.identity],
relationships)
Expand Down Expand Up @@ -260,6 +261,7 @@ def __db_merge__(self, tx, primary_label=None, primary_key=None):
for i, identity in enumerate(identities):
node = nodes[i]
node.identity = identity
node._remote_labels = frozenset(labels)
for (pk, r_type), relationships in rel_dict.items():
if pk is None:
raise ValueError("Primary key are required for relationship MERGE operation")
Expand All @@ -279,6 +281,60 @@ def __db_merge__(self, tx, primary_label=None, primary_key=None):
for i, identity in enumerate(identities):
relationship = relationships[i]
relationship.identity = identity

def __db_pull__(self, tx):
""" Copy data from a remote :class:`.Graph` into this
:class:`.Subgraph`.
:param tx:
"""
# Pull nodes
nodes = {}
for node in self.nodes:
if node.identity is not None:
nodes[node.identity] = node
query = tx.run("MATCH (_) WHERE id(_) in $x "
"RETURN id(_), labels(_), properties(_)", x=list(nodes.keys()))
for identity, new_labels, new_properties in query:
node = nodes[identity]
node.labels = set(new_labels)
node.properties = new_properties

# Pull relationships
relationships = {}
for relationship in self.relationships:
if relationship.identity is not None:
relationships[relationship.identity] = relationship
query = tx.run("MATCH ()-[_]->() WHERE id(_) in $x "
"RETURN id(_), properties(_)", x=list(relationships.keys()))
for identity, new_properties in query:
relationship = relationships[identity]
relationship.properties = new_properties

def __db_push__(self, tx):
""" Copy data into a remote :class:`.Graph` from this
:class:`.Subgraph`.
:param tx:
"""
for node in self.nodes:
if node.identity is not None:
clauses = ["MATCH (_) WHERE id(_) = $x", "SET _ = $y"]
parameters = {"x": node.identity, "y": dict(node)}
old_labels = node._remote_labels - node.labels
if old_labels:
clauses.append("REMOVE _:%s" % ":".join(map(cypher_escape, old_labels)))
new_labels = node.labels - node._remote_labels
if new_labels:
clauses.append("SET _:%s" % ":".join(map(cypher_escape, new_labels)))
tx.run("\n".join(clauses), parameters)
node._remote_labels = node.labels
for relationship in self.relationships:
if relationship.identity is not None:
clauses = ["MATCH ()-[_]->() WHERE id(_) = $x", "SET _ = $y"]
parameters = {"x": relationship.identity, "y": dict(relationship)}
tx.run("\n".join(clauses), parameters)

@property
def nodes(self):
""" The set of all nodes in this subgraph.
Expand Down Expand Up @@ -437,6 +493,7 @@ def __init__(self, *labels: str, **attributes: str) -> None:
attributes: Key value pairs of attributes for the Node
"""
self.labels = set(labels)
self._remote_labels = frozenset()
self.__primarylabel__ = labels[0]

PropertyDict.__init__(self, **attributes)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
setup(
name = "rel2graph",
packages = find_packages(),
version = "1.1.0",
version = "1.2.0",
description = "Library for converting relational data into graph data (neo4j)",
author = "Julian Minder",
author_email = "[email protected]",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import datetime
from neo4j import GraphDatabase, time

from rel2graph.neo4j import Node, Relationship, Subgraph, create, merge
from rel2graph.neo4j import Node, Relationship, Subgraph, create, merge, push, pull
from rel2graph.common_modules import MERGE_RELATIONSHIPS

@pytest.fixture
Expand All @@ -32,8 +32,9 @@ def session():
session.close()
driver.close()
return

def get_nodes(session):
return session.run("MATCH (c:test) RETURN c").data()
return session.run("MATCH (c:test) RETURN c, labels(c)").data()

def get_relationships(session):
return session.run("MATCH p=()-[d]->() RETURN p, properties(d) AS props").data()
Expand Down Expand Up @@ -315,3 +316,65 @@ def test_create_node_with_date(session):
assert(nodes[0]["c"]["id"] == 1)
assert(nodes[0]["c"]["date"] == time.Date(2020,1,1))

def test_push_node(session):
n1 = Node("test", id=1)
create(n1, session)
n1["id"] = 2
push(n1, session)
nodes = get_nodes(session)
assert(len(nodes) == 1)
assert(nodes[0]["c"]["id"] == 2)

# labels
n1.labels = set(["test", "another"])
push(n1, session)
nodes = get_nodes(session)
assert(len(nodes) == 1)
assert(nodes[0]["labels(c)"] == ["test", "another"])

# remove label
n1.labels = set(["test"])
push(n1, session)
nodes = get_nodes(session)
assert(len(nodes) == 1)
assert(nodes[0]["labels(c)"] == ["test"])

def test_pull_node(session):
n1 = Node("test", id=1)
create(n1, session)
n1["id"] = 2
pull(n1, session)
assert(n1["id"] == 1)

# remove label
n1.labels = set(["test", "another"])
pull(n1, session)
assert(n1.labels == {"test"})

# add label
n1.labels = set(["test", "another"])
push(n1, session)
n1.labels = set(["test"])
pull(n1, session)
assert(n1.labels == {"test", "another"})


def test_push_relationship(session):
n1 = Node("test", id=1)
n2 = Node("test", id=2)
r1 = Relationship(n1, "to", n2)
create(r1, session)
r1["id"] = 2
push(r1, session)
rels = get_relationships(session)
assert(len(rels) == 1)
assert(rels[0]["props"]["id"] == 2)

def test_pull_relationship(session):
n1 = Node("test", id=1)
n2 = Node("test", id=2)
r1 = Relationship(n1, "to", n2, id=1)
create(r1, session)
r1["id"] = 2
pull(r1, session)
assert(r1["id"] == 1)

0 comments on commit f07c226

Please sign in to comment.