728x90

prometheus_client 라이브러리는 Prometheus와 함께 사용할 수 있는 클라이언트 라이브러리로, 애플리케이션의 메트릭을 수집하고 노출하는 데 사용됨

이 라이브러리를 통해 다양한 유형의 메트릭을 생성하고 관리할 수 있음

이 라이브러리에서 메트릭 수집을 위하여 주로 사용되는 Counter, Gauge, Histogram 클래스에 대해 알아봄

Counter

Counter는 단조 증가하는 메트릭을 나타냄

주로 이벤트의 발생 횟수를 측정하는 데 사용됨

한 번 증가하면 줄어들지 않음

from prometheus_client import Counter

# Counter 메트릭 생성
requests_total = Counter('requests_total', 'Total number of requests')

# 메트릭 증가
requests_total.inc()  # 1 증가
requests_total.inc(5)  # 5 증가

Gauge

Gauge는 값이 증가하거나 감소할 수 있는 메트릭을 나타냄

현재 온도, 메모리 사용량 등과 같이 상태를 나타내는 데 사용됨

from prometheus_client import Gauge

# Gauge 메트릭 생성
temperature = Gauge('temperature', 'Current temperature')

# 메트릭 설정 및 변경
temperature.set(25.3)  # 값 설정
temperature.inc()      # 1 증가
temperature.dec(2.3)   # 2.3 감소

Histogram

Histogram은 값의 분포를 관찰하는 데 사용됨

요청 시간, 응답 크기 등의 분포를 측정하는 데 유용함

from prometheus_client import Histogram

# Histogram 메트릭 생성
request_latency = Histogram('request_latency_seconds', 'Request latency')

# 메트릭 관찰
request_latency.observe(0.5)  # 0.5초의 지연 시간 관찰
request_latency.observe(1.2)  # 1.2초의 지연 시간 관찰

종합 예제

Counter, Gauge, Histogram을 사용하여 간단한 웹 애플리케이션에서 메트릭을 수집하고 노출하는 예제

 
from prometheus_client import Counter, Gauge, Histogram, start_http_server
import time
import random

# 메트릭 정의
REQUESTS = Counter('requests_total', 'Total number of requests')
TEMPERATURE = Gauge('temperature', 'Current temperature')
LATENCY = Histogram('request_latency_seconds', 'Request latency')

# 메트릭 수집 및 노출 시작
start_http_server(8000)

while True:
    # Counter 증가
    REQUESTS.inc()

    # Gauge 설정
    TEMPERATURE.set(random.uniform(20.0, 30.0))

    # Histogram 관찰
    latency = random.uniform(0.1, 1.0)
    LATENCY.observe(latency)

    # 대기 시간
    time.sleep(1)

결론

  • Counter: 단조 증가 메트릭
  • Gauge: 값이 증가하거나 감소할 수 있는 메트릭
  • Histogram: 값의 분포를 관찰하는 메트릭

이러한 구성 요소를 사용하여 애플리케이션의 다양한 성능 지표를 수집하고 Prometheus 서버에 노출할 수 있음

728x90
728x90

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트

https://github.com/tiangolo/full-stack-fastapi-postgresql

이번 게시글에서는 DB 초기화를 어떻게 하는지 분석함

소스 개요

이 프로젝트에서는 prestart.sh을 이용하여 DB 초기화를 위한 스크립트 세 개를 수행함

#! /usr/bin/env bash

# Let the DB start
python /app/app/backend_pre_start.py # 데이터베이스 연결과 작동을 확인

# Run migrations
alembic upgrade head # 마이그레이션 수행

# Create initial data in DB
python /app/app/initial_data.py # DB 테이블에 초기 데이터 삽입

DB 초기화를 위한 스크립트 설명

backend_pre_start.py

이 스크립트는 데이터베이스가 정상적으로 작동하는지 확인하기 위해 사용될 수 있으며, 특히 Docker 컨테이너와 같은 환경에서 데이터베이스가 준비되었는지 확인할 때 유용함

init() 함수는 데이터베이스에 연결을 시도하고, 연결이 제대로 설정되었는지 확인

데이터베이스 연결이 실패할 경우 재시도할 수 있도록 @retry 데코레이터를 사용하여 init() 함수를 재시도 가능한 함수로 만듦

import logging

from sqlmodel import Session, select
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed

from app.core.db import engine

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

max_tries = 60 * 5  # 5 minutes
wait_seconds = 1


@retry(
    stop=stop_after_attempt(max_tries),
    wait=wait_fixed(wait_seconds),
    before=before_log(logger, logging.INFO),
    after=after_log(logger, logging.WARN),
)
def init() -> None:
    try:
        with Session(engine) as session:
            # Try to create session to check if DB is awake
            session.exec(select(1))
    except Exception as e:
        logger.error(e)
        raise e


def main() -> None:
    logger.info("Initializing service")
    init()
    logger.info("Service finished initializing")


if __name__ == "__main__":
    main()

 

alembic

데이터베이스 마이그레이션 도구로, DB에 테이블을 생성하거나 스키마 변경사항을 적용하는 데 사용

