tigrbl-kernel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl_kernel/__init__.py +44 -0
- tigrbl_kernel/_build.py +478 -0
- tigrbl_kernel/_compile.py +147 -0
- tigrbl_kernel/atoms.py +237 -0
- tigrbl_kernel/cache.py +85 -0
- tigrbl_kernel/core.py +186 -0
- tigrbl_kernel/events.py +6 -0
- tigrbl_kernel/helpers.py +165 -0
- tigrbl_kernel/hook_types.py +10 -0
- tigrbl_kernel/labels.py +369 -0
- tigrbl_kernel/models.py +218 -0
- tigrbl_kernel/opview_compiler.py +99 -0
- tigrbl_kernel/ordering.py +347 -0
- tigrbl_kernel/payload.py +61 -0
- tigrbl_kernel/trace.py +330 -0
- tigrbl_kernel/types.py +56 -0
- tigrbl_kernel/utils.py +215 -0
- tigrbl_kernel-0.1.0.dist-info/METADATA +52 -0
- tigrbl_kernel-0.1.0.dist-info/RECORD +20 -0
- tigrbl_kernel-0.1.0.dist-info/WHEEL +4 -0
tigrbl_kernel/atoms.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import inspect
|
|
5
|
+
import logging
|
|
6
|
+
import pkgutil
|
|
7
|
+
from types import SimpleNamespace
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Callable,
|
|
11
|
+
Dict,
|
|
12
|
+
Iterable,
|
|
13
|
+
List,
|
|
14
|
+
Mapping,
|
|
15
|
+
Optional,
|
|
16
|
+
Sequence,
|
|
17
|
+
cast,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from tigrbl_typing.phases import HOOK_PHASES as HOOK_PHASES
|
|
21
|
+
|
|
22
|
+
from tigrbl_atoms import StepFn
|
|
23
|
+
from . import events as _ev, ordering as _ordering
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
_AtomRun = Callable[[Optional[object], Any], Any]
|
|
28
|
+
_DiscoveredAtom = tuple[str, _AtomRun]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _discover_atoms() -> list[_DiscoveredAtom]:
|
|
32
|
+
out: list[_DiscoveredAtom] = []
|
|
33
|
+
try:
|
|
34
|
+
import tigrbl_atoms.atoms as atoms_pkg # type: ignore
|
|
35
|
+
except Exception:
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
for info in pkgutil.walk_packages(atoms_pkg.__path__, atoms_pkg.__name__ + "."): # type: ignore[attr-defined]
|
|
39
|
+
if info.ispkg:
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
mod = importlib.import_module(info.name)
|
|
43
|
+
instance = getattr(mod, "INSTANCE", None)
|
|
44
|
+
if instance is not None and callable(instance):
|
|
45
|
+
anchor = getattr(instance, "anchor", None) or getattr(
|
|
46
|
+
mod, "ANCHOR", None
|
|
47
|
+
)
|
|
48
|
+
if isinstance(anchor, str):
|
|
49
|
+
out.append((anchor, cast(_AtomRun, instance)))
|
|
50
|
+
continue
|
|
51
|
+
anchor = getattr(mod, "ANCHOR", None)
|
|
52
|
+
run = getattr(mod, "run", None)
|
|
53
|
+
if isinstance(anchor, str) and callable(run):
|
|
54
|
+
out.append((anchor, cast(_AtomRun, run)))
|
|
55
|
+
except Exception:
|
|
56
|
+
continue
|
|
57
|
+
logger.debug("kernel: discovered %d atoms", len(out))
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _infer_domain_subject(run: _AtomRun) -> tuple[Optional[str], Optional[str]]:
|
|
62
|
+
mod = getattr(run, "__module__", "") or ""
|
|
63
|
+
parts = mod.split(".")
|
|
64
|
+
try:
|
|
65
|
+
i = parts.index("atoms")
|
|
66
|
+
return (
|
|
67
|
+
parts[i + 1] if i + 1 < len(parts) else None,
|
|
68
|
+
parts[i + 2] if i + 2 < len(parts) else None,
|
|
69
|
+
)
|
|
70
|
+
except ValueError:
|
|
71
|
+
return None, None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _make_label(anchor: str, run: _AtomRun) -> Optional[str]:
|
|
75
|
+
domain, subject = _infer_domain_subject(run)
|
|
76
|
+
return f"atom:{domain}:{subject}@{anchor}" if (domain and subject) else None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _wrap_atom(run: _AtomRun, *, anchor: str) -> StepFn:
|
|
80
|
+
use_two_args = True
|
|
81
|
+
try:
|
|
82
|
+
params = tuple(inspect.signature(run).parameters.values())
|
|
83
|
+
positional = [
|
|
84
|
+
p
|
|
85
|
+
for p in params
|
|
86
|
+
if p.kind
|
|
87
|
+
in (
|
|
88
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
89
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
use_two_args = len(positional) != 1
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
use_two_args = True
|
|
95
|
+
|
|
96
|
+
async def _step(ctx: Any) -> Any:
|
|
97
|
+
rv = run(None, ctx) if use_two_args else run(ctx) # type: ignore[misc]
|
|
98
|
+
if inspect.isawaitable(rv):
|
|
99
|
+
return await cast(Any, rv)
|
|
100
|
+
return rv
|
|
101
|
+
|
|
102
|
+
label = getattr(run, "__tigrbl_label", None)
|
|
103
|
+
if not isinstance(label, str):
|
|
104
|
+
label = _make_label(anchor, run)
|
|
105
|
+
if label:
|
|
106
|
+
setattr(_step, "__tigrbl_label", label)
|
|
107
|
+
return _step
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _hook_phase_chains(model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
111
|
+
hooks_root = getattr(model, "hooks", None) or SimpleNamespace()
|
|
112
|
+
alias_ns = getattr(hooks_root, alias, None)
|
|
113
|
+
out: Dict[str, List[StepFn]] = {ph: [] for ph in HOOK_PHASES}
|
|
114
|
+
if alias_ns is None:
|
|
115
|
+
return out
|
|
116
|
+
for phase in HOOK_PHASES:
|
|
117
|
+
out[phase] = list(getattr(alias_ns, phase, []) or [])
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _is_persistent(chains: Mapping[str, Sequence[StepFn]]) -> bool:
|
|
122
|
+
for fn in chains.get("START_TX", ()) or ():
|
|
123
|
+
if getattr(fn, "__name__", "") == "start_tx":
|
|
124
|
+
return True
|
|
125
|
+
for fn in chains.get("PRE_TX_BEGIN", ()) or ():
|
|
126
|
+
if getattr(fn, "__name__", "") == "mark_skip_persist":
|
|
127
|
+
return False
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _label_dep_atom(*, kind: str, index: int, anchor: str) -> str:
|
|
132
|
+
return f"hook:dep:{kind}:{index}@{anchor}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _make_dep_atom_step(run_fn: _AtomRun, dep: Any, *, label: str) -> StepFn:
|
|
136
|
+
async def _step(ctx: Any) -> Any:
|
|
137
|
+
rv = run_fn(dep, ctx)
|
|
138
|
+
if hasattr(rv, "__await__"):
|
|
139
|
+
return await cast(Any, rv)
|
|
140
|
+
return rv
|
|
141
|
+
|
|
142
|
+
setattr(_step, "__tigrbl_label", label)
|
|
143
|
+
return _step
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _inject_pre_tx_dep_atoms(chains: Dict[str, List[StepFn]], sp: Any | None) -> None:
|
|
147
|
+
if sp is None:
|
|
148
|
+
return
|
|
149
|
+
try:
|
|
150
|
+
from tigrbl_atoms.atoms.dep.security import INSTANCE as sec_run # type: ignore
|
|
151
|
+
from tigrbl_atoms.atoms.dep.extra import INSTANCE as dep_run # type: ignore
|
|
152
|
+
except Exception:
|
|
153
|
+
try:
|
|
154
|
+
from tigrbl_atoms.atoms.dep.security import run as sec_run # type: ignore
|
|
155
|
+
from tigrbl_atoms.atoms.dep.extra import run as dep_run # type: ignore
|
|
156
|
+
except Exception:
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
pre_tx = chains.setdefault("PRE_TX_BEGIN", [])
|
|
160
|
+
for idx, dep in enumerate(getattr(sp, "secdeps", ()) or ()):
|
|
161
|
+
pre_tx.append(
|
|
162
|
+
_make_dep_atom_step(
|
|
163
|
+
sec_run,
|
|
164
|
+
dep,
|
|
165
|
+
label=_label_dep_atom(
|
|
166
|
+
kind="security", index=idx, anchor=_ev.DEP_SECURITY
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
for idx, dep in enumerate(getattr(sp, "deps", ()) or ()):
|
|
171
|
+
pre_tx.append(
|
|
172
|
+
_make_dep_atom_step(
|
|
173
|
+
dep_run,
|
|
174
|
+
dep,
|
|
175
|
+
label=_label_dep_atom(kind="extra", index=idx, anchor=_ev.DEP_EXTRA),
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _inject_atoms(
|
|
181
|
+
chains: Dict[str, List[StepFn]],
|
|
182
|
+
atoms: Iterable[_DiscoveredAtom],
|
|
183
|
+
*,
|
|
184
|
+
persistent: bool,
|
|
185
|
+
target: str | None = None,
|
|
186
|
+
) -> None:
|
|
187
|
+
order = {name: i for i, name in enumerate(_ev.all_events_ordered())}
|
|
188
|
+
|
|
189
|
+
def _sort_key(item: _DiscoveredAtom) -> tuple[int, int]:
|
|
190
|
+
anchor, run = item
|
|
191
|
+
anchor_idx = order.get(anchor, 10_000)
|
|
192
|
+
domain, subject = _infer_domain_subject(run)
|
|
193
|
+
token = f"{domain}:{subject}" if domain and subject else ""
|
|
194
|
+
pref = _ordering._PREF.get(anchor, ())
|
|
195
|
+
token_idx = pref.index(token) if token in pref else 10_000
|
|
196
|
+
return anchor_idx, token_idx
|
|
197
|
+
|
|
198
|
+
for anchor, run in sorted(atoms, key=_sort_key):
|
|
199
|
+
if _ev.is_valid_event(anchor):
|
|
200
|
+
info = _ev.get_anchor_info(anchor)
|
|
201
|
+
phase = info.phase
|
|
202
|
+
persist_tied = info.persist_tied
|
|
203
|
+
elif anchor == "INGRESS_ROUTE":
|
|
204
|
+
# Back-compat phase alias retained for route-stage atom injection.
|
|
205
|
+
phase = "INGRESS_ROUTE"
|
|
206
|
+
persist_tied = False
|
|
207
|
+
elif anchor in _ev.PHASES:
|
|
208
|
+
phase = anchor
|
|
209
|
+
persist_tied = False
|
|
210
|
+
elif anchor == "INGRESS_ROUTE":
|
|
211
|
+
# Compatibility alias retained for tests and legacy direct callers.
|
|
212
|
+
# Canonical runtime ingress routing phase is INGRESS_DISPATCH.
|
|
213
|
+
phase = anchor
|
|
214
|
+
persist_tied = False
|
|
215
|
+
else:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
domain, _subject = _infer_domain_subject(run)
|
|
219
|
+
if not persistent and persist_tied:
|
|
220
|
+
if not (
|
|
221
|
+
domain == "sys"
|
|
222
|
+
and isinstance(_subject, str)
|
|
223
|
+
and _subject.startswith("handler_")
|
|
224
|
+
):
|
|
225
|
+
continue
|
|
226
|
+
if (
|
|
227
|
+
domain == "sys"
|
|
228
|
+
and isinstance(_subject, str)
|
|
229
|
+
and _subject.startswith("handler_")
|
|
230
|
+
):
|
|
231
|
+
handler_target = _subject.removeprefix("handler_")
|
|
232
|
+
if handler_target != "persistence" and target and handler_target != target:
|
|
233
|
+
continue
|
|
234
|
+
if domain == "dep":
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
chains.setdefault(phase, []).append(_wrap_atom(run, anchor=anchor))
|
tigrbl_kernel/cache.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import weakref
|
|
5
|
+
from typing import Any, Dict, Generic, Mapping, Optional, Sequence, TypeVar
|
|
6
|
+
|
|
7
|
+
from tigrbl_core._spec.column_spec import mro_collect_columns
|
|
8
|
+
|
|
9
|
+
K = TypeVar("K")
|
|
10
|
+
V = TypeVar("V")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _WeakMaybeDict(Generic[K, V]):
|
|
14
|
+
"""Dictionary that uses weak references when possible.
|
|
15
|
+
|
|
16
|
+
Falls back to strong references when ``key`` cannot be weakly referenced.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._weak: "weakref.WeakKeyDictionary[Any, V]" = weakref.WeakKeyDictionary()
|
|
21
|
+
self._strong: Dict[int, tuple[Any, V]] = {}
|
|
22
|
+
|
|
23
|
+
def _use_weak(self, key: Any) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
weakref.ref(key)
|
|
26
|
+
return True
|
|
27
|
+
except TypeError:
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
31
|
+
if self._use_weak(key):
|
|
32
|
+
self._weak[key] = value
|
|
33
|
+
else:
|
|
34
|
+
self._strong[id(key)] = (key, value)
|
|
35
|
+
|
|
36
|
+
def __getitem__(self, key: K) -> V:
|
|
37
|
+
if self._use_weak(key):
|
|
38
|
+
return self._weak[key]
|
|
39
|
+
return self._strong[id(key)][1]
|
|
40
|
+
|
|
41
|
+
def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
|
|
42
|
+
if self._use_weak(key):
|
|
43
|
+
return self._weak.get(key, default)
|
|
44
|
+
return self._strong.get(id(key), (None, default))[1]
|
|
45
|
+
|
|
46
|
+
def setdefault(self, key: K, default: V) -> V:
|
|
47
|
+
if self._use_weak(key):
|
|
48
|
+
return self._weak.setdefault(key, default)
|
|
49
|
+
return self._strong.setdefault(id(key), (key, default))[1]
|
|
50
|
+
|
|
51
|
+
def pop(self, key: K, default: Optional[V] = None) -> Optional[V]:
|
|
52
|
+
if self._use_weak(key):
|
|
53
|
+
return self._weak.pop(key, default)
|
|
54
|
+
return self._strong.pop(id(key), (None, default))[1]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class _SpecsOnceCache:
|
|
58
|
+
"""Thread-safe, compute-once cache of per-model column specs."""
|
|
59
|
+
|
|
60
|
+
def __init__(self) -> None:
|
|
61
|
+
self._d: Dict[type, Mapping[str, Any]] = {}
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def get(self, model: type) -> Mapping[str, Any]:
|
|
65
|
+
try:
|
|
66
|
+
return self._d[model]
|
|
67
|
+
except KeyError:
|
|
68
|
+
pass
|
|
69
|
+
with self._lock:
|
|
70
|
+
rv = self._d.get(model)
|
|
71
|
+
if rv is None:
|
|
72
|
+
rv = mro_collect_columns(model)
|
|
73
|
+
self._d[model] = rv
|
|
74
|
+
return rv
|
|
75
|
+
|
|
76
|
+
def prime(self, models: Sequence[type]) -> None:
|
|
77
|
+
for model in models:
|
|
78
|
+
self.get(model)
|
|
79
|
+
|
|
80
|
+
def invalidate(self, model: Optional[type] = None) -> None:
|
|
81
|
+
with self._lock:
|
|
82
|
+
if model is None:
|
|
83
|
+
self._d.clear()
|
|
84
|
+
else:
|
|
85
|
+
self._d.pop(model, None)
|
tigrbl_kernel/core.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
from typing import Any, ClassVar, Mapping, Optional, Sequence
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from . import events as _ev
|
|
10
|
+
from ._build import (
|
|
11
|
+
_build_route_matrix,
|
|
12
|
+
_pack_kernel_plan,
|
|
13
|
+
_segment_label,
|
|
14
|
+
_build,
|
|
15
|
+
_build_egress,
|
|
16
|
+
_build_ingress,
|
|
17
|
+
_build_op,
|
|
18
|
+
_compile_bootstrap_plan,
|
|
19
|
+
_plan_labels,
|
|
20
|
+
)
|
|
21
|
+
from ._compile import _compile_opview_from_specs, _compile_plan
|
|
22
|
+
from .atoms import _DiscoveredAtom, _discover_atoms
|
|
23
|
+
from .cache import _SpecsOnceCache, _WeakMaybeDict
|
|
24
|
+
from .models import KernelPlan, OpView
|
|
25
|
+
from .opview_compiler import compile_opview_from_specs
|
|
26
|
+
from .types import DEFAULT_PHASE_ORDER as _DEFAULT_PHASE_ORDER
|
|
27
|
+
from .utils import (
|
|
28
|
+
_opspecs,
|
|
29
|
+
_table_iter,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
DEFAULT_PHASE_ORDER = tuple(getattr(_ev, "PHASES", ())) or _DEFAULT_PHASE_ORDER
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Kernel:
|
|
39
|
+
_instance: ClassVar["Kernel | None"] = None
|
|
40
|
+
|
|
41
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> "Kernel":
|
|
42
|
+
if cls._instance is None:
|
|
43
|
+
cls._instance = super().__new__(cls)
|
|
44
|
+
return cls._instance
|
|
45
|
+
|
|
46
|
+
def __init__(self, atoms: Optional[Sequence[_DiscoveredAtom]] = None):
|
|
47
|
+
if atoms is None and getattr(self, "_singleton_initialized", False):
|
|
48
|
+
self._reset(atoms)
|
|
49
|
+
return
|
|
50
|
+
self._reset(atoms)
|
|
51
|
+
if atoms is None:
|
|
52
|
+
self._singleton_initialized = True
|
|
53
|
+
|
|
54
|
+
def _reset(self, atoms: Optional[Sequence[_DiscoveredAtom]] = None) -> None:
|
|
55
|
+
self._atoms_cache = list(atoms) if atoms else None
|
|
56
|
+
self._specs_cache = _SpecsOnceCache()
|
|
57
|
+
self._opviews = _WeakMaybeDict()
|
|
58
|
+
self._phase_chains = _WeakMaybeDict()
|
|
59
|
+
self._phase_chains_by_id: dict[int, dict[str, tuple[Any, Any]]] = {}
|
|
60
|
+
self._kernel_plans = _WeakMaybeDict()
|
|
61
|
+
self._kernelz_payload = _WeakMaybeDict()
|
|
62
|
+
self._primed = _WeakMaybeDict()
|
|
63
|
+
self._lock = threading.Lock()
|
|
64
|
+
|
|
65
|
+
def _atoms(self) -> list[_DiscoveredAtom]:
|
|
66
|
+
if self._atoms_cache is None:
|
|
67
|
+
self._atoms_cache = _discover_atoms()
|
|
68
|
+
return self._atoms_cache
|
|
69
|
+
|
|
70
|
+
def get_specs(self, model: type) -> Mapping[str, Any]:
|
|
71
|
+
return self._specs_cache.get(model)
|
|
72
|
+
|
|
73
|
+
def plan_labels(self, model: type, alias: str) -> list[str]:
|
|
74
|
+
return self._plan_labels(model, alias)
|
|
75
|
+
|
|
76
|
+
def prime_specs(self, models: Sequence[type]) -> None:
|
|
77
|
+
self._specs_cache.prime(models)
|
|
78
|
+
|
|
79
|
+
def invalidate_specs(self, model: Optional[type] = None) -> None:
|
|
80
|
+
self._specs_cache.invalidate(model)
|
|
81
|
+
|
|
82
|
+
def ensure_primed(self, app: Any) -> None:
|
|
83
|
+
if self._primed.get(app):
|
|
84
|
+
return
|
|
85
|
+
with self._lock:
|
|
86
|
+
if self._primed.get(app):
|
|
87
|
+
return
|
|
88
|
+
self.prime_specs(_table_iter(app))
|
|
89
|
+
self._kernel_plans.pop(app, None)
|
|
90
|
+
self._kernelz_payload.pop(app, None)
|
|
91
|
+
self._opviews.pop(app, None)
|
|
92
|
+
self._phase_chains.pop(app, None)
|
|
93
|
+
self._phase_chains_by_id.clear()
|
|
94
|
+
self._primed[app] = True
|
|
95
|
+
|
|
96
|
+
def get_opview(self, app: Any, model: type, alias: str) -> OpView:
|
|
97
|
+
ov_map = self._opviews.get(app)
|
|
98
|
+
if isinstance(ov_map, dict):
|
|
99
|
+
opview = ov_map.get((model, alias))
|
|
100
|
+
if opview is not None:
|
|
101
|
+
return opview
|
|
102
|
+
|
|
103
|
+
self.ensure_primed(app)
|
|
104
|
+
|
|
105
|
+
ov_map = self._opviews.setdefault(app, {})
|
|
106
|
+
opview = ov_map.get((model, alias))
|
|
107
|
+
if opview is not None:
|
|
108
|
+
return opview
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
specs = self._specs_cache.get(model)
|
|
112
|
+
found = False
|
|
113
|
+
for sp in _opspecs(model):
|
|
114
|
+
ov_map.setdefault(
|
|
115
|
+
(model, sp.alias), compile_opview_from_specs(specs, sp)
|
|
116
|
+
)
|
|
117
|
+
if sp.alias == alias:
|
|
118
|
+
found = True
|
|
119
|
+
|
|
120
|
+
if not found:
|
|
121
|
+
temp_sp = SimpleNamespace(alias=alias)
|
|
122
|
+
ov_map[(model, alias)] = compile_opview_from_specs(specs, temp_sp)
|
|
123
|
+
|
|
124
|
+
return ov_map[(model, alias)]
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
raise RuntimeError(
|
|
127
|
+
f"opview_missing: app={app!r} model={getattr(model, '__name__', model)!r} alias={alias!r}"
|
|
128
|
+
) from exc
|
|
129
|
+
|
|
130
|
+
def kernel_plan(self, app: Any) -> KernelPlan:
|
|
131
|
+
self.ensure_primed(app)
|
|
132
|
+
plan = self._kernel_plans.get(app)
|
|
133
|
+
if isinstance(plan, KernelPlan):
|
|
134
|
+
return plan
|
|
135
|
+
|
|
136
|
+
compiled = self.compile_plan(app)
|
|
137
|
+
self._kernel_plans[app] = compiled
|
|
138
|
+
|
|
139
|
+
payload: dict[str, dict[str, list[str]]] = {}
|
|
140
|
+
for model in _table_iter(app):
|
|
141
|
+
model_name = getattr(model, "__name__", str(model))
|
|
142
|
+
payload[model_name] = {}
|
|
143
|
+
for sp in _opspecs(model):
|
|
144
|
+
payload[model_name][sp.alias] = self._plan_labels(model, sp.alias)
|
|
145
|
+
self._kernelz_payload[app] = payload
|
|
146
|
+
|
|
147
|
+
return compiled
|
|
148
|
+
|
|
149
|
+
def kernelz_payload(self, app: Any) -> dict[str, dict[str, list[str]]]:
|
|
150
|
+
self.kernel_plan(app)
|
|
151
|
+
payload = self._kernelz_payload.get(app)
|
|
152
|
+
if isinstance(payload, dict):
|
|
153
|
+
return payload
|
|
154
|
+
return {}
|
|
155
|
+
|
|
156
|
+
def invalidate_kernelz_payload(self, app: Optional[Any] = None) -> None:
|
|
157
|
+
with self._lock:
|
|
158
|
+
if app is None:
|
|
159
|
+
self._kernel_plans = _WeakMaybeDict()
|
|
160
|
+
self._kernelz_payload = _WeakMaybeDict()
|
|
161
|
+
self._opviews = _WeakMaybeDict()
|
|
162
|
+
self._phase_chains = _WeakMaybeDict()
|
|
163
|
+
self._phase_chains_by_id = {}
|
|
164
|
+
self._primed = _WeakMaybeDict()
|
|
165
|
+
else:
|
|
166
|
+
self._kernel_plans.pop(app, None)
|
|
167
|
+
self._kernelz_payload.pop(app, None)
|
|
168
|
+
self._opviews.pop(app, None)
|
|
169
|
+
self._phase_chains.pop(app, None)
|
|
170
|
+
self._phase_chains_by_id.clear()
|
|
171
|
+
self._primed.pop(app, None)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
Kernel._build_op = _build_op
|
|
175
|
+
Kernel._build = _build
|
|
176
|
+
Kernel._build_ingress = _build_ingress
|
|
177
|
+
Kernel._build_egress = _build_egress
|
|
178
|
+
Kernel._plan_labels = _plan_labels
|
|
179
|
+
Kernel._compile_bootstrap_plan = _compile_bootstrap_plan
|
|
180
|
+
Kernel._segment_label = _segment_label
|
|
181
|
+
Kernel._build_route_matrix = _build_route_matrix
|
|
182
|
+
Kernel._pack_kernel_plan = _pack_kernel_plan
|
|
183
|
+
|
|
184
|
+
Kernel.compile_plan = _compile_plan
|
|
185
|
+
Kernel.compile_bootstrap_plan = _compile_bootstrap_plan
|
|
186
|
+
Kernel._compile_opview_from_specs = _compile_opview_from_specs
|
tigrbl_kernel/events.py
ADDED
tigrbl_kernel/helpers.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from tigrbl_kernel import trace as _trace # type: ignore
|
|
9
|
+
except Exception: # pragma: no cover
|
|
10
|
+
_trace = None # type: ignore
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _normalize_payload(payload: Any) -> Any:
|
|
16
|
+
if isinstance(payload, (str, int, float, bool)) or payload is None:
|
|
17
|
+
return payload
|
|
18
|
+
if isinstance(payload, dict):
|
|
19
|
+
return {str(k): _normalize_payload(v) for k, v in payload.items()}
|
|
20
|
+
if isinstance(payload, (list, tuple, set)):
|
|
21
|
+
return [_normalize_payload(v) for v in payload]
|
|
22
|
+
|
|
23
|
+
model_dump = getattr(payload, "model_dump", None)
|
|
24
|
+
if callable(model_dump):
|
|
25
|
+
try:
|
|
26
|
+
return _normalize_payload(model_dump())
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
obj_dict = getattr(payload, "__dict__", None)
|
|
31
|
+
if isinstance(obj_dict, dict):
|
|
32
|
+
data = {
|
|
33
|
+
k: v
|
|
34
|
+
for k, v in obj_dict.items()
|
|
35
|
+
if not k.startswith("_") and not callable(v)
|
|
36
|
+
}
|
|
37
|
+
if data:
|
|
38
|
+
return _normalize_payload(data)
|
|
39
|
+
|
|
40
|
+
return str(payload)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
async def _maybe_await(v: Any) -> Any:
|
|
44
|
+
if inspect.isawaitable(v):
|
|
45
|
+
return await v # type: ignore[func-returns-value]
|
|
46
|
+
return v
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _trace_ready(ctx: Any) -> bool:
|
|
50
|
+
if _trace is None:
|
|
51
|
+
return False
|
|
52
|
+
temp = getattr(ctx, "temp", None)
|
|
53
|
+
return isinstance(temp, dict) and "__trace__" in temp
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def _run_chain(ctx: Any, chain: Optional[Iterable[Any]], *, phase: str) -> None:
|
|
57
|
+
temp = getattr(ctx, "temp", None)
|
|
58
|
+
if isinstance(temp, dict) and temp.get("rpc_short_circuit"):
|
|
59
|
+
if phase not in {"EGRESS_SHAPE", "EGRESS_FINALIZE", "POST_RESPONSE"}:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
if not chain:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
isawaitable = inspect.isawaitable
|
|
66
|
+
trace_active = _trace_ready(ctx)
|
|
67
|
+
|
|
68
|
+
for idx, step in enumerate(chain):
|
|
69
|
+
label = getattr(step, "__tigrbl_label", None)
|
|
70
|
+
if not isinstance(label, str) or not label:
|
|
71
|
+
label = f"phase:{phase}:step:{idx}"
|
|
72
|
+
|
|
73
|
+
seq = _trace.start(ctx, label) if trace_active else None
|
|
74
|
+
before_post_response = None
|
|
75
|
+
if phase == "POST_RESPONSE":
|
|
76
|
+
response = getattr(ctx, "response", None)
|
|
77
|
+
before_value = getattr(response, "result", None)
|
|
78
|
+
if before_value is None:
|
|
79
|
+
before_value = ctx.get("result")
|
|
80
|
+
before_post_response = _normalize_payload(before_value)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
rv = step(ctx)
|
|
84
|
+
if isawaitable(rv):
|
|
85
|
+
rv = await rv # type: ignore[func-returns-value]
|
|
86
|
+
if rv is not None and rv is not ctx:
|
|
87
|
+
ctx.result = rv
|
|
88
|
+
if trace_active:
|
|
89
|
+
_trace.end(ctx, seq, status=_trace.OK)
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
phase == "POST_RESPONSE"
|
|
93
|
+
and getattr(step, "__tigrbl_label", None) is None
|
|
94
|
+
):
|
|
95
|
+
temp_ns = getattr(ctx, "temp", None)
|
|
96
|
+
if not isinstance(temp_ns, dict):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
egress = temp_ns.get("egress")
|
|
100
|
+
if not isinstance(egress, dict):
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
response = getattr(ctx, "response", None)
|
|
104
|
+
result = getattr(response, "result", None)
|
|
105
|
+
if result is None:
|
|
106
|
+
result = ctx.get("result")
|
|
107
|
+
else:
|
|
108
|
+
ctx.result = result
|
|
109
|
+
|
|
110
|
+
if result is None:
|
|
111
|
+
continue
|
|
112
|
+
if hasattr(result, "status_code") and hasattr(result, "body"):
|
|
113
|
+
continue
|
|
114
|
+
if isinstance(result, dict) and any(
|
|
115
|
+
key in result for key in ("status_code", "headers", "body")
|
|
116
|
+
):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
safe_result = _normalize_payload(result)
|
|
120
|
+
if safe_result == before_post_response:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
egress["wire_payload"] = safe_result
|
|
124
|
+
enveloped = egress.get("enveloped")
|
|
125
|
+
if isinstance(enveloped, dict) and "result" in enveloped:
|
|
126
|
+
enveloped["result"] = safe_result
|
|
127
|
+
elif enveloped is not None and not isinstance(enveloped, dict):
|
|
128
|
+
egress["enveloped"] = safe_result
|
|
129
|
+
|
|
130
|
+
transport_response = egress.get("transport_response")
|
|
131
|
+
if isinstance(transport_response, dict):
|
|
132
|
+
transport_response["body"] = egress.get("enveloped", safe_result)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
temp = getattr(ctx, "temp", None)
|
|
135
|
+
route = temp.get("route") if isinstance(temp, dict) else None
|
|
136
|
+
rpc_env = route.get("rpc_envelope") if isinstance(route, dict) else None
|
|
137
|
+
if isinstance(rpc_env, Mapping):
|
|
138
|
+
ctx.status_code = 200
|
|
139
|
+
ctx.result = None
|
|
140
|
+
if isinstance(temp, dict):
|
|
141
|
+
temp["rpc_short_circuit"] = True
|
|
142
|
+
if not isinstance(temp.get("rpc_error"), dict):
|
|
143
|
+
builder = ctx.get("rpc_error_builder")
|
|
144
|
+
if callable(builder):
|
|
145
|
+
try:
|
|
146
|
+
temp["rpc_error"] = builder(exc)
|
|
147
|
+
except Exception:
|
|
148
|
+
temp["rpc_error"] = {"message": str(exc)}
|
|
149
|
+
else:
|
|
150
|
+
temp["rpc_error"] = {"message": str(exc)}
|
|
151
|
+
if trace_active:
|
|
152
|
+
_trace.attach_error(ctx, seq, exc)
|
|
153
|
+
_trace.end(ctx, seq, status=_trace.ERROR)
|
|
154
|
+
return
|
|
155
|
+
if trace_active:
|
|
156
|
+
_trace.attach_error(ctx, seq, exc)
|
|
157
|
+
_trace.end(ctx, seq, status=_trace.ERROR)
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _g(phases: Optional[Mapping[str, Sequence[Any]]], key: str) -> Sequence[Any]:
|
|
162
|
+
return () if not phases else phases.get(key, ())
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
__all__ = ["_maybe_await", "_run_chain", "_g"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Kernel-owned hook type definitions.
|
|
2
|
+
|
|
3
|
+
The kernel compiles plans and phase chains, but does not execute them.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from tigrbl_atoms import HookPhase, HookPhases, StepFn
|
|
9
|
+
|
|
10
|
+
__all__ = ["HookPhase", "HookPhases", "StepFn"]
|