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 "Python package" (una colección de "Python modules"): app.
Contiene un archivo app/main.py. Como está dentro de un Python package (un directorio con un archivo __init__.py), es un "module" de ese package: app.main.
También hay un archivo app/dependencies.py, al igual que app/main.py, es un "module": app.dependencies.
Hay un subdirectorio app/routers/ con otro archivo __init__.py, por lo que es un "Python subpackage": app.routers.
El archivo app/routers/items.py está dentro de un package, app/routers/, por lo que es un submodule: app.routers.items.
Lo mismo con app/routers/users.py, es otro submodule: app.routers.users.
También hay un subdirectorio app/internal/ con otro archivo __init__.py, por lo que es otro "Python subpackage": app.internal.
Y el archivo app/internal/admin.py es otro submodule: app.internal.admin.
La misma estructura de archivos con comentarios:
.
├──app# "app" is a Python package│├──__init__.py# this file makes "app" a "Python package"│├──main.py# "main" module, e.g. import app.main│├──dependencies.py# "dependencies" module, e.g. import app.dependencies│└──routers# "routers" is a "Python subpackage"││├──__init__.py# makes "routers" a "Python subpackage"││├──items.py# "items" submodule, e.g. import app.routers.items││└──users.py# "users" submodule, e.g. import app.routers.users│└──internal# "internal" is a "Python subpackage"│├──__init__.py# makes "internal" a "Python subpackage"│└──admin.py# "admin" submodule, e.g. import app.internal.admin
Digamos que también tienes los endpoints dedicados a manejar "items" de tu aplicación en el módulo en app/routers/items.py.
Tienes operaciones de ruta para:
/items/
/items/{item_id}
Es toda la misma estructura que con app/routers/users.py.
Pero queremos ser más inteligentes y simplificar el código un poco.
Sabemos que todas las operaciones de ruta en este módulo tienen el mismo:
prefix de ruta: /items.
tags: (solo un tag: items).
responses adicionales.
dependencies: todas necesitan esa dependencia X-Token que creamos.
Así que, en lugar de añadir todo eso a cada operación de ruta, podemos añadirlo al APIRouter.
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=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("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(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"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
Como la ruta de cada operación de ruta tiene que empezar con /, como en:
También podemos añadir una lista de tags y responses adicionales que se aplicarán a todas las operaciones de ruta incluidas en este router.
Y podemos añadir una lista de dependencies que se añadirán a todas las operaciones de ruta en el router y se ejecutarán/resolverán para cada petición hecha a ellas.
Tener dependencies en el APIRouter puede ser usado, por ejemplo, para requerir autenticación para todo un grupo de operaciones de ruta. Incluso si las dependencies no se añaden individualmente a cada una de ellas.
Consejo
Los parámetros prefix, tags, responses, y dependencies son (como en muchos otros casos) simplemente una característica de FastAPI para ayudarte a evitar la duplicación de código.
Este código vive en el módulo app.routers.items, el archivo app/routers/items.py.
Y necesitamos obtener la función dependency del módulo app.dependencies, el archivo app/dependencies.py.
Así que usamos un import relativo con .. para las dependencies:
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=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("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(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"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
Si sabes perfectamente cómo funcionan los imports, continúa a la siguiente sección más abajo.
Un solo punto ., como en:
from.dependenciesimportget_token_header
significaría:
Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
buscar el módulo dependencies (un archivo imaginario en app/routers/dependencies.py)...
y de él, importar la función get_token_header.
Pero ese archivo no existe, nuestras dependencies están en un archivo en app/dependencies.py.
Recuerda cómo se ve nuestra estructura de app/archivos:
Los dos puntos .., como en:
from..dependenciesimportget_token_header
significar:
Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
ir al package padre (el directorio app/)...
y ahí, buscar el módulo dependencies (el archivo en app/dependencies.py)...
y de él, importar la función get_token_header.
¡Eso funciona correctamente! 🎉
De la misma manera, si hubiéramos usado tres puntos ..., como en:
from...dependenciesimportget_token_header
eso significaría:
Empezando en el mismo package en el que vive este módulo (el archivo app/routers/items.py) (el directorio app/routers/)...
ir al package padre (el directorio app/)...
luego ir al padre de ese package (no hay package padre, app es el nivel superior 😱)...
y ahí, buscar el módulo dependencies (el archivo en app/dependencies.py)...
y de él, importar la función get_token_header.
Eso se referiría a algún package por encima 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 ya sabes cómo funciona, así que puedes usar imports relativos en tus propias apps no importa qué tan complejas sean. 🤓
Agregar algunos tags, responses y dependencies personalizados¶
No estamos añadiendo el prefix /items ni los tags=["items"] a cada operación de ruta porque los añadimos al APIRouter.
Pero todavía podemos añadir mástags que se aplicarán a una operación de ruta específica, y también algunos responses adicionales específicos para esa operación de ruta:
app/routers/items.py
fromfastapiimportAPIRouter,Depends,HTTPExceptionfrom..dependenciesimportget_token_headerrouter=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("/")asyncdefread_items():returnfake_items_db@router.get("/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinfake_items_db:raiseHTTPException(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"}},)asyncdefupdate_item(item_id:str):ifitem_id!="plumbus":raiseHTTPException(status_code=403,detail="You can only update the item: plumbus")return{"item_id":item_id,"name":"The great Plumbus"}
Consejo
Esta última operación de ruta tendrá la combinación de tags: ["items", "custom"].
Y también tendrá ambos responses en la documentación, uno para 404 y uno para 403.
Importas y creas una clase FastAPI como normalmente.
Y podemos incluso declarar dependencies globales que se combinarán con las dependencies de cada APIRouter:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Ahora importamos los otros submódulos que tienen APIRouters:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():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 Python package app, podemos usar un solo punto . para importarlos usando "imports relativos".
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
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Ahora, incluyamos los routers de los submódulos users y items:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
Nota
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 añadir cada APIRouter a la aplicación principal de FastAPI.
Incluirá todas las rutas de ese router como parte de él.
Detalles Técnicos
FastAPI mantiene el APIRouter original y sus APIRoutes activos cuando el router se incluye en la aplicación principal.
Eso significa que las subclases personalizadas de APIRouter y APIRoute pueden seguir participando después de que el router se incluya.
Consejo
No tienes que preocuparte por el rendimiento al incluir routers.
Esto está diseñado para ser ligero y evitar añadir overhead a cada petición.
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 operaciones de ruta de admin que tu organización comparte entre varios proyectos.
Para este ejemplo será super simple. Pero digamos que como se comparte con otros proyectos de la organización, no podemos modificarlo y añadir un prefix, dependencies, tags, etc. directamente al APIRouter:
Pero todavía queremos establecer un prefix personalizado al incluir el APIRouter para que todas sus operaciones de ruta empiecen con /admin, queremos asegurararlo 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
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
De esa manera, el APIRouter original se quedará sin modificar, así que podemos seguir compartiendo ese mismo archivo app/internal/admin.py con otros proyectos de la organización.
El resultado es que en nuestra app, cada una de las operaciones de ruta del módulo admin tendrá:
El prefix /admin.
El tag admin.
La dependency get_token_header.
El response 418. 🍵
Pero eso solo afectará a ese APIRouter en nuestra app, no en ningún otro código que lo use.
Así que, por ejemplo, otros proyectos podrían usar el mismo APIRouter con un método de autenticación diferente.
También podemos añadir operaciones de ruta directamente a la app de FastAPI.
Aquí lo hacemos... solo para mostrar que podemos 🤷:
app/main.py
fromfastapiimportDepends,FastAPIfrom.dependenciesimportget_query_token,get_token_headerfrom.internalimportadminfrom.routersimportitems,usersapp=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("/")asyncdefroot():return{"message":"Hello Bigger Applications!"}
y funcionará correctamente, junto con todas las otras operaciones de ruta añadidas con app.include_router().
Detalles Muy Técnicos
Nota: este es un detalle muy técnico que probablemente puedes simplemente saltar.
Los APIRouters no están "montados", no están aislados del resto de la aplicación.
Esto es porque queremos incluir sus operaciones de ruta en el esquema OpenAPI y en las interfaces de usuario.
FastAPI mantiene los routers y las operaciones de ruta originales activos, y combina los prefijos del router, dependencias, tags, responses y otros metadatos al manejar peticiones y generar OpenAPI.
Como tu objeto app de FastAPI vive en app/main.py, puedes configurar el entrypoint en tu archivo pyproject.toml así:
[tool.fastapi]entrypoint="app.main:app"
eso es equivalente a importar así:
fromapp.mainimportapp
De esa manera el comando fastapi sabrá dónde encontrar tu app.
Nota
También podrías pasar la ruta al comando, así:
$ fastapidevapp/main.py
Pero tendrías que recordar pasar la ruta correcta cada vez que llames al comando fastapi.
Además, es posible que otras herramientas no puedan encontrarlo, por ejemplo la Extensión de VS Code o FastAPI Cloud, por lo que se recomienda usar el entrypoint en pyproject.toml.
Verás la documentación automática de la API, incluyendo las rutas de todos los submódulos, usando las rutas correctas (y prefijos) y los tags correctos:
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, ej. /api/v1 y /api/latest.
Esto es un uso avanzado que quizás no necesites realmente, pero está ahí por si lo necesitas.
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)
Puedes hacer esto antes o después de incluir router en la app de FastAPI. FastAPI seguirá incluyendo las operaciones de ruta de other_router en el routing y en OpenAPI.
Lo mismo aplica a las operaciones de ruta añadidas posteriormente a los routers. También serán visibles a través de la inclusión anterior.
Detalles Técnicos
Evita mutar directamente router.routes después de incluir un router. FastAPI trata la inclusión del router como en vivo, por lo que el router original y sus rutas permanecen como parte del routing y de la generación de OpenAPI.
Usa APIs documentadas como los path operation decorators y .include_router() para añadir rutas y routers.
Trata router.routes como un árbol de rutas de bajo nivel que puede contener definiciones de rutas y routers incluidos, y evita depender de él como una lista plana de operaciones de ruta finales.