Skip to content

Commit

Permalink
Improve exception content; fix documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Marius Conjeaud committed Jun 5, 2024
1 parent 7c6662a commit 23fcd0c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 12 deletions.
23 changes: 14 additions & 9 deletions doc/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ additional relations with a single call::

# The following call will generate one MATCH with traversal per
# item in .fetch_relations() call
results = Person.nodes.all().fetch_relations('country')
results = Person.nodes.fetch_relations('country').all()
for result in results:
print(result[0]) # Person
print(result[1]) # associated Country
Expand All @@ -248,33 +248,38 @@ You can traverse more than one hop in your relations using the
following syntax::

# Go from person to City then Country
Person.nodes.all().fetch_relations('city__country')
Person.nodes.fetch_relations('city__country').all()

You can also force the use of an ``OPTIONAL MATCH`` statement using
the following syntax::

from neomodel.match import Optional

results = Person.nodes.all().fetch_relations(Optional('country'))
results = Person.nodes.fetch_relations(Optional('country')).all()

.. note::

Any relationship that you intend to traverse using this method **MUST have a model defined**, even if only the default StructuredRel, like::
class Person(StructuredNode):
country = RelationshipTo(Country, 'IS_FROM', model=StructuredRel)

Otherwise, neomodel will not be able to determine which relationship model to resolve into, and will fail.

.. note::

You can fetch one or more relations within the same call
to `.fetch_relations()` and you can mix optional and non-optional
relations, like::

Person.nodes.all().fetch_relations('city__country', Optional('country'))
Person.nodes.fetch_relations('city__country', Optional('country')).all()

.. note::

This feature is still a work in progress for extending path traversal and fecthing.
It currently stops at returning the resolved objects as they are returned in Cypher.
So for instance, if your path looks like ``(startNode:Person)-[r1]->(middleNode:City)<-[r2]-(endNode:Country)``,
If your path looks like ``(startNode:Person)-[r1]->(middleNode:City)<-[r2]-(endNode:Country)``,
then you will get a list of results, where each result is a list of ``(startNode, r1, middleNode, r2, endNode)``.
These will be resolved by neomodel, so ``startNode`` will be a ``Person`` class as defined in neomodel for example.

If you want to go further in the resolution process, you have to develop your own parser (for now).


Async neomodel
==============
Expand Down
7 changes: 6 additions & 1 deletion neomodel/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ class RelationshipClassNotDefined(ModelDefinitionException):

def __str__(self):
relationship_type = self.db_node_rel_class.type
return f"Relationship of type {relationship_type} does not resolve to any of the known objects\n{self._get_node_class_registry_formatted()}\n"
return f"""
Relationship of type {relationship_type} does not resolve to any of the known objects
{self._get_node_class_registry_formatted()}
Note that when using the fetch_relations method, the relationship type must be defined in the model, even if only defined to StructuredRel.
Otherwise, neomodel will not be able to determine which relationship model to resolve into.
"""


class RelationshipClassRedefined(ModelDefinitionException):
Expand Down
45 changes: 44 additions & 1 deletion test/async_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
IntegerProperty,
Q,
StringProperty,
UniqueIdProperty,
)
from neomodel._async_compat.util import AsyncUtil
from neomodel.async_.match import (
Expand All @@ -22,7 +23,7 @@
AsyncTraversal,
Optional,
)
from neomodel.exceptions import MultipleNodesReturned
from neomodel.exceptions import MultipleNodesReturned, RelationshipClassNotDefined


class SupplierRel(AsyncStructuredRel):
Expand Down Expand Up @@ -56,6 +57,28 @@ class Extension(AsyncStructuredNode):
extension = AsyncRelationshipTo("Extension", "extension")


class CountryX(AsyncStructuredNode):
code = StringProperty(unique_index=True, required=True)
inhabitant = AsyncRelationshipFrom("PersonX", "IS_FROM")


class CityX(AsyncStructuredNode):
name = StringProperty(required=True)
country = AsyncRelationshipTo(CountryX, "FROM_COUNTRY")


