Skip to main content

Decorator-driven persistence registry for Pydantic models and CLI tooling

Project description

registers

Decorator-driven tooling for Python:

  • registers.cli for ergonomic command-line apps
  • registers.db for Pydantic + SQLAlchemy persistence

The philosophy is simple: minimal setup, predictable behavior, and a fast path to shipping.

Install

pip install registers

Quick Start Guide

  1. Build one CLI command with a decorator.
  2. Build one DB model with a decorator.
  3. Use Model.objects for CRUD.

CLI in 60 seconds

from registers.cli import CommandRegistry

cli = CommandRegistry()

# ── built-in help alias ────────────────────────────────────────────────────

@cli.register(
    options=["-g", "--greet"],
    name="greet",
    description="Greet someone",
)
def greet(name: str) -> str:
    return f"Hello, {name}!"

@cli.register(
    options=["-h", "--help"],
    name="help",
    description="List all registered commands",
)
def list_clis() -> None:
    cli.list_clis()


if __name__ == "__main__":
    cli.run()
python app.py greet Alice
python app.py --greet Alice
python app.py -g Alice
python app.py g Alice

python app.py help
python app.py --help
python app.py -h
python app.py h
Hello, Alice!

Available commands:
  greet [-g, --greet]: Greet someone
  help [-h, --help]: List all registered commands

Database + FastAPI in 5 minutes

from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from registers.db import (
    RecordNotFoundError,
    UniqueConstraintError,
    database_registry,
)

DB_URL = "sqlite:///shop.db"


@database_registry(DB_URL, table_name="customers", unique_fields=["email"])
class Customer(BaseModel):
    id: int | None = None
    name: str
    email: str


@database_registry(DB_URL, table_name="products")
class Product(BaseModel):
    id: int | None = None
    name: str
    price: float


@database_registry(DB_URL, table_name="orders")
class Order(BaseModel):
    id: int | None = None
    customer_id: int
    product_id: int
    quantity: int
    total: float


class CreateCustomer(BaseModel):
    name: str
    email: str


class CreateProduct(BaseModel):
    name: str
    price: float


class CreateOrder(BaseModel):
    customer_id: int
    product_id: int
    quantity: int


@asynccontextmanager
async def lifespan(app: FastAPI):
    for model in (Customer, Product, Order):
        model.create_schema()
    yield
    for model in (Customer, Product, Order):
        model.objects.dispose()


app = FastAPI(lifespan=lifespan)


@app.post("/customers", response_model=Customer, status_code=201)
def create_customer(payload: CreateCustomer):
    try:
        return Customer.objects.create(**payload.model_dump())
    except UniqueConstraintError:
        raise HTTPException(status_code=409, detail="Email already exists")


@app.get("/customers/{customer_id}", response_model=Customer)
def get_customer(customer_id: int):
    try:
        return Customer.objects.require(customer_id)
    except RecordNotFoundError:
        raise HTTPException(status_code=404, detail="Customer not found")


@app.post("/products", response_model=Product, status_code=201)
def create_product(payload: CreateProduct):
    return Product.objects.create(**payload.model_dump())


@app.post("/orders", response_model=Order, status_code=201)
def create_order(payload: CreateOrder):
    customer = Customer.objects.get(payload.customer_id)
    if customer is None:
        raise HTTPException(status_code=404, detail="Customer not found")

    product = Product.objects.get(payload.product_id)
    if product is None:
        raise HTTPException(status_code=404, detail="Product not found")

    return Order.objects.create(
        customer_id=customer.id,
        product_id=product.id,
        quantity=payload.quantity,
        total=product.price * payload.quantity,
    )


@app.get("/orders/desc", response_model=list[Order])
def list_orders_desc(limit: int = 20, offset: int = 0):  # Filter by oldest   (1, 2, 3...n)
    return Order.objects.filter(order_by="id", limit=limit, offset=offset)


@app.get("/orders/asc", response_model=list[Order])
def list_orders_asc(limit: int = 20, offset: int = 0):  # Filter by newest  (n...3, 2, 1)
    return Order.objects.filter(order_by="-id", limit=limit, offset=offset)

Core Concepts

registers.cli

  • Register functions as commands with @cli.register(...).
  • Type annotations drive argument parsing.
  • Optional command aliases with options=["-x", "--long"].
  • Optional DI (DIContainer) and middleware (MiddlewareChain).

registers.db

  • Register BaseModel classes with @database_registry(...).
  • Access all persistence through Model.objects.
  • id: int | None = None gives database-managed autoincrement IDs.
  • Schema helpers are available as class methods: create_schema, drop_schema, schema_exists, truncate.

registers.db Usage Snapshot

# Filtering operators
Order.objects.filter(total__gte=100)
Customer.objects.filter(email__ilike="%@example.com")
Order.objects.filter(quantity__in=[1, 2, 3])

# Sorting and pagination
Order.objects.filter(order_by="-id", limit=20, offset=0)

# Bulk writes
Product.objects.bulk_create([...])
Product.objects.bulk_upsert([...])

# Additive migration helpers
Customer.objects.ensure_column("phone", str | None, nullable=True)
Customer.objects.rename_table("customers_archive")

If your model contains a field named password, password values are automatically hashed on write, and instances receive verify_password(...).

Documentation

  • DB guide: src/registers/db/USAGE.md
  • CLI source API: src/registers/cli
  • DB source API: src/registers/db

Requirements

  • Python 3.10+
  • pydantic>=2.0
  • sqlalchemy>=2.0

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

registers-2.2.0.tar.gz (48.4 kB view details)

Uploaded Source

Built Distribution

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

registers-2.2.0-py3-none-any.whl (42.2 kB view details)

Uploaded Python 3

File details

Details for the file registers-2.2.0.tar.gz.

File metadata

  • Download URL: registers-2.2.0.tar.gz
  • Upload date:
  • Size: 48.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for registers-2.2.0.tar.gz
Algorithm Hash digest
SHA256 ffc42f63e202c39313a23abb400a799ab4a63ee31a61b93b77431391a3b42694
MD5 256d636ae05d3709423e9ad2cd05228f
BLAKE2b-256 84e3533bd94929d1f4ff42c459a0f3871b4e7958e5d53a555bd743b9b72816a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for registers-2.2.0.tar.gz:

Publisher: publish.yml on nexustech101/registers

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file registers-2.2.0-py3-none-any.whl.

File metadata

  • Download URL: registers-2.2.0-py3-none-any.whl
  • Upload date:
  • Size: 42.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for registers-2.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5327de023e54b5c4c7414f2649dd4780e254e57c2c9c6e0dfa31ae5df9dcaeb5
MD5 4cede2160a582020653df0f8f7f7e34c
BLAKE2b-256 d63d8bb2de1bf02ee76869363bc969fdbe00e6be5c2a7934bb3b02186285a551

See more details on using hashes here.

Provenance

The following attestation bundles were made for registers-2.2.0-py3-none-any.whl:

Publisher: publish.yml on nexustech101/registers

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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