tigrbl-orm 0.4.5.dev1__tar.gz → 0.4.5.dev6__tar.gz

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 (34) hide show
  1. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/PKG-INFO +7 -8
  2. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/README.md +5 -6
  3. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/pyproject.toml +2 -2
  4. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/__init__.py +11 -1
  5. tigrbl_orm-0.4.5.dev6/tigrbl_orm/orm/mixins/principals.py +214 -0
  6. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/utils.py +1 -13
  7. tigrbl_orm-0.4.5.dev1/tigrbl_orm/orm/mixins/principals.py +0 -98
  8. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/LICENSE +0 -0
  9. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/NOTICE +0 -0
  10. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/__init__.py +0 -0
  11. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/_RowBound.py +0 -0
  12. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/bootstrappable.py +0 -0
  13. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/bound.py +0 -0
  14. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/edges.py +0 -0
  15. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/fields.py +0 -0
  16. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/hierarchy.py +0 -0
  17. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/key_digest.py +0 -0
  18. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/lifecycle.py +0 -0
  19. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/locks.py +0 -0
  20. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/markers.py +0 -0
  21. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/operations.py +0 -0
  22. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/ownable.py +0 -0
  23. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/tenant_bound.py +0 -0
  24. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/mixins/upsertable.py +0 -0
  25. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/__init__.py +0 -0
  26. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/_base.py +0 -0
  27. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/audit.py +0 -0
  28. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/client.py +0 -0
  29. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/group.py +0 -0
  30. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/org.py +0 -0
  31. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/rbac.py +0 -0
  32. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/status.py +0 -0
  33. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/tenant.py +0 -0
  34. {tigrbl_orm-0.4.5.dev1 → tigrbl_orm-0.4.5.dev6}/tigrbl_orm/orm/tables/user.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl-orm
3
- Version: 0.4.5.dev1
3
+ Version: 0.4.5.dev6
4
4
  Summary: SQLAlchemy ORM tables, mixins, columns, model helpers, and persistence primitives for Tigrbl applications.
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -230,7 +230,7 @@ Project-URL: Discord, https://discord.gg/K4YTAPapjR
230
230
  Project-URL: Homepage, https://github.com/tigrbl/tigrbl
231
231
  Project-URL: Issues, https://github.com/tigrbl/tigrbl/issues
232
232
  Project-URL: Organization, https://github.com/tigrbl
233
- Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm
233
+ Project-URL: Repository, https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm
234
234
  Description-Content-Type: text/markdown
235
235
 
236
236
  <div align="center">
@@ -240,7 +240,7 @@ Description-Content-Type: text/markdown
240
240
  <a href="https://pypi.org/project/tigrbl-orm/"><img src="https://img.shields.io/pypi/v/tigrbl-orm?label=PyPI" alt="PyPI version for tigrbl-orm"/></a>
241
241
  <a href="https://pypi.org/project/tigrbl-orm/"><img src="https://static.pepy.tech/badge/tigrbl-orm" alt="Downloads for tigrbl-orm"/></a>
242
242
  <a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord community for tigrbl-orm"/></a>
243
- <a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/core/tigrbl_orm/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/core/tigrbl_orm/README.md.svg?label=hits" alt="Repository hits for tigrbl-orm README"/></a>
243
+ <a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/30_orm/tigrbl_orm/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/30_orm/tigrbl_orm/README.md.svg?label=hits" alt="Repository hits for tigrbl-orm README"/></a>
244
244
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
245
245
  <a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python versions 3.10 | 3.11 | 3.12 | 3.13 | 3.14 for tigrbl-orm"/></a>
246
246
  <a href="https://github.com/tigrbl/tigrbl/blob/master/docs/README.md"><img src="https://img.shields.io/badge/workspace-core-1f6feb" alt="Workspace group for tigrbl-orm"/></a>
@@ -264,7 +264,7 @@ Framework maintainers, extension authors, and advanced users composing Tigrbl fr
264
264
 
265
265
  ## Where does tigrbl-orm fit?
266
266
 
267
- `tigrbl-orm` lives at `pkgs/core/tigrbl_orm` and serves a focused layer in the split Tigrbl framework.
267
+ `tigrbl-orm` lives at `pkgs/30_orm/tigrbl_orm` and serves a focused layer in the split Tigrbl framework.
268
268
 