class PersonX(AsyncStructuredNode):
uid = UniqueIdProperty()
name = StringProperty(unique_index=True)
age = IntegerProperty(index=True, default=0)

# traverse outgoing IS_FROM relations, inflate to Country objects
country = AsyncRelationshipTo(CountryX, "IS_FROM")

# traverse outgoing LIVES_IN relations, inflate to City objects
city = AsyncRelationshipTo(CityX, "LIVES_IN")


@mark_async_test
async def test_filter_exclude_via_labels():
await Coffee(name="Java", price=99).save()
Expand Down Expand Up @@ -532,6 +555,7 @@ async def test_fetch_relations():
.fetch_relations("coffees__species")
.all()
)
assert len(result[0]) == 5
assert arabica in result[0]
assert robusta not in result[0]
assert tesco in result[0]
Expand Down Expand Up @@ -571,6 +595,25 @@ async def test_fetch_relations():
)


@mark_async_test
async def test_issue_795():
jim = await PersonX(name="Jim", age=3).save() # Create
jim.age = 4
await jim.save() # Update, (with validation)

germany = await CountryX(code="DE").save()
await jim.country.connect(germany)
berlin = await CityX(name="Berlin").save()
await berlin.country.connect(germany)
await jim.city.connect(berlin)

with raises(
RelationshipClassNotDefined,
match=r"[\s\S]*Note that when using the fetch_relations method, the relationship type must be defined in the model.*",
):
_ = await PersonX.nodes.fetch_relations("country").all()


@mark_async_test
async def test_in_filter_with_array_property():
tags = ["smoother", "sweeter", "chocolate", "sugar"]
Expand Down
45 changes: 44 additions & 1 deletion test/sync_/test_match_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
StringProperty,
StructuredNode,
StructuredRel,
UniqueIdProperty,
)
from neomodel._async_compat.util import Util
from neomodel.exceptions import MultipleNodesReturned
from neomodel.exceptions import MultipleNodesReturned, RelationshipClassNotDefined
from neomodel.sync_.match import NodeSet, Optional, QueryBuilder, Traversal


Expand Down Expand Up @@ -49,6 +50,28 @@ class Extension(StructuredNode):
extension = RelationshipTo("Extension", "extension")


class CountryX(StructuredNode):
code = StringProperty(unique_index=True, required=True)
inhabitant = RelationshipFrom("PersonX", "IS_FROM")


class CityX(StructuredNode):
name = StringProperty(required=True)
country = RelationshipTo(CountryX, "FROM_COUNTRY")


class PersonX(StructuredNode):
uid = UniqueIdProperty()
name = StringProperty(unique_index=True)
age = IntegerProperty(index=True, default=0)

# traverse outgoing IS_FROM relations, inflate to Country objects
country = RelationshipTo(CountryX, "IS_FROM")

# traverse outgoing LIVES_IN relations, inflate to City objects
city = RelationshipTo(CityX, "LIVES_IN")


@mark_sync_test
def test_filter_exclude_via_labels():
Coffee(name="Java", price=99).save()
Expand Down Expand Up @@ -521,6 +544,7 @@ def test_fetch_relations():
.fetch_relations("coffees__species")
.all()
)
assert len(result[0]) == 5
assert arabica in result[0]
assert robusta not in result[0]
assert tesco in result[0]
Expand Down Expand Up @@ -560,6 +584,25 @@ def test_fetch_relations():
)


@mark_sync_test
def test_issue_795():
jim = PersonX(name="Jim", age=3).save() # Create
jim.age = 4
jim.save() # Update, (with validation)

germany = CountryX(code="DE").save()
jim.country.connect(germany)
berlin = CityX(name="Berlin").save()
berlin.country.connect(germany)
jim.city.connect(berlin)

with raises(
RelationshipClassNotDefined,
match=r"[\s\S]*Note that when using the fetch_relations method, the relationship type must be defined in the model.*",
):
_ = PersonX.nodes.fetch_relations("country").all()


@mark_sync_test
def test_in_filter_with_array_property():
tags = ["smoother", "sweeter", "chocolate", "sugar"]
Expand Down

0 comments on commit 23fcd0c

Please sign in to comment.