cuid2 2.0.1__tar.gz → 2.0.3__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.1
3
+ Version: 2.0.3
4
4
  Summary: Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance.
5
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.
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.
@@ -1,25 +1,25 @@
1
1
  [build-system]
2
- requires = [
3
- "pdm-backend",
4
- ]
5
- build-backend = "pdm.backend"
2
+ requires = ["uv_build>=0.7.0,<0.14.0"]
3
+ build-backend = "uv_build"
6
4
 
7
5
  [project]
8
6
  name = "cuid2"
9
- dynamic = []
10
- authors = [
11
- { name = "Will Gordon", email = "wgordon@redhat.com" },
12
- ]
7
+ version = "2.0.3"
13
8
  description = "Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."
14
9
  readme = "README.md"
15
- requires-python = ">=3.8"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
16
13
  classifiers = [
17
14
  "Development Status :: 5 - Production/Stable",
18
15
  "Intended Audience :: Developers",
19
- "License :: OSI Approved :: MIT License",
20
16
  "Operating System :: OS Independent",
21
17
  "Programming Language :: Python :: 3",
22
- "Programming Language :: Python :: 3.8",
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
23
  "Topic :: Security :: Cryptography",
24
24
  "Typing :: Typed",
25
25
  ]
@@ -31,10 +31,10 @@ keywords = [
31
31
  "cuid",
32
32
  "cryptography",
33
33
  ]
34
- version = "2.0.1"
35
34
 
36
- [project.license]
37
- file = "LICENSE"
35
+ [[project.authors]]
36
+ name = "Will Gordon"
37
+ email = "dev@gordoncode.dev"
38
38
 
39
39
  [project.urls]
40
40
  repository = "https://github.com/gordon-code/cuid2/"
@@ -42,72 +42,48 @@ repository = "https://github.com/gordon-code/cuid2/"
42
42
  [project.scripts]
43
43
  cuid2 = "cuid2.cli:main"
44
44
 
