46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
"""ULID generation (Section 35).
|
|
|
|
ULIDs are lexicographically sortable, timezone-independent identifiers
|
|
linking logs, commits and monitoring. Implemented without an external
|
|
dependency per the specification's "equivalent identifier" allowance.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import secrets
|
|
|
|
_ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" # Crockford base32
|
|
_RANDOM_LEN = 16
|
|
_TIME_LEN = 10
|
|
|
|
|
|
def _encode_time(timestamp_ms: int) -> str:
|
|
value = max(timestamp_ms, 0)
|
|
chars = [""] * _TIME_LEN
|
|
for i in range(_TIME_LEN - 1, -1, -1):
|
|
chars[i] = _ENCODING[value & 0x1F]
|
|
value >>= 5
|
|
return "".join(chars)
|
|
|
|
|
|
def _encode_random(count: int) -> str:
|
|
# 5 bits per char; gather enough random bytes then fold
|
|
bits = count * 5
|
|
nbytes = (bits + 7) // 8
|
|
raw = int.from_bytes(secrets.token_bytes(nbytes), "big")
|
|
chars = [""] * count
|
|
for i in range(count - 1, -1, -1):
|
|
chars[i] = _ENCODING[raw & 0x1F]
|
|
raw >>= 5
|
|
return "".join(chars)
|
|
|
|
|
|
def new_ulid(timestamp: dt.datetime | None = None) -> str:
|
|
"""Generate a ULID string, e.g. 01J7K8HYV84ZKFH1J4T9R8DZQN."""
|
|
moment = timestamp or dt.datetime.now(dt.UTC)
|
|
if moment.tzinfo is None:
|
|
moment = moment.replace(tzinfo=dt.UTC)
|
|
timestamp_ms = int(moment.timestamp() * 1000)
|
|
return _encode_time(timestamp_ms) + _encode_random(_RANDOM_LEN)
|