Giter Site home page Giter Site logo

pyneo4j-ogm's Introduction

pyneo4j-ogm

PyPI PyPI - Python Version PyPI - License PyPI - Downloads

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.

๐ŸŽฏ Features

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+ and 2+ fully supported.

๐Ÿ“ฃ Announcements

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.

๐Ÿ“ฆ Installation

Using pip:

pip install pyneo4j-ogm

or when using Poetry:

poetry add pyneo4j-ogm

๐Ÿš€ Quickstart

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.

Defining our data structures

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 and Coffee, and a relationship Consumed.
  • 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 found here.
  • The WithOptions function has been used to define constraints and indexes (more about them here) on model properties.

Creating a database client

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])

Interacting with the database

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!`

Full example

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.

๐Ÿ“š 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.

Table of contents

Running the test suite

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 deprecated ID.

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>

pyneo4j-ogm's People

Contributors

groc-prog avatar matmoncon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

clean-bb

pyneo4j-ogm's Issues

Polymorphism not working in relationships

Hey,
I have tried to implement multiple classes that inherit one from another and tried to connect them by relationships.
package version: >=0.4.0

The example below:

class VehicleNode(NodeModel):
    engine: str


class CarNode(VehicleNode):
    has_windows: bool


class Consumed(RelationshipModel):
    liters_consumed: int


class GasStationNode(NodeModel):
    volume_in_litres: int
    clients: RelationshipProperty[
        VehicleNode, Consumed
    ] = RelationshipProperty(
        target_model=VehicleNode,
        relationship_model=Consumed,
        direction=RelationshipPropertyDirection.OUTGOING,
        cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
        allow_multiple=True
    )

I tried to connect a CarNode and a GasStationNode with a basic relationship like the example below:

client: Pyneo4jClient # Already initialized
await client.register_models([VehicleNode, CarNode, GasStationNode, Consumed])
    
car = await CarNode(engine='v8', has_windows=True).create()
gas_station = await GasStationNode(volume_in_litres=6000).create()

# Trying to connect them by basic relationship
await gas_station.clients.connect(car, { 'liters_consumed': 50 })

The operation failed with an exception.
Exception print:

self = RelationshipProperty(target_model_name=VehicleNode, relationship_model=Consumed, direction=OUTGOING, cardinality=ZERO_OR_MORE, allow_multiple=True)
nodes = CarNode(element_id=4:218fdb95-3060-4d65-ab59-de3bc9a5722f:1, destroyed=False)

    def _ensure_alive(self, nodes: Union[T, List[T]]) -> None:
        """
        Ensures that the provided nodes are alive.
    
        Args:
            nodes (T | List[T]): Nodes to check for hydration and alive.
    
        Raises:
            InstanceNotHydrated: Raised if a node is not hydrated yet.
            InstanceDestroyed: Raised if a node has been marked as destroyed.
        """
        nodes_to_check = nodes if isinstance(nodes, list) else [nodes]
    
        for node in nodes_to_check:
            logger.debug(
                "Checking if node %s is alive and of correct type",
                getattr(node, "_element_id", None),
            )
            if getattr(node, "_element_id", None) is None or getattr(node, "_id", None) is None:
                raise InstanceNotHydrated()
    
            if getattr(node, "_destroyed", True):
                raise InstanceDestroyed()
    
            if cast(Type[T], self._target_model).__name__ != node.__class__.__name__:
>               raise InvalidTargetNode(
                    expected_type=cast(Type[T], self._target_model).__name__,
                    actual_type=node.__class__.__name__,
                )
E               pyneo4j_ogm.exceptions.InvalidTargetNode: Expected target node to be of type VehicleNode, but got CarNode

Memgraph Support?

I see that memgraph support is on the README -- wondering if any of the contributors to this package know off the top of their head the things that would need to change in this library to enable support for Memgraph.

At the very least, version parsing here needs to be updated (looks like memgraph returns v5 instead of a semver).

I really like and appreciate the use of Pydantic at the core of this library, and would be down to contribute this feature so I dont have to use gqlalchemy.

Multihop ModelInstance.find_connected_nodes raises TypeError when "*" explicitly passed to "$maxHops" param

Subject of the issue

When passing "*" explicitly as in snippet below, the following error is raised by pydantic:
TypeError: '>=' not supported between instances of 'str' and 'int'

It can be easily prevented by not passing the "*" and it will work correct, but in docs is mentined the possibility to do like this, so i decided to create an issue. I have ideas how to fix that, so i'll try in a few days and if i succeed i'll open a PR.

My environment

  • Version of pyneo4j-ogm = 0.5.0
  • Version of pydantic = 2.6.1
  • Version of python = 3.12

Steps to reproduce

Simple snippet to reproduce my issue:

import uuid

from pyneo4j_ogm import (
    Pyneo4jClient,
    NodeModel,
    RelationshipProperty,
    RelationshipPropertyCardinality,
    RelationshipPropertyDirection,
    RelationshipModel,
)


class Default(RelationshipModel):
    class Settings:
        type = "DEFAULT"


class Resource(NodeModel):
    ...


class Company(NodeModel):
    resources: RelationshipProperty["Resource", Default] = RelationshipProperty(
        target_model=Resource,
        relationship_model=Default,
        direction=RelationshipPropertyDirection.OUTGOING,
        cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
        allow_multiple=False,
    )


class User(NodeModel):
    companies: RelationshipProperty["Company", Default] = RelationshipProperty(
        target_model="Company",
        relationship_model=Default,
        direction=RelationshipPropertyDirection.OUTGOING,
        cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,
        allow_multiple=False,
    )


async def main():
    client = Pyneo4jClient()
    await client.connect(
        uri="neo4j://localhost:7687", auth=("neo4j", "password"), database="neo4j"
    )
    await client.register_models([Default, Resource, Company, User])
    user = User()
    await user.create()
    resource = Resource()
    await resource.create()
    company = Company()
    await company.create()
    await user.companies.connect(company)
    await company.resources.connect(resource)
    reachable_resources = await user.find_connected_nodes(  # here TypeError is raised
        {
            "$maxHops": "*",
            "$node": {
                "$labels": resource._settings.labels,
            },
        }
    )
    client.close()
    print(reachable_resources)


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

Expected behaviour

Output like below:
[Resource(element_id=4:0db21407-e480-4da7-9c4b-c9ddf43d9694:4, destroyed=False)]

Actual behaviour

TypeError: '>=' not supported between instances of 'str' and 'int'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.