본문 바로가기
코딩/FastAPI

FastAPI 배우기 - Extra Models

by 형큐 2023. 8. 1.
SMALL

공식문서를 번역(deepl)한 내용입니다

https://fastapi.tiangolo.com/ko/tutorial/extra-models/

 

Extra Models - FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production

fastapi.tiangolo.com


이전 예제에 이어서, 관련 모델이 두 개 이상 있는 것이 일반적입니다.

특히 사용자 모델의 경우 더욱 그렇습니다:

  • 입력 모델에 비밀번호를 사용할 수 있어야 합니다.
  • 출력 모델에는 비밀번호가 없어야 합니다.
  • 데이터베이스 모델에는 해시된 비밀번호가 필요할 수 있습니다.
❗주의
사용자의 일반 텍스트 비밀번호를 저장하지 마세요. 항상 확인할 수 있는 '보안 해시'를 저장하세요.
'비밀번호 해시'가 무엇인지 모르는 경우, 보안 챕터에서 '비밀번호 해시'가 무엇인지 알아보세요.

여러 모델

다음은 비밀번호 필드와 비밀번호가 사용되는 위치에서 모델이 어떻게 보일 수 있는지에 대한 일반적인 아이디어입니다:

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserIn(BaseModel):
    username: str
    password: str
    email: EmailStr
    full_name: str | None = None


class UserOut(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserInDB(BaseModel):
    username: str
    hashed_password: str
    email: EmailStr
    full_name: str | None = None


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

**user_in.dict() 정보

Pydantic's .dict()

user_in은 UserIn 클래스의 Pydantic 모델입니다.

Pydantic 모델에는 모델의 데이터와 함께 딕셔너리를 반환하는 .dict() 메서드가 있습니다.

따라서 다음과 같이 Pydantic 객체 user_in을 생성하면 됩니다:

user_in = UserIn(username="john", password="secret", email="john.doe@example.com")

를 클릭한 다음 호출합니다:

user_dict = user_in.dict()

변수에 데이터가 있는 딕셔너리가 생겼습니다(Pydantic 모델 객체가 아닌 딕셔너리입니다).

그리고 호출하면

print(user_dict)

를 사용하면 파이썬 딕셔너리를 얻을 수 있습니다:

{
    'username': 'john',
    'password': 'secret',
    'email': 'john.doe@example.com',
    'full_name': None,
}

딕셔너리 래핑 풀기

user_dict와 같은 딕셔너리를 **user_dict로 함수(또는 클래스)에 전달하면 파이썬은 이를 "언랩"합니다. 이 함수는 user_dict의 키와 값을 키-값 인수로 직접 전달합니다.

따라서 위의 user_dict를 계속 사용하여 다음과 같이 작성합니다:

UserInDB(**user_dict)

와 같은 결과를 가져올 수 있습니다:

UserInDB(
    username="john",
    password="secret",
    email="john.doe@example.com",
    full_name=None,
)

더 정확히 말하면, 향후 어떤 콘텐츠가 포함될지 모르는 user_dict를 직접 사용하는 것입니다:

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
)

다른 콘텐츠의 Pydantic 모델

위의 예제에서 user_in.dict()에서 user_dict를 가져온 것처럼 이 코드도 마찬가지입니다:

user_dict = user_in.dict()
UserInDB(**user_dict)

와 동일합니다:

 

UserInDB(**user_in.dict())

...user_in.dict()는 딕셔너리이므로 파이썬은 **를 앞에 붙인 UserInDB에 전달하여 이를 "언랩"하도록 합니다.


따라서 다른 Pydantic 모델의 데이터에서 Pydantic 모델을 얻습니다.

딕셔너리 및 추가 키워드 래핑 풀기

그리고 다음과 같이 추가 키워드 인수 해시드_암호=해시드_암호를 추가합니다:

UserInDB(**user_in.dict(), hashed_password=hashed_password)

...결국 이렇게 됩니다:

 

UserInDB(
    username = user_dict["username"],
    password = user_dict["password"],
    email = user_dict["email"],
    full_name = user_dict["full_name"],
    hashed_password = hashed_password,
)
❗경고
지원되는 추가 기능은 데이터의 가능한 흐름을 보여주기 위한 것일 뿐, 실제 보안을 제공하지는 않습니다.

중복 줄이기

코드 중복을 줄이는 것은 FastAPI의 핵심 아이디어 중 하나입니다.

코드가 중복되면 버그, 보안 문제, 코드 비동기화 문제(한 곳에서는 업데이트하지만 다른 곳에서는 업데이트하지 않는 경우) 등이 발생할 가능성이 높아집니다.

