Files
foodplanner/main.py
2025-09-29 17:27:02 -07:00

269 lines
11 KiB
Python

print("DEBUG: main.py started")
# Meal Planner FastAPI Application
# Run with: uvicorn main:app --reload
from fastapi import FastAPI, Depends, HTTPException, Request, Form, Body
from contextlib import asynccontextmanager
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from sqlalchemy import or_
from sqlalchemy.orm import Session
from typing import List, Optional
from datetime import date, datetime
import os
import csv
import requests
from fastapi import File, UploadFile
import logging
from alembic.config import Config
from alembic import command
from apscheduler.schedulers.background import BackgroundScheduler
import shutil
import sqlite3
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Import database components from the database module
from app.database import DATABASE_URL, engine, Base, get_db, SessionLocal, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedMeal, FoodCreate, FoodResponse, calculate_meal_nutrition, calculate_day_nutrition, calculate_day_nutrition_tracked
# Initialize FastAPI app
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup
logging.info("DEBUG: Startup event triggered")
run_migrations()
logging.info("DEBUG: Startup event completed")
# Schedule the backup job - temporarily disabled for debugging
scheduler = BackgroundScheduler()
scheduler.add_job(scheduled_backup, 'cron', hour=0)
scheduler.start()
logging.info("Scheduled backup job started.")
yield
# Shutdown
scheduler.shutdown()
app = FastAPI(title="Meal Planner", lifespan=lifespan)
templates = Jinja2Templates(directory="templates")
from app.api.routes import foods, meals, plans, templates as templates_router, weekly_menu, tracker, admin, export
app.include_router(foods.router, tags=["foods"])
app.include_router(meals.router, tags=["meals"])
app.include_router(plans.router, tags=["plans"])
app.include_router(templates_router.router, tags=["templates"])
app.include_router(weekly_menu.router, tags=["weekly_menu"])
app.include_router(tracker.router, tags=["tracker"])
app.include_router(admin.router, tags=["admin"])
app.include_router(export.router, tags=["export"])
# Add a logging middleware to see incoming requests
@app.middleware("http")
async def log_requests(request: Request, call_next):
logging.info(f"Incoming request: {request.method} {request.url.path}")
response = await call_next(request)
return response
# Get the port from environment variable or default to 8999
PORT = int(os.getenv("PORT", 8999))
# This will be called if running directly with Python
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=PORT)
# Import Pydantic models from the database module
from app.database import FoodCreate, FoodResponse, MealCreate, TrackedDayCreate, TrackedMealCreate, FoodExport, MealFoodExport, MealExport, PlanExport, TemplateMealExport, TemplateExport, TemplateMealDetail, TemplateDetail, WeeklyMenuDayExport, WeeklyMenuDayDetail, WeeklyMenuExport, WeeklyMenuDetail, TrackedMealExport, TrackedDayExport, AllData, TrackedDay
def backup_database(source_db_path, backup_db_path):
"""Backs up an SQLite database using the online backup API."""
logging.info(f"DEBUG: Starting backup - source: {source_db_path}, backup: {backup_db_path}")
import tempfile
try:
# Check if source database exists
if not os.path.exists(source_db_path):
logging.error(f"DEBUG: Source database file does not exist: {source_db_path}")
return False
# Create backup in temporary directory first (local fast storage)
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file:
temp_backup_path = temp_file.name
logging.info(f"DEBUG: Creating temporary backup at: {temp_backup_path}")
# Backup to local temp file (fast)
source_conn = sqlite3.connect(source_db_path)
temp_conn = sqlite3.connect(temp_backup_path)
with temp_conn:
source_conn.backup(temp_conn)
source_conn.close()
temp_conn.close()
logging.info(f"DEBUG: Temporary backup created, copying to final destination")
# Ensure backup directory exists
backup_dir = os.path.dirname(backup_db_path)
if backup_dir and not os.path.exists(backup_dir):
logging.info(f"DEBUG: Creating backup directory: {backup_dir}")
os.makedirs(backup_dir, exist_ok=True)
# Copy to NAS (this may be slow but won't block SQLite)
shutil.copy2(temp_backup_path, backup_db_path)
# Clean up temp file
os.unlink(temp_backup_path)
logging.info(f"Backup of '{source_db_path}' created successfully at '{backup_db_path}'")
return True
except sqlite3.Error as e:
logging.error(f"SQLite error during backup: {e}", exc_info=True)
return False
except Exception as e:
logging.error(f"Unexpected error during backup: {e}", exc_info=True)
return False
finally:
# Cleanup temp file if it still exists
if 'temp_backup_path' in locals() and os.path.exists(temp_backup_path):
try:
os.unlink(temp_backup_path)
except:
pass
def scheduled_backup():
"""Create a backup of the database."""
db_path = DATABASE_URL.split("///")[1]
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
backup_dir = "./backups"
# Ensure backup directory exists
os.makedirs(backup_dir, exist_ok=True)
backup_path = os.path.join(backup_dir, f"meal_planner_{timestamp}.db")
backup_database(db_path, backup_path)
def test_sqlite_connection(db_path):
"""Test if we can create and write to SQLite database file"""
logging.info(f"DEBUG: Starting SQLite connection test for path: {db_path}")
try:
import sqlite3
import stat
import os
# Log directory permissions
db_dir = os.path.dirname(db_path)
logging.info(f"DEBUG: Checking database directory: {db_dir}")
if os.path.exists(db_dir):
dir_stat = os.stat(db_dir)
dir_perm = stat.filemode(dir_stat.st_mode)
dir_uid = dir_stat.st_uid
dir_gid = dir_stat.st_gid
logging.info(f"DEBUG: Database directory permissions: {dir_perm}, UID:{dir_uid}, GID:{dir_gid}")
# Test write access
test_file = os.path.join(db_dir, "write_test.txt")
logging.info(f"DEBUG: Testing write access with file: {test_file}")
try:
with open(test_file, "w") as f:
f.write("test")
os.remove(test_file)
logging.info("DEBUG: Write test to directory succeeded")
except Exception as e:
logging.error(f"DEBUG: Write test to directory failed: {e}")
return False
else:
logging.warning(f"DEBUG: Database directory does not exist: {db_dir}")
return False
# Test SQLite operations
logging.info("DEBUG: Attempting SQLite connection...")
conn = sqlite3.connect(db_path)
logging.info("DEBUG: SQLite connection established")
cursor = conn.cursor()
logging.info("DEBUG: Creating test table...")
cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
logging.info("DEBUG: Test table created")
logging.info("DEBUG: Inserting test data...")
cursor.execute("INSERT INTO test VALUES (1)")
logging.info("DEBUG: Test data inserted")
logging.info("DEBUG: Committing transaction...")
conn.commit()
logging.info("DEBUG: Transaction committed")
logging.info("DEBUG: Dropping test table...")
cursor.execute("DROP TABLE test")
logging.info("DEBUG: Test table dropped")
logging.info("DEBUG: Closing connection...")
conn.close()
logging.info("DEBUG: Connection closed")
# Log file permissions
if os.path.exists(db_path):
file_stat = os.stat(db_path)
file_perm = stat.filemode(file_stat.st_mode)
file_uid = file_stat.st_uid
file_gid = file_stat.st_gid
logging.info(f"DEBUG: Database file permissions: {file_perm}, UID:{file_uid}, GID:{file_gid}")
logging.info("DEBUG: SQLite connection test completed successfully")
return True
except Exception as e:
logging.error(f"DEBUG: SQLite connection test failed: {e}", exc_info=True)
return False
def run_migrations():
logging.info("DEBUG: Starting database setup...")
try:
# Extract database path from URL
db_path = DATABASE_URL.split("///")[1]
logging.info(f"DEBUG: Database path extracted: {db_path}")
# Create directory if needed
db_dir = os.path.dirname(db_path)
logging.info(f"DEBUG: Database directory: {db_dir}")
if not os.path.exists(db_dir):
logging.info(f"DEBUG: Creating database directory: {db_dir}")
os.makedirs(db_dir, exist_ok=True)
logging.info(f"DEBUG: Database directory created successfully")
else:
logging.info(f"DEBUG: Database directory already exists")
# Test SQLite connection
logging.info("DEBUG: Testing SQLite connection...")
if not test_sqlite_connection(db_path):
logging.error("DEBUG: SQLite connection test failed")
raise Exception("SQLite connection test failed")
logging.info("DEBUG: SQLite connection test passed")
# Create all tables using SQLAlchemy directly instead of alembic
logging.info("DEBUG: Creating database tables using SQLAlchemy...")
Base.metadata.create_all(bind=engine)
logging.info("DEBUG: Database tables created successfully.")
logging.info("DEBUG: Database setup completed, returning to caller")
except Exception as e:
logging.error(f"DEBUG: Failed to setup database: {e}", exc_info=True)
raise
# Routes
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/tracker", status_code=302)
# Add a simple test route to confirm routing is working
@app.get("/test")
async def test_route():
logging.info("DEBUG: Test route called")
return {"status": "success", "message": "Test route is working"}