FastAPI를 배워보자 4일차 - Fastapi 시작 방법, path parameter, query parameter, request body

0

fastapi

목록 보기
4/13

Fastapi 시작 방법, path parameter, query parameter, request body

다시 처음부터 fastapi를 docs를 읽어보면서 내용을 정리하기로 했다. 처음에 설정 부분을 확인하고 싶다면 다음을 참고하도록 하자. https://velog.io/@chappi/FastAPI%EB%A5%BC-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90-1%EC%9D%BC%EC%B0%A8-FastAPI%EB%9E%80-WSGI-ASGI

1. FastAPI 시작

fastapi모듈로부터 FastAPI객체를 가져와서 사용하면 된다.

  • main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello world"}

실행하는 방법은 다음과 같다.

uvicorn main:app --reload

서버가 실행되었다면 다음의 path로 요청을 보낼 수 있다. 참고로, main:appmain은 python file이름이고, appFastAPI객체이름이다.

curl http://127.0.0.1:8000
{"message":"Hello world"}

위의 실행 방법 말고도 unvicorn을 python code에서 실행하는 방법이 있다.

  • main.py
import uvicorn
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello world"}

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

이렇게 만들면 uvicorn.run부분이 실행되면서 localhost:8888에서 실행된다.

실행방법은 다음과 같다.

python3 main.py 

INFO:     Started server process [2260121]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

잘 시작되었다면 8888으로 요청을 보내어 응답을 받을 수 있다.

curl http://127.0.0.1:8888
{"message":"Hello world"}

2. FastAPI의 http method

FastAPI에서 제공하는 주요 HTTP method는 다음과 같다.
1. POST: data를 만들기 위해 사용한다.
1. GET: data를 가져오기 위해 사용한다.
1. PUT: data를 업데이트하기 위해 사용한다.
1. DELETE: data를 삭제하기 위해 사용한다.

위애서 보듯이 사용방법은 매우 간단한데, @app.method(path), 이런 방식으로 쓰면 된다. 가령 get이.고 path가 /이면 @app.get('/')로 쓰면 된다. post이고 path가 /item이면 @app.post('/post')로 쓰면 된다.

이 밖에 tracehead 같은 것들이 있지만 실제적으로 쓰는 일은 거의 없으니 생략하도록 한다.

3. Path parameters

https://fastapi.tiangolo.com/tutorial/path-params/

path parameter는 다음과 같이 사용할 수 있다.

@app.get("/items/{item_id}")
async def read_item(item_id):
    return {"item_id": item_id}

/items/foo이렇게 요청하면 item_idfoo가 오게된다. 신기한 것은 read_item의 parameter로 path parameter를 넣어준다는 것이다. 이는 FastAPI에서 먼저 처리해주는 부분이며, 추후에 다른 query parameter나 header와 같은 값들과 구분하기위해 또 다른 방법에 대해서 알아볼 것이다.

curl http://127.0.0.1:8888/items/1
{"item_id":"1"}

curl http://127.0.0.1:8888/items/foo
{"item_id":"foo"}

기본적으로 path parameter에 type을 적어주지 않으면 string으로 처리되는 것을 볼 수 있다. 만약 interger로 처리하고 싶다면 다음과 같이 int를 적어주면 된다.

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

이제 path parameter인 item_idint타입만 가능하다. pydantic에서 int타입을 검사하고, int타입 요청하지 않으면 에러를 반환한다.

curl http://127.0.0.1:8888/items/1
{"item_id":1}

curl http://127.0.0.1:8888/items/foo
{
  "detail": [
    {
      "type": "int_parsing",
      "loc": [
        "path",
        "item_id"
      ],
      "msg": "Input should be a valid integer, unable to parse string as an integer",
      "input": "foo",
      "url": "https://errors.pydantic.dev/2.4/v/int_parsing"
    }
  ]
}

foo와 같이 str로 요청하였을 때 에러가 반환되는 것을 볼 수 있다. 심지어 float로 요청해도 똑같이 에러를 받는다. 이는 pydantic의 type검사가 엄격한 것을 알 수 있다.

