~/icsd.ir — bash
SYSTEM_ONLINE

پروژه نهایی – REST API با FastAPI

در این فصل پایانی، یک REST API کامل با FastAPI می‌سازیم - فریم‌ورک مدرن، سریع و async پایتون. شامل SQLAlchemy، Pydantic، احراز هویت JWT، تست خودکار، Docker و دیپلوی نهایی.

در این فصل پایانی، یک REST API کامل با FastAPI می‌سازیم – فریم‌ورک مدرن، سریع و async پایتون. شامل SQLAlchemy، Pydantic، احراز هویت JWT، تست خودکار، Docker و دیپلوی نهایی.

چرا FastAPI؟

  • سریع: یکی از سریع‌ترین فریم‌ورک‌های پایتون (هم‌سطح Node و Go)
  • Async: پشتیبانی native از async/await
  • Type Hints: validation خودکار از روی type hints
  • OpenAPI: مستندات خودکار Swagger UI
  • Pydantic: validation قدرتمند داده

نصب و راه‌اندازی

mkdir fastapi-shop
cd fastapi-shop

# venv
python -m venv venv
source venv/bin/activate    # Linux/Mac
venvScriptsactivate       # Windows

# نصب
pip install fastapi uvicorn[standard]
pip install sqlalchemy alembic
pip install python-jose[cryptography] passlib[bcrypt]
pip install pydantic[email]
pip install pytest httpx pytest-asyncio

ساختار پروژه

fastapi-shop/
├── app/
│   ├── __init__.py
│   ├── main.py              # FastAPI app
│   ├── database.py          # SQLAlchemy setup
│   ├── models.py            # SQLAlchemy models
│   ├── schemas.py           # Pydantic schemas
│   ├── crud.py              # Database operations
│   ├── auth.py              # JWT authentication
│   ├── dependencies.py      # FastAPI dependencies
│   └── routers/
│       ├── __init__.py
│       ├── users.py
│       └── products.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   └── test_main.py
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── .env

Hello FastAPI

# app/main.py
from fastapi import FastAPI

app = FastAPI(
    title="Shop API",
    description="API فروشگاه اینترنتی",
    version="1.0.0"
)

@app.get("/")
def root():
    return {"message": "خوش آمدید"}

@app.get("/health")
def health():
    return {"status": "ok"}
uvicorn app.main:app --reload

# حالا:
# http://localhost:8000        - API
# http://localhost:8000/docs   - Swagger UI خودکار
# http://localhost:8000/redoc  - ReDoc

دیتابیس با SQLAlchemy

# app/database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "sqlite:///./shop.db"
# DATABASE_URL = "postgresql://user:pass@localhost/shop"  # برای Postgres

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False}  # فقط برای SQLite
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    """Dependency برای دریافت session دیتابیس"""
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Models

# app/models.py
from sqlalchemy import Column, Integer, String, Float, Boolean, ForeignKey, DateTime
from sqlalchemy.orm import relationship
from datetime import datetime
from .database import Base

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(50), unique=True, index=True)
    email = Column(String(100), unique=True, index=True)
    hashed_password = Column(String(255))
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    products = relationship("Product", back_populates="owner")

class Product(Base):
    __tablename__ = "products"
    
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(200), index=True)
    description = Column(String(1000))
    price = Column(Float)
    stock = Column(Integer, default=0)
    owner_id = Column(Integer, ForeignKey("users.id"))
    created_at = Column(DateTime, default=datetime.utcnow)
    
    owner = relationship("User", back_populates="products")

Pydantic Schemas

# app/schemas.py
from pydantic import BaseModel, EmailStr, Field
from datetime import datetime

# User
class UserBase(BaseModel):
    username: str = Field(..., min_length=3, max_length=50)
    email: EmailStr

class UserCreate(UserBase):
    password: str = Field(..., min_length=6)

class UserResponse(UserBase):
    id: int
    is_active: bool
    created_at: datetime
    
    class Config:
        from_attributes = True  # برای SQLAlchemy

# Product
class ProductBase(BaseModel):
    name: str = Field(..., min_length=1, max_length=200)
    description: str | None = None
    price: float = Field(..., gt=0)
    stock: int = Field(default=0, ge=0)

class ProductCreate(ProductBase):
    pass

class ProductResponse(ProductBase):
    id: int
    owner_id: int
    created_at: datetime
    
    class Config:
        from_attributes = True

