graphdatascience 1.15a2__tar.gz → 1.16__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 (165) hide show
  1. {graphdatascience-1.15a2/graphdatascience.egg-info → graphdatascience-1.16}/PKG-INFO +2 -2
  2. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/cypher_warning_handler.py +4 -7
  3. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_cypher_runner.py +4 -2
  4. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_entity_ops_runner.py +9 -6
  5. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph_data_science.py +6 -1
  6. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/arrow_graph_constructor.py +7 -2
  7. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/arrow_query_runner.py +22 -1
  8. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/cypher_graph_constructor.py +4 -4
  9. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/gds_arrow_client.py +4 -1
  10. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/neo4j_query_runner.py +115 -14
  11. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/protocol/project_protocols.py +35 -7
  12. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/protocol/write_protocols.py +16 -10
  13. graphdatascience-1.16/graphdatascience/query_runner/query_mode.py +16 -0
  14. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/query_runner.py +18 -0
  15. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/session_query_runner.py +35 -9
  16. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/standalone_session_query_runner.py +20 -1
  17. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/retry_utils/retry_utils.py +1 -1
  18. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/aura_api.py +1 -1
  19. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/aura_graph_data_science.py +2 -2
  20. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/dbms/protocol_resolver.py +2 -2
  21. graphdatascience-1.16/graphdatascience/session/dbms_connection_info.py +57 -0
  22. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/dedicated_sessions.py +8 -4
  23. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/gds_sessions.py +25 -3
  24. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/session_info.py +1 -1
  25. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/session_sizes.py +2 -0
  26. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/topological_lp/topological_lp_alpha_runner.py +2 -2
  27. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/utils/direct_util_endpoints.py +2 -2
  28. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/utils/util_proc_runner.py +2 -0
  29. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/utils/util_remote_proc_runner.py +2 -2
  30. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/version.py +1 -1
  31. {graphdatascience-1.15a2 → graphdatascience-1.16/graphdatascience.egg-info}/PKG-INFO +2 -2
  32. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience.egg-info/SOURCES.txt +1 -0
  33. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience.egg-info/requires.txt +1 -1
  34. {graphdatascience-1.15a2 → graphdatascience-1.16}/requirements/base/base.txt +1 -1
  35. graphdatascience-1.15a2/graphdatascience/session/dbms_connection_info.py +0 -25
  36. {graphdatascience-1.15a2 → graphdatascience-1.16}/LICENSE +0 -0
  37. {graphdatascience-1.15a2 → graphdatascience-1.16}/MANIFEST.in +0 -0
  38. {graphdatascience-1.15a2 → graphdatascience-1.16}/README.md +0 -0
  39. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/__init__.py +0 -0
  40. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/algo/__init__.py +0 -0
  41. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/algo/algo_endpoints.py +0 -0
  42. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/algo/algo_proc_runner.py +0 -0
  43. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/algo/single_mode_algo_endpoints.py +0 -0
  44. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/call_builder.py +0 -0
  45. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/call_parameters.py +0 -0
  46. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/caller_base.py +0 -0
  47. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/endpoints.py +0 -0
  48. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/__init__.py +0 -0
  49. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/client_only_endpoint.py +0 -0
  50. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/endpoint_suggester.py +0 -0
  51. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/gds_not_installed.py +0 -0
  52. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/illegal_attr_checker.py +0 -0
  53. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/unable_to_connect.py +0 -0
  54. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/error/uncallable_namespace.py +0 -0
  55. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/__init__.py +0 -0
  56. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/base_graph_proc_runner.py +0 -0
  57. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_alpha_proc_runner.py +0 -0
  58. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_beta_proc_runner.py +0 -0
  59. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_create_result.py +0 -0
  60. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_endpoints.py +0 -0
  61. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_export_runner.py +0 -0
  62. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_object.py +0 -0
  63. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_proc_runner.py +0 -0
  64. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_project_runner.py +0 -0
  65. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_remote_proc_runner.py +0 -0
  66. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_remote_project_runner.py +0 -0
  67. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_sample_runner.py +0 -0
  68. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/graph_type_check.py +0 -0
  69. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/nx_loader.py +0 -0
  70. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/graph/ogb_loader.py +0 -0
  71. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/ignored_server_endpoints.py +0 -0
  72. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/__init__.py +0 -0
  73. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/graphsage_model.py +0 -0
  74. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/link_prediction_model.py +0 -0
  75. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model.py +0 -0
  76. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model_alpha_proc_runner.py +0 -0
  77. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model_beta_proc_runner.py +0 -0
  78. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model_endpoints.py +0 -0
  79. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model_proc_runner.py +0 -0
  80. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/model_resolver.py +0 -0
  81. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/node_classification_model.py +0 -0
  82. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/node_regression_model.py +0 -0
  83. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/pipeline_model.py +0 -0
  84. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/model/simple_rel_embedding_model.py +0 -0
  85. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/__init__.py +0 -0
  86. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/classification_training_pipeline.py +0 -0
  87. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/lp_pipeline_create_runner.py +0 -0
  88. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/lp_training_pipeline.py +0 -0
  89. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/nc_pipeline_create_runner.py +0 -0
  90. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/nc_training_pipeline.py +0 -0
  91. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/nr_pipeline_create_runner.py +0 -0
  92. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/nr_training_pipeline.py +0 -0
  93. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/pipeline_alpha_proc_runner.py +0 -0
  94. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/pipeline_beta_proc_runner.py +0 -0
  95. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/pipeline_endpoints.py +0 -0
  96. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/pipeline_proc_runner.py +0 -0
  97. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/pipeline/training_pipeline.py +0 -0
  98. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/py.typed +0 -0
  99. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/__init__.py +0 -0
  100. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/arrow_authentication.py +0 -0
  101. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/arrow_endpoint_version.py +0 -0
  102. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/arrow_info.py +0 -0
  103. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/graph_constructor.py +0 -0
  104. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/progress/__init__.py +0 -0
  105. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/progress/progress_provider.py +0 -0
  106. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/progress/query_progress_logger.py +0 -0
  107. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/progress/query_progress_provider.py +0 -0
  108. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/progress/static_progress_provider.py +0 -0
  109. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/protocol/__init__.py +0 -0
  110. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/protocol/status.py +0 -0
  111. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/query_runner/termination_flag.py +0 -0
  112. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/__init__.py +0 -0
  113. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/cora/__init__.py +0 -0
  114. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/cora/cora_nodes.parquet.gzip +0 -0
  115. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/cora/cora_rels.parquet.gzip +0 -0
  116. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/cora/serialize_cora.py +0 -0
  117. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/__init__.py +0 -0
  118. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_acted_in.parquet.gzip +0 -0
  119. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_actors.parquet.gzip +0 -0
  120. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_directed_in.parquet.gzip +0 -0
  121. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_directors.parquet.gzip +0 -0
  122. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_movies_with_genre.parquet.gzip +0 -0
  123. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/imdb_movies_without_genre.parquet.gzip +0 -0
  124. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/imdb/serialize_imdb.py +0 -0
  125. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/karate/__init__.py +0 -0
  126. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/karate/karate_club.parquet.gzip +0 -0
  127. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/__init__.py +0 -0
  128. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/artist_nodes.parquet.gzip +0 -0
  129. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/serialize_lastfm.py +0 -0
  130. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/user_friend_df_directed.parquet.gzip +0 -0
  131. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/user_listen_artist_rels.parquet.gzip +0 -0
  132. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/user_nodes.parquet.gzip +0 -0
  133. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/resources/lastfm/user_tag_artist_rels.parquet.gzip +0 -0
  134. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/retry_utils/__init__.py +0 -0
  135. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/retry_utils/retry_config.py +0 -0
  136. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/semantic_version/__init__.py +0 -0
  137. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/semantic_version/semantic_version.py +0 -0
  138. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/server_version/__init__.py +0 -0
  139. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/server_version/compatible_with.py +0 -0
  140. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/server_version/server_version.py +0 -0
  141. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/__init__.py +0 -0
  142. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/algorithm_category.py +0 -0
  143. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/aura_api_responses.py +0 -0
  144. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/aura_api_token_authentication.py +0 -0
  145. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/aurads_sessions.py +0 -0
  146. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/cloud_location.py +0 -0
  147. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/dbms/__init__.py +0 -0
  148. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/dbms/protocol_version.py +0 -0
  149. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/session/region_suggester.py +0 -0
  150. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/system/__init__.py +0 -0
  151. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/system/config_endpoints.py +0 -0
  152. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/system/system_endpoints.py +0 -0
  153. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/topological_lp/__init__.py +0 -0
  154. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/topological_lp/topological_lp_endpoints.py +0 -0
  155. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/utils/__init__.py +0 -0
  156. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience/utils/util_node_property_func_runner.py +0 -0
  157. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience.egg-info/dependency_links.txt +0 -0
  158. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience.egg-info/not-zip-safe +0 -0
  159. {graphdatascience-1.15a2 → graphdatascience-1.16}/graphdatascience.egg-info/top_level.txt +0 -0
  160. {graphdatascience-1.15a2 → graphdatascience-1.16}/pyproject.toml +0 -0
  161. {graphdatascience-1.15a2 → graphdatascience-1.16}/requirements/base/networkx.txt +0 -0
  162. {graphdatascience-1.15a2 → graphdatascience-1.16}/requirements/base/ogb.txt +0 -0
  163. {graphdatascience-1.15a2 → graphdatascience-1.16}/requirements/base/rust-ext.txt +0 -0
  164. {graphdatascience-1.15a2 → graphdatascience-1.16}/setup.cfg +0 -0
  165. {graphdatascience-1.15a2 → graphdatascience-1.16}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graphdatascience
