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,73 @@
|
|
|
1
|
+
"""Runtime engine selection."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import Protocol, cast
|
|
7
|
+
from warnings import warn
|
|
8
|
+
|
|
9
|
+
from pyoq.errors import NativeRuntimeUnavailableError, RuntimeSelectionError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RuntimeEngine(StrEnum):
|
|
13
|
+
NATIVE = "native"
|
|
14
|
+
PYTHON = "python"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RuntimeBackend(Protocol):
|
|
18
|
+
def runtime_marker(self) -> str: ...
|
|
19
|
+
|
|
20
|
+
def fingerprint_fragments(self, fragments: list[bytes]) -> int: ...
|
|
21
|
+
|
|
22
|
+
def group_integer_keys(
|
|
23
|
+
self,
|
|
24
|
+
keys: list[int | None],
|
|
25
|
+
) -> list[tuple[int, int]]: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
NativeLoader = Callable[[], RuntimeBackend]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def select_runtime(
|
|
32
|
+
preference: str | None,
|
|
33
|
+
native_loader: NativeLoader | None = None,
|
|
34
|
+
) -> RuntimeEngine:
|
|
35
|
+
normalized_preference = (preference or "auto").lower()
|
|
36
|
+
if normalized_preference == RuntimeEngine.PYTHON:
|
|
37
|
+
return RuntimeEngine.PYTHON
|
|
38
|
+
if normalized_preference not in {"auto", RuntimeEngine.NATIVE}:
|
|
39
|
+
message = "PYOQ_RUNTIME must be 'auto', 'native', or 'python'"
|
|
40
|
+
raise RuntimeSelectionError(message)
|
|
41
|
+
|
|
42
|
+
loader = native_loader or load_native_runtime
|
|
43
|
+
try:
|
|
44
|
+
native_runtime = loader()
|
|
45
|
+
except ImportError as error:
|
|
46
|
+
return _handle_missing_native(normalized_preference, error)
|
|
47
|
+
|
|
48
|
+
if native_runtime.runtime_marker() != RuntimeEngine.NATIVE:
|
|
49
|
+
message = "The loaded PyOQ native module failed its identity check"
|
|
50
|
+
raise NativeRuntimeUnavailableError(message)
|
|
51
|
+
return RuntimeEngine.NATIVE
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_native_runtime() -> RuntimeBackend:
|
|
55
|
+
return cast(RuntimeBackend, import_module("pyoq._native"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_python_runtime() -> RuntimeBackend:
|
|
59
|
+
return cast(RuntimeBackend, import_module("pyoq.runtime.python"))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _handle_missing_native(
|
|
63
|
+
preference: str,
|
|
64
|
+
error: ImportError,
|
|
65
|
+
) -> RuntimeEngine:
|
|
66
|
+
message = (
|
|
67
|
+
"The PyOQ native runtime is unavailable. Install a supported wheel or "
|
|
68
|
+
"set PYOQ_RUNTIME=python to select the slower fallback explicitly."
|
|
69
|
+
)
|
|
70
|
+
if preference == RuntimeEngine.NATIVE:
|
|
71
|
+
raise NativeRuntimeUnavailableError(message) from error
|
|
72
|
+
warn(message, RuntimeWarning, stacklevel=3)
|
|
73
|
+
return RuntimeEngine.PYTHON
|
pyoq/sanic/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Running PyOQ inside a Sanic application."""
|
|
2
|
+
|
|
3
|
+
from pyoq.sanic.scope import (
|
|
4
|
+
attach_request_budget,
|
|
5
|
+
attach_synchronous_request_budget,
|
|
6
|
+
budgeted_of,
|
|
7
|
+
database_of,
|
|
8
|
+
synchronous_budgeted_of,
|
|
9
|
+
synchronous_database_of,
|
|
10
|
+
synchronous_transaction,
|
|
11
|
+
transaction,
|
|
12
|
+
)
|
|
13
|
+
from pyoq.sanic.workers import WorkerDatabases, attach_databases, databases_of
|
|
14
|
+
from pyoq.serving import DEFAULT, AsyncDatabase, Databases, SyncDatabase
|
|
15
|
+
|
|
16
|
+
__all__ = (
|
|
17
|
+
"DEFAULT",
|
|
18
|
+
"AsyncDatabase",
|
|
19
|
+
"Databases",
|
|
20
|
+
"SyncDatabase",
|
|
21
|
+
"WorkerDatabases",
|
|
22
|
+
"attach_databases",
|
|
23
|
+
"attach_request_budget",
|
|
24
|
+
"attach_synchronous_request_budget",
|
|
25
|
+
"budgeted_of",
|
|
26
|
+
"database_of",
|
|
27
|
+
"databases_of",
|
|
28
|
+
"synchronous_budgeted_of",
|
|
29
|
+
"synchronous_database_of",
|
|
30
|
+
"synchronous_transaction",
|
|
31
|
+
"transaction",
|
|
32
|
+
)
|
pyoq/sanic/scope.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""What one request holds, and what ends it.
|
|
2
|
+
|
|
3
|
+
A transaction is scoped to the handler's own block rather than to a pair of
|
|
4
|
+
middlewares. A server that is told the client has gone cancels the task it
|
|
5
|
+
called the application on, and nothing after the handler runs, response
|
|
6
|
+
middleware included. A transaction ended there would keep its connection for as
|
|
7
|
+
long as the worker lives, inside an open transaction, holding whatever it
|
|
8
|
+
locked. A block the handler enters is ended by Python itself, so cancellation
|
|
9
|
+
reaches it like any other exception.
|
|
10
|
+
|
|
11
|
+
A budget holds nothing, so it is still attached once and read per request.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from contextlib import asynccontextmanager, contextmanager
|
|
17
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
18
|
+
|
|
19
|
+
from pyoq.diagnostics.scoped import AsyncScopedOperations, ScopedOperations
|
|
20
|
+
from pyoq.errors import OperationUnavailableError
|
|
21
|
+
from pyoq.sanic.workers import databases_of
|
|
22
|
+
from pyoq.serving import DEFAULT, SupportsAsyncTransaction, SupportsTransaction
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import AsyncGenerator, Awaitable, Callable, Generator
|
|
26
|
+
|
|
27
|
+
from sanic.config import Config
|
|
28
|
+
|
|
29
|
+
from pyoq.diagnostics.budget import QueryBudget
|
|
30
|
+
from pyoq.query.execution import QueryOperations
|
|
31
|
+
from pyoq.query.execution.aio import AsyncQueryOperations
|
|
32
|
+
from sanic import Request, Sanic
|
|
33
|
+
|
|
34
|
+
ConfigType = TypeVar("ConfigType", bound="Config")
|
|
35
|
+
ContextType = TypeVar("ContextType")
|
|
36
|
+
|
|
37
|
+
SCOPE_ATTRIBUTE = "pyoq_scope"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@asynccontextmanager
|
|
41
|
+
async def transaction(
|
|
42
|
+
request: Request,
|
|
43
|
+
/,
|
|
44
|
+
name: str = DEFAULT,
|
|
45
|
+
) -> AsyncGenerator[AsyncQueryOperations]:
|
|
46
|
+
"""One transaction for as long as the block runs.
|
|
47
|
+
|
|
48
|
+
Leaving the block normally commits. Leaving it any other way rolls back: a
|
|
49
|
+
handler that raised, and a request the server abandoned, which reaches the
|
|
50
|
+
block as a cancellation like anything else would.
|
|
51
|
+
"""
|
|
52
|
+
operations = databases_of(request.app).asynchronous(name)
|
|
53
|
+
async with _transactional(operations, name).transaction() as active:
|
|
54
|
+
yield active
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@contextmanager
|
|
58
|
+
def synchronous_transaction(
|
|
59
|
+
request: Request,
|
|
60
|
+
/,
|
|
61
|
+
name: str = DEFAULT,
|
|
62
|
+
) -> Generator[QueryOperations]:
|
|
63
|
+
"""The same, for a database that is not awaited."""
|
|
64
|
+
operations = databases_of(request.app).synchronous(name)
|
|
65
|
+
with _synchronous(operations, name).transaction() as active:
|
|
66
|
+
yield active
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def database_of(request: Request, /, name: str = DEFAULT) -> AsyncQueryOperations:
|
|
70
|
+
"""The worker's database, for a request that runs no transaction."""
|
|
71
|
+
return databases_of(request.app).asynchronous(name)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def synchronous_database_of(
|
|
75
|
+
request: Request,
|
|
76
|
+
/,
|
|
77
|
+
name: str = DEFAULT,
|
|
78
|
+
) -> QueryOperations:
|
|
79
|
+
return databases_of(request.app).synchronous(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def attach_request_budget(
|
|
83
|
+
app: Sanic[ConfigType, ContextType],
|
|
84
|
+
budget: QueryBudget,
|
|
85
|
+
/,
|
|
86
|
+
name: str = DEFAULT,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""A budget per request, so one request cannot spend another's.
|
|
89
|
+
|
|
90
|
+
A budget holds no connection, so nothing is left behind when a request ends
|
|
91
|
+
early and one middleware is enough.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
async def open_scope(request: Request) -> None:
|
|
95
|
+
from pyoq.diagnostics.budget import QueryScope
|
|
96
|
+
|
|
97
|
+
operations = databases_of(request.app).asynchronous(name)
|
|
98
|
+
counted = AsyncScopedOperations(operations, QueryScope(budget))
|
|
99
|
+
setattr(request.ctx, SCOPE_ATTRIBUTE, counted)
|
|
100
|
+
|
|
101
|
+
_on_request(app, open_scope)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def attach_synchronous_request_budget(
|
|
105
|
+
app: Sanic[ConfigType, ContextType],
|
|
106
|
+
budget: QueryBudget,
|
|
107
|
+
/,
|
|
108
|
+
name: str = DEFAULT,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""The same, for a database that is not awaited."""
|
|
111
|
+
|
|
112
|
+
async def open_scope(request: Request) -> None:
|
|
113
|
+
from pyoq.diagnostics.budget import QueryScope
|
|
114
|
+
|
|
115
|
+
operations = databases_of(request.app).synchronous(name)
|
|
116
|
+
setattr(
|
|
117
|
+
request.ctx,
|
|
118
|
+
SCOPE_ATTRIBUTE,
|
|
119
|
+
ScopedOperations(operations, QueryScope(budget)),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
_on_request(app, open_scope)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def budgeted_of(request: Request, /) -> AsyncQueryOperations:
|
|
126
|
+
"""The database this request may use, counting as it goes."""
|
|
127
|
+
counted = getattr(request.ctx, SCOPE_ATTRIBUTE, None)
|
|
128
|
+
if not isinstance(counted, AsyncScopedOperations):
|
|
129
|
+
raise _no_budget()
|
|
130
|
+
return counted
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def synchronous_budgeted_of(request: Request, /) -> QueryOperations:
|
|
134
|
+
counted = getattr(request.ctx, SCOPE_ATTRIBUTE, None)
|
|
135
|
+
if not isinstance(counted, ScopedOperations):
|
|
136
|
+
raise _no_budget()
|
|
137
|
+
return counted
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _no_budget() -> LookupError:
|
|
141
|
+
message = (
|
|
142
|
+
"this request holds no PyOQ budget; call attach_request_budget(app, "
|
|
143
|
+
"budget) to give it one"
|
|
144
|
+
)
|
|
145
|
+
return LookupError(message)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _on_request(
|
|
149
|
+
app: Sanic[ConfigType, ContextType],
|
|
150
|
+
middleware: Callable[[Request], Awaitable[None]],
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Sanic leaves the request parameter of its middleware type free.
|
|
153
|
+
|
|
154
|
+
That makes the registration read as partly unknown to a checker even though
|
|
155
|
+
the callable is fully typed where it is written. This is the one place that
|
|
156
|
+
says so.
|
|
157
|
+
"""
|
|
158
|
+
app.register_middleware(middleware, "request") # type: ignore[arg-type] # pyright: ignore[reportUnknownMemberType, reportArgumentType]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _transactional(
|
|
162
|
+
operations: AsyncQueryOperations,
|
|
163
|
+
name: str,
|
|
164
|
+
) -> SupportsAsyncTransaction:
|
|
165
|
+
if not isinstance(operations, SupportsAsyncTransaction):
|
|
166
|
+
raise _no_transactions(name)
|
|
167
|
+
return operations
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _synchronous(
|
|
171
|
+
operations: QueryOperations,
|
|
172
|
+
name: str,
|
|
173
|
+
) -> SupportsTransaction:
|
|
174
|
+
if not isinstance(operations, SupportsTransaction):
|
|
175
|
+
raise _no_transactions(name)
|
|
176
|
+
return operations
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _no_transactions(name: str) -> OperationUnavailableError:
|
|
180
|
+
message = (
|
|
181
|
+
f"the database named {name!r} opens no transactions of its own, so a "
|
|
182
|
+
f"request transaction cannot be started against it"
|
|
183
|
+
)
|
|
184
|
+
return OperationUnavailableError(message)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
__all__ = (
|
|
188
|
+
"SCOPE_ATTRIBUTE",
|
|
189
|
+
"attach_request_budget",
|
|
190
|
+
"attach_synchronous_request_budget",
|
|
191
|
+
"budgeted_of",
|
|
192
|
+
"database_of",
|
|
193
|
+
"synchronous_budgeted_of",
|
|
194
|
+
"synchronous_database_of",
|
|
195
|
+
"synchronous_transaction",
|
|
196
|
+
"transaction",
|
|
197
|
+
)
|
pyoq/sanic/workers.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Opening a database in the worker that will use it.
|
|
2
|
+
|
|
3
|
+
Sanic starts its workers by forking. A connection made before that fork is
|
|
4
|
+
inherited by every worker at once, and two processes talking over one socket
|
|
5
|
+
corrupt each other's results. So the pool is opened from the listener that runs
|
|
6
|
+
inside each worker, and never from the one that runs in the main process.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar
|
|
13
|
+
|
|
14
|
+
from pyoq.errors import PyOQError
|
|
15
|
+
from pyoq.serving import AsyncDatabase, Databases, SyncDatabase
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Mapping
|
|
19
|
+
|
|
20
|
+
from sanic.config import Config
|
|
21
|
+
|
|
22
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
23
|
+
|
|
24
|
+
from sanic import Sanic
|
|
25
|
+
|
|
26
|
+
ConfigType = TypeVar("ConfigType", bound="Config")
|
|
27
|
+
ContextType = TypeVar("ContextType")
|
|
28
|
+
|
|
29
|
+
Listener: TypeAlias = Callable[
|
|
30
|
+
["Sanic[ConfigType, ContextType]"], Coroutine[object, object, None]
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
CONTEXT_ATTRIBUTE = "pyoq_databases"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True, slots=True)
|
|
37
|
+
class WorkerDatabases(Generic[ConfigType, ContextType]):
|
|
38
|
+
"""The two listeners an attachment registered.
|
|
39
|
+
|
|
40
|
+
They are handed back so an application can run them itself, and so what
|
|
41
|
+
they do can be checked without asking Sanic what it was told.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
open: Listener[ConfigType, ContextType]
|
|
45
|
+
close: Listener[ConfigType, ContextType]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def attach_databases(
|
|
49
|
+
app: Sanic[ConfigType, ContextType],
|
|
50
|
+
/,
|
|
51
|
+
*,
|
|
52
|
+
synchronous: Mapping[str, Callable[[], SyncDatabase]] | None = None,
|
|
53
|
+
asynchronous: Mapping[str, Callable[[], Awaitable[AsyncDatabase]]] | None = None,
|
|
54
|
+
) -> WorkerDatabases[ConfigType, ContextType]:
|
|
55
|
+
"""Give every worker its own databases, opened and closed with the server.
|
|
56
|
+
|
|
57
|
+
`before_server_start` runs once inside each worker after the fork, which is
|
|
58
|
+
the earliest point a connection can be made without being shared.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
async def open_databases(running: Sanic[ConfigType, ContextType]) -> None:
|
|
62
|
+
opened_sync: dict[str, SyncDatabase] = {}
|
|
63
|
+
opened_async: dict[str, AsyncDatabase] = {}
|
|
64
|
+
try:
|
|
65
|
+
for name, build in (synchronous or {}).items():
|
|
66
|
+
opened_sync[name] = build()
|
|
67
|
+
for name, build_async in (asynchronous or {}).items():
|
|
68
|
+
opened_async[name] = await build_async()
|
|
69
|
+
except BaseException:
|
|
70
|
+
await _close_all(opened_sync, opened_async)
|
|
71
|
+
raise
|
|
72
|
+
setattr(
|
|
73
|
+
running.ctx,
|
|
74
|
+
CONTEXT_ATTRIBUTE,
|
|
75
|
+
Databases(opened_sync, opened_async),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
async def close_databases(running: Sanic[ConfigType, ContextType]) -> None:
|
|
79
|
+
held = getattr(running.ctx, CONTEXT_ATTRIBUTE, None)
|
|
80
|
+
if not isinstance(held, Databases):
|
|
81
|
+
return
|
|
82
|
+
delattr(running.ctx, CONTEXT_ATTRIBUTE)
|
|
83
|
+
await _close_all(*held.opened)
|
|
84
|
+
|
|
85
|
+
app.before_server_start(open_databases)
|
|
86
|
+
app.before_server_stop(close_databases)
|
|
87
|
+
return WorkerDatabases(open_databases, close_databases)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def _close_all(
|
|
91
|
+
synchronous: dict[str, SyncDatabase],
|
|
92
|
+
asynchronous: dict[str, AsyncDatabase],
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Give every database back, even when one of them refuses."""
|
|
95
|
+
failures: list[BaseException] = []
|
|
96
|
+
for waiting in reversed(list(asynchronous.values())):
|
|
97
|
+
try:
|
|
98
|
+
await waiting.close()
|
|
99
|
+
except PyOQError as error:
|
|
100
|
+
failures.append(error)
|
|
101
|
+
for immediate in reversed(list(synchronous.values())):
|
|
102
|
+
try:
|
|
103
|
+
immediate.close()
|
|
104
|
+
except PyOQError as error:
|
|
105
|
+
failures.append(error)
|
|
106
|
+
synchronous.clear()
|
|
107
|
+
asynchronous.clear()
|
|
108
|
+
if failures:
|
|
109
|
+
raise BaseExceptionGroup("closing databases failed", failures)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def databases_of(app: Sanic[ConfigType, ContextType], /) -> Databases:
|
|
113
|
+
"""What this worker opened, or a clear word about it not having run."""
|
|
114
|
+
held = getattr(app.ctx, CONTEXT_ATTRIBUTE, None)
|
|
115
|
+
if not isinstance(held, Databases):
|
|
116
|
+
message = (
|
|
117
|
+
"no PyOQ databases are open in this worker; call "
|
|
118
|
+
"attach_databases(app) before the server starts"
|
|
119
|
+
)
|
|
120
|
+
raise LookupError(message)
|
|
121
|
+
return held
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
__all__ = (
|
|
125
|
+
"CONTEXT_ATTRIBUTE",
|
|
126
|
+
"WorkerDatabases",
|
|
127
|
+
"attach_databases",
|
|
128
|
+
"databases_of",
|
|
129
|
+
)
|
pyoq/schema/__init__.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Public database schema interface."""
|
|
2
|
+
|
|
3
|
+
from pyoq.schema.models import (
|
|
4
|
+
SCHEMA_FORMAT_VERSION,
|
|
5
|
+
Capability,
|
|
6
|
+
CapabilitySet,
|
|
7
|
+
Catalog,
|
|
8
|
+
CheckConstraint,
|
|
9
|
+
Column,
|
|
10
|
+
Domain,
|
|
11
|
+
EnumType,
|
|
12
|
+
GeneratedKind,
|
|
13
|
+
GeneratedValue,
|
|
14
|
+
Identifier,
|
|
15
|
+
Index,
|
|
16
|
+
IndexTerm,
|
|
17
|
+
JsonScalar,
|
|
18
|
+
JsonValue,
|
|
19
|
+
Key,
|
|
20
|
+
KeyKind,
|
|
21
|
+
MetadataEntry,
|
|
22
|
+
ObjectReference,
|
|
23
|
+
ParameterMode,
|
|
24
|
+
ReferentialAction,
|
|
25
|
+
Relation,
|
|
26
|
+
Routine,
|
|
27
|
+
RoutineKind,
|
|
28
|
+
RoutineParameter,
|
|
29
|
+
Schema,
|
|
30
|
+
SchemaSnapshot,
|
|
31
|
+
SqlExpression,
|
|
32
|
+
SqlType,
|
|
33
|
+
Table,
|
|
34
|
+
TypeKind,
|
|
35
|
+
View,
|
|
36
|
+
)
|
|
37
|
+
from pyoq.schema.mysql import (
|
|
38
|
+
MySQLConnectionProvider,
|
|
39
|
+
MySQLConnectionSettings,
|
|
40
|
+
MySQLSchemaInspector,
|
|
41
|
+
MySQLSchemaSource,
|
|
42
|
+
mysql_type,
|
|
43
|
+
)
|
|
44
|
+
from pyoq.schema.postgres import (
|
|
45
|
+
PostgresConnectionProvider,
|
|
46
|
+
PostgresSchemaInspector,
|
|
47
|
+
PostgresSchemaSource,
|
|
48
|
+
postgres_type,
|
|
49
|
+
)
|
|
50
|
+
from pyoq.schema.registry import (
|
|
51
|
+
DialectRoutingSchemaSource,
|
|
52
|
+
SchemaSourceFactory,
|
|
53
|
+
schema_source_for,
|
|
54
|
+
supported_dialects,
|
|
55
|
+
)
|
|
56
|
+
from pyoq.schema.source import SchemaSource
|
|
57
|
+
from pyoq.schema.sqlite import SQLiteSchemaInspector, SQLiteSchemaSource
|
|
58
|
+
|
|
59
|
+
__all__ = (
|
|
60
|
+
"SCHEMA_FORMAT_VERSION",
|
|
61
|
+
"Capability",
|
|
62
|
+
"CapabilitySet",
|
|
63
|
+
"Catalog",
|
|
64
|
+
"CheckConstraint",
|
|
65
|
+
"Column",
|
|
66
|
+
"DialectRoutingSchemaSource",
|
|
67
|
+
"Domain",
|
|
68
|
+
"EnumType",
|
|
69
|
+
"GeneratedKind",
|
|
70
|
+
"GeneratedValue",
|
|
71
|
+
"Identifier",
|
|
72
|
+
"Index",
|
|
73
|
+
"IndexTerm",
|
|
74
|
+
"JsonScalar",
|
|
75
|
+
"JsonValue",
|
|
76
|
+
"Key",
|
|
77
|
+
"KeyKind",
|
|
78
|
+
"MetadataEntry",
|
|
79
|
+
"MySQLConnectionProvider",
|
|
80
|
+
"MySQLConnectionSettings",
|
|
81
|
+
"MySQLSchemaInspector",
|
|
82
|
+
"MySQLSchemaSource",
|
|
83
|
+
"ObjectReference",
|
|
84
|
+
"ParameterMode",
|
|
85
|
+
"PostgresConnectionProvider",
|
|
86
|
+
"PostgresSchemaInspector",
|
|
87
|
+
"PostgresSchemaSource",
|
|
88
|
+
"ReferentialAction",
|
|
89
|
+
"Relation",
|
|
90
|
+
"Routine",
|
|
91
|
+
"RoutineKind",
|
|
92
|
+
"RoutineParameter",
|
|
93
|
+
"SQLiteSchemaInspector",
|
|
94
|
+
"SQLiteSchemaSource",
|
|
95
|
+
"Schema",
|
|
96
|
+
"SchemaSnapshot",
|
|
97
|
+
"SchemaSource",
|
|
98
|
+
"SchemaSourceFactory",
|
|
99
|
+
"SqlExpression",
|
|
100
|
+
"SqlType",
|
|
101
|
+
"Table",
|
|
102
|
+
"TypeKind",
|
|
103
|
+
"View",
|
|
104
|
+
"mysql_type",
|
|
105
|
+
"postgres_type",
|
|
106
|
+
"schema_source_for",
|
|
107
|
+
"supported_dialects",
|
|
108
|
+
)
|