Metadata-Version: 2.4
Name: nulid
Version: 0.1.0
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
License-File: LICENSE
Summary: Nanosecond-Precision Universally Lexicographically Sortable Identifier (NULID), powered by Rust.
Author-email: kakilangit <python@kakilangit.dev>
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/kakilangit/pynulid
Project-URL: Repository, https://github.com/kakilangit/pynulid
Project-URL: Rust crate, https://github.com/kakilangit/nulid

[![PyPI](https://img.shields.io/pypi/v/nulid.svg)](https://pypi.org/project/nulid/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/kakilangit/pynulid/blob/main/LICENSE)

# nulid

**Nanosecond-Precision Universally Lexicographically Sortable Identifier (NULID) for Python**

Python bindings for the [nulid](https://crates.io/crates/nulid) Rust crate, powered by [PyO3](https://pyo3.rs/).

A NULID is a 128-bit identifier with:

- **68-bit nanosecond timestamp** for precise chronological ordering
- **60-bit cryptographically secure randomness** for collision resistance
- **26-character Crockford Base32 encoding** that is URL-safe and lexicographically sortable
- **UUID-compatible size** (16 bytes)

## Installation

```bash
pip install nulid
```

A Rust toolchain (1.88+) is required to build from source. Pre-built wheels are available for common platforms.

## Usage

### Generating NULIDs

```python
import nulid

# As a 26-character Base32 string
id = nulid.generate()
# => "01AN4Z07BY79K47PAZ7R9SZK18"

# As a 16-byte binary
raw = nulid.generate_bytes()
# => b'\x01\xan...'
```

### Encoding and Decoding

```python
raw = nulid.generate_bytes()

# Bytes -> String
encoded = nulid.encode(raw)

# String -> Bytes
decoded = nulid.decode(encoded)
assert raw == decoded
```

### Inspecting Components

```python
raw = nulid.generate_bytes()

ns = nulid.nanos(raw)    # nanoseconds since epoch
ms = nulid.millis(raw)   # milliseconds since epoch
rand = nulid.random(raw) # 60-bit random value
assert not nulid.is_nil(raw)

assert nulid.is_nil(b"\x00" * 16)
```

### Monotonic Generator

For guaranteed strictly increasing IDs, even within the same nanosecond:

```python
gen = nulid.Generator()

id1 = gen.generate()
id2 = gen.generate()
id3 = gen.generate()

assert id1 < id2 < id3
```

Binary output:

```python
gen = nulid.Generator()

b1 = gen.generate_bytes()
b2 = gen.generate_bytes()

assert b1 < b2
```

### Distributed Generation

For multi-node deployments, assign each node a unique ID (0-65535):

```python
gen = nulid.DistributedGenerator(node_id=1)

id = gen.generate()
assert gen.node_id == 1
```

The node ID is embedded in the random bits, guaranteeing cross-node uniqueness even with identical timestamps.

## Why NULID over ULID?

| Feature            | ULID              | NULID            |
| ------------------ | ----------------- | ---------------- |
| **Total Bits**     | 128               | 128              |
| **String Length**   | 26 chars          | 26 chars         |
| **Timestamp Bits** | 48 (milliseconds) | 68 (nanoseconds) |
| **Randomness Bits** | 80               | 60               |
| **Time Precision** | 1 millisecond     | 1 nanosecond     |
| **Lifespan**       | Until 10889 AD    | Until ~11326 AD  |

NULID trades 20 bits of randomness for 20 extra bits of timestamp precision, giving nanosecond-level ordering while still providing 1.15 quintillion unique IDs per nanosecond.

## Development

```bash
# Install in development mode
make develop

# Run all CI checks (format, clippy, lint, test)
make ci

# Run tests only
make test

# Format code (Python + Rust)
make fmt

# Show all available targets
make help
```

## License

MIT - see [LICENSE](LICENSE) for details.

