pUUID 1.0.0__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.
puuid-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: pUUID
3
+ Version: 1.0.0
4
+ Summary: Prefixed UUIDs for Python with Pydantic & SQLAlchemy support.
5
+ Author: Jendrik Potyka, Fabian Preiss
6
+ Author-email: Jendrik Potyka <jpotyka@digon.io>, Fabian Preiss <fpreiss@digon.io>
7
+ License-Expression: LGPL-3.0-only
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: pydantic>=2.12.5
18
+ Maintainer: Jendrik A. Potyka, Fabian A. Preiss
19
+ Maintainer-email: Jendrik A. Potyka <jpotyka@digon.io>, Fabian A. Preiss <fpreiss@digon.io>
20
+ Requires-Python: >=3.14
21
+ Description-Content-Type: text/markdown
22
+
23
+ ![Logo: pUUID - Prefixed UUIDs for Python](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/logo_font_path.svg "pUUID Logo")
24
+
25
+ **pUUID** - Prefixed UUIDs for Python with **Pydantic** & **SQLAlchemy** support.
26
+
27
+ [![repository](https://img.shields.io/badge/src-GitLab-orange)](https://gitlab.com/DigonIO/puuid)
28
+ [![mirror](https://img.shields.io/badge/mirror-GitHub-orange)](https://github.com/DigonIO/puuid)
29
+ [![License: LGPLv3](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/license.svg)](https://spdx.org/licenses/LGPL-3.0-only.html)
30
+ [![pipeline status](https://gitlab.com/DigonIO/puuid/badges/main/pipeline.svg)](https://gitlab.com/DigonIO/puuid/-/pipelines)
31
+ [![coverage report](https://gitlab.com/DigonIO/puuid/badges/main/coverage.svg)](https://gitlab.com/DigonIO/puuid/-/pipelines)
32
+ [![Code style: black](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/black.svg)](https://github.com/psf/black)
33
+ [![Imports: isort](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/isort.svg)](https://pycqa.github.io/isort/)
34
+
35
+ [![pkgversion](https://img.shields.io/pypi/v/puuid)](https://pypi.org/project/puuid/)
36
+ [![versionsupport](https://img.shields.io/pypi/pyversions/puuid)](https://pypi.org/project/puuid/)
37
+ [![Downloads Week](https://pepy.tech/badge/puuid/week)](https://pepy.tech/project/puuid)
38
+ [![Downloads Total](https://pepy.tech/badge/puuid)](https://pepy.tech/project/puuid)
39
+
40
+ ---
41
+
42
+ If you find the **pUUID** library beneficial, please consider supporting the project by [starring it on GitHub](https://github.com/DigonIO/pUUID).
43
+
44
+ [![GitHub Repo stars](https://img.shields.io/github/stars/digonio/puuid)](https://github.com/DigonIO/pUUID)
45
+
46
+ # pUUID - Prefixed UUIDs for Python
47
+
48
+ ## Features
49
+
50
+ - **Human-Friendly UUIDs:** `user_019b956e...` instead of just `019b956e...`
51
+ - **All UUID versions from [RFC 9562](https://www.rfc-editor.org/rfc/rfc95629).**
52
+ - **Pydantic support.** [(Read more)](https://puuid.digon.io/quick_start/#pydantic-integration)
53
+ - **SQLAlchemy support.** [(Read more)](https://puuid.digon.io/quick_start/#sqlalchemy-integration)
54
+ - **Strong type guarantees!**
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install pUUID
60
+
61
+ # For SQLAlchemy support:
62
+ pip install 'pUUID[sqlalchemy]'
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ Define a domain-specific ID by inheriting from a versioned base:
68
+
69
+ ```python
70
+ from typing import Literal
71
+ from puuid import PUUIDv7
72
+
73
+ class UserUUID(PUUIDv7[Literal["user"]]):
74
+ _prefix = "user"
75
+
76
+ # Generation
77
+ uid = UserUUID()
78
+ print(uid) # user_019b956e-ed25-70db-9d0a-0f30fb9047c2
79
+
80
+ # Deserialization
81
+ uid = UserUUID.from_string("user_019b956e-ed25-70db-9d0a-0f30fb9047c2")
82
+ ```
83
+
84
+ ## Resources
85
+
86
+ - [Online documentation](https://puuid.digon.io)
87
+ - [API Reference](https://puuid.digon.io/api_ref)
88
+ - [Changelog](https://puuid.digon.io/changelog)
89
+ - [Coverage Report](https://puuid.digon.io/coverage)
90
+ - [How to contribute](https://gitlab.com/DigonIO/puuid/-/blob/main/CONTRIBUTING.md)
91
+
92
+ ## Alternatives
93
+
94
+ If you only need lexicographically sortable IDs and want to build the **SQLAlchemy** support yourself, these two projects might be for you:
95
+
96
+ - [**TypeID**](https://github.com/akhundMurad/typeid-python) - **pUUID** supports all **UUID** versions because it uses Python’s standard `uuid` library for **UUID** generation, while **TypeID** uses a custom generator that comes with performance improvements but only supports **UUIDv7**. **TypeID** does not support **SQLAlchemy** out of the box.
97
+ - [**UPID**](https://github.com/carderne/upid) - **UPID** implements a modified version of the **ULID** standard, which was designed before **UUIDv7** was available. **UPID** does not support **SQLAlchemy** out of the box.
98
+
99
+ ## Sponsor
100
+
101
+ ![Digon.IO GmbH Logo](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/logo_digon.io_gmbh.png "Digon.IO GmbH")
102
+
103
+ Digon.IO provides dev & data end-to-end consulting for SMEs and software companies. [(Website)](https://digon.io) [(Technical Blog)](https://digon.io/en/blog)
104
+
105
+ *The sponsor logo is the property of Digon.IO GmbH. Standard trademark and copyright restrictions apply to any use outside this repository.*
106
+
107
+ ## License
108
+
109
+ - **Library source code:** Licensed under [LGPLv3](https://spdx.org/licenses/LGPL-3.0-only.html).
puuid-1.0.0/README.md ADDED
@@ -0,0 +1,87 @@
1
+ ![Logo: pUUID - Prefixed UUIDs for Python](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/logo_font_path.svg "pUUID Logo")
2
+
3
+ **pUUID** - Prefixed UUIDs for Python with **Pydantic** & **SQLAlchemy** support.
4
+
5
+ [![repository](https://img.shields.io/badge/src-GitLab-orange)](https://gitlab.com/DigonIO/puuid)
6
+ [![mirror](https://img.shields.io/badge/mirror-GitHub-orange)](https://github.com/DigonIO/puuid)
7
+ [![License: LGPLv3](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/license.svg)](https://spdx.org/licenses/LGPL-3.0-only.html)
8
+ [![pipeline status](https://gitlab.com/DigonIO/puuid/badges/main/pipeline.svg)](https://gitlab.com/DigonIO/puuid/-/pipelines)
9
+ [![coverage report](https://gitlab.com/DigonIO/puuid/badges/main/coverage.svg)](https://gitlab.com/DigonIO/puuid/-/pipelines)
10
+ [![Code style: black](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/black.svg)](https://github.com/psf/black)
11
+ [![Imports: isort](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/badges/isort.svg)](https://pycqa.github.io/isort/)
12
+
13
+ [![pkgversion](https://img.shields.io/pypi/v/puuid)](https://pypi.org/project/puuid/)
14
+ [![versionsupport](https://img.shields.io/pypi/pyversions/puuid)](https://pypi.org/project/puuid/)
15
+ [![Downloads Week](https://pepy.tech/badge/puuid/week)](https://pepy.tech/project/puuid)
16
+ [![Downloads Total](https://pepy.tech/badge/puuid)](https://pepy.tech/project/puuid)
17
+
18
+ ---
19
+
20
+ If you find the **pUUID** library beneficial, please consider supporting the project by [starring it on GitHub](https://github.com/DigonIO/pUUID).
21
+
22
+ [![GitHub Repo stars](https://img.shields.io/github/stars/digonio/puuid)](https://github.com/DigonIO/pUUID)
23
+
24
+ # pUUID - Prefixed UUIDs for Python
25
+
26
+ ## Features
27
+
28
+ - **Human-Friendly UUIDs:** `user_019b956e...` instead of just `019b956e...`
29
+ - **All UUID versions from [RFC 9562](https://www.rfc-editor.org/rfc/rfc95629).**
30
+ - **Pydantic support.** [(Read more)](https://puuid.digon.io/quick_start/#pydantic-integration)
31
+ - **SQLAlchemy support.** [(Read more)](https://puuid.digon.io/quick_start/#sqlalchemy-integration)
32
+ - **Strong type guarantees!**
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install pUUID
38
+
39
+ # For SQLAlchemy support:
40
+ pip install 'pUUID[sqlalchemy]'
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ Define a domain-specific ID by inheriting from a versioned base:
46
+
47
+ ```python
48
+ from typing import Literal
49
+ from puuid import PUUIDv7
50
+
51
+ class UserUUID(PUUIDv7[Literal["user"]]):
52
+ _prefix = "user"
53
+
54
+ # Generation
55
+ uid = UserUUID()
56
+ print(uid) # user_019b956e-ed25-70db-9d0a-0f30fb9047c2
57
+
58
+ # Deserialization
59
+ uid = UserUUID.from_string("user_019b956e-ed25-70db-9d0a-0f30fb9047c2")
60
+ ```
61
+
62
+ ## Resources
63
+
64
+ - [Online documentation](https://puuid.digon.io)
65
+ - [API Reference](https://puuid.digon.io/api_ref)
66
+ - [Changelog](https://puuid.digon.io/changelog)
67
+ - [Coverage Report](https://puuid.digon.io/coverage)
68
+ - [How to contribute](https://gitlab.com/DigonIO/puuid/-/blob/main/CONTRIBUTING.md)
69
+
70
+ ## Alternatives
71
+
72
+ If you only need lexicographically sortable IDs and want to build the **SQLAlchemy** support yourself, these two projects might be for you:
73
+
74
+ - [**TypeID**](https://github.com/akhundMurad/typeid-python) - **pUUID** supports all **UUID** versions because it uses Python’s standard `uuid` library for **UUID** generation, while **TypeID** uses a custom generator that comes with performance improvements but only supports **UUIDv7**. **TypeID** does not support **SQLAlchemy** out of the box.
75
+ - [**UPID**](https://github.com/carderne/upid) - **UPID** implements a modified version of the **ULID** standard, which was designed before **UUIDv7** was available. **UPID** does not support **SQLAlchemy** out of the box.
76
+
77
+ ## Sponsor
78
+
79
+ ![Digon.IO GmbH Logo](https://gitlab.com/DigonIO/puuid/-/raw/main/assets/logo_digon.io_gmbh.png "Digon.IO GmbH")
80
+
81
+ Digon.IO provides dev & data end-to-end consulting for SMEs and software companies. [(Website)](https://digon.io) [(Technical Blog)](https://digon.io/en/blog)
82
+
83
+ *The sponsor logo is the property of Digon.IO GmbH. Standard trademark and copyright restrictions apply to any use outside this repository.*
84
+
85
+ ## License
86
+
87
+ - **Library source code:** Licensed under [LGPLv3](https://spdx.org/licenses/LGPL-3.0-only.html).
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "pUUID"
3
+ version = "1.0.0"
4
+ license = "LGPL-3.0-only"
5
+ classifiers = [
6
+ "Development Status :: 4 - Beta",
7
+ "Intended Audience :: Developers",
8
+ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
9
+ "Programming Language :: Python :: 3.14",
10
+ "Programming Language :: Python :: Implementation :: CPython",
11
+ "Topic :: Software Development :: Libraries",
12
+ "Topic :: Utilities",
13
+ "Operating System :: OS Independent",
14
+ "Typing :: Typed",
15
+ ]
16
+ description = "Prefixed UUIDs for Python with Pydantic & SQLAlchemy support."
17
+ readme = "README.md"
18
+ authors = [
19
+ { name = "Jendrik Potyka", email = "jpotyka@digon.io" },
20
+ { name = "Fabian Preiss", email = "fpreiss@digon.io" },
21
+ ]
22
+ maintainers = [
23
+ { name = "Jendrik A. Potyka", email = "jpotyka@digon.io" },
24
+ { name = "Fabian A. Preiss", email = "fpreiss@digon.io" },
25
+ ]
26
+ requires-python = ">=3.14"
27
+ dependencies = ["pydantic>=2.12.5"]
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.9.13,<0.10.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "basedpyright>=1.37.0",
36
+ "black>=25.12.0",
37
+ "coverage>=7.13.1",
38
+ "isort>=7.0.0",
39
+ "marimo>=0.18.4",
40
+ "mkdocs>=1.6.1",
41
+ "mkdocs-coverage>=2.0.0",
42
+ "mkdocs-material[imaging]>=9.7.1",
43
+ "mkdocstrings[python]>=1.0.0",
44
+ "pdbpp>=0.11.7",
45
+ "pytest>=9.0.2",
46
+ "pytest-cov>=7.0.0",
47
+ "pytest-markdown-docs>=0.9.0",
48
+ "ruff>=0.14.10",
49
+ "sqlalchemy[mypy]>=2.0.45",
50
+ ]
51
+ sqlalchemy = ["sqlalchemy>=2.0.45"]
52
+
53
+ [tool.black]
54
+ target-version = ['py313'] # black is still not properly supporting py314
55
+
56
+ [tool.isort]
57
+ py_version = 314
58
+ profile = "black"
59
+
60
+ [tool.mypy]
61
+ python_version = "3.14"
62
+ strict = true
63
+ warn_return_any = true
64
+ warn_unused_configs = true
65
+ disallow_any_generics = true
66
+ disallow_subclassing_any = true
67
+
68
+ [tool.pyright]
69
+ typeCheckingMode = "strict"
70
+
71
+ [tool.coverage.run]
72
+ branch = true
73
+ source = ["src/puuid"]
74
+
75
+ [tool.coverage.report]
76
+ show_missing = true
@@ -0,0 +1,33 @@
1
+ """
2
+ pUUID - Prefixed UUIDs for Python with Pydantic & SQLAlchemy support.
3
+
4
+ Author: Jendrik Potyka, Fabian Preiss
5
+ """
6
+
7
+ __version__ = "1.0.0"
8
+ __author__ = "Jendrik Potyka, Fabian Preiss"
9
+
10
+
11
+ from puuid.base import (
12
+ PUUID,
13
+ PUUIDError,
14
+ PUUIDv1,
15
+ PUUIDv3,
16
+ PUUIDv4,
17
+ PUUIDv5,
18
+ PUUIDv6,
19
+ PUUIDv7,
20
+ PUUIDv8,
21
+ )
22
+
23
+ __all__ = [
24
+ "PUUID",
25
+ "PUUIDv1",
26
+ "PUUIDv3",
27
+ "PUUIDv4",
28
+ "PUUIDv5",
29
+ "PUUIDv6",
30
+ "PUUIDv7",
31
+ "PUUIDv8",
32
+ "PUUIDError",
33
+ ]
@@ -0,0 +1,616 @@
1
+ """
2
+ pUUID Base Implementation.
3
+
4
+ Provides the abstract base class and version-specific implementations for Prefixed UUIDs.
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Self, final, overload, override
9
+ from uuid import UUID, uuid1, uuid3, uuid4, uuid5, uuid6, uuid7, uuid8
10
+
11
+ from pydantic import GetCoreSchemaHandler
12
+ from pydantic_core import core_schema
13
+
14
+
15
+ @final
16
+ class ERR_MSG:
17
+ UUID_VERSION_MISMATCH = "Expected 'UUID' with version '{expected}', got '{actual}'"
18
+ FACTORY_UNSUPPORTED = "'PUUID.factory' is only supported for 'PUUIDv1', 'PUUIDv4', 'PUUIDv6', 'PUUIDv7' and 'PUUIDv8'!"
19
+ PREFIX_DESERIALIZATION_ERROR = "Unable to deserialize prefix '{prefix}', separator '_' or UUID for '{classname}' from '{serial_puuid}'!"
20
+ INVALID_TYPE_FOR_SERIAL_PUUID = "'{classname}' can not be created from invalid type '{type}' with value '{value}'!"
21
+ INVALID_PUUIDv1_ARGS = "Invalid 'PUUIDv1' arguments: Provide either 'node' and 'clock_seq' or a 'uuid'!"
22
+ INVALID_PUUIDv3_ARGS = "Invalid 'PUUIDv3' arguments: Provide either 'namespace' and 'name' or a 'uuid'!"
23
+ INVALID_PUUIDv5_ARGS = "Invalid 'PUUIDv5' arguments: Provide either 'namespace' and 'name' or a 'uuid'!"
24
+ INVALID_PUUIDv6_ARGS = "Invalid 'PUUIDv6' arguments: Provide either 'node' and 'clock_seq' or a 'uuid'!"
25
+ INVALID_PUUIDv8_ARGS = (
26
+ "Invalid 'PUUIDv8' arguments: Provide either 'a', 'b' and 'c' or 'uuid'!"
27
+ )
28
+
29
+
30
+ class PUUIDError(Exception):
31
+ """Base exception for pUUID related errors."""
32
+
33
+ message: str
34
+
35
+ def __init__(self, message: str = "") -> None:
36
+ super().__init__(message)
37
+ self.message = message
38
+
39
+
40
+ ################################################################################
41
+ #### PUUID
42
+ ################################################################################
43
+
44
+
45
+ class PUUID[TPrefix: str](ABC):
46
+ """Abstract Base Class for Prefixed UUIDs."""
47
+
48
+ _prefix: TPrefix
49
+ _serial: str
50
+ _uuid: UUID
51
+
52
+ @abstractmethod
53
+ def __init__(self, *, uuid: UUID) -> None: ...
54
+
55
+ @classmethod
56
+ def prefix(cls) -> TPrefix:
57
+ """
58
+ Return the defined prefix for the class.
59
+
60
+ Returns
61
+ -------
62
+ TPrefix
63
+ The prefix string.
64
+ """
65
+ return cls._prefix
66
+
67
+ @property
68
+ def uuid(self) -> UUID:
69
+ """
70
+ Return the underlying UUID object.
71
+
72
+ Returns
73
+ -------
74
+ UUID
75
+ The native UUID instance.
76
+ """
77
+ return self._uuid
78
+
79
+ def to_string(self) -> str:
80
+ """
81
+ Return the string representation of the Prefixed UUID.
82
+
83
+ Returns
84
+ -------
85
+ str
86
+ The formatted string (e.g., `<prefix>_<uuid-hex-string>`).
87
+ """
88
+ return self._serial
89
+
90
+ @classmethod
91
+ def factory(cls) -> Self:
92
+ """
93
+ Create a new instance using default generation.
94
+
95
+ Supported by version variants that allow generation without arguments.
96
+
97
+ Returns
98
+ -------
99
+ Self
100
+ A new instance of the pUUID class.
101
+
102
+ Raises
103
+ ------
104
+ PUUIDError
105
+ If the variant does not support parameterless generation.
106
+ """
107
+ raise PUUIDError(ERR_MSG.FACTORY_UNSUPPORTED)
108
+
109
+ @classmethod
110
+ def from_string(cls, serial_puuid: str) -> Self:
111
+ """
112
+ Create a pUUID instance from its string representation.
113
+
114
+ Parameters
115
+ ----------
116
+ serial_puuid : str
117
+ The prefixed UUID string (e.g., `user_550e8400-e29b...`).
118
+
119
+ Returns
120
+ -------
121
+ Self
122
+ The deserialized pUUID instance.
123
+
124
+ Raises
125
+ ------
126
+ PUUIDError
127
+ If the string is malformed or the prefix does not match.
128
+ """
129
+ try:
130
+ if "_" not in serial_puuid:
131
+ raise ValueError("Missing separator")
132
+
133
+ prefix, serialized_uuid = serial_puuid.split("_", 1)
134
+
135
+ if prefix != cls._prefix:
136
+ raise ValueError("Prefix mismatch")
137
+
138
+ uuid = UUID(serialized_uuid)
139
+ return cls(uuid=uuid)
140
+
141
+ except ValueError as err:
142
+ raise PUUIDError(
143
+ ERR_MSG.PREFIX_DESERIALIZATION_ERROR.format(
144
+ prefix=cls._prefix,
145
+ classname=cls.__name__,
146
+ serial_puuid=serial_puuid,
147
+ )
148
+ ) from err
149
+
150
+ @override
151
+ def __str__(self) -> str:
152
+ return self._serial
153
+
154
+ @override
155
+ def __eq__(self, other: object) -> bool:
156
+ if isinstance(other, PUUID):
157
+ return self._serial == other._serial
158
+ return False
159
+
160
+ @override
161
+ def __hash__(self) -> int:
162
+ return hash((self._prefix, self._uuid))
163
+
164
+ @classmethod
165
+ def __get_pydantic_core_schema__(
166
+ cls,
167
+ _source_type: object,
168
+ _handler: GetCoreSchemaHandler,
169
+ ) -> core_schema.CoreSchema:
170
+ def validate(value: object) -> PUUID[TPrefix]:
171
+
172
+ if isinstance(value, cls):
173
+ return value
174
+
175
+ if isinstance(value, str):
176
+ try:
177
+ return cls.from_string(value)
178
+ except PUUIDError as err:
179
+ raise ValueError(str(err)) from err
180
+
181
+ raise ValueError(
182
+ ERR_MSG.INVALID_TYPE_FOR_SERIAL_PUUID.format(
183
+ classname=cls.__name__, type=type(value), value=value
184
+ )
185
+ )
186
+
187
+ def serialize(value: PUUID[TPrefix]) -> str:
188
+ return value.to_string()
189
+
190
+ return core_schema.no_info_plain_validator_function(
191
+ validate,
192
+ serialization=core_schema.plain_serializer_function_ser_schema(
193
+ serialize,
194
+ return_schema=core_schema.str_schema(),
195
+ ),
196
+ )
197
+
198
+
199
+ ################################################################################
200
+ #### PUUIDv1
201
+ ################################################################################
202
+
203
+
204
+ class PUUIDv1[TPrefix: str](PUUID[TPrefix]):
205
+ """Prefixed UUID Version 1 (MAC address and time)."""
206
+
207
+ _uuid: UUID
208
+ _serial: str
209
+
210
+ @overload
211
+ def __init__(
212
+ self, *, node: int | None = None, clock_seq: int | None = None
213
+ ) -> None: ...
214
+
215
+ @overload
216
+ def __init__(self, *, uuid: UUID) -> None: ...
217
+
218
+ def __init__(
219
+ self,
220
+ *,
221
+ node: int | None = None,
222
+ clock_seq: int | None = None,
223
+ uuid: UUID | None = None,
224
+ ) -> None:
225
+ """
226
+ Initialize a PUUIDv1.
227
+
228
+ Parameters
229
+ ----------
230
+ node : int | None, optional
231
+ Hardware address. If None, `uuid1` generates a random value.
232
+ clock_seq : int | None, optional
233
+ Clock sequence.
234
+ uuid : UUID | None, optional
235
+ Existing UUID v1 instance.
236
+
237
+ Raises
238
+ ------
239
+ PUUIDError
240
+ If arguments are inconsistent or the UUID version is incorrect.
241
+ """
242
+ match node, clock_seq, uuid:
243
+ case int() | None, int() | None, None:
244
+ self._uuid = uuid1(node, clock_seq)
245
+ case None, None, UUID(version=1):
246
+ self._uuid = uuid
247
+ case None, None, UUID(version=version):
248
+ raise PUUIDError(
249
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=1, actual=version)
250
+ )
251
+ case _:
252
+ raise PUUIDError(ERR_MSG.INVALID_PUUIDv1_ARGS)
253
+
254
+ self._serial = f"{self._prefix}_{self._uuid}"
255
+
256
+ @override
257
+ @classmethod
258
+ def factory(cls) -> Self:
259
+ """
260
+ Create a new PUUIDv1 instance using current time and MAC address.
261
+
262
+ Returns
263
+ -------
264
+ Self
265
+ A new pUUID v1 instance.
266
+ """
267
+ return cls()
268
+
269
+
270
+ ################################################################################
271
+ #### PUUIDv3
272
+ ################################################################################
273
+
274
+
275
+ class PUUIDv3[TPrefix: str](PUUID[TPrefix]):
276
+ """Prefixed UUID Version 3 (MD5 hash of namespace and name)."""
277
+
278
+ _uuid: UUID
279
+ _serial: str
280
+
281
+ @overload
282
+ def __init__(self, *, namespace: UUID, name: str | bytes) -> None: ...
283
+
284
+ @overload
285
+ def __init__(self, *, uuid: UUID) -> None: ...
286
+
287
+ def __init__(
288
+ self,
289
+ *,
290
+ namespace: UUID | None = None,
291
+ name: str | bytes | None = None,
292
+ uuid: UUID | None = None,
293
+ ) -> None:
294
+ """
295
+ Initialize a PUUIDv3.
296
+
297
+ Parameters
298
+ ----------
299
+ namespace : UUID | None, optional
300
+ Namespace UUID.
301
+ name : str | bytes | None, optional
302
+ The name used for hashing.
303
+ uuid : UUID | None, optional
304
+ Existing UUID v3 instance.
305
+
306
+ Raises
307
+ ------
308
+ PUUIDError
309
+ If arguments are inconsistent or the UUID version is incorrect.
310
+ """
311
+ match namespace, name, uuid:
312
+ case UUID(), str() | bytes(), None:
313
+ self._uuid = uuid3(namespace, name)
314
+ case None, None, UUID(version=3):
315
+ self._uuid = uuid
316
+ case None, None, UUID(version=version):
317
+ raise PUUIDError(
318
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=3, actual=version)
319
+ )
320
+ case _:
321
+ raise PUUIDError(ERR_MSG.INVALID_PUUIDv3_ARGS)
322
+
323
+ self._serial = f"{self._prefix}_{self._uuid}"
324
+
325
+
326
+ ################################################################################
327
+ #### PUUIDv4
328
+ ################################################################################
329
+
330
+
331
+ class PUUIDv4[TPrefix: str](PUUID[TPrefix]):
332
+ """Prefixed UUID Version 4 (randomly generated)."""
333
+
334
+ _uuid: UUID
335
+ _serial: str
336
+
337
+ def __init__(self, uuid: UUID | None = None) -> None:
338
+ """
339
+ Initialize a PUUIDv4.
340
+
341
+ Parameters
342
+ ----------
343
+ uuid : UUID | None, optional
344
+ Existing UUID v4 instance. If None, a new random UUID is generated.
345
+
346
+ Raises
347
+ ------
348
+ PUUIDError
349
+ If the provided UUID is not version 4.
350
+ """
351
+ if uuid is not None and uuid.version != 4:
352
+ raise PUUIDError(
353
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=4, actual=uuid.version)
354
+ )
355
+ self._uuid = uuid if uuid else uuid4()
356
+ self._serial = f"{self._prefix}_{self._uuid}"
357
+
358
+ @override
359
+ @classmethod
360
+ def factory(cls) -> Self:
361
+ """
362
+ Create a new PUUIDv4 instance using random generation.
363
+
364
+ Returns
365
+ -------
366
+ Self
367
+ A new pUUID v4 instance.
368
+ """
369
+ return cls()
370
+
371
+
372
+ ################################################################################
373
+ #### PUUIDv5
374
+ ################################################################################
375
+
376
+
377
+ class PUUIDv5[TPrefix: str](PUUID[TPrefix]):
378
+ """Prefixed UUID Version 5 (SHA-1 hash of namespace and name)."""
379
+
380
+ _uuid: UUID
381
+ _serial: str
382
+
383
+ @overload
384
+ def __init__(self, *, namespace: UUID, name: str | bytes) -> None: ...
385
+
386
+ @overload
387
+ def __init__(self, *, uuid: UUID) -> None: ...
388
+
389
+ def __init__(
390
+ self,
391
+ *,
392
+ namespace: UUID | None = None,
393
+ name: str | bytes | None = None,
394
+ uuid: UUID | None = None,
395
+ ) -> None:
396
+ """
397
+ Initialize a PUUIDv5.
398
+
399
+ Parameters
400
+ ----------
401
+ namespace : UUID | None, optional
402
+ Namespace UUID.
403
+ name : str | bytes | None, optional
404
+ The name used for hashing.
405
+ uuid : UUID | None, optional
406
+ Existing UUID v5 instance.
407
+
408
+ Raises
409
+ ------
410
+ PUUIDError
411
+ If arguments are inconsistent or the UUID version is incorrect.
412
+ """
413
+ match namespace, name, uuid:
414
+ case UUID(), str() | bytes(), None:
415
+ self._uuid = uuid5(namespace, name)
416
+ case None, None, UUID(version=5):
417
+ self._uuid = uuid
418
+ case None, None, UUID(version=version):
419
+ raise PUUIDError(
420
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=5, actual=version)
421
+ )
422
+ case _:
423
+ raise PUUIDError(ERR_MSG.INVALID_PUUIDv5_ARGS)
424
+
425
+ self._serial = f"{self._prefix}_{self._uuid}"
426
+
427
+
428
+ ################################################################################
429
+ #### PUUIDv6
430
+ ################################################################################
431
+
432
+
433
+ class PUUIDv6[TPrefix: str](PUUID[TPrefix]):
434
+ """Prefixed UUID Version 6 (reordered v1 for DB locality)."""
435
+
436
+ _uuid: UUID
437
+ _serial: str
438
+
439
+ @overload
440
+ def __init__(
441
+ self, *, node: int | None = None, clock_seq: int | None = None
442
+ ) -> None: ...
443
+
444
+ @overload
445
+ def __init__(self, *, uuid: UUID) -> None: ...
446
+
447
+ def __init__(
448
+ self,
449
+ *,
450
+ node: int | None = None,
451
+ clock_seq: int | None = None,
452
+ uuid: UUID | None = None,
453
+ ) -> None:
454
+ """
455
+ Initialize a PUUIDv6.
456
+
457
+ Parameters
458
+ ----------
459
+ node : int | None, optional
460
+ Hardware address.
461
+ clock_seq : int | None, optional
462
+ Clock sequence.
463
+ uuid : UUID | None, optional
464
+ Existing UUID v6 instance.
465
+
466
+ Raises
467
+ ------
468
+ PUUIDError
469
+ If arguments are inconsistent or the UUID version is incorrect.
470
+ """
471
+ match node, clock_seq, uuid:
472
+ case int() | None, int() | None, None:
473
+ self._uuid = uuid6(node, clock_seq)
474
+ case None, None, UUID(version=6):
475
+ self._uuid = uuid
476
+ case None, None, UUID(version=version):
477
+ raise PUUIDError(
478
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=6, actual=version)
479
+ )
480
+ case _:
481
+ raise PUUIDError(ERR_MSG.INVALID_PUUIDv6_ARGS)
482
+
483
+ self._serial = f"{self._prefix}_{self._uuid}"
484
+
485
+ @override
486
+ @classmethod
487
+ def factory(cls) -> Self:
488
+ """
489
+ Create a new PUUIDv6 instance using reordered time-based generation.
490
+
491
+ Returns
492
+ -------
493
+ Self
494
+ A new pUUID v6 instance optimized for DB locality.
495
+ """
496
+ return cls()
497
+
498
+
499
+ ################################################################################
500
+ #### PUUIDv7
501
+ ################################################################################
502
+
503
+
504
+ class PUUIDv7[TPrefix: str](PUUID[TPrefix]):
505
+ """Prefixed UUID Version 7 (time-ordered)."""
506
+
507
+ _uuid: UUID
508
+ _serial: str
509
+
510
+ def __init__(self, uuid: UUID | None = None) -> None:
511
+ """
512
+ Initialize a PUUIDv7.
513
+
514
+ Parameters
515
+ ----------
516
+ uuid : UUID | None, optional
517
+ Existing UUID v7 instance. If None, a new time-ordered UUID is generated.
518
+
519
+ Raises
520
+ ------
521
+ PUUIDError
522
+ If the provided UUID is not version 7.
523
+ """
524
+ if uuid is not None and uuid.version != 7:
525
+ raise PUUIDError(
526
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=7, actual=uuid.version)
527
+ )
528
+ self._uuid = uuid if uuid else uuid7()
529
+ self._serial = f"{self._prefix}_{self._uuid}"
530
+
531
+ @override
532
+ @classmethod
533
+ def factory(cls) -> Self:
534
+ """
535
+ Create a new PUUIDv7 instance using time-ordered generation.
536
+
537
+ Returns
538
+ -------
539
+ Self
540
+ A new pUUID v7 instance.
541
+ """
542
+ return cls()
543
+
544
+
545
+ ################################################################################
546
+ #### PUUIDv8
547
+ ################################################################################
548
+
549
+
550
+ class PUUIDv8[TPrefix: str](PUUID[TPrefix]):
551
+ """Prefixed UUID Version 8 (custom implementation)."""
552
+
553
+ _uuid: UUID
554
+ _serial: str
555
+
556
+ @overload
557
+ def __init__(
558
+ self, *, a: int | None = None, b: int | None = None, c: int | None = None
559
+ ) -> None: ...
560
+
561
+ @overload
562
+ def __init__(self, *, uuid: UUID) -> None: ...
563
+
564
+ def __init__(
565
+ self,
566
+ *,
567
+ a: int | None = None,
568
+ b: int | None = None,
569
+ c: int | None = None,
570
+ uuid: UUID | None = None,
571
+ ) -> None:
572
+ """
573
+ Initialize a PUUIDv8.
574
+
575
+ Parameters
576
+ ----------
577
+ a : int | None, optional
578
+ First custom 48-bit value.
579
+ b : int | None, optional
580
+ Second custom 12-bit value.
581
+ c : int | None, optional
582
+ Third custom 62-bit value.
583
+ uuid : UUID | None, optional
584
+ Existing UUID v8 instance.
585
+
586
+ Raises
587
+ ------
588
+ PUUIDError
589
+ If arguments are inconsistent or the UUID version is incorrect.
590
+ """
591
+ match a, b, c, uuid:
592
+ case int() | None, int() | None, int() | None, None:
593
+ self._uuid = uuid8(a, b, c)
594
+ case None, None, None, UUID(version=8):
595
+ self._uuid = uuid
596
+ case None, None, None, UUID(version=version):
597
+ raise PUUIDError(
598
+ ERR_MSG.UUID_VERSION_MISMATCH.format(expected=8, actual=version)
599
+ )
600
+ case _:
601
+ raise PUUIDError(ERR_MSG.INVALID_PUUIDv8_ARGS)
602
+
603
+ self._serial = f"{self._prefix}_{self._uuid}"
604
+
605
+ @override
606
+ @classmethod
607
+ def factory(cls) -> Self:
608
+ """
609
+ Create a new PUUIDv8 instance using custom generation.
610
+
611
+ Returns
612
+ -------
613
+ Self
614
+ A new pUUID v8 instance.
615
+ """
616
+ return cls()
File without changes
@@ -0,0 +1,55 @@
1
+ from typing import final, override
2
+
3
+ from sqlalchemy.engine.interfaces import Dialect
4
+ from sqlalchemy.types import String, TypeDecorator
5
+
6
+ from puuid.base import PUUID
7
+
8
+ _SEPARATOR_LENGTH = 1
9
+ _UUID_LENGTH = 36
10
+
11
+
12
+ @final
13
+ class SqlPUUID(TypeDecorator[PUUID[str]]):
14
+ """
15
+ SQLAlchemy type for storing Prefixed UUIDs.
16
+
17
+ Maps a `PUUID` instance to a `VARCHAR` column in the database and
18
+ reconstructs the specific `PUUID` subclass on retrieval.
19
+ """
20
+
21
+ impl = String
22
+ cache_ok = True
23
+
24
+ puuid_cls: type[PUUID[str]]
25
+
26
+ def __init__(self, puuid_cls: type[PUUID[str]], prefix_length: int = 4) -> None:
27
+ """
28
+ Initialize the SqlPUUID type.
29
+
30
+ Parameters
31
+ ----------
32
+ puuid_cls : type[PUUID[str]]
33
+ The pUUID class (e.g., `UserUUID`) to associate with this column.
34
+ prefix_length : int, default 4
35
+ The length of the prefix string to calculate the column width.
36
+ """
37
+ self.puuid_cls = puuid_cls
38
+ varchar_length = prefix_length + _SEPARATOR_LENGTH + _UUID_LENGTH
39
+ super().__init__(length=varchar_length)
40
+
41
+ @override
42
+ def process_bind_param(
43
+ self, value: PUUID[str] | None, dialect: Dialect
44
+ ) -> str | None:
45
+ if value is None:
46
+ return None
47
+ return value.to_string()
48
+
49
+ @override
50
+ def process_result_value(
51
+ self, value: str | None, dialect: Dialect
52
+ ) -> PUUID[str] | None:
53
+ if value is None:
54
+ return None
55
+ return self.puuid_cls.from_string(value)