Files
Meal-Planner/backend/alembic/versions/0007_seed_recipes.py
T

141 lines
6.0 KiB
Python

"""seed canonical ingredients (and 30 starter recipes in P4-14)
Revision ID: 0007
Revises: 0006
Create Date: 2026-05-05
"""
from decimal import Decimal
from alembic import op
import sqlalchemy as sa
revision = "0007"
down_revision = "0006"
branch_labels = None
depends_on = None
# Canonical ingredients used by the 30 starter recipes (P4-14 references them
# by name_lower so this list stays the source of truth for both seed steps).
INGREDIENTS = [
# (name, aliases, aisle, unit, typical_price)
("Chicken Thighs, Boneless Skinless", ["chicken thigh", "BSL chicken thighs"], "meat_seafood", "lb", Decimal("4.99")),
("Chicken Breast, Boneless Skinless", ["chicken breast", "BSL chicken breast"], "meat_seafood", "lb", Decimal("5.99")),
("Ground Beef, 85/15", ["ground beef", "hamburger"], "meat_seafood", "lb", Decimal("6.99")),
("Ground Turkey", ["turkey mince"], "meat_seafood", "lb", Decimal("5.49")),
("Pork Chops, Bone-In", ["pork chop"], "meat_seafood", "lb", Decimal("4.49")),
("Salmon Fillet", ["salmon"], "meat_seafood", "lb", Decimal("12.99")),
("Shrimp, Peeled", ["shrimp"], "meat_seafood", "lb", Decimal("9.99")),
("Eggs, Large", ["egg"], "dairy", "ea", Decimal("0.40")),
("Black Beans, Canned", ["black bean"], "pantry", "can", Decimal("1.29")),
("Chickpeas, Canned", ["garbanzo", "chickpea"], "pantry", "can", Decimal("1.49")),
("Yellow Onion", ["onion"], "produce", "ea", Decimal("0.99")),
("Garlic", ["garlic clove"], "produce", "clove", Decimal("0.10")),
("Carrot", ["carrots"], "produce", "ea", Decimal("0.50")),
("Celery", [], "produce", "stalk", Decimal("0.30")),
("Bell Pepper, Red", ["red pepper"], "produce", "ea", Decimal("1.49")),
("Bell Pepper, Green", ["green pepper"], "produce", "ea", Decimal("0.99")),
("Tomato, Roma", ["tomato"], "produce", "ea", Decimal("0.79")),
("Lemon", [], "produce", "ea", Decimal("0.79")),
("Lime", [], "produce", "ea", Decimal("0.50")),
("Cilantro", ["coriander leaf"], "produce", "bunch", Decimal("0.99")),
("Parsley, Italian", ["parsley"], "produce", "bunch", Decimal("0.99")),
("Spinach, Fresh", ["spinach"], "produce", "oz", Decimal("0.40")),
("Broccoli", [], "produce", "lb", Decimal("2.49")),
("Zucchini", [], "produce", "ea", Decimal("0.99")),
("Sweet Potato", [], "produce", "ea", Decimal("1.29")),
("Olive Oil", ["EVOO", "extra virgin olive oil"], "pantry", "tbsp", Decimal("0.20")),
("Soy Sauce", [], "pantry", "tbsp", Decimal("0.15")),
("Rice, Long-Grain White", ["white rice", "rice"], "pantry", "cup", Decimal("0.40")),
("Pasta, Penne", ["penne"], "pantry", "lb", Decimal("1.29")),
("Pasta, Spaghetti", ["spaghetti"], "pantry", "lb", Decimal("1.29")),
("Tortilla, Flour", ["flour tortilla"], "pantry", "ea", Decimal("0.30")),
("Tortilla, Corn", ["corn tortilla"], "pantry", "ea", Decimal("0.20")),
("Diced Tomatoes, Canned", ["canned tomato"], "pantry", "can", Decimal("1.49")),
("Chicken Broth", ["chicken stock"], "pantry", "cup", Decimal("0.30")),
("Coconut Milk, Canned", [], "pantry", "can", Decimal("2.49")),
("Salt, Kosher", ["kosher salt"], "pantry", "tsp", Decimal("0.01")),
("Black Pepper", ["pepper"], "pantry", "tsp", Decimal("0.02")),
("Cumin, Ground", ["cumin"], "pantry", "tsp", Decimal("0.10")),
("Paprika, Smoked", ["smoked paprika"], "pantry", "tsp", Decimal("0.10")),
("Italian Seasoning", [], "pantry", "tsp", Decimal("0.10")),
("Curry Powder", [], "pantry", "tsp", Decimal("0.10")),
("Ginger, Fresh", ["ginger root"], "produce", "tbsp", Decimal("0.30")),
("Cheddar Cheese, Sharp", ["cheddar"], "dairy", "oz", Decimal("0.40")),
("Mozzarella, Shredded", ["mozzarella"], "dairy", "oz", Decimal("0.45")),
("Parmesan, Grated", ["parm"], "dairy", "tbsp", Decimal("0.20")),
("Sour Cream", [], "dairy", "tbsp", Decimal("0.10")),
("Greek Yogurt, Plain", ["yogurt"], "dairy", "cup", Decimal("1.50")),
("Butter, Unsalted", ["butter"], "dairy", "tbsp", Decimal("0.20")),
("Milk, Whole", ["milk"], "dairy", "cup", Decimal("0.30")),
("Avocado", [], "produce", "ea", Decimal("1.49")),
("Salsa, Jarred", ["salsa"], "pantry", "tbsp", Decimal("0.15")),
]
def _upsert_ingredients() -> None:
bind = op.get_bind()
for name, aliases, aisle, unit, typical_price in INGREDIENTS:
bind.execute(
sa.text(
"""
INSERT INTO ingredient (id, name, name_lower, aliases, aisle, unit, typical_price)
VALUES (
gen_random_uuid(), :name, :name_lower, :aliases, :aisle, :unit, :typical_price
)
ON CONFLICT (name_lower) DO UPDATE SET
aliases = EXCLUDED.aliases,
aisle = COALESCE(EXCLUDED.aisle, ingredient.aisle),
unit = COALESCE(EXCLUDED.unit, ingredient.unit),
typical_price = COALESCE(EXCLUDED.typical_price, ingredient.typical_price)
"""
),
{
"name": name,
"name_lower": name.lower(),
"aliases": aliases,
"aisle": aisle,
"unit": unit,
"typical_price": typical_price,
},
)
def _insert_recipes() -> None:
"""Populated in P4-14."""
pass
def _delete_recipes() -> None:
"""Populated in P4-14."""
pass
def _delete_ingredients() -> None:
"""Best-effort: only remove rows whose name_lower matches one of our seeds.
Pre-existing rows from migration 0002 may share names; their aliases/aisle
will revert to whatever was in 0002 (NULL aliases pre-0006). For a clean
rollback in this scenario, run alembic downgrade past 0002 as well.
"""
bind = op.get_bind()
name_lowers = [name.lower() for name, *_ in INGREDIENTS]
bind.execute(
sa.text("DELETE FROM ingredient WHERE name_lower = ANY(:nl)"),
{"nl": name_lowers},
)
def upgrade() -> None:
_upsert_ingredients()
_insert_recipes()
def downgrade() -> None:
_delete_recipes()
_delete_ingredients()