python-ulid 3.0.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.0.0.dist-info → python_ulid-3.2.0.dist-info}/METADATA +27 -4
- python_ulid-3.2.0.dist-info/RECORD +10 -0
- {python_ulid-3.0.0.dist-info → python_ulid-3.2.0.dist-info}/WHEEL +1 -1
- ulid/__init__.py +148 -26
- ulid/__main__.py +3 -0
- ulid/constants.py +6 -0
- python_ulid-3.0.0.dist-info/RECORD +0 -10
- {python_ulid-3.0.0.dist-info → python_ulid-3.2.0.dist-info}/entry_points.txt +0 -0
- {python_ulid-3.0.0.dist-info → python_ulid-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
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,12 +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
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
26
|
Classifier: Topic :: Software Development :: Libraries
|
|
26
|
-
Requires-Python: >=3.
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: typing-extensions; python_version < '3.11'
|
|
27
29
|
Provides-Extra: pydantic
|
|
28
30
|
Requires-Dist: pydantic>=2.0; extra == 'pydantic'
|
|
29
31
|
Description-Content-Type: text/x-rst
|
|
@@ -39,6 +41,7 @@ A ``ULID`` is a *universally unique lexicographically sortable identifier*. It i
|
|
|
39
41
|
* Uses Crockford's base32 for better efficiency and readability (5 bits per character)
|
|
40
42
|
* Case insensitive
|
|
41
43
|
* No special characters (URL safe)
|
|
44
|
+
* Monotonic sort order (correctly detects and handles the same millisecond)
|
|
42
45
|
|
|
43
46
|
In general the structure of a ULID is as follows:
|
|
44
47
|
|
|
@@ -147,6 +150,20 @@ The ``ULID`` class can be directly used for the popular data validation library
|
|
|
147
150
|
|
|
148
151
|
.. pydantic-end
|
|
149
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
|
+
|
|
150
167
|
.. cli-begin
|
|
151
168
|
|
|
152
169
|
Command line interface
|
|
@@ -204,3 +221,9 @@ Other implementations
|
|
|
204
221
|
* `ulid/javascript <https://github.com/ulid/javascript>`_
|
|
205
222
|
* `RobThree/NUlid <https://github.com/RobThree/NUlid>`_
|
|
206
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
|
@@ -6,6 +6,7 @@ import time
|
|
|
6
6
|
import uuid
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from datetime import timezone
|
|
9
|
+
from threading import Lock
|
|
9
10
|
from typing import Any
|
|
10
11
|
from typing import cast
|
|
11
12
|
from typing import Generic
|
|
@@ -17,12 +18,18 @@ from ulid import constants
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING: # pragma: no cover
|
|
21
|
+
import sys
|
|
20
22
|
from collections.abc import Callable
|
|
21
23
|
|
|
22
24
|
from pydantic import GetCoreSchemaHandler
|
|
23
25
|
from pydantic import ValidatorFunctionWrapHandler
|
|
24
26
|
from pydantic_core import CoreSchema
|
|
25
27
|
|
|
28
|
+
if sys.version_info >= (3, 11):
|
|
29
|
+
from typing import Self
|
|
30
|
+
else:
|
|
31
|
+
from typing_extensions import Self
|
|
32
|
+
|
|
26
33
|
try:
|
|
27
34
|
from importlib.metadata import version
|
|
28
35
|
except ImportError: # pragma: no cover
|
|
@@ -31,12 +38,12 @@ except ImportError: # pragma: no cover
|
|
|
31
38
|
|
|
32
39
|
__version__ = version("python-ulid")
|
|
33
40
|
|
|
34
|
-
T = TypeVar("T"
|
|
41
|
+
T = TypeVar("T")
|
|
35
42
|
R = TypeVar("R")
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
class validate_type(Generic[T]): # noqa: N801
|
|
39
|
-
def __init__(self, *types: T) -> None:
|
|
46
|
+
def __init__(self, *types: type[T]) -> None:
|
|
40
47
|
self.types = types
|
|
41
48
|
|
|
42
49
|
def __call__(self, func: Callable[..., R]) -> Callable[..., R]:
|
|
@@ -51,11 +58,45 @@ class validate_type(Generic[T]): # noqa: N801
|
|
|
51
58
|
return wrapped
|
|
52
59
|
|
|
53
60
|
|
|
54
|
-
|
|
61
|
+
class ValueProvider:
|
|
62
|
+
def __init__(self) -> None:
|
|
63
|
+
self.lock = Lock()
|
|
64
|
+
self.prev_timestamp = constants.MIN_TIMESTAMP
|
|
65
|
+
self.prev_randomness = constants.MIN_RANDOMNESS
|
|
66
|
+
|
|
67
|
+
def timestamp(self, value: float | None = None) -> int:
|
|
68
|
+
if value is None:
|
|
69
|
+
value = time.time_ns() // constants.NANOSECS_IN_MILLISECS
|
|
70
|
+
elif isinstance(value, float):
|
|
71
|
+
value = int(value * constants.MILLISECS_IN_SECS)
|
|
72
|
+
if value > constants.MAX_TIMESTAMP:
|
|
73
|
+
raise ValueError("Value exceeds maximum possible timestamp")
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
def randomness(self, current_timestamp: int | None = None) -> bytes:
|
|
77
|
+
with self.lock:
|
|
78
|
+
if current_timestamp is None:
|
|
79
|
+
current_timestamp = self.timestamp()
|
|
80
|
+
if current_timestamp == self.prev_timestamp:
|
|
81
|
+
if self.prev_randomness == constants.MAX_RANDOMNESS:
|
|
82
|
+
raise ValueError("Randomness within same millisecond exhausted")
|
|
83
|
+
randomness = self.increment_bytes(self.prev_randomness)
|
|
84
|
+
else:
|
|
85
|
+
randomness = os.urandom(constants.RANDOMNESS_LEN)
|
|
86
|
+
|
|
87
|
+
self.prev_randomness = randomness
|
|
88
|
+
self.prev_timestamp = current_timestamp
|
|
89
|
+
return randomness
|
|
90
|
+
|
|
91
|
+
def increment_bytes(self, value: bytes) -> bytes:
|
|
92
|
+
length = len(value)
|
|
93
|
+
return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big")
|
|
55
94
|
|
|
56
95
|
|
|
57
96
|
@functools.total_ordering
|
|
58
97
|
class ULID:
|
|
98
|
+
provider = ValueProvider()
|
|
99
|
+
|
|
59
100
|
"""The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
|
|
60
101
|
|
|
61
102
|
.. code-block:: text
|
|
@@ -83,13 +124,11 @@ class ULID:
|
|
|
83
124
|
def __init__(self, value: bytes | None = None) -> None:
|
|
84
125
|
if value is not None and len(value) != constants.BYTES_LEN:
|
|
85
126
|
raise ValueError("ULID has to be exactly 16 bytes long.")
|
|
86
|
-
self.bytes: bytes = (
|
|
87
|
-
value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
|
|
88
|
-
)
|
|
127
|
+
self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes
|
|
89
128
|
|
|
90
129
|
@classmethod
|
|
91
130
|
@validate_type(datetime)
|
|
92
|
-
def from_datetime(cls
|
|
131
|
+
def from_datetime(cls, value: datetime) -> Self:
|
|
93
132
|
"""Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
|
|
94
133
|
`ULID` will be set to the corresponding timestamp of the datetime.
|
|
95
134
|
|
|
@@ -103,7 +142,7 @@ class ULID:
|
|
|
103
142
|
|
|
104
143
|
@classmethod
|
|
105
144
|
@validate_type(int, float)
|
|
106
|
-
def from_timestamp(cls
|
|
145
|
+
def from_timestamp(cls, value: float) -> Self:
|
|
107
146
|
"""Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
|
|
108
147
|
`float` representing the time in seconds (as it would be returned by :func:`time.time()`)
|
|
109
148
|
or an `int` in milliseconds.
|
|
@@ -114,15 +153,14 @@ class ULID:
|
|
|
114
153
|
>>> ULID.from_timestamp(time.time())
|
|
115
154
|
ULID(01E75QWN5HKQ0JAVX9FG1K4YP4)
|
|
116
155
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
randomness = os.urandom(constants.RANDOMNESS_LEN)
|
|
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)
|
|
121
159
|
return cls.from_bytes(timestamp + randomness)
|
|
122
160
|
|
|
123
161
|
@classmethod
|
|
124
162
|
@validate_type(uuid.UUID)
|
|
125
|
-
def from_uuid(cls
|
|
163
|
+
def from_uuid(cls, value: uuid.UUID) -> Self:
|
|
126
164
|
"""Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
|
|
127
165
|
random in that case.
|
|
128
166
|
|
|
@@ -136,37 +174,38 @@ class ULID:
|
|
|
136
174
|
|
|
137
175
|
@classmethod
|
|
138
176
|
@validate_type(bytes)
|
|
139
|
-
def from_bytes(cls
|
|
177
|
+
def from_bytes(cls, bytes_: bytes) -> Self:
|
|
140
178
|
"""Create a new :class:`ULID`-object from sequence of 16 bytes."""
|
|
141
179
|
return cls(bytes_)
|
|
142
180
|
|
|
143
181
|
@classmethod
|
|
144
182
|
@validate_type(str)
|
|
145
|
-
def from_hex(cls
|
|
183
|
+
def from_hex(cls, value: str) -> Self:
|
|
146
184
|
"""Create a new :class:`ULID`-object from 32 character string of hex values."""
|
|
147
185
|
return cls.from_bytes(bytes.fromhex(value))
|
|
148
186
|
|
|
149
187
|
@classmethod
|
|
150
188
|
@validate_type(str)
|
|
151
|
-
def from_str(cls
|
|
189
|
+
def from_str(cls, string: str) -> Self:
|
|
152
190
|
"""Create a new :class:`ULID`-object from a 26 char long string representation."""
|
|
153
191
|
return cls(base32.decode(string))
|
|
154
192
|
|
|
155
193
|
@classmethod
|
|
156
194
|
@validate_type(int)
|
|
157
|
-
def from_int(cls
|
|
195
|
+
def from_int(cls, value: int) -> Self:
|
|
158
196
|
"""Create a new :class:`ULID`-object from an `int`."""
|
|
159
197
|
return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
|
|
160
198
|
|
|
161
199
|
@classmethod
|
|
162
|
-
def parse(cls
|
|
200
|
+
def parse(cls, value: Any) -> Self:
|
|
163
201
|
"""Create a new :class:`ULID`-object from a given value.
|
|
164
202
|
|
|
165
|
-
.. note::
|
|
166
|
-
|
|
203
|
+
.. note::
|
|
204
|
+
This method should only be used when the caller is trying to parse a ULID from
|
|
205
|
+
a value when they're unsure what format/primitive type it will be given in.
|
|
167
206
|
"""
|
|
168
207
|
if isinstance(value, ULID):
|
|
169
|
-
return cast(
|
|
208
|
+
return cast("Self", value)
|
|
170
209
|
if isinstance(value, uuid.UUID):
|
|
171
210
|
return cls.from_uuid(value)
|
|
172
211
|
if isinstance(value, str):
|
|
@@ -190,7 +229,7 @@ class ULID:
|
|
|
190
229
|
return cls.from_bytes(value)
|
|
191
230
|
raise TypeError(f"Cannot parse ULID from type {type(value)}")
|
|
192
231
|
|
|
193
|
-
@
|
|
232
|
+
@functools.cached_property
|
|
194
233
|
def milliseconds(self) -> int:
|
|
195
234
|
"""The timestamp part as epoch time in milliseconds.
|
|
196
235
|
|
|
@@ -201,7 +240,7 @@ class ULID:
|
|
|
201
240
|
"""
|
|
202
241
|
return int.from_bytes(self.bytes[: constants.TIMESTAMP_LEN], byteorder="big")
|
|
203
242
|
|
|
204
|
-
@
|
|
243
|
+
@functools.cached_property
|
|
205
244
|
def timestamp(self) -> float:
|
|
206
245
|
"""The timestamp part as epoch time in seconds.
|
|
207
246
|
|
|
@@ -212,7 +251,7 @@ class ULID:
|
|
|
212
251
|
"""
|
|
213
252
|
return self.milliseconds / constants.MILLISECS_IN_SECS
|
|
214
253
|
|
|
215
|
-
@
|
|
254
|
+
@functools.cached_property
|
|
216
255
|
def datetime(self) -> datetime:
|
|
217
256
|
"""Return the timestamp part as timezone-aware :class:`datetime` in UTC.
|
|
218
257
|
|
|
@@ -223,7 +262,7 @@ class ULID:
|
|
|
223
262
|
"""
|
|
224
263
|
return datetime.fromtimestamp(self.timestamp, timezone.utc)
|
|
225
264
|
|
|
226
|
-
@
|
|
265
|
+
@functools.cached_property
|
|
227
266
|
def hex(self) -> str:
|
|
228
267
|
"""Encode the :class:`ULID`-object as a 32 char sequence of hex values."""
|
|
229
268
|
return self.bytes.hex()
|
|
@@ -248,6 +287,84 @@ class ULID:
|
|
|
248
287
|
"""
|
|
249
288
|
return uuid.UUID(bytes=self.bytes, version=4)
|
|
250
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
|
+
|
|
251
368
|
def __repr__(self) -> str:
|
|
252
369
|
return f"ULID({self!s})"
|
|
253
370
|
|
|
@@ -297,7 +414,11 @@ class ULID:
|
|
|
297
414
|
core_schema.union_schema([
|
|
298
415
|
core_schema.is_instance_schema(ULID),
|
|
299
416
|
core_schema.no_info_plain_validator_function(ULID),
|
|
300
|
-
core_schema.str_schema(
|
|
417
|
+
core_schema.str_schema(
|
|
418
|
+
pattern=rf"[0-7][{base32.ENCODE}]{{25}}",
|
|
419
|
+
min_length=26,
|
|
420
|
+
max_length=26,
|
|
421
|
+
),
|
|
301
422
|
core_schema.bytes_schema(min_length=16, max_length=16),
|
|
302
423
|
]),
|
|
303
424
|
serialization=core_schema.to_string_ser_schema(
|
|
@@ -309,6 +430,7 @@ class ULID:
|
|
|
309
430
|
def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
|
|
310
431
|
from pydantic_core import PydanticCustomError
|
|
311
432
|
|
|
433
|
+
ulid: ULID
|
|
312
434
|
try:
|
|
313
435
|
if isinstance(value, int):
|
|
314
436
|
ulid = cls.from_int(value)
|
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:
|
ulid/constants.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
MILLISECS_IN_SECS = 1000
|
|
2
2
|
NANOSECS_IN_MILLISECS = 1000000
|
|
3
3
|
|
|
4
|
+
MIN_RANDOMNESS = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
|
5
|
+
MAX_RANDOMNESS = b"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"
|
|
6
|
+
|
|
7
|
+
MIN_TIMESTAMP = 0
|
|
8
|
+
MAX_TIMESTAMP = 2**48 - 1
|
|
9
|
+
|
|
4
10
|
TIMESTAMP_LEN = 6
|
|
5
11
|
RANDOMNESS_LEN = 10
|
|
6
12
|
BYTES_LEN = TIMESTAMP_LEN + RANDOMNESS_LEN
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ulid/__init__.py,sha256=yoBtXpTz-t8xnfOHtM70-hYIeYKk75ya7t6B6uWILhg,10971
|
|
2
|
-
ulid/__main__.py,sha256=TUo3dkWkJLfzRJyd55nfqGqvv1NcG7fBpsZHIi_p4rw,5421
|
|
3
|
-
ulid/base32.py,sha256=iUE5BGPW5Dul-Eu3ITvcymk8acNkIFAVTlrL12YIPKI,5961
|
|
4
|
-
ulid/constants.py,sha256=tJByGuryoTT_xfECvpMNPNwRvZMp2K_ZlZ7tOLeaXJs,335
|
|
5
|
-
ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
python_ulid-3.0.0.dist-info/METADATA,sha256=82s8fmElXLy5Um_bjqhtZfiXd54YF0TDI2iOlKliLMs,5782
|
|
7
|
-
python_ulid-3.0.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
|
8
|
-
python_ulid-3.0.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
|
|
9
|
-
python_ulid-3.0.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
|
|
10
|
-
python_ulid-3.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|