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,256 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from types import SimpleNamespace
5
+ from typing import Any, Dict, Sequence, Tuple
6
+
7
+ from .common import (
8
+ ApiLike,
9
+ _default_prefix,
10
+ _ensure_api_ns,
11
+ _has_include_router,
12
+ _mount_router,
13
+ _resource_name,
14
+ )
15
+ from .resource_proxy import _ResourceProxy
16
+ from .. import model as _binder
17
+ from ...config.constants import (
18
+ TIGRBL_AUTH_DEP_ATTR,
19
+ TIGRBL_AUTHORIZE_ATTR,
20
+ TIGRBL_GET_DB_ATTR,
21
+ TIGRBL_REST_DEPENDENCIES_ATTR,
22
+ TIGRBL_RPC_DEPENDENCIES_ATTR,
23
+ TIGRBL_ALLOW_ANON_ATTR,
24
+ )
25
+ from ...engine import resolver as _resolver
26
+
27
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
28
+ logger = logging.getLogger("uvicorn")
29
+ logger.debug("Loaded module v3/bindings/api/include")
30
+
31
+
32
+ # --- keep as helper, no behavior change to transports/kernel ---
33
+ def _seed_security_and_deps(api: Any, model: type) -> None:
34
+ """
35
+ Copy API-level dependency hooks onto the model so downstream binders can use them.
36
+ - __tigrbl_get_db__ : DB dep (FastAPI Depends-compatible)
37
+ - __tigrbl_auth_dep__ : auth dependency (returns user or raises 401)
38
+ - __tigrbl_authorize__ : callable(request, model, alias, payload, user)→None/raise 403
39
+ - __tigrbl_rest_dependencies__ : list of extra dependencies for REST (e.g., rate-limits)
40
+ - __tigrbl_rpc_dependencies__ : list of extra dependencies for JSON-RPC router
41
+ """
42
+ # DB deps
43
+ prov = _resolver.resolve_provider(api=api)
44
+ if prov is not None:
45
+ logger.debug("Resolved provider for %s", model.__name__)
46
+ setattr(model, TIGRBL_GET_DB_ATTR, prov.get_db)
47
+ else:
48
+ logger.debug("No provider resolved for %s", model.__name__)
49
+
50
+ # Authn (prefer optional dep when available)
51
+ auth_dep = None
52
+ if getattr(api, "_optional_authn_dep", None):
53
+ auth_dep = api._optional_authn_dep
54
+ logger.debug("Using optional auth dependency for %s", model.__name__)
55
+ elif getattr(api, "_allow_anon", True) is False and getattr(api, "_authn", None):
56
+ auth_dep = api._authn
57
+ logger.debug("Using required auth dependency for %s", model.__name__)
58
+ elif getattr(api, "_authn", None):
59
+ auth_dep = api._authn
60
+ logger.debug("Using default auth dependency for %s", model.__name__)
61
+ if auth_dep is not None:
62
+ setattr(model, TIGRBL_AUTH_DEP_ATTR, auth_dep)
63
+ else:
64
+ logger.debug("No auth dependency configured for %s", model.__name__)
65
+
66
+ # Allow anonymous verbs
67
+ allow_attr = getattr(model, TIGRBL_ALLOW_ANON_ATTR, None)
68
+ if allow_attr:
69
+ verbs = allow_attr() if callable(allow_attr) else allow_attr
70
+ logger.debug("Allowing anonymous verbs %s for %s", verbs, model.__name__)
71
+ for v in verbs:
72
+ api._allow_anon_ops.add(f"{model.__name__}.{v}")
73
+ else:
74
+ logger.debug("No anonymous verbs for %s", model.__name__)
75
+
76
+ # Authz
77
+ if getattr(api, "_authorize", None):
78
+ setattr(model, TIGRBL_AUTHORIZE_ATTR, api._authorize)
79
+ logger.debug("Authorization hook attached for %s", model.__name__)
80
+ else:
81
+ logger.debug("No authorization hook for %s", model.__name__)
82
+
83
+ # Extra deps (router-level only; never part of kernel plan)
84
+ if getattr(api, "rest_dependencies", None):
85
+ setattr(model, TIGRBL_REST_DEPENDENCIES_ATTR, list(api.rest_dependencies))
86
+ logger.debug("REST dependencies seeded for %s", model.__name__)
87
+ else:
88
+ logger.debug("No REST dependencies for %s", model.__name__)
89
+ if getattr(api, "rpc_dependencies", None):
90
+ setattr(model, TIGRBL_RPC_DEPENDENCIES_ATTR, list(api.rpc_dependencies))
91
+ logger.debug("RPC dependencies seeded for %s", model.__name__)
92
+ else:
93
+ logger.debug("No RPC dependencies for %s", model.__name__)
94
+
95
+
96
+ def _attach_to_api(api: ApiLike, model: type) -> None:
97
+ """
98
+ Attach the model’s bound namespaces to the api facade.
99
+ """
100
+ _ensure_api_ns(api)
101
+
102
+ mname = model.__name__
103
+ rname = _resource_name(model)
104
+ rtitle = rname[:1].upper() + rname[1:]
105
+ logger.debug("Attaching model %s as resource '%s'", mname, rname)
106
+
107
+ # Index model object
108
+ api.models[mname] = model
109
+ api.tables[mname] = getattr(model, "__table__", None)
110
+
111
+ # Direct references to model namespaces
112
+ setattr(api.schemas, mname, getattr(model, "schemas", SimpleNamespace()))
113
+ setattr(api.handlers, mname, getattr(model, "handlers", SimpleNamespace()))
114
+ setattr(api.hooks, mname, getattr(model, "hooks", SimpleNamespace()))
115
+ rpc_ns = getattr(model, "rpc", SimpleNamespace())
116
+ setattr(api.rpc, mname, rpc_ns)
117
+ if rtitle != mname:
118
+ setattr(api.rpc, rtitle, rpc_ns)
119
+ logger.debug("Registered RPC namespace alias '%s'", rtitle)
120
+ # rest (router lives on model.rest.router)
121
+ rest_ns = getattr(api, "rest")
122
+ setattr(
123
+ rest_ns,
124
+ mname,
125
+ SimpleNamespace(
126
+ router=getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
127
+ ),
128
+ )
129
+ # also keep a flat routers dict for quick access
130
+ api.routers[mname] = getattr(
131
+ getattr(model, "rest", SimpleNamespace()), "router", None
132
+ )
133
+
134
+ # Table metadata (introspection only)
135
+ api.columns[mname] = tuple(getattr(model, "columns", ()))
136
+ api.table_config[mname] = dict(getattr(model, "table_config", {}) or {})
137
+
138
+ # Core helper proxies (now aware of API for DB resolution precedence)
139
+ core_proxy = _ResourceProxy(model, api=api)
140
+ setattr(api.core, mname, core_proxy)
141
+ if rtitle != mname:
142
+ setattr(api.core, rtitle, core_proxy)
143
+ core_raw_proxy = _ResourceProxy(model, serialize=False, api=api)
144
+ setattr(api.core_raw, mname, core_raw_proxy)
145
+ if rtitle != mname:
146
+ setattr(api.core_raw, rtitle, core_raw_proxy)
147
+
148
+
149
+ def include_model(
150
+ api: ApiLike,
151
+ model: type,
152
+ *,
153
+ app: Any | None = None,
154
+ prefix: str | None = None,
155
+ mount_router: bool = True,
156
+ ) -> Tuple[type, Any]:
157
+ """
158
+ Bind a model (if not already bound), mount its REST router, and attach all namespaces to `api`.
159
+
160
+ Args:
161
+ api: An arbitrary facade object; we’ll attach containers onto it if missing.
162
+ model: The SQLAlchemy model (table class).
163
+ app: Optional FastAPI app or Router (anything with `include_router`).
164
+ Routers are always mounted on `api.router`; if provided, we also
165
+ mount onto this `app` (or `api.app` when not given).
166
+ prefix: Optional mount prefix. When None, defaults to `/{ModelClassName}` or
167
+ `/{__resource__}` if set on the model.
168
+ mount_router: If False, we skip mounting onto the host app but still bind
169
+ the router under `api.router`/`api.rest`/`api.routers`.
170
+
171
+ Returns:
172
+ (model, router) – the model class and its Router (or None if not present).
173
+ """
174
+ logger.debug("Including model %s", model.__name__)
175
+
176
+ # If another test or call disposed the SQLAlchemy registry, previously
177
+ # imported models lose their table mapping. Re-map on demand so tests that
178
+ # run after a registry dispose still have working models.
179
+ if not hasattr(model, "__table__"):
180
+ try: # pragma: no cover - defensive path exercised in tests
181
+ from ...table import Base
182
+ from ...table._base import _materialize_colspecs_to_sqla
183
+
184
+ # Recreate mapped_column attributes from ColumnSpecs then map
185
+ _materialize_colspecs_to_sqla(model)
186
+ Base.registry.map_declaratively(model)
187
+ except Exception: # pragma: no cover
188
+ logger.debug("Failed to remap model %s", model.__name__, exc_info=True)
189
+
190
+ # 0) seed deps/security so binders can see them (transport-level only)
191
+ _seed_security_and_deps(api, model)
192
+
193
+ # 1) Build/bind model namespaces (idempotent)
194
+ _binder.bind(model, api=api)
195
+
196
+ # 2) Pick a router & mount prefix
197
+ router = getattr(getattr(model, "rest", SimpleNamespace()), "router", None)
198
+ if prefix is None:
199
+ prefix = _default_prefix(model)
200
+ logger.debug("Computed default prefix '%s' for %s", prefix, model.__name__)
201
+ else:
202
+ logger.debug("Using provided prefix '%s' for %s", prefix, model.__name__)
203
+
204
+ # 3) Always bind model router to the API object when possible
205
+ root_router = api if _has_include_router(api) else getattr(api, "router", None)
206
+ if router is not None:
207
+ logger.debug("Mounting model router for %s on api", model.__name__)
208
+ _mount_router(root_router, router, prefix=prefix)
209
+ else:
210
+ logger.debug("Model %s has no router to mount", model.__name__)
211
+
212
+ # Optionally mount onto a host app
213
+ target_app = app or getattr(api, "app", None)
214
+ if mount_router and router is not None:
215
+ logger.debug("Mounting router for %s on host app", model.__name__)
216
+ _mount_router(target_app, router, prefix=prefix)
217
+ else:
218
+ logger.debug(
219
+ "Skipping host app mount for %s (mount_router=%s, router=%s)",
220
+ model.__name__,
221
+ mount_router,
222
+ router is not None,
223
+ )
224
+
225
+ # 4) Attach all namespaces onto api
226
+ _attach_to_api(api, model)
227
+
228
+ logger.debug("bindings.api: included %s at prefix %s", model.__name__, prefix)
229
+ return model, router
230
+
231
+
232
+ def include_models(
233
+ api: ApiLike,
234
+ models: Sequence[type],
235
+ *,
236
+ app: Any | None = None,
237
+ base_prefix: str | None = None,
238
+ mount_router: bool = True,
239
+ ) -> Dict[str, Any]:
240
+ """
241
+ Convenience helper to include multiple models.
242
+
243
+ If ``base_prefix`` is provided, each model's router is mounted under that
244
+ prefix. The model router itself already has its own `/{resource}` prefix.
245
+ """
246
+ logger.debug("Including %d models", len(models))
247
+ results: Dict[str, Any] = {}
248
+ for mdl in models:
249
+ px = base_prefix.rstrip("/") if base_prefix else None
250
+ logger.debug("Including model %s with base prefix %s", mdl.__name__, px)
251
+ _, router = include_model(
252
+ api, mdl, app=app, prefix=px, mount_router=mount_router
253
+ )
254
+ results[mdl.__name__] = router
255
+ logger.debug("Finished including models")
256
+ return results
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from types import SimpleNamespace
5
+ from typing import Any, Awaitable, Callable, Dict, Mapping, Optional
6
+
7
+ from ..rpc import _coerce_payload, _get_phase_chains, _validate_input, _serialize_output
8
+ from ...runtime import executor as _executor
9
+ from ...engine import resolver as _resolver
10
+
11
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
12
+ logger = logging.getLogger("uvicorn")
13
+ logger.debug("Loaded module v3/bindings/api/resource_proxy")
14
+
15
+
16
+ class _ResourceProxy:
17
+ """Dynamic proxy that executes core operations."""
18
+
19
+ __slots__ = ("_model", "_serialize", "_api")
20
+
21
+ def __init__(
22
+ self, model: type, *, serialize: bool = True, api: Any = None
23
+ ) -> None: # pragma: no cover - trivial
24
+ self._model = model
25
+ self._serialize = serialize
26
+ self._api = api
27
+
28
+ def __repr__(self) -> str: # pragma: no cover - debug helper
29
+ return f"<ResourceProxy {self._model.__name__}>"
30
+
31
+ def __getattr__(self, alias: str) -> Callable[..., Awaitable[Any]]:
32
+ logger.debug("Resolving core handler '%s' for %s", alias, self._model.__name__)
33
+ handlers_root = getattr(self._model, "handlers", None)
34
+ h_alias = getattr(handlers_root, alias, None) if handlers_root else None
35
+ if h_alias is None or not hasattr(h_alias, "core"):
36
+ logger.debug(
37
+ "No core handler '%s' found for %s", alias, self._model.__name__
38
+ )
39
+ raise AttributeError(f"{self._model.__name__} has no core method '{alias}'")
40
+
41
+ async def _call(
42
+ payload: Any = None,
43
+ *,
44
+ db: Any | None = None,
45
+ request: Any = None,
46
+ ctx: Optional[Dict[str, Any]] = None,
47
+ ) -> Any:
48
+ raw_payload = _coerce_payload(payload)
49
+ logger.debug(
50
+ "Calling %s.%s with payload %s",
51
+ self._model.__name__,
52
+ alias,
53
+ raw_payload,
54
+ )
55
+ if alias == "bulk_delete" and not isinstance(raw_payload, Mapping):
56
+ raw_payload = {"ids": raw_payload}
57
+ logger.debug("Coerced bulk_delete payload to mapping: %s", raw_payload)
58
+ norm_payload = _validate_input(self._model, alias, alias, raw_payload)
59
+ logger.debug(
60
+ "Validated payload for %s.%s: %s",
61
+ self._model.__name__,
62
+ alias,
63
+ norm_payload,
64
+ )
65
+
66
+ base_ctx: Dict[str, Any] = dict(ctx or {})
67
+ base_ctx.setdefault("payload", norm_payload)
68
+ if request is not None:
69
+ logger.debug("Request provided for %s.%s", self._model.__name__, alias)
70
+ base_ctx.setdefault("request", request)
71
+ # surface contextual metadata for runtime atoms
72
+ app_ref = getattr(request, "app", None) or base_ctx.get("app") or self._api
73
+ base_ctx.setdefault("app", app_ref)
74
+ base_ctx.setdefault("api", base_ctx.get("api") or self._api or app_ref)
75
+ base_ctx.setdefault("model", self._model)
76
+ base_ctx.setdefault("op", alias)
77
+ base_ctx.setdefault("method", alias)
78
+ base_ctx.setdefault("target", alias)
79
+ base_ctx.setdefault(
80
+ "env",
81
+ SimpleNamespace(
82
+ method=alias, params=norm_payload, target=alias, model=self._model
83
+ ),
84
+ )
85
+ if self._serialize:
86
+ logger.debug(
87
+ "Serialization enabled for %s.%s", self._model.__name__, alias
88
+ )
89
+ base_ctx.setdefault(
90
+ "response_serializer",
91
+ lambda r: _serialize_output(self._model, alias, alias, r),
92
+ )
93
+ else:
94
+ logger.debug(
95
+ "Serialization disabled for %s.%s", self._model.__name__, alias
96
+ )
97
+ base_ctx.setdefault("response_serializer", lambda r: r)
98
+
99
+ # Acquire DB if one was not explicitly provided (op > model > api > app)
100
+ _release_db = None
101
+ if db is None:
102
+ try:
103
+ logger.debug(
104
+ "Acquiring DB for %s.%s via resolver",
105
+ self._model.__name__,
106
+ alias,
107
+ )
108
+ db, _release_db = _resolver.acquire(
109
+ api=self._api, model=self._model, op_alias=alias
110
+ )
111
+ except Exception:
112
+ logger.exception(
113
+ "DB acquire failed for %s.%s; no default configured?",
114
+ self._model.__name__,
115
+ alias,
116
+ )
117
+ raise
118
+ else:
119
+ logger.debug("Using provided DB for %s.%s", self._model.__name__, alias)
120
+
121
+ base_ctx.setdefault("db", db)
122
+ phases = _get_phase_chains(self._model, alias)
123
+ logger.debug(
124
+ "Executing phases %s for %s.%s", phases, self._model.__name__, alias
125
+ )
126
+ try:
127
+ return await _executor._invoke(
128
+ request=request,
129
+ db=db,
130
+ phases=phases,
131
+ ctx=base_ctx,
132
+ )
133
+ finally:
134
+ if _release_db is not None:
135
+ try:
136
+ _release_db()
137
+ logger.debug(
138
+ "Released DB for %s.%s", self._model.__name__, alias
139
+ )
140
+ except Exception:
141
+ logger.debug(
142
+ "Non-fatal: error releasing acquired DB session",
143
+ exc_info=True,
144
+ )
145
+
146
+ _call.__name__ = f"{self._model.__name__}.{alias}"
147
+ _call.__qualname__ = _call.__name__
148
+ _call.__doc__ = f"Helper for core call {self._model.__name__}.{alias}"
149
+ return _call
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from types import SimpleNamespace
5
+ from typing import Any, Dict, Mapping, Optional, Union
6
+
7
+ from .common import ApiLike, _ensure_api_ns
8
+ from ...engine import resolver as _resolver
9
+ from ...core.crud.helpers.model import _single_pk_name
10
+
11
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
12
+ logger = logging.getLogger("uvicorn")
13
+ logger.debug("Loaded module v3/bindings/api/rpc")
14
+
15
+
16
+ async def rpc_call(
17
+ api: ApiLike,
18
+ model_or_name: Union[type, str],
19
+ method: str,
20
+ payload: Any = None,
21
+ *,
22
+ db: Any | None = None,
23
+ request: Any = None,
24
+ ctx: Optional[Dict[str, Any]] = None,
25
+ ) -> Any:
26
+ """
27
+ Call a registered RPC method by (model, method) pair.
28
+ `model_or_name` may be a model class or its name.
29
+ """
30
+ logger.debug("rpc_call invoked for model=%s method=%s", model_or_name, method)
31
+ _ensure_api_ns(api)
32
+
33
+ if isinstance(model_or_name, str):
34
+ mdl = api.models.get(model_or_name)
35
+ if mdl is None:
36
+ logger.debug("Unknown model name '%s'", model_or_name)
37
+ raise KeyError(f"Unknown model '{model_or_name}'")
38
+ logger.debug("Resolved model name '%s' to %s", model_or_name, mdl)
39
+ else:
40
+ mdl = model_or_name
41
+ logger.debug("Using model class %s", getattr(mdl, "__name__", mdl))
42
+
43
+ fn = getattr(getattr(mdl, "rpc", SimpleNamespace()), method, None)
44
+ if fn is None:
45
+ logger.debug(
46
+ "RPC method '%s' not found on %s", method, getattr(mdl, "__name__", mdl)
47
+ )
48
+ raise AttributeError(
49
+ f"{getattr(mdl, '__name__', mdl)} has no RPC method '{method}'"
50
+ )
51
+
52
+ # Acquire DB if not explicitly provided (op > model > api > app)
53
+ _release_db = None
54
+ if db is None:
55
+ try:
56
+ logger.debug(
57
+ "Acquiring DB for rpc_call %s.%s", getattr(mdl, "__name__", mdl), method
58
+ )
59
+ db, _release_db = _resolver.acquire(api=api, model=mdl, op_alias=method)
60
+ except Exception:
61
+ logger.exception(
62
+ "DB acquire failed for rpc_call %s.%s; no default configured?",
63
+ getattr(mdl, "__name__", mdl),
64
+ method,
65
+ )
66
+ raise
67
+ else:
68
+ logger.debug(
69
+ "Using provided DB for rpc_call %s.%s",
70
+ getattr(mdl, "__name__", mdl),
71
+ method,
72
+ )
73
+
74
+ # Ensure execution context contains basic runtime metadata. In tests or
75
+ # other direct calls there may be no ``request`` object to supply an app
76
+ # reference, which the runtime uses to resolve the opview. When absent, the
77
+ # kernel falls back to cached specs for the given model and alias.
78
+ ctx_dict: Dict[str, Any] = dict(ctx or {})
79
+ # Opportunistically derive path params from the payload when the caller
80
+ # supplies the primary key in the body. Many RPC handlers expect the
81
+ # identifier via ``ctx['path_params']`` (mirroring REST semantics), but
82
+ # test code invokes ``rpc_call`` directly with the id embedded in the
83
+ # payload. Normalizing here preserves backwards compatibility and keeps
84
+ # default CRUD handlers happy.
85
+ if isinstance(payload, Mapping):
86
+ try:
87
+ pk_name = _single_pk_name(mdl)
88
+ except Exception: # model may not be bound to a table
89
+ pk_name = None
90
+ if pk_name and pk_name in payload:
91
+ pp = dict(ctx_dict.get("path_params", {}))
92
+ pp.setdefault(pk_name, payload[pk_name])
93
+ ctx_dict["path_params"] = pp
94
+
95
+ try:
96
+ logger.debug("Executing rpc_call %s.%s", getattr(mdl, "__name__", mdl), method)
97
+ return await fn(payload, db=db, request=request, ctx=ctx_dict)
98
+ finally:
99
+ if _release_db is not None:
100
+ try:
101
+ _release_db()
102
+ logger.debug(
103
+ "Released DB for rpc_call %s.%s",
104
+ getattr(mdl, "__name__", mdl),
105
+ method,
106
+ )
107
+ except Exception:
108
+ logger.debug(
109
+ "Non-fatal: error releasing acquired DB session (rpc_call)",
110
+ exc_info=True,
111
+ )
@@ -0,0 +1,49 @@
1
+ import logging
2
+
3
+ # tigrbl/v3/bindings/columns.py
4
+ from sqlalchemy import Column
5
+ from ..specs import ColumnSpec, is_virtual
6
+
7
+ logger = logging.getLogger("uvicorn")
8
+ logger.debug("Loaded module v3/bindings/columns")
9
+
10
+
11
+ def build_and_attach(model: type, specs=None, only_keys=None):
12
+ cols = {}
13
+ for name, attr in list(model.__dict__.items()):
14
+ if not isinstance(attr, ColumnSpec):
15
+ continue
16
+ if is_virtual(attr):
17
+ cols[name] = attr
18
+ continue
19
+
20
+ st = attr.storage
21
+ # Ensure type instantiation
22
+ col_type = st.type_
23
+ if isinstance(col_type, type):
24
+ # Special case UUID/Enum
25
+ if col_type.__name__ == "UUID":
26
+ col_type = col_type(as_uuid=True)
27
+ elif col_type.__name__ == "Enum":
28
+ raise RuntimeError("Use SAEnum(enum_cls) not bare Enum class")
29
+ else:
30
+ col_type = col_type()
31
+
32
+ col = Column(
33
+ col_type,
34
+ primary_key=st.primary_key,
35
+ nullable=st.nullable,
36
+ unique=st.unique,
37
+ index=st.index,
38
+ autoincrement=st.autoincrement,
39
+ default=st.default,
40
+ onupdate=st.onupdate,
41
+ server_default=st.server_default,
42
+ comment=st.comment,
43
+ )
44
+ setattr(model, name, col)
45
+ cols[name] = attr
46
+
47
+ # register map for later (your Key already has one, but ensure default)
48
+ if not hasattr(model, "__tigrbl_cols__"):
49
+ model.__tigrbl_cols__ = cols
@@ -0,0 +1,11 @@
1
+ # tigrbl/v3/bindings/handlers/__init__.py
2
+ from __future__ import annotations
3
+ import logging
4
+
5
+ from .builder import build_and_attach
6
+
7
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
8
+ logger = logging.getLogger("uvicorn")
9
+ logger.debug("Loaded module v3/bindings/handlers/__init__")
10
+
11
+ __all__ = ["build_and_attach"]