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.
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
@@ -0,0 +1,9 @@
1
+ # Credits
2
+
3
+ ## Development Lead
4
+
5
+ - Eduard Christian Dumitrescu <eduard.c.dumitrescu@gmail.com>
6
+
7
+ ## Contributors
8
+
9
+ None yet. Why not be the first?
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.38.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sqlalchemy_boltons