Creando APIs en 5 Minutos con FastAPI

Ing. José Miguel Amaya Camacho

miguel.amaya99@gmail.com

José Miguel Amaya Camacho

  • Ing. Informático
  • Python/Django Remote Developer
  • Cofundador de Tallanix S.A.C
  • 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).

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 de FastAPI

  • Creamos nuestro entorno virtual, usaremos Pycharm como IDE.

  • pip install fastapi

  • pip install "uvicorn[standard]"

Type Hints

  • Son anotaciones que se agregan a las definiciones de funciones y variables para indicar qué tipos de datos se esperan. No son obligatorios, pero proporcionan documentación adicional.

# Sin type hints
name = "Alice"
age = 30
is_active = True

# Con type hints
name: str = "Alice"
age: int = 30
is_active: bool = True

Anotando Tipos en Funciones

Tipos de Argumentos

 

def greet(name: str) -> None:
    print(f"Hello, {name}!")

Tipos de Retorno

 

def add(x: int, y: int) -> int:
    return x + y

Pydantic

from pydantic import BaseModel

class Usuario(BaseModel):
    id: int
    nombre: str
    edad: int
    email: str

Biblioteca para validación de datos y gestión de modelos de datos. Proporciona una forma sencilla y eficiente de definir datos y garantizar que cumplan con ciertos esquemas.

Validación de Datos

try:
    usuario = Usuario(id='uno', 
    				  nombre='Juan', 
    				  edad='veinticinco', 
                      email='juan@example.com')
except ValueError as e:
    print(e)

Pydantic valida los datos al crear una instancia del modelo. Si los datos no cumplen con los tipos especificados, se lanzará una excepción:

Hola Mundo

from fastapi import FastAPI

app = FastAPI()


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

main.py

Lo corremos con uvicorn:

uvicorn main:app --reload

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:

Decorador de Operación

  • 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("/")

Función de Operación

  • 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.

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

Rutas y Métodos HTTP

  • Parámetros de Ruta: "item_id" será pasado a la función como el argumento item_id.
  • Parámetros de Consulta: 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 "&"
@app.get("/items/{item_id}")
def read_item(item_id: int, query_param: str = None):
    return {"item_id": item_id, "query_param": query_param}

Parámetros de Ruta

  • Un parámetro de ruta es un segmento variable de una URL que se puede capturar y utilizar dentro de tu aplicación. Los parámetros de ruta se definen usando llaves ({}) dentro de la ruta de la aplicación.
@app.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

Parámetros de Consulta

  • Son parámetros que se envían en la URL de una solicitud HTTP. Se utilizan para pasar información adicional al servidor. Por ejemplo, en la URL https://example.com/items?name=foo&price=10, name y price son query params.
@app.get("/items/")
async def read_item(name: str, price: float):
    return {"name": name, "price": price}

Otros métodos

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

Documentación Automática

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.

Validación de Datos

from pydantic import BaseModel


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


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

Inyección de Dependencias

  • Es la técnica de proporcionar automáticamente las dependencias necesarias a las rutas de una aplicación web.

  • Estas pueden ser funciones que proporcionan acceso a bases de datos, autenticación, servicios web externos o cualquier otra funcionalidad que una ruta pueda necesitar para funcionar correctamente.

Inyección de Dependencias

  • Automaticidad: FastAPI maneja automáticamente la resolución y la inyección de dependencias.

  • Reutilización de Código: Puedes definir una dependencia una vez y reutilizarla en múltiples rutas.

  • Testing: Facilita la escritura de pruebas unitarias, ya que puedes proporcionar fácilmente dependencias simuladas o de prueba cuando realizas pruebas de tus rutas.

  • Estructura Limpia: separa la lógica de la ruta de las operaciones que requieren dependencias externas.

Inyección de Dependencias

from fastapi import FastAPI, Depends


def get_current_user():
    return {"username": "johndoe"}


# Uso de la dependencia en una ruta
@app.get("/users/me")
def read_current_user(
	current_user: dict = Depends(get_current_user)
):
    return current_user

Autenticación y Autorización

  • Vamos a explorar cómo FastAPI permite agregar capas de seguridad a tus APIs, lo que incluye autenticación y autorización. Esto es esencial para proteger tus recursos y garantizar que solo usuarios autorizados puedan acceder a ciertas partes de tu API.

  • Debemos instalar python-multipart:

    • pip install python-multipart

