tigrbl-kernel 0.1.0.dev1__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/kernel/__init__.py +49 -0
- tigrbl_kernel/kernel/atoms.py +234 -0
- tigrbl_kernel/kernel/cache.py +85 -0
- tigrbl_kernel/kernel/core.py +582 -0
- tigrbl_kernel/kernel/models.py +124 -0
- tigrbl_kernel/kernel/opview_compiler.py +99 -0
- tigrbl_kernel/kernel/payload.py +64 -0
- tigrbl_kernel-0.1.0.dev1.dist-info/METADATA +51 -0
- tigrbl_kernel-0.1.0.dev1.dist-info/RECORD +10 -0
- tigrbl_kernel-0.1.0.dev1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Mapping, Optional
|
|
4
|
+
|
|
5
|
+
from tigrbl_runtime.hook_types import StepFn
|
|
6
|
+
from tigrbl_runtime.executor import _Ctx, _invoke
|
|
7
|
+
from .core import Kernel
|
|
8
|
+
from .models import OpView, SchemaIn, SchemaOut
|
|
9
|
+
|
|
10
|
+
_default_kernel = Kernel()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cached_specs(model: type) -> Mapping[str, Any]:
|
|
14
|
+
"""Atoms can call this; zero per-request collection."""
|
|
15
|
+
return _default_kernel.get_specs(model)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_phase_chains(model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
19
|
+
return _default_kernel.build_op(model, alias)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def plan_labels(model: type, alias: str) -> list[str]:
|
|
23
|
+
return _default_kernel.plan_labels(model, alias)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def run(
|
|
27
|
+
model: type,
|
|
28
|
+
alias: str,
|
|
29
|
+
*,
|
|
30
|
+
db: Any,
|
|
31
|
+
request: Any | None = None,
|
|
32
|
+
ctx: Optional[Mapping[str, Any]] = None,
|
|
33
|
+
) -> Any:
|
|
34
|
+
phases = _default_kernel.build_op(model, alias)
|
|
35
|
+
base_ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
36
|
+
return await _invoke(request=request, db=db, phases=phases, ctx=base_ctx)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Kernel",
|
|
41
|
+
"OpView",
|
|
42
|
+
"SchemaIn",
|
|
43
|
+
"SchemaOut",
|
|
44
|
+
"get_cached_specs",
|
|
45
|
+
"_default_kernel",
|
|
46
|
+
"build_phase_chains",
|
|
47
|
+
"plan_labels",
|
|
48
|
+
"run",
|
|
49
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
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_runtime.hook_types import PHASES as HOOK_PHASES
|
|
21
|
+
from tigrbl_runtime.hook_types import StepFn
|
|
22
|
+
from tigrbl_runtime import events as _ev, ordering as _ordering, system as _sys
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
_AtomRun = Callable[[Optional[object], Any], Any]
|
|
27
|
+
_DiscoveredAtom = tuple[str, _AtomRun]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _discover_atoms() -> list[_DiscoveredAtom]:
|
|
31
|
+
out: list[_DiscoveredAtom] = []
|
|
32
|
+
try:
|
|
33
|
+
import tigrbl_concrete.atoms as atoms_pkg # type: ignore
|
|
34
|
+
except Exception:
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
for info in pkgutil.walk_packages(atoms_pkg.__path__, atoms_pkg.__name__ + "."): # type: ignore[attr-defined]
|
|
38
|
+
if info.ispkg:
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
mod = importlib.import_module(info.name)
|
|
42
|
+
anchor = getattr(mod, "ANCHOR", None)
|
|
43
|
+
run = getattr(mod, "run", None)
|
|
44
|
+
if isinstance(anchor, str) and callable(run):
|
|
45
|
+
out.append((anchor, run))
|
|
46
|
+
except Exception:
|
|
47
|
+
continue
|
|
48
|
+
logger.debug("kernel: discovered %d atoms", len(out))
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _infer_domain_subject(run: _AtomRun) -> tuple[Optional[str], Optional[str]]:
|
|
53
|
+
mod = getattr(run, "__module__", "") or ""
|
|
54
|
+
parts = mod.split(".")
|
|
55
|
+
try:
|
|
56
|
+
i = parts.index("atoms")
|
|
57
|
+
return (
|
|
58
|
+
parts[i + 1] if i + 1 < len(parts) else None,
|
|
59
|
+
parts[i + 2] if i + 2 < len(parts) else None,
|
|
60
|
+
)
|
|
61
|
+
except ValueError:
|
|
62
|
+
return None, None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _make_label(anchor: str, run: _AtomRun) -> Optional[str]:
|
|
66
|
+
domain, subject = _infer_domain_subject(run)
|
|
67
|
+
return f"atom:{domain}:{subject}@{anchor}" if (domain and subject) else None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _wrap_atom(run: _AtomRun, *, anchor: str) -> StepFn:
|
|
71
|
+
use_two_args = True
|
|
72
|
+
try:
|
|
73
|
+
params = tuple(inspect.signature(run).parameters.values())
|
|
74
|
+
positional = [
|
|
75
|
+
p
|
|
76
|
+
for p in params
|
|
77
|
+
if p.kind
|
|
78
|
+
in (
|
|
79
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
80
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
81
|
+
)
|
|
82
|
+
]
|
|
83
|
+
# Kernel atoms default to ``run(obj, ctx)``. Support legacy
|
|
84
|
+
# ``run(ctx)`` atoms without masking TypeError raised inside the atom.
|
|
85
|
+
use_two_args = len(positional) != 1
|
|
86
|
+
except (TypeError, ValueError):
|
|
87
|
+
use_two_args = True
|
|
88
|
+
|
|
89
|
+
async def _step(ctx: Any) -> Any:
|
|
90
|
+
if use_two_args:
|
|
91
|
+
rv = run(None, ctx)
|
|
92
|
+
else:
|
|
93
|
+
rv = run(ctx) # type: ignore[misc]
|
|
94
|
+
if hasattr(rv, "__await__"):
|
|
95
|
+
return await cast(Any, rv)
|
|
96
|
+
return rv
|
|
97
|
+
|
|
98
|
+
label = getattr(run, "__tigrbl_label", None)
|
|
99
|
+
if not isinstance(label, str):
|
|
100
|
+
label = _make_label(anchor, run)
|
|
101
|
+
if label:
|
|
102
|
+
setattr(_step, "__tigrbl_label", label)
|
|
103
|
+
return _step
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _hook_phase_chains(model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
107
|
+
hooks_root = getattr(model, "hooks", None) or SimpleNamespace()
|
|
108
|
+
alias_ns = getattr(hooks_root, alias, None)
|
|
109
|
+
out: Dict[str, List[StepFn]] = {ph: [] for ph in HOOK_PHASES}
|
|
110
|
+
if alias_ns is None:
|
|
111
|
+
return out
|
|
112
|
+
for phase in HOOK_PHASES:
|
|
113
|
+
out[phase] = list(getattr(alias_ns, phase, []) or [])
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _is_persistent(chains: Mapping[str, Sequence[StepFn]]) -> bool:
|
|
118
|
+
for fn in chains.get("START_TX", ()) or ():
|
|
119
|
+
if getattr(fn, "__name__", "") == "start_tx":
|
|
120
|
+
return True
|
|
121
|
+
for fn in chains.get("PRE_TX_BEGIN", ()) or ():
|
|
122
|
+
if getattr(fn, "__name__", "") == "mark_skip_persist":
|
|
123
|
+
return False
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _label_dep_atom(*, kind: str, index: int, anchor: str) -> str:
|
|
128
|
+
return f"hook:dep:{kind}:{index}@{anchor}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _make_dep_atom_step(run_fn: _AtomRun, dep: Any, *, label: str) -> StepFn:
|
|
132
|
+
async def _step(ctx: Any) -> Any:
|
|
133
|
+
rv = run_fn(dep, ctx)
|
|
134
|
+
if hasattr(rv, "__await__"):
|
|
135
|
+
return await cast(Any, rv)
|
|
136
|
+
return rv
|
|
137
|
+
|
|
138
|
+
setattr(_step, "__tigrbl_label", label)
|
|
139
|
+
return _step
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _inject_pre_tx_dep_atoms(chains: Dict[str, List[StepFn]], sp: Any | None) -> None:
|
|
143
|
+
if sp is None:
|
|
144
|
+
return
|
|
145
|
+
try:
|
|
146
|
+
from tigrbl_concrete.atoms.dep.security import run as sec_run
|
|
147
|
+
from tigrbl_concrete.atoms.dep.extra import run as dep_run
|
|
148
|
+
except Exception:
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
pre_tx = chains.setdefault("PRE_TX_BEGIN", [])
|
|
152
|
+
for idx, dep in enumerate(getattr(sp, "secdeps", ()) or ()):
|
|
153
|
+
pre_tx.append(
|
|
154
|
+
_make_dep_atom_step(
|
|
155
|
+
sec_run,
|
|
156
|
+
dep,
|
|
157
|
+
label=_label_dep_atom(
|
|
158
|
+
kind="security",
|
|
159
|
+
index=idx,
|
|
160
|
+
anchor=_ev.DEP_SECURITY,
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
for idx, dep in enumerate(getattr(sp, "deps", ()) or ()):
|
|
165
|
+
pre_tx.append(
|
|
166
|
+
_make_dep_atom_step(
|
|
167
|
+
dep_run,
|
|
168
|
+
dep,
|
|
169
|
+
label=_label_dep_atom(
|
|
170
|
+
kind="extra",
|
|
171
|
+
index=idx,
|
|
172
|
+
anchor=_ev.DEP_EXTRA,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _inject_atoms(
|
|
179
|
+
chains: Dict[str, List[StepFn]],
|
|
180
|
+
atoms: Iterable[_DiscoveredAtom],
|
|
181
|
+
*,
|
|
182
|
+
persistent: bool,
|
|
183
|
+
) -> None:
|
|
184
|
+
order = {name: i for i, name in enumerate(_ev.all_events_ordered())}
|
|
185
|
+
|
|
186
|
+
def _sort_key(item: _DiscoveredAtom) -> tuple[int, int]:
|
|
187
|
+
anchor, run = item
|
|
188
|
+
anchor_idx = order.get(anchor, 10_000)
|
|
189
|
+
domain, subject = _infer_domain_subject(run)
|
|
190
|
+
token = f"{domain}:{subject}" if domain and subject else ""
|
|
191
|
+
pref = _ordering._PREF.get(anchor, ())
|
|
192
|
+
token_idx = pref.index(token) if token in pref else 10_000
|
|
193
|
+
return anchor_idx, token_idx
|
|
194
|
+
|
|
195
|
+
for anchor, run in sorted(atoms, key=_sort_key):
|
|
196
|
+
if _ev.is_valid_event(anchor):
|
|
197
|
+
info = _ev.get_anchor_info(anchor)
|
|
198
|
+
phase = info.phase
|
|
199
|
+
persist_tied = info.persist_tied
|
|
200
|
+
elif anchor in _ev.PHASES:
|
|
201
|
+
phase = anchor
|
|
202
|
+
persist_tied = False
|
|
203
|
+
else:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
if phase in ("START_TX", "END_TX"):
|
|
207
|
+
continue
|
|
208
|
+
if not persistent and persist_tied:
|
|
209
|
+
continue
|
|
210
|
+
if anchor == _ev.SYS_HANDLER_PERSISTENCE and chains.get("HANDLER"):
|
|
211
|
+
continue
|
|
212
|
+
domain, _subject = _infer_domain_subject(run)
|
|
213
|
+
if domain == "dep":
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
chains.setdefault(phase, []).append(_wrap_atom(run, anchor=anchor))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _inject_txn_system_steps(
|
|
220
|
+
chains: Dict[str, List[StepFn]], *, model: Any | None = None
|
|
221
|
+
) -> None:
|
|
222
|
+
start_anchor, start_run = _sys.get("txn", "begin")
|
|
223
|
+
end_anchor, end_run = _sys.get("txn", "commit")
|
|
224
|
+
chains.setdefault(start_anchor, []).append(
|
|
225
|
+
_wrap_atom(start_run, anchor=start_anchor)
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if not chains.get(_sys.HANDLER) and _sys.can_resolve_handler(model):
|
|
229
|
+
handler_anchor, handler_run = _sys.get("handler", "crud")
|
|
230
|
+
chains.setdefault(handler_anchor, []).append(
|
|
231
|
+
_wrap_atom(handler_run, anchor=handler_anchor)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
chains.setdefault(end_anchor, []).append(_wrap_atom(end_run, anchor=end_anchor))
|
|
@@ -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_canon.mapping.column_mro_collect 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)
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import threading
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from typing import Any, ClassVar, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
8
|
+
|
|
9
|
+
from tigrbl_runtime.hook_types import StepFn
|
|
10
|
+
from tigrbl_runtime.executor import _Ctx, _invoke
|
|
11
|
+
from tigrbl_runtime import events as _ev
|
|
12
|
+
from .atoms import (
|
|
13
|
+
_DiscoveredAtom,
|
|
14
|
+
_discover_atoms,
|
|
15
|
+
_hook_phase_chains,
|
|
16
|
+
_inject_atoms,
|
|
17
|
+
_inject_pre_tx_dep_atoms,
|
|
18
|
+
_inject_txn_system_steps,
|
|
19
|
+
_is_persistent,
|
|
20
|
+
_wrap_atom,
|
|
21
|
+
)
|
|
22
|
+
from .cache import _SpecsOnceCache, _WeakMaybeDict
|
|
23
|
+
from .models import KernelPlan, OpKey, OpMeta, OpView
|
|
24
|
+
from tigrbl_runtime.labels import label_hook
|
|
25
|
+
from .opview_compiler import compile_opview_from_specs
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _RestMatcher:
|
|
31
|
+
"""REST selector matcher supporting exact and templated paths."""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._exact: dict[tuple[str, str], int] = {}
|
|
35
|
+
self._templated: list[tuple[str, re.Pattern[str], tuple[str, ...], int]] = []
|
|
36
|
+
self._selectors: dict[str, int] = {}
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _normalize_path(path: str) -> str:
|
|
40
|
+
p = path if path.startswith("/") else f"/{path}"
|
|
41
|
+
return p.rstrip("/") or "/"
|
|
42
|
+
|
|
43
|
+
def add(self, method: str, path: str, meta_index: int) -> None:
|
|
44
|
+
m = method.upper()
|
|
45
|
+
normalized = self._normalize_path(path)
|
|
46
|
+
self._selectors[f"{m} {normalized}"] = meta_index
|
|
47
|
+
if "{" not in normalized:
|
|
48
|
+
self._exact[(m, normalized)] = meta_index
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
names = tuple(re.findall(r"\{([^{}]+)\}", normalized))
|
|
52
|
+
pattern = "^" + re.sub(r"\{([^{}]+)\}", r"(?P<\1>[^/]+)", normalized) + "$"
|
|
53
|
+
self._templated.append((m, re.compile(pattern), names, meta_index))
|
|
54
|
+
|
|
55
|
+
def match(self, method: str, path: str) -> tuple[int, dict[str, str]]:
|
|
56
|
+
m = method.upper()
|
|
57
|
+
normalized = self._normalize_path(path)
|
|
58
|
+
|
|
59
|
+
exact = self._exact.get((m, normalized))
|
|
60
|
+
if exact is not None:
|
|
61
|
+
return exact, {}
|
|
62
|
+
|
|
63
|
+
for templ_method, pattern, names, meta_index in self._templated:
|
|
64
|
+
if templ_method != m:
|
|
65
|
+
continue
|
|
66
|
+
matched = pattern.match(normalized)
|
|
67
|
+
if matched is None:
|
|
68
|
+
continue
|
|
69
|
+
params = {
|
|
70
|
+
name: matched.group(name)
|
|
71
|
+
for name in names
|
|
72
|
+
if matched.group(name) is not None
|
|
73
|
+
}
|
|
74
|
+
return meta_index, params
|
|
75
|
+
|
|
76
|
+
raise KeyError((m, normalized))
|
|
77
|
+
|
|
78
|
+
def __call__(self, method: str, path: str) -> tuple[int, dict[str, str]]:
|
|
79
|
+
return self.match(method, path)
|
|
80
|
+
|
|
81
|
+
# Compatibility mapping surface used by older tests and adapters.
|
|
82
|
+
def __contains__(self, selector: object) -> bool:
|
|
83
|
+
return isinstance(selector, str) and selector in self._selectors
|
|
84
|
+
|
|
85
|
+
def __getitem__(self, selector: str) -> int:
|
|
86
|
+
return self._selectors[selector]
|
|
87
|
+
|
|
88
|
+
def get(self, selector: str, default: Any = None) -> Any:
|
|
89
|
+
return self._selectors.get(selector, default)
|
|
90
|
+
|
|
91
|
+
def keys(self):
|
|
92
|
+
return self._selectors.keys()
|
|
93
|
+
|
|
94
|
+
def items(self):
|
|
95
|
+
return self._selectors.items()
|
|
96
|
+
|
|
97
|
+
def values(self):
|
|
98
|
+
return self._selectors.values()
|
|
99
|
+
|
|
100
|
+
def __iter__(self):
|
|
101
|
+
return iter(self._selectors)
|
|
102
|
+
|
|
103
|
+
def __len__(self) -> int:
|
|
104
|
+
return len(self._selectors)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def deepmerge_phase_chains(
|
|
108
|
+
*phase_maps: Mapping[str, Sequence[StepFn]],
|
|
109
|
+
) -> Dict[str, List[StepFn]]:
|
|
110
|
+
"""Deterministically concatenate phase step lists into a new mapping."""
|
|
111
|
+
merged: Dict[str, List[StepFn]] = {}
|
|
112
|
+
for phase_map in phase_maps:
|
|
113
|
+
for phase, steps in (phase_map or {}).items():
|
|
114
|
+
merged.setdefault(phase, []).extend(list(steps or ()))
|
|
115
|
+
return {phase: list(steps) for phase, steps in merged.items()}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _table_iter(app: Any) -> Sequence[type]:
|
|
119
|
+
tables = getattr(app, "tables", None)
|
|
120
|
+
if isinstance(tables, dict):
|
|
121
|
+
return tuple(v for v in tables.values() if isinstance(v, type))
|
|
122
|
+
return ()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _opspecs(model: type) -> Sequence[Any]:
|
|
126
|
+
return getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ()) or ()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _label_callable(fn: Any) -> str:
|
|
130
|
+
name = getattr(fn, "__qualname__", getattr(fn, "__name__", repr(fn)))
|
|
131
|
+
module = getattr(fn, "__module__", None)
|
|
132
|
+
return f"{module}.{name}" if module else name
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _label_step(step: Any, phase: str) -> str:
|
|
136
|
+
label = getattr(step, "__tigrbl_label", None)
|
|
137
|
+
if isinstance(label, str) and "@" in label:
|
|
138
|
+
return label
|
|
139
|
+
module = getattr(step, "__module__", "") or ""
|
|
140
|
+
name = getattr(step, "__name__", "") or ""
|
|
141
|
+
if module.startswith("tigrbl_core.core.crud") and name:
|
|
142
|
+
return f"hook:wire:tigrbl:core:crud:ops:{name}@{phase}"
|
|
143
|
+
return f"hook:wire:{_label_callable(step).replace('.', ':')}@{phase}"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class Kernel:
|
|
147
|
+
"""
|
|
148
|
+
SSoT for runtime scheduling. One Kernel per App (not per API).
|
|
149
|
+
Auto-primed under the hood. Downstream users never touch this.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
_instance: ClassVar["Kernel | None"] = None
|
|
153
|
+
|
|
154
|
+
def __new__(cls, *args: Any, **kwargs: Any) -> "Kernel":
|
|
155
|
+
if cls._instance is None:
|
|
156
|
+
cls._instance = super().__new__(cls)
|
|
157
|
+
return cls._instance
|
|
158
|
+
|
|
159
|
+
def __init__(self, atoms: Optional[Sequence[_DiscoveredAtom]] = None):
|
|
160
|
+
if atoms is None and getattr(self, "_singleton_initialized", False):
|
|
161
|
+
self._reset(atoms)
|
|
162
|
+
return
|
|
163
|
+
self._reset(atoms)
|
|
164
|
+
if atoms is None:
|
|
165
|
+
self._singleton_initialized = True
|
|
166
|
+
|
|
167
|
+
def _reset(self, atoms: Optional[Sequence[_DiscoveredAtom]] = None) -> None:
|
|
168
|
+
self._atoms_cache = list(atoms) if atoms else None
|
|
169
|
+
self._specs_cache = _SpecsOnceCache()
|
|
170
|
+
self._opviews = _WeakMaybeDict()
|
|
171
|
+
self._kernel_plans = _WeakMaybeDict()
|
|
172
|
+
self._kernelz_payload = _WeakMaybeDict()
|
|
173
|
+
self._primed = _WeakMaybeDict()
|
|
174
|
+
self._lock = threading.Lock()
|
|
175
|
+
|
|
176
|
+
def _atoms(self) -> list[_DiscoveredAtom]:
|
|
177
|
+
if self._atoms_cache is None:
|
|
178
|
+
self._atoms_cache = _discover_atoms()
|
|
179
|
+
return self._atoms_cache
|
|
180
|
+
|
|
181
|
+
def get_specs(self, model: type) -> Mapping[str, Any]:
|
|
182
|
+
return self._specs_cache.get(model)
|
|
183
|
+
|
|
184
|
+
def _compile_opview_from_specs(self, specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
185
|
+
"""Compatibility shim for callers using legacy Kernel method dispatch."""
|
|
186
|
+
return compile_opview_from_specs(specs, sp)
|
|
187
|
+
|
|
188
|
+
def prime_specs(self, models: Sequence[type]) -> None:
|
|
189
|
+
self._specs_cache.prime(models)
|
|
190
|
+
|
|
191
|
+
def invalidate_specs(self, model: Optional[type] = None) -> None:
|
|
192
|
+
self._specs_cache.invalidate(model)
|
|
193
|
+
|
|
194
|
+
def build_op(self, model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
195
|
+
chains = _hook_phase_chains(model, alias)
|
|
196
|
+
specs = getattr(getattr(model, "ops", SimpleNamespace()), "by_alias", {})
|
|
197
|
+
sp_list = specs.get(alias) or ()
|
|
198
|
+
sp = sp_list[0] if sp_list else None
|
|
199
|
+
target = (getattr(sp, "target", alias) or "").lower()
|
|
200
|
+
persist_policy = getattr(sp, "persist", "default")
|
|
201
|
+
persistent = (
|
|
202
|
+
persist_policy != "skip" and target not in {"read", "list"}
|
|
203
|
+
) or _is_persistent(chains)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
_inject_atoms(chains, self._atoms() or (), persistent=persistent)
|
|
207
|
+
except Exception:
|
|
208
|
+
logger.exception(
|
|
209
|
+
"kernel: atom injection failed for %s.%s",
|
|
210
|
+
getattr(model, "__name__", model),
|
|
211
|
+
alias,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
_inject_pre_tx_dep_atoms(chains, sp)
|
|
215
|
+
|
|
216
|
+
if persistent:
|
|
217
|
+
try:
|
|
218
|
+
_inject_txn_system_steps(chains, model=model)
|
|
219
|
+
except Exception:
|
|
220
|
+
logger.exception(
|
|
221
|
+
"kernel: failed to inject txn system steps for %s.%s",
|
|
222
|
+
getattr(model, "__name__", model),
|
|
223
|
+
alias,
|
|
224
|
+
)
|
|
225
|
+
for phase in _ev.PHASES:
|
|
226
|
+
chains.setdefault(phase, [])
|
|
227
|
+
return chains
|
|
228
|
+
|
|
229
|
+
def build(self, model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
230
|
+
return self.build_op(model, alias)
|
|
231
|
+
|
|
232
|
+
def build_ingress(self, app: Any) -> Dict[str, List[StepFn]]:
|
|
233
|
+
del app
|
|
234
|
+
order = {name: idx for idx, name in enumerate(_ev.all_events_ordered())}
|
|
235
|
+
ingress_atoms: Dict[str, List[tuple[str, Any]]] = {}
|
|
236
|
+
for anchor, run in self._atoms() or ():
|
|
237
|
+
if not _ev.is_valid_event(anchor):
|
|
238
|
+
continue
|
|
239
|
+
phase = _ev.phase_for_event(anchor)
|
|
240
|
+
if phase not in {"INGRESS_BEGIN", "INGRESS_PARSE", "INGRESS_ROUTE"}:
|
|
241
|
+
continue
|
|
242
|
+
ingress_atoms.setdefault(phase, []).append((anchor, run))
|
|
243
|
+
|
|
244
|
+
out: Dict[str, List[StepFn]] = {}
|
|
245
|
+
for phase, atoms in ingress_atoms.items():
|
|
246
|
+
ordered = sorted(atoms, key=lambda item: order.get(item[0], 10_000))
|
|
247
|
+
out[phase] = [_wrap_atom(run, anchor=anchor) for anchor, run in ordered]
|
|
248
|
+
return out
|
|
249
|
+
|
|
250
|
+
def build_egress(self, app: Any) -> Dict[str, List[StepFn]]:
|
|
251
|
+
del app
|
|
252
|
+
order = {name: idx for idx, name in enumerate(_ev.all_events_ordered())}
|
|
253
|
+
egress_atoms: Dict[str, List[tuple[str, Any]]] = {}
|
|
254
|
+
for anchor, run in self._atoms() or ():
|
|
255
|
+
if not _ev.is_valid_event(anchor):
|
|
256
|
+
continue
|
|
257
|
+
phase = _ev.phase_for_event(anchor)
|
|
258
|
+
if phase not in {"EGRESS_SHAPE", "EGRESS_FINALIZE", "POST_RESPONSE"}:
|
|
259
|
+
continue
|
|
260
|
+
egress_atoms.setdefault(phase, []).append((anchor, run))
|
|
261
|
+
|
|
262
|
+
out: Dict[str, List[StepFn]] = {}
|
|
263
|
+
for phase, atoms in egress_atoms.items():
|
|
264
|
+
ordered = sorted(atoms, key=lambda item: order.get(item[0], 10_000))
|
|
265
|
+
out[phase] = [_wrap_atom(run, anchor=anchor) for anchor, run in ordered]
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
def plan_labels(self, model: type, alias: str) -> list[str]:
|
|
269
|
+
labels: list[str] = []
|
|
270
|
+
chains = self.build(model, alias)
|
|
271
|
+
opspec = next(
|
|
272
|
+
(sp for sp in _opspecs(model) if getattr(sp, "alias", None) == alias),
|
|
273
|
+
None,
|
|
274
|
+
)
|
|
275
|
+
persist = getattr(opspec, "persist", "default") != "skip"
|
|
276
|
+
|
|
277
|
+
tx_begin = "START_TX:hook:sys:txn:begin@START_TX"
|
|
278
|
+
tx_end = "END_TX:hook:sys:txn:commit@END_TX"
|
|
279
|
+
if persist:
|
|
280
|
+
labels.append(tx_begin)
|
|
281
|
+
|
|
282
|
+
for phase in _ev.PHASES:
|
|
283
|
+
if phase in {
|
|
284
|
+
"INGRESS_BEGIN",
|
|
285
|
+
"INGRESS_PARSE",
|
|
286
|
+
"INGRESS_ROUTE",
|
|
287
|
+
"EGRESS_SHAPE",
|
|
288
|
+
"EGRESS_FINALIZE",
|
|
289
|
+
"START_TX",
|
|
290
|
+
"END_TX",
|
|
291
|
+
}:
|
|
292
|
+
continue
|
|
293
|
+
for step in chains.get(phase, ()) or ():
|
|
294
|
+
labels.append(f"{phase}:{label_hook(step, phase)}")
|
|
295
|
+
|
|
296
|
+
if persist:
|
|
297
|
+
labels.append(tx_end)
|
|
298
|
+
|
|
299
|
+
return labels
|
|
300
|
+
|
|
301
|
+
async def invoke(
|
|
302
|
+
self,
|
|
303
|
+
*,
|
|
304
|
+
model: type,
|
|
305
|
+
alias: str,
|
|
306
|
+
db: Any,
|
|
307
|
+
request: Any | None = None,
|
|
308
|
+
ctx: Optional[Mapping[str, Any]] = None,
|
|
309
|
+
) -> Any:
|
|
310
|
+
"""Execute an operation for ``model.alias`` using the executor."""
|
|
311
|
+
phases = self.build_op(model, alias)
|
|
312
|
+
base_ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
313
|
+
base_ctx.model = model
|
|
314
|
+
base_ctx.op = alias
|
|
315
|
+
specs = self.get_specs(model)
|
|
316
|
+
base_ctx.opview = compile_opview_from_specs(specs, SimpleNamespace(alias=alias))
|
|
317
|
+
return await _invoke(request=request, db=db, phases=phases, ctx=base_ctx)
|
|
318
|
+
|
|
319
|
+
def ensure_primed(self, app: Any) -> None:
|
|
320
|
+
"""Autoprime once per App: specs → OpViews → /kernelz payload."""
|
|
321
|
+
with self._lock:
|
|
322
|
+
if self._primed.get(app):
|
|
323
|
+
return
|
|
324
|
+
models = list(_table_iter(app))
|
|
325
|
+
for model in models:
|
|
326
|
+
self._specs_cache.get(model)
|
|
327
|
+
|
|
328
|
+
ov_map: Dict[Tuple[type, str], OpView] = {}
|
|
329
|
+
for model in models:
|
|
330
|
+
specs = self._specs_cache.get(model)
|
|
331
|
+
for sp in _opspecs(model):
|
|
332
|
+
ov_map[(model, sp.alias)] = compile_opview_from_specs(specs, sp)
|
|
333
|
+
self._opviews[app] = ov_map
|
|
334
|
+
|
|
335
|
+
self._kernelz_payload[app] = self.compile_plan(app)
|
|
336
|
+
self._primed[app] = True
|
|
337
|
+
|
|
338
|
+
def get_opview(self, app: Any, model: type, alias: str) -> OpView:
|
|
339
|
+
"""Return OpView for (model, alias); compile on-demand if missing."""
|
|
340
|
+
ov_map = self._opviews.get(app)
|
|
341
|
+
if isinstance(ov_map, dict):
|
|
342
|
+
opview = ov_map.get((model, alias))
|
|
343
|
+
if opview is not None:
|
|
344
|
+
return opview
|
|
345
|
+
|
|
346
|
+
self.ensure_primed(app)
|
|
347
|
+
|
|
348
|
+
ov_map = self._opviews.setdefault(app, {})
|
|
349
|
+
opview = ov_map.get((model, alias))
|
|
350
|
+
if opview is not None:
|
|
351
|
+
return opview
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
specs = self._specs_cache.get(model)
|
|
355
|
+
found = False
|
|
356
|
+
for sp in _opspecs(model):
|
|
357
|
+
ov_map.setdefault(
|
|
358
|
+
(model, sp.alias), compile_opview_from_specs(specs, sp)
|
|
359
|
+
)
|
|
360
|
+
if sp.alias == alias:
|
|
361
|
+
found = True
|
|
362
|
+
|
|
363
|
+
if not found:
|
|
364
|
+
temp_sp = SimpleNamespace(alias=alias)
|
|
365
|
+
ov_map[(model, alias)] = compile_opview_from_specs(specs, temp_sp)
|
|
366
|
+
|
|
367
|
+
return ov_map[(model, alias)]
|
|
368
|
+
except Exception as exc:
|
|
369
|
+
raise RuntimeError(
|
|
370
|
+
f"opview_missing: app={app!r} model={getattr(model, '__name__', model)!r} alias={alias!r}"
|
|
371
|
+
) from exc
|
|
372
|
+
|
|
373
|
+
async def _run_phase_chain(self, ctx: _Ctx, phases: Any) -> None:
|
|
374
|
+
for _phase, steps in (phases or {}).items():
|
|
375
|
+
for step in steps or ():
|
|
376
|
+
rv = step(ctx)
|
|
377
|
+
if hasattr(rv, "__await__"):
|
|
378
|
+
await rv
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def _without_ingress_phases(phases: Mapping[str, Any] | None) -> dict[str, Any]:
|
|
382
|
+
if not phases:
|
|
383
|
+
return {}
|
|
384
|
+
ingress = {"INGRESS_BEGIN", "INGRESS_PARSE", "INGRESS_ROUTE"}
|
|
385
|
+
return {phase: steps for phase, steps in phases.items() if phase not in ingress}
|
|
386
|
+
|
|
387
|
+
async def handle_http(self, env: Any, app: Any) -> None:
|
|
388
|
+
from tigrbl_canon.mapping.runtime_routes import invoke_runtime_route_handler
|
|
389
|
+
from tigrbl_concrete.atoms.egress.asgi_send import (
|
|
390
|
+
_send_json,
|
|
391
|
+
_send_transport_response,
|
|
392
|
+
)
|
|
393
|
+
from tigrbl_runtime.status import StatusDetailError
|
|
394
|
+
|
|
395
|
+
plan = self.kernel_plan(app)
|
|
396
|
+
ctx = _Ctx.ensure(request=None, db=None)
|
|
397
|
+
ctx.app = app
|
|
398
|
+
ctx.router = app
|
|
399
|
+
ctx.raw = env
|
|
400
|
+
ctx.kernel_plan = plan
|
|
401
|
+
|
|
402
|
+
await self._run_phase_chain(ctx, plan.ingress_chain)
|
|
403
|
+
|
|
404
|
+
route = (
|
|
405
|
+
ctx.temp.get("route", {})
|
|
406
|
+
if isinstance(getattr(ctx, "temp", None), dict)
|
|
407
|
+
else {}
|
|
408
|
+
)
|
|
409
|
+
egress = (
|
|
410
|
+
ctx.temp.get("egress", {})
|
|
411
|
+
if isinstance(getattr(ctx, "temp", None), dict)
|
|
412
|
+
else {}
|
|
413
|
+
)
|
|
414
|
+
if (
|
|
415
|
+
isinstance(route, dict)
|
|
416
|
+
and route.get("short_circuit") is True
|
|
417
|
+
and isinstance(egress, dict)
|
|
418
|
+
and egress.get("transport_response")
|
|
419
|
+
):
|
|
420
|
+
await _send_transport_response(env, ctx)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
opmeta_index = route.get("opmeta_index") if isinstance(route, dict) else None
|
|
424
|
+
if not isinstance(opmeta_index, int):
|
|
425
|
+
if isinstance(route, dict) and route.get("method_not_allowed") is True:
|
|
426
|
+
await _send_json(env, 405, {"detail": "Method Not Allowed"})
|
|
427
|
+
return
|
|
428
|
+
handler = route.get("handler") if isinstance(route, dict) else None
|
|
429
|
+
if callable(handler):
|
|
430
|
+
await invoke_runtime_route_handler(ctx, handler=handler)
|
|
431
|
+
await _send_transport_response(env, ctx)
|
|
432
|
+
return
|
|
433
|
+
await _send_json(
|
|
434
|
+
env, 404, {"detail": "No runtime operation matched request."}
|
|
435
|
+
)
|
|
436
|
+
return
|
|
437
|
+
|
|
438
|
+
opmeta = plan.opmeta[opmeta_index]
|
|
439
|
+
ctx.model = opmeta.model
|
|
440
|
+
ctx.op = opmeta.alias
|
|
441
|
+
ctx.opview = self.get_opview(app, opmeta.model, opmeta.alias)
|
|
442
|
+
|
|
443
|
+
phases = self._without_ingress_phases(plan.phase_chains.get(opmeta_index, {}))
|
|
444
|
+
try:
|
|
445
|
+
await _invoke(request=None, db=None, phases=phases, ctx=ctx)
|
|
446
|
+
except StatusDetailError as exc:
|
|
447
|
+
detail = (
|
|
448
|
+
exc.detail
|
|
449
|
+
if getattr(exc, "detail", None) not in (None, "")
|
|
450
|
+
else str(exc)
|
|
451
|
+
)
|
|
452
|
+
await _send_json(
|
|
453
|
+
env, int(getattr(exc, "status_code", 500) or 500), {"detail": detail}
|
|
454
|
+
)
|
|
455
|
+
return
|
|
456
|
+
except Exception as exc: # pragma: no cover - defensive runtime fallback
|
|
457
|
+
from tigrbl_runtime.status import create_standardized_error
|
|
458
|
+
|
|
459
|
+
std = create_standardized_error(exc)
|
|
460
|
+
detail = (
|
|
461
|
+
std.detail
|
|
462
|
+
if getattr(std, "detail", None) not in (None, "")
|
|
463
|
+
else str(std)
|
|
464
|
+
)
|
|
465
|
+
await _send_json(
|
|
466
|
+
env, int(getattr(std, "status_code", 500) or 500), {"detail": detail}
|
|
467
|
+
)
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
await _send_transport_response(env, ctx)
|
|
471
|
+
|
|
472
|
+
def compile_plan(self, app: Any) -> KernelPlan:
|
|
473
|
+
from tigrbl_core._spec.binding_spec import (
|
|
474
|
+
HttpJsonRpcBindingSpec,
|
|
475
|
+
HttpRestBindingSpec,
|
|
476
|
+
WsBindingSpec,
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
proto_indices: dict[str, Any] = {"http.rest": {}, "https.rest": {}}
|
|
480
|
+
opmeta: list[OpMeta] = []
|
|
481
|
+
opkey_to_meta: dict[OpKey, int] = {}
|
|
482
|
+
phase_chains: dict[int, Mapping[str, list[StepFn]]] = {}
|
|
483
|
+
ingress_chain = self.build_ingress(app)
|
|
484
|
+
egress_chain = self.build_egress(app)
|
|
485
|
+
|
|
486
|
+
for model in _table_iter(app):
|
|
487
|
+
for sp in _opspecs(model):
|
|
488
|
+
meta_index = len(opmeta)
|
|
489
|
+
target = (getattr(sp, "target", sp.alias) or sp.alias).lower()
|
|
490
|
+
opmeta.append(OpMeta(model=model, alias=sp.alias, target=target))
|
|
491
|
+
phase_chains[meta_index] = deepmerge_phase_chains(
|
|
492
|
+
ingress_chain,
|
|
493
|
+
self.build_op(model, sp.alias),
|
|
494
|
+
egress_chain,
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
for binding in getattr(sp, "bindings", ()) or ():
|
|
498
|
+
if isinstance(binding, HttpRestBindingSpec):
|
|
499
|
+
for method in binding.methods:
|
|
500
|
+
selector = f"{method.upper()} {binding.path}"
|
|
501
|
+
opkey = OpKey(proto=binding.proto, selector=selector)
|
|
502
|
+
opkey_to_meta[opkey] = meta_index
|
|
503
|
+
proto_indices.setdefault(binding.proto, {})[selector] = (
|
|
504
|
+
meta_index
|
|
505
|
+
)
|
|
506
|
+
elif isinstance(binding, HttpJsonRpcBindingSpec):
|
|
507
|
+
opkey = OpKey(proto=binding.proto, selector=binding.rpc_method)
|
|
508
|
+
opkey_to_meta[opkey] = meta_index
|
|
509
|
+
proto_indices.setdefault(binding.proto, {})[
|
|
510
|
+
binding.rpc_method
|
|
511
|
+
] = meta_index
|
|
512
|
+
elif isinstance(binding, WsBindingSpec):
|
|
513
|
+
selector = binding.path
|
|
514
|
+
if binding.subprotocols:
|
|
515
|
+
for subprotocol in binding.subprotocols:
|
|
516
|
+
full_selector = f"{selector}|{subprotocol}"
|
|
517
|
+
opkey = OpKey(
|
|
518
|
+
proto=binding.proto, selector=full_selector
|
|
519
|
+
)
|
|
520
|
+
opkey_to_meta[opkey] = meta_index
|
|
521
|
+
proto_indices.setdefault(binding.proto, {})[
|
|
522
|
+
full_selector
|
|
523
|
+
] = meta_index
|
|
524
|
+
else:
|
|
525
|
+
opkey = OpKey(proto=binding.proto, selector=selector)
|
|
526
|
+
opkey_to_meta[opkey] = meta_index
|
|
527
|
+
proto_indices.setdefault(binding.proto, {})[selector] = (
|
|
528
|
+
meta_index
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return KernelPlan(
|
|
532
|
+
proto_indices=proto_indices,
|
|
533
|
+
opmeta=tuple(opmeta),
|
|
534
|
+
opkey_to_meta=opkey_to_meta,
|
|
535
|
+
ingress_chain=ingress_chain,
|
|
536
|
+
phase_chains=phase_chains,
|
|
537
|
+
egress_chain=egress_chain,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def compile_bootstrap_plan(self, app: Any) -> Dict[str, List[StepFn]]:
|
|
541
|
+
"""Compatibility entrypoint for ingress-only bootstrap diagnostics."""
|
|
542
|
+
return self.build_ingress(app)
|
|
543
|
+
|
|
544
|
+
def kernel_plan(self, app: Any) -> KernelPlan:
|
|
545
|
+
self.ensure_primed(app)
|
|
546
|
+
plan = self._kernel_plans.get(app)
|
|
547
|
+
if isinstance(plan, KernelPlan):
|
|
548
|
+
return plan
|
|
549
|
+
|
|
550
|
+
compiled = self.compile_plan(app)
|
|
551
|
+
self._kernel_plans[app] = compiled
|
|
552
|
+
|
|
553
|
+
payload: dict[str, dict[str, list[str]]] = {}
|
|
554
|
+
for model in _table_iter(app):
|
|
555
|
+
model_name = getattr(model, "__name__", str(model))
|
|
556
|
+
payload[model_name] = {}
|
|
557
|
+
for sp in _opspecs(model):
|
|
558
|
+
payload[model_name][sp.alias] = self.plan_labels(model, sp.alias)
|
|
559
|
+
self._kernelz_payload[app] = payload
|
|
560
|
+
|
|
561
|
+
return compiled
|
|
562
|
+
|
|
563
|
+
def kernelz_payload(self, app: Any) -> dict[str, dict[str, list[str]]]:
|
|
564
|
+
"""Thin accessor for endpoint: guarantees primed diagnostics payload."""
|
|
565
|
+
self.kernel_plan(app)
|
|
566
|
+
payload = self._kernelz_payload.get(app)
|
|
567
|
+
if isinstance(payload, dict):
|
|
568
|
+
return payload
|
|
569
|
+
return {}
|
|
570
|
+
|
|
571
|
+
def invalidate_kernelz_payload(self, app: Optional[Any] = None) -> None:
|
|
572
|
+
with self._lock:
|
|
573
|
+
if app is None:
|
|
574
|
+
self._kernel_plans = _WeakMaybeDict()
|
|
575
|
+
self._kernelz_payload = _WeakMaybeDict()
|
|
576
|
+
self._opviews = _WeakMaybeDict()
|
|
577
|
+
self._primed = _WeakMaybeDict()
|
|
578
|
+
else:
|
|
579
|
+
self._kernel_plans.pop(app, None)
|
|
580
|
+
self._kernelz_payload.pop(app, None)
|
|
581
|
+
self._opviews.pop(app, None)
|
|
582
|
+
self._primed.pop(app, None)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, Dict, Iterator, Mapping, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _label_callable(fn: Any) -> str:
|
|
8
|
+
name = getattr(fn, "__qualname__", getattr(fn, "__name__", repr(fn)))
|
|
9
|
+
module = getattr(fn, "__module__", None)
|
|
10
|
+
return f"{module}.{name}" if module else name
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _label_step(step: Any, phase: str) -> str:
|
|
14
|
+
label = getattr(step, "__tigrbl_label", None)
|
|
15
|
+
if isinstance(label, str) and "@" in label:
|
|
16
|
+
return label
|
|
17
|
+
module = getattr(step, "__module__", "") or ""
|
|
18
|
+
name = getattr(step, "__name__", "") or ""
|
|
19
|
+
if module.startswith("tigrbl_core.core.crud") and name:
|
|
20
|
+
return f"hook:wire:tigrbl:core:crud:ops:{name}@{phase}"
|
|
21
|
+
return f"hook:wire:{_label_callable(step).replace('.', ':')}@{phase}"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class SchemaIn:
|
|
26
|
+
fields: Tuple[str, ...]
|
|
27
|
+
by_field: Dict[str, Dict[str, object]]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class SchemaOut:
|
|
32
|
+
fields: Tuple[str, ...]
|
|
33
|
+
by_field: Dict[str, Dict[str, object]]
|
|
34
|
+
expose: Tuple[str, ...]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class OpView:
|
|
39
|
+
schema_in: SchemaIn
|
|
40
|
+
schema_out: SchemaOut
|
|
41
|
+
paired_index: Dict[str, Dict[str, object]]
|
|
42
|
+
virtual_producers: Dict[str, Callable[[object, dict], object]]
|
|
43
|
+
to_stored_transforms: Dict[str, Callable[[object, dict], object]]
|
|
44
|
+
refresh_hints: Tuple[str, ...]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class OpKey:
|
|
49
|
+
proto: str
|
|
50
|
+
selector: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True, slots=True)
|
|
54
|
+
class OpMeta:
|
|
55
|
+
model: type
|
|
56
|
+
alias: str
|
|
57
|
+
target: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, slots=True)
|
|
61
|
+
class KernelPlan:
|
|
62
|
+
proto_indices: Mapping[str, Any] = field(default_factory=dict)
|
|
63
|
+
opmeta: tuple[OpMeta, ...] = ()
|
|
64
|
+
opkey_to_meta: Mapping[OpKey, int] = field(default_factory=dict)
|
|
65
|
+
ingress_chain: Mapping[str, list[Callable[..., Any]]] = field(default_factory=dict)
|
|
66
|
+
phase_chains: Mapping[int, Mapping[str, list[Callable[..., Any]]]] = field(
|
|
67
|
+
default_factory=dict
|
|
68
|
+
)
|
|
69
|
+
egress_chain: Mapping[str, list[Callable[..., Any]]] = field(default_factory=dict)
|
|
70
|
+
_appspec_mapping: Dict[str, Dict[str, list[str]]] = field(
|
|
71
|
+
default_factory=dict, init=False, repr=False, compare=False
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def _normalize_mappings(self) -> Dict[str, Dict[str, list[str]]]:
|
|
75
|
+
if self._appspec_mapping:
|
|
76
|
+
return self._appspec_mapping
|
|
77
|
+
|
|
78
|
+
from tigrbl_runtime import events as _ev
|
|
79
|
+
|
|
80
|
+
normalized: Dict[str, Dict[str, list[str]]] = {}
|
|
81
|
+
for meta_index, meta in enumerate(self.opmeta):
|
|
82
|
+
table_name = getattr(meta.model, "__name__", str(meta.model))
|
|
83
|
+
labels: list[str] = []
|
|
84
|
+
chains = self.phase_chains.get(meta_index, {})
|
|
85
|
+
for phase in _ev.PHASES:
|
|
86
|
+
phase_steps = chains.get(phase, ())
|
|
87
|
+
for step in phase_steps or ():
|
|
88
|
+
labels.append(_label_step(step, phase))
|
|
89
|
+
|
|
90
|
+
seen, deduped = set(), []
|
|
91
|
+
for label in labels:
|
|
92
|
+
if ":hook:wire:" in label:
|
|
93
|
+
if label in seen:
|
|
94
|
+
continue
|
|
95
|
+
seen.add(label)
|
|
96
|
+
deduped.append(label)
|
|
97
|
+
|
|
98
|
+
normalized.setdefault(table_name, {})[meta.alias] = deduped
|
|
99
|
+
|
|
100
|
+
self._appspec_mapping.update(normalized)
|
|
101
|
+
return self._appspec_mapping
|
|
102
|
+
|
|
103
|
+
def __getitem__(self, key: str) -> Dict[str, list[str]]:
|
|
104
|
+
return self._normalize_mappings()[key]
|
|
105
|
+
|
|
106
|
+
def __iter__(self) -> Iterator[str]:
|
|
107
|
+
return iter(self._normalize_mappings())
|
|
108
|
+
|
|
109
|
+
def __len__(self) -> int:
|
|
110
|
+
return len(self._normalize_mappings())
|
|
111
|
+
|
|
112
|
+
def get(
|
|
113
|
+
self, key: str, default: Dict[str, list[str]] | None = None
|
|
114
|
+
) -> Dict[str, list[str]] | None:
|
|
115
|
+
return self._normalize_mappings().get(key, default)
|
|
116
|
+
|
|
117
|
+
def items(self):
|
|
118
|
+
return self._normalize_mappings().items()
|
|
119
|
+
|
|
120
|
+
def keys(self):
|
|
121
|
+
return self._normalize_mappings().keys()
|
|
122
|
+
|
|
123
|
+
def values(self):
|
|
124
|
+
return self._normalize_mappings().values()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping
|
|
4
|
+
|
|
5
|
+
from .models import OpView, SchemaIn, SchemaOut
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compile_opview_from_specs(specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
9
|
+
"""Build a basic OpView from collected specs when no app/model is present."""
|
|
10
|
+
alias = getattr(sp, "alias", "")
|
|
11
|
+
|
|
12
|
+
in_fields: list[str] = []
|
|
13
|
+
out_fields: list[str] = []
|
|
14
|
+
by_field_in: Dict[str, Dict[str, object]] = {}
|
|
15
|
+
by_field_out: Dict[str, Dict[str, object]] = {}
|
|
16
|
+
|
|
17
|
+
for name, spec in specs.items():
|
|
18
|
+
io = getattr(spec, "io", None)
|
|
19
|
+
fs = getattr(spec, "field", None)
|
|
20
|
+
storage = getattr(spec, "storage", None)
|
|
21
|
+
in_verbs = set(getattr(io, "in_verbs", ()) or ())
|
|
22
|
+
out_verbs = set(getattr(io, "out_verbs", ()) or ())
|
|
23
|
+
|
|
24
|
+
if alias in in_verbs:
|
|
25
|
+
in_fields.append(name)
|
|
26
|
+
meta: Dict[str, object] = {"in_enabled": True}
|
|
27
|
+
if storage is None:
|
|
28
|
+
meta["virtual"] = True
|
|
29
|
+
default_factory = getattr(spec, "default_factory", None)
|
|
30
|
+
if callable(default_factory):
|
|
31
|
+
meta["default_factory"] = default_factory
|
|
32
|
+
alias_in = getattr(io, "alias_in", None)
|
|
33
|
+
if alias_in:
|
|
34
|
+
meta["alias_in"] = alias_in
|
|
35
|
+
header_in = getattr(io, "header_in", None)
|
|
36
|
+
if header_in:
|
|
37
|
+
meta["header_in"] = header_in
|
|
38
|
+
meta["header_required_in"] = bool(
|
|
39
|
+
getattr(io, "header_required_in", False)
|
|
40
|
+
)
|
|
41
|
+
required = bool(fs and alias in getattr(fs, "required_in", ()))
|
|
42
|
+
meta["required"] = required
|
|
43
|
+
base_nullable = (
|
|
44
|
+
True if storage is None else getattr(storage, "nullable", True)
|
|
45
|
+
)
|
|
46
|
+
meta["nullable"] = base_nullable
|
|
47
|
+
by_field_in[name] = meta
|
|
48
|
+
|
|
49
|
+
if alias in out_verbs:
|
|
50
|
+
out_fields.append(name)
|
|
51
|
+
meta_out: Dict[str, object] = {}
|
|
52
|
+
alias_out = getattr(io, "alias_out", None)
|
|
53
|
+
if alias_out:
|
|
54
|
+
meta_out["alias_out"] = alias_out
|
|
55
|
+
if storage is None:
|
|
56
|
+
meta_out["virtual"] = True
|
|
57
|
+
py_type = getattr(getattr(fs, "py_type", None), "__name__", None)
|
|
58
|
+
if py_type:
|
|
59
|
+
meta_out["py_type"] = py_type
|
|
60
|
+
by_field_out[name] = meta_out
|
|
61
|
+
|
|
62
|
+
schema_in = SchemaIn(
|
|
63
|
+
fields=tuple(sorted(in_fields)),
|
|
64
|
+
by_field={field: by_field_in.get(field, {}) for field in sorted(in_fields)},
|
|
65
|
+
)
|
|
66
|
+
schema_out = SchemaOut(
|
|
67
|
+
fields=tuple(sorted(out_fields)),
|
|
68
|
+
by_field={field: by_field_out.get(field, {}) for field in sorted(out_fields)},
|
|
69
|
+
expose=tuple(sorted(out_fields)),
|
|
70
|
+
)
|
|
71
|
+
paired_index: Dict[str, Dict[str, object]] = {}
|
|
72
|
+
for field, col in specs.items():
|
|
73
|
+
io = getattr(col, "io", None)
|
|
74
|
+
cfg = getattr(io, "_paired", None)
|
|
75
|
+
if cfg and alias in getattr(cfg, "verbs", ()): # type: ignore[attr-defined]
|
|
76
|
+
field_spec = getattr(col, "field", None)
|
|
77
|
+
max_length = None
|
|
78
|
+
if field_spec is not None:
|
|
79
|
+
max_length = getattr(
|
|
80
|
+
getattr(field_spec, "constraints", {}),
|
|
81
|
+
"get",
|
|
82
|
+
lambda k, d=None: None,
|
|
83
|
+
)("max_length")
|
|
84
|
+
paired_index[field] = {
|
|
85
|
+
"alias": cfg.alias,
|
|
86
|
+
"gen": cfg.gen,
|
|
87
|
+
"store": cfg.store,
|
|
88
|
+
"mask_last": cfg.mask_last,
|
|
89
|
+
"max_length": max_length,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return OpView(
|
|
93
|
+
schema_in=schema_in,
|
|
94
|
+
schema_out=schema_out,
|
|
95
|
+
paired_index=paired_index,
|
|
96
|
+
virtual_producers={},
|
|
97
|
+
to_stored_transforms={},
|
|
98
|
+
refresh_hints=(),
|
|
99
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from tigrbl_runtime import events as _ev
|
|
6
|
+
from tigrbl_runtime.labels import label_hook
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _callable_label(fn: Any) -> str:
|
|
10
|
+
module = getattr(fn, "__module__", "") or ""
|
|
11
|
+
qualname = getattr(fn, "__qualname__", getattr(fn, "__name__", repr(fn)))
|
|
12
|
+
return f"{module}.{qualname}" if module else qualname
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .core import Kernel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _table_iter(app: Any):
|
|
20
|
+
tables = getattr(app, "tables", None)
|
|
21
|
+
if isinstance(tables, dict):
|
|
22
|
+
return tuple(v for v in tables.values() if isinstance(v, type))
|
|
23
|
+
return ()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _opspecs(model: type):
|
|
27
|
+
return getattr(getattr(model, "opspecs", object()), "all", ()) or ()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_kernelz_payload(kernel: "Kernel", app: Any):
|
|
31
|
+
payload: dict[str, dict[str, list[str]]] = {}
|
|
32
|
+
for model in _table_iter(app):
|
|
33
|
+
mname = getattr(model, "__name__", str(model))
|
|
34
|
+
payload[mname] = {}
|
|
35
|
+
for sp in _opspecs(model):
|
|
36
|
+
labels: list[str] = []
|
|
37
|
+
for dep in getattr(sp, "secdeps", ()) or ():
|
|
38
|
+
labels.append(
|
|
39
|
+
f"PRE_TX_BEGIN:hook:dep:security:{_callable_label(getattr(dep, 'dependency', dep))}"
|
|
40
|
+
)
|
|
41
|
+
for dep in getattr(sp, "deps", ()) or ():
|
|
42
|
+
labels.append(
|
|
43
|
+
f"PRE_TX_BEGIN:hook:dep:extra:{_callable_label(getattr(dep, 'dependency', dep))}"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
persist = getattr(sp, "persist", "default") != "skip"
|
|
47
|
+
if persist:
|
|
48
|
+
labels.append("START_TX:hook:sys:txn:begin@START_TX")
|
|
49
|
+
|
|
50
|
+
chains = kernel.build(model, sp.alias)
|
|
51
|
+
for phase in _ev.PHASES:
|
|
52
|
+
if phase in {"START_TX", "END_TX", "PRE_TX_BEGIN", "POST_RESPONSE"}:
|
|
53
|
+
continue
|
|
54
|
+
for step in chains.get(phase, ()) or ():
|
|
55
|
+
labels.append(f"{phase}:{label_hook(step, phase)}")
|
|
56
|
+
|
|
57
|
+
if persist:
|
|
58
|
+
labels.append("END_TX:hook:sys:txn:commit@END_TX")
|
|
59
|
+
|
|
60
|
+
for step in chains.get("POST_RESPONSE", ()) or ():
|
|
61
|
+
labels.append(f"POST_RESPONSE:{label_hook(step, 'POST_RESPONSE')}")
|
|
62
|
+
|
|
63
|
+
payload[mname][sp.alias] = labels
|
|
64
|
+
return payload
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tigrbl-kernel
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Kernel orchestration for Tigrbl runtime composition.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Keywords: tigrbl,sdk,standards,framework
|
|
7
|
+
Author: Jacob Stewart
|
|
8
|
+
Author-email: jacob@swarmauri.com
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Development Status :: 1 - Planning
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Requires-Dist: tigrbl-atoms
|
|
19
|
+
Requires-Dist: tigrbl-canon
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
# tigrbl-kernel
|
|
25
|
+
|
|
26
|
+
    
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- Modular package in the Tigrbl namespace.
|
|
31
|
+
- Supports Python 3.10 through 3.12.
|
|
32
|
+
- Distributed as part of the swarmauri-sdk workspace.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
### uv
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add tigrbl-kernel
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### pip
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install tigrbl-kernel
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
Import from the shared package-specific module namespaces after installation in your environment.
|
|
51
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
tigrbl_kernel/kernel/__init__.py,sha256=Vnd3I9czWwQAAdCCKPEuDdg3xnZafSPXruLaXCRkFOU,1215
|
|
2
|
+
tigrbl_kernel/kernel/atoms.py,sha256=aaTOixPocG1Hxn5JJ3Ww-vH63aWSyoKzidz0ojXwypk,7308
|
|
3
|
+
tigrbl_kernel/kernel/cache.py,sha256=IBhLUsHo8ASlnQXGGbw9MoQYaOpID7YBBcLNfIjA-uE,2606
|
|
4
|
+
tigrbl_kernel/kernel/core.py,sha256=bstWeuJYhNgucOkzH9BVgnQC7I5ZkN1L99pH2Gxx1VY,22142
|
|
5
|
+
tigrbl_kernel/kernel/models.py,sha256=l31lxLk4_0-pUyiHLTojYYhhlD6VuPLiC7-iIqKCfoc,3983
|
|
6
|
+
tigrbl_kernel/kernel/opview_compiler.py,sha256=KDBllYxk-gqyohLWMKbNSo135mKMzsQgeitYdJJJJ-o,3738
|
|
7
|
+
tigrbl_kernel/kernel/payload.py,sha256=ofg0ejCtO5VICsXxrfT2UiT9SaGHSrbr-G49avqCiLo,2236
|
|
8
|
+
tigrbl_kernel-0.1.0.dev1.dist-info/METADATA,sha256=kKOzoqErirX14gzGVLDNzhPtAR17QqAeRno_EzryjFE,1642
|
|
9
|
+
tigrbl_kernel-0.1.0.dev1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
10
|
+
tigrbl_kernel-0.1.0.dev1.dist-info/RECORD,,
|