ulid-pure
Zero-dependency pure-stdlib ULID generator and parser.
A literal port of the canonical reference implementation from github.com/ulid/spec with a thread-safe monotonic-counter ULID class extension. ULIDs are 26-character Crockford base32 strings: 48-bit millisecond Unix timestamp + 80 bits of randomness, lexicographically sortable and URL-safe.
Install
pip install -e .
Quickstart — ULID class (thread-safe, monotonic)
from ulid_pure import ULID
u = ULID()
print(u.gen()) # e.g. '01H9PZ8K3T3N5X4Y9Q2F8B7W0V'
ts_ms, randomness = u.parse('01ARZ3NDEKTSV4RRFFQ69G5FAV')
When .gen() is called multiple times within the same millisecond, the
last 16 bits of the randomness increment as a monotonic counter. If the
counter exceeds 65535 in one millisecond, .gen() spins until the next
millisecond — overflow within a single ms is not possible.
Quickstart — low-level API
from ulid_pure import ulid_gen, ulid_decode, ulid_to_bytes, bytes_to_ulid
s = ulid_gen() # 26-char string
ts_ms, rand = ulid_decode(s) # split (ts, rand)
b = ulid_to_bytes(s) # 16 bytes big-endian
s2 = bytes_to_ulid(b) # 16 bytes -> ULID
Acceptance / specs / citations
- ULID spec: https://github.com/ulid/spec/raw/master/README.md
- Crockford base32: https://www.crockford.com/base32.html
- RFC 4122 (UUIDs, for context): https://www.rfc-editor.org/rfc/rfc4122.txt
Limitations
ULID().gen()raises no exception for normal use; overflow within a single millisecond (>65536 IDs in 1ms) silently advances to the next millisecond via spin-wait.- The
decodepath is forgiving —I/Ldecode as1,Oas0,Uas4— per Crockford.encodenever produces those characters. - This is the canonical reference implementation ported verbatim, not a novel ULID library. Format and behaviour match ulid/spec at the time of porting.
Known Issues
The following findings were identified by an autonomous 5-card adversarial fuzzing chain (111,108 iterations across 4 harnesses). All are accepted for v0.1.0 per the HIGHEST_QUALITY_REPO contract line 316 (0 Critical/High findings → ship-with-documentation permitted).
Medium — silent data corruption on adversarial input
| ID | Description | Location | Repro |
|---|---|---|---|
| F-001 | ulid_decode accepts non-26-char input silently (length not validated) |
ulid_pure/__init__.py:45-50 |
benchmarks/adversarial/cycle_76/repros/F-001.py |
| F-002 | bytes_to_ulid accepts non-16-byte input (round-trip collision) |
ulid_pure/__init__.py:59-62 |
benchmarks/adversarial/cycle_76/repros/F-002.py |
| F-006 | _crockford_encode accepts negative values or values >= 2**128 silently (overflow) |
ulid_pure/__init__.py:17-23 |
benchmarks/adversarial/cycle_76/repros/F-006.py |
Low — runtime errors / spec deviations
| ID | Description | Location | Repro |
|---|---|---|---|
| F-003 | _crockford_decode silently strips hyphens (length invariant not enforced) |
ulid_pure/__init__.py |
benchmarks/adversarial/cycle_76/repros/F-003.py |
| F-005 | Raises AttributeError instead of TypeError on non-string input |
ulid_pure/__init__.py |
benchmarks/adversarial/cycle_76/repros/F-005.py |
| F-007 | ulid_gen / ULID.gen not monotonic across backward wall-clock jumps |
ulid_pure/__init__.py |
benchmarks/adversarial/cycle_76/repros/F-007.py |
Info — spec drift (not runtime errors)
| ID | Description | Location | Repro |
|---|---|---|---|
| F-008 | _crockford_decode maps U to 4; canonical Crockford does not define U |
ulid_pure/__init__.py:14 |
benchmarks/adversarial/cycle_76/repros/F-008.py |
| F-009 | AC5 requires OverflowError on counter overflow; impl spin-waits instead |
ulid_pure/__init__.py |
benchmarks/adversarial/cycle_76/repros/F-009.py |
| F-010 | AC4 requires strict same-ms monotonicity; canonical spec says not guaranteed | ulid_pure/__init__.py |
benchmarks/adversarial/cycle_76/repros/F-010.py |
Tests
pytest -q
147 tests collected; all green. Tests cover the three canonical ULID
vectors (01ARZ3NDEKTSV4RRFFQ69G5FAV, 00000000000000000000000000,
7ZZZZZZZZZZZZZZZZZZZZZZZZ), boundary cases (epoch, max, invalid
charset, monotonic same-ms, overflow, thread-safety).
License
MIT.
Metadata
Release files for ulid-pure 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ulid_pure-0.1.0.tar.gz | 15.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ulid_pure-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 20.2 kB
Release files / ulid_pure-0.1.0.tar.gz
| Download URL | ulid_pure-0.1.0.tar.gz |
|---|---|
| Size | 15.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
be196c67d18efd9a688753e2b0798fb1e8a52d9ae6a6019bf7d64668b449ede1
|
|
BLAKE2b-256 checksum How to use checksums |
6cf8c043bb544d7bb0354cf9835e3a17d8b83c53345900069e36418e22953a2b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / ulid_pure-0.1.0-py3-none-any.whl
| Download URL | ulid_pure-0.1.0-py3-none-any.whl |
|---|---|
| Size | 5.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4884357e5a8a08633661f67a549a79190b78707b24974bb398bf35ee122e9c25
|
|
BLAKE2b-256 checksum How to use checksums |
fc64b6975c94f0179bd4a5c1778f2331a8760f30692552fbf03b726f7008c529
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|