python-ulid 3.0.0__py3-none-any.whl → 3.1.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.1.0.dist-info}/METADATA +3 -2
- python_ulid-3.1.0.dist-info/RECORD +10 -0
- {python_ulid-3.0.0.dist-info → python_ulid-3.1.0.dist-info}/WHEEL +1 -1
- ulid/__init__.py +62 -24
- 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.1.0.dist-info}/entry_points.txt +0 -0
- {python_ulid-3.0.0.dist-info → python_ulid-3.1.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.1.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
|
|
@@ -22,6 +22,7 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.11
|
|
24
24
|
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
26
|
Classifier: Topic :: Software Development :: Libraries
|
|
26
27
|
Requires-Python: >=3.9
|
|
27
28
|
Provides-Extra: pydantic
|
|
@@ -0,0 +1,10 @@
|
|
|
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,,
|
ulid/__init__.py
CHANGED
|
@@ -6,12 +6,15 @@ 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
|
|
12
13
|
from typing import TYPE_CHECKING
|
|
13
14
|
from typing import TypeVar
|
|
14
15
|
|
|
16
|
+
from typing_extensions import Self
|
|
17
|
+
|
|
15
18
|
from ulid import base32
|
|
16
19
|
from ulid import constants
|
|
17
20
|
|
|
@@ -51,11 +54,44 @@ class validate_type(Generic[T]): # noqa: N801
|
|
|
51
54
|
return wrapped
|
|
52
55
|
|
|
53
56
|
|
|
54
|
-
|
|
57
|
+
class ValueProvider:
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
self.lock = Lock()
|
|
60
|
+
self.prev_timestamp = constants.MIN_TIMESTAMP
|
|
61
|
+
self.prev_randomness = constants.MIN_RANDOMNESS
|
|
62
|
+
|
|
63
|
+
def timestamp(self, value: float | None = None) -> int:
|
|
64
|
+
if value is None:
|
|
65
|
+
value = time.time_ns() // constants.NANOSECS_IN_MILLISECS
|
|
66
|
+
elif isinstance(value, float):
|
|
67
|
+
value = int(value * constants.MILLISECS_IN_SECS)
|
|
68
|
+
if value > constants.MAX_TIMESTAMP:
|
|
69
|
+
raise ValueError("Value exceeds maximum possible timestamp")
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
def randomness(self) -> bytes:
|
|
73
|
+
with self.lock:
|
|
74
|
+
current_timestamp = self.timestamp()
|
|
75
|
+
if current_timestamp == self.prev_timestamp:
|
|
76
|
+
if self.prev_randomness == constants.MAX_RANDOMNESS:
|
|
77
|
+
raise ValueError("Randomness within same millisecond exhausted")
|
|
78
|
+
randomness = self.increment_bytes(self.prev_randomness)
|
|
79
|
+
else:
|
|
80
|
+
randomness = os.urandom(constants.RANDOMNESS_LEN)
|
|
81
|
+
|
|
82
|
+
self.prev_randomness = randomness
|
|
83
|
+
self.prev_timestamp = current_timestamp
|
|
84
|
+
return randomness
|
|
85
|
+
|
|
86
|
+
def increment_bytes(self, value: bytes) -> bytes:
|
|
87
|
+
length = len(value)
|
|
88
|
+
return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big")
|
|
55
89
|
|
|
56
90
|
|
|
57
91
|
@functools.total_ordering
|
|
58
92
|
class ULID:
|
|
93
|
+
provider = ValueProvider()
|
|
94
|
+
|
|
59
95
|
"""The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
|
|
60
96
|
|
|
61
97
|
.. code-block:: text
|
|
@@ -83,13 +119,11 @@ class ULID:
|
|
|
83
119
|
def __init__(self, value: bytes | None = None) -> None:
|
|
84
120
|
if value is not None and len(value) != constants.BYTES_LEN:
|
|
85
121
|
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
|
-
)
|
|
122
|
+
self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes
|
|
89
123
|
|
|
90
124
|
@classmethod
|
|
91
125
|
@validate_type(datetime)
|
|
92
|
-
def from_datetime(cls
|
|
126
|
+
def from_datetime(cls, value: datetime) -> Self:
|
|
93
127
|
"""Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
|
|
94
128
|
`ULID` will be set to the corresponding timestamp of the datetime.
|
|
95
129
|
|
|
@@ -103,7 +137,7 @@ class ULID:
|
|
|
103
137
|
|
|
104
138
|
@classmethod
|
|
105
139
|
@validate_type(int, float)
|
|
106
|
-
def from_timestamp(cls
|
|
140
|
+
def from_timestamp(cls, value: float) -> Self:
|
|
107
141
|
"""Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
|
|
108
142
|
`float` representing the time in seconds (as it would be returned by :func:`time.time()`)
|
|
109
143
|
or an `int` in milliseconds.
|
|
@@ -114,15 +148,13 @@ class ULID:
|
|
|
114
148
|
>>> ULID.from_timestamp(time.time())
|
|
115
149
|
ULID(01E75QWN5HKQ0JAVX9FG1K4YP4)
|
|
116
150
|
"""
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
timestamp = int.to_bytes(value, constants.TIMESTAMP_LEN, "big")
|
|
120
|
-
randomness = os.urandom(constants.RANDOMNESS_LEN)
|
|
151
|
+
timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big")
|
|
152
|
+
randomness = cls.provider.randomness()
|
|
121
153
|
return cls.from_bytes(timestamp + randomness)
|
|
122
154
|
|
|
123
155
|
@classmethod
|
|
124
156
|
@validate_type(uuid.UUID)
|
|
125
|
-
def from_uuid(cls
|
|
157
|
+
def from_uuid(cls, value: uuid.UUID) -> Self:
|
|
126
158
|
"""Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
|
|
127
159
|
random in that case.
|
|
128
160
|
|
|
@@ -136,37 +168,38 @@ class ULID:
|
|
|
136
168
|
|
|
137
169
|
@classmethod
|
|
138
170
|
@validate_type(bytes)
|
|
139
|
-
def from_bytes(cls
|
|
171
|
+
def from_bytes(cls, bytes_: bytes) -> Self:
|
|
140
172
|
"""Create a new :class:`ULID`-object from sequence of 16 bytes."""
|
|
141
173
|
return cls(bytes_)
|
|
142
174
|
|
|
143
175
|
@classmethod
|
|
144
176
|
@validate_type(str)
|
|
145
|
-
def from_hex(cls
|
|
177
|
+
def from_hex(cls, value: str) -> Self:
|
|
146
178
|
"""Create a new :class:`ULID`-object from 32 character string of hex values."""
|
|
147
179
|
return cls.from_bytes(bytes.fromhex(value))
|
|
148
180
|
|
|
149
181
|
@classmethod
|
|
150
182
|
@validate_type(str)
|
|
151
|
-
def from_str(cls
|
|
183
|
+
def from_str(cls, string: str) -> Self:
|
|
152
184
|
"""Create a new :class:`ULID`-object from a 26 char long string representation."""
|
|
153
185
|
return cls(base32.decode(string))
|
|
154
186
|
|
|
155
187
|
@classmethod
|
|
156
188
|
@validate_type(int)
|
|
157
|
-
def from_int(cls
|
|
189
|
+
def from_int(cls, value: int) -> Self:
|
|
158
190
|
"""Create a new :class:`ULID`-object from an `int`."""
|
|
159
191
|
return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
|
|
160
192
|
|
|
161
193
|
@classmethod
|
|
162
|
-
def parse(cls
|
|
194
|
+
def parse(cls, value: Any) -> Self:
|
|
163
195
|
"""Create a new :class:`ULID`-object from a given value.
|
|
164
196
|
|
|
165
|
-
.. note::
|
|
166
|
-
|
|
197
|
+
.. note::
|
|
198
|
+
This method should only be used when the caller is trying to parse a ULID from
|
|
199
|
+
a value when they're unsure what format/primitive type it will be given in.
|
|
167
200
|
"""
|
|
168
201
|
if isinstance(value, ULID):
|
|
169
|
-
return cast(
|
|
202
|
+
return cast(Self, value)
|
|
170
203
|
if isinstance(value, uuid.UUID):
|
|
171
204
|
return cls.from_uuid(value)
|
|
172
205
|
if isinstance(value, str):
|
|
@@ -190,7 +223,7 @@ class ULID:
|
|
|
190
223
|
return cls.from_bytes(value)
|
|
191
224
|
raise TypeError(f"Cannot parse ULID from type {type(value)}")
|
|
192
225
|
|
|
193
|
-
@
|
|
226
|
+
@functools.cached_property
|
|
194
227
|
def milliseconds(self) -> int:
|
|
195
228
|
"""The timestamp part as epoch time in milliseconds.
|
|
196
229
|
|
|
@@ -201,7 +234,7 @@ class ULID:
|
|
|
201
234
|
"""
|
|
202
235
|
return int.from_bytes(self.bytes[: constants.TIMESTAMP_LEN], byteorder="big")
|
|
203
236
|
|
|
204
|
-
@
|
|
237
|
+
@functools.cached_property
|
|
205
238
|
def timestamp(self) -> float:
|
|
206
239
|
"""The timestamp part as epoch time in seconds.
|
|
207
240
|
|
|
@@ -212,7 +245,7 @@ class ULID:
|
|
|
212
245
|
"""
|
|
213
246
|
return self.milliseconds / constants.MILLISECS_IN_SECS
|
|
214
247
|
|
|
215
|
-
@
|
|
248
|
+
@functools.cached_property
|
|
216
249
|
def datetime(self) -> datetime:
|
|
217
250
|
"""Return the timestamp part as timezone-aware :class:`datetime` in UTC.
|
|
218
251
|
|
|
@@ -223,7 +256,7 @@ class ULID:
|
|
|
223
256
|
"""
|
|
224
257
|
return datetime.fromtimestamp(self.timestamp, timezone.utc)
|
|
225
258
|
|
|
226
|
-
@
|
|
259
|
+
@functools.cached_property
|
|
227
260
|
def hex(self) -> str:
|
|
228
261
|
"""Encode the :class:`ULID`-object as a 32 char sequence of hex values."""
|
|
229
262
|
return self.bytes.hex()
|
|
@@ -297,7 +330,11 @@ class ULID:
|
|
|
297
330
|
core_schema.union_schema([
|
|
298
331
|
core_schema.is_instance_schema(ULID),
|
|
299
332
|
core_schema.no_info_plain_validator_function(ULID),
|
|
300
|
-
core_schema.str_schema(
|
|
333
|
+
core_schema.str_schema(
|
|
334
|
+
pattern=rf"[0-7][{base32.ENCODE}]{{25}}",
|
|
335
|
+
min_length=26,
|
|
336
|
+
max_length=26,
|
|
337
|
+
),
|
|
301
338
|
core_schema.bytes_schema(min_length=16, max_length=16),
|
|
302
339
|
]),
|
|
303
340
|
serialization=core_schema.to_string_ser_schema(
|
|
@@ -309,6 +346,7 @@ class ULID:
|
|
|
309
346
|
def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
|
|
310
347
|
from pydantic_core import PydanticCustomError
|
|
311
348
|
|
|
349
|
+
ulid: ULID
|
|
312
350
|
try:
|
|
313
351
|
if isinstance(value, int):
|
|
314
352
|
ulid = cls.from_int(value)
|
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
|