camusdb-py 0.1.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.
- camusdb/__init__.py +101 -0
- camusdb/_auth.py +122 -0
- camusdb/_base.py +300 -0
- camusdb/_codec.py +412 -0
- camusdb/_config.py +362 -0
- camusdb/_endpoints.py +59 -0
- camusdb/_protocol.py +335 -0
- camusdb/_sql.py +122 -0
- camusdb/_transport.py +217 -0
- camusdb/aio/__init__.py +15 -0
- camusdb/aio/_transport.py +189 -0
- camusdb/aio/connection.py +596 -0
- camusdb/codes.py +66 -0
- camusdb/connection.py +632 -0
- camusdb/errors.py +146 -0
- camusdb/py.typed +0 -0
- camusdb/retry.py +44 -0
- camusdb/rows.py +41 -0
- camusdb/types.py +209 -0
- camusdb_py-0.1.0.dist-info/METADATA +545 -0
- camusdb_py-0.1.0.dist-info/RECORD +23 -0
- camusdb_py-0.1.0.dist-info/WHEEL +4 -0
- camusdb_py-0.1.0.dist-info/licenses/LICENSE +21 -0
camusdb/__init__.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CamusDB driver for Python: PEP 249 (DB-API 2.0), with an asyncio interface in :mod:`camusdb.aio`.
|
|
2
|
+
|
|
3
|
+
::
|
|
4
|
+
|
|
5
|
+
import camusdb
|
|
6
|
+
|
|
7
|
+
with camusdb.connect("Endpoint=http://localhost:5095;Database=test") as conn:
|
|
8
|
+
cur = conn.execute("SELECT name, year FROM robots WHERE year > @year", {"year": 1980})
|
|
9
|
+
for name, year in cur:
|
|
10
|
+
print(name, year)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from . import codes, rows
|
|
14
|
+
from ._config import redact as redact_connection_string
|
|
15
|
+
from ._protocol import Branch
|
|
16
|
+
from .connection import Connection, Cursor, connect, run_transaction
|
|
17
|
+
from .errors import (
|
|
18
|
+
DatabaseError,
|
|
19
|
+
DataError,
|
|
20
|
+
Error,
|
|
21
|
+
IntegrityError,
|
|
22
|
+
InterfaceError,
|
|
23
|
+
InternalError,
|
|
24
|
+
NotSupportedError,
|
|
25
|
+
OperationalError,
|
|
26
|
+
ProgrammingError,
|
|
27
|
+
SerializationFailure,
|
|
28
|
+
Warning,
|
|
29
|
+
)
|
|
30
|
+
from .retry import is_retryable
|
|
31
|
+
from .types import (
|
|
32
|
+
BINARY,
|
|
33
|
+
DATETIME,
|
|
34
|
+
NUMBER,
|
|
35
|
+
ROWID,
|
|
36
|
+
STRING,
|
|
37
|
+
Binary,
|
|
38
|
+
ColumnType,
|
|
39
|
+
Date,
|
|
40
|
+
DateFromTicks,
|
|
41
|
+
IsolationLevel,
|
|
42
|
+
Locking,
|
|
43
|
+
ObjectId,
|
|
44
|
+
Time,
|
|
45
|
+
TimeFromTicks,
|
|
46
|
+
Timestamp,
|
|
47
|
+
TimestampFromTicks,
|
|
48
|
+
Typed,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__version__ = "0.1.0"
|
|
52
|
+
|
|
53
|
+
# PEP 249 module globals.
|
|
54
|
+
apilevel = "2.0"
|
|
55
|
+
# Threads may share the module, but not connections.
|
|
56
|
+
threadsafety = 1
|
|
57
|
+
# Placeholders are named, and they are written with "@", as the server reads them: WHERE id = @id.
|
|
58
|
+
paramstyle = "named"
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"BINARY",
|
|
62
|
+
"DATETIME",
|
|
63
|
+
"NUMBER",
|
|
64
|
+
"ROWID",
|
|
65
|
+
"STRING",
|
|
66
|
+
"Binary",
|
|
67
|
+
"Branch",
|
|
68
|
+
"ColumnType",
|
|
69
|
+
"Connection",
|
|
70
|
+
"Cursor",
|
|
71
|
+
"DataError",
|
|
72
|
+
"DatabaseError",
|
|
73
|
+
"Date",
|
|
74
|
+
"DateFromTicks",
|
|
75
|
+
"Error",
|
|
76
|
+
"IntegrityError",
|
|
77
|
+
"InterfaceError",
|
|
78
|
+
"InternalError",
|
|
79
|
+
"IsolationLevel",
|
|
80
|
+
"Locking",
|
|
81
|
+
"NotSupportedError",
|
|
82
|
+
"ObjectId",
|
|
83
|
+
"OperationalError",
|
|
84
|
+
"ProgrammingError",
|
|
85
|
+
"SerializationFailure",
|
|
86
|
+
"Time",
|
|
87
|
+
"TimeFromTicks",
|
|
88
|
+
"Timestamp",
|
|
89
|
+
"TimestampFromTicks",
|
|
90
|
+
"Typed",
|
|
91
|
+
"Warning",
|
|
92
|
+
"apilevel",
|
|
93
|
+
"codes",
|
|
94
|
+
"connect",
|
|
95
|
+
"is_retryable",
|
|
96
|
+
"paramstyle",
|
|
97
|
+
"redact_connection_string",
|
|
98
|
+
"rows",
|
|
99
|
+
"run_transaction",
|
|
100
|
+
"threadsafety",
|
|
101
|
+
]
|
camusdb/_auth.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Bearer tokens: one login for each set of credentials, shared by the whole process.
|
|
2
|
+
|
|
3
|
+
The server verifies a password slowly on purpose (PBKDF2) and limits logins to 20 for each account
|
|
4
|
+
each minute. Thus the token is cached for each (deployment, user, password), and every connection
|
|
5
|
+
with the same credentials uses it. A token is renewed when it reaches 80% of its lifetime. A token
|
|
6
|
+
that reaches 95% is never sent, because the server can expire it while the request travels.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from ._config import Config
|
|
17
|
+
|
|
18
|
+
RENEW_AT = 0.80
|
|
19
|
+
EXPIRE_AT = 0.95
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Token:
|
|
24
|
+
value: str
|
|
25
|
+
issued: float # time.monotonic() when the login reply arrived
|
|
26
|
+
lifetime: float # seconds
|
|
27
|
+
|
|
28
|
+
def age_fraction(self) -> float:
|
|
29
|
+
return (time.monotonic() - self.issued) / self.lifetime
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def fresh(self) -> bool:
|
|
33
|
+
"""The token is young enough that it needs no renewal."""
|
|
34
|
+
return self.age_fraction() < RENEW_AT
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def usable(self) -> bool:
|
|
38
|
+
"""The token can still be sent, while a renewal is not available."""
|
|
39
|
+
return self.age_fraction() < EXPIRE_AT
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _TokenStore:
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
self._tokens: dict[tuple[object, ...], Token] = {}
|
|
45
|
+
self._login_locks: dict[tuple[object, ...], threading.Lock] = {}
|
|
46
|
+
self._lock = threading.Lock()
|
|
47
|
+
|
|
48
|
+
def get(self, key: tuple[object, ...]) -> Token | None:
|
|
49
|
+
with self._lock:
|
|
50
|
+
return self._tokens.get(key)
|
|
51
|
+
|
|
52
|
+
def put(self, key: tuple[object, ...], token: Token) -> None:
|
|
53
|
+
with self._lock:
|
|
54
|
+
self._tokens[key] = token
|
|
55
|
+
|
|
56
|
+
def discard(self, key: tuple[object, ...], value: str | None = None) -> None:
|
|
57
|
+
"""Forget the token, but only if it is still ``value``: a newer token stays."""
|
|
58
|
+
with self._lock:
|
|
59
|
+
current = self._tokens.get(key)
|
|
60
|
+
if current is not None and (value is None or current.value == value):
|
|
61
|
+
del self._tokens[key]
|
|
62
|
+
|
|
63
|
+
def login_lock(self, key: tuple[object, ...]) -> threading.Lock:
|
|
64
|
+
with self._lock:
|
|
65
|
+
return self._login_locks.setdefault(key, threading.Lock())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
TOKENS = _TokenStore()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class AuthState:
|
|
72
|
+
"""The credentials of one connection, and access to the shared token for them."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, config: Config) -> None:
|
|
75
|
+
self._endpoints = config.endpoints
|
|
76
|
+
self.default_lifetime = config.token_lifetime
|
|
77
|
+
self.access_token = config.access_token
|
|
78
|
+
self.user: str | None = None
|
|
79
|
+
self._password: str | None = None
|
|
80
|
+
self._key: tuple[object, ...] = ()
|
|
81
|
+
self.set_credentials(config.user, config.password)
|
|
82
|
+
|
|
83
|
+
def set_credentials(self, user: str | None, password: str | None) -> None:
|
|
84
|
+
self.user = user
|
|
85
|
+
self._password = password
|
|
86
|
+
digest = hashlib.sha256(password.encode()).hexdigest() if password is not None else None
|
|
87
|
+
self._key = (self._endpoints, user, digest)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def can_login(self) -> bool:
|
|
91
|
+
return self.user is not None and self._password is not None
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def password(self) -> str:
|
|
95
|
+
assert self._password is not None
|
|
96
|
+
return self._password
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def enabled(self) -> bool:
|
|
100
|
+
return self.access_token is not None or self.can_login
|
|
101
|
+
|
|
102
|
+
def cached(self) -> Token | None:
|
|
103
|
+
return TOKENS.get(self._key) if self.can_login else None
|
|
104
|
+
|
|
105
|
+
def store(self, value: str, expires_in: float | None) -> Token:
|
|
106
|
+
lifetime = expires_in if expires_in and expires_in > 0 else self.default_lifetime
|
|
107
|
+
token = Token(value, time.monotonic(), lifetime)
|
|
108
|
+
TOKENS.put(self._key, token)
|
|
109
|
+
return token
|
|
110
|
+
|
|
111
|
+
def discard(self, value: str | None = None) -> None:
|
|
112
|
+
TOKENS.discard(self._key, value)
|
|
113
|
+
|
|
114
|
+
def login_lock(self) -> threading.Lock:
|
|
115
|
+
return TOKENS.login_lock(self._key)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def current_token(self) -> str | None:
|
|
119
|
+
if self.access_token is not None:
|
|
120
|
+
return self.access_token
|
|
121
|
+
token = self.cached()
|
|
122
|
+
return token.value if token is not None and token.usable else None
|
camusdb/_base.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""State and rules that the sync and the async connection and cursor share."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Iterator, Mapping
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import _protocol as proto
|
|
10
|
+
from . import _sql, codes
|
|
11
|
+
from ._codec import encode_parameters
|
|
12
|
+
from ._config import Config, coerce_isolation, coerce_locking
|
|
13
|
+
from .errors import InterfaceError, InternalError, ProgrammingError
|
|
14
|
+
from .rows import RowFactory, RowMaker, tuple_row
|
|
15
|
+
from .types import IsolationLevel, Locking
|
|
16
|
+
|
|
17
|
+
# A COMMIT or ROLLBACK whose outcome is not resolved yet (CADB0509) is sent again, the same one on the
|
|
18
|
+
# same transaction, never replayed from the start: the commit may already have applied.
|
|
19
|
+
MAX_FINALIZE_ATTEMPTS = 10
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def finalize_delay(attempt: int) -> float:
|
|
23
|
+
"""50 ms x 2^attempt, at most 3.2 s: the server's contract for an unresolved finalize."""
|
|
24
|
+
return 0.05 * (1 << min(attempt, 6))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def retry_delay(attempt: int) -> float:
|
|
28
|
+
"""min(20 ms x 2^attempt, 400 ms), with 25% jitter: the back-off between transaction replays."""
|
|
29
|
+
base = min(0.02 * (1 << attempt), 0.4)
|
|
30
|
+
return max(0.001, base + base * 0.25 * (2 * random.random() - 1))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def aborted_error() -> InternalError:
|
|
34
|
+
return InternalError(
|
|
35
|
+
"the current transaction failed and the server discarded it; call rollback() before you run "
|
|
36
|
+
"another statement",
|
|
37
|
+
codes.UNKNOWN,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConnectionState:
|
|
42
|
+
"""Transaction state and settings of one connection."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, config: Config, row_factory: RowFactory | None) -> None:
|
|
45
|
+
self.config = config
|
|
46
|
+
self.database = config.database
|
|
47
|
+
self.row_factory: RowFactory = row_factory or tuple_row
|
|
48
|
+
self.closed = False
|
|
49
|
+
self._autocommit = config.autocommit
|
|
50
|
+
self._isolation_level = config.isolation_level
|
|
51
|
+
self._read_only = config.read_only
|
|
52
|
+
self._locking = config.locking
|
|
53
|
+
self.txn: proto.TransactionHandle | None = None
|
|
54
|
+
# True after a statement of the transaction failed. The server discarded the transaction.
|
|
55
|
+
self.txn_failed = False
|
|
56
|
+
|
|
57
|
+
# --- Settings ---------------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
def _check_settable(self, name: str) -> None:
|
|
60
|
+
if self.txn is not None:
|
|
61
|
+
raise ProgrammingError(
|
|
62
|
+
f"cannot change {name} while a transaction is active", codes.INVALID_INPUT
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def autocommit(self) -> bool:
|
|
67
|
+
return self._autocommit
|
|
68
|
+
|
|
69
|
+
@autocommit.setter
|
|
70
|
+
def autocommit(self, value: bool) -> None:
|
|
71
|
+
self._check_settable("autocommit")
|
|
72
|
+
self._autocommit = bool(value)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def isolation_level(self) -> IsolationLevel | None:
|
|
76
|
+
return self._isolation_level
|
|
77
|
+
|
|
78
|
+
@isolation_level.setter
|
|
79
|
+
def isolation_level(self, value: IsolationLevel | str | None) -> None:
|
|
80
|
+
self._check_settable("isolation_level")
|
|
81
|
+
self._isolation_level = coerce_isolation(value)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def read_only(self) -> bool | None:
|
|
85
|
+
return self._read_only
|
|
86
|
+
|
|
87
|
+
@read_only.setter
|
|
88
|
+
def read_only(self, value: bool | None) -> None:
|
|
89
|
+
self._check_settable("read_only")
|
|
90
|
+
self._read_only = None if value is None else bool(value)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def locking(self) -> Locking | None:
|
|
94
|
+
return self._locking
|
|
95
|
+
|
|
96
|
+
@locking.setter
|
|
97
|
+
def locking(self, value: Locking | str | None) -> None:
|
|
98
|
+
self._check_settable("locking")
|
|
99
|
+
self._locking = coerce_locking(value)
|
|
100
|
+
|
|
101
|
+
def set_database(self, name: str) -> None:
|
|
102
|
+
self._check_settable("the database")
|
|
103
|
+
if not name:
|
|
104
|
+
raise ProgrammingError("a database name is required", codes.INVALID_INPUT)
|
|
105
|
+
self.database = name
|
|
106
|
+
|
|
107
|
+
def options(
|
|
108
|
+
self,
|
|
109
|
+
isolation_level: IsolationLevel | str | None = None,
|
|
110
|
+
read_only: bool | None = None,
|
|
111
|
+
locking: Locking | str | None = None,
|
|
112
|
+
) -> proto.TransactionOptions:
|
|
113
|
+
"""The options of a transaction: each one given here, else the connection's setting."""
|
|
114
|
+
return proto.TransactionOptions(
|
|
115
|
+
isolation_level=coerce_isolation(isolation_level) or self._isolation_level,
|
|
116
|
+
read_only=self._read_only if read_only is None else read_only,
|
|
117
|
+
locking=coerce_locking(locking) or self._locking,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# --- Rules ------------------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def check_open(self) -> None:
|
|
123
|
+
if self.closed:
|
|
124
|
+
raise InterfaceError("the connection is closed")
|
|
125
|
+
|
|
126
|
+
def check_usable(self) -> None:
|
|
127
|
+
self.check_open()
|
|
128
|
+
if self.txn_failed:
|
|
129
|
+
raise aborted_error()
|
|
130
|
+
|
|
131
|
+
def needs_begin(self) -> bool:
|
|
132
|
+
"""Whether a statement must begin the implicit transaction first (PEP 249 mode)."""
|
|
133
|
+
return self.txn is None and not self._autocommit
|
|
134
|
+
|
|
135
|
+
def statement_failed(self) -> None:
|
|
136
|
+
"""Record that a statement sent in the transaction failed.
|
|
137
|
+
|
|
138
|
+
The server discards a transaction when one of its statements fails. Thus a later statement
|
|
139
|
+
must not run as if the transaction were still there. A failure before the request was sent,
|
|
140
|
+
such as a value that cannot be bound, does not come here.
|
|
141
|
+
"""
|
|
142
|
+
if self.txn is not None:
|
|
143
|
+
self.txn_failed = True
|
|
144
|
+
|
|
145
|
+
def end_transaction(self) -> None:
|
|
146
|
+
self.txn = None
|
|
147
|
+
self.txn_failed = False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Plan:
|
|
151
|
+
"""What one ``execute`` sends: the route, the body builder input and the target."""
|
|
152
|
+
|
|
153
|
+
__slots__ = ("kind", "parameters", "sql")
|
|
154
|
+
|
|
155
|
+
def __init__(self, sql: str, parameters: Mapping[str, Any] | None) -> None:
|
|
156
|
+
if not isinstance(sql, str) or not sql.strip():
|
|
157
|
+
raise ProgrammingError("the statement is empty", codes.INVALID_INPUT)
|
|
158
|
+
self.sql = sql
|
|
159
|
+
self.kind = _sql.classify(sql)
|
|
160
|
+
if self.kind is _sql.StatementKind.DDL and parameters:
|
|
161
|
+
raise ProgrammingError("a DDL statement takes no parameters", codes.INVALID_INPUT)
|
|
162
|
+
self.parameters = encode_parameters(parameters)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def route(self) -> str:
|
|
166
|
+
if self.kind is _sql.StatementKind.QUERY:
|
|
167
|
+
return proto.EXECUTE_QUERY
|
|
168
|
+
if self.kind is _sql.StatementKind.DDL:
|
|
169
|
+
return proto.EXECUTE_DDL
|
|
170
|
+
return proto.EXECUTE_NON_QUERY
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def begins_transaction(self) -> bool:
|
|
174
|
+
# A schema change commits on its own, so it does not open the implicit transaction.
|
|
175
|
+
return self.kind is not _sql.StatementKind.DDL
|
|
176
|
+
|
|
177
|
+
def check_transaction(self, txn: proto.TransactionHandle | None) -> None:
|
|
178
|
+
if txn is not None and _sql.runs_in_own_transaction(self.sql):
|
|
179
|
+
raise ProgrammingError(
|
|
180
|
+
"TRUNCATE runs in its own transaction, which a ROLLBACK cannot undo, so it is refused "
|
|
181
|
+
"inside a transaction. Commit or roll back first, or use autocommit",
|
|
182
|
+
codes.STATEMENT_NOT_ALLOWED_IN_TRANSACTION,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def body(self, state: ConnectionState, txn: proto.TransactionHandle | None) -> dict[str, Any]:
|
|
186
|
+
# Reads run in a snapshot with no locking mode, so only writes carry the autocommit options.
|
|
187
|
+
autocommit = state.options() if self.kind is not _sql.StatementKind.QUERY else None
|
|
188
|
+
return proto.sql_body(state.database, self.sql, self.parameters, txn, autocommit)
|
|
189
|
+
|
|
190
|
+
def result(self, body: Mapping[str, Any]) -> proto.Result:
|
|
191
|
+
if self.kind is _sql.StatementKind.QUERY:
|
|
192
|
+
return proto.parse_query(body)
|
|
193
|
+
if self.kind is _sql.StatementKind.DDL:
|
|
194
|
+
proto.expect_ok(body, "DDL statement failed")
|
|
195
|
+
return proto.Result(None, [], -1)
|
|
196
|
+
return proto.parse_non_query(body)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class StreamPlan(Plan):
|
|
200
|
+
"""A statement for the streaming route: a read, or a write with a RETURNING list."""
|
|
201
|
+
|
|
202
|
+
def __init__(self, sql: str, parameters: Mapping[str, Any] | None) -> None:
|
|
203
|
+
super().__init__(sql, parameters)
|
|
204
|
+
if self.kind is _sql.StatementKind.DDL:
|
|
205
|
+
raise ProgrammingError(
|
|
206
|
+
"stream() runs queries; use execute() for DDL", codes.INVALID_INPUT
|
|
207
|
+
)
|
|
208
|
+
self.write = _sql.is_dml(sql)
|
|
209
|
+
# A write without RETURNING has no rows to stream; it runs on its own route.
|
|
210
|
+
self.streams = not self.write or _sql.has_returning(sql)
|
|
211
|
+
|
|
212
|
+
def body(self, state: ConnectionState, txn: proto.TransactionHandle | None) -> dict[str, Any]:
|
|
213
|
+
autocommit = state.options() if self.write else None
|
|
214
|
+
return proto.sql_body(state.database, self.sql, self.parameters, txn, autocommit)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class CursorState:
|
|
218
|
+
"""The result buffer and the PEP 249 attributes of a cursor."""
|
|
219
|
+
|
|
220
|
+
def __init__(self, row_factory: RowFactory) -> None:
|
|
221
|
+
self.row_factory = row_factory
|
|
222
|
+
self.arraysize = 1
|
|
223
|
+
self.description: tuple[tuple[Any, ...], ...] | None = None
|
|
224
|
+
self.rowcount = -1
|
|
225
|
+
self.closed = False
|
|
226
|
+
self._rows: list[tuple[Any, ...]] = []
|
|
227
|
+
self._pos = 0
|
|
228
|
+
self._make: RowMaker = tuple_row(())
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def rownumber(self) -> int | None:
|
|
232
|
+
return self._pos if self.description is not None else None
|
|
233
|
+
|
|
234
|
+
def reset(self) -> None:
|
|
235
|
+
self.description = None
|
|
236
|
+
self.rowcount = -1
|
|
237
|
+
self._rows = []
|
|
238
|
+
self._pos = 0
|
|
239
|
+
|
|
240
|
+
def set_result(self, result: proto.Result) -> None:
|
|
241
|
+
self.description = proto.description(result.schema)
|
|
242
|
+
self._make = (
|
|
243
|
+
self.row_factory(self.description) if self.description is not None else tuple_row(())
|
|
244
|
+
)
|
|
245
|
+
self._rows = result.rows
|
|
246
|
+
self._pos = 0
|
|
247
|
+
self.rowcount = result.rowcount
|
|
248
|
+
|
|
249
|
+
def set_streaming(self, description: tuple[tuple[Any, ...], ...] | None) -> RowMaker:
|
|
250
|
+
self.description = description
|
|
251
|
+
self._rows = []
|
|
252
|
+
self._pos = 0
|
|
253
|
+
self.rowcount = -1
|
|
254
|
+
self._make = self.row_factory(description) if description is not None else tuple_row(())
|
|
255
|
+
return self._make
|
|
256
|
+
|
|
257
|
+
def check_open(self) -> None:
|
|
258
|
+
if self.closed:
|
|
259
|
+
raise InterfaceError("the cursor is closed")
|
|
260
|
+
|
|
261
|
+
def check_result(self) -> None:
|
|
262
|
+
self.check_open()
|
|
263
|
+
if self.description is None:
|
|
264
|
+
raise ProgrammingError("the last statement returned no rows", codes.INVALID_INPUT)
|
|
265
|
+
|
|
266
|
+
def fetchone(self) -> Any:
|
|
267
|
+
self.check_result()
|
|
268
|
+
if self._pos >= len(self._rows):
|
|
269
|
+
return None
|
|
270
|
+
row = self._rows[self._pos]
|
|
271
|
+
self._pos += 1
|
|
272
|
+
return self._make(row)
|
|
273
|
+
|
|
274
|
+
def fetchmany(self, size: int | None = None) -> list[Any]:
|
|
275
|
+
self.check_result()
|
|
276
|
+
count = self.arraysize if size is None else size
|
|
277
|
+
rows = self._rows[self._pos : self._pos + max(count, 0)]
|
|
278
|
+
self._pos += len(rows)
|
|
279
|
+
return [self._make(row) for row in rows]
|
|
280
|
+
|
|
281
|
+
def fetchall(self) -> list[Any]:
|
|
282
|
+
self.check_result()
|
|
283
|
+
rows = self._rows[self._pos :]
|
|
284
|
+
self._pos = len(self._rows)
|
|
285
|
+
return [self._make(row) for row in rows]
|
|
286
|
+
|
|
287
|
+
def scroll(self, value: int, mode: str = "relative") -> None:
|
|
288
|
+
self.check_result()
|
|
289
|
+
position = self._pos + value if mode == "relative" else value
|
|
290
|
+
if mode not in ("relative", "absolute"):
|
|
291
|
+
raise ProgrammingError(
|
|
292
|
+
f"scroll mode {mode!r} is not 'relative' or 'absolute'", codes.INVALID_INPUT
|
|
293
|
+
)
|
|
294
|
+
if not 0 <= position <= len(self._rows):
|
|
295
|
+
raise IndexError("scroll target is out of the result set")
|
|
296
|
+
self._pos = position
|
|
297
|
+
|
|
298
|
+
def iter_buffered(self) -> Iterator[Any]:
|
|
299
|
+
while (row := self.fetchone()) is not None:
|
|
300
|
+
yield row
|