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/response/types.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import Dict, List, Literal, Optional
|
|
4
|
+
|
|
5
|
+
ResponseKind = Literal["auto", "json", "html", "text", "file", "stream", "redirect"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(slots=True)
|
|
9
|
+
class TemplateSpec:
|
|
10
|
+
name: str
|
|
11
|
+
search_paths: List[str] = field(default_factory=list)
|
|
12
|
+
package: Optional[str] = None
|
|
13
|
+
auto_reload: Optional[bool] = None
|
|
14
|
+
filters: Dict[str, object] = field(default_factory=dict)
|
|
15
|
+
globals: Dict[str, object] = field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class ResponseSpec:
|
|
20
|
+
kind: ResponseKind = "auto"
|
|
21
|
+
media_type: Optional[str] = None
|
|
22
|
+
status_code: Optional[int] = None
|
|
23
|
+
headers: Dict[str, str] = field(default_factory=dict)
|
|
24
|
+
envelope: Optional[bool] = None
|
|
25
|
+
template: Optional[TemplateSpec] = None
|
|
26
|
+
filename: Optional[str] = None
|
|
27
|
+
download: Optional[bool] = None
|
|
28
|
+
etag: Optional[str] = None
|
|
29
|
+
cache_control: Optional[str] = None
|
|
30
|
+
redirect_to: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(slots=True)
|
|
34
|
+
class Template(TemplateSpec):
|
|
35
|
+
"""Concrete template configuration used at runtime."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class Response(ResponseSpec):
|
|
40
|
+
"""Concrete response configuration used at runtime."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"TemplateSpec",
|
|
45
|
+
"ResponseSpec",
|
|
46
|
+
"ResponseKind",
|
|
47
|
+
"Template",
|
|
48
|
+
"Response",
|
|
49
|
+
]
|
tigrbl/rest/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""tigrbl_routes.py
|
|
2
|
+
Helpers that build path prefixes for nested REST endpoints.
|
|
3
|
+
The logic is intentionally minimal; extend or override as needed.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from typing import Optional, Type
|
|
8
|
+
|
|
9
|
+
from ..config.constants import TIGRBL_NESTED_PATHS_ATTR
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _nested_prefix(model: Type) -> Optional[str]:
|
|
13
|
+
"""Return the user-supplied hierarchical prefix or *None*.
|
|
14
|
+
|
|
15
|
+
• If the SQLAlchemy model defines `__tigrbl_nested_paths__`
|
|
16
|
+
→ call it and return the result.
|
|
17
|
+
• Else, fall back to legacy `_nested_path` string if present.
|
|
18
|
+
• Otherwise → signal ``no nested route wanted`` with ``None``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
cb = getattr(model, TIGRBL_NESTED_PATHS_ATTR, None)
|
|
22
|
+
if callable(cb):
|
|
23
|
+
return cb()
|
|
24
|
+
return getattr(model, "_nested_path", None)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["_nested_prefix"]
|
tigrbl/runtime/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
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
|
+
## Phase Chains
|
|
8
|
+
|
|
9
|
+
Phase chains map phase names to ordered handler lists. The executor runs the phases in the sequence below:
|
|
10
|
+
|
|
11
|
+
1. `PRE_TX_BEGIN` – pre-transaction checks.
|
|
12
|
+
2. `START_TX` – open a new transaction (system-only).
|
|
13
|
+
3. `PRE_HANDLER` – request validation and setup.
|
|
14
|
+
4. `HANDLER` – core operation logic.
|
|
15
|
+
5. `POST_HANDLER` – post-processing while still in the transaction.
|
|
16
|
+
6. `PRE_COMMIT` – final checks before committing.
|
|
17
|
+
7. `END_TX` – commit and close the transaction.
|
|
18
|
+
8. `POST_COMMIT` – steps after commit.
|
|
19
|
+
9. `POST_RESPONSE` – fire-and-forget side effects.
|
|
20
|
+
|
|
21
|
+
## Step Kinds
|
|
22
|
+
|
|
23
|
+
The kernel labels every piece of work so it can be ordered predictably:
|
|
24
|
+
|
|
25
|
+
- **secdeps** – security dependencies that run before any other steps. Downstream
|
|
26
|
+
applications configure these to enforce authentication or authorization.
|
|
27
|
+
- **deps** – general dependencies resolved ahead of handlers. These are also
|
|
28
|
+
provided downstream.
|
|
29
|
+
- **sys** – system steps shipped with Tigrbl to coordinate core behavior. They
|
|
30
|
+
are maintained by project maintainers.
|
|
31
|
+
- **atoms** – built-in runtime units such as schema collectors or wire
|
|
32
|
+
serializers. Maintainers own these components.
|
|
33
|
+
- **hooks** – user-supplied handlers that attach to anchors within phases.
|
|
34
|
+
|
|
35
|
+
Downstream consumers configure `secdeps`, `deps`, and `hooks`, while `sys` and
|
|
36
|
+
`atom` steps are maintained by the Tigrbl maintainers.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Step Precedence
|
|
40
|
+
|
|
41
|
+
When the kernel assembles an operation it flattens several step kinds into a
|
|
42
|
+
single execution plan. They run in the following precedence:
|
|
43
|
+
|
|
44
|
+
1. Security dependencies (`secdeps`)
|
|
45
|
+
2. General dependencies (`deps`)
|
|
46
|
+
3. System steps (`sys`) such as transaction begin, handler dispatch, and commit
|
|
47
|
+
4. Runtime atoms (`atoms`)
|
|
48
|
+
5. Hooks (`hooks`)
|
|
49
|
+
|
|
50
|
+
System steps appear only on the `START_TX`, `HANDLER`, and `END_TX` anchors. Within
|
|
51
|
+
each anchor, atoms execute before hooks and any remaining ties are resolved by
|
|
52
|
+
anchor-specific preferences.
|
|
53
|
+
|
|
54
|
+
## Atom Domains
|
|
55
|
+
|
|
56
|
+
Atoms are grouped into domain-specific registries so the kernel can inject them
|
|
57
|
+
at the correct stage of the lifecycle. Each domain focuses on a different slice
|
|
58
|
+
of request or response processing:
|
|
59
|
+
|
|
60
|
+
- **wire** – builds inbound data, validates it, and prepares outbound payloads at
|
|
61
|
+
the field level.
|
|
62
|
+
- **schema** – collects request and response schema definitions for models.
|
|
63
|
+
- **resolve** – assembles derived values or generates paired inputs before they
|
|
64
|
+
are flushed.
|
|
65
|
+
- **storage** – converts field specifications into storage-layer instructions.
|
|
66
|
+
- **emit** – surfaces runtime metadata such as aliases or extras for downstream
|
|
67
|
+
consumers.
|
|
68
|
+
- **out** – mutates data after handlers run, for example masking fields before
|
|
69
|
+
serialization.
|
|
70
|
+
- **response** – negotiates content types and renders the final HTTP response or
|
|
71
|
+
template.
|
|
72
|
+
- **refresh** – triggers post-commit refreshes like demand-driven reloads.
|
|
73
|
+
|
|
74
|
+
Domains differ by the moment they run and the guarantees they provide. A `wire`
|
|
75
|
+
atom transforms raw request values before validation, whereas a `response` atom
|
|
76
|
+
operates after the transaction is committed to shape the returned payload. The
|
|
77
|
+
kernel uses the pair `(domain, subject)` to register and inject atoms into phase
|
|
78
|
+
chains.
|
|
79
|
+
|
|
80
|
+
## DB Guards
|
|
81
|
+
|
|
82
|
+
For every phase the executor installs database guards that monkey‑patch
|
|
83
|
+
`commit` and `flush` on the session. Guards enforce which operations are
|
|
84
|
+
allowed and ensure only the owning transaction may commit.
|
|
85
|
+
|
|
86
|
+
The guard installer swaps these methods with stubs that raise
|
|
87
|
+
`RuntimeError` when a disallowed operation is attempted. Each phase passes
|
|
88
|
+
flags describing its policy:
|
|
89
|
+
|
|
90
|
+
- `allow_flush` – permit calls to `session.flush`.
|
|
91
|
+
- `allow_commit` – permit calls to `session.commit`.
|
|
92
|
+
- `require_owned_tx_for_commit` – when `True`, block commits if the
|
|
93
|
+
executor did not open the transaction.
|
|
94
|
+
|
|
95
|
+
The installer returns a handle that restores the original methods once the
|
|
96
|
+
phase finishes so restrictions do not leak across phases. A companion
|
|
97
|
+
helper triggers a rollback if the runtime owns the transaction and a phase
|
|
98
|
+
raises an error.
|
|
99
|
+
|
|
100
|
+
| Phase | Flush | Commit | Notes |
|
|
101
|
+
|-------|-------|--------|-------|
|
|
102
|
+
| PRE_TX_BEGIN | ❌ | ❌ | no database writes |
|
|
103
|
+
| START_TX | ❌ | ❌ | transaction opening |
|
|
104
|
+
| PRE_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
|
|
105
|
+
| HANDLER | ✅ | ❌ | writes allowed, commit blocked |
|
|
106
|
+
| POST_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
|
|
107
|
+
| PRE_COMMIT | ❌ | ❌ | freeze writes before commit |
|
|
108
|
+
| END_TX | ✅ | ✅ | commit allowed only if runtime owns the transaction |
|
|
109
|
+
| POST_COMMIT | ✅ | ❌ | post-commit writes without commit |
|
|
110
|
+
| POST_RESPONSE | ❌ | ❌ | background work, no writes |
|
|
111
|
+
|
|
112
|
+
### Transaction Boundaries
|
|
113
|
+
|
|
114
|
+
`start_tx` is a system step that opens a new database transaction when
|
|
115
|
+
no transaction is active and marks the runtime as its owner. While this
|
|
116
|
+
phase runs, both `session.flush` and `session.commit` are blocked. After a
|
|
117
|
+
transaction is started, phases such as `PRE_HANDLER`, `HANDLER`, and
|
|
118
|
+
`POST_HANDLER` allow flushes so SQL statements can be issued while the
|
|
119
|
+
commit remains deferred. The `end_tx` step executes during the `END_TX`
|
|
120
|
+
phase, performing a final flush and committing the transaction if the
|
|
121
|
+
runtime owns it. Once this phase completes, guards restore the original
|
|
122
|
+
session methods.
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# tigrbl/v3/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, errors, 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
|
+
"errors",
|
|
17
|
+
"context",
|
|
18
|
+
"STEP_KINDS",
|
|
19
|
+
"DOMAINS",
|
|
20
|
+
]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .. import events as _ev
|
|
8
|
+
|
|
9
|
+
# Domain registries
|
|
10
|
+
from .emit import REGISTRY as _EMIT
|
|
11
|
+
from .out import REGISTRY as _OUT
|
|
12
|
+
from .refresh import REGISTRY as _REFRESH
|
|
13
|
+
from .resolve import REGISTRY as _RESOLVE
|
|
14
|
+
from .schema import REGISTRY as _SCHEMA
|
|
15
|
+
from .storage import REGISTRY as _STORAGE
|
|
16
|
+
from .wire import REGISTRY as _WIRE
|
|
17
|
+
from .response import REGISTRY as _RESPONSE
|
|
18
|
+
|
|
19
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
20
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
21
|
+
|
|
22
|
+
#: Global registry consumed by the kernel plan:
|
|
23
|
+
#: { (domain, subject): (anchor, runner) }
|
|
24
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {}
|
|
25
|
+
|
|
26
|
+
logging.getLogger("uvicorn").setLevel(logging.DEBUG)
|
|
27
|
+
logger = logging.getLogger("uvicorn")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _add_bulk(source: Dict[Tuple[str, str], Tuple[str, RunFn]]) -> None:
|
|
31
|
+
for key, val in source.items():
|
|
32
|
+
if key in REGISTRY:
|
|
33
|
+
logger.error("Duplicate atom registration attempted: %s", key)
|
|
34
|
+
raise RuntimeError(f"Duplicate atom registration: {key!r}")
|
|
35
|
+
anchor, fn = val
|
|
36
|
+
if not _ev.is_valid_event(anchor):
|
|
37
|
+
logger.error("Atom %s declares unknown anchor %s", key, anchor)
|
|
38
|
+
raise ValueError(f"Atom {key!r} declares unknown anchor {anchor!r}")
|
|
39
|
+
REGISTRY[key] = (anchor, fn)
|
|
40
|
+
logger.debug("Registered atom %s -> %s", key, anchor)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Aggregate all domains
|
|
44
|
+
_add_bulk(_EMIT)
|
|
45
|
+
_add_bulk(_OUT)
|
|
46
|
+
_add_bulk(_REFRESH)
|
|
47
|
+
_add_bulk(_RESOLVE)
|
|
48
|
+
_add_bulk(_SCHEMA)
|
|
49
|
+
_add_bulk(_STORAGE)
|
|
50
|
+
_add_bulk(_WIRE)
|
|
51
|
+
_add_bulk(_RESPONSE)
|
|
52
|
+
|
|
53
|
+
logger.info("Loaded %d runtime atoms", len(REGISTRY))
|
|
54
|
+
|
|
55
|
+
# ── Back-compat subject aliases (optional) ────────────────────────────────────
|
|
56
|
+
# Allow "wire:validate" as an alias of "wire:validate_in".
|
|
57
|
+
if ("wire", "validate_in") in REGISTRY and ("wire", "validate") not in REGISTRY:
|
|
58
|
+
REGISTRY[("wire", "validate")] = REGISTRY[("wire", "validate_in")]
|
|
59
|
+
|
|
60
|
+
# ── Public helpers ────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def domains() -> Tuple[str, ...]:
|
|
64
|
+
"""Return all domains present in the registry."""
|
|
65
|
+
out = tuple(sorted({d for (d, _) in REGISTRY.keys()}))
|
|
66
|
+
logger.debug("Listing domains: %s", out)
|
|
67
|
+
return out
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def subjects(domain: str) -> Tuple[str, ...]:
|
|
71
|
+
"""Return subjects available for a given domain."""
|
|
72
|
+
out = tuple(sorted(s for (d, s) in REGISTRY.keys() if d == domain))
|
|
73
|
+
logger.debug("Listing subjects for %s: %s", domain, out)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get(domain: str, subject: str) -> Tuple[str, RunFn]:
|
|
78
|
+
"""Return (anchor, runner) for a given (domain, subject)."""
|
|
79
|
+
key = (domain, subject)
|
|
80
|
+
if key not in REGISTRY:
|
|
81
|
+
logger.error("Unknown atom requested: %s:%s", domain, subject)
|
|
82
|
+
raise KeyError(f"Unknown atom: {domain}:{subject}")
|
|
83
|
+
val = REGISTRY[key]
|
|
84
|
+
logger.debug("Retrieved atom %s:%s -> %s", domain, subject, val[0])
|
|
85
|
+
return val
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def all_items() -> Tuple[Tuple[Tuple[str, str], Tuple[str, RunFn]], ...]:
|
|
89
|
+
"""Return the registry items as a sorted tuple for deterministic iteration."""
|
|
90
|
+
items = tuple(sorted(REGISTRY.items(), key=lambda kv: (kv[0][0], kv[0][1])))
|
|
91
|
+
logger.debug("Listing all registry items (%d)", len(items))
|
|
92
|
+
return items
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
__all__ = [
|
|
96
|
+
"RunFn",
|
|
97
|
+
"REGISTRY",
|
|
98
|
+
"domains",
|
|
99
|
+
"subjects",
|
|
100
|
+
"get",
|
|
101
|
+
"all_items",
|
|
102
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/emit/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Atom implementations (model-scoped)
|
|
8
|
+
from . import paired_pre as _paired_pre
|
|
9
|
+
from . import paired_post as _paired_post
|
|
10
|
+
from . import readtime_alias as _readtime_alias
|
|
11
|
+
|
|
12
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
13
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
14
|
+
|
|
15
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
16
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
17
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
18
|
+
("emit", "paired_pre"): (_paired_pre.ANCHOR, _paired_pre.run),
|
|
19
|
+
("emit", "paired_post"): (_paired_post.ANCHOR, _paired_post.run),
|
|
20
|
+
("emit", "readtime_alias"): (_readtime_alias.ANCHOR, _readtime_alias.run),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("uvicorn")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def subjects() -> Tuple[str, ...]:
|
|
27
|
+
"""Return the subject names exported by this domain."""
|
|
28
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
29
|
+
logger.debug("Listing 'emit' subjects: %s", subjects)
|
|
30
|
+
return subjects
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
34
|
+
"""Return (anchor, runner) for a subject in the 'emit' domain."""
|
|
35
|
+
key = ("emit", subject)
|
|
36
|
+
if key not in REGISTRY:
|
|
37
|
+
raise KeyError(f"Unknown emit atom subject: {subject!r}")
|
|
38
|
+
logger.debug("Retrieving 'emit' subject %s", subject)
|
|
39
|
+
return REGISTRY[key]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/emit/paired_post.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
|
|
9
|
+
# Runs after DB flush + refresh, before out model construction.
|
|
10
|
+
ANCHOR = _ev.EMIT_ALIASES_POST # "emit:aliases:post_refresh"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""
|
|
17
|
+
emit:paired_post@emit:aliases:post_refresh
|
|
18
|
+
|
|
19
|
+
Purpose
|
|
20
|
+
-------
|
|
21
|
+
Resolve *deferred* alias emissions prepared by emit:paired_pre. For each pending
|
|
22
|
+
descriptor, look up the generated raw value (typically created by resolve:paired_gen),
|
|
23
|
+
copy it into a response-extras buffer, and scrub the raw from temp to enforce
|
|
24
|
+
the secret-once rule.
|
|
25
|
+
|
|
26
|
+
Inputs / Conventions
|
|
27
|
+
--------------------
|
|
28
|
+
- ctx.temp["emit_aliases"]["pre"] : list of descriptors from paired_pre
|
|
29
|
+
{ "field": str, "alias": str, "source": ("paired_values", field, "raw"), "meta": {...} }
|
|
30
|
+
- ctx.temp["paired_values"] : { field: {"raw": <value>, "meta"?: {...}, ...}, ... }
|
|
31
|
+
- ctx.persist : bool (this anchor should be pruned when False)
|
|
32
|
+
- ctx.temp["response_extras"] : dict aggregated here for out-building
|
|
33
|
+
|
|
34
|
+
Effects
|
|
35
|
+
-------
|
|
36
|
+
- Appends alias/value into ctx.temp["response_extras"][alias] = value
|
|
37
|
+
- Appends a redacted audit descriptor to ctx.temp["emit_aliases"]["post"]
|
|
38
|
+
- Scrubs ctx.temp["paired_values"][field]["raw"] after emission (secret-once)
|
|
39
|
+
- Clears "pre" queue after processing
|
|
40
|
+
"""
|
|
41
|
+
# Non-persisting ops should have pruned this anchor via the planner,
|
|
42
|
+
# but guard anyway for robustness.
|
|
43
|
+
logger.debug("Running emit:paired_post")
|
|
44
|
+
if getattr(ctx, "persist", True) is False:
|
|
45
|
+
logger.debug("Skipping emit:paired_post; ctx.persist is False")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
temp = _ensure_temp(ctx)
|
|
49
|
+
emit_buf = _ensure_emit_buf(temp)
|
|
50
|
+
pre_queue = list(emit_buf.get("pre") or ())
|
|
51
|
+
if not pre_queue:
|
|
52
|
+
logger.debug("No deferred aliases to emit")
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
pv = _get_paired_values(temp)
|
|
56
|
+
extras = _ensure_response_extras(temp)
|
|
57
|
+
|
|
58
|
+
for desc in pre_queue:
|
|
59
|
+
if not isinstance(desc, dict):
|
|
60
|
+
logger.debug("Skipping non-dict descriptor: %s", desc)
|
|
61
|
+
continue
|
|
62
|
+
field = desc.get("field")
|
|
63
|
+
alias = desc.get("alias")
|
|
64
|
+
if not isinstance(field, str) or not isinstance(alias, str) or not field:
|
|
65
|
+
logger.debug("Descriptor missing valid field/alias: %s", desc)
|
|
66
|
+
continue
|
|
67
|
+
|
|
68
|
+
value = _resolve_value_from_source(desc.get("source"), pv, field)
|
|
69
|
+
if value is None:
|
|
70
|
+
logger.debug("No value resolved for field %s; skipping", field)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
logger.debug("Emitting alias '%s' for field '%s'", alias, field)
|
|
74
|
+
# 1) Emit into response extras (to be merged by wire/out stages)
|
|
75
|
+
extras[alias] = value
|
|
76
|
+
|
|
77
|
+
# 2) Record a minimal (redacted) audit entry; do NOT keep raw value here
|
|
78
|
+
emit_buf["post"].append(
|
|
79
|
+
{
|
|
80
|
+
"field": field,
|
|
81
|
+
"alias": alias,
|
|
82
|
+
"emitted": True,
|
|
83
|
+
"meta": (desc.get("meta") or {}),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 3) Enforce secret-once: scrub raw so later steps cannot re-emit accidentally
|
|
88
|
+
_scrub_paired_raw(pv, field)
|
|
89
|
+
logger.debug("Scrubbed paired raw for field '%s'", field)
|
|
90
|
+
|
|
91
|
+
# All pre-emit descriptors consumed
|
|
92
|
+
emit_buf["pre"].clear()
|
|
93
|
+
logger.debug("Cleared pre-emit queue")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
# Internals
|
|
98
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _ensure_temp(ctx: Any) -> MutableMapping[str, Any]:
|
|
102
|
+
temp = getattr(ctx, "temp", None)
|
|
103
|
+
if not isinstance(temp, dict):
|
|
104
|
+
temp = {}
|
|
105
|
+
setattr(ctx, "temp", temp)
|
|
106
|
+
return temp
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
|
|
110
|
+
buf = temp.get("emit_aliases")
|
|
111
|
+
if not isinstance(buf, dict):
|
|
112
|
+
buf = {"pre": [], "post": [], "read": []}
|
|
113
|
+
temp["emit_aliases"] = buf
|
|
114
|
+
else:
|
|
115
|
+
buf.setdefault("pre", [])
|
|
116
|
+
buf.setdefault("post", [])
|
|
117
|
+
buf.setdefault("read", [])
|
|
118
|
+
return buf # type: ignore[return-value]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _ensure_response_extras(temp: MutableMapping[str, Any]) -> Dict[str, Any]:
|
|
122
|
+
extras = temp.get("response_extras")
|
|
123
|
+
if not isinstance(extras, dict):
|
|
124
|
+
extras = {}
|
|
125
|
+
temp["response_extras"] = extras
|
|
126
|
+
return extras # type: ignore[return-value]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _get_paired_values(temp: Mapping[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
130
|
+
pv = temp.get("paired_values")
|
|
131
|
+
return pv if isinstance(pv, dict) else {}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _resolve_value_from_source(
|
|
135
|
+
source: Any, pv: Mapping[str, Dict[str, Any]], field: str
|
|
136
|
+
) -> Optional[Any]:
|
|
137
|
+
"""
|
|
138
|
+
Resolve the value indicated by a ('paired_values', field, 'raw')-style pointer.
|
|
139
|
+
Falls back to pv[field]['raw'] when source is missing/malformed.
|
|
140
|
+
"""
|
|
141
|
+
if isinstance(source, (tuple, list)) and len(source) == 3:
|
|
142
|
+
base, fld, key = source
|
|
143
|
+
if base == "paired_values" and isinstance(fld, str) and key == "raw":
|
|
144
|
+
return pv.get(fld, {}).get("raw")
|
|
145
|
+
# Fallback
|
|
146
|
+
return pv.get(field, {}).get("raw")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _scrub_paired_raw(pv: MutableMapping[str, Dict[str, Any]], field: str) -> None:
|
|
150
|
+
entry = pv.get(field)
|
|
151
|
+
if not isinstance(entry, dict):
|
|
152
|
+
return
|
|
153
|
+
# Remove raw, mark emitted
|
|
154
|
+
entry.pop("raw", None)
|
|
155
|
+
entry["emitted"] = True
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/emit/paired_pre.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# This atom runs before the flush, after values have been assembled/generated.
|
|
11
|
+
ANCHOR = _ev.EMIT_ALIASES_PRE # "emit:aliases:pre_flush"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
emit:paired_pre@emit:aliases:pre_flush
|
|
19
|
+
|
|
20
|
+
Purpose
|
|
21
|
+
-------
|
|
22
|
+
Prepare *deferred* alias emissions for "paired" values (e.g., secret-once raw tokens)
|
|
23
|
+
that were generated earlier (typically by resolve:paired_gen) and stored on
|
|
24
|
+
ctx.temp["paired_values"] = { <field>: {"raw": <value>, "alias"?: <str>, "meta"?: {...}} }.
|
|
25
|
+
|
|
26
|
+
This atom:
|
|
27
|
+
- Ensures ctx.temp["emit_aliases"] = {"pre": [], "post": [], "read": []}
|
|
28
|
+
- Scans paired_values and pushes *deferred* emit specs into "pre"
|
|
29
|
+
so that emit:paired_post can resolve them after flush/refresh and
|
|
30
|
+
attach to the outbound payload/extras safely.
|
|
31
|
+
|
|
32
|
+
Contracts / Conventions
|
|
33
|
+
-----------------------
|
|
34
|
+
- ctx.temp is a dict-like scratch space shared across atoms.
|
|
35
|
+
- paired_values entries may provide an explicit "alias"; otherwise we infer one
|
|
36
|
+
using OpView metadata; if nothing is available, we default to the field name.
|
|
37
|
+
- This atom is a no-op when there are no paired values.
|
|
38
|
+
|
|
39
|
+
It is safe to call multiple times; it only appends idempotent descriptors.
|
|
40
|
+
"""
|
|
41
|
+
# Non-persisting ops should have pruned this anchor via the planner,
|
|
42
|
+
# but guard anyway for robustness.
|
|
43
|
+
logger.debug("Running emit:paired_pre")
|
|
44
|
+
if getattr(ctx, "persist", True) is False:
|
|
45
|
+
logger.debug("Skipping emit:paired_pre; ctx.persist is False")
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
temp = _ensure_temp(ctx)
|
|
49
|
+
emit_buf = _ensure_emit_buf(temp)
|
|
50
|
+
paired = _get_paired_values(temp)
|
|
51
|
+
|
|
52
|
+
if not paired:
|
|
53
|
+
logger.debug("No paired values found; nothing to schedule")
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
ov = opview_from_ctx(ctx)
|
|
57
|
+
|
|
58
|
+
for field, entry in paired.items():
|
|
59
|
+
if not isinstance(entry, dict):
|
|
60
|
+
logger.debug(
|
|
61
|
+
"Skipping non-dict paired entry for field %s: %s", field, entry
|
|
62
|
+
)
|
|
63
|
+
continue
|
|
64
|
+
if "raw" not in entry:
|
|
65
|
+
logger.debug("Paired entry for field %s lacks raw value", field)
|
|
66
|
+
# nothing to emit
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
desc = ov.paired_index.get(field, {})
|
|
70
|
+
alias = entry.get("alias") or desc.get("alias") or field
|
|
71
|
+
|
|
72
|
+
# Record a *deferred* emission descriptor; emit:paired_post will resolve it.
|
|
73
|
+
emit_buf["pre"].append(
|
|
74
|
+
{
|
|
75
|
+
"field": field,
|
|
76
|
+
"alias": alias,
|
|
77
|
+
"source": ("paired_values", field, "raw"),
|
|
78
|
+
"meta": entry.get("meta") or {},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
logger.debug("Queued deferred alias '%s' for field '%s'", alias, field)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
# Internals
|
|
86
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
|
|
90
|
+
buf = temp.get("emit_aliases")
|
|
91
|
+
if not isinstance(buf, dict):
|
|
92
|
+
buf = {"pre": [], "post": [], "read": []}
|
|
93
|
+
temp["emit_aliases"] = buf
|
|
94
|
+
else:
|
|
95
|
+
buf.setdefault("pre", [])
|
|
96
|
+
buf.setdefault("post", [])
|
|
97
|
+
buf.setdefault("read", [])
|
|
98
|
+
return buf # type: ignore[return-value]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_paired_values(temp: Mapping[str, Any]) -> Mapping[str, Dict[str, Any]]:
|
|
102
|
+
pv = temp.get("paired_values")
|
|
103
|
+
return pv if isinstance(pv, dict) else {}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
__all__ = ["ANCHOR", "run"]
|