gr-ulid 0.1.0__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.
- gr_ulid-0.1.0/.github/workflows/ci.yml +83 -0
- gr_ulid-0.1.0/.github/workflows/publish.yml +27 -0
- gr_ulid-0.1.0/.gitignore +10 -0
- gr_ulid-0.1.0/LICENSE +21 -0
- gr_ulid-0.1.0/PKG-INFO +333 -0
- gr_ulid-0.1.0/README.md +309 -0
- gr_ulid-0.1.0/pyproject.toml +67 -0
- gr_ulid-0.1.0/scripts/release.sh +36 -0
- gr_ulid-0.1.0/src/ulid/__init__.py +83 -0
- gr_ulid-0.1.0/src/ulid/generator.py +81 -0
- gr_ulid-0.1.0/src/ulid/monotonic.py +118 -0
- gr_ulid-0.1.0/src/ulid/py.typed +0 -0
- gr_ulid-0.1.0/src/ulid/ulid.py +671 -0
- gr_ulid-0.1.0/tests/__init__.py +0 -0
- gr_ulid-0.1.0/tests/test_benchmark.py +88 -0
- gr_ulid-0.1.0/tests/test_ulid.py +639 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
name: Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Python
|
|
17
|
+
uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.13"
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: pip install hatch
|
|
23
|
+
|
|
24
|
+
- name: Check formatting
|
|
25
|
+
run: hatch run fmt -- --check
|
|
26
|
+
|
|
27
|
+
- name: Lint
|
|
28
|
+
run: hatch run lint
|
|
29
|
+
|
|
30
|
+
typecheck:
|
|
31
|
+
name: Type Check
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.13"
|
|
40
|
+
|
|
41
|
+
- name: Install dependencies
|
|
42
|
+
run: pip install hatch
|
|
43
|
+
|
|
44
|
+
- name: mypy
|
|
45
|
+
run: hatch run typecheck
|
|
46
|
+
|
|
47
|
+
test:
|
|
48
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
strategy:
|
|
51
|
+
matrix:
|
|
52
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
|
|
56
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
57
|
+
uses: actions/setup-python@v5
|
|
58
|
+
with:
|
|
59
|
+
python-version: ${{ matrix.python-version }}
|
|
60
|
+
allow-prereleases: true
|
|
61
|
+
|
|
62
|
+
- name: Install dependencies
|
|
63
|
+
run: pip install hatch
|
|
64
|
+
|
|
65
|
+
- name: Test
|
|
66
|
+
run: hatch run test -- -v
|
|
67
|
+
|
|
68
|
+
benchmark:
|
|
69
|
+
name: Benchmarks
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v4
|
|
73
|
+
|
|
74
|
+
- name: Set up Python
|
|
75
|
+
uses: actions/setup-python@v5
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.13"
|
|
78
|
+
|
|
79
|
+
- name: Install dependencies
|
|
80
|
+
run: pip install hatch
|
|
81
|
+
|
|
82
|
+
- name: Run benchmarks
|
|
83
|
+
run: hatch run bench
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
|
|
17
|
+
- name: Install build dependencies
|
|
18
|
+
run: pip install build twine
|
|
19
|
+
|
|
20
|
+
- name: Build package
|
|
21
|
+
run: python -m build
|
|
22
|
+
|
|
23
|
+
- name: Publish to PyPI
|
|
24
|
+
env:
|
|
25
|
+
TWINE_USERNAME: __token__
|
|
26
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
27
|
+
run: twine upload dist/*
|
gr_ulid-0.1.0/.gitignore
ADDED
gr_ulid-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gaucho Racing
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gr_ulid-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gr-ulid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A blazing fast, production-grade ULID implementation in Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/gaucho-racing/ulid-py
|
|
6
|
+
Project-URL: Repository, https://github.com/gaucho-racing/ulid-py
|
|
7
|
+
Project-URL: Issues, https://github.com/gaucho-racing/ulid-py/issues
|
|
8
|
+
Author: Gaucho Racing
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: identifier,sortable,ulid,uuid
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# ulid-py
|
|
26
|
+
|
|
27
|
+
[](https://github.com/gaucho-racing/ulid-py/actions/workflows/ci.yml)
|
|
28
|
+
[](https://pypi.org/project/gr-ulid/)
|
|
29
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
|
|
31
|
+
A blazing fast, production-grade [ULID](https://github.com/ulid/spec) implementation in Python. Designed to provide a consistent, ergonomic identifier format, ulid-py is currently used across many of Gaucho Racing's services and projects.
|
|
32
|
+
|
|
33
|
+
- **Lowercase by default** — all string output uses lowercase Crockford Base32
|
|
34
|
+
- **Prefix support** — generate entity-scoped IDs like `user_01arz3ndek...` or `txn_01arz3ndek...`
|
|
35
|
+
- **Distributed uniqueness** — `Generator` with node ID partitioning guarantees collision-free IDs across up to 65,536 nodes without coordination
|
|
36
|
+
- **Monotonic sorting** — IDs generated within the same millisecond are strictly ordered
|
|
37
|
+
- **Fully unrolled encoding** — Crockford Base32 encode/decode with no loops
|
|
38
|
+
- **Thread-safe** — `make()`, `Generator`, and `default_entropy()` are safe for concurrent use
|
|
39
|
+
- **128-bit UUID compatible** — drop-in replacement for UUID columns in databases
|
|
40
|
+
- **Fully typed** — PEP 561 compliant with `py.typed` marker, passes `mypy --strict`
|
|
41
|
+
- **Zero runtime dependencies** — only stdlib
|
|
42
|
+
|
|
43
|
+
## Getting Started
|
|
44
|
+
|
|
45
|
+
### Installing
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pip install gr-ulid
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import ulid
|
|
55
|
+
|
|
56
|
+
# Generate a ULID
|
|
57
|
+
id = ulid.make()
|
|
58
|
+
print(id) # 01jgy5fz7rqv8s3n0x4m6k2w1h
|
|
59
|
+
|
|
60
|
+
# With a prefix
|
|
61
|
+
print(id.prefixed("user")) # user_01jgy5fz7rqv8s3n0x4m6k2w1h
|
|
62
|
+
|
|
63
|
+
# Parse it back
|
|
64
|
+
parsed = ulid.parse("01jgy5fz7rqv8s3n0x4m6k2w1h")
|
|
65
|
+
print(parsed.time()) # Unix millisecond timestamp
|
|
66
|
+
print(parsed.timestamp()) # datetime
|
|
67
|
+
|
|
68
|
+
# Parse prefixed IDs
|
|
69
|
+
prefix, parsed = ulid.parse_prefixed("user_01jgy5fz7rqv8s3n0x4m6k2w1h")
|
|
70
|
+
print(prefix) # "user"
|
|
71
|
+
|
|
72
|
+
# Use a Generator for distributed systems
|
|
73
|
+
gen = ulid.new_generator(
|
|
74
|
+
ulid.with_node_id(1),
|
|
75
|
+
ulid.with_prefix("evt"),
|
|
76
|
+
)
|
|
77
|
+
print(gen.make_prefixed()) # evt_01jgy5fz7r...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Specification
|
|
81
|
+
|
|
82
|
+
This library implements the [ULID spec](https://github.com/ulid/spec) with several opinionated extensions. This section covers the binary format, encoding, monotonicity behavior, distributed uniqueness strategy, and every deviation from the official spec.
|
|
83
|
+
|
|
84
|
+
### Binary Layout
|
|
85
|
+
|
|
86
|
+
A ULID is 128 bits (16 bytes), stored in big-endian (network byte order) as an immutable `bytes` object:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
0 1 2 3
|
|
90
|
+
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
|
91
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
92
|
+
| 32_bit_uint_time_high |
|
|
93
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
94
|
+
| 16_bit_uint_time_low | 16_bit_uint_random |
|
|
95
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
96
|
+
| 32_bit_uint_random |
|
|
97
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
98
|
+
| 32_bit_uint_random |
|
|
99
|
+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Component | Bytes | Bits | Description |
|
|
103
|
+
|---|---|---|---|
|
|
104
|
+
| Timestamp | `[0:6]` | 48 | Unix milliseconds, big-endian. Valid until year 10889 AD. |
|
|
105
|
+
| Entropy | `[6:16]` | 80 | Cryptographic randomness (or node-partitioned randomness). |
|
|
106
|
+
|
|
107
|
+
Using immutable `bytes` as the underlying type means ULIDs are **hashable**: they can be used as dictionary keys and set members. Byte comparison ordering is consistent with chronological and lexicographic string ordering because the timestamp occupies the most significant bytes.
|
|
108
|
+
|
|
109
|
+
### Crockford Base32 Encoding
|
|
110
|
+
|
|
111
|
+
The string representation is 26 characters using the Crockford Base32 alphabet:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
0123456789abcdefghjkmnpqrstvwxyz
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
The first 10 characters encode the 48-bit timestamp, the remaining 16 encode the 80-bit entropy:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
ttttttttttrrrrrrrrrrrrrrrr
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The encoding and decoding are **fully unrolled**: every bit extraction/insertion is a single explicit line with no loops. Decoding uses a 256-byte lookup table for O(1) character-to-value conversion, and both upper and lowercase map to the same values, making parsing inherently case-insensitive.
|
|
124
|
+
|
|
125
|
+
**Overflow check**: 26 Base32 characters technically encode 130 bits, but a ULID only uses 128. The first character is restricted to values `0`–`7` (3 bits). Any ULID string starting with `8` or higher is rejected with `ErrOverflow`. The largest valid ULID is `7zzzzzzzzzzzzzzzzzzzzzzzzz`.
|
|
126
|
+
|
|
127
|
+
#### `parse` vs `parse_strict`
|
|
128
|
+
|
|
129
|
+
`parse` skips character validation for speed. Invalid characters (like `I`, `L`, `O`, `U`) will silently produce wrong bits rather than returning an error. Use `parse_strict` when accepting untrusted input. Use `parse` when you control the input (e.g., reading from your own database).
|
|
130
|
+
|
|
131
|
+
### Monotonicity
|
|
132
|
+
|
|
133
|
+
When multiple ULIDs are generated within the same millisecond, the spec requires monotonic ordering. This library implements monotonicity through `MonotonicEntropy`:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import os
|
|
137
|
+
import ulid
|
|
138
|
+
|
|
139
|
+
entropy = ulid.monotonic(os.urandom, 0)
|
|
140
|
+
ms = ulid.now()
|
|
141
|
+
|
|
142
|
+
# All three share the same millisecond: entropy is incremented, not re-randomized
|
|
143
|
+
id1 = ulid.new(ms, entropy) # random entropy R
|
|
144
|
+
id2 = ulid.new(ms, entropy) # R + random_increment
|
|
145
|
+
id3 = ulid.new(ms, entropy) # R + random_increment + random_increment
|
|
146
|
+
# id1 < id2 < id3 guaranteed
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Overflow behavior**: The 80-bit entropy space is tracked using a custom `_UInt80` type (`uint16` high + `uint64` low) with explicit masking (since Python integers are arbitrary precision). When incrementing would overflow, `ErrMonotonicOverflow` is raised. The library **never** silently wraps around or advances the timestamp.
|
|
150
|
+
|
|
151
|
+
**Thread safety**: `MonotonicEntropy` itself is **not** thread-safe. For concurrent use, wrap it with `LockedMonotonicReader` (which adds a `threading.Lock`), or use `default_entropy()` / `make()` which do this automatically. The `Generator` class also handles its own locking internally.
|
|
152
|
+
|
|
153
|
+
### Entropy Sources
|
|
154
|
+
|
|
155
|
+
The library accepts any `Callable[[int], bytes]` as an entropy source:
|
|
156
|
+
|
|
157
|
+
| Source | Security | Notes |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `os.urandom` | Cryptographic | Default. Uses OS entropy pool. |
|
|
160
|
+
| Custom callable | Varies | Any function `(int) -> bytes`. |
|
|
161
|
+
| `monotonic(r, inc)` | Inherits from `r` | Increments within same ms instead of re-reading. |
|
|
162
|
+
| `None` | None | Zero entropy. Useful for timestamp-only IDs. |
|
|
163
|
+
|
|
164
|
+
### Distributed Uniqueness
|
|
165
|
+
|
|
166
|
+
For multi-node deployments, the `Generator` class supports embedding a **16-bit node ID** in the first 2 bytes of the entropy field:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
gen = ulid.new_generator(ulid.with_node_id(42))
|
|
170
|
+
id = gen.make()
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
This partitions the entropy layout as follows:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
Bytes [0:6] - 48-bit timestamp (unchanged)
|
|
177
|
+
Bytes [6:8] - 16-bit node ID (0–65535)
|
|
178
|
+
Bytes [8:16] - 64-bit monotonic random entropy
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Two generators with different node IDs **cannot** produce the same ULID, even within the same millisecond.
|
|
182
|
+
|
|
183
|
+
### Prefixed IDs
|
|
184
|
+
|
|
185
|
+
Prefixed IDs are a library extension for entity-scoped identifiers:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
id = ulid.make()
|
|
189
|
+
id.prefixed("user") # "user_01arz3ndektsv4rrffq69g5fav"
|
|
190
|
+
id.prefixed("txn") # "txn_01arz3ndektsv4rrffq69g5fav"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The prefix is **not** part of the ULID itself. `parse_prefixed` splits on the first `_` and parses the ULID portion:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
prefix, id = ulid.parse_prefixed("user_01arz3ndektsv4rrffq69g5fav")
|
|
197
|
+
# prefix = "user", id = the parsed ULID
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Deviations from the Official Spec
|
|
201
|
+
|
|
202
|
+
| Behavior | Official Spec | This Library |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| **String case** | Uppercase (`01ARZ3NDEK...`) | Lowercase (`01arz3ndek...`). Parsing remains case-insensitive. |
|
|
205
|
+
| **Prefixed IDs** | Not specified | Supported via `prefixed()` and `parse_prefixed()`. |
|
|
206
|
+
| **Node ID partitioning** | Not specified | Supported via `Generator` with `with_node_id()`. |
|
|
207
|
+
| **Excluded letter handling** | Crockford spec maps `I`→`1`, `L`→`1`, `O`→`0` during decoding | Not mapped. `I`, `L`, `O`, `U` are treated as invalid in strict mode and produce undefined results in non-strict mode. |
|
|
208
|
+
|
|
209
|
+
### Footguns
|
|
210
|
+
|
|
211
|
+
- **`parse` does not validate characters.** Use `parse_strict` for untrusted input.
|
|
212
|
+
- **`MonotonicEntropy` is not thread-safe.** Using it from multiple threads without `LockedMonotonicReader` will corrupt state. `make()` and `Generator` handle this for you.
|
|
213
|
+
- **`bytes()` and `entropy()` return the underlying immutable bytes.** Since `_data` is immutable `bytes`, no copy is needed.
|
|
214
|
+
- **`Generator` with node ID clobbers monotonic high bits.** If intra-millisecond ordering matters more than distributed uniqueness, use `make()` instead.
|
|
215
|
+
- **Monotonic overflow is an error, not a retry.** When `ErrMonotonicOverflow` is raised, the caller is responsible for handling it.
|
|
216
|
+
|
|
217
|
+
## Benchmarks
|
|
218
|
+
|
|
219
|
+
Measured with `pytest-benchmark` on Python 3.14, AMD EPYC 7763 (GitHub Actions CI). Pure Python, no C extensions.
|
|
220
|
+
|
|
221
|
+
| Operation | Median | Throughput |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| `marshal_binary()` | 93 ns | 10.7M ops/sec |
|
|
224
|
+
| `compare()` | 122 ns | 8.1M ops/sec |
|
|
225
|
+
| `now()` | 201 ns | 4.9M ops/sec |
|
|
226
|
+
| `new()` (crypto entropy) | 2.6 µs | 372K ops/sec |
|
|
227
|
+
| `string()` | 3.1 µs | 320K ops/sec |
|
|
228
|
+
| `parse()` | 3.7 µs | 260K ops/sec |
|
|
229
|
+
| `parse_strict()` | 5.0 µs | 198K ops/sec |
|
|
230
|
+
| `make()` | 5.5 µs | 180K ops/sec |
|
|
231
|
+
| `new_generator().make()` | 5.5 µs | 180K ops/sec |
|
|
232
|
+
| `new_generator().make_prefixed()` | 9.3 µs | 106K ops/sec |
|
|
233
|
+
|
|
234
|
+
Run benchmarks locally:
|
|
235
|
+
|
|
236
|
+
```sh
|
|
237
|
+
hatch run bench
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## API
|
|
241
|
+
|
|
242
|
+
### Constructors
|
|
243
|
+
|
|
244
|
+
| Function | Description |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `make()` | Generate a ULID with current time and default entropy. Thread-safe. |
|
|
247
|
+
| `new(ms, entropy)` | Generate with explicit timestamp and entropy source. |
|
|
248
|
+
| `must_new(ms, entropy)` | Like `new` (raises on error in Python). |
|
|
249
|
+
| `parse(s)` | Decode a 26-char Base32 string. Case-insensitive. |
|
|
250
|
+
| `parse_strict(s)` | Like `parse` with character validation. |
|
|
251
|
+
| `parse_prefixed(s)` | Parse a `prefix_ulid` string, returning `(prefix, ULID)`. |
|
|
252
|
+
| `must_parse(s)` | Like `parse` (raises on error in Python). |
|
|
253
|
+
| `must_parse_strict(s)` | Like `parse_strict` (raises on error in Python). |
|
|
254
|
+
|
|
255
|
+
### ULID Methods
|
|
256
|
+
|
|
257
|
+
| Method | Description |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `string()` | 26-char lowercase Crockford Base32 string. |
|
|
260
|
+
| `prefixed(p)` | Prefixed string: `p_<ulid>`. |
|
|
261
|
+
| `bytes()` | Raw 16-byte data. |
|
|
262
|
+
| `time()` | Unix millisecond timestamp. |
|
|
263
|
+
| `timestamp()` | Timestamp as `datetime`. |
|
|
264
|
+
| `entropy()` | 10-byte entropy. |
|
|
265
|
+
| `is_zero()` | True if zero value. |
|
|
266
|
+
| `compare(other)` | Lexicographic comparison (-1, 0, +1). |
|
|
267
|
+
| `set_time(ms)` | Return new ULID with updated timestamp. |
|
|
268
|
+
| `set_entropy(e)` | Return new ULID with updated entropy (10 bytes). |
|
|
269
|
+
|
|
270
|
+
### Python Special Methods
|
|
271
|
+
|
|
272
|
+
| Method | Description |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `__str__` | Same as `string()`. |
|
|
275
|
+
| `__bytes__` | Same as `bytes()`. |
|
|
276
|
+
| `__int__` | 128-bit integer value. |
|
|
277
|
+
| `__hash__` | Hashable (usable as dict key / set member). |
|
|
278
|
+
| `__eq__`, `__lt__`, etc. | Full rich comparison support. |
|
|
279
|
+
|
|
280
|
+
### Serialization
|
|
281
|
+
|
|
282
|
+
| Method | Description |
|
|
283
|
+
|---|---|
|
|
284
|
+
| `marshal_binary()` | Raw 16-byte data. |
|
|
285
|
+
| `marshal_text()` | 26-byte ASCII encoded string. |
|
|
286
|
+
| `marshal_json()` | JSON-encoded quoted string. |
|
|
287
|
+
| `ULID.unmarshal_binary(data)` | Parse from 16 bytes. |
|
|
288
|
+
| `ULID.unmarshal_text(data)` | Parse from 26-char string/bytes. |
|
|
289
|
+
| `ULID.unmarshal_json(data)` | Parse from JSON string. |
|
|
290
|
+
|
|
291
|
+
### Time Helpers
|
|
292
|
+
|
|
293
|
+
| Function | Description |
|
|
294
|
+
|---|---|
|
|
295
|
+
| `now()` | Current UTC Unix milliseconds. |
|
|
296
|
+
| `timestamp(dt)` | Convert `datetime` to Unix ms. |
|
|
297
|
+
| `time(ms)` | Convert Unix ms to `datetime`. |
|
|
298
|
+
| `max_time()` | Maximum encodable timestamp (year 10889). |
|
|
299
|
+
|
|
300
|
+
### Entropy
|
|
301
|
+
|
|
302
|
+
| Function | Description |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `default_entropy()` | Process-global thread-safe monotonic entropy (`os.urandom`). |
|
|
305
|
+
| `monotonic(r, inc)` | Create a monotonic entropy source wrapping any callable. |
|
|
306
|
+
|
|
307
|
+
### Generator
|
|
308
|
+
|
|
309
|
+
| Function/Method | Description |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `new_generator(*opts)` | Create a generator with options. |
|
|
312
|
+
| `with_node_id(id)` | Embed a 16-bit node ID for distributed uniqueness. |
|
|
313
|
+
| `with_entropy(r)` | Use a custom entropy source. |
|
|
314
|
+
| `with_prefix(p)` | Set a default prefix. |
|
|
315
|
+
| `gen.make()` | Generate a ULID. Thread-safe. |
|
|
316
|
+
| `gen.make_prefixed(p)` | Generate a prefixed ULID string. |
|
|
317
|
+
| `gen.new(ms)` | Generate with explicit timestamp. |
|
|
318
|
+
| `gen.node_id()` | Get `(node_id, has_node)`. |
|
|
319
|
+
|
|
320
|
+
## Contributing
|
|
321
|
+
|
|
322
|
+
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
|
|
323
|
+
Don't forget to give the project a star! Thanks again!
|
|
324
|
+
|
|
325
|
+
1. Fork the Project
|
|
326
|
+
2. Create your Feature Branch (`git checkout -b gh-username/my-amazing-feature`)
|
|
327
|
+
3. Commit your Changes (`git commit -m 'Add my amazing feature'`)
|
|
328
|
+
4. Push to the Branch (`git push origin gh-username/my-amazing-feature`)
|
|
329
|
+
5. Open a Pull Request
|
|
330
|
+
|
|
331
|
+
## License
|
|
332
|
+
|
|
333
|
+
MIT. See [LICENSE](LICENSE).
|