tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +72 -0
- tigrbl/api/api_spec.py +30 -0
- tigrbl/api/mro_collect.py +43 -0
- tigrbl/api/shortcuts.py +56 -0
- tigrbl/api/tigrbl_api.py +286 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +61 -0
- tigrbl/app/app_spec.py +42 -0
- tigrbl/app/mro_collect.py +67 -0
- tigrbl/app/shortcuts.py +65 -0
- tigrbl/app/tigrbl_app.py +314 -0
- tigrbl/bindings/__init__.py +73 -0
- tigrbl/bindings/api/__init__.py +12 -0
- tigrbl/bindings/api/common.py +109 -0
- tigrbl/bindings/api/include.py +256 -0
- tigrbl/bindings/api/resource_proxy.py +149 -0
- tigrbl/bindings/api/rpc.py +111 -0
- tigrbl/bindings/columns.py +49 -0
- tigrbl/bindings/handlers/__init__.py +11 -0
- tigrbl/bindings/handlers/builder.py +119 -0
- tigrbl/bindings/handlers/ctx.py +74 -0
- tigrbl/bindings/handlers/identifiers.py +228 -0
- tigrbl/bindings/handlers/namespaces.py +51 -0
- tigrbl/bindings/handlers/steps.py +276 -0
- tigrbl/bindings/hooks.py +311 -0
- tigrbl/bindings/model.py +194 -0
- tigrbl/bindings/model_helpers.py +139 -0
- tigrbl/bindings/model_registry.py +77 -0
- tigrbl/bindings/rest/__init__.py +7 -0
- tigrbl/bindings/rest/attach.py +34 -0
- tigrbl/bindings/rest/collection.py +265 -0
- tigrbl/bindings/rest/common.py +116 -0
- tigrbl/bindings/rest/fastapi.py +76 -0
- tigrbl/bindings/rest/helpers.py +119 -0
- tigrbl/bindings/rest/io.py +317 -0
- tigrbl/bindings/rest/member.py +367 -0
- tigrbl/bindings/rest/router.py +292 -0
- tigrbl/bindings/rest/routing.py +133 -0
- tigrbl/bindings/rpc.py +364 -0
- tigrbl/bindings/schemas/__init__.py +11 -0
- tigrbl/bindings/schemas/builder.py +348 -0
- tigrbl/bindings/schemas/defaults.py +260 -0
- tigrbl/bindings/schemas/utils.py +193 -0
- tigrbl/column/README.md +62 -0
- tigrbl/column/__init__.py +72 -0
- tigrbl/column/_column.py +96 -0
- tigrbl/column/column_spec.py +40 -0
- tigrbl/column/field_spec.py +31 -0
- tigrbl/column/infer/__init__.py +25 -0
- tigrbl/column/infer/core.py +92 -0
- tigrbl/column/infer/jsonhints.py +44 -0
- tigrbl/column/infer/planning.py +133 -0
- tigrbl/column/infer/types.py +102 -0
- tigrbl/column/infer/utils.py +59 -0
- tigrbl/column/io_spec.py +133 -0
- tigrbl/column/mro_collect.py +59 -0
- tigrbl/column/shortcuts.py +89 -0
- tigrbl/column/storage_spec.py +65 -0
- tigrbl/config/__init__.py +19 -0
- tigrbl/config/constants.py +224 -0
- tigrbl/config/defaults.py +29 -0
- tigrbl/config/resolver.py +295 -0
- tigrbl/core/__init__.py +47 -0
- tigrbl/core/crud/__init__.py +36 -0
- tigrbl/core/crud/bulk.py +168 -0
- tigrbl/core/crud/helpers/__init__.py +76 -0
- tigrbl/core/crud/helpers/db.py +92 -0
- tigrbl/core/crud/helpers/enum.py +86 -0
- tigrbl/core/crud/helpers/filters.py +162 -0
- tigrbl/core/crud/helpers/model.py +123 -0
- tigrbl/core/crud/helpers/normalize.py +99 -0
- tigrbl/core/crud/ops.py +235 -0
- tigrbl/ddl/__init__.py +344 -0
- tigrbl/decorators.py +17 -0
- tigrbl/deps/__init__.py +20 -0
- tigrbl/deps/fastapi.py +45 -0
- tigrbl/deps/favicon.svg +4 -0
- tigrbl/deps/jinja.py +27 -0
- tigrbl/deps/pydantic.py +10 -0
- tigrbl/deps/sqlalchemy.py +94 -0
- tigrbl/deps/starlette.py +36 -0
- tigrbl/engine/__init__.py +26 -0
- tigrbl/engine/_engine.py +130 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +108 -0
- tigrbl/engine/engine_spec.py +261 -0
- tigrbl/engine/resolver.py +224 -0
- tigrbl/engine/shortcuts.py +216 -0
- tigrbl/hook/__init__.py +21 -0
- tigrbl/hook/_hook.py +22 -0
- tigrbl/hook/decorators.py +28 -0
- tigrbl/hook/hook_spec.py +24 -0
- tigrbl/hook/mro_collect.py +98 -0
- tigrbl/hook/shortcuts.py +44 -0
- tigrbl/hook/types.py +76 -0
- tigrbl/op/__init__.py +50 -0
- tigrbl/op/_op.py +31 -0
- tigrbl/op/canonical.py +31 -0
- tigrbl/op/collect.py +11 -0
- tigrbl/op/decorators.py +238 -0
- tigrbl/op/model_registry.py +301 -0
- tigrbl/op/mro_collect.py +99 -0
- tigrbl/op/resolver.py +216 -0
- tigrbl/op/types.py +136 -0
- tigrbl/orm/__init__.py +1 -0
- tigrbl/orm/mixins/_RowBound.py +83 -0
- tigrbl/orm/mixins/__init__.py +95 -0
- tigrbl/orm/mixins/bootstrappable.py +113 -0
- tigrbl/orm/mixins/bound.py +47 -0
- tigrbl/orm/mixins/edges.py +40 -0
- tigrbl/orm/mixins/fields.py +165 -0
- tigrbl/orm/mixins/hierarchy.py +54 -0
- tigrbl/orm/mixins/key_digest.py +44 -0
- tigrbl/orm/mixins/lifecycle.py +115 -0
- tigrbl/orm/mixins/locks.py +51 -0
- tigrbl/orm/mixins/markers.py +16 -0
- tigrbl/orm/mixins/operations.py +57 -0
- tigrbl/orm/mixins/ownable.py +337 -0
- tigrbl/orm/mixins/principals.py +98 -0
- tigrbl/orm/mixins/tenant_bound.py +301 -0
- tigrbl/orm/mixins/upsertable.py +111 -0
- tigrbl/orm/mixins/utils.py +49 -0
- tigrbl/orm/tables/__init__.py +72 -0
- tigrbl/orm/tables/_base.py +8 -0
- tigrbl/orm/tables/audit.py +56 -0
- tigrbl/orm/tables/client.py +25 -0
- tigrbl/orm/tables/group.py +29 -0
- tigrbl/orm/tables/org.py +30 -0
- tigrbl/orm/tables/rbac.py +76 -0
- tigrbl/orm/tables/status.py +106 -0
- tigrbl/orm/tables/tenant.py +22 -0
- tigrbl/orm/tables/user.py +39 -0
- tigrbl/response/README.md +34 -0
- tigrbl/response/__init__.py +33 -0
- tigrbl/response/bind.py +12 -0
- tigrbl/response/decorators.py +37 -0
- tigrbl/response/resolver.py +83 -0
- tigrbl/response/shortcuts.py +144 -0
- tigrbl/response/types.py +49 -0
- tigrbl/rest/__init__.py +27 -0
- tigrbl/runtime/README.md +129 -0
- tigrbl/runtime/__init__.py +20 -0
- tigrbl/runtime/atoms/__init__.py +102 -0
- tigrbl/runtime/atoms/emit/__init__.py +42 -0
- tigrbl/runtime/atoms/emit/paired_post.py +158 -0
- tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
- tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
- tigrbl/runtime/atoms/out/__init__.py +38 -0
- tigrbl/runtime/atoms/out/masking.py +135 -0
- tigrbl/runtime/atoms/refresh/__init__.py +38 -0
- tigrbl/runtime/atoms/refresh/demand.py +130 -0
- tigrbl/runtime/atoms/resolve/__init__.py +40 -0
- tigrbl/runtime/atoms/resolve/assemble.py +167 -0
- tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
- tigrbl/runtime/atoms/response/__init__.py +17 -0
- tigrbl/runtime/atoms/response/negotiate.py +30 -0
- tigrbl/runtime/atoms/response/negotiation.py +43 -0
- tigrbl/runtime/atoms/response/render.py +36 -0
- tigrbl/runtime/atoms/response/renderer.py +116 -0
- tigrbl/runtime/atoms/response/template.py +44 -0
- tigrbl/runtime/atoms/response/templates.py +88 -0
- tigrbl/runtime/atoms/schema/__init__.py +40 -0
- tigrbl/runtime/atoms/schema/collect_in.py +21 -0
- tigrbl/runtime/atoms/schema/collect_out.py +21 -0
- tigrbl/runtime/atoms/storage/__init__.py +38 -0
- tigrbl/runtime/atoms/storage/to_stored.py +167 -0
- tigrbl/runtime/atoms/wire/__init__.py +45 -0
- tigrbl/runtime/atoms/wire/build_in.py +166 -0
- tigrbl/runtime/atoms/wire/build_out.py +87 -0
- tigrbl/runtime/atoms/wire/dump.py +206 -0
- tigrbl/runtime/atoms/wire/validate_in.py +227 -0
- tigrbl/runtime/context.py +206 -0
- tigrbl/runtime/errors/__init__.py +61 -0
- tigrbl/runtime/errors/converters.py +214 -0
- tigrbl/runtime/errors/exceptions.py +124 -0
- tigrbl/runtime/errors/mappings.py +71 -0
- tigrbl/runtime/errors/utils.py +150 -0
- tigrbl/runtime/events.py +209 -0
- tigrbl/runtime/executor/__init__.py +6 -0
- tigrbl/runtime/executor/guards.py +132 -0
- tigrbl/runtime/executor/helpers.py +88 -0
- tigrbl/runtime/executor/invoke.py +150 -0
- tigrbl/runtime/executor/types.py +84 -0
- tigrbl/runtime/kernel.py +628 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +87 -0
- tigrbl/runtime/ordering.py +256 -0
- tigrbl/runtime/system.py +279 -0
- tigrbl/runtime/trace.py +330 -0
- tigrbl/schema/__init__.py +38 -0
- tigrbl/schema/_schema.py +27 -0
- tigrbl/schema/builder/__init__.py +17 -0
- tigrbl/schema/builder/build_schema.py +209 -0
- tigrbl/schema/builder/cache.py +24 -0
- tigrbl/schema/builder/compat.py +16 -0
- tigrbl/schema/builder/extras.py +85 -0
- tigrbl/schema/builder/helpers.py +51 -0
- tigrbl/schema/builder/list_params.py +117 -0
- tigrbl/schema/builder/strip_parent_fields.py +70 -0
- tigrbl/schema/collect.py +55 -0
- tigrbl/schema/decorators.py +68 -0
- tigrbl/schema/get_schema.py +86 -0
- tigrbl/schema/schema_spec.py +20 -0
- tigrbl/schema/shortcuts.py +42 -0
- tigrbl/schema/types.py +34 -0
- tigrbl/schema/utils.py +143 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +12 -0
- tigrbl/system/diagnostics/__init__.py +24 -0
- tigrbl/system/diagnostics/compat.py +31 -0
- tigrbl/system/diagnostics/healthz.py +41 -0
- tigrbl/system/diagnostics/hookz.py +51 -0
- tigrbl/system/diagnostics/kernelz.py +20 -0
- tigrbl/system/diagnostics/methodz.py +43 -0
- tigrbl/system/diagnostics/router.py +73 -0
- tigrbl/system/diagnostics/utils.py +43 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +237 -0
- tigrbl/table/_table.py +54 -0
- tigrbl/table/mro_collect.py +69 -0
- tigrbl/table/shortcuts.py +57 -0
- tigrbl/table/table_spec.py +28 -0
- tigrbl/transport/__init__.py +74 -0
- tigrbl/transport/jsonrpc/__init__.py +19 -0
- tigrbl/transport/jsonrpc/dispatcher.py +352 -0
- tigrbl/transport/jsonrpc/helpers.py +115 -0
- tigrbl/transport/jsonrpc/models.py +41 -0
- tigrbl/transport/rest/__init__.py +25 -0
- tigrbl/transport/rest/aggregator.py +132 -0
- tigrbl/types/__init__.py +174 -0
- tigrbl/types/allow_anon_provider.py +19 -0
- tigrbl/types/authn_abc.py +30 -0
- tigrbl/types/nested_path_provider.py +22 -0
- tigrbl/types/op.py +35 -0
- tigrbl/types/op_config_provider.py +17 -0
- tigrbl/types/op_verb_alias_provider.py +33 -0
- tigrbl/types/request_extras_provider.py +22 -0
- tigrbl/types/response_extras_provider.py +22 -0
- tigrbl/types/table_config_provider.py +13 -0
- tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
- tigrbl/ExampleAgent.py +0 -1
- tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
- tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
tigrbl/runtime/kernel.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import pkgutil
|
|
6
|
+
import threading
|
|
7
|
+
import weakref
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from types import SimpleNamespace
|
|
11
|
+
from typing import (
|
|
12
|
+
Any,
|
|
13
|
+
Callable,
|
|
14
|
+
Dict,
|
|
15
|
+
Iterable,
|
|
16
|
+
List,
|
|
17
|
+
Mapping,
|
|
18
|
+
Optional,
|
|
19
|
+
Sequence,
|
|
20
|
+
Tuple,
|
|
21
|
+
cast,
|
|
22
|
+
Generic,
|
|
23
|
+
TypeVar,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .executor import _invoke, _Ctx
|
|
27
|
+
from . import events as _ev, ordering as _ordering, system as _sys
|
|
28
|
+
from ..op.types import PHASES, StepFn
|
|
29
|
+
from ..column.mro_collect import mro_collect_columns
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
K = TypeVar("K")
|
|
35
|
+
V = TypeVar("V")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _WeakMaybeDict(Generic[K, V]):
|
|
39
|
+
"""Dictionary that uses weak references when possible.
|
|
40
|
+
|
|
41
|
+
Falls back to strong references when ``key`` cannot be weakly referenced.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self._weak: "weakref.WeakKeyDictionary[Any, V]" = weakref.WeakKeyDictionary()
|
|
46
|
+
self._strong: Dict[int, tuple[Any, V]] = {}
|
|
47
|
+
|
|
48
|
+
def _use_weak(self, key: Any) -> bool:
|
|
49
|
+
try:
|
|
50
|
+
weakref.ref(key)
|
|
51
|
+
return True
|
|
52
|
+
except TypeError:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def __setitem__(self, key: K, value: V) -> None:
|
|
56
|
+
if self._use_weak(key):
|
|
57
|
+
self._weak[key] = value
|
|
58
|
+
else:
|
|
59
|
+
self._strong[id(key)] = (key, value)
|
|
60
|
+
|
|
61
|
+
def __getitem__(self, key: K) -> V:
|
|
62
|
+
if self._use_weak(key):
|
|
63
|
+
return self._weak[key]
|
|
64
|
+
return self._strong[id(key)][1]
|
|
65
|
+
|
|
66
|
+
def get(self, key: K, default: Optional[V] = None) -> Optional[V]:
|
|
67
|
+
if self._use_weak(key):
|
|
68
|
+
return self._weak.get(key, default)
|
|
69
|
+
return self._strong.get(id(key), (None, default))[1]
|
|
70
|
+
|
|
71
|
+
def setdefault(self, key: K, default: V) -> V:
|
|
72
|
+
if self._use_weak(key):
|
|
73
|
+
return self._weak.setdefault(key, default)
|
|
74
|
+
return self._strong.setdefault(id(key), (key, default))[1]
|
|
75
|
+
|
|
76
|
+
def pop(self, key: K, default: Optional[V] = None) -> Optional[V]:
|
|
77
|
+
if self._use_weak(key):
|
|
78
|
+
return self._weak.pop(key, default)
|
|
79
|
+
return self._strong.pop(id(key), (None, default))[1]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---- OpView shapes -------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class SchemaIn:
|
|
87
|
+
fields: Tuple[str, ...]
|
|
88
|
+
by_field: Dict[str, Dict[str, object]]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class SchemaOut:
|
|
93
|
+
fields: Tuple[str, ...]
|
|
94
|
+
by_field: Dict[str, Dict[str, object]]
|
|
95
|
+
expose: Tuple[str, ...]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class OpView:
|
|
100
|
+
schema_in: SchemaIn
|
|
101
|
+
schema_out: SchemaOut
|
|
102
|
+
paired_index: Dict[str, Dict[str, object]]
|
|
103
|
+
virtual_producers: Dict[str, Callable[[object, dict], object]]
|
|
104
|
+
to_stored_transforms: Dict[str, Callable[[object, dict], object]]
|
|
105
|
+
refresh_hints: Tuple[str, ...]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ──────────────────────────── Discovery ────────────────────────────
|
|
109
|
+
|
|
110
|
+
_AtomRun = Callable[[Optional[object], Any], Any]
|
|
111
|
+
_DiscoveredAtom = tuple[str, _AtomRun]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _discover_atoms() -> list[_DiscoveredAtom]:
|
|
115
|
+
out: list[_DiscoveredAtom] = []
|
|
116
|
+
try:
|
|
117
|
+
import tigrbl.runtime.atoms as atoms_pkg # type: ignore
|
|
118
|
+
except Exception:
|
|
119
|
+
return out
|
|
120
|
+
for info in pkgutil.walk_packages(atoms_pkg.__path__, atoms_pkg.__name__ + "."): # type: ignore[attr-defined]
|
|
121
|
+
if info.ispkg:
|
|
122
|
+
continue
|
|
123
|
+
try:
|
|
124
|
+
mod = importlib.import_module(info.name)
|
|
125
|
+
anchor = getattr(mod, "ANCHOR", None)
|
|
126
|
+
run = getattr(mod, "run", None)
|
|
127
|
+
if isinstance(anchor, str) and callable(run):
|
|
128
|
+
out.append((anchor, run))
|
|
129
|
+
except Exception:
|
|
130
|
+
continue
|
|
131
|
+
logger.debug("kernel: discovered %d atoms", len(out))
|
|
132
|
+
return out
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ──────────────────────────── Labeling ─────────────────────────────
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _infer_domain_subject(run: _AtomRun) -> tuple[Optional[str], Optional[str]]:
|
|
139
|
+
mod = getattr(run, "__module__", "") or ""
|
|
140
|
+
parts = mod.split(".")
|
|
141
|
+
try:
|
|
142
|
+
i = parts.index("atoms")
|
|
143
|
+
return (
|
|
144
|
+
parts[i + 1] if i + 1 < len(parts) else None,
|
|
145
|
+
parts[i + 2] if i + 2 < len(parts) else None,
|
|
146
|
+
)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return None, None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _make_label(anchor: str, run: _AtomRun) -> Optional[str]:
|
|
152
|
+
d, s = _infer_domain_subject(run)
|
|
153
|
+
return f"atom:{d}:{s}@{anchor}" if (d and s) else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _wrap_atom(run: _AtomRun, *, anchor: str) -> StepFn:
|
|
157
|
+
async def _step(ctx: Any) -> Any:
|
|
158
|
+
rv = run(None, ctx)
|
|
159
|
+
if hasattr(rv, "__await__"):
|
|
160
|
+
return await cast(Any, rv)
|
|
161
|
+
return rv
|
|
162
|
+
|
|
163
|
+
label = _make_label(anchor, run)
|
|
164
|
+
if label:
|
|
165
|
+
setattr(_step, "__tigrbl_label", label)
|
|
166
|
+
return _step
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ──────────────────────────── Specs cache (once) ────────────────────
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _SpecsOnceCache:
|
|
173
|
+
"""Thread-safe, compute-once cache of per-model column specs."""
|
|
174
|
+
|
|
175
|
+
def __init__(self) -> None:
|
|
176
|
+
self._d: Dict[type, Mapping[str, Any]] = {}
|
|
177
|
+
self._lock = threading.Lock()
|
|
178
|
+
|
|
179
|
+
def get(self, model: type) -> Mapping[str, Any]:
|
|
180
|
+
try:
|
|
181
|
+
return self._d[model]
|
|
182
|
+
except KeyError:
|
|
183
|
+
pass
|
|
184
|
+
with self._lock:
|
|
185
|
+
rv = self._d.get(model)
|
|
186
|
+
if rv is None:
|
|
187
|
+
rv = mro_collect_columns(model)
|
|
188
|
+
self._d[model] = rv
|
|
189
|
+
return rv
|
|
190
|
+
|
|
191
|
+
def prime(self, models: Sequence[type]) -> None:
|
|
192
|
+
for m in models:
|
|
193
|
+
self.get(m)
|
|
194
|
+
|
|
195
|
+
def invalidate(self, model: Optional[type] = None) -> None:
|
|
196
|
+
with self._lock:
|
|
197
|
+
if model is None:
|
|
198
|
+
self._d.clear()
|
|
199
|
+
else:
|
|
200
|
+
self._d.pop(model, None)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ──────────────────────────── Hooks & Phases ────────────────────────
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _hook_phase_chains(model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
207
|
+
hooks_root = getattr(model, "hooks", None) or SimpleNamespace()
|
|
208
|
+
alias_ns = getattr(hooks_root, alias, None)
|
|
209
|
+
out: Dict[str, List[StepFn]] = {ph: [] for ph in PHASES}
|
|
210
|
+
if alias_ns is None:
|
|
211
|
+
return out
|
|
212
|
+
for ph in PHASES:
|
|
213
|
+
out[ph] = list(getattr(alias_ns, ph, []) or [])
|
|
214
|
+
return out
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_persistent(chains: Mapping[str, Sequence[StepFn]]) -> bool:
|
|
218
|
+
for fn in chains.get("START_TX", ()) or ():
|
|
219
|
+
if getattr(fn, "__name__", "") == "start_tx":
|
|
220
|
+
return True
|
|
221
|
+
for fn in chains.get("PRE_TX_BEGIN", ()) or ():
|
|
222
|
+
if getattr(fn, "__name__", "") == "mark_skip_persist":
|
|
223
|
+
return False
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _inject_atoms(
|
|
228
|
+
chains: Dict[str, List[StepFn]],
|
|
229
|
+
atoms: Iterable[_DiscoveredAtom],
|
|
230
|
+
*,
|
|
231
|
+
persistent: bool,
|
|
232
|
+
) -> None:
|
|
233
|
+
order = {name: i for i, name in enumerate(_ev.all_events_ordered())}
|
|
234
|
+
|
|
235
|
+
def _sort_key(item: _DiscoveredAtom) -> tuple[int, int]:
|
|
236
|
+
anchor, run = item
|
|
237
|
+
anchor_idx = order.get(anchor, 10_000)
|
|
238
|
+
d, s = _infer_domain_subject(run)
|
|
239
|
+
token = f"{d}:{s}" if d and s else ""
|
|
240
|
+
pref = _ordering._PREF.get(anchor, ())
|
|
241
|
+
token_idx = pref.index(token) if token in pref else 10_000
|
|
242
|
+
return anchor_idx, token_idx
|
|
243
|
+
|
|
244
|
+
for anchor, run in sorted(atoms, key=_sort_key):
|
|
245
|
+
try:
|
|
246
|
+
info = _ev.get_anchor_info(anchor)
|
|
247
|
+
except Exception:
|
|
248
|
+
continue
|
|
249
|
+
if info.phase in ("START_TX", "END_TX"):
|
|
250
|
+
continue
|
|
251
|
+
if not persistent and info.persist_tied:
|
|
252
|
+
continue
|
|
253
|
+
chains.setdefault(info.phase, []).append(_wrap_atom(run, anchor=anchor))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# ───────────────────────────── Kernel ──────────────────────────────
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Kernel:
|
|
260
|
+
"""
|
|
261
|
+
SSoT for runtime scheduling. One Kernel per App (not per API).
|
|
262
|
+
Auto-primed under the hood. Downstream users never touch this.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, atoms: Optional[Sequence[_DiscoveredAtom]] = None):
|
|
266
|
+
self._atoms_cache: Optional[list[_DiscoveredAtom]] = (
|
|
267
|
+
list(atoms) if atoms else None
|
|
268
|
+
)
|
|
269
|
+
self._specs_cache = _SpecsOnceCache()
|
|
270
|
+
self._opviews: _WeakMaybeDict[Any, Dict[Tuple[type, str], OpView]] = (
|
|
271
|
+
_WeakMaybeDict()
|
|
272
|
+
)
|
|
273
|
+
self._kernelz_payload: _WeakMaybeDict[Any, Dict[str, Dict[str, List[str]]]] = (
|
|
274
|
+
_WeakMaybeDict()
|
|
275
|
+
)
|
|
276
|
+
self._primed: _WeakMaybeDict[Any, bool] = _WeakMaybeDict()
|
|
277
|
+
self._lock = threading.Lock()
|
|
278
|
+
|
|
279
|
+
# ——— atoms ———
|
|
280
|
+
def _atoms(self) -> list[_DiscoveredAtom]:
|
|
281
|
+
if self._atoms_cache is None:
|
|
282
|
+
self._atoms_cache = _discover_atoms()
|
|
283
|
+
return self._atoms_cache
|
|
284
|
+
|
|
285
|
+
# ——— specs cache (maintainers only; used internally) ———
|
|
286
|
+
def get_specs(self, model: type) -> Mapping[str, Any]:
|
|
287
|
+
return self._specs_cache.get(model)
|
|
288
|
+
|
|
289
|
+
def prime_specs(self, models: Sequence[type]) -> None:
|
|
290
|
+
self._specs_cache.prime(models)
|
|
291
|
+
|
|
292
|
+
def invalidate_specs(self, model: Optional[type] = None) -> None:
|
|
293
|
+
self._specs_cache.invalidate(model)
|
|
294
|
+
|
|
295
|
+
# ——— build / plan ———
|
|
296
|
+
def build(self, model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
297
|
+
chains = _hook_phase_chains(model, alias)
|
|
298
|
+
specs = getattr(getattr(model, "ops", SimpleNamespace()), "by_alias", {})
|
|
299
|
+
sp_list = specs.get(alias) or ()
|
|
300
|
+
sp = sp_list[0] if sp_list else None
|
|
301
|
+
target = (getattr(sp, "target", alias) or "").lower()
|
|
302
|
+
persist_policy = getattr(sp, "persist", "default")
|
|
303
|
+
persistent = (
|
|
304
|
+
persist_policy != "skip" and target not in {"read", "list"}
|
|
305
|
+
) or _is_persistent(chains)
|
|
306
|
+
try:
|
|
307
|
+
_inject_atoms(chains, self._atoms() or (), persistent=persistent)
|
|
308
|
+
except Exception:
|
|
309
|
+
logger.exception(
|
|
310
|
+
"kernel: atom injection failed for %s.%s",
|
|
311
|
+
getattr(model, "__name__", model),
|
|
312
|
+
alias,
|
|
313
|
+
)
|
|
314
|
+
if persistent:
|
|
315
|
+
try:
|
|
316
|
+
start_anchor, start_run = _sys.get("txn", "begin")
|
|
317
|
+
end_anchor, end_run = _sys.get("txn", "commit")
|
|
318
|
+
chains.setdefault(start_anchor, []).append(
|
|
319
|
+
_wrap_atom(start_run, anchor=start_anchor)
|
|
320
|
+
)
|
|
321
|
+
chains.setdefault(end_anchor, []).append(
|
|
322
|
+
_wrap_atom(end_run, anchor=end_anchor)
|
|
323
|
+
)
|
|
324
|
+
except Exception:
|
|
325
|
+
logger.exception(
|
|
326
|
+
"kernel: failed to inject txn system steps for %s.%s",
|
|
327
|
+
getattr(model, "__name__", model),
|
|
328
|
+
alias,
|
|
329
|
+
)
|
|
330
|
+
for ph in PHASES:
|
|
331
|
+
chains.setdefault(ph, [])
|
|
332
|
+
return chains
|
|
333
|
+
|
|
334
|
+
def plan_labels(self, model: type, alias: str) -> list[str]:
|
|
335
|
+
labels: list[str] = []
|
|
336
|
+
chains = self.build(model, alias)
|
|
337
|
+
ordered_anchors = _ev.all_events_ordered()
|
|
338
|
+
phase_for = {a: _ev.get_anchor_info(a).phase for a in ordered_anchors}
|
|
339
|
+
for anchor in ordered_anchors:
|
|
340
|
+
phase = phase_for[anchor]
|
|
341
|
+
for step in chains.get(phase, []) or []:
|
|
342
|
+
lbl = getattr(step, "__tigrbl_label", None)
|
|
343
|
+
if isinstance(lbl, str) and lbl.endswith(f"@{anchor}"):
|
|
344
|
+
labels.append(lbl)
|
|
345
|
+
return labels
|
|
346
|
+
|
|
347
|
+
async def invoke(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
model: type,
|
|
351
|
+
alias: str,
|
|
352
|
+
db: Any,
|
|
353
|
+
request: Any | None = None,
|
|
354
|
+
ctx: Optional[Mapping[str, Any]] = None,
|
|
355
|
+
) -> Any:
|
|
356
|
+
"""Execute an operation for ``model.alias`` using the executor."""
|
|
357
|
+
phases = self.build(model, alias)
|
|
358
|
+
base_ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
359
|
+
base_ctx.model = model
|
|
360
|
+
base_ctx.op = alias
|
|
361
|
+
specs = self.get_specs(model)
|
|
362
|
+
base_ctx.opview = self._compile_opview_from_specs(
|
|
363
|
+
specs, SimpleNamespace(alias=alias)
|
|
364
|
+
)
|
|
365
|
+
return await _invoke(request=request, db=db, phases=phases, ctx=base_ctx)
|
|
366
|
+
|
|
367
|
+
# ——— per-App autoprime (hidden) ———
|
|
368
|
+
def ensure_primed(self, app: Any) -> None:
|
|
369
|
+
"""Autoprime once per App: specs → OpViews → /kernelz payload."""
|
|
370
|
+
with self._lock:
|
|
371
|
+
if self._primed.get(app):
|
|
372
|
+
return
|
|
373
|
+
from ..system.diagnostics.utils import (
|
|
374
|
+
model_iter as _model_iter,
|
|
375
|
+
opspecs as _opspecs,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
models = list(_model_iter(app))
|
|
379
|
+
|
|
380
|
+
# 1) per-model specs once
|
|
381
|
+
for m in models:
|
|
382
|
+
self._specs_cache.get(m)
|
|
383
|
+
|
|
384
|
+
# 2) compile OpViews per (model, alias)
|
|
385
|
+
ov_map: Dict[Tuple[type, str], OpView] = {}
|
|
386
|
+
for m in models:
|
|
387
|
+
specs = self._specs_cache.get(m)
|
|
388
|
+
for sp in _opspecs(m):
|
|
389
|
+
ov_map[(m, sp.alias)] = self._compile_opview_from_specs(specs, sp)
|
|
390
|
+
self._opviews[app] = ov_map
|
|
391
|
+
|
|
392
|
+
# 3) build /kernelz payload once (dedup wire hooks)
|
|
393
|
+
payload = self._build_kernelz_payload_internal(app)
|
|
394
|
+
self._kernelz_payload[app] = payload
|
|
395
|
+
self._primed[app] = True
|
|
396
|
+
|
|
397
|
+
def get_opview(self, app: Any, model: type, alias: str) -> OpView:
|
|
398
|
+
"""Return OpView for (model, alias); compile on-demand if missing."""
|
|
399
|
+
self.ensure_primed(app)
|
|
400
|
+
|
|
401
|
+
ov_map: Dict[Tuple[type, str], OpView] = self._opviews.setdefault(app, {})
|
|
402
|
+
ov = ov_map.get((model, alias))
|
|
403
|
+
if ov is not None:
|
|
404
|
+
return ov
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
specs = self._specs_cache.get(model)
|
|
408
|
+
from types import SimpleNamespace
|
|
409
|
+
from ..system.diagnostics.utils import opspecs as _opspecs
|
|
410
|
+
|
|
411
|
+
found = False
|
|
412
|
+
for sp in _opspecs(model):
|
|
413
|
+
ov_map.setdefault(
|
|
414
|
+
(model, sp.alias), self._compile_opview_from_specs(specs, sp)
|
|
415
|
+
)
|
|
416
|
+
if sp.alias == alias:
|
|
417
|
+
found = True
|
|
418
|
+
|
|
419
|
+
if not found:
|
|
420
|
+
temp_sp = SimpleNamespace(alias=alias)
|
|
421
|
+
ov_map[(model, alias)] = self._compile_opview_from_specs(specs, temp_sp)
|
|
422
|
+
|
|
423
|
+
return ov_map[(model, alias)]
|
|
424
|
+
except Exception:
|
|
425
|
+
raise RuntimeError(
|
|
426
|
+
f"opview_missing: app={app!r} model={getattr(model, '__name__', model)!r} alias={alias!r}"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def kernelz_payload(self, app: Any) -> Dict[str, Dict[str, List[str]]]:
|
|
430
|
+
"""Thin accessor for endpoint: guarantees primed, returns cached payload."""
|
|
431
|
+
self.ensure_primed(app)
|
|
432
|
+
return self._kernelz_payload[app]
|
|
433
|
+
|
|
434
|
+
def invalidate_kernelz_payload(self, app: Optional[Any] = None) -> None:
|
|
435
|
+
with self._lock:
|
|
436
|
+
if app is None:
|
|
437
|
+
self._kernelz_payload = _WeakMaybeDict()
|
|
438
|
+
self._opviews = _WeakMaybeDict()
|
|
439
|
+
self._primed = _WeakMaybeDict()
|
|
440
|
+
else:
|
|
441
|
+
self._kernelz_payload.pop(app, None)
|
|
442
|
+
self._opviews.pop(app, None)
|
|
443
|
+
self._primed.pop(app, None)
|
|
444
|
+
|
|
445
|
+
def _compile_opview_from_specs(self, specs: Mapping[str, Any], sp: Any) -> OpView:
|
|
446
|
+
"""Build a basic OpView from collected specs when no app/model is present."""
|
|
447
|
+
alias = getattr(sp, "alias", "")
|
|
448
|
+
|
|
449
|
+
in_fields: list[str] = []
|
|
450
|
+
out_fields: list[str] = []
|
|
451
|
+
by_field_in: Dict[str, Dict[str, object]] = {}
|
|
452
|
+
by_field_out: Dict[str, Dict[str, object]] = {}
|
|
453
|
+
|
|
454
|
+
for name, spec in specs.items():
|
|
455
|
+
io = getattr(spec, "io", None)
|
|
456
|
+
fs = getattr(spec, "field", None)
|
|
457
|
+
storage = getattr(spec, "storage", None)
|
|
458
|
+
in_verbs = set(getattr(io, "in_verbs", ()) or ())
|
|
459
|
+
out_verbs = set(getattr(io, "out_verbs", ()) or ())
|
|
460
|
+
|
|
461
|
+
if alias in in_verbs:
|
|
462
|
+
in_fields.append(name)
|
|
463
|
+
meta: Dict[str, object] = {"in_enabled": True}
|
|
464
|
+
if storage is None:
|
|
465
|
+
meta["virtual"] = True
|
|
466
|
+
df = getattr(spec, "default_factory", None)
|
|
467
|
+
if callable(df):
|
|
468
|
+
meta["default_factory"] = df
|
|
469
|
+
alias_in = getattr(io, "alias_in", None)
|
|
470
|
+
if alias_in:
|
|
471
|
+
meta["alias_in"] = alias_in
|
|
472
|
+
required = bool(fs and alias in getattr(fs, "required_in", ()))
|
|
473
|
+
meta["required"] = required
|
|
474
|
+
base_nullable = (
|
|
475
|
+
True if storage is None else getattr(storage, "nullable", True)
|
|
476
|
+
)
|
|
477
|
+
meta["nullable"] = base_nullable
|
|
478
|
+
by_field_in[name] = meta
|
|
479
|
+
|
|
480
|
+
if alias in out_verbs:
|
|
481
|
+
out_fields.append(name)
|
|
482
|
+
meta_out: Dict[str, object] = {}
|
|
483
|
+
alias_out = getattr(io, "alias_out", None)
|
|
484
|
+
if alias_out:
|
|
485
|
+
meta_out["alias_out"] = alias_out
|
|
486
|
+
if storage is None:
|
|
487
|
+
meta_out["virtual"] = True
|
|
488
|
+
py_t = getattr(getattr(fs, "py_type", None), "__name__", None)
|
|
489
|
+
if py_t:
|
|
490
|
+
meta_out["py_type"] = py_t
|
|
491
|
+
by_field_out[name] = meta_out
|
|
492
|
+
|
|
493
|
+
schema_in = SchemaIn(
|
|
494
|
+
fields=tuple(sorted(in_fields)),
|
|
495
|
+
by_field={f: by_field_in.get(f, {}) for f in sorted(in_fields)},
|
|
496
|
+
)
|
|
497
|
+
schema_out = SchemaOut(
|
|
498
|
+
fields=tuple(sorted(out_fields)),
|
|
499
|
+
by_field={f: by_field_out.get(f, {}) for f in sorted(out_fields)},
|
|
500
|
+
expose=tuple(sorted(out_fields)),
|
|
501
|
+
)
|
|
502
|
+
paired_index: Dict[str, Dict[str, object]] = {}
|
|
503
|
+
for field, col in specs.items():
|
|
504
|
+
io = getattr(col, "io", None)
|
|
505
|
+
cfg = getattr(io, "_paired", None)
|
|
506
|
+
if cfg and sp.alias in getattr(cfg, "verbs", ()): # type: ignore[attr-defined]
|
|
507
|
+
field_spec = getattr(col, "field", None)
|
|
508
|
+
max_len = None
|
|
509
|
+
if field_spec is not None:
|
|
510
|
+
max_len = getattr(
|
|
511
|
+
getattr(field_spec, "constraints", {}),
|
|
512
|
+
"get",
|
|
513
|
+
lambda k, d=None: None,
|
|
514
|
+
)("max_length")
|
|
515
|
+
paired_index[field] = {
|
|
516
|
+
"alias": cfg.alias,
|
|
517
|
+
"gen": cfg.gen,
|
|
518
|
+
"store": cfg.store,
|
|
519
|
+
"mask_last": cfg.mask_last,
|
|
520
|
+
"max_length": max_len,
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return OpView(
|
|
524
|
+
schema_in=schema_in,
|
|
525
|
+
schema_out=schema_out,
|
|
526
|
+
paired_index=paired_index,
|
|
527
|
+
virtual_producers={},
|
|
528
|
+
to_stored_transforms={},
|
|
529
|
+
refresh_hints=(),
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# ——— internal: endpoint-ready payload (once per App) ———
|
|
533
|
+
def _build_kernelz_payload_internal(
|
|
534
|
+
self, app: Any
|
|
535
|
+
) -> Dict[str, Dict[str, List[str]]]:
|
|
536
|
+
from ..system.diagnostics.utils import (
|
|
537
|
+
model_iter as _model_iter,
|
|
538
|
+
opspecs as _opspecs,
|
|
539
|
+
label_callable as _label_callable,
|
|
540
|
+
label_hook as _label_hook,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
start = time.monotonic()
|
|
544
|
+
out: Dict[str, Dict[str, List[str]]] = {}
|
|
545
|
+
for model in _model_iter(app):
|
|
546
|
+
self.get_specs(model) # ensure cached
|
|
547
|
+
mname = getattr(model, "__name__", "Model")
|
|
548
|
+
model_map: Dict[str, List[str]] = {}
|
|
549
|
+
for sp in _opspecs(model):
|
|
550
|
+
seq: List[str] = []
|
|
551
|
+
|
|
552
|
+
# PRE_TX: secdeps / deps
|
|
553
|
+
secdeps = [
|
|
554
|
+
_label_callable(d) if callable(d) else str(d)
|
|
555
|
+
for d in (getattr(sp, "secdeps", []) or [])
|
|
556
|
+
]
|
|
557
|
+
deps = [
|
|
558
|
+
_label_callable(d) if callable(d) else str(d)
|
|
559
|
+
for d in (getattr(sp, "deps", []) or [])
|
|
560
|
+
]
|
|
561
|
+
seq.extend(f"PRE_TX:secdep:{s}" for s in secdeps)
|
|
562
|
+
seq.extend(f"PRE_TX:dep:{d}" for d in deps)
|
|
563
|
+
|
|
564
|
+
# Chains and system hooks in canonical phase order
|
|
565
|
+
chains = self.build(model, sp.alias)
|
|
566
|
+
persist = getattr(sp, "persist", "default") != "skip"
|
|
567
|
+
|
|
568
|
+
for ph in PHASES:
|
|
569
|
+
if ph == "START_TX" and persist:
|
|
570
|
+
seq.append("START_TX:hook:sys:txn:begin@START_TX")
|
|
571
|
+
|
|
572
|
+
for step in chains.get(ph, []) or []:
|
|
573
|
+
lbl = getattr(step, "__tigrbl_label", None)
|
|
574
|
+
seq.append(
|
|
575
|
+
f"{ph}:{lbl}"
|
|
576
|
+
if isinstance(lbl, str)
|
|
577
|
+
else f"{ph}:{_label_hook(step, ph)}"
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
if ph == "END_TX" and persist:
|
|
581
|
+
seq.append("END_TX:hook:sys:txn:commit@END_TX")
|
|
582
|
+
|
|
583
|
+
# De-dup wire hooks (memory/perf friendly)
|
|
584
|
+
seen, dedup = set(), []
|
|
585
|
+
for lbl in seq:
|
|
586
|
+
if ":hook:wire:" in lbl:
|
|
587
|
+
if lbl in seen:
|
|
588
|
+
continue
|
|
589
|
+
seen.add(lbl)
|
|
590
|
+
dedup.append(lbl)
|
|
591
|
+
|
|
592
|
+
model_map[sp.alias] = dedup
|
|
593
|
+
|
|
594
|
+
if model_map:
|
|
595
|
+
out[mname] = model_map
|
|
596
|
+
duration = time.monotonic() - start
|
|
597
|
+
logger.debug("kernel: built kernelz payload for app %s in %.3fs", app, duration)
|
|
598
|
+
return out
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# ───────────────────────── Module-level exports ────────────────────
|
|
602
|
+
|
|
603
|
+
_default_kernel = Kernel()
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def get_cached_specs(model: type) -> Mapping[str, Any]:
|
|
607
|
+
"""Atoms can call this; zero per-request collection."""
|
|
608
|
+
return _default_kernel.get_specs(model)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def build_phase_chains(model: type, alias: str) -> Dict[str, List[StepFn]]:
|
|
612
|
+
return _default_kernel.build(model, alias)
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
async def run(
|
|
616
|
+
model: type,
|
|
617
|
+
alias: str,
|
|
618
|
+
*,
|
|
619
|
+
db: Any,
|
|
620
|
+
request: Any | None = None,
|
|
621
|
+
ctx: Optional[Mapping[str, Any]] = None,
|
|
622
|
+
) -> Any:
|
|
623
|
+
phases = _default_kernel.build(model, alias)
|
|
624
|
+
base_ctx = _Ctx.ensure(request=request, db=db, seed=ctx)
|
|
625
|
+
return await _invoke(request=request, db=db, phases=phases, ctx=base_ctx)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
__all__ = ["Kernel", "get_cached_specs", "_default_kernel", "build_phase_chains", "run"]
|