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
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import logging
5
+ from types import SimpleNamespace
6
+ from typing import (
7
+ Annotated,
8
+ Any,
9
+ Awaitable,
10
+ Callable,
11
+ Dict,
12
+ Mapping,
13
+ Sequence,
14
+ List as _List,
15
+ Union as _Union,
16
+ )
17
+
18
+ from .common import (
19
+ TIGRBL_AUTH_CONTEXT_ATTR,
20
+ BaseModel,
21
+ Body,
22
+ Depends,
23
+ Response,
24
+ OpSpec,
25
+ Path,
26
+ Request,
27
+ _coerce_parent_kw,
28
+ _get_phase_chains,
29
+ _make_list_query_dep,
30
+ _request_model_for,
31
+ _serialize_output,
32
+ _validate_body,
33
+ _validate_query,
34
+ _executor,
35
+ _status_for,
36
+ )
37
+
38
+ from ...runtime.executor.types import _Ctx
39
+
40
+
41
+ logging.getLogger("uvicorn").debug("Loaded module v3/bindings/rest/collection")
42
+
43
+
44
+ def _ctx(model, alias, target, request, db, payload, parent_kw, api):
45
+ ctx: Dict[str, Any] = {
46
+ "request": request,
47
+ "db": db,
48
+ "payload": payload,
49
+ "path_params": parent_kw,
50
+ # expose both API router and FastAPI app; runtime opview resolution
51
+ # relies on the app object, which must be hashable.
52
+ "api": api if api is not None else getattr(request, "app", None),
53
+ "app": getattr(request, "app", None),
54
+ "model": model,
55
+ "op": alias,
56
+ "method": alias,
57
+ "target": target,
58
+ "env": SimpleNamespace(
59
+ method=alias, params=payload, target=target, model=model
60
+ ),
61
+ }
62
+ ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
63
+ if ac is not None:
64
+ ctx["auth_context"] = ac
65
+ return _Ctx(ctx)
66
+
67
+
68
+ def _sig(nested_vars, extra):
69
+ params = [
70
+ inspect.Parameter(
71
+ nv,
72
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
73
+ annotation=Annotated[str, Path(...)],
74
+ )
75
+ for nv in nested_vars
76
+ ]
77
+ params.extend(extra)
78
+ return inspect.Signature(params)
79
+
80
+
81
+ def _list_ann(tp):
82
+ try:
83
+ return list[tp] # type: ignore[valid-type]
84
+ except Exception: # pragma: no cover - best effort
85
+ return _List[tp] # type: ignore[name-defined]
86
+
87
+
88
+ def _union(a, b):
89
+ try:
90
+ return a | b # type: ignore[operator]
91
+ except Exception: # pragma: no cover - best effort
92
+ return _Union[a, b]
93
+
94
+
95
+ def _make_collection_endpoint(
96
+ model: type,
97
+ sp: OpSpec,
98
+ *,
99
+ resource: str,
100
+ db_dep: Callable[..., Any],
101
+ nested_vars: Sequence[str] | None = None,
102
+ api: Any | None = None,
103
+ ) -> Callable[..., Awaitable[Any]]:
104
+ alias, target, nested_vars = sp.alias, sp.target, list(nested_vars or [])
105
+ status_code = _status_for(sp)
106
+
107
+ if target in {"list", "clear"}:
108
+ list_dep = _make_list_query_dep(model, alias) if target == "list" else None
109
+
110
+ async def _endpoint(
111
+ request: Request,
112
+ db: Any = Depends(db_dep),
113
+ q: Mapping[str, Any] | None = None,
114
+ **kw: Any,
115
+ ):
116
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
117
+ _coerce_parent_kw(model, parent_kw)
118
+ if q is not None:
119
+ query = {**dict(q), **parent_kw}
120
+ payload = _validate_query(model, alias, target, query)
121
+ else:
122
+ payload = dict(parent_kw)
123
+ ctx = _ctx(model, alias, target, request, db, payload, parent_kw, api)
124
+ ctx["response_serializer"] = lambda r: _serialize_output(
125
+ model, alias, target, sp, r
126
+ )
127
+ phases = _get_phase_chains(model, alias)
128
+ result = await _executor._invoke(
129
+ request=request, db=db, phases=phases, ctx=ctx
130
+ )
131
+ if isinstance(result, Response):
132
+ if sp.status_code is not None or result.status_code == 200:
133
+ result.status_code = status_code
134
+ return result
135
+ return result
136
+
137
+ params = [
138
+ inspect.Parameter(
139
+ nv,
140
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
141
+ annotation=Annotated[str, Path(...)],
142
+ )
143
+ for nv in nested_vars
144
+ ]
145
+ params.extend(
146
+ [
147
+ inspect.Parameter(
148
+ "request",
149
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
150
+ annotation=Request,
151
+ ),
152
+ inspect.Parameter(
153
+ "db",
154
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
155
+ annotation=Annotated[Any, Depends(db_dep)],
156
+ ),
157
+ ]
158
+ )
159
+ if target == "list":
160
+ params.insert(
161
+ len(nested_vars) + 1,
162
+ inspect.Parameter(
163
+ "q",
164
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
165
+ annotation=Annotated[Mapping[str, Any], Depends(list_dep)],
166
+ ),
167
+ )
168
+ _endpoint.__signature__ = inspect.Signature(params)
169
+ else:
170
+ body_model = _request_model_for(sp, model)
171
+ base = body_model or Mapping[str, Any]
172
+ if target.startswith("bulk_"):
173
+ alias_ns = getattr(
174
+ getattr(model, "schemas", None) or SimpleNamespace(), alias, None
175
+ )
176
+ item_model = getattr(alias_ns, "in_item", None) if alias_ns else None
177
+ body_annotation = (
178
+ _list_ann(item_model)
179
+ if isinstance(item_model, type) and issubclass(item_model, BaseModel)
180
+ else _list_ann(Mapping[str, Any])
181
+ if body_model is None
182
+ else base
183
+ )
184
+ elif target in {"create", "update", "replace", "merge"}:
185
+ body_annotation = _union(
186
+ base if body_model else Mapping[str, Any],
187
+ _list_ann(Mapping[str, Any]),
188
+ )
189
+ else:
190
+ body_annotation = base
191
+
192
+ async def _endpoint(
193
+ request: Request,
194
+ db: Any = Depends(db_dep),
195
+ body=Body(...),
196
+ **kw: Any,
197
+ ):
198
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
199
+ _coerce_parent_kw(model, parent_kw)
200
+ payload = _validate_body(model, alias, target, body)
201
+ is_seq = (
202
+ target in {"create", "update", "replace", "merge"}
203
+ and isinstance(payload, Sequence)
204
+ and not isinstance(payload, Mapping)
205
+ )
206
+ exec_alias = f"bulk_{target}" if is_seq else alias
207
+ exec_target = f"bulk_{target}" if is_seq else target
208
+ if parent_kw:
209
+ if isinstance(payload, Mapping):
210
+ payload = {**payload, **parent_kw}
211
+ else:
212
+ payload = [{**dict(item), **parent_kw} for item in payload]
213
+ ctx = _ctx(
214
+ model, exec_alias, exec_target, request, db, payload, parent_kw, api
215
+ )
216
+
217
+ def _serializer(r, _ctx=ctx):
218
+ out = _serialize_output(model, exec_alias, exec_target, sp, r)
219
+ temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
220
+ extras = (
221
+ temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
222
+ )
223
+ if isinstance(out, dict) and isinstance(extras, dict):
224
+ out.update(extras)
225
+ return out
226
+
227
+ ctx["response_serializer"] = _serializer
228
+ phases = _get_phase_chains(model, exec_alias)
229
+ result = await _executor._invoke(
230
+ request=request, db=db, phases=phases, ctx=ctx
231
+ )
232
+ if isinstance(result, Response):
233
+ if sp.status_code is not None or result.status_code == 200:
234
+ result.status_code = status_code
235
+ return result
236
+ return result
237
+
238
+ _endpoint.__signature__ = _sig(
239
+ nested_vars,
240
+ [
241
+ inspect.Parameter(
242
+ "request",
243
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
244
+ annotation=Request,
245
+ ),
246
+ inspect.Parameter(
247
+ "db",
248
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
249
+ annotation=Annotated[Any, Depends(db_dep)],
250
+ ),
251
+ inspect.Parameter(
252
+ "body",
253
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
254
+ annotation=Annotated[body_annotation, Body(...)],
255
+ ),
256
+ ],
257
+ )
258
+ _endpoint.__annotations__["body"] = body_annotation
259
+
260
+ _endpoint.__name__ = f"rest_{model.__name__}_{alias}_collection"
261
+ _endpoint.__qualname__ = _endpoint.__name__
262
+ _endpoint.__doc__ = (
263
+ f"REST collection endpoint for {model.__name__}.{alias} ({target})"
264
+ )
265
+ return _endpoint
@@ -0,0 +1,116 @@
1
+ """Shared interfaces for tigrbl.bindings.rest.
2
+
3
+ This module re-exports helper utilities split across smaller modules to keep the
4
+ import surface stable while easing maintenance.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import logging
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from .fastapi import (
13
+ Body,
14
+ Depends,
15
+ HTTPException,
16
+ Path,
17
+ Query,
18
+ Request,
19
+ Response,
20
+ Router,
21
+ Security,
22
+ _status,
23
+ )
24
+ from .helpers import (
25
+ _Key,
26
+ _coerce_parent_kw,
27
+ _ensure_jsonable,
28
+ _get_phase_chains,
29
+ _pk_name,
30
+ _pk_names,
31
+ _req_state_db,
32
+ _resource_name,
33
+ )
34
+ from .io import (
35
+ _make_list_query_dep,
36
+ _optionalize_list_in_model,
37
+ _serialize_output,
38
+ _strip_optional,
39
+ _validate_body,
40
+ _validate_query,
41
+ )
42
+ from .routing import (
43
+ _DEFAULT_METHODS,
44
+ _RESPONSES_META,
45
+ _default_path_suffix,
46
+ _normalize_deps,
47
+ _normalize_secdeps,
48
+ _path_for_spec,
49
+ _request_model_for,
50
+ _response_model_for,
51
+ _status_for,
52
+ )
53
+ from ...config.constants import (
54
+ TIGRBL_ALLOW_ANON_ATTR,
55
+ TIGRBL_AUTH_CONTEXT_ATTR,
56
+ TIGRBL_AUTH_DEP_ATTR,
57
+ TIGRBL_GET_DB_ATTR,
58
+ TIGRBL_REST_DEPENDENCIES_ATTR,
59
+ )
60
+ from ...op import OpSpec
61
+ from ...op.types import CANON, PHASES
62
+ from ...rest import _nested_prefix
63
+ from ...runtime import executor as _executor
64
+ from ...schema.builder import _strip_parent_fields
65
+
66
+ logger = logging.getLogger("uvicorn")
67
+ logger.debug("Loaded module v3/bindings/rest/common")
68
+
69
+ __all__ = [
70
+ "Body",
71
+ "Depends",
72
+ "HTTPException",
73
+ "Path",
74
+ "Query",
75
+ "Request",
76
+ "Response",
77
+ "Router",
78
+ "Security",
79
+ "_status",
80
+ "BaseModel",
81
+ "OpSpec",
82
+ "CANON",
83
+ "PHASES",
84
+ "_executor",
85
+ "TIGRBL_GET_DB_ATTR",
86
+ "TIGRBL_AUTH_DEP_ATTR",
87
+ "TIGRBL_REST_DEPENDENCIES_ATTR",
88
+ "TIGRBL_ALLOW_ANON_ATTR",
89
+ "TIGRBL_AUTH_CONTEXT_ATTR",
90
+ "_nested_prefix",
91
+ "_strip_parent_fields",
92
+ "logger",
93
+ "_Key",
94
+ "_ensure_jsonable",
95
+ "_req_state_db",
96
+ "_resource_name",
97
+ "_pk_name",
98
+ "_pk_names",
99
+ "_get_phase_chains",
100
+ "_coerce_parent_kw",
101
+ "_serialize_output",
102
+ "_validate_body",
103
+ "_validate_query",
104
+ "_strip_optional",
105
+ "_make_list_query_dep",
106
+ "_optionalize_list_in_model",
107
+ "_normalize_deps",
108
+ "_normalize_secdeps",
109
+ "_status_for",
110
+ "_RESPONSES_META",
111
+ "_DEFAULT_METHODS",
112
+ "_default_path_suffix",
113
+ "_path_for_spec",
114
+ "_response_model_for",
115
+ "_request_model_for",
116
+ ]
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ from types import SimpleNamespace
5
+ from typing import Callable, Sequence
6
+
7
+ logger = logging.getLogger("uvicorn")
8
+ logger.debug("Loaded module v3/bindings/rest/fastapi")
9
+
10
+ try:
11
+ from ...types import (
12
+ Router,
13
+ Request,
14
+ Body,
15
+ Depends,
16
+ HTTPException,
17
+ Response,
18
+ Path,
19
+ Security,
20
+ )
21
+ from fastapi import Query
22
+ from fastapi import status as _status
23
+ except Exception: # pragma: no cover
24
+
25
+ class Router: # type: ignore
26
+ def __init__(self, *a, **kw):
27
+ self.routes = []
28
+
29
+ def add_api_route(
30
+ self, path: str, endpoint: Callable, methods: Sequence[str], **opts
31
+ ):
32
+ self.routes.append((path, methods, endpoint, opts))
33
+
34
+ class Request: # type: ignore
35
+ def __init__(self, scope=None):
36
+ self.scope = scope or {}
37
+ self.query_params = {}
38
+ self.state = SimpleNamespace()
39
+
40
+ def Body(default=None, **kw): # type: ignore
41
+ return default
42
+
43
+ def Depends(fn): # type: ignore
44
+ return fn
45
+
46
+ def Security(fn): # type: ignore
47
+ return fn
48
+
49
+ def Query(default=None, **kw): # type: ignore
50
+ return default
51
+
52
+ def Path(default=None, **kw): # type: ignore
53
+ return default
54
+
55
+ class HTTPException(Exception): # type: ignore
56
+ def __init__(self, status_code: int, detail: str | None = None):
57
+ super().__init__(detail)
58
+ self.status_code = status_code
59
+ self.detail = detail
60
+
61
+ class Response: # type: ignore
62
+ def __init__(self, *a, **kw):
63
+ pass
64
+
65
+ class _status: # type: ignore
66
+ HTTP_200_OK = 200
67
+ HTTP_201_CREATED = 201
68
+ HTTP_204_NO_CONTENT = 204
69
+ HTTP_400_BAD_REQUEST = 400
70
+ HTTP_401_UNAUTHORIZED = 401
71
+ HTTP_403_FORBIDDEN = 403
72
+ HTTP_404_NOT_FOUND = 404
73
+ HTTP_409_CONFLICT = 409
74
+ HTTP_422_UNPROCESSABLE_ENTITY = 422
75
+ HTTP_429_TOO_MANY_REQUESTS = 429
76
+ HTTP_500_INTERNAL_SERVER_ERROR = 500
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from types import SimpleNamespace
5
+ from typing import Any, Awaitable, Callable, Dict, Mapping, Sequence, Tuple
6
+
7
+ from .fastapi import Request
8
+ from ...op.types import PHASES
9
+
10
+ try:
11
+ from ...runtime.kernel import build_phase_chains as _kernel_build_phase_chains # type: ignore
12
+ except Exception: # pragma: no cover
13
+ _kernel_build_phase_chains = None # type: ignore
14
+
15
+ logger = logging.getLogger("uvicorn")
16
+ logger.debug("Loaded module v3/bindings/rest/helpers")
17
+
18
+ _Key = Tuple[str, str] # (alias, target)
19
+
20
+
21
+ def _ensure_jsonable(obj: Any) -> Any:
22
+ """Best-effort conversion of DB rows, row-mappings, or ORM objects to dicts."""
23
+ if isinstance(obj, (list, tuple)):
24
+ return [_ensure_jsonable(x) for x in obj]
25
+
26
+ if isinstance(obj, Mapping):
27
+ try:
28
+ return {k: _ensure_jsonable(v) for k, v in dict(obj).items()}
29
+ except Exception: # pragma: no cover - fall back to original object
30
+ pass
31
+
32
+ try:
33
+ data = vars(obj)
34
+ except TypeError:
35
+ return obj
36
+
37
+ return {k: _ensure_jsonable(v) for k, v in data.items() if not k.startswith("_")}
38
+
39
+
40
+ def _req_state_db(request: Request) -> Any:
41
+ return getattr(request.state, "db", None)
42
+
43
+
44
+ def _resource_name(model: type) -> str:
45
+ """
46
+ Resource segment for HTTP paths/tags.
47
+
48
+ IMPORTANT: Never use table name here. Only allow an explicit __resource__
49
+ override or fall back to the model class name in lowercase.
50
+ """
51
+ override = getattr(model, "__resource__", None)
52
+ return override or model.__name__.lower()
53
+
54
+
55
+ def _pk_name(model: type) -> str:
56
+ """
57
+ Single primary key name (fallback 'id'). For composite keys, still returns 'id'.
58
+ Used for backward-compat path-param aliasing and handler resolution.
59
+ """
60
+ table = getattr(model, "__table__", None)
61
+ if table is None:
62
+ return "id"
63
+ pk = getattr(table, "primary_key", None)
64
+ if pk is None:
65
+ return "id"
66
+ try:
67
+ cols = list(pk.columns)
68
+ except Exception:
69
+ return "id"
70
+ if len(cols) != 1:
71
+ return "id"
72
+ return getattr(cols[0], "name", "id")
73
+
74
+
75
+ def _pk_names(model: type) -> set[str]:
76
+ """All PK column names (fallback {'id'})."""
77
+ table = getattr(model, "__table__", None)
78
+ try:
79
+ cols = getattr(getattr(table, "primary_key", None), "columns", None)
80
+ if cols is None:
81
+ return {"id"}
82
+ out = {getattr(c, "name", None) for c in cols}
83
+ out.discard(None)
84
+ return out or {"id"}
85
+ except Exception:
86
+ return {"id"}
87
+
88
+
89
+ def _get_phase_chains(
90
+ model: type, alias: str
91
+ ) -> Dict[str, Sequence[Callable[..., Awaitable[Any]]]]:
92
+ """
93
+ Prefer building via runtime Kernel (atoms + system steps + hooks in one lifecycle).
94
+ Fallback: read the pre-built model.hooks.<alias> chains directly.
95
+ """
96
+ if _kernel_build_phase_chains is not None:
97
+ try:
98
+ return _kernel_build_phase_chains(model, alias)
99
+ except Exception:
100
+ logger.exception(
101
+ "Kernel build_phase_chains failed for %s.%s; falling back to hooks",
102
+ getattr(model, "__name__", model),
103
+ alias,
104
+ )
105
+ hooks_root = getattr(model, "hooks", None) or SimpleNamespace()
106
+ alias_ns = getattr(hooks_root, alias, None)
107
+ out: Dict[str, Sequence[Callable[..., Awaitable[Any]]]] = {}
108
+ for ph in PHASES:
109
+ out[ph] = list(getattr(alias_ns, ph, []) or [])
110
+ return out
111
+
112
+
113
+ def _coerce_parent_kw(model: type, parent_kw: Dict[str, Any]) -> None:
114
+ for name, val in list(parent_kw.items()):
115
+ col = getattr(model, name, None)
116
+ try:
117
+ parent_kw[name] = col.type.python_type(val) # type: ignore[attr-defined]
118
+ except Exception:
119
+ pass