https://bigseok.tistory.com/entry/FastAPI-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%A7%81%EC%A0%91-%EA%B0%9C%EB%B0%9C%ED%95%9C-FastAPI-backend-alembic

 

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트 https://github.com/tiangolo/full-stack-fastapi-postgresql 이번 게시글에서는 프로젝트 데이터베이스 마이그레이션 도구인 alembic을

bigseok.tistory.com

initial_data.py

이 스크립트는 데이터베이스 테이블에 초기 데이터를 삽입하는 모듈 init_db를 실행하여 데이터베이스를 초기화

init() 함수는 데이터베이스에 연결한 후, init_db() 함수를 사용하여 초기 데이터를 삽입

import logging

from sqlmodel import Session

from app.core.db import engine, init_db

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


def init() -> None:
    with Session(engine) as session:
        init_db(session)


def main() -> None:
    logger.info("Creating initial data")
    init()
    logger.info("Initial data created")


if __name__ == "__main__":
    main()

 

app.core.db.py > init_db

이 스크립트는 데이터베이스 초기화 및 슈퍼 유저 생성을 수행하는데 사용됨

데이터베이스 테이블을 초기화하고 슈퍼 유저를 추가하는 등의 초기 설정을 수행함

설정 파일(app.core.config.settings)에서 가져온 데이터베이스 연결 정보를 사용하여 SQLAlchemy 엔진을 생성하고 애플리케이션과 데이터베이스를 연결

init_db() 함수는 SQLAlchemy의 Session 객체를 인수로 받아 초기화 작업을 수행함

  • 설정에서 지정한 슈퍼 유저(superuser)의 이메일을 사용하여 데이터베이스에서 해당 유저를 조회
  • 만약 슈퍼 유저가 존재하지 않는다면, 설정에서 지정한 초 슈퍼 유저 정보를 사용하여 UserCreate 모델을 생성
  • 생성된 슈퍼 유저 정보를 사용하여 crud.create_user() 함수를 호출하여 초 슈퍼 유저를 데이터베이스에 추가

이 프로젝트는 기본적으로 Alembic 마이그레이션을 사용하여 테이블을 생성하지만,

마이그레이션을 사용하지 않는다면 주석 처리된 코드를 해제하여 SQLAlchemy의 SQLModel.metadata.create_all() 메서드를 주석 해제하여 모든 모델에 대한 테이블을 생성할 수 있도록 함

# init_db
from sqlmodel import Session, create_engine, select

from app import crud
from app.core.config import settings
from app.models import User, UserCreate

engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))


# make sure all SQLModel models are imported (app.models) before initializing DB
# otherwise, SQLModel might fail to initialize relationships properly
# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28


def init_db(session: Session) -> None:
    # Tables should be created with Alembic migrations
    # But if you don't want to use migrations, create
    # the tables un-commenting the next lines
    # from sqlmodel import SQLModel

    # from app.core.engine import engine
    # This works because the models are already imported and registered from app.models
    # SQLModel.metadata.create_all(engine)

    user = session.exec(
        select(User).where(User.email == settings.FIRST_SUPERUSER)
    ).first()
    if not user:
        user_in = UserCreate(
            email=settings.FIRST_SUPERUSER,
            password=settings.FIRST_SUPERUSER_PASSWORD,
            is_superuser=True,
        )
        user = crud.create_user(session=session, user_create=user_in)

 

참고 : FastAPI 개발자가 직접 개발한 FastAPI backend 시리즈

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트

https://github.com/tiangolo/full-stack-fastapi-postgresql

이번 게시글에서는 설정파일을 어떻게 구성하고 구조화하였는지 분석하려고 함

 

환경변수는 pydantic_settings를 기반으로 구성되어있으므로 pydantic_settings를 모른다면 pydantic_settings를 이용한 환경설정을 먼저 보고 오는 것을 권장함

소스 개요

config.py 파일 경로는 다음과 같음

```

app

└──  core # 데이터베이스 마이그레이션을 관리하는 Alembic 설정 파일 및 스크립트가 저장된 디렉토리

         └── config.py # 앱 전역 설정파일

```

import secrets
from typing import Any

