python-ulid 3.1.0__py3-none-any.whl → 3.2.0__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.
- {python_ulid-3.1.0.dist-info → python_ulid-3.2.0.dist-info}/METADATA +25 -3
- python_ulid-3.2.0.dist-info/RECORD +10 -0
- {python_ulid-3.1.0.dist-info → python_ulid-3.2.0.dist-info}/WHEEL +1 -1
- ulid/__init__.py +93 -9
- ulid/__main__.py +3 -0
- python_ulid-3.1.0.dist-info/RECORD +0 -10
- {python_ulid-3.1.0.dist-info → python_ulid-3.2.0.dist-info}/entry_points.txt +0 -0
- {python_ulid-3.1.0.dist-info → python_ulid-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ulid
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: Universally unique lexicographically sortable identifier
|
|
5
5
|
Project-URL: Homepage, https://github.com/mdomke/python-ulid
|
|
6
6
|
Project-URL: Documentation, https://python-ulid.readthedocs.io
|
|
@@ -18,13 +18,14 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
18
18
|
Classifier: Operating System :: MacOS :: MacOS X
|
|
19
19
|
Classifier: Operating System :: POSIX :: Linux
|
|
20
20
|
Classifier: Programming Language :: Python
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
26
|
Classifier: Topic :: Software Development :: Libraries
|
|
27
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
28
29
|
Provides-Extra: pydantic
|
|
29
30
|
Requires-Dist: pydantic>=2.0; extra == 'pydantic'
|
|
30
31
|
Description-Content-Type: text/x-rst
|
|
@@ -40,6 +41,7 @@ A ``ULID`` is a *universally unique lexicographically sortable identifier*. It i
|
|
|
40
41
|
* Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
|
41
42
|
* Case insensitive
|
|
42
43
|
* No special characters (URL safe)
|
|
44
|
+
* Monotonic sort order (correctly detects and handles the same millisecond)
|
|
43
45
|
|
|
44
46
|
In general the structure of a ULID is as follows:
|
|
45
47
|
|
|
@@ -148,6 +150,20 @@ The ``ULID`` class can be directly used for the popular data validation library
|
|
|
148
150
|
|
|
149
151
|
.. pydantic-end
|
|
150
152
|
|
|
153
|
+
.. monotonic-begin
|
|
154
|
+
|
|
155
|
+
Monotonic Support
|
|
156
|
+
-----------------
|
|
157
|
+
|
|
158
|
+
This library by default supports the implementation for monotonic sort order suggested by the
|
|
159
|
+
official ULID specification.
|
|
160
|
+
|
|
161
|
+
This means that ULID values generated in the same millisecond will have linear increasing randomness
|
|
162
|
+
values. If :math:`r_1` and :math:`r_2` are the randomness values of two ULIDs with the same
|
|
163
|
+
timestamp, then :math:`r_2 = r_1 + 1`.
|
|
164
|
+
|
|
165
|
+
.. monotonic-end
|
|
166
|
+
|
|
151
167
|
.. cli-begin
|
|
152
168
|
|
|
153
169
|
Command line interface
|
|
@@ -205,3 +221,9 @@ Other implementations
|
|
|
205
221
|
* `ulid/javascript <https://github.com/ulid/javascript>`_
|
|
206
222
|
* `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
|
|
207
223
|
* `imdario/go-ulid <https://github.com/imdario/go-ulid>`_
|
|
224
|
+
|
|
225
|
+
Contributions
|
|
226
|
+
-------------
|
|
227
|
+
|
|
228
|
+
Contributions are welcome! Feel free to create pull-requests for issues or feature requests.
|
|
229
|
+
It might be worth creating an issue upfront to discuss the matter.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ulid/__init__.py,sha256=MokfVXGP3iguVkHuzaJvLeFytWAqe_jAqrQqzWYDbRs,16352
|
|
2
|
+
ulid/__main__.py,sha256=ZP8ZbhQ_o-O4Ckbba87bZ4NkwzrSFhAkrRLmUnzkW0Y,5586
|
|
3
|
+
ulid/base32.py,sha256=iUE5BGPW5Dul-Eu3ITvcymk8acNkIFAVTlrL12YIPKI,5961
|
|
4
|
+
ulid/constants.py,sha256=lbCLRNQXq8qnOdz_w-nJf_j5dC2X7Gjv-8GJUbs5jX8,503
|
|
5
|
+
ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
python_ulid-3.2.0.dist-info/METADATA,sha256=4wbtd2VPXZ2fiKQx7lZexKFypftpTlj3qfoWbtN5bUw,6591
|
|
7
|
+
python_ulid-3.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
8
|
+
python_ulid-3.2.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
|
|
9
|
+
python_ulid-3.2.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
|
|
10
|
+
python_ulid-3.2.0.dist-info/RECORD,,
|
ulid/__init__.py
CHANGED
|
@@ -13,19 +13,23 @@ from typing import Generic
|
|
|
13
13
|
from typing import TYPE_CHECKING
|
|
14
14
|
from typing import TypeVar
|
|
15
15
|
|
|
16
|
-
from typing_extensions import Self
|
|
17
|
-
|
|
18
16
|
from ulid import base32
|
|
19
17
|
from ulid import constants
|
|
20
18
|
|
|
21
19
|
|
|
22
20
|
if TYPE_CHECKING: # pragma: no cover
|
|
21
|
+
import sys
|
|
23
22
|
from collections.abc import Callable
|
|
24
23
|
|
|
25
24
|
from pydantic import GetCoreSchemaHandler
|
|
26
25
|
from pydantic import ValidatorFunctionWrapHandler
|
|
27
26
|
from pydantic_core import CoreSchema
|
|
28
27
|
|
|
28
|
+
if sys.version_info >= (3, 11):
|
|
29
|
+
from typing import Self
|
|
30
|
+
else:
|
|
31
|
+
from typing_extensions import Self
|
|
32
|
+
|
|
29
33
|
try:
|
|
30
34
|
from importlib.metadata import version
|
|
31
35
|
except ImportError: # pragma: no cover
|
|
@@ -34,12 +38,12 @@ except ImportError: # pragma: no cover
|
|
|
34
38
|
|
|
35
39
|
__version__ = version("python-ulid")
|
|
36
40
|
|
|
37
|
-
T = TypeVar("T"
|
|
41
|
+
T = TypeVar("T")
|
|
38
42
|
R = TypeVar("R")
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class validate_type(Generic[T]): # noqa: N801
|
|
42
|
-
def __init__(self, *types: T) -> None:
|
|
46
|
+
def __init__(self, *types: type[T]) -> None:
|
|
43
47
|
self.types = types
|
|
44
48
|
|
|
45
49
|
def __call__(self, func: Callable[..., R]) -> Callable[..., R]:
|
|
@@ -69,9 +73,10 @@ class ValueProvider:
|
|
|
69
73
|
raise ValueError("Value exceeds maximum possible timestamp")
|
|
70
74
|
return value
|
|
71
75
|
|
|
72
|
-
def randomness(self) -> bytes:
|
|
76
|
+
def randomness(self, current_timestamp: int | None = None) -> bytes:
|
|
73
77
|
with self.lock:
|
|
74
|
-
current_timestamp
|
|
78
|
+
if current_timestamp is None:
|
|
79
|
+
current_timestamp = self.timestamp()
|
|
75
80
|
if current_timestamp == self.prev_timestamp:
|
|
76
81
|
if self.prev_randomness == constants.MAX_RANDOMNESS:
|
|
77
82
|
raise ValueError("Randomness within same millisecond exhausted")
|
|
@@ -148,8 +153,9 @@ class ULID:
|
|
|
148
153
|
>>> ULID.from_timestamp(time.time())
|
|
149
154
|
ULID(01E75QWN5HKQ0JAVX9FG1K4YP4)
|
|
150
155
|
"""
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
timestamp_value = cls.provider.timestamp(value)
|
|
157
|
+
timestamp = int.to_bytes(timestamp_value, constants.TIMESTAMP_LEN, "big")
|
|
158
|
+
randomness = cls.provider.randomness(timestamp_value)
|
|
153
159
|
return cls.from_bytes(timestamp + randomness)
|
|
154
160
|
|
|
155
161
|
@classmethod
|
|
@@ -199,7 +205,7 @@ class ULID:
|
|
|
199
205
|
a value when they're unsure what format/primitive type it will be given in.
|
|
200
206
|
"""
|
|
201
207
|
if isinstance(value, ULID):
|
|
202
|
-
return cast(Self, value)
|
|
208
|
+
return cast("Self", value)
|
|
203
209
|
if isinstance(value, uuid.UUID):
|
|
204
210
|
return cls.from_uuid(value)
|
|
205
211
|
if isinstance(value, str):
|
|
@@ -281,6 +287,84 @@ class ULID:
|
|
|
281
287
|
"""
|
|
282
288
|
return uuid.UUID(bytes=self.bytes, version=4)
|
|
283
289
|
|
|
290
|
+
def to_uuid7(self, *, compliant: bool = False) -> uuid.UUID:
|
|
291
|
+
"""Convert the :class:`ULID` to a UUIDv7 (:class:`uuid.UUID` version 7).
|
|
292
|
+
|
|
293
|
+
UUIDv7 encodes a Unix timestamp in milliseconds in the first 48 bits (just like ULID).
|
|
294
|
+
The timestamp is always transparently preserved regardless of compliant mode.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
compliant: If True, sets RFC 4122 version (0x7) and variant (0b10) bits,
|
|
298
|
+
losing 6 bits of randomness. If False (default), preserves all 80 bits
|
|
299
|
+
of randomness by clobbering version/variant bits, enabling perfect
|
|
300
|
+
round-trip conversion. Most tools (PostgreSQL, standard libraries)
|
|
301
|
+
accept non-compliant UUIDv7s.
|
|
302
|
+
|
|
303
|
+
Examples:
|
|
304
|
+
|
|
305
|
+
>>> ulid = ULID()
|
|
306
|
+
>>> uuid7 = ulid.to_uuidv7() # Perfect round-trip
|
|
307
|
+
>>> assert ULID.from_uuidv7(uuid7) == ulid
|
|
308
|
+
>>> uuid7_compliant = ulid.to_uuidv7(compliant=True) # RFC 4122 compliant
|
|
309
|
+
>>> uuid7_compliant.version
|
|
310
|
+
7
|
|
311
|
+
"""
|
|
312
|
+
# ULID: [48 bits timestamp_ms][80 bits randomness]
|
|
313
|
+
# UUIDv7: [48 bits timestamp_ms][4 bits ver][12 bits rand_a][2 bits var][62 bits rand_b]
|
|
314
|
+
|
|
315
|
+
timestamp_ms = self.milliseconds
|
|
316
|
+
|
|
317
|
+
# Get the 80 bits of randomness from ULID
|
|
318
|
+
randomness_bits = int.from_bytes(self.bytes[6:], byteorder="big")
|
|
319
|
+
|
|
320
|
+
if compliant:
|
|
321
|
+
# RFC 4122 compliant: set version and variant bits, losing 6 bits of randomness
|
|
322
|
+
# Extract 74 bits of randomness (losing 6 bits for version/variant)
|
|
323
|
+
rand_a = (randomness_bits >> 68) & 0xFFF # Top 12 bits
|
|
324
|
+
rand_b = randomness_bits & ((1 << 62) - 1) # Bottom 62 bits
|
|
325
|
+
|
|
326
|
+
# Build UUIDv7: [48-bit timestamp_ms][4-bit version][12-bit rand_a][2-bit variant][62-bit rand_b]
|
|
327
|
+
uuid_int = (timestamp_ms << 80) | (0x7 << 76) | (rand_a << 64) | (0x2 << 62) | rand_b
|
|
328
|
+
else:
|
|
329
|
+
# Non-compliant: preserve all 80 bits of randomness for perfect round-trip
|
|
330
|
+
# Build UUIDv7: [48-bit timestamp_ms][80-bit randomness] (clobbers version/variant)
|
|
331
|
+
uuid_int = (timestamp_ms << 80) | randomness_bits
|
|
332
|
+
|
|
333
|
+
uuid_bytes = uuid_int.to_bytes(16, byteorder="big")
|
|
334
|
+
return uuid.UUID(bytes=uuid_bytes)
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
@validate_type(uuid.UUID)
|
|
338
|
+
def from_uuidv7(cls, value: uuid.UUID) -> Self:
|
|
339
|
+
"""Create a new :class:`ULID` from a UUIDv7 (:class:`uuid.UUID` version 7).
|
|
340
|
+
|
|
341
|
+
Extracts the timestamp from the UUIDv7's first 48 bits (milliseconds since epoch)
|
|
342
|
+
and the remaining 80 bits as randomness. The timestamp is always transparently
|
|
343
|
+
preserved, providing perfect round-trip conversion with :meth:`to_uuidv7`.
|
|
344
|
+
|
|
345
|
+
Examples:
|
|
346
|
+
|
|
347
|
+
>>> uuid7 = uuid.UUID("01936c5e-f4c0-7000-8000-000000000000")
|
|
348
|
+
>>> ulid = ULID.from_uuidv7(uuid7)
|
|
349
|
+
>>> ulid.datetime
|
|
350
|
+
datetime.datetime(2025, 11, 10, ...)
|
|
351
|
+
"""
|
|
352
|
+
uuid_int = int.from_bytes(value.bytes, byteorder="big")
|
|
353
|
+
|
|
354
|
+
# Extract timestamp from UUIDv7 layout (always in first 48 bits)
|
|
355
|
+
# Bits 0-47: timestamp_ms (48 bits)
|
|
356
|
+
timestamp_ms = uuid_int >> 80
|
|
357
|
+
|
|
358
|
+
# Extract all 80 bits after the timestamp (bits 48-127) as randomness
|
|
359
|
+
# This includes version/variant bits if present, enabling perfect round-trip
|
|
360
|
+
randomness_bits = uuid_int & ((1 << 80) - 1)
|
|
361
|
+
|
|
362
|
+
# Build ULID bytes: [48-bit timestamp][80-bit randomness]
|
|
363
|
+
timestamp_bytes = timestamp_ms.to_bytes(6, byteorder="big")
|
|
364
|
+
randomness_bytes = randomness_bits.to_bytes(10, byteorder="big")
|
|
365
|
+
|
|
366
|
+
return cls.from_bytes(timestamp_bytes + randomness_bytes)
|
|
367
|
+
|
|
284
368
|
def __repr__(self) -> str:
|
|
285
369
|
return f"ULID({self!s})"
|
|
286
370
|
|
ulid/__main__.py
CHANGED
|
@@ -89,6 +89,7 @@ def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
|
89
89
|
s.add_argument("ulid", help="the ULID to inspect. The special value - reads from stdin")
|
|
90
90
|
s.add_argument("--uuid", action="store_true", help="convert to fully random UUID")
|
|
91
91
|
s.add_argument("--uuid4", action="store_true", help="convert to RFC 4122 compliant UUIDv4")
|
|
92
|
+
s.add_argument("--uuid7", action="store_true", help="convert to RFC 4122 compliant UUIDv7")
|
|
92
93
|
s.add_argument("--hex", action="store_true", help="convert to hex")
|
|
93
94
|
s.add_argument("--int", action="store_true", help="convert to int")
|
|
94
95
|
s.add_argument("--timestamp", "--ts", action="store_true", help="show timestamp")
|
|
@@ -141,6 +142,8 @@ def show(args: argparse.Namespace) -> str:
|
|
|
141
142
|
return str(ulid.to_uuid())
|
|
142
143
|
if args.uuid4:
|
|
143
144
|
return str(ulid.to_uuid4())
|
|
145
|
+
if args.uuid7:
|
|
146
|
+
return str(ulid.to_uuid7(compliant=True))
|
|
144
147
|
if args.hex:
|
|
145
148
|
return ulid.hex
|
|
146
149
|
if args.int:
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ulid/__init__.py,sha256=g9Fb4nyq2mt3wGtLdmv3h9HutFJXLsrv_3HiVM-Qph8,12419
|
|
2
|
-
ulid/__main__.py,sha256=TUo3dkWkJLfzRJyd55nfqGqvv1NcG7fBpsZHIi_p4rw,5421
|
|
3
|
-
ulid/base32.py,sha256=iUE5BGPW5Dul-Eu3ITvcymk8acNkIFAVTlrL12YIPKI,5961
|
|
4
|
-
ulid/constants.py,sha256=lbCLRNQXq8qnOdz_w-nJf_j5dC2X7Gjv-8GJUbs5jX8,503
|
|
5
|
-
ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
python_ulid-3.1.0.dist-info/METADATA,sha256=TM_dpKEq830WTj6mahb6qF_5wiVnL6-Rau-6REqSoSQ,5833
|
|
7
|
-
python_ulid-3.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
python_ulid-3.1.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
|
|
9
|
-
python_ulid-3.1.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
|
|
10
|
-
python_ulid-3.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|