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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-ulid
3
- Version: 3.1.0
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.9
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
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", bound=type)
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 = self.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
- timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big")
152
- randomness = cls.provider.randomness()
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,,