Días atrás os había hablado de Streamlit, un framework de Python que permite crear aplicaciones webs
de una forma muy sencilla. Este framework permite crear formularios de forma trivial apoyándose en Pydantic, en concreto nos
vamos a centrar en como usar Enums
para crear menús de selección de forma dinámica.
Introducción
Como decía en la intro, existe una librería llamada streamlit-pydantic que nos permite crear
formularios a partir de clases de forma increíblemente sencilla. Simplemente basta con crear una clase que herede de BaseModel
(clase de Pydantic
)
y la librería sabe como crear un formulario a partir de la misma.
De todos los posibles campos que puede tener el formulario (inputs, botones, checkbox, etc.) nos vamos a centrar en los select
que en esta
librería se forman a partir de Enums
.
Concretamente, si tenemos un Enum
con un par de valores como este:
from enum import Enum
class DummyEnum(Enum):
FOO = "foo"
BAR = "bar"
La librería nos creará un formulario con este campo como select
:
Problema
Como hemos visto, el caso típico es muy sencillo y no tiene historia. ¿Pero qué ocurre si queremos que las opciones de nuestro select
no sean constantes, sino que vengan de un servicio externo?.
Por suerte existe otra forma de crear Enums
en Python y además de forma genérica.
La clase Enum
(recordemos que en Python en los Enum
no dejan de ser clases)
acepta un par de parámetros: Enum(enum_name, names)
siendo enum_name
el nombre del Enum
y names
un diccionario clave valor de posibles opciones.
Por tanto, las opciones se vuelven casi infinitas, bastará con que le pasemos una lista de opciones (donde se obtiene ya no es relevante para el Enum
) y Python
será capaz de crear el Enum
de forma dinámica.
Dummy Enum
Empezamos por el caso más sencillo, vamos a convertir nuestro Enum
inicial en su versión dinámica aunque los valores siempre sean los mismos.
Para ello vamos a crear el test que nos permite válida que todo funciona:
class TestDummyEnum:
def test_enum(self) -> None:
def _values() -> dict:
return {"FOO": "foo", "BAR": "bar"}
DummyEnum = Enum("DummyEnum", _values())
expect(DummyEnum.FOO.value).to(equal("foo"))
expect(DummyEnum.BAR.value).to(equal("bar"))
El Enum
, por tanto, se puede crear de la siguiente manera:
def _values() -> dict:
return {"FOO": "foo", "BAR": "bar"}
DummyEnum = Enum("DummyEnum", _values())
Lo dicho, nada del otro mundo. Lo único que hemos hecho es pasar una función que nos devuelve un diccionario con los valores que queremos que tenga el Enum
.
Enum
genérico obteniendo los valores de una base de datos
Ahora imaginemos que en vez de tener los valores en un diccionario los tenemos en una base de datos.
Para simular la conexión a base de datos vamos a usar doublex
que nos permite hacer Stubs de forma muy sencilla.
Como siempre, vamos a empezar por el test:
class TestDBEnum:
def test_enum(self) -> None:
with Mimic(Stub, Session) as session:
results = [("Spain"), ("Italy")]
session.execute(ANY_ARG).returns(results)
def _values(session: Session) -> dict:
options = {}
for row in session.execute("SELECT * FROM countries"):
options[f"{row[0]}".upper()] = row[0]
return options
DBEnum = Enum("DBEnum", _values(session))
expect(DBEnum.SPAIN.value).to(equal("Spain"))
expect(DBEnum.ITALY.value).to(equal("Italy"))
El Enum
, por tanto, se puede crear de la siguiente manera:
def _values(session: Session) -> dict:
options = {}
for row in session.execute("SELECT * FROM countries"):
options[row[0]] = row[0]
return options
engine = create_engine(f"{DB_URL}")
session = Session(bind=engine)
DBEnum = Enum("DBEnum", _values(session)))
Enum
genérico obteniendo los valores de una API HTTP
De igual forma que hemos hecho con la base de datos, podemos hacerlo con una petición HTTP.
Lo primero el test:
class TestHTTPEnum:
def test_enum(self) -> None:
with Mimic(Stub, HTTPClient) as http_client:
http_client.find_all().returns(
[
{"id": "NIGERIA", "name": "Nigeria"},
{"id": "BRAZIL", "name": "Brazil"},
]
)
def _values(http_client: HTTPClient) -> dict:
options = {}
items = http_client.find_all()
for item in items:
options[item["id"]] = item["name"]
return options
HTTPEnum = Enum("HTTPEnum", _values(http_client))
expect(HTTPEnum.NIGERIA.value).to(equal("Brazil"))
expect(HTTPEnum.BRAZIL.value).to(equal("Nigeria"))
El Enum
, por tanto, se puede crear de la siguiente manera:
def _values(http_client: HTTPClient) -> dict:
options = {}
for item in http_client.find_all():
options[item["id"]] = item["name"]
return options
http_client = HttpClient()
HTTPEnum = Enum("HTTPEnum", _values(http_client))
Conclusiones
PROS
- Cada vez que se carga el
Enum
los datos están actualizados. - Podemos crear
Enums
de forma genérica.
CONTRAS
- Al ser un
Enum
dinámico no es posible para el IDE saber qué opciones hay. - Son muy útiles para casos concretos como el del formulario, pero no para cualquier caso.
¡Espero que os haya gustado!