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,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"]
@@ -0,0 +1,206 @@
1
+ # tigrbl/v3/runtime/context.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, Mapping, Optional, Sequence
7
+
8
+
9
+ def _canon_op(op: Optional[str]) -> str:
10
+ return (op or "").strip().lower() or "unknown"
11
+
12
+
13
+ @dataclass
14
+ class Context:
15
+ """
16
+ Canonical runtime context shared by the kernel and atoms.
17
+
18
+ Minimal contract (consumed by atoms we’ve written so far):
19
+ - op: operation name (e.g., 'create' | 'update' | 'read' | 'list' | custom)
20
+ - persist: write vs. read (affects pruning of persist-tied anchors)
21
+ - specs: mapping of field -> ColumnSpec (frozen at bind time)
22
+ - cfg: read-only config view (see config.resolver.CfgView)
23
+ - temp: dict scratchpad used by atoms to exchange data
24
+
25
+ Optional adapter slots:
26
+ - model: owning model type / class
27
+ - obj: hydrated ORM instance (if any)
28
+ - session: DB session / unit-of-work handle
29
+ - user, tenant, now: identity/time hints
30
+ - row/values/current_values: mapping fallbacks (for read paths)
31
+ - in_data / payload / data / body: inbound payload staging (for build_in)
32
+ """
33
+
34
+ # core
35
+ op: str
36
+ persist: bool
37
+ specs: Mapping[str, Any]
38
+ cfg: Any
39
+
40
+ # shared scratchpad
41
+ temp: Dict[str, Any] = field(default_factory=dict)
42
+
43
+ # optional context
44
+ model: Any | None = None
45
+ obj: Any | None = None
46
+ session: Any | None = None
47
+
48
+ # identity/time
49
+ user: Any | None = None
50
+ tenant: Any | None = None
51
+ now: _dt.datetime | None = None
52
+
53
+ # read-path fallbacks
54
+ row: Mapping[str, Any] | None = None
55
+ values: Mapping[str, Any] | None = None
56
+ current_values: Mapping[str, Any] | None = None
57
+
58
+ # inbound staging (router/adapters may set any one of these)
59
+ in_data: Any | None = None
60
+ payload: Any | None = None
61
+ data: Any | None = None
62
+ body: Any | None = None
63
+
64
+ def __post_init__(self) -> None:
65
+ self.op = _canon_op(self.op)
66
+ # Normalize now to a timezone-aware UTC timestamp when not provided
67
+ if self.now is None:
68
+ try:
69
+ self.now = _dt.datetime.now(_dt.timezone.utc)
70
+ except Exception: # pragma: no cover
71
+ self.now = _dt.datetime.utcnow().replace(tzinfo=None)
72
+
73
+ # Ensure temp is a dict (atoms rely on it)
74
+ if not isinstance(self.temp, dict):
75
+ self.temp = dict(self.temp)
76
+
77
+ # ── convenience flags ─────────────────────────────────────────────────────
78
+
79
+ @property
80
+ def is_write(self) -> bool:
81
+ """Alias for persist; reads better in some call sites."""
82
+ return bool(self.persist)
83
+
84
+ # ── safe read-only view for user callables (generators, default_factory) ──
85
+
86
+ def safe_view(
87
+ self,
88
+ *,
89
+ include_temp: bool = False,
90
+ temp_keys: Optional[Sequence[str]] = None,
91
+ ) -> Mapping[str, Any]:
92
+ """
93
+ Return a small, read-only mapping exposing only safe, frequently useful keys.
94
+
95
+ By default, temp is NOT included (to avoid leaking internals like paired raw values).
96
+ If include_temp=True, only exposes the keys listed in 'temp_keys' (if provided),
97
+ otherwise exposes a conservative subset.
98
+
99
+ This method is intended to be passed into author callables such as
100
+ default_factory(ctx_view) or paired token generators.
101
+ """
102
+ base = {
103
+ "op": self.op,
104
+ "persist": self.persist,
105
+ "model": self.model,
106
+ "specs": self.specs,
107
+ "user": self.user,
108
+ "tenant": self.tenant,
109
+ "now": self.now,
110
+ }
111
+ if include_temp:
112
+ allowed = set(temp_keys or ("assembled_values", "virtual_in"))
113
+ exposed: Dict[str, Any] = {}
114
+ for k in allowed:
115
+ if k in self.temp:
116
+ exposed[k] = self.temp[k]
117
+ base = {**base, "temp": MappingProxy(exposed)}
118
+ return MappingProxy(base)
119
+
120
+ # ── tiny helpers used by atoms / kernel ───────────────────────────────────
121
+
122
+ def mark_used_returning(self, value: bool = True) -> None:
123
+ """Flag that DB RETURNING already hydrated values."""
124
+ self.temp["used_returning"] = bool(value)
125
+
126
+ def merge_hydrated_values(
127
+ self, mapping: Mapping[str, Any], *, replace: bool = False
128
+ ) -> None:
129
+ """
130
+ Save values hydrated from DB (RETURNING/refresh). If replace=False (default),
131
+ performs a shallow merge into any existing 'hydrated_values'.
132
+ """
133
+ if not isinstance(mapping, Mapping):
134
+ return
135
+ hv = self.temp.get("hydrated_values")
136
+ if replace or not isinstance(hv, dict):
137
+ self.temp["hydrated_values"] = dict(mapping)
138
+ else:
139
+ hv.update(mapping)
140
+
141
+ def add_response_extras(
142
+ self, extras: Mapping[str, Any], *, overwrite: Optional[bool] = None
143
+ ) -> Sequence[str]:
144
+ """
145
+ Merge alias extras into temp['response_extras'].
146
+ Returns a tuple of conflicting keys that were skipped when overwrite=False.
147
+ """
148
+ if not isinstance(extras, Mapping) or not extras:
149
+ return ()
150
+ buf = self.temp.get("response_extras")
151
+ if not isinstance(buf, dict):
152
+ buf = {}
153
+ self.temp["response_extras"] = buf
154
+ if overwrite is None:
155
+ # fall back to cfg; atoms call wire:dump to honor final overwrite policy
156
+ overwrite = bool(getattr(self.cfg, "response_extras_overwrite", False))
157
+ conflicts: list[str] = []
158
+ for k, v in extras.items():
159
+ if (k in buf) and not overwrite:
160
+ conflicts.append(k)
161
+ continue
162
+ buf[k] = v
163
+ return tuple(conflicts)
164
+
165
+ def get_response_payload(self) -> Any:
166
+ """Return the payload assembled by wire:dump (or None if not yet available)."""
167
+ return self.temp.get("response_payload")
168
+
169
+ # ── representation (avoid leaking large/sensitive temp contents) ──────────
170
+
171
+ def __repr__(self) -> str: # pragma: no cover
172
+ model_name = getattr(self.model, "__name__", None) or str(self.model)
173
+ return (
174
+ f"Context(op={self.op!r}, persist={self.persist}, model={model_name!r}, "
175
+ f"user={(getattr(self.user, 'id', None) or None)!r}, temp_keys={sorted(self.temp.keys())})"
176
+ )
177
+
178
+
179
+ # ── tiny immutable mapping proxy (local; no external deps) ────────────────────
180
+
181
+
182
+ class MappingProxy(Mapping[str, Any]):
183
+ """A lightweight, read-only mapping wrapper."""
184
+
185
+ __slots__ = ("_d",)
186
+
187
+ def __init__(self, data: Mapping[str, Any]):
188
+ self._d = dict(data)
189
+
190
+ def __getitem__(self, k: str) -> Any:
191
+ return self._d[k]
192
+
193
+ def __iter__(self):
194
+ return iter(self._d)
195
+
196
+ def __len__(self) -> int:
197
+ return len(self._d)
198
+
199
+ def get(self, key: str, default: Any = None) -> Any:
200
+ return self._d.get(key, default)
201
+
202
+ def __repr__(self) -> str: # pragma: no cover
203
+ return f"MappingProxy({self._d!r})"
204
+
205
+
206
+ __all__ = ["Context", "MappingProxy"]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from .utils import HTTPException, status
4
+ from .mappings import (
5
+ HTTP_ERROR_MESSAGES,
6
+ ERROR_MESSAGES,
7
+ _HTTP_TO_RPC,
8
+ _RPC_TO_HTTP,
9
+ )
10
+ from .converters import (
11
+ http_exc_to_rpc,
12
+ rpc_error_to_http,
13
+ _http_exc_to_rpc,
14
+ _rpc_error_to_http,
15
+ create_standardized_error,
16
+ create_standardized_error_from_status,
17
+ to_rpc_error_payload,
18
+ )
19
+ from .exceptions import (
20
+ TigrblError,
21
+ PlanningError,
22
+ LabelError,
23
+ ConfigError,
24
+ SystemStepError,
25
+ ValidationError,
26
+ TransformError,
27
+ DeriveError,
28
+ KernelAbort,
29
+ coerce_runtime_error,
30
+ raise_for_in_errors,
31
+ )
32
+
33
+ __all__ = [
34
+ "HTTPException",
35
+ "status",
36
+ # maps & messages
37
+ "HTTP_ERROR_MESSAGES",
38
+ "ERROR_MESSAGES",
39
+ "_HTTP_TO_RPC",
40
+ "_RPC_TO_HTTP",
41
+ # conversions
42
+ "http_exc_to_rpc",
43
+ "rpc_error_to_http",
44
+ "_http_exc_to_rpc",
45
+ "_rpc_error_to_http",
46
+ "create_standardized_error",
47
+ "create_standardized_error_from_status",
48
+ "to_rpc_error_payload",
49
+ # typed errors + helpers
50
+ "TigrblError",
51
+ "PlanningError",
52
+ "LabelError",
53
+ "ConfigError",
54
+ "SystemStepError",
55
+ "ValidationError",
56
+ "TransformError",
57
+ "DeriveError",
58
+ "KernelAbort",
59
+ "coerce_runtime_error",
60
+ "raise_for_in_errors",
61
+ ]