Implement AI-powered gifting platform

- Setup project structure with FastAPI
- Create database models for users, gifts, preferences, and recommendations
- Configure SQLite database with SQLAlchemy ORM
- Setup Alembic for database migrations
- Implement user authentication with JWT
- Create API endpoints for users, gifts, preferences, and recommendations
- Integrate OpenAI API for gift recommendations
- Add comprehensive documentation
This commit is contained in:
Automated Action 2025-06-07 21:16:44 +00:00
parent 0f00e4f867
commit 6d3b1188d1
38 changed files with 1719 additions and 2 deletions

170
README.md
View File

@ -1,3 +1,169 @@
# FastAPI Application
# AI Powered Gifting Platform
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
An intelligent API service that helps users find the perfect gift for any recipient using AI recommendations based on preferences and interests.
## Features
- 🎁 **Gift Management**: Track gift ideas, purchases, and occasions
- 🧠 **AI-Powered Recommendations**: Get personalized gift suggestions based on recipient preferences
- 👤 **User Profiles**: Secure authentication and user management
- 🔍 **Preference Tracking**: Store recipient interests, hobbies, sizes, and dislikes
- 🔒 **Secure API**: JWT authentication with role-based access control
## Tech Stack
- **Framework**: FastAPI
- **Database**: SQLite with SQLAlchemy ORM
- **Authentication**: JWT with OAuth2
- **Migrations**: Alembic
- **AI Integration**: OpenAI API
- **Code Quality**: Ruff linter
## Getting Started
### Prerequisites
- Python 3.8+
- OpenAI API key (for AI-powered recommendations)
### Environment Variables
Create a `.env` file in the project root with the following variables:
```
SECRET_KEY=your-secret-key-for-jwt
OPENAI_API_KEY=your-openai-api-key
OPENAI_MODEL=gpt-3.5-turbo # or another OpenAI model
```
### Installation
1. Clone the repository:
```bash
git clone <repository-url>
cd ai-powered-gifting-platform
```
2. Install dependencies:
```bash
pip install -r requirements.txt
```
3. Run database migrations:
```bash
alembic upgrade head
```
4. Start the server:
```bash
uvicorn main:app --reload
```
The API will be available at http://localhost:8000.
## API Documentation
Once the application is running, you can access:
- Interactive API documentation: http://localhost:8000/docs
- Alternative documentation: http://localhost:8000/redoc
- OpenAPI schema: http://localhost:8000/openapi.json
## API Endpoints
### Health Check
- `GET /api/v1/health`: Check the API service health
### Authentication
- `POST /api/v1/auth/login`: Get access token
### Users
- `POST /api/v1/users`: Create new user
- `GET /api/v1/users/me`: Get current user info
- `PUT /api/v1/users/me`: Update current user
### Gifts
- `GET /api/v1/gifts`: List all gifts
- `POST /api/v1/gifts`: Create a new gift
- `GET /api/v1/gifts/{id}`: Get gift details
- `PUT /api/v1/gifts/{id}`: Update a gift
- `DELETE /api/v1/gifts/{id}`: Delete a gift
### Preferences
- `GET /api/v1/preferences`: List all preferences
- `POST /api/v1/preferences`: Create a new preference
- `GET /api/v1/preferences/{id}`: Get preference details
- `PUT /api/v1/preferences/{id}`: Update a preference
- `DELETE /api/v1/preferences/{id}`: Delete a preference
### Recommendations
- `GET /api/v1/recommendations`: List all recommendations
- `POST /api/v1/recommendations`: Get AI-powered gift recommendation
- `GET /api/v1/recommendations/{id}`: Get recommendation details
- `PUT /api/v1/recommendations/{id}`: Update a recommendation
- `DELETE /api/v1/recommendations/{id}`: Delete a recommendation
## Project Structure
```
.
├── alembic.ini # Alembic configuration
├── app # Application package
│ ├── api # API endpoints
│ │ └── v1 # API version 1
│ │ ├── endpoints # API endpoint modules
│ │ └── api.py # API router
│ ├── core # Core modules
│ │ ├── config.py # Settings and configuration
│ │ └── security.py # Security utilities
│ ├── db # Database modules
│ │ ├── base.py # SQLAlchemy Base
│ │ ├── deps.py # Dependency functions
│ │ └── session.py # Database session
│ ├── models # SQLAlchemy models
│ ├── schemas # Pydantic schemas
│ └── services # Business logic services
├── main.py # Application entry point
├── migrations # Alembic migrations
│ ├── versions # Migration scripts
│ └── env.py # Alembic environment
└── requirements.txt # Project dependencies
```
## Development
### Database Migrations
To create a new migration after modifying models:
```bash
alembic revision --autogenerate -m "Description of changes"
```
To apply migrations:
```bash
alembic upgrade head
```
### Linting
Run the linter to ensure code quality:
```bash
ruff check .
```
Fix auto-fixable issues:
```bash
ruff check --fix .
```
## License
[MIT License](LICENSE)

