Taller de FastAPI

Ing. José Miguel Amaya Camacho

miguel.amaya99@gmail.com

José Miguel Amaya Camacho

  • Ing. Informático
  • Python/Django Remote Developer
  • Cofundador de XPRENDE - Latinoamérica
  • Full Stack Engineer en Efilm Online - España
  • Activista del Software Libre
  • Fundador de Python Piura

¿Qúe es una API REST?

  • Representational State Transfer.
  • Conjunto de reglas y convenciones arquitectónicas para diseñar servicios web que permiten la comunicación y la transferencia de datos entre sistemas de manera eficiente y coherente.
  • Se basan en los principios del protocolo HTTP (Hypertext Transfer Protocol) y se utilizan ampliamente en el desarrollo de aplicaciones y servicios web.

Características

  • Recursos: todo se considera un recurso. Cada recurso tiene una URL (Uniform Resource Locator) única.

  • Verbos HTTP: operaciones sobre recursos se realizan utilizando los verbos HTTP estándar: GET (recuperar datos), POST (crear nuevos recursos), PUT (actualizar recursos existentes) y DELETE (eliminar recursos).

  • Estado Stateless: Cada solicitud HTTP debe contener toda la información necesaria para comprender y procesar la solicitud. La API no almacena información sobre el estado del cliente entre solicitudes.

Características API REST

  • Formatos de Datos: Los datos se transfieren entre el cliente y el servidor en formatos estándar: JSON o XML.

  • Niveles de Abstracción: ofrece diferentes niveles de abstracción, se puede acceder a recursos específicos o hacer operaciones más generales.

  • Endpoints: Los recursos se acceden a través de endpoints, que son URLs específicas. Cada endpoint corresponde a un recurso o una colección de recursos.

Características API REST

  • Respuestas de Estado y Datos: código de estado HTTP que indica el resultado de la solicitud y los datos solicitados (si corresponde):

    • 200 OK, 201 Created, 204 No Content

    • 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found

    • 500 Internal Server Error, 503 Service Unavailable

Ventajas para el desarrollo

  • Separación entre el cliente y el servidor: mejora la portabilidad de la interfaz, facilita tener en servidores distintos el frontend y el backend, los componentes del desarrollo evolucionen de forma independiente.

  • Visibilidad, fiabilidad y escalabilidad. se puede migrar a otros servidores o cambiar la base de datos, siempre y cuando los datos de cada una de las peticiones se envíen de forma correcta.

  • La API REST siempre es independiente del tipo de plataformas o lenguajes

FastAPI

  • Es un web framework moderno para construir APIs con Python 3.6+.

  • Basado en las anotaciones de tipos estándar de Python.

  • Usado por Microsoft, Uber, Netflix, etc.

  • Es bastante rápido, primer lugar frente a frameworks de Python como Django y Flask y otros frameworks de PHP y Javascript.

Características

  • Utiliza tipado estático (Pydantic) para definir la estructura de datos de las solicitudes y respuestas de la API de manera precisa.
  • Facilita la implementación de autenticación y autorización de usuarios.
  • Realiza la validación de datos automáticamente, lo que ayuda a evitar errores.
  • Genera documentación interactiva para la API de forma automática.
  • Compatible con varias bases de datos y ORM (Object-Relational Mapping).
  • Velocidad y bajo consumo de recursos.

Sobre hombros de gigantes

  • Starlette, es un framework/toolkit ASGI(sucesor espiritual de WSGI) liviano, ideal para crear servicios web asíncronos en Python, lo usamos para las partes web.

  • Pydantic, para los datos.

Instalación de Python

  • Usaremos python3
  • En Windows debes descargarlo desde: https://www.python.org/
  • Si usas GNU/Linux viene instalado por defecto.
  • Para probar que python funciona sin problemas deben escribir en la terminal lo siguiente:

Instalación

  • Creamos nuestro entorno virtual, usaremos Pycharm como IDE.

  • pip install fastapi

  • pip install "uvicorn[standard]"

Nuestra primera App

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {"Hola mundo"}

main.py

Lo corremos con uvicorn:

uvicorn main:app --reload

Documentación Automática

Analicemos el código

  • FastAPI es una clase de Python que provee toda la funcionalidad para nuestra API.