from pydantic import (
    AnyHttpUrl,
    HttpUrl,
    PostgresDsn,
    ValidationInfo,
    field_validator,
)
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    API_V1_STR: str = "/api/v1"
    SECRET_KEY: str = secrets.token_urlsafe(32)
    # 60 minutes * 24 hours * 8 days = 8 days
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
    SERVER_HOST: AnyHttpUrl
    # BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
    # e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
    # "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
    BACKEND_CORS_ORIGINS: list[AnyHttpUrl] | str = []

    @field_validator("BACKEND_CORS_ORIGINS", mode="before")
    @classmethod
    def assemble_cors_origins(cls, v: str | list[str]) -> list[str] | str:
        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)

    PROJECT_NAME: str
    SENTRY_DSN: HttpUrl | None = None

    @field_validator("SENTRY_DSN", mode="before")
    @classmethod
    def sentry_dsn_can_be_blank(cls, v: str) -> str | None:
        if not v:
            return None
        return v

    POSTGRES_SERVER: str
    POSTGRES_USER: str
    POSTGRES_PASSWORD: str
    POSTGRES_DB: str
    SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None

    @field_validator("SQLALCHEMY_DATABASE_URI", mode="before")
    def assemble_db_connection(cls, v: str | None, info: ValidationInfo) -> Any:
        if isinstance(v, str):
            return v
        return PostgresDsn.build(
            scheme="postgresql+psycopg",
            username=info.data.get("POSTGRES_USER"),
            password=info.data.get("POSTGRES_PASSWORD"),
            host=info.data.get("POSTGRES_SERVER"),
            path=f"{info.data.get('POSTGRES_DB') or ''}",
        )

    SMTP_TLS: bool = True
    SMTP_PORT: int | None = None
    SMTP_HOST: str | None = None
    SMTP_USER: str | None = None
    SMTP_PASSWORD: str | None = None
    # TODO: update type to EmailStr when sqlmodel supports it
    EMAILS_FROM_EMAIL: str | None = None
    EMAILS_FROM_NAME: str | None = None

    @field_validator("EMAILS_FROM_NAME")
    def get_project_name(cls, v: str | None, info: ValidationInfo) -> str:
        if not v:
            return info.data["PROJECT_NAME"]
        return v

    EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
    EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
    EMAILS_ENABLED: bool = False

    @field_validator("EMAILS_ENABLED", mode="before")
    def get_emails_enabled(cls, v: bool, info: ValidationInfo) -> bool:
        return bool(
            info.data.get("SMTP_HOST")
            and info.data.get("SMTP_PORT")
            and info.data.get("EMAILS_FROM_EMAIL")
        )

    # TODO: update type to EmailStr when sqlmodel supports it
    EMAIL_TEST_USER: str = "test@example.com"
    # TODO: update type to EmailStr when sqlmodel supports it
    FIRST_SUPERUSER: str
    FIRST_SUPERUSER_PASSWORD: str
    USERS_OPEN_REGISTRATION: bool = False
    model_config = SettingsConfigDict(case_sensitive=True)


settings = Settings()

주요 설정

  • 일반 설정
    • API_V1_STR: API 버전을 지정하는 문자열
    • SECRET_KEY: 인증 토큰 및 세션 등을 위한 시크릿 키
    • ACCESS_TOKEN_EXPIRE_MINUTES: 액세스 토큰 만료 시간(분 단위)
    • PROJECT_NAME: 프로젝트의 이름
    • EMAIL_RESET_TOKEN_EXPIRE_HOURS: 이메일 리셋 토큰의 만료 시간(시간 단위)
    • EMAIL_TEMPLATES_DIR: 이메일 템플릿 디렉토리 경로
    • EMAILS_ENABLED: 이메일 전송 기능 활성화 여부
    • EMAIL_TEST_USER: 이메일 테스트 사용자
    • FIRST_SUPERUSER: 최초 슈퍼 사용자 이름
    • FIRST_SUPERUSER_PASSWORD: 최초 슈퍼 사용자 비밀번호
    • USERS_OPEN_REGISTRATION: 사용자의 개방된 가입 여부
  • CORS 관련 설정
    • BACKEND_CORS_ORIGINS: 허용된 CORS 원본 목록.
  • 데이터베이스 관련 설정
    • POSTGRES_SERVER: PostgreSQL 서버 호스트
    • POSTGRES_USER: PostgreSQL 사용자 이름
    • POSTGRES_PASSWORD: PostgreSQL 비밀번호
    • POSTGRES_DB: PostgreSQL 데이터베이스 이름
    • SQLALCHEMY_DATABASE_URI: SQLAlchemy를 위한 PostgreSQL 데이터베이스 URI
  • 이메일 관련 설정
    • SMTP_TLS: SMTP TLS 활성화 여부
    • SMTP_PORT: SMTP 포트
    • SMTP_HOST: SMTP 호스트
    • SMTP_USER: SMTP 사용자
    • SMTP_PASSWORD: SMTP 비밀번호
    • EMAILS_FROM_EMAIL: 이메일 발신자 이메일 주소
    • EMAILS_FROM_NAME: 이메일 발신자 이름
  • 서버 관련 설정
    • SERVER_HOST: 서버 호스트 URL
  • 로그 및 모니터링 관련 설정
    • SENTRY_DSN: Sentry 프로젝트 DSN

소스 분석

Python에서 환경 설정을 다루는 데 사용되는 Pydantic 라이브러리를 사용하여 구조화함

Pydantic 및 pydantic_settings 라이브러리를 통해 설정 값을 관리하고 유효성을 검사하는 데 사용

Pydantic을 사용하면 설정을 타입 안정성이 있는 Python 객체로 정의할 수 있음

 

설정 파일에 있는 일부 필드들은 다른 필드의 값에 의존하는데, 이를 위하여 @field_validator 데코레이터와 ValidationInfo 객체를 이용

위의 코드를 보면 EMAILS_FROM_NAME 필드가 PROJECT_NAME 필드의 값을 기본값으로 사용하도록 설정되어 있음

