AI TECH

Fast API (2)

prefer_all 2023. 1. 14. 10:51
<목차>
1. FastAPI
- Path Parameter, Query Parameter, Optional Parameter
- Request Body, Response Body 
- Form, File

2. Pydantic 
- Pydantic Validation 
- Pydantic Config

 

Fast API로 간단한 웹서버 띄우기

uvicorn이란?

매우 가벼운 ASGI 서버이다.
fast api 프레임워크만으로는 웹 개발을 할 수 없고, ASGI와 호환되는 웹 서버가 필요하다.
-> Uvicorn은 비동기 방식이 가능한 python web server framework와 application 간의 표준 interface를 제공한다.
Uvicorn을 사용하면 배포 시 별도의 준비가 필요하지 않다.
# *** https://github.com/jjeongah/Boostcamp-AI-Tech-Product-Serving/tree/main/part3/01-fastapi***
from fastapi import FastAPI
import uvicorn

# FastAPI 객체 생성
app = FastAPI()


# "/"로 접근하면 return을 보여줌
@app.get("/")
def read_root():
    return {"Hello": "World"}


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

 

vscode 내에서 브라우저 창으로 이동하는 버튼이 뜨고 클릭 시 아래와 같은 화면을 볼 수 있다

localhost:8000/docs로 이동시 아래와 같이 자동으로 만들어진 Swagger를 볼 수 있다


Path VS Query Parameter

- 웹에서 GET Method를 사용해 데이터를 전송할 수 있음

- ID가 402인 사용자 정보를 가져오고 싶은 경우 방식 

Path Parameter Query Parameter
/users/402

- 서버에 402라는 값을 전달하고 변수로 사용
/users?id=402

- Query String
- API 뒤에 입력 데이터를 함께 제공하는 방식으로 사용
- Query String은 Key, Value의 쌍으로 이루어지며 &로 연결해 여러 데이터를 넘길 수 있음
- 경로에 존재하는 내용이 없으므로 404 Error 발생 -  데이터가 없는 경우 빈 리스트가 나옴
- Resource를 식별해야 하는 경우 - 정렬, 필터링을 해야 하는 경우
# ********** Path Parameter **********
from fastapi import FastAPI
import uvicorn

app = FastAPI()


@app.get("/users/{user_id}")
def get_user(user_id):
    return {"user_id": user_id}


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

 

# ********** Query Parameter **********
from fastapi import FastAPI
import uvicorn

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}] #⭐

#⭐ url 자체에는 아무것도 입력되지 않음
@app.get("/items/") 
def read_item(skip: int = 0, limit: int = 10): 
#⭐ 함수의 파라미터가 Query Parameter로 사용됨
    return fake_items_db[skip: skip + limit]


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)
    
#⭐ localhost:8000/items/로 접근시 fake_items_db[0:10]의 결과 출력

 

Optional Parameter

# ***** 특정 param을 선택적으로 하고 싶은 경우 *****
from typing import Optional
from fastapi import FastAPI
import uvicorn

app = FastAPI()

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/items/{item_id}")
def read_item(item_id: str, q: Optional[str] = None):#⭐
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)


Request Body & Response Body

  • Request Body : 클라이언트가 API로 데이터를 보낼때 사용되는 데이터
    • Request Body에 데이터가 항상 포함되어야 하는 것은 아님
    • Request Body에 데이터를 보내고 싶다면 POST Method를 사용
  • Response Body : API가 request의 응답으로 클라이언트에게 보내는 데이터
    • POST Method는 Request Body에 데이터를 넣어 보냄
    • Body의 데이터를 설명하는 Content-Type이란 Header 필드가 존재하고, 어떤 데이터 타입인지 명시해야 함
      • 대표적인 컨텐츠 타입
        • application/x-www-form-urlencoded : BODY에 Key, Value 사용. & 구분자 사용
        • text/plain : 단순 txt 파일
        • multipartform-data : 데이터를 바이너리 데이터로 전송

 

Request Body

# ***** POST 요청으로 item을 생성하는 예제 *****
# 깃헙 파일 경로: Boostcamp-AI-Tech-Product-Serving/part3/01-fastapi/examples/05~