# Token
class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: str | None = None

احراز هویت با JWT

# app/auth.py
from datetime import datetime, timedelta, timezone
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.orm import Session
from .database import get_db
from . import models, schemas

SECRET_KEY = "your-secret-key-change-this"  # از env بخوانید
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)

def create_access_token(data: dict, expires_delta: timedelta | None = None):
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + (
        expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    to_encode.update({"exp": expire})
    return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: Session = Depends(get_db)
):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="نمی‌توان اطلاعات هویت را تایید کرد",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None:
            raise credentials_exception
    except JWTError:
        raise credentials_exception
    
    user = db.query(models.User).filter(models.User.username == username).first()
    if user is None:
        raise credentials_exception
    return user

CRUD Operations

# app/crud.py
from sqlalchemy.orm import Session
from . import models, schemas, auth

# Users
def get_user_by_username(db: Session, username: str):
    return db.query(models.User).filter(models.User.username == username).first()

def create_user(db: Session, user: schemas.UserCreate):
    hashed = auth.hash_password(user.password)
    db_user = models.User(
        username=user.username,
        email=user.email,
        hashed_password=hashed
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

def authenticate_user(db: Session, username: str, password: str):
    user = get_user_by_username(db, username)
    if not user or not auth.verify_password(password, user.hashed_password):
        return None
    return user

# Products
def get_products(db: Session, skip: int = 0, limit: int = 100):
    return db.query(models.Product).offset(skip).limit(limit).all()

def get_product(db: Session, product_id: int):
    return db.query(models.Product).filter(models.Product.id == product_id).first()

def create_product(db: Session, product: schemas.ProductCreate, owner_id: int):
    db_product = models.Product(**product.model_dump(), owner_id=owner_id)
    db.add(db_product)
    db.commit()
    db.refresh(db_product)
    return db_product

def update_product(db: Session, product_id: int, product: schemas.ProductCreate):
    db_product = get_product(db, product_id)
    if db_product:
        for key, value in product.model_dump().items():
            setattr(db_product, key, value)
        db.commit()
        db.refresh(db_product)
    return db_product

def delete_product(db: Session, product_id: int):
    db_product = get_product(db, product_id)
    if db_product:
        db.delete(db_product)
        db.commit()
        return True
    return False

Routers

# app/routers/users.py
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from .. import crud, schemas, auth
from ..database import get_db

router = APIRouter(prefix="/users", tags=["users"])

@router.post("/register", response_model=schemas.UserResponse)
def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
    if crud.get_user_by_username(db, user.username):
        raise HTTPException(400, "این نام کاربری قبلاً ثبت شده")
    return crud.create_user(db, user)

@router.post("/login", response_model=schemas.Token)
def login(
    form_data: OAuth2PasswordRequestForm = Depends(),
    db: Session = Depends(get_db)
):
    user = crud.authenticate_user(db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(401, "نام کاربری یا رمز اشتباه")
    
    token = auth.create_access_token(data={"sub": user.username})
    return {"access_token": token, "token_type": "bearer"}

@router.get("/me", response_model=schemas.UserResponse)
def me(current_user = Depends(auth.get_current_user)):
    return current_user
# app/routers/products.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from .. import crud, schemas, auth
from ..database import get_db

router = APIRouter(prefix="/products", tags=["products"])

@router.get("/", response_model=List[schemas.ProductResponse])
def list_products(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    return crud.get_products(db, skip=skip, limit=limit)

@router.get("/{product_id}", response_model=schemas.ProductResponse)
def get_product(product_id: int, db: Session = Depends(get_db)):
    product = crud.get_product(db, product_id)
    if not product:
        raise HTTPException(404, "محصول یافت نشد")
    return product

@router.post("/", response_model=schemas.ProductResponse, status_code=201)
def create_product(
    product: schemas.ProductCreate,
    db: Session = Depends(get_db),
    current_user = Depends(auth.get_current_user)
):
    return crud.create_product(db, product, current_user.id)

@router.put("/{product_id}", response_model=schemas.ProductResponse)
def update_product(
    product_id: int,
    product: schemas.ProductCreate,
    db: Session = Depends(get_db),
    current_user = Depends(auth.get_current_user)
):
    db_product = crud.get_product(db, product_id)
    if not db_product:
        raise HTTPException(404, "محصول یافت نشد")
    if db_product.owner_id != current_user.id:
        raise HTTPException(403, "دسترسی ندارید")
    return crud.update_product(db, product_id, product)

@router.delete("/{product_id}", status_code=204)
def delete_product(
    product_id: int,
    db: Session = Depends(get_db),
    current_user = Depends(auth.get_current_user)
):
    db_product = crud.get_product(db, product_id)
    if not db_product:
        raise HTTPException(404, "محصول یافت نشد")
    if db_product.owner_id != current_user.id:
        raise HTTPException(403, "دسترسی ندارید")
    crud.delete_product(db, product_id)

main.py نهایی

# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from .database import engine, Base
from .routers import users, products

# ساخت جداول
Base.metadata.create_all(bind=engine)

app = FastAPI(
    title="Shop API",
    description="API فروشگاه اینترنتی - ICSD",
    version="1.0.0"
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # در تولید مشخص کنید
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Routers
app.include_router(users.router)
app.include_router(products.router)

@app.get("/")
def root():
    return {"message": "Shop API", "docs": "/docs"}

تست‌نویسی

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture
def db():
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    yield db
    db.close()
    Base.metadata.drop_all(bind=engine)

@pytest.fixture
def client(db):
    def override_get_db():
        yield db
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)
    app.dependency_overrides.clear()
# tests/test_main.py
def test_root(client):
    response = client.get("/")
    assert response.status_code == 200

def test_register(client):
    response = client.post("/users/register", json={
        "username": "testuser",
        "email": "test@example.com",
        "password": "secret123"
    })
    assert response.status_code == 200
    assert response.json()["username"] == "testuser"

def test_login_and_create_product(client):
    # ثبت‌نام
    client.post("/users/register", json={
        "username": "alice",
        "email": "alice@example.com",
        "password": "password"
    })
    
    # ورود
    response = client.post("/users/login", data={
        "username": "alice",
        "password": "password"
    })
    token = response.json()["access_token"]
    
    # ساخت محصول
    response = client.post(
        "/products/",
        json={"name": "فرش", "price": 5000000, "stock": 10},
        headers={"Authorization": f"Bearer {token}"}
    )
    assert response.status_code == 201
    assert response.json()["name"] == "فرش"

Docker

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
version: "3.9"

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://shop:secret@db/shopdb
    depends_on:
      - db
  
  db:
    image: postgres:16
    environment:
      - POSTGRES_USER=shop
      - POSTGRES_PASSWORD=secret
      - POSTGRES_DB=shopdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:
docker-compose up -d
docker-compose logs -f api

دیپلوی

گزینه‌ها

  • VPS (DigitalOcean، Hetzner): Docker + Nginx + Gunicorn
  • Railway/Render: ساده، با Git push
  • AWS/GCP: قدرتمند، پیچیده‌تر
  • سرورهای ایرانی: لیارا، آروان، رابین

پروداکشن – Gunicorn + Uvicorn workers

pip install gunicorn

gunicorn app.main:app 
  -w 4 
  -k uvicorn.workers.UvicornWorker 
  --bind 0.0.0.0:8000

قدم‌های بعدی

  • Migration با Alembic
  • کش با Redis
  • Background tasks با Celery یا BackgroundTasks
  • WebSocket برای real-time
  • Rate limiting با slowapi
  • Logging با loguru یا structlog
  • Monitoring با Prometheus + Grafana
  • Database: PostgreSQL (دوره بعدی!)

جمع‌بندی – پایان دوره

تبریک! 🎉 شما این دوره را به پایان رساندید. در 15 فصل آموختیم:

  • OOP پیشرفته و Magic Methods
  • Decorators، Generators، Context Managers
  • Type Hints و Static Typing
  • Async/Await و Concurrency
  • Metaclasses و Functional Programming
  • Testing، Performance، Packaging
  • Design Patterns و پروژه واقعی FastAPI

مرحله بعدی: تسلط بر دیتابیس‌ها با دوره PostgreSQL.

توصیه نهایی: هر چیزی که اینجا یاد گرفتید، باید با ساخت پروژه‌های واقعی تمرین کنید. کتاب‌های پیشنهادی: “Fluent Python” نوشته Luciano Ramalho، “High Performance Python” نوشته Micha Gorelick.

نمایش سایت

رنگ سایت
حالت نمایش
اندازهٔ متن
خوانایی

این تنظیمات فقط روی مرورگر شما ذخیره می‌شود.