diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8e12c6d --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Environment Configuration +DATABASE_URL_MONGODB=mongodb://localhost:27017/fastapi_app +DATABASE_URL_MYSQL=mysql://user:password@localhost:3306/fastapi_app +DATABASE_URL_POSTGRESQL=postgresql://user:password@localhost:5432/fastapi_app + +# Application Settings +APP_NAME=FastAPI Multi-Database API +APP_VERSION=1.0.0 +DEBUG=True \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94cdb44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + default-libmysqlclient-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Expose port +EXPOSE 8000 + +# Run the application +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/README.md b/README.md index 0671fa4..2f46556 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,262 @@ -# fastapi-mongo-mysql-postgresql-api -FastAPI api application that uses 3 different databases as backend +# FastAPI Multi-Database API + +A FastAPI application that demonstrates integration with three different database backends: +- **MongoDB** (NoSQL document database) +- **MySQL** (Relational database) +- **PostgreSQL** (Relational database) + +## Features + +- ๐Ÿš€ **FastAPI** - Modern, fast web framework for building APIs with Python 3.7+ +- ๐Ÿ“Š **Three Database Support**: MongoDB, MySQL, and PostgreSQL +- ๐Ÿ”„ **Async Operations** - Fully asynchronous database operations +- ๐Ÿ“ **CRUD Operations** - Complete Create, Read, Update, Delete operations for each database +- ๐Ÿงช **Testing** - Basic test suite included +- ๐Ÿณ **Docker Support** - Docker Compose setup for easy development +- ๐Ÿ“š **Auto-generated API Documentation** - Swagger UI and ReDoc + +## Project Structure + +``` +fastapi-mongo-mysql-postgresql-api/ +โ”œโ”€โ”€ app/ +โ”‚ โ”œโ”€โ”€ database/ # Database connection modules +โ”‚ โ”‚ โ”œโ”€โ”€ mongodb.py # MongoDB connection +โ”‚ โ”‚ โ”œโ”€โ”€ mysql.py # MySQL connection +โ”‚ โ”‚ โ””โ”€โ”€ postgresql.py # PostgreSQL connection +โ”‚ โ”œโ”€โ”€ models/ # Data models +โ”‚ โ”‚ โ”œโ”€โ”€ mongo_models.py +โ”‚ โ”‚ โ”œโ”€โ”€ mysql_models.py +โ”‚ โ”‚ โ””โ”€โ”€ postgresql_models.py +โ”‚ โ”œโ”€โ”€ routers/ # API route handlers +โ”‚ โ”‚ โ”œโ”€โ”€ mongodb_routes.py +โ”‚ โ”‚ โ”œโ”€โ”€ mysql_routes.py +โ”‚ โ”‚ โ””โ”€โ”€ postgresql_routes.py +โ”‚ โ””โ”€โ”€ config.py # Application configuration +โ”œโ”€โ”€ tests/ # Test suite +โ”œโ”€โ”€ main.py # Application entry point +โ”œโ”€โ”€ requirements.txt # Python dependencies +โ”œโ”€โ”€ docker-compose.yml # Docker setup for databases +โ”œโ”€โ”€ Dockerfile # Application container +โ””โ”€โ”€ .env.example # Environment variables template +``` + +## Quick Start + +### 1. Clone the Repository + +```bash +git clone https://github.com/moreskylab/fastapi-mongo-mysql-postgresql-api.git +cd fastapi-mongo-mysql-postgresql-api +``` + +### 2. Set up Environment + +```bash +# Copy environment template +cp .env.example .env + +# Edit .env file with your database configurations +``` + +### 3. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 4. Start Databases (using Docker) + +```bash +# Start all databases +docker-compose up -d + +# Check if containers are running +docker-compose ps +``` + +### 5. Run the Application + +```bash +# Development mode +uvicorn main:app --reload + +# Production mode +uvicorn main:app --host 0.0.0.0 --port 8000 +``` + +### 6. Access the API + +- **API Documentation (Swagger)**: http://localhost:8000/docs +- **Alternative Documentation (ReDoc)**: http://localhost:8000/redoc +- **API Base URL**: http://localhost:8000 + +## API Endpoints + +### Root Endpoints +- `GET /` - API information and available endpoints +- `GET /health` - Health check + +### MongoDB Endpoints +- `POST /api/v1/mongodb/users/` - Create user +- `GET /api/v1/mongodb/users/` - Get all users +- `GET /api/v1/mongodb/users/{user_id}` - Get user by ID +- `PUT /api/v1/mongodb/users/{user_id}` - Update user +- `DELETE /api/v1/mongodb/users/{user_id}` - Delete user + +### MySQL Endpoints +- `POST /api/v1/mysql/users/` - Create user +- `GET /api/v1/mysql/users/` - Get all users +- `GET /api/v1/mysql/users/{user_id}` - Get user by ID +- `PUT /api/v1/mysql/users/{user_id}` - Update user +- `DELETE /api/v1/mysql/users/{user_id}` - Delete user + +### PostgreSQL Endpoints +- `POST /api/v1/postgresql/users/` - Create user +- `GET /api/v1/postgresql/users/` - Get all users +- `GET /api/v1/postgresql/users/{user_id}` - Get user by ID +- `PUT /api/v1/postgresql/users/{user_id}` - Update user +- `DELETE /api/v1/postgresql/users/{user_id}` - Delete user + +## Example Usage + +### Create a User in MongoDB + +```bash +curl -X POST "http://localhost:8000/api/v1/mongodb/users/" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "age": 30 + }' +``` + +### Get All Users from MySQL + +```bash +curl -X GET "http://localhost:8000/api/v1/mysql/users/" +``` + +### Update a User in PostgreSQL + +```bash +curl -X PUT "http://localhost:8000/api/v1/postgresql/users/1" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Jane Doe", + "age": 25 + }' +``` + +## Database Configuration + +### Environment Variables + +Create a `.env` file based on `.env.example`: + +```env +# Database URLs +DATABASE_URL_MONGODB=mongodb://localhost:27017/fastapi_app +DATABASE_URL_MYSQL=mysql://user:password@localhost:3306/fastapi_app +DATABASE_URL_POSTGRESQL=postgresql://user:password@localhost:5432/fastapi_app + +# Application Settings +APP_NAME=FastAPI Multi-Database API +APP_VERSION=1.0.0 +DEBUG=True +``` + +### Database Schemas + +#### User Model (Common structure across all databases) + +```json +{ + "name": "string", + "email": "string", + "age": "integer", + "created_at": "datetime" (auto-generated) +} +``` + +## Testing + +```bash +# Run tests +pytest + +# Run tests with coverage +pytest --cov=app +``` + +## Docker Deployment + +### Using Docker Compose (Recommended) + +```bash +# Build and start all services +docker-compose up --build + +# Start in background +docker-compose up -d --build + +# Stop services +docker-compose down +``` + +### Manual Docker Build + +```bash +# Build the application image +docker build -t fastapi-multi-db . + +# Run the container +docker run -p 8000:8000 fastapi-multi-db +``` + +## Technologies Used + +- **FastAPI** - Web framework +- **MongoDB** with **Motor** - Async MongoDB driver +- **MySQL** with **aiomysql** + **SQLAlchemy** - Async MySQL driver +- **PostgreSQL** with **asyncpg** + **SQLAlchemy** - Async PostgreSQL driver +- **Pydantic** - Data validation and settings management +- **Uvicorn** - ASGI server +- **pytest** - Testing framework + +## Development + +### Adding New Endpoints + +1. Create new model in appropriate `models/` file +2. Add database operations in corresponding `database/` module +3. Create route handlers in appropriate `routers/` file +4. Include router in `main.py` + +### Database Migration + +For SQL databases (MySQL/PostgreSQL), you can use Alembic for migrations: + +```bash +# Initialize migrations +alembic init alembic + +# Create migration +alembic revision --autogenerate -m "Create users table" + +# Apply migrations +alembic upgrade head +``` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests +5. Submit a pull request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..bea1d02 --- /dev/null +++ b/app/config.py @@ -0,0 +1,20 @@ +from pydantic_settings import BaseSettings +from typing import Optional + + +class Settings(BaseSettings): + app_name: str = "FastAPI Multi-Database API" + app_version: str = "1.0.0" + debug: bool = True + + # Database URLs + database_url_mongodb: str = "mongodb://localhost:27017/fastapi_app" + database_url_mysql: str = "mysql://user:password@localhost:3306/fastapi_app" + database_url_postgresql: str = "postgresql://user:password@localhost:5432/fastapi_app" + + class Config: + env_file = ".env" + case_sensitive = False + + +settings = Settings() \ No newline at end of file diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/mongodb.py b/app/database/mongodb.py new file mode 100644 index 0000000..929a30f --- /dev/null +++ b/app/database/mongodb.py @@ -0,0 +1,40 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from app.config import settings +import logging + +logger = logging.getLogger(__name__) + + +class MongoDB: + client: AsyncIOMotorClient = None + database = None + + +mongodb = MongoDB() + + +async def connect_to_mongo(): + """Create database connection""" + try: + mongodb.client = AsyncIOMotorClient(settings.database_url_mongodb) + mongodb.database = mongodb.client.get_default_database() + + # Test connection + await mongodb.client.admin.command('ping') + logger.info("Connected to MongoDB successfully") + except Exception as e: + logger.error(f"Error connecting to MongoDB: {e}") + # For demo purposes, we'll continue without failing + pass + + +async def close_mongo_connection(): + """Close database connection""" + if mongodb.client: + mongodb.client.close() + logger.info("Disconnected from MongoDB") + + +async def get_database(): + """Get database instance""" + return mongodb.database \ No newline at end of file diff --git a/app/database/mysql.py b/app/database/mysql.py new file mode 100644 index 0000000..81ae14c --- /dev/null +++ b/app/database/mysql.py @@ -0,0 +1,55 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import declarative_base +from sqlalchemy.pool import StaticPool +from app.config import settings +import logging + +logger = logging.getLogger(__name__) + +# Convert mysql:// to mysql+aiomysql:// +mysql_url = settings.database_url_mysql.replace("mysql://", "mysql+aiomysql://") + +# Create async engine +engine = create_async_engine( + mysql_url, + poolclass=StaticPool, + connect_args={ + "check_same_thread": False, # Only needed for SQLite + } if "sqlite" in mysql_url else {}, + echo=True if settings.debug else False, +) + +# Create session factory +AsyncSessionLocal = async_sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, +) + +# Create declarative base +Base = declarative_base() + + +async def get_mysql_session(): + """Get MySQL database session""" + async with AsyncSessionLocal() as session: + try: + yield session + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + +async def init_mysql_db(): + """Initialize MySQL database""" + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("MySQL database initialized successfully") + except Exception as e: + logger.error(f"Error initializing MySQL database: {e}") + # For demo purposes, we'll continue without failing + pass \ No newline at end of file diff --git a/app/database/postgresql.py b/app/database/postgresql.py new file mode 100644 index 0000000..be94475 --- /dev/null +++ b/app/database/postgresql.py @@ -0,0 +1,50 @@ +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker +from sqlalchemy.orm import declarative_base +from app.config import settings +import logging + +logger = logging.getLogger(__name__) + +# Convert postgresql:// to postgresql+asyncpg:// +postgresql_url = settings.database_url_postgresql.replace("postgresql://", "postgresql+asyncpg://") + +# Create async engine +engine = create_async_engine( + postgresql_url, + echo=True if settings.debug else False, +) + +# Create session factory +AsyncSessionLocal = async_sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + class_=AsyncSession, +) + +# Create declarative base +Base = declarative_base() + + +async def get_postgresql_session(): + """Get PostgreSQL database session""" + async with AsyncSessionLocal() as session: + try: + yield session + except Exception as e: + await session.rollback() + raise e + finally: + await session.close() + + +async def init_postgresql_db(): + """Initialize PostgreSQL database""" + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + logger.info("PostgreSQL database initialized successfully") + except Exception as e: + logger.error(f"Error initializing PostgreSQL database: {e}") + # For demo purposes, we'll continue without failing + pass \ 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/models/mongo_models.py b/app/models/mongo_models.py new file mode 100644 index 0000000..e113a64 --- /dev/null +++ b/app/models/mongo_models.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional +from datetime import datetime + + +class UserMongo(BaseModel): + """MongoDB User model""" + model_config = ConfigDict(populate_by_name=True) + + id: Optional[str] = Field(None, alias="_id") + name: str + email: str + age: int + created_at: datetime = Field(default_factory=datetime.utcnow) + + +class UserMongoCreate(BaseModel): + """Create user model for MongoDB""" + name: str + email: str + age: int + + +class UserMongoUpdate(BaseModel): + """Update user model for MongoDB""" + name: Optional[str] = None + email: Optional[str] = None + age: Optional[int] = None \ No newline at end of file diff --git a/app/models/mysql_models.py b/app/models/mysql_models.py new file mode 100644 index 0000000..4e99687 --- /dev/null +++ b/app/models/mysql_models.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.sql import func +from app.database.mysql import Base +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class UserMySQL(Base): + """MySQL User table""" + __tablename__ = "users_mysql" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + age = Column(Integer, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + +from pydantic import BaseModel, ConfigDict +from typing import Optional +from datetime import datetime + + +class UserMySQLSchema(BaseModel): + """Pydantic schema for MySQL User""" + model_config = ConfigDict(from_attributes=True) + + id: Optional[int] = None + name: str + email: str + age: int + created_at: Optional[datetime] = None + + +class UserMySQLCreate(BaseModel): + """Create user schema for MySQL""" + name: str + email: str + age: int + + +class UserMySQLUpdate(BaseModel): + """Update user schema for MySQL""" + name: Optional[str] = None + email: Optional[str] = None + age: Optional[int] = None \ No newline at end of file diff --git a/app/models/postgresql_models.py b/app/models/postgresql_models.py new file mode 100644 index 0000000..aa7f042 --- /dev/null +++ b/app/models/postgresql_models.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.sql import func +from app.database.postgresql import Base +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class UserPostgreSQL(Base): + """PostgreSQL User table""" + __tablename__ = "users_postgresql" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + age = Column(Integer, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + +from pydantic import BaseModel, ConfigDict +from typing import Optional +from datetime import datetime + + +class UserPostgreSQLSchema(BaseModel): + """Pydantic schema for PostgreSQL User""" + model_config = ConfigDict(from_attributes=True) + + id: Optional[int] = None + name: str + email: str + age: int + created_at: Optional[datetime] = None + + +class UserPostgreSQLCreate(BaseModel): + """Create user schema for PostgreSQL""" + name: str + email: str + age: int + + +class UserPostgreSQLUpdate(BaseModel): + """Update user schema for PostgreSQL""" + name: Optional[str] = None + email: Optional[str] = None + age: Optional[int] = None \ No newline at end of file diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routers/mongodb_routes.py b/app/routers/mongodb_routes.py new file mode 100644 index 0000000..5cca8db --- /dev/null +++ b/app/routers/mongodb_routes.py @@ -0,0 +1,188 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +from app.models.mongo_models import UserMongo, UserMongoCreate, UserMongoUpdate +from app.database.mongodb import get_database +from bson import ObjectId +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/mongodb", tags=["MongoDB"]) + + +@router.post("/users/", response_model=UserMongo) +async def create_user(user: UserMongoCreate): + """Create a new user in MongoDB""" + try: + database = await get_database() + if not database: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="MongoDB is not available" + ) + + user_dict = user.dict() + user_dict["created_at"] = datetime.utcnow() + + result = await database.users.insert_one(user_dict) + + # Get the created user + created_user = await database.users.find_one({"_id": result.inserted_id}) + created_user["_id"] = str(created_user["_id"]) + + return UserMongo(**created_user) + except Exception as e: + logger.error(f"Error creating user in MongoDB: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to create user" + ) + + +@router.get("/users/", response_model=List[UserMongo]) +async def get_users(): + """Get all users from MongoDB""" + try: + database = await get_database() + if not database: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="MongoDB is not available" + ) + + users = [] + async for user in database.users.find(): + user["_id"] = str(user["_id"]) + users.append(UserMongo(**user)) + + return users + except Exception as e: + logger.error(f"Error retrieving users from MongoDB: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve users" + ) + + +@router.get("/users/{user_id}", response_model=UserMongo) +async def get_user(user_id: str): + """Get a specific user from MongoDB""" + try: + database = await get_database() + if not database: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="MongoDB is not available" + ) + + if not ObjectId.is_valid(user_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid user ID format" + ) + + user = await database.users.find_one({"_id": ObjectId(user_id)}) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + user["_id"] = str(user["_id"]) + return UserMongo(**user) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error retrieving user from MongoDB: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve user" + ) + + +@router.put("/users/{user_id}", response_model=UserMongo) +async def update_user(user_id: str, user_update: UserMongoUpdate): + """Update a user in MongoDB""" + try: + database = await get_database() + if not database: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="MongoDB is not available" + ) + + if not ObjectId.is_valid(user_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid user ID format" + ) + + update_data = {k: v for k, v in user_update.dict().items() if v is not None} + + if not update_data: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No data provided for update" + ) + + result = await database.users.update_one( + {"_id": ObjectId(user_id)}, + {"$set": update_data} + ) + + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Get updated user + updated_user = await database.users.find_one({"_id": ObjectId(user_id)}) + updated_user["_id"] = str(updated_user["_id"]) + + return UserMongo(**updated_user) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error updating user in MongoDB: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user" + ) + + +@router.delete("/users/{user_id}") +async def delete_user(user_id: str): + """Delete a user from MongoDB""" + try: + database = await get_database() + if not database: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="MongoDB is not available" + ) + + if not ObjectId.is_valid(user_id): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid user ID format" + ) + + result = await database.users.delete_one({"_id": ObjectId(user_id)}) + + if result.deleted_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return {"message": "User deleted successfully"} + except HTTPException: + raise + except Exception as e: + logger.error(f"Error deleting user from MongoDB: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user" + ) \ No newline at end of file diff --git a/app/routers/mysql_routes.py b/app/routers/mysql_routes.py new file mode 100644 index 0000000..4699017 --- /dev/null +++ b/app/routers/mysql_routes.py @@ -0,0 +1,126 @@ +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import List +from app.models.mysql_models import UserMySQL, UserMySQLSchema, UserMySQLCreate, UserMySQLUpdate +from app.database.mysql import get_mysql_session +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/mysql", tags=["MySQL"]) + + +@router.post("/users/", response_model=UserMySQLSchema) +async def create_user(user: UserMySQLCreate, db: AsyncSession = Depends(get_mysql_session)): + """Create a new user in MySQL""" + try: + db_user = UserMySQL(**user.dict()) + db.add(db_user) + await db.commit() + await db.refresh(db_user) + return UserMySQLSchema.model_validate(db_user) + except Exception as e: + await db.rollback() + logger.error(f"Error creating user in MySQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to create user" + ) + + +@router.get("/users/", response_model=List[UserMySQLSchema]) +async def get_users(db: AsyncSession = Depends(get_mysql_session)): + """Get all users from MySQL""" + try: + result = await db.execute(select(UserMySQL)) + users = result.scalars().all() + return [UserMySQLSchema.model_validate(user) for user in users] + except Exception as e: + logger.error(f"Error retrieving users from MySQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve users" + ) + + +@router.get("/users/{user_id}", response_model=UserMySQLSchema) +async def get_user(user_id: int, db: AsyncSession = Depends(get_mysql_session)): + """Get a specific user from MySQL""" + try: + result = await db.execute(select(UserMySQL).where(UserMySQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return UserMySQLSchema.model_validate(user) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error retrieving user from MySQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve user" + ) + + +@router.put("/users/{user_id}", response_model=UserMySQLSchema) +async def update_user(user_id: int, user_update: UserMySQLUpdate, db: AsyncSession = Depends(get_mysql_session)): + """Update a user in MySQL""" + try: + result = await db.execute(select(UserMySQL).where(UserMySQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + update_data = user_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(user, field, value) + + await db.commit() + await db.refresh(user) + return UserMySQLSchema.model_validate(user) + except HTTPException: + raise + except Exception as e: + await db.rollback() + logger.error(f"Error updating user in MySQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user" + ) + + +@router.delete("/users/{user_id}") +async def delete_user(user_id: int, db: AsyncSession = Depends(get_mysql_session)): + """Delete a user from MySQL""" + try: + result = await db.execute(select(UserMySQL).where(UserMySQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + await db.delete(user) + await db.commit() + return {"message": "User deleted successfully"} + except HTTPException: + raise + except Exception as e: + await db.rollback() + logger.error(f"Error deleting user from MySQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user" + ) \ No newline at end of file diff --git a/app/routers/postgresql_routes.py b/app/routers/postgresql_routes.py new file mode 100644 index 0000000..a801d3d --- /dev/null +++ b/app/routers/postgresql_routes.py @@ -0,0 +1,126 @@ +from fastapi import APIRouter, HTTPException, status, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import List +from app.models.postgresql_models import UserPostgreSQL, UserPostgreSQLSchema, UserPostgreSQLCreate, UserPostgreSQLUpdate +from app.database.postgresql import get_postgresql_session +import logging + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/postgresql", tags=["PostgreSQL"]) + + +@router.post("/users/", response_model=UserPostgreSQLSchema) +async def create_user(user: UserPostgreSQLCreate, db: AsyncSession = Depends(get_postgresql_session)): + """Create a new user in PostgreSQL""" + try: + db_user = UserPostgreSQL(**user.dict()) + db.add(db_user) + await db.commit() + await db.refresh(db_user) + return UserPostgreSQLSchema.model_validate(db_user) + except Exception as e: + await db.rollback() + logger.error(f"Error creating user in PostgreSQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to create user" + ) + + +@router.get("/users/", response_model=List[UserPostgreSQLSchema]) +async def get_users(db: AsyncSession = Depends(get_postgresql_session)): + """Get all users from PostgreSQL""" + try: + result = await db.execute(select(UserPostgreSQL)) + users = result.scalars().all() + return [UserPostgreSQLSchema.model_validate(user) for user in users] + except Exception as e: + logger.error(f"Error retrieving users from PostgreSQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve users" + ) + + +@router.get("/users/{user_id}", response_model=UserPostgreSQLSchema) +async def get_user(user_id: int, db: AsyncSession = Depends(get_postgresql_session)): + """Get a specific user from PostgreSQL""" + try: + result = await db.execute(select(UserPostgreSQL).where(UserPostgreSQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return UserPostgreSQLSchema.model_validate(user) + except HTTPException: + raise + except Exception as e: + logger.error(f"Error retrieving user from PostgreSQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to retrieve user" + ) + + +@router.put("/users/{user_id}", response_model=UserPostgreSQLSchema) +async def update_user(user_id: int, user_update: UserPostgreSQLUpdate, db: AsyncSession = Depends(get_postgresql_session)): + """Update a user in PostgreSQL""" + try: + result = await db.execute(select(UserPostgreSQL).where(UserPostgreSQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + update_data = user_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(user, field, value) + + await db.commit() + await db.refresh(user) + return UserPostgreSQLSchema.model_validate(user) + except HTTPException: + raise + except Exception as e: + await db.rollback() + logger.error(f"Error updating user in PostgreSQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to update user" + ) + + +@router.delete("/users/{user_id}") +async def delete_user(user_id: int, db: AsyncSession = Depends(get_postgresql_session)): + """Delete a user from PostgreSQL""" + try: + result = await db.execute(select(UserPostgreSQL).where(UserPostgreSQL.id == user_id)) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + await db.delete(user) + await db.commit() + return {"message": "User deleted successfully"} + except HTTPException: + raise + except Exception as e: + await db.rollback() + logger.error(f"Error deleting user from PostgreSQL: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to delete user" + ) \ No newline at end of file diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..0244d08 --- /dev/null +++ b/demo.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +""" +Example usage script for FastAPI Multi-Database API + +This script demonstrates how to interact with all three databases +through the API endpoints. +""" + +import requests +import json +import time + + +def main(): + base_url = "http://localhost:8000" + + print("๐Ÿš€ FastAPI Multi-Database API Demo") + print("=" * 50) + + # Check if the API is running + try: + response = requests.get(f"{base_url}/health", timeout=5) + if response.status_code == 200: + print("โœ… API is running!") + print(f"๐Ÿ“Š Response: {response.json()}") + else: + print("โŒ API is not responding correctly") + return + except requests.exceptions.RequestException as e: + print(f"โŒ Cannot connect to API: {e}") + print("๐Ÿ’ก Make sure the API is running: python main.py") + return + + print("\n" + "=" * 50) + print("๐Ÿ“‹ API Information") + response = requests.get(f"{base_url}/") + print(json.dumps(response.json(), indent=2)) + + # Test data + test_users = [ + {"name": "John Doe", "email": "john@example.com", "age": 30}, + {"name": "Jane Smith", "email": "jane@example.com", "age": 25}, + {"name": "Bob Johnson", "email": "bob@example.com", "age": 35} + ] + + databases = ["mongodb", "mysql", "postgresql"] + + for db in databases: + print(f"\n" + "=" * 50) + print(f"๐Ÿ—„๏ธ Testing {db.upper()} Database") + print("=" * 50) + + endpoint = f"{base_url}/api/v1/{db}/users/" + + # Test GET (should be empty initially or error if DB not available) + print(f"\n๐Ÿ“– GET {endpoint}") + try: + response = requests.get(endpoint) + print(f"Status: {response.status_code}") + if response.status_code == 200: + users = response.json() + print(f"Users found: {len(users)}") + if users: + print("Users:", json.dumps(users, indent=2)) + else: + print("No users found") + else: + print(f"Error: {response.json()}") + except Exception as e: + print(f"Request failed: {e}") + + # Test POST (create users) + print(f"\n๐Ÿ“ POST {endpoint}") + for i, user_data in enumerate(test_users): + try: + response = requests.post(endpoint, json=user_data) + print(f"Creating user {i+1}: Status {response.status_code}") + if response.status_code in [200, 201]: + created_user = response.json() + print(f"Created: {created_user.get('name', 'Unknown')} (ID: {created_user.get('id', 'N/A')})") + else: + print(f"Error: {response.json()}") + break + except Exception as e: + print(f"Request failed: {e}") + break + + # Test GET again (should show created users) + print(f"\n๐Ÿ“– GET {endpoint} (after creation)") + try: + response = requests.get(endpoint) + if response.status_code == 200: + users = response.json() + print(f"Total users: {len(users)}") + for user in users: + print(f"- {user.get('name', 'Unknown')} ({user.get('email', 'no-email')})") + else: + print(f"Error: {response.json()}") + except Exception as e: + print(f"Request failed: {e}") + + print(f"\n" + "=" * 50) + print("๐ŸŽ‰ Demo completed!") + print("๐Ÿ’ก Visit http://localhost:8000/docs for interactive API documentation") + print("๐Ÿ’ก Visit http://localhost:8000/redoc for alternative documentation") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8014b5a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +version: '3.8' + +services: + # MongoDB + mongodb: + image: mongo:7.0 + container_name: fastapi_mongodb + restart: unless-stopped + ports: + - "27017:27017" + environment: + MONGO_INITDB_DATABASE: fastapi_app + volumes: + - mongodb_data:/data/db + networks: + - fastapi_network + + # MySQL + mysql: + image: mysql:8.0 + container_name: fastapi_mysql + restart: unless-stopped + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: fastapi_app + MYSQL_USER: user + MYSQL_PASSWORD: password + volumes: + - mysql_data:/var/lib/mysql + networks: + - fastapi_network + + # PostgreSQL + postgresql: + image: postgres:15 + container_name: fastapi_postgresql + restart: unless-stopped + ports: + - "5432:5432" + environment: + POSTGRES_DB: fastapi_app + POSTGRES_USER: user + POSTGRES_PASSWORD: password + volumes: + - postgresql_data:/var/lib/postgresql/data + networks: + - fastapi_network + + # FastAPI Application (optional) + # fastapi_app: + # build: . + # container_name: fastapi_app + # restart: unless-stopped + # ports: + # - "8000:8000" + # environment: + # DATABASE_URL_MONGODB: mongodb://mongodb:27017/fastapi_app + # DATABASE_URL_MYSQL: mysql://user:password@mysql:3306/fastapi_app + # DATABASE_URL_POSTGRESQL: postgresql://user:password@postgresql:5432/fastapi_app + # depends_on: + # - mongodb + # - mysql + # - postgresql + # networks: + # - fastapi_network + +volumes: + mongodb_data: + mysql_data: + postgresql_data: + +networks: + fastapi_network: + driver: bridge \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b2f77e6 --- /dev/null +++ b/main.py @@ -0,0 +1,82 @@ +from fastapi import FastAPI +from contextlib import asynccontextmanager +from app.config import settings +from app.database.mongodb import connect_to_mongo, close_mongo_connection +from app.database.mysql import init_mysql_db +from app.database.postgresql import init_postgresql_db +from app.routers import mongodb_routes, mysql_routes, postgresql_routes +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Manage application lifespan events""" + # Startup + logger.info("Starting up FastAPI Multi-Database API") + + # Initialize databases + await connect_to_mongo() + await init_mysql_db() + await init_postgresql_db() + + logger.info("Database connections established") + + yield + + # Shutdown + logger.info("Shutting down FastAPI Multi-Database API") + await close_mongo_connection() + logger.info("Database connections closed") + + +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description="A FastAPI application demonstrating integration with MongoDB, MySQL, and PostgreSQL databases", + lifespan=lifespan, +) + +# Include routers +app.include_router(mongodb_routes.router, prefix="/api/v1") +app.include_router(mysql_routes.router, prefix="/api/v1") +app.include_router(postgresql_routes.router, prefix="/api/v1") + + +@app.get("/") +async def root(): + """Root endpoint""" + return { + "message": "Welcome to FastAPI Multi-Database API", + "version": settings.app_version, + "databases": ["MongoDB", "MySQL", "PostgreSQL"], + "endpoints": { + "MongoDB": "/api/v1/mongodb/users/", + "MySQL": "/api/v1/mysql/users/", + "PostgreSQL": "/api/v1/postgresql/users/" + } + } + + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "app_name": settings.app_name, + "version": settings.app_version + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=settings.debug, + log_level="info" + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..258a290 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,31 @@ +# FastAPI and core dependencies +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Database drivers and ORMs +# MongoDB +motor==3.3.2 +pymongo==4.6.0 + +# MySQL +aiomysql==0.2.0 +mysqlclient==2.2.0 + +# PostgreSQL +asyncpg==0.29.0 +psycopg2-binary==2.9.9 + +# SQLAlchemy for SQL databases +sqlalchemy==2.0.23 +alembic==1.13.0 + +# Additional utilities +python-dotenv==1.0.0 +python-multipart==0.0.6 +httpx==0.25.2 + +# Development and testing +pytest==7.4.3 +pytest-asyncio==0.21.1 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..c1d2e69 --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,48 @@ +import pytest +from fastapi.testclient import TestClient +from main import app + +client = TestClient(app) + + +def test_root_endpoint(): + """Test the root endpoint""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert "message" in data + assert "databases" in data + assert "MongoDB" in data["databases"] + assert "MySQL" in data["databases"] + assert "PostgreSQL" in data["databases"] + + +def test_health_check(): + """Test the health check endpoint""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert "app_name" in data + assert "version" in data + + +def test_mongodb_users_endpoint(): + """Test MongoDB users endpoint is accessible""" + response = client.get("/api/v1/mongodb/users/") + # Should return 200 or 500/503 (if MongoDB is not available) + assert response.status_code in [200, 500, 503] + + +def test_mysql_users_endpoint(): + """Test MySQL users endpoint is accessible""" + response = client.get("/api/v1/mysql/users/") + # Should return 200 or 500 (if MySQL is not available) + assert response.status_code in [200, 500] + + +def test_postgresql_users_endpoint(): + """Test PostgreSQL users endpoint is accessible""" + response = client.get("/api/v1/postgresql/users/") + # Should return 200 or 500 (if PostgreSQL is not available) + assert response.status_code in [200, 500] \ No newline at end of file