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,286 @@
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 .io_headers import _make_header_dep
39
+
40
+ from ...runtime.executor.types import _Ctx
41
+
42
+
43
+ logging.getLogger("uvicorn").debug("Loaded module v3/bindings/rest/collection")
44
+
45
+
46
+ def _ctx(model, alias, target, request, db, payload, parent_kw, api):
47
+ ctx: Dict[str, Any] = {
48
+ "request": request,
49
+ "db": db,
50
+ "payload": payload,
51
+ "path_params": parent_kw,
52
+ # expose both API router and FastAPI app; runtime opview resolution
53
+ # relies on the app object, which must be hashable.
54
+ "api": api if api is not None else getattr(request, "app", None),
55
+ "app": getattr(request, "app", None),
56
+ "model": model,
57
+ "op": alias,
58
+ "method": alias,
59
+ "target": target,
60
+ "env": SimpleNamespace(
61
+ method=alias, params=payload, target=target, model=model
62
+ ),
63
+ }
64
+ ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
65
+ if ac is not None:
66
+ ctx["auth_context"] = ac
67
+ return _Ctx(ctx)
68
+
69
+
70
+ def _sig(nested_vars, extra):
71
+ params = [
72
+ inspect.Parameter(
73
+ nv,
74
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
75
+ annotation=Annotated[str, Path(...)],
76
+ )
77
+ for nv in nested_vars
78
+ ]
79
+ params.extend(extra)
80
+ return inspect.Signature(params)
81
+
82
+
83
+ def _list_ann(tp):
84
+ try:
85
+ return list[tp] # type: ignore[valid-type]
86
+ except Exception: # pragma: no cover - best effort
87
+ return _List[tp] # type: ignore[name-defined]
88
+
89
+
90
+ def _union(a, b):
91
+ try:
92
+ return a | b # type: ignore[operator]
93
+ except Exception: # pragma: no cover - best effort
94
+ return _Union[a, b]
95
+
96
+
97
+ def _make_collection_endpoint(
98
+ model: type,
99
+ sp: OpSpec,
100
+ *,
101
+ resource: str,
102
+ db_dep: Callable[..., Any],
103
+ nested_vars: Sequence[str] | None = None,
104
+ api: Any | None = None,
105
+ ) -> Callable[..., Awaitable[Any]]:
106
+ alias, target, nested_vars = sp.alias, sp.target, list(nested_vars or [])
107
+ status_code = _status_for(sp)
108
+ hdr_dep = _make_header_dep(model, alias)
109
+
110
+ if target in {"list", "clear"}:
111
+ list_dep = _make_list_query_dep(model, alias) if target == "list" else None
112
+
113
+ async def _endpoint(
114
+ request: Request,
115
+ db: Any = Depends(db_dep),
116
+ h: Mapping[str, Any] = Depends(hdr_dep),
117
+ q: Mapping[str, Any] | None = None,
118
+ **kw: Any,
119
+ ):
120
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
121
+ _coerce_parent_kw(model, parent_kw)
122
+ if q is not None:
123
+ query = {**dict(q), **parent_kw}
124
+ payload = _validate_query(model, alias, target, query)
125
+ else:
126
+ payload = dict(parent_kw)
127
+ if isinstance(h, Mapping):
128
+ payload = {**payload, **dict(h)}
129
+ ctx = _ctx(model, alias, target, request, db, payload, parent_kw, api)
130
+ ctx["response_serializer"] = lambda r: _serialize_output(
131
+ model, alias, target, sp, r
132
+ )
133
+ phases = _get_phase_chains(model, alias)
134
+ result = await _executor._invoke(
135
+ request=request, db=db, phases=phases, ctx=ctx
136
+ )
137
+ if isinstance(result, Response):
138
+ if sp.status_code is not None or result.status_code == 200:
139
+ result.status_code = status_code
140
+ return result
141
+ return result
142
+
143
+ params = [
144
+ inspect.Parameter(
145
+ nv,
146
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
147
+ annotation=Annotated[str, Path(...)],
148
+ )
149
+ for nv in nested_vars
150
+ ]
151
+ params.extend(
152
+ [
153
+ inspect.Parameter(
154
+ "request",
155
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
156
+ annotation=Request,
157
+ ),
158
+ inspect.Parameter(
159
+ "db",
160
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
161
+ annotation=Annotated[Any, Depends(db_dep)],
162
+ ),
163
+ inspect.Parameter(
164
+ "h",
165
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
166
+ annotation=Annotated[Mapping[str, Any], Depends(hdr_dep)],
167
+ ),
168
+ ]
169
+ )
170
+ if target == "list":
171
+ params.append(
172
+ inspect.Parameter(
173
+ "q",
174
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
175
+ annotation=Annotated[Mapping[str, Any], Depends(list_dep)],
176
+ )
177
+ )
178
+ _endpoint.__signature__ = inspect.Signature(params)
179
+ else:
180
+ body_model = _request_model_for(sp, model)
181
+ base = body_model or Mapping[str, Any]
182
+ if target.startswith("bulk_"):
183
+ alias_ns = getattr(
184
+ getattr(model, "schemas", None) or SimpleNamespace(), alias, None
185
+ )
186
+ item_model = getattr(alias_ns, "in_item", None) if alias_ns else None
187
+ body_annotation = (
188
+ _list_ann(item_model)
189
+ if isinstance(item_model, type) and issubclass(item_model, BaseModel)
190
+ else _list_ann(Mapping[str, Any])
191
+ if body_model is None
192
+ else base
193
+ )
194
+ elif target in {"create", "update", "replace", "merge"}:
195
+ body_annotation = _union(
196
+ base if body_model else Mapping[str, Any],
197
+ _list_ann(Mapping[str, Any]),
198
+ )
199
+ else:
200
+ body_annotation = base
201
+
202
+ async def _endpoint(
203
+ request: Request,
204
+ db: Any = Depends(db_dep),
205
+ h: Mapping[str, Any] = Depends(hdr_dep),
206
+ body=Body(...),
207
+ **kw: Any,
208
+ ):
209
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
210
+ _coerce_parent_kw(model, parent_kw)
211
+ payload = _validate_body(model, alias, target, body)
212
+ if isinstance(h, Mapping):
213
+ if isinstance(payload, Mapping):
214
+ payload = {**payload, **dict(h)}
215
+ else:
216
+ payload = [{**dict(item), **dict(h)} for item in payload]
217
+ is_seq = (
218
+ target in {"create", "update", "replace", "merge"}
219
+ and isinstance(payload, Sequence)
220
+ and not isinstance(payload, Mapping)
221
+ )
222
+ exec_alias = f"bulk_{target}" if is_seq else alias
223
+ exec_target = f"bulk_{target}" if is_seq else target
224
+ if parent_kw:
225
+ if isinstance(payload, Mapping):
226
+ payload = {**payload, **parent_kw}
227
+ else:
228
+ payload = [{**dict(item), **parent_kw} for item in payload]
229
+ ctx = _ctx(
230
+ model, exec_alias, exec_target, request, db, payload, parent_kw, api
231
+ )
232
+
233
+ def _serializer(r, _ctx=ctx):
234
+ out = _serialize_output(model, exec_alias, exec_target, sp, r)
235
+ temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
236
+ extras = (
237
+ temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
238
+ )
239
+ if isinstance(out, dict) and isinstance(extras, dict):
240
+ out.update(extras)
241
+ return out
242
+
243
+ ctx["response_serializer"] = _serializer
244
+ phases = _get_phase_chains(model, exec_alias)
245
+ result = await _executor._invoke(
246
+ request=request, db=db, phases=phases, ctx=ctx
247
+ )
248
+ if isinstance(result, Response):
249
+ if sp.status_code is not None or result.status_code == 200:
250
+ result.status_code = status_code
251
+ return result
252
+ return result
253
+
254
+ _endpoint.__signature__ = _sig(
255
+ nested_vars,
256
+ [
257
+ inspect.Parameter(
258
+ "request",
259
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
260
+ annotation=Request,
261
+ ),
262
+ inspect.Parameter(
263
+ "db",
264
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
265
+ annotation=Annotated[Any, Depends(db_dep)],
266
+ ),
267
+ inspect.Parameter(
268
+ "h",
269
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
270
+ annotation=Annotated[Mapping[str, Any], Depends(hdr_dep)],
271
+ ),
272
+ inspect.Parameter(
273
+ "body",
274
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
275
+ annotation=Annotated[body_annotation, Body(...)],
276
+ ),
277
+ ],
278
+ )
279
+ _endpoint.__annotations__["body"] = body_annotation
280
+
281
+ _endpoint.__name__ = f"rest_{model.__name__}_{alias}_collection"
282
+ _endpoint.__qualname__ = _endpoint.__name__
283
+ _endpoint.__doc__ = (
284
+ f"REST collection endpoint for {model.__name__}.{alias} ({target})"
285
+ )
286
+ return _endpoint
@@ -0,0 +1,120 @@
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
+ _require_auth_header,
49
+ _requires_auth_header,
50
+ _path_for_spec,
51
+ _request_model_for,
52
+ _response_model_for,
53
+ _status_for,
54
+ )
55
+ from ...config.constants import (
56
+ TIGRBL_ALLOW_ANON_ATTR,
57
+ TIGRBL_AUTH_CONTEXT_ATTR,
58
+ TIGRBL_AUTH_DEP_ATTR,
59
+ TIGRBL_GET_DB_ATTR,
60
+ TIGRBL_REST_DEPENDENCIES_ATTR,
61
+ )
62
+ from ...op import OpSpec
63
+ from ...op.types import CANON, PHASES
64
+ from ...rest import _nested_prefix
65
+ from ...runtime import executor as _executor
66
+ from ...schema.builder import _strip_parent_fields
67
+
68
+ logger = logging.getLogger("uvicorn")
69
+ logger.debug("Loaded module v3/bindings/rest/common")
70
+
71
+ __all__ = [
72
+ "Body",
73
+ "Depends",
74
+ "HTTPException",
75
+ "Path",
76
+ "Query",
77
+ "Request",
78
+ "Response",
79
+ "Router",
80
+ "Security",
81
+ "_status",
82
+ "BaseModel",
83
+ "OpSpec",
84
+ "CANON",
85
+ "PHASES",
86
+ "_executor",
87
+ "TIGRBL_GET_DB_ATTR",
88
+ "TIGRBL_AUTH_DEP_ATTR",
89
+ "TIGRBL_REST_DEPENDENCIES_ATTR",
90
+ "TIGRBL_ALLOW_ANON_ATTR",
91
+ "TIGRBL_AUTH_CONTEXT_ATTR",
92
+ "_nested_prefix",
93
+ "_strip_parent_fields",
94
+ "logger",
95
+ "_Key",
96
+ "_ensure_jsonable",
97
+ "_req_state_db",
98
+ "_resource_name",
99
+ "_pk_name",
100
+ "_pk_names",
101
+ "_get_phase_chains",
102
+ "_coerce_parent_kw",
103
+ "_serialize_output",
104
+ "_validate_body",
105
+ "_validate_query",
106
+ "_strip_optional",
107
+ "_make_list_query_dep",
108
+ "_optionalize_list_in_model",
109
+ "_normalize_deps",
110
+ "_normalize_secdeps",
111
+ "_require_auth_header",
112
+ "_requires_auth_header",
113
+ "_status_for",
114
+ "_RESPONSES_META",
115
+ "_DEFAULT_METHODS",
116
+ "_default_path_suffix",
117
+ "_path_for_spec",
118
+ "_response_model_for",
119
+ "_request_model_for",
120
+ ]
@@ -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