nomosdb 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. nomosdb-0.16.0/LICENSE +17 -0
  2. nomosdb-0.16.0/PKG-INFO +179 -0
  3. nomosdb-0.16.0/README.md +137 -0
  4. nomosdb-0.16.0/nomosdb/__init__.py +59 -0
  5. nomosdb-0.16.0/nomosdb/_async/__init__.py +0 -0
  6. nomosdb-0.16.0/nomosdb/_async/connection.py +126 -0
  7. nomosdb-0.16.0/nomosdb/_async/driver.py +75 -0
  8. nomosdb-0.16.0/nomosdb/_async/pool.py +228 -0
  9. nomosdb-0.16.0/nomosdb/_async/work.py +504 -0
  10. nomosdb-0.16.0/nomosdb/_bolt.py +267 -0
  11. nomosdb-0.16.0/nomosdb/_common.py +235 -0
  12. nomosdb-0.16.0/nomosdb/_io.py +155 -0
  13. nomosdb-0.16.0/nomosdb/_packstream.py +237 -0
  14. nomosdb-0.16.0/nomosdb/_sync/__init__.py +1 -0
  15. nomosdb-0.16.0/nomosdb/_sync/connection.py +127 -0
  16. nomosdb-0.16.0/nomosdb/_sync/driver.py +76 -0
  17. nomosdb-0.16.0/nomosdb/_sync/pool.py +229 -0
  18. nomosdb-0.16.0/nomosdb/_sync/work.py +505 -0
  19. nomosdb-0.16.0/nomosdb/_version.py +3 -0
  20. nomosdb-0.16.0/nomosdb/async_client.py +230 -0
  21. nomosdb-0.16.0/nomosdb/client.py +266 -0
  22. nomosdb-0.16.0/nomosdb/connection.py +237 -0
  23. nomosdb-0.16.0/nomosdb/exceptions.py +134 -0
  24. nomosdb-0.16.0/nomosdb/graph.py +110 -0
  25. nomosdb-0.16.0/nomosdb/orm.py +238 -0
  26. nomosdb-0.16.0/nomosdb/py.typed +0 -0
  27. nomosdb-0.16.0/nomosdb/query.py +30 -0
  28. nomosdb-0.16.0/nomosdb/transaction.py +44 -0
  29. nomosdb-0.16.0/nomosdb/types.py +17 -0
  30. nomosdb-0.16.0/nomosdb/utils.py +30 -0
  31. nomosdb-0.16.0/nomosdb.egg-info/PKG-INFO +179 -0
  32. nomosdb-0.16.0/nomosdb.egg-info/SOURCES.txt +41 -0
  33. nomosdb-0.16.0/nomosdb.egg-info/dependency_links.txt +1 -0
  34. nomosdb-0.16.0/nomosdb.egg-info/requires.txt +14 -0
  35. nomosdb-0.16.0/nomosdb.egg-info/top_level.txt +1 -0
  36. nomosdb-0.16.0/pyproject.toml +56 -0
  37. nomosdb-0.16.0/setup.cfg +4 -0
  38. nomosdb-0.16.0/tests/test_async.py +68 -0
  39. nomosdb-0.16.0/tests/test_bolt.py +102 -0
  40. nomosdb-0.16.0/tests/test_client.py +58 -0
  41. nomosdb-0.16.0/tests/test_connection.py +71 -0
  42. nomosdb-0.16.0/tests/test_orm.py +50 -0
  43. nomosdb-0.16.0/tests/test_packstream.py +111 -0
