Demo integrador - Tabla de personas
Se crea un proyecto integrador imitando la organización de un servicio monolítico práctico.
Se utilizan todo tipo de recursos disponibles:
- Port mapping;
- Networks;
- Variables de entorno;
- Secrets;
- Argumentos de imagen.
Introducción
Se implementa una página web dinámica que muestra datos artificiales de personas ficticias y un botón flotante. Cada vez que se pulsa el botón se inventan los datos de una nueva persona y se agregan en una nueva fila a la tabla. Los datos se almacenan en una base de datos SQL.
Organización del proyecto
El proyecto se despliega en tres contenedores: uno para el servidor de la webapp (frontend), uno para el servidor backend y uno para la base de datos.
---
title: "Demo Integrador - Organización general"
config:
markdownAutoWrap: false
---
flowchart LR
subgraph proyecto [Entorno proyecto]
subgraph services [Servicios]
front["`Frontend
frontend_flet:8000`"]
back["`Backend
backend_fastapi:8000`"]
db["`Base de datos PostgreSQL
base_datos:5432`"]
end
subgraph redes [Redes]
red-database["red-database"]
red-frontend["red-frontend"]
end
end
front --- red-frontend
back --- red-frontend
back --- red-database
db --- red-database
subgraph host [Host]
subgraph ports [Puertos]
portf["`Navegador
localhost:PUERTO_FRONT`"]
portb["`API
localhost:PUERTO_BACK`"]
portdb["`Cliente SQL
localhost:PUERTO_DB`"]
end
end
portf --> front
portb --> back
portdb --> db
La API del backend
admite peticiones en dos URL paths:
/leer_todos
para pedir todos los datos existentes
y /nuevo
para ordenar la creación
de un nuevo registro de usuario.
El servicio frontend
realiza estas mismas peticiones a la API
cada vez que el usuario pulsa el botón flotante.
Se dejó abierto el acceso a la API del backend y a la base de datos con fines didácticos.
Configuración
La contraseña de la base de datos se asigna con ayuda de un elemento secret. El resto de las variables de configuración se asignan mediante variables de entorno.
Arbol del proyecto
Los servicios de frontend y backend están integramente escritos en Python. Se eligió como gestor de SQL una imagen PostgreSQL, aunque podría haberse elegido MaríaDB o MySQL.
.
├── frontend
│ ├── app
│ │ ├── main.py
│ │ └── tabla_personas.py
│ ├── Dockerfile
│ └── requirements.txt
├── backend
│ ├── app
│ │ ├── main.py
│ │ ├── persona.py
│ │ └── sql.py
│ ├── Dockerfile
│ └── requirements.txt
├── .env
├── compose.yml
└── secreto.txt
Archivo de configuración
El despliegue se configuró
mediante un único archivo compose.yml
.
En él se especificaron
todos los servicios y elementos auxiliares necesarios.
name: demo-integrador
services:
frontend_flet:
build:
context: frontend/ # ruta al Dockerfile
# args:
# VERSION_PYTHON: 3.13.7-slim-trixie # Debian 13 (recortada)
image: tabla_frontend_flet-web:v1
ports:
- ${PUERTO_FRONT:-8000}:8000
restart: always
networks:
- red-frontend
environment:
SERVICIO_BACKEND: backend_fastapi
PUERTO_BACKEND: 8000
depends_on:
- backend_fastapi
backend_fastapi:
build:
context: backend/ # ruta al Dockerfile
# args:
# VERSION_PYTHON: 3.13.7-slim-trixie # Debian 13 (recortada)
image: tabla_backend_fastapi:v1
ports:
- ${PUERTO_BACK:-8001}:8000
depends_on:
base_datos:
restart: true
condition: service_healthy
required: true
restart: always
environment:
TAG_LENGUAJE: "ES_AR"
POSTGRES_DB: ${NOMBRE_DB:-personas_tabla}
POSTGRES_DOMINIO: base_datos
POSTGRES_USER: ${USUARIO:-noname}
POSTGRES_PASSWORD_FILE: /run/secrets/secreto
secrets:
- secreto
networks:
- red-frontend
- red-database
base_datos:
restart: always
image: postgres:17.2-bookworm
environment:
POSTGRES_USER: ${USUARIO:-noname}
POSTGRES_PASSWORD_FILE: /run/secrets/secreto
POSTGRES_DB: ${NOMBRE_DB:-personas_tabla}
ports:
- ${PUERTO_DB:-5432}:5432
volumes:
- volumen_db:/var/lib/postgresql/data
secrets:
- secreto
networks:
- red-database
healthcheck:
test: ["CMD-SHELL", "psql -U ${USUARIO:-noname} -d ${NOMBRE_DB:-personas_tabla} -c 'SELECT 1' || exit 1"]
interval: 10s
timeout: 60s
retries: 5
networks:
red-frontend:
red-database:
volumes:
volumen_db:
secrets:
secreto:
file: ./secreto.txt
# file: $HOME/secreto.txt
Las variables de entorno
usadas para mapeo de puertos
se cargaron en el archivo .env
.
Valores de ejemplo:
Frontend
El servicio de frontend se encarga de proporcionar una pagina web dinámica construida con el framework Flet. Este servicio interactúa con el servicio de backend haciendo peticiones HTTP con ayuda del paquete requests.
Archivos del frontend
# imagen de referencia
ARG VERSION_PYTHON="3.13.5-alpine3.22"
FROM python:${VERSION_PYTHON}
# directorio de trabajo (se crea automáticamente)
WORKDIR /code
# instalación de dependencias
COPY requirements.txt ./
RUN pip install -r requirements.txt --no-cache-dir
# copia de rutinas al directorio de trabajo
COPY app/ ./
# Puerto expuesto (meramente informativo)
EXPOSE 8000
# comando, opciones y argumentos fijos
ENTRYPOINT ["uvicorn", "main:app", "--port", "8000"]
# opciones y argumentos sobreescribibles
CMD ["--host", "0.0.0.0"]
"""main.py - Rutina principal - incluye el maquetado de la pagina web"""
# biblioteca estandar
from logging import basicConfig
from logging import info
from logging import INFO
import os
# paquetes
import flet as ft
import requests
# modulos
from tabla_personas import TablaPersonas
# uso de la consola de logs
basicConfig(
level=INFO,
format="%(asctime)s - %(levelname)s - %(message)s", # info incorporada
)
# URLs al backend
servicio_backend = os.getenv("SERVICIO_BACKEND")
puerto_backend = os.getenv("PUERTO_BACKEND")
URL_CREAR_PERSONA = f"http://{servicio_backend}:{puerto_backend}/nuevo/"
URL_LEER_TODOS = f"http://{servicio_backend}:{puerto_backend}/leer_todos/"
# diseño de página
def main(page: ft.Page):
"""Esta función define el diseño de la página web."""
def crear_persona(e):
"""Este handler ordena crear una nueva persona ficticia."""
# orden al backend para crear nueva persona
requests.post(URL_CREAR_PERSONA, timeout=5)
# # relectura de tablas
actualizacion_tabla()
info("Nueva persona creada")
def actualizacion_tabla():
"""Esta funcion lee la tabla desde la base de datos y la carga a la página."""
# lectura de tabla (completa)
respuesta = requests.get(URL_LEER_TODOS, timeout=5)
personas = respuesta.json()
# borrado de filas
tabla.rows = []
# # creacion de filas - una a una
for persona in personas:
info(persona)
tabla.nueva_persona(persona)
# actualizacion grafica
page.update()
# boton flotante
page.floating_action_button = ft.FloatingActionButton(
icon=ft.Icons.ADD,
on_click=crear_persona,
)
# tabla web
tabla = TablaPersonas()
# maquetado y estilos
page.add(
ft.SafeArea(
ft.Container(
tabla,
alignment=ft.alignment.center,
expand=True,
)
)
)
# actualizacion grafica
actualizacion_tabla()
page.update()
# objeto renderizable por el server Uvicorn
app = ft.app(main, export_asgi_app=True)
"""tabla_personas.py - Módulo implementado para diseñar la tabla gráfica."""
# paquetes
from flet import DataTable, DataCell, DataRow, DataColumn
from flet import Text, FontWeight
class TablaPersonas(DataTable):
"""Tabla custom - con lectura de filas a medida"""
def __init__(self):
"""Inicializacion de la tabla gráfica."""
super().__init__(
width=1200,
height=700,
heading_row_height=50,
column_spacing=20,
columns=[
DataColumn(Text("ID", weight=FontWeight.BOLD, width=100)),
DataColumn(Text("Nombre", weight=FontWeight.BOLD, width=300)),
DataColumn(Text("Dirección", weight=FontWeight.BOLD, width=300)),
DataColumn(
Text("Edad", weight=FontWeight.BOLD, width=100), numeric=True
),
],
)
# def nueva_persona(self, persona: Persona):
def nueva_persona(self, persona: dict):
"""Agrega una fila a la tabla gráfica con los datos de entrada."""
fila = DataRow(
cells=[
DataCell(Text(persona["id"], width=100)),
DataCell(Text(persona["nombre"], width=300)),
DataCell(Text(persona["direccion"], width=300)),
DataCell(Text(persona["edad"], width=100)),
],
)
self.rows.append(fila)
return fila
Backend
El backend implementa un servidor HTTP con ayuda del framework FastAPI. Para interactuar con la base de datos utiliza el paquete SQLModel, el cual permite diseñar las tablas SQL e implementar tanto el guardado como la lectura de datos mediante clases pedefinidas, sin necesidad de agregar código SQL. Por último, los datos de los registros se inventan con ayuda del paquete Faker, que inventa datos de personas ficticias con un simple llamado a un método predefinido.
Archivos del backend
fastapi[all]==0.117.1
sqlmodel==0.0.24
psycopg2-binary==2.9.10
Faker==37.4.2
(psycopg2 es requerido para poder establecer la conexión con la base Postgres, sin embargo no requiere ser llamado explícitamente por la rutina de Python).
# imagen de referencia
ARG VERSION_PYTHON="3.13.5-alpine3.22"
FROM python:${VERSION_PYTHON}
# directorio de trabajo (se crea automáticamente)
WORKDIR /code
# instalación de dependencias
COPY requirements.txt ./
RUN pip install -r requirements.txt --no-cache-dir
# copia de rutinas al directorio de trabajo
COPY app/ ./
# Puerto expuesto (meramente informativo)
EXPOSE 8000
# comando, opciones y argumentos fijos
ENTRYPOINT ["uvicorn", "main:app", "--port", "8000"]
# opciones y argumentos sobreescribibles
CMD ["--host", "0.0.0.0"]
"""main.py - Rutina principal - incluye el maquetado de la pagina web"""
# paquetes
from fastapi import FastAPI
# modulos
from sql import leer_todas_personas_db, guardar_persona_db
from persona import nueva_persona
# objeto renderizable por el server Uvicorn
app = FastAPI()
# paths implementados
@app.get("/")
async def root():
"""Un mero mensaje informativo"""
return {"message": "Backend hecho en FastAPI"}
@app.post("/nuevo")
async def nuevo_usuario():
"""Path elegido para ordenar la creación de un nuevo usuario."""
persona = nueva_persona()
datos_nuevos:dict = await guardar_persona_db(persona)
return datos_nuevos
@app.get("/leer_todos")
async def leer_usuarios():
"""Path elegido para leer los datos de todos los usuarios en la base de datos.
se devuelven como una lista de diccionarios."""
lista_todos: list[dict]
lista_todos = await leer_todas_personas_db()
return lista_todos
"""persona.py - Este módulo crea los datos de personas ficticias."""
# bibliotecas estandar
import os
from random import randint
# paquetes
from faker import Faker
# lenguaje y región del registro de personas
lenguaje_region = os.getenv("TAG_LENGUAJE",default="ES_ES")
# generador de datos ficticios
datos_fake = Faker(locale=lenguaje_region)
# crear nueva persona
def nueva_persona()->dict:
"""Esta función crea los datos de una nueva persona ficticia."""
# generacion de nueva persona
nombre = datos_fake.name()
direccion = datos_fake.address()
edad = randint(13, 65)
# agrupacion como diccionario
data_persona = {
"nombre": nombre ,
"direccion": direccion,
"edad": edad
}
return data_persona
"""sql.py - Módulo dedicado a las consultas a la base de datos"""
# bibliotecas estandar
import os
from pathlib import Path
from typing import Optional
# paquetes
from sqlmodel import Field, SQLModel, create_engine
from sqlmodel import Session, select
# Variables de entorno - necesarias para componer la URL de la base de datos
user = os.getenv("POSTGRES_USER")
database = os.getenv("POSTGRES_DB")
dominio = os.getenv("POSTGRES_DOMINIO")
ruta_password_secreto = os.getenv("POSTGRES_PASSWORD_FILE")
if Path(ruta_password_secreto).is_file():
with open(ruta_password_secreto, "r",encoding="utf-8") as archivo:
password = archivo.read()
else:
password = os.getenv("POSTGRES_PASSWORD")
# composicion de la URL dela base de datos
ruta_db = f"postgresql://{user}:{password}@{dominio}:5432/{database}"
# creación del conector
engine = create_engine(
ruta_db,
# echo=True,
pool_pre_ping=True,
)
# Diseño de tabla SQL
class PersonaSQL(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
nombre: str
direccion: str
edad: Optional[int] = None
# creacion de base de datos vacía (sólo si aun no existe)
SQLModel.metadata.create_all(engine)
# funciones para peticiones remotas
async def guardar_persona_db(persona: dict)->dict:
"""Esta función guarda los datos de la persona en una base de datos."""
# carga en base datos
with Session(engine) as session:
# carga de datos - fila a fila
persona = PersonaSQL(
nombre=persona["nombre"],
direccion=persona["direccion"],
edad=persona["edad"],
)
session.add(persona)
# confirmación de cambios
session.commit()
return persona
return None
async def leer_todas_personas_db()->list[dict]:
"""Esta función lee los datos de todas las personas registradas
y los devuelve como lista de diccionarios. """
# lectura desde base de datos
with Session(engine) as session:
statement = select(PersonaSQL)
resultados = session.exec(statement)
data = resultados.all()
personas = data
return personas
return []
Despliegue y consulta
Simplemente ubicarse en la ruta del proyecto y ejecutar:
La web dinámica debe aparecer visible en el puerto 8181 o en el puerto por default que es el 8000.
La API se puede consultar desde el navegador puerto 8182 o en el puerto preasignado que es el 8001.