Fastapi CRUD

임정민·2024년 10월 21일

메모장

목록 보기
14/33
post-thumbnail
# Project Structure (unchanged)
crud-app/
│
├── api/
│   └── v1/
│       └── endpoints/
│           └── items.py
│
├── core/
│   ├── config.py
│   └── logger.py
│
├── crud/
│   └── crud_item.py
│
├── db/
│   └── session.py
│
├── exceptions/
│   └── http_exceptions.py
│
├── models/
│   └── item.py
│
├── schemas/
│   └── item.py
│
├── tests/
│   ├── conftest.py
│   ├── test_api.py
│   └── test_crud.py
│
├── main.py
├── Dockerfile
├── Makefile
├── dev.env
├── prod.env
└── requirements.txt

# 수정된 파일: api/v1/endpoints/items.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from crud import crud_item
from models.item import Item
from schemas.item import ItemCreate, ItemUpdate, ItemInDB
from db.session import get_db
from core.logger import logger
from exceptions.http_exceptions import ItemNotFoundException, ItemAlreadyExistsException

router = APIRouter()

@router.get("/items/", response_model=List[ItemInDB], summary="Get all items")
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    """
    Retrieve all items.

    - **skip**: Number of items to skip (pagination)
    - **limit**: Maximum number of items to return
    """
    items = crud_item.get_items(db, skip=skip, limit=limit)
    logger.info(f"Retrieved {len(items)} items")
    return items

@router.post("/items/", response_model=ItemInDB, summary="Create a new item")
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
    """
    Create a new item.

    - **name**: Required. The name of the item
    - **description**: Optional. A description of the item
    """
    db_item = crud_item.get_item_by_name(db, name=item.name)
    if db_item:
        logger.warning(f"Attempt to create duplicate item: {item.name}")
        raise ItemAlreadyExistsException(name=item.name)
    return crud_item.create_item(db=db, item=item)

@router.get("/items/{item_id}", response_model=ItemInDB, summary="Get a specific item")
def read_item(item_id: int, db: Session = Depends(get_db)):
    """
    Get a specific item by ID.

    - **item_id**: Required. The ID of the item to retrieve
    """
    db_item = crud_item.get_item(db, item_id=item_id)
    if db_item is None:
        logger.warning(f"Item not found: {item_id}")
        raise ItemNotFoundException(item_id=item_id)
    return db_item

@router.put("/items/{item_id}", response_model=ItemInDB, summary="Update an item")
def update_item(item_id: int, item: ItemUpdate, db: Session = Depends(get_db)):
    """
    Update an existing item.

    - **item_id**: Required. The ID of the item to update
    - **name**: Optional. The new name of the item
    - **description**: Optional. The new description of the item
    """
    db_item = crud_item.get_item(db, item_id=item_id)
    if db_item is None:
        logger.warning(f"Attempt to update non-existent item: {item_id}")
        raise ItemNotFoundException(item_id=item_id)
    return crud_item.update_item(db=db, item_id=item_id, item=item)

@router.delete("/items/{item_id}", response_model=ItemInDB, summary="Delete an item")
def delete_item(item_id: int, db: Session = Depends(get_db)):
    """
    Delete an existing item.

    - **item_id**: Required. The ID of the item to delete
    """
    db_item = crud_item.get_item(db, item_id=item_id)
    if db_item is None:
        logger.warning(f"Attempt to delete non-existent item: {item_id}")
        raise ItemNotFoundException(item_id=item_id)
    return crud_item.delete_item(db=db, item_id=item_id)

# 나머지 파일들은 변경 없음

# main.py
from fastapi import FastAPI
from api.v1.endpoints import items
from core.config import settings
from db.session import engine
from models import item as item_model

app = FastAPI(
    title=settings.PROJECT_NAME,
    openapi_url=f"{settings.API_V1_STR}/openapi.json"
)

# Create database tables
item_model.Base.metadata.create_all(bind=engine)

# Include routers
app.include_router(items.router, prefix=settings.API_V1_STR)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "main:app",
        host=settings.HOST,
        port=settings.PORT,
        reload=settings.DEBUG
    )

# core/config.py
from pydantic import BaseSettings
import os

class Settings(BaseSettings):
    API_V1_STR: str = "/api/v1"
    PROJECT_NAME: str = "CRUD App"
    DEBUG: bool = os.getenv("DEBUG", "False") == "True"
    HOST: str = os.getenv("HOST", "0.0.0.0")
    PORT: int = int(os.getenv("PORT", 8000))
    DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./sql_app.db")
    LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")

    class Config:
        env_file = "dev.env" if os.getenv("ENV") == "dev" else "prod.env"

settings = Settings()

# core/logger.py
import logging
from core.config import settings

# Configure logging
logging.basicConfig(
    level=settings.LOG_LEVEL,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)

logger = logging.getLogger(__name__)

