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,116 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from pathlib import Path
4
+ from typing import Any, AsyncIterable, Iterable, Mapping, Optional, Union, cast
5
+ import logging
6
+
7
+ from ....deps.starlette import BackgroundTask, Response
8
+
9
+ from ....response.shortcuts import (
10
+ as_file,
11
+ as_html,
12
+ as_json,
13
+ as_stream,
14
+ as_text,
15
+ )
16
+
17
+ JSON = Mapping[str, Any]
18
+
19
+ logger = logging.getLogger("uvicorn")
20
+
21
+
22
+ @dataclass
23
+ class ResponseHints:
24
+ media_type: Optional[str] = None
25
+ status_code: int = 200
26
+ headers: dict[str, str] = field(default_factory=dict)
27
+ filename: Optional[str] = None
28
+ download: bool = False
29
+ etag: Optional[str] = None
30
+ last_modified: Optional[Any] = None
31
+ background: Optional[BackgroundTask] = None
32
+
33
+
34
+ class ResponseKind:
35
+ JSON = "application/json"
36
+ HTML = "text/html"
37
+ TEXT = "text/plain"
38
+ FILE = "application/file"
39
+ STREAM = "application/stream"
40
+ REDIRECT = "application/redirect"
41
+
42
+
43
+ ResponseLike = Union[
44
+ Response,
45
+ bytes,
46
+ bytearray,
47
+ memoryview,
48
+ str,
49
+ Path,
50
+ JSON,
51
+ Iterable[bytes],
52
+ AsyncIterable[bytes],
53
+ ]
54
+
55
+
56
+ def render(
57
+ request: Any,
58
+ payload: ResponseLike,
59
+ *,
60
+ hints: Optional[ResponseHints] = None,
61
+ default_media: str = "application/json",
62
+ envelope_default: bool = False,
63
+ ) -> Response:
64
+ logger.debug("Rendering response with payload type %s", type(payload))
65
+ if isinstance(payload, Response):
66
+ return payload
67
+
68
+ hints = hints or ResponseHints()
69
+ chosen = hints.media_type or default_media
70
+
71
+ if isinstance(payload, Path):
72
+ return as_file(
73
+ payload,
74
+ filename=hints.filename,
75
+ download=hints.download,
76
+ status=hints.status_code,
77
+ headers=hints.headers,
78
+ )
79
+
80
+ if isinstance(payload, (bytes, bytearray, memoryview)):
81
+ return as_stream(
82
+ iter((bytes(payload),)),
83
+ media_type="application/octet-stream",
84
+ status=hints.status_code,
85
+ headers=hints.headers,
86
+ )
87
+
88
+ if hasattr(payload, "__aiter__") or (
89
+ hasattr(payload, "__iter__") and not isinstance(payload, (str, dict, list))
90
+ ):
91
+ return as_stream(
92
+ cast(Union[Iterable[bytes], AsyncIterable[bytes]], payload),
93
+ media_type="application/octet-stream",
94
+ status=hints.status_code,
95
+ headers=hints.headers,
96
+ )
97
+
98
+ if isinstance(payload, str):
99
+ if payload.lstrip().startswith("<") or chosen == "text/html":
100
+ return as_html(payload, status=hints.status_code, headers=hints.headers)
101
+ return as_text(payload, status=hints.status_code, headers=hints.headers)
102
+
103
+ return as_json(
104
+ payload,
105
+ status=hints.status_code,
106
+ headers=hints.headers,
107
+ envelope=envelope_default,
108
+ )
109
+
110
+
111
+ __all__ = [
112
+ "ResponseHints",
113
+ "ResponseKind",
114
+ "ResponseLike",
115
+ "render",
116
+ ]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+ from typing import Any, Optional
3
+
4
+ from ... import events as _ev
5
+ from .templates import render_template
6
+ from .renderer import ResponseHints
7
+
8
+ ANCHOR = _ev.OUT_DUMP # "out:dump"
9
+
10
+
11
+ async def run(obj: Optional[object], ctx: Any) -> None:
12
+ """response:template@out:dump
13
+
14
+ Render a template if configured on ``ctx.response``.
15
+ """
16
+ resp_ns = getattr(ctx, "response", None)
17
+ req = getattr(ctx, "request", None)
18
+ if resp_ns is None or req is None:
19
+ return
20
+ tmpl = getattr(resp_ns, "template", None)
21
+ if not tmpl:
22
+ return
23
+ result = getattr(resp_ns, "result", None)
24
+ context = result if isinstance(result, dict) else {"data": result}
25
+ html = await render_template(
26
+ name=tmpl.name,
27
+ context=context,
28
+ search_paths=tmpl.search_paths,
29
+ package=tmpl.package,
30
+ auto_reload=bool(tmpl.auto_reload),
31
+ filters=tmpl.filters,
32
+ globals_=tmpl.globals,
33
+ request=req,
34
+ )
35
+ resp_ns.result = html
36
+ hints = getattr(resp_ns, "hints", None)
37
+ if hints is None:
38
+ hints = ResponseHints()
39
+ resp_ns.hints = hints
40
+ if not hints.media_type:
41
+ hints.media_type = "text/html"
42
+
43
+
44
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+ from functools import lru_cache
3
+ import logging
4
+ from typing import Any, Dict, Iterable, Optional, Tuple
5
+
6
+ from ....deps.starlette import Request
7
+ from ....deps.jinja import (
8
+ Environment,
9
+ FileSystemLoader,
10
+ PackageLoader,
11
+ ChoiceLoader,
12
+ select_autoescape,
13
+ TemplateNotFound,
14
+ )
15
+
16
+ logger = logging.getLogger("uvicorn")
17
+ if Environment is None: # pragma: no cover - jinja2 not installed
18
+
19
+ async def render_template(
20
+ *,
21
+ name: str,
22
+ context: Dict[str, Any],
23
+ search_paths: Iterable[str] = (),
24
+ package: Optional[str] = None,
25
+ auto_reload: bool = False,
26
+ filters: Optional[Dict[str, Any]] = None,
27
+ globals_: Optional[Dict[str, Any]] = None,
28
+ request: Optional[Request] = None,
29
+ ) -> str:
30
+ logger.debug("Rendering template %s", name)
31
+ raise RuntimeError("jinja2 is required for template rendering")
32
+
33
+ else:
34
+
35
+ def _mk_loader(search_paths: Iterable[str], package: Optional[str]) -> ChoiceLoader:
36
+ loaders = []
37
+ if search_paths:
38
+ loaders.append(FileSystemLoader(list(search_paths)))
39
+ if package:
40
+ loaders.append(PackageLoader(package_name=package))
41
+ if not loaders:
42
+ loaders.append(FileSystemLoader(["."]))
43
+ return ChoiceLoader(loaders)
44
+
45
+ @lru_cache(maxsize=64)
46
+ def _get_env(
47
+ search_paths_key: Tuple[str, ...],
48
+ package: Optional[str],
49
+ auto_reload: bool,
50
+ ) -> Environment:
51
+ env = Environment(
52
+ loader=_mk_loader(search_paths_key, package),
53
+ autoescape=select_autoescape(["html", "xml"]),
54
+ auto_reload=auto_reload,
55
+ enable_async=True,
56
+ )
57
+ return env
58
+
59
+ async def render_template(
60
+ *,
61
+ name: str,
62
+ context: Dict[str, Any],
63
+ search_paths: Iterable[str] = (),
64
+ package: Optional[str] = None,
65
+ auto_reload: bool = False,
66
+ filters: Optional[Dict[str, Any]] = None,
67
+ globals_: Optional[Dict[str, Any]] = None,
68
+ request: Optional[Request] = None,
69
+ ) -> str:
70
+ logger.debug("Rendering template %s", name)
71
+ env = _get_env(tuple(search_paths), package, auto_reload)
72
+ if filters:
73
+ env.filters.update(filters)
74
+ if globals_:
75
+ env.globals.update(globals_)
76
+ if request is not None:
77
+ env.globals.setdefault("url_for", request.url_for)
78
+ env.globals.setdefault("request", request)
79
+
80
+ try:
81
+ tmpl = env.get_template(name)
82
+ except TemplateNotFound as e: # pragma: no cover - passthrough
83
+ raise FileNotFoundError(f"Template not found: {name}") from e
84
+
85
+ return await tmpl.render_async(**context)
86
+
87
+
88
+ __all__ = ["render_template"]
@@ -0,0 +1,40 @@
1
+ # tigrbl/v3/runtime/atoms/schema/__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 collect_in as _collect_in
9
+ from . import collect_out as _collect_out
10
+
11
+ # Runner signature: (obj|None, ctx) -> None
12
+ RunFn = Callable[[Optional[object], Any], None]
13
+
14
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
15
+ #: Keys are (domain, subject); values are (anchor, runner).
16
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
17
+ ("schema", "collect_in"): (_collect_in.ANCHOR, _collect_in.run),
18
+ ("schema", "collect_out"): (_collect_out.ANCHOR, _collect_out.run),
19
+ }
20
+
21
+ logger = logging.getLogger("uvicorn")
22
+
23
+
24
+ def subjects() -> Tuple[str, ...]:
25
+ """Return the subject names exported by this domain."""
26
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
27
+ logger.debug("Listing 'schema' subjects: %s", subjects)
28
+ return subjects
29
+
30
+
31
+ def get(subject: str) -> Tuple[str, RunFn]:
32
+ """Return (anchor, runner) for a subject in the 'schema' domain."""
33
+ key = ("schema", subject)
34
+ if key not in REGISTRY:
35
+ raise KeyError(f"Unknown schema atom subject: {subject!r}")
36
+ logger.debug("Retrieving 'schema' subject %s", subject)
37
+ return REGISTRY[key]
38
+
39
+
40
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+ import logging
5
+
6
+ from ... import events as _ev
7
+ from ...opview import opview_from_ctx, ensure_schema_in
8
+
9
+ # Runs at the very beginning of the lifecycle, before in-model build/validation.
10
+ ANCHOR = _ev.SCHEMA_COLLECT_IN # "schema:collect_in"
11
+
12
+ logger = logging.getLogger("uvicorn")
13
+
14
+
15
+ def run(obj: Optional[object], ctx: Any) -> None:
16
+ """Load precompiled inbound schema into ctx.temp."""
17
+ ov = opview_from_ctx(ctx)
18
+ ensure_schema_in(ctx, ov)
19
+
20
+
21
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+ import logging
5
+
6
+ from ... import events as _ev
7
+ from ...opview import opview_from_ctx, ensure_schema_out
8
+
9
+ # Runs late in POST_HANDLER, before out model build and dumping.
10
+ ANCHOR = _ev.SCHEMA_COLLECT_OUT # "schema:collect_out"
11
+
12
+ logger = logging.getLogger("uvicorn")
13
+
14
+
15
+ def run(obj: Optional[object], ctx: Any) -> None:
16
+ """Load precompiled outbound schema into ctx.temp."""
17
+ ov = opview_from_ctx(ctx)
18
+ ensure_schema_out(ctx, ov)
19
+
20
+
21
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,38 @@
1
+ # tigrbl/v3/runtime/atoms/storage/__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 to_stored as _to_stored
9
+
10
+ # Runner signature: (obj|None, ctx) -> None
11
+ RunFn = Callable[[Optional[object], Any], None]
12
+
13
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
14
+ #: Keys are (domain, subject); values are (anchor, runner).
15
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
16
+ ("storage", "to_stored"): (_to_stored.ANCHOR, _to_stored.run),
17
+ }
18
+
19
+ logger = logging.getLogger("uvicorn")
20
+
21
+
22
+ def subjects() -> Tuple[str, ...]:
23
+ """Return the subject names exported by this domain."""
24
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
25
+ logger.debug("Listing 'storage' subjects: %s", subjects)
26
+ return subjects
27
+
28
+
29
+ def get(subject: str) -> Tuple[str, RunFn]:
30
+ """Return (anchor, runner) for a subject in the 'storage' domain."""
31
+ key = ("storage", subject)
32
+ if key not in REGISTRY:
33
+ raise KeyError(f"Unknown storage atom subject: {subject!r}")
34
+ logger.debug("Retrieving 'storage' subject %s", subject)
35
+ return REGISTRY[key]
36
+
37
+
38
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]
@@ -0,0 +1,167 @@
1
+ # tigrbl/v3/runtime/atoms/storage/to_stored.py
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ from typing import Any, Dict, Mapping, MutableMapping, Optional
6
+
7
+ from ... import events as _ev
8
+ from ...opview import opview_from_ctx, ensure_schema_in, _ensure_temp
9
+
10
+ # Runs right before the handler flushes to the DB.
11
+ ANCHOR = _ev.PRE_FLUSH # "pre:flush"
12
+
13
+ logger = logging.getLogger("uvicorn")
14
+
15
+
16
+ def run(obj: Optional[object], ctx: Any) -> None:
17
+ """
18
+ storage:to_stored@pre:flush
19
+
20
+ Transform inbound values into their persisted representation.
21
+
22
+ - For *paired/secret-once* columns, derive the stored value from the prepared raw
23
+ and assign it to BOTH ctx.temp["assembled_values"][field] AND the ORM object.
24
+ Also handles a fallback when 'persist_from_paired' wasn't queued but a paired raw
25
+ exists (planner tolerance).
26
+ - Otherwise, apply an optional per-column inbound→stored transform and mirror the
27
+ transformed value onto the ORM instance.
28
+
29
+ On failure to derive for paired, raise ValueError (mapped by higher layers) to
30
+ avoid leaking DB IntegrityErrors.
31
+ """
32
+ logger.debug("Running storage:to_stored")
33
+ if getattr(ctx, "persist", True) is False:
34
+ logger.debug("Skipping storage:to_stored; ctx.persist is False")
35
+ return
36
+
37
+ ov = opview_from_ctx(ctx)
38
+ schema_in = ensure_schema_in(ctx, ov)
39
+ temp = _ensure_temp(ctx)
40
+ assembled = _ensure_dict(temp, "assembled_values")
41
+ paired_values = _ensure_dict(temp, "paired_values")
42
+ pf_paired = _ensure_dict(temp, "persist_from_paired")
43
+ slog = _ensure_list(temp, "storage_log")
44
+ serr = _ensure_list(temp, "storage_errors")
45
+
46
+ # Prefer explicit obj (hydrated instance), else ctx.model if adapter provided it
47
+ target_obj = obj or getattr(ctx, "model", None)
48
+
49
+ # Ensure paired fields are considered even when absent from inbound schema.
50
+ # schema_in["fields"] may omit columns like "digest" that generate their
51
+ # values server-side via IO(...).paired. To derive their stored values, merge
52
+ # the explicit schema fields with the paired index keys.
53
+ all_fields = set(schema_in["fields"]) | set(ov.paired_index.keys())
54
+
55
+ for field in sorted(all_fields):
56
+ if field in ov.paired_index:
57
+ if field in pf_paired or field in paired_values:
58
+ raw = None
59
+ if field in pf_paired:
60
+ raw = _resolve_from_pointer(
61
+ pf_paired[field].get("source"), paired_values, field
62
+ )
63
+ if raw is None:
64
+ raw = paired_values.get(field, {}).get("raw")
65
+ if raw is None:
66
+ serr.append({"field": field, "error": "missing_paired_raw"})
67
+ logger.debug("Missing paired raw for field %s", field)
68
+ raise RuntimeError(f"paired_raw_missing:{field}")
69
+ deriver = ov.paired_index[field].get("store")
70
+ try:
71
+ stored = deriver(raw, ctx) if callable(deriver) else raw
72
+ except Exception as e:
73
+ serr.append(
74
+ {"field": field, "error": f"deriver_failed:{type(e).__name__}"}
75
+ )
76
+ logger.debug("Deriver failed for field %s: %s", field, e)
77
+ raise
78
+ assembled[field] = stored
79
+ _assign_to_model(target_obj, field, stored)
80
+ slog.append({"field": field, "action": "derived_from_paired"})
81
+ logger.debug("Derived stored value for paired field %s", field)
82
+ continue
83
+
84
+ nullable = schema_in["by_field"].get(field, {}).get("nullable", True)
85
+ if (
86
+ not nullable
87
+ and field not in assembled
88
+ and not _has_attr_with_value(target_obj, field)
89
+ ):
90
+ serr.append({"field": field, "error": "paired_missing_before_flush"})
91
+ logger.debug("Paired field %s missing before flush", field)
92
+ raise RuntimeError(f"paired_missing_before_flush:{field}")
93
+ continue
94
+
95
+ if field in assembled:
96
+ transform = ov.to_stored_transforms.get(field)
97
+ if transform is None:
98
+ logger.debug("No transform for field %s; using assembled value", field)
99
+ _assign_to_model(target_obj, field, assembled[field])
100
+ continue
101
+ try:
102
+ stored_val = transform(assembled[field], ctx)
103
+ assembled[field] = stored_val
104
+ _assign_to_model(target_obj, field, stored_val)
105
+ slog.append({"field": field, "action": "transformed"})
106
+ logger.debug("Transformed field %s", field)
107
+ except Exception as e:
108
+ serr.append(
109
+ {"field": field, "error": f"transform_failed:{type(e).__name__}"}
110
+ )
111
+ logger.debug("Transform failed for field %s: %s", field, e)
112
+ raise
113
+
114
+
115
+ # ──────────────────────────────────────────────────────────────────────────────
116
+ # Internals (tolerant to spec shapes)
117
+ # ──────────────────────────────────────────────────────────────────────────────
118
+
119
+
120
+ def _assign_to_model(target: Optional[object], field: str, value: Any) -> None:
121
+ """Safely assign value onto the hydrated ORM object so SQLAlchemy flushes it."""
122
+ if target is None:
123
+ return
124
+ try:
125
+ setattr(target, field, value)
126
+ except Exception:
127
+ # Non-fatal: some adapters may not expose an assignable object here.
128
+ pass
129
+
130
+
131
+ def _has_attr_with_value(target: Optional[object], field: str) -> bool:
132
+ if target is None or not hasattr(target, field):
133
+ return False
134
+ try:
135
+ return getattr(target, field) is not None
136
+ except Exception:
137
+ return False
138
+
139
+
140
+ def _ensure_dict(temp: MutableMapping[str, Any], key: str) -> Dict[str, Any]:
141
+ d = temp.get(key)
142
+ if not isinstance(d, dict):
143
+ d = {}
144
+ temp[key] = d
145
+ return d # type: ignore[return-value]
146
+
147
+
148
+ def _ensure_list(temp: MutableMapping[str, Any], key: str) -> list:
149
+ lst = temp.get(key)
150
+ if not isinstance(lst, list):
151
+ lst = []
152
+ temp[key] = lst
153
+ return lst # type: ignore[return-value]
154
+
155
+
156
+ def _resolve_from_pointer(
157
+ source: Any, pv: Mapping[str, Dict[str, Any]], field: str
158
+ ) -> Optional[Any]:
159
+ """Resolve ('paired_values', field, 'raw') pointer, with fallback to pv[field]['raw']."""
160
+ if isinstance(source, (tuple, list)) and len(source) == 3:
161
+ base, fld, key = source
162
+ if base == "paired_values" and isinstance(fld, str) and key == "raw":
163
+ return pv.get(fld, {}).get("raw")
164
+ return pv.get(field, {}).get("raw")
165
+
166
+
167
+ __all__ = ["ANCHOR", "run"]
@@ -0,0 +1,45 @@
1
+ # tigrbl/v3/runtime/atoms/wire/__init__.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Callable, Dict, Optional, Tuple
5
+ import logging
6
+
7
+ # Atom implementations (per-field)
8
+ from . import build_in as _build_in
9
+ from . import validate_in as _validate_in
10
+ from . import build_out as _build_out
11
+ from . import dump as _dump
12
+
13
+ # Runner signature: (obj|None, ctx) -> None
14
+ RunFn = Callable[[Optional[object], Any], None]
15
+
16
+ #: Domain-scoped registry consumed by the kernel plan (and aggregated at atoms/__init__.py).
17
+ #: Keys are (domain, subject); values are (anchor, runner).
18
+ #: Canonical subjects mirror filenames; we keep "validate_in" (not "validate") to avoid duplicates.
19
+ REGISTRY: Dict[Tuple[str, str], Tuple[str, RunFn]] = {
20
+ ("wire", "build_in"): (_build_in.ANCHOR, _build_in.run),
21
+ ("wire", "validate_in"): (_validate_in.ANCHOR, _validate_in.run),
22
+ ("wire", "build_out"): (_build_out.ANCHOR, _build_out.run),
23
+ ("wire", "dump"): (_dump.ANCHOR, _dump.run),
24
+ }
25
+
26
+ logger = logging.getLogger("uvicorn")
27
+
28
+
29
+ def subjects() -> Tuple[str, ...]:
30
+ """Return the subject names exported by this domain."""
31
+ subjects = tuple(s for (_, s) in REGISTRY.keys())
32
+ logger.debug("Listing 'wire' subjects: %s", subjects)
33
+ return subjects
34
+
35
+
36
+ def get(subject: str) -> Tuple[str, RunFn]:
37
+ """Return (anchor, runner) for a subject in the 'wire' domain."""
38
+ key = ("wire", subject)
39
+ if key not in REGISTRY:
40
+ raise KeyError(f"Unknown wire atom subject: {subject!r}")
41
+ logger.debug("Retrieving 'wire' subject %s", subject)
42
+ return REGISTRY[key]
43
+
44
+
45
+ __all__ = ["REGISTRY", "RunFn", "subjects", "get"]