Blueprint para proyectos python

Kalim Al Razif
10 goto 10
Published in
8 min readOct 25, 2021
Tomado de pixabay

Bueno mucha agua ha pasado debajo del puente desde que escribí el primer artículo al respecto, he aprendido unas cuantas cositas sobre python y flask.

Algo de teoría

Arquitectura hexagonal

Desde esa primera aproximación he estudiado un poco y después de encontrar unos artículos sobre arquitectura hexagonal [1][2] Decidí que me gusta esa forma de plantear el desarrollo y procedí a “tropicalizar” un blueprint para usarlo yo en los desarrollos.

La arquitectura hexagonal, según entendí, hace uso de abstracciones llamadas core, adaptadores y puertos.

  • El core es donde vendría a estar toda la lógica de negocio del servicio, donde se definen las acciones que pueden realizarse. El core se representa como un hexágono.
  • Los puertos viven en el core y definen como hablarle. Estas vendrían a ser las interfaces que se definen para poder interactuar con el core.
  • Los adaptadores entonces se conectan a los puertos y transforman los datos del exterior a formatos aceptados por el core.

Con este tipo de arquitectura de software es posible cambiar por ejemplo una implementación de MySQL para el adaptador de la base de datos a una con MongoDB y no seria necesario cambiar una linea de código del core.

Igualmente si cambiamos UI de web a una implementada en cli el core permanecería exactamente igual.

Esto ayuda mucho a la hora de hacer el servicio versátil y adaptable.

Tuve que leer unos cuantos artículos y buscar en la red para poder pescar bien el concepto anterior. 😅

Ahora vamos a hablar del otro concepto que me costo más comprender. 😖

Inyección de dependencias

Cuando estuve estudiando la arquitectura hexagonal en los ejemplos usaban inyección de dependencias para implementar los puertos y adaptadores.

¿Pero, que demonios es inyección de dependencias?

Bueno, la wikipedia nos dice:

En informática, inyección de dependencias (en inglés Dependency Injection, DI) es un patrón de diseño orientado a objetos, en el que se suministran objetos a una clase en lugar de ser la propia clase la que cree dichos objetos. Esos objetos cumplen contratos que necesitan nuestras clases para poder funcionar (de ahí el concepto de dependencia). Nuestras clases no crean los objetos que necesitan, sino que se los suministra otra clase ‘contenedora’ que inyectará la implementación deseada a nuestro contrato.1

En otras palabras, se trata de un patrón de diseño que se encarga de extraer la responsabilidad de la creación de instancias de un componente para delegarla en otro. El término fue acuñado por primera vez por Martin Fowler.

Esto en teoría permite la libertad de poder desarrollar los adaptadores de forma independientes y usarlos sin muchas modificaciones en el core.

Para manejar la inyección de dependencias entonces encontré Dependency Injector. Maneja el concepto de containers, proveedores, configuraciones, cableado.

Entonces para implementar la inyección de dependencias vamos a manejar un container donde se va a agrupar todas las dependencias. Viendo este ejemplo sacado de la documentación podemos explicar un poco:

En la linea 5 tenemos la declaración del contenedor, en la linea 7 se define la configuración con un proveedor especifico para configuraciones, pero en el momento de la declaración no tenemos información sobre dicha configuración pero eso no impedirá poder usarla dentro de la definición del container, como por ejemplo en las lineas 11 y 12.

En la linea 9 tenemos la declaración de un provider singleton para definir la conexión a una api externa, pero también puede usarse para definir la conexión contra una DB.

Mas tarde cuando estemos haciendo uso del contenedor en nuestro código si declaramos varios objetos de tipo contenedor todos harán uso de una sola conexión contra la api externa como es en este caso. Nuevamente las lineas 11 y 12 hacen uso de de valores de la configuración.

En la linea 15 declaramos un provider factory que nos generara un objeto servicio nuevo cada vez que generemos un objeto container nuevo. Ese objeto hace uso del singleton de conexión contra la api.

Ahora en la linea 21 se declara un decorador que inyectara las dependencias como en el argumento service de la función main.

