Saltar a contenido

Aplicaciones más grandes - Múltiples archivos

Si estás construyendo una aplicación o una API web, rara vez podrás poner todo en un solo archivo.

FastAPI proporciona una herramienta conveniente para estructurar tu aplicación manteniendo toda la flexibilidad.

Información

Si vienes de Flask, esto sería el equivalente a los Blueprints de Flask.

Un ejemplo de estructura de archivos

Digamos que tienes una estructura de archivos como esta:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

Consejo

Hay varios archivos __init__.py: uno en cada directorio o subdirectorio.

Esto es lo que permite importar código de un archivo a otro.

Por ejemplo, en app/main.py podrías tener una línea como:

from app.routers import items
  • El directorio app contiene todo. Y tiene un archivo vacío app/__init__.py, por lo que es un "paquete de Python" (una colección de "módulos de Python"): app.
  • Contiene un archivo app/main.py. Como está dentro de un paquete de Python (un directorio con un archivo __init__.py), es un "módulo" de ese paquete: app.main.
  • También hay un archivo app/dependencies.py, al igual que app/main.py, es un "módulo": app.dependencies.
  • Hay un subdirectorio app/routers/ con otro archivo __init__.py, por lo que es un "subpaquete de Python": app.routers.
  • El archivo app/routers/items.py está dentro de un paquete, app/routers/, por lo que es un submódulo: app.routers.items.
  • Lo mismo con app/routers/users.py, es otro submódulo: app.routers.users.
  • También hay un subdirectorio app/internal/ con otro archivo __init__.py, por lo que es otro "subpaquete de Python": app.internal.
  • Y el archivo app/internal/admin.py es otro submódulo: app.internal.admin.

La misma estructura de archivos con comentarios:

.
├── app                  # "app" es un paquete de Python
│   ├── __init__.py      # este archivo hace que "app" sea un "paquete de Python"
│   ├── main.py          # módulo "main", por ejemplo import app.main
│   ├── dependencies.py  # módulo "dependencies", por ejemplo import app.dependencies
│   └── routers          # "routers" es un "subpaquete de Python"
│   │   ├── __init__.py  # hace que "routers" sea un "subpaquete de Python"
│   │   ├── items.py     # submódulo "items", por ejemplo import app.routers.items
│   │   └── users.py     # submódulo "users", por ejemplo import app.routers.users
│   └── internal         # "internal" es un "subpaquete de Python"
│       ├── __init__.py  # hace que "internal" sea un "subpaquete de Python"
│       └── admin.py     # submódulo "admin", por ejemplo import app.internal.admin

APIRouter

Digamos que el archivo dedicado solo a manejar usuarios es el submódulo en /app/routers/users.py.

Quieres tener las path operations relacionadas con tus usuarios separadas del resto del código, para mantenerlo organizado.

Pero todavía es parte de la misma aplicación/web API de FastAPI (es parte del mismo "paquete de Python").

Puedes crear las path operations para ese módulo usando APIRouter.

Importar APIRouter

Lo importas y creas una "instance" de la misma manera que lo harías con la clase FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Path operations con APIRouter

Y luego lo usas para declarar tus path operations.

Úsalo de la misma manera que usarías la clase FastAPI:

app/routers/users.py
from fastapi import APIRouter

router = APIRouter()


@router.get("/users/", tags=["users"])
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]


@router.get("/users/me", tags=["users"])
async def read_user_me():
    return {"username": "fakecurrentuser"}


@router.get("/users/{username}", tags=["users"])
async def read_user(username: str):
    return {"username": username}

Puedes pensar en APIRouter como una clase "mini FastAPI".

Se soportan todas las mismas opciones.

Todos los mismos parameters, responses, dependencies, tags, etc.

Consejo

En este ejemplo, la variable se llama router, pero puedes nombrarla como quieras.

Vamos a incluir este APIRouter en la aplicación principal de FastAPI, pero primero, revisemos las dependencias y otro APIRouter.

Dependencias

Vemos que vamos a necesitar algunas dependencias usadas en varios lugares de la aplicación.

Así que las ponemos en su propio módulo dependencies (app/dependencies.py).

Ahora utilizaremos una dependencia simple para leer un encabezado X-Token personalizado:

app/dependencies.py
from typing import Annotated

from fastapi import Header, HTTPException


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")
app/dependencies.py
from fastapi import Header, HTTPException
from typing_extensions import Annotated