from fastapi import FastAPI
app = FastAPI()
  • app, instancia de la clase FastAPI. Punto de interacción principal de toda la API.

  • Es la misma a la que nos referimos cuando usamos el comando de uvicorn:

Path/Endpoint

  • Última parte de una URL empieza desde el primer "/":

    https://example.com/items/foo
  • Separa los "intereses" y los "recursos".

  • Operaciones, métodos HTTP:

    • POST: para crear datos.

    • GET: para leer datos.

    • PUT / PATCH: para actualizar datos, total o parcialmente.

    • DELETE: para borrar datos.

Decorador de la Operación de Ruta

  • El decorador le dice a FastAPI que la función que tiene justo debajo está a cargo de manejar las peticiones que van a:

    • El path "/"

    • Usando una operación get

@app.get("/")

Otras operaciones

@app.post()
@app.put()
@app.patch()
@app.delete()
@app.options()
@app.head()
@app.patch()
@app.trace()

Función de la Operación de Ruta

  • Será llamada por FastAPI cada vez que reciba una petición en la URL "/" usando una operación GET.

  • Procesamos y devolvemos un resultado, se puede devolver dict, list, valores singulares como un str, int, etc. También modelos de Pydantic convertidos a JSON.

def root():
    return {"Hola mundo"}

Parámetros de Ruta

  • El valor del parámetro de path "item_id" será pasado a la función como el argumento item_id.

  • Probemos este nuevo endpoint.

@app.get("/items/{item_id}")
def read_item(item_id):
    return {"item_id": item_id}

Parámetros de Ruta con Tipos

  • En este caso, item_id es declarado como un int.

  • Con esta declaración de tipos FastAPI te da "parsing" automático del request.

@app.get("/items/types/{item_id}")
def read_item_type(item_id: int):
    return {"item_id": item_id}

Introducción a los Tipos de Python

  • Los "type hints" son una nueva sintaxis, desde Python 3.6+, que permite declarar el tipo de una variable.

  • Los editores y otras herramientas nos proveen un mejor soporte.

  • Todo FastAPI está basado en estos type hints.

  • Puedo usarlo con los tipos estándar de python: str, int, float, bool, bytes

Tipos con Sub-tipos

  • Para estructuras de datos que pueden contener otros valores, como dict, list, set y tuple, se usa el módulo estándar de Python: typing, que existe específicamente para darles soporte.

from typing import List


def process_items(items: List[str]):
    for item in items:
        print(item)

Valores Predefinidos

  • Si se tienen parámetros de path que necesitan valores predefinidos se puede usar Enum.

from enum import Enum


class Gender(str, Enum):
    MALE = "Masculino"
    FEMALE = "Famenino"
    UNDEFINED = "No define"


@app.get("/models/{gender}")
def get_gender(gender: Gender):
    return {"Gender": gender}

Parámetros de Consulta

  • Son parámetros de la función que no forman parte de los parámetros de ruta.

  • Son el conjunto de pares de key-value que van después del "?" en la URL, separados por caracteres "&".

    http://127.0.0.1:8000/items/?skip=0&limit=10

    Los parámetros de consulta(query) son:

    -skip: con un valor de 0
    -limit: con un valor de 10

Parámetros por Defecto

fake_items_db = [{"item_name": "Foo"}, {"item_name": "Bar"}, {"item_name": "Baz"}]


@app.get("/params/")
def params(skip: int = 0, limit: int = 10):
    return fake_items_db[skip: skip + limit]
  • Los parámetros de consulta no están fijos en una parte del path pueden ser opcionales y pueden tener valores por defecto.
  • En el siguiente ejemplo tenemos skip=0 y limit=10 como los valores por defecto.

Parámetros Opcionales

@app.get("/optional_params/")
def optional_params(q: str = None):
    if q:
        return {"q": q}
    return {"Parámetro opcional no enviado": q}
  • Se pueden declarar parámetros de query opcionales definiendo el valor por defecto como None:

Parámetros Requeridos

@app.get("/required_params/")
def required_params(q: str):
    return {"q": q}
  • Para hacer que un parámetro sea requerido, simplemente no se le asigna ningún valor por defecto.

Enviando Datos a la API

  • Request Body: Son datos enviados por el cliente a la API.

  • Response Body: Un cuerpo de respuesta son los datos que la API envía al cliente.

  • Para el envío de datos se utilizan los modelos de Pydantic que es una librería de Python para llevar a cabo validación de datos.

