from fastapi import FastAPI, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware from sqlalchemy.orm import Session from sqlalchemy import text from typing import List import models, schemas from database import engine, get_db # Create database tables models.Base.metadata.create_all(bind=engine) app = FastAPI( title="Grocery Tracker API", description="API for tracking grocery prices and shopping events", version="1.0.0" ) # CORS middleware for React frontend app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000", "http://localhost:5173"], # React dev servers allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) def build_shopping_event_response(event: models.ShoppingEvent, db: Session) -> schemas.ShoppingEventResponse: """Build a shopping event response with groceries from the association table""" # Get groceries with their event-specific data grocery_data = db.execute( text(""" SELECT g.id, g.name, g.category, g.organic, g.weight, g.weight_unit, seg.amount, seg.price FROM groceries g JOIN shopping_event_groceries seg ON g.id = seg.grocery_id WHERE seg.shopping_event_id = :event_id """), {"event_id": event.id} ).fetchall() # Convert to GroceryWithEventData objects groceries_with_data = [ schemas.GroceryWithEventData( id=row.id, name=row.name, category=row.category, organic=row.organic, weight=row.weight, weight_unit=row.weight_unit, amount=row.amount, price=row.price ) for row in grocery_data ] return schemas.ShoppingEventResponse( id=event.id, shop_id=event.shop_id, date=event.date, total_amount=event.total_amount, notes=event.notes, created_at=event.created_at, shop=event.shop, groceries=groceries_with_data ) # Root endpoint @app.get("/") def read_root(): return {"message": "Grocery Tracker API", "version": "1.0.0"} # Grocery endpoints @app.post("/groceries/", response_model=schemas.Grocery) def create_grocery(grocery: schemas.GroceryCreate, db: Session = Depends(get_db)): db_grocery = models.Grocery(**grocery.dict()) db.add(db_grocery) db.commit() db.refresh(db_grocery) return db_grocery @app.get("/groceries/", response_model=List[schemas.Grocery]) def read_groceries(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): groceries = db.query(models.Grocery).offset(skip).limit(limit).all() return groceries @app.get("/groceries/{grocery_id}", response_model=schemas.Grocery) def read_grocery(grocery_id: int, db: Session = Depends(get_db)): grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() if grocery is None: raise HTTPException(status_code=404, detail="Grocery not found") return grocery @app.put("/groceries/{grocery_id}", response_model=schemas.Grocery) def update_grocery(grocery_id: int, grocery_update: schemas.GroceryUpdate, db: Session = Depends(get_db)): grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() if grocery is None: raise HTTPException(status_code=404, detail="Grocery not found") update_data = grocery_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(grocery, field, value) db.commit() db.refresh(grocery) return grocery @app.delete("/groceries/{grocery_id}") def delete_grocery(grocery_id: int, db: Session = Depends(get_db)): grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_id).first() if grocery is None: raise HTTPException(status_code=404, detail="Grocery not found") db.delete(grocery) db.commit() return {"message": "Grocery deleted successfully"} # Shop endpoints @app.post("/shops/", response_model=schemas.Shop) def create_shop(shop: schemas.ShopCreate, db: Session = Depends(get_db)): db_shop = models.Shop(**shop.dict()) db.add(db_shop) db.commit() db.refresh(db_shop) return db_shop @app.get("/shops/", response_model=List[schemas.Shop]) def read_shops(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): shops = db.query(models.Shop).offset(skip).limit(limit).all() return shops @app.get("/shops/{shop_id}", response_model=schemas.Shop) def read_shop(shop_id: int, db: Session = Depends(get_db)): shop = db.query(models.Shop).filter(models.Shop.id == shop_id).first() if shop is None: raise HTTPException(status_code=404, detail="Shop not found") return shop @app.put("/shops/{shop_id}", response_model=schemas.Shop) def update_shop(shop_id: int, shop_update: schemas.ShopUpdate, db: Session = Depends(get_db)): shop = db.query(models.Shop).filter(models.Shop.id == shop_id).first() if shop is None: raise HTTPException(status_code=404, detail="Shop not found") update_data = shop_update.dict() for field, value in update_data.items(): setattr(shop, field, value) db.commit() db.refresh(shop) return shop @app.delete("/shops/{shop_id}") def delete_shop(shop_id: int, db: Session = Depends(get_db)): shop = db.query(models.Shop).filter(models.Shop.id == shop_id).first() if shop is None: raise HTTPException(status_code=404, detail="Shop not found") db.delete(shop) db.commit() return {"message": "Shop deleted successfully"} # Shopping Event endpoints @app.post("/shopping-events/", response_model=schemas.ShoppingEventResponse) def create_shopping_event(event: schemas.ShoppingEventCreate, db: Session = Depends(get_db)): # Verify shop exists shop = db.query(models.Shop).filter(models.Shop.id == event.shop_id).first() if shop is None: raise HTTPException(status_code=404, detail="Shop not found") # Create shopping event db_event = models.ShoppingEvent( shop_id=event.shop_id, date=event.date, total_amount=event.total_amount, notes=event.notes ) db.add(db_event) db.commit() db.refresh(db_event) # Add groceries to the event for grocery_item in event.groceries: grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first() if grocery is None: raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found") # Insert into association table db.execute( models.shopping_event_groceries.insert().values( shopping_event_id=db_event.id, grocery_id=grocery_item.grocery_id, amount=grocery_item.amount, price=grocery_item.price ) ) db.commit() db.refresh(db_event) return build_shopping_event_response(db_event, db) @app.get("/shopping-events/", response_model=List[schemas.ShoppingEventResponse]) def read_shopping_events(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): events = db.query(models.ShoppingEvent).offset(skip).limit(limit).all() return [build_shopping_event_response(event, db) for event in events] @app.get("/shopping-events/{event_id}", response_model=schemas.ShoppingEventResponse) def read_shopping_event(event_id: int, db: Session = Depends(get_db)): event = db.query(models.ShoppingEvent).filter(models.ShoppingEvent.id == event_id).first() if event is None: raise HTTPException(status_code=404, detail="Shopping event not found") return build_shopping_event_response(event, db) @app.put("/shopping-events/{event_id}", response_model=schemas.ShoppingEventResponse) def update_shopping_event(event_id: int, event_update: schemas.ShoppingEventCreate, db: Session = Depends(get_db)): # Get the existing event event = db.query(models.ShoppingEvent).filter(models.ShoppingEvent.id == event_id).first() if event is None: raise HTTPException(status_code=404, detail="Shopping event not found") # Verify shop exists shop = db.query(models.Shop).filter(models.Shop.id == event_update.shop_id).first() if shop is None: raise HTTPException(status_code=404, detail="Shop not found") # Update the shopping event event.shop_id = event_update.shop_id event.date = event_update.date event.total_amount = event_update.total_amount event.notes = event_update.notes # Remove existing grocery associations db.execute( models.shopping_event_groceries.delete().where( models.shopping_event_groceries.c.shopping_event_id == event_id ) ) # Add new grocery associations for grocery_item in event_update.groceries: grocery = db.query(models.Grocery).filter(models.Grocery.id == grocery_item.grocery_id).first() if grocery is None: raise HTTPException(status_code=404, detail=f"Grocery with id {grocery_item.grocery_id} not found") # Insert into association table db.execute( models.shopping_event_groceries.insert().values( shopping_event_id=event_id, grocery_id=grocery_item.grocery_id, amount=grocery_item.amount, price=grocery_item.price ) ) db.commit() db.refresh(event) return build_shopping_event_response(event, db) @app.delete("/shopping-events/{event_id}") def delete_shopping_event(event_id: int, db: Session = Depends(get_db)): event = db.query(models.ShoppingEvent).filter(models.ShoppingEvent.id == event_id).first() if event is None: raise HTTPException(status_code=404, detail="Shopping event not found") # Delete grocery associations first db.execute( models.shopping_event_groceries.delete().where( models.shopping_event_groceries.c.shopping_event_id == event_id ) ) # Delete the shopping event db.delete(event) db.commit() return {"message": "Shopping event deleted successfully"} # Statistics endpoints @app.get("/stats/categories", response_model=List[schemas.CategoryStats]) def get_category_stats(db: Session = Depends(get_db)): # This would need more complex SQL query - placeholder for now return [] @app.get("/stats/shops", response_model=List[schemas.ShopStats]) def get_shop_stats(db: Session = Depends(get_db)): # This would need more complex SQL query - placeholder for now return [] if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)