Spaces:
Sleeping
Sleeping
Sakshi
commited on
Commit
·
96f792c
1
Parent(s):
4f679f4
nutrition
Browse files- .DS_Store +0 -0
- Dockerfile +31 -0
- README.md +130 -12
- __pycache__/main.cpython-311.pyc +0 -0
- __pycache__/main.cpython-312.pyc +0 -0
- add_image_column.py +20 -0
- app/.DS_Store +0 -0
- app/__init__.py +0 -0
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/auth.cpython-311.pyc +0 -0
- app/__pycache__/auth.cpython-312.pyc +0 -0
- app/__pycache__/database.cpython-311.pyc +0 -0
- app/__pycache__/database.cpython-312.pyc +0 -0
- app/__pycache__/models.cpython-311.pyc +0 -0
- app/__pycache__/models.cpython-312.pyc +0 -0
- app/__pycache__/schemas.cpython-311.pyc +0 -0
- app/__pycache__/schemas.cpython-312.pyc +0 -0
- app/auth.py +33 -0
- app/database.py +19 -0
- app/models.py +47 -0
- app/routes/__init__.py +0 -0
- app/routes/__pycache__/__init__.cpython-311.pyc +0 -0
- app/routes/__pycache__/__init__.cpython-312.pyc +0 -0
- app/routes/__pycache__/admin.cpython-311.pyc +0 -0
- app/routes/__pycache__/admin.cpython-312.pyc +0 -0
- app/routes/__pycache__/nutrition.cpython-311.pyc +0 -0
- app/routes/__pycache__/nutrition.cpython-312.pyc +0 -0
- app/routes/__pycache__/user.cpython-311.pyc +0 -0
- app/routes/__pycache__/user.cpython-312.pyc +0 -0
- app/routes/admin.py +93 -0
- app/routes/nutrition.py +180 -0
- app/routes/user.py +93 -0
- app/schemas.py +86 -0
- create_admin.py +41 -0
- main.py +36 -0
- nutrition_app.db +0 -0
- pyproject.toml +19 -0
- requirements.txt +12 -0
- uv.lock +0 -0
- verify_image_upload.py +109 -0
.DS_Store
ADDED
|
Binary file (8.2 kB). View file
|
|
|
Dockerfile
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
# Set working directory
|
| 4 |
+
WORKDIR /app
|
| 5 |
+
|
| 6 |
+
# Install system dependencies (tesseract-ocr)
|
| 7 |
+
RUN apt-get update && apt-get install -y \
|
| 8 |
+
tesseract-ocr \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements file
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
|
| 14 |
+
# Install python dependencies
|
| 15 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
+
|
| 17 |
+
# Copy application code
|
| 18 |
+
COPY . .
|
| 19 |
+
|
| 20 |
+
# Create .tmp directory and set permissions
|
| 21 |
+
RUN mkdir -p .tmp && chmod 777 .tmp
|
| 22 |
+
|
| 23 |
+
# Create a non-root user (Hugging Face requirement)
|
| 24 |
+
RUN useradd -m -u 1000 user
|
| 25 |
+
USER user
|
| 26 |
+
|
| 27 |
+
# Expose the port
|
| 28 |
+
EXPOSE 7860
|
| 29 |
+
|
| 30 |
+
# Command to run the application
|
| 31 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,12 +1,130 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Nutrition Analysis API
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
A Python FastAPI backend system that provides comprehensive nutritional analysis and health recommendations. The system manages user authentication with role-based access (admin and normal users), product database management, health issue tracking, and AI-powered nutritional analysis using OCR and Google's Gemini API.
|
| 5 |
+
|
| 6 |
+
## Features
|
| 7 |
+
|
| 8 |
+
### Authentication & User Management
|
| 9 |
+
- **Role-Based Access Control**: Admin and normal user roles
|
| 10 |
+
- **JWT Token Authentication**: Secure authentication using JSON Web Tokens
|
| 11 |
+
- **User Registration & Login**: Email and username-based registration
|
| 12 |
+
- **Password Security**: Bcrypt password hashing
|
| 13 |
+
|
| 14 |
+
### Admin Capabilities
|
| 15 |
+
- Add products with complete nutrition facts to the database
|
| 16 |
+
- List all products in the system
|
| 17 |
+
- Delete products from the database
|
| 18 |
+
|
| 19 |
+
### User Features
|
| 20 |
+
- Manage personal health profile
|
| 21 |
+
- Add/track health issues (diabetes, hypertension, cholesterol, etc.)
|
| 22 |
+
- View and manage health issue records
|
| 23 |
+
- Upload nutrition label images for analysis
|
| 24 |
+
|
| 25 |
+
### AI-Powered Nutrition Analysis
|
| 26 |
+
- **OCR Processing**: Extract nutrition facts from images using Tesseract OCR
|
| 27 |
+
- **Gemini AI Integration**: Analyze nutrition data with Google's Gemini API
|
| 28 |
+
- **Health Rating**: Products rated on a 1-10 scale based on nutritional value
|
| 29 |
+
- **Personalized Recommendations**: Health-specific advice based on user's tracked health issues
|
| 30 |
+
- **Alternative Suggestions**: Healthier product alternatives from the admin database
|
| 31 |
+
|
| 32 |
+
## Project Structure
|
| 33 |
+
|
| 34 |
+
```
|
| 35 |
+
.
|
| 36 |
+
├── app/
|
| 37 |
+
│ ├── __init__.py
|
| 38 |
+
│ ├── database.py # SQLite database configuration
|
| 39 |
+
│ ├── models.py # SQLAlchemy ORM models
|
| 40 |
+
│ ├── schemas.py # Pydantic validation schemas
|
| 41 |
+
│ ├── auth.py # JWT authentication utilities
|
| 42 |
+
│ └── routes/
|
| 43 |
+
│ ├── __init__.py
|
| 44 |
+
│ ├── admin.py # Admin endpoints
|
| 45 |
+
│ ├── user.py # User auth and health management
|
| 46 |
+
│ └── nutrition.py # OCR and AI analysis endpoints
|
| 47 |
+
├── main.py # FastAPI application entry point
|
| 48 |
+
├── nutrition_app.db # SQLite database (auto-generated)
|
| 49 |
+
└── pyproject.toml # Python dependencies
|
| 50 |
+
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Database Schema
|
| 54 |
+
|
| 55 |
+
### Users Table
|
| 56 |
+
- id, username, email, hashed_password, role (admin/user)
|
| 57 |
+
|
| 58 |
+
### Products Table
|
| 59 |
+
- id, name, brand, calories, protein, fat, carbohydrates, sodium, sugar, fiber, cholesterol, serving_size
|
| 60 |
+
|
| 61 |
+
### Health Issues Table
|
| 62 |
+
- id, user_id (FK), issue_type, severity, notes
|
| 63 |
+
|
| 64 |
+
## API Endpoints
|
| 65 |
+
|
| 66 |
+
### Authentication
|
| 67 |
+
- `POST /auth/register` - Register new user
|
| 68 |
+
- `POST /auth/login` - Login and get JWT token
|
| 69 |
+
- `GET /user/me` - Get current user info
|
| 70 |
+
|
| 71 |
+
### User Health Management
|
| 72 |
+
- `POST /user/health-issues` - Add health issue
|
| 73 |
+
- `GET /user/health-issues` - List user's health issues
|
| 74 |
+
- `DELETE /user/health-issues/{id}` - Delete health issue
|
| 75 |
+
|
| 76 |
+
### Admin Product Management
|
| 77 |
+
- `POST /admin/products` - Add new product (admin only)
|
| 78 |
+
- `GET /admin/products` - List all products (admin only)
|
| 79 |
+
- `DELETE /admin/products/{id}` - Delete product (admin only)
|
| 80 |
+
- `POST /admin/users/{user_id}/promote` - Promote user to admin role (admin only)
|
| 81 |
+
|
| 82 |
+
### Nutrition Analysis
|
| 83 |
+
- `POST /nutrition/analyze` - Upload image for nutrition analysis
|
| 84 |
+
|
| 85 |
+
## Environment Variables
|
| 86 |
+
|
| 87 |
+
- `SESSION_SECRET` - JWT secret key (auto-configured by)
|
| 88 |
+
- `GEMINI_API_KEY` - Google Gemini API key (required for AI analysis)
|
| 89 |
+
|
| 90 |
+
## Security Notes
|
| 91 |
+
|
| 92 |
+
### Creating Admin Users
|
| 93 |
+
For security, all new user registrations default to normal user role. To create admin users:
|
| 94 |
+
1. Register a regular user account via `POST /auth/register`
|
| 95 |
+
2. Manually promote the user to admin using one of these methods:
|
| 96 |
+
- Use an existing admin account to call `POST /admin/users/{user_id}/promote`
|
| 97 |
+
- Directly modify the database to set the first admin (SQLite: `UPDATE users SET role='admin' WHERE id=1;`)
|
| 98 |
+
3. Once you have at least one admin, use the promotion endpoint for additional admins
|
| 99 |
+
|
| 100 |
+
### Production Deployment
|
| 101 |
+
- Ensure `SESSION_SECRET` is set to a strong, random value in production
|
| 102 |
+
- Keep `GEMINI_API_KEY` secure and never expose it in client-side code
|
| 103 |
+
- Consider adding rate limiting for authentication endpoints
|
| 104 |
+
- Regularly audit admin user accounts
|
| 105 |
+
|
| 106 |
+
## Recent Changes
|
| 107 |
+
|
| 108 |
+
- **2025-11-17**: Initial project setup with complete FastAPI backend implementation
|
| 109 |
+
- Configured SQLite database with SQLAlchemy ORM
|
| 110 |
+
- Implemented secure JWT-based authentication system with role-based access control
|
| 111 |
+
- Created admin and user role-based endpoints
|
| 112 |
+
- Integrated Tesseract OCR for nutrition label extraction
|
| 113 |
+
- Added Gemini API integration for AI-powered analysis
|
| 114 |
+
- Set up comprehensive error handling and validation
|
| 115 |
+
- Fixed critical security vulnerability: removed self-service admin role assignment
|
| 116 |
+
- Added admin-only user promotion endpoint
|
| 117 |
+
|
| 118 |
+
## Technology Stack
|
| 119 |
+
|
| 120 |
+
- **Framework**: FastAPI
|
| 121 |
+
- **Database**: SQLite with SQLAlchemy ORM
|
| 122 |
+
- **Authentication**: JWT (python-jose) + bcrypt
|
| 123 |
+
- **OCR**: Tesseract + pytesseract
|
| 124 |
+
- **AI**: Google Gemini API
|
| 125 |
+
- **Image Processing**: Pillow
|
| 126 |
+
- **Server**: Uvicorn ASGI server
|
| 127 |
+
|
| 128 |
+
## User Preferences
|
| 129 |
+
|
| 130 |
+
None specified yet.
|
__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (1.75 kB). View file
|
|
|
__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (1.59 kB). View file
|
|
|
add_image_column.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
|
| 3 |
+
def migrate():
|
| 4 |
+
conn = sqlite3.connect("nutrition_app.db")
|
| 5 |
+
cursor = conn.cursor()
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
cursor.execute("ALTER TABLE products ADD COLUMN image_path TEXT")
|
| 9 |
+
conn.commit()
|
| 10 |
+
print("Successfully added image_path column to products table")
|
| 11 |
+
except sqlite3.OperationalError as e:
|
| 12 |
+
if "duplicate column name" in str(e):
|
| 13 |
+
print("Column image_path already exists")
|
| 14 |
+
else:
|
| 15 |
+
print(f"Error: {e}")
|
| 16 |
+
finally:
|
| 17 |
+
conn.close()
|
| 18 |
+
|
| 19 |
+
if __name__ == "__main__":
|
| 20 |
+
migrate()
|
app/.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
app/__init__.py
ADDED
|
File without changes
|
app/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (147 Bytes). View file
|
|
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (155 Bytes). View file
|
|
|
app/__pycache__/auth.cpython-311.pyc
ADDED
|
Binary file (4.13 kB). View file
|
|
|
app/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (2.01 kB). View file
|
|
|
app/__pycache__/database.cpython-311.pyc
ADDED
|
Binary file (996 Bytes). View file
|
|
|
app/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (871 Bytes). View file
|
|
|
app/__pycache__/models.cpython-311.pyc
ADDED
|
Binary file (3.15 kB). View file
|
|
|
app/__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (2.58 kB). View file
|
|
|
app/__pycache__/schemas.cpython-311.pyc
ADDED
|
Binary file (5.61 kB). View file
|
|
|
app/__pycache__/schemas.cpython-312.pyc
ADDED
|
Binary file (4.52 kB). View file
|
|
|
app/auth.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Optional
|
| 2 |
+
from fastapi import Depends, HTTPException, status, Header
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from app.database import get_db
|
| 5 |
+
from app import models
|
| 6 |
+
|
| 7 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 8 |
+
return plain_password == hashed_password
|
| 9 |
+
|
| 10 |
+
def get_password_hash(password: str) -> str:
|
| 11 |
+
return password
|
| 12 |
+
|
| 13 |
+
def get_current_user(username: Optional[str] = Header(None, alias="X-Username"), db: Session = Depends(get_db)):
|
| 14 |
+
credentials_exception = HTTPException(
|
| 15 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 16 |
+
detail="Authentication required"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
if not username:
|
| 20 |
+
raise credentials_exception
|
| 21 |
+
|
| 22 |
+
user = db.query(models.User).filter(models.User.username == username).first()
|
| 23 |
+
if user is None:
|
| 24 |
+
raise credentials_exception
|
| 25 |
+
return user
|
| 26 |
+
|
| 27 |
+
def get_current_admin_user(current_user: models.User = Depends(get_current_user)):
|
| 28 |
+
if current_user.role != models.UserRole.ADMIN:
|
| 29 |
+
raise HTTPException(
|
| 30 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 31 |
+
detail="Admin access required"
|
| 32 |
+
)
|
| 33 |
+
return current_user
|
app/database.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
|
| 5 |
+
SQLALCHEMY_DATABASE_URL = "sqlite:///./nutrition_app.db"
|
| 6 |
+
|
| 7 |
+
engine = create_engine(
|
| 8 |
+
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
| 9 |
+
)
|
| 10 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 11 |
+
|
| 12 |
+
Base = declarative_base()
|
| 13 |
+
|
| 14 |
+
def get_db():
|
| 15 |
+
db = SessionLocal()
|
| 16 |
+
try:
|
| 17 |
+
yield db
|
| 18 |
+
finally:
|
| 19 |
+
db.close()
|
app/models.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Float, ForeignKey, Enum
|
| 2 |
+
from sqlalchemy.orm import relationship
|
| 3 |
+
from app.database import Base
|
| 4 |
+
import enum
|
| 5 |
+
|
| 6 |
+
class UserRole(str, enum.Enum):
|
| 7 |
+
ADMIN = "admin"
|
| 8 |
+
USER = "user"
|
| 9 |
+
|
| 10 |
+
class User(Base):
|
| 11 |
+
__tablename__ = "users"
|
| 12 |
+
|
| 13 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 14 |
+
username = Column(String, unique=True, index=True, nullable=False)
|
| 15 |
+
email = Column(String, unique=True, index=True, nullable=False)
|
| 16 |
+
hashed_password = Column(String, nullable=False)
|
| 17 |
+
role = Column(Enum(UserRole), default=UserRole.USER, nullable=False)
|
| 18 |
+
|
| 19 |
+
health_issues = relationship("HealthIssue", back_populates="user", cascade="all, delete-orphan")
|
| 20 |
+
|
| 21 |
+
class Product(Base):
|
| 22 |
+
__tablename__ = "products"
|
| 23 |
+
|
| 24 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 25 |
+
name = Column(String, index=True, nullable=False)
|
| 26 |
+
brand = Column(String, nullable=True)
|
| 27 |
+
calories = Column(Float, nullable=False)
|
| 28 |
+
protein = Column(Float, nullable=False)
|
| 29 |
+
fat = Column(Float, nullable=False)
|
| 30 |
+
carbohydrates = Column(Float, nullable=False)
|
| 31 |
+
sodium = Column(Float, nullable=False)
|
| 32 |
+
sugar = Column(Float, nullable=False)
|
| 33 |
+
fiber = Column(Float, nullable=True)
|
| 34 |
+
cholesterol = Column(Float, nullable=True)
|
| 35 |
+
serving_size = Column(String, nullable=True)
|
| 36 |
+
image_path = Column(String, nullable=True)
|
| 37 |
+
|
| 38 |
+
class HealthIssue(Base):
|
| 39 |
+
__tablename__ = "health_issues"
|
| 40 |
+
|
| 41 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 42 |
+
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
| 43 |
+
issue_type = Column(String, nullable=False)
|
| 44 |
+
severity = Column(String, nullable=True)
|
| 45 |
+
notes = Column(String, nullable=True)
|
| 46 |
+
|
| 47 |
+
user = relationship("User", back_populates="health_issues")
|
app/routes/__init__.py
ADDED
|
File without changes
|
app/routes/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (154 Bytes). View file
|
|
|
app/routes/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (162 Bytes). View file
|
|
|
app/routes/__pycache__/admin.cpython-311.pyc
ADDED
|
Binary file (4.22 kB). View file
|
|
|
app/routes/__pycache__/admin.cpython-312.pyc
ADDED
|
Binary file (5.03 kB). View file
|
|
|
app/routes/__pycache__/nutrition.cpython-311.pyc
ADDED
|
Binary file (11.7 kB). View file
|
|
|
app/routes/__pycache__/nutrition.cpython-312.pyc
ADDED
|
Binary file (7.61 kB). View file
|
|
|
app/routes/__pycache__/user.cpython-311.pyc
ADDED
|
Binary file (6.78 kB). View file
|
|
|
app/routes/__pycache__/user.cpython-312.pyc
ADDED
|
Binary file (5.97 kB). View file
|
|
|
app/routes/admin.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile, Form
|
| 2 |
+
import os
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from typing import List
|
| 5 |
+
from app.database import get_db
|
| 6 |
+
from app import models, schemas
|
| 7 |
+
from app.auth import get_current_admin_user
|
| 8 |
+
|
| 9 |
+
router = APIRouter(prefix="/admin", tags=["admin"])
|
| 10 |
+
|
| 11 |
+
@router.post("/users/{user_id}/promote", response_model=schemas.UserResponse)
|
| 12 |
+
def promote_user_to_admin(
|
| 13 |
+
user_id: int,
|
| 14 |
+
db: Session = Depends(get_db),
|
| 15 |
+
current_user: models.User = Depends(get_current_admin_user)
|
| 16 |
+
):
|
| 17 |
+
user = db.query(models.User).filter(models.User.id == user_id).first()
|
| 18 |
+
if not user:
|
| 19 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 20 |
+
user.role = models.UserRole.ADMIN
|
| 21 |
+
db.commit()
|
| 22 |
+
db.refresh(user)
|
| 23 |
+
return user
|
| 24 |
+
|
| 25 |
+
@router.post("/products", response_model=schemas.ProductResponse, status_code=status.HTTP_201_CREATED)
|
| 26 |
+
def create_product(
|
| 27 |
+
name: str = Form(...),
|
| 28 |
+
brand: str = Form(None),
|
| 29 |
+
calories: float = Form(...),
|
| 30 |
+
protein: float = Form(...),
|
| 31 |
+
fat: float = Form(...),
|
| 32 |
+
carbohydrates: float = Form(...),
|
| 33 |
+
sodium: float = Form(...),
|
| 34 |
+
sugar: float = Form(...),
|
| 35 |
+
fiber: float = Form(None),
|
| 36 |
+
cholesterol: float = Form(None),
|
| 37 |
+
serving_size: str = Form(None),
|
| 38 |
+
image: UploadFile = File(None),
|
| 39 |
+
db: Session = Depends(get_db),
|
| 40 |
+
current_user: models.User = Depends(get_current_admin_user)
|
| 41 |
+
):
|
| 42 |
+
image_path = None
|
| 43 |
+
if image:
|
| 44 |
+
# Create .tmp directory if it doesn't exist
|
| 45 |
+
os.makedirs(".tmp", exist_ok=True)
|
| 46 |
+
file_location = f".tmp/{image.filename}"
|
| 47 |
+
with open(file_location, "wb+") as file_object:
|
| 48 |
+
file_object.write(image.file.read())
|
| 49 |
+
image_path = file_location
|
| 50 |
+
|
| 51 |
+
product_data = {
|
| 52 |
+
"name": name,
|
| 53 |
+
"brand": brand,
|
| 54 |
+
"calories": calories,
|
| 55 |
+
"protein": protein,
|
| 56 |
+
"fat": fat,
|
| 57 |
+
"carbohydrates": carbohydrates,
|
| 58 |
+
"sodium": sodium,
|
| 59 |
+
"sugar": sugar,
|
| 60 |
+
"fiber": fiber,
|
| 61 |
+
"cholesterol": cholesterol,
|
| 62 |
+
"serving_size": serving_size,
|
| 63 |
+
"image_path": image_path
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
db_product = models.Product(**product_data)
|
| 67 |
+
db.add(db_product)
|
| 68 |
+
db.commit()
|
| 69 |
+
db.refresh(db_product)
|
| 70 |
+
return db_product
|
| 71 |
+
|
| 72 |
+
@router.get("/products", response_model=List[schemas.ProductResponse])
|
| 73 |
+
def list_products(
|
| 74 |
+
skip: int = 0,
|
| 75 |
+
limit: int = 100,
|
| 76 |
+
db: Session = Depends(get_db),
|
| 77 |
+
current_user: models.User = Depends(get_current_admin_user)
|
| 78 |
+
):
|
| 79 |
+
products = db.query(models.Product).offset(skip).limit(limit).all()
|
| 80 |
+
return products
|
| 81 |
+
|
| 82 |
+
@router.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 83 |
+
def delete_product(
|
| 84 |
+
product_id: int,
|
| 85 |
+
db: Session = Depends(get_db),
|
| 86 |
+
current_user: models.User = Depends(get_current_admin_user)
|
| 87 |
+
):
|
| 88 |
+
product = db.query(models.Product).filter(models.Product.id == product_id).first()
|
| 89 |
+
if not product:
|
| 90 |
+
raise HTTPException(status_code=404, detail="Product not found")
|
| 91 |
+
db.delete(product)
|
| 92 |
+
db.commit()
|
| 93 |
+
return None
|
app/routes/nutrition.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from typing import List
|
| 4 |
+
import io
|
| 5 |
+
import re
|
| 6 |
+
import os
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
from app.database import get_db
|
| 10 |
+
from app import models, schemas
|
| 11 |
+
from app.auth import get_current_user
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
import google.generativeai as genai
|
| 15 |
+
GEMINI_AVAILABLE = True
|
| 16 |
+
except ImportError:
|
| 17 |
+
GEMINI_AVAILABLE = False
|
| 18 |
+
genai = None
|
| 19 |
+
|
| 20 |
+
router = APIRouter(prefix="/nutrition", tags=["nutrition"])
|
| 21 |
+
|
| 22 |
+
GEMINI_API_KEY = "AIzaSyD2H-Oct8iMxsvpaDC_oaqgB1FiTzRF62k"
|
| 23 |
+
if GEMINI_API_KEY and GEMINI_AVAILABLE and genai:
|
| 24 |
+
genai.configure(api_key="AIzaSyD2H-Oct8iMxsvpaDC_oaqgB1FiTzRF62k")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def analyze_with_gemini(user_health_issues: List[models.HealthIssue], all_products: List[models.Product], image_data: bytes) -> dict:
|
| 29 |
+
if not GEMINI_API_KEY:
|
| 30 |
+
return {
|
| 31 |
+
"extracted_nutrition": {},
|
| 32 |
+
"health_rating": 5.0,
|
| 33 |
+
"health_recommendations": ["Gemini API key not configured. Please set GEMINI_API_KEY environment variable."],
|
| 34 |
+
"suggested_alternatives": [],
|
| 35 |
+
"analysis_summary": "Analysis unavailable without Gemini API key."
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
model = genai.GenerativeModel('gemini-2.0-flash')
|
| 40 |
+
|
| 41 |
+
health_issues_text = ", ".join([str(issue.issue_type) for issue in user_health_issues]) if user_health_issues else "None reported"
|
| 42 |
+
|
| 43 |
+
products_text = ""
|
| 44 |
+
if all_products:
|
| 45 |
+
products_text = "\n".join([
|
| 46 |
+
f"- {str(p.name)} (Brand: {str(p.brand) if p.brand else 'N/A'}): Calories: {p.calories}, Protein: {p.protein}g, Fat: {p.fat}g, Carbs: {p.carbohydrates}g, Sodium: {p.sodium}mg, Sugar: {p.sugar}g"
|
| 47 |
+
for p in all_products[:10]
|
| 48 |
+
])
|
| 49 |
+
|
| 50 |
+
prompt_text = f"""Analyze this nutrition label image.
|
| 51 |
+
|
| 52 |
+
First, extract the following nutrition facts from the image:
|
| 53 |
+
- Calories
|
| 54 |
+
- Protein (g)
|
| 55 |
+
- Fat (g)
|
| 56 |
+
- Sugar (g)
|
| 57 |
+
- Fiber (g)
|
| 58 |
+
- Extract others if they're available
|
| 59 |
+
Then, based on the extracted data and the user's health issues, provide health recommendations.
|
| 60 |
+
|
| 61 |
+
User's Health Issues: {health_issues_text}
|
| 62 |
+
|
| 63 |
+
Available Alternative Products:
|
| 64 |
+
{products_text if products_text else "No products in database"}
|
| 65 |
+
|
| 66 |
+
Please provide:
|
| 67 |
+
1. The extracted nutrition data in JSON format.
|
| 68 |
+
2. A health rating from 1-10 (where 1 is unhealthy and 10 is very healthy)
|
| 69 |
+
3. 3-5 specific health recommendations based on the user's health issues
|
| 70 |
+
4. Names of 2-3 healthier alternative products from the list above (if available)
|
| 71 |
+
5. A brief analysis summary
|
| 72 |
+
|
| 73 |
+
Format your response strictly as follows:
|
| 74 |
+
```json
|
| 75 |
+
{{
|
| 76 |
+
"nutrition": {{
|
| 77 |
+
"calories": 0.0,
|
| 78 |
+
"protein": 0.0,
|
| 79 |
+
"fat": 0.0,
|
| 80 |
+
"carbohydrates": 0.0,
|
| 81 |
+
"sugar": 0.0,
|
| 82 |
+
"fiber": 0.0,
|
| 83 |
+
}},
|
| 84 |
+
"rating": 0.0,
|
| 85 |
+
"recommendations": [
|
| 86 |
+
"recommendation 1",
|
| 87 |
+
"recommendation 2"
|
| 88 |
+
],
|
| 89 |
+
"alternatives": [
|
| 90 |
+
"product name 1",
|
| 91 |
+
"product name 2"
|
| 92 |
+
],
|
| 93 |
+
"summary": "your analysis summary"
|
| 94 |
+
}}
|
| 95 |
+
```
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
content = [prompt_text]
|
| 99 |
+
if image_data:
|
| 100 |
+
image_part = {
|
| 101 |
+
"mime_type": "image/jpeg",
|
| 102 |
+
"data": image_data
|
| 103 |
+
}
|
| 104 |
+
content.append(image_part)
|
| 105 |
+
|
| 106 |
+
response = model.generate_content(content)
|
| 107 |
+
result_text = response.text
|
| 108 |
+
|
| 109 |
+
# Clean up the response to get just the JSON part
|
| 110 |
+
json_match = re.search(r'```json\s*(.*?)\s*```', result_text, re.DOTALL)
|
| 111 |
+
if json_match:
|
| 112 |
+
json_str = json_match.group(1)
|
| 113 |
+
else:
|
| 114 |
+
# Try to find the first { and last }
|
| 115 |
+
start = result_text.find('{')
|
| 116 |
+
end = result_text.rfind('}')
|
| 117 |
+
if start != -1 and end != -1:
|
| 118 |
+
json_str = result_text[start:end+1]
|
| 119 |
+
else:
|
| 120 |
+
raise ValueError("Could not parse JSON from Gemini response")
|
| 121 |
+
|
| 122 |
+
result_data = json.loads(json_str)
|
| 123 |
+
|
| 124 |
+
extracted_nutrition = result_data.get("nutrition", {})
|
| 125 |
+
health_rating = float(result_data.get("rating", 5.0))
|
| 126 |
+
recommendations = result_data.get("recommendations", [])
|
| 127 |
+
alternative_names = result_data.get("alternatives", [])
|
| 128 |
+
analysis_summary = result_data.get("summary", "Analysis completed.")
|
| 129 |
+
|
| 130 |
+
suggested_products = []
|
| 131 |
+
for alt_name in alternative_names:
|
| 132 |
+
for product in all_products:
|
| 133 |
+
if alt_name.lower() in product.name.lower():
|
| 134 |
+
suggested_products.append(product)
|
| 135 |
+
break
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
"extracted_nutrition": extracted_nutrition,
|
| 139 |
+
"health_rating": health_rating,
|
| 140 |
+
"health_recommendations": recommendations,
|
| 141 |
+
"suggested_alternatives": suggested_products[:3],
|
| 142 |
+
"analysis_summary": analysis_summary
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
except Exception as e:
|
| 146 |
+
print(f"Gemini Error: {e}")
|
| 147 |
+
return {
|
| 148 |
+
"extracted_nutrition": {},
|
| 149 |
+
"health_rating": 5.0,
|
| 150 |
+
"health_recommendations": [f"Error during analysis: {str(e)}"],
|
| 151 |
+
"suggested_alternatives": [],
|
| 152 |
+
"analysis_summary": "Analysis failed due to an error."
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
@router.post("/analyze", response_model=schemas.NutritionAnalysisResponse)
|
| 156 |
+
async def analyze_nutrition_image(
|
| 157 |
+
file: UploadFile = File(...),
|
| 158 |
+
current_user: models.User = Depends(get_current_user),
|
| 159 |
+
db: Session = Depends(get_db)
|
| 160 |
+
):
|
| 161 |
+
if not file.content_type or not file.content_type.startswith('image/'):
|
| 162 |
+
raise HTTPException(status_code=400, detail="File must be an image")
|
| 163 |
+
|
| 164 |
+
image_data = await file.read()
|
| 165 |
+
|
| 166 |
+
user_health_issues = db.query(models.HealthIssue).filter(
|
| 167 |
+
models.HealthIssue.user_id == current_user.id
|
| 168 |
+
).all()
|
| 169 |
+
|
| 170 |
+
all_products = db.query(models.Product).all()
|
| 171 |
+
|
| 172 |
+
gemini_analysis = analyze_with_gemini(user_health_issues, all_products, image_data)
|
| 173 |
+
|
| 174 |
+
return schemas.NutritionAnalysisResponse(
|
| 175 |
+
extracted_nutrition=gemini_analysis["extracted_nutrition"],
|
| 176 |
+
health_rating=gemini_analysis["health_rating"],
|
| 177 |
+
health_recommendations=gemini_analysis["health_recommendations"],
|
| 178 |
+
suggested_alternatives=gemini_analysis["suggested_alternatives"],
|
| 179 |
+
analysis_summary=gemini_analysis["analysis_summary"]
|
| 180 |
+
)
|
app/routes/user.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
|
| 4 |
+
from typing import List
|
| 5 |
+
from app.database import get_db
|
| 6 |
+
from app import models, schemas
|
| 7 |
+
from app.auth import (
|
| 8 |
+
get_password_hash,
|
| 9 |
+
verify_password,
|
| 10 |
+
get_current_user,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
router = APIRouter(tags=["user"])
|
| 14 |
+
|
| 15 |
+
@router.post("/auth/register", response_model=schemas.UserResponse, status_code=status.HTTP_201_CREATED)
|
| 16 |
+
def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
| 17 |
+
db_user = db.query(models.User).filter(models.User.username == user.username).first()
|
| 18 |
+
if db_user:
|
| 19 |
+
raise HTTPException(status_code=400, detail="Username already registered")
|
| 20 |
+
|
| 21 |
+
db_email = db.query(models.User).filter(models.User.email == user.email).first()
|
| 22 |
+
if db_email:
|
| 23 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 24 |
+
|
| 25 |
+
hashed_password = get_password_hash(user.password)
|
| 26 |
+
db_user = models.User(
|
| 27 |
+
username=user.username,
|
| 28 |
+
email=user.email,
|
| 29 |
+
hashed_password=hashed_password,
|
| 30 |
+
role=models.UserRole.USER
|
| 31 |
+
)
|
| 32 |
+
db.add(db_user)
|
| 33 |
+
db.commit()
|
| 34 |
+
db.refresh(db_user)
|
| 35 |
+
return db_user
|
| 36 |
+
|
| 37 |
+
@router.post("/auth/login", response_model=schemas.UserResponse)
|
| 38 |
+
def login(credentials: schemas.UserLogin, db: Session = Depends(get_db)):
|
| 39 |
+
user = db.query(models.User).filter(models.User.username == credentials.username).first()
|
| 40 |
+
if not user:
|
| 41 |
+
raise HTTPException(
|
| 42 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 43 |
+
detail="Incorrect username or password"
|
| 44 |
+
)
|
| 45 |
+
if not verify_password(credentials.password, str(user.hashed_password)):
|
| 46 |
+
raise HTTPException(
|
| 47 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 48 |
+
detail="Incorrect username or password"
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
return user
|
| 52 |
+
|
| 53 |
+
@router.get("/user/me", response_model=schemas.UserResponse)
|
| 54 |
+
def get_current_user_info(current_user: models.User = Depends(get_current_user)):
|
| 55 |
+
return current_user
|
| 56 |
+
|
| 57 |
+
@router.post("/user/health-issues", response_model=schemas.HealthIssueResponse, status_code=status.HTTP_201_CREATED)
|
| 58 |
+
def add_health_issue(
|
| 59 |
+
health_issue: schemas.HealthIssueCreate,
|
| 60 |
+
current_user: models.User = Depends(get_current_user),
|
| 61 |
+
db: Session = Depends(get_db)
|
| 62 |
+
):
|
| 63 |
+
db_health_issue = models.HealthIssue(
|
| 64 |
+
user_id=current_user.id,
|
| 65 |
+
**health_issue.dict()
|
| 66 |
+
)
|
| 67 |
+
db.add(db_health_issue)
|
| 68 |
+
db.commit()
|
| 69 |
+
db.refresh(db_health_issue)
|
| 70 |
+
return db_health_issue
|
| 71 |
+
|
| 72 |
+
@router.get("/user/health-issues", response_model=List[schemas.HealthIssueResponse])
|
| 73 |
+
def get_health_issues(
|
| 74 |
+
current_user: models.User = Depends(get_current_user),
|
| 75 |
+
db: Session = Depends(get_db)
|
| 76 |
+
):
|
| 77 |
+
return db.query(models.HealthIssue).filter(models.HealthIssue.user_id == current_user.id).all()
|
| 78 |
+
|
| 79 |
+
@router.delete("/user/health-issues/{issue_id}", status_code=status.HTTP_204_NO_CONTENT)
|
| 80 |
+
def delete_health_issue(
|
| 81 |
+
issue_id: int,
|
| 82 |
+
current_user: models.User = Depends(get_current_user),
|
| 83 |
+
db: Session = Depends(get_db)
|
| 84 |
+
):
|
| 85 |
+
health_issue = db.query(models.HealthIssue).filter(
|
| 86 |
+
models.HealthIssue.id == issue_id,
|
| 87 |
+
models.HealthIssue.user_id == current_user.id
|
| 88 |
+
).first()
|
| 89 |
+
if not health_issue:
|
| 90 |
+
raise HTTPException(status_code=404, detail="Health issue not found")
|
| 91 |
+
db.delete(health_issue)
|
| 92 |
+
db.commit()
|
| 93 |
+
return None
|
app/schemas.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 2 |
+
from typing import Optional, List
|
| 3 |
+
from enum import Enum
|
| 4 |
+
|
| 5 |
+
class UserRole(str, Enum):
|
| 6 |
+
ADMIN = "admin"
|
| 7 |
+
USER = "user"
|
| 8 |
+
|
| 9 |
+
class UserCreate(BaseModel):
|
| 10 |
+
username: str
|
| 11 |
+
email: EmailStr
|
| 12 |
+
password: str
|
| 13 |
+
|
| 14 |
+
class UserLogin(BaseModel):
|
| 15 |
+
username: str
|
| 16 |
+
password: str
|
| 17 |
+
|
| 18 |
+
class UserResponse(BaseModel):
|
| 19 |
+
id: int
|
| 20 |
+
username: str
|
| 21 |
+
email: str
|
| 22 |
+
role: UserRole
|
| 23 |
+
|
| 24 |
+
class Config:
|
| 25 |
+
from_attributes = True
|
| 26 |
+
|
| 27 |
+
class Token(BaseModel):
|
| 28 |
+
access_token: str
|
| 29 |
+
token_type: str
|
| 30 |
+
|
| 31 |
+
class TokenData(BaseModel):
|
| 32 |
+
username: Optional[str] = None
|
| 33 |
+
role: Optional[str] = None
|
| 34 |
+
|
| 35 |
+
class ProductCreate(BaseModel):
|
| 36 |
+
name: str
|
| 37 |
+
brand: Optional[str] = None
|
| 38 |
+
calories: float
|
| 39 |
+
protein: float
|
| 40 |
+
fat: float
|
| 41 |
+
carbohydrates: float
|
| 42 |
+
sodium: float
|
| 43 |
+
sugar: float
|
| 44 |
+
fiber: Optional[float] = None
|
| 45 |
+
cholesterol: Optional[float] = None
|
| 46 |
+
serving_size: Optional[str] = None
|
| 47 |
+
|
| 48 |
+
class ProductResponse(BaseModel):
|
| 49 |
+
id: int
|
| 50 |
+
name: str
|
| 51 |
+
brand: Optional[str]
|
| 52 |
+
calories: float
|
| 53 |
+
protein: float
|
| 54 |
+
fat: float
|
| 55 |
+
carbohydrates: float
|
| 56 |
+
sodium: float
|
| 57 |
+
sugar: float
|
| 58 |
+
fiber: Optional[float]
|
| 59 |
+
cholesterol: Optional[float]
|
| 60 |
+
serving_size: Optional[str]
|
| 61 |
+
image_path: Optional[str] = None
|
| 62 |
+
|
| 63 |
+
class Config:
|
| 64 |
+
from_attributes = True
|
| 65 |
+
|
| 66 |
+
class HealthIssueCreate(BaseModel):
|
| 67 |
+
issue_type: str = Field(..., description="Type of health issue (e.g., diabetes, hypertension, high cholesterol)")
|
| 68 |
+
severity: Optional[str] = None
|
| 69 |
+
notes: Optional[str] = None
|
| 70 |
+
|
| 71 |
+
class HealthIssueResponse(BaseModel):
|
| 72 |
+
id: int
|
| 73 |
+
user_id: int
|
| 74 |
+
issue_type: str
|
| 75 |
+
severity: Optional[str]
|
| 76 |
+
notes: Optional[str]
|
| 77 |
+
|
| 78 |
+
class Config:
|
| 79 |
+
from_attributes = True
|
| 80 |
+
|
| 81 |
+
class NutritionAnalysisResponse(BaseModel):
|
| 82 |
+
extracted_nutrition: dict
|
| 83 |
+
health_rating: float = Field(..., description="Product rating from 1-10")
|
| 84 |
+
health_recommendations: List[str]
|
| 85 |
+
suggested_alternatives: List[ProductResponse]
|
| 86 |
+
analysis_summary: str
|
create_admin.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.database import SessionLocal
|
| 2 |
+
from app import models
|
| 3 |
+
from app.auth import get_password_hash
|
| 4 |
+
|
| 5 |
+
def create_admin(username, email, password):
|
| 6 |
+
db = SessionLocal()
|
| 7 |
+
try:
|
| 8 |
+
# Check if user exists
|
| 9 |
+
existing_user = db.query(models.User).filter(models.User.username == username).first()
|
| 10 |
+
if existing_user:
|
| 11 |
+
print(f"User {username} already exists.")
|
| 12 |
+
return
|
| 13 |
+
|
| 14 |
+
existing_email = db.query(models.User).filter(models.User.email == email).first()
|
| 15 |
+
if existing_email:
|
| 16 |
+
print(f"Email {email} already registered.")
|
| 17 |
+
return
|
| 18 |
+
|
| 19 |
+
# Create admin user
|
| 20 |
+
hashed_password = get_password_hash(password)
|
| 21 |
+
db_user = models.User(
|
| 22 |
+
username=username,
|
| 23 |
+
email=email,
|
| 24 |
+
hashed_password=hashed_password,
|
| 25 |
+
role=models.UserRole.ADMIN
|
| 26 |
+
)
|
| 27 |
+
db.add(db_user)
|
| 28 |
+
db.commit()
|
| 29 |
+
db.refresh(db_user)
|
| 30 |
+
print(f"Admin user '{username}' created successfully.")
|
| 31 |
+
except Exception as e:
|
| 32 |
+
print(f"Error creating admin: {e}")
|
| 33 |
+
finally:
|
| 34 |
+
db.close()
|
| 35 |
+
|
| 36 |
+
if __name__ == "__main__":
|
| 37 |
+
import sys
|
| 38 |
+
if len(sys.argv) != 4:
|
| 39 |
+
print("Usage: python create_admin.py <username> <email> <password>")
|
| 40 |
+
else:
|
| 41 |
+
create_admin(sys.argv[1], sys.argv[2], sys.argv[3])
|
main.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.database import engine, Base
|
| 4 |
+
from app.routes import admin, user, nutrition
|
| 5 |
+
|
| 6 |
+
Base.metadata.create_all(bind=engine)
|
| 7 |
+
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="Nutrition Analysis API",
|
| 10 |
+
description="FastAPI backend for nutrition analysis with OCR and AI-powered recommendations",
|
| 11 |
+
version="1.0.0"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=["*"],
|
| 17 |
+
allow_credentials=True,
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
app.include_router(user.router)
|
| 23 |
+
app.include_router(admin.router)
|
| 24 |
+
app.include_router(nutrition.router)
|
| 25 |
+
|
| 26 |
+
@app.get("/")
|
| 27 |
+
def root():
|
| 28 |
+
return {
|
| 29 |
+
"message": "Welcome to Nutrition Analysis API",
|
| 30 |
+
"docs": "/docs",
|
| 31 |
+
"redoc": "/redoc"
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
@app.get("/health")
|
| 35 |
+
def health_check():
|
| 36 |
+
return {"status": "healthy"}
|
nutrition_app.db
ADDED
|
Binary file (41 kB). View file
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "repl-nix-workspace"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "Add your description here"
|
| 5 |
+
requires-python = ">=3.11"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"email-validator>=2.3.0",
|
| 8 |
+
"fastapi>=0.121.2",
|
| 9 |
+
"google-genai>=1.50.1",
|
| 10 |
+
"google-generativeai>=0.8.5",
|
| 11 |
+
"passlib[bcrypt]>=1.7.4",
|
| 12 |
+
"pillow>=12.0.0",
|
| 13 |
+
"pydantic>=2.12.4",
|
| 14 |
+
"pytesseract>=0.3.13",
|
| 15 |
+
"python-jose[cryptography]>=3.5.0",
|
| 16 |
+
"python-multipart>=0.0.20",
|
| 17 |
+
"sqlalchemy>=2.0.44",
|
| 18 |
+
"uvicorn>=0.38.0",
|
| 19 |
+
]
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
email-validator>=2.3.0
|
| 2 |
+
fastapi>=0.121.2
|
| 3 |
+
google-genai>=1.50.1
|
| 4 |
+
google-generativeai>=0.8.5
|
| 5 |
+
passlib[bcrypt]>=1.7.4
|
| 6 |
+
pillow>=12.0.0
|
| 7 |
+
pydantic>=2.12.4
|
| 8 |
+
pytesseract>=0.3.13
|
| 9 |
+
python-jose[cryptography]>=3.5.0
|
| 10 |
+
python-multipart>=0.0.20
|
| 11 |
+
sqlalchemy>=2.0.44
|
| 12 |
+
uvicorn>=0.38.0
|
uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
verify_image_upload.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
BASE_URL = "http://localhost:8000"
|
| 6 |
+
ADMIN_USERNAME = "admin_test"
|
| 7 |
+
ADMIN_EMAIL = "admin_test@example.com"
|
| 8 |
+
ADMIN_PASSWORD = "password"
|
| 9 |
+
|
| 10 |
+
def create_admin_user():
|
| 11 |
+
# Use the existing create_admin.py logic or just call the register endpoint and then promote?
|
| 12 |
+
# Or just rely on the create_admin.py script.
|
| 13 |
+
# Let's try to register and then promote if possible, or just use create_admin.py
|
| 14 |
+
# Since I can't easily import create_admin.py if it's not a module, I'll just run it via subprocess or assume it's run.
|
| 15 |
+
# Actually, I'll just use the create_admin.py script in the main flow.
|
| 16 |
+
pass
|
| 17 |
+
|
| 18 |
+
def test_create_product_with_image():
|
| 19 |
+
print("Testing create product with image...")
|
| 20 |
+
|
| 21 |
+
# Create a dummy image
|
| 22 |
+
with open("test_image.jpg", "wb") as f:
|
| 23 |
+
f.write(b"fake image content")
|
| 24 |
+
|
| 25 |
+
url = f"{BASE_URL}/admin/products"
|
| 26 |
+
headers = {"X-Username": ADMIN_USERNAME}
|
| 27 |
+
|
| 28 |
+
data = {
|
| 29 |
+
"name": "Test Product",
|
| 30 |
+
"calories": 100,
|
| 31 |
+
"protein": 10,
|
| 32 |
+
"fat": 5,
|
| 33 |
+
"carbohydrates": 20,
|
| 34 |
+
"sodium": 50,
|
| 35 |
+
"sugar": 5
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
files = {
|
| 39 |
+
"image": ("test_image.jpg", open("test_image.jpg", "rb"), "image/jpeg")
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
try:
|
| 43 |
+
response = requests.post(url, headers=headers, data=data, files=files)
|
| 44 |
+
if response.status_code != 201:
|
| 45 |
+
print(f"Failed to create product: {response.status_code} {response.text}")
|
| 46 |
+
return False
|
| 47 |
+
|
| 48 |
+
product = response.json()
|
| 49 |
+
print(f"Product created: {product}")
|
| 50 |
+
|
| 51 |
+
if not product.get("image_path"):
|
| 52 |
+
print("Error: image_path is missing in response")
|
| 53 |
+
return False
|
| 54 |
+
|
| 55 |
+
if ".tmp/test_image.jpg" not in product["image_path"]:
|
| 56 |
+
print(f"Error: Unexpected image path: {product['image_path']}")
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
# Verify file exists
|
| 60 |
+
if not os.path.exists(product["image_path"]):
|
| 61 |
+
print(f"Error: File does not exist at {product['image_path']}")
|
| 62 |
+
return False
|
| 63 |
+
|
| 64 |
+
print("Image upload verified successfully.")
|
| 65 |
+
return True
|
| 66 |
+
|
| 67 |
+
except Exception as e:
|
| 68 |
+
print(f"Exception: {e}")
|
| 69 |
+
return False
|
| 70 |
+
finally:
|
| 71 |
+
# Cleanup
|
| 72 |
+
if os.path.exists("test_image.jpg"):
|
| 73 |
+
os.remove("test_image.jpg")
|
| 74 |
+
|
| 75 |
+
def test_list_products():
|
| 76 |
+
print("Testing list products...")
|
| 77 |
+
url = f"{BASE_URL}/admin/products"
|
| 78 |
+
headers = {"X-Username": ADMIN_USERNAME}
|
| 79 |
+
|
| 80 |
+
response = requests.get(url, headers=headers)
|
| 81 |
+
if response.status_code != 200:
|
| 82 |
+
print(f"Failed to list products: {response.status_code}")
|
| 83 |
+
return False
|
| 84 |
+
|
| 85 |
+
products = response.json()
|
| 86 |
+
# Find our test product
|
| 87 |
+
found = False
|
| 88 |
+
for p in products:
|
| 89 |
+
if p["name"] == "Test Product":
|
| 90 |
+
found = True
|
| 91 |
+
if not p.get("image_path"):
|
| 92 |
+
print("Error: image_path missing in list response")
|
| 93 |
+
return False
|
| 94 |
+
print(f"Found product in list with image_path: {p['image_path']}")
|
| 95 |
+
break
|
| 96 |
+
|
| 97 |
+
if not found:
|
| 98 |
+
print("Error: Test product not found in list")
|
| 99 |
+
return False
|
| 100 |
+
|
| 101 |
+
print("List products verified successfully.")
|
| 102 |
+
return True
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
if not test_create_product_with_image():
|
| 106 |
+
sys.exit(1)
|
| 107 |
+
if not test_list_products():
|
| 108 |
+
sys.exit(1)
|
| 109 |
+
print("All tests passed!")
|