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.
Files changed (269) 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 +97 -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 +291 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +86 -0
  11. tigrbl/app/_model_registry.py +41 -0
  12. tigrbl/app/app_spec.py +42 -0
  13. tigrbl/app/mro_collect.py +67 -0
  14. tigrbl/app/shortcuts.py +65 -0
  15. tigrbl/app/tigrbl_app.py +319 -0
  16. tigrbl/bindings/__init__.py +73 -0
  17. tigrbl/bindings/api/__init__.py +12 -0
  18. tigrbl/bindings/api/common.py +109 -0
  19. tigrbl/bindings/api/include.py +256 -0
  20. tigrbl/bindings/api/resource_proxy.py +149 -0
  21. tigrbl/bindings/api/rpc.py +111 -0
  22. tigrbl/bindings/columns.py +49 -0
  23. tigrbl/bindings/handlers/__init__.py +11 -0
  24. tigrbl/bindings/handlers/builder.py +119 -0
  25. tigrbl/bindings/handlers/ctx.py +74 -0
  26. tigrbl/bindings/handlers/identifiers.py +228 -0
  27. tigrbl/bindings/handlers/namespaces.py +51 -0
  28. tigrbl/bindings/handlers/steps.py +276 -0
  29. tigrbl/bindings/hooks.py +311 -0
  30. tigrbl/bindings/model.py +194 -0
  31. tigrbl/bindings/model_helpers.py +139 -0
  32. tigrbl/bindings/model_registry.py +77 -0
  33. tigrbl/bindings/rest/__init__.py +7 -0
  34. tigrbl/bindings/rest/attach.py +34 -0
  35. tigrbl/bindings/rest/collection.py +286 -0
  36. tigrbl/bindings/rest/common.py +120 -0
  37. tigrbl/bindings/rest/fastapi.py +76 -0
  38. tigrbl/bindings/rest/helpers.py +119 -0
  39. tigrbl/bindings/rest/io.py +317 -0
  40. tigrbl/bindings/rest/io_headers.py +49 -0
  41. tigrbl/bindings/rest/member.py +386 -0
  42. tigrbl/bindings/rest/router.py +296 -0
  43. tigrbl/bindings/rest/routing.py +153 -0
  44. tigrbl/bindings/rpc.py +364 -0
  45. tigrbl/bindings/schemas/__init__.py +11 -0
  46. tigrbl/bindings/schemas/builder.py +348 -0
  47. tigrbl/bindings/schemas/defaults.py +260 -0
  48. tigrbl/bindings/schemas/utils.py +193 -0
  49. tigrbl/column/README.md +62 -0
  50. tigrbl/column/__init__.py +72 -0
  51. tigrbl/column/_column.py +96 -0
  52. tigrbl/column/column_spec.py +40 -0
  53. tigrbl/column/field_spec.py +31 -0
  54. tigrbl/column/infer/__init__.py +25 -0
  55. tigrbl/column/infer/core.py +92 -0
  56. tigrbl/column/infer/jsonhints.py +44 -0
  57. tigrbl/column/infer/planning.py +133 -0
  58. tigrbl/column/infer/types.py +102 -0
  59. tigrbl/column/infer/utils.py +59 -0
  60. tigrbl/column/io_spec.py +136 -0
  61. tigrbl/column/mro_collect.py +59 -0
  62. tigrbl/column/shortcuts.py +89 -0
  63. tigrbl/column/storage_spec.py +65 -0
  64. tigrbl/config/__init__.py +19 -0
  65. tigrbl/config/constants.py +224 -0
  66. tigrbl/config/defaults.py +29 -0
  67. tigrbl/config/resolver.py +295 -0
  68. tigrbl/core/__init__.py +47 -0
  69. tigrbl/core/crud/__init__.py +36 -0
  70. tigrbl/core/crud/bulk.py +168 -0
  71. tigrbl/core/crud/helpers/__init__.py +76 -0
  72. tigrbl/core/crud/helpers/db.py +92 -0
  73. tigrbl/core/crud/helpers/enum.py +86 -0
  74. tigrbl/core/crud/helpers/filters.py +162 -0
  75. tigrbl/core/crud/helpers/model.py +123 -0
  76. tigrbl/core/crud/helpers/normalize.py +99 -0
  77. tigrbl/core/crud/ops.py +235 -0
  78. tigrbl/ddl/__init__.py +344 -0
  79. tigrbl/decorators.py +17 -0
  80. tigrbl/deps/__init__.py +20 -0
  81. tigrbl/deps/fastapi.py +45 -0
  82. tigrbl/deps/favicon.svg +4 -0
  83. tigrbl/deps/jinja.py +27 -0
  84. tigrbl/deps/pydantic.py +10 -0
  85. tigrbl/deps/sqlalchemy.py +94 -0
  86. tigrbl/deps/starlette.py +36 -0
  87. tigrbl/engine/__init__.py +45 -0
  88. tigrbl/engine/_engine.py +144 -0
  89. tigrbl/engine/bind.py +33 -0
  90. tigrbl/engine/builders.py +236 -0
  91. tigrbl/engine/capabilities.py +29 -0
  92. tigrbl/engine/collect.py +111 -0
  93. tigrbl/engine/decorators.py +110 -0
  94. tigrbl/engine/docs/PLUGINS.md +49 -0
  95. tigrbl/engine/engine_spec.py +355 -0
  96. tigrbl/engine/plugins.py +52 -0
  97. tigrbl/engine/registry.py +36 -0
  98. tigrbl/engine/resolver.py +224 -0
  99. tigrbl/engine/shortcuts.py +216 -0
  100. tigrbl/hook/__init__.py +21 -0
  101. tigrbl/hook/_hook.py +22 -0
  102. tigrbl/hook/decorators.py +28 -0
  103. tigrbl/hook/hook_spec.py +24 -0
  104. tigrbl/hook/mro_collect.py +98 -0
  105. tigrbl/hook/shortcuts.py +44 -0
  106. tigrbl/hook/types.py +76 -0
  107. tigrbl/op/__init__.py +50 -0
  108. tigrbl/op/_op.py +31 -0
  109. tigrbl/op/canonical.py +31 -0
  110. tigrbl/op/collect.py +11 -0
  111. tigrbl/op/decorators.py +238 -0
  112. tigrbl/op/model_registry.py +301 -0
  113. tigrbl/op/mro_collect.py +99 -0
  114. tigrbl/op/resolver.py +216 -0
  115. tigrbl/op/types.py +136 -0
  116. tigrbl/orm/__init__.py +1 -0
  117. tigrbl/orm/mixins/_RowBound.py +83 -0
  118. tigrbl/orm/mixins/__init__.py +95 -0
  119. tigrbl/orm/mixins/bootstrappable.py +113 -0
  120. tigrbl/orm/mixins/bound.py +47 -0
  121. tigrbl/orm/mixins/edges.py +40 -0
  122. tigrbl/orm/mixins/fields.py +165 -0
  123. tigrbl/orm/mixins/hierarchy.py +54 -0
  124. tigrbl/orm/mixins/key_digest.py +44 -0
  125. tigrbl/orm/mixins/lifecycle.py +115 -0
  126. tigrbl/orm/mixins/locks.py +51 -0
  127. tigrbl/orm/mixins/markers.py +16 -0
  128. tigrbl/orm/mixins/operations.py +57 -0
  129. tigrbl/orm/mixins/ownable.py +337 -0
  130. tigrbl/orm/mixins/principals.py +98 -0
  131. tigrbl/orm/mixins/tenant_bound.py +301 -0
  132. tigrbl/orm/mixins/upsertable.py +118 -0
  133. tigrbl/orm/mixins/utils.py +49 -0
  134. tigrbl/orm/tables/__init__.py +72 -0
  135. tigrbl/orm/tables/_base.py +8 -0
  136. tigrbl/orm/tables/audit.py +56 -0
  137. tigrbl/orm/tables/client.py +25 -0
  138. tigrbl/orm/tables/group.py +29 -0
  139. tigrbl/orm/tables/org.py +30 -0
  140. tigrbl/orm/tables/rbac.py +76 -0
  141. tigrbl/orm/tables/status.py +106 -0
  142. tigrbl/orm/tables/tenant.py +22 -0
  143. tigrbl/orm/tables/user.py +39 -0
  144. tigrbl/response/README.md +34 -0
  145. tigrbl/response/__init__.py +33 -0
  146. tigrbl/response/bind.py +12 -0
  147. tigrbl/response/decorators.py +37 -0
  148. tigrbl/response/resolver.py +83 -0
  149. tigrbl/response/shortcuts.py +171 -0
  150. tigrbl/response/types.py +49 -0
  151. tigrbl/rest/__init__.py +27 -0
  152. tigrbl/runtime/README.md +129 -0
  153. tigrbl/runtime/__init__.py +20 -0
  154. tigrbl/runtime/atoms/__init__.py +102 -0
  155. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  156. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  157. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  158. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  159. tigrbl/runtime/atoms/out/__init__.py +38 -0
  160. tigrbl/runtime/atoms/out/masking.py +135 -0
  161. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  162. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  163. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  164. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  165. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  166. tigrbl/runtime/atoms/response/__init__.py +19 -0
  167. tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
  168. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  169. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  170. tigrbl/runtime/atoms/response/render.py +36 -0
  171. tigrbl/runtime/atoms/response/renderer.py +116 -0
  172. tigrbl/runtime/atoms/response/template.py +44 -0
  173. tigrbl/runtime/atoms/response/templates.py +88 -0
  174. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  175. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  176. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  177. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  178. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  179. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  180. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  181. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  182. tigrbl/runtime/atoms/wire/dump.py +206 -0
  183. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  184. tigrbl/runtime/context.py +206 -0
  185. tigrbl/runtime/errors/__init__.py +61 -0
  186. tigrbl/runtime/errors/converters.py +214 -0
  187. tigrbl/runtime/errors/exceptions.py +124 -0
  188. tigrbl/runtime/errors/mappings.py +71 -0
  189. tigrbl/runtime/errors/utils.py +150 -0
  190. tigrbl/runtime/events.py +209 -0
  191. tigrbl/runtime/executor/__init__.py +6 -0
  192. tigrbl/runtime/executor/guards.py +132 -0
  193. tigrbl/runtime/executor/helpers.py +88 -0
  194. tigrbl/runtime/executor/invoke.py +150 -0
  195. tigrbl/runtime/executor/types.py +84 -0
  196. tigrbl/runtime/kernel.py +644 -0
  197. tigrbl/runtime/labels.py +353 -0
  198. tigrbl/runtime/opview.py +89 -0
  199. tigrbl/runtime/ordering.py +256 -0
  200. tigrbl/runtime/system.py +279 -0
  201. tigrbl/runtime/trace.py +330 -0
  202. tigrbl/schema/__init__.py +38 -0
  203. tigrbl/schema/_schema.py +27 -0
  204. tigrbl/schema/builder/__init__.py +17 -0
  205. tigrbl/schema/builder/build_schema.py +209 -0
  206. tigrbl/schema/builder/cache.py +24 -0
  207. tigrbl/schema/builder/compat.py +16 -0
  208. tigrbl/schema/builder/extras.py +85 -0
  209. tigrbl/schema/builder/helpers.py +51 -0
  210. tigrbl/schema/builder/list_params.py +117 -0
  211. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  212. tigrbl/schema/collect.py +79 -0
  213. tigrbl/schema/decorators.py +68 -0
  214. tigrbl/schema/get_schema.py +86 -0
  215. tigrbl/schema/schema_spec.py +20 -0
  216. tigrbl/schema/shortcuts.py +42 -0
  217. tigrbl/schema/types.py +34 -0
  218. tigrbl/schema/utils.py +143 -0
  219. tigrbl/session/README.md +14 -0
  220. tigrbl/session/__init__.py +28 -0
  221. tigrbl/session/abc.py +76 -0
  222. tigrbl/session/base.py +151 -0
  223. tigrbl/session/decorators.py +43 -0
  224. tigrbl/session/default.py +118 -0
  225. tigrbl/session/shortcuts.py +50 -0
  226. tigrbl/session/spec.py +112 -0
  227. tigrbl/shortcuts.py +22 -0
  228. tigrbl/specs.py +44 -0
  229. tigrbl/system/__init__.py +13 -0
  230. tigrbl/system/diagnostics/__init__.py +24 -0
  231. tigrbl/system/diagnostics/compat.py +31 -0
  232. tigrbl/system/diagnostics/healthz.py +41 -0
  233. tigrbl/system/diagnostics/hookz.py +51 -0
  234. tigrbl/system/diagnostics/kernelz.py +20 -0
  235. tigrbl/system/diagnostics/methodz.py +43 -0
  236. tigrbl/system/diagnostics/router.py +73 -0
  237. tigrbl/system/diagnostics/utils.py +43 -0
  238. tigrbl/system/uvicorn.py +60 -0
  239. tigrbl/table/__init__.py +9 -0
  240. tigrbl/table/_base.py +260 -0
  241. tigrbl/table/_table.py +54 -0
  242. tigrbl/table/mro_collect.py +69 -0
  243. tigrbl/table/shortcuts.py +57 -0
  244. tigrbl/table/table_spec.py +28 -0
  245. tigrbl/transport/__init__.py +74 -0
  246. tigrbl/transport/jsonrpc/__init__.py +19 -0
  247. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  248. tigrbl/transport/jsonrpc/helpers.py +115 -0
  249. tigrbl/transport/jsonrpc/models.py +41 -0
  250. tigrbl/transport/rest/__init__.py +25 -0
  251. tigrbl/transport/rest/aggregator.py +132 -0
  252. tigrbl/types/__init__.py +170 -0
  253. tigrbl/types/allow_anon_provider.py +19 -0
  254. tigrbl/types/authn_abc.py +30 -0
  255. tigrbl/types/nested_path_provider.py +22 -0
  256. tigrbl/types/op.py +35 -0
  257. tigrbl/types/op_config_provider.py +17 -0
  258. tigrbl/types/op_verb_alias_provider.py +33 -0
  259. tigrbl/types/request_extras_provider.py +22 -0
  260. tigrbl/types/response_extras_provider.py +22 -0
  261. tigrbl/types/table_config_provider.py +13 -0
  262. tigrbl/types/uuid.py +55 -0
  263. tigrbl-0.3.0.dist-info/METADATA +516 -0
  264. tigrbl-0.3.0.dist-info/RECORD +266 -0
  265. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
  266. tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
  267. tigrbl/ExampleAgent.py +0 -1
  268. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  269. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