기본적으로 parameter의 type들은 모두 적어주는 것이 좋은데, type을 적어주면 fastapi에서 자동으로 문서화를 해주기 때문이다.

localhost:8888/docs
localhost:8888/redoc

다음의 페이지에 접속하면 각 API들에 대한 명세가 나온다. 우리가 쓴 path paramter의 type도 적혀있을 것이다.

한 가지 조심해야할 것은 path가 겹치는 handler에 대해서는 등록하는 순서에 따라서 결과가 달라진다는 것이다. 자주하는 실수 중 하나로, 다음과 같이 한쪽은 fixed path를 사용하고 한 쪽은 fixed path에 겹치는 path parameter를 사용하는 경우가 있다.

@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}

@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}

다음의 경우 read_user_me/users/me path를 사용하는 반면에 read_user/users/{user_id}를 사용한다. 둘이 path가 겹치는 데 이 경우에 먼저 등록된 read_user_me가 사용된다.

curl http://127.0.0.1:8888/users/me
{"user_id":"the current user"}

아예 path가 동일한 경우도 마찬가지이다. 따라서 fastapi에서의 handler등록 순서가 매우 중요하다는 것을 알 수 있다.

path parameter에 재밌게도, enum또한 사용할 수 있다. 즉, 정해진 path parameter 값들이 있고 이외의 값들은 무시하고 싶을 때 사용할 수 있다.

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"

ModelName을 잘보면 Enumstr을 상속받는데, Enum은 enumerate이기 때문에 상속받아야 하는 것이라면, strEnumstr과 관련된 special method(magic method)를 첨가하기 위해서 사용한다. 즉, __str__등을 가능하게하여 pydantic에서 type검사를 할 때 str으로 받은 alexnetModelName.alexnet을 비교할 수 있게 되는 것이다. 만약 위에서 str 상속을 빼면 Enumstr을 비교하기 때문에 불가능하다.

from enum import Enum

from fastapi import FastAPI

class ModelName(str, Enum):
    alexnet = "alexnet"
    resnet = "resnet"
    lenet = "lenet"

app = FastAPI()

@app.get("/models/{model_name}")
async def get_model(model_name: ModelName):
    if model_name is ModelName.alexnet:
        return {"model_name": model_name, "message": "Deep Learning FTW!"}

    if model_name.value == "lenet":
        return {"model_name": model_name, "message": "LeCNN all the images"}

    return {"model_name": model_name, "message": "Have some residuals"}

다음의 예제는 /models/{model_name} path로 model_name path parameter를 받는다. model_nameModelName Enum타입으로 값을 받는데, str을 상속했기 때문에 "alexnet"이라는 str이 오면 ModelName.alexnet으로 치환된다.

curl http://127.0.0.1:8888/models/alexnet
{"model_name":"alexnet","message":"Deep Learning FTW!"}

다음과 같이 성공하는 것을 볼 수 있다.

curl http://127.0.0.1:8888/models/alexn
{
  "detail": [
    {
      "type": "enum",
      "loc": [
        "path",
        "model_name"
      ],
      "msg": "Input should be 'alexnet', 'resnet' or 'lenet'",
      "input": "alexn",
      "ctx": {
        "expected": "'alexnet', 'resnet' or 'lenet'"
      }
    }
  ]
}

Enum에 정의하지 않은 str값이 들어오면 위와 같이 에러를 반환한다.

4. Query parameters

https://fastapi.tiangolo.com/tutorial/query-params/

fastapi에서 GET method를 사용하는 handler에 대해서 path parameter가 따로 정의되지 않는 이상, handler의 parameter는 모두 query paramter이다.

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

@app.get("/items/")
async def read_item(skip: int = 0, limit: int = 10):
    return fake_items_db[skip: skip + limit]

query parameter로 skiplimit을 사용하고 있는 것이다. default값도 설정할 수 있는데, 여기서는 각각 010이다.

다음으로 요청을 보내보도록 하자.

curl "localhost:8888/items/?skip=1&limit=3"

[{"item_name":"Bar"},{"item_name":"Baz"}]