그리고 이러한 모델들은 모두 많은 데이터를 공유하고 속성 이름과 유형을 복제하고 있습니다.

더 잘할 수 있습니다.

다른 모델의 베이스 역할을 하는 UserBase 모델을 선언할 수 있습니다. 그런 다음 해당 모델의 속성(유형 선언, 유효성 검사 등)을 상속하는 하위 클래스를 만들 수 있습니다.

모든 데이터 변환, 유효성 검사, 문서화 등은 여전히 정상적으로 작동합니다.

이렇게 하면 모델 간의 차이점만 선언할 수 있습니다(일반 텍스트 암호 사용, 해시_암호 사용, 암호 없음):

from fastapi import FastAPI
from pydantic import BaseModel, EmailStr

app = FastAPI()


class UserBase(BaseModel):
    username: str
    email: EmailStr
    full_name: str | None = None


class UserIn(UserBase):
    password: str


class UserOut(UserBase):
    pass


class UserInDB(UserBase):
    hashed_password: str


def fake_password_hasher(raw_password: str):
    return "supersecret" + raw_password


def fake_save_user(user_in: UserIn):
    hashed_password = fake_password_hasher(user_in.password)
    user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
    print("User saved! ..not really")
    return user_in_db


@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
    user_saved = fake_save_user(user_in)
    return user_saved

Union or anyOf

응답을 두 가지 유형의 Union으로 선언할 수 있습니다. 즉, 응답이 두 가지 유형 중 하나가 될 수 있습니다.

이는 OpenAPI에서 anyOf로 정의됩니다.

이를 위해 표준 파이썬 유형 힌트 typing.Union을 사용합니다:

🔎참고
유니온을 정의할 때는 가장 구체적인 유형을 먼저 포함하고 그다음에 덜 구체적인 유형을 포함하세요. 아래 예제에서는 Union[PlaneItem, CarItem]에서 보다 구체적인 PlaneItem이 CarItem 앞에옵니다.
from typing import Union

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class BaseItem(BaseModel):
    description: str
    type: str


class CarItem(BaseItem):
    type: str = "car"


class PlaneItem(BaseItem):
    type: str = "plane"
    size: int


items = {
    "item1": {"description": "All my friends drive a low rider", "type": "car"},
    "item2": {
        "description": "Music is my aeroplane, it's my aeroplane",
        "type": "plane",
        "size": 5,
    },
}


@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
    return items[item_id]

Union in Python 3.10

이 예제에서는 Union[PlaneItem, CarItem]을 인자 response_model의 값으로 전달합니다.

유형 어노테이션에 넣는 대신 인수의 값으로 전달하기 때문에 Python 3.10에서도 Union을 사용해야 합니다.

유형 어노테이션에 있었다면 세로 막대를 사용할 수 있었을 것입니다:

some_variable: PlaneItem | CarItem

그러나 응답 모델=PlaneItem | CarItem에 넣으면 Python이 이를 유형 어노테이션으로 해석하는 대신 PlaneItem과 CarItem 간에 잘못된 연산을 수행하려고 시도하기 때문에 오류가 발생합니다.

List of models

같은 방식으로 객체 목록의 응답을 선언할 수 있습니다.

이를 위해서는 표준 파이썬 typing.List(또는 파이썬 3.9 이상에서는 그냥 list)를 사용하세요:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str


items = [
    {"name": "Foo", "description": "There comes my hero"},
    {"name": "Red", "description": "It's my aeroplane"},
]


@app.get("/items/", response_model=list[Item])
async def read_items():
    return items

임의 딕셔너리로 응답

 

Pydantic 모델을 사용하지 않고 키와 값의 유형만 선언하는 일반 임의 딕셔너리를 사용하여 응답을 선언할 수도 있습니다.

이 방법은 유효한 필드/속성 이름(Pydantic 모델에 필요한)을 미리 알지 못하는 경우에 유용합니다.

이 경우 typing.Dict(또는 Python 3.9 이상에서는 그냥 dict)를 사용할 수 있습니다:

from fastapi import FastAPI

app = FastAPI()


@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
    return {"foo": 2.3, "bar": 3.4}

Recap

여러 Pydantic 모델을 사용하고 각 사례에 대해 자유롭게 상속할 수 있습니다.

엔티티가 서로 다른 '상태'를 가질 수 있어야 하는 경우 엔티티당 하나의 데이터 모델을 가질 필요는 없습니다. 비밀번호, 비밀번호_해시, 비밀번호 없음 등의 상태를 가진 사용자 '엔티티'의 경우처럼 말입니다.

반응형
LIST