Create e-commerce API with FastAPI and SQLite

This commit is contained in:
Automated Action 2025-06-04 22:37:35 +00:00
parent e82b19985e
commit 65cfb5b050
43 changed files with 2115 additions and 2 deletions

188
README.md
View File

@ -1,3 +1,187 @@
# FastAPI Application
# E-Commerce API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A FastAPI-based RESTful API for an e-commerce application with user authentication, product catalog, shopping cart, order processing, and payment processing.
## Features
- User authentication with JWT
- Product catalog with categories
- Shopping cart functionality
- Order management
- Mock payment processing
- Search and filtering products
- Admin and regular user roles
## Technology Stack
- **Framework**: FastAPI
- **Database**: SQLite
- **ORM**: SQLAlchemy
- **Migration Tool**: Alembic
- **Authentication**: JWT with password hashing
## Project Structure
```
ecommerce-api/
├── app/
│ ├── api/
│ │ ├── deps.py
│ │ └── v1/
│ │ ├── api.py
│ │ └── endpoints/
│ │ ├── auth.py
│ │ ├── cart.py
│ │ ├── categories.py
│ │ ├── health.py
│ │ ├── orders.py
│ │ ├── payments.py
│ │ ├── products.py
│ │ └── users.py
│ ├── core/
│ │ ├── config.py
│ │ └── security.py
│ ├── db/
│ │ └── session.py
│ ├── models/
│ │ ├── base.py
│ │ ├── cart.py
│ │ ├── order.py
│ │ ├── product.py
│ │ └── user.py
│ ├── schemas/
│ │ ├── cart.py
│ │ ├── order.py
│ │ ├── product.py
│ │ ├── token.py
│ │ └── user.py
│ └── services/
├── migrations/
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│ └── 0001_create_tables.py
├── storage/
│ └── db/
├── alembic.ini
├── main.py
└── requirements.txt
```
## Setup and Installation
### 1. Clone the repository
```bash
git clone https://github.com/your-username/ecommerce-api.git
cd ecommerce-api
```
### 2. Create a virtual environment
```bash
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
### 3. Install dependencies
```bash
pip install -r requirements.txt
```
### 4. Set up environment variables
Create a `.env` file in the root directory:
```
JWT_SECRET_KEY=your_secret_key_here
```
### 5. Initialize the database
```bash
alembic upgrade head
```
### 6. Run the application
```bash
uvicorn main:app --reload
```
The API will be available at http://localhost:8000.
## API Documentation
Once the application is running, you can access the API documentation at:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## API Endpoints
### Authentication
- `POST /api/v1/auth/register` - Register a new user
- `POST /api/v1/auth/login` - Login and get access token
### Users
- `GET /api/v1/users/me` - Get current user information
- `PUT /api/v1/users/me` - Update current user information
- `GET /api/v1/users/{user_id}` - Get user by ID (admin only)
- `GET /api/v1/users/` - List all users (admin only)
### Categories
- `GET /api/v1/categories/` - List all categories
- `POST /api/v1/categories/` - Create a new category (admin only)
- `GET /api/v1/categories/{category_id}` - Get category by ID
- `PUT /api/v1/categories/{category_id}` - Update a category (admin only)
- `DELETE /api/v1/categories/{category_id}` - Delete a category (admin only)
### Products
- `GET /api/v1/products/` - List all products (with filtering options)
- `POST /api/v1/products/` - Create a new product (admin only)
- `GET /api/v1/products/{product_id}` - Get product by ID
- `PUT /api/v1/products/{product_id}` - Update a product (admin only)
- `DELETE /api/v1/products/{product_id}` - Delete a product (admin only)
### Cart
- `GET /api/v1/cart/` - Get user's cart
- `POST /api/v1/cart/items` - Add item to cart
- `PUT /api/v1/cart/items/{item_id}` - Update cart item quantity
- `DELETE /api/v1/cart/items/{item_id}` - Remove item from cart
- `DELETE /api/v1/cart/` - Clear cart
### Orders
- `GET /api/v1/orders/` - List user's orders (admin can see all)
- `POST /api/v1/orders/` - Create a new order from cart
- `GET /api/v1/orders/{order_id}` - Get order by ID
- `PUT /api/v1/orders/{order_id}` - Update order (limited for regular users)
- `DELETE /api/v1/orders/{order_id}` - Cancel order
### Payments
- `POST /api/v1/payments/` - Process payment for an order
### Health Check
- `GET /health` - Application health check
- `GET /api/v1/health/` - Detailed health check
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| JWT_SECRET_KEY | Secret key for JWT token generation | supersecretkey |
| JWT_ALGORITHM | Algorithm used for JWT | HS256 |
| ACCESS_TOKEN_EXPIRE_MINUTES | Token expiration time in minutes | 30 |
## Database
The application uses SQLite as the database. The database file is created at `/app/storage/db/db.sqlite`.

85
alembic.ini Normal file
View File

@ -0,0 +1,85 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = migrations
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# timezone to use when rendering the date
# within the migration file as well as the filename.
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; this defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path
# version_locations = %(here)s/bar %(here)s/bat migrations/versions
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# SQLite URL
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks=black
# black.type=console_scripts
# black.entrypoint=black
# black.options=-l 79
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

1
app/__init__.py Normal file
View File

@ -0,0 +1 @@
# This file marks the app directory as a Python package

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

@ -0,0 +1 @@
# This file marks the api directory as a Python package

63
app/api/deps.py Normal file
View File

@ -0,0 +1,63 @@
from fastapi import Depends, HTTPException, status
from jose import jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import oauth2_scheme
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import TokenPayload
def get_current_user(
db: Session = Depends(get_db), token: str = Depends(oauth2_scheme)
) -> User:
"""
Get current user from token.
"""
try:
payload = jwt.decode(
token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
)
token_data = TokenPayload(**payload)
except (jwt.JWTError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
user = db.query(User).filter(User.id == token_data.sub).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
if not user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
"""
Get current active user.
"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_admin(
current_user: User = Depends(get_current_user),
) -> User:
"""
Get current active admin user.
"""
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
if not current_user.is_admin:
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
return current_user

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

@ -0,0 +1 @@
# This file marks the v1 directory as a Python package

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

@ -0,0 +1,16 @@
from fastapi import APIRouter
from app.api.v1.endpoints import auth, cart, categories, health, orders, payments, products, users
from app.core.config import settings
api_router = APIRouter(prefix=settings.API_V1_STR)
# Include routers for different modules
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(categories.router, prefix="/categories", tags=["categories"])
api_router.include_router(products.router, prefix="/products", tags=["products"])
api_router.include_router(cart.router, prefix="/cart", tags=["cart"])
api_router.include_router(orders.router, prefix="/orders", tags=["orders"])
api_router.include_router(payments.router, prefix="/payments", tags=["payments"])
api_router.include_router(health.router, prefix="/health", tags=["health"])

View File

@ -0,0 +1 @@
# This file marks the endpoints directory as a Python package

View File

@ -0,0 +1,84 @@
from datetime import timedelta
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import create_access_token, get_password_hash, verify_password
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import Token
from app.schemas.user import UserCreate
router = APIRouter()
@router.post("/login", response_model=Token)
def login_access_token(
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
) -> Any:
"""
OAuth2 compatible token login, get an access token for future requests.
"""
user = db.query(User).filter(User.email == form_data.username).first()
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.post("/register", response_model=Token)
def register_user(
*, db: Session = Depends(get_db), user_in: UserCreate
) -> Any:
"""
Register a new user and return an access token.
"""
# Check if user already exists
user = db.query(User).filter(User.email == user_in.email).first()
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this email already exists",
)
# Create new user
db_user = User(
email=user_in.email,
hashed_password=get_password_hash(user_in.password),
full_name=user_in.full_name,
phone=user_in.phone,
address=user_in.address,
is_active=user_in.is_active,
is_admin=user_in.is_admin,
)
db.add(db_user)
db.commit()
db.refresh(db_user)
# Create access token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": create_access_token(
db_user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}

View File

@ -0,0 +1,172 @@
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.db.session import get_db
from app.models.cart import CartItem
from app.models.product import Product
from app.models.user import User
from app.schemas.cart import Cart, CartItem as CartItemSchema
from app.schemas.cart import CartItemCreate, CartItemUpdate
router = APIRouter()
@router.get("/", response_model=Cart)
def read_cart(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Retrieve current user's cart.
"""
cart_items = db.query(CartItem).filter(CartItem.user_id == current_user.id).all()
# Calculate total
total = 0.0
for item in cart_items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product:
total += product.price * item.quantity
return {
"items": cart_items,
"total": total,
}
@router.post("/items", response_model=CartItemSchema)
def add_to_cart(
*,
db: Session = Depends(get_db),
item_in: CartItemCreate,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Add item to cart.
"""
# Check if product exists and is active
product = db.query(Product).filter(Product.id == item_in.product_id).first()
if not product or not product.is_active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
# Check if product has enough stock
if product.stock_quantity < item_in.quantity:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Not enough stock available",
)
# Check if item is already in cart
cart_item = db.query(CartItem).filter(
CartItem.user_id == current_user.id,
CartItem.product_id == item_in.product_id,
).first()
if cart_item:
# Update quantity
cart_item.quantity += item_in.quantity
db.add(cart_item)
db.commit()
db.refresh(cart_item)
return cart_item
# Create new cart item
cart_item = CartItem(
user_id=current_user.id,
product_id=item_in.product_id,
quantity=item_in.quantity,
)
db.add(cart_item)
db.commit()
db.refresh(cart_item)
return cart_item
@router.put("/items/{item_id}", response_model=CartItemSchema)
def update_cart_item(
*,
db: Session = Depends(get_db),
item_id: str,
item_in: CartItemUpdate,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Update cart item quantity.
"""
cart_item = db.query(CartItem).filter(
CartItem.id == item_id,
CartItem.user_id == current_user.id,
).first()
if not cart_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cart item not found",
)
# Check if product has enough stock
product = db.query(Product).filter(Product.id == cart_item.product_id).first()
if not product or not product.is_active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
if product.stock_quantity < item_in.quantity:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Not enough stock available",
)
cart_item.quantity = item_in.quantity
db.add(cart_item)
db.commit()
db.refresh(cart_item)
return cart_item
@router.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def remove_cart_item(
*,
db: Session = Depends(get_db),
item_id: str,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Remove item from cart.
"""
cart_item = db.query(CartItem).filter(
CartItem.id == item_id,
CartItem.user_id == current_user.id,
).first()
if not cart_item:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cart item not found",
)
db.delete(cart_item)
db.commit()
return None
@router.delete("/", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def clear_cart(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
) -> Any:
"""
Clear cart.
"""
db.query(CartItem).filter(CartItem.user_id == current_user.id).delete()
db.commit()
return None

View File

@ -0,0 +1,119 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_admin
from app.db.session import get_db
from app.models.product import Category
from app.models.user import User
from app.schemas.product import Category as CategorySchema
from app.schemas.product import CategoryCreate, CategoryUpdate
router = APIRouter()
@router.get("/", response_model=List[CategorySchema])
def read_categories(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Retrieve categories.
"""
categories = db.query(Category).offset(skip).limit(limit).all()
return categories
@router.post("/", response_model=CategorySchema)
def create_category(
*,
db: Session = Depends(get_db),
category_in: CategoryCreate,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Create new category. Only admin users can create categories.
"""
# Check if category with the same name already exists
category = db.query(Category).filter(Category.name == category_in.name).first()
if category:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Category with this name already exists",
)
category = Category(**category_in.dict())
db.add(category)
db.commit()
db.refresh(category)
return category
@router.get("/{category_id}", response_model=CategorySchema)
def read_category(
*,
db: Session = Depends(get_db),
category_id: str,
) -> Any:
"""
Get category by ID.
"""
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Category not found",
)
return category
@router.put("/{category_id}", response_model=CategorySchema)
def update_category(
*,
db: Session = Depends(get_db),
category_id: str,
category_in: CategoryUpdate,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Update a category. Only admin users can update categories.
"""
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Category not found",
)
update_data = category_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(category, field, value)
db.add(category)
db.commit()
db.refresh(category)
return category
@router.delete("/{category_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_category(
*,
db: Session = Depends(get_db),
category_id: str,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Delete a category. Only admin users can delete categories.
"""
category = db.query(Category).filter(Category.id == category_id).first()
if not category:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Category not found",
)
db.delete(category)
db.commit()
return None

View File

@ -0,0 +1,29 @@
from typing import Any, Dict
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import get_db
router = APIRouter()
@router.get("/", response_model=Dict[str, Any])
async def health_check(db: Session = Depends(get_db)) -> Any:
"""
Check API health.
"""
# Check if database connection is working
db_status = "healthy"
try:
# Try executing a simple query
db.execute("SELECT 1")
except Exception:
db_status = "unhealthy"
return {
"status": "healthy",
"version": settings.PROJECT_VERSION,
"database": db_status,
}

View File

@ -0,0 +1,250 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.db.session import get_db
from app.models.cart import CartItem
from app.models.order import Order, OrderItem, OrderStatus
from app.models.product import Product
from app.models.user import User
from app.schemas.order import Order as OrderSchema
from app.schemas.order import OrderCreate, OrderUpdate
router = APIRouter()
@router.get("/", response_model=List[OrderSchema])
def read_orders(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Retrieve orders.
"""
# If user is admin, return all orders
if current_user.is_admin:
orders = db.query(Order).offset(skip).limit(limit).all()
else:
# If user is not admin, return only their orders
orders = db.query(Order).filter(Order.user_id == current_user.id).offset(skip).limit(limit).all()
return orders
@router.post("/", response_model=OrderSchema)
def create_order(
*,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
order_in: OrderCreate = None,
) -> Any:
"""
Create new order from cart.
"""
# Get cart items
cart_items = db.query(CartItem).filter(CartItem.user_id == current_user.id).all()
if not cart_items:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cart is empty",
)
# Calculate total amount
total_amount = 0.0
order_items_data = []
for cart_item in cart_items:
product = db.query(Product).filter(Product.id == cart_item.product_id).first()
if not product or not product.is_active:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Product {cart_item.product_id} not found or not active",
)
# Check if product has enough stock
if product.stock_quantity < cart_item.quantity:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Not enough stock for product {product.name}",
)
# Add to total
item_total = product.price * cart_item.quantity
total_amount += item_total
# Prepare order item data
order_items_data.append({
"product_id": product.id,
"quantity": cart_item.quantity,
"price": product.price,
})
# Update product stock
product.stock_quantity -= cart_item.quantity
db.add(product)
# Create order
order_data = {
"user_id": current_user.id,
"total_amount": total_amount,
"status": OrderStatus.PENDING,
}
if order_in:
order_data.update({
"shipping_address": order_in.shipping_address,
"tracking_number": order_in.tracking_number,
"payment_id": order_in.payment_id,
"status": order_in.status,
})
order = Order(**order_data)
db.add(order)
db.commit()
db.refresh(order)
# Create order items
for item_data in order_items_data:
order_item = OrderItem(
order_id=order.id,
**item_data,
)
db.add(order_item)
db.commit()
# Clear cart
db.query(CartItem).filter(CartItem.user_id == current_user.id).delete()
db.commit()
# Refresh order to get items
db.refresh(order)
return order
@router.get("/{order_id}", response_model=OrderSchema)
def read_order(
*,
db: Session = Depends(get_db),
order_id: str,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Get order by ID.
"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Check permissions
if not current_user.is_admin and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return order
@router.put("/{order_id}", response_model=OrderSchema)
def update_order(
*,
db: Session = Depends(get_db),
order_id: str,
order_in: OrderUpdate,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Update an order. Only admin users can update any order. Regular users can only update their own orders.
"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Check permissions
if not current_user.is_admin and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
# Regular users can only update shipping address
if not current_user.is_admin:
if hasattr(order_in, "status") and order_in.status != order.status:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Cannot update order status",
)
update_data = order_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(order, field, value)
db.add(order)
db.commit()
db.refresh(order)
return order
@router.delete("/{order_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def cancel_order(
*,
db: Session = Depends(get_db),
order_id: str,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Cancel an order. Only admin users can cancel any order. Regular users can only cancel their own orders.
"""
order = db.query(Order).filter(Order.id == order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Check permissions
if not current_user.is_admin and order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
# Check if order can be cancelled
if order.status not in [OrderStatus.PENDING, OrderStatus.PAID]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot cancel order with status {order.status}",
)
# Restore product stock
order_items = db.query(OrderItem).filter(OrderItem.order_id == order.id).all()
for item in order_items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product:
product.stock_quantity += item.quantity
db.add(product)
# Update order status
order.status = OrderStatus.CANCELLED
db.add(order)
db.commit()
return None

View File

@ -0,0 +1,81 @@
import uuid
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
from sqlalchemy.orm import Session
from app.api.deps import get_current_user
from app.db.session import get_db
from app.models.order import Order, OrderStatus
from app.models.user import User
router = APIRouter()
class PaymentRequest(BaseModel):
order_id: str
payment_method: str
card_number: str
card_expiry: str
card_cvv: str
class PaymentResponse(BaseModel):
payment_id: str
order_id: str
amount: float
status: str
message: str
@router.post("/", response_model=PaymentResponse)
def process_payment(
*,
db: Session = Depends(get_db),
payment_in: PaymentRequest,
current_user: User = Depends(get_current_user),
) -> Any:
"""
Process payment for an order. This is a mock implementation.
"""
# Get order
order = db.query(Order).filter(Order.id == payment_in.order_id).first()
if not order:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Order not found",
)
# Check permissions
if order.user_id != current_user.id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
# Check if order can be paid
if order.status != OrderStatus.PENDING:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Cannot process payment for order with status {order.status}",
)
# Mock payment processing
# In a real application, this would integrate with a payment gateway
payment_id = str(uuid.uuid4())
# Update order status and payment ID
order.status = OrderStatus.PAID
order.payment_id = payment_id
db.add(order)
db.commit()
return {
"payment_id": payment_id,
"order_id": order.id,
"amount": order.total_amount,
"status": "success",
"message": "Payment processed successfully",
}

View File

@ -0,0 +1,143 @@
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_active_admin
from app.db.session import get_db
from app.models.product import Product
from app.models.user import User
from app.schemas.product import Product as ProductSchema
from app.schemas.product import ProductCreate, ProductUpdate
router = APIRouter()
@router.get("/", response_model=List[ProductSchema])
def read_products(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
category_id: Optional[str] = None,
name: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
) -> Any:
"""
Retrieve products with optional filtering.
"""
query = db.query(Product)
# Apply filters if provided
if category_id:
query = query.filter(Product.category_id == category_id)
if name:
query = query.filter(Product.name.ilike(f"%{name}%"))
if min_price is not None:
query = query.filter(Product.price >= min_price)
if max_price is not None:
query = query.filter(Product.price <= max_price)
# Only show active products
query = query.filter(Product.is_active.is_(True))
products = query.offset(skip).limit(limit).all()
return products
@router.post("/", response_model=ProductSchema)
def create_product(
*,
db: Session = Depends(get_db),
product_in: ProductCreate,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Create new product. Only admin users can create products.
"""
product = Product(**product_in.dict())
db.add(product)
db.commit()
db.refresh(product)
return product
@router.get("/{product_id}", response_model=ProductSchema)
def read_product(
*,
db: Session = Depends(get_db),
product_id: str,
) -> Any:
"""
Get product by ID.
"""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
if not product.is_active:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
return product
@router.put("/{product_id}", response_model=ProductSchema)
def update_product(
*,
db: Session = Depends(get_db),
product_id: str,
product_in: ProductUpdate,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Update a product. Only admin users can update products.
"""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
update_data = product_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(product, field, value)
db.add(product)
db.commit()
db.refresh(product)
return product
@router.delete("/{product_id}", status_code=status.HTTP_204_NO_CONTENT, response_model=None)
def delete_product(
*,
db: Session = Depends(get_db),
product_id: str,
current_user: User = Depends(get_current_active_admin),
) -> Any:
"""
Delete a product. Only admin users can delete products.
"""
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Product not found",
)
# Soft delete by setting is_active to False
product.is_active = False
db.add(product)
db.commit()
return None

View File

@ -0,0 +1,106 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app.core.security import get_current_user, get_password_hash
from app.db.session import get_db
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserUpdate
router = APIRouter()
@router.get("/me", response_model=UserSchema)
def read_user_me(
db: Session = Depends(get_db),
current_user_id: str = Depends(get_current_user),
) -> Any:
"""
Get current user.
"""
user = db.query(User).filter(User.id == current_user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
current_user_id: str = Depends(get_current_user),
user_in: UserUpdate,
) -> Any:
"""
Update current user.
"""
user = db.query(User).filter(User.id == current_user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
update_data = user_in.dict(exclude_unset=True)
if "password" in update_data and update_data["password"]:
update_data["hashed_password"] = get_password_hash(update_data["password"])
del update_data["password"]
for field, value in update_data.items():
setattr(user, field, value)
db.add(user)
db.commit()
db.refresh(user)
return user
@router.get("/{user_id}", response_model=UserSchema)
def read_user_by_id(
user_id: str,
db: Session = Depends(get_db),
current_user_id: str = Depends(get_current_user),
) -> Any:
"""
Get a specific user by id.
"""
user = db.query(User).filter(User.id == current_user_id).first()
if not user or not user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found",
)
return user
@router.get("/", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
current_user_id: str = Depends(get_current_user),
skip: int = 0,
limit: int = 100,
) -> Any:
"""
Retrieve users. Only admin users can access this endpoint.
"""
user = db.query(User).filter(User.id == current_user_id).first()
if not user or not user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
users = db.query(User).offset(skip).limit(limit).all()
return users

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

@ -0,0 +1 @@
# This file marks the core directory as a Python package

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

@ -0,0 +1,38 @@
import os
from pathlib import Path
from typing import List
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
# Project settings
PROJECT_NAME: str = "E-Commerce API"
PROJECT_DESCRIPTION: str = "FastAPI E-Commerce Application"
PROJECT_VERSION: str = "0.1.0"
# API settings
API_V1_STR: str = "/api/v1"
# JWT Settings
JWT_SECRET_KEY: str = os.environ.get("JWT_SECRET_KEY", "supersecretkey")
JWT_ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Database settings
DB_DIR: Path = Path("/app") / "storage" / "db"
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
# CORS settings
CORS_ORIGINS: List[str] = ["*"]
# Security settings
PASSWORD_HASH_ROUNDS: int = 12
class Config:
env_file = ".env"
case_sensitive = True
# Create DB directory if it doesn't exist
Settings().DB_DIR.mkdir(parents=True, exist_ok=True)
settings = Settings()

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

@ -0,0 +1,63 @@
from datetime import datetime, timedelta
from typing import Any, Optional, Union
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy.orm import Session
from app.core.config import settings
from app.db.session import get_db
# Password hashing context
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# OAuth2 password bearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/auth/login")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify if plain password matches hashed password."""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""Hash password."""
return pwd_context.hash(password)
def create_access_token(subject: Union[str, Any], expires_delta: Optional[timedelta] = None) -> str:
"""Create JWT access token."""
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
"""Get current user from token."""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM]
)
user_id: str = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
# This will be implemented after we create the User model
# user = db.query(User).filter(User.id == user_id).first()
# if user is None:
# raise credentials_exception
# return user
# For now, return the user_id
return user_id

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

@ -0,0 +1 @@
# This file marks the db directory as a Python package

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

@ -0,0 +1,28 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Create DB directory if it doesn't exist
settings.DB_DIR.mkdir(parents=True, exist_ok=True)
# Create engine
engine = create_engine(
settings.SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
# Create SessionLocal
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create Base class
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

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

@ -0,0 +1,15 @@
# Import all models here
from app.models.user import User
from app.models.product import Category, Product
from app.models.order import Order, OrderItem, OrderStatus
from app.models.cart import CartItem
__all__ = [
"User",
"Category",
"Product",
"Order",
"OrderItem",
"OrderStatus",
"CartItem",
]

16
app/models/base.py Normal file
View File

@ -0,0 +1,16 @@
import uuid
from datetime import datetime
from sqlalchemy import Column, DateTime
from sqlalchemy.dialects.sqlite import TEXT
from sqlalchemy.ext.declarative import declared_attr
class Base:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower()
id = Column(TEXT, primary_key=True, default=lambda: str(uuid.uuid4()))
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)

19
app/models/cart.py Normal file
View File

@ -0,0 +1,19 @@
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from app.db.session import Base
from app.models.base import Base as BaseModel
class CartItem(Base, BaseModel):
"""Cart item model."""
quantity = Column(Integer, nullable=False, default=1)
# Foreign keys
user_id = Column(String(36), ForeignKey("user.id"), nullable=False)
product_id = Column(String(36), ForeignKey("product.id"), nullable=False)
# Relationships
user = relationship("User", back_populates="cart_items")
product = relationship("Product", back_populates="cart_items")

49
app/models/order.py Normal file
View File

@ -0,0 +1,49 @@
from enum import Enum as PyEnum
from sqlalchemy import Column, Enum, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from app.db.session import Base
from app.models.base import Base as BaseModel
class OrderStatus(str, PyEnum):
"""Order status enum."""
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(Base, BaseModel):
"""Order model."""
total_amount = Column(Float, nullable=False)
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING, nullable=False)
shipping_address = Column(Text, nullable=True)
tracking_number = Column(String(100), nullable=True)
payment_id = Column(String(100), nullable=True)
# Foreign keys
user_id = Column(String(36), ForeignKey("user.id"), nullable=False)
# Relationships
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")
class OrderItem(Base, BaseModel):
"""Order item model."""
quantity = Column(Integer, nullable=False)
price = Column(Float, nullable=False) # Price at the time of purchase
# Foreign keys
order_id = Column(String(36), ForeignKey("order.id"), nullable=False)
product_id = Column(String(36), ForeignKey("product.id"), nullable=False)
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="order_items")

34
app/models/product.py Normal file
View File

@ -0,0 +1,34 @@
from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, Text
from sqlalchemy.orm import relationship
from app.db.session import Base
from app.models.base import Base as BaseModel
class Category(Base, BaseModel):
"""Category model."""
name = Column(String(100), nullable=False, unique=True)
description = Column(Text, nullable=True)
# Relationships
products = relationship("Product", back_populates="category")
class Product(Base, BaseModel):
"""Product model."""
name = Column(String(255), nullable=False, index=True)
description = Column(Text, nullable=True)
price = Column(Float, nullable=False)
stock_quantity = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, default=True)
image_url = Column(String(255), nullable=True)
# Foreign keys
category_id = Column(String(36), ForeignKey("category.id"), nullable=True)
# Relationships
category = relationship("Category", back_populates="products")
order_items = relationship("OrderItem", back_populates="product")
cart_items = relationship("CartItem", back_populates="product")

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

@ -0,0 +1,21 @@
from sqlalchemy import Boolean, Column, String, Text
from sqlalchemy.orm import relationship
from app.db.session import Base
from app.models.base import Base as BaseModel
class User(Base, BaseModel):
"""User model."""
email = Column(String(255), unique=True, index=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
full_name = Column(String(255), nullable=True)
phone = Column(String(20), nullable=True)
address = Column(Text, nullable=True)
is_active = Column(Boolean, default=True)
is_admin = Column(Boolean, default=False)
# Relationships
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
cart_items = relationship("CartItem", back_populates="user", cascade="all, delete-orphan")

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

@ -0,0 +1,29 @@
# Import all schemas here for easier access
from app.schemas.user import User, UserCreate, UserUpdate
from app.schemas.product import Category, CategoryCreate, CategoryUpdate, Product, ProductCreate, ProductUpdate
from app.schemas.order import Order, OrderCreate, OrderItem, OrderItemCreate, OrderUpdate
from app.schemas.cart import Cart, CartItem, CartItemCreate, CartItemUpdate
from app.schemas.token import Token, TokenPayload
__all__ = [
"User",
"UserCreate",
"UserUpdate",
"Category",
"CategoryCreate",
"CategoryUpdate",
"Product",
"ProductCreate",
"ProductUpdate",
"Order",
"OrderCreate",
"OrderItem",
"OrderItemCreate",
"OrderUpdate",
"Cart",
"CartItem",
"CartItemCreate",
"CartItemUpdate",
"Token",
"TokenPayload",
]

35
app/schemas/cart.py Normal file
View File

@ -0,0 +1,35 @@
from typing import List, Optional
from pydantic import BaseModel, Field
from app.schemas.product import Product
class CartItemBase(BaseModel):
product_id: str
quantity: int = Field(..., gt=0)
class CartItemCreate(CartItemBase):
pass
class CartItemUpdate(BaseModel):
quantity: int = Field(..., gt=0)
class CartItem(CartItemBase):
id: str
user_id: str
product: Optional[Product] = None
class Config:
orm_mode = True
class Cart(BaseModel):
items: List[CartItem] = []
total: float = 0
class Config:
orm_mode = True

53
app/schemas/order.py Normal file
View File

@ -0,0 +1,53 @@
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, Field
from app.models.order import OrderStatus
# OrderItem schemas
class OrderItemBase(BaseModel):
product_id: str
quantity: int = Field(..., gt=0)
price: float = Field(..., gt=0)
class OrderItemCreate(OrderItemBase):
pass
class OrderItem(OrderItemBase):
id: str
order_id: str
class Config:
orm_mode = True
# Order schemas
class OrderBase(BaseModel):
shipping_address: Optional[str] = None
tracking_number: Optional[str] = None
payment_id: Optional[str] = None
status: OrderStatus = OrderStatus.PENDING
class OrderCreate(OrderBase):
items: List[OrderItemCreate]
class OrderUpdate(OrderBase):
pass
class Order(OrderBase):
id: str
user_id: str
total_amount: float
created_at: datetime
updated_at: datetime
items: List[OrderItem] = []
class Config:
orm_mode = True

53
app/schemas/product.py Normal file
View File

@ -0,0 +1,53 @@
from typing import Optional
from pydantic import BaseModel, Field
# Category schemas
class CategoryBase(BaseModel):
name: str
description: Optional[str] = None
class CategoryCreate(CategoryBase):
pass
class CategoryUpdate(CategoryBase):
name: Optional[str] = None
class Category(CategoryBase):
id: str
class Config:
orm_mode = True
# Product schemas
class ProductBase(BaseModel):
name: str
description: Optional[str] = None
price: float = Field(..., gt=0)
stock_quantity: int = Field(..., ge=0)
is_active: bool = True
image_url: Optional[str] = None
category_id: Optional[str] = None
class ProductCreate(ProductBase):
pass
class ProductUpdate(ProductBase):
name: Optional[str] = None
price: Optional[float] = Field(None, gt=0)
stock_quantity: Optional[int] = Field(None, ge=0)
class Product(ProductBase):
id: str
category: Optional[Category] = None
class Config:
orm_mode = True

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

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

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

@ -0,0 +1,39 @@
from typing import Optional
from pydantic import BaseModel, EmailStr, Field, validator
# Shared properties
class UserBase(BaseModel):
email: EmailStr
full_name: Optional[str] = None
phone: Optional[str] = None
address: Optional[str] = None
is_active: bool = True
is_admin: bool = False
# Properties to receive via API on creation
class UserCreate(UserBase):
password: str = Field(..., min_length=8)
password_confirm: str
@validator('password_confirm')
def passwords_match(cls, v, values, **kwargs):
if 'password' in values and v != values['password']:
raise ValueError('passwords do not match')
return v
# Properties to receive via API on update
class UserUpdate(UserBase):
email: Optional[EmailStr] = None
password: Optional[str] = Field(None, min_length=8)
# Properties to return via API
class User(UserBase):
id: str
class Config:
orm_mode = True

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

@ -0,0 +1 @@
# This file marks the services directory as a Python package

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

@ -0,0 +1 @@
# This file marks the utils directory as a Python package

35
main.py Normal file
View File

@ -0,0 +1,35 @@
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.api.v1.api import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
description=settings.PROJECT_DESCRIPTION,
version=settings.PROJECT_VERSION,
openapi_url="/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Set up CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router)
# Root health check endpoint
@app.get("/health", tags=["health"])
async def health_check():
return {"status": "healthy"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

1
migrations/README Normal file
View File

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

1
migrations/__init__.py Normal file
View File

@ -0,0 +1 @@
# This file marks the migrations directory as a Python package

80
migrations/env.py Normal file
View File

@ -0,0 +1,80 @@
import os
import sys
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
# Add the parent directory to sys.path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
# Import Base and all models
from app.db.session import Base
from app.models import * # noqa
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == 'sqlite'
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite, # Key configuration for SQLite
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

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

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

View File

@ -0,0 +1,114 @@
"""create tables
Revision ID: 0001
Revises:
Create Date: 2023-08-31 10:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0001'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# Create user table
op.create_table('user',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('hashed_password', sa.String(length=255), nullable=False),
sa.Column('full_name', sa.String(length=255), nullable=True),
sa.Column('phone', sa.String(length=20), nullable=True),
sa.Column('address', sa.Text(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
# Create category table
op.create_table('category',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
# Create product table
op.create_table('product',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('stock_quantity', sa.Integer(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('image_url', sa.String(length=255), nullable=True),
sa.Column('category_id', sa.String(length=36), nullable=True),
sa.ForeignKeyConstraint(['category_id'], ['category.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_name'), 'product', ['name'], unique=False)
# Create order table
op.create_table('order',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('total_amount', sa.Float(), nullable=False),
sa.Column('status', sa.Enum('pending', 'paid', 'shipped', 'delivered', 'cancelled', name='orderstatus'), nullable=False),
sa.Column('shipping_address', sa.Text(), nullable=True),
sa.Column('tracking_number', sa.String(length=100), nullable=True),
sa.Column('payment_id', sa.String(length=100), nullable=True),
sa.Column('user_id', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
# Create order_item table
op.create_table('order_item',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('order_id', sa.String(length=36), nullable=False),
sa.Column('product_id', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id')
)
# Create cart_item table
op.create_table('cart_item',
sa.Column('id', sa.Text(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=False),
sa.Column('product_id', sa.String(length=36), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
def downgrade():
op.drop_table('cart_item')
op.drop_table('order_item')
op.drop_table('order')
op.drop_table('product')
op.drop_table('category')
op.drop_table('user')
op.execute('DROP TYPE orderstatus')

View File

@ -0,0 +1 @@
# This file marks the versions directory as a Python package

13
requirements.txt Normal file
View File

@ -0,0 +1,13 @@
fastapi>=0.103.1
uvicorn>=0.23.2
sqlalchemy>=2.0.20
alembic>=1.12.0
pydantic>=2.3.0
pydantic-settings>=2.0.3
python-jose>=3.3.0
passlib>=1.7.4
bcrypt>=4.0.1
python-multipart>=0.0.6
email-validator>=2.0.0
ruff>=0.0.291
python-dotenv>=1.0.0