이를 위해 ValidationInfo 객체를 사용하여 필요한 정보를 얻고, 필드 값을 조작하여 반환하고 있음

 

타입 힌트(from typing import Any) 및 Pydantic의 타입들(AnyHttpUrl, PostgresDsn, ValidationInfo)을 사용하여 데이터의 형식을 지정하고 유효성을 검사하는 데 사용

참고

Fastapi 설정 docs : https://fastapi.tiangolo.com/ru/advanced/settings/

 

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

다양한 프레임워크와 ORM 도구에서 데이터 모델을 정의하는데 사용되는 파일로, 데이터베이스 테이블 구조를 코드로 표현

주 구성요소

  • 테이블을 나타내는 클래스 정의
  • 각 클래스 내에 속성 정의 (테이블 컬럼 정의)
  • 테이블 간의 관계 정의 (Foreign key, one to one, many to many 등)
  • 모델의 메타데이터 정의 
  • 필요한 인덱스, 제약조건 등 정의
  • 모델 검증을 위한 로직 (잘못된 입력 방지 및 특정 규칙 적용)
  • 데이터베이스 마이그레이션을 위한 설정
from sqlmodel import Field, Relationship, SQLModel


# Shared properties
# TODO replace email str with EmailStr when sqlmodel supports it
class UserBase(SQLModel):
    email: str = Field(unique=True, index=True)
    is_active: bool = True
    is_superuser: bool = False
    full_name: str | None = None


# Properties to receive via API on creation
class UserCreate(UserBase):
    password: str


# TODO replace email str with EmailStr when sqlmodel supports it
class UserCreateOpen(SQLModel):
    email: str
    password: str
    full_name: str | None = None


# Properties to receive via API on update, all are optional
# TODO replace email str with EmailStr when sqlmodel supports it
class UserUpdate(UserBase):
    email: str | None = None
    password: str | None = None


# TODO replace email str with EmailStr when sqlmodel supports it
class UserUpdateMe(SQLModel):
    full_name: str | None = None
    email: str | None = None


class UpdatePassword(SQLModel):
    current_password: str
    new_password: str


