Modelos Extra¶
Continuando con el ejemplo anterior, será común tener más de un modelo relacionado.
Esto es especialmente el caso para los modelos de usuario, porque:
- El modelo de entrada necesita poder tener una contraseña.
- El modelo de salida no debería tener una contraseña.
- El modelo de base de datos probablemente necesitaría tener una contraseña hasheada.
Peligro
Nunca almacenes contraseñas de usuarios en texto plano. Siempre almacena un "hash seguro" que puedas verificar luego.
Si no lo sabes, aprenderás qué es un "hash de contraseña" en los capítulos de seguridad.
Múltiples modelos¶
Aquí tienes una idea general de cómo podrían ser los modelos con sus campos de contraseña y los lugares donde se utilizan:
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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: Union[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
Información
En Pydantic v1 el método se llamaba .dict()
, fue deprecado (pero aún soportado) en Pydantic v2, y renombrado a .model_dump()
.
Los ejemplos aquí usan .dict()
para compatibilidad con Pydantic v1, pero deberías usar .model_dump()
en su lugar si puedes usar Pydantic v2.
Acerca de **user_in.dict()
¶
.dict()
de Pydantic¶
user_in
es un modelo Pydantic de la clase UserIn
.
Los modelos Pydantic tienen un método .dict()
que devuelve un dict
con los datos del modelo.
Así que, si creamos un objeto Pydantic user_in
como:
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
y luego llamamos a:
user_dict = user_in.dict()
ahora tenemos un dict
con los datos en la variable user_dict
(es un dict
en lugar de un objeto modelo Pydantic).
Y si llamamos a:
print(user_dict)
obtendremos un dict
de Python con:
{
'username': 'john',
'password': 'secret',
'email': 'john.doe@example.com',
'full_name': None,
}
Desempaquetando un dict
¶
Si tomamos un dict
como user_dict
y lo pasamos a una función (o clase) con **user_dict
, Python lo "desempaquetará". Pasará las claves y valores del user_dict
directamente como argumentos clave-valor.
Así que, continuando con el user_dict
anterior, escribir:
UserInDB(**user_dict)
sería equivalente a algo como:
UserInDB(
username="john",
password="secret",
email="john.doe@example.com",
full_name=None,
)
O más exactamente, usando user_dict
directamente, con cualquier contenido que pueda tener en el futuro:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
)
Un modelo Pydantic a partir del contenido de otro¶
Como en el ejemplo anterior obtuvimos user_dict
de user_in.dict()
, este código:
user_dict = user_in.dict()
UserInDB(**user_dict)
sería equivalente a:
UserInDB(**user_in.dict())
...porque user_in.dict()
es un dict
, y luego hacemos que Python lo "desempaquete" al pasarlo a UserInDB
con el prefijo **
.
Así, obtenemos un modelo Pydantic a partir de los datos en otro modelo Pydantic.
Desempaquetando un dict
y palabras clave adicionales¶
Y luego agregando el argumento de palabra clave adicional hashed_password=hashed_password
, como en:
UserInDB(**user_in.dict(), hashed_password=hashed_password)
...termina siendo como:
UserInDB(
username = user_dict["username"],
password = user_dict["password"],
email = user_dict["email"],
full_name = user_dict["full_name"],
hashed_password = hashed_password,
)
Advertencia
Las funciones adicionales de soporte fake_password_hasher
y fake_save_user
son solo para demostrar un posible flujo de datos, pero por supuesto no proporcionan ninguna seguridad real.
Reducir duplicación¶
Reducir la duplicación de código es una de las ideas centrales en FastAPI.
Ya que la duplicación de código incrementa las posibilidades de bugs, problemas de seguridad, problemas de desincronización de código (cuando actualizas en un lugar pero no en los otros), etc.
Y estos modelos están compartiendo muchos de los datos y duplicando nombres y tipos de atributos.
Podríamos hacerlo mejor.
Podemos declarar un modelo UserBase
que sirva como base para nuestros otros modelos. Y luego podemos hacer subclases de ese modelo que heredan sus atributos (declaraciones de tipo, validación, etc).
Toda la conversión de datos, validación, documentación, etc. seguirá funcionando normalmente.
De esa manera, podemos declarar solo las diferencias entre los modelos (con password
en texto plano, con hashed_password
y sin contraseña):
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
🤓 Other versions and variants
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: Union[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
o anyOf
¶
Puedes declarar un response que sea la Union
de dos o más tipos, eso significa que el response sería cualquiera de ellos.
Se definirá en OpenAPI con anyOf
.
Para hacerlo, usa el type hint estándar de Python typing.Union
:
Nota
Al definir una Union
, incluye el tipo más específico primero, seguido por el tipo menos específico. En el ejemplo a continuación, el más específico PlaneItem
viene antes de CarItem
en Union[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]
🤓 Other versions and variants
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
en Python 3.10¶
En este ejemplo pasamos Union[PlaneItem, CarItem]
como el valor del argumento response_model
.
Porque lo estamos pasando como un valor a un argumento en lugar de ponerlo en una anotación de tipo, tenemos que usar Union
incluso en Python 3.10.
Si estuviera en una anotación de tipo podríamos haber usado la barra vertical, como:
some_variable: PlaneItem | CarItem
Pero si ponemos eso en la asignación response_model=PlaneItem | CarItem
obtendríamos un error, porque Python intentaría realizar una operación inválida entre PlaneItem
y CarItem
en lugar de interpretar eso como una anotación de tipo.
Lista de modelos¶
De la misma manera, puedes declarar responses de listas de objetos.
Para eso, usa el typing.List
estándar de Python (o simplemente list
en Python 3.9 y posteriores):
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
🤓 Other versions and variants
from typing import 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
Response con dict
arbitrario¶
También puedes declarar un response usando un dict
arbitrario plano, declarando solo el tipo de las claves y valores, sin usar un modelo Pydantic.
Esto es útil si no conoces los nombres de los campos/atributos válidos (que serían necesarios para un modelo Pydantic) de antemano.
En este caso, puedes usar typing.Dict
(o solo dict
en Python 3.9 y posteriores):
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}
🤓 Other versions and variants
from typing import 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}
Recapitulación¶
Usa múltiples modelos Pydantic y hereda libremente para cada caso.
No necesitas tener un solo modelo de datos por entidad si esa entidad debe poder tener diferentes "estados". Como el caso con la "entidad" usuario con un estado que incluye password
, password_hash
y sin contraseña.