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.
Files changed (32) hide show
  1. {pyneoinstance-4.0.3/pyneoinstance.egg-info → pyneoinstance-4.0.5}/PKG-INFO +14 -9
  2. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/README.md +8 -8
  3. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/__init__.py +1 -1
  4. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/neo4jdbms.py +67 -8
  5. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5/pyneoinstance.egg-info}/PKG-INFO +14 -9
  6. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/SOURCES.txt +0 -1
  7. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/requires.txt +2 -0
  8. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/top_level.txt +0 -1
  9. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyproject.toml +7 -2
  10. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test_neo4jdb.py +68 -0
  11. pyneoinstance-4.0.3/test/age_data.csv +0 -300001
  12. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/LICENSE +0 -0
  13. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/MANIFEST.in +0 -0
  14. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/__init__.py +0 -0
  15. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/database/context.py +0 -0
  16. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/__init__.py +0 -0
  17. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/helpers.py +0 -0
  18. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/fileload/loadyaml.py +0 -0
  19. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/py.typed +0 -0
  20. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/util/__init__.py +0 -0
  21. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance/util/functions.py +0 -0
  22. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/pyneoinstance.egg-info/dependency_links.txt +0 -0
  23. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/setup.cfg +0 -0
  24. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/setup.py +0 -0
  25. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/cypher_queries.yaml +0 -0
  26. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/load_files.yaml +0 -0
  27. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/movies.csv +0 -0
  28. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/people.csv +0 -0
  29. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/roles.csv +0 -0
  30. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test.py +0 -0
  31. {pyneoinstance-4.0.3 → pyneoinstance-4.0.5}/test/test_fileload.py +0 -0
  32. {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
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/nodefreq.png" width="250">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaSourceTargetFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/properties.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/constraints.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/indexes.png" width="450" hight="400">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schema.png" width="450" hight="400">
1036
- <img src="https://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schemaHtml.png" width="450" hight="400">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/nodefreq.png" width="250">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaSourceTargetFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/properties.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/constraints.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/indexes.png" width="450" hight="400">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schema.png" width="450" hight="400">
334
- <img src="https://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schemaHtml.png" width="450" hight="400">
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
 
@@ -1,5 +1,5 @@
1
1
  from .database.neo4jdbms import Neo4jInstance
2
2
  from .fileload.loadyaml import load_yaml_file
3
3
 
4
- __version__ = '4.0.3'
4
+ __version__ = '4.0.5'
5
5
  __all__ = ['Neo4jInstance', 'load_yaml_file']
@@ -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
- read_result = session.run(query, **params)
208
- result = DataFrame(read_result.values(), columns=read_result.keys())
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
- graph_result = session.run(query, **params)
260
- list(graph_result) # consume all records so .graph() is populated
261
- graph = graph_result.graph()
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'],inplace=True)
699
- stats_df.reset_index(drop=True, inplace=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
- results = session.run(query, **kwargs).consume().counters.__dict__
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
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/nodefreq.png" width="250">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/relaSourceTargetFreq.png" width="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/properties.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/constraints.png" width="350" hight="300">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/indexes.png" width="450" hight="400">
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://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schema.png" width="450" hight="400">
1036
- <img src="https://s3.amazonaws.com/neo4j-ps-ds-bootcamp/img/schemaHtml.png" width="450" hight="400">
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
 
@@ -18,7 +18,6 @@ pyneoinstance/fileload/helpers.py
18
18
  pyneoinstance/fileload/loadyaml.py
19
19
  pyneoinstance/util/__init__.py
20
20
  pyneoinstance/util/functions.py
21
- test/age_data.csv
22
21
  test/cypher_queries.yaml
23
22
  test/load_files.yaml
24
23
  test/movies.csv
@@ -3,6 +3,8 @@ pandas>=1.3.0
3
3
  PyYAML>=5.1
4
4
  pyvis>=0.3.0
5
5
  neo4j-viz>=1.0.0
6
+ comm>=0.1.3
7
+ traitlets>=5.10.0
6
8
 
7
9
  [docs]
8
10
  mkdocs-material>=9.0.0
@@ -2,4 +2,3 @@ dist
2
2
  docs
3
3
  images
4
4
  pyneoinstance
5
- site
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
 
7
7
  [project]
8
8
  name = "pyneoinstance"
9
- version = "4.0.3"
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)