tigrbl-runtime 0.1.0.dev1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. tigrbl_runtime-0.1.0.dev1/PKG-INFO +50 -0
  2. tigrbl_runtime-0.1.0.dev1/README.md +29 -0
  3. tigrbl_runtime-0.1.0.dev1/pyproject.toml +42 -0
  4. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/README.md +180 -0
  5. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/__init__.py +20 -0
  6. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/context.py +206 -0
  7. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/events.py +435 -0
  8. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/exceptions.py +18 -0
  9. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/__init__.py +6 -0
  10. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/guards.py +132 -0
  11. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/helpers.py +194 -0
  12. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/invoke.py +242 -0
  13. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/executor/types.py +128 -0
  14. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/__init__.py +4 -0
  15. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/invoke.py +103 -0
  16. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/gw/raw.py +26 -0
  17. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/hook_types.py +60 -0
  18. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/kernel.py +667 -0
  19. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/labels.py +369 -0
  20. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/opview.py +89 -0
  21. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/ordering.py +331 -0
  22. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/__init__.py +63 -0
  23. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/converters.py +222 -0
  24. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/exceptions.py +149 -0
  25. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/mappings.py +94 -0
  26. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/status/utils.py +114 -0
  27. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/system.py +338 -0
  28. tigrbl_runtime-0.1.0.dev1/tigrbl_runtime/runtime/trace.py +330 -0