async def get_token_header(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Consejo

Preferiblemente usa la versión Annotated si es posible.

app/dependencies.py
from fastapi import Header, HTTPException


async def get_token_header(x_token: str = Header()):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")


async def get_query_token(token: str):
    if token != "jessica":
        raise HTTPException(status_code=400, detail="No Jessica token provided")

Consejo

Estamos usando un encabezado inventado para simplificar este ejemplo.

Pero en casos reales obtendrás mejores resultados usando las utilidades de Seguridad integradas.

Otro módulo con APIRouter

Digamos que también tienes los endpoints dedicados a manejar "items" de tu aplicación en el módulo app/routers/items.py.

Tienes path operations para:

  • /items/
  • /items/{item_id}

Es toda la misma estructura que con app/routers/users.py.

Pero queremos ser más inteligentes y simplificar un poco el código.

Sabemos que todas las path operations en este módulo tienen el mismo:

  • Prefijo de path: /items.
  • tags: (solo una etiqueta: items).
  • responses extra.
  • dependencies: todas necesitan esa dependencia X-Token que creamos.

Entonces, en lugar de agregar todo eso a cada path operation, podemos agregarlo al APIRouter.

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Como el path de cada path operation tiene que empezar con /, como en:

@router.get("/{item_id}")
async def read_item(item_id: str):
    ...

...el prefijo no debe incluir un / final.

Así que, el prefijo en este caso es /items.

También podemos agregar una lista de tags y responses extra que se aplicarán a todas las path operations incluidas en este router.

Y podemos agregar una lista de dependencies que se añadirá a todas las path operations en el router y se ejecutarán/solucionarán por cada request que les haga.

Consejo

Nota que, al igual que dependencias en decoradores de path operations, ningún valor será pasado a tu path operation function.

El resultado final es que los paths de item son ahora:

  • /items/
  • /items/{item_id}

...como pretendíamos.

  • Serán marcados con una lista de tags que contiene un solo string "items".
  • Estos "tags" son especialmente útiles para los sistemas de documentación interactiva automática (usando OpenAPI).
  • Todos incluirán las responses predefinidas.
  • Todas estas path operations tendrán la lista de dependencies evaluadas/ejecutadas antes de ellas.
  • Si también declaras dependencias en una path operation específica, también se ejecutarán.
  • Las dependencias del router se ejecutan primero, luego las dependencias en el decorador, y luego las dependencias de parámetros normales.
  • También puedes agregar dependencias de Security con scopes.

Consejo

Tener dependencies en el APIRouter puede ser usado, por ejemplo, para requerir autenticación para un grupo completo de path operations. Incluso si las dependencias no son añadidas individualmente a cada una de ellas.

Revisa

Los parámetros prefix, tags, responses, y dependencies son (como en muchos otros casos) solo una funcionalidad de FastAPI para ayudarte a evitar la duplicación de código.

Importar las dependencias

Este código vive en el módulo app.routers.items, el archivo app/routers/items.py.

Y necesitamos obtener la función de dependencia del módulo app.dependencies, el archivo app/dependencies.py.

Así que usamos un import relativo con .. para las dependencias:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Cómo funcionan los imports relativos

Consejo

Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección.

Un solo punto ., como en:

from .dependencies import get_token_header

significaría:

  • Partiendo en el mismo paquete en el que este módulo (el archivo app/routers/items.py) habita (el directorio app/routers/)...
  • busca el módulo dependencies (un archivo imaginario en app/routers/dependencies.py)...
  • y de él, importa la función get_token_header.

Pero ese archivo no existe, nuestras dependencias están en un archivo en app/dependencies.py.

Recuerda cómo se ve nuestra estructura de aplicación/archivo:


Los dos puntos .., como en:

from ..dependencies import get_token_header

significan:

  • Partiendo en el mismo paquete en el que este módulo (el archivo app/routers/items.py) habita (el directorio app/routers/)...
  • ve al paquete padre (el directorio app/)...
  • y allí, busca el módulo dependencies (el archivo en app/dependencies.py)...
  • y de él, importa la función get_token_header.

¡Eso funciona correctamente! 🎉


De la misma manera, si hubiéramos usado tres puntos ..., como en:

from ...dependencies import get_token_header

eso significaría:

  • Partiendo en el mismo paquete en el que este módulo (el archivo app/routers/items.py) habita (el directorio app/routers/)...
  • ve al paquete padre (el directorio app/)...
  • luego ve al paquete padre de ese paquete (no hay paquete padre, app es el nivel superior 😱)...
  • y allí, busca el módulo dependencies (el archivo en app/dependencies.py)...
  • y de él, importa la función get_token_header.

Eso se referiría a algún paquete arriba de app/, con su propio archivo __init__.py, etc. Pero no tenemos eso. Así que, eso lanzaría un error en nuestro ejemplo. 🚨

Pero ahora sabes cómo funciona, para que puedas usar imports relativos en tus propias aplicaciones sin importar cuán complejas sean. 🤓

Agregar algunos tags, responses, y dependencies personalizados

No estamos agregando el prefijo /items ni los tags=["items"] a cada path operation porque los hemos añadido al APIRouter.

Pero aún podemos agregar más tags que se aplicarán a una path operation específica, y también algunas responses extra específicas para esa path operation:

app/routers/items.py
from fastapi import APIRouter, Depends, HTTPException

from ..dependencies import get_token_header

router = APIRouter(
    prefix="/items",
    tags=["items"],
    dependencies=[Depends(get_token_header)],
    responses={404: {"description": "Not found"}},
)


fake_items_db = {"plumbus": {"name": "Plumbus"}, "gun": {"name": "Portal Gun"}}


@router.get("/")
async def read_items():
    return fake_items_db


@router.get("/{item_id}")
async def read_item(item_id: str):
    if item_id not in fake_items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return {"name": fake_items_db[item_id]["name"], "item_id": item_id}


@router.put(
    "/{item_id}",
    tags=["custom"],
    responses={403: {"description": "Operation forbidden"}},
)
async def update_item(item_id: str):
    if item_id != "plumbus":
        raise HTTPException(
            status_code=403, detail="You can only update the item: plumbus"
        )
    return {"item_id": item_id, "name": "The great Plumbus"}

Consejo

Esta última path operation tendrá la combinación de tags: ["items", "custom"].

Y también tendrá ambas responses en la documentación, una para 404 y otra para 403.

El FastAPI principal

Ahora, veamos el módulo en app/main.py.

Aquí es donde importas y usas la clase FastAPI.

Este será el archivo principal en tu aplicación que conecta todo.

Importar FastAPI

Importas y creas una clase FastAPI como de costumbre.

Y podemos incluso declarar dependencias globales que se combinarán con las dependencias para cada APIRouter:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Importar el APIRouter

Ahora importamos los otros submódulos que tienen APIRouters:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Como los archivos app/routers/users.py y app/routers/items.py son submódulos que son parte del mismo paquete de Python app, podemos usar un solo punto . para importarlos usando "imports relativos".

Cómo funciona la importación

La sección:

from .routers import items, users

significa:

  • Partiendo en el mismo paquete en el que este módulo (el archivo app/main.py) habita (el directorio app/)...
  • busca el subpaquete routers (el directorio en app/routers/)...
  • y de él, importa el submódulo items (el archivo en app/routers/items.py) y users (el archivo en app/routers/users.py)...

El módulo items tendrá una variable router (items.router). Este es el mismo que creamos en el archivo app/routers/items.py, es un objeto APIRouter.

Y luego hacemos lo mismo para el módulo users.

También podríamos importarlos así:

from app.routers import items, users

Información

La primera versión es un "import relativo":

from .routers import items, users

La segunda versión es un "import absoluto":

from app.routers import items, users

Para aprender más sobre Paquetes y Módulos de Python, lee la documentación oficial de Python sobre Módulos.

Evitar colisiones de nombres

Estamos importando el submódulo items directamente, en lugar de importar solo su variable router.

Esto se debe a que también tenemos otra variable llamada router en el submódulo users.

Si hubiéramos importado uno después del otro, como:

from .routers.items import router
from .routers.users import router

el router de users sobrescribiría el de items y no podríamos usarlos al mismo tiempo.

Así que, para poder usar ambos en el mismo archivo, importamos los submódulos directamente:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Incluir los APIRouters para users y items

Ahora, incluyamos los routers de los submódulos users y items:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

Información

users.router contiene el APIRouter dentro del archivo app/routers/users.py.

Y items.router contiene el APIRouter dentro del archivo app/routers/items.py.

Con app.include_router() podemos agregar cada APIRouter a la aplicación principal de FastAPI.

Incluirá todas las rutas de ese router como parte de ella.

Detalles Técnicos

En realidad creará internamente una path operation para cada path operation que fue declarada en el APIRouter.

Así, detrás de escena, funcionará como si todo fuera la misma única aplicación.

Revisa

No tienes que preocuparte por el rendimiento al incluir routers.

Esto tomará microsegundos y solo sucederá al inicio.

Así que no afectará el rendimiento. ⚡

Incluir un APIRouter con un prefix, tags, responses, y dependencies personalizados

Ahora, imaginemos que tu organización te dio el archivo app/internal/admin.py.

Contiene un APIRouter con algunas path operations de administración que tu organización comparte entre varios proyectos.

Para este ejemplo será súper simple. Pero digamos que porque está compartido con otros proyectos en la organización, no podemos modificarlo y agregar un prefix, dependencies, tags, etc. directamente al APIRouter:

app/internal/admin.py
from fastapi import APIRouter

router = APIRouter()


@router.post("/")
async def update_admin():
    return {"message": "Admin getting schwifty"}

Pero aún queremos configurar un prefix personalizado al incluir el APIRouter para que todas sus path operations comiencen con /admin, queremos asegurarlo con las dependencies que ya tenemos para este proyecto, y queremos incluir tags y responses.

Podemos declarar todo eso sin tener que modificar el APIRouter original pasando esos parámetros a app.include_router():

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

De esa manera, el APIRouter original permanecerá sin modificar, por lo que aún podemos compartir ese mismo archivo app/internal/admin.py con otros proyectos en la organización.

El resultado es que, en nuestra aplicación, cada una de las path operations del módulo admin tendrá:

  • El prefix /admin.
  • El tag admin.
  • La dependencia get_token_header.
  • La response 418. 🍵

Pero eso solo afectará a ese APIRouter en nuestra aplicación, no en ningún otro código que lo utilice.

Así, por ejemplo, otros proyectos podrían usar el mismo APIRouter con un método de autenticación diferente.

Incluir una path operation

También podemos agregar path operations directamente a la aplicación de FastAPI.

Aquí lo hacemos... solo para mostrar que podemos 🤷:

app/main.py
from fastapi import Depends, FastAPI

from .dependencies import get_query_token, get_token_header
from .internal import admin
from .routers import items, users

app = FastAPI(dependencies=[Depends(get_query_token)])


app.include_router(users.router)
app.include_router(items.router)
app.include_router(
    admin.router,
    prefix="/admin",
    tags=["admin"],
    dependencies=[Depends(get_token_header)],
    responses={418: {"description": "I'm a teapot"}},
)


@app.get("/")
async def root():
    return {"message": "Hello Bigger Applications!"}

y funcionará correctamente, junto con todas las otras path operations añadidas con app.include_router().

Detalles Muy Técnicos

Nota: este es un detalle muy técnico que probablemente puedes simplemente omitir.


Los APIRouters no están "montados", no están aislados del resto de la aplicación.

Esto se debe a que queremos incluir sus path operations en el esquema de OpenAPI y las interfaces de usuario.

Como no podemos simplemente aislarlos y "montarlos" independientemente del resto, se "clonan" las path operations (se vuelven a crear), no se incluyen directamente.

Revisa la documentación automática de la API

Ahora, ejecuta tu aplicación:

$ fastapi dev app/main.py

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Y abre la documentación en http://127.0.0.1:8000/docs.

Verás la documentación automática de la API, incluyendo los paths de todos los submódulos, usando los paths correctos (y prefijos) y las tags correctas:

Incluir el mismo router múltiples veces con diferentes prefix

También puedes usar .include_router() múltiples veces con el mismo router usando diferentes prefijos.

Esto podría ser útil, por ejemplo, para exponer la misma API bajo diferentes prefijos, por ejemplo, /api/v1 y /api/latest.

Este es un uso avanzado que quizás no necesites realmente, pero está allí en caso de que lo necesites.

Incluir un APIRouter en otro

De la misma manera que puedes incluir un APIRouter en una aplicación FastAPI, puedes incluir un APIRouter en otro APIRouter usando:

router.include_router(other_router)

Asegúrate de hacerlo antes de incluir router en la aplicación de FastAPI, para que las path operations de other_router también se incluyan.