Response Model

2/25
Recursos

Aportes 12

Preguntas 1

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad? Crea una cuenta o inicia sesi贸n.

Lo que se hizo en esta clase tambien se hubiera podido hacer de otra manera valiendonos del parametro 鈥榬esponse_model_exclude鈥 el cual recibe un set de strings con los atributos que deseamos excluir del response:

@app.post(
'/person/new', 
response_model=Person,
response_model_exclude={'password'}
) 

como ves us茅 el mismo modelo Person pero le especifiqu茅 a FastAPI que no tuviera en cuenta el atributo password y asi no tuve que crear otro modelo de 0

PD: No estoy criticando lo que el profesor hizo ni diciendo que este mal hecho, solo comparto un approach diferente para que as铆 conozcamos nuevos parametros que quizas nos sean utiles algun dia

Una buena pr谩ctica es generar un modelo Base con atributos generales e ir heredando de este para ir agregando que atributos extra necesitamos, por ejemplo BaseUser no contiene el password, y CreationUser que hereda de BaseUser solo agregar铆a el password dentro de ella, as铆 se organiza un poco mejor los modelos.

Por si depronto no se acuerdan de activar el entorno virtual y carga la aplicaci贸n en el localhost

source venv/bin/activate
uvicorn main:app --reload

esta sentencias son en terminal dentro de la carpeta del proyecto.
Luego en el navegador colocamos la url de localhost

Para realiza una aplicaci贸n segura debemos tener dos temas en cuenta a la hora de la creaci贸n de la contrase帽a

  • La contrase帽a no se le env铆a al cliente
  • La contrase帽a no se almacena en texto plano

Response Model:
Es un atributo de nuestro path operation el cual es llamado desde nuestro Path Operation Decorator, el cual es utilizado para evitar el filtrado de informaci贸n sensible

Leyendo la documentacion de FastAPI me di cuenta de un parametro que podemos a帽adir que hace algo bastante interesante, se trata de 鈥榬esponse_model_exclude_unset鈥. Este parametro lo que hace es basicamente que si tu modelo de Pydantic tiene campos con default values estos sean excluidos si el usuario no los seteo (esto cuando tenemos el response_model_exclude_unset=True), si lo tienes en False entonces no va a hacer nada, mas info Aqu铆

Podr谩s seguir todos mis apuntes escritos en Notion en:
https://rain-scabiosa-74f.notion.site/Curso-de-FastAPI-Avanzado-189668eeee1a45168e46c5be56312967

Si te gusta dejame un corazoncito 鈾

Response Model

Es un atributo de nuestro path operation el cual es llamado desde nuestro Path Operation Decorator, el cual es utilizado para evitar el filtrado de informaci贸n sensible, en este definiremos un nuevo modelo que ser谩 el que se usar谩 para el Response, por ejemplo si una persona realiza un POST con su contrase帽a en 茅l, no podemos enviar la contrase帽a de regreso en el Response, esto ser铆a un problema de seguridad bastante fuerte, entonces teniendo el modelo Person

Modelo Person Original

# Python
from typing import Optional

# Pydantic
from pydantic import BaseModel
from pydantic import Field, EmailStr

class Person(BaseModel):
    first_name: str = Field(
        ...,
        min_length=1,
        max_length=50,
        example='John',
    )
    last_name: str = Field(
        ...,
        min_length=1,
        max_length=50,
        example='Doe',
    )
    age: int = Field(
        ...,
        gt=0,
        le=110,
        example=25,
    )
    hair_color: Optional[HairColor] = Field(
        default=None,
        example=HairColor.blonde,
    )
    is_married: Optional[bool] = Field(
        default=None,
        example=False
    )
    email = EmailStr()

    password: str = Field(
        ...,
        min_length=8,
        max_length=50,
        example='password'
    )

Modelo PersonOut

Este es el modelo que se enviara por medio del Response

class PersonOut(BaseModel):
    first_name: str
    last_name: str
    age: int
    hair_color: Optional[HairColor]
    is_married: Optional[bool]
    email: EmailStr

A帽adir el modelo en el Path Operation Decorator

@app.post('/person/new', response_model=PersonOut)
def create_person(person: Person = Body(...)):
    return person

Se a帽ade el modelo modificado para el response dentro del Path Operation Decorator en el parametro Response Model

En Windows y en Linux:
- minimizar todo Ctrl + K y dps Ctrl + 0
- maxmizar todo Ctrl + K y dps Ctrl + J

El response model, es una utilidad de seguridad importante ya que evita que se filtre informaci贸n que puede estar almacenada en nuestros modelos.
Su importancia radica en que al momento de que tu tengas X objeto (diccionario) que devuelvas, lo que har谩 FAST API ser谩 simplemente solo devolver los datos indicados en response_model.
Modelos y Enums a considerar

class HairColor(Enum):
    white = "white"
    red = "red"
    brown = "brown"
    blonde = "blonde"
    black = "black"

class Person(BaseModel):
    first_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    last_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    age: int = Field(
        ...,
        gt=0,
        lt=100
        )
    hair_color: Optional[HairColor] = Field(
        default=None
    )
    is_single: Optional[bool] = Field(
        default=True
    )
    email : Optional[EmailStr] = Field(default=None)
    class Config:
        schema_extra = {
            "example":{
                "first_name":"Rodrigo",
                "last_name": "Lopez",
                "age": 21,
                "hair_color":"black",
                "is_single":True
            }
        }
