OpenWebinars

Frameworks

Dependencias en FastAPI, descubre por qué son tan útiles

Dentro de las ventajas que ofrece el framework FastAPI para el desarrollo, está la de la gestión de dependencias, que vamos a abordar en este artículo.

Juan Benito Pacheco Rubio

Juan Benito Pacheco Rubio

Experto Backend

Lectura 6 minutos

Publicado el 23 de diciembre de 2022

Compartir

FastAPI el nuevo framework de Python

FastAPI es uno de los frameworks para la creación de aplicaciones webs Python más populares en este momento, haciendo competencia a Frameworks con mucho más recorrido como Django o Flask, creando una muy buena comunidad a su alrededor, algo que no había hecho ningún otro Framework de Python hasta el momento.

Detrás del éxito de este framework está la facilidad de desarrollo desde la que hace gala en su página web, y la cantidad de automatización que ofrece a los desarrolladores.

Dentro de esta facilidad de desarrollo y esta automatización se encuentra su gestión de dependencias.

Sobre todo esto es de lo que profundizaremos en este artículo, así que si estás interesado, no dejes de seguir leyendo.

Dependencias

Tradicionalmente las dependencias se refieren a las clases o piezas de código que son utilizadas por otras para funcionar correctamente. Por ejemplo, cuando trabajamos con un repositorio que se encarga de recoger información de una base de datos, este componente necesitará una conexión a la base de datos, que no tendría por qué gestionar él, esto podría ser una dependencia del mismo repositorio.

class DBRepository:

        def __init__(self, db_connection: DBConnection):
                self.db_connection = db_connection

        def find_all(self):
                return self.db_connection.query(MyData).all()

Esta gestión de dependencias, el cómo satisfacerlas básicamente, se puede hacer de diferentes formas mediante librerías, algunas veces están incluidas en los frameworks que utilizamos, o incluso de forma manual. Por ejemplo, una gestión de dependencias efectiva para el repositorio de arriba sería:

if __name__ == '__main__':
        connection = get_connection()

        repository = DBRepository(db_connection=connection)
        data = repository.find_all()

        print(data)

El método get_connection se encargaría de conseguir el objeto connection apropiado y nosotros en nuestro script se lo inyectaríamos a nuestro repositorio.

Como vemos, esta gestión de dependencias puede ser fácil a priori, pero puede resultar bastante compleja cuando introducimos ciertas necesidades cómo gestionar el ciclo de vida de las dependencias, gestionar errores al crear las dependencias, mockearlas de forma efectiva cuando queremos ejecutar el testing, o cuando unas dependencias se necesitan unas a otras.

Una buena gestión de dependencias ayuda a nuestro código a abstraer la lógica necesaria sobre cómo conseguir esa pieza que le hace falta, esto es tremendamente útil sobre todo cuando la lógica para crear estas piezas es compleja.

Diferentes lenguajes cómo Java con Spring o TypeScript con Angular, cuentan con herramientas de gestión de dependencias de serie. Hasta ahora Python también contaba con herramientas de gestión de dependencias, pero o bien tenía que ser programada ad-hoc para tu framework, o bien tenías que reutilizar librerías como:

Estas herramientas nos ayudan a la satisfacer el principio de inversión de dependencias del que se habla en SOLID.

Dependencias en FastAPI

FastAPI nos ayuda con esta tarea de gestión de dependencias de forma completamente nativa, incluyéndola en el Framework de Python.

La forma de incluir estas dependencias es a través del método Depends en tus endpoints:

Ejemplo Depends

app = FastAPI()

def get_hello() -> str:
    return 'hello'

@app.route('/hello_world')
def hello_world(d: str = Depends(get_hello)):
    return d + ' world'

Cómo podemos ver en el ejemplo de arriba incluir dependencias en FastAPI con el parámetro Depends es muy sencillo y versátil ya que puede emplearse en situaciones muy diferentes.

Es importante saber que estas dependencias nos sirven para ejecutar código antes de que la petición llegue a nuestros endpoints.

Dependencias funciones

Las dependencias más usadas son a través de funciones, ya que el sistema de inyección de dependencias nos permite utilizar el resultado de las funciones en nuestros endpoints, como hemos visto en el ejemplo anterior.

Estas dependencias son realmente útiles para aplicar el principio Don’t Repeat Yourself (DRY) ya que nos permite compartir de forma realmente sencilla entre los endpoints.

Ejemplo Compartiendo funciones 1

app = FastAPI()

