Skip to main content

Decorator-driven CLI and database toolkit for Pydantic and SQLAlchemy.

Project description

Decorates

PyPI version Python versions License: MIT CLI DB Tests

Decorates is a production-oriented toolkit for two common Python surfaces:

  • decorates.cli for module-first command registration, typed arguments, and built-in help
  • decorates.db for Pydantic model persistence and additive schema operations on SQLAlchemy

The package emphasizes explicit APIs, predictable behavior, and test-backed reliability.

Install

pip install decorates

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

import decorates.cli as cli
import decorates.db as db
from pydantic import BaseModel

@db.database_registry("users.db", table_name="users", key_field="id")
class User(BaseModel):
    id: int | None = None
    name: str

@cli.register(name="add", description="Create a user")
@cli.argument("name", type=str)
@cli.option("--add")
@cli.option("-a")
def add_user(name: str) -> str:
    user = User(name=name)
    user.save()
    return f"Created user {user.id}: {user.name}"

@cli.register(name="list", description="List users")
@cli.option("--list")
@cli.option("-l")
def list_users() -> str:
    users = User.objects.all()
    if not users:
        return "No users found."
    return "\n".join(f"{u.id}: {u.name}" for u in users)

if __name__ == "__main__":
    cli.run()
python users.py add "Alice"
python users.py --add "Bob"
python users.py list
python users.py --help

Database + FastAPI in 5 minutes

from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from decorates.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

decorates.cli

  • Register functions with module-level decorators: @register, @argument, @option.
  • Run command handlers through the module registry via decorates.cli.run().
  • Support positional + named argument forms (for non-bool args), with bool flags as --flag.
  • Command aliases are declared with @option("-x") / @option("--long").
  • Built-in help command is always available: help, --help, and -h.
  • Runtime wraps unexpected handler crashes as CommandExecutionError (with original exception chaining).
  • Operational logs use standard Python logging namespaces under decorates.cli.*.

decorates.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.
  • Unexpected SQLAlchemy runtime failures are normalized into SchemaError for cleaner, predictable error handling.
  • Operational logs use standard Python logging namespaces under decorates.db.*.
  • DB exceptions provide structured metadata (exc.context, exc.to_dict()) for production diagnostics.

decorates.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")

After rename_table(...) succeeds, the same Model.objects manager and schema helpers are immediately bound to the new table name.

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

Documentation

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

Requirements

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

Testing

  • Default pytest includes SQLite plus PostgreSQL/MySQL rename-state integration tests.
  • Start Docker Desktop (or another Docker engine) before running tests so docker-compose.test-db.yml services can boot.
  • The decorates is backed by a rigorous, production-focused test suite (170+ tests) that covers unit, edge-case, and multi-dialect integration behavior.

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

decorates-4.0.6.tar.gz (54.3 kB view details)

Uploaded Source

Built Distribution

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

decorates-4.0.6-py3-none-any.whl (49.1 kB view details)

Uploaded Python 3

File details

Details for the file decorates-4.0.6.tar.gz.

File metadata

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

File hashes

Hashes for decorates-4.0.6.tar.gz
Algorithm Hash digest
SHA256 5e2fae3b124a821df1401402561b84d7d4084a17306f4523265c108ec3b87c87
MD5 bd8a431f2ba827e02cb70d2020b5a65d
BLAKE2b-256 9b5d60b525767b11c884339b13256ae6fbd2eec2dbff572a37aaec3a37ee302f

See more details on using hashes here.

Provenance

The following attestation bundles were made for decorates-4.0.6.tar.gz:

Publisher: publish.yml on nexustech101/decorates

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

File details

Details for the file decorates-4.0.6-py3-none-any.whl.

File metadata

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

File hashes

Hashes for decorates-4.0.6-py3-none-any.whl
Algorithm Hash digest
SHA256 c6e68f87cc9df93ee8cfa6d1af24b4b1fabb9a63a702ab8777ca99d3e7ecab00
MD5 ed8e5bf2e1850abdbf1449f4d7545c43
BLAKE2b-256 c79de1d62e887a1907919eeadacae03e1814907d3636ed3a7e68580cd4c0af0a

See more details on using hashes here.

Provenance

The following attestation bundles were made for decorates-4.0.6-py3-none-any.whl:

Publisher: publish.yml on nexustech101/decorates

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