FastAPI를 배워보자 9일차 - Form

0

fastapi

목록 보기
9/13

Form data

https://fastapi.tiangolo.com/tutorial/request-forms/

Form

fastapi에서도 Form형식을 처리할 수 있다. 이를 위해서 별도의 모듈인 python-multipart가 필요하다.

pip3 install python-multipart

설치 후에 fastapi모듈에서 Form을 import해 사용하면 된다.

from typing import Annotated
from fastapi import FastAPI, Form

app = FastAPI()

@app.post("/login/")
async def login(username: Annotated[str, Form()], password: Annotated[str, Form()]):
    return {"username": username}

딱히 어려울 것이 없이 Query, Path, Cookie, Body를 쓰듯이 쓰면 된다. 또한, Form으로 지정하기만 하면 QueryBody와 함께 사용할 수 있다. Form만 적어주면 Form형식으로 처리되고 fastapi에서 제공하는 검증과 같은 기능들이 가능하다.

만약 Form을 지정하지 않으면 해당 변수는 query parameter나 json body로 치부된다.

이제 요청을 보내보도록 하자.

curl -X POST http//:localhost:8888/login/ -H "Content-Type: application/x-www-form-urlencoded" -d "username=value1&password=value2" 

다음의 응답이 온다.

{"username":"value1"}

성공적으로 form data를 받아 처리한 것을 확인할 수 있다.

한가지 아쉬운 것은 Form을 받는 pydantic model을 만들어서 받고 싶은데 fastapi에서는 지원하지 않는다는 것이다. 가령 다음과 같이 pydantic model을 만들어 handler에서 받고싶은 것이다.

class User(BaseModel):
    username: Annotated[str, Form()]
    passwrod: Annotated[str, Form()]

@app.post("/login/")
async def login(user: User):
    return {"username": user.username}

문제는 fastapi의 경우 http handler에서 parmeter는 post일 때는 body이고 무조건 json이다. Form따로 적지 않으면 default로 json으로 처리되는 것이다. 이를 해결하기위해서 다음과 같은 방법이 있다.

https://stackoverflow.com/questions/60127234/how-to-use-a-pydantic-model-with-form-data-in-fastapi

class AnyForm(BaseModel):
    any_param: str
    any_other_param: int = 1

    @classmethod
    def as_form(
        cls,
        any_param: str = Form(...),
        any_other_param: int = Form(1)
    ) -> AnyForm:
        return cls(any_param=any_param, any_other_param=any_other_param)

@router.post('')
async def any_view(form_data: AnyForm = Depends(AnyForm.as_form)):
        ...

Dependsany_view라는 handler가 실행하기 전에 먼저 실행되는 code라고 생각하면 된다. as_form이라는 클래스 메서드에 any_view에 관한 모든 parameter들이 argument들로 담기게 되고, 이를 Form에 맞게 변경해주는 것이 전부이다. 처음보면 조금 어려울 수 있는데, 이 부분은 Depends를 배우고 나서 다시보면 좋을 것 같다.

HTML을 통해서 form data가 전달될 때 일반적으로 form data는 x-www-form-urlencoded로 인코딩되어 전달된다. 그러나 file과 같은 경우들은 multipart/form-data형식으로 인코딩되는데, 이에 대해서는 어떻게 해야할까?

Request files

fastapi모듈에서 FileUploadFile을 가져올 수 있다.

import uvicorn

from typing import Annotated
from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/files/")
async def create_file(file: Annotated[bytes, File()]):
    print(str(file, 'utf-8'))
    return {"file_size": len(file)}

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile):
    return {"filename": file.filename}

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

create_file handler는 filebytes로 받는데 File()이라는 표시를 해주었다. 이렇게 해주어야 file을 query parameter나 json body로 읽지 않는다.

참고로 FileForm의 하위 클래스인데, FormBody의 하위 클래스이다.

요청을 보내기전에 다음의 file을 만들도록 하자.

  • temp.txt
hello world!

이제 해당 file을 가지고 요청을 보내보도록 하자.

curl -X POST localhost:8888/files/ -F 'file=@./temp.txt'

다음의 응답이 도착한다.

{"file_size":12}

서버에 찍힌 로그도 확인하면 다음과 같다.

hello world!

제대로 file이 도착한 것을 알 수 있다.

