Build e-commerce API with FastAPI and SQLite

Create a complete e-commerce application with the following features:
- User authentication and authorization
- Product and category management
- Shopping cart functionality
- Order processing
- Database migrations with Alembic
- Comprehensive documentation
This commit is contained in:
Automated Action 2025-05-26 11:05:27 +00:00
parent b6b9d8b0bf
commit 538a985c8e
52 changed files with 2369 additions and 2 deletions

136
README.md
View File

@ -1,3 +1,135 @@
# FastAPI Application
# E-Commerce API
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
A full-featured e-commerce API built with FastAPI and SQLite.
## Features
- **User Management**: Registration, authentication, and profile management
- **Product Management**: Products with categories, pricing, and inventory management
- **Shopping Cart**: Add, update, remove items in user's shopping cart
- **Order Processing**: Create orders from cart, track order status
- **Authentication**: JWT-based authentication with role-based access control
## Tech Stack
- **FastAPI**: Modern, fast API framework
- **SQLAlchemy**: SQL toolkit and ORM
- **Alembic**: Database migration tool
- **SQLite**: Serverless database engine
- **JWT**: JSON Web Tokens for authentication
- **Pydantic**: Data validation and settings management
## API Endpoints
### Authentication
- `POST /api/v1/auth/login`: Login and get access token
- `POST /api/v1/auth/register`: Register a new user
- `GET /api/v1/auth/me`: Get current user information
### Users
- `GET /api/v1/users/`: List all users (admin only)
- `POST /api/v1/users/`: Create a new user (admin only)
- `GET /api/v1/users/me`: Get current user profile
- `PUT /api/v1/users/me`: Update current user profile
- `GET /api/v1/users/{user_id}`: Get user by ID
- `PUT /api/v1/users/{user_id}`: Update user (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 category (admin only)
- `DELETE /api/v1/categories/{category_id}`: Delete category (admin only)
### Products
- `GET /api/v1/products/`: List all products (filter by category optional)
- `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 product (admin only)
- `DELETE /api/v1/products/{product_id}`: Delete product (admin only)
### Cart
- `GET /api/v1/cart/`: Get current 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
- `POST /api/v1/orders/`: Create new order from cart
- `GET /api/v1/orders/{order_id}`: Get order by ID
- `PUT /api/v1/orders/{order_id}`: Update order status (admin only)
- `POST /api/v1/orders/{order_id}/cancel`: Cancel order (if pending)
### Health Check
- `GET /health`: Application health check
## Installation and Setup
### Prerequisites
- Python 3.8+
- pip (Python package installer)
### Installation
1. Clone the repository
```bash
git clone <repository-url>
cd ecommerceapplication
```
2. Install dependencies
```bash
pip install -r requirements.txt
```
3. Run database migrations
```bash
alembic upgrade head
```
4. Start the application
```bash
uvicorn main:app --reload
```
The API will be available at http://localhost:8000.
## Documentation
FastAPI provides automatic API documentation:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
## Development
### Database Migrations
Create a new migration after model changes:
```bash
alembic revision --autogenerate -m "Description of changes"
```
Apply migrations:
```bash
alembic upgrade head
```
### Running Tests
```bash
pytest
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

105
alembic.ini Normal file
View File

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

0
app/__init__.py Normal file
View File

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

19
app/api/api.py Normal file
View File

@ -0,0 +1,19 @@
from fastapi import APIRouter
from app.api.endpoints import (
auth,
users,
products,
categories,
cart,
orders,
)
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["Authentication"])
api_router.include_router(users.router, prefix="/users", tags=["Users"])
api_router.include_router(products.router, prefix="/products", tags=["Products"])
api_router.include_router(categories.router, prefix="/categories", tags=["Categories"])
api_router.include_router(cart.router, prefix="/cart", tags=["Cart"])
api_router.include_router(orders.router, prefix="/orders", tags=["Orders"])

View File

86
app/api/endpoints/auth.py Normal file
View File

@ -0,0 +1,86 @@
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 import security
from app.core.config import settings
from app.core.deps import get_current_user
from app.db.session import get_db
from app.models.user import User
from app.schemas.token import Token
from app.services import user as user_service
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 = user_service.authenticate(db, email=form_data.username, password=form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
elif not user_service.is_active(user):
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": security.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),
form_data: OAuth2PasswordRequestForm = Depends(),
) -> Any:
"""
Register a new user and return an access token
"""
user = user_service.get_by_email(db, email=form_data.username)
if user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A user with this email already exists",
)
# Create user
from app.schemas.user import UserCreate
user_in = UserCreate(email=form_data.username, password=form_data.password)
user = user_service.create(db, obj_in=user_in)
# Generate token
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return {
"access_token": security.create_access_token(
user.id, expires_delta=access_token_expires
),
"token_type": "bearer",
}
@router.get("/me", response_model=Any)
def read_users_me(
current_user: User = Depends(get_current_user),
) -> Any:
"""
Get current user
"""
from app.schemas.user import User as UserSchema
return UserSchema.from_orm(current_user)

151
app/api/endpoints/cart.py Normal file
View File

