pyoq-sql 1.0.2__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.
- pyoq/__init__.py +10 -0
- pyoq/__main__.py +5 -0
- pyoq/_native.pyi +5 -0
- pyoq/cli/__init__.py +5 -0
- pyoq/cli/commands.py +270 -0
- pyoq/cli/defaults.py +98 -0
- pyoq/cli/services.py +97 -0
- pyoq/config/__init__.py +31 -0
- pyoq/config/connection.py +161 -0
- pyoq/config/loader.py +289 -0
- pyoq/config/models.py +245 -0
- pyoq/config/values.py +142 -0
- pyoq/descriptors.py +165 -0
- pyoq/diagnostics/__init__.py +68 -0
- pyoq/diagnostics/budget.py +136 -0
- pyoq/diagnostics/events.py +137 -0
- pyoq/diagnostics/fingerprint.py +267 -0
- pyoq/diagnostics/instrumented.py +237 -0
- pyoq/diagnostics/metrics.py +61 -0
- pyoq/diagnostics/observation.py +227 -0
- pyoq/diagnostics/scoped.py +103 -0
- pyoq/django/__init__.py +15 -0
- pyoq/django/apps.py +17 -0
- pyoq/django/execution.py +317 -0
- pyoq/django/generation.py +59 -0
- pyoq/django/management/__init__.py +0 -0
- pyoq/django/management/commands/__init__.py +0 -0
- pyoq/django/management/commands/makemigrations.py +53 -0
- pyoq/django/management/commands/pyoq_codegen.py +75 -0
- pyoq/django/parameters.py +101 -0
- pyoq/django/schema.py +379 -0
- pyoq/django/settings.py +87 -0
- pyoq/django/timeouts.py +105 -0
- pyoq/dsl/__init__.py +64 -0
- pyoq/dsl/aio/__init__.py +31 -0
- pyoq/dsl/aio/context.py +295 -0
- pyoq/dsl/aio/queries.py +335 -0
- pyoq/dsl/aio/writes.py +368 -0
- pyoq/dsl/context.py +326 -0
- pyoq/dsl/entry.py +37 -0
- pyoq/dsl/labels.py +36 -0
- pyoq/dsl/queries.py +339 -0
- pyoq/dsl/result.py +164 -0
- pyoq/dsl/writes.py +360 -0
- pyoq/errors.py +317 -0
- pyoq/fastapi/__init__.py +32 -0
- pyoq/fastapi/dependencies.py +167 -0
- pyoq/fastapi/lifespan.py +119 -0
- pyoq/fetching/__init__.py +55 -0
- pyoq/fetching/collections.py +136 -0
- pyoq/fetching/execution.py +587 -0
- pyoq/fetching/joined.py +79 -0
- pyoq/fetching/nesting.py +183 -0
- pyoq/fetching/plans.py +541 -0
- pyoq/fetching/select_in.py +149 -0
- pyoq/fetching/tables.py +110 -0
- pyoq/generation/__init__.py +54 -0
- pyoq/generation/cleanup.py +44 -0
- pyoq/generation/contracts.py +248 -0
- pyoq/generation/drift.py +169 -0
- pyoq/generation/lock.py +33 -0
- pyoq/generation/manifest.py +114 -0
- pyoq/generation/model.py +1001 -0
- pyoq/generation/pipeline.py +119 -0
- pyoq/generation/rendering/__init__.py +5 -0
- pyoq/generation/rendering/domains.py +51 -0
- pyoq/generation/rendering/enums.py +29 -0
- pyoq/generation/rendering/exports.py +70 -0
- pyoq/generation/rendering/imports.py +63 -0
- pyoq/generation/rendering/package.py +56 -0
- pyoq/generation/rendering/relations.py +133 -0
- pyoq/generation/rendering/routines.py +396 -0
- pyoq/generation/rendering/rows.py +79 -0
- pyoq/generation/rendering/source.py +121 -0
- pyoq/generation/rendering/tables.py +300 -0
- pyoq/generation/rendering/writes.py +514 -0
- pyoq/generation/validation.py +27 -0
- pyoq/generation/writer.py +184 -0
- pyoq/hydration/__init__.py +24 -0
- pyoq/hydration/engine.py +155 -0
- pyoq/hydration/identity.py +194 -0
- pyoq/hydration/plan.py +116 -0
- pyoq/migrations/__init__.py +9 -0
- pyoq/migrations/alembic.py +106 -0
- pyoq/migrations/hooks.py +75 -0
- pyoq/naming.py +261 -0
- pyoq/policies/__init__.py +47 -0
- pyoq/policies/bypass.py +122 -0
- pyoq/policies/governed.py +430 -0
- pyoq/policies/model.py +242 -0
- pyoq/policies/rewriting.py +263 -0
- pyoq/py.typed +1 -0
- pyoq/query/__init__.py +312 -0
- pyoq/query/aggregates.py +172 -0
- pyoq/query/arrays.py +65 -0
- pyoq/query/binding.py +52 -0
- pyoq/query/capabilities.py +317 -0
- pyoq/query/casts.py +73 -0
- pyoq/query/choices.py +185 -0
- pyoq/query/decoding.py +360 -0
- pyoq/query/documents.py +56 -0
- pyoq/query/execution/__init__.py +63 -0
- pyoq/query/execution/aio/__init__.py +31 -0
- pyoq/query/execution/aio/operations.py +228 -0
- pyoq/query/execution/aio/pooling.py +233 -0
- pyoq/query/execution/aio/streaming.py +161 -0
- pyoq/query/execution/aio/transactions.py +105 -0
- pyoq/query/execution/batch.py +96 -0
- pyoq/query/execution/binding_style.py +30 -0
- pyoq/query/execution/compilation.py +48 -0
- pyoq/query/execution/context.py +61 -0
- pyoq/query/execution/control.py +50 -0
- pyoq/query/execution/operations.py +224 -0
- pyoq/query/execution/planning.py +107 -0
- pyoq/query/execution/pooling.py +279 -0
- pyoq/query/execution/results.py +36 -0
- pyoq/query/execution/streaming.py +178 -0
- pyoq/query/execution/transactions.py +95 -0
- pyoq/query/expressions.py +1200 -0
- pyoq/query/fields.py +60 -0
- pyoq/query/mysql/__init__.py +59 -0
- pyoq/query/mysql/aio/__init__.py +38 -0
- pyoq/query/mysql/aio/commands.py +389 -0
- pyoq/query/mysql/aio/driver.py +196 -0
- pyoq/query/mysql/aio/executor.py +123 -0
- pyoq/query/mysql/aio/factory.py +26 -0
- pyoq/query/mysql/aio/operations.py +38 -0
- pyoq/query/mysql/aio/pool.py +53 -0
- pyoq/query/mysql/aio/transactions.py +313 -0
- pyoq/query/mysql/commands.py +354 -0
- pyoq/query/mysql/compiler.py +134 -0
- pyoq/query/mysql/context.py +20 -0
- pyoq/query/mysql/executor.py +126 -0
- pyoq/query/mysql/expressions.py +244 -0
- pyoq/query/mysql/factory.py +46 -0
- pyoq/query/mysql/health.py +66 -0
- pyoq/query/mysql/identifiers.py +9 -0
- pyoq/query/mysql/model.py +79 -0
- pyoq/query/mysql/operations.py +43 -0
- pyoq/query/mysql/parameters.py +69 -0
- pyoq/query/mysql/planning.py +20 -0
- pyoq/query/mysql/pool.py +67 -0
- pyoq/query/mysql/transactions.py +331 -0
- pyoq/query/mysql/writes.py +73 -0
- pyoq/query/nodes.py +750 -0
- pyoq/query/postgres/__init__.py +48 -0
- pyoq/query/postgres/aio/__init__.py +25 -0
- pyoq/query/postgres/aio/bulk.py +56 -0
- pyoq/query/postgres/aio/commands.py +264 -0
- pyoq/query/postgres/aio/executor.py +152 -0
- pyoq/query/postgres/aio/factory.py +26 -0
- pyoq/query/postgres/aio/operations.py +26 -0
- pyoq/query/postgres/aio/pool.py +40 -0
- pyoq/query/postgres/aio/transactions.py +295 -0
- pyoq/query/postgres/bulk.py +62 -0
- pyoq/query/postgres/commands.py +238 -0
- pyoq/query/postgres/compiler.py +114 -0
- pyoq/query/postgres/context.py +20 -0
- pyoq/query/postgres/executor.py +147 -0
- pyoq/query/postgres/expressions.py +311 -0
- pyoq/query/postgres/factory.py +24 -0
- pyoq/query/postgres/health.py +24 -0
- pyoq/query/postgres/identifiers.py +9 -0
- pyoq/query/postgres/model.py +81 -0
- pyoq/query/postgres/operations.py +25 -0
- pyoq/query/postgres/parameters.py +71 -0
- pyoq/query/postgres/planning.py +20 -0
- pyoq/query/postgres/pool.py +52 -0
- pyoq/query/postgres/transactions.py +295 -0
- pyoq/query/postgres/writes.py +37 -0
- pyoq/query/projections.py +105 -0
- pyoq/query/raw.py +90 -0
- pyoq/query/recursion.py +265 -0
- pyoq/query/rendering/__init__.py +1 -0
- pyoq/query/rendering/expressions.py +913 -0
- pyoq/query/rendering/identifiers.py +40 -0
- pyoq/query/rendering/projections.py +63 -0
- pyoq/query/rendering/queries.py +334 -0
- pyoq/query/rendering/sources.py +66 -0
- pyoq/query/rendering/writes.py +176 -0
- pyoq/query/results.py +459 -0
- pyoq/query/routines.py +196 -0
- pyoq/query/rows.py +156 -0
- pyoq/query/select.py +793 -0
- pyoq/query/select_nodes.py +277 -0
- pyoq/query/sources.py +236 -0
- pyoq/query/sqlite/__init__.py +43 -0
- pyoq/query/sqlite/commands.py +201 -0
- pyoq/query/sqlite/compiler.py +139 -0
- pyoq/query/sqlite/context.py +20 -0
- pyoq/query/sqlite/executor.py +119 -0
- pyoq/query/sqlite/expressions.py +224 -0
- pyoq/query/sqlite/factory.py +32 -0
- pyoq/query/sqlite/health.py +28 -0
- pyoq/query/sqlite/identifiers.py +9 -0
- pyoq/query/sqlite/model.py +73 -0
- pyoq/query/sqlite/operations.py +36 -0
- pyoq/query/sqlite/parameters.py +50 -0
- pyoq/query/sqlite/planning.py +20 -0
- pyoq/query/sqlite/pool.py +50 -0
- pyoq/query/sqlite/streaming.py +13 -0
- pyoq/query/sqlite/transactions.py +274 -0
- pyoq/query/sqlite/writes.py +35 -0
- pyoq/query/statements.py +27 -0
- pyoq/query/values.py +23 -0
- pyoq/query/vendor.py +162 -0
- pyoq/query/windows.py +424 -0
- pyoq/query/write_nodes.py +174 -0
- pyoq/query/writes.py +628 -0
- pyoq/relations/__init__.py +66 -0
- pyoq/relations/batching.py +219 -0
- pyoq/relations/derivation.py +111 -0
- pyoq/relations/fetching.py +355 -0
- pyoq/relations/graph.py +245 -0
- pyoq/relations/loading.py +74 -0
- pyoq/relations/model.py +75 -0
- pyoq/relations/planning.py +206 -0
- pyoq/runtime/__init__.py +9 -0
- pyoq/runtime/kernels.py +25 -0
- pyoq/runtime/python.py +43 -0
- pyoq/runtime/selection.py +73 -0
- pyoq/sanic/__init__.py +32 -0
- pyoq/sanic/scope.py +197 -0
- pyoq/sanic/workers.py +129 -0
- pyoq/schema/__init__.py +108 -0
- pyoq/schema/codec.py +711 -0
- pyoq/schema/models.py +604 -0
- pyoq/schema/mysql/__init__.py +16 -0
- pyoq/schema/mysql/connection.py +73 -0
- pyoq/schema/mysql/dsn.py +72 -0
- pyoq/schema/mysql/records.py +354 -0
- pyoq/schema/mysql/reflection.py +309 -0
- pyoq/schema/mysql/source.py +30 -0
- pyoq/schema/mysql/sql.py +128 -0
- pyoq/schema/mysql/types.py +105 -0
- pyoq/schema/postgres/__init__.py +13 -0
- pyoq/schema/postgres/connection.py +63 -0
- pyoq/schema/postgres/records.py +384 -0
- pyoq/schema/postgres/reflection.py +466 -0
- pyoq/schema/postgres/source.py +30 -0
- pyoq/schema/postgres/sql.py +246 -0
- pyoq/schema/postgres/types.py +98 -0
- pyoq/schema/registry.py +45 -0
- pyoq/schema/source.py +15 -0
- pyoq/schema/sqlite/__init__.py +6 -0
- pyoq/schema/sqlite/connection.py +54 -0
- pyoq/schema/sqlite/records.py +167 -0
- pyoq/schema/sqlite/reflection.py +393 -0
- pyoq/schema/sqlite/source.py +30 -0
- pyoq/schema/sqlite/sql.py +254 -0
- pyoq/schema/sqlite/types.py +74 -0
- pyoq/serving/__init__.py +23 -0
- pyoq/serving/databases.py +107 -0
- pyoq/serving/opening.py +331 -0
- pyoq/snapshots/__init__.py +20 -0
- pyoq/snapshots/drift.py +312 -0
- pyoq/snapshots/files.py +96 -0
- pyoq/snapshots/routing.py +40 -0
- pyoq/snapshots/source.py +33 -0
- pyoq/tracing/__init__.py +5 -0
- pyoq/tracing/spans.py +89 -0
- pyoq/unset.py +14 -0
- pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
- pyoq_sql-1.0.2.dist-info/RECORD +267 -0
- pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
- pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
- pyoq_sql-1.0.2.dist-info/licenses/LICENSE +373 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Typed dependencies a route asks for.
|
|
2
|
+
|
|
3
|
+
Each one is a factory, so a route names the database it wants and gets back
|
|
4
|
+
something with a type rather than whatever was on the application object.
|
|
5
|
+
|
|
6
|
+
Asking twice for the same thing gives back the same dependency. FastAPI matches
|
|
7
|
+
an override by identity, so a factory that built a new function every call could
|
|
8
|
+
never be overridden, and a test would quietly keep talking to the real database.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import AsyncGenerator, Callable, Generator
|
|
14
|
+
from functools import cache
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from fastapi import Request
|
|
18
|
+
from pyoq.diagnostics.scoped import AsyncScopedOperations, ScopedOperations
|
|
19
|
+
from pyoq.errors import OperationUnavailableError
|
|
20
|
+
from pyoq.fastapi.lifespan import databases_of
|
|
21
|
+
from pyoq.query.execution import QueryOperations
|
|
22
|
+
from pyoq.query.execution.aio import AsyncQueryOperations
|
|
23
|
+
from pyoq.serving import (
|
|
24
|
+
DEFAULT,
|
|
25
|
+
SupportsAsyncTransaction,
|
|
26
|
+
SupportsTransaction,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# FastAPI resolves a dependency's annotations when the route is built, so the
|
|
30
|
+
# names in them have to exist at run time rather than only for a checker.
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from pyoq.diagnostics.budget import QueryBudget, QueryScope
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@cache
|
|
36
|
+
def database(
|
|
37
|
+
name: str = DEFAULT,
|
|
38
|
+
/,
|
|
39
|
+
) -> Callable[[Request], QueryOperations]:
|
|
40
|
+
"""The database itself, for a route that runs no transaction of its own."""
|
|
41
|
+
|
|
42
|
+
def dependency(request: Request) -> QueryOperations:
|
|
43
|
+
return databases_of(request.app).synchronous(name)
|
|
44
|
+
|
|
45
|
+
return dependency
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cache
|
|
49
|
+
def async_database(
|
|
50
|
+
name: str = DEFAULT,
|
|
51
|
+
/,
|
|
52
|
+
) -> Callable[[Request], AsyncQueryOperations]:
|
|
53
|
+
def dependency(request: Request) -> AsyncQueryOperations:
|
|
54
|
+
return databases_of(request.app).asynchronous(name)
|
|
55
|
+
|
|
56
|
+
return dependency
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@cache
|
|
60
|
+
def transaction(
|
|
61
|
+
name: str = DEFAULT,
|
|
62
|
+
/,
|
|
63
|
+
) -> Callable[[Request], Generator[QueryOperations]]:
|
|
64
|
+
"""One transaction for the whole request, ended by how the request ended.
|
|
65
|
+
|
|
66
|
+
A route that returns commits. A route that raises rolls back, and so does a
|
|
67
|
+
request the client gave up on, because a dependency is torn down either way
|
|
68
|
+
and the transaction sees what tore it down.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def dependency(request: Request) -> Generator[QueryOperations]:
|
|
72
|
+
operations = databases_of(request.app).synchronous(name)
|
|
73
|
+
with _transactional(operations, name).transaction() as active:
|
|
74
|
+
yield active
|
|
75
|
+
|
|
76
|
+
return dependency
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@cache
|
|
80
|
+
def async_transaction(
|
|
81
|
+
name: str = DEFAULT,
|
|
82
|
+
/,
|
|
83
|
+
) -> Callable[[Request], AsyncGenerator[AsyncQueryOperations]]:
|
|
84
|
+
async def dependency(
|
|
85
|
+
request: Request,
|
|
86
|
+
) -> AsyncGenerator[AsyncQueryOperations]:
|
|
87
|
+
operations = databases_of(request.app).asynchronous(name)
|
|
88
|
+
async with _async_transactional(operations, name).transaction() as active:
|
|
89
|
+
yield active
|
|
90
|
+
|
|
91
|
+
return dependency
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cache
|
|
95
|
+
def scoped(
|
|
96
|
+
budget: QueryBudget,
|
|
97
|
+
/,
|
|
98
|
+
name: str = DEFAULT,
|
|
99
|
+
) -> Callable[[Request], QueryOperations]:
|
|
100
|
+
"""A database that counts what this request runs, and refuses beyond it.
|
|
101
|
+
|
|
102
|
+
The scope belongs to the request, so one request cannot spend another's
|
|
103
|
+
budget. A statement the budget refuses never reaches the database.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def dependency(request: Request) -> QueryOperations:
|
|
107
|
+
return ScopedOperations(
|
|
108
|
+
databases_of(request.app).synchronous(name), _scope(budget)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return dependency
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@cache
|
|
115
|
+
def async_scoped(
|
|
116
|
+
budget: QueryBudget,
|
|
117
|
+
/,
|
|
118
|
+
name: str = DEFAULT,
|
|
119
|
+
) -> Callable[[Request], AsyncQueryOperations]:
|
|
120
|
+
def dependency(request: Request) -> AsyncQueryOperations:
|
|
121
|
+
return AsyncScopedOperations(
|
|
122
|
+
databases_of(request.app).asynchronous(name), _scope(budget)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return dependency
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _scope(budget: QueryBudget) -> QueryScope:
|
|
129
|
+
from pyoq.diagnostics.budget import QueryScope
|
|
130
|
+
|
|
131
|
+
return QueryScope(budget)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _transactional(
|
|
135
|
+
operations: QueryOperations,
|
|
136
|
+
name: str,
|
|
137
|
+
) -> SupportsTransaction:
|
|
138
|
+
if not isinstance(operations, SupportsTransaction):
|
|
139
|
+
raise _no_transactions(name)
|
|
140
|
+
return operations
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _async_transactional(
|
|
144
|
+
operations: AsyncQueryOperations,
|
|
145
|
+
name: str,
|
|
146
|
+
) -> SupportsAsyncTransaction:
|
|
147
|
+
if not isinstance(operations, SupportsAsyncTransaction):
|
|
148
|
+
raise _no_transactions(name)
|
|
149
|
+
return operations
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _no_transactions(name: str) -> OperationUnavailableError:
|
|
153
|
+
message = (
|
|
154
|
+
f"the database named {name!r} opens no transactions of its own, so a "
|
|
155
|
+
f"request transaction cannot be started against it"
|
|
156
|
+
)
|
|
157
|
+
return OperationUnavailableError(message)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = (
|
|
161
|
+
"async_database",
|
|
162
|
+
"async_scoped",
|
|
163
|
+
"async_transaction",
|
|
164
|
+
"database",
|
|
165
|
+
"scoped",
|
|
166
|
+
"transaction",
|
|
167
|
+
)
|
pyoq/fastapi/lifespan.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Opening the databases once, and giving them back once.
|
|
2
|
+
|
|
3
|
+
A pool opened per request is a pool that is never warm and never bounded. The
|
|
4
|
+
application owns them for as long as it runs, which is what a lifespan is for.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from pyoq.errors import PyOQError
|
|
13
|
+
from pyoq.serving import AsyncDatabase, Databases, SyncDatabase
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
|
|
20
|
+
STATE_ATTRIBUTE = "pyoq_databases"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def database_lifespan(
|
|
24
|
+
*,
|
|
25
|
+
synchronous: Mapping[str, Callable[[], SyncDatabase]] | None = None,
|
|
26
|
+
asynchronous: Mapping[str, Callable[[], Awaitable[AsyncDatabase]]] | None = None,
|
|
27
|
+
) -> Callable[[FastAPI], AbstractAsyncContextManager[None]]:
|
|
28
|
+
"""A lifespan that opens every named database and closes every one it opened.
|
|
29
|
+
|
|
30
|
+
A database that fails to open takes the application down, and the ones
|
|
31
|
+
already open are given back first. Starting up halfway and serving requests
|
|
32
|
+
against a partly opened application is worse than not starting.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@asynccontextmanager
|
|
36
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
|
37
|
+
opened_sync: dict[str, SyncDatabase] = {}
|
|
38
|
+
opened_async: dict[str, AsyncDatabase] = {}
|
|
39
|
+
try:
|
|
40
|
+
_open_synchronous(synchronous or {}, opened_sync)
|
|
41
|
+
await _open_asynchronous(asynchronous or {}, opened_async)
|
|
42
|
+
except BaseException:
|
|
43
|
+
await _close_all(opened_sync, opened_async)
|
|
44
|
+
raise
|
|
45
|
+
setattr(app.state, STATE_ATTRIBUTE, Databases(opened_sync, opened_async))
|
|
46
|
+
try:
|
|
47
|
+
yield
|
|
48
|
+
finally:
|
|
49
|
+
await _close_all(opened_sync, opened_async)
|
|
50
|
+
|
|
51
|
+
return lifespan
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _open_synchronous(
|
|
55
|
+
factories: Mapping[str, Callable[[], SyncDatabase]],
|
|
56
|
+
opened: dict[str, SyncDatabase],
|
|
57
|
+
) -> None:
|
|
58
|
+
for name, factory in factories.items():
|
|
59
|
+
opened[name] = factory()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _open_asynchronous(
|
|
63
|
+
factories: Mapping[str, Callable[[], Awaitable[AsyncDatabase]]],
|
|
64
|
+
opened: dict[str, AsyncDatabase],
|
|
65
|
+
) -> None:
|
|
66
|
+
for name, factory in factories.items():
|
|
67
|
+
opened[name] = await factory()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _close_all(
|
|
71
|
+
synchronous: dict[str, SyncDatabase],
|
|
72
|
+
asynchronous: dict[str, AsyncDatabase],
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Give every database back, even when one of them refuses.
|
|
75
|
+
|
|
76
|
+
Stopping at the first failure would leave the rest open, and the process is
|
|
77
|
+
on its way out either way. The failures are collected so none is silent.
|
|
78
|
+
"""
|
|
79
|
+
failures: list[BaseException] = []
|
|
80
|
+
for waiting in reversed(list(asynchronous.values())):
|
|
81
|
+
await _closed_async(waiting, failures)
|
|
82
|
+
for immediate in reversed(list(synchronous.values())):
|
|
83
|
+
_closed(immediate, failures)
|
|
84
|
+
synchronous.clear()
|
|
85
|
+
asynchronous.clear()
|
|
86
|
+
if failures:
|
|
87
|
+
raise BaseExceptionGroup("closing databases failed", failures)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _closed(database: SyncDatabase, failures: list[BaseException]) -> None:
|
|
91
|
+
try:
|
|
92
|
+
database.close()
|
|
93
|
+
except PyOQError as error:
|
|
94
|
+
failures.append(error)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _closed_async(
|
|
98
|
+
database: AsyncDatabase,
|
|
99
|
+
failures: list[BaseException],
|
|
100
|
+
) -> None:
|
|
101
|
+
try:
|
|
102
|
+
await database.close()
|
|
103
|
+
except PyOQError as error:
|
|
104
|
+
failures.append(error)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def databases_of(app: FastAPI, /) -> Databases:
|
|
108
|
+
"""What the lifespan opened, or a clear word about it not having run."""
|
|
109
|
+
found = getattr(app.state, STATE_ATTRIBUTE, None)
|
|
110
|
+
if not isinstance(found, Databases):
|
|
111
|
+
message = (
|
|
112
|
+
"no PyOQ databases are open; give the application the lifespan "
|
|
113
|
+
"returned by database_lifespan()"
|
|
114
|
+
)
|
|
115
|
+
raise LookupError(message)
|
|
116
|
+
return found
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
__all__ = ("STATE_ATTRIBUTE", "database_lifespan", "databases_of")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Carrying out a fetch plan against a database."""
|
|
2
|
+
|
|
3
|
+
from pyoq.fetching.collections import decode_collection, hydrate_collection
|
|
4
|
+
from pyoq.fetching.execution import (
|
|
5
|
+
AsyncFetchExecutor,
|
|
6
|
+
FetchAssembly,
|
|
7
|
+
FetchedRow,
|
|
8
|
+
FetchExecutor,
|
|
9
|
+
)
|
|
10
|
+
from pyoq.fetching.joined import join_condition, projection_layout
|
|
11
|
+
from pyoq.fetching.nesting import collection_for
|
|
12
|
+
from pyoq.fetching.plans import (
|
|
13
|
+
DeferredRelation,
|
|
14
|
+
FetchColumn,
|
|
15
|
+
FetchStatement,
|
|
16
|
+
JoinedRelation,
|
|
17
|
+
NestedJoin,
|
|
18
|
+
NestedRelation,
|
|
19
|
+
RelationBlock,
|
|
20
|
+
batch_statement,
|
|
21
|
+
columns_of,
|
|
22
|
+
root_statement,
|
|
23
|
+
)
|
|
24
|
+
from pyoq.fetching.select_in import (
|
|
25
|
+
KeyColumn,
|
|
26
|
+
batch_conditions,
|
|
27
|
+
key_condition,
|
|
28
|
+
select_in_conditions,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = (
|
|
32
|
+
"AsyncFetchExecutor",
|
|
33
|
+
"DeferredRelation",
|
|
34
|
+
"FetchAssembly",
|
|
35
|
+
"FetchColumn",
|
|
36
|
+
"FetchExecutor",
|
|
37
|
+
"FetchStatement",
|
|
38
|
+
"FetchedRow",
|
|
39
|
+
"JoinedRelation",
|
|
40
|
+
"KeyColumn",
|
|
41
|
+
"NestedJoin",
|
|
42
|
+
"NestedRelation",
|
|
43
|
+
"RelationBlock",
|
|
44
|
+
"batch_conditions",
|
|
45
|
+
"batch_statement",
|
|
46
|
+
"collection_for",
|
|
47
|
+
"columns_of",
|
|
48
|
+
"decode_collection",
|
|
49
|
+
"hydrate_collection",
|
|
50
|
+
"join_condition",
|
|
51
|
+
"key_condition",
|
|
52
|
+
"projection_layout",
|
|
53
|
+
"root_statement",
|
|
54
|
+
"select_in_conditions",
|
|
55
|
+
)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Reading a nested collection back into rows.
|
|
2
|
+
|
|
3
|
+
The value a collection returns is JSON, and what a driver hands over differs:
|
|
4
|
+
two of the three return it as text and one has already decoded it. Both arrive
|
|
5
|
+
here, and neither is trusted to hold what it promised.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
from itertools import zip_longest
|
|
13
|
+
from typing import cast
|
|
14
|
+
|
|
15
|
+
from pyoq.errors import CollectionDecodeError
|
|
16
|
+
from pyoq.hydration.plan import RowConstructor
|
|
17
|
+
from pyoq.query.decoding import decode_value
|
|
18
|
+
from pyoq.relations import RelationValue
|
|
19
|
+
|
|
20
|
+
_MISSING = object()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def decode_collection(
|
|
24
|
+
payload: object,
|
|
25
|
+
keys: Sequence[str],
|
|
26
|
+
/,
|
|
27
|
+
types: tuple[type[object] | None, ...] = (),
|
|
28
|
+
) -> tuple[tuple[object, ...], ...]:
|
|
29
|
+
"""Read one collection into a row of values per element.
|
|
30
|
+
|
|
31
|
+
Values come back in the order the keys are given, so a caller reads them
|
|
32
|
+
the same way it reads any other row rather than by looking names up.
|
|
33
|
+
|
|
34
|
+
JSON spells a date as a string and a decimal as a number, so a caller that
|
|
35
|
+
says what its columns hold gets them back as that, the same as it would from
|
|
36
|
+
a column read directly. A caller that says nothing gets what JSON spelled.
|
|
37
|
+
"""
|
|
38
|
+
_require_keys(keys)
|
|
39
|
+
elements = _elements(payload)
|
|
40
|
+
rows = tuple(_row(element, keys, index) for index, element in enumerate(elements))
|
|
41
|
+
if not types:
|
|
42
|
+
return rows
|
|
43
|
+
return tuple(_decoded(row, types) for row in rows)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _elements(payload: object) -> list[object]:
|
|
47
|
+
value = _parsed(payload)
|
|
48
|
+
if value is None:
|
|
49
|
+
return []
|
|
50
|
+
if not isinstance(value, list):
|
|
51
|
+
message = (
|
|
52
|
+
f"a collection must hold a list of rows, and this holds "
|
|
53
|
+
f"{type(value).__name__}"
|
|
54
|
+
)
|
|
55
|
+
raise CollectionDecodeError(message)
|
|
56
|
+
return cast("list[object]", value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parsed(payload: object) -> object:
|
|
60
|
+
"""One driver returns text and another returns what it already decoded."""
|
|
61
|
+
if payload is None:
|
|
62
|
+
return None
|
|
63
|
+
if isinstance(payload, (str, bytes, bytearray)):
|
|
64
|
+
return _loaded(payload)
|
|
65
|
+
return payload
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _loaded(payload: str | bytes | bytearray) -> object:
|
|
69
|
+
try:
|
|
70
|
+
return json.loads(payload)
|
|
71
|
+
except ValueError as error:
|
|
72
|
+
message = f"a collection did not hold readable JSON: {error}"
|
|
73
|
+
raise CollectionDecodeError(message) from error
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _row(
|
|
77
|
+
element: object,
|
|
78
|
+
keys: Sequence[str],
|
|
79
|
+
index: int,
|
|
80
|
+
) -> tuple[object, ...]:
|
|
81
|
+
if not isinstance(element, dict):
|
|
82
|
+
message = (
|
|
83
|
+
f"row {index} of a collection must be an object, and this is "
|
|
84
|
+
f"{type(element).__name__}"
|
|
85
|
+
)
|
|
86
|
+
raise CollectionDecodeError(message)
|
|
87
|
+
entries = cast("dict[str, object]", element)
|
|
88
|
+
return tuple(_value(entries, key, index) for key in keys)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _value(entries: dict[str, object], key: str, index: int) -> object:
|
|
92
|
+
value = entries.get(key, _MISSING)
|
|
93
|
+
if value is _MISSING:
|
|
94
|
+
message = f"row {index} of a collection has no {key!r}"
|
|
95
|
+
raise CollectionDecodeError(message)
|
|
96
|
+
return value
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _require_keys(keys: Sequence[str]) -> None:
|
|
100
|
+
if not keys:
|
|
101
|
+
message = "a collection is read by naming at least one value"
|
|
102
|
+
raise CollectionDecodeError(message)
|
|
103
|
+
if len(set(keys)) != len(keys):
|
|
104
|
+
message = "a collection cannot be read by one name twice"
|
|
105
|
+
raise CollectionDecodeError(message)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _decoded(
|
|
109
|
+
row: tuple[object, ...],
|
|
110
|
+
types: tuple[type[object] | None, ...],
|
|
111
|
+
) -> tuple[object, ...]:
|
|
112
|
+
return tuple(
|
|
113
|
+
decode_value(value, target)
|
|
114
|
+
for value, target in zip_longest(row, types[: len(row)])
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def hydrate_collection(
|
|
119
|
+
payload: object,
|
|
120
|
+
keys: Sequence[str],
|
|
121
|
+
construct: RowConstructor,
|
|
122
|
+
/,
|
|
123
|
+
types: tuple[type[object] | None, ...] = (),
|
|
124
|
+
) -> RelationValue[object]:
|
|
125
|
+
"""Read a collection into built rows, loaded and ready.
|
|
126
|
+
|
|
127
|
+
A collection that came back empty is a loaded relation holding nothing,
|
|
128
|
+
which is not the same as a relation nobody fetched.
|
|
129
|
+
"""
|
|
130
|
+
rows = tuple(
|
|
131
|
+
construct(values, {}) for values in decode_collection(payload, keys, types)
|
|
132
|
+
)
|
|
133
|
+
return RelationValue.loaded(rows)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = ("decode_collection", "hydrate_collection")
|