bazaar-contrib-sqlite 0.3.0rc1__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.
- bazaar_contrib_sqlite/__init__.py +26 -0
- bazaar_contrib_sqlite/compiler.py +74 -0
- bazaar_contrib_sqlite/database.py +323 -0
- bazaar_contrib_sqlite/executor.py +673 -0
- bazaar_contrib_sqlite/py.typed +0 -0
- bazaar_contrib_sqlite/resources/add_column.sql.tpl +3 -0
- bazaar_contrib_sqlite/resources/busy_timeout.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/copy.sql.tpl +3 -0
- bazaar_contrib_sqlite/resources/create_index.sql.tpl +13 -0
- bazaar_contrib_sqlite/resources/create_table.sql.tpl +21 -0
- bazaar_contrib_sqlite/resources/create_trigger.sql.tpl +7 -0
- bazaar_contrib_sqlite/resources/delete.sql.tpl +6 -0
- bazaar_contrib_sqlite/resources/drop_column.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_index.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_table.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/drop_trigger.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/fragments.sql.tpl +111 -0
- bazaar_contrib_sqlite/resources/insert.sql.tpl +23 -0
- bazaar_contrib_sqlite/resources/rename_column.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/rename_table.sql.tpl +1 -0
- bazaar_contrib_sqlite/resources/search_delete.sql.tpl +8 -0
- bazaar_contrib_sqlite/resources/search_insert.sql.tpl +8 -0
- bazaar_contrib_sqlite/resources/search_rebuild.sql.tpl +2 -0
- bazaar_contrib_sqlite/resources/search_table.sql.tpl +7 -0
- bazaar_contrib_sqlite/resources/search_update.sql.tpl +12 -0
- bazaar_contrib_sqlite/resources/select.sql.tpl +44 -0
- bazaar_contrib_sqlite/resources/update.sql.tpl +10 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/METADATA +35 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/RECORD +32 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/WHEEL +4 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/entry_points.txt +3 -0
- bazaar_contrib_sqlite-0.3.0rc1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Lifespan-owned SQLite implementation of the public storage contract."""
|
|
2
|
+
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
from bazaar_contrib_storage import STORAGE, backend_key
|
|
6
|
+
from bazaar_core import Context, Contrib
|
|
7
|
+
|
|
8
|
+
from .database import SqliteConfig, SqliteDatabase, open_database
|
|
9
|
+
|
|
10
|
+
__all__ = ["SqliteConfig", "SqliteDatabase", "open_database", "sqlite"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@asynccontextmanager
|
|
14
|
+
async def lifespan(ctx: Context, config: SqliteConfig):
|
|
15
|
+
async with open_database(config) as database:
|
|
16
|
+
with ctx.accept(STORAGE).register("sqlite", database):
|
|
17
|
+
ctx.offer(backend_key("sqlite"), database)
|
|
18
|
+
yield
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
sqlite = Contrib(
|
|
22
|
+
config=SqliteConfig,
|
|
23
|
+
accepts=[STORAGE],
|
|
24
|
+
offers=[backend_key("sqlite")],
|
|
25
|
+
lifespan=lifespan,
|
|
26
|
+
)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Render SQLite statements from packaged templates and collect bound values."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import cache
|
|
6
|
+
|
|
7
|
+
import bazaar_contrib_storage as storage
|
|
8
|
+
from jinja2 import Environment, PackageLoader, StrictUndefined
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def quote(name: str) -> str:
|
|
12
|
+
return '"' + name.replace('"', '""') + '"'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def literal(value: storage.Scalar) -> str:
|
|
16
|
+
if value is None:
|
|
17
|
+
return "NULL"
|
|
18
|
+
if isinstance(value, bytes):
|
|
19
|
+
return "X'" + value.hex() + "'"
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
return "'" + value.replace("'", "''") + "'"
|
|
22
|
+
if isinstance(value, bool):
|
|
23
|
+
return str(int(value))
|
|
24
|
+
return str(value)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cache
|
|
28
|
+
def load_templates() -> Environment:
|
|
29
|
+
"""Load all packaged SQL before serving queries on the event loop."""
|
|
30
|
+
templates = Environment(
|
|
31
|
+
loader=PackageLoader("bazaar_contrib_sqlite", "resources"),
|
|
32
|
+
undefined=StrictUndefined,
|
|
33
|
+
autoescape=False,
|
|
34
|
+
auto_reload=False,
|
|
35
|
+
trim_blocks=False,
|
|
36
|
+
lstrip_blocks=True,
|
|
37
|
+
)
|
|
38
|
+
templates.filters.update(identifier=quote, literal=literal)
|
|
39
|
+
for name in templates.list_templates(extensions=["tpl"]):
|
|
40
|
+
templates.get_template(name)
|
|
41
|
+
return templates
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Compiler:
|
|
45
|
+
def __init__(self, templates: Environment, *, inline: bool = False) -> None:
|
|
46
|
+
self.templates = templates
|
|
47
|
+
self.parameters: list[storage.Scalar] = []
|
|
48
|
+
self.inline = inline
|
|
49
|
+
|
|
50
|
+
def bind(self, value: storage.Scalar) -> str:
|
|
51
|
+
if self.inline:
|
|
52
|
+
return literal(value)
|
|
53
|
+
self.parameters.append(value)
|
|
54
|
+
return "?"
|
|
55
|
+
|
|
56
|
+
def render(self, template: str, **context: object) -> str:
|
|
57
|
+
return self.templates.get_template(template).render(bind=self.bind, **context)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def schema_sql(
|
|
61
|
+
operation: storage.SchemaOperation, templates: Environment
|
|
62
|
+
) -> tuple[str, ...]:
|
|
63
|
+
compiler = Compiler(templates, inline=True)
|
|
64
|
+
if isinstance(operation, storage.CreateIndex) and operation.index.kind == "search":
|
|
65
|
+
names = (
|
|
66
|
+
"search_table.sql.tpl",
|
|
67
|
+
"search_insert.sql.tpl",
|
|
68
|
+
"search_delete.sql.tpl",
|
|
69
|
+
"search_update.sql.tpl",
|
|
70
|
+
"search_rebuild.sql.tpl",
|
|
71
|
+
)
|
|
72
|
+
else:
|
|
73
|
+
names = (f"{operation.kind}.sql.tpl",)
|
|
74
|
+
return tuple(compiler.render(name, operation=operation) for name in names)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""SQLite sessions, connection ownership, and public storage errors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
|
|
6
|
+
from enum import Enum, auto
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
lazy import asyncio
|
|
9
|
+
lazy import os
|
|
10
|
+
lazy import sqlite3
|
|
11
|
+
lazy from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
|
|
12
|
+
lazy from typing import Self
|
|
13
|
+
|
|
14
|
+
import bazaar_contrib_storage as storage
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
16
|
+
lazy import aiosqlite
|
|
17
|
+
lazy from jinja2 import Environment
|
|
18
|
+
|
|
19
|
+
from .compiler import Compiler, load_templates, schema_sql
|
|
20
|
+
from .executor import (
|
|
21
|
+
SqliteExecutor,
|
|
22
|
+
SqliteExecutorClosedError,
|
|
23
|
+
SqliteQueryTimeoutError,
|
|
24
|
+
SqliteSession,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SqliteConfig(BaseModel):
|
|
29
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
30
|
+
path: Path | None = None
|
|
31
|
+
busy_timeout_ms: int = Field(default=5_000, gt=0)
|
|
32
|
+
max_idle_readers: int = Field(default=2, gt=0)
|
|
33
|
+
max_readers: int | None = Field(default=None, gt=0)
|
|
34
|
+
reader_idle_timeout: float = Field(default=60, gt=0, allow_inf_nan=False)
|
|
35
|
+
shutdown_timeout: float = Field(default=20, gt=0, allow_inf_nan=False)
|
|
36
|
+
|
|
37
|
+
@model_validator(mode="after")
|
|
38
|
+
def pool_size(self) -> Self:
|
|
39
|
+
if self.max_readers is not None and self.max_readers < self.max_idle_readers:
|
|
40
|
+
raise ValueError("max_readers must be at least max_idle_readers")
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@contextmanager
|
|
45
|
+
def storage_errors() -> Iterator[None]:
|
|
46
|
+
try:
|
|
47
|
+
yield
|
|
48
|
+
except SqliteExecutorClosedError as error:
|
|
49
|
+
raise storage.StorageClosedError(str(error)) from error
|
|
50
|
+
except SqliteQueryTimeoutError as error:
|
|
51
|
+
raise storage.StorageQueryTimeoutError(str(error)) from error
|
|
52
|
+
except sqlite3.IntegrityError as error:
|
|
53
|
+
raise storage.StorageIntegrityError(str(error)) from error
|
|
54
|
+
except sqlite3.OperationalError as error:
|
|
55
|
+
code = getattr(error, "sqlite_errorcode", 0) & 0xFF
|
|
56
|
+
if code in {
|
|
57
|
+
sqlite3.SQLITE_BUSY,
|
|
58
|
+
sqlite3.SQLITE_LOCKED,
|
|
59
|
+
sqlite3.SQLITE_IOERR,
|
|
60
|
+
sqlite3.SQLITE_CANTOPEN,
|
|
61
|
+
sqlite3.SQLITE_FULL,
|
|
62
|
+
}:
|
|
63
|
+
raise storage.StorageUnavailableError(str(error)) from error
|
|
64
|
+
if "no such module: fts5" in str(error) or "no such tokenizer: trigram" in str(
|
|
65
|
+
error
|
|
66
|
+
):
|
|
67
|
+
raise storage.StorageQueryError(
|
|
68
|
+
"text search requires SQLite FTS5 with the trigram tokenizer"
|
|
69
|
+
) from error
|
|
70
|
+
raise storage.StorageQueryError(str(error)) from error
|
|
71
|
+
except sqlite3.Error as error:
|
|
72
|
+
raise storage.StorageQueryError(str(error)) from error
|
|
73
|
+
except OSError as error:
|
|
74
|
+
raise storage.StorageUnavailableError(str(error)) from error
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _State(Enum):
|
|
78
|
+
OPEN = auto()
|
|
79
|
+
CLOSED = auto()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Session:
|
|
83
|
+
def __init__(
|
|
84
|
+
self, executor: SqliteExecutor | SqliteSession, templates: Environment
|
|
85
|
+
) -> None:
|
|
86
|
+
self._executor = executor
|
|
87
|
+
self._templates = templates
|
|
88
|
+
self._state = _State.OPEN
|
|
89
|
+
|
|
90
|
+
def _require_open(self) -> None:
|
|
91
|
+
if self._state is _State.CLOSED:
|
|
92
|
+
raise storage.StorageClosedError("transaction session has closed")
|
|
93
|
+
|
|
94
|
+
async def select(self, request: storage.Select) -> tuple[storage.Record, ...]:
|
|
95
|
+
self._require_open()
|
|
96
|
+
compiler = Compiler(self._templates)
|
|
97
|
+
statement = compiler.render("select.sql.tpl", request=request)
|
|
98
|
+
with storage_errors():
|
|
99
|
+
rows = await self._executor.fetchall(statement, compiler.parameters)
|
|
100
|
+
return tuple(dict(row) for row in rows)
|
|
101
|
+
|
|
102
|
+
async def _mutation(self, statement: str, compiler: Compiler) -> storage.Mutation:
|
|
103
|
+
self._require_open()
|
|
104
|
+
with storage_errors():
|
|
105
|
+
result = await self._executor.execute(statement, compiler.parameters)
|
|
106
|
+
return storage.Mutation(
|
|
107
|
+
count=max(result.rowcount, len(result.rows)),
|
|
108
|
+
rows=tuple(dict(row) for row in result.rows),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def copy(self, request: storage.Copy) -> storage.Mutation:
|
|
112
|
+
compiler = Compiler(self._templates)
|
|
113
|
+
statement = compiler.render("copy.sql.tpl", request=request)
|
|
114
|
+
return await self._mutation(statement, compiler)
|
|
115
|
+
|
|
116
|
+
async def table_exists(self, name: str) -> bool:
|
|
117
|
+
self._require_open()
|
|
118
|
+
with storage_errors():
|
|
119
|
+
row = await self._executor.fetchone(
|
|
120
|
+
"SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? "
|
|
121
|
+
"UNION ALL SELECT 1 FROM sqlite_temp_master WHERE type = 'table' AND name = ?",
|
|
122
|
+
(name, name),
|
|
123
|
+
)
|
|
124
|
+
return row is not None
|
|
125
|
+
|
|
126
|
+
async def insert(self, request: storage.Insert) -> storage.Mutation:
|
|
127
|
+
compiler = Compiler(self._templates)
|
|
128
|
+
return await self._mutation(
|
|
129
|
+
compiler.render("insert.sql.tpl", request=request), compiler
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
async def update(self, request: storage.Update) -> storage.Mutation:
|
|
133
|
+
compiler = Compiler(self._templates)
|
|
134
|
+
return await self._mutation(
|
|
135
|
+
compiler.render("update.sql.tpl", request=request), compiler
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
async def delete(self, request: storage.Delete) -> storage.Mutation:
|
|
139
|
+
compiler = Compiler(self._templates)
|
|
140
|
+
return await self._mutation(
|
|
141
|
+
compiler.render("delete.sql.tpl", request=request), compiler
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def apply_schema(
|
|
145
|
+
self, operations: tuple[storage.SchemaOperation, ...]
|
|
146
|
+
) -> None:
|
|
147
|
+
self._require_open()
|
|
148
|
+
with storage_errors():
|
|
149
|
+
for operation in operations:
|
|
150
|
+
if (
|
|
151
|
+
isinstance(operation, storage.CreateIndex)
|
|
152
|
+
and operation.if_not_exists
|
|
153
|
+
):
|
|
154
|
+
row = await self._executor.fetchone(
|
|
155
|
+
"SELECT type FROM sqlite_master WHERE name = ?",
|
|
156
|
+
(operation.index.name,),
|
|
157
|
+
)
|
|
158
|
+
expected = "table" if operation.index.kind == "search" else "index"
|
|
159
|
+
if row is not None and row[0] == expected:
|
|
160
|
+
continue
|
|
161
|
+
if isinstance(operation, storage.DropIndex):
|
|
162
|
+
row = await self._executor.fetchone(
|
|
163
|
+
"SELECT type FROM sqlite_master WHERE name = ?",
|
|
164
|
+
(operation.name,),
|
|
165
|
+
)
|
|
166
|
+
if row is not None and row[0] == "table":
|
|
167
|
+
triggers = await self._executor.fetchall(
|
|
168
|
+
"SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ? "
|
|
169
|
+
"AND (instr(sql, ?) > 0 OR instr(sql, ?) > 0 OR instr(sql, ?) > 0)",
|
|
170
|
+
(
|
|
171
|
+
operation.table,
|
|
172
|
+
'INSERT INTO "' + operation.name + '"',
|
|
173
|
+
"INSERT INTO " + operation.name + " ",
|
|
174
|
+
"INSERT INTO " + operation.name + "(",
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
for trigger in triggers:
|
|
178
|
+
for statement in schema_sql(
|
|
179
|
+
storage.DropTrigger(
|
|
180
|
+
name=trigger[0],
|
|
181
|
+
table=operation.table,
|
|
182
|
+
if_exists=True,
|
|
183
|
+
),
|
|
184
|
+
self._templates,
|
|
185
|
+
):
|
|
186
|
+
await self._executor.execute(statement)
|
|
187
|
+
for statement in schema_sql(
|
|
188
|
+
storage.DropTable(table=operation.name), self._templates
|
|
189
|
+
):
|
|
190
|
+
await self._executor.execute(statement)
|
|
191
|
+
continue
|
|
192
|
+
for statement in schema_sql(operation, self._templates):
|
|
193
|
+
await self._executor.execute(statement)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class SqliteDatabase(Session):
|
|
197
|
+
def __init__(self, executor: SqliteExecutor, templates: Environment) -> None:
|
|
198
|
+
super().__init__(executor, templates)
|
|
199
|
+
self._writer = executor
|
|
200
|
+
|
|
201
|
+
async def read[T](
|
|
202
|
+
self,
|
|
203
|
+
operation: Callable[[storage.Session], Awaitable[T]],
|
|
204
|
+
*,
|
|
205
|
+
snapshot: bool = False,
|
|
206
|
+
query_budget: float | None = None,
|
|
207
|
+
) -> T:
|
|
208
|
+
with storage_errors():
|
|
209
|
+
async with self._writer.reader() as reader, AsyncExitStack() as stack:
|
|
210
|
+
if snapshot:
|
|
211
|
+
await stack.enter_async_context(reader.transaction())
|
|
212
|
+
if query_budget is not None:
|
|
213
|
+
await stack.enter_async_context(reader.query_budget(query_budget))
|
|
214
|
+
bound = Session(reader, self._templates)
|
|
215
|
+
try:
|
|
216
|
+
return await operation(bound)
|
|
217
|
+
finally:
|
|
218
|
+
bound._state = _State.CLOSED
|
|
219
|
+
|
|
220
|
+
async def write[T](
|
|
221
|
+
self,
|
|
222
|
+
operation: Callable[[storage.Session], Awaitable[T]],
|
|
223
|
+
*,
|
|
224
|
+
transactional: bool = False,
|
|
225
|
+
) -> T:
|
|
226
|
+
async def apply(session: SqliteSession) -> T:
|
|
227
|
+
bound = Session(session, self._templates)
|
|
228
|
+
try:
|
|
229
|
+
return await operation(bound)
|
|
230
|
+
finally:
|
|
231
|
+
bound._state = _State.CLOSED
|
|
232
|
+
|
|
233
|
+
with storage_errors():
|
|
234
|
+
if transactional:
|
|
235
|
+
return await self._writer.transaction_write(apply)
|
|
236
|
+
return await self._writer.write(apply)
|
|
237
|
+
|
|
238
|
+
async def transaction[T](
|
|
239
|
+
self, operation: Callable[[storage.Session], Awaitable[T]]
|
|
240
|
+
) -> T:
|
|
241
|
+
return await self.write(operation, transactional=True)
|
|
242
|
+
|
|
243
|
+
async def apply_schema(
|
|
244
|
+
self, operations: tuple[storage.SchemaOperation, ...]
|
|
245
|
+
) -> None:
|
|
246
|
+
await self.transaction(lambda session: session.apply_schema(operations))
|
|
247
|
+
|
|
248
|
+
async def wait_failure(self) -> None:
|
|
249
|
+
await self._writer.wait_failure()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
async def _connect(
|
|
253
|
+
path: Path, config: SqliteConfig, templates: Environment, *, query_only: bool
|
|
254
|
+
) -> aiosqlite.Connection:
|
|
255
|
+
connection = await aiosqlite.connect(
|
|
256
|
+
path, timeout=config.busy_timeout_ms / 1000, isolation_level=None
|
|
257
|
+
)
|
|
258
|
+
try:
|
|
259
|
+
connection.row_factory = aiosqlite.Row
|
|
260
|
+
await connection.execute("PRAGMA journal_mode = WAL")
|
|
261
|
+
await connection.execute("PRAGMA synchronous = NORMAL")
|
|
262
|
+
await connection.execute("PRAGMA foreign_keys = ON")
|
|
263
|
+
await connection.execute(
|
|
264
|
+
templates.get_template("busy_timeout.sql.tpl").render(
|
|
265
|
+
timeout=config.busy_timeout_ms
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
if query_only:
|
|
269
|
+
await connection.execute("PRAGMA query_only = ON")
|
|
270
|
+
return connection
|
|
271
|
+
except BaseException:
|
|
272
|
+
await connection.close()
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@asynccontextmanager
|
|
277
|
+
async def open_database(config: SqliteConfig) -> AsyncIterator[SqliteDatabase]:
|
|
278
|
+
templates = await asyncio.to_thread(load_templates)
|
|
279
|
+
path = (
|
|
280
|
+
config.path
|
|
281
|
+
or Path(os.environ.get("BCN_HOME") or Path.home()) / ".bcn" / "bcn.sqlite3"
|
|
282
|
+
)
|
|
283
|
+
with storage_errors():
|
|
284
|
+
path = await asyncio.to_thread(lambda: path.expanduser().resolve())
|
|
285
|
+
await asyncio.to_thread(
|
|
286
|
+
path.parent.mkdir, mode=0o700, parents=True, exist_ok=True
|
|
287
|
+
)
|
|
288
|
+
writer = await _connect(path, config, templates, query_only=False)
|
|
289
|
+
readers: list[aiosqlite.Connection] = []
|
|
290
|
+
try:
|
|
291
|
+
with storage_errors():
|
|
292
|
+
if os.name != "nt":
|
|
293
|
+
await asyncio.to_thread(path.chmod, 0o600)
|
|
294
|
+
for _ in range(config.max_idle_readers):
|
|
295
|
+
readers.append(await _connect(path, config, templates, query_only=True))
|
|
296
|
+
async with asyncio.TaskGroup() as tasks:
|
|
297
|
+
executor = SqliteExecutor(
|
|
298
|
+
writer,
|
|
299
|
+
readers,
|
|
300
|
+
reader_factory=lambda: _connect(
|
|
301
|
+
path, config, templates, query_only=True
|
|
302
|
+
),
|
|
303
|
+
max_idle_readers=config.max_idle_readers,
|
|
304
|
+
max_readers=config.max_readers,
|
|
305
|
+
reader_idle_timeout=config.reader_idle_timeout,
|
|
306
|
+
tasks=tasks,
|
|
307
|
+
)
|
|
308
|
+
await executor.start()
|
|
309
|
+
try:
|
|
310
|
+
yield SqliteDatabase(executor, templates)
|
|
311
|
+
finally:
|
|
312
|
+
try:
|
|
313
|
+
async with asyncio.timeout(config.shutdown_timeout):
|
|
314
|
+
await executor.stop()
|
|
315
|
+
except BaseException:
|
|
316
|
+
await asyncio.shield(executor.abort())
|
|
317
|
+
raise
|
|
318
|
+
finally:
|
|
319
|
+
tasks.cancel()
|
|
320
|
+
finally:
|
|
321
|
+
for reader in readers:
|
|
322
|
+
await reader.close()
|
|
323
|
+
await writer.close()
|