gh-89083: add support for UUID version 6 (RFC 9562) (#120650)

Add support for generating UUIDv6 objects according to RFC 9562, §5.6 [1].

The functionality is provided by the `uuid.uuid6()` function which takes as inputs an optional 48-bit
hardware address and an optional 14-bit clock sequence. The UUIDv6 temporal fields are ordered
differently than those of UUIDv1, thereby providing improved database locality.

[1]: https://www.rfc-editor.org/rfc/rfc9562.html#section-5.6

---------

Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Victor Stinner <vstinner@python.org>
This commit is contained in:
Bénédikt Tran 2025-03-02 12:41:56 +01:00 committed by GitHub
parent 214562ed4d
commit 990ad272f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 235 additions and 17 deletions

View file

@ -1,8 +1,8 @@
r"""UUID objects (universally unique identifiers) according to RFC 4122/9562.
This module provides immutable UUID objects (class UUID) and the functions
uuid1(), uuid3(), uuid4(), uuid5(), and uuid8() for generating version 1, 3,
4, 5, and 8 UUIDs as specified in RFC 4122/9562.
uuid1(), uuid3(), uuid4(), uuid5(), uuid6(), and uuid8() for generating
version 1, 3, 4, 5, 6, and 8 UUIDs as specified in RFC 4122/9562.
If all you want is a unique ID, you should probably call uuid1() or uuid4().
Note that uuid1() may compromise privacy since it creates a UUID containing
@ -101,6 +101,7 @@ _RFC_4122_VERSION_1_FLAGS = ((1 << 76) | (0x8000 << 48))
_RFC_4122_VERSION_3_FLAGS = ((3 << 76) | (0x8000 << 48))
_RFC_4122_VERSION_4_FLAGS = ((4 << 76) | (0x8000 << 48))
_RFC_4122_VERSION_5_FLAGS = ((5 << 76) | (0x8000 << 48))
_RFC_4122_VERSION_6_FLAGS = ((6 << 76) | (0x8000 << 48))
_RFC_4122_VERSION_8_FLAGS = ((8 << 76) | (0x8000 << 48))
@ -127,7 +128,9 @@ class UUID:
fields a tuple of the six integer fields of the UUID,
which are also available as six individual attributes
and two derived attributes:
and two derived attributes. The time_* attributes are
only relevant to version 1, while the others are only
relevant to versions 1 and 6:
time_low the first 32 bits of the UUID
time_mid the next 16 bits of the UUID
@ -353,8 +356,19 @@ class UUID:
@property
def time(self):
return (((self.time_hi_version & 0x0fff) << 48) |
(self.time_mid << 32) | self.time_low)
if self.version == 6:
# time_hi (32) | time_mid (16) | ver (4) | time_lo (12) | ... (64)
time_hi = self.int >> 96
time_lo = (self.int >> 64) & 0x0fff
return time_hi << 28 | (self.time_mid << 12) | time_lo
else:
# time_lo (32) | time_mid (16) | ver (4) | time_hi (12) | ... (64)
#
# For compatibility purposes, we do not warn or raise when the
# version is not 1 (timestamp is irrelevant to other versions).
time_hi = (self.int >> 64) & 0x0fff
time_lo = self.int >> 96
return time_hi << 48 | (self.time_mid << 32) | time_lo
@property
def clock_seq(self):
@ -756,6 +770,44 @@ def uuid5(namespace, name):
int_uuid_5 |= _RFC_4122_VERSION_5_FLAGS
return UUID._from_int(int_uuid_5)
_last_timestamp_v6 = None
def uuid6(node=None, clock_seq=None):
"""Similar to :func:`uuid1` but where fields are ordered differently
for improved DB locality.
More precisely, given a 60-bit timestamp value as specified for UUIDv1,
for UUIDv6 the first 48 most significant bits are stored first, followed
by the 4-bit version (same position), followed by the remaining 12 bits
of the original 60-bit timestamp.
"""
global _last_timestamp_v6
import time
nanoseconds = time.time_ns()
# 0x01b21dd213814000 is the number of 100-ns intervals between the
# UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00.
timestamp = nanoseconds // 100 + 0x01b21dd213814000
if _last_timestamp_v6 is not None and timestamp <= _last_timestamp_v6:
timestamp = _last_timestamp_v6 + 1
_last_timestamp_v6 = timestamp
if clock_seq is None:
import random
clock_seq = random.getrandbits(14) # instead of stable storage
time_hi_and_mid = (timestamp >> 12) & 0xffff_ffff_ffff
time_lo = timestamp & 0x0fff # keep 12 bits and clear version bits
clock_s = clock_seq & 0x3fff # keep 14 bits and clear variant bits
if node is None:
node = getnode()
# --- 32 + 16 --- -- 4 -- -- 12 -- -- 2 -- -- 14 --- 48
# time_hi_and_mid | version | time_lo | variant | clock_seq | node
int_uuid_6 = time_hi_and_mid << 80
int_uuid_6 |= time_lo << 64
int_uuid_6 |= clock_s << 48
int_uuid_6 |= node & 0xffff_ffff_ffff
# by construction, the variant and version bits are already cleared
int_uuid_6 |= _RFC_4122_VERSION_6_FLAGS
return UUID._from_int(int_uuid_6)
def uuid8(a=None, b=None, c=None):
"""Generate a UUID from three custom blocks.
@ -788,6 +840,7 @@ def main():
"uuid3": uuid3,
"uuid4": uuid4,
"uuid5": uuid5,
"uuid6": uuid6,
"uuid8": uuid8,
}
uuid_namespace_funcs = ("uuid3", "uuid5")