103
alembic.ini Normal file
View File

@ -0,0 +1,103 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; The path separator used here should not be the
# same as in the :var:`version_locations` string.
# Using / as a separator
# will treat it as a path separator which would otherwise cause syntax errors with
# some database backends like SQLite. ":" is commonly used instead.
# version_path_separator = :
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# SQLite URL example
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
app/api/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

1
app/api/v1/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

11
app/api/v1/api.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
from app.api.v1.endpoints import users, auth, gifts, preferences, recommendations, health
api_router = APIRouter()
api_router.include_router(health.router, prefix="/health", tags=["health"])
api_router.include_router(auth.router, prefix="/auth", tags=["auth"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(gifts.router, prefix="/gifts", tags=["gifts"])
api_router.include_router(preferences.router, prefix="/preferences", tags=["preferences"])
api_router.include_router(recommendations.router, prefix="/recommendations", tags=["recommendations"])

View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

View File

@ -0,0 +1,36 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import create_access_token, verify_password
from app.db.deps import get_db
from app.models.user import User
from app.schemas.token import Token
router = APIRouter()
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}

View File

@ -0,0 +1,120 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app.db.deps import get_current_active_user, get_db
from app.models.gift import Gift
from app.models.user import User
from app.schemas.gift import Gift as GiftSchema
from app.schemas.gift import GiftCreate, GiftUpdate
router = APIRouter()
@router.get("", response_model=List[GiftSchema])
def read_gifts(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
recipient_name: str = Query(None, description="Filter by recipient name"),
occasion: str = Query(None, description="Filter by occasion"),
purchased: bool = Query(None, description="Filter by purchased status"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve gifts.
"""
query = db.query(Gift).filter(Gift.user_id == current_user.id)
# Apply filters if provided
if recipient_name:
query = query.filter(Gift.recipient_name == recipient_name)
if occasion:
query = query.filter(Gift.occasion == occasion)
if purchased is not None:
query = query.filter(Gift.purchased == purchased)
# Apply pagination
gifts = query.offset(skip).limit(limit).all()
return gifts
@router.post("", response_model=GiftSchema)
def create_gift(
*,
db: Session = Depends(get_db),
gift_in: GiftCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new gift.
"""
gift = Gift(
**gift_in.dict(),
user_id=current_user.id
)
db.add(gift)
db.commit()
db.refresh(gift)
return gift
@router.put("/{id}", response_model=GiftSchema)
def update_gift(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the gift to update"),
gift_in: GiftUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a gift.
"""
gift = db.query(Gift).filter(Gift.id == id, Gift.user_id == current_user.id).first()
if not gift:
raise HTTPException(status_code=404, detail="Gift not found")
update_data = gift_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(gift, field, value)
db.add(gift)
db.commit()
db.refresh(gift)
return gift
@router.get("/{id}", response_model=GiftSchema)
def read_gift(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the gift to get"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get gift by ID.
"""
gift = db.query(Gift).filter(Gift.id == id, Gift.user_id == current_user.id).first()
if not gift:
raise HTTPException(status_code=404, detail="Gift not found")
return gift
@router.delete("/{id}", status_code=204, response_model=None)
def delete_gift(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the gift to delete"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a gift.
"""
gift = db.query(Gift).filter(Gift.id == id, Gift.user_id == current_user.id).first()
if not gift:
raise HTTPException(status_code=404, detail="Gift not found")
db.delete(gift)
db.commit()
return None

View File

@ -0,0 +1,25 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db.deps import get_db
router = APIRouter()
@router.get("")
def health_check(db: Session = Depends(get_db)):
"""
Health check endpoint to verify API and database are running.
"""
try:
# Check if DB connection is working
db.execute("SELECT 1")
db_status = "healthy"
except Exception as e:
db_status = f"unhealthy: {str(e)}"
return {
"status": "healthy",
"database": db_status,
"version": "1.0.0"
}

View File

@ -0,0 +1,138 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app.db.deps import get_current_active_user, get_db
from app.models.preference import Preference
from app.models.user import User
from app.schemas.preference import Preference as PreferenceSchema
from app.schemas.preference import PreferenceCreate, PreferenceUpdate
router = APIRouter()
@router.get("", response_model=List[PreferenceSchema])
def read_preferences(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
recipient_name: str = Query(None, description="Filter by recipient name"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve preferences.
"""
query = db.query(Preference).filter(Preference.user_id == current_user.id)
# Apply filters if provided
if recipient_name:
query = query.filter(Preference.recipient_name == recipient_name)
# Apply pagination
preferences = query.offset(skip).limit(limit).all()
return preferences
@router.post("", response_model=PreferenceSchema)
def create_preference(
*,
db: Session = Depends(get_db),
preference_in: PreferenceCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new preference.
"""
# Check if a preference for this recipient already exists
existing = db.query(Preference).filter(
Preference.user_id == current_user.id,
Preference.recipient_name == preference_in.recipient_name
).first()
if existing:
raise HTTPException(
status_code=400,
detail=f"Preference for recipient '{preference_in.recipient_name}' already exists. Use PUT to update."
)
preference = Preference(
**preference_in.dict(),
user_id=current_user.id
)
db.add(preference)
db.commit()
db.refresh(preference)
return preference
@router.put("/{id}", response_model=PreferenceSchema)
def update_preference(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the preference to update"),
preference_in: PreferenceUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a preference.
"""
preference = db.query(Preference).filter(
Preference.id == id,
Preference.user_id == current_user.id
).first()
if not preference:
raise HTTPException(status_code=404, detail="Preference not found")
update_data = preference_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(preference, field, value)
db.add(preference)
db.commit()
db.refresh(preference)
return preference
@router.get("/{id}", response_model=PreferenceSchema)
def read_preference(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the preference to get"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get preference by ID.
"""
preference = db.query(Preference).filter(
Preference.id == id,
Preference.user_id == current_user.id
).first()
if not preference:
raise HTTPException(status_code=404, detail="Preference not found")
return preference
@router.delete("/{id}", status_code=204, response_model=None)
def delete_preference(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the preference to delete"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a preference.
"""
preference = db.query(Preference).filter(
Preference.id == id,
Preference.user_id == current_user.id
).first()
if not preference:
raise HTTPException(status_code=404, detail="Preference not found")
db.delete(preference)
db.commit()
return None

View File

@ -0,0 +1,170 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.orm import Session
from app.db.deps import get_current_active_user, get_db
from app.models.preference import Preference
from app.models.recommendation import Recommendation
from app.models.user import User
from app.schemas.recommendation import Recommendation as RecommendationSchema
from app.schemas.recommendation import RecommendationCreate, RecommendationUpdate
from app.services.ai_recommendation import AIRecommendationService
router = APIRouter()
recommendation_service = AIRecommendationService()
@router.get("", response_model=List[RecommendationSchema])
def read_recommendations(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
recipient_name: str = Query(None, description="Filter by recipient name"),
occasion: str = Query(None, description="Filter by occasion"),
saved: bool = Query(None, description="Filter by saved status"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve recommendations.
"""
query = db.query(Recommendation).filter(Recommendation.user_id == current_user.id)
# Apply filters if provided
if recipient_name:
query = query.filter(Recommendation.recipient_name == recipient_name)
if occasion:
query = query.filter(Recommendation.occasion == occasion)
if saved is not None:
query = query.filter(Recommendation.saved == saved)
# Apply pagination
recommendations = query.offset(skip).limit(limit).all()
return recommendations
@router.post("", response_model=RecommendationSchema)
async def create_recommendation(
*,
db: Session = Depends(get_db),
recommendation_in: RecommendationCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new recommendation using AI.
"""
# Get preferences if available
preferences = None
if recommendation_in.preferences_id:
preferences = db.query(Preference).filter(
Preference.id == recommendation_in.preferences_id,
Preference.user_id == current_user.id
).first()
if not preferences:
raise HTTPException(status_code=404, detail="Preference not found")
else:
# Try to find preferences by recipient name
preferences = db.query(Preference).filter(
Preference.recipient_name == recommendation_in.recipient_name,
Preference.user_id == current_user.id
).first()
# Generate recommendation using AI
ai_recommendation = await recommendation_service.generate_recommendations(
recipient_name=recommendation_in.recipient_name,
occasion=recommendation_in.occasion,
preferences=preferences,
budget_min=recommendation_in.budget_min,
budget_max=recommendation_in.budget_max,
additional_info=recommendation_in.additional_info,
)
# Create recommendation in database
recommendation = Recommendation(
user_id=current_user.id,
recipient_name=recommendation_in.recipient_name,
occasion=recommendation_in.occasion,
recommendation_text=ai_recommendation["recommendation_text"],
item_name=ai_recommendation["item_name"],
description=ai_recommendation["description"],
price_estimate=ai_recommendation["price_estimate"],
purchase_url=ai_recommendation["purchase_url"],
saved=False,
)
db.add(recommendation)
db.commit()
db.refresh(recommendation)
return recommendation
@router.put("/{id}", response_model=RecommendationSchema)
def update_recommendation(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the recommendation to update"),
recommendation_in: RecommendationUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update a recommendation.
"""
recommendation = db.query(Recommendation).filter(
Recommendation.id == id,
Recommendation.user_id == current_user.id
).first()
if not recommendation:
raise HTTPException(status_code=404, detail="Recommendation not found")
update_data = recommendation_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(recommendation, field, value)
db.add(recommendation)
db.commit()
db.refresh(recommendation)
return recommendation
@router.get("/{id}", response_model=RecommendationSchema)
def read_recommendation(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the recommendation to get"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get recommendation by ID.
"""
recommendation = db.query(Recommendation).filter(
Recommendation.id == id,
Recommendation.user_id == current_user.id
).first()
if not recommendation:
raise HTTPException(status_code=404, detail="Recommendation not found")
return recommendation
@router.delete("/{id}", status_code=204, response_model=None)
def delete_recommendation(
*,
db: Session = Depends(get_db),
id: int = Path(..., description="The ID of the recommendation to delete"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Delete a recommendation.
"""
recommendation = db.query(Recommendation).filter(
Recommendation.id == id,
Recommendation.user_id == current_user.id
).first()
if not recommendation:
raise HTTPException(status_code=404, detail="Recommendation not found")
db.delete(recommendation)
db.commit()
return None

View File

@ -0,0 +1,99 @@
from typing import Any
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from sqlalchemy.orm import Session
from app.core.security import get_password_hash
from app.db.deps import get_current_active_user, get_db
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, UserUpdate
router = APIRouter()
@router.get("/me", response_model=UserSchema)
def read_user_me(
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
password: str = Body(None),
full_name: str = Body(None),
email: str = Body(None),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update own user.
"""
current_user_data = jsonable_encoder(current_user)
user_in = UserUpdate(**current_user_data)
if password is not None:
user_in.password = password
if full_name is not None:
user_in.full_name = full_name
if email is not None:
user_in.email = email
# Check if email already exists
if email and email != current_user.email:
user = db.query(User).filter(User.email == email).first()
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
# Update user
if user_in.password:
hashed_password = get_password_hash(user_in.password)
current_user.hashed_password = hashed_password
if user_in.full_name:
current_user.full_name = user_in.full_name
if user_in.email:
current_user.email = user_in.email
if user_in.is_active is not None:
current_user.is_active = user_in.is_active
db.add(current_user)
db.commit()
db.refresh(current_user)
return current_user
@router.post("", response_model=UserSchema)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
) -> Any:
"""
Create new user.
"""
user = db.query(User).filter(User.email == user_in.email).first()
if user:
raise HTTPException(
status_code=400,
detail="The user with this email already exists in the system.",
)
# Create new user
user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
is_active=user_in.is_active,
)
db.add(user)
db.commit()
db.refresh(user)
return user

1
app/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

46
app/core/config.py Normal file
View File

@ -0,0 +1,46 @@
import os
from pathlib import Path
from typing import List, Optional, Union
from pydantic import AnyHttpUrl, validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
API_V1_STR: str = "/api/v1"
PROJECT_NAME: str = "AI Powered Gifting Platform"
# CORS
BACKEND_CORS_ORIGINS: List[Union[str, AnyHttpUrl]] = ["*"]
@validator("BACKEND_CORS_ORIGINS", pre=True)
def assemble_cors_origins(cls, v: Union[str, List[str]]) -> Union[List[str], str]:
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
# Database
DB_DIR: Path = Path("/app") / "storage" / "db"
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
# JWT
SECRET_KEY: str = os.getenv("SECRET_KEY", "CHANGEME_SECRET_KEY_CHANGEME")
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# OpenAI
OPENAI_API_KEY: Optional[str] = os.getenv("OPENAI_API_KEY")
OPENAI_MODEL: str = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
class Config:
case_sensitive = True
env_file = ".env"
# Create the settings object
settings = Settings()
# Ensure the database directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)

31
app/core/security.py Normal file
View File

@ -0,0 +1,31 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(
subject: Union[str, Any], expires_delta: timedelta = None
) -> str:
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)

1
app/db/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

3
app/db/base.py Normal file
View File

@ -0,0 +1,3 @@
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()

51
app/db/deps.py Normal file
View File

@ -0,0 +1,51 @@
from typing import Generator
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import SessionLocal
from app.models.user import User
from app.schemas.token import TokenPayload
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/auth/login"
)
def get_db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(reusable_oauth2)
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user

10
app/db/session.py Normal file
View File

@ -0,0 +1,10 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
engine = create_engine(
settings.SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

7
app/models/__init__.py Normal file
View File

@ -0,0 +1,7 @@
# Import all models here so they can be discovered by Alembic
from app.models.user import User as User
from app.models.gift import Gift as Gift
from app.models.preference import Preference as Preference
from app.models.recommendation import Recommendation as Recommendation
__all__ = ["User", "Gift", "Preference", "Recommendation"]

25
app/models/gift.py Normal file
View File

@ -0,0 +1,25 @@
from sqlalchemy import Boolean, Column, Integer, String, Float, ForeignKey, DateTime, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class Gift(Base):
__tablename__ = "gifts"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
recipient_name = Column(String, index=True, nullable=False)
relationship_to_user = Column(String, index=True)
occasion = Column(String, index=True)
name = Column(String, index=True, nullable=False)
description = Column(Text)
price = Column(Float)
purchase_url = Column(String)
purchased = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="gifts")

23
app/models/preference.py Normal file
View File

@ -0,0 +1,23 @@
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, Text
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class Preference(Base):
__tablename__ = "preferences"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
recipient_name = Column(String, index=True, nullable=False)
interests = Column(Text)
hobbies = Column(Text)
favorite_colors = Column(String)
clothing_size = Column(String)
dislikes = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="preferences")

View File

@ -0,0 +1,25 @@
from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime, Text, Boolean
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class Recommendation(Base):
__tablename__ = "recommendations"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"))
recipient_name = Column(String, index=True, nullable=False)
occasion = Column(String, index=True)
recommendation_text = Column(Text, nullable=False)
item_name = Column(String)
description = Column(Text)
price_estimate = Column(Float)
purchase_url = Column(String)
saved = Column(Boolean, default=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
user = relationship("User", back_populates="recommendations")

22
app/models/user.py Normal file
View File

@ -0,0 +1,22 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from app.db.base import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False)
hashed_password = Column(String, nullable=False)
full_name = Column(String, index=True)
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relationships
gifts = relationship("Gift", back_populates="user", cascade="all, delete-orphan")
preferences = relationship("Preference", back_populates="user", cascade="all, delete-orphan")
recommendations = relationship("Recommendation", back_populates="user", cascade="all, delete-orphan")

1
app/schemas/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

41
app/schemas/gift.py Normal file
View File

@ -0,0 +1,41 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Shared properties
class GiftBase(BaseModel):
recipient_name: str
relationship_to_user: Optional[str] = None
occasion: Optional[str] = None
name: str
description: Optional[str] = None
price: Optional[float] = None
purchase_url: Optional[str] = None
purchased: Optional[bool] = False
# Properties to receive via API on creation
class GiftCreate(GiftBase):
pass
# Properties to receive via API on update
class GiftUpdate(GiftBase):
recipient_name: Optional[str] = None
name: Optional[str] = None
class GiftInDBBase(GiftBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class Gift(GiftInDBBase):
pass

38
app/schemas/preference.py Normal file
View File

@ -0,0 +1,38 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Shared properties
class PreferenceBase(BaseModel):
recipient_name: str
interests: Optional[str] = None
hobbies: Optional[str] = None
favorite_colors: Optional[str] = None
clothing_size: Optional[str] = None
dislikes: Optional[str] = None
# Properties to receive via API on creation
class PreferenceCreate(PreferenceBase):
pass
# Properties to receive via API on update
class PreferenceUpdate(PreferenceBase):
recipient_name: Optional[str] = None
class PreferenceInDBBase(PreferenceBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class Preference(PreferenceInDBBase):
pass

View File

@ -0,0 +1,47 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Shared properties
class RecommendationBase(BaseModel):
recipient_name: str
occasion: Optional[str] = None
recommendation_text: str
item_name: Optional[str] = None
description: Optional[str] = None
price_estimate: Optional[float] = None
purchase_url: Optional[str] = None
saved: Optional[bool] = False
# Properties to receive via API on creation
class RecommendationCreate(BaseModel):
recipient_name: str
occasion: Optional[str] = None
preferences_id: Optional[int] = None
budget_min: Optional[float] = None
budget_max: Optional[float] = None
additional_info: Optional[str] = None
# Properties to receive via API on update
class RecommendationUpdate(RecommendationBase):
recipient_name: Optional[str] = None
recommendation_text: Optional[str] = None
saved: Optional[bool] = None
class RecommendationInDBBase(RecommendationBase):
id: int
user_id: int
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class Recommendation(RecommendationInDBBase):
pass

11
app/schemas/token.py Normal file
View File

@ -0,0 +1,11 @@
from typing import Optional
from pydantic import BaseModel
class Token(BaseModel):
access_token: str
token_type: str
class TokenPayload(BaseModel):
sub: Optional[int] = None

40
app/schemas/user.py Normal file
View File

@ -0,0 +1,40 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, EmailStr
# Shared properties
class UserBase(BaseModel):
email: Optional[EmailStr] = None
is_active: Optional[bool] = True
full_name: Optional[str] = None
# Properties to receive via API on creation
class UserCreate(UserBase):
email: EmailStr
password: str
# Properties to receive via API on update
class UserUpdate(UserBase):
password: Optional[str] = None
class UserInDBBase(UserBase):
id: Optional[int] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
# Additional properties to return via API
class User(UserInDBBase):
pass
# Additional properties stored in DB
class UserInDB(UserInDBBase):
hashed_password: str

1
app/services/__init__.py Normal file
View File

@ -0,0 +1 @@
# Empty init file to make the directory a package

View File

@ -0,0 +1,137 @@
import json
from typing import Dict, Optional
import openai
from fastapi import HTTPException, status
from app.core.config import settings
from app.schemas.preference import Preference
class AIRecommendationService:
def __init__(self):
self.api_key = settings.OPENAI_API_KEY
self.model = settings.OPENAI_MODEL
if not self.api_key:
print("Warning: OPENAI_API_KEY not set. AI recommendations will not work.")
async def generate_recommendations(
self,
recipient_name: str,
occasion: Optional[str] = None,
preferences: Optional[Preference] = None,
budget_min: Optional[float] = None,
budget_max: Optional[float] = None,
additional_info: Optional[str] = None,
) -> Dict:
"""Generate gift recommendations based on preferences and constraints."""
if not self.api_key:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="AI service not available. OPENAI_API_KEY not configured.",
)
# Construct the prompt
budget_text = ""
if budget_min is not None and budget_max is not None:
budget_text = f"Budget range: ${budget_min} - ${budget_max}"
elif budget_min is not None:
budget_text = f"Minimum budget: ${budget_min}"
elif budget_max is not None:
budget_text = f"Maximum budget: ${budget_max}"
preferences_text = ""
if preferences:
preferences_text = f"""
Recipient interests: {preferences.interests if preferences.interests else 'Not specified'}
Recipient hobbies: {preferences.hobbies if preferences.hobbies else 'Not specified'}
Favorite colors: {preferences.favorite_colors if preferences.favorite_colors else 'Not specified'}
Clothing size: {preferences.clothing_size if preferences.clothing_size else 'Not specified'}
Dislikes: {preferences.dislikes if preferences.dislikes else 'Not specified'}
"""
occasion_text = f"Occasion: {occasion}" if occasion else "Occasion: Not specified"
additional_text = f"Additional information: {additional_info}" if additional_info else ""
prompt = f"""
You are a gift recommendation expert. Please suggest a thoughtful gift for {recipient_name}.
{occasion_text}
{budget_text}
{preferences_text}
{additional_text}
Please provide your gift recommendation in JSON format with the following fields:
1. recommendation_text: A thoughtful explanation of your recommendation
2. item_name: The name of the recommended gift
3. description: A detailed description of the gift
4. price_estimate: Estimated price (as a number only, no currency symbol)
5. purchase_url: A suggested place to purchase the gift (can be a generic store name if specific URL not available)
"""
try:
openai.api_key = self.api_key
response = openai.ChatCompletion.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a helpful gift recommendation assistant."},
{"role": "user", "content": prompt}
],
temperature=0.7,
max_tokens=800,
)
# Extract the recommendation from the response
recommendation_text = response.choices[0].message.content.strip()
# Try to parse the JSON response
try:
# Find the JSON part in the response
json_start = recommendation_text.find('{')
json_end = recommendation_text.rfind('}') + 1
if json_start >= 0 and json_end > json_start:
json_content = recommendation_text[json_start:json_end]
recommendation_dict = json.loads(json_content)
else:
# If we can't find proper JSON, create a basic response
recommendation_dict = {
"recommendation_text": recommendation_text,
"item_name": "Custom Gift",
"description": "Based on the preferences provided",
"price_estimate": budget_min if budget_min else 50.0,
"purchase_url": "https://www.amazon.com"
}
# Ensure all required fields are present
required_fields = ["recommendation_text", "item_name", "description", "price_estimate", "purchase_url"]
for field in required_fields:
if field not in recommendation_dict:
recommendation_dict[field] = "Not specified"
# Convert price to float if it's a string
if isinstance(recommendation_dict["price_estimate"], str):
price_str = recommendation_dict["price_estimate"].replace("$", "").replace(",", "")
try:
recommendation_dict["price_estimate"] = float(price_str)
except ValueError:
recommendation_dict["price_estimate"] = budget_min if budget_min else 50.0
return recommendation_dict
except json.JSONDecodeError:
# If JSON parsing fails, create a structured response from the text
return {
"recommendation_text": recommendation_text,
"item_name": "Custom Gift",
"description": "Based on the preferences provided",
"price_estimate": budget_min if budget_min else 50.0,
"purchase_url": "https://www.amazon.com"
}
except Exception as e:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail=f"Error generating recommendation: {str(e)}",
)

37
main.py Normal file
View File

@ -0,0 +1,37 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.api import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Set all CORS enabled origins
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router, prefix=settings.API_V1_STR)
@app.get("/")
async def root():
return {
"name": settings.PROJECT_NAME,
"docs": f"{settings.API_V1_STR}/docs",
"health": f"{settings.API_V1_STR}/health"
}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

1
migrations/README Normal file
View File

@ -0,0 +1 @@
Generic single-database configuration with SQLite.

81
migrations/env.py Normal file
View File

@ -0,0 +1,81 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
from app.db.base import Base
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == 'sqlite'
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite,
compare_type=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Normal file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,127 @@
"""Initial migration
Revision ID: 001
Revises:
Create Date: 2023-10-20
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.sql import func
# revision identifiers, used by Alembic.
revision = '001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create users table
op.create_table(
'users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('email', sa.String(), nullable=False),
sa.Column('hashed_password', sa.String(), nullable=False),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_full_name'), 'users', ['full_name'], unique=False)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
# Create gifts table
op.create_table(
'gifts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('recipient_name', sa.String(), nullable=False),
sa.Column('relationship_to_user', sa.String(), nullable=True),
sa.Column('occasion', sa.String(), nullable=True),
sa.Column('name', sa.String(), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('price', sa.Float(), nullable=True),
sa.Column('purchase_url', sa.String(), nullable=True),
sa.Column('purchased', sa.Boolean(), nullable=True, default=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_gifts_id'), 'gifts', ['id'], unique=False)
op.create_index(op.f('ix_gifts_name'), 'gifts', ['name'], unique=False)
op.create_index(op.f('ix_gifts_occasion'), 'gifts', ['occasion'], unique=False)
op.create_index(op.f('ix_gifts_recipient_name'), 'gifts', ['recipient_name'], unique=False)
op.create_index(op.f('ix_gifts_relationship_to_user'), 'gifts', ['relationship_to_user'], unique=False)
# Create preferences table
op.create_table(
'preferences',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('recipient_name', sa.String(), nullable=False),
sa.Column('interests', sa.Text(), nullable=True),
sa.Column('hobbies', sa.Text(), nullable=True),
sa.Column('favorite_colors', sa.String(), nullable=True),
sa.Column('clothing_size', sa.String(), nullable=True),
sa.Column('dislikes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_preferences_id'), 'preferences', ['id'], unique=False)
op.create_index(op.f('ix_preferences_recipient_name'), 'preferences', ['recipient_name'], unique=False)
# Create recommendations table
op.create_table(
'recommendations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('recipient_name', sa.String(), nullable=False),
sa.Column('occasion', sa.String(), nullable=True),
sa.Column('recommendation_text', sa.Text(), nullable=False),
sa.Column('item_name', sa.String(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('price_estimate', sa.Float(), nullable=True),
sa.Column('purchase_url', sa.String(), nullable=True),
sa.Column('saved', sa.Boolean(), nullable=True, default=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=func.now(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_recommendations_id'), 'recommendations', ['id'], unique=False)
op.create_index(op.f('ix_recommendations_occasion'), 'recommendations', ['occasion'], unique=False)
op.create_index(op.f('ix_recommendations_recipient_name'), 'recommendations', ['recipient_name'], unique=False)
def downgrade():
# Drop recommendations table
op.drop_index(op.f('ix_recommendations_recipient_name'), table_name='recommendations')
op.drop_index(op.f('ix_recommendations_occasion'), table_name='recommendations')
op.drop_index(op.f('ix_recommendations_id'), table_name='recommendations')
op.drop_table('recommendations')
# Drop preferences table
op.drop_index(op.f('ix_preferences_recipient_name'), table_name='preferences')
op.drop_index(op.f('ix_preferences_id'), table_name='preferences')
op.drop_table('preferences')
# Drop gifts table
op.drop_index(op.f('ix_gifts_relationship_to_user'), table_name='gifts')
op.drop_index(op.f('ix_gifts_recipient_name'), table_name='gifts')
op.drop_index(op.f('ix_gifts_occasion'), table_name='gifts')
op.drop_index(op.f('ix_gifts_name'), table_name='gifts')
op.drop_index(op.f('ix_gifts_id'), table_name='gifts')
op.drop_table('gifts')
# Drop users table
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_full_name'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')

15
requirements.txt Normal file
View File

@ -0,0 +1,15 @@
fastapi==0.104.0
uvicorn==0.23.2
sqlalchemy==2.0.22
alembic==1.12.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
pydantic==2.4.2
pydantic-settings==2.0.3
python-multipart==0.0.6
python-dotenv==1.0.0
tenacity==8.2.3
httpx==0.25.0
ruff==0.1.0
openai==1.1.1
tiktoken==0.5.1