cuid2 2.0.0__tar.gz → 2.0.2__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.
@@ -1,48 +1,32 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cuid2
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance.
5
- Keywords: crypt security uuid guid cuid cryptography
6
- Author-Email: Will Gordon <wgordon@redhat.com>
7
- License: The MIT License (MIT)
8
-
9
- Copyright (c) 2023 Gordon Code
10
-
11
- Permission is hereby granted, free of charge, to any person obtaining a copy
12
- of this software and associated documentation files (the "Software"), to deal
13
- in the Software without restriction, including without limitation the rights
14
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
- copies of the Software, and to permit persons to whom the Software is
16
- furnished to do so, subject to the following conditions:
17
-
18
- The above copyright notice and this permission notice shall be included in all
19
- copies or substantial portions of the Software.
20
-
21
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
- SOFTWARE.
5
+ Keywords: crypt,security,uuid,guid,cuid,cryptography
6
+ Author: Will Gordon
7
+ Author-email: Will Gordon <dev@gordoncode.dev>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
28
10
  Classifier: Development Status :: 5 - Production/Stable
29
11
  Classifier: Intended Audience :: Developers
30
- Classifier: License :: OSI Approved :: MIT License
31
12
  Classifier: Operating System :: OS Independent
32
13
  Classifier: Programming Language :: Python :: 3
33
- Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
34
19
  Classifier: Topic :: Security :: Cryptography
35
20
  Classifier: Typing :: Typed
36
- Project-URL: Repository, https://github.com/gordon-code/cuid2/
37
- Requires-Python: >=3.8
21
+ Requires-Python: >=3.10
22
+ Project-URL: repository, https://github.com/gordon-code/cuid2/
38
23
  Description-Content-Type: text/markdown
39
24
 
40
25
  # cuid2
41
26
 