def get_user_data() -> str:
    return 'Antonio Martinez'

@app.get('/hello')
def hello(user_data: str = Depends(get_user_data)):
    return f'hello {user_data}'

@app.get('/goodbye')
def goodbye(user_data: str = Depends(get_user_data)):
    return f'goodbye {user_data}'

Por ejemplo, en el código de arriba estamos evitando duplicar el método get_hello entre los diferentes endpoints de nuestra aplicación.

Además de evitar la duplicidad de código, podemos complementarlo con la capacidad que tiene FastAPI de inyectar los parámetros de la request a nuestros endpoints, porque estos parámetros también pueden ser inyectados a nuestro sistema de dependencias.

Ejemplo Compartiendo funciones con Request Params

app = FastAPI()

def get_user_data(first_name: str, last_name: str) -> str:
    return f'{first_name} f{last_name}'

@app.get('/hello')
def hello(user_data: str = Depends(get_user_data)):
    return f'hello {user_data}'

@app.get('/goodbye')
def goodbye(user_data: str = Depends(get_user_data)):
    return f'goodbye {user_data}'

El sistema de dependencias se complementa con la habilidad de FastAPI de parsear la request entrante y pasar la información necesaria, es muy útil ya que permite incluir información de los: Query params, Headers, y Body. Incluyendo toda esta información en la documentación OpenAPI que se genera de forma automática por FastAPI, y que puede ser consultada en la ruta /docs de nuestra aplicación.

Clases como Dependencias

Finalmente podemos concluir que para FastAPI una dependencia es todo aquello que es “llamable” (callable).

Esto nos ayuda a no limitarnos solamente a usar exclusivamente funciones, sino que también se pueden inyectar instancias de clases que son gestionadas directamente por este sistema.

Ejemplo Clases cómo Dependencias

app = FastAPI()

class User:

        def __init__(self, first_name: str, last_name: str):
                self.first_name = first_name
                self.last_name = last_name

        @property
        def full_name(self) -> str:
                return f'{self.first_name} {self.last_name}'

@app.get('/hello')
def hello(user: User = Depends(User)):
    return f'hello {user.full_name}'

De esta forma se delega la creación de clases necesarias en nuestros endpoints a FastAPI y a los parámetros necesarios de la request. Esta información también es incluida en nuestra documentación OpenAPI.

Además, FastAPI te ofrece algunos shortcut con la inyección de clases. El siguiente código sería equivalente al primer ejemplo.

Ejemplo clases como dependencias shortcut 1

@app.get('/hello')
def hello(user = Depends(User)):
    return f'hello {user.full_name}'

En este ejemplo hemos eliminado el typing del parámetro user, este typing realmente no tiene por FastAPI en la inyección de clases, por ejemplo, no valida que la clase sea correcta. Entonces podemos borrarlo sin ningún problema.

Ejemplo clases como dependencias shortcut 2

@app.get('/hello')
def hello(user: User = Depends()):
    return f'hello {user.full_name}'

En este segundo ejemplo, sí especificamos el typing de la variable user, pero no hace falta que se vuelva a incluir la clase que queremos inyectar dentro del método Depends, ya que a través del typing FastAPI sabe que instancia está esperando el endpoint.

Dependencias anidadas

Un caso de uso muy frecuente es cuando nuestras dependencias necesitan a su vez otras dependencias para funcionar correctamente.

app = FastAPI()

def get_last_name(last_name: str = Header(...))

class User:

        def __init__(self, first_name: str, last_name: str = Depends(get_last_name)):
                self.first_name = first_name
                self.last_name = last_name

        @property
        def full_name(self) -> str:
                return f'{self.first_name} {self.last_name}'

@app.get('/hello')
def hello(user: User = Depends(User)):
    return f'hello {user.full_name}'

@app.get('/hello')
def goodbye(last_name: str = Depends(get_last_name)):
    return f'goodbye {last_name}'

Para solucionar esta causística FastAPI acaba por generar una especie árbol de dependencias que se van resolviendo de forma secuencial, hasta llegar al endpoint.

Dependencias FastAPI

Esto hace que compartir dependencias con otras dependencias que a su vez sean utilizadas en varios sitios sea realmente sencillo permitiendo crear sistemas complejos fácilmente. Hay que tener en cuenta que por defecto cada dependencia es ejecutada cada vez que es llamada dentro del árbol, es decir el valor de una dependencia no es compartido por el resto.

