Introducing columns formulas

This commit is contained in:
2026-02-13 21:38:00 +01:00
parent 0df78c0513
commit e8443f07f9
29 changed files with 3889 additions and 15 deletions

View File

@@ -418,9 +418,41 @@ class DataGrid(MultipleInstance):
self._df_store.ns_fast_access = _init_fast_access(self._df)
self._df_store.ns_row_data = _init_row_data(self._df)
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
if init_state:
self._register_existing_formulas()
return self
def _register_existing_formulas(self) -> None:
"""
Re-register all formula columns with the FormulaEngine.
Called after data reload to ensure the engine knows about all
formula columns and their expressions.
"""
engine = self._get_formula_engine()
if engine is None:
return
table = self.get_table_name()
for col_def in self._state.columns:
if col_def.formula:
try:
engine.set_formula(table, col_def.col_id, col_def.formula)
except Exception as e:
logger.warning("Failed to register formula for %s.%s: %s", table, col_def.col_id, e)
def _recalculate_formulas(self) -> None:
"""
Recalculate dirty formula columns before rendering.
Called at the start of mk_body_content_page() to ensure formula
columns are up-to-date before cells are rendered.
"""
engine = self._get_formula_engine()
if engine is None:
return
engine.recalculate_if_needed(self.get_table_name(), self._df_store)
def _get_format_rules(self, col_pos, row_index, col_def):
"""
Get format rules for a cell, returning only the most specific level defined.
@@ -575,6 +607,11 @@ class DataGrid(MultipleInstance):
def get_table_name(self):
return f"{self._settings.namespace}.{self._settings.name}" if self._settings.namespace else self._settings.name
def get_formula_engine(self):
"""Return the FormulaEngine from the DataGridsManager, if available."""
return self._parent.get_formula_engine()
def mk_headers(self):
resize_cmd = self.commands.set_column_width()
move_cmd = self.commands.move_column()
@@ -701,6 +738,7 @@ class DataGrid(MultipleInstance):
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
"""
self._recalculate_formulas()
df = self._get_filtered_df()
if df is None:
return []

View File

@@ -99,6 +99,9 @@ class DataGridColumnsManager(MultipleInstance):
col_def.type = ColumnType(v)
elif k == "width":
col_def.width = int(v)
elif k == "formula":
col_def.formula = v or ""
self._register_formula(col_def)
else:
setattr(col_def, k, v)
@@ -107,6 +110,21 @@ class DataGridColumnsManager(MultipleInstance):
return self.mk_all_columns()
def _register_formula(self, col_def) -> None:
"""Register or remove a formula column with the FormulaEngine."""
engine = self._parent.get_formula_engine()
if engine is None:
return
table = self._parent.get_table_name()
if col_def.formula:
try:
engine.set_formula(table, col_def.col_id, col_def.formula)
logger.debug("Registered formula for %s.%s", table, col_def.col_id)
except Exception as e:
logger.warning("Formula error for %s.%s: %s", table, col_def.col_id, e)
else:
engine.remove_formula(table, col_def.col_id)
def mk_column_label(self, col_def: DataGridColumnState):
return Div(
mk.mk(
@@ -168,6 +186,17 @@ class DataGridColumnsManager(MultipleInstance):
value=col_def.title,
),
*([
Label("Formula"),
Textarea(
col_def.formula or "",
name="formula",
cls=f"textarea textarea-{size} w-full font-mono",
placeholder="{Column} * {OtherColumn}",
rows=3,
),
] if col_def.type == ColumnType.Formula else []),
legend="Column details",
cls="fieldset border-base-300 rounded-box"
),

View File

