tigrbl 0.0.1.dev1__py3-none-any.whl → 0.3.0.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.dev2.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev2.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev2.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.dev2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Mapping, MutableMapping, Optional
4
+ import logging
5
+
6
+ from ... import events as _ev
7
+ from ...opview import opview_from_ctx, ensure_schema_out, _ensure_temp
8
+
9
+ # Runs near the end of the lifecycle, before wire:dump/out:masking.
10
+ ANCHOR = _ev.EMIT_ALIASES_READ # "emit:aliases:readtime"
11
+
12
+ logger = logging.getLogger("uvicorn")
13
+
14
+
15
+ def run(obj: Optional[object], ctx: Any) -> None:
16
+ """Emit safe read-time aliases into response extras."""
17
+ logger.debug("Running emit:readtime_alias")
18
+ temp = _ensure_temp(ctx)
19
+ emit_buf = _ensure_emit_buf(temp)
20
+ extras = _ensure_response_extras(temp)
21
+
22
+ ov = opview_from_ctx(ctx)
23
+ schema_out = ensure_schema_out(ctx, ov)
24
+ for field, desc in schema_out["by_field"].items():
25
+ out_alias = desc.get("alias_out")
26
+ if not out_alias:
27
+ continue
28
+ if out_alias in extras:
29
+ logger.debug("Alias %s already present in extras; skipping", out_alias)
30
+ continue
31
+
32
+ value = _read_current_value(obj, ctx, field)
33
+ if value is None:
34
+ logger.debug("No current value available for field %s", field)
35
+ continue
36
+
37
+ safe_val = _safe_readtime_value(value, desc)
38
+ extras[out_alias] = safe_val
39
+ logger.debug("Emitted read-time alias '%s' for field '%s'", out_alias, field)
40
+
41
+ emit_buf["read"].append(
42
+ {
43
+ "field": field,
44
+ "alias": out_alias,
45
+ "emitted": True,
46
+ "meta": _alias_meta(desc),
47
+ }
48
+ )
49
+
50
+
51
+ # ──────────────────────────────────────────────────────────────────────────────
52
+ # Internals
53
+ # ──────────────────────────────────────────────────────────────────────────────
54
+
55
+
56
+ def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
57
+ buf = temp.get("emit_aliases")
58
+ if not isinstance(buf, dict):
59
+ buf = {"pre": [], "post": [], "read": []}
60
+ temp["emit_aliases"] = buf
61
+ else:
62
+ buf.setdefault("pre", [])
63
+ buf.setdefault("post", [])
64
+ buf.setdefault("read", [])
65
+ return buf # type: ignore[return-value]
66
+
67
+
68
+ def _ensure_response_extras(temp: MutableMapping[str, Any]) -> Dict[str, Any]:
69
+ extras = temp.get("response_extras")
70
+ if not isinstance(extras, dict):
71
+ extras = {}
72
+ temp["response_extras"] = extras
73
+ return extras # type: ignore[return-value]
74
+
75
+
76
+ def _alias_meta(desc: Mapping[str, Any]) -> Dict[str, Any]:
77
+ meta: Dict[str, Any] = {}
78
+ for attr in ("sensitive", "mask_last"):
79
+ if attr in desc:
80
+ meta[attr] = desc[attr]
81
+ return meta
82
+
83
+
84
+ def _read_current_value(obj: Optional[object], ctx: Any, field: str) -> Optional[Any]:
85
+ if obj is not None and hasattr(obj, field):
86
+ try:
87
+ return getattr(obj, field)
88
+ except Exception:
89
+ pass
90
+ for name in ("row", "values", "current_values"):
91
+ src = getattr(ctx, name, None)
92
+ if isinstance(src, Mapping) and field in src:
93
+ return src.get(field)
94
+ hv = getattr(getattr(ctx, "temp", {}), "get", lambda *a, **k: None)(
95
+ "hydrated_values"
96
+ ) # type: ignore
97
+ if isinstance(hv, Mapping):
98
+ return hv.get(field)
99
+ return None
100
+
101
+
102
+ def _mask_value(value: Any, keep_last: Optional[int]) -> str:
103
+ if isinstance(value, (bytes, bytearray, memoryview)):
104
+ return "••••"
105
+ s = str(value) if value is not None else ""
106
+ if not s:
107
+ return ""
108
+ n = keep_last if (isinstance(keep_last, int) and keep_last >= 0) else 4
109
+ n = min(n, len(s))
110
+ return "•" * (len(s) - n) + s[-n:]
111
+
112
+
113
+ def _safe_readtime_value(value: Any, desc: Mapping[str, Any]) -> Any:
114
+ if desc.get("sensitive"):
115
+ keep_last = desc.get("mask_last")
116
+ return _mask_value(value, keep_last)
117
+ return value
118
+
119
+
120
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,38 @@
1
+ # tigrbl/v3/runtime/atoms/out/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+ import logging
6
+
7
+ # Atom implementations (model-scoped)
8
+ from . import masking as _masking
9
+
10
+ # Runner signature: (obj|None, ctx) -> None
11
+ RunFn = Callable[[Optional[object], Any], None]
12
+
13
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
14
+ #: Keys are (domain, subject); values are (anchor, runner).
15
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
16
+ ("out", "masking"): (_masking.ANCHOR, _masking.run),
17
+ }
18
+
19
+ logger = logging.getLogger("uvicorn")
20
+
21
+
22
+ def subjects() -> Tuple[str, ...]:
23
+ """Return the subject names exported by this domain."""
24
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
25
+ logger.debug("Listing 'out' subjects: %s", subjects)
26
+ return subjects
27
+
28
+
29
+ def get(subject: str) -> Tuple[str, RunFn]:
30
+ """Return (anchor, runner) for a subject in the 'out' domain."""
31
+ key = ("out", subject)
32
+ if key not in REGISTRY:
33
+ raise KeyError(f"Unknown out atom subject: {subject!r}")
34
+ logger.debug("Retrieving 'out' subject %s", subject)
35
+ return REGISTRY[key]
36
+
37
+
38
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]
@@ -0,0 +1,135 @@
1
+ # tigrbl/v3/runtime/atoms/out/masking.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Dict, Mapping, MutableMapping, Optional, Sequence
5
+ import logging
6
+
7
+ from ... import events as _ev
8
+ from ...opview import opview_from_ctx, ensure_schema_out, _ensure_temp
9
+
10
+ # Runs at the very end of the lifecycle (after wire:dump).
11
+ ANCHOR = _ev.OUT_DUMP # "out:dump"
12
+
13
+ logger = logging.getLogger("uvicorn")
14
+
15
+
16
+ def run(obj: Optional[object], ctx: Any) -> None:
17
+ """
18
+ out:masking@out:dump
19
+
20
+ Purpose
21
+ -------
22
+ Mask sensitive top-level fields in the already-built response payload.
23
+ This runs AFTER wire:dump so the payload exists and after emit:readtime_alias
24
+ so alias extras are already present. It does NOT redact explicitly emitted
25
+ alias extras (e.g., secret-once raw tokens) — those are intentional.
26
+
27
+ Inputs / Conventions
28
+ --------------------
29
+ - ctx.temp["response_payload"] : dict or list[dict] (produced by wire:dump)
30
+ - ctx.temp["emit_aliases"]["post"] / ["read"] : lists of descriptors that
31
+ include {"alias": "..."}; these alias keys are skipped (not masked).
32
+
33
+ Effects
34
+ -------
35
+ - For each payload item (dict), if a key equals a ColumnSpec field name and
36
+ that column is marked sensitive (via `sensitive`/`redact`/`redact_last`),
37
+ replace the value with a masked hint.
38
+ - Leaves alias extras untouched (based on the alias sets captured from
39
+ emit_aliases.post/read).
40
+ """
41
+ logger.debug("Running out:masking")
42
+ ov = opview_from_ctx(ctx)
43
+ schema_out = ensure_schema_out(ctx, ov)
44
+
45
+ temp = _ensure_temp(ctx)
46
+ payload = temp.get("response_payload")
47
+ if payload is None:
48
+ logger.debug("No response payload found; skipping masking")
49
+ return
50
+ logger.debug("Original payload before masking: %s", payload)
51
+
52
+ emit_buf = _ensure_emit_buf(temp)
53
+ skip_aliases = _collect_emitted_aliases(emit_buf)
54
+
55
+ if isinstance(payload, dict):
56
+ logger.debug("Masking single-object payload")
57
+ _mask_one(payload, schema_out["by_field"], skip_aliases)
58
+ elif isinstance(payload, (list, tuple)):
59
+ logger.debug("Masking list payload with %d items", len(payload))
60
+ for item in payload:
61
+ if isinstance(item, dict):
62
+ _mask_one(item, schema_out["by_field"], skip_aliases)
63
+ else:
64
+ logger.debug("Skipping non-dict item in payload: %s", item)
65
+ else:
66
+ logger.debug(
67
+ "Unsupported payload type %s; leaving as-is", type(payload).__name__
68
+ )
69
+
70
+ logger.debug("Payload after masking: %s", payload)
71
+
72
+
73
+ # ──────────────────────────────────────────────────────────────────────────────
74
+ # Internals
75
+ # ──────────────────────────────────────────────────────────────────────────────
76
+
77
+
78
+ def _ensure_emit_buf(temp: MutableMapping[str, Any]) -> Dict[str, list]:
79
+ buf = temp.get("emit_aliases")
80
+ if not isinstance(buf, dict):
81
+ buf = {"pre": [], "post": [], "read": []}
82
+ temp["emit_aliases"] = buf
83
+ else:
84
+ buf.setdefault("pre", [])
85
+ buf.setdefault("post", [])
86
+ buf.setdefault("read", [])
87
+ return buf # type: ignore[return-value]
88
+
89
+
90
+ def _collect_emitted_aliases(
91
+ emit_buf: Mapping[str, Sequence[Mapping[str, Any]]],
92
+ ) -> set[str]:
93
+ aliases: set[str] = set()
94
+ for bucket in ("post", "read"):
95
+ for d in emit_buf.get(bucket, ()) or ():
96
+ a = d.get("alias")
97
+ if isinstance(a, str) and a:
98
+ aliases.add(a)
99
+ return aliases
100
+
101
+
102
+ def _mask_one(
103
+ item: Dict[str, Any],
104
+ by_field: Mapping[str, Mapping[str, Any]],
105
+ skip_aliases: set[str],
106
+ ) -> None:
107
+ for field, desc in by_field.items():
108
+ if field not in item or field in skip_aliases:
109
+ continue
110
+ val = item.get(field, None)
111
+ if val is None:
112
+ continue
113
+ if not (desc.get("sensitive") or desc.get("mask_last") is not None):
114
+ continue
115
+ masked = _mask_value(val, desc.get("mask_last"))
116
+ logger.debug("Masking field '%s': %r -> %r", field, val, masked)
117
+ item[field] = masked
118
+ logger.debug("Item after masking: %s", item)
119
+
120
+
121
+ def _mask_value(value: Any, keep_last: Optional[int]) -> str:
122
+ """
123
+ Generic masking for strings/bytes; falls back to a fixed token when unknown.
124
+ """
125
+ if isinstance(value, (bytes, bytearray, memoryview)):
126
+ return "••••"
127
+ s = str(value) if value is not None else ""
128
+ if not s:
129
+ return ""
130
+ n = keep_last if (isinstance(keep_last, int) and keep_last >= 0) else 4
131
+ n = min(n, len(s))
132
+ return "•" * (len(s) - n) + s[-n:]
133
+
134
+
135
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,38 @@
1
+ # tigrbl/v3/runtime/atoms/refresh/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+ import logging
6
+
7
+ # Atom implementations (model-scoped)
8
+ from . import demand as _demand
9
+
10
+ # Runner signature: (obj|None, ctx) -> None
11
+ RunFn = Callable[[Optional[object], Any], None]
12
+
13
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
14
+ #: Keys are (domain, subject); values are (anchor, runner).
15
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
16
+ ("refresh", "demand"): (_demand.ANCHOR, _demand.run),
17
+ }
18
+
19
+ logger = logging.getLogger("uvicorn")
20
+
21
+
22
+ def subjects() -> Tuple[str, ...]:
23
+ """Return the subject names exported by this domain."""
24
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
25
+ logger.debug("Listing 'refresh' subjects: %s", subjects)
26
+ return subjects
27
+
28
+
29
+ def get(subject: str) -> Tuple[str, RunFn]:
30
+ """Return (anchor, runner) for a subject in the 'refresh' domain."""
31
+ key = ("refresh", subject)
32
+ if key not in REGISTRY:
33
+ raise KeyError(f"Unknown refresh atom subject: {subject!r}")
34
+ logger.debug("Retrieving 'refresh' subject %s", subject)
35
+ return REGISTRY[key]
36
+
37
+
38
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]
@@ -0,0 +1,130 @@
1
+ # tigrbl/v3/runtime/atoms/refresh/demand.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Iterable, Optional
5
+ import logging
6
+
7
+ from ... import events as _ev
8
+ from ...opview import opview_from_ctx, _ensure_temp
9
+
10
+ # After the handler flushes changes; decide whether to hydrate DB-generated values.
11
+ ANCHOR = _ev.POST_FLUSH # "post:flush"
12
+
13
+ logger = logging.getLogger("uvicorn")
14
+
15
+
16
+ def run(obj: Optional[object], ctx: Any) -> None:
17
+ """
18
+ refresh:demand@post:flush
19
+
20
+ Purpose
21
+ -------
22
+ Decide whether to hydrate refreshed values from the DB after a write (INSERT/UPDATE).
23
+ We do NOT perform the refresh here; we only mark intent on the context so the
24
+ executor (or handler) can perform the vendor-specific action (RETURNING/refresh()).
25
+
26
+ Inputs (conventions)
27
+ --------------------
28
+ - ctx.persist : bool → write op? (anchor is persist-tied but we guard)
29
+ - ctx.temp["used_returning"] : bool → a prior step already satisfied hydration
30
+ - ctx.temp["hydrated_values"] : Mapping[str, Any]→ values captured from RETURNING
31
+ - ctx.cfg.refresh_after_write : Optional[bool] → policy override (true/false)
32
+ (also checks ctx.cfg.refresh_policy.{always,never,auto})
33
+ - ctx.bulk : Optional[bool] → bulk write; executor may choose a different strategy
34
+
35
+ Effects
36
+ -------
37
+ - ctx.temp["refresh_demand"] : bool
38
+ - ctx.temp["refresh_fields"] : tuple[str, ...] (hint: which fields likely changed in DB)
39
+ - ctx.temp["refresh_reason"] : str (diagnostic only)
40
+ """
41
+ logger.debug("Running refresh:demand")
42
+ if getattr(ctx, "persist", True) is False:
43
+ logger.debug("Skipping refresh:demand; ctx.persist is False")
44
+ return
45
+
46
+ temp = _ensure_temp(ctx)
47
+ ov = opview_from_ctx(ctx)
48
+ refresh_hints = tuple(ov.refresh_hints)
49
+
50
+ # If RETURNING already produced hydrated values, skip unless policy forces refresh.
51
+ returning_satisfied = bool(temp.get("used_returning")) or bool(
52
+ temp.get("hydrated_values")
53
+ )
54
+ logger.debug("Returning satisfied: %s", returning_satisfied)
55
+
56
+ # Policy: cfg.refresh_after_write wins if explicitly set; otherwise "auto".
57
+ policy = _get_refresh_policy(ctx)
58
+ logger.debug("Refresh policy: %s", policy)
59
+ # auto → infer from specs (db-generated signals) OR absence of returning values
60
+ needs_by_specs = bool(refresh_hints)
61
+ logger.debug("Refresh hints: %s; fields=%s", needs_by_specs, refresh_hints)
62
+ need_refresh = _decide(policy, returning_satisfied, needs_by_specs)
63
+ logger.debug("Refresh decision: %s", need_refresh)
64
+
65
+ temp["refresh_demand"] = bool(need_refresh)
66
+ temp["refresh_fields"] = refresh_hints
67
+
68
+ if need_refresh:
69
+ temp["refresh_reason"] = _reason(
70
+ policy, returning_satisfied, needs_by_specs, refresh_hints
71
+ )
72
+ logger.debug("Refresh scheduled: %s", temp["refresh_reason"])
73
+ else:
74
+ temp["refresh_reason"] = "skipped: returning_satisfied or policy=false"
75
+ logger.debug("Refresh skipped: %s", temp["refresh_reason"])
76
+
77
+ # Executor/handler will look at ctx.temp["refresh_demand"] and act accordingly.
78
+
79
+
80
+ # ──────────────────────────────────────────────────────────────────────────────
81
+ # Internals
82
+ # ──────────────────────────────────────────────────────────────────────────────
83
+
84
+
85
+ def _get_refresh_policy(ctx: Any) -> str:
86
+ """
87
+ Return 'always' | 'never' | 'auto'.
88
+ Accepts any of the following on ctx.cfg (first hit wins):
89
+ - cfg.refresh_after_write: bool → True:'always' / False:'never'
90
+ - cfg.refresh_policy: str in {'always','never','auto'}
91
+ Defaults to 'auto'.
92
+ """
93
+ cfg = getattr(ctx, "cfg", None)
94
+ if cfg is not None:
95
+ val = getattr(cfg, "refresh_after_write", None)
96
+ if isinstance(val, bool):
97
+ return "always" if val else "never"
98
+ pol = getattr(getattr(cfg, "refresh_policy", None), "value", None) or getattr(
99
+ cfg, "refresh_policy", None
100
+ )
101
+ if isinstance(pol, str) and pol in {"always", "never", "auto"}:
102
+ return pol
103
+ return "auto"
104
+
105
+
106
+ def _decide(policy: str, returning_satisfied: bool, needs_by_specs: bool) -> bool:
107
+ if policy == "always":
108
+ return True
109
+ if policy == "never":
110
+ return False
111
+ # auto
112
+ if returning_satisfied:
113
+ # RETURNING already hydrated values; only refresh if specs strongly indicate more work.
114
+ return bool(needs_by_specs)
115
+ # No returning: default to refresh to honor "hydrate after flush" decision.
116
+ return True
117
+
118
+
119
+ def _reason(
120
+ policy: str, returning_satisfied: bool, needs_by_specs: bool, fields: Iterable[str]
121
+ ) -> str:
122
+ parts = [f"policy={policy}"]
123
+ parts.append(f"returning_satisfied={bool(returning_satisfied)}")
124
+ parts.append(f"specs_need={bool(needs_by_specs)}")
125
+ if fields:
126
+ parts.append(f"fields={','.join(fields)}")
127
+ return "; ".join(parts)
128
+
129
+
130
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,40 @@
1
+ # tigrbl/v3/runtime/atoms/resolve/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+ import logging
6
+
7
+ # Atom implementations (model-scoped)
8
+ from . import assemble as _assemble
9
+ from . import paired_gen as _paired_gen
10
+
11
+ # Runner signature: (obj|None, ctx) -> None
12
+ RunFn = Callable[[Optional[object], Any], None]
13
+
14
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
15
+ #: Keys are (domain, subject); values are (anchor, runner).
16
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
17
+ ("resolve", "assemble"): (_assemble.ANCHOR, _assemble.run),
18
+ ("resolve", "paired_gen"): (_paired_gen.ANCHOR, _paired_gen.run),
19
+ }
20
+
21
+ logger = logging.getLogger("uvicorn")
22
+
23
+
24
+ def subjects() -> Tuple[str, ...]:
25
+ """Return the subject names exported by this domain."""
26
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
27
+ logger.debug("Listing 'resolve' subjects: %s", subjects)
28
+ return subjects
29
+
30
+
31
+ def get(subject: str) -> Tuple[str, RunFn]:
32
+ """Return (anchor, runner) for a subject in the 'resolve' domain."""
33
+ key = ("resolve", subject)
34
+ if key not in REGISTRY:
35
+ raise KeyError(f"Unknown resolve atom subject: {subject!r}")
36
+ logger.debug("Retrieving 'resolve' subject %s", subject)
37
+ return REGISTRY[key]
38
+
39
+
40
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]
@@ -0,0 +1,167 @@
1
+ # tigrbl/v3/runtime/atoms/resolve/assemble.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Optional, Dict, Tuple
5
+ import logging
6
+
7
+ from ... import events as _ev
8
+ from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
9
+
10
+ # Runs in HANDLER phase, before pre:flush and any storage transforms.
11
+ ANCHOR = _ev.RESOLVE_VALUES # "resolve:values"
12
+
13
+ logger = logging.getLogger("uvicorn")
14
+
15
+
16
+ def run(obj: Optional[object], ctx: Any) -> None:
17
+ """
18
+ resolve:assemble@resolve:values
19
+
20
+ Purpose
21
+ -------
22
+ Build a normalized dict of inbound values to apply on the model (assembled_values).
23
+ - Prefer client-provided, validated values (from wire:build_in/validate).
24
+ - For fields that are **ABSENT** (not present in inbound), apply ColumnSpec.default_factory(ctx)
25
+ for simple server-side defaults (non-paired path).
26
+ - Virtual columns (storage=None) are never persisted; if present in inbound, stash them
27
+ under temp["virtual_in"] for downstream wire/out logic.
28
+
29
+ Inputs (conventions)
30
+ --------------------
31
+ - ctx.temp["in_values"] OR ctx.in_data / ctx.payload / ctx.data / ctx.body :
32
+ dict-like or Pydantic model; used as inbound source
33
+ - ctx.persist : bool (writes only; non-persist ops typically prune this anchor)
34
+ - ctx.op : str (verb name; optional, only for diagnostics)
35
+
36
+ Effects
37
+ -------
38
+ - ctx.temp["assembled_values"] : dict[field -> value] (only persisted fields)
39
+ - ctx.temp["virtual_in"] : dict[field -> value] (for storage=None)
40
+ - ctx.temp["absent_fields"] : tuple[str, ...] (those not in inbound)
41
+ - ctx.temp["used_default_factory"] : tuple[str, ...] (fields defaulted here)
42
+ """
43
+ # Non-persisting ops should have pruned this anchor; retain guard for safety.
44
+ if getattr(ctx, "persist", True) is False:
45
+ logger.debug("Skipping resolve:assemble; ctx.persist is False")
46
+ return
47
+
48
+ logger.debug("Running resolve:assemble")
49
+ ov = opview_from_ctx(ctx)
50
+ schema_in = ensure_schema_in(ctx, ov)
51
+ inbound = _coerce_inbound(getattr(ctx, "temp", {}).get("in_values", None), ctx)
52
+
53
+ temp = _ensure_temp(ctx)
54
+ assembled: Dict[str, Any] = {}
55
+ virtual_in: Dict[str, Any] = {}
56
+ absent: list[str] = []
57
+ used_default: list[str] = []
58
+
59
+ # Iterate fields in a stable order
60
+ for field in sorted(schema_in["fields"]):
61
+ meta = schema_in["by_field"].get(field, {})
62
+ in_enabled = meta.get("in_enabled", True)
63
+ is_virtual = meta.get("virtual", False)
64
+
65
+ present, value = _try_read_inbound(inbound, field)
66
+ if present:
67
+ if is_virtual:
68
+ virtual_in[field] = value
69
+ logger.debug("Captured virtual inbound %s=%s", field, value)
70
+ elif in_enabled:
71
+ assembled[field] = value
72
+ logger.debug("Assembled inbound %s=%s", field, value)
73
+ continue
74
+
75
+ absent.append(field)
76
+ logger.debug("Field %s absent from inbound", field)
77
+
78
+ default_fn = meta.get("default_factory")
79
+ if callable(default_fn) and in_enabled and not is_virtual:
80
+ try:
81
+ default_val = default_fn(_ctx_view(ctx))
82
+ assembled[field] = default_val
83
+ used_default.append(field)
84
+ logger.debug("Applied default for field %s", field)
85
+ except Exception:
86
+ logger.debug("Default factory failed for field %s", field)
87
+
88
+ # Stash results on ctx.temp
89
+ temp["assembled_values"] = assembled
90
+ temp["virtual_in"] = virtual_in
91
+ temp["absent_fields"] = tuple(absent)
92
+ temp["used_default_factory"] = tuple(used_default)
93
+ logger.debug(
94
+ "Assembled values: %s, virtual_in: %s, absent: %s, defaults: %s",
95
+ assembled,
96
+ virtual_in,
97
+ absent,
98
+ used_default,
99
+ )
100
+
101
+
102
+ # ──────────────────────────────────────────────────────────────────────────────
103
+ # Internals
104
+ # ──────────────────────────────────────────────────────────────────────────────
105
+
106
+
107
+ def _coerce_inbound(candidate: Any, ctx: Any) -> Mapping[str, Any]:
108
+ """
109
+ Return a dict-like for inbound values.
110
+ Priority: ctx.temp["in_values"] → ctx.in_data → ctx.payload → ctx.data → ctx.body.
111
+ Accepts Pydantic v1/v2 models (model_dump()/dict()).
112
+ """
113
+ for name in ("in_values",):
114
+ if isinstance(candidate, Mapping):
115
+ return candidate
116
+ # fallbacks on ctx
117
+ for attr in ("in_data", "payload", "data", "body"):
118
+ val = getattr(ctx, attr, None)
119
+ if isinstance(val, Mapping):
120
+ return val
121
+ # Pydantic v2
122
+ if hasattr(val, "model_dump") and callable(getattr(val, "model_dump")):
123
+ try:
124
+ return dict(val.model_dump())
125
+ except Exception:
126
+ pass
127
+ # Pydantic v1
128
+ if hasattr(val, "dict") and callable(getattr(val, "dict")):
129
+ try:
130
+ return dict(val.dict())
131
+ except Exception:
132
+ pass
133
+ # default empty
134
+ return {}
135
+
136
+
137
+ def _try_read_inbound(inbound: Mapping[str, Any], field: str) -> Tuple[bool, Any]:
138
+ """
139
+ Distinguish ABSENT vs present(None).
140
+ """
141
+ if field in inbound:
142
+ return True, inbound.get(field, None)
143
+ # Be tolerant to alias-style inputs (if present)
144
+ for alt in (field.lower(), field.upper()):
145
+ if alt in inbound:
146
+ return True, inbound.get(alt)
147
+ return False, None
148
+
149
+
150
+ def _ctx_view(ctx: Any) -> Dict[str, Any]:
151
+ """
152
+ Provide a small read-only view for default_factory functions
153
+ without exposing the entire executor context.
154
+ """
155
+ view = {
156
+ "op": getattr(ctx, "op", None),
157
+ "persist": getattr(ctx, "persist", True),
158
+ "temp": getattr(ctx, "temp", None),
159
+ # optional hints the executor might set
160
+ "tenant": getattr(ctx, "tenant", None),
161
+ "user": getattr(ctx, "user", None),
162
+ "now": getattr(ctx, "now", None),
163
+ }
164
+ return view
165
+
166
+
167
+ __all__ = ["ANCHOR", "run"]