curl을 사용할 때 &을 linux &명령어로 해석할 수 있으므로 ""로 감싸주도록 하자.

사실 curl문에 있는 query parameter는 원래 str이지만 handler에 정의한 query parameter 타입으로 fastapi가 자동 변환시켜준다. 가령 위에서는 skiplimitint로 변형해서 준 것이다.

query parameter에 default값을 써주면 생략된 query parameter에 대해서 default값을 적용한다.

curl "localhost:8888/items/?limit=3"

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

skip은 query parameter로 지정하지 않았기 때문에 default로 적은 0이 사용되는 것이다.

한 가지 걱정은 혹시, query parameter가 path parameter와 섞일 수도 있을 것 같다는 것인데 path parameter를 지정해놓으면 섞일 일이 없다.

import uvicorn
from typing import Union
from fastapi import FastAPI

app = FastAPI()

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

path를 보면 /items/{item_id}로 path parameter를 지정하였기 때문에 read_itemitem_id가 query parameter가 아니라 path parameter라는 것을 확정할 수 있다. 반면 q는 path parameter지정을 안했기 때문에 query parameter이다. 기본적으로 GET method일 때 handler에 아무런 지정이 없는 parameter라면 query parameter라고 생각하면 된다.

curl "localhost:8888/items/10?q=name"
{"item_id":"10","q":"name"}

제대로 parameter들이 구분된 것을 볼 수 있다.

query parameter는 기본적으로 문자열이지만 지정한 type으로 type conversion이 발생한다는 것을 앞서 말했다. 이는 굉장히 스마트한데, bool의 경우에 Falsy, Truthy가 가능하다. 즉, 문맥에 따라 TrueFalse를 알아서 결정해준다는 것이다.

@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None, short: bool = False):
    item = {"item_id": item_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

위의 코드에서 short query parameter가 bool로 되어있다. 다음의 요청의 결과들은 모두 True로 판명된다.

curl "localhost:8888/items/foo?short=1"
{"item_id":"foo"}

curl "localhost:8888/items/foo?short=True"
{"item_id":"foo"}

curl "localhost:8888/items/foo?short=true"
{"item_id":"foo"}

curl "localhost:8888/items/foo?short=on"
{"item_id":"foo"}

curl "localhost:8888/items/foo?short=yes"
{"item_id":"foo"}

한 가지 재밌는 점은 query parameter에 default값을 적어주지 않으면, 필수 설정값으로 된다는 것이다. 반대로 default값을 적어주면 반드시 해당 query parameter를 쓸 필요없다는 것이다.

@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
    item = {"item_id": item_id, "needy": needy}
    return item

다음의 needy는 query parameter이지만 default값이 없으므로 필수로 적어주야하는 값이 된다.

curl "localhost:8888/items/foo"

{
  "detail": [
    {
      "type": "missing",
      "loc": [
        "query",
        "needy"
      ],
      "msg": "Field required",
      "input": null,
      "url": "https://errors.pydantic.dev/2.4/v/missing"
    }
  ]
}

만약, 필수는 아니지만 default값도 따로 써줄게 없다면 None으로 default값을 설정해주는 것이 좋다.

from typing import Optional

@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: Optional[str] = None):
    item = {"item_id": item_id, "needy": needy}
    return item

다음과 같이 needyOptional로 설정하고 None을 default값으로 쓰도록 하면 필수로 query parameter를 쓸 필요도 없고, 적당한 default값으로 설정할 수 있어서 좋다.

5. Reuqest Body

https://fastapi.tiangolo.com/tutorial/body/

brower에서 우리의 system으로 data를 전달하고 싶다면 request body를 통해서 전달할 수 있을 것이다. FastAPI에서 request body를 사용하기 위해서는 POST, DELETE, PATCH와 같은 http method에서만 가능하다. GET도 가능하지만 매무매우 verbose한 작업들을 많이하기 때문에 추천하지 않는다. 이는 GET으로 reqeust body를 보내는 것 자체가 비스펙이기 때문이다.

BaseModel 정의하기

먼저 pydantic의 BaseModel을 상속받아 request body를 만들도록 하자. fastapi가 설치되면 기본적으로 pydantic이 있기 때문에 따로 설치할 필요는 없다.

