프로그래밍 언어/파이썬

FastAPI를 이용한 TDD 개발 - 3

hyeseong-dev 2024. 3. 12. 13:25

Postgres

Postgres 설정을 위해서 docker-compose.yml 파일에 새로운 서비스를 명세해야합니다. 

그리고 asyncpg 파이썬 라이브러리 설치도 해야합니다. 

 

project 디렉토리 아래 db 디렉토리를 만들고 그 안에 create.sql 파일을 생성하며 아래와 같이 명세합니다. 

CREATE DATABASE web_dev;
CREATE DATABASE web_test;

 

Dokcerfile을 같은 디렉토리에서 만들어 줍니다. 

# pull official base image
FROM postgres:16

# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d

 

DB 컨테이너의 `docker-entrypoint-initdb.d` 디렉토리에 `create.sql`파일을 넣어두면 이 파일을 실행을 통해서 초기화가 수행됩니다. 

`web-db`라는 서비스 명을 명세해줍니다. 

version: '3.8'

services:

  web:
    build: ./project
    command: uvicorn app.main:app --reload --workers 1 --host 0.0.0.0 --port 8000
    volumes:
      - ./project:/usr/src/app
    ports:
      - 8004:8000
    environment:
      - ENVIRONMENT=dev
      - TESTING=0
      - DATABASE_URL=postgres://postgres:postgres@web-db:5432/web_dev        # new
      - DATABASE_TEST_URL=postgres://postgres:postgres@web-db:5432/web_test  # new
    depends_on:   # new
      - web-db

  # new
  web-db:
    build:
      context: ./project/db
      dockerfile: Dockerfile
    expose:
      - 5432
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres

 

web-db 컨테이너가 가동되면 5432 포트를 통해서 다른 포트와 통신 할 수 있습니다. 

`entrypoint.sh`파일에서 web-db라는 컨테이너의 5432 포트를 통해서 정상적으로 PostgreSQL이 시작되었는지 확인 할 수 있습니다. 

 

#!/bin/sh

echo "Waiting for postgres..."

while ! nc -z web-db 5432; do
  sleep 0.1
done

echo "PostgreSQL started"

exec "$@"

 

app컨테이너의 Dockerfile을 아래와 같이 명세해줍니다. 

# pull official base image
FROM python:3.12.1-slim-bookworm

# set working directory
WORKDIR /usr/src/app

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# install system dependencies
RUN apt-get update \
  && apt-get -y install netcat-traditional gcc postgresql \
  && apt-get clean

# install python dependencies
RUN pip install --upgrade pip
COPY ./requirements.txt .
RUN pip install -r requirements.txt

# add app
COPY . .

# add entrypoint.sh
COPY ./entrypoint.sh .
RUN chmod +x /usr/src/app/entrypoint.sh

# run entrypoint.sh
ENTRYPOINT ["/usr/src/app/entrypoint.sh"]

 

asyncpg 라이브러리를 requirements.txt에 추가해줍니다. 

asyncpg==0.29.0

 

config.py 모듈에서 Settings클래스의 변수로 databse_url을 추가해줍니다. 

# project/app/config.py


import logging
from functools import lru_cache

from pydantic import AnyUrl
from pydantic_settings import BaseSettings


log = logging.getLogger("uvicorn")


class Settings(BaseSettings):
    environment: str = "dev"
    testing: bool = 0
    database_url: AnyUrl = None


@lru_cache()
def get_settings() -> BaseSettings:
    log.info("Loading config settings from the environment...")
    return Settings()

 

앱, 디비 컨테이너 두개를 기동하도록 해보겠습니다. 

$ chmod +x project/entrypoint.sh
$ docker-compose up -d --build

 

web 서비스의 로그를 확인해 보겠습니다. 

$docker-compose logs web
web_1  | Waiting for postgres...
web_1  | PostgreSQL started
web_1  | INFO:     Will watch for changes in these directories: ['/usr/src/app']
web_1  | INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
web_1  | INFO:     Started reloader process [1] using statreload
web_1  | INFO:     Started server process [37]
web_1  | INFO:     Waiting for application startup.
web_1  | INFO:     Application startup complete.

 

이전 `chmod +x` 명령어를 사용하였지만 환경에 따라서 chmod 755, 777 을 사용해야 할 수 있습니다. 

 

psql을 통해서 DB에 접속 해보겠습니다. 

$ docker-compose exec web-db psql -U postgres
postgres=# \c web_dev
postgres=# \q

 

Tortoise ORM

여러 비동기 ORM중 Tortoise ORM을 사용해 보겠습니다. 

우선 requirements.txt 파일에 라이브러리와 버전을 명세합니다. 

tortoise-orm==0.20.0

 

app 디렉토리 아래 models 디렉토리를 만들고 초기화 파일인 __init__.py 파일과 tortoise.py를 만듭니다. 
그리고 TextSummary 클래스를 명세해 줍니다. 

 

 

# project/app/models/tortoise.py


from tortoise import fields, models


class TextSummary(models.Model):
    url = fields.TextField()
    summary = fields.TextField()
    created_at = fields.DatetimeField(auto_now_add=True)

    def __str__(self):
        return self.url

 

main.py 모듈에서 register_tortoise 헬퍼 메소드를 사용하여 Tortoise 구성과 해제를 하도록 하겠습니다. 

