Metadata-Version: 2.4
Name: ulid-pure
Version: 0.1.0
Summary: Zero-dependency pure-stdlib ULID generator and parser (canonical ulid/spec port)
Author: ulid-pure contributors
License: MIT
Project-URL: Homepage, https://github.com/ulid/spec
Keywords: ulid,uuid,identifier,sortable,lexicographic,base32,crockford
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# ulid-pure

Zero-dependency pure-stdlib ULID generator and parser.

A literal port of the canonical reference implementation from
[github.com/ulid/spec](https://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

```bash
pip install -e .
```

## Quickstart — `ULID` class (thread-safe, monotonic)

```python
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

```python
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 `decode` path is forgiving — `I`/`L` decode as `1`, `O` as `0`,
  `U` as `4` — per Crockford. `encode` never produces those characters.
- This is the canonical reference implementation ported verbatim, not a
  novel ULID library. Format and behaviour match
  [ulid/spec](https://github.com/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

```bash
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.