3
- Version: 1.15a2
3
+ Version: 1.16
4
4
  Summary: A Python client for the Neo4j Graph Data Science (GDS) library
5
5
  Home-page: https://neo4j.com/product/graph-data-science/
6
6
  Author: Neo4j
@@ -32,7 +32,7 @@ Requires-Dist: multimethod<3.0,>=1.0
32
32
  Requires-Dist: neo4j<6.0,>=4.4.12
33
33
  Requires-Dist: numpy<2.3
34
34
  Requires-Dist: pandas<3.0,>=1.0
35
- Requires-Dist: pyarrow<20.0,>=16.0
35
+ Requires-Dist: pyarrow<21.0,>=17.0
36
36
  Requires-Dist: textdistance<5.0,>=4.0
37
37
  Requires-Dist: tqdm<5.0,>=4.0
38
38
  Requires-Dist: typing-extensions<5.0,>=4.0
@@ -19,15 +19,12 @@ def filter_id_func_deprecation_warning() -> Callable[[F], F]:
19
19
  message=r"^The query used a deprecated function: `id`\.",
20
20
  )
21
21
 
22
+ # previously The query used a deprecated function. ('id' is no longer supported)
23
+ # since 2025.04.0 The query used a deprecated function. ('id' has been replaced by 'elementId or an application-generated id')
24
+ # since 2025.06 The query used a deprecated function. ('id' has been replaced by 'elementId or consider using an application-generated id')
22
25
  warnings.filterwarnings(
23
26
  "ignore",
24
- message=r"^The query used a deprecated function. \('id' is no longer supported\)",
25
- )
26
-
27
- # since 2025.04.0
28
- warnings.filterwarnings(
29
- "ignore",
30
- message=r"^The query used a deprecated function. \('id' has been replaced by 'elementId or an application-generated id'\)",
27
+ message=r"The query used a deprecated function. \('id'.*",
31
28
  )