# crud/crud_item.py
from sqlalchemy.orm import Session
from models.item import Item
from schemas.item import ItemCreate, ItemUpdate

def get_item(db: Session, item_id: int):
    return db.query(Item).filter(Item.id == item_id).first()

def get_item_by_name(db: Session, name: str):
    return db.query(Item).filter(Item.name == name).first()

def get_items(db: Session, skip: int = 0, limit: int = 100):
    return db.query(Item).offset(skip).limit(limit).all()

def create_item(db: Session, item: ItemCreate):
    db_item = Item(**item.dict())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

def update_item(db: Session, item_id: int, item: ItemUpdate):
    db_item = get_item(db, item_id)
    update_data = item.dict(exclude_unset=True)
    for key, value in update_data.items():
        setattr(db_item, key, value)
    db.commit()
    db.refresh(db_item)
    return db_item

def delete_item(db: Session, item_id: int):
    db_item = get_item(db, item_id)
    db.delete(db_item)
    db.commit()
    return db_item

# db/session.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from core.config import settings

engine = create_engine(settings.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()

# exceptions/http_exceptions.py
from fastapi import HTTPException

class ItemNotFoundException(HTTPException):
    def __init__(self, item_id: int):
        super().__init__(status_code=404, detail=f"Item with id {item_id} not found")

class ItemAlreadyExistsException(HTTPException):
    def __init__(self, name: str):
        super().__init__(status_code=400, detail=f"Item with name {name} already exists")

# models/item.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, unique=True, index=True)
    description = Column(String, index=True)

# schemas/item.py
from pydantic import BaseModel, Field

class ItemBase(BaseModel):
    name: str = Field(..., example="Laptop")
    description: str = Field(None, example="A high-performance laptop")

class ItemCreate(ItemBase):
    pass

class ItemUpdate(ItemBase):
    name: str = Field(None, example="Updated Laptop")
    description: str = Field(None, example="An updated high-performance laptop")

class ItemInDB(ItemBase):
    id: int

    class Config:
        orm_mode = True

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from main import app
from db.session import get_db
from models.item import Base

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(scope="function")
def db():
    Base.metadata.create_all(bind=engine)
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()
        Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function")
def client(db):
    def override_get_db():
        try:
            yield db
        finally:
            db.close()
    
    app.dependency_overrides[get_db] = override_get_db
    yield TestClient(app)
    del app.dependency_overrides[get_db]

# tests/test_api.py
from fastapi.testclient import TestClient

def test_create_item(client):
    response = client.post(
        "/api/v1/items/",
        json={"name": "Test Item", "description": "This is a test item"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Test Item"
    assert data["description"] == "This is a test item"
    assert "id" in data

def test_read_item(client):
    # First, create an item
    response = client.post(
        "/api/v1/items/",
        json={"name": "Test Item", "description": "This is a test item"},
    )
    created_item = response.json()

    # Then, read the item
    response = client.get(f"/api/v1/items/{created_item['id']}")
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Test Item"
    assert data["description"] == "This is a test item"
    assert data["id"] == created_item["id"]

# tests/test_crud.py
from crud import crud_item
from schemas.item import ItemCreate

def test_create_item(db):
    item = ItemCreate(name="Test Item", description="This is a test item")
    db_item = crud_item.create_item(db, item)
    assert db_item.name == "Test Item"
    assert db_item.description == "This is a test item"

def test_get_item(db):
    item = ItemCreate(name="Test Item", description="This is a test item")
    db_item = crud_item.create_item(db, item)
    
    retrieved_item = crud_item.get_item(db, db_item.id)
    assert retrieved_item.name == "Test Item"
    assert retrieved_item.description == "This is a test item"

# Dockerfile
FROM python:3.9

WORKDIR /app

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

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

# Makefile
.PHONY: install run-dev run-prod test

install:
	pip install -r requirements.txt

run-dev:
	ENV=dev uvicorn main:app --reload

run-prod:
	ENV=prod gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

test:
	pytest

# dev.env
DEBUG=True
HOST=localhost
PORT=8000
DATABASE_URL=sqlite:///./sql_app_dev.db
LOG_LEVEL=DEBUG

# prod.env
DEBUG=False
HOST=0.0.0.0
PORT=8000
DATABASE_URL=sqlite:///./sql_app_prod.db
LOG_LEVEL=INFO

# requirements.txt
fastapi==0.68.0
uvicorn==0.15.0
sqlalchemy==1.4.23
pydantic==1.8.2
python-dotenv==0.19.0
pytest==6.2.5
gunicorn==20.1.0

windows

cd crud-app
set ENV=dev
uvicorn main:app --reload

cd crud-app
set ENV=prod
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker

linux

cd crud-app
ENV=dev uvicorn main:app --reload

cd crud-app
ENV=prod gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
profile
https://github.com/min731

0개의 댓글