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,193 @@
1
+ from __future__ import annotations
2
+ import logging
3
+
4
+ # tigrbl/v3/bindings/schemas/utils.py
5
+
6
+ from types import SimpleNamespace
7
+ from typing import Any, Optional, Tuple, Type
8
+
9
+ from pydantic import BaseModel, create_model
10
+
11
+ from ...schema.types import SchemaArg, SchemaRef
12
+ from ...schema import namely_model
13
+
14
+ logging.getLogger("uvicorn").setLevel(logging.DEBUG)
15
+ logger = logging.getLogger("uvicorn")
16
+ logger.debug("Loaded module v3/bindings/schemas/utils")
17
+
18
+ _Key = Tuple[str, str] # (alias, target)
19
+
20
+
21
+ def _camel(s: str) -> str:
22
+ return "".join(p.capitalize() or "_" for p in s.split("_"))
23
+
24
+
25
+ def _alias_schema(
26
+ schema: Type[BaseModel], *, model: type, alias: str, kind: str
27
+ ) -> Type[BaseModel]:
28
+ logger.debug(
29
+ "Aliasing schema %s for %s as %s %s",
30
+ schema.__name__,
31
+ model.__name__,
32
+ alias,
33
+ kind,
34
+ )
35
+ name = f"{model.__name__}{_camel(alias)}{kind}"
36
+ if getattr(schema, "__name__", None) == name:
37
+ logger.debug("Schema already aliased as %s", name)
38
+ return schema
39
+ try:
40
+ clone = create_model(name, __base__=schema) # type: ignore[arg-type]
41
+ except Exception as e: # pragma: no cover - best effort
42
+ logger.debug("Failed to clone schema %s: %s", schema.__name__, e)
43
+ return schema
44
+ logger.debug("Created alias schema %s", name)
45
+ return namely_model(
46
+ clone,
47
+ name=name,
48
+ doc=f"{alias} {kind.lower()} schema for {model.__name__}",
49
+ )
50
+
51
+
52
+ def _ensure_alias_namespace(model: type, alias: str) -> SimpleNamespace:
53
+ ns = getattr(model.schemas, alias, None)
54
+ if ns is None:
55
+ ns = SimpleNamespace()
56
+ setattr(model.schemas, alias, ns)
57
+ logger.debug("Created namespace for %s.%s", model.__name__, alias)
58
+ else:
59
+ logger.debug("Using existing namespace for %s.%s", model.__name__, alias)
60
+ return ns
61
+
62
+
63
+ def _pk_info(model: type) -> Tuple[str, type | Any]:
64
+ """
65
+ Return (pk_name, python_type) for single-PK tables. If composite, returns (pk, Any).
66
+ """
67
+ table = getattr(model, "__table__", None)
68
+ if table is None or not getattr(table, "primary_key", None):
69
+ logger.debug("Model %s has no table or primary key", model.__name__)
70
+ return ("id", Any)
71
+ cols = list(table.primary_key.columns) # type: ignore[attr-defined]
72
+ if not cols:
73
+ logger.debug("Model %s primary key columns empty", model.__name__)
74
+ return ("id", Any)
75
+ if len(cols) > 1:
76
+ logger.debug("Model %s has composite primary key", model.__name__)
77
+ # Composite keys: schema builder uses verb='delete' to require what's needed.
78
+ # For bulk_delete we fall back to Any.
79
+ return ("id", Any)
80
+ col = cols[0]
81
+ py_t = getattr(getattr(col, "type", None), "python_type", Any)
82
+ logger.debug(
83
+ "Model %s primary key %s of type %s",
84
+ model.__name__,
85
+ getattr(col, "name", "id"),
86
+ py_t,
87
+ )
88
+ return (getattr(col, "name", "id"), py_t or Any)
89
+
90
+
91
+ def _parse_str_ref(s: str) -> Tuple[str, str]:
92
+ """
93
+ Parse dotted schema ref "alias.in" | "alias.out".
94
+ """
95
+ s = s.strip()
96
+ logger.debug("Parsing schema ref '%s'", s)
97
+ if "." not in s:
98
+ logger.debug("Schema ref '%s' missing '.'", s)
99
+ raise ValueError(
100
+ f"Invalid schema path '{s}', expected 'alias.in' or 'alias.out'"
101
+ )
102
+ alias, kind = s.split(".", 1)
103
+ kind = kind.strip()
104
+ if kind not in {"in", "out"}:
105
+ logger.debug("Schema ref '%s' has invalid kind '%s'", s, kind)
106
+ raise ValueError(f"Invalid schema kind '{kind}', expected 'in' or 'out'")
107
+ logger.debug("Parsed schema ref alias=%s kind=%s", alias, kind)
108
+ return alias.strip(), kind
109
+
110
+
111
+ def _resolve_schema_arg(model: type, arg: SchemaArg) -> Optional[Type[BaseModel]]:
112
+ """
113
+ Resolve an override to a concrete Pydantic model or raw:
114
+ • SchemaRef("alias","in"|"out") → that model
115
+ • "alias.in" | "alias.out" → that model
116
+ • "raw" → None (raw passthrough)
117
+ • None → caller should keep defaults for canonical ops;
118
+ for custom ops no defaults exist → raw
119
+ Unsupported (will raise):
120
+ • direct Pydantic model classes
121
+ • callables/thunks
122
+ • any other strings
123
+ """
124
+ logger.debug("Resolving schema arg %s for %s", arg, model.__name__)
125
+ if arg is None:
126
+ logger.debug("Schema arg is None; returning None")
127
+ return None
128
+
129
+ # explicit raw
130
+ if isinstance(arg, str) and arg.strip().lower() == "raw":
131
+ logger.debug("Schema arg is explicit raw")
132
+ return None
133
+
134
+ # direct Pydantic model
135
+ if isinstance(arg, type) and issubclass(arg, BaseModel):
136
+ logger.debug("Schema arg is direct model %s", arg.__name__)
137
+ return arg
138
+
139
+ # SchemaRef
140
+ if isinstance(arg, SchemaRef):
141
+ logger.debug("Schema arg is SchemaRef(alias=%s, kind=%s)", arg.alias, arg.kind)
142
+ if arg.kind not in ("in", "out"):
143
+ logger.debug("Unsupported SchemaRef kind '%s'", arg.kind)
144
+ raise ValueError(
145
+ f"Unsupported SchemaRef kind '{arg.kind}'. Use 'in' or 'out'.",
146
+ )
147
+ ns = getattr(model, "schemas", None)
148
+ if ns is None or getattr(ns, arg.alias, None) is None:
149
+ logger.debug("Unknown schema alias '%s' on %s", arg.alias, model.__name__)
150
+ raise KeyError(f"Unknown schema alias '{arg.alias}' on {model.__name__}")
151
+ alias_ns = getattr(ns, arg.alias)
152
+ attr = "in_" if arg.kind == "in" else "out"
153
+ res = getattr(alias_ns, attr, None)
154
+ if res is None:
155
+ logger.debug(
156
+ "Schema '%s.%s' not found on %s", arg.alias, attr, model.__name__
157
+ )
158
+ raise KeyError(f"Schema '{arg.alias}.{attr}' not found on {model.__name__}")
159
+ logger.debug(
160
+ "Resolved SchemaRef %s.%s to %s",
161
+ arg.alias,
162
+ attr,
163
+ getattr(res, "__name__", None),
164
+ )
165
+ return res # type: ignore[return-value]
166
+
167
+ # dotted string
168
+ if isinstance(arg, str):
169
+ alias, kind = _parse_str_ref(arg)
170
+ ns = getattr(model, "schemas", None)
171
+ if ns is None or getattr(ns, alias, None) is None:
172
+ logger.debug("Unknown schema alias '%s' on %s", alias, model.__name__)
173
+ raise KeyError(f"Unknown schema alias '{alias}' on {model.__name__}")
174
+ alias_ns = getattr(ns, alias)
175
+ attr = "in_" if kind == "in" else "out"
176
+ res = getattr(alias_ns, attr, None)
177
+ if res is None:
178
+ logger.debug("Schema '%s.%s' not found on %s", alias, attr, model.__name__)
179
+ raise KeyError(f"Schema '{alias}.{attr}' not found on {model.__name__}")
180
+ logger.debug(
181
+ "Resolved schema path %s.%s to %s",
182
+ alias,
183
+ attr,
184
+ getattr(res, "__name__", None),
185
+ )
186
+ return res # type: ignore[return-value]
187
+
188
+ logger.debug("Unsupported SchemaArg type: %s", type(arg))
189
+ # Everything else is unsupported now
190
+ raise TypeError(
191
+ f"Unsupported SchemaArg type: {type(arg)}. "
192
+ "Use SchemaRef(...,'in'|'out'), 'alias.in'/'alias.out', 'raw', or None.",
193
+ )
@@ -0,0 +1,62 @@
1
+ # Column Specs
2
+
3
+ Tigrbl columns are declared with helper functions like `acol` and `vcol`.
4
+ Each column is driven by three smaller specs that describe how the value is
5
+ stored, validated, and exposed. These specs keep column behaviour
6
+ consistent across the ORM, schema generation, and the runtime API.
7
+
8
+ ## StorageSpec (`S`)
9
+ Describes the database shape and storage behaviour.
10
+
11
+ - `type_` – SQLAlchemy column type.
12
+ - `nullable`, `unique`, `index`, `primary_key`, `autoincrement`.
13
+ - Defaults and generation: `default`, `onupdate`, `server_default`,
14
+ `refresh_on_return`.
15
+ - Optional helpers: `transform`, `fk`, `check`, `comment`.
16
+
17
+ ## FieldSpec (`F`)
18
+ Captures Pydantic metadata and request policy.
19
+
20
+ - `py_type` – Python type, inferred when omitted.
21
+ - `constraints` – passed to `pydantic.Field` for validation.
22
+ - `required_in` / `allow_null_in` – control required and nullable verbs.
23
+
24
+ ## IOSpec (`IO`)
25
+ Controls API exposure and advanced value handling.
26
+
27
+ - `in_verbs` / `out_verbs` – verbs that accept or emit the value.
28
+ - `mutable_verbs` – verbs allowed to change the value.
29
+ - `alias_in` / `alias_out` – alternative field names.
30
+ - `sensitive`, `redact_last` – masking and redaction options.
31
+ - `filter_ops`, `sortable` – enable filtering and sorting.
32
+ - Helpers: `assemble`, `paired`, `alias_readtime` for computed aliases.
33
+
34
+ ## ColumnSpec
35
+ `ColumnSpec` ties the three specs together and adds runtime helpers such as
36
+ `default_factory` and `read_producer`.
37
+
38
+ ```python
39
+ from tigrbl.column import acol, vcol, F, IO, S
40
+
41
+ class Widget(Base):
42
+ __tablename__ = "widgets"
43
+
44
+ id: Mapped[int] = acol(storage=S(primary_key=True))
45
+ name: Mapped[str] = acol(
46
+ field=F(constraints={"max_length": 50}),
47
+ storage=S(nullable=False, index=True),
48
+ io=IO(
49
+ in_verbs=("create", "update"),
50
+ out_verbs=("read", "list"),
51
+ sortable=True,
52
+ ),
53
+ )
54
+ checksum: Mapped[str] = vcol(
55
+ field=F(),
56
+ io=IO(out_verbs=("read",)),
57
+ read_producer=lambda obj, ctx: f"{obj.name}:{obj.id}",
58
+ )
59
+ ```
60
+
61
+ For additional background see the "Column-Level Configuration" section in the
62
+ parent [README](../README.md).
@@ -0,0 +1,72 @@
1
+ # tigrbl/v3/column/__init__.py
2
+ """Tigrbl v3 – column specs public API.
3
+
4
+ Unifies StorageSpec (DB-facing), FieldSpec (python/wire semantics), and IOSpec (in/out exposure)
5
+ into a :class:`ColumnSpec` with a :class:`Column` descriptor. Provides ergonomic constructors
6
+ ``makeColumn`` and ``makeVirtualColumn`` (with ``acol``/``vcol`` convenience aliases) and re-exports the
7
+ bind-time type inference utilities and markers.
8
+
9
+ Public surface:
10
+ Column, ColumnSpec, FieldSpec as F, StorageSpec as S, IOSpec as IO
11
+ makeColumn, makeVirtualColumn, acol, vcol
12
+ infer, Inferred, DataKind, SATypePlan, JsonHint
13
+ markers: Email, Phone
14
+ exceptions: InferenceError, UnsupportedType
15
+ helper: is_virtual(ColumnSpec) -> bool
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ # Core spec types
21
+ from .column_spec import ColumnSpec
22
+ from ._column import Column
23
+ from .field_spec import FieldSpec as F
24
+ from .storage_spec import StorageSpec as S
25
+ from .io_spec import IOSpec as IO
26
+
27
+ # Ergonomic constructors
28
+ from .shortcuts import makeColumn, makeVirtualColumn, acol, vcol
29
+
30
+ # Bind-time inference (DB/vendor-agnostic)
31
+ from .infer import (
32
+ infer,
33
+ Inferred,
34
+ DataKind,
35
+ SATypePlan,
36
+ JsonHint,
37
+ Email,
38
+ Phone,
39
+ InferenceError,
40
+ UnsupportedType,
41
+ )
42
+
43
+ __all__ = [
44
+ "ColumnSpec",
45
+ "Column",
46
+ "F",
47
+ "S",
48
+ "IO",
49
+ "makeColumn",
50
+ "makeVirtualColumn",
51
+ "acol",
52
+ "vcol",
53
+ "infer",
54
+ "Inferred",
55
+ "DataKind",
56
+ "SATypePlan",
57
+ "JsonHint",
58
+ "Email",
59
+ "Phone",
60
+ "InferenceError",
61
+ "UnsupportedType",
62
+ "is_virtual",
63
+ ]
64
+
65
+
66
+ def is_virtual(col: ColumnSpec) -> bool:
67
+ """Return True if the column is wire-only (never persisted)."""
68
+ return getattr(col, "storage", None) is None
69
+
70
+
71
+ def __dir__():
72
+ return sorted(__all__)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional
4
+
5
+ from sqlalchemy import ForeignKey
6
+ from sqlalchemy.orm import MappedColumn
7
+
8
+ from .column_spec import ColumnSpec
9
+ from .field_spec import FieldSpec as F
10
+ from .io_spec import IOSpec as IO
11
+ from .storage_spec import StorageSpec as S
12
+
13
+
14
+ class Column(ColumnSpec, MappedColumn):
15
+ """SQLAlchemy column implementing a :class:`ColumnSpec`."""
16
+
17
+ __slots__ = ()
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ spec: ColumnSpec | None = None,
23
+ storage: S | None = None,
24
+ field: F | None = None,
25
+ io: IO | None = None,
26
+ default_factory: Optional[Callable[[dict], Any]] = None,
27
+ read_producer: Optional[Callable[[object, dict], Any]] = None,
28
+ **kw: Any,
29
+ ) -> None:
30
+ if spec is not None and any(
31
+ x is not None for x in (storage, field, io, default_factory, read_producer)
32
+ ):
33
+ raise ValueError("Provide either spec or individual components, not both.")
34
+ if spec is None:
35
+ spec = ColumnSpec(
36
+ storage=storage,
37
+ field=field,
38
+ io=io,
39
+ default_factory=default_factory,
40
+ read_producer=read_producer,
41
+ )
42
+ else:
43
+ storage = spec.storage
44
+ field = spec.field
45
+ io = spec.io
46
+ default_factory = spec.default_factory
47
+ read_producer = spec.read_producer
48
+
49
+ s = storage
50
+ if s is not None:
51
+ args: list[Any] = [s.type_]
52
+ fk = getattr(s, "fk", None)
53
+ if fk is not None:
54
+ args.append(
55
+ ForeignKey(
56
+ fk.target,
57
+ ondelete=fk.on_delete,
58
+ onupdate=fk.on_update,
59
+ deferrable=fk.deferrable,
60
+ initially="DEFERRED" if fk.initially_deferred else "IMMEDIATE",
61
+ match=fk.match,
62
+ )
63
+ )
64
+ MappedColumn.__init__(
65
+ self,
66
+ *args,
67
+ primary_key=s.primary_key,
68
+ nullable=s.nullable,
69
+ unique=s.unique,
70
+ index=s.index,
71
+ default=s.default,
72
+ autoincrement=s.autoincrement,
73
+ server_default=s.server_default,
74
+ onupdate=s.onupdate,
75
+ comment=s.comment,
76
+ **kw,
77
+ )
78
+ else:
79
+ MappedColumn.__init__(self, **kw)
80
+
81
+ self.storage = s
82
+ self.field = field if field is not None else F()
83
+ self.io = io if io is not None else IO()
84
+ self.default_factory = default_factory
85
+ self.read_producer = read_producer
86
+
87
+ def __set_name__(self, owner, name: str) -> None:
88
+ parent = getattr(super(), "__set_name__", None)
89
+ if parent:
90
+ parent(owner, name)
91
+ colspecs = owner.__dict__.get("__tigrbl_colspecs__")
92
+ if colspecs is None:
93
+ base_specs = getattr(owner, "__tigrbl_colspecs__", {})
94
+ colspecs = dict(base_specs)
95
+ setattr(owner, "__tigrbl_colspecs__", colspecs)
96
+ colspecs[name] = self
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional
4
+
5
+ from .field_spec import FieldSpec as F
6
+ from .io_spec import IOSpec as IO
7
+ from .storage_spec import StorageSpec as S
8
+
9
+
10
+ class ColumnSpec:
11
+ """Aggregate configuration for a model attribute.
12
+
13
+ A :class:`ColumnSpec` brings together the three lower-level specs used by
14
+ Tigrbl's declarative column system:
15
+
16
+ - ``storage`` (:class:`~tigrbl.column.storage_spec.StorageSpec`) controls
17
+ how the value is persisted in the database.
18
+ - ``field`` (:class:`~tigrbl.column.field_spec.FieldSpec`) describes the
19
+ Python type and any schema metadata.
20
+ - ``io`` (:class:`~tigrbl.column.io_spec.IOSpec`) governs inbound and
21
+ outbound API exposure.
22
+
23
+ Optional ``default_factory`` and ``read_producer`` callables allow for
24
+ programmatic defaults and virtual read-time values respectively.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ *,
30
+ storage: S | None,
31
+ field: F | None = None,
32
+ io: IO | None = None,
33
+ default_factory: Optional[Callable[[dict], Any]] = None,
34
+ read_producer: Optional[Callable[[object, dict], Any]] = None,
35
+ ) -> None:
36
+ self.storage = storage
37
+ self.field = field if field is not None else F()
38
+ self.io = io if io is not None else IO()
39
+ self.default_factory = default_factory
40
+ self.read_producer = read_producer
@@ -0,0 +1,31 @@
1
+ # field_spec.py
2
+ from __future__ import annotations
3
+ from dataclasses import dataclass, field as dc_field
4
+ from typing import Any, Dict, Tuple, Callable
5
+ from pydantic import ValidationInfo # v2
6
+
7
+ PreFn = Callable[[Any, ValidationInfo], Any] # BeforeValidator
8
+ PostFn = Callable[[Any, ValidationInfo], Any] # AfterValidator
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class FieldSpec:
13
+ """Describe Python-side metadata for a column or virtual field.
14
+
15
+ ``py_type`` denotes the expected Python type and may be omitted when the
16
+ model attribute is annotated; the type will then be inferred. ``constraints``
17
+ mirrors arguments accepted by :func:`pydantic.Field` and participates in
18
+ schema generation. ``required_in`` and ``allow_null_in`` govern which API
19
+ verbs must supply the value or may explicitly send ``null`` in requests.
20
+ Responses rely on Pydantic's built-in encoders based solely on the
21
+ declared type.
22
+ """
23
+
24
+ py_type: Any = Any
25
+
26
+ # For request/response schema generation (+ pydantic.Field)
27
+ constraints: Dict[str, Any] = dc_field(default_factory=dict)
28
+
29
+ # Request policy (DB nullability lives in StorageSpec.nullable)
30
+ required_in: Tuple[str, ...] = ()
31
+ allow_null_in: Tuple[str, ...] = ()
@@ -0,0 +1,25 @@
1
+ from .core import infer
2
+ from .types import (
3
+ Email,
4
+ Phone,
5
+ DataKind,
6
+ PyTypeInfo,
7
+ SATypePlan,
8
+ JsonHint,
9
+ Inferred,
10
+ InferenceError,
11
+ UnsupportedType,
12
+ )
13
+
14
+ __all__ = [
15
+ "infer",
16
+ "Email",
17
+ "Phone",
18
+ "DataKind",
19
+ "PyTypeInfo",
20
+ "SATypePlan",
21
+ "JsonHint",
22
+ "Inferred",
23
+ "InferenceError",
24
+ "UnsupportedType",
25
+ ]
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional, get_origin
4
+
5
+ from .types import DataKind, PyTypeInfo, Inferred
6
+ from .utils import _strip_optional, _strip_annotated, _array_item, _is_enum
7
+ from .planning import _plan_sa_type
8
+ from .jsonhints import _json_hint
9
+
10
+
11
+ def infer(
12
+ annotation: Any,
13
+ *,
14
+ prefer_dialect: Optional[str] = "postgresql",
15
+ max_length: Optional[int] = None,
16
+ decimal_precision: Optional[int] = None,
17
+ decimal_scale: Optional[int] = None,
18
+ ) -> Inferred:
19
+ """Bind-time inference from Python annotation to DataKind and hints."""
20
+ base, is_opt = _strip_optional(annotation)
21
+ base, meta = _strip_annotated(base)
22
+
23
+ enum_cls = _is_enum(base)
24
+ item_tp = _array_item(base)
25
+
26
+ array_item_info: Optional[PyTypeInfo] = None
27
+ if item_tp is not None:
28
+ nested = infer(
29
+ item_tp,
30
+ prefer_dialect=prefer_dialect,
31
+ max_length=max_length,
32
+ decimal_precision=decimal_precision,
33
+ decimal_scale=decimal_scale,
34
+ )
35
+ array_item_info = nested.py
36
+
37
+ py_info = PyTypeInfo(
38
+ base=base,
39
+ is_optional=is_opt,
40
+ enum_cls=enum_cls,
41
+ array_item=array_item_info,
42
+ annotated=meta,
43
+ )
44
+
45
+ import datetime as _dt
46
+ import decimal as _dc
47
+ import uuid as _uuid
48
+
49
+ origin = get_origin(base)
50
+
51
+ if enum_cls is not None:
52
+ kind = DataKind.ENUM
53
+ elif item_tp is not None:
54
+ kind = DataKind.ARRAY
55
+ elif base in (str,):
56
+ kind = DataKind.STRING
57
+ elif base in (bytes, bytearray, memoryview):
58
+ kind = DataKind.BYTES
59
+ elif base in (bool,):
60
+ kind = DataKind.BOOL
61
+ elif base in (int,):
62
+ kind = DataKind.INT
63
+ elif base in (float,):
64
+ kind = DataKind.FLOAT
65
+ elif base in (_dc.Decimal,):
66
+ kind = DataKind.DECIMAL
67
+ elif base in (_dt.datetime,):
68
+ kind = DataKind.DATETIME
69
+ elif base in (_dt.date,):
70
+ kind = DataKind.DATE
71
+ elif base in (_dt.time,):
72
+ kind = DataKind.TIME
73
+ elif base in (_uuid.UUID,):
74
+ kind = DataKind.UUID
75
+ else:
76
+ if origin in (dict, Dict):
77
+ kind = DataKind.JSON
78
+ else:
79
+ kind = DataKind.JSON
80
+
81
+ sa = _plan_sa_type(
82
+ kind,
83
+ py_info,
84
+ prefer_dialect=prefer_dialect,
85
+ max_length=max_length,
86
+ decimal_precision=decimal_precision,
87
+ decimal_scale=decimal_scale,
88
+ )
89
+
90
+ jh = _json_hint(kind, py_info, max_length=max_length)
91
+
92
+ return Inferred(kind=kind, py=py_info, sa=sa, json=jh, nullable=is_opt)
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from .types import DataKind, PyTypeInfo, JsonHint, Email, Phone
6
+ from .planning import _nested_kind_from_py
7
+
8
+
9
+ def _json_hint(
10
+ kind: DataKind, py: PyTypeInfo, *, max_length: Optional[int]
11
+ ) -> JsonHint:
12
+ if kind is DataKind.STRING:
13
+ fmt = None
14
+ if Email in py.annotated:
15
+ fmt = "email"
16
+ if Phone in py.annotated:
17
+ fmt = "phone"
18
+ return JsonHint(type="string", format=fmt, maxLength=max_length)
19
+ if kind is DataKind.BYTES:
20
+ return JsonHint(type="string", format="byte")
21
+ if kind is DataKind.BOOL:
22
+ return JsonHint(type="boolean")
23
+ if kind is DataKind.INT or kind is DataKind.BIGINT:
24
+ return JsonHint(type="integer")
25
+ if kind is DataKind.FLOAT or kind is DataKind.DECIMAL:
26
+ return JsonHint(type="number")
27
+ if kind is DataKind.DATE:
28
+ return JsonHint(type="string", format="date")
29
+ if kind is DataKind.TIME:
30
+ return JsonHint(type="string", format="time")
31
+ if kind is DataKind.DATETIME:
32
+ return JsonHint(type="string", format="date-time")
33
+ if kind is DataKind.UUID:
34
+ return JsonHint(type="string", format="uuid")
35
+ if kind is DataKind.JSON:
36
+ return JsonHint(type="object")
37
+ if kind is DataKind.ENUM and py.enum_cls:
38
+ return JsonHint(type="string", enum=[e.name for e in py.enum_cls])
39
+ if kind is DataKind.ARRAY and py.array_item:
40
+ elem_kind = _nested_kind_from_py(py.array_item)
41
+ return JsonHint(
42
+ type="array", items=_json_hint(elem_kind, py.array_item, max_length=None)
43
+ )
44
+ return JsonHint(type="string")