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,289 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2026-05-04
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = '0001'
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS \"citext\"")
|
||||
|
||||
op.create_enum = None
|
||||
enum_types = [
|
||||
"family_member_role_enum",
|
||||
"meal_type_enum",
|
||||
"meal_plan_status_enum",
|
||||
"meal_plan_item_status_enum",
|
||||
"approval_token_status_enum",
|
||||
"denial_reason_enum",
|
||||
"never_suggest_reason_enum",
|
||||
"scrape_status_enum",
|
||||
"email_status_enum"
|
||||
]
|
||||
for enum_name in enum_types:
|
||||
op.execute(f"CREATE TYPE {enum_name} AS ENUM ('placeholder')")
|
||||
op.execute(f"DROP TYPE {enum_name}")
|
||||
|
||||
op.execute("""CREATE TYPE family_member_role_enum AS ENUM ('adult', 'child')""")
|
||||
op.execute("""CREATE TYPE meal_type_enum AS ENUM ('breakfast', 'lunch', 'dinner')""")
|
||||
op.execute("""CREATE TYPE meal_plan_status_enum AS ENUM ('draft', 'pending_approval', 'approved', 'locked')""")
|
||||
op.execute("""CREATE TYPE meal_plan_item_status_enum AS ENUM ('pending', 'approved', 'denied', 'swapped')""")
|
||||
op.execute("""CREATE TYPE approval_token_status_enum AS ENUM ('active', 'used', 'expired')""")
|
||||
op.execute("""CREATE TYPE denial_reason_enum AS ENUM ('too_expensive', 'boring', 'disliked_ingredient', 'cultural', 'other')""")
|
||||
op.execute("""CREATE TYPE never_suggest_reason_enum AS ENUM ('allergy', 'dislike', 'tried_too_much', 'other')""")
|
||||
op.execute("""CREATE TYPE scrape_status_enum AS ENUM ('started', 'success', 'failed')""")
|
||||
op.execute("""CREATE TYPE email_status_enum AS ENUM ('sent', 'delivered', 'failed', 'bounced')""")
|
||||
|
||||
op.create_table('family_profile',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('household_size', sa.Integer(), nullable=False),
|
||||
sa.Column('adult_count', sa.Integer(), nullable=False),
|
||||
sa.Column('child_count', sa.Integer(), nullable=False),
|
||||
sa.Column('dietary_notes', sa.Text(), nullable=True),
|
||||
sa.Column('budget_per_meal', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('ingredient',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('name', sa.String(length=200), nullable=False),
|
||||
sa.Column('name_lower', sa.String(length=200), nullable=False),
|
||||
sa.Column('plural_name', sa.String(length=200), nullable=True),
|
||||
sa.Column('aisle', sa.String(length=100), nullable=True),
|
||||
sa.Column('typical_price', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('unit', sa.String(length=50), nullable=True),
|
||||
sa.Column('season_months', postgresql.ARRAY(sa.Integer()), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name_lower')
|
||||
)
|
||||
op.create_table('family_member',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('name', sa.String(length=100), nullable=False),
|
||||
sa.Column('email', sa.String(length=300), nullable=True),
|
||||
sa.Column('role', postgresql.ENUM(name='family_member_role_enum', create_type=False), nullable=False),
|
||||
sa.Column('likes_mushrooms', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('family_profile_id', 'email', name='uq_family_member_profile_email')
|
||||
)
|
||||
op.create_table('recipe',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('name', sa.String(length=300), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('image_url', sa.Text(), nullable=True),
|
||||
sa.Column('image_source', sa.String(length=50), nullable=True),
|
||||
sa.Column('prep_time_minutes', sa.Integer(), nullable=True),
|
||||
sa.Column('cook_time_minutes', sa.Integer(), nullable=True),
|
||||
sa.Column('servings', sa.Integer(), nullable=False),
|
||||
sa.Column('servings_scaled', sa.Integer(), nullable=True),
|
||||
sa.Column('cuisine_tags', postgresql.ARRAY(sa.String(length=50)), nullable=True),
|
||||
sa.Column('dietary_tags', postgresql.ARRAY(sa.String(length=50)), nullable=True),
|
||||
sa.Column('protein_type', sa.String(length=50), nullable=True),
|
||||
sa.Column('spice_level', sa.Integer(), nullable=True),
|
||||
sa.Column('ingredients', postgresql.JSONB(astext=True), nullable=False),
|
||||
sa.Column('instructions', postgresql.ARRAY(sa.Text()), nullable=False),
|
||||
sa.Column('source_url', sa.Text(), nullable=True),
|
||||
sa.Column('scraped_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('is_manually_added', sa.Boolean(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('meal_plan',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('week_start_date', sa.Date(), nullable=False),
|
||||
sa.Column('status', postgresql.ENUM(name='meal_plan_status_enum', create_type=False), nullable=False),
|
||||
sa.Column('approval_deadline', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('total_estimated_cost', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('family_profile_id', 'week_start_date', name='uq_meal_plan_family_week')
|
||||
)
|
||||
op.create_table('home_pantry',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('ingredient_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('quantity', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('unit', sa.String(length=50), nullable=True),
|
||||
sa.Column('expires_at', sa.Date(), nullable=True),
|
||||
sa.Column('added_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredient.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('family_profile_id', 'ingredient_id', name='uq_home_pantry_family_ingredient')
|
||||
)
|
||||
op.create_table('meal_plan_item',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('meal_plan_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('recipe_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('day_of_week', sa.Integer(), nullable=False),
|
||||
sa.Column('meal_type', postgresql.ENUM(name='meal_type_enum', create_type=False), nullable=False),
|
||||
sa.Column('approval_status', postgresql.ENUM(name='meal_plan_item_status_enum', create_type=False), nullable=True),
|
||||
sa.Column('denial_reason', postgresql.ENUM(name='denial_reason_enum', create_type=False), nullable=True),
|
||||
sa.Column('denial_details', sa.Text(), nullable=True),
|
||||
sa.Column('estimated_cost', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('used_pantry_items', postgresql.ARRAY(postgresql.UUID(as_uuid=True)), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['meal_plan_id'], ['meal_plan.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['recipe_id'], ['recipe.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('meal_plan_id', 'day_of_week', 'meal_type', name='uq_meal_plan_item_day_meal')
|
||||
)
|
||||
op.create_table('meal_plan_vote',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('meal_plan_item_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('family_member_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('vote', sa.Boolean(), nullable=False),
|
||||
sa.Column('voted_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_member_id'], ['family_member.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['meal_plan_item_id'], ['meal_plan_item.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('meal_plan_item_id', 'family_member_id', name='uq_meal_plan_vote_item_member')
|
||||
)
|
||||
op.create_table('approval_token',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('meal_plan_item_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('family_member_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('token', sa.String(length=64), nullable=False),
|
||||
sa.Column('status', postgresql.ENUM(name='approval_token_status_enum', create_type=False), nullable=True),
|
||||
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_member_id'], ['family_member.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['meal_plan_item_id'], ['meal_plan_item.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('meal_plan_item_id', 'family_member_id', name='uq_approval_token_item_member'),
|
||||
sa.UniqueConstraint('token')
|
||||
)
|
||||
op.create_table('feedback',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('family_member_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('meal_plan_item_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('rating', sa.Integer(), nullable=True),
|
||||
sa.Column('never_suggest', sa.Boolean(), nullable=True),
|
||||
sa.Column('denial_reason', postgresql.ENUM(name='denial_reason_enum', create_type=False), nullable=True),
|
||||
sa.Column('feedback_text', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ),
|
||||
sa.ForeignKeyConstraint(['family_member_id'], ['family_member.id'], ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['meal_plan_item_id'], ['meal_plan_item.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('never_suggest',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('family_profile_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('ingredient_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('recipe_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('reason', postgresql.ENUM(name='never_suggest_reason_enum', create_type=False), nullable=True),
|
||||
sa.Column('notes', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.ForeignKeyConstraint(['family_profile_id'], ['family_profile.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredient.id'], ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['recipe_id'], ['recipe.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('grocery_item',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('ingredient_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('name', sa.String(length=300), nullable=False),
|
||||
sa.Column('brand', sa.String(length=200), nullable=True),
|
||||
sa.Column('current_price', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('regular_price', sa.Numeric(precision=10, scale=2), nullable=True),
|
||||
sa.Column('unit', sa.String(length=50), nullable=True),
|
||||
sa.Column('aisle', sa.String(length=100), nullable=True),
|
||||
sa.Column('image_url', sa.Text(), nullable=True),
|
||||
sa.Column('product_url', sa.Text(), nullable=True),
|
||||
sa.Column('is_on_sale', sa.Boolean(), nullable=True),
|
||||
sa.Column('sale_start_date', sa.Date(), nullable=True),
|
||||
sa.Column('sale_end_date', sa.Date(), nullable=True),
|
||||
sa.Column('in_season', sa.Boolean(), nullable=True),
|
||||
sa.Column('scraped_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('scraped_url', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['ingredient_id'], ['ingredient.id'], ondelete='SET NULL'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('scrape_log',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('source', sa.String(length=50), nullable=False),
|
||||
sa.Column('scrape_type', sa.String(length=50), nullable=False),
|
||||
sa.Column('status', postgresql.ENUM(name='scrape_status_enum', create_type=False), nullable=False),
|
||||
sa.Column('items_scraped', sa.Integer(), nullable=True),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('duration_seconds', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('email_log',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||
sa.Column('recipient_email', sa.String(length=300), nullable=False),
|
||||
sa.Column('recipient_name', sa.String(length=200), nullable=True),
|
||||
sa.Column('template', sa.String(length=100), nullable=False),
|
||||
sa.Column('meal_plan_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('meal_plan_item_id', postgresql.UUID(as_uuid=True), nullable=True),
|
||||
sa.Column('sendgrid_message_id', sa.String(length=100), nullable=True),
|
||||
sa.Column('status', postgresql.ENUM(name='email_status_enum', create_type=False), nullable=False),
|
||||
sa.Column('error_message', sa.Text(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
|
||||
sa.Column('delivered_at', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(['meal_plan_id'], ['meal_plan.id'], ),
|
||||
sa.ForeignKeyConstraint(['meal_plan_item_id'], ['meal_plan_item.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
|
||||
op.create_index('idx_family_member_profile', 'family_member', ['family_profile_id'])
|
||||
op.create_index('idx_recipe_family_profile', 'recipe', ['family_profile_id'])
|
||||
op.create_index('idx_meal_plan_family_profile', 'meal_plan', ['family_profile_id'])
|
||||
op.create_index('idx_meal_plan_item_meal_plan', 'meal_plan_item', ['meal_plan_id'])
|
||||
op.create_index('idx_meal_plan_item_recipe', 'meal_plan_item', ['recipe_id'])
|
||||
op.create_index('idx_meal_plan_vote_item', 'meal_plan_vote', ['meal_plan_item_id'])
|
||||
op.create_index('idx_meal_plan_vote_member', 'meal_plan_vote', ['family_member_id'])
|
||||
op.create_index('idx_approval_token_item', 'approval_token', ['meal_plan_item_id'])
|
||||
op.create_index('idx_approval_token_member', 'approval_token', ['family_member_id'])
|
||||
op.create_index('idx_home_pantry_family_profile', 'home_pantry', ['family_profile_id'])
|
||||
op.create_index('idx_home_pantry_ingredient', 'home_pantry', ['ingredient_id'])
|
||||
op.create_index('idx_feedback_family_profile', 'feedback', ['family_profile_id'])
|
||||
op.create_index('idx_feedback_member', 'feedback', ['family_member_id'])
|
||||
op.create_index('idx_feedback_meal_plan_item', 'feedback', ['meal_plan_item_id'])
|
||||
op.create_index('idx_never_suggest_family_profile', 'never_suggest', ['family_profile_id'])
|
||||
op.create_index('idx_grocery_item_ingredient', 'grocery_item', ['ingredient_id'])
|
||||
op.create_index('idx_grocery_item_is_on_sale', 'grocery_item', [sa.text('is_on_sale')], postgresql_where=sa.text('is_on_sale = TRUE'))
|
||||
|
||||
op.execute("CREATE INDEX idx_ingredient_name_lower_fts ON ingredient USING gin(to_tsvector('english', name_lower))")
|
||||
op.execute("CREATE INDEX idx_recipe_name_fts ON recipe USING gin(to_tsvector('english', name))")
|
||||
|
||||
op.create_check_constraint('ck_household_size', 'family_profile', 'household_size > 0')
|
||||
op.create_check_constraint('ck_adult_count', 'family_profile', 'adult_count > 0')
|
||||
op.create_check_constraint('ck_household_sum', 'family_profile', 'adult_count + child_count = household_size')
|
||||
op.create_check_constraint('ck_day_of_week', 'meal_plan_item', 'day_of_week BETWEEN 1 AND 7')
|
||||
op.create_check_constraint('ck_rating', 'feedback', 'rating BETWEEN 1 AND 5')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Seed initial data
|
||||
|
||||
Revision ID: 0002
|
||||
Revises: 0001
|
||||
Create Date: 2026-05-04
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = '0002'
|
||||
down_revision: Union[str, None] = '0001'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
family_id = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
|
||||
adult1_id = 'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'
|
||||
adult2_id = 'c0eebc99-9c0b-4ef8-bb6d-6bb9bd380a12'
|
||||
|
||||
op.execute(f"""
|
||||
INSERT INTO family_profile (id, name, household_size, adult_count, child_count, dietary_notes, budget_per_meal)
|
||||
VALUES ('{family_id}', 'Test Family', 4, 2, 2, '3 of 4 family members do not like mushrooms', 50.00)
|
||||
""")
|
||||
|
||||
op.execute(f"""
|
||||
INSERT INTO family_member (id, family_profile_id, name, email, role, likes_mushrooms)
|
||||
VALUES
|
||||
('{adult1_id}', '{family_id}', 'Adult 1', 'adult1@example.com', 'adult', false),
|
||||
('{adult2_id}', '{family_id}', 'Adult 2', 'adult2@example.com', 'adult', true)
|
||||
""")
|
||||
|
||||
ingredients = [
|
||||
('Chicken Breast', 'chicken breast', 'lb', 'Meat', 8.99),
|
||||
('Chicken Thighs', 'chicken thighs', 'lb', 'Meat', 6.99),
|
||||
('Ground Beef', 'ground beef', 'lb', 'Meat', 7.99),
|
||||
('Ground Turkey', 'ground turkey', 'lb', 'Meat', 7.49),
|
||||
('Pork Chops', 'pork chops', 'lb', 'Meat', 8.49),
|
||||
('Bacon', 'bacon', 'oz', 'Meat', 6.99),
|
||||
('Shrimp', 'shrimp', 'lb', 'Seafood', 12.99),
|
||||
('Salmon', 'salmon', 'lb', 'Seafood', 14.99),
|
||||
('Tilapia', 'tilapia', 'lb', 'Seafood', 9.99),
|
||||
('Rice', 'rice', 'lb', 'Grains', 2.49),
|
||||
('Pasta', 'pasta', 'lb', 'Grains', 1.99),
|
||||
('Bread', 'bread', 'loaf', 'Bakery', 3.49),
|
||||
('Tortillas', 'tortillas', 'pack', 'Bakery', 4.49),
|
||||
('Quinoa', 'quinoa', 'lb', 'Grains', 5.99),
|
||||
('Black Beans', 'black beans', 'can', 'Canned Goods', 1.29),
|
||||
('Chickpeas', 'chickpeas', 'can', 'Canned Goods', 1.49),
|
||||
('Diced Tomatoes', 'diced tomatoes', 'can', 'Canned Goods', 1.99),
|
||||
('Tomato Sauce', 'tomato sauce', 'can', 'Canned Goods', 1.79),
|
||||
('Coconut Milk', 'coconut milk', 'can', 'Canned Goods', 2.49),
|
||||
('Broccoli', 'broccoli', 'bunch', 'Produce', 3.99),
|
||||
('Carrots', 'carrots', 'lb', 'Produce', 2.49),
|
||||
('Spinach', 'spinach', 'bag', 'Produce', 4.99),
|
||||
('Bell Peppers', 'bell peppers', 'each', 'Produce', 1.49),
|
||||
('Onion', 'onion', 'lb', 'Produce', 1.29),
|
||||
('Garlic', 'garlic', 'head', 'Produce', 0.79),
|
||||
('Ginger', 'ginger', 'lb', 'Produce', 4.99),
|
||||
('Potatoes', 'potatoes', 'lb', 'Produce', 1.99),
|
||||
('Sweet Potatoes', 'sweet potatoes', 'lb', 'Produce', 2.29),
|
||||
('Avocado', 'avocado', 'each', 'Produce', 1.99),
|
||||
('Lime', 'lime', 'each', 'Produce', 0.50),
|
||||
('Lemon', 'lemon', 'each', 'Produce', 0.60),
|
||||
('Mushrooms', 'mushrooms', 'oz', 'Produce', 3.99),
|
||||
('Zucchini', 'zucchini', 'lb', 'Produce', 2.49),
|
||||
('Cucumber', 'cucumber', 'each', 'Produce', 1.29),
|
||||
('Romaine Lettuce', 'romaine lettuce', 'head', 'Produce', 2.99),
|
||||
('Cherry Tomatoes', 'cherry tomatoes', 'pint', 'Produce', 4.49),
|
||||
('Cilantro', 'cilantro', 'bunch', 'Produce', 1.29),
|
||||
('Parsley', 'parsley', 'bunch', 'Produce', 1.29),
|
||||
('Milk', 'milk', 'gallon', 'Dairy', 4.49),
|
||||
('Eggs', 'eggs', 'dozen', 'Dairy', 4.99),
|
||||
('Cheese', 'cheese', 'oz', 'Dairy', 5.99),
|
||||
('Butter', 'butter', 'lb', 'Dairy', 4.99),
|
||||
('Greek Yogurt', 'greek yogurt', 'container', 'Dairy', 5.99),
|
||||
('Parmesan', 'parmesan', 'oz', 'Dairy', 7.99),
|
||||
('Mozzarella', 'mozzarella', 'oz', 'Dairy', 5.49),
|
||||
('Soy Sauce', 'soy sauce', 'oz', 'Condiments', 2.99),
|
||||
('Olive Oil', 'olive oil', 'oz', 'Condiments', 9.99),
|
||||
('Vegetable Oil', 'vegetable oil', 'oz', 'Condiments', 4.99),
|
||||
('Honey', 'honey', 'oz', 'Condiments', 7.99),
|
||||
('Peanut Butter', 'peanut butter', 'jar', 'Condiments', 4.49),
|
||||
('Salsa', 'salsa', 'jar', 'Condiments', 3.99),
|
||||
('Chicken Broth', 'chicken broth', 'carton', 'Broths', 3.49),
|
||||
('Vegetable Broth', 'vegetable broth', 'carton', 'Broths', 3.49),
|
||||
('Corn Tortillas', 'corn tortillas', 'pack', 'Bakery', 3.99),
|
||||
('Flour Tortillas', 'flour tortillas', 'pack', 'Bakery', 4.29),
|
||||
('Tofu', 'tofu', 'oz', 'Protein', 4.99),
|
||||
('Almonds', 'almonds', 'oz', 'Snacks', 8.99),
|
||||
('Peanuts', 'peanuts', 'oz', 'Snacks', 5.99),
|
||||
('Breadcrumbs', 'breadcrumbs', 'oz', 'Pantry', 2.99),
|
||||
('Flour', 'flour', 'lb', 'Pantry', 1.99),
|
||||
('Sugar', 'sugar', 'lb', 'Pantry', 2.49),
|
||||
('Brown Rice', 'brown rice', 'lb', 'Grains', 3.49),
|
||||
('Oats', 'oats', 'lb', 'Grains', 2.99),
|
||||
('Chickpeas', 'chickpeas', 'can', 'Canned Goods', 1.49),
|
||||
('Black Beans', 'black beans', 'can', 'Canned Goods', 1.29),
|
||||
('Kidney Beans', 'kidney beans', 'can', 'Canned Goods', 1.29),
|
||||
('Corn', 'corn', 'can', 'Canned Goods', 1.49),
|
||||
('Green Beans', 'green beans', 'can', 'Canned Goods', 1.49),
|
||||
('Diced Green Chiles', 'diced green chiles', 'can', 'Canned Goods', 1.29),
|
||||
]
|
||||
|
||||
for name, name_lower, unit, aisle, price in ingredients:
|
||||
op.execute(f"""
|
||||
INSERT INTO ingredient (id, name, name_lower, unit, aisle, typical_price)
|
||||
VALUES (uuid_generate_v4(), '{name}', '{name_lower}', '{unit}', '{aisle}', {price})
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
Reference in New Issue
Block a user