Decorator-driven CLI and database toolkit for Pydantic and SQLAlchemy.
Project description
Decorates
Decorates is a production-oriented toolkit for two common Python surfaces:
functionals.clifor module-first command registration, typed arguments, and built-in helpfunctionals.dbfor Pydantic model persistence and additive schema operations on SQLAlchemy
The package emphasizes explicit APIs, predictable behavior, and test-backed reliability.
Install
pip install functionals
Quick Start Guide
- Build one CLI command with a decorator.
- Build one DB model with a decorator.
- Use
Model.objectsfor CRUD.
CLI in 60 seconds
import functionals.cli as cli
import functionals.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 functionals.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
functionals.cli
- Register functions with module-level decorators:
@register,@argument,@option. - Run command handlers through the module registry via
functionals.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
functionals.cli.*.
functionals.db
- Register
BaseModelclasses with@database_registry(...). - Access all persistence through
Model.objects. id: int | None = Nonegives 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
SchemaErrorfor cleaner, predictable error handling. - Operational logs use standard Python logging namespaces under
functionals.db.*. - DB exceptions provide structured metadata (
exc.context,exc.to_dict()) for production diagnostics.
functionals.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/functionals/db/USAGE.md - CLI source API:
src/functionals/cli - DB source API:
src/functionals/db
Requirements
- Python 3.10+
pydantic>=2.0sqlalchemy>=2.0
Testing
- Default
pytestincludes SQLite plus PostgreSQL/MySQL rename-state integration tests. - Start Docker Desktop (or another Docker engine) before running tests so
docker-compose.test-db.ymlservices can boot. - The functionals 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file decorates-4.1.0.tar.gz.
File metadata
- Download URL: decorates-4.1.0.tar.gz
- Upload date:
- Size: 51.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db3af9a88818c81c540f7a6824443a8cc250801a2e7f989d05d3b686075f463f
|
|
| MD5 |
b1b7d8aedb21a9acffc33e48ae06902b
|
|
| BLAKE2b-256 |
320afc8f21d6d02cbd345c807945c2e0f6423eb0eccdaafc8927617e5e4e8a57
|
Provenance
The following attestation bundles were made for decorates-4.1.0.tar.gz:
Publisher:
publish.yml on nexustech101/functionals
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
decorates-4.1.0.tar.gz -
Subject digest:
db3af9a88818c81c540f7a6824443a8cc250801a2e7f989d05d3b686075f463f - Sigstore transparency entry: 1314528060
- Sigstore integration time:
-
Permalink:
nexustech101/functionals@798baf8613fdd80e5e11d1104fe7eb8a9d77ac36 -
Branch / Tag:
refs/tags/v4.1.1 - Owner: https://github.com/nexustech101
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@798baf8613fdd80e5e11d1104fe7eb8a9d77ac36 -
Trigger Event:
release
-
Statement type:
File details
Details for the file decorates-4.1.0-py3-none-any.whl.
File metadata
- Download URL: decorates-4.1.0-py3-none-any.whl
- Upload date:
- Size: 49.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3dd5d469423faa383e78df00ab0e6733a6166c56679362b1944398651e2cc542
|
|
| MD5 |
931bc3056a726100530382c559234642
|
|
| BLAKE2b-256 |
7ccb71deb9eac6547823af74ff88b4823490e763299ef7b323eede98dbe06a98
|
Provenance
The following attestation bundles were made for decorates-4.1.0-py3-none-any.whl:
Publisher:
publish.yml on nexustech101/functionals
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
decorates-4.1.0-py3-none-any.whl -
Subject digest:
3dd5d469423faa383e78df00ab0e6733a6166c56679362b1944398651e2cc542 - Sigstore transparency entry: 1314528119
- Sigstore integration time:
-
Permalink:
nexustech101/functionals@798baf8613fdd80e5e11d1104fe7eb8a9d77ac36 -
Branch / Tag:
refs/tags/v4.1.1 - Owner: https://github.com/nexustech101
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@798baf8613fdd80e5e11d1104fe7eb8a9d77ac36 -
Trigger Event:
release
-
Statement type: