Skip to content

Commit b9846b3

Browse files
Merge pull request #772 from neo4j-contrib/rc/5.2.1
Rc/5.2.1
2 parents 61a8118 + 70beab2 commit b9846b3

18 files changed

+245
-75
lines changed

.github/workflows/integration-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
strategy:
1616
fail-fast: false
1717
matrix:
18-
python-version: ["3.11", "3.10", "3.9", "3.8", "3.7"]
18+
python-version: ["3.12", "3.11", "3.10", "3.9", "3.8", "3.7"]
1919
neo4j-version: ["community", "enterprise", "5.5-enterprise", "4.4-enterprise", "4.4-community"]
2020

2121
steps:

Changelog

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
Version 5.2.1 2023-12
2+
* Add options to inspection script to skip heavy operations - rel props or cardinality inspection #767
3+
* Fixes database version parsing issues
4+
* Fixes bug when combining count with pagination #769
5+
* Bumps neo4j (driver) to 5.15.0
6+
17
Version 5.2.0 2023-11
28
* Add an option to pass your own driver instead of relying on the automatically created one. See set_connection method. NB : only accepts the synchronous driver for now.
39
* Add a close_connection method to explicitly close the driver to match Neo4j deprecation.

doc/source/configuration.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Adjust driver configuration - these options are only available for this connecti
3232
config.MAX_TRANSACTION_RETRY_TIME = 30.0 # default
3333
config.RESOLVER = None # default
3434
config.TRUST = neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES # default
35-
config.USER_AGENT = neomodel/v5.2.0 # default
35+
config.USER_AGENT = neomodel/v5.2.1 # default
3636

3737
Setting the database name, if different from the default one::
3838

doc/source/getting_started.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,18 @@ You can inspect an existing Neo4j database to generate a neomodel definition fil
8181
This will generate a file called ``models.py`` in the ``yourapp`` directory. This file can be used as a starting point,
8282
and will contain the necessary module imports, as well as class definition for nodes and, if relevant, relationships.
8383

84+
Ommitting the ``--db`` argument will default to the ``NEO4J_BOLT_URL`` environment variable. This is useful for masking
85+
your credentials.
86+
8487
Note that you can also print the output to the console instead of writing a file by omitting the ``--write-to`` option.
8588

89+
If you have a database with a large number of nodes and relationships,
90+
this script can take a long time to run (during our tests, it took 30 seconds for 500k nodes and 1.3M relationships).
91+
You can speed it up by not scanning for relationship properties and/or relationship cardinality, using these options :
92+
``--no-rel-props`` and ``--no-rel-cardinality``.
93+
Note that this will still add relationship definition to your nodes, but without relationship models ;
94+
and cardinality will be default (ZeroOrMore).
95+
8696
.. note::
8797

8898
This command will only generate the definition for nodes and relationships that are present in the
@@ -108,6 +118,9 @@ script (:ref:`neomodel_install_labels`) to automate this: ::
108118

109119
It is important to execute this after altering the schema and observe the number of classes it reports.
110120

121+
Ommitting the ``--db`` argument will default to the ``NEO4J_BOLT_URL`` environment variable. This is useful for masking
122+
your credentials.
123+
111124
Remove existing constraints and indexes
112125
=======================================
113126
Similarly, ``neomodel`` provides a script (:ref:`neomodel_remove_labels`) to automate the removal of all existing constraints and indexes from
@@ -117,6 +130,9 @@ the database, when this is required: ::
117130

118131
After executing, it will print all indexes and constraints it has removed.
119132

133+
Ommitting the ``--db`` argument will default to the ``NEO4J_BOLT_URL`` environment variable. This is useful for masking
134+
your credentials.
135+
120136
Create, Update, Delete operations
121137
=================================
122138

neomodel/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# pep8: noqa
2-
import pkg_resources
32

43
from neomodel.exceptions import *
54
from neomodel.match import EITHER, INCOMING, OUTGOING, NodeSet, Traversal

neomodel/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "5.2.0"
1+
__version__ = "5.2.1"

neomodel/match.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ class QueryAST:
323323
result_class: Optional[type]
324324
lookup: Optional[str]
325325
additional_return: Optional[list]
326+
is_count: Optional[bool]
326327

