Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:10 +08:00
commit 657f1e3da3
29 changed files with 2738 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
# Data Validation Templates
Copy-paste templates for common data validation patterns.
## Available Templates
### Pydantic Model Template
**File**: [pydantic-model.py](pydantic-model.py)
Complete Pydantic v2 model template with:
- Field definitions with constraints
- Custom validators (@field_validator, @model_validator)
- model_config configuration
- Nested models
- Documentation
**Use when**: Starting a new API request/response schema.
---
### SQLModel Template
**File**: [sqlmodel-model.py](sqlmodel-model.py)
Complete SQLModel database template with:
- Table configuration
- Field definitions with PostgreSQL types
- Multi-tenant pattern (tenant_id)
- Timestamps (created_at, updated_at)
- Indexes and constraints
- Relationships
**Use when**: Creating a new database table.
---
### FastAPI Endpoint Template
**File**: [fastapi-endpoint.py](fastapi-endpoint.py)
Complete FastAPI endpoint template with:
- Router configuration
- Pydantic request/response schemas
- Dependency injection (session, tenant_id, user_id)
- Validation error handling
- Database operations
- Multi-tenant isolation
**Use when**: Creating a new API endpoint with validation.
---
## Quick Start
1. **Copy template** to your project
2. **Rename** model/endpoint appropriately
3. **Customize** fields and validators
4. **Test** with comprehensive test cases
## Navigation
- **Examples**: [Examples Index](../examples/INDEX.md)
- **Reference**: [Reference Index](../reference/INDEX.md)
- **Main Agent**: [data-validator.md](../data-validator.md)
---
Return to [main agent](../data-validator.md)

View File