from typing import Optional
from fastapi import FastAPI
import uvicorn

from pydantic import BaseModel

# 🌟 pydantic로 Request Body 데이터 정의
class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None
# 🌟

app = FastAPI()

# ⭐ Type Hinting에 위에서 생성한 Class 주입. Request Body 데이터를 Validation
@app.post("/items/")
def create_item(item: Item):
    return item
# ⭐

if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

- python3 05_request_body.py로 웹서버 실행
- curl 등으로 POST를 실행할 수 있지만, localhost:8000/docs로 이동

웹서버 실행시 아래와 같은 화면이 뜬다

ㅏ래오Schemas에서 pydantic으로 정의한 내용을 볼 수 있음
POST 쪽을 클릭해도 해당 내용 확인할 수 있음
기본 설정 상태에서 Execute하면
curl 명령어, Response가 보임

 

Request body를 수정해보자!

tax에 String을 넣어서 Execute -> tax가 Float이 아니라는 메세지가 출력됨(Validation Check한 것)

 

Response Body

- decorator의 response_model 인자로 설정 가능하다
   <역할>
    - Output Data를 해당 정의에 맞게 변형
    - 데이터 Validation
    - Response에 대한 Json Schema 추가
     - 자동으로 문서화

# 깃헙 파일 경로: Boostcamp-AI-Tech-Product-Serving/part3/01-fastapi/examples/06_response_body.py

from typing import Optional
from fastapi import FastAPI
import uvicorn

from pydantic import BaseModel


class ItemIn(BaseModel):#💙
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None


class ItemOut(BaseModel):#💜 description 없음
    name: str
    price: float
    tax: Optional[float] = None


app = FastAPI()

# ⭐ Decorator의 response_model 인자로 주입 가능
@app.post("/items/", response_model=ItemOut) 
#💜
def create_item(item: ItemIn):#💙
    return item
# ⭐

if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

위 예제와 마찬가지로 python3 06_response_body.py 웹 서버 실행한 후, docs 확인하자
Try it out을 누르고 Execute를 실행시, Request 데이터와 Response 데이터가 다름을 알 수 있다.


Form

- Form을 사용하려면 python-multipart를 설치해야 함
pip install python-multipart

- 프론트도 간단히 만들기 위해 Jinja2 설치
pip install Jinja2
# 깃헙 파일 경로: Boostcamp-AI-Tech-Product-Serving/part3/01-fastapi/examples/07~
from fastapi import FastAPI, Form, Request
from fastapi.templating import Jinja2Templates

import uvicorn

app = FastAPI()
templates = Jinja2Templates(directory='./')

# ⭐ 없으면 405 Method Not Allowed 에러 발생
# login으로 접근하면 GET Method가 요청되기 때문임
@app.get("/login/")
def get_login_form(request: Request):
    return templates.TemplateResponse('login_form.html', context={'request': request})
# ⭐

# Form 클래스를 사용하면 Request의 Form Data에서 값을 가져옴
@app.post("/login/")
def login(username: str = Form(...), password: str = Form(...)):
    return {"username": username}


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)
1. import Request
Request 객체로 Request를 받음
2. templates = Jinja2Templates(directory='./')
파이썬에서 사용할 수 있는 템플릿 엔진 : Jinja Template => 프론트엔드 구성
3. return templates.TemplateResponse('login_form.html', context={'request': request})
templates.TemplateResponse로 해당 HTML로 데이터를 보냄

+ Form(...)이란?
Python ellipsis로, Required(필수 요소)를 의미한다.

login_form.html은 아래와 같다. 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sample Login Form</title>
</head>
<body>
<form method="post">
    <input type="string" name="username" value="{{ username }}"/>
    <input type="password" name="password" value="{{ password }}"/>

    <input type="submit">
</form>
</body>
</html>


python3 07_form.py을 사용해 웹 서버를 실행한 후 localhost:8000/login/ 으로 이동하자.

제출하면 POST 요청으로 오른쪽 처럼


File

File을 사용할 때도 python-multipart를 설치해야 함

from typing import List

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import HTMLResponse

import uvicorn

