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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. tigrbl/README.md +94 -0
  2. tigrbl/__init__.py +139 -14
  3. tigrbl/api/__init__.py +6 -0
  4. tigrbl/api/_api.py +72 -0
  5. tigrbl/api/api_spec.py +30 -0
  6. tigrbl/api/mro_collect.py +43 -0
  7. tigrbl/api/shortcuts.py +56 -0
  8. tigrbl/api/tigrbl_api.py +286 -0
  9. tigrbl/app/__init__.py +0 -0
  10. tigrbl/app/_app.py +61 -0
  11. tigrbl/app/app_spec.py +42 -0
  12. tigrbl/app/mro_collect.py +67 -0
  13. tigrbl/app/shortcuts.py +65 -0
  14. tigrbl/app/tigrbl_app.py +314 -0
  15. tigrbl/bindings/__init__.py +73 -0
  16. tigrbl/bindings/api/__init__.py +12 -0
  17. tigrbl/bindings/api/common.py +109 -0
  18. tigrbl/bindings/api/include.py +256 -0
  19. tigrbl/bindings/api/resource_proxy.py +149 -0
  20. tigrbl/bindings/api/rpc.py +111 -0
  21. tigrbl/bindings/columns.py +49 -0
  22. tigrbl/bindings/handlers/__init__.py +11 -0
  23. tigrbl/bindings/handlers/builder.py +119 -0
  24. tigrbl/bindings/handlers/ctx.py +74 -0
  25. tigrbl/bindings/handlers/identifiers.py +228 -0
  26. tigrbl/bindings/handlers/namespaces.py +51 -0
  27. tigrbl/bindings/handlers/steps.py +276 -0
  28. tigrbl/bindings/hooks.py +311 -0
  29. tigrbl/bindings/model.py +194 -0
  30. tigrbl/bindings/model_helpers.py +139 -0
  31. tigrbl/bindings/model_registry.py +77 -0
  32. tigrbl/bindings/rest/__init__.py +7 -0
  33. tigrbl/bindings/rest/attach.py +34 -0
  34. tigrbl/bindings/rest/collection.py +265 -0
  35. tigrbl/bindings/rest/common.py +116 -0
  36. tigrbl/bindings/rest/fastapi.py +76 -0
  37. tigrbl/bindings/rest/helpers.py +119 -0
  38. tigrbl/bindings/rest/io.py +317 -0
  39. tigrbl/bindings/rest/member.py +367 -0
  40. tigrbl/bindings/rest/router.py +292 -0
  41. tigrbl/bindings/rest/routing.py +133 -0
  42. tigrbl/bindings/rpc.py +364 -0
  43. tigrbl/bindings/schemas/__init__.py +11 -0
  44. tigrbl/bindings/schemas/builder.py +348 -0
  45. tigrbl/bindings/schemas/defaults.py +260 -0
  46. tigrbl/bindings/schemas/utils.py +193 -0
  47. tigrbl/column/README.md +62 -0
  48. tigrbl/column/__init__.py +72 -0
  49. tigrbl/column/_column.py +96 -0
  50. tigrbl/column/column_spec.py +40 -0
  51. tigrbl/column/field_spec.py +31 -0
  52. tigrbl/column/infer/__init__.py +25 -0
  53. tigrbl/column/infer/core.py +92 -0
  54. tigrbl/column/infer/jsonhints.py +44 -0
  55. tigrbl/column/infer/planning.py +133 -0
  56. tigrbl/column/infer/types.py +102 -0
  57. tigrbl/column/infer/utils.py +59 -0
  58. tigrbl/column/io_spec.py +133 -0
  59. tigrbl/column/mro_collect.py +59 -0
  60. tigrbl/column/shortcuts.py +89 -0
  61. tigrbl/column/storage_spec.py +65 -0
  62. tigrbl/config/__init__.py +19 -0
  63. tigrbl/config/constants.py +224 -0
  64. tigrbl/config/defaults.py +29 -0
  65. tigrbl/config/resolver.py +295 -0
  66. tigrbl/core/__init__.py +47 -0
  67. tigrbl/core/crud/__init__.py +36 -0
  68. tigrbl/core/crud/bulk.py +168 -0
  69. tigrbl/core/crud/helpers/__init__.py +76 -0
  70. tigrbl/core/crud/helpers/db.py +92 -0
  71. tigrbl/core/crud/helpers/enum.py +86 -0
  72. tigrbl/core/crud/helpers/filters.py +162 -0
  73. tigrbl/core/crud/helpers/model.py +123 -0
  74. tigrbl/core/crud/helpers/normalize.py +99 -0
  75. tigrbl/core/crud/ops.py +235 -0
  76. tigrbl/ddl/__init__.py +344 -0
  77. tigrbl/decorators.py +17 -0
  78. tigrbl/deps/__init__.py +20 -0
  79. tigrbl/deps/fastapi.py +45 -0
  80. tigrbl/deps/favicon.svg +4 -0
  81. tigrbl/deps/jinja.py +27 -0
  82. tigrbl/deps/pydantic.py +10 -0
  83. tigrbl/deps/sqlalchemy.py +94 -0
  84. tigrbl/deps/starlette.py +36 -0
  85. tigrbl/engine/__init__.py +26 -0
  86. tigrbl/engine/_engine.py +130 -0
  87. tigrbl/engine/bind.py +33 -0
  88. tigrbl/engine/builders.py +236 -0
  89. tigrbl/engine/collect.py +111 -0
  90. tigrbl/engine/decorators.py +108 -0
  91. tigrbl/engine/engine_spec.py +261 -0
  92. tigrbl/engine/resolver.py +224 -0
  93. tigrbl/engine/shortcuts.py +216 -0
  94. tigrbl/hook/__init__.py +21 -0
  95. tigrbl/hook/_hook.py +22 -0
  96. tigrbl/hook/decorators.py +28 -0
  97. tigrbl/hook/hook_spec.py +24 -0
  98. tigrbl/hook/mro_collect.py +98 -0
  99. tigrbl/hook/shortcuts.py +44 -0
  100. tigrbl/hook/types.py +76 -0
  101. tigrbl/op/__init__.py +50 -0
  102. tigrbl/op/_op.py +31 -0
  103. tigrbl/op/canonical.py +31 -0
  104. tigrbl/op/collect.py +11 -0
  105. tigrbl/op/decorators.py +238 -0
  106. tigrbl/op/model_registry.py +301 -0
  107. tigrbl/op/mro_collect.py +99 -0
  108. tigrbl/op/resolver.py +216 -0
  109. tigrbl/op/types.py +136 -0
  110. tigrbl/orm/__init__.py +1 -0
  111. tigrbl/orm/mixins/_RowBound.py +83 -0
  112. tigrbl/orm/mixins/__init__.py +95 -0
  113. tigrbl/orm/mixins/bootstrappable.py +113 -0
  114. tigrbl/orm/mixins/bound.py +47 -0
  115. tigrbl/orm/mixins/edges.py +40 -0
  116. tigrbl/orm/mixins/fields.py +165 -0
  117. tigrbl/orm/mixins/hierarchy.py +54 -0
  118. tigrbl/orm/mixins/key_digest.py +44 -0
  119. tigrbl/orm/mixins/lifecycle.py +115 -0
  120. tigrbl/orm/mixins/locks.py +51 -0
  121. tigrbl/orm/mixins/markers.py +16 -0
  122. tigrbl/orm/mixins/operations.py +57 -0
  123. tigrbl/orm/mixins/ownable.py +337 -0
  124. tigrbl/orm/mixins/principals.py +98 -0
  125. tigrbl/orm/mixins/tenant_bound.py +301 -0
  126. tigrbl/orm/mixins/upsertable.py +111 -0
  127. tigrbl/orm/mixins/utils.py +49 -0
  128. tigrbl/orm/tables/__init__.py +72 -0
  129. tigrbl/orm/tables/_base.py +8 -0
  130. tigrbl/orm/tables/audit.py +56 -0
  131. tigrbl/orm/tables/client.py +25 -0
  132. tigrbl/orm/tables/group.py +29 -0
  133. tigrbl/orm/tables/org.py +30 -0
  134. tigrbl/orm/tables/rbac.py +76 -0
  135. tigrbl/orm/tables/status.py +106 -0
  136. tigrbl/orm/tables/tenant.py +22 -0
  137. tigrbl/orm/tables/user.py +39 -0
  138. tigrbl/response/README.md +34 -0
  139. tigrbl/response/__init__.py +33 -0
  140. tigrbl/response/bind.py +12 -0
  141. tigrbl/response/decorators.py +37 -0
  142. tigrbl/response/resolver.py +83 -0
  143. tigrbl/response/shortcuts.py +144 -0
  144. tigrbl/response/types.py +49 -0
  145. tigrbl/rest/__init__.py +27 -0
  146. tigrbl/runtime/README.md +129 -0
  147. tigrbl/runtime/__init__.py +20 -0
  148. tigrbl/runtime/atoms/__init__.py +102 -0
  149. tigrbl/runtime/atoms/emit/__init__.py +42 -0
  150. tigrbl/runtime/atoms/emit/paired_post.py +158 -0
  151. tigrbl/runtime/atoms/emit/paired_pre.py +106 -0
  152. tigrbl/runtime/atoms/emit/readtime_alias.py +120 -0
  153. tigrbl/runtime/atoms/out/__init__.py +38 -0
  154. tigrbl/runtime/atoms/out/masking.py +135 -0
  155. tigrbl/runtime/atoms/refresh/__init__.py +38 -0
  156. tigrbl/runtime/atoms/refresh/demand.py +130 -0
  157. tigrbl/runtime/atoms/resolve/__init__.py +40 -0
  158. tigrbl/runtime/atoms/resolve/assemble.py +167 -0
  159. tigrbl/runtime/atoms/resolve/paired_gen.py +147 -0
  160. tigrbl/runtime/atoms/response/__init__.py +17 -0
  161. tigrbl/runtime/atoms/response/negotiate.py +30 -0
  162. tigrbl/runtime/atoms/response/negotiation.py +43 -0
  163. tigrbl/runtime/atoms/response/render.py +36 -0
  164. tigrbl/runtime/atoms/response/renderer.py +116 -0
  165. tigrbl/runtime/atoms/response/template.py +44 -0
  166. tigrbl/runtime/atoms/response/templates.py +88 -0
  167. tigrbl/runtime/atoms/schema/__init__.py +40 -0
  168. tigrbl/runtime/atoms/schema/collect_in.py +21 -0
  169. tigrbl/runtime/atoms/schema/collect_out.py +21 -0
  170. tigrbl/runtime/atoms/storage/__init__.py +38 -0
  171. tigrbl/runtime/atoms/storage/to_stored.py +167 -0
  172. tigrbl/runtime/atoms/wire/__init__.py +45 -0
  173. tigrbl/runtime/atoms/wire/build_in.py +166 -0
  174. tigrbl/runtime/atoms/wire/build_out.py +87 -0
  175. tigrbl/runtime/atoms/wire/dump.py +206 -0
  176. tigrbl/runtime/atoms/wire/validate_in.py +227 -0
  177. tigrbl/runtime/context.py +206 -0
  178. tigrbl/runtime/errors/__init__.py +61 -0
  179. tigrbl/runtime/errors/converters.py +214 -0
  180. tigrbl/runtime/errors/exceptions.py +124 -0
  181. tigrbl/runtime/errors/mappings.py +71 -0
  182. tigrbl/runtime/errors/utils.py +150 -0
  183. tigrbl/runtime/events.py +209 -0
  184. tigrbl/runtime/executor/__init__.py +6 -0
  185. tigrbl/runtime/executor/guards.py +132 -0
  186. tigrbl/runtime/executor/helpers.py +88 -0
  187. tigrbl/runtime/executor/invoke.py +150 -0
  188. tigrbl/runtime/executor/types.py +84 -0
  189. tigrbl/runtime/kernel.py +628 -0
  190. tigrbl/runtime/labels.py +353 -0
  191. tigrbl/runtime/opview.py +87 -0
  192. tigrbl/runtime/ordering.py +256 -0
  193. tigrbl/runtime/system.py +279 -0
  194. tigrbl/runtime/trace.py +330 -0
  195. tigrbl/schema/__init__.py +38 -0
  196. tigrbl/schema/_schema.py +27 -0
  197. tigrbl/schema/builder/__init__.py +17 -0
  198. tigrbl/schema/builder/build_schema.py +209 -0
  199. tigrbl/schema/builder/cache.py +24 -0
  200. tigrbl/schema/builder/compat.py +16 -0
  201. tigrbl/schema/builder/extras.py +85 -0
  202. tigrbl/schema/builder/helpers.py +51 -0
  203. tigrbl/schema/builder/list_params.py +117 -0
  204. tigrbl/schema/builder/strip_parent_fields.py +70 -0
  205. tigrbl/schema/collect.py +55 -0
  206. tigrbl/schema/decorators.py +68 -0
  207. tigrbl/schema/get_schema.py +86 -0
  208. tigrbl/schema/schema_spec.py +20 -0
  209. tigrbl/schema/shortcuts.py +42 -0
  210. tigrbl/schema/types.py +34 -0
  211. tigrbl/schema/utils.py +143 -0
  212. tigrbl/shortcuts.py +22 -0
  213. tigrbl/specs.py +44 -0
  214. tigrbl/system/__init__.py +12 -0
  215. tigrbl/system/diagnostics/__init__.py +24 -0
  216. tigrbl/system/diagnostics/compat.py +31 -0
  217. tigrbl/system/diagnostics/healthz.py +41 -0
  218. tigrbl/system/diagnostics/hookz.py +51 -0
  219. tigrbl/system/diagnostics/kernelz.py +20 -0
  220. tigrbl/system/diagnostics/methodz.py +43 -0
  221. tigrbl/system/diagnostics/router.py +73 -0
  222. tigrbl/system/diagnostics/utils.py +43 -0
  223. tigrbl/table/__init__.py +9 -0
  224. tigrbl/table/_base.py +237 -0
  225. tigrbl/table/_table.py +54 -0
  226. tigrbl/table/mro_collect.py +69 -0
  227. tigrbl/table/shortcuts.py +57 -0
  228. tigrbl/table/table_spec.py +28 -0
  229. tigrbl/transport/__init__.py +74 -0
  230. tigrbl/transport/jsonrpc/__init__.py +19 -0
  231. tigrbl/transport/jsonrpc/dispatcher.py +352 -0
  232. tigrbl/transport/jsonrpc/helpers.py +115 -0
  233. tigrbl/transport/jsonrpc/models.py +41 -0
  234. tigrbl/transport/rest/__init__.py +25 -0
  235. tigrbl/transport/rest/aggregator.py +132 -0
  236. tigrbl/types/__init__.py +174 -0
  237. tigrbl/types/allow_anon_provider.py +19 -0
  238. tigrbl/types/authn_abc.py +30 -0
  239. tigrbl/types/nested_path_provider.py +22 -0
  240. tigrbl/types/op.py +35 -0
  241. tigrbl/types/op_config_provider.py +17 -0
  242. tigrbl/types/op_verb_alias_provider.py +33 -0
  243. tigrbl/types/request_extras_provider.py +22 -0
  244. tigrbl/types/response_extras_provider.py +22 -0
  245. tigrbl/types/table_config_provider.py +13 -0
  246. tigrbl-0.3.0.dev3.dist-info/LICENSE +201 -0
  247. tigrbl-0.3.0.dev3.dist-info/METADATA +501 -0
  248. tigrbl-0.3.0.dev3.dist-info/RECORD +249 -0
  249. tigrbl/ExampleAgent.py +0 -1
  250. tigrbl-0.0.1.dev1.dist-info/METADATA +0 -18
  251. tigrbl-0.0.1.dev1.dist-info/RECORD +0 -5
  252. {tigrbl-0.0.1.dev1.dist-info → tigrbl-0.3.0.dev3.dist-info}/WHEEL +0 -0
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"]