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,291 @@
1
+ # tigrbl/v3/api/tigrbl_api.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 ._api import Api as _Api
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
+ from ..app._model_registry import initialize_model_registry
36
+
37
+
38
+ class TigrblApi(_Api):
39
+ """
40
+ Canonical router-focused facade that owns:
41
+ • containers (models, schemas, handlers, hooks, rpc, rest, routers, columns, table_config, core proxies)
42
+ • model inclusion (REST + RPC wiring)
43
+ • JSON-RPC / diagnostics mounting
44
+ • (optional) auth knobs recognized by some middlewares/dispatchers
45
+
46
+ It composes v3 primitives; you can still use the functions directly if you prefer.
47
+ """
48
+
49
+ PREFIX = ""
50
+ TAGS: Sequence[Any] = ()
51
+ APIS: Sequence[Any] = ()
52
+ MODELS: Sequence[Any] = ()
53
+
54
+ # --- optional auth knobs recognized by some middlewares/dispatchers (kept for back-compat) ---
55
+ _authn: Any = None
56
+ _allow_anon: bool = True
57
+ _authorize: Any = None
58
+ _optional_authn_dep: Any = None
59
+ _allow_anon_ops: set[str] = set()
60
+
61
+ def __init__(
62
+ self,
63
+ *,
64
+ engine: EngineCfg | None = None,
65
+ jsonrpc_prefix: str = "/rpc",
66
+ system_prefix: str = "/system",
67
+ api_hooks: Mapping[str, Iterable[Callable]]
68
+ | Mapping[str, Mapping[str, Iterable[Callable]]]
69
+ | None = None,
70
+ **router_kwargs: Any,
71
+ ) -> None:
72
+ _Api.__init__(self, engine=engine, **router_kwargs)
73
+ self.jsonrpc_prefix = jsonrpc_prefix
74
+ self.system_prefix = system_prefix
75
+
76
+ # public containers (mirrors used by bindings.api)
77
+ self.models = initialize_model_registry(getattr(self, "MODELS", ()))
78
+ self.schemas = SimpleNamespace()
79
+ self.handlers = SimpleNamespace()
80
+ self.hooks = SimpleNamespace()
81
+ self.rpc = SimpleNamespace()
82
+ self.rest = SimpleNamespace()
83
+ self.routers: Dict[str, Any] = {}
84
+ self.tables = AttrDict()
85
+ self.columns: Dict[str, Tuple[str, ...]] = {}
86
+ self.table_config: Dict[str, Dict[str, Any]] = {}
87
+ self.core = SimpleNamespace()
88
+ self.core_raw = SimpleNamespace()
89
+
90
+ # API-level hooks map (merged into each model at include-time; precedence handled in bindings.hooks)
91
+ self._api_hooks_map = copy.deepcopy(api_hooks) if api_hooks else None
92
+
93
+ # ------------------------- internal helpers -------------------------
94
+
95
+ @staticmethod
96
+ def _merge_api_hooks_into_model(model: type, hooks_map: Any) -> None:
97
+ """
98
+ Install API-level hooks on the model so the binder can see them.
99
+ Accepted shapes:
100
+ {phase: [fn, ...]} # global, all aliases
101
+ {alias: {phase: [fn, ...]}, "*": {...}} # per-alias + wildcard
102
+ If the model already has __tigrbl_api_hooks__, we shallow-merge keys.
103
+ """
104
+ if not hooks_map:
105
+ return
106
+ existing = getattr(model, "__tigrbl_api_hooks__", None)
107
+ if existing is None:
108
+ setattr(model, "__tigrbl_api_hooks__", copy.deepcopy(hooks_map))
109
+ return
110
+
111
+ # shallow merge (alias or phase keys); values are lists we extend
112
+ merged = copy.deepcopy(existing)
113
+ for k, v in (hooks_map or {}).items():
114
+ if k not in merged:
115
+ merged[k] = copy.deepcopy(v)
116
+ else:
117
+ # when both are dicts, merge phase lists
118
+ if isinstance(v, Mapping) and isinstance(merged[k], Mapping):
119
+ for ph, fns in v.items():
120
+ merged[k].setdefault(ph, [])
121
+ merged[k][ph] = list(merged[k][ph]) + list(fns or [])
122
+ else:
123
+ # fallback: prefer model-local value, then append api-level
124
+ if isinstance(merged[k], list):
125
+ merged[k] = list(merged[k]) + list(v or [])
126
+ else:
127
+ merged[k] = v
128
+ setattr(model, "__tigrbl_api_hooks__", merged)
129
+
130
+ # ------------------------- primary operations -------------------------
131
+
132
+ def include_model(
133
+ self, model: type, *, prefix: str | None = None, mount_router: bool = True
134
+ ) -> Tuple[type, Any]:
135
+ """
136
+ Bind a model, mount its REST router, and attach all namespaces to this facade.
137
+ """
138
+ # inject API-level hooks so the binder merges them
139
+ self._merge_api_hooks_into_model(model, self._api_hooks_map)
140
+ return _include_model(
141
+ self, model, app=None, prefix=prefix, mount_router=mount_router
142
+ )
143
+
144
+ def include_models(
145
+ self,
146
+ models: Sequence[type],
147
+ *,
148
+ base_prefix: str | None = None,
149
+ mount_router: bool = True,
150
+ ) -> Dict[str, Any]:
151
+ for m in models:
152
+ self._merge_api_hooks_into_model(m, self._api_hooks_map)
153
+ return _include_models(
154
+ self,
155
+ models,
156
+ app=None,
157
+ base_prefix=base_prefix,
158
+ mount_router=mount_router,
159
+ )
160
+
161
+ async def rpc_call(
162
+ self,
163
+ model_or_name: type | str,
164
+ method: str,
165
+ payload: Any = None,
166
+ *,
167
+ db: Any,
168
+ request: Any = None,
169
+ ctx: Optional[Dict[str, Any]] = None,
170
+ ) -> Any:
171
+ return await _rpc_call(
172
+ self, model_or_name, method, payload, db=db, request=request, ctx=ctx
173
+ )
174
+
175
+ # ------------------------- extras / mounting -------------------------
176
+
177
+ def mount_jsonrpc(self, *, prefix: str | None = None) -> Any:
178
+ """Mount a JSON-RPC router onto this TigrblApi instance."""
179
+ px = prefix if prefix is not None else self.jsonrpc_prefix
180
+ prov = _resolver.resolve_provider(api=self)
181
+ get_db = prov.get_db if prov else None
182
+ router = _mount_jsonrpc(
183
+ self,
184
+ self,
185
+ prefix=px,
186
+ get_db=get_db,
187
+ )
188
+ return router
189
+
190
+ def attach_diagnostics(
191
+ self, *, prefix: str | None = None, app: Any | None = None
192
+ ) -> Any:
193
+ """Mount a diagnostics router onto this TigrblApi instance or ``app``."""
194
+ px = prefix if prefix is not None else self.system_prefix
195
+ prov = _resolver.resolve_provider(api=self)
196
+ get_db = prov.get_db if prov else None
197
+ router = _mount_diagnostics(self, get_db=get_db)
198
+ include_self = getattr(self, "include_router", None)
199
+ if callable(include_self):
200
+ include_self(router, prefix=px)
201
+ if app is not None and app is not self:
202
+ include_other = getattr(app, "include_router", None)
203
+ if callable(include_other):
204
+ include_other(router, prefix=px)
205
+ return router
206
+
207
+ # ------------------------- registry passthroughs -------------------------
208
+
209
+ def registry(self, model: type):
210
+ """Return the per-model OpspecRegistry."""
211
+ return get_registry(model)
212
+
213
+ def bind(self, model: type) -> Tuple[OpSpec, ...]:
214
+ """Bind/rebuild a model in place (without mounting)."""
215
+ self._merge_api_hooks_into_model(model, self._api_hooks_map)
216
+ return _bind(model)
217
+
218
+ def rebind(
219
+ self, model: type, *, changed_keys: Optional[set[tuple[str, str]]] = None
220
+ ) -> Tuple[OpSpec, ...]:
221
+ """Targeted rebuild of a bound model."""
222
+ return _rebind(model, changed_keys=changed_keys)
223
+
224
+ # Optional: let callers set auth knobs used by some middlewares/dispatchers
225
+ def set_auth(
226
+ self,
227
+ *,
228
+ authn: Any = None,
229
+ allow_anon: Optional[bool] = None,
230
+ authorize: Any = None,
231
+ optional_authn_dep: Any = None,
232
+ ) -> None:
233
+ if authn is not None:
234
+ self._authn = authn
235
+ if allow_anon is None:
236
+ allow_anon = False
237
+ if allow_anon is not None:
238
+ self._allow_anon = bool(allow_anon)
239
+ if authorize is not None:
240
+ self._authorize = authorize
241
+ if optional_authn_dep is not None:
242
+ self._optional_authn_dep = optional_authn_dep
243
+
244
+ # Refresh already-included models so routers pick up new auth settings
245
+ if self.models:
246
+ self._refresh_security()
247
+
248
+ def _refresh_security(self) -> None:
249
+ """Re-seed auth deps on models and rebuild routers."""
250
+ # Reset routes and allow_anon ops cache
251
+ self.routes = []
252
+ self._allow_anon_ops = set()
253
+ for model in self.models.values():
254
+ _seed_security_and_deps(self, model)
255
+ specs = getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ())
256
+ if specs:
257
+ _build_router_and_attach(model, list(specs))
258
+ router = getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
259
+ if router is None:
260
+ continue
261
+ # update api-level references
262
+ mname = model.__name__
263
+ rest_ns = getattr(self.rest, mname, SimpleNamespace())
264
+ rest_ns.router = router
265
+ setattr(self.rest, mname, rest_ns)
266
+ self.routers[mname] = router
267
+ prefix = _default_prefix(model)
268
+ _mount_router(self, router, prefix=prefix)
269
+
270
+ def _collect_tables(self):
271
+ # dedupe; handle multiple DeclarativeBases (multiple metadatas)
272
+ seen = set()
273
+ tables = []
274
+ for m in self.models.values():
275
+ t = getattr(m, "__table__", None)
276
+ if t is not None and not t.columns:
277
+ continue
278
+ if t is not None and t not in seen:
279
+ seen.add(t)
280
+ tables.append(t)
281
+ return tables
282
+
283
+ initialize = _ddl_initialize
284
+
285
+ # ------------------------- repr -------------------------
286
+
287
+ def __repr__(self) -> str: # pragma: no cover
288
+ models = list(getattr(self, "models", {}))
289
+ rpc_ns = getattr(self, "rpc", None)
290
+ rpc_keys = list(getattr(rpc_ns, "__dict__", {}).keys()) if rpc_ns else []
291
+ return f"<TigrblApi models={models} rpc={rpc_keys}>"
tigrbl/app/__init__.py ADDED
File without changes
tigrbl/app/_app.py ADDED
@@ -0,0 +1,86 @@
1
+ # tigrbl/tigrbl/v3/app/_app.py
2
+ from __future__ import annotations
3
+ from typing import Any
4
+
5
+ from ..deps.fastapi import FastAPI
6
+ from ..engine.engine_spec import EngineCfg
7
+ from ..engine import resolver as _resolver
8
+ from ..engine import install_from_objects
9
+ from ..ddl import initialize as _ddl_initialize
10
+ from ._model_registry import initialize_model_registry
11
+ from .app_spec import AppSpec
12
+
13
+
14
+ class App(AppSpec, FastAPI):
15
+ def __init__(
16
+ self, *, engine: EngineCfg | None = None, **fastapi_kwargs: Any
17
+ ) -> None:
18
+ # Manually mirror ``AppSpec`` fields so the dataclass-generated ``repr``
19
+ # and friends have expected attributes while runtime structures remain
20
+ # mutable dictionaries or lists as needed.
21
+ self.title = self.TITLE
22
+ self.version = self.VERSION
23
+ self.engine = engine if engine is not None else getattr(self, "ENGINE", None)
24
+ self.apis = tuple(getattr(self, "APIS", ()))
25
+ self.ops = tuple(getattr(self, "OPS", ()))
26
+ # Runtime registries use mutable containers (dict/namespace), but the
27
+ # dataclass fields expect sequences. Storing a dict here satisfies both.
28
+ self.models = initialize_model_registry(getattr(self, "MODELS", ()))
29
+ self.schemas = tuple(getattr(self, "SCHEMAS", ()))
30
+ self.hooks = tuple(getattr(self, "HOOKS", ()))
31
+ self.security_deps = tuple(getattr(self, "SECURITY_DEPS", ()))
32
+ self.deps = tuple(getattr(self, "DEPS", ()))
33
+ self.response = getattr(self, "RESPONSE", None)
34
+ self.jsonrpc_prefix = getattr(self, "JSONRPC_PREFIX", "/rpc")
35
+ self.system_prefix = getattr(self, "SYSTEM_PREFIX", "/system")
36
+ self.middlewares = tuple(getattr(self, "MIDDLEWARES", ()))
37
+ self.lifespan = self.LIFESPAN
38
+
39
+ FastAPI.__init__(
40
+ self,
41
+ title=self.title,
42
+ version=self.version,
43
+ lifespan=self.lifespan,
44
+ **fastapi_kwargs,
45
+ )
46
+ _engine_ctx = self.engine
47
+ if _engine_ctx is not None:
48
+ _resolver.set_default(_engine_ctx)
49
+ _resolver.resolve_provider()
50
+ for mw in getattr(self, "MIDDLEWARES", []):
51
+ self.add_middleware(mw.__class__, **getattr(mw, "kwargs", {}))
52
+
53
+ def install_engines(
54
+ self, *, api: Any = None, models: tuple[Any, ...] | None = None
55
+ ) -> None:
56
+ # If class declared APIS/MODELS, use them unless explicit args are passed.
57
+ apis = (api,) if api is not None else self.APIS
58
+ models = models if models is not None else self.MODELS
59
+ if apis:
60
+ for a in apis:
61
+ install_from_objects(app=self, api=a, models=models)
62
+ else:
63
+ install_from_objects(app=self, api=None, models=models)
64
+
65
+ def _collect_tables(self) -> list[Any]:
66
+ seen = set()
67
+ tables = []
68
+ for model in self.models.values():
69
+ if not hasattr(model, "__table__"):
70
+ try: # pragma: no cover - defensive remap
71
+ from ..table import Base
72
+ from ..table._base import _materialize_colspecs_to_sqla
73
+
74
+ _materialize_colspecs_to_sqla(model)
75
+ Base.registry.map_declaratively(model)
76
+ except Exception:
77
+ pass
78
+ table = getattr(model, "__table__", None)
79
+ if table is not None and not table.columns:
80
+ continue
81
+ if table is not None and table not in seen:
82
+ seen.add(table)
83
+ tables.append(table)
84
+ return tables
85
+
86
+ initialize = _ddl_initialize
@@ -0,0 +1,41 @@
1
+ """Utilities for initializing model registries on App and API facades."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Iterable
6
+
7
+
8
+ def initialize_model_registry(models: Iterable[Any]) -> dict[str, Any]:
9
+ """Build the default ``models`` mapping for an App or Api instance.
10
+
11
+ ``defineAppSpec``/``defineApiSpec`` allow authors to declare default models
12
+ using bare model classes or ``("alias", Model)`` tuples. Runtime facades,
13
+ however, expect ``self.models`` to be a dictionary keyed by model name so
14
+ that lookups like ``app.models["Widget"]`` Just Work.
15
+
16
+ This helper normalizes the declared sequence into that dictionary shape and
17
+ preserves declaration order. When an alias is provided we register both the
18
+ alias and the model's ``__name__`` so either lookup style succeeds.
19
+ """
20
+
21
+ registry: dict[str, Any] = {}
22
+
23
+ for entry in models or ():
24
+ # Support ``("Alias", Model)`` declarations in addition to bare models.
25
+ if isinstance(entry, tuple) and len(entry) == 2 and isinstance(entry[0], str):
26
+ alias, model = entry
27
+ registry[alias] = model
28
+ model_name = getattr(model, "__name__", alias)
29
+ registry.setdefault(model_name, model)
30
+ continue
31
+
32
+ model = entry
33
+ model_name = getattr(model, "__name__", None)
34
+ if model_name is None:
35
+ model_name = str(model)
36
+ registry[model_name] = model
37
+
38
+ return registry
39
+
40
+
41
+ __all__ = ["initialize_model_registry"]
tigrbl/app/app_spec.py ADDED
@@ -0,0 +1,42 @@
1
+ # tigrbl/tigrbl/v3/app/app_spec.py
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass, field
4
+ from typing import Any, Callable, Optional, Sequence
5
+
6
+ from ..engine.engine_spec import EngineCfg
7
+ from ..response.types import ResponseSpec
8
+
9
+
10
+ @dataclass(eq=False)
11
+ class AppSpec:
12
+ """
13
+ Used to *produce an App subclass* via App.from_spec().
14
+ """
15
+
16
+ title: str = "Tigrbl"
17
+ version: str = "0.1.0"
18
+ engine: Optional[EngineCfg] = None
19
+
20
+ # NEW: multi-API composition (store API classes or instances)
21
+ apis: Sequence[Any] = field(default_factory=tuple)
22
+
23
+ # NEW: orchestration/topology knobs
24
+ ops: Sequence[Any] = field(default_factory=tuple) # op descriptors or specs
25
+ models: Sequence[Any] = field(default_factory=tuple) # ORM classes
26
+ schemas: Sequence[Any] = field(default_factory=tuple) # schema classes/defs
27
+ hooks: Sequence[Callable[..., Any]] = field(default_factory=tuple)
28
+
29
+ # security/dep stacks (FastAPI dependencies or callables)
30
+ security_deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
31
+ deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
32
+
33
+ # response defaults
34
+ response: Optional[ResponseSpec] = None
35
+
36
+ # system routing prefixes (REST/JSON-RPC namespaces)
37
+ jsonrpc_prefix: str = "/rpc"
38
+ system_prefix: str = "/system"
39
+
40
+ # optional framework bits
41
+ middlewares: Sequence[Any] = field(default_factory=tuple)
42
+ lifespan: Optional[Callable[..., Any]] = None
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from functools import lru_cache
5
+ from typing import Any, Tuple
6
+
7
+ from .app_spec import AppSpec
8
+
9
+ logger = logging.getLogger("uvicorn")
10
+
11
+
12
+ def _merge_seq_attr(app: type, attr: str) -> Tuple[Any, ...]:
13
+ values: list[Any] = []
14
+ for base in reversed(app.__mro__):
15
+ seq = getattr(base, attr, ()) or ()
16
+ try:
17
+ values.extend(seq)
18
+ except TypeError: # non-iterable
19
+ values.append(seq)
20
+ return tuple(values)
21
+
22
+
23
+ @lru_cache(maxsize=None)
24
+ def mro_collect_app_spec(app: type) -> AppSpec:
25
+ """Collect AppSpec-like declarations across the app's MRO."""
26
+ logger.info("Collecting app spec for %s", app.__name__)
27
+
28
+ title = "Tigrbl"
29
+ version = "0.1.0"
30
+ engine: Any | None = None
31
+ response = None
32
+ jsonrpc_prefix = "/rpc"
33
+ system_prefix = "/system"
34
+ lifespan = None
35
+
36
+ for base in reversed(app.__mro__):
37
+ title = getattr(base, "TITLE", title)
38
+ version = getattr(base, "VERSION", version)
39
+ eng = getattr(base, "ENGINE", None)
40
+ if eng is not None:
41
+ engine = eng
42
+ response = getattr(base, "RESPONSE", response)
43
+ jsonrpc_prefix = getattr(base, "JSONRPC_PREFIX", jsonrpc_prefix)
44
+ system_prefix = getattr(base, "SYSTEM_PREFIX", system_prefix)
45
+ lifespan = getattr(base, "LIFESPAN", lifespan)
46
+
47
+ spec = AppSpec(
48
+ title=title,
49
+ version=version,
50
+ engine=engine,
51
+ apis=_merge_seq_attr(app, "APIS"),
52
+ ops=_merge_seq_attr(app, "OPS"),
53
+ models=_merge_seq_attr(app, "MODELS"),
54
+ schemas=_merge_seq_attr(app, "SCHEMAS"),
55
+ hooks=_merge_seq_attr(app, "HOOKS"),
56
+ security_deps=_merge_seq_attr(app, "SECURITY_DEPS"),
57
+ deps=_merge_seq_attr(app, "DEPS"),
58
+ response=response,
59
+ jsonrpc_prefix=jsonrpc_prefix,
60
+ system_prefix=system_prefix,
61
+ middlewares=_merge_seq_attr(app, "MIDDLEWARES"),
62
+ lifespan=lifespan,
63
+ )
64
+ return spec
65
+
66
+
67
+ __all__ = ["mro_collect_app_spec"]
@@ -0,0 +1,65 @@
1
+ # tigrbl/tigrbl/v3/app/shortcuts.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Sequence, Type
5
+
6
+ from .app_spec import AppSpec
7
+ from ._app import App
8
+
9
+
10
+ def defineAppSpec(
11
+ *,
12
+ title: str = "Tigrbl",
13
+ version: str = "0.1.0",
14
+ engine: Any = None,
15
+ # composition
16
+ apis: Sequence[Any] = (),
17
+ ops: Sequence[Any] = (),
18
+ models: Sequence[Any] = (),
19
+ schemas: Sequence[Any] = (),
20
+ hooks: Sequence[Any] = (),
21
+ # deps & security
22
+ security_deps: Sequence[Any] = (),
23
+ deps: Sequence[Any] = (),
24
+ # prefixes
25
+ jsonrpc_prefix: str = "/rpc",
26
+ system_prefix: str = "/system",
27
+ # framework bits
28
+ middlewares: Sequence[Any] = (),
29
+ lifespan: Any = None,
30
+ ) -> Type[AppSpec]:
31
+ """
32
+ Build an App-spec class with class attributes only (no instances).
33
+ Use it directly in your class MRO:
34
+
35
+ class MyApp(defineAppSpec(title="Svc", engine=...)):
36
+ pass
37
+
38
+ or pass it to `deriveApp(...)` to get a concrete App subclass.
39
+ """
40
+ attrs = dict(
41
+ TITLE=title,
42
+ VERSION=version,
43
+ ENGINE=engine,
44
+ APIS=tuple(apis or ()),
45
+ OPS=tuple(ops or ()),
46
+ MODELS=tuple(models or ()),
47
+ SCHEMAS=tuple(schemas or ()),
48
+ HOOKS=tuple(hooks or ()),
49
+ SECURITY_DEPS=tuple(security_deps or ()),
50
+ DEPS=tuple(deps or ()),
51
+ JSONRPC_PREFIX=jsonrpc_prefix,
52
+ SYSTEM_PREFIX=system_prefix,
53
+ MIDDLEWARES=tuple(middlewares or ()),
54
+ LIFESPAN=lifespan,
55
+ )
56
+ return type("AppSpec", (AppSpec,), attrs)
57
+
58
+
59
+ def deriveApp(**kw: Any) -> Type[App]:
60
+ """Produce a concrete :class:`App` subclass that inherits the spec."""
61
+ Spec = defineAppSpec(**kw)
62
+ return type("AppWithSpec", (Spec, App), {})
63
+
64
+
65
+ __all__ = ["defineAppSpec", "deriveApp"]