app = FastAPI()

# 🌟 HTML에서 action으로 넘긴다
@app.post("/files/")
def create_files(files: List[bytes] = File(...)):
    return {"file_sizes": [len(file) for file in files]}


@app.post("/uploadfiles/")
def create_upload_files(files: List[UploadFile] = File(...)):
    return {"filenames": [file.filename for file in files]}

# ⭐ '/'로 접근할 때 보여줄 HTML 코드
@app.get("/")
def main():
    content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
<form action="/uploadfiles/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
    """
    return HTMLResponse(content=content)
# ⭐

if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8000)

python3 08_file.py를 실행하고 localhost:8000로 이동시 아래와 같이 뜬다.


Pydantic

- FastAPI에서 Class 사용할 때 보이던, Data Validation / Settings Management 라이브러리
- Type Hint를 런타임에서 강제해 안전하게 데이터 핸들링
- 파이썬 기본 타입(String, Int 등) + List, Dict, Tuple에 대한 Validation 지원
- 기존 Validation 라이브러리보다 빠름 (Benchmark)
- Config를 효과적으로 관리하도록 도와줌
- 머신러닝 Feature Data Validation으로도 활용 가능


- Pydantic의 두 가지 기능은 Validation과 config 관리이다.

Validation

<체크하는 Logic>
- 조건 1: 올바른 url을 입력 받음 (url)
- 조건 2: 1-10 사이의 정수 입력 받음 (rate)
- 조건 3: 올바른 폴더 이름을 입력 받음(target_dir)

<사용할 수 있는 방법>
1) 일반 Python Class를 활용한 Input Definition 및 Validation
  - Python Class로 Input Definition 및 Validation => 의미 없는 코드가 많아짐
  - 복잡한 검증 로직엔 Class Method가 복잡해지기 쉬움
   - Exception Handling을 어떻게 할지 등 커스텀하게 제어할 수 있는 있지만 메인 로직(Input을 받아서 Inference를 수행하는)에 집중하기 어려워짐

class ModelInput01:
    def __init__(self, url: str, rate: int, target_dir: str) -> None:
        self.url = url
        self.rate = rate
        self.target_dir = target_dir

    def _validate_url(self, url_like: str) -> bool:
        """
        올바른 url인지 검증합니다
        Args:
            url_like (str): 검증할 url
        Returns:
            bool: 검증 성공/실패 여부
        """
        from urllib.parse import urlparse

        try:
            result = urlparse(url_like)
            return all([result.scheme, result.netloc])
        except:
            return False

    def _validate_directory(self, directory_like: str) -> bool:
        """
        존재하는 디렉토리인지 검증합니다
        Args:
            directory_like (str): 검증할 디렉토리 경로
        Returns:
            bool: 검증 성공/실패 여부
        """
        import os

        return os.path.isdir(directory_like)

    def validate(self) -> bool:
        """
        클래스 필드가 올바른지 검증합니다.
        Returns:
            bool: 검증/성공 실패 여부
        """
        validation_results = [
            self._validate_url(self.url),
            1 <= self.rate <= 10,
            self._validate_directory(self.target_dir),
        ]
        return all(validation_results)

 


- 2) Dataclass를(python 3.7 이상 필요) 활용한 Input Definition 및 Validation
- 인스턴스 생성 시점에서 Validation을 수행하기 쉬움
- 여전히 Validation 로직들을 직접 작성해야 함
- Validation 로직을 따로 작성하지 않으면, 런타임에서 type checking을 지원하지 않음

from dataclasses import dataclass
from pydantic.networks import HttpUrl

class ValidationError(Exception):
    pass

# ⭐dataclass decorator 사용으로 init method를 따로 작성할 필요가 없어짐
@dataclass
class ModelInput02:
    url: str
    rate: int
    target_dir: str

    def _validate_url(self, url_like: str) -> bool:
        """
        올바른 url인지 검증합니다
        Args:
            url_like (str): 검증할 url
        Returns:
            bool: 검증 성공/실패 여부
        """
        from urllib.parse import urlparse

        try:
            result = urlparse(url_like)
            return all([result.scheme, result.netloc])
        except:
            return False

    def _validate_directory(self, directory_like: str) -> bool:
        """
        존재하는 디렉토리인지 검증합니다
        Args:
            directory_like (str): 검증할 디렉토리 경로
        Returns:
            bool: 검증 성공/실패 여부
        """
        import os

        return os.path.isdir(directory_like)

    def validate(self) -> bool:
        """
        클래스 필드가 올바른지 검증합니다.
        Returns:
            bool: 검증/성공 실패 여부
        """
        validation_results = [
            self._validate_url(self.url),
            1 <= self.rate <= 10,
            self._validate_directory(self.target_dir),
        ]
        return all(validation_results)

    def __post_init__(self):
        if not self.validate():
            raise ValidationError("올바르지 않은 input 입니다")
    #⭐ post init 메서드 같은 편의 매직 메서드 사용 가능
    #⭐ 하지만 여전히 validate method를 따로 만들어야 함 (길어지는 검증 로직을 분리하기 위해)


- 3) Pydantic을 활용한 Input Definition 및 Validation
- 훨씬 간결해진 코드 (6라인)(vs 52라인 Python Class, vs 50라인 dataclass)
- 주로 쓰이는 타입들(http url, db url, enum 등)에 대한 Validation이 만들어져 있음
- 런타임에서 Type Hint에 따라서 Validation Error 발생 - Custom Type에 대한 Validation도 쉽게 사용 가능

from pydantic import BaseModel, HttpUrl, Field, DirectoryPath

class ModelInput03(BaseModel):
    url: HttpUrl # ⭐ 올바른 Http URL인지 검증
    rate: int = Field(ge=1, le=10) # ⭐ 1보다 크거나 같고 10보다 작거나 같은 지 검증하는 옵션을 Field로 주입 
    target_dir: DirectoryPath # ⭐ 존재하는 디렉토리인지 검증

if __name__ == "__main__":
    import os

    VALID_INPUT = {
        "url": "https://content.presspage.com/uploads/2658/c800_logo-stackoverflow-square.jpg?98978",
        "rate": 4,
        "target_dir": os.path.join(os.getcwd(), "examples"),
    }

    INVALID_INPUT = {"url": "WRONG_URL", "rate": 11, "target_dir": "WRONG_DIR"}

    valid_python_class_model_input = ModelInput01(**VALID_INPUT)
    assert valid_python_class_model_input.validate() is True

    invalid_python_class_model_input = ModelInput01(**INVALID_INPUT)
    assert invalid_python_class_model_input.validate() is False

    valid_dataclass_model_input = ModelInput02(**VALID_INPUT)
    assert valid_dataclass_model_input.validate() is True

    try:
        invalid_dataclass_model_input = ModelInput02(**INVALID_INPUT)  # Error
    except ValidationError as exc:
        print("dataclass model input validation error", str(exc))
        pass

    from pydantic import ValidationError

    valid_pydantic_model_input = ModelInput03(**VALID_INPUT)
    try:
        invalid_pydantic_model_input = ModelInput03(**INVALID_INPUT)  # error
    except ValidationError as exc:
        print("pydantic model input validation error: ", exc.json())
        pass

Pydantic은 어디서 에러가 발생했는지를 알려줌

 

Pydantic Config

- (문제) 이전에 애플리케이션은 종종 설정을 상수로 코드에 저장함-> 이것은 Twelve-Factor를 위반함
- Twelve-Factor는 설정을 코드에서 엄격하게 분리하는 것을 요구함
- Twelve-Factor App은 설정을 환경 변수(envvars나 env라고도 불림)에 저장함
- (해결) 환경 변수는 코드 변경 없이 쉽게 배포 때마다 쉽게 변경할 수 있음
- The Twelve-Factor App이라는 SaaS(Software as a Service)를 만들기 위한 방법론을 정리한 규칙들에 따르면, 환경 설정은 애플리케이션 코드에서 분리되어 관리되어야 함
참고 글: https://12factor.net/ko/config

 

1) .ini, .yaml 파일 등으로 config 설정하기

yaml로 환경 설정을 관리할 경우, 쉽게 환경을 설정할 수 있지만, 환경에 대한 설정을 코드 하드코딩하는 형태이기 때문에 변경 사항이 생길 때 유연하게 코드를 변경하기 어려움

# dev_config.yaml 파일

env: dev
db:
  username: user
  password: user
  host: localhost
  port: 3306
  database: dev
import os
from typing import Dict, Any

with open("dev_config.yaml", "w") as f:
    f.write(dev_config_yaml)

# 1. .ini, .yaml 파일 기반 config 주입
from yaml import load, FullLoader

def load_config(config_path: str) -> Dict[str, Any]:
    """
    config YAML 파일을 로드합니다
    Args:
        config_path: config YAML 파일 경로
    Returns:
        Dict[str, Any]: Config dictionary
    """
    with open(config_path, "r") as f:
        config = load(f, FullLoader)
    return config


config = load_config(config_path="dev_config.yaml")

assert config["env"] == "dev"
expected = {"username": "user", "password": "user", "host": "localhost", "port": 3306, "database": "dev"}
assert config["db"] == expected

 

2) flask-style config.py

- Config 클래스에서 yaml, ini 파일을 불러와 python class 필드로 주입하는 과정을 구현
- Config를 상속한 클래스에서는 Config 클래스의 정보를 오버라이딩해서 사용
- 하지만 해당 파일의 데이터가 정상적인지 체크하거나(Validation) 또는 환경 변수로 부터 해당 필드를 오버라이딩(Overriding) 하려면 코드량이 늘어남

class Config(object):
    ENV: str = None
    TESTING: bool = False
    DB: Dict[str, Any] = {}

    @classmethod
    def from_yaml(cls, config_path: str):
        import yaml

        with open(config_path, "r") as config_file:
            config = yaml.load(config_file, Loader=yaml.FullLoader)

        cls.ENV = config["env"]
        cls.DB = config["db"]
        return cls


class DevConfig(Config):
    pass


class ProdConfig(Config):
    pass


config = DevConfig.from_yaml("dev_config.yaml")
assert config.ENV == "dev"
assert config.DB == expected

 

3) pydantic base settings

- Validation처럼 Pydantic은 BaseSettings를 상속한 클래스에서 Type Hint로 주입된 설정 데이터를 검증할 수 있음
- Field 클래스의 env 인자로, 환경 변수로 부터 해당 필드를 오버라이딩 할 수 있음
- yaml, ini 파일들을 추가적으로 만들지 않고, .env 파일들을 환경별로 만들어 두거나, 실행 환경에서 유연하게 오버라이딩 할 수 있음

from pydantic import BaseSettings, Field
from enum import Enum


class ConfigEnv(str, Enum):
    DEV = "dev"
    PROD = "prod"


class DBConfig(BaseSettings):
    host: str = Field(default="localhost", env="db_host")
    port: int = Field(default=3306, env="db_port")
    username: str = Field(default="user", env="db_username")
    password: str = Field(default="user", env="db_password")
    database: str = Field(default="dev", env="db_database")


class AppConfig(BaseSettings):
    env: ConfigEnv = Field(default="dev", env="env")
    db: DBConfig = DBConfig()


with open("dev_config.yaml", "r") as f:
    config = load(f, FullLoader)

config_with_pydantic = AppConfig(**config)

assert config_with_pydantic.env == "dev"
assert config_with_pydantic.db.dict() == expected

# 환경 변수로 필드를 오버라이딩합니다.
os.environ["ENV"] = "prod"
os.environ["DB_HOST"] = "mysql"
os.environ["DB_USERNAME"] = "admin"
os.environ["DB_PASSWORD"] = "SOME_SAFE_PASSWORD"

prod_config_with_pydantic = AppConfig()
assert prod_config_with_pydantic.env == "prod"
assert prod_config_with_pydantic.dict() != expected

# cleanup
os.remove("dev_config.yaml")

 

'AI TECH' 카테고리의 다른 글

Fast API (3)  (0) 2023.01.16
Fast API (1)  (1) 2023.01.12
Product Serving  (1) 2023.01.12
Passage Retrieval  (0) 2022.12.20
MRC  (0) 2022.12.20