42
- [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://img.shields.io/badge/python-3.8+-blue.svg)
43
- [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.python.org/pypi/cuid2)
44
- [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.python.org/pypi/cuid2)
45
- [![cuid2](https://snyk.io/advisor/python/cuid2/badge.svg)](https://snyk.io/advisor/python/cuid2)
27
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/cuid2/)
28
+ [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.org/project/cuid2/)
29
+ [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.org/project/cuid2/)
46
30
 
47
31
  CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
48
32
  horizontal scaling and performance.
@@ -79,21 +63,24 @@ pip install cuid2
79
63
 
80
64
  ## Usage
81
65
 
82
- You can generate CUIDs directly in the terminal with the following:
66
+ You can generate CUIDs directly in the terminal with the following (optionally specifying a custom length):
83
67
  ```bash
84
68
  $ cuid2
69
+ $ cuid2 10
85
70
  ```
86
71
 
87
- Or you can rely on a CUID wrapper if you don't need any customizations:
72
+ Or you can rely on a CUID wrapper if you only need default or custom-length generation:
88
73
  ```python
89
74
  from typing import Callable
90
75
  from cuid2 import cuid_wrapper
91
76
 
92
77
  cuid_generator: Callable[[], str] = cuid_wrapper()
78
+ short_cuid_generator: Callable[[], str] = cuid_wrapper(length=10)
79
+
93
80
 
94
81
  def main():
95
- my_cuid: str = cuid_generator()
96
- next_cuid: str = cuid_generator()
82
+ my_cuid: str = cuid_generator()
83
+ short_cuid: str = short_cuid_generator()
97
84
  ```
98
85
 
99
86
  Finally, for more explicit control of the CUID generator, you can instantiate the class directly:
@@ -102,7 +89,14 @@ from cuid2 import Cuid
102
89
 
103
90
  CUID_GENERATOR: Cuid = Cuid(length=10)
104
91
 
92
+
105
93
  def main():
106
- my_cuid: str = CUID_GENERATOR.generate()
107
- next_cuid: str = CUID_GENERATOR.generate()
94
+ my_cuid: str = CUID_GENERATOR.generate()
95
+ next_cuid: str = CUID_GENERATOR.generate()
96
+
97
+ # Validate CUID
98
+ print(Cuid.is_cuid(my_cuid)) # True
108
99
  ```
100
+
101
+ `Cuid.is_cuid` also accepts optional `min_length` and `max_length` arguments to tighten the allowed length.
102
+ The bounds are clamped to 2 and 98 characters, so they can narrow but never widen what is considered valid.
@@ -1,9 +1,8 @@
1
1
  # cuid2
2
2
 
3
- [![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://img.shields.io/badge/python-3.8+-blue.svg)
4
- [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.python.org/pypi/cuid2)
5
- [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.python.org/pypi/cuid2)
6
- [![cuid2](https://snyk.io/advisor/python/cuid2/badge.svg)](https://snyk.io/advisor/python/cuid2)
3
+ [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/cuid2/)
4
+ [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.org/project/cuid2/)
5
+ [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.org/project/cuid2/)
7
6
 
8
7
  CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
9
8
  horizontal scaling and performance.
@@ -40,21 +39,24 @@ pip install cuid2
40
39
 
41
40
  ## Usage
42
41
 
43
- You can generate CUIDs directly in the terminal with the following:
42
+ You can generate CUIDs directly in the terminal with the following (optionally specifying a custom length):
44
43
  ```bash
45
44
  $ cuid2
45
+ $ cuid2 10
46
46
  ```
47
47
 
48
- Or you can rely on a CUID wrapper if you don't need any customizations:
48
+ Or you can rely on a CUID wrapper if you only need default or custom-length generation:
49
49
  ```python
50
50
  from typing import Callable
51
51
  from cuid2 import cuid_wrapper
52
52
 
53
53
  cuid_generator: Callable[[], str] = cuid_wrapper()
54
+ short_cuid_generator: Callable[[], str] = cuid_wrapper(length=10)
55
+
54
56
 
55
57
  def main():
56
- my_cuid: str = cuid_generator()
57
- next_cuid: str = cuid_generator()
58
+ my_cuid: str = cuid_generator()
59
+ short_cuid: str = short_cuid_generator()
58
60
  ```
59
61
 
60
62
  Finally, for more explicit control of the CUID generator, you can instantiate the class directly:
@@ -63,7 +65,14 @@ from cuid2 import Cuid
63
65
 
64
66
  CUID_GENERATOR: Cuid = Cuid(length=10)
65
67
 
68
+
66
69
  def main():
67
- my_cuid: str = CUID_GENERATOR.generate()
68
- next_cuid: str = CUID_GENERATOR.generate()
70
+ my_cuid: str = CUID_GENERATOR.generate()
71
+ next_cuid: str = CUID_GENERATOR.generate()
72
+
73
+ # Validate CUID
74
+ print(Cuid.is_cuid(my_cuid)) # True
69
75
  ```
76
+
77
+ `Cuid.is_cuid` also accepts optional `min_length` and `max_length` arguments to tighten the allowed length.
78
+ The bounds are clamped to 2 and 98 characters, so they can narrow but never widen what is considered valid.
@@ -0,0 +1,212 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.7.0,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "cuid2"
7
+ version = "2.0.2"
8
+ description = "Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Security :: Cryptography",
24
+ "Typing :: Typed",
25
+ ]
26
+ keywords = [
27
+ "crypt",
28
+ "security",
29
+ "uuid",
30
+ "guid",
31
+ "cuid",
32
+ "cryptography",
33
+ ]
34
+
35
+ [[project.authors]]
36
+ name = "Will Gordon"
37
+ email = "dev@gordoncode.dev"
38
+
39
+ [project.urls]
40
+ repository = "https://github.com/gordon-code/cuid2/"
41
+
42
+ [project.scripts]
43
+ cuid2 = "cuid2.cli:main"
44
+
45
+ [tool.uv]
46
+ default-groups = [
47
+ "lint",
48
+ "test",
49
+ "tox",
50
+ "typing",
51
+ ]
52
+
53
+ [tool.tox]
54
+ legacy_tox_ini = """
55
+ [tox]
56
+ min_version = 4
57
+ env_list = py3{10,11,12,13,14}, check
58
+ work_dir = local/.tox
59
+ isolated_build = True
60
+
61
+ [testenv]
62
+ runner = uv-venv-lock-runner
63
+ description = run unit tests
64
+ dependency_groups = test
65
+ commands = pytest local/tests
66
+
67
+ [testenv:check]
68
+ runner = uv-venv-lock-runner
69
+ description = run linters and typing
70
+ package = skip
71
+ dependency_groups =
72
+ lint
73
+ typing
74
+ test
75
+ commands =
76
+ ruff check src/ local/tests/
77
+ ruff format --check src/ local/tests/
78
+ codespell src/ local/tests/
79
+ pylint src/ local/tests/
80
+ mypy src/ local/tests/
81
+ """
82
+
83
+ [tool.ruff]
84
+ line-length = 120
85
+ src = ["src"]
86
+ target-version = "py310"
87
+ cache-dir = "local/.ruff_cache"
88
+ force-exclude = true
89
+
90
+ [tool.ruff.lint]
91
+ select = [
92
+ "F",
93
+ "E",
94
+ "W",
95
+ "C90",
96
+ "I",
97
+ "N",
98
+ "ASYNC",
99
+ "S",
100
+ "BLE",
101
+ "B",
102
+ "A",
103
+ "COM",
104
+ "C4",
105
+ "DTZ",
106
+ "T10",
107
+ "EM",
108
+ "FA",
109
+ "ISC",
110
+ "ICN",
111
+ "G",
112
+ "INP",
113
+ "PIE",
114
+ "T20",
115
+ "PT",
116
+ "Q",
117
+ "RSE",
118
+ "RET",
119
+ "SLF",
120
+ "SLOT",
121
+ "SIM",
122
+ "INT",
123
+ "ARG",
124
+ "PTH",
125
+ "PGH",
126
+ "PL",
127
+ "TRY",
128
+ "FLY",
129
+ "PERF",
130
+ "LOG",
131
+ "RUF",
132
+ ]
133
+ ignore = [
134
+ "D100",
135
+ "D101",
136
+ "D102",
137
+ "D104",
138
+ "D205",
139
+ "D401",
140
+ "COM812",
141
+ "ISC001",
142
+ ]
143
+ unfixable = [
144
+ "T201",
145
+ "F401",
146
+ "F841",
147
+ ]
148
+
149
+ [tool.ruff.lint.per-file-ignores]
150
+ "test_*.py" = [
151
+ "S101",
152
+ "PLR2004",
153
+ "SLF001",
154
+ ]
155
+
156
+ [tool.ruff.lint.isort]
157
+ known-first-party = ["src"]
158
+ section-order = [
159
+ "future",
160
+ "standard-library",
161
+ "third-party",
162
+ "first-party",
163
+ "local-folder",
164
+ ]
165
+
166
+ [tool.ruff.lint.pydocstyle]
167
+ convention = "numpy"
168
+
169
+ [tool.pytest.ini_options]
170
+ minversion = "8.1"
171
+ cache_dir = "local/.pytest_cache"
172
+ python_files = "test_*.py"
173
+ testpaths = ["local/tests"]
174
+
175
+ [tool.pylint.master]
176
+ ignore-patterns = "test_.*.py"
177
+
178
+ [tool.pylint.format]
179
+ max-line-length = 120
180
+
181
+ [tool.pylint.messages_control]
182
+ disable = [
183
+ "missing-module-docstring",
184
+ "missing-class-docstring",
185
+ ]
186
+
187
+ [tool.pylint.design]
188
+ max-parents = 13
189
+ max-args = 10
190
+
191
+ [tool.pylint.string]
192
+ check-quote-consistency = "yes"
193
+
194
+ [tool.mypy]
195
+ cache_dir = "local/.mypy_cache"
196
+
197
+ [dependency-groups]
198
+ lint = [
199
+ "codespell==2.4.3",
200
+ "pylint==4.1.2",
201
+ "ruff==0.16.10",
202
+ ]
203
+ test = [
204
+ "pytest==9.1.1",
205
+ "pytest-mock==3.16.0",
206
+ "pytest-sugar==1.1.1",
207
+ ]
208
+ tox = [
209
+ "tox==4.64.8",
210
+ "tox-uv==1.36.1",
211
+ ]
212
+ typing = ["mypy==2.4.0"]
@@ -0,0 +1,157 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.7.0,<0.14.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "cuid2"
7
+ version = "2.0.2"
8
+ authors = [
9
+ {name = "Will Gordon", email = "dev@gordoncode.dev"},
10
+ ]
11
+ description = "Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."
12
+ readme = "README.md"
13
+ requires-python = ">=3.10"
14
+ license = "MIT"
15
+ license-files = ["LICENSE"]
16
+ classifiers = [
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Topic :: Security :: Cryptography",
27
+ "Typing :: Typed",
28
+ ]
29
+ keywords = ['crypt', 'security', 'uuid', 'guid', 'cuid', 'cryptography']
30
+
31
+ [project.urls]
32
+ repository = "https://github.com/gordon-code/cuid2/"
33
+
34
+ [project.scripts]
35
+ cuid2 = "cuid2.cli:main"
36
+
37
+ [tool.uv]
38
+ default-groups = ["lint", "test", "tox", "typing"]
39
+
40
+ [dependency-groups]
41
+ lint = [
42
+ "codespell==2.4.3",
43
+ "pylint==4.1.2",
44
+ "ruff==0.16.10",
45
+ ]
46
+ test = [
47
+ "pytest==9.1.1",
48
+ "pytest-mock==3.16.0",
49
+ "pytest-sugar==1.1.1",
50
+ ]
51
+ tox = [
52
+ "tox==4.64.8",
53
+ "tox-uv==1.36.1",
54
+ ]
55
+ typing = [
56
+ "mypy==2.4.0",
57
+ ]
58
+
59
+ [tool.tox]
60
+ legacy_tox_ini = """
61
+ [tox]
62
+ min_version = 4
63
+ env_list = py3{10,11,12,13,14}, check
64
+ work_dir = local/.tox
65
+ isolated_build = True
66
+
67
+ [testenv]
68
+ runner = uv-venv-lock-runner
69
+ description = run unit tests
70
+ dependency_groups = test
71
+ commands = pytest local/tests
72
+
73
+ [testenv:check]
74
+ runner = uv-venv-lock-runner
75
+ description = run linters and typing
76
+ package = skip
77
+ dependency_groups =
78
+ lint
79
+ typing
80
+ test
81
+ commands =
82
+ ruff check src/ local/tests/
83
+ ruff format --check src/ local/tests/
84
+ codespell src/ local/tests/
85
+ pylint src/ local/tests/
86
+ mypy src/ local/tests/
87
+ """
88
+
89
+ [tool.ruff]
90
+ line-length = 120
91
+ src = ["src"]
92
+ target-version = "py310"
93
+ cache-dir = "local/.ruff_cache"
94
+ force-exclude = true
95
+
96
+ [tool.ruff.lint]
97
+ # https://docs.astral.sh/ruff/rules/
98
+ select = [
99
+ "F", "E", "W", "C90", "I", "N", "ASYNC",
100
+ "S", "BLE", "B", "A", "COM", "C4",
101
+ "DTZ", "T10", "EM", "FA", "ISC", "ICN",
102
+ "G", "INP", "PIE", "T20", "PT", "Q", "RSE",
103
+ "RET", "SLF", "SLOT", "SIM", "INT", "ARG",
104
+ "PTH", "PGH", "PL", "TRY", "FLY", "PERF",
105
+ "LOG", "RUF",
106
+ ] # purposely not including "DJ", "YTT", "EXE", "PD", "NPY", "FBT" (boolean checking)
107
+ ignore = [
108
+ "D100", # Missing docstring in public module
109
+ "D101", # Missing docstring in public class
110
+ "D102", # Missing docstring in public method
111
+ "D104", # Missing docstring in public package
112
+ "D205", # Blank line required between summary line and description
113
+ "D401", # First line should be in imperative mood
114
+ "COM812", "ISC001", # Conflict with formatter
115
+ ]
116
+ # Don't automatically remove `print`
117
+ # Stop automatically removing unused imports
118
+ unfixable = ["T201", "F401", "F841"]
119
+
120
+ [tool.ruff.lint.per-file-ignores]
121
+ # Ignore `assert` in test files (S101), magic values (PLR2004), and private member accessed (SLF001)
122
+ "test_*.py" = ["S101", "PLR2004", "SLF001"]
123
+
124
+ [tool.ruff.lint.isort]
125
+ known-first-party = ["src"]
126
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
127
+
128
+ [tool.ruff.lint.pydocstyle]
129
+ convention = "numpy"
130
+
131
+ [tool.pytest.ini_options]
132
+ minversion = "8.1"
133
+ cache_dir = "local/.pytest_cache"
134
+ python_files = "test_*.py"
135
+ testpaths = ["local/tests"]
136
+
137
+ [tool.pylint.master]
138
+ ignore-patterns = "test_.*.py"
139
+
140
+ [tool.pylint.format]
141
+ max-line-length = 120
142
+
143
+ [tool.pylint.messages_control]
144
+ disable = [
145
+ "missing-module-docstring",
146
+ "missing-class-docstring",
147
+ ]
148
+
149
+ [tool.pylint.design]
150
+ max-parents = 13
151
+ max-args = 10
152
+
153
+ [tool.pylint.string]
154
+ check-quote-consistency = "yes"
155
+
156
+ [tool.mypy]
157
+ cache_dir = "local/.mypy_cache"
@@ -1,4 +1,5 @@
1
1
  """Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."""
2
+
2
3
  from .generator import DEFAULT_LENGTH, INITIAL_COUNT_MAX, Cuid, cuid_wrapper
3
4
 
4
- __all__ = ["Cuid", "DEFAULT_LENGTH", "INITIAL_COUNT_MAX", "cuid_wrapper"]
5
+ __all__ = ["DEFAULT_LENGTH", "INITIAL_COUNT_MAX", "Cuid", "cuid_wrapper"]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from argparse import ArgumentParser
4
+ from typing import TYPE_CHECKING, Optional
5
+
6
+ from cuid2.generator import DEFAULT_LENGTH, cuid_wrapper
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Sequence
10
+
11
+
12
+ def main(args: Optional[Sequence[str]] = None) -> None:
13
+ """Print out a CUID generated string. Used by the CLI console script."""
14
+ arguments_parser = ArgumentParser()
15
+ arguments_parser.add_argument(
16
+ nargs="?",
17
+ dest="length",
18
+ help="Length of generated cuid2",
19
+ type=int,
20
+ default=DEFAULT_LENGTH,
21
+ )
22
+ cli_arguments = arguments_parser.parse_args(args)
23
+
24
+ print(cuid_wrapper(cli_arguments.length)()) # noqa: T201 (print statement)
@@ -1,3 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ import re
1
4
  import time
2
5
  from math import floor
3
6
  from secrets import SystemRandom
@@ -9,8 +12,7 @@ if TYPE_CHECKING:
9
12
  from _random import Random
10
13
 
11
14
  class FingerprintCallable(Protocol): # pylint: disable=too-few-public-methods
12
- def __call__(self: "FingerprintCallable", random_generator: Random) -> str:
13
- ...
15
+ def __call__(self: FingerprintCallable, random_generator: Random) -> str: ...
14
16
 
15
17
 
16
18
  # ~22k hosts before 50% chance of initial counter collision
@@ -18,15 +20,16 @@ if TYPE_CHECKING:
18
20
  INITIAL_COUNT_MAX: Final[int] = 476782367
19
21
  DEFAULT_LENGTH: Final = 24
20
22
  MAXIMUM_LENGTH: Final = 98
23
+ _CUID_REGEX: Final[re.Pattern[str]] = re.compile(r"^[a-z][0-9a-z]+\Z")
21
24
 
22
25
 
23
26
  class Cuid: # pylint: disable=too-few-public-methods
24
27
  def __init__(
25
- self: "Cuid",
26
- random_generator: Callable[[], "Random"] = SystemRandom,
28
+ self: Cuid,
29
+ random_generator: Callable[[], Random] = SystemRandom,
27
30
  counter: Callable[[int], Callable[[], int]] = utils.create_counter,
28
31
  length: int = DEFAULT_LENGTH,
29
- fingerprint: "FingerprintCallable" = utils.create_fingerprint,
32
+ fingerprint: FingerprintCallable = utils.create_fingerprint,
30
33
  ) -> None:
31
34
  """Initialization function for the Cuid class that generates a universally unique,
32
35
  base36 encoded string.
@@ -39,28 +42,31 @@ class Cuid: # pylint: disable=too-few-public-methods
39
42
  counter : Callable[[int], Callable[[], int]], default=utils.create_counter
40
43
  The `counter` parameter is a callable that creates a counter returning an incremented value each time it
41
44
  is called. The `create_counter` function from the `utils` module is used by default.
42
- length : int, default=DEFAULT_LENGTH (4)
45
+ length : int, default=DEFAULT_LENGTH (24)
43
46
  The length parameter is an integer that determines the maximum length of the generated string.
44
- It has a default value of DEFAULT_LENGTH (4).
45
- A length value greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
47
+ It has a default value of DEFAULT_LENGTH (24).
48
+ A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
46
49
  fingerprint : "FingerprintCallable", default=utils.create_fingerprint
47
50
  The "fingerprint" parameter is a callable function that generates a unique identifier.
48
51
 
49
52
  Raises
50
53
  ------
51
54
  ValueError
52
- If the length parameter is greater than `MAXIMUM_LENGTH` (98 characters).
55
+ If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
53
56
  """
57
+ if isinstance(length, bool) or length < 1:
58
+ msg = "Length must be at least 1 character."
59
+ raise ValueError(msg)
54
60
  if length > MAXIMUM_LENGTH:
55
61
  msg = "Length must never exceed 98 characters."
56
62
  raise ValueError(msg)
57
63
 
58
- self._random: "Random" = random_generator()
64
+ self._random: Random = random_generator()
59
65
  self._counter: Callable[[], int] = counter(floor(self._random.random() * INITIAL_COUNT_MAX))
60
66
  self._length: int = length
61
67
  self._fingerprint: str = fingerprint(random_generator=self._random)
62
68
 
63
- def generate(self: "Cuid", length: Optional[int] = None) -> str:
69
+ def generate(self: Cuid, length: Optional[int] = None) -> str:
64
70
  """Generates a universally unique, base36 encoded string with a specified length.
65
71
 
66
72
  Parameters
@@ -68,7 +74,7 @@ class Cuid: # pylint: disable=too-few-public-methods
68
74
  length : int, optional
69
75
  The length parameter is an optional integer value that specifies the length of the generated string.
70
76
  If it is not provided, the default length value provided during class initialization is used.
71
- A length value greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
77
+ A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
72
78
 
73
79
  Returns
74
80
  -------
@@ -80,10 +86,13 @@ class Cuid: # pylint: disable=too-few-public-methods
80
86
  Raises
81
87
  ------
82
88
  ValueError
83
- If the length parameter is greater than `MAXIMUM_LENGTH` (98 characters).
89
+ If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
84
90
  """
85
- length = length or self._length
86
- if length > MAXIMUM_LENGTH:
91
+ 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:
87
96
  msg = "Length must never exceed 98 characters."
88
97
  raise ValueError(msg)
89
98
 
@@ -92,21 +101,62 @@ class Cuid: # pylint: disable=too-few-public-methods
92
101
  base36_time: str = utils.base36_encode(time.time_ns())
93
102
  base36_count: str = utils.base36_encode(self._counter())
94
103
 
95
- salt: str = utils.create_entropy(length=length, random_generator=self._random)
104
+ salt: str = utils.create_entropy(length=resolved_length, random_generator=self._random)
96
105
  hash_input: str = base36_time + salt + base36_count + self._fingerprint
97
106
 
98
- return first_letter + utils.create_hash(hash_input)[1 : length or self._length]
107
+ return first_letter + utils.create_hash(hash_input)[1:resolved_length]
108
+
109
+ @staticmethod
110
+ def is_cuid(cuid_str: str, min_length: int = 2, max_length: int = MAXIMUM_LENGTH) -> bool:
111
+ """Validates whether a given string is a valid CUID2 identifier.
112
+
113
+ Parameters
114
+ ----------
115
+ cuid_str : str
116
+ The string to validate as a CUID2 identifier.
117
+ min_length : int, default=2
118
+ The minimum allowed length of the identifier. Values below 2 have no effect, since a valid
119
+ identifier always has a leading letter followed by at least one more character.
120
+ max_length : int, default=MAXIMUM_LENGTH (98)
121
+ The maximum allowed length of the identifier. Values above `MAXIMUM_LENGTH` are clamped to it.
122
+
123
+ Returns
124
+ -------
125
+ bool
126
+ True if `cuid_str` is a string starting with a lowercase ASCII letter followed by
127
+ lowercase base36 characters and has length between `min_length` and `max_length`
128
+ inclusive; False otherwise. The length bounds can only narrow the range of valid
129
+ lengths (2 to `MAXIMUM_LENGTH`), never widen it.
130
+ """
131
+ max_length = min(max_length, MAXIMUM_LENGTH)
132
+ return (
133
+ isinstance(cuid_str, str)
134
+ and min_length <= len(cuid_str) <= max_length
135
+ and bool(_CUID_REGEX.match(cuid_str))
136
+ )
99
137
 
100
138
 
101
- def cuid_wrapper() -> Callable[[], str]:
139
+ def cuid_wrapper(length: int = DEFAULT_LENGTH) -> Callable[[], str]:
102
140
  """Wrap a single Cuid class instance and return a callable that generates a CUID string.
103
141
 
142
+ Parameters
143
+ ----------
144
+ length : int, default=DEFAULT_LENGTH (24)
145
+ The length parameter is an integer that determines the maximum length of the generated string.
146
+ It has a default value of DEFAULT_LENGTH (24).
147
+ A length value less than 1 or greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
148
+
104
149
  Returns
105
150
  -------
106
151
  Callable[[], str]
107
152
  A callable that generates a CUID string.
153
+
154
+ Raises
155
+ ------
156
+ ValueError
157
+ If the length parameter is less than 1 or greater than `MAXIMUM_LENGTH` (98 characters).
108
158
  """
109
- cuid_generator: Cuid = Cuid()
159
+ cuid_generator: Cuid = Cuid(length=length)
110
160
 
111
161
  def cuid() -> str:
112
162
  return cuid_generator.generate()
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import socket
3
5
  import string
@@ -17,9 +19,8 @@ BIG_LENGTH: Final = 32
17
19
 
18
20
 
19
21
  if TYPE_CHECKING:
20
- from hashlib import _Hash
21
-
22
22
  from _random import Random
23
+ from hashlib import _Hash
23
24
 
24
25
 
25
26
  def create_counter(count: int) -> Callable[[], int]:
@@ -46,7 +47,7 @@ def create_counter(count: int) -> Callable[[], int]:
46
47
  return counter
47
48
 
48
49
 
49
- def create_fingerprint(random_generator: "Random", fingerprint_data: Optional[str] = "") -> str:
50
+ def create_fingerprint(random_generator: Random, fingerprint_data: Optional[str] = "") -> str:
50
51
  """Creates a fingerprint, by default combining process ID, hostname, and environment variables
51
52
  with entropy and then hashing the result.
52
53
 
@@ -76,7 +77,7 @@ def create_fingerprint(random_generator: "Random", fingerprint_data: Optional[st
76
77
  return create_hash(fingerprint)[0:BIG_LENGTH]
77
78
 
78
79
 
79
- def create_entropy(random_generator: "Random", length: int = 4) -> str:
80
+ def create_entropy(random_generator: Random, length: int = 4) -> str:
80
81
  """Creates a random string of specified length using a base36 encoding.
81
82
 
82
83
  Parameters
@@ -126,14 +127,14 @@ def create_hash(data: str = "") -> str:
126
127
  Base36 encoding of the SHA-512 hash of the input string `data`, with the first character dropped.
127
128
 
128
129
  """
129
- hashed_value: "_Hash" = sha512(data.encode())
130
+ hashed_value: _Hash = sha512(data.encode())
130
131
  hashed_int: int = int.from_bytes(hashed_value.digest(), byteorder="big")
131
132
 
132
133
  # Drop the first character because it will bias the histogram to the left.
133
134
  return base36_encode(hashed_int)[1:]
134
135
 
135
136
 
136
- def create_letter(random_generator: "Random") -> str:
137
+ def create_letter(random_generator: Random) -> str:
137
138
  """Generates a random lowercase letter using a given random number generator.
138
139
 
139
140
  Parameters
@@ -1,213 +0,0 @@
1
- [build-system]
2
- requires = [
3
- "pdm-backend",
4
- ]
5
- build-backend = "pdm.backend"
6
-
7
- [project]
8
- name = "cuid2"
9
- dynamic = []
10
- authors = [
11
- { name = "Will Gordon", email = "wgordon@redhat.com" },
12
- ]
13
- description = "Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."
14
- readme = "README.md"
15
- requires-python = ">=3.8"
16
- classifiers = [
17
- "Development Status :: 5 - Production/Stable",
18
- "Intended Audience :: Developers",
19
- "License :: OSI Approved :: MIT License",
20
- "Operating System :: OS Independent",
21
- "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.8",
23
- "Topic :: Security :: Cryptography",
24
- "Typing :: Typed",
25
- ]
26
- keywords = [
27
- "crypt",
28
- "security",
29
- "uuid",
30
- "guid",
31
- "cuid",
32
- "cryptography",
33
- ]
34
- version = "2.0.0"
35
-
36
- [project.license]
37
- file = "LICENSE"
38
-
39
- [project.urls]
40
- repository = "https://github.com/gordon-code/cuid2/"
41
-
42
- [project.scripts]
43
- cuid2 = "cuid2.cli:main"
44
-
45
- [tool.pdm.version]
46
- source = "scm"
47
-
48
- [tool.pdm.scripts]
49
- black = "black {args:src/ local/tests/}"
50
- ruff = "ruff check --fix --exit-zero {args:src/ local/tests/}"
51
- spelling = "codespell {args:src/ local/tests/}"
52
- pylint = "pylint {args:src/ local/tests/}"
53
- safety = "safety check --bare"
54
- typing = "mypy {args:src/ local/tests/}"
55
- testing = "pytest local/tests"
56
- testing-slow = "pytest local/tests --runslow"
57
- tox = "tox --parallel auto"
58
-
59
- [tool.pdm.scripts.lint-fast]
60
- composite = [
61
- "black",
62
- "ruff",
63
- ]
64
-
65
- [tool.pdm.scripts.lint-full]
66
- composite = [
67
- "lint-fast",
68
- "spelling",
69
- "pylint",
70
- "typing",
71
- "safety",
72
- ]
73
-
74
- [tool.pdm.dev-dependencies]
75
- lint = [
76
- "black~=23.3.0",
77
- "codespell~=2.2.4",
78
- "pylint~=2.17.4",
79
- "ruff~=0.0.269",
80
- "safety==2.4.0b1",
81
- ]
82
- test = [
83
- "pytest~=7.3.1",
84
- "pytest-mock~=3.10.0",
85
- "pytest-sugar~=0.9.7",
86
- ]
87
- tox = [
88
- "tox~=4.4.12",
89
- "tox-pdm~=0.6.1",
90
- ]
91
- typing = [
92
- "mypy~=1.3.0",
93
- ]
94
-
95
- [tool.tox]
96
- legacy_tox_ini = " [tox]\n min_version = 4\n env_list = py3{8,9,10,11}, check\n work_dir = local/.tox\n isolated_build = True\n\n [testenv]\n description = run unit tests\n groups = test\n commands = testing\n\n [testenv:check]\n description = run linters and typing\n skip_install = true\n groups = lint, typing, test\n commands = lint-full\n"
97
-
98
- [tool.black]
99
- line-length = 120
100
- target_version = [
101
- "py38",
102
- ]
103
-
104
- [tool.ruff]
105
- line-length = 120
106
- src = [
107
- "src",
108
- ]
109
- target-version = "py38"
110
- cache-dir = "local/.ruff_cache"
111
- extend-select = [
112
- "W",
113
- "C90",
114
- "I",
115
- "N",
116
- "UP",
117
- "S",
118
- "BLE",
119
- "B",
120
- "A",
121
- "COM",
122
- "C4",
123
- "DTZ",
124
- "T10",
125
- "EM",
126
- "ISC",
127
- "ICN",
128
- "G",
129
- "INP",
130
- "PIE",
131
- "T20",
132
- "PT",
133
- "Q",
134
- "RSE",
135
- "RET",
136
- "SLF",
137
- "SIM",
138
- "INT",
139
- "ARG",
140
- "PTH",
141
- "PGH",
142
- "PL",
143
- "TRY",
144
- "RUF",
145
- "D",
146
- "ANN",
147
- "PYI",
148
- "TCH",
149
- "ERA",
150
- ]
151
- extend-ignore = [
152
- "D100",
153
- "D101",
154
- "D102",
155
- "D104",
156
- "D205",
157
- "D401",
158
- ]
159
- unfixable = [
160
- "T201",
161
- "F401",
162
- "F841",
163
- ]
164
- force-exclude = true
165
-
166
- [tool.ruff.per-file-ignores]
167
- "test_*.py" = [
168
- "S101",
169
- "PLR2004",
170
- "SLF001",
171
- ]
172
-
173
- [tool.ruff.isort]
174
- known-first-party = [
175
- "src",
176
- ]
177
- section-order = [
178
- "future",
179
- "standard-library",
180
- "third-party",
181
- "first-party",
182
- "local-folder",
183
- ]
184
-
185
- [tool.ruff.pydocstyle]
186
- convention = "numpy"
187
-
188
- [tool.pytest.ini_options]
189
- minversion = "7.3"
190
- cache_dir = "local/.pytest_cache"
191
- python_files = "test_*.py"
192
-
193
- [tool.pylint.master]
194
- ignore-patterns = "test_.*.py"
195
-
196
- [tool.pylint.format]
197
- max-line-length = 120
198
-
199
- [tool.pylint.messages_control]
200
- disable = [
201
- "missing-module-docstring",
202
- "missing-class-docstring",
203
- ]
204
-
205
- [tool.pylint.design]
206
- max-parents = 13
207
- max-args = 10
208
-
209
- [tool.pylint.string]
210
- check-quote-consistency = "yes"
211
-
212
- [tool.mypy]
213
- cache_dir = "local/.mypy_cache"
@@ -1,8 +0,0 @@
1
- from cuid2.generator import cuid_wrapper
2
-
3
- generate_cuid = cuid_wrapper()
4
-
5
-
6
- def main() -> None:
7
- """Print out a CUID generated string. Used by the CLI console script."""
8
- print(generate_cuid()) # noqa: T201 (print statement)
File without changes
File without changes