tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__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 (252) 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 +72 -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 +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,276 @@
1
+ # tigrbl/v3/bindings/handlers/steps.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ import inspect
6
+ from functools import lru_cache
7
+ from typing import Any, Callable, Mapping, Optional
8
+
9
+ from ... import core as _core
10
+ from ...op import OpSpec
11
+ from ...op.types import StepFn
12
+ from ...runtime.executor import _Ctx
13
+ from .ctx import _ctx_db, _ctx_payload, _ctx_request
14
+ from .identifiers import _resolve_ident
15
+
16
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
17
+ logger = logging.getLogger("uvicorn")
18
+ logger.debug("Loaded module v3/bindings/handlers/steps")
19
+
20
+
21
+ async def _call_list_core(
22
+ fn: Callable[..., Any],
23
+ model: type,
24
+ payload: Mapping[str, Any],
25
+ ctx: Mapping[str, Any],
26
+ ):
27
+ filters = dict(payload) if isinstance(payload, Mapping) else {}
28
+ skip = filters.pop("skip", None)
29
+ limit = filters.pop("limit", None)
30
+ filters_arg = filters if filters else None
31
+
32
+ db = _ctx_db(ctx)
33
+ req = _ctx_request(ctx)
34
+
35
+ candidates: list[tuple[tuple, dict]] = []
36
+
37
+ def add_candidate(
38
+ use_pos_filters: bool, use_pos_db: bool, with_req: bool, with_pag: bool
39
+ ):
40
+ args: tuple = ()
41
+ kwargs: dict = {}
42
+ if use_pos_filters:
43
+ args += (filters_arg,)
44
+ else:
45
+ kwargs["filters"] = filters_arg
46
+ if use_pos_db:
47
+ args += (db,)
48
+ else:
49
+ kwargs["db"] = db
50
+ if with_req and req is not None:
51
+ kwargs["request"] = req
52
+ if with_pag:
53
+ if skip is not None:
54
+ kwargs["skip"] = skip
55
+ if limit is not None:
56
+ kwargs["limit"] = limit
57
+ candidates.append((args, kwargs))
58
+
59
+ add_candidate(False, False, True, True)
60
+ add_candidate(True, False, True, True)
61
+ add_candidate(True, True, True, True)
62
+
63
+ add_candidate(False, False, False, True)
64
+ add_candidate(True, False, False, True)
65
+ add_candidate(True, True, False, True)
66
+
67
+ add_candidate(False, False, True, False)
68
+ add_candidate(True, False, True, False)
69
+ add_candidate(True, True, True, False)
70
+
71
+ add_candidate(False, False, False, False)
72
+ add_candidate(True, False, False, False)
73
+ add_candidate(True, True, False, False)
74
+
75
+ last_err: Optional[BaseException] = None
76
+ for args, kwargs in candidates:
77
+ try:
78
+ logger.debug("Trying list core call with args=%s kwargs=%s", args, kwargs)
79
+ rv = fn(model, *args, **kwargs)
80
+ if inspect.isawaitable(rv):
81
+ logger.debug("Awaiting async result for list core")
82
+ return await rv
83
+ return rv
84
+ except TypeError as e:
85
+ logger.debug("Candidate failed with TypeError: %s", e)
86
+ last_err = e
87
+ continue
88
+ if last_err:
89
+ logger.debug("Reraising last TypeError from list core resolution")
90
+ raise last_err
91
+ raise RuntimeError("list() call resolution failed unexpectedly")
92
+
93
+
94
+ def _accepted_kw(handler: Callable[..., Any]) -> set[str]:
95
+ try:
96
+ sig = inspect.signature(handler)
97
+ except Exception as exc:
98
+ logger.debug("Failed to inspect handler %r: %s", handler, exc)
99
+ return {"ctx"}
100
+
101
+ names: set[str] = set()
102
+ for p in sig.parameters.values():
103
+ if p.kind in (p.VAR_KEYWORD, p.VAR_POSITIONAL):
104
+ logger.debug("Handler %r accepts arbitrary params", handler)
105
+ return {"ctx", "db", "payload", "request", "model", "op", "spec", "alias"}
106
+ names.add(p.name)
107
+ logger.debug("Handler %r accepts keywords %s", handler, names)
108
+ return names
109
+
110
+
111
+ def _wrap_custom(model: type, sp: OpSpec, user_handler: Callable[..., Any]) -> StepFn:
112
+ async def step(ctx: Any) -> Any:
113
+ db = _ctx_db(ctx)
114
+ payload = _ctx_payload(ctx)
115
+ request = _ctx_request(ctx)
116
+ isolated = _Ctx.ensure(request=request, db=db, seed=ctx)
117
+ bound = getattr(model, getattr(user_handler, "__name__", ""), user_handler)
118
+ wanted = _accepted_kw(bound)
119
+
120
+ kw = {}
121
+ if "ctx" in wanted:
122
+ kw["ctx"] = isolated
123
+ if "db" in wanted:
124
+ kw["db"] = db
125
+ if "payload" in wanted:
126
+ kw["payload"] = payload
127
+ if "request" in wanted:
128
+ kw["request"] = request
129
+ if "model" in wanted:
130
+ kw["model"] = model
131
+ if "op" in wanted:
132
+ kw["op"] = sp
133
+ if "spec" in wanted:
134
+ kw["spec"] = sp
135
+ if "alias" in wanted:
136
+ kw["alias"] = sp.alias
137
+ logger.debug("Calling custom handler %r with kw=%s", bound, kw)
138
+ rv = bound(**kw) # type: ignore[misc]
139
+ if inspect.isawaitable(rv):
140
+ logger.debug("Awaiting async custom handler")
141
+ return await rv
142
+ return rv
143
+
144
+ step.__name__ = getattr(user_handler, "__name__", step.__name__)
145
+ step.__qualname__ = getattr(user_handler, "__qualname__", step.__name__)
146
+ step.__module__ = getattr(user_handler, "__module__", step.__module__)
147
+ return step
148
+
149
+
150
+ @lru_cache(maxsize=None)
151
+ def _wrap_core(model: type, target: str) -> StepFn:
152
+ logger.debug("Creating core wrapper for %s.%s", model.__name__, target)
153
+
154
+ async def create_step(ctx: Any) -> Any:
155
+ db = _ctx_db(ctx)
156
+ payload = _ctx_payload(ctx)
157
+ logger.debug("Dispatching to core.create")
158
+ return await _core.create(model, payload, db=db)
159
+
160
+ async def read_step(ctx: Any) -> Any:
161
+ db = _ctx_db(ctx)
162
+ ident = _resolve_ident(model, ctx)
163
+ logger.debug("Dispatching to core.read with ident=%r", ident)
164
+ return await _core.read(model, ident, db=db)
165
+
166
+ async def update_step(ctx: Any) -> Any:
167
+ db = _ctx_db(ctx)
168
+ ident = _resolve_ident(model, ctx)
169
+ payload = _ctx_payload(ctx)
170
+ logger.debug("Dispatching to core.update with ident=%r", ident)
171
+ return await _core.update(model, ident, payload, db=db)
172
+
173
+ async def replace_step(ctx: Any) -> Any:
174
+ db = _ctx_db(ctx)
175
+ ident = _resolve_ident(model, ctx)
176
+ payload = _ctx_payload(ctx)
177
+ logger.debug("Dispatching to core.replace with ident=%r", ident)
178
+ return await _core.replace(model, ident, payload, db=db)
179
+
180
+ async def merge_step(ctx: Any) -> Any:
181
+ db = _ctx_db(ctx)
182
+ ident = _resolve_ident(model, ctx)
183
+ payload = _ctx_payload(ctx)
184
+ logger.debug("Dispatching to core.merge with ident=%r", ident)
185
+ return await _core.merge(model, ident, payload, db=db)
186
+
187
+ async def delete_step(ctx: Any) -> Any:
188
+ db = _ctx_db(ctx)
189
+ ident = _resolve_ident(model, ctx)
190
+ logger.debug("Dispatching to core.delete with ident=%r", ident)
191
+ return await _core.delete(model, ident, db=db)
192
+
193
+ async def list_step(ctx: Any) -> Any:
194
+ payload = _ctx_payload(ctx)
195
+ logger.debug("Dispatching to core.list")
196
+ return await _call_list_core(_core.list, model, payload, ctx)
197
+
198
+ async def clear_step(ctx: Any) -> Any:
199
+ db = _ctx_db(ctx)
200
+ logger.debug("Dispatching to core.clear")
201
+ return await _core.clear(model, {}, db=db)
202
+
203
+ async def bulk_create_step(ctx: Any) -> Any:
204
+ db = _ctx_db(ctx)
205
+ payload = _ctx_payload(ctx)
206
+ logger.debug("Dispatching to core.bulk_create")
207
+ if not isinstance(payload, list):
208
+ raise TypeError("bulk_create expects a list payload")
209
+ return await _core.bulk_create(model, payload, db=db)
210
+
211
+ async def bulk_update_step(ctx: Any) -> Any:
212
+ db = _ctx_db(ctx)
213
+ payload = _ctx_payload(ctx)
214
+ logger.debug("Dispatching to core.bulk_update")
215
+ if not isinstance(payload, list):
216
+ raise TypeError("bulk_update expects a list payload")
217
+ return await _core.bulk_update(model, payload, db=db)
218
+
219
+ async def bulk_replace_step(ctx: Any) -> Any:
220
+ db = _ctx_db(ctx)
221
+ payload = _ctx_payload(ctx)
222
+ logger.debug("Dispatching to core.bulk_replace")
223
+ if not isinstance(payload, list):
224
+ raise TypeError("bulk_replace expects a list payload")
225
+ return await _core.bulk_replace(model, payload, db=db)
226
+
227
+ async def bulk_merge_step(ctx: Any) -> Any:
228
+ db = _ctx_db(ctx)
229
+ payload = _ctx_payload(ctx)
230
+ logger.debug("Dispatching to core.bulk_merge")
231
+ if not isinstance(payload, list):
232
+ raise TypeError("bulk_merge expects a list payload")
233
+ return await _core.bulk_merge(model, payload, db=db)
234
+
235
+ async def bulk_delete_step(ctx: Any) -> Any:
236
+ db = _ctx_db(ctx)
237
+ payload = _ctx_payload(ctx)
238
+ logger.debug("Dispatching to core.bulk_delete")
239
+ ids = payload.get("ids") if isinstance(payload, Mapping) else []
240
+ return await _core.bulk_delete(model, ids, db=db)
241
+
242
+ async def default_step(ctx: Any) -> Any:
243
+ logger.debug("No core operation matched; returning payload")
244
+ return _ctx_payload(ctx)
245
+
246
+ steps: dict[str, StepFn] = {
247
+ "create": create_step,
248
+ "read": read_step,
249
+ "update": update_step,
250
+ "replace": replace_step,
251
+ "merge": merge_step,
252
+ "delete": delete_step,
253
+ "list": list_step,
254
+ "clear": clear_step,
255
+ "bulk_create": bulk_create_step,
256
+ "bulk_update": bulk_update_step,
257
+ "bulk_replace": bulk_replace_step,
258
+ "bulk_merge": bulk_merge_step,
259
+ "bulk_delete": bulk_delete_step,
260
+ }
261
+
262
+ step = steps.get(target, default_step)
263
+
264
+ fn = getattr(_core, target, None)
265
+ step.__name__ = getattr(fn, "__name__", step.__name__)
266
+ step.__qualname__ = getattr(fn, "__qualname__", step.__name__)
267
+ step.__module__ = getattr(fn, "__module__", step.__module__)
268
+ return step
269
+
270
+
271
+ __all__ = [
272
+ "_call_list_core",
273
+ "_accepted_kw",
274
+ "_wrap_custom",
275
+ "_wrap_core",
276
+ ]
@@ -0,0 +1,311 @@
1
+ # tigrbl/v3/bindings/hooks.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import logging
6
+ from types import SimpleNamespace
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Dict,
11
+ Iterable,
12
+ List,
13
+ Mapping,
14
+ Optional,
15
+ Sequence,
16
+ Tuple,
17
+ )
18
+
19
+ from ..op import OpSpec
20
+ from ..hook import HookSpec
21
+ from ..hook.types import PHASES, StepFn
22
+ from ..config.constants import (
23
+ TIGRBL_API_HOOKS_ATTR,
24
+ TIGRBL_HOOKS_ATTR,
25
+ CTX_SKIP_PERSIST_FLAG,
26
+ )
27
+
28
+ logger = logging.getLogger("uvicorn")
29
+ logger.debug("Loaded module v3/bindings/hooks")
30
+
31
+ _Key = Tuple[str, str] # (alias, target)
32
+
33
+ # ───────────────────────────────────────────────────────────────────────────────
34
+ # Phase groupings (v2-compatible precedence)
35
+ # pre-like: API → MODEL → OP
36
+ # post/error: OP → MODEL → API
37
+ # ───────────────────────────────────────────────────────────────────────────────
38
+
39
+ _PRE_LIKE = frozenset({"PRE_TX_BEGIN", "START_TX", "PRE_HANDLER", "PRE_COMMIT"})
40
+ _POST_LIKE = frozenset({"POST_HANDLER", "POST_COMMIT", "POST_RESPONSE", "FINAL"})
41
+ _ERROR_LIKE = frozenset(
42
+ {
43
+ "ON_ROLLBACK",
44
+ "ON_PRE_HANDLER_ERROR",
45
+ "ON_HANDLER_ERROR",
46
+ "ON_POST_HANDLER_ERROR",
47
+ "ON_PRE_COMMIT_ERROR",
48
+ # v3 uses END_TX; map v2's ON_COMMIT_ERROR → ON_END_TX_ERROR
49
+ "ON_END_TX_ERROR",
50
+ "ON_POST_COMMIT_ERROR",
51
+ "ON_POST_RESPONSE_ERROR",
52
+ "ON_ERROR",
53
+ }
54
+ )
55
+
56
+
57
+ def _is_pre_like(p: str) -> bool:
58
+ return p in _PRE_LIKE
59
+
60
+
61
+ def _is_post_or_error(p: str) -> bool:
62
+ return p in _POST_LIKE or p in _ERROR_LIKE
63
+
64
+
65
+ # ───────────────────────────────────────────────────────────────────────────────
66
+ # ctx helpers
67
+ # ───────────────────────────────────────────────────────────────────────────────
68
+
69
+
70
+ def _ctx_get(ctx: Mapping[str, Any], key: str, default: Any = None) -> Any:
71
+ try:
72
+ return ctx[key]
73
+ except Exception:
74
+ return getattr(ctx, key, default)
75
+
76
+
77
+ def _ctx_db(ctx: Mapping[str, Any]) -> Any:
78
+ return _ctx_get(ctx, "db")
79
+
80
+
81
+ def _ctx_payload(ctx: Mapping[str, Any]) -> Mapping[str, Any]:
82
+ v = _ctx_get(ctx, "payload", None)
83
+ return v if v is not None else {}
84
+
85
+
86
+ # ───────────────────────────────────────────────────────────────────────────────
87
+ # System step helpers
88
+ # ───────────────────────────────────────────────────────────────────────────────
89
+
90
+
91
+ def _mark_skip_persist() -> StepFn:
92
+ async def _step(ctx: Any) -> None:
93
+ try:
94
+ ctx[CTX_SKIP_PERSIST_FLAG] = True
95
+ except Exception:
96
+ setattr(ctx, CTX_SKIP_PERSIST_FLAG, True)
97
+
98
+ _step.__name__ = "mark_skip_persist"
99
+ _step.__qualname__ = "mark_skip_persist"
100
+ return _step
101
+
102
+
103
+ # ───────────────────────────────────────────────────────────────────────────────
104
+ # Step wrappers
105
+ # ───────────────────────────────────────────────────────────────────────────────
106
+
107
+
108
+ def _wrap_hook(h: HookSpec) -> StepFn:
109
+ fn = h.fn
110
+ pred = h.when
111
+
112
+ async def _step(ctx: Any) -> Any:
113
+ if pred is not None:
114
+ payload = _ctx_payload(ctx)
115
+
116
+ # Evaluate predicate without ever boolean-testing SQLAlchemy clauses.
117
+ def _as_bool(val: object) -> bool:
118
+ if isinstance(val, bool):
119
+ return val
120
+ try:
121
+ return bool(val)
122
+ except TypeError:
123
+ # e.g., SQLAlchemy ClauseElement: no boolean value → treat as pass
124
+ return True
125
+
126
+ try:
127
+ res = pred(payload)
128
+ except TypeError:
129
+ # Signature mismatch? Try with ctx.
130
+ try:
131
+ res = pred(ctx) # type: ignore[misc]
132
+ except Exception:
133
+ res = True
134
+ except Exception:
135
+ res = True
136
+ if not _as_bool(res):
137
+ return None
138
+ # Pass the context explicitly as a keyword so wrapped hooks expecting a
139
+ # ``ctx`` parameter receive the correct seed. Positional invocation
140
+ # treated the context as the first positional argument (often the
141
+ # ``value`` parameter), resulting in a new empty context being created
142
+ # inside the wrapper and missing executor-provided keys like
143
+ # ``response``.
144
+ rv = fn(ctx=ctx)
145
+ if inspect.isawaitable(rv):
146
+ return await rv
147
+ return rv
148
+
149
+ _step.__name__ = getattr(fn, "__name__", _step.__name__)
150
+ _step.__qualname__ = getattr(fn, "__qualname__", _step.__name__)
151
+ return _step
152
+
153
+
154
+ def _wrap_step_fn(fn: Callable[..., Any]) -> StepFn:
155
+ async def _step(ctx: Any) -> Any:
156
+ # Similar to :func:`_wrap_hook`, pass the context as a keyword argument to
157
+ # support wrappers produced by ``@hook_ctx`` which expect ``ctx`` as a
158
+ # kw-only parameter.
159
+ rv = fn(ctx=ctx)
160
+ if inspect.isawaitable(rv):
161
+ return await rv
162
+ return rv
163
+
164
+ _step.__name__ = getattr(fn, "__name__", _step.__name__)
165
+ _step.__qualname__ = getattr(fn, "__qualname__", _step.__name__)
166
+ return _step
167
+
168
+
169
+ # ───────────────────────────────────────────────────────────────────────────────
170
+ # Source collection (API / MODEL / OP) for a single alias
171
+ # Accepted shapes for API/MODEL sources:
172
+ # • { phase: Iterable[callable] } (applies to all aliases)
173
+ # • { alias: { phase: Iterable[callable] } } (per-alias)
174
+ # ───────────────────────────────────────────────────────────────────────────────
175
+
176
+
177
+ def _to_phase_map_for_alias(source: Any, alias: str) -> Dict[str, List[StepFn]]:
178
+ """
179
+ Normalize a user-provided hooks source to { phase: [StepFn, ...], ... } for the given alias.
180
+ """
181
+ out: Dict[str, List[StepFn]] = {}
182
+ if not source:
183
+ return out
184
+
185
+ maybe = None
186
+ if isinstance(source, Mapping):
187
+ if alias in source:
188
+ maybe = source.get(alias)
189
+ elif "*" in source:
190
+ maybe = source.get("*")
191
+ else:
192
+ maybe = source # flat {phase: iterable}
193
+
194
+ if isinstance(maybe, Mapping):
195
+ for ph, items in (maybe or {}).items():
196
+ steps = out.setdefault(str(ph), [])
197
+ if isinstance(items, Iterable):
198
+ for fn in items:
199
+ if callable(fn):
200
+ steps.append(_wrap_step_fn(fn))
201
+ return out
202
+
203
+
204
+ # ───────────────────────────────────────────────────────────────────────────────
205
+ # Precedence merge (API/MODEL/OP only; no imperative source)
206
+ # ───────────────────────────────────────────────────────────────────────────────
207
+
208
+
209
+ def _merge_for_phase(
210
+ phase: str,
211
+ *,
212
+ api_map: Mapping[str, List[StepFn]] | None,
213
+ model_map: Mapping[str, List[StepFn]] | None,
214
+ op_map: Mapping[str, List[StepFn]] | None,
215
+ ) -> List[StepFn]:
216
+ """
217
+ Merge lists from sources for one phase:
218
+ • pre-like → API + MODEL + OP
219
+ • post/error→ OP + MODEL + API
220
+ """
221
+
222
+ def _get(m: Mapping[str, List[StepFn]] | None) -> List[StepFn]:
223
+ if not m:
224
+ return []
225
+ return list(m.get(phase, []) or [])
226
+
227
+ if _is_pre_like(phase):
228
+ return _get(api_map) + _get(model_map) + _get(op_map)
229
+ return _get(op_map) + _get(model_map) + _get(api_map)
230
+
231
+
232
+ # ───────────────────────────────────────────────────────────────────────────────
233
+ # Alias namespace helper
234
+ # ───────────────────────────────────────────────────────────────────────────────
235
+
236
+
237
+ def _ensure_alias_hooks_ns(model: type, alias: str) -> SimpleNamespace:
238
+ root = getattr(model, "hooks", None)
239
+ if root is None:
240
+ root = SimpleNamespace()
241
+ setattr(model, "hooks", root)
242
+ ns = getattr(root, alias, None)
243
+ if ns is None:
244
+ ns = SimpleNamespace()
245
+ setattr(root, alias, ns)
246
+ # Ensure all known phases exist as lists
247
+ for ph in PHASES:
248
+ if not hasattr(ns, ph):
249
+ setattr(ns, ph, [])
250
+ return ns
251
+
252
+
253
+ # ───────────────────────────────────────────────────────────────────────────────
254
+ # Build / attach (with precedence)
255
+ # ───────────────────────────────────────────────────────────────────────────────
256
+
257
+
258
+ def _attach_one(model: type, sp: OpSpec) -> None:
259
+ alias = sp.alias
260
+ ns = _ensure_alias_hooks_ns(model, alias)
261
+
262
+ # Reset existing chains for a clean rebuild
263
+ for ph in PHASES:
264
+ setattr(ns, ph, [])
265
+
266
+ # Resolve source maps for this alias
267
+ api_src = getattr(model, TIGRBL_API_HOOKS_ATTR, None)
268
+ model_src = getattr(model, TIGRBL_HOOKS_ATTR, None)
269
+
270
+ api_map = _to_phase_map_for_alias(api_src, alias)
271
+ model_map = _to_phase_map_for_alias(model_src, alias)
272
+
273
+ # Op-level (from OpSpec.hooks)
274
+ op_map: Dict[str, List[StepFn]] = {}
275
+ for h in sp.hooks or ():
276
+ phase = str(h.phase)
277
+ op_map.setdefault(phase, []).append(_wrap_hook(h))
278
+
279
+ # Build per-phase chains via precedence merge
280
+ for ph in PHASES:
281
+ merged = _merge_for_phase(
282
+ ph, api_map=api_map, model_map=model_map, op_map=op_map
283
+ )
284
+
285
+ # Ephemeral operations: mark skip in PRE_TX_BEGIN
286
+ if sp.persist == "skip" and ph == "PRE_TX_BEGIN":
287
+ merged = [_mark_skip_persist()] + merged
288
+
289
+ setattr(ns, ph, merged)
290
+
291
+ logger.debug("hooks: %s.%s merged (persist=%s)", model.__name__, alias, sp.persist)
292
+
293
+
294
+ def normalize_and_attach(
295
+ model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
296
+ ) -> None:
297
+ """
298
+ Build sequential phase chains for each OpSpec and attach them to model.hooks.<alias>.
299
+ Sources merged per phase (in precedence order):
300
+ • PRE-like: API → MODEL → OP
301
+ • POST/ERROR: OP → MODEL → API
302
+ """
303
+ wanted = set(only_keys or ())
304
+ for sp in specs:
305
+ key = (sp.alias, sp.target)
306
+ if wanted and key not in wanted:
307
+ continue
308
+ _attach_one(model, sp)
309
+
310
+
311
+ __all__ = ["normalize_and_attach"]