Modelos Pydantic

from pydantic import BaseModel
from typing import Optional


class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None


@app.post("/items/")
def create_item(item: Item):
    return item

Request Body

  • Se lee el cuerpo de la solicitud como JSON.

  • Se convierten los tipos correspondientes (si es necesario).

  • Se validan los datos.

  • Si los datos no son válidos, devolverá un error bastante detallado, indicando exactamente dónde y cuáles eran los datos incorrectos.

  • Recibimos los datos, los procesamos.

  • Devolvemos una respuesta(response body)

Request Body + Path Parámeters

  • FastAPI reconocerá que los parámetros de la función que coinciden con los parámetros de la ruta deben tomarse de la ruta, y que los parámetros de la función que se declaran como modelos de Pydantic deben tomarse del cuerpo de la solicitud.

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    return {"item_id": item_id, **item.dict()}

Request Body + Path + Query parameters

  • Si el parámetro se declara en la ruta, se utilizará como parámetro de ruta.

  • Si el parámetro es de un tipo singular (como int, float, str, bool, etc.) se interpretará como un parámetro de consulta.

  • Si se declara que el parámetro es del tipo de un modelo Pydantic, se interpretará como un cuerpo de solicitud.

Ejemplo

@app.put("/items_query/{item_id}")
def update_item_query(item_id: int, item: Item, q: str = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result

Response Model

  • Declaramos el modelo utilizado para la respuesta con el parámetro "response_model" en cualquiera de las operaciones de ruta.

class UserIn(BaseModel):
    username: str
    password: str
    name: str


class UserOut(BaseModel):
    username: str
    name: str


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

Response Status Code

  • HTTP envía un código de estado numérico de 3 dígitos como parte de la respuesta. Estos códigos de estado tienen asociado un número para reconocerlos.

  • Se puede especificar mediante el parámetro status_code en cualquiera de las operaciones de ruta:

@app.post("/status/", status_code=201)
def status(name: str):
    return {"name": name}

Testing

  • Gracias a Starlette, testear aplicaciones FastAPI es fácil y agradable.

  • Se basa en requests.

  • Se puede usar pytest directamente.

  • Debemos instalar ambos.

pip install requests
pip install pytest

Testing

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()


@app.get("/")
def root():
    return {"Hola mundo"}
    

client = TestClient(app)


def test_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == ["Hola mundo"]

Analicemos el código

  • Importamos TestClient.

  • Creamos una instancia de TestClient pasando la "app "como argumento.

  • Creamos una función cuyo nombre empiece con test_ (convenciones estándar de pytest).

  • Usamos el objeto TestClient igual que si fuera requests.

  • Escribimos declaraciones "assert" simples con las expresiones estándar de Python que necesitamos verificar (pytest estándar).

Testing

  • Corremos el ejemplo:

pytest

Separando los Tests

  • Separamos los tests en un archivo diferente dentro de app:

    • main.py, contiene el código a testear, el mismo del ejemplo anterior.

    • test_main.py, contiene el código del test

Separando los Tests

  • main.py:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def read_main():
    return {"msg": "Hello World"}

Separando los Tests

  • test_main.py:

from fastapi.testclient import TestClient

from .main import app

client = TestClient(app)


def test_read_main():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"msg": "Hello World"}

Tests Asíncronos

  • Si queremos llamar a funciones asíncronas en nuestros tests, estos deben ser asíncronos.

  • Con TestClient no se puede probar ni ejecutar funciones asíncronas en los tests de pytest (que son sincrónicos).
  • Usar funciones asíncronas en los tests es útil, por ejemplo, para consultar una base de datos de forma asíncrona.

Herramientas

  • pytest.mark.anyio, Anyio nos permite especificar que algunas funciones de prueba se llamarán de forma asíncrona.

  • HTTPX, es un cliente HTTP para Python 3 que nos permite realizar solicitudes asíncronas. Es casi idéntica a TestClient y por lo tanto a requests.

pip install httpx

Ejemplo

  • main.py:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Tomato"}

Ejemplo

  • test_main.py:

import pytest
from httpx import AsyncClient

from .main import app


@pytest.mark.anyio
async def test_root():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Tomato"}

