Package: pyneo4j-ogm
pyneo4j-ogmpyneo4j-ogmis a asynchronousObject-Graph-MapperforNeo4j 5+andPython 3.10+. It is inspired bybeanieand build on top of proven technologies likePydantic 1.10+ and 2+and theNeo4j Python Driver. It saves you from writing ever-repeating boilerplate queries and allows you to focus on thestuff that actually matters. It is designed to be simple and easy to use, but also flexible and powerful.🎯 Featurespyneo4j-ogmhas a lot to offer, including:Fully typed: pyneo4j-ogm isfully typedout 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 moreadvanced use-caseswhile 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 theNeo4j Python Driver.Supports Neo4j 5+: pyneo4j-ogm supportsNeo4j 5+and is tested against the latest version of Neo4j.Multi-version Pydantic support: BothPydantic 1.10+and2+fully supported.📣 AnnouncementsThings to come in the future. Truly exiting stuff! If you have feature requests which you think might improvepyneo4j-ogm, feel free to open up a feature request.Auto-generated migrations📦 InstallationUsingpip:pipinstallpyneo4j-ogmor when usingPoetry:poetryaddpyneo4j-ogm🚀 QuickstartBefore 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 structuresSince every developer has a coffee addiction one way or another, we are going to useCoffeeandDevelopersfor this guide. So let's start by defining what our data should look like:frompyneo4j_ogmimport(NodeModel,RelationshipModel,RelationshipProperty,RelationshipPropertyCardinality,RelationshipPropertyDirection,WithOptions,)frompydanticimportFieldfromuuidimportUUID,uuid4classDeveloper(NodeModel):"""This class represents a `Developer` node inside the graph. All interactionswith nodes of this type will be handled by this class."""uid:WithOptions(UUID,unique=True)=Field(default_factory=uuid4)name:strage:intcoffee:RelationshipProperty["Coffee","Consumed"]=RelationshipProperty(target_model="Coffee",relationship_model="Consumed",direction=RelationshipPropertyDirection.OUTGOING,cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,allow_multiple=True,)classSettings:# Hooks are available for all methods that interact with the database.post_hooks={"coffee.connect":lambdaself,*args,**kwargs:print(f"{self.name}chugged another one!")}classCoffee(NodeModel):"""This class represents a node with the labels `Beverage` and `Hot`. Noticethat the labels of this model are explicitly defined in the `Settings` class."""flavor:strsugar:boolmilk:booldevelopers:RelationshipProperty["Developer","Consumed"]=RelationshipProperty(target_model=Developer,relationship_model="Consumed",direction=RelationshipPropertyDirection.INCOMING,cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,allow_multiple=True,)classSettings:labels={"Beverage","Hot"}classConsumed(RelationshipModel):"""Unlike the models above, this class represents a relationship between twonodes. In this case, it represents the relationship between the `Developer`and `Coffee` models. Like with node-models, the `Settings` class allows us todefine 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:boolclassSettings: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 modelsDeveloperandCoffee, and a relationshipConsumed.Some models define a special innerSettingsclass. This is used to customize the behavior of our models inside the graph. More on these settings can be found'here'.TheWithOptionsfunction has been used to defineconstraints and indexes(more about themhere) on model properties.Creating a database clientIn pyneo4j-ogm, the real work is done by a database client. One of these bad-boys can be created by initializing aPyneo4jClientinstance. But for models to work as expected, we have to let our client know that we want to use them like so:frompyneo4j_ogmimportPyneo4jClientasyncdefmain():# 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.awaitclient.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.awaitclient.register_models([Developer,Coffee,Consumed])Interacting with the databaseNow the fun stuff begins! We are ready to interact with our database. For the sake of thisquickstart guidewe 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 newDeveloperand someCoffeeand give him something to drink:# Imagine your models have been defined above...asyncdefmain():# 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)awaitjohn.create()cappuccino=Coffee(flavor="Cappuccino",milk=True,sugar=False)awaitcappuccino.create()# Here we create a new relationship between `john` and his `cappuccino`.# Additionally, we set the `liked` property of the relationship to `True`.awaitjohn.coffee.connect(cappuccino,{"liked":True})# Will print `John chugged another one!`Full exampleimportasynciofrompyneo4j_ogmimport(NodeModel,Pyneo4jClient,RelationshipModel,RelationshipProperty,RelationshipPropertyCardinality,RelationshipPropertyDirection,WithOptions,)frompydanticimportFieldfromuuidimportUUID,uuid4classDeveloper(NodeModel):"""This class represents a `Developer` node inside the graph. All interactionwith nodes of this type will be handled by this class."""uid:WithOptions(UUID,unique=True)=Field(default_factory=uuid4)name:strage:intcoffee:RelationshipProperty["Coffee","Consumed"]=RelationshipProperty(target_model="Coffee",relationship_model="Consumed",direction=RelationshipPropertyDirection.OUTGOING,cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,allow_multiple=True,)classSettings:# Hooks are available for all methods that interact with the database.post_hooks={"coffee.connect":lambdaself,*args,**kwargs:print(f"{self.name}chugged another one!")}classCoffee(NodeModel):"""This class represents a node with the labels `Beverage` and `Hot`. Noticethat the labels of this model are explicitly defined in the `Settings` class."""flavor:strsugar:boolmilk:booldevelopers:RelationshipProperty["Developer","Consumed"]=RelationshipProperty(target_model=Developer,relationship_model="Consumed",direction=RelationshipPropertyDirection.INCOMING,cardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,allow_multiple=True,)classSettings:labels={"Beverage","Hot"}classConsumed(RelationshipModel):"""Unlike the models above, this class represents a relationship between twonodes. In this case, it represents the relationship between the `Developer`and `Coffee` models. Like with node-models, the `Settings` class allows us todefine 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:boolclassSettings:type="CHUGGED"asyncdefmain():# We initialize a new `Pyneo4jClient` instance and connect to the database.client=Pyneo4jClient()awaitclient.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.awaitclient.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)awaitjohn.create()cappuccino=Coffee(flavor="Cappuccino",milk=True,sugar=False)awaitcappuccino.create()# Here we create a new relationship between `john` and his `cappuccino`.# Additionally, we set the `liked` property of the relationship to `True`.awaitjohn.coffee.connect(cappuccino,{"liked":True})# Will print `John chugged another one!`# Be a good boy and close your connections after you are done.awaitclient.close()asyncio.run(main())And that's it! You should now see aDeveloperand aHot/Beveragenode, connected by aCONSUMEDrelationship. If you want to learn more about the library, you can check out the fullDocumentation.📚 DocumentationIn the following we are going to take a closer look at the different parts ofpyneo4j-ogmand how to use them. We will cover everything pyneo4j-ogm has to offer, from thePyneo4jClientto theNodeModelandRelationshipModelclasses all the way to theQuery filtersandAuto-fetching relationship-properties.Table of contentspyneo4j-ogm🎯 Features📣 Announcements📦 Installation🚀 QuickstartDefining our data structuresCreating a database clientInteracting with the databaseFull example📚 DocumentationTable of contentsBasic conceptsA note on Pydantic version supportDatabase clientConnecting to the databaseClosing an existing connectionRegistering modelsExecuting Cypher queriesBatching cypher queriesUsing bookmarks (Enterprise Edition only)Manual indexing and constraintsClient utilitiesModelsIndexes, constraints and propertiesReserved propertiesConfiguration settingsNodeModel configurationRelationshipModel configurationAvailable methodsInstance.update()Instance.delete()Instance.refresh()Model.find_one()ProjectionsAuto-fetching nodesRaise on empty resultModel.find_many()FiltersProjectionsQuery optionsAuto-fetching nodesModel.update_one()Returning the updated entityRaise on empty resultModel.update_many()FiltersReturning the updated entityModel.delete_one()Raise on empty resultModel.delete_many()FiltersModel.count()FiltersNodeModelInstance.create()NodeModelInstance.find_connected_nodes()ProjectionsQuery optionsAuto-fetching nodesRelationshipModelInstance.start_node()RelationshipModelInstance.end_node()Serializing modelsHooksPre-hooksPost-hooksModel settingsRelationship-propertiesAvailable methodsRelationshipProperty.relationships()FiltersProjectionsQuery optionsRelationshipProperty.connect()RelationshipProperty.disconnect()Raise on empty resultRelationshipProperty.disconnect_all()RelationshipProperty.replace()RelationshipProperty.find_connected_nodes()FiltersProjectionsQuery optionsAuto-fetching nodesHooks with relationship propertiesQueriesFiltering queriesComparison operatorsString operatorsList operatorsLogical operatorsElement operatorsPattern matchingMulti-hop filtersProjectionsQuery optionsAuto-fetching relationship-propertiesMigrationsInitializing migrations for your projectCreating a new migrationRunning migrationsListing migrationsProgrammatic usageLoggingRunning the test suiteBasic conceptsAs you might have guessed by now,pyneo4j-ogmis a library that allows you to interact with a Neo4j database using Python. It is designed to make your life as simple as possible, while still providing the most common operations and some more advanced features.But first, how does this even work!?! Well, the basic concept boils down to the following:You define your models that represent your nodes and relationships inside the graph.You use these models to do all sorts of things with your data.Of course, there is a lot more to it than that, but this is the basic idea. So let's take a closer look at the different parts ofpyneo4j-ogmand how to use them.Note:All of the examples in this documentation assume that you have already connected to a database and registered your models with the client like shown in thequickstart guide. The models used in the following examples will build upon the ones defined there. If you are new toNeo4jorCypherin general, you should get a basic understanding of how to use them before continuing.A note on Pydantic version supportAs of versionv0.3.0, pyneo4j-ogm now supports bothPydantic 1.10+ and 2+. All core features of pydantic should work, meaning full support for model serialization, validation and schema generation.Should you find any issues or run into any problems, feel free to open a issue!Database clientThis is where the magic happens! ThePyneo4jClientis the main entry point for interacting with the database. It handles all the heavy lifting for you and your models. Because of this, we have to always have at least one client initialized before doing anything else.Connecting to the databaseBefore you can run any queries, you have to connect to a database. This is done by calling theconnect()method of thePyneo4jClientinstance. Theconnect()method takes a few arguments:uri: The connection URI to the database.skip_constraints: Whether the client should skip creating any constraints defined on models when registering them. Defaults toFalse.skip_indexes: Whether the client should skip creating any indexes defined on models when registering them. Defaults toFalse.*args: Additional arguments that are passed directly to Neo4j'sAsyncDriver.driver()method.**kwargs: Additional keyword arguments that are passed directly to Neo4j'sAsyncDriver.driver()method.frompyneo4j_ogmimportPyneo4jClientclient=Pyneo4jClient()awaitclient.connect(uri="<connection-uri-to-database>",auth=("<username>","<password>"),max_connection_pool_size=10,...)# Or chained right after the instantiation of the classclient=awaitPyneo4jClient().connect(uri="<connection-uri-to-database>",auth=("<username>","<password>"),max_connection_pool_size=10,...)After connecting the client, you will be able to run any cypher queries against the database. Should you try to run a query without connecting to a database first (it happens to the best of us), you will get aNotConnectedToDatabaseexception.Closing an existing connectionConnections can explicitly be closed by calling theclose()method. This will close the connection to the database and free up any resources used by the client. Remember to always close your connections when you are done with them!# Do some heavy-duty work...# Finally done, so we close the connection to the database.awaitclient.close()Once you closed the client, it will be seen asdisconnectedand if you try to run any further queries with it, you will get aNotConnectedToDatabaseexceptionRegistering modelsModels are a core feature of pyneo4j-ogm, and therefore you probably want to use some. But to work with them, they have to be registered with the client by calling theregister_models()method and passing in your models as a list:# Create a new client instance and connect ...awaitclient.register_models([Developer,Coffee,Consumed])This is a crucial step, because if you don't register your models with the client, you won't be able to work with them in any way. Should you try to work with a model that has not been registered, you will get aUnregisteredModelexception. This exception also gets raised if a database model defines a relationship-property with other (unregistered) models as a target or relationship model and then runs a query with said relationship-property.If you have defined any indexes or constraints on your models, they will be created automatically when registering them. You can prevent this behavior by passingskip_constraints=Trueorskip_indexes=Trueto theconnect()method. If you do this, you will have to create the indexes and constraints yourself.Note: If you don't register your models with the client, you will still be able to run cypher queries directly with the client, but you willlose automatic model resolutionfrom queries. This means that, instead of resolved models, the raw Neo4j query results are returned.Executing Cypher queriesModels aren't the only things capable of running queries. The client can also be used to run queries, with some additional functionality to make your life easier.Node- and RelationshipModels provide many methods for commonly used cypher queries, but sometimes you might want to execute a custom cypher with more complex logic. For this purpose, the client instance provides acypher()method that allows you to execute custom cypher queries. Thecypher()method takes three arguments:query: The cypher query to execute.parameters: A dictionary containing the parameters to pass to the query.resolve_models: Whether the client should try to resolve the models from the query results. Defaults toTrue.This method will always return a tuple containing a list of results and a list of variables returned by the query. Internally, the client uses the.values()method of the Neo4j driver to get the results of the query.Note:If no models have been registered with the client and resolve_models is set to True, the client will not raise any exceptions but rather return the raw query results.Here is an example of how to execute a custom cypher query:results,meta=awaitclient.cypher(query="CREATE (d:Developer {uid: '553ac2c9-7b2d-404e-8271-40426ae80de0', name: 'John', age: 25}) RETURN d.name as developer_name, d.age",parameters={"name":"John Doe"},resolve_models=False,# Explicitly disable model resolution)print(results)# [["John", 25]]print(meta)# ["developer_name", "d.age"]Batching cypher queriesWe provide an easy way to batch multiple database queries together, regardless of whether you are using the client directly or via a model method. To do this you can use thebatch()method, which has to be called with a asynchronous context manager like in the following example:asyncwithclient.batch():# All queries executed inside the context manager will be batched into a single transaction# and executed once the context manager exits. If any of the queries fail, the whole transaction# will be rolled back.awaitclient.cypher(query="CREATE (d:Developer {uid: $uid, name: $name, age: $age})",parameters={"uid":"553ac2c9-7b2d-404e-8271-40426ae80de0","name":"John Doe","age":25},)awaitclient.cypher(query="CREATE (c:Coffee {flavour: $flavour, milk: $milk, sugar: $sugar})",parameters={"flavour":"Espresso","milk":False,"sugar":False},)# Model queries also can be batched together without any extra work!coffee=awaitCoffee(flavour="Americano",milk=False,sugar=False).create()You can batch anything that runs a query, be that a model method, a custom query or a relationship-property method. If any of the queries fail, the whole transaction will be rolled back and an exception will be raised.Using bookmarks (Enterprise Edition only)If you are using the Enterprise Edition of Neo4j, you can use bookmarks to keep track of the last transaction that has been committed. The client provides alast_bookmarksproperty that allows you to get the bookmarks from the last session. These bookmarks can be used in combination with theuse_bookmarks()method. Like thebatch()method, theuse_bookmarks()method has to be called with a context manager. All queries run inside the context manager will use the bookmarks passed to theuse_bookmarks()method. Here is an example of how to use bookmarks:# Create a new node and get the bookmarks from the last sessionawaitclient.cypher("CREATE (d:Developer {name: 'John Doe', age: 25})")bookmarks=client.last_bookmarks# Create another node, but this time don't get the bookmark# When we use the bookmarks from the last session, this node will not be visibleawaitclient.cypher("CREATE (c:Coffee {flavour: 'Espresso', milk: False, sugar: False})")withclient.use_bookmarks(bookmarks=bookmarks):# All queries executed inside the context manager will use the bookmarks# passed to the `use_bookmarks()` method.# Here we will only see the node created in the first queryresults,meta=awaitclient.cypher("MATCH (n) RETURN n")# Model queries also can be batched together without any extra work!# This will return no results, since the coffee node was created after# the bookmarks were taken.coffee=awaitCoffee.find_many()print(coffee)# []Manual indexing and constraintsMost of the time, the creation of indexes/constraints will be handled by the models themselves. But it can still be handy to have a simple way of creating new ones. This is where thecreate_lookup_index(),create_range_index,create_text_index,create_point_indexandcreate_uniqueness_constraint()methods come in.First, let's take a look at how to create a custom index in the database. Thecreate_range_index,create_text_indexandcreate_point_indexmethods take a few arguments:name: The name of the index to create (Make sure this is unique!).entity_type: The entity type the index is created for. Can be eitherEntityType.NODEorEntityType.RELATIONSHIP.properties: A list of properties to create the index for.labels_or_type: The node labels or relationship type the index is created for.Thecreate_lookup_index()takes the same arguments, except for thelabels_or_typeandpropertiesarguments.Thecreate_uniqueness_constraint()method also takes similar arguments.name: The name of the constraint to create.entity_type: The entity type the constraint is created for. Can be eitherEntityType.NODEorEntityType.RELATIONSHIP.properties: A list of properties to create the constraint for.labels_or_type: The node labels or relationship type the constraint is created for.Here is an example of how to use the methods:# Creates a `RANGE` index for a `Coffee's` `sugar` and `flavour` propertiesawaitclient.create_range_index("hot_beverage_index",EntityType.NODE,["sugar","flavour"],["Beverage","Hot"])# Creates a UNIQUENESS constraint for a `Developer's` `uid` propertyawaitclient.create_uniqueness_constraint("developer_constraint",EntityType.NODE,["uid"],["Developer"])Client utilitiesThe client also provides some additional utility methods, which mostly exist for convenience when writing tests or setting up environments:is_connected(): Returns whether the client is currently connected to a database.drop_nodes(): Drops all nodes from the database.drop_constraints(): Drops all constraints from the database.drop_indexes(): Drops all indexes from the database.ModelsAs shown in thequickstart guide, models are the main building blocks ofpyneo4j-ogm. They represent the nodes and relationships inside the graph and provide a lot of useful methods for interacting with them.A core mechanic ofpyneo4j-ogmis serialization and deserialization of models. Every model method uses this mechanic under the hood to convert the models to and from the format used by the Neo4j driver.This is necessary because the Neo4j driver can only handle certain data types, which means models with custom or complex data types have to be serialized before they can be saved to the database. Additionally, Neo4j itself does not support nested data structures. To combat this, nested dictionaries and Pydantic models are serialized to a JSON string before being saved to the database.Filters for nested properties are also not supported, since they are stored as strings inside the database. This means that you can't use filters on nested properties when running queries with models. If you want to use filters on nested properties, you will to run a complex regular expression query.Indexes, constraints and propertiesSincepyneo4j-ogmis built on top ofPydantic, all of the features provided byPydanticare available to you. This includes definingpropertieson your models. For more information about these features, please refer to thePydantic documentation.On the other hand,indexes and constraintsare handled solely bypyneo4j-ogm. You can define indexes and constraints on your models by using theWithOptionsmethod wrapped around the type of the property. You can pass the following arguments to theWithOptionsmethod:property_type: The datatype of the property. Must be a validPydantictype.range_index: Whether to create a range index on the property. Defaults toFalse.text_index: Whether to create a text index on the property. Defaults toFalse.point_index: Whether to create a point index on the property. Defaults toFalse.unique: Whether to create a uniqueness constraint on the property. Defaults toFalse.Note:Using theWithOptionswithout any index or constraint options will behave just like it was never there (but in that case you should probably just remove it).frompyneo4j_ogmimportNodeModel,WithOptionsfrompydanticimportFieldfromuuidimportUUID,uuid4classDeveloper(NodeModel):"""A model representing a developer node in the graph."""# Using the `WithOptions` method on the type, we can still use all of the features provided by# `Pydantic` while also defining indexes and constraints on the property.uid:WithOptions(UUID,unique=True)=Field(default_factory=uuid4)name:WithOptions(str,text_index=True)# Has no effect, since no index or constraint options are passedage:WithOptions(int)There also is a special type of property calledRelationshipProperty. This property can be used to define relationships between models. For more information about this property, see theRelationship-propertiessection.Reserved propertiesNode- and RelationshipModels have a few pre-defined properties which reflect the entity inside the graph and are used internally in queries. These properties are:element_id: The element id of the entity inside the graph. This property is used internally to identify the entity inside the graph.id: The id of the entity inside the graph.modified_properties: A set of properties which have been modified on theTheRelationshipModelclass has some additional properties:start_node_element_id: The element id of the start node of the relationship.start_node_id: The ID of the start node of the relationship.end_node_element_id: The element id of the end node of the relationship.end_node_id: The ID of the end node of the relationship.These properties are implemented as class properties and allow you to access the graph properties of you models.Configuration settingsBothNodeModelandRelationshipModelprovide a few properties that can be configured. In this section we are going to take a closer look at how to configure your models and what options are available to you.Model configuration is done by defining a innerSettingsclass inside the model itself. The properties of this class control how the model is handled bypyneo4j-ogm:classCoffee(NodeModel):flavour:strsugar:boolmilk:boolclassSettings:# This is the place where the magic happens!NodeModel configurationTheSettingsclass of aNodeModelprovides the following properties:Setting nameTypeDescriptionpre_hooksDict[str, List[Callable]]A dictionary where the key is the name of the method for which to register the hook and the value is a list of hook functions. The hook function can be synchronous or asynchronous. All hook functions receive the exact same arguments as the method they are registered for and the current model instance as the first argument. Defaults to{}.post_hooksDict[str, List[Callable]]Same aspre_hooks, but the hook functions are executed after the method they are registered for. Additionally, the result of the method is passed to the hook as the second argument. Defaults to{}.labelsSet[str]A set of labels to use for the node. If no labels are defined, the name of the model will be used as the label. Defaults to themodel name split by it's words.auto_fetch_nodesboolWhether to automatically fetch nodes of defined relationship-properties when getting a model instance from the database. Auto-fetched nodes are available at theinstance.<relationship-property>.nodesproperty. If no specific models are passed to a method when this setting is set toTrue, nodes from all defined relationship-properties are fetched. Defaults toFalse.RelationshipModel configurationFor RelationshipModels, thelabelssetting is not available, since relationships don't have labels in Neo4j. Instead, thetypesetting can be used to define the type of the relationship. If no type is defined, the name of the model name will be used as the type.Setting nameTypeDescriptionpre_hooksDict[str, List[Callable]]A dictionary where the key is the name of the method for which to register the hook and the value is a list of hook functions. The hook function can be synchronous or asynchronous. All hook functions receive the exact same arguments as the method they are registered for and the current model instance as the first argument. Defaults to{}.post_hooksDict[str, List[Callable]]Same aspre_hooks, but the hook functions are executed after the method they are registered for. Additionally, the result of the method is passed to the hook as the second argument. Defaults to{}.typestrThe type of the relationship to use. If no type is defined, the model name will be used as the type. Defaults to themodel name in all uppercase.Note:Hooks can be defined for all native methods that interact with the database. When defining a hook for a method on a relationship-property, you have to pass a string in the format<relationship-property>.<method>as the key. For example, if you want to define a hook for theconnect()method of a relationship-property namedcoffee, you would have to passcoffee.connectas the key. This is true for both Node- and Relationship-models.Available methodsRunning cypher queries manually is nice and all, but something else running them for you is even better. That's exactly what the model methods are for. They allow you to do all sorts of things with your models and the nodes and relationships they represent. In this section we are going to take a closer look at the different methods available to you.But before we jump in, let's get one thing out of the way: All of the methods described in this section areasynchronousmethods. This means that they have to be awaited when called. If you are new to asynchronous programming in Python, you should take a look at theasyncio documentationbefore continuing.Note: The name of the heading for each method defines what type of model it is available on and whether it is aclass methodor aninstance method.Model.method(): Theclass methodis available on instances of bothNodeModelandRelationshipModelclasses.Instance.method(): Theinstance methodis available on instances of bothNodeModelandRelationshipModelclasses.<Type>Model.method(): Theclass methodis available on instances of the<Type>Modelclass.<Type>ModelInstance.method(): Theinstance methodis available on instances of the<Type>Modelclass.Instance.update()Theupdate()method can be used to sync the modified properties of a node or relationship-model with the corresponding entity inside the graph. All models also provide a property calledmodified_propertiesthat contains a set of all properties that have been modified since the model was created, fetched or synced with the database. This property is used by theupdate()method to determine which properties to sync with the database.# In this context, the developer `john` has been created before and the `name` property has been# not been changed since.# Maybe we want to name him James instead?john.name="James"print(john.modified_properties)# {"name"}# Will update the `name` property of the `john` node inside the graph# And suddenly he is James!awaitjohn.update()Instance.delete()Thedelete()method can be used to delete the graph entity tied to the current model instance. Once deleted, the model instance will be marked asdestroyedand any further operations on it will raise aInstanceDestroyedexception.# In this context, the developer `john` has been created before and is seen as `hydrated` (aka it# has been saved to the database before).# This will delete the `john` node inside the graph and mark your local instance as `destroyed`.awaitjohn.delete()awaitjohn.update()# Raises `InstanceDestroyed` exceptionInstance.refresh()Syncs your local instance with the properties from the corresponding graph entity. ´This method can be useful if you want to make sure that your local instance is always up-to-date with the graph entity.It is recommended to always call this method when importing a model instance from a dictionary (but does not have to be called necessarily, which in turn could cause a data inconsistency locally, so be careful when!).# Maybe we want to name him James instead?john.name="James"# Oh no, don't take my `john` away!awaitjohn.refresh()print(john.name)# 'John'Model.find_one()Thefind_one()method can be used to find a single node or relationship in the graph. If multiple results are matched, the first one is returned. This method returns a single instance/dictionary orNoneif no results were found.This method takes a mandatoryfiltersargument, which is used to filter the results. For more about filters, see theFiltering queriessection.# Return the first encountered node where the name property equals `John`.# This method always needs a filter to go with it!john_or_nothing=awaitDeveloper.find_one({"name":"John"})print(developer)# <Developer> or NoneProjectionsProjectionscan be used to only return specific parts of the model as a dictionary. This can help to reduce bandwidth or to just pre-filter the query results to a more suitable format. For more about projections, seeProjections# Return a dictionary with the developers name at the `dev_name` key instead# of a model instance.developer=awaitDeveloper.find_one({"name":"John"},{"dev_name":"name"})print(developer)# {"dev_name": "John"}Auto-fetching nodesTheauto_fetch_nodesandauto_fetch_modelsarguments can be used to automatically fetch all or selected nodes from defined relationship-properties when running thefind_one()query. The pre-fetched nodes are available on their respective relationship-properties. For more about auto-fetching, seeAuto-fetching relationship-properties.Note: Theauto_fetch_nodesandauto_fetch_modelsparameters are only available for classes which inherit from theNodeModelclass.# Returns a developer instance with `instance.<property>.nodes` properties already fetcheddeveloper=awaitDeveloper.find_one({"name":"John"},auto_fetch_nodes=True)print(developer.coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developer.other_property.nodes)# [<OtherModel>, <OtherModel>, ...]# Returns a developer instance with only the `instance.coffee.nodes` property already fetcheddeveloper=awaitDeveloper.find_one({"name":"John"},auto_fetch_nodes=True,auto_fetch_models=[Coffee])# Auto-fetch models can also be passed as stringsdeveloper=awaitDeveloper.find_one({"name":"John"},auto_fetch_nodes=True,auto_fetch_models=["Coffee"])print(developer.coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developer.other_property.nodes)# []Raise on empty resultBy default, thefind_one()method will returnNoneif no results were found. If you want to raise an exception instead, you can passraise_on_empty=Trueto the method.# Raises a `NoResultFound` exception if no results were founddeveloper=awaitDeveloper.find_one({"name":"John"},raise_on_empty=True)Model.find_many()Thefind_many()method can be used to find multiple nodes or relationships in the graph. This method always returns a list of instances/dictionaries or an empty list if no results were found.# Returns ALL `Developer` nodesdevelopers=awaitDeveloper.find_many()print(developers)# [<Developer>, <Developer>, <Developer>, ...]FiltersJust like thefind_one()method, thefind_many()method also takes (optional) filters. For more about filters, see theFiltering queriessection.# Returns all `Developer` nodes where the age property is greater than or# equal to 21 and less than 45.developers=awaitDeveloper.find_many({"age":{"$and":[{"$gte":21},{"$lt":45}]}})print(developers)# [<Developer>, <Developer>, <Developer>, ...]ProjectionsProjectionscan be used to only return specific parts of the models as dictionaries. For more information about projections, see theProjectionssection.# Returns dictionaries with the developers name at the `dev_name` key instead# of model instancesdevelopers=awaitDeveloper.find_many({"name":"John"},{"dev_name":"name"})print(developers)# [{"dev_name": "John"}, {"dev_name": "John"}, ...]Query optionsQuery optionscan be used to define how results are returned from the query. They are provided via theoptionsargument. For more about query options, see theQuery optionssection.# Skips the first 10 results and returns the next 20developers=awaitDeveloper.find_many({"name":"John"},options={"limit":20,"skip":10})print(developers)# [<Developer>, <Developer>, ...] up to 20 resultsAuto-fetching nodesTheauto_fetch_nodesandauto_fetch_modelsparameters can be used to automatically fetch all or selected nodes from defined relationship-properties when running thefind_many()query. For more about auto-fetching, seeAuto-fetching relationship-properties.Note: Theauto_fetch_nodesandauto_fetch_modelsparameters are only available for classes which inherit from theNodeModelclass.# Returns developer instances with `instance.<property>.nodes` properties already fetcheddevelopers=awaitDeveloper.find_many({"name":"John"},auto_fetch_nodes=True)print(developers[0].coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developers[0].other_property.nodes)# [<OtherModel>, <OtherModel>, ...]# Returns developer instances with only the `instance.coffee.nodes` property already fetcheddevelopers=awaitDeveloper.find_many({"name":"John"},auto_fetch_nodes=True,auto_fetch_models=[Coffee])# Auto-fetch models can also be passed as stringsdevelopers=awaitDeveloper.find_many({"name":"John"},auto_fetch_nodes=True,auto_fetch_models=["Coffee"])print(developers[0].coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developers[0].other_property.nodes)# []Model.update_one()Theupdate_one()method finds the first matching graph entity and updates it with the provided properties. If no match was found, nothing is updated andNoneis returned. Properties provided in the update parameter, which have not been defined on the model, will be ignored.This method takes two mandatory arguments:update: A dictionary containing the properties to update.filters: A dictionary containing the filters to use when searching for a match. For more about filters, see theFiltering queriessection.# Updates the `age` property to `30` in the first encountered node where the name property equals `John`# The `i_do_not_exist` property will be ignored since it has not been defined on the modeldeveloper=awaitDeveloper.update_one({"age":30,"i_do_not_exist":True},{"name":"John"})print(developer)# <Developer age=25># Or if no match was foundprint(developer)# NoneReturning the updated entityBy default, theupdate_one()method returns the model instance before the update. If you want to return the updated model instance instead, you can do so by passing thenewparameter to the method and setting it toTrue.# Updates the `age` property to `30` in the first encountered node where the name property equals `John`# and returns the updated nodedeveloper=awaitDeveloper.update_one({"age":30},{"name":"John"},True)print(developer)# <Developer age=30>Raise on empty resultBy default, theupdate_one()method will returnNoneif no results were found. If you want to raise an exception instead, you can passraise_on_empty=Trueto the method.# Raises a `NoResultFound` exception if no results were matcheddeveloper=awaitDeveloper.update_one({"age":30},{"name":"John"},raise_on_empty=True)Model.update_many()Theupdate_many()method finds all matching graph entity and updates them with the provided properties. If no match was found, nothing is updated and aempty listis returned. Properties provided in the update parameter, which have not been defined on the model, will be ignored.This method takes one mandatory argumentupdatewhich defines which properties to update with which values.# Updates the `age` property of all `Developer` nodes to 40developers=awaitDeveloper.update_many({"age":40})print(developers)# [<Developer age=25>, <Developer age=23>, ...]# Or if no matches were foundprint(developers)# []FiltersOptionally, afiltersargument can be provided, which defines which entities to update. For more about filters, see theFiltering queriessection.# Updates all `Developer` nodes where the age property is between `22` and `30`# to `40`developers=awaitDeveloper.update_many({"age":40},{"age":{"$gte":22,"$lte":30}})print(developers)# [<Developer age=25>, <Developer age=23>, ...]Returning the updated entityBy default, theupdate_many()method returns the model instances before the update. If you want to return the updated model instances instead, you can do so by passing thenewparameter to the method and setting it toTrue.# Updates all `Developer` nodes where the age property is between `22` and `30`# to `40` and return the updated nodesdevelopers=awaitDeveloper.update_many({"age":40},{"age":{"$gte":22,"$lte":30}})print(developers)# [<Developer age=40>, <Developer age=40>, ...]Model.delete_one()Thedelete_one()method finds the first matching graph entity and deletes it. Unlike others, this method returns the number of deleted entities instead of the deleted entity itself. If no match was found, nothing is deleted and0is returned.This method takes one mandatory argumentfilterswhich defines which entity to delete. For more about filters, see theFiltering queriessection.# Deletes the first `Developer` node where the name property equals `John`count=awaitDeveloper.delete_one({"name":"John"})print(count)# 1# Or if no match was foundprint(count)# 0Raise on empty resultBy default, thedelete_one()method will returnNoneif no results were found. If you want to raise an exception instead, you can passraise_on_empty=Trueto the method.# Raises a `NoResultFound` exception if no results were matchedcount=awaitDeveloper.delete_one({"name":"John"},raise_on_empty=True)Model.delete_many()Thedelete_many()method finds all matching graph entity and deletes them. Like thedelete_one()method, this method returns the number of deleted entities instead of the deleted entity itself. If no match was found, nothing is deleted and0is returned.# Deletes all `Developer` nodescount=awaitDeveloper.delete_many()print(count)# However many nodes matched the filter# Or if no match was foundprint(count)# 0FiltersOptionally, afiltersargument can be provided, which defines which entities to delete. For more about filters, see theFiltering queriessection.# Deletes all `Developer` nodes where the age property is greater than `65`count=awaitDeveloper.delete_many({"age":{"$gt":65}})print(count)# However many nodes matched the filterModel.count()Thecount()method returns the total number of entities of this model in the graph.# Returns the total number of `Developer` nodes inside the databasecount=awaitDeveloper.count()print(count)# However many nodes matched the filter# Or if no match was foundprint(count)# 0FiltersOptionally, afiltersargument can be provided, which defines which entities to count. For more about filters, see theFiltering queriessection.# Counts all `Developer` nodes where the name property contains the letters `oH`# The `i` in `icontains` means that the filter is case insensitivecount=awaitDeveloper.count({"name":{"$icontains":"oH"}})print(count)# However many nodes matched the filterNodeModelInstance.create()Note: This method is only available for classes inheriting from theNodeModelclass.Thecreate()method allows you to create a new node from a given model instance. All properties defined on the instance will be carried over to the corresponding node inside the graph. After this method has successfully finished, the instance saved to the database will be seen ashydratedand other methods such asupdate(),refresh(), etc. will be available.# Creates a node inside the graph with the properties and labels# from the model belowdeveloper=Developer(name="John",age=24)awaitdeveloper.create()print(developer)# <Developer uid="..." age=24, name="John">NodeModelInstance.find_connected_nodes()Note: This method is only available for classes inheriting from theNodeModelclass.Thefind_connected_nodes()method can be used to find nodes over multiple hops. It returns all matched nodes with the defined labels in the given hop range or an empty list if no nodes where found. The method requires you to define the labels of the nodes you want to find inside the filters (You can only define the labels ofone modelat a time). For more about filters, see theFiltering queriessection.# Picture a structure like this inside the graph:# (:Producer)-[:SELLS_TO]->(:Barista)-[:PRODUCES {with_love: bool}]->(:Coffee)-[:CONSUMED_BY]->(:Developer)# If we want to get all `Developer` nodes connected to a `Producer` node over the `Barista` and `Coffee` nodes,# where the `Barista` created the coffee with love, we can do so like this:producer=awaitProducer.find_one({"name":"Coffee Inc."})ifproducerisNone:# No producer found, do something elsedevelopers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],# You can use all available filters here as well},# You can define filters on specific relationships inside the path"$relationships":[{# Here we define a filter for all `PRODUCES` relationships# Only nodes where the with_love property is set to `True` will be returned"$type":"PRODUCES","with_love":True}]})print(developers)# [<Developer>, <Developer>, ...]# Or if no matches were foundprint(developers)# []ProjectionsProjectionscan be used to only return specific parts of the models as dictionaries. For more information about projections, see theProjectionssection.# Returns dictionaries with the developers name at the `dev_name` key instead# of model instancesdevelopers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],},"$relationships":[{"$type":"PRODUCES","with_love":True}]},{"dev_name":"name"})print(developers)# [{"dev_name": "John"}, {"dev_name": "John"}, ...]Query optionsQuery optionscan be used to define how results are returned from the query. They are provided via theoptionsargument. For more about query options, see theQuery optionssection.# Skips the first 10 results and returns the next 20developers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],},"$relationships":[{"$type":"PRODUCES","with_love":True}]},options={"limit":20,"skip":10})print(developers)# [<Developer>, <Developer>, ...]Auto-fetching nodesTheauto_fetch_nodesandauto_fetch_modelsparameters can be used to automatically fetch all or selected nodes from defined relationship-properties when running thefind_connected_nodes()query. For more about auto-fetching, seeAuto-fetching relationship-properties.# Skips the first 10 results and returns the next 20developers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],},"$relationships":[{"$type":"PRODUCES","with_love":True}]},auto_fetch_nodes=True)print(developers[0].coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developers[0].other_property.nodes)# [<OtherModel>, <OtherModel>, ...]# Returns developer instances with only the `instance.coffee.nodes` property already fetcheddevelopers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],},"$relationships":[{"$type":"PRODUCES","with_love":True}]},auto_fetch_nodes=True,auto_fetch_models=[Coffee])developers=awaitproducer.find_connected_nodes({"$node":{"$labels":["Developer","Python"],},"$relationships":[{"$type":"PRODUCES","with_love":True}]},auto_fetch_nodes=True,auto_fetch_models=["Coffee"])print(developers[0].coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developers[0].other_property.nodes)# []RelationshipModelInstance.start_node()Note: This method is only available for classes inheriting from theRelationshipModelclass.This method returns the start node of the current relationship instance. This method takes no arguments.# The `coffee_relationship` variable is a relationship instance created somewhere abovestart_node=awaitcoffee_relationship.start_node()print(start_node)# <Coffee>RelationshipModelInstance.end_node()Note: This method is only available for classes inheriting from theRelationshipModelclass.This method returns the end node of the current relationship instance. This method takes no arguments.# The `coffee_relationship` variable is a relationship instance created somewhere aboveend_node=awaitcoffee_relationship.end_node()print(end_node)# <Developer>Serializing modelsWhen serializing models to a dictionary or JSON string, the modelselement_id and idfields areautomatically addedto the corresponding dictionary/JSON string when calling Pydantic'sdict()orjson()methods.If you want to exclude them from serialization, you can easily do so by passing them to theexcludeparameter of the according method.On node-models:idelement_idAdditional properties for relationship-models:start_node_idstart_node_element_idend_node_idend_node_element_idHooksHooks are a convenient way to execute code before or after a method is called A pre-hook function always receives theclass it is used onas it's first argument andany arguments the decorated method receives. They can be used to execute code that is not directly related to the method itself, but still needs to be executed when the method is called. This allows for all sorts of things, such as logging, caching, etc.pyneo4j-ogmprovides a hooks for all available methods out of the box, and will even work for custom methods. Hooks are simply registered with the method name as the key and a list of hook functions as the value. The hook functions can be synchronous or asynchronous and will receive the exact same arguments as the method they are registered for and the current model instance as the first argument.For relationship-properties, the key under which the hook is registered has to be in the format<relationship-property>.<method>. For example, if you want to register a hook for theconnect()method of a relationship-property namedcoffee, you would have to passcoffee.connectas the key. Additionally, instead of theRelationshipProperty class context, the hook function will receive theNodeModel class contextof the model it has been called on as the first argument.Note:If you implement custom methods and want to use hooks for them, you can simply define thehook decoratoron them and then register hooks under thename of your method.Pre-hooksPre-hooks are executed before the method they are registered for. They can be defined in themodel's Settingsclass under thepre_hooksproperty or by calling theregister_pre_hooks()method on the model.classDeveloper(NodeModel):...classSettings:post_hooks={"coffee.connect":lambdaself,*args,**kwargs:print(f"{self.name}chugged another one!")}# Or by calling the `register_pre_hooks()` method# Here `hook_func` can be a synchronous or asynchronous function referenceDeveloper.register_pre_hooks("create",hook_func)# By using the `register_pre_hooks()` method, you can also overwrite all previously registered hooks# This will overwrite all previously registered hooks for the defined hook nameDeveloper.register_pre_hooks("create",hook_func,overwrite=True)Post-hooksPost-hooks are executed after the method they are registered for. They can be defined in themodel's Settingsclass under thepost_hooksproperty or by calling theregister_post_hooks()method on the model.In addition to the same arguments a pre-hook function receives, a post-hook function also receives the result of the method it is registered for as the second argument.Note:Since post-hooks have the exact same usage/registration options as pre-hooks, they are not explained in detail here.Model settingsCan be used to access the model's settings. For more about model settings, see theModel settingssection.model_settings=Developer.model_settings()print(model_settings)# <NodeModelSettings labels={"Developer"}, auto_fetch_nodes=False, ...>Relationship-propertiesNote: Relationship-properties are only available for classes which inherit from theNodeModelclass.Relationship-properties are a special type of property that can only be defined on aNodeModelclass. They can be used to define relationships between nodes and other models. They provide a variate of options to fine-tune the relationship and how it behaves. The options are pretty self-explanatory, but let's go through them anyway:classDeveloper(NodeModel):# Here we define a relationship to one or more `Coffee` nodes, both the target# and relationship-model can be defined as strings (Has to be the exact name of the model)# Notice that the `RelationshipProperty` class takes two type arguments, the first# one being the target model and the second one being the relationship-model# Can can get away without defining these, but it is recommended to do so for# better type hintingcoffee:RelationshipProperty["Coffee","Consumed"]=RelationshipProperty(# The target model is the model we want to connect totarget_model="Coffee",# The relationship-model is the model which defines the relationship# between a target model (in this case `Coffee`) and the model it is defined onrelationship_model=Consumed,# The direction of the relationship inside the graphdirection=RelationshipPropertyDirection.OUTGOING,# Cardinality defines how many nodes can be connected to the relationship# **Note**: This only softly enforces cardinality from the model it's defined on# and does not enforce it on the database levelcardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,# Whether to allow multiple connections to the same nodeallow_multiple=True,)Available methodsJust like regular models, relationship-properties also provide a few methods to make working with them easier. In this section we are going to take a closer look at the different methods available to you.Note: In the following, the termssource nodeandtarget nodewill be used. Source node refers to thenode instance the method is called onand target node refers to thenode/s passed to the method.RelationshipProperty.relationships()Returns the relationships between the source node and the target node. The method expects a single argumentnodewhich has to be the target node of the relationship. This always returns a list of relationship instances or an empty list if no relationships were found.# The `developer` and `coffee` variables have been defined somewhere above# Returns the relationships between the two nodescoffee_relationships=awaitdeveloper.coffee.relationships(coffee)print(coffee_relationships)# [<Consumed>, <Consumed>, ...]# Or if no relationships were foundprint(coffee_relationships)# []FiltersThis method also allows for (optional) filters. For more about filters, see theFiltering queriessection.# Only returns the relationships between the two nodes where# the `developer liked the coffee`coffee_relationships=awaitdeveloper.coffee.relationships(coffee,{"likes_it":True})print(coffee_relationships)# [<Consumed liked=True>, <Consumed liked=True>, ...]ProjectionsProjectionscan be used to only return specific parts of the models as dictionaries. For more information about projections, see theProjectionssection.# Returns dictionaries with the relationships `liked` property is at the# `loved_it` key instead of model instancescoffee_relationships=awaitdeveloper.coffee.relationships(coffee,projections={"loved_it":"liked"})print(coffee_relationships)# [{"loved_it": True}, {"loved_it": False}, ...]Query optionsQuery optionscan be used to define how results are returned from the query. They are provided via theoptionsargument. For more about query options, see theQuery optionssection.# Skips the first 10 results and returns the next 20coffee_relationships=awaitdeveloper.coffee.relationships(coffee,options={"limit":20,"skip":10})print(coffee_relationships)# [<Consumed>, <Consumed>, ...] up to 20 resultsRelationshipProperty.connect()Connects the given target node to the source node. The method expects the target node as the first argument, and optional properties as the second argument. The properties provided will be carried over to the relationship inside the graph.Depending on theallow_multipleoption, which is defined on the relationship-property, this method will either create a new relationship or update the existing one. If theallow_multipleoption is set toTrue, this method will always create a new relationship. Otherwise, the query will use aMERGEstatement to update an existing relationship.# The `developer` and `coffee` variables have been defined somewhere abovecoffee_relationship=awaitdeveloper.coffee.connect(coffee,{"likes_it":True})print(coffee_relationship)# <Consumed>RelationshipProperty.disconnect()Disconnects the target node from the source node and deletes all relationships between them. The only argument to the method is the target node. If no relationships exist between the two, nothing is deleted and0is returned. Otherwise, the number of deleted relationships is returned.Note: Ifallow_multiplewas set toTrueand multiple relationships to the target node exist, all of them will be deleted.# The `developer` and `coffee` variables have been defined somewhere abovecoffee_relationship_count=awaitdeveloper.coffee.disconnect(coffee)print(coffee_relationship_count)# However many relationships were deletedRaise on empty resultBy default, thedisconnect()method will returnNoneif no results were found. If you want to raise an exception instead, you can passraise_on_empty=Trueto the method.# Raises a `NoResultFound` exception if no results were matchedcoffee_relationship_count=awaitdeveloper.coffee.disconnect(coffee,raise_on_empty=True)RelationshipProperty.disconnect_all()Disconnects all target nodes from the source node and deletes all relationships between them. Returns the number of deleted relationships.# This will delete all relationships to `Coffee` nodes for this `Developer` nodecoffee_relationship_count=awaitdeveloper.coffee.disconnect_all()print(coffee_relationship_count)# However many relationships were deletedRelationshipProperty.replace()Disconnects all relationships from the source node to the old target node and connects them back to the new target node, carrying over all properties defined in the relationship. Returns the replaced relationships.Note: Ifmultiple relationshipsbetween the target node and the old source node exist,all of themwill be replaced.# Currently there are two relationships defined between the `developer` and `coffee_latte`# nodes where the `likes_it` property is set to `True` and `False` respectively# Moves the relationships from `coffee_latte` to `coffee_americano`replaced_coffee_relationships=awaitdeveloper.coffee.replace(coffee_latte,coffee_americano)print(replaced_coffee_relationships)# [<Consumed likes_it=True>, <Consumed likes_it=False>]RelationshipProperty.find_connected_nodes()Finds and returns all connected nodes for the given relationship-property. This method always returns a list of instances/dictionaries or an empty list if no results were found.# Returns all `Coffee` nodescoffees=awaitdeveloper.coffee.find_connected_nodes()print(coffees)# [<Coffee>, <Coffee>, ...]# Or if no matches were foundprint(coffees)# []FiltersYou can pass filters using thefiltersargument to filter the returned nodes. For more about filters, see theFiltering queriessection.# Returns all `Coffee` nodes where the `sugar` property is set to `True`coffees=awaitdeveloper.coffee.find_connected_nodes({"sugar":True})print(coffees)# [<Coffee sugar=True>, <Coffee sugar=True>, ...]ProjectionsProjectionscan be used to only return specific parts of the models as dictionaries. For more information about projections, see theProjectionssection.# Returns dictionaries with the coffee's `sugar` property at the `contains_sugar` key instead# of model instancescoffees=awaitdeveloper.coffee.find_connected_nodes({"sugar":True},{"contains_sugar":"sugar"})print(coffees)# [{"contains_sugar": True}, {"contains_sugar": False}, ...]Query optionsQuery optionscan be used to define how results are returned from the query. They are provided via theoptionsargument. For more about query options, see theQuery optionssection.# Skips the first 10 results and returns the next 20coffees=awaitdeveloper.coffee.find_connected_nodes({"sugar":True},options={"limit":20,"skip":10})# Skips the first 10 results and returns up to 20print(coffees)# [<Coffee>, <Coffee>, ...]Auto-fetching nodesTheauto_fetch_nodesandauto_fetch_modelsparameters can be used to automatically fetch all or selected nodes from defined relationship-properties when running thefind_many()query. For more about auto-fetching, seeAuto-fetching relationship-properties.# Returns coffee instances with `instance.<property>.nodes` properties already fetchedcoffees=awaitdeveloper.coffee.find_connected_nodes(auto_fetch_nodes=True)print(coffees[0].developer.nodes)# [<Developer>, <Developer>, ...]print(coffees[0].other_property.nodes)# [<OtherModel>, <OtherModel>, ...]# Returns coffee instances with only the `instance.developer.nodes` property already fetchedcoffees=awaitdeveloper.coffee.find_connected_nodes(auto_fetch_nodes=True,auto_fetch_models=[Developer])# Auto-fetch models can also be passed as stringscoffees=awaitdeveloper.coffee.find_connected_nodes(auto_fetch_nodes=True,auto_fetch_models=["Developer"])print(coffees[0].developer.nodes)# [<Developer>, <Developer>, ...]print(coffees[0].other_property.nodes)# []Hooks with relationship propertiesAlthough slightly different, hooks can also be registered for relationship-properties. The only different lies in the arguments passed to the hook function. Since relationship-properties are defined on aNodeModelclass, the hook function will receive theNodeModel class contextof the model it has been called on as the first argument instead of theRelationshipProperty class context(like it would for regular models).Note:The rest of the arguments passed to the hook function are the same as for regular models.classDeveloper(NodeModel):# Here we define a relationship to one or more `Coffee` nodes, both the target# and relationship-model can be defined as strings (Has to be the exact name of the model)# Notice that the `RelationshipProperty` class takes two type arguments, the first# one being the target model and the second one being the relationship-model# Can can get away without defining these, but it is recommended to do so for# better type hintingcoffee:RelationshipProperty["Coffee","Consumed"]=RelationshipProperty(# The target model is the model we want to connect totarget_model="Coffee",# The relationship-model is the model which defines the relationship# between a target model (in this case `Coffee`) and the model it is defined onrelationship_model=Consumed,# The direction of the relationship inside the graphdirection=RelationshipPropertyDirection.OUTGOING,# Cardinality defines how many nodes can be connected to the relationship# **Note**: This only softly enforces cardinality from the model it's defined on# and does not enforce it on the database levelcardinality=RelationshipPropertyCardinality.ZERO_OR_MORE,# Whether to allow multiple connections to the same nodeallow_multiple=True,)classSettings:post_hooks={"coffee.connect":lambdaself,*args,**kwargs:print(type(self))}# Somewhere further down the line...# Prints `<class '__main__.Developer'>` instead of `<class '__main__.RelationshipProperty'>`awaitdeveloper.coffee.connect(coffee)The reason for this change in the hooks behavior is simple, really. Since relationship-properties are only used to define relationships between nodes, it makes more sense to have theNodeModel class contextavailable inside the hook function instead of theRelationshipProperty class context, since the hook function will most likely be used to execute code on the model the relationship-property is defined on.QueriesAs you might have seen by now,pyneo4j-ogmprovides a variate of methods to query the graph. If you followed the documentation up until this point, you might have seen that most of the methods take afiltersargument.If you have someprior experiencewithNeo4j and Cypher, you may know that it does not provide a easy way to generate queries from given inputs. This is wherepyneo4j-ogmcomes in. It provides avariety of filtersto make querying the graph as easy as possible.The filters are heavily inspired byMongoDB's query language, so if you have some experience with that, you will feel right at home.This is really nice to have, not only for normal usage, but especially if you are developing agRPC serviceorREST APIand want to provide a way to query the graph from the outside.But enough of that, let's take a look at the different filters available to you.Filtering queriesSince the filters are inspired by MongoDB's query language, they are also very similar. The filters are defined as dictionaries, where the keys are the properties you want to filter on and the values are the values you want to filter for.We can roughly separate them into thefollowing categories:Comparison operatorsString operatorsList operatorsLogical operatorsElement operatorsComparison operatorsComparison operators are used to compare values to each other. They are the most basic type of filter.OperatorDescriptionCorresponding Cypher query$eqMatches values that are equal to a specified value.WHERE node.property = value$neqMatches all values that are not equal to a specified value.WHERE node.property <> value$gtMatches values that are greater than a specified value.WHERE node.property > value$gteMatches values that are greater than or equal to a specified value.WHERE node.property >= value$ltMatches values that are less than a specified value.WHERE node.property < value$lteMatches values that are less than or equal to a specified value.WHERE node.property <= valueString operatorsString operators are used to compare string values to each other.OperatorDescriptionCorresponding Cypher query$containsMatches values that contain a specified value.WHERE node.property CONTAINS value$icontainsMatches values that contain a specified case insensitive value.WHERE toLower(node.property) CONTAINS toLower(value)$startsWithMatches values that start with a specified value.WHERE node.property STARTS WITH value$istartsWithMatches values that start with a specified case insensitive value.WHERE toLower(node.property) STARTS WITH toLower(value)$endsWithMatches values that end with a specified value.WHERE node.property ENDS WITH value$iendsWithMatches values that end with a specified case insensitive value.WHERE toLower(node.property) ENDS WITH toLower(value)$regexMatches values that match a specified regular expression (Regular expressions used by Neo4j and Cypher).WHERE node.property =~ valueList operatorsList operators are used to compare list values to each other.OperatorDescriptionCorresponding Cypher query$inMatches lists where at least one item is in the given list.WHERE ANY(i IN node.property WHERE i IN value)$ninMatches lists where no items are in the given listWHERE NONE(i IN node.property WHERE i IN value)$allMatches lists where all items are in the given list.WHERE ALL(i IN node.property WHERE i IN value)$sizeMatches lists where the size of the list is equal to the given value.WHERE size(node.property) = valueNote: The$sizeoperator can also be combined with the comparison operators by nesting them inside the$sizeoperator. For example:{"$size": {"$gt": 5}}.Logical operatorsLogical operators are used to combine multiple filters with each other.OperatorDescriptionCorresponding Cypher query$andJoins query clauses with a logical AND returns all nodes that match the conditions of both clauses (Used by default if multiple filters are present).WHERE node.property1 = value1 AND node.property2 = value2$orJoins query clauses with a logical OR returns all nodes that match the conditions of either clause.WHERE node.property1 = value1 OR node.property2 = value2$xorJoins query clauses with a logical XOR returns all nodes that match the conditions of either clause but not both.WHERE WHERE node.property1 = value1 XOR node.property2 = value2$notInverts the effect of a query expression nested within and returns nodes that do not match the query expression.WHERE NOT (node.property = value)Element operatorsElement operators are a special kind of operator not available for every filter type. They are used to check Neo4j-specific values.OperatorDescriptionCorresponding Cypher query$existsMatches nodes that have the specified property.WHERE EXISTS(node.property)$elementIdMatches nodes that have the specified element id.WHERE elementId(node) = value$idMatches nodes that have the specified id.WHERE id(node) = value$labelsMatches nodes that have the specified labels.WHERE ALL(i IN labels(n) WHERE i IN value)$typeMatches relationships that have the specified type. Can be either a list or a string.For a string:WHERE type(r) = value, For a list:WHERE type(r) IN valuePattern matchingThe filters we have seen so far are great for simple queries, but what if we need to filter our nodes based on relationships to other nodes? This is wherepattern matchingcomes in. Pattern matching allows us to define apatternof nodes and relationships we want to match (or ignore). This is done by defining alist of patternsinside the$patternskey of the filter. Here is a short summary of the available operators inside a pattern:$node: Filters applied to the target node. Expects a dictionary containing basic filters.$relationship: Filters applied to the relationship between the source node and the target node. Expects a dictionary containing basic filters.$direction: The direction of the pattern. Can be either INCOMING,OUTGOING or BOTH.$exists: A boolean value indicating whether the pattern must exist or not.Note: The$patternskey can only be used inside theroot filterand not inside nested filters. Furthermore, only patterns across a single hop are supported.To make this as easy to understand as possible, we are going to take a look at a quick example. Let's say ourDevelopercan define relationships to hisCoffee. We want to get allDeveloperswhodon't drinktheir coffeewith sugar:developers=awaitDeveloper.find_many({"$patterns":[{# The `$exists` operator tells the library to match/ignore the pattern"$exists":False,# The defines the direction of the relationship inside the pattern"$direction":RelationshipMatchDirection.OUTGOING,# The `$node` key is used to define the node we want to filter for. This means# the filters inside the `$node` key will be applied to our `Coffee` nodes"$node":{"$labels":["Beverage","Hot"],"sugar":False},# The `$relationship` key is used to filter the relationship between the two nodes# It can also define property filters for the relationship"$relationship":{"$type":"CHUGGED"}}]})We can take this even further by defining multiple patters inside the$patternskey. Let's say this time ourDevelopercan have some otherDeveloperfriends and we want to get allDeveloperswho liked their coffee. At the same time, our developer must beFRIENDS_WITH(now the relationship is an incoming one, because why not?) a developer namedJenny:developers=awaitDeveloper.find_many({"$patterns":[{"$exists":True,"$direction":RelationshipMatchDirection.OUTGOING,"$node":{"$labels":["Beverage","Hot"],},"$relationship":{"$type":"CHUGGED","liked":True}},{"$exists":True,"$direction":RelationshipMatchDirection.INCOMING,"$node":{"$labels":["Developer"],"name":"Jenny"},"$relationship":{"$type":"FRIENDS_WITH"}}]})Multi-hop filtersMulti-hop filters are a special type of filter which is only available forNodeModelInstance.find_connected_nodes(). They allow you to specify filter parameters on the target node and all relationships between them over, you guessed it, multiple hops. To define this filter, you have a few operators you can define:$node: Filters applied to the target node. Expects a dictionary containing basic filters. Can not contain pattern yet.$minHops: The minimum number of hops between the source node and the target node. Must be greater than 0.$maxHops: The maximum number of hops between the source node and the target node. You can pass "*" as a value to define no upper limit. Must be greater than 1.$relationships: A list of relationship filters. Each filter is a dictionary containing basic filters and must define a $type operator.# Picture a structure like this inside the graph:# (:Producer)-[:SELLS_TO]->(:Barista)-[:PRODUCES {with_love: bool}]->(:Coffee)-[:CONSUMED_BY]->(:Developer)# If we want to get all `Developer` nodes connected to a `Producer` node over the `Barista` and `Coffee` nodes,# where the `Barista` created the coffee with love.# Let's say, for the sake of this example, that there are connections possible# with 10+ hops, but we don't want to include them. To solve this, we can define# a `$maxHops` filter with a value of `10`.producer=awaitProducer.find_one({"name":"Coffee Inc."})ifproducerisNone:# No producer found, do something elsedevelopers=awaitproducer.find_connected_nodes({"$maxHops":10,"$node":{"$labels":["Developer","Python"],# You can use all available filters here as well},# You can define filters on specific relationships inside the path"$relationships":[{# Here we define a filter for all `PRODUCES` relationships# Only nodes where the with_love property is set to `True` will be returned"$type":"PRODUCES","with_love":True}]})print(developers)# [<Developer>, <Developer>, ...]# Or if no matches were foundprint(developers)# []ProjectionsProjections are used to only return specific parts of the models as dictionaries. They are defined as a dictionary where the key is the name of the property in the returned dictionary and the value is the name of the property on the model instance.Projections can help you to reduce bandwidth usage and speed up queries, since you only return the data you actually need.Note:Only top-level mapping is supported. This means that you can not map properties to a nested dictionary key.In the following example, we will return a dictionary with adev_namekey, which get's mapped to the modelsnameproperty and adev_agekey, which get's mapped to the modelsageproperty. Any defined mapping which does not exist on the model will haveNoneas it's value. You can also map the result'selementIdandIdusing either$elementIdor$idas the value for the mapped key.developer=awaitDeveloper.find_one({"name":"John"},{"dev_name":"name","dev_age":"age","i_do_not_exist":"some_non_existing_property"})print(developer)# {"dev_name": "John", "dev_age": 24, "i_do_not_exist": None}Query optionsQuery options are used to define how results are returned from the query. They provide some basic functionality for easily implementing pagination, sorting, etc. They are defined as a dictionary where the key is the name of the option and the value is the value of the option. The following options are available:limit: Limits the number of returned results.skip: Skips the firstnresults.sort: Sorts the results by the given property. Can be either a string or a list of strings. If a list is provided, the results will be sorted by the first property and then by the second property, etc.order: Defines the sort direction. Can be eitherASCorDESC. Defaults toASC.# Returns 50 results, skips the first 10 and sorts them by the `name` property in descending orderdevelopers=awaitDeveloper.find_many({},options={"limit":50,"skip":10,"sort":"name","order":QueryOptionsOrder.DESCENDING})print(len(developers))# 50print(developers)# [<Developer>, <Developer>, ...]Auto-fetching relationship-propertiesYou have the option to automatically fetch all defined relationship-properties of matched nodes. This will populate theinstance.<property>.nodesattribute with the fetched nodes. This can be useful in situations where you need to fetch a specific node and get all of it's related nodes at the same time.Note: Auto-fetching nodes with many relationships can be very expensive and slow down your queries. Use it with caution.To enable this behavior, you can either set theauto_fetch_nodesparameter toTrueor set theauto_fetch_nodes settingin the model settings toTrue, but doing so willalways enable auto-fetching.You can also define which relationship-properties to fetch by providing the fetched models to theauto_fetch_modelsparameter. This can be useful if you only want to fetch specific relationship-properties.Now, let's take a look at an example:# Fetches everything defined in the relationship-properties of the current matched nodedeveloper=awaitDeveloper.find_one({"name":"John"},auto_fetch_nodes=True)# All nodes for all defined relationship-properties are now fetchedprint(developer.coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developer.developer.nodes)# [<Developer>, <Developer>, ...]print(developer.other_property.nodes)# [<OtherModel>, <OtherModel>, ...]With theauto_fetch_modelsparameter, we can define which relationship-properties to fetch:# Only fetch nodes for `Coffee` and `Developer` models defined in relationship-properties# The models can also be passed as strings, where the string is the model's namedeveloper=awaitDeveloper.find_one({"name":"John"},auto_fetch_nodes=True,auto_fetch_models=[Coffee,"Developer"])# Only the defined models have been fetchedprint(developer.coffee.nodes)# [<Coffee>, <Coffee>, ...]print(developer.developer.nodes)# [<Developer>, <Developer>, ...]print(developer.other_property.nodes)# []MigrationsAs of versionv0.5.0, pyneo4j-ogm supports migrations using a built-in migration tool. The migration tool is basic but flexibly, which should cover most use-cases.Initializing migrations for your projectTo initialize migrations for your project, you can use thepoetry run pyneo4j_ogm initcommand. This will create amigrationsdirectory at the given path (which defaults to./migrations), which will contain all your migration files.poetryrunpyneo4j_ogminit--migration-dir./my/custom/migration/pathCreating a new migrationTo create a new migration, you can use thepoetry run pyneo4j_ogm createcommand. This will create a new migration file inside themigrationsdirectory. The migration file will contain aupanddownfunction, which you can use to define your migration.poetryrunpyneo4j_ogmcreatemy_first_migrationBoth theupanddownfunctions will receive the client used during the migration as their only arguments. This makes the migrations pretty flexible, since you can not only use the client to execute queries, but also register models on it and use them to execute methods.Note: When using models inside the migration, you have to make sure that the model used implements the same data structure as the data inside the graph. Otherwise you might run into validation issues."""Auto-generated migration file {name}. Do notrename this file or the `up` and `down` functions."""frompyneo4j_ogmimportPyneo4jClientasyncdefup(client:Pyneo4jClient)->None:"""Write your `UP migration` here."""awaitclient.cypher("CREATE (n:Node {name: 'John'})")asyncdefdown(client:Pyneo4jClient)->None:"""Write your `DOWN migration` here."""awaitclient.cypher("MATCH (n:Node {name: 'John'}) DELETE n")Running migrationsTo run the migrations, you can use theupordowncommands. Theupcommand will run all migrations that have not been run yet, while thedowncommand will run all migrations in reverse order.Both commands support a--up-countor--down-countargument, which can be used to limit the number of migrations to run. By default, theupcommand will runall pending migrationand thedowncommand will roll back thelast migration.poetryrunpyneo4j_ogmup--up-count3poetryrunpyneo4j_ogmdown--down-count2Listing migrationsThe current state of all migrations can be viewed anytime using thestatuscommand. This will show you all migrations that have been run and all migrations that are pending.poetryrunpyneo4j_ogmstatus# Output┌─────────────────────────────────────────┬─────────────────────┐
│Migration│AppliedAt│
├─────────────────────────────────────────┼─────────────────────┤
│20160608155948-my_awesome_migration│2022-03-0415:40:22│
│20160608155948-my_fixed_migration│2022-03-0415:41:13│
│20160608155948-final_fix_i_swear│PENDING│
└─────────────────────────────────────────┴─────────────────────┘Programmatic usageThe migration tool can also be used programmatically. This can be useful if you want to run migrations inside your application or if you want to integrate the migration tool into your own CLI.importasynciofrompyneo4j_ogm.migrationsimportcreate,down,init,status,up# Call with same arguments as you would with cliinit(migration_dir="./my/custom/migration/path")create("my_first_migration")asyncio.run(up())LoggingYou can control the log level and whether to log to the console or not by setting thePYNEO4J_OGM_LOG_LEVELandPYNEO4J_OGM_ENABLE_LOGGINGas environment variables. The available levels are the same as provided by the build-inloggingmodule. The default log level isWARNINGand logging to the console is enabled by default.Running the test suiteTo run the test suite, you have to install the development dependencies and run the tests usingpytest. The tests are located in thetestsdirectory. Any tests located in thetests/integrationdirectory will require you to have a Neo4j instance running onlocalhost:7687with the credentials (neo4j:password). This can easily be done using the provideddocker-compose.ymlfile.poetryrunpytesttests--asyncio-mode=auto-Wignore::DeprecationWarningNote:The-W ignore::DeprecationWarningcan 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 run the following command locally:poetryaddpydantic@1.10