Skip to main content

Universal Prefixed Literal IDs - type-safe, human-readable identifiers

Project description

UPLID

██╗   ██╗██████╗ ██╗     ██╗██████╗
██║   ██║██╔══██╗██║     ██║██╔══██╗
██║   ██║██████╔╝██║     ██║██║  ██║
██║   ██║██╔═══╝ ██║     ██║██║  ██║
╚██████╔╝██║     ███████╗██║██████╔╝
 ╚═════╝ ╚═╝     ╚══════╝╚═╝╚═════╝

Stripe-style IDs for Python.

# Before: WTF is this?
"550e8400-e29b-41d4-a716-446655440000"

# After: It's a user.
"usr_0M3xL9kQ7vR2nP5wY1jZ4c"

CI PyPI Python Coverage

Install

pip install uplid

Requires Python 3.14+ and Pydantic 2.10+.

Quick Start

>>> from uplid import UPLID
>>> UPLID.generate("usr")
usr_0M3xL9kQ7vR2nP5wY1jZ4c
>>> UPLID.generate("ord")
ord_7x9KmNpQrStUvWxYz012Ab

Why UPLID?

Debuggable - See usr_ in your logs and instantly know it's a user, not an order, not a session, not a mystery.

# Your logs now:
"User usr_0M3xL9kQ7vR2nP5wY1jZ4c created order ord_1a2B3c4D5e6F7g..."

# vs the nightmare:
"User 550e8400-e29b-41d4... created order 7c9e6679-7425-40de..."

Type-safe - Your type checker catches user_id = order_id mistakes before they hit production.

UserId = UPLID[Literal["usr"]]
OrgId = UPLID[Literal["org"]]

def get_user(user_id: UserId) -> User: ...

get_user(org_id)  # Type error! Caught by mypy/pyright/ty

Time-sortable - Built on UUIDv7. Sort by ID = sort by creation time. No extra column needed.

URL-safe - 26 characters, no special characters, no encoding. usr_0M3xL9kQ7vR2nP5wY1jZ4c

Minimal dependencies - Just Pydantic. UUID generation uses Python 3.14's stdlib uuid7().

Inspired by Stripe's prefixed IDs (sk_live_..., cus_..., pi_...) - the same pattern trusted by millions of API calls daily.

Pydantic Integration

from typing import Literal
from pydantic import BaseModel, Field
from uplid import UPLID, factory

UserId = UPLID[Literal["usr"]]

class User(BaseModel):
    id: UserId = Field(default_factory=factory(UserId))
    name: str

user = User(name="Alice")
user.model_dump()  # {"id": "usr_0M3xL9kQ7vR2nP5wY1jZ4c", "name": "Alice"}

User(id="org_xxx...", name="Bad")  # ValidationError: wrong prefix

FastAPI Integration

from typing import Annotated, Literal
from fastapi import Depends, FastAPI, HTTPException
from uplid import UPLID, UPLIDError, parse

UserId = UPLID[Literal["usr"]]
parse_user_id = parse(UserId)

def validate_user_id(user_id: str) -> UserId:
    try:
        return parse_user_id(user_id)
    except UPLIDError as e:
        raise HTTPException(422, str(e)) from None

@app.get("/users/{user_id}")
def get_user(user_id: Annotated[UserId, Depends(validate_user_id)]) -> User:
    ...  # user_id is validated and typed

SQLAlchemy Integration

from uplid import UPLID, factory
from uplid.sqlalchemy import uplid_column

UserId = UPLID[Literal["usr"]]

class User(Base):
    __tablename__ = "users"
    id: Mapped[UserId] = uplid_column(UserId, primary_key=True)
    name: Mapped[str]

# Stores as TEXT, returns as UPLID objects
user = session.execute(select(User)).scalar_one()
user.id.prefix    # "usr"
user.id.datetime  # When the ID was created

SQLModel Integration

from uplid import UPLID, factory
from uplid.sqlalchemy import uplid_field

UserId = UPLID[Literal["usr"]]

class User(SQLModel, table=True):
    id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)
    name: str

user.model_dump()  # {"id": "usr_...", "name": "Alice"} - Pydantic just works

Prefix Rules

Prefixes must be snake_case: lowercase letters and single underscores, cannot start/end with underscore, max 64 characters.

Examples: usr, api_key, org_member, sk_live

API Reference

UPLID[PREFIX]

uid = UPLID.generate("usr")                              # Generate new
uid = UPLID.from_string("usr_0M3xL9kQ7vR2nP5wY1jZ4c", "usr")  # Parse

uid.prefix      # "usr"
uid.uid         # UUID object
uid.base62_uid  # "0M3xL9kQ7vR2nP5wY1jZ4c"
uid.datetime    # When created (from UUIDv7)
uid.timestamp   # Unix timestamp

factory(UPLIDType) / parse(UPLIDType)

UserId = UPLID[Literal["usr"]]
UserIdFactory = factory(UserId)  # For Pydantic default_factory
parse_user_id = parse(UserId)    # For manual parsing, raises UPLIDError

UPLIDType

Protocol for functions accepting any UPLID:

def log_entity(id: UPLIDType) -> None:
    print(f"{id.prefix} created at {id.datetime}")

uplid_column / uplid_field

from uplid.sqlalchemy import uplid_column, uplid_field

# SQLAlchemy
id: Mapped[UserId] = uplid_column(UserId, primary_key=True)

# SQLModel
id: UserId = uplid_field(UserId, default_factory=factory(UserId), primary_key=True)

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

uplid-1.1.1.tar.gz (9.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

uplid-1.1.1-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

File details

Details for the file uplid-1.1.1.tar.gz.

File metadata

  • Download URL: uplid-1.1.1.tar.gz
  • Upload date:
  • Size: 9.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for uplid-1.1.1.tar.gz
Algorithm Hash digest
SHA256 82c99130170f313a8430c47a5fa0396328d9416367c07a40ad46d94a78f0588f
MD5 47c73d51e71176ce0455a5c6b1fe7289
BLAKE2b-256 98c7d7147af5defc340705c8142fb0de213e66c89e14325593e10dd955751d69

See more details on using hashes here.

File details

Details for the file uplid-1.1.1-py3-none-any.whl.

File metadata

  • Download URL: uplid-1.1.1-py3-none-any.whl
  • Upload date:
  • Size: 10.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for uplid-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c3a818cefccfc7614cfa973428641ba0a5e7093388e751c226ca93ac764e69b1
MD5 cbbfdb2916fe8a2ac8fca88b74dcab8f
BLAKE2b-256 3b1152e97a0a55364a9dde20460b9e3f0b5e00626e96deab1098e14e7e5c93c8

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page