Public Access
feat: implement Phase 2 - Alembic migrations, Pydantic schemas, and real API endpoints
- Add initial Alembic migration with full PostgreSQL schema (enums, tables, indexes, constraints) - Add seed data migration with basic ingredients (70+) and family profile - Add Pydantic schemas for all models (FamilyProfile, Recipe, MealPlan, etc.) - Implement /api/profile endpoints (CRUD, family member management) - Implement /api/recipes endpoints (CRUD, ingredients, filtering) - Implement /api/meals endpoints (meal plans, voting, approval tokens) - Implement /api/pantry endpoints (CRUD for home pantry) - Implement /api/shopping-list endpoints (aggregation, print-ready HTML) - Implement /api/admin endpoints (scrape trigger, logs, stats) - Update ORIENTATION.md with Phase 2 progress
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List, Any
|
||||
from uuid import UUID
|
||||
from datetime import date, datetime
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class FamilyMemberRole(str, Enum):
|
||||
adult = "adult"
|
||||
child = "child"
|
||||
|
||||
|
||||
class MealType(str, Enum):
|
||||
breakfast = "breakfast"
|
||||
lunch = "lunch"
|
||||
dinner = "dinner"
|
||||
|
||||
|
||||
class MealPlanStatus(str, Enum):
|
||||
draft = "draft"
|
||||
pending_approval = "pending_approval"
|
||||
approved = "approved"
|
||||
locked = "locked"
|
||||
|
||||
|
||||
class MealPlanItemStatus(str, Enum):
|
||||
pending = "pending"
|
||||
approved = "approved"
|
||||
denied = "denied"
|
||||
swapped = "swapped"
|
||||
|
||||
|
||||
class DenialReason(str, Enum):
|
||||
too_expensive = "too_expensive"
|
||||
boring = "boring"
|
||||
disliked_ingredient = "disliked_ingredient"
|
||||
cultural = "cultural"
|
||||
other = "other"
|
||||
|
||||
|
||||
class NeverSuggestReason(str, Enum):
|
||||
allergy = "allergy"
|
||||
dislike = "dislike"
|
||||
tried_too_much = "tried_too_much"
|
||||
other = "other"
|
||||
|
||||
|
||||
class IngredientBase(BaseModel):
|
||||
name: str
|
||||
name_lower: str
|
||||
plural_name: Optional[str] = None
|
||||
aisle: Optional[str] = None
|
||||
typical_price: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
season_months: Optional[List[int]] = None
|
||||
|
||||
|
||||
class IngredientResponse(IngredientBase):
|
||||
id: UUID
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class IngredientCreate(IngredientBase):
|
||||
pass
|
||||
|
||||
|
||||
class FamilyMemberBase(BaseModel):
|
||||
name: str
|
||||
email: Optional[str] = None
|
||||
role: FamilyMemberRole
|
||||
likes_mushrooms: bool = False
|
||||
|
||||
|
||||
class FamilyMemberResponse(FamilyMemberBase):
|
||||
id: UUID
|
||||
family_profile_id: UUID
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FamilyMemberCreate(FamilyMemberBase):
|
||||
pass
|
||||
|
||||
|
||||
class FamilyProfileBase(BaseModel):
|
||||
name: str
|
||||
household_size: int
|
||||
adult_count: int
|
||||
child_count: int
|
||||
dietary_notes: Optional[str] = None
|
||||
budget_per_meal: float = 50.00
|
||||
|
||||
|
||||
class FamilyProfileResponse(FamilyProfileBase):
|
||||
id: UUID
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
members: List[FamilyMemberResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FamilyProfileCreate(FamilyProfileBase):
|
||||
pass
|
||||
|
||||
|
||||
class FamilyProfileUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
household_size: Optional[int] = None
|
||||
adult_count: Optional[int] = None
|
||||
child_count: Optional[int] = None
|
||||
dietary_notes: Optional[str] = None
|
||||
budget_per_meal: Optional[float] = None
|
||||
|
||||
|
||||
class RecipeIngredient(BaseModel):
|
||||
ingredient_id: Optional[UUID] = None
|
||||
name: str
|
||||
quantity: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
is_optional: bool = False
|
||||
|
||||
|
||||
class RecipeBase(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
image_source: Optional[str] = None
|
||||
prep_time_minutes: Optional[int] = None
|
||||
cook_time_minutes: Optional[int] = None
|
||||
servings: int
|
||||
servings_scaled: Optional[int] = None
|
||||
cuisine_tags: Optional[List[str]] = []
|
||||
dietary_tags: Optional[List[str]] = []
|
||||
protein_type: Optional[str] = None
|
||||
spice_level: Optional[int] = None
|
||||
ingredients: List[RecipeIngredient] = []
|
||||
instructions: List[str] = []
|
||||
source_url: Optional[str] = None
|
||||
is_manually_added: bool = False
|
||||
|
||||
|
||||
class RecipeResponse(RecipeBase):
|
||||
id: UUID
|
||||
family_profile_id: Optional[UUID] = None
|
||||
scraped_at: Optional[datetime] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
total_time_minutes: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class RecipeCreate(RecipeBase):
|
||||
pass
|
||||
|
||||
|
||||
class MealPlanItemBase(BaseModel):
|
||||
recipe_id: UUID
|
||||
day_of_week: int = Field(..., ge=1, le=7)
|
||||
meal_type: MealType
|
||||
estimated_cost: Optional[float] = None
|
||||
|
||||
|
||||
class MealPlanItemResponse(MealPlanItemBase):
|
||||
id: UUID
|
||||
meal_plan_id: UUID
|
||||
approval_status: MealPlanItemStatus = MealPlanItemStatus.pending
|
||||
denial_reason: Optional[DenialReason] = None
|
||||
denial_details: Optional[str] = None
|
||||
used_pantry_items: Optional[List[UUID]] = []
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
recipe: Optional[RecipeResponse] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MealPlanItemCreate(MealPlanItemBase):
|
||||
pass
|
||||
|
||||
|
||||
class MealPlanBase(BaseModel):
|
||||
week_start_date: date
|
||||
status: MealPlanStatus = MealPlanStatus.draft
|
||||
approval_deadline: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class MealPlanResponse(MealPlanBase):
|
||||
id: UUID
|
||||
family_profile_id: UUID
|
||||
total_estimated_cost: Optional[float] = None
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
items: List[MealPlanItemResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MealPlanCreate(MealPlanBase):
|
||||
items: List[MealPlanItemCreate] = []
|
||||
|
||||
|
||||
class VoteRequest(BaseModel):
|
||||
vote: bool
|
||||
denial_reason: Optional[DenialReason] = None
|
||||
denial_details: Optional[str] = None
|
||||
|
||||
|
||||
class VoteResponse(BaseModel):
|
||||
id: UUID
|
||||
meal_plan_item_id: UUID
|
||||
family_member_id: UUID
|
||||
vote: bool
|
||||
voted_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class HomePantryBase(BaseModel):
|
||||
ingredient_id: UUID
|
||||
quantity: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
expires_at: Optional[date] = None
|
||||
|
||||
|
||||
class HomePantryResponse(HomePantryBase):
|
||||
id: UUID
|
||||
family_profile_id: UUID
|
||||
added_at: Optional[datetime] = None
|
||||
created_at: Optional[datetime] = None
|
||||
ingredient: Optional[IngredientResponse] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class HomePantryCreate(HomePantryBase):
|
||||
pass
|
||||
|
||||
|
||||
class FeedbackBase(BaseModel):
|
||||
rating: Optional[int] = Field(None, ge=1, le=5)
|
||||
never_suggest: bool = False
|
||||
denial_reason: Optional[DenialReason] = None
|
||||
feedback_text: Optional[str] = None
|
||||
|
||||
|
||||
class FeedbackResponse(FeedbackBase):
|
||||
id: UUID
|
||||
family_profile_id: UUID
|
||||
family_member_id: Optional[UUID] = None
|
||||
meal_plan_item_id: UUID
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FeedbackCreate(FeedbackBase):
|
||||
meal_plan_item_id: UUID
|
||||
|
||||
|
||||
class ShoppingListItem(BaseModel):
|
||||
ingredient_id: Optional[UUID] = None
|
||||
name: str
|
||||
quantity: Optional[float] = None
|
||||
unit: Optional[str] = None
|
||||
aisle: Optional[str] = None
|
||||
estimated_price: Optional[float] = None
|
||||
is_on_sale: bool = False
|
||||
sale_price: Optional[float] = None
|
||||
in_season: bool = False
|
||||
in_pantry: bool = False
|
||||
|
||||
|
||||
class ShoppingListResponse(BaseModel):
|
||||
week_start_date: date
|
||||
items: List[ShoppingListItem]
|
||||
total_estimated_cost: float
|
||||
sale_items_count: int
|
||||
by_aisle: dict[str, List[ShoppingListItem]]
|
||||
Reference in New Issue
Block a user