pyneoinstance 4.0.3__tar.gz → 4.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyneoinstance-4.0.3/pyneoinstance.egg-info → pyneoinstance-4.0.4}/PKG-INFO +1 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/__init__.py +1 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/database/neo4jdbms.py +65 -6
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4/pyneoinstance.egg-info}/PKG-INFO +1 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyproject.toml +1 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/test_neo4jdb.py +68 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/LICENSE +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/MANIFEST.in +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/README.md +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/database/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/database/context.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/fileload/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/fileload/helpers.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/fileload/loadyaml.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/py.typed +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/util/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance/util/functions.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance.egg-info/SOURCES.txt +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance.egg-info/dependency_links.txt +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance.egg-info/requires.txt +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/pyneoinstance.egg-info/top_level.txt +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/setup.cfg +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/setup.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/age_data.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/cypher_queries.yaml +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/load_files.yaml +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/movies.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/people.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/roles.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/test.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/test_fileload.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.4}/test/test_util_functions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyneoinstance
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.4
|
|
4
4
|
Summary: Handle Neo4j read and write transactions and provide methods for performing graph EDA
|
|
5
5
|
Author-email: "Jose A. Alvarado-Guzman" <jose.alvarado-guzman@neo4j.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -15,12 +15,15 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
15
15
|
import logging
|
|
16
16
|
import os
|
|
17
17
|
import re
|
|
18
|
+
import time
|
|
18
19
|
from pandas import DataFrame
|
|
19
20
|
from neo4j import GraphDatabase
|
|
20
21
|
from pyvis.network import Network
|
|
21
22
|
from neo4j_viz import VisualizationGraph
|
|
22
23
|
from neo4j_viz.neo4j import from_neo4j
|
|
23
24
|
from neo4j.exceptions import ServiceUnavailable
|
|
25
|
+
from neo4j.exceptions import SessionExpired
|
|
26
|
+
from neo4j.exceptions import TransientError
|
|
24
27
|
from neo4j.exceptions import AuthError
|
|
25
28
|
from neo4j.exceptions import ClientError
|
|
26
29
|
from neo4j.exceptions import ConfigurationError
|
|
@@ -34,6 +37,53 @@ _IMPLICIT_TX_RE = re.compile(
|
|
|
34
37
|
re.IGNORECASE,
|
|
35
38
|
)
|
|
36
39
|
|
|
40
|
+
# Errors treated as transient/retryable for auto-commit queries (run via
|
|
41
|
+
# session.run). session.execute_read/execute_write retry these automatically
|
|
42
|
+
# through the driver's managed-transaction logic; session.run does not.
|
|
43
|
+
_RETRYABLE_EXCEPTIONS = (ServiceUnavailable, SessionExpired, TransientError)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _run_autocommit_with_retry(session, consume, query, max_retries=5,
|
|
47
|
+
initial_delay=1.0, backoff_factor=2.0, **kwargs):
|
|
48
|
+
"""Run an auto-commit query (via session.run) with manual retry.
|
|
49
|
+
|
|
50
|
+
Queries using ``CALL { ... } IN TRANSACTIONS`` or
|
|
51
|
+
``IN CONCURRENT TRANSACTIONS`` are only valid in an implicit
|
|
52
|
+
(auto-commit) transaction, so they can't be executed through
|
|
53
|
+
session.execute_read/execute_write and don't benefit from the driver's
|
|
54
|
+
built-in managed-transaction retry behavior. This re-runs the query and
|
|
55
|
+
re-consumes its result from scratch on transient failures such as
|
|
56
|
+
ServiceUnavailable.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
session : neo4j.Session
|
|
61
|
+
The session to run the query on.
|
|
62
|
+
consume : Callable[[neo4j.Result], Any]
|
|
63
|
+
Function that consumes the Result object (e.g. reads values, builds
|
|
64
|
+
a graph, reads counters) and returns the desired output.
|
|
65
|
+
query : str
|
|
66
|
+
The Cypher query to run.
|
|
67
|
+
max_retries : int, optional
|
|
68
|
+
Maximum number of retry attempts after the initial try.
|
|
69
|
+
initial_delay : float, optional
|
|
70
|
+
Delay in seconds before the first retry.
|
|
71
|
+
backoff_factor : float, optional
|
|
72
|
+
Multiplier applied to the delay after each subsequent retry.
|
|
73
|
+
kwargs
|
|
74
|
+
Cypher query parameters.
|
|
75
|
+
"""
|
|
76
|
+
delay = initial_delay
|
|
77
|
+
for attempt in range(max_retries + 1):
|
|
78
|
+
try:
|
|
79
|
+
result = session.run(query, **kwargs)
|
|
80
|
+
return consume(result)
|
|
81
|
+
except _RETRYABLE_EXCEPTIONS:
|
|
82
|
+
if attempt == max_retries:
|
|
83
|
+
raise
|
|
84
|
+
time.sleep(delay)
|
|
85
|
+
delay *= backoff_factor
|
|
86
|
+
|
|
37
87
|
def _get_driver(neo_info : Dict[str, str]):
|
|
38
88
|
try:
|
|
39
89
|
if neo_info['encrypted']:
|
|
@@ -204,8 +254,11 @@ class Neo4jInstance:
|
|
|
204
254
|
if _IMPLICIT_TX_RE.search(query):
|
|
205
255
|
# 'CALL { ... } IN TRANSACTIONS' is only valid in an implicit
|
|
206
256
|
# (auto-commit) transaction, so it can't go through execute_read.
|
|
207
|
-
|
|
208
|
-
result =
|
|
257
|
+
# session.run isn't retried by the driver, so retry manually.
|
|
258
|
+
result = _run_autocommit_with_retry(
|
|
259
|
+
session,
|
|
260
|
+
lambda r: DataFrame(r.values(), columns=r.keys()),
|
|
261
|
+
query, **params)
|
|
209
262
|
else:
|
|
210
263
|
result = session.execute_read(
|
|
211
264
|
_read_transaction_function,query=query,**params)
|
|
@@ -256,9 +309,12 @@ class Neo4jInstance:
|
|
|
256
309
|
if _IMPLICIT_TX_RE.search(query):
|
|
257
310
|
# 'CALL { ... } IN TRANSACTIONS' is only valid in an implicit
|
|
258
311
|
# (auto-commit) transaction, so it can't go through execute_read.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
312
|
+
# session.run isn't retried by the driver, so retry manually.
|
|
313
|
+
def _consume_graph(result):
|
|
314
|
+
list(result) # consume records so .graph() is populated
|
|
315
|
+
return result.graph()
|
|
316
|
+
graph = _run_autocommit_with_retry(
|
|
317
|
+
session, _consume_graph, query, **params)
|
|
262
318
|
else:
|
|
263
319
|
graph = session.execute_read(
|
|
264
320
|
_graph_transaction_function, query=query, **params)
|
|
@@ -775,7 +831,10 @@ class Neo4jInstance:
|
|
|
775
831
|
if _IMPLICIT_TX_RE.search(query):
|
|
776
832
|
# 'CALL { ... } IN TRANSACTIONS' is only valid in an implicit
|
|
777
833
|
# (auto-commit) transaction, so it can't go through execute_write.
|
|
778
|
-
|
|
834
|
+
# session.run isn't retried by the driver, so retry manually.
|
|
835
|
+
results = _run_autocommit_with_retry(
|
|
836
|
+
session, lambda r: r.consume().counters.__dict__,
|
|
837
|
+
query, **kwargs)
|
|
779
838
|
else:
|
|
780
839
|
results = session.execute_write(
|
|
781
840
|
_write_transaction_function, query,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyneoinstance
|
|
3
|
-
Version: 4.0.
|
|
3
|
+
Version: 4.0.4
|
|
4
4
|
Summary: Handle Neo4j read and write transactions and provide methods for performing graph EDA
|
|
5
5
|
Author-email: "Jose A. Alvarado-Guzman" <jose.alvarado-guzman@neo4j.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "pyneoinstance"
|
|
9
|
-
version = "4.0.
|
|
9
|
+
version = "4.0.4"
|
|
10
10
|
description = "Handle Neo4j read and write transactions and provide methods for performing graph EDA"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
authors = [{ name = "Jose A. Alvarado-Guzman",email = "jose.alvarado-guzman@neo4j.com" }]
|
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
"""
|
|
11
11
|
import os
|
|
12
12
|
import unittest
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from unittest.mock import patch
|
|
15
|
+
import neo4j
|
|
13
16
|
import pandas as pd
|
|
14
17
|
import numpy as np
|
|
15
18
|
import pandas.testing as pd_testing
|
|
16
19
|
from neo4j.exceptions import ClientError
|
|
20
|
+
from neo4j.exceptions import ServiceUnavailable
|
|
17
21
|
from neo4j_viz import VisualizationGraph
|
|
18
22
|
from .context import database
|
|
19
23
|
from .context import fileload
|
|
@@ -40,6 +44,32 @@ def _clean_db(graph, db):
|
|
|
40
44
|
["CALL apoc.schema.assert({},{})", "MATCH(n) DETACH DELETE n"], db)
|
|
41
45
|
|
|
42
46
|
|
|
47
|
+
@contextmanager
|
|
48
|
+
def _patch_flaky_session_run(fail_times=1):
|
|
49
|
+
"""Patch neo4j.Session.run so the first `fail_times` calls raise a
|
|
50
|
+
transient ServiceUnavailable, then delegate to the real driver.
|
|
51
|
+
|
|
52
|
+
Used to verify that 'IN TRANSACTIONS'/'IN CONCURRENT TRANSACTIONS'
|
|
53
|
+
queries (which go through session.run rather than execute_read/
|
|
54
|
+
execute_write, and so don't get the driver's built-in managed-
|
|
55
|
+
transaction retries) are retried by the manual retry wrapper instead
|
|
56
|
+
of failing outright. Also patches time.sleep so the retry backoff
|
|
57
|
+
doesn't slow the test down.
|
|
58
|
+
"""
|
|
59
|
+
original_run = neo4j.Session.run
|
|
60
|
+
call_count = {'n': 0}
|
|
61
|
+
|
|
62
|
+
def flaky_run(session_self, *args, **kwargs):
|
|
63
|
+
call_count['n'] += 1
|
|
64
|
+
if call_count['n'] <= fail_times:
|
|
65
|
+
raise ServiceUnavailable('simulated transient failure')
|
|
66
|
+
return original_run(session_self, *args, **kwargs)
|
|
67
|
+
|
|
68
|
+
with patch.object(neo4j.Session, 'run', flaky_run), \
|
|
69
|
+
patch.object(database.neo4jdbms.time, 'sleep', return_value=None):
|
|
70
|
+
yield call_count
|
|
71
|
+
|
|
72
|
+
|
|
43
73
|
class TestWriteMethods(unittest.TestCase):
|
|
44
74
|
"""Test write operations. Each test gets a clean database."""
|
|
45
75
|
|
|
@@ -125,6 +155,24 @@ class TestWriteMethods(unittest.TestCase):
|
|
|
125
155
|
self.assertEqual(
|
|
126
156
|
{'nodes_created': 3, 'labels_added': 3, 'properties_set': 3}, result)
|
|
127
157
|
|
|
158
|
+
def test_execute_write_query_in_transactions_retries_on_transient_failure(self):
|
|
159
|
+
"""An 'IN TRANSACTIONS' write query is retried and still succeeds
|
|
160
|
+
after a transient ServiceUnavailable failure, since it goes through
|
|
161
|
+
session.run (not execute_write) and isn't covered by the driver's
|
|
162
|
+
built-in managed-transaction retries."""
|
|
163
|
+
query = """
|
|
164
|
+
UNWIND range(1, 3) AS i
|
|
165
|
+
CALL (i) {
|
|
166
|
+
CREATE (:Actor {id: i})
|
|
167
|
+
} IN TRANSACTIONS OF 1 ROWS
|
|
168
|
+
"""
|
|
169
|
+
with _patch_flaky_session_run(fail_times=1) as call_count:
|
|
170
|
+
result = self.graph.execute_write_query(query, self.db)
|
|
171
|
+
|
|
172
|
+
self.assertEqual(call_count['n'], 2)
|
|
173
|
+
self.assertEqual(
|
|
174
|
+
{'nodes_created': 3, 'labels_added': 3, 'properties_set': 3}, result)
|
|
175
|
+
|
|
128
176
|
|
|
129
177
|
class TestReadAndEdaMethods(unittest.TestCase):
|
|
130
178
|
"""Test read and EDA methods. Data is loaded once for the entire class."""
|
|
@@ -203,6 +251,26 @@ class TestReadAndEdaMethods(unittest.TestCase):
|
|
|
203
251
|
result = self.graph.execute_read_query(query, self.db)
|
|
204
252
|
self.assertEqual(plain_count, result['row_num'][0])
|
|
205
253
|
|
|
254
|
+
def test_execute_read_query_in_transactions_retries_on_transient_failure(self):
|
|
255
|
+
"""An 'IN TRANSACTIONS' read query is retried and still succeeds
|
|
256
|
+
after a transient ServiceUnavailable failure, since it goes through
|
|
257
|
+
session.run (not execute_read) and isn't covered by the driver's
|
|
258
|
+
built-in managed-transaction retries."""
|
|
259
|
+
plain_count = self.graph.execute_read_query(
|
|
260
|
+
"MATCH (p:Person) RETURN count(p) AS row_num", self.db)['row_num'][0]
|
|
261
|
+
query = """
|
|
262
|
+
MATCH (p:Person)
|
|
263
|
+
CALL (p) {
|
|
264
|
+
RETURN p AS person
|
|
265
|
+
} IN TRANSACTIONS OF 1000 ROWS
|
|
266
|
+
RETURN count(person) AS row_num
|
|
267
|
+
"""
|
|
268
|
+
with _patch_flaky_session_run(fail_times=1) as call_count:
|
|
269
|
+
result = self.graph.execute_read_query(query, self.db)
|
|
270
|
+
|
|
271
|
+
self.assertEqual(call_count['n'], 2)
|
|
272
|
+
self.assertEqual(plain_count, result['row_num'][0])
|
|
273
|
+
|
|
206
274
|
def test_get_node_label_freq(self):
|
|
207
275
|
"""Test node label frequency EDA."""
|
|
208
276
|
result = self.graph.get_node_label_freq(self.db)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|