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,292 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ import inspect
5
+ import re
6
+ from uuid import uuid4
7
+ from typing import Any, Sequence
8
+
9
+ from .collection import _make_collection_endpoint
10
+ from .member import _make_member_endpoint
11
+ from .common import (
12
+ TIGRBL_ALLOW_ANON_ATTR,
13
+ TIGRBL_AUTH_DEP_ATTR,
14
+ TIGRBL_GET_DB_ATTR,
15
+ TIGRBL_REST_DEPENDENCIES_ATTR,
16
+ BaseModel,
17
+ CANON,
18
+ OpSpec,
19
+ Response,
20
+ Router,
21
+ _DEFAULT_METHODS,
22
+ _default_path_suffix,
23
+ _nested_prefix,
24
+ _normalize_deps,
25
+ _normalize_secdeps,
26
+ _optionalize_list_in_model,
27
+ _path_for_spec,
28
+ _req_state_db,
29
+ _resource_name,
30
+ _status,
31
+ _status_for,
32
+ _strip_parent_fields,
33
+ _RESPONSES_META,
34
+ )
35
+ from ...schema import _make_bulk_rows_model
36
+ import typing as _typing
37
+ from typing import get_args as _get_args, get_origin as _get_origin
38
+
39
+ logger = logging.getLogger("uvicorn")
40
+ logger.debug("Loaded module v3/bindings/rest/router")
41
+
42
+
43
+ def _build_router(
44
+ model: type, specs: Sequence[OpSpec], *, api: Any | None = None
45
+ ) -> Router:
46
+ resource = _resource_name(model)
47
+
48
+ # Router-level deps: extra deps only (transport-level; never part of kernel plan)
49
+ extra_router_deps = _normalize_deps(
50
+ getattr(model, TIGRBL_REST_DEPENDENCIES_ATTR, None)
51
+ )
52
+ auth_dep = getattr(model, TIGRBL_AUTH_DEP_ATTR, None)
53
+
54
+ # Verbs explicitly allowed without auth
55
+ allow_anon_attr = getattr(model, TIGRBL_ALLOW_ANON_ATTR, None)
56
+ allow_anon = set(
57
+ allow_anon_attr() if callable(allow_anon_attr) else allow_anon_attr or []
58
+ )
59
+
60
+ router = Router(dependencies=extra_router_deps or None)
61
+
62
+ pk_param = "item_id"
63
+ db_dep = getattr(model, TIGRBL_GET_DB_ATTR, None) or _req_state_db
64
+
65
+ raw_nested = _nested_prefix(model) or ""
66
+ nested_pref = re.sub(r"/{2,}", "/", raw_nested).rstrip("/") or ""
67
+ nested_vars = re.findall(r"{(\w+)}", raw_nested)
68
+
69
+ # When models are mounted on nested paths, parent identifiers should not
70
+ # appear in request schemas. Capture the original spec sequence so we can
71
+ # prune request models even if some specs (e.g. ``create`` when
72
+ # ``bulk_create`` is present) are later dropped from the router.
73
+ all_specs = list(specs)
74
+
75
+ if nested_vars:
76
+ schemas_root = getattr(model, "schemas", None)
77
+ if schemas_root:
78
+ for sp in all_specs:
79
+ alias_ns = getattr(schemas_root, sp.alias, None)
80
+ if not alias_ns:
81
+ continue
82
+ in_model = getattr(alias_ns, "in_", None)
83
+ if (
84
+ in_model
85
+ and inspect.isclass(in_model)
86
+ and issubclass(in_model, BaseModel)
87
+ ):
88
+ root_field = getattr(in_model, "model_fields", {}).get("root")
89
+ if root_field is not None:
90
+ ann = root_field.annotation
91
+ inner = None
92
+ for t in _get_args(ann) or (ann,):
93
+ origin = _get_origin(t)
94
+ if origin in {list, _typing.List}:
95
+ t_args = _get_args(t)
96
+ if t_args:
97
+ t = t_args[0]
98
+ origin = _get_origin(t)
99
+ if inspect.isclass(t) and issubclass(t, BaseModel):
100
+ inner = t
101
+ break
102
+ if inner is not None:
103
+ pruned = _strip_parent_fields(inner, drop=set(nested_vars))
104
+ setattr(alias_ns, "in_item", pruned)
105
+ setattr(
106
+ alias_ns,
107
+ "in_",
108
+ _make_bulk_rows_model(model, sp.target, pruned),
109
+ )
110
+ continue
111
+ pruned = _strip_parent_fields(in_model, drop=set(nested_vars))
112
+ setattr(alias_ns, "in_", pruned)
113
+
114
+ # If bulk_delete is present, drop clear to avoid route conflicts
115
+ if any(sp.target == "bulk_delete" for sp in specs):
116
+ specs = [sp for sp in specs if sp.target != "clear"]
117
+
118
+ # When both ``create`` and ``bulk_create`` handlers are available,
119
+ # prefer ``bulk_create`` for the REST route to avoid conflicting POST
120
+ # registrations at the collection path. Both operations remain bound
121
+ # for schema generation, but only ``bulk_create`` should surface as a
122
+ # REST endpoint and in the OpenAPI spec.
123
+ if any(sp.target == "bulk_create" for sp in specs) and any(
124
+ sp.target == "create" for sp in specs
125
+ ):
126
+ specs = [sp for sp in specs if sp.target != "create"]
127
+
128
+ # Register collection-level bulk routes before member routes so static paths
129
+ # like "/resource/bulk" aren't captured by dynamic member routes such as
130
+ # "/resource/{item_id}". FastAPI matches routes in the order they are
131
+ # added, so sorting here prevents "bulk" from being treated as an
132
+ # identifier.
133
+ specs = sorted(
134
+ specs,
135
+ key=lambda sp: (
136
+ -1
137
+ if sp.target == "clear"
138
+ else 0
139
+ if sp.target in {"bulk_update", "bulk_replace", "bulk_delete", "bulk_merge"}
140
+ else 1
141
+ if sp.target in {"create", "merge"}
142
+ else 2
143
+ if sp.target in {"bulk_create"}
144
+ else 3
145
+ ),
146
+ )
147
+
148
+ for sp in specs:
149
+ if not sp.expose_routes:
150
+ continue
151
+
152
+ # Determine path and membership
153
+ if nested_pref:
154
+ if sp.path_suffix is None:
155
+ suffix = _default_path_suffix(sp) or ""
156
+ else:
157
+ suffix = sp.path_suffix or ""
158
+ if suffix and not suffix.startswith("/"):
159
+ suffix = "/" + suffix
160
+ base = nested_pref.rstrip("/")
161
+ if not base.endswith(f"/{resource}"):
162
+ base = f"{base}/{resource}"
163
+ if sp.arity == "member" or sp.target in {
164
+ "read",
165
+ "update",
166
+ "replace",
167
+ "merge",
168
+ "delete",
169
+ }:
170
+ path = f"{base}/{{{pk_param}}}{suffix}"
171
+ is_member = True
172
+ else:
173
+ path = f"{base}{suffix}"
174
+ is_member = False
175
+ else:
176
+ path, is_member = _path_for_spec(
177
+ model, sp, resource=resource, pk_param=pk_param
178
+ )
179
+
180
+ # HARDEN list.in_ at runtime to avoid bogus defaults blowing up empty GETs
181
+ if sp.target == "list":
182
+ schemas_root = getattr(model, "schemas", None)
183
+ if schemas_root:
184
+ alias_ns = getattr(schemas_root, sp.alias, None)
185
+ if alias_ns:
186
+ in_model = getattr(alias_ns, "in_", None)
187
+ if (
188
+ in_model
189
+ and inspect.isclass(in_model)
190
+ and issubclass(in_model, BaseModel)
191
+ and not getattr(in_model, "__tigrbl_optionalized__", False)
192
+ ):
193
+ safe = _optionalize_list_in_model(in_model)
194
+ setattr(alias_ns, "in_", safe)
195
+
196
+ # HTTP methods
197
+ methods = list(sp.http_methods or _DEFAULT_METHODS.get(sp.target, ("POST",)))
198
+ response_model = None # Allow hooks to mutate response freely
199
+
200
+ # Build endpoint (split by body/no-body)
201
+ if is_member:
202
+ endpoint = _make_member_endpoint(
203
+ model,
204
+ sp,
205
+ resource=resource,
206
+ db_dep=db_dep,
207
+ pk_param=pk_param,
208
+ nested_vars=nested_vars,
209
+ api=api,
210
+ )
211
+ else:
212
+ endpoint = _make_collection_endpoint(
213
+ model,
214
+ sp,
215
+ resource=resource,
216
+ db_dep=db_dep,
217
+ nested_vars=nested_vars,
218
+ api=api,
219
+ )
220
+
221
+ # Status codes
222
+ status_code = _status_for(sp)
223
+
224
+ # Capture OUT schema for OpenAPI without enforcing runtime validation
225
+ alias_ns = getattr(getattr(model, "schemas", None), sp.alias, None)
226
+ out_model = getattr(alias_ns, "out", None) if alias_ns else None
227
+
228
+ responses_meta = dict(_RESPONSES_META)
229
+ if out_model is not None and status_code != _status.HTTP_204_NO_CONTENT:
230
+ responses_meta[status_code] = {"model": out_model}
231
+ response_class = None
232
+ else:
233
+ responses_meta[status_code] = {"description": "Successful Response"}
234
+ response_class = Response
235
+
236
+ # Attach route
237
+ label = f"{model.__name__} - {sp.alias}"
238
+ route_deps = None
239
+ if auth_dep and sp.alias not in allow_anon and sp.target not in allow_anon:
240
+ route_deps = _normalize_deps([auth_dep])
241
+
242
+ unique_id = f"{endpoint.__name__}_{uuid4().hex}"
243
+ include_in_schema = bool(
244
+ getattr(sp, "extra", {}).get("include_in_schema", True)
245
+ )
246
+ route_kwargs = dict(
247
+ path=path,
248
+ endpoint=endpoint,
249
+ methods=methods,
250
+ name=f"{model.__name__}.{sp.alias}",
251
+ operation_id=unique_id,
252
+ summary=label,
253
+ description=label,
254
+ response_model=response_model,
255
+ status_code=status_code,
256
+ # IMPORTANT: only class name here; never table name
257
+ tags=list(sp.tags or (model.__name__,)),
258
+ responses=responses_meta,
259
+ include_in_schema=include_in_schema,
260
+ )
261
+ if route_deps:
262
+ route_kwargs["dependencies"] = route_deps
263
+ if response_class is not None:
264
+ route_kwargs["response_class"] = response_class
265
+
266
+ secdeps: list[Any] = []
267
+ if auth_dep and sp.alias not in allow_anon and sp.target not in allow_anon:
268
+ secdeps.append(auth_dep)
269
+ secdeps.extend(getattr(sp, "secdeps", ()))
270
+ route_secdeps = _normalize_secdeps(secdeps)
271
+ if route_secdeps:
272
+ route_kwargs["dependencies"] = route_secdeps
273
+
274
+ if (
275
+ sp.alias != sp.target
276
+ and sp.target in CANON
277
+ and sp.alias != getattr(sp.handler, "__name__", sp.alias)
278
+ ):
279
+ route_kwargs["include_in_schema"] = False
280
+
281
+ router.add_api_route(**route_kwargs)
282
+
283
+ logger.debug(
284
+ "rest: registered %s %s -> %s.%s (response_model=%s)",
285
+ methods,
286
+ path,
287
+ model.__name__,
288
+ sp.alias,
289
+ getattr(response_model, "__name__", None) if response_model else None,
290
+ )
291
+
292
+ return router
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ from types import SimpleNamespace
5
+ from typing import Any, Dict, Optional, Sequence, Tuple
6
+
7
+
8
+ from .fastapi import Depends, Security, _status
9
+ from ...op import OpSpec
10
+ from ...op.types import CANON
11
+
12
+ logger = logging.getLogger("uvicorn")
13
+ logger.debug("Loaded module v3/bindings/rest/routing")
14
+
15
+
16
+ def _normalize_deps(deps: Optional[Sequence[Any]]) -> list[Any]:
17
+ """Turn callables into Depends(...) unless already a dependency object."""
18
+ if not deps:
19
+ return []
20
+ out: list[Any] = []
21
+ for d in deps:
22
+ is_dep_obj = getattr(d, "dependency", None) is not None
23
+ out.append(d if is_dep_obj else Depends(d))
24
+ return out
25
+
26
+
27
+ def _normalize_secdeps(secdeps: Optional[Sequence[Any]]) -> list[Any]:
28
+ """Turn callables into Security(...) unless already a dependency object."""
29
+ if not secdeps:
30
+ return []
31
+ out: list[Any] = []
32
+ for d in secdeps:
33
+ is_dep_obj = getattr(d, "dependency", None) is not None
34
+ out.append(d if is_dep_obj else Security(d))
35
+ return out
36
+
37
+
38
+ def _status_for(sp: OpSpec) -> int:
39
+ if sp.status_code is not None:
40
+ return sp.status_code
41
+ target = sp.target
42
+ if target == "create":
43
+ return _status.HTTP_201_CREATED
44
+ if target in ("delete", "clear"):
45
+ return _status.HTTP_200_OK
46
+ return _status.HTTP_200_OK
47
+
48
+
49
+ _RESPONSES_META = {
50
+ 400: {"description": "Bad Request"},
51
+ 401: {"description": "Unauthorized"},
52
+ 403: {"description": "Forbidden"},
53
+ 404: {"description": "Not Found"},
54
+ 409: {"description": "Conflict"},
55
+ 422: {"description": "Unprocessable Entity"},
56
+ 429: {"description": "Too Many Requests"},
57
+ 500: {"description": "Internal Server Error"},
58
+ }
59
+
60
+
61
+ _DEFAULT_METHODS: Dict[str, Tuple[str, ...]] = {
62
+ "create": ("POST",),
63
+ "read": ("GET",),
64
+ "update": ("PATCH",),
65
+ "replace": ("PUT",),
66
+ "merge": ("PATCH",),
67
+ "delete": ("DELETE",),
68
+ "list": ("GET",),
69
+ "clear": ("DELETE",),
70
+ "bulk_create": ("POST",),
71
+ "bulk_update": ("PATCH",),
72
+ "bulk_replace": ("PUT",),
73
+ "bulk_merge": ("PATCH",),
74
+ "bulk_delete": ("DELETE",),
75
+ "custom": ("POST",),
76
+ }
77
+
78
+
79
+ def _default_path_suffix(sp: OpSpec) -> str | None:
80
+ if sp.target.startswith("bulk_"):
81
+ return None
82
+ if sp.alias != sp.target and (
83
+ sp.target in {"create", "custom"} or sp.target not in CANON
84
+ ):
85
+ return f"/{sp.alias}"
86
+ return None
87
+
88
+
89
+ def _path_for_spec(
90
+ model: type, sp: OpSpec, *, resource: str, pk_param: str = "item_id"
91
+ ) -> Tuple[str, bool]:
92
+ if sp.path_suffix is None:
93
+ suffix = _default_path_suffix(sp) or ""
94
+ else:
95
+ suffix = sp.path_suffix or ""
96
+ if suffix and not suffix.startswith("/"):
97
+ suffix = "/" + suffix
98
+
99
+ if sp.target == "create":
100
+ return f"/{resource}{suffix}", False
101
+ if sp.arity == "member" or sp.target in {
102
+ "read",
103
+ "update",
104
+ "replace",
105
+ "merge",
106
+ "delete",
107
+ }:
108
+ return f"/{resource}/{{{pk_param}}}{suffix}", True
109
+ return f"/{resource}{suffix}", False
110
+
111
+
112
+ def _response_model_for(sp: OpSpec, model: type) -> Any | None:
113
+ if sp.target == "delete":
114
+ return None
115
+ alias_ns = getattr(
116
+ getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
117
+ )
118
+ out_model = getattr(alias_ns, "out", None)
119
+ if out_model is None:
120
+ return None
121
+ if sp.target == "list":
122
+ try:
123
+ return list[out_model] # type: ignore[index]
124
+ except Exception:
125
+ return None
126
+ return out_model
127
+
128
+
129
+ def _request_model_for(sp: OpSpec, model: type) -> Any | None:
130
+ alias_ns = getattr(
131
+ getattr(model, "schemas", None) or SimpleNamespace(), sp.alias, None
132
+ )
133
+ return getattr(alias_ns, "in_", None)