psycopg 3.3.4__tar.gz → 3.3.6__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.
- {psycopg-3.3.4 → psycopg-3.3.6}/PKG-INFO +8 -7
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_adapters_map.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_compat.py +9 -6
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_connection_base.py +15 -8
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy_base.py +9 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_encodings.py +52 -53
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_preparing.py +10 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_py_transformer.py +10 -12
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tstrings.py +4 -4
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_typeinfo.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_typemod.py +18 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/abc.py +7 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/connection.py +45 -23
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/connection_async.py +48 -24
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/conninfo.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/_types.py +3 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/errors.py +8 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/generators.py +13 -16
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/postgres.py +3 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_debug.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/misc.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/rows.py +4 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/sql.py +3 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/array.py +1 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/datetime.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/json.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/multirange.py +11 -5
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/net.py +6 -12
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/numeric.py +48 -4
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/string.py +1 -3
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/uuid.py +1 -1
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/waiting.py +184 -84
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/PKG-INFO +8 -7
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/requires.txt +6 -6
- {psycopg-3.3.4 → psycopg-3.3.6}/pyproject.toml +8 -7
- {psycopg-3.3.4 → psycopg-3.3.6}/LICENSE.txt +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/README.rst +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/__init__.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_acompat.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_capabilities.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_cmodule.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_column.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_connection_info.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_attempts.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_attempts_async.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_conninfo_utils.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_copy_async.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_cursor_base.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_dns.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_enums.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_oids.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline_async.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_pipeline_base.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_queries.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor_async.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_server_cursor_base.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_struct.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tpc.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_transformer.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_tz.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/_wrappers.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/adapt.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/client_cursor.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/copy.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/__init__.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/crdb/connection.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/cursor.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/cursor_async.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/dbapi20.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/__init__.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_enums.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_pq_ctypes.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/_pq_ctypes.pyi +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/abc.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/pq/pq_ctypes.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/py.typed +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/raw_cursor.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/transaction.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/__init__.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/bool.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/composite.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/enum.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/hstore.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/none.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/numpy.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/range.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/types/shapely.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg/version.py +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/SOURCES.txt +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/dependency_links.txt +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/not-zip-safe +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/psycopg.egg-info/top_level.txt +0 -0
- {psycopg-3.3.4 → psycopg-3.3.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psycopg
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.6
|
|
4
4
|
Summary: PostgreSQL database adapter for Python
|
|
5
5
|
Author-email: Daniele Varrazzo <daniele.varrazzo@gmail.com>
|
|
6
6
|
License-Expression: LGPL-3.0-only
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
26
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
26
27
|
Classifier: Topic :: Database
|
|
@@ -33,14 +34,14 @@ License-File: LICENSE.txt
|
|
|
33
34
|
Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
|
|
34
35
|
Requires-Dist: tzdata; sys_platform == "win32"
|
|
35
36
|
Provides-Extra: c
|
|
36
|
-
Requires-Dist: psycopg-c==3.3.
|
|
37
|
+
Requires-Dist: psycopg-c==3.3.6; implementation_name != "pypy" and extra == "c"
|
|
37
38
|
Provides-Extra: binary
|
|
38
|
-
Requires-Dist: psycopg-binary==3.3.
|
|
39
|
+
Requires-Dist: psycopg-binary==3.3.6; implementation_name != "pypy" and extra == "binary"
|
|
39
40
|
Provides-Extra: pool
|
|
40
41
|
Requires-Dist: psycopg-pool; extra == "pool"
|
|
41
42
|
Provides-Extra: test
|
|
42
43
|
Requires-Dist: anyio>=4.0; extra == "test"
|
|
43
|
-
Requires-Dist: mypy>=1.
|
|
44
|
+
Requires-Dist: mypy>=2.1.0; implementation_name != "pypy" and extra == "test"
|
|
44
45
|
Requires-Dist: pproxy>=2.7; extra == "test"
|
|
45
46
|
Requires-Dist: pytest>=6.2.5; extra == "test"
|
|
46
47
|
Requires-Dist: pytest-cov>=3.0; extra == "test"
|
|
@@ -49,12 +50,12 @@ Provides-Extra: dev
|
|
|
49
50
|
Requires-Dist: ast-comments>=1.1.2; extra == "dev"
|
|
50
51
|
Requires-Dist: black>=26.1.0; extra == "dev"
|
|
51
52
|
Requires-Dist: codespell>=2.2; extra == "dev"
|
|
52
|
-
Requires-Dist: cython-lint>=0.
|
|
53
|
+
Requires-Dist: cython-lint>=0.21; extra == "dev"
|
|
53
54
|
Requires-Dist: dnspython>=2.1; extra == "dev"
|
|
54
55
|
Requires-Dist: flake8>=4.0; extra == "dev"
|
|
55
56
|
Requires-Dist: isort[colors]>=6.0; extra == "dev"
|
|
56
|
-
Requires-Dist: isort-psycopg; extra == "dev"
|
|
57
|
-
Requires-Dist: mypy>=1.
|
|
57
|
+
Requires-Dist: isort-psycopg>=0.0.3; extra == "dev"
|
|
58
|
+
Requires-Dist: mypy>=2.1.0; extra == "dev"
|
|
58
59
|
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
|
59
60
|
Requires-Dist: types-setuptools>=57.4; extra == "dev"
|
|
60
61
|
Requires-Dist: types-shapely>=2.0; extra == "dev"
|
|
@@ -48,7 +48,7 @@ class AdaptersMap:
|
|
|
48
48
|
already created are not affected if a wider scope (e.g. the global one) is
|
|
49
49
|
changed.
|
|
50
50
|
|
|
51
|
-
The connections adapters are initialised using a global `!
|
|
51
|
+
The connections adapters are initialised using a global `!AdaptersMap`
|
|
52
52
|
template, exposed as `psycopg.adapters`: changing such mapping allows to
|
|
53
53
|
customise the type mapping for every connections created afterwards.
|
|
54
54
|
|
|
@@ -33,24 +33,27 @@ else:
|
|
|
33
33
|
if sys.version_info >= (3, 14):
|
|
34
34
|
from string.templatelib import Interpolation, Template
|
|
35
35
|
else:
|
|
36
|
+
from typing import Generic, Literal
|
|
36
37
|
from dataclasses import dataclass
|
|
37
38
|
|
|
39
|
+
T = TypeVar("T")
|
|
40
|
+
|
|
38
41
|
class Template:
|
|
39
42
|
strings: tuple[str]
|
|
40
|
-
interpolations: tuple[Interpolation]
|
|
43
|
+
interpolations: tuple[Interpolation[Any]]
|
|
41
44
|
|
|
42
|
-
def __new__(cls, *args: str | Interpolation) -> Self:
|
|
45
|
+
def __new__(cls, *args: str | Interpolation[Any]) -> Self:
|
|
43
46
|
return cls()
|
|
44
47
|
|
|
45
|
-
def __iter__(self) -> Iterator[str | Interpolation]:
|
|
48
|
+
def __iter__(self) -> Iterator[str | Interpolation[Any]]:
|
|
46
49
|
return
|
|
47
50
|
yield
|
|
48
51
|
|
|
49
52
|
@dataclass
|
|
50
|
-
class Interpolation:
|
|
51
|
-
value:
|
|
53
|
+
class Interpolation(Generic[T]):
|
|
54
|
+
value: T
|
|
52
55
|
expression: str
|
|
53
|
-
conversion:
|
|
56
|
+
conversion: Literal["a", "r", "s"] | None
|
|
54
57
|
format_spec: str
|
|
55
58
|
|
|
56
59
|
|
|
@@ -314,10 +314,10 @@ class BaseConnection(Generic[Row]):
|
|
|
314
314
|
)
|
|
315
315
|
return True
|
|
316
316
|
|
|
317
|
-
def _cancel_gen(self
|
|
317
|
+
def _cancel_gen(self) -> PQGenConn[None]:
|
|
318
318
|
cancel_conn = self.pgconn.cancel_conn()
|
|
319
319
|
cancel_conn.start()
|
|
320
|
-
yield from generators.cancel(cancel_conn
|
|
320
|
+
yield from generators.cancel(cancel_conn)
|
|
321
321
|
|
|
322
322
|
def add_notice_handler(self, callback: NoticeHandler) -> None:
|
|
323
323
|
"""
|
|
@@ -434,11 +434,9 @@ class BaseConnection(Generic[Row]):
|
|
|
434
434
|
# should have a lock and hold it before calling and consuming them.
|
|
435
435
|
|
|
436
436
|
@classmethod
|
|
437
|
-
def _connect_gen(
|
|
438
|
-
cls, conninfo: str = "", *, timeout: float = 0.0
|
|
439
|
-
) -> PQGenConn[Self]:
|
|
437
|
+
def _connect_gen(cls, conninfo: str = "") -> PQGenConn[Self]:
|
|
440
438
|
"""Generator to connect to the database and create a new instance."""
|
|
441
|
-
pgconn = yield from generators.connect(conninfo
|
|
439
|
+
pgconn = yield from generators.connect(conninfo)
|
|
442
440
|
conn = cls(pgconn)
|
|
443
441
|
return conn
|
|
444
442
|
|
|
@@ -477,7 +475,13 @@ class BaseConnection(Generic[Row]):
|
|
|
477
475
|
else:
|
|
478
476
|
self.pgconn.send_query_params(command, None, result_format=result_format)
|
|
479
477
|
|
|
480
|
-
|
|
478
|
+
results: list[PGresult] = (yield from generators.execute(self.pgconn))
|
|
479
|
+
if len(results) != 1:
|
|
480
|
+
raise e.InternalError(
|
|
481
|
+
f"received {len(results)} results from command {command.decode()!r}"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
result = results[0]
|
|
481
485
|
if result.status != COMMAND_OK and result.status != TUPLES_OK:
|
|
482
486
|
if result.status == FATAL_ERROR:
|
|
483
487
|
raise e.error_from_result(result, encoding=self.pgconn._encoding)
|
|
@@ -514,7 +518,10 @@ class BaseConnection(Generic[Row]):
|
|
|
514
518
|
|
|
515
519
|
self.pgconn.send_close_prepared(name)
|
|
516
520
|
|
|
517
|
-
|
|
521
|
+
if not (results := (yield from generators.execute(self.pgconn))):
|
|
522
|
+
raise e.InternalError("no result from deallocate command")
|
|
523
|
+
|
|
524
|
+
result = results[-1]
|
|
518
525
|
if result.status != COMMAND_OK:
|
|
519
526
|
if result.status == FATAL_ERROR:
|
|
520
527
|
raise e.error_from_result(result, encoding=self.pgconn._encoding)
|
|
@@ -350,6 +350,8 @@ def _format_row_binary(row: Sequence[Any], tx: Transformer, out: bytearray) -> N
|
|
|
350
350
|
def _parse_row_text(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
|
|
351
351
|
if not isinstance(data, bytes):
|
|
352
352
|
data = bytes(data)
|
|
353
|
+
if not data.endswith(b"\n"):
|
|
354
|
+
raise e.DataError("bad copy data: field delimiter not found")
|
|
353
355
|
fields = data.split(b"\t")
|
|
354
356
|
fields[-1] = fields[-1][:-1] # drop \n
|
|
355
357
|
row = [None if f == b"\\N" else _load_re.sub(_load_sub, f) for f in fields]
|
|
@@ -358,12 +360,19 @@ def _parse_row_text(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
|
|
|
358
360
|
|
|
359
361
|
def _parse_row_binary(data: Buffer, tx: Transformer) -> tuple[Any, ...]:
|
|
360
362
|
row: list[Buffer | None] = []
|
|
363
|
+
len_data = len(data)
|
|
364
|
+
if len_data < 2:
|
|
365
|
+
raise e.DataError("bad copy data: fields number truncated")
|
|
361
366
|
nfields = _unpack_int2(data, 0)[0]
|
|
362
367
|
pos = 2
|
|
363
368
|
for i in range(nfields):
|
|
369
|
+
if pos + 4 > len_data:
|
|
370
|
+
raise e.DataError("bad copy data: field length truncated")
|
|
364
371
|
length = _unpack_int4(data, pos)[0]
|
|
365
372
|
pos += 4
|
|
366
373
|
if length >= 0:
|
|
374
|
+
if pos + length > len_data:
|
|
375
|
+
raise e.DataError("bad copy data: length exceeding data")
|
|
367
376
|
row.append(data[pos : pos + length])
|
|
368
377
|
pos += length
|
|
369
378
|
else:
|
|
@@ -13,70 +13,69 @@ from typing import TYPE_CHECKING, Any
|
|
|
13
13
|
from functools import cache
|
|
14
14
|
|
|
15
15
|
from .errors import NotSupportedError
|
|
16
|
-
from .pq._enums import ConnStatus
|
|
17
16
|
|
|
18
17
|
if TYPE_CHECKING:
|
|
19
18
|
from ._connection_base import BaseConnection
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
"SHIFT_JIS_2004": "shift_jis_2004",
|
|
52
|
-
"SJIS": "shift_jis",
|
|
20
|
+
# From: https://www.postgresql.org/docs/current/multibyte.html#CHARSET-TABLE
|
|
21
|
+
_py_codecs = (
|
|
22
|
+
(("BIG5", "WIN950", "Windows950"), "big5"),
|
|
23
|
+
(("EUC_CN",), "gb2312"),
|
|
24
|
+
(("EUC_JIS_2004",), "euc_jis_2004"),
|
|
25
|
+
(("EUC_JP",), "euc_jp"),
|
|
26
|
+
(("EUC_KR",), "euc_kr"),
|
|
27
|
+
# (("EUC_TW",), not available in Python
|
|
28
|
+
(("GB18030",), "gb18030"),
|
|
29
|
+
(("GBK", "WIN936", "Windows936"), "gbk"),
|
|
30
|
+
(("ISO_8859_5",), "iso8859-5"),
|
|
31
|
+
(("ISO_8859_6",), "iso8859-6"),
|
|
32
|
+
(("ISO_8859_7",), "iso8859-7"),
|
|
33
|
+
(("ISO_8859_8",), "iso8859-8"),
|
|
34
|
+
(("JOHAB",), "johab"),
|
|
35
|
+
(("KOI8R", "KOI8"), "koi8-r"),
|
|
36
|
+
(("KOI8U",), "koi8-u"),
|
|
37
|
+
(("LATIN1", "ISO88591"), "iso8859-1"),
|
|
38
|
+
(("LATIN10", "ISO885916"), "iso8859-16"),
|
|
39
|
+
(("LATIN2", "ISO88592"), "iso8859-2"),
|
|
40
|
+
(("LATIN3", "ISO88593"), "iso8859-3"),
|
|
41
|
+
(("LATIN4", "ISO88594"), "iso8859-4"),
|
|
42
|
+
(("LATIN5", "ISO88599"), "iso8859-9"),
|
|
43
|
+
(("LATIN6", "ISO885910"), "iso8859-10"),
|
|
44
|
+
(("LATIN7", "ISO885913"), "iso8859-13"),
|
|
45
|
+
(("LATIN8", "ISO885914"), "iso8859-14"),
|
|
46
|
+
(("LATIN9", "ISO885915"), "iso8859-15"),
|
|
47
|
+
# "MULE_INTERNAL" not available in Python
|
|
48
|
+
(("SHIFT_JIS_2004",), "shift_jis_2004"),
|
|
49
|
+
(("SJIS", "Mskanji", "ShiftJIS", "WIN932", "Windows932"), "shift_jis"),
|
|
53
50
|
# this actually means no encoding, see PostgreSQL docs
|
|
54
51
|
# it is special-cased by the text loader.
|
|
55
|
-
"SQL_ASCII"
|
|
56
|
-
"UHC"
|
|
57
|
-
"UTF8"
|
|
58
|
-
"WIN1250"
|
|
59
|
-
"WIN1251"
|
|
60
|
-
"WIN1252"
|
|
61
|
-
"WIN1253"
|
|
62
|
-
"WIN1254"
|
|
63
|
-
"WIN1255"
|
|
64
|
-
"WIN1256"
|
|
65
|
-
"WIN1257"
|
|
66
|
-
"WIN1258"
|
|
67
|
-
"WIN866"
|
|
68
|
-
"WIN874"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
py_codecs:
|
|
72
|
-
py_codecs.update((k.encode(), v) for k, v in _py_codecs.items())
|
|
52
|
+
(("SQL_ASCII",), "ascii"),
|
|
53
|
+
(("UHC", "WIN949", "Windows949"), "cp949"),
|
|
54
|
+
(("UTF8", "Unicode"), "utf-8"),
|
|
55
|
+
(("WIN1250",), "cp1250"),
|
|
56
|
+
(("WIN1251", "WIN"), "cp1251"),
|
|
57
|
+
(("WIN1252",), "cp1252"),
|
|
58
|
+
(("WIN1253",), "cp1253"),
|
|
59
|
+
(("WIN1254",), "cp1254"),
|
|
60
|
+
(("WIN1255",), "cp1255"),
|
|
61
|
+
(("WIN1256",), "cp1256"),
|
|
62
|
+
(("WIN1257",), "cp1257"),
|
|
63
|
+
(("WIN1258", "ABC", "TCVN", "TCVN5712", "VSCII"), "cp1258"),
|
|
64
|
+
(("WIN866", "ALT"), "cp866"),
|
|
65
|
+
(("WIN874",), "cp874"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
py_codecs = {a.upper().encode(): v for aliases, v in _py_codecs for a in aliases}
|
|
73
69
|
|
|
74
70
|
# Add an alias without underscore, for lenient lookups
|
|
75
71
|
py_codecs.update(
|
|
76
|
-
(
|
|
72
|
+
(a.replace("_", "").encode(), v)
|
|
73
|
+
for aliases, v in _py_codecs
|
|
74
|
+
for a in aliases
|
|
75
|
+
if "_" in a
|
|
77
76
|
)
|
|
78
77
|
|
|
79
|
-
pg_codecs = {v: k.encode() for k, v in _py_codecs
|
|
78
|
+
pg_codecs = {v: k[0].encode() for k, v in _py_codecs}
|
|
80
79
|
|
|
81
80
|
|
|
82
81
|
def conn_encoding(conn: BaseConnection[Any] | None) -> str:
|
|
@@ -6,6 +6,7 @@ Support for prepared statements
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import re
|
|
9
10
|
from enum import IntEnum, auto
|
|
10
11
|
from typing import TYPE_CHECKING, Any, TypeAlias
|
|
11
12
|
from collections import OrderedDict, deque
|
|
@@ -78,7 +79,14 @@ class PrepareManager:
|
|
|
78
79
|
# The query is not to be prepared yet
|
|
79
80
|
return Prepare.NO, b""
|
|
80
81
|
|
|
81
|
-
def _should_discard(
|
|
82
|
+
def _should_discard(
|
|
83
|
+
self,
|
|
84
|
+
prep: Prepare,
|
|
85
|
+
results: Sequence[PGresult],
|
|
86
|
+
__should_clear: Any = re.compile(
|
|
87
|
+
rb"^(?:DROP|ALTER|ROLLBACK|DISCARD ALL|DEALLOCATE ALL)\b"
|
|
88
|
+
).match,
|
|
89
|
+
) -> bool:
|
|
82
90
|
"""Check if we need to discard our entire state: it should happen on
|
|
83
91
|
rollback or on dropping objects, because the same object may get
|
|
84
92
|
recreated and postgres would fail internal lookups.
|
|
@@ -87,8 +95,7 @@ class PrepareManager:
|
|
|
87
95
|
for result in results:
|
|
88
96
|
if result.status != COMMAND_OK:
|
|
89
97
|
continue
|
|
90
|
-
cmdstat
|
|
91
|
-
if cmdstat and (cmdstat.startswith(b"DROP ") or cmdstat == b"ROLLBACK"):
|
|
98
|
+
if (cmdstat := result.command_status) and __should_clear(cmdstat):
|
|
92
99
|
return self.clear()
|
|
93
100
|
return False
|
|
94
101
|
|
|
@@ -65,6 +65,7 @@ class Transformer(AdaptContext):
|
|
|
65
65
|
|
|
66
66
|
def __init__(self, context: AdaptContext | None = None):
|
|
67
67
|
self._pgresult = self.types = self.formats = None
|
|
68
|
+
self._none_oid = -1
|
|
68
69
|
|
|
69
70
|
# WARNING: don't store context, or you'll create a loop with the Cursor
|
|
70
71
|
if context:
|
|
@@ -187,12 +188,14 @@ class Transformer(AdaptContext):
|
|
|
187
188
|
out[i] = self._row_dumpers[i].dump(param)
|
|
188
189
|
return out
|
|
189
190
|
|
|
190
|
-
types = [
|
|
191
|
+
types = [-1] * nparams
|
|
191
192
|
pqformats = [TEXT] * nparams
|
|
192
193
|
|
|
193
194
|
for i in range(nparams):
|
|
194
195
|
if (param := params[i]) is None:
|
|
196
|
+
types[i] = self._get_none_oid()
|
|
195
197
|
continue
|
|
198
|
+
|
|
196
199
|
dumper = self.get_dumper(param, formats[i])
|
|
197
200
|
out[i] = dumper.dump(param)
|
|
198
201
|
types[i] = dumper.oid
|
|
@@ -266,17 +269,12 @@ class Transformer(AdaptContext):
|
|
|
266
269
|
return dumper
|
|
267
270
|
|
|
268
271
|
def _get_none_oid(self) -> int:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
rv = self._none_oid = self._adapters.get_dumper(NoneType, PY_TEXT).oid
|
|
276
|
-
except KeyError:
|
|
277
|
-
raise e.InterfaceError("None dumper not found")
|
|
278
|
-
|
|
279
|
-
return rv
|
|
272
|
+
if self._none_oid < 0:
|
|
273
|
+
try:
|
|
274
|
+
self._none_oid = self._adapters.get_dumper(NoneType, PY_TEXT).oid
|
|
275
|
+
except KeyError:
|
|
276
|
+
raise e.InterfaceError("None dumper not found")
|
|
277
|
+
return self._none_oid
|
|
280
278
|
|
|
281
279
|
def get_dumper_by_oid(self, oid: int, format: pq.Format) -> abc.Dumper:
|
|
282
280
|
"""
|
|
@@ -40,7 +40,7 @@ class TemplateProcessor:
|
|
|
40
40
|
self._process_template(self.template)
|
|
41
41
|
self.query = b"".join(self._chunks)
|
|
42
42
|
|
|
43
|
-
def _check_template_format(self, item: Interpolation, want_fmt: str) -> None:
|
|
43
|
+
def _check_template_format(self, item: Interpolation[Any], want_fmt: str) -> None:
|
|
44
44
|
if item.format_spec == want_fmt:
|
|
45
45
|
return
|
|
46
46
|
fmt = f":{item.format_spec}" if item.format_spec else ""
|
|
@@ -94,7 +94,7 @@ class TemplateProcessor:
|
|
|
94
94
|
else:
|
|
95
95
|
self._process_client_variable(item, fmt)
|
|
96
96
|
|
|
97
|
-
def _process_server_variable(self, item: Interpolation, fmt: str) -> None:
|
|
97
|
+
def _process_server_variable(self, item: Interpolation[Any], fmt: str) -> None:
|
|
98
98
|
try:
|
|
99
99
|
pyfmt = PyFormat(fmt)
|
|
100
100
|
except ValueError:
|
|
@@ -107,7 +107,7 @@ class TemplateProcessor:
|
|
|
107
107
|
self.params.append(item.value)
|
|
108
108
|
self._chunks.append(b"$%d" % len(self.params))
|
|
109
109
|
|
|
110
|
-
def _process_client_variable(self, item: Interpolation, fmt: str) -> None:
|
|
110
|
+
def _process_client_variable(self, item: Interpolation[Any], fmt: str) -> None:
|
|
111
111
|
try:
|
|
112
112
|
PyFormat(fmt)
|
|
113
113
|
except ValueError:
|
|
@@ -120,7 +120,7 @@ class TemplateProcessor:
|
|
|
120
120
|
self._chunks.append(param)
|
|
121
121
|
self.params.append(param)
|
|
122
122
|
|
|
123
|
-
def _process_composable(self, item: Interpolation) -> None:
|
|
123
|
+
def _process_composable(self, item: Interpolation[Any]) -> None:
|
|
124
124
|
if isinstance(item.value, sql.Identifier):
|
|
125
125
|
self._check_template_format(item, FMT_IDENT)
|
|
126
126
|
self._chunks.append(item.value.as_bytes(self._tx))
|
|
@@ -84,3 +84,21 @@ class TimeTypeModifier(TypeModifier):
|
|
|
84
84
|
|
|
85
85
|
def get_precision(self, typemod: int) -> int | None:
|
|
86
86
|
return typemod & 0xFFFF if typemod >= 0 else None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class IntervalTypeModifier(TimeTypeModifier):
|
|
90
|
+
"""Handle interval type modifier.
|
|
91
|
+
|
|
92
|
+
Unlike the other time-related types, the interval typmod is a composite:
|
|
93
|
+
the fields restriction is packed in the high bits and the precision in the
|
|
94
|
+
low ones. A typmod written without an explicit precision (e.g. ``interval
|
|
95
|
+
day to second``) stores ``INTERVAL_FULL_PRECISION`` (0xFFFF) there, which
|
|
96
|
+
means "unspecified" rather than a precision of 65535.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def get_precision(self, typemod: int) -> int | None:
|
|
100
|
+
if typemod < 0:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
precision = typemod & 0xFFFF
|
|
104
|
+
return precision if precision != 0xFFFF else None
|
|
@@ -57,7 +57,13 @@ class WaitFunc(Protocol):
|
|
|
57
57
|
Wait on the connection which generated `PQgen` and return its final result.
|
|
58
58
|
"""
|
|
59
59
|
|
|
60
|
-
def __call__(
|
|
60
|
+
def __call__(
|
|
61
|
+
self,
|
|
62
|
+
gen: PQGen[RV],
|
|
63
|
+
fileno: int,
|
|
64
|
+
interval: float = ...,
|
|
65
|
+
timeout: float | None = ...,
|
|
66
|
+
) -> RV: ...
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
# Adaptation types
|
|
@@ -50,7 +50,7 @@ IDLE = pq.TransactionStatus.IDLE
|
|
|
50
50
|
ACTIVE = pq.TransactionStatus.ACTIVE
|
|
51
51
|
INTRANS = pq.TransactionStatus.INTRANS
|
|
52
52
|
|
|
53
|
-
_INTERRUPTED = KeyboardInterrupt
|
|
53
|
+
_INTERRUPTED = (KeyboardInterrupt, SystemExit)
|
|
54
54
|
|
|
55
55
|
logger = logging.getLogger("psycopg")
|
|
56
56
|
|
|
@@ -105,8 +105,12 @@ class Connection(BaseConnection[Row]):
|
|
|
105
105
|
logger.debug("connection attempt: %s", descr)
|
|
106
106
|
try:
|
|
107
107
|
conninfo = make_conninfo("", **attempt)
|
|
108
|
-
gen = cls._connect_gen(conninfo
|
|
109
|
-
rv = waiting.wait_conn(gen, interval=_WAIT_INTERVAL)
|
|
108
|
+
gen = cls._connect_gen(conninfo)
|
|
109
|
+
rv = waiting.wait_conn(gen, interval=_WAIT_INTERVAL, timeout=timeout)
|
|
110
|
+
except e._WaitTimeout:
|
|
111
|
+
tex = e.ConnectionTimeout("connection timeout expired")
|
|
112
|
+
logger.debug("connection failed: %s: %s", descr, str(tex))
|
|
113
|
+
conn_errors.append((tex, descr))
|
|
110
114
|
except e.Error as ex:
|
|
111
115
|
logger.debug("connection failed: %s: %s", descr, str(ex))
|
|
112
116
|
conn_errors.append((ex, descr))
|
|
@@ -329,9 +333,12 @@ class Connection(BaseConnection[Row]):
|
|
|
329
333
|
return
|
|
330
334
|
|
|
331
335
|
if capabilities.has_cancel_safe():
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
try:
|
|
337
|
+
waiting.wait_conn(
|
|
338
|
+
self._cancel_gen(), interval=_WAIT_INTERVAL, timeout=timeout or None
|
|
339
|
+
)
|
|
340
|
+
except e._WaitTimeout:
|
|
341
|
+
raise e.CancellationTimeout("cancellation timeout expired") from None
|
|
335
342
|
else:
|
|
336
343
|
self.cancel()
|
|
337
344
|
|
|
@@ -374,15 +381,7 @@ class Connection(BaseConnection[Row]):
|
|
|
374
381
|
You might actually receive more than this number if more than one
|
|
375
382
|
notifications arrives in the same packet.
|
|
376
383
|
"""
|
|
377
|
-
|
|
378
|
-
# into shorter intervals.
|
|
379
|
-
if timeout is not None:
|
|
380
|
-
deadline = monotonic() + timeout
|
|
381
|
-
interval = min(timeout, _WAIT_INTERVAL)
|
|
382
|
-
else:
|
|
383
|
-
deadline = None
|
|
384
|
-
interval = _WAIT_INTERVAL
|
|
385
|
-
|
|
384
|
+
deadline = monotonic() + timeout if timeout is not None else None
|
|
386
385
|
nreceived = 0
|
|
387
386
|
|
|
388
387
|
if self._notify_handlers:
|
|
@@ -407,8 +406,14 @@ class Connection(BaseConnection[Row]):
|
|
|
407
406
|
yield d.popleft()
|
|
408
407
|
nreceived += 1
|
|
409
408
|
else:
|
|
409
|
+
if deadline is not None:
|
|
410
|
+
remaining = max(0.0, deadline - monotonic())
|
|
411
|
+
else:
|
|
412
|
+
remaining = None
|
|
410
413
|
try:
|
|
411
|
-
pgns = self.wait(notifies(self.pgconn),
|
|
414
|
+
pgns = self.wait(notifies(self.pgconn), timeout=remaining)
|
|
415
|
+
except e._WaitTimeout:
|
|
416
|
+
break
|
|
412
417
|
except e._NO_TRACEBACK as ex:
|
|
413
418
|
raise ex.with_traceback(None)
|
|
414
419
|
# Emit the notifications received.
|
|
@@ -426,10 +431,8 @@ class Connection(BaseConnection[Row]):
|
|
|
426
431
|
|
|
427
432
|
# Check the deadline after the loop to ensure that timeout=0
|
|
428
433
|
# polls at least once.
|
|
429
|
-
if deadline:
|
|
430
|
-
|
|
431
|
-
if interval < 0.0:
|
|
432
|
-
break
|
|
434
|
+
if deadline is not None and monotonic() > deadline:
|
|
435
|
+
break
|
|
433
436
|
finally:
|
|
434
437
|
self._notifies_backlog = d
|
|
435
438
|
|
|
@@ -473,24 +476,43 @@ class Connection(BaseConnection[Row]):
|
|
|
473
476
|
assert pipeline is self._pipeline
|
|
474
477
|
self._pipeline = None
|
|
475
478
|
|
|
476
|
-
def wait(
|
|
479
|
+
def wait(
|
|
480
|
+
self,
|
|
481
|
+
gen: PQGen[RV],
|
|
482
|
+
interval: float = _WAIT_INTERVAL,
|
|
483
|
+
timeout: float | None = None,
|
|
484
|
+
) -> RV:
|
|
477
485
|
"""
|
|
478
486
|
Consume a generator operating on the connection.
|
|
479
487
|
|
|
480
488
|
The function must be used on generators that don't change connection
|
|
481
489
|
fd (i.e. not on connect and reset).
|
|
490
|
+
|
|
491
|
+
Raise `~errors._WaitTimeout` if `!gen` doesn't complete within
|
|
492
|
+
`!timeout` seconds.
|
|
482
493
|
"""
|
|
483
494
|
try:
|
|
484
|
-
return waiting.wait(
|
|
495
|
+
return waiting.wait(
|
|
496
|
+
gen, self.pgconn.socket, interval=interval, timeout=timeout
|
|
497
|
+
)
|
|
485
498
|
except _INTERRUPTED:
|
|
486
499
|
if self.pgconn.transaction_status == ACTIVE:
|
|
487
500
|
# On Ctrl-C, try to cancel the query in the server, otherwise
|
|
488
501
|
# the connection will remain stuck in ACTIVE state.
|
|
489
502
|
self._try_cancel(timeout=5.0)
|
|
490
503
|
try:
|
|
491
|
-
waiting.wait(
|
|
504
|
+
waiting.wait(
|
|
505
|
+
gen, self.pgconn.socket, interval=interval, timeout=5.0
|
|
506
|
+
)
|
|
492
507
|
except e.QueryCanceled:
|
|
493
508
|
pass # as expected
|
|
509
|
+
except e._WaitTimeout:
|
|
510
|
+
# The server didn't terminate the query: the connection
|
|
511
|
+
# is in an unknown state, so don't wait forever (#1371).
|
|
512
|
+
logger.warning(
|
|
513
|
+
"query not terminated after cancellation: closing connection"
|
|
514
|
+
)
|
|
515
|
+
self.pgconn.finish()
|
|
494
516
|
raise
|
|
495
517
|
|
|
496
518
|
def _set_autocommit(self, value: bool) -> None:
|