Dependencias con yield¶
FastAPI admite dependencias que realizan algunos pasos adicionales después de finalizar.
Para hacer esto, usa yield
en lugar de return
y escribe los pasos adicionales (código) después.
Consejo
Asegúrate de usar yield
una sola vez por dependencia.
Nota técnica
Cualquier función que sea válida para usar con:
sería válida para usar como una dependencia en FastAPI.
De hecho, FastAPI usa esos dos decoradores internamente.
Una dependencia de base de datos con yield
¶
Por ejemplo, podrías usar esto para crear una sesión de base de datos y cerrarla después de finalizar.
Solo el código anterior e incluyendo la declaración yield
se ejecuta antes de crear un response:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
El valor generado es lo que se inyecta en path operations y otras dependencias:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
El código posterior a la declaración yield
se ejecuta después de crear el response pero antes de enviarla:
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Consejo
Puedes usar funciones async
o regulares.
FastAPI hará lo correcto con cada una, igual que con dependencias normales.
Una dependencia con yield
y try
¶
Si usas un bloque try
en una dependencia con yield
, recibirás cualquier excepción que se haya lanzado al usar la dependencia.
Por ejemplo, si algún código en algún punto intermedio, en otra dependencia o en una path operation, realiza un "rollback" en una transacción de base de datos o crea cualquier otro error, recibirás la excepción en tu dependencia.
Por lo tanto, puedes buscar esa excepción específica dentro de la dependencia con except SomeException
.
Del mismo modo, puedes usar finally
para asegurarte de que los pasos de salida se ejecuten, sin importar si hubo una excepción o no.
async def get_db():
db = DBSession()
try:
yield db
finally:
db.close()
Sub-dependencias con yield
¶
Puedes tener sub-dependencias y "árboles" de sub-dependencias de cualquier tamaño y forma, y cualquiera o todas ellas pueden usar yield
.
FastAPI se asegurará de que el "código de salida" en cada dependencia con yield
se ejecute en el orden correcto.
Por ejemplo, dependency_c
puede tener una dependencia de dependency_b
, y dependency_b
de dependency_a
:
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 Other versions and variants
from fastapi import Depends
from typing_extensions import Annotated
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
Y todas ellas pueden usar yield
.
En este caso, dependency_c
, para ejecutar su código de salida, necesita que el valor de dependency_b
(aquí llamado dep_b
) todavía esté disponible.
Y, a su vez, dependency_b
necesita que el valor de dependency_a
(aquí llamado dep_a
) esté disponible para su código de salida.
from typing import Annotated
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
🤓 Other versions and variants
from fastapi import Depends
from typing_extensions import Annotated
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a: Annotated[DepA, Depends(dependency_a)]):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b: Annotated[DepB, Depends(dependency_b)]):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends
async def dependency_a():
dep_a = generate_dep_a()
try:
yield dep_a
finally:
dep_a.close()
async def dependency_b(dep_a=Depends(dependency_a)):
dep_b = generate_dep_b()
try:
yield dep_b
finally:
dep_b.close(dep_a)
async def dependency_c(dep_b=Depends(dependency_b)):
dep_c = generate_dep_c()
try:
yield dep_c
finally:
dep_c.close(dep_b)
De la misma manera, podrías tener algunas dependencias con yield
y otras dependencias con return
, y hacer que algunas de esas dependan de algunas de las otras.
Y podrías tener una sola dependencia que requiera varias otras dependencias con yield
, etc.
Puedes tener cualquier combinación de dependencias que quieras.
FastAPI se asegurará de que todo se ejecute en el orden correcto.
Nota técnica
Esto funciona gracias a los Context Managers de Python.
FastAPI los utiliza internamente para lograr esto.
Dependencias con yield
y HTTPException
¶
Viste que puedes usar dependencias con yield
y tener bloques try
que capturen excepciones.
De la misma manera, podrías lanzar una HTTPException
o similar en el código de salida, después del yield
.
Consejo
Esta es una técnica algo avanzada, y en la mayoría de los casos realmente no lo necesitarás, ya que puedes lanzar excepciones (incluyendo HTTPException
) desde dentro del resto del código de tu aplicación, por ejemplo, en la path operation function.
Pero está ahí para ti si la necesitas. 🤓
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
🤓 Other versions and variants
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
data = {
"plumbus": {"description": "Freshly pickled plumbus", "owner": "Morty"},
"portal-gun": {"description": "Gun to create portals", "owner": "Rick"},
}
class OwnerError(Exception):
pass
def get_username():
try:
yield "Rick"
except OwnerError as e:
raise HTTPException(status_code=400, detail=f"Owner error: {e}")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id not in data:
raise HTTPException(status_code=404, detail="Item not found")
item = data[item_id]
if item["owner"] != username:
raise OwnerError(username)
return item
Una alternativa que podrías usar para capturar excepciones (y posiblemente también lanzar otra HTTPException
) es crear un Manejador de Excepciones Personalizado.
Dependencias con yield
y except
¶
Si capturas una excepción usando except
en una dependencia con yield
y no la lanzas nuevamente (o lanzas una nueva excepción), FastAPI no podrá notar que hubo una excepción, al igual que sucedería con Python normal:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 Other versions and variants
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("Oops, we didn't raise again, Britney 😱")
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
En este caso, el cliente verá un response HTTP 500 Internal Server Error como debería, dado que no estamos lanzando una HTTPException
o similar, pero el servidor no tendrá ningún registro ni ninguna otra indicación de cuál fue el error. 😱
Siempre raise
en Dependencias con yield
y except
¶
Si capturas una excepción en una dependencia con yield
, a menos que estés lanzando otra HTTPException
o similar, deberías volver a lanzar la excepción original.
Puedes volver a lanzar la misma excepción usando raise
:
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
🤓 Other versions and variants
from fastapi import Depends, FastAPI, HTTPException
from typing_extensions import Annotated
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
Tip
Prefer to use the Annotated
version if possible.
from fastapi import Depends, FastAPI, HTTPException
app = FastAPI()
class InternalError(Exception):
pass
def get_username():
try:
yield "Rick"
except InternalError:
print("We don't swallow the internal error here, we raise again 😎")
raise
@app.get("/items/{item_id}")
def get_item(item_id: str, username: str = Depends(get_username)):
if item_id == "portal-gun":
raise InternalError(
f"The portal gun is too dangerous to be owned by {username}"
)
if item_id != "plumbus":
raise HTTPException(
status_code=404, detail="Item not found, there's only a plumbus here"
)
return item_id
Ahora el cliente obtendrá el mismo response HTTP 500 Internal Server Error, pero el servidor tendrá nuestro InternalError
personalizado en los registros. 😎
Ejecución de dependencias con yield
¶
La secuencia de ejecución es más o menos como este diagrama. El tiempo fluye de arriba a abajo. Y cada columna es una de las partes que interactúa o ejecuta código.
sequenceDiagram
participant client as Client
participant handler as Exception handler
participant dep as Dep with yield
participant operation as Path Operation
participant tasks as Background tasks
Note over client,operation: Puede lanzar excepciones, incluyendo HTTPException
client ->> dep: Iniciar request
Note over dep: Ejecutar código hasta yield
opt raise Exception
dep -->> handler: Lanzar Exception
handler -->> client: Response HTTP de error
end
dep ->> operation: Ejecutar dependencia, por ejemplo, sesión de BD
opt raise
operation -->> dep: Lanzar Exception (por ejemplo, HTTPException)
opt handle
dep -->> dep: Puede capturar excepción, lanzar una nueva HTTPException, lanzar otra excepción
end
handler -->> client: Response HTTP de error
end
operation ->> client: Devolver response al cliente
Note over client,operation: El response ya fue enviado, no se puede cambiar
opt Tasks
operation -->> tasks: Enviar tareas en background
end
opt Lanzar otra excepción
tasks -->> tasks: Manejar excepciones en el código de la tarea en background
end
Información
Solo un response será enviado al cliente. Podría ser uno de los responses de error o será el response de la path operation.
Después de que se envíe uno de esos responses, no se podrá enviar ningún otro response.
Consejo
Este diagrama muestra HTTPException
, pero también podrías lanzar cualquier otra excepción que captures en una dependencia con yield
o con un Manejador de Excepciones Personalizado.
Si lanzas alguna excepción, será pasada a las dependencias con yield, incluyendo HTTPException
. En la mayoría de los casos querrás volver a lanzar esa misma excepción o una nueva desde la dependencia con yield
para asegurarte de que se maneje correctamente.
Dependencias con yield
, HTTPException
, except
y Tareas en Background¶
Advertencia
Probablemente no necesites estos detalles técnicos, puedes omitir esta sección y continuar abajo.
Estos detalles son útiles principalmente si estabas usando una versión de FastAPI anterior a 0.106.0 y usabas recursos de dependencias con yield
en tareas en background.
Dependencias con yield
y except
, Detalles Técnicos¶
Antes de FastAPI 0.110.0, si usabas una dependencia con yield
, y luego capturabas una excepción con except
en esa dependencia, y no volvías a lanzar la excepción, la excepción se lanzaría automáticamente/transmitiría a cualquier manejador de excepciones o al manejador de errores interno del servidor.
Esto se cambió en la versión 0.110.0 para corregir el consumo no gestionado de memoria de excepciones transmitidas sin un manejador (errores internos del servidor), y para que sea consistente con el comportamiento del código regular de Python.
Tareas en Background y Dependencias con yield
, Detalles Técnicos¶
Antes de FastAPI 0.106.0, lanzar excepciones después de yield
no era posible, el código de salida en dependencias con yield
se ejecutaba después de que el response se enviara, por lo que los Manejadores de Excepciones ya se habrían ejecutado.
Esto se diseñó de esta manera principalmente para permitir usar los mismos objetos "extraídos" por dependencias dentro de tareas en background, porque el código de salida se ejecutaría después de que las tareas en background terminaran.
Sin embargo, ya que esto significaría esperar a que el response viaje a través de la red mientras se retiene innecesariamente un recurso en una dependencia con yield (por ejemplo, una conexión a base de datos), esto se cambió en FastAPI 0.106.0.
Consejo
Además, una tarea en background es normalmente un conjunto independiente de lógica que debería manejarse por separado, con sus propios recursos (por ejemplo, su propia conexión a base de datos).
De esta manera probablemente tendrás un código más limpio.
Si solías depender de este comportamiento, ahora deberías crear los recursos para tareas en background dentro de la propia tarea en background, y usar internamente solo datos que no dependan de los recursos de las dependencias con yield
.
Por ejemplo, en lugar de usar la misma sesión de base de datos, crearías una nueva sesión de base de datos dentro de la tarea en background, y obtendrías los objetos de la base de datos usando esta nueva sesión. Y luego, en lugar de pasar el objeto de la base de datos como parámetro a la función de tarea en background, pasarías el ID de ese objeto y luego obtendrías el objeto nuevamente dentro de la función de tarea en background.
Context Managers¶
Qué son los "Context Managers"¶
Los "Context Managers" son aquellos objetos de Python que puedes usar en una declaración with
.
Por ejemplo, puedes usar with
para leer un archivo:
with open("./somefile.txt") as f:
contents = f.read()
print(contents)
Internamente, open("./somefile.txt")
crea un objeto llamado "Context Manager".
Cuando el bloque with
termina, se asegura de cerrar el archivo, incluso si hubo excepciones.
Cuando creas una dependencia con yield
, FastAPI creará internamente un context manager para ella y lo combinará con algunas otras herramientas relacionadas.
Usando context managers en dependencias con yield
¶
Advertencia
Esto es, más o menos, una idea "avanzada".
Si apenas estás comenzando con FastAPI, podrías querer omitirlo por ahora.
En Python, puedes crear Context Managers creando una clase con dos métodos: __enter__()
y __exit__()
.
También puedes usarlos dentro de las dependencias de FastAPI con yield
usando
with
o async with
en la función de dependencia:
class MySuperContextManager:
def __init__(self):
self.db = DBSession()
def __enter__(self):
return self.db
def __exit__(self, exc_type, exc_value, traceback):
self.db.close()
async def get_db():
with MySuperContextManager() as db:
yield db
Consejo
Otra manera de crear un context manager es con:
usándolos para decorar una función con un solo yield
.
Eso es lo que FastAPI usa internamente para dependencias con yield
.
Pero no tienes que usar los decoradores para las dependencias de FastAPI (y no deberías).
FastAPI lo hará por ti internamente.