cuid2 2.0.2__tar.gz → 2.0.4__tar.gz
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.
- {cuid2-2.0.2 → cuid2-2.0.4}/PKG-INFO +1 -1
- {cuid2-2.0.2 → cuid2-2.0.4}/pyproject.toml +1 -1
- {cuid2-2.0.2 → cuid2-2.0.4}/pyproject.toml.orig +1 -1
- cuid2-2.0.4/src/cuid2/__init__.py +21 -0
- {cuid2-2.0.2 → cuid2-2.0.4}/src/cuid2/generator.py +65 -20
- {cuid2-2.0.2 → cuid2-2.0.4}/src/cuid2/utils.py +24 -26
- cuid2-2.0.2/src/cuid2/__init__.py +0 -5
- {cuid2-2.0.2 → cuid2-2.0.4}/LICENSE +0 -0
- {cuid2-2.0.2 → cuid2-2.0.4}/README.md +0 -0
- {cuid2-2.0.2 → cuid2-2.0.4}/src/cuid2/cli.py +0 -0
- {cuid2-2.0.2 → cuid2-2.0.4}/src/cuid2/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."""
|
|
2
|
+
|
|
3
|
+
from .generator import (
|
|
4
|
+
BIG_LENGTH,
|
|
5
|
+
DEFAULT_LENGTH,
|
|
6
|
+
INITIAL_COUNT_MAX,
|
|
7
|
+
MAXIMUM_LENGTH,
|
|
8
|
+
MIN_LENGTH,
|
|
9
|
+
Cuid,
|
|
10
|
+
cuid_wrapper,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BIG_LENGTH",
|
|
15
|
+
"DEFAULT_LENGTH",
|
|
16
|
+
"INITIAL_COUNT_MAX",
|
|
17
|
+
"MAXIMUM_LENGTH",
|
|
18
|
+
"MIN_LENGTH",
|
|
19
|
+
"Cuid",
|
|
20
|
+
"cuid_wrapper",
|
|
21
|
+
]
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import re
|
|
4
5
|
import time
|
|
6
|
+
import warnings
|
|
5
7
|
from math import floor
|
|
6
8
|
from secrets import SystemRandom
|
|
7
9
|
from typing import TYPE_CHECKING, Callable, Final, Optional, Protocol
|
|
8
10
|
|
|
9
11
|
from cuid2 import utils
|
|
12
|
+
from cuid2.utils import BIG_LENGTH, DEFAULT_LENGTH, INITIAL_COUNT_MAX, MAXIMUM_LENGTH, MIN_LENGTH
|
|
10
13
|
|
|
11
14
|
if TYPE_CHECKING:
|
|
12
15
|
from _random import Random
|
|
@@ -15,14 +18,47 @@ if TYPE_CHECKING:
|
|
|
15
18
|
def __call__(self: FingerprintCallable, random_generator: Random) -> str: ...
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
# ~22k hosts before 50% chance of initial counter collision
|
|
19
|
-
# with a remaining counter range of 9.0e+15 in JavaScript.
|
|
20
|
-
INITIAL_COUNT_MAX: Final[int] = 476782367
|
|
21
|
-
DEFAULT_LENGTH: Final = 24
|
|
22
|
-
MAXIMUM_LENGTH: Final = 98
|
|
23
21
|
_CUID_REGEX: Final[re.Pattern[str]] = re.compile(r"^[a-z][0-9a-z]+\Z")
|
|
24
22
|
|
|
25
23
|
|
|
24
|
+
def _validate_length(length: int) -> None:
|
|
25
|
+
"""Validate that the requested CUID length is within supported bounds.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
length : int
|
|
30
|
+
The requested CUID length to validate.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If `length` is a boolean, less than 1, or greater than `MAXIMUM_LENGTH` (98).
|
|
36
|
+
|
|
37
|
+
Warns
|
|
38
|
+
-----
|
|
39
|
+
DeprecationWarning
|
|
40
|
+
If `length` is outside the CUID2 specification range (`MIN_LENGTH`..`BIG_LENGTH`, i.e., 2..32).
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(length, bool) or length < 1:
|
|
43
|
+
msg = "Length must be at least 1 character."
|
|
44
|
+
raise ValueError(msg)
|
|
45
|
+
if length > MAXIMUM_LENGTH:
|
|
46
|
+
msg = "Length must never exceed 98 characters."
|
|
47
|
+
raise ValueError(msg)
|
|
48
|
+
if length < MIN_LENGTH or length > BIG_LENGTH:
|
|
49
|
+
stacklevel: int = 1
|
|
50
|
+
frame = inspect.currentframe()
|
|
51
|
+
while frame is not None and frame.f_code.co_filename == __file__:
|
|
52
|
+
stacklevel += 1
|
|
53
|
+
frame = frame.f_back
|
|
54
|
+
warnings.warn(
|
|
55
|
+
f"Length {length} outside the CUID2 specification range ({MIN_LENGTH}..{BIG_LENGTH}) "
|
|
56
|
+
"is deprecated and will raise ValueError in v3.0.0.",
|
|
57
|
+
DeprecationWarning,
|
|
58
|
+
stacklevel=stacklevel,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
26
62
|
class Cuid: # pylint: disable=too-few-public-methods
|
|
27
63
|
def __init__(
|
|
28
64
|
self: Cuid,
|
|
@@ -46,20 +82,21 @@ class Cuid: # pylint: disable=too-few-public-methods
|
|
|
46
82
|
The length parameter is an integer that determines the maximum length of the generated string.
|
|
47
83
|
It has a default value of DEFAULT_LENGTH (24).
|
|
48
84
|
A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
|
|
85
|
+
Lengths outside `MIN_LENGTH`..`BIG_LENGTH` (2..32) emit a DeprecationWarning.
|
|
49
86
|
fingerprint : "FingerprintCallable", default=utils.create_fingerprint
|
|
50
87
|
The "fingerprint" parameter is a callable function that generates a unique identifier.
|
|
51
88
|
|
|
52
89
|
Raises
|
|
53
90
|
------
|
|
54
91
|
ValueError
|
|
55
|
-
If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
92
|
+
If the length parameter is a boolean, less than 1, or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
93
|
+
|
|
94
|
+
Warns
|
|
95
|
+
-----
|
|
96
|
+
DeprecationWarning
|
|
97
|
+
If the length parameter is outside `MIN_LENGTH`..`BIG_LENGTH` (2..32 characters).
|
|
56
98
|
"""
|
|
57
|
-
|
|
58
|
-
msg = "Length must be at least 1 character."
|
|
59
|
-
raise ValueError(msg)
|
|
60
|
-
if length > MAXIMUM_LENGTH:
|
|
61
|
-
msg = "Length must never exceed 98 characters."
|
|
62
|
-
raise ValueError(msg)
|
|
99
|
+
_validate_length(length)
|
|
63
100
|
|
|
64
101
|
self._random: Random = random_generator()
|
|
65
102
|
self._counter: Callable[[], int] = counter(floor(self._random.random() * INITIAL_COUNT_MAX))
|
|
@@ -75,6 +112,7 @@ class Cuid: # pylint: disable=too-few-public-methods
|
|
|
75
112
|
The length parameter is an optional integer value that specifies the length of the generated string.
|
|
76
113
|
If it is not provided, the default length value provided during class initialization is used.
|
|
77
114
|
A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
|
|
115
|
+
Lengths outside `MIN_LENGTH`..`BIG_LENGTH` (2..32) emit a DeprecationWarning.
|
|
78
116
|
|
|
79
117
|
Returns
|
|
80
118
|
-------
|
|
@@ -86,15 +124,16 @@ class Cuid: # pylint: disable=too-few-public-methods
|
|
|
86
124
|
Raises
|
|
87
125
|
------
|
|
88
126
|
ValueError
|
|
89
|
-
If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
127
|
+
If the length parameter is a boolean, less than 1, or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
128
|
+
|
|
129
|
+
Warns
|
|
130
|
+
-----
|
|
131
|
+
DeprecationWarning
|
|
132
|
+
If the length parameter is outside `MIN_LENGTH`..`BIG_LENGTH` (2..32 characters).
|
|
90
133
|
"""
|
|
134
|
+
if length is not None:
|
|
135
|
+
_validate_length(length)
|
|
91
136
|
resolved_length: int = self._length if length is None else length
|
|
92
|
-
if isinstance(resolved_length, bool) or resolved_length < 1:
|
|
93
|
-
msg = "Length must be at least 1 character."
|
|
94
|
-
raise ValueError(msg)
|
|
95
|
-
if resolved_length > MAXIMUM_LENGTH:
|
|
96
|
-
msg = "Length must never exceed 98 characters."
|
|
97
|
-
raise ValueError(msg)
|
|
98
137
|
|
|
99
138
|
first_letter: str = utils.create_letter(random_generator=self._random)
|
|
100
139
|
|
|
@@ -145,6 +184,7 @@ def cuid_wrapper(length: int = DEFAULT_LENGTH) -> Callable[[], str]:
|
|
|
145
184
|
The length parameter is an integer that determines the maximum length of the generated string.
|
|
146
185
|
It has a default value of DEFAULT_LENGTH (24).
|
|
147
186
|
A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
|
|
187
|
+
Lengths outside `MIN_LENGTH`..`BIG_LENGTH` (2..32) emit a DeprecationWarning.
|
|
148
188
|
|
|
149
189
|
Returns
|
|
150
190
|
-------
|
|
@@ -154,7 +194,12 @@ def cuid_wrapper(length: int = DEFAULT_LENGTH) -> Callable[[], str]:
|
|
|
154
194
|
Raises
|
|
155
195
|
------
|
|
156
196
|
ValueError
|
|
157
|
-
If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
197
|
+
If the length parameter is a boolean, less than 1, or greater than `MAXIMUM_LENGTH` (98 characters).
|
|
198
|
+
|
|
199
|
+
Warns
|
|
200
|
+
-----
|
|
201
|
+
DeprecationWarning
|
|
202
|
+
If the length parameter is outside `MIN_LENGTH`..`BIG_LENGTH` (2..32 characters).
|
|
158
203
|
"""
|
|
159
204
|
cuid_generator: Cuid = Cuid(length=length)
|
|
160
205
|
|
|
@@ -3,19 +3,22 @@ from __future__ import annotations
|
|
|
3
3
|
import os
|
|
4
4
|
import socket
|
|
5
5
|
import string
|
|
6
|
+
from hashlib import sha3_512
|
|
6
7
|
from math import floor
|
|
7
8
|
from typing import TYPE_CHECKING, Callable, Final, Optional
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# ~22k hosts before 50% chance of initial counter collision
|
|
11
|
+
# with a remaining counter range of 9.0e+15 in JavaScript.
|
|
12
|
+
INITIAL_COUNT_MAX: Final[int] = 476782367
|
|
13
|
+
MIN_LENGTH: Final[int] = 2
|
|
14
|
+
DEFAULT_LENGTH: Final[int] = 24
|
|
15
|
+
BIG_LENGTH: Final[int] = 32
|
|
16
|
+
MAXIMUM_LENGTH: Final[int] = 98
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
BIG_LENGTH: Final = 32
|
|
18
|
+
ASCII_LOWERCASE: Final[str] = string.ascii_lowercase
|
|
19
|
+
ASCII_LOWERCASE_LEN: Final[int] = len(ASCII_LOWERCASE)
|
|
20
|
+
BASE36_ALPHABET: Final[str] = string.digits + string.ascii_lowercase
|
|
21
|
+
BASE36_RADIX: Final[int] = len(BASE36_ALPHABET)
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
if TYPE_CHECKING:
|
|
@@ -103,16 +106,11 @@ def create_entropy(random_generator: Random, length: int = 4) -> str:
|
|
|
103
106
|
msg = "Cannot create entropy without a length >= 1."
|
|
104
107
|
raise ValueError(msg)
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
while len(entropy) < length:
|
|
109
|
-
entropy += base36_encode(floor(random_generator.random() * 36))
|
|
110
|
-
|
|
111
|
-
return entropy
|
|
109
|
+
return "".join([BASE36_ALPHABET[floor(random_generator.random() * BASE36_RADIX)] for _ in range(length)])
|
|
112
110
|
|
|
113
111
|
|
|
114
112
|
def create_hash(data: str = "") -> str:
|
|
115
|
-
"""Creates a hash value for a given string using the
|
|
113
|
+
"""Creates a hash value for a given string using the SHA3-512 algorithm and returns
|
|
116
114
|
it in base36 encoding format after dropping the first character.
|
|
117
115
|
|
|
118
116
|
Parameters
|
|
@@ -124,10 +122,10 @@ def create_hash(data: str = "") -> str:
|
|
|
124
122
|
Returns
|
|
125
123
|
-------
|
|
126
124
|
str
|
|
127
|
-
Base36 encoding of the
|
|
125
|
+
Base36 encoding of the SHA3-512 hash of the input string `data`, with the first character dropped.
|
|
128
126
|
|
|
129
127
|
"""
|
|
130
|
-
hashed_value: _Hash =
|
|
128
|
+
hashed_value: _Hash = sha3_512(data.encode())
|
|
131
129
|
hashed_int: int = int.from_bytes(hashed_value.digest(), byteorder="big")
|
|
132
130
|
|
|
133
131
|
# Drop the first character because it will bias the histogram to the left.
|
|
@@ -147,8 +145,7 @@ def create_letter(random_generator: Random) -> str:
|
|
|
147
145
|
str
|
|
148
146
|
a randomly generated lowercase letter from the English alphabet.
|
|
149
147
|
"""
|
|
150
|
-
|
|
151
|
-
return alphabet[floor(random_generator.random() * len(alphabet))]
|
|
148
|
+
return ASCII_LOWERCASE[floor(random_generator.random() * ASCII_LOWERCASE_LEN)]
|
|
152
149
|
|
|
153
150
|
|
|
154
151
|
def base36_encode(number: int) -> str:
|
|
@@ -175,12 +172,13 @@ def base36_encode(number: int) -> str:
|
|
|
175
172
|
msg = "Cannot encode negative integers."
|
|
176
173
|
raise ValueError(msg)
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
alphabet_length: int = len(alphabet)
|
|
175
|
+
if number == 0:
|
|
176
|
+
return "0"
|
|
181
177
|
|
|
178
|
+
encoded_chars: list[str] = []
|
|
182
179
|
while number != 0:
|
|
183
|
-
number, mod = divmod(number,
|
|
184
|
-
|
|
180
|
+
number, mod = divmod(number, BASE36_RADIX)
|
|
181
|
+
encoded_chars.append(BASE36_ALPHABET[mod])
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
encoded_chars.reverse()
|
|
184
|
+
return "".join(encoded_chars)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|