big rework

This commit is contained in:
John Lancaster
2026-08-29 20:10:30 -05:00
parent 73eb490537
commit de95477480
30 changed files with 558 additions and 971 deletions
@@ -0,0 +1,185 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# "pandas",
# ]
# ///
from dataclasses import dataclass
import pandas as pd
from nicegui import binding
from nicegui import events
from nicegui import ui
STATUS_OPTIONS = ["draft", "active", "archived"]
EDITABLE_FIELDS = ("name", "quantity", "status")
TableValue = str | int
TableRow = dict[str, TableValue]
@binding.bindable_dataclass(bindable_fields=EDITABLE_FIELDS)
class EditableRow:
id: int
name: str
quantity: int
status: str
@dataclass(slots=True)
class EditableTableState:
rows_by_id: dict[int, EditableRow]
table_rows_by_id: dict[int, TableRow]
def table_rows(self) -> list[TableRow]:
return list(self.table_rows_by_id.values())
def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
required_columns = {"id", *EDITABLE_FIELDS}
missing_columns = required_columns.difference(dataframe.columns)
if missing_columns:
raise ValueError(f"Missing columns: {sorted(missing_columns)}")
if not dataframe["id"].is_unique:
raise ValueError("The id column must contain unique row keys")
rows_by_id: dict[int, EditableRow] = {}
table_rows_by_id: dict[int, TableRow] = {}
for record in dataframe.to_dict(orient="records"):
row_state = EditableRow(
id=int(record["id"]),
name=str(record["name"]),
quantity=int(record["quantity"]),
status=str(record["status"]),
)
if row_state.status not in STATUS_OPTIONS:
raise ValueError(f"Unknown status {row_state.status!r}")
if row_state.id in rows_by_id:
raise ValueError("Row keys must remain unique after normalization")
table_row: TableRow = {
"id": row_state.id,
"name": row_state.name,
"quantity": row_state.quantity,
"status": row_state.status,
}
for field_name in EDITABLE_FIELDS:
binding.bind_to(
row_state,
field_name,
table_row,
field_name,
other_strict=True,
)
rows_by_id[row_state.id] = row_state
table_rows_by_id[row_state.id] = table_row
return EditableTableState(rows_by_id, table_rows_by_id)
def normalize_edit(field: str, raw_value: object) -> TableValue:
match field:
case "name":
if not isinstance(raw_value, str) or not (name := raw_value.strip()):
raise ValueError("Name is required")
return name
case "quantity":
if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)):
raise TypeError("Quantity must be an integer")
if isinstance(raw_value, float) and not raw_value.is_integer():
raise ValueError("Quantity must be an integer")
try:
quantity = int(raw_value)
except (ValueError, OverflowError) as error:
raise ValueError("Quantity must be an integer") from error
if not 0 <= quantity <= 1_000:
raise ValueError("Quantity must be between 0 and 1000")
return quantity
case "status":
if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS:
raise ValueError("Unknown status")
return raw_value
case _:
raise ValueError(f"Field {field!r} is not editable")
def save_row(dataframe: pd.DataFrame, row_state: EditableRow) -> None:
matching_rows = dataframe["id"].eq(row_state.id)
if int(matching_rows.sum()) != 1:
raise ValueError("This row no longer exists")
dataframe.loc[matching_rows, "name"] = row_state.name
dataframe.loc[matching_rows, "quantity"] = row_state.quantity
dataframe.loc[matching_rows, "status"] = row_state.status
def render_table(dataframe: pd.DataFrame) -> EditableTableState:
state = dataframe_to_state(dataframe)
columns = [
{"name": "name", "label": "Name", "field": "name", "align": "left"},
{"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"},
{"name": "status", "label": "Status", "field": "status", "align": "left"},
]
table = ui.table(
columns=columns,
rows=state.table_rows(),
row_key="id",
selection="multiple",
).classes("w-full")
def apply_edit(event: events.GenericEventArguments) -> None:
try:
raw_row_id, raw_field, raw_value = event.args
row_id = int(raw_row_id)
field_name = str(raw_field)
row_state = state.rows_by_id.get(row_id)
if row_state is None:
raise ValueError("This row no longer exists")
normalized_value = normalize_edit(field_name, raw_value)
previous_value = getattr(row_state, field_name)
setattr(row_state, field_name, normalized_value)
try:
save_row(dataframe, row_state)
except Exception:
setattr(row_state, field_name, previous_value)
raise
except (TypeError, ValueError) as error:
ui.notify(str(error), type="negative")
finally:
table.update_rows(state.table_rows(), clear_selection=False)
with table.add_slot("body-cell-name"), table.cell("name"):
ui.input().props(':model-value="props.value" dense borderless debounce=400').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-quantity"), table.cell("quantity"):
ui.number(min=0, max=1_000).props(':model-value="props.value" dense borderless debounce=400').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
return state
if __name__ in {"__main__", "__mp_main__"}:
items = pd.DataFrame(
[
{"id": 101, "name": "Desk", "quantity": 4, "status": "active"},
{"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"},
]
)
table_state = render_table(items)
ui.run(port=8888, reload=True)