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.
- {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/PKG-INFO +5 -1
- {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/pyproject.toml +9 -1
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/config/__init__.py +3 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/config/constants.py +5 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/__init__.py +27 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/base.py +31 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/helpers.py +3 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/invoke.py +464 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/kernel_executor.py +29 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/numba_packed.py +79 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/packed.py +873 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/phase.py +37 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/executors/types.py +300 -0
- {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/tigrbl_runtime/runtime/README.md +16 -19
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/__init__.py +31 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/_typing_aliases.py +5 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/base.py +35 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/events.py +93 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/executor/__init__.py +6 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/executor/invoke.py +68 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/hook_types.py +7 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/kernel.py +27 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/labels.py +3 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/response.py +33 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/runtime.py +75 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/__init__.py +1 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/converters.py +1 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/exceptions.py +1 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/mappings.py +1 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/status/utils.py +1 -0
- tigrbl_runtime-0.1.0.dev6/tigrbl_runtime/runtime/system.py +156 -0
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/__init__.py +0 -20
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/context.py +0 -206
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/events.py +0 -435
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/__init__.py +0 -6
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/guards.py +0 -132
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/helpers.py +0 -194
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/invoke.py +0 -242
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/types.py +0 -128
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/__init__.py +0 -4
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/invoke.py +0 -103
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/raw.py +0 -26
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/hook_types.py +0 -60
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/kernel.py +0 -667
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/labels.py +0 -369
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/opview.py +0 -89
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/ordering.py +0 -331
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/__init__.py +0 -63
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/converters.py +0 -222
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/exceptions.py +0 -149
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/mappings.py +0 -94
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/utils.py +0 -114
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/system.py +0 -338
- tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/trace.py +0 -330
- {tigrbl_runtime-0.1.0.dev1 → tigrbl_runtime-0.1.0.dev6}/README.md +0 -0
- {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.
|
|
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
|

|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tigrbl-runtime"
|
|
3
|
-
version = "0.1.0.
|
|
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,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,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
|