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,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+
6
+ def build_methodz_endpoint(api: Any):
7
+ cache: Optional[Dict[str, List[Dict[str, Any]]]] = None
8
+
9
+ async def _methodz():
10
+ nonlocal cache
11
+ """Ordered, canonical operation list."""
12
+ if cache is not None:
13
+ return cache
14
+
15
+ from . import _model_iter, _opspecs
16
+
17
+ methods: List[Dict[str, Any]] = []
18
+ for model in _model_iter(api):
19
+ mname = getattr(model, "__name__", "Model")
20
+ for sp in _opspecs(model):
21
+ if not getattr(sp, "expose_rpc", True):
22
+ continue
23
+ methods.append(
24
+ {
25
+ "method": f"{mname}.{sp.alias}",
26
+ "model": mname,
27
+ "alias": sp.alias,
28
+ "target": sp.target,
29
+ "arity": sp.arity,
30
+ "persist": sp.persist,
31
+ "request_model": getattr(sp, "request_model", None) is not None,
32
+ "response_model": getattr(sp, "response_model", None)
33
+ is not None,
34
+ "routes": bool(getattr(sp, "expose_routes", True)),
35
+ "rpc": bool(getattr(sp, "expose_rpc", True)),
36
+ "tags": list(getattr(sp, "tags", ()) or (mname,)),
37
+ }
38
+ )
39
+ methods.sort(key=lambda x: (x["model"], x["alias"]))
40
+ cache = {"methods": methods}
41
+ return cache
42
+
43
+ return _methodz
@@ -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,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,237 @@
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 name.
178
+ try:
179
+ reg = Base.registry._class_registry
180
+ keys = [cls.__name__, f"{cls.__module__}.{cls.__name__}"]
181
+ existing = next((reg.get(k) for k in keys if reg.get(k) is not None), None)
182
+ if existing is not None:
183
+ try:
184
+ Base.registry._dispose_cls(existing)
185
+ except Exception:
186
+ pass
187
+ for k in keys:
188
+ reg.pop(k, None)
189
+ except Exception:
190
+ pass
191
+
192
+ # 1) BEFORE SQLAlchemy maps: turn ColumnSpecs into real mapped_column(...)
193
+ _materialize_colspecs_to_sqla(cls)
194
+
195
+ # 2) Let SQLAlchemy map the class (PK now exists)
196
+ super().__init_subclass__(**kw)
197
+
198
+ # 3) Seed model namespaces / index specs (ops/hooks/etc.) – idempotent
199
+ try:
200
+ from tigrbl.bindings import model as _model_bind
201
+
202
+ _model_bind.bind(cls)
203
+ except Exception:
204
+ pass
205
+
206
+ # 3) AUTO-BUILD CRUD schemas from ColumnSpecs so /docs has them
207
+ try:
208
+ from tigrbl.schema.build import build_for_model as _build_schemas
209
+
210
+ _build_schemas(
211
+ cls
212
+ ) # attaches request/response models to the model/registry
213
+ except Exception:
214
+ # Surface during development if needed:
215
+ # raise
216
+ pass
217
+
218
+ metadata = MetaData(
219
+ naming_convention={
220
+ "pk": "pk_%(table_name)s",
221
+ "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
222
+ "ix": "ix_%(table_name)s_%(column_0_name)s",
223
+ "uq": "uq_%(table_name)s_%(column_0_name)s",
224
+ "ck": "ck_%(table_name)s_%(column_0_name)s_%(constraint_type)s",
225
+ }
226
+ )
227
+
228
+ @declared_attr.directive
229
+ def __tablename__(cls) -> str: # noqa: N805
230
+ return cls.__name__.lower()
231
+
232
+ def __getitem__(self, key: str) -> Any:
233
+ """Allow dict-style access to model attributes."""
234
+ return getattr(self, key)
235
+
236
+
237
+ __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
@@ -0,0 +1,74 @@
1
+ # tigrbl/v3/transport/__init__.py
2
+ """
3
+ Tigrbl v3 – Transport package.
4
+
5
+ Routers & helpers for exposing your API over JSON-RPC and REST.
6
+
7
+ Quick usage:
8
+ from tigrbl.transport import (
9
+ build_jsonrpc_router, mount_jsonrpc,
10
+ build_rest_router, mount_rest,
11
+ )
12
+
13
+ # JSON-RPC
14
+ app.include_router(build_jsonrpc_router(api), prefix="/rpc")
15
+ # or supply a DB dependency from an Engine or Provider:
16
+ mount_jsonrpc(api, app, prefix="/rpc", get_db=my_engine.get_db)
17
+
18
+ # REST (aggregate all model routers under one prefix)
19
+ # after you include models with mount_router=False
20
+ app.include_router(build_rest_router(api, base_prefix="/api"))
21
+ # or:
22
+ mount_rest(api, app, base_prefix="/api")
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from typing import Any, Callable, Optional, Sequence
28
+
29
+ # JSON-RPC transport
30
+ from .jsonrpc import build_jsonrpc_router
31
+
32
+ # REST transport (aggregator over per-model routers)
33
+ from .rest import build_rest_router, mount_rest
34
+
35
+
36
+ def mount_jsonrpc(
37
+ api: Any,
38
+ app: Any,
39
+ *,
40
+ prefix: str = "/rpc",
41
+ get_db: Optional[Callable[..., Any]] = None,
42
+ tags: Sequence[str] | None = ("rpc",),
43
+ ):
44
+ """
45
+ Build a JSON-RPC router for `api` and include it on the given FastAPI `app`
46
+ (or any object exposing `include_router`).
47
+
48
+ Returns the created router so you can keep a reference if desired.
49
+
50
+ Parameters
51
+ ----------
52
+ tags:
53
+ Optional tags applied to the mounted "/rpc" endpoint. Defaults to
54
+ ``("rpc",)``.
55
+ """
56
+ router = build_jsonrpc_router(
57
+ api,
58
+ get_db=get_db,
59
+ tags=tags,
60
+ )
61
+ include_router = getattr(app, "include_router", None)
62
+ if callable(include_router):
63
+ include_router(router, prefix=prefix)
64
+ return router
65
+
66
+
67
+ __all__ = [
68
+ # JSON-RPC
69
+ "build_jsonrpc_router",
70
+ "mount_jsonrpc",
71
+ # REST
72
+ "build_rest_router",
73
+ "mount_rest",
74
+ ]