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,279 @@
1
+ # tigrbl/v3/runtime/system.py
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import inspect
6
+ from typing import Any, Callable, Dict, Mapping, MutableMapping, Optional, Tuple
7
+
8
+ from . import errors as _err
9
+ from .executor.helpers import _in_tx, _is_async_db
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+ # Canonical anchors for system steps (ordering uses these symbolic anchors)
14
+ START_TX = "START_TX"
15
+ HANDLER = "HANDLER"
16
+ END_TX = "END_TX"
17
+
18
+ # Runner signature (matches atoms): (obj|None, ctx) -> None
19
+ SysRunFn = Callable[[Optional[object], Any], None]
20
+
21
+
22
+ # ──────────────────────────────────────────────────────────────────────────────
23
+ # Pluggable runners (adapters install real implementations at app startup)
24
+ # ──────────────────────────────────────────────────────────────────────────────
25
+
26
+
27
+ class _Installed:
28
+ begin: Optional[Callable[[Any], None]] = None # (ctx) -> None
29
+ handler: Optional[Callable[[Optional[object], Any], None]] = (
30
+ None # (obj, ctx) -> None
31
+ )
32
+ commit: Optional[Callable[[Any], None]] = None # (ctx) -> None
33
+ rollback: Optional[Callable[[Any, BaseException | None], None]] = (
34
+ None # (ctx, err) -> None
35
+ )
36
+
37
+
38
+ INSTALLED = _Installed() # singleton container
39
+
40
+
41
+ def install(
42
+ *,
43
+ begin: Optional[Callable[[Any], None]] = None,
44
+ handler: Optional[Callable[[Optional[object], Any], None]] = None,
45
+ commit: Optional[Callable[[Any], None]] = None,
46
+ rollback: Optional[Callable[[Any, BaseException | None], None]] = None,
47
+ ) -> None:
48
+ """
49
+ Adapter entrypoint to install concrete system step functions.
50
+
51
+ Example (SQLAlchemy-ish pseudocode):
52
+ from tigrbl.runtime import system as sys
53
+ sys.install(
54
+ begin=lambda ctx: ctx.session.begin(),
55
+ handler=lambda obj, ctx: ctx.route_handler(obj, ctx),
56
+ commit=lambda ctx: ctx.session.commit(),
57
+ rollback=lambda ctx, err: ctx.session.rollback(),
58
+ )
59
+ """
60
+ if begin is not None:
61
+ INSTALLED.begin = begin
62
+ if handler is not None:
63
+ INSTALLED.handler = handler
64
+ if commit is not None:
65
+ INSTALLED.commit = commit
66
+ if rollback is not None:
67
+ INSTALLED.rollback = rollback
68
+
69
+
70
+ # ──────────────────────────────────────────────────────────────────────────────
71
+ # Default implementations (safe no-ops except handler, which errs if missing)
72
+ # ──────────────────────────────────────────────────────────────────────────────
73
+
74
+
75
+ def _sys_tx_begin(_obj: Optional[object], ctx: Any) -> None:
76
+ """
77
+ sys:txn:begin — open a transaction/savepoint if the adapter installed a runner.
78
+ Defaults to no-op; sets a small flag for diagnostics.
79
+ """
80
+ log.debug("system: begin_tx enter")
81
+ _ensure_temp(ctx)
82
+ has_open = any(
83
+ callable(fn) for fn in (INSTALLED.begin, INSTALLED.commit, INSTALLED.rollback)
84
+ )
85
+ ctx.temp["__sys_tx_open__"] = has_open
86
+ try:
87
+ if callable(INSTALLED.begin):
88
+ INSTALLED.begin(ctx)
89
+ log.debug("system: begin_tx executed.")
90
+ else:
91
+ log.debug("system: begin_tx no-op (no adapter installed).")
92
+ except Exception as e: # escalate as typed error
93
+ ctx.temp["__sys_tx_open__"] = False
94
+ raise _err.SystemStepError("Failed to begin transaction.", cause=e)
95
+ finally:
96
+ log.debug("system: begin_tx exit")
97
+
98
+
99
+ def _sys_handler_crud(obj: Optional[object], ctx: Any) -> None:
100
+ """
101
+ sys:handler:crud — invoke the model/router handler.
102
+ Resolution order:
103
+ 1) Installed adapter runner (INSTALLED.handler)
104
+ 2) ctx.temp['handler'] if callable
105
+ 3) getattr(ctx, 'handler')
106
+ 4) getattr(ctx.model, 'runtime').handler or ctx.model.handler
107
+ On total miss, raises SystemStepError.
108
+ """
109
+ try:
110
+ # 1) Adapter-installed
111
+ if callable(INSTALLED.handler):
112
+ return INSTALLED.handler(obj, ctx)
113
+
114
+ # 2) From ctx.temp (explicitly staged by adapter)
115
+ h = _get_temp(ctx).get("handler")
116
+ if callable(h):
117
+ return h(obj, ctx)
118
+
119
+ # 3) Directly on ctx (adapters may set this)
120
+ h = getattr(ctx, "handler", None)
121
+ if callable(h):
122
+ return h(obj, ctx)
123
+
124
+ # 4) On the model (common pattern: Model.runtime.handler / Model.handler)
125
+ mdl = getattr(ctx, "model", None)
126
+ if mdl is not None:
127
+ r = getattr(getattr(mdl, "runtime", None), "handler", None)
128
+ if callable(r):
129
+ return r(obj, ctx)
130
+ r = getattr(mdl, "handler", None)
131
+ if callable(r):
132
+ return r(obj, ctx)
133
+
134
+ # No handler found
135
+ raise _err.SystemStepError(
136
+ "No handler is installed or discoverable for this operation."
137
+ )
138
+
139
+ except _err.TigrblError:
140
+ # Pass through typed errors intact
141
+ raise
142
+ except Exception as e:
143
+ raise _err.SystemStepError("Handler execution failed.", cause=e)
144
+
145
+
146
+ async def _sys_tx_commit(_obj: Optional[object], ctx: Any) -> None:
147
+ """
148
+ sys:txn:commit — commit the transaction if begin ran and adapter installed commit.
149
+ Defaults to no-op; clears the 'open' flag.
150
+ """
151
+ log.debug("system: commit_tx enter")
152
+ _ensure_temp(ctx)
153
+ db = getattr(ctx, "db", None)
154
+ open_flag = bool(ctx.temp.get("__sys_tx_open__")) or (db is not None and _in_tx(db))
155
+ try:
156
+ if open_flag:
157
+ if callable(INSTALLED.commit):
158
+ rv = INSTALLED.commit(ctx)
159
+ if inspect.isawaitable(rv):
160
+ await rv # type: ignore[func-returns-value]
161
+ log.debug("system: commit_tx executed.")
162
+ else:
163
+ log.debug("system: commit_tx no-op (no adapter commit).")
164
+
165
+ if db is not None and _in_tx(db):
166
+ log.debug("system: commit_tx fallback commit executing.")
167
+ commit = getattr(db, "commit", None)
168
+ if callable(commit):
169
+ try:
170
+ if _is_async_db(db):
171
+ await commit() # type: ignore[misc]
172
+ else:
173
+ commit()
174
+ log.debug("system: commit_tx fallback commit succeeded.")
175
+ except Exception as e: # pragma: no cover - defensive safeguard
176
+ log.exception("system: commit_tx fallback commit failed: %s", e)
177
+ else:
178
+ log.debug(
179
+ "system: commit_tx fallback commit not possible (no commit attr)."
180
+ )
181
+ else:
182
+ log.debug("system: commit_tx no-op (open=%s).", open_flag)
183
+ except Exception as e:
184
+ raise _err.SystemStepError("Failed to commit transaction.", cause=e)
185
+ finally:
186
+ ctx.temp["__sys_tx_open__"] = False
187
+ log.debug("system: commit_tx exit")
188
+
189
+
190
+ def run_rollback(ctx: Any, err: BaseException | None = None) -> None:
191
+ """
192
+ Execute rollback logic when the kernel catches an exception.
193
+ Always safe to call (even if no begin was executed).
194
+ """
195
+ try:
196
+ if callable(INSTALLED.rollback):
197
+ INSTALLED.rollback(ctx, err)
198
+ log.debug("system: rollback executed.")
199
+ else:
200
+ # Best-effort fallback: try common session attribute
201
+ sess = getattr(ctx, "session", None)
202
+ rb = getattr(sess, "rollback", None)
203
+ if callable(rb):
204
+ rb()
205
+ log.debug("system: rollback via ctx.session.rollback().")
206
+ except Exception as e: # Never mask the original error; log only.
207
+ log.exception("system: rollback failed: %s", e)
208
+
209
+
210
+ # ──────────────────────────────────────────────────────────────────────────────
211
+ # Registry & dispatch (mirrors atoms registry shape for consistency)
212
+ # ──────────────────────────────────────────────────────────────────────────────
213
+
214
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, SysRunFn]] = {
215
+ ("txn", "begin"): (START_TX, _sys_tx_begin),
216
+ ("handler", "crud"): (HANDLER, _sys_handler_crud),
217
+ ("txn", "commit"): (END_TX, _sys_tx_commit),
218
+ }
219
+
220
+
221
+ def get(domain: str, subject: str) -> Tuple[str, SysRunFn]:
222
+ """
223
+ Return (anchor, runner) for a given system step, e.g., get('txn','begin').
224
+ """
225
+ key = (domain, subject)
226
+ if key not in REGISTRY:
227
+ raise KeyError(f"Unknown system step: {domain}:{subject}")
228
+ return REGISTRY[key]
229
+
230
+
231
+ def subjects(domain: str | None = None) -> Tuple[Tuple[str, str], ...]:
232
+ """
233
+ Return the available (domain, subject) tuples, optionally filtered by domain.
234
+ """
235
+ items = tuple(sorted(REGISTRY.keys()))
236
+ if domain is None:
237
+ return items
238
+ return tuple(k for k in items if k[0] == domain)
239
+
240
+
241
+ def all_items() -> Tuple[Tuple[Tuple[str, str], Tuple[str, SysRunFn]], ...]:
242
+ """Return the registry items as a sorted tuple (deterministic iteration)."""
243
+ return tuple(sorted(REGISTRY.items(), key=lambda kv: (kv[0][0], kv[0][1])))
244
+
245
+
246
+ # ──────────────────────────────────────────────────────────────────────────────
247
+ # Internals
248
+ # ──────────────────────────────────────────────────────────────────────────────
249
+
250
+
251
+ def _ensure_temp(ctx: Any) -> MutableMapping[str, Any]:
252
+ tmp = getattr(ctx, "temp", None)
253
+ if not isinstance(tmp, dict):
254
+ tmp = {}
255
+ setattr(ctx, "temp", tmp)
256
+ return tmp
257
+
258
+
259
+ def _get_temp(ctx: Any) -> Mapping[str, Any]:
260
+ tmp = getattr(ctx, "temp", None)
261
+ return tmp if isinstance(tmp, Mapping) else {}
262
+
263
+
264
+ __all__ = [
265
+ # anchors
266
+ "START_TX",
267
+ "HANDLER",
268
+ "END_TX",
269
+ # install surface
270
+ "install",
271
+ "INSTALLED",
272
+ # rollback
273
+ "run_rollback",
274
+ # registry facade
275
+ "REGISTRY",
276
+ "get",
277
+ "subjects",
278
+ "all_items",
279
+ ]
@@ -0,0 +1,330 @@
1
+ # tigrbl/v3/runtime/trace.py
2
+ from __future__ import annotations
3
+
4
+ import datetime as _dt
5
+ import random
6
+ import time
7
+ from contextlib import contextmanager
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, Mapping, MutableMapping, Optional, Sequence, Tuple
10
+
11
+ # Public status constants
12
+ OK = "ok"
13
+ ERROR = "error"
14
+ SKIPPED = "skipped"
15
+
16
+ # Soft caps to avoid unbounded growth
17
+ _MAX_STEPS = 5000
18
+ _MAX_EVENTS = 2000
19
+ _MAX_KV_KEYS = 16
20
+ _MAX_SCALAR_LEN = 256
21
+
22
+
23
+ @dataclass
24
+ class _TraceState:
25
+ enabled: bool = True
26
+ sampled: bool = True
27
+ started_at: _dt.datetime = field(
28
+ default_factory=lambda: _dt.datetime.now(_dt.timezone.utc)
29
+ )
30
+ t0: float = field(default_factory=time.perf_counter)
31
+ seq: int = 0
32
+ steps: list[Dict[str, Any]] = field(default_factory=list) # closed steps
33
+ events: list[Dict[str, Any]] = field(default_factory=list) # loose events
34
+ open: Dict[int, Tuple[str, float, Dict[str, Any]]] = field(
35
+ default_factory=dict
36
+ ) # seq -> (label, t_start, base_kv)
37
+ plan_labels: Tuple[
38
+ str, ...
39
+ ] = () # optional: the full ordered plan (for diagnostics)
40
+
41
+
42
+ # ──────────────────────────────────────────────────────────────────────────────
43
+ # Public API
44
+ # ──────────────────────────────────────────────────────────────────────────────
45
+
46
+
47
+ def init(ctx: Any, *, plan_labels: Optional[Sequence[str]] = None) -> None:
48
+ """
49
+ Initialize tracing for this context. Called once by the kernel before executing the plan.
50
+ Respects cfg.trace.enabled and cfg.trace.sample_rate (0..1).
51
+ """
52
+ st = _get_state(ctx, create=True)
53
+
54
+ # Read config (tolerant to missing cfg/attrs)
55
+ cfg = getattr(ctx, "cfg", None)
56
+ enabled = True
57
+ sample_rate = 1.0
58
+ tr = getattr(cfg, "trace", None)
59
+ if tr is not None:
60
+ en = getattr(tr, "enabled", None)
61
+ if isinstance(en, bool):
62
+ enabled = en
63
+ sr = getattr(tr, "sample_rate", None)
64
+ if isinstance(sr, (int, float)):
65
+ sample_rate = max(0.0, min(1.0, float(sr)))
66
+
67
+ st.enabled = bool(enabled)
68
+ st.sampled = (random.random() < sample_rate) if st.enabled else False
69
+
70
+ if plan_labels:
71
+ st.plan_labels = tuple(str(x) for x in plan_labels[:_MAX_STEPS])
72
+
73
+
74
+ def set_enabled(ctx: Any, enabled: bool) -> None:
75
+ """Force tracing on/off for this context."""
76
+ st = _get_state(ctx, create=True)
77
+ st.enabled = bool(enabled)
78
+ if not st.enabled:
79
+ st.sampled = False
80
+
81
+
82
+ def start(ctx: Any, label: str, **kv: Any) -> int | None:
83
+ """
84
+ Start a trace span for a plan label. Returns a step id (seq) you should pass to end().
85
+ If tracing is disabled or not sampled, returns None.
86
+ """
87
+ st = _get_state(ctx)
88
+ if not _active(st):
89
+ return None
90
+ if len(st.steps) + len(st.open) >= _MAX_STEPS:
91
+ return None # soft drop
92
+
93
+ seq = st.seq = st.seq + 1
94
+ base = _base_entry(label)
95
+ base["seq"] = seq
96
+ base.update(_safe_kv(kv))
97
+
98
+ t_start = time.perf_counter()
99
+ st.open[seq] = (label, t_start, base)
100
+ return seq
101
+
102
+
103
+ def end(
104
+ ctx: Any, seq: int | None, status: str = OK, **kv: Any
105
+ ) -> Optional[Dict[str, Any]]:
106
+ """
107
+ End a previously started span by its seq id. Returns the finalized step dict.
108
+ If seq is None or tracing is inactive, this is a no-op.
109
+ """
110
+ st = _get_state(ctx)
111
+ if not _active(st) or seq is None:
112
+ return None
113
+
114
+ opened = st.open.pop(seq, None)
115
+ if opened is None:
116
+ return None # already closed or never started
117
+
118
+ _label, t_start, base = opened
119
+ dur_ms = max(0.0, (time.perf_counter() - t_start) * 1000.0)
120
+
121
+ entry = dict(base)
122
+ entry["status"] = str(status or OK)
123
+ entry["dur_ms"] = round(dur_ms, 3)
124
+ # merge extra kv (sanitized), without clobbering base keys unintentionally
125
+ entry.update(_safe_kv(kv))
126
+
127
+ if len(st.steps) < _MAX_STEPS:
128
+ st.steps.append(entry)
129
+ return entry
130
+
131
+
132
+ def event(ctx: Any, name: str, **kv: Any) -> None:
133
+ """Record an ad-hoc event (not tied to a plan label)."""
134
+ st = _get_state(ctx)
135
+ if not _active(st) or len(st.events) >= _MAX_EVENTS:
136
+ return
137
+ st.events.append(
138
+ {
139
+ "ts": _iso_now(),
140
+ "name": str(name),
141
+ **_safe_kv(kv),
142
+ }
143
+ )
144
+
145
+
146
+ def attach_error(ctx: Any, seq: int | None, exc: BaseException) -> None:
147
+ """
148
+ Attach a compact, non-sensitive error summary to an open/closed step.
149
+ If `seq` is None or cannot be found, emits a free-standing 'error' event instead.
150
+ """
151
+ info = {
152
+ "err_type": exc.__class__.__name__,
153
+ "err_msg": _safe_scalar(getattr(exc, "detail", None)) or _safe_scalar(str(exc)),
154
+ }
155
+
156
+ st = _get_state(ctx)
157
+ if not _active(st):
158
+ return
159
+
160
+ # Try to update an open step first
161
+ if seq is not None and seq in st.open:
162
+ label, t_start, base = st.open[seq]
163
+ base.setdefault("error", info)
164
+ return
165
+
166
+ # Try to update the last closed step with matching seq
167
+ found = None
168
+ if seq is not None:
169
+ for step in reversed(st.steps):
170
+ if step.get("seq") == seq:
171
+ found = step
172
+ break
173
+ if found is not None:
174
+ found.setdefault("error", info)
175
+ return
176
+
177
+ # Fallback: emit an event
178
+ event(ctx, "error", **info)
179
+
180
+
181
+ def snapshot(ctx: Any) -> Dict[str, Any]:
182
+ """
183
+ Return a structured snapshot of the trace suitable for diagnostics endpoints.
184
+ """
185
+ st = _get_state(ctx)
186
+ total_ms = max(0.0, (time.perf_counter() - st.t0) * 1000.0)
187
+ return {
188
+ "enabled": st.enabled,
189
+ "sampled": st.sampled,
190
+ "started_at": st.started_at.isoformat(),
191
+ "duration_ms": round(total_ms, 3),
192
+ "counts": {
193
+ "steps": len(st.steps),
194
+ "open": len(st.open),
195
+ "events": len(st.events),
196
+ },
197
+ "plan": st.plan_labels,
198
+ "steps": tuple(st.steps), # immutable copy for consumers
199
+ "events": tuple(st.events),
200
+ }
201
+
202
+
203
+ def clear(ctx: Any) -> None:
204
+ """Erase all trace data for this context."""
205
+ tmp = _ensure_temp(ctx)
206
+ tmp.pop("__trace__", None)
207
+
208
+
209
+ @contextmanager
210
+ def span(ctx: Any, label: str, **kv: Any):
211
+ """
212
+ Context-manager convenience for start()/end(). On exception, marks status=error
213
+ and attaches a compact error summary; then re-raises.
214
+ """
215
+ seq = start(ctx, label, **kv)
216
+ try:
217
+ yield seq
218
+ end(ctx, seq, status=OK)
219
+ except Exception as e: # pragma: no cover (kernel normally handles)
220
+ attach_error(ctx, seq, e)
221
+ end(ctx, seq, status=ERROR)
222
+ raise
223
+
224
+
225
+ # ──────────────────────────────────────────────────────────────────────────────
226
+ # Internals
227
+ # ──────────────────────────────────────────────────────────────────────────────
228
+
229
+
230
+ def _ensure_temp(ctx: Any) -> MutableMapping[str, Any]:
231
+ tmp = getattr(ctx, "temp", None)
232
+ if not isinstance(tmp, dict):
233
+ tmp = {}
234
+ setattr(ctx, "temp", tmp)
235
+ return tmp
236
+
237
+
238
+ def _get_state(ctx: Any, *, create: bool = False) -> _TraceState:
239
+ tmp = _ensure_temp(ctx)
240
+ st = tmp.get("__trace__")
241
+ if isinstance(st, _TraceState):
242
+ return st
243
+ if create or st is None:
244
+ st = _TraceState()
245
+ tmp["__trace__"] = st
246
+ return st
247
+ # If someone stuffed a dict there, replace with a fresh state.
248
+ st = _TraceState()
249
+ tmp["__trace__"] = st
250
+ return st
251
+
252
+
253
+ def _active(st: _TraceState) -> bool:
254
+ return bool(st.enabled and st.sampled)
255
+
256
+
257
+ def _iso_now() -> str:
258
+ return _dt.datetime.now(_dt.timezone.utc).isoformat()
259
+
260
+
261
+ def _parse_label(label: str) -> Dict[str, Any]:
262
+ """
263
+ Parse step_kind:domain:subject@anchor#field into parts.
264
+ Tolerant: any segment may be missing.
265
+ """
266
+ out = {"label": str(label)}
267
+ left, anchor = (label.split("@", 1) + [""])[:2]
268
+ field = None
269
+ if "#" in anchor:
270
+ anchor, field = anchor.split("#", 1)
271
+ parts = left.split(":")
272
+ if parts:
273
+ out["step_kind"] = parts[0] or None
274
+ if len(parts) > 1:
275
+ out["domain"] = parts[1] or None
276
+ if len(parts) > 2:
277
+ out["subject"] = ":".join(parts[2:]) or None # tolerate extra colons
278
+ out["anchor"] = anchor or None
279
+ out["field"] = field or None
280
+ return out
281
+
282
+
283
+ def _base_entry(label: str) -> Dict[str, Any]:
284
+ entry = _parse_label(label)
285
+ entry["ts"] = _iso_now()
286
+ return entry
287
+
288
+
289
+ def _safe_kv(kv: Mapping[str, Any]) -> Dict[str, Any]:
290
+ """
291
+ Sanitize and cap key/values:
292
+ - keep at most _MAX_KV_KEYS keys
293
+ - value → scalar (bool/int/float/str<=N) or a short type tag
294
+ """
295
+ out: Dict[str, Any] = {}
296
+ for i, (k, v) in enumerate(kv.items()):
297
+ if i >= _MAX_KV_KEYS:
298
+ out["_kv_truncated"] = True
299
+ break
300
+ out[str(k)] = _safe_scalar(v)
301
+ return out
302
+
303
+
304
+ def _safe_scalar(v: Any) -> Any:
305
+ if v is None:
306
+ return None
307
+ if isinstance(v, (bool, int, float)):
308
+ return v
309
+ if isinstance(v, (bytes, bytearray, memoryview)):
310
+ return f"<{type(v).__name__}:{len(v)}B>"
311
+ s = str(v)
312
+ if len(s) > _MAX_SCALAR_LEN:
313
+ s = s[: _MAX_SCALAR_LEN - 1] + "…"
314
+ return s
315
+
316
+
317
+ __all__ = [
318
+ "OK",
319
+ "ERROR",
320
+ "SKIPPED",
321
+ "init",
322
+ "set_enabled",
323
+ "start",
324
+ "end",
325
+ "event",
326
+ "attach_error",
327
+ "snapshot",
328
+ "clear",
329
+ "span",
330
+ ]
@@ -0,0 +1,38 @@
1
+ # tigrbl/v3/schema/__init__.py
2
+ from .builder import _build_schema, _build_list_params
3
+ from .utils import (
4
+ namely_model,
5
+ _make_bulk_rows_model,
6
+ _make_bulk_rows_response_model,
7
+ _make_bulk_ids_model,
8
+ _make_deleted_response_model,
9
+ _make_pk_model,
10
+ )
11
+ from .get_schema import get_schema
12
+ from .decorators import schema_ctx
13
+ from .collect import collect_decorated_schemas
14
+ from ._schema import Schema
15
+ from .schema_spec import SchemaSpec
16
+ from .shortcuts import schema, schema_spec
17
+ from .types import SchemaRef, SchemaArg, SchemaKind
18
+
19
+ __all__ = [
20
+ "_build_schema",
21
+ "_build_list_params",
22
+ "namely_model",
23
+ "_make_bulk_rows_model",
24
+ "_make_bulk_rows_response_model",
25
+ "_make_bulk_ids_model",
26
+ "_make_deleted_response_model",
27
+ "_make_pk_model",
28
+ "get_schema",
29
+ "schema_ctx",
30
+ "collect_decorated_schemas",
31
+ "Schema",
32
+ "SchemaSpec",
33
+ "schema",
34
+ "schema_spec",
35
+ "SchemaRef",
36
+ "SchemaArg",
37
+ "SchemaKind",
38
+ ]
@@ -0,0 +1,27 @@
1
+ # tigrbl/v3/schema/_schema.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Type
6
+
7
+ try: # pragma: no cover
8
+ from pydantic import BaseModel # type: ignore
9
+ except Exception: # pragma: no cover
10
+
11
+ class BaseModel: # minimal stub for typing only
12
+ pass
13
+
14
+
15
+ from .types import SchemaKind
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class Schema:
20
+ """Concrete schema paired with its alias and kind."""
21
+
22
+ model: Type[BaseModel]
23
+ kind: SchemaKind = "out"
24
+ alias: str | None = None
25
+
26
+
27
+ __all__ = ["Schema"]
@@ -0,0 +1,17 @@
1
+ """Schema builder package for Tigrbl v3."""
2
+
3
+ from .cache import _SchemaCache, _SchemaVerb
4
+ from .extras import _merge_request_extras, _merge_response_extras
5
+ from .build_schema import _build_schema
6
+ from .list_params import _build_list_params
7
+ from .strip_parent_fields import _strip_parent_fields
8
+
9
+ __all__ = [
10
+ "_build_schema",
11
+ "_build_list_params",
12
+ "_strip_parent_fields",
13
+ "_merge_request_extras",
14
+ "_merge_response_extras",
15
+ "_SchemaCache",
16
+ "_SchemaVerb",
17
+ ]