Dependencias Avanzadas¶
Dependencias con par谩metros¶
Todas las dependencias que hemos visto son una funci贸n o clase fija.
Pero podr铆a haber casos en los que quieras poder establecer par谩metros en la dependencia, sin tener que declarar muchas funciones o clases diferentes.
Imaginemos que queremos tener una dependencia que revise si el par谩metro de query q contiene alg煤n contenido fijo.
Pero queremos poder parametrizar ese contenido fijo.
Una instance "callable"¶
En Python hay una forma de hacer que una instance de una clase sea un "callable".
No la clase en s铆 (que ya es un callable), sino una instance de esa clase.
Para hacer eso, declaramos un m茅todo __call__:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
馃 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
En este caso, este __call__ es lo que FastAPI usar谩 para comprobar par谩metros adicionales y sub-dependencias, y es lo que llamar谩 para pasar un valor al par谩metro en tu path operation function m谩s adelante.
Parametrizar la instance¶
Y ahora, podemos usar __init__ para declarar los par谩metros de la instance que podemos usar para "parametrizar" la dependencia:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
馃 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
En este caso, FastAPI nunca tocar谩 ni se preocupar谩 por __init__, lo usaremos directamente en nuestro c贸digo.
Crear una instance¶
Podr铆amos crear una instance de esta clase con:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
馃 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
Y de esa manera podemos "parametrizar" nuestra dependencia, que ahora tiene "bar" dentro de ella, como el atributo checker.fixed_content.
Usar la instance como una dependencia¶
Luego, podr铆amos usar este checker en un Depends(checker), en lugar de Depends(FixedContentQueryChecker), porque la dependencia es la instance, checker, no la clase en s铆.
Y al resolver la dependencia, FastAPI llamar谩 a este checker as铆:
checker(q="somequery")
...y pasar谩 lo que eso retorne como el valor de la dependencia en nuestra path operation function como el par谩metro fixed_content_included:
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: Annotated[bool, Depends(checker)]):
return {"fixed_content_in_query": fixed_content_included}
馃 Other versions and variants
Tip
Prefer to use the Annotated version if possible.
from fastapi import Depends, FastAPI
app = FastAPI()
class FixedContentQueryChecker:
def __init__(self, fixed_content: str):
self.fixed_content = fixed_content
def __call__(self, q: str = ""):
if q:
return self.fixed_content in q
return False
checker = FixedContentQueryChecker("bar")
@app.get("/query-checker/")
async def read_query_check(fixed_content_included: bool = Depends(checker)):
return {"fixed_content_in_query": fixed_content_included}
Consejo
Todo esto podr铆a parecer complicado. Y puede que no est茅 muy claro c贸mo es 煤til a煤n.
Estos ejemplos son intencionalmente simples, pero muestran c贸mo funciona todo.
En los cap铆tulos sobre seguridad, hay funciones utilitarias que se implementan de esta misma manera.
Si entendiste todo esto, ya sabes c贸mo funcionan por debajo esas herramientas de utilidad para seguridad.
Dependencias con yield, HTTPException, except y Tareas en segundo plano¶
Advertencia
Muy probablemente no necesites estos detalles t茅cnicos.
Estos detalles son 煤tiles principalmente si ten铆as una aplicaci贸n de FastAPI anterior a la 0.121.0 y est谩s enfrentando problemas con dependencias con yield.
Las dependencias con yield han evolucionado con el tiempo para cubrir diferentes casos de uso y arreglar algunos problemas; aqu铆 tienes un resumen de lo que ha cambiado.
Dependencias con yield y scope¶
En la versi贸n 0.121.0, FastAPI agreg贸 soporte para Depends(scope="function") para dependencias con yield.
Usando Depends(scope="function"), el c贸digo de salida despu茅s de yield se ejecuta justo despu茅s de que la path operation function termina, antes de que la response se env铆e de vuelta al cliente.
Y al usar Depends(scope="request") (el valor por defecto), el c贸digo de salida despu茅s de yield se ejecuta despu茅s de que la response es enviada.
Puedes leer m谩s al respecto en la documentaci贸n de Dependencias con yield - Salida temprana y scope.
Dependencias con yield y StreamingResponse, detalles t茅cnicos¶
Antes de FastAPI 0.118.0, si usabas una dependencia con yield, ejecutaba el c贸digo de salida despu茅s de que la path operation function retornaba pero justo antes de enviar la response.
La intenci贸n era evitar retener recursos por m谩s tiempo del necesario, esperando a que la response viajara por la red.
Este cambio tambi茅n significaba que si retornabas un StreamingResponse, el c贸digo de salida de la dependencia con yield ya se habr铆a ejecutado.
Por ejemplo, si ten铆as una sesi贸n de base de datos en una dependencia con yield, el StreamingResponse no podr铆a usar esa sesi贸n mientras hace streaming de datos porque la sesi贸n ya se habr铆a cerrado en el c贸digo de salida despu茅s de yield.
Este comportamiento se revirti贸 en la 0.118.0, para hacer que el c贸digo de salida despu茅s de yield se ejecute despu茅s de que la response sea enviada.
Informaci贸n
Como ver谩s abajo, esto es muy similar al comportamiento anterior a la versi贸n 0.106.0, pero con varias mejoras y arreglos de bugs para casos l铆mite.
Casos de uso con salida temprana del c贸digo¶
Hay algunos casos de uso con condiciones espec铆ficas que podr铆an beneficiarse del comportamiento antiguo de ejecutar el c贸digo de salida de dependencias con yield antes de enviar la response.
Por ejemplo, imagina que tienes c贸digo que usa una sesi贸n de base de datos en una dependencia con yield solo para verificar un usuario, pero la sesi贸n de base de datos no se vuelve a usar en la path operation function, solo en la dependencia, y la response tarda mucho en enviarse, como un StreamingResponse que env铆a datos lentamente, pero que por alguna raz贸n no usa la base de datos.
En este caso, la sesi贸n de base de datos se mantendr铆a hasta que la response termine de enviarse, pero si no la usas, entonces no ser铆a necesario mantenerla.
As铆 es como se ver铆a:
import time
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Field, Session, SQLModel, create_engine
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
El c贸digo de salida, el cierre autom谩tico de la Session en:
# Code above omitted 馃憜
def get_session():
with Session(engine) as session:
yield session
# Code below omitted 馃憞
馃憖 Full file preview
import time
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Field, Session, SQLModel, create_engine
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
...se ejecutar铆a despu茅s de que la response termine de enviar los datos lentos:
# Code above omitted 馃憜
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
馃憖 Full file preview
import time
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Field, Session, SQLModel, create_engine
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
Pero como generate_stream() no usa la sesi贸n de base de datos, no es realmente necesario mantener la sesi贸n abierta mientras se env铆a la response.
Si tienes este caso de uso espec铆fico usando SQLModel (o SQLAlchemy), podr铆as cerrar expl铆citamente la sesi贸n despu茅s de que ya no la necesites:
# Code above omitted 馃憜
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
session.close()
# Code below omitted 馃憞
馃憖 Full file preview
import time
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import Field, Session, SQLModel, create_engine
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
class User(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=403, detail="Not authorized")
session.close()
def generate_stream(query: str):
for ch in query:
yield ch
time.sleep(0.1)
@app.get("/generate", dependencies=[Depends(get_user)])
def generate(query: str):
return StreamingResponse(content=generate_stream(query))
De esa manera la sesi贸n liberar铆a la conexi贸n a la base de datos, para que otras requests puedan usarla.
Si tienes un caso de uso diferente que necesite salir temprano desde una dependencia con yield, por favor crea una Pregunta de Discusi贸n en GitHub con tu caso de uso espec铆fico y por qu茅 te beneficiar铆a tener cierre temprano para dependencias con yield.
Si hay casos de uso convincentes para el cierre temprano en dependencias con yield, considerar铆a agregar una nueva forma de optar por el cierre temprano.
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 elevar la excepci贸n, la excepci贸n se elevar铆a/remitir铆a autom谩ticamente a cualquier manejador de excepciones o al manejador de error interno del servidor.
Esto cambi贸 en la versi贸n 0.110.0 para arreglar consumo de memoria no manejado por excepciones reenviadas sin un manejador (errores internos del servidor), y para hacerlo consistente con el comportamiento del c贸digo Python normal.
Tareas en segundo plano y dependencias con yield, detalles t茅cnicos¶
Antes de FastAPI 0.106.0, elevar excepciones despu茅s de yield no era posible, el c贸digo de salida en dependencias con yield se ejecutaba despu茅s de que la response era enviada, por lo que Manejadores de Excepciones ya habr铆an corrido.
Esto se dise帽贸 as铆 principalmente para permitir usar los mismos objetos devueltos con yield por las dependencias dentro de tareas en segundo plano, porque el c贸digo de salida se ejecutar铆a despu茅s de que las tareas en segundo plano terminaran.
Esto cambi贸 en FastAPI 0.106.0 con la intenci贸n de no retener recursos mientras se espera a que la response viaje por la red.
Consejo
Adicionalmente, una tarea en segundo plano normalmente es un conjunto independiente de l贸gica que deber铆a manejarse por separado, con sus propios recursos (por ejemplo, su propia conexi贸n a la base de datos).
As铆, probablemente tendr谩s un c贸digo m谩s limpio.
Si sol铆as depender de este comportamiento, ahora deber铆as crear los recursos para las tareas en segundo plano dentro de la propia tarea en segundo plano, y usar internamente solo datos que no dependan de los recursos de 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 segundo plano, y obtendr铆as los objetos de la base de datos usando esta nueva sesi贸n. Y entonces, en lugar de pasar el objeto de la base de datos como par谩metro a la funci贸n de la tarea en segundo plano, pasar铆as el ID de ese objeto y luego obtendr铆as el objeto de nuevo dentro de la funci贸n de la tarea en segundo plano.