pyneoinstance 4.0.3__tar.gz → 4.0.5__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.5}/PKG-INFO +14 -9
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/README.md +8 -8
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/__init__.py +1 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/neo4jdbms.py +67 -8
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5/pyneoinstance.egg-info}/PKG-INFO +14 -9
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/SOURCES.txt +0 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/requires.txt +2 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/top_level.txt +0 -1
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyproject.toml +7 -2
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test_neo4jdb.py +68 -0
- pyneoinstance-4.0.3/test/age_data.csv +0 -300001
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/LICENSE +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/MANIFEST.in +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/context.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/helpers.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/loadyaml.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/py.typed +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/util/__init__.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/util/functions.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/dependency_links.txt +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/setup.cfg +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/setup.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/cypher_queries.yaml +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/load_files.yaml +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/movies.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/people.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/roles.csv +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test_fileload.py +0 -0
- {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/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.5
|
|
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
|
|
@@ -686,6 +686,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
686
686
|
Classifier: Programming Language :: Python :: 3.10
|
|
687
687
|
Classifier: Programming Language :: Python :: 3.11
|
|
688
688
|
Classifier: Programming Language :: Python :: 3.12
|
|
689
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
690
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
691
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
689
692
|
Classifier: Development Status :: 5 - Production/Stable
|
|
690
693
|
Requires-Python: >=3.10
|
|
691
694
|
Description-Content-Type: text/markdown
|
|
@@ -695,6 +698,8 @@ Requires-Dist: pandas>=1.3.0
|
|
|
695
698
|
Requires-Dist: PyYAML>=5.1
|
|
696
699
|
Requires-Dist: pyvis>=0.3.0
|
|
697
700
|
Requires-Dist: neo4j-viz>=1.0.0
|
|
701
|
+
Requires-Dist: comm>=0.1.3
|
|
702
|
+
Requires-Dist: traitlets>=5.10.0
|
|
698
703
|
Provides-Extra: docs
|
|
699
704
|
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
700
705
|
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
|
|
@@ -965,42 +970,42 @@ The get_node_label_freq method provides the frequency and relative frequency of
|
|
|
965
970
|
```python
|
|
966
971
|
graph.get_node_label_freq(database='mydbname')
|
|
967
972
|
```
|
|
968
|
-
<img src="https://
|
|
973
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/nodefreq.png" width="250">
|
|
969
974
|
|
|
970
975
|
### Relationship Type Frequency
|
|
971
976
|
The get_rela_type_freq method provides the frequency and relative frequency of all relationship types in the graph.
|
|
972
977
|
```python
|
|
973
978
|
graph.get_rela_type_freq(database='mydbname')
|
|
974
979
|
```
|
|
975
|
-
<img src="https://
|
|
980
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaFreq.png" width="300">
|
|
976
981
|
|
|
977
982
|
### Relationship Source-Target Frequency
|
|
978
983
|
The get_rela_source_target_freq method provides the frequency and relative frequency of all relationship types considering the node label of the source and targets of the relationships.
|
|
979
984
|
```python
|
|
980
985
|
graph.get_rela_type_freq(database='mydbname')
|
|
981
986
|
```
|
|
982
|
-
<img src="https://
|
|
987
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaSourceTargetFreq.png" width="300">
|
|
983
988
|
|
|
984
989
|
### Properties
|
|
985
990
|
The get_properties method provides information about all the nodes and relationships properties.
|
|
986
991
|
```python
|
|
987
992
|
graph.get_properties(database='mydbname')
|
|
988
993
|
```
|
|
989
|
-
<img src="https://
|
|
994
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/properties.png" width="350" hight="300">
|
|
990
995
|
|
|
991
996
|
### Constraints
|
|
992
997
|
The get_constraints method provides information about all the graph constraints.
|
|
993
998
|
```python
|
|
994
999
|
graph.get_constraints(database='mydbname')
|
|
995
1000
|
```
|
|
996
|
-
<img src="https://
|
|
1001
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/constraints.png" width="350" hight="300">
|
|
997
1002
|
|
|
998
1003
|
### Indexes
|
|
999
1004
|
The get_indexes method provides information about all the graph indexes.
|
|
1000
1005
|
```python
|
|
1001
1006
|
graph.get_indexes(database='mydbname')
|
|
1002
1007
|
```
|
|
1003
|
-
<img src="https://
|
|
1008
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/indexes.png" width="450" hight="400">
|
|
1004
1009
|
|
|
1005
1010
|
### Query Graph Visualization
|
|
1006
1011
|
The `get_query_visualization` method executes a Cypher query that returns nodes and relationships and renders the result as an interactive graph. Node labels are displayed as captions and all properties appear as tooltips on hover.
|
|
@@ -1032,8 +1037,8 @@ schema.write_html('schema.html')
|
|
|
1032
1037
|
|
|
1033
1038
|
Open `schema.html` in your browser to explore the schema interactively.
|
|
1034
1039
|
|
|
1035
|
-
<img src="https://
|
|
1036
|
-
<img src="https://
|
|
1040
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schema.png" width="450" hight="400">
|
|
1041
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schemaHtml.png" width="450" hight="400">
|
|
1037
1042
|
|
|
1038
1043
|
## More detailed examples
|
|
1039
1044
|
|
|
@@ -263,42 +263,42 @@ The get_node_label_freq method provides the frequency and relative frequency of
|
|
|
263
263
|
```python
|
|
264
264
|
graph.get_node_label_freq(database='mydbname')
|
|
265
265
|
```
|
|
266
|
-
<img src="https://
|
|
266
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/nodefreq.png" width="250">
|
|
267
267
|
|
|
268
268
|
### Relationship Type Frequency
|
|
269
269
|
The get_rela_type_freq method provides the frequency and relative frequency of all relationship types in the graph.
|
|
270
270
|
```python
|
|
271
271
|
graph.get_rela_type_freq(database='mydbname')
|
|
272
272
|
```
|
|
273
|
-
<img src="https://
|
|
273
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaFreq.png" width="300">
|
|
274
274
|
|
|
275
275
|
### Relationship Source-Target Frequency
|
|
276
276
|
The get_rela_source_target_freq method provides the frequency and relative frequency of all relationship types considering the node label of the source and targets of the relationships.
|
|
277
277
|
```python
|
|
278
278
|
graph.get_rela_type_freq(database='mydbname')
|
|
279
279
|
```
|
|
280
|
-
<img src="https://
|
|
280
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaSourceTargetFreq.png" width="300">
|
|
281
281
|
|
|
282
282
|
### Properties
|
|
283
283
|
The get_properties method provides information about all the nodes and relationships properties.
|
|
284
284
|
```python
|
|
285
285
|
graph.get_properties(database='mydbname')
|
|
286
286
|
```
|
|
287
|
-
<img src="https://
|
|
287
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/properties.png" width="350" hight="300">
|
|
288
288
|
|
|
289
289
|
### Constraints
|
|
290
290
|
The get_constraints method provides information about all the graph constraints.
|
|
291
291
|
```python
|
|
292
292
|
graph.get_constraints(database='mydbname')
|
|
293
293
|
```
|
|
294
|
-
<img src="https://
|
|
294
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/constraints.png" width="350" hight="300">
|
|
295
295
|
|
|
296
296
|
### Indexes
|
|
297
297
|
The get_indexes method provides information about all the graph indexes.
|
|
298
298
|
```python
|
|
299
299
|
graph.get_indexes(database='mydbname')
|
|
300
300
|
```
|
|
301
|
-
<img src="https://
|
|
301
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/indexes.png" width="450" hight="400">
|
|
302
302
|
|
|
303
303
|
### Query Graph Visualization
|
|
304
304
|
The `get_query_visualization` method executes a Cypher query that returns nodes and relationships and renders the result as an interactive graph. Node labels are displayed as captions and all properties appear as tooltips on hover.
|
|
@@ -330,8 +330,8 @@ schema.write_html('schema.html')
|
|
|
330
330
|
|
|
331
331
|
Open `schema.html` in your browser to explore the schema interactively.
|
|
332
332
|
|
|
333
|
-
<img src="https://
|
|
334
|
-
<img src="https://
|
|
333
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schema.png" width="450" hight="400">
|
|
334
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schemaHtml.png" width="450" hight="400">
|
|
335
335
|
|
|
336
336
|
## More detailed examples
|
|
337
337
|
|
|
@@ -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)
|
|
@@ -695,8 +751,8 @@ class Neo4jInstance:
|
|
|
695
751
|
'targetLabel': nodes[1], 'frequency': value}
|
|
696
752
|
stats_dicts.append(info)
|
|
697
753
|
stats_df = DataFrame(stats_dicts)
|
|
698
|
-
stats_df.sort_values(by=['relationshipType','sourceLabel','targetLabel']
|
|
699
|
-
stats_df.reset_index(drop=True
|
|
754
|
+
stats_df = stats_df.sort_values(by=['relationshipType','sourceLabel','targetLabel'])
|
|
755
|
+
stats_df = stats_df.reset_index(drop=True)
|
|
700
756
|
return stats_df
|
|
701
757
|
|
|
702
758
|
def get_schema_visualization(self, database: Optional[str] = None) -> Network:
|
|
@@ -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.5
|
|
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
|
|
@@ -686,6 +686,9 @@ Classifier: Programming Language :: Python :: 3
|
|
|
686
686
|
Classifier: Programming Language :: Python :: 3.10
|
|
687
687
|
Classifier: Programming Language :: Python :: 3.11
|
|
688
688
|
Classifier: Programming Language :: Python :: 3.12
|
|
689
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
690
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
691
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
689
692
|
Classifier: Development Status :: 5 - Production/Stable
|
|
690
693
|
Requires-Python: >=3.10
|
|
691
694
|
Description-Content-Type: text/markdown
|
|
@@ -695,6 +698,8 @@ Requires-Dist: pandas>=1.3.0
|
|
|
695
698
|
Requires-Dist: PyYAML>=5.1
|
|
696
699
|
Requires-Dist: pyvis>=0.3.0
|
|
697
700
|
Requires-Dist: neo4j-viz>=1.0.0
|
|
701
|
+
Requires-Dist: comm>=0.1.3
|
|
702
|
+
Requires-Dist: traitlets>=5.10.0
|
|
698
703
|
Provides-Extra: docs
|
|
699
704
|
Requires-Dist: mkdocs-material>=9.0.0; extra == "docs"
|
|
700
705
|
Requires-Dist: mkdocstrings[python]>=0.24.0; extra == "docs"
|
|
@@ -965,42 +970,42 @@ The get_node_label_freq method provides the frequency and relative frequency of
|
|
|
965
970
|
```python
|
|
966
971
|
graph.get_node_label_freq(database='mydbname')
|
|
967
972
|
```
|
|
968
|
-
<img src="https://
|
|
973
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/nodefreq.png" width="250">
|
|
969
974
|
|
|
970
975
|
### Relationship Type Frequency
|
|
971
976
|
The get_rela_type_freq method provides the frequency and relative frequency of all relationship types in the graph.
|
|
972
977
|
```python
|
|
973
978
|
graph.get_rela_type_freq(database='mydbname')
|
|
974
979
|
```
|
|
975
|
-
<img src="https://
|
|
980
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaFreq.png" width="300">
|
|
976
981
|
|
|
977
982
|
### Relationship Source-Target Frequency
|
|
978
983
|
The get_rela_source_target_freq method provides the frequency and relative frequency of all relationship types considering the node label of the source and targets of the relationships.
|
|
979
984
|
```python
|
|
980
985
|
graph.get_rela_type_freq(database='mydbname')
|
|
981
986
|
```
|
|
982
|
-
<img src="https://
|
|
987
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/relaSourceTargetFreq.png" width="300">
|
|
983
988
|
|
|
984
989
|
### Properties
|
|
985
990
|
The get_properties method provides information about all the nodes and relationships properties.
|
|
986
991
|
```python
|
|
987
992
|
graph.get_properties(database='mydbname')
|
|
988
993
|
```
|
|
989
|
-
<img src="https://
|
|
994
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/properties.png" width="350" hight="300">
|
|
990
995
|
|
|
991
996
|
### Constraints
|
|
992
997
|
The get_constraints method provides information about all the graph constraints.
|
|
993
998
|
```python
|
|
994
999
|
graph.get_constraints(database='mydbname')
|
|
995
1000
|
```
|
|
996
|
-
<img src="https://
|
|
1001
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/constraints.png" width="350" hight="300">
|
|
997
1002
|
|
|
998
1003
|
### Indexes
|
|
999
1004
|
The get_indexes method provides information about all the graph indexes.
|
|
1000
1005
|
```python
|
|
1001
1006
|
graph.get_indexes(database='mydbname')
|
|
1002
1007
|
```
|
|
1003
|
-
<img src="https://
|
|
1008
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/indexes.png" width="450" hight="400">
|
|
1004
1009
|
|
|
1005
1010
|
### Query Graph Visualization
|
|
1006
1011
|
The `get_query_visualization` method executes a Cypher query that returns nodes and relationships and renders the result as an interactive graph. Node labels are displayed as captions and all properties appear as tooltips on hover.
|
|
@@ -1032,8 +1037,8 @@ schema.write_html('schema.html')
|
|
|
1032
1037
|
|
|
1033
1038
|
Open `schema.html` in your browser to explore the schema interactively.
|
|
1034
1039
|
|
|
1035
|
-
<img src="https://
|
|
1036
|
-
<img src="https://
|
|
1040
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schema.png" width="450" hight="400">
|
|
1041
|
+
<img src="https://raw.githubusercontent.com/jose-alvarado-guzman/pyneoingest/main/images/schemaHtml.png" width="450" hight="400">
|
|
1037
1042
|
|
|
1038
1043
|
## More detailed examples
|
|
1039
1044
|
|
|
@@ -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.5"
|
|
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" }]
|
|
@@ -17,6 +17,9 @@ classifiers = [
|
|
|
17
17
|
"Programming Language :: Python :: 3.10",
|
|
18
18
|
"Programming Language :: Python :: 3.11",
|
|
19
19
|
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Programming Language :: Python :: 3.15",
|
|
20
23
|
"Development Status :: 5 - Production/Stable",
|
|
21
24
|
]
|
|
22
25
|
keywords = ["neo4j", "graph", "cypher"]
|
|
@@ -25,7 +28,9 @@ dependencies = [
|
|
|
25
28
|
"pandas>=1.3.0",
|
|
26
29
|
"PyYAML>=5.1",
|
|
27
30
|
"pyvis>=0.3.0",
|
|
28
|
-
"neo4j-viz>=1.0.0"
|
|
31
|
+
"neo4j-viz>=1.0.0",
|
|
32
|
+
"comm>=0.1.3",
|
|
33
|
+
"traitlets>=5.10.0"
|
|
29
34
|
]
|
|
30
35
|
requires-python = ">=3.10"
|
|
31
36
|
|
|
@@ -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)
|