@@ -0,0 +1,311 @@
"""
FastAPI Endpoint Template
Copy this template to create new API endpoints with validation.
Replace {ModelName}, {endpoint_prefix}, and {table_name} with your actual values.
"""
from fastapi import APIRouter, HTTPException, Depends, status
from sqlmodel import Session, select
from pydantic import ValidationError
from typing import List
from uuid import UUID
# Import your models and schemas
from app.models.{model_name} import {ModelName}
from app.schemas.{model_name} import (
{ModelName}CreateSchema,
{ModelName}UpdateSchema,
{ModelName}ResponseSchema
)
from app.database import get_session
from app.auth import get_current_user, get_current_tenant_id
# Create router
# -------------
router = APIRouter(
prefix="/api/{endpoint_prefix}",
tags=["{endpoint_prefix}"]
)
# Create Endpoint
# --------------
@router.post(
"/",
response_model={ModelName}ResponseSchema,
status_code=status.HTTP_201_CREATED,
summary="Create new {ModelName}",
description="Create a new {ModelName} with validation"
)
async def create_{model_name}(
data: {ModelName}CreateSchema,
session: Session = Depends(get_session),
user_id: UUID = Depends(get_current_user),
tenant_id: UUID = Depends(get_current_tenant_id)
):
"""
Create new {ModelName}.
Validates:
- Request data against Pydantic schema
- Business rules (if any)
- Uniqueness constraints
- Multi-tenant isolation
Returns:
{ModelName}ResponseSchema: Created {ModelName}
Raises:
HTTPException 409: If duplicate exists
HTTPException 422: If validation fails
"""
# Check for duplicates (if applicable)
existing = session.exec(
select({ModelName})
.where({ModelName}.email == data.email)
.where({ModelName}.tenant_id == tenant_id)
).first()
if existing:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="{ ModelName} with this email already exists"
)
# Create instance
instance = {ModelName}(
**data.model_dump(),
user_id=user_id,
tenant_id=tenant_id
)
session.add(instance)
session.commit()
session.refresh(instance)
return {ModelName}ResponseSchema.model_validate(instance)
# Read Endpoints
# -------------
@router.get(
"/{id}",
response_model={ModelName}ResponseSchema,
summary="Get {ModelName} by ID"
)
async def get_{model_name}(
id: UUID,
session: Session = Depends(get_session),
tenant_id: UUID = Depends(get_current_tenant_id)
):
"""
Get {ModelName} by ID with tenant isolation.
Returns:
{ModelName}ResponseSchema: Found {ModelName}
Raises:
HTTPException 404: If not found
"""
instance = session.exec(
select({ModelName})
.where({ModelName}.id == id)
.where({ModelName}.tenant_id == tenant_id)
).first()
if not instance:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="{ModelName} not found"
)
return {ModelName}ResponseSchema.model_validate(instance)
@router.get(
"/",
response_model=List[{ModelName}ResponseSchema],
summary="List {ModelName}s"
)
async def list_{model_name}s(
skip: int = 0,
limit: int = 100,
status: str | None = None,
session: Session = Depends(get_session),
tenant_id: UUID = Depends(get_current_tenant_id)
):
"""
List {ModelName}s with pagination and filtering.
Args:
skip: Number of records to skip (default 0)
limit: Maximum records to return (default 100, max 1000)
status: Filter by status (optional)
Returns:
List[{ModelName}ResponseSchema]: List of {ModelName}s
"""
# Build query
statement = (
select({ModelName})
.where({ModelName}.tenant_id == tenant_id)
.offset(skip)
.limit(min(limit, 1000))
)
# Apply filters
if status:
statement = statement.where({ModelName}.status == status)
# Execute
results = session.exec(statement).all()
return [
{ModelName}ResponseSchema.model_validate(item)
for item in results
]
# Update Endpoint
# --------------
@router.patch(
"/{id}",
response_model={ModelName}ResponseSchema,
summary="Update {ModelName}"
)
async def update_{model_name}(
id: UUID,
data: {ModelName}UpdateSchema,
session: Session = Depends(get_session),
tenant_id: UUID = Depends(get_current_tenant_id)
):
"""
Update {ModelName} with partial data.
Only provided fields are updated.
Returns:
{ModelName}ResponseSchema: Updated {ModelName}
Raises:
HTTPException 404: If not found
HTTPException 422: If validation fails
"""
# Get existing
instance = session.exec(
select({ModelName})
.where({ModelName}.id == id)
.where({ModelName}.tenant_id == tenant_id)
).first()
if not instance:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="{ModelName} not found"
)
# Update fields
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(instance, field, value)
# Update timestamp
instance.updated_at = datetime.utcnow()
session.add(instance)
session.commit()
session.refresh(instance)
return {ModelName}ResponseSchema.model_validate(instance)
# Delete Endpoint
# --------------
@router.delete(
"/{id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete {ModelName}"
)
async def delete_{model_name}(
id: UUID,
session: Session = Depends(get_session),
tenant_id: UUID = Depends(get_current_tenant_id)
):
"""
Delete {ModelName}.
Soft delete by setting is_active = False.
For hard delete, use session.delete() instead.
Raises:
HTTPException 404: If not found
"""
instance = session.exec(
select({ModelName})
.where({ModelName}.id == id)
.where({ModelName}.tenant_id == tenant_id)
).first()
if not instance:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="{ModelName} not found"
)
# Soft delete
instance.is_active = False
instance.updated_at = datetime.utcnow()
session.add(instance)
session.commit()
# For hard delete:
# session.delete(instance)
# session.commit()
# Error Handling
# -------------
@router.exception_handler(ValidationError)
async def validation_exception_handler(request, exc: ValidationError):
"""Handle Pydantic validation errors."""
errors = {}
for error in exc.errors():
field = '.'.join(str(loc) for loc in error['loc'])
message = error['msg']
if field not in errors:
errors[field] = []
errors[field].append(message)
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content={
'success': False,
'error': 'validation_error',
'message': 'Request validation failed',
'errors': errors
}
)
# Register Router
# --------------
# In your main FastAPI app:
# from app.api.{endpoint_prefix} import router as {model_name}_router
# app.include_router({model_name}_router)

View File

