python-ulid 2.7.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-2.7.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-2.7.0.dist-info → python_ulid-3.1.0.dist-info}/WHEEL +1 -1
- ulid/__init__.py +107 -38
- ulid/__main__.py +22 -18
- ulid/base32.py +51 -59
- ulid/constants.py +11 -0
- python_ulid-2.7.0.dist-info/RECORD +0 -10
- {python_ulid-2.7.0.dist-info → python_ulid-3.1.0.dist-info}/entry_points.txt +0 -0
- {python_ulid-2.7.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
|
+
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
|
@@ -4,19 +4,24 @@ import functools
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
|
-
from collections.abc import Callable
|
|
8
7
|
from datetime import datetime
|
|
9
8
|
from datetime import timezone
|
|
9
|
+
from threading import Lock
|
|
10
10
|
from typing import Any
|
|
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
|
|
|
18
21
|
|
|
19
22
|
if TYPE_CHECKING: # pragma: no cover
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
20
25
|
from pydantic import GetCoreSchemaHandler
|
|
21
26
|
from pydantic import ValidatorFunctionWrapHandler
|
|
22
27
|
from pydantic_core import CoreSchema
|
|
@@ -29,7 +34,6 @@ except ImportError: # pragma: no cover
|
|
|
29
34
|
|
|
30
35
|
__version__ = version("python-ulid")
|
|
31
36
|
|
|
32
|
-
|
|
33
37
|
T = TypeVar("T", bound=type)
|
|
34
38
|
R = TypeVar("R")
|
|
35
39
|
|
|
@@ -44,17 +48,50 @@ class validate_type(Generic[T]): # noqa: N801
|
|
|
44
48
|
if not isinstance(value, self.types):
|
|
45
49
|
message = "Value has to be of type "
|
|
46
50
|
message += " or ".join([t.__name__ for t in self.types])
|
|
47
|
-
raise
|
|
51
|
+
raise TypeError(message)
|
|
48
52
|
return func(cls, value)
|
|
49
53
|
|
|
50
54
|
return wrapped
|
|
51
55
|
|
|
52
56
|
|
|
53
|
-
|
|
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")
|
|
54
89
|
|
|
55
90
|
|
|
56
91
|
@functools.total_ordering
|
|
57
92
|
class ULID:
|
|
93
|
+
provider = ValueProvider()
|
|
94
|
+
|
|
58
95
|
"""The :class:`ULID` object consists of a timestamp part of 48 bits and of 80 random bits.
|
|
59
96
|
|
|
60
97
|
.. code-block:: text
|
|
@@ -82,13 +119,11 @@ class ULID:
|
|
|
82
119
|
def __init__(self, value: bytes | None = None) -> None:
|
|
83
120
|
if value is not None and len(value) != constants.BYTES_LEN:
|
|
84
121
|
raise ValueError("ULID has to be exactly 16 bytes long.")
|
|
85
|
-
self.bytes: bytes = (
|
|
86
|
-
value or ULID.from_timestamp(time.time_ns() // constants.NANOSECS_IN_MILLISECS).bytes
|
|
87
|
-
)
|
|
122
|
+
self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes
|
|
88
123
|
|
|
89
124
|
@classmethod
|
|
90
125
|
@validate_type(datetime)
|
|
91
|
-
def from_datetime(cls
|
|
126
|
+
def from_datetime(cls, value: datetime) -> Self:
|
|
92
127
|
"""Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the
|
|
93
128
|
`ULID` will be set to the corresponding timestamp of the datetime.
|
|
94
129
|
|
|
@@ -102,7 +137,7 @@ class ULID:
|
|
|
102
137
|
|
|
103
138
|
@classmethod
|
|
104
139
|
@validate_type(int, float)
|
|
105
|
-
def from_timestamp(cls
|
|
140
|
+
def from_timestamp(cls, value: float) -> Self:
|
|
106
141
|
"""Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a
|
|
107
142
|
`float` representing the time in seconds (as it would be returned by :func:`time.time()`)
|
|
108
143
|
or an `int` in milliseconds.
|
|
@@ -113,15 +148,13 @@ class ULID:
|
|
|
113
148
|
>>> ULID.from_timestamp(time.time())
|
|
114
149
|
ULID(01E75QWN5HKQ0JAVX9FG1K4YP4)
|
|
115
150
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
timestamp = int.to_bytes(value, constants.TIMESTAMP_LEN, "big")
|
|
119
|
-
randomness = os.urandom(constants.RANDOMNESS_LEN)
|
|
151
|
+
timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big")
|
|
152
|
+
randomness = cls.provider.randomness()
|
|
120
153
|
return cls.from_bytes(timestamp + randomness)
|
|
121
154
|
|
|
122
155
|
@classmethod
|
|
123
156
|
@validate_type(uuid.UUID)
|
|
124
|
-
def from_uuid(cls
|
|
157
|
+
def from_uuid(cls, value: uuid.UUID) -> Self:
|
|
125
158
|
"""Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be
|
|
126
159
|
random in that case.
|
|
127
160
|
|
|
@@ -135,40 +168,73 @@ class ULID:
|
|
|
135
168
|
|
|
136
169
|
@classmethod
|
|
137
170
|
@validate_type(bytes)
|
|
138
|
-
def from_bytes(cls
|
|
171
|
+
def from_bytes(cls, bytes_: bytes) -> Self:
|
|
139
172
|
"""Create a new :class:`ULID`-object from sequence of 16 bytes."""
|
|
140
173
|
return cls(bytes_)
|
|
141
174
|
|
|
142
175
|
@classmethod
|
|
143
176
|
@validate_type(str)
|
|
144
|
-
def from_hex(cls
|
|
177
|
+
def from_hex(cls, value: str) -> Self:
|
|
145
178
|
"""Create a new :class:`ULID`-object from 32 character string of hex values."""
|
|
146
179
|
return cls.from_bytes(bytes.fromhex(value))
|
|
147
180
|
|
|
148
181
|
@classmethod
|
|
149
182
|
@validate_type(str)
|
|
150
|
-
def from_str(cls
|
|
183
|
+
def from_str(cls, string: str) -> Self:
|
|
151
184
|
"""Create a new :class:`ULID`-object from a 26 char long string representation."""
|
|
152
185
|
return cls(base32.decode(string))
|
|
153
186
|
|
|
154
187
|
@classmethod
|
|
155
188
|
@validate_type(int)
|
|
156
|
-
def from_int(cls
|
|
189
|
+
def from_int(cls, value: int) -> Self:
|
|
157
190
|
"""Create a new :class:`ULID`-object from an `int`."""
|
|
158
191
|
return cls(int.to_bytes(value, constants.BYTES_LEN, "big"))
|
|
159
192
|
|
|
160
|
-
@
|
|
193
|
+
@classmethod
|
|
194
|
+
def parse(cls, value: Any) -> Self:
|
|
195
|
+
"""Create a new :class:`ULID`-object from a given value.
|
|
196
|
+
|
|
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.
|
|
200
|
+
"""
|
|
201
|
+
if isinstance(value, ULID):
|
|
202
|
+
return cast(Self, value)
|
|
203
|
+
if isinstance(value, uuid.UUID):
|
|
204
|
+
return cls.from_uuid(value)
|
|
205
|
+
if isinstance(value, str):
|
|
206
|
+
len_value = len(value)
|
|
207
|
+
if len_value == constants.UUID_REPR_LEN:
|
|
208
|
+
return cls.from_uuid(uuid.UUID(value))
|
|
209
|
+
if len_value == constants.HEX_REPR_LEN:
|
|
210
|
+
return cls.from_hex(value)
|
|
211
|
+
if len_value == constants.REPR_LEN:
|
|
212
|
+
return cls.from_str(value)
|
|
213
|
+
raise ValueError(f"Cannot parse ULID from string of length {len_value}")
|
|
214
|
+
if isinstance(value, int):
|
|
215
|
+
if len(str(value)) == constants.INT_REPR_LEN:
|
|
216
|
+
return cls.from_int(value)
|
|
217
|
+
return cls.from_timestamp(value)
|
|
218
|
+
if isinstance(value, float):
|
|
219
|
+
return cls.from_timestamp(value)
|
|
220
|
+
if isinstance(value, datetime):
|
|
221
|
+
return cls.from_datetime(value)
|
|
222
|
+
if isinstance(value, bytes):
|
|
223
|
+
return cls.from_bytes(value)
|
|
224
|
+
raise TypeError(f"Cannot parse ULID from type {type(value)}")
|
|
225
|
+
|
|
226
|
+
@functools.cached_property
|
|
161
227
|
def milliseconds(self) -> int:
|
|
162
228
|
"""The timestamp part as epoch time in milliseconds.
|
|
163
229
|
|
|
164
230
|
Examples:
|
|
165
231
|
|
|
166
|
-
>>> ulid.
|
|
232
|
+
>>> ulid.milliseconds
|
|
167
233
|
1588257207560
|
|
168
234
|
"""
|
|
169
235
|
return int.from_bytes(self.bytes[: constants.TIMESTAMP_LEN], byteorder="big")
|
|
170
236
|
|
|
171
|
-
@
|
|
237
|
+
@functools.cached_property
|
|
172
238
|
def timestamp(self) -> float:
|
|
173
239
|
"""The timestamp part as epoch time in seconds.
|
|
174
240
|
|
|
@@ -179,7 +245,7 @@ class ULID:
|
|
|
179
245
|
"""
|
|
180
246
|
return self.milliseconds / constants.MILLISECS_IN_SECS
|
|
181
247
|
|
|
182
|
-
@
|
|
248
|
+
@functools.cached_property
|
|
183
249
|
def datetime(self) -> datetime:
|
|
184
250
|
"""Return the timestamp part as timezone-aware :class:`datetime` in UTC.
|
|
185
251
|
|
|
@@ -190,7 +256,7 @@ class ULID:
|
|
|
190
256
|
"""
|
|
191
257
|
return datetime.fromtimestamp(self.timestamp, timezone.utc)
|
|
192
258
|
|
|
193
|
-
@
|
|
259
|
+
@functools.cached_property
|
|
194
260
|
def hex(self) -> str:
|
|
195
261
|
"""Encode the :class:`ULID`-object as a 32 char sequence of hex values."""
|
|
196
262
|
return self.bytes.hex()
|
|
@@ -233,22 +299,22 @@ class ULID:
|
|
|
233
299
|
def __lt__(self, other: Any) -> bool:
|
|
234
300
|
if isinstance(other, ULID):
|
|
235
301
|
return self.bytes < other.bytes
|
|
236
|
-
|
|
302
|
+
if isinstance(other, int):
|
|
237
303
|
return int(self) < other
|
|
238
|
-
|
|
304
|
+
if isinstance(other, bytes):
|
|
239
305
|
return self.bytes < other
|
|
240
|
-
|
|
306
|
+
if isinstance(other, str):
|
|
241
307
|
return str(self) < other
|
|
242
308
|
return NotImplemented
|
|
243
309
|
|
|
244
|
-
def __eq__(self, other:
|
|
310
|
+
def __eq__(self, other: object) -> bool:
|
|
245
311
|
if isinstance(other, ULID):
|
|
246
312
|
return self.bytes == other.bytes
|
|
247
|
-
|
|
313
|
+
if isinstance(other, int):
|
|
248
314
|
return int(self) == other
|
|
249
|
-
|
|
315
|
+
if isinstance(other, bytes):
|
|
250
316
|
return self.bytes == other
|
|
251
|
-
|
|
317
|
+
if isinstance(other, str):
|
|
252
318
|
return str(self) == other
|
|
253
319
|
return NotImplemented
|
|
254
320
|
|
|
@@ -261,14 +327,16 @@ class ULID:
|
|
|
261
327
|
|
|
262
328
|
return core_schema.no_info_wrap_validator_function(
|
|
263
329
|
cls._pydantic_validate,
|
|
264
|
-
core_schema.union_schema(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
330
|
+
core_schema.union_schema([
|
|
331
|
+
core_schema.is_instance_schema(ULID),
|
|
332
|
+
core_schema.no_info_plain_validator_function(ULID),
|
|
333
|
+
core_schema.str_schema(
|
|
334
|
+
pattern=rf"[0-7][{base32.ENCODE}]{{25}}",
|
|
335
|
+
min_length=26,
|
|
336
|
+
max_length=26,
|
|
337
|
+
),
|
|
338
|
+
core_schema.bytes_schema(min_length=16, max_length=16),
|
|
339
|
+
]),
|
|
272
340
|
serialization=core_schema.to_string_ser_schema(
|
|
273
341
|
when_used="json-unless-none",
|
|
274
342
|
),
|
|
@@ -278,6 +346,7 @@ class ULID:
|
|
|
278
346
|
def _pydantic_validate(cls, value: Any, handler: ValidatorFunctionWrapHandler) -> Any:
|
|
279
347
|
from pydantic_core import PydanticCustomError
|
|
280
348
|
|
|
349
|
+
ulid: ULID
|
|
281
350
|
try:
|
|
282
351
|
if isinstance(value, int):
|
|
283
352
|
ulid = cls.from_int(value)
|
ulid/__main__.py
CHANGED
|
@@ -4,17 +4,21 @@ import argparse
|
|
|
4
4
|
import shutil
|
|
5
5
|
import sys
|
|
6
6
|
import textwrap
|
|
7
|
-
from collections.abc import Callable
|
|
8
|
-
from collections.abc import Sequence
|
|
9
7
|
from datetime import datetime
|
|
10
8
|
from functools import partial
|
|
11
9
|
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
12
11
|
from uuid import UUID
|
|
13
12
|
|
|
14
13
|
import ulid
|
|
15
14
|
from ulid import ULID
|
|
16
15
|
|
|
17
16
|
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
def make_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
|
19
23
|
parser = argparse.ArgumentParser(
|
|
20
24
|
prog=prog,
|
|
@@ -135,30 +139,30 @@ def show(args: argparse.Namespace) -> str:
|
|
|
135
139
|
ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid))
|
|
136
140
|
if args.uuid:
|
|
137
141
|
return str(ulid.to_uuid())
|
|
138
|
-
|
|
142
|
+
if args.uuid4:
|
|
139
143
|
return str(ulid.to_uuid4())
|
|
140
|
-
|
|
144
|
+
if args.hex:
|
|
141
145
|
return ulid.hex
|
|
142
|
-
|
|
146
|
+
if args.int:
|
|
143
147
|
return str(int(ulid))
|
|
144
|
-
|
|
148
|
+
if args.timestamp:
|
|
145
149
|
return str(ulid.timestamp)
|
|
146
|
-
|
|
150
|
+
if args.datetime:
|
|
147
151
|
return ulid.datetime.isoformat()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
).strip()
|
|
152
|
+
return textwrap.dedent(
|
|
153
|
+
f"""
|
|
154
|
+
ULID: {ulid!s}
|
|
155
|
+
Hex: {ulid.hex}
|
|
156
|
+
Int: {int(ulid)}
|
|
157
|
+
Timestamp: {ulid.timestamp}
|
|
158
|
+
Datetime: {ulid.datetime.isoformat()}
|
|
159
|
+
"""
|
|
160
|
+
).strip()
|
|
158
161
|
|
|
159
162
|
|
|
160
163
|
def entrypoint() -> None: # pragma: no cover
|
|
161
|
-
|
|
164
|
+
if (value := main(sys.argv[1:])) is not None:
|
|
165
|
+
print(value) # noqa: T201
|
|
162
166
|
|
|
163
167
|
|
|
164
168
|
if __name__ == "__main__": # pragma: no cover
|
ulid/base32.py
CHANGED
|
@@ -153,46 +153,42 @@ def encode_timestamp(binary: bytes) -> str:
|
|
|
153
153
|
if len(binary) != constants.TIMESTAMP_LEN:
|
|
154
154
|
raise ValueError("Timestamp value has to be exactly 6 bytes long.")
|
|
155
155
|
lut = ENCODE
|
|
156
|
-
return "".join(
|
|
157
|
-
[
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
]
|
|
169
|
-
)
|
|
156
|
+
return "".join([
|
|
157
|
+
lut[(binary[0] & 224) >> 5],
|
|
158
|
+
lut[(binary[0] & 31)],
|
|
159
|
+
lut[(binary[1] & 248) >> 3],
|
|
160
|
+
lut[((binary[1] & 7) << 2) | ((binary[2] & 192) >> 6)],
|
|
161
|
+
lut[((binary[2] & 62) >> 1)],
|
|
162
|
+
lut[((binary[2] & 1) << 4) | ((binary[3] & 240) >> 4)],
|
|
163
|
+
lut[((binary[3] & 15) << 1) | ((binary[4] & 128) >> 7)],
|
|
164
|
+
lut[(binary[4] & 124) >> 2],
|
|
165
|
+
lut[((binary[4] & 3) << 3) | ((binary[5] & 224) >> 5)],
|
|
166
|
+
lut[(binary[5] & 31)],
|
|
167
|
+
])
|
|
170
168
|
|
|
171
169
|
|
|
172
170
|
def encode_randomness(binary: bytes) -> str:
|
|
173
171
|
if len(binary) != constants.RANDOMNESS_LEN:
|
|
174
172
|
raise ValueError("Randomness value has to be exactly 10 bytes long.")
|
|
175
173
|
lut = ENCODE
|
|
176
|
-
return "".join(
|
|
177
|
-
[
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
]
|
|
195
|
-
)
|
|
174
|
+
return "".join([
|
|
175
|
+
lut[(binary[0] & 248) >> 3],
|
|
176
|
+
lut[((binary[0] & 7) << 2) | ((binary[1] & 192) >> 6)],
|
|
177
|
+
lut[(binary[1] & 62) >> 1],
|
|
178
|
+
lut[((binary[1] & 1) << 4) | ((binary[2] & 240) >> 4)],
|
|
179
|
+
lut[((binary[2] & 15) << 1) | ((binary[3] & 128) >> 7)],
|
|
180
|
+
lut[(binary[3] & 124) >> 2],
|
|
181
|
+
lut[((binary[3] & 3) << 3) | ((binary[4] & 224) >> 5)],
|
|
182
|
+
lut[(binary[4] & 31)],
|
|
183
|
+
lut[(binary[5] & 248) >> 3],
|
|
184
|
+
lut[((binary[5] & 7) << 2) | ((binary[6] & 192) >> 6)],
|
|
185
|
+
lut[(binary[6] & 62) >> 1],
|
|
186
|
+
lut[((binary[6] & 1) << 4) | ((binary[7] & 240) >> 4)],
|
|
187
|
+
lut[((binary[7] & 15) << 1) | ((binary[8] & 128) >> 7)],
|
|
188
|
+
lut[(binary[8] & 124) >> 2],
|
|
189
|
+
lut[((binary[8] & 3) << 3) | ((binary[9] & 224) >> 5)],
|
|
190
|
+
lut[(binary[9] & 31)],
|
|
191
|
+
])
|
|
196
192
|
|
|
197
193
|
|
|
198
194
|
def decode(encoded: str) -> bytes:
|
|
@@ -211,18 +207,16 @@ def decode_timestamp(encoded: str) -> bytes:
|
|
|
211
207
|
lut = DECODE
|
|
212
208
|
values: bytes = bytes(encoded, "ascii")
|
|
213
209
|
# https://github.com/ulid/spec?tab=readme-ov-file#overflow-errors-when-parsing-base32-strings
|
|
214
|
-
if lut[values[0]] > 7:
|
|
210
|
+
if lut[values[0]] > 7: # noqa: PLR2004
|
|
215
211
|
raise ValueError(f"Timestamp value {encoded} is too large and will overflow 128-bits.")
|
|
216
|
-
return bytes(
|
|
217
|
-
[
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
]
|
|
225
|
-
)
|
|
212
|
+
return bytes([
|
|
213
|
+
((lut[values[0]] << 5) | lut[values[1]]) & 0xFF,
|
|
214
|
+
((lut[values[2]] << 3) | (lut[values[3]] >> 2)) & 0xFF,
|
|
215
|
+
((lut[values[3]] << 6) | (lut[values[4]] << 1) | (lut[values[5]] >> 4)) & 0xFF,
|
|
216
|
+
((lut[values[5]] << 4) | (lut[values[6]] >> 1)) & 0xFF,
|
|
217
|
+
((lut[values[6]] << 7) | (lut[values[7]] << 2) | (lut[values[8]] >> 3)) & 0xFF,
|
|
218
|
+
((lut[values[8]] << 5) | (lut[values[9]])) & 0xFF,
|
|
219
|
+
])
|
|
226
220
|
|
|
227
221
|
|
|
228
222
|
def decode_randomness(encoded: str) -> bytes:
|
|
@@ -230,17 +224,15 @@ def decode_randomness(encoded: str) -> bytes:
|
|
|
230
224
|
raise ValueError("ULID randomness has to be exactly 16 characters long.")
|
|
231
225
|
lut = DECODE
|
|
232
226
|
values = bytes(encoded, "ascii")
|
|
233
|
-
return bytes(
|
|
234
|
-
[
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
]
|
|
246
|
-
)
|
|
227
|
+
return bytes([
|
|
228
|
+
((lut[values[0]] << 3) | (lut[values[1]] >> 2)) & 0xFF,
|
|
229
|
+
((lut[values[1]] << 6) | (lut[values[2]] << 1) | (lut[values[3]] >> 4)) & 0xFF,
|
|
230
|
+
((lut[values[3]] << 4) | (lut[values[4]] >> 1)) & 0xFF,
|
|
231
|
+
((lut[values[4]] << 7) | (lut[values[5]] << 2) | (lut[values[6]] >> 3)) & 0xFF,
|
|
232
|
+
((lut[values[6]] << 5) | (lut[values[7]])) & 0xFF,
|
|
233
|
+
((lut[values[8]] << 3) | (lut[values[9]] >> 2)) & 0xFF,
|
|
234
|
+
((lut[values[9]] << 6) | (lut[values[10]] << 1) | (lut[values[11]] >> 4)) & 0xFF,
|
|
235
|
+
((lut[values[11]] << 4) | (lut[values[12]] >> 1)) & 0xFF,
|
|
236
|
+
((lut[values[12]] << 7) | (lut[values[13]] << 2) | (lut[values[14]] >> 3)) & 0xFF,
|
|
237
|
+
((lut[values[14]] << 5) | (lut[values[15]])) & 0xFF,
|
|
238
|
+
])
|
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
|
|
@@ -8,3 +14,8 @@ BYTES_LEN = TIMESTAMP_LEN + RANDOMNESS_LEN
|
|
|
8
14
|
TIMESTAMP_REPR_LEN = 10
|
|
9
15
|
RANDOMNESS_REPR_LEN = 16
|
|
10
16
|
REPR_LEN = TIMESTAMP_REPR_LEN + RANDOMNESS_REPR_LEN
|
|
17
|
+
|
|
18
|
+
INT_REPR_LEN = 37
|
|
19
|
+
|
|
20
|
+
HEX_REPR_LEN = 32
|
|
21
|
+
UUID_REPR_LEN = 36 # UUID with dash-separated segments
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
ulid/__init__.py,sha256=tPyxks3cQzUiqkRdLgoDuunqiz090RiFkUfCMGxw7fU,9588
|
|
2
|
-
ulid/__main__.py,sha256=Cmg0NEz3GUZ2WhsZVpY3-Kq-JOkxJFaLsRNgj3Aosyw,5341
|
|
3
|
-
ulid/base32.py,sha256=cMj7Ye9qRdWn3n3hwaEXGMi2OkqmleAAxE20y7qmBqI,6184
|
|
4
|
-
ulid/constants.py,sha256=N_nw4W2ciXO8C1IeuO3YK3j4_x9QW-0KyvrSQUuM-G4,241
|
|
5
|
-
ulid/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
python_ulid-2.7.0.dist-info/METADATA,sha256=zJASFWDrkYVNYfnjjA70QGCC_kyzfyufEog6Ig4emH8,5782
|
|
7
|
-
python_ulid-2.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
|
8
|
-
python_ulid-2.7.0.dist-info/entry_points.txt,sha256=9214ghzfSgS8a-TjjlWDF7bI7KSaeewOCBKn5uz7bdU,50
|
|
9
|
-
python_ulid-2.7.0.dist-info/licenses/LICENSE,sha256=DiDPNhW5yUHIqaKowvX0c1c9xWiI0xW4ji8R9N2NJbw,1056
|
|
10
|
-
python_ulid-2.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|