Analicemos el código

  • El marcador @pytest.mark.anyio le dice a pytest que esta función de prueba debe llamarse de forma asíncrona.

  • La función de prueba ahora es async def en lugar de solo def como con TestClient.

@pytest.mark.anyio
async def test_root():

Analicemos el código

  • Creamos un AsyncClient con la "app" y le enviamos solicitudes asíncronas usando await.

  • Es equivalente a: 

    • response = client.get('/')
  • Estamos usando async/await con el nuevo  AsyncClient: la solicitud es asíncrona.
async with AsyncClient(app=app, base_url="http://test") as ac:
	response = await ac.get("/")

Tareas en Segundo Plano

  • Se pueden definir tareas en segundo plano para que se ejecuten después de devolver una respuesta.

  • Útil para operaciones que deben realizarse después de una solicitud, pero que el cliente no tiene que esperar a que se complete la operación antes de recibir respuesta.

  • Por ejemplo: notificaciones por correo electrónico enviadas después de realizar una acción, procesamiento de gran cantidad de datos.

Usando BackgroundTasks

from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def write_notification(email: str, message=""):
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)


@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Notification sent in the background"}

Usando BackgroundTasks

  • Importamos BackgroundTasks

from fastapi import BackgroundTasks
  • Definimos un parámetro de tipo BackgroundTasks en la función de operación de ruta:

@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
	pass

Función de Tarea

  • Creamos una función para que se ejecute como tarea en segundo plano. Es solo una función estándar asíncrona o normal que puede recibir parámetros.

  • En este caso, la función escribirá en un archivo y como la operación de escritura no usa async y await, la función es normal:

def write_notification(email: str, message=""):
    with open("log.txt", mode="w") as email_file:
        content = f"notification for {email}: {message}"
        email_file.write(content)

Agregar la Tarea

  • Dentro de la función de operación de ruta, se pasa la función de tarea al objeto BackgroundTasks con el método add_task():

@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_notification, email, message="some notification")
    return {"message": "Notification sent in the background"}
  • add_task() recibe como argumentos:

    • La función de tarea que se ejecutará en segundo plano (write_notification).

    • Los argumentos de la función de tarea.

Inyeccion de Dependencias

  • BackgroundTasks también funciona con el sistema de inyección de dependencias.

  • Se pueden declarar un parámetro de tipo BackgroundTasks en múltiples niveles: en una función de operación de ruta, en una dependencia, en una subdependencia, etc.

  • FastAPI sabe qué hacer en cada caso y cómo reutilizar el mismo objeto, de modo que todas las tareas en segundo plano se fusionen y se ejecuten en segundo plano después.

Inyeccion de Dependencias

def get_query(background_tasks: BackgroundTasks, q: str = None):
    if q:
        message = f"found query: {q}\n"
        background_tasks.add_task(write_log, message)
    return q


@app.post("/send-notification-dependency-injection/{email}")
async def send_notification_dependency_injection(email: str, 
                                                 background_tasks: BackgroundTasks,
                                                 q: str = Depends(get_query)):
    message = f"message to {email}\n"
    background_tasks.add_task(write_log, message)
    return {"message": "Message sent"}

Inyeccion de Dependencias

  • En este ejemplo, los mensajes se escribirán en el archivo log.txt después de enviar la respuesta.

  • Si hubo una "query" en la solicitud, se escribirá en el registro en una tarea en segundo plano.

  • Y luego, otra tarea en segundo plano generada en la función de operación de ruta escribirá un mensaje utilizando el parámetro de ruta de correo electrónico.

Para tener en cuenta

  • Para realizar una tarea pesada en segundo plano que no necesite que lo ejecute el mismo proceso se pueden usar otras herramientas más potentes como Celery.

  • Celery requiere configuraciones más complejas, un administrador de colas, como RabbitMQ o Redis, pero permite ejecutar tareas en segundo plano en múltiples procesos y, especialmente, en múltiples servidores.

Que mas tiene FastAPI

  • Asincronismo

  • Manejo de FormData

  • Carga de archivos

  • Manejo de Cookies

  • Middleware

  • CORS

  • Inyección de Dependencias

  • Conexión a Base de Datos: relacionales y no relacionales.

  • Seguridad, etc.

MUCHAS GRACIAS

Preguntas

Made with Slides.com