45
- [tool.pdm.version]
46
- source = "scm"
47
-
48
- [tool.pdm.scripts]
49
- ruff-check = "ruff check --fix --exit-zero {args:src/ local/tests/}"
50
- ruff-format = "ruff format {args:src/ local/tests/}"
51
- spelling = "codespell {args:src/ local/tests/}"
52
- pylint = "pylint {args:src/ local/tests/}"
53
- safety = "safety {args:check --bare}"
54
- typing = "mypy {args:src/ local/tests/}"
55
- update-pip = "pip install --upgrade pip"
56
- testing = "pytest local/tests"
57
- testing-slow = "pytest local/tests --runslow"
58
- tox = "tox --parallel auto"
59
-
60
- [tool.pdm.scripts.ruff-fix]
61
- composite = [
62
- "ruff-check",
63
- "ruff-format",
64
- ]
65
-
66
- [tool.pdm.scripts.lint-full]
67
- composite = [
68
- "ruff-fix",
69
- "spelling",
70
- "pylint",
45
+ [tool.uv]
46
+ default-groups = [
47
+ "lint",
48
+ "test",
49
+ "tox",
71
50
  "typing",
72
- "safety",
73
- ]
74
-
75
- [tool.pdm.scripts.tox-full]
76
- composite = [
77
- "update-pip",
78
- "lint-full",
79
- ]
80
-
81
- [tool.pdm.dev-dependencies]
82
- lint = [
83
- "codespell~=2.2.6",
84
- "pylint~=3.1.0",
85
- "requests>=2.31.0",
86
- "ruff~=0.3.7",
87
- "safety==3.1.0",
88
- ]
89
- test = [
90
- "pytest~=8.1.1",
91
- "pytest-mock~=3.14.0",
92
- "pytest-sugar~=1.0.0",
93
- ]
94
- tox = [
95
- "tox~=4.14.2",
96
- "tox-pdm~=0.7.2",
97
- ]
98
- typing = [
99
- "mypy~=1.9.0",
100
51
  ]
101
52
 
102
53
  [tool.tox]
103
- legacy_tox_ini = " [tox]\n min_version = 4\n env_list = py3{8,9,10,11,12}, 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 = tox-full\n"
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
+ """
104
82
 
105
83
  [tool.ruff]
106
84
  line-length = 120
107
- src = [
108
- "src",
109
- ]
110
- target-version = "py38"
85
+ src = ["src"]
86
+ target-version = "py310"
111
87
  cache-dir = "local/.ruff_cache"
112
88
  force-exclude = true
113
89
 
@@ -120,7 +96,6 @@ select = [
120
96
  "I",
121
97
  "N",
122
98
  "ASYNC",
123
- "TRIO",
124
99
  "S",
125
100
  "BLE",
126
101
  "B",
@@ -179,9 +154,7 @@ unfixable = [
179
154
  ]
180
155
 
181
156
  [tool.ruff.lint.isort]
182
- known-first-party = [
183
- "src",
184
- ]
157
+ known-first-party = ["src"]
185
158
  section-order = [
186
159
  "future",
187
160
  "standard-library",
@@ -197,6 +170,7 @@ convention = "numpy"
197
170
  minversion = "8.1"
198
171
  cache_dir = "local/.pytest_cache"
199
172
  python_files = "test_*.py"
173
+ testpaths = ["local/tests"]
200
174
 
201
175
  [tool.pylint.master]
202
176
  ignore-patterns = "test_.*.py"
@@ -219,3 +193,20 @@ check-quote-consistency = "yes"
219
193
 
220
194
  [tool.mypy]
221
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.3"
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"
@@ -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
+ ]
@@ -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)
@@ -0,0 +1,209 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import re
5
+ import time
6
+ import warnings
7
+ from math import floor
8
+ from secrets import SystemRandom
9
+ from typing import TYPE_CHECKING, Callable, Final, Optional, Protocol
10
+
11
+ from cuid2 import utils
12
+ from cuid2.utils import BIG_LENGTH, DEFAULT_LENGTH, INITIAL_COUNT_MAX, MAXIMUM_LENGTH, MIN_LENGTH
13
+
14
+ if TYPE_CHECKING:
15
+ from _random import Random
16
+
17
+ class FingerprintCallable(Protocol): # pylint: disable=too-few-public-methods
18
+ def __call__(self: FingerprintCallable, random_generator: Random) -> str: ...
19
+
20
+
21
+ _CUID_REGEX: Final[re.Pattern[str]] = re.compile(r"^[a-z][0-9a-z]+\Z")
22
+
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
+
62
+ class Cuid: # pylint: disable=too-few-public-methods
63
+ def __init__(
64
+ self: Cuid,
65
+ random_generator: Callable[[], Random] = SystemRandom,
66
+ counter: Callable[[int], Callable[[], int]] = utils.create_counter,
67
+ length: int = DEFAULT_LENGTH,
68
+ fingerprint: FingerprintCallable = utils.create_fingerprint,
69
+ ) -> None:
70
+ """Initialization function for the Cuid class that generates a universally unique,
71
+ base36 encoded string.
72
+
73
+ Parameters
74
+ ----------
75
+ random_generator : Callable[[], "Random"], default=SystemRandom
76
+ Used as the base random generator. The default value is `secrets.SystemRandom`, which is a
77
+ cryptographically secure random number generator provided by the Python standard library.
78
+ counter : Callable[[int], Callable[[], int]], default=utils.create_counter
79
+ The `counter` parameter is a callable that creates a counter returning an incremented value each time it
80
+ is called. The `create_counter` function from the `utils` module is used by default.
81
+ length : int, default=DEFAULT_LENGTH (24)
82
+ The length parameter is an integer that determines the maximum length of the generated string.
83
+ It has a default value of DEFAULT_LENGTH (24).
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.
86
+ fingerprint : "FingerprintCallable", default=utils.create_fingerprint
87
+ The "fingerprint" parameter is a callable function that generates a unique identifier.
88
+
89
+ Raises
90
+ ------
91
+ ValueError
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).
98
+ """
99
+ _validate_length(length)
100
+
101
+ self._random: Random = random_generator()
102
+ self._counter: Callable[[], int] = counter(floor(self._random.random() * INITIAL_COUNT_MAX))
103
+ self._length: int = length
104
+ self._fingerprint: str = fingerprint(random_generator=self._random)
105
+
106
+ def generate(self: Cuid, length: Optional[int] = None) -> str:
107
+ """Generates a universally unique, base36 encoded string with a specified length.
108
+
109
+ Parameters
110
+ ----------
111
+ length : int, optional
112
+ The length parameter is an optional integer value that specifies the length of the generated string.
113
+ If it is not provided, the default length value provided during class initialization is used.
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.
116
+
117
+ Returns
118
+ -------
119
+ str
120
+ Starts with a single, random letter, followed by a hash. The hash is generated using a combination of
121
+ the current time in base 36, a random salt, a base 36 counter, and a system fingerprint.
122
+ The length of the returned string is limited by `length`.
123
+
124
+ Raises
125
+ ------
126
+ ValueError
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).
133
+ """
134
+ if length is not None:
135
+ _validate_length(length)
136
+ resolved_length: int = self._length if length is None else length
137
+
138
+ first_letter: str = utils.create_letter(random_generator=self._random)
139
+
140
+ base36_time: str = utils.base36_encode(time.time_ns())
141
+ base36_count: str = utils.base36_encode(self._counter())
142
+
143
+ salt: str = utils.create_entropy(length=resolved_length, random_generator=self._random)
144
+ hash_input: str = base36_time + salt + base36_count + self._fingerprint
145
+
146
+ return first_letter + utils.create_hash(hash_input)[1:resolved_length]
147
+
148
+ @staticmethod
149
+ def is_cuid(cuid_str: str, min_length: int = 2, max_length: int = MAXIMUM_LENGTH) -> bool:
150
+ """Validates whether a given string is a valid CUID2 identifier.
151
+
152
+ Parameters
153
+ ----------
154
+ cuid_str : str
155
+ The string to validate as a CUID2 identifier.
156
+ min_length : int, default=2
157
+ The minimum allowed length of the identifier. Values below 2 have no effect, since a valid
158
+ identifier always has a leading letter followed by at least one more character.
159
+ max_length : int, default=MAXIMUM_LENGTH (98)
160
+ The maximum allowed length of the identifier. Values above `MAXIMUM_LENGTH` are clamped to it.
161
+
162
+ Returns
163
+ -------
164
+ bool
165
+ True if `cuid_str` is a string starting with a lowercase ASCII letter followed by
166
+ lowercase base36 characters and has length between `min_length` and `max_length`
167
+ inclusive; False otherwise. The length bounds can only narrow the range of valid
168
+ lengths (2 to `MAXIMUM_LENGTH`), never widen it.
169
+ """
170
+ max_length = min(max_length, MAXIMUM_LENGTH)
171
+ return (
172
+ isinstance(cuid_str, str)
173
+ and min_length <= len(cuid_str) <= max_length
174
+ and bool(_CUID_REGEX.match(cuid_str))
175
+ )
176
+
177
+
178
+ def cuid_wrapper(length: int = DEFAULT_LENGTH) -> Callable[[], str]:
179
+ """Wrap a single Cuid class instance and return a callable that generates a CUID string.
180
+
181
+ Parameters
182
+ ----------
183
+ length : int, default=DEFAULT_LENGTH (24)
184
+ The length parameter is an integer that determines the maximum length of the generated string.
185
+ It has a default value of DEFAULT_LENGTH (24).
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.
188
+
189
+ Returns
190
+ -------
191
+ Callable[[], str]
192
+ A callable that generates a CUID string.
193
+
194
+ Raises
195
+ ------
196
+ ValueError
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).
203
+ """
204
+ cuid_generator: Cuid = Cuid(length=length)
205
+
206
+ def cuid() -> str:
207
+ return cuid_generator.generate()
208
+
209
+ return cuid
@@ -15,13 +15,18 @@ except ImportError:
15
15
  from hashlib import sha512 # type: ignore[assignment] # pylint: disable=ungrouped-imports
16
16
 
17
17
 
18
- BIG_LENGTH: Final = 32
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
+ MIN_LENGTH: Final[int] = 2
22
+ DEFAULT_LENGTH: Final[int] = 24
23
+ BIG_LENGTH: Final[int] = 32
24
+ MAXIMUM_LENGTH: Final[int] = 98
19
25
 
20
26
 
21
27
  if TYPE_CHECKING:
22
- from hashlib import _Hash
23
-
24
28
  from _random import Random
29
+ from hashlib import _Hash
25
30
 
26
31
 
27
32
  def create_counter(count: int) -> Callable[[], int]:
@@ -1,5 +0,0 @@
1
- """Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."""
2
-
3
- from .generator import DEFAULT_LENGTH, INITIAL_COUNT_MAX, Cuid, cuid_wrapper
4
-
5
- __all__ = ["Cuid", "DEFAULT_LENGTH", "INITIAL_COUNT_MAX", "cuid_wrapper"]
@@ -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)
@@ -1,115 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import time
4
- from math import floor
5
- from secrets import SystemRandom
6
- from typing import TYPE_CHECKING, Callable, Final, Optional, Protocol
7
-
8
- from cuid2 import utils
9
-
10
- if TYPE_CHECKING:
11
- from _random import Random
12
-
13
- class FingerprintCallable(Protocol): # pylint: disable=too-few-public-methods
14
- def __call__(self: FingerprintCallable, random_generator: Random) -> str: ...
15
-
16
-
17
- # ~22k hosts before 50% chance of initial counter collision
18
- # with a remaining counter range of 9.0e+15 in JavaScript.
19
- INITIAL_COUNT_MAX: Final[int] = 476782367
20
- DEFAULT_LENGTH: Final = 24
21
- MAXIMUM_LENGTH: Final = 98
22
-
23
-
24
- class Cuid: # pylint: disable=too-few-public-methods
25
- def __init__(
26
- self: Cuid,
27
- random_generator: Callable[[], Random] = SystemRandom,
28
- counter: Callable[[int], Callable[[], int]] = utils.create_counter,
29
- length: int = DEFAULT_LENGTH,
30
- fingerprint: FingerprintCallable = utils.create_fingerprint,
31
- ) -> None:
32
- """Initialization function for the Cuid class that generates a universally unique,
33
- base36 encoded string.
34
-
35
- Parameters
36
- ----------
37
- random_generator : Callable[[], "Random"], default=SystemRandom
38
- Used as the base random generator. The default value is `secrets.SystemRandom`, which is a
39
- cryptographically secure random number generator provided by the Python standard library.
40
- counter : Callable[[int], Callable[[], int]], default=utils.create_counter
41
- The `counter` parameter is a callable that creates a counter returning an incremented value each time it
42
- is called. The `create_counter` function from the `utils` module is used by default.
43
- length : int, default=DEFAULT_LENGTH (4)
44
- The length parameter is an integer that determines the maximum length of the generated string.
45
- It has a default value of DEFAULT_LENGTH (4).
46
- A length value greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
47
- fingerprint : "FingerprintCallable", default=utils.create_fingerprint
48
- The "fingerprint" parameter is a callable function that generates a unique identifier.
49
-
50
- Raises
51
- ------
52
- ValueError
53
- If the length parameter is greater than `MAXIMUM_LENGTH` (98 characters).
54
- """
55
- if length > MAXIMUM_LENGTH:
56
- msg = "Length must never exceed 98 characters."
57
- raise ValueError(msg)
58
-
59
- self._random: Random = random_generator()
60
- self._counter: Callable[[], int] = counter(floor(self._random.random() * INITIAL_COUNT_MAX))
61
- self._length: int = length
62
- self._fingerprint: str = fingerprint(random_generator=self._random)
63
-
64
- def generate(self: Cuid, length: Optional[int] = None) -> str:
65
- """Generates a universally unique, base36 encoded string with a specified length.
66
-
67
- Parameters
68
- ----------
69
- length : int, optional
70
- The length parameter is an optional integer value that specifies the length of the generated string.
71
- If it is not provided, the default length value provided during class initialization is used.
72
- A length value greater than `MAXIMUM_LENGTH` (98 characters) will raise a ValueError.
73
-
74
- Returns
75
- -------
76
- str
77
- Starts with a single, random letter, followed by a hash. The hash is generated using a combination of
78
- the current time in base 36, a random salt, a base 36 counter, and a system fingerprint.
79
- The length of the returned string is limited by `length`.
80
-
81
- Raises
82
- ------
83
- ValueError
84
- If the length parameter is greater than `MAXIMUM_LENGTH` (98 characters).
85
- """
86
- length = length or self._length
87
- if length > MAXIMUM_LENGTH:
88
- msg = "Length must never exceed 98 characters."
89
- raise ValueError(msg)
90
-
91
- first_letter: str = utils.create_letter(random_generator=self._random)
92
-
93
- base36_time: str = utils.base36_encode(time.time_ns())
94
- base36_count: str = utils.base36_encode(self._counter())
95
-
96
- salt: str = utils.create_entropy(length=length, random_generator=self._random)
97
- hash_input: str = base36_time + salt + base36_count + self._fingerprint
98
-
99
- return first_letter + utils.create_hash(hash_input)[1 : length or self._length]
100
-
101
-
102
- def cuid_wrapper() -> Callable[[], str]:
103
- """Wrap a single Cuid class instance and return a callable that generates a CUID string.
104
-
105
- Returns
106
- -------
107
- Callable[[], str]
108
- A callable that generates a CUID string.
109
- """
110
- cuid_generator: Cuid = Cuid()
111
-
112
- def cuid() -> str:
113
- return cuid_generator.generate()
114
-
115
- return cuid
File without changes
File without changes