Autenticación y Autorización

  • Esquema de seguridad: definimos el esquema oauth2_scheme que se utiliza para autenticar a los usuarios y obtener un token de acceso. El argumento tokenUrl especifica la URL donde los clientes pueden solicitar un token de acceso.

from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

Autenticación y Autorización

  • Ruta protegida: definimos la ruta /items/ que requiere autenticación de acceso. El parámetro token se inyecta automáticamente en la función cuando un usuario realiza una solicitud. FastAPI valida este token y se asegur que el usuario esté autenticado antes de permitir el acceso a la ruta. Si el token no es válido o no se proporciona, FastAPI devolverá un error HTTP.

from fastapi import FastAPI, Depends

@app.get("/items/")
async def read_items(token: str = Depends(oauth2_scheme)):
    return {"token": token}

Modelo User

  • Creamos un modelo de usuario Pydantic.

from typing import Optional
from pydantic import BaseModel, Field


class User(BaseModel):
    username: str
    email: str
    full_name: Optional[str]
    disabled: bool = Field(default=False)

Dependencia get_current_user

  • Creamos la dependencia get_current_user, que a su vez tendrá una dependencia con oauth2_scheme. Y utilizará la función fake_decode_token, que toma un token y devuelve el modelo de usuario:

def fake_decode_token(token):
    return User(username=token + "fakedecoded", 
    			email="john@example.com", 
                full_name="John Doe")


async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    return user

Inyectar al usuario actual

  • Ahora podemos usar el mismo Depends con nuestro get_current_user en la operación path:

@app.get("/users/me")
async def me(current_user: User = Depends(get_current_user)):
    return current_user

Login

  • Ruta para obtener un token de acceso: definimos una ruta /token/ que permite a los usuarios obtener un token de acceso. El token de acceso se utiliza para autenticar futuras solicitudes a rutas protegidas.

from fastapi.security import OAuth2PasswordRequestForm

@app.post("/token/")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    # Lógica de autenticación aquí (comprobar usuario y contraseña)
    return {"access_token": form_data.username, "token_type": "bearer"}

Bases de Datos

  • La integración de bases de datos es esencial para muchas aplicaciones web, y FastAPI simplifica este proceso al proporcionar una forma ordenada de configurar y utilizar bases de datos en nuestras aplicaciones.

  • Esto permite realizar operaciones CRUD de registros en la base de datos de manera eficiente.

Bases de Datos

  • Usaremos sqlalchemy y psycopg para conectarnos a una base de datos previamente creada en postgresql:

    • pip install sqlalchemy

    • pip install psycopg2-binary
       

Bases de Datos

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

DATABASE_URL = "postgresql://charla:s0p0rt3ccpp@localhost/charla"

engine = create_engine(DATABASE_URL)

SessionLocal = sessionmaker(
	autocommit=False, autoflush=False, bind=engine
)

Base = declarative_base()

Archivo database.py

Bases de Datos

from sqlalchemy import Column, Integer, String

from database import Base


class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(String)

Archivo models.py

Bases de Datos

from pydantic import BaseModel


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


class Item(ItemBase):
    id: int

    class Config:
        orm_mode = True


class ItemCreate(ItemBase):
    pass

Archivo schemas.py

Bases de Datos

from sqlalchemy.orm import Session

from models import Item
from schemas import ItemCreate


def create_item_db(db: Session, item: ItemCreate):
    db_item = Item(**item.model_dump())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item


def get_items(db: Session, skip: int = 0, limit: int = 100):
    return db.query(Item).offset(skip).limit(limit).all()

Archivo crud.py

Bases de Datos

from sqlalchemy.orm import Session

from crud import create_item_db, get_items
from database import SessionLocal, Base, engine
from schemas import ItemCreate, Item

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

Archivo main.py

Bases de Datos

@app.post("/items/", response_model=Item)
def create_item(item: ItemCreate, db: Session = Depends(get_db)):
    return create_item_db(db=db, item=item)


@app.get("/items_db/")
def items_db(
	skip: int = 0, 
    limit: int = 10, 
    db: Session = Depends(get_db)
):
    items = get_items(db, skip=skip, limit=limit)
    return items

Archivo main.py

Testing

  • 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"}

Que mas tiene FastAPI

  • Asincronismo

  • Manejo de FormData

  • Carga de archivos

  • Manejo de Cookies

  • Middleware

  • CORS

MUCHAS GRACIAS

Preguntas

Made with Slides.com