nomosdb 0.16.0__py3-none-any.whl

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/__init__.py ADDED
@@ -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)
nomosdb/_async/pool.py ADDED
@@ -0,0 +1,228 @@
1
+ """Connection pools: one server (bolt://) or a cluster through its routing table (nomosdb://)."""
2
+
3
+ from collections import deque
4
+ from typing import Deque, Dict, List, Optional
5
+
6
+ from .._bolt import Response
7
+ from .._common import READ_ACCESS, Address, Config, parse_address
8
+ from .._io import AsyncCondition, monotonic
9
+ from ..exceptions import ClientError, ServiceUnavailable
10
+ from .connection import AsyncConnection
11
+
12
+
13
+ class AsyncPool:
14
+ """Connections per address, at most max_connection_pool_size each."""
15
+
16
+ def __init__(self, config: Config):
17
+ self.config = config
18
+ self._idle: Dict[Address, Deque[AsyncConnection]] = {}
19
+ self._in_use: Dict[Address, int] = {}
20
+ self._cond = AsyncCondition()
21
+ self.closed = False
22
+
23
+ async def acquire_at(self, address: Address) -> AsyncConnection:
24
+ if self.closed:
25
+ raise ServiceUnavailable("the driver is closed")
26
+ deadline = monotonic() + self.config.connection_acquisition_timeout
27
+ while True:
28
+ async with self._cond:
29
+ idle = self._idle.setdefault(address, deque())
30
+ conn = idle.pop() if idle else None
31
+ if conn is None:
32
+ if self._in_use.get(address, 0) < self.config.max_connection_pool_size:
33
+ self._in_use[address] = self._in_use.get(address, 0) + 1
34
+ opening = True
35
+ else:
36
+ opening = False
37
+ left = deadline - monotonic()
38
+ if left <= 0 or not await self._cond.wait_for(
39
+ lambda: self._idle.get(address) or
40
+ self._in_use.get(address, 0) < self.config.max_connection_pool_size, left):
41
+ raise ServiceUnavailable(
42
+ f"no connection to {address[0]}:{address[1]} became free within "
43
+ f"{self.config.connection_acquisition_timeout} s (max_connection_pool_size="
44
+ f"{self.config.max_connection_pool_size})")
45
+ continue
46
+ else:
47
+ self._in_use[address] = self._in_use.get(address, 0) + 1
48
+ opening = False
49
+ if opening:
50
+ try:
51
+ return await AsyncConnection.open(address, self.config)
52
+ except BaseException:
53
+ await self._forget(address)
54
+ raise
55
+ if await self._alive(conn):
56
+ return conn
57
+ await self._forget(address)
58
+
59
+ async def _alive(self, conn: AsyncConnection) -> bool:
60
+ t = self.config.liveness_check_timeout
61
+ if conn.defunct or conn.closed:
62
+ return False
63
+ if t is not None and monotonic() - conn.last_used > t:
64
+ try:
65
+ await conn.reset()
66
+ except (ServiceUnavailable, ClientError):
67
+ await conn.close()
68
+ return False
69
+ return True
70
+
71
+ async def _forget(self, address: Address) -> None:
72
+ async with self._cond:
73
+ self._in_use[address] -= 1
74
+ self._cond.notify_all()
75
+
76
+ async def release(self, conn: AsyncConnection) -> None:
77
+ """Takes a connection back; one in a transaction or a failure is reset first."""
78
+ if not (conn.defunct or conn.closed) and not conn.ready:
79
+ try:
80
+ await conn.reset()
81
+ except Exception:
82
+ await conn.close()
83
+ async with self._cond:
84
+ self._in_use[conn.address] -= 1
85
+ if conn.defunct or conn.closed or self.closed:
86
+ keep = False
87
+ else:
88
+ conn.last_used = monotonic()
89
+ self._idle.setdefault(conn.address, deque()).append(conn)
90
+ keep = True
91
+ self._cond.notify_all()
92
+ if not keep:
93
+ await conn.close()
94
+
95
+ async def deactivate(self, address: Address) -> None:
96
+ """Closes the idle connections to a server that failed."""
97
+ async with self._cond:
98
+ idle = self._idle.pop(address, deque())
99
+ for c in idle:
100
+ await c.close()
101
+
102
+ async def close(self) -> None:
103
+ self.closed = True
104
+ async with self._cond:
105
+ idle = [c for q in self._idle.values() for c in q]
106
+ self._idle.clear()
107
+ for c in idle:
108
+ await c.close()
109
+
110
+
111
+ class AsyncDirectPool(AsyncPool):
112
+ """bolt://: every session goes to the one server."""
113
+
114
+ async def acquire(self, access_mode: str, bookmarks: List[str]) -> AsyncConnection:
115
+ return await self.acquire_at(self.config.address)
116
+
117
+ async def on_write_failure(self, address: Address) -> None:
118
+ pass
119
+
120
+ async def on_unavailable(self, address: Address) -> None:
121
+ await self.deactivate(address)
122
+
123
+
124
+ class _RoutingTable:
125
+ def __init__(self, seed: Address):
126
+ self.routers: List[Address] = [seed]
127
+ self.readers: List[Address] = []
128
+ self.writers: List[Address] = []
129
+ self.expires = 0.0
130
+ self._next_reader = 0
131
+ self._next_writer = 0
132
+
133
+ def fresh(self, access_mode: str) -> bool:
134
+ servers = self.readers if access_mode == READ_ACCESS else self.writers
135
+ return monotonic() < self.expires and bool(servers)
136
+
137
+ def update(self, rt: Dict) -> None:
138
+ ttl = float(rt.get("ttl", 300))
139
+ readers, writers, routers = [], [], []
140
+ for s in rt.get("servers", []):
141
+ addrs = [parse_address(a) for a in s.get("addresses", [])]
142
+ role = s.get("role")
143
+ if role == "READ":
144
+ readers += addrs
145
+ elif role == "WRITE":
146
+ writers += addrs
147
+ elif role == "ROUTE":
148
+ routers += addrs
149
+ if not routers:
150
+ raise ServiceUnavailable("the routing table names no routers")
151
+ self.routers, self.readers, self.writers = routers, readers, writers
152
+ self.expires = monotonic() + ttl
153
+
154
+ def pick(self, access_mode: str) -> Optional[Address]:
155
+ if access_mode == READ_ACCESS:
156
+ if not self.readers:
157
+ return None
158
+ self._next_reader = (self._next_reader + 1) % len(self.readers)
159
+ return self.readers[self._next_reader]
160
+ if not self.writers:
161
+ return None
162
+ self._next_writer = (self._next_writer + 1) % len(self.writers)
163
+ return self.writers[self._next_writer]
164
+
165
+ def remove(self, address: Address) -> None:
166
+ for lst in (self.routers, self.readers, self.writers):
167
+ while address in lst:
168
+ lst.remove(address)
169
+
170
+ def remove_writer(self, address: Address) -> None:
171
+ while address in self.writers:
172
+ self.writers.remove(address)
173
+
174
+
175
+ class AsyncRoutingPool(AsyncPool):
176
+ """nomosdb:// / neo4j://: asks the cluster which members write and read (ROUTE)."""
177
+
178
+ def __init__(self, config: Config):
179
+ super().__init__(config)
180
+ self.table = _RoutingTable(config.address)
181
+ self._refreshing = AsyncCondition()
182
+
183
+ async def acquire(self, access_mode: str, bookmarks: List[str]) -> AsyncConnection:
184
+ last: Optional[BaseException] = None
185
+ for _ in range(3):
186
+ if not self.table.fresh(access_mode):
187
+ await self._refresh(bookmarks)
188
+ address = self.table.pick(access_mode)
189
+ if address is None:
190
+ raise ServiceUnavailable(
191
+ f"the cluster has no {'reader' if access_mode == READ_ACCESS else 'leader'} at the moment")
192
+ try:
193
+ return await self.acquire_at(address)
194
+ except ServiceUnavailable as e:
195
+ last = e
196
+ await self.on_unavailable(address)
197
+ raise last if last else ServiceUnavailable("no cluster member could be reached")
198
+
199
+ async def _refresh(self, bookmarks: List[str]) -> None:
200
+ async with self._refreshing:
201
+ routers = list(self.table.routers)
202
+ if self.config.address not in routers:
203
+ routers.append(self.config.address) # the seed, in case every router moved
204
+ errors = []
205
+ for address in routers:
206
+ try:
207
+ conn = await self.acquire_at(address)
208
+ except ServiceUnavailable as e:
209
+ errors.append(str(e))
210
+ continue
211
+ try:
212
+ r = conn.protocol.route(self.config.routing_context or {}, list(bookmarks), Response("ROUTE"))
213
+ await conn.wait(r)
214
+ self.table.update(r.metadata.get("rt", {}))
215
+ return
216
+ except (ServiceUnavailable, ClientError) as e:
217
+ errors.append(f"{address[0]}:{address[1]}: {e}")
218
+ finally:
219
+ await self.release(conn)
220
+ raise ServiceUnavailable("no routing table: " + "; ".join(errors))
221
+
222
+ async def on_write_failure(self, address: Address) -> None:
223
+ """A write reached a member that is not the leader: ask again."""
224
+ self.table.remove_writer(address)
225
+
226
+ async def on_unavailable(self, address: Address) -> None:
227
+ self.table.remove(address)
228
+ await self.deactivate(address)