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,317 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ import inspect
5
+ from types import SimpleNamespace
6
+ from typing import Any, Dict, Mapping, Sequence
7
+ from typing import get_origin as _get_origin, get_args as _get_args
8
+ import typing as _typing
9
+
10
+ from pydantic import BaseModel, Field, create_model
11
+
12
+ from .fastapi import HTTPException, Query, Request, _status
13
+ from .helpers import _ensure_jsonable
14
+ from ...op import OpSpec
15
+
16
+ logger = logging.getLogger("uvicorn")
17
+ logger.debug("Loaded module v3/bindings/rest/io")
18
+
19
+
20
+ def _serialize_output(
21
+ model: type, alias: str, target: str, sp: OpSpec, result: Any
22
+ ) -> Any:
23
+ """
24
+ If a response schema exists (model.schemas.<alias>.out), serialize to it.
25
+ Otherwise, attempt a best-effort conversion to primitive types so FastAPI
26
+ can JSON-encode the response.
27
+ """
28
+
29
+ from ...types import Response as _Response # local import to avoid cycles
30
+
31
+ if isinstance(result, _Response):
32
+ return result
33
+
34
+ def _final(val: Any) -> Any:
35
+ if target == "list" and isinstance(val, (list, tuple)):
36
+ return [_ensure_jsonable(v) for v in val]
37
+ return _ensure_jsonable(val)
38
+
39
+ schemas_root = getattr(model, "schemas", None)
40
+ if not schemas_root:
41
+ return _final(result)
42
+ alias_ns = getattr(schemas_root, alias, None)
43
+ if not alias_ns:
44
+ return _final(result)
45
+ out_model = getattr(alias_ns, "out", None)
46
+ if (
47
+ not out_model
48
+ or not inspect.isclass(out_model)
49
+ or not issubclass(out_model, BaseModel)
50
+ ):
51
+ return _final(result)
52
+ try:
53
+ if target == "list" and isinstance(result, (list, tuple)):
54
+ return [
55
+ out_model.model_validate(x).model_dump(
56
+ exclude_none=False, by_alias=True
57
+ )
58
+ for x in result
59
+ ]
60
+ return out_model.model_validate(result).model_dump(
61
+ exclude_none=False, by_alias=True
62
+ )
63
+ except Exception:
64
+ logger.debug(
65
+ "rest output serialization failed for %s.%s",
66
+ model.__name__,
67
+ alias,
68
+ exc_info=True,
69
+ )
70
+ return _final(result)
71
+
72
+
73
+ def _validate_body(
74
+ model: type, alias: str, target: str, body: Any | None
75
+ ) -> Mapping[str, Any] | Sequence[Mapping[str, Any]]:
76
+ """Normalize and validate the incoming request body."""
77
+ if isinstance(body, BaseModel):
78
+ return body.model_dump(exclude_none=True)
79
+
80
+ if target in {"bulk_create", "bulk_update", "bulk_replace", "bulk_merge"}:
81
+ items: Sequence[Any] = body or []
82
+ if not isinstance(items, Sequence) or isinstance(items, (str, bytes)):
83
+ items = []
84
+
85
+ schemas_root = getattr(model, "schemas", None)
86
+ alias_ns = getattr(schemas_root, alias, None) if schemas_root else None
87
+ in_item = getattr(alias_ns, "in_item", None) if alias_ns else None
88
+
89
+ out: list[Mapping[str, Any]] = []
90
+ for item in items:
91
+ if isinstance(item, BaseModel):
92
+ out.append(item.model_dump(exclude_none=True))
93
+ continue
94
+ data: Mapping[str, Any] | None = None
95
+ if in_item and inspect.isclass(in_item) and issubclass(in_item, BaseModel):
96
+ try:
97
+ inst = in_item.model_validate(item) # type: ignore[arg-type]
98
+ data = inst.model_dump(exclude_none=True)
99
+ except Exception:
100
+ logger.debug(
101
+ "rest input body validation failed for %s.%s",
102
+ model.__name__,
103
+ alias,
104
+ exc_info=True,
105
+ )
106
+ if data is None:
107
+ data = dict(item) if isinstance(item, Mapping) else {}
108
+ out.append(data)
109
+ return out
110
+
111
+ if (
112
+ target in {"create", "update", "replace", "merge"}
113
+ and isinstance(body, Sequence)
114
+ and not isinstance(body, (str, bytes, Mapping))
115
+ ):
116
+ bulk_target = f"bulk_{target}"
117
+ items: Sequence[Any] = body
118
+ schemas_root = getattr(model, "schemas", None)
119
+ alias_ns = getattr(schemas_root, bulk_target, None) if schemas_root else None
120
+ in_item = getattr(alias_ns, "in_item", None) if alias_ns else None
121
+
122
+ out: list[Mapping[str, Any]] = []
123
+ for item in items:
124
+ if isinstance(item, BaseModel):
125
+ out.append(item.model_dump(exclude_none=True))
126
+ continue
127
+ data: Mapping[str, Any] | None = None
128
+ if in_item and inspect.isclass(in_item) and issubclass(in_item, BaseModel):
129
+ try:
130
+ inst = in_item.model_validate(item) # type: ignore[arg-type]
131
+ data = inst.model_dump(exclude_none=True)
132
+ except Exception:
133
+ logger.debug(
134
+ "rest input body validation failed for %s.%s",
135
+ model.__name__,
136
+ bulk_target,
137
+ exc_info=True,
138
+ )
139
+ if data is None:
140
+ data = dict(item) if isinstance(item, Mapping) else {}
141
+ out.append(data)
142
+ return out
143
+
144
+ body = body or {}
145
+ if not isinstance(body, Mapping):
146
+ body = {}
147
+
148
+ schemas_root = getattr(model, "schemas", None)
149
+ if not schemas_root:
150
+ return dict(body)
151
+ alias_ns = getattr(schemas_root, alias, None)
152
+ if not alias_ns:
153
+ return dict(body)
154
+ in_model = getattr(alias_ns, "in_", None)
155
+
156
+ if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
157
+ try:
158
+ inst = in_model.model_validate(body) # type: ignore[arg-type]
159
+ return inst.model_dump(exclude_none=True)
160
+ except Exception as e:
161
+ logger.debug(
162
+ "rest input body validation failed for %s.%s",
163
+ model.__name__,
164
+ alias,
165
+ exc_info=True,
166
+ )
167
+ raise HTTPException(
168
+ status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY,
169
+ detail=str(e),
170
+ )
171
+ return dict(body)
172
+
173
+
174
+ def _validate_query(
175
+ model: type, alias: str, target: str, query: Mapping[str, Any]
176
+ ) -> Mapping[str, Any]:
177
+ """Validate list/clear inputs coming from the query string."""
178
+ if not query or (isinstance(query, Mapping) and len(query) == 0):
179
+ return {}
180
+
181
+ schemas_root = getattr(model, "schemas", None)
182
+ if not schemas_root:
183
+ return dict(query)
184
+ alias_ns = getattr(schemas_root, alias, None)
185
+ if not alias_ns:
186
+ return dict(query)
187
+ in_model = getattr(alias_ns, "in_", None)
188
+
189
+ if in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel):
190
+ try:
191
+ fields = getattr(in_model, "model_fields", {})
192
+ data: Dict[str, Any] = {}
193
+ for name, f in fields.items():
194
+ alias_key = getattr(f, "alias", None) or name
195
+ if alias_key in query:
196
+ val = query[alias_key]
197
+ elif name in query:
198
+ val = query[name]
199
+ else:
200
+ continue
201
+ if val is None:
202
+ continue
203
+ if isinstance(val, str) and not val.strip():
204
+ continue
205
+ if isinstance(val, (list, tuple, set)) and len(val) == 0:
206
+ continue
207
+ data[name] = val
208
+
209
+ if not data:
210
+ return {}
211
+
212
+ inst = in_model.model_validate(data) # type: ignore[arg-type]
213
+ return inst.model_dump(exclude_none=True)
214
+ except Exception as e:
215
+ raise HTTPException(
216
+ status_code=_status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e)
217
+ )
218
+ return dict(query)
219
+
220
+
221
+ def _strip_optional(t: Any) -> Any:
222
+ """If annotation is Optional[T] return T; else return the input."""
223
+ origin = _get_origin(t)
224
+ if origin is _typing.Union:
225
+ args = tuple(a for a in _get_args(t) if a is not type(None))
226
+ return args[0] if len(args) == 1 else Any
227
+ return t
228
+
229
+
230
+ def _make_list_query_dep(model: type, alias: str):
231
+ """Build a dependency exposing Query(...) params from schemas.<alias>.in_."""
232
+ alias_ns = getattr(
233
+ getattr(model, "schemas", None) or SimpleNamespace(), alias, None
234
+ )
235
+ in_model = getattr(alias_ns, "in_", None)
236
+
237
+ if not (in_model and inspect.isclass(in_model) and issubclass(in_model, BaseModel)):
238
+
239
+ def _dep(request: Request) -> Dict[str, Any]:
240
+ return dict(request.query_params)
241
+
242
+ _dep.__name__ = f"list_params_{model.__name__}_{alias}"
243
+ return _dep
244
+
245
+ fields = getattr(in_model, "model_fields", {})
246
+
247
+ def _dep(**raw: Any) -> Dict[str, Any]:
248
+ """Collect only user-supplied values; never apply schema defaults here."""
249
+ data: Dict[str, Any] = {}
250
+ for name, f in fields.items():
251
+ key = getattr(f, "alias", None) or name
252
+ if key not in raw:
253
+ continue
254
+ val = raw[key]
255
+ if val is None:
256
+ continue
257
+ if isinstance(val, str) and not val.strip():
258
+ continue
259
+ if isinstance(val, (list, tuple, set)) and len(val) == 0:
260
+ continue
261
+ data[key] = val
262
+ return data
263
+
264
+ params: list[inspect.Parameter] = []
265
+ for name, f in fields.items():
266
+ key = getattr(f, "alias", None) or name
267
+ ann = getattr(f, "annotation", Any)
268
+ base = _strip_optional(ann)
269
+ origin = _get_origin(base)
270
+ if origin in (list, tuple, set):
271
+ inner = (_get_args(base) or (str,))[0]
272
+ annotation = list[inner] | None # type: ignore[index]
273
+ else:
274
+ annotation = base | None
275
+ default_q = Query(None, description=getattr(f, "description", None))
276
+ params.append(
277
+ inspect.Parameter(
278
+ name=key,
279
+ kind=inspect.Parameter.KEYWORD_ONLY,
280
+ default=default_q,
281
+ annotation=annotation,
282
+ )
283
+ )
284
+
285
+ _dep.__signature__ = inspect.Signature(
286
+ parameters=params, return_annotation=Dict[str, Any]
287
+ )
288
+ _dep.__name__ = f"list_params_{model.__name__}_{alias}"
289
+ return _dep
290
+
291
+
292
+ def _optionalize_list_in_model(in_model: type[BaseModel]) -> type[BaseModel]:
293
+ """Make every field Optional[...] with default=None."""
294
+ try:
295
+ fields = getattr(in_model, "model_fields", {})
296
+ except Exception:
297
+ return in_model
298
+
299
+ defs: Dict[str, tuple[Any, Any]] = {}
300
+ for name, f in fields.items():
301
+ ann = getattr(f, "annotation", Any)
302
+ opt_ann = _typing.Union[ann, type(None)]
303
+ defs[name] = (
304
+ opt_ann,
305
+ Field(
306
+ default=None,
307
+ alias=getattr(f, "alias", None),
308
+ description=getattr(f, "description", None),
309
+ ),
310
+ )
311
+
312
+ New = create_model( # type: ignore[misc]
313
+ f"{in_model.__name__}__Optionalized",
314
+ **defs,
315
+ )
316
+ setattr(New, "__tigrbl_optionalized__", True)
317
+ return New
@@ -0,0 +1,367 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ import inspect
5
+ from types import SimpleNamespace
6
+ from typing import (
7
+ Annotated,
8
+ Any,
9
+ Awaitable,
10
+ Callable,
11
+ Dict,
12
+ Mapping,
13
+ Optional,
14
+ Sequence,
15
+ )
16
+
17
+ from .common import (
18
+ TIGRBL_AUTH_CONTEXT_ATTR,
19
+ Body,
20
+ Depends,
21
+ HTTPException,
22
+ Response,
23
+ Path,
24
+ Request,
25
+ OpSpec,
26
+ _coerce_parent_kw,
27
+ _get_phase_chains,
28
+ _pk_name,
29
+ _pk_names,
30
+ _request_model_for,
31
+ _serialize_output,
32
+ _validate_body,
33
+ _executor,
34
+ _status,
35
+ _status_for,
36
+ )
37
+
38
+ from ...runtime.executor.types import _Ctx
39
+
40
+
41
+ logger = logging.getLogger("uvicorn")
42
+ logger.debug("Loaded module v3/bindings/rest/member")
43
+
44
+
45
+ def _make_member_endpoint(
46
+ model: type,
47
+ sp: OpSpec,
48
+ *,
49
+ resource: str,
50
+ db_dep: Callable[..., Any],
51
+ pk_param: str = "item_id",
52
+ nested_vars: Sequence[str] | None = None,
53
+ api: Any | None = None,
54
+ ) -> Callable[..., Awaitable[Any]]:
55
+ alias = sp.alias
56
+ target = sp.target
57
+ status_code = _status_for(sp)
58
+ real_pk = _pk_name(model)
59
+ pk_names = _pk_names(model)
60
+ nested_vars = list(nested_vars or [])
61
+
62
+ # --- No body on GET read / DELETE delete ---
63
+ if target in {"read", "delete"}:
64
+
65
+ async def _endpoint(
66
+ item_id: Any,
67
+ request: Request,
68
+ db: Any = Depends(db_dep),
69
+ **kw: Any,
70
+ ):
71
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
72
+ _coerce_parent_kw(model, parent_kw)
73
+ payload: Mapping[str, Any] = dict(parent_kw)
74
+ path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
75
+ ctx: Dict[str, Any] = {
76
+ "request": request,
77
+ "db": db,
78
+ "payload": payload,
79
+ "path_params": path_params,
80
+ # expose contextual metadata for downstream atoms
81
+ "api": api if api is not None else getattr(request, "app", None),
82
+ "app": getattr(request, "app", None),
83
+ "model": model,
84
+ "op": alias,
85
+ "method": alias,
86
+ "target": target,
87
+ "env": SimpleNamespace(
88
+ method=alias, params=payload, target=target, model=model
89
+ ),
90
+ }
91
+ ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
92
+ if ac is not None:
93
+ ctx["auth_context"] = ac
94
+ ctx = _Ctx(ctx)
95
+
96
+ def _serializer(r, _ctx=ctx):
97
+ out = _serialize_output(model, alias, target, sp, r)
98
+ temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
99
+ extras = (
100
+ temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
101
+ )
102
+ if isinstance(out, dict) and isinstance(extras, dict):
103
+ out.update(extras)
104
+ return out
105
+
106
+ ctx["response_serializer"] = _serializer
107
+ phases = _get_phase_chains(model, alias)
108
+ result = await _executor._invoke(
109
+ request=request,
110
+ db=db,
111
+ phases=phases,
112
+ ctx=ctx,
113
+ )
114
+ if isinstance(result, Response):
115
+ if sp.status_code is not None or result.status_code == 200:
116
+ result.status_code = status_code
117
+ return result
118
+ return result
119
+
120
+ params = [
121
+ inspect.Parameter(
122
+ nv,
123
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
124
+ annotation=Annotated[str, Path(...)],
125
+ )
126
+ for nv in nested_vars
127
+ ]
128
+ params.extend(
129
+ [
130
+ inspect.Parameter(
131
+ "item_id",
132
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
133
+ annotation=Annotated[Any, Path(...)],
134
+ ),
135
+ inspect.Parameter(
136
+ "request",
137
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
138
+ annotation=Request,
139
+ ),
140
+ inspect.Parameter(
141
+ "db",
142
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
143
+ annotation=Annotated[Any, Depends(db_dep)],
144
+ ),
145
+ ]
146
+ )
147
+ _endpoint.__signature__ = inspect.Signature(params)
148
+
149
+ _endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
150
+ _endpoint.__qualname__ = _endpoint.__name__
151
+ _endpoint.__doc__ = (
152
+ f"REST member endpoint for {model.__name__}.{alias} ({target})"
153
+ )
154
+ # NOTE: do NOT set body annotation for no-body endpoints
155
+ return _endpoint
156
+
157
+ body_model = _request_model_for(sp, model)
158
+ if body_model is None and sp.request_model is None and target == "custom":
159
+
160
+ async def _endpoint(
161
+ item_id: Any,
162
+ request: Request,
163
+ db: Any = Depends(db_dep),
164
+ **kw: Any,
165
+ ):
166
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
167
+ _coerce_parent_kw(model, parent_kw)
168
+ payload: Mapping[str, Any] = dict(parent_kw)
169
+ path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
170
+ ctx: Dict[str, Any] = {
171
+ "request": request,
172
+ "db": db,
173
+ "payload": payload,
174
+ "path_params": path_params,
175
+ # expose contextual metadata for downstream atoms
176
+ "api": api if api is not None else getattr(request, "app", None),
177
+ "app": getattr(request, "app", None),
178
+ "model": model,
179
+ "op": alias,
180
+ "method": alias,
181
+ "target": target,
182
+ "env": SimpleNamespace(
183
+ method=alias, params=payload, target=target, model=model
184
+ ),
185
+ }
186
+ ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
187
+ if ac is not None:
188
+ ctx["auth_context"] = ac
189
+ ctx = _Ctx(ctx)
190
+
191
+ def _serializer(r, _ctx=ctx):
192
+ out = _serialize_output(model, alias, target, sp, r)
193
+ temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
194
+ extras = (
195
+ temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
196
+ )
197
+ if isinstance(out, dict) and isinstance(extras, dict):
198
+ out.update(extras)
199
+ return out
200
+
201
+ ctx["response_serializer"] = _serializer
202
+ phases = _get_phase_chains(model, alias)
203
+ result = await _executor._invoke(
204
+ request=request,
205
+ db=db,
206
+ phases=phases,
207
+ ctx=ctx,
208
+ )
209
+ return result
210
+
211
+ params = [
212
+ inspect.Parameter(
213
+ nv,
214
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
215
+ annotation=Annotated[str, Path(...)],
216
+ )
217
+ for nv in nested_vars
218
+ ]
219
+ params.extend(
220
+ [
221
+ inspect.Parameter(
222
+ "item_id",
223
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
224
+ annotation=Annotated[Any, Path(...)],
225
+ ),
226
+ inspect.Parameter(
227
+ "request",
228
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
229
+ annotation=Request,
230
+ ),
231
+ inspect.Parameter(
232
+ "db",
233
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
234
+ annotation=Annotated[Any, Depends(db_dep)],
235
+ ),
236
+ ]
237
+ )
238
+ _endpoint.__signature__ = inspect.Signature(params)
239
+
240
+ _endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
241
+ _endpoint.__qualname__ = _endpoint.__name__
242
+ _endpoint.__doc__ = (
243
+ f"REST member endpoint for {model.__name__}.{alias} ({target})"
244
+ )
245
+ return _endpoint
246
+
247
+ # --- Body-based member endpoints: PATCH update / PUT replace (and custom member) ---
248
+
249
+ if body_model is None:
250
+ body_annotation = Optional[Mapping[str, Any]]
251
+ body_default = Body(None)
252
+ else:
253
+ body_annotation = body_model
254
+ body_default = Body(...)
255
+
256
+ async def _endpoint(
257
+ item_id: Any,
258
+ request: Request,
259
+ db: Any = Depends(db_dep),
260
+ body=body_default,
261
+ **kw: Any,
262
+ ):
263
+ parent_kw = {k: kw[k] for k in nested_vars if k in kw}
264
+ _coerce_parent_kw(model, parent_kw)
265
+ payload = _validate_body(model, alias, target, body)
266
+
267
+ # Enforce path-PK canonicality. If body echoes PK: drop if equal, 409 if mismatch.
268
+ for k in pk_names:
269
+ if k in payload:
270
+ if str(payload[k]) != str(item_id) and len(pk_names) == 1:
271
+ raise HTTPException(
272
+ status_code=_status.HTTP_409_CONFLICT,
273
+ detail=f"Identifier mismatch for '{k}': path={item_id}, body={payload[k]}",
274
+ )
275
+ payload.pop(k, None)
276
+ payload.pop(pk_param, None)
277
+ if parent_kw:
278
+ payload.update(parent_kw)
279
+
280
+ path_params = {real_pk: item_id, pk_param: item_id, **parent_kw}
281
+
282
+ ctx: Dict[str, Any] = {
283
+ "request": request,
284
+ "db": db,
285
+ "payload": payload,
286
+ "path_params": path_params,
287
+ # expose contextual metadata for downstream atoms
288
+ "app": getattr(request, "app", None),
289
+ "api": getattr(request, "app", None),
290
+ "model": model,
291
+ "op": alias,
292
+ "method": alias,
293
+ "target": target,
294
+ "env": SimpleNamespace(
295
+ method=alias, params=payload, target=target, model=model
296
+ ),
297
+ }
298
+ ac = getattr(request.state, TIGRBL_AUTH_CONTEXT_ATTR, None)
299
+ if ac is not None:
300
+ ctx["auth_context"] = ac
301
+ ctx = _Ctx(ctx)
302
+
303
+ def _serializer(r, _ctx=ctx):
304
+ out = _serialize_output(model, alias, target, sp, r)
305
+ temp = getattr(_ctx, "temp", {}) if isinstance(_ctx, Mapping) else {}
306
+ extras = (
307
+ temp.get("response_extras", {}) if isinstance(temp, Mapping) else {}
308
+ )
309
+ if isinstance(out, dict) and isinstance(extras, dict):
310
+ out.update(extras)
311
+ return out
312
+
313
+ ctx["response_serializer"] = _serializer
314
+ phases = _get_phase_chains(model, alias)
315
+ result = await _executor._invoke(
316
+ request=request,
317
+ db=db,
318
+ phases=phases,
319
+ ctx=ctx,
320
+ )
321
+
322
+ if isinstance(result, Response):
323
+ if sp.status_code is not None or result.status_code == 200:
324
+ result.status_code = status_code
325
+ return result
326
+ return result
327
+
328
+ params = [
329
+ inspect.Parameter(
330
+ nv,
331
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
332
+ annotation=Annotated[str, Path(...)],
333
+ )
334
+ for nv in nested_vars
335
+ ]
336
+ params.extend(
337
+ [
338
+ inspect.Parameter(
339
+ "item_id",
340
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
341
+ annotation=Annotated[Any, Path(...)],
342
+ ),
343
+ inspect.Parameter(
344
+ "request",
345
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
346
+ annotation=Request,
347
+ ),
348
+ inspect.Parameter(
349
+ "db",
350
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
351
+ annotation=Annotated[Any, Depends(db_dep)],
352
+ ),
353
+ inspect.Parameter(
354
+ "body",
355
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
356
+ annotation=body_annotation,
357
+ default=body_default,
358
+ ),
359
+ ]
360
+ )
361
+ _endpoint.__signature__ = inspect.Signature(params)
362
+
363
+ _endpoint.__name__ = f"rest_{model.__name__}_{alias}_member"
364
+ _endpoint.__qualname__ = _endpoint.__name__
365
+ _endpoint.__doc__ = f"REST member endpoint for {model.__name__}.{alias} ({target})"
366
+ _endpoint.__annotations__["body"] = body_annotation
367
+ return _endpoint