# Database model, database table inferred from class name
class User(UserBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    hashed_password: str
    items: list["Item"] = Relationship(back_populates="owner")


# Properties to return via API, id is always required
class UserOut(UserBase):
    id: int


class UsersOut(SQLModel):
    data: list[UserOut]
    count: int


# Shared properties
class ItemBase(SQLModel):
    title: str
    description: str | None = None


# Properties to receive on item creation
class ItemCreate(ItemBase):
    title: str


# Properties to receive on item update
class ItemUpdate(ItemBase):
    title: str | None = None


# Database model, database table inferred from class name
class Item(ItemBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    owner_id: int | None = Field(default=None, foreign_key="user.id", nullable=False)
    owner: User | None = Relationship(back_populates="items")


# Properties to return via API, id is always required
class ItemOut(ItemBase):
    id: int
    owner_id: int


class ItemsOut(SQLModel):
    data: list[ItemOut]
    count: int


# Generic message
class Message(SQLModel):
    message: str


# JSON payload containing access token
class Token(SQLModel):
    access_token: str
    token_type: str = "bearer"


# Contents of JWT token
class TokenPayload(SQLModel):
    sub: int | None = None


class NewPassword(SQLModel):
    token: str
    new_password: str

 

특징 및 장점

  • 코드가 간결하며, Pydantic 및 SQLModel을 사용하여 모델을 정의하고 데이터베이스 테이블을 생성
  • UserBase를 기반으로 하는 다양한 파생 클래스들이 있는데 공통된 속성을 하위 클래스에서 확장하여 사용하는 방식이 효과적이며, 코드 재사용성을 높임
  • Pydantic 모델은 FastAPI에서 사용하기에 이상적이며, API의 요청 및 응답에 대한 유효성 검사 및 자동 문서화를 제공
  • 모델 정의에서 데이터베이스 테이블 정의까지 일관된 패턴을 사용하고 있어 코드의 이해와 유지보수에 유리
  • UsersOut 및 ItemsOut과 같은 응답 모델을 정의하여 API 응답에 대한 구조를 명확하게 나타냄

주의할 점

  • 클래스 이름을 기반으로 한 암시적인 테이블 이름이 사용되고 있음 (명시적인 테이블 이름 정의가 필요할 수 있음)
  • 모델 정의에 클래스 계층 구조가 있어 데이터베이스와 API 간의 복잡한 관계에서는 코드의 이해가 어려워질 수 있음
  • SQLModel이 EmailStr를 지원하지 않을 때까지 email 속성은 일반 문자열로 남아있음 (주석에도 명시됨)
  • owner_id와 user.id 간의 Foreign Key 관계가 암시적으로 정의되어 있음 (명시적인 Foreign Key 제약이 필요할 수 있음)

참고 : FastAPI 개발자가 직접 개발한 FastAPI backend 시리즈

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트

https://github.com/tiangolo/full-stack-fastapi-postgresql

이번 게시글에서는 프로젝트 데이터베이스 마이그레이션 도구인 alembic을 어떻게 적용하였는지 분석하려고 함

 

alembic 관련 파일은 아래와 같음

```

.env # 환경 변수 설정이 담긴 파일 → DB 설정을 이 파일에서 확인 가능

alembic.ini # Alembic의 설정 파일로, 데이터베이스 마이그레이션 설정을 정의 

app

└──  alembic # 데이터베이스 마이그레이션을 관리하는 Alembic 설정 파일 및 스크립트가 저장된 디렉토리

           ├──  env.py # alembic 실행 시 실행되는 파일로, SQLAIchemy의 Engine을 설정하고 마이그레이션 스크립트 실행
           └──  versions # 버전 별 데이터베이스 마이그레이션 스크립트가 저장된 디렉토리

                   └── e2412789c190_initialize_models.py # 데이터 베이스 테이블 및 인덱스 생성 스크립트

└──  models.py # ORM 도구에서 데이터 모델을 정의하는데 사용되는 파일로, 이 파일의 모델과 DB 차이를 바탕으로 마이그레이션 스크립트가 생성됨

```

.env

파이썬에서는 프로젝트 별로 환경변수를 관리하기 위하여 .env파일을 널리 사용함

.env파일에 설정된 alembic에 사용되는 환경변수는 아래와 같음

# Postgres
POSTGRES_SERVER=db
POSTGRES_USER=postgres
POSTGRES_DB=app
POSTGRES_PASSWORD=changethis

alembic.ini

alembic.ini 파일은 Alembic 데이터베이스 마이그레이션 도구의 설정 파일로, 주로 데이터베이스 연결 및 마이그레이션 관련 설정을 포함

  • 일반적으로 사용되는 설정
    • 데이터베이스 연결 정보 설정 (sqlalchemy.url)
    • 마이그레이션 스크립트 경로 설정 (script_location)
    • 로깅 설정 (loggers, handlers, formatters)
    • 버전 관리 설정 (version_table)
    • 그 외 커스텀 설정

tiangolo 백엔드 프로젝트의 alembic.ini는 아래와 같은 설정으로 구성되어 있음

# A generic, single database configuration.

[alembic]
script_location = app/alembic # 마이그레이션 스크립트가 있는 디렉토리의 경로를 지정

[loggers]
keys = root,sqlalchemy,alembic # 설정될 로거의 이름을 지정

[handlers]
keys = console # "console"이라는 핸들러가 설정되어 있다는 것을 지정

[formatters]
keys = generic # "generic"이라는 포매터가 설정되어 있다는 것을 지정

[logger_root] # "root"에 대한 로거를 구성
level = WARN
handlers = console
qualname = 

[logger_sqlalchemy] # "sqlalchemy"에 대한 로거를 구성
level = WARN 
handlers =
qualname = sqlalchemy.engine

[logger_alembic] # "alembic"에 대한 로거를 구성
level = INFO
handlers =
qualname = alembic

[handler_console] # 콘솔 핸들러를 구성 (콘솔 핸들러는 로그 메시지를 콘솔에 출력)
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic] #  "generic" 포매터에 대한 설정을 정의
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

env.py

 alembic 실행 시 실행되는 파일로, SQLAIchemy의 Engine을 설정하고 마이그레이션 스크립트 실행

  • 주요 작업
    • 모듈 및 패키지 가져오기
    • context.config를 사용하여 Alembic Config 객체를 설정
      이 객체는 사용 중인
      .ini 파일 내의 값을 제공
    • fileConfig(config.config_file_name)을 호출하여 Python 로깅을 설정
      .ini 파일의 로깅 구성을 기반으로 로그 기능을 활성화
    • app.models에서 SQLModel을 가져와 target_metadata로 설정
      마이그레이션 스크립트에서 사용할 데이터베이스 모델의 메타데이터를 지정
    • get_url함수는 .env(환경 변수)를 통해 PostgreSQL 데이터베이스 연결 URL을 생성
    • run_migrations_offline 함수는 오프라인 모드에서 마이그레이션을 실행
    • run_migrations_online 함수는 온라인 모드에서 마이그레이션 실행
    • context.is_offline_mode()를 사용하여 현재 마이그레이션 모드가 오프라인인지 확인하고, 그에 따라 run_migrations_offline 또는 run_migrations_online 함수를 호출하여 마이그레이션을 실행

versions / e2412789c190_initialize_models.py

데이터 베이스 테이블 및 인덱스 생성 스크립트

일반적으로 alembic을 이용하여 마이그레이션 스크립트를 자동적으로 생성하면 models.py파일을 기반으로 테이블 생성/삭제 스크립트만 생성됨

index 생성 스크립트는 수동으로 추가했을 것으로 예상됨

alembic이 뭔지 모르는 경우 alembic 소개 및 기초 사용법 포스팅 참고

"""Initialize models

Revision ID: e2412789c190
Revises:
Create Date: 2023-11-24 22:55:43.195942

"""
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from alembic import op

# revision identifiers, used by Alembic.
revision = "e2412789c190"
down_revision = None
branch_labels = None
depends_on = None


def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table(
        "user",
        sa.Column("email", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
        sa.Column("is_active", sa.Boolean(), nullable=False),
        sa.Column("is_superuser", sa.Boolean(), nullable=False),
        sa.Column("full_name", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column(
            "hashed_password", sqlmodel.sql.sqltypes.AutoString(), nullable=False
        ),
        sa.PrimaryKeyConstraint("id"),
    )
    op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
    op.create_table(
        "item",
        sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
        sa.Column("id", sa.Integer(), nullable=False),
        sa.Column("title", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
        sa.Column("owner_id", sa.Integer(), nullable=False),
        sa.ForeignKeyConstraint(
            ["owner_id"],
            ["user.id"],
        ),
        sa.PrimaryKeyConstraint("id"),
    )
    # ### end Alembic commands ###


def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table("item")
    op.drop_index(op.f("ix_user_email"), table_name="user")
    op.drop_table("user")
    # ### end Alembic commands ###

 

참고 : FastAPI 개발자가 직접 개발한 FastAPI backend 시리즈

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

Flask-Migrate는 db와 sqlalchemy models.py의 차이를 추적하여 업그레이드/다운그레이드 스크립트 자동으로 생성해줌

 

Flask-Migrate 소개 및 기초 사용법

Flask-Migrate란? Flask-Migrate는 SQLAlchemy를 사용하는 Flask 애플리케이션에서 데이터베이스 스키마를 효과적으로 관리하기 위한 extension Flask-Migrate는 Alembic이라는 데이터베이스 마이그레이션 툴을 내부

bigseok.tistory.com

 

만약에 서비스에 필요한 DB table 초기 데이터도 함께 관리하고 싶다면 마이그레이션 스크립트에 수동으로  추가해주어야 함

 


1. DB 초기화

flask db init

위의 명령어를 수행한 위치에 migrations 폴더 자동으로 생성됨

2. 마이그레이션 스크립트 생성

flask db migrate -m "Initial migration."

migrations/versions 폴더 하위에 {revision}_{message}.py 스크립트가 자동으로 생성됨

3. 마이그레이션 스크립트에 수동으로 초기데이터 변경내용 추가

마이그레이션 스크립트 업그레이드 함수에 초기데이터삽입 코드 추가예시

# /migrations/versions/{revision}_{message}.py
def upgrade():
    ...생략...
    # ### end Alembic commands ###
    add_initial_data()

def add_initial_data():
    op.bulk_insert(
        sa.table('tb_users',
            sa.column('name'),
            sa.column('user_id'),
            sa.column('password'),
            sa.column('admin_yn'),
            sa.column('email')
        ),
        [
            {
                'name': '관리자',
                'user_id': 'admin',
                'password': 'admin123!@#',
                'admin_yn': 'Y',
                'email': 'admin@test.com'
            }
        ]
    )

 

  • alembic op모듈 주요 기능
    1. 테이블 작업
      • op.create_table(): 새로운 테이블을 생성
      • op.drop_table(): 기존 테이블을 삭제
      • op.add_column(): 기존 테이블에 새로운 열을 추가
      • op.drop_column(): 기존 테이블에서 열을 삭제
      • op.alter_column(): 열의 속성을 변경
    2. 인덱스 작업
      • op.create_index(): 새로운 인덱스를 생성
      • op.drop_index(): 기존 인덱스를 삭제
    3. 데이터 작업
      • op.execute(): SQL 문을 직접 실행
    4. 제약 조건 작업
      • op.create_foreign_key(): 새로운 외래 키 제약 조건을 생성
      • op.create_primary_key(): 새로운 기본 키 제약 조건을 생성
      • op.create_check_constraint(): 새로운 체크 제약 조건을 생성
    5. 기타 작업
      • op.rename_table(): 기존 테이블의 이름을 변경
      • op.rename_column(): 기존 열의 이름을 변경
      • op.create_sequence(): 새로운 데이터베이스 시퀀스를 생성
    6. 대량 데이터 작업
      • op.bulk_insert(): 테이블에 대량 삽입을 수행
      • op.bulk_update(): 테이블에서 대량 업데이트를 수행
728x90
728x90

Flask-Migrate란?

Flask-Migrate는 SQLAlchemy를 사용하는 Flask 애플리케이션에서 데이터베이스 스키마를 효과적으로 관리하기 위한 extension

Flask-Migrate는 Alembic이라는 데이터베이스 마이그레이션 툴을 내부적으로 사용

Flask 애플리케이션과의 원활한 통합을 제공하여 데이터베이스 마이그레이션을 더욱 편리하게 관리 가능

https://flask-migrate.readthedocs.io/en/latest/#why-use-flask-migrate-vs-alembic-directly

Flask-Migrate 사용법

설치

pip install Flask-Migrate

 

환경변수 설정

flask app 실행파일명이 app.py나 wsgi.py가 아닌 이상 환경변수(.env파일)에 flask app 실행파일명 설정 필수

FLASK_APP= "__run__.py"

 

마이그레이션 (alembic) 초기화

flask db init

 

마이그레이션 스크립트 생성

flask db migrate -m "Initial migration."

현재의 SQLAlchemy 모델과 데이터베이스 상태를 비교하여 마이그레이션 스크립트를 자동으로 생성

 

버전업 (DB에 스키마 변경사항 반영)

flask db upgrade [--sql] [--tag TAG] <revision>
# 데이터베이스를 업그레이드 revision주어지지 않으면 최신버전

 

버전 다운그레이드 (최초 버전으로)

flask db downgrade [--sql] [--tag TAG] <revision>
# 데이터베이스를 다운그레이드, revision이 주어지지 않으면 -1

 

 

728x90
728x90

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트

https://github.com/tiangolo/full-stack-fastapi-postgresql

이번 게시글에서는 프로젝트 설정파일인 project.toml을 분석하려고 함

 

pyproject.toml 설명

[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Admin <admin@example.com>"]

[tool.poetry.dependencies]
python = "^3.10"
uvicorn = "^0.24.0.post1"
fastapi = "^0.104.1"
python-multipart = "^0.0.6"
email-validator = "^2.1.0.post1"
celery = "^5.3.5"
passlib = {extras = ["bcrypt"], version = "^1.7.4"}
tenacity = "^8.2.3"
pydantic = ">2.0"
emails = "^0.6"
gunicorn = "^21.2.0"
jinja2 = "^3.1.2"
alembic = "^1.12.1"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
httpx = "^0.25.1"
psycopg = {extras = ["binary"], version = "^3.1.13"}
sqlmodel = "^0.0.16"
# Pin bcrypt until passlib supports the latest
bcrypt = "4.0.1"
pydantic-settings = "^2.2.1"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
pytest-cov = "^4.1.0"
mypy = "^1.8.0"
ruff = "^0.2.2"
pre-commit = "^3.6.2"

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
line_length = 88

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"

[tool.mypy]
strict = true

[tool.ruff]
target-version = "py310"

[tool.ruff.lint]
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "I",  # isort
    "B",  # flake8-bugbear
    "C4",  # flake8-comprehensions
    "UP",  # pyupgrade
]
ignore = [
    "E501",  # line too long, handled by black
    "B008",  # do not perform function calls in argument defaults
    "W191",  # indentation contains tabs
    "B904",  # Allow raising exceptions without from e, for HTTPException
]

[tool.ruff.lint.pyupgrade]
# Preserve types, even if a file imports `from __future__ import annotations`.
keep-runtime-typing = true

 

  • [tool.poetry] 섹션
    • 프로젝트의 기본 정보를 정의
    • name: 프로젝트의 이름
    • version: 프로젝트의 버전
    • description: 프로젝트에 대한 간단한 설명
    • authors: 프로젝트 저자들의 이메일 주소를 포함한 리스트
  • [tool.poetry.dependencies] 섹션
    • 여러 Python 라이브러리와 프레임워크에 대한 의존성을 정의 (requirements.txt와 유사한 역할)
    • python: 사용할 Python 버전
    • uvicorn, fastapi, python-multipart, ...: FastAPI와 관련된 라이브러리 및 도구들
    • pydantic: 데이터 유효성 검사 및 설정을 위한 라이브러리
    • gunicorn, jinja2, alembic, ...: 다양한 라이브러리 및 도구들
    • bcrypt: 비밀번호 해싱을 위한 라이브러리
    • pydantic-settings: Pydantic 설정을 관리하기 위한 도구
  • [tool.isort] 섹션
    • 코드를 정렬하기 위한 isort 설정
    • 참고 : isort는 Python 코드 파일의 import 문을 정렬해주는 도구
  • [build-system] 섹션
    • 프로젝트 빌드에 사용되는 도구 및 설정
  • [tool.mypy] 섹션
    • mypy 정적 타입 검사 도구의 설정
    • 참고 : mypy는 Python 코드의 정적 타입 검사를 수행하는 도구로 런타임 이전에 코드를 분석하여 타입 오류를 찾아내고 이를 사전에 방지
  • [tool.ruff] 섹션
    • ruff 코드 포맷터 및 정적 분석 도구의 설정
    • 참고 : ruff는 rust로 만든 매우 빠른 Python linter
    • 참고 : linter는 코드의 오류나 버그가 있는지 확인하고 정해진 규칙을 잘 지키고 있는지에 대한 것들을 개발하면서 확인 및 점검을 하기 위해 사용하는 도구

 

pyproject.toml을 이용한 poetry 가상환경 설정

pyproject.toml이 있는 경우 poetry를 이용하여 간단하게 가상환경을 세팅할 수 있음

pip install --upgrade pip
pip install poetry
poetry install
poetry shell

 

가상환경을 세팅한 후에는 vscode에서 F1Python: Select Interpreter 를 클릭하여 poetry 가상환경 사용 가능

 

참고 : FastAPI 개발자가 직접 개발한 FastAPI backend 시리즈

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

FastAPI 개발자 tiangolo는 FastAPI를 어떻게 사용할까 궁금해서 찾아본 프로젝트

https://github.com/tiangolo/full-stack-fastapi-postgresql

 

FastAPI(백엔드)의 기본적인 프로젝트 구조는 아래와 같음

```

app
├── alembic # 데이터베이스 마이그레이션을 관리하는 Alembic 설정 파일 및 스크립트가 저장된 디렉토리
├── api # FastAPI 애플리케이션의 API 엔드포인트 및 라우터가 정의된 디렉토리
├── core # 애플리케이션의 핵심 기능 및 설정이 정의된 모듈이 위치하는 디렉토리
├── schemas # Pydantic 스키마 정의가 포함된 디렉토리로, 데이터의 유효성 검사와 API 요청 및 응답의 구조를 정의
├── email-templates # 이메일 템플릿 파일이 저장된 디렉토리
├── tests # 테스트 파일이 위치한 디렉토리
├── __init__.py # Python에서 패키지로 인식되도록 하는 빈 __init__.py 파일
├── backend_pre_start.py # FastAPI 애플리케이션 시작 전에 실행할 코드가 정의된 파일
├── celeryworker_pre_start.py # Celery worker 시작 전에 실행할 코드가 정의된 파일
├── initial_data.py # 애플리케이션 초기 데이터를 설정하는 스크립트 파일
├── tests_pre_start.py # 테스트 시작 전에 실행할 코드가 정의된 파일
├── main.py # FastAPI 애플리케이션의 진입점이 되는 파일로, API 라우터를 구성하고 애플리케이션을 실행
├── models.py # SQLAlchemy 모델이 정의된 파일
├── crud.py # 데이터베이스 CRUD(Create, Read, Update, Delete) 연산을 수행하는 함수가 정의된 파일
├── utils.py # 애플리케이션에서 사용되는 유틸리티 함수가 정의된 파일
└── worker.py # Celery worker의 작업을 정의하는 파일

pyproject.toml # Poetry 프로젝트의 설정 파일로, 의존성 및 프로젝트 메타데이터를 정의
.env # 환경 변수 설정이 담긴 파일
.dockerignore # Docker 이미지 빌드 시 제외할 파일 및 디렉토리를 지정하는 파일
.gitignore # Git으로 추적하지 않을 파일 및 디렉토리를 지정하는 파일
alembic.ini # Alembic의 설정 파일로, 데이터베이스 마이그레이션 설정을 정의 # 
backend.dockerfile # FastAPI 애플리케이션을 위한 Docker 이미지를 빌드하는 데 사용되는 Docker 파일
celeryworker.dockerfile # Celery worker를 위한 Docker 이미지를 빌드하는 데 사용되는 Docker 파일
prestart.sh # FastAPI 애플리케이션 시작 전에 실행할 스크립트 파일
tests-start.sh # 테스트 시작 시 실행할 스크립트 파일
worker-start.sh # Celery worker 시작 시 실행할 스크립트 파일

```

 

참고 : FastAPI 개발자가 직접 개발한 FastAPI backend 시리즈

FastAPI 개발자가 직접 개발한 FastAPI backend 프로젝트 구조

FastAPI 개발자가 직접 개발한 FastAPI backend alembic

FastAPI 개발자가 직접 개발한 FastAPI backend DB 초기화 방법

FastAPI 개발자가 직접 개발한 FastAPI backend pyproject.toml

FastAPI 개발자가 직접 개발한 FastAPI backend 설정파일 config.py

FastAPI 개발자가 직접 개발한 FastAPI backend 데이터모델 models.py

 

728x90
728x90

WSGI(웹 서버 게이트웨이 인터페이스)와 ASGI(비동기 서버 게이트웨이 인터페이스)는 웹 애플리케이션 서버와 웹 애플리케이션 간의 표준화된 인터페이스를 제공하는 목적으로 사용되는 두 가지 프로토콜

 

WSGI는 주로 동기적인 애플리케이션에 사용됨

ASGI는 비동기적이며 실시간 기능이 필요한 애플리케이션에 사용하며, ASGI는 특히 웹 소켓을 이용한 양방향 통신과 같은 기능을 지원하여 실시간 애플리케이션을 개발할 때 유용함

 

WSGI (Web Server Gateway Interface):

  1. 동기적인 처리: WSGI는 동기적인 방식으로 동작하며, 요청이 처리될 때까지 블록 됨 (하나의 요청이 완전히 처리되기 전에 다른 요청을 처리하지 못하고 대기)
  2. 대부분의 경우에 적합: WSGI는 대부분의 웹 애플리케이션에 적합하며, 간단한 애플리케이션에서 잘 동작함
  3. 웹 서버와의 통합 용이성: WSGI 애플리케이션은 여러 웹 서버와 쉽게 통합될 수 있음
  4. 플라스크 프레임워크에서 사용

ASGI (Asynchronous Server Gateway Interface):

  1. 비동기적인 처리: ASGI는 비동기적으로 동작하며, 여러 요청을 동시에 처리 가능 → 실시간 기능이 필요한 애플리케이션에 적합
  2. 실시간 기능 및 이벤트 처리: ASGI는 웹 소켓을 비롯한 실시간 기능 및 이벤트 기능을 지원하여 실시간 채팅, 게임 등과 같은 애플리케이션에 유용함
  3. Django, FastAPI와 같은 프레임워크에서 비동기적으로 동작하는 애플리케이션을 지원하는 데 사용
728x90

+ Recent posts