522 lines
16 KiB
Python
522 lines
16 KiB
Python
import marimo
|
||
|
||
__generated_with = "0.15.5"
|
||
app = marimo.App(width="columns")
|
||
|
||
|
||
@app.cell
|
||
def _():
|
||
import marimo as mo
|
||
return (mo,)
|
||
|
||
|
||
@app.cell
|
||
def _():
|
||
from backend.db.session import engine
|
||
import pandas as pd
|
||
return engine, pd
|
||
|
||
|
||
@app.cell
|
||
def _(engine, mo, pd):
|
||
# Query directo a information_schema
|
||
query_tables = """
|
||
SELECT table_name
|
||
FROM information_schema.tables
|
||
WHERE table_schema = 'public'
|
||
ORDER BY table_name;
|
||
"""
|
||
tables_df = pd.read_sql(query_tables, engine)
|
||
|
||
# Dropdown para elegir tabla
|
||
table_selector = mo.ui.dropdown(
|
||
options=tables_df["table_name"].tolist(),
|
||
label="Selecciona una tabla",
|
||
allow_select_none= False,
|
||
value=tables_df["table_name"][0]
|
||
)
|
||
table_selector
|
||
return (table_selector,)
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(mo):
|
||
mo.md(r"""## Leer Datos""")
|
||
return
|
||
|
||
|
||
@app.cell
|
||
def _(
|
||
delete_status,
|
||
engine,
|
||
final_status,
|
||
insert_button,
|
||
inserted_data,
|
||
mo,
|
||
pd,
|
||
soft_delete_status,
|
||
soft_restore_status,
|
||
table_selector,
|
||
):
|
||
inserted_data
|
||
final_status
|
||
delete_status
|
||
soft_delete_status
|
||
soft_restore_status
|
||
insert_button
|
||
|
||
query_data = f'SELECT * FROM "{table_selector.value}" LIMIT 1000;'
|
||
df_preview = pd.read_sql(query_data, engine)
|
||
mo.ui.table(df_preview) # editor visual
|
||
return
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
get_obj_id_input = mo.ui.number(label="ID a recuperar", value=1)
|
||
get_obj_button = mo.ui.run_button(label="Recuperar objeto", kind="neutral")
|
||
|
||
mo.hstack([get_obj_id_input, get_obj_button])
|
||
return get_obj_button, get_obj_id_input
|
||
|
||
|
||
@app.cell
|
||
def _(get_obj_button, get_obj_id_input, mo, repo, session, table_selector):
|
||
obj_status = "ℹ️ Introduce un ID y pulsa *Recuperar*."
|
||
obj_python = None
|
||
|
||
if get_obj_button.value and table_selector.value:
|
||
try:
|
||
# Usamos el repo → devuelve un dominio (no un dict)
|
||
_fetched_obj = repo.get_by_id(get_obj_id_input.value)
|
||
if _fetched_obj:
|
||
obj_status = f"✅ Objeto recuperado con ID {get_obj_id_input.value}"
|
||
obj_python = _fetched_obj
|
||
else:
|
||
obj_status = f"⚠️ No se encontró registro con ID {get_obj_id_input.value}"
|
||
except Exception as exc:
|
||
session.rollback()
|
||
obj_status = f"❌ Error al recuperar: {exc!s}"
|
||
|
||
mo.vstack([
|
||
mo.md(f"**Estado:** {obj_status}"),
|
||
mo.inspect(obj_python)
|
||
])
|
||
return
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(mo):
|
||
mo.md(r"""## Crear datos""")
|
||
return
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(engine, pd, table_selector):
|
||
from sqlalchemy import text
|
||
|
||
table_selector
|
||
|
||
query_columns = text("""
|
||
SELECT column_name, data_type, column_default
|
||
FROM information_schema.columns
|
||
WHERE table_schema = 'public'
|
||
AND table_name = :table_name
|
||
AND column_name NOT LIKE 'sys_%'
|
||
ORDER BY ordinal_position;
|
||
""")
|
||
|
||
columns_df = pd.read_sql(query_columns, engine, params={"table_name": table_selector.value})
|
||
|
||
columns_df = columns_df[
|
||
~(
|
||
(columns_df["column_name"] == "id") &
|
||
(columns_df["column_default"].notnull()) &
|
||
(columns_df["column_default"].str.contains("nextval"))
|
||
)
|
||
]
|
||
return (columns_df,)
|
||
|
||
|
||
@app.cell
|
||
def _(columns_df, mo, table_selector):
|
||
# Generamos inputs dinámicamente con soporte para fechas
|
||
ui_inputs = {}
|
||
for _, row in columns_df.iterrows():
|
||
col, dtype = row["column_name"], row["data_type"]
|
||
|
||
if dtype in ("integer", "numeric", "bigint", "smallint"):
|
||
ui_inputs[col] = mo.ui.number(label=col)
|
||
elif dtype in ("boolean",):
|
||
ui_inputs[col] = mo.ui.checkbox(label=col)
|
||
elif dtype in ("date", "timestamp", "timestamp without time zone", "timestamp with time zone"):
|
||
ui_inputs[col] = mo.ui.date(label=col) # 👉 usar date picker de Marimo
|
||
else:
|
||
ui_inputs[col] = mo.ui.text(label=col)
|
||
|
||
# Botón
|
||
run_button_add = mo.ui.run_button(
|
||
label=f"Agregar fila a {table_selector.value}",
|
||
kind="neutral"
|
||
)
|
||
|
||
# Layout de inputs + botón
|
||
mo.vstack(list(ui_inputs.values()) + [run_button_add])
|
||
return run_button_add, ui_inputs
|
||
|
||
|
||
@app.cell
|
||
def _(MapperCls, mo, repo, run_button_add, table_selector, ui_inputs):
|
||
_status_message = "ℹ️ Completa los campos y pulsa *Agregar fila*."
|
||
inserted_id = None
|
||
inserted_data = None
|
||
|
||
if run_button_add.value and table_selector.value:
|
||
try:
|
||
# Recoger inputs y normalizar fechas a ISO
|
||
raw_data = {}
|
||
for _col, ui in ui_inputs.items():
|
||
value = ui.value
|
||
if hasattr(value, "isoformat"): # 👉 si es date/datetime
|
||
raw_data[_col] = value.isoformat()
|
||
elif value == "" or value is None: # 👉 convertir '' → None
|
||
raw_data[_col] = None
|
||
else:
|
||
raw_data[_col] = value
|
||
|
||
# Convertir dict → dominio con el mapper correcto
|
||
dominio_obj = MapperCls.from_dict(raw_data)
|
||
|
||
# Insertar en la tabla usando el repo correcto
|
||
inserted_id = repo.add(dominio_obj, created_by="admin")
|
||
|
||
_status_message = f"✅ Registro insertado en {table_selector.value} con ID: {inserted_id}"
|
||
inserted_data = raw_data
|
||
except Exception as exc:
|
||
_status_message = f"❌ Error al insertar: {exc!s}"
|
||
|
||
|
||
mo.vstack([
|
||
mo.md(f"**Estado:** {_status_message}"),
|
||
mo.json(inserted_data) if inserted_data else mo.md("")
|
||
])
|
||
|
||
return (inserted_data,)
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
mo.md(r"""Para añadir un objeto dentro de la base de datos:""")
|
||
return
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
from domains.nota import notaDom
|
||
|
||
# Aqui genera tu objeto de python
|
||
|
||
# objeto_python = notaDom(
|
||
# nombre="Prueba",
|
||
# descripcion="Registro de prueba insertado desde Python",
|
||
# activo=True,
|
||
|
||
# )
|
||
|
||
insert_button = mo.ui.run_button(label="Insertar objeto en la bbdd", kind="neutral")
|
||
|
||
mo.vstack([
|
||
# mo.inspect(objeto_python),
|
||
insert_button])
|
||
return (insert_button,)
|
||
|
||
|
||
@app.cell
|
||
def _(ejemploRepo, insert_button, mo, objeto_python, session):
|
||
insert_status = "ℹ️ Pulsa el botón para insertar el objeto."
|
||
nuevo_id = None
|
||
|
||
if insert_button.value:
|
||
try:
|
||
# Instancia del repo
|
||
repo_objeto = ejemploRepo(session)
|
||
|
||
# Inserción en la BD
|
||
nuevo_id = repo_objeto.add(
|
||
objeto_python,
|
||
created_by="admin",
|
||
notes="Inserción desde código Python"
|
||
)
|
||
insert_status = f"✅ Registro insertado con ID: {nuevo_id}"
|
||
except Exception as exc:
|
||
session.rollback()
|
||
insert_status = f"❌ Error al insertar: {exc!s}"
|
||
|
||
mo.md(insert_status)
|
||
return
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(mo):
|
||
mo.md(r"""## Actualizar datos""")
|
||
return
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(mo):
|
||
update_id_input = mo.ui.number(label="ID a actualizar", value=1)
|
||
update_fetch_button = mo.ui.run_button(label="Buscar registro", kind="neutral")
|
||
confirm_update_button = mo.ui.run_button(label="Confirmar actualización", kind="success")
|
||
|
||
mo.hstack([update_id_input, update_fetch_button, confirm_update_button])
|
||
return confirm_update_button, update_fetch_button, update_id_input
|
||
|
||
|
||
@app.cell
|
||
def _(
|
||
MapperCls,
|
||
mo,
|
||
repo,
|
||
session,
|
||
table_selector,
|
||
update_fetch_button,
|
||
update_id_input,
|
||
):
|
||
# Valores iniciales
|
||
_update_status = "ℹ️ Introduce un ID y pulsa *Buscar*."
|
||
update_dic = None
|
||
|
||
if update_fetch_button.value and table_selector.value:
|
||
try:
|
||
fetched_obj = repo.get_by_id(update_id_input.value)
|
||
if fetched_obj:
|
||
fetched_dict = MapperCls.to_dict(fetched_obj)
|
||
|
||
update_dic = {
|
||
col: (
|
||
mo.ui.checkbox(value=bool(fetched_dict[col]), label=col)
|
||
if isinstance(fetched_dict[col], bool) else
|
||
mo.ui.number(value=fetched_dict[col], label=col)
|
||
if isinstance(fetched_dict[col], (int, float)) else
|
||
mo.ui.date(value=fetched_dict[col], label=col)
|
||
if hasattr(fetched_dict[col], "isoformat") else
|
||
mo.ui.text(value=str(fetched_dict[col]) if fetched_dict[col] is not None else "", label=col)
|
||
)
|
||
for col in fetched_dict.keys()
|
||
if not col.startswith("sys_") and col != "id"
|
||
}
|
||
|
||
_update_status = f"✅ Registro encontrado con ID: {update_id_input.value}"
|
||
else:
|
||
_update_status = f"⚠️ No se encontró registro con ID: {update_id_input.value}"
|
||
except Exception as exc:
|
||
session.rollback()
|
||
_update_status = f"❌ Error al buscar: {exc!s}"
|
||
|
||
mo.vstack([
|
||
mo.md(f"**Estado:** {_update_status}"),
|
||
mo.vstack(list(update_dic.values())) if update_dic else mo.md("")
|
||
])
|
||
return (update_dic,)
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(confirm_update_button, mo, repo, session, update_dic, update_id_input):
|
||
# Estado por defecto
|
||
final_status = "ℹ️ Esperando confirmación de actualización."
|
||
|
||
if confirm_update_button.value and update_dic is not None:
|
||
try:
|
||
updated_data = {}
|
||
for _col, _ui in update_dic.items():
|
||
val = _ui.value
|
||
# Normalización: "" → None
|
||
if val == "":
|
||
updated_data[_col] = None
|
||
# Normalización: fechas → ISO string
|
||
elif hasattr(val, "isoformat"):
|
||
updated_data[_col] = val.isoformat()
|
||
else:
|
||
updated_data[_col] = val
|
||
|
||
ok = repo.update(
|
||
update_id_input.value,
|
||
updated_data,
|
||
updated_by="admin",
|
||
notes="Actualización desde UI"
|
||
)
|
||
if ok:
|
||
final_status = f"✅ Registro con ID {update_id_input.value} actualizado correctamente."
|
||
else:
|
||
final_status = f"⚠️ No se pudo actualizar el registro con ID {update_id_input.value}."
|
||
except Exception as exc:
|
||
session.rollback()
|
||
final_status = f"❌ Error al actualizar: {exc!s}"
|
||
|
||
mo.md(f"**Estado final:** {final_status}")
|
||
return (final_status,)
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(mo):
|
||
mo.md(r"""## Eliminar datos""")
|
||
return
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
delete_id_input = mo.ui.number(label="ID a eliminar", value=1)
|
||
delete_button = mo.ui.run_button(label="Hard delete", kind="danger")
|
||
|
||
mo.hstack([delete_id_input, delete_button])
|
||
return delete_button, delete_id_input
|
||
|
||
|
||
@app.cell
|
||
def _(delete_button, delete_id_input, mo, repo, session, table_selector):
|
||
delete_status = "ℹ️ Introduce un ID y pulsa *Eliminar*."
|
||
|
||
if delete_button.value and table_selector.value:
|
||
try:
|
||
_ok = repo.delete_by_id(delete_id_input.value)
|
||
if _ok:
|
||
delete_status = f"✅ Registro con ID {delete_id_input.value} eliminado permanentemente."
|
||
else:
|
||
delete_status = f"⚠️ No se encontró registro con ID {delete_id_input.value}."
|
||
except Exception as exc:
|
||
session.rollback()
|
||
delete_status = f"❌ Error al eliminar: {exc!s}"
|
||
|
||
mo.md(f"**Estado:** {delete_status}")
|
||
return (delete_status,)
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
soft_delete_id_input = mo.ui.number(label="ID a eliminar (soft)", value=1)
|
||
soft_delete_button = mo.ui.run_button(label="Soft delete", kind="warn")
|
||
|
||
mo.hstack([soft_delete_id_input, soft_delete_button])
|
||
return soft_delete_button, soft_delete_id_input
|
||
|
||
|
||
@app.cell
|
||
def _(
|
||
mo,
|
||
repo,
|
||
session,
|
||
soft_delete_button,
|
||
soft_delete_id_input,
|
||
table_selector,
|
||
):
|
||
soft_delete_status = "ℹ️ Introduce un ID y pulsa *Soft delete*."
|
||
|
||
if soft_delete_button.value and table_selector.value:
|
||
try:
|
||
_ok = repo.soft_delete(
|
||
soft_delete_id_input.value,
|
||
deleted_by="admin",
|
||
notes="Eliminado desde UI"
|
||
)
|
||
if _ok:
|
||
soft_delete_status = f"✅ Registro con ID {soft_delete_id_input.value} marcado como eliminado (soft delete)."
|
||
else:
|
||
soft_delete_status = f"⚠️ No se encontró registro con ID {soft_delete_id_input.value} o ya estaba eliminado."
|
||
except Exception as exc:
|
||
session.rollback()
|
||
soft_delete_status = f"❌ Error al aplicar soft delete: {exc!s}"
|
||
|
||
mo.md(f"**Estado:** {soft_delete_status}")
|
||
return (soft_delete_status,)
|
||
|
||
|
||
@app.cell
|
||
def _(mo):
|
||
soft_restore_id_input = mo.ui.number(label="ID a restaurar (soft)", value=1)
|
||
soft_restore_button = mo.ui.run_button(label="Restaurar registro", kind="success")
|
||
|
||
mo.hstack([soft_restore_id_input, soft_restore_button])
|
||
return soft_restore_button, soft_restore_id_input
|
||
|
||
|
||
@app.cell
|
||
def _(
|
||
mo,
|
||
repo,
|
||
session,
|
||
soft_restore_button,
|
||
soft_restore_id_input,
|
||
table_selector,
|
||
):
|
||
soft_restore_status = "ℹ️ Introduce un ID y pulsa *Restaurar*."
|
||
|
||
if soft_restore_button.value and table_selector.value:
|
||
try:
|
||
_ok = repo.soft_restore(
|
||
soft_restore_id_input.value,
|
||
restored_by="admin",
|
||
notes="Restaurado desde UI"
|
||
)
|
||
if _ok:
|
||
soft_restore_status = f"✅ Registro con ID {soft_restore_id_input.value} restaurado correctamente."
|
||
else:
|
||
soft_restore_status = f"⚠️ No se encontró registro con ID {soft_restore_id_input.value} o no estaba eliminado."
|
||
except Exception as exc:
|
||
session.rollback()
|
||
soft_restore_status = f"❌ Error al restaurar: {exc!s}"
|
||
|
||
mo.md(f"**Estado:** {soft_restore_status}")
|
||
return (soft_restore_status,)
|
||
|
||
|
||
@app.cell
|
||
def _():
|
||
|
||
# # Arregla transacciones rotas
|
||
|
||
# try:
|
||
# _dominio_obj = MapperCls.from_dict(raw_data)
|
||
# _inserted_id = repo.add(_dominio_obj, created_by="admin")
|
||
# _status_message = f"✅ Insertado con ID {_inserted_id}"
|
||
# except Exception as exc:
|
||
# session.rollback() # 👈 limpia la transacción rota
|
||
# _status_message = f"❌ Error al insertar: {exc!s}"
|
||
return
|
||
|
||
|
||
@app.cell(hide_code=True)
|
||
def _(table_selector):
|
||
import importlib
|
||
from backend.db.session import SessionLocal
|
||
|
||
def load_classes(entidad: str):
|
||
"""
|
||
Importa dinámicamente Repo, Mapper y Dom según convención:
|
||
- Módulo: domains.{entidad} (minúsculas)
|
||
- Clases: {EntidadCapitalizada}Repo, {EntidadCapitalizada}Mapper, {EntidadCapitalizada}Dom
|
||
"""
|
||
entidad = entidad.lower() # siempre minúscula para el módulo
|
||
module_name = f"domains.{entidad}"
|
||
module = importlib.import_module(module_name)
|
||
|
||
# Nombre base: capitalizar primera letra
|
||
base_name = entidad.lower()
|
||
|
||
repo_class = getattr(module, f"{base_name}Repo")
|
||
mapper_class = getattr(module, f"{base_name}Mapper")
|
||
dom_class = getattr(module, f"{base_name}Dom")
|
||
|
||
return repo_class, mapper_class, dom_class
|
||
|
||
|
||
# 👉 Uso interactivo en Marimo
|
||
session = SessionLocal()
|
||
if table_selector.value: # valor que escribiste en el textarea
|
||
RepoCls, MapperCls, DomCls = load_classes(table_selector.value)
|
||
repo = RepoCls(session)
|
||
return MapperCls, repo, session
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app.run()
|