tigrbl-orm 0.4.5.dev4__py3-none-any.whl → 0.4.5.dev6__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.
@@ -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",
@@ -1,12 +1,78 @@
1
1
  from __future__ import annotations
2
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
+
3
10
  from tigrbl.specs import ColumnSpec, F, S, acol
4
11
  from tigrbl._spec.storage_spec import ForeignKeySpec
5
12
  from tigrbl.types import PgUUID, UUID, declarative_mixin, declared_attr, uuid4, Mapped
13
+ from tigrbl_core._spec.datatypes import DataTypeSpec
6
14
 
7
15
  from .utils import _infer_schema, uuid_example, CRUD_IO, RO_IO
8
16
 
9
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
+
10
76
  @declarative_mixin
11
77
  class GUIDPk:
12
78
  """Universal surrogate primary key."""
@@ -24,6 +90,54 @@ class GUIDPk:
24
90
  )
25
91
 
26
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
+
27
141
  @declarative_mixin
28
142
  class TenantColumn:
29
143
  """Adds ``tenant_id`` with a schema-qualified FK to ``<schema>.tenants.id``."""
@@ -91,6 +205,8 @@ class Principal:
91
205
 
92
206
  __all__ = [
93
207
  "GUIDPk",
208
+ "UUID7Pk",
209
+ "ULIDPk",
94
210
  "TenantColumn",
95
211
  "UserColumn",
96
212
  "OrgColumn",
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tigrbl-orm
3
- Version: 0.4.5.dev4
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
@@ -1,6 +1,6 @@
1
1
  tigrbl_orm/orm/__init__.py,sha256=oQVk8swfAop9oNNC-WVf0_OdAmXRf7EjqIQfW7w_fLA,61
2
2
  tigrbl_orm/orm/mixins/_RowBound.py,sha256=ORIMeEzD5zPRuI3QY010cMlvkDUIyiEIkBcqpaa8aKU,3058
3
- tigrbl_orm/orm/mixins/__init__.py,sha256=X1qWzDcr_wSNNOo7JJ_hpHGR_tO1HsqDF7vHh6N-xNs,1914
3
+ tigrbl_orm/orm/mixins/__init__.py,sha256=wmSv9phPIegc337t_v4bMFCv6b9uY8VYH6RIPhnAh1Q,1993
4
4
  tigrbl_orm/orm/mixins/bootstrappable.py,sha256=4lWg5n1j4TV7u-J_aMQj3ZIFqILMS2eU-DpcgkFjOJI,3881
5
5
  tigrbl_orm/orm/mixins/bound.py,sha256=VcNM8r4Gr1MpK1SMdHnWKEoKpBX--HBZOv4wSIrhnDM,1348
6
6
  tigrbl_orm/orm/mixins/edges.py,sha256=7D0hK5slbvs6r4kLB5ZvBqWjDTBcDS7yYnM9gj4KBlc,862
@@ -12,10 +12,10 @@ tigrbl_orm/orm/mixins/locks.py,sha256=Z4k5WuE-wiyV6CxPuczfOT84vqgZzYc0vieUUX7Bxs
12
12
  tigrbl_orm/orm/mixins/markers.py,sha256=EVeAPXw6h6m9HtMrtrkCz_uxug5cdP7za_AFSmKOWnc,214
13
13
  tigrbl_orm/orm/mixins/operations.py,sha256=8pYhuiLtkrb81WREzp6VzXYq7KwEP4FXrtBKt4zNJtA,1843
14
14
  tigrbl_orm/orm/mixins/ownable.py,sha256=4enszRMvgpWwzEJSPVhDpQIfQ06kXY6CJG2mgQ9Iwfg,11328
15
- tigrbl_orm/orm/mixins/principals.py,sha256=JdX6s0HtVLoN87XBln4ML2qLjKc9T2a-637A4U6jrvk,2758
15
+ tigrbl_orm/orm/mixins/principals.py,sha256=FGsVJHtZuCZFaHEcIJ9D0fLvKVhH0l0dLrptCF-Yh8A,5819
16
16
  tigrbl_orm/orm/mixins/tenant_bound.py,sha256=2i0Da-wD07-ELcaXl68pv-e4JLV-CvHik_tnrDbF-uk,10119
17
17
  tigrbl_orm/orm/mixins/upsertable.py,sha256=_fj006p2d5Qz6-8T88Yq3SQJiRYFSC0W_m0XN9FmMjc,4010
18
- tigrbl_orm/orm/mixins/utils.py,sha256=B8ZvAh55ng8B6fz72XyC3oBbBDkynyt8jnWyXzchiw4,1265
18
+ tigrbl_orm/orm/mixins/utils.py,sha256=c3eaICdGYIKdFCc2Qjxu-7jb8NThiOg2036EDVgKguI,997
19
19
  tigrbl_orm/orm/tables/__init__.py,sha256=OtKDm6V5je8DJ6M-9Xq_rZUSBm0l5fpFo9JKO6a1Boo,1972
20
20
  tigrbl_orm/orm/tables/_base.py,sha256=tYNTG35sQ-4HQBJXfCm98ZSNL9BDTU-Cu8iYvSau5wM,163
21
21
  tigrbl_orm/orm/tables/audit.py,sha256=5hVeFUQQ7pw0NzZcf5Mz1yzZKNdAf5xYVfr0sq1CAQo,1434
@@ -26,8 +26,8 @@ tigrbl_orm/orm/tables/rbac.py,sha256=zv8XBC2vrQLqxjN3ilA7TyCNJnnBp6jlQs1yqylw0qs
26
26
  tigrbl_orm/orm/tables/status.py,sha256=eYY_13BJScrZN2VK6s_QG0Jlk0vrysKbINVR28GfklE,3596
27
27
  tigrbl_orm/orm/tables/tenant.py,sha256=yTDrGktHqp7EE3dtE959SAqKOY6guNUbt-H__FQvmIM,460
28
28
  tigrbl_orm/orm/tables/user.py,sha256=ROdfbnF4Nta96pY__sir-2CpSoGJp_YappmyDYJ_Nso,807
29
- tigrbl_orm-0.4.5.dev4.dist-info/METADATA,sha256=BuV4tDvK7qAbtjRu-JeFV0zVBq7OpTxmJmVjpjpH5xg,23959
30
- tigrbl_orm-0.4.5.dev4.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
31
- tigrbl_orm-0.4.5.dev4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
- tigrbl_orm-0.4.5.dev4.dist-info/licenses/NOTICE,sha256=EvJMTshzsWz43LiK-DeN2ZuLtrP49cxvlrFlJ8F_buc,221
33
- tigrbl_orm-0.4.5.dev4.dist-info/RECORD,,
29
+ tigrbl_orm-0.4.5.dev6.dist-info/METADATA,sha256=3xn9i8y6lYzdsahpFawlCT-YO4wx09fkCB_Zp5YAoH8,23881
30
+ tigrbl_orm-0.4.5.dev6.dist-info/WHEEL,sha256=L-WvLdSBvJWHXv5zrQxDvNRnIgoBzLUpcbuiw19KCuQ,88
31
+ tigrbl_orm-0.4.5.dev6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
32
+ tigrbl_orm-0.4.5.dev6.dist-info/licenses/NOTICE,sha256=EvJMTshzsWz43LiK-DeN2ZuLtrP49cxvlrFlJ8F_buc,221
33
+ tigrbl_orm-0.4.5.dev6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.4.1
2
+ Generator: poetry-core 2.5.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any