@@ -0,0 +1,231 @@
"""
Pydantic Model Template
Copy this template to create new Pydantic validation models.
Replace {ModelName}, {field_name}, and {FieldType} with your actual values.
"""
from pydantic import BaseModel, Field, field_validator, model_validator, EmailStr, HttpUrl
from typing import Optional, List, Literal
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID
from enum import Enum
# Optional: Define enums for categorical fields
class {ModelName}Status(str, Enum):
"""Status enum for {ModelName}."""
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class {ModelName}CreateSchema(BaseModel):
"""
Schema for creating a {ModelName}.
This schema defines the data contract for API requests.
All validation rules are enforced here.
"""
# Required string field with length constraints
name: str = Field(
...,
min_length=1,
max_length=100,
description="Display name",
examples=["Example Name"]
)
# Email field with built-in validation
email: EmailStr = Field(
...,
description="Email address"
)
# Optional string field
description: Optional[str] = Field(
None,
max_length=500,
description="Optional description"
)
# Integer with range constraints
quantity: int = Field(
...,
ge=1,
le=999,
description="Quantity (1-999)"
)
# Decimal for currency (recommended over float)
price: Decimal = Field(
...,
gt=0,
max_digits=10,
decimal_places=2,
description="Price in USD"
)
# Date field
start_date: date = Field(
...,
description="Start date"
)
# Enum field
status: {ModelName}Status = Field(
default={ModelName}Status.PENDING,
description="Current status"
)
# List field with constraints
tags: List[str] = Field(
default_factory=list,
max_length=10,
description="Associated tags"
)
# Literal type (inline enum)
priority: Literal['low', 'medium', 'high'] = Field(
default='medium',
description="Priority level"
)
# Field Validators
# ---------------
@field_validator('name')
@classmethod
def validate_name(cls, v: str) -> str:
"""Validate name format."""
if not v.strip():
raise ValueError('Name cannot be empty')
return v.strip()
@field_validator('tags')
@classmethod
def validate_unique_tags(cls, v: List[str]) -> List[str]:
"""Ensure tags are unique."""
if len(v) != len(set(v)):
raise ValueError('Duplicate tags not allowed')
return [tag.lower() for tag in v]
# Model Validators (cross-field validation)
# ----------------------------------------
@model_validator(mode='after')
def validate_business_rules(self):
"""Validate business rules across fields."""
# Example: Ensure high-priority items have descriptions
if self.priority == 'high' and not self.description:
raise ValueError('High priority items must have description')
return self
# Configuration
# ------------
model_config = {
# Strip whitespace from strings
'str_strip_whitespace': True,
# Validate on assignment
'validate_assignment': True,
# JSON schema examples
'json_schema_extra': {
'examples': [{
'name': 'Example {ModelName}',
'email': 'example@company.com',
'description': 'An example description',
'quantity': 5,
'price': '99.99',
'start_date': '2024-01-01',
'status': 'active',
'tags': ['tag1', 'tag2'],
'priority': 'medium'
}]
}
}
class {ModelName}UpdateSchema(BaseModel):
"""
Schema for updating a {ModelName}.
All fields are optional for partial updates.
"""
name: Optional[str] = Field(None, min_length=1, max_length=100)
email: Optional[EmailStr] = None
description: Optional[str] = Field(None, max_length=500)
quantity: Optional[int] = Field(None, ge=1, le=999)
price: Optional[Decimal] = Field(None, gt=0, max_digits=10, decimal_places=2)
status: Optional[{ModelName}Status] = None
tags: Optional[List[str]] = Field(None, max_length=10)
priority: Optional[Literal['low', 'medium', 'high']] = None
model_config = {
'str_strip_whitespace': True,
'validate_assignment': True
}
class {ModelName}ResponseSchema(BaseModel):
"""
Schema for {ModelName} responses.
Includes all fields plus auto-generated ones (id, timestamps).
"""
id: UUID
name: str
email: str
description: Optional[str]
quantity: int
price: Decimal
start_date: date
status: str
tags: List[str]
priority: str
# Auto-generated fields
created_at: datetime
updated_at: datetime
model_config = {
# Enable ORM mode for SQLModel compatibility
'from_attributes': True
}
# Usage Example
# -------------
if __name__ == '__main__':
# Valid data
data = {
'name': 'Test Item',
'email': 'test@example.com',
'quantity': 10,
'price': '49.99',
'start_date': '2024-01-01',
'status': 'active',
'tags': ['electronics', 'featured']
}
# Create instance
item = {ModelName}CreateSchema(**data)
print(f"Created: {item.model_dump_json()}")
# Validation will raise errors for invalid data
try:
invalid_data = {**data, 'quantity': 0} # Invalid quantity
{ModelName}CreateSchema(**invalid_data)
except ValidationError as e:
print(f"Validation error: {e.errors()}")

View File

