sqlalchemy-boltons 1.0.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.
- sqlalchemy_boltons/__init__.py +0 -0
- sqlalchemy_boltons/py.typed +0 -0
- sqlalchemy_boltons/sqlite.py +224 -0
- sqlalchemy_boltons-1.0.0.dist-info/AUTHORS.md +9 -0
- sqlalchemy_boltons-1.0.0.dist-info/LICENSE +0 -0
- sqlalchemy_boltons-1.0.0.dist-info/METADATA +57 -0
- sqlalchemy_boltons-1.0.0.dist-info/RECORD +9 -0
- sqlalchemy_boltons-1.0.0.dist-info/WHEEL +5 -0
- sqlalchemy_boltons-1.0.0.dist-info/top_level.txt +1 -0
File without changes
|
File without changes
|
@@ -0,0 +1,224 @@
|
|
1
|
+
import dataclasses as dc
|
2
|
+
import functools
|
3
|
+
from pathlib import Path
|
4
|
+
import typing as ty
|
5
|
+
from urllib import parse as _up
|
6
|
+
|
7
|
+
import sqlalchemy as sa
|
8
|
+
from sqlalchemy import pool as sap
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"create_engine_sqlite",
|
12
|
+
"SQLAlchemySqliteTransactionFix",
|
13
|
+
"sqlite_file_uri",
|
14
|
+
"sqlite_journal_mode",
|
15
|
+
"MissingExecutionOptionError",
|
16
|
+
]
|
17
|
+
|
18
|
+
|
19
|
+
class MissingExecutionOptionError(ValueError):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
@functools.lru_cache(maxsize=64)
|
24
|
+
def make_journal_mode_statement(mode: str | None) -> str:
|
25
|
+
if mode:
|
26
|
+
if not mode.isalnum():
|
27
|
+
raise ValueError(f"invalid mode {mode!r}")
|
28
|
+
return f"PRAGMA journal_mode={mode}"
|
29
|
+
else:
|
30
|
+
return "PRAGMA journal_mode"
|
31
|
+
|
32
|
+
|
33
|
+
@functools.lru_cache(maxsize=64)
|
34
|
+
def make_begin_statement(mode: str | None) -> str:
|
35
|
+
if mode and not mode.isalpha():
|
36
|
+
raise ValueError(f"invalid mode {mode!r}")
|
37
|
+
return f"BEGIN {mode}" if mode else "BEGIN"
|
38
|
+
|
39
|
+
|
40
|
+
@functools.lru_cache(maxsize=64)
|
41
|
+
def make_foreign_keys_settings(foreign_keys: bool | str) -> tuple[str, str | None]:
|
42
|
+
if foreign_keys is True:
|
43
|
+
return "PRAGMA foreign_keys=1", None
|
44
|
+
elif foreign_keys is False:
|
45
|
+
return "PRAGMA foreign_keys=0", None
|
46
|
+
elif foreign_keys == "defer":
|
47
|
+
return "PRAGMA foreign_keys=1", "PRAGMA defer_foreign_keys=1"
|
48
|
+
else:
|
49
|
+
raise ValueError("invalid value {foreign_keys!r} for x_sqlite_foreign_keys")
|
50
|
+
|
51
|
+
|
52
|
+
class SQLAlchemySqliteTransactionFix:
|
53
|
+
"""
|
54
|
+
This class exists because sqlalchemy doesn't automatically fix pysqlite's stupid default behaviour. Additionally,
|
55
|
+
we implement support for foreign keys.
|
56
|
+
|
57
|
+
https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl
|
58
|
+
"""
|
59
|
+
|
60
|
+
def register(self, engine):
|
61
|
+
sa.event.listen(engine, "connect", self.event_connect)
|
62
|
+
sa.event.listen(engine, "begin", self.event_begin)
|
63
|
+
|
64
|
+
def make_journal_mode_statement(self, mode: str | None):
|
65
|
+
return make_journal_mode_statement(mode)
|
66
|
+
|
67
|
+
def make_begin_statement(self, mode: str | None):
|
68
|
+
return make_begin_statement(mode)
|
69
|
+
|
70
|
+
def make_foreign_keys_settings(self, foreign_keys: bool | str) -> tuple[str, str | None]:
|
71
|
+
return make_foreign_keys_settings(foreign_keys)
|
72
|
+
|
73
|
+
def event_connect(self, dbapi_connection, connection_record):
|
74
|
+
# disable pysqlite's emitting of the BEGIN statement entirely.
|
75
|
+
# also stops it from emitting COMMIT before any DDL.
|
76
|
+
dbapi_connection.isolation_level = None
|
77
|
+
|
78
|
+
def event_begin(self, conn):
|
79
|
+
execution_options = conn.get_execution_options()
|
80
|
+
|
81
|
+
try:
|
82
|
+
opt_begin = execution_options["x_sqlite_begin_mode"]
|
83
|
+
opt_fk = execution_options["x_sqlite_foreign_keys"]
|
84
|
+
except KeyError as exc:
|
85
|
+
raise MissingExecutionOptionError(
|
86
|
+
"You must configure your engine (or connection) execution options. "
|
87
|
+
"For example:\n\n"
|
88
|
+
" engine = create_engine_sqlite(...)\n"
|
89
|
+
' engine = engine.execution_options(x_sqlite_foreign_keys="defer")\n'
|
90
|
+
" engine_ro = engine.execution_options(x_sqlite_begin_mode=None)\n"
|
91
|
+
' engine_rw = engine.execution_options(x_sqlite_begin_mode="IMMEDIATE")'
|
92
|
+
) from exc
|
93
|
+
begin = self.make_begin_statement(opt_begin)
|
94
|
+
before, after = self.make_foreign_keys_settings(opt_fk)
|
95
|
+
|
96
|
+
if mode := execution_options.get("x_sqlite_journal_mode"):
|
97
|
+
conn.exec_driver_sql(self.make_journal_mode_statement(mode)).close()
|
98
|
+
|
99
|
+
conn.exec_driver_sql(before).close()
|
100
|
+
conn.exec_driver_sql(begin).close()
|
101
|
+
if after:
|
102
|
+
conn.exec_driver_sql(after).close()
|
103
|
+
|
104
|
+
|
105
|
+
@dc.dataclass
|
106
|
+
class Memory:
|
107
|
+
"""
|
108
|
+
We keep a reference to an open connection because SQLite will free up the database otherwise.
|
109
|
+
|
110
|
+
Note that you still can't get concurrent readers and writers because you cannot currently set WAL mode on an
|
111
|
+
in-memory database. See https://sqlite.org/forum/info/6700ab1f9f6e8a00
|
112
|
+
"""
|
113
|
+
|
114
|
+
uri: str = dc.field(init=False)
|
115
|
+
connection_reference = None
|
116
|
+
|
117
|
+
def __post_init__(self):
|
118
|
+
self.uri = f"file:/sqlalchemy_boltons_memdb_{id(self)}"
|
119
|
+
|
120
|
+
def as_uri(self):
|
121
|
+
return self.uri
|
122
|
+
|
123
|
+
|
124
|
+
def sqlite_file_uri(path: Path | str | Memory, parameters: ty.Sequence[tuple[str | bytes, str | bytes]] = ()) -> str:
|
125
|
+
if isinstance(path, str):
|
126
|
+
path = Path(path)
|
127
|
+
if isinstance(path, Path) and not path.is_absolute():
|
128
|
+
path = path.absolute()
|
129
|
+
|
130
|
+
qs = _up.urlencode(parameters, quote_via=_up.quote)
|
131
|
+
qm = "?" if qs else ""
|
132
|
+
return f"{path.as_uri()}{qm}{qs}"
|
133
|
+
|
134
|
+
|
135
|
+
def sqlite_journal_mode(connection, mode: str | None = None) -> str:
|
136
|
+
[[value]] = connection.exec_driver_sql(make_journal_mode_statement(mode))
|
137
|
+
return value
|
138
|
+
|
139
|
+
|
140
|
+
def create_engine_sqlite(
|
141
|
+
path: Path | str | Memory,
|
142
|
+
*,
|
143
|
+
timeout: float | int | None,
|
144
|
+
parameters: ty.Iterable[tuple[str | bytes, str | bytes]] = (),
|
145
|
+
journal_mode: str | None = None,
|
146
|
+
check_same_thread: bool | None = False,
|
147
|
+
create_engine_args: dict | None = None,
|
148
|
+
create_engine: ty.Callable | None = None,
|
149
|
+
transaction_fix: SQLAlchemySqliteTransactionFix | bool = True,
|
150
|
+
) -> sa.Engine:
|
151
|
+
"""
|
152
|
+
Create a sqlite engine.
|
153
|
+
|
154
|
+
Parameters
|
155
|
+
----------
|
156
|
+
path: Path | str | Memory
|
157
|
+
Path to the db file, or a :class:`Memory` object. The same memory object can be shared across multiple engines.
|
158
|
+
timeout: float
|
159
|
+
How long will SQLite wait if the database is locked? See
|
160
|
+
https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
|
161
|
+
parameters: ty.Sequence, optional
|
162
|
+
SQLite URI query parameters as described in https://www.sqlite.org/uri.html
|
163
|
+
journal_mode: str, optional
|
164
|
+
If provided, set the journal mode to this string before every transaction. This option simply sets the
|
165
|
+
``x_sqlite_journal_mode`` engine execution option.
|
166
|
+
check_same_thread: bool, optional
|
167
|
+
Defaults to False. See https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
|
168
|
+
create_engine_args: dict, optional
|
169
|
+
Keyword arguments to be passed to :func:`sa.create_engine`.
|
170
|
+
create_engine: callable, optional
|
171
|
+
If provided, this will be used instead of :func:`sa.create_engine`. You can use this to further customize
|
172
|
+
the engine creation.
|
173
|
+
transaction_fix: SQLAlchemySqliteTransactionFix | bool, optional
|
174
|
+
See :class:`SQLAlchemySqliteTransactionFix`. If True, then instantiate one. If False, then do not apply the fix.
|
175
|
+
(default: True)
|
176
|
+
"""
|
177
|
+
|
178
|
+
parameters = list(parameters)
|
179
|
+
if isinstance(path, Memory):
|
180
|
+
parameters += (("vfs", "memdb"),)
|
181
|
+
parameters.append(("uri", "true"))
|
182
|
+
|
183
|
+
uri = sqlite_file_uri(path, parameters)
|
184
|
+
|
185
|
+
if create_engine_args is None:
|
186
|
+
create_engine_args = {} # pragma: no cover
|
187
|
+
|
188
|
+
# always default to QueuePool
|
189
|
+
if (k := "poolclass") not in create_engine_args:
|
190
|
+
create_engine_args[k] = sap.QueuePool
|
191
|
+
|
192
|
+
if (v := create_engine_args.get(k := "connect_args")) is None:
|
193
|
+
create_engine_args[k] = v = {}
|
194
|
+
if timeout is not None:
|
195
|
+
v["timeout"] = timeout
|
196
|
+
if check_same_thread is not None:
|
197
|
+
v["check_same_thread"] = check_same_thread
|
198
|
+
|
199
|
+
# do not pass through poolclass=None
|
200
|
+
if create_engine_args.get((k := "poolclass"), True) is None:
|
201
|
+
create_engine_args.pop(k, None) # pragma: no cover
|
202
|
+
|
203
|
+
if create_engine is None:
|
204
|
+
create_engine = sa.create_engine
|
205
|
+
|
206
|
+
engine = create_engine("sqlite:///" + uri, **create_engine_args)
|
207
|
+
|
208
|
+
if transaction_fix is True:
|
209
|
+
transaction_fix = SQLAlchemySqliteTransactionFix()
|
210
|
+
|
211
|
+
if transaction_fix:
|
212
|
+
transaction_fix.register(engine)
|
213
|
+
|
214
|
+
engine = engine.execution_options(x_sqlite_journal_mode=journal_mode)
|
215
|
+
|
216
|
+
if isinstance(path, Memory):
|
217
|
+
(conn := engine.raw_connection()).detach()
|
218
|
+
|
219
|
+
# force the connection to actually happen
|
220
|
+
conn.execute("SELECT 0 WHERE 0").close()
|
221
|
+
|
222
|
+
path.connection_reference = conn
|
223
|
+
|
224
|
+
return engine
|
File without changes
|
@@ -0,0 +1,57 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: sqlalchemy-boltons
|
3
|
+
Version: 1.0.0
|
4
|
+
Summary: Utilities that should've been inside SQLAlchemy but aren't
|
5
|
+
Author-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
|
6
|
+
Maintainer-email: Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
|
7
|
+
License: MIT
|
8
|
+
Project-URL: Homepage, https://hydra.ecd.space/deaduard/sqlalchemy_boltons/
|
9
|
+
Project-URL: Changelog, https://hydra.ecd.space/deaduard/sqlalchemy_boltons/file?name=CHANGELOG.md&ci=trunk
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
11
|
+
Description-Content-Type: text/markdown
|
12
|
+
License-File: LICENSE
|
13
|
+
License-File: AUTHORS.md
|
14
|
+
|
15
|
+
# sqlalchemy_boltons
|
16
|
+
|
17
|
+
SQLAlchemy is great. However, it doesn't have everything built-in. Some important things are missing, and need to be
|
18
|
+
"bolted on".
|
19
|
+
|
20
|
+
(Name inspired from [boltons](https://pypi.org/project/boltons/). Not affiliated.)
|
21
|
+
|
22
|
+
## sqlite
|
23
|
+
|
24
|
+
SQLAlchemy doesn't automatically fix pysqlite's broken transaction handling. Instead, it provides a recipe for doing so
|
25
|
+
inside the [documentation](https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#serializable-isolation-savepoints-transactional-ddl).
|
26
|
+
This module implements a fix for that broken behaviour.
|
27
|
+
|
28
|
+
You can customize, on a per-engine or per-connection basis:
|
29
|
+
|
30
|
+
- The type of transaction to be started, such as BEGIN or
|
31
|
+
[BEGIN IMMEDIATE](https://www.sqlite.org/lang_transaction.html) (or
|
32
|
+
[BEGIN CONCURRENT](https://www.sqlite.org/cgi/src/doc/begin-concurrent/doc/begin_concurrent.md) someday maybe).
|
33
|
+
- The [foreign-key enforcement setting](https://www.sqlite.org/foreignkeys.html). Can be `True`, `False`, or `"defer"`.
|
34
|
+
- The [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) such as DELETE or WAL.
|
35
|
+
|
36
|
+
```python
|
37
|
+
from sqlalchemy.orm import sessionmaker
|
38
|
+
from sqlalchemy_boltons.sqlite import create_engine_sqlite
|
39
|
+
|
40
|
+
engine = create_engine_sqlite("file.db", journal_mode="WAL", timeout=0.5, create_engine_args={"echo": True})
|
41
|
+
|
42
|
+
# use standard "BEGIN" and use deferred enforcement of foreign keys
|
43
|
+
engine = engine.execution_options(x_sqlite_begin_mode=None, x_sqlite_foreign_keys="defer")
|
44
|
+
|
45
|
+
# make a separate engine for write transactions using "BEGIN IMMEDIATE" for eager locking
|
46
|
+
engine_w = engine.execution_options(x_sqlite_begin_mode="IMMEDIATE")
|
47
|
+
|
48
|
+
Session = sessionmaker(engine)
|
49
|
+
SessionW = sessionmaker(engine_w)
|
50
|
+
|
51
|
+
with Session() as session:
|
52
|
+
session.execute(select(...))
|
53
|
+
|
54
|
+
# this locks the database eagerly
|
55
|
+
with SessionW() as session:
|
56
|
+
session.execute(update(...))
|
57
|
+
```
|
@@ -0,0 +1,9 @@
|
|
1
|
+
sqlalchemy_boltons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
sqlalchemy_boltons/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
3
|
+
sqlalchemy_boltons/sqlite.py,sha256=fIS9rsmEFRakSl8DLwHqVJ-M9K1w6Q0XEJieYNH1b98,7985
|
4
|
+
sqlalchemy_boltons-1.0.0.dist-info/AUTHORS.md,sha256=sXkm88GaYJ63k0Hy7UlGboAT4aytU8e1_K0wNo7WwD8,144
|
5
|
+
sqlalchemy_boltons-1.0.0.dist-info/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
6
|
+
sqlalchemy_boltons-1.0.0.dist-info/METADATA,sha256=fb70ARFvxpuPDdnyJBJ9Rrjiw6aidC-VEM6qXeTh2hs,2466
|
7
|
+
sqlalchemy_boltons-1.0.0.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
|
8
|
+
sqlalchemy_boltons-1.0.0.dist-info/top_level.txt,sha256=mlvZ3R5FelrQt2SNXCtw2bDXcTROxl5O_gDFnWXPQ84,19
|
9
|
+
sqlalchemy_boltons-1.0.0.dist-info/RECORD,,
|
@@ -0,0 +1 @@
|
|
1
|
+
sqlalchemy_boltons
|