tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +97 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +291 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +86 -0
  11. tigrbl/app/_model_registry.py +41 -0
  12. tigrbl/app/app_spec.py +42 -0
  13. tigrbl/app/mro_collect.py +67 -0
  14. tigrbl/app/shortcuts.py +65 -0
  15. tigrbl/app/tigrbl_app.py +319 -0
  16. tigrbl/bindings/__init__.py +73 -0
  17. tigrbl/bindings/api/__init__.py +12 -0
  18. tigrbl/bindings/api/common.py +109 -0
  19. tigrbl/bindings/api/include.py +256 -0
  20. tigrbl/bindings/api/resource_proxy.py +149 -0
  21. tigrbl/bindings/api/rpc.py +111 -0
  22. tigrbl/bindings/columns.py +49 -0
  23. tigrbl/bindings/handlers/__init__.py +11 -0
  24. tigrbl/bindings/handlers/builder.py +119 -0
  25. tigrbl/bindings/handlers/ctx.py +74 -0
  26. tigrbl/bindings/handlers/identifiers.py +228 -0
  27. tigrbl/bindings/handlers/namespaces.py +51 -0
  28. tigrbl/bindings/handlers/steps.py +276 -0
  29. tigrbl/bindings/hooks.py +311 -0
  30. tigrbl/bindings/model.py +194 -0
  31. tigrbl/bindings/model_helpers.py +139 -0
  32. tigrbl/bindings/model_registry.py +77 -0
  33. tigrbl/bindings/rest/__init__.py +7 -0
  34. tigrbl/bindings/rest/attach.py +34 -0
  35. tigrbl/bindings/rest/collection.py +286 -0
  36. tigrbl/bindings/rest/common.py +120 -0
  37. tigrbl/bindings/rest/fastapi.py +76 -0
  38. tigrbl/bindings/rest/helpers.py +119 -0
  39. tigrbl/bindings/rest/io.py +317 -0
  40. tigrbl/bindings/rest/io_headers.py +49 -0
  41. tigrbl/bindings/rest/member.py +386 -0
  42. tigrbl/bindings/rest/router.py +296 -0
  43. tigrbl/bindings/rest/routing.py +153 -0
  44. tigrbl/bindings/rpc.py +364 -0
  45. tigrbl/bindings/schemas/__init__.py +11 -0
  46. tigrbl/bindings/schemas/builder.py +348 -0
  47. tigrbl/bindings/schemas/defaults.py +260 -0
  48. tigrbl/bindings/schemas/utils.py +193 -0
  49. tigrbl/column/README.md +62 -0
  50. tigrbl/column/__init__.py +72 -0
  51. tigrbl/column/_column.py +96 -0
  52. tigrbl/column/column_spec.py +40 -0
  53. tigrbl/column/field_spec.py +31 -0
  54. tigrbl/column/infer/__init__.py +25 -0
  55. tigrbl/column/infer/core.py +92 -0
  56. tigrbl/column/infer/jsonhints.py +44 -0
  57. tigrbl/column/infer/planning.py +133 -0
  58. tigrbl/column/infer/types.py +102 -0
  59. tigrbl/column/infer/utils.py +59 -0
  60. tigrbl/column/io_spec.py +136 -0
  61. tigrbl/column/mro_collect.py +59 -0
  62. tigrbl/column/shortcuts.py +89 -0
  63. tigrbl/column/storage_spec.py +65 -0
  64. tigrbl/config/__init__.py +19 -0
  65. tigrbl/config/constants.py +224 -0
  66. tigrbl/config/defaults.py +29 -0
  67. tigrbl/config/resolver.py +295 -0
  68. tigrbl/core/__init__.py +47 -0
  69. tigrbl/core/crud/__init__.py +36 -0
  70. tigrbl/core/crud/bulk.py +168 -0
  71. tigrbl/core/crud/helpers/__init__.py +76 -0
  72. tigrbl/core/crud/helpers/db.py +92 -0
  73. tigrbl/core/crud/helpers/enum.py +86 -0
  74. tigrbl/core/crud/helpers/filters.py +162 -0
  75. tigrbl/core/crud/helpers/model.py +123 -0
  76. tigrbl/core/crud/helpers/normalize.py +99 -0
  77. tigrbl/core/crud/ops.py +235 -0
  78. tigrbl/ddl/__init__.py +344 -0
  79. tigrbl/decorators.py +17 -0
  80. tigrbl/deps/__init__.py +20 -0
  81. tigrbl/deps/fastapi.py +45 -0
  82. tigrbl/deps/favicon.svg +4 -0
  83. tigrbl/deps/jinja.py +27 -0
  84. tigrbl/deps/pydantic.py +10 -0
  85. tigrbl/deps/sqlalchemy.py +94 -0
  86. tigrbl/deps/starlette.py +36 -0
  87. tigrbl/engine/__init__.py +45 -0
  88. tigrbl/engine/_engine.py +144 -0
  89. tigrbl/engine/bind.py +33 -0
  90. tigrbl/engine/builders.py +236 -0
  91. tigrbl/engine/capabilities.py +29 -0
  92. tigrbl/engine/collect.py +111 -0
  93. tigrbl/engine/decorators.py +110 -0
  94. tigrbl/engine/docs/PLUGINS.md +49 -0
  95. tigrbl/engine/engine_spec.py +355 -0
  96. tigrbl/engine/plugins.py +52 -0
  97. tigrbl/engine/registry.py +36 -0
  98. tigrbl/engine/resolver.py +224 -0
  99. tigrbl/engine/shortcuts.py +216 -0
  100. tigrbl/hook/__init__.py +21 -0
  101. tigrbl/hook/_hook.py +22 -0
  102. tigrbl/hook/decorators.py +28 -0
  103. tigrbl/hook/hook_spec.py +24 -0
  104. tigrbl/hook/mro_collect.py +98 -0
  105. tigrbl/hook/shortcuts.py +44 -0
  106. tigrbl/hook/types.py +76 -0
  107. tigrbl/op/__init__.py +50 -0
  108. tigrbl/op/_op.py +31 -0
  109. tigrbl/op/canonical.py +31 -0
  110. tigrbl/op/collect.py +11 -0
  111. tigrbl/op/decorators.py +238 -0
  112. tigrbl/op/model_registry.py +301 -0
  113. tigrbl/op/mro_collect.py +99 -0
  114. tigrbl/op/resolver.py +216 -0
  115. tigrbl/op/types.py +136 -0
  116. tigrbl/orm/__init__.py +1 -0
  117. tigrbl/orm/mixins/_RowBound.py +83 -0
  118. tigrbl/orm/mixins/__init__.py +95 -0
  119. tigrbl/orm/mixins/bootstrappable.py +113 -0
  120. tigrbl/orm/mixins/bound.py +47 -0
  121. tigrbl/orm/mixins/edges.py +40 -0
  122. tigrbl/orm/mixins/fields.py +165 -0
  123. tigrbl/orm/mixins/hierarchy.py +54 -0
  124. tigrbl/orm/mixins/key_digest.py +44 -0
  125. tigrbl/orm/mixins/lifecycle.py +115 -0
  126. tigrbl/orm/mixins/locks.py +51 -0
  127. tigrbl/orm/mixins/markers.py +16 -0
  128. tigrbl/orm/mixins/operations.py +57 -0
  129. tigrbl/orm/mixins/ownable.py +337 -0
  130. tigrbl/orm/mixins/principals.py +98 -0
  131. tigrbl/orm/mixins/tenant_bound.py +301 -0
  132. tigrbl/orm/mixins/upsertable.py +118 -0
  133. tigrbl/orm/mixins/utils.py +49 -0
  134. tigrbl/orm/tables/__init__.py +72 -0
  135. tigrbl/orm/tables/_base.py +8 -0
  136. tigrbl/orm/tables/audit.py +56 -0
  137. tigrbl/orm/tables/client.py +25 -0
  138. tigrbl/orm/tables/group.py +29 -0
  139. tigrbl/orm/tables/org.py +30 -0
  140. tigrbl/orm/tables/rbac.py +76 -0
  141. tigrbl/orm/tables/status.py +106 -0
  142. tigrbl/orm/tables/tenant.py +22 -0
  143. tigrbl/orm/tables/user.py +39 -0
  144. tigrbl/response/README.md +34 -0
  145. tigrbl/response/__init__.py +33 -0
  146. tigrbl/response/bind.py +12 -0
  147. tigrbl/response/decorators.py +37 -0
  148. tigrbl/response/resolver.py +83 -0
  149. tigrbl/response/shortcuts.py +171 -0
  150. tigrbl/response/types.py +49 -0
  151. tigrbl/rest/__init__.py +27 -0
  152. tigrbl/runtime/README.md +129 -0
  153. tigrbl/runtime/__init__.py +20 -0
  154. tigrbl/runtime/atoms/__init__.py +102 -0
  155. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  156. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  157. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  158. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  159. tigrbl/runtime/atoms/out/__init__.py +38 -0
  160. tigrbl/runtime/atoms/out/masking.py +135 -0
  161. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  162. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  163. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  164. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  165. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  166. tigrbl/runtime/atoms/response/__init__.py +19 -0
  167. tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
  168. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  169. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  170. tigrbl/runtime/atoms/response/render.py +36 -0
  171. tigrbl/runtime/atoms/response/renderer.py +116 -0
  172. tigrbl/runtime/atoms/response/template.py +44 -0
  173. tigrbl/runtime/atoms/response/templates.py +88 -0
  174. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  175. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  176. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  177. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  178. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  179. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  180. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  181. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  182. tigrbl/runtime/atoms/wire/dump.py +206 -0
  183. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  184. tigrbl/runtime/context.py +206 -0
  185. tigrbl/runtime/errors/__init__.py +61 -0
  186. tigrbl/runtime/errors/converters.py +214 -0
  187. tigrbl/runtime/errors/exceptions.py +124 -0
  188. tigrbl/runtime/errors/mappings.py +71 -0
  189. tigrbl/runtime/errors/utils.py +150 -0
  190. tigrbl/runtime/events.py +209 -0
  191. tigrbl/runtime/executor/__init__.py +6 -0
  192. tigrbl/runtime/executor/guards.py +132 -0
  193. tigrbl/runtime/executor/helpers.py +88 -0
  194. tigrbl/runtime/executor/invoke.py +150 -0
  195. tigrbl/runtime/executor/types.py +84 -0
  196. tigrbl/runtime/kernel.py +644 -0
  197. tigrbl/runtime/labels.py +353 -0
  198. tigrbl/runtime/opview.py +89 -0
  199. tigrbl/runtime/ordering.py +256 -0
  200. tigrbl/runtime/system.py +279 -0
  201. tigrbl/runtime/trace.py +330 -0
  202. tigrbl/schema/__init__.py +38 -0
  203. tigrbl/schema/_schema.py +27 -0
  204. tigrbl/schema/builder/__init__.py +17 -0
  205. tigrbl/schema/builder/build_schema.py +209 -0
  206. tigrbl/schema/builder/cache.py +24 -0
  207. tigrbl/schema/builder/compat.py +16 -0
  208. tigrbl/schema/builder/extras.py +85 -0
  209. tigrbl/schema/builder/helpers.py +51 -0
  210. tigrbl/schema/builder/list_params.py +117 -0
  211. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  212. tigrbl/schema/collect.py +79 -0
  213. tigrbl/schema/decorators.py +68 -0
  214. tigrbl/schema/get_schema.py +86 -0
  215. tigrbl/schema/schema_spec.py +20 -0
  216. tigrbl/schema/shortcuts.py +42 -0
  217. tigrbl/schema/types.py +34 -0
  218. tigrbl/schema/utils.py +143 -0
  219. tigrbl/session/README.md +14 -0
  220. tigrbl/session/__init__.py +28 -0
  221. tigrbl/session/abc.py +76 -0
  222. tigrbl/session/base.py +151 -0
  223. tigrbl/session/decorators.py +43 -0
  224. tigrbl/session/default.py +118 -0
  225. tigrbl/session/shortcuts.py +50 -0
  226. tigrbl/session/spec.py +112 -0
  227. tigrbl/shortcuts.py +22 -0
  228. tigrbl/specs.py +44 -0
  229. tigrbl/system/__init__.py +13 -0
  230. tigrbl/system/diagnostics/__init__.py +24 -0
  231. tigrbl/system/diagnostics/compat.py +31 -0
  232. tigrbl/system/diagnostics/healthz.py +41 -0
  233. tigrbl/system/diagnostics/hookz.py +51 -0
  234. tigrbl/system/diagnostics/kernelz.py +20 -0
  235. tigrbl/system/diagnostics/methodz.py +43 -0
  236. tigrbl/system/diagnostics/router.py +73 -0
  237. tigrbl/system/diagnostics/utils.py +43 -0
  238. tigrbl/system/uvicorn.py +60 -0
  239. tigrbl/table/__init__.py +9 -0
  240. tigrbl/table/_base.py +260 -0
  241. tigrbl/table/_table.py +54 -0
  242. tigrbl/table/mro_collect.py +69 -0
  243. tigrbl/table/shortcuts.py +57 -0
  244. tigrbl/table/table_spec.py +28 -0
  245. tigrbl/transport/__init__.py +74 -0
  246. tigrbl/transport/jsonrpc/__init__.py +19 -0
  247. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  248. tigrbl/transport/jsonrpc/helpers.py +115 -0
  249. tigrbl/transport/jsonrpc/models.py +41 -0
  250. tigrbl/transport/rest/__init__.py +25 -0
  251. tigrbl/transport/rest/aggregator.py +132 -0
  252. tigrbl/types/__init__.py +170 -0
  253. tigrbl/types/allow_anon_provider.py +19 -0
  254. tigrbl/types/authn_abc.py +30 -0
  255. tigrbl/types/nested_path_provider.py +22 -0
  256. tigrbl/types/op.py +35 -0
  257. tigrbl/types/op_config_provider.py +17 -0
  258. tigrbl/types/op_verb_alias_provider.py +33 -0
  259. tigrbl/types/request_extras_provider.py +22 -0
  260. tigrbl/types/response_extras_provider.py +22 -0
  261. tigrbl/types/table_config_provider.py +13 -0
  262. tigrbl/types/uuid.py +55 -0
  263. tigrbl-0.3.0.dist-info/METADATA +516 -0
  264. tigrbl-0.3.0.dist-info/RECORD +266 -0
  265. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
  266. tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
  267. tigrbl/ExampleAgent.py +0 -1
  268. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  269. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
