diff --git a/README.md b/README.md index e8acfba..fd08243 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,78 @@ -# FastAPI Application +# Food Delivery API -This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform. +A backend API for a food delivery application built with FastAPI and SQLite. + +## Features + +- User authentication and registration +- Restaurant management +- Menu item management +- Order processing +- Delivery tracking +- Role-based access control + +## Tech Stack + +- **FastAPI**: Modern, fast web framework for building APIs +- **SQLite**: Lightweight, file-based relational database +- **SQLAlchemy**: SQL toolkit and Object-Relational Mapping (ORM) +- **Alembic**: Database migration tool +- **Pydantic**: Data validation and settings management +- **JWT**: JSON Web Tokens for authentication +- **Uvicorn**: ASGI server for running FastAPI applications + +## Project Structure + +``` +. +├── app +│ ├── api +│ │ └── v1 +│ │ ├── api.py +│ │ └── endpoints +│ │ ├── auth.py +│ │ ├── deliveries.py +│ │ ├── menu_items.py +│ │ ├── orders.py +│ │ ├── restaurants.py +│ │ └── users.py +│ ├── core +│ │ ├── config.py +│ │ └── security.py +│ ├── crud +│ ├── db +│ │ └── session.py +│ ├── models +│ ├── schemas +│ ├── services +│ └── utils +├── main.py +└── requirements.txt +``` + +## Getting Started + +1. Clone the repository +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` +3. Run the application: + ```bash + uvicorn main:app --reload + ``` + +## API Documentation + +Once the application is running, you can access the API documentation at: + +- **Swagger UI**: [http://localhost:8000/docs](http://localhost:8000/docs) +- **ReDoc**: [http://localhost:8000/redoc](http://localhost:8000/redoc) + +## Environment Variables + +Create a `.env` file in the root directory with the following variables: + +``` +SECRET_KEY=your_secret_key +``` \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/__init__.py b/app/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/api.py b/app/api/v1/api.py new file mode 100644 index 0000000..142d321 --- /dev/null +++ b/app/api/v1/api.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter + +from app.api.v1.endpoints import users, auth, restaurants, menu_items, orders, deliveries + +api_router = APIRouter() + +api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"]) +api_router.include_router(users.router, prefix="/users", tags=["Users"]) +api_router.include_router(restaurants.router, prefix="/restaurants", tags=["Restaurants"]) +api_router.include_router(menu_items.router, prefix="/menu-items", tags=["Menu Items"]) +api_router.include_router(orders.router, prefix="/orders", tags=["Orders"]) +api_router.include_router(deliveries.router, prefix="/deliveries", tags=["Deliveries"]) \ No newline at end of file diff --git a/app/api/v1/endpoints/__init__.py b/app/api/v1/endpoints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/v1/endpoints/auth.py b/app/api/v1/endpoints/auth.py new file mode 100644 index 0000000..1028714 --- /dev/null +++ b/app/api/v1/endpoints/auth.py @@ -0,0 +1,19 @@ +from fastapi import APIRouter, Depends +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.post("/login") +async def login( + form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db), +): + """ + OAuth2 compatible token login, get an access token for future requests + """ + # Authentication will be implemented in a later step + return {"detail": "Authentication endpoint placeholder"} \ No newline at end of file diff --git a/app/api/v1/endpoints/deliveries.py b/app/api/v1/endpoints/deliveries.py new file mode 100644 index 0000000..1d28705 --- /dev/null +++ b/app/api/v1/endpoints/deliveries.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/") +async def get_deliveries(db: Session = Depends(get_db)): + """ + Get list of deliveries + """ + # Delivery retrieval will be implemented in a later step + return {"detail": "Deliveries endpoint placeholder"} \ No newline at end of file diff --git a/app/api/v1/endpoints/menu_items.py b/app/api/v1/endpoints/menu_items.py new file mode 100644 index 0000000..8eb6678 --- /dev/null +++ b/app/api/v1/endpoints/menu_items.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/") +async def get_menu_items(db: Session = Depends(get_db)): + """ + Get list of menu items + """ + # Menu item retrieval will be implemented in a later step + return {"detail": "Menu items endpoint placeholder"} \ No newline at end of file diff --git a/app/api/v1/endpoints/orders.py b/app/api/v1/endpoints/orders.py new file mode 100644 index 0000000..7118193 --- /dev/null +++ b/app/api/v1/endpoints/orders.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/") +async def get_orders(db: Session = Depends(get_db)): + """ + Get list of orders + """ + # Order retrieval will be implemented in a later step + return {"detail": "Orders endpoint placeholder"} \ No newline at end of file diff --git a/app/api/v1/endpoints/restaurants.py b/app/api/v1/endpoints/restaurants.py new file mode 100644 index 0000000..0e55f83 --- /dev/null +++ b/app/api/v1/endpoints/restaurants.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/") +async def get_restaurants(db: Session = Depends(get_db)): + """ + Get list of restaurants + """ + # Restaurant retrieval will be implemented in a later step + return {"detail": "Restaurants endpoint placeholder"} \ No newline at end of file diff --git a/app/api/v1/endpoints/users.py b/app/api/v1/endpoints/users.py new file mode 100644 index 0000000..d4965ca --- /dev/null +++ b/app/api/v1/endpoints/users.py @@ -0,0 +1,15 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.db.session import get_db + +router = APIRouter() + + +@router.get("/") +async def get_users(db: Session = Depends(get_db)): + """ + Get list of users + """ + # User retrieval will be implemented in a later step + return {"detail": "Users endpoint placeholder"} \ No newline at end of file diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..1262bef --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,35 @@ +import os +from pathlib import Path +from typing import List + +from pydantic import AnyHttpUrl +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + API_V1_STR: str = "/api/v1" + PROJECT_NAME: str = "Food Delivery API" + VERSION: str = "0.1.0" + DESCRIPTION: str = "Backend API for a Food Delivery Application" + + # CORS configuration + CORS_ORIGINS: List[AnyHttpUrl] = [] + + # JWT secret key + SECRET_KEY: str = os.getenv("SECRET_KEY", "supersecretkey") + ALGORITHM: str = "HS256" + # 60 minutes * 24 hours * 7 days = 7 days + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 + + # Database + DB_DIR = Path("/app") / "storage" / "db" + DB_DIR.mkdir(parents=True, exist_ok=True) + + SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite" + + class Config: + case_sensitive = True + env_file = ".env" + + +settings = Settings() \ No newline at end of file diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..6feba2f --- /dev/null +++ b/app/core/security.py @@ -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) \ No newline at end of file diff --git a/app/crud/__init__.py b/app/crud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/session.py b/app/db/session.py new file mode 100644 index 0000000..ee00f4a --- /dev/null +++ b/app/db/session.py @@ -0,0 +1,23 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + +from app.core.config import settings + +engine = create_engine( + settings.SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False} # Needed only for SQLite +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +# Dependency to get the DB session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..81a062b --- /dev/null +++ b/main.py @@ -0,0 +1,53 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.openapi.utils import get_openapi + +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 CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include API router +app.include_router(api_router, prefix=settings.API_V1_STR) + + +# Health check endpoint +@app.get("/health", tags=["Health"]) +async def health_check(): + return {"status": "ok"} + + +def custom_openapi(): + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi( + title=settings.PROJECT_NAME, + version=settings.VERSION, + description=settings.DESCRIPTION, + routes=app.routes, + ) + app.openapi_schema = openapi_schema + return app.openapi_schema + + +app.openapi = custom_openapi + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ec416c6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +fastapi>=0.95.0 +uvicorn>=0.21.1 +pydantic>=2.0.0 +pydantic-settings>=2.0.0 +sqlalchemy>=2.0.0 +alembic>=1.10.0 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +python-multipart>=0.0.6 +ruff>=0.0.262 +email-validator>=2.0.0 +python-dotenv>=1.0.0 +tenacity>=8.2.2 \ No newline at end of file