Codebase list neo4j-python-driver / 3b0fc8bb-6f3f-40f0-9aa6-ec706b546e0f/upstream
Import upstream version 4.2.0a2 Kali Janitor 3 years ago
359 changed file(s) with 26199 addition(s) and 8323 deletion(s). Raw diff Collapse all Expand all
11 branch = True
22 omit =
33 .*/*
4 test/*
5 neo4j/compat/*
6 neo4j/util.py
4 tests/*
5 neo4j/meta.py
76 *neobolt*
7 *virtualenv*
88
99 [report]
10 exclude_lines =
11 pragma: no cover
12 def __repr__
13 if self.debug:
14 raise NotImplementedError
15 if __name__ == .__main__.:
16 except ImportError
17
1018 show_missing = True
0 # Neo4j Driver Change Log
1
2 ## Version 4.2
3
4 - No driver changes have been made for Neo4j 4.2
5
6
7 ## Version 4.1
8
9 - Routing context is now forwarded to the server for when required by server-side routing
10
11
12 ## Version 4.0 - Breaking Changes
13
14 - The package version has jumped from `1.7` directly to `4.0`, in order to bring the version in line with Neo4j itself.
15 - The package can now no longer be installed as `neo4j-driver`; use `pip install neo4j` instead.
16 - The `neo4j.v1` subpackage is now no longer available; all imports should be taken from the `neo4j` package instead.
17 - Changed `session(access_mode)` from a positional to a keyword argument
18 - The `bolt+routing` scheme is now named `neo4j`
19 - Connections are now unencrypted by default; to reproduce former behaviour, add `encrypted=True` to Driver configuration
20 - Removed `transaction.success` flag usage pattern.
21
22 + Python 3.8 supported.
23 + Python 3.7 supported.
24 + Python 3.6 supported.
25 + Python 3.5 supported.
26 + Python 3.4 support has been dropped.
27 + Python 3.3 support has been dropped.
28 + Python 3.2 support has been dropped.
29 + Python 3.1 support has been dropped.
30 + Python 3.0 support has been dropped.
31 + Python 2.7 support has been dropped.
00 global-exclude *.class *.pyc *.pyo *.so *.dll __pycache__
1 prune test
1 prune tests
00 Neo4j
1 Copyright (c) 2002-2019 Neo4j Sweden AB (referred to in this notice as "Neo4j") [http://neo4j.com]
1 Copyright (c) 2002-2020 Neo4j Sweden AB (referred to in this notice as "Neo4j") [http://neo4j.com]
22
33 This product includes software ("Software") developed by Neo4j
11 Neo4j Bolt Driver for Python
22 ****************************
33
4 The official Neo4j driver for Python supports Neo4j 3.0 and above and Python versions 2.7, 3.4, 3.5, 3.6, and 3.7.
4 This repository contains the official Neo4j driver for Python.
5 Each driver release (from 4.0 upwards) is built specifically to work with a corresponding Neo4j release, i.e. that with the same `major.minor` version number.
6 These drivers will also be compatible with the previous Neo4j release, although new server features will not be available.
57
6 .. note::
8 + Python 3.8 supported.
9 + Python 3.7 supported.
10 + Python 3.6 supported.
11 + Python 3.5 supported.
712
8 Python 2 support is deprecated and will be discontinued in the 2.x series driver releases.
13 Python 2.7 support has been dropped as of the Neo4j 4.0 release.
14
15
16 Installation
17 ============
18
19 To install the latest stable version, use:
20
21 .. code:: bash
22
23 pip install neo4j
924
1025
1126 Quick Example
1530
1631 from neo4j import GraphDatabase
1732
18 driver = GraphDatabase.driver("bolt://localhost:7687", auth=("neo4j", "password"))
33 driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "password"))
1934
2035 def add_friend(tx, name, friend_name):
2136 tx.run("MERGE (a:Person {name: $name}) "
3348 session.write_transaction(add_friend, "Arthur", "Merlin")
3449 session.read_transaction(print_friends, "Arthur")
3550
51 driver.close()
3652
37 Installation
38 ============
3953
40 To install the latest stable version, use:
54 Connection Settings Breaking Change
55 ===================================
4156
42 .. code:: bash
57 + The driver’s default configuration for encrypted is now false (meaning that driver will only attempt plain text connections by default).
4358
44 pip install neo4j
59 + Connections to encrypted services (such as Neo4j Aura) should now explicitly be set to encrypted.
4560
46 .. note::
61 + When encryption is explicitly enabled, the default trust mode is to trust the CAs that are trusted by operating system and use hostname verification.
4762
48 Installation from the ``neo4j-driver`` package on PyPI is now deprecated and will be discontinued in the 2.x series driver releases.
49 Please install from the ``neo4j`` package instead.
63 + This means that encrypted connections to servers holding self-signed certificates will now fail on certificate verification by default.
5064
51 For the most up-to-date version (generally unstable), use:
65 + Using the new `neo4j+ssc` scheme will allow to connect to servers holding self-signed certificates and not use hostname verification.
5266
53 .. code:: bash
67 + The `neo4j://` scheme replaces `bolt+routing://` and can be used for both clustered and single-instance configurations with Neo4j 4.0.
5468
55 pip install git+https://github.com/neo4j/neo4j-python-driver.git#egg=neo4j
69
70
71 See, https://neo4j.com/docs/migration-guide/4.0/upgrade-driver/#upgrade-driver-breakingchanges
72
73
74 See, https://neo4j.com/docs/driver-manual/current/client-applications/#driver-connection-uris for changes in default security settings between 3.x and 4.x
75
76
77 Connecting with Python Driver 4.x to Neo4j 3.5
78 ----------------------------------------------
79
80 Using the Python Driver 4.x and connecting to Neo4j 3.5 with default connection settings for Neo4j 3.5.
81
82 .. code-block:: python
83
84 # the preferred form
85
86 driver = GraphDatabase.driver("neo4j+ssc://localhost:7687", auth=("neo4j", "password"))
87
88 # is equivalent to
89
90 driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "password"), encrypted=True, trust=False)
91
92
93 Connecting with Python Driver 1.7 to Neo4j 4.x
94 ----------------------------------------------
95
96 Using the Python Driver 1.7 and connecting to Neo4j 4.x with default connection settings for Neo4j 4.x.
97
98 .. code-block:: python
99
100 driver = GraphDatabase.driver("neo4j://localhost:7687", auth=("neo4j", "password"), encrypted=False)
101
56102
57103
58104 Other Information
59105 =================
60106
61 * `Neo4j Manual`_
62 * `Neo4j Quick Reference Card`_
107 * `The Neo4j Operations Manual`_
108 * `The Neo4j Drivers Manual`_
109 * `Python Driver API Documentation`_
110 * `Neo4j Cypher Refcard`_
63111 * `Example Project`_
64112 * `Driver Wiki`_ (includes change logs)
113 * `Neo4j 4.0 Migration Guide`_
65114
66 .. _`Neo4j Manual`: https://neo4j.com/docs/developer-manual/current/drivers/
67 .. _`Neo4j Quick Reference Card`: https://neo4j.com/docs/cypher-refcard/current/
115 .. _`The Neo4j Operations Manual`: https://neo4j.com/docs/operations-manual/current/
116 .. _`The Neo4j Drivers Manual`: https://neo4j.com/docs/driver-manual/current/
117 .. _`Python Driver API Documentation`: https://neo4j.com/docs/api/python-driver/current/
118 .. _`Neo4j Cypher Refcard`: https://neo4j.com/docs/cypher-refcard/current/
68119 .. _`Example Project`: https://github.com/neo4j-examples/movies-python-bolt
69120 .. _`Driver Wiki`: https://github.com/neo4j/neo4j-python-driver/wiki
121 .. _`Neo4j 4.0 Migration Guide`: https://neo4j.com/docs/migration-guide/4.0/
0 #!/usr/bin/env bash
1
2 ROOT=$(dirname "$0")
3
4 find -name __pycache__ -delete
5 find -name *.py[co] -delete
6 find -name *.so -delete
7
8 rm -rf ${ROOT}/build ${ROOT}/dist ${ROOT}/docs/build ${ROOT}/*.egg-info ${ROOT}/.coverage ${ROOT}/.tox ${ROOT}/.cache ${ROOT}/.pytest_cache ${ROOT}/.benchmarks
9
10 # These are removed to avoid name collision problems with snapshot server packages
11 rm -rf ${ROOT}/test/integration/dist ${ROOT}/test/integration/run
4747 ORIGINAL_VERSION=$(get_version)
4848 echo "Source code originally configured for package ${ORIGINAL_PACKAGE}/${ORIGINAL_VERSION}"
4949 echo "----------------------------------------"
50 tail -2 neo4j/meta.py
50 grep "package\s\+=" neo4j/meta.py
51 grep "version\s\+=" neo4j/meta.py
5152 echo "----------------------------------------"
5253
5354 # Temporarily override package metadata
5556 set_version "${VERSION}"
5657 echo "Source code reconfigured for package ${PACKAGE}/${VERSION}"
5758 echo "----------------------------------------"
58 tail -2 neo4j/meta.py
59 grep "package\s\+=" neo4j/meta.py
60 grep "version\s\+=" neo4j/meta.py
5961 echo "----------------------------------------"
6062
6163 # Create source distribution
6971 set_version "${ORIGINAL_VERSION}"
7072 echo "Source code reconfigured back to original package ${ORIGINAL_PACKAGE}/${ORIGINAL_VERSION}"
7173 echo "----------------------------------------"
72 tail -2 neo4j/meta.py
74 grep "package\s\+=" neo4j/meta.py
75 grep "version\s\+=" neo4j/meta.py
7376 echo "----------------------------------------"
7477
7578 }
+0
-10
clean.sh less more
0 #!/usr/bin/env bash
1
2 ROOT=$(dirname "$0")
3
4 rm -rf ${ROOT}/build ${ROOT}/dist ${ROOT}/docs/build ${ROOT}/*.egg-info ${ROOT}/.coverage ${ROOT}/.tox ${ROOT}/.cache ${ROOT}/.pytest_cache ${ROOT}/.benchmarks
5
6 # These are removed to avoid name collision problems with snapshot server packages
7 rm -rf ${ROOT}/test/integration/dist ${ROOT}/test/integration/run
8
9 find -name *.so -delete
0 ====================
1 Sphinx Documentation
2 ====================
3
4 ```
5 pip install -r requirements.txt
6 ```
7
8 ```
9 make -C docs html
10 ```
11
12 ```
13 python -m sphinx -b html docs/source build/html
14 ```
+0
-10
docs/README.rst less more
0 ====================
1 Sphinx Documentation
2 ====================
3
4 From the root of this repository...
5
6 .. code:: bash
7
8 pip install -r docs_requirements.txt
9 make -C docs html
0 neotime
10 sphinx
0 <!-- Adds target=_blank to external links -->
1 $(document).ready(function () {
2 $('a[href^="http://"], a[href^="https://"]').not('a[class*=internal]').attr('target', '_blank');
3 });
0 /*
1 * nature_custom.css_t
2 * ~~~~~~~~~~~~
3 *
4 * This stylesheet have been customized.
5 * The original Sphinx stylesheet is -- nature theme.
6 *
7 * Copyright (c) 2007-2020 by the Sphinx team.
8 * Copyright (c) 2020-2020 Neo4j
9 * All rights reserved.
10 *
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions are
13 * met:
14 *
15 * * Redistributions of source code must retain the above copyright
16 * notice, this list of conditions and the following disclaimer.
17 *
18 * * Redistributions in binary form must reproduce the above copyright
19 * notice, this list of conditions and the following disclaimer in the
20 * documentation and/or other materials provided with the distribution.
21 *
22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
25 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
26 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
27 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
28 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
30 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 *
34 */
35
36 @import url("basic.css");
37
38 /* -- page layout ----------------------------------------------------------- */
39
40 body {
41 font-family: Arial, sans-serif;
42 font-size: 100%;
43 background-color: #fff;
44 color: #555;
45 margin: 0;
46 padding: 0;
47 }
48
49 div.documentwrapper {
50 float: left;
51 width: 100%;
52 }
53
54 div.bodywrapper {
55 margin: 0 0 0 400px !important;
56 /*margin: 0 0 0 {{ theme_sidebarwidth|todim }};*/
57 }
58
59 hr {
60 border: 1px solid #B1B4B6;
61 }
62
63 div.document {
64 background-color: #eee;
65 }
66
67 div.body {
68 background-color: #ffffff;
69 color: #3E4349;
70 padding: 0 30px 30px 30px;
71 font-size: 0.9em;
72 }
73
74 div.footer {
75 color: #555;
76 width: 100%;
77 padding: 13px 0;
78 text-align: center;
79 font-size: 75%;
80 }
81
82 div.footer a {
83 color: #444;
84 text-decoration: underline;
85 }
86
87 div.related {
88 background-color: #000000;
89 /*background-color: #6BA81E;*/
90 line-height: 32px;
91 color: #fff;
92 text-shadow: none;
93 /*text-shadow: 0px 1px 0 #444;*/
94 font-size: 0.9em;
95 }
96
97 div.related a {
98 color: #ffffff;
99 /*color: #E2F3CC;*/
100 }
101
102 div.sphinxsidebar {
103 width: 400px !important; /* new */
104 font-size: 0.75em;
105 line-height: 1.5em;
106 }
107
108 div.sphinxsidebarwrapper{
109 padding: 20px 0;
110 }
111
112 div.sphinxsidebar h3,
113 div.sphinxsidebar h4 {
114 font-family: Arial, sans-serif;
115 color: #000000;
116 /*color: #222;*/
117 font-size: 1.2em;
118 font-weight: normal;
119 margin: 0;
120 padding: 5px 10px;
121 background-color: #ffffff;
122 /*background-color: #ddd;*/
123 text-shadow: none;
124 /*text-shadow: 1px 1px 0 white;*/
125 }
126
127 div.sphinxsidebar h4{
128 font-size: 1.1em;
129 }
130
131 div.sphinxsidebar h3 a {
132 color: #444;
133 }
134
135
136 div.sphinxsidebar p {
137 color: #888;
138 padding: 5px 20px;
139 }
140
141 div.sphinxsidebar p.topless {
142 }
143
144 div.sphinxsidebar ul {
145 margin: 10px 20px;
146 padding: 0;
147 color: #000;
148 }
149
150 div.sphinxsidebar a {
151 color: #444;
152 }
153
154 div.sphinxsidebar input {
155 border: 1px solid #ccc;
156 font-family: sans-serif;
157 font-size: 1em;
158 }
159
160 div.sphinxsidebar .searchformwrapper {
161 margin-left: 20px;
162 margin-right: 20px;
163 }
164
165 /* -- body styles ----------------------------------------------------------- */
166
167 a {
168 color: #005B81;
169 text-decoration: none;
170 }
171
172 a:hover {
173 color: #E32E00;
174 text-decoration: underline;
175 }
176
177 div.body h1,
178 div.body h2,
179 div.body h3,
180 div.body h4,
181 div.body h5,
182 div.body h6 {
183 font-family: Arial, sans-serif;
184 background-color: #a9d3ff;
185 /*background-color: #BED4EB;*/
186 font-weight: normal;
187 color: #212224;
188 margin: 30px 0px 10px 0px;
189 padding: 5px 0 5px 10px;
190 text-shadow: none;
191 /*text-shadow: 0px 1px 0 white;*/
192 }
193
194 div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
195 div.body h2 { font-size: 150%; background-color: #a9d3ff; }
196 div.body h3 { font-size: 120%; background-color: #a9d3ff; }
197 div.body h4 { font-size: 110%; background-color: #a9d3ff; }
198 div.body h5 { font-size: 100%; background-color: #a9d3ff; }
199 div.body h6 { font-size: 100%; background-color: #a9d3ff; }
200
201 a.headerlink {
202 color: #c60f0f;
203 font-size: 0.8em;
204 padding: 0 4px 0 4px;
205 text-decoration: none;
206 }
207
208 a.headerlink:hover {
209 background-color: #c60f0f;
210 color: white;
211 }
212
213 div.body p, div.body dd, div.body li {
214 line-height: 1.5em;
215 }
216
217 div.admonition p.admonition-title + p {
218 display: inline;
219 }
220
221 div.highlight{
222 background-color: white;
223 }
224
225 div.note {
226 background-color: #eee;
227 border: 1px solid #ccc;
228 }
229
230 div.seealso {
231 background-color: #ffc;
232 border: 1px solid #ff6;
233 }
234
235 div.topic {
236 background-color: #eee;
237 }
238
239 div.warning {
240 background-color: #ffe4e4;
241 border: 1px solid #f66;
242 }
243
244 div.note {
245 background-color: #20ff0050;
246 border: 1px solid #000000;
247 }
248
249 p.admonition-title {
250 display: inline;
251 }
252
253 p.admonition-title:after {
254 content: ":";
255 }
256
257 pre {
258 padding: 10px;
259 background-color: #f0f0f0;
260 /*background-color: white;*/
261 color: #222;
262 line-height: 1.2em;
263 border: 1px solid #000000;
264 /*border: 1px solid #C6C9CB;*/
265 font-size: 1.1em;
266 margin: 1.5em 0 1.5em 0;
267 /*-webkit-box-shadow: 1px 1px 1px #d8d8d8;*/
268 /*-moz-box-shadow: 1px 1px 1px #d8d8d8;*/
269 }
270
271 code {
272 background-color: #ecf0f3;
273 color: #222;
274 /* padding: 1px 2px; */
275 font-size: 1.1em;
276 font-family: monospace;
277 }
278
279 .viewcode-back {
280 font-family: Arial, sans-serif;
281 }
282
283 div.viewcode-block:target {
284 background-color: #f4debf;
285 border-top: 1px solid #ac9;
286 border-bottom: 1px solid #ac9;
287 }
288
289 div.code-block-caption {
290 background-color: #ddd;
291 color: #222;
292 border: 1px solid #C6C9CB;
293 }
294
295
296 li > p:first-child {
297 margin-top: 10px;
298 }
299
300 li > p:last-child {
301 margin-top: 10px;
302 }
0 .. _api-documentation:
1
2 #################
3 API Documentation
4 #################
5
6 *************
7 GraphDatabase
8 *************
9
10 Driver Construction
11 ===================
12
13 The :class:`neo4j.Driver` construction is via a `classmethod` on the :class:`neo4j.GraphDatabase` class.
14
15 .. autoclass:: neo4j.GraphDatabase
16 :members: driver
17
18
19 Example, driver creation:
20
21 .. code-block:: python
22
23 from neo4j import GraphDatabase
24
25 uri = neo4j://example.com:7687
26 driver = GraphDatabase.driver(uri, auth=("neo4j", "password"), max_connection_lifetime=1000)
27
28 driver.close() # close the driver object
29
30
31 For basic auth, this can be a simple tuple, for example:
32
33 .. code-block:: python
34
35 auth = ("neo4j", "password")
36
37 This will implicitly create a :class:`neo4j.Auth` with a ``scheme="basic"``
38
39
40 Example, with block context:
41
42 .. code-block:: python
43
44 from neo4j import GraphDatabase
45
46 uri = neo4j://example.com:7687
47
48 with GraphDatabase.driver(uri, auth=("neo4j", "password")) as driver:
49 # use the driver
50
51
52
53 .. _uri-ref:
54
55 URI
56 ===
57
58 On construction, the `scheme` of the URI determines the type of :class:`neo4j.Driver` object created.
59
60 Available valid URIs:
61
62 + ``bolt://host[:port]``
63 + ``bolt+ssc://host[:port]``
64 + ``bolt+s://host[:port]``
65 + ``neo4j://host[:port][?routing_context]``
66 + ``neo4j+ssc://host[:port][?routing_context]``
67 + ``neo4j+s://host[:port][?routing_context]``
68
69 .. code-block:: python
70
71 uri = bolt://example.com:7687
72
73 .. code-block:: python
74
75 uri = neo4j://example.com:7687
76
77 Each supported scheme maps to a particular :class:`neo4j.Driver` subclass that implements a specific behaviour.
78
79 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
80 | URI Scheme | Driver Object and Setting |
81 +========================+=======================================================================================================================================+
82 | bolt | :ref:`bolt-driver-ref` with no encryption. |
83 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
84 | bolt+ssc | :ref:`bolt-driver-ref` with encryption (accepts self signed certificates). |
85 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
86 | bolt+s | :ref:`bolt-driver-ref` with encryption (accepts only certificates signed by a certificate authority), full certificate checks. |
87 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
88 | neo4j | :ref:`neo4j-driver-ref` with no encryption. |
89 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
90 | neo4j+ssc | :ref:`neo4j-driver-ref` with encryption (accepts self signed certificates). |
91 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
92 | neo4j+s | :ref:`neo4j-driver-ref` with encryption (accepts only certificates signed by a certificate authority), full certificate checks. |
93 +------------------------+---------------------------------------------------------------------------------------------------------------------------------------+
94
95 .. note::
96
97 See https://neo4j.com/docs/operations-manual/current/configuration/ports/ for Neo4j ports.
98
99
100 .. _auth-ref:
101
102 Auth
103 ====
104
105 To authenticate with Neo4j the authentication details are supplied at driver creation.
106
107 The auth token is an object of the class :class:`neo4j.Auth` containing the details.
108
109 .. autoclass:: neo4j.Auth
110
111
112
113 Example:
114
115 .. code-block:: python
116
117 import neo4j
118
119 auth = neo4j.Auth(scheme="basic", principal="neo4j", credentials="password")
120
121
122 Auth Token Helper Functions
123 ---------------------------
124
125 Alternatively, one of the auth token helper functions can be used.
126
127 .. autofunction:: neo4j.basic_auth
128
129 .. autofunction:: neo4j.kerberos_auth
130
131 .. autofunction:: neo4j.custom_auth
132
133
134 ******
135 Driver
136 ******
137
138 Every Neo4j-backed application will require a :class:`neo4j.Driver` object.
139
140 This object holds the details required to establish connections with a Neo4j database, including server URIs, credentials and other configuration.
141 :class:`neo4j.Driver` objects hold a connection pool from which :class:`neo4j.Session` objects can borrow connections.
142 Closing a driver will immediately shut down all connections in the pool.
143
144 .. autoclass:: neo4j.Driver()
145 :members: session, close
146
147
148 .. _driver-configuration-ref:
149
150 Driver Configuration
151 ====================
152
153 Additional configuration can be provided via the :class:`neo4j.Driver` constructor.
154
155
156 + :ref:`connection-acquisition-timeout-ref`
157 + :ref:`connection-timeout-ref`
158 + :ref:`encrypted-ref`
159 + :ref:`keep-alive-ref`
160 + :ref:`max-connection-lifetime-ref`
161 + :ref:`max-connection-pool-size-ref`
162 + :ref:`max-transaction-retry-time-ref`
163 + :ref:`resolver-ref`
164 + :ref:`trust-ref`
165 + :ref:`user-agent-ref`
166
167
168 .. _connection-acquisition-timeout-ref:
169
170 ``connection_acquisition_timeout``
171 ----------------------------------
172 The maximum amount of time in seconds a session will wait when requesting a connection from the connection pool.
173 Since the process of acquiring a connection may involve creating a new connection, ensure that the value of this configuration is higher than the configured :ref:`connection-timeout-ref`.
174
175 :Type: ``float``
176 :Default: ``60.0``
177
178
179 .. _connection-timeout-ref:
180
181 ``connection_timeout``
182 ----------------------
183 The maximum amount of time in seconds to wait for a TCP connection to be established.
184
185 :Type: ``float``
186 :Default: ``30.0``
187
188
189 .. _encrypted-ref:
190
191 ``encrypted``
192 -------------
193 Specify whether to use an encrypted connection between the driver and server.
194
195 :Type: ``bool``
196 :Default: ``False``
197
198
199 .. _keep-alive-ref:
200
201 ``keep_alive``
202 --------------
203 Specify whether TCP keep-alive should be enabled.
204
205 :Type: ``bool``
206 :Default: ``True``
207
208 **This is experimental.** (See :ref:`filter-warnings-ref`)
209
210
211 .. _max-connection-lifetime-ref:
212
213 ``max_connection_lifetime``
214 ---------------------------
215
216 The maximum duration in seconds that the driver will keep a connection for before being removed from the pool.
217
218 :Type: ``float``
219 :Default: ``3600``
220
221
222 .. _max-connection-pool-size-ref:
223
224 ``max_connection_pool_size``
225 ----------------------------
226 The maximum total number of connections allowed, per host (i.e. cluster nodes), to be managed by the connection pool.
227
228 :Type: ``int``
229 :Default: ``100``
230
231
232 .. _max-transaction-retry-time-ref:
233
234 ``max_transaction_retry_time``
235 ------------------------------
236 The maximum amount of time in seconds that a managed transaction will retry before failing.
237
238 :Type: ``float``
239 :Default: ``30.0``
240
241
242 .. _resolver-ref:
243
244 ``resolver``
245 ------------
246 A custom resolver function to resolve host and port values ahead of DNS resolution.
247 This function is called with a 2-tuple of (host, port) and should return an iterable of 2-tuples (host, port).
248
249 If no custom resolver function is supplied, the internal resolver moves straight to regular DNS resolution.
250
251 For example:
252
253 .. code-block:: python
254
255 from neo4j import GraphDatabase
256
257 def custom_resolver(socket_address):
258 if socket_address == ("example.com", 9999):
259 yield "::1", 7687
260 yield "127.0.0.1", 7687
261 else:
262 from socket import gaierror
263 raise gaierror("Unexpected socket address %r" % socket_address)
264
265 driver = GraphDatabase.driver("neo4j://example.com:9999",
266 auth=("neo4j", "password"),
267 resolver=custom_resolver)
268
269
270 :Default: ``None``
271
272
273 .. _trust-ref:
274
275 ``trust``
276 ---------
277 Specify how to determine the authenticity of encryption certificates provided by the Neo4j instance on connection.
278
279 This setting does not have any effect if ``encrypted`` is set to ``False``.
280
281 :Type: ``neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES``, ``neo4j.TRUST_ALL_CERTIFICATES``
282
283 .. py:attribute:: neo4j.TRUST_ALL_CERTIFICATES
284
285 Trust any server certificate (default). This ensures that communication
286 is encrypted but does not verify the server certificate against a
287 certificate authority. This option is primarily intended for use with
288 the default auto-generated server certificate.
289
290 .. py:attribute:: neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
291
292 Trust server certificates that can be verified against the system
293 certificate authority. This option is primarily intended for use with
294 full certificates.
295
296 :Default: ``neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES``.
297
298
299 .. _user-agent-ref:
300
301 ``user_agent``
302 --------------
303 Specify the client agent name.
304
305 :Type: ``str``
306 :Default: *The Python Driver will generate a user agent name.*
307
308
309
310 Driver Object Lifetime
311 ======================
312
313 For general applications, it is recommended to create one top-level :class:`neo4j.Driver` object that lives for the lifetime of the application.
314
315 For example:
316
317 .. code-block:: python
318
319 from neo4j import GraphDatabase
320
321 class Application:
322
323 def __init__(self, uri, user, password)
324 self.driver = GraphDatabase.driver(uri, auth=(user, password))
325
326 def close(self):
327 self.driver.close()
328
329 Connection details held by the :class:`neo4j.Driver` are immutable.
330 Therefore if, for example, a password is changed, a replacement :class:`neo4j.Driver` object must be created.
331 More than one :class:`.Driver` may be required if connections to multiple databases, or connections as multiple users, are required.
332
333 :class:`neo4j.Driver` objects are thread-safe but cannot be shared across processes.
334 Therefore, ``multithreading`` should generally be preferred over ``multiprocessing`` for parallel database access.
335 If using ``multiprocessing`` however, each process will require its own :class:`neo4j.Driver` object.
336
337
338 .. _bolt-driver-ref:
339
340 BoltDriver
341 ==========
342
343 URI schemes:
344 ``bolt``, ``bolt+ssc``, ``bolt+s``
345
346 Driver subclass:
347 :class:`neo4j.BoltDriver`
348
349 ..
350 .. autoclass:: neo4j.BoltDriver
351
352
353 .. _neo4j-driver-ref:
354
355 Neo4jDriver
356 ===========
357
358 URI schemes:
359 ``neo4j``, ``neo4j+ssc``, ``neo4j+s``
360
361 Driver subclass:
362 :class:`neo4j.Neo4jDriver`
363
364 ..
365 .. autoclass:: neo4j.Neo4jDriver
366
367
368 ***********************
369 Sessions & Transactions
370 ***********************
371 All database activity is co-ordinated through two mechanisms: the :class:`neo4j.Session` and the :class:`neo4j.Transaction`.
372
373 A :class:`neo4j.Session` is a logical container for any number of causally-related transactional units of work.
374 Sessions automatically provide guarantees of causal consistency within a clustered environment but multiple sessions can also be causally chained if required.
375 Sessions provide the top-level of containment for database activity.
376 Session creation is a lightweight operation and *sessions are not thread safe*.
377
378 Connections are drawn from the :class:`neo4j.Driver` connection pool as required.
379
380 A :class:`neo4j.Transaction` is a unit of work that is either committed in its entirety or is rolled back on failure.
381
382
383 .. _session-construction-ref:
384
385 ********************
386 Session Construction
387 ********************
388
389 To construct a :class:`neo4j.Session` use the :meth:`neo4j.Driver.session` method.
390
391 .. code-block:: python
392
393 from neo4j import GraphDatabase
394
395 driver = GraphDatabase(uri, auth=(user, password))
396 session = driver.session()
397 result = session.run("MATCH (a:Person) RETURN a.name AS name")
398 names = [record["name"] for record in result]
399 session.close()
400 driver.close()
401
402
403 Sessions will often be created and destroyed using a *with block context*.
404
405 .. code-block:: python
406
407 with driver.session() as session:
408 result = session.run("MATCH (a:Person) RETURN a.name AS name")
409 # do something with the result...
410
411
412 Sessions will often be created with some configuration settings, see :ref:`session-configuration-ref`.
413
414 .. code-block:: python
415
416 with driver.session(database="example_database", fetch_size=100) as session:
417 result = session.run("MATCH (a:Person) RETURN a.name AS name")
418 # do something with the result...
419
420
421 *******
422 Session
423 *******
424
425 .. autoclass:: neo4j.Session()
426
427 .. automethod:: close
428
429 .. automethod:: run
430
431 .. automethod:: last_bookmark
432
433 .. automethod:: begin_transaction
434
435 .. automethod:: read_transaction
436
437 .. automethod:: write_transaction
438
439
440 Query
441 =====
442
443 .. autoclass:: neo4j.Query
444
445
446
447 .. _session-configuration-ref:
448
449 Session Configuration
450 =====================
451
452 To construct a :class:`neo4j.Session` use the :meth:`neo4j.Driver.session` method. This section describes the session configuration key-word arguments.
453
454
455 + :ref:`bookmarks-ref`
456 + :ref:`database-ref`
457 + :ref:`default-access-mode-ref`
458 + :ref:`fetch-size-ref`
459
460
461 .. _bookmarks-ref:
462
463 ``bookmarks``
464 -------------
465 An iterable containing :class:`neo4j.Bookmark`
466
467 :Default: ``()``
468
469
470 .. _database-ref:
471
472 ``database``
473 ------------
474 Name of the database to query.
475
476 :Type: ``str``, ``neo4j.DEFAULT_DATABASE``
477
478
479 .. py:attribute:: neo4j.DEFAULT_DATABASE
480 :noindex:
481
482 This will use the default database on the Neo4j instance.
483
484
485 .. Note::
486
487 The default database can be set on the Neo4j instance settings.
488
489
490 .. code-block:: python
491
492 from neo4j import GraphDatabase
493 driver = GraphDatabase.driver(uri, auth=(user, password))
494 session = driver.session(database="system")
495
496
497 :Default: ``neo4j.DEFAULT_DATABASE``
498
499
500 .. _default-access-mode-ref:
501
502 ``default_access_mode``
503 -----------------------
504 The default access mode.
505
506 A session can be given a default access mode on construction.
507
508 This applies only in clustered environments and determines whether transactions carried out within that session should be routed to a `read` or `write` server by default.
509
510 Transactions (see :ref:`managed-transactions-ref`) within a session can override the access mode passed to that session on construction.
511
512 .. note::
513 The driver does not parse Cypher queries and cannot determine whether the access mode should be ``neo4j.ACCESS_WRITE`` or ``neo4j.ACCESS_READ``.
514 Since the access mode is not passed to the server, this can allow a ``neo4j.ACCESS_WRITE`` statement to be executed for a ``neo4j.ACCESS_READ`` call on a single instance.
515 Clustered environments are not susceptible to this loophole as cluster roles prevent it.
516 This behaviour should not be relied upon as the loophole may be closed in a future release.
517
518
519 :Type: ``neo4j.WRITE_ACCESS``, ``neo4j.READ_ACCESS``
520 :Default: ``neo4j.WRITE_ACCESS``
521
522
523 .. _fetch-size-ref:
524
525 ``fetch_size``
526 --------------
527 The fetch size used for requesting messages from Neo4j.
528
529 :Type: ``int``
530 :Default: ``1000``
531
532
533
534
535 ***********
536 Transaction
537 ***********
538
539 Neo4j supports three kinds of transaction:
540
541 + :ref:`auto-commit-transactions-ref`
542 + :ref:`explicit-transactions-ref`
543 + :ref:`managed-transactions-ref`
544
545 Each has pros and cons but if in doubt, use a managed transaction with a `transaction function`.
546
547
548 .. _auto-commit-transactions-ref:
549
550 Auto-commit Transactions
551 ========================
552 Auto-commit transactions are the simplest form of transaction, available via :py:meth:`neo4j.Session.run`.
553
554 These are easy to use but support only one statement per transaction and are not automatically retried on failure.
555 Auto-commit transactions are also the only way to run ``PERIODIC COMMIT`` statements, since this Cypher clause manages its own transactions internally.
556
557 Example:
558
559 .. code-block:: python
560
561 import neo4j
562
563 def create_person(driver, name):
564 with driver.session(default_access_mode=neo4j.WRITE_ACCESS) as session:
565 result = session.run("CREATE (a:Person { name: $name }) RETURN id(a) AS node_id", name=name)
566 record = result.single()
567 return record["node_id"]
568
569 Example:
570
571 .. code-block:: python
572
573 import neo4j
574
575 def get_numbers(driver):
576 numbers = []
577 with driver.session(default_access_mode=neo4j.READ_ACCESS) as session:
578 result = session.run("UNWIND [1, 2, 3] AS x RETURN x")
579 for record in result:
580 numbers.append(record["x"])
581 return numbers
582
583
584 .. _explicit-transactions-ref:
585
586 Explicit Transactions
587 =====================
588 Explicit transactions support multiple statements and must be created with an explicit :py:meth:`neo4j.Session.begin_transaction` call.
589
590 This creates a new :class:`neo4j.Transaction` object that can be used to run Cypher.
591
592 It also gives applications the ability to directly control `commit` and `rollback` activity.
593
594 .. autoclass:: neo4j.Transaction()
595
596 .. automethod:: run
597
598 .. automethod:: close
599
600 .. automethod:: closed
601
602 .. automethod:: commit
603
604 .. automethod:: rollback
605
606 Closing an explicit transaction can either happen automatically at the end of a ``with`` block,
607 or can be explicitly controlled through the :py:meth:`neo4j.Transaction.commit`, :py:meth:`neo4j.Transaction.rollback` or :py:meth:`neo4j.Transaction.close` methods.
608
609 Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction.
610
611 Example:
612
613 .. code-block:: python
614
615 import neo4j
616
617 def create_person(driver, name):
618 with driver.session(default_access_mode=neo4j.WRITE_ACCESS) as session:
619 tx = session.begin_transaction()
620 node_id = create_person_node(tx)
621 set_person_name(tx, node_id, name)
622 tx.commit()
623 tx.close()
624
625 def create_person_node(tx):
626 name = "default_name"
627 result = tx.run("CREATE (a:Person { name: $name }) RETURN id(a) AS node_id", name=name)
628 record = result.single()
629 return record["node_id"]
630
631 def set_person_name(tx, node_id, name):
632 result = tx.run("MATCH (a:Person) WHERE id(a) = $id SET a.name = $name", id=node_id, name=name)
633 info = result.consume()
634 # use the info for logging etc.
635
636 .. _managed-transactions-ref:
637
638 Managed Transactions (`transaction functions`)
639 ==============================================
640 Transaction functions are the most powerful form of transaction, providing access mode override and retry capabilities.
641
642 + :py:meth:`neo4j.Session.write_transaction`
643 + :py:meth:`neo4j.Session.read_transaction`
644
645 These allow a function object representing the transactional unit of work to be passed as a parameter.
646 This function is called one or more times, within a configurable time limit, until it succeeds.
647 Results should be fully consumed within the function and only aggregate or status values should be returned.
648 Returning a live result object would prevent the driver from correctly managing connections and would break retry guarantees.
649
650 Example:
651
652 .. code-block:: python
653
654 def create_person(driver, name)
655 with driver.session() as session:
656 node_id = session.write_transaction(create_person_tx, name)
657
658 def create_person_tx(tx, name):
659 result = tx.run("CREATE (a:Person { name: $name }) RETURN id(a) AS node_id", name=name)
660 record = result.single()
661 return record["node_id"]
662
663 To exert more control over how a transaction function is carried out, the :func:`neo4j.unit_of_work` decorator can be used.
664
665 .. autofunction:: neo4j.unit_of_work
666
667
668
669
670
671
672
673
674
675 ******
676 Result
677 ******
678
679 Every time a query is executed, a :class:`neo4j.Result` is returned.
680
681 This provides a handle to the result of the query, giving access to the records within it as well as the result metadata.
682
683 Results also contain a buffer that automatically stores unconsumed records when results are consumed out of order.
684
685 A :class:`neo4j.Result` is attached to an active connection, through a :class:`neo4j.Session`, until all its content has been buffered or consumed.
686
687 .. autoclass:: neo4j.Result()
688
689 .. describe:: iter(result)
690
691 .. automethod:: keys
692
693 .. automethod:: consume
694
695 .. automethod:: single
696
697 .. automethod:: peek
698
699 .. automethod:: graph
700
701 **This is experimental.** (See :ref:`filter-warnings-ref`)
702
703 .. automethod:: value
704
705 .. automethod:: values
706
707 .. automethod:: data
708
709 See https://neo4j.com/docs/driver-manual/current/cypher-workflow/#driver-type-mapping for more about type mapping.
710
711
712 Graph
713 =====
714
715 .. autoclass:: neo4j.graph.Graph()
716
717 A local, self-contained graph object that acts as a container for :class:`.Node` and :class:`neo4j.Relationship` instances.
718 This is typically obtained via the :meth:`neo4j.Result.graph` method.
719
720 .. autoattribute:: nodes
721
722 .. autoattribute:: relationships
723
724 .. automethod:: relationship_type
725
726 **This is experimental.** (See :ref:`filter-warnings-ref`)
727
728
729 ******
730 Record
731 ******
732
733 .. autoclass:: neo4j.Record()
734
735 A :class:`neo4j.Record` is an immutable ordered collection of key-value
736 pairs. It is generally closer to a :py:class:`namedtuple` than to an
737 :py:class:`OrderedDict` inasmuch as iteration of the collection will
738 yield values rather than keys.
739
740 .. describe:: Record(iterable)
741
742 Create a new record based on an dictionary-like iterable.
743 This can be a dictionary itself, or may be a sequence of key-value pairs, each represented by a tuple.
744
745 .. describe:: record == other
746
747 Compare a record for equality with another value.
748 The `other` value may be any `Sequence` or `Mapping`, or both.
749 If comparing with a `Sequence`, the values are compared in order.
750 If comparing with a `Mapping`, the values are compared based on their keys.
751 If comparing with a value that exhibits both traits, both comparisons must be true for the values to be considered equal.
752
753 .. describe:: record != other
754
755 Compare a record for inequality with another value.
756 See above for comparison rules.
757
758 .. describe:: hash(record)
759
760 Create a hash for this record.
761 This will raise a :exc:`TypeError` if any values within the record are unhashable.
762
763 .. describe:: record[index]
764
765 Obtain a value from the record by index.
766 This will raise an :exc:`IndexError` if the specified index is out of range.
767
768 .. describe:: record[i:j]
769
770 Derive a sub-record based on a start and end index.
771 All keys and values within those bounds will be copied across in the same order as in the original record.
772
773 .. automethod:: keys
774
775 .. describe:: record[key]
776
777 Obtain a value from the record by key.
778 This will raise a :exc:`KeyError` if the specified key does not exist.
779
780 .. automethod:: get(key, default=None)
781
782 .. automethod:: index(key)
783
784 .. automethod:: items
785
786 .. automethod:: value(key=0, default=None)
787
788 .. automethod:: values
789
790 .. automethod:: data
791
792
793
794 *************
795 ResultSummary
796 *************
797
798 .. autoclass:: neo4j.ResultSummary()
799 :members:
800
801 SummaryCounters
802 ===============
803
804 .. autoclass:: neo4j.SummaryCounters()
805 :members:
806
807
808 ServerInfo
809 ==========
810
811 .. autoclass:: neo4j.ServerInfo()
812 :members:
813
814
815
816 ***************
817 Core Data Types
818 ***************
819
820 Cypher supports a set of core data types that all map to built-in types in Python.
821
822 These include the common `Boolean`, `Integer`, `Float` and `String` types as well as `List` and `Map` that can hold heterogenous collections of any other type.
823
824 The core types with their general mappings are listed below:
825
826 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
827 | Cypher Type | Python Type |
828 +========================+===========================================================================================================================+
829 | Null | ``None`` |
830 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
831 | Boolean | ``bool`` |
832 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
833 | Integer | ``int`` |
834 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
835 | Float | ``float`` |
836 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
837 | String | ``str`` |
838 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
839 | Bytes :sup:`[1]` | ``bytearray`` |
840 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
841 | List | ``list`` |
842 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
843 | Map | ``dict`` |
844 +------------------------+---------------------------------------------------------------------------------------------------------------------------+
845
846 .. Note::
847
848 1. `Bytes` is not an actual Cypher type but is transparently passed through when used in parameters or query results.
849
850
851 In reality, the actual conversions and coercions that occur as values are passed through the system are more complex than just a simple mapping.
852 The diagram below illustrates the actual mappings between the various layers, from driver to data store, for the core types.
853
854 .. image:: ./_images/core_type_mappings.svg
855 :target: ./_images/core_type_mappings.svg
856
857
858 ****************
859 Graph Data Types
860 ****************
861
862 Cypher queries can return entire graph structures as well as individual property values.
863
864 The graph data types detailed here model graph data returned from a Cypher query.
865 Graph values cannot be passed in as parameters as it would be unclear whether the entity was intended to be passed by reference or by value.
866 The identity or properties of that entity should be passed explicitly instead.
867
868 The driver contains a corresponding class for each of the graph types that can be returned.
869
870 ============= =================================
871 Cypher Type Python Type
872 ============= =================================
873 Node :class:`neo4j.graph.Node`
874 Relationship :class:`neo4j.graph.Relationship`
875 Path :class:`neo4j.graph.Path`
876 ============= =================================
877
878
879 Node
880 ====
881
882 .. autoclass:: neo4j.graph.Node()
883
884 .. describe:: node == other
885
886 Compares nodes for equality.
887
888 .. describe:: node != other
889
890 Compares nodes for inequality.
891
892 .. describe:: hash(node)
893
894 Computes the hash of a node.
895
896 .. describe:: len(node)
897
898 Returns the number of properties on a node.
899
900 .. describe:: iter(node)
901
902 Iterates through all properties on a node.
903
904 .. describe:: node[key]
905
906 Returns a node property by key.
907 Raises :exc:`KeyError` if the key does not exist.
908
909 .. describe:: key in node
910
911 Checks whether a property key exists for a given node.
912
913 .. autoattribute:: graph
914
915 .. autoattribute:: id
916
917 .. autoattribute:: labels
918
919 .. automethod:: get
920
921 .. automethod:: keys
922
923 .. automethod:: values
924
925 .. automethod:: items
926
927
928 Relationship
929 ============
930
931 .. autoclass:: neo4j.graph.Relationship()
932
933 .. describe:: relationship == other
934
935 Compares relationships for equality.
936
937 .. describe:: relationship != other
938
939 Compares relationships for inequality.
940
941 .. describe:: hash(relationship)
942
943 Computes the hash of a relationship.
944
945 .. describe:: len(relationship)
946
947 Returns the number of properties on a relationship.
948
949 .. describe:: iter(relationship)
950
951 Iterates through all properties on a relationship.
952
953 .. describe:: relationship[key]
954
955 Returns a relationship property by key.
956 Raises :exc:`KeyError` if the key does not exist.
957
958 .. describe:: key in relationship
959
960 Checks whether a property key exists for a given relationship.
961
962 .. describe:: type(relationship)
963
964 Returns the type (class) of a relationship.
965 Relationship objects belong to a custom subtype based on the type name in the underlying database.
966
967 .. autoattribute:: graph
968
969 .. autoattribute:: id
970
971 .. autoattribute:: nodes
972
973 .. autoattribute:: start_node
974
975 .. autoattribute:: end_node
976
977 .. autoattribute:: type
978
979 .. automethod:: get
980
981 .. automethod:: keys
982
983 .. automethod:: values
984
985 .. automethod:: items
986
987
988
989 Path
990 ====
991
992 .. autoclass:: neo4j.graph.Path()
993
994 .. describe:: path == other
995
996 Compares paths for equality.
997
998 .. describe:: path != other
999
1000 Compares paths for inequality.
1001
1002 .. describe:: hash(path)
1003
1004 Computes the hash of a path.
1005
1006 .. describe:: len(path)
1007
1008 Returns the number of relationships in a path.
1009
1010 .. describe:: iter(path)
1011
1012 Iterates through all the relationships in a path.
1013
1014 .. autoattribute:: graph
1015
1016 .. autoattribute:: nodes
1017
1018 .. autoattribute:: start_node
1019
1020 .. autoattribute:: end_node
1021
1022 .. autoattribute:: relationships
1023
1024
1025 ******************
1026 Spatial Data Types
1027 ******************
1028
1029
1030 Cypher has built-in support for handling spatial values (points),
1031 and the underlying database supports storing these point values as properties on nodes and relationships.
1032
1033 https://neo4j.com/docs/cypher-manual/current/syntax/spatial/
1034
1035
1036 ================= =====================================
1037 Cypher Type Python Type
1038 ================= =====================================
1039 Point :class:`neo4j.spatial.Point`
1040
1041 Point (Cartesian) :class:`neo4j.spatial.CartesianPoint`
1042 Point (WGS-84) :class:`neo4j.spatial.WGS84Point`
1043 ================= =====================================
1044
1045
1046 Point
1047 =====
1048
1049
1050 .. autoclass:: neo4j.spatial.Point
1051 :members:
1052
1053
1054 CartesianPoint
1055 ==============
1056
1057 .. autoclass:: neo4j.spatial.CartesianPoint
1058 :show-inheritance:
1059
1060 .. autoproperty:: srid
1061
1062 .. autoproperty:: x
1063
1064 .. autoproperty:: y
1065
1066 .. autoproperty:: z
1067
1068 .. automethod:: count
1069
1070 .. automethod:: index
1071
1072
1073 .. code-block:: python
1074
1075 point=CartesianPoint((1.23, 4.56)
1076
1077 print(point.x, point.y)
1078
1079
1080 .. code-block:: python
1081
1082 point=CartesianPoint((1.23, 4.56, 7.89)
1083
1084 print(point.x, point.y, point.z)
1085
1086
1087 WGS84Point
1088 ==========
1089
1090 .. autoclass:: neo4j.spatial.WGS84Point
1091 :show-inheritance:
1092
1093 .. autoproperty:: srid
1094
1095 .. autoproperty:: x
1096
1097 .. autoproperty:: y
1098
1099 .. autoproperty:: z
1100
1101 .. autoproperty:: longitude
1102
1103 .. autoproperty:: latitude
1104
1105 .. autoproperty:: height
1106
1107 .. automethod:: count
1108
1109 .. automethod:: index
1110
1111
1112
1113
1114 .. code-block:: python
1115
1116 point=WGS84Point((1.23, 4.56))
1117 print(point.longitude, point.latitude)
1118
1119
1120 .. code-block:: python
1121
1122 point=WGS84Point((1.23, 4.56, 7.89))
1123 print(point.longitude, point.latitude, point.height)
1124
1125
1126 *******************
1127 Temporal Data Types
1128 *******************
1129
1130 Cypher has built-in support for handling temporal values,
1131 and the underlying database supports storing these temporal values as properties on nodes and relationships.
1132
1133 https://neo4j.com/docs/cypher-manual/current/syntax/temporal/
1134
1135 Temporal data types are implemented by the ``neo4j.time``
1136
1137 These provide a set of types compliant with ISO-8601 and Cypher, which are similar to those found in the built-in ``datetime`` module.
1138 Sub-second values are measured to nanosecond precision and the types are compatible with `pytz <http://pytz.sourceforge.net/>`_.
1139
1140 The table below shows the general mappings between Cypher and the temporal types provided by the driver.
1141 In addition, the built-in temporal types can be passed as parameters and will be mapped appropriately.
1142
1143 ============= ============================ ================================== ============
1144 Cypher Python driver type Python built-in type ``tzinfo``
1145 ============= ============================ ================================== ============
1146 Date :class:`neo4j.time.Date` :class:`python:datetime.date`
1147 Time :class:`neo4j.time.Time` :class:`python:datetime.time` ``not None``
1148 LocalTime :class:`neo4j.time.Time` :class:`python:datetime.time` ``None``
1149 DateTime :class:`neo4j.time.DateTime` :class:`python:datetime.datetime` ``not None``
1150 LocalDateTime :class:`neo4j.time.DateTime` :class:`python:datetime.datetime` ``None``
1151 Duration :class:`neo4j.time.Duration` :class:`python:datetime.timedelta`
1152 ============= ============================ ================================== ============
1153
1154
1155 See topic :ref:`temporal-data-types` for more details.
1156
1157
1158 .. _errors-ref:
1159
1160 ******
1161 Errors
1162 ******
1163
1164
1165 Neo4j Errors
1166 ============
1167
1168 Neo4j Execution Errors
1169
1170
1171 * :class:`neo4j.exceptions.Neo4jError`
1172
1173 * :class:`neo4j.exceptions.ClientError`
1174
1175 * :class:`neo4j.exceptions.DatabaseError`
1176
1177 * :class:`neo4j.exceptions.TransientError`
1178
1179
1180 .. autoclass:: neo4j.exceptions.Neo4jError
1181
1182 .. autoproperty:: message
1183
1184 .. autoproperty:: code
1185
1186 There are many Neo4j status codes, see `status code <https://neo4j.com/docs/status-codes/current/>`_.
1187
1188 .. autoproperty:: classification
1189
1190 .. autoproperty:: category
1191
1192 .. autoproperty:: title
1193
1194
1195 .. autoclass:: neo4j.exceptions.ClientError
1196 :show-inheritance:
1197
1198 .. autoclass:: neo4j.exceptions.CypherSyntaxError
1199 :show-inheritance:
1200
1201 .. autoclass:: neo4j.exceptions.CypherTypeError
1202 :show-inheritance:
1203
1204 .. autoclass:: neo4j.exceptions.ConstraintError
1205 :show-inheritance:
1206
1207 .. autoclass:: neo4j.exceptions.AuthError
1208 :show-inheritance:
1209
1210 .. autoclass:: neo4j.exceptions.Forbidden
1211 :show-inheritance:
1212
1213 .. autoclass:: neo4j.exceptions.ForbiddenOnReadOnlyDatabase
1214 :show-inheritance:
1215
1216 .. autoclass:: neo4j.exceptions.NotALeader
1217 :show-inheritance:
1218
1219 .. autoclass:: neo4j.exceptions.DatabaseError
1220 :show-inheritance:
1221
1222 .. autoclass:: neo4j.exceptions.TransientError
1223 :show-inheritance:
1224
1225 .. autoclass:: neo4j.exceptions.DatabaseUnavailable
1226 :show-inheritance:
1227
1228
1229
1230 Driver Errors
1231 =============
1232
1233 Connectivity Errors
1234
1235
1236 * :class:`neo4j.exceptions.DriverError`
1237
1238 * :class:`neo4j.exceptions.TransactionError`
1239
1240 * :class:`neo4j.exceptions.SessionExpired`
1241
1242 * :class:`neo4j.exceptions.ServiceUnavailable`
1243
1244 * :class:`neo4j.exceptions.ConfigurationError`
1245
1246 * :class:`neo4j.exceptions.ResultConsumedError`
1247
1248
1249 .. autoclass:: neo4j.exceptions.DriverError
1250
1251
1252 .. autoclass:: neo4j.exceptions.TransactionError
1253 :show-inheritance:
1254
1255 .. autoclass:: neo4j.exceptions.TransactionNestingError
1256 :show-inheritance:
1257
1258 .. autoclass:: neo4j.exceptions.SessionExpired
1259 :show-inheritance:
1260
1261 .. autoclass:: neo4j.exceptions.ServiceUnavailable
1262 :show-inheritance:
1263
1264 Raised when a database server or service is not available.
1265 This may be due to incorrect configuration or could indicate a runtime failure of a database service that the driver is unable to route around.
1266
1267 .. autoclass:: neo4j.exceptions.RoutingServiceUnavailable
1268 :show-inheritance:
1269
1270 .. autoclass:: neo4j.exceptions.WriteServiceUnavailable
1271 :show-inheritance:
1272
1273 .. autoclass:: neo4j.exceptions.ReadServiceUnavailable
1274 :show-inheritance:
1275
1276 .. autoclass:: neo4j.exceptions.ConfigurationError
1277 :show-inheritance:
1278
1279 .. autoclass:: neo4j.exceptions.AuthConfigurationError
1280 :show-inheritance:
1281
1282 .. autoclass:: neo4j.exceptions.CertificateConfigurationError
1283 :show-inheritance:
1284
1285 .. autoclass:: neo4j.exceptions.ResultConsumedError
1286 :show-inheritance:
1287
1288
1289
1290 Internal Driver Errors
1291 =======================
1292
1293 If an internal error (BoltError), in particular a protocol error (BoltProtocolError) is surfaced please open an issue on github.
1294
1295 https://github.com/neo4j/neo4j-python-driver/issues
1296
1297 Please provide details about your running environment,
1298
1299 + Operating System:
1300 + Python Version:
1301 + Python Driver Version:
1302 + Neo4j Version:
1303 + The code block with a description that produced the error:
1304 + The error message:
1305
1306
1307 ********
1308 Warnings
1309 ********
1310
1311 The Python Driver uses the built-in :class:`python:DeprecationWarning` class to warn about deprecations.
1312
1313 The Python Driver uses the :class:`neo4j.ExperimentalWarning` class to warn about experimental features.
1314
1315 .. autoclass:: neo4j.ExperimentalWarning
1316
1317
1318 .. _filter-warnings-ref:
1319
1320 Filter Warnings
1321 ===============
1322
1323 This example shows how to suppress the :class:`neo4j.ExperimentalWarning` using the :func:`python:warnings.filterwarnings` function.
1324
1325 .. code-block:: python
1326
1327 import warnings
1328 from neo4j import ExperimentalWarning
1329
1330 warnings.filterwarnings("ignore", category=ExperimentalWarning)
1331
1332
1333 ********
1334 Bookmark
1335 ********
1336
1337 .. autoclass:: neo4j.Bookmark
1338 :members:
0 .. _breaking-changes:
1
2 ****************
3 Breaking Changes
4 ****************
5
6 This describes the breaking changes between Python Driver 1.7 and Python Driver 4.0
7
8 Version Scheme Changes
9 ======================
10
11 The version number has jumped from **Python Driver 1.7** to **Python Driver 4.0** to align with the Neo4j Database version scheme.
12
13 Python Versions
14 ===============
15
16 Python 2.7 is no longer supported.
17
18
19 Namespace Changes
20 =================
21
22 .. code-block:: python
23
24 import neo4j.v1
25
26 Has changed to
27
28 .. code-block:: python
29
30 import neo4j
31
32
33 Secure Connection
34 =================
35
36 **Neo4j 4.0** is by default configured to use a **unsecured connection**.
37
38 The driver configuration argument :code:`encrypted` is by default set to :code:`False`.
39
40 **Note:** To be able to connect to **Neo4j 3.5** set :code:`encrypted=True` to have it configured as the default for that setup.
41
42 .. code-block:: python
43
44 from neo4j import GraphDatabase
45
46 driver = GraphDatabase("bolt://localhost:7687", auth=("neo4j", "password"), encrypted=True)
47 driver.close()
48
49
50 Bookmark Changes
51 ================
52
53 Introduced :class:`neo4j.Bookmark`
54
55
56 Exceptions Changes
57 ==================
58
59 The exceptions in :code:`neo4j.exceptions` has been updated and there are internal exceptions starting with the naming :code:`Bolt` that should be propagated into the exceptions API.
60
61 See :ref:`errors-ref` for more about errors.
62
63 URI Scheme Changes
64 ==================
65
66 **bolt+routing** has been renamed to **neo4j**.
67
68
69 Class Renaming Changes
70 ======================
71
72 * :code:`BoltStatementResult` is now :code:`Result`
73 * :code:`StatementResultSummary` is now :code:`ResultSummary`
74 * :code:`Statement` is now :code:`Query`
75
76
77 Argument Renaming Changes
78 =========================
79
80 * :code:`statement` is now :code:`query`
81 * :code:`cypher` is now :code:`query`
82 * :code:`Session.run(cypher, ...` is now :code:`Session.run(query, ...`
83 * :code:`Transaction.run(statement, ...` is now :code:`Transaction.run(query, ...`
84 * :code:`StatementResultSummary.statement` is now :code:`ResultSummary.query`
85 * :code:`StatementResultSummary.statement_type` is now :code:`ResultSummary.query_type`
86 * :code:`StatementResultSummary.protocol_version` is now :code:`ResultSummary.server.protocol_version`
87
88 API Changes
89 =========================
90
91 * :code:`Result.summary()` has been replaced with :code:`Result.consume()`, this behaviour is to consume all remaining records in the buffer and returns the ResultSummary.
92
93 * :code:`Result.data(*items)` has been changed to :code:`Result.data(*keys)` for alignment with :code:`Record.data(*keys)`.
94
95 * :code:`Result.value(item=0, default=None)` has been changed to :code:`Result.value(key=0, default=None)` for alignment with :code:`Record.value(key=0, default=None)`.
96
97 * :code:`Result.values(*items)` has been changed to :code:`Result.values(*keys)` for alignment with :code:`Record.values(*keys)`.
98
99 * :code:`Transaction.sync()` has been removed. Use :code:`Result.consume()` if the behaviour is to exhaust the result object.
100
101 * :code:`Transaction.success` has been removed.
102
103 * :code:`Transaction.close()` behaviour changed. Will now only perform rollback if no commit have been performed.
104
105 * :code:`Session.sync()` has been removed. Use :code:`Result.consume()` if the behaviour is to exhaust the result object.
106
107 * :code:`Session.detach()` has been removed. Use :code:`Result.consume()` if the behaviour is to exhaust the result object.
108
109 * :code:`Session.next_bookmarks()` has been removed.
110
111 * :code:`Session.has_transaction()` has been removed.
112
113 * :code:`Session.closed()` has been removed.
114
115 * :code:`Session.write_transaction` and :code:`Session.read_transaction` will start the retry timer after the first failed attempt.
116
117 Dependency Changes
118 ==================
119
120 * The dependency :code:`neobolt` has been removed.
121 * The dependency :code:`neotime` has been removed.
122 * The :code:`pytz` is now a dependency.
123
124 Configuration Name Changes
125 ==========================
126
127 * :code:`max_retry_time` is now :code:`max_transaction_retry_time`
5454 master_doc = 'index'
5555
5656 # General information about the project.
57 project = 'Neo4j Bolt Driver for Python'
58 copyright = '2002-2019, Neo Technology'
59 author = 'Neo Technology'
57 project = 'Neo4j Python Driver'
58 copyright = '2002-2020 Neo4j, Inc.'
59 author = 'Neo4j, Inc.'
6060
6161 # The version info for the project you're documenting, acts as replacement for
6262 # |version| and |release|, also used in various other places throughout the
101101 #show_authors = False
102102
103103 # The name of the Pygments (syntax highlighting) style to use.
104 pygments_style = 'sphinx'
104 # pygments_style = 'sphinx'
105 pygments_style = 'friendly'
105106
106107 # A list of ignored prefixes for module index sorting.
107108 #modindex_common_prefix = []
114115
115116
116117 # -- Options for HTML output ----------------------------------------------
118
119 # Add any paths that contain custom static files (such as style sheets) here,
120 # relative to this directory. They are copied after the builtin static files,
121 # so a file named "default.css" will overwrite the builtin "default.css".
122 html_static_path = ['_static']
117123
118124 # The theme to use for HTML and HTML Help pages. See the documentation for
119125 # a list of builtin themes.
120126 html_theme = 'nature'
127
128 # _static/nature_custom.css_t
129 html_style = 'nature_custom.css'
130
131 html_js_files = [
132 'anchor_new_target.js',
133 ]
121134
122135 # Theme options are theme-specific and customize the look and feel of a theme
123136 # further. For a list of options available for each theme, see the
129142
130143 # The name for this set of Sphinx documents. If None, it defaults to
131144 # "<project> v<release> documentation".
132 html_title = "Neo4j Bolt Driver %s for Python" % version
145 html_title = "Neo4j Python Driver {}".format(version)
133146
134147 # A shorter title for the navigation bar. Default is the same as html_title.
135148 #html_short_title = None
142155 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
143156 # pixels large.
144157 #html_favicon = None
145
146 # Add any paths that contain custom static files (such as style sheets) here,
147 # relative to this directory. They are copied after the builtin static files,
148 # so a file named "default.css" will overwrite the builtin "default.css".
149 html_static_path = ['_static']
150158
151159 # Add any extra paths that contain custom files (such as robots.txt or
152160 # .htaccess) here, relative to this directory. These files are copied
232240 # author, documentclass [howto, manual, or own class]).
233241 latex_documents = [
234242 (master_doc, 'Neo4jBoltDriverforPython.tex', 'Neo4j Bolt Driver for Python Documentation',
235 'Neo Technology', 'manual'),
243 'Neo4j', 'manual'),
236244 ]
237245
238246 # The name of an image file (relative to this directory) to place at the top of
292300 # If true, do not generate a @detailmenu in the "Top" node's menu.
293301 #texinfo_no_detailmenu = False
294302
295
296 def setup(app):
297 app.add_stylesheet('custom.css')
298
299
300303 # -- Options for Intersphinx
301304
302305 intersphinx_mapping = {
303306 'python': ('https://docs.python.org/3', None),
304 'neobolt': ('https://neobolt.readthedocs.io/en/latest/', None),
305 'neotime': ('https://neotime.readthedocs.io/en/latest/', None),
306307 }
308
309 autodoc_default_options = {
310 'member-order': 'bysource',
311 # 'special-members': '__init__',
312 'undoc-members': True,
313 'exclude-members': '__weakref__',
314 }
315
316 # Do not warn about files not included
317 exclude_patterns = [
318 'driver.rst',
319 'errors.rst',
320 'results.rst',
321 'transactions.rst',
322 'usage_patterns.rst',
323 'types/*.rst',
324 ]
11 Driver Objects
22 ==============
33
4 Every Neo4j-backed application will require a :class:`.Driver` object.
4 Every Neo4j-backed application will require a :class:`neo4j.Driver` object.
55 This object holds the details required to establish connections with a Neo4j database, including server URIs, credentials and other configuration.
6 :class:`.Driver` objects hold a connection pool from which :class:`.Session` objects can borrow connections.
6 :class:`neo4j.Driver` objects hold a connection pool from which :class:`neo4j.Session` objects can borrow connections.
77 Closing a driver will immediately shut down all connections in the pool.
88
99 Construction
1010 ============
1111
12 :class:`.Driver` construction can either be carried out directly or via a `classmethod` on the :class:`.GraphDatabase` class.
12 :class:`neo4j.Driver` construction can either be carried out directly or via a `classmethod` on the :class:`neo4j.GraphDatabase` class.
1313
1414 .. autoclass:: neo4j.GraphDatabase
1515 :members: driver
1616
17 .. autoclass:: neo4j.Driver(uri, **config)
17 .. autoclass:: neo4j.Driver()
1818 :members: session, close, closed
1919
2020
2121 URI
2222 ===
2323
24 On construction, the scheme of the URI determines the type of :class:`.Driver` object created.
25 Each supported scheme maps to a particular :class:`.Driver` subclass that implements a specific behaviour.
26 The remainder of the URI should be considered subclass-specific.
24 On construction, the `scheme` of the URI determines the type of :class:`neo4j.Driver` object created.
25
26 Example URI::
27
28 uri = bolt://localhost:7687
29
30 Example URI::
31
32 uri = neo4j://localhost:7687
33
34 Each supported scheme maps to a particular :class:`neo4j.Driver` subclass that implements a specific behaviour.
2735
2836 The alternative behaviours are described in the subsections below.
2937
3038
31 Bolt Direct
32 -----------
39 BoltDriver
40 ----------
3341
34 URI scheme:
35 ``bolt``
42 URI schemes:
43 ``bolt``, ``bolt+ssc``, ``bolt+s``
3644 Driver subclass:
37 :class:`.DirectDriver`
45 :class:`neo4j.BoltDriver`
3846
39 .. autoclass:: neo4j.DirectDriver
47 .. autoclass:: neo4j.BoltDriver
4048
4149
42 Bolt Routing
50 Neo4jDriver
4351 ------------
4452
45 URI scheme:
46 ``bolt+routing``
53 URI schemes:
54 ``neo4j``, ``neo4j+ssc``, ``neo4j+s``
4755 Driver subclass:
48 :class:`.RoutingDriver`
56 :class:`neo4j.Neo4jDriver`
4957
50 .. autoclass:: neo4j.RoutingDriver
58 .. autoclass:: neo4j.Neo4jDriver
5159
5260
5361 Configuration
5462 =============
5563
56 Additional configuration, including authentication details, can be provided via the :class:`.Driver` constructor.
64 Additional configuration, including authentication details, can be provided via the :class:`neo4j.Driver` constructor.
5765
5866 ``auth``
5967 --------
7078 -------------
7179
7280 A boolean indicating whether or not TLS should be used for connections.
73 Defaults to :py:const:`True` if TLS is available.
81
82 :Type: ``bool``
83 :Default: :py:const:`True`
84
7485
7586 ``trust``
7687 ---------
7788
7889 The trust level for certificates received from the server during TLS negotiation.
7990 This setting does not have any effect if ``encrypted`` is set to :py:const:`False`.
91
8092
8193 .. py:attribute:: neo4j.TRUST_ALL_CERTIFICATES
8294
91103 certificate authority. This option is primarily intended for use with
92104 full certificates.
93105
94 ``der_encoded_server_certificate``
95 ----------------------------------
96
97 The server certificate in DER format, if required.
106 :Default: ``neo4j.TRUST_SYSTEM_CA_SIGNED_CERTIFICATES``.
98107
99108 ``user_agent``
100109 --------------
116125 ----------------------------------
117126
118127 The maximum time to wait for a connection to be acquired from the pool.
119
120 ``connection_timeout``
121 ----------------------
122
123 The maximum time to wait for a new connection to be established.
124128
125129 ``keep_alive``
126130 --------------
151155 from socket import gaierror
152156 raise gaierror("Unexpected socket address %r" % socket_address)
153157
154 driver = GraphDatabase.driver("bolt+routing://foo:9999", auth=("neo4j", "password"), resolver=my_resolver)
158 driver = GraphDatabase.driver("neo4j://foo:9999", auth=("neo4j", "password"), resolver=my_resolver)
155159
156160
157161
158162 Object Lifetime
159163 ===============
160164
161 For general applications, it is recommended to create one top-level :class:`.Driver` object that lives for the lifetime of the application.
165 For general applications, it is recommended to create one top-level :class:`neo4j.Driver` object that lives for the lifetime of the application.
162166 For example:
163167
164168 .. code-block:: python
165169
166170 from neo4j import GraphDatabase
167171
168 class Application(object):
172 class Application:
169173
170174 def __init__(self, uri, user, password)
171175 self.driver = GraphDatabase.driver(uri, auth=(user, password))
173177 def close(self):
174178 self.driver.close()
175179
176 Connection details held by the :class:`.Driver` are immutable.
177 Therefore if, for example, a password is changed, a replacement :class:`.Driver` object must be created.
180 Connection details held by the :class:`neo4j.Driver` are immutable.
181 Therefore if, for example, a password is changed, a replacement :class:`neo4j.Driver` object must be created.
178182 More than one :class:`.Driver` may be required if connections to multiple databases, or connections as multiple users, are required.
179183
180 :class:`.Driver` objects are thread-safe but cannot be shared across processes.
184 :class:`neo4j.Driver` objects are thread-safe but cannot be shared across processes.
181185 Therefore, ``multithreading`` should generally be preferred over ``multiprocessing`` for parallel database access.
182 If using ``multiprocessing`` however, each process will require its own :class:`.Driver` object.
186 If using ``multiprocessing`` however, each process will require its own :class:`neo4j.Driver` object.
1010 Raised when a database server or service is not available.
1111 This may be due to incorrect configuration or could indicate a runtime failure of a database service that the driver is unable to route around.
1212
13 .. class:: neo4j.exceptions.SecurityError
1413
15 Raised when a security issue occurs, generally around TLS or authentication.
16
17
18 Cypher execution errors
14 Neo4j execution errors
1915 =======================
2016
21 .. class:: neo4j.exceptions.CypherError
17 .. class:: neo4j.exceptions.Neo4jError
2218
2319 Raised when the Cypher engine returns an error to the client.
24 There are many possible types of Cypher error, each identified by a unique `status code <https://neo4j.com/docs/developer-manual/current/reference/status-codes/>`_.
20 There are many possible types of Cypher error, each identified by a unique `status code <https://neo4j.com/docs/status-codes/current/>`_.
2521
26 The three classifications of status code are supported by the three subclasses of :class:`.CypherError`, listed below:
22 The three classifications of status code are supported by the three subclasses of :class:`.Neo4jError`, listed below:
2723
2824 .. autoclass:: neo4j.exceptions.ClientError
2925
3228 .. autoclass:: neo4j.exceptions.TransientError
3329
3430
35 Low-level errors
36 ================
31 Internal Driver Errors
32 =======================
3733
38 .. class:: neo4j.exceptions.ProtocolError
34 If users see an internal error, in particular a protocol error (BoltError*), they should open an issue on github.
3935
40 Raised when an unexpected or unsupported protocol event occurs.
41 This error generally indicates a fault with the driver or server software.
42 If you receive this error, please raise a GitHub issue or a support ticket.
36 https://github.com/neo4j/neo4j-python-driver/issues
37
38 Please provide details about your running environment,
39
40 Operating System:
41 Python Version:
42 Python Driver Version:
43 Neo4j Version:
44
45 the code block with a description that produced the error and the error message.
0 **************************************
1 Neo4j Bolt Driver |version| for Python
2 **************************************
3
4 The Official Neo4j Driver for Python supports Neo4j 3.2 and above and requires Python version 2.7 or 3.4+.
5 Note that support for Python 2.7 will be removed in the 2.0 driver.
6
7
0 #############################
1 Neo4j Python Driver |version|
2 #############################
3
4 The Official Neo4j Driver for Python.
5
6 Neo4j versions supported:
7
8 * Neo4j 4.1
9 * Neo4j 4.0
10 * Neo4j 3.5
11
12 Python versions supported:
13
14 * Python 3.8
15 * Python 3.7
16 * Python 3.6
17 * Python 3.5
18
19 .. note::
20
21 The `Python Driver 1.7`_ supports older versions of python, **Neo4j 4.1** will work in fallback mode with that driver.
22
23
24 ******
25 Topics
26 ******
27
28 + :ref:`api-documentation`
29
30 + :ref:`temporal-data-types`
31
32 + :ref:`breaking-changes`
33
34
35 .. toctree::
36 :hidden:
37
38 api.rst
39 temporal_types.rst
40 breaking_changes.rst
41
42
43 ************
44 Installation
45 ************
46
47 To install the latest stable release, use:
48
49 .. code:: bash
50
51 python -m pip install neo4j
52
53
54 To install the latest pre-release, use:
55
56 .. code:: bash
57
58 python -m pip install --pre neo4j
59
60
61 .. note::
62
63 It is always recommended to install python packages for user space in a virtual environment.
64
65
66 Virtual Environment
67 ===================
68
69 To create a virtual environment named sandbox, use:
70
71 .. code:: bash
72
73 python -m venv sandbox
74
75 To activate the virtual environment named sandbox, use:
76
77 .. code:: bash
78
79 source sandbox/bin/activate
80
81 To deactivate the current active virtual environment, use:
82
83 .. code:: bash
84
85 deactivate
86
87
88 *************
889 Quick Example
9 =============
90 *************
91
92 Creating nodes.
1093
1194 .. code-block:: python
1295
1396 from neo4j import GraphDatabase
1497
15 uri = "bolt://localhost:7687"
98 uri = "neo4j://localhost:7687"
1699 driver = GraphDatabase.driver(uri, auth=("neo4j", "password"))
17100
18 def print_friends_of(tx, name):
19 for record in tx.run("MATCH (a:Person)-[:KNOWS]->(f) "
20 "WHERE a.name = {name} "
21 "RETURN f.name", name=name):
22 print(record["f.name"])
101 def create_friend_of(tx, name, friend):
102 tx.run("CREATE (a:Person)-[:KNOWS]->(f:Person {name: $friend}) "
103 "WHERE a.name = $name "
104 "RETURN f.name AS friend", name=name, friend=friend)
23105
24106 with driver.session() as session:
25 session.read_transaction(print_friends_of, "Alice")
26
27
28 .. note::
29
30 While imports from ``neo4j.v1`` still work, these will be removed in the 2.0 driver.
31 It is therefore recommended to change all imports from ``neo4j.v1`` to ``neo4j``.
32
33
34 Installation
35 ============
36
37 To install the latest stable driver release, use:
38
39 .. code:: bash
40
41 pip install neo4j
42
43 .. note::
44
45 The driver is currently released under two package names on `PyPI <https://pypi.org/>`_: ``neo4j`` and ``neo4j-driver``.
46 Installing from ``neo4j`` is recommended since ``neo4j-driver`` will be removed in a future release.
47
48
49 API Documentation
50 =================
51
52 .. toctree::
53 :maxdepth: 1
54
55 driver
56 transactions
57 results
58 types/core
59 types/graph
60 types/spatial
61 types/temporal
62 errors
63
64
107 session.write_transaction(create_friend_of, "Alice", "Bob")
108
109 with driver.session() as session:
110 session.write_transaction(create_friend_of, "Alice", "Carl")
111
112 driver.close()
113
114
115 Finding nodes.
116
117 .. code-block:: python
118
119 from neo4j import GraphDatabase
120
121 uri = "neo4j://localhost:7687"
122 driver = GraphDatabase.driver(uri, auth=("neo4j", "password"))
123
124 def get_friends_of(tx, name):
125 friends = []
126 result = tx.run("MATCH (a:Person)-[:KNOWS]->(f) "
127 "WHERE a.name = $name "
128 "RETURN f.name AS friend", name=name):
129 for record in result:
130 friends.append(record["friend"])
131 return friends
132
133 with driver.session() as session:
134 friends = session.read_transaction(get_friends_of, "Alice")
135 for friend in friends:
136 print(friend)
137
138 driver.close()
139
140
141 *******************
142 Example Application
143 *******************
144
145 .. code-block:: python
146
147 import logging
148 from neo4j import GraphDatabase
149 from neo4j.exceptions import ServiceUnavailable
150
151 class App:
152
153 def __init__(self, uri, user, password):
154 self.driver = GraphDatabase.driver(uri, auth=(user, password))
155
156 def close(self):
157 # Don't forget to close the driver connection when you are finished with it
158 self.driver.close()
159
160 def create_friendship(self, person1_name, person2_name):
161 with self.driver.session() as session:
162 # Write transactions allow the driver to handle retries and transient errors
163 result = session.write_transaction(
164 self._create_and_return_friendship, person1_name, person2_name)
165 for record in result:
166 print("Created friendship between: {p1}, {p2}".format(
167 p1=record['p1'], p2=record['p2']))
168
169 @staticmethod
170 def _create_and_return_friendship(tx, person1_name, person2_name):
171
172 # To learn more about the Cypher syntax,
173 # see https://neo4j.com/docs/cypher-manual/current/
174
175 # The Reference Card is also a good resource for keywords,
176 # see https://neo4j.com/docs/cypher-refcard/current/
177
178 query = (
179 "CREATE (p1:Person { name: $person1_name }) "
180 "CREATE (p2:Person { name: $person2_name }) "
181 "CREATE (p1)-[:KNOWS]->(p2) "
182 "RETURN p1, p2"
183 )
184 result = tx.run(query, person1_name=person1_name, person2_name=person2_name)
185 try:
186 return [{"p1": record["p1"]["name"], "p2": record["p2"]["name"]}
187 for record in result]
188 # Capture any errors along with the query and data for traceability
189 except ServiceUnavailable as exception:
190 logging.error("{query} raised an error: \n {exception}".format(
191 query=query, exception=exception))
192 raise
193
194 def find_person(self, person_name):
195 with self.driver.session() as session:
196 result = session.read_transaction(self._find_and_return_person, person_name)
197 for record in result:
198 print("Found person: {record}".format(record=record))
199
200 @staticmethod
201 def _find_and_return_person(tx, person_name):
202 query = (
203 "MATCH (p:Person) "
204 "WHERE p.name = $person_name "
205 "RETURN p.name AS name"
206 )
207 result = tx.run(query, person_name=person_name)
208 return [record["name"] for record in result]
209
210 if __name__ == "__main__":
211 # See https://neo4j.com/developer/aura-connect-driver/ for Aura specific connection URL.
212 scheme = "neo4j" # Connecting to Aura, use the "neo4j+s" URI scheme
213 host_name = "example.com"
214 port = 7687
215 url = "{scheme}://{host_name}:{port}".format(scheme=scheme, host_name=host_name, port=port)
216 user = "<Username for Neo4j database>"
217 password = "<Password for Neo4j database>"
218 app = App(url, user, password)
219 app.create_friendship("Alice", "David")
220 app.find_person("Alice")
221 app.close()
222
223
224 *****************
65225 Other Information
66 =================
67
68 * `Neo4j Manual`_
226 *****************
227
228 * `Neo4j Documentation`_
229 * `The Neo4j Drivers Manual`_
69230 * `Neo4j Quick Reference Card`_
70231 * `Example Project`_
71232 * `Driver Wiki`_ (includes change logs)
72
73 .. _`Neo4j Manual`: https://neo4j.com/docs/
233 * `Migration Guide - Upgrade Neo4j drivers`_
234 * `Neo4j Aura`_
235
236 .. _`Python Driver 1.7`: https://neo4j.com/docs/api/python-driver/1.7/
237 .. _`Neo4j Documentation`: https://neo4j.com/docs/
238 .. _`The Neo4j Drivers Manual`: https://neo4j.com/docs/driver-manual/current/
74239 .. _`Neo4j Quick Reference Card`: https://neo4j.com/docs/cypher-refcard/current/
75240 .. _`Example Project`: https://github.com/neo4j-examples/movies-python-bolt
76241 .. _`Driver Wiki`: https://github.com/neo4j/neo4j-python-driver/wiki
242 .. _`Migration Guide - Upgrade Neo4j drivers`: https://neo4j.com/docs/migration-guide/4.0/upgrade-driver/
243 .. _`Neo4j Aura`: https://neo4j.com/neo4j-aura/
11 Consuming Results
22 *****************
33
4 Every time Cypher is executed, a :class:`.BoltStatementResult` is returned.
4 Every time a query is executed, a :class:`.Result` is returned.
5
56 This provides a handle to the result of the query, giving access to the records within it as well as the result metadata.
67
78 Each result consists of header metadata, zero or more :class:`.Record` objects and footer metadata (the summary).
89 Results also contain a buffer that automatically stores unconsumed records when results are consumed out of order.
9 A :class:`.BoltStatementResult` is attached to an active connection, through a :class:`.Session`, until all its content has been buffered or consumed.
1010
11 .. class:: neo4j.BoltStatementResult
11 A :class:`.Result` is attached to an active connection, through a :class:`.Session`, until all its content has been buffered or consumed.
12
13 .. class:: .Result
1214
1315 .. describe:: iter(result)
1416
2123 .. automethod:: keys
2224
2325 .. automethod:: records
24
25 .. automethod:: summary
2626
2727 .. automethod:: consume
2828
3939 .. automethod:: data
4040
4141
42 .. class:: neo4j.Record
42 .. class:: .Record
4343
4444 A :class:`.Record` is an immutable ordered collection of key-value
45 pairs. It is generally closer to a :py:class:`namedtuple` than to a
45 pairs. It is generally closer to a :py:class:`namedtuple` than to an
4646 :py:class:`OrderedDict` inasmuch as iteration of the collection will
4747 yield values rather than keys.
4848
102102 Summary Details
103103 ---------------
104104
105 .. autoclass:: neo4j.BoltStatementResultSummary
105 .. autoclass:: .ResultSummary
106106 :members:
107107
108 .. autoclass:: neo4j.SummaryCounters
108 .. autoclass:: .SummaryCounters
109109 :members:
0 .. _temporal-data-types:
1
2 *******************
3 Temporal Data Types
4 *******************
5
6 The table below shows the general mappings between Cypher and the temporal types provided by the Python Driver.
7
8 In addition, the built-in temporal types can be passed as parameters and will be mapped appropriately.
9
10 ============= ============================ ================================== ============
11 Cypher Python driver type Python built-in type ``tzinfo``
12 ============= ============================ ================================== ============
13 Date :class:`neo4j.time.Date` :class:`python:datetime.date`
14 Time :class:`neo4j.time.Time` :class:`python:datetime.time` ``not None``
15 LocalTime :class:`neo4j.time.Time` :class:`python:datetime.time` ``None``
16 DateTime :class:`neo4j.time.DateTime` :class:`python:datetime.datetime` ``not None``
17 LocalDateTime :class:`neo4j.time.DateTime` :class:`python:datetime.datetime` ``None``
18 Duration :class:`neo4j.time.Duration` :class:`python:datetime.timedelta`
19 ============= ============================ ================================== ============
20
21
22 Sub-second values are measured to nanosecond precision and the types are compatible with `pytz <http://pytz.sourceforge.net/>`_.
23
24
25 .. Note::
26
27 Cypher has built-in support for handling temporal values, see https://neo4j.com/docs/cypher-manual/current/syntax/temporal/
28
29
30 Constants
31 =========
32
33 .. autodata:: neo4j.time.MIN_YEAR
34
35 .. autodata:: neo4j.time.MAX_YEAR
36
37
38 Date
39 ====
40
41 A :class:`neo4j.time.Date` object represents a date in the `proleptic Gregorian Calendar <https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar>`_.
42
43 Years between `0001` and `9999` are supported, with additional support for the "zero date" used in some contexts.
44
45 Each date is based on a proleptic Gregorian ordinal, which models 1 Jan 0001 as `day 1` and counts each subsequent day up to, and including, 31 Dec 9999.
46 The standard `year`, `month` and `day` value of each date is also available.
47
48 Internally, the day of the month is always stored as-is, with the exception of the last three days of that month.
49 These are always stored as -1, -2 and -3 (counting from the last day).
50 This system allows some temporal arithmetic (particularly adding or subtracting months) to produce a more desirable outcome than would otherwise be produced.
51 Externally, the day number is always the same as would be written on a calendar.
52
53
54 Constructors and other class methods
55 ------------------------------------
56
57 .. class:: neo4j.time.Date(year, month, day)
58
59 Construct a new :class:`neo4j.time.Date` object.
60 All arguments are required and should be integers.
61 For general dates, the following ranges are supported:
62
63 ========= ======================== ===================================
64 Argument Minimum Maximum
65 --------- ------------------------ -----------------------------------
66 ``year`` :attr:`.MIN_YEAR` (0001) :attr:`.MAX_YEAR` (9999)
67 ``month`` 1 12
68 ``day`` 1 :attr:`Date.days_in_month(year, month) <Date.days_in_month>`
69 ========= ======================== ===================================
70
71 A zero date can also be acquired by passing all zeroes to the :class:`neo4j.time.Date` constructor or by using the :attr:`.ZeroDate` constant.
72
73 .. classmethod:: Date.today()
74
75 :raises OverflowError: if the timestamp is out of the range of values supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038.
76
77 .. classmethod:: Date.utc_today()
78
79 Return the current :class:`.Date` according to UTC.
80
81 .. classmethod:: Date.from_timestamp(timestamp, tz=None)
82
83 :raises OverflowError: if the timestamp is out of the range of values supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038.
84
85 .. classmethod:: Date.utc_from_timestamp(timestamp)
86
87 .. classmethod:: Date.from_ordinal(ordinal)
88
89 Construct and return a :class:`.Date` from a proleptic Gregorian ordinal.
90 This is simply an integer value that corresponds to a day, starting with `1` for 1 Jan 0001.
91
92 .. classmethod:: Date.parse(s)
93
94 .. classmethod:: Date.from_native(date)
95
96 .. classmethod:: Date.from_clock_time(cls, t, epoch):
97
98 .. classmethod:: Date.is_leap_year(year)
99
100 Return a `bool` value that indicates whether or not `year` is a leap year.
101
102 .. classmethod:: Date.days_in_year(year)
103
104 Return the number of days in `year`.
105
106 .. classmethod:: Date.days_in_month(year, month)
107
108 Return the number of days in `month` of `year`.
109
110
111 Class attributes
112 ----------------
113
114 .. attribute:: Date.min
115
116 .. attribute:: Date.max
117
118 .. attribute:: Date.resolution
119
120
121 Instance attributes
122 -------------------
123
124 .. attribute:: d.year
125
126 .. attribute:: d.month
127
128 .. attribute:: d.day
129
130 .. attribute:: d.year_month_day
131
132 .. attribute:: d.year_week_day
133
134 .. attribute:: d.year_day
135
136 Return a 2-tuple of year and day number.
137 This is the number of the day relative to the start of the year, with `1 Jan` corresponding to `1`.
138
139
140 Operations
141 ----------
142
143
144 Instance methods
145 ----------------
146
147 .. method:: d.replace(year=self.year, month=self.month, day=self.day)
148
149 Return a :class:`.Date` with one or more components replaced with new values.
150
151 .. method:: d.time_tuple()
152
153 .. method:: d.to_ordinal()
154
155 .. method:: d.weekday()
156
157 .. method:: d.iso_weekday()
158
159 .. method:: d.iso_calendar()
160
161 .. method:: d.iso_format()
162
163 .. method:: d.__repr__()
164
165 .. method:: d.__str__()
166
167 .. method:: d.__format__()
168
169
170 Special values
171 --------------
172
173 .. attribute:: ZeroDate
174
175 A :class:`neo4j.time.Date` instance set to `0000-00-00`.
176 This has an ordinal value of `0`.
177
178
179 Time
180 ====
181
182 The :class:`neo4j.time.Time` class is a nanosecond-precision drop-in replacement for the standard library :class:`datetime.time` class.
183
184 A high degree of API compatibility with the standard library classes is provided.
185
186 :class:`neo4j.time.Time` objects introduce the concept of `ticks`.
187 This is simply a count of the number of seconds since midnight, in many ways analogous to the :class:`neo4j.time.Date` ordinal.
188 `Ticks` values can be fractional, with a minimum value of `0` and a maximum of `86399.999999999`.
189
190
191 Constructors and other class methods
192 ------------------------------------
193
194 .. class:: neo4j.time.Time(hour, minute, second, tzinfo=None)
195
196 .. classmethod:: Time.now()
197
198 :raises OverflowError: if the timestamp is out of the range of values supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038.
199
200 .. classmethod:: Time.utc_now()
201
202 .. classmethod:: Time.from_ticks(ticks)
203
204 .. classmethod:: Time.from_native(time)
205
206 .. classmethod:: Time.from_clock_time(t, epoch)
207
208
209 Class attributes
210 ----------------
211
212 .. attribute:: Time.min
213
214 .. attribute:: Time.max
215
216 .. attribute:: Time.resolution
217
218
219 Instance attributes
220 -------------------
221
222 .. attribute:: t.ticks
223
224 .. attribute:: t.hour
225
226 .. attribute:: t.minute
227
228 .. attribute:: t.second
229
230 .. attribute:: t.hour_minute_second
231
232 .. attribute:: t.tzinfo
233
234
235 Operations
236 ----------
237
238 .. describe:: hash(t)
239
240 .. describe:: t1 == t2
241
242 .. describe:: t1 != t2
243
244 .. describe:: t1 < t2
245
246 .. describe:: t1 > t2
247
248 .. describe:: t1 <= t2
249
250 .. describe:: t1 >= t2
251
252 .. describe:: t1 + timedelta -> t2
253 t1 + duration -> t2
254
255 .. describe:: t1 - timedelta -> t2
256 t1 - duration -> t2
257
258 .. describe:: t1 - t2 -> timedelta
259
260
261 Instance methods
262 ----------------
263
264 .. method:: t.replace(hour=self.hour, minute=self.minute, second=self.second, tzinfo=self.tzinfo)
265
266 Return a :class:`.Time` with one or more components replaced with new values.
267
268 .. method:: t.utc_offset()
269
270 .. method:: t.dst()
271
272 .. method:: t.tzname()
273
274 .. method:: t.iso_format()
275
276 .. method:: t.__repr__()
277
278 .. method:: t.__str__()
279
280 .. method:: t.__format__()
281
282
283 Special values
284 --------------
285
286 .. attribute:: Midnight
287
288 A :class:`.Time` instance set to `00:00:00`.
289 This has a :attr:`ticks <.time.ticks>` value of `0`.
290
291 .. attribute:: Midday
292
293 A :class:`.Time` instance set to `12:00:00`.
294 This has a :attr:`ticks <.time.ticks>` value of `43200`.
295
296
297 LocalTime
298 ---------
299
300 When tzinfo is set to ``None``
301
302
303
304 DateTime
305 ========
306
307 The :class:`neo4j.time.DateTime` class is a nanosecond-precision drop-in replacement for the standard library :class:`datetime.datetime` class.
308
309 As such, it contains both :class:`neo4j.time.Date` and :class:`neo4j.time.Time` information and draws functionality from those individual classes.
310
311 A :class:`.DateTime` object is fully compatible with the Python time zone library `pytz <http://pytz.sourceforge.net/>`_.
312 Functions such as `normalize` and `localize` can be used in the same way as they are with the standard library classes.
313
314
315 Constructors and other class methods
316 ------------------------------------
317
318 .. autoclass:: neo4j.time.DateTime(year, month, day, hour=0, minute=0, second=0.0, tzinfo=None)
319
320 .. classmethod:: DateTime.now()
321
322 :raises OverflowError: if the timestamp is out of the range of values supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038.
323
324 .. classmethod:: DateTime.utc_now()
325
326 .. classmethod:: DateTime.from_timestamp(timestamp, tz=None)
327
328 :raises OverflowError: if the timestamp is out of the range of values supported by the platform C localtime() function. It’s common for this to be restricted to years from 1970 through 2038.
329
330 .. classmethod:: DateTime.utc_from_timestamp(timestamp)
331
332 .. classmethod:: DateTime.from_ordinal(ordinal)
333
334 .. classmethod:: DateTime.combine(date, time)
335
336 ..
337 NotImplementedError
338 .. classmethod:: DateTime.parse(timestamp, tz=None)
339
340 .. classmethod:: DateTime.from_native(datetime)
341
342 .. classmethod:: DateTime.from_clock_time(t, epoch)
343
344
345 Class attributes
346 ----------------
347
348 .. attribute:: DateTime.min
349
350 .. attribute:: DateTime.max
351
352 .. attribute:: DateTime.resolution
353
354
355 Instance attributes
356 -------------------
357
358 .. attribute:: dt.year
359
360 .. attribute:: dt.month
361
362 .. attribute:: dt.day
363
364 .. attribute:: dt.year_month_day
365
366 .. attribute:: dt.year_week_day
367
368 .. attribute:: dt.year_day
369
370 .. attribute:: dt.hour
371
372 .. attribute:: dt.minute
373
374 .. attribute:: dt.second
375
376 .. attribute:: dt.tzinfo
377
378 .. attribute:: dt.hour_minute_second
379
380
381 Operations
382 ----------
383
384 .. describe:: hash(dt)
385
386 .. describe:: dt1 == dt2
387
388 .. describe:: dt1 != dt2
389
390 .. describe:: dt1 < dt2
391
392 .. describe:: dt1 > dt2
393
394 .. describe:: dt1 <= dt2
395
396 .. describe:: dt1 >= dt2
397
398 .. describe:: dt1 + timedelta -> dt2
399 dt1 + duration -> dt2
400
401 .. describe:: dt1 - timedelta -> dt2
402 dt1 - duration -> dt2
403
404 .. describe:: dt1 - dt2 -> timedelta
405
406
407 Instance methods
408 ----------------
409
410 .. method:: dt.date()
411
412 .. method:: dt.time()
413
414 .. method:: dt.timetz()
415
416 .. method:: dt.replace(year=self.year, month=self.month, day=self.day, hour=self.hour, minute=self.minute, second=self.second, tzinfo=self.tzinfo)
417
418 Return a :class:`.DateTime` with one or more components replaced with new values.
419
420 .. method:: dt.as_timezone()
421
422 .. method:: dt.utc_offset()
423
424 .. method:: dt.dst()
425
426 .. method:: dt.tzname()
427
428 .. method:: dt.time_tuple()
429
430 .. method:: dt.utc_time_tuple()
431
432 .. method:: dt.to_ordinal()
433
434 .. method:: dt.weekday()
435
436 .. method:: dt.iso_weekday()
437
438 .. method:: dt.iso_calendar()
439
440 .. method:: dt.iso_format()
441
442 .. method:: dt.__repr__()
443
444 .. method:: dt.__str__()
445
446 .. method:: dt.__format__()
447
448
449 Special values
450 --------------
451
452 .. attribute:: Never
453
454 A :class:`.DateTime` instance set to `0000-00-00T00:00:00`.
455 This has a :class:`.Date` component equal to :attr:`.ZeroDate` and a :class:`.Time` component equal to :attr:`.Midnight`.
456
457 .. attribute:: UnixEpoch
458
459 A :class:`.DateTime` instance set to `1970-01-01T00:00:00`.
460
461
462 LocalDateTime
463 -------------
464
465 When tzinfo is set to ``None``
466
467
468
469 Duration
470 ========
471
472 A :class:`neo4j.time.Duration` represents the difference between two points in time.
473 Duration objects store a composite value of `months`, `days` and `seconds`.
474 Unlike :class:`datetime.timedelta` however, days and seconds are never interchanged
475 and are applied separately in calculations.
476
477 .. class:: neo4j.time.Duration(years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, subseconds=0, milliseconds=0, microseconds=0, nanoseconds=0)
478
479 All arguments are optional and default to zero.
480
481 .. attribute:: Duration.min
482
483 The lowest duration value possible.
484
485 .. attribute:: Duration.max
486
487 The highest duration value possible.
488
489
490 Instance methods and attributes
491 -------------------------------
492
493 A :class:`neo4j.time.Duration` stores four primary instance attributes internally: ``months``, ``days``, ``seconds`` and ``subseconds``.
494 These are maintained as individual values and are immutable.
495 Each of these four attributes can carry its own sign, with the exception of ``subseconds``, which must have the same sign as ``seconds``.
496 This structure allows the modelling of durations such as `3 months minus 2 days`.
497
498 Two additional secondary attributes are available, each returning a 3-tuple of derived values.
499 These are ``years_months_days`` and ``hours_minutes_seconds``.
500
501 The primary instance attributes and their permitted ranges are listed below.
502
503 ============== ========================================================
504 Attribute Value
505 -------------- --------------------------------------------------------
506 ``months`` Between -(2\ :sup:`63`) and (2\ :sup:`63` - 1) inclusive
507 ``days`` Between -(2\ :sup:`63`) and (2\ :sup:`63` - 1) inclusive
508 ``seconds`` Between -(2\ :sup:`63`) and (2\ :sup:`63` - 1) inclusive
509 ``subseconds`` Between -0.999,999,999 and +0.999,999,999 inclusive
510 ============== ========================================================
511
512
513 Operations
514 ----------
515
516 :class:`neo4j.time.Duration` objects support a number of operations. These are listed below.
517
518 ======================== ====================================================================================================================================================================================
519 Operation Result
520 ------------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
521 ``d1 + d2`` A ``Duration`` representing the sum of ``d1`` and ``d2`` .
522 ``d1 - d2`` A ``Duration`` representing the difference between ``d1`` and ``d2`` .
523 ``d1 * i`` A ``Duration`` representing ``d1`` times ``i``, where ``i`` is an ``int``.
524 ``d1 * f`` A ``Duration`` representing ``d1`` times ``f``, where ``f`` is a ``float``.
525 ``d1 / i`` A ``Duration`` representing ``d1`` divided by ``i``, where ``i`` is an ``int``. Month and day attributes are rounded to the nearest integer, using round-half-to-even.
526 ``d1 / f`` A ``Duration`` representing ``d1`` divided by ``f``, where ``f`` is a ``float``. Month and day attributes are rounded to the nearest integer, using round-half-to-even.
527 ``d1 // i`` A ``Duration`` representing the floor after ``d1`` is divided by ``i``, where ``i`` is an ``int``.
528 ``d1 % i`` A ``Duration`` representing the remainder after ``d1`` is divided by ``i``, where ``i`` is an ``int``.
529 ``divmod(d1, i)`` A pair of ``Duration`` objects representing the floor and remainder after ``d1`` is divided by ``i``, where ``i`` is an ``int``.
530 ``+d1`` A ``Duration`` identical to ``d1`` .
531 ``-d1`` A ``Duration`` that is the inverse of ``d1``. Equivalent to ``Duration(months=-d1.months, days=-d1.days, seconds=-d1.seconds, subseconds=-d1.subseconds)``.
532 ``abs(d1)`` A ``Duration`` equal to the absolute value of ``d1``. Equivalent to ``Duration(months=abs(d1.months), days=abs(d1.days), seconds=abs(d1.seconds), subseconds=abs(d1.subseconds))``.
533 ``str(d1)``
534 ``repr(d1)``
535 ``bool(d1)`` :const:`True` if any attribute is non-zero, :const:`False` otherwise.
536 ``tuple(d1)`` A 4-tuple of ``(months: int, days: int, seconds: int, subseconds: float)``.
537 ======================== ====================================================================================================================================================================================
538
1313 Sessions provide the top-level of containment for database activity.
1414 Session creation is a lightweight operation and sessions are `not` thread safe.
1515
16 Connections are drawn from the :class:`.Driver` connection pool as required; an idle session will not hold onto a connection.
16 Connections are drawn from the :class:`neo4j.Driver` connection pool as required; an idle session will not hold onto a connection.
1717
1818 Sessions will often be created and destroyed using a `with` block context.
1919 For example::
2424
2525 To construct a :class:`.Session` use the :meth:`.Driver.session` method.
2626
27 .. class:: neo4j.Session
27 .. class:: .Session
2828
2929 .. automethod:: close
3030
6565
6666 def create_person(driver, name):
6767 with driver.session() as session:
68 return session.run("CREATE (a:Person {name:$name}) "
68 return session.run("CREATE (a:Person { name: $name }) "
6969 "RETURN id(a)", name=name).single().value()
7070
7171 Explicit Transactions
7474 This creates a new :class:`.Transaction` object that can be used to run Cypher.
7575 It also gives applications the ability to directly control `commit` and `rollback` activity.
7676
77 .. class:: neo4j.Transaction
77 .. class:: .Transaction
7878
7979 .. automethod:: run
8080
8181 .. automethod:: sync
82
83 .. attribute:: success
84
85 This attribute can be used to determine the outcome of a transaction on closure.
86 Specifically, this will be either a COMMIT or a ROLLBACK.
87 A value can be set for this attribute multiple times in user code before a transaction completes, with only the final value taking effect.
88
89 On closure, the outcome is evaluated according to the following rules:
90
91 ================ ==================== =========================== ============== =============== =================
92 :attr:`.success` ``__exit__`` cleanly ``__exit__`` with exception ``tx.close()`` ``tx.commit()`` ``tx.rollback()``
93 ================ ==================== =========================== ============== =============== =================
94 :const:`None` COMMIT ROLLBACK ROLLBACK COMMIT ROLLBACK
95 :const:`True` COMMIT COMMIT [1]_ COMMIT COMMIT ROLLBACK
96 :const:`False` ROLLBACK ROLLBACK ROLLBACK COMMIT ROLLBACK
97 ================ ==================== =========================== ============== =============== =================
98
99 .. [1] While a COMMIT will be attempted in this scenario, it will likely fail if the exception originated from Cypher execution within that transaction.
100
101 .. automethod:: close
10282
10383 .. automethod:: closed
10484
10686
10787 .. automethod:: rollback
10888
109 Closing an explicit transaction can either happen automatically at the end of a ``with`` block, using the :attr:`.Transaction.success` attribute to determine success,
89 Closing an explicit transaction can either happen automatically at the end of a ``with`` block,
11090 or can be explicitly controlled through the :meth:`.Transaction.commit` and :meth:`.Transaction.rollback` methods.
11191 Explicit transactions are most useful for applications that need to distribute Cypher execution across multiple functions for the same transaction.
11292
138118 .. code-block:: python
139119
140120 def create_person(tx, name):
141 return tx.run("CREATE (a:Person {name:$name}) "
121 return tx.run("CREATE (a:Person { name: $name }) "
142122 "RETURN id(a)", name=name).single().value()
143123
144124 with driver.session() as session:
146126
147127 To exert more control over how a transaction function is carried out, the :func:`.unit_of_work` decorator can be used.
148128
149 .. autofunction:: neo4j.unit_of_work
129 .. autofunction:: neo4j.work.simple.unit_of_work
150130
151131
152132 Access modes
159139 This means that transaction functions within a session can override the access mode passed to that session on construction.
160140
161141 .. note::
162 The driver does not parse Cypher statements and cannot determine whether a statement tagged as `read` or `write` is tagged correctly.
163 Since the access mode is not passed to the server, this can allow a `write` statement to be executed in a `read` call on a single instance.
142 The driver does not parse Cypher queries and cannot determine whether the access mode should be :code:`ACCESS_READ` or :code:`ACCESS_WRITE`.
143 Since the access mode is not passed to the server, this can allow a :code:`ACCESS_WRITE` statement to be executed for a :code:`ACCESS_READ` call on a single instance.
164144 Clustered environments are not susceptible to this loophole as cluster roles prevent it.
165145 This behaviour should not be relied upon as the loophole may be closed in a future release.
1717 Path :class:`.Path`
1818 ============= ======================
1919
20 .. class:: neo4j.types.graph.Graph
20 .. class:: neo4j.graph.Graph
2121
2222 A local, self-contained graph object that acts as a container for :class:`.Node` and :class:`.Relationship` instances.
23 This is typically obtained via the :meth:`.BoltStatementResult.graph` method.
23 This is typically obtained via the :meth:`.Result.graph` method.
2424
2525 .. autoattribute:: nodes
2626
2929 .. automethod:: relationship_type
3030
3131
32 .. class:: neo4j.types.graph.Node
32 .. class:: neo4j.graph.Node
3333
3434 .. describe:: node == other
3535
7575 .. automethod:: items
7676
7777
78 .. class:: neo4j.types.graph.Relationship
78 .. class:: neo4j.graph.Relationship
7979
8080 .. describe:: relationship == other
8181
132132 .. automethod:: items
133133
134134
135 .. class:: neo4j.types.graph.Path
135 .. class:: neo4j.graph.Path
136136
137137 .. describe:: path == other
138138
77 Point :class:`.Point`
88 ============= ======================
99
10 .. autoclass:: neo4j.types.spatial.Point
10 .. autoclass:: neo4j.spatial.Point
1111 :members:
1212
13 .. autoclass:: neo4j.types.spatial.CartesianPoint
13 .. autoclass:: neo4j.spatial.CartesianPoint
1414 :members:
1515 :inherited-members:
1616
17 .. autoclass:: neo4j.types.spatial.WGS84Point
17 .. autoclass:: neo4j.spatial.WGS84Point
1818 :members:
1919 :inherited-members:
0 .. py:currentmodule:: neotime
1
20 ===================
31 Temporal Data Types
42 ===================
53
6 Temporal data types are implemented by the `neotime <http://neotime.readthedocs.io/en/latest/>`_ package.
4 Temporal data types are implemented by the ``neo4j.time``
5
76 These provide a set of types compliant with ISO-8601 and Cypher, which are similar to those found in the built-in ``datetime`` module.
87 Sub-second values are measured to nanosecond precision and the types are compatible with `pytz <http://pytz.sourceforge.net/>`_.
98
109 The table below shows the general mappings between Cypher and the temporal types provided by the driver.
1110 In addition, the built-in temporal types can be passed as parameters and will be mapped appropriately.
1211
13 ============= ========================= ================================== ============
14 Cypher Python driver type Python built-in type ``tzinfo``
15 ============= ========================= ================================== ============
16 Date :class:`neotime:Date` :class:`python:datetime.date`
17 Time :class:`neotime:Time` :class:`python:datetime.time` ``not None``
18 LocalTime :class:`neotime:Time` :class:`python:datetime.time` ``None``
19 DateTime :class:`neotime:DateTime` :class:`python:datetime.datetime` ``not None``
20 LocalDateTime :class:`neotime:DateTime` :class:`python:datetime.datetime` ``None``
21 Duration :class:`neotime:Duration` :class:`python:datetime.timedelta`
22 ============= ========================= ================================== ============
12 ============= ============================ ================================== ============
13 Cypher Python driver type Python built-in type ``tzinfo``
14 ============= ============================ ================================== ============
15 Date :class:`neo4j.time:Date` :class:`python:datetime.date`
16 Time :class:`neo4j.time:Time` :class:`python:datetime.time` ``not None``
17 LocalTime :class:`neo4j.time:Time` :class:`python:datetime.time` ``None``
18 DateTime :class:`neo4j.time:DateTime` :class:`python:datetime.datetime` ``not None``
19 LocalDateTime :class:`neo4j.time:DateTime` :class:`python:datetime.datetime` ``None``
20 Duration :class:`neo4j.time:Duration` :class:`python:datetime.timedelta`
21 ============= ============================ ================================== ============
0 ##############
1 Usage Patterns
2 ##############
3
4 .. warning::
5 This section is experimental! Breaking changes will occur!
6
7
8 ******************
9 Simple Application
10 ******************
11
12 .. code-block:: python
13
14 from neo4j import GraphDatabase
15
16
17 def print_friends_of(tx, name):
18 query = "MATCH (a:Person)-[:KNOWS]->(f) WHERE a.name = $name RETURN f.name"
19
20 result = tx.run(query, name=name)
21
22 for record in result:
23 print(record["f.name"])
24
25
26 if __name__ == "main":
27
28 uri = "bolt://localhost:7687"
29 driver = GraphDatabase.driver(uri, auth=("neo4j", "password"))
30
31 with driver.session() as session:
32 session.read_transaction(print_friends_of, "Alice")
33
34 driver.close()
35
36
37 **********************************
38 Driver Initialization Work Pattern
39 **********************************
40
41 .. code-block:: python
42
43 from neo4j import GraphDatabase, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
44 from neo4j.exceptions import ServiceUnavailable
45
46 uri = "bolt://localhost:7687"
47
48 driver_config = {
49 "encrypted": False,
50 "trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
51 "user_agent": "example",
52 "max_connection_lifetime": 1000,
53 "max_connection_pool_size": 100,
54 "keep_alive": False,
55 "max_transaction_retry_time": 10,
56 "resolver": None,
57 }
58
59 try:
60 driver = GraphDatabase.driver(uri, auth=("neo4j", "password"), **driver_config)
61 driver.close()
62 except ServiceUnavailable as e:
63 print(e)
64
65
66 Driver Initialization With Block Work Pattern
67 =============================================
68
69 .. Investigate the example 6 pattern for error handling
70 https://www.python.org/dev/peps/pep-0343/#examples
71
72
73 .. code-block:: python
74
75 from neo4j import GraphDatabase
76
77 with GraphDatabase.driver(uri, auth=("neo4j", "password"), **driver_config) as driver:
78 try:
79 session = driver.session()
80 session.close()
81 except ServiceUnavailable as e:
82 print(e)
83
84 ***********************************
85 Session Initialization Work Pattern
86 ***********************************
87
88 .. code-block:: python
89
90 from neo4j import (
91 ACCESS_READ,
92 ACCESS_WRITE,
93 )
94
95 session_config = {
96 "fetch_size": 100,
97 "database": "default",
98 "bookmarks": ["bookmark-1",],
99 "access_mode": ACCESS_WRITE,
100 "connection_acquisition_timeout": 60.0,
101 "max_transaction_retry_time": 30.0,
102 "initial_retry_delay": 1.0,
103 "retry_delay_multiplier": 2.0,
104 "retry_delay_jitter_factor": 0.2,
105 }
106
107 try:
108 session = driver.session(access_mode=None, **session_config)
109 session.close()
110 except ServiceUnavailable as e:
111 print(e)
112
113
114 Session Initialization With Block Work Pattern
115 ==============================================
116
117 .. Investigate the example 6 pattern for error handling
118 https://www.python.org/dev/peps/pep-0343/#examples
119
120
121 .. code-block:: python
122
123 from neo4j.exceptions import ServiceUnavailable
124
125 query = "RETURN 1 AS x"
126
127 with driver.session(access_mode=None, **session_config) as session:
128 try:
129 result = session.run(query)
130 for record in result:
131 print(record["x"])
132 except ServiceUnavailable as e:
133 print(e)
134
135
136 *******************************
137 Session Autocommit Work Pattern
138 *******************************
139
140 .. code-block:: python
141
142 statement = "RETURN $tag AS $name"
143
144 kwparameters = {"name": "test", "tag": 123}
145
146 session.run(query)
147
148 session.run(query, parameters=None, **kwparameters)
149
150 session.run(query, parameters={"name": "test", "tag": 123})
151
152 session.run(query, parameters={"name": "test", "tag": 123}, **kwparameters)
153
154 session.run(query, name="test", "tag"=123)
155
156
157 ****************************************
158 Session Managed Transaction Work Pattern
159 ****************************************
160
161 .. code-block:: python
162
163 def test_work(tx, *args, **kwargs):
164 query = "RETURN $tag AS $name"
165
166 kwparameters = {"name": "test", "tag": 123}
167
168 tx.run(query)
169
170 tx.run(query, parameters=None, **kwparameters)
171
172 tx.run(query, parameters={"name": "test", "tag": 123})
173
174 tx.run(query, parameters={"name": "test", "tag": 123}, **kwparameters)
175
176 tx.run(query, name="test", "tag"=123)
177
178
179 session.read_transaction(test_work)
180
181 session.read_transaction(test_work, *args, **kwargs)
182
183 session.read_transaction(test_work, **kwargs)
184
185
186 session.write_transaction(test_work)
187
188 session.write_transaction(test_work, *args, **kwargs)
189
190 session.write_transaction(test_work, **kwargs)
191
192
193 unit_of_work
194 ============
195
196 .. code-block:: python
197
198 from neo4j import unit_of_work
199
200
201 @unit_of_work(timeout=10)
202 def test_work(tx, *args, **kwargs):
203 query = "RETURN $tag AS $name"
204
205 result = tx.run(query)
206 # The result needs to be consumed
207
208 session.read_transaction(test_work)
209
210
211 .. code-block:: python
212
213 from neo4j import unit_of_work
214
215
216 @unit_of_work(metadata={"hello": 123})
217 def test_work(tx, *args, **kwargs):
218 query = "RETURN $tag AS $name"
219
220 result = tx.run(query)
221 # The result needs to be consumed
222
223 session.read_transaction(test_work)
224
225
226 .. code-block:: python
227
228 from neo4j import unit_of_work
229
230
231 @unit_of_work(timeout=10, metadata={"hello": 123})
232 def test_work(tx, *args, **kwargs):
233 query = "RETURN $tag AS $name"
234
235 result = tx.run(query)
236 # The result needs to be consumed
237
238 session.read_transaction(test_work)
239
240
241 The Query Object Work Pattern
242 =============================
243
244 .. code-block:: python
245
246 from neo4j import Query
247
248
249 def test_work(tx, *args, **kwargs):
250 query = Query("RETURN 1 AS x, timeout=10, metadata={"hello": 123})
251
252 result = tx.run(query)
253 # The result needs to be consumed
254
255 session.read_transaction(test_work)
256
257
258
259
260 *******************************
261 Transaction Object Work Pattern
262 *******************************
263
264 .. code-block:: python
265
266 query = Query("RETURN 1 AS x, timeout=10, metadata={"hello": 123})
267
268 tx = session.begin_transaction(metadata=None, timeout=None)
269 tx.run(query)
270 tx.commit()
00 #!/usr/bin/env python
11 # -*- encoding: utf-8 -*-
22
3 # Copyright (c) 2002-2019 "Neo4j,"
3 # Copyright (c) 2002-2020 "Neo4j,"
44 # Neo4j Sweden AB [http://neo4j.com]
55 #
66 # This file is part of Neo4j.
2020
2121 __all__ = [
2222 "__version__",
23 "GraphDatabase",
24 "Driver",
25 "BoltDriver",
26 "Neo4jDriver",
27 "Auth",
28 "AuthToken",
29 "basic_auth",
30 "kerberos_auth",
31 "custom_auth",
32 "Bookmark",
33 "ServerInfo",
34 "Version",
2335 "READ_ACCESS",
2436 "WRITE_ACCESS",
37 "DEFAULT_DATABASE",
2538 "TRUST_ALL_CERTIFICATES",
26 "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES",
2739 "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES",
28 "GraphDatabase",
29 "Driver",
30 "DirectDriver",
31 "RoutingDriver",
40 "Address",
41 "IPv4Address",
42 "IPv6Address",
43 "Config",
44 "PoolConfig",
45 "WorkspaceConfig",
46 "SessionConfig",
47 "Record",
48 "Transaction",
49 "Result",
50 "ResultSummary",
51 "SummaryCounters",
52 "Query",
3253 "Session",
33 "Transaction",
34 "Statement",
35 "StatementResult",
36 "BoltStatementResult",
37 "BoltStatementResultSummary",
38 "Record",
39 "DriverError",
40 "SessionError",
41 "SessionExpired",
42 "TransactionError",
4354 "unit_of_work",
44 "basic_auth",
45 "custom_auth",
46 "kerberos_auth",
55 "ExperimentalWarning",
4756 ]
4857
49 BOLT_VERSION_1 = 1
50 BOLT_VERSION_2 = 2
51 BOLT_VERSION_3 = 3
52
53 try:
54 from neobolt.exceptions import (
55 ConnectionExpired,
56 CypherError,
57 IncompleteCommitError,
58 ServiceUnavailable,
59 TransientError,
60 )
61 except ImportError:
62 # We allow this to fail because this module can be imported implicitly
63 # during setup. At that point, dependencies aren't available.
64 pass
65 else:
66 __all__.extend([
67 "ConnectionExpired",
68 "CypherError",
69 "IncompleteCommitError",
70 "ServiceUnavailable",
71 "TransientError",
72 ])
73
74
75 from collections import deque, namedtuple
76 from functools import reduce
7758 from logging import getLogger
78 from operator import xor as xor_operator
79 from random import random
80 from time import sleep
81 from warnings import warn
82
83
84 from .compat import perf_counter, urlparse, xstr, Sequence, Mapping
85 from .config import *
86 from .meta import version as __version__
87
88
89 READ_ACCESS = "READ"
90 WRITE_ACCESS = "WRITE"
91
92 INITIAL_RETRY_DELAY = 1.0
93 RETRY_DELAY_MULTIPLIER = 2.0
94 RETRY_DELAY_JITTER_FACTOR = 0.2
95
96 STATEMENT_TYPE_READ_ONLY = "r"
97 STATEMENT_TYPE_READ_WRITE = "rw"
98 STATEMENT_TYPE_WRITE_ONLY = "w"
99 STATEMENT_TYPE_SCHEMA_WRITE = "s"
59
60
61 from neo4j.addressing import (
62 Address,
63 IPv4Address,
64 IPv6Address,
65 )
66 from neo4j.api import (
67 Auth, # TODO: Validate naming for Auth compared to other drivers.
68 AuthToken,
69 basic_auth,
70 kerberos_auth,
71 custom_auth,
72 Bookmark,
73 ServerInfo,
74 Version,
75 READ_ACCESS,
76 WRITE_ACCESS,
77 SYSTEM_DATABASE,
78 DEFAULT_DATABASE,
79 TRUST_ALL_CERTIFICATES,
80 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
81 )
82 from neo4j.conf import (
83 Config,
84 PoolConfig,
85 WorkspaceConfig,
86 SessionConfig,
87 )
88 from neo4j.meta import (
89 experimental,
90 ExperimentalWarning,
91 get_user_agent,
92 version as __version__,
93 )
94 from neo4j.data import (
95 Record,
96 )
97 from neo4j.work.simple import (
98 Query,
99 Session,
100 unit_of_work,
101 )
102 from neo4j.work.transaction import (
103 Transaction,
104 )
105 from neo4j.work.result import (
106 Result,
107 )
108 from neo4j.work.summary import (
109 ResultSummary,
110 SummaryCounters,
111 )
100112
101113
102114 log = getLogger("neo4j")
103115
104116
105 # TODO: remove in 2.0
106 _warned_about_transaction_bookmarks = False
107
108
109 class GraphDatabase(object):
110 """ Accessor for :class:`.Driver` construction.
117 class GraphDatabase:
118 """Accessor for :class:`neo4j.Driver` construction.
111119 """
112120
113121 @classmethod
114 def driver(cls, uri, **config):
115 """ Create a :class:`.Driver` object. Calling this method provides
116 identical functionality to constructing a :class:`.Driver` or
117 :class:`.Driver` subclass instance directly.
118 """
119 return Driver(uri, **config)
120
121
122 class Driver(object):
123 """ Base class for all types of :class:`.Driver`, instances of which are
122 def driver(cls, uri, *, auth=None, **config):
123 """Create a driver.
124
125 :param uri: the connection URI for the driver, see :ref:`uri-ref` for available URIs.
126 :param auth: the authentication details, see :ref:`auth-ref` for available authentication details.
127 :param config: driver configuration key-word arguments, see :ref:`driver-configuration-ref` for available key-word arguments.
128
129 :return: :ref:`neo4j-driver-ref` or :ref:`bolt-driver-ref`
130 """
131
132 from neo4j.api import (
133 parse_neo4j_uri,
134 parse_routing_context,
135 DRIVER_BOLT,
136 DRIVER_NEO4j,
137 SECURITY_TYPE_NOT_SECURE,
138 SECURITY_TYPE_SELF_SIGNED_CERTIFICATE,
139 SECURITY_TYPE_SECURE,
140 URI_SCHEME_BOLT,
141 URI_SCHEME_NEO4J,
142 URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
143 URI_SCHEME_BOLT_SECURE,
144 URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
145 URI_SCHEME_NEO4J_SECURE,
146 )
147
148 driver_type, security_type, parsed = parse_neo4j_uri(uri)
149
150 if "trust" in config.keys():
151 if config.get("trust") not in [TRUST_ALL_CERTIFICATES, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES]:
152 from neo4j.exceptions import ConfigurationError
153 raise ConfigurationError("The config setting `trust` values are {!r}".format(
154 [
155 TRUST_ALL_CERTIFICATES,
156 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
157 ]
158 ))
159
160 if security_type in [SECURITY_TYPE_SELF_SIGNED_CERTIFICATE, SECURITY_TYPE_SECURE] and ("encrypted" in config.keys() or "trust" in config.keys()):
161 from neo4j.exceptions import ConfigurationError
162 raise ConfigurationError("The config settings 'encrypted' and 'trust' can only be used with the URI schemes {!r}. Use the other URI schemes {!r} for setting encryption settings.".format(
163 [
164 URI_SCHEME_BOLT,
165 URI_SCHEME_NEO4J,
166 ],
167 [
168 URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
169 URI_SCHEME_BOLT_SECURE,
170 URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
171 URI_SCHEME_NEO4J_SECURE,
172 ]
173 ))
174
175 if security_type == SECURITY_TYPE_SECURE:
176 config["encrypted"] = True
177 elif security_type == SECURITY_TYPE_SELF_SIGNED_CERTIFICATE:
178 config["encrypted"] = True
179 config["trust"] = TRUST_ALL_CERTIFICATES
180
181 if driver_type == DRIVER_BOLT:
182 return cls.bolt_driver(parsed.netloc, auth=auth, **config)
183 elif driver_type == DRIVER_NEO4j:
184 routing_context = parse_routing_context(parsed.query)
185 return cls.neo4j_driver(parsed.netloc, auth=auth, routing_context=routing_context, **config)
186
187 @classmethod
188 def bolt_driver(cls, target, *, auth=None, **config):
189 """ Create a driver for direct Bolt server access that uses
190 socket I/O and thread-based concurrency.
191 """
192 from neo4j._exceptions import BoltHandshakeError, BoltSecurityError
193
194 try:
195 return BoltDriver.open(target, auth=auth, **config)
196 except (BoltHandshakeError, BoltSecurityError) as error:
197 from neo4j.exceptions import ServiceUnavailable
198 raise ServiceUnavailable(str(error)) from error
199
200 @classmethod
201 def neo4j_driver(cls, *targets, auth=None, routing_context=None, **config):
202 """ Create a driver for routing-capable Neo4j service access
203 that uses socket I/O and thread-based concurrency.
204 """
205 from neo4j._exceptions import BoltHandshakeError, BoltSecurityError
206
207 try:
208 return Neo4jDriver.open(*targets, auth=auth, routing_context=routing_context, **config)
209 except (BoltHandshakeError, BoltSecurityError) as error:
210 from neo4j.exceptions import ServiceUnavailable
211 raise ServiceUnavailable(str(error)) from error
212
213
214 class Direct:
215
216 default_host = "localhost"
217 default_port = 7687
218
219 default_target = ":"
220
221 def __init__(self, address):
222 self._address = address
223
224 @property
225 def address(self):
226 return self._address
227
228 @classmethod
229 def parse_target(cls, target):
230 """ Parse a target string to produce an address.
231 """
232 if not target:
233 target = cls.default_target
234 address = Address.parse(target, default_host=cls.default_host,
235 default_port=cls.default_port)
236 return address
237
238
239 class Routing:
240
241 default_host = "localhost"
242 default_port = 7687
243
244 default_targets = ": :17601 :17687"
245
246 def __init__(self, initial_addresses):
247 self._initial_addresses = initial_addresses
248
249 @property
250 def initial_addresses(self):
251 return self._initial_addresses
252
253 @classmethod
254 def parse_targets(cls, *targets):
255 """ Parse a sequence of target strings to produce an address
256 list.
257 """
258 targets = " ".join(targets)
259 if not targets:
260 targets = cls.default_targets
261 addresses = Address.parse_list(targets, default_host=cls.default_host, default_port=cls.default_port)
262 return addresses
263
264
265 class Driver:
266 """ Base class for all types of :class:`neo4j.Driver`, instances of which are
124267 used as the primary access point to Neo4j.
125
126 :param uri: URI for a graph database service
127 :param config: configuration and authentication details (valid keys are listed below)
128268 """
129
130 #: Overridden by subclasses to specify the URI scheme owned by that
131 #: class.
132 uri_schemes = ()
133269
134270 #: Connection pool
135271 _pool = None
136272
137 #: Indicator of driver closure.
138 _closed = False
139
140 @classmethod
141 def _check_uri(cls, uri):
142 """ Check whether a URI is compatible with a :class:`.Driver`
143 subclass. When called from a subclass, execution simply passes
144 through if the URI scheme is valid for that class. If invalid,
145 a `ValueError` is raised.
146
147 :param uri: URI to check for compatibility
148 :raise: `ValueError` if URI scheme is incompatible
149 """
150 parsed = urlparse(uri)
151 if parsed.scheme not in cls.uri_schemes:
152 raise ValueError("%s objects require the one of the URI "
153 "schemes %r" % (cls.__name__, cls.uri_schemes))
154
155 def __new__(cls, uri, **config):
156 parsed = urlparse(uri)
157 parsed_scheme = parsed.scheme
158 for subclass in Driver.__subclasses__():
159 if parsed_scheme in subclass.uri_schemes:
160 return subclass(uri, **config)
161 raise ValueError("URI scheme %r not supported" % parsed.scheme)
273 def __init__(self, pool):
274 assert pool is not None
275 self._pool = pool
162276
163277 def __del__(self):
164278 self.close()
169283 def __exit__(self, exc_type, exc_value, traceback):
170284 self.close()
171285
172 def session(self, access_mode=None, **parameters):
173 """ Create a new :class:`.Session` object based on this
174 :class:`.Driver`.
175
176 :param access_mode: default access mode (read or write) for
177 transactions in this session
178 :param parameters: custom session parameters (see
179 :class:`.Session` for details)
180 :returns: new :class:`.Session` object
181 """
182 if self.closed():
183 raise DriverError("Driver closed")
286 @property
287 def encrypted(self):
288 return bool(self._pool.pool_config.encrypted)
289
290 def session(self, **config):
291 """Create a session, see :ref:`session-construction-ref`
292
293 :param config: session configuration key-word arguments, see :ref:`session-configuration-ref` for available key-word arguments.
294
295 :returns: new :class:`neo4j.Session` object
296 """
297 raise NotImplementedError
298
299 @experimental("The pipeline API is experimental and may be removed or changed in a future release")
300 def pipeline(self, **config):
301 """ Create a pipeline.
302 """
303 raise NotImplementedError
184304
185305 def close(self):
186306 """ Shut down, closing any open connections in the pool.
187307 """
188 if not self._closed:
189 self._closed = True
190 if self._pool is not None:
191 self._pool.close()
192 self._pool = None
193
194 def closed(self):
195 """ Return :const:`True` if closed, :const:`False` otherwise.
196 """
197 return self._closed
198
199
200 class DirectDriver(Driver):
201 """ A :class:`.DirectDriver` is created from a ``bolt`` URI and addresses
308 self._pool.close()
309
310 @experimental("The configuration may change in the future.")
311 def verify_connectivity(self, **config):
312 """ This verifies if the driver can connect to a remote server or a cluster
313 by establishing a network connection with the remote and possibly exchanging
314 a few data before closing the connection. It throws exception if fails to connect.
315
316 Use the exception to further understand the cause of the connectivity problem.
317
318 Note: Even if this method throws an exception, the driver still need to be closed via close() to free up all resources.
319 """
320 raise NotImplementedError
321
322 @experimental("Feature support query, based on Bolt Protocol Version and Neo4j Server Version will change in the future.")
323 def supports_multi_db(self):
324 """ Check if the server or cluster supports multi-databases.
325
326 :return: Returns true if the server or cluster the driver connects to supports multi-databases, otherwise false.
327 :rtype: bool
328 """
329 cx = self._pool.acquire(access_mode=READ_ACCESS, timeout=self._pool.workspace_config.connection_acquisition_timeout, database=self._pool.workspace_config.database)
330 support = cx.supports_multiple_databases
331 self._pool.release(cx)
332
333 return support
334
335
336 class BoltDriver(Direct, Driver):
337 """ A :class:`.BoltDriver` is created from a ``bolt`` URI and addresses
202338 a single database machine. This may be a standalone server or could be a
203339 specific member of a cluster.
204340
205 Connections established by a :class:`.DirectDriver` are always made to the
341 Connections established by a :class:`.BoltDriver` are always made to the
206342 exact host and port detailed in the URI.
207343 """
208344
209 uri_schemes = ("bolt",)
210
211 def __new__(cls, uri, **config):
212 from neobolt.addressing import SocketAddress
213 from neobolt.direct import ConnectionPool, DEFAULT_PORT, connect
214 from neobolt.security import ENCRYPTION_OFF, ENCRYPTION_ON, SSL_AVAILABLE, SecurityPlan
215 cls._check_uri(uri)
216 if SocketAddress.parse_routing_context(uri):
217 raise ValueError("Parameters are not supported with scheme 'bolt'. Given URI: '%s'." % uri)
218 instance = object.__new__(cls)
219 # We keep the address containing the host name or IP address exactly
220 # as-is from the original URI. This means that every new connection
221 # will carry out DNS resolution, leading to the possibility that
222 # the connection pool may contain multiple IP address keys, one for
223 # an old address and one for a new address.
224 instance.address = SocketAddress.from_uri(uri, DEFAULT_PORT)
225 if config.get("encrypted") is None:
226 config["encrypted"] = ENCRYPTION_ON if SSL_AVAILABLE else ENCRYPTION_OFF
227 instance.security_plan = security_plan = SecurityPlan.build(**config)
228 instance.encrypted = security_plan.encrypted
229
230 def connector(address, **kwargs):
231 return connect(address, **dict(config, **kwargs))
232
233 pool = ConnectionPool(connector, instance.address, **config)
234 pool.release(pool.acquire())
235 instance._pool = pool
236 instance._max_retry_time = config.get("max_retry_time", default_config["max_retry_time"])
237 return instance
238
239 def session(self, access_mode=None, **parameters):
240 if "max_retry_time" not in parameters:
241 parameters["max_retry_time"] = self._max_retry_time
242 return Session(self._pool.acquire, access_mode, **parameters)
243
244
245 class RoutingDriver(Driver):
246 """ A :class:`.RoutingDriver` is created from a ``neo4j`` URI. The
345 @classmethod
346 def open(cls, target, *, auth=None, **config):
347 """
348 :param target:
349 :param auth:
350 :param config: The values that can be specified are found in :class: `neo4j.PoolConfig` and :class: `neo4j.WorkspaceConfig`
351
352 :return:
353 :rtype: :class: `neo4j.BoltDriver`
354 """
355 from neo4j.io import BoltPool
356 address = cls.parse_target(target)
357 pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)
358 pool = BoltPool.open(address, auth=auth, pool_config=pool_config, workspace_config=default_workspace_config)
359 return cls(pool, default_workspace_config)
360
361 def __init__(self, pool, default_workspace_config):
362 Direct.__init__(self, pool.address)
363 Driver.__init__(self, pool)
364 self._default_workspace_config = default_workspace_config
365
366 def session(self, **config):
367 """
368 :param config: The values that can be specified are found in :class: `neo4j.SessionConfig`
369
370 :return:
371 :rtype: :class: `neo4j.Session`
372 """
373 from neo4j.work.simple import Session
374 session_config = SessionConfig(self._default_workspace_config, config)
375 SessionConfig.consume(config) # Consume the config
376 return Session(self._pool, session_config)
377
378 def pipeline(self, **config):
379 from neo4j.work.pipelining import Pipeline, PipelineConfig
380 pipeline_config = PipelineConfig(self._default_workspace_config, config)
381 PipelineConfig.consume(config) # Consume the config
382 return Pipeline(self._pool, pipeline_config)
383
384 @experimental("The configuration may change in the future.")
385 def verify_connectivity(self, **config):
386 server_agent = None
387 config["fetch_size"] = -1
388 with self.session(**config) as session:
389 result = session.run("RETURN 1 AS x")
390 value = result.single().value()
391 summary = result.consume()
392 server_agent = summary.server.agent
393 return server_agent
394
395
396 class Neo4jDriver(Routing, Driver):
397 """ A :class:`.Neo4jDriver` is created from a ``neo4j`` URI. The
247398 routing behaviour works in tandem with Neo4j's `Causal Clustering
248 <https://neo4j.com/docs/operations-manual/current/clustering/>`_ feature
249 by directing read and write behaviour to appropriate cluster members.
399 <https://neo4j.com/docs/operations-manual/current/clustering/>`_
400 feature by directing read and write behaviour to appropriate
401 cluster members.
250402 """
251403
252 uri_schemes = ("neo4j", "bolt+routing")
253
254 def __new__(cls, uri, **config):
255 from neobolt.addressing import SocketAddress
256 from neobolt.direct import DEFAULT_PORT, connect
257 from neobolt.routing import RoutingConnectionPool
258 from neobolt.security import ENCRYPTION_OFF, ENCRYPTION_ON, SSL_AVAILABLE, SecurityPlan
259 cls._check_uri(uri)
260 instance = object.__new__(cls)
261 instance.initial_address = initial_address = SocketAddress.from_uri(uri, DEFAULT_PORT)
262 if config.get("encrypted") is None:
263 config["encrypted"] = ENCRYPTION_ON if SSL_AVAILABLE else ENCRYPTION_OFF
264 instance.security_plan = security_plan = SecurityPlan.build(**config)
265 instance.encrypted = security_plan.encrypted
266 routing_context = SocketAddress.parse_routing_context(uri)
267 if not security_plan.routing_compatible:
268 # this error message is case-specific as there is only one incompatible
269 # scenario right now
270 raise ValueError("TRUST_ON_FIRST_USE is not compatible with routing")
271
272 def connector(address, **kwargs):
273 return connect(address, **dict(config, **kwargs))
274
275 pool = RoutingConnectionPool(connector, initial_address, routing_context, initial_address, **config)
276 try:
277 pool.update_routing_table()
278 except:
279 pool.close()
280 raise
281 else:
282 instance._pool = pool
283 instance._max_retry_time = config.get("max_retry_time", default_config["max_retry_time"])
284 return instance
285
286 def session(self, access_mode=None, **parameters):
287 if "max_retry_time" not in parameters:
288 parameters["max_retry_time"] = self._max_retry_time
289 return Session(self._pool.acquire, access_mode, **parameters)
290
291
292 class Session(object):
293 """ A :class:`.Session` is a logical context for transactional units
294 of work. Connections are drawn from the :class:`.Driver` connection
295 pool as required.
296
297 Session creation is a lightweight operation and sessions are not thread
298 safe. Therefore a session should generally be short-lived, and not
299 span multiple threads.
300
301 In general, sessions will be created and destroyed within a `with`
302 context. For example::
303
304 with driver.session() as session:
305 result = session.run("MATCH (a:Person) RETURN a.name")
306 # do something with the result...
307
308 :param acquirer: callback function for acquiring new connections
309 with a given access mode
310 :param access_mode: default access mode (read or write) for
311 transactions in this session
312 :param parameters: custom session parameters, including:
313
314 `bookmark`
315 A single bookmark after which this session should begin.
316 (Deprecated, use `bookmarks` instead)
317
318 `bookmarks`
319 A collection of bookmarks after which this session should begin.
320
321 `max_retry_time`
322 The maximum time after which to stop attempting retries of failed
323 transactions.
324
325 """
326
327 # The current connection.
328 _connection = None
329
330 # The current :class:`.Transaction` instance, if any.
331 _transaction = None
332
333 # The last result received.
334 _last_result = None
335
336 # The set of bookmarks after which the next
337 # :class:`.Transaction` should be carried out.
338 _bookmarks_in = None
339
340 # The bookmark returned from the last commit.
341 _bookmark_out = None
342
343 # Default maximum time to keep retrying failed transactions.
344 _max_retry_time = default_config["max_retry_time"]
345
346 _closed = False
347
348 def __init__(self, acquirer, access_mode, **parameters):
349 self._acquirer = acquirer
350 self._default_access_mode = access_mode
351 for key, value in parameters.items():
352 if key == "bookmark":
353 if value:
354 self._bookmarks_in = tuple([value])
355 elif key == "bookmarks":
356 if value:
357 self._bookmarks_in = tuple(value)
358 elif key == "max_retry_time":
359 self._max_retry_time = value
360 else:
361 pass # for compatibility
362
363 def __del__(self):
364 try:
365 self.close()
366 except:
367 pass
368
369 def __enter__(self):
370 return self
371
372 def __exit__(self, exc_type, exc_value, traceback):
373 self.close()
374
375 def _connect(self, access_mode=None):
376 if access_mode is None:
377 access_mode = self._default_access_mode
378 if self._connection:
379 log.warn("FIXME: should always disconnect before connect")
380 self._connection.sync()
381 self._disconnect()
382 self._connection = self._acquirer(access_mode)
383
384 def _disconnect(self):
385 if self._connection:
386 self._connection.in_use = False
387 self._connection = None
388
389 def close(self):
390 """ Close the session. This will release any borrowed resources,
391 such as connections, and will roll back any outstanding transactions.
392 """
393 if self._connection:
394 if self._transaction:
395 self._connection.rollback()
396 self._transaction = None
404 @classmethod
405 def open(cls, *targets, auth=None, routing_context=None, **config):
406 from neo4j.io import Neo4jPool
407 addresses = cls.parse_targets(*targets)
408 pool_config, default_workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)
409 pool = Neo4jPool.open(*addresses, auth=auth, routing_context=routing_context, pool_config=pool_config, workspace_config=default_workspace_config)
410 return cls(pool, default_workspace_config)
411
412 def __init__(self, pool, default_workspace_config):
413 Routing.__init__(self, pool.get_default_database_initial_router_addresses())
414 Driver.__init__(self, pool)
415 self._default_workspace_config = default_workspace_config
416
417 def session(self, **config):
418 session_config = SessionConfig(self._default_workspace_config, config)
419 SessionConfig.consume(config) # Consume the config
420 return Session(self._pool, session_config)
421
422 def pipeline(self, **config):
423 from neo4j.work.pipelining import Pipeline, PipelineConfig
424 pipeline_config = PipelineConfig(self._default_workspace_config, config)
425 PipelineConfig.consume(config) # Consume the config
426 return Pipeline(self._pool, pipeline_config)
427
428 @experimental("The configuration may change in the future.")
429 def verify_connectivity(self, **config):
430 """
431 :raise ServiceUnavailable: raised if the server does not support routing or if routing support is broken.
432 """
433 # TODO: Improve and update Stub Test Server to be able to test.
434 return self._verify_routing_connectivity()
435
436 def _verify_routing_connectivity(self):
437 from neo4j.exceptions import ServiceUnavailable
438 from neo4j._exceptions import BoltHandshakeError
439
440 table = self._pool.get_routing_table_for_default_database()
441 routing_info = {}
442 for ix in list(table.routers):
397443 try:
398 self._connection.sync()
399 except (ConnectionExpired, CypherError, TransactionError,
400 ServiceUnavailable, SessionError):
401 pass
402 finally:
403 self._disconnect()
404 self._closed = True
405
406 def closed(self):
407 """ Indicator for whether or not this session has been closed.
408
409 :returns: :const:`True` if closed, :const:`False` otherwise.
410 """
411 return self._closed
412
413 def run(self, statement, parameters=None, **kwparameters):
414 """ Run a Cypher statement within an auto-commit transaction.
415
416 The statement is sent and the result header received
417 immediately but the :class:`.StatementResult` content is
418 fetched lazily as consumed by the client application.
419
420 If a statement is executed before a previous
421 :class:`.StatementResult` in the same :class:`.Session` has
422 been fully consumed, the first result will be fully fetched
423 and buffered. Note therefore that the generally recommended
424 pattern of usage is to fully consume one result before
425 executing a subsequent statement. If two results need to be
426 consumed in parallel, multiple :class:`.Session` objects
427 can be used as an alternative to result buffering.
428
429 For more usage details, see :meth:`.Transaction.run`.
430
431 :param statement: template Cypher statement
432 :param parameters: dictionary of parameters
433 :param kwparameters: additional keyword parameters
434 :returns: :class:`.StatementResult` object
435 """
436 from neobolt.exceptions import ConnectionExpired
437
438 self._assert_open()
439 if not statement:
440 raise ValueError("Cannot run an empty statement")
441
442 if not self._connection:
443 self._connect()
444 cx = self._connection
445 protocol_version = cx.protocol_version
446 server = cx.server
447
448 has_transaction = self.has_transaction()
449
450 statement_text = ustr(statement)
451 statement_metadata = getattr(statement, "metadata", None)
452 statement_timeout = getattr(statement, "timeout", None)
453 parameters = fix_parameters(dict(parameters or {}, **kwparameters), protocol_version,
454 supports_bytes=server.supports("bytes"))
455
456 def fail(_):
457 self._close_transaction()
458
459 hydrant = PackStreamHydrator(protocol_version)
460 result_metadata = {
461 "statement": statement_text,
462 "parameters": parameters,
463 "server": server,
464 "protocol_version": protocol_version,
465 }
466 run_metadata = {
467 "metadata": statement_metadata,
468 "timeout": statement_timeout,
469 "on_success": result_metadata.update,
470 "on_failure": fail,
471 }
472
473 def done(summary_metadata):
474 result_metadata.update(summary_metadata)
475 bookmark = result_metadata.get("bookmark")
476 if bookmark:
477 self._bookmarks_in = tuple([bookmark])
478 self._bookmark_out = bookmark
479
480 self._last_result = result = BoltStatementResult(self, hydrant, result_metadata)
481
482 if has_transaction:
483 if statement_metadata:
484 raise ValueError("Metadata can only be attached at transaction level")
485 if statement_timeout:
486 raise ValueError("Timeouts only apply at transaction level")
487 else:
488 run_metadata["bookmarks"] = self._bookmarks_in
489
490 cx.run(statement_text, parameters, **run_metadata)
491 cx.pull_all(
492 on_records=lambda records: result._records.extend(
493 hydrant.hydrate_records(result.keys(), records)),
494 on_success=done,
495 on_failure=fail,
496 on_summary=lambda: result.detach(sync=False),
497 )
498
499 if not has_transaction:
500 try:
501 self._connection.send()
502 self._connection.fetch()
503 except ConnectionExpired as error:
504 raise SessionExpired(*error.args)
505
506 return result
507
508 def send(self):
509 """ Send all outstanding requests.
510 """
511 from neobolt.exceptions import ConnectionExpired
512 if self._connection:
513 try:
514 self._connection.send()
515 except ConnectionExpired as error:
516 raise SessionExpired(*error.args)
517
518 def fetch(self):
519 """ Attempt to fetch at least one more record.
520
521 :returns: number of records fetched
522 """
523 from neobolt.exceptions import ConnectionExpired
524 if self._connection:
525 try:
526 detail_count, _ = self._connection.fetch()
527 except ConnectionExpired as error:
528 raise SessionExpired(*error.args)
529 else:
530 return detail_count
531 return 0
532
533 def sync(self):
534 """ Carry out a full send and receive.
535
536 :returns: number of records fetched
537 """
538 from neobolt.exceptions import ConnectionExpired
539 if self._connection:
540 try:
541 detail_count, _ = self._connection.sync()
542 except ConnectionExpired as error:
543 raise SessionExpired(*error.args)
544 else:
545 return detail_count
546 return 0
547
548 def detach(self, result, sync=True):
549 """ Detach a result from this session by fetching and buffering any
550 remaining records.
551
552 :param result:
553 :param sync:
554 :returns: number of records fetched
555 """
556 count = 0
557
558 if sync and result.attached():
559 self.send()
560 fetch = self.fetch
561 while result.attached():
562 count += fetch()
563
564 if self._last_result is result:
565 self._last_result = None
566 if not self.has_transaction():
567 self._disconnect()
568
569 result._session = None
570 return count
571
572 def next_bookmarks(self):
573 """ The set of bookmarks to be passed into the next
574 :class:`.Transaction`.
575 """
576 return self._bookmarks_in
577
578 def last_bookmark(self):
579 """ The bookmark returned by the last :class:`.Transaction`.
580 """
581 return self._bookmark_out
582
583 def has_transaction(self):
584 return bool(self._transaction)
585
586 def _close_transaction(self):
587 self._transaction = None
588
589 def begin_transaction(self, bookmark=None, metadata=None, timeout=None):
590 """ Create a new :class:`.Transaction` within this session.
591 Calling this method with a bookmark is equivalent to
592
593 :param bookmark: a bookmark to which the server should
594 synchronise before beginning the transaction
595 :param metadata:
596 :param timeout:
597 :returns: new :class:`.Transaction` instance.
598 :raise: :class:`.TransactionError` if a transaction is already open
599 """
600 self._assert_open()
601 if self.has_transaction():
602 raise TransactionError("Explicit transaction already open")
603
604 # TODO: remove in 2.0
605 if bookmark is not None:
606 global _warned_about_transaction_bookmarks
607 if not _warned_about_transaction_bookmarks:
608 from warnings import warn
609 warn("Passing bookmarks at transaction level is deprecated", category=DeprecationWarning, stacklevel=2)
610 _warned_about_transaction_bookmarks = True
611 self._bookmarks_in = tuple([bookmark])
612
613 self._open_transaction(metadata=metadata, timeout=timeout)
614 return self._transaction
615
616 def _open_transaction(self, access_mode=None, metadata=None, timeout=None):
617 self._transaction = Transaction(self, on_close=self._close_transaction)
618 self._connect(access_mode)
619 self._connection.begin(bookmarks=self._bookmarks_in, metadata=metadata, timeout=timeout)
620
621 def commit_transaction(self):
622 """ Commit the current transaction.
623
624 :returns: the bookmark returned from the server, if any
625 :raise: :class:`.TransactionError` if no transaction is currently open
626 """
627 self._assert_open()
628 if not self._transaction:
629 raise TransactionError("No transaction to commit")
630 metadata = {}
631 try:
632 self._connection.commit(on_success=metadata.update)
633 self._connection.sync()
634 except IncompleteCommitError:
635 raise ServiceUnavailable("Connection closed during commit")
636 finally:
637 self._disconnect()
638 self._transaction = None
639 bookmark = metadata.get("bookmark")
640 self._bookmarks_in = tuple([bookmark])
641 self._bookmark_out = bookmark
642 return bookmark
643
644 def rollback_transaction(self):
645 """ Rollback the current transaction.
646
647 :raise: :class:`.TransactionError` if no transaction is currently open
648 """
649 self._assert_open()
650 if not self._transaction:
651 raise TransactionError("No transaction to rollback")
652 cx = self._connection
653 if cx:
654 metadata = {}
655 try:
656 cx.rollback(on_success=metadata.update)
657 cx.sync()
658 finally:
659 self._disconnect()
660 self._transaction = None
661
662 def _run_transaction(self, access_mode, unit_of_work, *args, **kwargs):
663 from neobolt.exceptions import ConnectionExpired, TransientError, ServiceUnavailable
664
665 if not callable(unit_of_work):
666 raise TypeError("Unit of work is not callable")
667
668 metadata = getattr(unit_of_work, "metadata", None)
669 timeout = getattr(unit_of_work, "timeout", None)
670
671 retry_delay = retry_delay_generator(INITIAL_RETRY_DELAY,
672 RETRY_DELAY_MULTIPLIER,
673 RETRY_DELAY_JITTER_FACTOR)
674 errors = []
675 t0 = perf_counter()
676 while True:
677 try:
678 self._open_transaction(access_mode, metadata, timeout)
679 tx = self._transaction
680 try:
681 result = unit_of_work(tx, *args, **kwargs)
682 except Exception:
683 tx.success = False
684 raise
685 else:
686 if tx.success is None:
687 tx.success = True
688 finally:
689 tx.close()
690 except (ServiceUnavailable, SessionExpired, ConnectionExpired) as error:
691 errors.append(error)
692 except TransientError as error:
693 if is_retriable_transient_error(error):
694 errors.append(error)
695 else:
696 raise
697 else:
698 return result
699 t1 = perf_counter()
700 if t1 - t0 > self._max_retry_time:
701 break
702 delay = next(retry_delay)
703 log.warning("Transaction failed and will be retried in {}s "
704 "({})".format(delay, "; ".join(errors[-1].args)))
705 sleep(delay)
706 if errors:
707 raise errors[-1]
708 else:
709 raise ServiceUnavailable("Transaction failed")
710
711 def read_transaction(self, unit_of_work, *args, **kwargs):
712 self._assert_open()
713 return self._run_transaction(READ_ACCESS, unit_of_work, *args, **kwargs)
714
715 def write_transaction(self, unit_of_work, *args, **kwargs):
716 self._assert_open()
717 return self._run_transaction(WRITE_ACCESS, unit_of_work, *args, **kwargs)
718
719 def _assert_open(self):
720 if self._closed:
721 raise SessionError("Session closed")
722
723
724 class Transaction(object):
725 """ Container for multiple Cypher queries to be executed within
726 a single context. Transactions can be used within a :py:const:`with`
727 block where the value of :attr:`.success` will determine whether
728 the transaction is committed or rolled back on :meth:`.Transaction.close`::
729
730 with session.begin_transaction() as tx:
731 pass
732
733 """
734
735 #: When set, the transaction will be committed on close, otherwise it
736 #: will be rolled back. This attribute can be set in user code
737 #: multiple times before a transaction completes, with only the final
738 #: value taking effect.
739 success = None
740
741 _closed = False
742
743 def __init__(self, session, on_close):
744 self.session = session
745 self.on_close = on_close
746
747 def __enter__(self):
748 return self
749
750 def __exit__(self, exc_type, exc_value, traceback):
751 if self._closed:
752 return
753 if self.success is None:
754 self.success = not bool(exc_type)
755 self.close()
756
757 def run(self, statement, parameters=None, **kwparameters):
758 """ Run a Cypher statement within the context of this transaction.
759
760 The statement is sent to the server lazily, when its result is
761 consumed. To force the statement to be sent to the server, use
762 the :meth:`.Transaction.sync` method.
763
764 Cypher is typically expressed as a statement template plus a
765 set of named parameters. In Python, parameters may be expressed
766 through a dictionary of parameters, through individual parameter
767 arguments, or as a mixture of both. For example, the `run`
768 statements below are all equivalent::
769
770 >>> statement = "CREATE (a:Person {name:{name}, age:{age}})"
771 >>> tx.run(statement, {"name": "Alice", "age": 33})
772 >>> tx.run(statement, {"name": "Alice"}, age=33)
773 >>> tx.run(statement, name="Alice", age=33)
774
775 Parameter values can be of any type supported by the Neo4j type
776 system. In Python, this includes :class:`bool`, :class:`int`,
777 :class:`str`, :class:`list` and :class:`dict`. Note however that
778 :class:`list` properties must be homogenous.
779
780 :param statement: template Cypher statement
781 :param parameters: dictionary of parameters
782 :param kwparameters: additional keyword parameters
783 :returns: :class:`.StatementResult` object
784 :raise TransactionError: if the transaction is closed
785 """
786 self._assert_open()
787 return self.session.run(statement, parameters, **kwparameters)
788
789 def sync(self):
790 """ Force any queued statements to be sent to the server and
791 all related results to be fetched and buffered.
792
793 :raise TransactionError: if the transaction is closed
794 """
795 self._assert_open()
796 self.session.sync()
797
798 def commit(self):
799 """ Mark this transaction as successful and close in order to
800 trigger a COMMIT. This is functionally equivalent to::
801
802 tx.success = True
803 tx.close()
804
805 :raise TransactionError: if already closed
806 """
807 self.success = True
808 self.close()
809
810 def rollback(self):
811 """ Mark this transaction as unsuccessful and close in order to
812 trigger a ROLLBACK. This is functionally equivalent to::
813
814 tx.success = False
815 tx.close()
816
817 :raise TransactionError: if already closed
818 """
819 self.success = False
820 self.close()
821
822 def close(self):
823 """ Close this transaction, triggering either a COMMIT or a ROLLBACK,
824 depending on the value of :attr:`.success`.
825
826 :raise TransactionError: if already closed
827 """
828 from neobolt.exceptions import CypherError
829 self._assert_open()
830 try:
831 self.sync()
832 except CypherError:
833 self.success = False
834 raise
835 finally:
836 if self.session.has_transaction():
837 if self.success:
838 self.session.commit_transaction()
839 else:
840 self.session.rollback_transaction()
841 self._closed = True
842 self.on_close()
843
844 def closed(self):
845 """ Indicator to show whether the transaction has been closed.
846 :returns: :const:`True` if closed, :const:`False` otherwise.
847 """
848 return self._closed
849
850 def _assert_open(self):
851 if self._closed:
852 raise TransactionError("Transaction closed")
853
854
855 class Statement(object):
856
857 def __init__(self, text, metadata=None, timeout=None):
858 self.text = text
859 try:
860 self.metadata = metadata
861 except TypeError:
862 raise TypeError("Metadata must be coercible to a dict")
863 try:
864 self.timeout = timeout
865 except TypeError:
866 raise TypeError("Timeout must be specified as a number of seconds")
867
868 def __str__(self):
869 return xstr(self.text)
870
871
872 def fix_parameters(parameters, protocol_version, **kwargs):
873 if not parameters:
874 return {}
875 dehydrator = PackStreamDehydrator(protocol_version, **kwargs)
876 try:
877 dehydrated, = dehydrator.dehydrate([parameters])
878 except TypeError as error:
879 value = error.args[0]
880 raise TypeError("Parameters of type {} are not supported".format(type(value).__name__))
881 else:
882 return dehydrated
883
884
885 class StatementResult(object):
886 """ A handler for the result of Cypher statement execution. Instances
887 of this class are typically constructed and returned by
888 :meth:`.Session.run` and :meth:`.Transaction.run`.
889 """
890
891 def __init__(self, session, hydrant, metadata):
892 self._session = session
893 self._hydrant = hydrant
894 self._metadata = metadata
895 self._records = deque()
896 self._summary = None
897
898 def __iter__(self):
899 return self.records()
900
901 @property
902 def session(self):
903 """ The :class:`.Session` to which this result is attached, if any.
904 """
905 return self._session
906
907 def attached(self):
908 """ Indicator for whether or not this result is still attached to
909 an open :class:`.Session`.
910 """
911 return self._session and not self._session.closed()
912
913 def detach(self, sync=True):
914 """ Detach this result from its parent session by fetching the
915 remainder of this result from the network into the buffer.
916
917 :returns: number of records fetched
918 """
919 if self.attached():
920 return self._session.detach(self, sync=sync)
921 else:
922 return 0
923
924 def keys(self):
925 """ The keys for the records in this result.
926
927 :returns: tuple of key names
928 """
929 try:
930 return self._metadata["fields"]
931 except KeyError:
932 if self.attached():
933 self._session.send()
934 while self.attached() and "fields" not in self._metadata:
935 self._session.fetch()
936 return self._metadata.get("fields")
937
938 def records(self):
939 """ Generator for records obtained from this result.
940
941 :yields: iterable of :class:`.Record` objects
942 """
943 records = self._records
944 next_record = records.popleft
945 while records:
946 yield next_record()
947 attached = self.attached
948 if attached():
949 self._session.send()
950 while attached():
951 self._session.fetch()
952 while records:
953 yield next_record()
954
955 def summary(self):
956 """ Obtain the summary of this result, buffering any remaining records.
957
958 :returns: The :class:`.ResultSummary` for this result
959 """
960 self.detach()
961 if self._summary is None:
962 self._summary = BoltStatementResultSummary(**self._metadata)
963 return self._summary
964
965 def consume(self):
966 """ Consume the remainder of this result and return the summary.
967
968 :returns: The :class:`.ResultSummary` for this result
969 """
970 if self.attached():
971 for _ in self:
972 pass
973 return self.summary()
974
975 def single(self):
976 """ Obtain the next and only remaining record from this result.
977
978 A warning is generated if more than one record is available but
979 the first of these is still returned.
980
981 :returns: the next :class:`.Record` or :const:`None` if none remain
982 :warns: if more than one record is available
983 """
984 records = list(self)
985 size = len(records)
986 if size == 0:
987 return None
988 if size != 1:
989 warn("Expected a result with a single record, but this result contains %d" % size)
990 return records[0]
991
992 def peek(self):
993 """ Obtain the next record from this result without consuming it.
994 This leaves the record in the buffer for further processing.
995
996 :returns: the next :class:`.Record` or :const:`None` if none remain
997 """
998 records = self._records
999 if records:
1000 return records[0]
1001 if not self.attached():
1002 return None
1003 if self.attached():
1004 self._session.send()
1005 while self.attached() and not records:
1006 self._session.fetch()
1007 if records:
1008 return records[0]
1009 return None
1010
1011 def graph(self):
1012 """ Return a Graph instance containing all the graph objects
1013 in the result. After calling this method, the result becomes
1014 detached, buffering all remaining records.
1015
1016 :returns: result graph
1017 """
1018 self.detach()
1019 return self._hydrant.graph
1020
1021
1022 class BoltStatementResult(StatementResult):
1023 """ A handler for the result of Cypher statement execution.
1024 """
1025
1026 def __init__(self, session, hydrant, metadata):
1027 super(BoltStatementResult, self).__init__(session, hydrant, metadata)
1028
1029 def value(self, item=0, default=None):
1030 """ Return the remainder of the result as a list of values.
1031
1032 :param item: field to return for each remaining record
1033 :param default: default value, used if the index of key is unavailable
1034 :returns: list of individual values
1035 """
1036 return [record.value(item, default) for record in self.records()]
1037
1038 def values(self, *items):
1039 """ Return the remainder of the result as a list of tuples.
1040
1041 :param items: fields to return for each remaining record
1042 :returns: list of value tuples
1043 """
1044 return [record.values(*items) for record in self.records()]
1045
1046 def data(self, *items):
1047 """ Return the remainder of the result as a list of dictionaries.
1048
1049 :param items: fields to return for each remaining record
1050 :returns: list of dictionaries
1051 """
1052 return [record.data(*items) for record in self.records()]
1053
1054
1055 class BoltStatementResultSummary(object):
1056 """ A summary of execution returned with a :class:`.StatementResult` object.
1057 """
1058
1059 #: The version of Bolt protocol over which this result was obtained.
1060 protocol_version = None
1061
1062 #: The server on which this result was generated.
1063 server = None
1064
1065 #: The statement that was executed to produce this result.
1066 statement = None
1067
1068 #: Dictionary of parameters passed with the statement.
1069 parameters = None
1070
1071 #: The type of statement (``'r'`` = read-only, ``'rw'`` = read/write).
1072 statement_type = None
1073
1074 #: A set of statistical information held in a :class:`.Counters` instance.
1075 counters = None
1076
1077 #: A :class:`.Plan` instance
1078 plan = None
1079
1080 #: A :class:`.ProfiledPlan` instance
1081 profile = None
1082
1083 #: The time it took for the server to have the result available. (milliseconds)
1084 result_available_after = None
1085
1086 #: The time it took for the server to consume the result. (milliseconds)
1087 result_consumed_after = None
1088
1089 #: Notifications provide extra information for a user executing a statement.
1090 #: They can be warnings about problematic queries or other valuable information that can be
1091 #: presented in a client.
1092 #: Unlike failures or errors, notifications do not affect the execution of a statement.
1093 notifications = None
1094
1095 def __init__(self, **metadata):
1096 self.metadata = metadata
1097 self.protocol_version = metadata.get("protocol_version")
1098 self.server = metadata.get("server")
1099 self.statement = metadata.get("statement")
1100 self.parameters = metadata.get("parameters")
1101 self.statement_type = metadata.get("type")
1102 self.counters = SummaryCounters(metadata.get("stats", {}))
1103 if self.protocol_version < BOLT_VERSION_3:
1104 self.result_available_after = metadata.get("result_available_after")
1105 self.result_consumed_after = metadata.get("result_consumed_after")
1106 else:
1107 self.result_available_after = metadata.get("t_first")
1108 self.result_consumed_after = metadata.get("t_last")
1109 if "plan" in metadata:
1110 self.plan = _make_plan(metadata["plan"])
1111 if "profile" in metadata:
1112 self.profile = _make_plan(metadata["profile"])
1113 self.plan = self.profile
1114 self.notifications = []
1115 for notification in metadata.get("notifications", []):
1116 position = notification.get("position")
1117 if position is not None:
1118 position = Position(position["offset"], position["line"], position["column"])
1119 self.notifications.append(Notification(notification["code"], notification["title"],
1120 notification["description"], notification["severity"], position))
1121
1122
1123 class SummaryCounters(object):
1124 """ Set of statistics from a Cypher statement execution.
1125 """
1126
1127 #:
1128 nodes_created = 0
1129
1130 #:
1131 nodes_deleted = 0
1132
1133 #:
1134 relationships_created = 0
1135
1136 #:
1137 relationships_deleted = 0
1138
1139 #:
1140 properties_set = 0
1141
1142 #:
1143 labels_added = 0
1144
1145 #:
1146 labels_removed = 0
1147
1148 #:
1149 indexes_added = 0
1150
1151 #:
1152 indexes_removed = 0
1153
1154 #:
1155 constraints_added = 0
1156
1157 #:
1158 constraints_removed = 0
1159
1160 def __init__(self, statistics):
1161 for key, value in dict(statistics).items():
1162 key = key.replace("-", "_")
1163 setattr(self, key, value)
1164
1165 def __repr__(self):
1166 return repr(vars(self))
1167
1168 @property
1169 def contains_updates(self):
1170 return bool(self.nodes_created or self.nodes_deleted or
1171 self.relationships_created or self.relationships_deleted or
1172 self.properties_set or self.labels_added or self.labels_removed or
1173 self.indexes_added or self.indexes_removed or
1174 self.constraints_added or self.constraints_removed)
1175
1176
1177 #: A plan describes how the database will execute your statement.
1178 #:
1179 #: operator_type:
1180 #: the name of the operation performed by the plan
1181 #: identifiers:
1182 #: the list of identifiers used by this plan
1183 #: arguments:
1184 #: a dictionary of arguments used in the specific operation performed by the plan
1185 #: children:
1186 #: a list of sub-plans
1187 Plan = namedtuple("Plan", ("operator_type", "identifiers", "arguments", "children"))
1188
1189 #: A profiled plan describes how the database executed your statement.
1190 #:
1191 #: db_hits:
1192 #: the number of times this part of the plan touched the underlying data stores
1193 #: rows:
1194 #: the number of records this part of the plan produced
1195 ProfiledPlan = namedtuple("ProfiledPlan", Plan._fields + ("db_hits", "rows"))
1196
1197 #: Representation for notifications found when executing a statement. A
1198 #: notification can be visualized in a client pinpointing problems or
1199 #: other information about the statement.
1200 #:
1201 #: code:
1202 #: a notification code for the discovered issue.
1203 #: title:
1204 #: a short summary of the notification
1205 #: description:
1206 #: a long description of the notification
1207 #: severity:
1208 #: the severity level of the notification
1209 #: position:
1210 #: the position in the statement where this notification points to, if relevant.
1211 Notification = namedtuple("Notification", ("code", "title", "description", "severity", "position"))
1212
1213 #: A position within a statement, consisting of offset, line and column.
1214 #:
1215 #: offset:
1216 #: the character offset referred to by this position; offset numbers start at 0
1217 #: line:
1218 #: the line number referred to by the position; line numbers start at 1
1219 #: column:
1220 #: the column number referred to by the position; column numbers start at 1
1221 Position = namedtuple("Position", ("offset", "line", "column"))
1222
1223
1224 def _make_plan(plan_dict):
1225 """ Construct a Plan or ProfiledPlan from a dictionary of metadata values.
1226
1227 :param plan_dict:
1228 :return:
1229 """
1230 operator_type = plan_dict["operatorType"]
1231 identifiers = plan_dict.get("identifiers", [])
1232 arguments = plan_dict.get("args", [])
1233 children = [_make_plan(child) for child in plan_dict.get("children", [])]
1234 if "dbHits" in plan_dict or "rows" in plan_dict:
1235 db_hits = plan_dict.get("dbHits", 0)
1236 rows = plan_dict.get("rows", 0)
1237 return ProfiledPlan(operator_type, identifiers, arguments, children, db_hits, rows)
1238 else:
1239 return Plan(operator_type, identifiers, arguments, children)
1240
1241
1242 class Record(tuple, Mapping):
1243 """ A :class:`.Record` is an immutable ordered collection of key-value
1244 pairs. It is generally closer to a :py:class:`namedtuple` than to a
1245 :py:class:`OrderedDict` inasmuch as iteration of the collection will
1246 yield values rather than keys.
1247 """
1248
1249 __keys = None
1250
1251 def __new__(cls, iterable):
1252 keys = []
1253 values = []
1254 for key, value in iter_items(iterable):
1255 keys.append(key)
1256 values.append(value)
1257 inst = tuple.__new__(cls, values)
1258 inst.__keys = tuple(keys)
1259 return inst
1260
1261 def __repr__(self):
1262 return "<%s %s>" % (self.__class__.__name__,
1263 " ".join("%s=%r" % (field, self[i]) for i, field in enumerate(self.__keys)))
1264
1265 def __eq__(self, other):
1266 """ In order to be flexible regarding comparison, the equality rules
1267 for a record permit comparison with any other Sequence or Mapping.
1268
1269 :param other:
1270 :return:
1271 """
1272 compare_as_sequence = isinstance(other, Sequence)
1273 compare_as_mapping = isinstance(other, Mapping)
1274 if compare_as_sequence and compare_as_mapping:
1275 return list(self) == list(other) and dict(self) == dict(other)
1276 elif compare_as_sequence:
1277 return list(self) == list(other)
1278 elif compare_as_mapping:
1279 return dict(self) == dict(other)
1280 else:
1281 return False
1282
1283 def __ne__(self, other):
1284 return not self.__eq__(other)
1285
1286 def __hash__(self):
1287 return reduce(xor_operator, map(hash, self.items()))
1288
1289 def __getitem__(self, key):
1290 if isinstance(key, slice):
1291 keys = self.__keys[key]
1292 values = super(Record, self).__getitem__(key)
1293 return self.__class__(zip(keys, values))
1294 index = self.index(key)
1295 if 0 <= index < len(self):
1296 return super(Record, self).__getitem__(index)
1297 else:
1298 return None
1299
1300 def __getslice__(self, start, stop):
1301 key = slice(start, stop)
1302 keys = self.__keys[key]
1303 values = tuple(self)[key]
1304 return self.__class__(zip(keys, values))
1305
1306 def get(self, key, default=None):
1307 """ Obtain a value from the record by key, returning a default
1308 value if the key does not exist.
1309
1310 :param key:
1311 :param default:
1312 :return:
1313 """
1314 try:
1315 index = self.__keys.index(ustr(key))
1316 except ValueError:
1317 return default
1318 if 0 <= index < len(self):
1319 return super(Record, self).__getitem__(index)
1320 else:
1321 return default
1322
1323 def index(self, key):
1324 """ Return the index of the given item.
1325
1326 :param key:
1327 :return:
1328 """
1329 if isinstance(key, integer):
1330 if 0 <= key < len(self.__keys):
1331 return key
1332 raise IndexError(key)
1333 elif isinstance(key, string):
1334 try:
1335 return self.__keys.index(key)
1336 except ValueError:
1337 raise KeyError(key)
1338 else:
1339 raise TypeError(key)
1340
1341 def value(self, key=0, default=None):
1342 """ Obtain a single value from the record by index or key. If no
1343 index or key is specified, the first value is returned. If the
1344 specified item does not exist, the default value is returned.
1345
1346 :param key:
1347 :param default:
1348 :return:
1349 """
1350 try:
1351 index = self.index(key)
1352 except (IndexError, KeyError):
1353 return default
1354 else:
1355 return self[index]
1356
1357 def keys(self):
1358 """ Return the keys of the record.
1359
1360 :return: list of key names
1361 """
1362 return list(self.__keys)
1363
1364 def values(self, *keys):
1365 """ Return the values of the record, optionally filtering to
1366 include only certain values by index or key.
1367
1368 :param keys: indexes or keys of the items to include; if none
1369 are provided, all values will be included
1370 :return: list of values
1371 """
1372 if keys:
1373 d = []
1374 for key in keys:
1375 try:
1376 i = self.index(key)
1377 except KeyError:
1378 d.append(None)
1379 else:
1380 d.append(self[i])
1381 return d
1382 return list(self)
1383
1384 def items(self, *keys):
1385 """ Return the fields of the record as a list of key and value tuples
1386
1387 :return:
1388 """
1389 if keys:
1390 d = []
1391 for key in keys:
1392 try:
1393 i = self.index(key)
1394 except KeyError:
1395 d.append((key, None))
1396 else:
1397 d.append((self.__keys[i], self[i]))
1398 return d
1399 return list((self.__keys[i], super(Record, self).__getitem__(i)) for i in range(len(self)))
1400
1401 def data(self, *keys):
1402 """ Return the keys and values of this record as a dictionary,
1403 optionally including only certain values by index or key. Keys
1404 provided in the items that are not in the record will be
1405 inserted with a value of :const:`None`; indexes provided
1406 that are out of bounds will trigger an :exc:`IndexError`.
1407
1408 :param keys: indexes or keys of the items to include; if none
1409 are provided, all values will be included
1410 :return: dictionary of values, keyed by field name
1411 :raises: :exc:`IndexError` if an out-of-bounds index is specified
1412 """
1413 if keys:
1414 d = {}
1415 for key in keys:
1416 try:
1417 i = self.index(key)
1418 except KeyError:
1419 d[key] = None
1420 else:
1421 d[self.__keys[i]] = self[i]
1422 return d
1423 return dict(self)
1424
1425
1426 class DriverError(Exception):
1427 """ Raised when an error occurs while using a driver.
1428 """
1429
1430 def __init__(self, driver, *args, **kwargs):
1431 super(DriverError, self).__init__(*args, **kwargs)
1432 self.driver = driver
1433
1434
1435 class SessionError(Exception):
1436 """ Raised when an error occurs while using a session.
1437 """
1438
1439 def __init__(self, session, *args, **kwargs):
1440 super(SessionError, self).__init__(*args, **kwargs)
1441 self.session = session
1442
1443
1444 class SessionExpired(SessionError):
1445 """ Raised when no a session is no longer able to fulfil
1446 the purpose described by its original parameters.
1447 """
1448
1449 def __init__(self, session, *args, **kwargs):
1450 super(SessionExpired, self).__init__(session, *args, **kwargs)
1451
1452
1453 class TransactionError(Exception):
1454 """ Raised when an error occurs while using a transaction.
1455 """
1456
1457 def __init__(self, transaction, *args, **kwargs):
1458 super(TransactionError, self).__init__(*args, **kwargs)
1459 self.transaction = transaction
1460
1461
1462 def unit_of_work(metadata=None, timeout=None):
1463 """ This function is a decorator for transaction functions that allows
1464 extra control over how the transaction is carried out.
1465
1466 For example, a timeout (in seconds) may be applied::
1467
1468 @unit_of_work(timeout=25.0)
1469 def count_people(tx):
1470 return tx.run("MATCH (a:Person) RETURN count(a)").single().value()
1471
1472 """
1473
1474 def wrapper(f):
1475
1476 def wrapped(*args, **kwargs):
1477 return f(*args, **kwargs)
1478
1479 wrapped.metadata = metadata
1480 wrapped.timeout = timeout
1481 return wrapped
1482
1483 return wrapper
1484
1485
1486 def basic_auth(user, password, realm=None):
1487 """ Generate a basic auth token for a given user and password.
1488
1489 :param user: user name
1490 :param password: current password
1491 :param realm: specifies the authentication provider
1492 :return: auth token for use with :meth:`GraphDatabase.driver`
1493 """
1494 from neobolt.security import AuthToken
1495 return AuthToken("basic", user, password, realm)
1496
1497
1498 def kerberos_auth(base64_encoded_ticket):
1499 """ Generate a kerberos auth token with the base64 encoded ticket
1500
1501 :param base64_encoded_ticket: a base64 encoded service ticket
1502 :return: an authentication token that can be used to connect to Neo4j
1503 """
1504 from neobolt.security import AuthToken
1505 return AuthToken("kerberos", "", base64_encoded_ticket)
1506
1507
1508 def custom_auth(principal, credentials, realm, scheme, **parameters):
1509 """ Generate a basic auth token for a given user and password.
1510
1511 :param principal: specifies who is being authenticated
1512 :param credentials: authenticates the principal
1513 :param realm: specifies the authentication provider
1514 :param scheme: specifies the type of authentication
1515 :param parameters: parameters passed along to the authentication provider
1516 :return: auth token for use with :meth:`GraphDatabase.driver`
1517 """
1518 from neobolt.security import AuthToken
1519 return AuthToken(scheme, principal, credentials, realm, **parameters)
1520
1521
1522 def iter_items(iterable):
1523 """ Iterate through all items (key-value pairs) within an iterable
1524 dictionary-like object. If the object has a `keys` method, this is
1525 used along with `__getitem__` to yield each pair in turn. If no
1526 `keys` method exists, each iterable element is assumed to be a
1527 2-tuple of key and value.
1528 """
1529 if hasattr(iterable, "keys"):
1530 for key in iterable.keys():
1531 yield key, iterable[key]
1532 else:
1533 for key, value in iterable:
1534 yield key, value
1535
1536
1537 def retry_delay_generator(initial_delay, multiplier, jitter_factor):
1538 delay = initial_delay
1539 while True:
1540 jitter = jitter_factor * delay
1541 yield delay - jitter + (2 * jitter * random())
1542 delay *= multiplier
1543
1544
1545 def is_retriable_transient_error(error):
1546 """
1547 :type error: TransientError
1548 """
1549 return not (error.code in ("Neo.TransientError.Transaction.Terminated",
1550 "Neo.TransientError.Transaction.LockClientStopped"))
1551
1552
1553 from neo4j.types import *
444 routing_info[ix] = self._pool.fetch_routing_info(address=table.routers[0], timeout=self._default_workspace_config.connection_acquisition_timeout, database=self._default_workspace_config.database)
445 except BoltHandshakeError as error:
446 routing_info[ix] = None
447
448 for key, val in routing_info.items():
449 if val is not None:
450 return routing_info
451 raise ServiceUnavailable("Could not connect to any routing servers.")
+0
-102
neo4j/__main__.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from __future__ import unicode_literals
22
23 import logging
24 from argparse import ArgumentParser
25 from getpass import getpass
26 from json import loads as json_loads
27 from sys import stdout, stderr
28
29 from neobolt.diagnostics import Watcher
30
31 from neo4j import GraphDatabase
32 from neo4j.exceptions import CypherError
33
34
35 def main():
36 parser = ArgumentParser(description="Execute one or more Cypher statements using Bolt.")
37 parser.add_argument("statement", nargs="+")
38 parser.add_argument("-H", "--header", action="store_true")
39 parser.add_argument("-P", "--password")
40 parser.add_argument("-p", "--parameter", action="append", metavar="NAME=VALUE")
41 parser.add_argument("-q", "--quiet", action="store_true")
42 parser.add_argument("-r", "--read-only", action="store_true")
43 parser.add_argument("-s", "--secure", action="store_true")
44 parser.add_argument("-U", "--user", default="neo4j")
45 parser.add_argument("-u", "--uri", default="bolt://", metavar="CONNECTION_URI")
46 parser.add_argument("-v", "--verbose", action="count")
47 parser.add_argument("-x", "--times", type=int, default=1)
48 parser.add_argument("-z", "--summary", action="store_true")
49 args = parser.parse_args()
50
51 if args.verbose:
52 level = logging.INFO if args.verbose == 1 else logging.DEBUG
53 Watcher("neobolt").watch(level, stderr)
54
55 parameters = {}
56 for parameter in args.parameter or []:
57 name, _, value = parameter.partition("=")
58 if value == "" and name in parameters:
59 del parameters[name]
60 else:
61 try:
62 parameters[name] = json_loads(value)
63 except ValueError:
64 parameters[name] = value
65
66 try:
67 password = args.password or getpass()
68 except KeyboardInterrupt:
69 exit(0)
70 else:
71 with GraphDatabase.driver(args.uri, auth=(args.user, password), encrypted=args.secure) as driver:
72 with driver.session() as session:
73
74 run = session.read_transaction if args.read_only else session.write_transaction
75
76 for _ in range(args.times):
77 for statement in args.statement:
78
79 def work(tx, p):
80 result = tx.run(statement, p)
81 if not args.quiet:
82 if args.header:
83 stdout.write("%s\r\n" % "\t".join(result.keys()))
84 for i, record in enumerate(result):
85 stdout.write("%s\r\n" % "\t".join(map(repr, record.values())))
86 if args.summary:
87 summary = result.summary()
88 stdout.write("Statement : %r\r\n" % summary.statement)
89 stdout.write("Parameters : %r\r\n" % summary.parameters)
90 stdout.write("Statement Type : %r\r\n" % summary.statement_type)
91 stdout.write("Counters : %r\r\n" % summary.counters)
92 stdout.write("\r\n")
93
94 try:
95 run(work, parameters)
96 except CypherError as error:
97 stderr.write("%s: %s\r\n" % (error.code, error.message))
98
99
100 if __name__ == "__main__":
101 main()
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """ Internal module for all Bolt exception classes.
22 """
23
24
25 from os import strerror
26
27
28 class BoltError(Exception):
29 """ Base class for all Bolt protocol errors.
30 """
31
32 def __init__(self, message, address):
33 super().__init__(message)
34 self.address = address
35
36
37 class BoltConnectionError(BoltError):
38 """ Raised when a connection fails.
39 """
40
41 def __init__(self, message, address):
42 msg = (
43 "Connection Failed. "
44 "Please ensure that your database is listening on the correct host and port and that you have enabled encryption if required. "
45 "Note that the default encryption setting has changed in Neo4j 4.0. See the docs for more information. "
46 "{}")
47
48 super().__init__(msg.format(message), address)
49
50 def __str__(self):
51 s = super().__str__()
52 errno = self.errno
53 if errno:
54 s += " (code {}: {})".format(errno, strerror(errno))
55 return s
56
57 @property
58 def errno(self):
59 try:
60 return self.__cause__.errno
61 except AttributeError:
62 return None
63
64
65 class BoltSecurityError(BoltConnectionError):
66 """ Raised when a connection fails for security reasons.
67 """
68
69 def __str__(self):
70 return "[{}] {}".format(self.__cause__.__class__.__name__, super().__str__())
71
72
73 class BoltConnectionBroken(BoltConnectionError):
74 """ Raised when an established connection is lost or when an
75 attempt is made to use a connection that has previously broken.
76 """
77
78 # TODO: add details of outstanding commits (if any), plus maybe other requests outstanding
79
80
81 class BoltConnectionClosed(BoltConnectionError):
82 """ Raised when an attempt is made to use a connection that has
83 been closed locally.
84 """
85
86
87 class BoltHandshakeError(BoltError):
88 """ Raised when a handshake completes unsuccessfully.
89 """
90
91 def __init__(self, message, address, request_data, response_data):
92 super().__init__(message, address)
93 self.request_data = request_data
94 self.response_data = response_data
95
96
97 class BoltTransactionError(BoltError):
98 """ Raised when a fault occurs with a transaction, or when a
99 transaction is used incorrectly.
100 """
101 # TODO: pass the transaction object in as an argument
102
103
104 class BoltRoutingError(BoltError):
105 """ Raised when a fault occurs with obtaining a routing table.
106 """
107
108
109 class BoltFailure(BoltError):
110 """ Holds a Cypher failure.
111 """
112
113 #:
114 code = None
115
116 #:
117 classification = None
118
119 #:
120 category = None
121
122 #:
123 title = None
124
125 #: Flag to indicate whether an error is safe to retry or not. False
126 #: (not retryable) by default. This can be overridden by instances
127 #: or subclasses.
128 transient = False
129
130 @classmethod
131 def _find_subclass(cls, predicate, default=None):
132 if hasattr(cls, "__subclasses__"):
133 for subclass in cls.__subclasses__():
134 if predicate(subclass):
135 return subclass
136 return default
137
138 def __new__(cls, message, address, code, response):
139 code_parts = code.split(".")
140 classification = code_parts[1]
141 c1 = cls._find_subclass(lambda k: k.__name__ == classification, cls)
142 assert issubclass(c1, cls)
143 c2 = c1._find_subclass(lambda k: k.code == code, c1)
144 assert issubclass(c2, c1)
145 return super().__new__(c2, message, address)
146
147 def __init__(self, message, address, code, response):
148 super().__init__(message, address)
149 self.code = code
150 code_parts = self.code.split(".")
151 self.classification = code_parts[1]
152 self.category = code_parts[2]
153 self.title = code_parts[3]
154 self.response = response
155
156 def __str__(self):
157 return "[{}.{}] {}".format(self.category, self.title, super().__str__())
158
159 @property
160 def result(self):
161 """ The Result object to which this failure is attached (if any).
162 """
163 try:
164 return self.response.result
165 except AttributeError:
166 return None
167
168 @property
169 def transaction(self):
170 try:
171 return self.result.transaction
172 except AttributeError:
173 return None
174
175
176 class BoltIncompleteCommitError(BoltError):
177 """ Raised when a disconnection occurs while still waiting for a commit
178 response. For non-idempotent write transactions, this leaves the data
179 in an unknown state with regard to whether the transaction completed
180 successfully or not.
181 """
182
183
184 class BoltProtocolError(BoltError):
185 """ Raised when an unexpected or unsupported protocol event occurs.
186 """
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from socket import (
22 getaddrinfo,
23 getservbyname,
24 SOCK_STREAM,
25 AF_INET,
26 AF_INET6,
27 )
28
29
30 class Address(tuple):
31
32 @classmethod
33 def from_socket(cls, socket):
34 address = socket.getpeername()
35 return cls(address)
36
37 @classmethod
38 def parse(cls, s, default_host=None, default_port=None):
39 if not isinstance(s, str):
40 raise TypeError("Address.parse requires a string argument")
41 if s.startswith("["):
42 # IPv6
43 host, _, port = s[1:].rpartition("]")
44 port = port.lstrip(":")
45 try:
46 port = int(port)
47 except (TypeError, ValueError):
48 pass
49 return cls((host or default_host or "localhost",
50 port or default_port or 0, 0, 0))
51 else:
52 # IPv4
53 host, _, port = s.partition(":")
54 try:
55 port = int(port)
56 except (TypeError, ValueError):
57 pass
58 return cls((host or default_host or "localhost",
59 port or default_port or 0))
60
61 @classmethod
62 def parse_list(cls, *s, default_host=None, default_port=None):
63 """ Parse a string containing one or more socket addresses, each
64 separated by whitespace.
65 """
66 if not all(isinstance(s0, str) for s0 in s):
67 raise TypeError("Address.parse_list requires a string argument")
68 return [Address.parse(a, default_host, default_port)
69 for a in " ".join(s).split()]
70
71 def __new__(cls, iterable):
72 if isinstance(iterable, cls):
73 return iterable
74 n_parts = len(iterable)
75 inst = tuple.__new__(cls, iterable)
76 if n_parts == 2:
77 inst.__class__ = IPv4Address
78 elif n_parts == 4:
79 inst.__class__ = IPv6Address
80 else:
81 raise ValueError("Addresses must consist of either "
82 "two parts (IPv4) or four parts (IPv6)")
83 return inst
84
85 #: Address family (AF_INET or AF_INET6)
86 family = None
87
88 def __repr__(self):
89 return "{}({!r})".format(self.__class__.__name__, tuple(self))
90
91 @property
92 def host(self):
93 return self[0]
94
95 @property
96 def port(self):
97 return self[1]
98
99 @classmethod
100 def _dns_resolve(cls, address, family=0):
101 """ Regular DNS resolver. Takes an address object and optional
102 address family for filtering.
103
104 :param address:
105 :param family:
106 :return:
107 """
108 try:
109 info = getaddrinfo(address.host, address.port, family, SOCK_STREAM)
110 except OSError:
111 raise ValueError("Cannot resolve address {}".format(address))
112 else:
113 resolved = []
114 for fam, _, _, _, addr in info:
115 if fam == AF_INET6 and addr[3] != 0:
116 # skip any IPv6 addresses with a non-zero scope id
117 # as these appear to cause problems on some platforms
118 continue
119 if addr not in resolved:
120 resolved.append(Address(addr))
121 return resolved
122
123 def resolve(self, family=0, resolver=None):
124 """ Carry out domain name resolution on this Address object.
125
126 If a resolver function is supplied, and is callable, this is
127 called first, with this object as its argument. This may yield
128 multiple output addresses, which are chained into a subsequent
129 regular DNS resolution call. If no resolver function is passed,
130 the DNS resolution is carried out on the original Address
131 object.
132
133 This function returns a list of resolved Address objects.
134
135 :param family: optional address family to filter resolved
136 addresses by (e.g. AF_INET6)
137 :param resolver: optional customer resolver function to be
138 called before regular DNS resolution
139 """
140 resolved = []
141 if resolver:
142 for address in map(Address, resolver(self)):
143 resolved.extend(self._dns_resolve(address, family))
144 else:
145 resolved.extend(self._dns_resolve(self, family))
146 return resolved
147
148 @property
149 def port_number(self):
150 try:
151 return getservbyname(self[1])
152 except (OSError, TypeError):
153 # OSError: service/proto not found
154 # TypeError: getservbyname() argument 1 must be str, not X
155 try:
156 return int(self[1])
157 except (TypeError, ValueError) as e:
158 raise type(e)("Unknown port value %r" % self[1])
159
160
161 class IPv4Address(Address):
162
163 family = AF_INET
164
165 def __str__(self):
166 return "{}:{}".format(*self)
167
168
169 class IPv6Address(Address):
170
171 family = AF_INET6
172
173 def __str__(self):
174 return "[{}]:{}".format(*self)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from urllib.parse import (
21 urlparse,
22 parse_qs,
23 )
24 from.exceptions import (
25 DriverError,
26 ConfigurationError,
27 )
28
29 """ Base classes and helpers.
30 """
31
32 READ_ACCESS = "READ"
33 WRITE_ACCESS = "WRITE"
34
35 DRIVER_BOLT = "DRIVER_BOLT"
36 DRIVER_NEO4j = "DRIVER_NEO4J"
37
38 SECURITY_TYPE_NOT_SECURE = "SECURITY_TYPE_NOT_SECURE"
39 SECURITY_TYPE_SELF_SIGNED_CERTIFICATE = "SECURITY_TYPE_SELF_SIGNED_CERTIFICATE"
40 SECURITY_TYPE_SECURE = "SECURITY_TYPE_SECURE"
41
42 URI_SCHEME_BOLT = "bolt"
43 URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE = "bolt+ssc"
44 URI_SCHEME_BOLT_SECURE = "bolt+s"
45
46 URI_SCHEME_NEO4J = "neo4j"
47 URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE = "neo4j+ssc"
48 URI_SCHEME_NEO4J_SECURE = "neo4j+s"
49
50 URI_SCHEME_BOLT_ROUTING = "bolt+routing"
51
52 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES = "TRUST_SYSTEM_CA_SIGNED_CERTIFICATES" # Default
53 TRUST_ALL_CERTIFICATES = "TRUST_ALL_CERTIFICATES"
54
55 SYSTEM_DATABASE = "system"
56 DEFAULT_DATABASE = None # Must be a non string hashable value
57
58
59 # TODO: This class is not tested
60 class Auth:
61 """ Container for auth details.
62
63 :param scheme: specifies the type of authentication, examples: "basic", "kerberos"
64 :type scheme: str
65 :param principal: specifies who is being authenticated
66 :type principal: str
67 :param credentials: authenticates the principal
68 :type credentials: str
69 :param realm: specifies the authentication provider
70 :type realm: str
71 :param parameters: extra key word parameters passed along to the authentication provider
72 :type parameters: str
73 """
74
75 #: By default we should not send any realm
76 realm = None
77
78 def __init__(self, scheme, principal, credentials, realm=None, **parameters):
79 self.scheme = scheme
80 self.principal = principal
81 self.credentials = credentials
82 if realm:
83 self.realm = realm
84 if parameters:
85 self.parameters = parameters
86
87
88 # For backwards compatibility
89 AuthToken = Auth
90
91
92 def basic_auth(user, password, realm=None):
93 """ Generate a basic auth token for a given user and password.
94
95 This will set the scheme to "basic" for the auth token.
96
97 :param user: user name, this will set the principal
98 :param password: current password, this will set the credentials
99 :param realm: specifies the authentication provider
100
101 :return: auth token for use with :meth:`GraphDatabase.driver`
102 :rtype: :class:`neo4j.Auth`
103 """
104 return Auth("basic", user, password, realm)
105
106
107 def kerberos_auth(base64_encoded_ticket):
108 """ Generate a kerberos auth token with the base64 encoded ticket
109
110 This will set the scheme to "kerberos" for the auth token.
111
112 :param base64_encoded_ticket: a base64 encoded service ticket, this will set the credentials
113
114 :return: auth token for use with :meth:`GraphDatabase.driver`
115 :rtype: :class:`neo4j.Auth`
116 """
117 return Auth("kerberos", "", base64_encoded_ticket)
118
119
120 def custom_auth(principal, credentials, realm, scheme, **parameters):
121 """ Generate a custom auth token.
122
123 :param principal: specifies who is being authenticated
124 :param credentials: authenticates the principal
125 :param realm: specifies the authentication provider
126 :param scheme: specifies the type of authentication
127 :param parameters: extra key word parameters passed along to the authentication provider
128
129 :return: auth token for use with :meth:`GraphDatabase.driver`
130 :rtype: :class:`neo4j.Auth`
131 """
132 return Auth(scheme, principal, credentials, realm, **parameters)
133
134
135 class Bookmark:
136 """A Bookmark object contains an immutable list of bookmark string values.
137
138 :param values: ASCII string values
139 """
140
141 def __init__(self, *values):
142 if values:
143 bookmarks = []
144 for ix in values:
145 try:
146 if ix:
147 ix.encode("ascii")
148 bookmarks.append(ix)
149 except UnicodeEncodeError as e:
150 raise ValueError("The value {} is not ASCII".format(ix))
151 self._values = frozenset(bookmarks)
152 else:
153 self._values = frozenset()
154
155 def __repr__(self):
156 """
157 :return: repr string with sorted values
158 """
159 return "<Bookmark values={{{}}}>".format(", ".join(["'{}'".format(ix) for ix in sorted(self._values)]))
160
161 def __bool__(self):
162 return bool(self._values)
163
164 @property
165 def values(self):
166 """
167 :return: immutable list of bookmark string values
168 :rtype: frozenset
169 """
170 return self._values
171
172
173 class ServerInfo:
174
175 def __init__(self, address, protocol_version):
176 self.address = address
177 self.protocol_version = protocol_version
178 self.metadata = {}
179
180 @property
181 def agent(self):
182 """The server agent string the server responded with.
183
184 :return: Server agent string
185 :rtype: str
186 """
187 # Example "Neo4j/4.0.5"
188 # Example "Neo4j/4"
189 return self.metadata.get("server")
190
191 def version_info(self):
192 """Return the server version if available.
193
194 :return: Server Version or None
195 :rtype: tuple
196 """
197 if not self.agent:
198 return None
199 # Note: Confirm that the server agent string begins with "Neo4j/" and fail gracefully if not.
200 # This is intended to help prevent drivers working for non-genuine Neo4j instances.
201
202 prefix, _, value = self.agent.partition("/")
203 try:
204 assert prefix in ["Neo4j"]
205 except AssertionError:
206 raise DriverError("Server name does not start with Neo4j/")
207
208 try:
209 if self.protocol_version >= (4, 0):
210 return self.protocol_version
211 except TypeError:
212 pass
213
214 value = value.replace("-", ".").split(".")
215 for i, v in enumerate(value):
216 try:
217 value[i] = int(v)
218 except ValueError:
219 pass
220 return tuple(value)
221
222 def _update_metadata(self, metadata):
223 """Internal, update the metadata and perform check that the prefix is whitelisted by calling self.version()
224
225 :param metadata: metadata from the server
226 :type metadata: dict
227 """
228 self.metadata.update(metadata)
229 _ = self.version_info()
230
231
232 class Version(tuple):
233
234 def __new__(cls, *v):
235 return super().__new__(cls, v)
236
237 def __repr__(self):
238 return "{}{}".format(self.__class__.__name__, super().__repr__())
239
240 def __str__(self):
241 return ".".join(map(str, self))
242
243 def to_bytes(self):
244 b = bytearray(4)
245 for i, v in enumerate(self):
246 if not 0 <= i < 2:
247 raise ValueError("Too many version components")
248 if not 0 <= v < 256:
249 raise ValueError("Version component {} is out of range".format(v))
250 b[-i - 1] = v
251 return bytes(b)
252
253 @classmethod
254 def from_bytes(cls, b):
255 b = bytearray(b)
256 if len(b) != 4:
257 raise ValueError("Byte representation must be exactly four bytes")
258 if b[0] != 0 or b[1] != 0:
259 raise ValueError("First two bytes must contain zero")
260 return Version(b[-1], b[-2])
261
262
263 def parse_neo4j_uri(uri):
264 parsed = urlparse(uri)
265
266 if parsed.username:
267 raise ConfigurationError("Username is not supported in the URI")
268
269 if parsed.password:
270 raise ConfigurationError("Password is not supported in the URI")
271
272 if parsed.scheme == URI_SCHEME_BOLT_ROUTING:
273 raise ConfigurationError("Uri scheme {!r} have been renamed. Use {!r}".format(parsed.scheme, URI_SCHEME_NEO4J))
274 elif parsed.scheme == URI_SCHEME_BOLT:
275 driver_type = DRIVER_BOLT
276 security_type = SECURITY_TYPE_NOT_SECURE
277 elif parsed.scheme == URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE:
278 driver_type = DRIVER_BOLT
279 security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
280 elif parsed.scheme == URI_SCHEME_BOLT_SECURE:
281 driver_type = DRIVER_BOLT
282 security_type = SECURITY_TYPE_SECURE
283 elif parsed.scheme == URI_SCHEME_NEO4J:
284 driver_type = DRIVER_NEO4j
285 security_type = SECURITY_TYPE_NOT_SECURE
286 elif parsed.scheme == URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE:
287 driver_type = DRIVER_NEO4j
288 security_type = SECURITY_TYPE_SELF_SIGNED_CERTIFICATE
289 elif parsed.scheme == URI_SCHEME_NEO4J_SECURE:
290 driver_type = DRIVER_NEO4j
291 security_type = SECURITY_TYPE_SECURE
292 else:
293 raise ConfigurationError("URI scheme {!r} is not supported. Supported URI schemes are {}. Examples: bolt://host[:port] or neo4j://host[:port][?routing_context]".format(
294 parsed.scheme,
295 [
296 URI_SCHEME_BOLT,
297 URI_SCHEME_BOLT_SELF_SIGNED_CERTIFICATE,
298 URI_SCHEME_BOLT_SECURE,
299 URI_SCHEME_NEO4J,
300 URI_SCHEME_NEO4J_SELF_SIGNED_CERTIFICATE,
301 URI_SCHEME_NEO4J_SECURE
302 ]
303 ))
304
305 return driver_type, security_type, parsed
306
307
308 def check_access_mode(access_mode):
309 if access_mode is None:
310 return WRITE_ACCESS
311 if access_mode not in (READ_ACCESS, WRITE_ACCESS):
312 msg = "Unsupported access mode {}".format(access_mode)
313 raise ConfigurationError(msg)
314
315 return access_mode
316
317
318 def parse_routing_context(query):
319 """ Parse the query portion of a URI to generate a routing context dictionary.
320 """
321 if not query:
322 return {}
323
324 context = {}
325 parameters = parse_qs(query, True)
326 for key in parameters:
327 value_list = parameters[key]
328 if len(value_list) != 1:
329 raise ConfigurationError("Duplicated query parameters with key '%s', value '%s' found in query string '%s'" % (key, value_list, query))
330 value = value_list[0]
331 if not value:
332 raise ConfigurationError("Invalid parameters:'%s=%s' in query string '%s'." % (key, value, query))
333 context[key] = value
334
335 return context
+0
-158
neo4j/compat/__init__.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """
22 This module provides compatibility functions between different versions
23 and flavours of Python. It is separate for clarity and deliberately
24 excluded from test coverage.
25 """
26
27
28 map_type = type(map(str, range(0)))
29
30 # Workaround for Python 2/3 type differences
31 try:
32 unicode
33 except NameError:
34 # Python 3
35
36 integer = int
37 string = str
38 unicode = str
39 unichr = chr
40
41 def bstr(x):
42 if isinstance(x, bytes):
43 return x
44 elif isinstance(x, str):
45 return x.encode("utf-8")
46 else:
47 return str(x).encode("utf-8")
48
49 def ustr(x):
50 if isinstance(x, bytes):
51 return x.decode("utf-8")
52 elif isinstance(x, str):
53 return x
54 else:
55 return str(x)
56
57 xstr = ustr
58
59 def memoryview_at(view, index):
60 return view[index]
61
62 else:
63 # Python 2
64
65 integer = (int, long)
66 string = (str, unicode)
67 unicode = unicode
68 unichr = unichr
69
70 def bstr(x):
71 if isinstance(x, str):
72 return x
73 elif isinstance(x, unicode):
74 return x.encode("utf-8")
75 else:
76 return unicode(x).encode("utf-8")
77
78 def ustr(x):
79 if isinstance(x, str):
80 return x.decode("utf-8")
81 elif isinstance(x, unicode):
82 return x
83 else:
84 return unicode(x)
85
86 xstr = bstr
87
88 def memoryview_at(view, index):
89 return ord(view[index])
90
91 try:
92 from multiprocessing import Array, Process
93 except ImportError:
94 # Workaround for Jython
95
96 from array import array
97 from threading import Thread as Process
98
99 def Array(typecode, size):
100 return array(typecode, [0] * size)
101
102
103 # Obtain a performance timer - this varies by platform and
104 # Jython support is even more tricky as the standard timer
105 # does not support nanoseconds. The combination below
106 # works with Python 2, Python 3 and Jython.
107 try:
108 from java.lang.System import nanoTime
109 except ImportError:
110 JYTHON = False
111
112 try:
113 from time import perf_counter
114 except ImportError:
115 from time import time as perf_counter
116 else:
117 JYTHON = True
118
119 def perf_counter():
120 return nanoTime() / 1000000000
121
122
123 # Using or importing the ABCs from 'collections' instead of from
124 # 'collections.abc' is deprecated, and in 3.8 it will stop working
125 try:
126 from collections.abc import Sequence, Mapping
127 except ImportError:
128 from collections import Sequence, Mapping
129
130
131 # The location of urlparse varies between Python 2 and 3
132 try:
133 from urllib.parse import urlparse, parse_qs
134 except ImportError:
135 from urlparse import urlparse, parse_qs
136
137
138 def deprecated(message):
139 """ Decorator for deprecating functions and methods.
140
141 ::
142
143 @deprecated("'foo' has been deprecated in favour of 'bar'")
144 def foo(x):
145 pass
146
147 """
148 def f__(f):
149 def f_(*args, **kwargs):
150 from warnings import warn
151 warn(message, category=DeprecationWarning, stacklevel=2)
152 return f(*args, **kwargs)
153 f_.__name__ = f.__name__
154 f_.__doc__ = f.__doc__
155 f_.__dict__.update(f.__dict__)
156 return f_
157 return f__
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from abc import ABCMeta
22 from collections.abc import Mapping
23 from warnings import warn
24
25 from neo4j.meta import get_user_agent
26
27 from neo4j.api import (
28 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
29 TRUST_ALL_CERTIFICATES,
30 WRITE_ACCESS,
31 DEFAULT_DATABASE,
32 )
33 from neo4j.exceptions import (
34 ConfigurationError,
35 )
36
37
38 def iter_items(iterable):
39 """ Iterate through all items (key-value pairs) within an iterable
40 dictionary-like object. If the object has a `keys` method, this is
41 used along with `__getitem__` to yield each pair in turn. If no
42 `keys` method exists, each iterable element is assumed to be a
43 2-tuple of key and value.
44 """
45 if hasattr(iterable, "keys"):
46 for key in iterable.keys():
47 yield key, iterable[key]
48 else:
49 for key, value in iterable:
50 yield key, value
51
52
53 class DeprecatedAlias:
54
55 def __init__(self, new):
56 self.new = new
57
58
59 class ConfigType(ABCMeta):
60
61 def __new__(mcs, name, bases, attributes):
62 fields = []
63 deprecated_aliases = {}
64
65 for base in bases:
66 if type(base) is mcs:
67 fields += base.keys()
68 deprecated_aliases.update(base._deprecated_aliases())
69
70 for k, v in attributes.items():
71 if isinstance(v, DeprecatedAlias):
72 deprecated_aliases[k] = v.new
73 elif not k.startswith("_") and not callable(v):
74 fields.append(k)
75
76 def keys(_):
77 return fields
78
79 def _deprecated_aliases(_):
80 return deprecated_aliases
81
82 def _deprecated_keys(_):
83 return list(deprecated_aliases)
84
85 def _get_new(_, key):
86 return deprecated_aliases.get(key)
87
88 attributes.setdefault("keys", classmethod(keys))
89 attributes.setdefault("_deprecated_aliases", classmethod(_deprecated_aliases))
90 attributes.setdefault("_deprecated_keys", classmethod(_deprecated_keys))
91 attributes.setdefault("_get_new", classmethod(_get_new))
92
93 return super(ConfigType, mcs).__new__(mcs, name, bases,
94 {k: v for k, v in attributes.items()
95 if k not in deprecated_aliases})
96
97
98 class Config(Mapping, metaclass=ConfigType):
99 """ Base class for all configuration containers.
100 """
101
102 @staticmethod
103 def consume_chain(data, *config_classes):
104 values = []
105 for config_class in config_classes:
106 if not issubclass(config_class, Config):
107 raise TypeError("%r is not a Config subclass" % config_class)
108 values.append(config_class._consume(data))
109 if data:
110 raise ConfigurationError("Unexpected config keys: %s" % ", ".join(data.keys()))
111 return values
112
113 @classmethod
114 def consume(cls, data):
115 config, = cls.consume_chain(data, cls)
116 return config
117
118 @classmethod
119 def _consume(cls, data):
120 config = {}
121 if data:
122 for key in list(cls.keys()) + list(cls._deprecated_keys()):
123 try:
124 value = data.pop(key)
125 except KeyError:
126 pass
127 else:
128 config[key] = value
129 return cls(config)
130
131 def __update(self, data):
132 data_dict = dict(iter_items(data))
133
134 def set_attr(k, v):
135 if k in self.keys():
136 setattr(self, k, v)
137 elif k in self._deprecated_keys():
138 k0 = self._get_new(k)
139 if k0 in data_dict:
140 raise ValueError("Cannot specify both '{}' and '{}' in config".format(k0, k))
141 warn("The '{}' config key is deprecated, please use '{}' instead".format(k, k0))
142 set_attr(k0, v)
143 else:
144 raise AttributeError(k)
145
146 for key, value in data_dict.items():
147 if value is not None:
148 set_attr(key, value)
149
150 def __init__(self, *args, **kwargs):
151 for arg in args:
152 self.__update(arg)
153 self.__update(kwargs)
154
155 def __repr__(self):
156 attrs = []
157 for key in self:
158 attrs.append(" %s=%r" % (key, getattr(self, key)))
159 return "<%s%s>" % (self.__class__.__name__, "".join(attrs))
160
161 def __len__(self):
162 return len(self.keys())
163
164 def __getitem__(self, key):
165 return getattr(self, key)
166
167 def __iter__(self):
168 return iter(self.keys())
169
170
171 class PoolConfig(Config):
172 """ Connection pool configuration.
173 """
174
175 #: Max Connection Lifetime
176 max_connection_lifetime = 3600 # seconds
177 # The maximum duration the driver will keep a connection for before being removed from the pool.
178
179 #: Max Connection Pool Size
180 max_connection_pool_size = 100
181 # The maximum total number of connections allowed, per host (i.e. cluster nodes), to be managed by the connection pool.
182
183 #: Connection Timeout
184 connection_timeout = 30.0 # seconds
185 # The maximum amount of time to wait for a TCP connection to be established.
186
187 #: Trust
188 trust = TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
189 # Specify how to determine the authenticity of encryption certificates provided by the Neo4j instance on connection.
190
191 #: Custom Resolver
192 resolver = None
193 # Custom resolver function, returning list of resolved addresses.
194
195 #: Encrypted
196 encrypted = False
197 # Specify whether to use an encrypted connection between the driver and server.
198
199 #: User Agent (Python Driver Specific)
200 user_agent = get_user_agent()
201 # Specify the client agent name.
202
203 #: Protocol Version (Python Driver Specific)
204 protocol_version = None # Version(4, 0)
205 # Specify a specific Bolt Protocol Version
206
207 #: Initial Connection Pool Size (Python Driver Specific)
208 init_size = 1 # The other drivers do not seed from the start.
209 # This will seed the pool with the specified number of connections.
210
211 #: Socket Keep Alive (Python and .NET Driver Specific)
212 keep_alive = True
213 # Specify whether TCP keep-alive should be enabled.
214
215 def get_ssl_context(self):
216 if not self.encrypted:
217 return None
218
219 import ssl
220
221 ssl_context = None
222
223 # SSL stands for Secure Sockets Layer and was originally created by Netscape.
224 # SSLv2 and SSLv3 are the 2 versions of this protocol (SSLv1 was never publicly released).
225 # After SSLv3, SSL was renamed to TLS.
226 # TLS stands for Transport Layer Security and started with TLSv1.0 which is an upgraded version of SSLv3.
227
228 # SSLv2 - (Disabled)
229 # SSLv3 - (Disabled)
230 # TLS 1.0 - Released in 1999, published as RFC 2246. (Disabled)
231 # TLS 1.1 - Released in 2006, published as RFC 4346. (Disabled)
232 # TLS 1.2 - Released in 2008, published as RFC 5246.
233
234 try:
235 # python 3.6+
236 # https://docs.python.org/3.6/library/ssl.html#ssl.PROTOCOL_TLS_CLIENT
237 ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
238
239 # For recommended security options see
240 # https://docs.python.org/3.6/library/ssl.html#protocol-versions
241 ssl_context.options |= ssl.OP_NO_TLSv1 # Python 3.2
242 ssl_context.options |= ssl.OP_NO_TLSv1_1 # Python 3.4
243
244 except AttributeError:
245 # python 3.5
246 # https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLS
247 ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
248
249 # For recommended security options see
250 # https://docs.python.org/3.5/library/ssl.html#protocol-versions
251 ssl_context.options |= ssl.OP_NO_SSLv2 # Python 3.2
252 ssl_context.options |= ssl.OP_NO_SSLv3 # Python 3.2
253 ssl_context.options |= ssl.OP_NO_TLSv1 # Python 3.2
254 ssl_context.options |= ssl.OP_NO_TLSv1_1 # Python 3.4
255
256 ssl_context.verify_mode = ssl.CERT_REQUIRED # https://docs.python.org/3.5/library/ssl.html#ssl.SSLContext.verify_mode
257 ssl_context.check_hostname = True # https://docs.python.org/3.5/library/ssl.html#ssl.SSLContext.check_hostname
258
259 if self.trust == TRUST_ALL_CERTIFICATES:
260 ssl_context.check_hostname = False
261 ssl_context.verify_mode = ssl.CERT_NONE # https://docs.python.org/3.5/library/ssl.html#ssl.CERT_NONE
262
263 # Must be load_default_certs, not set_default_verify_paths to work
264 # on Windows with system CAs.
265 ssl_context.load_default_certs()
266
267 return ssl_context
268
269
270 class WorkspaceConfig(Config):
271 """ WorkSpace configuration.
272 """
273
274 #: Connection Acquisition Timeout
275 connection_acquisition_timeout = 60.0 # seconds
276 # The maximum amount of time a session will wait when requesting a connection from the connection pool.
277 # Since the process of acquiring a connection may involve creating a new connection, ensure that the value
278 # of this configuration is higher than the configured Connection Timeout.
279
280 #: Max Transaction Retry Time
281 max_transaction_retry_time = 30.0 # seconds
282 # The maximum amount of time that a managed transaction will retry before failing.
283
284 #: Initial Retry Delay
285 initial_retry_delay = 1.0 # seconds
286
287 #: Retry Delay Multiplier
288 retry_delay_multiplier = 2.0 # seconds
289
290 #: Retry Delay Jitter Factor
291 retry_delay_jitter_factor = 0.2 # seconds
292
293 #: Database Name
294 database = DEFAULT_DATABASE
295 # Name of the database to query.
296 # Note: The default database can be set on the Neo4j instance settings.
297
298 #: Fetch Size
299 fetch_size = 1000
300
301
302 class SessionConfig(WorkspaceConfig):
303 """ Session configuration.
304 """
305
306 #: Bookmarks
307 bookmarks = ()
308
309 #: Default AccessMode
310 default_access_mode = WRITE_ACCESS
311 # access_mode = DeprecatedAlias("default_access_mode")
312
313
314 class TransactionConfig(Config):
315 """ Transaction configuration. This is internal for now.
316
317 neo4j.session.begin_transaction
318 neo4j.Query
319 neo4j.unit_of_work
320
321 are both using the same settings.
322 """
323 #: Metadata
324 metadata = None # dictionary
325
326 #: Timeout
327 timeout = None # seconds
328
329
330 class RoutingConfig(Config):
331 """ Neo4jDriver routing settings. This is internal for now.
332 """
333
334 #: Routing Table Purge_Delay
335 routing_table_purge_delay = 30.0 # seconds
336 # The TTL + routing_table_purge_delay should be used to check if the database routing table should be removed.
337
338 #: Max Routing Failures
339 # max_routing_failures = 1
340
341 #: Retry Timeout Delay
342 # retry_timeout_delay = 5.0 # seconds
+0
-74
neo4j/config.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from sys import platform, version_info
22
23 from neo4j.meta import version
24
25 # Auth
26 TRUST_ON_FIRST_USE = 0 # Deprecated
27 TRUST_SIGNED_CERTIFICATES = 1 # Deprecated
28 TRUST_ALL_CERTIFICATES = 2
29 TRUST_CUSTOM_CA_SIGNED_CERTIFICATES = 3
30 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES = 4
31 TRUST_DEFAULT = TRUST_ALL_CERTIFICATES
32
33 # Connection Pool Management
34 INFINITE = -1
35 DEFAULT_MAX_CONNECTION_LIFETIME = 3600 # 1h
36 DEFAULT_MAX_CONNECTION_POOL_SIZE = 100
37 DEFAULT_CONNECTION_TIMEOUT = 30.0 # 30s
38
39 # Connection Settings
40 DEFAULT_CONNECTION_ACQUISITION_TIMEOUT = 60 # 1m
41
42 # Routing settings
43 DEFAULT_MAX_RETRY_TIME = 30.0 # 30s
44
45 LOAD_BALANCING_STRATEGY_LEAST_CONNECTED = 0
46 LOAD_BALANCING_STRATEGY_ROUND_ROBIN = 1
47 DEFAULT_LOAD_BALANCING_STRATEGY = LOAD_BALANCING_STRATEGY_LEAST_CONNECTED
48
49 # Client name
50 DEFAULT_USER_AGENT = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})".format(
51 *((version,) + tuple(version_info) + (platform,)))
52
53 default_config = {
54 "auth": None, # provide your own authentication token such as {"username", "password"}
55 "encrypted": None, # default to have encryption enabled if ssl is available on your platform
56 "trust": TRUST_DEFAULT,
57 "der_encoded_server_certificate": None,
58
59 "user_agent": DEFAULT_USER_AGENT,
60
61 # Connection pool management
62 "max_connection_lifetime": DEFAULT_MAX_CONNECTION_LIFETIME,
63 "max_connection_pool_size": DEFAULT_MAX_CONNECTION_POOL_SIZE,
64 "connection_acquisition_timeout": DEFAULT_CONNECTION_ACQUISITION_TIMEOUT,
65
66 # Connection settings:
67 "connection_timeout": DEFAULT_CONNECTION_TIMEOUT,
68 "keep_alive": True,
69
70 # Routing settings:
71 "max_retry_time": DEFAULT_MAX_RETRY_TIME,
72 "load_balancing_strategy": DEFAULT_LOAD_BALANCING_STRATEGY,
73 }
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from abc import ABCMeta, abstractmethod
22 from collections.abc import Sequence, Set, Mapping
23 from datetime import date, time, datetime, timedelta
24 from functools import reduce
25 from operator import xor as xor_operator
26
27 from neo4j.conf import iter_items
28 from neo4j.graph import Graph, Node, Relationship, Path
29 from neo4j.packstream import INT64_MIN, INT64_MAX, Structure
30 from neo4j.spatial import Point, hydrate_point, dehydrate_point
31 from neo4j.time import Date, Time, DateTime, Duration
32 from neo4j.time.hydration import (
33 hydrate_date, dehydrate_date,
34 hydrate_time, dehydrate_time,
35 hydrate_datetime, dehydrate_datetime,
36 hydrate_duration, dehydrate_duration, dehydrate_timedelta,
37 )
38
39
40 map_type = type(map(str, range(0)))
41
42
43 class Record(tuple, Mapping):
44 """ A :class:`.Record` is an immutable ordered collection of key-value
45 pairs. It is generally closer to a :py:class:`namedtuple` than to a
46 :py:class:`OrderedDict` inasmuch as iteration of the collection will
47 yield values rather than keys.
48 """
49
50 __keys = None
51
52 def __new__(cls, iterable=()):
53 keys = []
54 values = []
55 for key, value in iter_items(iterable):
56 keys.append(key)
57 values.append(value)
58 inst = tuple.__new__(cls, values)
59 inst.__keys = tuple(keys)
60 return inst
61
62 def __repr__(self):
63 return "<%s %s>" % (self.__class__.__name__,
64 " ".join("%s=%r" % (field, self[i]) for i, field in enumerate(self.__keys)))
65
66 def __eq__(self, other):
67 """ In order to be flexible regarding comparison, the equality rules
68 for a record permit comparison with any other Sequence or Mapping.
69
70 :param other:
71 :return:
72 """
73 compare_as_sequence = isinstance(other, Sequence)
74 compare_as_mapping = isinstance(other, Mapping)
75 if compare_as_sequence and compare_as_mapping:
76 return list(self) == list(other) and dict(self) == dict(other)
77 elif compare_as_sequence:
78 return list(self) == list(other)
79 elif compare_as_mapping:
80 return dict(self) == dict(other)
81 else:
82 return False
83
84 def __ne__(self, other):
85 return not self.__eq__(other)
86
87 def __hash__(self):
88 return reduce(xor_operator, map(hash, self.items()))
89
90 def __getitem__(self, key):
91 if isinstance(key, slice):
92 keys = self.__keys[key]
93 values = super(Record, self).__getitem__(key)
94 return self.__class__(zip(keys, values))
95 try:
96 index = self.index(key)
97 except IndexError:
98 return None
99 else:
100 return super(Record, self).__getitem__(index)
101
102 def __getslice__(self, start, stop):
103 key = slice(start, stop)
104 keys = self.__keys[key]
105 values = tuple(self)[key]
106 return self.__class__(zip(keys, values))
107
108 def get(self, key, default=None):
109 """ Obtain a value from the record by key, returning a default
110 value if the key does not exist.
111
112 :param key: a key
113 :param default: default value
114 :return: a value
115 """
116 try:
117 index = self.__keys.index(str(key))
118 except ValueError:
119 return default
120 if 0 <= index < len(self):
121 return super(Record, self).__getitem__(index)
122 else:
123 return default
124
125 def index(self, key):
126 """ Return the index of the given item.
127
128 :param key: a key
129 :return: index
130 :rtype: int
131 """
132 if isinstance(key, int):
133 if 0 <= key < len(self.__keys):
134 return key
135 raise IndexError(key)
136 elif isinstance(key, str):
137 try:
138 return self.__keys.index(key)
139 except ValueError:
140 raise KeyError(key)
141 else:
142 raise TypeError(key)
143
144 def value(self, key=0, default=None):
145 """ Obtain a single value from the record by index or key. If no
146 index or key is specified, the first value is returned. If the
147 specified item does not exist, the default value is returned.
148
149 :param key: an index or key
150 :param default: default value
151 :return: a single value
152 """
153 try:
154 index = self.index(key)
155 except (IndexError, KeyError):
156 return default
157 else:
158 return self[index]
159
160 def keys(self):
161 """ Return the keys of the record.
162
163 :return: list of key names
164 """
165 return list(self.__keys)
166
167 def values(self, *keys):
168 """ Return the values of the record, optionally filtering to
169 include only certain values by index or key.
170
171 :param keys: indexes or keys of the items to include; if none
172 are provided, all values will be included
173 :return: list of values
174 :rtype: list
175 """
176 if keys:
177 d = []
178 for key in keys:
179 try:
180 i = self.index(key)
181 except KeyError:
182 d.append(None)
183 else:
184 d.append(self[i])
185 return d
186 return list(self)
187
188 def items(self, *keys):
189 """ Return the fields of the record as a list of key and value tuples
190
191 :return: a list of value tuples
192 :rtype: list
193 """
194 if keys:
195 d = []
196 for key in keys:
197 try:
198 i = self.index(key)
199 except KeyError:
200 d.append((key, None))
201 else:
202 d.append((self.__keys[i], self[i]))
203 return d
204 return list((self.__keys[i], super(Record, self).__getitem__(i)) for i in range(len(self)))
205
206 def data(self, *keys):
207 """ Return the keys and values of this record as a dictionary,
208 optionally including only certain values by index or key. Keys
209 provided in the items that are not in the record will be
210 inserted with a value of :const:`None`; indexes provided
211 that are out of bounds will trigger an :exc:`IndexError`.
212
213 :param keys: indexes or keys of the items to include; if none
214 are provided, all values will be included
215 :return: dictionary of values, keyed by field name
216 :raises: :exc:`IndexError` if an out-of-bounds index is specified
217 """
218 return RecordExporter().transform(dict(self.items(*keys)))
219
220
221 class DataTransformer(metaclass=ABCMeta):
222 """ Abstract base class for transforming data from one form into
223 another.
224 """
225
226 @abstractmethod
227 def transform(self, x):
228 """ Transform a value, or collection of values.
229
230 :param x: input value
231 :return: output value
232 """
233
234
235 class RecordExporter(DataTransformer):
236 """ Transformer class used by the :meth:`.Record.data` method.
237 """
238
239 def transform(self, x):
240 if isinstance(x, Node):
241 return self.transform(dict(x))
242 elif isinstance(x, Relationship):
243 return (self.transform(dict(x.start_node)),
244 x.__class__.__name__,
245 self.transform(dict(x.end_node)))
246 elif isinstance(x, Path):
247 path = [self.transform(x.start_node)]
248 for i, relationship in enumerate(x.relationships):
249 path.append(self.transform(relationship.__class__.__name__))
250 path.append(self.transform(x.nodes[i + 1]))
251 return path
252 elif isinstance(x, str):
253 return x
254 elif isinstance(x, Sequence):
255 t = type(x)
256 return t(map(self.transform, x))
257 elif isinstance(x, Set):
258 t = type(x)
259 return t(map(self.transform, x))
260 elif isinstance(x, Mapping):
261 t = type(x)
262 return t((k, self.transform(v)) for k, v in x.items())
263 else:
264 return x
265
266
267 class DataHydrator:
268 # TODO: extend DataTransformer
269
270 def __init__(self):
271 super(DataHydrator, self).__init__()
272 self.graph = Graph()
273 self.graph_hydrator = Graph.Hydrator(self.graph)
274 self.hydration_functions = {
275 b"N": self.graph_hydrator.hydrate_node,
276 b"R": self.graph_hydrator.hydrate_relationship,
277 b"r": self.graph_hydrator.hydrate_unbound_relationship,
278 b"P": self.graph_hydrator.hydrate_path,
279 b"X": hydrate_point,
280 b"Y": hydrate_point,
281 b"D": hydrate_date,
282 b"T": hydrate_time, # time zone offset
283 b"t": hydrate_time, # no time zone
284 b"F": hydrate_datetime, # time zone offset
285 b"f": hydrate_datetime, # time zone name
286 b"d": hydrate_datetime, # no time zone
287 b"E": hydrate_duration,
288 }
289
290 def hydrate(self, values):
291 """ Convert PackStream values into native values.
292 """
293
294 def hydrate_(obj):
295 if isinstance(obj, Structure):
296 try:
297 f = self.hydration_functions[obj.tag]
298 except KeyError:
299 # If we don't recognise the structure
300 # type, just return it as-is
301 return obj
302 else:
303 return f(*map(hydrate_, obj.fields))
304 elif isinstance(obj, list):
305 return list(map(hydrate_, obj))
306 elif isinstance(obj, dict):
307 return {key: hydrate_(value) for key, value in obj.items()}
308 else:
309 return obj
310
311 return tuple(map(hydrate_, values))
312
313 def hydrate_records(self, keys, record_values):
314 for values in record_values:
315 yield Record(zip(keys, self.hydrate(values)))
316
317
318 class DataDehydrator:
319 # TODO: extend DataTransformer
320
321 @classmethod
322 def fix_parameters(cls, parameters):
323 if not parameters:
324 return {}
325 dehydrator = cls()
326 try:
327 dehydrated, = dehydrator.dehydrate([parameters])
328 except TypeError as error:
329 value = error.args[0]
330 raise TypeError("Parameters of type {} are not supported".format(type(value).__name__))
331 else:
332 return dehydrated
333
334 def __init__(self):
335 self.dehydration_functions = {}
336 self.dehydration_functions.update({
337 Point: dehydrate_point,
338 Date: dehydrate_date,
339 date: dehydrate_date,
340 Time: dehydrate_time,
341 time: dehydrate_time,
342 DateTime: dehydrate_datetime,
343 datetime: dehydrate_datetime,
344 Duration: dehydrate_duration,
345 timedelta: dehydrate_timedelta,
346 })
347 # Allow dehydration from any direct Point subclass
348 self.dehydration_functions.update({cls: dehydrate_point for cls in Point.__subclasses__()})
349
350 def dehydrate(self, values):
351 """ Convert native values into PackStream values.
352 """
353
354 def dehydrate_(obj):
355 try:
356 f = self.dehydration_functions[type(obj)]
357 except KeyError:
358 pass
359 else:
360 return f(obj)
361 if obj is None:
362 return None
363 elif isinstance(obj, bool):
364 return obj
365 elif isinstance(obj, int):
366 if INT64_MIN <= obj <= INT64_MAX:
367 return obj
368 raise ValueError("Integer out of bounds (64-bit signed "
369 "integer values only)")
370 elif isinstance(obj, float):
371 return obj
372 elif isinstance(obj, str):
373 return obj
374 elif isinstance(obj, (bytes, bytearray)):
375 # order is important here - bytes must be checked after str
376 return obj
377 elif isinstance(obj, (list, map_type)):
378 return list(map(dehydrate_, obj))
379 elif isinstance(obj, dict):
380 if any(not isinstance(key, str) for key in obj.keys()):
381 raise TypeError("Non-string dictionary keys are "
382 "not supported")
383 return {key: dehydrate_(value) for key, value in obj.items()}
384 else:
385 raise TypeError(obj)
386
387 return tuple(map(dehydrate_, values))
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from logging import CRITICAL, ERROR, WARNING, INFO, DEBUG, Formatter, StreamHandler, getLogger
22 from sys import stderr
23
24
25 class ColourFormatter(Formatter):
26 """ Colour formatter for pretty log output.
27 """
28
29 def format(self, record):
30 s = super(ColourFormatter, self).format(record)
31 if record.levelno == CRITICAL:
32 return "\x1b[31;1m%s\x1b[0m" % s # bright red
33 elif record.levelno == ERROR:
34 return "\x1b[33;1m%s\x1b[0m" % s # bright yellow
35 elif record.levelno == WARNING:
36 return "\x1b[33m%s\x1b[0m" % s # yellow
37 elif record.levelno == INFO:
38 return "\x1b[37m%s\x1b[0m" % s # white
39 elif record.levelno == DEBUG:
40 return "\x1b[36m%s\x1b[0m" % s # cyan
41 else:
42 return s
43
44
45 class Watcher:
46 """ Log watcher for monitoring driver and protocol activity.
47 """
48
49 handlers = {}
50
51 def __init__(self, *logger_names):
52 super(Watcher, self).__init__()
53 self.logger_names = logger_names
54 self.loggers = [getLogger(name) for name in self.logger_names]
55 self.formatter = ColourFormatter("%(asctime)s %(message)s")
56
57 def __enter__(self):
58 self.watch()
59 return self
60
61 def __exit__(self, exc_type, exc_val, exc_tb):
62 self.stop()
63
64 def watch(self, level=DEBUG, out=stderr):
65 self.stop()
66 handler = StreamHandler(out)
67 handler.setFormatter(self.formatter)
68 for logger in self. loggers:
69 self.handlers[logger.name] = handler
70 logger.addHandler(handler)
71 logger.setLevel(level)
72
73 def stop(self):
74 try:
75 for logger in self.loggers:
76 logger.removeHandler(self.handlers[logger.name])
77 except KeyError:
78 pass
79
80
81 def watch(*logger_names, level=DEBUG, out=stderr):
82 """ Quick wrapper for using the Watcher.
83
84 :param logger_name: name of logger to watch
85 :param level: minimum log level to show (default DEBUG)
86 :param out: where to send output (default stderr)
87 :return: Watcher instance
88 """
89 watcher = Watcher(*logger_names)
90 watcher.watch(level, out)
91 return watcher
00 #!/usr/bin/env python
11 # -*- encoding: utf-8 -*-
22
3 # Copyright (c) 2002-2019 "Neo4j,"
3 # Copyright (c) 2002-2020 "Neo4j,"
44 # Neo4j Sweden AB [http://neo4j.com]
55 #
66 # This file is part of Neo4j.
2020
2121 """
2222 This module contains the core driver exceptions.
23
24 Driver API Errors
25 =================
26 + Neo4jError
27 + ClientError
28 + CypherSyntaxError
29 + CypherTypeError
30 + ConstraintError
31 + AuthError
32 + Forbidden
33 + ForbiddenOnReadOnlyDatabase
34 + NotALeader
35 + DatabaseError
36 + TransientError
37 + DatabaseUnavailable
38
39 + DriverError
40 + TransactionError
41 + TransactionNestingError
42 + SessionExpired
43 + ServiceUnavailable
44 + RoutingServiceUnavailable
45 + WriteServiceUnavailable
46 + ReadServiceUnavailable
47 + ConfigurationError
48 + AuthConfigurationError
49 + CertificateConfigurationError
50 + ResultConsumedError
51
52 Connector API Errors
53 ====================
54 + BoltError
55 + BoltHandshakeError
56 + BoltRoutingError
57 + BoltConnectionError
58 + BoltSecurityError
59 + BoltConnectionBroken
60 + BoltConnectionClosed
61 + BoltFailure
62 + BoltIncompleteCommitError
63 + BoltProtocolError
64 + Bolt*
65
2366 """
2467
25
26 from neobolt.exceptions import *
68 CLASSIFICATION_CLIENT = "ClientError"
69 CLASSIFICATION_TRANSIENT = "TransientError"
70 CLASSIFICATION_DATABASE = "DatabaseError"
71
72
73 class Neo4jError(Exception):
74 """ Raised when the Cypher engine returns an error to the client.
75 """
76
77 message = None
78 code = None
79 classification = None
80 category = None
81 title = None
82 metadata = None
83
84 @classmethod
85 def hydrate(cls, message=None, code=None, **metadata):
86 message = message or "An unknown error occurred"
87 code = code or "Neo.DatabaseError.General.UnknownError"
88 try:
89 _, classification, category, title = code.split(".")
90 except ValueError:
91 classification = CLASSIFICATION_DATABASE
92 category = "General"
93 title = "UnknownError"
94
95 error_class = cls._extract_error_class(classification, code)
96
97 inst = error_class(message)
98 inst.message = message
99 inst.code = code
100 inst.classification = classification
101 inst.category = category
102 inst.title = title
103 inst.metadata = metadata
104 return inst
105
106 @classmethod
107 def _extract_error_class(cls, classification, code):
108 if classification == CLASSIFICATION_CLIENT:
109 try:
110 return client_errors[code]
111 except KeyError:
112 return ClientError
113
114 elif classification == CLASSIFICATION_TRANSIENT:
115 try:
116 return transient_errors[code]
117 except KeyError:
118 return TransientError
119
120 elif classification == CLASSIFICATION_DATABASE:
121 return DatabaseError
122
123 else:
124 return cls
125
126 def __str__(self):
127 return "{{code: {code}}} {{message: {message}}}".format(code=self.code, message=self.message)
128
129
130 class ClientError(Neo4jError):
131 """ The Client sent a bad request - changing the request might yield a successful outcome.
132 """
133
134
135 class DatabaseError(Neo4jError):
136 """ The database failed to service the request.
137 """
138
139
140 class TransientError(Neo4jError):
141 """ The database cannot service the request right now, retrying later might yield a successful outcome.
142 """
143
144 def is_retriable(self):
145 """These are really client errors but classification on the server is not entirely correct and they are classified as transient.
146
147 :return: True if it is a retriable TransientError, otherwise False.
148 :rtype: bool
149 """
150 return not (self.code in (
151 "Neo.TransientError.Transaction.Terminated",
152 "Neo.TransientError.Transaction.LockClientStopped",
153 ))
154
155
156 class DatabaseUnavailable(TransientError):
157 """
158 """
159
160
161 class ConstraintError(ClientError):
162 """
163 """
164
165
166 class CypherSyntaxError(ClientError):
167 """
168 """
169
170
171 class CypherTypeError(ClientError):
172 """
173 """
174
175
176 class NotALeader(ClientError):
177 """
178 """
179
180
181 class Forbidden(ClientError):
182 """
183 """
184
185
186 class ForbiddenOnReadOnlyDatabase(Forbidden):
187 """
188 """
189
190
191 class AuthError(ClientError):
192 """ Raised when authentication failure occurs.
193 """
194
195
196 client_errors = {
197
198 # ConstraintError
199 "Neo.ClientError.Schema.ConstraintValidationFailed": ConstraintError,
200 "Neo.ClientError.Schema.ConstraintViolation": ConstraintError,
201 "Neo.ClientError.Statement.ConstraintVerificationFailed": ConstraintError,
202 "Neo.ClientError.Statement.ConstraintViolation": ConstraintError,
203
204 # CypherSyntaxError
205 "Neo.ClientError.Statement.InvalidSyntax": CypherSyntaxError,
206 "Neo.ClientError.Statement.SyntaxError": CypherSyntaxError,
207
208 # CypherTypeError
209 "Neo.ClientError.Procedure.TypeError": CypherTypeError,
210 "Neo.ClientError.Statement.InvalidType": CypherTypeError,
211 "Neo.ClientError.Statement.TypeError": CypherTypeError,
212
213 # Forbidden
214 "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase": ForbiddenOnReadOnlyDatabase,
215 "Neo.ClientError.General.ReadOnly": Forbidden,
216 "Neo.ClientError.Schema.ForbiddenOnConstraintIndex": Forbidden,
217 "Neo.ClientError.Schema.IndexBelongsToConstraint": Forbidden,
218 "Neo.ClientError.Security.Forbidden": Forbidden,
219 "Neo.ClientError.Transaction.ForbiddenDueToTransactionType": Forbidden,
220
221 # AuthError
222 "Neo.ClientError.Security.AuthorizationFailed": AuthError,
223 "Neo.ClientError.Security.Unauthorized": AuthError,
224
225 # NotALeader
226 "Neo.ClientError.Cluster.NotALeader": NotALeader
227 }
228
229 transient_errors = {
230
231 # DatabaseUnavailableError
232 "Neo.TransientError.General.DatabaseUnavailable": DatabaseUnavailable
233 }
234
235
236 class DriverError(Exception):
237 """ Raised when the Driver raises an error.
238 """
239
240
241 class SessionExpired(DriverError):
242 """ Raised when no a session is no longer able to fulfil
243 the purpose described by its original parameters.
244 """
245
246 def __init__(self, session, *args, **kwargs):
247 super(SessionExpired, self).__init__(session, *args, **kwargs)
248
249
250 class TransactionError(DriverError):
251 """ Raised when an error occurs while using a transaction.
252 """
253
254 def __init__(self, transaction, *args, **kwargs):
255 super(TransactionError, self).__init__(*args, **kwargs)
256 self.transaction = transaction
257
258
259 class TransactionNestingError(DriverError):
260 """ Raised when transactions are nested incorrectly.
261 """
262
263 def __init__(self, transaction, *args, **kwargs):
264 super(TransactionError, self).__init__(*args, **kwargs)
265 self.transaction = transaction
266
267
268 class ServiceUnavailable(DriverError):
269 """ Raised when no database service is available.
270 """
271
272
273 class RoutingServiceUnavailable(ServiceUnavailable):
274 """ Raised when no routing service is available.
275 """
276
277
278 class WriteServiceUnavailable(ServiceUnavailable):
279 """ Raised when no write service is available.
280 """
281
282
283 class ReadServiceUnavailable(ServiceUnavailable):
284 """ Raised when no read service is available.
285 """
286
287
288 class ResultConsumedError(DriverError):
289 """ Raised when trying to access records after the records have been consumed.
290 """
291
292
293 class ConfigurationError(DriverError):
294 """ Raised when there is an error concerning a configuration.
295 """
296
297
298 class AuthConfigurationError(ConfigurationError):
299 """ Raised when there is an error with the authentication configuration.
300 """
301
302
303 class CertificateConfigurationError(ConfigurationError):
304 """ Raised when there is an error with the authentication configuration.
305 """
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 """
21 Graph data types
22 """
23
24
25 from collections.abc import Mapping
26
27
28 __all__ = [
29 "Graph",
30 "Node",
31 "Relationship",
32 "Path",
33 ]
34
35
36 class Graph:
37 """ Local, self-contained graph object that acts as a container for
38 :class:`.Node` and :class:`.Relationship` instances.
39 """
40
41 def __init__(self):
42 self._nodes = {}
43 self._relationships = {}
44 self._relationship_types = {}
45 self._node_set_view = EntitySetView(self._nodes)
46 self._relationship_set_view = EntitySetView(self._relationships)
47
48 @property
49 def nodes(self):
50 """ Access a set view of the nodes in this graph.
51 """
52 return self._node_set_view
53
54 @property
55 def relationships(self):
56 """ Access a set view of the relationships in this graph.
57 """
58 return self._relationship_set_view
59
60 def relationship_type(self, name):
61 """ Obtain a :class:`.Relationship` subclass for a given
62 relationship type name.
63 """
64 try:
65 cls = self._relationship_types[name]
66 except KeyError:
67 cls = self._relationship_types[name] = type(str(name), (Relationship,), {})
68 return cls
69
70 class Hydrator:
71
72 def __init__(self, graph):
73 self.graph = graph
74
75 def hydrate_node(self, n_id, n_labels=None, properties=None):
76 assert isinstance(self.graph, Graph)
77 try:
78 inst = self.graph._nodes[n_id]
79 except KeyError:
80 inst = self.graph._nodes[n_id] = Node(self.graph, n_id, n_labels, properties)
81 else:
82 # If we have already hydrated this node as the endpoint of
83 # a relationship, it won't have any labels or properties.
84 # Therefore, we need to add the ones we have here.
85 if n_labels:
86 inst._labels = inst._labels.union(n_labels) # frozen_set
87 if properties:
88 inst._properties.update(properties)
89 return inst
90
91 def hydrate_relationship(self, r_id, n0_id, n1_id, r_type, properties=None):
92 inst = self.hydrate_unbound_relationship(r_id, r_type, properties)
93 inst._start_node = self.hydrate_node(n0_id)
94 inst._end_node = self.hydrate_node(n1_id)
95 return inst
96
97 def hydrate_unbound_relationship(self, r_id, r_type, properties=None):
98 assert isinstance(self.graph, Graph)
99 try:
100 inst = self.graph._relationships[r_id]
101 except KeyError:
102 r = self.graph.relationship_type(r_type)
103 inst = self.graph._relationships[r_id] = r(self.graph, r_id, properties)
104 return inst
105
106 def hydrate_path(self, nodes, relationships, sequence):
107 assert isinstance(self.graph, Graph)
108 assert len(nodes) >= 1
109 assert len(sequence) % 2 == 0
110 last_node = nodes[0]
111 entities = [last_node]
112 for i, rel_index in enumerate(sequence[::2]):
113 assert rel_index != 0
114 next_node = nodes[sequence[2 * i + 1]]
115 if rel_index > 0:
116 r = relationships[rel_index - 1]
117 r._start_node = last_node
118 r._end_node = next_node
119 entities.append(r)
120 else:
121 r = relationships[-rel_index - 1]
122 r._start_node = next_node
123 r._end_node = last_node
124 entities.append(r)
125 last_node = next_node
126 return Path(*entities)
127
128
129 class Entity(Mapping):
130 """ Base class for :class:`.Node` and :class:`.Relationship` that
131 provides :class:`.Graph` membership and property containment
132 functionality.
133 """
134
135 def __init__(self, graph, id, properties):
136 self._graph = graph
137 self._id = id
138 self._properties = dict((k, v) for k, v in (properties or {}).items() if v is not None)
139
140 def __eq__(self, other):
141 try:
142 return type(self) == type(other) and self.graph == other.graph and self.id == other.id
143 except AttributeError:
144 return False
145
146 def __ne__(self, other):
147 return not self.__eq__(other)
148
149 def __hash__(self):
150 return hash(self.id)
151
152 def __len__(self):
153 return len(self._properties)
154
155 def __getitem__(self, name):
156 return self._properties.get(name)
157
158 def __contains__(self, name):
159 return name in self._properties
160
161 def __iter__(self):
162 return iter(self._properties)
163
164 @property
165 def graph(self):
166 """ The :class:`.Graph` to which this entity belongs.
167 """
168 return self._graph
169
170 @property
171 def id(self):
172 """ The identity of this entity in its container :class:`.Graph`.
173 """
174 return self._id
175
176 def get(self, name, default=None):
177 """ Get a property value by name, optionally with a default.
178 """
179 return self._properties.get(name, default)
180
181 def keys(self):
182 """ Return an iterable of all property names.
183 """
184 return self._properties.keys()
185
186 def values(self):
187 """ Return an iterable of all property values.
188 """
189 return self._properties.values()
190
191 def items(self):
192 """ Return an iterable of all property name-value pairs.
193 """
194 return self._properties.items()
195
196
197 class EntitySetView(Mapping):
198 """ View of a set of :class:`.Entity` instances within a :class:`.Graph`.
199 """
200
201 def __init__(self, entity_dict):
202 self._entity_dict = entity_dict
203
204 def __getitem__(self, e_id):
205 return self._entity_dict[e_id]
206
207 def __len__(self):
208 return len(self._entity_dict)
209
210 def __iter__(self):
211 return iter(self._entity_dict.values())
212
213
214 class Node(Entity):
215 """ Self-contained graph node.
216 """
217
218 def __init__(self, graph, n_id, n_labels=None, properties=None):
219 Entity.__init__(self, graph, n_id, properties)
220 self._labels = frozenset(n_labels or ())
221
222 def __repr__(self):
223 return "<Node id=%r labels=%r properties=%r>" % (self._id, self._labels, self._properties)
224
225 @property
226 def labels(self):
227 """ The set of labels attached to this node.
228 """
229 return self._labels
230
231
232 class Relationship(Entity):
233 """ Self-contained graph relationship.
234 """
235
236 def __init__(self, graph, r_id, properties):
237 Entity.__init__(self, graph, r_id, properties)
238 self._start_node = None
239 self._end_node = None
240
241 def __repr__(self):
242 return "<Relationship id=%r nodes=(%r, %r) type=%r properties=%r>" % (
243 self._id, self._start_node, self._end_node, self.type, self._properties)
244
245 @property
246 def nodes(self):
247 """ The pair of nodes which this relationship connects.
248 """
249 return self._start_node, self._end_node
250
251 @property
252 def start_node(self):
253 """ The start node of this relationship.
254 """
255 return self._start_node
256
257 @property
258 def end_node(self):
259 """ The end node of this relationship.
260 """
261 return self._end_node
262
263 @property
264 def type(self):
265 """ The type name of this relationship.
266 This is functionally equivalent to ``type(relationship).__name__``.
267 """
268 return type(self).__name__
269
270
271 class Path:
272 """ Self-contained graph path.
273 """
274
275 def __init__(self, start_node, *relationships):
276 assert isinstance(start_node, Node)
277 nodes = [start_node]
278 for i, relationship in enumerate(relationships, start=1):
279 assert isinstance(relationship, Relationship)
280 if relationship.start_node == nodes[-1]:
281 nodes.append(relationship.end_node)
282 elif relationship.end_node == nodes[-1]:
283 nodes.append(relationship.start_node)
284 else:
285 raise ValueError("Relationship %d does not connect to the last node" % i)
286 self._nodes = tuple(nodes)
287 self._relationships = relationships
288
289 def __repr__(self):
290 return "<Path start=%r end=%r size=%s>" % \
291 (self.start_node, self.end_node, len(self))
292
293 def __eq__(self, other):
294 try:
295 return self.start_node == other.start_node and self.relationships == other.relationships
296 except AttributeError:
297 return False
298
299 def __ne__(self, other):
300 return not self.__eq__(other)
301
302 def __hash__(self):
303 value = hash(self._nodes[0])
304 for relationship in self._relationships:
305 value ^= hash(relationship)
306 return value
307
308 def __len__(self):
309 return len(self._relationships)
310
311 def __iter__(self):
312 return iter(self._relationships)
313
314 @property
315 def graph(self):
316 """ The :class:`.Graph` to which this path belongs.
317 """
318 return self._nodes[0].graph
319
320 @property
321 def nodes(self):
322 """ The sequence of :class:`.Node` objects in this path.
323 """
324 return self._nodes
325
326 @property
327 def start_node(self):
328 """ The first :class:`.Node` in this path.
329 """
330 return self._nodes[0]
331
332 @property
333 def end_node(self):
334 """ The last :class:`.Node` in this path.
335 """
336 return self._nodes[-1]
337
338 @property
339 def relationships(self):
340 """ The sequence of :class:`.Relationship` objects in this path.
341 """
342 return self._relationships
0 Regular (non-async) I/O for Neo4j.
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """
22 This module contains the low-level functionality required for speaking
23 Bolt. It is not intended to be used directly by driver users. Instead,
24 the `session` module provides the main user-facing abstractions.
25 """
26
27
28 __all__ = [
29 "Bolt",
30 "BoltPool",
31 "Neo4jPool",
32 ]
33
34
35 from collections import deque
36 from logging import getLogger
37 from random import choice
38 from select import select
39 from time import perf_counter
40
41 from socket import (
42 socket,
43 SOL_SOCKET,
44 SO_KEEPALIVE,
45 SHUT_RDWR,
46 timeout as SocketTimeout,
47 AF_INET,
48 AF_INET6,
49 )
50
51 from ssl import (
52 HAS_SNI,
53 SSLError,
54 )
55
56 from struct import (
57 pack as struct_pack,
58 )
59
60 from threading import (
61 Lock,
62 RLock,
63 Condition,
64 )
65
66 from neo4j.addressing import Address
67 from neo4j.conf import PoolConfig
68 from neo4j._exceptions import (
69 BoltRoutingError,
70 BoltSecurityError,
71 BoltProtocolError,
72 BoltHandshakeError,
73 )
74 from neo4j.exceptions import (
75 ServiceUnavailable,
76 ClientError,
77 SessionExpired,
78 ReadServiceUnavailable,
79 WriteServiceUnavailable,
80 ConfigurationError,
81 )
82 from neo4j.routing import RoutingTable
83 from neo4j.conf import (
84 PoolConfig,
85 WorkspaceConfig,
86 )
87 from neo4j.api import (
88 READ_ACCESS,
89 WRITE_ACCESS,
90 )
91
92 # Set up logger
93 log = getLogger("neo4j")
94
95
96 class Bolt:
97 """ Server connection for Bolt protocol.
98
99 A :class:`.Bolt` should be constructed following a
100 successful .open()
101
102 Bolt handshake and takes the socket over which
103 the handshake was carried out.
104 """
105
106 MAGIC_PREAMBLE = b"\x60\x60\xB0\x17"
107
108 PROTOCOL_VERSION = None
109
110 @classmethod
111 def get_handshake(cls):
112 """ Return the supported Bolt versions as bytes.
113 The length is 16 bytes as specified in the Bolt version negotiation.
114 :return: bytes
115 """
116 offered_versions = sorted(cls.protocol_handlers().keys(), reverse=True)[:4]
117 return b"".join(version.to_bytes() for version in offered_versions).ljust(16, b"\x00")
118
119 @classmethod
120 def protocol_handlers(cls, protocol_version=None):
121 """ Return a dictionary of available Bolt protocol handlers,
122 keyed by version tuple. If an explicit protocol version is
123 provided, the dictionary will contain either zero or one items,
124 depending on whether that version is supported. If no protocol
125 version is provided, all available versions will be returned.
126
127 :param protocol_version: tuple identifying a specific protocol
128 version (e.g. (3, 5)) or None
129 :return: dictionary of version tuple to handler class for all
130 relevant and supported protocol versions
131 :raise TypeError: if protocol version is not passed in a tuple
132 """
133
134 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
135 from neo4j.io._bolt3 import Bolt3
136 from neo4j.io._bolt4 import Bolt4x0, Bolt4x1, Bolt4x2
137
138 handlers = {
139 Bolt3.PROTOCOL_VERSION: Bolt3,
140 Bolt4x0.PROTOCOL_VERSION: Bolt4x0,
141 Bolt4x1.PROTOCOL_VERSION: Bolt4x1,
142 Bolt4x2.PROTOCOL_VERSION: Bolt4x2,
143 }
144
145 if protocol_version is None:
146 return handlers
147
148 if not isinstance(protocol_version, tuple):
149 raise TypeError("Protocol version must be specified as a tuple")
150
151 if protocol_version in handlers:
152 return {protocol_version: handlers[protocol_version]}
153
154 return {}
155
156 @classmethod
157 def ping(cls, address, *, timeout=None, **config):
158 """ Attempt to establish a Bolt connection, returning the
159 agreed Bolt protocol version if successful.
160 """
161 config = PoolConfig.consume(config)
162 try:
163 s, protocol_version, handshake, data = connect(
164 address,
165 timeout=timeout,
166 custom_resolver=config.resolver,
167 ssl_context=config.get_ssl_context(),
168 keep_alive=config.keep_alive,
169 )
170 except ServiceUnavailable:
171 return None
172 except BoltHandshakeError as e:
173 return None
174 else:
175 s.close()
176 return protocol_version
177
178 @classmethod
179 def open(cls, address, *, auth=None, timeout=None, routing_context=None, **pool_config):
180 """ Open a new Bolt connection to a given server address.
181
182 :param address:
183 :param auth:
184 :param timeout: the connection timeout in seconds
185 :param routing_context: dict containing routing context
186 :param pool_config:
187 :return:
188 :raise BoltHandshakeError: raised if the Bolt Protocol can not negotiate a protocol version.
189 :raise ServiceUnavailable: raised if there was a connection issue.
190 """
191 pool_config = PoolConfig.consume(pool_config)
192 s, pool_config.protocol_version, handshake, data = connect(
193 address,
194 timeout=timeout,
195 custom_resolver=pool_config.resolver,
196 ssl_context=pool_config.get_ssl_context(),
197 keep_alive=pool_config.keep_alive,
198 )
199
200 if pool_config.protocol_version == (3, 0):
201 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
202 from neo4j.io._bolt3 import Bolt3
203 connection = Bolt3(address, s, pool_config.max_connection_lifetime, auth=auth, user_agent=pool_config.user_agent, routing_context=routing_context)
204 elif pool_config.protocol_version == (4, 0):
205 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
206 from neo4j.io._bolt4 import Bolt4x0
207 connection = Bolt4x0(address, s, pool_config.max_connection_lifetime, auth=auth, user_agent=pool_config.user_agent, routing_context=routing_context)
208 elif pool_config.protocol_version == (4, 1):
209 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
210 from neo4j.io._bolt4 import Bolt4x1
211 connection = Bolt4x1(address, s, pool_config.max_connection_lifetime, auth=auth, user_agent=pool_config.user_agent, routing_context=routing_context)
212 elif pool_config.protocol_version == (4, 2):
213 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
214 from neo4j.io._bolt4 import Bolt4x2
215 connection = Bolt4x2(address, s, pool_config.max_connection_lifetime, auth=auth, user_agent=pool_config.user_agent, routing_context=routing_context)
216 else:
217 log.debug("[#%04X] S: <CLOSE>", s.getpeername()[1])
218 s.shutdown(SHUT_RDWR)
219 s.close()
220
221 supported_versions = Bolt.protocol_handlers().keys()
222 raise BoltHandshakeError("The Neo4J server does not support communication with this driver. This driver have support for Bolt Protocols {}".format(supported_versions), address=address, request_data=handshake, response_data=data)
223
224 try:
225 connection.hello()
226 except Exception as error:
227 log.debug("[#%04X] C: <CLOSE> %s", s.getsockname()[1], str(error))
228 s.shutdown(SHUT_RDWR)
229 s.close()
230 raise error
231
232 return connection
233
234 @property
235 def encrypted(self):
236 raise NotImplementedError
237
238 @property
239 def der_encoded_server_certificate(self):
240 raise NotImplementedError
241
242 @property
243 def local_port(self):
244 raise NotImplementedError
245
246 def hello(self):
247 raise NotImplementedError
248
249 def __del__(self):
250 try:
251 self.close()
252 except OSError:
253 pass
254
255 def __enter__(self):
256 return self
257
258 def __exit__(self, exc_type, exc_value, traceback):
259 self.close()
260
261 def run(self, query, parameters=None, mode=None, bookmarks=None, metadata=None,
262 timeout=None, db=None, **handlers):
263 """ Appends a RUN message to the output stream.
264
265 :param query: Cypher query string
266 :param parameters: dictionary of Cypher parameters
267 :param mode: access mode for routing - "READ" or "WRITE" (default)
268 :param bookmarks: iterable of bookmark values after which this transaction should begin
269 :param metadata: custom metadata dictionary to attach to the transaction
270 :param timeout: timeout for transaction execution (seconds)
271 :param db: name of the database against which to begin the transaction
272 :param handlers: handler functions passed into the returned Response object
273 :return: Response object
274 """
275
276 def discard(self, n=-1, qid=-1, **handlers):
277 """ Appends a DISCARD message to the output stream.
278
279 :param n: number of records to discard, default = -1 (ALL)
280 :param qid: query ID to discard for, default = -1 (last query)
281 :param handlers: handler functions passed into the returned Response object
282 :return: Response object
283 """
284
285 def pull(self, n=-1, qid=-1, **handlers):
286 """ Appends a PULL message to the output stream.
287
288 :param n: number of records to pull, default = -1 (ALL)
289 :param qid: query ID to pull for, default = -1 (last query)
290 :param handlers: handler functions passed into the returned Response object
291 :return: Response object
292 """
293
294 def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, db=None, **handlers):
295 """ Appends a BEGIN message to the output stream.
296
297 :param mode: access mode for routing - "READ" or "WRITE" (default)
298 :param bookmarks: iterable of bookmark values after which this transaction should begin
299 :param metadata: custom metadata dictionary to attach to the transaction
300 :param timeout: timeout for transaction execution (seconds)
301 :param db: name of the database against which to begin the transaction
302 :param handlers: handler functions passed into the returned Response object
303 :return: Response object
304 """
305
306 def commit(self, **handlers):
307 raise NotImplementedError
308
309 def rollback(self, **handlers):
310 raise NotImplementedError
311
312 def reset(self):
313 """ Add a RESET message to the outgoing queue, send
314 it and consume all remaining messages.
315 """
316 raise NotImplementedError
317
318 def send_all(self):
319 """ Send all queued messages to the server.
320 """
321 raise NotImplementedError
322
323 def fetch_message(self):
324 """ Receive at least one message from the server, if available.
325
326 :return: 2-tuple of number of detail messages and number of summary
327 messages fetched
328 """
329 raise NotImplementedError
330
331 def timedout(self):
332 raise NotImplementedError
333
334 def fetch_all(self):
335 """ Fetch all outstanding messages.
336
337 :return: 2-tuple of number of detail messages and number of summary
338 messages fetched
339 """
340 raise NotImplementedError
341
342 def close(self):
343 """ Close the connection.
344 """
345 raise NotImplementedError
346
347 def closed(self):
348 raise NotImplementedError
349
350 def defunct(self):
351 raise NotImplementedError
352
353
354 class IOPool:
355 """ A collection of connections to one or more server addresses.
356 """
357
358 def __init__(self, opener, pool_config, workspace_config):
359 assert callable(opener)
360 assert isinstance(pool_config, PoolConfig)
361 assert isinstance(workspace_config, WorkspaceConfig)
362
363 self.opener = opener
364 self.pool_config = pool_config
365 self.workspace_config = workspace_config
366 self.connections = {}
367 self.lock = RLock()
368 self.cond = Condition(self.lock)
369
370 def __enter__(self):
371 return self
372
373 def __exit__(self, exc_type, exc_value, traceback):
374 self.close()
375
376 def _acquire(self, address, timeout):
377 """ Acquire a connection to a given address from the pool.
378 The address supplied should always be an IP address, not
379 a host name.
380
381 This method is thread safe.
382 """
383 t0 = perf_counter()
384 if timeout is None:
385 timeout = self.workspace_config.connection_acquisition_timeout
386
387 with self.lock:
388 try:
389 connections = self.connections[address]
390 except KeyError:
391 connections = self.connections[address] = deque()
392
393 def time_remaining():
394 t = timeout - (perf_counter() - t0)
395 return t if t > 0 else 0
396
397 while True:
398 # try to find a free connection in pool
399 for connection in list(connections):
400 if connection.closed() or connection.defunct() or connection.timedout():
401 connections.remove(connection)
402 continue
403 if not connection.in_use:
404 connection.in_use = True
405 return connection
406 # all connections in pool are in-use
407 infinite_pool_size = (self.pool_config.max_connection_pool_size < 0 or self.pool_config.max_connection_pool_size == float("inf"))
408 can_create_new_connection = infinite_pool_size or len(connections) < self.pool_config.max_connection_pool_size
409 if can_create_new_connection:
410 timeout = min(self.pool_config.connection_timeout, time_remaining())
411 try:
412 connection = self.opener(address, timeout)
413 except ServiceUnavailable:
414 self.remove(address)
415 raise
416 else:
417 connection.pool = self
418 connection.in_use = True
419 connections.append(connection)
420 return connection
421
422 # failed to obtain a connection from pool because the
423 # pool is full and no free connection in the pool
424 if time_remaining():
425 self.cond.wait(time_remaining())
426 # if timed out, then we throw error. This time
427 # computation is needed, as with python 2.7, we
428 # cannot tell if the condition is notified or
429 # timed out when we come to this line
430 if not time_remaining():
431 raise ClientError("Failed to obtain a connection from pool "
432 "within {!r}s".format(timeout))
433 else:
434 raise ClientError("Failed to obtain a connection from pool "
435 "within {!r}s".format(timeout))
436
437 def acquire(self, access_mode=None, timeout=None, database=None):
438 """ Acquire a connection to a server that can satisfy a set of parameters.
439
440 :param access_mode:
441 :param timeout:
442 :param database:
443 """
444
445 def release(self, *connections):
446 """ Release a connection back into the pool.
447 This method is thread safe.
448 """
449 with self.lock:
450 for connection in connections:
451 connection.in_use = False
452 self.cond.notify_all()
453
454 def in_use_connection_count(self, address):
455 """ Count the number of connections currently in use to a given
456 address.
457 """
458 try:
459 connections = self.connections[address]
460 except KeyError:
461 return 0
462 else:
463 return sum(1 if connection.in_use else 0 for connection in connections)
464
465 def deactivate(self, address):
466 """ Deactivate an address from the connection pool, if present, closing
467 all idle connection to that address
468 """
469 with self.lock:
470 try:
471 connections = self.connections[address]
472 except KeyError: # already removed from the connection pool
473 return
474 for conn in list(connections):
475 if not conn.in_use:
476 connections.remove(conn)
477 try:
478 conn.close()
479 except IOError:
480 pass
481 if not connections:
482 self.remove(address)
483
484 def on_write_failure(self, address):
485 raise WriteServiceUnavailable("No write service available for pool {}".format(self))
486
487 def remove(self, address):
488 """ Remove an address from the connection pool, if present, closing
489 all connections to that address.
490 """
491 with self.lock:
492 for connection in self.connections.pop(address, ()):
493 try:
494 connection.close()
495 except IOError:
496 pass
497
498 def close(self):
499 """ Close all connections and empty the pool.
500 This method is thread safe.
501 """
502 try:
503 with self.lock:
504 for address in list(self.connections):
505 self.remove(address)
506 except TypeError:
507 pass
508
509
510 class BoltPool(IOPool):
511
512 @classmethod
513 def open(cls, address, *, auth, pool_config, workspace_config, routing_context=None):
514 """Create a new BoltPool
515
516 :param address:
517 :param auth:
518 :param pool_config:
519 :param workspace_config:
520 :param routing_context:
521 :return: BoltPool
522 """
523
524 if routing_context is None:
525 routing_context = {}
526 elif "address" in routing_context:
527 raise ConfigurationError("The key 'address' is reserved for routing context.")
528 routing_context["address"] = str(address)
529
530 def opener(addr, timeout):
531 return Bolt.open(addr, auth=auth, timeout=timeout, routing_context=routing_context, **pool_config)
532
533 pool = cls(opener, pool_config, workspace_config, routing_context, address)
534 seeds = [pool.acquire() for _ in range(pool_config.init_size)]
535 pool.release(*seeds)
536 return pool
537
538 def __init__(self, opener, pool_config, workspace_config, routing_context, address):
539 super(BoltPool, self).__init__(opener, pool_config, workspace_config)
540 self.address = address
541 self.routing_context = routing_context
542
543 def __repr__(self):
544 return "<{} address={!r}>".format(self.__class__.__name__, self.address)
545
546 def acquire(self, access_mode=None, timeout=None, database=None):
547 # The access_mode and database is not needed for a direct connection, its just there for consistency.
548 return self._acquire(self.address, timeout)
549
550
551 class Neo4jPool(IOPool):
552 """ Connection pool with routing table.
553 """
554
555 @classmethod
556 def open(cls, *addresses, auth, pool_config, workspace_config, routing_context=None):
557 """Create a new Neo4jPool
558
559 :param addresses: one or more address as positional argument
560 :param auth:
561 :param pool_config:
562 :param workspace_config:
563 :param routing_context:
564 :return: Neo4jPool
565 """
566
567 address = addresses[0]
568 if routing_context is None:
569 routing_context = {}
570 elif "address" in routing_context:
571 raise ConfigurationError("The key 'address' is reserved for routing context.")
572 routing_context["address"] = str(address)
573
574 def opener(addr, timeout):
575 return Bolt.open(addr, auth=auth, timeout=timeout, routing_context=routing_context, **pool_config)
576
577 pool = cls(opener, pool_config, workspace_config, routing_context, address)
578
579 try:
580 pool.update_routing_table(database=workspace_config.database)
581 except Exception:
582 pool.close()
583 raise
584 else:
585 return pool
586
587 def __init__(self, opener, pool_config, workspace_config, routing_context, address):
588 """
589
590 :param opener:
591 :param pool_config:
592 :param workspace_config:
593 :param routing_context: Dictionary with routing information
594 :param addresses:
595 """
596 super(Neo4jPool, self).__init__(opener, pool_config, workspace_config)
597 # Each database have a routing table, the default database is a special case.
598 log.debug("[#0000] C: <NEO4J POOL> routing address %r", address)
599 self.address = address
600 self.routing_tables = {workspace_config.database: RoutingTable(database=workspace_config.database, routers=[address])}
601 self.routing_context = routing_context
602 self.refresh_lock = Lock()
603
604 def __repr__(self):
605 """ The representation shows the initial routing addresses.
606
607 :return: The representation
608 :rtype: str
609 """
610 return "<{} addresses={!r}>".format(self.__class__.__name__, self.get_default_database_initial_router_addresses())
611
612 @property
613 def first_initial_routing_address(self):
614 return self.get_default_database_initial_router_addresses()[0]
615
616 def get_default_database_initial_router_addresses(self):
617 """ Get the initial router addresses for the default database.
618
619 :return:
620 :rtype: OrderedSet
621 """
622 return self.get_routing_table_for_default_database().initial_routers
623
624 def get_default_database_router_addresses(self):
625 """ Get the router addresses for the default database.
626
627 :return:
628 :rtype: OrderedSet
629 """
630 return self.get_routing_table_for_default_database().routers
631
632 def get_routing_table_for_default_database(self):
633 return self.routing_tables[self.workspace_config.database]
634
635 def create_routing_table(self, database):
636 if database not in self.routing_tables:
637 self.routing_tables[database] = RoutingTable(database=database, routers=self.get_default_database_initial_router_addresses())
638
639 def fetch_routing_info(self, *, address, timeout, database):
640 """ Fetch raw routing info from a given router address.
641
642 :param address: router address
643 :param timeout: seconds
644 :param database: the data base name to get routing table for
645 :param address: the address by which the client initially contacted the server as a hint for inclusion in the returned routing table.
646
647 :return: list of routing records or
648 None if no connection could be established
649 :raise ServiceUnavailable: if the server does not support routing or
650 if routing support is broken
651 """
652
653 # The name of system database is fixed and named as "system".
654 # It cannot be changed for a single instance or a cluster. (We can reliably assume that the system db exists on each instance.)
655 #
656 # Database name is NOT case sensitive.
657 #
658 # Each cluster member will host the exact same databases. For example, if a cluster member A has databases "foo" and
659 # "system", then all other members in the cluster should also have and only have "foo" and "system".
660 # However at a certain time, the cluster members may or may not up-to-date, as a result, cluster members may contain different databases.
661 #
662 # Maintain a routing table for each database.
663 #
664 # Default database is named "neo4j", (this can be renamed on the Neo4j server).
665 #
666 # Any core member in a cluster can provide a routing table for any database inside the cluster.
667 # The seed_url can be used to find all databases in the cluster.
668 #
669 # If the driver failed to refresh routing table with all known routers, then the driver should retry a few times before it raises a ServiceUnavailable.
670 #
671 # A valid routing table should at least have one router and one reader.
672 #
673 # To prevent the routing tables from growing infinitely.
674 # Stale/Aged routing tables is removed when there is a failure to obtain a routing table.
675 # Remove a routing table if it have been aged, timeout = TTL + RoutingConfig.routing_table_purge_delay
676
677 # Carry out Bolt subclass imports locally to avoid circular dependency issues.
678 from neo4j.io._bolt3 import Bolt3
679 from neo4j.io._bolt4 import Bolt4x0, Bolt4x1, Bolt4x2
680
681 from neo4j.api import (
682 SYSTEM_DATABASE,
683 DEFAULT_DATABASE,
684 READ_ACCESS,
685 )
686
687 metadata = {}
688 records = []
689
690 def fail(md):
691 if md.get("code") == "Neo.ClientError.Procedure.ProcedureNotFound":
692 raise BoltRoutingError("Server does not support routing", address)
693 else:
694 raise BoltRoutingError("Routing support broken on server", address)
695
696 try:
697 with self._acquire(address, timeout) as cx:
698 log.debug("[#%04X] C: <ROUTING> query=%r", cx.local_port, self.routing_context or {})
699
700 if database is None:
701 database = self.workspace_config.database
702
703 cx.run_get_routing_table(on_success=metadata.update, on_failure=fail, database=database)
704 cx.pull(on_success=metadata.update, on_records=records.extend)
705 cx.send_all()
706 cx.fetch_all()
707 routing_info = [dict(zip(metadata.get("fields", ()), values)) for values in records]
708 log.debug("[#%04X] S: <ROUTING> info=%r", cx.local_port, routing_info)
709 return routing_info
710 except BoltRoutingError as error:
711 raise ServiceUnavailable(*error.args)
712 except ServiceUnavailable:
713 self.deactivate(address=address)
714 return None
715
716 def fetch_routing_table(self, *, address, timeout, database):
717 """ Fetch a routing table from a given router address.
718
719 :param address: router address
720 :param timeout: seconds
721 :param database: the database name
722 :type: str
723
724 :return: a new RoutingTable instance or None if the given router is
725 currently unable to provide routing information
726
727 :raise neo4j.exceptions.ServiceUnavailable: if no writers are available
728 :raise neo4j._exceptions.BoltProtocolError: if the routing information received is unusable
729 """
730 new_routing_info = self.fetch_routing_info(address=address, timeout=timeout, database=database)
731 if new_routing_info is None:
732 return None
733 elif not new_routing_info:
734 raise BoltRoutingError("Invalid routing table", address)
735 else:
736 servers = new_routing_info[0]["servers"]
737 ttl = new_routing_info[0]["ttl"]
738 new_routing_table = RoutingTable.parse_routing_info(database=database, servers=servers, ttl=ttl)
739
740 # Parse routing info and count the number of each type of server
741 num_routers = len(new_routing_table.routers)
742 num_readers = len(new_routing_table.readers)
743
744 # num_writers = len(new_routing_table.writers)
745 # If no writers are available. This likely indicates a temporary state,
746 # such as leader switching, so we should not signal an error.
747
748 # No routers
749 if num_routers == 0:
750 raise BoltRoutingError("No routing servers returned from server", address)
751
752 # No readers
753 if num_readers == 0:
754 raise BoltRoutingError("No read servers returned from server", address)
755
756 # At least one of each is fine, so return this table
757 return new_routing_table
758
759 def update_routing_table_from(self, *routers, database=None):
760 """ Try to update routing tables with the given routers.
761
762 :return: True if the routing table is successfully updated,
763 otherwise False
764 """
765 log.debug("Attempting to update routing table from {}".format(", ".join(map(repr, routers))))
766 for router in routers:
767 new_routing_table = self.fetch_routing_table(address=router, timeout=self.pool_config.connection_timeout, database=database)
768 if new_routing_table is not None:
769 self.routing_tables[database].update(new_routing_table)
770 log.debug("[#0000] C: <UPDATE ROUTING TABLE> address={!r} ({!r})".format(router, self.routing_tables[database]))
771 return True
772 return False
773
774 def update_routing_table(self, *, database):
775 """ Update the routing table from the first router able to provide
776 valid routing information.
777
778 :param database: The database name
779
780 :raise neo4j.exceptions.ServiceUnavailable:
781 """
782 # copied because it can be modified
783 existing_routers = list(self.routing_tables[database].routers)
784
785 has_tried_initial_routers = False
786 if self.routing_tables[database].missing_fresh_writer():
787 # TODO: Test this state
788 has_tried_initial_routers = True
789 if self.update_routing_table_from(self.first_initial_routing_address, database=database):
790 # Why is only the first initial routing address used?
791 return
792
793 if self.update_routing_table_from(*existing_routers, database=database):
794 return
795
796 if not has_tried_initial_routers and self.first_initial_routing_address not in existing_routers:
797 if self.update_routing_table_from(self.first_initial_routing_address, database=database):
798 # Why is only the first initial routing address used?
799 return
800
801 # None of the routers have been successful, so just fail
802 log.error("Unable to retrieve routing information")
803 raise ServiceUnavailable("Unable to retrieve routing information")
804
805 def update_connection_pool(self, *, database):
806 servers = self.routing_tables[database].servers()
807 for address in list(self.connections):
808 if address not in servers:
809 super(Neo4jPool, self).deactivate(address)
810
811 def ensure_routing_table_is_fresh(self, *, access_mode, database):
812 """ Update the routing table if stale.
813
814 This method performs two freshness checks, before and after acquiring
815 the refresh lock. If the routing table is already fresh on entry, the
816 method exits immediately; otherwise, the refresh lock is acquired and
817 the second freshness check that follows determines whether an update
818 is still required.
819
820 This method is thread-safe.
821
822 :return: `True` if an update was required, `False` otherwise.
823 """
824 from neo4j.api import READ_ACCESS
825 if self.routing_tables[database].is_fresh(readonly=(access_mode == READ_ACCESS)):
826 # Readers are fresh.
827 return False
828 with self.refresh_lock:
829
830 self.update_routing_table(database=database)
831 self.update_connection_pool(database=database)
832
833 for database in list(self.routing_tables.keys()):
834 # Remove unused databases in the routing table
835 # Remove the routing table after a timeout = TTL + 30s
836 log.debug("[#0000] C: <ROUTING AGED> database=%s", database)
837 if self.routing_tables[database].should_be_purged_from_memory() and database != self.workspace_config.database:
838 del self.routing_tables[database]
839
840 return True
841
842 def _select_address(self, *, access_mode, database):
843 from neo4j.api import READ_ACCESS
844 """ Selects the address with the fewest in-use connections.
845 """
846 self.create_routing_table(database)
847 self.ensure_routing_table_is_fresh(access_mode=access_mode, database=database)
848 log.debug("[#0000] C: <ROUTING TABLE ENSURE FRESH> %r", self.routing_tables)
849 if access_mode == READ_ACCESS:
850 addresses = self.routing_tables[database].readers
851 else:
852 addresses = self.routing_tables[database].writers
853 addresses_by_usage = {}
854 for address in addresses:
855 addresses_by_usage.setdefault(self.in_use_connection_count(address), []).append(address)
856 if not addresses_by_usage:
857 if access_mode == READ_ACCESS:
858 raise ReadServiceUnavailable("No read service currently available")
859 else:
860 raise WriteServiceUnavailable("No write service currently available")
861 return choice(addresses_by_usage[min(addresses_by_usage)])
862
863 def acquire(self, access_mode=None, timeout=None, database=None):
864 if access_mode not in (WRITE_ACCESS, READ_ACCESS):
865 raise ClientError("Non valid 'access_mode'; {}".format(access_mode))
866 if not timeout:
867 raise ClientError("'timeout' must be a float larger than 0; {}".format(timeout))
868
869 from neo4j.api import check_access_mode
870 access_mode = check_access_mode(access_mode)
871 while True:
872 try:
873 # Get an address for a connection that have the fewest in-use connections.
874 address = self._select_address(access_mode=access_mode, database=database)
875 log.debug("[#0000] C: <ACQUIRE ADDRESS> database=%r address=%r", database, address)
876 except (ReadServiceUnavailable, WriteServiceUnavailable) as err:
877 raise SessionExpired("Failed to obtain connection towards '%s' server." % access_mode) from err
878 try:
879 connection = self._acquire(address, timeout=timeout) # should always be a resolved address
880 except ServiceUnavailable:
881 self.deactivate(address=address)
882 else:
883 return connection
884
885 def deactivate(self, address):
886 """ Deactivate an address from the connection pool,
887 if present, remove from the routing table and also closing
888 all idle connections to that address.
889 """
890 log.debug("[#0000] C: <ROUTING> Deactivating address %r", address)
891 # We use `discard` instead of `remove` here since the former
892 # will not fail if the address has already been removed.
893 for database in self.routing_tables.keys():
894 self.routing_tables[database].routers.discard(address)
895 self.routing_tables[database].readers.discard(address)
896 self.routing_tables[database].writers.discard(address)
897 log.debug("[#0000] C: <ROUTING> table=%r", self.routing_tables)
898 super(Neo4jPool, self).deactivate(address)
899
900 def on_write_failure(self, address):
901 """ Remove a writer address from the routing table, if present.
902 """
903 log.debug("[#0000] C: <ROUTING> Removing writer %r", address)
904 for database in self.routing_tables.keys():
905 self.routing_tables[database].writers.discard(address)
906 log.debug("[#0000] C: <ROUTING> table=%r", self.routing_tables)
907
908
909 def _connect(resolved_address, timeout, keep_alive):
910 """
911
912 :param resolved_address:
913 :param timeout: seconds
914 :param keep_alive: True or False
915 :return: socket object
916 """
917
918 s = None # The socket
919
920 try:
921 if len(resolved_address) == 2:
922 s = socket(AF_INET)
923 elif len(resolved_address) == 4:
924 s = socket(AF_INET6)
925 else:
926 raise ValueError("Unsupported address {!r}".format(resolved_address))
927 t = s.gettimeout()
928 if timeout:
929 s.settimeout(timeout)
930 log.debug("[#0000] C: <OPEN> %s", resolved_address)
931 s.connect(resolved_address)
932 s.settimeout(t)
933 keep_alive = 1 if keep_alive else 0
934 s.setsockopt(SOL_SOCKET, SO_KEEPALIVE, keep_alive)
935 except SocketTimeout:
936 log.debug("[#0000] C: <TIMEOUT> %s", resolved_address)
937 log.debug("[#0000] C: <CLOSE> %s", resolved_address)
938 s.close()
939 raise ServiceUnavailable("Timed out trying to establish connection to {!r}".format(resolved_address))
940 except OSError as error:
941 log.debug("[#0000] C: <ERROR> %s %s", type(error).__name__,
942 " ".join(map(repr, error.args)))
943 log.debug("[#0000] C: <CLOSE> %s", resolved_address)
944 s.close()
945 raise ServiceUnavailable("Failed to establish connection to {!r} (reason {})".format(resolved_address, error))
946 else:
947 return s
948
949
950 def _secure(s, host, ssl_context):
951 local_port = s.getsockname()[1]
952 # Secure the connection if an SSL context has been provided
953 if ssl_context:
954 log.debug("[#%04X] C: <SECURE> %s", local_port, host)
955 try:
956 sni_host = host if HAS_SNI and host else None
957 s = ssl_context.wrap_socket(s, server_hostname=sni_host)
958 except (SSLError, OSError) as cause:
959 s.close()
960 error = BoltSecurityError(message="Failed to establish encrypted connection.", address=(host, local_port))
961 error.__cause__ = cause
962 raise error
963 else:
964 # Check that the server provides a certificate
965 der_encoded_server_certificate = s.getpeercert(binary_form=True)
966 if der_encoded_server_certificate is None:
967 s.close()
968 raise BoltProtocolError("When using an encrypted socket, the server should always provide a certificate", address=(host, local_port))
969 return s
970
971
972 def _handshake(s, resolved_address):
973 """
974
975 :param s: Socket
976 :param resolved_address:
977
978 :return: (socket, version, client_handshake, server_response_data)
979 """
980 local_port = s.getsockname()[1]
981
982 # TODO: Optimize logging code
983 handshake = Bolt.get_handshake()
984 import struct
985 handshake = struct.unpack(">16B", handshake)
986 handshake = [handshake[i:i + 4] for i in range(0, len(handshake), 4)]
987
988 supported_versions = [("0x%02X%02X%02X%02X" % (vx[0], vx[1], vx[2], vx[3])) for vx in handshake]
989
990 log.debug("[#%04X] C: <MAGIC> 0x%08X", local_port, int.from_bytes(Bolt.MAGIC_PREAMBLE, byteorder="big"))
991 log.debug("[#%04X] C: <HANDSHAKE> %s %s %s %s", local_port, *supported_versions)
992
993 data = Bolt.MAGIC_PREAMBLE + Bolt.get_handshake()
994 s.sendall(data)
995
996 # Handle the handshake response
997 ready_to_read = False
998 while not ready_to_read:
999 ready_to_read, _, _ = select((s,), (), (), 1)
1000 try:
1001 data = s.recv(4)
1002 except OSError:
1003 raise ServiceUnavailable("Failed to read any data from server {!r} "
1004 "after connected".format(resolved_address))
1005 data_size = len(data)
1006 if data_size == 0:
1007 # If no data is returned after a successful select
1008 # response, the server has closed the connection
1009 log.debug("[#%04X] S: <CLOSE>", local_port)
1010 s.close()
1011 raise BoltHandshakeError("Connection to {address} closed without handshake response".format(address=resolved_address), address=resolved_address, request_data=handshake, response_data=None)
1012 if data_size != 4:
1013 # Some garbled data has been received
1014 log.debug("[#%04X] S: @*#!", local_port)
1015 s.close()
1016 raise BoltProtocolError("Expected four byte Bolt handshake response from %r, received %r instead; check for incorrect port number" % (resolved_address, data), address=resolved_address)
1017 elif data == b"HTTP":
1018 log.debug("[#%04X] S: <CLOSE>", local_port)
1019 s.close()
1020 raise ServiceUnavailable("Cannot to connect to Bolt service on {!r} "
1021 "(looks like HTTP)".format(resolved_address))
1022 agreed_version = data[-1], data[-2]
1023 log.debug("[#%04X] S: <HANDSHAKE> 0x%06X%02X", local_port, agreed_version[1], agreed_version[0])
1024 return s, agreed_version, handshake, data
1025
1026
1027 def connect(address, *, timeout, custom_resolver, ssl_context, keep_alive):
1028 """ Connect and perform a handshake and return a valid Connection object,
1029 assuming a protocol version can be agreed.
1030 """
1031 last_error = None
1032 # Establish a connection to the host and port specified
1033 # Catches refused connections see:
1034 # https://docs.python.org/2/library/errno.html
1035 log.debug("[#0000] C: <RESOLVE> %s", address)
1036
1037 for resolved_address in Address(address).resolve(resolver=custom_resolver):
1038 s = None
1039 try:
1040 host = address[0]
1041 s = _connect(resolved_address, timeout, keep_alive)
1042 s = _secure(s, host, ssl_context)
1043 return _handshake(s, address)
1044 except Exception as error:
1045 if s:
1046 s.close()
1047 last_error = error
1048 if last_error is None:
1049 raise ServiceUnavailable("Failed to resolve addresses for %s" % address)
1050 else:
1051 raise last_error
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from collections import deque
21 from ssl import SSLSocket
22 from time import perf_counter
23 from neo4j.api import (
24 Version,
25 READ_ACCESS,
26 DEFAULT_DATABASE,
27 )
28 from neo4j.io._common import (
29 Inbox,
30 Outbox,
31 Response,
32 InitResponse,
33 CommitResponse,
34 )
35 from neo4j.meta import get_user_agent
36 from neo4j.exceptions import (
37 AuthError,
38 ServiceUnavailable,
39 DatabaseUnavailable,
40 NotALeader,
41 ForbiddenOnReadOnlyDatabase,
42 SessionExpired,
43 ConfigurationError,
44 )
45 from neo4j._exceptions import (
46 BoltIncompleteCommitError,
47 BoltProtocolError,
48 )
49 from neo4j.packstream import (
50 Unpacker,
51 Packer,
52 )
53 from neo4j.io import (
54 Bolt,
55 BoltPool,
56 )
57 from neo4j.api import ServerInfo
58 from neo4j.addressing import Address
59
60 from logging import getLogger
61 log = getLogger("neo4j")
62
63
64 class Bolt3(Bolt):
65 """ Protocol handler for Bolt 3.
66
67 This is supported by Neo4j versions 3.5, 4.0, 4.1 and 4.2.
68 """
69
70 PROTOCOL_VERSION = Version(3, 0)
71
72 # The socket
73 in_use = False
74
75 # The socket
76 _closed = False
77
78 # The socket
79 _defunct = False
80
81 #: The pool of which this connection is a member
82 pool = None
83
84 def __init__(self, unresolved_address, sock, max_connection_lifetime, *, auth=None, user_agent=None, routing_context=None):
85 self.unresolved_address = unresolved_address
86 self.socket = sock
87 self.server_info = ServerInfo(Address(sock.getpeername()), self.PROTOCOL_VERSION)
88 self.outbox = Outbox()
89 self.inbox = Inbox(self.socket, on_error=self._set_defunct)
90 self.packer = Packer(self.outbox)
91 self.unpacker = Unpacker(self.inbox)
92 self.responses = deque()
93 self._max_connection_lifetime = max_connection_lifetime
94 self._creation_timestamp = perf_counter()
95 self.supports_multiple_results = False
96 self.supports_multiple_databases = False
97 self._is_reset = True
98 self.routing_context = routing_context
99
100 # Determine the user agent
101 if user_agent:
102 self.user_agent = user_agent
103 else:
104 self.user_agent = get_user_agent()
105
106 # Determine auth details
107 if not auth:
108 self.auth_dict = {}
109 elif isinstance(auth, tuple) and 2 <= len(auth) <= 3:
110 from neo4j import Auth
111 self.auth_dict = vars(Auth("basic", *auth))
112 else:
113 try:
114 self.auth_dict = vars(auth)
115 except (KeyError, TypeError):
116 raise AuthError("Cannot determine auth details from %r" % auth)
117
118 # Check for missing password
119 try:
120 credentials = self.auth_dict["credentials"]
121 except KeyError:
122 pass
123 else:
124 if credentials is None:
125 raise AuthError("Password cannot be None")
126
127 @property
128 def encrypted(self):
129 return isinstance(self.socket, SSLSocket)
130
131 @property
132 def der_encoded_server_certificate(self):
133 return self.socket.getpeercert(binary_form=True)
134
135 @property
136 def local_port(self):
137 try:
138 return self.socket.getsockname()[1]
139 except IOError:
140 return 0
141
142 def get_base_headers(self):
143 return {
144 "user_agent": self.user_agent,
145 }
146
147 def hello(self):
148 headers = self.get_base_headers()
149 headers.update(self.auth_dict)
150 logged_headers = dict(headers)
151 if "credentials" in logged_headers:
152 logged_headers["credentials"] = "*******"
153 log.debug("[#%04X] C: HELLO %r", self.local_port, logged_headers)
154 self._append(b"\x01", (headers,),
155 response=InitResponse(self, on_success=self.server_info._update_metadata))
156 self.send_all()
157 self.fetch_all()
158
159 def run(self, query, parameters=None, mode=None, bookmarks=None, metadata=None, timeout=None, db=None, **handlers):
160 if db is not None:
161 raise ConfigurationError("Database name parameter for selecting database is not supported in Bolt Protocol {!r}. Database name {!r}.".format(Bolt3.PROTOCOL_VERSION, db))
162 if not parameters:
163 parameters = {}
164 extra = {}
165 if mode in (READ_ACCESS, "r"):
166 extra["mode"] = "r" # It will default to mode "w" if nothing is specified
167 if bookmarks:
168 try:
169 extra["bookmarks"] = list(bookmarks)
170 except TypeError:
171 raise TypeError("Bookmarks must be provided within an iterable")
172 if metadata:
173 try:
174 extra["tx_metadata"] = dict(metadata)
175 except TypeError:
176 raise TypeError("Metadata must be coercible to a dict")
177 if timeout:
178 try:
179 extra["tx_timeout"] = int(1000 * timeout)
180 except TypeError:
181 raise TypeError("Timeout must be specified as a number of seconds")
182 fields = (query, parameters, extra)
183 log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields)))
184 if query.upper() == u"COMMIT":
185 self._append(b"\x10", fields, CommitResponse(self, **handlers))
186 else:
187 self._append(b"\x10", fields, Response(self, **handlers))
188 self._is_reset = False
189
190 def run_get_routing_table(self, on_success, on_failure, database=DEFAULT_DATABASE):
191 if database != DEFAULT_DATABASE:
192 raise ConfigurationError("Database name parameter for selecting database is not supported in Bolt Protocol {!r}. Database name {!r}. Server Agent {!r}.".format(Bolt3.PROTOCOL_VERSION, database, self.server_info.agent))
193 self.run(
194 "CALL dbms.cluster.routing.getRoutingTable($context)", # This is an internal procedure call. Only available if the Neo4j 3.5 is setup with clustering.
195 {"context": self.routing_context},
196 mode="r", # Bolt Protocol Version(3, 0) supports mode="r"
197 on_success=on_success,
198 on_failure=on_failure,
199 )
200
201 def discard(self, n=-1, qid=-1, **handlers):
202 # Just ignore n and qid, it is not supported in the Bolt 3 Protocol.
203 log.debug("[#%04X] C: DISCARD_ALL", self.local_port)
204 self._append(b"\x2F", (), Response(self, **handlers))
205
206 def pull(self, n=-1, qid=-1, **handlers):
207 # Just ignore n and qid, it is not supported in the Bolt 3 Protocol.
208 log.debug("[#%04X] C: PULL_ALL", self.local_port)
209 self._append(b"\x3F", (), Response(self, **handlers))
210 self._is_reset = False
211
212 def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None, db=None, **handlers):
213 if db is not None:
214 raise ConfigurationError("Database name parameter for selecting database is not supported in Bolt Protocol {!r}. Database name {!r}.".format(Bolt3.PROTOCOL_VERSION, db))
215 extra = {}
216 if mode in (READ_ACCESS, "r"):
217 extra["mode"] = "r" # It will default to mode "w" if nothing is specified
218 if bookmarks:
219 try:
220 extra["bookmarks"] = list(bookmarks)
221 except TypeError:
222 raise TypeError("Bookmarks must be provided within an iterable")
223 if metadata:
224 try:
225 extra["tx_metadata"] = dict(metadata)
226 except TypeError:
227 raise TypeError("Metadata must be coercible to a dict")
228 if timeout:
229 try:
230 extra["tx_timeout"] = int(1000 * timeout)
231 except TypeError:
232 raise TypeError("Timeout must be specified as a number of seconds")
233 log.debug("[#%04X] C: BEGIN %r", self.local_port, extra)
234 self._append(b"\x11", (extra,), Response(self, **handlers))
235 self._is_reset = False
236
237 def commit(self, **handlers):
238 log.debug("[#%04X] C: COMMIT", self.local_port)
239 self._append(b"\x12", (), CommitResponse(self, **handlers))
240
241 def rollback(self, **handlers):
242 log.debug("[#%04X] C: ROLLBACK", self.local_port)
243 self._append(b"\x13", (), Response(self, **handlers))
244
245 def _append(self, signature, fields=(), response=None):
246 """ Add a message to the outgoing queue.
247
248 :arg signature: the signature of the message
249 :arg fields: the fields of the message as a tuple
250 :arg response: a response object to handle callbacks
251 """
252 self.packer.pack_struct(signature, fields)
253 self.outbox.chunk()
254 self.outbox.chunk()
255 self.responses.append(response)
256
257 def reset(self):
258 """ Add a RESET message to the outgoing queue, send
259 it and consume all remaining messages.
260 """
261
262 def fail(metadata):
263 raise BoltProtocolError("RESET failed %r" % metadata, address=self.unresolved_address)
264
265 log.debug("[#%04X] C: RESET", self.local_port)
266 self._append(b"\x0F", response=Response(self, on_failure=fail))
267 self.send_all()
268 self.fetch_all()
269 self._is_reset = True
270
271 def _send_all(self):
272 data = self.outbox.view()
273 if data:
274 self.socket.sendall(data)
275 self.outbox.clear()
276
277 def send_all(self):
278 """ Send all queued messages to the server.
279 """
280 if self.closed():
281 raise ServiceUnavailable("Failed to write to closed connection {!r} ({!r})".format(
282 self.unresolved_address, self.server_info.address))
283
284 if self.defunct():
285 raise ServiceUnavailable("Failed to write to defunct connection {!r} ({!r})".format(
286 self.unresolved_address, self.server_info.address))
287
288 try:
289 self._send_all()
290 except (IOError, OSError) as error:
291 log.error("Failed to write data to connection "
292 "{!r} ({!r}); ({!r})".
293 format(self.unresolved_address,
294 self.server_info.address,
295 "; ".join(map(repr, error.args))))
296 if self.pool:
297 self.pool.deactivate(address=self.unresolved_address)
298 raise
299
300 def fetch_message(self):
301 """ Receive at least one message from the server, if available.
302
303 :return: 2-tuple of number of detail messages and number of summary
304 messages fetched
305 """
306 if self._closed:
307 raise ServiceUnavailable("Failed to read from closed connection {!r} ({!r})".format(
308 self.unresolved_address, self.server_info.address))
309
310 if self._defunct:
311 raise ServiceUnavailable("Failed to read from defunct connection {!r} ({!r})".format(
312 self.unresolved_address, self.server_info.address))
313
314 if not self.responses:
315 return 0, 0
316
317 # Receive exactly one message
318 try:
319 details, summary_signature, summary_metadata = next(self.inbox)
320 except (IOError, OSError) as error:
321 log.error("Failed to read data from connection "
322 "{!r} ({!r}); ({!r})".
323 format(self.unresolved_address,
324 self.server_info.address,
325 "; ".join(map(repr, error.args))))
326 if self.pool:
327 self.pool.deactivate(address=self.unresolved_address)
328 raise
329
330 if details:
331 log.debug("[#%04X] S: RECORD * %d", self.local_port, len(details)) # Do not log any data
332 self.responses[0].on_records(details)
333
334 if summary_signature is None:
335 return len(details), 0
336
337 response = self.responses.popleft()
338 response.complete = True
339 if summary_signature == b"\x70":
340 log.debug("[#%04X] S: SUCCESS %r", self.local_port, summary_metadata)
341 response.on_success(summary_metadata or {})
342 elif summary_signature == b"\x7E":
343 log.debug("[#%04X] S: IGNORED", self.local_port)
344 response.on_ignored(summary_metadata or {})
345 elif summary_signature == b"\x7F":
346 log.debug("[#%04X] S: FAILURE %r", self.local_port, summary_metadata)
347 try:
348 response.on_failure(summary_metadata or {})
349 except (ServiceUnavailable, DatabaseUnavailable):
350 if self.pool:
351 self.pool.deactivate(address=self.unresolved_address),
352 raise
353 except (NotALeader, ForbiddenOnReadOnlyDatabase):
354 if self.pool:
355 self.pool.on_write_failure(address=self.unresolved_address),
356 raise
357 else:
358 raise BoltProtocolError("Unexpected response message with signature %02X" % summary_signature, address=self.unresolved_address)
359
360 return len(details), 1
361
362 def _set_defunct(self, error=None):
363 direct_driver = isinstance(self.pool, BoltPool)
364
365 message = ("Failed to read from defunct connection {!r} ({!r})".format(
366 self.unresolved_address, self.server_info.address))
367
368 if error:
369 log.error(str(error))
370 log.error(message)
371 # We were attempting to receive data but the connection
372 # has unexpectedly terminated. So, we need to close the
373 # connection from the client side, and remove the address
374 # from the connection pool.
375 self._defunct = True
376 self.close()
377 if self.pool:
378 self.pool.deactivate(address=self.unresolved_address)
379 # Iterate through the outstanding responses, and if any correspond
380 # to COMMIT requests then raise an error to signal that we are
381 # unable to confirm that the COMMIT completed successfully.
382 for response in self.responses:
383 if isinstance(response, CommitResponse):
384 if error:
385 raise BoltIncompleteCommitError(message, address=None) from error
386 else:
387 raise BoltIncompleteCommitError(message, address=None)
388
389 if direct_driver:
390 if error:
391 raise ServiceUnavailable(message) from error
392 else:
393 raise ServiceUnavailable(message)
394 else:
395 if error:
396 raise SessionExpired(message) from error
397 else:
398 raise SessionExpired(message)
399
400 def timedout(self):
401 return 0 <= self._max_connection_lifetime <= perf_counter() - self._creation_timestamp
402
403 def fetch_all(self):
404 """ Fetch all outstanding messages.
405
406 :return: 2-tuple of number of detail messages and number of summary
407 messages fetched
408 """
409 detail_count = summary_count = 0
410 while self.responses:
411 response = self.responses[0]
412 while not response.complete:
413 detail_delta, summary_delta = self.fetch_message()
414 detail_count += detail_delta
415 summary_count += summary_delta
416 return detail_count, summary_count
417
418 def close(self):
419 """ Close the connection.
420 """
421 if not self._closed:
422 if not self._defunct:
423 log.debug("[#%04X] C: GOODBYE", self.local_port)
424 self._append(b"\x02", ())
425 try:
426 self._send_all()
427 except:
428 pass
429 log.debug("[#%04X] C: <CLOSE>", self.local_port)
430 try:
431 self.socket.close()
432 except IOError:
433 pass
434 finally:
435 self._closed = True
436
437 def closed(self):
438 return self._closed
439
440 def defunct(self):
441 return self._defunct
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from collections import deque
21 from ssl import SSLSocket
22 from time import perf_counter
23 from neo4j.api import (
24 Version,
25 READ_ACCESS,
26 DEFAULT_DATABASE,
27 SYSTEM_DATABASE,
28 )
29 from neo4j.io._common import (
30 Inbox,
31 Outbox,
32 Response,
33 InitResponse,
34 CommitResponse,
35 )
36 from neo4j.meta import get_user_agent
37 from neo4j.exceptions import (
38 AuthError,
39 ServiceUnavailable,
40 DatabaseUnavailable,
41 NotALeader,
42 ForbiddenOnReadOnlyDatabase,
43 SessionExpired,
44 )
45 from neo4j._exceptions import (
46 BoltIncompleteCommitError,
47 BoltProtocolError,
48 )
49 from neo4j.packstream import (
50 Unpacker,
51 Packer,
52 )
53 from neo4j.io import (
54 Bolt,
55 BoltPool,
56 )
57 from neo4j.api import ServerInfo
58 from neo4j.addressing import Address
59
60 from logging import getLogger
61 log = getLogger("neo4j")
62
63
64 class Bolt4x0(Bolt):
65 """ Protocol handler for Bolt 4.0.
66
67 This is supported by Neo4j versions 4.0, 4.1 and 4.2.
68 """
69
70 PROTOCOL_VERSION = Version(4, 0)
71
72 # The socket
73 in_use = False
74
75 # The socket
76 _closed = False
77
78 # The socket
79 _defunct = False
80
81 #: The pool of which this connection is a member
82 pool = None
83
84 def __init__(self, unresolved_address, sock, max_connection_lifetime, *, auth=None, user_agent=None, routing_context=None):
85 self.unresolved_address = unresolved_address
86 self.socket = sock
87 self.server_info = ServerInfo(Address(sock.getpeername()), self.PROTOCOL_VERSION)
88 self.outbox = Outbox()
89 self.inbox = Inbox(self.socket, on_error=self._set_defunct)
90 self.packer = Packer(self.outbox)
91 self.unpacker = Unpacker(self.inbox)
92 self.responses = deque()
93 self._max_connection_lifetime = max_connection_lifetime # self.pool_config.max_connection_lifetime
94 self._creation_timestamp = perf_counter()
95 self.supports_multiple_results = True
96 self.supports_multiple_databases = True
97 self._is_reset = True
98 self.routing_context = routing_context
99
100 # Determine the user agent
101 if user_agent:
102 self.user_agent = user_agent
103 else:
104 self.user_agent = get_user_agent()
105
106 # Determine auth details
107 if not auth:
108 self.auth_dict = {}
109 elif isinstance(auth, tuple) and 2 <= len(auth) <= 3:
110 from neo4j import Auth
111 self.auth_dict = vars(Auth("basic", *auth))
112 else:
113 try:
114 self.auth_dict = vars(auth)
115 except (KeyError, TypeError):
116 raise AuthError("Cannot determine auth details from %r" % auth)
117
118 # Check for missing password
119 try:
120 credentials = self.auth_dict["credentials"]
121 except KeyError:
122 pass
123 else:
124 if credentials is None:
125 raise AuthError("Password cannot be None")
126
127 @property
128 def encrypted(self):
129 return isinstance(self.socket, SSLSocket)
130
131 @property
132 def der_encoded_server_certificate(self):
133 return self.socket.getpeercert(binary_form=True)
134
135 @property
136 def local_port(self):
137 try:
138 return self.socket.getsockname()[1]
139 except IOError:
140 return 0
141
142 def get_base_headers(self):
143 return {
144 "user_agent": self.user_agent,
145 }
146
147 def hello(self):
148 headers = self.get_base_headers()
149 headers.update(self.auth_dict)
150 logged_headers = dict(headers)
151 if "credentials" in logged_headers:
152 logged_headers["credentials"] = "*******"
153 log.debug("[#%04X] C: HELLO %r", self.local_port, logged_headers)
154 self._append(b"\x01", (headers,),
155 response=InitResponse(self, on_success=self.server_info._update_metadata))
156 self.send_all()
157 self.fetch_all()
158
159 def run(self, query, parameters=None, mode=None, bookmarks=None, metadata=None, timeout=None, db=None, **handlers):
160 if not parameters:
161 parameters = {}
162 extra = {}
163 if mode in (READ_ACCESS, "r"):
164 extra["mode"] = "r" # It will default to mode "w" if nothing is specified
165 if db:
166 extra["db"] = db
167 if bookmarks:
168 try:
169 extra["bookmarks"] = list(bookmarks)
170 except TypeError:
171 raise TypeError("Bookmarks must be provided within an iterable")
172 if metadata:
173 try:
174 extra["tx_metadata"] = dict(metadata)
175 except TypeError:
176 raise TypeError("Metadata must be coercible to a dict")
177 if timeout:
178 try:
179 extra["tx_timeout"] = int(1000 * timeout)
180 except TypeError:
181 raise TypeError("Timeout must be specified as a number of seconds")
182 fields = (query, parameters, extra)
183 log.debug("[#%04X] C: RUN %s", self.local_port, " ".join(map(repr, fields)))
184 if query.upper() == u"COMMIT":
185 self._append(b"\x10", fields, CommitResponse(self, **handlers))
186 else:
187 self._append(b"\x10", fields, Response(self, **handlers))
188 self._is_reset = False
189
190 def run_get_routing_table(self, on_success, on_failure, database=DEFAULT_DATABASE):
191 if database == DEFAULT_DATABASE:
192 self.run(
193 "CALL dbms.routing.getRoutingTable($context)",
194 {"context": self.routing_context},
195 mode="r",
196 db=SYSTEM_DATABASE,
197 on_success=on_success,
198 on_failure=on_failure,
199 )
200 else:
201 self.run(
202 "CALL dbms.routing.getRoutingTable($context, $database)",
203 {"context": self.routing_context, "database": database},
204 mode="r",
205 db=SYSTEM_DATABASE,
206 on_success=on_success,
207 on_failure=on_failure,
208 )
209
210 def discard(self, n=-1, qid=-1, **handlers):
211 extra = {"n": n}
212 if qid != -1:
213 extra["qid"] = qid
214 log.debug("[#%04X] C: DISCARD %r", self.local_port, extra)
215 self._append(b"\x2F", (extra,), Response(self, **handlers))
216
217 def pull(self, n=-1, qid=-1, **handlers):
218 extra = {"n": n}
219 if qid != -1:
220 extra["qid"] = qid
221 log.debug("[#%04X] C: PULL %r", self.local_port, extra)
222 self._append(b"\x3F", (extra,), Response(self, **handlers))
223 self._is_reset = False
224
225 def begin(self, mode=None, bookmarks=None, metadata=None, timeout=None,
226 db=None, **handlers):
227 extra = {}
228 if mode in (READ_ACCESS, "r"):
229 extra["mode"] = "r" # It will default to mode "w" if nothing is specified
230 if db:
231 extra["db"] = db
232 if bookmarks:
233 try:
234 extra["bookmarks"] = list(bookmarks)
235 except TypeError:
236 raise TypeError("Bookmarks must be provided within an iterable")
237 if metadata:
238 try:
239 extra["tx_metadata"] = dict(metadata)
240 except TypeError:
241 raise TypeError("Metadata must be coercible to a dict")
242 if timeout:
243 try:
244 extra["tx_timeout"] = int(1000 * timeout)
245 except TypeError:
246 raise TypeError("Timeout must be specified as a number of seconds")
247 log.debug("[#%04X] C: BEGIN %r", self.local_port, extra)
248 self._append(b"\x11", (extra,), Response(self, **handlers))
249 self._is_reset = False
250
251 def commit(self, **handlers):
252 log.debug("[#%04X] C: COMMIT", self.local_port)
253 self._append(b"\x12", (), CommitResponse(self, **handlers))
254
255 def rollback(self, **handlers):
256 log.debug("[#%04X] C: ROLLBACK", self.local_port)
257 self._append(b"\x13", (), Response(self, **handlers))
258
259 def _append(self, signature, fields=(), response=None):
260 """ Add a message to the outgoing queue.
261
262 :arg signature: the signature of the message
263 :arg fields: the fields of the message as a tuple
264 :arg response: a response object to handle callbacks
265 """
266 self.packer.pack_struct(signature, fields)
267 self.outbox.chunk()
268 self.outbox.chunk()
269 self.responses.append(response)
270
271 def reset(self):
272 """ Add a RESET message to the outgoing queue, send
273 it and consume all remaining messages.
274 """
275
276 def fail(metadata):
277 raise BoltProtocolError("RESET failed %r" % metadata, self.unresolved_address)
278
279 log.debug("[#%04X] C: RESET", self.local_port)
280 self._append(b"\x0F", response=Response(self, on_failure=fail))
281 self.send_all()
282 self.fetch_all()
283 self._is_reset = True
284
285 def _send_all(self):
286 data = self.outbox.view()
287 if data:
288 self.socket.sendall(data)
289 self.outbox.clear()
290
291 def send_all(self):
292 """ Send all queued messages to the server.
293 """
294 if self.closed():
295 raise ServiceUnavailable("Failed to write to closed connection {!r} ({!r})".format(
296 self.unresolved_address, self.server_info.address))
297
298 if self.defunct():
299 raise ServiceUnavailable("Failed to write to defunct connection {!r} ({!r})".format(
300 self.unresolved_address, self.server_info.address))
301
302 try:
303 self._send_all()
304 except (IOError, OSError) as error:
305 log.error("Failed to write data to connection "
306 "{!r} ({!r}); ({!r})".
307 format(self.unresolved_address,
308 self.server_info.address,
309 "; ".join(map(repr, error.args))))
310 if self.pool:
311 self.pool.deactivate(address=self.unresolved_address)
312 raise
313
314 def fetch_message(self):
315 """ Receive at least one message from the server, if available.
316
317 :return: 2-tuple of number of detail messages and number of summary
318 messages fetched
319 """
320 if self._closed:
321 raise ServiceUnavailable("Failed to read from closed connection {!r} ({!r})".format(
322 self.unresolved_address, self.server_info.address))
323
324 if self._defunct:
325 raise ServiceUnavailable("Failed to read from defunct connection {!r} ({!r})".format(
326 self.unresolved_address, self.server_info.address))
327
328 if not self.responses:
329 return 0, 0
330
331 # Receive exactly one message
332 try:
333 details, summary_signature, summary_metadata = next(self.inbox)
334 except (IOError, OSError) as error:
335 log.error("Failed to read data from connection "
336 "{!r} ({!r}); ({!r})".
337 format(self.unresolved_address,
338 self.server_info.address,
339 "; ".join(map(repr, error.args))))
340 if self.pool:
341 self.pool.deactivate(address=self.unresolved_address)
342 raise
343
344 if details:
345 log.debug("[#%04X] S: RECORD * %d", self.local_port, len(details)) # Do not log any data
346 self.responses[0].on_records(details)
347
348 if summary_signature is None:
349 return len(details), 0
350
351 response = self.responses.popleft()
352 response.complete = True
353 if summary_signature == b"\x70":
354 log.debug("[#%04X] S: SUCCESS %r", self.local_port, summary_metadata)
355 response.on_success(summary_metadata or {})
356 elif summary_signature == b"\x7E":
357 log.debug("[#%04X] S: IGNORED", self.local_port)
358 response.on_ignored(summary_metadata or {})
359 elif summary_signature == b"\x7F":
360 log.debug("[#%04X] S: FAILURE %r", self.local_port, summary_metadata)
361 try:
362 response.on_failure(summary_metadata or {})
363 except (ServiceUnavailable, DatabaseUnavailable):
364 if self.pool:
365 self.pool.deactivate(address=self.unresolved_address),
366 raise
367 except (NotALeader, ForbiddenOnReadOnlyDatabase):
368 if self.pool:
369 self.pool.on_write_failure(address=self.unresolved_address),
370 raise
371 else:
372 raise BoltProtocolError("Unexpected response message with signature "
373 "%02X" % ord(summary_signature), self.unresolved_address)
374
375 return len(details), 1
376
377 def _set_defunct(self, error=None):
378 direct_driver = isinstance(self.pool, BoltPool)
379
380 message = ("Failed to read from defunct connection {!r} ({!r})".format(
381 self.unresolved_address, self.server_info.address))
382
383 if error:
384 log.error(str(error))
385 log.error(message)
386 # We were attempting to receive data but the connection
387 # has unexpectedly terminated. So, we need to close the
388 # connection from the client side, and remove the address
389 # from the connection pool.
390 self._defunct = True
391 self.close()
392 if self.pool:
393 self.pool.deactivate(address=self.unresolved_address)
394 # Iterate through the outstanding responses, and if any correspond
395 # to COMMIT requests then raise an error to signal that we are
396 # unable to confirm that the COMMIT completed successfully.
397 for response in self.responses:
398 if isinstance(response, CommitResponse):
399 if error:
400 raise BoltIncompleteCommitError(message, address=None) from error
401 else:
402 raise BoltIncompleteCommitError(message, address=None)
403
404 if direct_driver:
405 if error:
406 raise ServiceUnavailable(message) from error
407 else:
408 raise ServiceUnavailable(message)
409 else:
410 if error:
411 raise SessionExpired(message) from error
412 else:
413 raise SessionExpired(message)
414
415 def timedout(self):
416 return 0 <= self._max_connection_lifetime <= perf_counter() - self._creation_timestamp
417
418 def fetch_all(self):
419 """ Fetch all outstanding messages.
420
421 :return: 2-tuple of number of detail messages and number of summary
422 messages fetched
423 """
424 detail_count = summary_count = 0
425 while self.responses:
426 response = self.responses[0]
427 while not response.complete:
428 detail_delta, summary_delta = self.fetch_message()
429 detail_count += detail_delta
430 summary_count += summary_delta
431 return detail_count, summary_count
432
433 def close(self):
434 """ Close the connection.
435 """
436 if not self._closed:
437 if not self._defunct:
438 log.debug("[#%04X] C: GOODBYE", self.local_port)
439 self._append(b"\x02", ())
440 try:
441 self._send_all()
442 except:
443 pass
444 log.debug("[#%04X] C: <CLOSE>", self.local_port)
445 try:
446 self.socket.close()
447 except IOError:
448 pass
449 finally:
450 self._closed = True
451
452 def closed(self):
453 return self._closed
454
455 def defunct(self):
456 return self._defunct
457
458
459 class Bolt4x1(Bolt4x0):
460 """ Protocol handler for Bolt 4.1.
461
462 This is supported by Neo4j versions 4.1 and 4.2.
463 """
464
465 PROTOCOL_VERSION = Version(4, 1)
466
467 def get_base_headers(self):
468 """ Bolt 4.1 passes the routing context, originally taken from
469 the URI, into the connection initialisation message. This
470 enables server-side routing to propagate the same behaviour
471 through its driver.
472 """
473 return {
474 "user_agent": self.user_agent,
475 "routing": self.routing_context,
476 }
477
478
479 class Bolt4x2(Bolt4x1):
480 """ Protocol handler for Bolt 4.2.
481
482 This is supported by Neo4j version 4.2.
483 """
484
485 PROTOCOL_VERSION = Version(4, 2)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from struct import pack as struct_pack
22
23 from neo4j.exceptions import (
24 Neo4jError,
25 AuthError,
26 ServiceUnavailable,
27 )
28 from neo4j.packstream import (
29 UnpackableBuffer,
30 Unpacker,
31 )
32
33 import logging
34 log = logging.getLogger("neo4j")
35
36
37 class MessageInbox:
38
39 def __init__(self, s, on_error):
40 self.on_error = on_error
41 self._messages = self._yield_messages(s)
42
43 def _yield_messages(self, sock):
44 try:
45 buffer = UnpackableBuffer()
46 unpacker = Unpacker(buffer)
47 chunk_size = 0
48 while True:
49
50 while chunk_size == 0:
51 # Determine the chunk size and skip noop
52 buffer.receive(sock, 2)
53 chunk_size = buffer.pop_u16()
54 if chunk_size == 0:
55 log.debug("[#%04X] S: <NOOP>", sock.getsockname()[1])
56
57 buffer.receive(sock, chunk_size + 2)
58 chunk_size = buffer.pop_u16()
59
60 if chunk_size == 0:
61 # chunk_size was the end marker for the message
62 size, tag = unpacker.unpack_structure_header()
63 fields = [unpacker.unpack() for _ in range(size)]
64 yield tag, fields
65 # Reset for new message
66 unpacker.reset()
67
68 except OSError as error:
69 self.on_error(error)
70
71 def pop(self):
72 return next(self._messages)
73
74
75 class Inbox(MessageInbox):
76
77 def __next__(self):
78 tag, fields = self.pop()
79 if tag == b"\x71":
80 return fields, None, None
81 elif fields:
82 return [], tag, fields[0]
83 else:
84 return [], tag, None
85
86
87 class Outbox:
88
89 def __init__(self, capacity=8192, max_chunk_size=16384):
90 self._max_chunk_size = max_chunk_size
91 self._header = 0
92 self._start = 2
93 self._end = 2
94 self._data = bytearray(capacity)
95
96 def max_chunk_size(self):
97 return self._max_chunk_size
98
99 def clear(self):
100 self._header = 0
101 self._start = 2
102 self._end = 2
103 self._data[0:2] = b"\x00\x00"
104
105 def write(self, b):
106 to_write = len(b)
107 max_chunk_size = self._max_chunk_size
108 pos = 0
109 while to_write > 0:
110 chunk_size = self._end - self._start
111 remaining = max_chunk_size - chunk_size
112 if remaining == 0 or remaining < to_write <= max_chunk_size:
113 self.chunk()
114 else:
115 wrote = min(to_write, remaining)
116 new_end = self._end + wrote
117 self._data[self._end:new_end] = b[pos:pos+wrote]
118 self._end = new_end
119 pos += wrote
120 new_chunk_size = self._end - self._start
121 self._data[self._header:(self._header + 2)] = struct_pack(">H", new_chunk_size)
122 to_write -= wrote
123
124 def chunk(self):
125 self._header = self._end
126 self._start = self._header + 2
127 self._end = self._start
128 self._data[self._header:self._start] = b"\x00\x00"
129
130 def view(self):
131 end = self._end
132 chunk_size = end - self._start
133 if chunk_size == 0:
134 return memoryview(self._data[:self._header])
135 else:
136 return memoryview(self._data[:end])
137
138
139 class Response:
140 """ Subscriber object for a full response (zero or
141 more detail messages followed by one summary message).
142 """
143
144 def __init__(self, connection, **handlers):
145 self.connection = connection
146 self.handlers = handlers
147 self.complete = False
148
149 def on_records(self, records):
150 """ Called when one or more RECORD messages have been received.
151 """
152 handler = self.handlers.get("on_records")
153 if callable(handler):
154 handler(records)
155
156 def on_success(self, metadata):
157 """ Called when a SUCCESS message has been received.
158 """
159 handler = self.handlers.get("on_success")
160 if callable(handler):
161 handler(metadata)
162
163 if not metadata.get("has_more"):
164 handler = self.handlers.get("on_summary")
165 if callable(handler):
166 handler()
167
168 def on_failure(self, metadata):
169 """ Called when a FAILURE message has been received.
170 """
171 self.connection.reset()
172 handler = self.handlers.get("on_failure")
173 if callable(handler):
174 handler(metadata)
175 handler = self.handlers.get("on_summary")
176 if callable(handler):
177 handler()
178 raise Neo4jError.hydrate(**metadata)
179
180 def on_ignored(self, metadata=None):
181 """ Called when an IGNORED message has been received.
182 """
183 handler = self.handlers.get("on_ignored")
184 if callable(handler):
185 handler(metadata)
186 handler = self.handlers.get("on_summary")
187 if callable(handler):
188 handler()
189
190
191 class InitResponse(Response):
192
193 def on_failure(self, metadata):
194 code = metadata.get("code")
195 message = metadata.get("message", "Connection initialisation failed")
196 if code == "Neo.ClientError.Security.Unauthorized":
197 raise AuthError(message)
198 else:
199 raise ServiceUnavailable(message)
200
201
202 class CommitResponse(Response):
203
204 pass
00 #!/usr/bin/env python
11 # -*- encoding: utf-8 -*-
22
3 # Copyright (c) 2002-2019 "Neo4j,"
3 # Copyright (c) 2002-2020 "Neo4j,"
44 # Neo4j Sweden AB [http://neo4j.com]
55 #
66 # This file is part of Neo4j.
2020
2121 # Can be automatically overridden in builds
2222 package = "neo4j"
23 version = "1.7.0.dev0"
23 version = "4.2.dev0"
24
25
26 def get_user_agent():
27 """ Obtain the default user agent string sent to the server after
28 a successful handshake.
29 """
30 from sys import platform, version_info
31 template = "neo4j-python/{} Python/{}.{}.{}-{}-{} ({})"
32 fields = (version,) + tuple(version_info) + (platform,)
33 return template.format(*fields)
34
35
36 def deprecated(message):
37 """ Decorator for deprecating functions and methods.
38
39 ::
40
41 @deprecated("'foo' has been deprecated in favour of 'bar'")
42 def foo(x):
43 pass
44
45 """
46 def f__(f):
47 def f_(*args, **kwargs):
48 from warnings import warn
49 warn(message, category=DeprecationWarning, stacklevel=2)
50 return f(*args, **kwargs)
51 f_.__name__ = f.__name__
52 f_.__doc__ = f.__doc__
53 f_.__dict__.update(f.__dict__)
54 return f_
55 return f__
56
57
58 class ExperimentalWarning(Warning):
59 """ Base class for warnings about experimental features.
60 """
61
62
63 def experimental(message):
64 """ Decorator for tagging experimental functions and methods.
65
66 ::
67
68 @experimental("'foo' is an experimental function and may be "
69 "removed in a future release")
70 def foo(x):
71 pass
72
73 """
74 def f__(f):
75 def f_(*args, **kwargs):
76 from warnings import warn
77 warn(message, category=ExperimentalWarning, stacklevel=2)
78 return f(*args, **kwargs)
79 f_.__name__ = f.__name__
80 f_.__doc__ = f.__doc__
81 f_.__dict__.update(f.__dict__)
82 return f_
83 return f__
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from codecs import decode
22 from io import BytesIO
23 from struct import pack as struct_pack, unpack as struct_unpack
24
25 PACKED_UINT_8 = [struct_pack(">B", value) for value in range(0x100)]
26 PACKED_UINT_16 = [struct_pack(">H", value) for value in range(0x10000)]
27
28 UNPACKED_UINT_8 = {bytes(bytearray([x])): x for x in range(0x100)}
29 UNPACKED_UINT_16 = {struct_pack(">H", x): x for x in range(0x10000)}
30
31 UNPACKED_MARKERS = {b"\xC0": None, b"\xC2": False, b"\xC3": True}
32 UNPACKED_MARKERS.update({bytes(bytearray([z])): z for z in range(0x00, 0x80)})
33 UNPACKED_MARKERS.update({bytes(bytearray([z + 256])): z for z in range(-0x10, 0x00)})
34
35
36 INT64_MIN = -(2 ** 63)
37 INT64_MAX = 2 ** 63
38
39
40 EndOfStream = object()
41
42
43 class Structure:
44
45 def __init__(self, tag, *fields):
46 self.tag = tag
47 self.fields = list(fields)
48
49 def __repr__(self):
50 return "Structure[0x%02X](%s)" % (ord(self.tag), ", ".join(map(repr, self.fields)))
51
52 def __eq__(self, other):
53 try:
54 return self.tag == other.tag and self.fields == other.fields
55 except AttributeError:
56 return False
57
58 def __ne__(self, other):
59 return not self.__eq__(other)
60
61 def __len__(self):
62 return len(self.fields)
63
64 def __getitem__(self, key):
65 return self.fields[key]
66
67 def __setitem__(self, key, value):
68 self.fields[key] = value
69
70
71 class Packer:
72
73 def __init__(self, stream):
74 self.stream = stream
75 self._write = self.stream.write
76
77 def pack_raw(self, data):
78 self._write(data)
79
80 def pack(self, value):
81 return self._pack(value)
82
83 def _pack(self, value):
84 write = self._write
85
86 # None
87 if value is None:
88 write(b"\xC0") # NULL
89
90 # Boolean
91 elif value is True:
92 write(b"\xC3")
93 elif value is False:
94 write(b"\xC2")
95
96 # Float (only double precision is supported)
97 elif isinstance(value, float):
98 write(b"\xC1")
99 write(struct_pack(">d", value))
100
101 # Integer
102 elif isinstance(value, int):
103 if -0x10 <= value < 0x80:
104 write(PACKED_UINT_8[value % 0x100])
105 elif -0x80 <= value < -0x10:
106 write(b"\xC8")
107 write(PACKED_UINT_8[value % 0x100])
108 elif -0x8000 <= value < 0x8000:
109 write(b"\xC9")
110 write(PACKED_UINT_16[value % 0x10000])
111 elif -0x80000000 <= value < 0x80000000:
112 write(b"\xCA")
113 write(struct_pack(">i", value))
114 elif INT64_MIN <= value < INT64_MAX:
115 write(b"\xCB")
116 write(struct_pack(">q", value))
117 else:
118 raise OverflowError("Integer %s out of range" % value)
119
120 # String
121 elif isinstance(value, str):
122 encoded = value.encode("utf-8")
123 self.pack_string_header(len(encoded))
124 self.pack_raw(encoded)
125
126 # Bytes
127 elif isinstance(value, bytes):
128 self.pack_bytes_header(len(value))
129 self.pack_raw(value)
130 elif isinstance(value, bytearray):
131 self.pack_bytes_header(len(value))
132 self.pack_raw(bytes(value))
133
134 # List
135 elif isinstance(value, list):
136 self.pack_list_header(len(value))
137 for item in value:
138 self._pack(item)
139
140 # Map
141 elif isinstance(value, dict):
142 self.pack_map_header(len(value))
143 for key, item in value.items():
144 self._pack(key)
145 self._pack(item)
146
147 # Structure
148 elif isinstance(value, Structure):
149 self.pack_struct(value.tag, value.fields)
150
151 # Other
152 else:
153 raise ValueError("Values of type %s are not supported" % type(value))
154
155 def pack_bytes_header(self, size):
156 write = self._write
157 if size < 0x100:
158 write(b"\xCC")
159 write(PACKED_UINT_8[size])
160 elif size < 0x10000:
161 write(b"\xCD")
162 write(PACKED_UINT_16[size])
163 elif size < 0x100000000:
164 write(b"\xCE")
165 write(struct_pack(">I", size))
166 else:
167 raise OverflowError("Bytes header size out of range")
168
169 def pack_string_header(self, size):
170 write = self._write
171 if size == 0x00:
172 write(b"\x80")
173 elif size == 0x01:
174 write(b"\x81")
175 elif size == 0x02:
176 write(b"\x82")
177 elif size == 0x03:
178 write(b"\x83")
179 elif size == 0x04:
180 write(b"\x84")
181 elif size == 0x05:
182 write(b"\x85")
183 elif size == 0x06:
184 write(b"\x86")
185 elif size == 0x07:
186 write(b"\x87")
187 elif size == 0x08:
188 write(b"\x88")
189 elif size == 0x09:
190 write(b"\x89")
191 elif size == 0x0A:
192 write(b"\x8A")
193 elif size == 0x0B:
194 write(b"\x8B")
195 elif size == 0x0C:
196 write(b"\x8C")
197 elif size == 0x0D:
198 write(b"\x8D")
199 elif size == 0x0E:
200 write(b"\x8E")
201 elif size == 0x0F:
202 write(b"\x8F")
203 elif size < 0x100:
204 write(b"\xD0")
205 write(PACKED_UINT_8[size])
206 elif size < 0x10000:
207 write(b"\xD1")
208 write(PACKED_UINT_16[size])
209 elif size < 0x100000000:
210 write(b"\xD2")
211 write(struct_pack(">I", size))
212 else:
213 raise OverflowError("String header size out of range")
214
215 def pack_list_header(self, size):
216 write = self._write
217 if size == 0x00:
218 write(b"\x90")
219 elif size == 0x01:
220 write(b"\x91")
221 elif size == 0x02:
222 write(b"\x92")
223 elif size == 0x03:
224 write(b"\x93")
225 elif size == 0x04:
226 write(b"\x94")
227 elif size == 0x05:
228 write(b"\x95")
229 elif size == 0x06:
230 write(b"\x96")
231 elif size == 0x07:
232 write(b"\x97")
233 elif size == 0x08:
234 write(b"\x98")
235 elif size == 0x09:
236 write(b"\x99")
237 elif size == 0x0A:
238 write(b"\x9A")
239 elif size == 0x0B:
240 write(b"\x9B")
241 elif size == 0x0C:
242 write(b"\x9C")
243 elif size == 0x0D:
244 write(b"\x9D")
245 elif size == 0x0E:
246 write(b"\x9E")
247 elif size == 0x0F:
248 write(b"\x9F")
249 elif size < 0x100:
250 write(b"\xD4")
251 write(PACKED_UINT_8[size])
252 elif size < 0x10000:
253 write(b"\xD5")
254 write(PACKED_UINT_16[size])
255 elif size < 0x100000000:
256 write(b"\xD6")
257 write(struct_pack(">I", size))
258 else:
259 raise OverflowError("List header size out of range")
260
261 def pack_list_stream_header(self):
262 self._write(b"\xD7")
263
264 def pack_map_header(self, size):
265 write = self._write
266 if size == 0x00:
267 write(b"\xA0")
268 elif size == 0x01:
269 write(b"\xA1")
270 elif size == 0x02:
271 write(b"\xA2")
272 elif size == 0x03:
273 write(b"\xA3")
274 elif size == 0x04:
275 write(b"\xA4")
276 elif size == 0x05:
277 write(b"\xA5")
278 elif size == 0x06:
279 write(b"\xA6")
280 elif size == 0x07:
281 write(b"\xA7")
282 elif size == 0x08:
283 write(b"\xA8")
284 elif size == 0x09:
285 write(b"\xA9")
286 elif size == 0x0A:
287 write(b"\xAA")
288 elif size == 0x0B:
289 write(b"\xAB")
290 elif size == 0x0C:
291 write(b"\xAC")
292 elif size == 0x0D:
293 write(b"\xAD")
294 elif size == 0x0E:
295 write(b"\xAE")
296 elif size == 0x0F:
297 write(b"\xAF")
298 elif size < 0x100:
299 write(b"\xD8")
300 write(PACKED_UINT_8[size])
301 elif size < 0x10000:
302 write(b"\xD9")
303 write(PACKED_UINT_16[size])
304 elif size < 0x100000000:
305 write(b"\xDA")
306 write(struct_pack(">I", size))
307 else:
308 raise OverflowError("Map header size out of range")
309
310 def pack_map_stream_header(self):
311 self._write(b"\xDB")
312
313 def pack_struct(self, signature, fields):
314 if len(signature) != 1 or not isinstance(signature, bytes):
315 raise ValueError("Structure signature must be a single byte value")
316 write = self._write
317 size = len(fields)
318 if size == 0x00:
319 write(b"\xB0")
320 elif size == 0x01:
321 write(b"\xB1")
322 elif size == 0x02:
323 write(b"\xB2")
324 elif size == 0x03:
325 write(b"\xB3")
326 elif size == 0x04:
327 write(b"\xB4")
328 elif size == 0x05:
329 write(b"\xB5")
330 elif size == 0x06:
331 write(b"\xB6")
332 elif size == 0x07:
333 write(b"\xB7")
334 elif size == 0x08:
335 write(b"\xB8")
336 elif size == 0x09:
337 write(b"\xB9")
338 elif size == 0x0A:
339 write(b"\xBA")
340 elif size == 0x0B:
341 write(b"\xBB")
342 elif size == 0x0C:
343 write(b"\xBC")
344 elif size == 0x0D:
345 write(b"\xBD")
346 elif size == 0x0E:
347 write(b"\xBE")
348 elif size == 0x0F:
349 write(b"\xBF")
350 else:
351 raise OverflowError("Structure size out of range")
352 write(signature)
353 for field in fields:
354 self._pack(field)
355
356 def pack_end_of_stream(self):
357 self._write(b"\xDF")
358
359
360 class Unpacker:
361
362 def __init__(self, unpackable):
363 self.unpackable = unpackable
364
365 def reset(self):
366 self.unpackable.reset()
367
368 def read(self, n=1):
369 return self.unpackable.read(n)
370
371 def read_u8(self):
372 return self.unpackable.read_u8()
373
374 def unpack(self):
375 return self._unpack()
376
377 def _unpack(self):
378 marker = self.read_u8()
379
380 if marker == -1:
381 raise ValueError("Nothing to unpack")
382
383 # Tiny Integer
384 if 0x00 <= marker <= 0x7F:
385 return marker
386 elif 0xF0 <= marker <= 0xFF:
387 return marker - 0x100
388
389 # Null
390 elif marker == 0xC0:
391 return None
392
393 # Float
394 elif marker == 0xC1:
395 value, = struct_unpack(">d", self.read(8))
396 return value
397
398 # Boolean
399 elif marker == 0xC2:
400 return False
401 elif marker == 0xC3:
402 return True
403
404 # Integer
405 elif marker == 0xC8:
406 return struct_unpack(">b", self.read(1))[0]
407 elif marker == 0xC9:
408 return struct_unpack(">h", self.read(2))[0]
409 elif marker == 0xCA:
410 return struct_unpack(">i", self.read(4))[0]
411 elif marker == 0xCB:
412 return struct_unpack(">q", self.read(8))[0]
413
414 # Bytes
415 elif marker == 0xCC:
416 size, = struct_unpack(">B", self.read(1))
417 return self.read(size).tobytes()
418 elif marker == 0xCD:
419 size, = struct_unpack(">H", self.read(2))
420 return self.read(size).tobytes()
421 elif marker == 0xCE:
422 size, = struct_unpack(">I", self.read(4))
423 return self.read(size).tobytes()
424
425 else:
426 marker_high = marker & 0xF0
427 # String
428 if marker_high == 0x80: # TINY_STRING
429 return decode(self.read(marker & 0x0F), "utf-8")
430 elif marker == 0xD0: # STRING_8:
431 size, = struct_unpack(">B", self.read(1))
432 return decode(self.read(size), "utf-8")
433 elif marker == 0xD1: # STRING_16:
434 size, = struct_unpack(">H", self.read(2))
435 return decode(self.read(size), "utf-8")
436 elif marker == 0xD2: # STRING_32:
437 size, = struct_unpack(">I", self.read(4))
438 return decode(self.read(size), "utf-8")
439
440 # List
441 elif 0x90 <= marker <= 0x9F or 0xD4 <= marker <= 0xD7:
442 return list(self._unpack_list_items(marker))
443
444 # Map
445 elif 0xA0 <= marker <= 0xAF or 0xD8 <= marker <= 0xDB:
446 return self._unpack_map(marker)
447
448 # Structure
449 elif 0xB0 <= marker <= 0xBF:
450 size, tag = self._unpack_structure_header(marker)
451 value = Structure(tag, *([None] * size))
452 for i in range(len(value)):
453 value[i] = self._unpack()
454 return value
455
456 elif marker == 0xDF: # END_OF_STREAM:
457 return EndOfStream
458
459 else:
460 raise ValueError("Unknown PackStream marker %02X" % marker)
461
462 def _unpack_list_items(self, marker):
463 marker_high = marker & 0xF0
464 if marker_high == 0x90:
465 size = marker & 0x0F
466 if size == 0:
467 return
468 elif size == 1:
469 yield self._unpack()
470 else:
471 for _ in range(size):
472 yield self._unpack()
473 elif marker == 0xD4: # LIST_8:
474 size, = struct_unpack(">B", self.read(1))
475 for _ in range(size):
476 yield self._unpack()
477 elif marker == 0xD5: # LIST_16:
478 size, = struct_unpack(">H", self.read(2))
479 for _ in range(size):
480 yield self._unpack()
481 elif marker == 0xD6: # LIST_32:
482 size, = struct_unpack(">I", self.read(4))
483 for _ in range(size):
484 yield self._unpack()
485 elif marker == 0xD7: # LIST_STREAM:
486 item = None
487 while item is not EndOfStream:
488 item = self._unpack()
489 if item is not EndOfStream:
490 yield item
491 else:
492 return
493
494 def unpack_map(self):
495 marker = self.read_u8()
496 return self._unpack_map(marker)
497
498 def _unpack_map(self, marker):
499 marker_high = marker & 0xF0
500 if marker_high == 0xA0:
501 size = marker & 0x0F
502 value = {}
503 for _ in range(size):
504 key = self._unpack()
505 value[key] = self._unpack()
506 return value
507 elif marker == 0xD8: # MAP_8:
508 size, = struct_unpack(">B", self.read(1))
509 value = {}
510 for _ in range(size):
511 key = self._unpack()
512 value[key] = self._unpack()
513 return value
514 elif marker == 0xD9: # MAP_16:
515 size, = struct_unpack(">H", self.read(2))
516 value = {}
517 for _ in range(size):
518 key = self._unpack()
519 value[key] = self._unpack()
520 return value
521 elif marker == 0xDA: # MAP_32:
522 size, = struct_unpack(">I", self.read(4))
523 value = {}
524 for _ in range(size):
525 key = self._unpack()
526 value[key] = self._unpack()
527 return value
528 elif marker == 0xDB: # MAP_STREAM:
529 value = {}
530 key = None
531 while key is not EndOfStream:
532 key = self._unpack()
533 if key is not EndOfStream:
534 value[key] = self._unpack()
535 return value
536 else:
537 return None
538
539 def unpack_structure_header(self):
540 marker = self.read_u8()
541 if marker == -1:
542 return None, None
543 else:
544 return self._unpack_structure_header(marker)
545
546 def _unpack_structure_header(self, marker):
547 marker_high = marker & 0xF0
548 if marker_high == 0xB0: # TINY_STRUCT
549 signature = self.read(1).tobytes()
550 return marker & 0x0F, signature
551 else:
552 raise ValueError("Expected structure, found marker %02X" % marker)
553
554
555 class UnpackableBuffer:
556
557 initial_capacity = 8192
558
559 def __init__(self, data=None):
560 if data is None:
561 self.data = bytearray(self.initial_capacity)
562 self.used = 0
563 else:
564 self.data = bytearray(data)
565 self.used = len(self.data)
566 self.p = 0
567
568 def reset(self):
569 self.used = 0
570 self.p = 0
571
572 def read(self, n=1):
573 view = memoryview(self.data)
574 q = self.p + n
575 subview = view[self.p:q]
576 self.p = q
577 return subview
578
579 def read_u8(self):
580 if self.used - self.p >= 1:
581 value = self.data[self.p]
582 self.p += 1
583 return value
584 else:
585 return -1
586
587 def pop_u16(self):
588 """ Remove the last two bytes of data, returning them as a big-endian
589 16-bit unsigned integer.
590 """
591 if self.used >= 2:
592 value = 0x100 * self.data[self.used - 2] + self.data[self.used - 1]
593 self.used -= 2
594 return value
595 else:
596 return -1
597
598 def receive(self, sock, n_bytes):
599 end = self.used + n_bytes
600 if end > len(self.data):
601 self.data += bytearray(end - len(self.data))
602 view = memoryview(self.data)
603 while self.used < end:
604 n = sock.recv_into(view[self.used:end], end - self.used)
605 if n == 0:
606 raise OSError("No data")
607 self.used += n
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import OrderedDict
22 from collections.abc import MutableSet
23 from logging import getLogger
24 from time import perf_counter
25
26 from neo4j.addressing import Address
27
28
29 log = getLogger("neo4j")
30
31
32 class OrderedSet(MutableSet):
33
34 def __init__(self, elements=()):
35 self._elements = OrderedDict.fromkeys(elements)
36 self._current = None
37
38 def __repr__(self):
39 return "{%s}" % ", ".join(map(repr, self._elements))
40
41 def __contains__(self, element):
42 return element in self._elements
43
44 def __iter__(self):
45 return iter(self._elements)
46
47 def __len__(self):
48 return len(self._elements)
49
50 def __getitem__(self, index):
51 return list(self._elements.keys())[index]
52
53 def add(self, element):
54 self._elements[element] = None
55
56 def clear(self):
57 self._elements.clear()
58
59 def discard(self, element):
60 try:
61 del self._elements[element]
62 except KeyError:
63 pass
64
65 def remove(self, element):
66 try:
67 del self._elements[element]
68 except KeyError:
69 raise ValueError(element)
70
71 def update(self, elements=()):
72 self._elements.update(OrderedDict.fromkeys(elements))
73
74 def replace(self, elements=()):
75 e = self._elements
76 e.clear()
77 e.update(OrderedDict.fromkeys(elements))
78
79
80 class RoutingTable:
81
82 @classmethod
83 def parse_routing_info(cls, *, database, servers, ttl):
84 """ Parse the records returned from the procedure call and
85 return a new RoutingTable instance.
86 """
87 routers = []
88 readers = []
89 writers = []
90 try:
91 for server in servers:
92 role = server["role"]
93 addresses = []
94 for address in server["addresses"]:
95 addresses.append(Address.parse(address, default_port=7687))
96 if role == "ROUTE":
97 routers.extend(addresses)
98 elif role == "READ":
99 readers.extend(addresses)
100 elif role == "WRITE":
101 writers.extend(addresses)
102 except (KeyError, TypeError):
103 raise ValueError("Cannot parse routing info")
104 else:
105 return cls(database=database, routers=routers, readers=readers, writers=writers, ttl=ttl)
106
107 def __init__(self, *, database, routers=(), readers=(), writers=(), ttl=0):
108 self.initial_routers = OrderedSet(routers)
109 self.routers = OrderedSet(routers)
110 self.readers = OrderedSet(readers)
111 self.writers = OrderedSet(writers)
112 self.last_updated_time = perf_counter()
113 self.ttl = ttl
114 self.database = database
115
116 def __repr__(self):
117 return "RoutingTable(database=%r routers=%r, readers=%r, writers=%r, last_updated_time=%r, ttl=%r)" % (
118 self.database,
119 self.routers,
120 self.readers,
121 self.writers,
122 self.last_updated_time,
123 self.ttl,
124 )
125
126 def __contains__(self, address):
127 return address in self.routers or address in self.readers or address in self.writers
128
129 def is_fresh(self, readonly=False):
130 """ Indicator for whether routing information is still usable.
131 """
132 assert isinstance(readonly, bool)
133 log.debug("[#0000] C: <ROUTING> Checking table freshness (readonly=%r)", readonly)
134 expired = self.last_updated_time + self.ttl <= perf_counter()
135 if readonly:
136 has_server_for_mode = bool(self.readers)
137 else:
138 has_server_for_mode = bool(self.writers)
139 log.debug("[#0000] C: <ROUTING> Table expired=%r", expired)
140 log.debug("[#0000] C: <ROUTING> Table routers=%r", self.routers)
141 log.debug("[#0000] C: <ROUTING> Table has_server_for_mode=%r", has_server_for_mode)
142 return not expired and self.routers and has_server_for_mode
143
144 def missing_fresh_writer(self):
145 """ Check if the routing table have a fresh write address.
146
147 :return: Return true if it does not have a fresh write address.
148 :rtype: bool
149 """
150 return not self.is_fresh(readonly=False)
151
152 def should_be_purged_from_memory(self):
153 """ Check if the routing table is stale and not used for a long time and should be removed from memory.
154
155 :return: Returns true if it is old and not used for a while.
156 :rtype: bool
157 """
158 from neo4j.conf import RoutingConfig
159 perf_time = perf_counter()
160 log.debug("[#0000] C: <ROUTING AGED> last_updated_time=%r perf_time=%r", self.last_updated_time, perf_time)
161 return self.last_updated_time + self.ttl + RoutingConfig.routing_table_purge_delay <= perf_time
162
163 def update(self, new_routing_table):
164 """ Update the current routing table with new routing information
165 from a replacement table.
166 """
167 self.routers.replace(new_routing_table.routers)
168 self.readers.replace(new_routing_table.readers)
169 self.writers.replace(new_routing_table.writers)
170 self.last_updated_time = perf_counter()
171 self.ttl = new_routing_table.ttl
172 log.debug("[#0000] S: <ROUTING> table=%r", self)
173
174 def servers(self):
175 return set(self.routers) | set(self.writers) | set(self.readers)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """
22 This module defines spatial data types.
23 """
24
25
26 from threading import Lock
27
28 from neo4j.packstream import Structure
29
30
31 __all__ = [
32 "Point",
33 "CartesianPoint",
34 "WGS84Point",
35 "point_type",
36 "hydrate_point",
37 "dehydrate_point",
38 ]
39
40
41 # SRID to subclass mappings
42 __srid_table = {}
43 __srid_table_lock = Lock()
44
45
46 class Point(tuple):
47 """ A point within a geometric space. This type is generally used
48 via its subclasses and should not be instantiated directly unless
49 there is no subclass defined for the required SRID.
50 """
51
52 srid = None
53
54 def __new__(cls, iterable):
55 return tuple.__new__(cls, iterable)
56
57 def __repr__(self):
58 return "POINT(%s)" % " ".join(map(str, self))
59
60 def __eq__(self, other):
61 try:
62 return type(self) is type(other) and tuple(self) == tuple(other)
63 except (AttributeError, TypeError):
64 return False
65
66 def __ne__(self, other):
67 return not self.__eq__(other)
68
69 def __hash__(self):
70 return hash(type(self)) ^ hash(tuple(self))
71
72
73 def point_type(name, fields, srid_map):
74 """ Dynamically create a Point subclass.
75 """
76
77 def srid(self):
78 try:
79 return srid_map[len(self)]
80 except KeyError:
81 return None
82
83 attributes = {"srid": property(srid)}
84
85 for index, subclass_field in enumerate(fields):
86
87 def accessor(self, i=index, f=subclass_field):
88 try:
89 return self[i]
90 except IndexError:
91 raise AttributeError(f)
92
93 for field_alias in {subclass_field, "xyz"[index]}:
94 attributes[field_alias] = property(accessor)
95
96 cls = type(name, (Point,), attributes)
97
98 with __srid_table_lock:
99 for dim, srid in srid_map.items():
100 __srid_table[srid] = (cls, dim)
101
102 return cls
103
104
105 # Point subclass definitions
106 CartesianPoint = point_type("CartesianPoint", ["x", "y", "z"], {2: 7203, 3: 9157})
107 WGS84Point = point_type("WGS84Point", ["longitude", "latitude", "height"], {2: 4326, 3: 4979})
108
109
110 def hydrate_point(srid, *coordinates):
111 """ Create a new instance of a Point subclass from a raw
112 set of fields. The subclass chosen is determined by the
113 given SRID; a ValueError will be raised if no such
114 subclass can be found.
115 """
116 try:
117 point_class, dim = __srid_table[srid]
118 except KeyError:
119 point = Point(coordinates)
120 point.srid = srid
121 return point
122 else:
123 if len(coordinates) != dim:
124 raise ValueError("SRID %d requires %d coordinates (%d provided)" % (srid, dim, len(coordinates)))
125 return point_class(coordinates)
126
127
128 def dehydrate_point(value):
129 """ Dehydrator for Point data.
130
131 :param value:
132 :type value: Point
133 :return:
134 """
135 dim = len(value)
136 if dim == 2:
137 return Structure(b"X", value.srid, *value)
138 elif dim == 3:
139 return Structure(b"Y", value.srid, *value)
140 else:
141 raise ValueError("Cannot dehydrate Point with %d dimensions" % dim)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """ This module contains the fundamental types used for temporal accounting as well as
22 a number of utility functions.
23 """
24
25 from datetime import (
26 timedelta,
27 date,
28 time,
29 datetime,
30 )
31 from functools import total_ordering
32 from re import compile as re_compile
33 from time import (
34 gmtime,
35 mktime,
36 struct_time,
37 )
38 from decimal import Decimal
39
40 from neo4j.time.arithmetic import (
41 nano_add,
42 nano_sub,
43 nano_mul,
44 nano_div,
45 nano_mod,
46 nano_divmod,
47 symmetric_divmod,
48 round_half_to_even,
49 )
50 from neo4j.time.metaclasses import (
51 DateType,
52 TimeType,
53 DateTimeType,
54 )
55
56 MIN_INT64 = -(2 ** 63)
57 MAX_INT64 = (2 ** 63) - 1
58
59 MIN_YEAR = 1
60 """The smallest year number allowed in a :class:`neo4j.time.Date` or :class:`neo4j.time.DateTime` object to be compatible with :class:`python:datetime.date` and :class:`python:datetime.datetime`."""
61
62 MAX_YEAR = 9999
63 """The largest year number allowed in a :class:`neo4j.time.Date` or :class:`neo4j.time.DateTime` object to be compatible with :class:`python:datetime.date` and :class:`python:datetime.datetime`."""
64
65 DATE_ISO_PATTERN = re_compile(r"^(\d{4})-(\d{2})-(\d{2})$")
66 TIME_ISO_PATTERN = re_compile(r"^(\d{2})(:(\d{2})(:((\d{2})"
67 r"(\.\d*)?))?)?(([+-])(\d{2}):(\d{2})(:((\d{2})(\.\d*)?))?)?$")
68 DURATION_ISO_PATTERN = re_compile(r"^P((\d+)Y)?((\d+)M)?((\d+)D)?"
69 r"(T((\d+)H)?((\d+)M)?((\d+(\.\d+)?)?S)?)?$")
70
71 NANO_SECONDS = 1000000000
72
73
74 def _is_leap_year(year):
75 if year % 4 != 0:
76 return False
77 if year % 100 != 0:
78 return True
79 return year % 400 == 0
80
81
82 IS_LEAP_YEAR = {year: _is_leap_year(year) for year in range(MIN_YEAR, MAX_YEAR + 1)}
83
84
85 def _days_in_year(year):
86 return 366 if IS_LEAP_YEAR[year] else 365
87
88
89 DAYS_IN_YEAR = {year: _days_in_year(year) for year in range(MIN_YEAR, MAX_YEAR + 1)}
90
91
92 def _days_in_month(year, month):
93 if month in (9, 4, 6, 11):
94 return 30
95 elif month != 2:
96 return 31
97 else:
98 return 29 if IS_LEAP_YEAR[year] else 28
99
100
101 DAYS_IN_MONTH = {(year, month): _days_in_month(year, month)
102 for year in range(MIN_YEAR, MAX_YEAR + 1) for month in range(1, 13)}
103
104
105 def _normalize_day(year, month, day):
106 """ Coerce the day of the month to an internal value that may or
107 may not match the "public" value.
108
109 With the exception of the last three days of every month, all
110 days are stored as-is. The last three days are instead stored
111 as -1 (the last), -2 (the second to last) and -3 (the third to
112 last).
113
114 Therefore, for a 28-day month, the last week is as follows:
115
116 Day | 22 23 24 25 26 27 28
117 Value | 22 23 24 25 -3 -2 -1
118
119 For a 29-day month, the last week is as follows:
120
121 Day | 23 24 25 26 27 28 29
122 Value | 23 24 25 26 -3 -2 -1
123
124 For a 30-day month, the last week is as follows:
125
126 Day | 24 25 26 27 28 29 30
127 Value | 24 25 26 27 -3 -2 -1
128
129 For a 31-day month, the last week is as follows:
130
131 Day | 25 26 27 28 29 30 31
132 Value | 25 26 27 28 -3 -2 -1
133
134 This slightly unintuitive system makes some temporal arithmetic
135 produce a more desirable outcome.
136
137 :param year:
138 :param month:
139 :param day:
140 :return:
141 """
142 if year < MIN_YEAR or year > MAX_YEAR:
143 raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR))
144 if month < 1 or month > 12:
145 raise ValueError("Month out of range (1..12)")
146 days_in_month = DAYS_IN_MONTH[(year, month)]
147 if day in (days_in_month, -1):
148 return year, month, -1
149 if day in (days_in_month - 1, -2):
150 return year, month, -2
151 if day in (days_in_month - 2, -3):
152 return year, month, -3
153 if 1 <= day <= days_in_month - 3:
154 return year, month, int(day)
155 # TODO improve this error message
156 raise ValueError("Day %d out of range (1..%d, -1, -2 ,-3)" % (day, days_in_month))
157
158
159 class ClockTime(tuple):
160 """ A count of `seconds` and `nanoseconds`. This class can be used to
161 mark a particular point in time, relative to an externally-specified
162 epoch.
163
164 The `seconds` and `nanoseconds` values provided to the constructor can
165 can have any sign but will be normalized internally into a positive or
166 negative `seconds` value along with a positive `nanoseconds` value
167 between `0` and `999,999,999`. Therefore ``ClockTime(-1, -1)`` is
168 normalized to ``ClockTime(-2, 999999999)``.
169
170 Note that the structure of a :class:`.ClockTime` object is similar to
171 the ``timespec`` struct in C.
172 """
173
174 def __new__(cls, seconds=0, nanoseconds=0):
175 seconds, nanoseconds = nano_divmod(int(1000000000 * seconds) + int(nanoseconds), 1000000000)
176 return tuple.__new__(cls, (seconds, nanoseconds))
177
178 def __add__(self, other):
179 if isinstance(other, (int, float)):
180 other = ClockTime(other)
181 if isinstance(other, ClockTime):
182 return ClockTime(self.seconds + other.seconds, self.nanoseconds + other.nanoseconds)
183 if isinstance(other, Duration):
184 if other.months or other.days:
185 raise ValueError("Cannot add Duration with months or days")
186 return ClockTime(self.seconds + other.seconds, self.nanoseconds +
187 int(other.subseconds * 1000000000))
188 return NotImplemented
189
190 def __sub__(self, other):
191 if isinstance(other, (int, float)):
192 other = ClockTime(other)
193 if isinstance(other, ClockTime):
194 return ClockTime(self.seconds - other.seconds, self.nanoseconds - other.nanoseconds)
195 if isinstance(other, Duration):
196 if other.months or other.days:
197 raise ValueError("Cannot subtract Duration with months or days")
198 return ClockTime(self.seconds - other.seconds, self.nanoseconds - int(other.subseconds * 1000000000))
199 return NotImplemented
200
201 def __repr__(self):
202 return "ClockTime(seconds=%r, nanoseconds=%r)" % self
203
204 @property
205 def seconds(self):
206 return self[0]
207
208 @property
209 def nanoseconds(self):
210 return self[1]
211
212
213 class Clock:
214 """ Accessor for time values. This class is fulfilled by implementations
215 that subclass :class:`.Clock`. These implementations are contained within
216 the ``neo4j.time.clock_implementations`` module, and are not intended to be
217 accessed directly.
218
219 Creating a new :class:`.Clock` instance will produce the highest
220 precision clock implementation available.
221
222 >>> clock = Clock()
223 >>> type(clock) # doctest: +SKIP
224 neo4j.time.clock_implementations.LibCClock
225 >>> clock.local_time() # doctest: +SKIP
226 ClockTime(seconds=1525265942, nanoseconds=506844026)
227
228 """
229
230 __implementations = None
231
232 def __new__(cls):
233 if cls.__implementations is None:
234 # Find an available clock with the best precision
235 import neo4j.time.clock_implementations
236 cls.__implementations = sorted((clock for clock in Clock.__subclasses__() if clock.available()),
237 key=lambda clock: clock.precision(), reverse=True)
238 if not cls.__implementations:
239 raise RuntimeError("No clock implementations available")
240 instance = object.__new__(cls.__implementations[0])
241 return instance
242
243 @classmethod
244 def precision(cls):
245 """ The precision of this clock implementation, represented as a
246 number of decimal places. Therefore, for a nanosecond precision
247 clock, this function returns `9`.
248 """
249 raise NotImplementedError("No clock implementation selected")
250
251 @classmethod
252 def available(cls):
253 """ Return a boolean flag to indicate whether or not this clock
254 implementation is available on this platform.
255 """
256 raise NotImplementedError("No clock implementation selected")
257
258 @classmethod
259 def local_offset(cls):
260 """The offset from UTC for local time read from this clock.
261 This may raise OverflowError if not supported, because of platform depending C libraries.
262
263 :returns:
264 :rtype:
265
266 :raises OverflowError:
267 """
268 return ClockTime(-int(mktime(gmtime(0))))
269
270 def local_time(self):
271 """ Read and return the current local time from this clock, measured relative to the Unix Epoch.
272 This may raise OverflowError if not supported, because of platform depending C libraries.
273
274 :returns:
275 :rtype:
276
277 :raises OverflowError:
278 """
279 return self.utc_time() + self.local_offset()
280
281 def utc_time(self):
282 """ Read and return the current UTC time from this clock, measured
283 relative to the Unix Epoch.
284 """
285 raise NotImplementedError("No clock implementation selected")
286
287
288 class Duration(tuple):
289 """A Duration represents the difference between two points in time.
290 Duration objects store a composite value of months, days and seconds.
291 Unlike :class:`datetime.timedelta` however, days and seconds are never interchanged and are applied separately in calculations.
292 """
293
294 # i64: i64:i64: i32
295
296 min = None
297 max = None
298
299 def __new__(cls, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0,
300 subseconds=0, milliseconds=0, microseconds=0, nanoseconds=0):
301 mo = int(12 * years + months)
302 if mo < MIN_INT64 or mo > MAX_INT64:
303 raise ValueError("Months value out of range")
304 d = int(7 * weeks + days)
305 if d < MIN_INT64 or d > MAX_INT64:
306 raise ValueError("Days value out of range")
307 s = (int(3600000000000 * hours) +
308 int(60000000000 * minutes) +
309 int(1000000000 * seconds) +
310 int(1000000000 * subseconds) +
311 int(1000000 * milliseconds) +
312 int(1000 * microseconds) +
313 int(nanoseconds))
314 s, ss = symmetric_divmod(s, 1000000000)
315 if s < MIN_INT64 or s > MAX_INT64:
316 raise ValueError("Seconds value out of range")
317 return tuple.__new__(cls, (mo, d, s, ss / 1000000000))
318
319 def __bool__(self):
320 return any(map(bool, self))
321
322 __nonzero__ = __bool__
323
324 def __add__(self, other):
325 if isinstance(other, Duration):
326 return Duration(months=self[0] + int(other[0]), days=self[1] + int(other[1]),
327 seconds=self[2] + int(other[2]), subseconds=nano_add(self[3], other[3]))
328 if isinstance(other, timedelta):
329 return Duration(months=self[0], days=self[1] + int(other.days),
330 seconds=self[2] + int(other.seconds),
331 subseconds=nano_add(self[3], other.microseconds / 1000000))
332 return NotImplemented
333
334 def __sub__(self, other):
335 if isinstance(other, Duration):
336 return Duration(months=self[0] - int(other[0]), days=self[1] - int(other[1]),
337 seconds=self[2] - int(other[2]), subseconds=nano_sub(self[3], other[3]))
338 if isinstance(other, timedelta):
339 return Duration(months=self[0], days=self[1] - int(other.days),
340 seconds=self[2] - int(other.seconds),
341 subseconds=nano_sub(self[3], other.microseconds / 1000000))
342 return NotImplemented
343
344 def __mul__(self, other):
345 if isinstance(other, (int, float)):
346 return Duration(months=self[0] * other, days=self[1] * other,
347 seconds=self[2] * other, subseconds=nano_mul(self[3], other))
348 return NotImplemented
349
350 def __floordiv__(self, other):
351 if isinstance(other, int):
352 return Duration(months=int(self[0] // other), days=int(self[1] // other),
353 seconds=int(nano_add(self[2], self[3]) // other), subseconds=0)
354 return NotImplemented
355
356 def __mod__(self, other):
357 if isinstance(other, int):
358 seconds, subseconds = symmetric_divmod(nano_add(self[2], self[3]) % other, 1)
359 return Duration(months=round_half_to_even(self[0] % other), days=round_half_to_even(self[1] % other),
360 seconds=seconds, subseconds=subseconds)
361 return NotImplemented
362
363 def __divmod__(self, other):
364 if isinstance(other, int):
365 return self.__floordiv__(other), self.__mod__(other)
366 return NotImplemented
367
368 def __truediv__(self, other):
369 if isinstance(other, (int, float)):
370 return Duration(months=round_half_to_even(float(self[0]) / other), days=round_half_to_even(float(self[1]) / other),
371 seconds=float(self[2]) / other, subseconds=nano_div(self[3], other))
372 return NotImplemented
373
374 __div__ = __truediv__
375
376 def __pos__(self):
377 return self
378
379 def __neg__(self):
380 return Duration(months=-self[0], days=-self[1], seconds=-self[2], subseconds=-self[3])
381
382 def __abs__(self):
383 return Duration(months=abs(self[0]), days=abs(self[1]), seconds=abs(self[2]), subseconds=abs(self[3]))
384
385 def __repr__(self):
386 return "Duration(months=%r, days=%r, seconds=%r, subseconds=%r)" % self
387
388 def __str__(self):
389 return self.iso_format()
390
391 @classmethod
392 def from_iso_format(cls, s):
393 m = DURATION_ISO_PATTERN.match(s)
394 if m:
395 return cls(
396 years=int(m.group(2) or 0),
397 months=int(m.group(4) or 0),
398 days=int(m.group(6) or 0),
399 hours=int(m.group(9) or 0),
400 minutes=int(m.group(11) or 0),
401 seconds=float(m.group(13) or 0.0),
402 )
403 raise ValueError("Duration string must be in ISO format")
404
405 fromisoformat = from_iso_format
406
407 def iso_format(self, sep="T"):
408 """
409
410 :param sep: the seperation string
411 :returns:
412 :rtype: str
413 """
414 parts = []
415 hours, minutes, seconds = self.hours_minutes_seconds
416 if hours:
417 parts.append("%dH" % hours)
418 if minutes:
419 parts.append("%dM" % minutes)
420 if seconds:
421 if seconds == seconds // 1:
422 parts.append("%dS" % seconds)
423 else:
424 parts.append("%rS" % seconds)
425 if parts:
426 parts.insert(0, sep)
427 years, months, days = self.years_months_days
428 if days:
429 parts.insert(0, "%dD" % days)
430 if months:
431 parts.insert(0, "%dM" % months)
432 if years:
433 parts.insert(0, "%dY" % years)
434 if parts:
435 parts.insert(0, "P")
436 return "".join(parts)
437 else:
438 return "PT0S"
439
440 @property
441 def months(self):
442 """
443
444 :return:
445 """
446 return self[0]
447
448 @property
449 def days(self):
450 """
451
452 :return:
453 """
454 return self[1]
455
456 @property
457 def seconds(self):
458 """
459
460 :return:
461 """
462 return self[2]
463
464 @property
465 def subseconds(self):
466 """
467
468 :return:
469 """
470 return self[3]
471
472 @property
473 def years_months_days(self):
474 """
475
476 :return:
477 """
478 years, months = symmetric_divmod(self[0], 12)
479 return years, months, self[1]
480
481 @property
482 def hours_minutes_seconds(self):
483 """ A 3-tuple of (hours, minutes, seconds).
484 """
485 minutes, seconds = symmetric_divmod(self[2], 60)
486 hours, minutes = symmetric_divmod(minutes, 60)
487 return hours, minutes, float(seconds) + self[3]
488
489
490 Duration.min = Duration(months=MIN_INT64, days=MIN_INT64, seconds=MIN_INT64, subseconds=-0.999999999)
491 Duration.max = Duration(months=MAX_INT64, days=MAX_INT64, seconds=MAX_INT64, subseconds=+0.999999999)
492
493
494 class Date(metaclass=DateType):
495 """
496 A Date object represents a date (year, month and day) in an idealized calendar, the current Gregorian calendar indefinitely extended in both directions.
497
498 For example::
499
500 0xxxxxxx xxxxxxxx -- Date(1970-01-01..2059-09-18) -- 719163..
501 10xxxxxx xxxxxxxx xxxxxxxx -- Date(0001-01-01..9999-12-31) -- 0..
502 """
503
504 # CONSTRUCTOR #
505
506 def __new__(cls, year, month, day):
507 if year == month == day == 0:
508 return ZeroDate
509 year, month, day = _normalize_day(year, month, day)
510 ordinal = cls.__calc_ordinal(year, month, day)
511 return cls.__new(ordinal, year, month, day)
512
513 @classmethod
514 def __new(cls, ordinal, year, month, day):
515 instance = object.__new__(cls)
516 instance.__ordinal = int(ordinal)
517 instance.__year = int(year)
518 instance.__month = int(month)
519 instance.__day = int(day)
520 return instance
521
522 def __getattr__(self, name):
523 """ Map standard library attribute names to local attribute names,
524 for compatibility.
525 """
526 try:
527 return {
528 "isocalendar": self.iso_calendar,
529 "isoformat": self.iso_format,
530 "isoweekday": self.iso_weekday,
531 "strftime": self.__format__,
532 "toordinal": self.to_ordinal,
533 "timetuple": self.time_tuple,
534 }[name]
535 except KeyError:
536 raise AttributeError("Date has no attribute %r" % name)
537
538 # CLASS METHODS #
539
540 @classmethod
541 def today(cls, tz=None):
542 """This may raise OverflowError if not supported, because of platform depending C libraries.
543
544 :param tz: time zone
545 :returns:
546 :rtype:
547
548 :raises OverflowError:
549 """
550 if tz is None:
551 return cls.from_clock_time(Clock().local_time(), UnixEpoch)
552 else:
553 return tz.fromutc(DateTime.from_clock_time(Clock().utc_time(), UnixEpoch).replace(tzinfo=tz)).date()
554
555 @classmethod
556 def utc_today(cls):
557 return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
558
559 @classmethod
560 def from_timestamp(cls, timestamp, tz=None):
561 """This may raise OverflowError, if the timestamp is out of the range of values supported by the platform C localtime() function.
562 It’s common for this to be restricted to years from 1970 through 2038.
563
564 :param timestamp:
565 :param tz: time zone
566 :returns:
567 :rtype:
568
569 :raises OverflowError:
570 """
571 if tz is None:
572 return cls.from_clock_time(ClockTime(timestamp) + Clock().local_offset(), UnixEpoch)
573 else:
574 return tz.fromutc(DateTime.utcfromtimestamp(timestamp).replace(tzinfo=tz)).date()
575
576 @classmethod
577 def utc_from_timestamp(cls, timestamp):
578 return cls.from_clock_time((timestamp, 0), UnixEpoch)
579
580 @classmethod
581 def from_ordinal(cls, ordinal):
582 """ Return the :class:`.Date` that corresponds to the proleptic
583 Gregorian ordinal, where ``0001-01-01`` has ordinal 1 and
584 ``9999-12-31`` has ordinal 3,652,059. Values outside of this
585 range trigger a :exc:`ValueError`. The corresponding instance
586 method for the reverse date-to-ordinal transformation is
587 :meth:`.to_ordinal`.
588 """
589 if ordinal == 0:
590 return ZeroDate
591 if ordinal >= 736695:
592 year = 2018 # Project release year
593 month = 1
594 day = int(ordinal - 736694)
595 elif ordinal >= 719163:
596 year = 1970 # Unix epoch
597 month = 1
598 day = int(ordinal - 719162)
599 else:
600 year = 1
601 month = 1
602 day = int(ordinal)
603 if day < 1 or day > 3652059:
604 # Note: this requires a maximum of 22 bits for storage
605 # Could be transferred in 3 bytes.
606 raise ValueError("Ordinal out of range (1..3652059)")
607 if year < MIN_YEAR or year > MAX_YEAR:
608 raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR))
609 days_in_year = DAYS_IN_YEAR[year]
610 while day > days_in_year:
611 day -= days_in_year
612 year += 1
613 days_in_year = DAYS_IN_YEAR[year]
614 days_in_month = DAYS_IN_MONTH[(year, month)]
615 while day > days_in_month:
616 day -= days_in_month
617 month += 1
618 days_in_month = DAYS_IN_MONTH[(year, month)]
619 year, month, day = _normalize_day(year, month, day)
620 return cls.__new(ordinal, year, month, day)
621
622 @classmethod
623 def parse(cls, s):
624 """ Parse a string to produce a :class:`.Date`.
625
626 Accepted formats:
627 'YYYY-MM-DD'
628
629 :param s:
630 :return:
631 """
632 try:
633 numbers = map(int, s.split("-"))
634 except (ValueError, AttributeError):
635 raise ValueError("Date string must be in format YYYY-MM-DD")
636 else:
637 numbers = list(numbers)
638 if len(numbers) == 3:
639 return cls(*numbers)
640 raise ValueError("Date string must be in format YYYY-MM-DD")
641
642 @classmethod
643 def from_iso_format(cls, s):
644 m = DATE_ISO_PATTERN.match(s)
645 if m:
646 year = int(m.group(1))
647 month = int(m.group(2))
648 day = int(m.group(3))
649 return cls(year, month, day)
650 raise ValueError("Date string must be in format YYYY-MM-DD")
651
652 @classmethod
653 def from_native(cls, d):
654 """ Convert from a native Python `datetime.date` value.
655 """
656 return Date.from_ordinal(d.toordinal())
657
658 @classmethod
659 def from_clock_time(cls, clock_time, epoch):
660 """ Convert from a ClockTime relative to a given epoch.
661 """
662 try:
663 clock_time = ClockTime(*clock_time)
664 except (TypeError, ValueError):
665 raise ValueError("Clock time must be a 2-tuple of (s, ns)")
666 else:
667 ordinal = clock_time.seconds // 86400
668 return Date.from_ordinal(ordinal + epoch.date().to_ordinal())
669
670 @classmethod
671 def is_leap_year(cls, year):
672 if year < MIN_YEAR or year > MAX_YEAR:
673 raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR))
674 return IS_LEAP_YEAR[year]
675
676 @classmethod
677 def days_in_year(cls, year):
678 if year < MIN_YEAR or year > MAX_YEAR:
679 raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR))
680 return DAYS_IN_YEAR[year]
681
682 @classmethod
683 def days_in_month(cls, year, month):
684 if year < MIN_YEAR or year > MAX_YEAR:
685 raise ValueError("Year out of range (%d..%d)" % (MIN_YEAR, MAX_YEAR))
686 if month < 1 or month > 12:
687 raise ValueError("Month out of range (1..12)")
688 return DAYS_IN_MONTH[(year, month)]
689
690 @classmethod
691 def __calc_ordinal(cls, year, month, day):
692 if day < 0:
693 day = cls.days_in_month(year, month) + int(day) + 1
694 # The built-in date class does this faster than a
695 # long-hand pure Python algorithm could
696 return date(year, month, day).toordinal()
697
698 # CLASS ATTRIBUTES #
699
700 min = None
701
702 max = None
703
704 resolution = None
705
706 # INSTANCE ATTRIBUTES #
707
708 __ordinal = 0
709
710 __year = 0
711
712 __month = 0
713
714 __day = 0
715
716 @property
717 def year(self):
718 return self.__year
719
720 @property
721 def month(self):
722 return self.__month
723
724 @property
725 def day(self):
726 if self.__day == 0:
727 return 0
728 if self.__day >= 1:
729 return self.__day
730 return self.days_in_month(self.__year, self.__month) + self.__day + 1
731
732 @property
733 def year_month_day(self):
734 return self.year, self.month, self.day
735
736 @property
737 def year_week_day(self):
738 ordinal = self.__ordinal
739 year = self.__year
740
741 def day_of_week(o):
742 return ((o - 1) % 7) + 1
743
744 def iso_week_1(y):
745 j4 = Date(y, 1, 4)
746 return j4 + Duration(days=(1 - day_of_week(j4.to_ordinal())))
747
748 if ordinal >= Date(year, 12, 29).to_ordinal():
749 week1 = iso_week_1(year + 1)
750 if ordinal < week1.to_ordinal():
751 week1 = iso_week_1(year)
752 else:
753 year += 1
754 else:
755 week1 = iso_week_1(year)
756 if ordinal < week1.to_ordinal():
757 year -= 1
758 week1 = iso_week_1(year)
759 return year, int((ordinal - week1.to_ordinal()) / 7 + 1), day_of_week(ordinal)
760
761 @property
762 def year_day(self):
763 return self.__year, self.toordinal() - Date(self.__year, 1, 1).toordinal() + 1
764
765 # OPERATIONS #
766
767 def __hash__(self):
768 return hash(self.toordinal())
769
770 def __eq__(self, other):
771 if isinstance(other, (Date, date)):
772 return self.toordinal() == other.toordinal()
773 return False
774
775 def __ne__(self, other):
776 return not self.__eq__(other)
777
778 def __lt__(self, other):
779 if isinstance(other, (Date, date)):
780 return self.toordinal() < other.toordinal()
781 raise TypeError("'<' not supported between instances of 'Date' and %r" % type(other).__name__)
782
783 def __le__(self, other):
784 if isinstance(other, (Date, date)):
785 return self.toordinal() <= other.toordinal()
786 raise TypeError("'<=' not supported between instances of 'Date' and %r" % type(other).__name__)
787
788 def __ge__(self, other):
789 if isinstance(other, (Date, date)):
790 return self.toordinal() >= other.toordinal()
791 raise TypeError("'>=' not supported between instances of 'Date' and %r" % type(other).__name__)
792
793 def __gt__(self, other):
794 if isinstance(other, (Date, date)):
795 return self.toordinal() > other.toordinal()
796 raise TypeError("'>' not supported between instances of 'Date' and %r" % type(other).__name__)
797
798 def __add__(self, other):
799
800 def add_months(d, months):
801 years, months = symmetric_divmod(months, 12)
802 year = d.__year + years
803 month = d.__month + months
804 while month > 12:
805 year += 1
806 month -= 12
807 while month < 1:
808 year -= 1
809 month += 12
810 d.__year = year
811 d.__month = month
812
813 def add_days(d, days):
814 assert 1 <= d.__day <= 28 or -28 <= d.__day <= -1
815 if d.__day >= 1:
816 new_days = d.__day + days
817 if 1 <= new_days <= 27:
818 d.__day = new_days
819 return
820 d0 = Date.from_ordinal(d.__ordinal + days)
821 d.__year, d.__month, d.__day = d0.__year, d0.__month, d0.__day
822
823 if isinstance(other, Duration):
824 if other.seconds or other.subseconds:
825 raise ValueError("Cannot add a Duration with seconds or subseconds to a Date")
826 if other.months == other.days == 0:
827 return self
828 new_date = self.replace()
829 # Add days before months as the former sometimes
830 # requires the current ordinal to be correct.
831 if other.days:
832 add_days(new_date, other.days)
833 if other.months:
834 add_months(new_date, other.months)
835 new_date.__ordinal = self.__calc_ordinal(new_date.year, new_date.month, new_date.day)
836 return new_date
837 return NotImplemented
838
839 def __sub__(self, other):
840 if isinstance(other, (Date, date)):
841 return Duration(days=(self.toordinal() - other.toordinal()))
842 try:
843 return self.__add__(-other)
844 except TypeError:
845 return NotImplemented
846
847 # INSTANCE METHODS #
848
849 def replace(self, **kwargs):
850 """ Return a :class:`.Date` with one or more components replaced
851 with new values.
852 """
853 return Date(kwargs.get("year", self.__year),
854 kwargs.get("month", self.__month),
855 kwargs.get("day", self.__day))
856
857 def time_tuple(self):
858 _, _, day_of_week = self.year_week_day
859 _, day_of_year = self.year_day
860 return struct_time((self.year, self.month, self.day, 0, 0, 0, day_of_week - 1, day_of_year, -1))
861
862 def to_ordinal(self):
863 """ Return the current value as an ordinal.
864 """
865 return self.__ordinal
866
867 def to_clock_time(self, epoch):
868 try:
869 return ClockTime(86400 * (self.to_ordinal() - epoch.to_ordinal()))
870 except AttributeError:
871 raise TypeError("Epoch has no ordinal value")
872
873 def to_native(self):
874 """ Convert to a native Python `datetime.date` value.
875 """
876 return date.fromordinal(self.to_ordinal())
877
878 def weekday(self):
879 return self.year_week_day[2] - 1
880
881 def iso_weekday(self):
882 return self.year_week_day[2]
883
884 def iso_calendar(self):
885 return self.year_week_day
886
887 def iso_format(self):
888 if self.__ordinal == 0:
889 return "0000-00-00"
890 return "%04d-%02d-%02d" % self.year_month_day
891
892 def __repr__(self):
893 if self.__ordinal == 0:
894 return "neo4j.time.ZeroDate"
895 return "neo4j.time.Date(%r, %r, %r)" % self.year_month_day
896
897 def __str__(self):
898 return self.iso_format()
899
900 def __format__(self, format_spec):
901 raise NotImplementedError()
902
903
904 Date.min = Date.from_ordinal(1)
905 Date.max = Date.from_ordinal(3652059)
906 Date.resolution = Duration(days=1)
907
908
909 ZeroDate = object.__new__(Date)
910
911
912 class Time(metaclass=TimeType):
913 """ Time of day.
914 """
915
916 # CONSTRUCTOR #
917
918 def __new__(cls, hour, minute, second, tzinfo=None):
919 hour, minute, second = cls.__normalize_second(hour, minute, second)
920 ticks = 3600 * hour + 60 * minute + second
921 return cls.__new(ticks, hour, minute, second, tzinfo)
922
923 @classmethod
924 def __new(cls, ticks, hour, minute, second, tzinfo):
925 instance = object.__new__(cls)
926 instance.__ticks = float(ticks)
927 instance.__hour = int(hour)
928 instance.__minute = int(minute)
929 instance.__second = float(second)
930 instance.__tzinfo = tzinfo
931 return instance
932
933 def __getattr__(self, name):
934 """ Map standard library attribute names to local attribute names,
935 for compatibility.
936 """
937 try:
938 return {
939 "isoformat": self.iso_format,
940 "utcoffset": self.utc_offset,
941 }[name]
942 except KeyError:
943 raise AttributeError("Date has no attribute %r" % name)
944
945 # CLASS METHODS #
946
947 @classmethod
948 def now(cls, tz=None):
949 """This may raise OverflowError if not supported, because of platform depending C libraries.
950
951 :param tz:
952 :returns:
953 :rtype:
954
955 :raises OverflowError:
956 """
957 if tz is None:
958 return cls.from_clock_time(Clock().local_time(), UnixEpoch)
959 else:
960 return tz.fromutc(DateTime.from_clock_time(Clock().utc_time(), UnixEpoch)).time().replace(tzinfo=tz)
961
962 @classmethod
963 def utc_now(cls):
964 return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
965
966 @classmethod
967 def from_iso_format(cls, s):
968 from pytz import FixedOffset
969 m = TIME_ISO_PATTERN.match(s)
970 if m:
971 hour = int(m.group(1))
972 minute = int(m.group(3) or 0)
973 second = float(m.group(5) or 0.0)
974 if m.group(8) is None:
975 return cls(hour, minute, second)
976 else:
977 offset_multiplier = 1 if m.group(9) == "+" else -1
978 offset_hour = int(m.group(10))
979 offset_minute = int(m.group(11))
980 # pytz only supports offsets of minute resolution
981 # so we can ignore this part
982 # offset_second = float(m.group(13) or 0.0)
983 offset = 60 * offset_hour + offset_minute
984 return cls(hour, minute, second, tzinfo=FixedOffset(offset_multiplier * offset))
985 raise ValueError("Time string is not in ISO format")
986
987 @classmethod
988 def from_ticks(cls, ticks, tz=None):
989 if 0 <= ticks < 86400:
990 minute, second = nano_divmod(ticks, 60)
991 hour, minute = divmod(minute, 60)
992 return cls.__new(ticks, hour, minute, second, tz)
993 raise ValueError("Ticks out of range (0..86400)")
994
995 @classmethod
996 def from_native(cls, t):
997 """ Convert from a native Python `datetime.time` value.
998 """
999 second = (1000000 * t.second + t.microsecond) / 1000000
1000 return Time(t.hour, t.minute, second, t.tzinfo)
1001
1002 @classmethod
1003 def from_clock_time(cls, clock_time, epoch):
1004 """ Convert from a `.ClockTime` relative to a given epoch.
1005 """
1006 clock_time = ClockTime(*clock_time)
1007 ts = clock_time.seconds % 86400
1008 nanoseconds = int(1000000000 * ts + clock_time.nanoseconds)
1009 return Time.from_ticks(epoch.time().ticks + nanoseconds / 1000000000)
1010
1011 @classmethod
1012 def __normalize_hour(cls, hour):
1013 if 0 <= hour < 24:
1014 return int(hour)
1015 raise ValueError("Hour out of range (0..23)")
1016
1017 @classmethod
1018 def __normalize_minute(cls, hour, minute):
1019 hour = cls.__normalize_hour(hour)
1020 if 0 <= minute < 60:
1021 return hour, int(minute)
1022 raise ValueError("Minute out of range (0..59)")
1023
1024 @classmethod
1025 def __normalize_second(cls, hour, minute, second):
1026 hour, minute = cls.__normalize_minute(hour, minute)
1027 if 0 <= second < 60:
1028 return hour, minute, float(second)
1029 raise ValueError("Second out of range (0..<60)")
1030
1031 # CLASS ATTRIBUTES #
1032
1033 min = None
1034
1035 max = None
1036
1037 resolution = None
1038
1039 # INSTANCE ATTRIBUTES #
1040
1041 __ticks = 0
1042
1043 __hour = 0
1044
1045 __minute = 0
1046
1047 __second = 0
1048
1049 __tzinfo = None
1050
1051 @property
1052 def ticks(self):
1053 """ Return the total number of seconds since midnight.
1054 """
1055 return self.__ticks
1056
1057 @property
1058 def hour(self):
1059 return self.__hour
1060
1061 @property
1062 def minute(self):
1063 return self.__minute
1064
1065 @property
1066 def second(self):
1067 return self.__second
1068
1069 @property
1070 def hour_minute_second(self):
1071 return self.__hour, self.__minute, self.__second
1072
1073 @property
1074 def tzinfo(self):
1075 return self.__tzinfo
1076
1077 # OPERATIONS #
1078
1079 def __hash__(self):
1080 return hash(self.ticks) ^ hash(self.tzinfo)
1081
1082 def __eq__(self, other):
1083 if isinstance(other, Time):
1084 return self.ticks == other.ticks and self.tzinfo == other.tzinfo
1085 if isinstance(other, time):
1086 other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1087 return self.ticks == other_ticks and self.tzinfo == other.tzinfo
1088 return False
1089
1090 def __ne__(self, other):
1091 return not self.__eq__(other)
1092
1093 def __lt__(self, other):
1094 if isinstance(other, Time):
1095 return self.ticks < other.ticks
1096 if isinstance(other, time):
1097 other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1098 return self.ticks < other_ticks
1099 raise TypeError("'<' not supported between instances of 'Time' and %r" % type(other).__name__)
1100
1101 def __le__(self, other):
1102 if isinstance(other, Time):
1103 return self.ticks <= other.ticks
1104 if isinstance(other, time):
1105 other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1106 return self.ticks <= other_ticks
1107 raise TypeError("'<=' not supported between instances of 'Time' and %r" % type(other).__name__)
1108
1109 def __ge__(self, other):
1110 if isinstance(other, Time):
1111 return self.ticks >= other.ticks
1112 if isinstance(other, time):
1113 other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1114 return self.ticks >= other_ticks
1115 raise TypeError("'>=' not supported between instances of 'Time' and %r" % type(other).__name__)
1116
1117 def __gt__(self, other):
1118 if isinstance(other, Time):
1119 return self.ticks >= other.ticks
1120 if isinstance(other, time):
1121 other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1122 return self.ticks >= other_ticks
1123 raise TypeError("'>' not supported between instances of 'Time' and %r" % type(other).__name__)
1124
1125 def __add__(self, other):
1126 if isinstance(other, Duration):
1127 return NotImplemented
1128 if isinstance(other, timedelta):
1129 return NotImplemented
1130 return NotImplemented
1131
1132 def __sub__(self, other):
1133 return NotImplemented
1134
1135 # INSTANCE METHODS #
1136
1137 def replace(self, **kwargs):
1138 """ Return a :class:`.Time` with one or more components replaced
1139 with new values.
1140 """
1141 return Time(kwargs.get("hour", self.__hour),
1142 kwargs.get("minute", self.__minute),
1143 kwargs.get("second", self.__second),
1144 kwargs.get("tzinfo", self.__tzinfo))
1145
1146 def utc_offset(self):
1147 if self.tzinfo is None:
1148 return None
1149 value = self.tzinfo.utcoffset(self)
1150 if value is None:
1151 return None
1152 if isinstance(value, timedelta):
1153 s = value.total_seconds()
1154 if not (-86400 < s < 86400):
1155 raise ValueError("utcoffset must be less than a day")
1156 if s % 60 != 0 or value.microseconds != 0:
1157 raise ValueError("utcoffset must be a whole number of minutes")
1158 return value
1159 raise TypeError("utcoffset must be a timedelta")
1160
1161 def dst(self):
1162 if self.tzinfo is None:
1163 return None
1164 value = self.tzinfo.dst(self)
1165 if value is None:
1166 return None
1167 if isinstance(value, timedelta):
1168 if value.days != 0:
1169 raise ValueError("dst must be less than a day")
1170 if value.seconds % 60 != 0 or value.microseconds != 0:
1171 raise ValueError("dst must be a whole number of minutes")
1172 return value
1173 raise TypeError("dst must be a timedelta")
1174
1175 def tzname(self):
1176 if self.tzinfo is None:
1177 return None
1178 return self.tzinfo.tzname(self)
1179
1180 def to_clock_time(self):
1181 seconds, nanoseconds = nano_divmod(self.ticks, 1) # int, float
1182 nanoseconds_int = int(Decimal(str(nanoseconds)) * NANO_SECONDS) # Convert fractions to an integer without losing precision
1183 return ClockTime(seconds, nanoseconds_int)
1184
1185 def to_native(self):
1186 """ Convert to a native Python `datetime.time` value.
1187 """
1188 h, m, s = self.hour_minute_second
1189 s, ns = nano_divmod(s, 1)
1190 ms = int(nano_mul(ns, 1000000))
1191 tz = self.tzinfo
1192 return time(h, m, s, ms, tz)
1193
1194 def iso_format(self):
1195 s = "%02d:%02d:%012.9f" % self.hour_minute_second
1196 if self.tzinfo is not None:
1197 offset = self.tzinfo.utcoffset(self)
1198 s += "%+03d:%02d" % divmod(offset.total_seconds() // 60, 60)
1199 return s
1200
1201 def __repr__(self):
1202 if self.tzinfo is None:
1203 return "neo4j.time.Time(%r, %r, %r)" % self.hour_minute_second
1204 else:
1205 return "neo4j.time.Time(%r, %r, %r, tzinfo=%r)" % (self.hour_minute_second + (self.tzinfo,))
1206
1207 def __str__(self):
1208 return self.iso_format()
1209
1210 def __format__(self, format_spec):
1211 raise NotImplementedError()
1212
1213
1214 Time.min = Time(0, 0, 0)
1215 Time.max = Time(23, 59, 59.999999999)
1216
1217 Midnight = Time.min
1218 Midday = Time(12, 0, 0)
1219
1220
1221 @total_ordering
1222 class DateTime(metaclass=DateTimeType):
1223 """ Regular construction of a :class:`.DateTime` object requires at
1224 least the `year`, `month` and `day` arguments to be supplied. The
1225 optional `hour`, `minute` and `second` arguments default to zero and
1226 `tzinfo` defaults to :const:`None`.
1227
1228 While `year`, `month`, `day`, `hour` and `minute` accept only :func:`int`
1229 values, `second` can also accept a :func:`float` value. This allows
1230 sub-second values to be passed, with up to nine decimal places of
1231 precision held by the object within the `second` attribute.
1232
1233 >>> dt = DateTime(2018, 4, 30, 12, 34, 56.789123456); dt
1234 neo4j.time.DateTime(2018, 4, 30, 12, 34, 56.789123456)
1235 >>> dt.second
1236 56.789123456
1237
1238 """
1239
1240 # CONSTRUCTOR #
1241
1242 def __new__(cls, year, month, day, hour=0, minute=0, second=0.0, tzinfo=None):
1243 return cls.combine(Date(year, month, day), Time(hour, minute, second, tzinfo))
1244
1245 def __getattr__(self, name):
1246 """ Map standard library attribute names to local attribute names,
1247 for compatibility.
1248 """
1249 try:
1250 return {
1251 "astimezone": self.as_timezone,
1252 "isocalendar": self.iso_calendar,
1253 "isoformat": self.iso_format,
1254 "isoweekday": self.iso_weekday,
1255 "strftime": self.__format__,
1256 "toordinal": self.to_ordinal,
1257 "timetuple": self.time_tuple,
1258 "utcoffset": self.utc_offset,
1259 "utctimetuple": self.utc_time_tuple,
1260 }[name]
1261 except KeyError:
1262 raise AttributeError("DateTime has no attribute %r" % name)
1263
1264 # CLASS METHODS #
1265
1266 @classmethod
1267 def now(cls, tz=None):
1268 """This may raise OverflowError if not supported, because of platform depending C libraries.
1269
1270 :param tz: time zone
1271 :returns:
1272 :rtype:
1273
1274 :raises OverflowError:
1275 """
1276 if tz is None:
1277 return cls.from_clock_time(Clock().local_time(), UnixEpoch)
1278 else:
1279 return tz.fromutc(cls.from_clock_time(Clock().utc_time(), UnixEpoch).replace(tzinfo=tz))
1280
1281 @classmethod
1282 def utc_now(cls):
1283 return cls.from_clock_time(Clock().utc_time(), UnixEpoch)
1284
1285 @classmethod
1286 def from_iso_format(cls, s):
1287 try:
1288 return cls.combine(Date.from_iso_format(s[0:10]), Time.from_iso_format(s[11:]))
1289 except ValueError:
1290 raise ValueError("DateTime string is not in ISO format")
1291
1292 @classmethod
1293 def from_timestamp(cls, timestamp, tz=None):
1294 """This may raise OverflowError, if the timestamp is out of the range of values supported by the platform C localtime() function,
1295 and OverflowError on localtime() failure. It’s common for this to be restricted to years from 1970 through 2038.
1296
1297 :param timestamp:
1298 :param tz:
1299 :returns:
1300
1301 :raises OverflowError:
1302 """
1303 if tz is None:
1304 return cls.from_clock_time(ClockTime(timestamp) + Clock().local_offset(), UnixEpoch)
1305 else:
1306 return tz.fromutc(cls.utcfromtimestamp(timestamp).replace(tzinfo=tz))
1307
1308 @classmethod
1309 def utc_from_timestamp(cls, timestamp):
1310 return cls.from_clock_time((timestamp, 0), UnixEpoch)
1311
1312 @classmethod
1313 def from_ordinal(cls, ordinal):
1314 return cls.combine(Date.from_ordinal(ordinal), Midnight)
1315
1316 @classmethod
1317 def combine(cls, date, time):
1318 assert isinstance(date, Date)
1319 assert isinstance(time, Time)
1320 instance = object.__new__(cls)
1321 instance.__date = date
1322 instance.__time = time
1323 return instance
1324
1325 @classmethod
1326 def parse(cls, date_string, format):
1327 raise NotImplementedError()
1328
1329 @classmethod
1330 def from_native(cls, dt):
1331 """ Convert from a native Python `datetime.datetime` value.
1332 """
1333 return cls.combine(Date.from_native(dt.date()), Time.from_native(dt.timetz()))
1334
1335 @classmethod
1336 def from_clock_time(cls, clock_time, epoch):
1337 """ Convert from a ClockTime relative to a given epoch.
1338 """
1339 try:
1340 seconds, nanoseconds = ClockTime(*clock_time)
1341 except (TypeError, ValueError):
1342 raise ValueError("Clock time must be a 2-tuple of (s, ns)")
1343 else:
1344 ordinal, ticks = divmod(seconds, 86400)
1345 date_ = Date.from_ordinal(ordinal + epoch.date().to_ordinal())
1346 nanoseconds = int(1000000000 * ticks + nanoseconds)
1347 time_ = Time.from_ticks(epoch.time().ticks + (nanoseconds / 1000000000))
1348 return cls.combine(date_, time_)
1349
1350 # CLASS ATTRIBUTES #
1351
1352 min = None
1353
1354 max = None
1355
1356 resolution = None
1357
1358 # INSTANCE ATTRIBUTES #
1359
1360 @property
1361 def year(self):
1362 return self.__date.year
1363
1364 @property
1365 def month(self):
1366 return self.__date.month
1367
1368 @property
1369 def day(self):
1370 return self.__date.day
1371
1372 @property
1373 def year_month_day(self):
1374 return self.__date.year_month_day
1375
1376 @property
1377 def year_week_day(self):
1378 return self.__date.year_week_day
1379
1380 @property
1381 def year_day(self):
1382 return self.__date.year_day
1383
1384 @property
1385 def hour(self):
1386 return self.__time.hour
1387
1388 @property
1389 def minute(self):
1390 return self.__time.minute
1391
1392 @property
1393 def second(self):
1394 return self.__time.second
1395
1396 @property
1397 def tzinfo(self):
1398 return self.__time.tzinfo
1399
1400 @property
1401 def hour_minute_second(self):
1402 return self.__time.hour_minute_second
1403
1404 # OPERATIONS #
1405
1406 def __hash__(self):
1407 return hash(self.date()) ^ hash(self.time())
1408
1409 def __eq__(self, other):
1410 if isinstance(other, (DateTime, datetime)):
1411 return self.date() == other.date() and self.time() == other.time()
1412 return False
1413
1414 def __ne__(self, other):
1415 return not self.__eq__(other)
1416
1417 def __lt__(self, other):
1418 if isinstance(other, (DateTime, datetime)):
1419 if self.date() == other.date():
1420 return self.time() < other.time()
1421 else:
1422 return self.date() < other.date()
1423 raise TypeError("'<' not supported between instances of 'DateTime' and %r" % type(other).__name__)
1424
1425 def __le__(self, other):
1426 if isinstance(other, (DateTime, datetime)):
1427 if self.date() == other.date():
1428 return self.time() <= other.time()
1429 else:
1430 return self.date() < other.date()
1431 raise TypeError("'<=' not supported between instances of 'DateTime' and %r" % type(other).__name__)
1432
1433 def __ge__(self, other):
1434 if isinstance(other, (DateTime, datetime)):
1435 if self.date() == other.date():
1436 return self.time() >= other.time()
1437 else:
1438 return self.date() > other.date()
1439 raise TypeError("'>=' not supported between instances of 'DateTime' and %r" % type(other).__name__)
1440
1441 def __gt__(self, other):
1442 if isinstance(other, (DateTime, datetime)):
1443 if self.date() == other.date():
1444 return self.time() > other.time()
1445 else:
1446 return self.date() > other.date()
1447 raise TypeError("'>' not supported between instances of 'DateTime' and %r" % type(other).__name__)
1448
1449 def __add__(self, other):
1450 if isinstance(other, timedelta):
1451 t = self.to_clock_time() + ClockTime(86400 * other.days + other.seconds, other.microseconds * 1000)
1452 days, seconds = symmetric_divmod(t.seconds, 86400)
1453 date_ = Date.from_ordinal(days + 1)
1454 time_ = Time.from_ticks(seconds + (t.nanoseconds / 1000000000))
1455 return self.combine(date_, time_)
1456 return NotImplemented
1457
1458 def __sub__(self, other):
1459 if isinstance(other, DateTime):
1460 self_month_ordinal = 12 * (self.year - 1) + self.month
1461 other_month_ordinal = 12 * (other.year - 1) + other.month
1462 months = self_month_ordinal - other_month_ordinal
1463 days = self.day - other.day
1464 t = self.time().to_clock_time() - other.time().to_clock_time()
1465 return Duration(months=months, days=days, seconds=t.seconds, nanoseconds=t.nanoseconds)
1466 if isinstance(other, datetime):
1467 days = self.to_ordinal() - other.toordinal()
1468 t = self.time().to_clock_time() - ClockTime(3600 * other.hour + 60 * other.minute + other.second, other.microsecond * 1000)
1469 return timedelta(days=days, seconds=t.seconds, microseconds=(t.nanoseconds // 1000))
1470 if isinstance(other, Duration):
1471 return NotImplemented
1472 if isinstance(other, timedelta):
1473 return self.__add__(-other)
1474 return NotImplemented
1475
1476 # INSTANCE METHODS #
1477
1478 def date(self):
1479 """The the date
1480
1481 :return: the date
1482 :rtype: :class:`neo4j.time.Date`
1483 """
1484 return self.__date
1485
1486 def time(self):
1487 """The the time without time zone info
1488
1489 :return: the time without time zone info
1490 :rtype: :class:`neo4j.time.Time`
1491 """
1492 return self.__time.replace(tzinfo=None)
1493
1494 def timetz(self):
1495 """The the time with time zone info
1496
1497 :return: the time with time zone info
1498 :rtype: :class:`neo4j.time.Time`
1499 """
1500 return self.__time
1501
1502 def replace(self, **kwargs):
1503 date_ = self.__date.replace(**kwargs)
1504 time_ = self.__time.replace(**kwargs)
1505 return self.combine(date_, time_)
1506
1507 def as_timezone(self, tz):
1508 if self.tzinfo is None:
1509 return self
1510 utc = (self - self.utcoffset()).replace(tzinfo=tz)
1511 return tz.fromutc(utc)
1512
1513 def utc_offset(self):
1514 return self.__time.utc_offset()
1515
1516 def dst(self):
1517 return self.__time.dst()
1518
1519 def tzname(self):
1520 return self.__time.tzname()
1521
1522 def time_tuple(self):
1523 raise NotImplementedError()
1524
1525 def utc_time_tuple(self):
1526 raise NotImplementedError()
1527
1528 def to_ordinal(self):
1529 return self.__date.to_ordinal()
1530
1531 def to_clock_time(self):
1532 total_seconds = 0
1533 for year in range(1, self.year):
1534 total_seconds += 86400 * DAYS_IN_YEAR[year]
1535 for month in range(1, self.month):
1536 total_seconds += 86400 * Date.days_in_month(self.year, month)
1537 total_seconds += 86400 * (self.day - 1)
1538 seconds, nanoseconds = nano_divmod(self.__time.ticks, 1) # int, float
1539 nanoseconds_int = int(Decimal(str(nanoseconds)) * NANO_SECONDS) # Convert fractions to an integer without losing precision
1540 return ClockTime(total_seconds + seconds, nanoseconds_int)
1541
1542 def to_native(self):
1543 """ Convert to a native Python `datetime.datetime` value.
1544 """
1545 y, mo, d = self.year_month_day
1546 h, m, s = self.hour_minute_second
1547 s, ns = nano_divmod(s, 1)
1548 ms = int(nano_mul(ns, 1000000))
1549 tz = self.tzinfo
1550 return datetime(y, mo, d, h, m, s, ms, tz)
1551
1552 def weekday(self):
1553 return self.__date.weekday()
1554
1555 def iso_weekday(self):
1556 return self.__date.iso_weekday()
1557
1558 def iso_calendar(self):
1559 return self.__date.iso_calendar()
1560
1561 def iso_format(self, sep="T"):
1562 return "%s%s%s" % (self.date().iso_format(), sep, self.timetz().iso_format())
1563
1564 def __repr__(self):
1565 if self.tzinfo is None:
1566 fields = self.year_month_day + self.hour_minute_second
1567 return "neo4j.time.DateTime(%r, %r, %r, %r, %r, %r)" % fields
1568 else:
1569 fields = self.year_month_day + self.hour_minute_second + (self.tzinfo,)
1570 return "neo4j.time.DateTime(%r, %r, %r, %r, %r, %r, tzinfo=%r)" % fields
1571
1572 def __str__(self):
1573 return self.iso_format()
1574
1575 def __format__(self, format_spec):
1576 raise NotImplementedError()
1577
1578
1579 DateTime.min = DateTime.combine(Date.min, Time.min)
1580 DateTime.max = DateTime.combine(Date.max, Time.max)
1581 DateTime.resolution = Time.resolution
1582
1583 Never = DateTime.combine(ZeroDate, Midnight)
1584 UnixEpoch = DateTime(1970, 1, 1, 0, 0, 0)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 def main():
22 from neo4j.time import Clock, DateTime, UnixEpoch
23 clock = Clock()
24 time = clock.utc_time()
25 print("Using %s" % type(clock).__name__)
26 print("%s -> %s" % (time, DateTime.from_clock_time(time, UnixEpoch)))
27
28
29 if __name__ == "__main__":
30 main()
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from math import isnan
22
23
24 def nano_add(x, y):
25 """
26
27 >>> 0.7 + 0.2
28 0.8999999999999999
29 >>> -0.7 + 0.2
30 -0.49999999999999994
31 >>> nano_add(0.7, 0.2)
32 0.9
33 >>> nano_add(-0.7, 0.2)
34 -0.5
35
36 :param x:
37 :param y:
38 :return:
39 """
40 return (int(1000000000 * x) + int(1000000000 * y)) / 1000000000
41
42
43 def nano_sub(x, y):
44 """
45
46 >>> 0.7 - 0.2
47 0.49999999999999994
48 >>> -0.7 - 0.2
49 -0.8999999999999999
50 >>> nano_sub(0.7, 0.2)
51 0.5
52 >>> nano_sub(-0.7, 0.2)
53 -0.9
54
55 :param x:
56 :param y:
57 :return:
58 """
59 return (int(1000000000 * x) - int(1000000000 * y)) / 1000000000
60
61
62 def nano_mul(x, y):
63 """
64
65 >>> 0.7 * 0.2
66 0.13999999999999999
67 >>> -0.7 * 0.2
68 -0.13999999999999999
69 >>> nano_mul(0.7, 0.2)
70 0.14
71 >>> nano_mul(-0.7, 0.2)
72 -0.14
73
74 :param x:
75 :param y:
76 :return:
77 """
78 return int(1000000000 * x) * int(1000000000 * y) / 1000000000000000000
79
80
81 def nano_div(x, y):
82 """
83
84 >>> 0.7 / 0.2
85 3.4999999999999996
86 >>> -0.7 / 0.2
87 -3.4999999999999996
88 >>> nano_div(0.7, 0.2)
89 3.5
90 >>> nano_div(-0.7, 0.2)
91 -3.5
92
93 :param x:
94 :param y:
95 :return:
96 """
97 return float(1000000000 * x) / int(1000000000 * y)
98
99
100 def nano_mod(x, y):
101 """
102
103 >>> 0.7 % 0.2
104 0.09999999999999992
105 >>> -0.7 % 0.2
106 0.10000000000000009
107 >>> nano_mod(0.7, 0.2)
108 0.1
109 >>> nano_mod(-0.7, 0.2)
110 0.1
111
112 :param x:
113 :param y:
114 :return:
115 """
116 number = type(x)
117 nx = int(1000000000 * x)
118 ny = int(1000000000 * y)
119 q, r = divmod(nx, ny)
120 return number(r / 1000000000)
121
122
123 def nano_divmod(x, y):
124 """
125
126 >>> divmod(0.7, 0.2)
127 (3.0, 0.09999999999999992)
128 >>> nano_divmod(0.7, 0.2)
129 (3, 0.1)
130
131 :param x:
132 :param y:
133 :return:
134 """
135 number = type(x)
136 nx = int(1000000000 * x)
137 ny = int(1000000000 * y)
138 q, r = divmod(nx, ny)
139 return int(q), number(r / 1000000000)
140
141
142 def signum(n):
143 try:
144 if isnan(n):
145 return float("nan")
146 if n > 0 or n == float("inf"):
147 return 1
148 if n < 0 or n == float("-inf"):
149 return -1
150 return 0
151 except TypeError:
152 raise TypeError(n)
153
154
155 def symmetric_divmod(dividend, divisor):
156 number = type(dividend)
157 if dividend >= 0:
158 quotient, remainder = divmod(dividend, divisor)
159 return int(quotient), number(remainder)
160 else:
161 quotient, remainder = divmod(-dividend, divisor)
162 return -int(quotient), -number(remainder)
163
164
165 def round_half_to_even(n):
166 """
167
168 >>> round_half_to_even(3)
169 3
170 >>> round_half_to_even(3.2)
171 3
172 >>> round_half_to_even(3.5)
173 4
174 >>> round_half_to_even(3.7)
175 4
176 >>> round_half_to_even(4)
177 4
178 >>> round_half_to_even(4.2)
179 4
180 >>> round_half_to_even(4.5)
181 4
182 >>> round_half_to_even(4.7)
183 5
184
185 :param n:
186 :return:
187 """
188 ten_n = 10 * n
189 if ten_n == int(ten_n) and ten_n % 10 == 5:
190 up = int(n + 0.5)
191 down = int(n - 0.5)
192 return up if up % 2 == 0 else down
193 else:
194 return int(round(n))
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from ctypes import CDLL, Structure, c_longlong, c_long, byref
22 from platform import uname
23
24 from neo4j.time import Clock, ClockTime
25 from neo4j.time.arithmetic import nano_divmod
26
27
28 class SafeClock(Clock):
29 """ Clock implementation that should work for any variant of Python.
30 This clock is guaranteed microsecond precision.
31 """
32
33 @classmethod
34 def precision(cls):
35 return 6
36
37 @classmethod
38 def available(cls):
39 return True
40
41 def utc_time(self):
42 from time import time
43 seconds, nanoseconds = nano_divmod(int(time() * 1000000), 1000000)
44 return ClockTime(seconds, nanoseconds * 1000)
45
46
47 class LibCClock(Clock):
48 """ Clock implementation that works only on platforms that provide
49 libc. This clock is guaranteed nanosecond precision.
50 """
51
52 __libc = "libc.dylib" if uname()[0] == "Darwin" else "libc.so.6"
53
54 class _TimeSpec(Structure):
55 _fields_ = [
56 ("seconds", c_longlong),
57 ("nanoseconds", c_long),
58 ]
59
60 @classmethod
61 def precision(cls):
62 return 9
63
64 @classmethod
65 def available(cls):
66 try:
67 _ = CDLL(cls.__libc)
68 except OSError:
69 return False
70 else:
71 return True
72
73 def utc_time(self):
74 libc = CDLL(self.__libc)
75 ts = self._TimeSpec()
76 status = libc.clock_gettime(0, byref(ts))
77 if status == 0:
78 return ClockTime(ts.seconds, ts.nanoseconds)
79 else:
80 raise RuntimeError("clock_gettime failed with status %d" % status)
81
82
83 class PEP564Clock(Clock):
84 """ Clock implementation based on the PEP564 additions to Python 3.7.
85 This clock is guaranteed nanosecond precision.
86 """
87
88 @classmethod
89 def precision(cls):
90 return 9
91
92 @classmethod
93 def available(cls):
94 try:
95 from time import time_ns
96 except ImportError:
97 return False
98 else:
99 return True
100
101 def utc_time(self):
102 from time import time_ns
103 t = time_ns()
104 seconds, nanoseconds = divmod(t, 1000000000)
105 return ClockTime(seconds, nanoseconds)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from datetime import (
22 time,
23 datetime,
24 timedelta,
25 )
26
27 from neo4j.packstream import Structure
28 from neo4j.time import (
29 Duration,
30 Date,
31 Time,
32 DateTime,
33 )
34
35
36 def get_date_unix_epoch():
37 return Date(1970, 1, 1)
38
39
40 def get_date_unix_epoch_ordinal():
41 return get_date_unix_epoch().to_ordinal()
42
43
44 def get_datetime_unix_epoch_utc():
45 from pytz import utc
46 return DateTime(1970, 1, 1, 0, 0, 0, utc)
47
48
49 def hydrate_date(days):
50 """ Hydrator for `Date` values.
51
52 :param days:
53 :return: Date
54 """
55 return Date.from_ordinal(get_date_unix_epoch_ordinal() + days)
56
57
58 def dehydrate_date(value):
59 """ Dehydrator for `date` values.
60
61 :param value:
62 :type value: Date
63 :return:
64 """
65 return Structure(b"D", value.toordinal() - get_date_unix_epoch().toordinal())
66
67
68 def hydrate_time(nanoseconds, tz=None):
69 """ Hydrator for `Time` and `LocalTime` values.
70
71 :param nanoseconds:
72 :param tz:
73 :return: Time
74 """
75 from pytz import FixedOffset
76 seconds, nanoseconds = map(int, divmod(nanoseconds, 1000000000))
77 minutes, seconds = map(int, divmod(seconds, 60))
78 hours, minutes = map(int, divmod(minutes, 60))
79 seconds = (1000000000 * seconds + nanoseconds) / 1000000000
80 t = Time(hours, minutes, seconds)
81 if tz is None:
82 return t
83 tz_offset_minutes, tz_offset_seconds = divmod(tz, 60)
84 zone = FixedOffset(tz_offset_minutes)
85 return zone.localize(t)
86
87
88 def dehydrate_time(value):
89 """ Dehydrator for `time` values.
90
91 :param value:
92 :type value: Time
93 :return:
94 """
95 if isinstance(value, Time):
96 nanoseconds = int(value.ticks * 1000000000)
97 elif isinstance(value, time):
98 nanoseconds = (3600000000000 * value.hour + 60000000000 * value.minute +
99 1000000000 * value.second + 1000 * value.microsecond)
100 else:
101 raise TypeError("Value must be a neo4j.time.Time or a datetime.time")
102 if value.tzinfo:
103 return Structure(b"T", nanoseconds, value.tzinfo.utcoffset(value).seconds)
104 else:
105 return Structure(b"t", nanoseconds)
106
107
108 def hydrate_datetime(seconds, nanoseconds, tz=None):
109 """ Hydrator for `DateTime` and `LocalDateTime` values.
110
111 :param seconds:
112 :param nanoseconds:
113 :param tz:
114 :return: datetime
115 """
116 from pytz import FixedOffset, timezone
117 minutes, seconds = map(int, divmod(seconds, 60))
118 hours, minutes = map(int, divmod(minutes, 60))
119 days, hours = map(int, divmod(hours, 24))
120 seconds = (1000000000 * seconds + nanoseconds) / 1000000000
121 t = DateTime.combine(Date.from_ordinal(get_date_unix_epoch_ordinal() + days), Time(hours, minutes, seconds))
122 if tz is None:
123 return t
124 if isinstance(tz, int):
125 tz_offset_minutes, tz_offset_seconds = divmod(tz, 60)
126 zone = FixedOffset(tz_offset_minutes)
127 else:
128 zone = timezone(tz)
129 return zone.localize(t)
130
131
132 def dehydrate_datetime(value):
133 """ Dehydrator for `datetime` values.
134
135 :param value:
136 :type value: datetime
137 :return:
138 """
139
140 def seconds_and_nanoseconds(dt):
141 if isinstance(dt, datetime):
142 dt = DateTime.from_native(dt)
143 zone_epoch = DateTime(1970, 1, 1, tzinfo=dt.tzinfo)
144 dt_clock_time = dt.to_clock_time()
145 zone_epoch_clock_time = zone_epoch.to_clock_time()
146 t = dt_clock_time - zone_epoch_clock_time
147 return t.seconds, t.nanoseconds
148
149 tz = value.tzinfo
150 if tz is None:
151 # without time zone
152 from pytz import utc
153 value = utc.localize(value)
154 seconds, nanoseconds = seconds_and_nanoseconds(value)
155 return Structure(b"d", seconds, nanoseconds)
156 elif hasattr(tz, "zone") and tz.zone:
157 # with named time zone
158 seconds, nanoseconds = seconds_and_nanoseconds(value)
159 return Structure(b"f", seconds, nanoseconds, tz.zone)
160 else:
161 # with time offset
162 seconds, nanoseconds = seconds_and_nanoseconds(value)
163 return Structure(b"F", seconds, nanoseconds, tz.utcoffset(value).seconds)
164
165
166 def hydrate_duration(months, days, seconds, nanoseconds):
167 """ Hydrator for `Duration` values.
168
169 :param months:
170 :param days:
171 :param seconds:
172 :param nanoseconds:
173 :return: `duration` namedtuple
174 """
175 return Duration(months=months, days=days, seconds=seconds, nanoseconds=nanoseconds)
176
177
178 def dehydrate_duration(value):
179 """ Dehydrator for `duration` values.
180
181 :param value:
182 :type value: Duration
183 :return:
184 """
185 return Structure(b"E", value.months, value.days, value.seconds, int(1000000000 * value.subseconds))
186
187
188 def dehydrate_timedelta(value):
189 """ Dehydrator for `timedelta` values.
190
191 :param value:
192 :type value: timedelta
193 :return:
194 """
195 months = 0
196 days = value.days
197 seconds = value.seconds
198 nanoseconds = 1000 * value.microseconds
199 return Structure(b"E", months, days, seconds, nanoseconds)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 class DateType(type):
22
23 def __getattr__(cls, name):
24 try:
25 return {
26 "fromisoformat": cls.from_iso_format,
27 "fromordinal": cls.from_ordinal,
28 "fromtimestamp": cls.from_timestamp,
29 "utcfromtimestamp": cls.utc_from_timestamp,
30 }[name]
31 except KeyError:
32 raise AttributeError("%s has no attribute %r" % (cls.__name__, name))
33
34
35 class TimeType(type):
36
37 def __getattr__(cls, name):
38 try:
39 return {
40 "fromisoformat": cls.from_iso_format,
41 "utcnow": cls.utc_now,
42 }[name]
43 except KeyError:
44 raise AttributeError("%s has no attribute %r" % (cls.__name__, name))
45
46
47 class DateTimeType(type):
48
49 def __getattr__(cls, name):
50 try:
51 return {
52 "fromisoformat": cls.from_iso_format,
53 "fromordinal": cls.from_ordinal,
54 "fromtimestamp": cls.from_timestamp,
55 "strptime": cls.parse,
56 "today": cls.now,
57 "utcfromtimestamp": cls.utc_from_timestamp,
58 "utcnow": cls.utc_now,
59 }[name]
60 except KeyError:
61 raise AttributeError("%s has no attribute %r" % (cls.__name__, name))
+0
-134
neo4j/types/__init__.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 """
21 This package contains classes for modelling the standard set of data types
22 available within a Neo4j graph database. Most non-primitive types are
23 represented by PackStream structures on the wire before being converted
24 into concrete values through the PackStreamHydrant.
25 """
26
27
28 from neo4j import Record
29 from neo4j.compat import map_type, string, integer, ustr
30
31 # These classes are imported in order to retain backward compatibility with 1.5.
32 # They should be removed in 2.0.
33 from .graph import Entity, Node, Relationship, Path
34
35
36 INT64_MIN = -(2 ** 63)
37 INT64_MAX = (2 ** 63) - 1
38
39
40 class PackStreamHydrator(object):
41
42 def __init__(self, protocol_version):
43 from .graph import Graph, hydration_functions as graph_hydration_functions
44
45 super(PackStreamHydrator, self).__init__()
46 self.graph = Graph()
47 self.hydration_functions = {}
48 self.hydration_functions.update(graph_hydration_functions(self.graph))
49 if protocol_version >= 2:
50 from .spatial import hydration_functions as spatial_hydration_functions
51 from .temporal import hydration_functions as temporal_hydration_functions
52 self.hydration_functions.update(spatial_hydration_functions())
53 self.hydration_functions.update(temporal_hydration_functions())
54
55 def hydrate(self, values):
56 """ Convert PackStream values into native values.
57 """
58 from neobolt.packstream import Structure
59
60 def hydrate_(obj):
61 if isinstance(obj, Structure):
62 try:
63 f = self.hydration_functions[obj.tag]
64 except KeyError:
65 # If we don't recognise the structure type, just return it as-is
66 return obj
67 else:
68 return f(*map(hydrate_, obj.fields))
69 elif isinstance(obj, list):
70 return list(map(hydrate_, obj))
71 elif isinstance(obj, dict):
72 return {key: hydrate_(value) for key, value in obj.items()}
73 else:
74 return obj
75
76 return tuple(map(hydrate_, values))
77
78 def hydrate_records(self, keys, record_values):
79 for values in record_values:
80 yield Record(zip(keys, self.hydrate(values)))
81
82
83 class PackStreamDehydrator(object):
84
85 def __init__(self, protocol_version, supports_bytes=False):
86 from .graph import Graph, dehydration_functions as graph_dehydration_functions
87 self.supports_bytes = supports_bytes
88 self.dehydration_functions = {}
89 self.dehydration_functions.update(graph_dehydration_functions())
90 if protocol_version >= 2:
91 from .spatial import dehydration_functions as spatial_dehydration_functions
92 from .temporal import dehydration_functions as temporal_dehydration_functions
93 self.dehydration_functions.update(spatial_dehydration_functions())
94 self.dehydration_functions.update(temporal_dehydration_functions())
95
96 def dehydrate(self, values):
97 """ Convert native values into PackStream values.
98 """
99
100 def dehydrate_(obj):
101 try:
102 f = self.dehydration_functions[type(obj)]
103 except KeyError:
104 pass
105 else:
106 return f(obj)
107 if obj is None:
108 return None
109 elif isinstance(obj, bool):
110 return obj
111 elif isinstance(obj, integer):
112 if INT64_MIN <= obj <= INT64_MAX:
113 return obj
114 raise ValueError("Integer out of bounds (64-bit signed integer values only)")
115 elif isinstance(obj, float):
116 return obj
117 elif isinstance(obj, string):
118 return ustr(obj)
119 elif isinstance(obj, (bytes, bytearray)): # order is important here - bytes must be checked after string
120 if self.supports_bytes:
121 return obj
122 else:
123 raise TypeError("This PackSteam channel does not support BYTES (consider upgrading to Neo4j 3.2+)")
124 elif isinstance(obj, (list, map_type)):
125 return list(map(dehydrate_, obj))
126 elif isinstance(obj, dict):
127 if any(not isinstance(key, string) for key in obj.keys()):
128 raise TypeError("Non-string dictionary keys are not supported")
129 return {key: dehydrate_(value) for key, value in obj.items()}
130 else:
131 raise TypeError(obj)
132
133 return tuple(map(dehydrate_, values))
+0
-379
neo4j/types/graph.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 """
21 Graph data types
22 """
23
24
25 from neo4j.compat import xstr, deprecated, Mapping
26
27
28 __all__ = [
29 "Graph",
30 "Entity",
31 "Node",
32 "Relationship",
33 "Path",
34 ]
35
36
37 class Graph(object):
38 """ Local, self-contained graph object that acts as a container for
39 :class:`.Node` and :class:`.Relationship` instances.
40 """
41
42 def __init__(self):
43 self._nodes = {}
44 self._relationships = {}
45 self._relationship_types = {}
46 self._node_set_view = EntitySetView(self._nodes)
47 self._relationship_set_view = EntitySetView(self._relationships)
48
49 @property
50 def nodes(self):
51 """ Access a set view of the nodes in this graph.
52 """
53 return self._node_set_view
54
55 @property
56 def relationships(self):
57 """ Access a set view of the relationships in this graph.
58 """
59 return self._relationship_set_view
60
61 def relationship_type(self, name):
62 """ Obtain a :class:`.Relationship` subclass for a given
63 relationship type name.
64 """
65 try:
66 cls = self._relationship_types[name]
67 except KeyError:
68 cls = self._relationship_types[name] = type(xstr(name), (Relationship,), {})
69 return cls
70
71 def put_node(self, n_id, labels=(), properties=None, **kwproperties):
72 inst = Node(self, n_id)
73 inst._labels.update(labels)
74 inst._update(properties, **kwproperties)
75 return inst
76
77 def put_relationship(self, r_id, start_node, end_node, r_type, properties=None, **kwproperties):
78 if not isinstance(start_node, Node) or not isinstance(end_node, Node):
79 raise TypeError("Start and end nodes must be Node instances (%s and %s passed)" %
80 (type(start_node).__name__, type(end_node).__name__))
81 inst = _put_unbound_relationship(self, r_id, r_type, properties, **kwproperties)
82 inst._start_node = start_node
83 inst._end_node = end_node
84 return inst
85
86
87 def _put_unbound_relationship(graph, r_id, r_type, properties=None, **kwproperties):
88 inst = Relationship(graph, r_id, r_type)
89 inst._update(properties, **kwproperties)
90 return inst
91
92
93 class Entity(Mapping):
94 """ Base class for :class:`.Node` and :class:`.Relationship` that
95 provides :class:`.Graph` membership and property containment
96 functionality.
97 """
98
99 def __new__(cls, graph, id):
100 inst = object.__new__(cls)
101 inst._graph = graph
102 inst._id = id
103 inst._properties = {}
104 return inst
105
106 def __eq__(self, other):
107 try:
108 return type(self) == type(other) and self.graph == other.graph and self.id == other.id
109 except AttributeError:
110 return False
111
112 def __ne__(self, other):
113 return not self.__eq__(other)
114
115 def __hash__(self):
116 return hash(self.id)
117
118 def __len__(self):
119 return len(self._properties)
120
121 def __getitem__(self, name):
122 return self._properties.get(name)
123
124 def __contains__(self, name):
125 return name in self._properties
126
127 def __iter__(self):
128 return iter(self._properties)
129
130 @property
131 def graph(self):
132 """ The :class:`.Graph` to which this entity belongs.
133 """
134 return self._graph
135
136 @property
137 def id(self):
138 """ The identity of this entity in its container :class:`.Graph`.
139 """
140 return self._id
141
142 def _update(self, properties, **kwproperties):
143 properties = dict(properties or {}, **kwproperties)
144 self._properties.update((k, v) for k, v in properties.items() if v is not None)
145
146 def get(self, name, default=None):
147 """ Get a property value by name, optionally with a default.
148 """
149 return self._properties.get(name, default)
150
151 def keys(self):
152 """ Return an iterable of all property names.
153 """
154 return self._properties.keys()
155
156 def values(self):
157 """ Return an iterable of all property values.
158 """
159 return self._properties.values()
160
161 def items(self):
162 """ Return an iterable of all property name-value pairs.
163 """
164 return self._properties.items()
165
166
167 class EntitySetView(Mapping):
168 """ View of a set of :class:`.Entity` instances within a :class:`.Graph`.
169 """
170
171 def __init__(self, entity_dict):
172 self._entity_dict = entity_dict
173
174 def __getitem__(self, e_id):
175 return self._entity_dict[e_id]
176
177 def __len__(self):
178 return len(self._entity_dict)
179
180 def __iter__(self):
181 return iter(self._entity_dict.values())
182
183
184 class Node(Entity):
185 """ Self-contained graph node.
186 """
187
188 def __new__(cls, graph, n_id):
189 try:
190 inst = graph._nodes[n_id]
191 except KeyError:
192 inst = graph._nodes[n_id] = Entity.__new__(cls, graph, n_id)
193 inst._labels = set()
194 return inst
195
196 def __repr__(self):
197 return "<Node id=%r labels=%r properties=%r>" % (self._id, self._labels, self._properties)
198
199 @property
200 def labels(self):
201 """ The set of labels attached to this node.
202 """
203 return frozenset(self._labels)
204
205
206 class Relationship(Entity):
207 """ Self-contained graph relationship.
208 """
209
210 def __new__(cls, graph, r_id, r_type):
211 try:
212 inst = graph._relationships[r_id]
213 except KeyError:
214 inst = graph._relationships[r_id] = Entity.__new__(cls, graph, r_id)
215 inst.__class__ = graph.relationship_type(r_type)
216 inst._start_node = None
217 inst._end_node = None
218 return inst
219
220 def __repr__(self):
221 return "<Relationship id=%r nodes=(%r, %r) type=%r properties=%r>" % (
222 self._id, self._start_node, self._end_node, self.type, self._properties)
223
224 @property
225 def nodes(self):
226 """ The pair of nodes which this relationship connects.
227 """
228 return self._start_node, self._end_node
229
230 @property
231 def start_node(self):
232 """ The start node of this relationship.
233 """
234 return self._start_node
235
236 @property
237 def end_node(self):
238 """ The end node of this relationship.
239 """
240 return self._end_node
241
242 @property
243 def type(self):
244 """ The type name of this relationship.
245 This is functionally equivalent to ``type(relationship).__name__``.
246 """
247 return type(self).__name__
248
249 @property
250 @deprecated("Relationship.start is deprecated, please use Relationship.start_node.id instead")
251 def start(self):
252 return self.start_node.id
253
254 @property
255 @deprecated("Relationship.end is deprecated, please use Relationship.end_node.id instead")
256 def end(self):
257 return self.end_node.id
258
259
260 class Path(object):
261 """ Self-contained graph path.
262 """
263
264 def __init__(self, start_node, *relationships):
265 assert isinstance(start_node, Node)
266 nodes = [start_node]
267 for i, relationship in enumerate(relationships, start=1):
268 assert isinstance(relationship, Relationship)
269 if relationship.start_node == nodes[-1]:
270 nodes.append(relationship.end_node)
271 elif relationship.end_node == nodes[-1]:
272 nodes.append(relationship.start_node)
273 else:
274 raise ValueError("Relationship %d does not connect to the last node" % i)
275 self._nodes = tuple(nodes)
276 self._relationships = relationships
277
278 def __repr__(self):
279 return "<Path start=%r end=%r size=%s>" % \
280 (self.start_node, self.end_node, len(self))
281
282 def __eq__(self, other):
283 try:
284 return self.start_node == other.start_node and self.relationships == other.relationships
285 except AttributeError:
286 return False
287
288 def __ne__(self, other):
289 return not self.__eq__(other)
290
291 def __hash__(self):
292 value = hash(self._nodes[0])
293 for relationship in self._relationships:
294 value ^= hash(relationship)
295 return value
296
297 def __len__(self):
298 return len(self._relationships)
299
300 def __iter__(self):
301 return iter(self._relationships)
302
303 @property
304 def graph(self):
305 """ The :class:`.Graph` to which this path belongs.
306 """
307 return self._nodes[0].graph
308
309 @property
310 def nodes(self):
311 """ The sequence of :class:`.Node` objects in this path.
312 """
313 return self._nodes
314
315 @property
316 def start_node(self):
317 """ The first :class:`.Node` in this path.
318 """
319 return self._nodes[0]
320
321 @property
322 def end_node(self):
323 """ The last :class:`.Node` in this path.
324 """
325 return self._nodes[-1]
326
327 @property
328 def relationships(self):
329 """ The sequence of :class:`.Relationship` objects in this path.
330 """
331 return self._relationships
332
333 @property
334 @deprecated("Path.start is deprecated, please use Path.start_node instead")
335 def start(self):
336 return self.start_node
337
338 @property
339 @deprecated("Path.end is deprecated, please use Path.end_node instead")
340 def end(self):
341 return self.end_node
342
343
344 def hydrate_path(nodes, relationships, sequence):
345 assert len(nodes) >= 1
346 assert len(sequence) % 2 == 0
347 last_node = nodes[0]
348 entities = [last_node]
349 for i, rel_index in enumerate(sequence[::2]):
350 assert rel_index != 0
351 next_node = nodes[sequence[2 * i + 1]]
352 if rel_index > 0:
353 r = relationships[rel_index - 1]
354 r._start_node = last_node
355 r._end_node = next_node
356 entities.append(r)
357 else:
358 r = relationships[-rel_index - 1]
359 r._start_node = next_node
360 r._end_node = last_node
361 entities.append(r)
362 last_node = next_node
363 return Path(*entities)
364
365
366 def hydration_functions(graph):
367 return {
368 b"N": graph.put_node,
369 b"R": lambda r_id, n0_id, n1_id, r_type, properties:
370 graph.put_relationship(r_id, Node(graph, n0_id), Node(graph, n1_id), r_type, properties),
371 b"r": lambda *args: _put_unbound_relationship(graph, *args),
372 b"P": hydrate_path,
373 }
374
375
376 def dehydration_functions():
377 # There is no support for passing graph types into queries as parameters
378 return {}
+0
-154
neo4j/types/spatial.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 """
22 This module defines spatial data types.
23 """
24
25
26 from neobolt.packstream import Structure
27
28
29 __all__ = [
30 "Point",
31 "CartesianPoint",
32 "WGS84Point",
33 ]
34
35
36 # SRID to subclass mappings
37 __srid_table = {}
38
39
40 class Point(tuple):
41 """ A point within a geometric space. This type is generally used
42 via its subclasses and should not be instantiated directly unless
43 there is no subclass defined for the required SRID.
44 """
45
46 srid = None
47
48 def __new__(cls, iterable):
49 return tuple.__new__(cls, iterable)
50
51 def __repr__(self):
52 return "POINT(%s)" % " ".join(map(str, self))
53
54 def __eq__(self, other):
55 try:
56 return type(self) is type(other) and tuple(self) == tuple(other)
57 except (AttributeError, TypeError):
58 return False
59
60 def __ne__(self, other):
61 return not self.__eq__(other)
62
63 def __hash__(self):
64 return hash(type(self)) ^ hash(tuple(self))
65
66
67 def __point_subclass(name, fields, srid_map):
68 """ Dynamically create a Point subclass.
69 """
70
71 def srid(self):
72 try:
73 return srid_map[len(self)]
74 except KeyError:
75 return None
76
77 attributes = {"srid": property(srid)}
78
79 for index, subclass_field in enumerate(fields):
80
81 def accessor(self, i=index, f=subclass_field):
82 try:
83 return self[i]
84 except IndexError:
85 raise AttributeError(f)
86
87 for field_alias in {subclass_field, "xyz"[index]}:
88 attributes[field_alias] = property(accessor)
89
90 cls = type(name, (Point,), attributes)
91
92 for dim, srid in srid_map.items():
93 __srid_table[srid] = (cls, dim)
94
95 return cls
96
97
98 # Point subclass definitions
99 CartesianPoint = __point_subclass("CartesianPoint", ["x", "y", "z"], {2: 7203, 3: 9157})
100 WGS84Point = __point_subclass("WGS84Point", ["longitude", "latitude", "height"], {2: 4326, 3: 4979})
101
102
103 def hydrate_point(srid, *coordinates):
104 """ Create a new instance of a Point subclass from a raw
105 set of fields. The subclass chosen is determined by the
106 given SRID; a ValueError will be raised if no such
107 subclass can be found.
108 """
109 try:
110 point_class, dim = __srid_table[srid]
111 except KeyError:
112 point = Point(coordinates)
113 point.srid = srid
114 return point
115 else:
116 if len(coordinates) != dim:
117 raise ValueError("SRID %d requires %d coordinates (%d provided)" % (srid, dim, len(coordinates)))
118 return point_class(coordinates)
119
120
121 def dehydrate_point(value):
122 """ Dehydrator for Point data.
123
124 :param value:
125 :type value: Point
126 :return:
127 """
128 dim = len(value)
129 if dim == 2:
130 return Structure(b"X", value.srid, *value)
131 elif dim == 3:
132 return Structure(b"Y", value.srid, *value)
133 else:
134 raise ValueError("Cannot dehydrate Point with %d dimensions" % dim)
135
136
137 __hydration_functions = {
138 b"X": hydrate_point,
139 b"Y": hydrate_point,
140 }
141
142 __dehydration_functions = {
143 Point: dehydrate_point,
144 }
145 __dehydration_functions.update({cls: dehydrate_point for cls in Point.__subclasses__()})
146
147
148 def hydration_functions():
149 return __hydration_functions
150
151
152 def dehydration_functions():
153 return __dehydration_functions
+0
-218
neo4j/types/temporal.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from __future__ import division
22
23
24 """
25 This module defines temporal data types.
26 """
27
28 from datetime import date, time, datetime, timedelta
29
30 from neotime import Duration, Date, Time, DateTime
31 from pytz import FixedOffset, timezone, utc
32
33 from neobolt.packstream import Structure
34
35
36 UNIX_EPOCH_DATE = Date(1970, 1, 1)
37 UNIX_EPOCH_DATE_ORDINAL = UNIX_EPOCH_DATE.to_ordinal()
38 UNIX_EPOCH_DATETIME_UTC = DateTime(1970, 1, 1, 0, 0, 0, utc)
39
40
41 def hydrate_date(days):
42 """ Hydrator for `Date` values.
43
44 :param days:
45 :return: Date
46 """
47 return Date.from_ordinal(UNIX_EPOCH_DATE_ORDINAL + days)
48
49
50 def dehydrate_date(value):
51 """ Dehydrator for `date` values.
52
53 :param value:
54 :type value: Date
55 :return:
56 """
57 return Structure(b"D", value.toordinal() - UNIX_EPOCH_DATE.toordinal())
58
59
60 def hydrate_time(nanoseconds, tz=None):
61 """ Hydrator for `Time` and `LocalTime` values.
62
63 :param nanoseconds:
64 :param tz:
65 :return: Time
66 """
67 seconds, nanoseconds = map(int, divmod(nanoseconds, 1000000000))
68 minutes, seconds = map(int, divmod(seconds, 60))
69 hours, minutes = map(int, divmod(minutes, 60))
70 seconds = (1000000000 * seconds + nanoseconds) / 1000000000
71 t = Time(hours, minutes, seconds)
72 if tz is None:
73 return t
74 tz_offset_minutes, tz_offset_seconds = divmod(tz, 60)
75 zone = FixedOffset(tz_offset_minutes)
76 return zone.localize(t)
77
78
79 def dehydrate_time(value):
80 """ Dehydrator for `time` values.
81
82 :param value:
83 :type value: Time
84 :return:
85 """
86 if isinstance(value, Time):
87 nanoseconds = int(value.ticks * 1000000000)
88 elif isinstance(value, time):
89 nanoseconds = (3600000000000 * value.hour + 60000000000 * value.minute +
90 1000000000 * value.second + 1000 * value.microsecond)
91 else:
92 raise TypeError("Value must be a neotime.Time or a datetime.time")
93 if value.tzinfo:
94 return Structure(b"T", nanoseconds, value.tzinfo.utcoffset(value).seconds)
95 else:
96 return Structure(b"t", nanoseconds)
97
98
99 def hydrate_datetime(seconds, nanoseconds, tz=None):
100 """ Hydrator for `DateTime` and `LocalDateTime` values.
101
102 :param seconds:
103 :param nanoseconds:
104 :param tz:
105 :return: datetime
106 """
107 minutes, seconds = map(int, divmod(seconds, 60))
108 hours, minutes = map(int, divmod(minutes, 60))
109 days, hours = map(int, divmod(hours, 24))
110 seconds = (1000000000 * seconds + nanoseconds) / 1000000000
111 t = DateTime.combine(Date.from_ordinal(UNIX_EPOCH_DATE_ORDINAL + days), Time(hours, minutes, seconds))
112 if tz is None:
113 return t
114 if isinstance(tz, int):
115 tz_offset_minutes, tz_offset_seconds = divmod(tz, 60)
116 zone = FixedOffset(tz_offset_minutes)
117 else:
118 zone = timezone(tz)
119 return zone.localize(t)
120
121
122 def dehydrate_datetime(value):
123 """ Dehydrator for `datetime` values.
124
125 :param value:
126 :type value: datetime
127 :return:
128 """
129
130 def seconds_and_nanoseconds(dt):
131 if isinstance(dt, datetime):
132 dt = DateTime.from_native(dt)
133 zone_epoch = DateTime(1970, 1, 1, tzinfo=dt.tzinfo)
134 t = dt.to_clock_time() - zone_epoch.to_clock_time()
135 return t.seconds, t.nanoseconds
136
137 tz = value.tzinfo
138 if tz is None:
139 # without time zone
140 value = utc.localize(value)
141 seconds, nanoseconds = seconds_and_nanoseconds(value)
142 return Structure(b"d", seconds, nanoseconds)
143 elif hasattr(tz, "zone") and tz.zone:
144 # with named time zone
145 seconds, nanoseconds = seconds_and_nanoseconds(value)
146 return Structure(b"f", seconds, nanoseconds, tz.zone)
147 else:
148 # with time offset
149 seconds, nanoseconds = seconds_and_nanoseconds(value)
150 return Structure(b"F", seconds, nanoseconds, tz.utcoffset(value).seconds)
151
152
153 def hydrate_duration(months, days, seconds, nanoseconds):
154 """ Hydrator for `Duration` values.
155
156 :param months:
157 :param days:
158 :param seconds:
159 :param nanoseconds:
160 :return: `duration` namedtuple
161 """
162 return Duration(months=months, days=days, seconds=seconds, nanoseconds=nanoseconds)
163
164
165 def dehydrate_duration(value):
166 """ Dehydrator for `duration` values.
167
168 :param value:
169 :type value: Duration
170 :return:
171 """
172 return Structure(b"E", value.months, value.days, value.seconds, int(1000000000 * value.subseconds))
173
174
175 def dehydrate_timedelta(value):
176 """ Dehydrator for `timedelta` values.
177
178 :param value:
179 :type value: timedelta
180 :return:
181 """
182 months = 0
183 days = value.days
184 seconds = value.seconds
185 nanoseconds = 1000 * value.microseconds
186 return Structure(b"E", months, days, seconds, nanoseconds)
187
188
189 __hydration_functions = {
190 b"D": hydrate_date,
191 b"T": hydrate_time, # time zone offset
192 b"t": hydrate_time, # no time zone
193 b"F": hydrate_datetime, # time zone offset
194 b"f": hydrate_datetime, # time zone name
195 b"d": hydrate_datetime, # no time zone
196 b"E": hydrate_duration,
197 }
198
199 # TODO: re-add built-in types
200 __dehydration_functions = {
201 Date: dehydrate_date,
202 date: dehydrate_date,
203 Time: dehydrate_time,
204 time: dehydrate_time,
205 DateTime: dehydrate_datetime,
206 datetime: dehydrate_datetime,
207 Duration: dehydrate_duration,
208 timedelta: dehydrate_timedelta,
209 }
210
211
212 def hydration_functions():
213 return __hydration_functions
214
215
216 def dehydration_functions():
217 return __dehydration_functions
+0
-26
neo4j/v1/__init__.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from .. import *
22
23
24 from warnings import warn as _warn
25 _warn("The 'neo4j.v1' package is deprecated, import from 'neo4j' instead", category=DeprecationWarning, stacklevel=2)
+0
-22
neo4j/v1/types/__init__.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.types import *
+0
-22
neo4j/v1/types/graph.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.types.graph import *
+0
-22
neo4j/v1/types/spatial.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.types.spatial import *
+0
-22
neo4j/v1/types/temporal.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.types.temporal import *
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.conf import WorkspaceConfig
22 from neo4j.exceptions import ServiceUnavailable
23
24
25 class Workspace:
26
27 def __init__(self, pool, config):
28 assert isinstance(config, WorkspaceConfig)
29 self._pool = pool
30 self._config = config
31 self._connection = None
32 self._connection_access_mode = None
33
34 def __del__(self):
35 try:
36 self.close()
37 except OSError:
38 pass
39
40 def __enter__(self):
41 return self
42
43 def __exit__(self, exc_type, exc_value, traceback):
44 self.close()
45
46 def _connect(self, access_mode):
47 if self._connection:
48 if access_mode == self._connection_access_mode:
49 return
50 self._disconnect(sync=True)
51 self._connection = self._pool.acquire(access_mode=access_mode, timeout=self._config.connection_acquisition_timeout, database=self._config.database)
52 self._connection_access_mode = access_mode
53
54 def _disconnect(self, sync):
55 if self._connection:
56 if sync:
57 try:
58 self._connection.send_all()
59 self._connection.fetch_all()
60 except (WorkspaceError, ServiceUnavailable):
61 pass
62 if self._connection:
63 self._connection.in_use = False
64 self._connection = None
65 self._connection_access_mode = None
66
67 def close(self):
68 self._disconnect(sync=True)
69
70
71 class WorkspaceError(Exception):
72
73 pass
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2018 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import deque
22 from threading import Thread, Lock
23 from time import sleep
24
25 from neo4j.work import Workspace
26 from neo4j.conf import WorkspaceConfig
27 from neo4j.api import (
28 WRITE_ACCESS,
29 )
30
31 class PipelineConfig(WorkspaceConfig):
32
33 #:
34 flush_every = 8192 # bytes
35
36
37 class Pipeline(Workspace):
38
39 def __init__(self, pool, config):
40 assert isinstance(config, PipelineConfig)
41 super(Pipeline, self).__init__(pool, config)
42 self._connect(WRITE_ACCESS)
43 self._flush_every = config.flush_every
44 self._data = deque()
45 self._pull_lock = Lock()
46
47 def push(self, statement, parameters=None):
48 self._connection.run(statement, parameters)
49 self._connection.pull(on_records=self._data.extend)
50 output_buffer_size = len(self._connection.outbox.view())
51 if output_buffer_size >= self._flush_every:
52 self._connection.send_all()
53
54 def _results_generator(self):
55 results_returned_count = 0
56 try:
57 summary = 0
58 while summary == 0:
59 _, summary = self._connection.fetch_message()
60 summary = 0
61 while summary == 0:
62 detail, summary = self._connection.fetch_message()
63 for n in range(detail):
64 response = self._data.popleft()
65 results_returned_count += 1
66 yield response
67 finally:
68 self._pull_lock.release()
69
70 def pull(self):
71 """Returns a generator containing the results of the next query in the pipeline"""
72 # n.b. pull is now somewhat misleadingly named because it doesn't do anything
73 # the connection isn't touched until you try and iterate the generator we return
74 lock_acquired = self._pull_lock.acquire(blocking=False)
75 if not lock_acquired:
76 raise PullOrderException()
77 return self._results_generator()
78
79
80 class PullOrderException(Exception):
81 """Raise when calling pull if a previous pull result has not been fully consumed"""
82
83
84 class Pusher(Thread):
85
86 def __init__(self, pipeline):
87 super(Pusher, self).__init__()
88 self.pipeline = pipeline
89 self.running = True
90 self.count = 0
91
92 def run(self):
93 while self.running:
94 self.pipeline.push("RETURN $x", {"x": self.count})
95 self.count += 1
96
97
98 class Puller(Thread):
99
100 def __init__(self, pipeline):
101 super(Puller, self).__init__()
102 self.pipeline = pipeline
103 self.running = True
104 self.count = 0
105
106 def run(self):
107 while self.running:
108 for _ in self.pipeline.pull():
109 pass # consume and discard records
110 self.count += 1
111
112
113 def main():
114 from neo4j import Driver
115 # from neo4j.bolt.diagnostics import watch
116 # watch("neobolt")
117 with Driver("bolt://", auth=("neo4j", "password")) as dx:
118 p = dx.pipeline(flush_every=1024)
119 pusher = Pusher(p)
120 puller = Puller(p)
121 try:
122 pusher.start()
123 puller.start()
124 while True:
125 print("sent %d, received %d, backlog %d" % (pusher.count, puller.count, pusher.count - puller.count))
126 sleep(1)
127 except KeyboardInterrupt:
128 pusher.running = False
129 pusher.join()
130 puller.running = False
131 puller.join()
132
133
134 if __name__ == "__main__":
135 main()
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import deque
22 from warnings import warn
23
24 from neo4j.data import DataDehydrator
25 from neo4j.work.summary import ResultSummary
26
27
28 class Result:
29 """A handler for the result of Cypher query execution. Instances
30 of this class are typically constructed and returned by
31 :meth:`.Session.run` and :meth:`.Transaction.run`.
32 """
33
34 def __init__(self, connection, hydrant, fetch_size, on_closed):
35 self._connection = connection
36 self._hydrant = hydrant
37 self._on_closed = on_closed
38 self._metadata = None
39 self._record_buffer = deque()
40 self._summary = None
41 self._bookmark = None
42 self._qid = -1
43 self._fetch_size = fetch_size
44
45 # states
46 self._discarding = False # discard the remainder of records
47 self._attached = False # attached to a connection
48 self._streaming = False # there is still more records to buffer upp on the wire
49 self._has_more = False # there is more records available to pull from the server
50 self._closed = False # the result have been properly iterated or consumed fully
51
52 def _tx_ready_run(self, query, parameters, **kwparameters):
53 # BEGIN+RUN does not carry any extra on the RUN message.
54 # BEGIN {extra}
55 # RUN "query" {parameters} {extra}
56 self._run(query, parameters, None, None, None, **kwparameters)
57
58 def _run(self, query, parameters, db, access_mode, bookmarks, **kwparameters):
59 query_text = str(query) # Query or string object
60 query_metadata = getattr(query, "metadata", None)
61 query_timeout = getattr(query, "timeout", None)
62
63 parameters = DataDehydrator.fix_parameters(dict(parameters or {}, **kwparameters))
64
65 self._metadata = {
66 "query": query_text,
67 "parameters": parameters,
68 "server": self._connection.server_info,
69 }
70
71 run_metadata = {
72 "metadata": query_metadata,
73 "timeout": query_timeout,
74 }
75
76 def on_attached(metadata):
77 self._metadata.update(metadata)
78 self._qid = metadata.get("qid", -1) # For auto-commit there is no qid and Bolt 3 do not support qid
79 self._keys = metadata.get("fields")
80 self._attached = True
81
82 def on_failed_attach(metadata):
83 self._metadata.update(metadata)
84 self._attached = False
85 self._on_closed()
86
87 self._connection.run(
88 query_text,
89 parameters=parameters,
90 mode=access_mode,
91 bookmarks=bookmarks,
92 metadata=query_metadata,
93 timeout=query_timeout,
94 db=db,
95 on_success=on_attached,
96 on_failure=on_failed_attach,
97 )
98 self._pull()
99 self._connection.send_all()
100 self._attach()
101
102 def _pull(self):
103
104 def on_records(records):
105 self._streaming = True
106 if not self._discarding:
107 self._record_buffer.extend(self._hydrant.hydrate_records(self._keys, records))
108
109 def on_summary():
110 self._attached = False
111 self._on_closed()
112
113 def on_failure(metadata):
114 self._attached = False
115 self._on_closed()
116
117 def on_success(summary_metadata):
118 has_more = summary_metadata.get("has_more")
119 if has_more:
120 self._has_more = True
121 self._streaming = False
122 return
123 else:
124 self._has_more = False
125
126 self._metadata.update(summary_metadata)
127 self._bookmark = summary_metadata.get("bookmark")
128
129 self._connection.pull(
130 n=self._fetch_size,
131 qid=self._qid,
132 on_records=on_records,
133 on_success=on_success,
134 on_failure=on_failure,
135 on_summary=on_summary,
136 )
137
138 def _discard(self):
139 def on_records(records):
140 pass
141
142 def on_summary():
143 self._attached = False
144 self._on_closed()
145
146 def on_failure(metadata):
147 self._metadata.update(metadata)
148 self._attached = False
149 self._on_closed()
150
151 def on_success(summary_metadata):
152 has_more = summary_metadata.get("has_more")
153 if has_more:
154 self._has_more = True
155 self._streaming = False
156 else:
157 self._has_more = False
158 self._discarding = False
159
160 self._metadata.update(summary_metadata)
161 self._bookmark = summary_metadata.get("bookmark")
162
163 # This was the last page received, discard the rest
164 self._connection.discard(
165 n=-1,
166 qid=self._qid,
167 on_records=on_records,
168 on_success=on_success,
169 on_failure=on_failure,
170 on_summary=on_summary,
171 )
172
173 def __iter__(self):
174 """Iterator returning Records.
175 :returns: Record, it is an immutable ordered collection of key-value pairs.
176 :rtype: :class:`neo4j.Record`
177 """
178 while self._record_buffer or self._attached:
179 while self._record_buffer:
180 yield self._record_buffer.popleft()
181
182 while self._attached is True: # _attached is set to False for _pull on_summary and _discard on_summary
183 self._connection.fetch_message() # Receive at least one message from the server, if available.
184 if self._attached:
185 if self._record_buffer:
186 yield self._record_buffer.popleft()
187 elif self._discarding and self._streaming is False:
188 self._discard()
189 self._connection.send_all()
190 elif self._has_more and self._streaming is False:
191 self._pull()
192 self._connection.send_all()
193
194 self._closed = True
195
196 def _attach(self):
197 """Sets the Result object in an attached state by fetching messages from the connection to the buffer.
198 """
199 if self._closed is False:
200 while self._attached is False:
201 self._connection.fetch_message()
202
203 def _buffer_all(self):
204 """Sets the Result object in an detached state by fetching all records from the connection to the buffer.
205 """
206 record_buffer = deque()
207 for record in self:
208 record_buffer.append(record)
209 self._closed = False
210 self._record_buffer = record_buffer
211
212 def _obtain_summary(self):
213 """Obtain the summary of this result, buffering any remaining records.
214
215 :returns: The :class:`neo4j.ResultSummary` for this result
216 """
217 if self._summary is None:
218 if self._metadata:
219 self._summary = ResultSummary(**self._metadata)
220 elif self._connection:
221 self._summary = ResultSummary(server=self._connection.server_info)
222
223 return self._summary
224
225 def keys(self):
226 """The keys for the records in this result.
227
228 :returns: tuple of key names
229 :rtype: tuple
230 """
231 return self._keys
232
233 def consume(self):
234 """Consume the remainder of this result and return a :class:`neo4j.ResultSummary`.
235
236 Example::
237
238 def create_node_tx(tx, name):
239 result = tx.run("CREATE (n:ExampleNode { name: $name }) RETURN n", name=name)
240 record = result.single()
241 value = record.value()
242 info = result.consume()
243 return value, info
244
245 with driver.session() as session:
246 node_id, info = session.write_transaction(create_node_tx, "example")
247
248 Example::
249
250 def get_two_tx(tx):
251 result = tx.run("UNWIND [1,2,3,4] AS x RETURN x")
252 values = []
253 for ix, record in enumerate(result):
254 if x > 1:
255 break
256 values.append(record.values())
257 info = result.consume() # discard the remaining records if there are any
258 # use the info for logging etc.
259 return values, info
260
261 with driver.session() as session:
262 values, info = session.read_transaction(get_two_tx)
263
264 :returns: The :class:`neo4j.ResultSummary` for this result
265 """
266 if self._closed is False:
267 self._discarding = True
268 for _ in self:
269 pass
270
271 return self._obtain_summary()
272
273 def single(self):
274 """Obtain the next and only remaining record from this result if available else return None.
275 Calling this method always exhausts the result.
276
277 A warning is generated if more than one record is available but
278 the first of these is still returned.
279
280 :returns: the next :class:`neo4j.Record` or :const:`None` if none remain
281 :warns: if more than one record is available
282 """
283 records = list(self) # TODO: exhausts the result with self.consume if there are more records.
284 size = len(records)
285 if size == 0:
286 return None
287 if size != 1:
288 warn("Expected a result with a single record, but this result contains %d" % size)
289 return records[0]
290
291 def peek(self):
292 """Obtain the next record from this result without consuming it.
293 This leaves the record in the buffer for further processing.
294
295 :returns: the next :class:`.Record` or :const:`None` if none remain
296 """
297 if self._record_buffer:
298 return self._record_buffer[0]
299 if not self._attached:
300 return None
301 while self._attached:
302 self._connection.fetch_message()
303 if self._record_buffer:
304 return self._record_buffer[0]
305
306 return None
307
308 def graph(self):
309 """Return a :class:`neo4j.graph.Graph` instance containing all the graph objects
310 in the result. After calling this method, the result becomes
311 detached, buffering all remaining records.
312
313 :returns: a result graph
314 :rtype: :class:`neo4j.graph.Graph`
315 """
316 self._buffer_all()
317 return self._hydrant.graph
318
319 def value(self, key=0, default=None):
320 """Helper function that return the remainder of the result as a list of values.
321
322 See :class:`neo4j.Record.value`
323
324 :param key: field to return for each remaining record. Obtain a single value from the record by index or key.
325 :param default: default value, used if the index of key is unavailable
326 :returns: list of individual values
327 :rtype: list
328 """
329 return [record.value(key, default) for record in self]
330
331 def values(self, *keys):
332 """Helper function that return the remainder of the result as a list of values lists.
333
334 See :class:`neo4j.Record.values`
335
336 :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key.
337 :returns: list of values lists
338 :rtype: list
339 """
340 return [record.values(*keys) for record in self]
341
342 def data(self, *keys):
343 """Helper function that return the remainder of the result as a list of dictionaries.
344
345 See :class:`neo4j.Record.data`
346
347 :param keys: fields to return for each remaining record. Optionally filtering to include only certain values by index or key.
348 :returns: list of dictionaries
349 :rtype: list
350 """
351 return [record.data(*keys) for record in self]
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import deque
22 from logging import getLogger
23 from random import random
24 from time import perf_counter, sleep
25 from warnings import warn
26
27 from neo4j.conf import SessionConfig
28 from neo4j.api import READ_ACCESS, WRITE_ACCESS
29 from neo4j.data import DataHydrator, DataDehydrator
30 from neo4j.exceptions import (
31 Neo4jError,
32 ServiceUnavailable,
33 TransientError,
34 SessionExpired,
35 TransactionError,
36 ClientError,
37 )
38 from neo4j._exceptions import BoltIncompleteCommitError
39 from neo4j.work import Workspace
40 from neo4j.work.summary import ResultSummary
41 from neo4j.work.transaction import Transaction
42 from neo4j.work.result import Result
43 from neo4j.io._bolt3 import Bolt3
44
45 log = getLogger("neo4j")
46
47
48 class Session(Workspace):
49 """A :class:`.Session` is a logical context for transactional units
50 of work. Connections are drawn from the :class:`.Driver` connection
51 pool as required.
52
53 Session creation is a lightweight operation and sessions are not thread
54 safe. Therefore a session should generally be short-lived, and not
55 span multiple threads.
56
57 In general, sessions will be created and destroyed within a `with`
58 context. For example::
59
60 with driver.session() as session:
61 result = session.run("MATCH (n:Person) RETURN n.name AS name")
62 # do something with the result...
63
64 :param pool: connection pool instance
65 :param config: session config instance
66 """
67
68 # The current connection.
69 _connection = None
70
71 # The current :class:`.Transaction` instance, if any.
72 _transaction = None
73
74 # The current auto-transaction result, if any.
75 _autoResult = None
76
77 # The set of bookmarks after which the next
78 # :class:`.Transaction` should be carried out.
79 _bookmarks = None
80
81 # The state this session is in.
82 _state_failed = False
83
84 # Session have been properly closed.
85 _closed = False
86
87 def __init__(self, pool, session_config):
88 super().__init__(pool, session_config)
89 assert isinstance(session_config, SessionConfig)
90 self._bookmarks = tuple(session_config.bookmarks)
91
92 def __del__(self):
93 try:
94 self.close()
95 except OSError:
96 pass
97
98 def __enter__(self):
99 return self
100
101 def __exit__(self, exception_type, exception_value, traceback):
102 if exception_type:
103 self._state_failed = True
104 self.close()
105
106 def _connect(self, access_mode, database):
107 if access_mode is None:
108 access_mode = self._config.default_access_mode
109 if self._connection:
110 # TODO: Investigate this
111 # log.warning("FIXME: should always disconnect before connect")
112 self._connection.send_all()
113 self._connection.fetch_all()
114 self._disconnect()
115 self._connection = self._pool.acquire(access_mode=access_mode, timeout=self._config.connection_acquisition_timeout, database=database)
116
117 def _disconnect(self):
118 if self._connection:
119 self._connection.in_use = False
120 self._connection = None
121
122 def _collect_bookmark(self, bookmark):
123 if bookmark:
124 self._bookmarks = [bookmark]
125
126 def _result_closed(self):
127 if self._autoResult:
128 self._collect_bookmark(self._autoResult._bookmark)
129 self._autoResult = None
130 self._disconnect()
131
132 def close(self):
133 """Close the session. This will release any borrowed resources, such as connections, and will roll back any outstanding transactions.
134 """
135 if self._connection:
136 if self._autoResult:
137 if self._state_failed is False:
138 try:
139 self._autoResult.consume()
140 self._collect_bookmark(self._autoResult._bookmark)
141 except Exception as error:
142 # TODO: Investigate potential non graceful close states
143 self._autoResult = None
144 self._state_failed = True
145
146 if self._transaction:
147 if self._transaction.closed() is False:
148 self._transaction.rollback() # roll back the transaction if it is not closed
149 self._transaction = None
150
151 try:
152 if self._connection:
153 self._connection.send_all()
154 self._connection.fetch_all()
155 # TODO: Investigate potential non graceful close states
156 except Neo4jError:
157 pass
158 except TransactionError:
159 pass
160 except ServiceUnavailable:
161 pass
162 except SessionExpired:
163 pass
164 finally:
165 self._disconnect()
166
167 self._state_failed = False
168 self._closed = True
169
170 def run(self, query, parameters=None, **kwparameters):
171 """Run a Cypher query within an auto-commit transaction.
172
173 The query is sent and the result header received
174 immediately but the :class:`neo4j.Result` content is
175 fetched lazily as consumed by the client application.
176
177 If a query is executed before a previous
178 :class:`neo4j.Result` in the same :class:`.Session` has
179 been fully consumed, the first result will be fully fetched
180 and buffered. Note therefore that the generally recommended
181 pattern of usage is to fully consume one result before
182 executing a subsequent query. If two results need to be
183 consumed in parallel, multiple :class:`.Session` objects
184 can be used as an alternative to result buffering.
185
186 For more usage details, see :meth:`.Transaction.run`.
187
188 :param query: cypher query
189 :type query: str, neo4j.Query
190 :param parameters: dictionary of parameters
191 :type parameters: dict
192 :param kwparameters: additional keyword parameters
193 :returns: a new :class:`neo4j.Result` object
194 :rtype: :class:`neo4j.Result`
195 """
196 if not query:
197 raise ValueError("Cannot run an empty query")
198 if not isinstance(query, (str, Query)):
199 raise TypeError("query must be a string or a Query instance")
200
201 if self._transaction:
202 raise ClientError("Explicit Transaction must be handled explicitly")
203
204 if self._autoResult:
205 self._autoResult._buffer_all() # This will buffer upp all records for the previous auto-transaction
206
207 if not self._connection:
208 self._connect(self._config.default_access_mode, database=self._config.database)
209 cx = self._connection
210 protocol_version = cx.PROTOCOL_VERSION
211 server_info = cx.server_info
212
213 hydrant = DataHydrator()
214
215 self._autoResult = Result(cx, hydrant, self._config.fetch_size, self._result_closed)
216 self._autoResult._run(query, parameters, self._config.database, self._config.default_access_mode, self._bookmarks, **kwparameters)
217
218 return self._autoResult
219
220 def last_bookmark(self):
221 """Return the bookmark received following the last completed transaction.
222 Note: For auto-transaction (Session.run) this will trigger an consume for the current result.
223
224 :returns: :class:`neo4j.Bookmark` object
225 """
226 # The set of bookmarks to be passed into the next transaction.
227
228 if self._autoResult:
229 self._autoResult.consume()
230
231 if self._transaction and self._transaction._closed:
232 self._collect_bookmark(self._transaction._bookmark)
233 self._transaction = None
234
235 if len(self._bookmarks):
236 return self._bookmarks[len(self._bookmarks)-1]
237 return None
238
239 def _transaction_closed_handler(self):
240 if self._transaction:
241 self._collect_bookmark(self._transaction._bookmark)
242 self._transaction = None
243 self._disconnect()
244
245 def _open_transaction(self, *, access_mode, database, metadata=None, timeout=None):
246 self._connect(access_mode=access_mode, database=database)
247 self._transaction = Transaction(self._connection, self._config.fetch_size, self._transaction_closed_handler)
248 self._transaction._begin(database, self._bookmarks, access_mode, metadata, timeout)
249
250 def begin_transaction(self, metadata=None, timeout=None):
251 """ Begin a new unmanaged transaction. Creates a new :class:`.Transaction` within this session.
252 At most one transaction may exist in a session at any point in time.
253 To maintain multiple concurrent transactions, use multiple concurrent sessions.
254
255 Note: For auto-transaction (Session.run) this will trigger an consume for the current result.
256
257 :param metadata:
258 a dictionary with metadata.
259 Specified metadata will be attached to the executing transaction and visible in the output of ``dbms.listQueries`` and ``dbms.listTransactions`` procedures.
260 It will also get logged to the ``query.log``.
261 This functionality makes it easier to tag transactions and is equivalent to ``dbms.setTXMetaData`` procedure, see https://neo4j.com/docs/operations-manual/current/reference/procedures/ for procedure reference.
262 :type metadata: dict
263
264 :param timeout:
265 the transaction timeout in milliseconds.
266 Transactions that execute longer than the configured timeout will be terminated by the database.
267 This functionality allows to limit query/transaction execution time.
268 Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting.
269 Value should not represent a duration of zero or negative duration.
270 :type timeout: int
271
272 :returns: A new transaction instance.
273 :rtype: :class:`neo4j.Transaction`
274
275 :raises TransactionError: :class:`neo4j.exceptions.TransactionError` if a transaction is already open.
276 """
277 # TODO: Implement TransactionConfig consumption
278
279 if self._autoResult:
280 self._autoResult.consume()
281
282 if self._transaction:
283 raise TransactionError("Explicit transaction already open")
284
285 self._open_transaction(access_mode=self._config.default_access_mode, database=self._config.database, metadata=metadata, timeout=timeout)
286
287 return self._transaction
288
289 def _run_transaction(self, access_mode, transaction_function, *args, **kwargs):
290
291 if not callable(transaction_function):
292 raise TypeError("Unit of work is not callable")
293
294 metadata = getattr(transaction_function, "metadata", None)
295 timeout = getattr(transaction_function, "timeout", None)
296
297 retry_delay = retry_delay_generator(self._config.initial_retry_delay, self._config.retry_delay_multiplier, self._config.retry_delay_jitter_factor)
298
299 errors = []
300
301 t0 = -1 # Timer
302
303 while True:
304 try:
305 self._open_transaction(access_mode=access_mode, database=self._config.database, metadata=metadata, timeout=timeout)
306 tx = self._transaction
307 try:
308 result = transaction_function(tx, *args, **kwargs)
309 except Exception:
310 tx.rollback()
311 raise
312 else:
313 tx.commit()
314 except (ServiceUnavailable, SessionExpired) as error:
315 errors.append(error)
316 self._disconnect()
317 except TransientError as transient_error:
318 if not transient_error.is_retriable():
319 raise
320 errors.append(transient_error)
321 else:
322 return result
323 if t0 == -1:
324 t0 = perf_counter() # The timer should be started after the first attempt
325 t1 = perf_counter()
326 if t1 - t0 > self._config.max_transaction_retry_time:
327 break
328 delay = next(retry_delay)
329 log.warning("Transaction failed and will be retried in {}s ({})".format(delay, "; ".join(errors[-1].args)))
330 sleep(delay)
331
332 if errors:
333 raise errors[-1]
334 else:
335 raise ServiceUnavailable("Transaction failed")
336
337 def read_transaction(self, transaction_function, *args, **kwargs):
338 """Execute a unit of work in a managed read transaction.
339 This transaction will automatically be committed unless an exception is thrown during query execution or by the user code.
340 Note, that this function perform retries and that the supplied `transaction_function` might get invoked more than once.
341
342 Managed transactions should not generally be explicitly committed (via tx.commit()).
343
344 Example::
345
346 def do_cypher_tx(tx, cypher):
347 result = tx.run(cypher)
348 values = []
349 for record in result:
350 values.append(record.values())
351 return values
352
353 with driver.session() as session:
354 values = session.read_transaction(do_cypher_tx, "RETURN 1 AS x")
355
356 Example::
357
358 def get_two_tx(tx):
359 result = tx.run("UNWIND [1,2,3,4] AS x RETURN x")
360 values = []
361 for ix, record in enumerate(result):
362 if x > 1:
363 break
364 values.append(record.values())
365 info = result.consume() # discard the remaining records if there are any
366 # use the info for logging etc.
367 return values
368
369 with driver.session() as session:
370 values = session.read_transaction(get_two_tx)
371
372 :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. `tx_function(tx, \*args, \*\*kwargs)`
373 :param args: arguments for the `transaction_function`
374 :param kwargs: key word arguments for the `transaction_function`
375 :return: a result as returned by the given unit of work
376 """
377 return self._run_transaction(READ_ACCESS, transaction_function, *args, **kwargs)
378
379 def write_transaction(self, transaction_function, *args, **kwargs):
380 """Execute a unit of work in a managed write transaction.
381 This transaction will automatically be committed unless an exception is thrown during query execution or by the user code.
382 Note, that this function perform retries and that the supplied `transaction_function` might get invoked more than once.
383
384 Managed transactions should not generally be explicitly committed (via tx.commit()).
385
386 Example::
387
388 def create_node_tx(tx, name):
389 result = tx.run("CREATE (n:NodeExample { name: $name }) RETURN id(n) AS node_id", name=name)
390 record = result.single()
391 return record["node_id"]
392
393 with driver.session() as session:
394 node_id = session.write_transaction(create_node_tx, "example")
395
396
397 :param transaction_function: a function that takes a transaction as an argument and does work with the transaction. `tx_function(tx, \*args, \*\*kwargs)`
398 :param args: key word arguments for the `transaction_function`
399 :param kwargs: key word arguments for the `transaction_function`
400 :return: a result as returned by the given unit of work
401 """
402 return self._run_transaction(WRITE_ACCESS, transaction_function, *args, **kwargs)
403
404
405 class Query:
406 """ Create a new query.
407
408 :param text: The query text.
409 :type text: str
410 :param metadata: metadata attached to the query.
411 :type metadata: dict
412 :param timeout: seconds.
413 :type timeout: int
414 """
415 def __init__(self, text, metadata=None, timeout=None):
416 self.text = text
417
418 self.metadata = metadata
419 self.timeout = timeout
420
421 def __str__(self):
422 return str(self.text)
423
424
425 def unit_of_work(metadata=None, timeout=None):
426 """This function is a decorator for transaction functions that allows extra control over how the transaction is carried out.
427
428 For example, a timeout may be applied::
429
430 @unit_of_work(timeout=100)
431 def count_people_tx(tx):
432 result = tx.run("MATCH (a:Person) RETURN count(a) AS persons")
433 record = result.single()
434 return record["persons"]
435
436 :param metadata:
437 a dictionary with metadata.
438 Specified metadata will be attached to the executing transaction and visible in the output of ``dbms.listQueries`` and ``dbms.listTransactions`` procedures.
439 It will also get logged to the ``query.log``.
440 This functionality makes it easier to tag transactions and is equivalent to ``dbms.setTXMetaData`` procedure, see https://neo4j.com/docs/operations-manual/current/reference/procedures/ for procedure reference.
441 :type metadata: dict
442
443 :param timeout:
444 the transaction timeout in milliseconds.
445 Transactions that execute longer than the configured timeout will be terminated by the database.
446 This functionality allows to limit query/transaction execution time.
447 Specified timeout overrides the default timeout configured in the database using ``dbms.transaction.timeout`` setting.
448 Value should not represent a duration of zero or negative duration.
449 :type timeout: int
450 """
451
452 def wrapper(f):
453
454 def wrapped(*args, **kwargs):
455 return f(*args, **kwargs)
456
457 wrapped.metadata = metadata
458 wrapped.timeout = timeout
459 return wrapped
460
461 return wrapper
462
463
464 def retry_delay_generator(initial_delay, multiplier, jitter_factor):
465 delay = initial_delay
466 while True:
467 jitter = jitter_factor * delay
468 yield delay - jitter + (2 * jitter * random())
469 delay *= multiplier
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import namedtuple
22
23 BOLT_VERSION_1 = 1
24 BOLT_VERSION_2 = 2
25 BOLT_VERSION_3 = 3
26 BOLT_VERSION_4 = 4
27
28 # TODO: This logic should be inside the Bolt subclasses, because it can change depending on Bolt Protocol Version.
29
30
31 class ResultSummary:
32 """ A summary of execution returned with a :class:`.Result` object.
33 """
34
35 #: A :class:`neo4j.ServerInfo` instance. Provides some basic information of the server where the result is obtained from.
36 server = None
37
38 #: The database name where this summary is obtained from.
39 database = None
40
41 #: The query that was executed to produce this result.
42 query = None
43
44 #: Dictionary of parameters passed with the statement.
45 parameters = None
46
47 #: A string that describes the type of query (``'r'`` = read-only, ``'rw'`` = read/write).
48 query_type = None
49
50 #: A :class:`neo4j.SummaryCounters` instance. Counters for operations the query triggered.
51 counters = None
52
53 #: Dictionary that describes how the database will execute the query.
54 plan = None
55
56 #: Dictionary that describes how the database executed the query.
57 profile = None
58
59 #: The time it took for the server to have the result available. (milliseconds)
60 result_available_after = None
61
62 #: The time it took for the server to consume the result. (milliseconds)
63 result_consumed_after = None
64
65 #: A list of Dictionaries containing notification information.
66 #: Notifications provide extra information for a user executing a statement.
67 #: They can be warnings about problematic queries or other valuable information that can be
68 #: presented in a client.
69 #: Unlike failures or errors, notifications do not affect the execution of a statement.
70 notifications = None
71
72 def __init__(self, **metadata):
73 self.metadata = metadata
74 self.server = metadata.get("server")
75 self.database = metadata.get("db")
76 self.query = metadata.get("query")
77 self.parameters = metadata.get("parameters")
78 self.query_type = metadata.get("type")
79 self.plan = metadata.get("plan")
80 self.profile = metadata.get("profile")
81 self.notifications = metadata.get("notifications")
82 self.counters = SummaryCounters(metadata.get("stats", {}))
83 if self.server.protocol_version[0] < BOLT_VERSION_3:
84 self.result_available_after = metadata.get("result_available_after")
85 self.result_consumed_after = metadata.get("result_consumed_after")
86 else:
87 self.result_available_after = metadata.get("t_first")
88 self.result_consumed_after = metadata.get("t_last")
89
90
91 class SummaryCounters:
92 """ Contains counters for various operations that a query triggered.
93 """
94
95 #:
96 nodes_created = 0
97
98 #:
99 nodes_deleted = 0
100
101 #:
102 relationships_created = 0
103
104 #:
105 relationships_deleted = 0
106
107 #:
108 properties_set = 0
109
110 #:
111 labels_added = 0
112
113 #:
114 labels_removed = 0
115
116 #:
117 indexes_added = 0
118
119 #:
120 indexes_removed = 0
121
122 #:
123 constraints_added = 0
124
125 #:
126 constraints_removed = 0
127
128 #:
129 system_updates = 0
130
131 def __init__(self, statistics):
132 for key, value in dict(statistics).items():
133 key = key.replace("-", "_")
134 setattr(self, key, value)
135
136 def __repr__(self):
137 return repr(vars(self))
138
139 @property
140 def contains_updates(self):
141 """True if any of the counters except for system_updates, are greater than 0. Otherwise False."""
142 return bool(self.nodes_created or self.nodes_deleted or
143 self.relationships_created or self.relationships_deleted or
144 self.properties_set or self.labels_added or self.labels_removed or
145 self.indexes_added or self.indexes_removed or
146 self.constraints_added or self.constraints_removed)
147
148 @property
149 def contains_system_updates(self):
150 """True if the system database was updated, otherwise False."""
151 return self.system_updates > 0
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.work.result import Result
22 from neo4j.data import DataHydrator
23 from neo4j._exceptions import BoltIncompleteCommitError
24 from neo4j.exceptions import (
25 ServiceUnavailable,
26 TransactionError,
27 )
28
29
30 class Transaction:
31 """ Container for multiple Cypher queries to be executed within
32 a single context. Transactions can be used within a :py:const:`with`
33 block where the transaction is committed or rolled back on based on
34 whether or not an exception is raised::
35
36 with session.begin_transaction() as tx:
37 pass
38
39 """
40
41 def __init__(self, connection, fetch_size, on_closed):
42 self._connection = connection
43 self._bookmark = None
44 self._results = []
45 self._closed = False
46 self._fetch_size = fetch_size
47 self._on_closed = on_closed
48
49 def __enter__(self):
50 return self
51
52 def __exit__(self, exception_type, exception_value, traceback):
53 if self._closed:
54 return
55 success = not bool(exception_type)
56 if success:
57 self.commit()
58 self.close()
59
60 def _begin(self, database, bookmarks, access_mode, metadata, timeout):
61 self._connection.begin(bookmarks=bookmarks, metadata=metadata, timeout=timeout, mode=access_mode, db=database)
62
63 def _result_on_closed_handler(self):
64 pass
65
66 def _consume_results(self):
67 for result in self._results:
68 result.consume()
69 self._results = []
70
71 def run(self, query, parameters=None, **kwparameters):
72 """ Run a Cypher query within the context of this transaction.
73
74 The query is sent to the server lazily, when its result is
75 consumed. To force the query to be sent to the server, use
76 the :meth:`.Transaction.sync` method.
77
78 Cypher is typically expressed as a query template plus a
79 set of named parameters. In Python, parameters may be expressed
80 through a dictionary of parameters, through individual parameter
81 arguments, or as a mixture of both. For example, the `run`
82 queries below are all equivalent::
83
84 >>> query = "CREATE (a:Person { name: $name, age: $age })"
85 >>> result = tx.run(query, {"name": "Alice", "age": 33})
86 >>> result = tx.run(query, {"name": "Alice"}, age=33)
87 >>> result = tx.run(query, name="Alice", age=33)
88
89 Parameter values can be of any type supported by the Neo4j type
90 system. In Python, this includes :class:`bool`, :class:`int`,
91 :class:`str`, :class:`list` and :class:`dict`. Note however that
92 :class:`list` properties must be homogenous.
93
94 :param query: cypher query
95 :type query: str
96 :param parameters: dictionary of parameters
97 :type parameters: dict
98 :param kwparameters: additional keyword parameters
99 :returns: a new :class:`neo4j.Result` object
100 :rtype: :class:`neo4j.Result`
101 :raise TransactionError: if the transaction is already closed
102 """
103 from neo4j.work.simple import Query
104 if isinstance(query, Query):
105 raise ValueError("Query object is only supported for session.run")
106
107 if self._closed:
108 raise TransactionError("Transaction closed")
109
110 if self._results and self._connection.supports_multiple_results is False:
111 # Bolt 3 Support
112 self._results[-1]._buffer_all() # Buffer upp all records for the previous Result because it does not have any qid to fetch in batches.
113
114 result = Result(self._connection, DataHydrator(), self._fetch_size, self._result_on_closed_handler)
115 self._results.append(result)
116
117 result._tx_ready_run(query, parameters, **kwparameters)
118
119 return result
120
121 def commit(self):
122 """Mark this transaction as successful and close in order to trigger a COMMIT.
123
124 :raise TransactionError: if the transaction is already closed
125 """
126 if self._closed:
127 raise TransactionError("Transaction closed")
128 metadata = {}
129 self._consume_results() # DISCARD pending records then do a commit.
130 try:
131 self._connection.commit(on_success=metadata.update)
132 self._connection.send_all()
133 self._connection.fetch_all()
134 except BoltIncompleteCommitError:
135 self._closed = True
136 self._on_closed()
137 raise ServiceUnavailable("Connection closed during commit")
138 self._bookmark = metadata.get("bookmark")
139 self._closed = True
140 self._on_closed()
141
142 return self._bookmark
143
144 def rollback(self):
145 """Mark this transaction as unsuccessful and close in order to trigger a ROLLBACK.
146
147 :raise TransactionError: if the transaction is already closed
148 """
149 if self._closed:
150 raise TransactionError("Transaction closed")
151 metadata = {}
152 if not self._connection._is_reset:
153 self._consume_results() # DISCARD pending records then do a rollback.
154 self._connection.rollback(on_success=metadata.update)
155 self._connection.send_all()
156 self._connection.fetch_all()
157 self._closed = True
158 self._on_closed()
159
160 def close(self):
161 """Close this transaction, triggering a ROLLBACK if not closed.
162
163 :raise TransactionError: if the transaction could not perform a ROLLBACK.
164 """
165 if self._closed:
166 return
167 self.rollback()
168
169 def closed(self):
170 """Indicator to show whether the transaction has been closed.
171
172 :return: :const:`True` if closed, :const:`False` otherwise.
173 :rtype: bool
174 """
175 return self._closed
0 neobolt~=1.7.15
1 neotime~=1.7.1
0 pytz
00 #!/usr/bin/env python
11 # -*- encoding: utf-8 -*-
22
3 # Copyright (c) 2002-2019 "Neo4j,"
3 # Copyright (c) 2002-2020 "Neo4j,"
44 # Neo4j Sweden AB [http://neo4j.com]
55 #
66 # This file is part of Neo4j.
1818 # limitations under the License.
1919
2020
21 from __future__ import print_function
22
23 from os.path import dirname, join as path_join
24 try:
25 from setuptools import setup, Extension
26 except ImportError:
27 from distutils.core import setup, Extension
21 import os
22 from setuptools import find_packages, setup
2823
2924 from neo4j.meta import package, version
3025
3126 install_requires = [
32 "neobolt~=1.7.15",
33 "neotime~=1.7.1",
27 "pytz",
3428 ]
3529 classifiers = [
3630 "Intended Audience :: Developers",
3832 "Operating System :: OS Independent",
3933 "Topic :: Database",
4034 "Topic :: Software Development",
41 "Programming Language :: Python :: 2.7", # TODO 2.0: remove
42 "Programming Language :: Python :: 3.4",
4335 "Programming Language :: Python :: 3.5",
4436 "Programming Language :: Python :: 3.6",
4537 "Programming Language :: Python :: 3.7",
38 "Programming Language :: Python :: 3.8",
4639 ]
47 packages = [
48 "neo4j",
49 "neo4j.compat",
50 "neo4j.types",
51 "neo4j.v1", # TODO 2.0: remove
52 "neo4j.v1.types", # TODO 2.0: remove
53 ]
40 entry_points = {
41 "console_scripts": [
42 "pybolt = neo4j.bolt.__main__:main",
43 ],
44 }
45 packages = find_packages(exclude=["tests"])
46
47 readme_path = os.path.abspath(os.path.join(os.path.dirname(__file__), "README.rst"))
48 with open(readme_path, mode="r", encoding="utf-8") as fr:
49 readme = fr.read()
50
5451 setup_args = {
5552 "name": package,
5653 "version": version,
5754 "description": "Neo4j Bolt driver for Python",
5855 "license": "Apache License, Version 2.0",
59 "long_description": open(path_join(dirname(__file__), "README.rst")).read(),
60 "author": "Neo Technology",
56 "long_description": readme,
57 "author": "Neo4j, Inc.",
6158 "author_email": "[email protected]",
6259 "keywords": "neo4j graph database",
6360 "url": "https://github.com/neo4j/neo4j-python-driver",
6461 "install_requires": install_requires,
6562 "classifiers": classifiers,
6663 "packages": packages,
64 "entry_points": entry_points,
6765 }
6866
6967 setup(**setup_args)
+0
-0
test/__init__.py less more
(Empty file)
+0
-44
test/auth.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from sys import argv
22
23 from neobolt.util import Watcher
24
25 from neo4j import GraphDatabase, basic_auth
26
27
28 def update_password(user, password, new_password):
29 """ Test utility for setting the initial password.
30
31 :param user: user name
32 :param password: current password
33 :param new_password: new password
34 """
35
36 token = basic_auth(user, password)
37 setattr(token, "new-credentials", new_password) # TODO: hopefully switch hyphen to underscore on server
38 GraphDatabase.driver("bolt://localhost:7687", auth=token).session().close()
39
40
41 if __name__ == "__main__":
42 Watcher("neo4j.bolt").watch()
43 update_password("neo4j", "neo4j", argv[1])
+0
-34
test/env.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from os import getenv
22
23
24 # Full path of a server package to be used for integration testing
25 NEO4J_SERVER_PACKAGE = getenv("NEO4J_SERVER_PACKAGE")
26
27 # Name of a user for the currently running server
28 NEO4J_USER = getenv("NEO4J_USER")
29
30 # Password for the currently running server
31 NEO4J_PASSWORD = getenv("NEO4J_PASSWORD")
32
33 NEOCTRL_ARGS = getenv("NEOCTRL_ARGS", "3.4.1")
+0
-0
test/examples/__init__.py less more
(Empty file)
+0
-37
test/examples/auth_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::basic-auth-import[]
21 from neo4j import GraphDatabase
22 # end::basic-auth-import[]
23
24
25 class BasicAuthExample:
26 # tag::basic-auth[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password))
29 # end::basic-auth[]
30
31 def close(self):
32 self._driver.close()
33
34 def can_connect(self):
35 result = self._driver.session().run("RETURN 1")
36 return result.single()[0] == 1
+0
-42
test/examples/autocommit_transaction_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from test.examples.base_application import BaseApplication
22
23 # tag::autocommit-transaction-import[]
24 from neo4j import Statement
25 # end::autocommit-transaction-import[]
26
27
28 class AutocommitTransactionExample(BaseApplication):
29 def __init__(self, uri, user, password):
30 super(AutocommitTransactionExample, self).__init__(uri, user, password)
31
32 # tag::autocommit-transaction[]
33 def add_person(self, name):
34 with self._driver.session() as session:
35 session.run("CREATE (a:Person {name: $name})", name=name)
36
37 # Alternative implementation, with timeout
38 def add_person_within_half_a_second(self, name):
39 with self._driver.session() as session:
40 session.run(Statement("CREATE (a:Person {name: $name})", timeout=0.5), name=name)
41 # end::autocommit-transaction[]
+0
-29
test/examples/base_application.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from neo4j import GraphDatabase
21
22
23 class BaseApplication(object):
24 def __init__(self, uri, user, password):
25 self._driver = GraphDatabase.driver(uri, auth=(user, password), max_retry_time=0)
26
27 def close(self):
28 self._driver.close()
+0
-39
test/examples/config_connection_pool_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-connection-pool-import[]
21 from neo4j import GraphDatabase
22 # end::config-connection-pool-import[]
23
24
25 class ConfigConnectionPoolExample:
26 # tag::config-connection-pool[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password),
29 max_connection_lifetime=30 * 60, max_connection_pool_size=50,
30 connection_acquisition_timeout=2 * 60)
31 # end::config-connection-pool[]
32
33 def close(self):
34 self._driver.close()
35
36 def can_connect(driver):
37 result = driver.session().run("RETURN 1")
38 return result.single()[0] == 1
+0
-37
test/examples/config_connection_timeout_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-connection-timeout-import[]
21 from neo4j import GraphDatabase
22 # end::config-connection-timeout-import[]
23
24
25 class ConfigConnectionTimeoutExample:
26 # tag::config-connection-timeout[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password), connection_timeout=15)
29 # end::config-connection-timeout[]
30
31 def close(self):
32 self._driver.close()
33
34 def can_connect(self):
35 result = self._driver.session().run("RETURN 1")
36 return result.single()[0] == 1
+0
-37
test/examples/config_load_balancing_strategy_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-load-balancing-strategy-import[]
21 from neo4j import GraphDatabase, LOAD_BALANCING_STRATEGY_LEAST_CONNECTED
22 # end::config-load-balancing-strategy-import[]
23
24
25 class ConfigLoadBalancingStrategyExample:
26 # tag::config-load-balancing-strategy[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password), load_balancing_strategy=LOAD_BALANCING_STRATEGY_LEAST_CONNECTED)
29 # end::config-load-balancing-strategy[]
30
31 def close(self):
32 self._driver.close()
33
34 def can_connect(self):
35 result = self._driver.session().run("RETURN 1")
36 return result.single()[0] == 1
+0
-37
test/examples/config_max_retry_time_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-max-retry-time-import[]
21 from neo4j import GraphDatabase
22 # end::config-max-retry-time-import[]
23
24
25 class ConfigMaxRetryTimeExample:
26 # tag::config-max-retry-time[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password), max_retry_time=15)
29 # end::config-max-retry-time[]
30
31 def close(self):
32 self._driver.close()
33
34 def can_connect(self):
35 result = self._driver.session().run("RETURN 1")
36 return result.single()[0] == 1
+0
-33
test/examples/config_trust_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-trust-import[]
21 from neo4j import GraphDatabase, TRUST_ALL_CERTIFICATES
22 # end::config-trust-import[]
23
24
25 class ConfigTrustExample:
26 # tag::config-trust[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password), trust=TRUST_ALL_CERTIFICATES)
29 # end::config-trust[]
30
31 def close(self):
32 self._driver.close()
+0
-33
test/examples/config_unencrypted_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::config-unencrypted-import[]
21 from neo4j import GraphDatabase
22 # end::config-unencrypted-import[]
23
24
25 class ConfigUnencryptedExample:
26 # tag::config-unencrypted[]
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password), encrypted=False)
29 # end::config-unencrypted[]
30
31 def close(self):
32 self._driver.close()
+0
-38
test/examples/custom_auth_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::custom-auth-import[]
21 from neo4j import GraphDatabase, custom_auth
22 # end::custom-auth-import[]
23
24
25 class CustomAuthExample:
26 # tag::custom-auth[]
27 def __init__(self, uri, principal, credentials, realm, scheme, **parameters):
28 self._driver = GraphDatabase.driver(uri, auth=custom_auth(principal, credentials, realm, scheme, **parameters))
29 # end::custom-auth[]
30
31 def close(self):
32 self._driver.close()
33
34 def can_connect(self):
35 with self._driver.session() as session:
36 result = session.run("RETURN 1")
37 return result.single()[0] == 1
+0
-46
test/examples/custom_resolver_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::custom-resolver-import[]
22 from neo4j import GraphDatabase
23 # end::custom-resolver-import[]
24
25
26 # tag::custom-resolver[]
27
28 class CustomResolverExample:
29
30 def __init__(self, uri, user, password):
31 self._driver = GraphDatabase.driver(uri, auth=(user, password), resolver=self.resolve)
32
33 def resolve(self, address):
34 host, port = address
35 if host == "x.example.com":
36 yield "a.example.com", port
37 yield "b.example.com", port
38 yield "c.example.com", port
39 else:
40 yield host, port
41
42 def close(self):
43 self._driver.close()
44
45 # end::custom-resolver[]
+0
-45
test/examples/cypher_error_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::cypher-error-import[]
23 from neo4j import ClientError
24 # end::cypher-error-import[]
25
26
27 class CypherErrorExample(BaseApplication):
28 def __init__(self, uri, user, password):
29 super(CypherErrorExample, self).__init__(uri, user, password)
30
31 # tag::cypher-error[]
32 def get_employee_number(self, name):
33 with self._driver.session() as session:
34 return session.read_transaction(self.select_employee, name)
35
36 @staticmethod
37 def select_employee(tx, name):
38 try:
39 result = tx.run("SELECT * FROM Employees WHERE name = $name", name=name)
40 return result.single()["employee_number"]
41 except ClientError as error:
42 print(error.message)
43 return -1
44 # end::cypher-error[]
+0
-33
test/examples/driver_lifecycle_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::driver-lifecycle-import[]
21 from neo4j import GraphDatabase
22 # end::driver-lifecycle-import[]
23
24
25 # tag::driver-lifecycle[]
26 class DriverLifecycleExample:
27 def __init__(self, uri, user, password):
28 self._driver = GraphDatabase.driver(uri, auth=(user, password))
29
30 def close(self):
31 self._driver.close()
32 # end::driver-lifecycle[]
+0
-50
test/examples/hello_world_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::hello-world-import[]
21 from neo4j import GraphDatabase
22 # end::hello-world-import[]
23
24
25 # tag::hello-world[]
26 class HelloWorldExample(object):
27
28 def __init__(self, uri, user, password):
29 self._driver = GraphDatabase.driver(uri, auth=(user, password))
30
31 def close(self):
32 self._driver.close()
33
34 def print_greeting(self, message):
35 with self._driver.session() as session:
36 greeting = session.write_transaction(self._create_and_return_greeting, message)
37 print(greeting)
38
39 @staticmethod
40 def _create_and_return_greeting(tx, message):
41 result = tx.run("CREATE (a:Greeting) "
42 "SET a.message = $message "
43 "RETURN a.message + ', from node ' + id(a)", message=message)
44 return result.single()[0]
45 # end::hello-world[]
46
47 # tag::hello-world-output[]
48 # hello, world, from node 1234
49 # end::hello-world-output[]
+0
-33
test/examples/kerberos_auth_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag::kerberos-auth-import[]
21 from neo4j import GraphDatabase, kerberos_auth
22 # end::kerberos-auth-import[]
23
24
25 class KerberosAuthExample:
26 # tag::kerberos-auth[]
27 def __init__(self, uri, ticket):
28 self._driver = GraphDatabase.driver(uri, auth=kerberos_auth(ticket))
29 # end::kerberos-auth[]
30
31 def close(self):
32 self._driver.close()
+0
-84
test/examples/pass_bookmarks_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 # tag:pass-bookmarks-import[]
21 from neo4j import GraphDatabase
22 # end::pass-bookmarks-import[]
23
24
25 # tag::pass-bookmarks[]
26 class BookmarksExample(object):
27
28 def __init__(self, uri, user, password):
29 self._driver = GraphDatabase.driver(uri, auth=(user, password))
30
31 def close(self):
32 self._driver.close()
33
34 # Create a person node.
35 @classmethod
36 def create_person(cls, tx, name):
37 tx.run("CREATE (:Person {name: $name})", name=name)
38
39 # Create an employment relationship to a pre-existing company node.
40 # This relies on the person first having been created.
41 @classmethod
42 def employ(cls, tx, person_name, company_name):
43 tx.run("MATCH (person:Person {name: $person_name}) "
44 "MATCH (company:Company {name: $company_name}) "
45 "CREATE (person)-[:WORKS_FOR]->(company)",
46 person_name=person_name, company_name=company_name)
47
48 # Create a friendship between two people.
49 @classmethod
50 def create_friendship(cls, tx, name_a, name_b):
51 tx.run("MATCH (a:Person {name: $name_a}) "
52 "MATCH (b:Person {name: $name_b}) "
53 "MERGE (a)-[:KNOWS]->(b)",
54 name_a=name_a, name_b=name_b)
55
56 # Match and display all friendships.
57 @classmethod
58 def print_friendships(cls, tx):
59 result = tx.run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
60 for record in result:
61 print("{} knows {}".format(record["a.name"] ,record["b.name"]))
62
63 def main(self):
64 saved_bookmarks = [] # To collect the session bookmarks
65
66 # Create the first person and employment relationship.
67 with self._driver.session() as session_a:
68 session_a.write_transaction(self.create_person, "Alice")
69 session_a.write_transaction(self.employ, "Alice", "Wayne Enterprises")
70 saved_bookmarks.append(session_a.last_bookmark())
71
72 # Create the second person and employment relationship.
73 with self._driver.session() as session_b:
74 session_b.write_transaction(self.create_person, "Bob")
75 session_b.write_transaction(self.employ, "Bob", "LexCorp")
76 saved_bookmarks.append(session_b.last_bookmark())
77
78 # Create a friendship between the two people created above.
79 with self._driver.session(bookmarks=saved_bookmarks) as session_c:
80 session_c.write_transaction(self.create_friendship, "Alice", "Bob")
81 session_c.read_transaction(self.print_friendships)
82
83 # end::pass-bookmarks[]
+0
-46
test/examples/read_write_transaction_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::read-write-transaction-import[]
23 # end::read-write-transaction-import[]
24
25
26 class ReadWriteTransactionExample(BaseApplication):
27 def __init__(self, uri, user, password):
28 super(ReadWriteTransactionExample, self).__init__(uri, user, password)
29
30 # tag::read-write-transaction[]
31 def add_person(self, name):
32 with self._driver.session() as session:
33 session.write_transaction(self.create_person_node, name)
34 return session.read_transaction(self.match_person_node, name)
35
36 @staticmethod
37 def create_person_node(tx, name):
38 tx.run("CREATE (a:Person {name: $name})", name=name)
39 return None
40
41 @staticmethod
42 def match_person_node(tx, name):
43 result = tx.run("MATCH (a:Person {name: $name}) RETURN count(a)", name=name)
44 return result.single()[0]
45 # end::read-write-transaction[]
+0
-40
test/examples/result_consume_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::result-consume-import[]
23 # end::result-consume-import[]
24
25
26 class ResultConsumeExample(BaseApplication):
27 def __init__(self, uri, user, password):
28 super(ResultConsumeExample, self).__init__(uri, user, password)
29
30 # tag::result-consume[]
31 def get_people(self):
32 with self._driver.session() as session:
33 return session.read_transaction(self.match_person_nodes)
34
35 @staticmethod
36 def match_person_nodes(tx):
37 result = tx.run("MATCH (a:Person) RETURN a.name ORDER BY a.name")
38 return [record["a.name"] for record in result]
39 # end::result-consume[]
+0
-53
test/examples/result_retain_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::result-retain-import[]
23 # end::result-retain-import[]
24
25
26 class ResultRetainExample(BaseApplication):
27 def __init__(self, uri, user, password):
28 super(ResultRetainExample, self).__init__(uri, user, password)
29
30 # tag::result-retain[]
31 def add_employees(self, company_name):
32 with self._driver.session() as session:
33 employees = 0
34 persons = session.read_transaction(self.match_person_nodes)
35
36 for person in persons:
37 employees += session.write_transaction(self.add_employee_to_company, person, company_name)
38
39 return employees
40
41 @staticmethod
42 def add_employee_to_company(tx, person, company_name):
43 tx.run("MATCH (emp:Person {name: $person_name}) "
44 "MERGE (com:Company {name: $company_name}) "
45 "MERGE (emp)-[:WORKS_FOR]->(com)",
46 person_name=person["name"], company_name=company_name)
47 return 1
48
49 @staticmethod
50 def match_person_nodes(tx):
51 return list(tx.run("MATCH (a:Person) RETURN a.name AS name"))
52 # end::result-retain[]
+0
-40
test/examples/service_unavailable_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::service-unavailable-import[]
23 from neo4j.exceptions import ServiceUnavailable
24 # end::service-unavailable-import[]
25
26
27 class ServiceUnavailableExample(BaseApplication):
28 def __init__(self, uri, user, password):
29 super(ServiceUnavailableExample, self).__init__(uri, user, password)
30
31 # tag::service-unavailable[]
32 def add_item(self):
33 try:
34 with self._driver.session() as session:
35 session.write_transaction(lambda tx: tx.run("CREATE (a:Item)"))
36 return True
37 except ServiceUnavailable:
38 return False
39 # end::service-unavailable[]
+0
-35
test/examples/session_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from test.examples.base_application import BaseApplication
21
22 # tag::session-import[]
23 # end::session-import[]
24
25
26 class SessionExample(BaseApplication):
27 def __init__(self, uri, user, password):
28 super(SessionExample, self).__init__(uri, user, password)
29
30 # tag::session[]
31 def add_person(self, name):
32 with self._driver.session() as session:
33 session.run("CREATE (a:Person {name: $name})", name=name)
34 # end::session[]
+0
-208
test/examples/test_examples.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from contextlib import contextmanager
22 from io import StringIO
23 import sys
24 from unittest import TestCase
25
26 from test.integration.tools import IntegrationTestCase
27
28
29 # Python2 doesn't have contextlib.redirect_stdout()
30 # http://eli.thegreenplace.net/2015/redirecting-all-kinds-of-stdout-in-python/
31 @contextmanager
32 def stdout_redirector(stream):
33 old_stdout = sys.stdout
34 sys.stdout = stream
35 try:
36 yield
37 finally:
38 sys.stdout = old_stdout
39
40
41 class ExamplesTest(IntegrationTestCase):
42
43 def setUp(self):
44 self.clean()
45
46 def test_autocommit_transaction_example(self):
47 from test.examples.autocommit_transaction_example import AutocommitTransactionExample
48
49 example = AutocommitTransactionExample(self.bolt_uri, self.user, self.password)
50 example.add_person('Alice')
51
52 self.assertTrue(self.person_count('Alice') > 0)
53
54 def test_config_connection_pool_example(self):
55 from test.examples.config_connection_pool_example import ConfigConnectionPoolExample
56 example = ConfigConnectionPoolExample(self.bolt_uri, self.user, self.password)
57 self.assertTrue(example.can_connect())
58
59 def test_connection_timeout_example(self):
60 from test.examples.config_connection_timeout_example import ConfigConnectionTimeoutExample
61 example = ConfigConnectionTimeoutExample(self.bolt_uri, self.user, self.password)
62 self.assertTrue(example.can_connect())
63
64 def test_load_balancing_strategy_example(self):
65 from test.examples.config_load_balancing_strategy_example import ConfigLoadBalancingStrategyExample
66 example = ConfigLoadBalancingStrategyExample(self.bolt_uri, self.user, self.password)
67 self.assertTrue(example.can_connect())
68
69 def test_max_retry_time_example(self):
70 from test.examples.config_max_retry_time_example import ConfigMaxRetryTimeExample
71 example = ConfigMaxRetryTimeExample(self.bolt_uri, self.user, self.password)
72 self.assertTrue(example.can_connect())
73
74 def test_basic_auth_example(self):
75 from test.examples.auth_example import BasicAuthExample
76
77 example = BasicAuthExample(self.bolt_uri, self.user, self.password)
78
79 self.assertTrue(example.can_connect())
80
81 def test_custom_auth_example(self):
82 from test.examples.custom_auth_example import CustomAuthExample
83
84 example = CustomAuthExample(self.bolt_uri, self.user, self.password, None, "basic", **{"key":"value"})
85
86 self.assertTrue(example.can_connect())
87
88 def test_config_unencrypted_example(self):
89 from test.examples.config_unencrypted_example import ConfigUnencryptedExample
90
91 example = ConfigUnencryptedExample(self.bolt_uri, self.user, self.password)
92
93 self.assertIsInstance(example, ConfigUnencryptedExample)
94
95 def test_cypher_error_example(self):
96 from test.examples.cypher_error_example import CypherErrorExample
97
98 f = StringIO()
99 with stdout_redirector(f):
100 example = CypherErrorExample(self.bolt_uri, self.user, self.password)
101 try:
102 example.get_employee_number('Alice')
103 except:
104 pass
105
106 self.assertTrue(f.getvalue().startswith(
107 """Invalid input 'L': expected 't/T' (line 1, column 3 (offset: 2))
108 "SELECT * FROM Employees WHERE name = $name"
109 ^"""))
110
111 def test_driver_lifecycle_example(self):
112 from test.examples.driver_lifecycle_example import DriverLifecycleExample
113
114 example = DriverLifecycleExample(self.bolt_uri, self.user, self.password)
115 example.close()
116
117 self.assertIsInstance(example, DriverLifecycleExample)
118
119 def test_hello_world_example(self):
120 from test.examples.hello_world_example import HelloWorldExample
121
122 f = StringIO()
123 with stdout_redirector(f):
124 example = HelloWorldExample(self.bolt_uri, self.user, self.password)
125 example.print_greeting("hello, world")
126 example.close()
127
128 self.assertTrue(f.getvalue().startswith("hello, world, from node"))
129
130 def test_read_write_transaction_example(self):
131 from test.examples.read_write_transaction_example import ReadWriteTransactionExample
132
133 example = ReadWriteTransactionExample(self.bolt_uri, self.user, self.password)
134 node_count = example.add_person('Alice')
135
136 self.assertTrue(node_count > 0)
137
138 def test_result_consume_example(self):
139 from test.examples.result_consume_example import ResultConsumeExample
140
141 self.write("CREATE (a:Person {name: 'Alice'})")
142 self.write("CREATE (a:Person {name: 'Bob'})")
143 example = ResultConsumeExample(self.bolt_uri, self.user, self.password)
144 people = list(example.get_people())
145
146 self.assertEqual(['Alice', 'Bob'], people)
147
148 def test_result_retain_example(self):
149 from test.examples.result_retain_example import ResultRetainExample
150
151 self.write("CREATE (a:Person {name: 'Alice'})")
152 self.write("CREATE (a:Person {name: 'Bob'})")
153 example = ResultRetainExample(self.bolt_uri, self.user, self.password)
154 example.add_employees('Acme')
155 employee_count = self.read("MATCH (emp:Person)-[:WORKS_FOR]->(com:Company) WHERE com.name = 'Acme' RETURN count(emp)").single()[0]
156
157 self.assertEqual(employee_count, 2)
158
159 def test_session_example(self):
160 from test.examples.session_example import SessionExample
161
162 example = SessionExample(self.bolt_uri, self.user, self.password)
163 example.add_person("Alice")
164
165 self.assertIsInstance(example, SessionExample)
166 self.assertEqual(self.person_count("Alice"), 1)
167
168 def test_transaction_function_example(self):
169 from test.examples.transaction_function_example import TransactionFunctionExample
170
171 example = TransactionFunctionExample(self.bolt_uri, self.user, self.password)
172 example.add_person("Alice")
173
174 self.assertEqual(self.person_count("Alice"), 1)
175
176 def read(self, statement):
177 from neo4j import GraphDatabase
178 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token) as driver:
179 with driver.session() as session:
180 return session.read_transaction(lambda tx: tx.run(statement))
181
182 def write(self, statement):
183 from neo4j import GraphDatabase
184 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token) as driver:
185 with driver.session() as session:
186 return session.write_transaction(lambda tx: tx.run(statement))
187
188 def clean(self):
189 self.write("MATCH (a) DETACH DELETE a")
190
191 def person_count(self, name):
192 from neo4j import GraphDatabase
193 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token) as driver:
194 with driver.session() as session:
195 record_list = list(session.run("MATCH (a:Person {name: $name}) RETURN count(a)", {"name": name}))
196 return len(record_list)
197
198
199 class ServiceUnavailableTest(IntegrationTestCase):
200
201 def test_service_unavailable_example(self):
202 from test.examples.service_unavailable_example import ServiceUnavailableExample
203
204 example = ServiceUnavailableExample(self.bolt_uri, self.user, self.password)
205 self._stop_server()
206
207 self.assertFalse(example.add_item())
+0
-53
test/examples/transaction_function_example.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from test.examples.base_application import BaseApplication
22
23 # tag::transaction-function-import[]
24 from neo4j import unit_of_work
25 # end::transaction-function-import[]
26
27
28 # tag::transaction-function[]
29 def add_person(driver, name):
30 with driver.session() as session:
31 # Caller for transactional unit of work
32 return session.write_transaction(create_person_node, name)
33
34
35 # Simple implementation of the unit of work
36 def create_person_node(tx, name):
37 return tx.run("CREATE (a:Person {name: $name}) RETURN id(a)", name=name).single().value()
38
39
40 # Alternative implementation, with timeout
41 @unit_of_work(timeout=0.5)
42 def create_person_node_within_half_a_second(tx, name):
43 return tx.run("CREATE (a:Person {name: $name}) RETURN id(a)", name=name).single().value()
44 # end::transaction-function[]
45
46
47 class TransactionFunctionExample(BaseApplication):
48 def __init__(self, uri, user, password):
49 super(TransactionFunctionExample, self).__init__(uri, user, password)
50
51 def add_person(self, name):
52 return add_person(self._driver, name)
+0
-0
test/integration/__init__.py less more
(Empty file)
+0
-61
test/integration/test_driver.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neobolt.direct import DEFAULT_PORT
22
23 from neo4j import GraphDatabase, Driver, ServiceUnavailable
24
25 from test.integration.tools import IntegrationTestCase
26
27
28 class DriverTestCase(IntegrationTestCase):
29
30 def test_must_use_valid_url_scheme(self):
31 with self.assertRaises(ValueError):
32 GraphDatabase.driver("x://xxx", auth=self.auth_token)
33
34 def test_connections_are_reused(self):
35 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token) as driver:
36 session_1 = driver.session()
37 connection_1 = session_1._connection
38 session_1.close()
39 session_2 = driver.session()
40 connection_2 = session_2._connection
41 session_2.close()
42 assert connection_1 is connection_2
43
44 def test_fail_nicely_when_using_http_port(self):
45 uri = "bolt://localhost:7474"
46 with self.assertRaises(ServiceUnavailable):
47 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False):
48 pass
49
50 def test_custom_resolver(self):
51
52 def my_resolver(socket_address):
53 self.assertEqual(socket_address, ("*", DEFAULT_PORT))
54 yield "99.99.99.99", self.bolt_port # this should be rejected as unable to connect
55 yield "127.0.0.1", self.bolt_port # this should succeed
56
57 with Driver("bolt://*", auth=self.auth_token, resolver=my_resolver) as driver:
58 with driver.session() as session:
59 summary = session.run("RETURN 1").summary()
60 self.assertEqual(summary.server.address, ("127.0.0.1", 7687))
+0
-50
test/integration/test_readme.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from __future__ import print_function
22
23
24 from test.integration.tools import IntegrationTestCase
25
26
27 class ReadmeTestCase(IntegrationTestCase):
28
29 def test_should_run_readme(self):
30 names = set()
31 print = names.add
32
33 from neo4j import GraphDatabase
34
35 driver = GraphDatabase.driver(self.bolt_uri, auth=self.auth_token)
36
37 def print_friends(tx, name):
38 for record in tx.run("MATCH (a:Person)-[:KNOWS]->(friend) "
39 "WHERE a.name = {name} "
40 "RETURN friend.name", name=name):
41 print(record["friend.name"])
42
43 with driver.session() as session:
44 session.run("MATCH (a) DETACH DELETE a")
45 session.run("CREATE (a:Person {name:'Alice'})-[:KNOWS]->({name:'Bob'})")
46 session.read_transaction(print_friends, "Alice")
47
48 assert len(names) == 1
49 assert "Bob" in names
+0
-327
test/integration/test_result.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import CypherError
24
25 from test.integration.tools import DirectIntegrationTestCase
26
27
28 class ResultConsumptionTestCase(DirectIntegrationTestCase):
29
30 def test_can_consume_result_immediately(self):
31
32 def _(tx):
33 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
34 self.assertEqual([record[0] for record in result], [1, 2, 3])
35
36 with self.driver.session() as session:
37 session.read_transaction(_)
38
39 def test_can_consume_result_from_buffer(self):
40
41 def _(tx):
42 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
43 result.detach()
44 self.assertEqual([record[0] for record in result], [1, 2, 3])
45
46 with self.driver.session() as session:
47 session.read_transaction(_)
48
49 def test_can_consume_result_after_commit(self):
50 with self.driver.session() as session:
51 tx = session.begin_transaction()
52 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
53 tx.commit()
54 self.assertEqual([record[0] for record in result], [1, 2, 3])
55
56 def test_can_consume_result_after_rollback(self):
57 with self.driver.session() as session:
58 tx = session.begin_transaction()
59 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
60 tx.rollback()
61 self.assertEqual([record[0] for record in result], [1, 2, 3])
62
63 def test_can_consume_result_after_session_close(self):
64 with self.driver.session() as session:
65 tx = session.begin_transaction()
66 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
67 tx.commit()
68 self.assertEqual([record[0] for record in result], [1, 2, 3])
69
70 def test_can_consume_result_after_session_reuse(self):
71 session = self.driver.session()
72 tx = session.begin_transaction()
73 result_a = tx.run("UNWIND range(1, 3) AS n RETURN n")
74 tx.commit()
75 session.close()
76 session = self.driver.session()
77 tx = session.begin_transaction()
78 result_b = tx.run("UNWIND range(4, 6) AS n RETURN n")
79 tx.commit()
80 session.close()
81 assert [record[0] for record in result_a] == [1, 2, 3]
82 assert [record[0] for record in result_b] == [4, 5, 6]
83
84 def test_can_consume_results_after_harsh_session_death(self):
85 session = self.driver.session()
86 result_a = session.run("UNWIND range(1, 3) AS n RETURN n")
87 del session
88 session = self.driver.session()
89 result_b = session.run("UNWIND range(4, 6) AS n RETURN n")
90 del session
91 assert [record[0] for record in result_a] == [1, 2, 3]
92 assert [record[0] for record in result_b] == [4, 5, 6]
93
94 def test_can_consume_result_after_session_with_error(self):
95 session = self.driver.session()
96 with self.assertRaises(CypherError):
97 session.run("X").consume()
98 session.close()
99 session = self.driver.session()
100 tx = session.begin_transaction()
101 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
102 tx.commit()
103 session.close()
104 assert [record[0] for record in result] == [1, 2, 3]
105
106 def test_single_with_exactly_one_record(self):
107 session = self.driver.session()
108 result = session.run("UNWIND range(1, 1) AS n RETURN n")
109 record = result.single()
110 assert list(record.values()) == [1]
111
112 def test_value_with_no_records(self):
113 with self.driver.session() as session:
114 result = session.run("CREATE ()")
115 self.assertEqual(result.value(), [])
116
117 def test_values_with_no_records(self):
118 with self.driver.session() as session:
119 result = session.run("CREATE ()")
120 self.assertEqual(result.values(), [])
121
122 def test_peek_can_look_one_ahead(self):
123 session = self.driver.session()
124 result = session.run("UNWIND range(1, 3) AS n RETURN n")
125 record = result.peek()
126 assert list(record.values()) == [1]
127
128 def test_peek_fails_if_nothing_remains(self):
129 session = self.driver.session()
130 result = session.run("CREATE ()")
131 upcoming = result.peek()
132 assert upcoming is None
133
134 def test_peek_does_not_advance_cursor(self):
135 session = self.driver.session()
136 result = session.run("UNWIND range(1, 3) AS n RETURN n")
137 result.peek()
138 assert [record[0] for record in result] == [1, 2, 3]
139
140 def test_peek_at_different_stages(self):
141 session = self.driver.session()
142 result = session.run("UNWIND range(0, 9) AS n RETURN n")
143 # Peek ahead to the first record
144 expected_next = 0
145 upcoming = result.peek()
146 assert upcoming[0] == expected_next
147 # Then look through all the other records
148 for expected, record in enumerate(result):
149 # Check this record is as expected
150 assert record[0] == expected
151 # Check the upcoming record is as expected...
152 if expected < 9:
153 # ...when one should follow
154 expected_next = expected + 1
155 upcoming = result.peek()
156 assert upcoming[0] == expected_next
157 else:
158 # ...when none should follow
159 upcoming = result.peek()
160 assert upcoming is None
161
162 def test_can_safely_exit_session_without_consuming_result(self):
163 with self.driver.session() as session:
164 session.run("RETURN 1")
165 assert True
166
167 def test_multiple_value(self):
168 with self.driver.session() as session:
169 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
170 self.assertEqual(result.value(), [1,
171 2,
172 3])
173
174 def test_multiple_indexed_value(self):
175 with self.driver.session() as session:
176 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
177 self.assertEqual(result.value(2), [3,
178 6,
179 9])
180
181 def test_multiple_keyed_value(self):
182 with self.driver.session() as session:
183 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
184 self.assertEqual(result.value("z"), [3,
185 6,
186 9])
187
188 def test_multiple_values(self):
189 with self.driver.session() as session:
190 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
191 self.assertEqual(result.values(), [[1, 2, 3],
192 [2, 4, 6],
193 [3, 6, 9]])
194
195 def test_multiple_indexed_values(self):
196 with self.driver.session() as session:
197 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
198 self.assertEqual(result.values(2, 0), [[3, 1],
199 [6, 2],
200 [9, 3]])
201
202 def test_multiple_keyed_values(self):
203 with self.driver.session() as session:
204 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
205 self.assertEqual(result.values("z", "x"), [[3, 1],
206 [6, 2],
207 [9, 3]])
208
209 def test_multiple_data(self):
210 with self.driver.session() as session:
211 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
212 self.assertEqual(result.data(), [{"x": 1, "y": 2, "z": 3},
213 {"x": 2, "y": 4, "z": 6},
214 {"x": 3, "y": 6, "z": 9}])
215
216 def test_multiple_indexed_data(self):
217 with self.driver.session() as session:
218 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
219 self.assertEqual(result.data(2, 0), [{"x": 1, "z": 3},
220 {"x": 2, "z": 6},
221 {"x": 3, "z": 9}])
222
223 def test_multiple_keyed_data(self):
224 with self.driver.session() as session:
225 result = session.run("UNWIND range(1, 3) AS n RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
226 self.assertEqual(result.data("z", "x"), [{"x": 1, "z": 3},
227 {"x": 2, "z": 6},
228 {"x": 3, "z": 9}])
229
230 def test_value_with_no_keys_and_no_records(self):
231 with self.driver.session() as session:
232 result = session.run("CREATE ()")
233 self.assertEqual(result.value(), [])
234
235 def test_values_with_one_key_and_no_records(self):
236 with self.driver.session() as session:
237 result = session.run("UNWIND range(1, 0) AS n RETURN n")
238 self.assertEqual(result.values(), [])
239
240 def test_data_with_one_key_and_no_records(self):
241 with self.driver.session() as session:
242 result = session.run("UNWIND range(1, 0) AS n RETURN n")
243 self.assertEqual(result.data(), [])
244
245
246 class SingleRecordTestCase(DirectIntegrationTestCase):
247
248 def test_single_with_no_keys_and_no_records(self):
249 with self.driver.session() as session:
250 result = session.run("CREATE ()")
251 record = result.single()
252 self.assertIsNone(record)
253
254 def test_single_with_one_key_and_no_records(self):
255 with self.driver.session() as session:
256 result = session.run("UNWIND range(1, 0) AS n RETURN n")
257 record = result.single()
258 self.assertIsNone(record)
259
260 def test_single_with_multiple_records(self):
261 import warnings
262 session = self.driver.session()
263 result = session.run("UNWIND range(1, 3) AS n RETURN n")
264 with warnings.catch_warnings(record=True) as warning_list:
265 warnings.simplefilter("always")
266 record = result.single()
267 assert len(warning_list) == 1
268 assert record[0] == 1
269
270 def test_single_consumes_entire_result_if_one_record(self):
271 session = self.driver.session()
272 result = session.run("UNWIND range(1, 1) AS n RETURN n")
273 _ = result.single()
274 assert not result.session
275
276 def test_single_consumes_entire_result_if_multiple_records(self):
277 session = self.driver.session()
278 result = session.run("UNWIND range(1, 3) AS n RETURN n")
279 with pytest.warns(UserWarning):
280 _ = result.single()
281 assert not result.session
282
283 def test_single_value(self):
284 with self.driver.session() as session:
285 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
286 self.assertEqual(result.single().value(), 1)
287
288 def test_single_indexed_value(self):
289 with self.driver.session() as session:
290 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
291 self.assertEqual(result.single().value(2), 3)
292
293 def test_single_keyed_value(self):
294 with self.driver.session() as session:
295 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
296 self.assertEqual(result.single().value("z"), 3)
297
298 def test_single_values(self):
299 with self.driver.session() as session:
300 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
301 self.assertEqual(result.single().values(), [1, 2, 3])
302
303 def test_single_indexed_values(self):
304 with self.driver.session() as session:
305 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
306 self.assertEqual(result.single().values(2, 0), [3, 1])
307
308 def test_single_keyed_values(self):
309 with self.driver.session() as session:
310 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
311 self.assertEqual(result.single().values("z", "x"), [3, 1])
312
313 def test_single_data(self):
314 with self.driver.session() as session:
315 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
316 self.assertEqual(result.single().data(), {"x": 1, "y": 2, "z": 3})
317
318 def test_single_indexed_data(self):
319 with self.driver.session() as session:
320 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
321 self.assertEqual(result.single().data(2, 0), {"x": 1, "z": 3})
322
323 def test_single_keyed_data(self):
324 with self.driver.session() as session:
325 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
326 self.assertEqual(result.single().data("z", "x"), {"x": 1, "z": 3})
+0
-54
test/integration/test_security.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from socket import socket
22
23 from neo4j import GraphDatabase, TRUST_CUSTOM_CA_SIGNED_CERTIFICATES
24 from neo4j.exceptions import AuthError
25
26 from test.integration.tools import IntegrationTestCase
27
28
29 class SecurityTestCase(IntegrationTestCase):
30
31 def test_secure_by_default(self):
32 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token) as driver:
33 self.assertTrue(driver.encrypted)
34
35 def test_insecure_session_uses_normal_socket(self):
36 with GraphDatabase.driver(self.bolt_uri, auth=self.auth_token, encrypted=False) as driver:
37 with driver.session() as session:
38 result = session.run("RETURN 1")
39 connection = session._connection
40 assert isinstance(connection.socket, socket)
41 assert connection.der_encoded_server_certificate is None
42 result.consume()
43
44 def test_custom_ca_not_implemented(self):
45 with self.assertRaises(NotImplementedError):
46 _ = GraphDatabase.driver(self.bolt_uri, auth=self.auth_token,
47 trust=TRUST_CUSTOM_CA_SIGNED_CERTIFICATES)
48
49 def test_should_fail_on_incorrect_password(self):
50 with self.assertRaises(AuthError):
51 with GraphDatabase.driver(self.bolt_uri, auth=("neo4j", "wrong-password")) as driver:
52 with driver.session() as session:
53 _ = session.run("RETURN 1")
+0
-614
test/integration/test_session.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from time import sleep
22 from unittest import SkipTest
23 from uuid import uuid4
24
25 from neo4j import \
26 READ_ACCESS, WRITE_ACCESS, \
27 CypherError, SessionError, TransactionError, unit_of_work, Statement
28 from neo4j.types.graph import Node, Relationship, Path
29 from neo4j.exceptions import CypherSyntaxError, TransientError
30
31 from test.integration.tools import DirectIntegrationTestCase
32
33
34 class AutoCommitTransactionTestCase(DirectIntegrationTestCase):
35
36 def test_can_run_simple_statement(self):
37 session = self.driver.session()
38 result = session.run("RETURN 1 AS n")
39 for record in result:
40 assert record[0] == 1
41 assert record["n"] == 1
42 with self.assertRaises(KeyError):
43 _ = record["x"]
44 assert record["n"] == 1
45 with self.assertRaises(KeyError):
46 _ = record["x"]
47 with self.assertRaises(TypeError):
48 _ = record[object()]
49 assert repr(record)
50 assert len(record) == 1
51 session.close()
52
53 def test_can_run_simple_statement_with_params(self):
54 session = self.driver.session()
55 count = 0
56 for record in session.run("RETURN {x} AS n", {"x": {"abc": ["d", "e", "f"]}}):
57 assert record[0] == {"abc": ["d", "e", "f"]}
58 assert record["n"] == {"abc": ["d", "e", "f"]}
59 assert repr(record)
60 assert len(record) == 1
61 count += 1
62 session.close()
63 assert count == 1
64
65 def test_autocommit_transactions_use_bookmarks(self):
66 if self.protocol_version() < 3:
67 raise SkipTest("Test requires Bolt v3")
68 bookmarks = []
69 # Generate an initial bookmark
70 with self.driver.session() as session:
71 session.run("CREATE ()").consume()
72 bookmark = session.last_bookmark()
73 self.assertIsNotNone(bookmark)
74 bookmarks.append(bookmark)
75 # Propagate into another session
76 with self.driver.session(bookmarks=bookmarks) as session:
77 self.assertEqual(list(session.next_bookmarks()), bookmarks)
78 session.run("CREATE ()").consume()
79 bookmark = session.last_bookmark()
80 self.assertIsNotNone(bookmark)
81 self.assertNotIn(bookmark, bookmarks)
82
83 def test_fails_on_bad_syntax(self):
84 session = self.driver.session()
85 with self.assertRaises(CypherError):
86 session.run("X").consume()
87
88 def test_fails_on_missing_parameter(self):
89 session = self.driver.session()
90 with self.assertRaises(CypherError):
91 session.run("RETURN {x}").consume()
92
93 def test_can_run_simple_statement_from_bytes_string(self):
94 session = self.driver.session()
95 count = 0
96 for record in session.run(b"RETURN 1 AS n"):
97 assert record[0] == 1
98 assert record["n"] == 1
99 assert repr(record)
100 assert len(record) == 1
101 count += 1
102 session.close()
103 assert count == 1
104
105 def test_can_run_statement_that_returns_multiple_records(self):
106 session = self.driver.session()
107 count = 0
108 for record in session.run("unwind(range(1, 10)) AS z RETURN z"):
109 assert 1 <= record[0] <= 10
110 count += 1
111 session.close()
112 assert count == 10
113
114 def test_can_use_with_to_auto_close_session(self):
115 with self.driver.session() as session:
116 record_list = list(session.run("RETURN 1"))
117 assert len(record_list) == 1
118 for record in record_list:
119 assert record[0] == 1
120
121 def test_can_return_node(self):
122 with self.driver.session() as session:
123 record_list = list(session.run("CREATE (a:Person {name:'Alice'}) RETURN a"))
124 assert len(record_list) == 1
125 for record in record_list:
126 alice = record[0]
127 assert isinstance(alice, Node)
128 assert alice.labels == {"Person"}
129 assert dict(alice) == {"name": "Alice"}
130
131 def test_can_return_relationship(self):
132 with self.driver.session() as session:
133 record_list = list(session.run("CREATE ()-[r:KNOWS {since:1999}]->() RETURN r"))
134 assert len(record_list) == 1
135 for record in record_list:
136 rel = record[0]
137 assert isinstance(rel, Relationship)
138 assert rel.type == "KNOWS"
139 assert dict(rel) == {"since": 1999}
140
141 def test_can_return_path(self):
142 with self.driver.session() as session:
143 record_list = list(session.run("MERGE p=({name:'Alice'})-[:KNOWS]->({name:'Bob'}) RETURN p"))
144 assert len(record_list) == 1
145 for record in record_list:
146 path = record[0]
147 assert isinstance(path, Path)
148 assert path.start_node["name"] == "Alice"
149 assert path.end_node["name"] == "Bob"
150 assert path.relationships[0].type == "KNOWS"
151 assert len(path.nodes) == 2
152 assert len(path.relationships) == 1
153
154 def test_can_handle_cypher_error(self):
155 with self.driver.session() as session:
156 with self.assertRaises(CypherError):
157 session.run("X").consume()
158
159 def test_keys_are_available_before_and_after_stream(self):
160 with self.driver.session() as session:
161 result = session.run("UNWIND range(1, 10) AS n RETURN n")
162 assert list(result.keys()) == ["n"]
163 list(result)
164 assert list(result.keys()) == ["n"]
165
166 def test_keys_with_an_error(self):
167 with self.driver.session() as session:
168 with self.assertRaises(CypherError):
169 result = session.run("X")
170 list(result.keys())
171
172 def test_should_not_allow_empty_statements(self):
173 with self.driver.session() as session:
174 with self.assertRaises(ValueError):
175 _ = session.run("")
176
177 def test_statement_object(self):
178 if self.protocol_version() < 3:
179 raise SkipTest("Test requires Bolt v3")
180 with self.driver.session() as session:
181 value = session.run(Statement("RETURN $x"), x=1).single().value()
182 self.assertEqual(value, 1)
183
184 def test_autocommit_transactions_should_support_metadata(self):
185 if self.protocol_version() < 3:
186 raise SkipTest("Test requires Bolt v3")
187 metadata_in = {"foo": "bar"}
188 with self.driver.session() as session:
189 metadata_out = session.run(Statement("CALL dbms.getTXMetaData", metadata=metadata_in)).single().value()
190 self.assertEqual(metadata_in, metadata_out)
191
192 def test_autocommit_transactions_should_support_timeout(self):
193 if self.protocol_version() < 3:
194 raise SkipTest("Test requires Bolt v3")
195 with self.driver.session() as s1:
196 s1.run("CREATE (a:Node)").consume()
197 with self.driver.session() as s2:
198 tx1 = s1.begin_transaction()
199 tx1.run("MATCH (a:Node) SET a.property = 1").consume()
200 with self.assertRaises(TransientError):
201 s2.run(Statement("MATCH (a:Node) SET a.property = 2", timeout=0.25)).consume()
202
203
204 class SummaryTestCase(DirectIntegrationTestCase):
205
206 def test_can_obtain_summary_after_consuming_result(self):
207 with self.driver.session() as session:
208 result = session.run("CREATE (n) RETURN n")
209 summary = result.summary()
210 assert summary.statement == "CREATE (n) RETURN n"
211 assert summary.parameters == {}
212 assert summary.statement_type == "rw"
213 assert summary.counters.nodes_created == 1
214
215 def test_no_plan_info(self):
216 with self.driver.session() as session:
217 result = session.run("CREATE (n) RETURN n")
218 summary = result.summary()
219 assert summary.plan is None
220 assert summary.profile is None
221
222 def test_can_obtain_plan_info(self):
223 with self.driver.session() as session:
224 result = session.run("EXPLAIN CREATE (n) RETURN n")
225 summary = result.summary()
226 plan = summary.plan
227 assert plan.operator_type == "ProduceResults"
228 assert plan.identifiers == ["n"]
229 assert len(plan.children) == 1
230
231 def test_can_obtain_profile_info(self):
232 with self.driver.session() as session:
233 result = session.run("PROFILE CREATE (n) RETURN n")
234 summary = result.summary()
235 profile = summary.profile
236 assert profile.db_hits == 0
237 assert profile.rows == 1
238 assert profile.operator_type == "ProduceResults"
239 assert profile.identifiers == ["n"]
240 assert len(profile.children) == 1
241
242 def test_no_notification_info(self):
243 with self.driver.session() as session:
244 result = session.run("CREATE (n) RETURN n")
245 summary = result.summary()
246 notifications = summary.notifications
247 assert notifications == []
248
249 def test_can_obtain_notification_info(self):
250 with self.driver.session() as session:
251 result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
252 summary = result.summary()
253 notifications = summary.notifications
254
255 assert len(notifications) == 1
256 notification = notifications[0]
257 assert notification.code == "Neo.ClientNotification.Statement.CartesianProductWarning"
258 assert notification.title == "This query builds a cartesian product between " \
259 "disconnected patterns."
260 assert notification.severity == "WARNING"
261 assert notification.description == "If a part of a query contains multiple " \
262 "disconnected patterns, this will build a " \
263 "cartesian product between all those parts. This " \
264 "may produce a large amount of data and slow down " \
265 "query processing. While occasionally intended, " \
266 "it may often be possible to reformulate the " \
267 "query that avoids the use of this cross product, " \
268 "perhaps by adding a relationship between the " \
269 "different parts or by using OPTIONAL MATCH " \
270 "(identifier is: (m))"
271 position = notification.position
272 assert position
273
274 def test_contains_time_information(self):
275 if not self.at_least_server_version(3, 1):
276 raise SkipTest("Execution times are not supported before server 3.1")
277 with self.driver.session() as session:
278 summary = session.run("UNWIND range(1,1000) AS n RETURN n AS number").consume()
279
280 self.assertIsInstance(summary.result_available_after, int)
281 self.assertIsInstance(summary.result_consumed_after, int)
282
283 with self.assertRaises(AttributeError) as ex:
284 summary.t_first
285
286 with self.assertRaises(AttributeError) as ex:
287 summary.t_last
288
289
290 class ResetTestCase(DirectIntegrationTestCase):
291
292 def test_automatic_reset_after_failure(self):
293 with self.driver.session() as session:
294 try:
295 session.run("X").consume()
296 except CypherError:
297 result = session.run("RETURN 1")
298 record = next(iter(result))
299 assert record[0] == 1
300 else:
301 assert False, "A Cypher error should have occurred"
302
303
304 class ExplicitTransactionTestCase(DirectIntegrationTestCase):
305
306 def test_can_commit_transaction(self):
307
308 with self.driver.session() as session:
309 tx = session.begin_transaction()
310
311 # Create a node
312 result = tx.run("CREATE (a) RETURN id(a)")
313 record = next(iter(result))
314 node_id = record[0]
315 assert isinstance(node_id, int)
316
317 # Update a property
318 tx.run("MATCH (a) WHERE id(a) = {n} "
319 "SET a.foo = {foo}", {"n": node_id, "foo": "bar"})
320
321 tx.commit()
322
323 # Check the property value
324 result = session.run("MATCH (a) WHERE id(a) = {n} "
325 "RETURN a.foo", {"n": node_id})
326 record = next(iter(result))
327 value = record[0]
328 assert value == "bar"
329
330 def test_can_rollback_transaction(self):
331 with self.driver.session() as session:
332 tx = session.begin_transaction()
333
334 # Create a node
335 result = tx.run("CREATE (a) RETURN id(a)")
336 record = next(iter(result))
337 node_id = record[0]
338 assert isinstance(node_id, int)
339
340 # Update a property
341 tx.run("MATCH (a) WHERE id(a) = {n} "
342 "SET a.foo = {foo}", {"n": node_id, "foo": "bar"})
343
344 tx.rollback()
345
346 # Check the property value
347 result = session.run("MATCH (a) WHERE id(a) = {n} "
348 "RETURN a.foo", {"n": node_id})
349 assert len(list(result)) == 0
350
351 def test_can_commit_transaction_using_with_block(self):
352 with self.driver.session() as session:
353 with session.begin_transaction() as tx:
354 # Create a node
355 result = tx.run("CREATE (a) RETURN id(a)")
356 record = next(iter(result))
357 node_id = record[0]
358 assert isinstance(node_id, int)
359
360 # Update a property
361 tx.run("MATCH (a) WHERE id(a) = {n} "
362 "SET a.foo = {foo}", {"n": node_id, "foo": "bar"})
363
364 tx.success = True
365
366 # Check the property value
367 result = session.run("MATCH (a) WHERE id(a) = {n} "
368 "RETURN a.foo", {"n": node_id})
369 record = next(iter(result))
370 value = record[0]
371 assert value == "bar"
372
373 def test_can_rollback_transaction_using_with_block(self):
374 with self.driver.session() as session:
375 with session.begin_transaction() as tx:
376 # Create a node
377 result = tx.run("CREATE (a) RETURN id(a)")
378 record = next(iter(result))
379 node_id = record[0]
380 assert isinstance(node_id, int)
381
382 # Update a property
383 tx.run("MATCH (a) WHERE id(a) = {n} "
384 "SET a.foo = {foo}", {"n": node_id, "foo": "bar"})
385
386 tx.success = False
387
388 # Check the property value
389 result = session.run("MATCH (a) WHERE id(a) = {n} "
390 "RETURN a.foo", {"n": node_id})
391 assert len(list(result)) == 0
392
393 def test_broken_transaction_should_not_break_session(self):
394 with self.driver.session() as session:
395 with self.assertRaises(CypherSyntaxError):
396 with session.begin_transaction() as tx:
397 tx.run("X")
398 with session.begin_transaction() as tx:
399 tx.run("RETURN 1")
400
401 def test_statement_object_not_supported(self):
402 with self.driver.session() as session:
403 with session.begin_transaction() as tx:
404 with self.assertRaises(ValueError):
405 tx.run(Statement("RETURN 1", timeout=0.25))
406
407 def test_transaction_metadata(self):
408 if self.protocol_version() < 3:
409 raise SkipTest("Test requires Bolt v3")
410 with self.driver.session() as session:
411 metadata_in = {"foo": "bar"}
412 with session.begin_transaction(metadata=metadata_in) as tx:
413 metadata_out = tx.run("CALL dbms.getTXMetaData").single().value()
414 self.assertEqual(metadata_in, metadata_out)
415
416 def test_transaction_timeout(self):
417 if self.protocol_version() < 3:
418 raise SkipTest("Test requires Bolt v3")
419 with self.driver.session() as s1:
420 s1.run("CREATE (a:Node)").consume()
421 with self.driver.session() as s2:
422 tx1 = s1.begin_transaction()
423 tx1.run("MATCH (a:Node) SET a.property = 1").consume()
424 tx2 = s2.begin_transaction(timeout=0.25)
425 with self.assertRaises(TransientError):
426 tx2.run("MATCH (a:Node) SET a.property = 2").consume()
427
428 def test_exit_after_explicit_close_should_be_silent(self):
429 with self.driver.session() as s:
430 with s.begin_transaction() as tx:
431 self.assertFalse(tx.closed())
432 tx.close()
433 self.assertTrue(tx.closed())
434 self.assertTrue(tx.closed())
435
436
437 class BookmarkingTestCase(DirectIntegrationTestCase):
438
439 def test_can_obtain_bookmark_after_commit(self):
440 if not self.at_least_server_version(3, 1):
441 raise SkipTest("Bookmarking is not supported before server 3.1")
442 with self.driver.session() as session:
443 with session.begin_transaction() as tx:
444 tx.run("RETURN 1")
445 assert session.last_bookmark() is not None
446
447 def test_can_pass_bookmark_into_next_transaction(self):
448 if not self.at_least_server_version(3, 1):
449 raise SkipTest("Bookmarking is not supported before server 3.1")
450
451 unique_id = uuid4().hex
452
453 with self.driver.session(access_mode=WRITE_ACCESS) as session:
454 with session.begin_transaction() as tx:
455 tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=unique_id)
456 bookmark = session.last_bookmark()
457
458 assert bookmark is not None
459
460 with self.driver.session(access_mode=READ_ACCESS, bookmark=bookmark) as session:
461 with session.begin_transaction() as tx:
462 result = tx.run("MATCH (a:Thing {uuid:$uuid}) RETURN a", uuid=unique_id)
463 record_list = list(result)
464 assert len(record_list) == 1
465 record = record_list[0]
466 assert len(record) == 1
467 thing = record[0]
468 assert isinstance(thing, Node)
469 assert thing["uuid"] == unique_id
470
471 def test_bookmark_should_be_none_after_rollback(self):
472 if not self.at_least_server_version(3, 1):
473 raise SkipTest("Bookmarking is not supported before server 3.1")
474
475 with self.driver.session(access_mode=WRITE_ACCESS) as session:
476 with session.begin_transaction() as tx:
477 tx.run("CREATE (a)")
478 assert session.last_bookmark() is not None
479 with self.driver.session(access_mode=WRITE_ACCESS) as session:
480 with session.begin_transaction() as tx:
481 tx.run("CREATE (a)")
482 tx.success = False
483 assert session.last_bookmark() is None
484
485
486 class SessionCompletionTestCase(DirectIntegrationTestCase):
487
488 def test_should_sync_after_commit(self):
489 with self.driver.session() as session:
490 tx = session.begin_transaction()
491 result = tx.run("RETURN 1")
492 tx.commit()
493 buffer = result._records
494 assert len(buffer) == 1
495 assert buffer[0][0] == 1
496
497 def test_should_sync_after_rollback(self):
498 with self.driver.session() as session:
499 tx = session.begin_transaction()
500 result = tx.run("RETURN 1")
501 tx.rollback()
502 buffer = result._records
503 assert len(buffer) == 1
504 assert buffer[0][0] == 1
505
506 def test_errors_on_write_transaction(self):
507 session = self.driver.session()
508 with self.assertRaises(TypeError):
509 session.write_transaction(lambda tx, uuid : tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=uuid), uuid4())
510 session.close()
511
512 def test_errors_on_run_transaction(self):
513 session = self.driver.session()
514 tx = session.begin_transaction()
515 with self.assertRaises(TypeError):
516 tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=uuid4())
517 tx.rollback()
518 session.close()
519
520 def test_errors_on_run_session(self):
521 session = self.driver.session()
522 session.close()
523 with self.assertRaises(SessionError):
524 session.run("RETURN 1")
525
526 def test_errors_on_begin_transaction(self):
527 session = self.driver.session()
528 session.close()
529 with self.assertRaises(SessionError):
530 session.begin_transaction()
531
532 def test_large_values(self):
533 for i in range(1, 7):
534 session = self.driver.session()
535 session.run("RETURN '{}'".format("A" * 2 ** 20))
536 session.close()
537
538
539 class TransactionCommittedTestCase(DirectIntegrationTestCase):
540
541 def setUp(self):
542 super(TransactionCommittedTestCase, self).setUp()
543 self.session = self.driver.session()
544 self.transaction = self.session.begin_transaction()
545 self.transaction.run("RETURN 1")
546 self.transaction.commit()
547
548 def test_errors_on_run(self):
549 with self.assertRaises(TransactionError):
550 self.transaction.run("RETURN 1")
551
552
553 class TransactionFunctionTestCase(DirectIntegrationTestCase):
554
555 def test_simple_read(self):
556
557 def work(tx):
558 return tx.run("RETURN 1").single().value()
559
560 with self.driver.session() as session:
561 value = session.read_transaction(work)
562 self.assertEqual(value, 1)
563
564 def test_read_with_arg(self):
565
566 def work(tx, x):
567 return tx.run("RETURN $x", x=x).single().value()
568
569 with self.driver.session() as session:
570 value = session.read_transaction(work, x=1)
571 self.assertEqual(value, 1)
572
573 def test_read_with_arg_and_metadata(self):
574 if self.protocol_version() < 3:
575 raise SkipTest("Transaction metadata and timeout only supported in Bolt v3+")
576
577 @unit_of_work(timeout=25, metadata={"foo": "bar"})
578 def work(tx):
579 return tx.run("CALL dbms.getTXMetaData").single().value()
580
581 with self.driver.session() as session:
582 value = session.read_transaction(work)
583 self.assertEqual(value, {"foo": "bar"})
584
585 def test_simple_write(self):
586
587 def work(tx):
588 return tx.run("CREATE (a {x: 1}) RETURN a.x").single().value()
589
590 with self.driver.session() as session:
591 value = session.write_transaction(work)
592 self.assertEqual(value, 1)
593
594 def test_write_with_arg(self):
595
596 def work(tx, x):
597 return tx.run("CREATE (a {x: $x}) RETURN a.x", x=x).single().value()
598
599 with self.driver.session() as session:
600 value = session.write_transaction(work, x=1)
601 self.assertEqual(value, 1)
602
603 def test_write_with_arg_and_metadata(self):
604 if self.protocol_version() < 3:
605 raise SkipTest("Transaction metadata and timeout only supported in Bolt v3+")
606
607 @unit_of_work(timeout=25, metadata={"foo": "bar"})
608 def work(tx, x):
609 return tx.run("CREATE (a {x: $x}) RETURN a.x", x=x).single().value()
610
611 with self.driver.session() as session:
612 value = session.write_transaction(work, x=1)
613 self.assertEqual(value, 1)
+0
-577
test/integration/test_types.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from math import isnan
22 from unittest import SkipTest
23
24 from pytz import FixedOffset, timezone, utc
25
26 from neo4j.exceptions import CypherTypeError
27 from neo4j.types.graph import Node, Relationship, Path
28 from neo4j.types.spatial import CartesianPoint, WGS84Point
29 from neo4j.types.temporal import Duration, Date, Time, DateTime
30
31 from test.integration.tools import DirectIntegrationTestCase
32
33
34 def run_and_rollback(tx, statement, **parameters):
35 result = tx.run(statement, **parameters)
36 value = result.single().value()
37 tx.success = False
38 return value
39
40
41 class CoreTypeOutputTestCase(DirectIntegrationTestCase):
42
43 def test_null(self):
44 with self.driver.session() as session:
45 result = session.run("RETURN null")
46 self.assertIs(result.single().value(), None)
47
48 def test_boolean(self):
49 with self.driver.session() as session:
50 result = session.run("RETURN true")
51 self.assertIs(result.single().value(), True)
52
53 def test_integer(self):
54 with self.driver.session() as session:
55 result = session.run("RETURN 123456789")
56 self.assertEqual(result.single().value(), 123456789)
57
58 def test_float(self):
59 with self.driver.session() as session:
60 result = session.run("RETURN 3.1415926")
61 self.assertEqual(result.single().value(), 3.1415926)
62
63 def test_float_nan(self):
64 not_a_number = float("NaN")
65 with self.driver.session() as session:
66 result = session.run("WITH $x AS x RETURN x", x=not_a_number)
67 self.assertTrue(isnan(result.single().value()))
68
69 def test_float_positive_infinity(self):
70 infinity = float("+Inf")
71 with self.driver.session() as session:
72 result = session.run("WITH $x AS x RETURN x", x=infinity)
73 self.assertEqual(result.single().value(), infinity)
74
75 def test_float_negative_infinity(self):
76 infinity = float("-Inf")
77 with self.driver.session() as session:
78 result = session.run("WITH $x AS x RETURN x", x=infinity)
79 self.assertEqual(result.single().value(), infinity)
80
81 def test_string(self):
82 with self.driver.session() as session:
83 result = session.run("RETURN 'hello, world'")
84 self.assertEqual(result.single().value(), "hello, world")
85
86 def test_bytes(self):
87 with self.driver.session() as session:
88 data = bytearray([0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF])
89 try:
90 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
91 except TypeError:
92 raise SkipTest("Bytes not supported in this server version")
93 self.assertEqual(value, data)
94
95 def test_list(self):
96 with self.driver.session() as session:
97 result = session.run("RETURN ['one', 'two', 'three']")
98 self.assertEqual(result.single().value(), ["one", "two", "three"])
99
100 def test_map(self):
101 with self.driver.session() as session:
102 result = session.run("RETURN {one: 'eins', two: 'zwei', three: 'drei'}")
103 self.assertEqual(result.single().value(), {"one": "eins", "two": "zwei", "three": "drei"})
104
105 def test_non_string_map_keys(self):
106 with self.driver.session() as session:
107 with self.assertRaises(TypeError):
108 _ = session.run("RETURN $x", x={1: 'eins', 2: 'zwei', 3: 'drei'})
109
110
111 class GraphTypeOutputTestCase(DirectIntegrationTestCase):
112
113 def test_node(self):
114 with self.driver.session() as session:
115 a = session.write_transaction(run_and_rollback, "CREATE (a:Person {name:'Alice'}) RETURN a")
116 self.assertIsInstance(a, Node)
117 self.assertEqual(a.labels, {"Person"})
118 self.assertEqual(dict(a), {"name": "Alice"})
119
120 def test_relationship(self):
121 with self.driver.session() as session:
122 a, b, r = session.write_transaction(
123 run_and_rollback, "CREATE (a)-[r:KNOWS {since:1999}]->(b) RETURN [a, b, r]")
124 self.assertIsInstance(r, Relationship)
125 self.assertEqual(r.type, "KNOWS")
126 self.assertEqual(dict(r), {"since": 1999})
127 self.assertEqual(r.start_node, a)
128 self.assertEqual(r.end_node, b)
129
130 def test_path(self):
131 with self.driver.session() as session:
132 a, b, c, ab, bc, p = session.write_transaction(
133 run_and_rollback, "CREATE p=(a)-[ab:X]->(b)-[bc:X]->(c) RETURN [a, b, c, ab, bc, p]")
134 self.assertIsInstance(p, Path)
135 self.assertEqual(len(p), 2)
136 self.assertEqual(p.nodes, (a, b, c))
137 self.assertEqual(p.relationships, (ab, bc))
138 self.assertEqual(p.start_node, a)
139 self.assertEqual(p.end_node, c)
140
141
142 class SpatialTypeInputTestCase(DirectIntegrationTestCase):
143
144 def test_cartesian_point(self):
145 self.assert_supports_spatial_types()
146 with self.driver.session() as session:
147 result = session.run("CYPHER runtime=interpreted WITH $point AS point "
148 "RETURN point.x, point.y",
149 point=CartesianPoint((1.23, 4.56)))
150 x, y = result.single()
151 self.assertEqual(x, 1.23)
152 self.assertEqual(y, 4.56)
153
154 def test_cartesian_3d_point(self):
155 self.assert_supports_spatial_types()
156 with self.driver.session() as session:
157 result = session.run("CYPHER runtime=interpreted WITH $point AS point "
158 "RETURN point.x, point.y, point.z",
159 point=CartesianPoint((1.23, 4.56, 7.89)))
160 x, y, z = result.single()
161 self.assertEqual(x, 1.23)
162 self.assertEqual(y, 4.56)
163 self.assertEqual(z, 7.89)
164
165 def test_wgs84_point(self):
166 self.assert_supports_spatial_types()
167 with self.driver.session() as session:
168 result = session.run("CYPHER runtime=interpreted WITH $point AS point "
169 "RETURN point.latitude, point.longitude",
170 point=WGS84Point((1.23, 4.56)))
171 latitude, longitude = result.single()
172 self.assertEqual(longitude, 1.23)
173 self.assertEqual(latitude, 4.56)
174
175 def test_wgs84_3d_point(self):
176 self.assert_supports_spatial_types()
177 with self.driver.session() as session:
178 result = session.run("CYPHER runtime=interpreted WITH $point AS point "
179 "RETURN point.latitude, point.longitude, point.height",
180 point=WGS84Point((1.23, 4.56, 7.89)))
181 latitude, longitude, height = result.single()
182 self.assertEqual(longitude, 1.23)
183 self.assertEqual(latitude, 4.56)
184 self.assertEqual(height, 7.89)
185
186 def test_point_array(self):
187 self.assert_supports_temporal_types()
188 with self.driver.session() as session:
189 data = [WGS84Point((1.23, 4.56)), WGS84Point((9.87, 6.54))]
190 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
191 self.assertEqual(value, data)
192
193
194 class SpatialTypeOutputTestCase(DirectIntegrationTestCase):
195
196 def test_cartesian_point(self):
197 self.assert_supports_spatial_types()
198 with self.driver.session() as session:
199 result = session.run("RETURN point({x:3, y:4})")
200 value = result.single().value()
201 self.assertIsInstance(value, CartesianPoint)
202 self.assertEqual(value.x, 3.0)
203 self.assertEqual(value.y, 4.0)
204 with self.assertRaises(AttributeError):
205 _ = value.z
206
207 def test_cartesian_3d_point(self):
208 self.assert_supports_spatial_types()
209 with self.driver.session() as session:
210 result = session.run("RETURN point({x:3, y:4, z:5})")
211 value = result.single().value()
212 self.assertIsInstance(value, CartesianPoint)
213 self.assertEqual(value.x, 3.0)
214 self.assertEqual(value.y, 4.0)
215 self.assertEqual(value.z, 5.0)
216
217 def test_wgs84_point(self):
218 self.assert_supports_spatial_types()
219 with self.driver.session() as session:
220 result = session.run("RETURN point({latitude:3, longitude:4})")
221 value = result.single().value()
222 self.assertIsInstance(value, WGS84Point)
223 self.assertEqual(value.latitude, 3.0)
224 self.assertEqual(value.y, 3.0)
225 self.assertEqual(value.longitude, 4.0)
226 self.assertEqual(value.x, 4.0)
227 with self.assertRaises(AttributeError):
228 _ = value.height
229 with self.assertRaises(AttributeError):
230 _ = value.z
231
232 def test_wgs84_3d_point(self):
233 self.assert_supports_spatial_types()
234 with self.driver.session() as session:
235 result = session.run("RETURN point({latitude:3, longitude:4, height:5})")
236 value = result.single().value()
237 self.assertIsInstance(value, WGS84Point)
238 self.assertEqual(value.latitude, 3.0)
239 self.assertEqual(value.y, 3.0)
240 self.assertEqual(value.longitude, 4.0)
241 self.assertEqual(value.x, 4.0)
242 self.assertEqual(value.height, 5.0)
243 self.assertEqual(value.z, 5.0)
244
245
246 class TemporalTypeInputTestCase(DirectIntegrationTestCase):
247
248 def test_native_date(self):
249 from datetime import date
250 self.assert_supports_temporal_types()
251 with self.driver.session() as session:
252 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
253 "RETURN x.year, x.month, x.day",
254 x=date(1976, 6, 13))
255 year, month, day = result.single()
256 self.assertEqual(year, 1976)
257 self.assertEqual(month, 6)
258 self.assertEqual(day, 13)
259
260 def test_date(self):
261 self.assert_supports_temporal_types()
262 with self.driver.session() as session:
263 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
264 "RETURN x.year, x.month, x.day",
265 x=Date(1976, 6, 13))
266 year, month, day = result.single()
267 self.assertEqual(year, 1976)
268 self.assertEqual(month, 6)
269 self.assertEqual(day, 13)
270
271 def test_date_array(self):
272 self.assert_supports_temporal_types()
273 with self.driver.session() as session:
274 data = [DateTime.now().date(), Date(1976, 6, 13)]
275 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
276 self.assertEqual(value, data)
277
278 def test_native_time(self):
279 from datetime import time
280 self.assert_supports_temporal_types()
281 with self.driver.session() as session:
282 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
283 "RETURN x.hour, x.minute, x.second, x.nanosecond",
284 x=time(12, 34, 56, 789012))
285 hour, minute, second, nanosecond = result.single()
286 self.assertEqual(hour, 12)
287 self.assertEqual(minute, 34)
288 self.assertEqual(second, 56)
289 self.assertEqual(nanosecond, 789012000)
290
291 def test_whole_second_time(self):
292 self.assert_supports_temporal_types()
293 with self.driver.session() as session:
294 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
295 "RETURN x.hour, x.minute, x.second",
296 x=Time(12, 34, 56))
297 hour, minute, second = result.single()
298 self.assertEqual(hour, 12)
299 self.assertEqual(minute, 34)
300 self.assertEqual(second, 56)
301
302 def test_nanosecond_resolution_time(self):
303 self.assert_supports_temporal_types()
304 with self.driver.session() as session:
305 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
306 "RETURN x.hour, x.minute, x.second, x.nanosecond",
307 x=Time(12, 34, 56.789012345))
308 hour, minute, second, nanosecond = result.single()
309 self.assertEqual(hour, 12)
310 self.assertEqual(minute, 34)
311 self.assertEqual(second, 56)
312 self.assertEqual(nanosecond, 789012345)
313
314 def test_time_with_numeric_time_offset(self):
315 self.assert_supports_temporal_types()
316 with self.driver.session() as session:
317 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
318 "RETURN x.hour, x.minute, x.second, x.nanosecond, x.offset",
319 x=Time(12, 34, 56.789012345, tzinfo=FixedOffset(90)))
320 hour, minute, second, nanosecond, offset = result.single()
321 self.assertEqual(hour, 12)
322 self.assertEqual(minute, 34)
323 self.assertEqual(second, 56)
324 self.assertEqual(nanosecond, 789012345)
325 self.assertEqual(offset, "+01:30")
326
327 def test_time_array(self):
328 self.assert_supports_temporal_types()
329 with self.driver.session() as session:
330 data = [Time(12, 34, 56), Time(10, 0, 0)]
331 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
332 self.assertEqual(value, data)
333
334 def test_native_datetime(self):
335 from datetime import datetime
336 self.assert_supports_temporal_types()
337 with self.driver.session() as session:
338 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
339 "RETURN x.year, x.month, x.day, "
340 " x.hour, x.minute, x.second, x.nanosecond",
341 x=datetime(1976, 6, 13, 12, 34, 56, 789012))
342 year, month, day, hour, minute, second, nanosecond = result.single()
343 self.assertEqual(year, 1976)
344 self.assertEqual(month, 6)
345 self.assertEqual(day, 13)
346 self.assertEqual(hour, 12)
347 self.assertEqual(minute, 34)
348 self.assertEqual(second, 56)
349 self.assertEqual(nanosecond, 789012000)
350
351 def test_whole_second_datetime(self):
352 self.assert_supports_temporal_types()
353 with self.driver.session() as session:
354 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
355 "RETURN x.year, x.month, x.day, "
356 " x.hour, x.minute, x.second",
357 x=DateTime(1976, 6, 13, 12, 34, 56))
358 year, month, day, hour, minute, second = result.single()
359 self.assertEqual(year, 1976)
360 self.assertEqual(month, 6)
361 self.assertEqual(day, 13)
362 self.assertEqual(hour, 12)
363 self.assertEqual(minute, 34)
364 self.assertEqual(second, 56)
365
366 def test_nanosecond_resolution_datetime(self):
367 self.assert_supports_temporal_types()
368 with self.driver.session() as session:
369 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
370 "RETURN x.year, x.month, x.day, "
371 " x.hour, x.minute, x.second, x.nanosecond",
372 x=DateTime(1976, 6, 13, 12, 34, 56.789012345))
373 year, month, day, hour, minute, second, nanosecond = result.single()
374 self.assertEqual(year, 1976)
375 self.assertEqual(month, 6)
376 self.assertEqual(day, 13)
377 self.assertEqual(hour, 12)
378 self.assertEqual(minute, 34)
379 self.assertEqual(second, 56)
380 self.assertEqual(nanosecond, 789012345)
381
382 def test_datetime_with_numeric_time_offset(self):
383 self.assert_supports_temporal_types()
384 with self.driver.session() as session:
385 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
386 "RETURN x.year, x.month, x.day, "
387 " x.hour, x.minute, x.second, x.nanosecond, x.offset",
388 x=DateTime(1976, 6, 13, 12, 34, 56.789012345, tzinfo=FixedOffset(90)))
389 year, month, day, hour, minute, second, nanosecond, offset = result.single()
390 self.assertEqual(year, 1976)
391 self.assertEqual(month, 6)
392 self.assertEqual(day, 13)
393 self.assertEqual(hour, 12)
394 self.assertEqual(minute, 34)
395 self.assertEqual(second, 56)
396 self.assertEqual(nanosecond, 789012345)
397 self.assertEqual(offset, "+01:30")
398
399 def test_datetime_with_named_time_zone(self):
400 self.assert_supports_temporal_types()
401 with self.driver.session() as session:
402 input_value = timezone("US/Pacific").localize(DateTime(1976, 6, 13, 12, 34, 56.789012345))
403 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
404 "RETURN x.year, x.month, x.day, "
405 " x.hour, x.minute, x.second, x.nanosecond, x.timezone",
406 x=input_value)
407 year, month, day, hour, minute, second, nanosecond, tz = result.single()
408 self.assertEqual(year, input_value.year)
409 self.assertEqual(month, input_value.month)
410 self.assertEqual(day, input_value.day)
411 self.assertEqual(hour, input_value.hour)
412 self.assertEqual(minute, input_value.minute)
413 self.assertEqual(second, int(input_value.second))
414 self.assertEqual(nanosecond, int (1000000000 * input_value.second) % 1000000000)
415 self.assertEqual(tz, input_value.tzinfo.zone)
416
417 def test_datetime_array(self):
418 self.assert_supports_temporal_types()
419 with self.driver.session() as session:
420 data = [DateTime(2018, 4, 6, 13, 4, 42.516120), DateTime(1976, 6, 13)]
421 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
422 self.assertEqual(value, data)
423
424 def test_duration(self):
425 self.assert_supports_temporal_types()
426 with self.driver.session() as session:
427 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
428 "RETURN x.months, x.days, x.seconds, x.microsecondsOfSecond",
429 x=Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6.789012))
430 months, days, seconds, microseconds = result.single()
431 self.assertEqual(months, 14)
432 self.assertEqual(days, 3)
433 self.assertEqual(seconds, 14706)
434 self.assertEqual(microseconds, 789012)
435
436 def test_duration_array(self):
437 self.assert_supports_temporal_types()
438 with self.driver.session() as session:
439 data = [Duration(1, 2, 3, 4, 5, 6), Duration(9, 8, 7, 6, 5, 4)]
440 value = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
441 self.assertEqual(value, data)
442
443 def test_timedelta(self):
444 from datetime import timedelta
445 self.assert_supports_temporal_types()
446 with self.driver.session() as session:
447 result = session.run("CYPHER runtime=interpreted WITH $x AS x "
448 "RETURN x.months, x.days, x.seconds, x.microsecondsOfSecond",
449 x=timedelta(days=3, hours=4, minutes=5, seconds=6.789012))
450 months, days, seconds, microseconds = result.single()
451 self.assertEqual(months, 0)
452 self.assertEqual(days, 3)
453 self.assertEqual(seconds, 14706)
454 self.assertEqual(microseconds, 789012)
455
456 def test_mixed_array(self):
457 self.assert_supports_temporal_types()
458 with self.driver.session() as session:
459 data = [Date(1976, 6, 13), Duration(9, 8, 7, 6, 5, 4)]
460 with self.assertRaises(CypherTypeError):
461 _ = session.write_transaction(run_and_rollback, "CREATE (a {x:$x}) RETURN a.x", x=data)
462
463
464 class TemporalTypeOutputTestCase(DirectIntegrationTestCase):
465
466 def test_date(self):
467 self.assert_supports_temporal_types()
468 with self.driver.session() as session:
469 result = session.run("RETURN date('1976-06-13')")
470 value = result.single().value()
471 self.assertIsInstance(value, Date)
472 self.assertEqual(value, Date(1976, 6, 13))
473
474 def test_whole_second_time(self):
475 self.assert_supports_temporal_types()
476 with self.driver.session() as session:
477 result = session.run("RETURN time('12:34:56')")
478 value = result.single().value()
479 self.assertIsInstance(value, Time)
480 self.assertEqual(value, Time(12, 34, 56, tzinfo=FixedOffset(0)))
481
482 def test_nanosecond_resolution_time(self):
483 self.assert_supports_temporal_types()
484 with self.driver.session() as session:
485 result = session.run("RETURN time('12:34:56.789012345')")
486 value = result.single().value()
487 self.assertIsInstance(value, Time)
488 self.assertEqual(value, Time(12, 34, 56.789012345, tzinfo=FixedOffset(0)))
489
490 def test_time_with_numeric_time_offset(self):
491 self.assert_supports_temporal_types()
492 with self.driver.session() as session:
493 result = session.run("RETURN time('12:34:56.789012345+0130')")
494 value = result.single().value()
495 self.assertIsInstance(value, Time)
496 self.assertEqual(value, Time(12, 34, 56.789012345, tzinfo=FixedOffset(90)))
497
498 def test_whole_second_localtime(self):
499 self.assert_supports_temporal_types()
500 with self.driver.session() as session:
501 result = session.run("RETURN localtime('12:34:56')")
502 value = result.single().value()
503 self.assertIsInstance(value, Time)
504 self.assertEqual(value, Time(12, 34, 56))
505
506 def test_nanosecond_resolution_localtime(self):
507 self.assert_supports_temporal_types()
508 with self.driver.session() as session:
509 result = session.run("RETURN localtime('12:34:56.789012345')")
510 value = result.single().value()
511 self.assertIsInstance(value, Time)
512 self.assertEqual(value, Time(12, 34, 56.789012345))
513
514 def test_whole_second_datetime(self):
515 self.assert_supports_temporal_types()
516 with self.driver.session() as session:
517 result = session.run("RETURN datetime('1976-06-13T12:34:56')")
518 value = result.single().value()
519 self.assertIsInstance(value, DateTime)
520 self.assertEqual(value, DateTime(1976, 6, 13, 12, 34, 56, tzinfo=utc))
521
522 def test_nanosecond_resolution_datetime(self):
523 self.assert_supports_temporal_types()
524 with self.driver.session() as session:
525 result = session.run("RETURN datetime('1976-06-13T12:34:56.789012345')")
526 value = result.single().value()
527 self.assertIsInstance(value, DateTime)
528 self.assertEqual(value, DateTime(1976, 6, 13, 12, 34, 56.789012345, tzinfo=utc))
529
530 def test_datetime_with_numeric_time_offset(self):
531 self.assert_supports_temporal_types()
532 with self.driver.session() as session:
533 result = session.run("RETURN datetime('1976-06-13T12:34:56.789012345+01:30')")
534 value = result.single().value()
535 self.assertIsInstance(value, DateTime)
536 self.assertEqual(value, DateTime(1976, 6, 13, 12, 34, 56.789012345, tzinfo=FixedOffset(90)))
537
538 def test_datetime_with_named_time_zone(self):
539 self.assert_supports_temporal_types()
540 with self.driver.session() as session:
541 result = session.run("RETURN datetime('1976-06-13T12:34:56.789012345[Europe/London]')")
542 value = result.single().value()
543 self.assertIsInstance(value, DateTime)
544 self.assertEqual(value, timezone("Europe/London").localize(DateTime(1976, 6, 13, 12, 34, 56.789012345)))
545
546 def test_whole_second_localdatetime(self):
547 self.assert_supports_temporal_types()
548 with self.driver.session() as session:
549 result = session.run("RETURN localdatetime('1976-06-13T12:34:56')")
550 value = result.single().value()
551 self.assertIsInstance(value, DateTime)
552 self.assertEqual(value, DateTime(1976, 6, 13, 12, 34, 56))
553
554 def test_nanosecond_resolution_localdatetime(self):
555 self.assert_supports_temporal_types()
556 with self.driver.session() as session:
557 result = session.run("RETURN localdatetime('1976-06-13T12:34:56.789012345')")
558 value = result.single().value()
559 self.assertIsInstance(value, DateTime)
560 self.assertEqual(value, DateTime(1976, 6, 13, 12, 34, 56.789012345))
561
562 def test_duration(self):
563 self.assert_supports_temporal_types()
564 with self.driver.session() as session:
565 result = session.run("RETURN duration('P1Y2M3DT4H5M6.789S')")
566 value = result.single().value()
567 self.assertIsInstance(value, Duration)
568 self.assertEqual(value, Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6.789))
569
570 def test_nanosecond_resolution_duration(self):
571 self.assert_supports_temporal_types()
572 with self.driver.session() as session:
573 result = session.run("RETURN duration('P1Y2M3DT4H5M6.789123456S')")
574 value = result.single().value()
575 self.assertIsInstance(value, Duration)
576 self.assertEqual(value, Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6.789123456))
+0
-223
test/integration/tools.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from os import makedirs, remove
22 from os.path import basename, dirname, join as path_join, realpath, isfile, expanduser
23 import platform
24 from unittest import TestCase, SkipTest
25 from shutil import copyfile
26 from sys import exit, stderr
27 try:
28 from urllib.request import urlretrieve
29 except ImportError:
30 from urllib import urlretrieve
31
32 from boltkit.config import update as update_config
33 from boltkit.controller import _install, WindowsController, UnixController
34
35 from neo4j import GraphDatabase
36 from neo4j.exceptions import AuthError
37
38 from test.env import NEO4J_SERVER_PACKAGE, NEO4J_USER, NEO4J_PASSWORD, NEOCTRL_ARGS
39
40
41 def copy_dist(source, target):
42 if isfile(target) and "SNAPSHOT" not in basename(source):
43 return target
44 try:
45 makedirs(dirname(target))
46 except OSError:
47 pass
48 if source.startswith("http:"):
49 stderr.write("Downloading package from {}\n".format(source))
50 urlretrieve(source, target)
51 return target
52 else:
53 return copyfile(source, target)
54
55
56 def is_listening(address):
57 from socket import create_connection
58 try:
59 s = create_connection(address)
60 except IOError:
61 return False
62 else:
63 s.close()
64 return True
65
66
67 class ServerVersion(object):
68 def __init__(self, product, version_tuple, tags_tuple):
69 self.product = product
70 self.version_tuple = version_tuple
71 self.tags_tuple = tags_tuple
72
73 def at_least_version(self, major, minor):
74 return self.version_tuple >= (major, minor)
75
76 @classmethod
77 def from_str(cls, full_version):
78 if full_version is None:
79 return ServerVersion("Neo4j", (3, 0), ())
80 product, _, tagged_version = full_version.partition("/")
81 tags = tagged_version.split("-")
82 version = map(int, tags[0].split("."))
83 return ServerVersion(product, tuple(version), tuple(tags[1:]))
84
85
86 class IntegrationTestCase(TestCase):
87 """ Base class for test cases that integrate with a server.
88 """
89
90 bolt_port = 7687
91 bolt_address = ("localhost", bolt_port)
92
93 bolt_uri = "bolt://%s:%d" % bolt_address
94 bolt_routing_uri = "bolt+routing://%s:%d" % bolt_address
95
96 user = NEO4J_USER or "neo4j"
97 password = NEO4J_PASSWORD or "password"
98 auth_token = (user, password)
99
100 controller = None
101 dist_path = path_join(dirname(__file__), "dist")
102 run_path = path_join(dirname(__file__), "run")
103
104 server_package = NEO4J_SERVER_PACKAGE
105 local_server_package = path_join(dist_path, basename(server_package)) if server_package else None
106 neoctrl_args = NEOCTRL_ARGS
107
108 @classmethod
109 def server_version_info(cls):
110 with GraphDatabase.driver(cls.bolt_uri, auth=cls.auth_token) as driver:
111 with driver.session() as session:
112 full_version = session.run("RETURN 1").summary().server.version
113 return ServerVersion.from_str(full_version)
114
115 @classmethod
116 def at_least_server_version(cls, major, minor):
117 return cls.server_version_info().at_least_version(major, minor)
118
119 @classmethod
120 def protocol_version(cls):
121 with GraphDatabase.driver(cls.bolt_uri, auth=cls.auth_token) as driver:
122 with driver.session() as session:
123 return session.run("RETURN 1").summary().protocol_version
124
125 @classmethod
126 def at_least_protocol_version(cls, version):
127 return cls.protocol_version() >= version
128
129 @classmethod
130 def assert_supports_spatial_types(cls):
131 if not cls.at_least_protocol_version(2):
132 raise SkipTest("Spatial types require Bolt protocol v2 or above")
133
134 @classmethod
135 def assert_supports_temporal_types(cls):
136 if not cls.at_least_protocol_version(2):
137 raise SkipTest("Temporal types require Bolt protocol v2 or above")
138
139 @classmethod
140 def delete_known_hosts_file(cls):
141 known_hosts = path_join(expanduser("~"), ".neo4j", "known_hosts")
142 if isfile(known_hosts):
143 remove(known_hosts)
144
145 @classmethod
146 def _unpack(cls, package):
147 try:
148 makedirs(cls.run_path)
149 except OSError:
150 pass
151 controller_class = WindowsController if platform.system() == "Windows" else UnixController
152 home = realpath(controller_class.extract(package, cls.run_path))
153 return home
154
155 @classmethod
156 def _start_server(cls, home):
157 controller_class = WindowsController if platform.system() == "Windows" else UnixController
158 cls.controller = controller_class(home, 1)
159 update_config(cls.controller.home, {"dbms.connectors.default_listen_address": "::"})
160 if NEO4J_USER is None:
161 cls.controller.create_user(cls.user, cls.password)
162 cls.controller.set_user_role(cls.user, "admin")
163 cls.controller.start(timeout=90)
164
165 @classmethod
166 def _stop_server(cls):
167 if cls.controller is not None:
168 cls.controller.stop()
169 if NEO4J_USER is None:
170 pass # TODO: delete user
171
172 @classmethod
173 def setUpClass(cls):
174 if is_listening(cls.bolt_address):
175 print("Using existing server listening on port {}\n".format(cls.bolt_port))
176 with GraphDatabase.driver(cls.bolt_uri, auth=cls.auth_token) as driver:
177 try:
178 with driver.session():
179 pass
180 except AuthError as error:
181 raise RuntimeError("Failed to authenticate (%s)" % error)
182 elif cls.server_package is not None:
183 print("Using server from package {}\n".format(cls.server_package))
184 package = copy_dist(cls.server_package, cls.local_server_package)
185 home = cls._unpack(package)
186 cls._start_server(home)
187 elif cls.neoctrl_args is not None:
188 print("Using boltkit to install server 'neoctrl-install {}'\n".format(cls.neoctrl_args))
189 edition = "enterprise" if "-e" in cls.neoctrl_args else "community"
190 version = cls.neoctrl_args.split()[-1]
191 home = _install(edition, version, cls.run_path)
192 cls._start_server(home)
193 else:
194 raise SkipTest("No Neo4j server available for %s" % cls.__name__)
195
196 @classmethod
197 def tearDownClass(cls):
198 cls._stop_server()
199
200
201 class DirectIntegrationTestCase(IntegrationTestCase):
202
203 driver = None
204
205 def setUp(self):
206 from neo4j import GraphDatabase
207 self.driver = GraphDatabase.driver(self.bolt_uri, auth=self.auth_token)
208
209 def tearDown(self):
210 self.driver.close()
211
212
213 class RoutingIntegrationTestCase(IntegrationTestCase):
214
215 driver = None
216
217 def setUp(self):
218 from neo4j import GraphDatabase
219 self.driver = GraphDatabase.driver(self.bolt_routing_uri, auth=self.auth_token)
220
221 def tearDown(self):
222 self.driver.close()
+0
-8
test/performance/README.md less more
0 # Performance Tests
1
2 This package contains performance tests.
3 To run a performance test, simply execute the module.
4 For example:
5 ```bash
6 python -m test.performance.stress
7 ```
+0
-0
test/performance/__init__.py less more
(Empty file)
+0
-106
test/performance/jexp.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from __future__ import division
22
23 from sys import stderr
24 from threading import Thread
25 from timeit import default_timer as time
26
27 from neo4j import GraphDatabase, CypherError
28
29
30 class Runner(Thread):
31
32 def __init__(self, size, batch_size):
33 super(Runner, self).__init__()
34 self.size = size
35 self.batch_size = batch_size
36 self.driver = GraphDatabase.driver("bolt://localhost:7687/", auth=("neo4j", "password"))
37
38 def run(self):
39 self.drop_index()
40 self.delete_all()
41 self.create_nodes()
42 self.create_index()
43 match_runs = 20
44 times = []
45 for _ in range(match_runs):
46 times.append(self.match_nodes())
47 stderr.write("=======\n")
48 stderr.write("Average match run time was %fs\n" % (sum(times) / match_runs,))
49 stderr.write("Minimum match run time was %fs\n" % (min(times),))
50
51 def drop_index(self):
52 with self.driver.session() as session:
53 try:
54 session.run("DROP INDEX ON :Person(name)").consume()
55 except CypherError:
56 pass
57
58 def delete_all(self):
59 t0 = time()
60 with self.driver.session() as session:
61 total_nodes_deleted = 0
62 deleting = True
63 while deleting:
64 summary = session.run("""\
65 MATCH (a) WITH a LIMIT {size}
66 DETACH DELETE a
67 """, size=self.batch_size).summary()
68 total_nodes_deleted += summary.counters.nodes_deleted
69 stderr.write("Deleted %d nodes\r" % total_nodes_deleted)
70 deleting = bool(summary.counters.nodes_deleted)
71 t1 = time()
72 stderr.write("Deleted %d nodes in %fs\n" % (total_nodes_deleted, t1 - t0))
73
74 def create_nodes(self):
75 t0 = time()
76 with self.driver.session() as session:
77 session.run("""\
78 UNWIND range(1, {count}) AS id
79 CREATE (a:Person {id: id, name: 'name ' + id})
80 """, count=self.size)
81 t1 = time()
82 stderr.write("Created %d nodes in %fs\n" % (self.size, t1 - t0))
83
84 def create_index(self):
85 with self.driver.session() as session:
86 session.run("CREATE INDEX ON :Person(name)").consume()
87
88 def match_nodes(self):
89 t0 = time()
90 with self.driver.session() as session:
91 with session.begin_transaction() as tx:
92 n = 0
93 result = tx.run("MATCH (a) RETURN a.name AS name")
94 for n, record in enumerate(result, 1):
95 _ = record["name"]
96 if n % self.batch_size == 0:
97 stderr.write("Matched %d nodes\r" % n)
98 t1 = time()
99 time_delta = t1 - t0
100 stderr.write("Matched %d nodes in %fs\n" % (n, time_delta))
101 return time_delta
102
103
104 if __name__ == "__main__":
105 Runner(1000000, 20000).run()
+0
-104
test/performance/stress.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 from sys import stderr
21 from threading import Thread
22 from timeit import default_timer as time
23
24 from neo4j import GraphDatabase, CypherError
25
26
27 class Runner(Thread):
28
29 def __init__(self, size, batch_size):
30 super(Runner, self).__init__()
31 self.size = size
32 self.batch_size = batch_size
33 self.driver = GraphDatabase.driver("bolt://localhost:7687/", auth=("neo4j", "password"))
34
35 def run(self):
36 self.drop_index()
37 self.delete_all()
38 self.create_nodes()
39 self.create_index()
40 self.match_nodes()
41
42 def drop_index(self):
43 with self.driver.session() as session:
44 try:
45 session.run("DROP INDEX ON :Thing(integer)").consume()
46 except CypherError:
47 pass
48
49 def delete_all(self):
50 t0 = time()
51 with self.driver.session() as session:
52 total_nodes_deleted = 0
53 deleting = True
54 while deleting:
55 summary = session.run("""\
56 MATCH (a) WITH a LIMIT {size}
57 DETACH DELETE a
58 """, size=self.batch_size).summary()
59 total_nodes_deleted += summary.counters.nodes_deleted
60 stderr.write("Deleted %d nodes\r" % total_nodes_deleted)
61 deleting = bool(summary.counters.nodes_deleted)
62 t1 = time()
63 stderr.write("Deleted %d nodes in %fs\n" % (total_nodes_deleted, t1 - t0))
64
65 def create_nodes(self):
66 t0 = time()
67 with self.driver.session() as session:
68 p = 0
69 while p < self.size:
70 q = min(p + self.batch_size, self.size)
71 with session.begin_transaction() as tx:
72 for n in range(p + 1, q + 1):
73 tx.run("CREATE (a:Thing {x})", x={
74 "boolean": n % 2 == 0,
75 "integer": n,
76 "float": float(n),
77 "string": "number %d" % n,
78 })
79 stderr.write("Created %d nodes\r" % q)
80 p = q
81 t1 = time()
82 stderr.write("Created %d nodes in %fs\n" % (self.size, t1 - t0))
83
84 def create_index(self):
85 with self.driver.session() as session:
86 session.run("CREATE INDEX ON :Thing(integer)").consume()
87
88 def match_nodes(self):
89 t0 = time()
90 with self.driver.session() as session:
91 with session.begin_transaction() as tx:
92 n = 0
93 result = tx.run("MATCH (a:Thing) RETURN a")
94 for n, record in enumerate(result, 1):
95 _ = record["a"]
96 if n % self.batch_size == 0:
97 stderr.write("Matched %d nodes\r" % n)
98 t1 = time()
99 stderr.write("Matched %d nodes in %fs\n" % (n, t1 - t0))
100
101
102 if __name__ == "__main__":
103 Runner(100000, 20000).run()
+0
-78
test/performance/test_results.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from itertools import product
22
23 from pytest import mark
24
25 from neo4j import GraphDatabase
26 from .tools import GraphDatabaseServer
27
28
29 class ReadWorkload(object):
30
31 server = None
32 driver = None
33
34 @classmethod
35 def setup_class(cls):
36 cls.server = server = GraphDatabaseServer()
37 server.start()
38 cls.driver = GraphDatabase.driver(server.bolt_uri, auth=server.auth_token)
39
40 @classmethod
41 def teardown_class(cls):
42 cls.driver.close()
43 cls.server.stop()
44
45 def work(self, *units_of_work):
46 def runner():
47 with self.driver.session() as session:
48 for unit_of_work in units_of_work:
49 session.read_transaction(unit_of_work)
50 return runner
51
52
53 class TestReadWorkload(ReadWorkload):
54
55 @staticmethod
56 def test_cypher(width):
57 return "UNWIND range(1, $count) AS _ RETURN {}".format(", ".join("$x AS x{}".format(i) for i in range(width)))
58
59 @staticmethod
60 def uow(record_count, record_width, value):
61
62 def _(tx):
63 s = "UNWIND range(1, $record_count) AS _ RETURN {}".format(
64 ", ".join("$x AS x{}".format(i) for i in range(record_width)))
65 p = {"record_count": record_count, "x": value}
66 for record in tx.run(s, p):
67 assert all(x == value for x in record.values())
68
69 return _
70
71 @mark.parametrize("record_count,record_width,value", product(
72 [1, 1000], # record count
73 [1, 10], # record width
74 [1, u'hello, world'], # value
75 ))
76 def test_1x1(self, benchmark, record_count, record_width, value):
77 benchmark(self.work(self.uow(record_count, record_width, value)))
+0
-157
test/performance/tools.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from test.integration.tools import IntegrationTestCase
22
23 from os import makedirs, remove
24 from os.path import basename, dirname, join as path_join, realpath, isfile, expanduser
25 import platform
26 from unittest import TestCase, SkipTest
27 from shutil import copyfile
28 from sys import exit, stderr
29 try:
30 from urllib.request import urlretrieve
31 except ImportError:
32 from urllib import urlretrieve
33
34 from boltkit.controller import WindowsController, UnixController
35
36 from neo4j import GraphDatabase
37 from neo4j.exceptions import AuthError
38
39 from test.env import NEO4J_SERVER_PACKAGE, NEO4J_USER, NEO4J_PASSWORD
40 from test.integration.tools import ServerVersion
41
42
43 def copy_dist(source, target):
44 if isfile(target) and "SNAPSHOT" not in basename(source):
45 return target
46 try:
47 makedirs(dirname(target))
48 except OSError:
49 pass
50 if source.startswith("http:"):
51 stderr.write("Downloading package from {}\n".format(source))
52 urlretrieve(source, target)
53 return target
54 else:
55 return copyfile(source, target)
56
57
58 def is_listening(address):
59 from socket import create_connection
60 try:
61 s = create_connection(address)
62 except IOError:
63 return False
64 else:
65 s.close()
66 return True
67
68
69 class GraphDatabaseServer(object):
70
71 bolt_port = 7687
72 bolt_address = ("localhost", bolt_port)
73
74 bolt_uri = "bolt://%s:%d" % bolt_address
75 bolt_routing_uri = "bolt+routing://%s:%d" % bolt_address
76
77 user = NEO4J_USER or "test"
78 password = NEO4J_PASSWORD or "test"
79 auth_token = (user, password)
80
81 controller = None
82 dist_path = path_join(dirname(__file__), "dist")
83 run_path = path_join(dirname(__file__), "run")
84
85 server_package = NEO4J_SERVER_PACKAGE
86 local_server_package = path_join(dist_path, basename(server_package)) if server_package else None
87
88 @classmethod
89 def server_version_info(cls):
90 with GraphDatabase.driver(cls.bolt_uri, auth=cls.auth_token) as driver:
91 with driver.session() as session:
92 full_version = session.run("RETURN 1").summary().server.version
93 return ServerVersion.from_str(full_version)
94
95 @classmethod
96 def at_least_version(cls, major, minor):
97 return cls.server_version_info().at_least_version(major, minor)
98
99 @classmethod
100 def delete_known_hosts_file(cls):
101 known_hosts = path_join(expanduser("~"), ".neo4j", "known_hosts")
102 if isfile(known_hosts):
103 remove(known_hosts)
104
105 @classmethod
106 def _start_server(cls, package):
107 try:
108 makedirs(cls.run_path)
109 except OSError:
110 pass
111 if platform.system() == "Windows":
112 controller_class = WindowsController
113 else:
114 controller_class = UnixController
115 home = realpath(controller_class.extract(package, cls.run_path))
116 cls.controller = controller_class(home, 1)
117 if NEO4J_USER is None:
118 cls.controller.create_user(cls.user, cls.password)
119 cls.controller.set_user_role(cls.user, "admin")
120 cls.controller.start()
121
122 @classmethod
123 def _stop_server(cls):
124 if cls.controller is not None:
125 cls.controller.stop()
126 if NEO4J_USER is None:
127 pass # TODO: delete user
128
129 def __enter__(self):
130 self.start()
131 return self
132
133 def __exit__(self, exc_type, exc_value, traceback):
134 self.stop()
135
136 @classmethod
137 def start(cls):
138 if is_listening(cls.bolt_address):
139 stderr.write("Using existing server listening on port {}\n".format(cls.bolt_port))
140 with GraphDatabase.driver(cls.bolt_uri, auth=cls.auth_token) as driver:
141 try:
142 with driver.session():
143 pass
144 except AuthError as error:
145 stderr.write("{}\n".format(error))
146 exit(1)
147 return
148 if cls.server_package is None:
149 raise RuntimeError("No Neo4j server available for %s" % cls.__name__)
150 stderr.write("Using server from package {}\n".format(cls.server_package))
151 package = copy_dist(cls.server_package, cls.local_server_package)
152 cls._start_server(package)
153
154 @classmethod
155 def stop(cls):
156 cls._stop_server()
+0
-9
test/requirements.txt less more
0 more-itertools<=5.0.0 ; python_version=="2.7"
1 boltkit==1.2.0
2 coverage
3 mock
4 pytest
5 pytest-benchmark
6 pytest-cov
7 teamcity-messages
8 tox
+0
-0
test/stub/__init__.py less more
(Empty file)
+0
-20
test/stub/scripts/bookmark_chain.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:0", "bookmark:1"]}
4 DISCARD_ALL
5 S: SUCCESS {}
6 SUCCESS {}
7 C: RUN "COMMIT" {}
8 DISCARD_ALL
9 S: SUCCESS {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
10 SUCCESS {}
11
12 C: RUN "BEGIN" {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
13 DISCARD_ALL
14 S: SUCCESS {}
15 SUCCESS {}
16 C: RUN "COMMIT" {}
17 DISCARD_ALL
18 S: SUCCESS {"bookmark": "bookmark:3", "bookmarks": ["bookmark:3"]}
19 SUCCESS {}
+0
-19
test/stub/scripts/bookmark_chain_with_autocommit.script less more
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: BEGIN {"bookmarks": ["bookmark:1"]}
6 S: SUCCESS {}
7 C: COMMIT
8 S: SUCCESS {"bookmark": "bookmark:2"}
9
10 C: RUN "RETURN 1" {} {"bookmarks": ["bookmark:2"]}
11 PULL_ALL
12 S: SUCCESS {"bookmark": "bookmark:3"}
13 SUCCESS {}
14
15 C: BEGIN {"bookmarks": ["bookmark:3"]}
16 S: SUCCESS {}
17 C: COMMIT
18 S: SUCCESS {"bookmark": "bookmark:4"}
+0
-9
test/stub/scripts/broken_router.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: FAILURE {"code": "Neo.DatabaseError.General.UnknownError", "message": "An unknown error occurred."}
6 IGNORED
7 C: RESET
8 S: SUCCESS {}
+0
-12
test/stub/scripts/connection_error_on_commit.script less more
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO RESET
3
4 C: BEGIN {}
5 RUN "CREATE (n {name:'Bob'})" {} {}
6 PULL_ALL
7 S: SUCCESS {}
8 SUCCESS {}
9 SUCCESS {}
10 C: COMMIT
11 S: <EXIT>
+0
-7
test/stub/scripts/create_a.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CREATE (a $x)" {"x": {"name": "Alice"}}
4 PULL_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
+0
-11
test/stub/scripts/database_unavailable.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2 !: AUTO DISCARD_ALL
3 !: AUTO RUN "ROLLBACK" {}
4 !: AUTO RUN "BEGIN" {}
5 !: AUTO RUN "COMMIT" {}
6
7 C: RUN "RETURN 1" {}
8 C: PULL_ALL
9 S: FAILURE {"code": "Neo.TransientError.General.DatabaseUnavailable", "message": "Database is busy doing store copy"}
10 S: IGNORED
+0
-5
test/stub/scripts/disconnect_after_init.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 S: SUCCESS {}
4 S: <EXIT>
+0
-6
test/stub/scripts/disconnect_on_pull_all.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN $x" {"x": 1}
4 PULL_ALL
5 S: <EXIT>
+0
-5
test/stub/scripts/disconnect_on_run.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN $x" {"x": 1}
4 S: <EXIT>
+0
-2
test/stub/scripts/empty.script less more
0 !: AUTO INIT
1 !: AUTO RESET
+0
-20
test/stub/scripts/error_in_tx.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "X" {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.ClientError.Statement.SyntaxError", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
15
16 C: RUN "ROLLBACK" {}
17 DISCARD_ALL
18 S: SUCCESS {}
19 SUCCESS {}
+0
-4
test/stub/scripts/fail_on_init.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 S: <EXIT>
+0
-11
test/stub/scripts/forbidden_on_read_only_database.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2 !: AUTO DISCARD_ALL
3 !: AUTO RUN "ROLLBACK" {}
4 !: AUTO RUN "BEGIN" {}
5 !: AUTO RUN "COMMIT" {}
6
7 C: RUN "CREATE (n {name:'Bob'})" {}
8 C: PULL_ALL
9 S: FAILURE {"code": "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase", "message": "Unable to write"}
10 S: IGNORED
+0
-9
test/stub/scripts/get_routing_table.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 S: SUCCESS {"server": "Neo4j/3.2.2"}
4 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
5 PULL_ALL
6 S: SUCCESS {"fields": ["ttl", "servers"]}
7 RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
8 SUCCESS {}
+0
-9
test/stub/scripts/get_routing_table_with_context.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 S: SUCCESS {"server": "Neo4j/3.2.3"}
4 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"name": "molly", "age": "1"}}
5 PULL_ALL
6 S: SUCCESS {"fields": ["ttl", "servers"]}
7 RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
8 SUCCESS {}
+0
-9
test/stub/scripts/non_router.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "Not a router"}
6 IGNORED
7 C: RESET
8 S: SUCCESS {}
+0
-11
test/stub/scripts/not_a_leader.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2 !: AUTO DISCARD_ALL
3 !: AUTO RUN "ROLLBACK" {}
4 !: AUTO RUN "BEGIN" {}
5 !: AUTO RUN "COMMIT" {}
6
7 C: RUN "CREATE (n {name:'Bob'})" {}
8 C: PULL_ALL
9 S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "Leader switched has happened"}
10 S: IGNORED
+0
-8
test/stub/scripts/return_1.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN $x" {"x": 1}
4 PULL_ALL
5 S: SUCCESS {"fields": ["x"]}
6 RECORD [1]
7 SUCCESS {}
+0
-26
test/stub/scripts/return_1_four_times.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN $x" {"x": 1}
4 PULL_ALL
5 S: SUCCESS {"fields": ["x"]}
6 RECORD [1]
7 SUCCESS {}
8
9 C: RUN "RETURN $x" {"x": 1}
10 PULL_ALL
11 S: SUCCESS {"fields": ["x"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: RUN "RETURN $x" {"x": 1}
16 PULL_ALL
17 S: SUCCESS {"fields": ["x"]}
18 RECORD [1]
19 SUCCESS {}
20
21 C: RUN "RETURN $x" {"x": 1}
22 PULL_ALL
23 S: SUCCESS {"fields": ["x"]}
24 RECORD [1]
25 SUCCESS {}
+0
-18
test/stub/scripts/return_1_in_tx.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "RETURN 1" {}
9 PULL_ALL
10 S: SUCCESS {"fields": ["1"]}
11 RECORD [1]
12 SUCCESS {}
13
14 C: RUN "COMMIT" {}
15 DISCARD_ALL
16 S: SUCCESS {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
17 SUCCESS {}
+0
-34
test/stub/scripts/return_1_in_tx_twice.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "RETURN 1" {}
9 PULL_ALL
10 S: SUCCESS {"fields": ["1"]}
11 RECORD [1]
12 SUCCESS {}
13
14 C: RUN "COMMIT" {}
15 DISCARD_ALL
16 S: SUCCESS {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
17 SUCCESS {}
18
19 C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
20 DISCARD_ALL
21 S: SUCCESS {"fields": []}
22 SUCCESS {}
23
24 C: RUN "RETURN 1" {}
25 PULL_ALL
26 S: SUCCESS {"fields": ["1"]}
27 RECORD [1]
28 SUCCESS {}
29
30 C: RUN "COMMIT" {}
31 DISCARD_ALL
32 S: SUCCESS {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
33 SUCCESS {}
+0
-14
test/stub/scripts/return_1_twice.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN $x" {"x": 1}
4 PULL_ALL
5 S: SUCCESS {"fields": ["x"]}
6 RECORD [1]
7 SUCCESS {}
8
9 C: RUN "RETURN $x" {"x": 1}
10 PULL_ALL
11 S: SUCCESS {"fields": ["x"]}
12 RECORD [1]
13 SUCCESS {}
+0
-24
test/stub/scripts/return_1_twice_in_tx.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "RETURN $x" {"x": 1}
9 PULL_ALL
10 S: SUCCESS {"fields": ["x"]}
11 RECORD [1]
12 SUCCESS {}
13
14 C: RUN "RETURN $x" {"x": 1}
15 PULL_ALL
16 S: SUCCESS {"fields": ["x"]}
17 RECORD [1]
18 SUCCESS {}
19
20 C: RUN "COMMIT" {}
21 DISCARD_ALL
22 S: SUCCESS {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
23 SUCCESS {}
+0
-18
test/stub/scripts/return_2_in_tx.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {"bookmark": "bookmark:1", "bookmarks": ["bookmark:1"]}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "RETURN 2" {}
9 PULL_ALL
10 S: SUCCESS {"fields": ["2"]}
11 RECORD [2]
12 SUCCESS {}
13
14 C: RUN "COMMIT" {}
15 DISCARD_ALL
16 S: SUCCESS {"bookmark": "bookmark:2", "bookmarks": ["bookmark:2"]}
17 SUCCESS {}
+0
-8
test/stub/scripts/router.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
7 SUCCESS {}
+0
-8
test/stub/scripts/router_no_readers.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":[]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
7 SUCCESS {}
+0
-8
test/stub/scripts/router_no_routers.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":[]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
7 SUCCESS {}
+0
-8
test/stub/scripts/router_no_writers.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":[]}]]
7 SUCCESS {}
+0
-8
test/stub/scripts/router_with_multiple_servers.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002"]},{"role":"READ","addresses":["127.0.0.1:9001","127.0.0.1:9003"]},{"role":"WRITE","addresses":["127.0.0.1:9004"]}]]
7 SUCCESS {}
+0
-8
test/stub/scripts/router_with_multiple_writers.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006","127.0.0.1:9007"]}]]
7 SUCCESS {}
+0
-7
test/stub/scripts/rude_reader.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "RETURN 1" {}
4 PULL_ALL
5 S: <EXIT>
6
+0
-7
test/stub/scripts/rude_router.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: <EXIT>
6
+0
-7
test/stub/scripts/silent_router.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {}}
4 PULL_ALL
5 S: SUCCESS {"fields": ["ttl", "servers"]}
6 SUCCESS {}
+0
-20
test/stub/scripts/user_canceled_tx.script.script less more
0 !: AUTO INIT
1 !: AUTO RESET
2
3 C: RUN "BEGIN" {}
4 DISCARD_ALL
5 S: SUCCESS {"fields": []}
6 SUCCESS {}
7
8 C: RUN "RETURN 1" {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.TransientError.Transaction.LockClientStopped", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
15
16 C: RUN "ROLLBACK" {}
17 DISCARD_ALL
18 S: SUCCESS {}
19 SUCCESS {}
+0
-185
test/stub/test_accesslevel.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j import GraphDatabase, CypherError, TransientError
22
23 from test.stub.tools import StubTestCase, StubCluster
24
25
26 class AccessLevelTestCase(StubTestCase):
27
28 def test_read_transaction(self):
29 with StubCluster({9001: "router.script", 9004: "return_1_in_tx.script"}):
30 uri = "bolt+routing://localhost:9001"
31 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
32 with driver.session() as session:
33
34 def unit_of_work(tx):
35 total = 0
36 for record in tx.run("RETURN 1"):
37 total += record[0]
38 return total
39
40 value = session.read_transaction(unit_of_work)
41 assert value == 1
42
43 def test_write_transaction(self):
44 with StubCluster({9001: "router.script", 9006: "return_1_in_tx.script"}):
45 uri = "bolt+routing://localhost:9001"
46 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
47 with driver.session() as session:
48
49 def unit_of_work(tx):
50 total = 0
51 for record in tx.run("RETURN 1"):
52 total += record[0]
53 return total
54
55 value = session.write_transaction(unit_of_work)
56 assert value == 1
57
58 def test_read_transaction_with_error(self):
59 with StubCluster({9001: "router.script", 9004: "error_in_tx.script"}):
60 uri = "bolt+routing://localhost:9001"
61 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
62 with driver.session() as session:
63
64 def unit_of_work(tx):
65 tx.run("X")
66
67 with self.assertRaises(CypherError):
68 _ = session.read_transaction(unit_of_work)
69
70 def test_write_transaction_with_error(self):
71 with StubCluster({9001: "router.script", 9006: "error_in_tx.script"}):
72 uri = "bolt+routing://localhost:9001"
73 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
74 with driver.session() as session:
75
76 def unit_of_work(tx):
77 tx.run("X")
78
79 with self.assertRaises(CypherError):
80 _ = session.write_transaction(unit_of_work)
81
82 def test_two_subsequent_read_transactions(self):
83 with StubCluster({9001: "router.script", 9004: "return_1_in_tx_twice.script"}):
84 uri = "bolt+routing://localhost:9001"
85 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
86 with driver.session() as session:
87
88 def unit_of_work(tx):
89 total = 0
90 for record in tx.run("RETURN 1"):
91 total += record[0]
92 return total
93
94 value = session.read_transaction(unit_of_work)
95 assert value == 1
96 value = session.read_transaction(unit_of_work)
97 assert value == 1
98
99 def test_two_subsequent_write_transactions(self):
100 with StubCluster({9001: "router.script", 9006: "return_1_in_tx_twice.script"}):
101 uri = "bolt+routing://localhost:9001"
102 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
103 with driver.session() as session:
104
105 def unit_of_work(tx):
106 total = 0
107 for record in tx.run("RETURN 1"):
108 total += record[0]
109 return total
110
111 value = session.write_transaction(unit_of_work)
112 assert value == 1
113 value = session.write_transaction(unit_of_work)
114 assert value == 1
115
116 def test_read_tx_then_write_tx(self):
117 with StubCluster({9001: "router.script", 9004: "return_1_in_tx.script", 9006: "return_2_in_tx.script"}):
118 uri = "bolt+routing://localhost:9001"
119 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
120 with driver.session() as session:
121
122 def unit_of_work_1(tx):
123 total = 0
124 for record in tx.run("RETURN 1"):
125 total += record[0]
126 return total
127
128 def unit_of_work_2(tx):
129 total = 0
130 for record in tx.run("RETURN 2"):
131 total += record[0]
132 return total
133
134 value = session.read_transaction(unit_of_work_1)
135 assert session.last_bookmark() == "bookmark:1"
136 assert value == 1
137 value = session.write_transaction(unit_of_work_2)
138 assert session.last_bookmark() == "bookmark:2"
139 assert value == 2
140
141 def test_write_tx_then_read_tx(self):
142 with StubCluster({9001: "router.script", 9004: "return_2_in_tx.script", 9006: "return_1_in_tx.script"}):
143 uri = "bolt+routing://localhost:9001"
144 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
145 with driver.session() as session:
146
147 def unit_of_work_1(tx):
148 total = 0
149 for record in tx.run("RETURN 1"):
150 total += record[0]
151 return total
152
153 def unit_of_work_2(tx):
154 total = 0
155 for record in tx.run("RETURN 2"):
156 total += record[0]
157 return total
158
159 value = session.write_transaction(unit_of_work_1)
160 assert value == 1
161 value = session.read_transaction(unit_of_work_2)
162 assert value == 2
163
164 def test_no_retry_read_on_user_canceled_tx(self):
165 with StubCluster({9001: "router.script", 9004: "user_canceled_tx.script.script"}):
166 uri = "bolt+routing://127.0.0.1:9001"
167 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
168 with driver.session() as session:
169 def unit_of_work(tx):
170 tx.run("RETURN 1")
171
172 with self.assertRaises(TransientError):
173 _ = session.read_transaction(unit_of_work)
174
175 def test_no_retry_write_on_user_canceled_tx(self):
176 with StubCluster({9001: "router.script", 9006: "user_canceled_tx.script.script"}):
177 uri = "bolt+routing://127.0.0.1:9001"
178 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
179 with driver.session() as session:
180 def unit_of_work(tx):
181 tx.run("RETURN 1")
182
183 with self.assertRaises(TransientError):
184 _ = session.write_transaction(unit_of_work)
+0
-74
test/stub/test_bookmarking.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j import GraphDatabase, READ_ACCESS
22
23 from test.stub.tools import StubTestCase, StubCluster
24
25
26 class BookmarkingTestCase(StubTestCase):
27
28 def test_should_be_no_bookmark_in_new_session(self):
29 with StubCluster({9001: "router.script"}):
30 uri = "bolt+routing://localhost:9001"
31 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
32 with driver.session() as session:
33 assert session.last_bookmark() is None
34
35 def test_should_be_able_to_set_bookmark(self):
36 with StubCluster({9001: "router.script"}):
37 uri = "bolt+routing://localhost:9001"
38 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
39 with driver.session(bookmark="X") as session:
40 assert session.next_bookmarks() == ("X",)
41
42 def test_should_be_able_to_set_multiple_bookmarks(self):
43 with StubCluster({9001: "router.script"}):
44 uri = "bolt+routing://localhost:9001"
45 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
46 with driver.session(bookmarks=[":1", ":2"]) as session:
47 assert session.next_bookmarks() == (":1", ":2")
48
49 def test_should_automatically_chain_bookmarks(self):
50 with StubCluster({9001: "router.script", 9004: "bookmark_chain.script"}):
51 uri = "bolt+routing://localhost:9001"
52 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
53 with driver.session(access_mode=READ_ACCESS, bookmarks=["bookmark:0", "bookmark:1"]) as session:
54 with session.begin_transaction():
55 pass
56 assert session.last_bookmark() == "bookmark:2"
57 with session.begin_transaction():
58 pass
59 assert session.last_bookmark() == "bookmark:3"
60
61 def test_autocommit_transaction_included_in_chain(self):
62 with StubCluster({9001: "router.script", 9004: "bookmark_chain_with_autocommit.script"}):
63 uri = "bolt+routing://localhost:9001"
64 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
65 with driver.session(access_mode=READ_ACCESS, bookmark="bookmark:1") as session:
66 with session.begin_transaction():
67 pass
68 assert session.last_bookmark() == "bookmark:2"
69 session.run("RETURN 1").consume()
70 assert session.last_bookmark() == "bookmark:3"
71 with session.begin_transaction():
72 pass
73 assert session.last_bookmark() == "bookmark:4"
+0
-64
test/stub/test_directdriver.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neobolt.exceptions import ServiceUnavailable
22
23 from neo4j import GraphDatabase, DirectDriver
24
25 from test.stub.tools import StubTestCase, StubCluster
26
27
28 class DirectDriverTestCase(StubTestCase):
29
30 def test_bolt_uri_constructs_direct_driver(self):
31 with StubCluster({9001: "empty.script"}):
32 uri = "bolt://127.0.0.1:9001"
33 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
34 assert isinstance(driver, DirectDriver)
35
36 def test_direct_disconnect_on_run(self):
37 with StubCluster({9001: "disconnect_on_run.script"}):
38 uri = "bolt://127.0.0.1:9001"
39 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
40 with self.assertRaises(ServiceUnavailable):
41 with driver.session() as session:
42 session.run("RETURN $x", {"x": 1}).consume()
43
44 def test_direct_disconnect_on_pull_all(self):
45 with StubCluster({9001: "disconnect_on_pull_all.script"}):
46 uri = "bolt://127.0.0.1:9001"
47 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
48 with self.assertRaises(ServiceUnavailable):
49 with driver.session() as session:
50 session.run("RETURN $x", {"x": 1}).consume()
51
52 def test_direct_should_reject_routing_context(self):
53 uri = "bolt://127.0.0.1:9001/?name=molly&age=1"
54 with self.assertRaises(ValueError):
55 GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False)
56
57 def test_direct_session_close_after_server_close(self):
58 with StubCluster({9001: "disconnect_after_init.script"}):
59 uri = "bolt://127.0.0.1:9001"
60 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, max_retry_time=0) as driver:
61 with driver.session() as session:
62 with self.assertRaises(ServiceUnavailable):
63 session.write_transaction(lambda tx: tx.run("CREATE (a:Item)"))
+0
-330
test/stub/test_routingdriver.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from pytest import mark
22
23 from neobolt.exceptions import ServiceUnavailable
24 from neobolt.routing import LeastConnectedLoadBalancingStrategy, RoundRobinLoadBalancingStrategy, \
25 LOAD_BALANCING_STRATEGY_ROUND_ROBIN, RoutingProtocolError
26
27 from neo4j.exceptions import ClientError
28 from neo4j import GraphDatabase, READ_ACCESS, WRITE_ACCESS, SessionExpired, RoutingDriver, TransientError
29
30 from test.stub.tools import StubTestCase, StubCluster
31
32
33 class RoutingDriverTestCase(StubTestCase):
34
35 def test_bolt_plus_routing_uri_constructs_routing_driver(self):
36 with StubCluster({9001: "router.script"}):
37 uri = "bolt+routing://127.0.0.1:9001"
38 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
39 assert isinstance(driver, RoutingDriver)
40
41 def test_cannot_discover_servers_on_non_router(self):
42 with StubCluster({9001: "non_router.script"}):
43 uri = "bolt+routing://127.0.0.1:9001"
44 with self.assertRaises(ServiceUnavailable):
45 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False):
46 pass
47
48 def test_cannot_discover_servers_on_silent_router(self):
49 with StubCluster({9001: "silent_router.script"}):
50 uri = "bolt+routing://127.0.0.1:9001"
51 with self.assertRaises(RoutingProtocolError):
52 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False):
53 pass
54
55 def test_should_discover_servers_on_driver_construction(self):
56 with StubCluster({9001: "router.script"}):
57 uri = "bolt+routing://127.0.0.1:9001"
58 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
59 table = driver._pool.routing_table
60 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002),
61 ('127.0.0.1', 9003)}
62 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
63 assert table.writers == {('127.0.0.1', 9006)}
64
65 def test_should_be_able_to_read(self):
66 with StubCluster({9001: "router.script", 9004: "return_1.script"}):
67 uri = "bolt+routing://127.0.0.1:9001"
68 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
69 with driver.session(access_mode=READ_ACCESS) as session:
70 result = session.run("RETURN $x", {"x": 1})
71 for record in result:
72 assert record["x"] == 1
73 assert result.summary().server.address == ('127.0.0.1', 9004)
74
75 def test_should_be_able_to_write(self):
76 with StubCluster({9001: "router.script", 9006: "create_a.script"}):
77 uri = "bolt+routing://127.0.0.1:9001"
78 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
79 with driver.session(access_mode=WRITE_ACCESS) as session:
80 result = session.run("CREATE (a $x)", {"x": {"name": "Alice"}})
81 assert not list(result)
82 assert result.summary().server.address == ('127.0.0.1', 9006)
83
84 def test_should_be_able_to_write_as_default(self):
85 with StubCluster({9001: "router.script", 9006: "create_a.script"}):
86 uri = "bolt+routing://127.0.0.1:9001"
87 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
88 with driver.session() as session:
89 result = session.run("CREATE (a $x)", {"x": {"name": "Alice"}})
90 assert not list(result)
91 assert result.summary().server.address == ('127.0.0.1', 9006)
92
93 def test_routing_disconnect_on_run(self):
94 with StubCluster({9001: "router.script", 9004: "disconnect_on_run.script"}):
95 uri = "bolt+routing://127.0.0.1:9001"
96 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
97 with self.assertRaises(SessionExpired):
98 with driver.session(access_mode=READ_ACCESS) as session:
99 session.run("RETURN $x", {"x": 1}).consume()
100
101 def test_routing_disconnect_on_pull_all(self):
102 with StubCluster({9001: "router.script", 9004: "disconnect_on_pull_all.script"}):
103 uri = "bolt+routing://127.0.0.1:9001"
104 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
105 with self.assertRaises(SessionExpired):
106 with driver.session(access_mode=READ_ACCESS) as session:
107 session.run("RETURN $x", {"x": 1}).consume()
108
109 def test_should_disconnect_after_fetching_autocommit_result(self):
110 with StubCluster({9001: "router.script", 9004: "return_1.script"}):
111 uri = "bolt+routing://127.0.0.1:9001"
112 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
113 with driver.session(access_mode=READ_ACCESS) as session:
114 result = session.run("RETURN $x", {"x": 1})
115 assert session._connection is not None
116 result.consume()
117 assert session._connection is None
118
119 def test_should_disconnect_after_explicit_commit(self):
120 with StubCluster({9001: "router.script", 9004: "return_1_twice_in_tx.script"}):
121 uri = "bolt+routing://127.0.0.1:9001"
122 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
123 with driver.session(access_mode=READ_ACCESS) as session:
124 with session.begin_transaction() as tx:
125 result = tx.run("RETURN $x", {"x": 1})
126 assert session._connection is not None
127 result.consume()
128 assert session._connection is not None
129 result = tx.run("RETURN $x", {"x": 1})
130 assert session._connection is not None
131 result.consume()
132 assert session._connection is not None
133 assert session._connection is None
134
135 def test_should_reconnect_for_new_query(self):
136 with StubCluster({9001: "router.script", 9004: "return_1_twice.script"}):
137 uri = "bolt+routing://127.0.0.1:9001"
138 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
139 with driver.session(access_mode=READ_ACCESS) as session:
140 result_1 = session.run("RETURN $x", {"x": 1})
141 assert session._connection is not None
142 result_1.consume()
143 assert session._connection is None
144 result_2 = session.run("RETURN $x", {"x": 1})
145 assert session._connection is not None
146 result_2.consume()
147 assert session._connection is None
148
149 def test_should_retain_connection_if_fetching_multiple_results(self):
150 with StubCluster({9001: "router.script", 9004: "return_1_twice.script"}):
151 uri = "bolt+routing://127.0.0.1:9001"
152 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
153 with driver.session(access_mode=READ_ACCESS) as session:
154 result_1 = session.run("RETURN $x", {"x": 1})
155 result_2 = session.run("RETURN $x", {"x": 1})
156 assert session._connection is not None
157 result_1.consume()
158 assert session._connection is not None
159 result_2.consume()
160 assert session._connection is None
161
162 def test_two_sessions_can_share_a_connection(self):
163 with StubCluster({9001: "router.script", 9004: "return_1_four_times.script"}):
164 uri = "bolt+routing://127.0.0.1:9001"
165 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
166 session_1 = driver.session(access_mode=READ_ACCESS)
167 session_2 = driver.session(access_mode=READ_ACCESS)
168
169 result_1a = session_1.run("RETURN $x", {"x": 1})
170 c = session_1._connection
171 result_1a.consume()
172
173 result_2a = session_2.run("RETURN $x", {"x": 1})
174 assert session_2._connection is c
175 result_2a.consume()
176
177 result_1b = session_1.run("RETURN $x", {"x": 1})
178 assert session_1._connection is c
179 result_1b.consume()
180
181 result_2b = session_2.run("RETURN $x", {"x": 1})
182 assert session_2._connection is c
183 result_2b.consume()
184
185 session_2.close()
186 session_1.close()
187
188 def test_should_call_get_routing_table_procedure(self):
189 with StubCluster({9001: "get_routing_table.script", 9002: "return_1.script"}):
190 uri = "bolt+routing://127.0.0.1:9001"
191 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
192 with driver.session(READ_ACCESS) as session:
193 result = session.run("RETURN $x", {"x": 1})
194 for record in result:
195 assert record["x"] == 1
196 assert result.summary().server.address == ('127.0.0.1', 9002)
197
198 def test_should_call_get_routing_table_with_context(self):
199 with StubCluster({9001: "get_routing_table_with_context.script", 9002: "return_1.script"}):
200 uri = "bolt+routing://127.0.0.1:9001/?name=molly&age=1"
201 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
202 with driver.session(READ_ACCESS) as session:
203 result = session.run("RETURN $x", {"x": 1})
204 for record in result:
205 assert record["x"] == 1
206 assert result.summary().server.address == ('127.0.0.1', 9002)
207
208 def test_should_serve_read_when_missing_writer(self):
209 with StubCluster({9001: "router_no_writers.script", 9005: "return_1.script"}):
210 uri = "bolt+routing://127.0.0.1:9001"
211 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
212 with driver.session(READ_ACCESS) as session:
213 result = session.run("RETURN $x", {"x": 1})
214 for record in result:
215 assert record["x"] == 1
216 assert result.summary().server.address == ('127.0.0.1', 9005)
217
218 def test_should_error_when_missing_reader(self):
219 with StubCluster({9001: "router_no_readers.script"}):
220 uri = "bolt+routing://127.0.0.1:9001"
221 with self.assertRaises(RoutingProtocolError):
222 GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False)
223
224 def test_default_load_balancing_strategy_is_least_connected(self):
225 from neobolt.routing import RoutingConnectionPool
226 with StubCluster({9001: "router.script"}):
227 uri = "bolt+routing://127.0.0.1:9001"
228 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
229 self.assertIsInstance(driver, RoutingDriver)
230 self.assertIsInstance(driver._pool, RoutingConnectionPool)
231 self.assertIsInstance(driver._pool.load_balancing_strategy, LeastConnectedLoadBalancingStrategy)
232
233 def test_can_select_round_robin_load_balancing_strategy(self):
234 from neobolt.routing import RoutingConnectionPool
235 with StubCluster({9001: "router.script"}):
236 uri = "bolt+routing://127.0.0.1:9001"
237 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False,
238 load_balancing_strategy=LOAD_BALANCING_STRATEGY_ROUND_ROBIN) as driver:
239 self.assertIsInstance(driver, RoutingDriver)
240 self.assertIsInstance(driver._pool, RoutingConnectionPool)
241 self.assertIsInstance(driver._pool.load_balancing_strategy, RoundRobinLoadBalancingStrategy)
242
243 def test_no_other_load_balancing_strategies_are_available(self):
244 uri = "bolt+routing://127.0.0.1:9001"
245 with self.assertRaises(ValueError):
246 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, load_balancing_strategy=-1):
247 pass
248
249 @mark.skip
250 def test_forgets_address_on_not_a_leader_error(self):
251 with StubCluster({9001: "router.script", 9006: "not_a_leader.script"}):
252 uri = "bolt+routing://127.0.0.1:9001"
253 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
254 with driver.session(WRITE_ACCESS) as session:
255 with self.assertRaises(ClientError):
256 _ = session.run("CREATE (n {name:'Bob'})")
257
258 pool = driver._pool
259 table = pool.routing_table
260
261 # address might still have connections in the pool, failed instance just can't serve writes
262 assert ('127.0.0.1', 9006) in pool.connections
263 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
264 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
265 # writer 127.0.0.1:9006 should've been forgotten because of an error
266 assert len(table.writers) == 0
267
268 @mark.skip
269 def test_forgets_address_on_forbidden_on_read_only_database_error(self):
270 with StubCluster({9001: "router.script", 9006: "forbidden_on_read_only_database.script"}):
271 uri = "bolt+routing://127.0.0.1:9001"
272 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
273 with driver.session(WRITE_ACCESS) as session:
274 with self.assertRaises(ClientError):
275 _ = session.run("CREATE (n {name:'Bob'})")
276
277 pool = driver._pool
278 table = pool.routing_table
279
280 # address might still have connections in the pool, failed instance just can't serve writes
281 assert ('127.0.0.1', 9006) in pool.connections
282 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
283 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
284 # writer 127.0.0.1:9006 should've been forgotten because of an error
285 assert len(table.writers) == 0
286
287 @mark.skip
288 def test_forgets_address_on_service_unavailable_error(self):
289 with StubCluster({9001: "router.script", 9004: "rude_reader.script"}):
290 uri = "bolt+routing://127.0.0.1:9001"
291 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
292 with driver.session(READ_ACCESS) as session:
293 with self.assertRaises(SessionExpired):
294 _ = session.run("RETURN 1")
295
296 pool = driver._pool
297 table = pool.routing_table
298
299 # address should have connections in the pool but be inactive, it has failed
300 assert ('127.0.0.1', 9004) in pool.connections
301 conns = pool.connections[('127.0.0.1', 9004)]
302 conn = conns[0]
303 assert conn._closed == True
304 assert conn.in_use == True
305 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
306 # reader 127.0.0.1:9004 should've been forgotten because of an error
307 assert table.readers == {('127.0.0.1', 9005)}
308 assert table.writers == {('127.0.0.1', 9006)}
309
310 assert conn.in_use == False
311
312 @mark.skip
313 def test_forgets_address_on_database_unavailable_error(self):
314 with StubCluster({9001: "router.script", 9004: "database_unavailable.script"}):
315 uri = "bolt+routing://127.0.0.1:9001"
316 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False) as driver:
317 with driver.session(READ_ACCESS) as session:
318 with self.assertRaises(TransientError):
319 _ = session.run("RETURN 1")
320
321 pool = driver._pool
322 table = pool.routing_table
323
324 # address should not have connections in the pool, it has failed
325 assert ('127.0.0.1', 9004) not in pool.connections
326 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
327 # reader 127.0.0.1:9004 should've been forgotten because of an error
328 assert table.readers == {('127.0.0.1', 9005)}
329 assert table.writers == {('127.0.0.1', 9006)}
+0
-51
test/stub/test_transactions.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neobolt.exceptions import ServiceUnavailable
22
23 from neo4j import GraphDatabase
24
25 from test.stub.tools import StubTestCase, StubCluster
26
27
28 class TransactionTestCase(StubTestCase):
29
30 @staticmethod
31 def create_bob(tx):
32 tx.run("CREATE (n {name:'Bob'})").data()
33
34 def test_connection_error_on_explicit_commit(self):
35 with StubCluster({9001: "connection_error_on_commit.script"}):
36 uri = "bolt://127.0.0.1:9001"
37 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, max_retry_time=0) as driver:
38 with driver.session() as session:
39 tx = session.begin_transaction()
40 tx.run("CREATE (n {name:'Bob'})").data()
41 with self.assertRaises(ServiceUnavailable):
42 tx.commit()
43
44 def test_connection_error_on_commit(self):
45 with StubCluster({9001: "connection_error_on_commit.script"}):
46 uri = "bolt://127.0.0.1:9001"
47 with GraphDatabase.driver(uri, auth=self.auth_token, encrypted=False, max_retry_time=0) as driver:
48 with driver.session() as session:
49 with self.assertRaises(ServiceUnavailable):
50 session.write_transaction(self.create_bob)
+0
-70
test/stub/tools.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from os.path import dirname, join as path_join
22 from subprocess import check_call
23 from threading import Thread
24 from time import sleep
25 from unittest import TestCase
26
27
28 class StubTestCase(TestCase):
29 """ Base class for test cases that integrate with a server.
30 """
31
32 bolt_uri = "bolt://localhost:7687"
33 bolt_routing_uri = "bolt+routing://localhost:7687"
34
35 user = "test"
36 password = "test"
37 auth_token = (user, password)
38
39
40 class StubServer(Thread):
41
42 def __init__(self, port, script):
43 super(StubServer, self).__init__()
44 self.port = port
45 self.script = path_join(dirname(__file__), "scripts", script)
46
47 def run(self):
48 check_call(["boltstub", str(self.port), self.script])
49
50
51 class StubCluster(object):
52
53 def __init__(self, servers):
54 self.servers = {port: StubServer(port, script) for port, script in dict(servers).items()}
55
56 def __enter__(self):
57 self.start()
58
59 def __exit__(self, exc_type, exc_value, traceback):
60 self.wait()
61
62 def start(self):
63 for port, server in self.servers.items():
64 server.start()
65 sleep(0.5)
66
67 def wait(self):
68 for port, server in self.servers.items():
69 server.join()
+0
-0
test/unit/__init__.py less more
(Empty file)
+0
-81
test/unit/test_api.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22 from uuid import uuid4
23
24 from neo4j import fix_parameters
25
26
27 def dehydrated_value(value):
28 return fix_parameters({"_": value}, 1, supports_bytes=True)["_"]
29
30
31 class ValueDehydrationTestCase(TestCase):
32
33 def test_should_allow_none(self):
34 self.assertIsNone(dehydrated_value(None))
35
36 def test_should_allow_boolean(self):
37 self.assertTrue(dehydrated_value(True))
38 self.assertFalse(dehydrated_value(False))
39
40 def test_should_allow_integer(self):
41 self.assertEqual(dehydrated_value(0), 0)
42 self.assertEqual(dehydrated_value(0x7F), 0x7F)
43 self.assertEqual(dehydrated_value(0x7FFF), 0x7FFF)
44 self.assertEqual(dehydrated_value(0x7FFFFFFF), 0x7FFFFFFF)
45 self.assertEqual(dehydrated_value(0x7FFFFFFFFFFFFFFF), 0x7FFFFFFFFFFFFFFF)
46
47 def test_should_disallow_oversized_integer(self):
48 with self.assertRaises(ValueError):
49 dehydrated_value(0x10000000000000000)
50 with self.assertRaises(ValueError):
51 dehydrated_value(-0x10000000000000000)
52
53 def test_should_allow_float(self):
54 self.assertEqual(dehydrated_value(0.0), 0.0)
55 self.assertEqual(dehydrated_value(3.1415926), 3.1415926)
56
57 def test_should_allow_string(self):
58 self.assertEqual(dehydrated_value(u""), u"")
59 self.assertEqual(dehydrated_value(u"hello, world"), u"hello, world")
60
61 def test_should_allow_bytes(self):
62 self.assertEqual(dehydrated_value(bytearray()), bytearray())
63 self.assertEqual(dehydrated_value(bytearray([1, 2, 3])), bytearray([1, 2, 3]))
64
65 def test_should_allow_list(self):
66 self.assertEqual(dehydrated_value([]), [])
67 self.assertEqual(dehydrated_value([1, 2, 3]), [1, 2, 3])
68
69 def test_should_allow_dict(self):
70 self.assertEqual(dehydrated_value({}), {})
71 self.assertEqual(dehydrated_value({u"one": 1, u"two": 1, u"three": 1}), {u"one": 1, u"two": 1, u"three": 1})
72 self.assertEqual(dehydrated_value(
73 {u"list": [1, 2, 3, [4, 5, 6]], u"dict": {u"a": 1, u"b": 2}}),
74 {u"list": [1, 2, 3, [4, 5, 6]], u"dict": {u"a": 1, u"b": 2}})
75
76 def test_should_disallow_object(self):
77 with self.assertRaises(TypeError):
78 dehydrated_value(object())
79 with self.assertRaises(TypeError):
80 dehydrated_value(uuid4())
+0
-143
test/unit/test_record.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j import Record
24
25
26 class RecordTestCase(TestCase):
27
28 def test_record_equality(self):
29 record1 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
30 record2 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
31 record3 = Record(zip(["name", "empire"], ["Stefan", "Das Deutschland"]))
32 assert record1 == record2
33 assert record1 != record3
34 assert record2 != record3
35
36 def test_record_hashing(self):
37 record1 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
38 record2 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
39 record3 = Record(zip(["name", "empire"], ["Stefan", "Das Deutschland"]))
40 assert hash(record1) == hash(record2)
41 assert hash(record1) != hash(record3)
42 assert hash(record2) != hash(record3)
43
44 def test_record_iter(self):
45 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
46 assert list(a_record.__iter__()) == ["Nigel", "The British Empire"]
47
48 def test_record_as_dict(self):
49 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
50 assert dict(a_record) == {"name": "Nigel", "empire": "The British Empire"}
51
52 def test_record_as_list(self):
53 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
54 assert list(a_record) == ["Nigel", "The British Empire"]
55
56 def test_record_len(self):
57 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
58 assert len(a_record) == 2
59
60 def test_record_repr(self):
61 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
62 assert repr(a_record) == "<Record name='Nigel' empire='The British Empire'>"
63
64 def test_record_data(self):
65 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
66 self.assertEqual(r.data(), {"name": "Alice", "age": 33, "married": True})
67 self.assertEqual(r.data("name"), {"name": "Alice"})
68 self.assertEqual(r.data("age", "name"), {"age": 33, "name": "Alice"})
69 self.assertEqual(r.data("age", "name", "shoe size"), {"age": 33, "name": "Alice", "shoe size": None})
70 self.assertEqual(r.data(0, "name"), {"name": "Alice"})
71 self.assertEqual(r.data(0), {"name": "Alice"})
72 self.assertEqual(r.data(1, 0), {"age": 33, "name": "Alice"})
73 with self.assertRaises(IndexError):
74 _ = r.data(1, 0, 999)
75
76 def test_record_keys(self):
77 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
78 self.assertEqual(r.keys(), ["name", "age", "married"])
79
80 def test_record_values(self):
81 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
82 self.assertEqual(r.values(), ["Alice", 33, True])
83 self.assertEqual(r.values("name"), ["Alice"])
84 self.assertEqual(r.values("age", "name"), [33, "Alice"])
85 self.assertEqual(r.values("age", "name", "shoe size"), [33, "Alice", None])
86 self.assertEqual(r.values(0, "name"), ["Alice", "Alice"])
87 self.assertEqual(r.values(0), ["Alice"])
88 self.assertEqual(r.values(1, 0), [33, "Alice"])
89 with self.assertRaises(IndexError):
90 _ = r.values(1, 0, 999)
91
92 def test_record_items(self):
93 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
94 self.assertEqual(r.items(), [("name", "Alice"), ("age", 33), ("married", True)])
95 self.assertEqual(r.items("name"), [("name", "Alice")])
96 self.assertEqual(r.items("age", "name"), [("age", 33), ("name", "Alice")])
97 self.assertEqual(r.items("age", "name", "shoe size"), [("age", 33), ("name", "Alice"), ("shoe size", None)])
98 self.assertEqual(r.items(0, "name"), [("name", "Alice"), ("name", "Alice")])
99 self.assertEqual(r.items(0), [("name", "Alice")])
100 self.assertEqual(r.items(1, 0), [("age", 33), ("name", "Alice")])
101 with self.assertRaises(IndexError):
102 _ = r.items(1, 0, 999)
103
104 def test_record_index(self):
105 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
106 self.assertEqual(r.index("name"), 0)
107 self.assertEqual(r.index("age"), 1)
108 self.assertEqual(r.index("married"), 2)
109 with self.assertRaises(KeyError):
110 _ = r.index("shoe size")
111 self.assertEqual(r.index(0), 0)
112 self.assertEqual(r.index(1), 1)
113 self.assertEqual(r.index(2), 2)
114 with self.assertRaises(IndexError):
115 _ = r.index(3)
116 with self.assertRaises(TypeError):
117 _ = r.index(None)
118
119 def test_record_value(self):
120 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
121 self.assertEqual(r.value(), "Alice")
122 self.assertEqual(r.value("name"), "Alice")
123 self.assertEqual(r.value("age"), 33)
124 self.assertEqual(r.value("married"), True)
125 self.assertEqual(r.value("shoe size"), None)
126 self.assertEqual(r.value("shoe size", 6), 6)
127 self.assertEqual(r.value(0), "Alice")
128 self.assertEqual(r.value(1), 33)
129 self.assertEqual(r.value(2), True)
130 self.assertEqual(r.value(3), None)
131 self.assertEqual(r.value(3, 6), 6)
132 with self.assertRaises(TypeError):
133 _ = r.value(None)
134
135 def test_record_contains(self):
136 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
137 self.assertTrue("Alice" in r)
138 self.assertTrue(33 in r)
139 self.assertTrue(True in r)
140 self.assertFalse(7.5 in r)
141 with self.assertRaises(TypeError):
142 _ = r.index(None)
+0
-58
test/unit/test_security.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22 from neo4j import kerberos_auth, basic_auth, custom_auth
23
24
25 class AuthTokenTestCase(TestCase):
26
27 def test_should_generate_kerberos_auth_token_correctly(self):
28 auth = kerberos_auth("I am a base64 service ticket")
29 assert auth.scheme == "kerberos"
30 assert auth.principal == ""
31 assert auth.credentials == "I am a base64 service ticket"
32 assert not auth.realm
33 assert not hasattr(auth, "parameters")
34
35 def test_should_generate_basic_auth_without_realm_correctly(self):
36 auth = basic_auth("molly", "meoooow")
37 assert auth.scheme == "basic"
38 assert auth.principal == "molly"
39 assert auth.credentials == "meoooow"
40 assert not auth.realm
41 assert not hasattr(auth, "parameters")
42
43 def test_should_generate_base_auth_with_realm_correctly(self):
44 auth = basic_auth("molly", "meoooow", "cat_cafe")
45 assert auth.scheme == "basic"
46 assert auth.principal == "molly"
47 assert auth.credentials == "meoooow"
48 assert auth.realm == "cat_cafe"
49 assert not hasattr(auth, "parameters")
50
51 def test_should_generate_custom_auth_correctly(self):
52 auth = custom_auth("molly", "meoooow", "cat_cafe", "cat", age="1", color="white")
53 assert auth.scheme == "cat"
54 assert auth.principal == "molly"
55 assert auth.credentials == "meoooow"
56 assert auth.realm == "cat_cafe"
57 assert auth.parameters == {"age": "1", "color": "white"}
+0
-206
test/unit/test_types.py less more
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2019 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neobolt.packstream import Structure
24 from neo4j.types import PackStreamHydrator
25 from neo4j.types.graph import Node, Path, Graph
26
27
28 class NodeTestCase(TestCase):
29
30 def test_can_create_node(self):
31 g = Graph()
32 alice = g.put_node(1, {"Person"}, {"name": "Alice", "age": 33})
33 self.assertEqual(alice.labels, {"Person"})
34 self.assertEqual(set(alice.keys()), {"name", "age"})
35 self.assertEqual(set(alice.values()), {"Alice", 33})
36 self.assertEqual(set(alice.items()), {("name", "Alice"), ("age", 33)})
37 self.assertEqual(alice.get("name"), "Alice")
38 self.assertEqual(alice.get("age"), 33)
39 self.assertTrue(repr(alice))
40 self.assertEqual(len(alice), 2)
41 self.assertEqual(alice["name"], "Alice")
42 self.assertEqual(alice["age"], 33)
43 self.assertIn("name", alice)
44 self.assertIn("age", alice)
45 self.assertEqual(set(iter(alice)), {"name", "age"})
46
47 def test_null_properties(self):
48 g = Graph()
49 stuff = g.put_node(1, (), good=["puppies", "kittens"], bad=None)
50 self.assertEqual(set(stuff.keys()), {"good"})
51 self.assertEqual(stuff.get("good"), ["puppies", "kittens"])
52 self.assertIsNone(stuff.get("bad"))
53 self.assertEqual(len(stuff), 1)
54 self.assertEqual(stuff["good"], ["puppies", "kittens"])
55 self.assertIsNone(stuff["bad"])
56 self.assertIn("good", stuff)
57 self.assertNotIn("bad", stuff)
58
59 def test_node_equality(self):
60 g = Graph()
61 node_1 = Node(g, 1234)
62 node_2 = Node(g, 1234)
63 node_3 = Node(g, 5678)
64 self.assertEqual(node_1, node_2)
65 self.assertNotEqual(node_1, node_3)
66 self.assertNotEqual(node_1, "this is not a node")
67
68 def test_node_hashing(self):
69 g = Graph()
70 node_1 = Node(g, 1234)
71 node_2 = Node(g, 1234)
72 node_3 = Node(g, 5678)
73 self.assertEqual(hash(node_1), hash(node_2))
74 self.assertNotEqual(hash(node_1), hash(node_3))
75
76
77 class RelationshipTestCase(TestCase):
78
79 def test_can_create_relationship(self):
80 g = Graph()
81 alice = g.put_node(1, {"Person"}, name="Alice", age=33)
82 bob = g.put_node(2, {"Person"}, name="Bob", age=44)
83 alice_knows_bob = g.put_relationship(1, alice, bob, "KNOWS", since=1999)
84 self.assertEqual(alice_knows_bob.start_node, alice)
85 self.assertEqual(alice_knows_bob.type, "KNOWS")
86 self.assertEqual(alice_knows_bob.end_node, bob)
87 self.assertEqual(set(alice_knows_bob.keys()), {"since"})
88 self.assertEqual(set(alice_knows_bob.values()), {1999})
89 self.assertEqual(set(alice_knows_bob.items()), {("since", 1999)})
90 self.assertEqual(alice_knows_bob.get("since"), 1999)
91 self.assertTrue(repr(alice_knows_bob))
92
93
94 class PathTestCase(TestCase):
95
96 def test_can_create_path(self):
97 g = Graph()
98 alice = g.put_node(1, {"Person"}, {"name": "Alice", "age": 33})
99 bob = g.put_node(2, {"Person"}, {"name": "Bob", "age": 44})
100 carol = g.put_node(3, {"Person"}, {"name": "Carol", "age": 55})
101 alice_knows_bob = g.put_relationship(1, alice, bob, "KNOWS", {"since": 1999})
102 carol_dislikes_bob = g.put_relationship(2, carol, bob, "DISLIKES")
103 path = Path(alice, alice_knows_bob, carol_dislikes_bob)
104 self.assertEqual(path.start_node, alice)
105 self.assertEqual(path.end_node, carol)
106 self.assertEqual(path.nodes, (alice, bob, carol))
107 self.assertEqual(path.relationships, (alice_knows_bob, carol_dislikes_bob))
108 self.assertEqual(list(path), [alice_knows_bob, carol_dislikes_bob])
109 self.assertTrue(repr(path))
110
111 def test_can_hydrate_path(self):
112 from neo4j.types.graph import hydrate_path, _put_unbound_relationship
113 g = Graph()
114 alice = g.put_node(1, {"Person"}, {"name": "Alice", "age": 33})
115 bob = g.put_node(2, {"Person"}, {"name": "Bob", "age": 44})
116 carol = g.put_node(3, {"Person"}, {"name": "Carol", "age": 55})
117 r = [_put_unbound_relationship(g, 1, "KNOWS", {"since": 1999}),
118 _put_unbound_relationship(g, 2, "DISLIKES")]
119 path = hydrate_path([alice, bob, carol], r, [1, 1, -2, 2])
120 self.assertEqual(path.start_node, alice)
121 self.assertEqual(path.end_node, carol)
122 self.assertEqual(path.nodes, (alice, bob, carol))
123 expected_alice_knows_bob = g.put_relationship(1, alice, bob, "KNOWS", {"since": 1999})
124 expected_carol_dislikes_bob = g.put_relationship(2, carol, bob, "DISLIKES")
125 self.assertEqual(path.relationships, (expected_alice_knows_bob, expected_carol_dislikes_bob))
126 self.assertEqual(list(path), [expected_alice_knows_bob, expected_carol_dislikes_bob])
127 self.assertTrue(repr(path))
128
129 def test_path_equality(self):
130 g = Graph()
131 alice = g.put_node(1, {"Person"}, {"name": "Alice", "age": 33})
132 bob = g.put_node(2, {"Person"}, {"name": "Bob", "age": 44})
133 carol = g.put_node(3, {"Person"}, {"name": "Carol", "age": 55})
134 alice_knows_bob = g.put_relationship(1, alice, bob, "KNOWS", {"since": 1999})
135 carol_dislikes_bob = g.put_relationship(2, carol, bob, "DISLIKES")
136 path_1 = Path(alice, alice_knows_bob, carol_dislikes_bob)
137 path_2 = Path(alice, alice_knows_bob, carol_dislikes_bob)
138 self.assertEqual(path_1, path_2)
139 self.assertNotEqual(path_1, "this is not a path")
140
141 def test_path_hashing(self):
142 g = Graph()
143 alice = g.put_node(1, {"Person"}, {"name": "Alice", "age": 33})
144 bob = g.put_node(2, {"Person"}, {"name": "Bob", "age": 44})
145 carol = g.put_node(3, {"Person"}, {"name": "Carol", "age": 55})
146 alice_knows_bob = g.put_relationship(1, alice, bob, "KNOWS", {"since": 1999})
147 carol_dislikes_bob = g.put_relationship(2, carol, bob, "DISLIKES")
148 path_1 = Path(alice, alice_knows_bob, carol_dislikes_bob)
149 path_2 = Path(alice, alice_knows_bob, carol_dislikes_bob)
150 self.assertEqual(hash(path_1), hash(path_2))
151
152
153 class HydrationTestCase(TestCase):
154
155 def setUp(self):
156 self.hydrant = PackStreamHydrator(1)
157
158 def test_can_hydrate_node_structure(self):
159 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
160 alice, = self.hydrant.hydrate([struct])
161 self.assertEqual(alice.id, 123)
162 self.assertEqual(alice.labels, {"Person"})
163 self.assertEqual(set(alice.keys()), {"name"})
164 self.assertEqual(alice.get("name"), "Alice")
165
166 def test_hydrating_unknown_structure_returns_same(self):
167 struct = Structure(b'?', "foo")
168 mystery, = self.hydrant.hydrate([struct])
169 self.assertEqual(mystery, struct)
170
171 def test_can_hydrate_in_list(self):
172 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
173 alice_in_list, = self.hydrant.hydrate([[struct]])
174 self.assertIsInstance(alice_in_list, list)
175 alice, = alice_in_list
176 self.assertEqual(alice.id, 123)
177 self.assertEqual(alice.labels, {"Person"})
178 self.assertEqual(set(alice.keys()), {"name"})
179 self.assertEqual(alice.get("name"), "Alice")
180
181 def test_can_hydrate_in_dict(self):
182 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
183 alice_in_dict, = self.hydrant.hydrate([{"foo": struct}])
184 self.assertIsInstance(alice_in_dict, dict)
185 alice = alice_in_dict["foo"]
186 self.assertEqual(alice.id, 123)
187 self.assertEqual(alice.labels, {"Person"})
188 self.assertEqual(set(alice.keys()), {"name"})
189 self.assertEqual(alice.get("name"), "Alice")
190
191
192 class TemporalHydrationTestCase(TestCase):
193
194 def setUp(self):
195 self.hydrant = PackStreamHydrator(2)
196
197 def test_can_hydrate_date_time_structure(self):
198 struct = Structure(b'd', 1539344261, 474716862)
199 dt, = self.hydrant.hydrate([struct])
200 self.assertEqual(dt.year, 2018)
201 self.assertEqual(dt.month, 10)
202 self.assertEqual(dt.day, 12)
203 self.assertEqual(dt.hour, 11)
204 self.assertEqual(dt.minute, 37)
205 self.assertEqual(dt.second, 41.474716862)
(New empty file)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from math import ceil
22 from os import getenv
23 from os.path import dirname, join
24 from threading import RLock
25
26 import pytest
27 import urllib
28
29 from neo4j import (
30 GraphDatabase,
31 )
32 from neo4j.exceptions import ServiceUnavailable
33 from neo4j._exceptions import BoltHandshakeError
34 from neo4j.io import Bolt
35
36 # import logging
37 # log = logging.getLogger("neo4j")
38 #
39 # from neo4j.debug import watch
40 # watch("neo4j")
41
42 NEO4J_RELEASES = getenv("NEO4J_RELEASES", "snapshot-enterprise 3.5-enterprise").split()
43 NEO4J_HOST = "localhost"
44 NEO4J_PORTS = {
45 "bolt": 17601,
46 "http": 17401,
47 "https": 17301,
48 }
49 NEO4J_CORES = 3
50 NEO4J_REPLICAS = 2
51 NEO4J_USER = "neo4j"
52 NEO4J_PASSWORD = "password"
53 NEO4J_AUTH = (NEO4J_USER, NEO4J_PASSWORD)
54 NEO4J_LOCK = RLock()
55 NEO4J_SERVICE = None
56 NEO4J_DEBUG = getenv("NEO4J_DEBUG", "")
57
58 # TODO: re-enable when Docker is feasible
59 # from boltkit.server import Neo4jService, Neo4jClusterService
60
61
62 class Machine(object):
63
64 def __init__(self, address):
65 self.address = address
66
67
68 class Neo4jService(object):
69
70 run = d = join(dirname(__file__), ".run")
71
72 edition = "enterprise"
73
74 def __init__(self, name=None, image=None, auth=None,
75 n_cores=None, n_replicas=None,
76 bolt_port=None, http_port=None, debug_port=None,
77 debug_suspend=None, dir_spec=None, config=None):
78 from boltkit.legacy.controller import _install, create_controller
79 assert image.endswith("-enterprise")
80 release = image[:-11]
81 if release == "snapshot":
82 release = "4.0"
83 self.home = _install("enterprise", release, self.run, verbose=1)
84 self.auth = NEO4J_AUTH
85 self.controller = create_controller(self.home)
86 self.controller.set_initial_password(NEO4J_PASSWORD)
87 self.info = None
88
89 def start(self, timeout=None):
90 self.info = self.controller.start(timeout=timeout)
91
92 def stop(self, timeout=None):
93 from shutil import rmtree
94 self.controller.stop()
95 rmtree(self.home)
96
97 def machines(self):
98 parsed = self.info.bolt_uri
99 return [Machine((parsed.hostname, parsed.port or 7687))]
100
101 @property
102 def addresses(self):
103 return [machine.address for machine in self.machines()]
104
105 def cores(self):
106 return self.machines()
107
108
109 class Neo4jClusterService(Neo4jService):
110
111 @classmethod
112 def _port_range(cls, base_port, count):
113 if base_port is None:
114 return [None] * count
115 else:
116 return range(base_port, base_port + count)
117
118
119 def _ping_range(port_range):
120 count = 0
121 for port in port_range:
122 count += 1 if Bolt.ping((NEO4J_HOST, port)) else 0
123 return count
124
125
126 def get_existing_service():
127 core_port_range = Neo4jClusterService._port_range(NEO4J_PORTS["bolt"], NEO4J_CORES)
128 replica_port_range = Neo4jClusterService._port_range(ceil(core_port_range.stop / 10) * 10 + 1,
129 NEO4J_REPLICAS)
130 core_count = _ping_range(core_port_range)
131 replica_count = _ping_range(replica_port_range)
132 if core_count == 0 and replica_count == 0:
133 return None
134 elif core_count == NEO4J_CORES and replica_count == NEO4J_REPLICAS:
135 return ExistingService(NEO4J_HOST, core_port_range, replica_port_range)
136 else:
137 raise OSError("Some ports required by the test service are already in use")
138
139
140 class ExistingMachine:
141
142 def __init__(self, address):
143 self.address = address
144
145
146 class ExistingService:
147
148 def __init__(self, host, core_port_range, replica_port_range):
149 self._cores = [ExistingMachine((host, port)) for port in core_port_range]
150 self._replicas = [ExistingMachine((host, port)) for port in replica_port_range]
151
152 @property
153 def addresses(self):
154 return [machine.address for machine in self.cores()]
155
156 @property
157 def auth(self):
158 return NEO4J_AUTH
159
160 def cores(self):
161 return self._cores
162
163 def replicas(self):
164 return self._replicas
165
166 def start(self, timeout=None):
167 pass
168
169 def stop(self, timeout=None):
170 pass
171
172
173 existing_service = get_existing_service()
174
175 if existing_service:
176 NEO4J_RELEASES = ["existing"]
177
178
179 @pytest.fixture(scope="session", params=NEO4J_RELEASES)
180 def service(request):
181 global NEO4J_SERVICE
182 if NEO4J_DEBUG:
183 from neo4j.debug import watch
184 watch("neo4j", "boltkit")
185 with NEO4J_LOCK:
186 assert NEO4J_SERVICE is None
187 NEO4J_SERVICE = existing_service
188 if existing_service:
189 NEO4J_SERVICE = existing_service
190 else:
191 try:
192 NEO4J_SERVICE = Neo4jService(auth=NEO4J_AUTH, image=request.param, n_cores=NEO4J_CORES, n_replicas=NEO4J_REPLICAS)
193 NEO4J_SERVICE.start(timeout=300)
194 except urllib.error.HTTPError as error:
195 # pytest.skip(str(error))
196 pytest.xfail(str(error) + " " + request.param)
197 yield NEO4J_SERVICE
198 if NEO4J_SERVICE is not None:
199 NEO4J_SERVICE.stop(timeout=300)
200 NEO4J_SERVICE = None
201
202
203 @pytest.fixture(scope="session")
204 def addresses(service):
205 try:
206 machines = service.cores()
207 except AttributeError:
208 machines = list(service.machines.values())
209 return [machine.address for machine in machines]
210
211
212 # @fixture(scope="session")
213 # def readonly_addresses(service):
214 # try:
215 # machines = service.replicas()
216 # except AttributeError:
217 # machines = []
218 # return [machine.address for machine in machines]
219
220
221 @pytest.fixture(scope="session")
222 def address(addresses):
223 try:
224 return addresses[0]
225 except IndexError:
226 return None
227
228
229 # @fixture(scope="session")
230 # def readonly_address(readonly_addresses):
231 # try:
232 # return readonly_addresses[0]
233 # except IndexError:
234 # return None
235
236
237 @pytest.fixture(scope="session")
238 def targets(addresses):
239 return " ".join("{}:{}".format(address[0], address[1]) for address in addresses)
240
241
242 # @fixture(scope="session")
243 # def readonly_targets(addresses):
244 # return " ".join("{}:{}".format(address[0], address[1]) for address in readonly_addresses)
245
246
247 @pytest.fixture(scope="session")
248 def target(address):
249 return "{}:{}".format(address[0], address[1])
250
251
252 # @fixture(scope="session")
253 # def readonly_target(readonly_address):
254 # if readonly_address:
255 # return "{}:{}".format(readonly_address[0], readonly_address[1])
256 # else:
257 # return None
258
259
260 @pytest.fixture(scope="session")
261 def bolt_uri(service, target):
262 return "bolt://" + target
263
264
265 @pytest.fixture(scope="session")
266 def neo4j_uri(service, target):
267 return "neo4j://" + target
268
269
270 @pytest.fixture(scope="session")
271 def uri(bolt_uri):
272 return bolt_uri
273
274
275 # @fixture(scope="session")
276 # def readonly_bolt_uri(service, readonly_target):
277 # if readonly_target:
278 # return "bolt://" + readonly_target
279 # else:
280 # return None
281
282
283 @pytest.fixture(scope="session")
284 def auth():
285 return NEO4J_AUTH
286
287
288 @pytest.fixture(scope="session")
289 def bolt_driver(target, auth):
290 try:
291 driver = GraphDatabase.bolt_driver(target, auth=auth)
292 try:
293 yield driver
294 finally:
295 driver.close()
296 except ServiceUnavailable as error:
297 if isinstance(error.__cause__, BoltHandshakeError):
298 pytest.skip(error.args[0])
299
300
301 @pytest.fixture(scope="session")
302 def neo4j_driver(target, auth):
303 try:
304 driver = GraphDatabase.neo4j_driver(target, auth=auth)
305 except ServiceUnavailable as error:
306 if isinstance(error.__cause__, BoltHandshakeError):
307 pytest.skip(error.args[0])
308 elif error.args[0] == "Server does not support routing":
309 pytest.skip(error.args[0])
310 else:
311 raise
312 else:
313 try:
314 yield driver
315 finally:
316 driver.close()
317
318
319 @pytest.fixture(scope="session")
320 def driver(neo4j_driver):
321 return neo4j_driver
322
323
324 @pytest.fixture()
325 def session(bolt_driver):
326 session = bolt_driver.session()
327 try:
328 yield session
329 finally:
330 session.close()
331
332
333 @pytest.fixture()
334 def protocol_version(session):
335 result = session.run("RETURN 1")
336 yield session._connection.server_info.protocol_version
337 result.consume()
338
339
340 @pytest.fixture()
341 def cypher_eval(bolt_driver):
342
343 def run_and_rollback(tx, cypher, **parameters):
344 result = tx.run(cypher, **parameters)
345 value = result.single().value()
346 tx._success = False # This is not a recommended pattern
347 return value
348
349 def f(cypher, **parameters):
350 with bolt_driver.session() as session:
351 return session.write_transaction(run_and_rollback, cypher, **parameters)
352
353 return f
354
355
356 def pytest_sessionfinish(session, exitstatus):
357 """ Called after the entire session to ensure Neo4j is shut down.
358 """
359 global NEO4J_SERVICE
360 with NEO4J_LOCK:
361 if NEO4J_SERVICE is not None:
362 NEO4J_SERVICE.stop(timeout=300)
363 NEO4J_SERVICE = None
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # List of example tags used in the drivers manual version 4.0
22
23
24 # hello-world, hello-world-import - Example 1.6. Hello World
25 # driver-lifecycle, driver-lifecycle-import - Example 2.1. The driver lifecycle
26 # custom-resolver, custom-resolver-import - Example 2.2. Custom Address Resolver
27 # basic-auth, basic-auth-import - Example 2.5. Basic authentication
28 # kerberos-auth, kerberos-auth-import - Example 2.6. Kerberos authentication
29 # custom-auth, custom-auth-import - Example 2.7. Custom authentication
30 # config-connection-pool, config-connection-pool-import - Example 2.8. Configure connection pool
31 # config-connection-timeout, config-connection-timeout-import - Example 2.9. Configure connection timeout
32 # config-unencrypted, config-unencrypted-import - Example 2.10. Unencrypted configuration
33 # config-max-retry-time, config-max-retry-time-import - Example 2.11. Configure maximum transaction retry time
34 # config-trust, config-trust-import - Example 2.12. Configure trusted certificates
35 # pass-bookmarks, pass-bookmarks-import - Example 3.1. Pass bookmarks
36 # read-write-transaction - Example 3.2. Read-write transaction
37 # database-selection, database-selection-import - Example 3.3. Database selection on session creation
38 # Hard coded (session) - Example 4.1. Session construction and closure
39 # transaction-function, transaction-function-import - Example 4.2. Transaction function
40 # autocommit-transaction, autocommit-transaction-import - Example 4.3. Simple auto-commit transactions
41 # result-consume - Example 4.4. Consuming results
42 # result-retain - Example 4.5. Retain results for further processing
43 # - Example 4.6. Asynchronous transaction functions
44 # - Example 4.7. Asynchronous auto-commit transactions
45 # - Example 4.8. Asynchronous consuming results
46 # - Example 4.9. Reactive transaction functions
47 # - Example 4.10.Reactive auto-commit transactions
48 # - Example 4.11.Reactive consuming results
49
50
51 class DriverSetupExample:
52
53 driver = None
54
55 def close(self):
56 if self.driver:
57 self.driver.close()
58
59 @classmethod
60 def test(cls, *args, **kwargs):
61 example = cls(*args, **kwargs)
62 try:
63 with example.driver.session() as session:
64 assert session.run("RETURN 1").single().value() == 1
65 finally:
66 example.close()
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::autocommit-transaction-import[]
22 from neo4j import Query
23 # end::autocommit-transaction-import[]
24
25
26 # python -m pytest tests/integration/examples/test_autocommit_transaction_example.py -s -v
27
28 class AutocommitTransactionExample:
29
30 def __init__(self, driver):
31 self.driver = driver
32
33 # tag::autocommit-transaction[]
34 def add_person(self, name):
35 with self.driver.session() as session:
36 session.run("CREATE (a:Person {name: $name})", name=name)
37
38 # Alternative implementation, with a one second timeout
39 def add_person_within_a_second(self, name):
40 with self.driver.session() as session:
41 session.run(Query("CREATE (a:Person {name: $name})", timeout=1.0), name=name)
42 # end::autocommit-transaction[]
43
44
45 def test_example(driver):
46 eg = AutocommitTransactionExample(driver)
47 with eg.driver.session() as session:
48 session.run("MATCH (_) DETACH DELETE _")
49 eg.add_person("Alice")
50 n = session.run("MATCH (a:Person) RETURN count(a)").single().value()
51 assert n == 1
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::basic-auth-import[]
24 from neo4j import GraphDatabase
25 # end::basic-auth-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29 from tests.integration.examples import DriverSetupExample
30
31
32 # python -m pytest tests/integration/examples/test_basic_auth_example.py -s -v
33
34 class BasicAuthExample(DriverSetupExample):
35
36 # tag::basic-auth[]
37 def __init__(self, uri, user, password):
38 self.driver = GraphDatabase.driver(uri, auth=(user, password))
39 # end::basic-auth[]
40
41
42 def test_example(uri, auth):
43 try:
44 BasicAuthExample.test(uri, user=auth[0], password=auth[1])
45 except ServiceUnavailable as error:
46 if isinstance(error.__cause__, BoltHandshakeError):
47 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import ServiceUnavailable
24 from neo4j._exceptions import BoltHandshakeError
25
26 # tag::config-connection-pool-import[]
27 from neo4j import GraphDatabase
28 # end::config-connection-pool-import[]
29
30 from tests.integration.examples import DriverSetupExample
31
32
33 # python -m pytest tests/integration/examples/test_config_connection_pool_example.py -s -v
34
35 class ConfigConnectionPoolExample(DriverSetupExample):
36
37 # tag::config-connection-pool[]
38 def __init__(self, uri, auth):
39 self.driver = GraphDatabase.driver(uri, auth=auth,
40 max_connection_lifetime=30 * 60,
41 max_connection_pool_size=50,
42 connection_acquisition_timeout=2 * 60)
43 # end::config-connection-pool[]
44
45
46 def test(uri, auth):
47 try:
48 ConfigConnectionPoolExample.test(uri, auth)
49 except ServiceUnavailable as error:
50 if isinstance(error.__cause__, BoltHandshakeError):
51 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import ServiceUnavailable
24 from neo4j._exceptions import BoltHandshakeError
25
26 # tag::config-connection-timeout-import[]
27 from neo4j import GraphDatabase
28 # end::config-connection-timeout-import[]
29
30 from tests.integration.examples import DriverSetupExample
31
32
33 # python -m pytest tests/integration/examples/test_config_connection_timeout_example.py -s -v
34
35 class ConfigConnectionTimeoutExample(DriverSetupExample):
36
37 # tag::config-connection-timeout[]
38 def __init__(self, uri, auth):
39 self.driver = GraphDatabase.driver(uri, auth=auth, connection_timeout=15)
40 # end::config-connection-timeout[]
41
42
43 def test(uri, auth):
44 try:
45 ConfigConnectionTimeoutExample.test(uri, auth)
46 except ServiceUnavailable as error:
47 if isinstance(error.__cause__, BoltHandshakeError):
48 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::config-max-retry-time-import[]
24 from neo4j import GraphDatabase
25 # end::config-max-retry-time-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29 from tests.integration.examples import DriverSetupExample
30
31
32 # python -m pytest tests/integration/examples/test_config_max_retry_time_example.py -s -v
33
34 class ConfigMaxRetryTimeExample(DriverSetupExample):
35
36 # tag::config-max-retry-time[]
37 def __init__(self, uri, auth):
38 self.driver = GraphDatabase.driver(uri, auth=auth, max_transaction_retry_time=15)
39 # end::config-max-retry-time[]
40
41
42 def test(uri, auth):
43 try:
44 ConfigMaxRetryTimeExample.test(uri, auth)
45 except ServiceUnavailable as error:
46 if isinstance(error.__cause__, BoltHandshakeError):
47 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::config-secure-import[]
24 from neo4j import GraphDatabase, TRUST_SYSTEM_CA_SIGNED_CERTIFICATES
25 # end::config-secure-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29 from tests.integration.examples import DriverSetupExample
30
31
32 # python -m pytest tests/integration/examples/test_config_secure_example.py -s -v
33
34 class ConfigSecureExample(DriverSetupExample):
35
36 # tag::config-secure[]
37 def __init__(self, uri, auth):
38 self.driver = GraphDatabase.driver(uri, auth=auth, encrypted=True, trust=TRUST_SYSTEM_CA_SIGNED_CERTIFICATES)
39 # end::config-secure[]
40
41
42 def test_example(uri, auth):
43 pytest.skip("re-enable when we can test with encrypted=True on Docker")
44 try:
45 ConfigSecureExample.test(uri, auth)
46 except ServiceUnavailable as error:
47 if isinstance(error.__cause__, BoltHandshakeError):
48 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::config-trust-import[]
24 from neo4j import (
25 GraphDatabase,
26 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
27 TRUST_ALL_CERTIFICATES,
28 )
29 # end::config-trust-import[]
30
31 from tests.integration.examples import DriverSetupExample
32
33
34 class ConfigTrustExample(DriverSetupExample):
35
36 # tag::config-trust[]
37 def __init__(self, uri, auth):
38 self.driver = GraphDatabase.driver(uri, auth=auth, encrypted=True, trust=TRUST_ALL_CERTIFICATES)
39 # end::config-trust[]
40
41
42 def test_example(uri, auth):
43 # TODO: re-enable when we can test with encrypted=True on Docker
44 # ConfigTrustExample.test(uri, auth)
45 pytest.skip("re-enable when we can test with encrypted=True on Docker")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::config-unencrypted-import[]
24 from neo4j import GraphDatabase
25 # end::config-unencrypted-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29
30 from tests.integration.examples import DriverSetupExample
31
32
33 # python -m pytest tests/integration/examples/test_config_unencrypted_example.py -s -v
34
35 class ConfigUnencryptedExample(DriverSetupExample):
36
37 # tag::config-unencrypted[]
38 def __init__(self, uri, auth):
39 self.driver = GraphDatabase.driver(uri, auth=auth, encrypted=False)
40 # end::config-unencrypted[]
41
42
43 def test_example(uri, auth):
44 try:
45 ConfigUnencryptedExample.test(uri, auth)
46 except ServiceUnavailable as error:
47 if isinstance(error.__cause__, BoltHandshakeError):
48 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::custom-auth-import[]
24 from neo4j import (
25 GraphDatabase,
26 custom_auth,
27 )
28 # end::custom-auth-import[]
29
30 from neo4j.exceptions import ServiceUnavailable
31 from neo4j._exceptions import BoltHandshakeError
32 from tests.integration.examples import DriverSetupExample
33
34
35 # python -m pytest tests/integration/examples/test_custom_auth_example.py -s -v
36
37 class CustomAuthExample(DriverSetupExample):
38
39 # tag::custom-auth[]
40 def __init__(self, uri, principal, credentials, realm, scheme, **parameters):
41 auth = custom_auth(principal, credentials, realm, scheme, **parameters)
42 self.driver = GraphDatabase.driver(uri, auth=auth)
43 # end::custom-auth[]
44
45
46 def test_example(uri, auth):
47 try:
48 CustomAuthExample.test(uri, auth[0], auth[1], None, "basic", key="value")
49 except ServiceUnavailable as error:
50 if isinstance(error.__cause__, BoltHandshakeError):
51 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::custom-resolver-import[]
24 from neo4j import (
25 GraphDatabase,
26 WRITE_ACCESS,
27 )
28 # end::custom-resolver-import[]
29
30 from neo4j.exceptions import ServiceUnavailable
31 from neo4j._exceptions import BoltHandshakeError
32
33
34 # python -m pytest tests/integration/examples/test_custom_resolver_example.py -s -v
35
36 # tag::custom-resolver[]
37 def create_driver(uri, user, password):
38
39 def resolver(address):
40 host, port = address
41 if host == "x.example.com":
42 yield "a.example.com", port
43 yield "b.example.com", port
44 yield "c.example.com", port
45 else:
46 yield host, port
47
48 return GraphDatabase.driver(uri, auth=(user, password), resolver=resolver)
49
50
51 def add_person(name):
52 driver = create_driver("neo4j://x.example.com", user="neo4j", password="password")
53 session = driver.session(default_access_mode=WRITE_ACCESS)
54 session.run("CREATE (a:Person {name: $name})", {"name", name})
55 session.close()
56 driver.close()
57 # end::custom-resolver[]
58
59
60 def test_example(uri, auth):
61 try:
62 add_person("testing_resolver")
63 except ServiceUnavailable as error:
64 if isinstance(error.__cause__, BoltHandshakeError):
65 pytest.skip(error.args[0])
66 except ValueError as error:
67 assert error.args[0] == "Cannot resolve address a.example.com:7687"
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from contextlib import redirect_stdout
22 from io import StringIO
23
24 # tag::cypher-error-import[]
25 from neo4j.exceptions import ClientError
26 # end::cypher-error-import[]
27
28
29 class Neo4jErrorExample:
30
31 def __init__(self, driver):
32 self.driver = driver
33
34 # tag::cypher-error[]
35 def get_employee_number(self, name):
36 with self.driver.session() as session:
37 try:
38 session.read_transaction(self.select_employee, name)
39 except ClientError as error:
40 print(error.message)
41 return -1
42
43 @staticmethod
44 def select_employee(tx, name):
45 result = tx.run("SELECT * FROM Employees WHERE name = $name", name=name)
46 return result.single()["employee_number"]
47 # end::cypher-error[]
48
49
50 def test_example(bolt_driver):
51 s = StringIO()
52 with redirect_stdout(s):
53 example = Neo4jErrorExample(bolt_driver)
54 example.get_employee_number('Alice')
55 assert s.getvalue().startswith("Invalid input")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from contextlib import redirect_stdout
24 from io import StringIO
25
26
27 from neo4j import GraphDatabase
28 # tag::database-selection-import[]
29 from neo4j import READ_ACCESS
30 # end::database-selection-import[]
31
32 from neo4j.exceptions import ServiceUnavailable
33 from neo4j._exceptions import BoltHandshakeError
34
35
36 # python -m pytest tests/integration/examples/test_database_selection_example.py -s -v
37
38
39 class DatabaseSelectionExample:
40
41 def __init__(self, uri, user, password):
42 self.driver = GraphDatabase.driver(uri, auth=(user, password))
43 with self.driver.session(database="system") as session:
44 session.run("DROP DATABASE example IF EXISTS").consume()
45 session.run("CREATE DATABASE example").consume()
46
47 def close(self):
48 with self.driver.session(database="system") as session:
49 session.run("DROP DATABASE example").consume()
50 self.driver.close()
51
52 def run_example_code(self):
53
54 driver = self.driver
55 # tag::database-selection[]
56 with driver.session(database="example") as session:
57 session.run("CREATE (a:Greeting {message: 'Hello, Example-Database'}) RETURN a").consume()
58
59 with driver.session(database="example", default_access_mode=READ_ACCESS) as session:
60 message = session.run("MATCH (a:Greeting) RETURN a.message as msg").single().get("msg")
61 print(message)
62 # end::database-selection[]
63
64
65 def test_database_selection_example(neo4j_uri, auth):
66 try:
67 s = StringIO()
68 with redirect_stdout(s):
69 example = DatabaseSelectionExample(neo4j_uri, auth[0], auth[1])
70 example.run_example_code()
71 example.close()
72 assert s.getvalue().startswith("Hello, Example-Database")
73 except ServiceUnavailable as error:
74 if isinstance(error.__cause__, BoltHandshakeError):
75 pytest.skip(error.args[0])
76 if error.args[0] == "Server does not support routing":
77 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
78 pytest.skip(error.args[0])
79 except ConfigurationError as error:
80 pytest.skip(error.args[0])
81
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from contextlib import redirect_stdout
24 from io import StringIO
25
26 # tag::driver-introduction-example-import[]
27 from neo4j import GraphDatabase
28 import logging
29 from neo4j.exceptions import ServiceUnavailable
30 # end::driver-introduction-example-import[]
31
32 from neo4j._exceptions import BoltHandshakeError
33
34
35 # python -m pytest tests/integration/examples/test_aura_example.py -s -v
36
37 # tag::driver-introduction-example[]
38 class App:
39
40 def __init__(self, uri, user, password):
41 self.driver = GraphDatabase.driver(uri, auth=(user, password))
42
43 def close(self):
44 # Don't forget to close the driver connection when you are finished with it
45 self.driver.close()
46
47 def create_friendship(self, person1_name, person2_name):
48 with self.driver.session() as session:
49 # Write transactions allow the driver to handle retries and transient errors
50 result = session.write_transaction(
51 self._create_and_return_friendship, person1_name, person2_name)
52 for row in result:
53 print("Created friendship between: {p1}, {p2}".format(p1=row['p1'], p2=row['p2']))
54
55 @staticmethod
56 def _create_and_return_friendship(tx, person1_name, person2_name):
57 # To learn more about the Cypher syntax, see https://neo4j.com/docs/cypher-manual/current/
58 # The Reference Card is also a good resource for keywords https://neo4j.com/docs/cypher-refcard/current/
59 query = (
60 "CREATE (p1:Person { name: $person1_name }) "
61 "CREATE (p2:Person { name: $person2_name }) "
62 "CREATE (p1)-[:KNOWS]->(p2) "
63 "RETURN p1, p2"
64 )
65 result = tx.run(query, person1_name=person1_name, person2_name=person2_name)
66 try:
67 return [{"p1": row["p1"]["name"], "p2": row["p2"]["name"]}
68 for row in result]
69 # Capture any errors along with the query and data for traceability
70 except ServiceUnavailable as exception:
71 logging.error("{query} raised an error: \n {exception}".format(
72 query=query, exception=exception))
73 raise
74
75 def find_person(self, person_name):
76 with self.driver.session() as session:
77 result = session.read_transaction(self._find_and_return_person, person_name)
78 for row in result:
79 print("Found person: {row}".format(row=row))
80
81 @staticmethod
82 def _find_and_return_person(tx, person_name):
83 query = (
84 "MATCH (p:Person) "
85 "WHERE p.name = $person_name "
86 "RETURN p.name AS name"
87 )
88 result = tx.run(query, person_name=person_name)
89 return [row["name"] for row in result]
90
91
92 if __name__ == "__main__":
93 # Aura queries use an encrypted connection using the "neo4j+s" URI scheme
94 bolt_url = "%%BOLT_URL_PLACEHOLDER%%"
95 user = "<Username for Neo4j Aura database>"
96 password = "<Password for Neo4j Aura database>"
97 app = App(bolt_url, user, password)
98 app.create_friendship("Alice", "David")
99 app.find_person("Alice")
100 app.close()
101 # end::driver-introduction-example[]
102
103
104 def test_driver_introduction_example(uri, auth):
105 try:
106 s = StringIO()
107 with redirect_stdout(s):
108 app = App(uri, auth[0], auth[1])
109 app.create_friendship("Alice", "David")
110 app.find_person("Alice")
111 app.close()
112
113 # assert s.getvalue().startswith("Found person: Alice")
114 except ServiceUnavailable as error:
115 if isinstance(error.__cause__, BoltHandshakeError):
116 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::driver-lifecycle-import[]
24 from neo4j import GraphDatabase
25 # end::driver-lifecycle-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29
30
31 # python -m pytest tests/integration/examples/test_driver_lifecycle_example.py -s -v
32
33 # tag::driver-lifecycle[]
34 class DriverLifecycleExample:
35 def __init__(self, uri, auth):
36 self.driver = GraphDatabase.driver(uri, auth=auth)
37
38 def close(self):
39 self.driver.close()
40 # end::driver-lifecycle[]
41
42
43 def test_example(uri, auth):
44 try:
45 eg = DriverLifecycleExample(uri, auth)
46 eg.close()
47 except ServiceUnavailable as error:
48 if isinstance(error.__cause__, BoltHandshakeError):
49 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from contextlib import redirect_stdout
24 from io import StringIO
25
26 # tag::hello-world-import[]
27 from neo4j import GraphDatabase
28 # end::hello-world-import[]
29
30 from neo4j.exceptions import ServiceUnavailable
31 from neo4j._exceptions import BoltHandshakeError
32
33
34 # python -m pytest tests/integration/examples/test_hello_world_example.py -s -v
35
36 # tag::hello-world[]
37 class HelloWorldExample:
38
39 def __init__(self, uri, user, password):
40 self.driver = GraphDatabase.driver(uri, auth=(user, password))
41
42 def close(self):
43 self.driver.close()
44
45 def print_greeting(self, message):
46 with self.driver.session() as session:
47 greeting = session.write_transaction(self._create_and_return_greeting, message)
48 print(greeting)
49
50 @staticmethod
51 def _create_and_return_greeting(tx, message):
52 result = tx.run("CREATE (a:Greeting) "
53 "SET a.message = $message "
54 "RETURN a.message + ', from node ' + id(a)", message=message)
55 return result.single()[0]
56
57
58 if __name__ == "__main__":
59 greeter = HelloWorldExample("bolt://localhost:7687", "neo4j", "password")
60 greeter.print_greeting("hello, world")
61 greeter.close()
62 # end::hello-world[]
63
64 # tag::hello-world-output[]
65 # hello, world, from node 1234
66 # end::hello-world-output[]
67
68
69 def test_hello_world_example(uri, auth):
70 try:
71 s = StringIO()
72 with redirect_stdout(s):
73 example = HelloWorldExample(uri, auth[0], auth[1])
74 example.print_greeting("hello, world")
75 example.close()
76
77 assert s.getvalue().startswith("hello, world, from node ")
78 except ServiceUnavailable as error:
79 if isinstance(error.__cause__, BoltHandshakeError):
80 pytest.skip(error.args[0])
81
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::kerberos-auth-import[]
24 from neo4j import (
25 GraphDatabase,
26 kerberos_auth,
27 )
28 # end::kerberos-auth-import[]
29
30 from tests.integration.examples import DriverSetupExample
31
32
33 # python -m pytest tests/integration/examples/test_kerberos_auth_example.py -s -v
34
35 class KerberosAuthExample(DriverSetupExample):
36 # tag::kerberos-auth[]
37 def __init__(self, uri, ticket):
38 self._driver = GraphDatabase.driver(uri, auth=kerberos_auth(ticket))
39 # end::kerberos-auth[]
40
41
42 def test_example():
43 pytest.skip("Currently no way to test Kerberos auth")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::pass-bookmarks-import[]
24 from neo4j import GraphDatabase
25 # end::pass-bookmarks-import[]
26
27 from neo4j.exceptions import ServiceUnavailable
28 from neo4j._exceptions import BoltHandshakeError
29
30
31 # python -m pytest tests/integration/examples/test_pass_bookmarks_example.py -s -v
32
33 # tag::pass-bookmarks[]
34 class BookmarksExample:
35
36 def __init__(self, uri, auth):
37 self.driver = GraphDatabase.driver(uri, auth=auth)
38
39 def close(self):
40 self.driver.close()
41
42 # Create a person node.
43 @classmethod
44 def create_person(cls, tx, name):
45 tx.run("CREATE (:Person {name: $name})", name=name)
46
47 # Create an employment relationship to a pre-existing company node.
48 # This relies on the person first having been created.
49 @classmethod
50 def employ(cls, tx, person_name, company_name):
51 tx.run("MATCH (person:Person {name: $person_name}) "
52 "MATCH (company:Company {name: $company_name}) "
53 "CREATE (person)-[:WORKS_FOR]->(company)",
54 person_name=person_name, company_name=company_name)
55
56 # Create a friendship between two people.
57 @classmethod
58 def create_friendship(cls, tx, name_a, name_b):
59 tx.run("MATCH (a:Person {name: $name_a}) "
60 "MATCH (b:Person {name: $name_b}) "
61 "MERGE (a)-[:KNOWS]->(b)",
62 name_a=name_a, name_b=name_b)
63
64 # Match and display all friendships.
65 @classmethod
66 def print_friendships(cls, tx):
67 result = tx.run("MATCH (a)-[:KNOWS]->(b) RETURN a.name, b.name")
68 for record in result:
69 print("{} knows {}".format(record["a.name"], record["b.name"]))
70
71 def main(self):
72 saved_bookmarks = [] # To collect the session bookmarks
73
74 # Create the first person and employment relationship.
75 with self.driver.session() as session_a:
76 session_a.write_transaction(self.create_person, "Alice")
77 session_a.write_transaction(self.employ, "Alice", "Wayne Enterprises")
78 saved_bookmarks.append(session_a.last_bookmark())
79
80 # Create the second person and employment relationship.
81 with self.driver.session() as session_b:
82 session_b.write_transaction(self.create_person, "Bob")
83 session_b.write_transaction(self.employ, "Bob", "LexCorp")
84 saved_bookmarks.append(session_b.last_bookmark())
85
86 # Create a friendship between the two people created above.
87 with self.driver.session(bookmarks=saved_bookmarks) as session_c:
88 session_c.write_transaction(self.create_friendship, "Alice", "Bob")
89 session_c.read_transaction(self.print_friendships)
90
91 # end::pass-bookmarks[]
92
93
94 def test(uri, auth):
95 try:
96 eg = BookmarksExample(uri, auth)
97 with eg.driver.session() as session:
98 session.run("MATCH (_) DETACH DELETE _")
99 eg.main()
100 with eg.driver.session() as session:
101 session.run("MATCH (_) DETACH DELETE _")
102 except ServiceUnavailable as error:
103 if isinstance(error.__cause__, BoltHandshakeError):
104 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::read-write-transaction-import[]
22 # end::read-write-transaction-import[]
23
24 # python -m pytest tests/integration/examples/test_read_write_transaction_example.py -s -v
25
26 def read_write_transaction_example(driver):
27 with driver.session() as session:
28 session.run("MATCH (_) DETACH DELETE _")
29
30 # tag::read-write-transaction[]
31 def create_person_node(tx, name):
32 tx.run("CREATE (a:Person {name: $name})", name=name)
33
34 def match_person_node(tx, name):
35 result = tx.run("MATCH (a:Person {name: $name}) RETURN count(a)", name=name)
36 return result.single()[0]
37
38 def add_person(name):
39 with driver.session() as session:
40 session.write_transaction(create_person_node, name)
41 persons = session.read_transaction(match_person_node, name)
42 return persons
43 # end::read-write-transaction[]
44
45 result = add_person("Alice")
46 result = add_person("Alice")
47
48 with driver.session() as session:
49 session.run("MATCH (_) DETACH DELETE _")
50
51 return result
52
53
54 def test_example(driver):
55 result = read_write_transaction_example(driver)
56 assert result == 2
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::result-consume-import[]
22 # end::result-consume-import[]
23
24 # python -m pytest tests/integration/examples/test_result_consume_example.py -s -v
25
26 def result_consume_example(driver):
27 with driver.session() as session:
28 session.run("MATCH (_) DETACH DELETE _").consume()
29
30 with driver.session() as session:
31 session.run("CREATE (a:Person {name: $name}) RETURN a", name="Alice").single().value()
32 session.run("CREATE (a:Person {name: $name}) RETURN a", name="Bob").single().value()
33
34 # tag::result-consume[]
35 def match_person_nodes(tx):
36 result = tx.run("MATCH (a:Person) RETURN a.name ORDER BY a.name")
37 return [record["a.name"] for record in result]
38
39 with driver.session() as session:
40 people = session.read_transaction(match_person_nodes)
41 # end::result-consume[]
42
43 with driver.session() as session:
44 session.run("MATCH (_) DETACH DELETE _").consume()
45
46 return people
47
48
49 def test_example(driver):
50 people = result_consume_example(driver)
51 assert people == ['Alice', 'Bob']
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::result-retain-import[]
22 # end::result-retain-import[]
23
24
25 # python -m pytest tests/integration/examples/test_result_retain_example.py -s -v
26
27 def result_retain_example(driver):
28 with driver.session() as session:
29 session.run("MATCH (_) DETACH DELETE _").consume()
30
31 with driver.session() as session:
32 session.run("CREATE (a:Person {name: $name}) RETURN a", name="Alice").single().value()
33 session.run("CREATE (a:Person {name: $name}) RETURN a", name="Bob").single().value()
34
35 # tag::result-retain[]
36 def add_employee_to_company(tx, person, company_name):
37 tx.run("MATCH (emp:Person {name: $person_name}) "
38 "MERGE (com:Company {name: $company_name}) "
39 "MERGE (emp)-[:WORKS_FOR]->(com)",
40 person_name=person["name"], company_name=company_name)
41 return 1
42
43 def match_person_nodes(tx):
44 return list(tx.run("MATCH (a:Person) RETURN a.name AS name"))
45
46 def add_employees(company_name):
47 employees = 0
48 with driver.session() as session:
49 persons = session.read_transaction(match_person_nodes)
50
51 for person in persons:
52 employees += session.write_transaction(add_employee_to_company, person, company_name)
53
54 return employees
55 # end::result-retain[]
56
57 employees = add_employees(company_name="Neo4j")
58
59 def count_employees(company_name):
60 with driver.session() as session:
61 head_count = session.run(
62 "MATCH (emp:Person)-[:WORKS_FOR]->(com:Company) "
63 "WHERE com.name = $company_name "
64 "RETURN count(emp)",
65 company_name=company_name).single().value()
66
67 return head_count
68
69 head_count = count_employees(company_name="Neo4j")
70
71 with driver.session() as session:
72 session.run("MATCH (_) DETACH DELETE _").consume()
73
74 return employees, head_count
75
76
77 def test_example(driver):
78 employees, head_count = result_retain_example(driver)
79 assert employees == 2
80 assert head_count == 2
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # tag::service-unavailable-import[]
24 from neo4j.exceptions import ServiceUnavailable
25 # end::service-unavailable-import[]
26
27
28 def service_unavailable_example(driver):
29 with driver.session() as session:
30 session.run("MATCH (_) DETACH DELETE _")
31
32 # tag::service-unavailable[]
33 def add_item():
34 try:
35 with driver.session() as session:
36 session.write_transaction(lambda tx: tx.run("CREATE (a:Item)"))
37 return True
38 except ServiceUnavailable:
39 return False
40 # end::service-unavailable[]
41
42 add_item()
43 add_item()
44 add_item()
45
46 with driver.session() as session:
47 items = session.run("MATCH (a:Item) RETURN count(a)").single().value()
48
49 with driver.session() as session:
50 session.run("MATCH (_) DETACH DELETE _")
51
52 return items
53
54
55 def test_example():
56 pytest.skip("Fix better error messages for the user. Be able to kill the server.")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::session-import[]
22 # end::session-import[]
23
24 # python -m pytest tests/integration/examples/test_session_example.py -s -v
25
26 def session_example(driver):
27 with driver.session() as session:
28 session.run("MATCH (_) DETACH DELETE _")
29
30 # tag::session[]
31 def add_person(name):
32 with driver.session() as session:
33 session.run("CREATE (a:Person {name: $name})", name=name)
34 # end::session[]
35
36 add_person("Alice")
37 add_person("Bob")
38
39 with driver.session() as session:
40 persons = session.run("MATCH (a:Person) RETURN count(a)").single().value()
41
42 with driver.session() as session:
43 session.run("MATCH (_) DETACH DELETE _")
44
45 return persons
46
47
48 def test_example(driver):
49 persons = session_example(driver)
50 assert persons == 2
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 # tag::transaction-function-import[]
22 from neo4j import unit_of_work
23 # end::transaction-function-import[]
24
25
26 # python -m pytest tests/integration/examples/test_transaction_function_example.py -s -v
27
28 # tag::transaction-function[]
29 @unit_of_work(timeout=5)
30 def create_person(tx, name):
31 return tx.run("CREATE (a:Person {name: $name}) RETURN id(a)", name=name).single().value()
32
33
34 def add_person(driver, name):
35 with driver.session() as session:
36 return session.write_transaction(create_person, name)
37 # end::transaction-function[]
38
39
40 class TransactionFunctionExample:
41
42 def __init__(self, driver):
43 self.driver = driver
44
45 def add_person(self, name):
46 return add_person(self.driver, name)
47
48
49 def test_example(bolt_driver):
50 eg = TransactionFunctionExample(bolt_driver)
51 with eg.driver.session() as session:
52 session.run("MATCH (_) DETACH DELETE _")
53 eg.add_person("Alice")
54 n = session.run("MATCH (a:Person) RETURN count(a)").single().value()
55 assert n == 1
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.work.simple import Query
24 from neo4j.exceptions import Neo4jError, ClientError, TransientError
25 from neo4j.graph import Node, Relationship
26 from neo4j.api import Version
27
28
29 def test_can_run_simple_statement(session):
30 result = session.run("RETURN 1 AS n")
31 for record in result:
32 assert record[0] == 1
33 assert record["n"] == 1
34 with pytest.raises(KeyError):
35 _ = record["x"]
36 assert record["n"] == 1
37 with pytest.raises(KeyError):
38 _ = record["x"]
39 with pytest.raises(TypeError):
40 _ = record[object()]
41 assert repr(record)
42 assert len(record) == 1
43
44
45 def test_can_run_simple_statement_with_params(session):
46 count = 0
47 for record in session.run("RETURN $x AS n",
48 {"x": {"abc": ["d", "e", "f"]}}):
49 assert record[0] == {"abc": ["d", "e", "f"]}
50 assert record["n"] == {"abc": ["d", "e", "f"]}
51 assert repr(record)
52 assert len(record) == 1
53 count += 1
54 assert count == 1
55
56
57 @pytest.mark.skip(reason="BOOKMARK, AttributeError: 'Session' object has no attribute 'last_bookmark'")
58 def test_autocommit_transactions_use_bookmarks(neo4j_driver):
59 bookmarks = []
60 # Generate an initial bookmark
61 with neo4j_driver.session() as session:
62 session.run("CREATE ()").consume()
63 bookmark = session.last_bookmark()
64 assert bookmark is not None
65 bookmarks.append(bookmark)
66 # Propagate into another session
67 with neo4j_driver.session(bookmarks=bookmarks) as session:
68 assert list(session.next_bookmarks()) == bookmarks
69 session.run("CREATE ()").consume()
70 bookmark = session.last_bookmark()
71 assert bookmark is not None
72 assert bookmark not in bookmarks
73
74
75 def test_fails_on_bad_syntax(session):
76 with pytest.raises(Neo4jError):
77 session.run("X").consume()
78
79
80 def test_fails_on_missing_parameter(session):
81 with pytest.raises(Neo4jError):
82 session.run("RETURN {x}").consume()
83
84
85 def test_keys_with_an_error(session):
86 with pytest.raises(Neo4jError):
87 result = session.run("X")
88 list(result.keys())
89
90
91 def test_should_not_allow_empty_statements(session):
92 with pytest.raises(ValueError):
93 _ = session.run("")
94
95
96 def test_can_run_statement_that_returns_multiple_records(session):
97 count = 0
98 for record in session.run("unwind(range(1, 10)) AS z RETURN z"):
99 assert 1 <= record[0] <= 10
100 count += 1
101 assert count == 10
102
103
104 def test_can_use_with_to_auto_close_session(session):
105 record_list = list(session.run("RETURN 1"))
106 assert len(record_list) == 1
107 for record in record_list:
108 assert record[0] == 1
109
110
111 def test_can_return_node(neo4j_driver):
112 with neo4j_driver.session() as session:
113 record_list = list(session.run("CREATE (a:Person {name:'Alice'}) "
114 "RETURN a"))
115 assert len(record_list) == 1
116 for record in record_list:
117 alice = record[0]
118 assert isinstance(alice, Node)
119 assert alice.labels == {"Person"}
120 assert dict(alice) == {"name": "Alice"}
121
122
123 def test_can_return_relationship(neo4j_driver):
124 with neo4j_driver.session() as session:
125 record_list = list(session.run("CREATE ()-[r:KNOWS {since:1999}]->() "
126 "RETURN r"))
127 assert len(record_list) == 1
128 for record in record_list:
129 rel = record[0]
130 assert isinstance(rel, Relationship)
131 assert rel.type == "KNOWS"
132 assert dict(rel) == {"since": 1999}
133
134
135 # TODO: re-enable after server bug is fixed
136 # def test_can_return_path(session):
137 # with self.driver.session() as session:
138 # record_list = list(session.run("MERGE p=({name:'Alice'})-[:KNOWS]->"
139 # "({name:'Bob'}) RETURN p"))
140 # assert len(record_list) == 1
141 # for record in record_list:
142 # path = record[0]
143 # assert isinstance(path, Path)
144 # assert path.start_node["name"] == "Alice"
145 # assert path.end_node["name"] == "Bob"
146 # assert path.relationships[0].type == "KNOWS"
147 # assert len(path.nodes) == 2
148 # assert len(path.relationships) == 1
149
150
151 def test_keys_are_available_before_and_after_stream(session):
152 result = session.run("UNWIND range(1, 10) AS n RETURN n")
153 assert list(result.keys()) == ["n"]
154 list(result)
155 assert list(result.keys()) == ["n"]
156
157
158 def test_result_single_record_value(session):
159 record = session.run(Query("RETURN $x"), x=1).single()
160 assert record.value() == 1
161
162
163 @pytest.mark.parametrize(
164 "test_input, neo4j_version",
165 [
166 ("CALL dbms.getTXMetaData", Version(3, 0)),
167 ("CALL tx.getMetaData", Version(4, 0)),
168 ]
169 )
170 def test_autocommit_transactions_should_support_metadata(session, test_input, neo4j_version):
171 # python -m pytest tests/integration/test_autocommit.py -s -r fEsxX -k test_autocommit_transactions_should_support_metadata
172 metadata_in = {"foo": "bar"}
173
174 result = session.run("RETURN 1")
175 value = result.single().value()
176 summary = result.consume()
177 server_agent = summary.server.agent
178
179 try:
180 statement = Query(test_input, metadata=metadata_in)
181 result = session.run(statement)
182 metadata_out = result.single().value()
183 except ClientError as e:
184 if e.code == "Neo.ClientError.Procedure.ProcedureNotFound":
185 pytest.skip("Cannot assert correct metadata as {} does not support procedure '{}' introduced in Neo4j {}".format(server_agent, test_input, neo4j_version))
186 else:
187 raise
188 else:
189 assert metadata_in == metadata_out
190
191
192 def test_autocommit_transactions_should_support_timeout(neo4j_driver):
193 with neo4j_driver.session() as s1:
194 s1.run("CREATE (a:Node)").consume()
195 with neo4j_driver.session() as s2:
196 tx1 = s1.begin_transaction()
197 tx1.run("MATCH (a:Node) SET a.property = 1").consume()
198 try:
199 result = s2.run(Query("MATCH (a:Node) SET a.property = 2", timeout=0.25))
200 result.consume()
201 # On 4.0 and older
202 except TransientError:
203 pass
204 # On 4.1 and forward
205 except ClientError:
206 pass
207 else:
208 raise
209
210
211 def test_regex_in_parameter(session):
212 matches = []
213 result = session.run("UNWIND ['A', 'B', 'C', 'A B', 'B C', 'A B C', "
214 "'A BC', 'AB C'] AS t WITH t "
215 "WHERE t =~ $re RETURN t", re=r'.*\bB\b.*')
216 for record in result:
217 matches.append(record.value())
218 assert matches == ["B", "A B", "B C", "A B C"]
219
220
221 def test_regex_inline(session):
222 matches = []
223 result = session.run(r"UNWIND ['A', 'B', 'C', 'A B', 'B C', 'A B C', "
224 r"'A BC', 'AB C'] AS t WITH t "
225 r"WHERE t =~ '.*\\bB\\b.*' RETURN t")
226 for record in result:
227 matches.append(record.value())
228 assert matches == ["B", "A B", "B C", "A B C"]
229
230
231 def test_automatic_reset_after_failure(session):
232 try:
233 result = session.run("X")
234 result.consume()
235 except Neo4jError:
236 result = session.run("RETURN 1")
237 record = next(iter(result))
238 assert record[0] == 1
239 else:
240 assert False, "A Cypher error should have occurred"
241
242
243 def test_large_values(bolt_driver):
244 for i in range(1, 7):
245 with bolt_driver.session() as session:
246 session.run("RETURN '{}'".format("A" * 2 ** 20))
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 BoltDriver,
26 Version,
27 READ_ACCESS,
28 WRITE_ACCESS,
29 ResultSummary,
30 unit_of_work,
31 Transaction,
32 Result,
33 ServerInfo,
34 )
35 from neo4j.exceptions import (
36 ServiceUnavailable,
37 AuthError,
38 ConfigurationError,
39 ClientError,
40 )
41 from neo4j._exceptions import BoltHandshakeError
42 from neo4j.io._bolt3 import Bolt3
43
44 # python -m pytest tests/integration/test_bolt_driver.py -s -v
45
46
47 def test_bolt_uri(bolt_uri, auth):
48 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_uri
49 try:
50 with GraphDatabase.driver(bolt_uri, auth=auth) as driver:
51 with driver.session() as session:
52 value = session.run("RETURN 1").single().value()
53 assert value == 1
54 except ServiceUnavailable as error:
55 assert isinstance(error.__cause__, BoltHandshakeError)
56 pytest.skip(error.args[0])
57
58
59 # def test_readonly_bolt_uri(readonly_bolt_uri, auth):
60 # with GraphDatabase.driver(readonly_bolt_uri, auth=auth) as driver:
61 # with driver.session() as session:
62 # value = session.run("RETURN 1").single().value()
63 # assert value == 1
64
65
66 def test_normal_use_case(bolt_driver):
67 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_normal_use_case
68 session = bolt_driver.session()
69 value = session.run("RETURN 1").single().value()
70 assert value == 1
71
72
73 def test_invalid_url_scheme(service):
74 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_invalid_url_scheme
75 address = service.addresses[0]
76 uri = "x://{}:{}".format(address[0], address[1])
77 try:
78 with pytest.raises(ConfigurationError):
79 _ = GraphDatabase.driver(uri, auth=service.auth)
80 except ServiceUnavailable as error:
81 if isinstance(error.__cause__, BoltHandshakeError):
82 pytest.skip(error.args[0])
83
84
85 def test_fail_nicely_when_using_http_port(service):
86 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_fail_nicely_when_using_http_port
87 from tests.integration.conftest import NEO4J_PORTS
88 address = service.addresses[0]
89 uri = "bolt://{}:{}".format(address[0], NEO4J_PORTS["http"])
90 with pytest.raises(ServiceUnavailable):
91 _ = GraphDatabase.driver(uri, auth=service.auth)
92
93
94 def test_custom_resolver(service):
95 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_custom_resolver
96 _, port = service.addresses[0]
97
98 def my_resolver(socket_address):
99 assert socket_address == ("*", 7687)
100 yield "99.99.99.99", port # should be rejected as unable to connect
101 yield "127.0.0.1", port # should succeed
102
103 try:
104 with GraphDatabase.driver("bolt://*", auth=service.auth,
105 connection_timeout=3, # enables rapid timeout
106 resolver=my_resolver) as driver:
107 with driver.session() as session:
108 summary = session.run("RETURN 1").consume()
109 assert summary.server.address == ("127.0.0.1", port)
110 except ServiceUnavailable as error:
111 if isinstance(error.__cause__, BoltHandshakeError):
112 pytest.skip(error.args[0])
113
114
115 def test_encrypted_set_to_false_by_default(bolt_driver):
116 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_encrypted_set_to_false_by_default
117 assert bolt_driver.encrypted is False
118
119
120 def test_should_fail_on_incorrect_password(bolt_uri):
121 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_should_fail_on_incorrect_password
122 with pytest.raises(AuthError):
123 try:
124 with GraphDatabase.driver(bolt_uri, auth=("neo4j", "wrong-password")) as driver:
125 with driver.session() as session:
126 _ = session.run("RETURN 1")
127 except ServiceUnavailable as error:
128 if isinstance(error.__cause__, BoltHandshakeError):
129 pytest.skip(error.args[0])
130
131
132 def test_supports_multi_db(bolt_uri, auth):
133 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_supports_multi_db
134 try:
135 driver = GraphDatabase.driver(bolt_uri, auth=auth)
136 assert isinstance(driver, BoltDriver)
137 except ServiceUnavailable as error:
138 if isinstance(error.__cause__, BoltHandshakeError):
139 pytest.skip(error.args[0])
140
141 with driver.session() as session:
142 result = session.run("RETURN 1")
143 _ = result.single().value() # Consumes the result
144 summary = result.consume()
145 server_info = summary.server
146
147 assert isinstance(summary, ResultSummary)
148 assert isinstance(server_info, ServerInfo)
149 assert server_info.version_info() is not None
150 assert isinstance(server_info.protocol_version, Version)
151
152 result = driver.supports_multi_db()
153 driver.close()
154
155 if server_info.protocol_version == Bolt3.PROTOCOL_VERSION:
156 assert result is False
157 assert summary.database is None
158 assert summary.query_type == "r"
159 else:
160 assert result is True
161 assert server_info.version_info() >= Version(4, 0)
162 assert server_info.protocol_version >= Version(4, 0)
163 assert summary.database == "neo4j" # This is the default database name if not set explicitly on the Neo4j Server
164 assert summary.query_type == "r"
165
166
167 def test_test_multi_db_specify_database(bolt_uri, auth):
168 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_test_multi_db_specify_database
169 try:
170 with GraphDatabase.driver(bolt_uri, auth=auth, database="test_database") as driver:
171 with driver.session() as session:
172 result = session.run("RETURN 1")
173 result_iter = iter(result)
174 assert next(result_iter) == 1
175 summary = result.consume()
176 assert summary.database == "test_database"
177 except ServiceUnavailable as error:
178 if isinstance(error.__cause__, BoltHandshakeError):
179 pytest.skip(error.args[0])
180 except ConfigurationError as error:
181 assert "Database name parameter for selecting database is not supported in Bolt Protocol Version(3, 0)" in error.args[0]
182 except ClientError as error:
183 # FAILURE {'code': 'Neo.ClientError.Database.DatabaseNotFound' - This message is sent from the server
184 assert error.args[0] == "Database does not exist. Database name: 'test_database'."
185
186
187 def test_bolt_driver_fetch_size_config_case_on_close_result_consume(bolt_uri, auth):
188 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_fetch_size_config_case_on_close_result_consume
189 try:
190 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
191 assert isinstance(driver, BoltDriver)
192 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
193 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
194 # Check the expected result with logging manually
195 except ServiceUnavailable as error:
196 if isinstance(error.__cause__, BoltHandshakeError):
197 pytest.skip(error.args[0])
198
199
200 def test_bolt_driver_fetch_size_config_case_normal(bolt_uri, auth):
201 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_fetch_size_config_case_normal
202 try:
203 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
204 assert isinstance(driver, BoltDriver)
205 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
206 expected = []
207 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
208 for record in result:
209 expected.append(record["x"])
210
211 assert expected == [1, 2, 3, 4]
212 except ServiceUnavailable as error:
213 if isinstance(error.__cause__, BoltHandshakeError):
214 pytest.skip(error.args[0])
215
216
217 def test_bolt_driver_fetch_size_config_run_consume_run(bolt_uri, auth):
218 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_fetch_size_config_run_consume_run
219 try:
220 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
221 assert isinstance(driver, BoltDriver)
222 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
223 expected = []
224 result1 = session.run("UNWIND [1,2,3,4] AS x RETURN x")
225 result1.consume()
226 result2 = session.run("UNWIND [5,6,7,8] AS x RETURN x")
227
228 for record in result2:
229 expected.append(record["x"])
230
231 result_summary = result2.consume()
232 assert isinstance(result_summary, ResultSummary)
233
234 assert expected == [5, 6, 7, 8]
235 except ServiceUnavailable as error:
236 if isinstance(error.__cause__, BoltHandshakeError):
237 pytest.skip(error.args[0])
238
239
240 def test_bolt_driver_fetch_size_config_run_run(bolt_uri, auth):
241 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_fetch_size_config_run_run
242 try:
243 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
244 assert isinstance(driver, BoltDriver)
245 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
246 expected = []
247 result1 = session.run("UNWIND [1,2,3,4] AS x RETURN x")
248 result2 = session.run("UNWIND [5,6,7,8] AS x RETURN x")
249
250 for record in result2:
251 expected.append(record["x"])
252
253 result_summary = result2.consume()
254 assert isinstance(result_summary, ResultSummary)
255
256 assert expected == [5, 6, 7, 8]
257 except ServiceUnavailable as error:
258 if isinstance(error.__cause__, BoltHandshakeError):
259 pytest.skip(error.args[0])
260
261
262 def test_bolt_driver_read_transaction_fetch_size_config_normal_case(bolt_uri, auth):
263 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_read_transaction_fetch_size_config_normal_case
264 @unit_of_work(timeout=3, metadata={"foo": "bar"})
265 def unwind(transaction):
266 assert isinstance(transaction, Transaction)
267 values = []
268 result = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
269 assert isinstance(result, Result)
270 for record in result:
271 values.append(record["x"])
272 return values
273
274 try:
275 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
276 assert isinstance(driver, BoltDriver)
277 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
278 expected = session.read_transaction(unwind)
279
280 assert expected == [1, 2, 3, 4]
281 except ServiceUnavailable as error:
282 if isinstance(error.__cause__, BoltHandshakeError):
283 pytest.skip(error.args[0])
284
285
286 def test_bolt_driver_multiple_results_case_1(bolt_uri, auth):
287 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_multiple_results_case_1
288 try:
289 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
290 assert isinstance(driver, BoltDriver)
291 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
292 transaction = session.begin_transaction(timeout=3, metadata={"foo": "bar"})
293 result1 = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
294 values1 = []
295 for ix in result1:
296 values1.append(ix["x"])
297 transaction.commit()
298 assert values1 == [1, 2, 3, 4]
299
300 except ServiceUnavailable as error:
301 if isinstance(error.__cause__, BoltHandshakeError):
302 pytest.skip(error.args[0])
303
304
305 def test_bolt_driver_multiple_results_case_2(bolt_uri, auth):
306 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_multiple_results_case_2
307 try:
308 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
309 assert isinstance(driver, BoltDriver)
310 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
311 transaction = session.begin_transaction(timeout=3, metadata={"foo": "bar"})
312 result1 = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
313 result2 = transaction.run("UNWIND [5,6,7,8] AS x RETURN x")
314 values1 = []
315 values2 = []
316 for ix in result2:
317 values2.append(ix["x"])
318 for ix in result1:
319 values1.append(ix["x"])
320 transaction.commit()
321 assert values2 == [5, 6, 7, 8]
322 assert values1 == [1, 2, 3, 4]
323
324 except ServiceUnavailable as error:
325 if isinstance(error.__cause__, BoltHandshakeError):
326 pytest.skip(error.args[0])
327
328
329 def test_bolt_driver_multiple_results_case_3(bolt_uri, auth):
330 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_multiple_results_case_3
331 try:
332 with GraphDatabase.driver(bolt_uri, auth=auth, user_agent="test") as driver:
333 assert isinstance(driver, BoltDriver)
334 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
335 transaction = session.begin_transaction(timeout=3, metadata={"foo": "bar"})
336 values1 = []
337 values2 = []
338 result1 = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
339 result1_iter = iter(result1)
340 values1.append(next(result1_iter)["x"])
341 result2 = transaction.run("UNWIND [5,6,7,8] AS x RETURN x")
342 for ix in result2:
343 values2.append(ix["x"])
344 transaction.commit() # Should discard the rest of records in result1 and result2 -> then set mode discard.
345 assert values2 == [5, 6, 7, 8]
346 assert result2._closed is True
347 assert values1 == [1, ]
348
349 try:
350 values1.append(next(result1_iter)["x"])
351 except StopIteration as e:
352 # Bolt 4.0
353 assert values1 == [1, ]
354 assert result1._closed is True
355 else:
356 # Bolt 3 only have PULL ALL and no qid so it will behave like autocommit r1=session.run rs2=session.run
357 values1.append(next(result1_iter)["x"])
358 assert values1 == [1, 2, 3]
359 assert result1._closed is False
360
361 assert result1._closed is True
362
363 except ServiceUnavailable as error:
364 if isinstance(error.__cause__, BoltHandshakeError):
365 pytest.skip(error.args[0])
366
367
368 def test_bolt_driver_case_pull_no_records(driver):
369 # python -m pytest tests/integration/test_bolt_driver.py -s -v -k test_bolt_driver_case_pull_no_records
370 try:
371 with driver.session(default_access_mode=WRITE_ACCESS) as session:
372 with session.begin_transaction() as tx:
373 result = tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=123)
374 except ServiceUnavailable as error:
375 if isinstance(error.__cause__, BoltHandshakeError):
376 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from uuid import uuid4
22
23 from neo4j.api import READ_ACCESS, WRITE_ACCESS
24 from neo4j.graph import Node
25
26
27 def test_can_obtain_bookmark_after_commit(bolt_driver):
28 with bolt_driver.session() as session:
29 with session.begin_transaction() as tx:
30 tx.run("RETURN 1")
31 assert session.last_bookmark() is not None
32
33
34 def test_can_pass_bookmark_into_next_transaction(driver):
35 unique_id = uuid4().hex
36
37 with driver.session(default_access_mode=WRITE_ACCESS) as session:
38 with session.begin_transaction() as tx:
39 tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=unique_id)
40 bookmark = session.last_bookmark()
41
42 assert bookmark is not None
43
44 with driver.session(default_access_mode=READ_ACCESS, bookmarks=[bookmark]) as session:
45 with session.begin_transaction() as tx:
46 result = tx.run("MATCH (a:Thing {uuid:$uuid}) RETURN a", uuid=unique_id)
47 record_list = list(result)
48 assert len(record_list) == 1
49 record = record_list[0]
50 assert len(record) == 1
51 thing = record[0]
52 assert isinstance(thing, Node)
53 assert thing["uuid"] == unique_id
54
55
56 def test_bookmark_should_be_none_after_rollback(driver):
57 with driver.session(default_access_mode=WRITE_ACCESS) as session:
58 with session.begin_transaction() as tx:
59 tx.run("CREATE (a)")
60
61 assert session.last_bookmark() is not None
62
63 with driver.session(default_access_mode=WRITE_ACCESS) as session:
64 with session.begin_transaction() as tx:
65 tx.run("CREATE (a)")
66 tx.rollback()
67
68 assert session.last_bookmark() is None
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from math import isnan
22
23 import pytest
24
25
26 def test_null(cypher_eval):
27 assert cypher_eval("RETURN null") is None
28
29
30 def test_boolean_true(cypher_eval):
31 assert cypher_eval("RETURN true") is True
32
33
34 def test_boolean_false(cypher_eval):
35 assert cypher_eval("RETURN false") is False
36
37
38 def test_integer(cypher_eval):
39 assert cypher_eval("RETURN 123456789") == 123456789
40
41
42 def test_float(cypher_eval):
43 assert cypher_eval("RETURN 3.1415926") == 3.1415926
44
45
46 def test_float_nan(cypher_eval):
47 assert isnan(cypher_eval("WITH $x AS x RETURN x", x=float("NaN")))
48
49
50 def test_float_positive_infinity(cypher_eval):
51 infinity = float("+Inf")
52 assert cypher_eval("WITH $x AS x RETURN x", x=infinity) == infinity
53
54
55 def test_float_negative_infinity(cypher_eval):
56 infinity = float("-Inf")
57 assert cypher_eval("WITH $x AS x RETURN x", x=infinity) == infinity
58
59
60 def test_string(cypher_eval):
61 assert cypher_eval("RETURN 'hello, world'") == "hello, world"
62
63
64 def test_bytes(cypher_eval):
65 data = bytearray([0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF])
66 assert cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data) == data
67
68
69 def test_list(cypher_eval):
70 data = ["one", "two", "three"]
71 assert cypher_eval("WITH $x AS x RETURN x", x=data) == data
72
73
74 def test_map(cypher_eval):
75 data = {"one": "eins", "two": "zwei", "three": "drei"}
76 assert cypher_eval("WITH $x AS x RETURN x", x=data) == data
77
78
79 def test_non_string_map_keys(session):
80 with pytest.raises(TypeError):
81 _ = session.run("RETURN $x", x={1: 'eins', 2: 'zwei', 3: 'drei'})
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 from uuid import uuid4
23
24
25 from neo4j.work.simple import (
26 Query,
27 TransactionError,
28 )
29 from neo4j.exceptions import (
30 CypherSyntaxError,
31 ClientError,
32 TransientError,
33 )
34
35
36 def test_can_commit_transaction(session):
37
38 tx = session.begin_transaction()
39
40 # Create a node
41 result = tx.run("CREATE (a) RETURN id(a)")
42 record = next(iter(result))
43 node_id = record[0]
44 assert isinstance(node_id, int)
45
46 # Update a property
47 tx.run("MATCH (a) WHERE id(a) = $n "
48 "SET a.foo = $foo", {"n": node_id, "foo": "bar"})
49
50 tx.commit()
51
52 # Check the property value
53 result = session.run("MATCH (a) WHERE id(a) = $n "
54 "RETURN a.foo", {"n": node_id})
55 record = next(iter(result))
56 value = record[0]
57 assert value == "bar"
58
59
60 def test_can_rollback_transaction(session):
61 tx = session.begin_transaction()
62
63 # Create a node
64 result = tx.run("CREATE (a) RETURN id(a)")
65 record = next(iter(result))
66 node_id = record[0]
67 assert isinstance(node_id, int)
68
69 # Update a property
70 tx.run("MATCH (a) WHERE id(a) = $n "
71 "SET a.foo = $foo", {"n": node_id, "foo": "bar"})
72
73 tx.rollback()
74
75 # Check the property value
76 result = session.run("MATCH (a) WHERE id(a) = $n "
77 "RETURN a.foo", {"n": node_id})
78 assert len(list(result)) == 0
79
80
81 def test_can_commit_transaction_using_with_block(session):
82 with session.begin_transaction() as tx:
83 # Create a node
84 result = tx.run("CREATE (a) RETURN id(a)")
85 record = next(iter(result))
86 node_id = record[0]
87 assert isinstance(node_id, int)
88
89 # Update a property
90 tx.run("MATCH (a) WHERE id(a) = $n "
91 "SET a.foo = $foo", {"n": node_id, "foo": "bar"})
92
93 tx.commit()
94
95 # Check the property value
96 result = session.run("MATCH (a) WHERE id(a) = $n "
97 "RETURN a.foo", {"n": node_id})
98 record = next(iter(result))
99 value = record[0]
100 assert value == "bar"
101
102
103 def test_can_rollback_transaction_using_with_block(session):
104 with session.begin_transaction() as tx:
105 # Create a node
106 result = tx.run("CREATE (a) RETURN id(a)")
107 record = next(iter(result))
108 node_id = record[0]
109 assert isinstance(node_id, int)
110
111 # Update a property
112 tx.run("MATCH (a) WHERE id(a) = $n "
113 "SET a.foo = $foo", {"n": node_id, "foo": "bar"})
114 tx.rollback()
115
116 # Check the property value
117 result = session.run("MATCH (a) WHERE id(a) = $n "
118 "RETURN a.foo", {"n": node_id})
119 assert len(list(result)) == 0
120
121
122 def test_broken_transaction_should_not_break_session(session):
123 with pytest.raises(CypherSyntaxError):
124 with session.begin_transaction() as tx:
125 tx.run("X")
126 with session.begin_transaction() as tx:
127 tx.run("RETURN 1")
128
129
130 def test_begin_transaction_time_out_metadata(session):
131 with session.begin_transaction(timeout=2, metadata={"foo": "bar"}) as transaction:
132 _ = transaction.run("RETURN 1").single()
133
134
135 def test_statement_object_not_supported(session):
136 with session.begin_transaction() as tx:
137 with pytest.raises(ValueError):
138 tx.run(Query("RETURN 1", timeout=0.25))
139
140
141 def test_transaction_metadata(session):
142 metadata_in = {"foo": "bar"}
143 with session.begin_transaction(metadata=metadata_in) as tx:
144 try:
145 metadata_out = tx.run("CALL dbms.getTXMetaData").single().value()
146 except ClientError as e:
147 if e.code == "Neo.ClientError.Procedure.ProcedureNotFound":
148 pytest.skip("Cannot assert correct metadata as Neo4j edition does not support procedure dbms.getTXMetaData")
149 else:
150 raise
151 else:
152 assert metadata_in == metadata_out
153
154
155 def test_transaction_timeout(driver):
156 with driver.session() as s1:
157 s1.run("CREATE (a:Node)").consume()
158 with driver.session() as s2:
159 tx1 = s1.begin_transaction()
160 tx1.run("MATCH (a:Node) SET a.property = 1").consume()
161 tx2 = s2.begin_transaction(timeout=0.25)
162 try:
163 tx2.run("MATCH (a:Node) SET a.property = 2").consume()
164 # On 4.0 and older
165 except TransientError:
166 pass
167 # On 4.1 and forward
168 except ClientError:
169 pass
170 else:
171 raise
172
173
174 # TODO: Re-enable and test when TC is available again
175 # def test_exit_after_explicit_close_should_be_silent(bolt_driver):
176 # with bolt_driver.session() as s:
177 # with s.begin_transaction() as tx:
178 # assert not tx.closed()
179 # tx.close()
180 # assert tx.closed()
181 # assert tx.closed()
182
183
184 @pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
185 def test_should_sync_after_commit(session):
186 tx = session.begin_transaction()
187 result = tx.run("RETURN 1")
188 tx.commit()
189 buffer = result._records
190 assert len(buffer) == 1
191 assert buffer[0][0] == 1
192
193
194 @pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
195 def test_should_sync_after_rollback(session):
196 tx = session.begin_transaction()
197 result = tx.run("RETURN 1")
198 tx.rollback()
199 buffer = result._records
200 assert len(buffer) == 1
201 assert buffer[0][0] == 1
202
203
204 def test_errors_on_run_transaction(session):
205 tx = session.begin_transaction()
206 with pytest.raises(TypeError):
207 tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=uuid4())
208 tx.rollback()
209
210
211 def test_error_on_using_closed_transaction(session):
212 tx = session.begin_transaction()
213 tx.run("RETURN 1")
214 tx.commit()
215 with pytest.raises(TransactionError):
216 tx.run("RETURN 1")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.graph import (
22 Node,
23 Relationship,
24 Path,
25 )
26
27
28 def test_node(cypher_eval):
29 a = cypher_eval("CREATE (a:Person {name:'Alice'}) "
30 "RETURN a")
31 assert isinstance(a, Node)
32 assert set(a.labels) == {"Person"}
33 assert dict(a) == {"name": "Alice"}
34
35
36 def test_relationship(cypher_eval):
37 a, b, r = cypher_eval("CREATE (a)-[r:KNOWS {since:1999}]->(b) "
38 "RETURN [a, b, r]")
39 assert isinstance(r, Relationship)
40 assert r.type == "KNOWS"
41 assert dict(r) == {"since": 1999}
42 assert r.start_node == a
43 assert r.end_node == b
44
45
46 def test_path(cypher_eval):
47 a, b, c, ab, bc, p = cypher_eval("CREATE p=(a)-[ab:X]->(b)-[bc:X]->(c) "
48 "RETURN [a, b, c, ab, bc, p]")
49 assert isinstance(p, Path)
50 assert len(p) == 2
51 assert p.nodes == (a, b, c)
52 assert p.relationships == (ab, bc)
53 assert p.start_node == a
54 assert p.end_node == c
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 Neo4jDriver,
26 Version,
27 READ_ACCESS,
28 ResultSummary,
29 ServerInfo,
30 )
31 from neo4j.exceptions import (
32 ServiceUnavailable,
33 ConfigurationError,
34 ClientError,
35 )
36 from neo4j._exceptions import (
37 BoltHandshakeError,
38 )
39 from neo4j.conf import (
40 RoutingConfig,
41 )
42 from neo4j.io._bolt3 import Bolt3
43
44 # python -m pytest tests/integration/test_neo4j_driver.py -s -v
45
46
47 def test_neo4j_uri(neo4j_uri, auth, target):
48 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_uri
49 try:
50 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
51 with driver.session() as session:
52 value = session.run("RETURN 1").single().value()
53 assert value == 1
54 except ServiceUnavailable as error:
55 if error.args[0] == "Server does not support routing":
56 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
57 pytest.skip(error.args[0])
58 elif isinstance(error.__cause__, BoltHandshakeError):
59 pytest.skip(error.args[0])
60
61
62 def test_supports_multi_db(neo4j_uri, auth, target):
63 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_supports_multi_db
64 try:
65 driver = GraphDatabase.driver(neo4j_uri, auth=auth)
66 assert isinstance(driver, Neo4jDriver)
67 except ServiceUnavailable as error:
68 if error.args[0] == "Server does not support routing":
69 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
70 pytest.skip(error.args[0])
71 elif isinstance(error.__cause__, BoltHandshakeError):
72 pytest.skip(error.args[0])
73
74 with driver.session() as session:
75 result = session.run("RETURN 1")
76 _ = result.single().value() # Consumes the result
77 summary = result.consume()
78 server_info = summary.server
79
80 assert isinstance(summary, ResultSummary)
81 assert isinstance(server_info, ServerInfo)
82 assert server_info.version_info() is not None
83 assert isinstance(server_info.protocol_version, Version)
84
85 result = driver.supports_multi_db()
86 driver.close()
87
88 if server_info.protocol_version == Bolt3.PROTOCOL_VERSION:
89 assert result is False
90 assert summary.database is None
91 assert summary.query_type == "r"
92 else:
93 assert result is True
94 assert server_info.version_info() >= Version(4, 0)
95 assert server_info.protocol_version >= Version(4, 0)
96 assert summary.database == "neo4j" # This is the default database name if not set explicitly on the Neo4j Server
97 assert summary.query_type == "r"
98
99
100 def test_test_multi_db_specify_database(neo4j_uri, auth, target):
101 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_test_multi_db_specify_database
102 try:
103 with GraphDatabase.driver(neo4j_uri, auth=auth, database="test_database") as driver:
104 with driver.session() as session:
105 result = session.run("RETURN 1")
106 assert next(result) == 1
107 summary = result.consume()
108 assert summary.database == "test_database"
109 except ServiceUnavailable as error:
110 if isinstance(error.__cause__, BoltHandshakeError):
111 pytest.skip(error.args[0])
112 except ConfigurationError as error:
113 assert "Database name parameter for selecting database is not supported in Bolt Protocol Version(3, 0)." in error.args[0]
114 except ClientError as error:
115 # FAILURE {'code': 'Neo.ClientError.Database.DatabaseNotFound' - This message is sent from the server
116 assert error.args[0] == "Unable to get a routing table for database 'test_database' because this database does not exist"
117
118
119 def test_neo4j_multi_database_support_create(neo4j_uri, auth, target):
120 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_multi_database_support_create
121 try:
122 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
123 with driver.session(database="system") as session:
124 session.run("DROP DATABASE test IF EXISTS").consume()
125 result = session.run("SHOW DATABASES")
126 databases = set()
127 for record in result:
128 databases.add(record.get("name"))
129 assert "system" in databases
130 assert "neo4j" in databases
131
132 session.run("CREATE DATABASE test").consume()
133 result = session.run("SHOW DATABASES")
134 for record in result:
135 databases.add(record.get("name"))
136 assert "system" in databases
137 assert "neo4j" in databases
138 assert "test" in databases
139 with driver.session(database="system") as session:
140 session.run("DROP DATABASE test IF EXISTS").consume()
141 except ServiceUnavailable as error:
142 if error.args[0] == "Server does not support routing":
143 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
144 pytest.skip(error.args[0])
145 elif isinstance(error.__cause__, BoltHandshakeError):
146 pytest.skip(error.args[0])
147
148
149 def test_neo4j_multi_database_support_different(neo4j_uri, auth, target):
150 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_multi_database_support_different
151 try:
152 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
153 with driver.session() as session:
154 # Test that default database is empty
155 session.run("MATCH (n) DETACH DELETE n").consume()
156 result = session.run("MATCH (p:Person) RETURN p")
157 names = set()
158 for ix in result:
159 names.add(ix["p"].get("name"))
160 assert names == set() # THIS FAILS?
161 with driver.session(database="system") as session:
162 session.run("DROP DATABASE testa IF EXISTS").consume()
163 session.run("DROP DATABASE testb IF EXISTS").consume()
164 with driver.session(database="system") as session:
165 result = session.run("SHOW DATABASES")
166 databases = set()
167 for record in result:
168 databases.add(record.get("name"))
169 assert databases == {"system", "neo4j"}
170 result = session.run("CREATE DATABASE testa")
171 result.consume()
172 result = session.run("CREATE DATABASE testb")
173 result.consume()
174 with driver.session(database="testa") as session:
175 result = session.run('CREATE (p:Person {name: "ALICE"})')
176 result.consume()
177 with driver.session(database="testb") as session:
178 result = session.run('CREATE (p:Person {name: "BOB"})')
179 result.consume()
180 with driver.session() as session:
181 # Test that default database is still empty
182 result = session.run("MATCH (p:Person) RETURN p")
183 names = set()
184 for ix in result:
185 names.add(ix["p"].get("name"))
186 assert names == set() # THIS FAILS?
187 with driver.session(database="testa") as session:
188 result = session.run("MATCH (p:Person) RETURN p")
189 names = set()
190 for ix in result:
191 names.add(ix["p"].get("name"))
192 assert names == {"ALICE", }
193 with driver.session(database="testb") as session:
194 result = session.run("MATCH (p:Person) RETURN p")
195 names = set()
196 for ix in result:
197 names.add(ix["p"].get("name"))
198 assert names == {"BOB", }
199 with driver.session(database="system") as session:
200 session.run("DROP DATABASE testa IF EXISTS").consume()
201 with driver.session(database="system") as session:
202 session.run("DROP DATABASE testb IF EXISTS").consume()
203 with driver.session() as session:
204 session.run("MATCH (n) DETACH DELETE n").consume()
205 except ServiceUnavailable as error:
206 if error.args[0] == "Server does not support routing":
207 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
208 pytest.skip(error.args[0])
209 elif isinstance(error.__cause__, BoltHandshakeError):
210 pytest.skip(error.args[0])
211
212
213 def test_neo4j_multi_database_test_routing_table_creates_new_if_deleted(neo4j_uri, auth, target):
214 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_multi_database_test_routing_table_creates_new_if_deleted
215 try:
216 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
217 with driver.session(database="system") as session:
218 result = session.run("DROP DATABASE test IF EXISTS")
219 result.consume()
220 result = session.run("SHOW DATABASES")
221 databases = set()
222 for record in result:
223 databases.add(record.get("name"))
224 assert databases == {"system", "neo4j"}
225
226 result = session.run("CREATE DATABASE test")
227 result.consume()
228 result = session.run("SHOW DATABASES")
229 for record in result:
230 databases.add(record.get("name"))
231 assert databases == {"system", "neo4j", "test"}
232 with driver.session(database="test") as session:
233 result = session.run("RETURN 1 AS x")
234 result.consume()
235 del driver._pool.routing_tables["test"]
236 with driver.session(database="test") as session:
237 result = session.run("RETURN 1 AS x")
238 result.consume()
239 with driver.session(database="system") as session:
240 result = session.run("DROP DATABASE test IF EXISTS")
241 result.consume()
242 except ServiceUnavailable as error:
243 if error.args[0] == "Server does not support routing":
244 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
245 pytest.skip(error.args[0])
246 elif isinstance(error.__cause__, BoltHandshakeError):
247 pytest.skip(error.args[0])
248
249
250 def test_neo4j_multi_database_test_routing_table_updates_if_stale(neo4j_uri, auth, target):
251 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_multi_database_test_routing_table_updates_if_stale
252 try:
253 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
254 with driver.session(database="system") as session:
255 result = session.run("DROP DATABASE test IF EXISTS")
256 result.consume()
257 result = session.run("SHOW DATABASES")
258 databases = set()
259 for record in result:
260 databases.add(record.get("name"))
261 assert databases == {"system", "neo4j"}
262
263 result = session.run("CREATE DATABASE test")
264 result.consume()
265 result = session.run("SHOW DATABASES")
266 for record in result:
267 databases.add(record.get("name"))
268 assert databases == {"system", "neo4j", "test"}
269 with driver.session(database="test") as session:
270 result = session.run("RETURN 1 AS x")
271 result.consume()
272 driver._pool.routing_tables["test"].ttl = 0
273 old_value = driver._pool.routing_tables["test"].last_updated_time
274 with driver.session(database="test") as session:
275 result = session.run("RETURN 1 AS x")
276 result.consume()
277 with driver.session(database="system") as session:
278 result = session.run("DROP DATABASE test IF EXISTS")
279 result.consume()
280 assert driver._pool.routing_tables["test"].last_updated_time > old_value
281 except ServiceUnavailable as error:
282 if error.args[0] == "Server does not support routing":
283 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
284 pytest.skip(error.args[0])
285 elif isinstance(error.__cause__, BoltHandshakeError):
286 pytest.skip(error.args[0])
287
288
289 def test_neo4j_multi_database_test_routing_table_removes_aged(neo4j_uri, auth, target):
290 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_multi_database_test_routing_table_removes_aged
291 try:
292 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
293 with driver.session(database="system") as session:
294 result = session.run("DROP DATABASE testa IF EXISTS")
295 result.consume()
296 result = session.run("DROP DATABASE testb IF EXISTS")
297 result.consume()
298 result = session.run("SHOW DATABASES")
299 databases = set()
300 for record in result:
301 databases.add(record.get("name"))
302 assert databases == {"system", "neo4j"}
303
304 result = session.run("CREATE DATABASE testa")
305 result.consume()
306 result = session.run("CREATE DATABASE testb")
307 result.consume()
308 result = session.run("SHOW DATABASES")
309 for record in result:
310 databases.add(record.get("name"))
311 assert databases == {"system", "neo4j", "testa", "testb"}
312 with driver.session(database="testa") as session:
313 result = session.run("RETURN 1 AS x")
314 result.consume()
315 with driver.session(database="testb") as session:
316 result = session.run("RETURN 1 AS x")
317 result.consume()
318 driver._pool.routing_tables["testa"].ttl = 0
319 driver._pool.routing_tables["testb"].ttl = -1 * RoutingConfig.routing_table_purge_delay
320 old_value = driver._pool.routing_tables["testa"].last_updated_time
321 with driver.session(database="testa") as session:
322 # This will refresh the routing table for "testa" and the refresh will trigger a cleanup of aged routing tables
323 result = session.run("RETURN 1 AS x")
324 result.consume()
325 with driver.session(database="system") as session:
326 result = session.run("DROP DATABASE testa IF EXISTS")
327 result.consume()
328 result = session.run("DROP DATABASE testb IF EXISTS")
329 result.consume()
330 assert driver._pool.routing_tables["testa"].last_updated_time > old_value
331 assert "testb" not in driver._pool.routing_tables
332 except ServiceUnavailable as error:
333 if error.args[0] == "Server does not support routing":
334 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
335 pytest.skip(error.args[0])
336 elif isinstance(error.__cause__, BoltHandshakeError):
337 pytest.skip(error.args[0])
338
339
340 def test_neo4j_driver_fetch_size_config_autocommit_normal_case(neo4j_uri, auth):
341 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_driver_fetch_size_config_autocommit_normal_case
342 try:
343 with GraphDatabase.driver(neo4j_uri, auth=auth, user_agent="test") as driver:
344 assert isinstance(driver, Neo4jDriver)
345 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
346 expected = []
347 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
348 for record in result:
349 expected.append(record["x"])
350
351 assert expected == [1, 2, 3, 4]
352 except ServiceUnavailable as error:
353 if error.args[0] == "Server does not support routing":
354 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
355 pytest.skip(error.args[0])
356 elif isinstance(error.__cause__, BoltHandshakeError):
357 pytest.skip(error.args[0])
358
359
360 def test_neo4j_driver_fetch_size_config_autocommit_consume_case(neo4j_uri, auth):
361 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_driver_fetch_size_config_autocommit_consume_case
362 try:
363 with GraphDatabase.driver(neo4j_uri, auth=auth, user_agent="test") as driver:
364 assert isinstance(driver, Neo4jDriver)
365 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
366 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
367 result_summary_consume = result.consume()
368
369 assert isinstance(result_summary_consume, ResultSummary)
370 except ServiceUnavailable as error:
371 if error.args[0] == "Server does not support routing":
372 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
373 pytest.skip(error.args[0])
374 elif isinstance(error.__cause__, BoltHandshakeError):
375 pytest.skip(error.args[0])
376
377
378 def test_neo4j_driver_fetch_size_config_explicit_transaction(neo4j_uri, auth):
379 # python -m pytest tests/integration/test_neo4j_driver.py -s -v -k test_neo4j_driver_fetch_size_config_explicit_transaction
380 try:
381 with GraphDatabase.driver(neo4j_uri, auth=auth, user_agent="test") as driver:
382 assert isinstance(driver, Neo4jDriver)
383 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
384 expected = []
385 tx = session.begin_transaction()
386 result = tx.run("UNWIND [1,2,3,4] AS x RETURN x")
387 for record in result:
388 expected.append(record["x"])
389 tx.commit()
390
391 assert expected == [1, 2, 3, 4]
392 except ServiceUnavailable as error:
393 if error.args[0] == "Server does not support routing":
394 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
395 pytest.skip(error.args[0])
396 elif isinstance(error.__cause__, BoltHandshakeError):
397 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2018 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 from uuid import uuid4
23
24
25 from neo4j.packstream import Structure
26 from neo4j.exceptions import (
27 Neo4jError,
28 CypherSyntaxError,
29 )
30 from neo4j.graph import (
31 Node,
32 Relationship,
33 Path,
34 )
35 from neo4j.work.pipelining import PullOrderException
36
37
38 def test_can_run_simple_statement(bolt_driver):
39 pipeline = bolt_driver.pipeline(flush_every=0)
40 pipeline.push("RETURN 1 AS n")
41 for record in pipeline.pull():
42 assert len(record) == 1
43 assert record[0] == 1
44 # TODO: why does pipeline result not look like a regular result?
45 # assert record["n"] == 1
46 # with pytest.raises(KeyError):
47 # _ = record["x"]
48 # assert record["n"] == 1
49 # with pytest.raises(KeyError):
50 # _ = record["x"]
51 with pytest.raises(TypeError):
52 _ = record[object()]
53 assert repr(record)
54 assert len(record) == 1
55 pipeline.close()
56
57
58 def test_can_run_simple_statement_with_params(bolt_driver):
59 pipeline = bolt_driver.pipeline(flush_every=0)
60 count = 0
61 pipeline.push("RETURN $x AS n", {"x": {"abc": ["d", "e", "f"]}})
62 for record in pipeline.pull():
63 assert record[0] == {"abc": ["d", "e", "f"]}
64 # TODO: why does pipeline result not look like a regular result?
65 # assert record["n"] == {"abc": ["d", "e", "f"]}
66 assert repr(record)
67 assert len(record) == 1
68 count += 1
69 pipeline.close()
70 assert count == 1
71
72
73 def test_can_run_write_statement_with_no_return(driver):
74 pipeline = driver.pipeline(flush_every=0)
75 count = 0
76 test_uid = str(uuid4())
77 pipeline.push("CREATE (a:Person {uid:$test_uid})", dict(test_uid=test_uid))
78
79 for _ in pipeline.pull():
80 raise Exception("Should not return any results from create with no return")
81 # Note you still have to consume the generator if you want to be allowed to pull from the pipeline again even
82 # though it doesn't apparently return any items.
83
84 pipeline.push("MATCH (a:Person {uid:$test_uid}) RETURN a LIMIT 1", dict(test_uid=test_uid))
85 for _ in pipeline.pull():
86 count += 1
87 pipeline.close()
88 assert count == 1
89
90
91 def test_fails_on_bad_syntax(bolt_driver):
92 pipeline = bolt_driver.pipeline(flush_every=0)
93 with pytest.raises(Neo4jError):
94 pipeline.push("X")
95 next(pipeline.pull())
96
97
98 def test_doesnt_fail_on_bad_syntax_somewhere(bolt_driver):
99 pipeline = bolt_driver.pipeline(flush_every=0)
100 pipeline.push("RETURN 1 AS n")
101 pipeline.push("X")
102 assert next(pipeline.pull())[0] == 1
103 with pytest.raises(Neo4jError):
104 next(pipeline.pull())
105
106
107 def test_fails_on_missing_parameter(bolt_driver):
108 pipeline = bolt_driver.pipeline(flush_every=0)
109 with pytest.raises(Neo4jError):
110 pipeline.push("RETURN $x")
111 next(pipeline.pull())
112
113
114 def test_can_run_simple_statement_from_bytes_string(bolt_driver):
115 pipeline = bolt_driver.pipeline(flush_every=0)
116 count = 0
117 pytest.skip("FIXME: why can't pipeline handle bytes string?")
118 pipeline.push(b"RETURN 1 AS n")
119 for record in pipeline.pull():
120 assert record[0] == 1
121 assert record["n"] == 1
122 assert repr(record)
123 assert len(record) == 1
124 count += 1
125 pipeline.close()
126 assert count == 1
127
128
129 def test_can_run_statement_that_returns_multiple_records(bolt_driver):
130 pipeline = bolt_driver.pipeline(flush_every=0)
131 count = 0
132 pipeline.push("unwind(range(1, 10)) AS z RETURN z")
133 for record in pipeline.pull():
134 assert 1 <= record[0] <= 10
135 count += 1
136 pipeline.close()
137 assert count == 10
138
139
140 def test_can_return_node(neo4j_driver):
141 with neo4j_driver.pipeline(flush_every=0) as pipeline:
142 pipeline.push("CREATE (a:Person {name:'Alice'}) RETURN a")
143 record_list = list(pipeline.pull())
144 assert len(record_list) == 1
145 for record in record_list:
146 alice = record[0]
147 print(alice)
148 pytest.skip("FIXME: why does pipeline result not look like a regular result?")
149 assert isinstance(alice, Node)
150 assert alice.labels == {"Person"}
151 assert dict(alice) == {"name": "Alice"}
152
153
154 def test_can_return_relationship(neo4j_driver):
155 with neo4j_driver.pipeline(flush_every=0) as pipeline:
156 pipeline.push("CREATE ()-[r:KNOWS {since:1999}]->() RETURN r")
157 record_list = list(pipeline.pull())
158 assert len(record_list) == 1
159 for record in record_list:
160 rel = record[0]
161 print(rel)
162 pytest.skip("FIXME: why does pipeline result not look like a regular result?")
163 assert isinstance(rel, Relationship)
164 assert rel.type == "KNOWS"
165 assert dict(rel) == {"since": 1999}
166
167
168 def test_can_return_path(neo4j_driver):
169 with neo4j_driver.pipeline(flush_every=0) as pipeline:
170 test_uid = str(uuid4())
171 pipeline.push(
172 "MERGE p=(alice:Person {name:'Alice', test_uid: $test_uid})"
173 "-[:KNOWS {test_uid: $test_uid}]->"
174 "(:Person {name:'Bob', test_uid: $test_uid})"
175 " RETURN p",
176 dict(test_uid=test_uid)
177 )
178 record_list = list(pipeline.pull())
179 assert len(record_list) == 1
180 for record in record_list:
181 path = record[0]
182 print(path)
183 pytest.skip("FIXME: why does pipeline result not look like a regular result?")
184 assert isinstance(path, Path)
185 assert path.start_node["name"] == "Alice"
186 assert path.end_node["name"] == "Bob"
187 assert path.relationships[0].type == "KNOWS"
188 assert len(path.nodes) == 2
189 assert len(path.relationships) == 1
190
191
192 def test_can_handle_cypher_error(bolt_driver):
193 with bolt_driver.pipeline(flush_every=0) as pipeline:
194 pipeline.push("X")
195 with pytest.raises(Neo4jError):
196 next(pipeline.pull())
197
198
199 def test_should_not_allow_empty_statements(bolt_driver):
200 with bolt_driver.pipeline(flush_every=0) as pipeline:
201 pipeline.push("")
202 with pytest.raises(CypherSyntaxError):
203 next(pipeline.pull())
204
205
206 def test_can_queue_multiple_statements(bolt_driver):
207 count = 0
208 with bolt_driver.pipeline(flush_every=0) as pipeline:
209 pipeline.push("unwind(range(1, 10)) AS z RETURN z")
210 pipeline.push("unwind(range(11, 20)) AS z RETURN z")
211 pipeline.push("unwind(range(21, 30)) AS z RETURN z")
212 for i in range(3):
213 for record in pipeline.pull():
214 assert (i * 10 + 1) <= record[0] <= ((i + 1) * 10)
215 count += 1
216 assert count == 30
217
218
219 def test_pull_order_exception(bolt_driver):
220 """If you try and pull when you haven't finished iterating the previous result you get an error"""
221 pipeline = bolt_driver.pipeline(flush_every=0)
222 with pytest.raises(PullOrderException):
223 pipeline.push("unwind(range(1, 10)) AS z RETURN z")
224 pipeline.push("unwind(range(11, 20)) AS z RETURN z")
225 generator_one = pipeline.pull()
226 generator_two = pipeline.pull()
227
228
229 def test_pipeline_can_read_own_writes(neo4j_driver):
230 """I am not sure that we _should_ guarantee this"""
231 count = 0
232 with neo4j_driver.pipeline(flush_every=0) as pipeline:
233 test_uid = str(uuid4())
234 pipeline.push(
235 "CREATE (a:Person {name:'Alice', test_uid: $test_uid})",
236 dict(test_uid=test_uid)
237 )
238 pipeline.push(
239 "MATCH (alice:Person {name:'Alice', test_uid: $test_uid}) "
240 "MERGE (alice)"
241 "-[:KNOWS {test_uid: $test_uid}]->"
242 "(:Person {name:'Bob', test_uid: $test_uid})",
243 dict(test_uid=test_uid)
244 )
245 pipeline.push("MATCH (n:Person {test_uid: $test_uid}) RETURN n", dict(test_uid=test_uid))
246 pipeline.push(
247 "MATCH"
248 " p=(:Person {test_uid: $test_uid})-[:KNOWS {test_uid: $test_uid}]->(:Person {test_uid: $test_uid})"
249 " RETURN p",
250 dict(test_uid=test_uid)
251 )
252
253 # create Alice
254 # n.b. we have to consume the result
255 assert next(pipeline.pull(), True) == True
256
257 # merge "knows Bob"
258 # n.b. we have to consume the result
259 assert next(pipeline.pull(), True) == True
260
261 # get people
262 for result in pipeline.pull():
263 count += 1
264
265 assert len(result) == 1
266 person = result[0]
267 print(person)
268 assert isinstance(person, Structure)
269 assert person.tag == b'N'
270 print(person.fields)
271 assert set(person.fields[1]) == {"Person"}
272
273 print(count)
274 assert count == 2
275
276 # get path
277 for result in pipeline.pull():
278 count += 1
279
280 assert len(result) == 1
281 path = result[0]
282 print(path)
283 assert isinstance(path, Structure)
284 assert path.tag == b'P'
285
286 # TODO: return Path / Node / Rel instances rather than Structures
287 # assert isinstance(path, Path)
288 # assert path.start_node["name"] == "Alice"
289 # assert path.end_node["name"] == "Bob"
290 # assert path.relationships[0].type == "KNOWS"
291 # assert len(path.nodes) == 2
292 # assert len(path.relationships) == 1
293
294 assert count == 3
295
296
297 def test_automatic_reset_after_failure(bolt_driver):
298 with bolt_driver.pipeline(flush_every=0) as pipeline:
299 try:
300 pipeline.push("X")
301 next(pipeline.pull())
302 except Neo4jError:
303 pipeline.push("RETURN 1")
304 record = next(pipeline.pull())
305 assert record[0] == 1
306 else:
307 assert False, "A Cypher error should have occurred"
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 import pytest
21
22 from neo4j.exceptions import ServiceUnavailable
23 from neo4j._exceptions import BoltHandshakeError
24
25 # python -m pytest tests/integration/test_readme.py -s -v
26
27
28 def test_should_run_readme(uri, auth):
29 names = set()
30 print = names.add
31
32 from neo4j import GraphDatabase
33
34 try:
35 driver = GraphDatabase.driver(uri, auth=auth)
36 except ServiceUnavailable as error:
37 if isinstance(error.__cause__, BoltHandshakeError):
38 pytest.skip(error.args[0])
39
40 def print_friends(tx, name):
41 for record in tx.run("MATCH (a:Person)-[:KNOWS]->(friend) "
42 "WHERE a.name = $name "
43 "RETURN friend.name", name=name):
44 print(record["friend.name"])
45
46 with driver.session() as session:
47 session.run("MATCH (a) DETACH DELETE a")
48 session.run("CREATE (a:Person {name:'Alice'})-[:KNOWS]->({name:'Bob'})")
49 session.read_transaction(print_friends, "Alice")
50
51 driver.close()
52 assert len(names) == 1
53 assert "Bob" in names
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23
24 from neo4j.exceptions import Neo4jError
25
26
27 def test_can_consume_result_immediately(session):
28
29 def f(tx):
30 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
31 assert [record[0] for record in result] == [1, 2, 3]
32
33 session.read_transaction(f)
34
35
36 def test_can_consume_result_from_buffer(session):
37
38 def f(tx):
39 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
40 result._buffer_all()
41 assert [record[0] for record in result] == [1, 2, 3]
42
43 session.read_transaction(f)
44
45
46 @pytest.mark.skip(reason="This behaviour have changed in 4.0. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
47 def test_can_consume_result_after_commit(session):
48 tx = session.begin_transaction()
49 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
50 tx.commit()
51 assert [record[0] for record in result] == [1, 2, 3]
52
53
54 @pytest.mark.skip(reason="This behaviour have changed in 4.0. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
55 def test_can_consume_result_after_rollback(session):
56 tx = session.begin_transaction()
57 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
58 tx.rollback()
59 assert [record[0] for record in result] == [1, 2, 3]
60
61
62 @pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
63 def test_can_consume_result_after_session_close(bolt_driver):
64 with bolt_driver.session() as session:
65 tx = session.begin_transaction()
66 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
67 tx.commit()
68 assert [record[0] for record in result] == [1, 2, 3]
69
70
71 @pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
72 def test_can_consume_result_after_session_reuse(bolt_driver):
73 session = bolt_driver.session()
74 tx = session.begin_transaction()
75 result_a = tx.run("UNWIND range(1, 3) AS n RETURN n")
76 tx.commit()
77 session.close()
78 session = bolt_driver.session()
79 tx = session.begin_transaction()
80 result_b = tx.run("UNWIND range(4, 6) AS n RETURN n")
81 tx.commit()
82 session.close()
83 assert [record[0] for record in result_a] == [1, 2, 3]
84 assert [record[0] for record in result_b] == [4, 5, 6]
85
86
87 def test_can_consume_results_after_harsh_session_death(bolt_driver):
88 session = bolt_driver.session()
89 result_a = session.run("UNWIND range(1, 3) AS n RETURN n")
90 del session
91 session = bolt_driver.session()
92 result_b = session.run("UNWIND range(4, 6) AS n RETURN n")
93 del session
94 assert [record[0] for record in result_a] == [1, 2, 3]
95 assert [record[0] for record in result_b] == [4, 5, 6]
96
97
98 @pytest.mark.skip(reason="This behaviour have changed. transaction.commit/rollback -> DISCARD n=-1, COMMIT/ROLL_BACK")
99 def test_can_consume_result_after_session_with_error(bolt_driver):
100 session = bolt_driver.session()
101 with pytest.raises(Neo4jError):
102 session.run("X").consume()
103 session.close()
104 session = bolt_driver.session()
105 tx = session.begin_transaction()
106 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
107 tx.commit()
108 session.close()
109 assert [record[0] for record in result] == [1, 2, 3]
110
111
112 def test_single_with_exactly_one_record(session):
113 result = session.run("UNWIND range(1, 1) AS n RETURN n")
114 record = result.single()
115 assert list(record.values()) == [1]
116
117
118 # def test_value_with_no_records(session):
119 # result = session.run("CREATE ()")
120 # assert result.value() == []
121 #
122 #
123 # def test_values_with_no_records(session):
124 # result = session.run("CREATE ()")
125 # assert result.values() == []
126
127
128 def test_peek_can_look_one_ahead(session):
129 result = session.run("UNWIND range(1, 3) AS n RETURN n")
130 record = result.peek()
131 assert list(record.values()) == [1]
132
133
134 def test_peek_fails_if_nothing_remains(neo4j_driver):
135 with neo4j_driver.session() as session:
136 result = session.run("CREATE ()")
137 upcoming = result.peek()
138 assert upcoming is None
139
140
141 def test_peek_does_not_advance_cursor(session):
142 result = session.run("UNWIND range(1, 3) AS n RETURN n")
143 result.peek()
144 assert [record[0] for record in result] == [1, 2, 3]
145
146
147 def test_peek_at_different_stages(session):
148 result = session.run("UNWIND range(0, 9) AS n RETURN n")
149 # Peek ahead to the first record
150 expected_next = 0
151 upcoming = result.peek()
152 assert upcoming[0] == expected_next
153 # Then look through all the other records
154 for expected, record in enumerate(result):
155 # Check this record is as expected
156 assert record[0] == expected
157 # Check the upcoming record is as expected...
158 if expected < 9:
159 # ...when one should follow
160 expected_next = expected + 1
161 upcoming = result.peek()
162 assert upcoming[0] == expected_next
163 else:
164 # ...when none should follow
165 upcoming = result.peek()
166 assert upcoming is None
167
168
169 def test_can_safely_exit_session_without_consuming_result(session):
170 session.run("RETURN 1")
171 assert True
172
173
174 def test_multiple_record_value_case_a(session):
175 result = session.run("UNWIND range(1, 3) AS n "
176 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
177 values = []
178 for record in result:
179 values.append(record.value(key=0, default=None))
180 assert values == [1, 2, 3]
181
182
183 def test_multiple_record_value_case_b(session):
184 result = session.run("UNWIND range(1, 3) AS n "
185 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
186 values = []
187 for record in result:
188 values.append(record.value(key=2, default=None))
189 assert values == [3, 6, 9]
190
191
192 def test_multiple_record_value_case_c(session):
193 result = session.run("UNWIND range(1, 3) AS n "
194 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
195 values = []
196 for record in result:
197 values.append(record.value(key="z", default=None))
198 assert values == [3, 6, 9]
199
200
201 def test_record_values_case_a(session):
202 result = session.run("UNWIND range(1, 3) AS n "
203 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
204 values = []
205 for record in result:
206 values.append(record.values())
207 assert values == [[1, 2, 3], [2, 4, 6], [3, 6, 9]]
208
209
210 def test_record_values_case_b(session):
211 result = session.run("UNWIND range(1, 3) AS n "
212 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
213 values = []
214 for record in result:
215 values.append(record.values(2, 0))
216 assert values == [[3, 1], [6, 2], [9, 3]]
217
218
219 def test_record_values_case_c(session):
220 result = session.run("UNWIND range(1, 3) AS n "
221 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
222 values = []
223 for record in result:
224 values.append(record.values("z", "x"))
225 assert values == [[3, 1], [6, 2], [9, 3]]
226
227
228 def test_no_records(neo4j_driver):
229 with neo4j_driver.session() as session:
230 result = session.run("CREATE ()")
231 values = []
232 for record in result:
233 values.append(record.value())
234 assert values == []
235
236
237 def test_result_single_with_no_records(session):
238 result = session.run("CREATE ()")
239 record = result.single()
240 assert record is None
241
242
243 def test_result_single_with_one_record(session):
244 result = session.run("UNWIND [1] AS n RETURN n")
245 record = result.single()
246 assert record["n"] == 1
247
248
249 def test_result_single_with_multiple_records(session):
250 import warnings
251 result = session.run("UNWIND [1, 2, 3] AS n RETURN n")
252 with pytest.warns(UserWarning, match="Expected a result with a single record"):
253 record = result.single()
254 assert record[0] == 1
255
256
257 def test_result_single_consumes_the_result(session):
258 result = session.run("UNWIND [1, 2, 3] AS n RETURN n")
259 with pytest.warns(UserWarning, match="Expected a result with a single record"):
260 _ = result.single()
261 records = list(result)
262 assert records == []
263
264
265 def test_single_value(session):
266 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
267 assert result.single().value() == 1
268
269
270 def test_single_indexed_value(session):
271 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
272 assert result.single().value(2) == 3
273
274
275 def test_single_keyed_value(session):
276 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
277 assert result.single().value("z") == 3
278
279
280 def test_single_values(session):
281 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
282 assert result.single().values() == [1, 2, 3]
283
284
285 def test_single_indexed_values(session):
286 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
287 assert result.single().values(2, 0) == [3, 1]
288
289
290 def test_single_keyed_values(session):
291 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
292 assert result.single().values("z", "x") == [3, 1]
293
294
295 def test_result_with_helper_function_value(session):
296
297 def f(tx):
298 result = tx.run("UNWIND range(1, 3) AS n RETURN n")
299 assert result.value(0) == [1, 2, 3]
300
301 session.read_transaction(f)
302
303
304 def test_result_with_helper_function_values(session):
305
306 def f(tx):
307 result = tx.run("UNWIND range(1, 3) AS n RETURN n, 0")
308 assert result.values(0, 1) == [[1, 0], [2, 0], [3, 0]]
309
310 session.read_transaction(f)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 def test_data_with_one_key_and_no_records(session):
22 result = session.run("UNWIND range(1, 0) AS n RETURN n")
23 data = [record.data() for record in result]
24 assert data == []
25
26
27 def test_data_with_one_key_and_no_records_with_helper_function(session):
28 result = session.run("UNWIND range(1, 0) AS n RETURN n")
29 assert result.data() == []
30
31
32 def test_multiple_data(session):
33 result = session.run("UNWIND range(1, 3) AS n "
34 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
35 data = [record.data() for record in result]
36 assert data == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}]
37
38
39 def test_multiple_data_with_helper_function(session):
40 result = session.run("UNWIND range(1, 3) AS n "
41 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
42 assert result.data() == [{"x": 1, "y": 2, "z": 3}, {"x": 2, "y": 4, "z": 6}, {"x": 3, "y": 6, "z": 9}]
43
44
45 def test_multiple_indexed_data(session):
46 result = session.run("UNWIND range(1, 3) AS n "
47 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
48 data = [record.data(2, 0) for record in result]
49 assert data == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}]
50
51
52 def test_multiple_indexed_data_with_helper_function(session):
53 result = session.run("UNWIND range(1, 3) AS n "
54 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
55 assert result.data(2, 0) == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}]
56
57
58 def test_multiple_keyed_data(session):
59 result = session.run("UNWIND range(1, 3) AS n "
60 "RETURN 1 * n AS x, 2 * n AS y, 3 * n AS z")
61 data = [record.data("z", "x") for record in result]
62 assert data == [{"x": 1, "z": 3}, {"x": 2, "z": 6}, {"x": 3, "z": 9}]
63
64
65 def test_single_data(session):
66 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
67 assert result.single().data() == {"x": 1, "y": 2, "z": 3}
68
69
70 def test_single_indexed_data(session):
71 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
72 assert result.single().data(2, 0) == {"x": 1, "z": 3}
73
74
75 def test_single_keyed_data(session):
76 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
77 assert result.single().data("z", "x") == {"x": 1, "z": 3}
78
79
80 def test_none(session):
81 result = session.run("RETURN null AS x")
82 data = [record.data() for record in result]
83 assert data == [{"x": None}]
84
85
86 def test_bool(session):
87 result = session.run("RETURN true AS x, false AS y")
88 data = [record.data() for record in result]
89 assert data == [{"x": True, "y": False}]
90
91
92 def test_int(session):
93 result = session.run("RETURN 1 AS x, 2 AS y, 3 AS z")
94 data = [record.data() for record in result]
95 assert data == [{"x": 1, "y": 2, "z": 3}]
96
97
98 def test_float(session):
99 result = session.run("RETURN 0.0 AS x, 1.0 AS y, 3.141592653589 AS z")
100 data = [record.data() for record in result]
101 assert data == [{"x": 0.0, "y": 1.0, "z": 3.141592653589}]
102
103
104 def test_string(session):
105 result = session.run("RETURN 'hello, world' AS x")
106 data = [record.data() for record in result]
107 assert data == [{"x": "hello, world"}]
108
109
110 def test_byte_array(session):
111 result = session.run("RETURN $x AS x", x=bytearray([1, 2, 3]))
112 data = [record.data() for record in result]
113 assert data == [{"x": bytearray([1, 2, 3])}]
114
115
116 def test_list(session):
117 result = session.run("RETURN [1, 2, 3] AS x")
118 data = [record.data() for record in result]
119 assert data == [{"x": [1, 2, 3]}]
120
121
122 def test_dict(session):
123 result = session.run("RETURN {one: 1, two: 2} AS x")
124 data = [record.data() for record in result]
125 assert data == [{"x": {"one": 1, "two": 2}}]
126
127
128 def test_node(session):
129 result = session.run("CREATE (x:Person {name:'Alice'}) RETURN x")
130 data = [record.data() for record in result]
131 assert data == [{"x": {"name": "Alice"}}]
132
133
134 def test_relationship_with_pre_known_nodes(session):
135 result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
136 "RETURN a, b, x")
137 data = [record.data() for record in result]
138 assert data == [{"a": {"name": "Alice"}, "b": {"name": "Bob"}, "x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"})}]
139
140
141 def test_relationship_with_post_known_nodes(session):
142 result = session.run("CREATE (a:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
143 "RETURN x, a, b")
144 data = [record.data() for record in result]
145 assert data == [{"x": ({"name": "Alice"}, "KNOWS", {"name": "Bob"}), "a": {"name": "Alice"}, "b": {"name": "Bob"}}]
146
147
148 def test_relationship_with_unknown_nodes(session):
149 result = session.run("CREATE (:Person {name:'Alice'})-[x:KNOWS {since:1999}]->(:Person {name:'Bob'}) "
150 "RETURN x")
151 data = [record.data() for record in result]
152 assert data == [{"x": ({}, "KNOWS", {})}]
153
154
155 def test_path(session):
156 result = session.run("CREATE x = (a:Person {name:'Alice'})-[:KNOWS {since:1999}]->(b:Person {name:'Bob'}) "
157 "RETURN x")
158 data = [record.data() for record in result]
159 assert data == [{"x": [{"name": "Alice"}, "KNOWS", {"name": "Bob"}]}]
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.graph import (
24 Node,
25 Relationship,
26 Graph,
27 Path,
28 )
29 from neo4j.exceptions import Neo4jError
30
31
32 def test_result_graph_instance(session):
33 # python -m pytest tests/integration/test_result_graph.py -s -v -k test_result_graph_instance
34 result = session.run("RETURN 1")
35 graph = result.graph()
36
37 assert isinstance(graph, Graph)
38
39
40 def test_result_graph_case_1(session):
41 # python -m pytest tests/integration/test_result_graph.py -s -v -k test_result_graph_case_1
42 result = session.run("CREATE (n1:Person:LabelTest1 {name:'Alice'})-[r1:KNOWS {since:1999}]->(n2:Person:LabelTest2 {name:'Bob'}) RETURN n1, r1, n2")
43 graph = result.graph()
44 assert isinstance(graph, Graph)
45
46 node_view = graph.nodes
47 relationships_view = graph.relationships
48
49 for node in node_view:
50 name = node["name"]
51 if name == "Alice":
52 assert node.labels == frozenset(["Person", "LabelTest1"])
53 elif name == "Bob":
54 assert node.labels == frozenset(["Person", "LabelTest2"])
55 else:
56 pytest.fail("should only contain 2 nodes, Alice and Bob. {}".format(name))
57
58 for relationship in relationships_view:
59 since = relationship["since"]
60 assert since == 1999
61 assert relationship.type == "KNOWS"
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23
24 from neo4j.spatial import (
25 CartesianPoint,
26 WGS84Point,
27 )
28
29
30 def test_cartesian_point_input(cypher_eval):
31 x, y = cypher_eval("CYPHER runtime=interpreted "
32 "WITH $point AS point "
33 "RETURN [point.x, point.y]",
34 point=CartesianPoint((1.23, 4.56)))
35 assert x == 1.23
36 assert y == 4.56
37
38
39 def test_cartesian_3d_point_input(cypher_eval):
40 x, y, z = cypher_eval("CYPHER runtime=interpreted "
41 "WITH $point AS point "
42 "RETURN [point.x, point.y, point.z]",
43 point=CartesianPoint((1.23, 4.56, 7.89)))
44 assert x == 1.23
45 assert y == 4.56
46 assert z == 7.89
47
48
49 def test_wgs84_point_input(cypher_eval):
50 lat, long = cypher_eval("CYPHER runtime=interpreted "
51 "WITH $point AS point "
52 "RETURN [point.latitude, point.longitude]",
53 point=WGS84Point((1.23, 4.56)))
54 assert long == 1.23
55 assert lat == 4.56
56
57
58 def test_wgs84_3d_point_input(cypher_eval):
59 lat, long, height = cypher_eval("CYPHER runtime=interpreted "
60 "WITH $point AS point "
61 "RETURN [point.latitude, point.longitude, "
62 "point.height]",
63 point=WGS84Point((1.23, 4.56, 7.89)))
64 assert long == 1.23
65 assert lat == 4.56
66 assert height == 7.89
67
68
69 def test_point_array_input(cypher_eval):
70 data = [WGS84Point((1.23, 4.56)), WGS84Point((9.87, 6.54))]
71 value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
72 assert value == data
73
74
75 def test_cartesian_point_output(cypher_eval):
76 value = cypher_eval("RETURN point({x:3, y:4})")
77 assert isinstance(value, CartesianPoint)
78 assert value.x == 3.0
79 assert value.y == 4.0
80 with pytest.raises(AttributeError):
81 _ = value.z
82
83
84 def test_cartesian_3d_point_output(cypher_eval):
85 value = cypher_eval("RETURN point({x:3, y:4, z:5})")
86 assert isinstance(value, CartesianPoint)
87 assert value.x == 3.0
88 assert value.y == 4.0
89 assert value.z == 5.0
90
91
92 def test_wgs84_point_output(cypher_eval):
93 value = cypher_eval("RETURN point({latitude:3, longitude:4})")
94 assert isinstance(value, WGS84Point)
95 assert value.latitude == 3.0
96 assert value.y == 3.0
97 assert value.longitude == 4.0
98 assert value.x == 4.0
99 with pytest.raises(AttributeError):
100 _ = value.height
101 with pytest.raises(AttributeError):
102 _ = value.z
103
104
105 def test_wgs84_3d_point_output(cypher_eval):
106 value = cypher_eval("RETURN point({latitude:3, longitude:4, height:5})")
107 assert isinstance(value, WGS84Point)
108 assert value.latitude == 3.0
109 assert value.y == 3.0
110 assert value.longitude == 4.0
111 assert value.x == 4.0
112 assert value.height == 5.0
113 assert value.z == 5.0
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20 import pytest
21
22 from neo4j import (
23 ResultSummary,
24 SummaryCounters,
25 GraphDatabase,
26 )
27 from neo4j.exceptions import (
28 ServiceUnavailable,
29 )
30 from neo4j._exceptions import (
31 BoltHandshakeError,
32 )
33
34
35 def test_can_obtain_summary_after_consuming_result(session):
36 # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_summary_after_consuming_result
37
38 result = session.run("CREATE (n) RETURN n")
39 summary = result.consume()
40 assert summary.query == "CREATE (n) RETURN n"
41 assert summary.parameters == {}
42 assert summary.query_type == "rw"
43 assert summary.counters.nodes_created == 1
44
45
46 def test_no_plan_info(session):
47 result = session.run("CREATE (n) RETURN n")
48 summary = result.consume()
49 assert summary.plan is None
50 assert summary.profile is None
51
52
53 def test_can_obtain_plan_info(session):
54 # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_plan_info
55 result = session.run("EXPLAIN CREATE (n) RETURN n")
56 summary = result.consume()
57 assert isinstance(summary.plan, dict)
58
59
60 def test_can_obtain_profile_info(session):
61 # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_profile_info
62 result = session.run("PROFILE CREATE (n) RETURN n")
63 summary = result.consume()
64 assert isinstance(summary.profile, dict)
65
66
67 def test_no_notification_info(session):
68 # python -m pytest tests/integration/test_summary.py -s -v -k test_no_notification_info
69 result = session.run("CREATE (n) RETURN n")
70 summary = result.consume()
71 assert summary.notifications is None
72
73
74 def test_can_obtain_notification_info(session):
75 # python -m pytest tests/integration/test_summary.py -s -v -k test_can_obtain_notification_info
76 result = session.run("EXPLAIN MATCH (n), (m) RETURN n, m")
77 summary = result.consume()
78 assert isinstance(summary, ResultSummary)
79
80 notifications = summary.notifications
81 assert isinstance(notifications, list)
82 assert len(notifications) == 1
83 assert isinstance(notifications[0], dict)
84
85
86 def test_contains_time_information(session):
87 summary = session.run("UNWIND range(1,1000) AS n RETURN n AS number").consume()
88
89 assert isinstance(summary.result_available_after, int)
90 assert isinstance(summary.result_consumed_after, int)
91
92 with pytest.raises(AttributeError) as ex:
93 assert isinstance(summary.t_first, int)
94
95 with pytest.raises(AttributeError) as ex:
96 assert isinstance(summary.t_last, int)
97
98
99 def test_protocol_version_information(session):
100 summary = session.run("UNWIND range(1,100) AS n RETURN n AS number").consume()
101
102 assert isinstance(summary.server.protocol_version, tuple)
103 assert isinstance(summary.server.protocol_version[0], int)
104 assert isinstance(summary.server.protocol_version[1], int)
105
106
107 def test_summary_counters_case_1(session):
108 # python -m pytest tests/integration/test_summary.py -s -v -k test_summary_counters_case_1
109
110 result = session.run("RETURN $number AS x", number=3)
111 summary = result.consume()
112
113 assert summary.query == "RETURN $number AS x"
114 assert summary.parameters == {"number": 3}
115
116 assert isinstance(summary.query_type, str)
117
118 counters = summary.counters
119
120 assert isinstance(counters, SummaryCounters)
121 assert counters.nodes_created == 0
122 assert counters.nodes_deleted == 0
123 assert counters.relationships_created == 0
124 assert counters.relationships_deleted == 0
125 assert counters.properties_set == 0
126 assert counters.labels_added == 0
127 assert counters.labels_removed == 0
128 assert counters.indexes_added == 0
129 assert counters.indexes_removed == 0
130 assert counters.constraints_added == 0
131 assert counters.constraints_removed == 0
132 assert counters.contains_updates is False
133
134 assert counters.system_updates == 0
135 assert counters.contains_system_updates is False
136
137
138 def test_summary_counters_case_2(neo4j_uri, auth, target):
139 # python -m pytest tests/integration/test_summary.py -s -v -k test_summary_counters_case_2
140 try:
141 with GraphDatabase.driver(neo4j_uri, auth=auth) as driver:
142
143 with driver.session(database="system") as session:
144 session.run("DROP DATABASE test IF EXISTS").consume()
145
146 # SHOW DATABASES
147
148 result = session.run("SHOW DATABASES")
149 databases = set()
150 for record in result:
151 databases.add(record.get("name"))
152 assert "system" in databases
153 assert "neo4j" in databases
154
155 summary = result.consume()
156
157 assert summary.query == "SHOW DATABASES"
158 assert summary.parameters == {}
159
160 assert isinstance(summary.query_type, str)
161
162 counters = summary.counters
163
164 assert isinstance(counters, SummaryCounters)
165 assert counters.nodes_created == 0
166 assert counters.nodes_deleted == 0
167 assert counters.relationships_created == 0
168 assert counters.relationships_deleted == 0
169 assert counters.properties_set == 0
170 assert counters.labels_added == 0
171 assert counters.labels_removed == 0
172 assert counters.indexes_added == 0
173 assert counters.indexes_removed == 0
174 assert counters.constraints_added == 0
175 assert counters.constraints_removed == 0
176 assert counters.contains_updates is False
177
178 assert counters.system_updates == 0
179 assert counters.contains_system_updates is False
180
181 # CREATE DATABASE test
182
183 summary = session.run("CREATE DATABASE test").consume()
184
185 assert summary.query == "CREATE DATABASE test"
186 assert summary.parameters == {}
187
188 assert isinstance(summary.query_type, str)
189
190 counters = summary.counters
191
192 assert isinstance(counters, SummaryCounters)
193 assert counters.nodes_created == 0
194 assert counters.nodes_deleted == 0
195 assert counters.relationships_created == 0
196 assert counters.relationships_deleted == 0
197 assert counters.properties_set == 0
198 assert counters.labels_added == 0
199 assert counters.labels_removed == 0
200 assert counters.indexes_added == 0
201 assert counters.indexes_removed == 0
202 assert counters.constraints_added == 0
203 assert counters.constraints_removed == 0
204 assert counters.contains_updates is False
205
206 assert counters.system_updates == 1
207 assert counters.contains_system_updates is True
208
209 with driver.session(database="test") as session:
210 summary = session.run("CREATE (n)").consume()
211 assert summary.counters.contains_updates is True
212 assert summary.counters.contains_system_updates is False
213 assert summary.counters.nodes_created == 1
214
215 with driver.session(database="test") as session:
216 summary = session.run("MATCH (n) DELETE (n)").consume()
217 assert summary.counters.contains_updates is True
218 assert summary.counters.contains_system_updates is False
219 assert summary.counters.nodes_deleted == 1
220
221 with driver.session(database="test") as session:
222 summary = session.run("CREATE ()-[:KNOWS]->()").consume()
223 assert summary.counters.contains_updates is True
224 assert summary.counters.contains_system_updates is False
225 assert summary.counters.relationships_created == 1
226
227 with driver.session(database="test") as session:
228 summary = session.run("MATCH ()-[r:KNOWS]->() DELETE r").consume()
229 assert summary.counters.contains_updates is True
230 assert summary.counters.contains_system_updates is False
231 assert summary.counters.relationships_deleted == 1
232
233 with driver.session(database="test") as session:
234 summary = session.run("CREATE (n:ALabel)").consume()
235 assert summary.counters.contains_updates is True
236 assert summary.counters.contains_system_updates is False
237 assert summary.counters.labels_added == 1
238
239 with driver.session(database="test") as session:
240 summary = session.run("MATCH (n:ALabel) REMOVE n:ALabel").consume()
241 assert summary.counters.contains_updates is True
242 assert summary.counters.contains_system_updates is False
243 assert summary.counters.labels_removed == 1
244
245 with driver.session(database="test") as session:
246 summary = session.run("CREATE (n {magic: 42})").consume()
247 assert summary.counters.contains_updates is True
248 assert summary.counters.contains_system_updates is False
249 assert summary.counters.properties_set == 1
250
251 with driver.session(database="test") as session:
252 summary = session.run("CREATE INDEX ON :ALabel(prop)").consume()
253 assert summary.counters.contains_updates is True
254 assert summary.counters.contains_system_updates is False
255 assert summary.counters.indexes_added == 1
256
257 with driver.session(database="test") as session:
258 summary = session.run("DROP INDEX ON :ALabel(prop)").consume()
259 assert summary.counters.contains_updates is True
260 assert summary.counters.contains_system_updates is False
261 assert summary.counters.indexes_removed == 1
262
263 with driver.session(database="test") as session:
264 summary = session.run("CREATE CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE").consume()
265 assert summary.counters.contains_updates is True
266 assert summary.counters.contains_system_updates is False
267 assert summary.counters.constraints_added == 1
268
269 with driver.session(database="test") as session:
270 summary = session.run("DROP CONSTRAINT ON (book:Book) ASSERT book.isbn IS UNIQUE").consume()
271 assert summary.counters.contains_updates is True
272 assert summary.counters.contains_system_updates is False
273 assert summary.counters.constraints_removed == 1
274
275 with driver.session(database="system") as session:
276 session.run("DROP DATABASE test IF EXISTS").consume()
277 except ServiceUnavailable as error:
278 if error.args[0] == "Server does not support routing":
279 # This is because a single instance Neo4j 3.5 does not have dbms.routing.cluster.getRoutingTable() call
280 pytest.skip(error.args[0])
281 elif isinstance(error.__cause__, BoltHandshakeError):
282 pytest.skip(error.args[0])
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 import datetime
23
24 from pytz import (
25 FixedOffset,
26 timezone,
27 utc,
28 )
29
30 from neo4j.exceptions import CypherTypeError
31 from neo4j.time import (
32 Date,
33 Time,
34 DateTime,
35 Duration,
36 )
37
38
39 def test_native_date_input(cypher_eval):
40 from datetime import date
41 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
42 "RETURN [x.year, x.month, x.day]",
43 x=date(1976, 6, 13))
44 year, month, day = result
45 assert year == 1976
46 assert month == 6
47 assert day == 13
48
49
50 def test_date_input(cypher_eval):
51 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
52 "RETURN [x.year, x.month, x.day]",
53 x=Date(1976, 6, 13))
54 year, month, day = result
55 assert year == 1976
56 assert month == 6
57 assert day == 13
58
59
60 def test_date_array_input(cypher_eval):
61 data = [DateTime.now().date(), Date(1976, 6, 13)]
62 value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
63 assert value == data
64
65
66 def test_native_time_input(cypher_eval):
67 from datetime import time
68 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
69 "RETURN [x.hour, x.minute, x.second, x.nanosecond]",
70 x=time(12, 34, 56, 789012))
71 hour, minute, second, nanosecond = result
72 assert hour == 12
73 assert minute == 34
74 assert second == 56
75 assert nanosecond == 789012000
76
77
78 def test_whole_second_time_input(cypher_eval):
79 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
80 "RETURN [x.hour, x.minute, x.second]",
81 x=Time(12, 34, 56))
82 hour, minute, second = result
83 assert hour == 12
84 assert minute == 34
85 assert second == 56
86
87
88 def test_nanosecond_resolution_time_input(cypher_eval):
89 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
90 "RETURN [x.hour, x.minute, x.second, x.nanosecond]",
91 x=Time(12, 34, 56.789012345))
92 hour, minute, second, nanosecond = result
93 assert hour == 12
94 assert minute == 34
95 assert second == 56
96 assert nanosecond == 789012345
97
98
99 def test_time_with_numeric_time_offset_input(cypher_eval):
100 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
101 "RETURN [x.hour, x.minute, x.second, "
102 " x.nanosecond, x.offset]",
103 x=Time(12, 34, 56.789012345, tzinfo=FixedOffset(90)))
104 hour, minute, second, nanosecond, offset = result
105 assert hour == 12
106 assert minute == 34
107 assert second == 56
108 assert nanosecond == 789012345
109 assert offset == "+01:30"
110
111
112 def test_time_array_input(cypher_eval):
113 data = [Time(12, 34, 56), Time(10, 0, 0)]
114 value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
115 assert value == data
116
117
118 def test_native_datetime_input(cypher_eval):
119 from datetime import datetime
120 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
121 "RETURN [x.year, x.month, x.day, "
122 " x.hour, x.minute, x.second, x.nanosecond]",
123 x=datetime(1976, 6, 13, 12, 34, 56, 789012))
124 year, month, day, hour, minute, second, nanosecond = result
125 assert year == 1976
126 assert month == 6
127 assert day == 13
128 assert hour == 12
129 assert minute == 34
130 assert second == 56
131 assert nanosecond == 789012000
132
133
134 def test_whole_second_datetime_input(cypher_eval):
135 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
136 "RETURN [x.year, x.month, x.day, "
137 " x.hour, x.minute, x.second]",
138 x=DateTime(1976, 6, 13, 12, 34, 56))
139 year, month, day, hour, minute, second = result
140 assert year == 1976
141 assert month == 6
142 assert day == 13
143 assert hour == 12
144 assert minute == 34
145 assert second == 56
146
147
148 def test_nanosecond_resolution_datetime_input(cypher_eval):
149 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
150 "RETURN [x.year, x.month, x.day, "
151 " x.hour, x.minute, x.second, x.nanosecond]",
152 x=DateTime(1976, 6, 13, 12, 34, 56.789012345))
153 year, month, day, hour, minute, second, nanosecond = result
154 assert year == 1976
155 assert month == 6
156 assert day == 13
157 assert hour == 12
158 assert minute == 34
159 assert second == 56
160 assert nanosecond == 789012345
161
162
163 def test_datetime_with_numeric_time_offset_input(cypher_eval):
164 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
165 "RETURN [x.year, x.month, x.day, "
166 " x.hour, x.minute, x.second, "
167 " x.nanosecond, x.offset]",
168 x=DateTime(1976, 6, 13, 12, 34, 56.789012345,
169 tzinfo=FixedOffset(90)))
170 year, month, day, hour, minute, second, nanosecond, offset = result
171 assert year == 1976
172 assert month == 6
173 assert day == 13
174 assert hour == 12
175 assert minute == 34
176 assert second == 56
177 assert nanosecond == 789012345
178 assert offset == "+01:30"
179
180
181 def test_datetime_with_named_time_zone_input(cypher_eval):
182 dt = DateTime(1976, 6, 13, 12, 34, 56.789012345)
183 input_value = timezone("US/Pacific").localize(dt)
184 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
185 "RETURN [x.year, x.month, x.day, "
186 " x.hour, x.minute, x.second, "
187 " x.nanosecond, x.timezone]",
188 x=input_value)
189 year, month, day, hour, minute, second, nanosecond, tz = result
190 assert year == input_value.year
191 assert month == input_value.month
192 assert day == input_value.day
193 assert hour == input_value.hour
194 assert minute == input_value.minute
195 assert second == int(input_value.second)
196 assert nanosecond == int(1000000000 * input_value.second % 1000000000)
197 assert tz == input_value.tzinfo.zone
198
199
200 def test_datetime_array_input(cypher_eval):
201 data = [DateTime(2018, 4, 6, 13, 4, 42.516120), DateTime(1976, 6, 13)]
202 value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
203 assert value == data
204
205
206 def test_duration_input(cypher_eval):
207 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
208 "RETURN [x.months, x.days, x.seconds, "
209 " x.microsecondsOfSecond]",
210 x=Duration(years=1, months=2, days=3, hours=4,
211 minutes=5, seconds=6.789012))
212 months, days, seconds, microseconds = result
213 assert months == 14
214 assert days == 3
215 assert seconds == 14706
216 assert microseconds == 789012
217
218
219 def test_duration_array_input(cypher_eval):
220 data = [Duration(1, 2, 3, 4, 5, 6), Duration(9, 8, 7, 6, 5, 4)]
221 value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
222 assert value == data
223
224
225 def test_timedelta_input(cypher_eval):
226 from datetime import timedelta
227 result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x "
228 "RETURN [x.months, x.days, x.seconds, "
229 " x.microsecondsOfSecond]",
230 x=timedelta(days=3, hours=4, minutes=5,
231 seconds=6.789012))
232 months, days, seconds, microseconds = result
233 assert months == 0
234 assert days == 3
235 assert seconds == 14706
236 assert microseconds == 789012
237
238
239 def test_mixed_array_input(cypher_eval):
240 data = [Date(1976, 6, 13), Duration(9, 8, 7, 6, 5, 4)]
241 with pytest.raises(CypherTypeError):
242 _ = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data)
243
244
245 def test_date_output(cypher_eval):
246 value = cypher_eval("RETURN date('1976-06-13')")
247 assert isinstance(value, Date)
248 assert value == Date(1976, 6, 13)
249
250
251 def test_whole_second_time_output(cypher_eval):
252 value = cypher_eval("RETURN time('12:34:56')")
253 assert isinstance(value, Time)
254 assert value == Time(12, 34, 56, tzinfo=FixedOffset(0))
255
256
257 def test_nanosecond_resolution_time_output(cypher_eval):
258 value = cypher_eval("RETURN time('12:34:56.789012345')")
259 assert isinstance(value, Time)
260 assert value == Time(12, 34, 56.789012345, tzinfo=FixedOffset(0))
261
262
263 def test_time_with_numeric_time_offset_output(cypher_eval):
264 value = cypher_eval("RETURN time('12:34:56.789012345+0130')")
265 assert isinstance(value, Time)
266 assert value == Time(12, 34, 56.789012345, tzinfo=FixedOffset(90))
267
268
269 def test_whole_second_localtime_output(cypher_eval):
270 value = cypher_eval("RETURN localtime('12:34:56')")
271 assert isinstance(value, Time)
272 assert value == Time(12, 34, 56)
273
274
275 def test_nanosecond_resolution_localtime_output(cypher_eval):
276 value = cypher_eval("RETURN localtime('12:34:56.789012345')")
277 assert isinstance(value, Time)
278 assert value == Time(12, 34, 56.789012345)
279
280
281 def test_whole_second_datetime_output(cypher_eval):
282 value = cypher_eval("RETURN datetime('1976-06-13T12:34:56')")
283 assert isinstance(value, DateTime)
284 assert value == DateTime(1976, 6, 13, 12, 34, 56, tzinfo=utc)
285
286
287 def test_nanosecond_resolution_datetime_output(cypher_eval):
288 value = cypher_eval("RETURN datetime('1976-06-13T12:34:56.789012345')")
289 assert isinstance(value, DateTime)
290 assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345, tzinfo=utc)
291
292
293 def test_datetime_with_numeric_time_offset_output(cypher_eval):
294 value = cypher_eval("RETURN "
295 "datetime('1976-06-13T12:34:56.789012345+01:30')")
296 assert isinstance(value, DateTime)
297 assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345,
298 tzinfo=FixedOffset(90))
299
300
301 def test_datetime_with_named_time_zone_output(cypher_eval):
302 value = cypher_eval("RETURN datetime('1976-06-13T12:34:56.789012345"
303 "[Europe/London]')")
304 assert isinstance(value, DateTime)
305 dt = DateTime(1976, 6, 13, 12, 34, 56.789012345)
306 assert value == timezone("Europe/London").localize(dt)
307
308
309 def test_whole_second_localdatetime_output(cypher_eval):
310 value = cypher_eval("RETURN localdatetime('1976-06-13T12:34:56')")
311 assert isinstance(value, DateTime)
312 assert value == DateTime(1976, 6, 13, 12, 34, 56)
313
314
315 def test_nanosecond_resolution_localdatetime_output(cypher_eval):
316 value = cypher_eval("RETURN "
317 "localdatetime('1976-06-13T12:34:56.789012345')")
318 assert isinstance(value, DateTime)
319 assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345)
320
321
322 def test_duration_output(cypher_eval):
323 value = cypher_eval("RETURN duration('P1Y2M3DT4H5M6.789S')")
324 assert isinstance(value, Duration)
325 assert value == Duration(years=1, months=2, days=3, hours=4,
326 minutes=5, seconds=6.789)
327
328
329 def test_nanosecond_resolution_duration_output(cypher_eval):
330 value = cypher_eval("RETURN duration('P1Y2M3DT4H5M6.789123456S')")
331 assert isinstance(value, Duration)
332 assert value == Duration(years=1, months=2, days=3, hours=4,
333 minutes=5, seconds=6.789123456)
334
335
336 def test_datetime_parameter_case1(session):
337 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case1
338 dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790001+00:00')").single().value()
339 assert isinstance(dt1, DateTime)
340
341 dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
342 assert isinstance(dt2, DateTime)
343
344 assert dt1 == dt2
345
346
347 def test_datetime_parameter_case2(session):
348 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case2
349 dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790999[UTC]')").single().value()
350 assert isinstance(dt1, DateTime)
351 assert dt1.iso_format() == "2019-10-30T07:54:02.129790999+00:00"
352
353 dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
354 assert isinstance(dt2, DateTime)
355
356 assert dt1 == dt2
357
358
359 def test_datetime_parameter_case3(session):
360 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_datetime_parameter_case1
361 dt1 = session.run("RETURN datetime('2019-10-30T07:54:02.129790+00:00')").single().value()
362 assert isinstance(dt1, DateTime)
363
364 dt2 = session.run("RETURN $date_time", date_time=dt1).single().value()
365 assert isinstance(dt2, DateTime)
366
367 assert dt1 == dt2
368
369
370 def test_time_parameter_case1(session):
371 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case1
372 t1 = session.run("RETURN time('07:54:02.129790001+00:00')").single().value()
373 assert isinstance(t1, Time)
374
375 t2 = session.run("RETURN $time", time=t1).single().value()
376 assert isinstance(t2, Time)
377
378 assert t1 == t2
379
380
381 def test_time_parameter_case2(session):
382 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case2
383 t1 = session.run("RETURN time('07:54:02.129790999+00:00')").single().value()
384 assert isinstance(t1, Time)
385 assert t1.iso_format() == "07:54:02.129790999+00:00"
386 time_zone_delta = t1.utc_offset()
387 assert isinstance(time_zone_delta, datetime.timedelta)
388 assert time_zone_delta == datetime.timedelta(0)
389
390 t2 = session.run("RETURN $time", time=t1).single().value()
391 assert isinstance(t2, Time)
392
393 assert t1 == t2
394
395
396 def test_time_parameter_case3(session):
397 # python -m pytest tests/integration/test_temporal_types.py -s -v -k test_time_parameter_case3
398 t1 = session.run("RETURN time('07:54:02.129790+00:00')").single().value()
399 assert isinstance(t1, Time)
400
401 t2 = session.run("RETURN $time", time=t1).single().value()
402 assert isinstance(t2, Time)
403
404 assert t1 == t2
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 from uuid import uuid4
23
24 from neo4j.work.simple import unit_of_work
25 from neo4j.exceptions import (
26 Neo4jError,
27 ClientError,
28 )
29
30 # python -m pytest tests/integration/test_tx_functions.py -s -v
31
32
33 def test_simple_read(session):
34
35 def work(tx):
36 return tx.run("RETURN 1").single().value()
37
38 value = session.read_transaction(work)
39 assert value == 1
40
41
42 def test_read_with_arg(session):
43
44 def work(tx, x):
45 return tx.run("RETURN $x", x=x).single().value()
46
47 value = session.read_transaction(work, x=1)
48 assert value == 1
49
50
51 def test_read_with_arg_and_metadata(session):
52
53 @unit_of_work(timeout=25, metadata={"foo": "bar"})
54 def work(tx):
55 return tx.run("CALL dbms.getTXMetaData").single().value()
56
57 try:
58 value = session.read_transaction(work)
59 except ClientError:
60 pytest.skip("Transaction metadata and timeout only supported in Neo4j EE 3.5+")
61 else:
62 assert value == {"foo": "bar"}
63
64
65 def test_simple_write(session):
66
67 def work(tx):
68 return tx.run("CREATE (a {x: 1}) RETURN a.x").single().value()
69
70 value = session.write_transaction(work)
71 assert value == 1
72
73
74 def test_write_with_arg(session):
75
76 def work(tx, x):
77 return tx.run("CREATE (a {x: $x}) RETURN a.x", x=x).single().value()
78
79 value = session.write_transaction(work, x=1)
80 assert value == 1
81
82
83 def test_write_with_arg_and_metadata(session):
84
85 @unit_of_work(timeout=25, metadata={"foo": "bar"})
86 def work(tx, x):
87 return tx.run("CREATE (a {x: $x}) RETURN a.x", x=x).single().value()
88
89 try:
90 value = session.write_transaction(work, x=1)
91 except ClientError:
92 pytest.skip("Transaction metadata and timeout only supported in Neo4j EE 3.5+")
93 else:
94 assert value == 1
95
96
97 def test_error_on_write_transaction(session):
98
99 def f(tx, uuid):
100 tx.run("CREATE (a:Thing {uuid:$uuid})", uuid=uuid), uuid4()
101
102 with pytest.raises(TypeError):
103 session.write_transaction(f)
104
105
106 def test_retry_logic(driver):
107 # python -m pytest tests/integration/test_tx_functions.py -s -v -k test_retry_logic
108
109 pytest.global_counter = 0
110
111 def get_one(tx):
112 result = tx.run("UNWIND [1,2,3,4] AS x RETURN x")
113 records = list(result)
114 pytest.global_counter += 1
115
116 if pytest.global_counter < 3:
117 database_unavailable = Neo4jError.hydrate(message="The database is not currently available to serve your request, refer to the database logs for more details. Retrying your request at a later time may succeed.", code="Neo.TransientError.Database.DatabaseUnavailable")
118 raise database_unavailable
119
120 return records
121
122 with driver.session() as session:
123 records = session.read_transaction(get_one)
124
125 assert pytest.global_counter == 3
126
127 del pytest.global_counter
0 git+https://github.com/neo4j-drivers/[email protected]#egg=boltkit
1 coverage
2 pytest
3 pytest-benchmark
4 pytest-cov
5 teamcity-messages
(New empty file)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import subprocess
22 import os
23 import time
24
25 from platform import system
26 from threading import Thread
27 from time import sleep
28
29 from boltkit.server.stub import BoltStubService
30 from pytest import fixture
31
32 import logging
33 log = logging.getLogger("neo4j")
34
35 # from neo4j.debug import watch
36 # watch("neo4j")
37
38
39 class StubServer:
40
41 def __init__(self, port, script):
42 self.port = port
43 self.script = os.path.join(os.path.dirname(__file__), "scripts", script)
44
45 def run(self):
46 shell = system() == "Windows" # I hate myself for doing this
47 self._process = subprocess.Popen(["python", "-m", "boltkit", "stub", "-v", "-l", ":{}".format(str(self.port)), "-t", "10", self.script], stdout=subprocess.PIPE, shell=shell)
48 # Need verbose for this to work
49 line = self._process.stdout.readline().decode("utf-8")
50 log.debug("started stub server {}".format(self.port))
51 log.debug(line.strip("\n"))
52
53 def wait(self):
54 while True:
55 return_code = self._process.poll()
56 if return_code is not None:
57 line = self._process.stdout.readline().decode("utf-8")
58 if line == "":
59 break
60 log.debug(line.strip("\n"))
61
62 return True
63
64 def kill(self):
65 # Kill process if not already dead
66 if self._process.poll() is None:
67 self._process.kill()
68
69
70 class StubCluster:
71
72 def __init__(self, servers):
73 self.servers = {port: StubServer(port, script) for port, script in dict(servers).items()}
74
75 def __enter__(self):
76 self.start()
77
78 def __exit__(self, exc_type, exc_value, traceback):
79 self.wait()
80
81 def start(self):
82 for port, server in self.servers.items():
83 server.run()
84
85 def wait(self):
86 success = True
87 for port, server in self.servers.items():
88 if not server.wait():
89 success = False
90 server.kill()
91
92 if not success:
93 raise Exception("Stub server failed")
94
95
96 class LegacyStubServer(Thread):
97
98 def __init__(self, port, script):
99 super(LegacyStubServer, self).__init__()
100 self.port = port
101 self.script = os.path.join(os.path.dirname(__file__), "scripts", script)
102
103 def run(self):
104 check_call(["python", "-m", "boltkit.legacy.stub", "-v", str(self.port), self.script])
105
106
107 class LegacyStubCluster:
108
109 def __init__(self, servers):
110 self.servers = {port: LegacyStubServer(port, script) for port, script in dict(servers).items()}
111
112 def __enter__(self):
113 self.start()
114
115 def __exit__(self, exc_type, exc_value, traceback):
116 self.wait()
117
118 def start(self):
119 for port, server in self.servers.items():
120 server.start()
121 sleep(0.5)
122
123 def wait(self):
124 for port, server in self.servers.items():
125 server.join()
126
127
128 class DefaultBoltStubService(BoltStubService):
129
130 default_base_port = 9001
131
132
133 class StubCluster(StubCluster):
134
135 def __init__(self, *servers):
136 print("")
137 scripts = [os.path.join(os.path.dirname(__file__), "scripts", server) for server in servers]
138
139 bss = DefaultBoltStubService.load(*scripts)
140 servers2 = {port: script.filename for port, script in bss.scripts.items()}
141 super().__init__(servers2)
142
143 # def run():
144 # check_call(["bolt", "stub", "-v", "-t", "10", "-l", ":9001"] + scripts)
145
146 # self.thread = Thread(target=run)
147
148 # def __enter__(self):
149 # self.thread.start()
150 # sleep(0.5)
151
152 # def __exit__(self, exc_type, exc_value, traceback):
153 # self.thread.join(3)
154
155
156 @fixture
157 def script():
158 return lambda *paths: path_join(dirname(__file__), "scripts", *paths)
159
160
161 @fixture
162 def driver_info():
163 """ Base class for test cases that integrate with a server.
164 """
165 return {
166 "uri_bolt": "bolt://localhost:9001",
167 "uri_neo4j": "neo4j://localhost:9001",
168 "user": "test",
169 "password": "test",
170 "auth_token": ("test", "test")
171 }
(New empty file)
0 !: BOLT 1
1
2 C: INIT {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
3 S: SUCCESS {"server": "Neo4j/3.3.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
4 C: RESET
5 S: <EXIT>
0 !: BOLT 2
1
2 C: INIT {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
3 S: SUCCESS {"server": "Neo4j/3.4.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
4 C: RESET
5 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:0", "bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8 C: COMMIT
9 S: SUCCESS {"bookmark": "bookmark:2"}
10
11 C: BEGIN {"bookmarks": ["bookmark:2"], "mode": "r"}
12 S: SUCCESS {}
13 C: COMMIT
14 S: SUCCESS {"bookmark": "bookmark:3"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8 C: COMMIT
9 S: SUCCESS {"bookmark": "bookmark:2"}
10
11 C: RUN "RETURN 1" {} {"bookmarks": ["bookmark:2"], "mode": "r"}
12 PULL_ALL
13 S: SUCCESS {}
14 SUCCESS {"bookmark": "bookmark:3"}
15
16 C: BEGIN {"bookmarks": ["bookmark:3"], "mode": "r"}
17 S: SUCCESS {}
18 C: COMMIT
19 S: SUCCESS {"bookmark": "bookmark:4"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {}
7 PULL_ALL
8 S: FAILURE {"code": "Neo.DatabaseError.General.UnknownError", "message": "An unknown error occurred."}
9 IGNORED
10 C: RESET
11 S: SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: BEGIN {}
6 RUN "CREATE (n {name:'Bob'})" {} {}
7 PULL_ALL
8 S: SUCCESS {}
9 SUCCESS {}
10 SUCCESS {}
11 C: COMMIT
12 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: RUN "CREATE (a $x)" {"x": {"name": "Alice"}} {}
7 PULL_ALL
8 S: SUCCESS {"fields": []}
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO BEGIN {}
5 !: AUTO COMMIT
6 !: AUTO ROLLBACK
7 !: PORT 9004
8
9 C: RUN "RETURN 1" {} {"mode": "r"}
10 C: PULL_ALL
11 S: FAILURE {"code": "Neo.TransientError.General.DatabaseUnavailable", "message": "Database is busy doing store copy"}
12 S: IGNORED
0 !: BOLT 3
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
4 S: SUCCESS {"server": "Neo4j/3.5.0", "connection_id": "12345678-1234-1234-1234-123456789000"}
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [1234, [{"role":"WRITE", "addresses":["127.0.0.1:9001"]}, {"role":"READ", "addresses":["127.0.0.1:9002", "127.0.0.1:9003"]}, {"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]}]]
10 SUCCESS {}
11
12 C: GOODBYE
13 S: <EXIT>
0 !: BOLT 3
1 !: AUTO GOODBYE
2 !: AUTO RESET
3
4 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
5 S: SUCCESS {"server": "Neo4j/3.5.0", "connection_id": "bolt-0"}
6 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
7 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
0 !: BOLT 3
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
4 S: SUCCESS {"server": "Neo4j/3.5.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: GOODBYE
6 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9004
4
5 C: BEGIN {"mode": "r"}
6 S: SUCCESS {}
7
8 C: RUN "X" {} {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.ClientError.Statement.SyntaxError", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9006
4
5 C: BEGIN {}
6 S: SUCCESS {}
7
8 C: RUN "X" {} {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.ClientError.Statement.SyntaxError", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO DISCARD_ALL
5 !: AUTO BEGIN {}
6 !: AUTO COMMIT
7 !: AUTO ROLLBACK
8 !: PORT 9006
9
10 C: RUN "CREATE (n {name:'Bob'})" {} {}
11 C: PULL_ALL
12 S: FAILURE {"code": "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase", "message": "Unable to write"}
13 S: IGNORED
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"name": "molly", "age": "1", "address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [9223372036854775807, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"},{"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "RETURN 1" {} {"bookmarks": ["bookmark1"]}
6 PULL_ALL
7 S: SUCCESS {"fields": ["1"]}
8 RECORD [1]
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "RETURN 1" {} {"tx_metadata": {"foo": "bar"}}
6 PULL_ALL
7 S: SUCCESS {"fields": ["1"]}
8 RECORD [1]
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "RETURN 1" {} {"tx_timeout": 15000}
6 PULL_ALL
7 S: SUCCESS {"fields": ["1"]}
8 RECORD [1]
9 SUCCESS {}
0 !: BOLT 3
1
2 S: <RAW> 00 40 B1 70 A2
3 S: <EXIT>
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9001
4
5 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode":"r"}
6 PULL_ALL
7 S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "Not a router"}
8 IGNORED
9 C: RESET
10 S: SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO DISCARD_ALL
5 !: AUTO BEGIN {}
6 !: AUTO COMMIT
7 !: AUTO ROLLBACK
8 !: PORT 9006
9
10 C: RUN "CREATE (n {name:'Bob'})" {} {}
11 C: PULL_ALL
12 S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "Leader switched has happened"}
13 S: IGNORED
0 !: HANDSHAKE 00 00 01 00
1
2 S: <SLEEP> 1
3 S: <EXIT>
0 !: BOLT 3
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/3.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {"mode": "r"}
8 S: SUCCESS {"fields": ["x"], "t_first": 300}
9 C: PULL_ALL
10 S: RECORD [1]
11 RECORD [2]
12 RECORD [3]
13 RECORD [4]
14 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 500}
0 !: BOLT 3
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/3.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: BEGIN {"mode": "r", "tx_metadata": {"foo": "bar"}, "tx_timeout": 3000}
8 S: SUCCESS {}
9 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {}
10 S: SUCCESS {"fields": ["x"], "t_first": 300}
11 C: PULL_ALL
12 S: RECORD [1]
13 RECORD [2]
14 RECORD [3]
15 RECORD [4]
16 SUCCESS {"type": "r", "t_last": 500}
17 C: COMMIT
18 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "RETURN 1" {} {}
6 PULL_ALL
7 S: SUCCESS {"fields": ["1"]}
8 RECORD [1]
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "RETURN 1" {} {"mode": "r"}
6 PULL_ALL
7 S: SUCCESS {"fields": ["1"]}
8 RECORD [1]
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
11
12 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
13 PULL_ALL
14 S: SUCCESS {"fields": ["x"]}
15 RECORD [1]
16 SUCCESS {}
17
18 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
19 PULL_ALL
20 S: SUCCESS {"fields": ["x"]}
21 RECORD [1]
22 SUCCESS {}
23
24 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
25 PULL_ALL
26 S: SUCCESS {"fields": ["x"]}
27 RECORD [1]
28 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
17
18 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
19 S: SUCCESS {}
20
21 C: RUN "RETURN 1" {} {}
22 PULL_ALL
23 S: SUCCESS {"fields": ["1"]}
24 RECORD [1]
25 SUCCESS {}
26
27 C: COMMIT
28 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
17
18 C: BEGIN {"bookmarks": ["bookmark:1"]}
19 S: SUCCESS {}
20
21 C: RUN "RETURN 1" {} {}
22 PULL_ALL
23 S: SUCCESS {"fields": ["1"]}
24 RECORD [1]
25 SUCCESS {}
26
27 C: COMMIT
28 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9002
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9005
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/3.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL_ALL
9 S: SUCCESS {"fields": ["x"]}
10 RECORD [1]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
11
12 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
13 PULL_ALL
14 S: SUCCESS {"fields": ["x"]}
15 RECORD [1]
16 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN $x" {"x": 1} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["x"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: RUN "RETURN $x" {"x": 1} {}
16 PULL_ALL
17 S: SUCCESS {"fields": ["x"]}
18 RECORD [1]
19 SUCCESS {}
20
21 C: COMMIT
22 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 2" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["2"]}
12 RECORD [2]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {"bookmarks": ["bookmark:1"]}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 2" {} {}
10 PULL_ALL
11 S: SUCCESS {"fields": ["2"]}
12 RECORD [2]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":[]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":[]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":[]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002"]},{"role":"READ","addresses":["127.0.0.1:9001","127.0.0.1:9003"]},{"role":"WRITE","addresses":["127.0.0.1:9004"]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006","127.0.0.1:9007"]}]]
10 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN 1" {} {"mode": "r"}
7 PULL_ALL
8 S: <EXIT>
9
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: <EXIT>
9
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.cluster.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r"}
7 PULL_ALL
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9004
4
5 C: BEGIN {"mode": "r"}
6 S: SUCCESS {}
7
8 C: RUN "RETURN 1" {} {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.TransientError.Transaction.LockClientStopped", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 3
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9006
4
5 C: BEGIN {}
6 S: SUCCESS {}
7
8 C: RUN "RETURN 1" {} {}
9 PULL_ALL
10 S: FAILURE {"code": "Neo.TransientError.Transaction.LockClientStopped", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: RUN "CREATE (a $x)" {"x": {"name": "Alice"}} {}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": []}
9 SUCCESS {}
0 !: BOLT 4
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
4 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "12345678-1234-1234-1234-123456789000"}
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [1234, [{"role":"WRITE", "addresses":["127.0.0.1:9001"]}, {"role":"READ", "addresses":["127.0.0.1:9002", "127.0.0.1:9003"]}, {"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 15, "db": "system"}
11
12 C: GOODBYE
13 S: <EXIT>
0 !: BOLT 4
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
4 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "12345678-1234-1234-1234-123456789000"}
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context, $database)" {"context": {"address": "localhost:9001"}, "database": "neo4j"} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [1234, [{"role":"WRITE", "addresses":["127.0.0.1:9001"]}, {"role":"READ", "addresses":["127.0.0.1:9002", "127.0.0.1:9003"]}, {"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 15, "db": "neo4j"}
11
12 C: GOODBYE
13 S: <EXIT>
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3
4 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
5 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "bolt-0"}
6 S: <EXIT>
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL {"n": -1}
8 S: <EXIT>
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL {"n": -1}
8 S: <EXIT>
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
7 S: <EXIT>
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 S: <EXIT>
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
0 !: BOLT 4
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
4 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: GOODBYE
6 S: <EXIT>
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {"mode": "r", "db": "test"}
8 S: SUCCESS {"fields": ["x"], "t_first": 300}
9 C: PULL {"n": 2}
10 S: RECORD [1]
11 RECORD [2]
12 SUCCESS {"has_more": true}
13 C: DISCARD {"n": -1}
14 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 500, "db": "test"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {"mode": "r", "db": "test"}
8 S: SUCCESS {"fields": ["x"], "t_first": 300}
9 C: PULL {"n": 2}
10 S: RECORD [1]
11 RECORD [2]
12 SUCCESS {"has_more": true}
13 C: DISCARD {"n": -1}
14 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 500, "db": "test"}
15 C: RUN "UNWIND [5,6,7,8] AS x RETURN x" {} {"mode": "r", "db": "test", "bookmarks": ["neo4j:bookmark-test-1"]}
16 C: PULL {"n": 2}
17 S: RECORD [5]
18 RECORD [6]
19 SUCCESS {"has_more": true}
20 C: PULL {"n": 2}
21 S: RECORD [7]
22 RECORD [8]
23 S: SUCCESS {"bookmark": "neo4j:bookmark-test-2", type": "r", "t_last": 500, "db": "test"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {"mode": "r", "db": "test"}
8 S: SUCCESS {"fields": ["x"], "t_first": 300}
9 C: PULL {"n": 2}
10 S: RECORD [1]
11 RECORD [2]
12 SUCCESS {"has_more": true}
13 C: PULL {"n": 2}
14 S: RECORD [3]
15 RECORD [4]
16 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 500, "db": "test"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {"mode": "r", "db": "test"}
8 S: SUCCESS {"fields": ["x"], "t_first": 300}
9 C: PULL {"n": 2}
10 S: RECORD [1]
11 RECORD [2]
12 SUCCESS {"has_more": true}
13 C: PULL {"n": 2}
14 S: <SLEEP> 1
15 S: RECORD [3]
16 RECORD [4]
17 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 500, "db": "test"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN 1" {} {"mode": "r"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["1"]}
9 RECORD [1]
10 SUCCESS {}
11
12 C: RUN "RETURN 1" {} {"mode": "r"}
13 PULL {"n": -1}
14 S: SUCCESS {"fields": ["1"]}
15 RECORD [1]
16 SUCCESS {}
17
18 C: RUN "RETURN 1" {} {"mode": "r"}
19 PULL {"n": -1}
20 S: SUCCESS {"fields": ["1"]}
21 RECORD [1]
22 SUCCESS {}
23
24 C: RUN "RETURN 1" {} {"mode": "r"}
25 PULL {"n": -1}
26 S: SUCCESS {"fields": ["1"]}
27 RECORD [1]
28 SUCCESS {}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL {"n": -1}
9 S: SUCCESS {"fields": ["x"]}
10 RECORD [1]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9002
5
6 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN $x" {"x": 1} {"mode": "r"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9005
5
6 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["x"]}
9 RECORD [1]
10 SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: RUN "RETURN 1" {} {"mode": "r"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["1"]}
9 RECORD [1]
10 SUCCESS {}
11
12 C: RUN "RETURN 1" {} {"mode": "r"}
13 PULL {"n": -1}
14 S: SUCCESS {"fields": ["1"]}
15 RECORD [1]
16 SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [300, [{"role":"ROUTE","addresses":["127.0.0.1:9001","127.0.0.1:9002","127.0.0.1:9003"]},{"role":"READ","addresses":["127.0.0.1:9004","127.0.0.1:9005"]},{"role":"WRITE","addresses":["127.0.0.1:9006"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 15, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"name": "molly", "age": "1", "address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [302, [{"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002"]}, {"role":"READ", "addresses":["127.0.0.1:9002"]}, {"role":"WRITE", "addresses":["127.0.0.1:9001"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 15, "db": "system"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7
8 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
9 PULL {"n": -1}
10 S: SUCCESS {"fields": ["ttl", "servers"]}
11 RECORD [300, [{"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002"]}, {"role":"READ", "addresses":["127.0.0.1:9004"]}, {"role":"WRITE", "addresses":["127.0.0.1:9006"]}]]
12 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 5, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [302, [{"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002"]}, {"role":"READ", "addresses":["127.0.0.1:9002"]}, {"role":"WRITE", "addresses":["127.0.0.1:9001"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 5, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [304, [{"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]}, {"role":"READ", "addresses":[]}, {"role":"WRITE", "addresses":["127.0.0.1:9006"]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 5, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9001
5
6 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
7 PULL {"n": -1}
8 S: SUCCESS {"fields": ["ttl", "servers"]}
9 RECORD [303, [{"role":"ROUTE", "addresses":["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]}, {"role":"READ", "addresses":["127.0.0.1:9004", "127.0.0.1:9005"]}, {"role":"WRITE", "addresses":[]}]]
10 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "s", "t_last": 5, "db": "system"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3
4 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
5 PULL {"n": -1}
6 S: FAILURE {"code": "Neo.ClientError.Procedure.ProcedureNotFound", "message": "Not a router"}
7 IGNORED
8 C: RESET
9 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001"}} {"mode": "r", "db": "system"}
6 PULL {"n": -1}
7 S: SUCCESS {"fields": ["ttl", "servers"]}
8 SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO BEGIN {}
5 !: AUTO COMMIT
6 !: AUTO ROLLBACK
7 !: PORT 9006
8
9 C: RUN "CREATE (n:TEST {name:'test'})" {} {}
10 C: PULL {"n": -1}
11 S: FAILURE {"code": "Neo.ClientError.Cluster.NotALeader", "message": "Leader switched has happened"}
12 S: IGNORED
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO BEGIN {}
5 !: AUTO COMMIT
6 !: AUTO ROLLBACK
7 !: PORT 9004
8
9 C: RUN "RETURN 1" {} {"mode": "r"}
10 C: PULL {"n": -1}
11 S: FAILURE {"code": "Neo.TransientError.General.DatabaseUnavailable", "message": "Database is busy doing store copy"}
12 S: IGNORED
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: AUTO BEGIN {}
5 !: AUTO COMMIT
6 !: AUTO ROLLBACK
7 !: PORT 9006
8
9 C: RUN "CREATE (n:TEST {name:'test'})" {} {}
10 C: PULL {"n": -1}
11 S: FAILURE {"code": "Neo.ClientError.General.ForbiddenOnReadOnlyDatabase", "message": "Unable to write"}
12 S: IGNORED
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:0", "bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8 C: COMMIT
9 S: SUCCESS {"bookmark": "bookmark:2"}
10
11 C: BEGIN {"bookmarks": ["bookmark:2"], "mode": "r"}
12 S: SUCCESS {}
13 C: COMMIT
14 S: SUCCESS {"bookmark": "bookmark:3"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8 C: COMMIT
9 S: SUCCESS {"bookmark": "bookmark:2"}
10
11 C: RUN "RETURN 1" {} {"bookmarks": ["bookmark:2"], "mode": "r"}
12 PULL {"n": -1}
13 S: SUCCESS {}
14 SUCCESS {"bookmark": "bookmark:3"}
15
16 C: BEGIN {"bookmarks": ["bookmark:3"], "mode": "r"}
17 S: SUCCESS {}
18 C: COMMIT
19 S: SUCCESS {"bookmark": "bookmark:4"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4
5 C: BEGIN {}
6 RUN "CREATE (n {name:'Bob'})" {} {}
7 PULL {"n": -1}
8 S: SUCCESS {}
9 SUCCESS {}
10 SUCCESS {}
11 C: COMMIT
12 S: <EXIT>
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: BEGIN {"mode": "r", "db": "test", "tx_metadata": {"foo": "bar"}, "tx_timeout": 3000}
8 S: SUCCESS {}
9 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {}
10 S: SUCCESS {"fields": ["x"], "t_first": 300, "qid": 0}
11 C: PULL {"n": 2}
12 S: RECORD [1]
13 RECORD [2]
14 SUCCESS {"has_more": true}
15 C: DISCARD {"n": -1, "qid": 0}
16 S: SUCCESS {"type": "r", "t_last": 500, "db": "test"}
17 C: COMMIT
18 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: BEGIN {"mode": "r", "db": "test", "tx_metadata": {"foo": "bar"}, "tx_timeout": 3000}
8 S: SUCCESS {}
9 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {}
10 S: SUCCESS {"fields": ["x"], "t_first": 300, "qid": 0}
11 C: PULL {"n": 2}
12 S: RECORD [1]
13 RECORD [2]
14 SUCCESS {"has_more": true}
15 C: PULL {"n": 2, "qid": 0}
16 S: RECORD [3]
17 RECORD [4]
18 SUCCESS {"type": "r", "t_last": 500, "db": "test"}
19 C: COMMIT
20 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1"}
0 !: BOLT 4
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test"}
6 S: SUCCESS {"server": "Neo4j/4.0.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: BEGIN {"mode": "r", "db": "test", "tx_metadata": {"foo": "bar"}, "tx_timeout": 3000}
8 S: SUCCESS {}
9 C: RUN "UNWIND [1,2,3,4] AS x RETURN x" {} {}
10 S: SUCCESS {"fields": ["x"], "t_first": 300, "qid": 0}
11 C: PULL {"n": 2}
12 S: RECORD [1]
13 RECORD [2]
14 SUCCESS {"has_more": true}
15 C: PULL {"n": 2, "qid": 0}
16 S: <SLEEP> 1
17 S: RECORD [3]
18 RECORD [4]
19 SUCCESS {"type": "r", "t_last": 500, "db": "test"}
20 C: COMMIT
21 S: SUCCESS {"bookmark": "neo4j:bookmark-test-1"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9004
4
5 C: BEGIN {"mode": "r"}
6 S: SUCCESS {}
7
8 C: RUN "RETURN 1" {} {}
9 PULL {"n": -1}
10 S: FAILURE {"code": "Neo.TransientError.Transaction.LockClientStopped", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9006
4
5 C: BEGIN {}
6 S: SUCCESS {}
7
8 C: RUN "RETURN 1" {} {}
9 PULL {"n": -1}
10 S: FAILURE {"code": "Neo.TransientError.Transaction.LockClientStopped", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: RUN "RETURN 1" {} {}
16 PULL {"n": -1}
17 S: SUCCESS {"fields": ["1"]}
18 RECORD [1]
19 SUCCESS {}
20
21 C: COMMIT
22 S: SUCCESS {"bookmark": "bookmark:1"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 2" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["2"]}
12 RECORD [2]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {"bookmarks": ["bookmark:1"]}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 2" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["2"]}
12 RECORD [2]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9004
4
5 C: BEGIN {"mode": "r"}
6 S: SUCCESS {}
7
8 C: RUN "X" {} {}
9 PULL {"n": -1}
10 S: FAILURE {"code": "Neo.ClientError.Statement.SyntaxError", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: PORT 9006
4
5 C: BEGIN {}
6 S: SUCCESS {}
7
8 C: RUN "X" {} {}
9 PULL {"n": -1}
10 S: FAILURE {"code": "Neo.ClientError.Statement.SyntaxError", "message": "X"}
11 IGNORED {}
12
13 C: RESET
14 S: SUCCESS {}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9004
5
6 C: BEGIN {"mode": "r"}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
17
18 C: BEGIN {"bookmarks": ["bookmark:1"], "mode": "r"}
19 S: SUCCESS {}
20
21 C: RUN "RETURN 1" {} {}
22 PULL {"n": -1}
23 S: SUCCESS {"fields": ["1"]}
24 RECORD [1]
25 SUCCESS {}
26
27 C: COMMIT
28 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 4
1 !: AUTO HELLO
2 !: AUTO GOODBYE
3 !: AUTO RESET
4 !: PORT 9006
5
6 C: BEGIN {}
7 S: SUCCESS {}
8
9 C: RUN "RETURN 1" {} {}
10 PULL {"n": -1}
11 S: SUCCESS {"fields": ["1"]}
12 RECORD [1]
13 SUCCESS {}
14
15 C: COMMIT
16 S: SUCCESS {"bookmark": "bookmark:1"}
17
18 C: BEGIN {"bookmarks": ["bookmark:1"]}
19 S: SUCCESS {}
20
21 C: RUN "RETURN 1" {} {}
22 PULL {"n": -1}
23 S: SUCCESS {"fields": ["1"]}
24 RECORD [1]
25 SUCCESS {}
26
27 C: COMMIT
28 S: SUCCESS {"bookmark": "bookmark:2"}
0 !: BOLT 4.1
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
4 S: SUCCESS {"server": "Neo4j/4.1.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: GOODBYE
6 S: <EXIT>
0 !: BOLT 4.1
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9002
4
5 C: HELLO {"scheme": "basic", "principal": "test", "credentials": "test", "user_agent": "test", "routing": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}}
6 S: SUCCESS {"server": "Neo4j/4.1.0", "connection_id": "bolt-123456789"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL {"n": -1}
9 S: SUCCESS {"fields": ["x"]}
10 RECORD [1]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-2", "type": "r", "t_last": 5, "db": "system"}
0 !: BOLT 4.1
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
6 S: SUCCESS {"server": "Neo4j/4.1.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL {"n": 2}
9 S: SUCCESS {"fields": ["x"]}
10 <NOOP>
11 <NOOP>
12 RECORD [1]
13 <NOOP>
14 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "neo4j"}
0 !: BOLT 4.1
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
4 S: SUCCESS {"server": "Bogus/4.1.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
6 PULL {"n": -1}
7 S: SUCCESS {"fields": ["x"]}
8 RECORD [1]
9 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "neo4j"}
0 !: BOLT 4.1
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"scheme": "basic", "principal": "test", "credentials": "test", "user_agent": "test", "routing": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}}
6 S: SUCCESS {"server": "Neo4j/4.1.0", "connection_id": "bolt-123456789"}
7 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}} {"mode": "r", "db": "system"}
8 PULL {"n": -1}
9 S: SUCCESS {"fields": ["ttl", "servers"]}
10 RECORD [4321, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"}, {"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "system"}
0 !: BOLT 4.2
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
4 S: SUCCESS {"server": "Neo4j/4.2.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: GOODBYE
6 S: <EXIT>
0 !: BOLT 4.2
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9002
4
5 C: HELLO {"scheme": "basic", "principal": "test", "credentials": "test", "user_agent": "test", "routing": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}}
6 S: SUCCESS {"server": "Neo4j/4.2.0", "connection_id": "bolt-123456789"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL {"n": -1}
9 S: SUCCESS {"fields": ["x"]}
10 RECORD [1]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-2", "type": "r", "t_last": 5, "db": "system"}
0 !: BOLT 4.2
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
6 S: SUCCESS {"server": "Neo4j/4.2.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
7 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
8 PULL {"n": 2}
9 S: SUCCESS {"fields": ["x"]}
10 <NOOP>
11 <NOOP>
12 RECORD [1]
13 <NOOP>
14 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "neo4j"}
0 !: BOLT 4.2
1 !: PORT 9001
2
3 C: HELLO {"user_agent": "test", "scheme": "basic", "principal": "test", "credentials": "test", "routing": {"address": "localhost:9001"}}
4 S: SUCCESS {"server": "Bogus/4.2.0", "connection_id": "123e4567-e89b-12d3-a456-426655440000"}
5 C: RUN "RETURN 1 AS x" {} {"mode": "r"}
6 PULL {"n": -1}
7 S: SUCCESS {"fields": ["x"]}
8 RECORD [1]
9 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "neo4j"}
0 !: BOLT 4.2
1 !: AUTO GOODBYE
2 !: AUTO RESET
3 !: PORT 9001
4
5 C: HELLO {"scheme": "basic", "principal": "test", "credentials": "test", "user_agent": "test", "routing": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}}
6 S: SUCCESS {"server": "Neo4j/4.2.0", "connection_id": "bolt-123456789"}
7 C: RUN "CALL dbms.routing.getRoutingTable($context)" {"context": {"address": "localhost:9001", "policy": "my_policy", "region": "china"}} {"mode": "r", "db": "system"}
8 PULL {"n": -1}
9 S: SUCCESS {"fields": ["ttl", "servers"]}
10 RECORD [4321, [{"addresses": ["127.0.0.1:9001"],"role": "WRITE"}, {"addresses": ["127.0.0.1:9002"], "role": "READ"}, {"addresses": ["127.0.0.1:9001", "127.0.0.1:9002"], "role": "ROUTE"}]]
11 SUCCESS {"bookmark": "neo4j:bookmark-test-1", "type": "r", "t_last": 5, "db": "system"}
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import GraphDatabase
24 from neo4j.exceptions import (
25 Neo4jError,
26 TransientError,
27 )
28
29 from tests.stub.conftest import StubCluster
30
31 # python -m pytest tests/stub/test_accesslevel.py -s -v
32
33
34 @pytest.mark.parametrize(
35 "test_scripts",
36 [
37 ("v3/router.script", "v3/return_1_in_read_tx.script"),
38 ("v4x0/router.script", "v4x0/tx_return_1_port_9004.script"),
39 ]
40 )
41 def test_read_transaction(driver_info, test_scripts):
42 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_read_transaction
43 with StubCluster(*test_scripts):
44 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
45 with driver.session(fetch_size=-1) as session:
46
47 def unit_of_work(tx):
48 total = 0
49 for record in tx.run("RETURN 1"):
50 total += record[0]
51 return total
52
53 value = session.read_transaction(unit_of_work)
54 assert value == 1
55
56
57 @pytest.mark.parametrize(
58 "test_scripts",
59 [
60 ("v3/router.script", "v3/return_1_in_write_tx.script"),
61 ("v4x0/router.script", "v4x0/tx_return_1_port_9006.script"),
62 ]
63 )
64 def test_write_transaction(driver_info, test_scripts):
65 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_write_transaction
66 with StubCluster(*test_scripts):
67 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
68 with driver.session(fetch_size=-1) as session:
69
70 def unit_of_work(tx):
71 total = 0
72 for record in tx.run("RETURN 1"):
73 total += record[0]
74 return total
75
76 value = session.write_transaction(unit_of_work)
77 assert value == 1
78
79
80 @pytest.mark.skip(reason="BROKEN, This test is broken and should be an integration test.")
81 @pytest.mark.parametrize(
82 "test_scripts",
83 [
84 ("v3/router.script", "v3/error_in_read_tx.script"),
85 ("v4x0/router.script", "v4x0/tx_run_with_failure_syntax_error_port_9004.script"),
86 ]
87 )
88 def test_read_transaction_with_error(driver_info, test_scripts):
89 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_read_transaction_with_error
90 with StubCluster(*test_scripts):
91 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
92 with driver.session(fetch_size=-1) as session:
93
94 def unit_of_work(tx):
95 tx.run("X")
96
97 with pytest.raises(Neo4jError):
98 _ = session.read_transaction(unit_of_work)
99
100
101 @pytest.mark.skip(reason="BROKEN, This test is broken and should be an integration test.")
102 @pytest.mark.parametrize(
103 "test_scripts",
104 [
105 ("v3/router.script", "v3/error_in_write_tx.script"),
106 ("v4x0/router.script", "v4x0/tx_run_with_failure_syntax_error_port_9006.script"),
107 ]
108 )
109 def test_write_transaction_with_error(driver_info, test_scripts):
110 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_write_transaction_with_error
111 with StubCluster(*test_scripts):
112 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
113 with driver.session(fetch_size=-1) as session:
114
115 def unit_of_work(tx):
116 tx.run("X")
117
118 with pytest.raises(Neo4jError):
119 _ = session.write_transaction(unit_of_work)
120
121
122 @pytest.mark.parametrize(
123 "test_scripts",
124 [
125 ("v3/router.script", "v3/return_1_in_read_tx_twice.script"),
126 ("v4x0/router.script", "v4x0/tx_two_subsequent_return_1_port_9004.script"),
127 ]
128 )
129 def test_two_subsequent_read_transactions(driver_info, test_scripts):
130 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_two_subsequent_read_transactions
131 with StubCluster(*test_scripts):
132 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
133 with driver.session(fetch_size=-1) as session:
134
135 def unit_of_work(tx):
136 total = 0
137 for record in tx.run("RETURN 1"):
138 total += record[0]
139 return total
140
141 value = session.read_transaction(unit_of_work)
142 assert value == 1
143 value = session.read_transaction(unit_of_work)
144 assert value == 1
145
146
147 @pytest.mark.parametrize(
148 "test_scripts",
149 [
150 ("v3/router.script", "v3/return_1_in_write_tx_twice.script"),
151 ("v4x0/router.script", "v4x0/tx_two_subsequent_return_1_port_9006.script"),
152 ]
153 )
154 def test_two_subsequent_write_transactions(driver_info, test_scripts):
155 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_two_subsequent_write_transactions
156 with StubCluster(*test_scripts):
157 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
158 with driver.session(fetch_size=-1) as session:
159
160 def unit_of_work(tx):
161 total = 0
162 for record in tx.run("RETURN 1"):
163 total += record[0]
164 return total
165
166 value = session.write_transaction(unit_of_work)
167 assert value == 1
168 value = session.write_transaction(unit_of_work)
169 assert value == 1
170
171
172 @pytest.mark.skip(reason="BOOKMARK, AttributeError: 'Session' object has no attribute 'last_bookmark'")
173 @pytest.mark.parametrize(
174 "test_scripts",
175 [
176 ("v3/router.script", "v3/return_1_in_read_tx.script", "v3/return_2_in_write_tx.script"),
177 ("v4x0/router.script", "v4x0/tx_return_1_port_9004.script", "v4x0/tx_return_2_with_bookmark_port_9006.script"),
178 ]
179 )
180 def test_read_tx_then_write_tx(driver_info, test_scripts):
181 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_read_tx_then_write_tx
182 with StubCluster(*test_scripts):
183 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
184 with driver.session(fetch_size=-1) as session:
185
186 def unit_of_work_1(tx):
187 total = 0
188 for record in tx.run("RETURN 1"):
189 total += record[0]
190 return total
191
192 def unit_of_work_2(tx):
193 total = 0
194 for record in tx.run("RETURN 2"):
195 total += record[0]
196 return total
197
198 value = session.read_transaction(unit_of_work_1)
199 assert session.last_bookmark() == "bookmark:1"
200 assert value == 1
201 value = session.write_transaction(unit_of_work_2)
202 assert session.last_bookmark() == "bookmark:2"
203 assert value == 2
204
205
206 @pytest.mark.parametrize(
207 "test_scripts",
208 [
209 ("v3/router.script", "v3/return_1_in_write_tx.script", "v3/return_2_in_read_tx.script"),
210 ("v4x0/router.script", "v4x0/tx_return_1_port_9006.script", "v4x0/tx_return_2_with_bookmark_port_9004.script"),
211 ]
212 )
213 def test_write_tx_then_read_tx(driver_info, test_scripts):
214 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_write_tx_then_read_tx
215 with StubCluster(*test_scripts):
216 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
217 with driver.session(fetch_size=-1) as session:
218
219 def unit_of_work_1(tx):
220 total = 0
221 for record in tx.run("RETURN 1"):
222 total += record[0]
223 return total
224
225 def unit_of_work_2(tx):
226 total = 0
227 for record in tx.run("RETURN 2"):
228 total += record[0]
229 return total
230
231 value = session.write_transaction(unit_of_work_1)
232 assert value == 1
233 value = session.read_transaction(unit_of_work_2)
234 assert value == 2
235
236
237 @pytest.mark.skip(reason="BROKEN")
238 @pytest.mark.parametrize(
239 "test_scripts",
240 [
241 ("v3/router.script", "v3/user_canceled_read.script"),
242 ("v4x0/router.script", "v4x0/tx_return_1_reset_port_9004.script"),
243 ]
244 )
245 def test_no_retry_read_on_user_canceled_tx(driver_info, test_scripts):
246 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_no_retry_read_on_user_canceled_tx
247 with StubCluster(*test_scripts):
248 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
249 with driver.session(fetch_size=-1) as session:
250 def unit_of_work(tx):
251 tx.run("RETURN 1")
252
253 with pytest.raises(TransientError):
254 _ = session.read_transaction(unit_of_work)
255
256
257 @pytest.mark.skip(reason="BROKEN")
258 @pytest.mark.parametrize(
259 "test_scripts",
260 [
261 ("v3/router.script", "v3/user_canceled_write.script"),
262 ("v4x0/router.script", "v4x0/tx_return_1_reset_port_9006.script"),
263 ]
264 )
265 def test_no_retry_write_on_user_canceled_tx(driver_info, test_scripts):
266 # python -m pytest tests/stub/test_accesslevel.py -s -v -k test_no_retry_write_on_user_canceled_tx
267 with StubCluster(*test_scripts):
268 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
269 with driver.session(fetch_size=-1) as session:
270 def unit_of_work(tx):
271 tx.run("RETURN 1")
272
273 with pytest.raises(TransientError):
274 _ = session.write_transaction(unit_of_work)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 )
26 from neo4j.api import READ_ACCESS
27
28 from tests.stub.conftest import StubCluster
29
30 # python -m pytest tests/stub/test_bookmarking.py -s -v
31
32
33 @pytest.mark.parametrize(
34 "test_script",
35 [
36 "v3/router.script",
37 "v4x0/router.script",
38 ]
39 )
40 def test_should_be_no_bookmark_in_new_session(driver_info, test_script):
41 # python -m pytest tests/stub/test_bookmarking.py -s -v -k test_should_be_no_bookmark_in_new_session
42 with StubCluster(test_script):
43 uri = "neo4j://localhost:9001"
44 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
45 with driver.session(fetch_size=-1) as session:
46 assert session.last_bookmark() is None
47
48
49 @pytest.mark.parametrize(
50 "test_script",
51 [
52 "v3/router.script",
53 "v4x0/router.script",
54 ]
55 )
56 def test_should_be_able_to_set_bookmark(driver_info, test_script):
57 # python -m pytest tests/stub/test_bookmarking.py -s -v -k test_should_be_able_to_set_bookmark
58 with StubCluster(test_script):
59 uri = "neo4j://localhost:9001"
60 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
61 with driver.session(bookmarks=["X"], fetch_size=-1) as session:
62 assert session._bookmarks == ("X",)
63
64
65 @pytest.mark.parametrize(
66 "test_script",
67 [
68 "v3/router.script",
69 "v4x0/router.script",
70 ]
71 )
72 def test_should_be_able_to_set_multiple_bookmarks(driver_info, test_script):
73 # python -m pytest tests/stub/test_bookmarking.py -s -v -k test_should_be_able_to_set_multiple_bookmarks
74 with StubCluster(test_script):
75 uri = "neo4j://localhost:9001"
76 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
77 with driver.session(bookmarks=[":1", ":2"], fetch_size=-1) as session:
78 assert session._bookmarks == (":1", ":2")
79
80
81 @pytest.mark.parametrize(
82 "test_scripts",
83 [
84 ("v3/router.script", "v3/bookmark_chain.script"),
85 ("v4x0/router.script", "v4x0/tx_bookmark_chain.script"),
86 ]
87 )
88 def test_should_automatically_chain_bookmarks(driver_info, test_scripts):
89 # python -m pytest tests/stub/test_bookmarking.py -s -v -k test_should_automatically_chain_bookmarks
90 with StubCluster(*test_scripts):
91 uri = "neo4j://localhost:9001"
92 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
93 with driver.session(default_access_mode=READ_ACCESS, bookmarks=["bookmark:0", "bookmark:1"], fetch_size=-1) as session:
94 with session.begin_transaction():
95 pass
96 assert session.last_bookmark() == "bookmark:2"
97 with session.begin_transaction():
98 pass
99 assert session.last_bookmark() == "bookmark:3"
100
101
102 @pytest.mark.parametrize(
103 "test_scripts",
104 [
105 ("v3/router.script", "v3/bookmark_chain_with_autocommit.script"),
106 ("v4x0/router.script", "v4x0/tx_bookmark_chain_with_autocommit.script"),
107 ]
108 )
109 def test_autocommit_transaction_included_in_chain(driver_info, test_scripts):
110 # python -m pytest tests/stub/test_bookmarking.py -s -v -k test_autocommit_transaction_included_in_chain
111 with StubCluster(*test_scripts):
112 uri = "neo4j://localhost:9001"
113 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
114 with driver.session(default_access_mode=READ_ACCESS, bookmarks=["bookmark:1"], fetch_size=-1) as session:
115 with session.begin_transaction():
116 pass
117 assert session.last_bookmark() == "bookmark:2"
118 session.run("RETURN 1").consume()
119 assert session.last_bookmark() == "bookmark:3"
120 with session.begin_transaction():
121 pass
122 assert session.last_bookmark() == "bookmark:4"
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import (
24 ServiceUnavailable,
25 ConfigurationError,
26 DriverError,
27 )
28 from neo4j._exceptions import (
29 BoltHandshakeError,
30 BoltSecurityError,
31 )
32
33 from neo4j import (
34 GraphDatabase,
35 BoltDriver,
36 Query,
37 WRITE_ACCESS,
38 READ_ACCESS,
39 TRUST_ALL_CERTIFICATES,
40 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
41 DEFAULT_DATABASE,
42 Result,
43 unit_of_work,
44 Transaction,
45 )
46
47 from tests.stub.conftest import (
48 StubCluster,
49 )
50
51 # python -m pytest tests/stub/test_directdriver.py -s -v
52
53
54 driver_config = {
55 "encrypted": False,
56 "user_agent": "test",
57 "max_connection_lifetime": 1000,
58 "max_connection_pool_size": 10,
59 "keep_alive": True,
60 "resolver": None,
61 }
62
63
64 session_config = {
65 "default_access_mode": READ_ACCESS,
66 "connection_acquisition_timeout": 1.0,
67 "max_transaction_retry_time": 1.0,
68 "initial_retry_delay": 1.0,
69 "retry_delay_multiplier": 1.0,
70 "retry_delay_jitter_factor": 0.1,
71 "fetch_size": -1,
72 }
73
74
75 @pytest.mark.parametrize(
76 "test_script",
77 [
78 "v3/empty.script",
79 "v4x0/empty.script",
80 ]
81 )
82 def test_bolt_uri_constructs_bolt_driver(driver_info, test_script):
83 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_uri_constructs_bolt_driver
84 with StubCluster(test_script):
85 uri = "bolt://127.0.0.1:9001"
86 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
87 assert isinstance(driver, BoltDriver)
88
89
90 @pytest.mark.parametrize(
91 "test_script",
92 [
93 "v3/empty_explicit_hello_goodbye.script",
94 "v4x0/empty_explicit_hello_goodbye.script",
95 "v4x1/empty_explicit_hello_goodbye.script",
96 "v4x2/empty_explicit_hello_goodbye.script",
97 ]
98 )
99 def test_direct_driver_handshake_negotiation(driver_info, test_script):
100 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_driver_handshake_negotiation
101 with StubCluster(test_script):
102 uri = "bolt://localhost:9001"
103 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config)
104 assert isinstance(driver, BoltDriver)
105 driver.close()
106
107
108 @pytest.mark.parametrize(
109 "test_script, test_expected",
110 [
111 ("v3/return_1_port_9001.script", "Neo4j/3.0.0"),
112 ("v4x0/return_1_port_9001.script", "Neo4j/4.0.0"),
113 ("v4x1/return_1_port_9001_bogus_server.script", DriverError),
114 ("v4x2/return_1_port_9001_bogus_server.script", DriverError),
115 ]
116 )
117 def test_return_1_as_x(driver_info, test_script, test_expected):
118 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_return_1_as_x
119 with StubCluster(test_script):
120 uri = "bolt://localhost:9001"
121 try:
122 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test")
123 assert isinstance(driver, BoltDriver)
124 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
125 result = session.run("RETURN 1 AS x")
126 value = result.single().value()
127 assert value == 1
128 summary = result.consume()
129 assert summary.server.agent == test_expected
130 assert summary.server.agent.startswith("Neo4j")
131 driver.close()
132 except DriverError as error:
133 assert isinstance(error, test_expected)
134
135
136 def test_direct_driver_with_wrong_port(driver_info):
137 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_driver_with_wrong_port
138 uri = "bolt://127.0.0.1:9002"
139 with pytest.raises(ServiceUnavailable):
140 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config)
141
142
143 @pytest.mark.parametrize(
144 "test_script, test_expected",
145 [
146 ("v3/return_1_port_9001.script", "Neo4j/3.0.0"),
147 ("v4x0/return_1_port_9001.script", "Neo4j/4.0.0"),
148 ]
149 )
150 def test_direct_verify_connectivity(driver_info, test_script, test_expected):
151 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_verify_connectivity
152 with StubCluster(test_script):
153 uri = "bolt://localhost:9001"
154 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
155 assert isinstance(driver, BoltDriver)
156 assert driver.verify_connectivity(default_access_mode=READ_ACCESS) == test_expected
157
158
159 @pytest.mark.parametrize(
160 "test_script",
161 [
162 "v3/disconnect_on_run.script",
163 "v4x0/disconnect_on_run.script",
164 ]
165 )
166 def test_direct_verify_connectivity_disconnect_on_run(driver_info, test_script):
167 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_verify_connectivity_disconnect_on_run
168 with StubCluster(test_script):
169 uri = "bolt://127.0.0.1:9001"
170 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
171 with pytest.raises(ServiceUnavailable):
172 driver.verify_connectivity(default_access_mode=READ_ACCESS)
173
174
175 @pytest.mark.parametrize(
176 "test_script",
177 [
178 "v3/disconnect_on_run.script",
179 "v4x0/disconnect_on_run.script",
180 ]
181 )
182 def test_direct_disconnect_on_run(driver_info, test_script):
183 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_disconnect_on_run
184 with StubCluster(test_script):
185 uri = "bolt://127.0.0.1:9001"
186 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
187 with pytest.raises(ServiceUnavailable):
188 with driver.session(**session_config) as session:
189 session.run("RETURN 1 AS x").consume()
190
191
192 @pytest.mark.parametrize(
193 "test_script",
194 [
195 "v3/disconnect_on_pull_all.script",
196 "v4x0/disconnect_on_pull.script",
197 ]
198 )
199 def test_direct_disconnect_on_pull_all(driver_info, test_script):
200 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_disconnect_on_pull_all
201 with StubCluster(test_script):
202 uri = "bolt://127.0.0.1:9001"
203 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
204 with pytest.raises(ServiceUnavailable):
205 with driver.session(**session_config) as session:
206 session.run("RETURN $x", {"x": 1}).consume()
207
208
209 @pytest.mark.parametrize(
210 "test_script",
211 [
212 "v3/disconnect_after_init.script",
213 "v4x0/disconnect_after_init.script",
214 ]
215 )
216 def test_direct_session_close_after_server_close(driver_info, test_script):
217 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_session_close_after_server_close
218 with StubCluster(test_script):
219 uri = "bolt://127.0.0.1:9001"
220
221 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
222 with driver.session(**session_config) as session:
223 with pytest.raises(ServiceUnavailable):
224 session.write_transaction(lambda tx: tx.run(Query("CREATE (a:Item)", timeout=1)))
225
226
227 @pytest.mark.skip(reason="Cant close the Stub Server gracefully")
228 @pytest.mark.parametrize(
229 "test_script",
230 [
231 "v3/empty.script",
232 "v4x0/empty.script",
233 ]
234 )
235 def test_bolt_uri_scheme_self_signed_certificate_constructs_bolt_driver(driver_info, test_script):
236 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_uri_scheme_self_signed_certificate_constructs_bolt_driver
237
238 test_config = {
239 "user_agent": "test",
240 "max_connection_lifetime": 1000,
241 "max_connection_pool_size": 10,
242 "keep_alive": False,
243 "max_transaction_retry_time": 1,
244 "resolver": None,
245 }
246
247 with StubCluster(test_script):
248 uri = "bolt+ssc://127.0.0.1:9001"
249 try:
250 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
251 assert isinstance(driver, BoltDriver)
252 driver.close()
253 except ServiceUnavailable as error:
254 assert isinstance(error.__cause__, BoltSecurityError)
255 pytest.skip("Failed to establish encrypted connection")
256
257
258 @pytest.mark.skip(reason="Cant close the Stub Server gracefully")
259 @pytest.mark.parametrize(
260 "test_script",
261 [
262 "v3/empty.script",
263 "v4x0/empty.script",
264 ]
265 )
266 def test_bolt_uri_scheme_secure_constructs_bolt_driver(driver_info, test_script):
267 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_uri_scheme_secure_constructs_bolt_driver
268
269 test_config = {
270 "user_agent": "test",
271 "max_connection_lifetime": 1000,
272 "max_connection_pool_size": 10,
273 "keep_alive": False,
274 "max_transaction_retry_time": 1,
275 "resolver": None,
276 }
277
278 with StubCluster(test_script):
279 uri = "bolt+s://127.0.0.1:9001"
280 try:
281 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
282 assert isinstance(driver, BoltDriver)
283 driver.close()
284 except ServiceUnavailable as error:
285 assert isinstance(error.__cause__, BoltSecurityError)
286 pytest.skip("Failed to establish encrypted connection")
287
288
289 @pytest.mark.parametrize(
290 "test_uri",
291 [
292 "bolt+ssc://127.0.0.1:9001",
293 "bolt+s://127.0.0.1:9001",
294 ]
295 )
296 @pytest.mark.parametrize(
297 "test_config, expected_failure, expected_failure_message",
298 [
299 ({"encrypted": False}, ConfigurationError, "The config settings"),
300 ({"encrypted": True}, ConfigurationError, "The config settings"),
301 ({"encrypted": True, "trust": TRUST_ALL_CERTIFICATES}, ConfigurationError, "The config settings"),
302 ({"trust": TRUST_ALL_CERTIFICATES}, ConfigurationError, "The config settings"),
303 ({"trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES}, ConfigurationError, "The config settings"),
304 ]
305 )
306 def test_bolt_uri_scheme_secure_constructs_driver_config_error(driver_info, test_uri, test_config, expected_failure, expected_failure_message):
307 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_uri_scheme_secure_constructs_driver_config_error
308 with pytest.raises(expected_failure) as error:
309 driver = GraphDatabase.driver(test_uri, auth=driver_info["auth_token"], **test_config)
310
311 assert error.match(expected_failure_message)
312
313
314 @pytest.mark.parametrize(
315 "test_config, expected_failure, expected_failure_message",
316 [
317 ({"trust": 1}, ConfigurationError, "The config setting `trust`"),
318 ({"trust": True}, ConfigurationError, "The config setting `trust`"),
319 ({"trust": None}, ConfigurationError, "The config setting `trust`"),
320 ]
321 )
322 def test_driver_trust_config_error(driver_info, test_config, expected_failure, expected_failure_message):
323 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_driver_trust_config_error
324 uri = "bolt://127.0.0.1:9001"
325 with pytest.raises(expected_failure) as error:
326 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
327
328 assert error.match(expected_failure_message)
329
330
331 @pytest.mark.parametrize(
332 "test_script, database",
333 [
334 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
335 ("v4x0/pull_n_port_9001.script", "test"),
336 ("v4x0/pull_n_port_9001_slow_network.script", "test"),
337 ]
338 )
339 def test_bolt_driver_fetch_size_config_normal_case(driver_info, test_script, database):
340 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_normal_case
341 with StubCluster(test_script):
342 uri = "bolt://127.0.0.1:9001"
343 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
344 assert isinstance(driver, BoltDriver)
345 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
346 expected = []
347 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
348 assert isinstance(result, Result)
349 for record in result:
350 expected.append(record["x"])
351
352 assert expected == [1, 2, 3, 4]
353
354
355 @pytest.mark.parametrize(
356 "test_script, database",
357 [
358 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
359 ("v4x0/pull_n_port_9001.script", "test"),
360 ]
361 )
362 def test_bolt_driver_fetch_size_config_result_implements_iterator_protocol(driver_info, test_script, database):
363 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_result_implements_iterator_protocol
364 with StubCluster(test_script):
365 uri = "bolt://127.0.0.1:9001"
366 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
367 assert isinstance(driver, BoltDriver)
368 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
369 expected = []
370 result = iter(session.run("UNWIND [1,2,3,4] AS x RETURN x"))
371 expected.append(next(result)["x"])
372 expected.append(next(result)["x"])
373 expected.append(next(result)["x"])
374 expected.append(next(result)["x"])
375 with pytest.raises(StopIteration):
376 expected.append(next(result)["x"])
377
378 assert expected == [1, 2, 3, 4]
379
380
381 @pytest.mark.parametrize(
382 "test_script, database",
383 [
384 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
385 ("v4x0/pull_2_discard_all_port_9001.script", "test"),
386 ]
387 )
388 def test_bolt_driver_fetch_size_config_consume_case(driver_info, test_script, database):
389 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_consume_case
390 with StubCluster(test_script):
391 uri = "bolt://127.0.0.1:9001"
392 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
393 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
394 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
395 result_summary = result.consume()
396
397 assert result_summary.server.address == ('127.0.0.1', 9001)
398
399
400 @pytest.mark.parametrize(
401 "test_script, database",
402 [
403 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
404 ("v4x0/pull_2_discard_all_port_9001.script", "test"),
405 ]
406 )
407 def test_bolt_driver_on_exit_consume_remaining_result(driver_info, test_script, database):
408 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_on_exit_consume_remaining_result
409 with StubCluster(test_script):
410 uri = "bolt://127.0.0.1:9001"
411 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
412 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
413 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
414
415
416 @pytest.mark.parametrize(
417 "test_script, database",
418 [
419 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
420 ("v4x0/pull_2_discard_all_port_9001.script", "test"),
421 ]
422 )
423 def test_bolt_driver_on_close_result_consume(driver_info, test_script, database):
424 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_on_close_result_consume
425 with StubCluster(test_script):
426 uri = "bolt://127.0.0.1:9001"
427 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
428 session = driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS)
429 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
430 session.close()
431
432
433 @pytest.mark.parametrize(
434 "test_script, database",
435 [
436 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
437 ("v4x0/pull_2_discard_all_port_9001.script", "test"),
438 ]
439 )
440 def test_bolt_driver_on_exit_result_consume(driver_info, test_script, database):
441 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_on_exit_result_consume
442 with StubCluster(test_script):
443 uri = "bolt://127.0.0.1:9001"
444 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
445 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
446 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
447
448
449 @pytest.mark.parametrize(
450 "test_script, database",
451 [
452 ("v3/pull_all_port_9001.script", DEFAULT_DATABASE),
453 ("v4x0/pull_n_port_9001.script", "test"),
454 ]
455 )
456 def test_bolt_driver_fetch_size_config_case_result_consume(driver_info, test_script, database):
457 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_case_result_consume
458 with StubCluster(test_script):
459 uri = "bolt://127.0.0.1:9001"
460 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
461 assert isinstance(driver, BoltDriver)
462 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
463 expected = []
464 result = session.run("UNWIND [1,2,3,4] AS x RETURN x")
465 for record in result:
466 expected.append(record["x"])
467
468 result_summary = result.consume()
469 assert result_summary.server.address == ('127.0.0.1', 9001)
470
471 result_summary = result.consume() # It will just return the result summary
472
473 assert expected == [1, 2, 3, 4]
474 assert result_summary.server.address == ('127.0.0.1', 9001)
475
476
477 @pytest.mark.skip(reason="Bolt Stub Server cant handle this script correctly, see tests/integration/test_bolt_driver.py test_bolt_driver_fetch_size_config_run_consume_run")
478 @pytest.mark.parametrize(
479 "test_script, database",
480 [
481 ("v4x0/pull_2_discard_all_pull_n_port_9001.script", "test"),
482 ]
483 )
484 def test_bolt_driver_fetch_size_config_run_consume_run(driver_info, test_script, database):
485 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_run_consume_run
486 with StubCluster(test_script):
487 uri = "bolt://127.0.0.1:9001"
488 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
489 assert isinstance(driver, BoltDriver)
490 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
491 expected = []
492 result1 = session.run("UNWIND [1,2,3,4] AS x RETURN x")
493 result1.consume()
494 result2 = session.run("UNWIND [5,6,7,8] AS x RETURN x")
495
496 for record in result2:
497 expected.append(record["x"])
498
499 result_summary = result2.consume()
500 assert result_summary.server.address == ('127.0.0.1', 9001)
501
502 assert expected == [5, 6, 7, 8]
503 assert result_summary.server.address == ('127.0.0.1', 9001)
504
505
506 @pytest.mark.skip(reason="Bolt Stub Server cant handle this script correctly, see tests/integration/test_bolt_driver.py test_bolt_driver_fetch_size_config_run_run")
507 @pytest.mark.parametrize(
508 "test_script, database",
509 [
510 ("v4x0/pull_2_discard_all_pull_n_port_9001.script", "test"),
511 ]
512 )
513 def test_bolt_driver_fetch_size_config_run_run(driver_info, test_script, database):
514 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_fetch_size_config_run_run
515 with StubCluster(test_script):
516 uri = "bolt://127.0.0.1:9001"
517 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
518 assert isinstance(driver, BoltDriver)
519 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
520 expected = []
521 result1 = session.run("UNWIND [1,2,3,4] AS x RETURN x") # The session should discard all if in streaming mode and a new run is invoked.
522 result2 = session.run("UNWIND [5,6,7,8] AS x RETURN x")
523
524 for record in result2:
525 expected.append(record["x"])
526
527 result_summary = result2.consume()
528 assert result_summary.server.address == ('127.0.0.1', 9001)
529
530 assert expected == [5, 6, 7, 8]
531 assert result_summary.server.address == ('127.0.0.1', 9001)
532
533
534 @pytest.mark.parametrize(
535 "test_script, database",
536 [
537 ("v3/pull_all_port_9001_transaction_function.script", DEFAULT_DATABASE),
538 ("v4x0/tx_pull_n_port_9001.script", "test"),
539 ("v4x0/tx_pull_n_port_9001_slow_network.script", "test"),
540 # TODO: Test retry mechanism
541 ]
542 )
543 def test_bolt_driver_read_transaction_fetch_size_config_normal_case(driver_info, test_script, database):
544 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_read_transaction_fetch_size_config_normal_case
545 @unit_of_work(timeout=3, metadata={"foo": "bar"})
546 def unwind(transaction):
547 assert isinstance(transaction, Transaction)
548 values = []
549 result = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
550 assert isinstance(result, Result)
551 for record in result:
552 values.append(record["x"])
553 return values
554
555 with StubCluster(test_script):
556 uri = "bolt://127.0.0.1:9001"
557 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
558 assert isinstance(driver, BoltDriver)
559 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
560 expected = session.read_transaction(unwind)
561
562 assert expected == [1, 2, 3, 4]
563
564
565 @pytest.mark.parametrize(
566 "test_script, database",
567 [
568 ("v4x0/tx_pull_2_discard_all_port_9001.script", "test"),
569 ]
570 )
571 def test_bolt_driver_explicit_transaction_consume_result_case_a(driver_info, test_script, database):
572 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_explicit_transaction_consume_result_case_a
573 # This test is to check that the implicit consume that is triggered on transaction.commit() is working properly.
574 with StubCluster(test_script):
575 uri = "bolt://127.0.0.1:9001"
576 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
577 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
578 transaction = session.begin_transaction(timeout=3, metadata={"foo": "bar"})
579 result = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
580 transaction.commit()
581
582
583 @pytest.mark.parametrize(
584 "test_script, database",
585 [
586 ("v4x0/tx_pull_2_discard_all_port_9001.script", "test"),
587 ]
588 )
589 def test_bolt_driver_explicit_transaction_consume_result_case_b(driver_info, test_script, database):
590 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_bolt_driver_explicit_transaction_consume_result_case_b
591 # This test is to check that the implicit consume that is triggered on transaction.commit() is working properly.
592 with StubCluster(test_script):
593 uri = "bolt://127.0.0.1:9001"
594 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
595 with driver.session(database=database, fetch_size=2, default_access_mode=READ_ACCESS) as session:
596 transaction = session.begin_transaction(timeout=3, metadata={"foo": "bar"})
597 result = transaction.run("UNWIND [1,2,3,4] AS x RETURN x")
598 result.consume()
599 transaction.commit()
600
601
602 @pytest.mark.parametrize(
603 "test_script",
604 [
605 "v4x1/return_1_noop_port_9001.script",
606 "v4x2/return_1_noop_port_9001.script",
607 ]
608 )
609 def test_direct_can_handle_noop(driver_info, test_script):
610 # python -m pytest tests/stub/test_directdriver.py -s -v -k test_direct_can_handle_noop
611 with StubCluster(test_script):
612 uri = "bolt://localhost:9001"
613 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
614 assert isinstance(driver, BoltDriver)
615 with driver.session(fetch_size=2, default_access_mode=READ_ACCESS) as session:
616 result = session.run("RETURN 1 AS x")
617 value = result.single().value()
618 assert value == 1
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 Neo4jDriver,
26 DEFAULT_DATABASE,
27 )
28 from tests.stub.conftest import StubCluster
29
30 # python -m pytest tests/stub/test_multi_database.py -s -v
31
32
33 @pytest.mark.parametrize(
34 "test_script, test_database",
35 [
36 ("v3/dbms_cluster_routing_get_routing_table_system.script", DEFAULT_DATABASE),
37 ("v4x0/dbms_routing_get_routing_table_system_default.script", DEFAULT_DATABASE),
38 ("v4x0/dbms_routing_get_routing_table_system_neo4j.script", "neo4j"),
39 ]
40 )
41 def test_dbms_cluster_routing_get_routing_table(driver_info, test_script, test_database):
42 # python -m pytest tests/stub/test_multi_database.py -s -v -k test_dbms_cluster_routing_get_routing_table
43
44 test_config = {
45 "user_agent": "test",
46 "database": test_database,
47 }
48
49 with StubCluster(test_script):
50 uri = "neo4j://localhost:9001"
51 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
52 assert isinstance(driver, Neo4jDriver)
53 driver.close()
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 Neo4jDriver,
26 TRUST_ALL_CERTIFICATES,
27 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
28 DEFAULT_DATABASE,
29 )
30 from neo4j.api import (
31 READ_ACCESS,
32 WRITE_ACCESS,
33 )
34 from neo4j.exceptions import (
35 ServiceUnavailable,
36 ClientError,
37 TransientError,
38 SessionExpired,
39 ConfigurationError,
40 )
41 from neo4j._exceptions import (
42 BoltRoutingError,
43 BoltSecurityError,
44 )
45 from tests.stub.conftest import StubCluster
46
47 # python -m pytest tests/stub/test_routingdriver.py -s -v
48
49
50 @pytest.mark.parametrize(
51 "test_script",
52 [
53 "v3/router.script",
54 "v4x0/router.script",
55 ]
56 )
57 def test_neo4j_uri_scheme_constructs_neo4j_driver(driver_info, test_script):
58 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_uri_scheme_constructs_neo4j_driver
59 with StubCluster(test_script):
60 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
61 assert isinstance(driver, Neo4jDriver)
62
63
64 @pytest.mark.skip(reason="Cant close the Stub Server gracefully")
65 @pytest.mark.parametrize(
66 "test_script",
67 [
68 "v3/router.script",
69 "v4x0/router.script",
70 ]
71 )
72 def test_neo4j_uri_scheme_self_signed_certificate_constructs_neo4j_driver(driver_info, test_script):
73 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_uri_scheme_self_signed_certificate_constructs_neo4j_driver
74 with StubCluster(test_script):
75 uri = "neo4j+ssc://localhost:9001"
76
77 test_config = {
78 "user_agent": "test",
79 "max_connection_lifetime": 1000,
80 "max_connection_pool_size": 10,
81 "keep_alive": False,
82 "max_transaction_retry_time": 1,
83 "resolver": None,
84 }
85
86 try:
87 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
88 assert isinstance(driver, Neo4jDriver)
89 driver.close()
90 except ServiceUnavailable as error:
91 assert isinstance(error.__cause__, BoltSecurityError)
92 pytest.skip("Failed to establish encrypted connection")
93
94
95 @pytest.mark.skip(reason="Cant close the Stub Server gracefully")
96 @pytest.mark.parametrize(
97 "test_script",
98 [
99 "v3/router.script",
100 "v4x0/router.script",
101 ]
102 )
103 def test_neo4j_uri_scheme_secure_constructs_neo4j_driver(driver_info, test_script):
104 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_uri_scheme_secure_constructs_neo4j_driver
105 with StubCluster(test_script):
106 uri = "neo4j+s://localhost:9001"
107
108 test_config = {
109 "user_agent": "test",
110 "max_connection_lifetime": 1000,
111 "max_connection_pool_size": 10,
112 "keep_alive": False,
113 "max_transaction_retry_time": 1,
114 "resolver": None,
115 }
116
117 try:
118 driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **test_config)
119 assert isinstance(driver, Neo4jDriver)
120 driver.close()
121 except ServiceUnavailable as error:
122 assert isinstance(error.__cause__, BoltSecurityError)
123 pytest.skip("Failed to establish encrypted connection")
124
125
126 @pytest.mark.parametrize(
127 "test_uri",
128 [
129 "neo4j+ssc://localhost:9001",
130 "neo4j+s://localhost:9001",
131 ]
132 )
133 @pytest.mark.parametrize(
134 "test_config, expected_failure, expected_failure_message",
135 [
136 ({"encrypted": False}, ConfigurationError, "The config settings"),
137 ({"encrypted": True}, ConfigurationError, "The config settings"),
138 ({"encrypted": True, "trust": TRUST_ALL_CERTIFICATES}, ConfigurationError, "The config settings"),
139 ({"trust": TRUST_ALL_CERTIFICATES}, ConfigurationError, "The config settings"),
140 ({"trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES}, ConfigurationError, "The config settings"),
141 ]
142 )
143 def test_neo4j_uri_scheme_secure_constructs_neo4j_driver_config_error(driver_info, test_uri, test_config, expected_failure, expected_failure_message):
144 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_uri_scheme_secure_constructs_neo4j_driver_config_error
145 with pytest.raises(expected_failure) as error:
146 driver = GraphDatabase.driver(test_uri, auth=driver_info["auth_token"], **test_config)
147
148 assert error.match(expected_failure_message)
149
150
151 @pytest.mark.skip(reason="Flaky")
152 @pytest.mark.parametrize(
153 "test_script",
154 [
155 "v3/router.script",
156 "v4x0/router_port_9001_one_read_port_9004_one_write_port_9006.script",
157 ]
158 )
159 def test_neo4j_driver_verify_connectivity(driver_info, test_script):
160 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_driver_verify_connectivity
161 with StubCluster(test_script):
162 driver = GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"], user_agent="test")
163 assert isinstance(driver, Neo4jDriver)
164
165 with StubCluster(test_script):
166 assert driver.verify_connectivity() is not None
167 driver.close()
168
169
170 @pytest.mark.skip(reason="Flaky")
171 @pytest.mark.parametrize(
172 "test_script",
173 [
174 "v3/router.script",
175 "v4x0/router_port_9001_one_read_port_9004_one_write_port_9006.script",
176 ]
177 )
178 def test_neo4j_driver_verify_connectivity_server_down(driver_info, test_script):
179 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_neo4j_driver_verify_connectivity_server_down
180 with StubCluster(test_script):
181 driver = GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"], user_agent="test")
182 assert isinstance(driver, Neo4jDriver)
183
184 with pytest.raises(ServiceUnavailable):
185 driver.verify_connectivity()
186
187 driver.close()
188
189
190 @pytest.mark.parametrize(
191 "test_script",
192 [
193 "v3/non_router.script",
194 "v4x0/routing_table_failure_not_a_router.script",
195 ]
196 )
197 def test_cannot_discover_servers_on_non_router(driver_info, test_script):
198 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_cannot_discover_servers_on_non_router
199 with StubCluster(test_script):
200 with pytest.raises(ServiceUnavailable):
201 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]):
202 pass
203
204
205 @pytest.mark.parametrize(
206 "test_script",
207 [
208 "v3/silent_router.script",
209 "v4x0/routing_table_silent_router.script",
210 ]
211 )
212 def test_cannot_discover_servers_on_silent_router(driver_info, test_script):
213 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_cannot_discover_servers_on_silent_router
214 with StubCluster(test_script):
215 with pytest.raises(BoltRoutingError):
216 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]):
217 pass
218
219
220 @pytest.mark.parametrize(
221 "test_script",
222 [
223 "v3/router.script",
224 "v4x0/router.script",
225 ]
226 )
227 def test_should_discover_servers_on_driver_construction(driver_info, test_script):
228 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_discover_servers_on_driver_construction
229 with StubCluster(test_script):
230 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
231 table = driver._pool.routing_tables[DEFAULT_DATABASE]
232 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002),
233 ('127.0.0.1', 9003)}
234 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
235 assert table.writers == {('127.0.0.1', 9006)}
236
237
238 @pytest.mark.parametrize(
239 "test_scripts",
240 [
241 ("v3/router.script", "v3/return_1.script"),
242 ("v4x0/router.script", "v4x0/return_1_port_9004.script"),
243 ]
244 )
245 def test_should_be_able_to_read(driver_info, test_scripts):
246 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_be_able_to_read
247 with StubCluster(*test_scripts):
248 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
249 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
250 result = session.run("RETURN $x", {"x": 1})
251 for record in result:
252 assert record["x"] == 1
253 assert result.consume().server.address == ('127.0.0.1', 9004)
254
255
256 @pytest.mark.parametrize(
257 "test_scripts",
258 [
259 ("v3/router.script", "v3/create_a.script"),
260 ("v4x0/router.script", "v4x0/create_test_node_port_9006.script"),
261 ]
262 )
263 def test_should_be_able_to_write(driver_info, test_scripts):
264 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_be_able_to_write
265 with StubCluster(*test_scripts):
266 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
267 with driver.session(default_access_mode=WRITE_ACCESS, fetch_size=-1) as session:
268 result = session.run("CREATE (a $x)", {"x": {"name": "Alice"}})
269 assert not list(result)
270 assert result.consume().server.address == ('127.0.0.1', 9006)
271
272
273 @pytest.mark.parametrize(
274 "test_scripts",
275 [
276 ("v3/router.script", "v3/create_a.script"),
277 ("v4x0/router.script", "v4x0/create_test_node_port_9006.script"),
278 ]
279 )
280 def test_should_be_able_to_write_as_default(driver_info, test_scripts):
281 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_be_able_to_write_as_default
282 with StubCluster(*test_scripts):
283 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
284 with driver.session(fetch_size=-1) as session:
285 result = session.run("CREATE (a $x)", {"x": {"name": "Alice"}})
286 assert not list(result)
287 assert result.consume().server.address == ('127.0.0.1', 9006)
288
289
290 @pytest.mark.parametrize(
291 "test_scripts",
292 [
293 ("v3/router.script", "v3/disconnect_on_run_9004.script"),
294 ("v4x0/router.script", "v4x0/disconnect_on_run_port_9004.script"),
295 ]
296 )
297 def test_routing_disconnect_on_run(driver_info, test_scripts):
298 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_routing_disconnect_on_run
299 with StubCluster(*test_scripts):
300 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
301 with pytest.raises(SessionExpired):
302 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
303 session.run("RETURN $x", {"x": 1}).consume()
304
305
306 @pytest.mark.parametrize(
307 "test_scripts",
308 [
309 ("v3/router.script", "v3/disconnect_on_pull_all_9004.script"),
310 ("v4x0/router.script", "v4x0/disconnect_on_pull_port_9004.script"),
311 ]
312 )
313 def test_routing_disconnect_on_pull_all(driver_info, test_scripts):
314 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_routing_disconnect_on_pull_all
315 with StubCluster(*test_scripts):
316 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
317 with pytest.raises(SessionExpired):
318 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
319 session.run("RETURN $x", {"x": 1}).consume()
320
321
322 @pytest.mark.parametrize(
323 "test_scripts",
324 [
325 ("v3/router.script", "v3/return_1.script"),
326 ("v4x0/router.script", "v4x0/return_1_port_9004.script"),
327 ]
328 )
329 def test_should_disconnect_after_fetching_autocommit_result(driver_info, test_scripts):
330 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_disconnect_after_fetching_autocommit_result
331 with StubCluster(*test_scripts):
332 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
333 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
334 result = session.run("RETURN $x", {"x": 1})
335 assert session._connection is not None
336 result.consume()
337 assert session._connection is None
338
339
340 @pytest.mark.parametrize(
341 "test_scripts, test_run_args",
342 [
343 (("v3/router.script", "v3/return_1_twice_in_read_tx.script"), ("RETURN $x", {"x": 1})),
344 (("v4x0/router.script", "v4x0/tx_return_1_twice_port_9004.script"), ("RETURN 1", )),
345 ]
346 )
347 def test_should_disconnect_after_explicit_commit(driver_info, test_scripts, test_run_args):
348 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_disconnect_after_explicit_commit
349 with StubCluster(*test_scripts):
350 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
351 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
352 with session.begin_transaction() as tx:
353 result = tx.run(*test_run_args)
354 assert session._connection is not None
355 result.consume()
356 assert session._connection is not None
357 result = tx.run(*test_run_args)
358 assert session._connection is not None
359 result.consume()
360 assert session._connection is not None
361 assert session._connection is None
362
363
364 @pytest.mark.parametrize(
365 "test_scripts, test_run_args",
366 [
367 (("v3/router.script", "v3/return_1_twice_in_read_tx.script"), ("RETURN $x", {"x": 1})),
368 (("v4x0/router.script", "v4x0/tx_return_1_twice_port_9004.script"), ("RETURN 1", )),
369 ]
370 )
371 def test_default_access_mode_defined_at_session_level(driver_info, test_scripts, test_run_args):
372 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_default_access_mode_defined_at_session_level
373 with StubCluster(*test_scripts):
374 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
375 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
376 with session.begin_transaction() as tx:
377 result = tx.run(*test_run_args)
378 assert session._connection is not None
379 result.consume()
380 assert session._connection is not None
381 result = tx.run(*test_run_args)
382 assert session._connection is not None
383 result.consume()
384 assert session._connection is not None
385 assert session._connection is None
386
387
388 @pytest.mark.parametrize(
389 "test_scripts, test_run_args",
390 [
391 (("v3/router.script", "v3/return_1_twice.script"), ("RETURN $x", {"x": 1})),
392 (("v4x0/router.script", "v4x0/return_1_twice_port_9004.script"), ("RETURN 1", )),
393 ]
394 )
395 def test_should_reconnect_for_new_query(driver_info, test_scripts, test_run_args):
396 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_reconnect_for_new_query
397 with StubCluster(*test_scripts):
398 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
399 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
400 result_1 = session.run(*test_run_args)
401 assert session._connection is not None
402 result_1.consume()
403 assert session._connection is None
404 result_2 = session.run(*test_run_args)
405 assert session._connection is not None
406 result_2.consume()
407 assert session._connection is None
408
409
410 @pytest.mark.parametrize(
411 "test_scripts, test_run_args",
412 [
413 (("v3/router.script", "v3/return_1_twice.script"), ("RETURN $x", {"x": 1})),
414 (("v4x0/router.script", "v4x0/return_1_twice_port_9004.script"), ("RETURN 1", )),
415 ]
416 )
417 def test_should_retain_connection_if_fetching_multiple_results(driver_info, test_scripts, test_run_args):
418 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_retain_connection_if_fetching_multiple_results
419 with StubCluster(*test_scripts):
420 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
421 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
422 result_1 = session.run(*test_run_args)
423 result_2 = session.run(*test_run_args)
424 assert session._connection is not None
425 result_1.consume()
426 assert session._connection is not None
427 result_2.consume()
428 assert session._connection is None
429
430
431 @pytest.mark.parametrize(
432 "test_scripts, test_run_args",
433 [
434 (("v3/router.script", "v3/return_1_four_times.script"), ("RETURN $x", {"x": 1})),
435 (("v4x0/router.script", "v4x0/return_1_four_times_port_9004.script"), ("RETURN 1", )),
436 ]
437 )
438 def test_two_sessions_can_share_a_connection(driver_info, test_scripts, test_run_args):
439 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_two_sessions_can_share_a_connection
440 with StubCluster(*test_scripts):
441 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
442 session_1 = driver.session(default_access_mode=READ_ACCESS, fetch_size=-1)
443 session_2 = driver.session(default_access_mode=READ_ACCESS, fetch_size=-1)
444
445 result_1a = session_1.run(*test_run_args)
446 c = session_1._connection
447 result_1a.consume()
448
449 result_2a = session_2.run(*test_run_args)
450 assert session_2._connection is c
451 result_2a.consume()
452
453 result_1b = session_1.run(*test_run_args)
454 assert session_1._connection is c
455 result_1b.consume()
456
457 result_2b = session_2.run(*test_run_args)
458 assert session_2._connection is c
459 result_2b.consume()
460
461 session_2.close()
462 session_1.close()
463
464
465 @pytest.mark.parametrize(
466 "test_scripts, test_run_args",
467 [
468 (("v3/get_routing_table.script", "v3/return_1_on_9002.script"), ("RETURN $x", {"x": 1})),
469 (("v4x0/router_role_route_share_port_with_role_read_and_role_write.script", "v4x0/return_1_port_9002.script"), ("RETURN 1 AS x", )),
470 ]
471 )
472 def test_should_call_get_routing_table_procedure(driver_info, test_scripts, test_run_args):
473 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_call_get_routing_table_procedure
474 with StubCluster(*test_scripts):
475 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
476 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
477 result = session.run(*test_run_args)
478 for record in result:
479 assert record["x"] == 1
480 assert result.consume().server.address == ('127.0.0.1', 9002)
481
482
483 @pytest.mark.parametrize(
484 "test_scripts, test_run_args",
485 [
486 (("v3/get_routing_table_with_context.script", "v3/return_1_on_9002.script"), ("RETURN $x", {"x": 1})),
487 (("v4x0/router_get_routing_table_with_context.script", "v4x0/return_1_port_9002.script"), ("RETURN 1 AS x", )),
488 ]
489 )
490 def test_should_call_get_routing_table_with_context(driver_info, test_scripts, test_run_args):
491 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_call_get_routing_table_with_context
492 with StubCluster(*test_scripts):
493 uri = "neo4j://localhost:9001/?name=molly&age=1"
494 with GraphDatabase.driver(uri, auth=driver_info["auth_token"]) as driver:
495 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
496 result = session.run(*test_run_args)
497 for record in result:
498 assert record["x"] == 1
499 assert result.consume().server.address == ('127.0.0.1', 9002)
500
501
502 @pytest.mark.parametrize(
503 "test_scripts, test_run_args",
504 [
505 (("v3/router_no_writers.script", "v3/return_1_on_9005.script"), ("RETURN $x", {"x": 1})),
506 (("v4x0/router_with_no_role_write.script", "v4x0/return_1_port_9005.script"), ("RETURN 1 AS x", )),
507 ]
508 )
509 def test_should_serve_read_when_missing_writer(driver_info, test_scripts, test_run_args):
510 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_serve_read_when_missing_writer
511 with StubCluster(*test_scripts):
512 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
513 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
514 result = session.run(*test_run_args)
515 for record in result:
516 assert record["x"] == 1
517 assert result.consume().server.address == ('127.0.0.1', 9005)
518
519
520 @pytest.mark.parametrize(
521 "test_script",
522 [
523 "v3/router_no_readers.script",
524 "v4x0/router_with_no_role_read.script",
525 ]
526 )
527 def test_should_error_when_missing_reader(driver_info, test_script):
528 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_should_error_when_missing_reader
529 with StubCluster(test_script):
530 with pytest.raises(BoltRoutingError):
531 GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"])
532
533
534 @pytest.mark.parametrize(
535 "test_scripts, test_run_args",
536 [
537 (("v3/router.script", "v3/not_a_leader.script"), ("CREATE (n {name:'Bob'})", )),
538 (("v4x0/router.script", "v4x0/run_with_failure_cluster_not_a_leader.script"), ("CREATE (n:TEST {name:'test'})", )),
539 ]
540 )
541 def test_forgets_address_on_not_a_leader_error(driver_info, test_scripts, test_run_args):
542 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_forgets_address_on_not_a_leader_error
543 with StubCluster(*test_scripts):
544 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
545 with driver.session(default_access_mode=WRITE_ACCESS, fetch_size=-1) as session:
546 with pytest.raises(ClientError):
547 _ = session.run(*test_run_args)
548
549 pool = driver._pool
550 table = driver._pool.routing_tables[DEFAULT_DATABASE]
551
552 # address might still have connections in the pool, failed instance just can't serve writes
553 assert ('127.0.0.1', 9006) in pool.connections
554 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
555 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
556 # writer 127.0.0.1:9006 should've been forgotten because of an error
557 assert len(table.writers) == 0
558
559
560 @pytest.mark.parametrize(
561 "test_scripts, test_run_args",
562 [
563 (("v3/router.script", "v3/forbidden_on_read_only_database.script"), ("CREATE (n {name:'Bob'})", )),
564 (("v4x0/router.script", "v4x0/run_with_failure_forbidden_on_read_only_database.script"), ("CREATE (n:TEST {name:'test'})", )),
565 ]
566 )
567 def test_forgets_address_on_forbidden_on_read_only_database_error(driver_info, test_scripts, test_run_args):
568 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_forgets_address_on_forbidden_on_read_only_database_error
569 with StubCluster(*test_scripts):
570 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
571 with driver.session(default_access_mode=WRITE_ACCESS, fetch_size=-1) as session:
572 with pytest.raises(ClientError):
573 _ = session.run(*test_run_args)
574
575 pool = driver._pool
576 table = driver._pool.routing_tables[DEFAULT_DATABASE]
577
578 # address might still have connections in the pool, failed instance just can't serve writes
579 assert ('127.0.0.1', 9006) in pool.connections
580 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
581 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
582 # writer 127.0.0.1:9006 should've been forgotten because of an error
583 assert len(table.writers) == 0
584
585
586 @pytest.mark.parametrize(
587 "test_scripts, test_run_args",
588 [
589 (("v3/router.script", "v3/rude_reader.script"), ("RETURN 1", )),
590 (("v4x0/router.script", "v4x0/disconnect_on_pull_port_9004.script"), ("RETURN $x", {"x": 1})),
591 ]
592 )
593 def test_forgets_address_on_service_unavailable_error(driver_info, test_scripts, test_run_args):
594 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_forgets_address_on_service_unavailable_error
595 with StubCluster(*test_scripts):
596 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
597 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
598
599 pool = driver._pool
600 table = driver._pool.routing_tables[DEFAULT_DATABASE]
601 table.readers.remove(('127.0.0.1', 9005))
602
603 with pytest.raises(SessionExpired):
604 _ = session.run(*test_run_args)
605
606 # address should have connections in the pool but be inactive, it has failed
607 assert ('127.0.0.1', 9004) in pool.connections
608 conns = pool.connections[('127.0.0.1', 9004)]
609 conn = conns[0]
610 assert conn._closed is True
611 assert conn.in_use is True
612 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
613 # reader 127.0.0.1:9004 should've been forgotten because of an error
614 assert not table.readers
615 assert table.writers == {('127.0.0.1', 9006)}
616
617 assert conn.in_use is False
618
619
620 @pytest.mark.parametrize(
621 "test_scripts, test_run_args",
622 [
623 (("v3/router.script", "v3/database_unavailable.script"), ("RETURN 1", )),
624 (("v4x0/router.script", "v4x0/run_with_failure_database_unavailable.script"), ("RETURN 1", )),
625 ]
626 )
627 def test_forgets_address_on_database_unavailable_error(driver_info, test_scripts, test_run_args):
628 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_forgets_address_on_database_unavailable_error
629 with StubCluster(*test_scripts):
630 with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"]) as driver:
631 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
632
633 pool = driver._pool
634 table = driver._pool.routing_tables[DEFAULT_DATABASE]
635 table.readers.remove(('127.0.0.1', 9005))
636
637 with pytest.raises(TransientError) as raised:
638 _ = session.run(*test_run_args)
639 assert raised.exception.title == "DatabaseUnavailable"
640
641 pool = driver._pool
642 table = driver._pool.routing_tables[DEFAULT_DATABASE]
643
644 # address should not have connections in the pool, it has failed
645 assert ('127.0.0.1', 9004) not in pool.connections
646 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
647 # reader 127.0.0.1:9004 should've been forgotten because of an raised
648 assert not table.readers
649 assert table.writers == {('127.0.0.1', 9006)}
650
651
652 @pytest.mark.parametrize(
653 "test_scripts",
654 [
655 ("v4x1/router_get_routing_table_with_context.script", "v4x2/hello_with_routing_context_return_1_port_9002.script",),
656 ("v4x1/router_get_routing_table_with_context.script", "v4x2/hello_with_routing_context_return_1_port_9002.script",),
657 ]
658 )
659 def test_hello_routing(driver_info, test_scripts):
660 # python -m pytest tests/stub/test_routingdriver.py -s -v -k test_hello_routing
661 with StubCluster(*test_scripts):
662 uri = "neo4j://localhost:9001/?region=china&policy=my_policy"
663 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], user_agent="test") as driver:
664 with driver.session(default_access_mode=READ_ACCESS, fetch_size=-1) as session:
665 result = session.run("RETURN 1 AS x")
666 for record in result:
667 assert record["x"] == 1
668 address = result.consume().server.address
669 assert address.host == "127.0.0.1"
670 assert address.port == 9002
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j import (
24 GraphDatabase,
25 WRITE_ACCESS,
26 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
27 )
28 from neo4j.exceptions import ServiceUnavailable
29
30 from tests.stub.conftest import StubCluster
31
32 # python -m pytest tests/stub/test_transactions.py -s -v
33
34
35 driver_config = {
36 "encrypted": False,
37 "trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
38 "user_agent": "test",
39 "max_connection_lifetime": 1000,
40 "max_connection_pool_size": 10,
41 "keep_alive": False,
42 "max_transaction_retry_time": 1,
43 "resolver": None,
44 }
45
46 session_config = {
47 # "fetch_size": 100,
48 # "database": "default",
49 # "bookmarks": ["bookmark-1", ],
50 "default_access_mode": WRITE_ACCESS,
51 "connection_acquisition_timeout": 1.0,
52 "max_transaction_retry_time": 1.0,
53 "initial_retry_delay": 1.0,
54 "retry_delay_multiplier": 1.0,
55 "retry_delay_jitter_factor": 0.1,
56 "fetch_size": -1,
57 }
58
59
60
61
62
63 @pytest.mark.parametrize(
64 "test_script",
65 [
66 "v3/connection_error_on_commit.script",
67 "v4x0/tx_connection_error_on_commit.script",
68 ]
69 )
70 def test_connection_error_on_explicit_commit(driver_info, test_script):
71 # python -m pytest tests/stub/test_transactions.py -s -v -k test_connection_error_on_explicit_commit
72 with StubCluster(test_script):
73 uri = "bolt://127.0.0.1:9001"
74 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
75 with driver.session(**session_config) as session:
76 tx = session.begin_transaction()
77 result = tx.run("CREATE (n {name:'Bob'})")
78 _ = list(result)
79 with pytest.raises(ServiceUnavailable):
80 tx.commit()
81
82
83 @pytest.mark.parametrize(
84 "test_script",
85 [
86 "v3/connection_error_on_commit.script",
87 "v4x0/tx_connection_error_on_commit.script",
88 ]
89 )
90 def test_connection_error_on_commit(driver_info, test_script):
91 # python -m pytest tests/stub/test_transactions.py -s -v -k test_connection_error_on_commit
92 def create_bob(tx):
93 result = tx.run("CREATE (n {name:'Bob'})")
94 return list(result)
95
96 with StubCluster(test_script):
97 uri = "bolt://127.0.0.1:9001"
98 with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver:
99 with driver.session(**session_config) as session:
100 with pytest.raises(ServiceUnavailable):
101 session.write_transaction(create_bob)
(New empty file)
(New empty file)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import struct
22 from collections import OrderedDict
23 from io import BytesIO
24 from math import pi
25 from unittest import TestCase
26 from uuid import uuid4
27
28 from pytest import raises
29
30 from neo4j.packstream import Packer, UnpackableBuffer, Unpacker, Structure
31
32
33 class PackStreamTestCase(TestCase):
34
35 @classmethod
36 def packb(cls, *values):
37 stream = BytesIO()
38 packer = Packer(stream)
39 for value in values:
40 packer.pack(value)
41 return stream.getvalue()
42
43 @classmethod
44 def assert_packable(cls, value, packed_value):
45 stream_out = BytesIO()
46 packer = Packer(stream_out)
47 packer.pack(value)
48 packed = stream_out.getvalue()
49 try:
50 assert packed == packed_value
51 except AssertionError:
52 raise AssertionError("Packed value %r is %r instead of expected %r" %
53 (value, packed, packed_value))
54 unpacked = Unpacker(UnpackableBuffer(packed)).unpack()
55 try:
56 assert unpacked == value
57 except AssertionError:
58 raise AssertionError("Unpacked value %r is not equal to original %r" % (unpacked, value))
59
60 def test_none(self):
61 self.assert_packable(None, b"\xC0")
62
63 def test_boolean(self):
64 self.assert_packable(True, b"\xC3")
65 self.assert_packable(False, b"\xC2")
66
67 def test_negative_tiny_int(self):
68 for z in range(-16, 0):
69 self.assert_packable(z, bytes(bytearray([z + 0x100])))
70
71 def test_positive_tiny_int(self):
72 for z in range(0, 128):
73 self.assert_packable(z, bytes(bytearray([z])))
74
75 def test_negative_int8(self):
76 for z in range(-128, -16):
77 self.assert_packable(z, bytes(bytearray([0xC8, z + 0x100])))
78
79 def test_positive_int16(self):
80 for z in range(128, 32768):
81 expected = b"\xC9" + struct.pack(">h", z)
82 self.assert_packable(z, expected)
83
84 def test_negative_int16(self):
85 for z in range(-32768, -128):
86 expected = b"\xC9" + struct.pack(">h", z)
87 self.assert_packable(z, expected)
88
89 def test_positive_int32(self):
90 for e in range(15, 31):
91 z = 2 ** e
92 expected = b"\xCA" + struct.pack(">i", z)
93 self.assert_packable(z, expected)
94
95 def test_negative_int32(self):
96 for e in range(15, 31):
97 z = -(2 ** e + 1)
98 expected = b"\xCA" + struct.pack(">i", z)
99 self.assert_packable(z, expected)
100
101 def test_positive_int64(self):
102 for e in range(31, 63):
103 z = 2 ** e
104 expected = b"\xCB" + struct.pack(">q", z)
105 self.assert_packable(z, expected)
106
107 def test_negative_int64(self):
108 for e in range(31, 63):
109 z = -(2 ** e + 1)
110 expected = b"\xCB" + struct.pack(">q", z)
111 self.assert_packable(z, expected)
112
113 def test_integer_positive_overflow(self):
114 with raises(OverflowError):
115 self.packb(2 ** 63 + 1)
116
117 def test_integer_negative_overflow(self):
118 with raises(OverflowError):
119 self.packb(-(2 ** 63) - 1)
120
121 def test_zero_float64(self):
122 zero = 0.0
123 expected = b"\xC1" + struct.pack(">d", zero)
124 self.assert_packable(zero, expected)
125
126 def test_tau_float64(self):
127 tau = 2 * pi
128 expected = b"\xC1" + struct.pack(">d", tau)
129 self.assert_packable(tau, expected)
130
131 def test_positive_float64(self):
132 for e in range(0, 100):
133 r = float(2 ** e) + 0.5
134 expected = b"\xC1" + struct.pack(">d", r)
135 self.assert_packable(r, expected)
136
137 def test_negative_float64(self):
138 for e in range(0, 100):
139 r = -(float(2 ** e) + 0.5)
140 expected = b"\xC1" + struct.pack(">d", r)
141 self.assert_packable(r, expected)
142
143 def test_empty_bytes(self):
144 self.assert_packable(b"", b"\xCC\x00")
145
146 def test_empty_bytearray(self):
147 self.assert_packable(bytearray(), b"\xCC\x00")
148
149 def test_bytes_8(self):
150 self.assert_packable(bytearray(b"hello"), b"\xCC\x05hello")
151
152 def test_bytes_16(self):
153 b = bytearray(40000)
154 self.assert_packable(b, b"\xCD\x9C\x40" + b)
155
156 def test_bytes_32(self):
157 b = bytearray(80000)
158 self.assert_packable(b, b"\xCE\x00\x01\x38\x80" + b)
159
160 def test_bytearray_size_overflow(self):
161 stream_out = BytesIO()
162 packer = Packer(stream_out)
163 with raises(OverflowError):
164 packer.pack_bytes_header(2 ** 32)
165
166 def test_empty_string(self):
167 self.assert_packable(u"", b"\x80")
168
169 def test_tiny_strings(self):
170 for size in range(0x10):
171 self.assert_packable(u"A" * size, bytes(bytearray([0x80 + size]) + (b"A" * size)))
172
173 def test_string_8(self):
174 t = u"A" * 40
175 b = t.encode("utf-8")
176 self.assert_packable(t, b"\xD0\x28" + b)
177
178 def test_string_16(self):
179 t = u"A" * 40000
180 b = t.encode("utf-8")
181 self.assert_packable(t, b"\xD1\x9C\x40" + b)
182
183 def test_string_32(self):
184 t = u"A" * 80000
185 b = t.encode("utf-8")
186 self.assert_packable(t, b"\xD2\x00\x01\x38\x80" + b)
187
188 def test_unicode_string(self):
189 t = u"héllö"
190 b = t.encode("utf-8")
191 self.assert_packable(t, bytes(bytearray([0x80 + len(b)])) + b)
192
193 def test_string_size_overflow(self):
194 stream_out = BytesIO()
195 packer = Packer(stream_out)
196 with raises(OverflowError):
197 packer.pack_string_header(2 ** 32)
198
199 def test_empty_list(self):
200 self.assert_packable([], b"\x90")
201
202 def test_tiny_lists(self):
203 for size in range(0x10):
204 data_out = bytearray([0x90 + size]) + bytearray([1] * size)
205 self.assert_packable([1] * size, bytes(data_out))
206
207 def test_list_8(self):
208 l = [1] * 40
209 self.assert_packable(l, b"\xD4\x28" + (b"\x01" * 40))
210
211 def test_list_16(self):
212 l = [1] * 40000
213 self.assert_packable(l, b"\xD5\x9C\x40" + (b"\x01" * 40000))
214
215 def test_list_32(self):
216 l = [1] * 80000
217 self.assert_packable(l, b"\xD6\x00\x01\x38\x80" + (b"\x01" * 80000))
218
219 def test_nested_lists(self):
220 self.assert_packable([[[]]], b"\x91\x91\x90")
221
222 def test_list_stream(self):
223 packed_value = b"\xD7\x01\x02\x03\xDF"
224 unpacked_value = [1, 2, 3]
225 stream_out = BytesIO()
226 packer = Packer(stream_out)
227 packer.pack_list_stream_header()
228 packer.pack(1)
229 packer.pack(2)
230 packer.pack(3)
231 packer.pack_end_of_stream()
232 packed = stream_out.getvalue()
233 try:
234 assert packed == packed_value
235 except AssertionError:
236 raise AssertionError("Packed value is %r instead of expected %r" %
237 (packed, packed_value))
238 unpacked = Unpacker(UnpackableBuffer(packed)).unpack()
239 try:
240 assert unpacked == unpacked_value
241 except AssertionError:
242 raise AssertionError("Unpacked value %r is not equal to expected %r" %
243 (unpacked, unpacked_value))
244
245 def test_list_size_overflow(self):
246 stream_out = BytesIO()
247 packer = Packer(stream_out)
248 with raises(OverflowError):
249 packer.pack_list_header(2 ** 32)
250
251 def test_empty_map(self):
252 self.assert_packable({}, b"\xA0")
253
254 def test_tiny_maps(self):
255 for size in range(0x10):
256 data_in = OrderedDict()
257 data_out = bytearray([0xA0 + size])
258 for el in range(1, size + 1):
259 data_in[chr(64 + el)] = el
260 data_out += bytearray([0x81, 64 + el, el])
261 self.assert_packable(data_in, bytes(data_out))
262
263 def test_map_8(self):
264 d = OrderedDict([(u"A%s" % i, 1) for i in range(40)])
265 b = b"".join(self.packb(u"A%s" % i, 1) for i in range(40))
266 self.assert_packable(d, b"\xD8\x28" + b)
267
268 def test_map_16(self):
269 d = OrderedDict([(u"A%s" % i, 1) for i in range(40000)])
270 b = b"".join(self.packb(u"A%s" % i, 1) for i in range(40000))
271 self.assert_packable(d, b"\xD9\x9C\x40" + b)
272
273 def test_map_32(self):
274 d = OrderedDict([(u"A%s" % i, 1) for i in range(80000)])
275 b = b"".join(self.packb(u"A%s" % i, 1) for i in range(80000))
276 self.assert_packable(d, b"\xDA\x00\x01\x38\x80" + b)
277
278 def test_map_stream(self):
279 packed_value = b"\xDB\x81A\x01\x81B\x02\xDF"
280 unpacked_value = {u"A": 1, u"B": 2}
281 stream_out = BytesIO()
282 packer = Packer(stream_out)
283 packer.pack_map_stream_header()
284 packer.pack(u"A")
285 packer.pack(1)
286 packer.pack(u"B")
287 packer.pack(2)
288 packer.pack_end_of_stream()
289 packed = stream_out.getvalue()
290 try:
291 assert packed == packed_value
292 except AssertionError:
293 raise AssertionError("Packed value is %r instead of expected %r" %
294 (packed, packed_value))
295 unpacked = Unpacker(UnpackableBuffer(packed)).unpack()
296 try:
297 assert unpacked == unpacked_value
298 except AssertionError:
299 raise AssertionError("Unpacked value %r is not equal to expected %r" %
300 (unpacked, unpacked_value))
301
302 def test_map_size_overflow(self):
303 stream_out = BytesIO()
304 packer = Packer(stream_out)
305 with raises(OverflowError):
306 packer.pack_map_header(2 ** 32)
307
308 def test_illegal_signature(self):
309 with self.assertRaises(ValueError):
310 self.assert_packable(Structure(b"XXX"), b"\xB0XXX")
311
312 def test_empty_struct(self):
313 self.assert_packable(Structure(b"X"), b"\xB0X")
314
315 def test_tiny_structs(self):
316 for size in range(0x10):
317 fields = [1] * size
318 data_in = Structure(b"A", *fields)
319 data_out = bytearray([0xB0 + size, 0x41] + fields)
320 self.assert_packable(data_in, bytes(data_out))
321
322 def test_struct_size_overflow(self):
323 with raises(OverflowError):
324 fields = [1] * 16
325 self.packb(Structure(b"X", *fields))
326
327 def test_illegal_uuid(self):
328 with self.assertRaises(ValueError):
329 self.assert_packable(uuid4(), b"\xB0XXX")
(New empty file)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from collections import deque
22 from struct import unpack as struct_unpack
23
24 import pytest
25
26 from neo4j.io._common import MessageInbox
27 from neo4j.packstream import UnpackableBuffer, Unpacker
28
29
30 class FakeSocket:
31
32 def __init__(self, address):
33 self.address = address
34 self.captured = b""
35 self.messages = MessageInbox(self, on_error=print)
36
37 def getsockname(self):
38 return "127.0.0.1", 0xFFFF
39
40 def getpeername(self):
41 return self.address
42
43 def recv_into(self, buffer, nbytes):
44 data = self.captured[:nbytes]
45 actual = len(data)
46 buffer[:actual] = data
47 self.captured = self.captured[actual:]
48 return actual
49
50 def sendall(self, data):
51 self.captured += data
52
53 def close(self):
54 return
55
56 def pop_message(self):
57 return self.messages.pop()
58
59
60 class FakeSocket2:
61
62 def __init__(self, address=None, on_send=None):
63 self.address = address
64 self.recv_buffer = bytearray()
65 self._messages = MessageInbox(self, on_error=print)
66 self.on_send = on_send
67
68 def getsockname(self):
69 return "127.0.0.1", 0xFFFF
70
71 def getpeername(self):
72 return self.address
73
74 def recv_into(self, buffer, nbytes):
75 data = self.recv_buffer[:nbytes]
76 actual = len(data)
77 buffer[:actual] = data
78 self.recv_buffer = self.recv_buffer[actual:]
79 return actual
80
81 def sendall(self, data):
82 if callable(self.on_send):
83 self.on_send(data)
84
85 def close(self):
86 return
87
88 def inject(self, data):
89 self.recv_buffer += data
90
91 def pop_chunk(self):
92 chunk_size, = struct_unpack(">H", self.recv_buffer[:2])
93 print("CHUNK SIZE %r" % chunk_size)
94 end = 2 + chunk_size
95 chunk_data, self.recv_buffer = self.recv_buffer[2:end], self.recv_buffer[end:]
96 return chunk_data
97
98 def pop_message(self):
99 data = bytearray()
100 while True:
101 chunk = self.pop_chunk()
102 print("CHUNK %r" % chunk)
103 if chunk:
104 data.extend(chunk)
105 elif data:
106 break # end of message
107 else:
108 continue # NOOP
109 header = data[0]
110 n_fields = header % 0x10
111 tag = data[1]
112 buffer = UnpackableBuffer(data[2:])
113 unpacker = Unpacker(buffer)
114 fields = [unpacker.unpack() for _ in range(n_fields)]
115 return tag, fields
116
117
118 class FakeSocketPair:
119
120 def __init__(self, address):
121 self.client = FakeSocket2(address)
122 self.server = FakeSocket2()
123 self.client.on_send = self.server.inject
124 self.server.on_send = self.client.inject
125
126
127 @pytest.fixture
128 def fake_socket():
129 return FakeSocket
130
131
132 @pytest.fixture
133 def fake_socket_2():
134 return FakeSocket2
135
136
137 @pytest.fixture
138 def fake_socket_pair():
139 return FakeSocketPair
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 from neo4j.io import Bolt
23
24 # python -m pytest tests/unit/io/test_class_bolt.py -s -v
25
26
27 def test_class_method_protocol_handlers():
28 # python -m pytest tests/unit/io/test_class_bolt.py -s -v -k test_class_method_protocol_handlers
29 protocol_handlers = Bolt.protocol_handlers()
30 assert len(protocol_handlers) == 4
31
32
33 @pytest.mark.parametrize(
34 "test_input, expected",
35 [
36 ((0, 0), 0),
37 ((4, 0), 1),
38 ]
39 )
40 def test_class_method_protocol_handlers_with_protocol_version(test_input, expected):
41 # python -m pytest tests/unit/io/test_class_bolt.py -s -v -k test_class_method_protocol_handlers_with_protocol_version
42 protocol_handlers = Bolt.protocol_handlers(protocol_version=test_input)
43 assert len(protocol_handlers) == expected
44
45
46 def test_class_method_protocol_handlers_with_invalid_protocol_version():
47 # python -m pytest tests/unit/io/test_class_bolt.py -s -v -k test_class_method_protocol_handlers_with_invalid_protocol_version
48 with pytest.raises(TypeError):
49 Bolt.protocol_handlers(protocol_version=2)
50
51
52 def test_class_method_get_handshake():
53 # python -m pytest tests/unit/io/test_class_bolt.py -s -v -k test_class_method_get_handshake
54 handshake = Bolt.get_handshake()
55 assert handshake == b"\x00\x00\x02\x04\x00\x00\x01\x04\x00\x00\x00\x04\x00\x00\x00\x03"
56
57
58 def test_magic_preamble():
59 # python -m pytest tests/unit/io/test_class_bolt.py -s -v -k test_magic_preamble
60 preamble = 0x6060B017
61 preamble_bytes = preamble.to_bytes(4, byteorder="big")
62 assert Bolt.MAGIC_PREAMBLE == preamble_bytes
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.io._bolt3 import Bolt3
24 from neo4j.conf import PoolConfig
25 from neo4j.exceptions import (
26 ConfigurationError,
27 )
28
29 # python -m pytest tests/unit/io/test_class_bolt3.py -s -v
30
31
32 def test_conn_timed_out(fake_socket):
33 address = ("127.0.0.1", 7687)
34 max_connection_lifetime = 0
35 connection = Bolt3(address, fake_socket(address), max_connection_lifetime)
36 assert connection.timedout() is True
37
38
39 def test_conn_not_timed_out_if_not_enabled(fake_socket):
40 address = ("127.0.0.1", 7687)
41 max_connection_lifetime = -1
42 connection = Bolt3(address, fake_socket(address), max_connection_lifetime)
43 assert connection.timedout() is False
44
45
46 def test_conn_not_timed_out(fake_socket):
47 address = ("127.0.0.1", 7687)
48 max_connection_lifetime = 999999999
49 connection = Bolt3(address, fake_socket(address), max_connection_lifetime)
50 assert connection.timedout() is False
51
52
53 def test_db_extra_not_supported_in_begin(fake_socket):
54 address = ("127.0.0.1", 7687)
55 connection = Bolt3(address, fake_socket(address), PoolConfig.max_connection_lifetime)
56 with pytest.raises(ConfigurationError):
57 connection.begin(db="something")
58
59
60 def test_db_extra_not_supported_in_run(fake_socket):
61 address = ("127.0.0.1", 7687)
62 connection = Bolt3(address, fake_socket(address), PoolConfig.max_connection_lifetime)
63 with pytest.raises(ConfigurationError):
64 connection.run("", db="something")
65
66
67 def test_simple_discard(fake_socket):
68 address = ("127.0.0.1", 7687)
69 socket = fake_socket(address)
70 connection = Bolt3(address, socket, PoolConfig.max_connection_lifetime)
71 connection.discard()
72 connection.send_all()
73 tag, fields = socket.pop_message()
74 assert tag == b"\x2F"
75 assert len(fields) == 0
76
77
78 def test_simple_pull(fake_socket):
79 address = ("127.0.0.1", 7687)
80 socket = fake_socket(address)
81 connection = Bolt3(address, socket, PoolConfig.max_connection_lifetime)
82 connection.pull()
83 connection.send_all()
84 tag, fields = socket.pop_message()
85 assert tag == b"\x3F"
86 assert len(fields) == 0
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.io._bolt4 import Bolt4x0
24 from neo4j.conf import PoolConfig
25
26
27 def test_conn_timed_out(fake_socket):
28 address = ("127.0.0.1", 7687)
29 max_connection_lifetime = 0
30 connection = Bolt4x0(address, fake_socket(address), max_connection_lifetime)
31 assert connection.timedout() is True
32
33
34 def test_conn_not_timed_out_if_not_enabled(fake_socket):
35 address = ("127.0.0.1", 7687)
36 max_connection_lifetime = -1
37 connection = Bolt4x0(address, fake_socket(address), max_connection_lifetime)
38 assert connection.timedout() is False
39
40
41 def test_conn_not_timed_out(fake_socket):
42 address = ("127.0.0.1", 7687)
43 max_connection_lifetime = 999999999
44 connection = Bolt4x0(address, fake_socket(address), max_connection_lifetime)
45 assert connection.timedout() is False
46
47
48 def test_db_extra_in_begin(fake_socket):
49 address = ("127.0.0.1", 7687)
50 socket = fake_socket(address)
51 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
52 connection.begin(db="something")
53 connection.send_all()
54 tag, fields = socket.pop_message()
55 assert tag == b"\x11"
56 assert len(fields) == 1
57 assert fields[0] == {"db": "something"}
58
59
60 def test_db_extra_in_run(fake_socket):
61 address = ("127.0.0.1", 7687)
62 socket = fake_socket(address)
63 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
64 connection.run("", {}, db="something")
65 connection.send_all()
66 tag, fields = socket.pop_message()
67 assert tag == b"\x10"
68 assert len(fields) == 3
69 assert fields[0] == ""
70 assert fields[1] == {}
71 assert fields[2] == {"db": "something"}
72
73
74 def test_n_extra_in_discard(fake_socket):
75 address = ("127.0.0.1", 7687)
76 socket = fake_socket(address)
77 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
78 connection.discard(n=666)
79 connection.send_all()
80 tag, fields = socket.pop_message()
81 assert tag == b"\x2F"
82 assert len(fields) == 1
83 assert fields[0] == {"n": 666}
84
85
86 @pytest.mark.parametrize(
87 "test_input, expected",
88 [
89 (666, {"n": -1, "qid": 666}),
90 (-1, {"n": -1}),
91 ]
92 )
93 def test_qid_extra_in_discard(fake_socket, test_input, expected):
94 address = ("127.0.0.1", 7687)
95 socket = fake_socket(address)
96 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
97 connection.discard(qid=test_input)
98 connection.send_all()
99 tag, fields = socket.pop_message()
100 assert tag == b"\x2F"
101 assert len(fields) == 1
102 assert fields[0] == expected
103
104
105 @pytest.mark.parametrize(
106 "test_input, expected",
107 [
108 (777, {"n": 666, "qid": 777}),
109 (-1, {"n": 666}),
110 ]
111 )
112 def test_n_and_qid_extras_in_discard(fake_socket, test_input, expected):
113 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_n_and_qid_extras_in_discard
114 address = ("127.0.0.1", 7687)
115 socket = fake_socket(address)
116 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
117 connection.discard(n=666, qid=test_input)
118 connection.send_all()
119 tag, fields = socket.pop_message()
120 assert tag == b"\x2F"
121 assert len(fields) == 1
122 assert fields[0] == expected
123
124
125 @pytest.mark.parametrize(
126 "test_input, expected",
127 [
128 (666, {"n": 666}),
129 (-1, {"n": -1}),
130 ]
131 )
132 def test_n_extra_in_pull(fake_socket, test_input, expected):
133 address = ("127.0.0.1", 7687)
134 socket = fake_socket(address)
135 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
136 connection.pull(n=test_input)
137 connection.send_all()
138 tag, fields = socket.pop_message()
139 assert tag == b"\x3F"
140 assert len(fields) == 1
141 assert fields[0] == expected
142
143
144 @pytest.mark.parametrize(
145 "test_input, expected",
146 [
147 (777, {"n": -1, "qid": 777}),
148 (-1, {"n": -1}),
149 ]
150 )
151 def test_qid_extra_in_pull(fake_socket, test_input, expected):
152 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_qid_extra_in_pull
153 address = ("127.0.0.1", 7687)
154 socket = fake_socket(address)
155 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
156 connection.pull(qid=test_input)
157 connection.send_all()
158 tag, fields = socket.pop_message()
159 assert tag == b"\x3F"
160 assert len(fields) == 1
161 assert fields[0] == expected
162
163
164 def test_n_and_qid_extras_in_pull(fake_socket):
165 address = ("127.0.0.1", 7687)
166 socket = fake_socket(address)
167 connection = Bolt4x0(address, socket, PoolConfig.max_connection_lifetime)
168 connection.pull(n=666, qid=777)
169 connection.send_all()
170 tag, fields = socket.pop_message()
171 assert tag == b"\x3F"
172 assert len(fields) == 1
173 assert fields[0] == {"n": 666, "qid": 777}
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.io._bolt4 import Bolt4x1
24 from neo4j.conf import PoolConfig
25
26
27 def test_conn_timed_out(fake_socket):
28 address = ("127.0.0.1", 7687)
29 max_connection_lifetime = 0
30 connection = Bolt4x1(address, fake_socket(address), max_connection_lifetime)
31 assert connection.timedout() is True
32
33
34 def test_conn_not_timed_out_if_not_enabled(fake_socket):
35 address = ("127.0.0.1", 7687)
36 max_connection_lifetime = -1
37 connection = Bolt4x1(address, fake_socket(address), max_connection_lifetime)
38 assert connection.timedout() is False
39
40
41 def test_conn_not_timed_out(fake_socket):
42 address = ("127.0.0.1", 7687)
43 max_connection_lifetime = 999999999
44 connection = Bolt4x1(address, fake_socket(address), max_connection_lifetime)
45 assert connection.timedout() is False
46
47
48 def test_db_extra_in_begin(fake_socket):
49 address = ("127.0.0.1", 7687)
50 socket = fake_socket(address)
51 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
52 connection.begin(db="something")
53 connection.send_all()
54 tag, fields = socket.pop_message()
55 assert tag == b"\x11"
56 assert len(fields) == 1
57 assert fields[0] == {"db": "something"}
58
59
60 def test_db_extra_in_run(fake_socket):
61 address = ("127.0.0.1", 7687)
62 socket = fake_socket(address)
63 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
64 connection.run("", {}, db="something")
65 connection.send_all()
66 tag, fields = socket.pop_message()
67 assert tag == b"\x10"
68 assert len(fields) == 3
69 assert fields[0] == ""
70 assert fields[1] == {}
71 assert fields[2] == {"db": "something"}
72
73
74 def test_n_extra_in_discard(fake_socket):
75 address = ("127.0.0.1", 7687)
76 socket = fake_socket(address)
77 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
78 connection.discard(n=666)
79 connection.send_all()
80 tag, fields = socket.pop_message()
81 assert tag == b"\x2F"
82 assert len(fields) == 1
83 assert fields[0] == {"n": 666}
84
85
86 @pytest.mark.parametrize(
87 "test_input, expected",
88 [
89 (666, {"n": -1, "qid": 666}),
90 (-1, {"n": -1}),
91 ]
92 )
93 def test_qid_extra_in_discard(fake_socket, test_input, expected):
94 address = ("127.0.0.1", 7687)
95 socket = fake_socket(address)
96 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
97 connection.discard(qid=test_input)
98 connection.send_all()
99 tag, fields = socket.pop_message()
100 assert tag == b"\x2F"
101 assert len(fields) == 1
102 assert fields[0] == expected
103
104
105 @pytest.mark.parametrize(
106 "test_input, expected",
107 [
108 (777, {"n": 666, "qid": 777}),
109 (-1, {"n": 666}),
110 ]
111 )
112 def test_n_and_qid_extras_in_discard(fake_socket, test_input, expected):
113 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_n_and_qid_extras_in_discard
114 address = ("127.0.0.1", 7687)
115 socket = fake_socket(address)
116 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
117 connection.discard(n=666, qid=test_input)
118 connection.send_all()
119 tag, fields = socket.pop_message()
120 assert tag == b"\x2F"
121 assert len(fields) == 1
122 assert fields[0] == expected
123
124
125 @pytest.mark.parametrize(
126 "test_input, expected",
127 [
128 (666, {"n": 666}),
129 (-1, {"n": -1}),
130 ]
131 )
132 def test_n_extra_in_pull(fake_socket, test_input, expected):
133 address = ("127.0.0.1", 7687)
134 socket = fake_socket(address)
135 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
136 connection.pull(n=test_input)
137 connection.send_all()
138 tag, fields = socket.pop_message()
139 assert tag == b"\x3F"
140 assert len(fields) == 1
141 assert fields[0] == expected
142
143
144 @pytest.mark.parametrize(
145 "test_input, expected",
146 [
147 (777, {"n": -1, "qid": 777}),
148 (-1, {"n": -1}),
149 ]
150 )
151 def test_qid_extra_in_pull(fake_socket, test_input, expected):
152 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_qid_extra_in_pull
153 address = ("127.0.0.1", 7687)
154 socket = fake_socket(address)
155 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
156 connection.pull(qid=test_input)
157 connection.send_all()
158 tag, fields = socket.pop_message()
159 assert tag == b"\x3F"
160 assert len(fields) == 1
161 assert fields[0] == expected
162
163
164 def test_n_and_qid_extras_in_pull(fake_socket):
165 address = ("127.0.0.1", 7687)
166 socket = fake_socket(address)
167 connection = Bolt4x1(address, socket, PoolConfig.max_connection_lifetime)
168 connection.pull(n=666, qid=777)
169 connection.send_all()
170 tag, fields = socket.pop_message()
171 assert tag == b"\x3F"
172 assert len(fields) == 1
173 assert fields[0] == {"n": 666, "qid": 777}
174
175
176 def test_hello_passes_routing_metadata(fake_socket_pair):
177 address = ("127.0.0.1", 7687)
178 sockets = fake_socket_pair(address)
179 # TODO helper method for encoding messages
180 sockets.server.sendall(b"\x00\x03\xB1\x70\xA0\x00\x00")
181 connection = Bolt4x1(address, sockets.client, PoolConfig.max_connection_lifetime,
182 routing_context={"foo": "bar"})
183 connection.hello()
184 tag, fields = sockets.server.pop_message()
185 assert tag == 0x01
186 assert len(fields) == 1
187 assert fields[0]["routing"] == {"foo": "bar"}
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.io._bolt4 import Bolt4x2
24 from neo4j.conf import PoolConfig
25
26
27 def test_conn_timed_out(fake_socket):
28 address = ("127.0.0.1", 7687)
29 max_connection_lifetime = 0
30 connection = Bolt4x2(address, fake_socket(address), max_connection_lifetime)
31 assert connection.timedout() is True
32
33
34 def test_conn_not_timed_out_if_not_enabled(fake_socket):
35 address = ("127.0.0.1", 7687)
36 max_connection_lifetime = -1
37 connection = Bolt4x2(address, fake_socket(address), max_connection_lifetime)
38 assert connection.timedout() is False
39
40
41 def test_conn_not_timed_out(fake_socket):
42 address = ("127.0.0.1", 7687)
43 max_connection_lifetime = 999999999
44 connection = Bolt4x2(address, fake_socket(address), max_connection_lifetime)
45 assert connection.timedout() is False
46
47
48 def test_db_extra_in_begin(fake_socket):
49 address = ("127.0.0.1", 7687)
50 socket = fake_socket(address)
51 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
52 connection.begin(db="something")
53 connection.send_all()
54 tag, fields = socket.pop_message()
55 assert tag == b"\x11"
56 assert len(fields) == 1
57 assert fields[0] == {"db": "something"}
58
59
60 def test_db_extra_in_run(fake_socket):
61 address = ("127.0.0.1", 7687)
62 socket = fake_socket(address)
63 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
64 connection.run("", {}, db="something")
65 connection.send_all()
66 tag, fields = socket.pop_message()
67 assert tag == b"\x10"
68 assert len(fields) == 3
69 assert fields[0] == ""
70 assert fields[1] == {}
71 assert fields[2] == {"db": "something"}
72
73
74 def test_n_extra_in_discard(fake_socket):
75 address = ("127.0.0.1", 7687)
76 socket = fake_socket(address)
77 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
78 connection.discard(n=666)
79 connection.send_all()
80 tag, fields = socket.pop_message()
81 assert tag == b"\x2F"
82 assert len(fields) == 1
83 assert fields[0] == {"n": 666}
84
85
86 @pytest.mark.parametrize(
87 "test_input, expected",
88 [
89 (666, {"n": -1, "qid": 666}),
90 (-1, {"n": -1}),
91 ]
92 )
93 def test_qid_extra_in_discard(fake_socket, test_input, expected):
94 address = ("127.0.0.1", 7687)
95 socket = fake_socket(address)
96 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
97 connection.discard(qid=test_input)
98 connection.send_all()
99 tag, fields = socket.pop_message()
100 assert tag == b"\x2F"
101 assert len(fields) == 1
102 assert fields[0] == expected
103
104
105 @pytest.mark.parametrize(
106 "test_input, expected",
107 [
108 (777, {"n": 666, "qid": 777}),
109 (-1, {"n": 666}),
110 ]
111 )
112 def test_n_and_qid_extras_in_discard(fake_socket, test_input, expected):
113 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_n_and_qid_extras_in_discard
114 address = ("127.0.0.1", 7687)
115 socket = fake_socket(address)
116 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
117 connection.discard(n=666, qid=test_input)
118 connection.send_all()
119 tag, fields = socket.pop_message()
120 assert tag == b"\x2F"
121 assert len(fields) == 1
122 assert fields[0] == expected
123
124
125 @pytest.mark.parametrize(
126 "test_input, expected",
127 [
128 (666, {"n": 666}),
129 (-1, {"n": -1}),
130 ]
131 )
132 def test_n_extra_in_pull(fake_socket, test_input, expected):
133 address = ("127.0.0.1", 7687)
134 socket = fake_socket(address)
135 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
136 connection.pull(n=test_input)
137 connection.send_all()
138 tag, fields = socket.pop_message()
139 assert tag == b"\x3F"
140 assert len(fields) == 1
141 assert fields[0] == expected
142
143
144 @pytest.mark.parametrize(
145 "test_input, expected",
146 [
147 (777, {"n": -1, "qid": 777}),
148 (-1, {"n": -1}),
149 ]
150 )
151 def test_qid_extra_in_pull(fake_socket, test_input, expected):
152 # python -m pytest tests/unit/io/test_class_bolt4x0.py -s -k test_qid_extra_in_pull
153 address = ("127.0.0.1", 7687)
154 socket = fake_socket(address)
155 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
156 connection.pull(qid=test_input)
157 connection.send_all()
158 tag, fields = socket.pop_message()
159 assert tag == b"\x3F"
160 assert len(fields) == 1
161 assert fields[0] == expected
162
163
164 def test_n_and_qid_extras_in_pull(fake_socket):
165 address = ("127.0.0.1", 7687)
166 socket = fake_socket(address)
167 connection = Bolt4x2(address, socket, PoolConfig.max_connection_lifetime)
168 connection.pull(n=666, qid=777)
169 connection.send_all()
170 tag, fields = socket.pop_message()
171 assert tag == b"\x3F"
172 assert len(fields) == 1
173 assert fields[0] == {"n": 666, "qid": 777}
174
175
176 def test_hello_passes_routing_metadata(fake_socket_pair):
177 address = ("127.0.0.1", 7687)
178 sockets = fake_socket_pair(address)
179 # TODO helper method for encoding messages
180 sockets.server.sendall(b"\x00\x03\xB1\x70\xA0\x00\x00")
181 connection = Bolt4x2(address, sockets.client, PoolConfig.max_connection_lifetime,
182 routing_context={"foo": "bar"})
183 connection.hello()
184 tag, fields = sockets.server.pop_message()
185 assert tag == 0x01
186 assert len(fields) == 1
187 assert fields[0]["routing"] == {"foo": "bar"}
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22 import pytest
23 from threading import (
24 Thread,
25 Event,
26 )
27 from neo4j import (
28 Config,
29 PoolConfig,
30 WorkspaceConfig,
31 )
32 from neo4j.io import (
33 Bolt,
34 BoltPool,
35 IOPool
36 )
37 from neo4j.exceptions import (
38 ClientError,
39 ServiceUnavailable,
40 )
41
42
43 class FakeSocket:
44 def __init__(self, address):
45 self.address = address
46
47 def getpeername(self):
48 return self.address
49
50 def sendall(self, data):
51 return
52
53 def close(self):
54 return
55
56
57 class QuickConnection:
58
59 def __init__(self, socket):
60 self.socket = socket
61 self.address = socket.getpeername()
62
63 def reset(self):
64 pass
65
66 def close(self):
67 self.socket.close()
68
69 def closed(self):
70 return False
71
72 def defunct(self):
73 return False
74
75 def timedout(self):
76 return False
77
78
79 class FakeBoltPool(IOPool):
80
81 def __init__(self, address, *, auth=None, **config):
82 self.pool_config, self.workspace_config = Config.consume_chain(config, PoolConfig, WorkspaceConfig)
83 if config:
84 raise ValueError("Unexpected config keys: %s" % ", ".join(config.keys()))
85
86 def opener(addr, timeout):
87 return QuickConnection(FakeSocket(addr))
88
89 super().__init__(opener, self.pool_config, self.workspace_config)
90 self.address = address
91
92 def acquire(self, access_mode=None, timeout=None, database=None):
93 return self._acquire(self.address, timeout)
94
95
96 class BoltTestCase(TestCase):
97
98 def test_open(self):
99 with pytest.raises(ServiceUnavailable):
100 connection = Bolt.open(("localhost", 9999), auth=("test", "test"))
101
102 def test_open_timeout(self):
103 with pytest.raises(ServiceUnavailable):
104 connection = Bolt.open(("localhost", 9999), auth=("test", "test"), timeout=1)
105
106 def test_ping(self):
107 protocol_version = Bolt.ping(("localhost", 9999))
108 assert protocol_version is None
109
110 def test_ping_timeout(self):
111 protocol_version = Bolt.ping(("localhost", 9999), timeout=1)
112 assert protocol_version is None
113
114
115 class ConnectionPoolTestCase(TestCase):
116
117 def setUp(self):
118 self.pool = FakeBoltPool(("127.0.0.1", 7687))
119
120 def tearDown(self):
121 self.pool.close()
122
123 def assert_pool_size(self, address, expected_active, expected_inactive, pool=None):
124 if pool is None:
125 pool = self.pool
126 try:
127 connections = pool.connections[address]
128 except KeyError:
129 self.assertEqual(0, expected_active)
130 self.assertEqual(0, expected_inactive)
131 else:
132 self.assertEqual(expected_active, len([cx for cx in connections if cx.in_use]))
133 self.assertEqual(expected_inactive, len([cx for cx in connections if not cx.in_use]))
134
135 def test_can_acquire(self):
136 address = ("127.0.0.1", 7687)
137 connection = self.pool._acquire(address, timeout=3)
138 assert connection.address == address
139 self.assert_pool_size(address, 1, 0)
140
141 def test_can_acquire_twice(self):
142 address = ("127.0.0.1", 7687)
143 connection_1 = self.pool._acquire(address, timeout=3)
144 connection_2 = self.pool._acquire(address, timeout=3)
145 assert connection_1.address == address
146 assert connection_2.address == address
147 assert connection_1 is not connection_2
148 self.assert_pool_size(address, 2, 0)
149
150 def test_can_acquire_two_addresses(self):
151 address_1 = ("127.0.0.1", 7687)
152 address_2 = ("127.0.0.1", 7474)
153 connection_1 = self.pool._acquire(address_1, timeout=3)
154 connection_2 = self.pool._acquire(address_2, timeout=3)
155 assert connection_1.address == address_1
156 assert connection_2.address == address_2
157 self.assert_pool_size(address_1, 1, 0)
158 self.assert_pool_size(address_2, 1, 0)
159
160 def test_can_acquire_and_release(self):
161 address = ("127.0.0.1", 7687)
162 connection = self.pool._acquire(address, timeout=3)
163 self.assert_pool_size(address, 1, 0)
164 self.pool.release(connection)
165 self.assert_pool_size(address, 0, 1)
166
167 def test_releasing_twice(self):
168 address = ("127.0.0.1", 7687)
169 connection = self.pool._acquire(address, timeout=3)
170 self.pool.release(connection)
171 self.assert_pool_size(address, 0, 1)
172 self.pool.release(connection)
173 self.assert_pool_size(address, 0, 1)
174
175 def test_in_use_count(self):
176 address = ("127.0.0.1", 7687)
177 self.assertEqual(self.pool.in_use_connection_count(address), 0)
178 connection = self.pool._acquire(address, timeout=3)
179 self.assertEqual(self.pool.in_use_connection_count(address), 1)
180 self.pool.release(connection)
181 self.assertEqual(self.pool.in_use_connection_count(address), 0)
182
183 def test_max_conn_pool_size(self):
184 with FakeBoltPool((), max_connection_pool_size=1) as pool:
185 address = ("127.0.0.1", 7687)
186 pool._acquire(address, timeout=0)
187 self.assertEqual(pool.in_use_connection_count(address), 1)
188 with self.assertRaises(ClientError):
189 pool._acquire(address, timeout=0)
190 self.assertEqual(pool.in_use_connection_count(address), 1)
191
192 def test_multithread(self):
193 with FakeBoltPool((), max_connection_pool_size=5) as pool:
194 address = ("127.0.0.1", 7687)
195 releasing_event = Event()
196
197 # We start 10 threads to compete connections from pool with size of 5
198 threads = []
199 for i in range(10):
200 t = Thread(target=acquire_release_conn, args=(pool, address, releasing_event))
201 t.start()
202 threads.append(t)
203
204 # The pool size should be 5, all are in-use
205 self.assert_pool_size(address, 5, 0, pool)
206 # Now we allow thread to release connections they obtained from pool
207 releasing_event.set()
208
209 # wait for all threads to release connections back to pool
210 for t in threads:
211 t.join()
212 # The pool size is still 5, but all are free
213 self.assert_pool_size(address, 0, 5, pool)
214
215
216 def acquire_release_conn(pool, address, releasing_event):
217 conn = pool._acquire(address, timeout=3)
218 releasing_event.wait()
219 pool.release(conn)
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j.io import (
24 Bolt,
25 Neo4jPool,
26 )
27 from neo4j.routing import (
28 OrderedSet,
29 RoutingTable,
30 )
31 from neo4j.api import (
32 DEFAULT_DATABASE,
33 )
34
35 VALID_ROUTING_RECORD = {
36 "ttl": 300,
37 "servers": [
38 {"role": "ROUTE", "addresses": ["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]},
39 {"role": "READ", "addresses": ["127.0.0.1:9004", "127.0.0.1:9005"]},
40 {"role": "WRITE", "addresses": ["127.0.0.1:9006"]},
41 ],
42 }
43
44 VALID_ROUTING_RECORD_WITH_EXTRA_ROLE = {
45 "ttl": 300,
46 "servers": [
47 {"role": "ROUTE", "addresses": ["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]},
48 {"role": "READ", "addresses": ["127.0.0.1:9004", "127.0.0.1:9005"]},
49 {"role": "WRITE", "addresses": ["127.0.0.1:9006"]},
50 {"role": "MAGIC", "addresses": ["127.0.0.1:9007"]},
51 ],
52 }
53
54
55 class OrderedSetTestCase(TestCase):
56 def test_should_repr_as_set(self):
57 s = OrderedSet([1, 2, 3])
58 assert repr(s) == "{1, 2, 3}"
59
60 def test_should_contain_element(self):
61 s = OrderedSet([1, 2, 3])
62 assert 2 in s
63
64 def test_should_not_contain_non_element(self):
65 s = OrderedSet([1, 2, 3])
66 assert 4 not in s
67
68 def test_should_be_able_to_get_item_if_empty(self):
69 s = OrderedSet([])
70 with self.assertRaises(IndexError):
71 _ = s[0]
72
73 def test_should_be_able_to_get_items_by_index(self):
74 s = OrderedSet([1, 2, 3])
75 self.assertEqual(s[0], 1)
76 self.assertEqual(s[1], 2)
77 self.assertEqual(s[2], 3)
78
79 def test_should_be_iterable(self):
80 s = OrderedSet([1, 2, 3])
81 assert list(iter(s)) == [1, 2, 3]
82
83 def test_should_have_length(self):
84 s = OrderedSet([1, 2, 3])
85 assert len(s) == 3
86
87 def test_should_be_able_to_add_new(self):
88 s = OrderedSet([1, 2, 3])
89 s.add(4)
90 assert list(s) == [1, 2, 3, 4]
91
92 def test_should_be_able_to_add_existing(self):
93 s = OrderedSet([1, 2, 3])
94 s.add(2)
95 assert list(s) == [1, 2, 3]
96
97 def test_should_be_able_to_clear(self):
98 s = OrderedSet([1, 2, 3])
99 s.clear()
100 assert list(s) == []
101
102 def test_should_be_able_to_discard_existing(self):
103 s = OrderedSet([1, 2, 3])
104 s.discard(2)
105 assert list(s) == [1, 3]
106
107 def test_should_be_able_to_discard_non_existing(self):
108 s = OrderedSet([1, 2, 3])
109 s.discard(4)
110 assert list(s) == [1, 2, 3]
111
112 def test_should_be_able_to_remove_existing(self):
113 s = OrderedSet([1, 2, 3])
114 s.remove(2)
115 assert list(s) == [1, 3]
116
117 def test_should_not_be_able_to_remove_non_existing(self):
118 s = OrderedSet([1, 2, 3])
119 with self.assertRaises(ValueError):
120 s.remove(4)
121
122 def test_should_be_able_to_update(self):
123 s = OrderedSet([1, 2, 3])
124 s.update([3, 4, 5])
125 assert list(s) == [1, 2, 3, 4, 5]
126
127 def test_should_be_able_to_replace(self):
128 s = OrderedSet([1, 2, 3])
129 s.replace([3, 4, 5])
130 assert list(s) == [3, 4, 5]
131
132
133 class RoutingTableConstructionTestCase(TestCase):
134 def test_should_be_initially_stale(self):
135 table = RoutingTable(database=DEFAULT_DATABASE)
136 assert not table.is_fresh(readonly=True)
137 assert not table.is_fresh(readonly=False)
138
139
140 class RoutingTableParseRoutingInfoTestCase(TestCase):
141 def test_should_return_routing_table_on_valid_record(self):
142 table = RoutingTable.parse_routing_info(
143 database=DEFAULT_DATABASE,
144 servers=VALID_ROUTING_RECORD["servers"],
145 ttl=VALID_ROUTING_RECORD["ttl"],
146 )
147 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
148 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
149 assert table.writers == {('127.0.0.1', 9006)}
150 assert table.ttl == 300
151
152 def test_should_return_routing_table_on_valid_record_with_extra_role(self):
153 table = RoutingTable.parse_routing_info(
154 database=DEFAULT_DATABASE,
155 servers=VALID_ROUTING_RECORD_WITH_EXTRA_ROLE["servers"],
156 ttl=VALID_ROUTING_RECORD_WITH_EXTRA_ROLE["ttl"],
157 )
158 assert table.routers == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003)}
159 assert table.readers == {('127.0.0.1', 9004), ('127.0.0.1', 9005)}
160 assert table.writers == {('127.0.0.1', 9006)}
161 assert table.ttl == 300
162
163
164 class RoutingTableServersTestCase(TestCase):
165 def test_should_return_all_distinct_servers_in_routing_table(self):
166 routing_table = {
167 "ttl": 300,
168 "servers": [
169 {"role": "ROUTE", "addresses": ["127.0.0.1:9001", "127.0.0.1:9002", "127.0.0.1:9003"]},
170 {"role": "READ", "addresses": ["127.0.0.1:9001", "127.0.0.1:9005"]},
171 {"role": "WRITE", "addresses": ["127.0.0.1:9002"]},
172 ],
173 }
174 table = RoutingTable.parse_routing_info(
175 database=DEFAULT_DATABASE,
176 servers=routing_table["servers"],
177 ttl=routing_table["ttl"],
178 )
179 assert table.servers() == {('127.0.0.1', 9001), ('127.0.0.1', 9002), ('127.0.0.1', 9003), ('127.0.0.1', 9005)}
180
181
182 class RoutingTableFreshnessTestCase(TestCase):
183 def test_should_be_fresh_after_update(self):
184 table = RoutingTable.parse_routing_info(
185 database=DEFAULT_DATABASE,
186 servers=VALID_ROUTING_RECORD["servers"],
187 ttl=VALID_ROUTING_RECORD["ttl"],
188 )
189 assert table.is_fresh(readonly=True)
190 assert table.is_fresh(readonly=False)
191
192 def test_should_become_stale_on_expiry(self):
193 table = RoutingTable.parse_routing_info(
194 database=DEFAULT_DATABASE,
195 servers=VALID_ROUTING_RECORD["servers"],
196 ttl=VALID_ROUTING_RECORD["ttl"],
197 )
198 table.ttl = 0
199 assert not table.is_fresh(readonly=True)
200 assert not table.is_fresh(readonly=False)
201
202 def test_should_become_stale_if_no_readers(self):
203 table = RoutingTable.parse_routing_info(
204 database=DEFAULT_DATABASE,
205 servers=VALID_ROUTING_RECORD["servers"],
206 ttl=VALID_ROUTING_RECORD["ttl"],
207 )
208 table.readers.clear()
209 assert not table.is_fresh(readonly=True)
210 assert table.is_fresh(readonly=False)
211
212 def test_should_become_stale_if_no_writers(self):
213 table = RoutingTable.parse_routing_info(
214 database=DEFAULT_DATABASE,
215 servers=VALID_ROUTING_RECORD["servers"],
216 ttl=VALID_ROUTING_RECORD["ttl"],
217 )
218 table.writers.clear()
219 assert table.is_fresh(readonly=True)
220 assert not table.is_fresh(readonly=False)
221
222
223 class RoutingTableUpdateTestCase(TestCase):
224 def setUp(self):
225 self.table = RoutingTable(
226 database=DEFAULT_DATABASE,
227 routers=[("192.168.1.1", 7687), ("192.168.1.2", 7687)],
228 readers=[("192.168.1.3", 7687)],
229 writers=[],
230 ttl=0,
231 )
232 self.new_table = RoutingTable(
233 database=DEFAULT_DATABASE,
234 routers=[("127.0.0.1", 9001), ("127.0.0.1", 9002), ("127.0.0.1", 9003)],
235 readers=[("127.0.0.1", 9004), ("127.0.0.1", 9005)],
236 writers=[("127.0.0.1", 9006)],
237 ttl=300,
238 )
239
240 def test_update_should_replace_routers(self):
241 self.table.update(self.new_table)
242 assert self.table.routers == {("127.0.0.1", 9001), ("127.0.0.1", 9002), ("127.0.0.1", 9003)}
243
244 def test_update_should_replace_readers(self):
245 self.table.update(self.new_table)
246 assert self.table.readers == {("127.0.0.1", 9004), ("127.0.0.1", 9005)}
247
248 def test_update_should_replace_writers(self):
249 self.table.update(self.new_table)
250 assert self.table.writers == {("127.0.0.1", 9006)}
251
252 def test_update_should_replace_ttl(self):
253 self.table.update(self.new_table)
254 assert self.table.ttl == 300
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 import unittest.mock as mock
23 from socket import (
24 AF_INET,
25 AF_INET6,
26 )
27
28 from neo4j.addressing import (
29 Address,
30 IPv4Address,
31 IPv6Address,
32 )
33 from neo4j import GraphDatabase
34
35 mock_socket_ipv4 = mock.Mock()
36 mock_socket_ipv4.getpeername = lambda: ("127.0.0.1", 7687) # (address, port)
37
38 mock_socket_ipv6 = mock.Mock()
39 mock_socket_ipv6.getpeername = lambda: ("[::1]", 7687, 0, 0) # (address, port, flow info, scope id)
40
41 # python -m pytest tests/unit/test_addressing.py -s
42
43
44 @pytest.mark.parametrize(
45 "test_input, expected",
46 [
47 (("127.0.0.1", 7687), {"family": AF_INET, "host": "127.0.0.1", "port": 7687, "str": "127.0.0.1:7687", "repr": "IPv4Address(('127.0.0.1', 7687))"}),
48 (("localhost", 7687), {"family": AF_INET, "host": "localhost", "port": 7687, "str": "localhost:7687", "repr": "IPv4Address(('localhost', 7687))"}),
49 ((None, None), {"family": AF_INET, "host": None, "port": None, "str": "None:None", "repr": "IPv4Address((None, None))"}),
50 (("::1", 7687), {"family": AF_INET, "host": "::1", "port": 7687, "str": "::1:7687", "repr": "IPv4Address(('::1', 7687))"}),
51 (("::1", 7687, 0, 0), {"family": AF_INET6, "host": "::1", "port": 7687, "str": "[::1]:7687", "repr": "IPv6Address(('::1', 7687, 0, 0))"}),
52 (("::1", 7687, 1, 2), {"family": AF_INET6, "host": "::1", "port": 7687, "str": "[::1]:7687", "repr": "IPv6Address(('::1', 7687, 1, 2))"}),
53 ((None, None, None, None), {"family": AF_INET6, "host": None, "port": None, "str": "[None]:None", "repr": "IPv6Address((None, None, None, None))"}),
54 (Address(("127.0.0.1", 7687)), {"family": AF_INET, "host": "127.0.0.1", "port": 7687, "str": "127.0.0.1:7687", "repr": "IPv4Address(('127.0.0.1', 7687))"}),
55 (Address(("::1", 7687, 1, 2)), {"family": AF_INET6, "host": "::1", "port": 7687, "str": "[::1]:7687", "repr": "IPv6Address(('::1', 7687, 1, 2))"}),
56 ]
57 )
58 def test_address_initialization(test_input, expected):
59 # python -m pytest tests/unit/test_addressing.py -s -k test_address_initialization
60 address = Address(test_input)
61 assert address.family == expected["family"]
62 assert address.host == expected["host"]
63 assert address.port == expected["port"]
64 assert str(address) == expected["str"]
65 assert repr(address) == expected["repr"]
66
67
68 @pytest.mark.parametrize(
69 "test_input",
70 [
71 Address(("127.0.0.1", 7687)),
72 Address(("127.0.0.1", 7687, 1, 2)),
73 ]
74 )
75 def test_address_init_with_address_object_returns_same_instance(test_input):
76 # python -m pytest tests/unit/test_addressing.py -s -k test_address_init_with_address_object_returns_same_instance
77 address = Address(test_input)
78 assert address is test_input
79 assert id(address) == id(test_input)
80
81
82 @pytest.mark.parametrize(
83 "test_input, expected",
84 [
85 (("127.0.0.1",), ValueError),
86 (("127.0.0.1", 7687, 0), ValueError),
87 (("[::1]", 7687, 0), ValueError),
88 (("[::1]", 7687, 0, 0, 0), ValueError),
89 ]
90 )
91 def test_address_initialization_with_incorrect_input(test_input, expected):
92 # python -m pytest tests/unit/test_addressing.py -s -k test_address_initialization_with_incorrect_input
93 with pytest.raises(expected):
94 address = Address(test_input)
95
96
97 @pytest.mark.parametrize(
98 "test_input, expected",
99 [
100 (mock_socket_ipv4, ("127.0.0.1", 7687)),
101 (mock_socket_ipv6, ("[::1]", 7687, 0, 0))
102 ]
103 )
104 def test_address_from_socket(test_input, expected):
105 # python -m pytest tests/unit/test_addressing.py -s -k test_address_from_socket
106
107 address = Address.from_socket(test_input)
108 assert address == expected
109
110
111 def test_address_from_socket_with_none():
112 # python -m pytest tests/unit/test_addressing.py -s -k test_address_from_socket_with_none
113 with pytest.raises(AttributeError):
114 address = Address.from_socket(None)
115
116
117 @pytest.mark.parametrize(
118 "test_input, expected",
119 [
120 ("127.0.0.1:7687", ("127.0.0.1", 7687)),
121 ("localhost:7687", ("localhost", 7687)),
122 (":7687", ("localhost", 7687)),
123 (":", ("localhost", 0)),
124 ("", ("localhost", 0)),
125 (":abcd", ("localhost", "abcd")),
126 (" ", (" ", 0)),
127 ]
128 )
129 def test_address_parse_with_ipv4(test_input, expected):
130 # python -m pytest tests/unit/test_addressing.py -s -k test_address_parse_with_ipv4
131 parsed = Address.parse(test_input)
132 assert parsed == expected
133
134
135 @pytest.mark.parametrize(
136 "test_input, expected",
137 [
138 ("[::1]:7687", ("::1", 7687, 0, 0)),
139 ("[::1]:abcd", ("::1", "abcd", 0, 0)),
140 ("[::1]:", ("::1", 0, 0, 0)),
141 ("[::1]", ("::1", 0, 0, 0)),
142 ]
143 )
144 def test_address_should_parse_ipv6(test_input, expected):
145 # python -m pytest tests/unit/test_addressing.py -s -k test_address_should_parse_ipv6
146 parsed = Address.parse(test_input)
147 assert parsed == expected
148
149
150 @pytest.mark.parametrize(
151 "test_input, expected",
152 [
153 (None, TypeError),
154 (123, TypeError),
155 (("127.0.0.1", 7687), TypeError),
156 (("[::1]", 7687, 1, 2), TypeError),
157 (Address(("127.0.0.1", 7687)), TypeError),
158 ]
159 )
160 def test_address_parse_with_invalid_input(test_input, expected):
161 # python -m pytest tests/unit/test_addressing.py -s -k test_address_parse_with_invalid_input
162 with pytest.raises(expected):
163 parsed = Address.parse(test_input)
164
165
166 @pytest.mark.parametrize(
167 "test_input, expected",
168 [
169 (("localhost:7687 [::1]:7687",), 2),
170 (("localhost:7687", "[::1]:7687"), 2),
171 (("localhost:7687 localhost:7688", "[::1]:7687"), 3),
172 (("localhost:7687 localhost:7687", "[::1]:7687"), 3),
173 ]
174 )
175 def test_address_parse_list(test_input, expected):
176 # python -m pytest tests/unit/test_addressing.py -s -k test_address_parse_list
177 addresses = Address.parse_list(*test_input)
178 assert len(addresses) == expected
179
180
181 @pytest.mark.parametrize(
182 "test_input, expected",
183 [
184 (("localhost:7687", None), TypeError),
185 (("localhost:7687", 123), TypeError),
186 (("localhost:7687", ("127.0.0.1", 7687)), TypeError),
187 (("localhost:7687", ("[::1]", 7687, 1, 2)), TypeError),
188 (("localhost:7687", Address(("127.0.0.1", 7687))), TypeError),
189 ]
190 )
191 def test_address_parse_list_with_invalid_input(test_input, expected):
192 # python -m pytest tests/unit/test_addressing.py -s -k test_address_parse_list_with_invalid_input
193 with pytest.raises(TypeError):
194 addresses = Address.parse_list(*test_input)
195
196
197 def test_address_resolve():
198 # python -m pytest tests/unit/test_addressing.py -s -k test_address_resolve
199 address = Address(("127.0.0.1", 7687))
200 resolved = address.resolve()
201 assert isinstance(resolved, Address) is False
202 assert isinstance(resolved, list) is True
203 assert len(resolved) == 1
204 assert resolved[0] == IPv4Address(('127.0.0.1', 7687))
205
206
207 def test_address_resolve_with_custom_resolver_none():
208 # python -m pytest tests/unit/test_addressing.py -s -k test_address_resolve_with_custom_resolver_none
209 address = Address(("127.0.0.1", 7687))
210 resolved = address.resolve(resolver=None)
211 assert isinstance(resolved, Address) is False
212 assert isinstance(resolved, list) is True
213 assert len(resolved) == 1
214 assert resolved[0] == IPv4Address(('127.0.0.1', 7687))
215
216
217 @pytest.mark.parametrize(
218 "test_input, expected",
219 [
220 (Address(("127.0.0.1", "abcd")), ValueError),
221 (Address((None, None)), ValueError),
222 ]
223 )
224 def test_address_resolve_with_unresolvable_address(test_input, expected):
225 # python -m pytest tests/unit/test_addressing.py -s -k test_address_resolve_with_unresolvable_address
226 with pytest.raises(expected):
227 test_input.resolve(resolver=None)
228
229
230 def test_address_resolve_with_custom_resolver():
231 # python -m pytest tests/unit/test_addressing.py -s -k test_address_resolve_with_custom_resolver
232 custom_resolver = lambda a: [("127.0.0.1", 7687), ("localhost", 1234)]
233
234 address = Address(("127.0.0.1", 7687))
235 resolved = address.resolve(resolver=custom_resolver)
236 assert isinstance(resolved, Address) is False
237 assert isinstance(resolved, list) is True
238 if len(resolved) == 2:
239 # IPv4 only
240 assert resolved[0] == IPv4Address(('127.0.0.1', 7687))
241 assert resolved[1] == IPv4Address(('127.0.0.1', 1234))
242 elif len(resolved) == 3:
243 # IPv4 and IPv6
244 assert resolved[0] == IPv4Address(('127.0.0.1', 7687))
245 assert resolved[1] == IPv6Address(('::1', 1234, 0, 0))
246 assert resolved[2] == IPv4Address(('127.0.0.1', 1234))
247 else:
248 assert False
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22 from uuid import uuid4
23
24 import neo4j.api
25 from neo4j.work.simple import DataDehydrator
26 from neo4j.exceptions import (
27 ConfigurationError,
28 )
29
30 standard_ascii = [chr(i) for i in range(128)]
31 not_ascii = "♥O◘♦♥O◘♦"
32
33 # python -m pytest tests/unit/test_api.py -s
34
35
36 def dehydrated_value(value):
37 return DataDehydrator.fix_parameters({"_": value})["_"]
38
39
40 def test_value_dehydration_should_allow_none():
41 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_none
42 assert dehydrated_value(None) is None
43
44
45 @pytest.mark.parametrize(
46 "test_input, expected",
47 [
48 (True, True),
49 (False, False),
50 ]
51 )
52 def test_value_dehydration_should_allow_boolean(test_input, expected):
53 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_boolean
54 assert dehydrated_value(test_input) is expected
55
56
57 @pytest.mark.parametrize(
58 "test_input, expected",
59 [
60 (0, 0),
61 (1, 1),
62 (0x7F, 0x7F),
63 (0x7FFF, 0x7FFF),
64 (0x7FFFFFFF, 0x7FFFFFFF),
65 (0x7FFFFFFFFFFFFFFF, 0x7FFFFFFFFFFFFFFF),
66 ]
67 )
68 def test_value_dehydration_should_allow_integer(test_input, expected):
69 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_integer
70 assert dehydrated_value(test_input) == expected
71
72
73 @pytest.mark.parametrize(
74 "test_input, expected",
75 [
76 (0x10000000000000000, ValueError),
77 (-0x10000000000000000, ValueError),
78 ]
79 )
80 def test_value_dehydration_should_disallow_oversized_integer(test_input, expected):
81 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_disallow_oversized_integer
82 with pytest.raises(expected):
83 dehydrated_value(test_input)
84
85
86 @pytest.mark.parametrize(
87 "test_input, expected",
88 [
89 (0.0, 0.0),
90 (-0.1, -0.1),
91 (3.1415926, 3.1415926),
92 (-3.1415926, -3.1415926),
93 ]
94 )
95 def test_value_dehydration_should_allow_float(test_input, expected):
96 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_float
97 assert dehydrated_value(test_input) == expected
98
99
100 @pytest.mark.parametrize(
101 "test_input, expected",
102 [
103 (u"", u""),
104 (u"hello, world", u"hello, world"),
105 ("".join(standard_ascii), "".join(standard_ascii)),
106 ]
107 )
108 def test_value_dehydration_should_allow_string(test_input, expected):
109 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_string
110 assert dehydrated_value(test_input) == expected
111
112
113 @pytest.mark.parametrize(
114 "test_input, expected",
115 [
116 (bytearray(), bytearray()),
117 (bytearray([1, 2, 3]), bytearray([1, 2, 3])),
118 ]
119 )
120 def test_value_dehydration_should_allow_bytes(test_input, expected):
121 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_bytes
122 assert dehydrated_value(test_input) == expected
123
124
125 @pytest.mark.parametrize(
126 "test_input, expected",
127 [
128 ([], []),
129 ([1, 2, 3], [1, 2, 3]),
130 ([1, 3.1415926, "string", None], [1, 3.1415926, "string", None])
131 ]
132 )
133 def test_value_dehydration_should_allow_list(test_input, expected):
134 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_list
135 assert dehydrated_value(test_input) == expected
136
137
138 @pytest.mark.parametrize(
139 "test_input, expected",
140 [
141 ({}, {}),
142 ({u"one": 1, u"two": 1, u"three": 1}, {u"one": 1, u"two": 1, u"three": 1}),
143 ({u"list": [1, 2, 3, [4, 5, 6]], u"dict": {u"a": 1, u"b": 2}}, {u"list": [1, 2, 3, [4, 5, 6]], u"dict": {u"a": 1, u"b": 2}}),
144 ({"alpha": [1, 3.1415926, "string", None]}, {"alpha": [1, 3.1415926, "string", None]}),
145 ]
146 )
147 def test_value_dehydration_should_allow_dict(test_input, expected):
148 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_allow_dict
149 assert dehydrated_value(test_input) == expected
150
151
152 @pytest.mark.parametrize(
153 "test_input, expected",
154 [
155 (object(), TypeError),
156 (uuid4(), TypeError),
157 ]
158 )
159 def test_value_dehydration_should_disallow_object(test_input, expected):
160 # python -m pytest tests/unit/test_api.py -s -k test_value_dehydration_should_disallow_object
161 with pytest.raises(expected):
162 dehydrated_value(test_input)
163
164
165 def test_bookmark_initialization_with_no_values():
166 # python -m pytest tests/unit/test_api.py -s -k test_bookmark_initialization_with_no_values
167 bookmark = neo4j.api.Bookmark()
168 assert bookmark.values == frozenset()
169 assert bool(bookmark) is False
170 assert repr(bookmark) == "<Bookmark values={}>"
171
172
173 @pytest.mark.parametrize(
174 "test_input, expected_values, expected_bool, expected_repr",
175 [
176 ((None,), frozenset(), False, "<Bookmark values={}>"),
177 ((None, None), frozenset(), False, "<Bookmark values={}>"),
178 (("bookmark1", None), frozenset({"bookmark1"}), True, "<Bookmark values={'bookmark1'}>"),
179 (("bookmark1", None, "bookmark2", None), frozenset({"bookmark1", "bookmark2"}), True, "<Bookmark values={'bookmark1', 'bookmark2'}>"),
180 ((None, "bookmark1", None, "bookmark2", None, None, "bookmark3"), frozenset({"bookmark1", "bookmark2", "bookmark3"}), True, "<Bookmark values={'bookmark1', 'bookmark2', 'bookmark3'}>"),
181 ]
182 )
183 def test_bookmark_initialization_with_values_none(test_input, expected_values, expected_bool, expected_repr):
184 # python -m pytest tests/unit/test_api.py -s -k test_bookmark_initialization_with_values_none
185 bookmark = neo4j.api.Bookmark(*test_input)
186 assert bookmark.values == expected_values
187 assert bool(bookmark) is expected_bool
188 assert repr(bookmark) == expected_repr
189
190
191 @pytest.mark.parametrize(
192 "test_input, expected_values, expected_bool, expected_repr",
193 [
194 (("",), frozenset(), False, "<Bookmark values={}>"),
195 (("", ""), frozenset(), False, "<Bookmark values={}>"),
196 (("bookmark1", ""), frozenset({"bookmark1"}), True, "<Bookmark values={'bookmark1'}>"),
197 (("bookmark1", "", "bookmark2", ""), frozenset({"bookmark1", "bookmark2"}), True, "<Bookmark values={'bookmark1', 'bookmark2'}>"),
198 (("", "bookmark1", "", "bookmark2", "", "", "bookmark3"), frozenset({"bookmark1", "bookmark2", "bookmark3"}), True, "<Bookmark values={'bookmark1', 'bookmark2', 'bookmark3'}>"),
199 ]
200 )
201 def test_bookmark_initialization_with_values_empty_string(test_input, expected_values, expected_bool, expected_repr):
202 # python -m pytest tests/unit/test_api.py -s -k test_bookmark_initialization_with_values_empty_string
203 bookmark = neo4j.api.Bookmark(*test_input)
204 assert bookmark.values == expected_values
205 assert bool(bookmark) is expected_bool
206 assert repr(bookmark) == expected_repr
207
208
209 @pytest.mark.parametrize(
210 "test_input, expected_values, expected_bool, expected_repr",
211 [
212 (("bookmark1",), frozenset({"bookmark1"}), True, "<Bookmark values={'bookmark1'}>"),
213 (("bookmark1", "bookmark2", "bookmark3"), frozenset({"bookmark1", "bookmark2", "bookmark3"}), True, "<Bookmark values={'bookmark1', 'bookmark2', 'bookmark3'}>"),
214 (standard_ascii, frozenset(standard_ascii), True, "<Bookmark values={{'{values}'}}>".format(values="', '".join(standard_ascii)))
215 ]
216 )
217 def test_bookmark_initialization_with_valid_strings(test_input, expected_values, expected_bool, expected_repr):
218 # python -m pytest tests/unit/test_api.py -s -k test_bookmark_initialization_with_valid_strings
219 bookmark = neo4j.api.Bookmark(*test_input)
220 assert bookmark.values == expected_values
221 assert bool(bookmark) is expected_bool
222 assert repr(bookmark) == expected_repr
223
224
225 @pytest.mark.parametrize(
226 "test_input, expected",
227 [
228 ((not_ascii,), ValueError),
229 (("", not_ascii,), ValueError),
230 (("bookmark1", chr(129),), ValueError),
231 ]
232 )
233 def test_bookmark_initialization_with_invalid_strings(test_input, expected):
234 # python -m pytest tests/unit/test_api.py -s -k test_bookmark_initialization_with_invalid_strings
235 with pytest.raises(expected) as e:
236 bookmark = neo4j.api.Bookmark(*test_input)
237
238
239 @pytest.mark.parametrize(
240 "test_input, expected_str, expected_repr",
241 [
242 ((), "", "Version()"),
243 ((None,), "None", "Version(None,)"),
244 (("3",), "3", "Version('3',)"),
245 (("3", "0"), "3.0", "Version('3', '0')"),
246 ((3,), "3", "Version(3,)"),
247 ((3, 0), "3.0", "Version(3, 0)"),
248 ((3, 0, 0), "3.0.0", "Version(3, 0, 0)"),
249 ((3, 0, 0, 0), "3.0.0.0", "Version(3, 0, 0, 0)"),
250 ]
251 )
252 def test_version_initialization(test_input, expected_str, expected_repr):
253 # python -m pytest tests/unit/test_api.py -s -k test_version_initialization
254 version = neo4j.api.Version(*test_input)
255 assert str(version) == expected_str
256 assert repr(version) == expected_repr
257
258
259 @pytest.mark.parametrize(
260 "test_input, expected_str, expected_repr",
261 [
262 (bytearray([0, 0, 0, 0]), "0.0", "Version(0, 0)"),
263 (bytearray([0, 0, 0, 1]), "1.0", "Version(1, 0)"),
264 (bytearray([0, 0, 1, 0]), "0.1", "Version(0, 1)"),
265 (bytearray([0, 0, 1, 1]), "1.1", "Version(1, 1)"),
266 (bytearray([0, 0, 254, 254]), "254.254", "Version(254, 254)"),
267 ]
268 )
269 def test_version_from_bytes_with_valid_bolt_version_handshake(test_input, expected_str, expected_repr):
270 # python -m pytest tests/unit/test_api.py -s -k test_version_from_bytes_with_valid_bolt_version_handshake
271 version = neo4j.api.Version.from_bytes(test_input)
272 assert str(version) == expected_str
273 assert repr(version) == expected_repr
274
275
276 @pytest.mark.parametrize(
277 "test_input, expected",
278 [
279 (bytearray([0, 0, 0]), ValueError),
280 (bytearray([0, 0, 0, 0, 0]), ValueError),
281 (bytearray([1, 0, 0, 0]), ValueError),
282 (bytearray([0, 1, 0, 0]), ValueError),
283 (bytearray([1, 1, 0, 0]), ValueError),
284 ]
285 )
286 def test_version_from_bytes_with_not_valid_bolt_version_handshake(test_input, expected):
287 # python -m pytest tests/unit/test_api.py -s -k test_version_from_bytes_with_not_valid_bolt_version_handshake
288 with pytest.raises(expected):
289 version = neo4j.api.Version.from_bytes(test_input)
290
291
292 @pytest.mark.parametrize(
293 "test_input, expected",
294 [
295 ((), bytearray([0, 0, 0, 0])),
296 ((0,), bytearray([0, 0, 0, 0])),
297 ((1,), bytearray([0, 0, 0, 1])),
298 ((0, 0), bytearray([0, 0, 0, 0])),
299 ((1, 0), bytearray([0, 0, 0, 1])),
300 ((1, 2), bytearray([0, 0, 2, 1])),
301 ((255, 255), bytearray([0, 0, 255, 255])),
302 ]
303 )
304 def test_version_to_bytes_with_valid_bolt_version(test_input, expected):
305 # python -m pytest tests/unit/test_api.py -s -k test_version_to_bytes_with_valid_bolt_version
306 version = neo4j.api.Version(*test_input)
307 assert version.to_bytes() == expected
308
309
310 @pytest.mark.parametrize(
311 "test_input, expected",
312 [
313 ((0, 0, 0), ValueError),
314 ((-1, -1), ValueError),
315 ((256, 256), ValueError),
316 ((None, None), TypeError),
317 (("0", "0"), TypeError),
318 ]
319 )
320 def test_version_to_bytes_with_not_valid_bolt_version(test_input, expected):
321 # python -m pytest tests/unit/test_api.py -s -k test_version_to_bytes_with_valid_bolt_version
322 version = neo4j.api.Version(*test_input)
323 with pytest.raises(expected):
324 byte_version = version.to_bytes()
325
326
327 def test_serverinfo_initialization():
328 # python -m pytest tests/unit/test_api.py -s -k test_serverinfo_initialization
329
330 from neo4j.addressing import Address
331
332 address = Address(("bolt://localhost", 7687))
333 version = neo4j.api.Version(3, 0)
334
335 server_info = neo4j.api.ServerInfo(address, version)
336 assert server_info.address is address
337 assert server_info.protocol_version is version
338 assert server_info.metadata == {}
339
340 assert server_info.agent is None
341 assert server_info.version_info() is None
342
343
344 @pytest.mark.parametrize(
345 "test_input, expected_agent, expected_version_info",
346 [
347 ({"server": "Neo4j/3.0.0"}, "Neo4j/3.0.0", (3, 0, 0)),
348 ({"server": "Neo4j/3.X.Y"}, "Neo4j/3.X.Y", (3, "X", "Y")),
349 ]
350 )
351 def test_serverinfo_with_metadata(test_input, expected_agent, expected_version_info):
352 # python -m pytest tests/unit/test_api.py -s -k test_serverinfo_with_metadata
353 from neo4j.addressing import Address
354
355 address = Address(("bolt://localhost", 7687))
356 version = neo4j.api.Version(3, 0)
357
358 server_info = neo4j.api.ServerInfo(address, version)
359
360 server_info.metadata = test_input
361
362 assert server_info.agent == expected_agent
363 assert server_info.version_info() == expected_version_info
364
365
366 @pytest.mark.parametrize(
367 "test_input, expected_driver_type, expected_security_type, expected_error",
368 [
369 ("bolt://localhost:7676", neo4j.api.DRIVER_BOLT, neo4j.api.SECURITY_TYPE_NOT_SECURE, None),
370 ("bolt+ssc://localhost:7676", neo4j.api.DRIVER_BOLT, neo4j.api.SECURITY_TYPE_SELF_SIGNED_CERTIFICATE, None),
371 ("bolt+s://localhost:7676", neo4j.api.DRIVER_BOLT, neo4j.api.SECURITY_TYPE_SECURE, None),
372 ("neo4j://localhost:7676", neo4j.api.DRIVER_NEO4j, neo4j.api.SECURITY_TYPE_NOT_SECURE, None),
373 ("neo4j+ssc://localhost:7676", neo4j.api.DRIVER_NEO4j, neo4j.api.SECURITY_TYPE_SELF_SIGNED_CERTIFICATE, None),
374 ("neo4j+s://localhost:7676", neo4j.api.DRIVER_NEO4j, neo4j.api.SECURITY_TYPE_SECURE, None),
375 ("undefined://localhost:7676", None, None, ConfigurationError),
376 ("localhost:7676", None, None, ConfigurationError),
377 ("://localhost:7676", None, None, ConfigurationError),
378 ("bolt+routing://localhost:7676", neo4j.api.DRIVER_NEO4j, neo4j.api.SECURITY_TYPE_NOT_SECURE, ConfigurationError),
379 ("bolt://username@localhost:7676", None, None, ConfigurationError),
380 ("bolt://username:password@localhost:7676", None, None, ConfigurationError),
381 ]
382 )
383 def test_uri_scheme(test_input, expected_driver_type, expected_security_type, expected_error):
384 # python -m pytest tests/unit/test_api.py -s -k test_uri_scheme
385 if expected_error:
386 with pytest.raises(expected_error):
387 neo4j.api.parse_neo4j_uri(test_input)
388 else:
389 driver_type, security_type, parsed = neo4j.api.parse_neo4j_uri(test_input)
390 assert driver_type == expected_driver_type
391 assert security_type == expected_security_type
392
393
394 def test_parse_routing_context():
395 # python -m pytest tests/unit/test_api.py -s -v -k test_parse_routing_context
396 context = neo4j.api.parse_routing_context(query="name=molly&color=white")
397 assert context == {"name": "molly", "color": "white"}
398
399
400 def test_parse_routing_context_should_error_when_value_missing():
401 # python -m pytest tests/unit/test_api.py -s -v -k test_parse_routing_context_should_error_when_value_missing
402 with pytest.raises(ConfigurationError):
403 neo4j.api.parse_routing_context("name=&color=white")
404
405
406 def test_parse_routing_context_should_error_when_key_duplicate():
407 # python -m pytest tests/unit/test_api.py -s -v -k test_parse_routing_context_should_error_when_key_duplicate
408 with pytest.raises(ConfigurationError):
409 neo4j.api.parse_routing_context("name=molly&name=white")
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import (
24 ConfigurationError,
25 )
26 from neo4j.conf import (
27 Config,
28 PoolConfig,
29 WorkspaceConfig,
30 SessionConfig,
31 )
32 from neo4j.api import (
33 TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
34 WRITE_ACCESS,
35 READ_ACCESS,
36 )
37
38 # python -m pytest tests/unit/test_conf.py -s -v
39
40 from neo4j.debug import watch
41 watch("neo4j")
42
43 test_pool_config = {
44 "connection_timeout": 30.0,
45 "init_size": 1,
46 "keep_alive": True,
47 "max_connection_lifetime": 3600,
48 "max_connection_pool_size": 100,
49 "protocol_version": None,
50 "resolver": None,
51 "encrypted": False,
52 "user_agent": "test",
53 "trust": TRUST_SYSTEM_CA_SIGNED_CERTIFICATES,
54 }
55
56 test_session_config = {
57 "connection_acquisition_timeout": 60.0,
58 "max_transaction_retry_time": 30.0,
59 "initial_retry_delay": 1.0,
60 "retry_delay_multiplier": 2.0,
61 "retry_delay_jitter_factor": 0.2,
62 "bookmarks": (),
63 "default_access_mode": WRITE_ACCESS,
64 "database": None,
65 "fetch_size": 100,
66 }
67
68 config_function_names = ["consume_chain", "consume"]
69
70
71 def test_pool_config_consume():
72
73 test_config = dict(test_pool_config)
74
75 consumed_pool_config = PoolConfig.consume(test_config)
76
77 assert isinstance(consumed_pool_config, PoolConfig)
78
79 assert len(test_config) == 0
80
81 for key in test_pool_config.keys():
82 assert consumed_pool_config[key] == test_pool_config[key]
83
84 for key in consumed_pool_config.keys():
85 if key not in config_function_names:
86 assert test_pool_config[key] == consumed_pool_config[key]
87
88 assert len(consumed_pool_config) - len(config_function_names) == len(test_pool_config)
89
90
91 def test_pool_config_consume_default_values():
92
93 test_config = {}
94
95 consumed_pool_config = PoolConfig.consume(test_config)
96
97 assert isinstance(consumed_pool_config, PoolConfig)
98
99 assert len(test_config) == 0
100
101 consumed_pool_config.keep_alive = "changed"
102
103 assert PoolConfig.keep_alive != consumed_pool_config.keep_alive
104
105
106 def test_pool_config_consume_key_not_valid():
107
108 test_config = dict(test_pool_config)
109
110 test_config["not_valid_key"] = "test"
111
112 with pytest.raises(ConfigurationError) as error:
113 consumed_pool_config = PoolConfig.consume(test_config)
114
115 error.match("Unexpected config keys: not_valid_key")
116
117
118 def test_pool_config_set_value():
119
120 test_config = dict(test_pool_config)
121
122 consumed_pool_config = PoolConfig.consume(test_config)
123
124 assert consumed_pool_config.get("encrypted") is False
125 assert consumed_pool_config["encrypted"] is False
126 assert consumed_pool_config.encrypted is False
127
128 consumed_pool_config.encrypted = "test"
129
130 assert consumed_pool_config.get("encrypted") == "test"
131 assert consumed_pool_config["encrypted"] == "test"
132 assert consumed_pool_config.encrypted == "test"
133
134 consumed_pool_config.not_valid_key = "test" # Use consume functions
135
136
137 def test_pool_config_consume_and_then_consume_again():
138
139 test_config = dict(test_pool_config)
140 consumed_pool_config = PoolConfig.consume(test_config)
141 assert consumed_pool_config.encrypted is False
142 consumed_pool_config.encrypted = "test"
143
144 with pytest.raises(AttributeError):
145 consumed_pool_config = PoolConfig.consume(consumed_pool_config)
146
147 consumed_pool_config = PoolConfig.consume(dict(consumed_pool_config.items()))
148 consumed_pool_config = PoolConfig.consume(dict(consumed_pool_config.items()))
149
150 assert consumed_pool_config.encrypted == "test"
151
152
153 def test_config_consume_chain():
154
155 test_config = {}
156
157 test_config.update(test_pool_config)
158
159 test_config.update(test_session_config)
160
161 consumed_pool_config, consumed_session_config = Config.consume_chain(test_config, PoolConfig, SessionConfig)
162
163 assert isinstance(consumed_pool_config, PoolConfig)
164 assert isinstance(consumed_session_config, SessionConfig)
165
166 assert len(test_config) == 0
167
168 for key, val in test_pool_config.items():
169 assert consumed_pool_config[key] == val
170
171 for key, val in consumed_pool_config.items():
172 if key not in config_function_names:
173 assert test_pool_config[key] == val
174
175 assert len(consumed_pool_config) - len(config_function_names) == len(test_pool_config)
176
177 assert len(consumed_session_config) - len(config_function_names) == len(test_session_config)
178
179
180 def test_init_session_config_merge():
181 # python -m pytest tests/unit/test_conf.py -s -v -k test_init_session_config
182
183 test_config_a = {"connection_acquisition_timeout": 111}
184 test_config_c = {"max_transaction_retry_time": 222}
185
186 workspace_config = WorkspaceConfig(test_config_a, WorkspaceConfig.consume(test_config_c))
187 assert len(test_config_a) == 1
188 assert len(test_config_c) == 0
189 assert isinstance(workspace_config, WorkspaceConfig)
190 assert workspace_config.connection_acquisition_timeout == WorkspaceConfig.connection_acquisition_timeout
191 assert workspace_config.max_transaction_retry_time == 222
192
193 workspace_config = WorkspaceConfig(test_config_c, test_config_a)
194 assert isinstance(workspace_config, WorkspaceConfig)
195 assert workspace_config.connection_acquisition_timeout == 111
196 assert workspace_config.max_transaction_retry_time == WorkspaceConfig.max_transaction_retry_time
197
198 test_config_b = {"default_access_mode": READ_ACCESS, "connection_acquisition_timeout": 333}
199
200 session_config = SessionConfig(workspace_config, test_config_b)
201 assert session_config.connection_acquisition_timeout == 333
202 assert session_config.default_access_mode == READ_ACCESS
203
204 session_config = SessionConfig(test_config_b, workspace_config)
205 assert session_config.connection_acquisition_timeout == 111
206 assert session_config.default_access_mode == READ_ACCESS
207
208
209 def test_init_session_config_with_not_valid_key():
210 # python -m pytest tests/unit/test_conf.py -s -v -k test_init_session_config_with_not_valid_key
211
212 test_config_a = {"connection_acquisition_timeout": 111}
213 workspace_config = WorkspaceConfig.consume(test_config_a)
214
215 test_config_b = {"default_access_mode": READ_ACCESS, "connection_acquisition_timeout": 333, "not_valid_key": None}
216 session_config = SessionConfig(workspace_config, test_config_b)
217
218 with pytest.raises(AttributeError):
219 assert session_config.not_valid_key is None
220
221 with pytest.raises(ConfigurationError):
222 _ = SessionConfig.consume(test_config_b)
223
224 assert session_config.connection_acquisition_timeout == 333
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.data import DataHydrator
24 from neo4j.packstream import Structure
25
26 # python -m pytest -s -v tests/unit/test_data.py
27
28
29 def test_can_hydrate_node_structure():
30 hydrant = DataHydrator()
31
32 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
33 alice, = hydrant.hydrate([struct])
34
35 assert alice.id == 123
36 assert alice.labels == {"Person"}
37 assert set(alice.keys()) == {"name"}
38 assert alice.get("name") == "Alice"
39
40
41 def test_hydrating_unknown_structure_returns_same():
42 hydrant = DataHydrator()
43
44 struct = Structure(b'?', "foo")
45 mystery, = hydrant.hydrate([struct])
46
47 assert mystery == struct
48
49
50 def test_can_hydrate_in_list():
51 hydrant = DataHydrator()
52
53 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
54 alice_in_list, = hydrant.hydrate([[struct]])
55
56 assert isinstance(alice_in_list, list)
57
58 alice, = alice_in_list
59
60 assert alice.id == 123
61 assert alice.labels == {"Person"}
62 assert set(alice.keys()) == {"name"}
63 assert alice.get("name") == "Alice"
64
65
66 def test_can_hydrate_in_dict():
67 hydrant = DataHydrator()
68
69 struct = Structure(b'N', 123, ["Person"], {"name": "Alice"})
70 alice_in_dict, = hydrant.hydrate([{"foo": struct}])
71
72 assert isinstance(alice_in_dict, dict)
73
74 alice = alice_in_dict["foo"]
75
76 assert alice.id == 123
77 assert alice.labels == {"Person"}
78 assert set(alice.keys()) == {"name"}
79 assert alice.get("name") == "Alice"
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.exceptions import (
24 Neo4jError,
25 ClientError,
26 CypherSyntaxError,
27 CypherTypeError,
28 ConstraintError,
29 AuthError,
30 Forbidden,
31 ForbiddenOnReadOnlyDatabase,
32 NotALeader,
33 DatabaseError,
34 TransientError,
35 DatabaseUnavailable,
36 DriverError,
37 TransactionError,
38 TransactionNestingError,
39 SessionExpired,
40 ServiceUnavailable,
41 RoutingServiceUnavailable,
42 WriteServiceUnavailable,
43 ReadServiceUnavailable,
44 ConfigurationError,
45 AuthConfigurationError,
46 CertificateConfigurationError,
47 ResultConsumedError,
48 CLASSIFICATION_CLIENT,
49 CLASSIFICATION_DATABASE,
50 CLASSIFICATION_TRANSIENT,
51 )
52
53 from neo4j._exceptions import (
54 BoltError,
55 BoltHandshakeError,
56 BoltRoutingError,
57 BoltConnectionError,
58 BoltSecurityError,
59 BoltConnectionBroken,
60 BoltConnectionClosed,
61 BoltFailure,
62 BoltIncompleteCommitError,
63 BoltProtocolError,
64 )
65
66 from neo4j.io import Bolt
67
68
69 # python -m pytest tests/unit/test_exceptions.py -s -v
70
71 def test_bolt_error():
72 with pytest.raises(BoltError) as e:
73 error = BoltError("Error Message", address="localhost")
74 # assert repr(error) == "BoltError('Error Message')" This differs between python version 3.6 "BoltError('Error Message',)" and 3.7
75 assert str(error) == "Error Message"
76 assert error.args == ("Error Message",)
77 assert error.address == "localhost"
78 raise error
79
80 # The regexp parameter of the match method is matched with the re.search function.
81 with pytest.raises(AssertionError):
82 e.match("FAIL!")
83
84 assert e.match("Error Message")
85
86
87 def test_bolt_protocol_error():
88 with pytest.raises(BoltProtocolError) as e:
89 error = BoltProtocolError("Driver does not support Bolt protocol version: 0x%06X%02X" % (2, 5), address="localhost")
90 assert error.address == "localhost"
91 raise error
92
93 # The regexp parameter of the match method is matched with the re.search function.
94 with pytest.raises(AssertionError):
95 e.match("FAIL!")
96
97 e.match("Driver does not support Bolt protocol version: 0x00000205")
98
99
100 def test_bolt_handshake_error():
101 handshake = b"\x00\x00\x00\x04\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00"
102 response = b"\x00\x00\x00\x00"
103 supported_versions = Bolt.protocol_handlers().keys()
104
105 with pytest.raises(BoltHandshakeError) as e:
106 error = BoltHandshakeError("The Neo4J server does not support communication with this driver. Supported Bolt Protocols {}".format(supported_versions), address="localhost", request_data=handshake, response_data=response)
107 assert error.address == "localhost"
108 assert error.request_data == handshake
109 assert error.response_data == response
110 raise error
111
112 e.match("The Neo4J server does not support communication with this driver. Supported Bolt Protocols ")
113
114
115 def test_serviceunavailable():
116 with pytest.raises(ServiceUnavailable) as e:
117 error = ServiceUnavailable("Test error message")
118 raise error
119
120 assert e.value.__cause__ is None
121
122
123 def test_serviceunavailable_raised_from_bolt_protocol_error_with_implicit_style():
124 error = BoltProtocolError("Driver does not support Bolt protocol version: 0x%06X%02X" % (2, 5), address="localhost")
125 with pytest.raises(ServiceUnavailable) as e:
126 assert error.address == "localhost"
127 try:
128 raise error
129 except BoltProtocolError as error_bolt_protocol:
130 raise ServiceUnavailable(str(error_bolt_protocol)) from error_bolt_protocol
131
132 # The regexp parameter of the match method is matched with the re.search function.
133 with pytest.raises(AssertionError):
134 e.match("FAIL!")
135
136 e.match("Driver does not support Bolt protocol version: 0x00000205")
137 assert e.value.__cause__ is error
138
139
140 def test_serviceunavailable_raised_from_bolt_protocol_error_with_explicit_style():
141 error = BoltProtocolError("Driver does not support Bolt protocol version: 0x%06X%02X" % (2, 5), address="localhost")
142
143 with pytest.raises(ServiceUnavailable) as e:
144 assert error.address == "localhost"
145 try:
146 raise error
147 except BoltProtocolError as error_bolt_protocol:
148 error_nested = ServiceUnavailable(str(error_bolt_protocol))
149 error_nested.__cause__ = error_bolt_protocol
150 raise error_nested
151
152 # The regexp parameter of the match method is matched with the re.search function.
153 with pytest.raises(AssertionError):
154 e.match("FAIL!")
155
156 e.match("Driver does not support Bolt protocol version: 0x00000205")
157 assert e.value.__cause__ is error
158
159
160 def test_neo4jerror_hydrate_with_no_args():
161 error = Neo4jError.hydrate()
162
163 assert isinstance(error, DatabaseError)
164 assert error.classification == CLASSIFICATION_DATABASE
165 assert error.category == "General"
166 assert error.title == "UnknownError"
167 assert error.metadata == {}
168 assert error.message == "An unknown error occurred"
169 assert error.code == "Neo.DatabaseError.General.UnknownError"
170
171
172 def test_neo4jerror_hydrate_with_message_and_code_rubish():
173 error = Neo4jError.hydrate(message="Test error message", code="ASDF_asdf")
174
175 assert isinstance(error, DatabaseError)
176 assert error.classification == CLASSIFICATION_DATABASE
177 assert error.category == "General"
178 assert error.title == "UnknownError"
179 assert error.metadata == {}
180 assert error.message == "Test error message"
181 assert error.code == "ASDF_asdf"
182
183
184 def test_neo4jerror_hydrate_with_message_and_code_database():
185 error = Neo4jError.hydrate(message="Test error message", code="Neo.DatabaseError.General.UnknownError")
186
187 assert isinstance(error, DatabaseError)
188 assert error.classification == CLASSIFICATION_DATABASE
189 assert error.category == "General"
190 assert error.title == "UnknownError"
191 assert error.metadata == {}
192 assert error.message == "Test error message"
193 assert error.code == "Neo.DatabaseError.General.UnknownError"
194
195
196 def test_neo4jerror_hydrate_with_message_and_code_transient():
197 error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.General.TestError")
198 # error = Neo4jError.hydrate(message="Test error message", code="Neo.{}.General.TestError".format(CLASSIFICATION_TRANSIENT))
199
200 assert isinstance(error, TransientError)
201 assert error.classification == CLASSIFICATION_TRANSIENT
202 assert error.category == "General"
203 assert error.title == "TestError"
204 assert error.metadata == {}
205 assert error.message == "Test error message"
206 assert error.code == "Neo.{}.General.TestError".format(CLASSIFICATION_TRANSIENT)
207
208
209 def test_neo4jerror_hydrate_with_message_and_code_client():
210 error = Neo4jError.hydrate(message="Test error message", code="Neo.{}.General.TestError".format(CLASSIFICATION_CLIENT))
211
212 assert isinstance(error, ClientError)
213 assert error.classification == CLASSIFICATION_CLIENT
214 assert error.category == "General"
215 assert error.title == "TestError"
216 assert error.metadata == {}
217 assert error.message == "Test error message"
218 assert error.code == "Neo.{}.General.TestError".format(CLASSIFICATION_CLIENT)
219
220
221 def test_transient_error_is_retriable_case_1():
222 error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.Terminated")
223
224 assert isinstance(error, TransientError)
225 assert error.is_retriable() is False
226
227
228 def test_transient_error_is_retriable_case_2():
229 error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.Transaction.LockClientStopped")
230
231 assert isinstance(error, TransientError)
232 assert error.is_retriable() is False
233
234
235 def test_transient_error_is_retriable_case_3():
236 error = Neo4jError.hydrate(message="Test error message", code="Neo.TransientError.General.TestError")
237
238 assert isinstance(error, TransientError)
239 assert error.is_retriable() is True
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 # python -m pytest tests/unit/test_import_neo4j.py -s -v
24
25
26 def test_import_dunder_version():
27 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_dunder_version
28 from neo4j import __version__
29
30
31 def test_import_graphdatabase():
32 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_graphdatabase
33 from neo4j import GraphDatabase
34
35
36 def test_import_driver():
37 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_driver
38 from neo4j import Driver
39
40
41 def test_import_boltdriver():
42 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_boltdriver
43 from neo4j import BoltDriver
44
45
46 def test_import_neo4jdriver():
47 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_neo4jdriver
48 from neo4j import Neo4jDriver
49
50
51 def test_import_auth():
52 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_auth
53 from neo4j import Auth
54
55
56 def test_import_authtoken():
57 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_authtoken
58 from neo4j import AuthToken
59
60
61 def test_import_basic_auth():
62 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_auth
63 from neo4j import basic_auth
64
65
66 def test_import_kerberos_auth():
67 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_kerberos_auth
68 from neo4j import kerberos_auth
69
70
71 def test_import_custom_auth():
72 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_custom_auth
73 from neo4j import custom_auth
74
75
76 def test_import_read_access():
77 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_read_access
78 from neo4j import READ_ACCESS
79
80
81 def test_import_write_access():
82 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_write_access
83 from neo4j import WRITE_ACCESS
84
85
86 def test_import_transaction():
87 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_transaction
88 from neo4j import Transaction
89
90
91 def test_import_record():
92 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_record
93 from neo4j import Record
94
95
96 def test_import_session():
97 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_session
98 from neo4j import Session
99
100
101 def test_import_sessionconfig():
102 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_sessionconfig
103 from neo4j import SessionConfig
104
105
106 def test_import_query():
107 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_query
108 from neo4j import Query
109
110
111 def test_import_result():
112 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_result
113 from neo4j import Result
114
115
116 def test_import_resultsummary():
117 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_resultsummary
118 from neo4j import ResultSummary
119
120
121 def test_import_unit_of_work():
122 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_unit_of_work
123 from neo4j import unit_of_work
124
125
126 def test_import_config():
127 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_config
128 from neo4j import Config
129
130
131 def test_import_poolconfig():
132 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_poolconfig
133 from neo4j import PoolConfig
134
135
136 def test_import_graph():
137 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_graph
138 import neo4j.graph as graph
139
140
141 def test_import_graph_node():
142 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_graph_node
143 from neo4j.graph import Node
144
145
146 def test_import_graph_path():
147 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_graph_path
148 from neo4j.graph import Path
149
150
151 def test_import_graph_graph():
152 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_graph_graph
153 from neo4j.graph import Graph
154
155
156 def test_import_spatial():
157 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_spatial
158 import neo4j.spatial as spatial
159
160
161 def test_import_time():
162 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_time
163 import neo4j.time as time
164
165
166 def test_import_exceptions():
167 # python -m pytest tests/unit/test_import_neo4j.py -s -v -k test_import_exceptions
168 import neo4j.exceptions as exceptions
169
170
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 import pytest
22
23 from neo4j.data import Record
24
25 # python -m pytest -s -v tests/unit/test_record.py
26
27
28 def test_record_equality():
29 record1 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
30 record2 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
31 record3 = Record(zip(["name", "empire"], ["Stefan", "Das Deutschland"]))
32 assert record1 == record2
33 assert record1 != record3
34 assert record2 != record3
35
36
37 def test_record_hashing():
38 record1 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
39 record2 = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
40 record3 = Record(zip(["name", "empire"], ["Stefan", "Das Deutschland"]))
41 assert hash(record1) == hash(record2)
42 assert hash(record1) != hash(record3)
43 assert hash(record2) != hash(record3)
44
45
46 def test_record_iter():
47 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
48 assert list(a_record.__iter__()) == ["Nigel", "The British Empire"]
49
50
51 def test_record_as_dict():
52 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
53 assert dict(a_record) == {"name": "Nigel", "empire": "The British Empire"}
54
55
56 def test_record_as_list():
57 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
58 assert list(a_record) == ["Nigel", "The British Empire"]
59
60
61 def test_record_len():
62 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
63 assert len(a_record) == 2
64
65
66 def test_record_repr():
67 a_record = Record(zip(["name", "empire"], ["Nigel", "The British Empire"]))
68 assert repr(a_record) == "<Record name='Nigel' empire='The British Empire'>"
69
70
71 def test_record_data():
72 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
73 assert r.data() == {"name": "Alice", "age": 33, "married": True}
74 assert r.data("name") == {"name": "Alice"}
75 assert r.data("age", "name") == {"age": 33, "name": "Alice"}
76 assert r.data("age", "name", "shoe size") == {"age": 33, "name": "Alice", "shoe size": None}
77 assert r.data(0, "name") == {"name": "Alice"}
78 assert r.data(0) == {"name": "Alice"}
79 assert r.data(1, 0) == {"age": 33, "name": "Alice"}
80 with pytest.raises(IndexError):
81 _ = r.data(1, 0, 999)
82
83
84 def test_record_keys():
85 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
86 assert r.keys() == ["name", "age", "married"]
87
88
89 def test_record_values():
90 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
91 assert r.values() == ["Alice", 33, True]
92 assert r.values("name") == ["Alice"]
93 assert r.values("age", "name") == [33, "Alice"]
94 assert r.values("age", "name", "shoe size") == [33, "Alice", None]
95 assert r.values(0, "name") == ["Alice", "Alice"]
96 assert r.values(0) == ["Alice"]
97 assert r.values(1, 0) == [33, "Alice"]
98 with pytest.raises(IndexError):
99 _ = r.values(1, 0, 999)
100
101
102 def test_record_items():
103 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
104 assert r.items() == [("name", "Alice"), ("age", 33), ("married", True)]
105 assert r.items("name") == [("name", "Alice")]
106 assert r.items("age", "name") == [("age", 33), ("name", "Alice")]
107 assert r.items("age", "name", "shoe size") == [("age", 33), ("name", "Alice"), ("shoe size", None)]
108 assert r.items(0, "name") == [("name", "Alice"), ("name", "Alice")]
109 assert r.items(0) == [("name", "Alice")]
110 assert r.items(1, 0) == [("age", 33), ("name", "Alice")]
111 with pytest.raises(IndexError):
112 _ = r.items(1, 0, 999)
113
114
115 def test_record_index():
116 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
117 assert r.index("name") == 0
118 assert r.index("age") == 1
119 assert r.index("married") == 2
120 with pytest.raises(KeyError):
121 _ = r.index("shoe size")
122 assert r.index(0) == 0
123 assert r.index(1) == 1
124 assert r.index(2) == 2
125 with pytest.raises(IndexError):
126 _ = r.index(3)
127 with pytest.raises(TypeError):
128 _ = r.index(None)
129
130
131 def test_record_value():
132 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
133 assert r.value() == "Alice"
134 assert r.value("name") == "Alice"
135 assert r.value("age") == 33
136 assert r.value("married") is True
137 assert r.value("shoe size") is None
138 assert r.value("shoe size", 6) == 6
139 assert r.value(0) == "Alice"
140 assert r.value(1) == 33
141 assert r.value(2) is True
142 assert r.value(3) is None
143 assert r.value(3, 6) == 6
144 with pytest.raises(TypeError):
145 _ = r.value(None)
146
147
148 def test_record_contains():
149 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
150 assert "Alice" in r
151 assert 33 in r
152 assert True in r
153 assert 7.5 not in r
154 with pytest.raises(TypeError):
155 _ = r.index(None)
156
157
158 def test_record_from_dict():
159 r = Record({"name": "Alice", "age": 33})
160 assert r["name"] == "Alice"
161 assert r["age"] == 33
162
163
164 def test_record_get_slice():
165 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
166 assert Record(zip(["name", "age"], ["Alice", 33])) == r[0:2]
167
168
169 def test_record_get_by_index():
170 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
171 assert r[0] == "Alice"
172
173
174 def test_record_get_by_name():
175 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
176 assert r["name"] == "Alice"
177
178
179 def test_record_get_by_out_of_bounds_index():
180 r = Record(zip(["name", "age", "married"], ["Alice", 33, True]))
181 assert r[9] is None
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from neo4j.api import (
22 kerberos_auth,
23 basic_auth,
24 custom_auth,
25 )
26
27 # python -m pytest -s -v tests/unit/test_security.py
28
29
30 def test_should_generate_kerberos_auth_token_correctly():
31 auth = kerberos_auth("I am a base64 service ticket")
32 assert auth.scheme == "kerberos"
33 assert auth.principal == ""
34 assert auth.credentials == "I am a base64 service ticket"
35 assert not auth.realm
36 assert not hasattr(auth, "parameters")
37
38
39 def test_should_generate_basic_auth_without_realm_correctly():
40 auth = basic_auth("molly", "meoooow")
41 assert auth.scheme == "basic"
42 assert auth.principal == "molly"
43 assert auth.credentials == "meoooow"
44 assert not auth.realm
45 assert not hasattr(auth, "parameters")
46
47
48 def test_should_generate_base_auth_with_realm_correctly():
49 auth = basic_auth("molly", "meoooow", "cat_cafe")
50 assert auth.scheme == "basic"
51 assert auth.principal == "molly"
52 assert auth.credentials == "meoooow"
53 assert auth.realm == "cat_cafe"
54 assert not hasattr(auth, "parameters")
55
56
57 def test_should_generate_custom_auth_correctly():
58 auth = custom_auth("molly", "meoooow", "cat_cafe", "cat", age="1", color="white")
59 assert auth.scheme == "cat"
60 assert auth.principal == "molly"
61 assert auth.credentials == "meoooow"
62 assert auth.realm == "cat_cafe"
63 assert auth.parameters == {"age": "1", "color": "white"}
0 #!/usr/bin/env python
1 # -*- encoding: utf-8 -*-
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j.data import DataHydrator
24 from neo4j.graph import (
25 Node,
26 Path,
27 Graph,
28 Relationship,
29 )
30 from neo4j.packstream import Structure
31
32 # python -m pytest -s -v tests/unit/test_types.py
33
34
35 # Node
36
37
38 def test_can_create_node():
39 g = Graph()
40 gh = Graph.Hydrator(g)
41 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
42 assert isinstance(alice, Node)
43 assert alice.labels == {"Person"}
44 assert set(alice.keys()) == {"name", "age"}
45 assert set(alice.values()) == {"Alice", 33}
46 assert set(alice.items()) == {("name", "Alice"), ("age", 33)}
47 assert alice.get("name") == "Alice"
48 assert alice.get("age") == 33
49 assert len(alice) == 2
50 assert alice["name"] == "Alice"
51 assert alice["age"] == 33
52 assert "name" in alice
53 assert "age" in alice
54 assert set(iter(alice)) == {"name", "age"}
55
56
57 def test_null_properties():
58 g = Graph()
59 gh = Graph.Hydrator(g)
60 stuff = gh.hydrate_node(1, (), {"good": ["puppies", "kittens"], "bad": None})
61 assert isinstance(stuff, Node)
62 assert set(stuff.keys()) == {"good"}
63 assert stuff.get("good") == ["puppies", "kittens"]
64 assert stuff.get("bad") is None
65 assert len(stuff) == 1
66 assert stuff["good"] == ["puppies", "kittens"]
67 assert stuff["bad"] is None
68 assert "good" in stuff
69 assert "bad" not in stuff
70
71
72 def test_node_equality():
73 g = Graph()
74 node_1 = Node(g, 1234)
75 node_2 = Node(g, 1234)
76 node_3 = Node(g, 5678)
77 assert node_1 == node_2
78 assert node_1 != node_3
79 assert node_1 != "this is not a node"
80
81
82 def test_node_hashing():
83 g = Graph()
84 node_1 = Node(g, 1234)
85 node_2 = Node(g, 1234)
86 node_3 = Node(g, 5678)
87 assert hash(node_1) == hash(node_2)
88 assert hash(node_1) != hash(node_3)
89
90
91 def test_node_repr():
92 g = Graph()
93 gh = Graph.Hydrator(g)
94 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice"})
95 assert repr(alice) == "<Node id=1 labels=frozenset({'Person'}) properties={'name': 'Alice'}>"
96
97
98 # Relationship
99
100
101 def test_can_create_relationship():
102 g = Graph()
103 gh = Graph.Hydrator(g)
104 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
105 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44})
106 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
107 assert isinstance(alice_knows_bob, Relationship)
108 assert alice_knows_bob.start_node == alice
109 assert alice_knows_bob.type == "KNOWS"
110 assert alice_knows_bob.end_node == bob
111 assert set(alice_knows_bob.keys()) == {"since"}
112 assert set(alice_knows_bob.values()) == {1999}
113 assert set(alice_knows_bob.items()) == {("since", 1999)}
114 assert alice_knows_bob.get("since") == 1999
115
116
117 def test_relationship_repr():
118 g = Graph()
119 gh = Graph.Hydrator(g)
120 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice"})
121 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob"})
122 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
123 assert repr(alice_knows_bob) == "<Relationship id=1 nodes=(<Node id=1 labels=frozenset({'Person'}) properties={'name': 'Alice'}>, <Node id=2 labels=frozenset({'Person'}) properties={'name': 'Bob'}>) type='KNOWS' properties={'since': 1999}>"
124
125
126 # Path
127
128
129 def test_can_create_path():
130 g = Graph()
131 gh = Graph.Hydrator(g)
132 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
133 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44})
134 carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol", "age": 55})
135 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
136 carol_dislikes_bob = gh.hydrate_relationship(2, carol.id, bob.id, "DISLIKES", {})
137 path = Path(alice, alice_knows_bob, carol_dislikes_bob)
138 assert isinstance(path, Path)
139 assert path.start_node == alice
140 assert path.end_node == carol
141 assert path.nodes == (alice, bob, carol)
142 assert path.relationships == (alice_knows_bob, carol_dislikes_bob)
143 assert list(path) == [alice_knows_bob, carol_dislikes_bob]
144
145
146 def test_can_hydrate_path():
147 g = Graph()
148 gh = Graph.Hydrator(g)
149 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
150 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44})
151 carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol", "age": 55})
152 r = [gh.hydrate_unbound_relationship(1, "KNOWS", {"since": 1999}),
153 gh.hydrate_unbound_relationship(2, "DISLIKES", {})]
154 path = gh.hydrate_path([alice, bob, carol], r, [1, 1, -2, 2])
155 assert path.start_node == alice
156 assert path.end_node == carol
157 assert path.nodes == (alice, bob, carol)
158 expected_alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
159 expected_carol_dislikes_bob = gh.hydrate_relationship(2, carol.id, bob.id, "DISLIKES", {})
160 assert path.relationships == (expected_alice_knows_bob, expected_carol_dislikes_bob)
161 assert list(path) == [expected_alice_knows_bob, expected_carol_dislikes_bob]
162
163
164 def test_path_equality():
165 g = Graph()
166 gh = Graph.Hydrator(g)
167 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
168 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44})
169 carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol", "age": 55})
170 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
171 carol_dislikes_bob = gh.hydrate_relationship(2, carol.id, bob.id, "DISLIKES", {})
172 path_1 = Path(alice, alice_knows_bob, carol_dislikes_bob)
173 path_2 = Path(alice, alice_knows_bob, carol_dislikes_bob)
174 assert path_1 == path_2
175 assert path_1 != "this is not a path"
176
177
178 def test_path_hashing():
179 g = Graph()
180 gh = Graph.Hydrator(g)
181 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice", "age": 33})
182 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob", "age": 44})
183 carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol", "age": 55})
184 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
185 carol_dislikes_bob = gh.hydrate_relationship(2, carol.id, bob.id, "DISLIKES", {})
186 path_1 = Path(alice, alice_knows_bob, carol_dislikes_bob)
187 path_2 = Path(alice, alice_knows_bob, carol_dislikes_bob)
188 assert hash(path_1) == hash(path_2)
189
190
191 def test_path_repr():
192 g = Graph()
193 gh = Graph.Hydrator(g)
194 alice = gh.hydrate_node(1, {"Person"}, {"name": "Alice"})
195 bob = gh.hydrate_node(2, {"Person"}, {"name": "Bob"})
196 carol = gh.hydrate_node(3, {"Person"}, {"name": "Carol"})
197 alice_knows_bob = gh.hydrate_relationship(1, alice.id, bob.id, "KNOWS", {"since": 1999})
198 carol_dislikes_bob = gh.hydrate_relationship(2, carol.id, bob.id, "DISLIKES", {})
199 path = Path(alice, alice_knows_bob, carol_dislikes_bob)
200 assert repr(path) == "<Path start=<Node id=1 labels=frozenset({'Person'}) properties={'name': 'Alice'}> end=<Node id=3 labels=frozenset({'Person'}) properties={'name': 'Carol'}> size=2>"
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j.time import Clock, ClockTime
24
25
26 class ClockTestCase(TestCase):
27
28 def test_no_clock_implementations(self):
29 try:
30 Clock._Clock__implementations = []
31 with self.assertRaises(RuntimeError):
32 _ = Clock()
33 finally:
34 Clock._Clock__implementations = None
35
36 def test_base_clock_precision(self):
37 clock = object.__new__(Clock)
38 with self.assertRaises(NotImplementedError):
39 _ = clock.precision()
40
41 def test_base_clock_available(self):
42 clock = object.__new__(Clock)
43 with self.assertRaises(NotImplementedError):
44 _ = clock.available()
45
46 def test_base_clock_utc_time(self):
47 clock = object.__new__(Clock)
48 with self.assertRaises(NotImplementedError):
49 _ = clock.utc_time()
50
51 def test_local_offset(self):
52 clock = object.__new__(Clock)
53 offset = clock.local_offset()
54 self.assertIsInstance(offset, ClockTime)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j.time import ClockTime, Duration
24
25
26 class ClockTimeTestCase(TestCase):
27
28 def test_zero_(self):
29 ct = ClockTime()
30 self.assertEqual(ct.seconds, 0)
31 self.assertEqual(ct.nanoseconds, 0)
32
33 def test_only_seconds(self):
34 ct = ClockTime(123456)
35 self.assertEqual(ct.seconds, 123456)
36 self.assertEqual(ct.nanoseconds, 0)
37
38 def test_float(self):
39 ct = ClockTime(123456.789)
40 self.assertEqual(ct.seconds, 123456)
41 self.assertEqual(ct.nanoseconds, 789000000)
42
43 def test_only_nanoseconds(self):
44 ct = ClockTime(0, 123456789)
45 self.assertEqual(ct.seconds, 0)
46 self.assertEqual(ct.nanoseconds, 123456789)
47
48 def test_nanoseconds_overflow(self):
49 ct = ClockTime(0, 2123456789)
50 self.assertEqual(ct.seconds, 2)
51 self.assertEqual(ct.nanoseconds, 123456789)
52
53 def test_positive_nanoseconds(self):
54 ct = ClockTime(1, 1)
55 self.assertEqual(ct.seconds, 1)
56 self.assertEqual(ct.nanoseconds, 1)
57
58 def test_negative_nanoseconds(self):
59 ct = ClockTime(1, -1)
60 self.assertEqual(ct.seconds, 0)
61 self.assertEqual(ct.nanoseconds, 999999999)
62
63 def test_add_float(self):
64 ct = ClockTime(123456.789) + 0.1
65 self.assertEqual(ct.seconds, 123456)
66 self.assertEqual(ct.nanoseconds, 889000000)
67
68 def test_add_duration(self):
69 ct = ClockTime(123456.789) + Duration(seconds=1)
70 self.assertEqual(ct.seconds, 123457)
71 self.assertEqual(ct.nanoseconds, 789000000)
72
73 def test_add_duration_with_months(self):
74 with self.assertRaises(ValueError):
75 _ = ClockTime(123456.789) + Duration(months=1)
76
77 def test_add_object(self):
78 with self.assertRaises(TypeError):
79 _ = ClockTime(123456.789) + object()
80
81 def test_sub_float(self):
82 ct = ClockTime(123456.789) - 0.1
83 self.assertEqual(ct.seconds, 123456)
84 self.assertEqual(ct.nanoseconds, 689000000)
85
86 def test_sub_duration(self):
87 ct = ClockTime(123456.789) - Duration(seconds=1)
88 self.assertEqual(ct.seconds, 123455)
89 self.assertEqual(ct.nanoseconds, 789000000)
90
91 def test_sub_duration_with_months(self):
92 with self.assertRaises(ValueError):
93 _ = ClockTime(123456.789) - Duration(months=1)
94
95 def test_sub_object(self):
96 with self.assertRaises(TypeError):
97 _ = ClockTime(123456.789) - object()
98
99 def test_repr(self):
100 ct = ClockTime(123456.789)
101 self.assertTrue(repr(ct).startswith("ClockTime"))
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from datetime import date
22 from time import struct_time
23 from unittest import TestCase
24
25 import pytz
26
27 from neo4j.time import Duration, Date, UnixEpoch, ZeroDate
28
29
30 eastern = pytz.timezone("US/Eastern")
31
32
33 class DateTestCase(TestCase):
34
35 def test_bad_attribute(self):
36 d = Date(2000, 1, 1)
37 with self.assertRaises(AttributeError):
38 _ = d.x
39
40 def test_zero_date(self):
41 d = Date(0, 0, 0)
42 self.assertEqual(d.year_month_day, (0, 0, 0))
43 self.assertEqual(d.year, 0)
44 self.assertEqual(d.month, 0)
45 self.assertEqual(d.day, 0)
46 self.assertIs(d, ZeroDate)
47
48 def test_zero_ordinal(self):
49 d = Date.from_ordinal(0)
50 self.assertEqual(d.year_month_day, (0, 0, 0))
51 self.assertEqual(d.year, 0)
52 self.assertEqual(d.month, 0)
53 self.assertEqual(d.day, 0)
54 self.assertIs(d, ZeroDate)
55
56 def test_ordinal_at_start_of_1970(self):
57 d = Date.from_ordinal(719163)
58 self.assertEqual(d.year_month_day, (1970, 1, 1))
59 self.assertEqual(d.year, 1970)
60 self.assertEqual(d.month, 1)
61 self.assertEqual(d.day, 1)
62
63 def test_ordinal_at_end_of_1969(self):
64 d = Date.from_ordinal(719162)
65 self.assertEqual(d.year_month_day, (1969, 12, 31))
66 self.assertEqual(d.year, 1969)
67 self.assertEqual(d.month, 12)
68 self.assertEqual(d.day, 31)
69
70 def test_ordinal_at_start_of_2018(self):
71 d = Date.from_ordinal(736695)
72 self.assertEqual(d.year_month_day, (2018, 1, 1))
73 self.assertEqual(d.year, 2018)
74 self.assertEqual(d.month, 1)
75 self.assertEqual(d.day, 1)
76
77 def test_ordinal_at_end_of_2017(self):
78 d = Date.from_ordinal(736694)
79 self.assertEqual(d.year_month_day, (2017, 12, 31))
80 self.assertEqual(d.year, 2017)
81 self.assertEqual(d.month, 12)
82 self.assertEqual(d.day, 31)
83
84 def test_all_positive_days_of_month_for_31_day_month(self):
85 for day in range(1, 32):
86 t = Date(1976, 1, day)
87 self.assertEqual(t.year_month_day, (1976, 1, day))
88 self.assertEqual(t.year, 1976)
89 self.assertEqual(t.month, 1)
90 self.assertEqual(t.day, day)
91 with self.assertRaises(ValueError):
92 _ = Date(1976, 1, 32)
93
94 def test_all_positive_days_of_month_for_30_day_month(self):
95 for day in range(1, 31):
96 t = Date(1976, 6, day)
97 self.assertEqual(t.year_month_day, (1976, 6, day))
98 self.assertEqual(t.year, 1976)
99 self.assertEqual(t.month, 6)
100 self.assertEqual(t.day, day)
101 with self.assertRaises(ValueError):
102 _ = Date(1976, 6, 31)
103
104 def test_all_positive_days_of_month_for_29_day_month(self):
105 for day in range(1, 30):
106 t = Date(1976, 2, day)
107 self.assertEqual(t.year_month_day, (1976, 2, day))
108 self.assertEqual(t.year, 1976)
109 self.assertEqual(t.month, 2)
110 self.assertEqual(t.day, day)
111 with self.assertRaises(ValueError):
112 _ = Date(1976, 2, 30)
113
114 def test_all_positive_days_of_month_for_28_day_month(self):
115 for day in range(1, 29):
116 t = Date(1977, 2, day)
117 self.assertEqual(t.year_month_day, (1977, 2, day))
118 self.assertEqual(t.year, 1977)
119 self.assertEqual(t.month, 2)
120 self.assertEqual(t.day, day)
121 with self.assertRaises(ValueError):
122 _ = Date(1977, 2, 29)
123
124 def test_last_but_2_day_for_31_day_month(self):
125 t = Date(1976, 1, -3)
126 self.assertEqual(t.year_month_day, (1976, 1, 29))
127 self.assertEqual(t.year, 1976)
128 self.assertEqual(t.month, 1)
129 self.assertEqual(t.day, 29)
130
131 def test_last_but_1_day_for_31_day_month(self):
132 t = Date(1976, 1, -2)
133 self.assertEqual(t.year_month_day, (1976, 1, 30))
134 self.assertEqual(t.year, 1976)
135 self.assertEqual(t.month, 1)
136 self.assertEqual(t.day, 30)
137
138 def test_last_day_for_31_day_month(self):
139 t = Date(1976, 1, -1)
140 self.assertEqual(t.year_month_day, (1976, 1, 31))
141 self.assertEqual(t.year, 1976)
142 self.assertEqual(t.month, 1)
143 self.assertEqual(t.day, 31)
144
145 def test_last_but_1_day_for_30_day_month(self):
146 t = Date(1976, 6, -2)
147 self.assertEqual(t.year_month_day, (1976, 6, 29))
148 self.assertEqual(t.year, 1976)
149 self.assertEqual(t.month, 6)
150 self.assertEqual(t.day, 29)
151
152 def test_last_day_for_30_day_month(self):
153 t = Date(1976, 6, -1)
154 self.assertEqual(t.year_month_day, (1976, 6, 30))
155 self.assertEqual(t.year, 1976)
156 self.assertEqual(t.month, 6)
157 self.assertEqual(t.day, 30)
158
159 def test_day_28_for_29_day_month(self):
160 t = Date(1976, 2, 28)
161 self.assertEqual(t.year_month_day, (1976, 2, 28))
162 self.assertEqual(t.year, 1976)
163 self.assertEqual(t.month, 2)
164 self.assertEqual(t.day, 28)
165
166 def test_last_day_for_29_day_month(self):
167 t = Date(1976, 2, -1)
168 self.assertEqual(t.year_month_day, (1976, 2, 29))
169 self.assertEqual(t.year, 1976)
170 self.assertEqual(t.month, 2)
171 self.assertEqual(t.day, 29)
172
173 def test_last_day_for_28_day_month(self):
174 t = Date(1977, 2, -1)
175 self.assertEqual(t.year_month_day, (1977, 2, 28))
176 self.assertEqual(t.year, 1977)
177 self.assertEqual(t.month, 2)
178 self.assertEqual(t.day, 28)
179
180 def test_cannot_use_year_lower_than_one(self):
181 with self.assertRaises(ValueError):
182 _ = Date(0, 2, 1)
183
184 def test_cannot_use_year_higher_than_9999(self):
185 with self.assertRaises(ValueError):
186 _ = Date(10000, 2, 1)
187
188 def test_today(self):
189 d = Date.today()
190 self.assertIsInstance(d, Date)
191
192 def test_today_with_tz(self):
193 d = Date.today(tz=eastern)
194 self.assertIsInstance(d, Date)
195
196 def test_utc_today(self):
197 d = Date.utc_today()
198 self.assertIsInstance(d, Date)
199
200 def test_from_timestamp_without_tz(self):
201 d = Date.from_timestamp(0)
202 self.assertEqual(d, Date(1970, 1, 1))
203
204 def test_from_timestamp_with_tz(self):
205 d = Date.from_timestamp(0, tz=eastern)
206 self.assertEqual(d, Date(1969, 12, 31))
207
208 def test_utc_from_timestamp(self):
209 d = Date.utc_from_timestamp(0)
210 self.assertEqual(d, Date(1970, 1, 1))
211
212 def test_from_ordinal(self):
213 d = Date.from_ordinal(1)
214 self.assertEqual(d, Date(1, 1, 1))
215
216 def test_parse(self):
217 d = Date.parse("2018-04-30")
218 self.assertEqual(d, Date(2018, 4, 30))
219
220 def test_bad_parse_1(self):
221 with self.assertRaises(ValueError):
222 _ = Date.parse("30 April 2018")
223
224 def test_bad_parse_2(self):
225 with self.assertRaises(ValueError):
226 _ = Date.parse("2018-04")
227
228 def test_bad_parse_3(self):
229 with self.assertRaises(ValueError):
230 _ = Date.parse(object())
231
232 def test_replace(self):
233 d1 = Date(2018, 4, 30)
234 d2 = d1.replace(year=2017)
235 self.assertEqual(d2, Date(2017, 4, 30))
236
237 def test_from_clock_time(self):
238 d = Date.from_clock_time((0, 0), epoch=UnixEpoch)
239 self.assertEqual(d, Date(1970, 1, 1))
240
241 def test_bad_from_clock_time(self):
242 with self.assertRaises(ValueError):
243 _ = Date.from_clock_time(object(), None)
244
245 def test_is_leap_year(self):
246 self.assertTrue(Date.is_leap_year(2000))
247 self.assertFalse(Date.is_leap_year(2001))
248
249 def test_days_in_year(self):
250 self.assertEqual(Date.days_in_year(2000), 366)
251 self.assertEqual(Date.days_in_year(2001), 365)
252
253 def test_days_in_month(self):
254 self.assertEqual(Date.days_in_month(2000, 1), 31)
255 self.assertEqual(Date.days_in_month(2000, 2), 29)
256 self.assertEqual(Date.days_in_month(2001, 2), 28)
257
258 def test_instance_attributes(self):
259 d = Date(2018, 4, 30)
260 self.assertEqual(d.year, 2018)
261 self.assertEqual(d.month, 4)
262 self.assertEqual(d.day, 30)
263 self.assertEqual(d.year_month_day, (2018, 4, 30))
264 self.assertEqual(d.year_week_day, (2018, 18, 1))
265 self.assertEqual(d.year_day, (2018, 120))
266
267 def test_can_add_years(self):
268 d1 = Date(1976, 6, 13)
269 d2 = d1 + Duration(years=2)
270 self.assertEqual(d2, Date(1978, 6, 13))
271
272 def test_can_add_negative_years(self):
273 d1 = Date(1976, 6, 13)
274 d2 = d1 + Duration(years=-2)
275 self.assertEqual(d2, Date(1974, 6, 13))
276
277 def test_can_add_years_and_months(self):
278 d1 = Date(1976, 6, 13)
279 d2 = d1 + Duration(years=2, months=3)
280 self.assertEqual(d2, Date(1978, 9, 13))
281
282 def test_can_add_negative_years_and_months(self):
283 d1 = Date(1976, 6, 13)
284 d2 = d1 + Duration(years=-2, months=-3)
285 self.assertEqual(d2, Date(1974, 3, 13))
286
287 def test_can_retain_offset_from_end_of_month(self):
288 d = Date(1976, 1, -1)
289 self.assertEqual(d, Date(1976, 1, 31))
290 d += Duration(months=1)
291 self.assertEqual(d, Date(1976, 2, 29))
292 d += Duration(months=1)
293 self.assertEqual(d, Date(1976, 3, 31))
294 d += Duration(months=1)
295 self.assertEqual(d, Date(1976, 4, 30))
296 d += Duration(months=1)
297 self.assertEqual(d, Date(1976, 5, 31))
298 d += Duration(months=1)
299 self.assertEqual(d, Date(1976, 6, 30))
300
301 def test_can_roll_over_end_of_year(self):
302 d = Date(1976, 12, 1)
303 self.assertEqual(d, Date(1976, 12, 1))
304 d += Duration(months=1)
305 self.assertEqual(d, Date(1977, 1, 1))
306
307 def test_can_add_months_and_days(self):
308 d1 = Date(1976, 6, 13)
309 d2 = d1 + Duration(months=1, days=1)
310 self.assertEqual(d2, Date(1976, 7, 14))
311
312 def test_can_add_months_then_days(self):
313 d1 = Date(1976, 6, 13)
314 d2 = d1 + Duration(months=1) + Duration(days=1)
315 self.assertEqual(d2, Date(1976, 7, 14))
316
317 def test_cannot_add_seconds(self):
318 d1 = Date(1976, 6, 13)
319 with self.assertRaises(ValueError):
320 _ = d1 + Duration(seconds=1)
321
322 def test_adding_empty_duration_returns_self(self):
323 d1 = Date(1976, 6, 13)
324 d2 = d1 + Duration()
325 self.assertIs(d1, d2)
326
327 def test_adding_object(self):
328 d1 = Date(1976, 6, 13)
329 with self.assertRaises(TypeError):
330 _ = d1 + object()
331
332 def test_can_add_days_then_months(self):
333 d1 = Date(1976, 6, 13)
334 d2 = d1 + Duration(days=1) + Duration(months=1)
335 self.assertEqual(d2, Date(1976, 7, 14))
336
337 def test_can_add_months_and_days_for_last_day_of_short_month(self):
338 d1 = Date(1976, 6, 30)
339 d2 = d1 + Duration(months=1, days=1)
340 self.assertEqual(d2, Date(1976, 8, 1))
341
342 def test_can_add_months_then_days_for_last_day_of_short_month(self):
343 d1 = Date(1976, 6, 30)
344 d2 = d1 + Duration(months=1) + Duration(days=1)
345 self.assertEqual(d2, Date(1976, 8, 1))
346
347 def test_can_add_days_then_months_for_last_day_of_short_month(self):
348 d1 = Date(1976, 6, 30)
349 d2 = d1 + Duration(days=1) + Duration(months=1)
350 self.assertEqual(d2, Date(1976, 8, 1))
351
352 def test_can_add_months_and_days_for_last_day_of_long_month(self):
353 d1 = Date(1976, 1, 31)
354 d2 = d1 + Duration(months=1, days=1)
355 self.assertEqual(d2, Date(1976, 3, 1))
356
357 def test_can_add_months_then_days_for_last_day_of_long_month(self):
358 d1 = Date(1976, 1, 31)
359 d2 = d1 + Duration(months=1) + Duration(days=1)
360 self.assertEqual(d2, Date(1976, 3, 1))
361
362 def test_can_add_days_then_months_for_last_day_of_long_month(self):
363 d1 = Date(1976, 1, 31)
364 d2 = d1 + Duration(days=1) + Duration(months=1)
365 self.assertEqual(d2, Date(1976, 3, 1))
366
367 def test_can_add_negative_months_and_days(self):
368 d1 = Date(1976, 6, 13)
369 d2 = d1 + Duration(months=-1, days=-1)
370 self.assertEqual(d2, Date(1976, 5, 12))
371
372 def test_can_add_negative_months_then_days(self):
373 d1 = Date(1976, 6, 13)
374 d2 = d1 + Duration(months=-1) + Duration(days=-1)
375 self.assertEqual(d2, Date(1976, 5, 12))
376
377 def test_can_add_negative_days_then_months(self):
378 d1 = Date(1976, 6, 13)
379 d2 = d1 + Duration(days=-1) + Duration(months=-1)
380 self.assertEqual(d2, Date(1976, 5, 12))
381
382 def test_can_add_negative_months_and_days_for_first_day_of_month(self):
383 d1 = Date(1976, 6, 1)
384 d2 = d1 + Duration(months=-1, days=-1)
385 self.assertEqual(d2, Date(1976, 4, 30))
386
387 def test_can_add_negative_months_then_days_for_first_day_of_month(self):
388 d1 = Date(1976, 6, 1)
389 d2 = d1 + Duration(months=-1) + Duration(days=-1)
390 self.assertEqual(d2, Date(1976, 4, 30))
391
392 def test_can_add_negative_days_then_months_for_last_day_of_month(self):
393 d1 = Date(1976, 6, 1)
394 d2 = d1 + Duration(days=-1) + Duration(months=-1)
395 self.assertEqual(d2, Date(1976, 4, 30))
396
397 def test_can_add_negative_month_for_last_day_of_long_month(self):
398 d1 = Date(1976, 5, 31)
399 d2 = d1 + Duration(months=-1)
400 self.assertEqual(d2, Date(1976, 4, 30))
401
402 def test_can_add_negative_month_for_january(self):
403 d1 = Date(1976, 1, 31)
404 d2 = d1 + Duration(months=-1)
405 self.assertEqual(d2, Date(1975, 12, 31))
406
407 def test_subtract_date(self):
408 new_year = Date(2000, 1, 1)
409 christmas = Date(1999, 12, 25)
410 self.assertEqual(new_year - christmas, Duration(days=7))
411
412 def test_subtract_duration(self):
413 new_year = Date(2000, 1, 1)
414 christmas = Date(1999, 12, 25)
415 self.assertEqual(new_year - Duration(days=7), christmas)
416
417 def test_subtract_object(self):
418 new_year = Date(2000, 1, 1)
419 with self.assertRaises(TypeError):
420 _ = new_year - object()
421
422 def test_date_less_than(self):
423 new_year = Date(2000, 1, 1)
424 christmas = Date(1999, 12, 25)
425 self.assertLess(christmas, new_year)
426
427 def test_date_less_than_object(self):
428 d = Date(2000, 1, 1)
429 with self.assertRaises(TypeError):
430 _ = d < object()
431
432 def test_date_less_than_or_equal_to(self):
433 new_year = Date(2000, 1, 1)
434 christmas = Date(1999, 12, 25)
435 self.assertLessEqual(christmas, new_year)
436
437 def test_date_less_than_or_equal_to_object(self):
438 d = Date(2000, 1, 1)
439 with self.assertRaises(TypeError):
440 _ = d <= object()
441
442 def test_date_greater_than_or_equal_to(self):
443 new_year = Date(2000, 1, 1)
444 christmas = Date(1999, 12, 25)
445 self.assertGreaterEqual(new_year, christmas)
446
447 def test_date_greater_than_or_equal_to_object(self):
448 d = Date(2000, 1, 1)
449 with self.assertRaises(TypeError):
450 _ = d >= object()
451
452 def test_date_greater_than(self):
453 new_year = Date(2000, 1, 1)
454 christmas = Date(1999, 12, 25)
455 self.assertGreater(new_year, christmas)
456
457 def test_date_greater_than_object(self):
458 d = Date(2000, 1, 1)
459 with self.assertRaises(TypeError):
460 _ = d > object()
461
462 def test_date_equal(self):
463 d1 = Date(2000, 1, 1)
464 d2 = Date(2000, 1, 1)
465 self.assertEqual(d1, d2)
466
467 def test_date_not_equal(self):
468 d1 = Date(2000, 1, 1)
469 d2 = Date(2000, 1, 2)
470 self.assertNotEqual(d1, d2)
471
472 def test_date_not_equal_to_object(self):
473 d1 = Date(2000, 1, 1)
474 self.assertNotEqual(d1, object())
475
476 def test_year_week_day(self):
477 for ordinal in range(Date(2001, 1, 1).to_ordinal(), Date(2008, 1, 1).to_ordinal()):
478 self.assertEqual(Date.from_ordinal(ordinal).iso_calendar(), date.fromordinal(ordinal).isocalendar())
479
480 def test_time_tuple(self):
481 d = Date(2018, 4, 30)
482 self.assertEqual(d.time_tuple(), struct_time((2018, 4, 30, 0, 0, 0, 0, 120, -1)))
483
484 def test_to_clock_time(self):
485 d = Date(2018, 4, 30)
486 self.assertEqual(d.to_clock_time(UnixEpoch), (1525046400, 0))
487 self.assertEqual(d.to_clock_time(d), (0, 0))
488 with self.assertRaises(TypeError):
489 _ = d.to_clock_time(object())
490
491 def test_weekday(self):
492 d = Date(2018, 4, 30)
493 self.assertEqual(d.weekday(), 0)
494
495 def test_iso_weekday(self):
496 d = Date(2018, 4, 30)
497 self.assertEqual(d.iso_weekday(), 1)
498
499 def test_str(self):
500 self.assertEqual(str(Date(2018, 4, 30)), "2018-04-30")
501 self.assertEqual(str(Date(0, 0, 0)), "0000-00-00")
502
503 def test_repr(self):
504 self.assertEqual(repr(Date(2018, 4, 30)), "neo4j.time.Date(2018, 4, 30)")
505 self.assertEqual(repr(Date(0, 0, 0)), "neo4j.time.ZeroDate")
506
507 def test_format(self):
508 d = Date(2018, 4, 30)
509 with self.assertRaises(NotImplementedError):
510 _ = d.__format__("")
511
512 def test_from_native(self):
513 native = date(2018, 10, 1)
514 d = Date.from_native(native)
515 self.assertEqual(d.year, native.year)
516 self.assertEqual(d.month, native.month)
517 self.assertEqual(d.day, native.day)
518
519 def test_to_native(self):
520 d = Date(2018, 10, 1)
521 native = d.to_native()
522 self.assertEqual(d.year, native.year)
523 self.assertEqual(d.month, native.month)
524 self.assertEqual(d.day, native.day)
525
526 def test_iso_format(self):
527 d = Date(2018, 10, 1)
528 self.assertEqual("2018-10-01", d.iso_format())
529
530 def test_from_iso_format(self):
531 expected = Date(2018, 10, 1)
532 actual = Date.from_iso_format("2018-10-01")
533 self.assertEqual(expected, actual)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22 from datetime import (
23 datetime,
24 timedelta,
25 )
26 from pytz import (
27 timezone,
28 FixedOffset,
29 )
30
31 from neo4j.time import (
32 DateTime,
33 MIN_YEAR,
34 MAX_YEAR,
35 Duration,
36 )
37 from neo4j.time.arithmetic import (
38 nano_add,
39 nano_div,
40 )
41 from neo4j.time.clock_implementations import (
42 Clock,
43 ClockTime,
44 )
45 from neo4j.time.hydration import (
46 hydrate_date,
47 dehydrate_date,
48 hydrate_time,
49 dehydrate_time,
50 hydrate_datetime,
51 dehydrate_datetime,
52 hydrate_duration,
53 dehydrate_duration,
54 dehydrate_timedelta,
55 )
56
57 timezone_us_eastern = timezone("US/Eastern")
58 timezone_utc = timezone("UTC")
59
60
61 class FixedClock(Clock):
62
63 @classmethod
64 def available(cls):
65 return True
66
67 @classmethod
68 def precision(cls):
69 return 12
70
71 @classmethod
72 def local_offset(cls):
73 return ClockTime()
74
75 def utc_time(self):
76 return ClockTime(45296, 789000000)
77
78
79 class DateTimeTestCase(TestCase):
80
81 def test_zero(self):
82 t = DateTime(0, 0, 0, 0, 0, 0)
83 self.assertEqual(t.year, 0)
84 self.assertEqual(t.month, 0)
85 self.assertEqual(t.day, 0)
86 self.assertEqual(t.hour, 0)
87 self.assertEqual(t.minute, 0)
88 self.assertEqual(t.second, 0)
89
90 def test_non_zero_naive(self):
91 t = DateTime(2018, 4, 26, 23, 0, 17.914390409)
92 self.assertEqual(t.year, 2018)
93 self.assertEqual(t.month, 4)
94 self.assertEqual(t.day, 26)
95 self.assertEqual(t.hour, 23)
96 self.assertEqual(t.minute, 0)
97 self.assertEqual(t.second, 17.914390409)
98
99 def test_year_lower_bound(self):
100 with self.assertRaises(ValueError):
101 _ = DateTime(MIN_YEAR - 1, 1, 1, 0, 0, 0)
102
103 def test_year_upper_bound(self):
104 with self.assertRaises(ValueError):
105 _ = DateTime(MAX_YEAR + 1, 1, 1, 0, 0, 0)
106
107 def test_month_lower_bound(self):
108 with self.assertRaises(ValueError):
109 _ = DateTime(2000, 0, 1, 0, 0, 0)
110
111 def test_month_upper_bound(self):
112 with self.assertRaises(ValueError):
113 _ = DateTime(2000, 13, 1, 0, 0, 0)
114
115 def test_day_zero(self):
116 with self.assertRaises(ValueError):
117 _ = DateTime(2000, 1, 0, 0, 0, 0)
118
119 def test_day_30_of_29_day_month(self):
120 with self.assertRaises(ValueError):
121 _ = DateTime(2000, 2, 30, 0, 0, 0)
122
123 def test_day_32_of_31_day_month(self):
124 with self.assertRaises(ValueError):
125 _ = DateTime(2000, 3, 32, 0, 0, 0)
126
127 def test_day_31_of_30_day_month(self):
128 with self.assertRaises(ValueError):
129 _ = DateTime(2000, 4, 31, 0, 0, 0)
130
131 def test_day_29_of_28_day_month(self):
132 with self.assertRaises(ValueError):
133 _ = DateTime(1999, 2, 29, 0, 0, 0)
134
135 def test_last_day_of_month(self):
136 t = DateTime(2000, 1, -1, 0, 0, 0)
137 self.assertEqual(t.year, 2000)
138 self.assertEqual(t.month, 1)
139 self.assertEqual(t.day, 31)
140
141 def test_today(self):
142 t = DateTime.today()
143 self.assertEqual(t.year, 1970)
144 self.assertEqual(t.month, 1)
145 self.assertEqual(t.day, 1)
146 self.assertEqual(t.hour, 12)
147 self.assertEqual(t.minute, 34)
148 self.assertEqual(t.second, 56.789)
149
150 def test_now_without_tz(self):
151 t = DateTime.now()
152 self.assertEqual(t.year, 1970)
153 self.assertEqual(t.month, 1)
154 self.assertEqual(t.day, 1)
155 self.assertEqual(t.hour, 12)
156 self.assertEqual(t.minute, 34)
157 self.assertEqual(t.second, 56.789)
158 self.assertIsNone(t.tzinfo)
159
160 def test_now_with_tz(self):
161 t = DateTime.now(timezone_us_eastern)
162 self.assertEqual(t.year, 1970)
163 self.assertEqual(t.month, 1)
164 self.assertEqual(t.day, 1)
165 self.assertEqual(t.hour, 7)
166 self.assertEqual(t.minute, 34)
167 self.assertEqual(t.second, 56.789)
168 self.assertEqual(t.utcoffset(), timedelta(seconds=-18000))
169 self.assertEqual(t.dst(), timedelta())
170 self.assertEqual(t.tzname(), "EST")
171
172 def test_utc_now(self):
173 t = DateTime.utc_now()
174 self.assertEqual(t.year, 1970)
175 self.assertEqual(t.month, 1)
176 self.assertEqual(t.day, 1)
177 self.assertEqual(t.hour, 12)
178 self.assertEqual(t.minute, 34)
179 self.assertEqual(t.second, 56.789)
180 self.assertIsNone(t.tzinfo)
181
182 def test_from_timestamp(self):
183 t = DateTime.from_timestamp(0)
184 self.assertEqual(t.year, 1970)
185 self.assertEqual(t.month, 1)
186 self.assertEqual(t.day, 1)
187 self.assertEqual(t.hour, 0)
188 self.assertEqual(t.minute, 0)
189 self.assertEqual(t.second, 0.0)
190 self.assertIsNone(t.tzinfo)
191
192 def test_from_overflowing_timestamp(self):
193 with self.assertRaises(ValueError):
194 _ = DateTime.from_timestamp(999999999999999999)
195
196 def test_from_timestamp_with_tz(self):
197 t = DateTime.from_timestamp(0, timezone_us_eastern)
198 self.assertEqual(t.year, 1969)
199 self.assertEqual(t.month, 12)
200 self.assertEqual(t.day, 31)
201 self.assertEqual(t.hour, 19)
202 self.assertEqual(t.minute, 0)
203 self.assertEqual(t.second, 0.0)
204 self.assertEqual(t.utcoffset(), timedelta(seconds=-18000))
205 self.assertEqual(t.dst(), timedelta())
206 self.assertEqual(t.tzname(), "EST")
207
208 def test_conversion_to_t(self):
209 dt = DateTime(2018, 4, 26, 23, 0, 17.914390409)
210 t = dt.to_clock_time()
211 self.assertEqual(t, ClockTime(63660380417, 914390409))
212
213 def test_add_timedelta(self):
214 dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409)
215 delta = timedelta(days=1)
216 dt2 = dt1 + delta
217 self.assertEqual(dt2, DateTime(2018, 4, 27, 23, 0, 17.914390409))
218
219 def test_subtract_datetime_1(self):
220 dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409)
221 dt2 = DateTime(2018, 1, 1, 0, 0, 0.0)
222 t = dt1 - dt2
223 self.assertEqual(t, Duration(months=3, days=25, hours=23, seconds=17.914390409))
224
225 def test_subtract_datetime_2(self):
226 dt1 = DateTime(2018, 4, 1, 23, 0, 17.914390409)
227 dt2 = DateTime(2018, 1, 26, 0, 0, 0.0)
228 t = dt1 - dt2
229 self.assertEqual(t, Duration(months=3, days=-25, hours=23, seconds=17.914390409))
230
231 def test_subtract_native_datetime_1(self):
232 dt1 = DateTime(2018, 4, 26, 23, 0, 17.914390409)
233 dt2 = datetime(2018, 1, 1, 0, 0, 0)
234 t = dt1 - dt2
235 self.assertEqual(t, timedelta(days=115, hours=23, seconds=17.914390409))
236
237 def test_subtract_native_datetime_2(self):
238 dt1 = DateTime(2018, 4, 1, 23, 0, 17.914390409)
239 dt2 = datetime(2018, 1, 26, 0, 0, 0)
240 t = dt1 - dt2
241 self.assertEqual(t, timedelta(days=65, hours=23, seconds=17.914390409))
242
243 def test_normalization(self):
244 ndt1 = timezone_us_eastern.normalize(DateTime(2018, 4, 27, 23, 0, 17, tzinfo=timezone_us_eastern))
245 ndt2 = timezone_us_eastern.normalize(datetime(2018, 4, 27, 23, 0, 17, tzinfo=timezone_us_eastern))
246 self.assertEqual(ndt1, ndt2)
247
248 def test_localization(self):
249 ldt1 = timezone_us_eastern.localize(datetime(2018, 4, 27, 23, 0, 17))
250 ldt2 = timezone_us_eastern.localize(DateTime(2018, 4, 27, 23, 0, 17))
251 self.assertEqual(ldt1, ldt2)
252
253 def test_from_native(self):
254 native = datetime(2018, 10, 1, 12, 34, 56, 789123)
255 dt = DateTime.from_native(native)
256 self.assertEqual(dt.year, native.year)
257 self.assertEqual(dt.month, native.month)
258 self.assertEqual(dt.day, native.day)
259 self.assertEqual(dt.hour, native.hour)
260 self.assertEqual(dt.minute, native.minute)
261 self.assertEqual(dt.second, nano_add(native.second, nano_div(native.microsecond, 1000000)))
262
263 def test_to_native(self):
264 dt = DateTime(2018, 10, 1, 12, 34, 56.789123456)
265 native = dt.to_native()
266 self.assertEqual(dt.year, native.year)
267 self.assertEqual(dt.month, native.month)
268 self.assertEqual(dt.day, native.day)
269 self.assertEqual(dt.hour, native.hour)
270 self.assertEqual(dt.minute, native.minute)
271 self.assertEqual(56.789123, nano_add(native.second, nano_div(native.microsecond, 1000000)))
272
273 def test_iso_format(self):
274 dt = DateTime(2018, 10, 1, 12, 34, 56.789123456)
275 self.assertEqual("2018-10-01T12:34:56.789123456", dt.iso_format())
276
277 def test_iso_format_with_trailing_zeroes(self):
278 dt = DateTime(2018, 10, 1, 12, 34, 56.789)
279 self.assertEqual("2018-10-01T12:34:56.789000000", dt.iso_format())
280
281 def test_iso_format_with_tz(self):
282 dt = timezone_us_eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789123456))
283 self.assertEqual("2018-10-01T12:34:56.789123456-04:00", dt.iso_format())
284
285 def test_iso_format_with_tz_and_trailing_zeroes(self):
286 dt = timezone_us_eastern.localize(DateTime(2018, 10, 1, 12, 34, 56.789))
287 self.assertEqual("2018-10-01T12:34:56.789000000-04:00", dt.iso_format())
288
289 def test_from_iso_format_hour_only(self):
290 expected = DateTime(2018, 10, 1, 12, 0, 0)
291 actual = DateTime.from_iso_format("2018-10-01T12")
292 self.assertEqual(expected, actual)
293
294 def test_from_iso_format_hour_and_minute(self):
295 expected = DateTime(2018, 10, 1, 12, 34, 0)
296 actual = DateTime.from_iso_format("2018-10-01T12:34")
297 self.assertEqual(expected, actual)
298
299 def test_from_iso_format_hour_minute_second(self):
300 expected = DateTime(2018, 10, 1, 12, 34, 56)
301 actual = DateTime.from_iso_format("2018-10-01T12:34:56")
302 self.assertEqual(expected, actual)
303
304 def test_from_iso_format_hour_minute_second_milliseconds(self):
305 expected = DateTime(2018, 10, 1, 12, 34, 56.123)
306 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123")
307 self.assertEqual(expected, actual)
308
309 def test_from_iso_format_hour_minute_second_microseconds(self):
310 expected = DateTime(2018, 10, 1, 12, 34, 56.123456)
311 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456")
312 self.assertEqual(expected, actual)
313
314 def test_from_iso_format_hour_minute_second_nanoseconds(self):
315 expected = DateTime(2018, 10, 1, 12, 34, 56.123456789)
316 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789")
317 self.assertEqual(expected, actual)
318
319 def test_from_iso_format_with_positive_tz(self):
320 expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(754))
321 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789+12:34")
322 self.assertEqual(expected, actual)
323
324 def test_from_iso_format_with_negative_tz(self):
325 expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(-754))
326 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789-12:34")
327 self.assertEqual(expected, actual)
328
329 def test_from_iso_format_with_positive_long_tz(self):
330 expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(754))
331 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789+12:34:56.123456")
332 self.assertEqual(expected, actual)
333
334 def test_from_iso_format_with_negative_long_tz(self):
335 expected = DateTime(2018, 10, 1, 12, 34, 56.123456789, tzinfo=FixedOffset(-754))
336 actual = DateTime.from_iso_format("2018-10-01T12:34:56.123456789-12:34:56.123456")
337 self.assertEqual(expected, actual)
338
339
340 def test_iso_format_with_time_zone_case_1():
341 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_iso_format_with_time_zone_case_1
342 expected = DateTime(2019, 10, 30, 7, 54, 2.129790999, tzinfo=timezone_utc)
343 assert expected.iso_format() == "2019-10-30T07:54:02.129790999+00:00"
344 assert expected.tzinfo == FixedOffset(0)
345 actual = DateTime.from_iso_format("2019-10-30T07:54:02.129790999+00:00")
346 assert expected == actual
347
348
349 def test_iso_format_with_time_zone_case_2():
350 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_iso_format_with_time_zone_case_2
351 expected = DateTime.from_iso_format("2019-10-30T07:54:02.129790999+01:00")
352 assert expected.tzinfo == FixedOffset(60)
353 assert expected.iso_format() == "2019-10-30T07:54:02.129790999+01:00"
354
355
356 def test_to_native_case_1():
357 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_to_native_case_1
358 dt = DateTime.from_iso_format("2019-10-30T12:34:56.789123456")
359 native = dt.to_native()
360 assert native.hour == dt.hour
361 assert native.minute == dt.minute
362 assert nano_add(native.second, nano_div(native.microsecond, 1000000)) == 56.789123
363 assert native.tzinfo is None
364 assert native.isoformat() == "2019-10-30T12:34:56.789123"
365
366
367 def test_to_native_case_2():
368 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_to_native_case_2
369 dt = DateTime.from_iso_format("2019-10-30T12:34:56.789123456+00:00")
370 native = dt.to_native()
371 assert native.hour == dt.hour
372 assert native.minute == dt.minute
373 assert nano_add(native.second, nano_div(native.microsecond, 1000000)) == 56.789123
374 assert native.tzinfo == FixedOffset(0)
375 assert native.isoformat() == "2019-10-30T12:34:56.789123+00:00"
376
377
378 def test_from_native_case_1():
379 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_from_native_case_1
380 native = datetime(2018, 10, 1, 12, 34, 56, 789123)
381 dt = DateTime.from_native(native)
382 assert dt.year == native.year
383 assert dt.month == native.month
384 assert dt.day == native.day
385 assert dt.hour == native.hour
386 assert dt.minute == native.minute
387 assert dt.second == nano_add(native.second, nano_div(native.microsecond, 1000000))
388 assert dt.tzinfo is None
389
390
391 def test_from_native_case_2():
392 # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_from_native_case_2
393 native = datetime(2018, 10, 1, 12, 34, 56, 789123, FixedOffset(0))
394 dt = DateTime.from_native(native)
395 assert dt.year == native.year
396 assert dt.month == native.month
397 assert dt.day == native.day
398 assert dt.hour == native.hour
399 assert dt.minute == native.minute
400 assert dt.second == nano_add(native.second, nano_div(native.microsecond, 1000000))
401 assert dt.tzinfo == FixedOffset(0)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from datetime import timedelta
22 from unittest import TestCase
23
24 from neo4j.time import Duration
25
26
27 class DurationTestCase(TestCase):
28
29 def test_zero(self):
30 d = Duration()
31 self.assertEqual(d.months, 0)
32 self.assertEqual(d.days, 0)
33 self.assertEqual(d.seconds, 0)
34 self.assertEqual(d.subseconds, 0.0)
35 self.assertEqual(d.years_months_days, (0, 0, 0))
36 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0))
37 self.assertFalse(bool(d))
38
39 def test_years_only(self):
40 d = Duration(years=2)
41 self.assertEqual(d.months, 24)
42 self.assertEqual(d.days, 0)
43 self.assertEqual(d.seconds, 0)
44 self.assertEqual(d.subseconds, 0.0)
45 self.assertEqual(d.years_months_days, (2, 0, 0))
46 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0))
47
48 def test_months_only(self):
49 d = Duration(months=20)
50 self.assertEqual(d.months, 20)
51 self.assertEqual(d.days, 0)
52 self.assertEqual(d.seconds, 0)
53 self.assertEqual(d.subseconds, 0.0)
54 self.assertEqual(d.years_months_days, (1, 8, 0))
55 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0))
56
57 def test_months_out_of_range(self):
58 with self.assertRaises(ValueError):
59 _ = Duration(months=(2**64))
60
61 def test_weeks_only(self):
62 d = Duration(weeks=4)
63 self.assertEqual(d.months, 0)
64 self.assertEqual(d.days, 28)
65 self.assertEqual(d.seconds, 0)
66 self.assertEqual(d.subseconds, 0.0)
67 self.assertEqual(d.years_months_days, (0, 0, 28))
68 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0))
69
70 def test_days_only(self):
71 d = Duration(days=40)
72 self.assertEqual(d.months, 0)
73 self.assertEqual(d.days, 40)
74 self.assertEqual(d.seconds, 0)
75 self.assertEqual(d.subseconds, 0.0)
76 self.assertEqual(d.years_months_days, (0, 0, 40))
77 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.0))
78
79 def test_days_out_of_range(self):
80 with self.assertRaises(ValueError):
81 _ = Duration(days=(2**64))
82
83 def test_hours_only(self):
84 d = Duration(hours=10)
85 self.assertEqual(d.months, 0)
86 self.assertEqual(d.days, 0)
87 self.assertEqual(d.seconds, 36000)
88 self.assertEqual(d.subseconds, 0.0)
89 self.assertEqual(d.years_months_days, (0, 0, 0))
90 self.assertEqual(d.hours_minutes_seconds, (10, 0, 0.0))
91
92 def test_minutes_only(self):
93 d = Duration(minutes=90.5)
94 self.assertEqual(d.months, 0)
95 self.assertEqual(d.days, 0)
96 self.assertEqual(d.seconds, 5430)
97 self.assertEqual(d.subseconds, 0.0)
98 self.assertEqual(d.years_months_days, (0, 0, 0))
99 self.assertEqual(d.hours_minutes_seconds, (1, 30, 30.0))
100
101 def test_seconds_only(self):
102 d = Duration(seconds=123.456)
103 self.assertEqual(d.months, 0)
104 self.assertEqual(d.days, 0)
105 self.assertEqual(d.seconds, 123)
106 self.assertEqual(d.subseconds, 0.456)
107 self.assertEqual(d.years_months_days, (0, 0, 0))
108 self.assertEqual(d.hours_minutes_seconds, (0, 2, 3.456))
109
110 def test_seconds_out_of_range(self):
111 with self.assertRaises(ValueError):
112 _ = Duration(seconds=(2**64))
113
114 def test_subseconds_only(self):
115 d = Duration(subseconds=123.456)
116 self.assertEqual(d.months, 0)
117 self.assertEqual(d.days, 0)
118 self.assertEqual(d.seconds, 123)
119 self.assertEqual(d.subseconds, 0.456)
120 self.assertEqual(d.years_months_days, (0, 0, 0))
121 self.assertEqual(d.hours_minutes_seconds, (0, 2, 3.456))
122
123 def test_milliseconds_only(self):
124 d = Duration(milliseconds=1234.567)
125 self.assertEqual(d.months, 0)
126 self.assertEqual(d.days, 0)
127 self.assertEqual(d.seconds, 1)
128 self.assertEqual(d.subseconds, 0.234567)
129 self.assertEqual(d.years_months_days, (0, 0, 0))
130 self.assertEqual(d.hours_minutes_seconds, (0, 0, 1.234567))
131
132 def test_microseconds_only(self):
133 d = Duration(microseconds=1234.567)
134 self.assertEqual(d.months, 0)
135 self.assertEqual(d.days, 0)
136 self.assertEqual(d.seconds, 0)
137 self.assertEqual(d.subseconds, 0.001234567)
138 self.assertEqual(d.years_months_days, (0, 0, 0))
139 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.001234567))
140
141 def test_nanoseconds_only(self):
142 d = Duration(nanoseconds=1234.567)
143 self.assertEqual(d.months, 0)
144 self.assertEqual(d.days, 0)
145 self.assertEqual(d.seconds, 0)
146 self.assertEqual(d.subseconds, 0.000001234)
147 self.assertEqual(d.years_months_days, (0, 0, 0))
148 self.assertEqual(d.hours_minutes_seconds, (0, 0, 0.000001234))
149
150 def test_can_combine_years_months(self):
151 t = Duration(years=5, months=3)
152 self.assertEqual(t.months, 63)
153
154 def test_can_combine_weeks_and_days(self):
155 t = Duration(weeks=5, days=3)
156 self.assertEqual(t.days, 38)
157
158 def test_can_combine_hours_minutes_seconds(self):
159 t = Duration(hours=5, minutes=4, seconds=3)
160 self.assertEqual(t.seconds, 18243)
161
162 def test_can_combine_seconds_and_subseconds(self):
163 t = Duration(seconds=123.456, subseconds=0.321)
164 self.assertEqual(t.seconds, 123)
165 self.assertEqual(t.subseconds, 0.777)
166
167 def test_full_positive(self):
168 d = Duration(years=1, months=2, days=3, hours=4, minutes=5, seconds=6.789)
169 self.assertEqual(d.months, 14)
170 self.assertEqual(d.days, 3)
171 self.assertEqual(d.seconds, 14706)
172 self.assertEqual(d.subseconds, 0.789)
173 self.assertEqual(d.years_months_days, (1, 2, 3))
174 self.assertEqual(d.hours_minutes_seconds, (4, 5, 6.789))
175 self.assertTrue(bool(d))
176
177 def test_full_negative(self):
178 d = Duration(years=-1, months=-2, days=-3, hours=-4, minutes=-5, seconds=-6.789)
179 self.assertEqual(d.months, -14)
180 self.assertEqual(d.days, -3)
181 self.assertEqual(d.seconds, -14706)
182 self.assertEqual(d.subseconds, -0.789)
183 self.assertEqual(d.years_months_days, (-1, -2, -3))
184 self.assertEqual(d.hours_minutes_seconds, (-4, -5, -6.789))
185 self.assertTrue(bool(d))
186
187 def test_negative_positive_negative(self):
188 d = Duration(years=-1, months=-2, days=3, hours=-4, minutes=-5, seconds=-6.789)
189 self.assertEqual(d.months, -14)
190 self.assertEqual(d.days, 3)
191 self.assertEqual(d.seconds, -14706)
192 self.assertEqual(d.subseconds, -0.789)
193 self.assertEqual(d.years_months_days, (-1, -2, 3))
194 self.assertEqual(d.hours_minutes_seconds, (-4, -5, -6.789))
195
196 def test_positive_negative_positive(self):
197 d = Duration(years=1, months=2, days=-3, hours=4, minutes=5, seconds=6.789)
198 self.assertEqual(d.months, 14)
199 self.assertEqual(d.days, -3)
200 self.assertEqual(d.seconds, 14706)
201 self.assertEqual(d.subseconds, 0.789)
202 self.assertEqual(d.years_months_days, (1, 2, -3))
203 self.assertEqual(d.hours_minutes_seconds, (4, 5, 6.789))
204
205 def test_add_duration(self):
206 d1 = Duration(months=2, days=3, seconds=5.7)
207 d2 = Duration(months=7, days=5, seconds=3.2)
208 self.assertEqual(d1 + d2, Duration(months=9, days=8, seconds=8.9))
209
210 def test_add_timedelta(self):
211 d1 = Duration(months=2, days=3, seconds=5.7)
212 td = timedelta(days=5, seconds=3.2)
213 self.assertEqual(d1 + td, Duration(months=2, days=8, seconds=8.9))
214
215 def test_add_object(self):
216 with self.assertRaises(TypeError):
217 _ = Duration(months=2, days=3, seconds=5.7) + object()
218
219 def test_subtract_duration(self):
220 d1 = Duration(months=2, days=3, seconds=5.7)
221 d2 = Duration(months=7, days=5, seconds=3.2)
222 self.assertEqual(d1 - d2, Duration(months=-5, days=-2, seconds=2.5))
223
224 def test_subtract_timedelta(self):
225 d1 = Duration(months=2, days=3, seconds=5.7)
226 td = timedelta(days=5, seconds=3.2)
227 self.assertEqual(d1 - td, Duration(months=2, days=-2, seconds=2.5))
228
229 def test_subtract_object(self):
230 with self.assertRaises(TypeError):
231 _ = Duration(months=2, days=3, seconds=5.7) - object()
232
233 def test_multiplication_by_int(self):
234 d1 = Duration(months=2, days=3, seconds=5.7)
235 i = 11
236 self.assertEqual(d1 * i, Duration(months=22, days=33, seconds=62.7))
237
238 def test_multiplication_by_float(self):
239 d1 = Duration(months=2, days=3, seconds=5.7)
240 f = 5.5
241 self.assertEqual(d1 * f, Duration(months=11, days=16, seconds=31.35))
242
243 def test_multiplication_by_object(self):
244 with self.assertRaises(TypeError):
245 _ = Duration(months=2, days=3, seconds=5.7) * object()
246
247 def test_floor_division_by_int(self):
248 d1 = Duration(months=11, days=33, seconds=55.77)
249 i = 2
250 self.assertEqual(d1 // i, Duration(months=5, days=16, seconds=27.0))
251
252 def test_floor_division_by_object(self):
253 with self.assertRaises(TypeError):
254 _ = Duration(months=2, days=3, seconds=5.7) // object()
255
256 def test_modulus_by_int(self):
257 d1 = Duration(months=11, days=33, seconds=55.77)
258 i = 2
259 self.assertEqual(d1 % i, Duration(months=1, days=1, seconds=1.77))
260
261 def test_modulus_by_object(self):
262 with self.assertRaises(TypeError):
263 _ = Duration(months=2, days=3, seconds=5.7) % object()
264
265 def test_floor_division_and_modulus_by_int(self):
266 d1 = Duration(months=11, days=33, seconds=55.77)
267 i = 2
268 self.assertEqual(divmod(d1, i), (Duration(months=5, days=16, seconds=27.0),
269 Duration(months=1, days=1, seconds=1.77)))
270
271 def test_floor_division_and_modulus_by_object(self):
272 with self.assertRaises(TypeError):
273 _ = divmod(Duration(months=2, days=3, seconds=5.7), object())
274
275 def test_true_division_by_int(self):
276 d1 = Duration(months=11, days=33, seconds=55.77)
277 i = 2
278 self.assertEqual(d1 / i, Duration(months=6, days=16, seconds=27.885))
279
280 def test_true_division_by_float(self):
281 d1 = Duration(months=11, days=33, seconds=55.77)
282 f = 2.5
283 self.assertEqual(d1 / f, Duration(months=4, days=13, seconds=22.308))
284
285 def test_true_division_by_object(self):
286 with self.assertRaises(TypeError):
287 _ = Duration(months=2, days=3, seconds=5.7) / object()
288
289 def test_unary_plus(self):
290 d = Duration(months=11, days=33, seconds=55.77)
291 self.assertEqual(+d, Duration(months=11, days=33, seconds=55.77))
292
293 def test_unary_minus(self):
294 d = Duration(months=11, days=33, seconds=55.77)
295 self.assertEqual(-d, Duration(months=-11, days=-33, seconds=-55.77))
296
297 def test_absolute(self):
298 d = Duration(months=-11, days=-33, seconds=-55.77)
299 self.assertEqual(abs(d), Duration(months=11, days=33, seconds=55.77))
300
301 def test_str(self):
302 self.assertEqual(str(Duration()), "PT0S")
303 self.assertEqual(str(Duration(years=1, months=2)), "P1Y2M")
304 self.assertEqual(str(Duration(years=-1, months=2)), "P-10M")
305 self.assertEqual(str(Duration(months=-13)), "P-1Y-1M")
306 self.assertEqual(str(Duration(months=2, days=3, seconds=5.7)), "P2M3DT5.7S")
307 self.assertEqual(str(Duration(hours=12, minutes=34)), "PT12H34M")
308 self.assertEqual(str(Duration(seconds=59)), "PT59S")
309 self.assertEqual(str(Duration(seconds=0.123456789)), "PT0.123456789S")
310 self.assertEqual(str(Duration(seconds=-0.123456789)), "PT-0.123456789S")
311
312 def test_repr(self):
313 d = Duration(months=2, days=3, seconds=5.7)
314 self.assertEqual(repr(d), "Duration(months=2, days=3, seconds=5, subseconds=0.7)")
315
316 def test_iso_format(self):
317 self.assertEqual(Duration().iso_format(), "PT0S")
318 self.assertEqual(Duration(years=1, months=2).iso_format(), "P1Y2M")
319 self.assertEqual(Duration(years=-1, months=2).iso_format(), "P-10M")
320 self.assertEqual(Duration(months=-13).iso_format(), "P-1Y-1M")
321 self.assertEqual(Duration(months=2, days=3, seconds=5.7).iso_format(), "P2M3DT5.7S")
322 self.assertEqual(Duration(hours=12, minutes=34).iso_format(), "PT12H34M")
323 self.assertEqual(Duration(seconds=59).iso_format(), "PT59S")
324 self.assertEqual(Duration(seconds=0.123456789).iso_format(), "PT0.123456789S")
325 self.assertEqual(Duration(seconds=-0.123456789).iso_format(), "PT-0.123456789S")
326
327 def test_from_iso_format(self):
328 self.assertEqual(Duration(), Duration.from_iso_format("PT0S"))
329 self.assertEqual(Duration(hours=12, minutes=34, seconds=56.789),
330 Duration.from_iso_format("PT12H34M56.789S"))
331 self.assertEqual(Duration(years=1, months=2, days=3),
332 Duration.from_iso_format("P1Y2M3D"))
333 self.assertEqual(Duration(years=1, months=2, days=3, hours=12, minutes=34, seconds=56.789),
334 Duration.from_iso_format("P1Y2M3DT12H34M56.789S"))
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from unittest import TestCase
22
23 from neo4j.data import DataHydrator
24 from neo4j.packstream import Structure
25
26
27 class TemporalHydrationTestCase(TestCase):
28
29 def setUp(self):
30 self.hydrant = DataHydrator()
31
32 def test_can_hydrate_date_time_structure(self):
33 struct = Structure(b'd', 1539344261, 474716862)
34 dt, = self.hydrant.hydrate([struct])
35 self.assertEqual(dt.year, 2018)
36 self.assertEqual(dt.month, 10)
37 self.assertEqual(dt.day, 12)
38 self.assertEqual(dt.hour, 11)
39 self.assertEqual(dt.minute, 37)
40 self.assertEqual(dt.second, 41.474716862)
0 #!/usr/bin/env python
1 # coding: utf-8
2
3 # Copyright (c) 2002-2020 "Neo4j,"
4 # Neo4j Sweden AB [http://neo4j.com]
5 #
6 # This file is part of Neo4j.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19
20
21 from datetime import time
22 from unittest import TestCase
23
24 from pytz import (
25 timezone,
26 FixedOffset,
27 )
28
29 from neo4j.time import Time
30 from neo4j.time.arithmetic import (
31 nano_add,
32 nano_div,
33 )
34
35
36 timezone_us_eastern = timezone("US/Eastern")
37 timezone_utc = timezone("UTC")
38
39
40 class TimeTestCase(TestCase):
41
42 def test_bad_attribute(self):
43 t = Time(12, 34, 56.789)
44 with self.assertRaises(AttributeError):
45 _ = t.x
46
47 def test_simple_time(self):
48 t = Time(12, 34, 56.789)
49 self.assertEqual(t.hour_minute_second, (12, 34, 56.789))
50 self.assertEqual(t.ticks, 45296.789)
51 self.assertEqual(t.hour, 12)
52 self.assertEqual(t.minute, 34)
53 self.assertEqual(t.second, 56.789)
54
55 def test_midnight(self):
56 t = Time(0, 0, 0)
57 self.assertEqual(t.hour_minute_second, (0, 0, 0))
58 self.assertEqual(t.ticks, 0)
59 self.assertEqual(t.hour, 0)
60 self.assertEqual(t.minute, 0)
61 self.assertEqual(t.second, 0)
62
63 def test_nanosecond_precision(self):
64 t = Time(12, 34, 56.789123456)
65 self.assertEqual(t.hour_minute_second, (12, 34, 56.789123456))
66 self.assertEqual(t.ticks, 45296.789123456)
67 self.assertEqual(t.hour, 12)
68 self.assertEqual(t.minute, 34)
69 self.assertEqual(t.second, 56.789123456)
70
71 def test_str(self):
72 t = Time(12, 34, 56.789123456)
73 self.assertEqual(str(t), "12:34:56.789123456")
74
75 def test_now_without_tz(self):
76 t = Time.now()
77 self.assertIsInstance(t, Time)
78
79 def test_now_with_tz(self):
80 t = Time.now(tz=timezone_us_eastern)
81 self.assertIsInstance(t, Time)
82 self.assertEqual(t.tzinfo, timezone_us_eastern)
83
84 def test_utc_now(self):
85 t = Time.utc_now()
86 self.assertIsInstance(t, Time)
87
88 def test_from_native(self):
89 native = time(12, 34, 56, 789123)
90 t = Time.from_native(native)
91 self.assertEqual(t.hour, native.hour)
92 self.assertEqual(t.minute, native.minute)
93 self.assertEqual(t.second, nano_add(native.second, nano_div(native.microsecond, 1000000)))
94
95 def test_to_native(self):
96 t = Time(12, 34, 56.789123456)
97 native = t.to_native()
98 self.assertEqual(t.hour, native.hour)
99 self.assertEqual(t.minute, native.minute)
100 self.assertEqual(56.789123, nano_add(native.second, nano_div(native.microsecond, 1000000)))
101
102 def test_iso_format(self):
103 t = Time(12, 34, 56.789123456)
104 self.assertEqual("12:34:56.789123456", t.iso_format())
105
106 def test_iso_format_with_trailing_zeroes(self):
107 t = Time(12, 34, 56.789)
108 self.assertEqual("12:34:56.789000000", t.iso_format())
109
110 def test_from_iso_format_hour_only(self):
111 expected = Time(12, 0, 0)
112 actual = Time.from_iso_format("12")
113 self.assertEqual(expected, actual)
114
115 def test_from_iso_format_hour_and_minute(self):
116 expected = Time(12, 34, 0)
117 actual = Time.from_iso_format("12:34")
118 self.assertEqual(expected, actual)
119
120 def test_from_iso_format_hour_minute_second(self):
121 expected = Time(12, 34, 56)
122 actual = Time.from_iso_format("12:34:56")
123 self.assertEqual(expected, actual)
124
125 def test_from_iso_format_hour_minute_second_milliseconds(self):
126 expected = Time(12, 34, 56.123)
127 actual = Time.from_iso_format("12:34:56.123")
128 self.assertEqual(expected, actual)
129
130 def test_from_iso_format_hour_minute_second_microseconds(self):
131 expected = Time(12, 34, 56.123456)
132 actual = Time.from_iso_format("12:34:56.123456")
133 self.assertEqual(expected, actual)
134
135 def test_from_iso_format_hour_minute_second_nanoseconds(self):
136 expected = Time(12, 34, 56.123456789)
137 actual = Time.from_iso_format("12:34:56.123456789")
138 self.assertEqual(expected, actual)
139
140 def test_from_iso_format_with_positive_tz(self):
141 expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(754))
142 actual = Time.from_iso_format("12:34:56.123456789+12:34")
143 self.assertEqual(expected, actual)
144
145 def test_from_iso_format_with_negative_tz(self):
146 expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(-754))
147 actual = Time.from_iso_format("12:34:56.123456789-12:34")
148 self.assertEqual(expected, actual)
149
150 def test_from_iso_format_with_positive_long_tz(self):
151 expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(754))
152 actual = Time.from_iso_format("12:34:56.123456789+12:34:56.123456")
153 self.assertEqual(expected, actual)
154
155 def test_from_iso_format_with_negative_long_tz(self):
156 expected = Time(12, 34, 56.123456789, tzinfo=FixedOffset(-754))
157 actual = Time.from_iso_format("12:34:56.123456789-12:34:56.123456")
158 self.assertEqual(expected, actual)
159
160
161 def test_iso_format_with_time_zone_case_1():
162 # python -m pytest tests/unit/time/test_time.py -s -v -k test_iso_format_with_time_zone_case_1
163 expected = Time(7, 54, 2.129790999, tzinfo=timezone_utc)
164 assert expected.iso_format() == "07:54:02.129790999+00:00"
165 assert expected.tzinfo == FixedOffset(0)
166 actual = Time.from_iso_format("07:54:02.129790999+00:00")
167 assert expected == actual
168
169
170 def test_iso_format_with_time_zone_case_2():
171 # python -m pytest tests/unit/time/test_time.py -s -v -k test_iso_format_with_time_zone_case_2
172 expected = Time.from_iso_format("07:54:02.129790999+01:00")
173 assert expected.tzinfo == FixedOffset(60)
174 assert expected.iso_format() == "07:54:02.129790999+01:00"
175
176
177 def test_to_native_case_1():
178 # python -m pytest tests/unit/time/test_time.py -s -v -k test_to_native_case_1
179 t = Time(12, 34, 56.789123456)
180 native = t.to_native()
181 assert native.hour == t.hour
182 assert native.minute == t.minute
183 assert nano_add(native.second, nano_div(native.microsecond, 1000000)) == 56.789123
184 assert native.tzinfo is None
185 assert native.isoformat() == "12:34:56.789123"
186
187
188 def test_to_native_case_2():
189 # python -m pytest tests/unit/time/test_time.py -s -v -k test_to_native_case_2
190 t = Time(12, 34, 56.789123456, tzinfo=timezone_utc)
191 native = t.to_native()
192 assert native.hour == t.hour
193 assert native.minute == t.minute
194 assert nano_add(native.second, nano_div(native.microsecond, 1000000)) == 56.789123
195 assert native.tzinfo == FixedOffset(0)
196 assert native.isoformat() == "12:34:56.789123+00:00"
197
198
199 def test_from_native_case_1():
200 # python -m pytest tests/unit/time/test_time.py -s -v -k test_from_native_case_1
201 native = time(12, 34, 56, 789123)
202 t = Time.from_native(native)
203 assert t.hour == native.hour
204 assert t.minute == native.minute
205 assert t.second == nano_add(native.second, nano_div(native.microsecond, 1000000))
206 assert t.tzinfo is None
207
208
209 def test_from_native_case_2():
210 # python -m pytest tests/unit/time/test_time.py -s -v -k test_from_native_case_2
211 native = time(12, 34, 56, 789123, FixedOffset(0))
212 t = Time.from_native(native)
213 assert t.hour == native.hour
214 assert t.minute == native.minute
215 assert t.second == nano_add(native.second, nano_div(native.microsecond, 1000000))
216 assert t.tzinfo == FixedOffset(0)
00 [tox]
11 envlist =
2 ; py27
3 py34
42 py35
53 py36
4 py37
5 py38
66
77 [testenv]
88 passenv =
99 NEO4J_SERVER_PACKAGE
10 NEO4J_RELEASES
1011 NEO4J_USER
1112 NEO4J_PASSWORD
12 NEOCTRL_ARGS
13 TEAMCITY_VERSION
1314 TEAMCITY_HOST
1415 TEAMCITY_USER
1516 TEAMCITY_PASSWORD
1617 JAVA_HOME
18 AWS_ACCESS_KEY_ID
19 AWS_SECRET_ACCESS_KEY
20 deps =
21 -r tests/requirements.txt
1722 commands =
18 python setup.py develop
19 pip install --upgrade -r {toxinidir}/test/requirements.txt
2023 coverage erase
21 coverage run -m pytest -v {posargs} test/unit test/stub test/integration #test/performance
24 coverage run -m pytest -v {posargs} \
25 tests/unit \
26 tests/stub \
27 tests/integration
2228 coverage report