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.
- nomosdb-0.16.0/LICENSE +17 -0
- nomosdb-0.16.0/PKG-INFO +179 -0
- nomosdb-0.16.0/README.md +137 -0
- nomosdb-0.16.0/nomosdb/__init__.py +59 -0
- nomosdb-0.16.0/nomosdb/_async/__init__.py +0 -0
- nomosdb-0.16.0/nomosdb/_async/connection.py +126 -0
- nomosdb-0.16.0/nomosdb/_async/driver.py +75 -0
- nomosdb-0.16.0/nomosdb/_async/pool.py +228 -0
- nomosdb-0.16.0/nomosdb/_async/work.py +504 -0
- nomosdb-0.16.0/nomosdb/_bolt.py +267 -0
- nomosdb-0.16.0/nomosdb/_common.py +235 -0
- nomosdb-0.16.0/nomosdb/_io.py +155 -0
- nomosdb-0.16.0/nomosdb/_packstream.py +237 -0
- nomosdb-0.16.0/nomosdb/_sync/__init__.py +1 -0
- nomosdb-0.16.0/nomosdb/_sync/connection.py +127 -0
- nomosdb-0.16.0/nomosdb/_sync/driver.py +76 -0
- nomosdb-0.16.0/nomosdb/_sync/pool.py +229 -0
- nomosdb-0.16.0/nomosdb/_sync/work.py +505 -0
- nomosdb-0.16.0/nomosdb/_version.py +3 -0
- nomosdb-0.16.0/nomosdb/async_client.py +230 -0
- nomosdb-0.16.0/nomosdb/client.py +266 -0
- nomosdb-0.16.0/nomosdb/connection.py +237 -0
- nomosdb-0.16.0/nomosdb/exceptions.py +134 -0
- nomosdb-0.16.0/nomosdb/graph.py +110 -0
- nomosdb-0.16.0/nomosdb/orm.py +238 -0
- nomosdb-0.16.0/nomosdb/py.typed +0 -0
- nomosdb-0.16.0/nomosdb/query.py +30 -0
- nomosdb-0.16.0/nomosdb/transaction.py +44 -0
- nomosdb-0.16.0/nomosdb/types.py +17 -0
- nomosdb-0.16.0/nomosdb/utils.py +30 -0
- nomosdb-0.16.0/nomosdb.egg-info/PKG-INFO +179 -0
- nomosdb-0.16.0/nomosdb.egg-info/SOURCES.txt +41 -0
- nomosdb-0.16.0/nomosdb.egg-info/dependency_links.txt +1 -0
- nomosdb-0.16.0/nomosdb.egg-info/requires.txt +14 -0
- nomosdb-0.16.0/nomosdb.egg-info/top_level.txt +1 -0
- nomosdb-0.16.0/pyproject.toml +56 -0
- nomosdb-0.16.0/setup.cfg +4 -0
- nomosdb-0.16.0/tests/test_async.py +68 -0
- nomosdb-0.16.0/tests/test_bolt.py +102 -0
- nomosdb-0.16.0/tests/test_client.py +58 -0
- nomosdb-0.16.0/tests/test_connection.py +71 -0
- nomosdb-0.16.0/tests/test_orm.py +50 -0
- 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.
|
nomosdb-0.16.0/PKG-INFO
ADDED
|
@@ -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.
|
nomosdb-0.16.0/README.md
ADDED
|
@@ -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)
|