https://fastapi.tiangolo.com/tutorial/request-forms/
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
으로 지정하기만 하면 Query
나 Body
와 함께 사용할 수 있다. 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)):
...
Depends
는 any_view
라는 handler가 실행하기 전에 먼저 실행되는 code라고 생각하면 된다. as_form
이라는 클래스 메서드에 any_view
에 관한 모든 parameter들이 argument들로 담기게 되고, 이를 Form
에 맞게 변경해주는 것이 전부이다. 처음보면 조금 어려울 수 있는데, 이 부분은 Depends
를 배우고 나서 다시보면 좋을 것 같다.
HTML을 통해서 form
data가 전달될 때 일반적으로 form data는 x-www-form-urlencoded
로 인코딩되어 전달된다. 그러나 file
과 같은 경우들은 multipart/form-data
형식으로 인코딩되는데, 이에 대해서는 어떻게 해야할까?
fastapi
모듈에서 File
과 UploadFile
을 가져올 수 있다.
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는 file
을 bytes
로 받는데 File()
이라는 표시를 해주었다. 이렇게 해주어야 file
을 query parameter나 json body로 읽지 않는다.
참고로 File
은 Form
의 하위 클래스인데, Form
은 Body
의 하위 클래스이다.
요청을 보내기전에 다음의 file을 만들도록 하자.
hello world!
이제 해당 file을 가지고 요청을 보내보도록 하자.
curl -X POST localhost:8888/files/ -F 'file=@./temp.txt'
다음의 응답이 도착한다.
{"file_size":12}
서버에 찍힌 로그도 확인하면 다음과 같다.
hello world!
제대로 file이 도착한 것을 알 수 있다.
주의
File
을bytes
로 받아내면, 전체 file이 memory에 그대로 적제된다. 따라서file
사이즈가 작다면 이러한 방식이 가능하겠지만file
사이즈가 커지는 순간부터는 OOM이 발생할 수도 있다.
사실 fastapi
handler에 file을 전달하면 UploadFile
type으로 전달된다. 그러나 Annotated
에 bytes
와 함께 File
을 사용하면 UploadFile
을 bytes
로 변환하여 받을 수 있다. 그러면 UploadFile
이 도대체 무엇일까??
UploadFile
을 사용하면 bytes
에 비해서 다음과 같은 장점을 얻을 수 있다.
File()
을 parameter의 default value로 사용할 필요가 없다.spooled
file을 사용한다.file-like
async interface를 가진다. (https://docs.python.org/3/glossary.html#term-file-like-object)file-like
object인 실제 SpooledTemporaryFile
object를 노출한다. UploadFile
은 다음의 attribute들을가진다.
1. filename
: str
로 업로드된 file의 이름
2. content_type
: str
로 content type이 무엇인지 알려준다. 가령 image/jpeg
같은 것들이 있다.
3. file
: SpooledTemporaryFile
로 file-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
data가 file
과 같이 쓰인다면 handler의 parameter에서 Form
으로 일반 데이터를 처리하고 File
로 file data를 처리할 수 있다. 단, Body
는 같이 쓸 수 없다는 것에 유의하자. 이는 http 정의상 Content-Type
이 application/json
과 form
에 관련된 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
을 써주면 된다.
위에서도 말했듯이 form
에 file
이 들어가면 Content-Type
이 multipart/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"}