refactor: split table models into Base/Table/Public for proper FastAPI serialization
Add ProductBase, ProductPublic, ProductWithInventory and SkinConditionSnapshotBase, SkinConditionSnapshotPublic. Table models now inherit from their Base counterpart and override JSON fields with sa_column. All field_serializer hacks removed; FastAPI response models use the non-table Public classes so Pydantic coerces raw DB dicts → typed models cleanly. ProductCreate and SnapshotCreate now simply inherit their respective Base classes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
479be25112
commit
c09acc7c81
15 changed files with 225 additions and 198 deletions
|
|
@ -2,30 +2,33 @@ from datetime import date
|
|||
from typing import Optional
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlmodel import Session, SQLModel, select
|
||||
|
||||
from db import get_session
|
||||
from innercontext.api.utils import get_or_404
|
||||
from innercontext.models import (
|
||||
Product,
|
||||
ProductBase,
|
||||
ProductCategory,
|
||||
ProductInventory,
|
||||
ProductPublic,
|
||||
ProductWithInventory,
|
||||
SkinConcern,
|
||||
)
|
||||
from innercontext.models.enums import (
|
||||
AbsorptionSpeed,
|
||||
DayTime,
|
||||
PriceTier,
|
||||
SkinType,
|
||||
TextureType,
|
||||
)
|
||||
from innercontext.models.product import (
|
||||
ActiveIngredient,
|
||||
ProductContext,
|
||||
ProductEffectProfile,
|
||||
ProductInteraction,
|
||||
)
|
||||
from innercontext.models.enums import (
|
||||
AbsorptionSpeed,
|
||||
DayTime,
|
||||
PriceTier,
|
||||
TextureType,
|
||||
SkinType,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -35,57 +38,8 @@ router = APIRouter()
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ProductCreate(SQLModel):
|
||||
name: str
|
||||
brand: str
|
||||
line_name: Optional[str] = None
|
||||
sku: Optional[str] = None
|
||||
url: Optional[str] = None
|
||||
barcode: Optional[str] = None
|
||||
|
||||
category: ProductCategory
|
||||
recommended_time: DayTime
|
||||
|
||||
texture: Optional[TextureType] = None
|
||||
absorption_speed: Optional[AbsorptionSpeed] = None
|
||||
leave_on: bool
|
||||
|
||||
price_tier: Optional[PriceTier] = None
|
||||
size_ml: Optional[float] = None
|
||||
pao_months: Optional[int] = None
|
||||
|
||||
inci: list[str] = []
|
||||
actives: Optional[list[ActiveIngredient]] = None
|
||||
|
||||
recommended_for: list[SkinType] = []
|
||||
|
||||
targets: list[SkinConcern] = []
|
||||
contraindications: list[str] = []
|
||||
usage_notes: Optional[str] = None
|
||||
|
||||
fragrance_free: Optional[bool] = None
|
||||
essential_oils_free: Optional[bool] = None
|
||||
alcohol_denat_free: Optional[bool] = None
|
||||
pregnancy_safe: Optional[bool] = None
|
||||
|
||||
product_effect_profile: ProductEffectProfile = ProductEffectProfile()
|
||||
|
||||
ph_min: Optional[float] = None
|
||||
ph_max: Optional[float] = None
|
||||
|
||||
incompatible_with: Optional[list[ProductInteraction]] = None
|
||||
synergizes_with: Optional[list[str]] = None
|
||||
context_rules: Optional[ProductContext] = None
|
||||
|
||||
min_interval_hours: Optional[int] = None
|
||||
max_frequency_per_week: Optional[int] = None
|
||||
|
||||
is_medication: bool = False
|
||||
is_tool: bool = False
|
||||
needle_length_mm: Optional[float] = None
|
||||
|
||||
personal_tolerance_notes: Optional[str] = None
|
||||
personal_repurchase_intent: Optional[bool] = None
|
||||
class ProductCreate(ProductBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProductUpdate(SQLModel):
|
||||
|
|
@ -159,17 +113,12 @@ class InventoryUpdate(SQLModel):
|
|||
notes: Optional[str] = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Product routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@router.get("", response_model=list[Product])
|
||||
@router.get("", response_model=list[ProductPublic])
|
||||
def list_products(
|
||||
category: Optional[ProductCategory] = None,
|
||||
brand: Optional[str] = None,
|
||||
|
|
@ -205,7 +154,7 @@ def list_products(
|
|||
return products
|
||||
|
||||
|
||||
@router.post("", response_model=Product, status_code=201)
|
||||
@router.post("", response_model=ProductPublic, status_code=201)
|
||||
def create_product(data: ProductCreate, session: Session = Depends(get_session)):
|
||||
product = Product(
|
||||
id=uuid4(),
|
||||
|
|
@ -217,16 +166,18 @@ def create_product(data: ProductCreate, session: Session = Depends(get_session))
|
|||
return product
|
||||
|
||||
|
||||
@router.get("/{product_id}")
|
||||
@router.get("/{product_id}", response_model=ProductWithInventory)
|
||||
def get_product(product_id: UUID, session: Session = Depends(get_session)):
|
||||
product = get_or_404(session, Product, product_id)
|
||||
inventory = session.exec(select(ProductInventory).where(ProductInventory.product_id == product_id)).all()
|
||||
data = product.model_dump(mode="json")
|
||||
data["inventory"] = [item.model_dump(mode="json") for item in inventory]
|
||||
return data
|
||||
inventory = session.exec(
|
||||
select(ProductInventory).where(ProductInventory.product_id == product_id)
|
||||
).all()
|
||||
result = ProductWithInventory.model_validate(product, from_attributes=True)
|
||||
result.inventory = list(inventory)
|
||||
return result
|
||||
|
||||
|
||||
@router.patch("/{product_id}", response_model=Product)
|
||||
@router.patch("/{product_id}", response_model=ProductPublic)
|
||||
def update_product(
|
||||
product_id: UUID, data: ProductUpdate, session: Session = Depends(get_session)
|
||||
):
|
||||
|
|
@ -252,9 +203,7 @@ def delete_product(product_id: UUID, session: Session = Depends(get_session)):
|
|||
|
||||
|
||||
@router.get("/{product_id}/inventory", response_model=list[ProductInventory])
|
||||
def list_product_inventory(
|
||||
product_id: UUID, session: Session = Depends(get_session)
|
||||
):
|
||||
def list_product_inventory(product_id: UUID, session: Session = Depends(get_session)):
|
||||
get_or_404(session, Product, product_id)
|
||||
stmt = select(ProductInventory).where(ProductInventory.product_id == product_id)
|
||||
return session.exec(stmt).all()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue