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

About

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 letters and single underscores, cannot start/end with underscore, max 64 characters.

Examples: usr, api_key, org_member, sk_live, USR, ApiKey

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)

Credits

Created by ZVS

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.2.0.tar.gz (9.3 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.2.0-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: uplid-1.2.0.tar.gz
  • Upload date:
  • Size: 9.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","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.2.0.tar.gz
Algorithm Hash digest
SHA256 e5cb34b661f10fbe393a8a8873ac28b4028c3a49b8a8ad8d119c6f8eeb035e74
MD5 98f7863507a9f09ce5302f7297d69bea
BLAKE2b-256 e89b5eaaee81da85625150e2379469139002e97b7c2f54dcd01b6b7522c8680a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: uplid-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 10.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.10.2 {"installer":{"name":"uv","version":"0.10.2","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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2df980717a7a3a83a2f80b659df50dd9bce9374fe2850432b0c4b27deab9ab32
MD5 293f4d091300a4af9c55d69942477acc
BLAKE2b-256 cb52ecda7a88cc06a71c57f241ac2cbdebf20c2abf0ab87e01907e426c83c9d7

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