pyneo4j-ogm
is a asynchronous Object-Graph-Mapper
for Neo4j 5+
and Python 3.10+
. It is inspired by beanie
and build on top of proven technologies like Pydantic 1.10+ and 2+
and the Neo4j Python Driver
. It saves you from writing ever-repeating boilerplate queries and allows you to focus on the stuff that actually matters
. It is designed to be simple and easy to use, but also flexible and powerful.
pyneo4j-ogm
has a lot to offer, including:
- Fully typed: pyneo4j-ogm is
fully typed
out of the box. - Powerful validation: Since we use Pydantic under the hood, you can use it's powerful validation and serialization features without any issues.
- Focus on developer experience: Designed to be simple to use, pyneo4j-ogm provides features for both simple queries and more
advanced use-cases
while keeping it's API as simple as possible. - Build-in migration tooling: Shipped with simple, yet flexible migration tooling.
- Fully asynchronous: Completely asynchronous code, thanks to the
Neo4j Python Driver
. - Supports Neo4j 5+: pyneo4j-ogm supports
Neo4j 5+
and is tested against the latest version of Neo4j. - Multi-version Pydantic support: Both
Pydantic 1.10+
and2+
fully supported.
Things to come in the future. Truly exiting stuff! If you have feature requests which you think might improve pyneo4j-ogm
, feel free to open up a feature request.
- MemGraph support.
Using pip
:
pip install pyneo4j-ogm
or when using Poetry
:
poetry add pyneo4j-ogm
Before we can get going, we have to take care of some things:
- We need to define our models, which will represent the nodes and relationships inside our database.
- We need a database client, which will do the actual work for us.
Since every developer has a coffee addiction one way or another, we are going to use Coffee
and Developers
for this guide. So let's start by defining what our data should look like:
from pyneo4j_ogm import (
NodeModel,
RelationshipModel,
RelationshipProperty,
RelationshipPropertyCardinality,
RelationshipPropertyDirection,
WithOptions,
)
from pydantic import Field
from uuid import UUID, uuid4
class Developer(NodeModel):
"""
This class represents a `Developer` node inside the graph. All interactions
with nodes of this type will be handled by this class.
"""
uid: WithOptions(UUID, unique=True) = Field(default_factory=uuid4)
name: str
age: int
coffee: RelationshipProperty["Coffee", "Consumed"] = RelationshipProperty(
target_model="Coffee",
relationship_model="Consumed",
direction=RelationshipPropertyDirection.OUTGOING,
cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
allow_multiple=True,
)
class Settings:
# Hooks are available for all methods that interact with the database.
post_hooks = {
"coffee.connect": lambda self, *args, **kwargs: print(f"{self.name} chugged another one!")
}
class Coffee(NodeModel):
"""
This class represents a node with the labels `Beverage` and `Hot`. Notice
that the labels of this model are explicitly defined in the `Settings` class.
"""
flavor: str
sugar: bool
milk: bool
developers: RelationshipProperty["Developer", "Consumed"] = RelationshipProperty(
target_model=Developer,
relationship_model="Consumed",
direction=RelationshipPropertyDirection.INCOMING,
cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
allow_multiple=True,
)
class Settings:
labels = {"Beverage", "Hot"}
class Consumed(RelationshipModel):
"""
Unlike the models above, this class represents a relationship between two
nodes. In this case, it represents the relationship between the `Developer`
and `Coffee` models. Like with node-models, the `Settings` class allows us to
define some configuration for this relationship.
Note that the relationship itself does not define it's start- and end-nodes,
making it reusable for other models as well.
"""
liked: bool
class Settings:
type = "CHUGGED"
Until now everything seems pretty standard if you have worked with other ORM's before. But if you haven't, we are going to go over what happened above:
- We defined 2 node models
Developer
andCoffee
, and a relationshipConsumed
. - Some models define a special inner
Settings
class. This is used to customize the behavior of our models inside the graph. More on these settings can be foundhere
. - The
WithOptions
function has been used to defineconstraints and indexes
(more about themhere
) on model properties.
In pyneo4j-ogm, the real work is done by a database client. One of these bad-boys can be created by initializing a Pyneo4jClient
instance. But for models to work as expected, we have to let our client know that we want to use them like so:
from pyneo4j_ogm import Pyneo4jClient
async def main():
# We initialize a new `Pyneo4jClient` instance and connect to the database.
client = Pyneo4jClient()
# Replace `<connection-uri-to-database>`, `<username>` and `<password>` with the
# actual values.
await client.connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"))
# To use our models for running queries later on, we have to register
# them with the client.
# **Note**: You only have to register the models that you want to use
# for queries and you can even skip this step if you want to use the
# `Pyneo4jClient` instance for running raw queries.
await client.register_models([Developer, Coffee, Consumed])
Now the fun stuff begins! We are ready to interact with our database. For the sake of this quickstart guide
we are going to keep it nice and simple, but this is just the surface of what pyneo4j-ogm has to offer.
We are going to create a new Developer
and some Coffee
and give him something to drink:
# Imagine your models have been defined above...
async def main():
# And your client has been initialized and connected to the database...
# We create a new `Developer` node and the `Coffee` he is going to drink.
john = Developer(name="John", age=25)
await john.create()
cappuccino = Coffee(flavor="Cappuccino", milk=True, sugar=False)
await cappuccino.create()
# Here we create a new relationship between `john` and his `cappuccino`.
# Additionally, we set the `liked` property of the relationship to `True`.
await john.coffee.connect(cappuccino, {"liked": True}) # Will print `John chugged another one!`
import asyncio
from pyneo4j_ogm import (
NodeModel,
Pyneo4jClient,
RelationshipModel,
RelationshipProperty,
RelationshipPropertyCardinality,
RelationshipPropertyDirection,
WithOptions,
)
from pydantic import Field
from uuid import UUID, uuid4
class Developer(NodeModel):
"""
This class represents a `Developer` node inside the graph. All interaction
with nodes of this type will be handled by this class.
"""
uid: WithOptions(UUID, unique=True) = Field(default_factory=uuid4)
name: str
age: int
coffee: RelationshipProperty["Coffee", "Consumed"] = RelationshipProperty(
target_model="Coffee",
relationship_model="Consumed",
direction=RelationshipPropertyDirection.OUTGOING,
cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
allow_multiple=True,
)
class Settings:
# Hooks are available for all methods that interact with the database.
post_hooks = {
"coffee.connect": lambda self, *args, **kwargs: print(f"{self.name} chugged another one!")
}
class Coffee(NodeModel):
"""
This class represents a node with the labels `Beverage` and `Hot`. Notice
that the labels of this model are explicitly defined in the `Settings` class.
"""
flavor: str
sugar: bool
milk: bool
developers: RelationshipProperty["Developer", "Consumed"] = RelationshipProperty(
target_model=Developer,
relationship_model="Consumed",
direction=RelationshipPropertyDirection.INCOMING,
cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
allow_multiple=True,
)
class Settings:
labels = {"Beverage", "Hot"}
class Consumed(RelationshipModel):
"""
Unlike the models above, this class represents a relationship between two
nodes. In this case, it represents the relationship between the `Developer`
and `Coffee` models. Like with node-models, the `Settings` class allows us to
define some settings for this relationship.
Note that the relationship itself does not define it's start- and end-nodes,
making it reusable for other models as well.
"""
liked: bool
class Settings:
type = "CHUGGED"
async def main():
# We initialize a new `Pyneo4jClient` instance and connect to the database.
client = Pyneo4jClient()
await client.connect(uri="<connection-uri-to-database>", auth=("<username>", "<password>"))
# To use our models for running queries later on, we have to register
# them with the client.
# **Note**: You only have to register the models that you want to use
# for queries and you can even skip this step if you want to use the
# `Pyneo4jClient` instance for running raw queries.
await client.register_models([Developer, Coffee, Consumed])
# We create a new `Developer` node and the `Coffee` he is going to drink.
john = Developer(name="John", age=25)
await john.create()
cappuccino = Coffee(flavor="Cappuccino", milk=True, sugar=False)
await cappuccino.create()
# Here we create a new relationship between `john` and his `cappuccino`.
# Additionally, we set the `liked` property of the relationship to `True`.
await john.coffee.connect(cappuccino, {"liked": True}) # Will print `John chugged another one!`
# Be a good boy and close your connections after you are done.
await client.close()
asyncio.run(main())
And that's it! You should now see a Developer
and a Hot/Beverage
node, connected by a CONSUMED
relationship. If you want to learn more about the library, you can check out the full Documentation
.
In the following we are going to take a closer look at the different parts of pyneo4j-ogm
and how to use them. We will cover everything pyneo4j-ogm has to offer, from the Pyneo4jClient
to the NodeModel
and RelationshipModel
classes all the way to the Query filters
and Auto-fetching relationship-properties
.
- pyneo4j-ogm
- ๐ฏ Features
- ๐ฃ Announcements
- ๐ฆ Installation
- ๐ Quickstart
- Running the test suite
- ๐ Documentation
- Basic concepts
- Database client
- Models
- Indexes, constraints and properties
- Reserved properties
- Configuration settings
- Available methods
- Instance.update()
- Instance.delete()
- Instance.refresh()
- Model.find_one()
- Model.find_many()
- Model.update_one()
- Model.update_many()
- Model.delete_one()
- Model.delete_many()
- Model.count()
- NodeModelInstance.create()
- NodeModelInstance.find_connected_nodes()
- RelationshipModelInstance.start_node()
- RelationshipModelInstance.end_node()
- Serializing models
- Hooks
- Model settings
- Relationship-properties
- Queries
- Migrations
- Logging
To run the test suite, you have to install the development dependencies and run the tests using pytest
. The tests are located in the tests
directory. Some tests will require you to have a Neo4j instance running on localhost:7687
with the credentials (neo4j:password
). This can easily be done using the provided docker-compose.yml
file.
poetry run pytest tests --asyncio-mode=auto -W ignore::DeprecationWarning
Note: The
-W ignore::DeprecationWarning
can be omitted but will result in a lot of deprication warnings by Neo4j itself about the usage of the now deprecatedID
.
As for running the tests with a different pydantic version, you can just install a different pydantic version with the following command:
poetry add pydantic@<version>