nomosdb-0.16.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2024 DingBo and NomosDB Contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.4
2
+ Name: nomosdb
3
+ Version: 0.16.0
4
+ Summary: Python driver for NomosDB, the graph database: Bolt, transactions, cluster routing, asyncio
5
+ Author: DingBo
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/cndingbo2030/nomosdb
8
+ Project-URL: Documentation, https://github.com/cndingbo2030/nomosdb/blob/main/docs/sdk/python.md
9
+ Project-URL: Source, https://github.com/cndingbo2030/nomosdb/tree/main/nomosdb-python
10
+ Project-URL: Changelog, https://github.com/cndingbo2030/nomosdb/blob/main/CHANGELOG.md
11
+ Project-URL: Issues, https://github.com/cndingbo2030/nomosdb/issues
12
+ Keywords: graph database,nomosdb,bolt,cypher,neo4j
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Framework :: AsyncIO
24
+ Classifier: Topic :: Database
25
+ Classifier: Topic :: Database :: Front-Ends
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: async
31
+ Requires-Dist: aiohttp>=3.8.0; extra == "async"
32
+ Provides-Extra: pandas
33
+ Requires-Dist: pandas>=1.3.0; extra == "pandas"
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=7.0; extra == "dev"
36
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
37
+ Requires-Dist: aiohttp>=3.8.0; extra == "dev"
38
+ Requires-Dist: neo4j>=5; extra == "dev"
39
+ Requires-Dist: build; extra == "dev"
40
+ Requires-Dist: twine; extra == "dev"
41
+ Dynamic: license-file
42
+
43
+ # nomosdb — the Python driver for NomosDB
44
+
45
+ The official Python driver for [NomosDB](https://github.com/cndingbo2030/nomosdb), a graph database you query in Cypher (NomosDB calls its dialect DQL).
46
+
47
+ - Native **Bolt 5.0–5.4**, the protocol the Neo4j drivers use.
48
+ - **Transactions** that commit or roll back as a whole.
49
+ - **Managed transactions** that are retried on conflicts and leader changes.
50
+ - **Cluster routing** with causal consistency.
51
+ - **asyncio**.
52
+ - Pure Python, no dependencies, Python 3.9+.
53
+
54
+ ```bash
55
+ pip install nomosdb
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```python
61
+ import nomosdb
62
+
63
+ driver = nomosdb.driver("bolt://localhost:7687", auth=("nomosdb", "secret"))
64
+
65
+ with driver.session() as session:
66
+ # one statement, committed on its own; records stream in batches
67
+ for record in session.run("MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name", min=30):
68
+ print(record["name"])
69
+
70
+ # a managed transaction: on a transient failure the whole function runs again
71
+ def transfer(tx, a, b, amount):
72
+ tx.run("MATCH (x:Account {id: $a}) SET x.balance = x.balance - $n", a=a, n=amount)
73
+ tx.run("MATCH (x:Account {id: $b}) SET x.balance = x.balance + $n", b=b, n=amount)
74
+
75
+ session.execute_write(transfer, 1, 2, 10)
76
+
77
+ # an explicit transaction
78
+ with session.begin_transaction() as tx:
79
+ tx.run("CREATE (:Person {name: $name})", name="Ann")
80
+ tx.commit() # leaving the block without commit() rolls back
81
+
82
+ # bulk loading: one retried transaction per batch, the batch as $rows
83
+ session.write_batch("UNWIND $rows AS r CREATE (:Person {name: r.name})",
84
+ ({"name": f"p{i}"} for i in range(100_000)), batch_size=5000)
85
+
86
+ driver.close()
87
+ ```
88
+
89
+ ### asyncio
90
+
91
+ ```python
92
+ import asyncio
93
+ import nomosdb
94
+
95
+ async def main():
96
+ async with nomosdb.async_driver("bolt://localhost:7687", auth=("nomosdb", "secret")) as driver:
97
+ async with driver.session() as session:
98
+ result = await session.run("MATCH (p:Person) RETURN p.name AS name")
99
+ async for record in result:
100
+ print(record["name"])
101
+
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ Every method of the blocking API exists in the async one under the same name. The async version adds `await`, `async with` and `async for`.
106
+
107
+ ## URIs
108
+
109
+ | URI | Meaning |
110
+ |-----|---------|
111
+ | `bolt://host:7687` | One server. |
112
+ | `bolt+s://host:7687` | One server over TLS, with the certificate verified (system CAs, or `trusted_certificates="ca.pem"`). |
113
+ | `bolt+ssc://host:7687` | One server over TLS, accepting a self-signed certificate. |
114
+ | `nomosdb://host:7687` | A cluster. `neo4j://` is also accepted. |
115
+ | `nomosdb+s://`, `nomosdb+ssc://` | A cluster over TLS. |
116
+
117
+ With a cluster URI, the driver asks any member for the routing table. Writes go to the leader and reads are spread over the members. Each session passes its bookmark to the next transaction, so a read sees the session's earlier writes on whichever member serves it.
118
+
119
+ ## Results
120
+
121
+ `session.run()` and `tx.run()` return a `Result`. You can iterate it, or call one of these methods:
122
+
123
+ - `single()`: the one record, or `None`.
124
+ - `data()`: a list of dicts.
125
+ - `value()`: the first column.
126
+ - `to_list()`: all records.
127
+ - `peek()`: the next record without consuming it.
128
+ - `consume()`: skips the remaining records and returns the `ResultSummary` (`counters.nodes_created` and the other counters, `query_type`, `bookmark`).
129
+
130
+ A `Record` reads by name or by position: `record["name"]`, `record[0]`, `record.data()`.
131
+
132
+ Nodes, relationships and paths come back as `nomosdb.graph.Node`, `Relationship` and `Path`. You read their properties like a dict, and they also have `labels`, `type`, `start_node`, `end_node` and `element_id`.
133
+
134
+ ## Errors
135
+
136
+ | Exception | When |
137
+ |-----------|------|
138
+ | `ClientError` | The statement is wrong: syntax, a missing parameter, a constraint. `code` holds the server's `Neo.ClientError.*` code. |
139
+ | `TransientError` | A write conflict or a lock timeout. Managed transactions retry it. |
140
+ | `AuthError` | Wrong credentials, or a role that is too low. |
141
+ | `ServiceUnavailable`, `SessionExpired` | No server is reachable, or a connection broke. Managed transactions retry these. |
142
+ | `TransactionCommitUnknown` | The connection broke after COMMIT was sent, so the transaction may have committed. It is never retried automatically. |
143
+
144
+ ## Driver options
145
+
146
+ `nomosdb.driver(uri, auth, **options)` accepts these options:
147
+
148
+ | Option | Default | Meaning |
149
+ |--------|---------|---------|
150
+ | `max_connection_pool_size` | 100 | Most connections kept per server. |
151
+ | `connection_timeout` | 30 s | Limit on TCP connect, TLS and Bolt handshake. |
152
+ | `connection_acquisition_timeout` | 60 s | How long to wait for a free pooled connection. |
153
+ | `max_transaction_retry_time` | 30 s | How long managed transactions keep retrying. |
154
+ | `fetch_size` | 1000 | Records per batch. |
155
+ | `trusted_certificates` | system CAs | CA file for `+s` URIs. |
156
+ | `user_agent` | `nomosdb-python/<version>` | Sent to the server. |
157
+
158
+ ## The earlier client
159
+
160
+ The 0.1 API still works:
161
+
162
+ - `NomosDB(uri).query()` returns rows as dicts.
163
+ - The ORM (`@node_class`, `Model.objects.filter(...)`) works as before.
164
+
165
+ With a `bolt://` URI these use the driver above, so `db.session().begin_transaction()` is a real transaction.
166
+
167
+ With `http://`, each statement commits on its own, and `begin_transaction()` raises `TransactionError`. It no longer returns an object that only looked like a transaction.
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ cd nomosdb-python
173
+ python tools/unasync.py # after editing nomosdb/_async/: regenerates nomosdb/_sync/
174
+ NOMOSDB_SERVER=../build/tools/nomosdb-server NOMOSDB_CERTS=../build/tools/nomosdb-certs python -m pytest tests
175
+ ```
176
+
177
+ The tests start real `nomosdb-server` processes: a single server, a TLS server and a three-member cluster.
178
+
179
+ License: Apache-2.0.
@@ -0,0 +1,137 @@
1
+ # nomosdb — the Python driver for NomosDB
2
+
3
+ The official Python driver for [NomosDB](https://github.com/cndingbo2030/nomosdb), a graph database you query in Cypher (NomosDB calls its dialect DQL).
4
+
5
+ - Native **Bolt 5.0–5.4**, the protocol the Neo4j drivers use.
6
+ - **Transactions** that commit or roll back as a whole.
7
+ - **Managed transactions** that are retried on conflicts and leader changes.
8
+ - **Cluster routing** with causal consistency.
9
+ - **asyncio**.
10
+ - Pure Python, no dependencies, Python 3.9+.
11
+
12
+ ```bash
13
+ pip install nomosdb
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```python
19
+ import nomosdb
20
+
21
+ driver = nomosdb.driver("bolt://localhost:7687", auth=("nomosdb", "secret"))
22
+
23
+ with driver.session() as session:
24
+ # one statement, committed on its own; records stream in batches
25
+ for record in session.run("MATCH (p:Person) WHERE p.age > $min RETURN p.name AS name", min=30):
26
+ print(record["name"])
27
+
28
+ # a managed transaction: on a transient failure the whole function runs again
29
+ def transfer(tx, a, b, amount):
30
+ tx.run("MATCH (x:Account {id: $a}) SET x.balance = x.balance - $n", a=a, n=amount)
31
+ tx.run("MATCH (x:Account {id: $b}) SET x.balance = x.balance + $n", b=b, n=amount)
32
+
33
+ session.execute_write(transfer, 1, 2, 10)
34
+
35
+ # an explicit transaction
36
+ with session.begin_transaction() as tx:
37
+ tx.run("CREATE (:Person {name: $name})", name="Ann")
38
+ tx.commit() # leaving the block without commit() rolls back
39
+
40
+ # bulk loading: one retried transaction per batch, the batch as $rows
41
+ session.write_batch("UNWIND $rows AS r CREATE (:Person {name: r.name})",
42
+ ({"name": f"p{i}"} for i in range(100_000)), batch_size=5000)
43
+
44
+ driver.close()
45
+ ```
46
+
47
+ ### asyncio
48
+
49
+ ```python
50
+ import asyncio
51
+ import nomosdb
52
+
53
+ async def main():
54
+ async with nomosdb.async_driver("bolt://localhost:7687", auth=("nomosdb", "secret")) as driver:
55
+ async with driver.session() as session:
56
+ result = await session.run("MATCH (p:Person) RETURN p.name AS name")
57
+ async for record in result:
58
+ print(record["name"])
59
+
60
+ asyncio.run(main())
61
+ ```
62
+
63
+ Every method of the blocking API exists in the async one under the same name. The async version adds `await`, `async with` and `async for`.
64
+
65
+ ## URIs
66
+
67
+ | URI | Meaning |
68
+ |-----|---------|
69
+ | `bolt://host:7687` | One server. |
70
+ | `bolt+s://host:7687` | One server over TLS, with the certificate verified (system CAs, or `trusted_certificates="ca.pem"`). |
71
+ | `bolt+ssc://host:7687` | One server over TLS, accepting a self-signed certificate. |
72
+ | `nomosdb://host:7687` | A cluster. `neo4j://` is also accepted. |
73
+ | `nomosdb+s://`, `nomosdb+ssc://` | A cluster over TLS. |
74
+
75
+ With a cluster URI, the driver asks any member for the routing table. Writes go to the leader and reads are spread over the members. Each session passes its bookmark to the next transaction, so a read sees the session's earlier writes on whichever member serves it.
76
+
77
+ ## Results
78
+
79
+ `session.run()` and `tx.run()` return a `Result`. You can iterate it, or call one of these methods:
80
+
81
+ - `single()`: the one record, or `None`.
82
+ - `data()`: a list of dicts.
83
+ - `value()`: the first column.
84
+ - `to_list()`: all records.
85
+ - `peek()`: the next record without consuming it.
86
+ - `consume()`: skips the remaining records and returns the `ResultSummary` (`counters.nodes_created` and the other counters, `query_type`, `bookmark`).
87
+
88
+ A `Record` reads by name or by position: `record["name"]`, `record[0]`, `record.data()`.
89
+
90
+ Nodes, relationships and paths come back as `nomosdb.graph.Node`, `Relationship` and `Path`. You read their properties like a dict, and they also have `labels`, `type`, `start_node`, `end_node` and `element_id`.
91
+
92
+ ## Errors
93
+
94
+ | Exception | When |
95
+ |-----------|------|
96
+ | `ClientError` | The statement is wrong: syntax, a missing parameter, a constraint. `code` holds the server's `Neo.ClientError.*` code. |
97
+ | `TransientError` | A write conflict or a lock timeout. Managed transactions retry it. |
98
+ | `AuthError` | Wrong credentials, or a role that is too low. |
99
+ | `ServiceUnavailable`, `SessionExpired` | No server is reachable, or a connection broke. Managed transactions retry these. |
100
+ | `TransactionCommitUnknown` | The connection broke after COMMIT was sent, so the transaction may have committed. It is never retried automatically. |
101
+
102
+ ## Driver options
103
+
104
+ `nomosdb.driver(uri, auth, **options)` accepts these options:
105
+
106
+ | Option | Default | Meaning |
107
+ |--------|---------|---------|
108
+ | `max_connection_pool_size` | 100 | Most connections kept per server. |
109
+ | `connection_timeout` | 30 s | Limit on TCP connect, TLS and Bolt handshake. |
110
+ | `connection_acquisition_timeout` | 60 s | How long to wait for a free pooled connection. |
111
+ | `max_transaction_retry_time` | 30 s | How long managed transactions keep retrying. |
112
+ | `fetch_size` | 1000 | Records per batch. |
113
+ | `trusted_certificates` | system CAs | CA file for `+s` URIs. |
114
+ | `user_agent` | `nomosdb-python/<version>` | Sent to the server. |
115
+
116
+ ## The earlier client
117
+
118
+ The 0.1 API still works:
119
+
120
+ - `NomosDB(uri).query()` returns rows as dicts.
121
+ - The ORM (`@node_class`, `Model.objects.filter(...)`) works as before.
122
+
123
+ With a `bolt://` URI these use the driver above, so `db.session().begin_transaction()` is a real transaction.
124
+
125
+ With `http://`, each statement commits on its own, and `begin_transaction()` raises `TransactionError`. It no longer returns an object that only looked like a transaction.
126
+
127
+ ## Development
128
+
129
+ ```bash
130
+ cd nomosdb-python
131
+ python tools/unasync.py # after editing nomosdb/_async/: regenerates nomosdb/_sync/
132
+ NOMOSDB_SERVER=../build/tools/nomosdb-server NOMOSDB_CERTS=../build/tools/nomosdb-certs python -m pytest tests
133
+ ```
134
+
135
+ The tests start real `nomosdb-server` processes: a single server, a TLS server and a three-member cluster.
136
+
137
+ License: Apache-2.0.
@@ -0,0 +1,59 @@
1
+ """
2
+ NomosDB Python driver.
3
+
4
+ Bolt (recommended): real transactions, retries, cluster routing, asyncio.
5
+
6
+ >>> import nomosdb
7
+ >>> drv = nomosdb.driver("bolt://localhost:7687", auth=("nomosdb", "secret"))
8
+ >>> with drv.session() as s:
9
+ ... for rec in s.run("MATCH (p:Person) RETURN p.name AS name LIMIT 10"):
10
+ ... print(rec["name"])
11
+ >>> drv.close()
12
+
13
+ The earlier client stays available: NomosDB("bolt://...") or NomosDB("http://...")
14
+ with query() returning dict rows, and the ORM.
15
+ """
16
+
17
+ from ._async.driver import AsyncDriver, async_driver
18
+ from ._async.work import AsyncManagedTransaction, AsyncResult, AsyncSession, AsyncTransaction
19
+ from ._common import READ_ACCESS, WRITE_ACCESS, Record, ResultSummary, SummaryCounters
20
+ from ._sync.driver import Driver, driver
21
+ from ._sync.work import ManagedTransaction, Result, Session, Transaction
22
+ from ._version import __version__
23
+ from .client import NomosDB
24
+ from .connection import Result as QueryResult
25
+ from .exceptions import (
26
+ AuthError,
27
+ AuthenticationError,
28
+ ClientError,
29
+ ConfigurationError,
30
+ ConnectionPoolFull,
31
+ DatabaseError,
32
+ NomosConnectionError,
33
+ NomosDBError,
34
+ NotALeader,
35
+ ProtocolError,
36
+ QueryException,
37
+ ResultConsumedError,
38
+ ResultNotSingleError,
39
+ ServerError,
40
+ ServiceUnavailable,
41
+ SessionExpired,
42
+ TransactionCommitUnknown,
43
+ TransactionError,
44
+ TransientError,
45
+ )
46
+ from .orm import Entity, Node, Relationship, node_class, relationship_class
47
+
48
+ __all__ = [
49
+ "driver", "async_driver", "Driver", "AsyncDriver",
50
+ "Session", "AsyncSession", "Transaction", "AsyncTransaction",
51
+ "ManagedTransaction", "AsyncManagedTransaction", "Result", "AsyncResult",
52
+ "Record", "ResultSummary", "SummaryCounters", "READ_ACCESS", "WRITE_ACCESS",
53
+ "NomosDB", "QueryResult", "Node", "Relationship", "Entity", "node_class", "relationship_class",
54
+ "NomosDBError", "NomosConnectionError", "ConnectionPoolFull", "QueryException", "TransactionError",
55
+ "AuthenticationError", "ProtocolError", "ConfigurationError", "ServerError", "ClientError",
56
+ "TransientError", "DatabaseError", "AuthError", "NotALeader", "ServiceUnavailable", "SessionExpired",
57
+ "TransactionCommitUnknown", "ResultConsumedError", "ResultNotSingleError",
58
+ "__version__",
59
+ ]
File without changes
@@ -0,0 +1,126 @@
1
+ """One Bolt connection: handshake, authentication, pipelined requests."""
2
+
3
+ from typing import Optional, Tuple
4
+
5
+ from .._bolt import HANDSHAKE, Dechunker, Protocol, Response, parse_version
6
+ from .._common import Address, Config
7
+ from .._io import AsyncBoltSocket, monotonic
8
+ from ..exceptions import ServerError, ServiceUnavailable, SessionExpired
9
+
10
+
11
+ class AsyncConnection:
12
+ def __init__(self, sock: AsyncBoltSocket, address: Address, version: Tuple[int, int]):
13
+ self._sock = sock
14
+ self.address = address
15
+ self.protocol = Protocol(version)
16
+ self._dechunker = Dechunker()
17
+ self._inbox: list = []
18
+ self.server_agent = ""
19
+ self.connection_id = ""
20
+ self.defunct = False
21
+ self.closed = False
22
+ self.last_used = monotonic()
23
+
24
+ @classmethod
25
+ async def open(cls, address: Address, config: Config) -> "AsyncConnection":
26
+ sock = await AsyncBoltSocket.connect(address, config.ssl_context, config.connection_timeout)
27
+ try:
28
+ await sock.sendall(HANDSHAKE)
29
+ answer = b""
30
+ while len(answer) < 4:
31
+ part = await sock.recv(config.connection_timeout)
32
+ if not part:
33
+ raise ServiceUnavailable(f"{address[0]}:{address[1]} closed the connection during the Bolt handshake "
34
+ "(is it the Bolt port? nomosdb-server listens for Bolt on 7687)")
35
+ answer += part
36
+ conn = cls(sock, address, parse_version(answer[:4]))
37
+ if len(answer) > 4:
38
+ conn._inbox.extend(conn._dechunker.feed(answer[4:]))
39
+ hello = conn.protocol.hello(config.user_agent, config.auth, config.routing_context)
40
+ await conn.send_all()
41
+ await conn.fetch_all()
42
+ except BaseException as e:
43
+ await sock.close()
44
+ if isinstance(e, OSError):
45
+ raise ServiceUnavailable(f"{address[0]}:{address[1]} did not complete the Bolt handshake "
46
+ f"(is it the Bolt port? nomosdb-server listens for Bolt on 7687): {e}") from None
47
+ raise
48
+ conn.server_agent = str(hello.metadata.get("server", ""))
49
+ conn.connection_id = str(hello.metadata.get("connection_id", ""))
50
+ return conn
51
+
52
+ # -- I/O
53
+ async def send_all(self) -> None:
54
+ data = self.protocol.take_outbox()
55
+ if not data:
56
+ return
57
+ try:
58
+ await self._sock.sendall(data)
59
+ except OSError as e:
60
+ await self._broken(e)
61
+
62
+ async def fetch_message(self) -> None:
63
+ """Reads until at least one answer has been handled."""
64
+ while not self._inbox:
65
+ try:
66
+ data = await self._sock.recv()
67
+ except OSError as e:
68
+ await self._broken(e)
69
+ if not data:
70
+ await self._broken(None)
71
+ self._inbox.extend(self._dechunker.feed(data))
72
+ tag, fields = self._inbox.pop(0)
73
+ self.protocol.handle(tag, fields)
74
+
75
+ async def fetch_all(self) -> None:
76
+ """Reads the answers to every request sent; raises the first failure."""
77
+ error: Optional[ServerError] = None
78
+ while self.protocol.pending:
79
+ r = self.protocol.pending[0]
80
+ while not r.done:
81
+ await self.fetch_message()
82
+ if r.error is not None and error is None:
83
+ error = r.error
84
+ if error is not None:
85
+ raise error
86
+
87
+ async def wait(self, r: Response) -> Response:
88
+ """Sends what is queued and reads until r is answered; raises its failure."""
89
+ await self.send_all()
90
+ while not r.done:
91
+ await self.fetch_message()
92
+ if r.error is not None:
93
+ raise r.error
94
+ return r
95
+
96
+ async def _broken(self, e: Optional[BaseException]) -> None:
97
+ self.defunct = True
98
+ await self._sock.close()
99
+ why = f": {e}" if e else ""
100
+ raise SessionExpired(f"the connection to {self.address[0]}:{self.address[1]} broke{why}")
101
+
102
+ # -- state
103
+ async def reset(self) -> None:
104
+ """Back to READY: ends a transaction or a failure."""
105
+ r = self.protocol.reset(Response("RESET"))
106
+ await self.wait(r)
107
+ await self.fetch_all()
108
+
109
+ @property
110
+ def ready(self) -> bool:
111
+ return not (self.defunct or self.closed or self.protocol.pending or self.protocol.failed)
112
+
113
+ async def close(self) -> None:
114
+ if self.closed:
115
+ return
116
+ self.closed = True
117
+ if not self.defunct:
118
+ try:
119
+ self.protocol.goodbye()
120
+ await self._sock.sendall(self.protocol.take_outbox())
121
+ except OSError:
122
+ pass
123
+ await self._sock.close()
124
+
125
+ def __repr__(self) -> str:
126
+ return f"<AsyncConnection {self.address[0]}:{self.address[1]} bolt/{self.protocol.version[0]}.{self.protocol.version[1]}>"
@@ -0,0 +1,75 @@
1
+ """The driver: configuration, the connection pool, sessions."""
2
+
3
+ from typing import Any, Dict, List, Optional, Tuple
4
+
5
+ from .._common import READ_ACCESS, WRITE_ACCESS, Config, Record, ResultSummary
6
+ from ..exceptions import ConfigurationError
7
+ from .pool import AsyncDirectPool, AsyncRoutingPool
8
+ from .work import AsyncManagedTransaction, AsyncSession
9
+
10
+
11
+ class AsyncDriver:
12
+ """Holds the connection pool of one server or cluster. Thread safe (the blocking
13
+ Driver) / task safe (AsyncDriver); sessions are not."""
14
+
15
+ def __init__(self, uri: str, auth: Any = None, **config: Any):
16
+ self._config = Config(uri, auth, **config)
17
+ self._pool = AsyncRoutingPool(self._config) if self._config.routing else AsyncDirectPool(self._config)
18
+
19
+ @property
20
+ def encrypted(self) -> bool:
21
+ return self._config.ssl_context is not None
22
+
23
+ def session(self, default_access_mode: str = WRITE_ACCESS, bookmarks: Optional[List[str]] = None,
24
+ fetch_size: Optional[int] = None, database: Optional[str] = None) -> AsyncSession:
25
+ return AsyncSession(self._pool, self._config, default_access_mode, bookmarks, fetch_size, database)
26
+
27
+ async def verify_connectivity(self) -> None:
28
+ """Raises unless a server answers and accepts the credentials."""
29
+ conn = await self._pool.acquire(READ_ACCESS, [])
30
+ await self._pool.release(conn)
31
+
32
+ async def get_server_info(self) -> Dict[str, Any]:
33
+ conn = await self._pool.acquire(READ_ACCESS, [])
34
+ try:
35
+ return {"address": conn.address, "agent": conn.server_agent,
36
+ "protocol_version": conn.protocol.version, "connection_id": conn.connection_id}
37
+ finally:
38
+ await self._pool.release(conn)
39
+
40
+ async def execute_query(self, query: str, parameters: Optional[Dict[str, Any]] = None, routing: str = "w",
41
+ bookmarks: Optional[List[str]] = None,
42
+ **kwparameters: Any) -> Tuple[List[Record], ResultSummary, List[str]]:
43
+ """Runs one statement in a retried transaction and returns (records, summary, keys)."""
44
+ if routing not in ("r", "w"):
45
+ raise ConfigurationError("routing is 'r' (a reader) or 'w' (the leader)")
46
+ params = {**(parameters or {}), **kwparameters}
47
+
48
+ async def work(tx: AsyncManagedTransaction) -> Tuple[List[Record], ResultSummary, List[str]]:
49
+ res = await tx.run(query, params)
50
+ records = await res.to_list()
51
+ return records, await res.consume(), await res.keys()
52
+
53
+ async with self.session(bookmarks=bookmarks) as s:
54
+ if routing == "r":
55
+ return await s.execute_read(work)
56
+ return await s.execute_write(work)
57
+
58
+ async def close(self) -> None:
59
+ await self._pool.close()
60
+
61
+ async def __aenter__(self) -> "AsyncDriver":
62
+ return self
63
+
64
+ async def __aexit__(self, *exc: Any) -> None:
65
+ await self.close()
66
+
67
+
68
+ def async_driver(uri: str, auth: Any = None, **config: Any) -> AsyncDriver:
69
+ """A driver for bolt://host:7687 (one server) or nomosdb://host:7687 (a cluster),
70
+ with +s (TLS, verified) or +ssc (TLS, self-signed) for encryption.
71
+
72
+ auth is (user, password). Options: max_connection_pool_size, connection_timeout,
73
+ connection_acquisition_timeout, max_transaction_retry_time, fetch_size,
74
+ trusted_certificates (a CA file for +s), user_agent."""
75
+ return AsyncDriver(uri, auth, **config)