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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +97 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +291 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +86 -0
  11. tigrbl/app/_model_registry.py +41 -0
  12. tigrbl/app/app_spec.py +42 -0
  13. tigrbl/app/mro_collect.py +67 -0
  14. tigrbl/app/shortcuts.py +65 -0
  15. tigrbl/app/tigrbl_app.py +319 -0
  16. tigrbl/bindings/__init__.py +73 -0
  17. tigrbl/bindings/api/__init__.py +12 -0
  18. tigrbl/bindings/api/common.py +109 -0
  19. tigrbl/bindings/api/include.py +256 -0
  20. tigrbl/bindings/api/resource_proxy.py +149 -0
  21. tigrbl/bindings/api/rpc.py +111 -0
  22. tigrbl/bindings/columns.py +49 -0
  23. tigrbl/bindings/handlers/__init__.py +11 -0
  24. tigrbl/bindings/handlers/builder.py +119 -0
  25. tigrbl/bindings/handlers/ctx.py +74 -0
  26. tigrbl/bindings/handlers/identifiers.py +228 -0
  27. tigrbl/bindings/handlers/namespaces.py +51 -0
  28. tigrbl/bindings/handlers/steps.py +276 -0
  29. tigrbl/bindings/hooks.py +311 -0
  30. tigrbl/bindings/model.py +194 -0
  31. tigrbl/bindings/model_helpers.py +139 -0
  32. tigrbl/bindings/model_registry.py +77 -0
  33. tigrbl/bindings/rest/__init__.py +7 -0
  34. tigrbl/bindings/rest/attach.py +34 -0
  35. tigrbl/bindings/rest/collection.py +286 -0
  36. tigrbl/bindings/rest/common.py +120 -0
  37. tigrbl/bindings/rest/fastapi.py +76 -0
  38. tigrbl/bindings/rest/helpers.py +119 -0
  39. tigrbl/bindings/rest/io.py +317 -0
  40. tigrbl/bindings/rest/io_headers.py +49 -0
  41. tigrbl/bindings/rest/member.py +386 -0
  42. tigrbl/bindings/rest/router.py +296 -0
  43. tigrbl/bindings/rest/routing.py +153 -0
  44. tigrbl/bindings/rpc.py +364 -0
  45. tigrbl/bindings/schemas/__init__.py +11 -0
  46. tigrbl/bindings/schemas/builder.py +348 -0
  47. tigrbl/bindings/schemas/defaults.py +260 -0
  48. tigrbl/bindings/schemas/utils.py +193 -0
  49. tigrbl/column/README.md +62 -0
  50. tigrbl/column/__init__.py +72 -0
  51. tigrbl/column/_column.py +96 -0
  52. tigrbl/column/column_spec.py +40 -0
  53. tigrbl/column/field_spec.py +31 -0
  54. tigrbl/column/infer/__init__.py +25 -0
  55. tigrbl/column/infer/core.py +92 -0
  56. tigrbl/column/infer/jsonhints.py +44 -0
  57. tigrbl/column/infer/planning.py +133 -0
  58. tigrbl/column/infer/types.py +102 -0
  59. tigrbl/column/infer/utils.py +59 -0
  60. tigrbl/column/io_spec.py +136 -0
  61. tigrbl/column/mro_collect.py +59 -0
  62. tigrbl/column/shortcuts.py +89 -0
  63. tigrbl/column/storage_spec.py +65 -0
  64. tigrbl/config/__init__.py +19 -0
  65. tigrbl/config/constants.py +224 -0
  66. tigrbl/config/defaults.py +29 -0
  67. tigrbl/config/resolver.py +295 -0
  68. tigrbl/core/__init__.py +47 -0
  69. tigrbl/core/crud/__init__.py +36 -0
  70. tigrbl/core/crud/bulk.py +168 -0
  71. tigrbl/core/crud/helpers/__init__.py +76 -0
  72. tigrbl/core/crud/helpers/db.py +92 -0
  73. tigrbl/core/crud/helpers/enum.py +86 -0
  74. tigrbl/core/crud/helpers/filters.py +162 -0
  75. tigrbl/core/crud/helpers/model.py +123 -0
  76. tigrbl/core/crud/helpers/normalize.py +99 -0
  77. tigrbl/core/crud/ops.py +235 -0
  78. tigrbl/ddl/__init__.py +344 -0
  79. tigrbl/decorators.py +17 -0
  80. tigrbl/deps/__init__.py +20 -0
  81. tigrbl/deps/fastapi.py +45 -0
  82. tigrbl/deps/favicon.svg +4 -0
  83. tigrbl/deps/jinja.py +27 -0
  84. tigrbl/deps/pydantic.py +10 -0
  85. tigrbl/deps/sqlalchemy.py +94 -0
  86. tigrbl/deps/starlette.py +36 -0
  87. tigrbl/engine/__init__.py +45 -0
  88. tigrbl/engine/_engine.py +144 -0
  89. tigrbl/engine/bind.py +33 -0
  90. tigrbl/engine/builders.py +236 -0
  91. tigrbl/engine/capabilities.py +29 -0
  92. tigrbl/engine/collect.py +111 -0
  93. tigrbl/engine/decorators.py +110 -0
  94. tigrbl/engine/docs/PLUGINS.md +49 -0
  95. tigrbl/engine/engine_spec.py +355 -0
  96. tigrbl/engine/plugins.py +52 -0
  97. tigrbl/engine/registry.py +36 -0
  98. tigrbl/engine/resolver.py +224 -0
  99. tigrbl/engine/shortcuts.py +216 -0
  100. tigrbl/hook/__init__.py +21 -0
  101. tigrbl/hook/_hook.py +22 -0
  102. tigrbl/hook/decorators.py +28 -0
  103. tigrbl/hook/hook_spec.py +24 -0
  104. tigrbl/hook/mro_collect.py +98 -0
  105. tigrbl/hook/shortcuts.py +44 -0
  106. tigrbl/hook/types.py +76 -0
  107. tigrbl/op/__init__.py +50 -0
  108. tigrbl/op/_op.py +31 -0
  109. tigrbl/op/canonical.py +31 -0
  110. tigrbl/op/collect.py +11 -0
  111. tigrbl/op/decorators.py +238 -0
  112. tigrbl/op/model_registry.py +301 -0
  113. tigrbl/op/mro_collect.py +99 -0
  114. tigrbl/op/resolver.py +216 -0
  115. tigrbl/op/types.py +136 -0
  116. tigrbl/orm/__init__.py +1 -0
  117. tigrbl/orm/mixins/_RowBound.py +83 -0
  118. tigrbl/orm/mixins/__init__.py +95 -0
  119. tigrbl/orm/mixins/bootstrappable.py +113 -0
  120. tigrbl/orm/mixins/bound.py +47 -0
  121. tigrbl/orm/mixins/edges.py +40 -0
  122. tigrbl/orm/mixins/fields.py +165 -0
  123. tigrbl/orm/mixins/hierarchy.py +54 -0
  124. tigrbl/orm/mixins/key_digest.py +44 -0
  125. tigrbl/orm/mixins/lifecycle.py +115 -0
  126. tigrbl/orm/mixins/locks.py +51 -0
  127. tigrbl/orm/mixins/markers.py +16 -0
  128. tigrbl/orm/mixins/operations.py +57 -0
  129. tigrbl/orm/mixins/ownable.py +337 -0
  130. tigrbl/orm/mixins/principals.py +98 -0
  131. tigrbl/orm/mixins/tenant_bound.py +301 -0
  132. tigrbl/orm/mixins/upsertable.py +118 -0
  133. tigrbl/orm/mixins/utils.py +49 -0
  134. tigrbl/orm/tables/__init__.py +72 -0
  135. tigrbl/orm/tables/_base.py +8 -0
  136. tigrbl/orm/tables/audit.py +56 -0
  137. tigrbl/orm/tables/client.py +25 -0
  138. tigrbl/orm/tables/group.py +29 -0
  139. tigrbl/orm/tables/org.py +30 -0
  140. tigrbl/orm/tables/rbac.py +76 -0
  141. tigrbl/orm/tables/status.py +106 -0
  142. tigrbl/orm/tables/tenant.py +22 -0
  143. tigrbl/orm/tables/user.py +39 -0
  144. tigrbl/response/README.md +34 -0
  145. tigrbl/response/__init__.py +33 -0
  146. tigrbl/response/bind.py +12 -0
  147. tigrbl/response/decorators.py +37 -0
  148. tigrbl/response/resolver.py +83 -0
  149. tigrbl/response/shortcuts.py +171 -0
  150. tigrbl/response/types.py +49 -0
  151. tigrbl/rest/__init__.py +27 -0
  152. tigrbl/runtime/README.md +129 -0
  153. tigrbl/runtime/__init__.py +20 -0
  154. tigrbl/runtime/atoms/__init__.py +102 -0
  155. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  156. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  157. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  158. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  159. tigrbl/runtime/atoms/out/__init__.py +38 -0
  160. tigrbl/runtime/atoms/out/masking.py +135 -0
  161. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  162. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  163. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  164. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  165. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  166. tigrbl/runtime/atoms/response/__init__.py +19 -0
  167. tigrbl/runtime/atoms/response/headers_from_payload.py +57 -0
  168. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  169. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  170. tigrbl/runtime/atoms/response/render.py +36 -0
  171. tigrbl/runtime/atoms/response/renderer.py +116 -0
  172. tigrbl/runtime/atoms/response/template.py +44 -0
  173. tigrbl/runtime/atoms/response/templates.py +88 -0
  174. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  175. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  176. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  177. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  178. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  179. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  180. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  181. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  182. tigrbl/runtime/atoms/wire/dump.py +206 -0
  183. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  184. tigrbl/runtime/context.py +206 -0
  185. tigrbl/runtime/errors/__init__.py +61 -0
  186. tigrbl/runtime/errors/converters.py +214 -0
  187. tigrbl/runtime/errors/exceptions.py +124 -0
  188. tigrbl/runtime/errors/mappings.py +71 -0
  189. tigrbl/runtime/errors/utils.py +150 -0
  190. tigrbl/runtime/events.py +209 -0
  191. tigrbl/runtime/executor/__init__.py +6 -0
  192. tigrbl/runtime/executor/guards.py +132 -0
  193. tigrbl/runtime/executor/helpers.py +88 -0
  194. tigrbl/runtime/executor/invoke.py +150 -0
  195. tigrbl/runtime/executor/types.py +84 -0
  196. tigrbl/runtime/kernel.py +644 -0
  197. tigrbl/runtime/labels.py +353 -0
  198. tigrbl/runtime/opview.py +89 -0
  199. tigrbl/runtime/ordering.py +256 -0
  200. tigrbl/runtime/system.py +279 -0
  201. tigrbl/runtime/trace.py +330 -0
  202. tigrbl/schema/__init__.py +38 -0
  203. tigrbl/schema/_schema.py +27 -0
  204. tigrbl/schema/builder/__init__.py +17 -0
  205. tigrbl/schema/builder/build_schema.py +209 -0
  206. tigrbl/schema/builder/cache.py +24 -0
  207. tigrbl/schema/builder/compat.py +16 -0
  208. tigrbl/schema/builder/extras.py +85 -0
  209. tigrbl/schema/builder/helpers.py +51 -0
  210. tigrbl/schema/builder/list_params.py +117 -0
  211. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  212. tigrbl/schema/collect.py +79 -0
  213. tigrbl/schema/decorators.py +68 -0
  214. tigrbl/schema/get_schema.py +86 -0
  215. tigrbl/schema/schema_spec.py +20 -0
  216. tigrbl/schema/shortcuts.py +42 -0
  217. tigrbl/schema/types.py +34 -0
  218. tigrbl/schema/utils.py +143 -0
  219. tigrbl/session/README.md +14 -0
  220. tigrbl/session/__init__.py +28 -0
  221. tigrbl/session/abc.py +76 -0
  222. tigrbl/session/base.py +151 -0
  223. tigrbl/session/decorators.py +43 -0
  224. tigrbl/session/default.py +118 -0
  225. tigrbl/session/shortcuts.py +50 -0
  226. tigrbl/session/spec.py +112 -0
  227. tigrbl/shortcuts.py +22 -0
  228. tigrbl/specs.py +44 -0
  229. tigrbl/system/__init__.py +13 -0
  230. tigrbl/system/diagnostics/__init__.py +24 -0
  231. tigrbl/system/diagnostics/compat.py +31 -0
  232. tigrbl/system/diagnostics/healthz.py +41 -0
  233. tigrbl/system/diagnostics/hookz.py +51 -0
  234. tigrbl/system/diagnostics/kernelz.py +20 -0
  235. tigrbl/system/diagnostics/methodz.py +43 -0
  236. tigrbl/system/diagnostics/router.py +73 -0
  237. tigrbl/system/diagnostics/utils.py +43 -0
  238. tigrbl/system/uvicorn.py +60 -0
  239. tigrbl/table/__init__.py +9 -0
  240. tigrbl/table/_base.py +260 -0
  241. tigrbl/table/_table.py +54 -0
  242. tigrbl/table/mro_collect.py +69 -0
  243. tigrbl/table/shortcuts.py +57 -0
  244. tigrbl/table/table_spec.py +28 -0
  245. tigrbl/transport/__init__.py +74 -0
  246. tigrbl/transport/jsonrpc/__init__.py +19 -0
  247. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  248. tigrbl/transport/jsonrpc/helpers.py +115 -0
  249. tigrbl/transport/jsonrpc/models.py +41 -0
  250. tigrbl/transport/rest/__init__.py +25 -0
  251. tigrbl/transport/rest/aggregator.py +132 -0
  252. tigrbl/types/__init__.py +170 -0
  253. tigrbl/types/allow_anon_provider.py +19 -0
  254. tigrbl/types/authn_abc.py +30 -0
  255. tigrbl/types/nested_path_provider.py +22 -0
  256. tigrbl/types/op.py +35 -0
  257. tigrbl/types/op_config_provider.py +17 -0
  258. tigrbl/types/op_verb_alias_provider.py +33 -0
  259. tigrbl/types/request_extras_provider.py +22 -0
  260. tigrbl/types/response_extras_provider.py +22 -0
  261. tigrbl/types/table_config_provider.py +13 -0
  262. tigrbl/types/uuid.py +55 -0
  263. tigrbl-0.3.0.dist-info/METADATA +516 -0
  264. tigrbl-0.3.0.dist-info/RECORD +266 -0
  265. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dist-info}/WHEEL +1 -1
  266. tigrbl-0.3.0.dist-info/licenses/LICENSE +201 -0
  267. tigrbl/ExampleAgent.py +0 -1
  268. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  269. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Iterable, List, Mapping, Optional
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ # Prefer FastAPI HTTPException/status; fall back to Starlette; finally a tiny shim.
9
+ try: # FastAPI present
10
+ from fastapi import HTTPException, status
11
+ except Exception: # pragma: no cover
12
+ try: # Starlette present
13
+ from starlette.exceptions import HTTPException # type: ignore
14
+ from starlette import status # type: ignore
15
+ except Exception: # pragma: no cover
16
+
17
+ class HTTPException(Exception): # minimal shim
18
+ def __init__(
19
+ self,
20
+ status_code: int,
21
+ detail: Any = None,
22
+ headers: Optional[dict] = None,
23
+ ) -> None:
24
+ super().__init__(detail)
25
+ self.status_code = status_code
26
+ self.detail = detail
27
+ self.headers = headers
28
+
29
+ class _Status:
30
+ HTTP_400_BAD_REQUEST = 400
31
+ HTTP_401_UNAUTHORIZED = 401
32
+ HTTP_403_FORBIDDEN = 403
33
+ HTTP_404_NOT_FOUND = 404
34
+ HTTP_409_CONFLICT = 409
35
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
36
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
37
+ HTTP_501_NOT_IMPLEMENTED = 501
38
+ HTTP_503_SERVICE_UNAVAILABLE = 503
39
+ HTTP_504_GATEWAY_TIMEOUT = 504
40
+
41
+ status = _Status() # type: ignore
42
+
43
+ # Optional imports – code must run even if these packages aren’t installed.
44
+ try:
45
+ from pydantic import ValidationError as PydanticValidationError # v2
46
+ except Exception: # pragma: no cover
47
+ PydanticValidationError = None # type: ignore
48
+
49
+ try:
50
+ from fastapi.exceptions import (
51
+ RequestValidationError,
52
+ ) # emitted by FastAPI input validation
53
+ except Exception: # pragma: no cover
54
+ RequestValidationError = None # type: ignore
55
+
56
+ try:
57
+ # SQLAlchemy v1/v2 exception sets
58
+ from sqlalchemy.exc import IntegrityError, DBAPIError, OperationalError
59
+ from sqlalchemy.orm.exc import NoResultFound # type: ignore
60
+ except Exception: # pragma: no cover
61
+ IntegrityError = DBAPIError = OperationalError = NoResultFound = None # type: ignore
62
+
63
+
64
+ # Detect asyncpg constraint errors without importing asyncpg (optional dep).
65
+ _ASYNCPG_CONSTRAINT_NAMES = {
66
+ "UniqueViolationError",
67
+ "ForeignKeyViolationError",
68
+ "NotNullViolationError",
69
+ "CheckViolationError",
70
+ "ExclusionViolationError",
71
+ }
72
+
73
+
74
+ def _is_asyncpg_constraint_error(exc: BaseException) -> bool:
75
+ cls = type(exc)
76
+ return (cls.__module__ or "").startswith("asyncpg") and (
77
+ cls.__name__ in _ASYNCPG_CONSTRAINT_NAMES
78
+ )
79
+
80
+
81
+ def _limit(s: str, n: int = 4000) -> str:
82
+ return s if len(s) <= n else s[: n - 3] + "..."
83
+
84
+
85
+ def _stringify_exc(exc: BaseException) -> str:
86
+ detail = getattr(exc, "detail", None)
87
+ if detail:
88
+ return _limit(str(detail))
89
+ return _limit(f"{exc.__class__!r}: {str(exc) or repr(exc)}")
90
+
91
+
92
+ def _format_validation(err: Any) -> Any:
93
+ try:
94
+ items = err.errors() # pydantic / fastapi RequestValidationError
95
+ if isinstance(items, Iterable):
96
+ return list(items)
97
+ except Exception: # pragma: no cover
98
+ pass
99
+ return _limit(str(err))
100
+
101
+
102
+ def _get_temp(ctx: Any) -> Mapping[str, Any]:
103
+ tmp = getattr(ctx, "temp", None)
104
+ return tmp if isinstance(tmp, Mapping) else {}
105
+
106
+
107
+ def _has_in_errors(ctx: Any) -> bool:
108
+ tmp = _get_temp(ctx)
109
+ if tmp.get("in_invalid") is True:
110
+ return True
111
+ errs = tmp.get("in_errors")
112
+ return isinstance(errs, (list, tuple)) and len(errs) > 0
113
+
114
+
115
+ def _read_in_errors(ctx: Any) -> List[Dict[str, Any]]:
116
+ tmp = _get_temp(ctx)
117
+ errs = tmp.get("in_errors")
118
+ if isinstance(errs, list):
119
+ norm: List[Dict[str, Any]] = []
120
+ for e in errs:
121
+ if isinstance(e, Mapping):
122
+ field = e.get("field")
123
+ code = e.get("code") or "invalid"
124
+ msg = e.get("message") or "Invalid value."
125
+ entry = {"field": field, "code": code, "message": msg}
126
+ for k, v in e.items():
127
+ if k not in entry:
128
+ entry[k] = v
129
+ norm.append(entry)
130
+ return norm
131
+ return []
132
+
133
+
134
+ __all__ = [
135
+ "HTTPException",
136
+ "status",
137
+ "PydanticValidationError",
138
+ "RequestValidationError",
139
+ "IntegrityError",
140
+ "DBAPIError",
141
+ "OperationalError",
142
+ "NoResultFound",
143
+ "_is_asyncpg_constraint_error",
144
+ "_limit",
145
+ "_stringify_exc",
146
+ "_format_validation",
147
+ "_get_temp",
148
+ "_has_in_errors",
149
+ "_read_in_errors",
150
+ ]
@@ -0,0 +1,209 @@
1
+ # tigrbl/v3/runtime/events.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+ from typing import Dict, Iterable, List, Literal, Tuple
6
+
7
+ # ──────────────────────────────────────────────────────────────────────────────
8
+ # Phases
9
+ # - PRE_TX is a synthetic phase for security/dependency checks.
10
+ # - START_TX / END_TX are reserved for system steps (no atom anchors there).
11
+ # - Atoms bind only to the event anchors below.
12
+ # ──────────────────────────────────────────────────────────────────────────────
13
+
14
+ Phase = Literal[
15
+ "PRE_TX",
16
+ "START_TX",
17
+ "PRE_HANDLER",
18
+ "HANDLER",
19
+ "POST_HANDLER",
20
+ "END_TX",
21
+ "POST_RESPONSE",
22
+ ]
23
+
24
+ PHASES: Tuple[Phase, ...] = (
25
+ "PRE_TX",
26
+ "START_TX", # system-only
27
+ "PRE_HANDLER",
28
+ "HANDLER",
29
+ "POST_HANDLER",
30
+ "END_TX", # system-only
31
+ "POST_RESPONSE",
32
+ )
33
+
34
+ # ──────────────────────────────────────────────────────────────────────────────
35
+ # Canonical anchors (events) — the only moments atoms can bind to
36
+ # Keep these names stable; labels use them directly: step_kind:domain:subject@ANCHOR
37
+ # ──────────────────────────────────────────────────────────────────────────────
38
+
39
+ # PRE_HANDLER
40
+ SCHEMA_COLLECT_IN = "schema:collect_in"
41
+ IN_VALIDATE = "in:validate"
42
+
43
+ # HANDLER
44
+ RESOLVE_VALUES = "resolve:values"
45
+ PRE_FLUSH = "pre:flush"
46
+ EMIT_ALIASES_PRE = "emit:aliases:pre_flush"
47
+
48
+ # POST_HANDLER
49
+ POST_FLUSH = "post:flush"
50
+ EMIT_ALIASES_POST = "emit:aliases:post_refresh"
51
+ SCHEMA_COLLECT_OUT = "schema:collect_out"
52
+ OUT_BUILD = "out:build"
53
+
54
+ # POST_RESPONSE
55
+ EMIT_ALIASES_READ = "emit:aliases:readtime"
56
+ OUT_DUMP = "out:dump"
57
+
58
+ # Canonical order of event anchors within the request lifecycle.
59
+ # This ordering is global and stable; use it to produce deterministic plans/traces.
60
+ _EVENT_ORDER: Tuple[str, ...] = (
61
+ # PRE_HANDLER
62
+ SCHEMA_COLLECT_IN,
63
+ IN_VALIDATE,
64
+ # HANDLER
65
+ RESOLVE_VALUES,
66
+ PRE_FLUSH,
67
+ EMIT_ALIASES_PRE,
68
+ # POST_HANDLER
69
+ POST_FLUSH,
70
+ EMIT_ALIASES_POST,
71
+ SCHEMA_COLLECT_OUT,
72
+ OUT_BUILD,
73
+ # POST_RESPONSE
74
+ EMIT_ALIASES_READ,
75
+ OUT_DUMP,
76
+ )
77
+
78
+
79
+ # Map each anchor to its phase and persistence tie.
80
+ # "persist_tied=True" means the anchor is pruned for non-persisting ops
81
+ # (e.g., read/list) and whenever an op is executed with persist=False.
82
+ @dataclass(frozen=True)
83
+ class AnchorInfo:
84
+ name: str
85
+ phase: Phase
86
+ ordinal: int
87
+ persist_tied: bool
88
+
89
+
90
+ _ANCHORS: Dict[str, AnchorInfo] = {
91
+ # PRE_HANDLER (not persist-tied)
92
+ SCHEMA_COLLECT_IN: AnchorInfo(SCHEMA_COLLECT_IN, "PRE_HANDLER", 0, False),
93
+ IN_VALIDATE: AnchorInfo(IN_VALIDATE, "PRE_HANDLER", 1, False),
94
+ RESOLVE_VALUES: AnchorInfo(RESOLVE_VALUES, "PRE_HANDLER", 2, True),
95
+ PRE_FLUSH: AnchorInfo(PRE_FLUSH, "PRE_HANDLER", 3, True),
96
+ EMIT_ALIASES_PRE: AnchorInfo(EMIT_ALIASES_PRE, "PRE_HANDLER", 4, True),
97
+ # POST_HANDLER (mixed)
98
+ POST_FLUSH: AnchorInfo(POST_FLUSH, "POST_HANDLER", 5, True),
99
+ EMIT_ALIASES_POST: AnchorInfo(EMIT_ALIASES_POST, "POST_HANDLER", 6, True),
100
+ SCHEMA_COLLECT_OUT: AnchorInfo(SCHEMA_COLLECT_OUT, "POST_HANDLER", 7, False),
101
+ OUT_BUILD: AnchorInfo(OUT_BUILD, "POST_HANDLER", 8, False),
102
+ # POST_RESPONSE (not persist-tied)
103
+ EMIT_ALIASES_READ: AnchorInfo(EMIT_ALIASES_READ, "POST_RESPONSE", 9, False),
104
+ OUT_DUMP: AnchorInfo(OUT_DUMP, "POST_RESPONSE", 10, False),
105
+ }
106
+
107
+ # ──────────────────────────────────────────────────────────────────────────────
108
+ # Public helpers
109
+ # ──────────────────────────────────────────────────────────────────────────────
110
+
111
+
112
+ def is_valid_event(anchor: str) -> bool:
113
+ """True if the given anchor is one of the canonical events."""
114
+ return anchor in _ANCHORS
115
+
116
+
117
+ def phase_for_event(anchor: str) -> Phase:
118
+ """Return the Phase for a canonical event; raises on unknown anchors."""
119
+ try:
120
+ return _ANCHORS[anchor].phase
121
+ except KeyError as e:
122
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
123
+
124
+
125
+ def is_persist_tied(anchor: str) -> bool:
126
+ """Return True if this event is pruned for non-persisting ops."""
127
+ try:
128
+ return _ANCHORS[anchor].persist_tied
129
+ except KeyError as e:
130
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
131
+
132
+
133
+ def get_anchor_info(anchor: str) -> AnchorInfo:
134
+ """Return the full :class:`AnchorInfo` for a canonical event."""
135
+ try:
136
+ return _ANCHORS[anchor]
137
+ except KeyError as e:
138
+ raise ValueError(f"Unknown event anchor: {anchor!r}") from e
139
+
140
+
141
+ def all_events_ordered() -> List[str]:
142
+ """Return all canonical events in deterministic, lifecycle order."""
143
+ return list(_EVENT_ORDER)
144
+
145
+
146
+ def events_for_phase(phase: Phase) -> List[str]:
147
+ """Return the subset of events that belong to the given phase."""
148
+ if phase not in PHASES:
149
+ raise ValueError(f"Unknown phase: {phase!r}")
150
+ return [a for a, info in _ANCHORS.items() if info.phase == phase]
151
+
152
+
153
+ def prune_events_for_persist(anchors: Iterable[str], *, persist: bool) -> List[str]:
154
+ """
155
+ Given a sequence of anchors, return a new list with persistence-tied events
156
+ removed when persist=False. Unknown anchors raise ValueError.
157
+ """
158
+ out: List[str] = []
159
+ for a in anchors:
160
+ if not is_valid_event(a):
161
+ raise ValueError(f"Unknown event anchor: {a!r}")
162
+ if not persist and _ANCHORS[a].persist_tied:
163
+ continue
164
+ out.append(a)
165
+ # keep canonical order irrespective of input order
166
+ out.sort(key=lambda x: _ANCHORS[x].ordinal)
167
+ return out
168
+
169
+
170
+ def order_events(anchors: Iterable[str]) -> List[str]:
171
+ """
172
+ Sort a set/list of anchors into canonical lifecycle order.
173
+ Raises on unknown anchors.
174
+ """
175
+ anchors = list(anchors)
176
+ for a in anchors:
177
+ if a not in _ANCHORS:
178
+ raise ValueError(f"Unknown event anchor: {a!r}")
179
+ anchors.sort(key=lambda x: _ANCHORS[x].ordinal)
180
+ return anchors
181
+
182
+
183
+ __all__ = [
184
+ # Phases
185
+ "Phase",
186
+ "PHASES",
187
+ # Anchors (constants)
188
+ "SCHEMA_COLLECT_IN",
189
+ "IN_VALIDATE",
190
+ "RESOLVE_VALUES",
191
+ "PRE_FLUSH",
192
+ "EMIT_ALIASES_PRE",
193
+ "POST_FLUSH",
194
+ "EMIT_ALIASES_POST",
195
+ "SCHEMA_COLLECT_OUT",
196
+ "OUT_BUILD",
197
+ "EMIT_ALIASES_READ",
198
+ "OUT_DUMP",
199
+ # Types / helpers
200
+ "AnchorInfo",
201
+ "is_valid_event",
202
+ "phase_for_event",
203
+ "is_persist_tied",
204
+ "get_anchor_info",
205
+ "all_events_ordered",
206
+ "events_for_phase",
207
+ "prune_events_for_persist",
208
+ "order_events",
209
+ ]
@@ -0,0 +1,6 @@
1
+ # tigrbl/v3/runtime/executor/__init__.py
2
+ from .types import _Ctx, HandlerStep, PhaseChains
3
+ from .helpers import _in_tx
4
+ from .invoke import _invoke
5
+
6
+ __all__ = ["_Ctx", "_invoke", "_in_tx", "HandlerStep", "PhaseChains"]
@@ -0,0 +1,132 @@
1
+ """Database session guard utilities for the runtime executor.
2
+
3
+ This module temporarily replaces ``commit`` and ``flush`` on SQLAlchemy
4
+ sessions to enforce phase-specific policies. Each guard returns a handle that
5
+ restores the original methods once the phase completes and provides helpers to
6
+ rollback when the runtime owns the transaction.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Optional, Union
13
+
14
+ try:
15
+ from sqlalchemy.orm import Session # type: ignore
16
+ from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
17
+ except Exception: # pragma: no cover
18
+ Session = Any # type: ignore
19
+ AsyncSession = Any # type: ignore
20
+
21
+ from .types import _Ctx, PhaseChains
22
+ from .helpers import _is_async_db, _run_chain, _g
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class _GuardHandle:
28
+ """Stores original ``commit``/``flush`` methods for later restoration."""
29
+
30
+ __slots__ = ("db", "orig_commit", "orig_flush")
31
+
32
+ def __init__(self, db: Any, orig_commit: Any, orig_flush: Any) -> None:
33
+ self.db = db
34
+ self.orig_commit = orig_commit
35
+ self.orig_flush = orig_flush
36
+
37
+ def restore(self) -> None:
38
+ if self.orig_commit is not None:
39
+ try:
40
+ setattr(self.db, "commit", self.orig_commit)
41
+ except Exception:
42
+ pass # pragma: no cover
43
+ if self.orig_flush is not None:
44
+ try:
45
+ setattr(self.db, "flush", self.orig_flush)
46
+ except Exception:
47
+ pass # pragma: no cover
48
+
49
+
50
+ def _install_db_guards(
51
+ db: Union[Session, AsyncSession, None],
52
+ *,
53
+ phase: str,
54
+ allow_flush: bool,
55
+ allow_commit: bool,
56
+ require_owned_tx_for_commit: bool,
57
+ owns_tx: bool,
58
+ ) -> _GuardHandle:
59
+ """Install guards that restrict ``commit``/``flush`` during a phase.
60
+
61
+ Parameters:
62
+ db: SQLAlchemy ``Session``/``AsyncSession`` to guard.
63
+ phase: Name of the executing phase for error messages.
64
+ allow_flush: Whether ``flush`` should be permitted.
65
+ allow_commit: Whether ``commit`` should be permitted.
66
+ require_owned_tx_for_commit: Block commits if the executor did not
67
+ open the transaction.
68
+ owns_tx: Whether the runtime opened the transaction.
69
+
70
+ Returns:
71
+ A ``_GuardHandle`` for restoring original methods.
72
+ """
73
+ if db is None:
74
+ return _GuardHandle(None, None, None)
75
+ orig_commit = getattr(db, "commit", None)
76
+ orig_flush = getattr(db, "flush", None)
77
+
78
+ def _raise(op: str) -> None:
79
+ raise RuntimeError(f"db.{op}() is not allowed during {phase} phase")
80
+
81
+ if not allow_commit or (require_owned_tx_for_commit and not owns_tx):
82
+ if _is_async_db(db):
83
+
84
+ async def _blocked_commit() -> None: # type: ignore[func-returns-value]
85
+ _raise("commit")
86
+ else:
87
+
88
+ def _blocked_commit() -> None: # type: ignore[func-returns-value]
89
+ _raise("commit")
90
+
91
+ setattr(db, "commit", _blocked_commit) # type: ignore[assignment]
92
+
93
+ if not allow_flush:
94
+ if _is_async_db(db):
95
+
96
+ async def _blocked_flush() -> None: # type: ignore[func-returns-value]
97
+ _raise("flush")
98
+ else:
99
+
100
+ def _blocked_flush() -> None: # type: ignore[func-returns-value]
101
+ _raise("flush")
102
+
103
+ setattr(db, "flush", _blocked_flush) # type: ignore[assignment]
104
+
105
+ return _GuardHandle(db, orig_commit, orig_flush)
106
+
107
+
108
+ async def _rollback_if_owned(
109
+ db: Union[Session, AsyncSession, None],
110
+ owns_tx: bool,
111
+ *,
112
+ phases: Optional[PhaseChains],
113
+ ctx: _Ctx,
114
+ ) -> None:
115
+ """Rollback the session if this runtime owns the transaction."""
116
+
117
+ if not owns_tx or db is None:
118
+ return
119
+ try:
120
+ if _is_async_db(db):
121
+ await db.rollback() # type: ignore[func-returns-value]
122
+ else:
123
+ db.rollback()
124
+ except Exception as rb_exc: # pragma: no cover
125
+ logger.exception("Rollback failed: %s", rb_exc)
126
+ try:
127
+ await _run_chain(ctx, _g(phases, "ON_ROLLBACK"), phase="ON_ROLLBACK")
128
+ except Exception: # pragma: no cover
129
+ pass
130
+
131
+
132
+ __all__ = ["_GuardHandle", "_install_db_guards", "_rollback_if_owned"]
@@ -0,0 +1,88 @@
1
+ # tigrbl/v3/runtime/executor/helpers.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import logging
6
+ from typing import Any, Iterable, Optional, Sequence
7
+
8
+ try:
9
+ from sqlalchemy.ext.asyncio import AsyncSession # type: ignore
10
+ except Exception: # pragma: no cover
11
+ AsyncSession = Any # type: ignore
12
+
13
+ try:
14
+ from .. import trace as _trace # type: ignore
15
+ except Exception: # pragma: no cover
16
+ _trace = None # type: ignore
17
+
18
+ from .types import _Ctx, HandlerStep, PhaseChains
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def _is_async_db(db: Any) -> bool:
24
+ """Detect DB interfaces that require `await` for transactional methods."""
25
+ if isinstance(db, AsyncSession) or hasattr(db, "run_sync"):
26
+ return True
27
+ for attr in ("commit", "begin", "rollback", "flush"):
28
+ if inspect.iscoroutinefunction(getattr(db, attr, None)):
29
+ return True
30
+ return False
31
+
32
+
33
+ def _bool_call(meth: Any) -> bool:
34
+ try:
35
+ return bool(meth())
36
+ except Exception: # pragma: no cover
37
+ return False
38
+
39
+
40
+ def _in_tx(db: Any) -> bool:
41
+ for name in ("in_transaction", "in_nested_transaction"):
42
+ attr = getattr(db, name, None)
43
+ if callable(attr):
44
+ if _bool_call(attr):
45
+ return True
46
+ elif attr:
47
+ return True
48
+ return False
49
+
50
+
51
+ async def _maybe_await(v: Any) -> Any:
52
+ if inspect.isawaitable(v):
53
+ return await v # type: ignore[func-returns-value]
54
+ return v
55
+
56
+
57
+ async def _run_chain(
58
+ ctx: _Ctx, chain: Optional[Iterable[HandlerStep]], *, phase: str
59
+ ) -> None:
60
+ if not chain:
61
+ return
62
+ if _trace is not None:
63
+ with _trace.span(ctx, f"phase:{phase}"):
64
+ for step in chain:
65
+ rv = step(ctx)
66
+ rv = await _maybe_await(rv)
67
+ if rv is not None:
68
+ ctx.result = rv
69
+ return
70
+ for step in chain:
71
+ rv = step(ctx)
72
+ rv = await _maybe_await(rv)
73
+ if rv is not None:
74
+ ctx.result = rv
75
+
76
+
77
+ def _g(phases: Optional[PhaseChains], key: str) -> Sequence[HandlerStep]:
78
+ return () if not phases else phases.get(key, ())
79
+
80
+
81
+ __all__ = [
82
+ "_is_async_db",
83
+ "_bool_call",
84
+ "_in_tx",
85
+ "_maybe_await",
86
+ "_run_chain",
87
+ "_g",
88
+ ]