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,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,49 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from typing import Mapping, Any
5
+
6
+ from fastapi import Header
7
+
8
+
9
+ def _build_signature_with_header_params(
10
+ hdr_fields: list[tuple[str, str, bool]],
11
+ ) -> inspect.Signature:
12
+ params: list[inspect.Parameter] = []
13
+ for _field, header, required in hdr_fields:
14
+ param = header.lower().replace("-", "_")
15
+ default = Header(... if required else None, alias=header)
16
+ params.append(
17
+ inspect.Parameter(
18
+ name=param,
19
+ kind=inspect.Parameter.KEYWORD_ONLY,
20
+ default=default,
21
+ annotation=str | None,
22
+ )
23
+ )
24
+ return inspect.Signature(parameters=params, return_annotation=Mapping[str, object])
25
+
26
+
27
+ def _make_header_dep(model: type, alias: str):
28
+ hdr_fields: list[tuple[str, str, bool]] = []
29
+ for name, spec in getattr(model, "__tigrbl_cols__", {}).items():
30
+ io = getattr(spec, "io", None)
31
+ if not io or not getattr(io, "header_in", None):
32
+ continue
33
+ if alias not in set(getattr(io, "in_verbs", ()) or ()): # honor IO.in_verbs
34
+ continue
35
+ hdr_fields.append(
36
+ (name, io.header_in, bool(getattr(io, "header_required_in", False)))
37
+ )
38
+
39
+ async def _dep(**kw: Any) -> Mapping[str, object]:
40
+ out: dict[str, object] = {}
41
+ for field, header, _req in hdr_fields:
42
+ param = header.lower().replace("-", "_")
43
+ v = kw.get(param)
44
+ if v is not None:
45
+ out[field] = v
46
+ return out
47
+
48
+ _dep.__signature__ = _build_signature_with_header_params(hdr_fields)
49
+ return _dep