@@ -0,0 +1,67 @@
"""
DataGridFormulaEditor — DslEditor for formula column expressions.
Extends DslEditor with formula-specific behavior:
- Parses the formula on content change
- Registers the formula with FormulaEngine
- Triggers a body re-render on the parent DataGrid
"""
import logging
from myfasthtml.controls.DslEditor import DslEditor
from myfasthtml.core.formula.dsl.exceptions import FormulaSyntaxError, FormulaCycleError
logger = logging.getLogger("DataGridFormulaEditor")
class DataGridFormulaEditor(DslEditor):
"""
Formula editor for a specific DataGrid column.
Args:
parent: The parent DataGrid instance.
col_def: The DataGridColumnState for the formula column.
conf: DslEditorConf for CodeMirror configuration.
_id: Optional instance ID.
"""
def __init__(self, parent, col_def, conf=None, _id=None):
super().__init__(parent, conf=conf, _id=_id)
self._col_def = col_def
def on_content_changed(self):
"""
Called when the formula text is changed in the editor.
1. Updates col_def.formula with the new text.
2. Registers the formula with the FormulaEngine.
3. Triggers a body re-render of the parent DataGrid.
"""
formula_text = self.get_content()
# Update the column definition
self._col_def.formula = formula_text or ""
# Register with the FormulaEngine
engine = self._parent._get_formula_engine()
if engine is not None:
table = self._parent.get_table_name()
try:
engine.set_formula(table, self._col_def.col_id, formula_text)
logger.debug(
"Formula updated for %s.%s: %s",
table, self._col_def.col_id, formula_text,
)
except FormulaSyntaxError as e:
logger.debug("Formula syntax error, keeping old formula: %s", e)
return
except FormulaCycleError as e:
logger.warning("Formula cycle detected for %s.%s: %s", table, self._col_def.col_id, e)
return
except Exception as e:
logger.warning("Formula engine error for %s.%s: %s", table, self._col_def.col_id, e)
return
# Save state and re-render the grid body
self._parent.save_state()
return self._parent.render_partial("body")

View File

@@ -12,7 +12,7 @@ from myfasthtml.icons.fluent import brain_circuit20_regular
from myfasthtml.icons.fluent_p1 import filter20_regular, search20_regular
from myfasthtml.icons.fluent_p2 import dismiss_circle20_regular
logger = logging.getLogger("DataGridFilter")
logger = logging.getLogger("DataGridQuery")
DG_QUERY_FILTER = "filter"
DG_QUERY_SEARCH = "search"

View File

@@ -16,6 +16,7 @@ from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.formatting.dsl.completion.provider import DatagridMetadataProvider
from myfasthtml.core.formatting.presets import DEFAULT_STYLE_PRESETS, DEFAULT_FORMATTER_PRESETS
from myfasthtml.core.formula.engine import FormulaEngine
from myfasthtml.core.instances import InstancesManager, SingleInstance
from myfasthtml.icons.fluent_p1 import table_add20_regular
from myfasthtml.icons.fluent_p3 import folder_open20_regular
@@ -91,6 +92,11 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
self.style_presets: dict = DEFAULT_STYLE_PRESETS.copy()
self.formatter_presets: dict = DEFAULT_FORMATTER_PRESETS.copy()
self.all_tables_formats: list = []
# Formula engine shared across all DataGrids in this session
self._formula_engine = FormulaEngine(
registry_resolver=self._resolve_store_for_table
)
def upload_from_source(self):
file_upload = FileUpload(self)
@@ -167,10 +173,10 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
def list_column_values(self, table_name, column_name):
return self._registry.get_column_values(table_name, column_name)
def get_row_count(self, table_name):
return self._registry.get_row_count(table_name)
def get_column_type(self, table_name, column_name):
return self._registry.get_column_type(table_name, column_name)
@@ -180,7 +186,29 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
def list_format_presets(self) -> list[str]:
return list(self.formatter_presets.keys())
# === Presets Management ===
def _resolve_store_for_table(self, table_name: str):
"""
Resolve the DatagridStore for a given table name.
Used by FormulaEngine as the registry_resolver callback.
Args:
table_name: Full table name in ``"namespace.name"`` format.
Returns:
DatagridStore instance or None if not found.
"""
try:
as_fullname_dict = self._registry._get_entries_as_full_name_dict()
grid_id = as_fullname_dict.get(table_name)
if grid_id is None:
return None
datagrid = InstancesManager.get(self._session, grid_id, None)
if datagrid is None:
return None
return datagrid._df_store
except Exception:
return None
def get_style_presets(self) -> dict:
"""Get the global style presets."""
@@ -190,6 +218,10 @@ class DataGridsManager(SingleInstance, DatagridMetadataProvider):
"""Get the global formatter presets."""
return self.formatter_presets
def get_formula_engine(self) -> FormulaEngine:
"""The FormulaEngine shared across all DataGrids in this session."""
return self._formula_engine
def add_style_preset(self, name: str, preset: dict):
"""
Add or update a style preset.

View File

@@ -20,6 +20,7 @@ class DataGridColumnState:
visible: bool = True
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
format: list = field(default_factory=list) #
formula: str = "" # formula expression for ColumnType.Formula columns
@dataclass