class Location(BaseModel):
    city: str
    state: str
    country: str
    class config:
        schema_extra ={
            "example": {
                "city":"Corregidora",
                "state":"Queretaro",
                "country":"Mexico"
            }
        }
class PersonOut(BaseModel):
    first_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    last_name: str = Field(...,
                            min_length=1,
                            max_length=50)
    age: int = Field(
        ...,
        gt=0,
        lt=100
        )
    city: str
    state: str
    country: str

Consideremos la siguiente ruta de la API

@app.put("/person/{person_id}")
async def update_person(
    person_id: int = Path
    (..., gt=0, title="Here you put the id of the person",description="The id of the person must be greater than 0",example=777),
    person: Person = Body(...),
    location: Location = Body(...)):
    data = {**dict(person),**dict(location)}
    data["adicional"] = "jajajas"
    return data

Si nososotros no definimos un modelo tendremos la siguiente salida al momento de llamar la api

{
  "first_name": "Rodrigo",
  "last_name": "Lopez",
  "age": 21,
  "hair_color": "black",
  "is_single": true,
  "email": null,
  "city": "string",
  "state": "string",
  "country": "string",
  "adicional": "jajajas"
}

Ahora si modificamos la funci贸n especificamos el response_model, de la siguiente forma:

@app.put("/person/{person_id}", response_model=PersonOut)
async def update_person(
    person_id: int = Path
    (..., gt=0, title="Here you put the id of the person",description="The id of the person must be greater than 0",example=777),
    person: Person = Body(...),
    location: Location = Body(...)):
    data = {**dict(person),**dict(location)}
    data["adicional"] = "jajajas"
    return data

Entonces tendremos la siguiente salida:

{
  "first_name": "Rodrigo",
  "last_name": "Lopez",
  "age": 21,
  "city": "string",
  "state": "string",
  "country": "string"
}

Ahora ya no aparece, ni el campo 鈥渁dicional鈥, ni todo el modelo, a pesar de que nuestro diccionario es m谩s grande y es lo que estamos regresando un diccionario, no un modelo.
Como nota adicional, es importante cubrir los campos obligatorios del response_model, puesto que de no hacerlo la respuesta del servidor sera de tipo 500, en caso de que reconstruyas otro objeto por aparte.

Refactorizaci贸n y modularizaci贸n

Nota importante: JAMAS debemos enviar la contrase帽a a un cliente. Ni almacenarla en texto plano, sino en un hash.

Response model es un atributo de nuestra path operation.

Otra soluci贸n es tener una clase BasePerson sin la contrase帽a y otra clase Person que herede de BasePerson y adem谩s se le agregue la contrase帽a as铆:

class BasePerson(BaseModel):
	# Atributos normales

class Person(BasePerson):
    password: SecretStr = Field(..., title="Password", min_length=8)

@app.post("/person/new", response_model=BasePerson)
def create_person(person: Person = Body(..., embed=True)):
    return person

Lo us茅 para ocultar el password y la tarjeta de cr茅dito:

class PersonOut(BaseModel): # Response model (protect pasword & credit card)
    first_name: str = Field(
        ...,
        min_length=3,
        max_length=35,
        example="Tony"
        )
    last_name: str = Field(
        ...,
        min_length=3,
        max_length=35,
        example="Demarkovich"
        )
    age_person: int = Field(
        ...,
        ge=18,
        le=115,
        example=39
        )
    color_hair: Optional[HairColor] = Field(default=None, example=HairColor.black)
    married: Optional[bool] = Field(default=None, example=True)
    email_usr : EmailStr = Field(default=None, example="[email protected]")
    web_usr : HttpUrl = Field(default=None, example="")

De esta manera se oculta esa informaci贸n:

Hablando de Tipos de Datos Exoticos y despues de indagar un poco en la documentaci贸n de Pydantic, tambien existe un tipo de dato llamado SecretStr, puedes verlo aqu铆, que nos permite esconder de manera visual los datos enviados de un string, y se utilizar铆a de la misma forma que utilizamos el tipo de dato EmailStr que usamos en el primer curso de FastAPI.

Quedar铆a de la siguiente forma:

from pydantic import BaseModel, Field, EmailStr, SecretStr

class Person(BaseModel):
    first_name: str = Field(
        ...,
        min_length=1,
        max_length=50
    )
    last_name: str = Field(
        ...,
        min_length=1,
        max_length=50
    )
    age: int = Field(
        ...,
        gt= 0,
        le= 100
    )
    hair_color: Optional[HairColor] = Field(default= None)
    is_married: Optional[bool] = Field(default=None)
    email: EmailStr = Field(...)
    password: SecretStr = Field(..., min_length=5)
    class Config:
        schema_extra = {
            "example": {
                "first_name": "Illich",
                "last_name": "Rada",
                "age": 21,
                "hair_color": "blonde",
                "is_married": False,
                "email": "[email protected]",
                "password": "12345UrlIllich"
            }
        }

Y al momento de verificarlo en la documentaci贸n de Swagger se ve asi:

{
  "first_name": "Illich",
  "last_name": "Rada",
  "age": 21,
  "hair_color": "blonde",
  "is_married": false,
  "email": "[email protected]",
  "password": "**********"
}

Puede que siga siendo una mejor soluci贸n crear un modelo nuevo excluyendo los valores que no queremos mostrar pero me parece que puede ser util para otro tipo de dato sensible que no queremos que quede de forma visual en la traza 馃槂