Ing. José Miguel Amaya Camacho
miguel.amaya99@gmail.com
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.
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.
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
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
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.
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.
Creamos nuestro entorno virtual, usaremos Pycharm como IDE.
pip install fastapi
pip install "uvicorn[standard]"
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"Hola mundo"}
main.py
Lo corremos con uvicorn:
uvicorn main:app --reload
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:
Última parte de una URL empieza desde el primer "/":
https://example.com/items/fooSepara 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.
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("/")
@app.post()
@app.put()
@app.patch()
@app.delete()
@app.options()
@app.head()
@app.patch()
@app.trace()
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"}
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}
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}
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
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)
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}
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
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]
@app.get("/optional_params/")
def optional_params(q: str = None):
if q:
return {"q": q}
return {"Parámetro opcional no enviado": q}
@app.get("/required_params/")
def required_params(q: str):
return {"q": q}
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.
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
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)
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()}
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.
@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
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
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}
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
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"]
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).
Corremos el ejemplo:
pytest
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
main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_main():
return {"msg": "Hello World"}
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"}
Si queremos llamar a funciones asíncronas en nuestros tests, estos deben ser asíncronos.
Usar funciones asíncronas en los tests es útil, por ejemplo, para consultar una base de datos de forma asíncrona.
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
main.py:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Tomato"}
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"}
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():
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("/")
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.
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"}
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
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)
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.
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.
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"}
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 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.
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.
Preguntas