From f30bde26254472ebf4c535af9abe8403c6a06b08 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Sat, 21 Sep 2024 12:55:20 +0200 Subject: [PATCH 01/16] Add new TabulatorOptions --- .idea/misc.xml | 2 +- .idea/py-tabulator.iml | 2 +- pytabulator/data.py | 4 ++ pytabulator/tabulator.py | 9 +++- pytabulator/tabulator_options.py | 78 ++++++++++++++++++++++++++++++++ tests/conftest.py | 7 +++ tests/test_tabulator_columns.py | 2 + 7 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 pytabulator/data.py create mode 100644 pytabulator/tabulator_options.py create mode 100644 tests/conftest.py create mode 100644 tests/test_tabulator_columns.py diff --git a/.idea/misc.xml b/.idea/misc.xml index 574ac89..c223352 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/.idea/py-tabulator.iml b/.idea/py-tabulator.iml index c0c85dc..351d4dd 100644 --- a/.idea/py-tabulator.iml +++ b/.idea/py-tabulator.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/pytabulator/data.py b/pytabulator/data.py new file mode 100644 index 0000000..40ecbd2 --- /dev/null +++ b/pytabulator/data.py @@ -0,0 +1,4 @@ +from pandas import read_csv + +def titanic(): + pass diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 5245f7e..695923f 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -4,7 +4,7 @@ from ._types import TableOptions from ._utils import df_to_dict - +from typing import Self # TODO: Move somewhere else!? def jsonifiable_table_options( @@ -33,7 +33,12 @@ def __init__( # self.table_options = table_options self._table_options = jsonifiable_table_options(table_options) - def options(self, **kwargs) -> Tabulator: + @property + def columns(self) -> list[dict]: + return self._table_options["columns"] + + # TODO: Rename to set_options + def options(self, **kwargs) -> Self: self._table_options.update(kwargs) return self diff --git a/pytabulator/tabulator_options.py b/pytabulator/tabulator_options.py new file mode 100644 index 0000000..3005fd3 --- /dev/null +++ b/pytabulator/tabulator_options.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import Literal, Union, Optional + +from pydantic import BaseModel, ConfigDict, Field, field_validator + +class TabulatorOptions(BaseModel): + """Tabulator options. + + Attributes: + index (str, optional): The index of the table. Defaults to `id`. + header_visible (bool, optional): Whether to display the header of the table. Defaults to `True`. + movable_rows (bool, optional): Whether rows are movable or not. Defaults to `False`. + group_by: Columns to group by. Defaults to `None`. + height (int, optional): Height in px. Defaults to `300`. + pagination (bool, optional): Whether to enable pagination. Defaults to `False`. + pagination_counter (str, optional): Whether to display counted rows in footer. Defaults to `rows`. + pagination_add_row: Where to add rows when pagination is enabled. Defaults to `page`. + selectable_rows: Whether a row is selectable. An integer value sets the maximum number of rows, that can be selected. + If set to `highlight`, rows do not change state when clicked. Defaults to `highlight`. + columns (list, optional): Columns configuration. Defaults to `None`, + which means that the default configuration is used. + layout: The layout of the table. Defaults to `fitColumns`. + add_row_pos: Where to add rows. Defaults to `bottom`. + frozen_rows (int, optional): Number of frozen rows. Defaults to `Ǹone`. + row_height: Fixed height of rows. Defaults to `None`. + history (bool, optional): Whether to enable history. Must be set if `undo` and `redo` is used. Defaults to `False`. + + Note: + See [Tabulator Setup Options](https://tabulator.info/docs/5.5/options) for details. + """ + + index: str = "id" + header_visible: Optional[bool] = Field(True, serialization_alias="headerVisible") + movable_rows: Optional[bool] = Field(False, serialization_alias="movableRows") + group_by: Union[str, list, None] = Field(None, serialization_alias="groupBy") + height: Union[int, str, None] = None + pagination: Optional[bool] = False + pagination_counter: str = Field("rows", serialization_alias="paginationCounter") + pagination_add_row: Literal["page", "table"] = Field( + "page", serialization_alias="paginationAddRow" + ) + selectable_rows: Union[str, bool, int] = Field( + "highlight", serialization_alias="selectableRows" + ) + columns: Optional[list] = None + layout: Literal[ + "fitData", "fitDataFill", "fitDataStretch", "fitDataTable", "fitColumns" + ] = "fitColumns" + add_row_pos: Literal["bottom", "top"] = Field( + "bottom", serialization_alias="addRowPos" + ) + frozen_rows: Optional[int] = Field(None, serialization_alias="frozenRows") + row_height: Optional[int] = Field(None, serialization_alias="rowHeight") + resizable_column_fit: Optional[bool] = Field(False, serialization_alias="resizableColumnFit") + history: Optional[bool] = False + + # New features to be added in the next release + """ + responsiveLayout: str = "hide" + columnDefaults: dict = {"tooltip": True} + """ + + model_config = ConfigDict( + validate_assignment=True, + extra="allow", + # use_enum_values=True + ) + + @field_validator("height") + def validate_height(cls, v): + if isinstance(v, int): + return f"{v}px" + + return v + + def to_dict(self) -> dict: + return self.model_dump(by_alias=True, exclude_none=True) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..57472cf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import pytest +import pandas as pd + +@pytest.fixture +def persons(): + data = [["Peter", 10, 10.5], ["Hans", 12, 13.7]] + return pd.DataFrame(data, columns=["Name", "Age", "JustANumber"]) diff --git a/tests/test_tabulator_columns.py b/tests/test_tabulator_columns.py new file mode 100644 index 0000000..0ed3edd --- /dev/null +++ b/tests/test_tabulator_columns.py @@ -0,0 +1,2 @@ +def test_tabulator_columns(persons): + print(persons) From 39ed0d1f641ae19072c43856d1a78657c4bde1a1 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Sat, 21 Sep 2024 13:18:36 +0200 Subject: [PATCH 02/16] Add method to update column --- pytabulator/shiny_bindings.py | 7 +++-- pytabulator/tabulator.py | 51 ++++++++++++++++++++------------- tests/test_tabulator_columns.py | 14 ++++++++- 3 files changed, 49 insertions(+), 23 deletions(-) diff --git a/pytabulator/shiny_bindings.py b/pytabulator/shiny_bindings.py index c6b212a..341b5b5 100644 --- a/pytabulator/shiny_bindings.py +++ b/pytabulator/shiny_bindings.py @@ -10,7 +10,8 @@ from ._types import TableOptions from ._utils import df_to_dict -from .tabulator import Tabulator, jsonifiable_table_options +# from .tabulator import Tabulator, jsonifiable_table_options +from .tabulator import Tabulator # from . import TableOptions @@ -95,5 +96,7 @@ async def render(self) -> Jsonifiable: # return {"values": value.values.tolist(), "columns": value.columns.tolist()} # TODO: convert with js data = df_to_dict(df) - data["options"] = jsonifiable_table_options(self.table_options) + + # TODO: Fix this, func was removed + # data["options"] = jsonifiable_table_options(self.table_options) return data diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 695923f..6c9fc01 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -2,18 +2,11 @@ from pandas import DataFrame -from ._types import TableOptions from ._utils import df_to_dict -from typing import Self +from typing import Self, Any -# TODO: Move somewhere else!? -def jsonifiable_table_options( - table_options: TableOptions | dict, -) -> dict: - if isinstance(table_options, TableOptions): - return table_options.to_dict() - - return table_options +from .tabulator_options import TabulatorOptions +from .utils import create_columns class Tabulator(object): @@ -21,29 +14,47 @@ class Tabulator(object): Args: df (DataFrame): A data frame. - table_options (TableOptions): Table options. + options (TabulatorOptions): Setup options. """ def __init__( self, df: DataFrame, - table_options: TableOptions | dict = {}, + options: TabulatorOptions | dict = TabulatorOptions(), ) -> None: self.df = df - # self.table_options = table_options - self._table_options = jsonifiable_table_options(table_options) + self._options = ( + options + if isinstance(options, TabulatorOptions) + else TabulatorOptions(**options) + ) + if not self._options.columns: + self._options.columns = create_columns(self.df) @property def columns(self) -> list[dict]: - return self._table_options["columns"] + return self._options.columns + + def _find_column(self, col_name: str) -> tuple: + for i, col in enumerate(self.columns): + if col["field"] == col_name: + return i, col + + return None, None + + def update_column(self, col_name: str, **kwargs: Any) -> Self: + i, col = self._find_column(col_name) + if col is not None: + self._options.columns[i] = col | kwargs + + return self - # TODO: Rename to set_options - def options(self, **kwargs) -> Self: - self._table_options.update(kwargs) + def set_options(self, **kwargs) -> Self: + pass return self def to_dict(self) -> dict: + # TODO: Rename 'data' to ??? data = df_to_dict(self.df) - # data["options"] = jsonifiable_table_options(self.table_options) - data["options"] = self._table_options + data["options"] = self._options.to_dict() return data diff --git a/tests/test_tabulator_columns.py b/tests/test_tabulator_columns.py index 0ed3edd..bb1c1b8 100644 --- a/tests/test_tabulator_columns.py +++ b/tests/test_tabulator_columns.py @@ -1,2 +1,14 @@ +from pytabulator import Tabulator + + def test_tabulator_columns(persons): - print(persons) + # print(persons) + table = Tabulator(persons) + + print(table.columns) + + col = table._find_column("Name") + print(col) + + table = table.update_column("Name", editor = True) + print(table.columns) From ad8e4e60e2fa04bbe7875f411a994b2abf6a824c Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Sat, 21 Sep 2024 13:28:26 +0200 Subject: [PATCH 03/16] Add generic set_formatter func --- pytabulator/tabulator.py | 16 ++++++++++++++++ tests/test_tabulator_columns.py | 3 +++ 2 files changed, 19 insertions(+) diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 6c9fc01..b9b9613 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -49,6 +49,22 @@ def update_column(self, col_name: str, **kwargs: Any) -> Self: return self + def set_formatter( + self, + col_name: str, + formatter: str, + formatter_params: dict = None, + **kwargs: Any, + ) -> Self: + return self.update_column( + col_name, + **dict( + formatter=formatter, + formatterParams=formatter_params or dict(), + **kwargs, + ), + ) + def set_options(self, **kwargs) -> Self: pass return self diff --git a/tests/test_tabulator_columns.py b/tests/test_tabulator_columns.py index bb1c1b8..ec7ebc8 100644 --- a/tests/test_tabulator_columns.py +++ b/tests/test_tabulator_columns.py @@ -12,3 +12,6 @@ def test_tabulator_columns(persons): table = table.update_column("Name", editor = True) print(table.columns) + + table = table.set_formatter("Age", "html", hozAlign="center") + print(table.columns) From 7645f87d776812c47ea1e9fe13ca7cee04e78253 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 11:01:18 +0200 Subject: [PATCH 04/16] Use latest bindings --- docs/examples/getting_started/app.py | 2 +- get-py-bindings.sh | 4 + pytabulator/shiny_bindings.py | 4 +- pytabulator/srcjs/pytabulator.js | 129 ++++++++++++++++++++++++ pytabulator/srcjs/tabulator-bindings.js | 1 - pytabulator/tabulator.py | 6 +- 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100755 get-py-bindings.sh create mode 100644 pytabulator/srcjs/pytabulator.js delete mode 100644 pytabulator/srcjs/tabulator-bindings.js diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index fa1695e..6d7e018 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -15,7 +15,7 @@ def tabulator(): df = pd.read_csv( "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv" ) - return Tabulator(df, table_options={"height": 311}) + return Tabulator(df, options={"height": 311}) @render.code async def txt(): diff --git a/get-py-bindings.sh b/get-py-bindings.sh new file mode 100755 index 0000000..e3da277 --- /dev/null +++ b/get-py-bindings.sh @@ -0,0 +1,4 @@ +#!/bin/sh +branch=${1:-dev} +# curl -O https://raw.githubusercontent.com/eodaGmbH/tabulator-bindings/${branch}/r-bindings/rtabulator.js +curl -o pytabulator/srcjs/pytabulator.js https://raw.githubusercontent.com/eodaGmbH/tabulator-bindings/refs/heads/feature/typescript/py-bindings/pytabulator.js diff --git a/pytabulator/shiny_bindings.py b/pytabulator/shiny_bindings.py index 341b5b5..07fc315 100644 --- a/pytabulator/shiny_bindings.py +++ b/pytabulator/shiny_bindings.py @@ -33,10 +33,10 @@ def tabulator_dep() -> HTMLDependency: tabulator_bindings_dep = HTMLDependency( - "tabulator-bindings", + "pytabulator", "0.1.0", source={"package": "pytabulator", "subdir": "srcjs"}, - script={"src": "tabulator-bindings.js", "type": "module"}, + script={"src": "pytabulator.js", "type": "module"}, all_files=False, ) diff --git a/pytabulator/srcjs/pytabulator.js b/pytabulator/srcjs/pytabulator.js new file mode 100644 index 0000000..0078270 --- /dev/null +++ b/pytabulator/srcjs/pytabulator.js @@ -0,0 +1,129 @@ +"use strict"; +(() => { + // built/utils.js + function convertToDataFrame(data) { + const res = {}; + if (data.length === 0) { + return res; + } + const keys = Object.keys(data[0]); + keys.forEach((key) => res[key] = data.map((item) => item[key])); + return res; + } + + // built/events.js + function addEventListeners(tabulatorWidget) { + const table = tabulatorWidget.getTable(); + const elementId = tabulatorWidget.getElementId(); + const bindingLang = tabulatorWidget.getBindingLang(); + console.log("binding lang", bindingLang); + table.on("rowClick", function(e, row) { + const inputName = `${elementId}_row_clicked`; + console.log(inputName, row.getData()); + Shiny.onInputChange(inputName, row.getData()); + }); + table.on("rowClick", (e, row) => { + const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`; + const data = table.getSelectedRows().map((row2) => row2.getData()); + console.log(inputName, data); + Shiny.onInputChange(inputName, { data: convertToDataFrame(data) }); + }); + table.on("cellEdited", function(cell) { + const inputName = `${elementId}_cell_edited`; + console.log(inputName, cell.getData()); + Shiny.onInputChange(inputName, cell.getData()); + }); + table.on("dataFiltered", function(filters, rows) { + const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`; + const data = rows.map((row) => row.getData()); + console.log(inputName, data); + Shiny.onInputChange(inputName, { data: convertToDataFrame(data) }); + }); + } + + // built/widget.js + function run_calls(tabulatorWidget, calls) { + const table = tabulatorWidget.getTable(); + const elementId = tabulatorWidget.getElementId(); + const bindingLang = tabulatorWidget.getBindingLang(); + console.log("binding lang", bindingLang); + calls.forEach(([method_name, options]) => { + if (method_name === "getData") { + const inputName = bindingLang === "r" ? `${elementId}_data:rtabulator.data` : `${elementId}_data`; + console.log("custom call", inputName); + Shiny.setInputValue(inputName, { data: convertToDataFrame(table.getData()) }, { priority: "event" }); + return; + } + if (method_name === "deleteSelectedRows") { + console.log("custom call"); + const rows = table.getSelectedRows(); + rows.forEach((row) => { + console.log(row.getIndex()); + table.deleteRow(row.getIndex()); + }); + return; + } + if (method_name === "getSheetData") { + const inputName = bindingLang === "r" ? `${elementId}_sheet_data:rtabulator.sheet_data` : `${elementId}_sheet_data`; + console.log("custom call", inputName); + Shiny.setInputValue(inputName, { data: table.getSheetData() }, { priority: "event" }); + return; + } + console.log(method_name, options); + table[method_name](...options); + }); + } + var TabulatorWidget = class { + constructor(container, data, options, bindingOptions) { + options.data = data; + this._container = container; + this._bindingOptions = bindingOptions; + console.log("columns", options.columns); + if (data !== null && options.columns == null) { + options.autoColumns = true; + } + if (options.spreadsheet && options.spreadsheetData == null) { + options.spreadsheetData = []; + } + this._table = new Tabulator(this._container, options); + if (typeof Shiny === "object") { + addEventListeners(this); + this._addShinyMessageHandler(); + } + } + _addShinyMessageHandler() { + const messageHandlerName = `tabulator-${this._container.id}`; + Shiny.addCustomMessageHandler(messageHandlerName, (payload) => { + console.log(payload); + run_calls(this, payload.calls); + }); + } + getTable() { + return this._table; + } + getElementId() { + return this._container.id; + } + getBindingLang() { + return this._bindingOptions.lang; + } + }; + + // built/index-py.js + var TabulatorOutputBinding = class extends Shiny.OutputBinding { + find(scope) { + return scope.find(".shiny-tabulator-output"); + } + renderValue(el, payload) { + console.log("payload", payload); + const widget = new TabulatorWidget(el, payload.data, payload.options, payload.bindingOptions); + const table = widget.getTable(); + table.on("tableBuilt", function() { + if (payload.options.columnUpdates != null) { + console.log("column updates", payload.options.columnUpdates); + } + }); + } + }; + Shiny.outputBindings.register(new TabulatorOutputBinding(), "shiny-tabulator-output"); +})(); diff --git a/pytabulator/srcjs/tabulator-bindings.js b/pytabulator/srcjs/tabulator-bindings.js deleted file mode 100644 index 81b0324..0000000 --- a/pytabulator/srcjs/tabulator-bindings.js +++ /dev/null @@ -1 +0,0 @@ -(()=>{function c(s,e){s.on("rowClick",function(n,t){let o=`${e.id}_row_clicked`;console.log(o,t.getData()),Shiny.onInputChange(o,t.getData())}),s.on("rowClick",(n,t)=>{let o=`${e.id}_rows_selected`,a=s.getSelectedRows().map(i=>i.getData());console.log(o,a),Shiny.onInputChange(o,a)}),s.on("cellEdited",function(n){let t=`${e.id}_row_edited`;console.log(t,n.getData()),Shiny.onInputChange(t,n.getData())}),s.on("dataFiltered",function(n,t){let o=`${e.id}_data_filtered`,a=t.map(i=>i.getData());console.log(o,a),Shiny.onInputChange(o,a)})}function r(s,e,n){n.forEach(([t,o])=>{if(t==="getData"){console.log("custom call"),Shiny.onInputChange(`${s.id}_data`,e.getData());return}if(t==="deleteSelectedRows"){console.log("custom call"),e.getSelectedRows().forEach(i=>{console.log(i.getIndex()),e.deleteRow(i.getIndex())});return}console.log(t,o),e[t](...o)})}var l=class{constructor(e,n,t){t.data=n,this._container=e,console.log("columns",t.columns),t.columns==null&&(t.autoColumns=!0),this._table=new Tabulator(this._container,t),typeof Shiny=="object"&&(c(this._table,this._container),this._addShinyMessageHandler())}_addShinyMessageHandler(){let e=`tabulator-${this._container.id}`;Shiny.addCustomMessageHandler(e,n=>{console.log(n),r(this._container,this._table,n.calls)})}getTable(){return this._table}};var u=class extends Shiny.OutputBinding{find(e){return e.find(".shiny-tabulator-output")}renderValue(e,n){console.log("payload",n),new l(e,n.data,n.options).getTable().on("tableBuilt",function(){n.options.columnUpdates!=null&&console.log("column updates",n.options.columnUpdates)})}};Shiny.outputBindings.register(new u,"shiny-tabulator-output");})(); diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index b9b9613..efba098 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -70,7 +70,11 @@ def set_options(self, **kwargs) -> Self: return self def to_dict(self) -> dict: - # TODO: Rename 'data' to ??? + # TODO: Rename 'data' to ??? model!? data = df_to_dict(self.df) data["options"] = self._options.to_dict() + data["bindingOptions"] = dict(lang = "python") return data + + def to_html(self): + pass From 4adf47f972cd2b16e3c4a77cafb5a4a87e2ddd6c Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 11:49:53 +0200 Subject: [PATCH 05/16] Add formatter funcs --- docs/examples/getting_started/app.py | 12 +++++++++++- pytabulator/tabulator.py | 27 ++++++++++++++++++++------- tests/test_tabulator_columns.py | 2 +- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index 6d7e018..ecd7287 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -15,7 +15,17 @@ def tabulator(): df = pd.read_csv( "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv" ) - return Tabulator(df, options={"height": 311}) + """ + return Tabulator(df, options={"height": 311}).set_column_formatter( + "Pclass", "star", {"stars": 3}, hozAlign="center" + ) + """ + return ( + Tabulator(df) + .set_options(height=311) + .set_column_formatter_star("Pclass", 3) + .set_column_formatter_tick_cross("Survived", hozAlign="center") + ) @render.code async def txt(): diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index efba098..45691a2 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -49,7 +49,7 @@ def update_column(self, col_name: str, **kwargs: Any) -> Self: return self - def set_formatter( + def set_column_formatter( self, col_name: str, formatter: str, @@ -65,16 +65,29 @@ def set_formatter( ), ) + def set_column_formatter_star(self, column: str, stars: int, **kwargs) -> Self: + formatter_params = dict(stars=stars) + self.set_column_formatter( + column, "star", formatter_params, hozAlign="center", **kwargs + ) + return self + + def set_column_formatter_tick_cross(self, column, **kwargs) -> Self: + self.set_column_formatter(column, "tickCross", **kwargs) + return self + + def set_column_editor(self) -> Self: + return self + def set_options(self, **kwargs) -> Self: - pass + self._options = self._options.model_copy(update = kwargs) return self def to_dict(self) -> dict: - # TODO: Rename 'data' to ??? model!? - data = df_to_dict(self.df) - data["options"] = self._options.to_dict() - data["bindingOptions"] = dict(lang = "python") - return data + payload = df_to_dict(self.df) + payload["options"] = self._options.to_dict() + payload["bindingOptions"] = dict(lang="python") + return payload def to_html(self): pass diff --git a/tests/test_tabulator_columns.py b/tests/test_tabulator_columns.py index ec7ebc8..5cde48a 100644 --- a/tests/test_tabulator_columns.py +++ b/tests/test_tabulator_columns.py @@ -13,5 +13,5 @@ def test_tabulator_columns(persons): table = table.update_column("Name", editor = True) print(table.columns) - table = table.set_formatter("Age", "html", hozAlign="center") + table = table.set_column_formatter("Age", "html", hozAlign="center") print(table.columns) From 65c2e6766b37bdc271761d400ea7ab13b11e336e Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 11:59:31 +0200 Subject: [PATCH 06/16] Add editor example --- docs/examples/getting_started/app.py | 1 + pytabulator/tabulator.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index ecd7287..0d0c268 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -25,6 +25,7 @@ def tabulator(): .set_options(height=311) .set_column_formatter_star("Pclass", 3) .set_column_formatter_tick_cross("Survived", hozAlign="center") + .set_column_editor("Fare", "number", dict(min=0, max=10)) ) @render.code diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 45691a2..2e6165d 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -65,19 +65,26 @@ def set_column_formatter( ), ) - def set_column_formatter_star(self, column: str, stars: int, **kwargs) -> Self: + def set_column_formatter_star(self, col_name: str, stars: int, **kwargs) -> Self: formatter_params = dict(stars=stars) self.set_column_formatter( - column, "star", formatter_params, hozAlign="center", **kwargs + col_name, "star", formatter_params, hozAlign="center", **kwargs ) return self - def set_column_formatter_tick_cross(self, column, **kwargs) -> Self: - self.set_column_formatter(column, "tickCross", **kwargs) + def set_column_formatter_tick_cross(self, col_name, **kwargs) -> Self: + self.set_column_formatter(col_name, "tickCross", **kwargs) return self - def set_column_editor(self) -> Self: - return self + def set_column_editor(self, col_name: str, editor: str, editor_params: dict = None, **kwargs: Any) -> Self: + return self.update_column( + col_name, + **dict( + editor=editor, + editorParams=editor_params or dict(), + **kwargs, + ), + ) def set_options(self, **kwargs) -> Self: self._options = self._options.model_copy(update = kwargs) From d4f66e6da6d2584e395d371b672f676fcbfbc974 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 13:21:32 +0200 Subject: [PATCH 07/16] Add method to change col title --- docs/examples/getting_started/app.py | 4 +++- pytabulator/tabulator.py | 31 ++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index 0d0c268..2720007 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -25,7 +25,9 @@ def tabulator(): .set_options(height=311) .set_column_formatter_star("Pclass", 3) .set_column_formatter_tick_cross("Survived", hozAlign="center") - .set_column_editor("Fare", "number", dict(min=0, max=10)) + # .set_column_editor("Fare", "number", dict(min=0, max=10)) + .set_column_editor_number("Fare", min_=0, max_=5) + .set_column_title("Pclass", "PassengerClass") ) @render.code diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 2e6165d..7c7be4d 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -42,6 +42,7 @@ def _find_column(self, col_name: str) -> tuple: return None, None + # ----- Column generics ----- def update_column(self, col_name: str, **kwargs: Any) -> Self: i, col = self._find_column(col_name) if col is not None: @@ -65,6 +66,18 @@ def set_column_formatter( ), ) + # ----- Column formatters ----- + def set_column_editor(self, col_name: str, editor: str, editor_params: dict = None, validator: Any = None, **kwargs: Any) -> Self: + return self.update_column( + col_name, + **dict( + editor=editor, + editorParams=editor_params or dict(), + validator = validator, + **kwargs, + ), + ) + def set_column_formatter_star(self, col_name: str, stars: int, **kwargs) -> Self: formatter_params = dict(stars=stars) self.set_column_formatter( @@ -76,16 +89,16 @@ def set_column_formatter_tick_cross(self, col_name, **kwargs) -> Self: self.set_column_formatter(col_name, "tickCross", **kwargs) return self - def set_column_editor(self, col_name: str, editor: str, editor_params: dict = None, **kwargs: Any) -> Self: - return self.update_column( - col_name, - **dict( - editor=editor, - editorParams=editor_params or dict(), - **kwargs, - ), - ) + # ----- Column editor ----- + def set_column_editor_number(self, col_name: str, min_: float = None, max_: float = None, **kwargs) -> Self: + editor_params = dict(min=min_, max=max_) + return self.set_column_editor(col_name, "number", editor_params, **kwargs) + + # ----- Column headers ----- + def set_column_title(self, col_name: str, title: str, **kwargs) -> Self: + return self.update_column(col_name, title=title, **kwargs) + # ----- Misc ----- def set_options(self, **kwargs) -> Self: self._options = self._options.model_copy(update = kwargs) return self From 5e6b4192d5a29d99eab1688820dd90fb4f0a45ca Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 14:17:25 +0200 Subject: [PATCH 08/16] Add as_camel_recursive --- .idea/watcherTasks.xml | 1 - docs/examples/getting_started/app.py | 1 + pytabulator/_utils.py | 18 +++++++++++++++--- pytabulator/tabulator.py | 4 ++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml index a2f3433..c7e1a41 100644 --- a/.idea/watcherTasks.xml +++ b/.idea/watcherTasks.xml @@ -2,7 +2,6 @@ - diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index 2720007..e7203ed 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -28,6 +28,7 @@ def tabulator(): # .set_column_editor("Fare", "number", dict(min=0, max=10)) .set_column_editor_number("Fare", min_=0, max_=5) .set_column_title("Pclass", "PassengerClass") + .set_column_editor("Name", "input", hoz_align="center") ) @render.code diff --git a/pytabulator/_utils.py b/pytabulator/_utils.py index 6cd9876..42a55ce 100644 --- a/pytabulator/_utils.py +++ b/pytabulator/_utils.py @@ -15,6 +15,18 @@ def set_theme(stylesheet): def snake_to_camel_case(snake_str: str) -> str: return snake_str[0].lower() + snake_str.title()[1:].replace("_", "") - # return "".join( - # [item if not i else item.title() for i, item in enumerate(snake_str.split("_"))] - # ) + +def as_camel_dict(snake_dict: dict) -> dict: + return {snake_to_camel_case(k): v for (k, v) in snake_dict.items() if v is not None} + + +def as_camel_dict_recursive(snake_dict: dict) -> dict: + camel_case_dict = {} + for k, v in snake_dict.items(): + if v is not None: + if isinstance(v, dict): + camel_case_dict[snake_to_camel_case(k)] = as_camel_dict_recursive(v) + else: + camel_case_dict[snake_to_camel_case(k)] = v + + return camel_case_dict diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 7c7be4d..ea72d90 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -2,7 +2,7 @@ from pandas import DataFrame -from ._utils import df_to_dict +from ._utils import df_to_dict, as_camel_dict_recursive from typing import Self, Any from .tabulator_options import TabulatorOptions @@ -46,7 +46,7 @@ def _find_column(self, col_name: str) -> tuple: def update_column(self, col_name: str, **kwargs: Any) -> Self: i, col = self._find_column(col_name) if col is not None: - self._options.columns[i] = col | kwargs + self._options.columns[i] = col | as_camel_dict_recursive(kwargs) return self From a7ddcf3c563a9e9baa459075120d82b78c0f94cb Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 16:34:22 +0200 Subject: [PATCH 09/16] Add formatter and editor classes --- docs/examples/getting_started/app.py | 17 +++++-- pytabulator/_utils.py | 6 ++- pytabulator/editors.py | 47 ++++++++++++++++++ pytabulator/formatters.py | 24 ++++++++++ pytabulator/tabulator.py | 72 +++++++++++++++++++++------- 5 files changed, 145 insertions(+), 21 deletions(-) create mode 100644 pytabulator/editors.py create mode 100644 pytabulator/formatters.py diff --git a/docs/examples/getting_started/app.py b/docs/examples/getting_started/app.py index e7203ed..f6725ec 100644 --- a/docs/examples/getting_started/app.py +++ b/docs/examples/getting_started/app.py @@ -1,6 +1,8 @@ import pandas as pd from pytabulator.shiny_bindings import output_tabulator, render_tabulator from pytabulator.tabulator import Tabulator +from pytabulator.editors import NumberEditor, StarEditor, ProgressEditor +from pytabulator.formatters import StarFormatter from shiny import App, render, ui app_ui = ui.page_fluid( @@ -23,12 +25,21 @@ def tabulator(): return ( Tabulator(df) .set_options(height=311) - .set_column_formatter_star("Pclass", 3) - .set_column_formatter_tick_cross("Survived", hozAlign="center") + # .set_column_formatter_star("Pclass", 3) + .set_column_formatter("Pclass", StarFormatter(stars=3), hoz_align="center") + .set_column_formatter_tick_cross("Survived", hoz_align="center") # .set_column_editor("Fare", "number", dict(min=0, max=10)) .set_column_editor_number("Fare", min_=0, max_=5) .set_column_title("Pclass", "PassengerClass") - .set_column_editor("Name", "input", hoz_align="center") + .set_column_editor(["Name", "Sex"], "input", hoz_align="center") + .set_column_editor("PassengerId", NumberEditor(min=0, max=1000, step=1)) + .set_column_editor("Pclass", StarEditor()) + .set_column_formatter("Fare", "progress") + .set_column_editor( + "Fare", + ProgressEditor(min=0, max=100, element_attributes=dict(title="Hey ho")), + hoz_align="left", + ) ) @render.code diff --git a/pytabulator/_utils.py b/pytabulator/_utils.py index 42a55ce..1e102ca 100644 --- a/pytabulator/_utils.py +++ b/pytabulator/_utils.py @@ -24,9 +24,11 @@ def as_camel_dict_recursive(snake_dict: dict) -> dict: camel_case_dict = {} for k, v in snake_dict.items(): if v is not None: + camel_key = snake_to_camel_case(k) if "_" in k else k + if isinstance(v, dict): - camel_case_dict[snake_to_camel_case(k)] = as_camel_dict_recursive(v) + camel_case_dict[camel_key] = as_camel_dict_recursive(v) else: - camel_case_dict[snake_to_camel_case(k)] = v + camel_case_dict[camel_key] = v return camel_case_dict diff --git a/pytabulator/editors.py b/pytabulator/editors.py new file mode 100644 index 0000000..50b71e3 --- /dev/null +++ b/pytabulator/editors.py @@ -0,0 +1,47 @@ +from pydantic import BaseModel + +from typing import Optional + +from ._utils import as_camel_dict_recursive +from enum import Enum + + +class Editors(Enum): + NUMBER = "number" + INPUT = "input" + STAR = "star" + PROGRESS = "progress" + + +class Editor(BaseModel): + @property + def name(self) -> str: + return "" + + def to_dict(self) -> dict: + return as_camel_dict_recursive(self.model_dump(exclude_none=True)) + + +class NumberEditor(Editor): + min: Optional[float] = None + max: Optional[float] = None + step: Optional[float] = None + + @property + def name(self) -> str: + return Editors.NUMBER.value + + +class StarEditor(Editor): + @property + def name(self) -> str: + return Editors.STAR.value + +class ProgressEditor(Editor): + min: Optional[float] = None + max: Optional[float] = None + element_attributes: Optional[dict] = None + + @property + def name(self) -> str: + return Editors.PROGRESS.value diff --git a/pytabulator/formatters.py b/pytabulator/formatters.py new file mode 100644 index 0000000..dc70041 --- /dev/null +++ b/pytabulator/formatters.py @@ -0,0 +1,24 @@ +from enum import Enum +from pydantic import BaseModel +from typing import Optional +from ._utils import as_camel_dict_recursive + +class Formatters(Enum): + STAR = "star" + PROGRESS = "progress" + TICK_CROSS = "tickCross" + +class Formatter(BaseModel): + def to_dict(self) -> dict: + return as_camel_dict_recursive(self.model_dump(exclude_none=True)) + + @property + def name(self) -> str: + return "" + +class StarFormatter(Formatter): + stars: Optional[int] = None + + @property + def name(self) -> str: + return Formatters.STAR.value diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index ea72d90..2d73edf 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -7,7 +7,8 @@ from .tabulator_options import TabulatorOptions from .utils import create_columns - +from .editors import Editor +from .formatters import Formatter class Tabulator(object): """Tabulator @@ -42,57 +43,96 @@ def _find_column(self, col_name: str) -> tuple: return None, None - # ----- Column generics ----- - def update_column(self, col_name: str, **kwargs: Any) -> Self: + # Update single column + def _update_column(self, col_name: str, **kwargs: Any) -> Self: i, col = self._find_column(col_name) if col is not None: self._options.columns[i] = col | as_camel_dict_recursive(kwargs) return self + # ----- Column generics ----- + def update_column(self, col_name: str | list, **kwargs: Any) -> Self: + col_names = [col_name] if isinstance(col_name, str) else col_name + for col_name in col_names: + self._update_column(col_name, **kwargs) + + return self + def set_column_formatter( self, - col_name: str, - formatter: str, + col_name: str | list, + formatter: str | Formatter, formatter_params: dict = None, **kwargs: Any, ) -> Self: + if isinstance(formatter, Formatter): + formatter_name = formatter.name + formatter_params = formatter.to_dict() + else: + formatter_name = formatter + return self.update_column( col_name, **dict( - formatter=formatter, + formatter=formatter_name, formatterParams=formatter_params or dict(), **kwargs, ), ) - # ----- Column formatters ----- - def set_column_editor(self, col_name: str, editor: str, editor_params: dict = None, validator: Any = None, **kwargs: Any) -> Self: + def set_column_editor( + self, + col_name: str | list, + editor: str | Editor, + editor_params: dict = None, + validator: Any = None, + **kwargs: Any, + ) -> Self: + if isinstance(editor, Editor): + editor_name = editor.name + editor_params = editor.to_dict() + else: + editor_name = editor + return self.update_column( col_name, **dict( - editor=editor, + editor=editor_name, editorParams=editor_params or dict(), - validator = validator, + validator=validator, **kwargs, ), ) - def set_column_formatter_star(self, col_name: str, stars: int, **kwargs) -> Self: + # ----- Column formatters ----- + def set_column_formatter_star( + self, col_name: str | list, stars: int, **kwargs + ) -> Self: formatter_params = dict(stars=stars) self.set_column_formatter( col_name, "star", formatter_params, hozAlign="center", **kwargs ) return self - def set_column_formatter_tick_cross(self, col_name, **kwargs) -> Self: + def set_column_formatter_tick_cross(self, col_name: str | list, **kwargs) -> Self: self.set_column_formatter(col_name, "tickCross", **kwargs) return self # ----- Column editor ----- - def set_column_editor_number(self, col_name: str, min_: float = None, max_: float = None, **kwargs) -> Self: - editor_params = dict(min=min_, max=max_) - return self.set_column_editor(col_name, "number", editor_params, **kwargs) + def set_column_editor_number( + self, + col_name: str | list, + min_value: float = None, + max_value: float = None, + step: float = None, + validator=None, + **kwargs, + ) -> Self: + editor_params = dict(min=min_value, max=max_value, step=step) + return self.set_column_editor( + col_name, "number", editor_params, validator, **kwargs + ) # ----- Column headers ----- def set_column_title(self, col_name: str, title: str, **kwargs) -> Self: @@ -100,7 +140,7 @@ def set_column_title(self, col_name: str, title: str, **kwargs) -> Self: # ----- Misc ----- def set_options(self, **kwargs) -> Self: - self._options = self._options.model_copy(update = kwargs) + self._options = self._options.model_copy(update=kwargs) return self def to_dict(self) -> dict: From d73a1a691046b448e57978dc1b42e46058e98285 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 16:40:33 +0200 Subject: [PATCH 10/16] Fix tests --- tests/test_table.py | 31 +++++-------------------------- tests/test_table_options.py | 19 +++++-------------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/tests/test_table.py b/tests/test_table.py index 4a368a1..a975cda 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -1,12 +1,7 @@ import pytest from pandas import DataFrame -from pydantic import BaseModel from pytabulator import Tabulator -from pytabulator._table_options_dc import TableOptionsDC as TableOptionsDC - -# from pytabulator import TableOptions as TableOptionsPydantic -from pytabulator._table_options_pydantic import TableOptionsPydantic - +from pytabulator.tabulator_options import TabulatorOptions @pytest.fixture def df() -> DataFrame: @@ -14,31 +9,15 @@ def df() -> DataFrame: return DataFrame(data, columns=["Name", "Age"]) -def test_table_dc(df: DataFrame) -> None: - # Prepare - table_options = TableOptionsDC(selectable_rows=3) - - # Act - table = Tabulator(df, table_options=table_options) - table_dict = table.to_dict() - print(table_dict) - - # Assert - assert list(table_dict.keys()) == ["schema", "data", "options"] - assert isinstance(table_dict["options"], dict) - # assert hasattr(table.table_options, "__dataclass_fields__") - - def test_table_pydantic(df: DataFrame) -> None: # Prepare - table_options = TableOptionsPydantic(selectable_rows=3) + table_options = TabulatorOptions(selectable_rows=3) # Act - table = Tabulator(df, table_options=table_options) + table = Tabulator(df, options=table_options) table_dict = table.to_dict() print(table_dict) - # assert isinstance(table.table_options, BaseModel) - # print("pydantic", type(table.table_options)) - assert list(table_dict.keys()) == ["schema", "data", "options"] + # Assert + assert list(table_dict.keys()) == ["schema", "data", "options", "bindingOptions"] assert isinstance(table_dict["options"], dict) diff --git a/tests/test_table_options.py b/tests/test_table_options.py index 8f390a9..4dc0ce8 100644 --- a/tests/test_table_options.py +++ b/tests/test_table_options.py @@ -1,9 +1,6 @@ import pytest -# from pytabulator import TableOptions -from pytabulator._table_options_dc import TableOptionsDC -from pytabulator._table_options_dc import TableOptionsDC as TableOptionsDC -from pytabulator._table_options_pydantic import TableOptionsPydantic as TableOptions +from pytabulator.tabulator_options import TabulatorOptions @pytest.fixture @@ -18,17 +15,11 @@ def some_table_options(): def test_table_options(some_table_options): # Prepare - table_options_pydantic = TableOptions(**some_table_options) - print("pydantic", table_options_pydantic) - - table_options_dc = TableOptionsDC(**some_table_options) - print("dc", table_options_dc) + table_options = TabulatorOptions(**some_table_options) # Act - table_options_pydantic_dict = table_options_pydantic.to_dict() - table_options_dc_dict = table_options_dc.to_dict() + table_options_dict = table_options.to_dict() # Assert - assert list(table_options_pydantic_dict.items()).sort( - key=lambda item: item[0] - ) == list(table_options_dc_dict.items()).sort(key=lambda item: item[0]) + print(table_options_dict) + assert table_options_dict["movableRows"] == False From 29a409fcebeef38de336b491c2b94d60f315d430 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 16:42:34 +0200 Subject: [PATCH 11/16] Update init imports --- pytabulator/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytabulator/__init__.py b/pytabulator/__init__.py index 4b66192..233257d 100644 --- a/pytabulator/__init__.py +++ b/pytabulator/__init__.py @@ -1,3 +1,4 @@ +""" from importlib.metadata import PackageNotFoundError, version try: @@ -12,7 +13,8 @@ from ._table_options_dc import TableOptionsDC as TableOptions # print("dataclass") - +""" +from .tabulator_options import TabulatorOptions as TableOptions # from ._table_options_pydantic import TableOptionsPydantic as TableOptions from .shiny_bindings import output_tabulator, render_data_frame, render_tabulator from .tabulator import Tabulator From 8e54aede1aea1b74d2ce2d957afeac6b54e6e0b7 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 16:48:39 +0200 Subject: [PATCH 12/16] Fix import --- pytabulator/tabulator.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index 2d73edf..d584593 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -1,14 +1,20 @@ from __future__ import annotations +from typing import Any + from pandas import DataFrame -from ._utils import df_to_dict, as_camel_dict_recursive -from typing import Self, Any +try: + from typing_extensions import Self +except ImportError: + from typing import Self -from .tabulator_options import TabulatorOptions -from .utils import create_columns +from ._utils import as_camel_dict_recursive, df_to_dict from .editors import Editor from .formatters import Formatter +from .tabulator_options import TabulatorOptions +from .utils import create_columns + class Tabulator(object): """Tabulator From e826953bba4dd3ae7a0ee0355bdd6197a16edf4b Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 17:05:20 +0200 Subject: [PATCH 13/16] Add formatters --- .../getting_started/shiny_express_all.py | 4 +- .../shiny_express_all_new_style.py | 143 ++++++++++++++++++ pytabulator/formatters.py | 10 ++ 3 files changed, 155 insertions(+), 2 deletions(-) create mode 100644 docs/examples/getting_started/shiny_express_all_new_style.py diff --git a/docs/examples/getting_started/shiny_express_all.py b/docs/examples/getting_started/shiny_express_all.py index 74409b7..ad96325 100644 --- a/docs/examples/getting_started/shiny_express_all.py +++ b/docs/examples/getting_started/shiny_express_all.py @@ -77,7 +77,7 @@ def selected_rows(): @render_tabulator def tabulator(): - return Tabulator(df, table_options).options( + return Tabulator(df, table_options).set_options( editTriggerEvent="dblclick" ) # .options(selectableRows=True) @@ -136,4 +136,4 @@ async def trigger_get_data(): @reactive.Effect @reactive.event(input.tabulator_data) def tabulator_data(): - print(input.tabulator_data()[0]) + print(input.tabulator_data()) diff --git a/docs/examples/getting_started/shiny_express_all_new_style.py b/docs/examples/getting_started/shiny_express_all_new_style.py new file mode 100644 index 0000000..99e3a43 --- /dev/null +++ b/docs/examples/getting_started/shiny_express_all_new_style.py @@ -0,0 +1,143 @@ +from random import randrange + +import pandas as pd +from pytabulator import TableOptions, Tabulator, TabulatorContext, render_tabulator +from pytabulator.utils import create_columns +from pytabulator.formatters import ProgressFormatter, TickCrossFormatter +from shiny import reactive, render +from shiny.express import input, ui + +# Fetch data +# +df = pd.read_csv( + "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv" +)[["PassengerId", "Name", "Pclass", "Sex", "Age", "Fare", "Survived"]] + +# Setup +# +table_options = TableOptions( + columns=create_columns( + df, + default_filter=True, + default_editor=True, + updates={ + "Pclass": { + "formatter": "star", + "formatterParams": {"stars": 3}, + "hozAlign": "center", + }, + # "Survived": {"formatter": "tickCross"}, + # "Fare": {"formatter": "progress", "hozAlign": "left"}, + }, + ), + height=413, + pagination=True, + pagination_add_row="table", + layout="fitColumns", + index="PassengerId", + add_row_pos="top", + selectable_rows=True, + history=True, +) + +# Shiny Express App +# +with ui.div(style="padding-top: 0px;"): + ui.input_action_button("trigger_download", "Download") + ui.input_action_button("add_row", "Add row") + ui.input_action_button("delete_selected_rows", "Delete selected rows") + ui.input_action_button("undo", "Undo") + ui.input_action_button("redo", "Redo") + ui.input_action_button("trigger_get_data", "Submit data") + +ui.div( + ui.input_text("name", "Click on 'Add row' to add the Person to the table."), + style="padding-top: 20px;", +) +ui.div("Click on a row to print the name of the person.", style="padding: 10px;"), + + +@render.code +async def txt(): + print(input.tabulator_row_clicked()) + return input.tabulator_row_clicked()["Name"] + + +ui.div( + "Select multiple rows to print the names of the selected persons.", + style="padding: 10px;", +), + + +@render.code +def selected_rows(): + data = input.tabulator_rows_selected() + output = [item["Name"] for item in data] + return "\n".join(output) + + +@render_tabulator +def tabulator(): + return ( + Tabulator(df, table_options) + .set_options(editTriggerEvent="dblclick") + .set_column_formatter("Fare", ProgressFormatter(), hoz_align="left") + .set_column_formatter("Survived", TickCrossFormatter(), hoz_align="center") + ) + + +@reactive.Effect +@reactive.event(input.trigger_download) +async def trigger_download(): + print("download triggered") + async with TabulatorContext("tabulator") as table: + table.trigger_download("csv") + + +@reactive.Effect +@reactive.event(input.add_row) +async def add_row(): + async with TabulatorContext("tabulator") as table: + table.add_row( + { + "Name": input.name() or "Hans", + "Age": randrange(55), + "Survived": randrange(2), + "PassengerId": randrange(10000, 20000, 1), + "SibSp": randrange(9), + } + ) + + +@reactive.Effect +@reactive.event(input.delete_selected_rows) +async def delete_selected_rows(): + async with TabulatorContext("tabulator") as table: + table.delete_selected_rows() + + +@reactive.Effect +@reactive.event(input.undo) +async def undo(): + async with TabulatorContext("tabulator") as table: + table.undo() + + +@reactive.Effect +@reactive.event(input.redo) +async def redo(): + async with TabulatorContext("tabulator") as table: + table.redo() + + +@reactive.Effect +@reactive.event(input.trigger_get_data) +async def trigger_get_data(): + async with TabulatorContext("tabulator") as table: + table.trigger_get_data() + + +@reactive.Effect +@reactive.event(input.tabulator_data) +def tabulator_data(): + print(input.tabulator_data()) diff --git a/pytabulator/formatters.py b/pytabulator/formatters.py index dc70041..29500b6 100644 --- a/pytabulator/formatters.py +++ b/pytabulator/formatters.py @@ -22,3 +22,13 @@ class StarFormatter(Formatter): @property def name(self) -> str: return Formatters.STAR.value + +class ProgressFormatter(Formatter): + @property + def name(self) -> str: + return Formatters.PROGRESS.value + +class TickCrossFormatter(Formatter): + @property + def name(self) -> str: + return Formatters.TICK_CROSS.value From 6398735d8166573034ab7028fc10491350a9bb14 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Tue, 24 Sep 2024 19:49:33 +0200 Subject: [PATCH 14/16] Add editors --- .../shiny_express_all_new_style.py | 2 + pytabulator/editors.py | 65 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/docs/examples/getting_started/shiny_express_all_new_style.py b/docs/examples/getting_started/shiny_express_all_new_style.py index 99e3a43..97500ad 100644 --- a/docs/examples/getting_started/shiny_express_all_new_style.py +++ b/docs/examples/getting_started/shiny_express_all_new_style.py @@ -4,6 +4,7 @@ from pytabulator import TableOptions, Tabulator, TabulatorContext, render_tabulator from pytabulator.utils import create_columns from pytabulator.formatters import ProgressFormatter, TickCrossFormatter +from pytabulator.editors import ListEditor from shiny import reactive, render from shiny.express import input, ui @@ -83,6 +84,7 @@ def tabulator(): .set_options(editTriggerEvent="dblclick") .set_column_formatter("Fare", ProgressFormatter(), hoz_align="left") .set_column_formatter("Survived", TickCrossFormatter(), hoz_align="center") + .set_column_editor("Sex", ListEditor()) ) diff --git a/pytabulator/editors.py b/pytabulator/editors.py index 50b71e3..47ab6c8 100644 --- a/pytabulator/editors.py +++ b/pytabulator/editors.py @@ -1,16 +1,20 @@ from pydantic import BaseModel -from typing import Optional +from typing import Optional, Literal from ._utils import as_camel_dict_recursive from enum import Enum class Editors(Enum): - NUMBER = "number" INPUT = "input" + TEXTAREA = "textarea" + NUMBER = "number" + RANGE = "range" + TICK_CROSS = "tickCross" STAR = "star" PROGRESS = "progress" + LIST = "list" class Editor(BaseModel): @@ -22,21 +26,69 @@ def to_dict(self) -> dict: return as_camel_dict_recursive(self.model_dump(exclude_none=True)) +class InputEditor(Editor): + search: Optional[bool] = None + mask: Optional[str] = None + select_contents: Optional[bool] = None + element_attributes: Optional[dict] = None + + @property + def name(self) -> str: + return Editors.INPUT.value + + +class TextareaEditor(Editor): + mask: Optional[str] = None + select_contents: Optional[bool] = None + vertical_navigation: Literal["hybrid", "editor", "table"] = None + shift_enter_submit: Optional[bool] = None + + @property + def name(self) -> str: + return Editors.TEXTAREA.value + + class NumberEditor(Editor): min: Optional[float] = None max: Optional[float] = None step: Optional[float] = None + element_attributes: Optional[dict] = None + mask: Optional[str] = None + select_contents: Optional[bool] = None + vertical_navigation: Literal["editor", "table"] = None @property def name(self) -> str: return Editors.NUMBER.value +class RangeEditor(Editor): + min: Optional[float] = None + max: Optional[float] = None + step: Optional[float] = None + element_attributes: Optional[dict] = None + + @property + def name(self) -> str: + return Editors.RANGE.value + + +class TickCrossEditor(Editor): + true_value: Optional[str] = None + false_value: Optional[str] = None + element_attributes: Optional[dict] = None + + @property + def name(self) -> str: + return Editors.TICK_CROSS.value + + class StarEditor(Editor): @property def name(self) -> str: return Editors.STAR.value + class ProgressEditor(Editor): min: Optional[float] = None max: Optional[float] = None @@ -45,3 +97,12 @@ class ProgressEditor(Editor): @property def name(self) -> str: return Editors.PROGRESS.value + + +class ListEditor(Editor): + values: Optional[list] = None + values_lookup: Optional[bool] = True + + @property + def name(self) -> str: + return Editors.LIST.value From b69f9a519ef55047c35d5a6a307273b9efca8bfb Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Wed, 25 Sep 2024 08:31:53 +0200 Subject: [PATCH 15/16] Refactor --- .../shiny_express_all_new_style.py | 4 +- pytabulator/_abstracts.py | 8 +++ pytabulator/editors.py | 63 +++++++------------ 3 files changed, 33 insertions(+), 42 deletions(-) create mode 100644 pytabulator/_abstracts.py diff --git a/docs/examples/getting_started/shiny_express_all_new_style.py b/docs/examples/getting_started/shiny_express_all_new_style.py index 97500ad..bd4332d 100644 --- a/docs/examples/getting_started/shiny_express_all_new_style.py +++ b/docs/examples/getting_started/shiny_express_all_new_style.py @@ -4,7 +4,7 @@ from pytabulator import TableOptions, Tabulator, TabulatorContext, render_tabulator from pytabulator.utils import create_columns from pytabulator.formatters import ProgressFormatter, TickCrossFormatter -from pytabulator.editors import ListEditor +from pytabulator.editors import ListEditor, InputEditor, ProgressEditor from shiny import reactive, render from shiny.express import input, ui @@ -85,6 +85,8 @@ def tabulator(): .set_column_formatter("Fare", ProgressFormatter(), hoz_align="left") .set_column_formatter("Survived", TickCrossFormatter(), hoz_align="center") .set_column_editor("Sex", ListEditor()) + .set_column_editor("Name", InputEditor()) + .set_column_editor("Fare", ProgressEditor(), hoz_align="left") ) diff --git a/pytabulator/_abstracts.py b/pytabulator/_abstracts.py new file mode 100644 index 0000000..8ef7454 --- /dev/null +++ b/pytabulator/_abstracts.py @@ -0,0 +1,8 @@ +from pydantic import BaseModel + +from ._utils import as_camel_dict_recursive + + +class MyBaseModel(BaseModel): + def to_dict(self) -> dict: + return as_camel_dict_recursive(self.model_dump(exclude_none=True)) diff --git a/pytabulator/editors.py b/pytabulator/editors.py index 47ab6c8..593b10a 100644 --- a/pytabulator/editors.py +++ b/pytabulator/editors.py @@ -1,9 +1,7 @@ -from pydantic import BaseModel - -from typing import Optional, Literal - -from ._utils import as_camel_dict_recursive from enum import Enum +from typing import Literal, Optional + +from ._abstracts import MyBaseModel class Editors(Enum): @@ -17,38 +15,35 @@ class Editors(Enum): LIST = "list" -class Editor(BaseModel): +class Editor(MyBaseModel): + _name: str = "" + @property def name(self) -> str: - return "" - - def to_dict(self) -> dict: - return as_camel_dict_recursive(self.model_dump(exclude_none=True)) + return self._name class InputEditor(Editor): + _name: str = Editors.INPUT.value + search: Optional[bool] = None mask: Optional[str] = None select_contents: Optional[bool] = None element_attributes: Optional[dict] = None - @property - def name(self) -> str: - return Editors.INPUT.value - class TextareaEditor(Editor): + _name: str = Editors.TEXTAREA.value + mask: Optional[str] = None select_contents: Optional[bool] = None vertical_navigation: Literal["hybrid", "editor", "table"] = None shift_enter_submit: Optional[bool] = None - @property - def name(self) -> str: - return Editors.TEXTAREA.value - class NumberEditor(Editor): + _name: str = Editors.NUMBER.value + min: Optional[float] = None max: Optional[float] = None step: Optional[float] = None @@ -57,52 +52,38 @@ class NumberEditor(Editor): select_contents: Optional[bool] = None vertical_navigation: Literal["editor", "table"] = None - @property - def name(self) -> str: - return Editors.NUMBER.value - class RangeEditor(Editor): + _name: str = Editors.RANGE.value + min: Optional[float] = None max: Optional[float] = None step: Optional[float] = None element_attributes: Optional[dict] = None - @property - def name(self) -> str: - return Editors.RANGE.value - class TickCrossEditor(Editor): + _name: str = Editors.TICK_CROSS.value + true_value: Optional[str] = None false_value: Optional[str] = None element_attributes: Optional[dict] = None - @property - def name(self) -> str: - return Editors.TICK_CROSS.value - class StarEditor(Editor): - @property - def name(self) -> str: - return Editors.STAR.value + _name: str = Editors.STAR.value class ProgressEditor(Editor): + _name: str = Editors.PROGRESS.value + min: Optional[float] = None max: Optional[float] = None element_attributes: Optional[dict] = None - @property - def name(self) -> str: - return Editors.PROGRESS.value - class ListEditor(Editor): + _name: str = Editors.LIST.value + values: Optional[list] = None values_lookup: Optional[bool] = True - - @property - def name(self) -> str: - return Editors.LIST.value From 4adcccb294c4e515d62ac8ef130b7d18774938f9 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Thu, 31 Oct 2024 08:08:30 +0100 Subject: [PATCH 16/16] Add generic method to update columns --- pytabulator/formatters.py | 9 ++++++++- pytabulator/tabulator.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/pytabulator/formatters.py b/pytabulator/formatters.py index 29500b6..eab2774 100644 --- a/pytabulator/formatters.py +++ b/pytabulator/formatters.py @@ -1,13 +1,17 @@ from enum import Enum -from pydantic import BaseModel from typing import Optional + +from pydantic import BaseModel + from ._utils import as_camel_dict_recursive + class Formatters(Enum): STAR = "star" PROGRESS = "progress" TICK_CROSS = "tickCross" + class Formatter(BaseModel): def to_dict(self) -> dict: return as_camel_dict_recursive(self.model_dump(exclude_none=True)) @@ -16,6 +20,7 @@ def to_dict(self) -> dict: def name(self) -> str: return "" + class StarFormatter(Formatter): stars: Optional[int] = None @@ -23,11 +28,13 @@ class StarFormatter(Formatter): def name(self) -> str: return Formatters.STAR.value + class ProgressFormatter(Formatter): @property def name(self) -> str: return Formatters.PROGRESS.value + class TickCrossFormatter(Formatter): @property def name(self) -> str: diff --git a/pytabulator/tabulator.py b/pytabulator/tabulator.py index d584593..83e3a8f 100644 --- a/pytabulator/tabulator.py +++ b/pytabulator/tabulator.py @@ -65,6 +65,17 @@ def update_column(self, col_name: str | list, **kwargs: Any) -> Self: return self + def update_column2(self, col_name, formatter: dict | Formatter = None, editor: dict | Editor = None, **kwargs: Any) -> Self: + if formatter is not None: + self.set_column_formatter(col_name, formatter) + + if editor is not None: + self.set_column_editor(col_name, editor) + + self._update_column(col_name, **kwargs) + + return self + def set_column_formatter( self, col_name: str | list,