python-ulid

repository·main·Indexed 20 days ago

https://github.com/mdomke/python-ulid

A Python implementation of Universally Unique Lexicographically Sortable Identifiers (ULID). It provides 128-bit compatible, sortable, and URL-safe identifiers consisting of a 48-bit timestamp and 80 bits of randomness, encoded as 26-character Crockford's Base32 strings. The library includes a ULIDGenerator with configurable MonotonicityPolicies (StrictMonotonicPolicy, LaxMonotonicPolicy, and PureRandomPolicy), Pydantic integration, and a command-line interface for building and inspecting ULIDs.

Tokens
3.1K
Snippets
9
Records
16
Agent score
67%

What's inside python-ulid

  1. What is a ULID

    main

    A ULID (Universally Unique Lexicographically Sortable Identifier) is a 128-bit value designed to be immutable and hashable. It consists of two parts:

    1. Timestamp (48 bits): Represents epoch time in milliseconds.
    2. Randomness (80 bits): High-entropy random data.

    ULIDs are represented as 26-character strings using Crockford's Base32 encoding, which excludes ambiguous characters (like I, L, O, and U) to ensure readability.

  2. How ULID generators and monotonicity policies work

    main

    A ULIDGenerator is responsible for producing ULIDs by sampling a clock, sourcing entropy, and applying a monotonicity policy. While the default ULID() constructor uses a shared default_generator, you can create custom generators to control behavior.

    Monotonicity Policies

    When multiple ULIDs are generated within the same millisecond, the policy determines how the randomness component is handled:

    1. StrictMonotonicPolicy (Default): Increments the randomness by 1 for same-millisecond collisions. If the randomness is exhausted, it raises a ValueError.
    2. LaxMonotonicPolicy: Increments the randomness by 1 for same-millisecond collisions. If randomness is exhausted, it regenerates fresh randomness instead of raising an error.
    3. PureRandomPolicy: Ignores previous state and always draws fresh randomness. This maximizes entropy but sacrifices the guaranteed monotonic sort order within the same millisecond.

    Customizing the Global Generator

    You can override the global ulid.default_generator so that all calls to ULID() and ULID.from_* use your custom configuration.

    import ulid
    from ulid import ULID, ULIDGenerator, LaxMonotonicPolicy
    
    # 1. Using a custom generator instance
    generator = ULIDGenerator(policy=LaxMonotonicPolicy())
    new_ulid = generator.generate()
    
    # 2. Overriding the global default generator
    ulid.default_generator = ULIDGenerator(policy=LaxMonotonicPolicy())
    # Now all ULID() calls use the Lax policy
    new_ulid_global = ULID()
  3. Configure Monotonicity Policies for ULID generation

    main

    A MonotonicityPolicy determines how the randomness component of a ULID is resolved when multiple ULIDs are generated within the same millisecond. This ensures lexicographical sortability even during high-frequency generation.

    To use a policy, pass an instance of it to a ULIDGenerator. You can use the built-in policies or implement a custom one by subclassing BaseMonotonicPolicy and implementing the overflow behavior.

  4. Configure generator behavior with MonotonicityPolicy

    main

    You can control how the ULIDGenerator handles multiple identifiers generated within the same millisecond using different MonotonicityPolicy implementations. This ensures deterministic ordering and prevents sorting collisions.

    Available policies:

    • StrictMonotonicPolicy: Always increments the randomness component by 1 when a same-millisecond collision occurs. It will raise an overflow error if the randomness component is exhausted.
    • PureRandomPolicy: Ignores previous states and always generates fresh random bytes. This maximizes security and entropy but does not guarantee strict monotonic ordering within the same millisecond.
    • LaxMonotonicPolicy: Increments monotonically for same-millisecond collisions. If randomness is exhausted, it regenerates fresh randomness instead of raising an error or sleeping.
  5. How ULIDGenerator works

    main

    The ULIDGenerator is a stateful object responsible for creating new ULID instances. It orchestrates the following:

    • Timestamping: Sampling the system clock or an injected clock.
    • Entropy: Sourcing randomness for the identifier.
    • State Management: Tracking state to enforce Monotonicity rules.
    • Concurrency: Guaranteeing thread-safe generation across different execution contexts.
  6. Basic usage of ULID

    main

    You can create a new ULID object using the default constructor for the current timestamp, or use named constructors to specify a timestamp.

    Creating ULIDs

    • ULID(): Generates a ULID from the current time.
    • ULID.from_timestamp(ts): Generates a ULID from a UNIX epoch timestamp.
    • ULID.from_datetime(dt): Generates a ULID from a datetime.datetime object.

    Encoding and Conversion

    ULID objects can be converted to several formats:

    • str(ulid): Returns the 26-character Crockford's base32 string.
    • ulid.hex: Returns the hexadecimal string representation.
    • int(ulid): Returns the integer representation.
    • bytes(ulid): Returns the raw bytes.
    • ulid.to_uuid(): Returns a standard Python UUID object.

    Accessing Timestamp Data

    • ulid.timestamp: Returns the UNIX epoch timestamp.
    • ulid.datetime: Returns the timestamp as a datetime.datetime object in UTC.
    from ulid import ULID
    import time
    import datetime
    
    # Creation
    new_ulid = ULID()
    from_ts = ULID.from_timestamp(time.time())
    from_dt = ULID.from_datetime(datetime.datetime.now())
    
    # Conversion
    print(str(new_ulid))
    print(new_ulid.hex)
    print(int(new_ulid))
    print(bytes(new_ulid))
    print(new_ulid.to_uuid())
    
    # Timestamp access
    print(new_ulid.timestamp)
    print(new_ulid.datetime)
  7. Integrate ULID with Pydantic

    main

    The ULID class is compatible with Pydantic. You can use it as a type hint in your models. Pydantic will automatically validate strings as valid ULIDs.

    from pydantic import BaseModel
    from ulid import ULID
    
    class Model(BaseModel):
        ulid: ULID
    
    # Valid ULID string
    model = Model(ulid="DX89370400440532013000")  # OK
    
    # Invalid ULID string
    # model = Model(ulid="not-a-ulid")  # Raises ValidationError
  8. Generate ULIDs using the default generator

    main
    The ulid module provides a ULID class for creating Universally Unique Lexicographically Sortable Identifiers. By default, calling the ULID() constructor or using factory methods like ULID.from_bytes() or ULID.from_str() uses a shared module-level default_generator.
  9. Customize ULID generation with ULIDGenerator

    main
    While the default generator is sufficient for most use cases, you can instantiate your own ULIDGenerator to customize the clock, the randomness source, or the MonotonicityPolicy. This is useful when you need specific control over how identifiers are produced in a particular environment.
  10. Available Monotonicity Policies

    main

    The following policies are available to control how ULIDs behave when generated in the same millisecond:

    • StrictMonotonicPolicy: Ensures strict monotonicity.
    • LaxMonotonicPolicy: Provides a more relaxed approach to monotonicity.
    • PureRandomPolicy: Uses pure randomness without attempting to maintain monotonicity within the same millisecond.
    StrictMonotonicPolicy
    LaxMonotonicPolicy
    PureRandomPolicy
  11. Understand the Base32 Engine encoding

    main

    The library uses Crockford's Base32 translation layer to encode and decode the binary representation of ULIDs. It uses a restricted alphabet of 32 characters:

    0123456789ABCDEFGHJKMNPQRSTVWXYZ

    This alphabet specifically omits ambiguous characters such as I, L, O, and U to maximize human readability.

    0123456789ABCDEFGHJKMNPQRSTVWXYZ