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.
Files changed (252) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +72 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.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"]