32
29
 
33
30
  return func(self, *args, **kwargs)
@@ -45,7 +45,9 @@ class GraphCypherRunner(CallerBase):
45
45
 
46
46
  GraphCypherRunner._verify_query_ends_with_return_clause(self._namespace, query)
47
47
 
48
- result: Optional[dict[str, Any]] = self._query_runner.run_cypher(query, params, database, False).squeeze()
48
+ result: Optional[dict[str, Any]] = self._query_runner.run_retryable_cypher(
49
+ query, params, database, custom_error=False
50
+ ).squeeze()
49
51
 
50
52
  if not result:
51
53
  raise ValueError("Projected graph cannot be empty.")
@@ -101,7 +103,7 @@ class GraphCypherRunner(CallerBase):
101
103
  at_end = True
102
104
  break
103
105
 
104
- if query_token == "RETURN":
106
+ if query_token.upper() == "RETURN":
105
107
  # State 1: We found the start of a `RETURN` clause.
106
108
  # Check if it is the `RETURN gds.graph.project` call.
107
109
  # We split tokens on `__separators` and flatten the nested iters.
@@ -158,12 +158,13 @@ class GraphNodePropertiesRunner(GraphEntityOpsBaseRunner):
158
158
  duplicate_properties = set(db_node_properties).intersection(set(node_properties))
159
159
  if duplicate_properties:
160
160
  raise ValueError(
161
- f"Duplicate property keys '{duplicate_properties}' in db_node_properties and " f"node_properties."
161
+ f"Duplicate property keys '{duplicate_properties}' in db_node_properties and node_properties."
162
162
  )
163
163
 
164
164
  unique_node_ids = result["nodeId"].drop_duplicates().tolist()
165
- db_properties_df = query_runner.run_cypher(
166
- GraphNodePropertiesRunner._build_query(db_node_properties), {"ids": unique_node_ids}
165
+
166
+ db_properties_df = query_runner.run_retryable_cypher(
167
+ GraphNodePropertiesRunner._build_query(db_node_properties), params={"ids": unique_node_ids}
167
168
  )
168
169
 
169
170
  if "propertyValue" not in result.keys():
@@ -342,7 +343,7 @@ class GraphRelationshipsRunner(GraphEntityOpsBaseRunner):
342
343
  def stream(self, G: Graph, relationship_types: list[str] = ["*"], **config: Any) -> TopologyDataFrame:
343
344
  self._namespace += ".stream"
344
345
  params = CallParameters(graph_name=G.name(), relationship_types=relationship_types, config=config)
345
- result = self._query_runner.call_procedure(endpoint=self._namespace, params=params)
346
+ result = self._query_runner.call_procedure(endpoint=self._namespace, params=params, retryable=True)
346
347
 
347
348
  return TopologyDataFrame(result)
348
349
 
@@ -360,7 +361,9 @@ class GraphRelationshipsBetaRunner(GraphEntityOpsBaseRunner):
360
361
  self._namespace += ".stream"
361
362
  params = CallParameters(graph_name=G.name(), relationship_types=relationship_types, config=config)
362
363
 
363
- return TopologyDataFrame(self._query_runner.call_procedure(endpoint=self._namespace, params=params))
364
+ return TopologyDataFrame(
365
+ self._query_runner.call_procedure(endpoint=self._namespace, params=params, retryable=True)
366
+ )
364
367
 
365
368
  @property
366
369
  @compatible_with("toUndirected", min_inclusive=ServerVersion(2, 3, 0))
@@ -381,7 +384,7 @@ class GraphPropertyRunner(UncallableNamespace, IllegalAttrChecker):
381
384
  self._namespace += ".stream"
382
385
  params = CallParameters(graph_name=G.name(), graph_property=graph_property, config=config)
383
386
 
384
- return self._query_runner.call_procedure(endpoint=self._namespace, params=params)
387
+ return self._query_runner.call_procedure(endpoint=self._namespace, params=params, retryable=True)
385
388
 
386
389
  @compatible_with("drop", min_inclusive=ServerVersion(2, 2, 0))
387
390
  @graph_type_check
@@ -4,6 +4,7 @@ import warnings
4
4
  from types import TracebackType
5
5
  from typing import Any, Optional, Type, Union
6
6
 
7
+ import neo4j
7
8
  from neo4j import Driver
8
9
  from pandas import DataFrame
9
10
 
@@ -78,8 +79,11 @@ class GraphDataScience(DirectEndpoints, UncallableNamespace):
78
79
  if isinstance(endpoint, QueryRunner):
79
80
  self._query_runner = endpoint
80
81
  else:
82
+ db_auth = None
83
+ if auth:
84
+ db_auth = neo4j.basic_auth(*auth)
81
85
  self._query_runner = Neo4jQueryRunner.create_for_db(
82
- endpoint, auth, aura_ds, database, bookmarks, show_progress
86
+ endpoint, db_auth, aura_ds, database, bookmarks, show_progress
83
87
  )
84
88
 
85
89
  self._server_version = self._query_runner.server_version()
@@ -218,6 +222,7 @@ class GraphDataScience(DirectEndpoints, UncallableNamespace):
218
222
  if isinstance(self._query_runner, ArrowQueryRunner):
219
223
  qr = self._query_runner.fallback_query_runner()
220
224
 
225
+ # not using qr.run_retryable_cypher as we dont know if it can be retried
221
226
  return qr.run_cypher(query, params, database, False)
222
227
 
223
228
  def driver_config(self) -> dict[str, Any]:
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import concurrent
4
+ import logging
4
5
  import math
5
6
  import warnings
6
7
  from concurrent.futures import ThreadPoolExecutor
@@ -33,6 +34,7 @@ class ArrowGraphConstructor(GraphConstructor):
33
34
  )
