Loading...
Loading...
02-reusable-code-python/models/pydantic_patterns.py
"""
Pydantic v2 검증/설정/직렬화 패턴
@source GitHub-커뮤니티
@extracted 2026-02-16
@version 1.0.0
의존성:
- pydantic (필수, pip install pydantic)
- pydantic-settings (권장, pip install pydantic-settings)
사용법:
from models.pydantic_patterns import BaseSchema, PaginatedResponse
class UserCreate(BaseSchema):
name: str
email: str
"""
import logging
from datetime import datetime
from typing import Any, Generic, TypeVar
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
logger = logging.getLogger(__name__)
T = TypeVar("T")
# ============================================
# 기본 스키마
# ============================================
class BaseSchema(BaseModel):
"""공통 기본 스키마
- snake_case ↔ camelCase 자동 변환
- 불변 객체
- 추가 필드 금지
Example:
class UserCreate(BaseSchema):
full_name: str
email_address: str
# JSON 직렬화 시: {"fullName": "...", "emailAddress": "..."}
user = UserCreate(full_name="홍길동", email_address="hong@test.com")
user.model_dump(by_alias=True)
"""
model_config = ConfigDict(
# snake_case → camelCase 자동 변환
populate_by_name=True,
alias_generator=lambda s: "".join(
word.capitalize() if i > 0 else word
for i, word in enumerate(s.split("_"))
),
# 추가 필드 금지
extra="forbid",
# 불변
frozen=True,
# 검증 모드
strict=False,
)
class TimestampMixin(BaseModel):
"""생성/수정 타임스탬프 믹스인"""
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
# ============================================
# 페이지네이션 응답
# ============================================
class PaginatedResponse(BaseModel, Generic[T]):
"""페이지네이션 응답 스키마
Example:
response = PaginatedResponse[User](
data=users,
total=100,
page=1,
limit=10,
)
"""
data: list[T]
total: int
page: int
limit: int
total_pages: int = 0
@model_validator(mode="after")
def calculate_total_pages(self):
"""총 페이지 수 자동 계산"""
if self.limit > 0:
object.__setattr__(
self, "total_pages", (self.total + self.limit - 1) // self.limit
)
return self
@property
def has_next(self) -> bool:
"""다음 페이지 존재 여부"""
return self.page < self.total_pages
@property
def has_prev(self) -> bool:
"""이전 페이지 존재 여부"""
return self.page > 1
# ============================================
# API 응답 래퍼
# ============================================
class ApiResponse(BaseModel, Generic[T]):
"""표준 API 응답 래퍼
Example:
ApiResponse.success(data=user)
ApiResponse.error(message="사용자를 찾을 수 없습니다", code="NOT_FOUND")
"""
success: bool
data: T | None = None
message: str | None = None
code: str | None = None
errors: dict[str, list[str]] | None = None
@classmethod
def ok(cls, data: Any, message: str | None = None) -> "ApiResponse":
"""성공 응답 생성"""
return cls(success=True, data=data, message=message)
@classmethod
def error(
cls,
message: str,
code: str = "ERROR",
errors: dict[str, list[str]] | None = None,
) -> "ApiResponse":
"""에러 응답 생성"""
return cls(success=False, message=message, code=code, errors=errors)
# ============================================
# 검증 예제
# ============================================
class UserCreate(BaseSchema):
"""사용자 생성 스키마 예제
Example:
user = UserCreate(
name="홍길동",
email="hong@test.com",
password="MyP@ss123",
)
"""
name: str = Field(min_length=2, max_length=50)
email: str = Field(max_length=254)
password: str = Field(min_length=8, max_length=128)
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""이메일 형식 검증"""
if "@" not in v:
raise ValueError("유효한 이메일 주소를 입력하세요")
return v.lower().strip()
@field_validator("name")
@classmethod
def validate_name(cls, v: str) -> str:
"""이름 공백 정리"""
return v.strip()
# ============================================
# 환경 설정 (pydantic-settings)
# ============================================
# pydantic-settings가 설치된 경우에만 사용
try:
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppSettings(BaseSettings):
"""애플리케이션 설정 (환경변수에서 자동 로드)
Example:
settings = AppSettings() # .env 파일 + 환경변수에서 로드
print(settings.database_url)
"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
)
# 필수 설정
database_url: str
secret_key: str
# 선택 설정 (기본값)
debug: bool = False
host: str = "0.0.0.0"
port: int = 8000
log_level: str = "INFO"
except ImportError:
logger.debug("pydantic-settings 미설치: AppSettings 사용 불가")