@@ -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"]
@@ -0,0 +1,227 @@
1
+ # tigrbl/v3/runtime/atoms/wire/validate_in.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ import decimal as _dc
6
+ import uuid as _uuid
7
+ import logging
8
+ from typing import Any, Dict, Mapping, Optional, Tuple
9
+
10
+ from fastapi import HTTPException, status as _status
11
+
12
+ from ... import events as _ev
13
+ from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
14
+
15
+ # PRE_HANDLER, runs after wire:build_in
16
+ ANCHOR = _ev.IN_VALIDATE # "in:validate"
17
+
18
+ logger = logging.getLogger("uvicorn")
19
+
20
+
21
+ def run(obj: Optional[object], ctx: Any) -> None:
22
+ """
23
+ wire:validate_in@in:validate
24
+
25
+ Validates the normalized inbound payload (ctx.temp["in_values"]) using the
26
+ schema collected by schema:collect_in plus tolerant signals from ColumnSpec.
27
+
28
+ What it checks
29
+ --------------
30
+ - Required fields are present (ABSENT → error; present(None) is a separate check)
31
+ - Nullability (value is None while column is non-nullable → error)
32
+ - Lightweight type conformance (optional coercion if enabled)
33
+ - String max_length (when available)
34
+ - Author validators (validate_in / in_validator / validator_in on ColumnSpec/FieldSpec/IOSpec)
35
+
36
+ Effects
37
+ -------
38
+ - Mutates ctx.temp["in_values"] with any successful coercions/validator returns
39
+ - Writes diagnostics to:
40
+ ctx.temp["in_errors"] : list of {field, code, message}
41
+ ctx.temp["in_invalid"] : bool
42
+ ctx.temp["in_coerced"] : tuple of coerced field names (if any)
43
+ - Raises HTTPException(422) if any validation errors are found
44
+ """
45
+ logger.debug("Running wire:validate_in")
46
+ temp = _ensure_temp(ctx)
47
+ ov = opview_from_ctx(ctx)
48
+ schema_in = ensure_schema_in(ctx, ov)
49
+
50
+ in_values: Dict[str, Any] = dict(temp.get("in_values") or {})
51
+ by_field: Mapping[str, Mapping[str, Any]] = schema_in.get("by_field", {}) # type: ignore[assignment]
52
+ required: Tuple[str, ...] = tuple(schema_in.get("required", ())) # type: ignore[assignment]
53
+
54
+ errors: list[Dict[str, Any]] = []
55
+ coerced: list[str] = []
56
+
57
+ # 1) Required presence (ABSENT → error)
58
+ for name in required:
59
+ if name not in in_values:
60
+ logger.debug("Required field %s missing", name)
61
+ errors.append(
62
+ _err(name, "required", "Field is required but was not provided.")
63
+ )
64
+
65
+ # 2) Per-field validation
66
+ for name, value in list(in_values.items()):
67
+ entry = by_field.get(name) or {}
68
+
69
+ # Nullability
70
+ nullable = entry.get("nullable", None)
71
+ if value is None and nullable is False:
72
+ logger.debug("Field %s is null but not nullable", name)
73
+ errors.append(
74
+ _err(name, "null_not_allowed", "Null is not allowed for this field.")
75
+ )
76
+ continue # skip further checks for this field
77
+
78
+ # Type (optionally coerce)
79
+ target_type = entry.get("py_type")
80
+ if value is not None and isinstance(target_type, type):
81
+ allow_coerce = bool(entry.get("coerce", True))
82
+ ok, new_val, msg = _coerce_if_needed(value, target_type, allow=allow_coerce)
83
+ if not ok:
84
+ logger.debug("Type mismatch for field %s", name)
85
+ errors.append(
86
+ _err(
87
+ name,
88
+ "type_mismatch",
89
+ msg or f"Expected {_type_name(target_type)}.",
90
+ )
91
+ )
92
+ continue
93
+ if new_val is not value:
94
+ in_values[name] = new_val
95
+ coerced.append(name)
96
+
97
+ # Max length (strings)
98
+ max_len = entry.get("max_length", None)
99
+ if (
100
+ isinstance(max_len, int)
101
+ and max_len > 0
102
+ and isinstance(in_values.get(name), str)
103
+ ):
104
+ if len(in_values[name]) > max_len:
105
+ logger.debug("Field %s exceeds max_length %d", name, max_len)
106
+ errors.append(
107
+ _err(name, "max_length", f"String exceeds max_length={max_len}.")
108
+ )
109
+ continue
110
+
111
+ # Author-supplied validator(s)
112
+ vfn = entry.get("validator")
113
+ if callable(vfn) and in_values.get(name) is not None:
114
+ try:
115
+ out = vfn(in_values[name], ctx)
116
+ if out is not None:
117
+ in_values[name] = out
118
+ except Exception as e:
119
+ logger.debug("Validator failed for field %s: %s", name, e)
120
+ errors.append(
121
+ _err(name, "validator_failed", f"{type(e).__name__}: {e}")
122
+ )
123
+ continue
124
+
125
+ # 3) Unknown keys policy (handled after build_in captured samples)
126
+ unknown = tuple(temp.get("in_unknown") or ())
127
+ if unknown and _reject_unknown(ctx):
128
+ logger.debug("Rejecting unknown fields: %s", unknown)
129
+ for k in unknown:
130
+ errors.append(_err(k, "unknown_field", "Unknown input key."))
131
+
132
+ # Persist results + diagnostics
133
+ temp["in_values"] = in_values
134
+ if coerced:
135
+ temp["in_coerced"] = tuple(coerced)
136
+
137
+ if errors:
138
+ temp["in_errors"] = errors
139
+ temp["in_invalid"] = True
140
+ logger.debug("Validation errors found: %s", errors)
141
+ raise HTTPException(
142
+ status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY, detail=errors
143
+ )
144
+ else:
145
+ temp["in_invalid"] = False
146
+ logger.debug("Inbound payload validated successfully")
147
+
148
+
149
+ # ──────────────────────────────────────────────────────────────────────────────
150
+ # Internals
151
+ # ──────────────────────────────────────────────────────────────────────────────
152
+
153
+
154
+ def _reject_unknown(ctx: Any) -> bool:
155
+ """
156
+ Check cfg for a 'reject_unknown_fields' (bool); default False.
157
+ """
158
+ cfg = getattr(ctx, "cfg", None)
159
+ val = getattr(cfg, "reject_unknown_fields", None)
160
+ return bool(val) if isinstance(val, bool) else False
161
+
162
+
163
+ def _err(field: str, code: str, msg: str) -> Dict[str, Any]:
164
+ return {"field": field, "code": code, "message": msg}
165
+
166
+
167
+ def _type_name(t: type) -> str:
168
+ return getattr(t, "__name__", str(t))
169
+
170
+
171
+ # ── coercion helpers ──────────────────────────────────────────────────────────
172
+
173
+
174
+ def _coerce_if_needed(
175
+ value: Any, target: type, *, allow: bool
176
+ ) -> Tuple[bool, Any, Optional[str]]:
177
+ """Return (ok, new_value, error_message). Only coerces when allow=True."""
178
+ if isinstance(value, target):
179
+ return True, value, None
180
+ if not allow:
181
+ return False, value, f"Expected {_type_name(target)}."
182
+ try:
183
+ coerced = _coerce(value, target)
184
+ return True, coerced, None
185
+ except Exception:
186
+ return False, value, f"Could not convert to {_type_name(target)}."
187
+
188
+
189
+ def _coerce(value: Any, target: type) -> Any:
190
+ if target is str:
191
+ return str(value)
192
+ if target is int:
193
+ if isinstance(value, bool):
194
+ return int(value)
195
+ return int(str(value).strip())
196
+ if target is float:
197
+ return float(str(value).strip())
198
+ if target is bool:
199
+ if isinstance(value, bool):
200
+ return value
201
+ s = str(value).strip().lower()
202
+ if s in {"true", "1", "yes", "y", "on"}:
203
+ return True
204
+ if s in {"false", "0", "no", "n", "off"}:
205
+ return False
206
+ raise ValueError("not a boolean")
207
+ if target is _dc.Decimal:
208
+ return _dc.Decimal(str(value).strip())
209
+ if target is _uuid.UUID:
210
+ return _uuid.UUID(str(value))
211
+ if target is _dt.datetime:
212
+ # allow both date-time and date-only (promote to midnight)
213
+ s = str(value).strip()
214
+ try:
215
+ return _dt.datetime.fromisoformat(s)
216
+ except Exception:
217
+ d = _dt.date.fromisoformat(s)
218
+ return _dt.datetime.combine(d, _dt.time())
219
+ if target is _dt.date:
220
+ return _dt.date.fromisoformat(str(value).strip())
221
+ if target is _dt.time:
222
+ return _dt.time.fromisoformat(str(value).strip())
223
+ # Fallback: try direct construction
224
+ return target(value)
225
+
226
+
227
+ __all__ = ["ANCHOR", "run"]