327328
def __init__(
328329
self,
@@ -337,6 +338,7 @@ def __init__(
337338
result_class: Optional[type] = None,
338339
lookup: Optional[str] = None,
339340
additional_return: Optional[list] = None,
341+
is_count: Optional[bool] = False,
340342
):
341343
self.match = match if match else []
342344
self.optional_match = optional_match if optional_match else []
@@ -349,6 +351,7 @@ def __init__(
349351
self.result_class = result_class
350352
self.lookup = lookup
351353
self.additional_return = additional_return if additional_return else []
354+
self.is_count = is_count
352355

353356

354357
class QueryBuilder:
@@ -649,15 +652,27 @@ def build_query(self):
649652
query += " ORDER BY "
650653
query += ", ".join(self._ast.order_by)
651654

652-
if self._ast.skip:
655+
# If we return a count with pagination, pagination has to happen before RETURN
656+
# It will then be included in the WITH clause already
657+
if self._ast.skip and not self._ast.is_count:
653658
query += f" SKIP {self._ast.skip}"
654659

655-
if self._ast.limit:
660+
if self._ast.limit and not self._ast.is_count:
656661
query += f" LIMIT {self._ast.limit}"
657662

658663
return query
659664

660665
def _count(self):
666+
self._ast.is_count = True
667+
# If we return a count with pagination, pagination has to happen before RETURN
668+
# Like : WITH my_var SKIP 10 LIMIT 10 RETURN count(my_var)
669+
self._ast.with_clause = f"{self._ast.return_clause}"
670+
if self._ast.skip:
671+
self._ast.with_clause += f" SKIP {self._ast.skip}"
672+
673+
if self._ast.limit:
674+
self._ast.with_clause += f" LIMIT {self._ast.limit}"
675+
661676
self._ast.return_clause = f"count({self._ast.return_clause})"
662677
# drop order_by, results in an invalid query
663678
self._ast.order_by = None

neomodel/scripts/neomodel_inspect_database.py

Lines changed: 78 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
If no file is specified, the tool will print the class definitions to stdout.
2020
2121
options:
22-
-h, --help show this help message and exit
23-
--db bolt://neo4j:neo4j@localhost:7687
22+
-h, --help show this help message and exit
23+
--db bolt://neo4j:neo4j@localhost:7687
2424
Neo4j Server URL
25-
-T, --write-to someapp/models.py
25+
-T, --write-to someapp/models.py
2626
File where to write output.
27+
--no-rel-props Do not inspect relationship properties
28+
--no-rel-cardinality Do not infer relationship cardinality
2729
"""
2830

2931
import argparse
@@ -116,13 +118,20 @@ def get_indexed_properties_for_label(label):
116118

117119
class RelationshipInspector:
118120
@classmethod
119-
def outgoing_relationships(cls, start_label):
120-
query = f"""
121-
MATCH (n:`{start_label}`)-[r]->(m)
122-
WITH DISTINCT type(r) as rel_type, head(labels(m)) AS target_label, keys(r) AS properties, head(collect(r)) AS sampleRel
123-
ORDER BY size(properties) DESC
124-
RETURN rel_type, target_label, apoc.meta.cypher.types(properties(sampleRel)) AS properties LIMIT 1
125-
"""
121+
def outgoing_relationships(cls, start_label, get_properties: bool = True):
122+
if get_properties:
123+
query = f"""
124+
MATCH (n:`{start_label}`)-[r]->(m)
125+
WITH DISTINCT type(r) as rel_type, head(labels(m)) AS target_label, keys(r) AS properties, head(collect(r)) AS sampleRel
126+
ORDER BY size(properties) DESC
127+
RETURN rel_type, target_label, apoc.meta.cypher.types(properties(sampleRel)) AS properties LIMIT 1
128+
"""
129+
else:
130+
query = f"""
131+
MATCH (n:`{start_label}`)-[r]->(m)
132+
WITH DISTINCT type(r) as rel_type, head(labels(m)) AS target_label
133+
RETURN rel_type, target_label, {{}} AS properties LIMIT 1
134+
"""
126135
result, _ = db.cypher_query(query)
127136
return [(record[0], record[1], record[2]) for record in result]
128137

@@ -222,7 +231,9 @@ def parse_imports():
222231
return imports
223232

224233

225-
def build_rel_type_definition(label, outgoing_relationships, defined_rel_types):
234+
def build_rel_type_definition(
235+
label, outgoing_relationships, defined_rel_types, infer_cardinality: bool = True
236+
):
226237
class_definition_append = ""
227238
rel_type_definitions = ""
228239

@@ -241,9 +252,12 @@ def build_rel_type_definition(label, outgoing_relationships, defined_rel_types):
241252
rel_type
242253
)
243254

244-
cardinality = RelationshipInspector.infer_cardinality(rel_type, label)
255+
cardinality_string = ""
256+
if infer_cardinality:
257+
cardinality = RelationshipInspector.infer_cardinality(rel_type, label)
258+
cardinality_string += f", cardinality={cardinality}"
245259

246-
class_definition_append += f' {clean_class_member_key(rel_name)} = RelationshipTo("{target_label}", "{rel_type}", cardinality={cardinality}'
260+
class_definition_append += f' {clean_class_member_key(rel_name)} = RelationshipTo("{target_label}", "{rel_type}"{cardinality_string}'
247261

248262
if rel_props and rel_type not in defined_rel_types:
249263
rel_model_name = generate_rel_class_name(rel_type)
@@ -265,7 +279,11 @@ def build_rel_type_definition(label, outgoing_relationships, defined_rel_types):
265279
return class_definition_append
266280

267281

268-
def inspect_database(bolt_url):
282+
def inspect_database(
283+
bolt_url,
284+
get_relationship_properties: bool = True,
285+
infer_relationship_cardinality: bool = True,
286+
):
269287
# Connect to the database
270288
print(f"Connecting to {bolt_url}")
271289
db.set_connection(bolt_url)
@@ -284,23 +302,32 @@ def inspect_database(bolt_url):
284302
indexed_properties = NodeInspector.get_indexed_properties_for_label(label)
285303

286304
class_definition = f"class {class_name}(StructuredNode):\n"
287-
class_definition += "".join(
288-
[
289-
build_prop_string(
290-
unique_properties, indexed_properties, prop, prop_type
291-
)
292-
for prop, prop_type in properties.items()
293-
]
294-
)
305+
if properties:
306+
class_definition += "".join(
307+
[
308+
build_prop_string(
309+
unique_properties, indexed_properties, prop, prop_type
310+
)
311+
for prop, prop_type in properties.items()
312+
]
313+
)
295314

296-
outgoing_relationships = RelationshipInspector.outgoing_relationships(label)
315+
outgoing_relationships = RelationshipInspector.outgoing_relationships(
316+
label, get_relationship_properties
317+
)
297318

298319
if outgoing_relationships and "StructuredRel" not in IMPORTS:
299320
IMPORTS.append("RelationshipTo")
300-
IMPORTS.append("StructuredRel")
321+
# No rel properties = no rel classes
322+
# Then StructuredRel import is not needed
323+
if get_relationship_properties:
324+
IMPORTS.append("StructuredRel")
301325

302326
class_definition += build_rel_type_definition(
303-
label, outgoing_relationships, defined_rel_types
327+
label,
328+
outgoing_relationships,
329+
defined_rel_types,
330+
infer_relationship_cardinality,
304331
)
305332

306333
if not properties and not outgoing_relationships:
@@ -353,6 +380,20 @@ def main():
353380
help="File where to write output.",
354381
)
355382

383+
parser.add_argument(
384+
"--no-rel-props",
385+
dest="get_relationship_properties",
386+
action="store_false",
387+
help="Do not inspect relationship properties",
388+
)
389+
390+
parser.add_argument(
391+
"--no-rel-cardinality",
392+
dest="infer_relationship_cardinality",
393+
action="store_false",
394+
help="Do not infer relationship cardinality",
395+
)
396+
356397
args = parser.parse_args()
357398

358399
bolt_url = args.neo4j_bolt_url
@@ -364,12 +405,22 @@ def main():
364405
# Before connecting to the database
365406
if args.write_to:
366407
with open(args.write_to, "w") as file:
367-
output = inspect_database(bolt_url=bolt_url)
408+
output = inspect_database(
409+
bolt_url=bolt_url,
410+
get_relationship_properties=args.get_relationship_properties,
411+
infer_relationship_cardinality=args.infer_relationship_cardinality,
412+
)
368413
print(f"Writing to {args.write_to}")
369414
file.write(output)
370415
# If no file is specified, print to stdout
371416
else:
372-
print(inspect_database(bolt_url=bolt_url))
417+
print(
418+
inspect_database(
419+
bolt_url=bolt_url,
420+
get_relationship_properties=args.get_relationship_properties,
421+
infer_relationship_cardinality=args.infer_relationship_cardinality,
422+
)
423+
)
373424

374425

375426
if __name__ == "__main__":

neomodel/util.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -679,15 +679,19 @@ def version_tag_to_integer(version_tag):
679679
"""
680680
Converts a version string to an integer representation to allow for quick comparisons between versions.
681681
682-
:param a_version_string: The version string to be converted (e.g. '3.4.0')
682+
:param a_version_string: The version string to be converted (e.g. '5.4.0')
683683
:type a_version_string: str
684-
:return: An integer representation of the version string (e.g. '3.4.0' --> 340)
684+
:return: An integer representation of the version string (e.g. '5.4.0' --> 50400)
685685
:rtype: int
686686
"""
687687
components = version_tag.split(".")
688688
while len(components) < 3:
689689
components.append("0")
690690
num = 0
691691
for index, component in enumerate(components):
692-
num += (10 ** ((len(components) - 1) - index)) * int(component)
692+
# Aura started adding a -aura suffix in version numbers, like "5.14-aura"
693+
# This will strip the suffix to allow for proper comparison : 14 instead of 14-aura
694+
if "-" in component:
695+
component = component.split("-")[0]
696+
num += (100 ** ((len(components) - 1) - index)) * int(component)
693697
return num

pyproject.toml

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,15 @@
1-
[build-system]
2-
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
3-
build-backend = "setuptools.build_meta"
4-
5-
[tool.setuptools_scm]
6-
7-
[tool.setuptools.packages.find]
8-
where = ["./"]
9-
101
[project]
112
name = "neomodel"
123
authors = [
134
{name = "Robin Edwards", email = "[email protected]"},
145
]
156
maintainers = [
7+
{name = "Marius Conjeaud", email = "[email protected]"},
168
{name = "Athanasios Anastasiou", email = "[email protected]"},
179
{name = "Cristina Escalante"},
18-
{name = "Marius Conjeaud", email = "[email protected]"},
1910
]
2011
description = "An object mapper for the neo4j graph database."
2112
readme = "README.md"
22-
requires-python = ">=3.7"
2313
keywords = ["graph", "neo4j", "ORM", "OGM", "mapper"]
2414
license = {text = "MIT"}
2515
classifiers = [
@@ -33,12 +23,10 @@ classifiers = [
3323
"Topic :: Database",
3424
]
3525
dependencies = [
36-
"neo4j==5.12.0",
37-
"pytz>=2021.1",
38-
"neobolt==1.7.17",
39-
"six==1.16.0",
26+
"neo4j~=5.15.0",
4027
]
41-
version='5.2.0'
28+
requires-python = ">=3.7"
29+
dynamic = ["version"]
4230

4331
[project.urls]
4432
documentation = "https://neomodel.readthedocs.io/en/latest/"
@@ -57,6 +45,16 @@ dev = [
5745
pandas = ["pandas"]
5846
numpy = ["numpy"]
5947

48+
[build-system]
49+
requires = ["setuptools>=68"]
50+
build-backend = "setuptools.build_meta"
51+
52+
[tool.setuptools.dynamic]
53+
version = {attr = "neomodel._version.__version__"}
54+
55+
[tool.setuptools.packages.find]
56+
where = ["./"]
57+
6058
[tool.pytest.ini_options]
6159
addopts = "--resetdb"
6260
testpaths = "test"

0 commit comments

Comments
 (0)