34
35
  self._chunk_size = chunk_size
35
36
  self._min_batch_size = chunk_size * 10
37
+ self._logger = logging.getLogger()
36
38
 
37
39
  def run(self, node_dfs: list[DataFrame], relationship_dfs: list[DataFrame]) -> None:
38
40
  try:
@@ -60,8 +62,11 @@ class ArrowGraphConstructor(GraphConstructor):
60
62
 
61
63
  self._client.relationship_load_done(self._graph_name)
62
64
  except (Exception, KeyboardInterrupt) as e:
63
- self._client.abort(self._graph_name)
64
-
65
+ try:
66
+ self._client.abort(self._graph_name)
67
+ except Exception as abort_exception:
68
+ if "No arrow process" not in str(abort_exception):
69
+ self._logger.warning(f"error aborting graph creation: {abort_exception}")
65
70
  raise e
66
71
 
67
72
  def _partition_dfs(self, dfs: list[DataFrame]) -> list[DataFrame]:
@@ -6,6 +6,7 @@ from typing import Any, Optional
6
6
  from pandas import DataFrame
7
7
 
8
8
  from graphdatascience.query_runner.arrow_authentication import ArrowAuthentication
9
+ from graphdatascience.query_runner.query_mode import QueryMode
9
10
  from graphdatascience.retry_utils.retry_config import RetryConfig
10
11
 
11
12
  from ..call_parameters import CallParameters
@@ -68,6 +69,15 @@ class ArrowQueryRunner(QueryRunner):
68
69
  ) -> DataFrame:
69
70
  return self._fallback_query_runner.run_cypher(query, params, database, custom_error)
70
71
 
72
+ def run_retryable_cypher(
73
+ self,
74
+ query: str,
75
+ params: Optional[dict[str, Any]] = None,
76
+ database: Optional[str] = None,
77
+ custom_error: bool = True,
78
+ ) -> DataFrame:
79
+ return self._fallback_query_runner.run_retryable_cypher(query, params, database, custom_error=custom_error)
80
+
71
81
  def call_function(self, endpoint: str, params: Optional[CallParameters] = None) -> Any:
72
82
  return self._fallback_query_runner.call_function(endpoint, params)
73
83
 
@@ -77,7 +87,9 @@ class ArrowQueryRunner(QueryRunner):
77
87
  params: Optional[CallParameters] = None,
78
88
  yields: Optional[list[str]] = None,
79
89
  database: Optional[str] = None,
90
+ mode: QueryMode = QueryMode.READ,
80
91
  logging: bool = False,
92
+ retryable: bool = False,
81
93
  custom_error: bool = True,
82
94
  ) -> DataFrame:
83
95
  if params is None:
@@ -171,7 +183,9 @@ class ArrowQueryRunner(QueryRunner):
171
183
  graph_name, self._database_or_throw(), relationship_types, concurrency
172
184
  )
173
185
 
174
- return self._fallback_query_runner.call_procedure(endpoint, params, yields, database, logging, custom_error)
186
+ return self._fallback_query_runner.call_procedure(
187
+ endpoint, params, yields, database, logging=logging, retryable=retryable, custom_error=custom_error
188
+ )
175
189
 
176
190
  def server_version(self) -> ServerVersion:
177
191
  return self._fallback_query_runner.server_version()
@@ -211,6 +225,13 @@ class ArrowQueryRunner(QueryRunner):
211
225
  self._fallback_query_runner.close()
212
226
  self._gds_arrow_client.close()
213
227
 
228
+ def cloneWithoutRouting(self, host: str, port: int) -> "QueryRunner":
229
+ return ArrowQueryRunner(
230
+ self._gds_arrow_client,
231
+ self._fallback_query_runner.cloneWithoutRouting(host, port),
232
+ self._server_version,
233
+ )
234
+
214
235
  def fallback_query_runner(self) -> QueryRunner:
