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.
Files changed (267) hide show
  1. pyoq/__init__.py +10 -0
  2. pyoq/__main__.py +5 -0
  3. pyoq/_native.pyi +5 -0
  4. pyoq/cli/__init__.py +5 -0
  5. pyoq/cli/commands.py +270 -0
  6. pyoq/cli/defaults.py +98 -0
  7. pyoq/cli/services.py +97 -0
  8. pyoq/config/__init__.py +31 -0
  9. pyoq/config/connection.py +161 -0
  10. pyoq/config/loader.py +289 -0
  11. pyoq/config/models.py +245 -0
  12. pyoq/config/values.py +142 -0
  13. pyoq/descriptors.py +165 -0
  14. pyoq/diagnostics/__init__.py +68 -0
  15. pyoq/diagnostics/budget.py +136 -0
  16. pyoq/diagnostics/events.py +137 -0
  17. pyoq/diagnostics/fingerprint.py +267 -0
  18. pyoq/diagnostics/instrumented.py +237 -0
  19. pyoq/diagnostics/metrics.py +61 -0
  20. pyoq/diagnostics/observation.py +227 -0
  21. pyoq/diagnostics/scoped.py +103 -0
  22. pyoq/django/__init__.py +15 -0
  23. pyoq/django/apps.py +17 -0
  24. pyoq/django/execution.py +317 -0
  25. pyoq/django/generation.py +59 -0
  26. pyoq/django/management/__init__.py +0 -0
  27. pyoq/django/management/commands/__init__.py +0 -0
  28. pyoq/django/management/commands/makemigrations.py +53 -0
  29. pyoq/django/management/commands/pyoq_codegen.py +75 -0
  30. pyoq/django/parameters.py +101 -0
  31. pyoq/django/schema.py +379 -0
  32. pyoq/django/settings.py +87 -0
  33. pyoq/django/timeouts.py +105 -0
  34. pyoq/dsl/__init__.py +64 -0
  35. pyoq/dsl/aio/__init__.py +31 -0
  36. pyoq/dsl/aio/context.py +295 -0
  37. pyoq/dsl/aio/queries.py +335 -0
  38. pyoq/dsl/aio/writes.py +368 -0
  39. pyoq/dsl/context.py +326 -0
  40. pyoq/dsl/entry.py +37 -0
  41. pyoq/dsl/labels.py +36 -0
  42. pyoq/dsl/queries.py +339 -0
  43. pyoq/dsl/result.py +164 -0
  44. pyoq/dsl/writes.py +360 -0
  45. pyoq/errors.py +317 -0
  46. pyoq/fastapi/__init__.py +32 -0
  47. pyoq/fastapi/dependencies.py +167 -0
  48. pyoq/fastapi/lifespan.py +119 -0
  49. pyoq/fetching/__init__.py +55 -0
  50. pyoq/fetching/collections.py +136 -0
  51. pyoq/fetching/execution.py +587 -0
  52. pyoq/fetching/joined.py +79 -0
  53. pyoq/fetching/nesting.py +183 -0
  54. pyoq/fetching/plans.py +541 -0
  55. pyoq/fetching/select_in.py +149 -0
  56. pyoq/fetching/tables.py +110 -0
  57. pyoq/generation/__init__.py +54 -0
  58. pyoq/generation/cleanup.py +44 -0
  59. pyoq/generation/contracts.py +248 -0
  60. pyoq/generation/drift.py +169 -0
  61. pyoq/generation/lock.py +33 -0
  62. pyoq/generation/manifest.py +114 -0
  63. pyoq/generation/model.py +1001 -0
  64. pyoq/generation/pipeline.py +119 -0
  65. pyoq/generation/rendering/__init__.py +5 -0
  66. pyoq/generation/rendering/domains.py +51 -0
  67. pyoq/generation/rendering/enums.py +29 -0
  68. pyoq/generation/rendering/exports.py +70 -0
  69. pyoq/generation/rendering/imports.py +63 -0
  70. pyoq/generation/rendering/package.py +56 -0
  71. pyoq/generation/rendering/relations.py +133 -0
  72. pyoq/generation/rendering/routines.py +396 -0
  73. pyoq/generation/rendering/rows.py +79 -0
  74. pyoq/generation/rendering/source.py +121 -0
  75. pyoq/generation/rendering/tables.py +300 -0
  76. pyoq/generation/rendering/writes.py +514 -0
  77. pyoq/generation/validation.py +27 -0
  78. pyoq/generation/writer.py +184 -0
  79. pyoq/hydration/__init__.py +24 -0
  80. pyoq/hydration/engine.py +155 -0
  81. pyoq/hydration/identity.py +194 -0
  82. pyoq/hydration/plan.py +116 -0
  83. pyoq/migrations/__init__.py +9 -0
  84. pyoq/migrations/alembic.py +106 -0
  85. pyoq/migrations/hooks.py +75 -0
  86. pyoq/naming.py +261 -0
  87. pyoq/policies/__init__.py +47 -0
  88. pyoq/policies/bypass.py +122 -0
  89. pyoq/policies/governed.py +430 -0
  90. pyoq/policies/model.py +242 -0
  91. pyoq/policies/rewriting.py +263 -0
  92. pyoq/py.typed +1 -0
  93. pyoq/query/__init__.py +312 -0
  94. pyoq/query/aggregates.py +172 -0
  95. pyoq/query/arrays.py +65 -0
  96. pyoq/query/binding.py +52 -0
  97. pyoq/query/capabilities.py +317 -0
  98. pyoq/query/casts.py +73 -0
  99. pyoq/query/choices.py +185 -0
  100. pyoq/query/decoding.py +360 -0
  101. pyoq/query/documents.py +56 -0
  102. pyoq/query/execution/__init__.py +63 -0
  103. pyoq/query/execution/aio/__init__.py +31 -0
  104. pyoq/query/execution/aio/operations.py +228 -0
  105. pyoq/query/execution/aio/pooling.py +233 -0
  106. pyoq/query/execution/aio/streaming.py +161 -0
  107. pyoq/query/execution/aio/transactions.py +105 -0
  108. pyoq/query/execution/batch.py +96 -0
  109. pyoq/query/execution/binding_style.py +30 -0
  110. pyoq/query/execution/compilation.py +48 -0
  111. pyoq/query/execution/context.py +61 -0
  112. pyoq/query/execution/control.py +50 -0
  113. pyoq/query/execution/operations.py +224 -0
  114. pyoq/query/execution/planning.py +107 -0
  115. pyoq/query/execution/pooling.py +279 -0
  116. pyoq/query/execution/results.py +36 -0
  117. pyoq/query/execution/streaming.py +178 -0
  118. pyoq/query/execution/transactions.py +95 -0
  119. pyoq/query/expressions.py +1200 -0
  120. pyoq/query/fields.py +60 -0
  121. pyoq/query/mysql/__init__.py +59 -0
  122. pyoq/query/mysql/aio/__init__.py +38 -0
  123. pyoq/query/mysql/aio/commands.py +389 -0
  124. pyoq/query/mysql/aio/driver.py +196 -0
  125. pyoq/query/mysql/aio/executor.py +123 -0
  126. pyoq/query/mysql/aio/factory.py +26 -0
  127. pyoq/query/mysql/aio/operations.py +38 -0
  128. pyoq/query/mysql/aio/pool.py +53 -0
  129. pyoq/query/mysql/aio/transactions.py +313 -0
  130. pyoq/query/mysql/commands.py +354 -0
  131. pyoq/query/mysql/compiler.py +134 -0
  132. pyoq/query/mysql/context.py +20 -0
  133. pyoq/query/mysql/executor.py +126 -0
  134. pyoq/query/mysql/expressions.py +244 -0
  135. pyoq/query/mysql/factory.py +46 -0
  136. pyoq/query/mysql/health.py +66 -0
  137. pyoq/query/mysql/identifiers.py +9 -0
  138. pyoq/query/mysql/model.py +79 -0
  139. pyoq/query/mysql/operations.py +43 -0
  140. pyoq/query/mysql/parameters.py +69 -0
  141. pyoq/query/mysql/planning.py +20 -0
  142. pyoq/query/mysql/pool.py +67 -0
  143. pyoq/query/mysql/transactions.py +331 -0
  144. pyoq/query/mysql/writes.py +73 -0
  145. pyoq/query/nodes.py +750 -0
  146. pyoq/query/postgres/__init__.py +48 -0
  147. pyoq/query/postgres/aio/__init__.py +25 -0
  148. pyoq/query/postgres/aio/bulk.py +56 -0
  149. pyoq/query/postgres/aio/commands.py +264 -0
  150. pyoq/query/postgres/aio/executor.py +152 -0
  151. pyoq/query/postgres/aio/factory.py +26 -0
  152. pyoq/query/postgres/aio/operations.py +26 -0
  153. pyoq/query/postgres/aio/pool.py +40 -0
  154. pyoq/query/postgres/aio/transactions.py +295 -0
  155. pyoq/query/postgres/bulk.py +62 -0
  156. pyoq/query/postgres/commands.py +238 -0
  157. pyoq/query/postgres/compiler.py +114 -0
  158. pyoq/query/postgres/context.py +20 -0
  159. pyoq/query/postgres/executor.py +147 -0
  160. pyoq/query/postgres/expressions.py +311 -0
  161. pyoq/query/postgres/factory.py +24 -0
  162. pyoq/query/postgres/health.py +24 -0
  163. pyoq/query/postgres/identifiers.py +9 -0
  164. pyoq/query/postgres/model.py +81 -0
  165. pyoq/query/postgres/operations.py +25 -0
  166. pyoq/query/postgres/parameters.py +71 -0
  167. pyoq/query/postgres/planning.py +20 -0
  168. pyoq/query/postgres/pool.py +52 -0
  169. pyoq/query/postgres/transactions.py +295 -0
  170. pyoq/query/postgres/writes.py +37 -0
  171. pyoq/query/projections.py +105 -0
  172. pyoq/query/raw.py +90 -0
  173. pyoq/query/recursion.py +265 -0
  174. pyoq/query/rendering/__init__.py +1 -0
  175. pyoq/query/rendering/expressions.py +913 -0
  176. pyoq/query/rendering/identifiers.py +40 -0
  177. pyoq/query/rendering/projections.py +63 -0
  178. pyoq/query/rendering/queries.py +334 -0
  179. pyoq/query/rendering/sources.py +66 -0
  180. pyoq/query/rendering/writes.py +176 -0
  181. pyoq/query/results.py +459 -0
  182. pyoq/query/routines.py +196 -0
  183. pyoq/query/rows.py +156 -0
  184. pyoq/query/select.py +793 -0
  185. pyoq/query/select_nodes.py +277 -0
  186. pyoq/query/sources.py +236 -0
  187. pyoq/query/sqlite/__init__.py +43 -0
  188. pyoq/query/sqlite/commands.py +201 -0
  189. pyoq/query/sqlite/compiler.py +139 -0
  190. pyoq/query/sqlite/context.py +20 -0
  191. pyoq/query/sqlite/executor.py +119 -0
  192. pyoq/query/sqlite/expressions.py +224 -0
  193. pyoq/query/sqlite/factory.py +32 -0
  194. pyoq/query/sqlite/health.py +28 -0
  195. pyoq/query/sqlite/identifiers.py +9 -0
  196. pyoq/query/sqlite/model.py +73 -0
  197. pyoq/query/sqlite/operations.py +36 -0
  198. pyoq/query/sqlite/parameters.py +50 -0
  199. pyoq/query/sqlite/planning.py +20 -0
  200. pyoq/query/sqlite/pool.py +50 -0
  201. pyoq/query/sqlite/streaming.py +13 -0
  202. pyoq/query/sqlite/transactions.py +274 -0
  203. pyoq/query/sqlite/writes.py +35 -0
  204. pyoq/query/statements.py +27 -0
  205. pyoq/query/values.py +23 -0
  206. pyoq/query/vendor.py +162 -0
  207. pyoq/query/windows.py +424 -0
  208. pyoq/query/write_nodes.py +174 -0
  209. pyoq/query/writes.py +628 -0
  210. pyoq/relations/__init__.py +66 -0
  211. pyoq/relations/batching.py +219 -0
  212. pyoq/relations/derivation.py +111 -0
  213. pyoq/relations/fetching.py +355 -0
  214. pyoq/relations/graph.py +245 -0
  215. pyoq/relations/loading.py +74 -0
  216. pyoq/relations/model.py +75 -0
  217. pyoq/relations/planning.py +206 -0
  218. pyoq/runtime/__init__.py +9 -0
  219. pyoq/runtime/kernels.py +25 -0
  220. pyoq/runtime/python.py +43 -0
  221. pyoq/runtime/selection.py +73 -0
  222. pyoq/sanic/__init__.py +32 -0
  223. pyoq/sanic/scope.py +197 -0
  224. pyoq/sanic/workers.py +129 -0
  225. pyoq/schema/__init__.py +108 -0
  226. pyoq/schema/codec.py +711 -0
  227. pyoq/schema/models.py +604 -0
  228. pyoq/schema/mysql/__init__.py +16 -0
  229. pyoq/schema/mysql/connection.py +73 -0
  230. pyoq/schema/mysql/dsn.py +72 -0
  231. pyoq/schema/mysql/records.py +354 -0
  232. pyoq/schema/mysql/reflection.py +309 -0
  233. pyoq/schema/mysql/source.py +30 -0
  234. pyoq/schema/mysql/sql.py +128 -0
  235. pyoq/schema/mysql/types.py +105 -0
  236. pyoq/schema/postgres/__init__.py +13 -0
  237. pyoq/schema/postgres/connection.py +63 -0
  238. pyoq/schema/postgres/records.py +384 -0
  239. pyoq/schema/postgres/reflection.py +466 -0
  240. pyoq/schema/postgres/source.py +30 -0
  241. pyoq/schema/postgres/sql.py +246 -0
  242. pyoq/schema/postgres/types.py +98 -0
  243. pyoq/schema/registry.py +45 -0
  244. pyoq/schema/source.py +15 -0
  245. pyoq/schema/sqlite/__init__.py +6 -0
  246. pyoq/schema/sqlite/connection.py +54 -0
  247. pyoq/schema/sqlite/records.py +167 -0
  248. pyoq/schema/sqlite/reflection.py +393 -0
  249. pyoq/schema/sqlite/source.py +30 -0
  250. pyoq/schema/sqlite/sql.py +254 -0
  251. pyoq/schema/sqlite/types.py +74 -0
  252. pyoq/serving/__init__.py +23 -0
  253. pyoq/serving/databases.py +107 -0
  254. pyoq/serving/opening.py +331 -0
  255. pyoq/snapshots/__init__.py +20 -0
  256. pyoq/snapshots/drift.py +312 -0
  257. pyoq/snapshots/files.py +96 -0
  258. pyoq/snapshots/routing.py +40 -0
  259. pyoq/snapshots/source.py +33 -0
  260. pyoq/tracing/__init__.py +5 -0
  261. pyoq/tracing/spans.py +89 -0
  262. pyoq/unset.py +14 -0
  263. pyoq_sql-1.0.2.dist-info/METADATA +3050 -0
  264. pyoq_sql-1.0.2.dist-info/RECORD +267 -0
  265. pyoq_sql-1.0.2.dist-info/WHEEL +4 -0
  266. pyoq_sql-1.0.2.dist-info/entry_points.txt +3 -0
  267. 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
+ )
@@ -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
+ )