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,133 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from .types import DataKind, PyTypeInfo, SATypePlan, InferenceError
6
+
7
+
8
+ def _plan_sa_type(
9
+ kind: DataKind,
10
+ py: PyTypeInfo,
11
+ *,
12
+ prefer_dialect: Optional[str],
13
+ max_length: Optional[int],
14
+ decimal_precision: Optional[int],
15
+ decimal_scale: Optional[int],
16
+ ) -> SATypePlan:
17
+ d = prefer_dialect
18
+
19
+ if kind is DataKind.STRING:
20
+ if max_length and max_length > 0:
21
+ return SATypePlan(
22
+ name="String", args=(max_length,), kwargs={}, dialect=None
23
+ )
24
+ return SATypePlan(name="String", args=(), kwargs={}, dialect=None)
25
+
26
+ if kind is DataKind.TEXT:
27
+ return SATypePlan(name="Text", args=(), kwargs={}, dialect=None)
28
+
29
+ if kind is DataKind.BYTES:
30
+ return SATypePlan(name="LargeBinary", args=(), kwargs={}, dialect=None)
31
+
32
+ if kind is DataKind.BOOL:
33
+ return SATypePlan(name="Boolean", args=(), kwargs={}, dialect=None)
34
+
35
+ if kind is DataKind.INT:
36
+ return SATypePlan(name="Integer", args=(), kwargs={}, dialect=None)
37
+
38
+ if kind is DataKind.BIGINT:
39
+ return SATypePlan(name="BigInteger", args=(), kwargs={}, dialect=None)
40
+
41
+ if kind is DataKind.FLOAT:
42
+ return SATypePlan(name="Float", args=(), kwargs={}, dialect=None)
43
+
44
+ if kind is DataKind.DECIMAL:
45
+ kwargs: Dict[str, Any] = {}
46
+ if decimal_precision is not None:
47
+ kwargs["precision"] = decimal_precision
48
+ if decimal_scale is not None:
49
+ kwargs["scale"] = decimal_scale
50
+ return SATypePlan(name="Numeric", args=(), kwargs=kwargs, dialect=None)
51
+
52
+ if kind is DataKind.DATE:
53
+ return SATypePlan(name="Date", args=(), kwargs={}, dialect=None)
54
+
55
+ if kind is DataKind.TIME:
56
+ return SATypePlan(name="Time", args=(), kwargs={"timezone": True}, dialect=None)
57
+
58
+ if kind is DataKind.DATETIME:
59
+ return SATypePlan(
60
+ name="DateTime", args=(), kwargs={"timezone": True}, dialect=None
61
+ )
62
+
63
+ if kind is DataKind.UUID:
64
+ if d == "postgresql":
65
+ return SATypePlan(
66
+ name="UUID", args=(), kwargs={"as_uuid": True}, dialect="postgresql"
67
+ )
68
+ return SATypePlan(name="String", args=(36,), kwargs={}, dialect=None)
69
+
70
+ if kind is DataKind.JSON:
71
+ if d == "postgresql":
72
+ return SATypePlan(name="JSONB", args=(), kwargs={}, dialect="postgresql")
73
+ return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
74
+
75
+ if kind is DataKind.ENUM:
76
+ if not py.enum_cls:
77
+ raise InferenceError("ENUM kind requires enum_cls in PyTypeInfo")
78
+ return SATypePlan(
79
+ name="Enum", args=(py.enum_cls,), kwargs={"native_enum": True}, dialect=None
80
+ )
81
+
82
+ if kind is DataKind.ARRAY:
83
+ if not py.array_item:
84
+ raise InferenceError("ARRAY kind requires array_item in PyTypeInfo")
85
+ elem_plan = _plan_sa_type(
86
+ _nested_kind_from_py(py.array_item),
87
+ py.array_item,
88
+ prefer_dialect=prefer_dialect,
89
+ max_length=None,
90
+ decimal_precision=None,
91
+ decimal_scale=None,
92
+ )
93
+ if prefer_dialect == "postgresql":
94
+ return SATypePlan(
95
+ name="ARRAY",
96
+ args=(elem_plan.name,),
97
+ kwargs={},
98
+ dialect="postgresql",
99
+ )
100
+ return SATypePlan(name="JSON", args=(), kwargs={}, dialect=None)
101
+
102
+ raise InferenceError(f"Cannot plan SA type for kind={kind!r}")
103
+
104
+
105
+ def _nested_kind_from_py(nested_py: PyTypeInfo) -> DataKind:
106
+ if nested_py.enum_cls is not None:
107
+ return DataKind.ENUM
108
+ b = nested_py.base
109
+ import datetime as _dt
110
+ import decimal as _dc
111
+ import uuid as _uuid
112
+
113
+ if b is str:
114
+ return DataKind.STRING
115
+ if b in (bytes, bytearray, memoryview):
116
+ return DataKind.BYTES
117
+ if b is bool:
118
+ return DataKind.BOOL
119
+ if b is int:
120
+ return DataKind.INT
121
+ if b is float:
122
+ return DataKind.FLOAT
123
+ if b is _dc.Decimal:
124
+ return DataKind.DECIMAL
125
+ if b is _dt.datetime:
126
+ return DataKind.DATETIME
127
+ if b is _dt.date:
128
+ return DataKind.DATE
129
+ if b is _dt.time:
130
+ return DataKind.TIME
131
+ if b is _uuid.UUID:
132
+ return DataKind.UUID
133
+ return DataKind.JSON
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional, Tuple, Type
6
+
7
+
8
+ # ---------------------------------------------------------------------------
9
+ # Public markers (wire/adapters provide validation/normalization at runtime)
10
+ # ---------------------------------------------------------------------------
11
+
12
+
13
+ class Email:
14
+ """Marker indicating email string semantics."""
15
+
16
+
17
+ class Phone:
18
+ """Marker indicating E.164 phone string semantics."""
19
+
20
+
21
+ # ---------------------------------------------------------------------------
22
+ # Portable data-kind (DB- and adapter-agnostic)
23
+ # ---------------------------------------------------------------------------
24
+
25
+
26
+ class DataKind(str, Enum):
27
+ STRING = "string"
28
+ TEXT = "text"
29
+ BYTES = "bytes"
30
+ BOOL = "bool"
31
+ INT = "int"
32
+ BIGINT = "bigint"
33
+ FLOAT = "float"
34
+ DECIMAL = "decimal"
35
+ DATE = "date"
36
+ TIME = "time"
37
+ DATETIME = "datetime"
38
+ UUID = "uuid"
39
+ JSON = "json"
40
+ ENUM = "enum"
41
+ ARRAY = "array"
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Structured outputs
46
+ # ---------------------------------------------------------------------------
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class PyTypeInfo:
51
+ """Normalized info about the Python-side type annotation."""
52
+
53
+ base: Any
54
+ is_optional: bool = False
55
+ enum_cls: Optional[Type[Enum]] = None
56
+ array_item: Optional["PyTypeInfo"] = None
57
+ annotated: Tuple[Any, ...] = ()
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class SATypePlan:
62
+ """Declarative plan for constructing an SA column type downstream."""
63
+
64
+ name: str # e.g., "UUID", "String", "JSONB", "Enum", "ARRAY"
65
+ args: Tuple[Any, ...] # positional args (e.g., (enum_cls,))
66
+ kwargs: Dict[str, Any] # keyword args (e.g., {"as_uuid": True})
67
+ dialect: Optional[str] = None # e.g., "postgresql" for JSONB/UUID/ARRAY
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class JsonHint:
72
+ """Minimal JSON Schema-ish hints for docs."""
73
+
74
+ type: str
75
+ format: Optional[str] = None
76
+ maxLength: Optional[int] = None
77
+ enum: Optional[List[str]] = None
78
+ items: Optional["JsonHint"] = None
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class Inferred:
83
+ """Primary product of inference."""
84
+
85
+ kind: DataKind
86
+ py: PyTypeInfo
87
+ sa: SATypePlan
88
+ json: JsonHint
89
+ nullable: bool
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Errors
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class InferenceError(ValueError):
98
+ """Base class for inference-related errors."""
99
+
100
+
101
+ class UnsupportedType(InferenceError):
102
+ """Raised when a type cannot be inferred."""
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+ from typing import (
5
+ Any,
6
+ List,
7
+ Optional,
8
+ Tuple,
9
+ Type,
10
+ Union,
11
+ get_args,
12
+ get_origin,
13
+ Annotated,
14
+ )
15
+
16
+
17
+ def _strip_optional(tp: Any) -> Tuple[Any, bool]:
18
+ """Return (inner_type, is_optional) for Optional[T] / Union[T, None]."""
19
+ origin = get_origin(tp)
20
+ if origin is Union:
21
+ args = tuple(a for a in get_args(tp))
22
+ if len(args) == 2 and type(None) in args:
23
+ inner = args[0] if args[1] is type(None) else args[1]
24
+ return inner, True
25
+ return tp, False
26
+
27
+
28
+ def _strip_annotated(tp: Any) -> Tuple[Any, Tuple[Any, ...]]:
29
+ """Return (base, metadata) for Annotated[base, *meta]; otherwise (tp, ())."""
30
+ origin = get_origin(tp)
31
+ if origin is Annotated:
32
+ args = get_args(tp)
33
+ if len(args) >= 1:
34
+ base, meta = args[0], tuple(args[1:])
35
+ return base, meta
36
+ return tp, ()
37
+
38
+
39
+ def _array_item(tp: Any) -> Optional[Any]:
40
+ origin = get_origin(tp)
41
+ if origin in (list, List, tuple, Tuple, set, frozenset):
42
+ args = get_args(tp)
43
+ if not args:
44
+ return Any
45
+ if origin in (tuple, Tuple) and len(args) == 2 and args[1] is Ellipsis:
46
+ return args[0]
47
+ if len(args) == 1:
48
+ return args[0]
49
+ return Any
50
+ return None
51
+
52
+
53
+ def _is_enum(tp: Any) -> Optional[Type[Enum]]:
54
+ try:
55
+ if isinstance(tp, type) and issubclass(tp, Enum):
56
+ return tp
57
+ except Exception:
58
+ pass
59
+ return None
@@ -0,0 +1,133 @@
1
+ # --- io_spec.py --------------------------------------------------------------
2
+ from dataclasses import dataclass, replace
3
+ from typing import Callable, Tuple, Literal
4
+ from .field_spec import FieldSpec as F
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class _PairedCfg:
9
+ gen: Callable[[dict], object] # ctx -> raw
10
+ store: Callable[[object, dict], object] # (raw, ctx) -> stored
11
+ alias: str
12
+ verbs: Tuple[str, ...]
13
+ emit: str # "pre_flush" | "post_refresh" | "readtime"
14
+ alias_field: F
15
+ mask_last: int | None
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class _AssembleCfg:
20
+ sources: Tuple[str, ...]
21
+ fn: Callable[[dict, dict], object] # (payload, ctx) -> stored
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class _ReadtimeAlias:
26
+ name: str
27
+ fn: Callable[[object, dict], object] # (obj, ctx) -> alias value
28
+ verbs: Tuple[str, ...]
29
+ alias_field: F
30
+ mask_last: int | None
31
+
32
+
33
+ EmitPoint = Literal["pre_flush", "post_refresh", "pre_response", "readtime"]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Pair:
38
+ raw: object
39
+ stored: object
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class IOSpec:
44
+ """Control how a column participates in API input and output.
45
+
46
+ ``in_verbs`` and ``out_verbs`` enumerate which operations may accept or
47
+ emit the field. ``alias_in``/``alias_out`` allow different payload keys,
48
+ ``sensitive`` and ``redact_last`` mark values for masking, and
49
+ ``filter_ops``/``sortable`` flag whether the field can be used in query
50
+ filters or ordering. Advanced helpers like :meth:`assemble`,
51
+ :meth:`paired`, and :meth:`alias_readtime` derive values or expose extra
52
+ read-time aliases.
53
+ """
54
+
55
+ in_verbs: Tuple[str, ...] = ()
56
+ out_verbs: Tuple[str, ...] = ()
57
+ mutable_verbs: Tuple[str, ...] = ()
58
+ alias_in: str | None = None
59
+ alias_out: str | None = None
60
+ sensitive: bool = False
61
+ redact_last: int | None = None
62
+ filter_ops: Tuple[str, ...] = ()
63
+ sortable: bool = False
64
+ allow_in: Callable[[str, dict], bool] | None = None
65
+ allow_out: Callable[[str, dict], bool] | None = None
66
+ _paired: _PairedCfg | None = None
67
+ _assemble: _AssembleCfg | None = None
68
+ _readtime_aliases: Tuple[_ReadtimeAlias, ...] = ()
69
+
70
+ def assemble(self, sources, fn):
71
+ """Return a new spec that derives a value from ``sources`` using ``fn``."""
72
+ cfg = _AssembleCfg(sources=tuple(sources), fn=fn)
73
+ return replace(self, _assemble=cfg)
74
+
75
+ def paired(
76
+ self,
77
+ make,
78
+ *,
79
+ alias,
80
+ verbs=("create",),
81
+ emit: EmitPoint = "pre_flush",
82
+ alias_field: F = F(py_type=str),
83
+ mask_last: int | None = None,
84
+ ):
85
+ """Return a new spec with a paired field configuration."""
86
+
87
+ def gen(ctx):
88
+ pair = make(ctx)
89
+ temp = (
90
+ ctx.get("temp") if isinstance(ctx, dict) else getattr(ctx, "temp", None)
91
+ )
92
+ if isinstance(temp, dict):
93
+ temp.setdefault("_paired_cache", {})[alias] = pair.stored
94
+ return pair.raw
95
+
96
+ def store(raw, ctx):
97
+ temp = getattr(ctx, "temp", None)
98
+ if isinstance(temp, dict):
99
+ cached = temp.get("_paired_cache", {}).pop(alias, None)
100
+ if cached is not None:
101
+ return cached
102
+ return make(ctx).stored
103
+
104
+ cfg = _PairedCfg(
105
+ gen=gen,
106
+ store=store,
107
+ alias=alias,
108
+ verbs=tuple(verbs),
109
+ emit=emit,
110
+ alias_field=alias_field,
111
+ mask_last=mask_last,
112
+ )
113
+ return replace(self, _paired=cfg)
114
+
115
+ def alias_readtime(
116
+ self,
117
+ name,
118
+ fn,
119
+ *,
120
+ verbs=("read", "list"),
121
+ alias_field: F = F(py_type=str),
122
+ mask_last: int | None = None,
123
+ ):
124
+ """Return a new spec with an additional read-time alias."""
125
+
126
+ alias_cfg = _ReadtimeAlias(
127
+ name=name,
128
+ fn=fn,
129
+ verbs=tuple(verbs),
130
+ alias_field=alias_field,
131
+ mask_last=mask_last,
132
+ )
133
+ return replace(self, _readtime_aliases=self._readtime_aliases + (alias_cfg,))
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from functools import lru_cache
5
+ from typing import Dict
6
+
7
+ from .column_spec import ColumnSpec
8
+ from .io_spec import IOSpec as IO
9
+ from .storage_spec import StorageSpec as S
10
+
11
+ logger = logging.getLogger("uvicorn")
12
+
13
+
14
+ # Default inbound/outbound verbs for columns lacking an explicit ColumnSpec.
15
+ #
16
+ # Without this, plain SQLAlchemy ``Column`` definitions are omitted from the
17
+ # collected spec map, causing downstream components to treat their values as
18
+ # unknown. By seeding such columns with a permissive IO spec we ensure they
19
+ # participate in canonical CRUD operations just like columns defined via
20
+ # ``acol``.
21
+ _DEFAULT_IO = IO(
22
+ in_verbs=("create", "update", "replace"),
23
+ out_verbs=("read", "list"),
24
+ mutable_verbs=("create", "update", "replace"),
25
+ )
26
+
27
+
28
+ @lru_cache(maxsize=None)
29
+ def mro_collect_columns(
30
+ model: type, *, _cache_bust: int | None = None
31
+ ) -> Dict[str, ColumnSpec]:
32
+ """Collect ColumnSpecs declared on *model* and all mixins.
33
+
34
+ Iterates across the model's MRO so that mixin-defined columns are included
35
+ in the resulting mapping. Later definitions take precedence over earlier
36
+ ones in the MRO. Any table-backed columns lacking a spec are populated with
37
+ a default ColumnSpec so they participate in opviews and schema generation.
38
+ """
39
+ logger.info("Collecting columns for %s", model.__name__)
40
+ out: Dict[str, ColumnSpec] = {}
41
+ for base in reversed(model.__mro__):
42
+ mapping = getattr(base, "__tigrbl_colspecs__", None)
43
+ if isinstance(mapping, dict):
44
+ out.update(mapping)
45
+ mapping = getattr(base, "__tigrbl_cols__", None)
46
+ if isinstance(mapping, dict):
47
+ out.update(mapping)
48
+
49
+ table = getattr(model, "__table__", None)
50
+ if table is not None:
51
+ for col in table.columns:
52
+ name = getattr(col, "key", None) or col.name
53
+ out.setdefault(name, ColumnSpec(storage=S(), io=_DEFAULT_IO))
54
+
55
+ logger.info("Collected %d columns for %s", len(out), model.__name__)
56
+ return out
57
+
58
+
59
+ __all__ = ["mro_collect_columns"]
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional
4
+
5
+ from ._column import Column
6
+ from .column_spec import ColumnSpec
7
+ from .field_spec import FieldSpec as F
8
+ from .io_spec import IOSpec as IO
9
+ from .storage_spec import StorageSpec as S
10
+
11
+ __all__ = [
12
+ "makeColumn",
13
+ "makeVirtualColumn",
14
+ "acol",
15
+ "vcol",
16
+ "F",
17
+ "IO",
18
+ "S",
19
+ "ColumnSpec",
20
+ "Column",
21
+ ]
22
+
23
+
24
+ def makeColumn(
25
+ *,
26
+ storage: S | None = None,
27
+ field: F | None = None,
28
+ io: IO | None = None,
29
+ default_factory: Optional[Callable[[dict], Any]] = None,
30
+ read_producer: Optional[Callable[[object, dict], Any]] = None,
31
+ spec: ColumnSpec | None = None,
32
+ **kw: Any,
33
+ ) -> Column:
34
+ """Return a :class:`Column` descriptor for declarative models."""
35
+ if spec is not None:
36
+ if any(
37
+ x is not None for x in (storage, field, io, default_factory, read_producer)
38
+ ):
39
+ raise ValueError("Provide either spec or individual components, not both.")
40
+ if spec is None:
41
+ if read_producer is not None and storage is not None:
42
+ raise ValueError(
43
+ "read_producer is only valid for virtual (storage=None) columns."
44
+ )
45
+ spec = ColumnSpec(
46
+ storage=storage,
47
+ field=field,
48
+ io=io,
49
+ default_factory=default_factory,
50
+ read_producer=read_producer,
51
+ )
52
+ return Column(spec=spec, **kw)
53
+
54
+
55
+ def makeVirtualColumn(
56
+ *,
57
+ field: F | None = None,
58
+ io: IO | None = None,
59
+ default_factory: Optional[Callable[[dict], Any]] = None,
60
+ producer: Optional[Callable[[object, dict], Any]] = None,
61
+ read_producer: Optional[Callable[[object, dict], Any]] = None,
62
+ spec: ColumnSpec | None = None,
63
+ **kw: Any,
64
+ ) -> Column:
65
+ """Convenience for wire-only virtual columns."""
66
+ if spec is not None:
67
+ if any(
68
+ x is not None for x in (field, io, default_factory, producer, read_producer)
69
+ ):
70
+ raise ValueError("Provide either spec or individual components, not both.")
71
+ return Column(spec=spec, **kw)
72
+ if producer is not None and read_producer is not None:
73
+ raise ValueError("Provide only one of producer= or read_producer=, not both.")
74
+ rp = read_producer or producer
75
+ return Column(
76
+ spec=ColumnSpec(
77
+ storage=None,
78
+ field=field,
79
+ io=io,
80
+ default_factory=default_factory,
81
+ read_producer=rp,
82
+ ),
83
+ **kw,
84
+ )
85
+
86
+
87
+ # Convenience aliases retained for backward compatibility
88
+ acol = makeColumn
89
+ vcol = makeVirtualColumn
@@ -0,0 +1,65 @@
1
+ # ---------------------------------------
2
+ # storage_spec.py (S)
3
+ # ---------------------------------------
4
+ from dataclasses import KW_ONLY, dataclass
5
+ from typing import Any, Literal, Union
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class StorageTransform:
10
+ """Functions used to transform values on the way to and from the database."""
11
+
12
+ to_stored: Union[callable, None] = (
13
+ None # (python, ctx) -> python persisted (e.g., hash/encrypt/normalize)
14
+ )
15
+ from_stored: Union[callable, None] = (
16
+ None # (python, ctx) -> python exposed (rare; only if you ever expose stored)
17
+ )
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ForeignKeySpec:
22
+ """Lightweight description of a foreign key relationship."""
23
+
24
+ target: str # "tenant(id)" or fully-qualified
25
+ on_delete: Literal["CASCADE", "RESTRICT", "SET NULL", "SET DEFAULT"] = "RESTRICT"
26
+ on_update: Literal["CASCADE", "RESTRICT", "SET NULL", "SET DEFAULT"] = "RESTRICT"
27
+ deferrable: bool = False
28
+ initially_deferred: bool = False
29
+ match: Literal["FULL", "PARTIAL", "SIMPLE"] = "SIMPLE"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class StorageSpec:
34
+ """Describe the database-level shape and behaviour of a column.
35
+
36
+ The spec maps closely to SQLAlchemy's :class:`~sqlalchemy.Column` keyword
37
+ arguments: ``type_`` and flags such as ``nullable`` or ``primary_key``
38
+ define the table schema while ``default`` and ``onupdate`` represent ORM
39
+ side defaults. ``server_default`` and ``refresh_on_return`` support
40
+ database-generated values. Optional helpers provide value transforms,
41
+ foreign keys, check constraints and comments.
42
+ """
43
+
44
+ # SQLAlchemy column shape (DDL/runtime)
45
+ type_: Any | None = None
46
+ _: KW_ONLY
47
+ nullable: bool | None = None
48
+ unique: bool = False
49
+ index: bool = False
50
+ primary_key: bool = False
51
+ autoincrement: bool | None = None
52
+
53
+ # ORM-side defaults (run in SQLAlchemy) – optional if you use API defaults/paired
54
+ default: Any | None = None # scalar or callable()
55
+ onupdate: Any | None = None
56
+
57
+ # DB-side defaults/generation (run in the database)
58
+ server_default: Any | None = None # e.g., func.now(), text("...")
59
+ refresh_on_return: bool = False # force refresh after flush when DB generated
60
+
61
+ # Optional storage helpers
62
+ transform: StorageTransform | None = None
63
+ fk: ForeignKeySpec | None = None
64
+ check: str | None = None
65
+ comment: str | None = None
@@ -0,0 +1,19 @@
1
+ # tigrbl/v3/config/__init__.py
2
+ """Tigrbl v3 – configuration surface.
3
+
4
+ Exports:
5
+ - DEFAULTS: canonical configuration defaults
6
+ - CfgView: read-only config view (attr + dict access)
7
+ - resolve_cfg(...): precedence-based merger across apps/api/tab/cols/op/overrides
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from .defaults import DEFAULTS
13
+ from .resolver import CfgView, resolve_cfg
14
+
15
+ __all__ = [
16
+ "DEFAULTS",
17
+ "CfgView",
18
+ "resolve_cfg",
19
+ ]