@@ -0,0 +1,224 @@
1
+ # tigrbl/tigrbl/v3/engine/resolver.py
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import inspect
6
+ import logging
7
+ import threading
8
+ from typing import Any, Callable, Optional
9
+
10
+ from ._engine import AsyncSession, Engine, Provider, Session
11
+ from .engine_spec import EngineSpec, EngineCfg
12
+
13
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
14
+ logger = logging.getLogger("uvicorn")
15
+
16
+ # Registry with strict precedence: op > model > api > app
17
+ _LOCK = threading.RLock()
18
+ _DEFAULT: Optional[Provider] = None
19
+ _API: dict[int, Provider] = {}
20
+ _TAB: dict[Any, Provider] = {}
21
+ _OP: dict[tuple[Any, str], Provider] = {}
22
+
23
+
24
+ def _with_class(obj: Any) -> list[Any]:
25
+ """Return ``obj`` and its class when ``obj`` is an instance.
26
+
27
+ This allows resolution to honor providers registered on classes even when
28
+ an instance is supplied at lookup time.
29
+ """
30
+ return [obj] if isinstance(obj, type) else [obj, type(obj)]
31
+
32
+
33
+ def _coerce(ctx: Optional[EngineCfg]) -> Optional[Provider]:
34
+ """
35
+ Promote an @engine_ctx value to a lazy Provider.
36
+ """
37
+ logger.debug("_coerce called with ctx=%r", ctx)
38
+ if ctx is None:
39
+ logger.debug("_coerce: ctx is None")
40
+ return None
41
+ if isinstance(ctx, Provider):
42
+ logger.debug("_coerce: ctx is already a Provider")
43
+ return ctx
44
+ if isinstance(ctx, Engine):
45
+ logger.debug("_coerce: ctx is an Engine; returning provider")
46
+ return ctx.provider
47
+ if isinstance(ctx, EngineSpec):
48
+ logger.debug("_coerce: ctx is an EngineSpec; converting to provider")
49
+ return ctx.to_provider()
50
+ spec = EngineSpec.from_any(ctx)
51
+ logger.debug("_coerce: EngineSpec.from_any returned %r", spec)
52
+ return spec.to_provider() if spec else None
53
+
54
+
55
+ # ---- registration -----------------------------------------------------------
56
+
57
+
58
+ def set_default(ctx: EngineCfg | None) -> None:
59
+ """
60
+ Register the app-level default Provider used when no API/table/op binds.
61
+ """
62
+ global _DEFAULT
63
+ prov = _coerce(ctx)
64
+ logger.debug("set_default: setting default provider to %r", prov)
65
+ with _LOCK:
66
+ _DEFAULT = prov
67
+
68
+
69
+ def register_api(api: Any, ctx: EngineCfg | None) -> None:
70
+ """
71
+ Register an API-level Provider.
72
+ """
73
+ prov = _coerce(ctx)
74
+ logger.debug("register_api: api=%r coerced provider=%r", api, prov)
75
+ if prov is None:
76
+ logger.debug("register_api: no provider; skipping registration")
77
+ return
78
+ with _LOCK:
79
+ _API[id(api)] = prov
80
+ logger.debug("register_api: registered provider for api id %s", id(api))
81
+
82
+
83
+ def register_table(model: Any, ctx: EngineCfg | None) -> None:
84
+ """
85
+ Register a table/model-level Provider.
86
+ """
87
+ prov = _coerce(ctx)
88
+ logger.debug("register_table: model=%r coerced provider=%r", model, prov)
89
+ if prov is None:
90
+ logger.debug("register_table: no provider; skipping registration")
91
+ return
92
+ with _LOCK:
93
+ _TAB[model] = prov
94
+ logger.debug("register_table: registered provider for model %r", model)
95
+
96
+
97
+ def register_op(model: Any, alias: str, ctx: EngineCfg | None) -> None:
98
+ """
99
+ Register an op-level Provider for (model, alias).
100
+ """
101
+ prov = _coerce(ctx)
102
+ logger.debug(
103
+ "register_op: model=%r alias=%r coerced provider=%r", model, alias, prov
104
+ )
105
+ if prov is None:
106
+ logger.debug("register_op: no provider; skipping registration")
107
+ return
108
+ with _LOCK:
109
+ _OP[(model, alias)] = prov
110
+ logger.debug(
111
+ "register_op: registered provider for model %r alias %s", model, alias
112
+ )
113
+
114
+
115
+ # ---- resolution -------------------------------------------------------------
116
+
117
+
118
+ def resolve_provider(
119
+ *,
120
+ api: Any = None,
121
+ model: Any = None,
122
+ op_alias: str | None = None,
123
+ ) -> Optional[Provider]:
124
+ """
125
+ Resolve the effective Provider using precedence:
126
+ op > model > api > app(default)
127
+ """
128
+ logger.debug(
129
+ "resolve_provider called with api=%r model=%r op_alias=%r",
130
+ api,
131
+ model,
132
+ op_alias,
133
+ )
134
+ with _LOCK:
135
+ if model is not None and op_alias is not None:
136
+ logger.debug("resolve_provider: checking op-level provider")
137
+ for m in _with_class(model):
138
+ logger.debug(
139
+ "resolve_provider: looking for op provider for %r alias %s",
140
+ m,
141
+ op_alias,
142
+ )
143
+ p = _OP.get((m, op_alias))
144
+ if p:
145
+ logger.debug("resolve_provider: found op-level provider %r", p)
146
+ return p
147
+ if model is not None:
148
+ logger.debug("resolve_provider: checking model-level provider")
149
+ for m in _with_class(model):
150
+ logger.debug("resolve_provider: looking for model provider %r", m)
151
+ p = _TAB.get(m)
152
+ if p:
153
+ logger.debug("resolve_provider: found model-level provider %r", p)
154
+ return p
155
+ if api is not None:
156
+ logger.debug("resolve_provider: checking api-level provider")
157
+ for a in _with_class(api):
158
+ logger.debug("resolve_provider: looking for api provider %r", a)
159
+ # APIs are keyed by ``id`` to avoid relying on ``__hash__``
160
+ p = _API.get(id(a))
161
+ if p:
162
+ logger.debug("resolve_provider: found api-level provider %r", p)
163
+ return p
164
+ logger.debug("resolve_provider: returning default provider %r", _DEFAULT)
165
+ return _DEFAULT
166
+
167
+
168
+ SessionT = Session | AsyncSession
169
+
170
+
171
+ def acquire(
172
+ *,
173
+ api: Any = None,
174
+ model: Any = None,
175
+ op_alias: str | None = None,
176
+ ) -> tuple[SessionT, Callable[[], None]]:
177
+ """
178
+ Acquire a DB session from the resolved Provider.
179
+
180
+ Returns:
181
+ (session, release_fn)
182
+
183
+ Raises:
184
+ RuntimeError: if no Provider can be resolved and no default is set.
185
+ """
186
+ logger.debug(
187
+ "acquire called with api=%r model=%r op_alias=%r", api, model, op_alias
188
+ )
189
+ p = resolve_provider(api=api, model=model, op_alias=op_alias)
190
+ if p is None:
191
+ logger.debug("acquire: no provider resolved; raising error")
192
+ raise RuntimeError(
193
+ f"No database provider configured for op={op_alias} "
194
+ f"model={getattr(model, '__name__', model)} "
195
+ f"api={type(api).__name__ if api else None} and no default"
196
+ )
197
+ db: SessionT = p.session()
198
+ logger.debug("acquire: session %r acquired from provider %r", db, p)
199
+
200
+ def _release() -> None:
201
+ logger.debug("_release: attempting to release session %r", db)
202
+ close = getattr(db, "close", None)
203
+ if callable(close):
204
+ try:
205
+ rv = close()
206
+ logger.debug("_release: close returned %r", rv)
207
+ if inspect.isawaitable(rv):
208
+ logger.debug("_release: awaiting asynchronous close")
209
+ try:
210
+ loop = asyncio.get_running_loop()
211
+ except RuntimeError:
212
+ logger.debug("_release: no running loop; using asyncio.run")
213
+ asyncio.run(rv)
214
+ else:
215
+ logger.debug("_release: scheduling close on running loop")
216
+ loop.create_task(rv)
217
+ # If close is sync, it has already executed
218
+ except Exception:
219
+ logger.debug("_release: error during close", exc_info=True)
220
+ # best-effort close; swallow to avoid masking handler errors
221
+ pass
222
+ logger.debug("_release: release complete for session %r", db)
223
+
224
+ return db, _release
@@ -0,0 +1,216 @@
1
+ # tigrbl/tigrbl/v3/engine/shortcuts.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, Union
5
+
6
+ from .engine_spec import EngineSpec
7
+ from ._engine import Provider, Engine
8
+
9
+ EngineCfg = Union[str, Mapping[str, object]] # DSN string or structured mapping
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # EngineSpec / Provider / Engine helpers (ctx builder collapsed into
14
+ # engine_spec)
15
+ # ---------------------------------------------------------------------------
16
+
17
+
18
+ def engine_spec(
19
+ spec: Union[EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
20
+ ) -> EngineSpec:
21
+ """Build an :class:`EngineSpec` from a DSN string, mapping, or keyword fields."""
22
+ if spec is None and kw:
23
+ # Inline the former ctx builder behavior (no double wrap)
24
+ dsn: Optional[str] = kw.get("dsn")
25
+ if dsn:
26
+ spec = dsn
27
+ else:
28
+ kind = kw.get("kind")
29
+ if not kind:
30
+ raise ValueError(
31
+ "Provide spec=<DSN|mapping> or kind=('sqlite'|'postgres') with appropriate fields"
32
+ )
33
+
34
+ async_kw = kw.get("async_")
35
+ if async_kw is None:
36
+ async_kw = kw.get("async")
37
+
38
+ if kind == "sqlite":
39
+ path = kw.get("path")
40
+ mode = kw.get("mode")
41
+ memory_flag = kw.get("memory")
42
+ memory = (
43
+ (mode == "memory")
44
+ or (memory_flag is True)
45
+ or (not path and mode != "file")
46
+ )
47
+ async_default = True if async_kw is None and memory else False
48
+ async_ = bool(async_kw) if async_kw is not None else async_default
49
+ if memory:
50
+ spec = {"kind": "sqlite", "async": async_, "mode": "memory"}
51
+ else:
52
+ if not path:
53
+ raise ValueError("sqlite file requires 'path'")
54
+ spec = {"kind": "sqlite", "async": async_, "path": path}
55
+
56
+ elif kind == "postgres":
57
+ async_ = bool(async_kw) if async_kw is not None else False
58
+ spec = {
59
+ "kind": "postgres",
60
+ "async": async_,
61
+ "user": kw.get("user", "app"),
62
+ "pwd": kw.get("pwd", "secret"),
63
+ "host": kw.get("host", "localhost"),
64
+ "port": kw.get("port", 5432),
65
+ "db": kw.get("name", kw.get("db", "app_db")),
66
+ "pool_size": kw.get("pool_size", 10),
67
+ "max": kw.get("max", 20),
68
+ }
69
+ else:
70
+ raise ValueError("kind must be 'sqlite' or 'postgres'")
71
+
72
+ return EngineSpec.from_any(spec)
73
+
74
+
75
+ def prov(
76
+ spec: Union[EngineSpec, EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
77
+ ) -> Provider:
78
+ """
79
+ Get a lazy Provider (engine+sessionmaker).
80
+ Accepts EngineSpec, EngineCfg (mapping/DSN), or kw fields (collapsed former ctxS).
81
+ """
82
+ if isinstance(spec, EngineSpec):
83
+ return spec.to_provider()
84
+ return engine_spec(spec, **kw).to_provider()
85
+
86
+
87
+ def engine(
88
+ spec: Union[EngineSpec, EngineCfg, Mapping[str, Any], str, None] = None, **kw: Any
89
+ ) -> Engine:
90
+ """Return an Engine façade for convenience in ad-hoc flows."""
91
+ if isinstance(spec, EngineSpec):
92
+ return Engine(spec)
93
+ return Engine(engine_spec(spec, **kw))
94
+
95
+
96
+ # ---------------------------------------------------------------------------
97
+ # Convenience helpers (construct EngineCfg mappings directly; no ctxS needed)
98
+ # ---------------------------------------------------------------------------
99
+
100
+
101
+ def sqlite_cfg(
102
+ path: Optional[str] = None, *, async_: bool = True, memory: Optional[bool] = None
103
+ ) -> EngineCfg:
104
+ return (
105
+ {"kind": "sqlite", "async": async_, "mode": "memory"}
106
+ if (memory or path is None)
107
+ else {"kind": "sqlite", "async": async_, "path": path}
108
+ )
109
+
110
+
111
+ def pg_cfg(
112
+ *,
113
+ async_: bool = False,
114
+ user: str = "app",
115
+ pwd: str = "secret",
116
+ host: str = "localhost",
117
+ port: int = 5432,
118
+ name: str = "app_db",
119
+ pool_size: int = 10,
120
+ max: int = 20,
121
+ ) -> EngineCfg:
122
+ return {
123
+ "kind": "postgres",
124
+ "async": async_,
125
+ "user": user,
126
+ "pwd": pwd,
127
+ "host": host,
128
+ "port": port,
129
+ "db": name,
130
+ "pool_size": pool_size,
131
+ "max": max,
132
+ }
133
+
134
+
135
+ def mem(async_: bool = True) -> EngineCfg:
136
+ """SQLite in-memory (StaticPool) EngineCfg mapping."""
137
+ return {"kind": "sqlite", "async": async_, "mode": "memory"}
138
+
139
+
140
+ def sqlitef(path: str, *, async_: bool = False) -> EngineCfg:
141
+ """SQLite file EngineCfg mapping."""
142
+ return {"kind": "sqlite", "async": async_, "path": path}
143
+
144
+
145
+ def pg(**kw: Any) -> EngineCfg:
146
+ """Postgres EngineCfg; set async_=True for asyncpg."""
147
+ return pg_cfg(**kw)
148
+
149
+
150
+ def pga(**kw: Any) -> EngineCfg:
151
+ """Async Postgres EngineCfg (asyncpg)."""
152
+ kw.setdefault("async_", True)
153
+ return pg_cfg(**kw)
154
+
155
+
156
+ def pgs(**kw: Any) -> EngineCfg:
157
+ """Sync Postgres EngineCfg (psycopg/pg8000 depending on your builders)."""
158
+ kw.setdefault("async_", False)
159
+ return pg_cfg(**kw)
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Provider one-liners
164
+ # ---------------------------------------------------------------------------
165
+
166
+
167
+ def provider_sqlite_memory(async_: bool = True) -> Provider:
168
+ return engine_spec(kind="sqlite", mode="memory", async_=async_).to_provider()
169
+
170
+
171
+ def provider_sqlite_file(path: str, async_: bool = False) -> Provider:
172
+ return engine_spec(kind="sqlite", path=path, async_=async_).to_provider()
173
+
174
+
175
+ def provider_postgres(
176
+ *,
177
+ async_: bool = False,
178
+ user: str = "app",
179
+ pwd: str = "secret",
180
+ host: str = "localhost",
181
+ port: int = 5432,
182
+ name: str = "app_db",
183
+ pool_size: int = 10,
184
+ max: int = 20,
185
+ ) -> Provider:
186
+ return engine_spec(
187
+ kind="postgres",
188
+ async_=async_,
189
+ user=user,
190
+ pwd=pwd,
191
+ host=host,
192
+ port=port,
193
+ name=name,
194
+ pool_size=pool_size,
195
+ max=max,
196
+ ).to_provider()
197
+
198
+
199
+ __all__ = [
200
+ # EngineSpec / Provider / Engine helpers
201
+ "engine_spec",
202
+ "prov",
203
+ "engine",
204
+ # convenience EngineCfg helpers
205
+ "sqlite_cfg",
206
+ "pg_cfg",
207
+ "mem",
208
+ "sqlitef",
209
+ "pg",
210
+ "pga",
211
+ "pgs",
212
+ # direct providers
213
+ "provider_sqlite_memory",
214
+ "provider_sqlite_file",
215
+ "provider_postgres",
216
+ ]
@@ -0,0 +1,21 @@
1
+ from ..config.constants import HOOK_DECLS_ATTR
2
+ from .decorators import hook_ctx
3
+ from .types import PHASE, HookPhase, PHASES, Ctx, StepFn, HookPredicate
4
+ from .shortcuts import hook, hook_spec
5
+ from ._hook import Hook
6
+ from .hook_spec import HookSpec
7
+
8
+ __all__ = [
9
+ "hook_ctx",
10
+ "HOOK_DECLS_ATTR",
11
+ "Hook",
12
+ "PHASE",
13
+ "HookPhase",
14
+ "PHASES",
15
+ "Ctx",
16
+ "StepFn",
17
+ "HookPredicate",
18
+ "hook",
19
+ "hook_spec",
20
+ "HookSpec",
21
+ ]
tigrbl/hook/_hook.py ADDED
@@ -0,0 +1,22 @@
1
+ """Runtime hook wrapper for Tigrbl v3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable, Optional, Union
7
+
8
+ from .types import HookPhase, StepFn
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class Hook:
13
+ """Concrete hook bound to a phase and one or more ops."""
14
+
15
+ phase: HookPhase
16
+ fn: StepFn
17
+ ops: Union[str, Iterable[str]]
18
+ name: Optional[str] = None
19
+ description: Optional[str] = None
20
+
21
+
22
+ __all__ = ["Hook"]
@@ -0,0 +1,28 @@
1
+ """Hook-related decorators for Tigrbl v3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable, Union
6
+
7
+ from ..config.constants import HOOK_DECLS_ATTR
8
+ from ._hook import Hook
9
+
10
+
11
+ def hook_ctx(ops: Union[str, Iterable[str]], *, phase: str):
12
+ """Declare a ctx-only hook for one/many ops at a given phase."""
13
+
14
+ def deco(fn):
15
+ from ..op.decorators import _ensure_cm, _unwrap
16
+
17
+ cm = _ensure_cm(fn)
18
+ f = _unwrap(cm)
19
+ f.__tigrbl_ctx_only__ = True
20
+ lst = getattr(f, HOOK_DECLS_ATTR, [])
21
+ lst.append(Hook(phase=phase, fn=f, ops=ops))
22
+ setattr(f, HOOK_DECLS_ATTR, lst)
23
+ return cm
24
+
25
+ return deco
26
+
27
+
28
+ __all__ = ["hook_ctx", "HOOK_DECLS_ATTR"]
@@ -0,0 +1,24 @@
1
+ """Hook specification for Tigrbl v3."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .types import HookPhase, StepFn, HookPredicate
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class HookSpec:
13
+ phase: HookPhase
14
+ fn: StepFn
15
+ order: int = 0
16
+ when: Optional[HookPredicate] = None
17
+ name: Optional[str] = None
18
+ description: Optional[str] = None
19
+
20
+
21
+ # Backwards compatibility alias
22
+ OpHook = HookSpec
23
+
24
+ __all__ = ["HookSpec", "OpHook"]
@@ -0,0 +1,98 @@
1
+ """Helpers for collecting ctx-only hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from functools import lru_cache
7
+ from typing import Any, Callable, Dict, Iterable, Union
8
+
9
+ from ..runtime.executor import _Ctx
10
+ from ..op.collect import apply_alias
11
+ from ..op.mro_collect import mro_alias_map_for
12
+ from ..op.decorators import _maybe_await, _unwrap
13
+ from .decorators import HOOK_DECLS_ATTR, Hook
14
+
15
+ logger = logging.getLogger("uvicorn")
16
+
17
+
18
+ def _phase_io_key(phase: str) -> str | None:
19
+ p = str(phase)
20
+ if p.startswith("PRE_"):
21
+ return "payload"
22
+ if p.startswith("POST_"):
23
+ return "result"
24
+ if p.startswith("ON_"):
25
+ return "error"
26
+ return None
27
+
28
+
29
+ def _wrap_ctx_hook(
30
+ table: type, func: Callable[..., Any], phase: str
31
+ ) -> Callable[..., Any]:
32
+ io_key = _phase_io_key(phase)
33
+
34
+ async def hook(
35
+ value=None, *, db=None, request=None, ctx: Dict[str, Any] | None = None
36
+ ):
37
+ ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
38
+ if io_key is not None and value is not None:
39
+ ctx[io_key] = value
40
+ bound = func.__get__(table, table)
41
+ _ = await _maybe_await(bound(ctx))
42
+ if io_key is None:
43
+ return None
44
+ return ctx.get(io_key, value)
45
+
46
+ hook.__name__ = getattr(func, "__name__", "hook")
47
+ hook.__qualname__ = getattr(func, "__qualname__", hook.__name__)
48
+ return hook
49
+
50
+
51
+ @lru_cache(maxsize=None)
52
+ def _mro_collect_decorated_hooks_cached(
53
+ table: type, visible_aliases_fs: frozenset[str]
54
+ ) -> Dict[str, Dict[str, list[Callable[..., Any]]]]:
55
+ """Cached helper for :func:`mro_collect_decorated_hooks`."""
56
+
57
+ visible_aliases = set(visible_aliases_fs)
58
+ logger.info("Collecting hooks for %s", table.__name__)
59
+ mapping: Dict[str, Dict[str, list[Callable[..., Any]]]] = {}
60
+ aliases = mro_alias_map_for(table)
61
+
62
+ def _resolve_ops(spec: Union[str, Iterable[str]]) -> Iterable[str]:
63
+ if spec == "*":
64
+ return visible_aliases
65
+ if isinstance(spec, str):
66
+ return [spec if spec in visible_aliases else apply_alias(spec, aliases)]
67
+ out: list[str] = []
68
+ for x in spec:
69
+ out.append(x if x in visible_aliases else apply_alias(x, aliases))
70
+ return out
71
+
72
+ for base in reversed(table.__mro__):
73
+ for name, attr in base.__dict__.items():
74
+ func = _unwrap(attr)
75
+ decls: list[Hook] | None = getattr(func, HOOK_DECLS_ATTR, None)
76
+ if not decls:
77
+ continue
78
+ for d in decls:
79
+ for op in _resolve_ops(d.ops):
80
+ if op not in visible_aliases:
81
+ continue
82
+ ph = d.phase
83
+ mapping.setdefault(op, {}).setdefault(ph, []).append(
84
+ _wrap_ctx_hook(table, d.fn, ph)
85
+ )
86
+ logger.debug("Collected hooks for aliases: %s", list(mapping.keys()))
87
+ return mapping
88
+
89
+
90
+ def mro_collect_decorated_hooks(
91
+ table: type, *, visible_aliases: set[str]
92
+ ) -> Dict[str, Dict[str, list[Callable[..., Any]]]]:
93
+ """Collect alias→phase→[hook] declarations across a table's MRO."""
94
+
95
+ return _mro_collect_decorated_hooks_cached(table, frozenset(visible_aliases))
96
+
97
+
98
+ __all__ = ["mro_collect_decorated_hooks"]
@@ -0,0 +1,44 @@
1
+ """Shortcut helpers for building Hook specs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Iterable, Union
6
+
7
+ from .types import HookPhase, HookPredicate, StepFn
8
+ from ._hook import Hook
9
+ from .hook_spec import HookSpec
10
+
11
+
12
+ def hook(
13
+ phase: HookPhase,
14
+ ops: Union[str, Iterable[str]],
15
+ fn: StepFn,
16
+ *,
17
+ name: str | None = None,
18
+ description: str | None = None,
19
+ ) -> Hook:
20
+ """Build a :class:`Hook` instance."""
21
+ return Hook(phase=phase, fn=fn, ops=ops, name=name, description=description)
22
+
23
+
24
+ def hook_spec(
25
+ phase: HookPhase,
26
+ fn: StepFn,
27
+ *,
28
+ order: int = 0,
29
+ when: HookPredicate | None = None,
30
+ name: str | None = None,
31
+ description: str | None = None,
32
+ ) -> HookSpec:
33
+ """Build a :class:`HookSpec` instance."""
34
+ return HookSpec(
35
+ phase=phase,
36
+ fn=fn,
37
+ order=order,
38
+ when=when,
39
+ name=name,
40
+ description=description,
41
+ )
42
+
43
+
44
+ __all__ = ["hook", "hook_spec"]