본문 바로가기
MLOps

5. FastAPI

by 데브겸 2023. 2. 11.

0. 필요한 패키지 설치 

$ pip install "fastapi[all]"

 

1. FastAPI 개론

1-1. FastAPI로 간단한 api 만들어보기

# main.py

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"hello":"world"}

app = FastAPI()로 클래스 인스턴스 생성

 

Path Operation Decorator: @app.get("/")와 같이 API 작업의 endpoint를 HTTP method를 통해 지정

- Path: 첫번째 '/'로부터 시작되는 마지막 부분 (ex. https://fastapi.com/tutorial/first-steps/ 에서 /tutorial/first-steps/')

- Operation: POST, GET, PUT, DELETE와 같은 HTTP method를 의미

- 어떤 path로 가서 어떤 operation을 수행할 것인지를 path operation decorator를 사용한다는 의미로 생각하기

 

Path Operation Function: Path Operation이 수행되었을 때 호출될 python 함수

- 위 예시에서는 read_root라는 함수를 시행하는 것으로 코드를 짠 것

- 이외에 function을 통해 return 하는 값으로는 dict, list, str, int, pydantic model 등이 가능

 

 

1-2. FastAPI 코드 실행하기

$ uvicorn main:app --reload

 

- uvicorn: FastAPI를 실행하는 웹 서버 실행 Command Line Tool

- main: 위에서 작성한 python 모듈 main.py를 의미

- app: main.py에서 app=FastAPI()를 통해 생성된 객체를 의미

- --reload: 코드가 바뀌었을 때 서버가 재시작할 수 있도록 해주는 옵션

uvicorn main:app --reload 실행, http://127.0.0.1:8000로 접속

 

 

1-3. Path Parameter & Query Parameter

Path Parameter는 Path Operation에 포함된 변수로 사용자에게 입력받아 function의 argument로 사용되는 parameter를 뜻함

Query Parameter는 function parameter로는 사용되나 path operation에는 포함되지 않아 path parameter와는 다른 parameter

(Query parameter는 url 뒤에 ?가 붙고 key-value 쌍으로 나타내고 &로 구분 - ex. http://localhost:8000/items/?skip=0&limit=10)

from fastapi import FastAPI
from typing import Optional

app = FastAPI()

@app.get("/users/{user_id}/items/{item_id}")
def read_user_item(user_id: int, item_id: str, q: Optional[str] = None, short: bool = False):
	item = {"item_id": item_id, "owner_id": user_id}
    if q:
    	item.update({"q":q})
    if not short:
    	item.update(
        	{"description": "hello world"}
        )
    return item

 

Path Operation: @app.get("/users/{user_id}/items/{item_id}")

Path Operation Function: read_user_item()

Path Parameter: user_id, item_id

Query Parameter: q, short

 

Path Parameter, Query Parameter 중 어떤 것을 사용하면 좋을까?

Path Parameter의 경우 경로에 존재하는 내용이 없으면 404 에러를 뱉음 
-> 따라서 리소스를 식별해야 하는 경우 path parameter를 사용하는 것이 좋음

Query Parameter의 데이터가 없으면 빈 리스트가 나오기 때문에 추가로 에러 핸들링이 필요함
-> ?뒤에 key-value 쌍으로 값들이 나옴. 값들을 정렬하거나 필터링해야 하는 경우 사용하는 것이 좋음

 

Path Operation Function에 Parameter 형식, 기본값 지정해주기

- 자료형 지정해주기
def read_user_item(user_id:int) 와 같이 parameter의 type을 지정해줄 수 있다.
만약 지정한 것과 다른 자료형을 입력하면 HTTP Error를 반환
type hinting에는 단순 자료형만 쓸 수 있는 것이 아니라, 후에 나올 pydantic으로 생성한 class도 넣을 수 있다.

- 기본값 지정해주기
def read_user_item(short: bool = False) 와 같이 = 뒤에 기본값을 지정해줄 수 있다.

- Optional인거 나타내주기
from typing import Optional을 통해 특정 파라미터가 Optional임을 지정하면서도 자료형도 동시에 지정해줄 수 있다
ex. def read_user_item(q: Optional[str] = None)

 

2. FastAPI CRUD

CRUD:

- C: 이름과 별명을 입력하여 사용자를 생성

- R: 이름을 받아 해당 이름을 지닌 사용자의 별명 반환 (이름 없으면 HTTPException)

- U: 이름과 별명을 받아 해당 이름을 가진 사용자의 별명 업데이트 (이름 없으면 HTTPException)

- D: 이름을 받아 해당 이름을 지닌 사용자 정보 삭제 (이름 없으면 HTTPException)

 

2-1. Path Parameter를 이용한 API 구현

Path Parameter를 이용하는 경우 API에 사용되는 파라미터를 Request Header에 넣어 전달

(POST 메소드를 이용하더라도...?)

# crud_path.py

from fastapi import FastAPI, HTTPException

app = FastAPI()
USER_DB = {}
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")


@app.post("/users/name/{name}/nicname/{nickname})
def create_user(name: str, nickname: str):
    USER_DB[name] = nickname
    return {"status":"success"}

@app.get("/users/name/{name}")
def read_user(name: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    return {"nickname": USER_DB[name]}

@app.put("/users/name/{name}/nickname/{nickname}")
def update_user(name: str, nickname: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    USER_DB[name] = nickname
    return {"status":"success"}

@app.delete("/users/name/{name}")
def delete_user(name:str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    del USER_DB[name]
    return {"status":"success"}

 

 

2-2. Query Parameter를 이용한 API 구현

Query Parameter를 이용하는 경우 API에 사용되는 파라미터를 Request Body에 넣어 전달

# crud_query.py

from fastapi import FastAPI, HTTPException
app = FastAPI()
USER_DB = {}
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")

@app.post("/users")
def create_user(name:str, nickname:str):
    USER_DB[name] = nickname
    return {"status":"success"}

@app.get("/users")
def read_user(name:str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    return {"nickname": USER_DB[name]}

@app.put("/users")
def update_user(name: str, nickname: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    USER_DB[name] = nickname
    return {"status":"success"}

@app.delete("/users")
def delete_user(name: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    del USER_DB[name]
    return {"status":"success"}

 

- 클라이언트에서 API에 데이터를 보낼 때 Request Body를 사용함
- API에서 클라이언트에 데이터를 보낼 때는 Response Body를 사용함
- Request Body에 데이터가 항상 포함되어야 하는 것은 아님
- Request Body에 데이터를 보내고 싶다면 POST 메소드를 사용  (GET 메소드는 URL, Request Header로 데이터를 전달함) 
- Body의 데이터를 설명하는 Content-Type이란 Header 필드가 존재하고, 어떤 데이터 타입인지 명시해야 함

대표적인 콘텐츠 타입
- application/x-www-form-urlencoded: BODY에 Key, Value 사용. & 구분자 사용
- text/plain: 단순 txt 파일
- multipartform-data: 데이터를 바이너리 데이터로 전송 

 

 

2-3. 작성한 코드 실행 & API 테스트 확인

 

# path parameter 버전 코드 실행
$ uvicorn crud_path:app --reload

# query parameter 버전 코드 실행
$ uvicorn crud_path:app --reload

 

uvicorn crud_000:app --reload 실행

 

swagger UI 확인

 

Swagger UI로 API 테스트

 

 

 

3. FastAPI CRUD using Pydantic

Pydantic이란 data validation, setting management 라이브러리 (공식 document link)

런타임 환경에서 타입을 강제하고, 강제한 타입과 맞지 않을 때 에러를 발생시켜서 안전하게 데이터를 핸들링할 수 있다

데이터를 주고 받을 때 데이터의 형식을 지정해줄 수 있는데, 이때 Pydantic Model을 사용 가능

(다양한 곳에서 사용하고 있으나 FastAPI에서는 거의 Pydandtic을 활용하여 코드를 작성하는 것 같다)

 

3-1. 입력되는 데이터에 대해서 데이터의 종류, 타입 강제하기

pydantic은 데이터 타입을 정의해주는 class를 만들 때 pydantic에서 미리 만들어둔 것들을 상속받게 하고,

path operation에서 해당 class를 받아서 사용하는 형태로 사용

from pydantic import BaseModel

class CreateIn(BaseModel):
    name: str
    nickname: str

@app.post("/users")
def create_user(user: CreateIn):
	USER_DB[user.name] = user.nickname
    return {"status":"success"}

 

3-2. Path Operation의 결과물에 대한 데이터의 종류, 타입 강제하기 (response_model)

Pydantic을 사용할 때는 reponse_model을 이용하여 path operation의 반환 결과에 대해서 어떤 것을 반환하고, 그 타입을 강제하는 것이 가능함. (주의해야 할 것은 path operation function의 input이 아니라 path operation에 대한 input이라는 것!)

이게 왜 필요하냐

- 반환되는 데이터의 형식을 지정해주는 것이 훨씬 더 안전하고

- 특정 정보(ex. 비밀번호와 같이 조심히 다뤄야 하는 정보) 를 필터링하여 반환하는데도 좋음

 

response_model의 용도
- output data를 response_model에서 지정한 모델로 변환 ☆
- response에 JSON 스키마 추가
- doc 시스템에 사용

 

from pydantic import BaseModel

class CreateOut(BaseModel):
	status: str
    id: int
    
@app.post("/users", response_model=CreateOut)
def create_user(user: CreateIn) -> CreateOut:
	USER_DB[user.name] = user.nickname
    return CreateOut(status="success", id=len(USER_DB))

위 예시는 살짝 어렵게 작성되어 있는 것 같아서, 조금 더 자세히 이해하고 싶다면 이 블로그 글을 보기 권함

 

 

3-3. 종합 & 실행

# crud_pydantic.py

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()
USER_DB = {}
NAME_NOT_FOUND = HTTPException(status_code=400, detail="Name not found.")

class CreateIn(BaseModel):
    name: str
    nickname: str

class CreateOut(BaseModel):
    status: str
    id: int

@app.post("/users", response_model=CreateOut)
def create_user(user: CreateIn):
    USER_DB[user.name] = user.nickname
    user_dict = user.dict()
    user_dict["status"] = "success"
    user_dict["id"] = len(USER_DB)
    return user_dict


@app.get("/users")
def read_user(name: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    return {"nickname": USER_DB[name]}


@app.put("/users")
def update_user(name: str, nickname: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    USER_DB[name] = nickname
    return {"status": "success"}


@app.delete("/users")
def delete_user(name: str):
    if name not in USER_DB:
        raise NAME_NOT_FOUND
    del USER_DB[name]
    return {"status": "success"}

 

 

$ uvicorn crud_pydantic:app --reload

Pydantic Model을 사용하기 전에는 parameter를 이용하여 데이터를 전달했지만,

Pydantic Model을 사용하고 나서는 Request Body를 통해 데이터가 전달됨을 확인할 수 있음

-> 왜...????

 

 

4. FastAPI on Docker

4-1. Dockerfile 작성 및 build

# Dockerfile

FROM amd64/python:3.9-slim

WORKDIR /usr/app

RUN pip install -U pip \
    && pip install "fastapi[all]"

COPY crud_pydantic.py crud_pydantic.py

CMD ["uvicorn", "crud_pydantic:app", "--host", "0.0.0.0", "--reload"]

CMD 명령어로 컨테이너가 실행되었을 때 uvicorn crud_pydantic:app --reload 돌아갈 수 있도록 설정

 

# m1 노트북에서 진행 중이라 linux/amd64로 platform 지정해서 build
$ docker build --platform linux/amd64 -t part5-api-server .

 

4-2. Docker image run & 종료

$ docker run -d --name api-server -p 8000:8000 --platform linux/amd64 part5-api-server

 

8000:8000으로 포트포워딩 했기 때문에 localhost:8000으로 접속

 

API가 잘 작동되는 것 확인했으니 컨테이너 종료하기

$ docker rm --force api-server

'MLOps' 카테고리의 다른 글

BentoML 시작하기 & v0와 v1 비교 (v1.0 migration 이후)  (0) 2023.08.27
6. API Serving  (0) 2023.02.19
2. Model Development  (0) 2023.01.27