주의 Filebytes로 받아내면, 전체 file이 memory에 그대로 적제된다. 따라서 file사이즈가 작다면 이러한 방식이 가능하겠지만 file 사이즈가 커지는 순간부터는 OOM이 발생할 수도 있다.

사실 fastapi handler에 file을 전달하면 UploadFile type으로 전달된다. 그러나 Annotatedbytes와 함께 File을 사용하면 UploadFilebytes로 변환하여 받을 수 있다. 그러면 UploadFile이 도대체 무엇일까??

UploadFile

UploadFile을 사용하면 bytes에 비해서 다음과 같은 장점을 얻을 수 있다.

  1. File()을 parameter의 default value로 사용할 필요가 없다.
  2. spooled file을 사용한다.
    • file이 메모리에 저장될 때 최대 메모리 사이즈를 넘어서거나 한계치를 넘기면 disk에 저장된다.
  3. 이 덕분에 모든 메모리 자원을 사용하지 않아도 큰 image와 video, large binaries 등을 처리할 수 있다.
  4. uploaded file으로부터 metadata들을 쉽게 얻을 수 있다.
  5. file-like async interface를 가진다. (https://docs.python.org/3/glossary.html#term-file-like-object)
  6. 다른 라이브러리와 호환되는 file-like object인 실제 SpooledTemporaryFile object를 노출한다.

UploadFile은 다음의 attribute들을가진다.
1. filename: str로 업로드된 file의 이름
2. content_type: str로 content type이 무엇인지 알려준다. 가령 image/jpeg같은 것들이 있다.
3. file: SpooledTemporaryFilefile-like object이다. 이는 실제 python file로 file-like object를 지원하는 다른 library와 function에 사용할 수 있다.

UploadFile은 다음의 async method들을 지원한다. 이들은 내부적으로 SpooledTemporaryFile을 사용하여 상응하는 file method를 수행한다.
1. write(data): data(str, bytes)를 file에 쓴다.
2. read(size): file의 bytes, charaters를 size(int)만큼 읽는다.
3. seek(offset): file의 offset위치로 이동한다. 가령 0이면 맨처음 시작점이 딘다.
4. close(): file을 닫는다.

가령 file의 content를 읽을 때는 다음과 같이 사용하면 된다.

contents = await myfile.read()

원한다면 동기적으로 file을 읽는 method를 사용할 수 있다.

contents = myfile.file.read()

그렇다면 요청을 보내보도록 하자.

curl -X POST localhost:8888/uploadfile/ -F 'file=@./temp.txt'

다음의 응답이 돌아온다.

{"filename":"temp.txt"}

참고로 File()로 handler의 해당 parameter가 file임을 알려주면 자동으로 UploadFile로 변환되어 전달된다. 따라서 UploadFile타입을 직접 쓰기보다는 File()을 쓰도록 하자.

from typing import Annotated
from fastapi import FastAPI, File, UploadFile

app = FastAPI()

@app.post("/files/")
async def create_file(file: Annotated[UploadFile, File()]):
    return {"file_size": file.filename}

다음과 같이 사용하는 것이 좋다.

Form과 File 같이 사용하기

Form data가 file과 같이 쓰인다면 handler의 parameter에서 Form으로 일반 데이터를 처리하고 File로 file data를 처리할 수 있다. 단, Body는 같이 쓸 수 없다는 것에 유의하자. 이는 http 정의상 Content-Typeapplication/jsonform에 관련된 Content-Type이 양립할 수 없기 때문이다.

from typing import Annotated
from fastapi import FastAPI, File, Form, UploadFile

app = FastAPI()

@app.post("/files/")
async def create_file(
    file: Annotated[bytes, File()],
    fileb: Annotated[UploadFile, File()],
    token: Annotated[str, Form()],
):
    return {
        "file_size": len(file),
        "token": token,
        "fileb_content_type": fileb.content_type,
    }

다음과 같이 handler의 parameter로 form data는 Form으로 써주고 file인 곳은 File을 써주면 된다.

위에서도 말했듯이 formfile이 들어가면 Content-Typemultipart/form-data가 된다. 따라서 curl로 요청할 때 form data일지라도 -d가 아니라 -F를 쓰도록 한다.

curl -X POST localhost:8888/files/ -F 'file=@./temp.txt' -F 'fileb=@./temp.txt' -F "token=hello"

다음의 결과를 받게 된다.

{"file_size":12,"token":"hello","fileb_content_type":"text/plain"}

0개의 댓글

관련 채용 정보