215
236
  return self._fallback_query_runner
216
237
 
@@ -104,15 +104,14 @@ class CypherGraphConstructor(GraphConstructor):
104
104
 
105
105
  def _should_warn_about_arrow_missing(self) -> bool:
106
106
  try:
107
- license: str = self._query_runner.run_cypher(
107
+ license: str = self._query_runner.run_retryable_cypher(
108
108
  "CALL gds.debug.sysInfo() YIELD key, value WHERE key = 'gdsEdition' RETURN value", custom_error=False
109
109
  ).squeeze()
110
110
  should_warn = license == "Licensed"
111
111
  except Exception as e:
112
112
  # It's not a user's concern whether Arrow is set up or not in AuraDS.
113
- if (
114
- "There is no procedure with the name `gds.debug.sysInfo` "
115
- "registered for this database instance." in str(e)
113
+ if "There is no procedure with the name `gds.debug.sysInfo` registered for this database instance." in str(
114
+ e
116
115
  ):
117
116
  should_warn = False
118
117
  else:
@@ -210,6 +209,7 @@ class CypherGraphConstructor(GraphConstructor):
210
209
  "undirectedRelationshipTypes": self._undirected_relationship_types,
211
210
  }
212
211
 
212
+ # not using retryable here as gds.graph.project adds a graph to the gds graph catalog
213
213
  self._query_runner.run_cypher(
214
214
  query,
215
215
  {
@@ -640,7 +640,10 @@ class GdsArrowClient:
640
640
  )
641
641
  def send_with_retry() -> dict[str, Any]:
642
642
  try:
643
- result = client.do_action(flight.Action(action_type, json.dumps(meta_data).encode("utf-8")))
643
+ result = client.do_action(
644
+ action=flight.Action(action_type, json.dumps(meta_data).encode("utf-8")),
645
+ options=flight.FlightCallOptions(timeout=20.0),
646
+ )
644
647
 
645
648
  # Consume result fully to sanity check and avoid cancelled streams
646
649
  collected_result = list(result)
@@ -9,6 +9,8 @@ from typing import Any, NamedTuple, Optional, Union
9
9
  import neo4j
10
10
  from pandas import DataFrame
11
11
 
12
+ from graphdatascience.query_runner.query_mode import QueryMode
13
+
12
14
  from ..call_parameters import CallParameters
13
15
  from ..error.endpoint_suggester import generate_suggestive_error_message
14
16
  from ..error.gds_not_installed import GdsNotFound
@@ -30,14 +32,18 @@ class Neo4jQueryRunner(QueryRunner):
30
32
  @staticmethod
31
33
  def create_for_db(
32
34
  endpoint: Union[str, neo4j.Driver],
33
- auth: Optional[tuple[str, str]] = None,
35
+ auth: Union[tuple[str, str], neo4j.Auth, None] = None,
34
36
  aura_ds: bool = False,
35
37
  database: Optional[str] = None,
36
38
  bookmarks: Optional[Any] = None,
37
39
  show_progress: bool = True,
40
+ config: Optional[dict[str, Any]] = None,
38
41
  ) -> Neo4jQueryRunner:
39
42
  if isinstance(endpoint, str):
40
- config: dict[str, Any] = {"user_agent": f"neo4j-graphdatascience-v{__version__}"}
43
+ if config is None:
44
+ config = {}
45
+
46
+ config["user_agent"] = f"neo4j-graphdatascience-v{__version__}"
41
47
 
42
48
  if aura_ds:
43
49
  Neo4jQueryRunner._configure_aura(config)
@@ -46,6 +52,8 @@ class Neo4jQueryRunner(QueryRunner):
46
52
 
47
53
  query_runner = Neo4jQueryRunner(
48
54
  driver,
55
+ Neo4jQueryRunner.parse_protocol(endpoint),
56
+ auth,
49
57
  auto_close=True,
50
58
  bookmarks=bookmarks,
51
59
  config=config,
@@ -54,8 +62,14 @@ class Neo4jQueryRunner(QueryRunner):
54
62
  )
55
63
 
56
64
  elif isinstance(endpoint, neo4j.Driver):
65
+ protocol = "neo4j+s" if endpoint.encrypted else "bolt"
57
66
  query_runner = Neo4jQueryRunner(
58
- endpoint, auto_close=False, bookmarks=bookmarks, database=database, show_progress=show_progress
67
+ endpoint,
68
+ protocol,
69
+ auto_close=False,
70
+ bookmarks=bookmarks,
71
+ database=database,
72
+ show_progress=show_progress,
59
73
  )
60
74
  else:
61
75
  raise ValueError(f"Invalid endpoint type: {type(endpoint)}")
@@ -65,7 +79,7 @@ class Neo4jQueryRunner(QueryRunner):
65
79
  @staticmethod
66
80
  def create_for_session(
67
81
  endpoint: str,
68
- auth: Optional[tuple[str, str]] = None,
82
+ auth: Union[tuple[str, str], neo4j.Auth, None] = None,
69
83
  show_progress: bool = True,
70
84
  ) -> Neo4jQueryRunner:
71
85
  driver_config: dict[str, Any] = {"user_agent": f"neo4j-graphdatascience-v{__version__}"}
@@ -76,6 +90,8 @@ class Neo4jQueryRunner(QueryRunner):
76
90
 
77
91
  query_runner = Neo4jQueryRunner(
78
92
  driver,
93
+ Neo4jQueryRunner.parse_protocol(endpoint),
94
+ auth,
79
95
  auto_close=True,
80
96
  show_progress=show_progress,
81
97
  bookmarks=None,
@@ -90,13 +106,26 @@ class Neo4jQueryRunner(QueryRunner):
90
106
 
91
107
  @staticmethod
92
108
  def _configure_aura(config: dict[str, Any]) -> None:
93
- config["max_connection_lifetime"] = 60 * 8 # 8 minutes
94
- config["keep_alive"] = True
95
- config["max_connection_pool_size"] = 50
109
+ # defaults as documented in https://support.neo4j.com/s/article/1500001173021-How-to-handle-Session-Expired-Errors-while-connecting-to-Neo4j-Aura
110
+ config.setdefault("max_connection_lifetime", 60 * 50) # 50 minutes
111
+ config.setdefault("keep_alive", True)
112
+ config.setdefault("max_connection_pool_size", 50)
113
+
114
+ if Neo4jQueryRunner._NEO4J_DRIVER_VERSION >= SemanticVersion(5, 16, 0):
115
+ config.setdefault("liveness_check_timeout", 60 * 5) # 5 minutes
116
+
117
+ @staticmethod
118
+ def parse_protocol(endpoint: str) -> str:
119
+ protocol_match = re.match(r"^([^:]+)://", endpoint)
120
+ if not protocol_match:
121
+ raise ValueError(f"Invalid endpoint URI format: {endpoint}")
122
+ return protocol_match.group(1)
96
123
 
97
124
  def __init__(
98
125
  self,
99
126
  driver: neo4j.Driver,
127
+ protocol: str,
128
+ auth: Union[tuple[str, str], neo4j.Auth, None] = None,
100
129
  config: dict[str, Any] = {},
101
130
  database: Optional[str] = neo4j.DEFAULT_DATABASE,
102
131
  auto_close: bool = False,
@@ -105,6 +134,8 @@ class Neo4jQueryRunner(QueryRunner):
105
134
  instance_description: str = "Neo4j DBMS",
106
135
  ):
107
136
  self._driver = driver
137
+ self._protocol = protocol
138
+ self._auth = auth
108
139
  self._config = config
109
140
  self._auto_close = auto_close
110
141
  self._database = database
@@ -121,8 +152,10 @@ class Neo4jQueryRunner(QueryRunner):
121
152
  def __run_cypher_simplified_for_query_progress_logger(self, query: str, database: Optional[str]) -> DataFrame:
122
153
  # progress logging should not retry a lot as it perodically fetches the latest progress anyway
123
154
  connectivity_retry_config = Neo4jQueryRunner.ConnectivityRetriesConfig(max_retries=2)
155
+ # not using retryable cypher as failing is okay
124
156
  return self.run_cypher(query=query, database=database, connectivity_retry_config=connectivity_retry_config)
125
157
 
158
+ # only use for user defined queries
126
159
  def run_cypher(
127
160
  self,
128
161
  query: str,
@@ -166,12 +199,50 @@ class Neo4jQueryRunner(QueryRunner):
166
199
 
167
200
  return df
168
201
 
169
- def call_function(self, endpoint: str, params: Optional[CallParameters] = None) -> Any:
202
+ # better retry mechanism than run_cypher. The neo4j driver handles retryable errors internally
203
+ def run_retryable_cypher(
204
+ self,
205
+ query: str,
206
+ params: Optional[dict[str, Any]] = None,
207
+ database: Optional[str] = None,
208
+ custom_error: bool = True,
209
+ mode: Optional[QueryMode] = None,
210
+ connectivity_retry_config: Optional[ConnectivityRetriesConfig] = None,
211
+ ) -> DataFrame:
212
+ if not database:
213
+ database = self._database
214
+
215
+ if self._NEO4J_DRIVER_VERSION < SemanticVersion(5, 5, 0):
216
+ return self.run_cypher(query, params, database, custom_error, connectivity_retry_config)
217
+
218
+ if not mode:
219
+ routing = neo4j.RoutingControl.READ
220
+ else:
221
+ routing = mode.neo4j_routing()
222
+
223
+ try:
224
+ return self._driver.execute_query(
225
+ query_=query,
226
+ parameters_=params,
227
+ database_=database,
228
+ result_transformer_=neo4j.Result.to_df,
229
+ bookmark_manager_=self.bookmarks(),
230
+ routing_=routing,
231
+ )
232
+ except Exception as e:
233
+ if custom_error:
234
+ Neo4jQueryRunner.handle_driver_exception(self._driver, e)
235
+ raise e
236
+ else:
237
+ raise e
238
+
239
+ def call_function(self, endpoint: str, params: Optional[CallParameters] = None, custom_error: bool = True) -> Any:
170
240
  if params is None:
171
241
  params = CallParameters()
172
242
  query = f"RETURN {endpoint}({params.placeholder_str()})"
173
243
 
174
- return self.run_cypher(query, params).squeeze()
244
+ # we can use retryable cypher as we expect all gds functions to be idempotent
245
+ return self.run_retryable_cypher(query, params, custom_error=custom_error, mode=QueryMode.READ).squeeze()
175
246
 
176
247
  def call_procedure(
177
248
  self,
@@ -179,7 +250,9 @@ class Neo4jQueryRunner(QueryRunner):
179
250
  params: Optional[CallParameters] = None,
180
251
  yields: Optional[list[str]] = None,
181
252
  database: Optional[str] = None,
253
+ mode: QueryMode = QueryMode.READ,
182
254
  logging: bool = False,
255
+ retryable: bool = False,
183
256
  custom_error: bool = True,
184
257
  ) -> DataFrame:
185
258
  if params is None:
@@ -189,7 +262,10 @@ class Neo4jQueryRunner(QueryRunner):
189
262
  query = f"CALL {endpoint}({params.placeholder_str()}){yields_clause}"
190
263
 
191
264
  def run_cypher_query() -> DataFrame:
192
- return self.run_cypher(query, params, database, custom_error)
265
+ if retryable:
266
+ return self.run_retryable_cypher(query, params, database, custom_error, mode=mode)
267
+ else:
268
+ return self.run_cypher(query, params, database, custom_error)
193
269
 
194
270
  job_id = None if not params else params.get_job_id()
195
271
  if self._resolve_show_progress(logging) and job_id:
@@ -205,7 +281,7 @@ class Neo4jQueryRunner(QueryRunner):
205
281
  return self._server_version
206
282
 
207
283
  try:
208
- server_version_string = self.run_cypher("RETURN gds.version()", custom_error=False).squeeze()
284
+ server_version_string = self.call_function("gds.version", custom_error=False)
209
285
  server_version = ServerVersion.from_string(server_version_string)
210
286
  self._server_version = server_version
211
287
  return server_version
@@ -279,8 +355,25 @@ class Neo4jQueryRunner(QueryRunner):
279
355
  def set_show_progress(self, show_progress: bool) -> None:
280
356
  self._show_progress = show_progress
281
357
 
358
+ def cloneWithoutRouting(self, host: str, port: int) -> QueryRunner:
359
+ protocol = self._protocol.replace("neo4j", "bolt")
360
+ endpoint = "{}://{}:{}".format(protocol, host, port)
361
+ driver = neo4j.GraphDatabase.driver(endpoint, auth=self._auth, **self.driver_config())
362
+
363
+ return Neo4jQueryRunner(
364
+ driver=driver,
365
+ protocol=protocol,
366
+ auth=self._auth,
367
+ config=self._config,
368
+ database=self._database,
369
+ auto_close=self._auto_close,
370
+ bookmarks=self._bookmarks,
371
+ show_progress=self._show_progress,
372
+ instance_description=self._instance_description,
373
+ )
374
+
282
375
  @staticmethod
283
- def handle_driver_exception(session: neo4j.Session, e: Exception) -> None:
376
+ def handle_driver_exception(cypher_executor: Union[neo4j.Session, neo4j.Driver], e: Exception) -> None:
284
377
  reg_gds_hit = re.search(
285
378
  r"There is no procedure with the name `(gds(?:\.\w+)+)` registered for this database instance",
286
379
  str(e),
@@ -290,8 +383,16 @@ class Neo4jQueryRunner(QueryRunner):
290
383
 
291
384
  requested_endpoint = reg_gds_hit.group(1)
292
385
 
293
- list_result = session.run("CALL gds.list() YIELD name")
294
- all_endpoints = list_result.to_df()["name"].tolist()
386
+ if isinstance(cypher_executor, neo4j.Session):
387
+ list_result = cypher_executor.run("CALL gds.list() YIELD name")
388
+ all_endpoints = list_result.to_df()["name"].tolist()
389
+ elif isinstance(cypher_executor, neo4j.Driver):
390
+ result = cypher_executor.execute_query("CALL gds.list() YIELD name", result_transformer_=neo4j.Result.to_df)
391
+ all_endpoints = result["name"].tolist()
392
+ else:
393
+ raise TypeError(
394
+ f"Expected cypher_executor to be a neo4j.Session or neo4j.Driver, got {type(cypher_executor)}"
395
+ )
295
396
 
296
397
  raise SyntaxError(generate_suggestive_error_message(requested_endpoint, all_endpoints)) from e
297
398
 
@@ -68,7 +68,9 @@ class ProjectProtocolV1(ProjectProtocol):
68
68
  logging: bool = False,
69
69
  ) -> DataFrame:
70
70
  versioned_endpoint = ProtocolVersion.V1.versioned_procedure_name(endpoint)
71
- return query_runner.call_procedure(versioned_endpoint, params, yields, database, logging, False)
71
+ return query_runner.call_procedure(
72
+ versioned_endpoint, params, yields, database=database, logging=logging, retryable=False, custom_error=False
73
+ )
72
74
 
73
75
 
74
76
  class ProjectProtocolV2(ProjectProtocol):
@@ -97,7 +99,9 @@ class ProjectProtocolV2(ProjectProtocol):
97
99
  logging: bool = False,
98
100
  ) -> DataFrame:
99
101
  versioned_endpoint = ProtocolVersion.V2.versioned_procedure_name(endpoint)
100
- return query_runner.call_procedure(versioned_endpoint, params, yields, database, logging, False)
102
+ return query_runner.call_procedure(
103
+ versioned_endpoint, params, yields, database=database, logging=logging, retryable=False, custom_error=False
104
+ )
101
105
 
102
106
 
103
107
  class ProjectProtocolV3(ProjectProtocol):
@@ -121,7 +125,7 @@ class ProjectProtocolV3(ProjectProtocol):
121
125
  query_runner: QueryRunner,
122
126
  endpoint: str,
123
127
  params: CallParameters,
124
- terminationFlag: TerminationFlag,
128
+ termination_flag: TerminationFlag,
125
129
  yields: Optional[list[str]] = None,
126
130
  database: Optional[str] = None,
127
131
  logging: bool = False,
@@ -132,6 +136,20 @@ class ProjectProtocolV3(ProjectProtocol):
132
136
 
133
137
  logger = getLogger()
134
138
 
139
+ # We need to pin the driver to a specific cluster member
140
+ response = query_runner.call_procedure(
141
+ ProtocolVersion.V3.versioned_procedure_name(endpoint),
142
+ params,
143
+ yields,
144
+ database,
145
+ logging=logging,
146
+ custom_error=False,
147
+ retryable=True,
148
+ ).squeeze()
149
+ member_host = response["host"]
150
+ member_port = response["port"] if ("port" in response.index) else 7687
151
+ projection_query_runner = query_runner.cloneWithoutRouting(member_host, member_port)
152
+
135
153
  @retry(
136
154
  reraise=True,
137
155
  before=before_log(f"Projection (graph: `{params['graph_name']}`)", logger, DEBUG),
@@ -139,9 +157,19 @@ class ProjectProtocolV3(ProjectProtocol):
139
157
  wait=wait_incrementing(start=0.2, increment=0.2, max=2),
140
158
  )
141
159
  def project_fn() -> DataFrame:
142
- terminationFlag.assert_running()
143
- return query_runner.call_procedure(
144
- ProtocolVersion.V3.versioned_procedure_name(endpoint), params, yields, database, logging, False
160
+ termination_flag.assert_running()
161
+ return projection_query_runner.call_procedure(
162
+ ProtocolVersion.V3.versioned_procedure_name(endpoint),
163
+ params,
164
+ yields,
165
+ database=database,
166
+ logging=logging,
167
+ retryable=True,
168
+ custom_error=False,
145
169
  )
146
170
 
147
- return project_fn()
171
+ projection_result = project_fn()
172
+
173
+ projection_query_runner.close()
174
+
175
+ return projection_result
@@ -5,9 +5,10 @@ from typing import Any, Optional
5
5
  from pandas import DataFrame
6
6
  from tenacity import retry, retry_if_result, wait_incrementing
7
7
 
8
- from graphdatascience import QueryRunner
9
8
  from graphdatascience.call_parameters import CallParameters
10
9
  from graphdatascience.query_runner.protocol.status import Status
10
+ from graphdatascience.query_runner.query_mode import QueryMode
11
+ from graphdatascience.query_runner.query_runner import QueryRunner
11
12
  from graphdatascience.query_runner.termination_flag import TerminationFlag
12
13
  from graphdatascience.retry_utils.retry_utils import before_log
13
14
  from graphdatascience.session.dbms.protocol_version import ProtocolVersion
@@ -73,9 +74,11 @@ class RemoteWriteBackV1(WriteProtocol):
73
74
  ProtocolVersion.V1.versioned_procedure_name("gds.arrow.write"),
74
75
  parameters,
75
76
  yields,
76
- None,
77
- False,
78
- False,
77
+ retryable=False,
78
+ database=None,
79
+ logging=False,
80
+ mode=QueryMode.WRITE,
81
+ custom_error=False,
79
82
  )
80
83
 
81
84
 
@@ -111,9 +114,11 @@ class RemoteWriteBackV2(WriteProtocol):
111
114
  ProtocolVersion.V2.versioned_procedure_name("gds.arrow.write"),
112
115
  parameters,
113
116
  yields,
114
- None,
115
- False,
116
- False,
117
+ retryable=False,
118
+ database=None,
119
+ logging=False,
120
+ mode=QueryMode.WRITE,
121
+ custom_error=False,
117
122
  )
118
123
 
119
124
 
@@ -157,9 +162,10 @@ class RemoteWriteBackV3(WriteProtocol):
157
162
  ProtocolVersion.V3.versioned_procedure_name("gds.arrow.write"),
158
163
  parameters,
159
164
  yields,
160
- None,
161
- False,
162
- False,
165
+ retryable=True,
166
+ logging=False,
167
+ mode=QueryMode.WRITE,
168
+ custom_error=False,
163
169
  )
164
170
 
165
171
  return write_fn()
@@ -0,0 +1,16 @@
1
+ from enum import Enum
2
+
3
+ import neo4j
4
+
5
+
6
+ class QueryMode(str, Enum):
7
+ READ = "read"
8
+ WRITE = "write"
9
+
10
+ def neo4j_routing(self) -> "neo4j.RoutingControl":
11
+ if self == QueryMode.READ:
12
+ return neo4j.RoutingControl.READ
13
+ elif self == QueryMode.WRITE:
14
+ return neo4j.RoutingControl.WRITE
15
+ else:
16
+ raise ValueError(f"Unknown query mode: {self}")