269
269
  ## How does tigrbl-orm work?
270
270
 
@@ -286,7 +286,7 @@ pip install tigrbl-orm
286
286
  | Surface | Value |
287
287
  |---|---|
288
288
  | PyPI package | [`tigrbl-orm`](https://pypi.org/project/tigrbl-orm/) |
289
- | Repository path | [`pkgs/core/tigrbl_orm`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm) |
289
+ | Repository path | [`pkgs/30_orm/tigrbl_orm`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm) |
290
290
  | Python import root | `tigrbl_orm` |
291
291
  | Console scripts | none declared |
292
292
  | Entry points | none declared |
@@ -395,7 +395,6 @@ Choose `tigrbl-orm` when the quick-answer table matches your use case. Choose [`
395
395
  - [Package catalog](https://github.com/tigrbl/tigrbl/blob/master/docs/developer/PACKAGE_CATALOG.md)
396
396
  - [Package layout](https://github.com/tigrbl/tigrbl/blob/master/docs/developer/PACKAGE_LAYOUT.md)
397
397
  - [Current target](https://github.com/tigrbl/tigrbl/blob/master/docs/conformance/CURRENT_TARGET.md)
398
- - [Current state](https://github.com/tigrbl/tigrbl/blob/master/docs/conformance/CURRENT_STATE.md)
399
398
  - [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json)
400
399
  - [Release workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml)
401
400
 
@@ -403,7 +402,7 @@ Choose `tigrbl-orm` when the quick-answer table matches your use case. Choose [`
403
402
 
404
403
  - Community: [Discord](https://discord.gg/K4YTAPapjR).
405
404
  - Issues: [GitHub Issues](https://github.com/tigrbl/tigrbl/issues).
406
- - Repository: [pkgs/core/tigrbl_orm](https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm).
405
+ - Repository: [pkgs/30_orm/tigrbl_orm](https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm).
407
406
 
408
407
  ## Package-local Boundary
409
408
 
@@ -414,7 +413,7 @@ This README is the package-local distribution entry point for `tigrbl-orm`. It a
414
413
  - Package status: governed package in the `tigrbl/tigrbl` workspace.
415
414
  - Governance source: [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json).
416
415
  - Release evidence: [publish workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml) validates package builds, tests, GitHub release assets, and PyPI publication for managed packages.
417
- - Local certification guard: `pkgs/core/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
416
+ - Local certification guard: `pkgs/97_tests/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
418
417
  - Scope note: this README documents the package boundary. Runtime feature support remains governed by `.ssot/` entities and the conformance docs linked below.
419
418
 
420
419
  ## License
@@ -5,7 +5,7 @@
5
5
  <a href="https://pypi.org/project/tigrbl-orm/"><img src="https://img.shields.io/pypi/v/tigrbl-orm?label=PyPI" alt="PyPI version for tigrbl-orm"/></a>
6
6
  <a href="https://pypi.org/project/tigrbl-orm/"><img src="https://static.pepy.tech/badge/tigrbl-orm" alt="Downloads for tigrbl-orm"/></a>
7
7
  <a href="https://discord.gg/K4YTAPapjR"><img src="https://img.shields.io/badge/Discord-Join%20chat-5865F2?logo=discord&logoColor=white" alt="Discord community for tigrbl-orm"/></a>
8
- <a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/core/tigrbl_orm/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/core/tigrbl_orm/README.md.svg?label=hits" alt="Repository hits for tigrbl-orm README"/></a>
8
+ <a href="https://github.com/tigrbl/tigrbl/blob/master/pkgs/30_orm/tigrbl_orm/README.md"><img src="https://hits.sh/github.com/tigrbl/tigrbl/blob/master/pkgs/30_orm/tigrbl_orm/README.md.svg?label=hits" alt="Repository hits for tigrbl-orm README"/></a>
9
9
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-525252" alt="Apache 2.0 license"/></a>
10
10
  <a href="pyproject.toml"><img src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-3776ab" alt="Python versions 3.10 | 3.11 | 3.12 | 3.13 | 3.14 for tigrbl-orm"/></a>
11
11
  <a href="https://github.com/tigrbl/tigrbl/blob/master/docs/README.md"><img src="https://img.shields.io/badge/workspace-core-1f6feb" alt="Workspace group for tigrbl-orm"/></a>
@@ -29,7 +29,7 @@ Framework maintainers, extension authors, and advanced users composing Tigrbl fr
29
29
 
30
30
  ## Where does tigrbl-orm fit?
31
31
 
32
- `tigrbl-orm` lives at `pkgs/core/tigrbl_orm` and serves a focused layer in the split Tigrbl framework.
32
+ `tigrbl-orm` lives at `pkgs/30_orm/tigrbl_orm` and serves a focused layer in the split Tigrbl framework.
33
33
 
34
34
  ## How does tigrbl-orm work?
35
35
 
@@ -51,7 +51,7 @@ pip install tigrbl-orm
51
51
  | Surface | Value |
52
52
  |---|---|
53
53
  | PyPI package | [`tigrbl-orm`](https://pypi.org/project/tigrbl-orm/) |
54
- | Repository path | [`pkgs/core/tigrbl_orm`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm) |
54
+ | Repository path | [`pkgs/30_orm/tigrbl_orm`](https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm) |
55
55
  | Python import root | `tigrbl_orm` |
56
56
  | Console scripts | none declared |
57
57
  | Entry points | none declared |
@@ -160,7 +160,6 @@ Choose `tigrbl-orm` when the quick-answer table matches your use case. Choose [`
160
160
  - [Package catalog](https://github.com/tigrbl/tigrbl/blob/master/docs/developer/PACKAGE_CATALOG.md)
161
161
  - [Package layout](https://github.com/tigrbl/tigrbl/blob/master/docs/developer/PACKAGE_LAYOUT.md)
162
162
  - [Current target](https://github.com/tigrbl/tigrbl/blob/master/docs/conformance/CURRENT_TARGET.md)
163
- - [Current state](https://github.com/tigrbl/tigrbl/blob/master/docs/conformance/CURRENT_STATE.md)
164
163
  - [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json)
165
164
  - [Release workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml)
166
165
 
@@ -168,7 +167,7 @@ Choose `tigrbl-orm` when the quick-answer table matches your use case. Choose [`
168
167
 
169
168
  - Community: [Discord](https://discord.gg/K4YTAPapjR).
170
169
  - Issues: [GitHub Issues](https://github.com/tigrbl/tigrbl/issues).
171
- - Repository: [pkgs/core/tigrbl_orm](https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm).
170
+ - Repository: [pkgs/30_orm/tigrbl_orm](https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm).
172
171
 
173
172
  ## Package-local Boundary
174
173
 
@@ -179,7 +178,7 @@ This README is the package-local distribution entry point for `tigrbl-orm`. It a
179
178
  - Package status: governed package in the `tigrbl/tigrbl` workspace.
180
179
  - Governance source: [SSOT registry](https://github.com/tigrbl/tigrbl/blob/master/.ssot/registry.json).
181
180
  - Release evidence: [publish workflow](https://github.com/tigrbl/tigrbl/actions/workflows/publish.yml) validates package builds, tests, GitHub release assets, and PyPI publication for managed packages.
182
- - Local certification guard: `pkgs/core/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
181
+ - Local certification guard: `pkgs/97_tests/tigrbl_tests/tests/unit/test_package_badges_and_notices.py` verifies every package README keeps the Discord badge, Apache 2.0 badge, explicit Python-version badge, `LICENSE`, and `NOTICE`.
183
182
  - Scope note: this README documents the package boundary. Runtime feature support remains governed by `.ssot/` entities and the conformance docs linked below.
184
183
 
185
184
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tigrbl-orm"
3
- version = "0.4.5.dev1"
3
+ version = "0.4.5.dev6"
4
4
  description = "SQLAlchemy ORM tables, mixins, columns, model helpers, and persistence primitives for Tigrbl applications."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -48,7 +48,7 @@ dependencies = [
48
48
  Organization = "https://github.com/tigrbl"
49
49
  Discord = "https://discord.gg/K4YTAPapjR"
50
50
  Homepage = "https://github.com/tigrbl/tigrbl"
51
- Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/core/tigrbl_orm"
51
+ Repository = "https://github.com/tigrbl/tigrbl/tree/master/pkgs/30_orm/tigrbl_orm"
52
52
  Issues = "https://github.com/tigrbl/tigrbl/issues"
53
53
 
54
54
  [build-system]
@@ -16,7 +16,15 @@ from .utils import (
16
16
  CRUD_IO,
17
17
  RO_IO,
18
18
  )
19
- from .principals import GUIDPk, TenantColumn, UserColumn, OrgColumn, Principal
19
+ from .principals import (
20
+ GUIDPk,
21
+ UUID7Pk,
22
+ ULIDPk,
23
+ TenantColumn,
24
+ UserColumn,
25
+ OrgColumn,
26
+ Principal,
27
+ )
20
28
  from .bound import OwnerBound, UserBound
21
29
  from .lifecycle import (
22
30
  Created,
@@ -59,6 +67,8 @@ __all__ = [
59
67
  "CRUD_IO",
60
68
  "RO_IO",
61
69
  "GUIDPk",
70
+ "UUID7Pk",
71
+ "ULIDPk",
62
72
  "TenantColumn",
63
73
  "UserColumn",
64
74
  "OrgColumn",
@@ -0,0 +1,214 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from secrets import randbits
5
+ from time import time_ns
6
+
7
+ from pydantic import UUID7
8
+ from sqlalchemy import String, TypeDecorator
9
+
10
+ from tigrbl.specs import ColumnSpec, F, S, acol
11
+ from tigrbl._spec.storage_spec import ForeignKeySpec
12
+ from tigrbl.types import PgUUID, UUID, declarative_mixin, declared_attr, uuid4, Mapped
13
+ from tigrbl_core._spec.datatypes import DataTypeSpec
14
+
15
+ from .utils import _infer_schema, uuid_example, CRUD_IO, RO_IO
16
+
17
+
18
+ _ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
19
+ _ULID_PATTERN = rf"^[0-7][{_ULID_ALPHABET}]{{25}}$"
20
+
21
+
22
+ def _new_ulid() -> str:
23
+ timestamp_ms = time_ns() // 1_000_000
24
+ value = (timestamp_ms << 80) | randbits(80)
25
+ return "".join(
26
+ _ULID_ALPHABET[(value >> shift) & 31] for shift in range(125, -1, -5)
27
+ )
28
+
29
+
30
+ def _new_uuid7() -> UUID:
31
+ timestamp_ms = time_ns() // 1_000_000
32
+ # RFC 9562 §5.7: 48-bit time, version, 12 random bits, variant, 62 random bits.
33
+ return UUID(
34
+ int=(timestamp_ms << 80)
35
+ | (7 << 76)
36
+ | (randbits(12) << 64)
37
+ | (2 << 62)
38
+ | randbits(62)
39
+ )
40
+
41
+
42
+ class _UUID7Type(TypeDecorator[UUID]):
43
+ impl = PgUUID
44
+ cache_ok = True
45
+
46
+ @property
47
+ def python_type(self) -> type[UUID]:
48
+ return UUID
49
+
50
+ def process_bind_param(self, value: UUID | str | None, dialect) -> UUID | None:
51
+ if value is None:
52
+ return None
53
+ if isinstance(value, str):
54
+ value = UUID(value)
55
+ if not isinstance(value, UUID) or value.version != 7:
56
+ raise ValueError("Expected an RFC 9562 UUID version 7")
57
+ return value
58
+
59
+
60
+ class _ULIDString(TypeDecorator[str]):
61
+ impl = String(26)
62
+ cache_ok = True
63
+
64
+ @property
65
+ def python_type(self) -> type[str]:
66
+ return str
67
+
68
+ def process_bind_param(self, value: str | None, dialect) -> str | None:
69
+ if value is not None and (
70
+ not isinstance(value, str) or re.fullmatch(_ULID_PATTERN, value) is None
71
+ ):
72
+ raise ValueError("Expected a canonical 26-character uppercase ULID")
73
+ return value
74
+
75
+
76
+ @declarative_mixin
77
+ class GUIDPk:
78
+ """Universal surrogate primary key."""
79
+
80
+ id: Mapped[UUID] = acol(
81
+ spec=ColumnSpec(
82
+ storage=S(
83
+ type_=PgUUID(as_uuid=True),
84
+ primary_key=True,
85
+ default=uuid4,
86
+ ),
87
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
88
+ io=RO_IO,
89
+ )
90
+ )
91
+
92
+
93
+ @declarative_mixin
94
+ class UUID7Pk:
95
+ """Add a UUIDv7 primary key generated on insert."""
96
+
97
+ id: Mapped[UUID] = acol(
98
+ spec=ColumnSpec(
99
+ storage=S(
100
+ type_=_UUID7Type(),
101
+ primary_key=True,
102
+ nullable=False,
103
+ default=_new_uuid7,
104
+ ),
105
+ field=F(
106
+ py_type=UUID7,
107
+ constraints={"examples": ["017f22e2-79b0-7cc3-98c4-dc0c0c07398f"]},
108
+ ),
109
+ io=RO_IO,
110
+ datatype=DataTypeSpec(logical_name="uuid"),
111
+ )
112
+ )
113
+
114
+
115
+ @declarative_mixin
116
+ class ULIDPk:
117
+ """Add a canonical ULID string primary key, generated on insert."""
118
+
119
+ id: Mapped[str] = acol(
120
+ spec=ColumnSpec(
121
+ storage=S(
122
+ type_=_ULIDString(),
123
+ primary_key=True,
124
+ nullable=False,
125
+ default=_new_ulid,
126
+ ),
127
+ field=F(
128
+ py_type=str,
129
+ constraints={
130
+ "min_length": 26,
131
+ "max_length": 26,
132
+ "pattern": _ULID_PATTERN,
133
+ },
134
+ ),
135
+ io=RO_IO,
136
+ datatype=DataTypeSpec(logical_name="ulid"),
137
+ )
138
+ )
139
+
140
+
141
+ @declarative_mixin
142
+ class TenantColumn:
143
+ """Adds ``tenant_id`` with a schema-qualified FK to ``<schema>.tenants.id``."""
144
+
145
+ @declared_attr
146
+ def tenant_id(cls) -> Mapped[UUID]:
147
+ schema = getattr(cls, "__tenant_table_schema__", None) or _infer_schema(cls)
148
+ spec = ColumnSpec(
149
+ storage=S(
150
+ type_=PgUUID(as_uuid=True),
151
+ fk=ForeignKeySpec(target=f"{schema}.tenants.id"),
152
+ nullable=False,
153
+ index=True,
154
+ ),
155
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
156
+ io=CRUD_IO,
157
+ )
158
+ return acol(spec=spec)
159
+
160
+
161
+ @declarative_mixin
162
+ class UserColumn:
163
+ """Adds ``user_id`` with a schema-qualified FK to ``<schema>.users.id``."""
164
+
165
+ @declared_attr
166
+ def user_id(cls) -> Mapped[UUID]:
167
+ schema = getattr(cls, "__user_table_schema__", None) or _infer_schema(cls)
168
+ spec = ColumnSpec(
169
+ storage=S(
170
+ type_=PgUUID(as_uuid=True),
171
+ fk=ForeignKeySpec(target=f"{schema}.users.id"),
172
+ nullable=False,
173
+ index=True,
174
+ ),
175
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
176
+ io=CRUD_IO,
177
+ )
178
+ return acol(spec=spec)
179
+
180
+
181
+ @declarative_mixin
182
+ class OrgColumn:
183
+ """Adds ``org_id`` with a schema-qualified FK to ``<schema>.orgs.id``."""
184
+
185
+ @declared_attr
186
+ def org_id(cls) -> Mapped[UUID]:
187
+ schema = getattr(cls, "__org_table_schema__", None) or _infer_schema(cls)
188
+ spec = ColumnSpec(
189
+ storage=S(
190
+ type_=PgUUID(as_uuid=True),
191
+ fk=ForeignKeySpec(target=f"{schema}.orgs.id"),
192
+ nullable=False,
193
+ index=True,
194
+ ),
195
+ field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
196
+ io=CRUD_IO,
197
+ )
198
+ return acol(spec=spec)
199
+
200
+
201
+ @declarative_mixin
202
+ class Principal:
203
+ __abstract__ = True
204
+
205
+
206
+ __all__ = [
207
+ "GUIDPk",
208
+ "UUID7Pk",
209
+ "ULIDPk",
210
+ "TenantColumn",
211
+ "UserColumn",
212
+ "OrgColumn",
213
+ "Principal",
214
+ ]
@@ -1,19 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime as dt
4
-
5
3
  from tigrbl.specs import IO
6
- from tigrbl.types import UUID
7
-
8
-
9
- def tzutcnow() -> dt.datetime:
10
- """Return an aware UTC ``datetime``."""
11
- return dt.datetime.now(dt.timezone.utc)
12
-
13
-
14
- def tzutcnow_plus_day() -> dt.datetime:
15
- """Return an aware UTC ``datetime`` one day in the future."""
16
- return tzutcnow() + dt.timedelta(days=1)
4
+ from tigrbl.types import UUID, tzutcnow, tzutcnow_plus_day
17
5
 
18
6
 
19
7
  def _infer_schema(cls, default: str = "public") -> str:
@@ -1,98 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from tigrbl.specs import ColumnSpec, F, S, acol
4
- from tigrbl._spec.storage_spec import ForeignKeySpec
5
- from tigrbl.types import PgUUID, UUID, declarative_mixin, declared_attr, uuid4, Mapped
6
-
7
- from .utils import _infer_schema, uuid_example, CRUD_IO, RO_IO
8
-
9
-
10
- @declarative_mixin
11
- class GUIDPk:
12
- """Universal surrogate primary key."""
13
-
14
- id: Mapped[UUID] = acol(
15
- spec=ColumnSpec(
16
- storage=S(
17
- type_=PgUUID(as_uuid=True),
18
- primary_key=True,
19
- default=uuid4,
20
- ),
21
- field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
22
- io=RO_IO,
23
- )
24
- )
25
-
26
-
27
- @declarative_mixin
28
- class TenantColumn:
29
- """Adds ``tenant_id`` with a schema-qualified FK to ``<schema>.tenants.id``."""
30
-
31
- @declared_attr
32
- def tenant_id(cls) -> Mapped[UUID]:
33
- schema = getattr(cls, "__tenant_table_schema__", None) or _infer_schema(cls)
34
- spec = ColumnSpec(
35
- storage=S(
36
- type_=PgUUID(as_uuid=True),
37
- fk=ForeignKeySpec(target=f"{schema}.tenants.id"),
38
- nullable=False,
39
- index=True,
40
- ),
41
- field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
42
- io=CRUD_IO,
43
- )
44
- return acol(spec=spec)
45
-
46
-
47
- @declarative_mixin
48
- class UserColumn:
49
- """Adds ``user_id`` with a schema-qualified FK to ``<schema>.users.id``."""
50
-
51
- @declared_attr
52
- def user_id(cls) -> Mapped[UUID]:
53
- schema = getattr(cls, "__user_table_schema__", None) or _infer_schema(cls)
54
- spec = ColumnSpec(
55
- storage=S(
56
- type_=PgUUID(as_uuid=True),
57
- fk=ForeignKeySpec(target=f"{schema}.users.id"),
58
- nullable=False,
59
- index=True,
60
- ),
61
- field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
62
- io=CRUD_IO,
63
- )
64
- return acol(spec=spec)
65
-
66
-
67
- @declarative_mixin
68
- class OrgColumn:
69
- """Adds ``org_id`` with a schema-qualified FK to ``<schema>.orgs.id``."""
70
-
71
- @declared_attr
72
- def org_id(cls) -> Mapped[UUID]:
73
- schema = getattr(cls, "__org_table_schema__", None) or _infer_schema(cls)
74
- spec = ColumnSpec(
75
- storage=S(
76
- type_=PgUUID(as_uuid=True),
77
- fk=ForeignKeySpec(target=f"{schema}.orgs.id"),
78
- nullable=False,
79
- index=True,
80
- ),
81
- field=F(py_type=UUID, constraints={"examples": [uuid_example]}),
82
- io=CRUD_IO,
83
- )
84
- return acol(spec=spec)
85
-
86
-
87
- @declarative_mixin
88
- class Principal:
89
- __abstract__ = True
90
-
91
-
92
- __all__ = [
93
- "GUIDPk",
94
- "TenantColumn",
95
- "UserColumn",
96
- "OrgColumn",
97
- "Principal",
98
- ]
File without changes
File without changes