@@ -0,0 +1,246 @@
"""
SQLModel Database Model Template
Copy this template to create new database models.
Replace {ModelName} and {table_name} with your actual values.
"""
from sqlmodel import SQLModel, Field, Relationship
from datetime import datetime
from uuid import UUID, uuid4
from decimal import Decimal
from typing import Optional, List
from enum import Enum
# Optional: Define enum for status field
class {ModelName}Status(str, Enum):
"""Status enum for {ModelName}."""
ACTIVE = 'active'
INACTIVE = 'inactive'
PENDING = 'pending'
class {ModelName}(SQLModel, table=True):
"""
{ModelName} database model.
Represents {table_name} table in PostgreSQL.
Includes multi-tenant isolation via tenant_id.
"""
__tablename__ = '{table_name}'
# Primary Key
# -----------
id: UUID = Field(
default_factory=uuid4,
primary_key=True,
description="Unique identifier"
)
# Multi-Tenant Isolation (REQUIRED)
# ---------------------------------
tenant_id: UUID = Field(
foreign_key="tenants.id",
index=True,
description="Tenant for RLS isolation"
)
# Foreign Keys
# -----------
user_id: UUID = Field(
foreign_key="users.id",
index=True,
description="Owner user ID"
)
# Data Fields
# ----------
# String fields with length constraints
name: str = Field(
max_length=100,
index=True,
description="Display name"
)
email: str = Field(
max_length=255,
unique=True,
index=True,
description="Email address"
)
# Optional text field
description: Optional[str] = Field(
default=None,
max_length=500,
description="Optional description"
)
# Integer field
quantity: int = Field(
description="Quantity"
)
# Decimal for currency
price: Decimal = Field(
max_digits=10,
decimal_places=2,
description="Price in USD"
)
# Enum/Status field
status: str = Field(
default='pending',
max_length=20,
index=True,
description="Current status"
)
# Boolean field
is_active: bool = Field(
default=True,
description="Active flag"
)
# JSON field (stored as JSONB in PostgreSQL)
metadata: Optional[dict] = Field(
default=None,
sa_column_kwargs={"type_": "JSONB"},
description="Additional metadata"
)
# Timestamps (REQUIRED)
# --------------------
created_at: datetime = Field(
default_factory=datetime.utcnow,
nullable=False,
description="Creation timestamp"
)
updated_at: datetime = Field(
default_factory=datetime.utcnow,
nullable=False,
description="Last update timestamp"
)
# Relationships
# ------------
# One-to-many: This model has many related items
# items: List["RelatedItem"] = Relationship(back_populates="{model_name}")
# Many-to-one: This model belongs to a user
# user: Optional["User"] = Relationship(back_populates="{model_name}s")
# Related model example
# ---------------------
class {ModelName}Item(SQLModel, table=True):
"""Related items for {ModelName}."""
__tablename__ = '{table_name}_items'
id: UUID = Field(default_factory=uuid4, primary_key=True)
# Foreign key to parent
{model_name}_id: UUID = Field(
foreign_key="{table_name}.id",
index=True
)
# Item fields
name: str = Field(max_length=100)
quantity: int = Field(gt=0)
unit_price: Decimal = Field(max_digits=10, decimal_places=2)
created_at: datetime = Field(default_factory=datetime.utcnow)
# Relationship back to parent
{model_name}: Optional[{ModelName}] = Relationship(back_populates="items")
# Update parent model with relationship
{ModelName}.items = Relationship(back_populates="{model_name}")
# Database Migration
# ------------------
"""
After creating this model, generate a migration:
```bash
# Using Alembic
alembic revision --autogenerate -m "create {table_name} table"
alembic upgrade head
```
Migration will create:
- Table {table_name}
- Indexes on tenant_id, user_id, name, email, status
- Unique constraint on email
- Foreign key constraints
- Timestamps with defaults
"""
# Row-Level Security (RLS)
# ------------------------
"""
Enable RLS for multi-tenant isolation:
```sql
-- Enable RLS
ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY;
-- Create policy
CREATE POLICY tenant_isolation ON {table_name}
USING (tenant_id = current_setting('app.tenant_id')::UUID);
-- Grant access
GRANT SELECT, INSERT, UPDATE, DELETE ON {table_name} TO app_user;
```
"""
# Usage Example
# -------------
if __name__ == '__main__':
from sqlmodel import Session, create_engine, select
# Create engine
engine = create_engine('postgresql://user:pass@localhost/db')
# Create tables
SQLModel.metadata.create_all(engine)
# Insert data
with Session(engine) as session:
item = {ModelName}(
tenant_id=uuid4(),
user_id=uuid4(),
name='Test Item',
email='test@example.com',
quantity=10,
price=Decimal('49.99'),
status='active'
)
session.add(item)
session.commit()
session.refresh(item)
print(f"Created {ModelName}: {item.id}")
# Query data
with Session(engine) as session:
statement = select({ModelName}).where({ModelName}.status == 'active')
results = session.exec(statement).all()
print(f"Found {len(results)} active items")