tigrbl-runtime 0.1.0.dev1__tar.gz → 0.1.0.dev6__tar.gz

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 (56) hide show
  1. {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/PKG-INFO +5 -1
  2. {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/pyproject.toml +9 -1
  3. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/config/__init__.py +3 -0
  4. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/config/constants.py +5 -0
  5. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/__init__.py +27 -0
  6. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/base.py +31 -0
  7. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/helpers.py +3 -0
  8. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/invoke.py +464 -0
  9. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/kernel_executor.py +29 -0
  10. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/numba_packed.py +79 -0
  11. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/packed.py +873 -0
  12. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/phase.py +37 -0
  13. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/types.py +300 -0
  14. {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/tigrbl_runtime/runtime/README.md +16 -19
  15. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/__init__.py +31 -0
  16. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/_typing_aliases.py +5 -0
  17. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/base.py +35 -0
  18. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/events.py +93 -0
  19. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/executor/__init__.py +6 -0
  20. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/executor/invoke.py +68 -0
  21. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/hook_types.py +7 -0
  22. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/kernel.py +27 -0
  23. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/labels.py +3 -0
  24. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/response.py +33 -0
  25. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/runtime.py +75 -0
  26. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/__init__.py +1 -0
  27. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/converters.py +1 -0
  28. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/exceptions.py +1 -0
  29. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/mappings.py +1 -0
  30. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/utils.py +1 -0
  31. tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/system.py +156 -0
  32. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/__init__.py +0 -20
  33. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/context.py +0 -206
  34. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/events.py +0 -435
  35. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/__init__.py +0 -6
  36. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/guards.py +0 -132
  37. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/helpers.py +0 -194
  38. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/invoke.py +0 -242
  39. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/types.py +0 -128
  40. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/__init__.py +0 -4
  41. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/invoke.py +0 -103
  42. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/raw.py +0 -26
  43. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/hook_types.py +0 -60
  44. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/kernel.py +0 -667
  45. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/labels.py +0 -369
  46. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/opview.py +0 -89
  47. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/ordering.py +0 -331
  48. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/__init__.py +0 -63
  49. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/converters.py +0 -222
  50. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/exceptions.py +0 -149
  51. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/mappings.py +0 -94
  52. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/utils.py +0 -114
  53. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/system.py +0 -338
  54. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/trace.py +0 -330
  55. {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/README.md +0 -0
  56. {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/tigrbl_runtime/runtime/exceptions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl-runtime
3
- Version: 0.1.0.dev1
3
+ Version: 0.1.0.dev6
4
4
  Summary: Runtime pipeline and executor components for Tigrbl.
5
5
  License-Expression: Apache-2.0
6
6
  Keywords: tigrbl,sdk,standards,framework
@@ -15,7 +15,11 @@ Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Programming Language :: Python :: 3 :: Only
18
+ Requires-Dist: numba (>=0.61.2)
19
+ Requires-Dist: tigrbl-atoms
20
+ Requires-Dist: tigrbl-concrete
18
21
  Requires-Dist: tigrbl-kernel
22
+ Requires-Dist: tigrbl-typing
19
23
  Description-Content-Type: text/markdown
20
24
 
21
25
  ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tigrbl-runtime"
3
- version = "0.1.0.dev1"
3
+ version = "0.1.0.dev6"
4
4
  description = "Runtime pipeline and executor components for Tigrbl."
5
5
  license = "Apache-2.0"
6
6
  readme = "README.md"
@@ -18,12 +18,19 @@ classifiers = [
18
18
  ]
19
19
  authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
20
20
  dependencies = [
21
+ "tigrbl-typing",
21
22
  "tigrbl-kernel",
23
+ "tigrbl-concrete",
24
+ "tigrbl-atoms",
25
+ "numba>=0.61.2",
22
26
  ]
23
27
  keywords = ["tigrbl", "sdk", "standards", "framework"]
24
28
 
25
29
  [tool.uv.sources]
30
+ "tigrbl-typing" = { workspace = true }
26
31
  "tigrbl-kernel" = { workspace = true }
32
+ "tigrbl-concrete" = { workspace = true }
33
+ "tigrbl-atoms" = { workspace = true }
27
34
 
28
35
  [build-system]
29
36
  requires = ["poetry-core>=1.0.0"]
@@ -38,5 +45,6 @@ packages = [
38
45
  [dependency-groups]
39
46
  dev = [
40
47
  "pytest>=8.0",
48
+ "pytest-asyncio>=0.23",
41
49
  "ruff>=0.9",
42
50
  ]
@@ -0,0 +1,3 @@
1
+ from .constants import CTX_SKIP_PERSIST_FLAG
2
+
3
+ __all__ = ["CTX_SKIP_PERSIST_FLAG"]
@@ -0,0 +1,5 @@
1
+ """Runtime config constants."""
2
+
3
+ CTX_SKIP_PERSIST_FLAG = "__tigrbl_skip_persist__"
4
+
5
+ __all__ = ["CTX_SKIP_PERSIST_FLAG"]
@@ -0,0 +1,27 @@
1
+ """Executor public API with lazy imports to avoid circular startup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import import_module
6
+ from typing import Any
7
+
8
+ _EXPORTS = {
9
+ "ExecutorBase": "base",
10
+ "PhaseExecutor": "phase",
11
+ "PackedPlanExecutor": "packed",
12
+ "NumbaPackedPlanExecutor": "numba_packed",
13
+ "_Ctx": "types",
14
+ "_invoke": "invoke",
15
+ }
16
+
17
+ __all__ = list(_EXPORTS)
18
+
19
+
20
+ def __getattr__(name: str) -> Any:
21
+ module_name = _EXPORTS.get(name)
22
+ if module_name is None:
23
+ raise AttributeError(name)
24
+ module = import_module(f"{__name__}.{module_name}")
25
+ value = getattr(module, name)
26
+ globals()[name] = value
27
+ return value
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, ClassVar
5
+
6
+
7
+ class ExecutorBase(ABC):
8
+ """Contract for runtime executors."""
9
+
10
+ name: ClassVar[str]
11
+
12
+ def __init__(self) -> None:
13
+ self.runtime: Any | None = None
14
+
15
+ def attach_runtime(self, runtime: Any) -> None:
16
+ self.runtime = runtime
17
+
18
+ @abstractmethod
19
+ async def invoke(
20
+ self,
21
+ *,
22
+ runtime: Any,
23
+ env: Any,
24
+ ctx: Any,
25
+ plan: Any,
26
+ packed_plan: Any | None = None,
27
+ ) -> Any:
28
+ """Execute a kernel plan or packed kernel plan."""
29
+
30
+
31
+ __all__ = ["ExecutorBase"]
@@ -0,0 +1,3 @@
1
+ from tigrbl_kernel.helpers import _maybe_await, _run_chain, _g
2
+
3
+ __all__ = ["_maybe_await", "_run_chain", "_g"]
@@ -0,0 +1,464 @@
1
+ # tigrbl/runtime/executor/invoke.py
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from types import SimpleNamespace
6
+ from typing import Any, Mapping, MutableMapping, Optional, Union
7
+
8
+ from .types import _Ctx, PhaseChains, Request, Session, AsyncSession
9
+ from tigrbl_kernel.helpers import _run_chain, _g
10
+ from tigrbl_atoms.atoms.sys._db import _in_transaction
11
+ from ..runtime.status import create_standardized_error, to_rpc_error_payload
12
+ from ..config.constants import CTX_SKIP_PERSIST_FLAG
13
+ from tigrbl_ops_oltp.crud import ops as _crud_ops
14
+ from tigrbl_ops_oltp.crud.helpers.model import _coerce_pk_value, _single_pk_name
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ _OPVIEW_CACHE_ATTR = "__tigrbl_cached_opviews__"
19
+ _LOG_NOISE_REDUCED = False
20
+ _NOISY_TIGRBL_LOGGERS = (
21
+ "tigrbl_ops_oltp.crud.helpers.model",
22
+ "tigrbl_core._spec.column_spec",
23
+ )
24
+
25
+
26
+ def _reduce_log_noise() -> None:
27
+ global _LOG_NOISE_REDUCED
28
+ if _LOG_NOISE_REDUCED:
29
+ return
30
+ _LOG_NOISE_REDUCED = True
31
+ for logger_name in _NOISY_TIGRBL_LOGGERS:
32
+ target_logger = logging.getLogger(logger_name)
33
+ if target_logger.getEffectiveLevel() <= logging.INFO:
34
+ target_logger.setLevel(logging.WARNING)
35
+
36
+
37
+ def _default_status_for_alias(alias: Any, target: Any = None) -> int:
38
+ verb = target if isinstance(target, str) and target else alias
39
+ return 201 if verb in {"create", "bulk_create"} else 200
40
+
41
+
42
+ def _normalize_result_payload(payload: Any) -> Any:
43
+ if (
44
+ isinstance(payload, (str, int, float, bool, bytes, bytearray))
45
+ or payload is None
46
+ ):
47
+ return payload
48
+ if hasattr(payload, "status_code") and hasattr(payload, "body"):
49
+ return payload
50
+ if isinstance(payload, Mapping):
51
+ return {str(k): _normalize_result_payload(v) for k, v in payload.items()}
52
+ if isinstance(payload, (list, tuple, set)):
53
+ return [_normalize_result_payload(v) for v in payload]
54
+
55
+ model_dump = getattr(payload, "model_dump", None)
56
+ if callable(model_dump):
57
+ try:
58
+ return _normalize_result_payload(model_dump())
59
+ except Exception:
60
+ pass
61
+
62
+ obj_dict = getattr(payload, "__dict__", None)
63
+ if isinstance(obj_dict, dict):
64
+ data = {
65
+ k: v
66
+ for k, v in obj_dict.items()
67
+ if not k.startswith("_") and not callable(v)
68
+ }
69
+ if data:
70
+ return _normalize_result_payload(data)
71
+
72
+ return str(payload)
73
+
74
+
75
+ def _unwrap_ctx_result(value: Any) -> Any:
76
+ """Return user-facing payload when runtime atoms return context objects."""
77
+ current = value
78
+ for _ in range(8):
79
+ if current is None or isinstance(
80
+ current, (str, int, float, bool, Mapping, list, tuple, set)
81
+ ):
82
+ return current
83
+
84
+ direct = getattr(current, "result", None)
85
+ if direct is not None and direct is not current:
86
+ current = direct
87
+ continue
88
+
89
+ payload = getattr(current, "response_payload", None)
90
+ if payload is not None and payload is not current:
91
+ current = payload
92
+ continue
93
+
94
+ response = getattr(current, "response", None)
95
+ if response is not None:
96
+ response_result = getattr(response, "result", None)
97
+ if response_result is not None and response_result is not current:
98
+ current = response_result
99
+ continue
100
+
101
+ bag = getattr(current, "bag", None)
102
+ if isinstance(bag, Mapping) and bag.get("result") is not None:
103
+ nested = bag.get("result")
104
+ if nested is not current:
105
+ current = nested
106
+ continue
107
+
108
+ return current
109
+
110
+ return current
111
+
112
+
113
+ async def _maybe_await(value: Any) -> Any:
114
+ if hasattr(value, "__await__"):
115
+ return await value
116
+ return value
117
+
118
+
119
+ async def _crud_result_fallback(ctx: _Ctx, current_result: Any) -> Any:
120
+ alias = str(ctx.get("op") or "").lower()
121
+ if alias not in {"read", "update", "replace"}:
122
+ return current_result
123
+
124
+ model = ctx.get("model")
125
+ db = ctx.get("db")
126
+ if not isinstance(model, type) or db is None:
127
+ return current_result
128
+
129
+ try:
130
+ pk_name = _single_pk_name(model)
131
+ except Exception:
132
+ return current_result
133
+
134
+ payload = ctx.get("payload")
135
+ path_params = ctx.get("path_params")
136
+ ident = None
137
+ if isinstance(path_params, Mapping) and pk_name in path_params:
138
+ ident = path_params.get(pk_name)
139
+ elif isinstance(payload, Mapping) and pk_name in payload:
140
+ ident = payload.get(pk_name)
141
+ if ident is None:
142
+ return current_result
143
+
144
+ ident = _coerce_pk_value(model, ident)
145
+
146
+ if alias == "read":
147
+ needs_fallback = current_result is None
148
+ if isinstance(current_result, Mapping):
149
+ if pk_name not in current_result:
150
+ needs_fallback = True
151
+ else:
152
+ data_keys = [k for k in current_result.keys() if k != pk_name]
153
+ if data_keys and all(current_result.get(k) is None for k in data_keys):
154
+ needs_fallback = True
155
+ if not needs_fallback:
156
+ return current_result
157
+ return await _crud_ops.read(model, ident, db)
158
+
159
+ if current_result is None and isinstance(payload, Mapping):
160
+ if alias == "update":
161
+ return await _crud_ops.update(model, ident, dict(payload), db)
162
+ return await _crud_ops.replace(model, ident, dict(payload), db)
163
+
164
+ return current_result
165
+
166
+
167
+ async def _rollback_if_owned(
168
+ db: Union[Session, AsyncSession, None],
169
+ owns_tx: bool,
170
+ *,
171
+ phases: Optional[PhaseChains],
172
+ ctx: Any,
173
+ ) -> None:
174
+ if not owns_tx or db is None:
175
+ return
176
+ if not _g(phases, "ON_ROLLBACK"):
177
+ try:
178
+ await _maybe_await(db.rollback())
179
+ except Exception: # pragma: no cover
180
+ logger.exception("Rollback failed", exc_info=True)
181
+ try:
182
+ await _run_chain(ctx, _g(phases, "ON_ROLLBACK"), phase="ON_ROLLBACK")
183
+ except Exception: # pragma: no cover
184
+ pass
185
+
186
+
187
+ async def _invoke(
188
+ *,
189
+ request: Optional[Request],
190
+ db: Union[Session, AsyncSession, None],
191
+ phases: Optional[PhaseChains],
192
+ ctx: Optional[MutableMapping[str, Any]] = None,
193
+ ) -> Any:
194
+ """Execute an operation through explicit phases with strict write policies."""
195
+
196
+ _reduce_log_noise()
197
+
198
+ ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
199
+ if getattr(ctx, "app", None) is None and getattr(ctx, "router", None) is not None:
200
+ ctx.app = ctx.router
201
+ if getattr(ctx, "op", None) is None and getattr(ctx, "method", None) is not None:
202
+ ctx.op = ctx.method
203
+ env = ctx.get("env")
204
+ op_name = getattr(ctx, "op", None) or getattr(ctx, "method", None)
205
+ if env is None:
206
+ ctx["env"] = SimpleNamespace(method=op_name)
207
+ elif getattr(env, "method", None) in (None, "", "unknown"):
208
+ try:
209
+ setattr(env, "method", op_name)
210
+ except Exception:
211
+ ctx["env"] = SimpleNamespace(method=op_name)
212
+ if getattr(ctx, "model", None) is None:
213
+ obj = getattr(ctx, "obj", None)
214
+ if obj is not None:
215
+ ctx.model = type(obj)
216
+ if getattr(ctx, "opview", None) is None:
217
+ model = getattr(ctx, "model", None)
218
+ alias = getattr(ctx, "op", None)
219
+ specs = ctx.get("specs")
220
+ if (
221
+ isinstance(model, type)
222
+ and isinstance(alias, str)
223
+ and isinstance(specs, Mapping)
224
+ ):
225
+ try:
226
+ cached_views = getattr(model, _OPVIEW_CACHE_ATTR, None)
227
+ if not isinstance(cached_views, dict):
228
+ cached_views = {}
229
+ setattr(model, _OPVIEW_CACHE_ATTR, cached_views)
230
+
231
+ cached_view = cached_views.get(alias)
232
+ if cached_view is not None:
233
+ ctx.opview = cached_view
234
+ else:
235
+ from tigrbl_kernel.opview_compiler import compile_opview_from_specs
236
+
237
+ op_spec = next(
238
+ (
239
+ sp
240
+ for sp in (
241
+ getattr(getattr(model, "ops", None), "all", ()) or ()
242
+ )
243
+ if getattr(sp, "alias", None) == alias
244
+ ),
245
+ None,
246
+ )
247
+ if op_spec is None:
248
+ op_spec = SimpleNamespace(alias=alias)
249
+ compiled_view = compile_opview_from_specs(specs, op_spec)
250
+ cached_views[alias] = compiled_view
251
+ ctx.opview = compiled_view
252
+ except Exception:
253
+ pass
254
+ skip_persist: bool = bool(ctx.get(CTX_SKIP_PERSIST_FLAG) or ctx.get("skip_persist"))
255
+ skip_egress: bool = bool(ctx.get("skip_egress"))
256
+ if not callable(ctx.get("rpc_error_builder")):
257
+ ctx["rpc_error_builder"] = lambda exc: to_rpc_error_payload(
258
+ create_standardized_error(exc)
259
+ )
260
+
261
+ existed_tx_before = _in_transaction(db) if db is not None else False
262
+
263
+ async def _run_phase(
264
+ name: str,
265
+ *,
266
+ allow_flush: bool,
267
+ allow_commit: bool,
268
+ in_tx: bool,
269
+ require_owned_for_commit: bool = True,
270
+ nonfatal: bool = False,
271
+ owns_tx_for_phase: Optional[bool] = None,
272
+ ) -> None:
273
+ chain = _g(phases, name)
274
+ if not chain:
275
+ return
276
+
277
+ owns_tx_now = bool(owns_tx_for_phase)
278
+ if owns_tx_for_phase is None:
279
+ owns_tx_now = not existed_tx_before
280
+
281
+ del allow_flush, allow_commit, require_owned_for_commit
282
+ ctx.phase = name
283
+ ctx.owns_tx = owns_tx_now
284
+
285
+ try:
286
+ await _run_chain(ctx, chain, phase=name)
287
+ except Exception as exc:
288
+ ctx.error = exc
289
+ if in_tx:
290
+ await _rollback_if_owned(db, owns_tx_now, phases=phases, ctx=ctx)
291
+ err_name = f"ON_{name}_ERROR"
292
+ try:
293
+ await _run_chain(
294
+ ctx, _g(phases, err_name) or _g(phases, "ON_ERROR"), phase=err_name
295
+ )
296
+ except Exception: # pragma: no cover
297
+ pass
298
+ if nonfatal:
299
+ logger.exception("%s failed (nonfatal): %s", name, exc)
300
+ return
301
+ raise create_standardized_error(exc)
302
+
303
+ await _run_phase(
304
+ "INGRESS_BEGIN", allow_flush=False, allow_commit=False, in_tx=False
305
+ )
306
+ await _run_phase(
307
+ "INGRESS_PARSE", allow_flush=False, allow_commit=False, in_tx=False
308
+ )
309
+ await _run_phase(
310
+ "INGRESS_ROUTE", allow_flush=False, allow_commit=False, in_tx=False
311
+ )
312
+ await _run_phase("PRE_TX_BEGIN", allow_flush=False, allow_commit=False, in_tx=False)
313
+
314
+ if not skip_persist:
315
+ await _run_phase(
316
+ "START_TX",
317
+ allow_flush=False,
318
+ allow_commit=False,
319
+ in_tx=False,
320
+ require_owned_for_commit=False,
321
+ )
322
+
323
+ await _run_phase(
324
+ "PRE_HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
325
+ )
326
+
327
+ await _run_phase(
328
+ "HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
329
+ )
330
+
331
+ await _run_phase(
332
+ "POST_HANDLER", allow_flush=True, allow_commit=False, in_tx=not skip_persist
333
+ )
334
+
335
+ await _run_phase(
336
+ "PRE_COMMIT", allow_flush=False, allow_commit=False, in_tx=not skip_persist
337
+ )
338
+
339
+ if not skip_persist:
340
+ # If this invocation started outside a transaction, the runtime owns the
341
+ # commit decision even when the backend uses implicit/autobegin semantics.
342
+ owns_tx_for_commit = not existed_tx_before
343
+ await _run_phase(
344
+ "END_TX",
345
+ allow_flush=True,
346
+ allow_commit=True,
347
+ in_tx=True,
348
+ require_owned_for_commit=False,
349
+ owns_tx_for_phase=owns_tx_for_commit,
350
+ )
351
+
352
+ from types import SimpleNamespace as _NS
353
+
354
+ if ctx.get("result") is None:
355
+ fallback = (
356
+ ctx.get("obj")
357
+ or ctx.get("objs")
358
+ or (
359
+ ctx.get("temp", {}).get("egress", {}).get("result")
360
+ if isinstance(ctx.get("temp"), Mapping)
361
+ else None
362
+ )
363
+ )
364
+ if fallback is not None:
365
+ ctx["result"] = fallback
366
+
367
+ serializer = ctx.get("response_serializer")
368
+ current_result = ctx.get("result")
369
+ temp = ctx.get("temp") if isinstance(ctx, Mapping) else None
370
+ rpc_error = temp.get("rpc_error") if isinstance(temp, Mapping) else None
371
+ response_state = getattr(ctx, "response", None)
372
+ if current_result is None and response_state is not None:
373
+ current_result = getattr(response_state, "result", None)
374
+ if current_result is None:
375
+ current_result = getattr(ctx, "obj", None)
376
+
377
+ current_result = _unwrap_ctx_result(current_result)
378
+ current_result = await _crud_result_fallback(ctx, current_result)
379
+
380
+ if isinstance(rpc_error, Mapping):
381
+ ctx["result"] = None
382
+ elif callable(serializer):
383
+ try:
384
+ ctx["result"] = serializer(current_result)
385
+ except Exception:
386
+ logger.exception("response serialization failed", exc_info=True)
387
+ else:
388
+ ctx["result"] = _normalize_result_payload(current_result)
389
+
390
+ if getattr(ctx, "status_code", None) is None:
391
+ ctx.status_code = _default_status_for_alias(
392
+ getattr(ctx, "op", None), getattr(ctx, "target", None)
393
+ )
394
+
395
+ response_obj = getattr(ctx, "response", None)
396
+ if response_obj is None:
397
+ ctx.response = _NS(result=ctx.get("result"))
398
+ else:
399
+ setattr(response_obj, "result", ctx.get("result"))
400
+
401
+ pre_egress_result = ctx.get("result")
402
+
403
+ await _run_phase("POST_COMMIT", allow_flush=True, allow_commit=False, in_tx=False)
404
+
405
+ if not skip_egress:
406
+ await _run_phase(
407
+ "POST_RESPONSE",
408
+ allow_flush=False,
409
+ allow_commit=False,
410
+ in_tx=False,
411
+ nonfatal=True,
412
+ )
413
+
414
+ await _run_phase(
415
+ "EGRESS_SHAPE", allow_flush=False, allow_commit=False, in_tx=False
416
+ )
417
+ await _run_phase(
418
+ "EGRESS_FINALIZE", allow_flush=False, allow_commit=False, in_tx=False
419
+ )
420
+ if ctx.get("result") is not None and getattr(ctx, "response", None) is not None:
421
+ setattr(ctx.response, "result", ctx.get("result"))
422
+
423
+ release = None
424
+ if isinstance(temp, Mapping):
425
+ release = temp.pop("__sys_db_release__", None)
426
+ if callable(release):
427
+ release()
428
+
429
+ if skip_egress:
430
+ result = _unwrap_ctx_result(pre_egress_result)
431
+ result = _normalize_result_payload(result)
432
+ if result is not None:
433
+ ctx["result"] = result
434
+ if getattr(ctx, "response", None) is not None:
435
+ setattr(ctx.response, "result", result)
436
+ return result
437
+
438
+ if getattr(ctx, "response", None) is not None:
439
+ result = getattr(ctx.response, "result", ctx.get("result"))
440
+ result = _unwrap_ctx_result(result)
441
+ if isinstance(result, Mapping) and {"status_code", "headers", "body"}.issubset(
442
+ result
443
+ ):
444
+ body = result.get("body")
445
+ if body is None and pre_egress_result is not None:
446
+ result = pre_egress_result
447
+ if result is not None:
448
+ ctx["result"] = result
449
+ setattr(ctx.response, "result", result)
450
+ return result
451
+
452
+ result = _unwrap_ctx_result(ctx.get("result"))
453
+ if isinstance(result, Mapping) and {"status_code", "headers", "body"}.issubset(
454
+ result
455
+ ):
456
+ body = result.get("body")
457
+ if body is None and pre_egress_result is not None:
458
+ result = pre_egress_result
459
+ if result is not None:
460
+ ctx["result"] = result
461
+ return result
462
+
463
+
464
+ __all__ = ["_invoke"]
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from .invoke import _invoke
6
+ from .types import _Ctx
7
+
8
+
9
+ async def _run(
10
+ self,
11
+ model: type,
12
+ alias: str,
13
+ *,
14
+ db: Any,
15
+ request: Any | None = None,
16
+ ctx: Any | None = None,
17
+ ) -> Any:
18
+ phases = self._build_op(model, alias)
19
+ base_ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
20
+ return await _invoke(request=request, db=db, phases=phases, ctx=base_ctx)
21
+
22
+
23
+ async def _run_phase_chain(self, ctx: _Ctx, phases: Any) -> None:
24
+ for _phase, steps in (phases or {}).items():
25
+ ctx.phase = _phase
26
+ for step in steps or ():
27
+ rv = step(ctx)
28
+ if hasattr(rv, "__await__"):
29
+ await rv