*TIP: FastAPI permite reutilizar el valor de las diferentes dependencias (cómo un Singleton) utilizando el parámetro cache, Depends(cache=True), del método Depends. Este método evita que se llame cada vez que se necesita la dependencia.*

Dependencias en las rutas

Un tipo de dependencia específico introducido por FastAPI son las dependencias en la ruta, este tipo de dependencias permite ejecutar métodos, sin necesidad de devolver un resultado.

Estas dependencias son especialmente útiles cuando quieres autorizar o desautorizar el acceso a tus endpoints.

Ejemplo dependencia en rutas

app = FastAPI()

def check_token_is_valid(auth_token: str = Header(...)):
        if auth_token != 'valid_token':
                raise HTTPException(status_code=403, detail='Not Authorized')

@app.get('user/{user_id}', dependencies=[Depends(check_token_is_valid)])
def get_user():
        return {'username': 'Juan'}

En caso de llamar a nuestro endpoint sin el token correcto recibiremos un error 403.

Dependencias Globales

Además de todo lo visto anteriormente, hay veces que queremos llamar a una dependencia de ruta, pero que sea ejecutada para cualquier endpoint de nuestra aplicación. Para esto son muy útiles las dependencias globales:

Ejemplo dependencias globales

def check_token_is_valid(auth_token: str = Header(...)):
        if auth_token != 'valid_token':
                raise HTTPException(status_code=403, detail='Not Authorized')

app = FastAPI(dependencies=[check_token_is_valid])

@app.get('user/{user_id}')
def get_user(user_id: int):
        return {'username': 'Juan'}

@app.get('users/')
def list_users():
        return [{'username': 'Juan'}, {'username': 'Diego'}]

En este caso nuestra dependencia global comprobará para los dos endpoints que tiene nuestra aplicación si el token que viene en la cabecera es el correcto. Como siempre estas dependencias se incluirán en nuestra documentación automática.

Dependencias con generadores

Finalmente, hay un tipo de dependencias en FastAPI que nos permite ejecutar código no solo cuando nos llega una nueva petición, sino además cuando devolvemos una respuesta a nuestro cliente. Para esto se han incluido las dependencias con yield, o dependencias con generadores.

Ejemplo dependencia con generador

app = FastAPI()

def get_db_connection() -> Session:
        db_connection = DatabaseConnection()
        try:
             yield db_connection
        except Exception:
                db_connection.rollback()
        finally:
                db_connection.close()

@app.post('users')
def create_user(db_connection = Depends(get_db_connection)):
        user = User(first_name='Juan')
        db_connection.save(user)

Este tipo de dependencia tiene múltiples ventajas, cómo poder capturar errores que ocurran cuando el endpoint que ejecuta esta dependencia lanza un error. Este error puede capturarse por nuestra dependencia para hacer acciones adicionales, cómo en el caso del ejemplo, hacer rollback en caso de error.

Este tipo de dependencias son realmente útiles para gestionar las conexiones a sistemas de terceros como bases de datos, ya que nos ayuda a liberar la conexión o hacer rollback al finalizar la petición o detectar un error.

Conclusión

Las dependencias son una necesidad a tener en cuenta en nuestras aplicaciones y FastAPI nos da un sistema fácil y flexible para gestionarlas, esto nos ayuda en diferentes situaciones como conexiones a la base de datos, compartir código o implementar autorizadores.

Todo esto unido a la simplicidad con la que FastAPI consigue gestionar la documentación o como extrae la información de las requests, lo hace una herramienta extremadamente útil mientras creamos nuestras aplicaciones.

Para profundizar

Para seguir profundizando sobre cómo funciona FastAPI y su gestión de dependencias os recomiendo que realicéis las siguientes formaciones que tenemos en OpenWebinars: Curso Creación de APIs con FastAPI y Taller Creando una aplicación con FastAPI.

Además, FastAPI tiene una gran documentación oficial: FastAPI Docs y Dependencias FastAPI.

Compartir este post

También te puede interesar

Icono de la tecnología
Taller

Trabajando la concurrencia en Python

Intermedio
43 min.

En este taller veremos como mejorar el rendimiento de nuestras aplicaciones trabajando con threads y diferentes procesos, utilizando...

Juan Benito Pacheco Rubio
4.5
Icono de la tecnología
Curso

Python 3

Intermedio
10 h. y 16 min.

Tu pasaporte a sabelotodo de Python. Desde las bases hasta la creación de tu primer programa, este curso...

José Domingo Muñoz
4.3