Una vez declarado todo a partir de la linea 26 vamos a poner todo en marcha y generaremos un contenedor(27), al que luego pasaremos los parámetros de configuración desde variables de entorno (28,29).

La linea 30 tiene container.wire() que especifica con que módulos queremos cablear nuestro código, en este caso estaremos cableando nuestro código con nosotros mismos.

Por ultimo ejecutamos main() y como puede verse no es necesario pasar ningún parámetro, eso lo hace el dependency injector por nosotros.

En la documentación de dependency injector se puede ver el listado completo de providers.

¡Ok… ok… ok… suficiente!

Luego de toda esta teoría medio enredada vamos, ahora si, a hablar del blueprint. 😆

Idea general del blueprint

Mi idea al crear este blueprint es generar un paquete de python que contenga el servicio, un paquete que puede ser desplegado como un .egg en el sistema base (OS o contenedor de docker). Y definiendo una variable de entorno (MS_CONFIG) que apunta a la configuración de una instancia del servicio, ejecutarlo.

Es posible ejecutar el mismo código con distintas configuraciones, puertos, conexiones a db, etc. Apuntando a distintos archivos de configuración con la variable MS_CONFIG en diferentes sesiones del shell.

Distribución de directorios

Veamos un poco la distribución de directorios, vista desde la raíz del repositorio git:

./                    # Raiz del repo
+-instance # Datos de la instancia
| +etc # Configuraciones del servicio
| +log # Logs del servicio
+-service_pgk_name # Paquete del servicio
| +-adaptadores # Adaptadores del servicio
| | +-db # Adaptador de la base de datos
| | +-celery # Adaptador celery
| | +-web # Adaptador web del servicio
| +-core # Core del servicio
| +-acciones # Acciones del core
| +-exceptions # Excepciones del core
| +-interfaces # Puertos (Interfaces) del core
| +-libs # Componentes especializados del core
| +-model # Modelos de transferencia de datos
+-test # Unit test del servicio

Expliquemos un poco esta distribución.

Instance

El directorio instance debe guardar la información referente a una instancia del servicio. Dentro del directorio etc se guarda un archivo yaml que es la configuración del servicio.

La anterior es una configuración de ejemplo, tiene varias secciones.

Environment define el ambiente del servicio.

Info guarda la información general del servicio, por lo general para generar la documentación de swagger (openapi).

Web contiene las configuraciones del adaptador de web, en este caso la configuración es para python flask.

DB debe contener la información para conectarse a la base o bases de datos (es posible tener mas de una), dependiendo del adaptador puede definirse una cadena de conexión o las credenciales en variables separadas.

Celery en esta sección esta toda la información para crear un worker de celery.

Logging contiene la configuración de logging para el servicio, esta información luego se pasa a un dict de python y alimenta a la función dictConfig. Este ejemplo contiene configuraciones para usar con python flask.

Test

En este directorio deben estar las pruebas unitarias del servicio. Cuando aprenda como hacer test unitarios seguro que voy a llenar este directorio de pruebas.

Paquete del servicio

Dentro del paquete del servicio viven los siguientes directorios:

Adapters, contiene todas las implementaciones de los distintos adaptadores del servicio, en nuestro ejemplo tenemos adaptadores para una base de datos. Una interfaz web, en este caso flask. Y una para python celery.

Core, en el core esta todo lo que el servicio debe hacer.

En el directorio de las acciones se implementa el código que se necesite para definir todas las acciones que puede realizar el servicio. En el directorio exceptions deberían definirse todas las excepciones que el servicio quiera manejar.

En interfaces se definen las interfaces para los adaptadores, mas que nada para la base de datos (por eso lo de tropicalizar).

En el directorio libs estarían partes del software del servicio que pudieran funcionar “independientemente”, este directorio se completamente opcional, dependerá mucho de la complejidad de lo que el servio haga.

Model debería contener las definiciones de los modelos de datos del servicio, acá no se trata de los modelos de base de datos. Se trata de modelar dataclases o clases de pydantic para el intercambio de datos entre las distintas partes del servicio.

