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,209 @@
1
+ # tigrbl/v3/runtime/events.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Iterable, List, Literal, Tuple
6
+
7
+ # ──────────────────────────────────────────────────────────────────────────────
8
+ # Phases
9
+ # - PRE_TX is a synthetic phase for security/dependency checks.
10
+ # - START_TX / END_TX are reserved for system steps (no atom anchors there).
11
+ # - Atoms bind only to the event anchors below.
12
+ # ──────────────────────────────────────────────────────────────────────────────
13
+
14
+ Phase = Literal[
15
+ "PRE_TX",
16
+ "START_TX",
17
+ "PRE_HANDLER",
18
+ "HANDLER",
19
+ "POST_HANDLER",
20
+ "END_TX",
21
+ "POST_RESPONSE",
22
+ ]
23
+
24
+ PHASES: Tuple[Phase, ...] = (
25
+ "PRE_TX",
26
+ "START_TX", # system-only
27
+ "PRE_HANDLER",
28
+ "HANDLER",
29
+ "POST_HANDLER",
30
+ "END_TX", # system-only
31
+ "POST_RESPONSE",
32
+ )
33
+
34
+ # ──────────────────────────────────────────────────────────────────────────────
35
+ # Canonical anchors (events) — the only moments atoms can bind to
36
+ # Keep these names stable; labels use them directly: step_kind:domain:subject@ANCHOR
37
+ # ──────────────────────────────────────────────────────────────────────────────
38
+
39
+ # PRE_HANDLER
40
+ SCHEMA_COLLECT_IN = "schema:collect_in"
41
+ IN_VALIDATE = "in:validate"
42
+
43
+ # HANDLER
44
+ RESOLVE_VALUES = "resolve:values"
45
+ PRE_FLUSH = "pre:flush"
46
+ EMIT_ALIASES_PRE = "emit:aliases:pre_flush"
47
+
48
+ # POST_HANDLER
49
+ POST_FLUSH = "post:flush"
50
+ EMIT_ALIASES_POST = "emit:aliases:post_refresh"
51
+ SCHEMA_COLLECT_OUT = "schema:collect_out"
52
+ OUT_BUILD = "out:build"
53
+
54
+ # POST_RESPONSE
55
+ EMIT_ALIASES_READ = "emit:aliases:readtime"
56
+ OUT_DUMP = "out:dump"
57
+
58
+ # Canonical order of event anchors within the request lifecycle.
59
+ # This ordering is global and stable; use it to produce deterministic plans/traces.
60
+ _EVENT_ORDER: Tuple[str, ...] = (
61
+ # PRE_HANDLER
62
+ SCHEMA_COLLECT_IN,
63
+ IN_VALIDATE,
64
+ # HANDLER
65
+ RESOLVE_VALUES,
66
+ PRE_FLUSH,
67
+ EMIT_ALIASES_PRE,
68
+ # POST_HANDLER
69
+ POST_FLUSH,
70
+ EMIT_ALIASES_POST,
71
+ SCHEMA_COLLECT_OUT,
72
+ OUT_BUILD,
73
+ # POST_RESPONSE
74
+ EMIT_ALIASES_READ,
75
+ OUT_DUMP,
76
+ )
77
+
78
+
79
+ # Map each anchor to its phase and persistence tie.
80
+ # "persist_tied=True" means the anchor is pruned for non-persisting ops
81
+ # (e.g., read/list) and whenever an op is executed with persist=False.
82
+ @dataclass(frozen=True)
83
+ class AnchorInfo:
84
+ name: str
85
+ phase: Phase
86
+ ordinal: int
87
+ persist_tied: bool
88
+
89
+
90
+ _ANCHORS: Dict[str, AnchorInfo] = {
91
+ # PRE_HANDLER (not persist-tied)
92
+ SCHEMA_COLLECT_IN: AnchorInfo(SCHEMA_COLLECT_IN, "PRE_HANDLER", 0, False),
93
+ IN_VALIDATE: AnchorInfo(IN_VALIDATE, "PRE_HANDLER", 1, False),
94
+ RESOLVE_VALUES: AnchorInfo(RESOLVE_VALUES, "PRE_HANDLER", 2, True),
95
+ PRE_FLUSH: AnchorInfo(PRE_FLUSH, "PRE_HANDLER", 3, True),
96
+ EMIT_ALIASES_PRE: AnchorInfo(EMIT_ALIASES_PRE, "PRE_HANDLER", 4, True),
97
+ # POST_HANDLER (mixed)
98
+ POST_FLUSH: AnchorInfo(POST_FLUSH, "POST_HANDLER", 5, True),
99
+ EMIT_ALIASES_POST: AnchorInfo(EMIT_ALIASES_POST, "POST_HANDLER", 6, True),
100
+ SCHEMA_COLLECT_OUT: AnchorInfo(SCHEMA_COLLECT_OUT, "POST_HANDLER", 7, False),
101
+ OUT_BUILD: AnchorInfo(OUT_BUILD, "POST_HANDLER", 8, False),
102
+ # POST_RESPONSE (not persist-tied)
103
+ EMIT_ALIASES_READ: AnchorInfo(EMIT_ALIASES_READ, "POST_RESPONSE", 9, False),
104
+ OUT_DUMP: AnchorInfo(OUT_DUMP, "POST_RESPONSE", 10, False),
105
+ }
106
+
107
+ # ──────────────────────────────────────────────────────────────────────────────
108
+ # Public helpers
109
+ # ──────────────────────────────────────────────────────────────────────────────
110
+
111
+
112
+ def is_valid_event(anchor: str) -> bool:
113
+ """True if the given anchor is one of the canonical events."""
114
+ return anchor in _ANCHORS
115
+
116
+
117
+ def phase_for_event(anchor: str) -> Phase:
118
+ """Return the Phase for a canonical event; raises on unknown anchors."""
119
+ try:
120
+ return _ANCHORS[anchor].phase
121
+ except KeyError as e:
122
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
123
+
124
+
125
+ def is_persist_tied(anchor: str) -> bool:
126
+ """Return True if this event is pruned for non-persisting ops."""
127
+ try:
128
+ return _ANCHORS[anchor].persist_tied
129
+ except KeyError as e:
130
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
131
+
132
+
133
+ def get_anchor_info(anchor: str) -> AnchorInfo:
134
+ """Return the full :class:`AnchorInfo` for a canonical event."""
135
+ try:
136
+ return _ANCHORS[anchor]
137
+ except KeyError as e:
138
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
139
+
140
+
141
+ def all_events_ordered() -> List[str]:
142
+ """Return all canonical events in deterministic, lifecycle order."""
143
+ return list(_EVENT_ORDER)
144
+
145
+
146
+ def events_for_phase(phase: Phase) -> List[str]:
147
+ """Return the subset of events that belong to the given phase."""
148
+ if phase not in PHASES:
149
+ raise ValueError(f"Unknown phase: {phase!r}")
150
+ return [a for a, info in _ANCHORS.items() if info.phase == phase]
151
+
152
+
153
+ def prune_events_for_persist(anchors: Iterable[str], *, persist: bool) -> List[str]:
154
+ """
155
+ Given a sequence of anchors, return a new list with persistence-tied events
156
+ removed when persist=False. Unknown anchors raise ValueError.
157
+ """
158
+ out: List[str] = []
159
+ for a in anchors:
160
+ if not is_valid_event(a):
161
+ raise ValueError(f"Unknown event anchor: {a!r}")
162
+ if not persist and _ANCHORS[a].persist_tied:
163
+ continue
164
+ out.append(a)
165
+ # keep canonical order irrespective of input order
166
+ out.sort(key=lambda x: _ANCHORS[x].ordinal)
167
+ return out
168
+
169
+
170
+ def order_events(anchors: Iterable[str]) -> List[str]:
171
+ """
172
+ Sort a set/list of anchors into canonical lifecycle order.
173
+ Raises on unknown anchors.
174
+ """
175
+ anchors = list(anchors)
176
+ for a in anchors:
177
+ if a not in _ANCHORS:
178
+ raise ValueError(f"Unknown event anchor: {a!r}")
179
+ anchors.sort(key=lambda x: _ANCHORS[x].ordinal)
180
+ return anchors
181
+
182
+
183
+ __all__ = [
184
+ # Phases
185
+ "Phase",
186
+ "PHASES",
187
+ # Anchors (constants)
188
+ "SCHEMA_COLLECT_IN",
189
+ "IN_VALIDATE",
190
+ "RESOLVE_VALUES",
191
+ "PRE_FLUSH",
192
+ "EMIT_ALIASES_PRE",
193
+ "POST_FLUSH",
194
+ "EMIT_ALIASES_POST",
195
+ "SCHEMA_COLLECT_OUT",
196
+ "OUT_BUILD",
197
+ "EMIT_ALIASES_READ",
198
+ "OUT_DUMP",
199
+ # Types / helpers
200
+ "AnchorInfo",
201
+ "is_valid_event",
202
+ "phase_for_event",
203
+ "is_persist_tied",
204
+ "get_anchor_info",
205
+ "all_events_ordered",
206
+ "events_for_phase",
207
+ "prune_events_for_persist",
208
+ "order_events",
209
+ ]
@@ -0,0 +1,6 @@
1
+ # tigrbl/v3/runtime/executor/__init__.py
2
+ from .types import _Ctx, HandlerStep, PhaseChains
3
+ from .helpers import _in_tx
4
+ from .invoke import _invoke
5
+
6
+ __all__ = ["_Ctx", "_invoke", "_in_tx", "HandlerStep", "PhaseChains"]
@@ -0,0 +1,132 @@
1
+ """Database session guard utilities for the runtime executor.
2
+
3
+ This module temporarily replaces ``commit`` and ``flush`` on SQLAlchemy
4
+ sessions to enforce phase-specific policies. Each guard returns a handle that
5
+ restores the original methods once the phase completes and provides helpers to
6
+ rollback when the runtime owns the transaction.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Optional, Union
13
+
14
+ try:
15
+ from sqlalchemy.orm import Session # type: ignore
16
+ from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
17
+ except Exception: # pragma: no cover
18
+ Session = Any # type: ignore
19
+ AsyncSession = Any # type: ignore
20
+
21
+ from .types import _Ctx, PhaseChains
22
+ from .helpers import _is_async_db, _run_chain, _g
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class _GuardHandle:
28
+ """Stores original ``commit``/``flush`` methods for later restoration."""
29
+
30
+ __slots__ = ("db", "orig_commit", "orig_flush")
31
+
32
+ def __init__(self, db: Any, orig_commit: Any, orig_flush: Any) -> None:
33
+ self.db = db
34
+ self.orig_commit = orig_commit
35
+ self.orig_flush = orig_flush
36
+
37
+ def restore(self) -> None:
38
+ if self.orig_commit is not None:
39
+ try:
40
+ setattr(self.db, "commit", self.orig_commit)
41
+ except Exception:
42
+ pass # pragma: no cover
43
+ if self.orig_flush is not None:
44
+ try:
45
+ setattr(self.db, "flush", self.orig_flush)
46
+ except Exception:
47
+ pass # pragma: no cover
48
+
49
+
50
+ def _install_db_guards(
51
+ db: Union[Session, AsyncSession, None],
52
+ *,
53
+ phase: str,
54
+ allow_flush: bool,
55
+ allow_commit: bool,
56
+ require_owned_tx_for_commit: bool,
57
+ owns_tx: bool,
58
+ ) -> _GuardHandle:
59
+ """Install guards that restrict ``commit``/``flush`` during a phase.
60
+
61
+ Parameters:
62
+ db: SQLAlchemy ``Session``/``AsyncSession`` to guard.
63
+ phase: Name of the executing phase for error messages.
64
+ allow_flush: Whether ``flush`` should be permitted.
65
+ allow_commit: Whether ``commit`` should be permitted.
66
+ require_owned_tx_for_commit: Block commits if the executor did not
67
+ open the transaction.
68
+ owns_tx: Whether the runtime opened the transaction.
69
+
70
+ Returns:
71
+ A ``_GuardHandle`` for restoring original methods.
72
+ """
73
+ if db is None:
74
+ return _GuardHandle(None, None, None)
75
+ orig_commit = getattr(db, "commit", None)
76
+ orig_flush = getattr(db, "flush", None)
77
+
78
+ def _raise(op: str) -> None:
79
+ raise RuntimeError(f"db.{op}() is not allowed during {phase} phase")
80
+
81
+ if not allow_commit or (require_owned_tx_for_commit and not owns_tx):
82
+ if _is_async_db(db):
83
+
84
+ async def _blocked_commit() -> None: # type: ignore[func-returns-value]
85
+ _raise("commit")
86
+ else:
87
+
88
+ def _blocked_commit() -> None: # type: ignore[func-returns-value]
89
+ _raise("commit")
90
+
91
+ setattr(db, "commit", _blocked_commit) # type: ignore[assignment]
92
+
93
+ if not allow_flush:
94
+ if _is_async_db(db):
95
+
96
+ async def _blocked_flush() -> None: # type: ignore[func-returns-value]
97
+ _raise("flush")
98
+ else:
99
+
100
+ def _blocked_flush() -> None: # type: ignore[func-returns-value]
101
+ _raise("flush")
102
+
103
+ setattr(db, "flush", _blocked_flush) # type: ignore[assignment]
104
+
105
+ return _GuardHandle(db, orig_commit, orig_flush)
106
+
107
+
108
+ async def _rollback_if_owned(
109
+ db: Union[Session, AsyncSession, None],
110
+ owns_tx: bool,
111
+ *,
112
+ phases: Optional[PhaseChains],
113
+ ctx: _Ctx,
114
+ ) -> None:
115
+ """Rollback the session if this runtime owns the transaction."""
116
+
117
+ if not owns_tx or db is None:
118
+ return
119
+ try:
120
+ if _is_async_db(db):
121
+ await db.rollback() # type: ignore[func-returns-value]
122
+ else:
123
+ db.rollback()
124
+ except Exception as rb_exc: # pragma: no cover
125
+ logger.exception("Rollback failed: %s", rb_exc)
126
+ try:
127
+ await _run_chain(ctx, _g(phases, "ON_ROLLBACK"), phase="ON_ROLLBACK")
128
+ except Exception: # pragma: no cover
129
+ pass
130
+
131
+
132
+ __all__ = ["_GuardHandle", "_install_db_guards", "_rollback_if_owned"]
@@ -0,0 +1,88 @@
1
+ # tigrbl/v3/runtime/executor/helpers.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import logging
6
+ from typing import Any, Iterable, Optional, Sequence
7
+
8
+ try:
9
+ from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
10
+ except Exception: # pragma: no cover
11
+ AsyncSession = Any # type: ignore
12
+
13
+ try:
14
+ from .. import trace as _trace # type: ignore
15
+ except Exception: # pragma: no cover
16
+ _trace = None # type: ignore
17
+
18
+ from .types import _Ctx, HandlerStep, PhaseChains
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _is_async_db(db: Any) -> bool:
24
+ """Detect DB interfaces that require `await` for transactional methods."""
25
+ if isinstance(db, AsyncSession) or hasattr(db, "run_sync"):
26
+ return True
27
+ for attr in ("commit", "begin", "rollback", "flush"):
28
+ if inspect.iscoroutinefunction(getattr(db, attr, None)):
29
+ return True
30
+ return False
31
+
32
+
33
+ def _bool_call(meth: Any) -> bool:
34
+ try:
35
+ return bool(meth())
36
+ except Exception: # pragma: no cover
37
+ return False
38
+
39
+
40
+ def _in_tx(db: Any) -> bool:
41
+ for name in ("in_transaction", "in_nested_transaction"):
42
+ attr = getattr(db, name, None)
43
+ if callable(attr):
44
+ if _bool_call(attr):
45
+ return True
46
+ elif attr:
47
+ return True
48
+ return False
49
+
50
+
51
+ async def _maybe_await(v: Any) -> Any:
52
+ if inspect.isawaitable(v):
53
+ return await v # type: ignore[func-returns-value]
54
+ return v
55
+
56
+
57
+ async def _run_chain(
58
+ ctx: _Ctx, chain: Optional[Iterable[HandlerStep]], *, phase: str
59
+ ) -> None:
60
+ if not chain:
61
+ return
62
+ if _trace is not None:
63
+ with _trace.span(ctx, f"phase:{phase}"):
64
+ for step in chain:
65
+ rv = step(ctx)
66
+ rv = await _maybe_await(rv)
67
+ if rv is not None:
68
+ ctx.result = rv
69
+ return
70
+ for step in chain:
71
+ rv = step(ctx)
72
+ rv = await _maybe_await(rv)
73
+ if rv is not None:
74
+ ctx.result = rv
75
+
76
+
77
+ def _g(phases: Optional[PhaseChains], key: str) -> Sequence[HandlerStep]:
78
+ return () if not phases else phases.get(key, ())
79
+
80
+
81
+ __all__ = [
82
+ "_is_async_db",
83
+ "_bool_call",
84
+ "_in_tx",
85
+ "_maybe_await",
86
+ "_run_chain",
87
+ "_g",
88
+ ]
@@ -0,0 +1,150 @@
1
+ # tigrbl/v3/runtime/executor/invoke.py
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, MutableMapping, Optional, Union
6
+
7
+ from .types import _Ctx, PhaseChains, Request, Session, AsyncSession
8
+ from .helpers import _in_tx, _run_chain, _g
9
+ from .guards import _install_db_guards, _rollback_if_owned
10
+ from ..errors import create_standardized_error
11
+ from ...config.constants import CTX_SKIP_PERSIST_FLAG
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def _invoke(
17
+ *,
18
+ request: Optional[Request],
19
+ db: Union[Session, AsyncSession, None],
20
+ phases: Optional[PhaseChains],
21
+ ctx: Optional[MutableMapping[str, Any]] = None,
22
+ ) -> Any:
23
+ """Execute an operation through explicit phases with strict write policies."""
24
+
25
+ ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
26
+ if getattr(ctx, "app", None) is None and getattr(ctx, "api", None) is not None:
27
+ ctx.app = ctx.api
28
+ if getattr(ctx, "op", None) is None and getattr(ctx, "method", None) is not None:
29
+ ctx.op = ctx.method
30
+ if getattr(ctx, "model", None) is None:
31
+ obj = getattr(ctx, "obj", None)
32
+ if obj is not None:
33
+ ctx.model = type(obj)
34
+ skip_persist: bool = bool(ctx.get(CTX_SKIP_PERSIST_FLAG) or ctx.get("skip_persist"))
35
+
36
+ existed_tx_before = _in_tx(db) if db is not None else False
37
+
38
+ async def _run_phase(
39
+ name: str,
40
+ *,
41
+ allow_flush: bool,
42
+ allow_commit: bool,
43
+ in_tx: bool,
44
+ require_owned_for_commit: bool = True,
45
+ nonfatal: bool = False,
46
+ owns_tx_for_phase: Optional[bool] = None,
47
+ ) -> None:
48
+ chain = _g(phases, name)
49
+ if not chain:
50
+ return
51
+
52
+ eff_allow_flush = allow_flush and (not skip_persist)
53
+ eff_allow_commit = allow_commit and (not skip_persist)
54
+
55
+ owns_tx_now = bool(owns_tx_for_phase)
56
+ if owns_tx_for_phase is None:
57
+ owns_tx_now = not existed_tx_before
58
+
59
+ guard = _install_db_guards(
60
+ db,
61
+ phase=name,
62
+ allow_flush=eff_allow_flush,
63
+ allow_commit=eff_allow_commit,
64
+ require_owned_tx_for_commit=require_owned_for_commit,
65
+ owns_tx=owns_tx_now,
66
+ )
67
+
68
+ try:
69
+ await _run_chain(ctx, chain, phase=name)
70
+ except Exception as exc:
71
+ ctx.error = exc
72
+ if in_tx:
73
+ await _rollback_if_owned(db, owns_tx_now, phases=phases, ctx=ctx)
74
+ err_name = f"ON_{name}_ERROR"
75
+ try:
76
+ await _run_chain(
77
+ ctx, _g(phases, err_name) or _g(phases, "ON_ERROR"), phase=err_name
78
+ )
79
+ except Exception: # pragma: no cover
80
+ pass
81
+ if nonfatal:
82
+ logger.exception("%s failed (nonfatal): %s", name, exc)
83
+ return
84
+ raise create_standardized_error(exc)
85
+ finally:
86
+ guard.restore()
87
+
88
+ await _run_phase("PRE_TX_BEGIN", allow_flush=False, allow_commit=False, in_tx=False)
89
+
90
+ if not skip_persist:
91
+ await _run_phase(
92
+ "START_TX",
93
+ allow_flush=False,
94
+ allow_commit=False,
95
+ in_tx=False,
96
+ require_owned_for_commit=True,
97
+ )
98
+
99
+ await _run_phase(
100
+ "PRE_HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
101
+ )
102
+
103
+ await _run_phase(
104
+ "HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
105
+ )
106
+
107
+ await _run_phase(
108
+ "POST_HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
109
+ )
110
+
111
+ await _run_phase(
112
+ "PRE_COMMIT", allow_flush=False, allow_commit=False, in_tx=not skip_persist
113
+ )
114
+
115
+ if not skip_persist:
116
+ owns_tx_for_commit = (not existed_tx_before) and (db is not None and _in_tx(db))
117
+ await _run_phase(
118
+ "END_TX",
119
+ allow_flush=True,
120
+ allow_commit=True,
121
+ in_tx=True,
122
+ require_owned_for_commit=True,
123
+ owns_tx_for_phase=owns_tx_for_commit,
124
+ )
125
+
126
+ from types import SimpleNamespace as _NS
127
+
128
+ serializer = ctx.get("response_serializer")
129
+ if callable(serializer):
130
+ try:
131
+ ctx["result"] = serializer(ctx.get("result"))
132
+ except Exception:
133
+ logger.exception("response serialization failed", exc_info=True)
134
+ ctx.response = _NS(result=ctx.get("result"))
135
+
136
+ await _run_phase("POST_COMMIT", allow_flush=True, allow_commit=False, in_tx=False)
137
+
138
+ await _run_phase(
139
+ "POST_RESPONSE",
140
+ allow_flush=False,
141
+ allow_commit=False,
142
+ in_tx=False,
143
+ nonfatal=True,
144
+ )
145
+ if ctx.get("result") is not None:
146
+ ctx.response.result = ctx.get("result")
147
+ return ctx.response.result
148
+
149
+
150
+ __all__ = ["_invoke"]
@@ -0,0 +1,84 @@
1
+ # tigrbl/v3/runtime/executor/types.py
2
+ from __future__ import annotations
3
+
4
+ from typing import (
5
+ Any,
6
+ Awaitable,
7
+ Callable,
8
+ Mapping,
9
+ MutableMapping,
10
+ Optional,
11
+ Sequence,
12
+ Union,
13
+ Protocol,
14
+ runtime_checkable,
15
+ )
16
+
17
+ try:
18
+ from fastapi import Request # type: ignore
19
+ except Exception: # pragma: no cover
20
+ Request = Any # type: ignore
21
+
22
+ try:
23
+ from sqlalchemy.orm import Session # type: ignore
24
+ from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
25
+ except Exception: # pragma: no cover
26
+ Session = Any # type: ignore
27
+ AsyncSession = Any # type: ignore
28
+
29
+
30
+ @runtime_checkable
31
+ class _Step(Protocol):
32
+ def __call__(self, ctx: "_Ctx") -> Union[Any, Awaitable[Any]]: ...
33
+
34
+
35
+ HandlerStep = Union[
36
+ _Step,
37
+ Callable[["_Ctx"], Any],
38
+ Callable[["_Ctx"], Awaitable[Any]],
39
+ ]
40
+ PhaseChains = Mapping[str, Sequence[HandlerStep]]
41
+
42
+
43
+ class _Ctx(dict):
44
+ """Dict-like context with attribute access.
45
+
46
+ Common keys:
47
+ • request: FastAPI Request (optional)
48
+ • db: Session | AsyncSession
49
+ • api/model/op: optional metadata
50
+ • result: last non-None step result
51
+ • error: last exception caught (on failure paths)
52
+ • response: SimpleNamespace(result=...) for POST_RESPONSE shaping
53
+ • temp: scratch dict used by atoms/hook steps
54
+ """
55
+
56
+ __slots__ = ()
57
+ __getattr__ = dict.get # type: ignore[assignment]
58
+ __setattr__ = dict.__setitem__ # type: ignore[assignment]
59
+
60
+ @classmethod
61
+ def ensure(
62
+ cls,
63
+ *,
64
+ request: Optional[Request],
65
+ db: Union[Session, AsyncSession, None],
66
+ seed: Optional[MutableMapping[str, Any]] = None,
67
+ ) -> "_Ctx":
68
+ ctx = cls() if seed is None else (seed if isinstance(seed, _Ctx) else cls(seed))
69
+ if request is not None:
70
+ ctx.request = request
71
+ state = getattr(request, "state", None)
72
+ if state is not None and getattr(state, "ctx", None) is None:
73
+ try:
74
+ state.ctx = ctx # make ctx available to deps
75
+ except Exception: # pragma: no cover
76
+ pass
77
+ if db is not None:
78
+ ctx.db = db
79
+ if "temp" not in ctx or not isinstance(ctx.get("temp"), dict):
80
+ ctx.temp = {}
81
+ return ctx
82
+
83
+
84
+ __all__ = ["_Ctx", "HandlerStep", "PhaseChains", "Request", "Session", "AsyncSession"]