from typing import Union
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None

위와 같이 Item클래스를 만들어 BaseModel을 상속받도록 하자. 그 다음 차레대로 field의 타입들과 default값들을 써주면 되는데, pydantic이 BaseModel을 상속받은 Item클래스를 검사할 수 있게 된다. 따라서 pydantic에 의해 Item의 field들은 각각의 타입에 맞게 설정될 수 있는 것이다.

Request body

POST, PUT, DELETE와 같은 HTTP method들은 handler의 기본 parameter가 request body이다. 따라서 Item타입의 request body를 받는 변수를 하나 만들어주면된다.

@app.post("/items/")
async def create_item(item: Item):
    return item

다음과 같이 handler의 parameter로 request body를 넣을 수 있다. 또한, request body를 pydanticBaseModel을 이용한 객체로 사용하여 pydantic이 제공해주는 type검사 기능을 제공받을 수 있는 것이다.

curl -X POST -H "Content-Type: application/json"  "localhost:8888/items/" -d '{"name": "hello", "price": 10.2}'
{"name":"hello","description":null,"price":10.2,"tax":null}

요청에 성공한 것을 볼 수 있다. 재밌는 것은 -d요청한 request body 데이터가 key이름대로 Item타입의 request body item에 하나하나씩 들어간다는 것이다. 또한, None으로 default value가 정해진 descriptiontax는 전달해주지 않아도 성공적으로 받는 것을 볼 수 있다.

query parameter와 같이 default value가 있다면 필수적이지 않는 field이고 default가 없다면 필수적인 field라는 것을 알 수 있다.

Request body와 path, query parameter

request body와 path, query parameter를 같이 사용할 수 있는데, 먼저 request body와 path parameter를 같이 사용할 때이다.

@app.post("/items/{item_id}")
async def create_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.model_dump()}

path에서 /items/{item_id}item_id를 path parameter로 쓰고있다는 것을 내포하고 있다. 때문에 item은 request body이고 item_id는 path parameter라는 것을 알 수 있다.

curl -X POST -H "Content-Type: application/json"  "localhost:8888/items/12" -d '{"name": "hello", "price": 10.2}'
{"item_id":12,"name":"hello","description":null,"price":10.2,"tax":null}

문제없이 요청이 진행되는 것을 볼 수 있다.

다음으로 request body와 query parameter, path parameter를 같이 사용하기로 해보자. 한 가지 의문은 path parameter는 path에 지정하기 때문에 request body와 구분하기 쉽지만, request body를 어떻게 query parameter와 구분할 것이냐는 것이다.

여기서, fastapi는 다음의 로직으로 request body와 query parameter, path parameter를 찾는다.
1. path에 path parameter가 지정되어 있으면 해당 parameter는 path parameter이다.
2. parameter가 singular type(int, float, str, bool)을 사용한다면 query parameter이다.
3. parameter가 pydantic model로 만들어져 있다면 request body로 생각한다.

from typing import Union, Optional

@app.post("/items/{item_id}")
async def create_item(item_id: int, item: Item, q: Optional[str] = None):
    result = {"item_id": item_id, **item.model_dump()}
    if q:
        result.update({"q": q})
    return result

따라서, 다음의 코드에서 item_id는 path에 지정되어 있는 변수이기 때문에 path parameter가 되고, itemItem이라는 pydantic class를 사용하기 때문에 reqeust body이고, q는 singular type(str)이기 때문에 query parameter이다.

curl -X POST -H "Content-Type: application/json"  "localhost:8888/items/12?q=bye" -d '{"name": "hello", "price": 10.2}'
{"item_id":12,"name":"hello","description":null,"price":10.2,"tax":null,"q":"bye"}

문제없이 구분되는 것을 볼 수 있다.

물론, request body를 반드시 pydantic model로만 만들 필요는 없다. 단, 이를 위해서는 request body라는 것을 명시적으로 알려주는 Body parameter가 필요하다. 이에 대해서는 추후에 알아보도록 하자.

0개의 댓글

관련 채용 정보