Ya que hemos definido los directorios vamos a hablar de lo que puede ir dentro.

Algunos archivos importantes del servicio

Archivo ./service_pkg_name/__init__.py :

En este archivo define una interfaz cli para controlar el servicio. La idea es definir comandos tales como: iniciar el servicio, o iniciar el worker de celery, inicializar la base de datos, etc. Acá tome la decisión de usar click para crear la interfaz cli.

Por ejemplo el comando run del grupo de comandos web inicia el servidor de desarrollo de flask. Primero revisa que la variable de entorno MS_CONFIG exista y si no existe lanza un mensaje. Después crea un objeto core, se le pasa el archivo de configuración. Luego inicializa el provider de la base de datos y los recursos (logging) y al final crea una app de flask y la pone a correr.

Archivo ./service_pkg_name/core/__init__.py :

Este archivo define el container del core, tenemos entonces una definición para la configuración, una para el log como recurso. Luego definimos una dependencia para poder trabajar con objetos de tipo DBAdapter, objetos que definiremos en la linea 51 del archivo ./service_pkg_name/__init__.py como singleton. Y luego definiremos como factory las acciones del servicio pasandole como parámetros la conexión a la base de datos.

Como ultimo paso creamos la web_app, si tuviéramos una cli en lugar de una web, acá podríamos crearla o tal vez podríamos tener ambas. Todo depende de como queremos enfocar el servicio.

Archivo ./service_pkg_name/core/model/db.py :

En este archivo vamos a definir todas las funciones que nuestro adaptador de base de datos debe implementar, esto nos asegura que el core siempre reciba y envíe exactamente lo mismo sin importar si la base de datos es MySQL, PostgreSQL o MongoDB.

Por tanto, el adaptador debe transformar los datos de salida nativos de la base de datos al formato pre establecido en el servicio, por tanto tener modelos para la transferencia de datos es importante.

Archivos ./service_pkg_name/core/model/*.py

Los modelos de trasferencia de datos son útiles para estandarizar los datos. En este punto el modelado dependerá exclusivamente de lo que vaya a hacer el servicio. Pero es posible modelarlos con las dataclass de python o con el paquete de pydantic.

Archivo ./service_pkg_name/adapters/db/__init__.py :

El adaptador para la base de datos podría ser algo como esto…

La clase deberá implementar la interfaz declarada en DBAdapter, este ejemplo seria solo el __init__de la clase.

Archivo ./service_pkg_name/adapters/celery/app.py :

Esta seria la implementación para crear un worker de celery que utilizara las acciones del core para realizar tareas en paralelo de forma muy eficiente.

Luego sera posible ejecutar el worker agregando algo como esto al archivo ./service_pkg_name/__init__.py :

Archivo ./service_pkg_name/adapters/web/__init__.py :

En la linea 16 puede verse la inclusión de las acciones en la aplicación flask. luego es posible usar las acciones desde los controladores incluyendo.

from flask import current_app

Y llamándolas con current_app.ms_actions .

Archivo ./service_pkg_name/core/acciones.py :

La clase de las acciones recibe entonces vía parámetros la información sobre la base de datos y la configuración del servicio, incorporándolos a sus propios datos.

Conclusión

Resumiendo, esta es la versión actual(todavía en proceso de desarrollo, mejoras y simplificación). De un blueprint para desarrollar servicios en python basado en la arquitectura hexagonal y la inyección de dependencias.

Actualmente el adaptador web esta implementado en flask usando flask-restx que ayuda mucho para generar automáticamente la documentación de openapi, pero estoy haciendo la migración a FastAPI después de leer sobre este framework que tiene integrado todo lo de openapi y no se necesita un paquete externo.

Muchas gracias por su atención.

--

--

Kalim Al Razif
10 goto 10

Nací, crecí y aquí sigo. Curioso de nacimiento. Ávido lector. Animeadicto. Cinéfilo o cinefilico XD. SysAdmin por vocación.