psycopg 3.3.6__tar.gz → 3.3.7__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.6 → psycopg-3.3.7}/PKG-INFO +3 -3
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_dns.py +10 -8
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/adapt.py +6 -2
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/generators.py +6 -8
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/transaction.py +25 -3
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/array.py +91 -59
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/composite.py +22 -12
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/datetime.py +6 -3
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/json.py +1 -1
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/PKG-INFO +3 -3
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/requires.txt +2 -2
- {psycopg-3.3.6 → psycopg-3.3.7}/pyproject.toml +3 -3
- {psycopg-3.3.6 → psycopg-3.3.7}/LICENSE.txt +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/README.rst +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/__init__.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_acompat.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_adapters_map.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_capabilities.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_cmodule.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_column.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_compat.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_connection_base.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_connection_info.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_attempts.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_attempts_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_conninfo_utils.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_copy_base.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_cursor_base.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_encodings.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_enums.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_oids.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_pipeline_base.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_preparing.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_py_transformer.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_queries.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_server_cursor_base.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_struct.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tpc.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_transformer.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tstrings.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_typeinfo.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_typemod.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_tz.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/_wrappers.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/abc.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/client_cursor.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/connection.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/connection_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/conninfo.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/copy.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/__init__.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/_types.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/crdb/connection.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/cursor.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/cursor_async.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/dbapi20.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/errors.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/postgres.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/__init__.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_debug.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_enums.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_pq_ctypes.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/_pq_ctypes.pyi +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/abc.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/misc.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/pq/pq_ctypes.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/py.typed +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/raw_cursor.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/rows.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/sql.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/__init__.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/bool.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/enum.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/hstore.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/multirange.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/net.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/none.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/numeric.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/numpy.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/range.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/shapely.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/string.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/types/uuid.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/version.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg/waiting.py +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/SOURCES.txt +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/dependency_links.txt +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/not-zip-safe +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/psycopg.egg-info/top_level.txt +0 -0
- {psycopg-3.3.6 → psycopg-3.3.7}/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.7
|
|
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
|
|
@@ -34,9 +34,9 @@ License-File: LICENSE.txt
|
|
|
34
34
|
Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
|
|
35
35
|
Requires-Dist: tzdata; sys_platform == "win32"
|
|
36
36
|
Provides-Extra: c
|
|
37
|
-
Requires-Dist: psycopg-c==3.3.
|
|
37
|
+
Requires-Dist: psycopg-c==3.3.7; implementation_name != "pypy" and extra == "c"
|
|
38
38
|
Provides-Extra: binary
|
|
39
|
-
Requires-Dist: psycopg-binary==3.3.
|
|
39
|
+
Requires-Dist: psycopg-binary==3.3.7; implementation_name != "pypy" and extra == "binary"
|
|
40
40
|
Provides-Extra: pool
|
|
41
41
|
Requires-Dist: psycopg-pool; extra == "pool"
|
|
42
42
|
Provides-Extra: test
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# type: ignore # dnspython is currently optional and mypy fails if missing
|
|
2
1
|
"""
|
|
3
2
|
DNS query support
|
|
4
3
|
"""
|
|
@@ -13,12 +12,13 @@ import warnings
|
|
|
13
12
|
from random import randint
|
|
14
13
|
from typing import TYPE_CHECKING, Any, DefaultDict, NamedTuple
|
|
15
14
|
from collections import defaultdict
|
|
16
|
-
from collections.abc import Sequence
|
|
15
|
+
from collections.abc import Iterable, Sequence
|
|
17
16
|
|
|
18
17
|
try:
|
|
19
18
|
from dns.resolver import Cache, Resolver
|
|
20
19
|
from dns.exception import DNSException
|
|
21
20
|
from dns.asyncresolver import Resolver as AsyncResolver
|
|
21
|
+
from dns.rdtypes.IN.SRV import SRV
|
|
22
22
|
except ImportError:
|
|
23
23
|
raise ImportError(
|
|
24
24
|
"the module psycopg._dns requires the package 'dnspython' installed"
|
|
@@ -28,7 +28,7 @@ from . import conninfo
|
|
|
28
28
|
from . import errors as e
|
|
29
29
|
|
|
30
30
|
if TYPE_CHECKING:
|
|
31
|
-
from dns.
|
|
31
|
+
from dns.rdata import Rdata
|
|
32
32
|
|
|
33
33
|
resolver = Resolver()
|
|
34
34
|
resolver.cache = Cache()
|
|
@@ -56,9 +56,9 @@ async def resolve_hostaddr_async(params: dict[str, Any]) -> dict[str, Any]:
|
|
|
56
56
|
|
|
57
57
|
for attempt in await conninfo.conninfo_attempts_async(params):
|
|
58
58
|
if attempt.get("host") is not None:
|
|
59
|
-
hosts.append(attempt["host"])
|
|
59
|
+
hosts.append(str(attempt["host"]))
|
|
60
60
|
if attempt.get("hostaddr") is not None:
|
|
61
|
-
hostaddrs.append(attempt["hostaddr"])
|
|
61
|
+
hostaddrs.append(str(attempt["hostaddr"]))
|
|
62
62
|
if attempt.get("port") is not None:
|
|
63
63
|
ports.append(str(attempt["port"]))
|
|
64
64
|
|
|
@@ -165,6 +165,7 @@ class Rfc2782Resolver:
|
|
|
165
165
|
return out if srv_found else []
|
|
166
166
|
|
|
167
167
|
def _resolve_srv(self, hp: HostPort) -> list[HostPort]:
|
|
168
|
+
ans: Iterable[Rdata]
|
|
168
169
|
try:
|
|
169
170
|
ans = resolver.resolve(hp.host, "SRV")
|
|
170
171
|
except DNSException:
|
|
@@ -172,15 +173,16 @@ class Rfc2782Resolver:
|
|
|
172
173
|
return self._get_solved_entries(hp, ans)
|
|
173
174
|
|
|
174
175
|
async def _resolve_srv_async(self, hp: HostPort) -> list[HostPort]:
|
|
176
|
+
ans: Iterable[Rdata]
|
|
175
177
|
try:
|
|
176
178
|
ans = await async_resolver.resolve(hp.host, "SRV")
|
|
177
179
|
except DNSException:
|
|
178
180
|
ans = ()
|
|
179
181
|
return self._get_solved_entries(hp, ans)
|
|
180
182
|
|
|
181
|
-
def _get_solved_entries(
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
def _get_solved_entries(self, hp: HostPort, ans: Iterable[Rdata]) -> list[HostPort]:
|
|
184
|
+
# A SRV query only returns SRV records: this only narrows the type.
|
|
185
|
+
entries = [rr for rr in ans if isinstance(rr, SRV)]
|
|
184
186
|
if not entries:
|
|
185
187
|
# No SRV entry found. Delegate the libpq a QNAME=target lookup
|
|
186
188
|
if hp.target and hp.port.lower() != "srv":
|
|
@@ -141,7 +141,9 @@ class RecursiveDumper(Dumper):
|
|
|
141
141
|
|
|
142
142
|
def __init__(self, cls: type, context: abc.AdaptContext | None = None):
|
|
143
143
|
super().__init__(cls, context)
|
|
144
|
-
|
|
144
|
+
# Don't use from_context(): if the context is a transformer, it will
|
|
145
|
+
# cache this adapter, creating a reference loop.
|
|
146
|
+
self._tx = Transformer(context)
|
|
145
147
|
|
|
146
148
|
|
|
147
149
|
class RecursiveLoader(Loader):
|
|
@@ -149,4 +151,6 @@ class RecursiveLoader(Loader):
|
|
|
149
151
|
|
|
150
152
|
def __init__(self, oid: int, context: abc.AdaptContext | None = None):
|
|
151
153
|
super().__init__(oid, context)
|
|
152
|
-
|
|
154
|
+
# Don't use from_context(): if the context is a transformer, it will
|
|
155
|
+
# cache this adapter, creating a reference loop.
|
|
156
|
+
self._tx = Transformer(context)
|
|
@@ -83,10 +83,8 @@ def _connect(conninfo: str) -> PQGenConn[PGconn]:
|
|
|
83
83
|
|
|
84
84
|
if status == POLL_READING or status == POLL_WRITING:
|
|
85
85
|
wait = WAIT_R if status == POLL_READING else WAIT_W
|
|
86
|
-
while
|
|
87
|
-
|
|
88
|
-
if ready:
|
|
89
|
-
break
|
|
86
|
+
while not (yield conn.socket, wait):
|
|
87
|
+
pass
|
|
90
88
|
|
|
91
89
|
elif status == POLL_OK:
|
|
92
90
|
break
|
|
@@ -109,10 +107,10 @@ def _cancel(cancel_conn: PGcancelConn) -> PQGenConn[None]:
|
|
|
109
107
|
while True:
|
|
110
108
|
if (status := cancel_conn.poll()) == POLL_OK:
|
|
111
109
|
break
|
|
112
|
-
elif status == POLL_READING:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
110
|
+
elif status == POLL_READING or status == POLL_WRITING:
|
|
111
|
+
wait = WAIT_R if status == POLL_READING else WAIT_W
|
|
112
|
+
while not (yield cancel_conn.socket, wait):
|
|
113
|
+
pass
|
|
116
114
|
elif status == POLL_FAILED:
|
|
117
115
|
raise e.OperationalError(
|
|
118
116
|
f"cancellation failed: {cancel_conn.get_error_message()}"
|
|
@@ -96,9 +96,11 @@ class BaseTransaction(Generic[ConnectionType]):
|
|
|
96
96
|
sp = f"{self.savepoint_name!r} " if self.savepoint_name else ""
|
|
97
97
|
return f"<{cls} {sp}({self.status.value}) {info} at 0x{id(self):x}>"
|
|
98
98
|
|
|
99
|
-
def
|
|
99
|
+
def _check_enter(self) -> None:
|
|
100
100
|
if self.status != self.Status.NOT_STARTED:
|
|
101
101
|
raise TypeError("transaction blocks can be used only once")
|
|
102
|
+
|
|
103
|
+
def _enter_gen(self) -> PQGen[None]:
|
|
102
104
|
self.status = self.Status.ACTIVE
|
|
103
105
|
|
|
104
106
|
self._push_savepoint()
|
|
@@ -257,7 +259,17 @@ class Transaction(BaseTransaction["Connection[Any]"]):
|
|
|
257
259
|
|
|
258
260
|
def __enter__(self) -> Self:
|
|
259
261
|
with self._conn.lock:
|
|
260
|
-
self.
|
|
262
|
+
self._check_enter()
|
|
263
|
+
try:
|
|
264
|
+
self._conn.wait(self._enter_gen())
|
|
265
|
+
except BaseException as ex:
|
|
266
|
+
# Undo the enter if it was interrupted (e.g. by Ctrl-C)
|
|
267
|
+
# as __exit__ won't be called.
|
|
268
|
+
if self.pgconn.status == OK:
|
|
269
|
+
self._conn.wait(self._exit_gen(type(ex), ex, None))
|
|
270
|
+
else:
|
|
271
|
+
self.status = self.Status.FAILED
|
|
272
|
+
raise
|
|
261
273
|
return self
|
|
262
274
|
|
|
263
275
|
def __exit__(
|
|
@@ -291,7 +303,17 @@ class AsyncTransaction(BaseTransaction["AsyncConnection[Any]"]):
|
|
|
291
303
|
|
|
292
304
|
async def __aenter__(self) -> Self:
|
|
293
305
|
async with self._conn.lock:
|
|
294
|
-
|
|
306
|
+
self._check_enter()
|
|
307
|
+
try:
|
|
308
|
+
await self._conn.wait(self._enter_gen())
|
|
309
|
+
except BaseException as ex:
|
|
310
|
+
# Undo the enter if it was interrupted (e.g. by a cancellation)
|
|
311
|
+
# as __aexit__ won't be called.
|
|
312
|
+
if self.pgconn.status == OK:
|
|
313
|
+
await self._conn.wait(self._exit_gen(type(ex), ex, None))
|
|
314
|
+
else:
|
|
315
|
+
self.status = self.Status.FAILED
|
|
316
|
+
raise
|
|
295
317
|
return self
|
|
296
318
|
|
|
297
319
|
async def __aexit__(
|
|
@@ -157,33 +157,39 @@ class ListDumper(BaseListDumper):
|
|
|
157
157
|
def dump(self, obj: list[Any]) -> Buffer | None:
|
|
158
158
|
tokens: list[Buffer] = []
|
|
159
159
|
needs_quotes = _get_needs_quotes_regexp(self.delimiter).search
|
|
160
|
+
self._dump_list(obj, tokens, needs_quotes)
|
|
161
|
+
return b"".join(tokens)
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
else:
|
|
174
|
-
if needs_quotes(ad):
|
|
175
|
-
ad = b'"' + self._re_esc.sub(rb"\\\1", ad) + b'"'
|
|
176
|
-
tokens.append(ad)
|
|
177
|
-
else:
|
|
178
|
-
tokens.append(b"NULL")
|
|
179
|
-
|
|
180
|
-
tokens.append(self.delimiter)
|
|
163
|
+
def _dump_list(
|
|
164
|
+
self,
|
|
165
|
+
obj: list[Any],
|
|
166
|
+
tokens: list[Buffer],
|
|
167
|
+
needs_quotes: Callable[[Buffer], object],
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Append the tokens of the text representation of a list to *tokens*.
|
|
171
|
+
"""
|
|
172
|
+
if not obj:
|
|
173
|
+
tokens.append(b"{}")
|
|
174
|
+
return
|
|
181
175
|
|
|
182
|
-
|
|
176
|
+
tokens.append(b"{")
|
|
177
|
+
for item in obj:
|
|
178
|
+
if isinstance(item, list):
|
|
179
|
+
self._dump_list(item, tokens, needs_quotes)
|
|
180
|
+
elif item is not None:
|
|
181
|
+
if (ad := self._dump_item(item)) is None:
|
|
182
|
+
tokens.append(b"NULL")
|
|
183
|
+
else:
|
|
184
|
+
if needs_quotes(ad):
|
|
185
|
+
ad = b'"' + self._re_esc.sub(rb"\\\1", ad) + b'"'
|
|
186
|
+
tokens.append(ad)
|
|
187
|
+
else:
|
|
188
|
+
tokens.append(b"NULL")
|
|
183
189
|
|
|
184
|
-
|
|
190
|
+
tokens.append(self.delimiter)
|
|
185
191
|
|
|
186
|
-
|
|
192
|
+
tokens[-1] = b"}"
|
|
187
193
|
|
|
188
194
|
def _dump_item(self, item: Any) -> Buffer | None:
|
|
189
195
|
if self.sub_dumper:
|
|
@@ -201,11 +207,19 @@ def _get_needs_quotes_regexp(delimiter: bytes) -> re.Pattern[bytes]:
|
|
|
201
207
|
The array output routine will put double quotes around element values if
|
|
202
208
|
they are empty strings, contain curly braces, delimiter characters,
|
|
203
209
|
double quotes, backslashes, or white space, or match the word NULL.
|
|
210
|
+
|
|
211
|
+
Also quote values containing Unicode white space: Postgres doesn't strip
|
|
212
|
+
it from unquoted elements, but CockroachDB does (see
|
|
213
|
+
https://github.com/cockroachdb/cockroach/issues/176211).
|
|
204
214
|
"""
|
|
205
215
|
return re.compile(rb"""(?xi)
|
|
206
216
|
^$ # the empty string
|
|
207
217
|
| ["{}%s\\\s] # or a char to escape
|
|
208
218
|
| ^null$ # or the word NULL
|
|
219
|
+
| \xe1\x9a\x80 # or UTF-8 Unicode spaces stripped by CockroachDB:
|
|
220
|
+
| \xe2\x80[\x80-\x8a\xa8\xa9\xaf] # U+1680, U+2000-U+200A,
|
|
221
|
+
| \xe2\x81\x9f # U+2028, U+2029, U+202F, U+205F, U+3000
|
|
222
|
+
| \xe3\x80\x80
|
|
209
223
|
""" % delimiter)
|
|
210
224
|
|
|
211
225
|
|
|
@@ -247,58 +261,76 @@ class ListBinaryDumper(BaseListDumper):
|
|
|
247
261
|
|
|
248
262
|
data: list[Buffer] = [b"", b""] # placeholders to avoid a resize
|
|
249
263
|
dims: list[int] = []
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def calc_dims(L: list[Any]) -> None:
|
|
253
|
-
if isinstance(L, self.cls):
|
|
254
|
-
if not L:
|
|
255
|
-
raise e.DataError("lists cannot contain empty lists")
|
|
256
|
-
dims.append(len(L))
|
|
257
|
-
calc_dims(L[0])
|
|
258
|
-
|
|
259
|
-
calc_dims(obj)
|
|
260
|
-
|
|
261
|
-
def dump_list(L: list[Any], dim: int) -> None:
|
|
262
|
-
nonlocal hasnull
|
|
263
|
-
if len(L) != dims[dim]:
|
|
264
|
-
raise e.DataError("nested lists have inconsistent lengths")
|
|
265
|
-
|
|
266
|
-
if dim == len(dims) - 1:
|
|
267
|
-
for item in L:
|
|
268
|
-
if item is not None:
|
|
269
|
-
# If we get here, the sub_dumper must have been set
|
|
270
|
-
item = self.sub_dumper.dump(item) # type: ignore[union-attr]
|
|
271
|
-
if item is not None:
|
|
272
|
-
data.append(pack_len(len(item)))
|
|
273
|
-
data.append(item)
|
|
274
|
-
else:
|
|
275
|
-
hasnull = 1
|
|
276
|
-
data.append(ARRAY_NULL)
|
|
277
|
-
else:
|
|
278
|
-
for item in L:
|
|
279
|
-
if not isinstance(item, self.cls):
|
|
280
|
-
raise e.DataError("nested lists have inconsistent depths")
|
|
281
|
-
dump_list(item, dim + 1) # type: ignore
|
|
282
|
-
|
|
283
|
-
dump_list(obj, 0)
|
|
264
|
+
self._calc_dims(obj, dims)
|
|
265
|
+
hasnull = self._dump_list(obj, 0, dims, data)
|
|
284
266
|
|
|
285
267
|
data[0] = _pack_head(len(dims), hasnull, sub_oid)
|
|
286
268
|
data[1] = b"".join(_pack_dim(dim, 1) for dim in dims)
|
|
287
269
|
return b"".join(data)
|
|
288
270
|
|
|
271
|
+
def _calc_dims(self, L: list[Any], dims: list[int]) -> None:
|
|
272
|
+
if isinstance(L, self.cls):
|
|
273
|
+
if not L:
|
|
274
|
+
raise e.DataError("lists cannot contain empty lists")
|
|
275
|
+
dims.append(len(L))
|
|
276
|
+
self._calc_dims(L[0], dims)
|
|
277
|
+
|
|
278
|
+
def _dump_list(
|
|
279
|
+
self, L: list[Any], dim: int, dims: list[int], data: list[Buffer]
|
|
280
|
+
) -> int:
|
|
281
|
+
"""
|
|
282
|
+
Append the dumped items of a list to *data*.
|
|
283
|
+
|
|
284
|
+
Return 1 if any item is null, else 0.
|
|
285
|
+
"""
|
|
286
|
+
if len(L) != dims[dim]:
|
|
287
|
+
raise e.DataError("nested lists have inconsistent lengths")
|
|
288
|
+
|
|
289
|
+
hasnull = 0
|
|
290
|
+
if dim == len(dims) - 1:
|
|
291
|
+
for item in L:
|
|
292
|
+
if item is not None:
|
|
293
|
+
# If we get here, the sub_dumper must have been set
|
|
294
|
+
item = self.sub_dumper.dump(item) # type: ignore[union-attr]
|
|
295
|
+
if item is not None:
|
|
296
|
+
data.append(pack_len(len(item)))
|
|
297
|
+
data.append(item)
|
|
298
|
+
else:
|
|
299
|
+
hasnull = 1
|
|
300
|
+
data.append(ARRAY_NULL)
|
|
301
|
+
else:
|
|
302
|
+
for item in L:
|
|
303
|
+
if not isinstance(item, self.cls):
|
|
304
|
+
raise e.DataError("nested lists have inconsistent depths")
|
|
305
|
+
hasnull |= self._dump_list(item, dim + 1, dims, data) # type: ignore
|
|
306
|
+
|
|
307
|
+
return hasnull
|
|
308
|
+
|
|
289
309
|
|
|
290
310
|
class ArrayLoader(RecursiveLoader):
|
|
291
311
|
delimiter = b","
|
|
292
312
|
base_oid: int
|
|
293
313
|
|
|
314
|
+
def __init__(self, oid: int, context: AdaptContext | None = None):
|
|
315
|
+
super().__init__(oid, context)
|
|
316
|
+
# Create the element loader now: the connection might be closed
|
|
317
|
+
# by the time data is loaded (#1428).
|
|
318
|
+
self._loader = self._tx.get_loader(self.base_oid, self.format)
|
|
319
|
+
|
|
294
320
|
def load(self, data: Buffer) -> list[Any]:
|
|
295
|
-
|
|
296
|
-
return _load_text(data, loader, self.delimiter)
|
|
321
|
+
return _load_text(data, self._loader, self.delimiter)
|
|
297
322
|
|
|
298
323
|
|
|
299
324
|
class ArrayBinaryLoader(RecursiveLoader):
|
|
300
325
|
format = pq.Format.BINARY
|
|
301
326
|
|
|
327
|
+
def __init__(self, oid: int, context: AdaptContext | None = None):
|
|
328
|
+
super().__init__(oid, context)
|
|
329
|
+
# The element oid is in the data, but if the array type is known,
|
|
330
|
+
# create the element loader now, while the connection is still open.
|
|
331
|
+
if (info := self._tx.adapters.types.get(oid)) and info.array_oid == oid:
|
|
332
|
+
self._tx.get_loader(info.oid, self.format)
|
|
333
|
+
|
|
302
334
|
def load(self, data: Buffer) -> list[Any]:
|
|
303
335
|
return _load_binary(data, self._tx)
|
|
304
336
|
|
|
@@ -324,9 +324,7 @@ def register_composite(
|
|
|
324
324
|
factory = cast("Callable[..., T]", _nt_from_info(info))
|
|
325
325
|
|
|
326
326
|
if not make_object:
|
|
327
|
-
|
|
328
|
-
def make_object(values: Sequence[Any], info: CompositeInfo) -> T:
|
|
329
|
-
return factory(*values)
|
|
327
|
+
make_object = _make_object_from_factory(factory)
|
|
330
328
|
|
|
331
329
|
adapters = context.adapters if context else postgres.adapters
|
|
332
330
|
|
|
@@ -353,17 +351,10 @@ def register_composite(
|
|
|
353
351
|
" argument in the `register_composite()` call",
|
|
354
352
|
factory.__name__,
|
|
355
353
|
)
|
|
356
|
-
|
|
357
|
-
def make_sequence(obj: T, into: CompositeInfo) -> Sequence[Any]:
|
|
358
|
-
raise TypeError(
|
|
359
|
-
f"{type(obj).__name__!r} objects cannot be dumped without"
|
|
360
|
-
" specifying 'make_sequence' in 'register_composite()'"
|
|
361
|
-
)
|
|
354
|
+
make_sequence = _make_sequence_unsupported
|
|
362
355
|
|
|
363
356
|
else:
|
|
364
|
-
|
|
365
|
-
def make_sequence(obj: T, info: CompositeInfo) -> Sequence[Any]:
|
|
366
|
-
return obj # type: ignore[return-value] # it's a sequence
|
|
357
|
+
make_sequence = _make_sequence_from_sequence
|
|
367
358
|
|
|
368
359
|
type_name = factory.__name__
|
|
369
360
|
dumper: type[Dumper] = _make_binary_dumper(type_name, info, make_sequence)
|
|
@@ -514,6 +505,25 @@ def _make_nt(name: str, fields: tuple[str, ...]) -> type[NamedTuple]:
|
|
|
514
505
|
return namedtuple(name, fields) # type: ignore[return-value]
|
|
515
506
|
|
|
516
507
|
|
|
508
|
+
@cache
|
|
509
|
+
def _make_object_from_factory(factory: Callable[..., T]) -> ObjectMaker[T]:
|
|
510
|
+
def make_object(values: Sequence[Any], info: CompositeInfo) -> T:
|
|
511
|
+
return factory(*values)
|
|
512
|
+
|
|
513
|
+
return make_object
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _make_sequence_from_sequence(obj: Any, info: CompositeInfo) -> Sequence[Any]:
|
|
517
|
+
return obj # type: ignore[no-any-return] # it's a sequence
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _make_sequence_unsupported(obj: Any, info: CompositeInfo) -> Sequence[Any]:
|
|
521
|
+
raise TypeError(
|
|
522
|
+
f"{type(obj).__name__!r} objects cannot be dumped without"
|
|
523
|
+
" specifying 'make_sequence' in 'register_composite()'"
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
|
|
517
527
|
@cache
|
|
518
528
|
def _make_loader(
|
|
519
529
|
info: CompositeInfo, make_object: ObjectMaker[T]
|
|
@@ -613,7 +613,8 @@ class IntervalLoader(Loader):
|
|
|
613
613
|
|
|
614
614
|
ye, mo, da, sgn, ho, mi, se = m.groups()
|
|
615
615
|
days = 0
|
|
616
|
-
seconds = 0
|
|
616
|
+
seconds = 0
|
|
617
|
+
micros = 0
|
|
617
618
|
|
|
618
619
|
if ye:
|
|
619
620
|
days += 365 * int(ye)
|
|
@@ -623,12 +624,14 @@ class IntervalLoader(Loader):
|
|
|
623
624
|
days += int(da)
|
|
624
625
|
|
|
625
626
|
if ho:
|
|
626
|
-
seconds = 3600 * int(ho) + 60 * int(mi)
|
|
627
|
+
seconds = 3600 * int(ho) + 60 * int(mi)
|
|
628
|
+
micros = round(float(se) * 1_000_000)
|
|
627
629
|
if sgn == b"-":
|
|
628
630
|
seconds = -seconds
|
|
631
|
+
micros = -micros
|
|
629
632
|
|
|
630
633
|
try:
|
|
631
|
-
return timedelta(days=days, seconds=seconds)
|
|
634
|
+
return timedelta(days=days, seconds=seconds, microseconds=micros)
|
|
632
635
|
except OverflowError as e:
|
|
633
636
|
s = bytes(data).decode("utf8", "replace")
|
|
634
637
|
raise DataError(f"can't parse interval {s!r}: {e}") from None
|
|
@@ -296,7 +296,7 @@ _default_dumpers: dict[tuple[type[_JsonWrapper], PyFormat], type[Dumper]] = {
|
|
|
296
296
|
(Json, PyFormat.BINARY): JsonBinaryDumper,
|
|
297
297
|
(Json, PyFormat.TEXT): JsonDumper,
|
|
298
298
|
(Jsonb, PyFormat.BINARY): JsonbBinaryDumper,
|
|
299
|
-
(Jsonb, PyFormat.TEXT):
|
|
299
|
+
(Jsonb, PyFormat.TEXT): JsonbDumper,
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: psycopg
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.7
|
|
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
|
|
@@ -34,9 +34,9 @@ License-File: LICENSE.txt
|
|
|
34
34
|
Requires-Dist: typing-extensions>=4.6; python_version < "3.13"
|
|
35
35
|
Requires-Dist: tzdata; sys_platform == "win32"
|
|
36
36
|
Provides-Extra: c
|
|
37
|
-
Requires-Dist: psycopg-c==3.3.
|
|
37
|
+
Requires-Dist: psycopg-c==3.3.7; implementation_name != "pypy" and extra == "c"
|
|
38
38
|
Provides-Extra: binary
|
|
39
|
-
Requires-Dist: psycopg-binary==3.3.
|
|
39
|
+
Requires-Dist: psycopg-binary==3.3.7; implementation_name != "pypy" and extra == "binary"
|
|
40
40
|
Provides-Extra: pool
|
|
41
41
|
Requires-Dist: psycopg-pool; extra == "pool"
|
|
42
42
|
Provides-Extra: test
|
|
@@ -7,7 +7,7 @@ name = "psycopg"
|
|
|
7
7
|
description = "PostgreSQL database adapter for Python"
|
|
8
8
|
|
|
9
9
|
# STOP AND READ! if you change:
|
|
10
|
-
version = "3.3.
|
|
10
|
+
version = "3.3.7"
|
|
11
11
|
# also change:
|
|
12
12
|
# - `docs/news.rst` to declare this as the current version or an unreleased one;
|
|
13
13
|
# - `psycopg_c/pyproject.toml` to the same version;
|
|
@@ -61,10 +61,10 @@ content-type = "text/x-rst"
|
|
|
61
61
|
|
|
62
62
|
[project.optional-dependencies]
|
|
63
63
|
c = [
|
|
64
|
-
"psycopg-c == 3.3.
|
|
64
|
+
"psycopg-c == 3.3.7; implementation_name != \"pypy\"",
|
|
65
65
|
]
|
|
66
66
|
binary = [
|
|
67
|
-
"psycopg-binary == 3.3.
|
|
67
|
+
"psycopg-binary == 3.3.7; implementation_name != \"pypy\"",
|
|
68
68
|
]
|
|
69
69
|
pool = [
|
|
70
70
|
"psycopg-pool",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|