cuid2 2.1.0__tar.gz → 2.2.1__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cuid2
3
- Version: 2.1.0
3
+ Version: 2.2.1
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
6
  Author: Will Gordon
@@ -16,10 +16,14 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
19
21
  Classifier: Topic :: Security :: Cryptography
20
22
  Classifier: Typing :: Typed
21
23
  Requires-Python: >=3.10
22
24
  Project-URL: repository, https://github.com/gordon-code/cuid2/
25
+ Project-URL: Changelog, https://github.com/gordon-code/cuid2/blob/main/CHANGELOG.md
26
+ Project-URL: Issues, https://github.com/gordon-code/cuid2/issues
23
27
  Description-Content-Type: text/markdown
24
28
 
25
29
  # cuid2
@@ -27,19 +31,21 @@ Description-Content-Type: text/markdown
27
31
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/cuid2/)
28
32
  [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.org/project/cuid2/)
29
33
  [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.org/project/cuid2/)
34
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/gordon-code/cuid2/badge)](https://scorecard.dev/viewer/?uri=github.com/gordon-code/cuid2)
35
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/16036/badge)](https://www.bestpractices.dev/projects/16036)
30
36
 
31
- CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
37
+ CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
32
38
  horizontal scaling and performance.
33
39
 
34
- A port of the [CUID2 reference implementation](https://github.com/paralleldrive/cuid2)
40
+ A port of the [CUID2 reference implementation](https://github.com/paralleldrive/cuid2)
35
41
  by [Parallel Drive](https://github.com/paralleldrive) to Python 3.
36
42
 
37
- > :memo: Note: Originally taken from https://github.com/overflowdigital before it
43
+ > :memo: Note: Originally taken from <https://github.com/overflowdigital> before it
38
44
  > was unpublished. Thank you to @joshuathompsonlindley for your original contribution!
39
45
 
40
46
  ## What is CUID2?
41
47
 
42
- * Secure: It's not possible to guess the next ID.
48
+ * Secure: It's not feasible to guess the next ID (unguessability derives from the `random.SystemRandom` CSPRNG salt; per [RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8), IDs are identifiers, not capability tokens or secrets).
43
49
  * Collision resistant: It's extremely unlikely to generate the same ID twice.
44
50
  * Horizontally scalable: Generate IDs on multiple machines without coordination.
45
51
  * Offline-compatible: Generate IDs without a network connection.
@@ -47,15 +53,14 @@ by [Parallel Drive](https://github.com/paralleldrive) to Python 3.
47
53
 
48
54
  ## Why?
49
55
 
50
- For more information on the theory and usage of CUID2, see the
56
+ For more information on the theory and usage of CUID2, see the
51
57
  [following](https://github.com/paralleldrive/cuid2#why).
52
58
 
53
59
  ## Improvements Over CUID
54
60
 
55
- For more information on the improvements of CUID2 over CUID, see the
61
+ For more information on the improvements of CUID2 over CUID, see the
56
62
  [following](https://github.com/paralleldrive/cuid2#improvements-over-cuid).
57
63
 
58
-
59
64
  ## When to Use CUID2
60
65
 
61
66
  * **Zero Timestamp or Creation-Rate Leakage**: Time-ordered formats (`uuid.uuid7()` in Python 3.14, PostgreSQL 18 `uuidv7()`, ULID) embed a 48-bit millisecond timestamp in their prefix, allowing anyone with a public ID to read exact creation times and infer business volume or request velocity ([RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8)). CUID2 hashes nanosecond time, CSPRNG salt, a monotonic counter, and a host fingerprint through **SHA3-512**, revealing nothing about when or where an ID was generated.
@@ -64,7 +69,7 @@ For more information on the improvements of CUID2 over CUID, see the
64
69
  * **123.6-Bit Default Keyspace (`length=24`)**: 1 leading lowercase letter ($26$) followed by 23 base36 characters ($36^{23}$) yields $26 \times 36^{23} \approx \mathbf{1.61 \times 10^{37}}$ possible IDs ($\approx \mathbf{123.6\text{ bits}}$ of entropy)—slightly more than UUIDv4's 122 random bits in a 33% shorter string (24 vs. 36 chars). Reaching a 50% birthday-bound collision probability requires $\sqrt{2 \ln(2) \times 26 \times 36^{23}} \approx \mathbf{4.7 \times 10^{18}}$ IDs (generating **149 million IDs/sec continuously for 1,000 years**).
65
70
  * **Up to 165.0 Bits (`BIG_LENGTH = 32`)**: At `length=32`, the keyspace expands to $26 \times 36^{31} \approx \mathbf{4.54 \times 10^{49}}$ IDs ($\approx \mathbf{165.0\text{ bits}}$; 50% collision bound at $\approx 7.9 \times 10^{24}$ IDs).
66
71
  * **Histogram De-Biasing**: Because $2^{512} \approx 1.34 \times 10^{154}$ is not an exact power of 36, the most-significant base36 digit of a 512-bit SHA3-512 digest is biased toward low digits; CUID2 drops that leading hash character (`[1:]`) and prepends an independent uniform `[a-z]` letter so every character position has a uniform distribution.
67
- * **Pre-Fork & Container Snapshot Resilience**: Combining `os.getpid()`, `socket.gethostname()`, environment variable keys, `time.time_ns()`, a randomly initialized monotonic counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts even starting at the same counter value), and `secrets.SystemRandom` (`os.urandom`) provides defense-in-depth against PRNG or counter state collisions across forked worker processes (Gunicorn, uWSGI, Celery) and cloned VM/container snapshots.
72
+ * **Thread, Pre-Fork, & Container Snapshot Resilience**: `Cuid`, `create_id`, `cuid_wrapper`, and `create_counter` are thread-safe (including on free-threaded Python, tested on `3.14t` and `3.15t`). Each generator records the process ID (`os.getpid()`) that built its state; the first ID generated in a forked child process (`multiprocessing`, Gunicorn, Celery prefork, and uWSGI workers, including uWSGI's default mode, which skips Python's `os.register_at_fork` hooks) rebuilds that state with a fresh RNG state, lock (when `random_generator` returns something other than a `random.SystemRandom` instance or `counter` is not the default; the default `random.SystemRandom` and counter are already thread-safe), random initial counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts starting at the same counter value), and child-PID fingerprints (`os.getpid()`, `socket.gethostname()`, and environment variable keys). For cloned VM/container snapshots where no `fork()` syscall occurs, per-call `random.SystemRandom` (`os.urandom`) salt and `time.time_ns()` provide defense-in-depth against state collisions.
68
73
  * **Polyglot ORM Compatibility (Prisma & Drizzle)**: Generates 24-character base36 primary keys compatible with TypeScript/Node.js services using Prisma (`@default(cuid(2))` / `@onCreate(cuid(2))`) or Drizzle (`@paralleldrive/cuid2`), and replaces the deprecated CUIDv1 (`cuid`) format.
69
74
 
70
75
  | Property | CUID2 (`cuid2`) | UUIDv7 (`uuid.uuid7`) | UUIDv4 (`uuid.uuid4`) | ULID (`python-ulid`) | NanoID (`nanoid`) |
@@ -76,19 +81,21 @@ For more information on the improvements of CUID2 over CUID, see the
76
81
  | **Entropy / Random Bits** | **~123.6 bits** (`165.0` at 32) | 74 bits / ms | 122 bits | 80 bits / ms | 126 bits |
77
82
  | **50% Collision Bound** | **~4.7 × 10¹⁸** (`~7.9 × 10²⁴` at 32) | ~1.6 × 10¹¹ / ms | ~2.7 × 10¹⁸ | ~1.3 × 10¹² / ms | ~1.1 × 10¹⁹ |
78
83
  | **Hides Creation Timestamp?** | **Yes (SHA3-512)** | No (leaks 48-bit ms) | **Yes** | No (leaks 48-bit ms) | **Yes** |
79
- | **Fork / Snapshot Safe?** | **Yes (PID + host + CSPRNG)** | Counter can collide | OS CSPRNG only | Counter can collide | OS CSPRNG only |
84
+ | **Fork / Snapshot Safe?** | **Yes (per-PID state + CSPRNG)** | Counter can collide | OS CSPRNG only | Counter can collide | OS CSPRNG only |
80
85
  | **B-Tree Index Locality** | Low (uniform hash) | **High (time-ordered)** | Low (uniform random) | **High (time-ordered)** | Low (uniform random) |
81
86
 
82
87
  > **When to choose UUIDv7 instead:** Because CUID2 hashes its inputs via SHA3-512, IDs are uniformly distributed across the key space. For write-heavy relational tables where B-tree insert locality and 16-byte binary `UUID` storage matter more than hiding creation timestamps, prefer Python 3.14's `uuid.uuid7()` or PostgreSQL 18's `uuidv7()`.
83
88
 
84
89
  ## Install
85
- ```
90
+
91
+ ```bash
86
92
  pip install cuid2
87
93
  ```
88
94
 
89
95
  ## Usage
90
96
 
91
97
  For most use cases—including Django, SQLAlchemy, and Pydantic model defaults—use the top-level `create_id` and `is_cuid` functions directly:
98
+
92
99
  ```python
93
100
  from cuid2 import create_id, is_cuid
94
101
 
@@ -100,35 +107,42 @@ print(is_cuid(my_cuid)) # True
100
107
 
101
108
  > **Tip for Django & ORM defaults:** Pass `default=create_id` directly on model fields (e.g., `models.CharField(max_length=24, default=create_id)`). Because `create_id` is a top-level function backed by a lazy singleton generator, Django `makemigrations` can serialize it directly and `Cuid()` initialization is deferred until the first ID is generated in each worker process.
102
109
 
103
- You can also generate CUIDs directly in the terminal (optionally specifying a custom length):
110
+ You can also generate and validate CUIDs directly in the terminal:
111
+
104
112
  ```bash
105
- $ cuid2
106
- $ cuid2 10
113
+ cuid2 # Generate a 24-character CUID2
114
+ cuid2 -l 10 # Generate a 10-character CUID2 (or positional: cuid2 10)
115
+ cuid2 --slug # Generate a 5-character short ID slug
116
+ cuid2 -n 5 # Generate 5 CUID2s in a batch
117
+ cuid2 -f server-1 # Use "server-1" verbatim as the fingerprint (IDs stay random)
118
+ cuid2 --check <id> # Validate an ID (prints "<id>: valid" or "<id>: invalid"; -q for exit code only)
119
+ cuid2 -n 5 | cuid2 --check - # Validate newline-delimited IDs from stdin (blank lines are skipped)
120
+ python -m cuid2 # Run via module execution
107
121
  ```
108
122
 
109
- If a framework, dataclass, or Django migration requires a zero-argument callable with a custom length, use `functools.partial` with `create_id`:
123
+ If a framework, dataclass, or Django migration requires a zero-argument callable with a custom length, use `cuid_wrapper` (which returns `functools.partial(create_id, length=10)` directly, making it picklable for `multiprocessing` and serializable by Django `makemigrations`):
124
+
110
125
  ```python
111
- from functools import partial
112
- from typing import Callable
113
- from cuid2 import create_id
126
+ from collections.abc import Callable
127
+
128
+ from cuid2 import cuid_wrapper
114
129
 
115
- short_cuid_generator: Callable[[], str] = partial(create_id, length=10)
130
+ short_cuid_generator: Callable[[], str] = cuid_wrapper(length=10)
116
131
 
117
132
 
118
- def main():
133
+ def main() -> None:
119
134
  short_cuid: str = short_cuid_generator()
120
135
  ```
121
136
 
122
- > **Note:** `cuid_wrapper()` is deprecated as of `v2.1.0` and will be removed in `v3.0.0`. Use `create_id` directly for default 24-character IDs or `functools.partial(create_id, length=...)` for pre-configured custom lengths.
123
-
124
137
  Finally, for more explicit control of the CUID generator, you can instantiate the class directly:
138
+
125
139
  ```python
126
140
  from cuid2 import Cuid
127
141
 
128
142
  CUID_GENERATOR: Cuid = Cuid(length=10)
129
143
 
130
144
 
131
- def main():
145
+ def main() -> None:
132
146
  my_cuid: str = CUID_GENERATOR.generate()
133
147
  next_cuid: str = CUID_GENERATOR.generate()
134
148
 
@@ -138,3 +152,7 @@ def main():
138
152
 
139
153
  `is_cuid` (and `Cuid.is_cuid`) also accepts optional `min_length` and `max_length` arguments to tighten the allowed length.
140
154
  The bounds are clamped to 2 and 98 characters, so they can narrow but never widen what is considered valid.
155
+
156
+ ## Changelog
157
+
158
+ See [CHANGELOG.md](https://github.com/gordon-code/cuid2/blob/main/CHANGELOG.md) for release history.
@@ -3,19 +3,21 @@
3
3
  [![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/cuid2/)
4
4
  [![PyPi](https://img.shields.io/pypi/v/cuid2.svg)](https://pypi.org/project/cuid2/)
5
5
  [![PyPi](https://img.shields.io/pypi/dm/cuid2.svg)](https://pypi.org/project/cuid2/)
6
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/gordon-code/cuid2/badge)](https://scorecard.dev/viewer/?uri=github.com/gordon-code/cuid2)
7
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/16036/badge)](https://www.bestpractices.dev/projects/16036)
6
8
 
7
- CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
9
+ CUID2 for Python 3. Next generation GUIDs. Collision-resistant ids optimized for
8
10
  horizontal scaling and performance.
9
11
 
10
- A port of the [CUID2 reference implementation](https://github.com/paralleldrive/cuid2)
12
+ A port of the [CUID2 reference implementation](https://github.com/paralleldrive/cuid2)
11
13
  by [Parallel Drive](https://github.com/paralleldrive) to Python 3.
12
14
 
13
- > :memo: Note: Originally taken from https://github.com/overflowdigital before it
15
+ > :memo: Note: Originally taken from <https://github.com/overflowdigital> before it
14
16
  > was unpublished. Thank you to @joshuathompsonlindley for your original contribution!
15
17
 
16
18
  ## What is CUID2?
17
19
 
18
- * Secure: It's not possible to guess the next ID.
20
+ * Secure: It's not feasible to guess the next ID (unguessability derives from the `random.SystemRandom` CSPRNG salt; per [RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8), IDs are identifiers, not capability tokens or secrets).
19
21
  * Collision resistant: It's extremely unlikely to generate the same ID twice.
20
22
  * Horizontally scalable: Generate IDs on multiple machines without coordination.
21
23
  * Offline-compatible: Generate IDs without a network connection.
@@ -23,15 +25,14 @@ by [Parallel Drive](https://github.com/paralleldrive) to Python 3.
23
25
 
24
26
  ## Why?
25
27
 
26
- For more information on the theory and usage of CUID2, see the
28
+ For more information on the theory and usage of CUID2, see the
27
29
  [following](https://github.com/paralleldrive/cuid2#why).
28
30
 
29
31
  ## Improvements Over CUID
30
32
 
31
- For more information on the improvements of CUID2 over CUID, see the
33
+ For more information on the improvements of CUID2 over CUID, see the
32
34
  [following](https://github.com/paralleldrive/cuid2#improvements-over-cuid).
33
35
 
34
-
35
36
  ## When to Use CUID2
36
37
 
37
38
  * **Zero Timestamp or Creation-Rate Leakage**: Time-ordered formats (`uuid.uuid7()` in Python 3.14, PostgreSQL 18 `uuidv7()`, ULID) embed a 48-bit millisecond timestamp in their prefix, allowing anyone with a public ID to read exact creation times and infer business volume or request velocity ([RFC 9562 §8](https://www.rfc-editor.org/rfc/rfc9562.html#section-8)). CUID2 hashes nanosecond time, CSPRNG salt, a monotonic counter, and a host fingerprint through **SHA3-512**, revealing nothing about when or where an ID was generated.
@@ -40,7 +41,7 @@ For more information on the improvements of CUID2 over CUID, see the
40
41
  * **123.6-Bit Default Keyspace (`length=24`)**: 1 leading lowercase letter ($26$) followed by 23 base36 characters ($36^{23}$) yields $26 \times 36^{23} \approx \mathbf{1.61 \times 10^{37}}$ possible IDs ($\approx \mathbf{123.6\text{ bits}}$ of entropy)—slightly more than UUIDv4's 122 random bits in a 33% shorter string (24 vs. 36 chars). Reaching a 50% birthday-bound collision probability requires $\sqrt{2 \ln(2) \times 26 \times 36^{23}} \approx \mathbf{4.7 \times 10^{18}}$ IDs (generating **149 million IDs/sec continuously for 1,000 years**).
41
42
  * **Up to 165.0 Bits (`BIG_LENGTH = 32`)**: At `length=32`, the keyspace expands to $26 \times 36^{31} \approx \mathbf{4.54 \times 10^{49}}$ IDs ($\approx \mathbf{165.0\text{ bits}}$; 50% collision bound at $\approx 7.9 \times 10^{24}$ IDs).
42
43
  * **Histogram De-Biasing**: Because $2^{512} \approx 1.34 \times 10^{154}$ is not an exact power of 36, the most-significant base36 digit of a 512-bit SHA3-512 digest is biased toward low digits; CUID2 drops that leading hash character (`[1:]`) and prepends an independent uniform `[a-z]` letter so every character position has a uniform distribution.
43
- * **Pre-Fork & Container Snapshot Resilience**: Combining `os.getpid()`, `socket.gethostname()`, environment variable keys, `time.time_ns()`, a randomly initialized monotonic counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts even starting at the same counter value), and `secrets.SystemRandom` (`os.urandom`) provides defense-in-depth against PRNG or counter state collisions across forked worker processes (Gunicorn, uWSGI, Celery) and cloned VM/container snapshots.
44
+ * **Thread, Pre-Fork, & Container Snapshot Resilience**: `Cuid`, `create_id`, `cuid_wrapper`, and `create_counter` are thread-safe (including on free-threaded Python, tested on `3.14t` and `3.15t`). Each generator records the process ID (`os.getpid()`) that built its state; the first ID generated in a forked child process (`multiprocessing`, Gunicorn, Celery prefork, and uWSGI workers, including uWSGI's default mode, which skips Python's `os.register_at_fork` hooks) rebuilds that state with a fresh RNG state, lock (when `random_generator` returns something other than a `random.SystemRandom` instance or `counter` is not the default; the default `random.SystemRandom` and counter are already thread-safe), random initial counter (`INITIAL_COUNT_MAX = 476,782,367`, giving $\approx 25,700$ concurrent hosts before a 50% chance of two hosts starting at the same counter value), and child-PID fingerprints (`os.getpid()`, `socket.gethostname()`, and environment variable keys). For cloned VM/container snapshots where no `fork()` syscall occurs, per-call `random.SystemRandom` (`os.urandom`) salt and `time.time_ns()` provide defense-in-depth against state collisions.
44
45
  * **Polyglot ORM Compatibility (Prisma & Drizzle)**: Generates 24-character base36 primary keys compatible with TypeScript/Node.js services using Prisma (`@default(cuid(2))` / `@onCreate(cuid(2))`) or Drizzle (`@paralleldrive/cuid2`), and replaces the deprecated CUIDv1 (`cuid`) format.
45
46
 
46
47
  | Property | CUID2 (`cuid2`) | UUIDv7 (`uuid.uuid7`) | UUIDv4 (`uuid.uuid4`) | ULID (`python-ulid`) | NanoID (`nanoid`) |
@@ -52,19 +53,21 @@ For more information on the improvements of CUID2 over CUID, see the
52
53
  | **Entropy / Random Bits** | **~123.6 bits** (`165.0` at 32) | 74 bits / ms | 122 bits | 80 bits / ms | 126 bits |
53
54
  | **50% Collision Bound** | **~4.7 × 10¹⁸** (`~7.9 × 10²⁴` at 32) | ~1.6 × 10¹¹ / ms | ~2.7 × 10¹⁸ | ~1.3 × 10¹² / ms | ~1.1 × 10¹⁹ |
54
55
  | **Hides Creation Timestamp?** | **Yes (SHA3-512)** | No (leaks 48-bit ms) | **Yes** | No (leaks 48-bit ms) | **Yes** |
55
- | **Fork / Snapshot Safe?** | **Yes (PID + host + CSPRNG)** | Counter can collide | OS CSPRNG only | Counter can collide | OS CSPRNG only |
56
+ | **Fork / Snapshot Safe?** | **Yes (per-PID state + CSPRNG)** | Counter can collide | OS CSPRNG only | Counter can collide | OS CSPRNG only |
56
57
  | **B-Tree Index Locality** | Low (uniform hash) | **High (time-ordered)** | Low (uniform random) | **High (time-ordered)** | Low (uniform random) |
57
58
 
58
59
  > **When to choose UUIDv7 instead:** Because CUID2 hashes its inputs via SHA3-512, IDs are uniformly distributed across the key space. For write-heavy relational tables where B-tree insert locality and 16-byte binary `UUID` storage matter more than hiding creation timestamps, prefer Python 3.14's `uuid.uuid7()` or PostgreSQL 18's `uuidv7()`.
59
60
 
60
61
  ## Install
61
- ```
62
+
63
+ ```bash
62
64
  pip install cuid2
63
65
  ```
64
66
 
65
67
  ## Usage
66
68
 
67
69
  For most use cases—including Django, SQLAlchemy, and Pydantic model defaults—use the top-level `create_id` and `is_cuid` functions directly:
70
+
68
71
  ```python
69
72
  from cuid2 import create_id, is_cuid
70
73
 
@@ -76,35 +79,42 @@ print(is_cuid(my_cuid)) # True
76
79
 
77
80
  > **Tip for Django & ORM defaults:** Pass `default=create_id` directly on model fields (e.g., `models.CharField(max_length=24, default=create_id)`). Because `create_id` is a top-level function backed by a lazy singleton generator, Django `makemigrations` can serialize it directly and `Cuid()` initialization is deferred until the first ID is generated in each worker process.
78
81
 
79
- You can also generate CUIDs directly in the terminal (optionally specifying a custom length):
82
+ You can also generate and validate CUIDs directly in the terminal:
83
+
80
84
  ```bash
81
- $ cuid2
82
- $ cuid2 10
85
+ cuid2 # Generate a 24-character CUID2
86
+ cuid2 -l 10 # Generate a 10-character CUID2 (or positional: cuid2 10)
87
+ cuid2 --slug # Generate a 5-character short ID slug
88
+ cuid2 -n 5 # Generate 5 CUID2s in a batch
89
+ cuid2 -f server-1 # Use "server-1" verbatim as the fingerprint (IDs stay random)
90
+ cuid2 --check <id> # Validate an ID (prints "<id>: valid" or "<id>: invalid"; -q for exit code only)
91
+ cuid2 -n 5 | cuid2 --check - # Validate newline-delimited IDs from stdin (blank lines are skipped)
92
+ python -m cuid2 # Run via module execution
83
93
  ```
84
94
 
85
- If a framework, dataclass, or Django migration requires a zero-argument callable with a custom length, use `functools.partial` with `create_id`:
95
+ If a framework, dataclass, or Django migration requires a zero-argument callable with a custom length, use `cuid_wrapper` (which returns `functools.partial(create_id, length=10)` directly, making it picklable for `multiprocessing` and serializable by Django `makemigrations`):
96
+
86
97
  ```python
87
- from functools import partial
88
- from typing import Callable
89
- from cuid2 import create_id
98
+ from collections.abc import Callable
99
+
100
+ from cuid2 import cuid_wrapper
90
101
 
91
- short_cuid_generator: Callable[[], str] = partial(create_id, length=10)
102
+ short_cuid_generator: Callable[[], str] = cuid_wrapper(length=10)
92
103
 
93
104
 
94
- def main():
105
+ def main() -> None:
95
106
  short_cuid: str = short_cuid_generator()
96
107
  ```
97
108
 
98
- > **Note:** `cuid_wrapper()` is deprecated as of `v2.1.0` and will be removed in `v3.0.0`. Use `create_id` directly for default 24-character IDs or `functools.partial(create_id, length=...)` for pre-configured custom lengths.
99
-
100
109
  Finally, for more explicit control of the CUID generator, you can instantiate the class directly:
110
+
101
111
  ```python
102
112
  from cuid2 import Cuid
103
113
 
104
114
  CUID_GENERATOR: Cuid = Cuid(length=10)
105
115
 
106
116
 
107
- def main():
117
+ def main() -> None:
108
118
  my_cuid: str = CUID_GENERATOR.generate()
109
119
  next_cuid: str = CUID_GENERATOR.generate()
110
120
 
@@ -114,3 +124,7 @@ def main():
114
124
 
115
125
  `is_cuid` (and `Cuid.is_cuid`) also accepts optional `min_length` and `max_length` arguments to tighten the allowed length.
116
126
  The bounds are clamped to 2 and 98 characters, so they can narrow but never widen what is considered valid.
127
+
128
+ ## Changelog
129
+
130
+ See [CHANGELOG.md](https://github.com/gordon-code/cuid2/blob/main/CHANGELOG.md) for release history.
@@ -0,0 +1,199 @@
1
+ [build-system]
2
+ requires = ["uv_build==0.13.5"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "cuid2"
7
+ version = "2.2.1"
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
+ "Programming Language :: Python :: 3.15",
24
+ "Programming Language :: Python :: Free Threading :: 3 - Stable",
25
+ "Topic :: Security :: Cryptography",
26
+ "Typing :: Typed",
27
+ ]
28
+ keywords = [
29
+ "crypt",
30
+ "security",
31
+ "uuid",
32
+ "guid",
33
+ "cuid",
34
+ "cryptography",
35
+ ]
36
+
37
+ [[project.authors]]
38
+ name = "Will Gordon"
39
+ email = "dev@gordoncode.dev"
40
+
41
+ [project.urls]
42
+ repository = "https://github.com/gordon-code/cuid2/"
43
+ Changelog = "https://github.com/gordon-code/cuid2/blob/main/CHANGELOG.md"
44
+ Issues = "https://github.com/gordon-code/cuid2/issues"
45
+
46
+ [project.scripts]
47
+ cuid2 = "cuid2.cli:main"
48
+
49
+ [tool.uv]
50
+ default-groups = [
51
+ "build",
52
+ "lint",
53
+ "test",
54
+ "typing",
55
+ ]
56
+
57
+ [tool.codespell]
58
+ check-hidden = ""
59
+ ignore-words-list = "intoto"
60
+ skip = ".cache,.claude,.coverage,.coverage.*,.git,.hypothesis,.mypy_cache,.pdm-build,.pytest_cache,.ruff_cache,.rumdl_cache,.venv,__pycache__,build,dist,hack,uv.lock"
61
+
62
+ [tool.rumdl]
63
+ cache-dir = "local/.rumdl_cache"
64
+ disable = ["MD013"]
65
+ exclude = ["hack"]
66
+
67
+ [tool.rumdl.per-file-ignores]
68
+ ".github/**/*.md" = ["MD041"]
69
+
70
+ [tool.ruff]
71
+ line-length = 120
72
+ src = ["src"]
73
+ target-version = "py310"
74
+ cache-dir = "local/.ruff_cache"
75
+ extend-exclude = ["hack"]
76
+ force-exclude = true
77
+
78
+ [tool.ruff.lint]
79
+ select = ["ALL"]
80
+ ignore = [
81
+ "COM812",
82
+ "ISC001",
83
+ "CPY001",
84
+ ]
85
+ unfixable = [
86
+ "T201",
87
+ "F401",
88
+ "F841",
89
+ ]
90
+
91
+ [tool.ruff.lint.per-file-ignores]
92
+ "local/tests/*.py" = [
93
+ "D100",
94
+ "D101",
95
+ "D102",
96
+ "D103",
97
+ "D104",
98
+ "D205",
99
+ "D401",
100
+ "FBT001",
101
+ "PLR2004",
102
+ "S101",
103
+ "SLF001",
104
+ ]
105
+
106
+ [tool.ruff.lint.isort]
107
+ section-order = [
108
+ "future",
109
+ "standard-library",
110
+ "third-party",
111
+ "first-party",
112
+ "local-folder",
113
+ ]
114
+
115
+ [tool.ruff.lint.pydocstyle]
116
+ convention = "numpy"
117
+
118
+ [tool.pytest.ini_options]
119
+ minversion = "9.0"
120
+ strict = true
121
+ cache_dir = "local/.pytest_cache"
122
+ python_files = "test_*.py"
123
+ pythonpath = ["."]
124
+ testpaths = ["local/tests"]
125
+ addopts = [
126
+ "--import-mode=importlib",
127
+ "--cov",
128
+ "--cov-report=term-missing:skip-covered",
129
+ "-m",
130
+ "not slow and not heavy",
131
+ ]
132
+ filterwarnings = ["error"]
133
+ markers = [
134
+ "slow: mark test as slow to run",
135
+ "heavy: mark collision test as heavy (>=10M IDs)",
136
+ ]
137
+
138
+ [tool.coverage.run]
139
+ branch = true
140
+ source_pkgs = ["cuid2"]
141
+
142
+ [tool.coverage.paths]
143
+ source = [
144
+ "src/cuid2",
145
+ "*/site-packages/cuid2",
146
+ ]
147
+
148
+ [tool.coverage.report]
149
+ fail_under = 100
150
+ show_missing = true
151
+ exclude_also = ["if TYPE_CHECKING:"]
152
+
153
+ [tool.mypy]
154
+ cache_dir = "local/.mypy_cache"
155
+ files = ["."]
156
+ exclude = [
157
+ "^build/",
158
+ "^dist/",
159
+ "^hack/",
160
+ ]
161
+ strict = true
162
+
163
+ [[tool.mypy.overrides]]
164
+ module = [
165
+ "atheris",
166
+ "keepachangelog",
167
+ ]
168
+ ignore_missing_imports = true
169
+
170
+ [tool.pyright]
171
+ exclude = [
172
+ "build",
173
+ "dist",
174
+ "hack",
175
+ ]
176
+ strict = ["src"]
177
+
178
+ [dependency-groups]
179
+ build = ["twine==7.0.0"]
180
+ lint = [
181
+ "codespell==2.4.3",
182
+ "prek==0.5.7",
183
+ "ruff==0.17.0",
184
+ "rumdl==0.3.0",
185
+ "zizmor==1.31.1; python_version >= '3.11'",
186
+ ]
187
+ test = [
188
+ "coverage[toml]==7.16.2",
189
+ "hypothesis==6.168.5; python_version < '3.11'",
190
+ "hypothesis==6.170.5; python_version >= '3.11'",
191
+ "keepachangelog==2.0.0",
192
+ "pytest==9.1.1",
193
+ "pytest-cov==7.1.0",
194
+ "pytest-mock==3.16.0",
195
+ ]
196
+ typing = [
197
+ "mypy==2.4.0",
198
+ "pyright[nodejs]==1.1.414",
199
+ ]
@@ -0,0 +1,163 @@
1
+ [build-system]
2
+ requires = ["uv_build==0.13.5"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "cuid2"
7
+ version = "2.2.1"
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
+ "Programming Language :: Python :: 3.15",
27
+ "Programming Language :: Python :: Free Threading :: 3 - Stable",
28
+ "Topic :: Security :: Cryptography",
29
+ "Typing :: Typed",
30
+ ]
31
+ keywords = ['crypt', 'security', 'uuid', 'guid', 'cuid', 'cryptography']
32
+
33
+ [project.urls]
34
+ repository = "https://github.com/gordon-code/cuid2/"
35
+ Changelog = "https://github.com/gordon-code/cuid2/blob/main/CHANGELOG.md"
36
+ Issues = "https://github.com/gordon-code/cuid2/issues"
37
+
38
+ [project.scripts]
39
+ cuid2 = "cuid2.cli:main"
40
+
41
+ [tool.uv]
42
+ default-groups = ["build", "lint", "test", "typing"]
43
+
44
+ [dependency-groups]
45
+ build = [
46
+ "twine==7.0.0",
47
+ ]
48
+ lint = [
49
+ "codespell==2.4.3",
50
+ "prek==0.5.7",
51
+ "ruff==0.17.0",
52
+ "rumdl==0.3.0",
53
+ "zizmor==1.31.1; python_version >= '3.11'",
54
+ ]
55
+ test = [
56
+ "coverage[toml]==7.16.2",
57
+ "hypothesis==6.168.5; python_version < '3.11'",
58
+ "hypothesis==6.170.5; python_version >= '3.11'",
59
+ "keepachangelog==2.0.0",
60
+ "pytest==9.1.1",
61
+ "pytest-cov==7.1.0",
62
+ "pytest-mock==3.16.0",
63
+ ]
64
+ typing = [
65
+ "mypy==2.4.0",
66
+ "pyright[nodejs]==1.1.414",
67
+ ]
68
+
69
+ [tool.codespell]
70
+ check-hidden = ""
71
+ ignore-words-list = "intoto"
72
+ skip = ".cache,.claude,.coverage,.coverage.*,.git,.hypothesis,.mypy_cache,.pdm-build,.pytest_cache,.ruff_cache,.rumdl_cache,.venv,__pycache__,build,dist,hack,uv.lock"
73
+
74
+ [tool.rumdl]
75
+ cache-dir = "local/.rumdl_cache"
76
+ disable = ["MD013"]
77
+ exclude = ["hack"]
78
+
79
+ [tool.rumdl.per-file-ignores]
80
+ ".github/**/*.md" = ["MD041"]
81
+
82
+ [tool.ruff]
83
+ line-length = 120
84
+ src = ["src"]
85
+ target-version = "py310"
86
+ cache-dir = "local/.ruff_cache"
87
+ extend-exclude = ["hack"]
88
+ force-exclude = true
89
+
90
+ [tool.ruff.lint]
91
+ # https://docs.astral.sh/ruff/rules/
92
+ select = ["ALL"]
93
+ ignore = [
94
+ "COM812", "ISC001", # Conflict with formatter
95
+ "CPY001", # Missing copyright notice at top of file
96
+ ]
97
+ # Don't automatically remove `print`
98
+ # Stop automatically removing unused imports
99
+ unfixable = ["T201", "F401", "F841"]
100
+
101
+ [tool.ruff.lint.per-file-ignores]
102
+ # Test files: allow assert (S101), magic values (PLR2004), private member access (SLF001),
103
+ # boolean test params (FBT001), and test docstring conventions (D1xx, D205, D401)
104
+ "local/tests/*.py" = ["D100", "D101", "D102", "D103", "D104", "D205", "D401", "FBT001", "PLR2004", "S101", "SLF001"]
105
+
106
+ [tool.ruff.lint.isort]
107
+ section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]
108
+
109
+ [tool.ruff.lint.pydocstyle]
110
+ convention = "numpy"
111
+
112
+ [tool.pytest.ini_options]
113
+ minversion = "9.0"
114
+ strict = true
115
+ cache_dir = "local/.pytest_cache"
116
+ python_files = "test_*.py"
117
+ pythonpath = ["."]
118
+ testpaths = ["local/tests"]
119
+ addopts = [
120
+ "--import-mode=importlib",
121
+ "--cov",
122
+ "--cov-report=term-missing:skip-covered",
123
+ "-m",
124
+ "not slow and not heavy",
125
+ ]
126
+ filterwarnings = [
127
+ "error",
128
+ ]
129
+ markers = [
130
+ "slow: mark test as slow to run",
131
+ "heavy: mark collision test as heavy (>=10M IDs)",
132
+ ]
133
+
134
+ [tool.coverage.run]
135
+ branch = true
136
+ source_pkgs = ["cuid2"]
137
+
138
+ [tool.coverage.paths]
139
+ source = [
140
+ "src/cuid2",
141
+ "*/site-packages/cuid2",
142
+ ]
143
+
144
+ [tool.coverage.report]
145
+ fail_under = 100
146
+ show_missing = true
147
+ exclude_also = [
148
+ "if TYPE_CHECKING:",
149
+ ]
150
+
151
+ [tool.mypy]
152
+ cache_dir = "local/.mypy_cache"
153
+ files = ["."]
154
+ exclude = ["^build/", "^dist/", "^hack/"]
155
+ strict = true
156
+
157
+ [[tool.mypy.overrides]]
158
+ module = ["atheris", "keepachangelog"]
159
+ ignore_missing_imports = true
160
+
161
+ [tool.pyright]
162
+ exclude = ["build", "dist", "hack"]
163
+ strict = ["src"]