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
tigrbl/op/types.py ADDED
@@ -0,0 +1,136 @@
1
+ # tigrbl/v3/ops/types.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+
6
+ from typing import Any, Literal, Mapping, Optional, Tuple, cast
7
+
8
+ from ..config.constants import CANON as CANONICAL_VERB_TUPLE
9
+ from ..hook.types import PHASE, HookPhase, PHASES, Ctx, StepFn, HookPredicate
10
+ from ..hook import HookSpec as OpHook
11
+ from ..response.types import ResponseSpec
12
+ from ..engine.engine_spec import EngineCfg
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING: # pragma: no cover
16
+ from ..schema.types import SchemaArg
17
+ # ───────────────────────────────────────────────────────────────────────────────
18
+ # Core aliases & enums
19
+ # ───────────────────────────────────────────────────────────────────────────────
20
+
21
+ PersistPolicy = Literal[
22
+ "default",
23
+ "prepend",
24
+ "append",
25
+ "override",
26
+ "skip",
27
+ ] # TX policy
28
+ Arity = Literal["collection", "member"] # HTTP path shape
29
+
30
+ TargetOp = Literal[
31
+ "create",
32
+ "read",
33
+ "update",
34
+ "replace",
35
+ "merge",
36
+ "delete",
37
+ "list",
38
+ "clear",
39
+ "bulk_create",
40
+ "bulk_update",
41
+ "bulk_replace",
42
+ "bulk_merge",
43
+ "bulk_delete",
44
+ "custom",
45
+ ]
46
+
47
+ VerbAliasPolicy = Literal["both", "alias_only", "canonical_only"] # legacy export
48
+
49
+ # ───────────────────────────────────────────────────────────────────────────────
50
+ # Lazy-capable schema argument types
51
+ # ───────────────────────────────────────────────────────────────────────────────
52
+
53
+
54
+ # ───────────────────────────────────────────────────────────────────────────────
55
+ # Engine binding (optional, used by resolver precedence: op > table > api > app)
56
+ # ───────────────────────────────────────────────────────────────────────────────
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class OpSpec:
61
+ """
62
+ Single source of truth for an operation.
63
+
64
+ • `target` = canonical verb ("create"…,"custom")
65
+ • `arity` = REST shape ("member"|"collection")
66
+
67
+ Serialization mode is inferred **only** from schema presence:
68
+ - if model.schemas.<alias>.out exists → serialize
69
+ - otherwise → raw pass-through
70
+
71
+ Optional engine binding:
72
+ - `engine` allows per-op routing (DSN string or structured mapping).
73
+ When present, it participates in resolver precedence (op > table > api > app).
74
+ """
75
+
76
+ # Identity & exposure
77
+ alias: str
78
+ target: TargetOp
79
+ table: Optional[type] = None
80
+ expose_routes: bool = True
81
+ expose_rpc: bool = True
82
+ expose_method: bool = True
83
+
84
+ # Optional per-op engine binding (DSN string or mapping spec)
85
+ engine: Optional[EngineCfg] = None
86
+
87
+ # HTTP behavior
88
+ arity: Arity = "collection"
89
+ http_methods: Optional[Tuple[str, ...]] = None
90
+ path_suffix: Optional[str] = None
91
+ tags: Tuple[str, ...] = field(default_factory=tuple)
92
+ status_code: Optional[int] = None
93
+ response: Optional[ResponseSpec] = None
94
+
95
+ # Persistence
96
+ persist: PersistPolicy = "default"
97
+
98
+ # Schema overrides (resolved later by binder)
99
+ request_model: Optional[SchemaArg] = None
100
+ response_model: Optional[SchemaArg] = None
101
+
102
+ # Return shaping: "raw" passthrough vs "model" serialization
103
+ returns: Literal["raw", "model"] = "raw"
104
+
105
+ # Handler & hooks
106
+ handler: Optional[StepFn] = None
107
+ hooks: Tuple[OpHook, ...] = field(default_factory=tuple)
108
+
109
+ # RBAC / diagnostics
110
+ rbac_guard_op: Optional[str] = None
111
+ core: Optional[StepFn] = None
112
+ core_raw: Optional[StepFn] = None
113
+ extra: Mapping[str, Any] = field(default_factory=dict)
114
+ deps: Tuple[StepFn | str, ...] = field(default_factory=tuple)
115
+ secdeps: Tuple[StepFn | str, ...] = field(default_factory=tuple)
116
+
117
+
118
+ # Canonical verb set
119
+ CANON: Tuple[TargetOp, ...] = cast(Tuple[TargetOp, ...], CANONICAL_VERB_TUPLE)
120
+
121
+ __all__ = [
122
+ "PersistPolicy",
123
+ "Arity",
124
+ "TargetOp",
125
+ "VerbAliasPolicy",
126
+ "PHASE",
127
+ "HookPhase",
128
+ "PHASES",
129
+ "Ctx",
130
+ "StepFn",
131
+ "HookPredicate",
132
+ "EngineCfg",
133
+ "OpHook",
134
+ "OpSpec",
135
+ "CANON",
136
+ ]
tigrbl/orm/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """ORM utilities containing SQLAlchemy tables and mixins."""
@@ -0,0 +1,83 @@
1
+ # tigrbl/v3/mixins/_RowBound.py
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Mapping, Sequence
5
+
6
+ from ...runtime.errors import HTTP_ERROR_MESSAGES, create_standardized_error
7
+
8
+
9
+ class _RowBound:
10
+ """
11
+ Base mix-in for row-level visibility.
12
+
13
+ Concrete subclasses **must** override:
14
+
15
+ @staticmethod
16
+ def is_visible(obj, ctx) -> bool
17
+
18
+ Hooks are wired only if the subclass actually provides an implementation.
19
+ """
20
+
21
+ # ────────────────────────────────────────────────────────────────────
22
+ # Tigrbl bootstrap
23
+ # -------------------------------------------------------------------
24
+ def __init_subclass__(cls, **kw):
25
+ super().__init_subclass__(**kw)
26
+ cls._install_rowbound_hooks()
27
+
28
+ @classmethod
29
+ def _install_rowbound_hooks(cls) -> None:
30
+ # Skip abstract helpers or unmapped mix-ins
31
+ if cls.is_visible is _RowBound.is_visible:
32
+ return
33
+ if not hasattr(cls, "__table__"):
34
+ return
35
+
36
+ hook = cls._make_row_visibility_hook()
37
+ hooks_attr = getattr(cls, "__tigrbl_hooks__", {})
38
+ hooks = {**hooks_attr} if isinstance(hooks_attr, dict) else {}
39
+
40
+ def _append(alias: str, phase: str, fn) -> None:
41
+ phase_map = hooks.get(alias) or {}
42
+ lst = list(phase_map.get(phase) or [])
43
+ if fn not in lst:
44
+ lst.append(fn)
45
+ phase_map[phase] = tuple(lst)
46
+ hooks[alias] = phase_map
47
+
48
+ for op in ("read", "list"):
49
+ _append(op, "POST_HANDLER", hook)
50
+
51
+ setattr(cls, "__tigrbl_hooks__", hooks)
52
+
53
+ # ────────────────────────────────────────────────────────────────────
54
+ # Per-request hook
55
+ # -------------------------------------------------------------------
56
+ @classmethod
57
+ def _make_row_visibility_hook(cls):
58
+ def _row_visibility_hook(ctx: Mapping[str, Any]) -> None:
59
+ if "result" not in ctx: # nothing to filter
60
+ return
61
+
62
+ res = ctx["result"]
63
+
64
+ # LIST → keep only visible rows
65
+ if isinstance(res, Sequence):
66
+ ctx["result"] = [row for row in res if cls.is_visible(row, ctx)]
67
+ return
68
+
69
+ # READ → invisible row → pretend 404
70
+ if not cls.is_visible(res, ctx):
71
+ http_exc, _, _ = create_standardized_error(
72
+ 404, message=HTTP_ERROR_MESSAGES[404]
73
+ )
74
+ raise http_exc
75
+
76
+ return _row_visibility_hook
77
+
78
+ # -------------------------------------------------------------------
79
+ # Must be overridden
80
+ # -------------------------------------------------------------------
81
+ @staticmethod
82
+ def is_visible(obj, ctx) -> bool: # pragma: no cover
83
+ raise NotImplementedError
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ from .bootstrappable import Bootstrappable
4
+ from .upsertable import Upsertable
5
+ from .ownable import Ownable, OwnerPolicy
6
+ from .tenant_bound import TenantBound, TenantPolicy
7
+ from .key_digest import KeyDigest
8
+
9
+ from .utils import (
10
+ tzutcnow,
11
+ tzutcnow_plus_day,
12
+ _infer_schema,
13
+ uuid_example,
14
+ CRUD_IN,
15
+ CRUD_OUT,
16
+ CRUD_IO,
17
+ RO_IO,
18
+ )
19
+ from .principals import GUIDPk, TenantColumn, UserColumn, OrgColumn, Principal
20
+ from .bound import OwnerBound, UserBound
21
+ from .lifecycle import (
22
+ Created,
23
+ LastUsed,
24
+ Timestamped,
25
+ ActiveToggle,
26
+ SoftDelete,
27
+ Versioned,
28
+ )
29
+ from .hierarchy import Contained, TreeNode
30
+ from .edges import RelationEdge, MaskableEdge, TaggableEdge
31
+ from .markers import AsyncCapable, Audited
32
+ from .locks import RowLock, SoftLock
33
+ from .operations import BulkCapable, Replaceable, Mergeable, Streamable
34
+ from .fields import (
35
+ Slugged,
36
+ StatusColumn,
37
+ ValidityWindow,
38
+ Monetary,
39
+ ExtRef,
40
+ MetaJSON,
41
+ BlobRef,
42
+ SearchVector,
43
+ )
44
+
45
+ __all__ = [
46
+ "Bootstrappable",
47
+ "Upsertable",
48
+ "Ownable",
49
+ "OwnerPolicy",
50
+ "TenantBound",
51
+ "TenantPolicy",
52
+ "KeyDigest",
53
+ "tzutcnow",
54
+ "tzutcnow_plus_day",
55
+ "_infer_schema",
56
+ "uuid_example",
57
+ "CRUD_IN",
58
+ "CRUD_OUT",
59
+ "CRUD_IO",
60
+ "RO_IO",
61
+ "GUIDPk",
62
+ "TenantColumn",
63
+ "UserColumn",
64
+ "OrgColumn",
65
+ "Principal",
66
+ "OwnerBound",
67
+ "UserBound",
68
+ "Created",
69
+ "LastUsed",
70
+ "Timestamped",
71
+ "ActiveToggle",
72
+ "SoftDelete",
73
+ "Versioned",
74
+ "Contained",
75
+ "TreeNode",
76
+ "RelationEdge",
77
+ "MaskableEdge",
78
+ "TaggableEdge",
79
+ "AsyncCapable",
80
+ "Audited",
81
+ "RowLock",
82
+ "SoftLock",
83
+ "BulkCapable",
84
+ "Replaceable",
85
+ "Mergeable",
86
+ "Streamable",
87
+ "Slugged",
88
+ "StatusColumn",
89
+ "ValidityWindow",
90
+ "Monetary",
91
+ "ExtRef",
92
+ "MetaJSON",
93
+ "BlobRef",
94
+ "SearchVector",
95
+ ]
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, ClassVar, Iterable
4
+ import logging
5
+ import sqlalchemy as sa
6
+ from sqlalchemy import inspect as sa_inspect
7
+ from sqlalchemy.exc import IntegrityError
8
+
9
+ from ...types import Session, event
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+
14
+ class Bootstrappable:
15
+ """
16
+ Seed DEFAULT_ROWS for *this mapped class only* with zero magic.
17
+
18
+ Rules:
19
+ - Insert ONLY keys present on this class's mapped columns.
20
+ - No auto defaults, no timestamp injection, no unique probing.
21
+ - Idempotency only if ALL primary key columns are present in the row.
22
+ - Listener is attached to cls.__table__ (no cross-class/global effects).
23
+ """
24
+
25
+ DEFAULT_ROWS: ClassVar[list[dict[str, Any]]] = []
26
+
27
+ def __init_subclass__(cls, **kw):
28
+ super().__init_subclass__(**kw)
29
+ event.listen(
30
+ cls.__table__, "after_create", cls._after_create_insert_default_rows
31
+ )
32
+
33
+ @classmethod
34
+ def _after_create_insert_default_rows(cls, target, connection, **_):
35
+ if not getattr(cls, "DEFAULT_ROWS", None):
36
+ return
37
+ from sqlalchemy.orm import sessionmaker
38
+
39
+ SessionLocal = sessionmaker(bind=connection, future=True)
40
+ db: Session = SessionLocal()
41
+ try:
42
+ cls._insert_rows(db, cls.DEFAULT_ROWS)
43
+ db.commit()
44
+ except Exception as e:
45
+ db.rollback()
46
+ log.warning(
47
+ "Bootstrappable seed failed for %s: %s",
48
+ cls.__name__,
49
+ repr(e),
50
+ exc_info=True,
51
+ )
52
+ finally:
53
+ db.close()
54
+
55
+ @classmethod
56
+ def ensure_bootstrapped(
57
+ cls, db: Session, rows: Iterable[dict[str, Any]] | None = None
58
+ ) -> None:
59
+ rows = cls.DEFAULT_ROWS if rows is None else list(rows)
60
+ if rows:
61
+ cls._insert_rows(db, rows)
62
+
63
+ @classmethod
64
+ def _insert_rows(cls, db: Session, rows: Iterable[dict[str, Any]]) -> None:
65
+ mapper = sa_inspect(cls)
66
+
67
+ # --- pick an insertable target (avoid boolean eval + avoid JOINs) ---
68
+ local = mapper.local_table
69
+ table = local if local is not None else mapper.persist_selectable
70
+ if not hasattr(table, "insert"): # e.g., persist_selectable is a JOIN
71
+ table = mapper.local_table
72
+
73
+ col_keys = {c.key for c in mapper.columns}
74
+ pk_cols = list(table.primary_key.columns) if table.primary_key else []
75
+ pk_keys = {c.key for c in pk_cols}
76
+
77
+ def clean(r: dict[str, Any]) -> dict[str, Any]:
78
+ # keep only columns mapped on THIS class
79
+ return {k: r[k] for k in r.keys() & col_keys}
80
+
81
+ payloads = [clean(r) for r in rows if r]
82
+ if not payloads:
83
+ return
84
+
85
+ # Idempotent path if all PK columns are provided (per row)
86
+ can_upsert = bool(pk_cols) and all(pk_keys <= set(p.keys()) for p in payloads)
87
+
88
+ dialect = db.get_bind().dialect.name
89
+
90
+ if can_upsert and dialect == "postgresql":
91
+ from sqlalchemy.dialects.postgresql import insert as pg_insert
92
+
93
+ stmt = (
94
+ pg_insert(table)
95
+ .values(payloads)
96
+ .on_conflict_do_nothing(index_elements=[c.name for c in pk_cols])
97
+ )
98
+ db.execute(stmt)
99
+ return
100
+
101
+ if can_upsert and dialect == "sqlite":
102
+ # Best-effort idempotency for SQLite
103
+ from sqlalchemy.dialects.sqlite import insert as sqlite_insert
104
+
105
+ db.execute(sqlite_insert(table).values(payloads).prefix_with("OR IGNORE"))
106
+ return
107
+
108
+ # Fallback: plain inserts; swallow duplicate races
109
+ for p in payloads:
110
+ try:
111
+ db.execute(sa.insert(table).values(**p))
112
+ except IntegrityError:
113
+ db.rollback() # treat as already present
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from ...config.constants import CTX_AUTH_KEY, CTX_USER_ID_KEY
4
+ from ...specs import ColumnSpec, F, S, acol
5
+ from ...specs.storage_spec import ForeignKeySpec
6
+ from ...types import PgUUID, UUID, Mapped
7
+
8
+ from .utils import uuid_example, CRUD_IO
9
+
10
+
11
+ class OwnerBound:
12
+ owner_id: Mapped[UUID] = acol(
13
+ spec=ColumnSpec(
14
+ storage=S(
15
+ type_=PgUUID(as_uuid=True),
16
+ fk=ForeignKeySpec(target="users.id"),
17
+ ),
18
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
19
+ io=CRUD_IO,
20
+ )
21
+ )
22
+
23
+ @classmethod
24
+ def filter_for_ctx(cls, q, ctx):
25
+ auto_fields = ctx.get(CTX_AUTH_KEY, {})
26
+ return q.filter(cls.owner_id == auto_fields.get(CTX_USER_ID_KEY))
27
+
28
+
29
+ class UserBound:
30
+ user_id: Mapped[UUID] = acol(
31
+ spec=ColumnSpec(
32
+ storage=S(
33
+ type_=PgUUID(as_uuid=True),
34
+ fk=ForeignKeySpec(target="users.id"),
35
+ ),
36
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
37
+ io=CRUD_IO,
38
+ )
39
+ )
40
+
41
+ @classmethod
42
+ def filter_for_ctx(cls, q, ctx):
43
+ auto_fields = ctx.get(CTX_AUTH_KEY, {})
44
+ return q.filter(cls.user_id == auto_fields.get(CTX_USER_ID_KEY))
45
+
46
+
47
+ __all__ = ["OwnerBound", "UserBound"]
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from ...specs import ColumnSpec, F, S, acol
4
+ from ...types import Integer, String, declarative_mixin, Mapped
5
+
6
+ from .utils import CRUD_IO
7
+
8
+
9
+ @declarative_mixin
10
+ class RelationEdge:
11
+ """Marker: row itself is an association—no extra columns required."""
12
+
13
+ pass
14
+
15
+
16
+ @declarative_mixin
17
+ class MaskableEdge:
18
+ """Edge row with bitmap of verbs/roles."""
19
+
20
+ mask: Mapped[int] = acol(
21
+ spec=ColumnSpec(
22
+ storage=S(type_=Integer, nullable=False),
23
+ field=F(py_type=int),
24
+ io=CRUD_IO,
25
+ )
26
+ )
27
+
28
+
29
+ @declarative_mixin
30
+ class TaggableEdge:
31
+ tag: Mapped[str] = acol(
32
+ spec=ColumnSpec(
33
+ storage=S(type_=String, nullable=False),
34
+ field=F(py_type=str),
35
+ io=CRUD_IO,
36
+ )
37
+ )
38
+
39
+
40
+ __all__ = ["RelationEdge", "MaskableEdge", "TaggableEdge"]
@@ -0,0 +1,165 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ from decimal import Decimal
5
+
6
+ from ...specs import ColumnSpec, F, IO, S, acol
7
+ from ...types import (
8
+ TZDateTime,
9
+ PgUUID,
10
+ String,
11
+ SAEnum,
12
+ Numeric,
13
+ JSONB,
14
+ TSVECTOR,
15
+ UUID,
16
+ Index,
17
+ declarative_mixin,
18
+ declared_attr,
19
+ Mapped,
20
+ )
21
+
22
+ from .utils import tzutcnow, tzutcnow_plus_day, CRUD_IO
23
+
24
+
25
+ @declarative_mixin
26
+ class Slugged:
27
+ slug: Mapped[str] = acol(
28
+ spec=ColumnSpec(
29
+ storage=S(type_=String, unique=True, nullable=False),
30
+ field=F(py_type=str, constraints={"max_length": 120}),
31
+ io=CRUD_IO,
32
+ )
33
+ )
34
+
35
+
36
+ @declarative_mixin
37
+ class StatusColumn:
38
+ status: Mapped[str] = acol(
39
+ spec=ColumnSpec(
40
+ storage=S(
41
+ type_=SAEnum(
42
+ "queued",
43
+ "waiting",
44
+ "input_required",
45
+ "auth_required",
46
+ "approved",
47
+ "rejected",
48
+ "dispatched",
49
+ "running",
50
+ "paused",
51
+ "success",
52
+ "failed",
53
+ "cancelled",
54
+ name="status_enum",
55
+ ),
56
+ default="waiting",
57
+ nullable=False,
58
+ ),
59
+ field=F(py_type=str),
60
+ io=CRUD_IO,
61
+ )
62
+ )
63
+
64
+
65
+ @declarative_mixin
66
+ class ValidityWindow:
67
+ valid_from: Mapped[dt.datetime] = acol(
68
+ spec=ColumnSpec(
69
+ storage=S(type_=TZDateTime, default=tzutcnow, nullable=False),
70
+ field=F(py_type=dt.datetime),
71
+ io=CRUD_IO,
72
+ )
73
+ )
74
+ valid_to: Mapped[dt.datetime | None] = acol(
75
+ spec=ColumnSpec(
76
+ storage=S(type_=TZDateTime, default=tzutcnow_plus_day),
77
+ field=F(py_type=dt.datetime),
78
+ io=CRUD_IO,
79
+ )
80
+ )
81
+
82
+
83
+ @declarative_mixin
84
+ class Monetary:
85
+ amount: Mapped[Decimal] = acol(
86
+ spec=ColumnSpec(
87
+ storage=S(type_=Numeric(18, 2), nullable=False),
88
+ field=F(py_type=Decimal),
89
+ io=CRUD_IO,
90
+ )
91
+ )
92
+ currency: Mapped[str] = acol(
93
+ spec=ColumnSpec(
94
+ storage=S(type_=String, default="USD", nullable=False),
95
+ field=F(py_type=str, constraints={"max_length": 3}),
96
+ io=CRUD_IO,
97
+ )
98
+ )
99
+
100
+
101
+ @declarative_mixin
102
+ class ExtRef:
103
+ external_id: Mapped[str] = acol(
104
+ spec=ColumnSpec(
105
+ storage=S(type_=String),
106
+ field=F(py_type=str),
107
+ io=CRUD_IO,
108
+ )
109
+ )
110
+ provider: Mapped[str] = acol(
111
+ spec=ColumnSpec(
112
+ storage=S(type_=String),
113
+ field=F(py_type=str),
114
+ io=CRUD_IO,
115
+ )
116
+ )
117
+
118
+
119
+ @declarative_mixin
120
+ class MetaJSON:
121
+ meta: Mapped[dict] = acol(
122
+ spec=ColumnSpec(
123
+ storage=S(type_=JSONB, default=dict),
124
+ field=F(py_type=dict),
125
+ io=CRUD_IO,
126
+ )
127
+ )
128
+
129
+
130
+ @declarative_mixin
131
+ class BlobRef:
132
+ blob_id: Mapped[UUID | None] = acol(
133
+ spec=ColumnSpec(
134
+ storage=S(type_=PgUUID(as_uuid=True)),
135
+ field=F(py_type=UUID),
136
+ io=CRUD_IO,
137
+ )
138
+ )
139
+
140
+
141
+ @declarative_mixin
142
+ class SearchVector:
143
+ tsv: Mapped[str] = acol(
144
+ spec=ColumnSpec(
145
+ storage=S(type_=TSVECTOR),
146
+ field=F(py_type=str),
147
+ io=IO(),
148
+ )
149
+ )
150
+
151
+ @declared_attr
152
+ def __table_args__(cls):
153
+ return (Index(f"ix_{cls.__tablename__}_tsv", "tsv"),)
154
+
155
+
156
+ __all__ = [
157
+ "Slugged",
158
+ "StatusColumn",
159
+ "ValidityWindow",
160
+ "Monetary",
161
+ "ExtRef",
162
+ "MetaJSON",
163
+ "BlobRef",
164
+ "SearchVector",
165
+ ]