Files

854 lines
32 KiB
Python

"""seed canonical ingredients (and 30 starter recipes in P4-14)
Revision ID: 0007
Revises: 0006
Create Date: 2026-05-05
"""
import json
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,
},
)
# 30 starter recipes. Each entry:
# (name, prep_min, cook_min, servings, cuisine_tags, dietary_tags,
# protein_type, calories_per_serving,
# ingredients [(canonical_name, qty, unit), ...],
# instructions [str, ...])
RECIPES = [
# ---------- Chicken (8) ----------
(
"Sheet-Pan Chicken Thighs with Roasted Vegetables",
10, 30, 4, ["american"], [], "chicken", 520,
[
("Chicken Thighs, Boneless Skinless", 2.0, "lb"),
("Olive Oil", 2.0, "tbsp"),
("Broccoli", 1.0, "lb"),
("Sweet Potato", 2.0, "ea"),
("Salt, Kosher", 1.0, "tsp"),
("Black Pepper", 0.5, "tsp"),
],
[
"Preheat oven to 425F.",
"Toss chicken and vegetables with olive oil, salt, and pepper on a sheet pan.",
"Roast 30 minutes until chicken reads 165F internal.",
"Rest 5 minutes before serving.",
],
),
(
"Chicken Tikka Masala",
15, 25, 4, ["indian"], [], "chicken", 610,
[
("Chicken Breast, Boneless Skinless", 1.5, "lb"),
("Greek Yogurt, Plain", 0.5, "cup"),
("Curry Powder", 2.0, "tsp"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Coconut Milk, Canned", 1.0, "can"),
("Garlic", 3.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Yellow Onion", 1.0, "ea"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Marinate diced chicken in yogurt and curry powder 10 minutes.",
"Saute onion, garlic, and ginger until soft.",
"Add chicken; brown 5 minutes.",
"Stir in tomatoes and coconut milk; simmer 15 minutes.",
"Season with salt; serve over rice.",
],
),
(
"Lemon Garlic Chicken Piccata",
10, 20, 4, ["italian"], [], "chicken", 480,
[
("Chicken Breast, Boneless Skinless", 1.5, "lb"),
("Butter, Unsalted", 3.0, "tbsp"),
("Garlic", 3.0, "clove"),
("Lemon", 2.0, "ea"),
("Chicken Broth", 1.0, "cup"),
("Parsley, Italian", 0.25, "bunch"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Pound chicken to even thickness; season with salt.",
"Sear in butter 4 minutes per side; remove.",
"Add garlic, lemon juice, and broth to pan; reduce 5 minutes.",
"Return chicken to pan; spoon sauce over.",
"Garnish with parsley.",
],
),
(
"Thai Basil Chicken Stir-Fry",
10, 12, 4, ["thai", "asian"], [], "chicken", 460,
[
("Chicken Thighs, Boneless Skinless", 1.5, "lb"),
("Soy Sauce", 3.0, "tbsp"),
("Garlic", 4.0, "clove"),
("Bell Pepper, Red", 1.0, "ea"),
("Ginger, Fresh", 1.0, "tbsp"),
("Olive Oil", 2.0, "tbsp"),
("Rice, Long-Grain White", 1.5, "cup"),
],
[
"Cook rice per package directions.",
"Heat oil in wok; add garlic and ginger 30 seconds.",
"Add diced chicken; stir-fry 6 minutes.",
"Add bell pepper and soy sauce; cook 4 minutes more.",
"Serve over rice.",
],
),
(
"Chicken Fajitas",
10, 15, 4, ["mexican"], [], "chicken", 510,
[
("Chicken Breast, Boneless Skinless", 1.5, "lb"),
("Bell Pepper, Red", 1.0, "ea"),
("Bell Pepper, Green", 1.0, "ea"),
("Yellow Onion", 1.0, "ea"),
("Cumin, Ground", 1.5, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Olive Oil", 2.0, "tbsp"),
("Tortilla, Flour", 8.0, "ea"),
("Lime", 1.0, "ea"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Slice chicken and vegetables into strips.",
"Toss with cumin, paprika, salt, and oil.",
"Sear chicken in hot skillet 6 minutes; remove.",
"Cook peppers and onions 6 minutes.",
"Combine, squeeze lime, and serve in warm tortillas.",
],
),
(
"Mediterranean Chicken Bowls",
15, 20, 4, ["mediterranean"], [], "chicken", 540,
[
("Chicken Breast, Boneless Skinless", 1.25, "lb"),
("Olive Oil", 2.0, "tbsp"),
("Lemon", 1.0, "ea"),
("Chickpeas, Canned", 1.0, "can"),
("Spinach, Fresh", 4.0, "oz"),
("Tomato, Roma", 2.0, "ea"),
("Greek Yogurt, Plain", 0.5, "cup"),
("Garlic", 2.0, "clove"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Marinate chicken in olive oil, lemon juice, garlic, and salt 10 minutes.",
"Grill or pan-sear chicken 6 minutes per side; slice.",
"Warm chickpeas in skillet 3 minutes.",
"Assemble bowls with spinach, tomato, chickpeas, chicken.",
"Top with yogurt and a lemon wedge.",
],
),
(
"Honey Soy Glazed Chicken",
10, 20, 4, ["asian"], [], "chicken", 500,
[
("Chicken Thighs, Boneless Skinless", 2.0, "lb"),
("Soy Sauce", 4.0, "tbsp"),
("Garlic", 3.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Rice, Long-Grain White", 1.5, "cup"),
("Broccoli", 0.75, "lb"),
],
[
"Cook rice per package directions; steam broccoli.",
"Whisk soy sauce, garlic, and ginger.",
"Sear chicken 4 minutes per side.",
"Pour sauce over; simmer 6 minutes until glazed.",
"Slice and serve over rice with broccoli.",
],
),
(
"Creamy Tuscan Chicken",
10, 20, 4, ["italian"], [], "chicken", 620,
[
("Chicken Breast, Boneless Skinless", 1.5, "lb"),
("Butter, Unsalted", 2.0, "tbsp"),
("Garlic", 3.0, "clove"),
("Spinach, Fresh", 4.0, "oz"),
("Tomato, Roma", 2.0, "ea"),
("Milk, Whole", 1.0, "cup"),
("Parmesan, Grated", 4.0, "tbsp"),
("Italian Seasoning", 1.0, "tsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Season chicken with salt and Italian seasoning; sear in butter 5 minutes per side.",
"Remove chicken; add garlic and tomato to pan 2 minutes.",
"Stir in milk and parmesan; simmer 4 minutes.",
"Add spinach until wilted.",
"Return chicken to sauce and serve.",
],
),
# ---------- Beef (4) ----------
(
"Spaghetti Bolognese",
10, 30, 4, ["italian"], [], "beef", 640,
[
("Ground Beef, 85/15", 1.0, "lb"),
("Pasta, Spaghetti", 1.0, "lb"),
("Yellow Onion", 1.0, "ea"),
("Carrot", 2.0, "ea"),
("Celery", 2.0, "stalk"),
("Garlic", 3.0, "clove"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Italian Seasoning", 1.0, "tsp"),
("Olive Oil", 2.0, "tbsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Saute onion, carrot, celery, and garlic in olive oil 6 minutes.",
"Add ground beef; brown 8 minutes.",
"Stir in tomatoes and Italian seasoning; simmer 20 minutes.",
"Cook spaghetti to al dente; drain.",
"Toss pasta with sauce; season with salt.",
],
),
(
"Classic Beef Tacos",
10, 15, 4, ["mexican"], [], "beef", 560,
[
("Ground Beef, 85/15", 1.0, "lb"),
("Yellow Onion", 1.0, "ea"),
("Cumin, Ground", 1.5, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Tortilla, Corn", 8.0, "ea"),
("Cheddar Cheese, Sharp", 4.0, "oz"),
("Salsa, Jarred", 4.0, "tbsp"),
("Cilantro", 0.25, "bunch"),
("Lime", 1.0, "ea"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Brown beef and diced onion 8 minutes.",
"Add cumin, paprika, salt; cook 2 minutes more.",
"Warm tortillas in dry skillet.",
"Assemble tacos with beef, cheese, salsa, cilantro.",
"Squeeze lime over and serve.",
],
),
(
"Korean-Style Beef Bowls",
10, 12, 4, ["asian", "korean"], [], "beef", 580,
[
("Ground Beef, 85/15", 1.0, "lb"),
("Soy Sauce", 4.0, "tbsp"),
("Garlic", 3.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Rice, Long-Grain White", 1.5, "cup"),
("Broccoli", 0.75, "lb"),
],
[
"Cook rice and steam broccoli.",
"Brown beef 5 minutes; drain excess fat.",
"Add garlic, ginger, soy sauce; simmer 4 minutes.",
"Spoon over rice with broccoli alongside.",
],
),
(
"Slow-Braised Beef Stew",
15, 120, 6, ["american"], [], "beef", 560,
[
("Ground Beef, 85/15", 1.5, "lb"),
("Yellow Onion", 1.0, "ea"),
("Carrot", 3.0, "ea"),
("Celery", 2.0, "stalk"),
("Sweet Potato", 2.0, "ea"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Chicken Broth", 3.0, "cup"),
("Olive Oil", 2.0, "tbsp"),
("Salt, Kosher", 1.5, "tsp"),
("Black Pepper", 0.5, "tsp"),
],
[
"Brown beef in olive oil; remove.",
"Saute onion, carrot, celery 6 minutes.",
"Return beef; add tomatoes, broth, sweet potato, salt, pepper.",
"Cover; simmer low 2 hours until tender.",
"Adjust seasoning and serve.",
],
),
# ---------- Turkey (3) ----------
(
"Turkey Chili",
10, 30, 6, ["american"], [], "turkey", 430,
[
("Ground Turkey", 1.25, "lb"),
("Black Beans, Canned", 2.0, "can"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Yellow Onion", 1.0, "ea"),
("Bell Pepper, Red", 1.0, "ea"),
("Cumin, Ground", 2.0, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Garlic", 3.0, "clove"),
("Olive Oil", 2.0, "tbsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Saute onion, pepper, garlic in oil 6 minutes.",
"Add turkey; brown 8 minutes.",
"Stir in cumin, paprika, salt.",
"Add tomatoes and beans; simmer 20 minutes.",
"Adjust seasoning and serve.",
],
),
(
"Turkey Meatballs in Marinara",
15, 25, 4, ["italian"], [], "turkey", 540,
[
("Ground Turkey", 1.25, "lb"),
("Eggs, Large", 1.0, "ea"),
("Parmesan, Grated", 4.0, "tbsp"),
("Italian Seasoning", 1.0, "tsp"),
("Garlic", 3.0, "clove"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Pasta, Spaghetti", 0.75, "lb"),
("Olive Oil", 2.0, "tbsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Mix turkey, egg, parmesan, half the garlic, salt; form 16 meatballs.",
"Brown meatballs in oil 6 minutes.",
"Add remaining garlic, tomatoes, Italian seasoning; simmer 18 minutes.",
"Cook spaghetti to al dente; drain.",
"Serve meatballs and sauce over pasta.",
],
),
(
"Turkey Lettuce Wraps",
10, 12, 4, ["asian"], [], "turkey", 380,
[
("Ground Turkey", 1.0, "lb"),
("Soy Sauce", 3.0, "tbsp"),
("Garlic", 2.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Bell Pepper, Red", 1.0, "ea"),
("Cilantro", 0.25, "bunch"),
("Lime", 1.0, "ea"),
("Olive Oil", 1.0, "tbsp"),
],
[
"Heat oil; saute garlic and ginger 30 seconds.",
"Add turkey; brown 6 minutes.",
"Stir in diced bell pepper and soy sauce; cook 4 minutes.",
"Spoon into lettuce leaves; top with cilantro and lime.",
],
),
# ---------- Pork (3) ----------
(
"Pan-Seared Pork Chops with Apple Glaze",
10, 18, 4, ["american"], [], "pork", 520,
[
("Pork Chops, Bone-In", 2.0, "lb"),
("Butter, Unsalted", 2.0, "tbsp"),
("Garlic", 2.0, "clove"),
("Chicken Broth", 0.5, "cup"),
("Salt, Kosher", 1.0, "tsp"),
("Black Pepper", 0.5, "tsp"),
],
[
"Season chops with salt and pepper.",
"Sear in butter 5 minutes per side.",
"Add garlic and broth; simmer 4 minutes.",
"Spoon pan sauce over chops and serve.",
],
),
(
"Pork Carnitas Tacos",
15, 150, 6, ["mexican"], [], "pork", 580,
[
("Pork Chops, Bone-In", 2.5, "lb"),
("Yellow Onion", 1.0, "ea"),
("Garlic", 4.0, "clove"),
("Cumin, Ground", 1.5, "tsp"),
("Lime", 2.0, "ea"),
("Tortilla, Corn", 12.0, "ea"),
("Cilantro", 0.5, "bunch"),
("Salt, Kosher", 1.5, "tsp"),
("Chicken Broth", 1.0, "cup"),
],
[
"Cube pork; season with salt and cumin.",
"Combine pork, onion, garlic, broth, and lime juice in pot.",
"Cover; simmer 2.5 hours until shreddable.",
"Shred pork; broil 4 minutes for crispy edges.",
"Serve in warm tortillas with cilantro and lime.",
],
),
(
"Pork Stir-Fry with Bell Peppers",
10, 12, 4, ["asian"], [], "pork", 470,
[
("Pork Chops, Bone-In", 1.25, "lb"),
("Bell Pepper, Red", 1.0, "ea"),
("Bell Pepper, Green", 1.0, "ea"),
("Soy Sauce", 3.0, "tbsp"),
("Garlic", 3.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Olive Oil", 2.0, "tbsp"),
("Rice, Long-Grain White", 1.5, "cup"),
],
[
"Cook rice per package directions.",
"Slice pork off bone into strips.",
"Stir-fry pork in oil 5 minutes; remove.",
"Cook peppers, garlic, ginger 4 minutes.",
"Return pork; add soy sauce; cook 2 minutes; serve over rice.",
],
),
# ---------- Fish / Seafood (3) ----------
(
"Lemon Herb Baked Salmon",
5, 18, 4, ["mediterranean"], [], "fish", 480,
[
("Salmon Fillet", 1.5, "lb"),
("Lemon", 1.0, "ea"),
("Olive Oil", 2.0, "tbsp"),
("Garlic", 2.0, "clove"),
("Parsley, Italian", 0.25, "bunch"),
("Salt, Kosher", 1.0, "tsp"),
("Black Pepper", 0.5, "tsp"),
],
[
"Preheat oven to 400F.",
"Place salmon on lined sheet; drizzle with oil and lemon juice.",
"Top with minced garlic, salt, pepper.",
"Bake 15 minutes until flaky.",
"Garnish with parsley.",
],
),
(
"Garlic Shrimp Scampi",
5, 12, 4, ["italian"], [], "fish", 520,
[
("Shrimp, Peeled", 1.25, "lb"),
("Pasta, Spaghetti", 0.75, "lb"),
("Butter, Unsalted", 3.0, "tbsp"),
("Garlic", 4.0, "clove"),
("Lemon", 1.0, "ea"),
("Parsley, Italian", 0.25, "bunch"),
("Olive Oil", 1.0, "tbsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Cook pasta to al dente; reserve 0.5 cup water.",
"Saute garlic in butter and oil 1 minute.",
"Add shrimp; cook 4 minutes until pink.",
"Toss with pasta, lemon juice, reserved water, salt.",
"Top with parsley.",
],
),
(
"Shrimp Tacos with Cilantro Lime Slaw",
15, 8, 4, ["mexican"], [], "fish", 460,
[
("Shrimp, Peeled", 1.0, "lb"),
("Cumin, Ground", 1.0, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Olive Oil", 1.0, "tbsp"),
("Tortilla, Corn", 8.0, "ea"),
("Cilantro", 0.5, "bunch"),
("Lime", 2.0, "ea"),
("Sour Cream", 4.0, "tbsp"),
("Avocado", 1.0, "ea"),
("Salt, Kosher", 0.75, "tsp"),
],
[
"Toss shrimp with cumin, paprika, salt, oil.",
"Sear 3 minutes per side.",
"Mix cilantro and lime juice with sour cream for slaw.",
"Warm tortillas; fill with shrimp, slaw, sliced avocado.",
],
),
# ---------- Vegetarian (5) ----------
(
"Black Bean Tacos",
5, 10, 4, ["mexican"], ["vegetarian"], "vegetarian", 410,
[
("Black Beans, Canned", 2.0, "can"),
("Cumin, Ground", 1.0, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Tortilla, Corn", 8.0, "ea"),
("Avocado", 1.0, "ea"),
("Salsa, Jarred", 4.0, "tbsp"),
("Cilantro", 0.25, "bunch"),
("Lime", 1.0, "ea"),
("Salt, Kosher", 0.5, "tsp"),
],
[
"Drain beans; warm with cumin, paprika, salt 6 minutes.",
"Mash slightly with fork.",
"Warm tortillas in dry skillet.",
"Fill with beans, avocado, salsa, cilantro.",
"Squeeze lime over and serve.",
],
),
(
"Chickpea Curry",
10, 25, 4, ["indian"], ["vegetarian", "vegan"], "vegetarian", 480,
[
("Chickpeas, Canned", 2.0, "can"),
("Coconut Milk, Canned", 1.0, "can"),
("Yellow Onion", 1.0, "ea"),
("Garlic", 3.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Curry Powder", 2.0, "tsp"),
("Diced Tomatoes, Canned", 1.0, "can"),
("Spinach, Fresh", 4.0, "oz"),
("Rice, Long-Grain White", 1.5, "cup"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Cook rice per package directions.",
"Saute onion, garlic, ginger 5 minutes.",
"Stir in curry powder, tomatoes, coconut milk; simmer 15 minutes.",
"Add chickpeas and spinach; cook 5 minutes.",
"Season with salt; serve over rice.",
],
),
(
"Caprese Pasta",
10, 15, 4, ["italian"], ["vegetarian"], "vegetarian", 540,
[
("Pasta, Penne", 1.0, "lb"),
("Tomato, Roma", 4.0, "ea"),
("Mozzarella, Shredded", 6.0, "oz"),
("Olive Oil", 3.0, "tbsp"),
("Garlic", 2.0, "clove"),
("Italian Seasoning", 1.0, "tsp"),
("Parsley, Italian", 0.25, "bunch"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Cook penne to al dente; drain.",
"Saute garlic and diced tomato in oil 5 minutes.",
"Toss with pasta, Italian seasoning, salt.",
"Top with mozzarella; cover 2 minutes to melt.",
"Garnish with parsley.",
],
),
(
"Zucchini and Spinach Frittata",
10, 20, 4, ["mediterranean"], ["vegetarian"], "vegetarian", 360,
[
("Eggs, Large", 8.0, "ea"),
("Zucchini", 1.0, "ea"),
("Spinach, Fresh", 3.0, "oz"),
("Yellow Onion", 0.5, "ea"),
("Parmesan, Grated", 4.0, "tbsp"),
("Olive Oil", 2.0, "tbsp"),
("Salt, Kosher", 0.75, "tsp"),
("Black Pepper", 0.25, "tsp"),
],
[
"Preheat oven to 375F.",
"Saute onion and zucchini in oil 5 minutes; add spinach.",
"Whisk eggs, parmesan, salt, pepper; pour over vegetables.",
"Cook 3 minutes on stovetop, then bake 12 minutes until set.",
],
),
(
"Roasted Sweet Potato and Chickpea Bowl",
10, 30, 4, ["mediterranean"], ["vegetarian", "vegan"], "vegetarian", 470,
[
("Sweet Potato", 3.0, "ea"),
("Chickpeas, Canned", 1.0, "can"),
("Olive Oil", 2.0, "tbsp"),
("Cumin, Ground", 1.0, "tsp"),
("Paprika, Smoked", 1.0, "tsp"),
("Spinach, Fresh", 4.0, "oz"),
("Greek Yogurt, Plain", 0.5, "cup"),
("Lemon", 1.0, "ea"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Preheat oven to 425F.",
"Toss diced sweet potato and drained chickpeas with oil, cumin, paprika, salt.",
"Roast 30 minutes.",
"Whisk yogurt with lemon juice.",
"Plate over spinach; drizzle with yogurt sauce.",
],
),
# ---------- Mixed / Other (4) ----------
(
"Veggie Fried Rice with Egg",
10, 12, 4, ["asian"], ["vegetarian"], "vegetarian", 440,
[
("Rice, Long-Grain White", 2.0, "cup"),
("Eggs, Large", 3.0, "ea"),
("Soy Sauce", 3.0, "tbsp"),
("Carrot", 2.0, "ea"),
("Bell Pepper, Red", 1.0, "ea"),
("Garlic", 2.0, "clove"),
("Ginger, Fresh", 1.0, "tbsp"),
("Olive Oil", 2.0, "tbsp"),
],
[
"Cook rice; spread on tray to cool (or use day-old rice).",
"Scramble eggs in oil; remove.",
"Stir-fry carrot, pepper, garlic, ginger 4 minutes.",
"Add rice and soy sauce; toss 4 minutes.",
"Fold eggs back in and serve.",
],
),
(
"Breakfast-for-Dinner Veggie Scramble",
5, 12, 4, ["american"], ["vegetarian"], "vegetarian", 380,
[
("Eggs, Large", 8.0, "ea"),
("Bell Pepper, Green", 1.0, "ea"),
("Tomato, Roma", 2.0, "ea"),
("Cheddar Cheese, Sharp", 3.0, "oz"),
("Butter, Unsalted", 2.0, "tbsp"),
("Salt, Kosher", 0.5, "tsp"),
("Black Pepper", 0.25, "tsp"),
("Tortilla, Flour", 4.0, "ea"),
],
[
"Saute diced pepper and tomato in butter 4 minutes.",
"Whisk eggs with salt and pepper; add to pan.",
"Stir gently 4 minutes until just set.",
"Fold in cheese; serve with warm tortillas.",
],
),
(
"Chicken and Rice Soup",
10, 30, 6, ["american"], [], "chicken", 360,
[
("Chicken Breast, Boneless Skinless", 1.0, "lb"),
("Chicken Broth", 6.0, "cup"),
("Carrot", 3.0, "ea"),
("Celery", 2.0, "stalk"),
("Yellow Onion", 1.0, "ea"),
("Garlic", 2.0, "clove"),
("Rice, Long-Grain White", 0.75, "cup"),
("Parsley, Italian", 0.25, "bunch"),
("Olive Oil", 1.0, "tbsp"),
("Salt, Kosher", 1.0, "tsp"),
],
[
"Saute onion, carrot, celery, garlic 6 minutes.",
"Add broth and chicken; simmer 15 minutes.",
"Remove chicken; shred. Add rice; cook 12 minutes.",
"Return chicken; season with salt.",
"Garnish with parsley.",
],
),
(
"Loaded Veggie Quesadillas",
10, 12, 4, ["mexican"], ["vegetarian"], "vegetarian", 520,
[
("Tortilla, Flour", 8.0, "ea"),
("Cheddar Cheese, Sharp", 6.0, "oz"),
("Black Beans, Canned", 1.0, "can"),
("Bell Pepper, Red", 1.0, "ea"),
("Yellow Onion", 0.5, "ea"),
("Cumin, Ground", 0.5, "tsp"),
("Olive Oil", 1.0, "tbsp"),
("Salsa, Jarred", 4.0, "tbsp"),
("Sour Cream", 4.0, "tbsp"),
],
[
"Saute pepper and onion in oil 5 minutes; stir in beans and cumin.",
"Layer cheese and filling on tortillas; top with second tortilla.",
"Toast in dry skillet 2 minutes per side until golden.",
"Cut into wedges; serve with salsa and sour cream.",
],
),
]
def _insert_recipes() -> None:
bind = op.get_bind()
# Build a name_lower -> id map once.
rows = bind.execute(sa.text("SELECT id, name_lower FROM ingredient")).fetchall()
name_to_id = {row.name_lower: str(row.id) for row in rows}
for (
name, prep, cook, servings, cuisine_tags, dietary_tags,
protein, calories, ingredients, instructions,
) in RECIPES:
ingredient_payload = []
for ing_name, qty, unit in ingredients:
ing_id = name_to_id.get(ing_name.lower())
if ing_id is None:
raise RuntimeError(
f"Recipe {name!r} references unknown ingredient {ing_name!r}. "
f"Add it to INGREDIENTS or fix the name."
)
ingredient_payload.append({
"ingredient_id": ing_id,
"qty": float(qty),
"unit": unit,
})
bind.execute(
sa.text(
"""
INSERT INTO recipe (
id, name, prep_time_minutes, cook_time_minutes, servings,
cuisine_tags, dietary_tags, protein_type, calories_per_serving,
ingredients, instructions, is_manually_added
)
VALUES (
gen_random_uuid(), :name, :prep, :cook, :servings,
:cuisine_tags, :dietary_tags, :protein, :calories,
CAST(:ingredients AS jsonb), :instructions, false
)
ON CONFLICT DO NOTHING
"""
),
{
"name": name,
"prep": prep,
"cook": cook,
"servings": servings,
"cuisine_tags": cuisine_tags,
"dietary_tags": dietary_tags,
"protein": protein,
"calories": calories,
"ingredients": json.dumps(ingredient_payload),
"instructions": instructions,
},
)
def _delete_recipes() -> None:
bind = op.get_bind()
names = [row[0] for row in RECIPES]
bind.execute(
sa.text("DELETE FROM recipe WHERE name = ANY(:names) AND is_manually_added = false"),
{"names": names},
)
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()