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,73 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional
4
+
5
+ from .compat import Router
6
+ from .healthz import build_healthz_endpoint
7
+ from .methodz import build_methodz_endpoint
8
+ from .hookz import build_hookz_endpoint
9
+ from .kernelz import build_kernelz_endpoint
10
+
11
+
12
+ def mount_diagnostics(
13
+ api: Any,
14
+ *,
15
+ get_db: Optional[Callable[..., Any]] = None,
16
+ ) -> Router:
17
+ """
18
+ Create & return a Router that exposes:
19
+ GET /healthz
20
+ GET /methodz
21
+ GET /hookz
22
+ GET /kernelz
23
+ """
24
+ router = Router()
25
+
26
+ dep = get_db
27
+
28
+ router.add_api_route(
29
+ "/healthz",
30
+ build_healthz_endpoint(dep),
31
+ methods=["GET"],
32
+ name="healthz",
33
+ tags=["system"],
34
+ summary="Health",
35
+ description="Database connectivity check.",
36
+ )
37
+ router.add_api_route(
38
+ "/methodz",
39
+ build_methodz_endpoint(api),
40
+ methods=["GET"],
41
+ name="methodz",
42
+ tags=["system"],
43
+ summary="Methods",
44
+ description="Ordered, canonical operation list.",
45
+ )
46
+ router.add_api_route(
47
+ "/hookz",
48
+ build_hookz_endpoint(api),
49
+ methods=["GET"],
50
+ name="hookz",
51
+ tags=["system"],
52
+ summary="Hooks",
53
+ description=(
54
+ "Expose hook execution order for each method.\n\n"
55
+ "Phases appear in runner order; error phases trail.\n"
56
+ "Within each phase, hooks are listed in execution order: "
57
+ "global (None) hooks, then method-specific hooks."
58
+ ),
59
+ )
60
+ router.add_api_route(
61
+ "/kernelz",
62
+ build_kernelz_endpoint(api),
63
+ methods=["GET"],
64
+ name="kernelz",
65
+ tags=["system"],
66
+ summary="Kernel Plan",
67
+ description="Phase-chain plan as built by the kernel per operation.",
68
+ )
69
+
70
+ return router
71
+
72
+
73
+ __all__ = ["mount_diagnostics"]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from types import SimpleNamespace
5
+ from typing import Any, Iterable
6
+
7
+ from sqlalchemy import text
8
+
9
+
10
+ def model_iter(api: Any) -> Iterable[type]:
11
+ models = getattr(api, "models", {}) or {}
12
+ return models.values()
13
+
14
+
15
+ def opspecs(model: type):
16
+ return getattr(getattr(model, "opspecs", SimpleNamespace()), "all", ()) or ()
17
+
18
+
19
+ def label_callable(fn: Any) -> str:
20
+ n = getattr(fn, "__qualname__", getattr(fn, "__name__", repr(fn)))
21
+ m = getattr(fn, "__module__", None)
22
+ return f"{m}.{n}" if m else n
23
+
24
+
25
+ def label_hook(fn: Any, phase: str) -> str:
26
+ label = getattr(fn, "__tigrbl_label", None)
27
+ if isinstance(label, str):
28
+ return label
29
+ subj = label_callable(fn).replace(".", ":")
30
+ return f"hook:wire:{subj}@{phase}"
31
+
32
+
33
+ async def maybe_execute(db: Any, stmt: str):
34
+ try:
35
+ rv = db.execute(text(stmt)) # type: ignore[attr-defined]
36
+ if inspect.isawaitable(rv):
37
+ return await rv
38
+ return rv
39
+ except Exception:
40
+ rv = db.execute(text("select 1")) # type: ignore[attr-defined]
41
+ if inspect.isawaitable(rv):
42
+ return await rv
43
+ return rv
@@ -0,0 +1,60 @@
1
+ """Utilities for running uvicorn during tests or tooling."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import uvicorn
8
+
9
+
10
+ async def _cancel_task(task: asyncio.Task) -> None:
11
+ if task.done():
12
+ return
13
+ task.cancel()
14
+ try:
15
+ await task
16
+ except asyncio.CancelledError:
17
+ return
18
+
19
+
20
+ async def _close_servers(server: uvicorn.Server) -> None:
21
+ servers = []
22
+ primary = getattr(server, "server", None)
23
+ if primary is not None:
24
+ servers.append(primary)
25
+ extra = getattr(server, "servers", None)
26
+ if extra:
27
+ servers.extend(extra)
28
+ for srv in servers:
29
+ close = getattr(srv, "close", None)
30
+ if callable(close):
31
+ close()
32
+ wait_closed = getattr(srv, "wait_closed", None)
33
+ if callable(wait_closed):
34
+ await wait_closed()
35
+
36
+
37
+ async def stop_uvicorn_server(
38
+ server: uvicorn.Server,
39
+ task: asyncio.Task,
40
+ *,
41
+ timeout: float = 5.0,
42
+ ) -> None:
43
+ """Request uvicorn shutdown and ensure the task exits."""
44
+ if task.done():
45
+ return
46
+
47
+ server.should_exit = True
48
+ try:
49
+ await asyncio.wait_for(task, timeout=timeout)
50
+ return
51
+ except asyncio.TimeoutError:
52
+ server.force_exit = True
53
+ shutdown = getattr(server, "shutdown", None)
54
+ if callable(shutdown):
55
+ try:
56
+ await asyncio.wait_for(shutdown(), timeout=timeout)
57
+ except asyncio.TimeoutError:
58
+ pass
59
+ await _close_servers(server)
60
+ await _cancel_task(task)
@@ -0,0 +1,9 @@
1
+ """Table module exposing Base and Table."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._base import Base
6
+ from ._table import Table
7
+ from .table_spec import TableSpec
8
+
9
+ __all__ = ["Base", "Table", "TableSpec"]
tigrbl/table/_base.py ADDED
@@ -0,0 +1,260 @@
1
+ # tigrbl/tigrbl/v3/table/_base.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Optional, Union, get_args, get_origin
5
+ from enum import Enum as PyEnum
6
+
7
+ from sqlalchemy.orm import DeclarativeBase, declared_attr, mapped_column
8
+ from sqlalchemy import CheckConstraint, ForeignKey, MetaData
9
+ from sqlalchemy.types import Enum as SAEnum, String
10
+
11
+ # ──────────────────────────────────────────────────────────────────────────────
12
+ # Helpers – type inference & SA type instantiation
13
+ # ──────────────────────────────────────────────────────────────────────────────
14
+
15
+
16
+ def _unwrap_optional(t: Any) -> Any:
17
+ """Optional[T] / Union[T, None] → T"""
18
+ if get_origin(t) is Union:
19
+ args = [a for a in get_args(t) if a is not type(None)]
20
+ return args[0] if args else t
21
+ return t
22
+
23
+
24
+ def _infer_py_type(cls, name: str, spec: Any) -> Optional[type]:
25
+ """
26
+ Prefer FieldSpec.py_type if provided; otherwise unwrap Mapped[...] / Optional[...]
27
+ from the class' annotation to get the real Python type for the column.
28
+ """
29
+ fld = getattr(spec, "field", None)
30
+ py = getattr(fld, "py_type", None)
31
+ if isinstance(py, type):
32
+ return py
33
+
34
+ ann = getattr(cls, "__annotations__", {}).get(name)
35
+ if ann is None:
36
+ return None
37
+
38
+ # Mapped[T] → T (then unwrap Optional)
39
+ try:
40
+ from ..types import Mapped
41
+
42
+ if get_origin(ann) is Mapped:
43
+ inner = get_args(ann)[0]
44
+ return _unwrap_optional(inner)
45
+ except Exception:
46
+ pass
47
+
48
+ # Optional[T]/Union[T, None] → T
49
+ return _unwrap_optional(ann)
50
+
51
+
52
+ def _instantiate_dtype(
53
+ dtype: Any, py_type: Any, spec: Any, cls_name: str, col_name: str
54
+ ):
55
+ """
56
+ Create a SQLAlchemy TypeEngine instance from either a type CLASS or an instance.
57
+ - SAEnum: instantiate from the actual Enum class with a stable name
58
+ - String: honor FieldSpec.constraints['max_length'] if present
59
+ - UUID (PG): prefer as_uuid=True when available
60
+ """
61
+ # Already an instance? keep it.
62
+ try:
63
+ from sqlalchemy.sql.type_api import TypeEngine
64
+
65
+ if isinstance(dtype, TypeEngine):
66
+ return dtype
67
+ except Exception:
68
+ pass
69
+
70
+ # SAEnum from a Python Enum class
71
+ if dtype is SAEnum and isinstance(py_type, type) and issubclass(py_type, PyEnum):
72
+ enum_name = f"{cls_name.lower()}_{col_name.lower()}"
73
+ return SAEnum(py_type, name=enum_name, native_enum=True, validate_strings=True)
74
+
75
+ # String – pick up max_length from FieldSpec
76
+ if dtype is String:
77
+ max_len = getattr(getattr(spec, "field", None), "constraints", {}).get(
78
+ "max_length"
79
+ )
80
+ return String(max_len) if max_len else String()
81
+
82
+ # PostgreSQL UUID (or similar) – try as_uuid=True first
83
+ try:
84
+ return dtype(as_uuid=True) # e.g., PG UUID
85
+ except TypeError:
86
+ try:
87
+ return dtype()
88
+ except TypeError:
89
+ # As a last resort, return the class; SQLA will raise clearly if unusable
90
+ return dtype
91
+
92
+
93
+ def _materialize_colspecs_to_sqla(cls) -> None:
94
+ """
95
+ Replace ColumnSpec attributes with sqlalchemy.orm.mapped_column(...) BEFORE mapping.
96
+ Keep the original specs in __tigrbl_cols__ for downstream builders.
97
+ """
98
+ try:
99
+ from tigrbl.column.column_spec import ColumnSpec
100
+ except Exception:
101
+ return
102
+
103
+ # Prefer explicit registry if present; otherwise collect specs from the
104
+ # entire MRO so mixins contribute their ColumnSpec definitions.
105
+ specs: dict[str, ColumnSpec] = {}
106
+ for base in reversed(cls.__mro__):
107
+ base_specs = getattr(base, "__tigrbl_cols__", None)
108
+ if isinstance(base_specs, dict) and base_specs:
109
+ specs.update(base_specs)
110
+ continue
111
+ for name, attr in getattr(base, "__dict__", {}).items():
112
+ if isinstance(attr, ColumnSpec):
113
+ specs.setdefault(name, attr)
114
+
115
+ if not specs:
116
+ return
117
+
118
+ # Ensure downstream code can find the spec map
119
+ setattr(cls, "__tigrbl_cols__", dict(specs))
120
+
121
+ for name, spec in specs.items():
122
+ storage = getattr(spec, "storage", None)
123
+ if not storage:
124
+ # Virtual (wire-only) column – no DB column
125
+ continue
126
+
127
+ dtype = getattr(storage, "type_", None)
128
+ if not dtype:
129
+ # No SA dtype specified – cannot materialize
130
+ continue
131
+
132
+ py_type = _infer_py_type(cls, name, spec)
133
+ dtype_inst = _instantiate_dtype(dtype, py_type, spec, cls.__name__, name)
134
+
135
+ # Foreign key (if any)
136
+ fk = getattr(storage, "fk", None)
137
+ fk_arg = None
138
+ if fk is not None:
139
+ # ForeignKeySpec: target="table(col)", on_delete/on_update: "CASCADE"/...
140
+ fk_arg = ForeignKey(fk.target, ondelete=fk.on_delete, onupdate=fk.on_update)
141
+
142
+ check = getattr(storage, "check", None)
143
+ args: list[Any] = []
144
+ if fk_arg is not None:
145
+ args.append(fk_arg)
146
+ if check is not None:
147
+ cname = f"ck_{cls.__name__.lower()}_{name}"
148
+ args.append(CheckConstraint(check, name=cname))
149
+
150
+ # Build mapped_column from StorageSpec flags
151
+ mc = mapped_column(
152
+ dtype_inst,
153
+ *args,
154
+ primary_key=getattr(storage, "primary_key", False),
155
+ nullable=getattr(storage, "nullable", True),
156
+ unique=getattr(storage, "unique", False),
157
+ index=getattr(storage, "index", False),
158
+ default=getattr(storage, "default", None),
159
+ onupdate=getattr(storage, "onupdate", None),
160
+ server_default=getattr(storage, "server_default", None),
161
+ comment=getattr(storage, "comment", None),
162
+ autoincrement=getattr(storage, "autoincrement", None),
163
+ )
164
+
165
+ setattr(cls, name, mc)
166
+
167
+
168
+ # ──────────────────────────────────────────────────────────────────────────────
169
+ # Declarative Base
170
+ # ──────────────────────────────────────────────────────────────────────────────
171
+
172
+
173
+ class Base(DeclarativeBase):
174
+ __allow_unmapped__ = True
175
+
176
+ def __init_subclass__(cls, **kw):
177
+ # 0) Remove any previously registered class with the same module path.
178
+ try:
179
+ reg = Base.registry._class_registry
180
+ key = f"{cls.__module__}.{cls.__name__}"
181
+ existing = reg.get(key)
182
+ if existing is not None:
183
+ try:
184
+ Base.registry._dispose_cls(existing)
185
+ except Exception:
186
+ pass
187
+ reg.pop(key, None)
188
+ if reg.get(cls.__name__) is existing:
189
+ reg.pop(cls.__name__, None)
190
+ except Exception:
191
+ pass
192
+
193
+ # 0.5) If a table with the same name already exists, allow this class
194
+ # to extend it instead of raising duplicate-table errors.
195
+ try:
196
+ table_name = getattr(cls, "__tablename__", None)
197
+ if table_name and table_name in Base.metadata.tables:
198
+ table_args = getattr(cls, "__table_args__", None)
199
+ if table_args is None:
200
+ cls.__table_args__ = {"extend_existing": True}
201
+ elif isinstance(table_args, dict):
202
+ table_args = dict(table_args)
203
+ table_args["extend_existing"] = True
204
+ cls.__table_args__ = table_args
205
+ elif isinstance(table_args, tuple):
206
+ if table_args and isinstance(table_args[-1], dict):
207
+ table_dict = dict(table_args[-1])
208
+ table_dict["extend_existing"] = True
209
+ cls.__table_args__ = (*table_args[:-1], table_dict)
210
+ else:
211
+ cls.__table_args__ = (*table_args, {"extend_existing": True})
212
+ except Exception:
213
+ pass
214
+
215
+ # 1) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
216
+ _materialize_colspecs_to_sqla(cls)
217
+
218
+ # 2) Let SQLAlchemy map the class (PK now exists)
219
+ super().__init_subclass__(**kw)
220
+
221
+ # 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
222
+ try:
223
+ from tigrbl.bindings import model as _model_bind
224
+
225
+ _model_bind.bind(cls)
226
+ except Exception:
227
+ pass
228
+
229
+ # 3) AUTO-BUILD CRUD schemas from ColumnSpecs so /docs has them
230
+ try:
231
+ from tigrbl.schema.build import build_for_model as _build_schemas
232
+
233
+ _build_schemas(
234
+ cls
235
+ ) # attaches request/response models to the model/registry
236
+ except Exception:
237
+ # Surface during development if needed:
238
+ # raise
239
+ pass
240
+
241
+ metadata = MetaData(
242
+ naming_convention={
243
+ "pk": "pk_%(table_name)s",
244
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
245
+ "ix": "ix_%(table_name)s_%(column_0_name)s",
246
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
247
+ "ck": "ck_%(table_name)s_%(column_0_name)s_%(constraint_type)s",
248
+ }
249
+ )
250
+
251
+ @declared_attr.directive
252
+ def __tablename__(cls) -> str: # noqa: N805
253
+ return cls.__name__.lower()
254
+
255
+ def __getitem__(self, key: str) -> Any:
256
+ """Allow dict-style access to model attributes."""
257
+ return getattr(self, key)
258
+
259
+
260
+ __all__ = ["Base"]
tigrbl/table/_table.py ADDED
@@ -0,0 +1,54 @@
1
+ # tigrbl/tigrbl/v3/table/_table.py
2
+ from __future__ import annotations
3
+
4
+ from types import SimpleNamespace
5
+ from typing import Any, Callable
6
+
7
+ from ..engine._engine import AsyncSession, Session
8
+ from ..engine import install_from_objects # reuse the collector
9
+ from ..engine import resolver as _resolver
10
+ from ..ddl import initialize as _ddl_initialize
11
+ from ._base import Base
12
+ from .table_spec import TableSpec
13
+
14
+
15
+ class Table(Base, TableSpec):
16
+ """Declarative ORM table base.
17
+
18
+ This class now integrates :class:`Base` so ORM models and tables share
19
+ the same type. Column specifications are exposed via ``columns`` for
20
+ convenience.
21
+ """
22
+
23
+ __abstract__ = True
24
+ columns: SimpleNamespace = SimpleNamespace()
25
+
26
+ def __init__(self, **kw: Any) -> None: # pragma: no cover - SQLA sets attrs
27
+ for k, v in kw.items():
28
+ setattr(self, k, v)
29
+
30
+ def __init_subclass__(cls, **kw: Any) -> None: # noqa: D401
31
+ super().__init_subclass__(**kw)
32
+
33
+ # expose ColumnSpecs under `columns` namespace
34
+ specs = getattr(cls, "__tigrbl_cols__", {})
35
+ cls.columns = SimpleNamespace(**specs)
36
+
37
+ # auto-register table-level bindings if declared
38
+ try:
39
+ install_from_objects(models=[cls])
40
+ except Exception: # pragma: no cover - best effort
41
+ pass
42
+
43
+ @classmethod
44
+ def install_engines(cls, *, api: Any | None = None) -> None:
45
+ install_from_objects(api=api, models=[cls])
46
+
47
+ @classmethod
48
+ def acquire(
49
+ cls, *, op_alias: str | None = None
50
+ ) -> tuple[Session | AsyncSession, Callable[[], None]]:
51
+ db, release = _resolver.acquire(model=cls, op_alias=op_alias)
52
+ return db, release
53
+
54
+ initialize = classmethod(_ddl_initialize)
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from functools import lru_cache
5
+ from typing import Any, Mapping, Tuple
6
+
7
+ from .table_spec import TableSpec
8
+
9
+ logger = logging.getLogger("uvicorn")
10
+
11
+
12
+ def _merge_seq_attr(model: type, attr: str) -> Tuple[Any, ...]:
13
+ values: list[Any] = []
14
+ for base in model.__mro__:
15
+ seq = base.__dict__.get(attr, ()) or ()
16
+ try:
17
+ values.extend(seq)
18
+ except TypeError: # pragma: no cover - non-iterable
19
+ values.append(seq)
20
+ return tuple(values)
21
+
22
+
23
+ @lru_cache(maxsize=None)
24
+ def mro_collect_table_spec(model: type) -> TableSpec:
25
+ """Collect TableSpec-like declarations across the model's MRO.
26
+
27
+ Merges common spec attributes (OPS, COLUMNS, SCHEMAS, HOOKS, SECURITY_DEPS,
28
+ DEPS) declared on the class or any mixins. Engine bindings declared via
29
+ ``table_config`` use the same precedence: later classes in the MRO override
30
+ earlier ones.
31
+ """
32
+
33
+ logger.info("Collecting table spec for %s", model.__name__)
34
+
35
+ engine: Any | None = None
36
+ for base in model.__mro__:
37
+ cfg = base.__dict__.get("table_config")
38
+ if isinstance(cfg, Mapping):
39
+ eng = (
40
+ cfg.get("engine")
41
+ or cfg.get("db")
42
+ or cfg.get("database")
43
+ or cfg.get("engine_provider")
44
+ or cfg.get("db_provider")
45
+ )
46
+ if eng is not None:
47
+ engine = eng
48
+
49
+ spec = TableSpec(
50
+ model=model,
51
+ engine=engine,
52
+ ops=_merge_seq_attr(model, "OPS"),
53
+ columns=_merge_seq_attr(model, "COLUMNS"),
54
+ schemas=_merge_seq_attr(model, "SCHEMAS"),
55
+ hooks=_merge_seq_attr(model, "HOOKS"),
56
+ security_deps=_merge_seq_attr(model, "SECURITY_DEPS"),
57
+ deps=_merge_seq_attr(model, "DEPS"),
58
+ )
59
+
60
+ logger.debug(
61
+ "Collected table spec for %s: %d ops, %d columns",
62
+ model.__name__,
63
+ len(spec.ops),
64
+ len(spec.columns),
65
+ )
66
+ return spec
67
+
68
+
69
+ __all__ = ["mro_collect_table_spec"]
@@ -0,0 +1,57 @@
1
+ # tigrbl/tigrbl/v3/table/shortcuts.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Sequence, Type
5
+
6
+ from .table_spec import TableSpec
7
+ from ._table import Table
8
+
9
+
10
+ def defineTableSpec(
11
+ *,
12
+ # engine binding
13
+ engine: Any = None,
14
+ # composition
15
+ ops: Sequence[Any] = (),
16
+ columns: Sequence[Any] = (),
17
+ schemas: Sequence[Any] = (),
18
+ hooks: Sequence[Any] = (),
19
+ # dependency stacks
20
+ security_deps: Sequence[Any] = (),
21
+ deps: Sequence[Any] = (),
22
+ ) -> Type[TableSpec]:
23
+ """
24
+ Build a Table-spec class with class attributes only (no instances).
25
+ Use directly in your ORM class MRO:
26
+
27
+ class User(defineTableSpec(engine=..., ops=(...)), Base, Table):
28
+ __tablename__ = "users"
29
+
30
+ or pass it to `deriveTable(Model, ...)` to get a configured subclass.
31
+ """
32
+ attrs = {
33
+ # top-level mirrors read by collectors
34
+ "OPS": tuple(ops or ()),
35
+ "COLUMNS": tuple(columns or ()),
36
+ "SCHEMAS": tuple(schemas or ()),
37
+ "HOOKS": tuple(hooks or ()),
38
+ "SECURITY_DEPS": tuple(security_deps or ()),
39
+ "DEPS": tuple(deps or ()),
40
+ }
41
+
42
+ # Engine binding is conventionally stored under table_config["engine"]
43
+ # (and legacy "db" for backward compatibility) so collectors can find it.
44
+ if engine is not None:
45
+ attrs["table_config"] = {"engine": engine, "db": engine}
46
+
47
+ return type("TableSpec", (TableSpec,), attrs)
48
+
49
+
50
+ def deriveTable(model: Type[Table], **kw: Any) -> Type[Table]:
51
+ """Produce a concrete ORM subclass that inherits the spec."""
52
+ Spec = defineTableSpec(**kw)
53
+ name = f"{model.__name__}WithSpec"
54
+ return type(name, (Spec, model), {})
55
+
56
+
57
+ __all__ = ["defineTableSpec", "deriveTable"]
@@ -0,0 +1,28 @@
1
+ # tigrbl/tigrbl/v3/table/table_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
11
+ class TableSpec:
12
+ """
13
+ Declarative enrichments for an ORM class (model == table).
14
+ This does not construct an instance; it decorates/produces a class.
15
+ """
16
+
17
+ model: Any # ORM class
18
+ engine: Optional[EngineCfg] = None
19
+
20
+ # NEW
21
+ ops: Sequence[Any] = field(default_factory=tuple) # OpSpec or shorthands
22
+ columns: Sequence[Any] = field(default_factory=tuple) # ColumnSpec or shorthands
23
+ schemas: Sequence[Any] = field(default_factory=tuple)
24
+ hooks: Sequence[Callable[..., Any]] = field(default_factory=tuple)
25
+ security_deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
26
+ deps: Sequence[Callable[..., Any]] = field(default_factory=tuple)
27
+
28
+ response: Optional[ResponseSpec] = None