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,153 @@
1
+ from __future__ import annotations
2
+ import inspect
3
+ import logging
4
+
5
+ from types import SimpleNamespace
6
+ from typing import Any, Dict, Optional, Sequence, Tuple
7
+
8
+
9
+ from fastapi.security import HTTPBearer
10
+ from .fastapi import Depends, HTTPException, Request, Security, _status
11
+ from ...op import OpSpec
12
+ from ...op.types import CANON
13
+
14
+ logger = logging.getLogger("uvicorn")
15
+ logger.debug("Loaded module v3/bindings/rest/routing")
16
+
17
+
18
+ def _normalize_deps(deps: Optional[Sequence[Any]]) -> list[Any]:
19
+ """Turn callables into Depends(...) unless already a dependency object."""
20
+ if not deps:
21
+ return []
22
+ out: list[Any] = []
23
+ for d in deps:
24
+ is_dep_obj = getattr(d, "dependency", None) is not None
25
+ out.append(d if is_dep_obj else Depends(d))
26
+ return out
27
+
28
+
29
+ def _normalize_secdeps(secdeps: Optional[Sequence[Any]]) -> list[Any]:
30
+ """Turn callables into Security(...) unless already a dependency object."""
31
+ if not secdeps:
32
+ return []
33
+ out: list[Any] = []
34
+ for d in secdeps:
35
+ is_dep_obj = getattr(d, "dependency", None) is not None
36
+ out.append(d if is_dep_obj else Security(d))
37
+ return out
38
+
39
+
40
+ def _requires_auth_header(auth_dep: Any) -> bool:
41
+ try:
42
+ sig = inspect.signature(auth_dep)
43
+ except (TypeError, ValueError):
44
+ return False
45
+ for param in sig.parameters.values():
46
+ default = param.default
47
+ dep = getattr(default, "dependency", None)
48
+ if isinstance(dep, HTTPBearer) and getattr(dep, "auto_error", True):
49
+ return True
50
+ return False
51
+
52
+
53
+ def _require_auth_header(request: Request) -> None:
54
+ if not request.headers.get("Authorization"):
55
+ raise HTTPException(status_code=_status.HTTP_403_FORBIDDEN, detail="Forbidden")
56
+
57
+
58
+ def _status_for(sp: OpSpec) -> int:
59
+ if sp.status_code is not None:
60
+ return sp.status_code
61
+ target = sp.target
62
+ if target == "create":
63
+ return _status.HTTP_201_CREATED
64
+ if target in ("delete", "clear"):
65
+ return _status.HTTP_200_OK
66
+ return _status.HTTP_200_OK
67
+
68
+
69
+ _RESPONSES_META = {
70
+ 400: {"description": "Bad Request"},
71
+ 401: {"description": "Unauthorized"},
72
+ 403: {"description": "Forbidden"},
73
+ 404: {"description": "Not Found"},
74
+ 409: {"description": "Conflict"},
75
+ 422: {"description": "Unprocessable Entity"},
76
+ 429: {"description": "Too Many Requests"},
77
+ 500: {"description": "Internal Server Error"},
78
+ }
79
+
80
+
81
+ _DEFAULT_METHODS: Dict[str, Tuple[str, ...]] = {
82
+ "create": ("POST",),
83
+ "read": ("GET",),
84
+ "update": ("PATCH",),
85
+ "replace": ("PUT",),
86
+ "merge": ("PATCH",),
87
+ "delete": ("DELETE",),
88
+ "list": ("GET",),
89
+ "clear": ("DELETE",),
90
+ "bulk_create": ("POST",),
91
+ "bulk_update": ("PATCH",),
92
+ "bulk_replace": ("PUT",),
93
+ "bulk_merge": ("PATCH",),
94
+ "bulk_delete": ("DELETE",),
95
+ "custom": ("POST",),
96
+ }
97
+
98
+
99
+ def _default_path_suffix(sp: OpSpec) -> str | None:
100
+ if sp.target.startswith("bulk_"):
101
+ return None
102
+ if sp.alias != sp.target and (
103
+ sp.target in {"create", "custom"} or sp.target not in CANON
104
+ ):
105
+ return f"/{sp.alias}"
106
+ return None
107
+
108
+
109
+ def _path_for_spec(
110
+ model: type, sp: OpSpec, *, resource: str, pk_param: str = "item_id"
111
+ ) -> Tuple[str, bool]:
112
+ if sp.path_suffix is None:
113
+ suffix = _default_path_suffix(sp) or ""
114
+ else:
115
+ suffix = sp.path_suffix or ""
116
+ if suffix and not suffix.startswith("/"):
117
+ suffix = "/" + suffix
118
+
119
+ if sp.target == "create":
120
+ return f"/{resource}{suffix}", False
121
+ if sp.arity == "member" or sp.target in {
122
+ "read",
123
+ "update",
124
+ "replace",
125
+ "merge",
126
+ "delete",
127
+ }:
128
+ return f"/{resource}/{{{pk_param}}}{suffix}", True
129
+ return f"/{resource}{suffix}", False
130
+
131
+
132
+ def _response_model_for(sp: OpSpec, model: type) -> Any | None:
133
+ if sp.target == "delete":
134
+ return None
135
+ alias_ns = getattr(
136
+ getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
137
+ )
138
+ out_model = getattr(alias_ns, "out", None)
139
+ if out_model is None:
140
+ return None
141
+ if sp.target == "list":
142
+ try:
143
+ return list[out_model] # type: ignore[index]
144
+ except Exception:
145
+ return None
146
+ return out_model
147
+
148
+
149
+ def _request_model_for(sp: OpSpec, model: type) -> Any | None:
150
+ alias_ns = getattr(
151
+ getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
152
+ )
153
+ return getattr(alias_ns, "in_", None)
tigrbl/bindings/rpc.py ADDED
@@ -0,0 +1,364 @@
1
+ # tigrbl/v3/bindings/rpc.py
2
+ from __future__ import annotations
3
+
4
+ import inspect
5
+ import logging
6
+ from types import SimpleNamespace
7
+ from typing import (
8
+ Any,
9
+ Awaitable,
10
+ Callable,
11
+ Dict,
12
+ Mapping,
13
+ Optional,
14
+ Sequence,
15
+ Tuple,
16
+ )
17
+
18
+ from pydantic import BaseModel
19
+
20
+ from ..op import OpSpec
21
+ from ..op.types import PHASES
22
+ from ..runtime import executor as _executor # expects _invoke(request, db, phases, ctx)
23
+
24
+ # Prefer Kernel phase-chains if available (atoms + system steps + hooks)
25
+ try:
26
+ from ..runtime.kernel import build_phase_chains as _kernel_build_phase_chains # type: ignore
27
+ except Exception: # pragma: no cover
28
+ _kernel_build_phase_chains = None # type: ignore
29
+
30
+ logger = logging.getLogger("uvicorn")
31
+ logger.debug("Loaded module v3/bindings/rpc")
32
+
33
+ _Key = Tuple[str, str] # (alias, target)
34
+
35
+
36
+ # Mapping with attribute-style access
37
+ class AttrDict(dict):
38
+ def __getattr__(self, item: str) -> Any: # pragma: no cover - trivial
39
+ try:
40
+ return self[item]
41
+ except KeyError as e: # pragma: no cover - debug helper
42
+ raise AttributeError(item) from e
43
+
44
+
45
+ # ───────────────────────────────────────────────────────────────────────────────
46
+ # Helpers
47
+ # ───────────────────────────────────────────────────────────────────────────────
48
+
49
+
50
+ def _ns(obj: Any, name: str) -> Any:
51
+ ns = getattr(obj, name, None)
52
+ if ns is None:
53
+ ns = SimpleNamespace()
54
+ setattr(obj, name, ns)
55
+ return ns
56
+
57
+
58
+ def _get_phase_chains(
59
+ model: type, alias: str
60
+ ) -> Dict[str, Sequence[Callable[..., Awaitable[Any]]]]:
61
+ """
62
+ Prefer building via runtime Kernel (atoms + system steps + hooks in one lifecycle).
63
+ Fallback: read the pre-built model.hooks.<alias> chains directly.
64
+ """
65
+ if _kernel_build_phase_chains is not None:
66
+ try:
67
+ return _kernel_build_phase_chains(model, alias)
68
+ except Exception:
69
+ logger.exception(
70
+ "Kernel build_phase_chains failed for %s.%s; falling back to hooks",
71
+ getattr(model, "__name__", model),
72
+ alias,
73
+ )
74
+ hooks_root = _ns(model, "hooks")
75
+ alias_ns = getattr(hooks_root, alias, None)
76
+ out: Dict[str, Sequence[Callable[..., Awaitable[Any]]]] = {}
77
+ for ph in PHASES:
78
+ out[ph] = list(getattr(alias_ns, ph, []) or [])
79
+ return out
80
+
81
+
82
+ def _coerce_payload(payload: Any) -> Any:
83
+ """Normalize common payload shapes.
84
+
85
+ ``dict``-like and Pydantic models become plain ``dict``s. ``None`` becomes an
86
+ empty ``dict``. Sequence payloads (used by bulk operations) pass through as
87
+ lists of ``dict``s when possible; otherwise the original sequence is
88
+ returned. Any other type yields an empty ``dict``.
89
+ """
90
+ if payload is None:
91
+ return {}
92
+ if isinstance(payload, BaseModel):
93
+ try:
94
+ return payload.model_dump(exclude_none=False)
95
+ except Exception:
96
+ return dict(payload.__dict__)
97
+ if isinstance(payload, Mapping):
98
+ return dict(payload)
99
+ if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes)):
100
+ out: list[Any] = []
101
+ for item in payload:
102
+ if isinstance(item, Mapping):
103
+ out.append(dict(item))
104
+ else:
105
+ out.append(item)
106
+ return out
107
+ return {}
108
+
109
+
110
+ def _ensure_jsonable(obj: Any) -> Any:
111
+ """Best-effort conversion of DB rows or ORM objects to primitives."""
112
+ if isinstance(obj, (list, tuple)):
113
+ return [_ensure_jsonable(x) for x in obj]
114
+ if isinstance(obj, Mapping):
115
+ try:
116
+ return AttrDict({k: _ensure_jsonable(v) for k, v in dict(obj).items()})
117
+ except Exception:
118
+ pass
119
+ try:
120
+ data = vars(obj)
121
+ except TypeError:
122
+ return obj
123
+ return AttrDict(
124
+ {k: _ensure_jsonable(v) for k, v in data.items() if not k.startswith("_")}
125
+ )
126
+
127
+
128
+ def _validate_input(
129
+ model: type, alias: str, target: str, payload: Mapping[str, Any]
130
+ ) -> Mapping[str, Any]:
131
+ """Choose the appropriate request schema (if any) and validate/normalize payload."""
132
+ schemas_root = getattr(model, "schemas", None)
133
+ if not schemas_root:
134
+ return payload
135
+ alias_ns = getattr(schemas_root, alias, None)
136
+ if not alias_ns:
137
+ return payload
138
+
139
+ in_model = getattr(alias_ns, "in_", None)
140
+
141
+ if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
142
+ try:
143
+ inst = in_model.model_validate(payload) # type: ignore[arg-type]
144
+ return inst.model_dump(exclude_none=True)
145
+ except Exception as e:
146
+ # Let the executor/runtime error mappers standardize later; pass original payload
147
+ logger.debug(
148
+ "rpc input validation failed for %s.%s: %s",
149
+ model.__name__,
150
+ alias,
151
+ e,
152
+ exc_info=True,
153
+ )
154
+ return payload
155
+
156
+
157
+ def _serialize_output(model: type, alias: str, target: str, result: Any) -> Any:
158
+ """Serialize result(s) if an OUT schema is available for the op.
159
+
160
+ For 'list', the OUT schema represents the element shape.
161
+ """
162
+ schemas_root = getattr(model, "schemas", None)
163
+ if not schemas_root:
164
+ return _ensure_jsonable(result)
165
+ alias_ns = getattr(schemas_root, alias, None)
166
+ if not alias_ns:
167
+ return _ensure_jsonable(result)
168
+
169
+ if target in {"bulk_create", "bulk_update", "bulk_replace", "bulk_merge"}:
170
+ out_model = getattr(alias_ns, "out_item", None)
171
+ else:
172
+ out_model = getattr(alias_ns, "out", None)
173
+
174
+ if (
175
+ not out_model
176
+ or not inspect.isclass(out_model)
177
+ or not issubclass(out_model, BaseModel)
178
+ ):
179
+ return _ensure_jsonable(result)
180
+
181
+ try:
182
+ if target == "list" and isinstance(result, (list, tuple)):
183
+ return [
184
+ out_model.model_validate(x).model_dump(
185
+ exclude_none=False, by_alias=True
186
+ )
187
+ for x in result
188
+ ]
189
+ if target in {
190
+ "bulk_create",
191
+ "bulk_update",
192
+ "bulk_replace",
193
+ "bulk_merge",
194
+ } and isinstance(result, (list, tuple)):
195
+ return [
196
+ out_model.model_validate(x).model_dump(
197
+ exclude_none=False, by_alias=True
198
+ )
199
+ for x in result
200
+ ]
201
+ # Single object case
202
+ return out_model.model_validate(result).model_dump(
203
+ exclude_none=False, by_alias=True
204
+ )
205
+ except Exception as e:
206
+ # If serialization fails, let raw result through rather than failing the call
207
+ logger.debug(
208
+ "rpc output serialization failed for %s.%s: %s",
209
+ model.__name__,
210
+ alias,
211
+ e,
212
+ exc_info=True,
213
+ )
214
+ return _ensure_jsonable(result)
215
+
216
+
217
+ # ───────────────────────────────────────────────────────────────────────────────
218
+ # RPC wrapper builder
219
+ # ───────────────────────────────────────────────────────────────────────────────
220
+
221
+
222
+ def _build_rpc_callable(model: type, sp: OpSpec) -> Callable[..., Awaitable[Any]]:
223
+ """
224
+ Create an async callable that:
225
+ 1) validates payload (if schema present),
226
+ 2) runs the executor with the model's phase chains (Kernel-preferred),
227
+ 3) serializes the result to the expected return form.
228
+
229
+ Signature:
230
+ async def rpc_method(payload: Mapping | BaseModel | None = None, *, db, request=None, ctx=None) -> Any
231
+ """
232
+ alias = sp.alias
233
+ target = sp.target
234
+
235
+ async def _rpc_method(
236
+ payload: Any = None,
237
+ *,
238
+ db: Any,
239
+ request: Any = None,
240
+ ctx: Optional[Dict[str, Any]] = None,
241
+ ) -> Any:
242
+ # 1) normalize + validate input
243
+ schemas_root = getattr(model, "schemas", None)
244
+ alias_ns = getattr(schemas_root, alias, None)
245
+ item_in_model = getattr(alias_ns, "in_item", None)
246
+
247
+ raw_payload = _coerce_payload(payload)
248
+ if target == "bulk_delete" and not isinstance(raw_payload, Mapping):
249
+ raw_payload = {"ids": raw_payload}
250
+ if (
251
+ target.startswith("bulk_")
252
+ and target != "bulk_delete"
253
+ and isinstance(raw_payload, Sequence)
254
+ ):
255
+ merged_payload = []
256
+ for item in raw_payload:
257
+ if item_in_model and isinstance(item, Mapping):
258
+ norm = item_in_model.model_validate(dict(item)).model_dump(
259
+ exclude_none=True
260
+ )
261
+ merged_payload.append({**dict(item), **norm})
262
+ elif item_in_model:
263
+ norm = item_in_model.model_validate(item).model_dump(
264
+ exclude_none=True
265
+ )
266
+ merged_payload.append(norm)
267
+ else:
268
+ merged_payload.append(item)
269
+ else:
270
+ norm_payload = _validate_input(model, alias, target, raw_payload)
271
+ merged_payload = dict(raw_payload)
272
+ for key, value in norm_payload.items():
273
+ merged_payload[key] = value
274
+
275
+ # 2) build executor context & phases
276
+ base_ctx: Dict[str, Any] = dict(ctx or {})
277
+ base_ctx.setdefault("payload", merged_payload)
278
+ base_ctx.setdefault("db", db)
279
+ if request is not None:
280
+ base_ctx.setdefault("request", request)
281
+ # surface contextual metadata for runtime atoms
282
+ app_ref = (
283
+ getattr(request, "app", None)
284
+ or base_ctx.get("app")
285
+ or getattr(model, "api", None)
286
+ or model
287
+ )
288
+ base_ctx.setdefault("app", app_ref)
289
+ base_ctx.setdefault("api", base_ctx.get("api") or app_ref)
290
+ base_ctx.setdefault("model", model)
291
+ base_ctx.setdefault("op", alias)
292
+ base_ctx.setdefault("method", alias)
293
+ base_ctx.setdefault("target", target)
294
+ # helpful env metadata
295
+ base_ctx.setdefault(
296
+ "env",
297
+ SimpleNamespace(
298
+ method=alias, params=merged_payload, target=target, model=model
299
+ ),
300
+ )
301
+
302
+ phases = _get_phase_chains(model, alias)
303
+ # RPC methods should return raw data for JSON-RPC envelopes;
304
+ # remove response rendering atoms (which produce Starlette responses)
305
+ # JSON-RPC endpoints handle rendering at the transport layer. Filter
306
+ # out response rendering atoms but preserve any POST_RESPONSE hooks.
307
+ phases["POST_RESPONSE"] = [
308
+ fn
309
+ for fn in phases.get("POST_RESPONSE", [])
310
+ if not (
311
+ isinstance(getattr(fn, "__tigrbl_label", None), str)
312
+ and getattr(fn, "__tigrbl_label").endswith("@out:dump")
313
+ )
314
+ ]
315
+
316
+ base_ctx["response_serializer"] = lambda r: _serialize_output(
317
+ model, alias, target, r
318
+ )
319
+ # 3) run executor
320
+ result = await _executor._invoke(
321
+ request=request,
322
+ db=db,
323
+ phases=phases,
324
+ ctx=base_ctx,
325
+ )
326
+
327
+ return result
328
+
329
+ # Give the callable a nice name for introspection/logging
330
+ _rpc_method.__name__ = f"rpc_{model.__name__}_{alias}"
331
+ _rpc_method.__qualname__ = _rpc_method.__name__
332
+ _rpc_method.__doc__ = f"RPC method for {model.__name__}.{alias} ({target})"
333
+
334
+ return _rpc_method
335
+
336
+
337
+ def _attach_one(model: type, sp: OpSpec) -> None:
338
+ rpc_root = _ns(model, "rpc")
339
+ fn = _build_rpc_callable(model, sp)
340
+ setattr(rpc_root, sp.alias, fn)
341
+ logger.debug("rpc: %s.%s registered", model.__name__, sp.alias)
342
+
343
+
344
+ # ───────────────────────────────────────────────────────────────────────────────
345
+ # Public API
346
+ # ───────────────────────────────────────────────────────────────────────────────
347
+
348
+
349
+ def register_and_attach(
350
+ model: type, specs: Sequence[OpSpec], *, only_keys: Optional[Sequence[_Key]] = None
351
+ ) -> None:
352
+ """
353
+ Register async callables under `model.rpc.<alias>` for each OpSpec.
354
+ If `only_keys` is provided, limit work to those (alias,target) pairs.
355
+ """
356
+ wanted = set(only_keys or ())
357
+ for sp in specs:
358
+ key = (sp.alias, sp.target)
359
+ if wanted and key not in wanted:
360
+ continue
361
+ _attach_one(model, sp)
362
+
363
+
364
+ __all__ = ["register_and_attach"]
@@ -0,0 +1,11 @@
1
+ # tigrbl/v3/bindings/schemas/__init__.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ from .builder import build_and_attach
6
+
7
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.debug("Loaded module v3/bindings/schemas/__init__")
10
+
11
+ __all__ = ["build_and_attach"]