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,171 @@
1
+ from __future__ import annotations
2
+ from typing import Any, AsyncIterable, Iterable, Mapping, Optional, Union
3
+ from datetime import datetime, timezone
4
+ from pathlib import Path
5
+ import json
6
+ import os
7
+ import mimetypes
8
+ import base64
9
+
10
+ from ..deps.starlette import (
11
+ JSONResponse,
12
+ HTMLResponse,
13
+ PlainTextResponse,
14
+ StreamingResponse,
15
+ FileResponse as StarletteFileResponse,
16
+ RedirectResponse,
17
+ Response,
18
+ )
19
+
20
+
21
+ def _json_default(value: Any) -> Any:
22
+ if isinstance(value, (bytes, bytearray, memoryview)):
23
+ return base64.b64encode(bytes(value)).decode("ascii")
24
+ if isinstance(value, Path):
25
+ return str(value)
26
+ return str(value)
27
+
28
+
29
+ try:
30
+ import orjson as _orjson
31
+
32
+ _ORJSON_OPTIONS = (
33
+ getattr(_orjson, "OPT_NON_STR_KEYS", 0)
34
+ | getattr(_orjson, "OPT_SERIALIZE_NUMPY", 0)
35
+ | getattr(_orjson, "OPT_SERIALIZE_BYTES", 0)
36
+ )
37
+
38
+ def _dumps(obj: Any) -> bytes:
39
+ try:
40
+ return _orjson.dumps(
41
+ obj,
42
+ option=_ORJSON_OPTIONS,
43
+ default=_json_default,
44
+ )
45
+ except TypeError:
46
+ # Fallback for older orjson builds missing optional flags
47
+ return json.dumps(
48
+ obj,
49
+ separators=(",", ":"),
50
+ ensure_ascii=False,
51
+ default=_json_default,
52
+ ).encode("utf-8")
53
+ except Exception: # pragma: no cover - fallback
54
+
55
+ def _dumps(obj: Any) -> bytes:
56
+ return json.dumps(
57
+ obj,
58
+ separators=(",", ":"),
59
+ ensure_ascii=False,
60
+ default=_json_default,
61
+ ).encode("utf-8")
62
+
63
+
64
+ def _maybe_envelope(data: Any) -> Any:
65
+ if isinstance(data, Mapping) and ("data" in data or "error" in data):
66
+ return data
67
+ return {"data": data, "ok": True}
68
+
69
+
70
+ JSON = Mapping[str, Any]
71
+ Headers = Mapping[str, str]
72
+
73
+
74
+ def as_json(
75
+ data: Any,
76
+ *,
77
+ status: int = 200,
78
+ headers: Optional[Headers] = None,
79
+ envelope: bool = True,
80
+ dumps=_dumps,
81
+ ) -> Response:
82
+ payload = _maybe_envelope(data) if envelope else data
83
+ try:
84
+ return JSONResponse(
85
+ payload,
86
+ status_code=status,
87
+ headers=dict(headers or {}),
88
+ dumps=lambda o: dumps(o).decode(),
89
+ )
90
+ except TypeError: # pragma: no cover - starlette >= 0.44
91
+ return Response(
92
+ dumps(payload),
93
+ status_code=status,
94
+ headers=dict(headers or {}),
95
+ media_type="application/json",
96
+ )
97
+
98
+
99
+ def as_html(
100
+ html: str, *, status: int = 200, headers: Optional[Headers] = None
101
+ ) -> Response:
102
+ return HTMLResponse(html, status_code=status, headers=dict(headers or {}))
103
+
104
+
105
+ def as_text(
106
+ text: str, *, status: int = 200, headers: Optional[Headers] = None
107
+ ) -> Response:
108
+ return PlainTextResponse(text, status_code=status, headers=dict(headers or {}))
109
+
110
+
111
+ def as_redirect(
112
+ url: str, *, status: int = 307, headers: Optional[Headers] = None
113
+ ) -> Response:
114
+ return RedirectResponse(url, status_code=status, headers=dict(headers or {}))
115
+
116
+
117
+ def as_stream(
118
+ chunks: Union[Iterable[bytes], AsyncIterable[bytes]],
119
+ *,
120
+ media_type: str = "application/octet-stream",
121
+ status: int = 200,
122
+ headers: Optional[Headers] = None,
123
+ ) -> Response:
124
+ return StreamingResponse(
125
+ chunks, media_type=media_type, status_code=status, headers=dict(headers or {})
126
+ )
127
+
128
+
129
+ def as_file(
130
+ path: Union[str, Path],
131
+ *,
132
+ filename: Optional[str] = None,
133
+ download: bool = False,
134
+ status: int = 200,
135
+ headers: Optional[Headers] = None,
136
+ stat_result: Optional[os.stat_result] = None,
137
+ etag: Optional[str] = None,
138
+ last_modified: Optional[datetime] = None,
139
+ ) -> Response:
140
+ p = Path(path)
141
+ if not p.exists() or not p.is_file():
142
+ return PlainTextResponse("Not Found", status_code=404)
143
+ media_type, _ = mimetypes.guess_type(str(p))
144
+ media_type = media_type or "application/octet-stream"
145
+ hdrs = dict(headers or {})
146
+ st = stat_result or os.stat(p)
147
+ if etag is None:
148
+ etag = f'W/"{st.st_mtime_ns}-{st.st_size}"'
149
+ lm = last_modified or datetime.fromtimestamp(st.st_mtime, tz=timezone.utc)
150
+ hdrs.setdefault("ETag", etag)
151
+ hdrs.setdefault("Last-Modified", lm.strftime("%a, %d %b %Y %H:%M:%S GMT"))
152
+ if download or filename:
153
+ fname = filename or p.name
154
+ hdrs.setdefault("Content-Disposition", f'attachment; filename="{fname}"')
155
+ return StarletteFileResponse(
156
+ str(p),
157
+ status_code=status,
158
+ media_type=media_type,
159
+ filename=filename,
160
+ headers=hdrs,
161
+ )
162
+
163
+
164
+ __all__ = [
165
+ "as_json",
166
+ "as_html",
167
+ "as_text",
168
+ "as_redirect",
169
+ "as_stream",
170
+ "as_file",
171
+ ]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Dict, List, Literal, Optional
4
+
5
+ ResponseKind = Literal["auto", "json", "html", "text", "file", "stream", "redirect"]
6
+
7
+
8
+ @dataclass(slots=True)
9
+ class TemplateSpec:
10
+ name: str
11
+ search_paths: List[str] = field(default_factory=list)
12
+ package: Optional[str] = None
13
+ auto_reload: Optional[bool] = None
14
+ filters: Dict[str, object] = field(default_factory=dict)
15
+ globals: Dict[str, object] = field(default_factory=dict)
16
+
17
+
18
+ @dataclass(slots=True)
19
+ class ResponseSpec:
20
+ kind: ResponseKind = "auto"
21
+ media_type: Optional[str] = None
22
+ status_code: Optional[int] = None
23
+ headers: Dict[str, str] = field(default_factory=dict)
24
+ envelope: Optional[bool] = None
25
+ template: Optional[TemplateSpec] = None
26
+ filename: Optional[str] = None
27
+ download: Optional[bool] = None
28
+ etag: Optional[str] = None
29
+ cache_control: Optional[str] = None
30
+ redirect_to: Optional[str] = None
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class Template(TemplateSpec):
35
+ """Concrete template configuration used at runtime."""
36
+
37
+
38
+ @dataclass(slots=True)
39
+ class Response(ResponseSpec):
40
+ """Concrete response configuration used at runtime."""
41
+
42
+
43
+ __all__ = [
44
+ "TemplateSpec",
45
+ "ResponseSpec",
46
+ "ResponseKind",
47
+ "Template",
48
+ "Response",
49
+ ]
@@ -0,0 +1,27 @@
1
+ """tigrbl_routes.py
2
+ Helpers that build path prefixes for nested REST endpoints.
3
+ The logic is intentionally minimal; extend or override as needed.
4
+ """
5
+
6
+ from __future__ import annotations
7
+ from typing import Optional, Type
8
+
9
+ from ..config.constants import TIGRBL_NESTED_PATHS_ATTR
10
+
11
+
12
+ def _nested_prefix(model: Type) -> Optional[str]:
13
+ """Return the user-supplied hierarchical prefix or *None*.
14
+
15
+ • If the SQLAlchemy model defines `__tigrbl_nested_paths__`
16
+ → call it and return the result.
17
+ • Else, fall back to legacy `_nested_path` string if present.
18
+ • Otherwise → signal ``no nested route wanted`` with ``None``.
19
+ """
20
+
21
+ cb = getattr(model, TIGRBL_NESTED_PATHS_ATTR, None)
22
+ if callable(cb):
23
+ return cb()
24
+ return getattr(model, "_nested_path", None)
25
+
26
+
27
+ __all__ = ["_nested_prefix"]
@@ -0,0 +1,129 @@
1
+ # Runtime Execution Module (v3)
2
+
3
+ > **Maintainer-only:** This module is internal to the SDK. Downstream users **must not** modify or rely on it directly.
4
+
5
+ The runtime executor coordinates Tigrbl operations through a fixed set of **phase chains**. Each phase has a list of steps built by the kernel and is executed under strict database guards.
6
+
7
+ ## Phase Chains
8
+
9
+ Phase chains map phase names to ordered handler lists. The executor runs the phases in the sequence below:
10
+
11
+ 1. `PRE_TX_BEGIN` – pre-transaction checks.
12
+ 2. `START_TX` – open a new transaction (system-only).
13
+ 3. `PRE_HANDLER` – request validation and setup.
14
+ 4. `HANDLER` – core operation logic.
15
+ 5. `POST_HANDLER` – post-processing while still in the transaction.
16
+ 6. `PRE_COMMIT` – final checks before committing.
17
+ 7. `END_TX` – commit and close the transaction.
18
+ 8. `POST_COMMIT` – steps after commit.
19
+ 9. `POST_RESPONSE` – fire-and-forget side effects.
20
+
21
+ ## Step Kinds
22
+
23
+ The kernel labels every piece of work so it can be ordered predictably:
24
+
25
+ - **secdeps** – security dependencies that run before any other steps. Downstream
26
+ applications configure these to enforce authentication or authorization.
27
+ - **deps** – general dependencies resolved ahead of handlers. These are also
28
+ provided downstream.
29
+ - **sys** – system steps shipped with Tigrbl to coordinate core behavior. They
30
+ are maintained by project maintainers.
31
+ - **atoms** – built-in runtime units such as schema collectors or wire
32
+ serializers. Maintainers own these components.
33
+ - **hooks** – user-supplied handlers that attach to anchors within phases.
34
+
35
+ Downstream consumers configure `secdeps`, `deps`, and `hooks`, while `sys` and
36
+ `atom` steps are maintained by the Tigrbl maintainers.
37
+
38
+
39
+ ## Step Precedence
40
+
41
+ When the kernel assembles an operation it flattens several step kinds into a
42
+ single execution plan. They run in the following precedence:
43
+
44
+ 1. Security dependencies (`secdeps`)
45
+ 2. General dependencies (`deps`)
46
+ 3. System steps (`sys`) such as transaction begin, handler dispatch, and commit
47
+ 4. Runtime atoms (`atoms`)
48
+ 5. Hooks (`hooks`)
49
+
50
+ System steps appear only on the `START_TX`, `HANDLER`, and `END_TX` anchors. Within
51
+ each anchor, atoms execute before hooks and any remaining ties are resolved by
52
+ anchor-specific preferences.
53
+
54
+ ## Atom Domains
55
+
56
+ Atoms are grouped into domain-specific registries so the kernel can inject them
57
+ at the correct stage of the lifecycle. Each domain focuses on a different slice
58
+ of request or response processing:
59
+
60
+ - **wire** – builds inbound data, validates it, and prepares outbound payloads at
61
+ the field level.
62
+ - **schema** – collects request and response schema definitions for models.
63
+ - **resolve** – assembles derived values or generates paired inputs before they
64
+ are flushed.
65
+ - **storage** – converts field specifications into storage-layer instructions.
66
+ - **emit** – surfaces runtime metadata such as aliases or extras for downstream
67
+ consumers.
68
+ - **out** – mutates data after handlers run, for example masking fields before
69
+ serialization.
70
+ - **response** – negotiates content types and renders the final HTTP response or
71
+ template.
72
+ - **refresh** – triggers post-commit refreshes like demand-driven reloads.
73
+
74
+ Domains differ by the moment they run and the guarantees they provide. A `wire`
75
+ atom transforms raw request values before validation, whereas a `response` atom
76
+ operates after the transaction is committed to shape the returned payload. The
77
+ kernel uses the pair `(domain, subject)` to register and inject atoms into phase
78
+ chains.
79
+
80
+ ## DB Guards
81
+
82
+ For every phase the executor installs database guards that monkey‑patch
83
+ `commit` and `flush` on the session. Guards enforce which operations are
84
+ allowed and ensure only the owning transaction may commit.
85
+
86
+ The guard installer swaps these methods with stubs that raise
87
+ `RuntimeError` when a disallowed operation is attempted. Each phase passes
88
+ flags describing its policy:
89
+
90
+ - `allow_flush` – permit calls to `session.flush`.
91
+ - `allow_commit` – permit calls to `session.commit`.
92
+ - `require_owned_tx_for_commit` – when `True`, block commits if the
93
+ executor did not open the transaction.
94
+
95
+ The installer returns a handle that restores the original methods once the
96
+ phase finishes so restrictions do not leak across phases. A companion
97
+ helper triggers a rollback if the runtime owns the transaction and a phase
98
+ raises an error.
99
+
100
+ | Phase | Flush | Commit | Notes |
101
+ |-------|-------|--------|-------|
102
+ | PRE_TX_BEGIN | ❌ | ❌ | no database writes |
103
+ | START_TX | ❌ | ❌ | transaction opening |
104
+ | PRE_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
105
+ | HANDLER | ✅ | ❌ | writes allowed, commit blocked |
106
+ | POST_HANDLER | ✅ | ❌ | writes allowed, commit blocked |
107
+ | PRE_COMMIT | ❌ | ❌ | freeze writes before commit |
108
+ | END_TX | ✅ | ✅ | commit allowed only if runtime owns the transaction |
109
+ | POST_COMMIT | ✅ | ❌ | post-commit writes without commit |
110
+ | POST_RESPONSE | ❌ | ❌ | background work, no writes |
111
+
112
+ ### Transaction Boundaries
113
+
114
+ `start_tx` is a system step that opens a new database transaction when
115
+ no transaction is active and marks the runtime as its owner. While this
116
+ phase runs, both `session.flush` and `session.commit` are blocked. After a
117
+ transaction is started, phases such as `PRE_HANDLER`, `HANDLER`, and
118
+ `POST_HANDLER` allow flushes so SQL statements can be issued while the
119
+ commit remains deferred. The `end_tx` step executes during the `END_TX`
120
+ phase, performing a final flush and committing the transaction if the
121
+ runtime owns it. Once this phase completes, guards restore the original
122
+ session methods.
123
+
124
+ If a phase fails, the guard restores the original methods and the executor rolls back when it owns the transaction. Optional `ON_<PHASE>_ERROR` chains can handle cleanup.
125
+
126
+ ---
127
+
128
+ This runtime layer is maintained by the core team. Downstream packages should treat it as read‑only and interact only through the public Tigrbl interfaces.
129
+
@@ -0,0 +1,20 @@
1
+ # tigrbl/v3/runtime/__init__.py
2
+ from .executor import _invoke, _Ctx
3
+ from .kernel import Kernel, build_phase_chains, run, get_cached_specs, _default_kernel
4
+ from . import events, errors, context
5
+ from .labels import STEP_KINDS, DOMAINS
6
+
7
+ __all__ = [
8
+ "_invoke",
9
+ "_Ctx",
10
+ "Kernel",
11
+ "build_phase_chains",
12
+ "run",
13
+ "get_cached_specs",
14
+ "_default_kernel",
15
+ "events",
16
+ "errors",
17
+ "context",
18
+ "STEP_KINDS",
19
+ "DOMAINS",
20
+ ]
@@ -0,0 +1,102 @@
1
+ # tigrbl/v3/runtime/atoms/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+ import logging
6
+
7
+ from .. import events as _ev
8
+
9
+ # Domain registries
10
+ from .emit import REGISTRY as _EMIT
11
+ from .out import REGISTRY as _OUT
12
+ from .refresh import REGISTRY as _REFRESH
13
+ from .resolve import REGISTRY as _RESOLVE
14
+ from .schema import REGISTRY as _SCHEMA
15
+ from .storage import REGISTRY as _STORAGE
16
+ from .wire import REGISTRY as _WIRE
17
+ from .response import REGISTRY as _RESPONSE
18
+
19
+ # Runner signature: (obj|None, ctx) -> None
20
+ RunFn = Callable[[Optional[object], Any], None]
21
+
22
+ #: Global registry consumed by the kernel plan:
23
+ #: { (domain, subject): (anchor, runner) }
24
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {}
25
+
26
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
27
+ logger = logging.getLogger("uvicorn")
28
+
29
+
30
+ def _add_bulk(source: Dict[Tuple[str, str], Tuple[str, RunFn]]) -> None:
31
+ for key, val in source.items():
32
+ if key in REGISTRY:
33
+ logger.error("Duplicate atom registration attempted: %s", key)
34
+ raise RuntimeError(f"Duplicate atom registration: {key!r}")
35
+ anchor, fn = val
36
+ if not _ev.is_valid_event(anchor):
37
+ logger.error("Atom %s declares unknown anchor %s", key, anchor)
38
+ raise ValueError(f"Atom {key!r} declares unknown anchor {anchor!r}")
39
+ REGISTRY[key] = (anchor, fn)
40
+ logger.debug("Registered atom %s -> %s", key, anchor)
41
+
42
+
43
+ # Aggregate all domains
44
+ _add_bulk(_EMIT)
45
+ _add_bulk(_OUT)
46
+ _add_bulk(_REFRESH)
47
+ _add_bulk(_RESOLVE)
48
+ _add_bulk(_SCHEMA)
49
+ _add_bulk(_STORAGE)
50
+ _add_bulk(_WIRE)
51
+ _add_bulk(_RESPONSE)
52
+
53
+ logger.info("Loaded %d runtime atoms", len(REGISTRY))
54
+
55
+ # ── Back-compat subject aliases (optional) ────────────────────────────────────
56
+ # Allow "wire:validate" as an alias of "wire:validate_in".
57
+ if ("wire", "validate_in") in REGISTRY and ("wire", "validate") not in REGISTRY:
58
+ REGISTRY[("wire", "validate")] = REGISTRY[("wire", "validate_in")]
59
+
60
+ # ── Public helpers ────────────────────────────────────────────────────────────
61
+
62
+
63
+ def domains() -> Tuple[str, ...]:
64
+ """Return all domains present in the registry."""
65
+ out = tuple(sorted({d for (d, _) in REGISTRY.keys()}))
66
+ logger.debug("Listing domains: %s", out)
67
+ return out
68
+
69
+
70
+ def subjects(domain: str) -> Tuple[str, ...]:
71
+ """Return subjects available for a given domain."""
72
+ out = tuple(sorted(s for (d, s) in REGISTRY.keys() if d == domain))
73
+ logger.debug("Listing subjects for %s: %s", domain, out)
74
+ return out
75
+
76
+
77
+ def get(domain: str, subject: str) -> Tuple[str, RunFn]:
78
+ """Return (anchor, runner) for a given (domain, subject)."""
79
+ key = (domain, subject)
80
+ if key not in REGISTRY:
81
+ logger.error("Unknown atom requested: %s:%s", domain, subject)
82
+ raise KeyError(f"Unknown atom: {domain}:{subject}")
83
+ val = REGISTRY[key]
84
+ logger.debug("Retrieved atom %s:%s -> %s", domain, subject, val[0])
85
+ return val
86
+
87
+
88
+ def all_items() -> Tuple[Tuple[Tuple[str, str], Tuple[str, RunFn]], ...]:
89
+ """Return the registry items as a sorted tuple for deterministic iteration."""
90
+ items = tuple(sorted(REGISTRY.items(), key=lambda kv: (kv[0][0], kv[0][1])))
91
+ logger.debug("Listing all registry items (%d)", len(items))
92
+ return items
93
+
94
+
95
+ __all__ = [
96
+ "RunFn",
97
+ "REGISTRY",
98
+ "domains",
99
+ "subjects",
100
+ "get",
101
+ "all_items",
102
+ ]
@@ -0,0 +1,42 @@
1
+ # tigrbl/v3/runtime/atoms/emit/__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 paired_pre as _paired_pre
9
+ from . import paired_post as _paired_post
10
+ from . import readtime_alias as _readtime_alias
11
+
12
+ # Runner signature: (obj|None, ctx) -> None
13
+ RunFn = Callable[[Optional[object], Any], None]
14
+
15
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
16
+ #: Keys are (domain, subject); values are (anchor, runner).
17
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
18
+ ("emit", "paired_pre"): (_paired_pre.ANCHOR, _paired_pre.run),
19
+ ("emit", "paired_post"): (_paired_post.ANCHOR, _paired_post.run),
20
+ ("emit", "readtime_alias"): (_readtime_alias.ANCHOR, _readtime_alias.run),
21
+ }
22
+
23
+ logger = logging.getLogger("uvicorn")
24
+
25
+
26
+ def subjects() -> Tuple[str, ...]:
27
+ """Return the subject names exported by this domain."""
28
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
29
+ logger.debug("Listing 'emit' subjects: %s", subjects)
30
+ return subjects
31
+
32
+
33
+ def get(subject: str) -> Tuple[str, RunFn]:
34
+ """Return (anchor, runner) for a subject in the 'emit' domain."""
35
+ key = ("emit", subject)
36
+ if key not in REGISTRY:
37
+ raise KeyError(f"Unknown emit atom subject: {subject!r}")
38
+ logger.debug("Retrieving 'emit' subject %s", subject)
39
+ return REGISTRY[key]
40
+
41
+
42
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]