diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..13275ff --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# FastAPI Multi-Database API Configuration +# Copy this file to .env and update with your database credentials + +# API Settings +APP_NAME=FastAPI Multi-Database API +APP_VERSION=1.0.0 +DEBUG=true + +# MongoDB Settings +MONGODB_URL=mongodb://localhost:27017 +MONGODB_DATABASE=fastapi_db + +# MySQL Settings +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=password +MYSQL_DATABASE=fastapi_db + +# PostgreSQL Settings +POSTGRESQL_HOST=localhost +POSTGRESQL_PORT=5432 +POSTGRESQL_USER=postgres +POSTGRESQL_PASSWORD=password +POSTGRESQL_DATABASE=fastapi_db + +# Security +SECRET_KEY=your-secret-key-change-this-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7860ebb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +## [1.0.0] - 2024-12-XX + +### Added +- Initial FastAPI template with multi-database support +- MongoDB integration with Motor (async driver) +- MySQL integration with aiomysql (async driver) +- PostgreSQL integration with asyncpg (async driver) +- Comprehensive CRUD operations for all databases +- Pydantic models for data validation +- Health check endpoints for all databases +- Docker and Docker Compose configuration +- Auto-generated API documentation +- Environment-based configuration management +- Comprehensive error handling and logging +- Production-ready project structure + +### Features +- **Async/Await Support**: Full asynchronous operation support +- **Multiple Databases**: MongoDB, MySQL, PostgreSQL examples +- **Auto Documentation**: Swagger UI and ReDoc integration +- **Docker Ready**: Complete containerization setup +- **Type Safety**: Full type hints with Pydantic +- **Configuration Management**: Environment variable support +- **Error Handling**: Comprehensive error responses +- **Health Monitoring**: Database connectivity checks \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2cf782d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements first for better caching +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/Makefile b/Makefile new file mode 100644 index 0000000..470cc39 --- /dev/null +++ b/Makefile @@ -0,0 +1,50 @@ +.PHONY: help install install-dev run test test-basic clean format lint docker-up docker-down + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +install: ## Install production dependencies + pip install -r requirements.txt + +install-dev: ## Install development dependencies + pip install -r requirements-dev.txt + +run: ## Run the FastAPI application + python main.py + +test-basic: ## Run basic structure validation + python test_basic.py + +test: ## Run full test suite (requires dev dependencies) + pytest + +format: ## Format code with black and isort + black . + isort . + +lint: ## Run linting with flake8 + flake8 . + +clean: ## Clean up Python cache files + find . -type f -name "*.pyc" -delete + find . -type d -name "__pycache__" -delete + find . -type d -name "*.egg-info" -exec rm -rf {} + + +docker-up: ## Start all services with Docker Compose + docker-compose up -d + +docker-down: ## Stop all Docker services + docker-compose down + +docker-logs: ## View Docker logs + docker-compose logs -f + +# Database specific commands +db-mongo: ## Start only MongoDB + docker-compose up -d mongodb + +db-mysql: ## Start only MySQL + docker-compose up -d mysql + +db-postgres: ## Start only PostgreSQL + docker-compose up -d postgresql \ No newline at end of file diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..f1cd068 --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,51 @@ +# FastAPI Multi-Database Template + +## Development Setup + +### Prerequisites +- Python 3.11+ +- Docker and Docker Compose (for database services) + +### Quick Start +1. Clone the repository +2. Install dependencies: `pip install -r requirements.txt` +3. Copy environment variables: `cp .env.example .env` +4. Start databases: `docker-compose up -d mongodb mysql postgresql` +5. Run the application: `python main.py` + +### Testing the Template +Run the basic validation test: `python test_basic.py` + +### API Documentation +Once running, visit: +- http://localhost:8000/docs (Swagger UI) +- http://localhost:8000/redoc (ReDoc) + +### Database Models + +#### MongoDB (Users) +- Create user: `POST /mongodb/users` +- Get users: `GET /mongodb/users` +- Get user: `GET /mongodb/users/{id}` +- Update user: `PUT /mongodb/users/{id}` +- Delete user: `DELETE /mongodb/users/{id}` + +#### MySQL (Products) +- Create product: `POST /mysql/products` +- Get products: `GET /mysql/products` +- Get product: `GET /mysql/products/{id}` +- Update product: `PUT /mysql/products/{id}` +- Delete product: `DELETE /mysql/products/{id}` + +#### PostgreSQL (Orders) +- Create order: `POST /postgresql/orders` +- Get orders: `GET /postgresql/orders` +- Get order: `GET /postgresql/orders/{id}` +- Update order: `PUT /postgresql/orders/{id}` +- Delete order: `DELETE /postgresql/orders/{id}` + +### Health Checks +- Overall: `GET /health` +- MongoDB: `GET /mongodb/health` +- MySQL: `GET /mysql/health` +- PostgreSQL: `GET /postgresql/health` \ No newline at end of file diff --git a/README.md b/README.md index 0671fa4..832905f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,269 @@ -# fastapi-mongo-mysql-postgresql-api -FastAPI api application that uses 3 different databases as backend +# FastAPI Multi-Database API Template + +A comprehensive FastAPI template that demonstrates how to build a robust API application with support for three different databases: **MongoDB**, **MySQL**, and **PostgreSQL**. + +## Features + +- ✅ **FastAPI** - Modern, fast web framework for building APIs +- ✅ **Multiple Databases** - MongoDB, MySQL, PostgreSQL support +- ✅ **Async Operations** - Fully asynchronous database operations +- ✅ **Pydantic Models** - Data validation and serialization +- ✅ **Auto-generated Documentation** - Interactive API docs with Swagger UI +- ✅ **Health Checks** - Database connectivity monitoring +- ✅ **CRUD Operations** - Complete Create, Read, Update, Delete examples +- ✅ **Docker Support** - Easy deployment with Docker Compose +- ✅ **Environment Configuration** - Flexible configuration management +- ✅ **Error Handling** - Comprehensive error handling and logging + +## Project Structure + +``` +├── app/ +│ ├── api/ # API route handlers +│ │ ├── mongodb_routes.py # MongoDB endpoints (Users) +│ │ ├── mysql_routes.py # MySQL endpoints (Products) +│ │ └── postgresql_routes.py# PostgreSQL endpoints (Orders) +│ ├── core/ +│ │ └── config.py # Application configuration +│ ├── db/ # Database connections +│ │ ├── mongodb.py # MongoDB connection and operations +│ │ ├── mysql.py # MySQL connection and operations +│ │ └── postgresql.py # PostgreSQL connection and operations +│ └── models/ +│ └── schemas.py # Pydantic models for data validation +├── main.py # FastAPI application entry point +├── requirements.txt # Python dependencies +├── docker-compose.yml # Docker services configuration +├── Dockerfile # Application container +├── .env.example # Environment variables template +└── README.md # This file +``` + +## Database Models + +### MongoDB - Users Collection +- **id**: Unique identifier +- **name**: User's full name +- **email**: User's email address +- **age**: User's age (optional) +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### MySQL - Products Table +- **id**: Unique identifier +- **name**: Product name +- **description**: Product description (optional) +- **price**: Product price +- **category**: Product category +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +### PostgreSQL - Orders Table +- **id**: Unique identifier +- **user_id**: Reference to user +- **product_id**: Reference to product +- **quantity**: Order quantity +- **total_amount**: Total order amount +- **created_at**: Creation timestamp +- **updated_at**: Last update timestamp + +## Quick Start + +### 1. Using Docker Compose (Recommended) + +```bash +# Clone the repository +git clone +cd fastapi-mongo-mysql-postgresql-api + +# Start all services +docker-compose up -d + +# The API will be available at http://localhost:8000 +``` + +### 2. Manual Setup + +#### Prerequisites +- Python 3.11+ +- MongoDB +- MySQL +- PostgreSQL + +#### Installation + +```bash +# Clone the repository +git clone +cd fastapi-mongo-mysql-postgresql-api + +# Create virtual environment +python -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies +pip install -r requirements.txt + +# Copy environment configuration +cp .env.example .env +# Edit .env with your database credentials + +# Run the application +python main.py +``` + +## API Endpoints + +### Root Endpoints +- `GET /` - API information and available endpoints +- `GET /health` - Application health check +- `GET /docs` - Interactive API documentation (Swagger UI) +- `GET /redoc` - Alternative API documentation (ReDoc) + +### MongoDB Endpoints (Users) +- `GET /mongodb/health` - MongoDB health check +- `POST /mongodb/users` - Create a new user +- `GET /mongodb/users` - Get all users +- `GET /mongodb/users/{user_id}` - Get user by ID +- `PUT /mongodb/users/{user_id}` - Update user +- `DELETE /mongodb/users/{user_id}` - Delete user + +### MySQL Endpoints (Products) +- `GET /mysql/health` - MySQL health check +- `POST /mysql/products` - Create a new product +- `GET /mysql/products` - Get all products +- `GET /mysql/products/{product_id}` - Get product by ID +- `PUT /mysql/products/{product_id}` - Update product +- `DELETE /mysql/products/{product_id}` - Delete product + +### PostgreSQL Endpoints (Orders) +- `GET /postgresql/health` - PostgreSQL health check +- `POST /postgresql/orders` - Create a new order +- `GET /postgresql/orders` - Get all orders +- `GET /postgresql/orders/{order_id}` - Get order by ID +- `PUT /postgresql/orders/{order_id}` - Update order +- `DELETE /postgresql/orders/{order_id}` - Delete order + +## Usage Examples + +### Create a User (MongoDB) +```bash +curl -X POST "http://localhost:8000/mongodb/users" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "age": 30 + }' +``` + +### Create a Product (MySQL) +```bash +curl -X POST "http://localhost:8000/mysql/products" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "category": "Electronics" + }' +``` + +### Create an Order (PostgreSQL) +```bash +curl -X POST "http://localhost:8000/postgresql/orders" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "user-uuid-here", + "product_id": "product-uuid-here", + "quantity": 2, + "total_amount": 1999.98 + }' +``` + +## Configuration + +The application uses environment variables for configuration. Copy `.env.example` to `.env` and update the values: + +```env +# API Settings +APP_NAME=FastAPI Multi-Database API +DEBUG=true + +# MongoDB +MONGODB_URL=mongodb://localhost:27017 +MONGODB_DATABASE=fastapi_db + +# MySQL +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_USER=root +MYSQL_PASSWORD=password +MYSQL_DATABASE=fastapi_db + +# PostgreSQL +POSTGRESQL_HOST=localhost +POSTGRESQL_PORT=5432 +POSTGRESQL_USER=postgres +POSTGRESQL_PASSWORD=password +POSTGRESQL_DATABASE=fastapi_db +``` + +## Development + +### Running Tests +```bash +# Install test dependencies +pip install pytest pytest-asyncio httpx + +# Run tests +pytest +``` + +### Code Formatting +```bash +# Install formatting tools +pip install black isort + +# Format code +black . +isort . +``` + +## Deployment + +### Docker Deployment +```bash +# Build and run with Docker Compose +docker-compose up --build -d + +# View logs +docker-compose logs -f api + +# Stop services +docker-compose down +``` + +### Production Considerations +- Update database credentials and security settings +- Configure CORS settings appropriately +- Set up proper logging and monitoring +- Use environment-specific configuration files +- Implement authentication and authorization +- Set up database backups and monitoring + +## Contributing + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add some amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Support + +If you have any questions or issues, please open an issue on GitHub or contact the maintainers. 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/mongodb_routes.py b/app/api/mongodb_routes.py new file mode 100644 index 0000000..299872e --- /dev/null +++ b/app/api/mongodb_routes.py @@ -0,0 +1,152 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +import uuid +from datetime import datetime +from app.models.schemas import UserCreate, UserUpdate, UserResponse, HealthCheck +from app.db.mongodb import get_mongo_database + +router = APIRouter(prefix="/mongodb", tags=["MongoDB"]) + + +@router.get("/health", response_model=HealthCheck) +async def health_check(): + """Health check for MongoDB""" + try: + db = get_mongo_database() + await db.command("ping") + return HealthCheck( + status="healthy", + database="MongoDB", + timestamp=datetime.utcnow() + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"MongoDB health check failed: {str(e)}" + ) + + +@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED) +async def create_user(user: UserCreate): + """Create a new user in MongoDB""" + try: + db = get_mongo_database() + user_data = user.model_dump() + user_data["id"] = str(uuid.uuid4()) + user_data["created_at"] = datetime.utcnow() + user_data["updated_at"] = None + + result = await db.users.insert_one(user_data) + if result.inserted_id: + return UserResponse(**user_data) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to create user" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error creating user: {str(e)}" + ) + + +@router.get("/users", response_model=List[UserResponse]) +async def get_users(): + """Get all users from MongoDB""" + try: + db = get_mongo_database() + users = [] + async for user in db.users.find(): + user.pop("_id", None) # Remove MongoDB _id field + users.append(UserResponse(**user)) + return users + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching users: {str(e)}" + ) + + +@router.get("/users/{user_id}", response_model=UserResponse) +async def get_user(user_id: str): + """Get a specific user by ID from MongoDB""" + try: + db = get_mongo_database() + user = await db.users.find_one({"id": user_id}) + if user: + user.pop("_id", None) # Remove MongoDB _id field + return UserResponse(**user) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching user: {str(e)}" + ) + + +@router.put("/users/{user_id}", response_model=UserResponse) +async def update_user(user_id: str, user_update: UserUpdate): + """Update a user in MongoDB""" + try: + db = get_mongo_database() + update_data = {k: v for k, v in user_update.model_dump().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" + ) + + update_data["updated_at"] = datetime.utcnow() + + result = await db.users.update_one( + {"id": user_id}, + {"$set": update_data} + ) + + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + updated_user = await db.users.find_one({"id": user_id}) + updated_user.pop("_id", None) # Remove MongoDB _id field + return UserResponse(**updated_user) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error updating user: {str(e)}" + ) + + +@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_user(user_id: str): + """Delete a user from MongoDB""" + try: + db = get_mongo_database() + result = await db.users.delete_one({"id": user_id}) + + if result.deleted_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error deleting user: {str(e)}" + ) \ No newline at end of file diff --git a/app/api/mysql_routes.py b/app/api/mysql_routes.py new file mode 100644 index 0000000..cbd58d2 --- /dev/null +++ b/app/api/mysql_routes.py @@ -0,0 +1,205 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +import uuid +from datetime import datetime +from app.models.schemas import ProductCreate, ProductUpdate, ProductResponse, HealthCheck +from app.db.mysql import get_mysql_connection + +router = APIRouter(prefix="/mysql", tags=["MySQL"]) + + +@router.get("/health", response_model=HealthCheck) +async def health_check(): + """Health check for MySQL""" + try: + async with get_mysql_connection() as cursor: + await cursor.execute("SELECT 1") + await cursor.fetchone() + return HealthCheck( + status="healthy", + database="MySQL", + timestamp=datetime.utcnow() + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"MySQL health check failed: {str(e)}" + ) + + +@router.post("/products", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) +async def create_product(product: ProductCreate): + """Create a new product in MySQL""" + try: + product_id = str(uuid.uuid4()) + created_at = datetime.utcnow() + + async with get_mysql_connection() as cursor: + await cursor.execute(""" + INSERT INTO products (id, name, description, price, category, created_at, updated_at) + VALUES (%s, %s, %s, %s, %s, %s, %s) + """, (product_id, product.name, product.description, product.price, + product.category, created_at, created_at)) + + return ProductResponse( + id=product_id, + name=product.name, + description=product.description, + price=product.price, + category=product.category, + created_at=created_at, + updated_at=None + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error creating product: {str(e)}" + ) + + +@router.get("/products", response_model=List[ProductResponse]) +async def get_products(): + """Get all products from MySQL""" + try: + async with get_mysql_connection() as cursor: + await cursor.execute(""" + SELECT id, name, description, price, category, created_at, updated_at + FROM products + ORDER BY created_at DESC + """) + rows = await cursor.fetchall() + + products = [] + for row in rows: + products.append(ProductResponse( + id=row[0], + name=row[1], + description=row[2], + price=float(row[3]), + category=row[4], + created_at=row[5], + updated_at=row[6] + )) + return products + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching products: {str(e)}" + ) + + +@router.get("/products/{product_id}", response_model=ProductResponse) +async def get_product(product_id: str): + """Get a specific product by ID from MySQL""" + try: + async with get_mysql_connection() as cursor: + await cursor.execute(""" + SELECT id, name, description, price, category, created_at, updated_at + FROM products WHERE id = %s + """, (product_id,)) + row = await cursor.fetchone() + + if row: + return ProductResponse( + id=row[0], + name=row[1], + description=row[2], + price=float(row[3]), + category=row[4], + created_at=row[5], + updated_at=row[6] + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found" + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching product: {str(e)}" + ) + + +@router.put("/products/{product_id}", response_model=ProductResponse) +async def update_product(product_id: str, product_update: ProductUpdate): + """Update a product in MySQL""" + try: + update_fields = [] + values = [] + + # Build dynamic update query + for field, value in product_update.model_dump().items(): + if value is not None: + update_fields.append(f"{field} = %s") + values.append(value) + + if not update_fields: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No data provided for update" + ) + + # Add updated_at field + update_fields.append("updated_at = %s") + values.append(datetime.utcnow()) + values.append(product_id) + + async with get_mysql_connection() as cursor: + query = f"UPDATE products SET {', '.join(update_fields)} WHERE id = %s" + await cursor.execute(query, values) + + if cursor.rowcount == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found" + ) + + # Fetch updated product + await cursor.execute(""" + SELECT id, name, description, price, category, created_at, updated_at + FROM products WHERE id = %s + """, (product_id,)) + row = await cursor.fetchone() + + return ProductResponse( + id=row[0], + name=row[1], + description=row[2], + price=float(row[3]), + category=row[4], + created_at=row[5], + updated_at=row[6] + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error updating product: {str(e)}" + ) + + +@router.delete("/products/{product_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_product(product_id: str): + """Delete a product from MySQL""" + try: + async with get_mysql_connection() as cursor: + await cursor.execute("DELETE FROM products WHERE id = %s", (product_id,)) + + if cursor.rowcount == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Product not found" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error deleting product: {str(e)}" + ) \ No newline at end of file diff --git a/app/api/postgresql_routes.py b/app/api/postgresql_routes.py new file mode 100644 index 0000000..b2c15ab --- /dev/null +++ b/app/api/postgresql_routes.py @@ -0,0 +1,204 @@ +from fastapi import APIRouter, HTTPException, status +from typing import List +import uuid +from datetime import datetime +from app.models.schemas import OrderCreate, OrderUpdate, OrderResponse, HealthCheck +from app.db.postgresql import get_postgresql_connection + +router = APIRouter(prefix="/postgresql", tags=["PostgreSQL"]) + + +@router.get("/health", response_model=HealthCheck) +async def health_check(): + """Health check for PostgreSQL""" + try: + async with get_postgresql_connection() as connection: + await connection.execute("SELECT 1") + return HealthCheck( + status="healthy", + database="PostgreSQL", + timestamp=datetime.utcnow() + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=f"PostgreSQL health check failed: {str(e)}" + ) + + +@router.post("/orders", response_model=OrderResponse, status_code=status.HTTP_201_CREATED) +async def create_order(order: OrderCreate): + """Create a new order in PostgreSQL""" + try: + order_id = str(uuid.uuid4()) + created_at = datetime.utcnow() + + async with get_postgresql_connection() as connection: + await connection.execute(""" + INSERT INTO orders (id, user_id, product_id, quantity, total_amount, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7) + """, order_id, order.user_id, order.product_id, order.quantity, + order.total_amount, created_at, created_at) + + return OrderResponse( + id=order_id, + user_id=order.user_id, + product_id=order.product_id, + quantity=order.quantity, + total_amount=order.total_amount, + created_at=created_at, + updated_at=None + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error creating order: {str(e)}" + ) + + +@router.get("/orders", response_model=List[OrderResponse]) +async def get_orders(): + """Get all orders from PostgreSQL""" + try: + async with get_postgresql_connection() as connection: + rows = await connection.fetch(""" + SELECT id, user_id, product_id, quantity, total_amount, created_at, updated_at + FROM orders + ORDER BY created_at DESC + """) + + orders = [] + for row in rows: + orders.append(OrderResponse( + id=row['id'], + user_id=row['user_id'], + product_id=row['product_id'], + quantity=row['quantity'], + total_amount=float(row['total_amount']), + created_at=row['created_at'], + updated_at=row['updated_at'] + )) + return orders + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching orders: {str(e)}" + ) + + +@router.get("/orders/{order_id}", response_model=OrderResponse) +async def get_order(order_id: str): + """Get a specific order by ID from PostgreSQL""" + try: + async with get_postgresql_connection() as connection: + row = await connection.fetchrow(""" + SELECT id, user_id, product_id, quantity, total_amount, created_at, updated_at + FROM orders WHERE id = $1 + """, order_id) + + if row: + return OrderResponse( + id=row['id'], + user_id=row['user_id'], + product_id=row['product_id'], + quantity=row['quantity'], + total_amount=float(row['total_amount']), + created_at=row['created_at'], + updated_at=row['updated_at'] + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found" + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error fetching order: {str(e)}" + ) + + +@router.put("/orders/{order_id}", response_model=OrderResponse) +async def update_order(order_id: str, order_update: OrderUpdate): + """Update an order in PostgreSQL""" + try: + update_fields = [] + values = [] + param_count = 1 + + # Build dynamic update query + for field, value in order_update.model_dump().items(): + if value is not None: + update_fields.append(f"{field} = ${param_count}") + values.append(value) + param_count += 1 + + if not update_fields: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No data provided for update" + ) + + # Add updated_at field + update_fields.append(f"updated_at = ${param_count}") + values.append(datetime.utcnow()) + param_count += 1 + values.append(order_id) + + async with get_postgresql_connection() as connection: + query = f"UPDATE orders SET {', '.join(update_fields)} WHERE id = ${param_count}" + result = await connection.execute(query, *values) + + if result == "UPDATE 0": + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found" + ) + + # Fetch updated order + row = await connection.fetchrow(""" + SELECT id, user_id, product_id, quantity, total_amount, created_at, updated_at + FROM orders WHERE id = $1 + """, order_id) + + return OrderResponse( + id=row['id'], + user_id=row['user_id'], + product_id=row['product_id'], + quantity=row['quantity'], + total_amount=float(row['total_amount']), + created_at=row['created_at'], + updated_at=row['updated_at'] + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error updating order: {str(e)}" + ) + + +@router.delete("/orders/{order_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_order(order_id: str): + """Delete an order from PostgreSQL""" + try: + async with get_postgresql_connection() as connection: + result = await connection.execute("DELETE FROM orders WHERE id = $1", order_id) + + if result == "DELETE 0": + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Order not found" + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Error deleting order: {str(e)}" + ) \ 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..5c5acdd --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,40 @@ +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class Settings(BaseSettings): + # API Settings + app_name: str = "FastAPI Multi-Database API" + app_version: str = "1.0.0" + debug: bool = True + + # MongoDB Settings + mongodb_url: str = "mongodb://localhost:27017" + mongodb_database: str = "fastapi_db" + + # MySQL Settings + mysql_host: str = "localhost" + mysql_port: int = 3306 + mysql_user: str = "root" + mysql_password: str = "password" + mysql_database: str = "fastapi_db" + + # PostgreSQL Settings + postgresql_host: str = "localhost" + postgresql_port: int = 5432 + postgresql_user: str = "postgres" + postgresql_password: str = "password" + postgresql_database: str = "fastapi_db" + + # Security + secret_key: str = "your-secret-key-change-this-in-production" + algorithm: str = "HS256" + access_token_expire_minutes: int = 30 + + class Config: + env_file = ".env" + + +@lru_cache() +def get_settings(): + return Settings() \ No newline at end of file diff --git a/app/db/__init__.py b/app/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/db/mongodb.py b/app/db/mongodb.py new file mode 100644 index 0000000..34a461d --- /dev/null +++ b/app/db/mongodb.py @@ -0,0 +1,28 @@ +from motor.motor_asyncio import AsyncIOMotorClient +from app.core.config import get_settings + +settings = get_settings() + + +class MongoDB: + client: AsyncIOMotorClient = None + database = None + + +mongodb = MongoDB() + + +async def connect_to_mongo(): + """Create database connection""" + mongodb.client = AsyncIOMotorClient(settings.mongodb_url) + mongodb.database = mongodb.client[settings.mongodb_database] + + +async def close_mongo_connection(): + """Close database connection""" + if mongodb.client: + mongodb.client.close() + + +def get_mongo_database(): + return mongodb.database \ No newline at end of file diff --git a/app/db/mysql.py b/app/db/mysql.py new file mode 100644 index 0000000..a8beb0d --- /dev/null +++ b/app/db/mysql.py @@ -0,0 +1,88 @@ +import aiomysql +from contextlib import asynccontextmanager +from app.core.config import get_settings + +settings = get_settings() + + +class MySQL: + pool = None + + +mysql = MySQL() + + +async def connect_to_mysql(): + """Create MySQL connection pool""" + mysql.pool = await aiomysql.create_pool( + host=settings.mysql_host, + port=settings.mysql_port, + user=settings.mysql_user, + password=settings.mysql_password, + db=settings.mysql_database, + minsize=1, + maxsize=10, + autocommit=True + ) + + +async def close_mysql_connection(): + """Close MySQL connection pool""" + if mysql.pool: + mysql.pool.close() + await mysql.pool.wait_closed() + + +@asynccontextmanager +async def get_mysql_connection(): + """Get MySQL connection from pool""" + if not mysql.pool: + raise Exception("MySQL connection pool is not initialized") + + async with mysql.pool.acquire() as connection: + async with connection.cursor() as cursor: + yield cursor + + +async def create_mysql_tables(): + """Create MySQL tables if they don't exist""" + async with get_mysql_connection() as cursor: + # Users table + await cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + age INT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """) + + # Products table + await cursor.execute(""" + CREATE TABLE IF NOT EXISTS products ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + price DECIMAL(10, 2) NOT NULL, + category VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + """) + + # Orders table + await cursor.execute(""" + CREATE TABLE IF NOT EXISTS orders ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + product_id VARCHAR(36) NOT NULL, + quantity INT NOT NULL, + total_amount DECIMAL(10, 2) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE + ) + """) \ No newline at end of file diff --git a/app/db/postgresql.py b/app/db/postgresql.py new file mode 100644 index 0000000..caaf8e3 --- /dev/null +++ b/app/db/postgresql.py @@ -0,0 +1,105 @@ +import asyncpg +from contextlib import asynccontextmanager +from app.core.config import get_settings + +settings = get_settings() + + +class PostgreSQL: + pool = None + + +postgresql = PostgreSQL() + + +async def connect_to_postgresql(): + """Create PostgreSQL connection pool""" + postgresql.pool = await asyncpg.create_pool( + host=settings.postgresql_host, + port=settings.postgresql_port, + user=settings.postgresql_user, + password=settings.postgresql_password, + database=settings.postgresql_database, + min_size=1, + max_size=10 + ) + + +async def close_postgresql_connection(): + """Close PostgreSQL connection pool""" + if postgresql.pool: + await postgresql.pool.close() + + +@asynccontextmanager +async def get_postgresql_connection(): + """Get PostgreSQL connection from pool""" + if not postgresql.pool: + raise Exception("PostgreSQL connection pool is not initialized") + + async with postgresql.pool.acquire() as connection: + yield connection + + +async def create_postgresql_tables(): + """Create PostgreSQL tables if they don't exist""" + async with get_postgresql_connection() as connection: + # Users table + await connection.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + age INTEGER CHECK (age >= 0 AND age <= 150), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Products table + await connection.execute(""" + CREATE TABLE IF NOT EXISTS products ( + id VARCHAR(36) PRIMARY KEY, + name VARCHAR(200) NOT NULL, + description TEXT, + price DECIMAL(10, 2) NOT NULL CHECK (price > 0), + category VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + """) + + # Orders table + await connection.execute(""" + CREATE TABLE IF NOT EXISTS orders ( + id VARCHAR(36) PRIMARY KEY, + user_id VARCHAR(36) NOT NULL, + product_id VARCHAR(36) NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + total_amount DECIMAL(10, 2) NOT NULL CHECK (total_amount > 0), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE + ) + """) + + # Create updated_at trigger function + await connection.execute(""" + CREATE OR REPLACE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + $$ language 'plpgsql'; + """) + + # Create triggers for updated_at + for table in ['users', 'products', 'orders']: + await connection.execute(f""" + DROP TRIGGER IF EXISTS update_{table}_updated_at ON {table}; + CREATE TRIGGER update_{table}_updated_at + BEFORE UPDATE ON {table} + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + """) \ 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/schemas.py b/app/models/schemas.py new file mode 100644 index 0000000..01b4819 --- /dev/null +++ b/app/models/schemas.py @@ -0,0 +1,86 @@ +from typing import Optional +from pydantic import BaseModel, Field +from datetime import datetime + + +class UserBase(BaseModel): + name: str = Field(..., min_length=1, max_length=100) + email: str = Field(..., pattern=r'^[^@]+@[^@]+\.[^@]+$') + age: Optional[int] = Field(None, ge=0, le=150) + + +class UserCreate(UserBase): + pass + + +class UserUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=100) + email: Optional[str] = Field(None, pattern=r'^[^@]+@[^@]+\.[^@]+$') + age: Optional[int] = Field(None, ge=0, le=150) + + +class UserResponse(UserBase): + id: str + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class ProductBase(BaseModel): + name: str = Field(..., min_length=1, max_length=200) + description: Optional[str] = Field(None, max_length=1000) + price: float = Field(..., gt=0) + category: str = Field(..., min_length=1, max_length=100) + + +class ProductCreate(ProductBase): + pass + + +class ProductUpdate(BaseModel): + name: Optional[str] = Field(None, min_length=1, max_length=200) + description: Optional[str] = Field(None, max_length=1000) + price: Optional[float] = Field(None, gt=0) + category: Optional[str] = Field(None, min_length=1, max_length=100) + + +class ProductResponse(ProductBase): + id: str + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class OrderBase(BaseModel): + user_id: str + product_id: str + quantity: int = Field(..., gt=0) + total_amount: float = Field(..., gt=0) + + +class OrderCreate(OrderBase): + pass + + +class OrderUpdate(BaseModel): + quantity: Optional[int] = Field(None, gt=0) + total_amount: Optional[float] = Field(None, gt=0) + + +class OrderResponse(OrderBase): + id: str + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class HealthCheck(BaseModel): + status: str + database: str + timestamp: datetime \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..507d833 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,74 @@ +version: '3.8' + +services: + # FastAPI Application + api: + build: . + ports: + - "8000:8000" + environment: + - MONGODB_URL=mongodb://mongodb:27017 + - MYSQL_HOST=mysql + - MYSQL_USER=root + - MYSQL_PASSWORD=password + - MYSQL_DATABASE=fastapi_db + - POSTGRESQL_HOST=postgresql + - POSTGRESQL_USER=postgres + - POSTGRESQL_PASSWORD=password + - POSTGRESQL_DATABASE=fastapi_db + depends_on: + - mongodb + - mysql + - postgresql + volumes: + - .:/app + networks: + - app-network + + # MongoDB + mongodb: + image: mongo:7 + ports: + - "27017:27017" + environment: + - MONGO_INITDB_DATABASE=fastapi_db + volumes: + - mongodb_data:/data/db + networks: + - app-network + + # MySQL + mysql: + image: mysql:8.0 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=password + - MYSQL_DATABASE=fastapi_db + volumes: + - mysql_data:/var/lib/mysql + networks: + - app-network + + # PostgreSQL + postgresql: + image: postgres:15 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=fastapi_db + volumes: + - postgresql_data:/var/lib/postgresql/data + networks: + - app-network + +volumes: + mongodb_data: + mysql_data: + postgresql_data: + +networks: + app-network: + driver: bridge \ No newline at end of file diff --git a/examples/API_EXAMPLES.md b/examples/API_EXAMPLES.md new file mode 100644 index 0000000..4cd1cdf --- /dev/null +++ b/examples/API_EXAMPLES.md @@ -0,0 +1,200 @@ +# API Examples + +Here are practical examples for using the FastAPI multi-database template: + +## Quick Test Commands + +### 1. Health Checks +```bash +# Application health +curl http://localhost:8000/health + +# Database health checks +curl http://localhost:8000/mongodb/health +curl http://localhost:8000/mysql/health +curl http://localhost:8000/postgresql/health +``` + +### 2. MongoDB Operations (Users) + +```bash +# Create a user +curl -X POST "http://localhost:8000/mongodb/users" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Doe", + "email": "john@example.com", + "age": 30 + }' + +# Get all users +curl http://localhost:8000/mongodb/users + +# Get specific user +curl http://localhost:8000/mongodb/users/{user_id} + +# Update user +curl -X PUT "http://localhost:8000/mongodb/users/{user_id}" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "John Smith", + "age": 31 + }' + +# Delete user +curl -X DELETE http://localhost:8000/mongodb/users/{user_id} +``` + +### 3. MySQL Operations (Products) + +```bash +# Create a product +curl -X POST "http://localhost:8000/mysql/products" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "category": "Electronics" + }' + +# Get all products +curl http://localhost:8000/mysql/products + +# Get specific product +curl http://localhost:8000/mysql/products/{product_id} + +# Update product +curl -X PUT "http://localhost:8000/mysql/products/{product_id}" \ + -H "Content-Type: application/json" \ + -d '{ + "price": 899.99, + "description": "Discounted laptop" + }' + +# Delete product +curl -X DELETE http://localhost:8000/mysql/products/{product_id} +``` + +### 4. PostgreSQL Operations (Orders) + +```bash +# Create an order +curl -X POST "http://localhost:8000/postgresql/orders" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "user-uuid-here", + "product_id": "product-uuid-here", + "quantity": 2, + "total_amount": 1999.98 + }' + +# Get all orders +curl http://localhost:8000/postgresql/orders + +# Get specific order +curl http://localhost:8000/postgresql/orders/{order_id} + +# Update order +curl -X PUT "http://localhost:8000/postgresql/orders/{order_id}" \ + -H "Content-Type: application/json" \ + -d '{ + "quantity": 3, + "total_amount": 2999.97 + }' + +# Delete order +curl -X DELETE http://localhost:8000/postgresql/orders/{order_id} +``` + +## Python Client Example + +```python +import httpx +import asyncio + +async def example_usage(): + async with httpx.AsyncClient() as client: + # Create a user + user_response = await client.post( + "http://localhost:8000/mongodb/users", + json={ + "name": "Alice Smith", + "email": "alice@example.com", + "age": 28 + } + ) + user = user_response.json() + print(f"Created user: {user['id']}") + + # Create a product + product_response = await client.post( + "http://localhost:8000/mysql/products", + json={ + "name": "Smartphone", + "description": "Latest smartphone", + "price": 699.99, + "category": "Electronics" + } + ) + product = product_response.json() + print(f"Created product: {product['id']}") + + # Create an order + order_response = await client.post( + "http://localhost:8000/postgresql/orders", + json={ + "user_id": user['id'], + "product_id": product['id'], + "quantity": 1, + "total_amount": 699.99 + } + ) + order = order_response.json() + print(f"Created order: {order['id']}") + +if __name__ == "__main__": + asyncio.run(example_usage()) +``` + +## JavaScript/TypeScript Example + +```javascript +// Using fetch API +async function createUser() { + const response = await fetch('http://localhost:8000/mongodb/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'Bob Johnson', + email: 'bob@example.com', + age: 35 + }) + }); + + const user = await response.json(); + console.log('Created user:', user); + return user; +} + +async function createProduct() { + const response = await fetch('http://localhost:8000/mysql/products', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: 'Tablet', + description: 'Portable tablet device', + price: 399.99, + category: 'Electronics' + }) + }); + + const product = await response.json(); + console.log('Created product:', product); + return product; +} +``` \ No newline at end of file diff --git a/examples/api_demo.py b/examples/api_demo.py new file mode 100644 index 0000000..4eccddf --- /dev/null +++ b/examples/api_demo.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +Example usage of the FastAPI multi-database template + +This script demonstrates how the template can be used by making API calls +to show the different database operations. +""" + +import json +import asyncio +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from datetime import datetime + + +async def demo_api_calls(): + """Demonstrate API calls to the FastAPI template""" + + print("FastAPI Multi-Database Template - API Demo") + print("=" * 50) + + try: + import httpx + base_url = "http://localhost:8000" + + async with httpx.AsyncClient() as client: + # Test root endpoint + print("\n1. Testing root endpoint...") + response = await client.get(f"{base_url}/") + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}") + + # Test health endpoint + print("\n2. Testing health endpoint...") + response = await client.get(f"{base_url}/health") + print(f"Status: {response.status_code}") + print(f"Response: {json.dumps(response.json(), indent=2)}") + + except ImportError: + print("\n⚠️ httpx not installed. Install with: pip install httpx") + except Exception: + print("\n⚠️ API server is not running.") + print("To start the server:") + print("1. Install dependencies: pip install -r requirements.txt") + print("2. Start databases: docker-compose up -d") + print("3. Run server: python main.py") + print("4. Then run this demo again: python examples/api_demo.py") + + # Show API examples even if server is not running + print("\n3. Example API calls (when server is running):") + + # Example MongoDB user creation + user_data = { + "name": "John Doe", + "email": "john@example.com", + "age": 30 + } + print(f"\nCreate user (MongoDB): POST /mongodb/users") + print(f"Data: {json.dumps(user_data, indent=2)}") + + # Example MySQL product creation + product_data = { + "name": "Laptop", + "description": "High-performance laptop", + "price": 999.99, + "category": "Electronics" + } + print(f"\nCreate product (MySQL): POST /mysql/products") + print(f"Data: {json.dumps(product_data, indent=2)}") + + # Example PostgreSQL order creation + order_data = { + "user_id": "user-uuid-here", + "product_id": "product-uuid-here", + "quantity": 2, + "total_amount": 1999.98 + } + print(f"\nCreate order (PostgreSQL): POST /postgresql/orders") + print(f"Data: {json.dumps(order_data, indent=2)}") + + +def demo_without_server(): + """Show examples without requiring a running server""" + print("\nFastAPI Multi-Database Template - Code Examples") + print("=" * 50) + + # Show example model usage + print("\n1. Pydantic Model Examples:") + try: + from app.models.schemas import UserCreate, ProductCreate, OrderCreate + + user = UserCreate(name="Alice Smith", email="alice@example.com", age=28) + print(f"✓ User model: {user.model_dump()}") + + product = ProductCreate( + name="Smartphone", + description="Latest smartphone", + price=699.99, + category="Electronics" + ) + print(f"✓ Product model: {product.model_dump()}") + + order = OrderCreate( + user_id="123e4567-e89b-12d3-a456-426614174000", + product_id="123e4567-e89b-12d3-a456-426614174001", + quantity=1, + total_amount=699.99 + ) + print(f"✓ Order model: {order.model_dump()}") + + except ImportError as e: + print(f"✗ Import error: {e}") + + # Show configuration example + print("\n2. Configuration Example:") + try: + from app.core.config import get_settings + settings = get_settings() + print(f"✓ App name: {settings.app_name}") + print(f"✓ MongoDB URL: {settings.mongodb_url}") + print(f"✓ MySQL host: {settings.mysql_host}") + print(f"✓ PostgreSQL host: {settings.postgresql_host}") + + except ImportError as e: + print(f"✗ Import error: {e}") + + +async def main(): + """Main demo function""" + + # Try API demo first + await demo_api_calls() + + # Always show code examples + demo_without_server() + + print("\n" + "=" * 50) + print("Template Demo Complete!") + print("\nNext steps:") + print("1. Start databases: docker-compose up -d") + print("2. Run the API: python main.py") + print("3. Visit: http://localhost:8000/docs") + print("4. Try the interactive API documentation!") + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..70a4188 --- /dev/null +++ b/main.py @@ -0,0 +1,136 @@ +from fastapi import FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from contextlib import asynccontextmanager +import logging + +from app.core.config import get_settings +from app.db.mongodb import connect_to_mongo, close_mongo_connection +from app.db.mysql import connect_to_mysql, close_mysql_connection, create_mysql_tables +from app.db.postgresql import connect_to_postgresql, close_postgresql_connection, create_postgresql_tables +from app.api import mongodb_routes, mysql_routes, postgresql_routes + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +settings = get_settings() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application startup and shutdown event handler""" + try: + # Startup + logger.info("Starting up FastAPI application...") + + # Connect to databases + logger.info("Connecting to MongoDB...") + await connect_to_mongo() + + logger.info("Connecting to MySQL...") + await connect_to_mysql() + await create_mysql_tables() + + logger.info("Connecting to PostgreSQL...") + await connect_to_postgresql() + await create_postgresql_tables() + + logger.info("All databases connected successfully!") + + yield + + except Exception as e: + logger.error(f"Error during startup: {e}") + raise + finally: + # Shutdown + logger.info("Shutting down FastAPI application...") + + logger.info("Closing MongoDB connection...") + await close_mongo_connection() + + logger.info("Closing MySQL connection...") + await close_mysql_connection() + + logger.info("Closing PostgreSQL connection...") + await close_postgresql_connection() + + logger.info("All database connections closed!") + + +# Create FastAPI application +app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description="A comprehensive FastAPI template supporting MongoDB, MySQL, and PostgreSQL databases", + lifespan=lifespan +) + +# Add CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # Configure this properly for production + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/") +async def root(): + """Root endpoint with API information""" + return { + "message": "Welcome to FastAPI Multi-Database API", + "version": settings.app_version, + "databases": ["MongoDB", "MySQL", "PostgreSQL"], + "endpoints": { + "MongoDB Users": "/mongodb/users", + "MySQL Products": "/mysql/products", + "PostgreSQL Orders": "/postgresql/orders" + }, + "health_checks": { + "MongoDB": "/mongodb/health", + "MySQL": "/mysql/health", + "PostgreSQL": "/postgresql/health" + }, + "docs": "/docs", + "redoc": "/redoc" + } + + +@app.get("/health") +async def health(): + """Overall health check endpoint""" + return { + "status": "healthy", + "application": settings.app_name, + "version": settings.app_version + } + + +# Include routers +app.include_router(mongodb_routes.router) +app.include_router(mysql_routes.router) +app.include_router(postgresql_routes.router) + + +# Error handlers +@app.exception_handler(Exception) +async def global_exception_handler(request, exc): + """Global exception handler""" + logger.error(f"Global exception: {exc}") + return HTTPException( + status_code=500, + detail="Internal server error" + ) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=True, + log_level="info" + ) \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..ebe2bbb --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,16 @@ +# Development dependencies +-r requirements.txt + +# Testing +pytest==7.4.3 +pytest-asyncio==0.21.1 +httpx==0.25.2 + +# Code formatting and linting +black==23.11.0 +isort==5.12.0 +flake8==6.1.0 +mypy==1.7.1 + +# Development tools +pre-commit==3.5.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3baebf7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,18 @@ +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Database drivers +pymongo==4.6.0 +motor==3.3.2 # Async MongoDB driver +mysql-connector-python==8.2.0 +aiomysql==0.2.0 # Async MySQL driver +psycopg2-binary==2.9.9 +asyncpg==0.29.0 # Async PostgreSQL driver + +# Additional utilities +python-multipart==0.0.6 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/test_basic.py b/test_basic.py new file mode 100644 index 0000000..acddca3 --- /dev/null +++ b/test_basic.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +""" +Simple test script to validate core FastAPI functionality without database dependencies +""" + +import sys + + +def test_basic_functionality(): + """Test basic FastAPI application without database connections""" + try: + print("Testing basic FastAPI functionality...") + + # Test core imports + from app.core.config import get_settings + from app.models.schemas import UserCreate, ProductCreate, OrderCreate + + settings = get_settings() + print(f"✓ Application: {settings.app_name}") + + # Test model creation + user = UserCreate(name="Test User", email="test@example.com", age=25) + product = ProductCreate(name="Test Product", price=99.99, category="Test") + order = OrderCreate(user_id="123", product_id="456", quantity=1, total_amount=99.99) + + print("✓ Pydantic models work correctly") + + # Test FastAPI app creation (basic version) + from fastapi import FastAPI + app = FastAPI(title=settings.app_name, version=settings.app_version) + + @app.get("/") + def read_root(): + return {"message": "FastAPI template is working"} + + @app.get("/health") + def health(): + return {"status": "healthy"} + + print("✓ FastAPI application can be created") + print("✓ Basic template structure is valid") + + return True + + except Exception as e: + print(f"✗ Basic functionality test failed: {e}") + return False + + +def main(): + """Run basic validation""" + print("FastAPI Multi-Database Template - Basic Validation") + print("=" * 55) + + if test_basic_functionality(): + print("\n✓ Core template structure is valid!") + print("✓ Install database dependencies to use full functionality") + print("\nTo install all dependencies:") + print(" pip install -r requirements.txt") + print("\nTo run the full application:") + print(" python main.py") + return 0 + else: + print("\n✗ Basic validation failed") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/test_structure.py b/test_structure.py new file mode 100644 index 0000000..4347236 --- /dev/null +++ b/test_structure.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Test script to validate the FastAPI application structure +This script tests the application without requiring actual database connections +""" + +import sys +import traceback + + +def test_imports(): + """Test all critical imports""" + try: + print("Testing imports...") + + # Test core imports + from app.core.config import get_settings + print("✓ Core config import successful") + + # Test model imports + from app.models.schemas import UserCreate, ProductCreate, OrderCreate, HealthCheck + print("✓ Schema imports successful") + + # Test database modules (without connecting) + from app.db import mongodb, mysql, postgresql + print("✓ Database module imports successful") + + # Test API routes + from app.api import mongodb_routes, mysql_routes, postgresql_routes + print("✓ API route imports successful") + + # Test main application + from main import app + print("✓ Main application import successful") + + return True + + except Exception as e: + print(f"✗ Import failed: {e}") + traceback.print_exc() + return False + + +def test_config(): + """Test configuration""" + try: + print("\nTesting configuration...") + from app.core.config import get_settings + + settings = get_settings() + assert settings.app_name + assert settings.mongodb_url + assert settings.mysql_host + assert settings.postgresql_host + + print("✓ Configuration validation successful") + return True + + except Exception as e: + print(f"✗ Configuration test failed: {e}") + return False + + +def test_models(): + """Test Pydantic models""" + try: + print("\nTesting Pydantic models...") + from app.models.schemas import UserCreate, ProductCreate, OrderCreate + + # Test UserCreate + user = UserCreate(name="Test User", email="test@example.com", age=25) + assert user.name == "Test User" + assert user.email == "test@example.com" + assert user.age == 25 + + # Test ProductCreate + product = ProductCreate( + name="Test Product", + description="A test product", + price=99.99, + category="Test" + ) + assert product.name == "Test Product" + assert product.price == 99.99 + + # Test OrderCreate + order = OrderCreate( + user_id="user-123", + product_id="product-456", + quantity=2, + total_amount=199.98 + ) + assert order.quantity == 2 + assert order.total_amount == 199.98 + + print("✓ Pydantic model validation successful") + return True + + except Exception as e: + print(f"✗ Model test failed: {e}") + return False + + +def test_fastapi_app(): + """Test FastAPI application creation""" + try: + print("\nTesting FastAPI application...") + from main import app + + # Check that app is created + assert app is not None + assert hasattr(app, 'routes') + + # Check that routers are included + routes = [route.path for route in app.routes] + + # Should have root route + assert "/" in routes + assert "/health" in routes + + print("✓ FastAPI application structure validation successful") + return True + + except Exception as e: + print(f"✗ FastAPI application test failed: {e}") + return False + + +def main(): + """Run all tests""" + print("FastAPI Multi-Database Template - Structure Validation") + print("=" * 60) + + tests = [ + test_imports, + test_config, + test_models, + test_fastapi_app + ] + + passed = 0 + failed = 0 + + for test in tests: + try: + if test(): + passed += 1 + else: + failed += 1 + except Exception as e: + print(f"✗ Test {test.__name__} failed with exception: {e}") + failed += 1 + + print("\n" + "=" * 60) + print(f"Tests completed: {passed} passed, {failed} failed") + + if failed == 0: + print("✓ All structure validation tests passed!") + print("✓ FastAPI template is ready for use!") + return 0 + else: + print("✗ Some tests failed. Please check the issues above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file