Files
myrag/crud interactivo.py
T

522 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()