tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tigrbl/README.md +94 -0
- tigrbl/__init__.py +139 -14
- tigrbl/api/__init__.py +6 -0
- tigrbl/api/_api.py +97 -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 +291 -0
- tigrbl/app/__init__.py +0 -0
- tigrbl/app/_app.py +86 -0
- tigrbl/app/_model_registry.py +41 -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 +319 -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 +286 -0
- tigrbl/bindings/rest/common.py +120 -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/io_headers.py +49 -0
- tigrbl/bindings/rest/member.py +386 -0
- tigrbl/bindings/rest/router.py +296 -0
- tigrbl/bindings/rest/routing.py +153 -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 +136 -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 +45 -0
- tigrbl/engine/_engine.py +144 -0
- tigrbl/engine/bind.py +33 -0
- tigrbl/engine/builders.py +236 -0
- tigrbl/engine/capabilities.py +29 -0
- tigrbl/engine/collect.py +111 -0
- tigrbl/engine/decorators.py +110 -0
- tigrbl/engine/docs/PLUGINS.md +49 -0
- tigrbl/engine/engine_spec.py +355 -0
- tigrbl/engine/plugins.py +52 -0
- tigrbl/engine/registry.py +36 -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 +118 -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 +171 -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 +19 -0
- tigrbl/runtime/atoms/response/headers_from_payload.py +57 -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 +644 -0
- tigrbl/runtime/labels.py +353 -0
- tigrbl/runtime/opview.py +89 -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 +79 -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/session/README.md +14 -0
- tigrbl/session/__init__.py +28 -0
- tigrbl/session/abc.py +76 -0
- tigrbl/session/base.py +151 -0
- tigrbl/session/decorators.py +43 -0
- tigrbl/session/default.py +118 -0
- tigrbl/session/shortcuts.py +50 -0
- tigrbl/session/spec.py +112 -0
- tigrbl/shortcuts.py +22 -0
- tigrbl/specs.py +44 -0
- tigrbl/system/__init__.py +13 -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/system/uvicorn.py +60 -0
- tigrbl/table/__init__.py +9 -0
- tigrbl/table/_base.py +260 -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 +170 -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/types/uuid.py +55 -0
- tigrbl-0.3.0.dist-info/METADATA +516 -0
- tigrbl-0.3.0.dist-info/RECORD +266 -0
- {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
- tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -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
|
@@ -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"]
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from ... import events as _ev
|
|
7
|
+
from ...opview import opview_from_ctx, ensure_schema_out, _ensure_temp
|
|
8
|
+
|
|
9
|
+
# Runs near the end of the lifecycle, before wire:dump/out:masking.
|
|
10
|
+
ANCHOR = _ev.EMIT_ALIASES_READ # "emit:aliases:readtime"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""Emit safe read-time aliases into response extras."""
|
|
17
|
+
logger.debug("Running emit:readtime_alias")
|
|
18
|
+
temp = _ensure_temp(ctx)
|
|
19
|
+
emit_buf = _ensure_emit_buf(temp)
|
|
20
|
+
extras = _ensure_response_extras(temp)
|
|
21
|
+
|
|
22
|
+
ov = opview_from_ctx(ctx)
|
|
23
|
+
schema_out = ensure_schema_out(ctx, ov)
|
|
24
|
+
for field, desc in schema_out["by_field"].items():
|
|
25
|
+
out_alias = desc.get("alias_out")
|
|
26
|
+
if not out_alias:
|
|
27
|
+
continue
|
|
28
|
+
if out_alias in extras:
|
|
29
|
+
logger.debug("Alias %s already present in extras; skipping", out_alias)
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
value = _read_current_value(obj, ctx, field)
|
|
33
|
+
if value is None:
|
|
34
|
+
logger.debug("No current value available for field %s", field)
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
safe_val = _safe_readtime_value(value, desc)
|
|
38
|
+
extras[out_alias] = safe_val
|
|
39
|
+
logger.debug("Emitted read-time alias '%s' for field '%s'", out_alias, field)
|
|
40
|
+
|
|
41
|
+
emit_buf["read"].append(
|
|
42
|
+
{
|
|
43
|
+
"field": field,
|
|
44
|
+
"alias": out_alias,
|
|
45
|
+
"emitted": True,
|
|
46
|
+
"meta": _alias_meta(desc),
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
# Internals
|
|
53
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
|
|
57
|
+
buf = temp.get("emit_aliases")
|
|
58
|
+
if not isinstance(buf, dict):
|
|
59
|
+
buf = {"pre": [], "post": [], "read": []}
|
|
60
|
+
temp["emit_aliases"] = buf
|
|
61
|
+
else:
|
|
62
|
+
buf.setdefault("pre", [])
|
|
63
|
+
buf.setdefault("post", [])
|
|
64
|
+
buf.setdefault("read", [])
|
|
65
|
+
return buf # type: ignore[return-value]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ensure_response_extras(temp: MutableMapping[str, Any]) -> Dict[str, Any]:
|
|
69
|
+
extras = temp.get("response_extras")
|
|
70
|
+
if not isinstance(extras, dict):
|
|
71
|
+
extras = {}
|
|
72
|
+
temp["response_extras"] = extras
|
|
73
|
+
return extras # type: ignore[return-value]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _alias_meta(desc: Mapping[str, Any]) -> Dict[str, Any]:
|
|
77
|
+
meta: Dict[str, Any] = {}
|
|
78
|
+
for attr in ("sensitive", "mask_last"):
|
|
79
|
+
if attr in desc:
|
|
80
|
+
meta[attr] = desc[attr]
|
|
81
|
+
return meta
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _read_current_value(obj: Optional[object], ctx: Any, field: str) -> Optional[Any]:
|
|
85
|
+
if obj is not None and hasattr(obj, field):
|
|
86
|
+
try:
|
|
87
|
+
return getattr(obj, field)
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
for name in ("row", "values", "current_values"):
|
|
91
|
+
src = getattr(ctx, name, None)
|
|
92
|
+
if isinstance(src, Mapping) and field in src:
|
|
93
|
+
return src.get(field)
|
|
94
|
+
hv = getattr(getattr(ctx, "temp", {}), "get", lambda *a, **k: None)(
|
|
95
|
+
"hydrated_values"
|
|
96
|
+
) # type: ignore
|
|
97
|
+
if isinstance(hv, Mapping):
|
|
98
|
+
return hv.get(field)
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mask_value(value: Any, keep_last: Optional[int]) -> str:
|
|
103
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
104
|
+
return "••••"
|
|
105
|
+
s = str(value) if value is not None else ""
|
|
106
|
+
if not s:
|
|
107
|
+
return ""
|
|
108
|
+
n = keep_last if (isinstance(keep_last, int) and keep_last >= 0) else 4
|
|
109
|
+
n = min(n, len(s))
|
|
110
|
+
return "•" * (len(s) - n) + s[-n:]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _safe_readtime_value(value: Any, desc: Mapping[str, Any]) -> Any:
|
|
114
|
+
if desc.get("sensitive"):
|
|
115
|
+
keep_last = desc.get("mask_last")
|
|
116
|
+
return _mask_value(value, keep_last)
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/out/__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 masking as _masking
|
|
9
|
+
|
|
10
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
11
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
12
|
+
|
|
13
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
14
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
15
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
16
|
+
("out", "masking"): (_masking.ANCHOR, _masking.run),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("uvicorn")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def subjects() -> Tuple[str, ...]:
|
|
23
|
+
"""Return the subject names exported by this domain."""
|
|
24
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
25
|
+
logger.debug("Listing 'out' subjects: %s", subjects)
|
|
26
|
+
return subjects
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
30
|
+
"""Return (anchor, runner) for a subject in the 'out' domain."""
|
|
31
|
+
key = ("out", subject)
|
|
32
|
+
if key not in REGISTRY:
|
|
33
|
+
raise KeyError(f"Unknown out atom subject: {subject!r}")
|
|
34
|
+
logger.debug("Retrieving 'out' subject %s", subject)
|
|
35
|
+
return REGISTRY[key]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/out/masking.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional, Sequence
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, ensure_schema_out, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# Runs at the very end of the lifecycle (after wire:dump).
|
|
11
|
+
ANCHOR = _ev.OUT_DUMP # "out:dump"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
out:masking@out:dump
|
|
19
|
+
|
|
20
|
+
Purpose
|
|
21
|
+
-------
|
|
22
|
+
Mask sensitive top-level fields in the already-built response payload.
|
|
23
|
+
This runs AFTER wire:dump so the payload exists and after emit:readtime_alias
|
|
24
|
+
so alias extras are already present. It does NOT redact explicitly emitted
|
|
25
|
+
alias extras (e.g., secret-once raw tokens) — those are intentional.
|
|
26
|
+
|
|
27
|
+
Inputs / Conventions
|
|
28
|
+
--------------------
|
|
29
|
+
- ctx.temp["response_payload"] : dict or list[dict] (produced by wire:dump)
|
|
30
|
+
- ctx.temp["emit_aliases"]["post"] / ["read"] : lists of descriptors that
|
|
31
|
+
include {"alias": "..."}; these alias keys are skipped (not masked).
|
|
32
|
+
|
|
33
|
+
Effects
|
|
34
|
+
-------
|
|
35
|
+
- For each payload item (dict), if a key equals a ColumnSpec field name and
|
|
36
|
+
that column is marked sensitive (via `sensitive`/`redact`/`redact_last`),
|
|
37
|
+
replace the value with a masked hint.
|
|
38
|
+
- Leaves alias extras untouched (based on the alias sets captured from
|
|
39
|
+
emit_aliases.post/read).
|
|
40
|
+
"""
|
|
41
|
+
logger.debug("Running out:masking")
|
|
42
|
+
ov = opview_from_ctx(ctx)
|
|
43
|
+
schema_out = ensure_schema_out(ctx, ov)
|
|
44
|
+
|
|
45
|
+
temp = _ensure_temp(ctx)
|
|
46
|
+
payload = temp.get("response_payload")
|
|
47
|
+
if payload is None:
|
|
48
|
+
logger.debug("No response payload found; skipping masking")
|
|
49
|
+
return
|
|
50
|
+
logger.debug("Original payload before masking: %s", payload)
|
|
51
|
+
|
|
52
|
+
emit_buf = _ensure_emit_buf(temp)
|
|
53
|
+
skip_aliases = _collect_emitted_aliases(emit_buf)
|
|
54
|
+
|
|
55
|
+
if isinstance(payload, dict):
|
|
56
|
+
logger.debug("Masking single-object payload")
|
|
57
|
+
_mask_one(payload, schema_out["by_field"], skip_aliases)
|
|
58
|
+
elif isinstance(payload, (list, tuple)):
|
|
59
|
+
logger.debug("Masking list payload with %d items", len(payload))
|
|
60
|
+
for item in payload:
|
|
61
|
+
if isinstance(item, dict):
|
|
62
|
+
_mask_one(item, schema_out["by_field"], skip_aliases)
|
|
63
|
+
else:
|
|
64
|
+
logger.debug("Skipping non-dict item in payload: %s", item)
|
|
65
|
+
else:
|
|
66
|
+
logger.debug(
|
|
67
|
+
"Unsupported payload type %s; leaving as-is", type(payload).__name__
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
logger.debug("Payload after masking: %s", payload)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
# Internals
|
|
75
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
|
|
79
|
+
buf = temp.get("emit_aliases")
|
|
80
|
+
if not isinstance(buf, dict):
|
|
81
|
+
buf = {"pre": [], "post": [], "read": []}
|
|
82
|
+
temp["emit_aliases"] = buf
|
|
83
|
+
else:
|
|
84
|
+
buf.setdefault("pre", [])
|
|
85
|
+
buf.setdefault("post", [])
|
|
86
|
+
buf.setdefault("read", [])
|
|
87
|
+
return buf # type: ignore[return-value]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _collect_emitted_aliases(
|
|
91
|
+
emit_buf: Mapping[str, Sequence[Mapping[str, Any]]],
|
|
92
|
+
) -> set[str]:
|
|
93
|
+
aliases: set[str] = set()
|
|
94
|
+
for bucket in ("post", "read"):
|
|
95
|
+
for d in emit_buf.get(bucket, ()) or ():
|
|
96
|
+
a = d.get("alias")
|
|
97
|
+
if isinstance(a, str) and a:
|
|
98
|
+
aliases.add(a)
|
|
99
|
+
return aliases
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mask_one(
|
|
103
|
+
item: Dict[str, Any],
|
|
104
|
+
by_field: Mapping[str, Mapping[str, Any]],
|
|
105
|
+
skip_aliases: set[str],
|
|
106
|
+
) -> None:
|
|
107
|
+
for field, desc in by_field.items():
|
|
108
|
+
if field not in item or field in skip_aliases:
|
|
109
|
+
continue
|
|
110
|
+
val = item.get(field, None)
|
|
111
|
+
if val is None:
|
|
112
|
+
continue
|
|
113
|
+
if not (desc.get("sensitive") or desc.get("mask_last") is not None):
|
|
114
|
+
continue
|
|
115
|
+
masked = _mask_value(val, desc.get("mask_last"))
|
|
116
|
+
logger.debug("Masking field '%s': %r -> %r", field, val, masked)
|
|
117
|
+
item[field] = masked
|
|
118
|
+
logger.debug("Item after masking: %s", item)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _mask_value(value: Any, keep_last: Optional[int]) -> str:
|
|
122
|
+
"""
|
|
123
|
+
Generic masking for strings/bytes; falls back to a fixed token when unknown.
|
|
124
|
+
"""
|
|
125
|
+
if isinstance(value, (bytes, bytearray, memoryview)):
|
|
126
|
+
return "••••"
|
|
127
|
+
s = str(value) if value is not None else ""
|
|
128
|
+
if not s:
|
|
129
|
+
return ""
|
|
130
|
+
n = keep_last if (isinstance(keep_last, int) and keep_last >= 0) else 4
|
|
131
|
+
n = min(n, len(s))
|
|
132
|
+
return "•" * (len(s) - n) + s[-n:]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/refresh/__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 demand as _demand
|
|
9
|
+
|
|
10
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
11
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
12
|
+
|
|
13
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
14
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
15
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
16
|
+
("refresh", "demand"): (_demand.ANCHOR, _demand.run),
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("uvicorn")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def subjects() -> Tuple[str, ...]:
|
|
23
|
+
"""Return the subject names exported by this domain."""
|
|
24
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
25
|
+
logger.debug("Listing 'refresh' subjects: %s", subjects)
|
|
26
|
+
return subjects
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
30
|
+
"""Return (anchor, runner) for a subject in the 'refresh' domain."""
|
|
31
|
+
key = ("refresh", subject)
|
|
32
|
+
if key not in REGISTRY:
|
|
33
|
+
raise KeyError(f"Unknown refresh atom subject: {subject!r}")
|
|
34
|
+
logger.debug("Retrieving 'refresh' subject %s", subject)
|
|
35
|
+
return REGISTRY[key]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|