# project/app/main.py

import os

from fastapi import FastAPI, Depends
from tortoise.contrib.fastapi import register_tortoise

from app.config import get_settings, Settings


app = FastAPI()


register_tortoise(
    app,
    db_url=os.environ.get("DATABASE_URL"),
    modules={"models": ["app.models.tortoise"]},
    generate_schemas=True,
    add_exception_handlers=True,
)


@app.get("/ping")
async def pong(settings: Settings = Depends(get_settings)):
    return {
        "ping": "pong!",
        "environment": settings.environment,
        "testing": settings.testing
    }

 

sanity check하겠습니다. 

$ docker-compose up -d --build

 

textsummary 테이블이 정상적으로 생성되었는지 확인합니다. 

$ docker-compose exec web-db psql -U postgres

psql (16.1)
Type "help" for help.

postgres=# \c web_dev
You are now connected to database "web_dev" as user "postgres".

web_dev=# \dt
            List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | textsummary | table | postgres
(1 row)

web_dev=# \q

 

지금까지 필요한 기능 개발을 위한 모듈 생성과 코드를 구성하였다면 프로젝트 트리 구조는 아래와 같습니다. 

├── .gitignore
├── docker-compose.yml
└── project
    ├── .dockerignore
    ├── Dockerfile
    ├── app
    │   ├── __init__.py
    │   ├── config.py
    │   ├── main.py
    │   └── models
    │       ├── __init__.py
    │       └── tortoise.py
    ├── db
    │   ├── Dockerfile
    │   └── create.sql
    ├── entrypoint.sh
    └── requirements.txt

 

마이그레이션

Tortoise는 Aerich을 통하여 데이터베이스 마이그레이션을 지원합니다. 

우선 Aerich이 schema관리를 하도록 하기 위해서 기존 컨테이너 볼륨을 삭제하도록 하겠습니다. 

$ docker-compose down -v

 

기존에는 서버 기동시 자동으로 스키마가 생성되도록 하였는데요. 자동으로 생성되는 것을 방지하고자 register_tortoise 헬퍼 함수를 수정하겠습니다.

 

register_tortoise(
    app,
    db_url=os.environ.get("DATABASE_URL"),
    modules={"models": ["app.models.tortoise"]},
    generate_schemas=False,  # updated
    add_exception_handlers=True,
)

 

이미지를 다시 빌드하고 컨테이너를 돌려보겠습니다. 

$ docker-compose up -d --build

 

textsummary 테이블이 생성되지 않아야 합니다. 확인해보세요. 

$ docker-compose exec web-db psql -U postgres

psql (16.1)
Type "help" for help.

postgres=# \c web_dev
You are now connected to database "web_dev" as user "postgres".

web_dev=# \dt
Did not find any relations.

web_dev=# \q

 

requirements 파일에 aerich라이브러리 버전과 이름을 명세합니다. 

aerich==0.7.2

 

컨테이너를 다시 빌드합니다. 

$ docker-compose up -d --build

 

Aerich을 사용하려면 Tortoise 설정이 필요합니다. app디렉토리 아래 db.py 파일을 작성합니다. 

# project/app/db.py


import os


TORTOISE_ORM = {
    "connections": {"default": os.environ.get("DATABASE_URL")},
    "apps": {
        "models": {
            "models": ["app.models.tortoise", "aerich.models"],
            "default_connection": "default",
        },
    },
}

 

aerich 사용이 가능합니다. 초기화 명령어를 실행합니다. 

$ docker-compose exec web aerich init -t app.db.TORTOISE_ORM

해당 명령어를 실행하면 project/pyproject.toml  파일이 생성 됩니다. 

 

[tool.aerich]
tortoise_orm = "app.db.TORTOISE_ORM"
location = "./migrations"
src_folder = "./."

 

첫 마이그레이션을 만들어 보겠습니다. 

$ docker-compose exec web aerich init-db

Success create app migrate location migrations/models
Success generate schema for app "models"

 

그렇다면 아래 마이그레이션 결과를 migrations/models에서 볼 수 있습니다. 

from tortoise import BaseDBAsyncClient


async def upgrade(db: BaseDBAsyncClient) -> str:
    return """
        CREATE TABLE IF NOT EXISTS "textsummary" (
    "id" SERIAL NOT NULL PRIMARY KEY,
    "url" TEXT NOT NULL,
    "summary" TEXT NOT NULL,
    "created_at" TIMESTAMPTZ NOT NULL  DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "aerich" (
    "id" SERIAL NOT NULL PRIMARY KEY,
    "version" VARCHAR(255) NOT NULL,
    "app" VARCHAR(100) NOT NULL,
    "content" JSONB NOT NULL
);"""


async def downgrade(db: BaseDBAsyncClient) -> str:
    return """
        """

 

실제 psql에서 테이블이 생성 되었는지 확인 할 수 있습니다. 

$ docker-compose exec web-db psql -U postgres

psql (16.1)
Type "help" for help.

postgres=# \c web_dev
You are now connected to database "web_dev" as user "postgres".

web_dev=# \dt
            List of relations
 Schema |    Name     | Type  |  Owner
--------+-------------+-------+----------
 public | aerich      | table | postgres
 public | textsummary | table | postgres
(2 rows)

web_dev=# \q