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,314 @@
1
+ # tigrbl/v3/app/tigrbl_app.py
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+ from types import SimpleNamespace
6
+ from typing import (
7
+ Any,
8
+ Callable,
9
+ Dict,
10
+ Iterable,
11
+ Mapping,
12
+ Optional,
13
+ Sequence,
14
+ Tuple,
15
+ )
16
+
17
+ from ._app import App as _App
18
+ from ..engine.engine_spec import EngineCfg
19
+ from ..engine import resolver as _resolver
20
+ from ..ddl import initialize as _ddl_initialize
21
+ from ..bindings.api import (
22
+ include_model as _include_model,
23
+ include_models as _include_models,
24
+ rpc_call as _rpc_call,
25
+ _seed_security_and_deps,
26
+ _mount_router,
27
+ _default_prefix,
28
+ AttrDict,
29
+ )
30
+ from ..bindings.model import rebind as _rebind, bind as _bind
31
+ from ..bindings.rest import build_router_and_attach as _build_router_and_attach
32
+ from ..transport import mount_jsonrpc as _mount_jsonrpc
33
+ from ..system import mount_diagnostics as _mount_diagnostics
34
+ from ..op import get_registry, OpSpec
35
+
36
+
37
+ # optional compat: legacy transactional decorator
38
+ try:
39
+ from .compat.transactional import transactional as _txn_decorator
40
+ except Exception: # pragma: no cover
41
+ _txn_decorator = None
42
+
43
+
44
+ class TigrblApp(_App):
45
+ """
46
+ Monolithic facade that owns:
47
+ • containers (models, schemas, handlers, hooks, rpc, rest, routers, columns, table_config, core proxies)
48
+ • model inclusion (REST + RPC wiring)
49
+ • JSON-RPC / diagnostics mounting
50
+ • (optional) legacy-friendly helpers (transactional decorator, auth flags)
51
+
52
+ It composes v3 primitives; you can still use the functions directly if you prefer.
53
+ """
54
+
55
+ TITLE = "TigrblApp"
56
+ VERSION = "0.1.0"
57
+ LIFESPAN = None
58
+ MIDDLEWARES: Sequence[Any] = ()
59
+ APIS: Sequence[Any] = ()
60
+ MODELS: Sequence[Any] = ()
61
+
62
+ # --- optional auth knobs recognized by some middlewares/dispatchers (kept for back-compat) ---
63
+ _authn: Any = None
64
+ _allow_anon: bool = True
65
+ _authorize: Any = None
66
+ _optional_authn_dep: Any = None
67
+ _allow_anon_ops: set[str] = set()
68
+
69
+ def __init__(
70
+ self,
71
+ *,
72
+ engine: EngineCfg | None = None,
73
+ jsonrpc_prefix: str = "/rpc",
74
+ system_prefix: str = "/system",
75
+ api_hooks: Mapping[str, Iterable[Callable]]
76
+ | Mapping[str, Mapping[str, Iterable[Callable]]]
77
+ | None = None,
78
+ **fastapi_kwargs: Any,
79
+ ) -> None:
80
+ title = fastapi_kwargs.pop("title", None)
81
+ if title is not None:
82
+ self.TITLE = title
83
+ version = fastapi_kwargs.pop("version", None)
84
+ if version is not None:
85
+ self.VERSION = version
86
+ lifespan = fastapi_kwargs.pop("lifespan", None)
87
+ if lifespan is not None:
88
+ self.LIFESPAN = lifespan
89
+ super().__init__(engine=engine, **fastapi_kwargs)
90
+ # capture initial routes so refreshes retain FastAPI defaults
91
+ self._base_routes = list(self.router.routes)
92
+ self.jsonrpc_prefix = jsonrpc_prefix
93
+ self.system_prefix = system_prefix
94
+
95
+ # public containers (mirrors used by bindings.api)
96
+ self.models: Dict[str, type] = {}
97
+ self.schemas = SimpleNamespace()
98
+ self.handlers = SimpleNamespace()
99
+ self.hooks = SimpleNamespace()
100
+ self.rpc = SimpleNamespace()
101
+ self.rest = SimpleNamespace()
102
+ self.routers: Dict[str, Any] = {}
103
+ self.tables = AttrDict()
104
+ self.columns: Dict[str, Tuple[str, ...]] = {}
105
+ self.table_config: Dict[str, Dict[str, Any]] = {}
106
+ self.core = SimpleNamespace()
107
+ self.core_raw = SimpleNamespace()
108
+
109
+ # API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
110
+ self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
111
+
112
+ # ------------------------- internal helpers -------------------------
113
+
114
+ @staticmethod
115
+ def _merge_api_hooks_into_model(model: type, hooks_map: Any) -> None:
116
+ """
117
+ Install API-level hooks on the model so the binder can see them.
118
+ Accepted shapes:
119
+ {phase: [fn, ...]} # global, all aliases
120
+ {alias: {phase: [fn, ...]}, "*": {...}} # per-alias + wildcard
121
+ If the model already has __tigrbl_api_hooks__, we shallow-merge keys.
122
+ """
123
+ if not hooks_map:
124
+ return
125
+ existing = getattr(model, "__tigrbl_api_hooks__", None)
126
+ if existing is None:
127
+ setattr(model, "__tigrbl_api_hooks__", copy.deepcopy(hooks_map))
128
+ return
129
+
130
+ # shallow merge (alias or phase keys); values are lists we extend
131
+ merged = copy.deepcopy(existing)
132
+ for k, v in (hooks_map or {}).items():
133
+ if k not in merged:
134
+ merged[k] = copy.deepcopy(v)
135
+ else:
136
+ # when both are dicts, merge phase lists
137
+ if isinstance(v, Mapping) and isinstance(merged[k], Mapping):
138
+ for ph, fns in v.items():
139
+ merged[k].setdefault(ph, [])
140
+ merged[k][ph] = list(merged[k][ph]) + list(fns or [])
141
+ else:
142
+ # fallback: prefer model-local value, then append api-level
143
+ if isinstance(merged[k], list):
144
+ merged[k] = list(merged[k]) + list(v or [])
145
+ else:
146
+ merged[k] = v
147
+ setattr(model, "__tigrbl_api_hooks__", merged)
148
+
149
+ # ------------------------- primary operations -------------------------
150
+
151
+ def include_model(
152
+ self, model: type, *, prefix: str | None = None, mount_router: bool = True
153
+ ) -> Tuple[type, Any]:
154
+ """
155
+ Bind a model, mount its REST router, and attach all namespaces to this facade.
156
+ """
157
+ # inject API-level hooks so the binder merges them
158
+ self._merge_api_hooks_into_model(model, self._api_hooks_map)
159
+ return _include_model(self, model, prefix=prefix, mount_router=mount_router)
160
+
161
+ def include_models(
162
+ self,
163
+ models: Sequence[type],
164
+ *,
165
+ base_prefix: str | None = None,
166
+ mount_router: bool = True,
167
+ ) -> Dict[str, Any]:
168
+ for m in models:
169
+ self._merge_api_hooks_into_model(m, self._api_hooks_map)
170
+ return _include_models(
171
+ self,
172
+ models,
173
+ base_prefix=base_prefix,
174
+ mount_router=mount_router,
175
+ )
176
+
177
+ async def rpc_call(
178
+ self,
179
+ model_or_name: type | str,
180
+ method: str,
181
+ payload: Any = None,
182
+ *,
183
+ db: Any,
184
+ request: Any = None,
185
+ ctx: Optional[Dict[str, Any]] = None,
186
+ ) -> Any:
187
+ return await _rpc_call(
188
+ self, model_or_name, method, payload, db=db, request=request, ctx=ctx
189
+ )
190
+
191
+ # ------------------------- extras / mounting -------------------------
192
+
193
+ def mount_jsonrpc(self, *, prefix: str | None = None) -> Any:
194
+ """Mount JSON-RPC router onto this app."""
195
+ px = prefix if prefix is not None else self.jsonrpc_prefix
196
+ prov = _resolver.resolve_provider(api=self)
197
+ get_db = prov.get_db if prov else None
198
+ router = _mount_jsonrpc(
199
+ self,
200
+ self,
201
+ prefix=px,
202
+ get_db=get_db,
203
+ )
204
+ self._base_routes = list(self.router.routes)
205
+ return router
206
+
207
+ def attach_diagnostics(
208
+ self, *, prefix: str | None = None, app: Any | None = None
209
+ ) -> Any:
210
+ """Mount diagnostics router onto this app or the provided ``app``."""
211
+ px = prefix if prefix is not None else self.system_prefix
212
+ prov = _resolver.resolve_provider(api=self)
213
+ get_db = prov.get_db if prov else None
214
+ router = _mount_diagnostics(self, get_db=get_db)
215
+ include_self = getattr(self, "include_router", None)
216
+ if callable(include_self):
217
+ include_self(router, prefix=px)
218
+ if app is not None and app is not self:
219
+ include_other = getattr(app, "include_router", None)
220
+ if callable(include_other):
221
+ include_other(router, prefix=px)
222
+ if app is None:
223
+ self._base_routes = list(self.router.routes)
224
+ return router
225
+
226
+ # ------------------------- registry passthroughs -------------------------
227
+
228
+ def registry(self, model: type):
229
+ """Return the per-model OpspecRegistry."""
230
+ return get_registry(model)
231
+
232
+ def bind(self, model: type) -> Tuple[OpSpec, ...]:
233
+ """Bind/rebuild a model in place (without mounting)."""
234
+ self._merge_api_hooks_into_model(model, self._api_hooks_map)
235
+ return _bind(model)
236
+
237
+ def rebind(
238
+ self, model: type, *, changed_keys: Optional[set[tuple[str, str]]] = None
239
+ ) -> Tuple[OpSpec, ...]:
240
+ """Targeted rebuild of a bound model."""
241
+ return _rebind(model, changed_keys=changed_keys)
242
+
243
+ # ------------------------- legacy helpers -------------------------
244
+
245
+ def transactional(self, *dargs, **dkw):
246
+ """
247
+ Legacy-friendly decorator: @api.transactional(...)
248
+ Wraps a function as a v3 custom op with START_TX/END_TX.
249
+ """
250
+ if _txn_decorator is None:
251
+ raise RuntimeError("transactional decorator not available")
252
+ return _txn_decorator(self, *dargs, **dkw)
253
+
254
+ # Optional: let callers set auth knobs used by some middlewares/dispatchers
255
+ def set_auth(
256
+ self,
257
+ *,
258
+ authn: Any = None,
259
+ allow_anon: Optional[bool] = None,
260
+ authorize: Any = None,
261
+ optional_authn_dep: Any = None,
262
+ ) -> None:
263
+ if authn is not None:
264
+ self._authn = authn
265
+ if allow_anon is not None:
266
+ self._allow_anon = bool(allow_anon)
267
+ if authorize is not None:
268
+ self._authorize = authorize
269
+ if optional_authn_dep is not None:
270
+ self._optional_authn_dep = optional_authn_dep
271
+
272
+ # Refresh already-included models so routers pick up new auth settings
273
+ if self.models:
274
+ self._refresh_security()
275
+
276
+ def _refresh_security(self) -> None:
277
+ """Re-seed auth deps on models and rebuild routers."""
278
+ # Reset router to baseline and allow_anon ops cache
279
+ self.router.routes = list(self._base_routes)
280
+ self._allow_anon_ops = set()
281
+ for model in self.models.values():
282
+ _seed_security_and_deps(self, model)
283
+ specs = getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ())
284
+ if specs:
285
+ _build_router_and_attach(model, list(specs))
286
+ router = getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
287
+ if router is None:
288
+ continue
289
+ # update api-level references
290
+ mname = model.__name__
291
+ rest_ns = getattr(self.rest, mname, SimpleNamespace())
292
+ rest_ns.router = router
293
+ setattr(self.rest, mname, rest_ns)
294
+ self.routers[mname] = router
295
+ prefix = _default_prefix(model)
296
+ _mount_router(self, router, prefix=prefix)
297
+
298
+ def _collect_tables(self):
299
+ # dedupe; handle multiple DeclarativeBases (multiple metadatas)
300
+ seen = set()
301
+ tables = []
302
+ for m in self.models.values():
303
+ t = getattr(m, "__table__", None)
304
+ if t is not None and t not in seen:
305
+ seen.add(t)
306
+ tables.append(t)
307
+ return tables
308
+
309
+ initialize = _ddl_initialize
310
+
311
+ # ------------------------- repr -------------------------
312
+
313
+ def __repr__(self) -> str: # pragma: no cover
314
+ return f"<TigrblApp models={list(self.models)} rpc={list(getattr(self.rpc, '__dict__', {}).keys())}>"
@@ -0,0 +1,73 @@
1
+ # tigrbl/v3/bindings/__init__.py
2
+ """
3
+ Tigrbl v3 – Bindings package.
4
+
5
+ This package wires OpSpec-derived artifacts onto models and an API facade.
6
+ Concerns are kept strictly separated:
7
+ • Security deps & extra deps → transport/router only (REST/RPC)
8
+ • System steps → injected by runtime lifecycle (Kernel), not by bindings
9
+ • Atoms → discovered/injected by runtime, not by bindings
10
+ • Hooks → bindable at API / model / op levels (no imperative hooks)
11
+
12
+ Public surface (re-exports)
13
+
14
+ Model binding:
15
+ - bind(model, *, only_keys=None) → builds/refreshes model namespaces
16
+ - rebind(model, *, changed_keys=None) → targeted refresh
17
+
18
+ Per-concern builders:
19
+ - build_schemas(model, specs, *, only_keys=None)
20
+ • Seeds schemas declared via @schema_ctx before computing defaults.
21
+ • Supports overrides via SchemaRef("alias","in|out"), "alias.in"/"alias.out", or "raw".
22
+ - build_hooks(model, specs, *, only_keys=None)
23
+ • Merges API → MODEL → OP for pre-like phases; OP → MODEL → API for post/error.
24
+ • No imperative hook source.
25
+ - build_handlers(model, specs, *, only_keys=None)
26
+ • Inserts the core/raw step into the HANDLER phase (other phases handled by runtime).
27
+ - register_rpc(model, specs, *, only_keys=None)
28
+ - build_rest(model, specs, *, only_keys=None)
29
+ • Router-level only; serializes responses iff a response schema exists.
30
+
31
+ API integration:
32
+ - include_model(api, model, *, app=None, prefix=None, mount_router=True)
33
+ - include_models(api, models, *, app=None, base_prefix=None, mount_router=True)
34
+ - rpc_call(api, model_or_name, method, payload=None, *, db, request=None, ctx=None)
35
+ """
36
+
37
+ from __future__ import annotations
38
+ import logging
39
+
40
+ # Core model orchestrator
41
+ from .model import bind, rebind
42
+
43
+ # Per-concern builders (aliased for a clean public API)
44
+ from .schemas import build_and_attach as build_schemas
45
+ from .hooks import normalize_and_attach as build_hooks
46
+ from .handlers import build_and_attach as build_handlers
47
+ from .rpc import register_and_attach as register_rpc
48
+ from .rest import build_router_and_attach as build_rest
49
+ from ..response.bind import bind as bind_response
50
+
51
+ # API facade integration
52
+ from .api import include_model, include_models, rpc_call
53
+
54
+ logger = logging.getLogger("uvicorn")
55
+ logger.debug("Loaded module v3/bindings/__init__")
56
+
57
+
58
+ __all__ = [
59
+ # model orchestrator
60
+ "bind",
61
+ "rebind",
62
+ # per-concern builders
63
+ "build_schemas",
64
+ "build_hooks",
65
+ "build_handlers",
66
+ "register_rpc",
67
+ "build_rest",
68
+ "bind_response",
69
+ # api integration
70
+ "include_model",
71
+ "include_models",
72
+ "rpc_call",
73
+ ]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ from .common import AttrDict, _default_prefix, _mount_router # noqa: F401
5
+ from .include import include_model, include_models, _seed_security_and_deps # noqa: F401
6
+ from .rpc import rpc_call
7
+
8
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
9
+ logger = logging.getLogger("uvicorn")
10
+ logger.debug("Loaded module v3/bindings/api/__init__")
11
+
12
+ __all__ = ["include_model", "include_models", "rpc_call"]
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from types import SimpleNamespace
5
+ from typing import Any
6
+
7
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.debug("Loaded module v3/bindings/api/common")
10
+
11
+
12
+ class AttrDict(dict):
13
+ """Dictionary providing attribute-style access."""
14
+
15
+ def __getattr__(self, item: str) -> Any: # pragma: no cover - trivial
16
+ try:
17
+ return self[item]
18
+ except KeyError as e: # pragma: no cover - debug aid
19
+ raise AttributeError(item) from e
20
+
21
+ def __setattr__(self, key: str, value: Any) -> None: # pragma: no cover - trivial
22
+ self[key] = value
23
+
24
+
25
+ # Public type for the API facade object users pass to include_model(...)
26
+ ApiLike = Any
27
+
28
+
29
+ def _resource_name(model: type) -> str:
30
+ """
31
+ Compute the API resource segment.
32
+
33
+ Policy:
34
+ - Prefer explicit `__resource__` when present (caller-controlled).
35
+ - Otherwise, use the model *class name* in lowercase.
36
+ - DO NOT use `__tablename__` here (strictly DB-only per project policy).
37
+ """
38
+ if hasattr(model, "__resource__"):
39
+ resource = model.__resource__
40
+ logger.debug("Using explicit resource '%s' for %s", resource, model.__name__)
41
+ else:
42
+ resource = model.__name__.lower()
43
+ logger.debug("Derived resource '%s' for %s", resource, model.__name__)
44
+ return resource
45
+
46
+
47
+ def _default_prefix(model: type) -> str:
48
+ """Default mount prefix for a model router.
49
+
50
+ Historically routers were mounted under ``/{resource}``, resulting in
51
+ duplicated path segments such as ``/item/item``. To expose REST endpoints
52
+ under ``/item`` we now mount routers at the application root by default.
53
+ """
54
+ logger.debug("Default prefix for %s is root '/'", model.__name__)
55
+ return ""
56
+
57
+
58
+ def _has_include_router(obj: Any) -> bool:
59
+ has_router = hasattr(obj, "include_router") and callable(
60
+ getattr(obj, "include_router")
61
+ )
62
+ logger.debug("Object %s has include_router: %s", obj, has_router)
63
+ return has_router
64
+
65
+
66
+ def _mount_router(app_or_router: Any, router: Any, *, prefix: str) -> None:
67
+ """
68
+ Best-effort mount onto a FastAPI app or Router.
69
+ If not available, we still attach router under api.routers for later use.
70
+ """
71
+ if app_or_router is None:
72
+ logger.debug("No app/router; skipping mount for prefix %s", prefix)
73
+ return
74
+ try:
75
+ if _has_include_router(app_or_router):
76
+ logger.debug("Mounting router %s at prefix %s", router, prefix)
77
+ app_or_router.include_router(router, prefix=prefix) # FastAPI / Router
78
+ else:
79
+ logger.debug(
80
+ "Provided object %s lacks include_router; not mounting router",
81
+ app_or_router,
82
+ )
83
+ except Exception:
84
+ logger.exception("Failed to mount router at %s", prefix)
85
+
86
+
87
+ def _ensure_api_ns(api: ApiLike) -> None:
88
+ """
89
+ Ensure containers exist on the api facade object.
90
+ """
91
+ for attr, default in (
92
+ ("models", {}),
93
+ ("tables", AttrDict()),
94
+ ("schemas", SimpleNamespace()),
95
+ ("handlers", SimpleNamespace()),
96
+ ("hooks", SimpleNamespace()),
97
+ ("rpc", SimpleNamespace()),
98
+ ("rest", SimpleNamespace()),
99
+ ("routers", {}),
100
+ ("columns", {}),
101
+ ("table_config", {}),
102
+ ("core", SimpleNamespace()), # helper method proxies
103
+ ("core_raw", SimpleNamespace()),
104
+ ):
105
+ if not hasattr(api, attr):
106
+ setattr(api, attr, default)
107
+ logger.debug("Initialized api.%s", attr)
108
+ else:
109
+ logger.debug("api already has attribute %s", attr)