tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__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.dev2.dist-info/LICENSE +201 -0
- tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
- tigrbl-0.3.0.dev2.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.dev2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/storage/to_stored.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
6
|
+
|
|
7
|
+
from ... import events as _ev
|
|
8
|
+
from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
|
|
9
|
+
|
|
10
|
+
# Runs right before the handler flushes to the DB.
|
|
11
|
+
ANCHOR = _ev.PRE_FLUSH # "pre:flush"
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("uvicorn")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
17
|
+
"""
|
|
18
|
+
storage:to_stored@pre:flush
|
|
19
|
+
|
|
20
|
+
Transform inbound values into their persisted representation.
|
|
21
|
+
|
|
22
|
+
- For *paired/secret-once* columns, derive the stored value from the prepared raw
|
|
23
|
+
and assign it to BOTH ctx.temp["assembled_values"][field] AND the ORM object.
|
|
24
|
+
Also handles a fallback when 'persist_from_paired' wasn't queued but a paired raw
|
|
25
|
+
exists (planner tolerance).
|
|
26
|
+
- Otherwise, apply an optional per-column inbound→stored transform and mirror the
|
|
27
|
+
transformed value onto the ORM instance.
|
|
28
|
+
|
|
29
|
+
On failure to derive for paired, raise ValueError (mapped by higher layers) to
|
|
30
|
+
avoid leaking DB IntegrityErrors.
|
|
31
|
+
"""
|
|
32
|
+
logger.debug("Running storage:to_stored")
|
|
33
|
+
if getattr(ctx, "persist", True) is False:
|
|
34
|
+
logger.debug("Skipping storage:to_stored; ctx.persist is False")
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
ov = opview_from_ctx(ctx)
|
|
38
|
+
schema_in = ensure_schema_in(ctx, ov)
|
|
39
|
+
temp = _ensure_temp(ctx)
|
|
40
|
+
assembled = _ensure_dict(temp, "assembled_values")
|
|
41
|
+
paired_values = _ensure_dict(temp, "paired_values")
|
|
42
|
+
pf_paired = _ensure_dict(temp, "persist_from_paired")
|
|
43
|
+
slog = _ensure_list(temp, "storage_log")
|
|
44
|
+
serr = _ensure_list(temp, "storage_errors")
|
|
45
|
+
|
|
46
|
+
# Prefer explicit obj (hydrated instance), else ctx.model if adapter provided it
|
|
47
|
+
target_obj = obj or getattr(ctx, "model", None)
|
|
48
|
+
|
|
49
|
+
# Ensure paired fields are considered even when absent from inbound schema.
|
|
50
|
+
# schema_in["fields"] may omit columns like "digest" that generate their
|
|
51
|
+
# values server-side via IO(...).paired. To derive their stored values, merge
|
|
52
|
+
# the explicit schema fields with the paired index keys.
|
|
53
|
+
all_fields = set(schema_in["fields"]) | set(ov.paired_index.keys())
|
|
54
|
+
|
|
55
|
+
for field in sorted(all_fields):
|
|
56
|
+
if field in ov.paired_index:
|
|
57
|
+
if field in pf_paired or field in paired_values:
|
|
58
|
+
raw = None
|
|
59
|
+
if field in pf_paired:
|
|
60
|
+
raw = _resolve_from_pointer(
|
|
61
|
+
pf_paired[field].get("source"), paired_values, field
|
|
62
|
+
)
|
|
63
|
+
if raw is None:
|
|
64
|
+
raw = paired_values.get(field, {}).get("raw")
|
|
65
|
+
if raw is None:
|
|
66
|
+
serr.append({"field": field, "error": "missing_paired_raw"})
|
|
67
|
+
logger.debug("Missing paired raw for field %s", field)
|
|
68
|
+
raise RuntimeError(f"paired_raw_missing:{field}")
|
|
69
|
+
deriver = ov.paired_index[field].get("store")
|
|
70
|
+
try:
|
|
71
|
+
stored = deriver(raw, ctx) if callable(deriver) else raw
|
|
72
|
+
except Exception as e:
|
|
73
|
+
serr.append(
|
|
74
|
+
{"field": field, "error": f"deriver_failed:{type(e).__name__}"}
|
|
75
|
+
)
|
|
76
|
+
logger.debug("Deriver failed for field %s: %s", field, e)
|
|
77
|
+
raise
|
|
78
|
+
assembled[field] = stored
|
|
79
|
+
_assign_to_model(target_obj, field, stored)
|
|
80
|
+
slog.append({"field": field, "action": "derived_from_paired"})
|
|
81
|
+
logger.debug("Derived stored value for paired field %s", field)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
nullable = schema_in["by_field"].get(field, {}).get("nullable", True)
|
|
85
|
+
if (
|
|
86
|
+
not nullable
|
|
87
|
+
and field not in assembled
|
|
88
|
+
and not _has_attr_with_value(target_obj, field)
|
|
89
|
+
):
|
|
90
|
+
serr.append({"field": field, "error": "paired_missing_before_flush"})
|
|
91
|
+
logger.debug("Paired field %s missing before flush", field)
|
|
92
|
+
raise RuntimeError(f"paired_missing_before_flush:{field}")
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if field in assembled:
|
|
96
|
+
transform = ov.to_stored_transforms.get(field)
|
|
97
|
+
if transform is None:
|
|
98
|
+
logger.debug("No transform for field %s; using assembled value", field)
|
|
99
|
+
_assign_to_model(target_obj, field, assembled[field])
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
stored_val = transform(assembled[field], ctx)
|
|
103
|
+
assembled[field] = stored_val
|
|
104
|
+
_assign_to_model(target_obj, field, stored_val)
|
|
105
|
+
slog.append({"field": field, "action": "transformed"})
|
|
106
|
+
logger.debug("Transformed field %s", field)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
serr.append(
|
|
109
|
+
{"field": field, "error": f"transform_failed:{type(e).__name__}"}
|
|
110
|
+
)
|
|
111
|
+
logger.debug("Transform failed for field %s: %s", field, e)
|
|
112
|
+
raise
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
# Internals (tolerant to spec shapes)
|
|
117
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _assign_to_model(target: Optional[object], field: str, value: Any) -> None:
|
|
121
|
+
"""Safely assign value onto the hydrated ORM object so SQLAlchemy flushes it."""
|
|
122
|
+
if target is None:
|
|
123
|
+
return
|
|
124
|
+
try:
|
|
125
|
+
setattr(target, field, value)
|
|
126
|
+
except Exception:
|
|
127
|
+
# Non-fatal: some adapters may not expose an assignable object here.
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _has_attr_with_value(target: Optional[object], field: str) -> bool:
|
|
132
|
+
if target is None or not hasattr(target, field):
|
|
133
|
+
return False
|
|
134
|
+
try:
|
|
135
|
+
return getattr(target, field) is not None
|
|
136
|
+
except Exception:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _ensure_dict(temp: MutableMapping[str, Any], key: str) -> Dict[str, Any]:
|
|
141
|
+
d = temp.get(key)
|
|
142
|
+
if not isinstance(d, dict):
|
|
143
|
+
d = {}
|
|
144
|
+
temp[key] = d
|
|
145
|
+
return d # type: ignore[return-value]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _ensure_list(temp: MutableMapping[str, Any], key: str) -> list:
|
|
149
|
+
lst = temp.get(key)
|
|
150
|
+
if not isinstance(lst, list):
|
|
151
|
+
lst = []
|
|
152
|
+
temp[key] = lst
|
|
153
|
+
return lst # type: ignore[return-value]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_from_pointer(
|
|
157
|
+
source: Any, pv: Mapping[str, Dict[str, Any]], field: str
|
|
158
|
+
) -> Optional[Any]:
|
|
159
|
+
"""Resolve ('paired_values', field, 'raw') pointer, with fallback to pv[field]['raw']."""
|
|
160
|
+
if isinstance(source, (tuple, list)) and len(source) == 3:
|
|
161
|
+
base, fld, key = source
|
|
162
|
+
if base == "paired_values" and isinstance(fld, str) and key == "raw":
|
|
163
|
+
return pv.get(fld, {}).get("raw")
|
|
164
|
+
return pv.get(field, {}).get("raw")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/wire/__init__.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Callable, Dict, Optional, Tuple
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Atom implementations (per-field)
|
|
8
|
+
from . import build_in as _build_in
|
|
9
|
+
from . import validate_in as _validate_in
|
|
10
|
+
from . import build_out as _build_out
|
|
11
|
+
from . import dump as _dump
|
|
12
|
+
|
|
13
|
+
# Runner signature: (obj|None, ctx) -> None
|
|
14
|
+
RunFn = Callable[[Optional[object], Any], None]
|
|
15
|
+
|
|
16
|
+
#: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
|
|
17
|
+
#: Keys are (domain, subject); values are (anchor, runner).
|
|
18
|
+
#: Canonical subjects mirror filenames; we keep "validate_in" (not "validate") to avoid duplicates.
|
|
19
|
+
REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
|
|
20
|
+
("wire", "build_in"): (_build_in.ANCHOR, _build_in.run),
|
|
21
|
+
("wire", "validate_in"): (_validate_in.ANCHOR, _validate_in.run),
|
|
22
|
+
("wire", "build_out"): (_build_out.ANCHOR, _build_out.run),
|
|
23
|
+
("wire", "dump"): (_dump.ANCHOR, _dump.run),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("uvicorn")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def subjects() -> Tuple[str, ...]:
|
|
30
|
+
"""Return the subject names exported by this domain."""
|
|
31
|
+
subjects = tuple(s for (_, s) in REGISTRY.keys())
|
|
32
|
+
logger.debug("Listing 'wire' subjects: %s", subjects)
|
|
33
|
+
return subjects
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get(subject: str) -> Tuple[str, RunFn]:
|
|
37
|
+
"""Return (anchor, runner) for a subject in the 'wire' domain."""
|
|
38
|
+
key = ("wire", subject)
|
|
39
|
+
if key not in REGISTRY:
|
|
40
|
+
raise KeyError(f"Unknown wire atom subject: {subject!r}")
|
|
41
|
+
logger.debug("Retrieving 'wire' subject %s", subject)
|
|
42
|
+
return REGISTRY[key]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
__all__ = ["REGISTRY", "RunFn", "subjects", "get"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/wire/build_in.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 in PRE_HANDLER just before validation.
|
|
10
|
+
ANCHOR = _ev.IN_VALIDATE # "in:validate"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""
|
|
17
|
+
wire:build_in@in:validate
|
|
18
|
+
|
|
19
|
+
Purpose
|
|
20
|
+
-------
|
|
21
|
+
Normalize the inbound request payload into canonical field names based on
|
|
22
|
+
the schema collected by schema:collect_in. This prepares a dict that
|
|
23
|
+
downstream atoms (resolve:assemble, validate_in, etc.) can use.
|
|
24
|
+
|
|
25
|
+
What it does
|
|
26
|
+
------------
|
|
27
|
+
- Reads ctx.temp["schema_in"] (built by schema:collect_in).
|
|
28
|
+
- Extracts a dict-like payload from ctx (ctx.in_data/payload/data/body or Pydantic model).
|
|
29
|
+
- Maps known *aliases* (alias_in) → canonical field names.
|
|
30
|
+
- Keeps only fields enabled for inbound IO; unknown keys captured to diagnostics.
|
|
31
|
+
- Distinguishes **ABSENT** (missing) from present(None) by *not* inserting missing keys.
|
|
32
|
+
- Stores results in ctx.temp["in_values"] and supporting diagnostics.
|
|
33
|
+
|
|
34
|
+
Notes
|
|
35
|
+
-----
|
|
36
|
+
- This atom does *not* perform validation—that belongs to wire:validate_in.
|
|
37
|
+
- For bulk inputs, adapters may pre-split and invoke the executor per-item.
|
|
38
|
+
"""
|
|
39
|
+
schema_in = _schema_in(ctx)
|
|
40
|
+
if not schema_in:
|
|
41
|
+
logger.debug("No schema_in available; skipping wire:build_in")
|
|
42
|
+
return # nothing to do
|
|
43
|
+
|
|
44
|
+
logger.debug("Running wire:build_in")
|
|
45
|
+
temp = _ensure_temp(ctx)
|
|
46
|
+
|
|
47
|
+
payload = _coerce_payload(ctx)
|
|
48
|
+
if not isinstance(payload, Mapping):
|
|
49
|
+
logger.debug("Payload is not a mapping; skipping normalization")
|
|
50
|
+
# Non-mapping payloads are ignored here; adapters can pre-normalize.
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
by_field: Mapping[str, Mapping[str, Any]] = schema_in.get("by_field", {}) # type: ignore[assignment]
|
|
54
|
+
# Build alias→field and ingress whitelist (field and alias forms)
|
|
55
|
+
alias_to_field: Dict[str, str] = {}
|
|
56
|
+
ingress_keys: set[str] = set()
|
|
57
|
+
|
|
58
|
+
for fname, entry in by_field.items():
|
|
59
|
+
alias = _safe_str(entry.get("alias_in"))
|
|
60
|
+
ingress_keys.add(fname)
|
|
61
|
+
if alias:
|
|
62
|
+
alias_to_field[alias] = fname
|
|
63
|
+
ingress_keys.add(alias)
|
|
64
|
+
|
|
65
|
+
# Normalize
|
|
66
|
+
in_values: Dict[str, Any] = {}
|
|
67
|
+
present_fields: set[str] = set()
|
|
68
|
+
unknown_keys: Dict[str, Any] = {}
|
|
69
|
+
|
|
70
|
+
# First pass: direct field-name matches win
|
|
71
|
+
for key, val in payload.items():
|
|
72
|
+
if key in by_field:
|
|
73
|
+
in_values[key] = val
|
|
74
|
+
present_fields.add(key)
|
|
75
|
+
else:
|
|
76
|
+
# Track unknowns for now; we may reclassify as alias below
|
|
77
|
+
unknown_keys[key] = val
|
|
78
|
+
|
|
79
|
+
# Second pass: alias matches for anything not already set
|
|
80
|
+
for key, val in list(unknown_keys.items()):
|
|
81
|
+
target = alias_to_field.get(key)
|
|
82
|
+
if target and target not in in_values:
|
|
83
|
+
logger.debug("Resolved alias %s -> %s", key, target)
|
|
84
|
+
in_values[target] = val
|
|
85
|
+
present_fields.add(target)
|
|
86
|
+
unknown_keys.pop(key, None)
|
|
87
|
+
|
|
88
|
+
# Keep minimal diagnostics
|
|
89
|
+
temp["in_values"] = in_values
|
|
90
|
+
temp["in_present"] = tuple(sorted(present_fields))
|
|
91
|
+
if unknown_keys:
|
|
92
|
+
temp["in_unknown"] = tuple(sorted(unknown_keys.keys()))
|
|
93
|
+
logger.debug("Unknown inbound keys: %s", list(unknown_keys.keys()))
|
|
94
|
+
# optionally stash raw unknowns for tooling (avoid huge payloads)
|
|
95
|
+
if len(unknown_keys) <= 16: # small guard
|
|
96
|
+
temp["in_unknown_samples"] = {
|
|
97
|
+
k: unknown_keys[k] for k in list(unknown_keys)[:16]
|
|
98
|
+
}
|
|
99
|
+
logger.debug("Normalized inbound values: %s", in_values)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
# Internals
|
|
104
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _ensure_temp(ctx: Any) -> MutableMapping[str, Any]:
|
|
108
|
+
tmp = getattr(ctx, "temp", None)
|
|
109
|
+
if not isinstance(tmp, dict):
|
|
110
|
+
tmp = {}
|
|
111
|
+
setattr(ctx, "temp", tmp)
|
|
112
|
+
return tmp
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _schema_in(ctx: Any) -> Mapping[str, Any]:
|
|
116
|
+
tmp = getattr(ctx, "temp", {})
|
|
117
|
+
sch = getattr(tmp, "get", lambda *_a, **_k: None)("schema_in") # type: ignore
|
|
118
|
+
if isinstance(sch, Mapping):
|
|
119
|
+
return sch
|
|
120
|
+
# allow adapters to stuff schema_in directly on ctx
|
|
121
|
+
sch2 = getattr(ctx, "schema_in", None)
|
|
122
|
+
return sch2 if isinstance(sch2, Mapping) else {}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _coerce_payload(ctx: Any) -> Mapping[str, Any] | Any:
|
|
126
|
+
"""
|
|
127
|
+
Try to obtain a dict-like payload from common places on the context.
|
|
128
|
+
Accepts Pydantic v1/v2 models and simple dataclasses.
|
|
129
|
+
"""
|
|
130
|
+
# Preferred explicit staging from router/adapters
|
|
131
|
+
for name in ("in_data", "payload", "data", "body"):
|
|
132
|
+
val = getattr(ctx, name, None)
|
|
133
|
+
if val is None:
|
|
134
|
+
continue
|
|
135
|
+
# Mapping already?
|
|
136
|
+
if isinstance(val, Mapping):
|
|
137
|
+
return val
|
|
138
|
+
# Pydantic v2
|
|
139
|
+
if hasattr(val, "model_dump") and callable(getattr(val, "model_dump")):
|
|
140
|
+
try:
|
|
141
|
+
return dict(val.model_dump())
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
# Pydantic v1
|
|
145
|
+
if hasattr(val, "dict") and callable(getattr(val, "dict")):
|
|
146
|
+
try:
|
|
147
|
+
return dict(val.dict())
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
# Dataclass?
|
|
151
|
+
try:
|
|
152
|
+
import dataclasses as _dc # local import; safe if missing
|
|
153
|
+
|
|
154
|
+
if _dc.is_dataclass(val):
|
|
155
|
+
return _dc.asdict(val)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass
|
|
158
|
+
return val # give back as-is; validator can complain later
|
|
159
|
+
return {}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _safe_str(v: Any) -> Optional[str]:
|
|
163
|
+
return v if isinstance(v, str) and v else None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Mapping, 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
|
+
# POST_HANDLER, runs before readtime aliases and dump.
|
|
10
|
+
ANCHOR = _ev.OUT_BUILD # "out:build"
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("uvicorn")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
16
|
+
"""Build canonical outbound values keyed by field name."""
|
|
17
|
+
logger.debug("Running wire:build_out")
|
|
18
|
+
ov = opview_from_ctx(ctx)
|
|
19
|
+
schema_out = ensure_schema_out(ctx, ov)
|
|
20
|
+
by_field = schema_out["by_field"]
|
|
21
|
+
expose = schema_out["expose"]
|
|
22
|
+
|
|
23
|
+
temp = _ensure_temp(ctx)
|
|
24
|
+
out_values: Dict[str, Any] = {}
|
|
25
|
+
produced_virtuals: list[str] = []
|
|
26
|
+
missing: list[str] = []
|
|
27
|
+
|
|
28
|
+
for field in expose:
|
|
29
|
+
desc = by_field.get(field, {})
|
|
30
|
+
if desc.get("virtual"):
|
|
31
|
+
producer = ov.virtual_producers.get(field)
|
|
32
|
+
if callable(producer):
|
|
33
|
+
try:
|
|
34
|
+
out_values[field] = producer(obj, ctx)
|
|
35
|
+
produced_virtuals.append(field)
|
|
36
|
+
logger.debug("Produced virtual field %s", field)
|
|
37
|
+
except Exception:
|
|
38
|
+
missing.append(field)
|
|
39
|
+
logger.debug("Virtual producer failed for field %s", field)
|
|
40
|
+
else:
|
|
41
|
+
missing.append(field)
|
|
42
|
+
logger.debug("No producer for virtual field %s", field)
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
value = _read_current_value(obj, ctx, field)
|
|
46
|
+
if value is None:
|
|
47
|
+
missing.append(field)
|
|
48
|
+
logger.debug("No value available for field %s", field)
|
|
49
|
+
out_values[field] = value
|
|
50
|
+
|
|
51
|
+
temp["out_values"] = out_values
|
|
52
|
+
if produced_virtuals:
|
|
53
|
+
temp["out_virtual_produced"] = tuple(produced_virtuals)
|
|
54
|
+
if missing:
|
|
55
|
+
temp["out_missing"] = tuple(missing)
|
|
56
|
+
logger.debug(
|
|
57
|
+
"Built outbound values: %s (virtuals=%s, missing=%s)",
|
|
58
|
+
out_values,
|
|
59
|
+
produced_virtuals,
|
|
60
|
+
missing,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
# Internals
|
|
66
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_current_value(obj: Optional[object], ctx: Any, field: str) -> Optional[Any]:
|
|
70
|
+
if obj is not None and hasattr(obj, field):
|
|
71
|
+
try:
|
|
72
|
+
return getattr(obj, field)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
for name in ("row", "values", "current_values"):
|
|
76
|
+
src = getattr(ctx, name, None)
|
|
77
|
+
if isinstance(src, Mapping) and field in src:
|
|
78
|
+
return src.get(field)
|
|
79
|
+
hv = getattr(getattr(ctx, "temp", {}), "get", lambda *a, **k: None)(
|
|
80
|
+
"hydrated_values"
|
|
81
|
+
) # type: ignore
|
|
82
|
+
if isinstance(hv, Mapping):
|
|
83
|
+
return hv.get(field)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
__all__ = ["ANCHOR", "run"]
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# tigrbl/v3/runtime/atoms/wire/dump.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import datetime as _dt
|
|
6
|
+
import decimal as _dc
|
|
7
|
+
import uuid as _uuid
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any, Dict, Mapping, MutableMapping, Optional
|
|
10
|
+
|
|
11
|
+
from ... import events as _ev
|
|
12
|
+
|
|
13
|
+
# Runs at the very end of model shaping; out:masking follows at the same anchor.
|
|
14
|
+
ANCHOR = _ev.OUT_DUMP # "out:dump"
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("uvicorn")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run(obj: Optional[object], ctx: Any) -> None:
|
|
20
|
+
"""
|
|
21
|
+
wire:dump@out:dump
|
|
22
|
+
|
|
23
|
+
Purpose
|
|
24
|
+
-------
|
|
25
|
+
Build the final wire payload (dict or list[dict]) from:
|
|
26
|
+
- ctx.temp["out_values"] (field -> value) produced by wire:build_out
|
|
27
|
+
- ctx.temp["schema_out"] (for alias mapping)
|
|
28
|
+
- ctx.temp["response_extras"] (alias extras from emit atoms)
|
|
29
|
+
|
|
30
|
+
What it does
|
|
31
|
+
------------
|
|
32
|
+
- Maps canonical field names → wire keys using alias_out (when provided).
|
|
33
|
+
- Optionally omits null values (cfg.exclude_none / cfg.omit_nulls).
|
|
34
|
+
- Applies minimal JSON-friendly scalar conversions (date/time/uuid/decimal/bytes).
|
|
35
|
+
- Merges response_extras (without overwriting unless configured) for single-object payloads.
|
|
36
|
+
- Stores the result in ctx.temp["response_payload"] for downstream masking/transport.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
- This step does NOT perform masking/redaction; that is handled by out:masking.
|
|
41
|
+
- For collection/list responses, extras are not merged (they are usually per-item).
|
|
42
|
+
"""
|
|
43
|
+
logger.debug("Running wire:dump")
|
|
44
|
+
temp = _ensure_temp(ctx)
|
|
45
|
+
out_values = temp.get("out_values")
|
|
46
|
+
|
|
47
|
+
if not out_values:
|
|
48
|
+
logger.debug("No out_values available; skipping dump")
|
|
49
|
+
return # nothing to dump
|
|
50
|
+
|
|
51
|
+
schema_out = _schema_out(ctx)
|
|
52
|
+
aliases: Mapping[str, str] = (
|
|
53
|
+
schema_out.get("aliases", {}) if isinstance(schema_out, Mapping) else {}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
omit_nulls = _omit_nulls(ctx)
|
|
57
|
+
allow_overwrite = _allow_extras_overwrite(ctx)
|
|
58
|
+
|
|
59
|
+
# Single object
|
|
60
|
+
if isinstance(out_values, Mapping):
|
|
61
|
+
logger.debug("Dumping single-object payload")
|
|
62
|
+
payload = _dump_one(out_values, aliases, omit_nulls)
|
|
63
|
+
# Merge extras (single-object only)
|
|
64
|
+
extras = temp.get("response_extras")
|
|
65
|
+
if isinstance(extras, Mapping) and extras:
|
|
66
|
+
conflicts = []
|
|
67
|
+
for k, v in extras.items():
|
|
68
|
+
if (k in payload) and not allow_overwrite:
|
|
69
|
+
conflicts.append(k)
|
|
70
|
+
logger.debug("Conflict on extra key %s", k)
|
|
71
|
+
continue
|
|
72
|
+
payload[k] = _dump_scalar(v)
|
|
73
|
+
if conflicts:
|
|
74
|
+
temp["dump_conflicts"] = tuple(sorted(set(conflicts)))
|
|
75
|
+
temp["response_payload"] = payload
|
|
76
|
+
logger.debug("Response payload built: %s", payload)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
# List/tuple of objects (already expanded by executor)
|
|
80
|
+
if isinstance(out_values, (list, tuple)) and all(
|
|
81
|
+
isinstance(x, Mapping) for x in out_values
|
|
82
|
+
):
|
|
83
|
+
logger.debug("Dumping list payload with %d items", len(out_values))
|
|
84
|
+
payload_list = [
|
|
85
|
+
_dump_one(item, aliases, omit_nulls)
|
|
86
|
+
for item in out_values # type: ignore[arg-type]
|
|
87
|
+
]
|
|
88
|
+
temp["response_payload"] = payload_list
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Unknown shape — stash as-is to avoid surprises (transport may serialize).
|
|
92
|
+
temp["response_payload"] = out_values
|
|
93
|
+
logger.debug("Stored opaque response payload: %s", type(out_values).__name__)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
# Internals
|
|
99
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _ensure_temp(ctx: Any) -> MutableMapping[str, Any]:
|
|
103
|
+
tmp = getattr(ctx, "temp", None)
|
|
104
|
+
if not isinstance(tmp, dict):
|
|
105
|
+
tmp = {}
|
|
106
|
+
setattr(ctx, "temp", tmp)
|
|
107
|
+
return tmp
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _schema_out(ctx: Any) -> Mapping[str, Any]:
|
|
111
|
+
tmp = getattr(ctx, "temp", {})
|
|
112
|
+
sch = getattr(tmp, "get", lambda *_a, **_k: None)("schema_out") # type: ignore
|
|
113
|
+
if isinstance(sch, Mapping):
|
|
114
|
+
return sch
|
|
115
|
+
sch2 = getattr(ctx, "schema_out", None)
|
|
116
|
+
return sch2 if isinstance(sch2, Mapping) else {}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _omit_nulls(ctx: Any) -> bool:
|
|
120
|
+
"""
|
|
121
|
+
Config flags to drop null-valued keys:
|
|
122
|
+
- cfg.exclude_none (preferred)
|
|
123
|
+
- cfg.omit_nulls
|
|
124
|
+
Default False.
|
|
125
|
+
"""
|
|
126
|
+
cfg = getattr(ctx, "cfg", None)
|
|
127
|
+
for name in ("exclude_none", "omit_nulls"):
|
|
128
|
+
val = getattr(cfg, name, None)
|
|
129
|
+
if isinstance(val, bool):
|
|
130
|
+
return val
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _allow_extras_overwrite(ctx: Any) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
If True, extras can overwrite existing keys; default False.
|
|
137
|
+
cfg.extras_overwrite or cfg.response_extras_overwrite.
|
|
138
|
+
"""
|
|
139
|
+
cfg = getattr(ctx, "cfg", None)
|
|
140
|
+
for name in ("response_extras_overwrite", "extras_overwrite"):
|
|
141
|
+
val = getattr(cfg, name, None)
|
|
142
|
+
if isinstance(val, bool):
|
|
143
|
+
return val
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _dump_one(
|
|
148
|
+
values: Mapping[str, Any], aliases: Mapping[str, str], omit_nulls: bool
|
|
149
|
+
) -> Dict[str, Any]:
|
|
150
|
+
"""
|
|
151
|
+
Convert a single out_values mapping to a wire payload dict with alias mapping and scalar dumps.
|
|
152
|
+
"""
|
|
153
|
+
out: Dict[str, Any] = {}
|
|
154
|
+
used_aliases: list[str] = []
|
|
155
|
+
omitted: list[str] = []
|
|
156
|
+
|
|
157
|
+
for field, val in values.items():
|
|
158
|
+
if omit_nulls and val is None:
|
|
159
|
+
omitted.append(field)
|
|
160
|
+
continue
|
|
161
|
+
key = aliases.get(field) or field
|
|
162
|
+
if key != field:
|
|
163
|
+
used_aliases.append(key)
|
|
164
|
+
out[key] = _dump_scalar(val)
|
|
165
|
+
|
|
166
|
+
if used_aliases:
|
|
167
|
+
# diagnostics hook
|
|
168
|
+
# note: this goes on the payload's temp, not user-visible
|
|
169
|
+
pass
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _dump_scalar(v: Any) -> Any:
|
|
174
|
+
"""
|
|
175
|
+
Minimal JSON-friendly conversion for common scalars.
|
|
176
|
+
Leave complex types as-is; transport may have its own encoder.
|
|
177
|
+
"""
|
|
178
|
+
if v is None:
|
|
179
|
+
return None
|
|
180
|
+
if isinstance(v, (_dt.datetime, _dt.date, _dt.time)):
|
|
181
|
+
# ISO 8601
|
|
182
|
+
try:
|
|
183
|
+
return v.isoformat()
|
|
184
|
+
except Exception:
|
|
185
|
+
return str(v)
|
|
186
|
+
if isinstance(v, _uuid.UUID):
|
|
187
|
+
return str(v)
|
|
188
|
+
if isinstance(v, _dc.Decimal):
|
|
189
|
+
# Preserve precision via string
|
|
190
|
+
return str(v)
|
|
191
|
+
if isinstance(v, (bytes, bytearray, memoryview)):
|
|
192
|
+
try:
|
|
193
|
+
return base64.b64encode(bytes(v)).decode("ascii")
|
|
194
|
+
except Exception:
|
|
195
|
+
return None
|
|
196
|
+
# Plain containers → recurse shallowly
|
|
197
|
+
if isinstance(v, Mapping):
|
|
198
|
+
return {k: _dump_scalar(v[k]) for k in v}
|
|
199
|
+
if isinstance(v, list):
|
|
200
|
+
return [_dump_scalar(x) for x in v]
|
|
201
|
+
if isinstance(v, tuple):
|
|
202
|
+
return tuple(_dump_scalar(x) for x in v)
|
|
203
|
+
return v
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
__all__ = ["ANCHOR", "run"]
|