pgtern 0.3.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.
- pgtern/__init__.py +46 -0
- pgtern/_binary.py +226 -0
- pgtern/_composite.py +103 -0
- pgtern/_conn.py +907 -0
- pgtern/_copy.py +186 -0
- pgtern/_cursor.py +261 -0
- pgtern/_dsn.py +182 -0
- pgtern/_protocol.py +258 -0
- pgtern/_query.py +69 -0
- pgtern/_scram.py +101 -0
- pgtern/_types.py +442 -0
- pgtern/adapters.py +85 -0
- pgtern/aio.py +1287 -0
- pgtern/errors.py +248 -0
- pgtern/hooks.py +53 -0
- pgtern/pipeline.py +202 -0
- pgtern/pool.py +309 -0
- pgtern/py.typed +0 -0
- pgtern/rows.py +31 -0
- pgtern/sql.py +200 -0
- pgtern/types/__init__.py +30 -0
- pgtern/types/json.py +5 -0
- pgtern-0.3.0.dist-info/METADATA +265 -0
- pgtern-0.3.0.dist-info/RECORD +27 -0
- pgtern-0.3.0.dist-info/WHEEL +5 -0
- pgtern-0.3.0.dist-info/licenses/LICENSE +202 -0
- pgtern-0.3.0.dist-info/top_level.txt +1 -0
pgtern/aio.py
ADDED
|
@@ -0,0 +1,1287 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Native asyncio API: ``AsyncConnection``, ``AsyncCursor``, ``AsyncConnectionPool``.
|
|
3
|
+
|
|
4
|
+
Same wire protocol, authentication, TLS and type handling as the blocking driver;
|
|
5
|
+
only the I/O is different. If a task is cancelled (or a timeout fires) while a
|
|
6
|
+
statement is in flight the connection is closed, because its protocol state is then unknown.
|
|
7
|
+
|
|
8
|
+
async with await pgtern.aio.connect("postgresql://...") as conn:
|
|
9
|
+
async with conn.transaction():
|
|
10
|
+
cur = await conn.execute("SELECT %s::int", [1])
|
|
11
|
+
print(await cur.fetchall())
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import collections
|
|
17
|
+
import hashlib
|
|
18
|
+
import itertools
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
|
|
22
|
+
from typing import Any, AsyncIterator, Callable, Iterable, Sequence
|
|
23
|
+
|
|
24
|
+
from . import _binary
|
|
25
|
+
from . import _protocol as P
|
|
26
|
+
from . import _query, _types, errors, hooks
|
|
27
|
+
from ._conn import (
|
|
28
|
+
_BROKEN, _IDLE, PRIME_SQL, ConnectionInfo, _ssl_context,
|
|
29
|
+
)
|
|
30
|
+
from ._copy import format_row, parse_rows
|
|
31
|
+
from ._cursor import Cursor
|
|
32
|
+
from ._dsn import ConnParams, build_params
|
|
33
|
+
from ._scram import MECHANISM, ScramClient
|
|
34
|
+
from .pipeline import _enter_hooks, _encode, _exit_hooks, _Item
|
|
35
|
+
from .pool import PoolClosed, PoolTimeout
|
|
36
|
+
from .rows import tuple_row
|
|
37
|
+
from .sql import SQL, Composable, quote_ident
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger("pgtern.aio")
|
|
40
|
+
|
|
41
|
+
__all__ = ["connect", "AsyncConnection", "AsyncCursor", "AsyncServerCursor", "AsyncTransaction",
|
|
42
|
+
"AsyncCopy", "AsyncConnectionPool"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AsyncCursor(Cursor):
|
|
46
|
+
"""Like :class:`pgtern.Cursor`; fetching is in-memory (so synchronous), executing is awaited."""
|
|
47
|
+
|
|
48
|
+
async def execute(self, query: Any, params: Any = None, *, prepare: bool | None = None, # type: ignore[override]
|
|
49
|
+
binary: bool | None = None) -> "AsyncCursor":
|
|
50
|
+
if self._closed:
|
|
51
|
+
raise errors.InterfaceError("the cursor is closed")
|
|
52
|
+
self._set_results(await self.connection._execute(query, params, bool(binary)))
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def executemany(self, query: Any, params_seq: Sequence[Any]) -> None: # type: ignore[override]
|
|
56
|
+
"""Run ``query`` once per parameter set as one pipelined batch, in one transaction
|
|
57
|
+
(see ``AsyncConnection._run_many``). ``rowcount`` is the total of the statements' rowcounts."""
|
|
58
|
+
if self._closed:
|
|
59
|
+
raise errors.InterfaceError("the cursor is closed")
|
|
60
|
+
total = await self.connection._run_many(query, params_seq)
|
|
61
|
+
self._set_results([])
|
|
62
|
+
self._total_rowcount = total
|
|
63
|
+
|
|
64
|
+
def copy(self, query: Any, params: Any = None) -> "AsyncCopy": # type: ignore[override]
|
|
65
|
+
if self._closed:
|
|
66
|
+
raise errors.InterfaceError("the cursor is closed")
|
|
67
|
+
return AsyncCopy(self, query, params)
|
|
68
|
+
|
|
69
|
+
async def close(self) -> None: # type: ignore[override]
|
|
70
|
+
Cursor.close(self)
|
|
71
|
+
|
|
72
|
+
async def __aenter__(self) -> "AsyncCursor":
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
76
|
+
await self.close()
|
|
77
|
+
|
|
78
|
+
def __aiter__(self) -> AsyncIterator:
|
|
79
|
+
return self._aiter()
|
|
80
|
+
|
|
81
|
+
async def _aiter(self) -> AsyncIterator:
|
|
82
|
+
while True:
|
|
83
|
+
row = self.fetchone()
|
|
84
|
+
if row is None:
|
|
85
|
+
return
|
|
86
|
+
yield row
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AsyncServerCursor(AsyncCursor):
|
|
90
|
+
"""Named server-side cursor (``DECLARE``); rows are fetched in batches with ``await``."""
|
|
91
|
+
|
|
92
|
+
itersize = 100
|
|
93
|
+
|
|
94
|
+
def __init__(self, connection: Any, name: str, *, row_factory: Callable | None = None,
|
|
95
|
+
scrollable: bool | None = None, withhold: bool = False) -> None:
|
|
96
|
+
super().__init__(connection, row_factory=row_factory)
|
|
97
|
+
self.name = name
|
|
98
|
+
self._scrollable, self._withhold = scrollable, withhold
|
|
99
|
+
self._binary = False
|
|
100
|
+
self._declared = False
|
|
101
|
+
self._columns = None
|
|
102
|
+
self._fetched = 0
|
|
103
|
+
self._batch = None
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def description(self):
|
|
107
|
+
return list(self._columns) if self._columns is not None else None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def rowcount(self) -> int:
|
|
111
|
+
return self._fetched if self._declared else -1
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def rownumber(self) -> int | None:
|
|
115
|
+
return self._fetched if self._declared else None
|
|
116
|
+
|
|
117
|
+
async def execute(self, query: Any, params: Any = None, *, binary: bool | None = None, # type: ignore[override]
|
|
118
|
+
**_: Any) -> "AsyncServerCursor":
|
|
119
|
+
if self._closed:
|
|
120
|
+
raise errors.InterfaceError("the cursor is closed")
|
|
121
|
+
if self._declared:
|
|
122
|
+
raise errors.ProgrammingError("the server-side cursor was already executed; create a new one")
|
|
123
|
+
self._binary = bool(binary)
|
|
124
|
+
prefix = f"DECLARE {quote_ident(self.name)} "
|
|
125
|
+
if self._scrollable is not None:
|
|
126
|
+
prefix += "SCROLL " if self._scrollable else "NO SCROLL "
|
|
127
|
+
prefix += "CURSOR " + ("WITH HOLD " if self._withhold else "") + "FOR "
|
|
128
|
+
body = query if isinstance(query, Composable) else SQL(query if isinstance(query, str) else bytes(query).decode())
|
|
129
|
+
await self.connection._execute(SQL(prefix) + body, params)
|
|
130
|
+
self._declared = True
|
|
131
|
+
first = (await self.connection._execute(f"FETCH FORWARD 0 FROM {quote_ident(self.name)}", None, self._binary))[0]
|
|
132
|
+
self._columns = first.columns
|
|
133
|
+
self._set_results([first])
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
async def _fetch(self, command: str):
|
|
137
|
+
if not self._declared:
|
|
138
|
+
raise errors.ProgrammingError("the cursor has not been executed")
|
|
139
|
+
result = (await self.connection._execute(f"{command} FROM {quote_ident(self.name)}", None, self._binary))[0]
|
|
140
|
+
if result.columns is None:
|
|
141
|
+
result.columns = self._columns
|
|
142
|
+
self._set_results([result])
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
async def fetchmany(self, size: int | None = None) -> list: # type: ignore[override]
|
|
146
|
+
result = await self._fetch(f"FETCH FORWARD {int(self.arraysize if size is None else size)}")
|
|
147
|
+
self._fetched += len(result.rows)
|
|
148
|
+
return Cursor.fetchall(self)
|
|
149
|
+
|
|
150
|
+
async def fetchone(self) -> Any: # type: ignore[override]
|
|
151
|
+
rows = await self.fetchmany(1)
|
|
152
|
+
return rows[0] if rows else None
|
|
153
|
+
|
|
154
|
+
async def fetchall(self) -> list: # type: ignore[override]
|
|
155
|
+
result = await self._fetch("FETCH FORWARD ALL")
|
|
156
|
+
self._fetched += len(result.rows)
|
|
157
|
+
return Cursor.fetchall(self)
|
|
158
|
+
|
|
159
|
+
async def scroll(self, value: int, mode: str = "relative") -> None:
|
|
160
|
+
if mode not in ("relative", "absolute"):
|
|
161
|
+
raise ValueError(f"bad mode: {mode}; it should be 'relative' or 'absolute'")
|
|
162
|
+
await self._fetch(f"MOVE {mode.upper()} {int(value)}")
|
|
163
|
+
self._fetched = (self._fetched + value) if mode == "relative" else value
|
|
164
|
+
|
|
165
|
+
def _aiter(self) -> AsyncIterator:
|
|
166
|
+
return self._server_iter()
|
|
167
|
+
|
|
168
|
+
async def _server_iter(self) -> AsyncIterator:
|
|
169
|
+
while True:
|
|
170
|
+
rows = await self.fetchmany(self.itersize)
|
|
171
|
+
for row in rows:
|
|
172
|
+
yield row
|
|
173
|
+
if len(rows) < self.itersize:
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
async def close(self) -> None: # type: ignore[override]
|
|
177
|
+
if self._declared and not self.connection.closed and self.connection._tx_status != "E":
|
|
178
|
+
try:
|
|
179
|
+
await self.connection._execute(f"CLOSE {quote_ident(self.name)}", None)
|
|
180
|
+
except errors.Error:
|
|
181
|
+
pass
|
|
182
|
+
self._declared = False
|
|
183
|
+
Cursor.close(self)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class AsyncTransaction:
|
|
187
|
+
def __init__(self, conn: "AsyncConnection", savepoint_name: str | None = None,
|
|
188
|
+
force_rollback: bool = False) -> None:
|
|
189
|
+
self._conn, self._name, self._force_rollback = conn, savepoint_name, force_rollback
|
|
190
|
+
self._savepoint: str | None = None
|
|
191
|
+
|
|
192
|
+
async def __aenter__(self) -> "AsyncTransaction":
|
|
193
|
+
conn = self._conn
|
|
194
|
+
async with conn._lock:
|
|
195
|
+
conn._check_open()
|
|
196
|
+
if conn._tx_status == _IDLE:
|
|
197
|
+
self._savepoint = None
|
|
198
|
+
await conn._simple("BEGIN")
|
|
199
|
+
else:
|
|
200
|
+
self._savepoint = self._name or f"_pgtern_{next(conn._savepoint_ids)}"
|
|
201
|
+
await conn._simple(f"SAVEPOINT {quote_ident(self._savepoint)}")
|
|
202
|
+
conn._tx_depth += 1
|
|
203
|
+
return self
|
|
204
|
+
|
|
205
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
206
|
+
conn = self._conn
|
|
207
|
+
async with conn._lock:
|
|
208
|
+
conn._tx_depth = max(0, conn._tx_depth - 1)
|
|
209
|
+
if conn.closed:
|
|
210
|
+
return False
|
|
211
|
+
commit = exc_type is None and not self._force_rollback
|
|
212
|
+
try:
|
|
213
|
+
if self._savepoint is None:
|
|
214
|
+
await conn._simple("COMMIT" if commit else "ROLLBACK")
|
|
215
|
+
else:
|
|
216
|
+
sp = quote_ident(self._savepoint)
|
|
217
|
+
await conn._simple(f"RELEASE {sp}" if commit else f"ROLLBACK TO {sp}; RELEASE {sp}")
|
|
218
|
+
except Exception:
|
|
219
|
+
if exc_type is None:
|
|
220
|
+
raise
|
|
221
|
+
log.warning("error while ending a failed transaction block", exc_info=True)
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class AsyncCopy:
|
|
226
|
+
"""``async with cur.copy("COPY t FROM STDIN") as copy: await copy.write_row(...)``."""
|
|
227
|
+
|
|
228
|
+
def __init__(self, cursor: AsyncCursor, query: Any, params: Any = None) -> None:
|
|
229
|
+
self._cursor, self._conn, self._query, self._params = cursor, cursor.connection, query, params
|
|
230
|
+
self._kind = ""
|
|
231
|
+
self._stack = AsyncExitStack()
|
|
232
|
+
self._done = False
|
|
233
|
+
self._buffer = bytearray()
|
|
234
|
+
self._pending_error: dict[str, str] | None = None
|
|
235
|
+
|
|
236
|
+
async def __aenter__(self) -> "AsyncCopy":
|
|
237
|
+
conn = self._conn
|
|
238
|
+
await conn._lock.acquire()
|
|
239
|
+
try:
|
|
240
|
+
self._kind = await conn._start_copy(self._query, self._params, self._stack)
|
|
241
|
+
except BaseException:
|
|
242
|
+
await self._stack.aclose()
|
|
243
|
+
conn._lock.release()
|
|
244
|
+
raise
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
248
|
+
conn = self._conn
|
|
249
|
+
try:
|
|
250
|
+
if conn.closed:
|
|
251
|
+
return
|
|
252
|
+
if exc_type is asyncio.CancelledError:
|
|
253
|
+
conn._mark_broken()
|
|
254
|
+
return
|
|
255
|
+
if self._kind == "in":
|
|
256
|
+
if exc_type is not None:
|
|
257
|
+
await conn._send(P.copy_fail(f"error in Python: {exc_type.__name__}"))
|
|
258
|
+
try:
|
|
259
|
+
await conn._read_cycle()
|
|
260
|
+
except errors.Error:
|
|
261
|
+
pass
|
|
262
|
+
else:
|
|
263
|
+
await conn._send(P.COPY_DONE)
|
|
264
|
+
results = await conn._read_cycle()
|
|
265
|
+
self._cursor._set_results(results)
|
|
266
|
+
else:
|
|
267
|
+
while not self._done:
|
|
268
|
+
await self._next_chunk()
|
|
269
|
+
err = self._pending_error
|
|
270
|
+
results = await conn._read_cycle()
|
|
271
|
+
if err is not None:
|
|
272
|
+
raise errors.error_from_fields(err)
|
|
273
|
+
if exc_type is None:
|
|
274
|
+
self._cursor._set_results(results)
|
|
275
|
+
finally:
|
|
276
|
+
try:
|
|
277
|
+
await self._stack.__aexit__(exc_type, exc, tb)
|
|
278
|
+
finally:
|
|
279
|
+
conn._lock.release()
|
|
280
|
+
|
|
281
|
+
async def write(self, data: bytes | bytearray | memoryview | str) -> None:
|
|
282
|
+
if self._kind != "in":
|
|
283
|
+
raise errors.ProgrammingError("write() needs a COPY ... FROM STDIN statement")
|
|
284
|
+
data = data.encode("utf-8") if isinstance(data, str) else bytes(data)
|
|
285
|
+
for i in range(0, max(len(data), 1), 1 << 20):
|
|
286
|
+
await self._conn._send(P.copy_data(data[i:i + (1 << 20)]))
|
|
287
|
+
|
|
288
|
+
async def write_row(self, row: Sequence[Any]) -> None:
|
|
289
|
+
await self.write(format_row(row))
|
|
290
|
+
|
|
291
|
+
async def _next_chunk(self) -> bytes | None:
|
|
292
|
+
conn = self._conn
|
|
293
|
+
while True:
|
|
294
|
+
tag, payload = await conn._next_message()
|
|
295
|
+
if tag == 0x64:
|
|
296
|
+
return payload
|
|
297
|
+
if tag == 0x63:
|
|
298
|
+
self._done = True
|
|
299
|
+
return None
|
|
300
|
+
if tag == 0x45:
|
|
301
|
+
self._done = True
|
|
302
|
+
self._pending_error = P.parse_error_fields(payload)
|
|
303
|
+
return None
|
|
304
|
+
conn._handle_async_message(tag, payload)
|
|
305
|
+
|
|
306
|
+
async def read(self) -> bytes:
|
|
307
|
+
if self._kind != "out":
|
|
308
|
+
raise errors.ProgrammingError("read() needs a COPY ... TO STDOUT statement")
|
|
309
|
+
if self._done:
|
|
310
|
+
return b""
|
|
311
|
+
chunk = await self._next_chunk()
|
|
312
|
+
return chunk if chunk is not None else b""
|
|
313
|
+
|
|
314
|
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
|
315
|
+
while True:
|
|
316
|
+
chunk = await self.read()
|
|
317
|
+
if not chunk:
|
|
318
|
+
return
|
|
319
|
+
yield chunk
|
|
320
|
+
|
|
321
|
+
async def rows(self) -> AsyncIterator[tuple]:
|
|
322
|
+
async for chunk in self:
|
|
323
|
+
self._buffer += chunk
|
|
324
|
+
for row in parse_rows(self._buffer):
|
|
325
|
+
yield row
|
|
326
|
+
for row in parse_rows(self._buffer, final=True):
|
|
327
|
+
yield row
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
class AsyncConnection:
|
|
331
|
+
"""An asyncio connection. Create it with :func:`pgtern.aio.connect`."""
|
|
332
|
+
|
|
333
|
+
def __init__(self, params: ConnParams, *, autocommit: bool = False,
|
|
334
|
+
row_factory: Callable | None = None) -> None:
|
|
335
|
+
self._params = params
|
|
336
|
+
self._autocommit = autocommit
|
|
337
|
+
self.row_factory = row_factory or tuple_row
|
|
338
|
+
self._lock = asyncio.Lock()
|
|
339
|
+
self._reader: asyncio.StreamReader | None = None
|
|
340
|
+
self._writer: asyncio.StreamWriter | None = None
|
|
341
|
+
self._closed = True
|
|
342
|
+
self._parser = P.MessageParser()
|
|
343
|
+
self._pending: collections.deque[tuple[int, bytes]] = collections.deque()
|
|
344
|
+
self._server_params: dict[str, str] = {}
|
|
345
|
+
self._backend_pid = 0
|
|
346
|
+
self._tx_status = _BROKEN
|
|
347
|
+
self._tx_depth = 0
|
|
348
|
+
self._savepoint_ids = itertools.count(1)
|
|
349
|
+
self._host, self._port = "", 0
|
|
350
|
+
self._dates_ok = True
|
|
351
|
+
self._notice_handlers: list[Callable] = []
|
|
352
|
+
self._notify_handlers: list[Callable] = []
|
|
353
|
+
self._notifies: collections.deque[P.Notify] = collections.deque()
|
|
354
|
+
self._loader_cache: dict[int, Any] = {}
|
|
355
|
+
self._pipeline: Any = None
|
|
356
|
+
self.info = ConnectionInfo(self) # type: ignore[arg-type]
|
|
357
|
+
self.created = time.monotonic()
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------- opening
|
|
360
|
+
@classmethod
|
|
361
|
+
async def connect(cls, conninfo: str = "", *, autocommit: bool = False,
|
|
362
|
+
row_factory: Callable | None = None, **kwargs: Any) -> "AsyncConnection":
|
|
363
|
+
conn = cls(build_params(conninfo, **kwargs), autocommit=autocommit, row_factory=row_factory)
|
|
364
|
+
await conn._open()
|
|
365
|
+
return conn
|
|
366
|
+
|
|
367
|
+
async def _open(self) -> None:
|
|
368
|
+
modes = ["disable", "require"] if self._params.sslmode == "allow" else [self._params.sslmode]
|
|
369
|
+
last: errors.OperationalError | None = None
|
|
370
|
+
for host, port in self._params.hosts:
|
|
371
|
+
for mode in modes:
|
|
372
|
+
try:
|
|
373
|
+
await asyncio.wait_for(self._connect_host(host, port, mode), self._params.connect_timeout)
|
|
374
|
+
return
|
|
375
|
+
except (errors.OperationalError, asyncio.TimeoutError, OSError) as exc:
|
|
376
|
+
self._drop()
|
|
377
|
+
last = exc if isinstance(exc, errors.OperationalError) else errors.OperationalError(
|
|
378
|
+
f'connection to server at "{host}", port {port} failed: {exc or "timeout expired"}')
|
|
379
|
+
assert last is not None
|
|
380
|
+
raise last
|
|
381
|
+
|
|
382
|
+
def _drop(self) -> None:
|
|
383
|
+
writer, self._writer, self._reader = self._writer, None, None
|
|
384
|
+
self._closed = True
|
|
385
|
+
self._parser = P.MessageParser()
|
|
386
|
+
self._pending.clear()
|
|
387
|
+
if writer is not None:
|
|
388
|
+
writer.close()
|
|
389
|
+
|
|
390
|
+
async def _connect_host(self, host: str, port: int, mode: str) -> None:
|
|
391
|
+
unix = host.startswith("/")
|
|
392
|
+
try:
|
|
393
|
+
if unix:
|
|
394
|
+
reader, writer = await asyncio.open_unix_connection(f"{host}/.s.PGSQL.{port}")
|
|
395
|
+
else:
|
|
396
|
+
reader, writer = await asyncio.open_connection(host, port)
|
|
397
|
+
sock = writer.get_extra_info("socket")
|
|
398
|
+
if sock is not None:
|
|
399
|
+
import socket as _s
|
|
400
|
+
sock.setsockopt(_s.IPPROTO_TCP, _s.TCP_NODELAY, 1)
|
|
401
|
+
sock.setsockopt(_s.SOL_SOCKET, _s.SO_KEEPALIVE, 1)
|
|
402
|
+
except OSError as exc:
|
|
403
|
+
raise errors.OperationalError(f'connection to server at "{host}", port {port} failed: {exc}') from exc
|
|
404
|
+
self._reader, self._writer = reader, writer
|
|
405
|
+
self._host, self._port = host, port
|
|
406
|
+
self._closed = False
|
|
407
|
+
self._parser = P.MessageParser()
|
|
408
|
+
self._pending.clear()
|
|
409
|
+
self._server_params = {}
|
|
410
|
+
if not unix and mode != "disable":
|
|
411
|
+
try:
|
|
412
|
+
writer.write(P.SSL_REQUEST)
|
|
413
|
+
await writer.drain()
|
|
414
|
+
answer = await reader.readexactly(1)
|
|
415
|
+
if answer == b"S":
|
|
416
|
+
await writer.start_tls(_ssl_context(self._params, mode), server_hostname=host)
|
|
417
|
+
elif answer == b"N":
|
|
418
|
+
if mode in ("require", "verify-ca", "verify-full"):
|
|
419
|
+
raise errors.OperationalError("server does not support SSL, but SSL was required")
|
|
420
|
+
else:
|
|
421
|
+
raise errors.OperationalError(f"received an invalid response to the SSL request: {answer!r}")
|
|
422
|
+
except (OSError, asyncio.IncompleteReadError) as exc:
|
|
423
|
+
self._drop()
|
|
424
|
+
if mode == "prefer":
|
|
425
|
+
return await self._connect_host(host, port, "disable")
|
|
426
|
+
raise errors.OperationalError(
|
|
427
|
+
f'SSL connection to server at "{host}", port {port} failed: {exc}') from exc
|
|
428
|
+
await self._startup()
|
|
429
|
+
self._dates_ok = self._server_params.get("DateStyle", "ISO").upper().startswith("ISO")
|
|
430
|
+
|
|
431
|
+
def _password(self) -> str:
|
|
432
|
+
if self._params.password is None:
|
|
433
|
+
raise errors.OperationalError("the server requested a password, but none was provided")
|
|
434
|
+
return self._params.password
|
|
435
|
+
|
|
436
|
+
async def _startup(self) -> None:
|
|
437
|
+
p = self._params
|
|
438
|
+
startup = {"user": p.user, "database": p.dbname, "client_encoding": "UTF8", "DateStyle": "ISO"}
|
|
439
|
+
if p.application_name:
|
|
440
|
+
startup["application_name"] = p.application_name
|
|
441
|
+
if p.options:
|
|
442
|
+
startup["options"] = p.options
|
|
443
|
+
await self._send(P.startup_message(startup))
|
|
444
|
+
scram: ScramClient | None = None
|
|
445
|
+
while True:
|
|
446
|
+
tag, payload = await self._next_message()
|
|
447
|
+
if tag == 0x52:
|
|
448
|
+
code = int.from_bytes(payload[:4], "big")
|
|
449
|
+
if code == 0:
|
|
450
|
+
continue
|
|
451
|
+
if code == 3:
|
|
452
|
+
await self._send(P.password_message(self._password().encode()))
|
|
453
|
+
elif code == 5:
|
|
454
|
+
inner = hashlib.md5((self._password() + p.user).encode(), usedforsecurity=False).hexdigest()
|
|
455
|
+
outer = hashlib.md5(inner.encode() + payload[4:8], usedforsecurity=False).hexdigest()
|
|
456
|
+
await self._send(P.password_message(b"md5" + outer.encode()))
|
|
457
|
+
elif code == 10:
|
|
458
|
+
mechanisms = [m for m in payload[4:].split(b"\0") if m]
|
|
459
|
+
if MECHANISM.encode() not in mechanisms:
|
|
460
|
+
raise errors.NotSupportedError(
|
|
461
|
+
"the server offers no supported SASL mechanism: " + ", ".join(m.decode() for m in mechanisms))
|
|
462
|
+
scram = ScramClient(self._password())
|
|
463
|
+
await self._send(P.sasl_initial_response(MECHANISM, scram.client_first()))
|
|
464
|
+
elif code == 11 and scram is not None:
|
|
465
|
+
await self._send(P.sasl_response(scram.client_final(payload[4:])))
|
|
466
|
+
elif code == 12 and scram is not None:
|
|
467
|
+
scram.verify_server_final(payload[4:])
|
|
468
|
+
else:
|
|
469
|
+
raise errors.NotSupportedError(f"authentication method {code} is not supported")
|
|
470
|
+
elif tag == 0x45:
|
|
471
|
+
raise errors.error_from_fields(P.parse_error_fields(payload))
|
|
472
|
+
elif tag == 0x4B:
|
|
473
|
+
self._backend_pid = int.from_bytes(payload[:4], "big")
|
|
474
|
+
elif tag == 0x5A:
|
|
475
|
+
self._tx_status = chr(payload[0])
|
|
476
|
+
return
|
|
477
|
+
else:
|
|
478
|
+
self._handle_async_message(tag, payload)
|
|
479
|
+
|
|
480
|
+
# ---------------------------------------------------------------- I/O
|
|
481
|
+
def _mark_broken(self) -> None:
|
|
482
|
+
self._closed = True
|
|
483
|
+
self._tx_status = _BROKEN
|
|
484
|
+
writer, self._writer, self._reader = self._writer, None, None
|
|
485
|
+
if writer is not None:
|
|
486
|
+
writer.close()
|
|
487
|
+
|
|
488
|
+
async def _send(self, data: bytes) -> None:
|
|
489
|
+
writer = self._writer
|
|
490
|
+
if writer is None:
|
|
491
|
+
raise errors.OperationalError("the connection is closed")
|
|
492
|
+
try:
|
|
493
|
+
writer.write(data)
|
|
494
|
+
await writer.drain()
|
|
495
|
+
except (OSError, ConnectionError) as exc:
|
|
496
|
+
self._mark_broken()
|
|
497
|
+
raise errors.OperationalError(f"connection failed while sending: {exc}") from exc
|
|
498
|
+
except BaseException:
|
|
499
|
+
self._mark_broken() # cancelled mid-write: the stream is in an unknown state
|
|
500
|
+
raise
|
|
501
|
+
|
|
502
|
+
def _write(self, data: bytes) -> None:
|
|
503
|
+
"""Buffer bytes for the socket without waiting: pipelines drain at their flush points."""
|
|
504
|
+
writer = self._writer
|
|
505
|
+
if writer is None:
|
|
506
|
+
raise errors.OperationalError("the connection is closed")
|
|
507
|
+
writer.write(data)
|
|
508
|
+
|
|
509
|
+
async def _drain(self) -> None:
|
|
510
|
+
writer = self._writer
|
|
511
|
+
if writer is None:
|
|
512
|
+
raise errors.OperationalError("the connection is closed")
|
|
513
|
+
try:
|
|
514
|
+
await writer.drain()
|
|
515
|
+
except (OSError, ConnectionError) as exc:
|
|
516
|
+
self._mark_broken()
|
|
517
|
+
raise errors.OperationalError(f"connection failed while sending: {exc}") from exc
|
|
518
|
+
except BaseException:
|
|
519
|
+
self._mark_broken()
|
|
520
|
+
raise
|
|
521
|
+
|
|
522
|
+
async def _next_message(self) -> tuple[int, bytes]:
|
|
523
|
+
while not self._pending:
|
|
524
|
+
reader = self._reader
|
|
525
|
+
if reader is None:
|
|
526
|
+
raise errors.OperationalError("the connection is closed")
|
|
527
|
+
try:
|
|
528
|
+
data = await reader.read(65536)
|
|
529
|
+
except (OSError, ConnectionError) as exc:
|
|
530
|
+
self._mark_broken()
|
|
531
|
+
raise errors.OperationalError(f"connection failed while receiving: {exc}") from exc
|
|
532
|
+
except BaseException:
|
|
533
|
+
self._mark_broken() # cancelled mid-statement
|
|
534
|
+
raise
|
|
535
|
+
if not data:
|
|
536
|
+
self._mark_broken()
|
|
537
|
+
raise errors.OperationalError("server closed the connection unexpectedly")
|
|
538
|
+
self._pending.extend(self._parser.feed(data))
|
|
539
|
+
return self._pending.popleft()
|
|
540
|
+
|
|
541
|
+
def _notice(self, payload: bytes) -> None:
|
|
542
|
+
diag = errors.Diagnostic(P.parse_error_fields(payload))
|
|
543
|
+
for handler in self._notice_handlers:
|
|
544
|
+
handler(diag)
|
|
545
|
+
if not self._notice_handlers:
|
|
546
|
+
log.debug("server notice: %s", diag.message_primary)
|
|
547
|
+
|
|
548
|
+
def add_notice_handler(self, handler: Callable) -> None:
|
|
549
|
+
self._notice_handlers.append(handler)
|
|
550
|
+
|
|
551
|
+
def add_notify_handler(self, handler: Callable) -> None:
|
|
552
|
+
self._notify_handlers.append(handler)
|
|
553
|
+
|
|
554
|
+
def _handle_async_message(self, tag: int, payload: bytes) -> None:
|
|
555
|
+
if tag == 0x4E:
|
|
556
|
+
self._notice(payload)
|
|
557
|
+
elif tag == 0x53:
|
|
558
|
+
name, value = P.parse_parameter_status(payload)
|
|
559
|
+
self._server_params[name] = value
|
|
560
|
+
elif tag == 0x41:
|
|
561
|
+
note = P.parse_notification(payload)
|
|
562
|
+
if self._notify_handlers:
|
|
563
|
+
for handler in self._notify_handlers:
|
|
564
|
+
handler(note)
|
|
565
|
+
else:
|
|
566
|
+
self._notifies.append(note)
|
|
567
|
+
|
|
568
|
+
async def notifies(self, *, timeout: float | None = None, stop_after: int | None = None) -> AsyncIterator[P.Notify]:
|
|
569
|
+
"""``async for note in conn.notifies(timeout=5): ...`` (after ``LISTEN``)."""
|
|
570
|
+
count = 0
|
|
571
|
+
while stop_after is None or count < stop_after:
|
|
572
|
+
async with self._lock:
|
|
573
|
+
self._check_open()
|
|
574
|
+
if not self._notifies:
|
|
575
|
+
try:
|
|
576
|
+
await asyncio.wait_for(self._wait_notify(), timeout)
|
|
577
|
+
except asyncio.TimeoutError:
|
|
578
|
+
pass
|
|
579
|
+
note = self._notifies.popleft() if self._notifies else None
|
|
580
|
+
if note is None:
|
|
581
|
+
return
|
|
582
|
+
count += 1
|
|
583
|
+
yield note
|
|
584
|
+
|
|
585
|
+
async def _wait_notify(self) -> None:
|
|
586
|
+
# Never leaves a partial message behind: messages are only taken whole from the parser.
|
|
587
|
+
while not self._notifies:
|
|
588
|
+
tag, payload = await self._next_message_keep_open()
|
|
589
|
+
if tag == 0x45:
|
|
590
|
+
self._mark_broken()
|
|
591
|
+
raise errors.error_from_fields(P.parse_error_fields(payload))
|
|
592
|
+
self._handle_async_message(tag, payload)
|
|
593
|
+
|
|
594
|
+
async def _next_message_keep_open(self) -> tuple[int, bytes]:
|
|
595
|
+
"""Like _next_message, but a cancelled wait (timeout) does not poison the connection."""
|
|
596
|
+
while not self._pending:
|
|
597
|
+
reader = self._reader
|
|
598
|
+
if reader is None:
|
|
599
|
+
raise errors.OperationalError("the connection is closed")
|
|
600
|
+
try:
|
|
601
|
+
data = await reader.read(65536)
|
|
602
|
+
except (OSError, ConnectionError) as exc:
|
|
603
|
+
self._mark_broken()
|
|
604
|
+
raise errors.OperationalError(f"connection failed while receiving: {exc}") from exc
|
|
605
|
+
if not data:
|
|
606
|
+
self._mark_broken()
|
|
607
|
+
raise errors.OperationalError("server closed the connection unexpectedly")
|
|
608
|
+
self._pending.extend(self._parser.feed(data))
|
|
609
|
+
return self._pending.popleft()
|
|
610
|
+
|
|
611
|
+
async def _read_cycle(self) -> list[P.Result]:
|
|
612
|
+
collector = P.QueryCollector()
|
|
613
|
+
error: dict[str, str] | None = None
|
|
614
|
+
unsupported = False
|
|
615
|
+
while True:
|
|
616
|
+
tag, payload = await self._next_message()
|
|
617
|
+
if tag == 0x5A:
|
|
618
|
+
self._tx_status = chr(payload[0])
|
|
619
|
+
break
|
|
620
|
+
if tag == 0x45:
|
|
621
|
+
fields = P.parse_error_fields(payload)
|
|
622
|
+
error = error or fields
|
|
623
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
624
|
+
self._mark_broken()
|
|
625
|
+
raise errors.error_from_fields(fields)
|
|
626
|
+
elif tag in (0x4E, 0x53, 0x41):
|
|
627
|
+
self._handle_async_message(tag, payload)
|
|
628
|
+
elif tag == 0x47:
|
|
629
|
+
unsupported = True
|
|
630
|
+
await self._send(P.copy_fail("COPY needs cursor.copy()"))
|
|
631
|
+
elif tag in (0x48, 0x57, 0x64, 0x63):
|
|
632
|
+
unsupported = True
|
|
633
|
+
else:
|
|
634
|
+
collector.handle(tag, payload)
|
|
635
|
+
if unsupported:
|
|
636
|
+
raise errors.NotSupportedError("COPY must be run with cursor.copy(), not execute()")
|
|
637
|
+
if error is not None:
|
|
638
|
+
raise errors.error_from_fields(error)
|
|
639
|
+
return collector.results
|
|
640
|
+
|
|
641
|
+
def _check_open(self) -> None:
|
|
642
|
+
if self._closed:
|
|
643
|
+
raise errors.OperationalError("the connection is closed")
|
|
644
|
+
|
|
645
|
+
def _check_alive(self) -> bool:
|
|
646
|
+
reader, writer = self._reader, self._writer
|
|
647
|
+
if self._closed or reader is None or writer is None:
|
|
648
|
+
return False
|
|
649
|
+
if reader.at_eof() or writer.is_closing() or reader.exception() is not None:
|
|
650
|
+
self._mark_broken()
|
|
651
|
+
return False
|
|
652
|
+
# An idle connection must have nothing buffered. If the server shut it down, its FATAL
|
|
653
|
+
# ErrorResponse is sitting in the buffer (and at_eof() is False until it is read).
|
|
654
|
+
buffered = bytes(getattr(reader, "_buffer", b""))
|
|
655
|
+
if buffered:
|
|
656
|
+
try:
|
|
657
|
+
if any(tag == 0x45 for tag, _ in P.MessageParser().feed(buffered)):
|
|
658
|
+
self._mark_broken()
|
|
659
|
+
return False
|
|
660
|
+
except errors.Error:
|
|
661
|
+
self._mark_broken()
|
|
662
|
+
return False
|
|
663
|
+
return True
|
|
664
|
+
|
|
665
|
+
# ---------------------------------------------------------------- execution
|
|
666
|
+
async def _simple(self, sql: str) -> list[P.Result]:
|
|
667
|
+
await self._send(P.query_message(sql))
|
|
668
|
+
return await self._read_cycle()
|
|
669
|
+
|
|
670
|
+
async def _send_query(self, sql: str, params: Any, *, inline: bool = False) -> None:
|
|
671
|
+
begin = not self._autocommit and self._tx_status == _IDLE
|
|
672
|
+
if inline and params is not None:
|
|
673
|
+
sql, params = _query.translate(sql, params, _types.render_literal)[0], None
|
|
674
|
+
body = P.build_query(sql, params, _query.translate, _types.dump)
|
|
675
|
+
if begin:
|
|
676
|
+
await self._send(P.query_message("BEGIN") + body)
|
|
677
|
+
try:
|
|
678
|
+
await self._read_cycle()
|
|
679
|
+
except BaseException:
|
|
680
|
+
self._mark_broken()
|
|
681
|
+
raise
|
|
682
|
+
else:
|
|
683
|
+
await self._send(body)
|
|
684
|
+
|
|
685
|
+
def _query_text(self, query: Any) -> tuple[str, str]:
|
|
686
|
+
if isinstance(query, Composable):
|
|
687
|
+
return query.as_string(), query.as_redacted()
|
|
688
|
+
if isinstance(query, str):
|
|
689
|
+
return query, query
|
|
690
|
+
if isinstance(query, (bytes, bytearray)):
|
|
691
|
+
text = bytes(query).decode()
|
|
692
|
+
return text, text
|
|
693
|
+
raise TypeError(f"the query must be str, bytes or sql.Composable, not {type(query).__name__}")
|
|
694
|
+
|
|
695
|
+
async def _start_copy(self, query: Any, params: Any, stack: AsyncExitStack) -> str:
|
|
696
|
+
sql, redacted = self._query_text(query)
|
|
697
|
+
self._check_open()
|
|
698
|
+
if hooks._HOOKS:
|
|
699
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
700
|
+
for hook in tuple(hooks._HOOKS):
|
|
701
|
+
try:
|
|
702
|
+
cm = hook(info)
|
|
703
|
+
if isinstance(cm, AbstractAsyncContextManager):
|
|
704
|
+
await stack.enter_async_context(cm)
|
|
705
|
+
else:
|
|
706
|
+
stack.enter_context(cm)
|
|
707
|
+
except Exception:
|
|
708
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
709
|
+
await self._send_query(sql, params, inline=True)
|
|
710
|
+
error: dict[str, str] | None = None
|
|
711
|
+
while True:
|
|
712
|
+
tag, payload = await self._next_message()
|
|
713
|
+
if error is None and tag in (0x47, 0x48):
|
|
714
|
+
return "in" if tag == 0x47 else "out"
|
|
715
|
+
if tag == 0x45:
|
|
716
|
+
fields = P.parse_error_fields(payload)
|
|
717
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
718
|
+
self._mark_broken()
|
|
719
|
+
raise errors.error_from_fields(fields)
|
|
720
|
+
error = error or fields
|
|
721
|
+
elif tag == 0x5A:
|
|
722
|
+
self._tx_status = chr(payload[0])
|
|
723
|
+
if error is not None:
|
|
724
|
+
raise errors.error_from_fields(error)
|
|
725
|
+
raise errors.ProgrammingError("the statement is not a COPY ... FROM STDIN / TO STDOUT")
|
|
726
|
+
else:
|
|
727
|
+
self._handle_async_message(tag, payload)
|
|
728
|
+
|
|
729
|
+
async def _prime_oids(self, oids: Iterable[int]) -> None:
|
|
730
|
+
if not _types.has_loaders():
|
|
731
|
+
return
|
|
732
|
+
wanted = set(oids) - self._loader_cache.keys()
|
|
733
|
+
if not wanted:
|
|
734
|
+
return
|
|
735
|
+
ids = ", ".join(str(int(o)) for o in sorted(wanted))
|
|
736
|
+
rows = (await self._simple(PRIME_SQL.format(ids=ids)))[0].rows
|
|
737
|
+
_types.resolve_loaders(rows, self._loader_cache, sorted(wanted))
|
|
738
|
+
|
|
739
|
+
async def _prime_loaders(self, results: list[P.Result]) -> None:
|
|
740
|
+
await self._prime_oids(c.type_code for r in results if r.columns for c in r.columns)
|
|
741
|
+
|
|
742
|
+
async def _describe(self, sql: str) -> tuple[list[int], list[P.Column] | None]:
|
|
743
|
+
await self._send(P.describe_statement(sql))
|
|
744
|
+
param_oids: list[int] = []
|
|
745
|
+
columns: list[P.Column] | None = None
|
|
746
|
+
error: dict[str, str] | None = None
|
|
747
|
+
while True:
|
|
748
|
+
tag, payload = await self._next_message()
|
|
749
|
+
if tag == 0x5A:
|
|
750
|
+
self._tx_status = chr(payload[0])
|
|
751
|
+
break
|
|
752
|
+
if tag == 0x74:
|
|
753
|
+
param_oids = P.parse_param_description(payload)
|
|
754
|
+
elif tag == 0x54:
|
|
755
|
+
columns = P.parse_row_description(payload)
|
|
756
|
+
elif tag == 0x45:
|
|
757
|
+
fields = P.parse_error_fields(payload)
|
|
758
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
759
|
+
self._mark_broken()
|
|
760
|
+
raise errors.error_from_fields(fields)
|
|
761
|
+
error = error or fields
|
|
762
|
+
elif tag in (0x31, 0x6E):
|
|
763
|
+
pass
|
|
764
|
+
else:
|
|
765
|
+
self._handle_async_message(tag, payload)
|
|
766
|
+
if error is not None:
|
|
767
|
+
raise errors.error_from_fields(error)
|
|
768
|
+
return param_oids, columns
|
|
769
|
+
|
|
770
|
+
async def _run_binary(self, sql: str, params: Any) -> list[P.Result]:
|
|
771
|
+
begin = not self._autocommit and self._tx_status == _IDLE
|
|
772
|
+
if begin:
|
|
773
|
+
await self._simple("BEGIN")
|
|
774
|
+
values: list[Any]
|
|
775
|
+
if params is None:
|
|
776
|
+
pg_sql, values = sql, []
|
|
777
|
+
else:
|
|
778
|
+
pg_sql, values = _query.translate(sql, params)
|
|
779
|
+
param_oids, columns = await self._describe(pg_sql)
|
|
780
|
+
if len(param_oids) != len(values):
|
|
781
|
+
raise errors.ProgrammingError(
|
|
782
|
+
f"the query has {len(param_oids)} parameters but {len(values)} were passed")
|
|
783
|
+
pformats, payloads = _types.binary_param_plan(param_oids, values)
|
|
784
|
+
rformats = None
|
|
785
|
+
if columns is not None:
|
|
786
|
+
await self._prime_oids(c.type_code for c in columns)
|
|
787
|
+
rformats = [1 if _binary.scalar_decoder(c.type_code) is not None and self._loader_cache.get(c.type_code) is None
|
|
788
|
+
else 0 for c in columns]
|
|
789
|
+
await self._send(P.extended_query(pg_sql, param_oids, payloads, pformats, rformats))
|
|
790
|
+
results = await self._read_cycle()
|
|
791
|
+
for r in results:
|
|
792
|
+
if r.columns is not None:
|
|
793
|
+
r.formats = rformats
|
|
794
|
+
return results
|
|
795
|
+
|
|
796
|
+
async def _run_text(self, sql: str, params: Any) -> list[P.Result]:
|
|
797
|
+
await self._send_query(sql, params)
|
|
798
|
+
return await self._read_cycle()
|
|
799
|
+
|
|
800
|
+
def _check_no_pipeline(self) -> None:
|
|
801
|
+
if self._pipeline is not None:
|
|
802
|
+
raise errors.ProgrammingError(
|
|
803
|
+
"a pipeline is active on this connection: queue statements with pipe.execute() "
|
|
804
|
+
"or leave the pipeline block first")
|
|
805
|
+
|
|
806
|
+
async def _execute(self, query: Any, params: Any, binary: bool = False) -> list[P.Result]:
|
|
807
|
+
sql, redacted = self._query_text(query)
|
|
808
|
+
run = self._run_binary if binary else self._run_text
|
|
809
|
+
async with self._lock:
|
|
810
|
+
self._check_open()
|
|
811
|
+
self._check_no_pipeline()
|
|
812
|
+
if not hooks._HOOKS:
|
|
813
|
+
results = await run(sql, params)
|
|
814
|
+
else:
|
|
815
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
816
|
+
from contextlib import ExitStack
|
|
817
|
+
with ExitStack() as stack:
|
|
818
|
+
for hook in tuple(hooks._HOOKS):
|
|
819
|
+
try:
|
|
820
|
+
stack.enter_context(hook(info))
|
|
821
|
+
except Exception:
|
|
822
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
823
|
+
results = await run(sql, params)
|
|
824
|
+
info.rowcount = results[-1].rowcount if results else -1
|
|
825
|
+
await self._prime_loaders(results)
|
|
826
|
+
return results
|
|
827
|
+
|
|
828
|
+
async def _run_many(self, sql: str, params_seq: Iterable[Any]) -> int:
|
|
829
|
+
"""``executemany`` as one pipeline (async twin of ``Connection._run_many``): every statement is
|
|
830
|
+
sent before its reply is read, one transaction per batch (BEGIN..COMMIT under autocommit,
|
|
831
|
+
the implicit transaction otherwise), replies read through a sliding window with Flush."""
|
|
832
|
+
from contextlib import ExitStack
|
|
833
|
+
|
|
834
|
+
from .pipeline import _encode # internal helper; imported here to avoid a cycle
|
|
835
|
+
|
|
836
|
+
window = 64
|
|
837
|
+
pg_sql, redacted = self._query_text(sql)
|
|
838
|
+
async with self._lock:
|
|
839
|
+
self._check_open()
|
|
840
|
+
self._check_no_pipeline()
|
|
841
|
+
own_tx = self._autocommit and self._tx_status == _IDLE
|
|
842
|
+
if own_tx or (not self._autocommit and self._tx_status == _IDLE):
|
|
843
|
+
await self._send(P.query_message("BEGIN"))
|
|
844
|
+
await self._read_cycle()
|
|
845
|
+
total = 0
|
|
846
|
+
in_flight: collections.deque = collections.deque()
|
|
847
|
+
failure: BaseException | None = None
|
|
848
|
+
sent_sync = False
|
|
849
|
+
|
|
850
|
+
def enter_hooks() -> tuple[ExitStack, Any]:
|
|
851
|
+
stack = ExitStack()
|
|
852
|
+
info = None
|
|
853
|
+
if hooks._HOOKS:
|
|
854
|
+
info = hooks.QueryInfo(redacted, self._host, self._port, self._params.dbname, self._params.user)
|
|
855
|
+
for hook in tuple(hooks._HOOKS):
|
|
856
|
+
try:
|
|
857
|
+
stack.enter_context(hook(info))
|
|
858
|
+
except Exception:
|
|
859
|
+
log.exception("a pgtern query hook failed on entry; ignoring it")
|
|
860
|
+
return stack, info
|
|
861
|
+
|
|
862
|
+
async def read_one() -> errors.Error | None:
|
|
863
|
+
nonlocal total
|
|
864
|
+
stack, info = in_flight.popleft()
|
|
865
|
+
results, error = await AsyncPipeline._read_reply(self)
|
|
866
|
+
if results is not None:
|
|
867
|
+
rc = results[-1].rowcount if results else -1
|
|
868
|
+
if info is not None:
|
|
869
|
+
info.rowcount = rc
|
|
870
|
+
if rc > 0:
|
|
871
|
+
total += rc
|
|
872
|
+
if error is None:
|
|
873
|
+
stack.__exit__(None, None, None)
|
|
874
|
+
else:
|
|
875
|
+
stack.__exit__(type(error), error, None)
|
|
876
|
+
return error
|
|
877
|
+
|
|
878
|
+
try:
|
|
879
|
+
pending: list[bytes] = [] # encoded statements not yet handed to the socket
|
|
880
|
+
for params in params_seq:
|
|
881
|
+
pending.append(_encode(pg_sql, params)) # no Sync: the batch ends with one
|
|
882
|
+
in_flight.append(enter_hooks())
|
|
883
|
+
if len(pending) >= window: # coalesce: one socket write per window, not per statement
|
|
884
|
+
self._write(b"".join(pending))
|
|
885
|
+
pending.clear()
|
|
886
|
+
if len(in_flight) > window:
|
|
887
|
+
self._write(b"".join(pending) + P.FLUSH) # Flush: the server sends replies so far
|
|
888
|
+
pending.clear()
|
|
889
|
+
await self._drain()
|
|
890
|
+
while len(in_flight) > window // 2:
|
|
891
|
+
failure = await read_one()
|
|
892
|
+
if failure is not None:
|
|
893
|
+
break # statements sent after a failure are skipped by the server
|
|
894
|
+
if failure is not None:
|
|
895
|
+
break
|
|
896
|
+
if failure is not None:
|
|
897
|
+
pending.clear() # the server would skip these anyway
|
|
898
|
+
self._write(b"".join(pending) + P.SYNC)
|
|
899
|
+
pending.clear()
|
|
900
|
+
await self._drain()
|
|
901
|
+
sent_sync = True
|
|
902
|
+
while failure is None and in_flight:
|
|
903
|
+
failure = await read_one()
|
|
904
|
+
except BaseException as exc:
|
|
905
|
+
failure = exc
|
|
906
|
+
if not self._closed:
|
|
907
|
+
try:
|
|
908
|
+
if not sent_sync:
|
|
909
|
+
await self._send(P.SYNC)
|
|
910
|
+
while True:
|
|
911
|
+
tag, payload = await self._next_message()
|
|
912
|
+
if tag == 0x5A:
|
|
913
|
+
self._tx_status = chr(payload[0])
|
|
914
|
+
break
|
|
915
|
+
if own_tx and self._tx_status != _IDLE:
|
|
916
|
+
await self._simple("ROLLBACK")
|
|
917
|
+
except Exception:
|
|
918
|
+
self._mark_broken()
|
|
919
|
+
raise
|
|
920
|
+
finally:
|
|
921
|
+
while in_flight:
|
|
922
|
+
stack, _ = in_flight.popleft()
|
|
923
|
+
if failure is None:
|
|
924
|
+
stack.__exit__(None, None, None)
|
|
925
|
+
else:
|
|
926
|
+
stack.__exit__(type(failure), failure, None)
|
|
927
|
+
while True:
|
|
928
|
+
tag, payload = await self._next_message()
|
|
929
|
+
if tag == 0x5A:
|
|
930
|
+
self._tx_status = chr(payload[0])
|
|
931
|
+
break
|
|
932
|
+
if tag == 0x45:
|
|
933
|
+
fields = P.parse_error_fields(payload)
|
|
934
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
935
|
+
self._mark_broken()
|
|
936
|
+
raise errors.error_from_fields(fields)
|
|
937
|
+
else:
|
|
938
|
+
self._handle_async_message(tag, payload)
|
|
939
|
+
if own_tx:
|
|
940
|
+
await self._simple("ROLLBACK" if failure is not None else "COMMIT")
|
|
941
|
+
if failure is not None:
|
|
942
|
+
raise failure
|
|
943
|
+
return total
|
|
944
|
+
|
|
945
|
+
# ---------------------------------------------------------------- public API
|
|
946
|
+
def cursor(self, name: str | None = None, *, row_factory: Callable | None = None,
|
|
947
|
+
scrollable: bool | None = None, withhold: bool = False) -> AsyncCursor:
|
|
948
|
+
if name is not None:
|
|
949
|
+
return AsyncServerCursor(self, name, row_factory=row_factory, scrollable=scrollable, withhold=withhold)
|
|
950
|
+
return AsyncCursor(self, row_factory=row_factory)
|
|
951
|
+
|
|
952
|
+
async def execute(self, query: Any, params: Any = None, *, prepare: bool | None = None,
|
|
953
|
+
binary: bool | None = None) -> AsyncCursor:
|
|
954
|
+
return await self.cursor().execute(query, params, binary=bool(binary))
|
|
955
|
+
|
|
956
|
+
@property
|
|
957
|
+
def closed(self) -> bool:
|
|
958
|
+
return self._closed
|
|
959
|
+
|
|
960
|
+
@property
|
|
961
|
+
def autocommit(self) -> bool:
|
|
962
|
+
return self._autocommit
|
|
963
|
+
|
|
964
|
+
async def set_autocommit(self, value: bool) -> None:
|
|
965
|
+
async with self._lock:
|
|
966
|
+
if bool(value) != self._autocommit and self._tx_status not in (_IDLE, _BROKEN):
|
|
967
|
+
raise errors.ProgrammingError("can't change 'autocommit' now: connection in a transaction")
|
|
968
|
+
self._autocommit = bool(value)
|
|
969
|
+
|
|
970
|
+
async def commit(self) -> None:
|
|
971
|
+
async with self._lock:
|
|
972
|
+
self._check_open()
|
|
973
|
+
self._check_no_pipeline()
|
|
974
|
+
if self._tx_depth:
|
|
975
|
+
raise errors.ProgrammingError("explicit commit() is forbidden inside a transaction() block")
|
|
976
|
+
if self._tx_status != _IDLE:
|
|
977
|
+
await self._simple("COMMIT")
|
|
978
|
+
|
|
979
|
+
async def rollback(self) -> None:
|
|
980
|
+
async with self._lock:
|
|
981
|
+
self._check_open()
|
|
982
|
+
self._check_no_pipeline()
|
|
983
|
+
if self._tx_depth:
|
|
984
|
+
raise errors.ProgrammingError("explicit rollback() is forbidden inside a transaction() block")
|
|
985
|
+
if self._tx_status != _IDLE:
|
|
986
|
+
await self._simple("ROLLBACK")
|
|
987
|
+
|
|
988
|
+
def transaction(self, savepoint_name: str | None = None, force_rollback: bool = False) -> AsyncTransaction:
|
|
989
|
+
self._check_no_pipeline()
|
|
990
|
+
return AsyncTransaction(self, savepoint_name, force_rollback)
|
|
991
|
+
|
|
992
|
+
def pipeline(self, batch_size: int = 64) -> "AsyncPipeline":
|
|
993
|
+
"""Pipeline mode (asyncio). See :mod:`pgtern.pipeline`."""
|
|
994
|
+
return AsyncPipeline(self, batch_size)
|
|
995
|
+
|
|
996
|
+
async def _reset_for_pool(self) -> None:
|
|
997
|
+
async with self._lock:
|
|
998
|
+
self._loader_cache.clear()
|
|
999
|
+
self._tx_depth = 0
|
|
1000
|
+
if not self._closed and self._tx_status != _IDLE:
|
|
1001
|
+
await self._simple("ROLLBACK")
|
|
1002
|
+
|
|
1003
|
+
async def close(self) -> None:
|
|
1004
|
+
if self._closed:
|
|
1005
|
+
return
|
|
1006
|
+
writer = self._writer
|
|
1007
|
+
self._closed = True
|
|
1008
|
+
self._tx_status = _BROKEN
|
|
1009
|
+
self._reader = self._writer = None
|
|
1010
|
+
if writer is not None:
|
|
1011
|
+
try:
|
|
1012
|
+
writer.write(P.TERMINATE)
|
|
1013
|
+
await writer.drain()
|
|
1014
|
+
except (OSError, ConnectionError):
|
|
1015
|
+
pass
|
|
1016
|
+
writer.close()
|
|
1017
|
+
try:
|
|
1018
|
+
await writer.wait_closed()
|
|
1019
|
+
except (OSError, ConnectionError):
|
|
1020
|
+
pass
|
|
1021
|
+
|
|
1022
|
+
async def __aenter__(self) -> "AsyncConnection":
|
|
1023
|
+
return self
|
|
1024
|
+
|
|
1025
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
1026
|
+
try:
|
|
1027
|
+
if not self._closed:
|
|
1028
|
+
if exc_type is None:
|
|
1029
|
+
await self.commit()
|
|
1030
|
+
elif exc_type is asyncio.CancelledError:
|
|
1031
|
+
self._mark_broken()
|
|
1032
|
+
else:
|
|
1033
|
+
try:
|
|
1034
|
+
await self.rollback()
|
|
1035
|
+
except Exception:
|
|
1036
|
+
log.warning("rollback failed while leaving a connection block", exc_info=True)
|
|
1037
|
+
finally:
|
|
1038
|
+
await self.close()
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
class AsyncPipeline:
|
|
1042
|
+
"""asyncio pipeline: ``async with conn.pipeline() as pipe: h = await pipe.execute(...)``.
|
|
1043
|
+
Same semantics as :class:`pgtern.pipeline.Pipeline`."""
|
|
1044
|
+
|
|
1045
|
+
def __init__(self, conn: AsyncConnection, batch_size: int = 64) -> None:
|
|
1046
|
+
if batch_size < 1:
|
|
1047
|
+
raise ValueError("batch_size must be at least 1")
|
|
1048
|
+
self._conn = conn
|
|
1049
|
+
self._batch_size = batch_size
|
|
1050
|
+
self._queue: list[_Item] = []
|
|
1051
|
+
self._begin_queued = False
|
|
1052
|
+
self._open = False
|
|
1053
|
+
|
|
1054
|
+
async def __aenter__(self) -> "AsyncPipeline":
|
|
1055
|
+
conn = self._conn
|
|
1056
|
+
async with conn._lock:
|
|
1057
|
+
conn._check_open()
|
|
1058
|
+
if conn._pipeline is not None:
|
|
1059
|
+
raise errors.ProgrammingError("a pipeline is already active on this connection")
|
|
1060
|
+
conn._pipeline = self
|
|
1061
|
+
self._open = True
|
|
1062
|
+
return self
|
|
1063
|
+
|
|
1064
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
1065
|
+
try:
|
|
1066
|
+
if exc_type is None:
|
|
1067
|
+
await self.sync()
|
|
1068
|
+
elif not self._conn.closed:
|
|
1069
|
+
try:
|
|
1070
|
+
await self.sync()
|
|
1071
|
+
except Exception: # noqa: BLE001 - the body's exception is the one to report
|
|
1072
|
+
pass
|
|
1073
|
+
finally:
|
|
1074
|
+
self._open = False
|
|
1075
|
+
if self._conn._pipeline is self:
|
|
1076
|
+
self._conn._pipeline = None
|
|
1077
|
+
|
|
1078
|
+
async def execute(self, query: Any, params: Any = None) -> AsyncCursor:
|
|
1079
|
+
conn = self._conn
|
|
1080
|
+
if self._open and len(self._queue) >= self._batch_size:
|
|
1081
|
+
await self.sync() # before taking the lock: sync() takes it itself
|
|
1082
|
+
async with conn._lock:
|
|
1083
|
+
conn._check_open()
|
|
1084
|
+
if not self._open:
|
|
1085
|
+
raise errors.ProgrammingError("the pipeline is not active (use it in an 'async with' block)")
|
|
1086
|
+
sql, redacted = conn._query_text(query)
|
|
1087
|
+
if not conn._autocommit and conn._tx_status == _IDLE and not self._begin_queued:
|
|
1088
|
+
await conn._send(P.extended_query("BEGIN", [], [], sync=False))
|
|
1089
|
+
self._queue.append(_Item(None, "BEGIN", "BEGIN", hidden=True))
|
|
1090
|
+
self._begin_queued = True
|
|
1091
|
+
cursor = AsyncCursor(conn)
|
|
1092
|
+
cursor._pipeline_pending = True
|
|
1093
|
+
await conn._send(_encode(sql, params))
|
|
1094
|
+
self._queue.append(_Item(cursor, sql, redacted))
|
|
1095
|
+
return cursor
|
|
1096
|
+
|
|
1097
|
+
async def sync(self) -> None:
|
|
1098
|
+
conn = self._conn
|
|
1099
|
+
async with conn._lock:
|
|
1100
|
+
conn._check_open()
|
|
1101
|
+
items, self._queue = self._queue, []
|
|
1102
|
+
self._begin_queued = False
|
|
1103
|
+
await conn._send(P.SYNC)
|
|
1104
|
+
failure: errors.Error | None = None
|
|
1105
|
+
# Items are shared with the sync pipeline (typed Cursor); here the cursor is always an AsyncCursor.
|
|
1106
|
+
collected: list[tuple[Any, list]] = []
|
|
1107
|
+
for item in items:
|
|
1108
|
+
if failure is not None:
|
|
1109
|
+
if item.cursor is not None:
|
|
1110
|
+
item.cursor._pipeline_pending = False
|
|
1111
|
+
item.cursor._pipeline_error = errors.PipelineAborted(
|
|
1112
|
+
"not executed: an earlier statement in the pipeline failed")
|
|
1113
|
+
continue
|
|
1114
|
+
info, entered = _enter_hooks(conn, item)
|
|
1115
|
+
results, error = await self._read_reply(conn)
|
|
1116
|
+
if info is not None and results is not None:
|
|
1117
|
+
info.rowcount = results[-1].rowcount if results else -1
|
|
1118
|
+
_exit_hooks(entered, error)
|
|
1119
|
+
if error is not None:
|
|
1120
|
+
failure = error
|
|
1121
|
+
if item.cursor is not None:
|
|
1122
|
+
item.cursor._pipeline_pending = False
|
|
1123
|
+
item.cursor._pipeline_error = error
|
|
1124
|
+
elif item.cursor is not None and results is not None:
|
|
1125
|
+
collected.append((item.cursor, results))
|
|
1126
|
+
while True:
|
|
1127
|
+
tag, payload = await conn._next_message()
|
|
1128
|
+
if tag == 0x5A:
|
|
1129
|
+
conn._tx_status = chr(payload[0])
|
|
1130
|
+
break
|
|
1131
|
+
if tag == 0x45:
|
|
1132
|
+
fields = P.parse_error_fields(payload)
|
|
1133
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
1134
|
+
conn._mark_broken()
|
|
1135
|
+
raise errors.error_from_fields(fields)
|
|
1136
|
+
else:
|
|
1137
|
+
conn._handle_async_message(tag, payload)
|
|
1138
|
+
await conn._prime_oids(c.type_code for _, res in collected for r in res if r.columns for c in r.columns)
|
|
1139
|
+
for cursor, res in collected:
|
|
1140
|
+
cursor._pipeline_pending = False
|
|
1141
|
+
cursor._set_results(res)
|
|
1142
|
+
|
|
1143
|
+
@staticmethod
|
|
1144
|
+
async def _read_reply(conn: AsyncConnection) -> tuple[list | None, errors.Error | None]:
|
|
1145
|
+
collector = P.QueryCollector()
|
|
1146
|
+
while True:
|
|
1147
|
+
tag, payload = await conn._next_message()
|
|
1148
|
+
if tag == 0x45:
|
|
1149
|
+
fields = P.parse_error_fields(payload)
|
|
1150
|
+
if fields.get("V", fields.get("S")) in ("FATAL", "PANIC"):
|
|
1151
|
+
conn._mark_broken()
|
|
1152
|
+
raise errors.error_from_fields(fields)
|
|
1153
|
+
return None, errors.error_from_fields(fields)
|
|
1154
|
+
if tag in (0x43, 0x49):
|
|
1155
|
+
collector.handle(tag, payload)
|
|
1156
|
+
return collector.results, None
|
|
1157
|
+
if tag in (0x4E, 0x53, 0x41):
|
|
1158
|
+
conn._handle_async_message(tag, payload)
|
|
1159
|
+
else:
|
|
1160
|
+
collector.handle(tag, payload)
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
async def connect(conninfo: str = "", **kwargs: Any) -> AsyncConnection:
|
|
1164
|
+
"""Open an :class:`AsyncConnection` (same arguments as :func:`pgtern.connect`)."""
|
|
1165
|
+
return await AsyncConnection.connect(conninfo, **kwargs)
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
class AsyncConnectionPool:
|
|
1169
|
+
"""asyncio connection pool: ``min_size`` kept warm, up to ``max_size`` on demand.
|
|
1170
|
+
|
|
1171
|
+
Connections are liveness-checked on checkout, replaced when dead or older than
|
|
1172
|
+
``max_lifetime``, and rolled back when returned mid-transaction.
|
|
1173
|
+
"""
|
|
1174
|
+
|
|
1175
|
+
def __init__(self, conninfo: str = "", *, kwargs: dict[str, Any] | None = None, min_size: int = 4,
|
|
1176
|
+
max_size: int | None = None, timeout: float = 30.0, max_lifetime: float = 3600.0,
|
|
1177
|
+
check: Callable | None = None, name: str | None = None) -> None:
|
|
1178
|
+
max_size = min_size if max_size is None else max_size
|
|
1179
|
+
if min_size < 0 or max_size < 1 or max_size < min_size:
|
|
1180
|
+
raise ValueError(f"bad pool sizes: min_size={min_size}, max_size={max_size}")
|
|
1181
|
+
self.conninfo, self.kwargs = conninfo, dict(kwargs or {})
|
|
1182
|
+
self.min_size, self.max_size = min_size, max_size
|
|
1183
|
+
self.timeout, self.max_lifetime = timeout, max_lifetime
|
|
1184
|
+
self.name = name or f"apool-{id(self):x}"
|
|
1185
|
+
self._check = check
|
|
1186
|
+
self._idle: collections.deque[AsyncConnection] = collections.deque()
|
|
1187
|
+
self._slots: asyncio.Semaphore | None = None
|
|
1188
|
+
self._closed = True
|
|
1189
|
+
self._default_autocommit = bool(self.kwargs.get("autocommit", False))
|
|
1190
|
+
|
|
1191
|
+
async def open(self, wait: bool = True) -> None:
|
|
1192
|
+
if not self._closed:
|
|
1193
|
+
return
|
|
1194
|
+
self._closed = False
|
|
1195
|
+
self._slots = asyncio.Semaphore(self.max_size)
|
|
1196
|
+
if wait:
|
|
1197
|
+
conns = [await self._new() for _ in range(self.min_size)]
|
|
1198
|
+
self._idle.extend(conns)
|
|
1199
|
+
|
|
1200
|
+
async def _new(self) -> AsyncConnection:
|
|
1201
|
+
return await connect(self.conninfo, **self.kwargs)
|
|
1202
|
+
|
|
1203
|
+
async def getconn(self, timeout: float | None = None) -> AsyncConnection:
|
|
1204
|
+
if self._closed or self._slots is None:
|
|
1205
|
+
raise PoolClosed("the pool is closed")
|
|
1206
|
+
wait = self.timeout if timeout is None else timeout
|
|
1207
|
+
try:
|
|
1208
|
+
await asyncio.wait_for(self._slots.acquire(), wait)
|
|
1209
|
+
except asyncio.TimeoutError:
|
|
1210
|
+
raise PoolTimeout(f"couldn't get a connection after {wait:.2f} sec") from None
|
|
1211
|
+
try:
|
|
1212
|
+
while self._idle:
|
|
1213
|
+
conn = self._idle.popleft()
|
|
1214
|
+
if conn._check_alive() and time.monotonic() - conn.created < self.max_lifetime:
|
|
1215
|
+
if await self._passes_check(conn):
|
|
1216
|
+
return conn
|
|
1217
|
+
await conn.close()
|
|
1218
|
+
conn = await asyncio.wait_for(self._new(), wait)
|
|
1219
|
+
return conn
|
|
1220
|
+
except BaseException:
|
|
1221
|
+
self._slots.release()
|
|
1222
|
+
raise
|
|
1223
|
+
|
|
1224
|
+
async def _passes_check(self, conn: AsyncConnection) -> bool:
|
|
1225
|
+
if self._check is None:
|
|
1226
|
+
return True
|
|
1227
|
+
try:
|
|
1228
|
+
await self._check(conn)
|
|
1229
|
+
return True
|
|
1230
|
+
except Exception:
|
|
1231
|
+
return False
|
|
1232
|
+
|
|
1233
|
+
async def putconn(self, conn: AsyncConnection) -> None:
|
|
1234
|
+
assert self._slots is not None
|
|
1235
|
+
try:
|
|
1236
|
+
if not self._closed and not conn.closed:
|
|
1237
|
+
try:
|
|
1238
|
+
await conn._reset_for_pool()
|
|
1239
|
+
if conn.autocommit != self._default_autocommit:
|
|
1240
|
+
await conn.set_autocommit(self._default_autocommit)
|
|
1241
|
+
except Exception:
|
|
1242
|
+
await conn.close()
|
|
1243
|
+
if self._closed or conn.closed or time.monotonic() - conn.created >= self.max_lifetime:
|
|
1244
|
+
await conn.close()
|
|
1245
|
+
else:
|
|
1246
|
+
self._idle.append(conn)
|
|
1247
|
+
finally:
|
|
1248
|
+
self._slots.release()
|
|
1249
|
+
|
|
1250
|
+
@asynccontextmanager
|
|
1251
|
+
async def connection(self, timeout: float | None = None) -> AsyncIterator[AsyncConnection]:
|
|
1252
|
+
"""Check a connection out; commit on success, roll back on error, then return it."""
|
|
1253
|
+
conn = await self.getconn(timeout)
|
|
1254
|
+
try:
|
|
1255
|
+
try:
|
|
1256
|
+
yield conn
|
|
1257
|
+
except BaseException:
|
|
1258
|
+
if conn.closed or isinstance(_current_exc(), asyncio.CancelledError):
|
|
1259
|
+
conn._mark_broken()
|
|
1260
|
+
else:
|
|
1261
|
+
try:
|
|
1262
|
+
await conn.rollback()
|
|
1263
|
+
except Exception:
|
|
1264
|
+
await conn.close()
|
|
1265
|
+
raise
|
|
1266
|
+
else:
|
|
1267
|
+
if not conn.closed:
|
|
1268
|
+
await conn.commit()
|
|
1269
|
+
finally:
|
|
1270
|
+
await self.putconn(conn)
|
|
1271
|
+
|
|
1272
|
+
async def close(self) -> None:
|
|
1273
|
+
self._closed = True
|
|
1274
|
+
while self._idle:
|
|
1275
|
+
await self._idle.popleft().close()
|
|
1276
|
+
|
|
1277
|
+
async def __aenter__(self) -> "AsyncConnectionPool":
|
|
1278
|
+
await self.open()
|
|
1279
|
+
return self
|
|
1280
|
+
|
|
1281
|
+
async def __aexit__(self, *exc: Any) -> None:
|
|
1282
|
+
await self.close()
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
def _current_exc() -> BaseException | None:
|
|
1286
|
+
import sys
|
|
1287
|
+
return sys.exc_info()[1]
|