@ -0,0 +1,151 @@
from typing import Any, Dict
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_user, get_db
from app.models.user import User
from app.schemas.cart import Cart, CartItem, CartItemCreate, CartItemUpdate
from app.services import cart as cart_service
from app.services import product as product_service
router = APIRouter()
@router.get("/", response_model=Cart)
def read_cart(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user's cart
"""
cart = cart_service.get_by_user_id(db, user_id=current_user.id)
if not cart:
# Create a new cart if it doesn't exist
cart = cart_service.ensure_cart_exists(db, user_id=current_user.id)
return cart
@router.post("/items", response_model=CartItem)
def add_cart_item(
*,
db: Session = Depends(get_db),
item_in: CartItemCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Add item to cart
"""
# Verify product exists and is active
product = product_service.get_by_id(db, product_id=item_in.product_id)
if not product or not product.is_active:
raise HTTPException(
status_code=404,
detail="Product not found or unavailable",
)
# Check stock
if product.stock < item_in.quantity:
raise HTTPException(
status_code=400,
detail=f"Not enough stock. Available: {product.stock}",
)
# Ensure cart exists
cart = cart_service.ensure_cart_exists(db, user_id=current_user.id)
# Add item to cart
cart_item = cart_service.add_to_cart(db, cart_id=cart.id, obj_in=item_in)
return cart_item
@router.put("/items/{item_id}", response_model=CartItem)
def update_cart_item(
*,
db: Session = Depends(get_db),
item_id: str,
item_in: CartItemUpdate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update cart item quantity
"""
# Ensure cart exists
cart = cart_service.ensure_cart_exists(db, user_id=current_user.id)
# Verify cart item exists and belongs to current user's cart
cart_item = db.query(Cart).filter(
Cart.id == cart.id
).join(
CartItem, CartItem.cart_id == Cart.id
).filter(
CartItem.id == item_id
).first()
if not cart_item:
raise HTTPException(
status_code=404,
detail="Cart item not found",
)
# Check product stock
product = product_service.get_by_id(db, product_id=cart_item.product_id)
if product.stock < item_in.quantity:
raise HTTPException(
status_code=400,
detail=f"Not enough stock. Available: {product.stock}",
)
# Update cart item
updated_item = cart_service.update_cart_item(db, item_id=item_id, obj_in=item_in)
return updated_item
@router.delete("/items/{item_id}", response_model=Dict[str, str])
def delete_cart_item(
*,
db: Session = Depends(get_db),
item_id: str,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Remove item from cart
"""
# Ensure cart exists
cart = cart_service.ensure_cart_exists(db, user_id=current_user.id)
# Verify cart item exists and belongs to current user's cart
cart_item = db.query(Cart).filter(
Cart.id == cart.id
).join(
CartItem, CartItem.cart_id == Cart.id
).filter(
CartItem.id == item_id
).first()
if not cart_item:
raise HTTPException(
status_code=404,
detail="Cart item not found",
)
# Remove item from cart
cart_service.remove_from_cart(db, item_id=item_id)
return {"status": "success", "message": "Item removed from cart"}
@router.delete("/", response_model=Dict[str, str])
def clear_cart(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Clear cart
"""
# Ensure cart exists
cart = cart_service.ensure_cart_exists(db, user_id=current_user.id)
# Clear cart
cart_service.clear_cart(db, cart_id=cart.id)
return {"status": "success", "message": "Cart cleared"}

View File

@ -0,0 +1,105 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_superuser, get_current_active_user, get_db
from app.models.user import User
from app.schemas.category import Category, CategoryCreate, CategoryUpdate
from app.services import category as category_service
router = APIRouter()
@router.get("/", response_model=List[Category])
def read_categories(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve categories.
"""
categories = category_service.get_multi(db, skip=skip, limit=limit)
return categories
@router.post("/", response_model=Category)
def create_category(
*,
db: Session = Depends(get_db),
category_in: CategoryCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create new category.
"""
category = category_service.get_by_name(db, name=category_in.name)
if category:
raise HTTPException(
status_code=400,
detail="A category with this name already exists",
)
category = category_service.create(db, obj_in=category_in)
return category
@router.get("/{category_id}", response_model=Category)
def read_category(
*,
db: Session = Depends(get_db),
category_id: str,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get category by ID.
"""
category = category_service.get_by_id(db, category_id=category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
return category
@router.put("/{category_id}", response_model=Category)
def update_category(
*,
db: Session = Depends(get_db),
category_id: str,
category_in: CategoryUpdate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Update a category.
"""
category = category_service.get_by_id(db, category_id=category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
category = category_service.update(db, db_obj=category, obj_in=category_in)
return category
@router.delete("/{category_id}", response_model=Category)
def delete_category(
*,
db: Session = Depends(get_db),
category_id: str,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Delete a category.
"""
category = category_service.get_by_id(db, category_id=category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
category = category_service.delete(db, category_id=category_id)
return category

180
app/api/endpoints/orders.py Normal file
View File

@ -0,0 +1,180 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_superuser, get_current_active_user, get_db
from app.models.user import User
from app.models.order import OrderStatus
from app.schemas.order import Order, OrderCreate, OrderUpdate, OrderList, OrderItemCreate
from app.services import order as order_service
from app.services import cart as cart_service
from app.services import product as product_service
router = APIRouter()
@router.get("/", response_model=List[OrderList])
def read_orders(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get user's orders
"""
orders = order_service.get_by_user_id(
db, user_id=current_user.id, skip=skip, limit=limit
)
return orders
@router.post("/", response_model=Order)
def create_order(
*,
db: Session = Depends(get_db),
order_in: OrderCreate,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Create new order from user's cart
"""
# Ensure cart exists
cart = cart_service.get_by_user_id(db, user_id=current_user.id)
if not cart:
raise HTTPException(
status_code=400,
detail="Cart is empty",
)
# Get cart items
cart_items = cart_service.get_cart_items(db, cart_id=cart.id)
if not cart_items:
raise HTTPException(
status_code=400,
detail="Cart is empty",
)
# Convert cart items to order items
order_items = []
for item in cart_items:
product = product_service.get_by_id(db, product_id=item.product_id)
if not product or not product.is_active:
raise HTTPException(
status_code=400,
detail=f"Product {item.product_id} is unavailable",
)
# Check stock
if product.stock < item.quantity:
raise HTTPException(
status_code=400,
detail=f"Not enough stock for product {product.name}. Available: {product.stock}",
)
order_item = OrderItemCreate(
product_id=item.product_id,
quantity=item.quantity,
price=product.price,
)
order_items.append(order_item)
# Create order
order = order_service.create(
db,
obj_in=OrderCreate(
user_id=current_user.id,
shipping_address=order_in.shipping_address,
),
items=order_items,
)
# Clear cart after order is created
cart_service.clear_cart(db, cart_id=cart.id)
return order
@router.get("/{order_id}", response_model=Order)
def read_order(
*,
db: Session = Depends(get_db),
order_id: str,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get order by ID
"""
order = order_service.get_by_id(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=404,
detail="Order not found",
)
# Check if user is allowed to access this order
if order.user_id != current_user.id and not current_user.is_superuser:
raise HTTPException(
status_code=403,
detail="Not enough permissions",
)
return order
@router.put("/{order_id}", response_model=Order)
def update_order(
*,
db: Session = Depends(get_db),
order_id: str,
order_in: OrderUpdate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Update order (admin only)
"""
order = order_service.get_by_id(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=404,
detail="Order not found",
)
order = order_service.update(db, db_obj=order, obj_in=order_in)
return order
@router.post("/{order_id}/cancel", response_model=Order)
def cancel_order(
*,
db: Session = Depends(get_db),
order_id: str,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Cancel order
"""
order = order_service.get_by_id(db, order_id=order_id)
if not order:
raise HTTPException(
status_code=404,
detail="Order not found",
)
# Check if user is allowed to cancel this order
if order.user_id != current_user.id and not current_user.is_superuser:
raise HTTPException(
status_code=403,
detail="Not enough permissions",
)
# Check if order can be cancelled
if order.status != OrderStatus.PENDING:
raise HTTPException(
status_code=400,
detail=f"Order with status {order.status} cannot be cancelled",
)
order = order_service.cancel_order(db, order_id=order_id)
return order

View File

@ -0,0 +1,128 @@
from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_superuser, get_current_active_user, get_db
from app.models.user import User
from app.schemas.product import Product, ProductCreate, ProductList, ProductUpdate
from app.services import product as product_service
from app.services import category as category_service
router = APIRouter()
@router.get("/", response_model=List[ProductList])
def read_products(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
category_id: str = Query(None, description="Filter by category ID"),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Retrieve products.
"""
if category_id:
category = category_service.get_by_id(db, category_id=category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
products = product_service.get_by_category(
db, category_id=category_id, skip=skip, limit=limit
)
else:
products = product_service.get_multi(db, skip=skip, limit=limit)
return products
@router.post("/", response_model=Product)
def create_product(
*,
db: Session = Depends(get_db),
product_in: ProductCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create new product.
"""
category = category_service.get_by_id(db, category_id=product_in.category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
product = product_service.create(db, obj_in=product_in)
return product
@router.get("/{product_id}", response_model=Product)
def read_product(
*,
db: Session = Depends(get_db),
product_id: str,
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get product by ID.
"""
product = product_service.get_by_id(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=404,
detail="Product not found",
)
return product
@router.put("/{product_id}", response_model=Product)
def update_product(
*,
db: Session = Depends(get_db),
product_id: str,
product_in: ProductUpdate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Update a product.
"""
product = product_service.get_by_id(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=404,
detail="Product not found",
)
# Verify category exists if updating category
if product_in.category_id and product_in.category_id != product.category_id:
category = category_service.get_by_id(db, category_id=product_in.category_id)
if not category:
raise HTTPException(
status_code=404,
detail="Category not found",
)
product = product_service.update(db, db_obj=product, obj_in=product_in)
return product
@router.delete("/{product_id}", response_model=Product)
def delete_product(
*,
db: Session = Depends(get_db),
product_id: str,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Delete a product.
"""
product = product_service.get_by_id(db, product_id=product_id)
if not product:
raise HTTPException(
status_code=404,
detail="Product not found",
)
product = product_service.delete(db, product_id=product_id)
return product

122
app/api/endpoints/users.py Normal file
View File

@ -0,0 +1,122 @@
from typing import Any, List
from fastapi import APIRouter, Body, Depends, HTTPException
from fastapi.encoders import jsonable_encoder
from pydantic import EmailStr
from sqlalchemy.orm import Session
from app.core.deps import get_current_active_superuser, get_current_active_user, get_db
from app.models.user import User
from app.schemas.user import User as UserSchema
from app.schemas.user import UserCreate, UserUpdate
from app.services import user as user_service
router = APIRouter()
@router.get("/", response_model=List[UserSchema])
def read_users(
db: Session = Depends(get_db),
skip: int = 0,
limit: int = 100,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Retrieve users.
"""
users = db.query(User).offset(skip).limit(limit).all()
return users
@router.post("/", response_model=UserSchema)
def create_user(
*,
db: Session = Depends(get_db),
user_in: UserCreate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Create new user.
"""
user = user_service.get_by_email(db, email=user_in.email)
if user:
raise HTTPException(
status_code=400,
detail="A user with this email already exists",
)
user = user_service.create(db, obj_in=user_in)
return user
@router.put("/me", response_model=UserSchema)
def update_user_me(
*,
db: Session = Depends(get_db),
password: str = Body(None),
full_name: str = Body(None),
email: EmailStr = Body(None),
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Update own user.
"""
current_user_data = jsonable_encoder(current_user)
user_in = UserUpdate(**current_user_data)
if password is not None:
user_in.password = password
if full_name is not None:
user_in.full_name = full_name
if email is not None:
user_in.email = email
user = user_service.update(db, db_obj=current_user, obj_in=user_in)
return user
@router.get("/me", response_model=UserSchema)
def read_user_me(
current_user: User = Depends(get_current_active_user),
) -> Any:
"""
Get current user.
"""
return current_user
@router.get("/{user_id}", response_model=UserSchema)
def read_user_by_id(
user_id: str,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
) -> Any:
"""
Get a specific user by id.
"""
user = user_service.get_by_id(db, user_id=user_id)
if user == current_user:
return user
if not user_service.is_superuser(current_user):
raise HTTPException(
status_code=400, detail="The user doesn't have enough privileges"
)
return user
@router.put("/{user_id}", response_model=UserSchema)
def update_user(
*,
db: Session = Depends(get_db),
user_id: str,
user_in: UserUpdate,
current_user: User = Depends(get_current_active_superuser),
) -> Any:
"""
Update a user.
"""
user = user_service.get_by_id(db, user_id=user_id)
if not user:
raise HTTPException(
status_code=404,
detail="The user with this id does not exist in the system",
)
user = user_service.update(db, db_obj=user, obj_in=user_in)
return user

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

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

@ -0,0 +1,40 @@
from typing import List
from pydantic import AnyHttpUrl, field_validator
from pydantic_settings import BaseSettings
from pathlib import Path
class Settings(BaseSettings):
PROJECT_NAME: str = "E-Commerce API"
VERSION: str = "0.1.0"
API_V1_STR: str = "/api/v1"
# CORS Settings
CORS_ORIGINS: List[AnyHttpUrl] = []
@field_validator("CORS_ORIGINS", mode="before")
@classmethod
def validate_cors_origins(cls, v):
if isinstance(v, str) and not v.startswith("["):
return [i.strip() for i in v.split(",")]
elif isinstance(v, (list, str)):
return v
raise ValueError(v)
# JWT Settings
SECRET_KEY: str = "supersecretkey" # Change in production
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# Database Settings
DB_DIR: Path = Path("/app") / "storage" / "db"
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()
# Ensure DB directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)

53
app/core/deps.py Normal file
View File

@ -0,0 +1,53 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from pydantic import ValidationError
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.user import User
from app.core.config import settings
from app.schemas.token import TokenPayload
oauth2_scheme = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/auth/login"
)
def get_current_user(
db: Session = Depends(get_db),
token: str = Depends(oauth2_scheme),
) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[settings.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")
return user
def get_current_active_user(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_active:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
def get_current_active_superuser(
current_user: User = Depends(get_current_user),
) -> User:
if not current_user.is_superuser:
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
return current_user

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

@ -0,0 +1,38 @@
from datetime import datetime, timedelta
from typing import Any, Union
from jose import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str:
"""
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.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify plain password against hashed password
"""
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
"""
Hash a password for storing
"""
return pwd_context.hash(password)

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

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

@ -0,0 +1,10 @@
# Import all the models, so that Base has them before being
# imported by Alembic
from app.db.base_class import Base # noqa
from app.models.user import User # noqa
from app.models.product import Product # noqa
from app.models.category import Category # noqa
from app.models.order import Order # noqa
from app.models.order_item import OrderItem # noqa
from app.models.cart import Cart # noqa
from app.models.cart_item import CartItem # noqa

13
app/db/base_class.py Normal file
View File

@ -0,0 +1,13 @@
from typing import Any
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import DeclarativeBase
class Base(DeclarativeBase):
id: Any
__name__: str
# Generate __tablename__ automatically
@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

13
app/db/init_db.py Normal file
View File

@ -0,0 +1,13 @@
from app.db.base import Base
from app.db.session import engine
def init_db() -> None:
# Create tables if they don't exist
Base.metadata.create_all(bind=engine)
def drop_db() -> None:
# Drop all tables
Base.metadata.drop_all(bind=engine)

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

@ -0,0 +1,24 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.core.config import settings
# Ensure DB directory exists
settings.DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL = f"sqlite:///{settings.DB_DIR}/db.sqlite"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={"check_same_thread": False}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()

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

@ -0,0 +1,20 @@
# Import all models
from app.models.user import User # noqa
from app.models.category import Category # noqa
from app.models.product import Product # noqa
from app.models.order import Order, OrderStatus # noqa
from app.models.order_item import OrderItem # noqa
from app.models.cart import Cart # noqa
from app.models.cart_item import CartItem # noqa
# Define __all__ to specify what gets imported with `from app.models import *`
__all__ = [
"User",
"Category",
"Product",
"Order",
"OrderStatus",
"OrderItem",
"Cart",
"CartItem",
]

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

@ -0,0 +1,16 @@
from datetime import datetime
from sqlalchemy import Column, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class Cart(Base):
id = Column(String, primary_key=True, index=True)
user_id = Column(String, ForeignKey("user.id"), unique=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="cart")
items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan")

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

@ -0,0 +1,21 @@
from datetime import datetime
from sqlalchemy import Column, String, Integer, DateTime, ForeignKey, UniqueConstraint
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class CartItem(Base):
id = Column(String, primary_key=True, index=True)
cart_id = Column(String, ForeignKey("cart.id"))
product_id = Column(String, ForeignKey("product.id"))
quantity = Column(Integer, nullable=False, default=1)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Ensure each product can only be in the cart once
__table_args__ = (UniqueConstraint('cart_id', 'product_id', name='_cart_product_uc'),)
# Relationships
cart = relationship("Cart", back_populates="items")
product = relationship("Product", back_populates="cart_items")

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

@ -0,0 +1,16 @@
from datetime import datetime
from sqlalchemy import Column, String, DateTime, Text
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class Category(Base):
id = Column(String, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
products = relationship("Product", back_populates="category")

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

@ -0,0 +1,29 @@
from datetime import datetime
from sqlalchemy import Column, String, Float, DateTime, Text, ForeignKey
from sqlalchemy.orm import relationship
import enum
from app.db.base_class import Base
class OrderStatus(str, enum.Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(Base):
id = Column(String, primary_key=True, index=True)
user_id = Column(String, ForeignKey("user.id"))
status = Column(String, default=OrderStatus.PENDING)
total_amount = Column(Float, nullable=False, default=0.0)
shipping_address = Column(Text, nullable=True)
payment_id = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
user = relationship("User", back_populates="orders")
items = relationship("OrderItem", back_populates="order", cascade="all, delete-orphan")

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

@ -0,0 +1,19 @@
from datetime import datetime
from sqlalchemy import Column, String, Float, Integer, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class OrderItem(Base):
id = Column(String, primary_key=True, index=True)
order_id = Column(String, ForeignKey("order.id"))
product_id = Column(String, ForeignKey("product.id"))
quantity = Column(Integer, nullable=False, default=1)
price = Column(Float, nullable=False) # Price at the time of purchase
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
order = relationship("Order", back_populates="items")
product = relationship("Product", back_populates="order_items")

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

@ -0,0 +1,23 @@
from datetime import datetime
from sqlalchemy import Column, String, Float, Integer, DateTime, Text, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class Product(Base):
id = Column(String, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(Text, nullable=True)
price = Column(Float, nullable=False)
stock = Column(Integer, nullable=False, default=0)
is_active = Column(Boolean, default=True)
category_id = Column(String, ForeignKey("category.id"))
image_url = Column(String, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
category = relationship("Category", back_populates="products")
order_items = relationship("OrderItem", back_populates="product")
cart_items = relationship("CartItem", back_populates="product")

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

@ -0,0 +1,20 @@
from datetime import datetime
from sqlalchemy import Boolean, Column, String, DateTime
from sqlalchemy.orm import relationship
from app.db.base_class import Base
class User(Base):
id = Column(String, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
full_name = Column(String, index=True)
is_active = Column(Boolean, default=True)
is_superuser = Column(Boolean, default=False)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
orders = relationship("Order", back_populates="user", cascade="all, delete-orphan")
cart = relationship("Cart", back_populates="user", uselist=False, cascade="all, delete-orphan")

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

@ -0,0 +1,17 @@
# Import all schema models
from app.schemas.token import Token, TokenPayload # noqa
from app.schemas.user import User, UserCreate, UserInDB, UserUpdate # noqa
from app.schemas.category import Category, CategoryCreate, CategoryUpdate # noqa
from app.schemas.product import Product, ProductCreate, ProductUpdate, ProductList # noqa
from app.schemas.cart import Cart, CartCreate, CartUpdate, CartItem, CartItemCreate, CartItemUpdate # noqa
from app.schemas.order import Order, OrderCreate, OrderUpdate, OrderItem, OrderItemCreate, OrderList # noqa
# Define __all__ to specify what gets imported with `from app.schemas import *`
__all__ = [
"Token", "TokenPayload",
"User", "UserCreate", "UserInDB", "UserUpdate",
"Category", "CategoryCreate", "CategoryUpdate",
"Product", "ProductCreate", "ProductUpdate", "ProductList",
"Cart", "CartCreate", "CartUpdate", "CartItem", "CartItemCreate", "CartItemUpdate",
"Order", "OrderCreate", "OrderUpdate", "OrderItem", "OrderItemCreate", "OrderList",
]

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

@ -0,0 +1,56 @@
from typing import List
from datetime import datetime
from pydantic import BaseModel, Field
class CartItemBase(BaseModel):
product_id: str
quantity: int = Field(..., gt=0)
class CartItemCreate(CartItemBase):
pass
class CartItemUpdate(BaseModel):
quantity: int = Field(..., gt=0)
class CartItemInDBBase(CartItemBase):
id: str
cart_id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class CartItem(CartItemInDBBase):
pass
class CartBase(BaseModel):
pass
class CartCreate(CartBase):
user_id: str
class CartUpdate(CartBase):
pass
class CartInDBBase(CartBase):
id: str
user_id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
class Cart(CartInDBBase):
items: List[CartItem] = []

33
app/schemas/category.py Normal file
View File

@ -0,0 +1,33 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel
# Shared properties
class CategoryBase(BaseModel):
name: str
description: Optional[str] = None
# Properties to receive via API on creation
class CategoryCreate(CategoryBase):
pass
# Properties to receive via API on update
class CategoryUpdate(CategoryBase):
name: Optional[str] = None
class CategoryInDBBase(CategoryBase):
id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
# Additional properties to return via API
class Category(CategoryInDBBase):
pass

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

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

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

@ -0,0 +1,57 @@
from typing import Optional
from datetime import datetime
from pydantic import BaseModel, Field
# Shared properties
class ProductBase(BaseModel):
name: str
description: Optional[str] = None
price: float = Field(..., gt=0)
stock: int = Field(..., ge=0)
is_active: bool = True
category_id: str
image_url: Optional[str] = None
# Properties to receive via API on creation
class ProductCreate(ProductBase):
pass
# Properties to receive via API on update
class ProductUpdate(ProductBase):
name: Optional[str] = None
description: Optional[str] = None
price: Optional[float] = None
stock: Optional[int] = None
is_active: Optional[bool] = None
category_id: Optional[str] = None
image_url: Optional[str] = None
class ProductInDBBase(ProductBase):
id: str
created_at: datetime
updated_at: datetime
class Config:
orm_mode = True
# Additional properties to return via API
class Product(ProductInDBBase):
pass
# Product with minimal data for list views
class ProductList(BaseModel):
id: str
name: str
price: float
image_url: Optional[str] = None
category_id: str
is_active: bool
class Config:
orm_mode = True

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

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

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

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

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

104
app/services/cart.py Normal file
View File

@ -0,0 +1,104 @@
import uuid
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.cart import Cart
from app.models.cart_item import CartItem
from app.models.product import Product
from app.schemas.cart import CartCreate, CartItemCreate, CartItemUpdate
def get_by_user_id(db: Session, user_id: str) -> Optional[Cart]:
return db.query(Cart).filter(Cart.user_id == user_id).first()
def get_cart_items(db: Session, cart_id: str) -> List[CartItem]:
return db.query(CartItem).filter(CartItem.cart_id == cart_id).all()
def create_cart(db: Session, *, obj_in: CartCreate) -> Cart:
db_obj = Cart(
id=str(uuid.uuid4()),
user_id=obj_in.user_id,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def ensure_cart_exists(db: Session, user_id: str) -> Cart:
cart = get_by_user_id(db, user_id=user_id)
if not cart:
cart_in = CartCreate(user_id=user_id)
cart = create_cart(db, obj_in=cart_in)
return cart
def add_to_cart(db: Session, *, cart_id: str, obj_in: CartItemCreate) -> CartItem:
# Check if item already exists in cart
cart_item = db.query(CartItem).filter(
CartItem.cart_id == cart_id,
CartItem.product_id == obj_in.product_id
).first()
if cart_item:
# Update quantity if item already exists
cart_item.quantity += obj_in.quantity
db.add(cart_item)
db.commit()
db.refresh(cart_item)
return cart_item
# Create new cart item
db_obj = CartItem(
id=str(uuid.uuid4()),
cart_id=cart_id,
product_id=obj_in.product_id,
quantity=obj_in.quantity,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_cart_item(db: Session, *, item_id: str, obj_in: CartItemUpdate) -> Optional[CartItem]:
cart_item = db.query(CartItem).filter(CartItem.id == item_id).first()
if not cart_item:
return None
cart_item.quantity = obj_in.quantity
db.add(cart_item)
db.commit()
db.refresh(cart_item)
return cart_item
def remove_from_cart(db: Session, *, item_id: str) -> Optional[CartItem]:
cart_item = db.query(CartItem).filter(CartItem.id == item_id).first()
if not cart_item:
return None
db.delete(cart_item)
db.commit()
return cart_item
def clear_cart(db: Session, *, cart_id: str) -> None:
db.query(CartItem).filter(CartItem.cart_id == cart_id).delete()
db.commit()
return None
def calculate_cart_total(db: Session, *, cart_id: str) -> float:
cart_items = get_cart_items(db, cart_id=cart_id)
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 total

50
app/services/category.py Normal file
View File

@ -0,0 +1,50 @@
import uuid
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.category import Category
from app.schemas.category import CategoryCreate, CategoryUpdate
def get_by_id(db: Session, category_id: str) -> Optional[Category]:
return db.query(Category).filter(Category.id == category_id).first()
def get_by_name(db: Session, name: str) -> Optional[Category]:
return db.query(Category).filter(Category.name == name).first()
def get_multi(db: Session, *, skip: int = 0, limit: int = 100) -> List[Category]:
return db.query(Category).offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: CategoryCreate) -> Category:
db_obj = Category(
id=str(uuid.uuid4()),
name=obj_in.name,
description=obj_in.description,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(db: Session, *, db_obj: Category, obj_in: CategoryUpdate) -> Category:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete(db: Session, *, category_id: str) -> Category:
obj = db.query(Category).get(category_id)
db.delete(obj)
db.commit()
return obj

94
app/services/order.py Normal file
View File

@ -0,0 +1,94 @@
import uuid
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.order import Order, OrderStatus
from app.models.order_item import OrderItem
from app.models.product import Product
from app.schemas.order import OrderCreate, OrderUpdate, OrderItemCreate
def get_by_id(db: Session, order_id: str) -> Optional[Order]:
return db.query(Order).filter(Order.id == order_id).first()
def get_by_user_id(db: Session, user_id: str, skip: int = 0, limit: int = 100) -> List[Order]:
return db.query(Order).filter(Order.user_id == user_id).order_by(Order.created_at.desc()).offset(skip).limit(limit).all()
def get_order_items(db: Session, order_id: str) -> List[OrderItem]:
return db.query(OrderItem).filter(OrderItem.order_id == order_id).all()
def create(db: Session, *, obj_in: OrderCreate, items: List[OrderItemCreate]) -> Order:
# Calculate total amount
total_amount = 0.0
for item in items:
total_amount += item.price * item.quantity
# Create order
db_obj = Order(
id=str(uuid.uuid4()),
user_id=obj_in.user_id,
status=OrderStatus.PENDING,
total_amount=total_amount,
shipping_address=obj_in.shipping_address,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
# Create order items
for item in items:
order_item = OrderItem(
id=str(uuid.uuid4()),
order_id=db_obj.id,
product_id=item.product_id,
quantity=item.quantity,
price=item.price,
)
db.add(order_item)
# Update product stock
product = db.query(Product).filter(Product.id == item.product_id).first()
if product:
product.stock -= item.quantity
db.add(product)
db.commit()
return db_obj
def update(db: Session, *, db_obj: Order, obj_in: OrderUpdate) -> Order:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def cancel_order(db: Session, *, order_id: str) -> Order:
order = get_by_id(db, order_id=order_id)
if not order or order.status != OrderStatus.PENDING:
return None
# Update order status
order.status = OrderStatus.CANCELLED
db.add(order)
# Restore product stock
order_items = get_order_items(db, order_id=order.id)
for item in order_items:
product = db.query(Product).filter(Product.id == item.product_id).first()
if product:
product.stock += item.quantity
db.add(product)
db.commit()
db.refresh(order)
return order

60
app/services/product.py Normal file
View File

@ -0,0 +1,60 @@
import uuid
from typing import List, Optional
from sqlalchemy.orm import Session
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate
def get_by_id(db: Session, product_id: str) -> Optional[Product]:
return db.query(Product).filter(Product.id == product_id).first()
def get_multi(db: Session, *, skip: int = 0, limit: int = 100) -> List[Product]:
return db.query(Product).filter(Product.is_active).offset(skip).limit(limit).all()
def get_by_category(db: Session, *, category_id: str, skip: int = 0, limit: int = 100) -> List[Product]:
return db.query(Product).filter(
Product.category_id == category_id,
Product.is_active
).offset(skip).limit(limit).all()
def create(db: Session, *, obj_in: ProductCreate) -> Product:
db_obj = Product(
id=str(uuid.uuid4()),
name=obj_in.name,
description=obj_in.description,
price=obj_in.price,
stock=obj_in.stock,
is_active=obj_in.is_active,
category_id=obj_in.category_id,
image_url=obj_in.image_url,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(db: Session, *, db_obj: Product, obj_in: ProductUpdate) -> Product:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def delete(db: Session, *, product_id: str) -> Product:
obj = db.query(Product).get(product_id)
obj.is_active = False
db.add(obj)
db.commit()
db.refresh(obj)
return obj

63
app/services/user.py Normal file
View File

@ -0,0 +1,63 @@
import uuid
from typing import Optional
from sqlalchemy.orm import Session
from app.core.security import get_password_hash, verify_password
from app.models.user import User
from app.schemas.user import UserCreate, UserUpdate
def get_by_id(db: Session, user_id: str) -> Optional[User]:
return db.query(User).filter(User.id == user_id).first()
def get_by_email(db: Session, email: str) -> Optional[User]:
return db.query(User).filter(User.email == email).first()
def create(db: Session, *, obj_in: UserCreate) -> User:
db_obj = User(
id=str(uuid.uuid4()),
email=obj_in.email,
hashed_password=get_password_hash(obj_in.password),
full_name=obj_in.full_name,
is_superuser=obj_in.is_superuser,
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(db: Session, *, db_obj: User, obj_in: UserUpdate) -> User:
update_data = obj_in.dict(exclude_unset=True)
if update_data.get("password"):
hashed_password = get_password_hash(update_data["password"])
del update_data["password"]
update_data["hashed_password"] = hashed_password
for field, value in update_data.items():
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def authenticate(db: Session, *, email: str, password: str) -> Optional[User]:
user = get_by_email(db, email=email)
if not user:
return None
if not verify_password(password, user.hashed_password):
return None
return user
def is_active(user: User) -> bool:
return user.is_active
def is_superuser(user: User) -> bool:
return user.is_superuser

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

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.api import api_router
from app.core.config import settings
app = FastAPI(
title=settings.PROJECT_NAME,
version=settings.VERSION,
description="E-commerce API with FastAPI",
openapi_url=f"{settings.API_V1_STR}/openapi.json",
docs_url="/docs",
redoc_url="/redoc",
)
# Set CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include API router
app.include_router(api_router, prefix=settings.API_V1_STR)
# Health check endpoint
@app.get("/health", tags=["Health"])
async def health_check():
return {"status": "healthy"}
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

15
migrations/README Normal file
View File

@ -0,0 +1,15 @@
Generic single-database configuration with SQLAlchemy.
This directory contains Alembic migration scripts for the e-commerce application database.
To create a new migration script:
$ alembic revision --autogenerate -m "Description of the changes"
To apply all migration scripts to the database:
$ alembic upgrade head
To downgrade to a specific revision:
$ alembic downgrade <revision_id>
To get the current revision:
$ alembic current

0
migrations/__init__.py Normal file
View File

84
migrations/env.py Normal file
View File

@ -0,0 +1,84 @@
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# Import Base model
from app.db.base import Base
# This is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""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() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
is_sqlite = connection.dialect.name == 'sqlite'
context.configure(
connection=connection,
target_metadata=target_metadata,
render_as_batch=is_sqlite,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

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

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

View File

@ -0,0 +1,156 @@
"""Initial tables
Revision ID: 1a2b3c4d5e6f
Revises:
Create Date: 2023-11-16 00:00:00.000000
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1a2b3c4d5e6f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# Create user table
op.create_table(
'user',
sa.Column('id', sa.String(), nullable=False),
sa.Column('email', sa.String(), nullable=True),
sa.Column('hashed_password', sa.String(), nullable=True),
sa.Column('full_name', sa.String(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_superuser', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
# Create category table
op.create_table(
'category',
sa.Column('id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_category_id'), 'category', ['id'], unique=False)
op.create_index(op.f('ix_category_name'), 'category', ['name'], unique=False)
# Create product table
op.create_table(
'product',
sa.Column('id', sa.String(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('stock', sa.Integer(), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('category_id', sa.String(), nullable=True),
sa.Column('image_url', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['category_id'], ['category.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_id'), 'product', ['id'], unique=False)
op.create_index(op.f('ix_product_name'), 'product', ['name'], unique=False)
# Create cart table
op.create_table(
'cart',
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id')
)
op.create_index(op.f('ix_cart_id'), 'cart', ['id'], unique=False)
# Create order table
op.create_table(
'order',
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=True),
sa.Column('status', sa.String(), nullable=True),
sa.Column('total_amount', sa.Float(), nullable=False),
sa.Column('shipping_address', sa.Text(), nullable=True),
sa.Column('payment_id', sa.String(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_order_id'), 'order', ['id'], unique=False)
# Create order_item table
op.create_table(
'order_item',
sa.Column('id', sa.String(), nullable=False),
sa.Column('order_id', sa.String(), nullable=True),
sa.Column('product_id', sa.String(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('price', sa.Float(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['order_id'], ['order.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_order_item_id'), 'order_item', ['id'], unique=False)
# Create cart_item table
op.create_table(
'cart_item',
sa.Column('id', sa.String(), nullable=False),
sa.Column('cart_id', sa.String(), nullable=True),
sa.Column('product_id', sa.String(), nullable=True),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['cart_id'], ['cart.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('cart_id', 'product_id', name='_cart_product_uc')
)
op.create_index(op.f('ix_cart_item_id'), 'cart_item', ['id'], unique=False)
def downgrade() -> None:
# Drop all tables in reverse order
op.drop_index(op.f('ix_cart_item_id'), table_name='cart_item')
op.drop_table('cart_item')
op.drop_index(op.f('ix_order_item_id'), table_name='order_item')
op.drop_table('order_item')
op.drop_index(op.f('ix_order_id'), table_name='order')
op.drop_table('order')
op.drop_index(op.f('ix_cart_id'), table_name='cart')
op.drop_table('cart')
op.drop_index(op.f('ix_product_name'), table_name='product')
op.drop_index(op.f('ix_product_id'), table_name='product')
op.drop_table('product')
op.drop_index(op.f('ix_category_name'), table_name='category')
op.drop_index(op.f('ix_category_id'), table_name='category')
op.drop_table('category')
op.drop_index(op.f('ix_user_id'), table_name='user')
op.drop_index(op.f('ix_user_full_name'), table_name='user')
op.drop_index(op.f('ix_user_email'), table_name='user')
op.drop_table('user')

View File

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
fastapi==0.104.1
pydantic==2.4.2
pydantic-settings==2.0.3
python-multipart==0.0.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
sqlalchemy==2.0.23
alembic==1.12.1
uvicorn==0.24.0
ruff==0.1.5
email-validator==2.1.0
pytest==7.4.3
httpx==0.25.1
python-dotenv==1.0.0

0
storage/db/.gitkeep Normal file
View File