@@ -0,0 +1,50 @@
1
+ Metadata-Version: 2.4
2
+ Name: tigrbl-runtime
3
+ Version: 0.1.0.dev1
4
+ Summary: Runtime pipeline and executor components for Tigrbl.
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-kernel
19
+ Description-Content-Type: text/markdown
20
+
21
+ ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
22
+
23
+ # tigrbl-runtime
24
+
25
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/tigrbl-runtime.svg) ![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk.svg) ![Python Versions](https://img.shields.io/pypi/pyversions/tigrbl-runtime.svg) ![License](https://img.shields.io/pypi/l/tigrbl-runtime.svg) ![Version](https://img.shields.io/pypi/v/tigrbl-runtime.svg)
26
+
27
+ ## Features
28
+
29
+ - Modular package in the Tigrbl namespace.
30
+ - Supports Python 3.10 through 3.12.
31
+ - Distributed as part of the swarmauri-sdk workspace.
32
+
33
+ ## Installation
34
+
35
+ ### uv
36
+
37
+ ```bash
38
+ uv add tigrbl-runtime
39
+ ```
40
+
41
+ ### pip
42
+
43
+ ```bash
44
+ pip install tigrbl-runtime
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ Import from the shared package-specific module namespaces after installation in your environment.
50
+
@@ -0,0 +1,29 @@
1
+ ![Tigrbl branding](https://github.com/swarmauri/swarmauri-sdk/blob/a170683ecda8ca1c4f912c966d4499649ffb8224/assets/tigrbl.brand.theme.svg)
2
+
3
+ # tigrbl-runtime
4
+
5
+ ![PyPI - Downloads](https://img.shields.io/pypi/dm/tigrbl-runtime.svg) ![Hits](https://hits.sh/github.com/swarmauri/swarmauri-sdk.svg) ![Python Versions](https://img.shields.io/pypi/pyversions/tigrbl-runtime.svg) ![License](https://img.shields.io/pypi/l/tigrbl-runtime.svg) ![Version](https://img.shields.io/pypi/v/tigrbl-runtime.svg)
6
+
7
+ ## Features
8
+
9
+ - Modular package in the Tigrbl namespace.
10
+ - Supports Python 3.10 through 3.12.
11
+ - Distributed as part of the swarmauri-sdk workspace.
12
+
13
+ ## Installation
14
+
15
+ ### uv
16
+
17
+ ```bash
18
+ uv add tigrbl-runtime
19
+ ```
20
+
21
+ ### pip
22
+
23
+ ```bash
24
+ pip install tigrbl-runtime
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ Import from the shared package-specific module namespaces after installation in your environment.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "tigrbl-runtime"
3
+ version = "0.1.0.dev1"
4
+ description = "Runtime pipeline and executor components for Tigrbl."
5
+ license = "Apache-2.0"
6
+ readme = "README.md"
7
+ repository = "http://github.com/swarmauri/swarmauri-sdk"
8
+ requires-python = ">=3.10,<3.13"
9
+ classifiers = [
10
+ "License :: OSI Approved :: Apache Software License",
11
+ "Development Status :: 1 - Planning",
12
+ "Programming Language :: Python :: 3.10",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3 :: Only",
18
+ ]
19
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
20
+ dependencies = [
21
+ "tigrbl-kernel",
22
+ ]
23
+ keywords = ["tigrbl", "sdk", "standards", "framework"]
24
+
25
+ [tool.uv.sources]
26
+ "tigrbl-kernel" = { workspace = true }
27
+
28
+ [build-system]
29
+ requires = ["poetry-core>=1.0.0"]
30
+ build-backend = "poetry.core.masonry.api"
31
+
32
+
33
+ [tool.poetry]
34
+ packages = [
35
+ { include = "tigrbl_runtime" },
36
+ ]
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=8.0",
41
+ "ruff>=0.9",
42
+ ]
@@ -0,0 +1,180 @@
1
+ # Runtime Execution Module (v3)
2
+
3
+ > **Maintainer-only:** This module is internal to the SDK. Downstream users **must not** modify or rely on it directly.
4
+
5
+ The runtime executor coordinates Tigrbl operations through a fixed set of **phase chains**. Each phase has a list of steps built by the kernel and is executed under strict database guards.
6
+
7
+ ## Kernel Architecture Form
8
+
9
+ Tigrbl's runtime kernel follows a **compiled spec form with predicate-gated
10
+ phase-chain execution**. It is not imperative request handling and not purely
11
+ declarative configuration.
12
+
13
+ The kernel form is:
14
+
15
+ `Spec → Normalization → Compilation → KernelPlan → PhaseChain execution over GwRawEnvelope`
16
+
17
+ 1. `AppSpec` is normalized into `NormalizedSpec`.
18
+ 2. `NormalizedSpec` is compiled once into a deterministic `KernelPlan`.
19
+ 3. `Executor(plan).invoke(env)` executes ordered phase chains over a raw
20
+ transport envelope.
21
+
22
+ At runtime, the executor behaves like a compiled plan engine:
23
+
24
+ - compile once and cache the plan,
25
+ - run ingress chain,
26
+ - resolve an operation key by indexed protocol lookup,
27
+ - run operation chain,
28
+ - run egress chain.
29
+
30
+ Dispatch is index-based (`proto_indices[proto][route_key] -> opkey`), not
31
+ router traversal. Operation metadata (`opmeta[opkey]`) drives deterministic
32
+ execution.
33
+
34
+ ### Canonical KernelPlan Shape
35
+
36
+ Conceptually, the compiled plan contains:
37
+
38
+ - `proto_indices` – protocol-to-route hash maps for O(1) dispatch,
39
+ - `opmeta` – operation metadata keyed by operation key,
40
+ - `ingress_chain` – ordered atoms before operation execution,
41
+ - `op_chains` – per-operation ordered atoms,
42
+ - `egress_chain` – ordered atoms after operation execution.
43
+
44
+ Atoms are small execution units with optional predicates. Predicate checks gate
45
+ execution without introducing heuristic branching in the executor.
46
+
47
+ ### Classification
48
+
49
+ In systems terms, the runtime kernel is a deterministic, compiled,
50
+ phase-oriented dispatch engine derived from normalized application
51
+ specification. This gives:
52
+
53
+ - compile-once / execute-many behavior,
54
+ - deterministic and inspectable plans,
55
+ - protocol abstraction over a raw envelope,
56
+ - phase-level security and diagnostics injection,
57
+ - consistent phase ordering.
58
+
59
+ ## Phase Chains
60
+
61
+ Phase chains map phase names to ordered handler lists. The executor runs the phases in the sequence below:
62
+
63
+ 1. `PRE_TX_BEGIN` – pre-transaction checks.
64
+ 2. `START_TX` – open a new transaction (system-only).
65
+ 3. `PRE_HANDLER` – request validation and setup.
66
+ 4. `HANDLER` – core operation logic.
67
+ 5. `POST_HANDLER` – post-processing while still in the transaction.
68
+ 6. `PRE_COMMIT` – final checks before committing.
69
+ 7. `END_TX` – commit and close the transaction.
70
+ 8. `POST_COMMIT` – steps after commit.
71
+ 9. `POST_RESPONSE` – fire-and-forget side effects.
72
+
73
+ ## Step Kinds
74
+
75
+ The kernel labels every piece of work so it can be ordered predictably:
76
+
77
+ - **secdeps** – security dependencies that run before any other steps. Downstream
78
+ applications configure these to enforce authentication or authorization.
79
+ - **deps** – general dependencies resolved ahead of handlers. These are also
80
+ provided downstream.
81
+ - **sys** – system steps shipped with Tigrbl to coordinate core behavior. They
82
+ are maintained by project maintainers.
83
+ - **atoms** – built-in runtime units such as schema collectors or wire
84
+ serializers. Maintainers own these components.
85
+ - **hooks** – user-supplied handlers that attach to anchors within phases.
86
+
87
+ Downstream consumers configure `secdeps`, `deps`, and `hooks`, while `sys` and
88
+ `atom` steps are maintained by the Tigrbl maintainers.
89
+
90
+
91
+ ## Step Precedence
92
+
93
+ When the kernel assembles an operation it flattens several step kinds into a
94
+ single execution plan. They run in the following precedence:
95
+
96
+ 1. Security dependencies (`secdeps`)
97
+ 2. General dependencies (`deps`)
98
+ 3. System steps (`sys`) such as transaction begin, handler dispatch, and commit
99
+ 4. Runtime atoms (`atoms`)
100
+ 5. Hooks (`hooks`)
101
+
102
+ System steps appear only on the `START_TX`, `HANDLER`, and `END_TX` anchors. Within
103
+ each anchor, atoms execute before hooks and any remaining ties are resolved by
104
+ anchor-specific preferences.
105
+
106
+ ## Atom Domains
107
+
108
+ Atoms are grouped into domain-specific registries so the kernel can inject them
109
+ at the correct stage of the lifecycle. Each domain focuses on a different slice
110
+ of request or response processing:
111
+
112
+ - **wire** – builds inbound data, validates it, and prepares outbound payloads at
113
+ the field level.
114
+ - **schema** – collects request and response schema definitions for models.
115
+ - **resolve** – assembles derived values or generates paired inputs before they
116
+ are flushed.
117
+ - **storage** – converts field specifications into storage-layer instructions.
118
+ - **emit** – surfaces runtime metadata such as aliases or extras for downstream
119
+ consumers.
120
+ - **out** – mutates data after handlers run, for example masking fields before
121
+ serialization.
122
+ - **response** – negotiates content types and renders the final HTTP response or
123
+ template.
124
+ - **refresh** – triggers post-commit refreshes like demand-driven reloads.
125
+
126
+ Domains differ by the moment they run and the guarantees they provide. A `wire`
127
+ atom transforms raw request values before validation, whereas a `response` atom
128
+ operates after the transaction is committed to shape the returned payload. The
129
+ kernel uses the pair `(domain, subject)` to register and inject atoms into phase
130
+ chains.
131
+
132
+ ## DB Guards
133
+
134
+ For every phase the executor installs database guards that monkey‑patch
135
+ `commit` and `flush` on the session. Guards enforce which operations are
136
+ allowed and ensure only the owning transaction may commit.
137
+
138
+ The guard installer swaps these methods with stubs that raise
139
+ `RuntimeError` when a disallowed operation is attempted. Each phase passes
140
+ flags describing its policy:
141
+
142
+ - `allow_flush` – permit calls to `session.flush`.
143
+ - `allow_commit` – permit calls to `session.commit`.
144
+ - `require_owned_tx_for_commit` – when `True`, block commits if the
145
+ executor did not open the transaction.
146
+
147
+ The installer returns a handle that restores the original methods once the
148
+ phase finishes so restrictions do not leak across phases. A companion
149
+ helper triggers a rollback if the runtime owns the transaction and a phase
150
+ raises an error.
151
+
152
+ | Phase | Flush | Commit | Notes |
153
+ |-------|-------|--------|-------|
154
+ | PRE_TX_BEGIN | ❌ | ❌ | no database writes |
155
+ | START_TX | ❌ | ❌ | transaction opening |
156
+ | PRE_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
157
+ | HANDLER | ✅ | ❌ | writes allowed, commit blocked |
158
+ | POST_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
159
+ | PRE_COMMIT | ❌ | ❌ | freeze writes before commit |
160
+ | END_TX | ✅ | ✅ | commit allowed only if runtime owns the transaction |
161
+ | POST_COMMIT | ✅ | ❌ | post-commit writes without commit |
162
+ | POST_RESPONSE | ❌ | ❌ | background work, no writes |
163
+
164
+ ### Transaction Boundaries
165
+
166
+ `start_tx` is a system step that opens a new database transaction when
167
+ no transaction is active and marks the runtime as its owner. While this
168
+ phase runs, both `session.flush` and `session.commit` are blocked. After a
169
+ transaction is started, phases such as `PRE_HANDLER`, `HANDLER`, and
170
+ `POST_HANDLER` allow flushes so SQL statements can be issued while the
171
+ commit remains deferred. The `end_tx` step executes during the `END_TX`
172
+ phase, performing a final flush and committing the transaction if the
173
+ runtime owns it. Once this phase completes, guards restore the original
174
+ session methods.
175
+
176
+ If a phase fails, the guard restores the original methods and the executor rolls back when it owns the transaction. Optional `ON_<PHASE>_ERROR` chains can handle cleanup.
177
+
178
+ ---
179
+
180
+ This runtime layer is maintained by the core team. Downstream packages should treat it as read‑only and interact only through the public Tigrbl interfaces.
@@ -0,0 +1,20 @@
1
+ # tigrbl/runtime/__init__.py
2
+ from .executor import _invoke, _Ctx
3
+ from .kernel import Kernel, build_phase_chains, run, get_cached_specs, _default_kernel
4
+ from . import events, status, context
5
+ from .labels import STEP_KINDS, DOMAINS
6
+
7
+ __all__ = [
8
+ "_invoke",
9
+ "_Ctx",
10
+ "Kernel",
11
+ "build_phase_chains",
12
+ "run",
13
+ "get_cached_specs",
14
+ "_default_kernel",
15
+ "events",
16
+ "status",
17
+ "context",
18
+ "STEP_KINDS",
19
+ "DOMAINS",
20
+ ]
@@ -0,0 +1,206 @@
1
+ # tigrbl/runtime/context.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, Mapping, Optional, Sequence
7
+
8
+
9
+ def _canon_op(op: Optional[str]) -> str:
10
+ return (op or "").strip().lower() or "unknown"
11
+
12
+
13
+ @dataclass
14
+ class Context:
15
+ """
16
+ Canonical runtime context shared by the kernel and atoms.
17
+
18
+ Minimal contract (consumed by atoms we’ve written so far):
19
+ - op: operation name (e.g., 'create' | 'update' | 'read' | 'list' | custom)
20
+ - persist: write vs. read (affects pruning of persist-tied anchors)
21
+ - specs: mapping of field -> ColumnSpec (frozen at bind time)
22
+ - cfg: read-only config view (see config.resolver.CfgView)
23
+ - temp: dict scratchpad used by atoms to exchange data
24
+
25
+ Optional adapter slots:
26
+ - model: owning model type / class
27
+ - obj: hydrated ORM instance (if any)
28
+ - session: DB session / unit-of-work handle
29
+ - user, tenant, now: identity/time hints
30
+ - row/values/current_values: mapping fallbacks (for read paths)
31
+ - in_data / payload / data / body: inbound payload staging (for build_in)
32
+ """
33
+
34
+ # core
35
+ op: str
36
+ persist: bool
37
+ specs: Mapping[str, Any]
38
+ cfg: Any
39
+
40
+ # shared scratchpad
41
+ temp: Dict[str, Any] = field(default_factory=dict)
42
+
43
+ # optional context
44
+ model: Any | None = None
45
+ obj: Any | None = None
46
+ session: Any | None = None
47
+
48
+ # identity/time
49
+ user: Any | None = None
50
+ tenant: Any | None = None
51
+ now: _dt.datetime | None = None
52
+
53
+ # read-path fallbacks
54
+ row: Mapping[str, Any] | None = None
55
+ values: Mapping[str, Any] | None = None
56
+ current_values: Mapping[str, Any] | None = None
57
+
58
+ # inbound staging (router/adapters may set any one of these)
59
+ in_data: Any | None = None
60
+ payload: Any | None = None
61
+ data: Any | None = None
62
+ body: Any | None = None
63
+
64
+ def __post_init__(self) -> None:
65
+ self.op = _canon_op(self.op)
66
+ # Normalize now to a timezone-aware UTC timestamp when not provided
67
+ if self.now is None:
68
+ try:
69
+ self.now = _dt.datetime.now(_dt.timezone.utc)
70
+ except Exception: # pragma: no cover
71
+ self.now = _dt.datetime.utcnow().replace(tzinfo=None)
72
+
73
+ # Ensure temp is a dict (atoms rely on it)
74
+ if not isinstance(self.temp, dict):
75
+ self.temp = dict(self.temp)
76
+
77
+ # ── convenience flags ─────────────────────────────────────────────────────
78
+
79
+ @property
80
+ def is_write(self) -> bool:
81
+ """Alias for persist; reads better in some call sites."""
82
+ return bool(self.persist)
83
+
84
+ # ── safe read-only view for user callables (generators, default_factory) ──
85
+
86
+ def safe_view(
87
+ self,
88
+ *,
89
+ include_temp: bool = False,
90
+ temp_keys: Optional[Sequence[str]] = None,
91
+ ) -> Mapping[str, Any]:
92
+ """
93
+ Return a small, read-only mapping exposing only safe, frequently useful keys.
94
+
95
+ By default, temp is NOT included (to avoid leaking internals like paired raw values).
96
+ If include_temp=True, only exposes the keys listed in 'temp_keys' (if provided),
97
+ otherwise exposes a conservative subset.
98
+
99
+ This method is intended to be passed into author callables such as
100
+ default_factory(ctx_view) or paired token generators.
101
+ """
102
+ base = {
103
+ "op": self.op,
104
+ "persist": self.persist,
105
+ "model": self.model,
106
+ "specs": self.specs,
107
+ "user": self.user,
108
+ "tenant": self.tenant,
109
+ "now": self.now,
110
+ }
111
+ if include_temp:
112
+ allowed = set(temp_keys or ("assembled_values", "virtual_in"))
113
+ exposed: Dict[str, Any] = {}
114
+ for k in allowed:
115
+ if k in self.temp:
116
+ exposed[k] = self.temp[k]
117
+ base = {**base, "temp": MappingProxy(exposed)}
118
+ return MappingProxy(base)
119
+
120
+ # ── tiny helpers used by atoms / kernel ───────────────────────────────────
121
+
122
+ def mark_used_returning(self, value: bool = True) -> None:
123
+ """Flag that DB RETURNING already hydrated values."""
124
+ self.temp["used_returning"] = bool(value)
125
+
126
+ def merge_hydrated_values(
127
+ self, mapping: Mapping[str, Any], *, replace: bool = False
128
+ ) -> None:
129
+ """
130
+ Save values hydrated from DB (RETURNING/refresh). If replace=False (default),
131
+ performs a shallow merge into any existing 'hydrated_values'.
132
+ """
133
+ if not isinstance(mapping, Mapping):
134
+ return
135
+ hv = self.temp.get("hydrated_values")
136
+ if replace or not isinstance(hv, dict):
137
+ self.temp["hydrated_values"] = dict(mapping)
138
+ else:
139
+ hv.update(mapping)
140
+
141
+ def add_response_extras(
142
+ self, extras: Mapping[str, Any], *, overwrite: Optional[bool] = None
143
+ ) -> Sequence[str]:
144
+ """
145
+ Merge alias extras into temp['response_extras'].
146
+ Returns a tuple of conflicting keys that were skipped when overwrite=False.
147
+ """
148
+ if not isinstance(extras, Mapping) or not extras:
149
+ return ()
150
+ buf = self.temp.get("response_extras")
151
+ if not isinstance(buf, dict):
152
+ buf = {}
153
+ self.temp["response_extras"] = buf
154
+ if overwrite is None:
155
+ # fall back to cfg; atoms call wire:dump to honor final overwrite policy
156
+ overwrite = bool(getattr(self.cfg, "response_extras_overwrite", False))
157
+ conflicts: list[str] = []
158
+ for k, v in extras.items():
159
+ if (k in buf) and not overwrite:
160
+ conflicts.append(k)
161
+ continue
162
+ buf[k] = v
163
+ return tuple(conflicts)
164
+
165
+ def get_response_payload(self) -> Any:
166
+ """Return the payload assembled by wire:dump (or None if not yet available)."""
167
+ return self.temp.get("response_payload")
168
+
169
+ # ── representation (avoid leaking large/sensitive temp contents) ──────────
170
+
171
+ def __repr__(self) -> str: # pragma: no cover
172
+ model_name = getattr(self.model, "__name__", None) or str(self.model)
173
+ return (
174
+ f"Context(op={self.op!r}, persist={self.persist}, model={model_name!r}, "
175
+ f"user={(getattr(self.user, 'id', None) or None)!r}, temp_keys={sorted(self.temp.keys())})"
176
+ )
177
+
178
+
179
+ # ── tiny immutable mapping proxy (local; no external deps) ────────────────────
180
+
181
+
182
+ class MappingProxy(Mapping[str, Any]):
183
+ """A lightweight, read-only mapping wrapper."""
184
+
185
+ __slots__ = ("_d",)
186
+
187
+ def __init__(self, data: Mapping[str, Any]):
188
+ self._d = dict(data)
189
+
190
+ def __getitem__(self, k: str) -> Any:
191
+ return self._d[k]
192
+
193
+ def __iter__(self):
194
+ return iter(self._d)
195
+
196
+ def __len__(self) -> int:
197
+ return len(self._d)
198
+
199
+ def get(self, key: str, default: Any = None) -> Any:
200
+ return self._d.get(key, default)
201
+
202
+ def __repr__(self) -> str: # pragma: no cover
203
+ return f"MappingProxy({self._d!r})"
204
+
205
+
206
+ __all__ = ["Context", "MappingProxy"]