lhngrid 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
lhngrid-0.0.1/LICENSE ADDED
@@ -0,0 +1 @@
1
+ YOUR LICENSE HERE
@@ -0,0 +1,2 @@
1
+ recursive-include lhngrid/frontend/build *
2
+ include pyproject.toml
lhngrid-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.4
2
+ Name: lhngrid
3
+ Version: 0.0.1
4
+ Summary: A custom grid to handle tabular data
5
+ Author-email: Anu <anu@abc.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: streamlit>=1.51
11
+ Provides-Extra: snowpark
12
+ Requires-Dist: snowflake-snowpark-python>=1.5.0; extra == "snowpark"
13
+ Provides-Extra: devel
14
+ Requires-Dist: wheel; extra == "devel"
15
+ Requires-Dist: pytest==7.4.0; extra == "devel"
16
+ Requires-Dist: playwright==1.48.0; extra == "devel"
17
+ Requires-Dist: requests==2.31.0; extra == "devel"
18
+ Requires-Dist: pytest-playwright-snapshot==1.0; extra == "devel"
19
+ Requires-Dist: pytest-rerunfailures==12.0; extra == "devel"
20
+ Dynamic: license-file
21
+
22
+ # lhngrid
23
+
24
+ A custom grid to handle tabular data
25
+
26
+ ## Installation instructions
27
+
28
+ ```sh
29
+ uv pip install lhngrid
30
+ ```
31
+
32
+ ### Development install (editable)
33
+
34
+ When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`:
35
+
36
+ ```sh
37
+ uv pip install -e . --force-reinstall
38
+ ```
39
+
40
+ ## Usage instructions
41
+
42
+ ```python
43
+ import streamlit as st
44
+
45
+ from lhngrid import lhngrid
46
+
47
+ TABLE_KEY = "tasks"
48
+ PAGE_INDEX_KEY = f"{TABLE_KEY}_page_index"
49
+ PAGE_SIZE_KEY = f"{TABLE_KEY}_page_size"
50
+
51
+ st.session_state.setdefault(PAGE_INDEX_KEY, 0)
52
+ st.session_state.setdefault(PAGE_SIZE_KEY, 10)
53
+
54
+
55
+ def sync_grid_state(key: str) -> None:
56
+ component = st.session_state.get(key)
57
+
58
+ if component is None:
59
+ return
60
+
61
+ st.session_state[PAGE_INDEX_KEY] = component.pageIndex
62
+ st.session_state[PAGE_SIZE_KEY] = component.pageSize
63
+
64
+
65
+ value = lhngrid(
66
+ rows=rows,
67
+ columns=columns,
68
+ row_key="id",
69
+ page_index=st.session_state[PAGE_INDEX_KEY],
70
+ page_size=st.session_state[PAGE_SIZE_KEY],
71
+ theme="streamlit-dataframe",
72
+ show_row_selection=False,
73
+ pagination_position="top-left",
74
+ key=TABLE_KEY,
75
+ on_pageIndex_change=sync_grid_state,
76
+ on_pageSize_change=sync_grid_state,
77
+ )
78
+
79
+ st.write(value)
80
+ ```
81
+
82
+ ### Snowpark DataFrames
83
+
84
+ LHNGrid can also accept a Snowpark DataFrame through the same `rows`
85
+ parameter. Install the optional Snowpark extra when using this path:
86
+
87
+ ```sh
88
+ uv pip install "lhngrid[snowpark]"
89
+ ```
90
+
91
+ When `rows` is a Snowpark DataFrame, LHNGrid queries only the current page,
92
+ applies supported sorting/filtering in Snowpark, and sends page-sized JSON rows
93
+ to the frontend.
94
+
95
+ ```python
96
+ value = lhngrid(
97
+ rows=session.table("TASKS"),
98
+ columns=None, # infer columns from the Snowpark schema
99
+ row_key="ID",
100
+ page_index=st.session_state[PAGE_INDEX_KEY],
101
+ page_size=st.session_state[PAGE_SIZE_KEY],
102
+ sorting=st.session_state.get(SORTING_KEY, []),
103
+ key=TABLE_KEY,
104
+ on_pageIndex_change=sync_grid_state,
105
+ on_pageSize_change=sync_grid_state,
106
+ on_sorting_change=sync_grid_state,
107
+ )
108
+ ```
109
+
110
+ Explicit columns remain the source of truth when provided. Use `sourceKey` when
111
+ the Snowpark column name differs from the frontend row key.
112
+
113
+ ```python
114
+ columns = [
115
+ {
116
+ "id": "task",
117
+ "header": "Task",
118
+ "accessorKey": "task",
119
+ "sourceKey": "TASK",
120
+ "type": "text",
121
+ "sortable": True,
122
+ },
123
+ {
124
+ "id": "created_at",
125
+ "header": "Created At",
126
+ "accessorKey": "createdAt",
127
+ "sourceKey": "CREATED_AT",
128
+ "type": "date",
129
+ "sortable": True,
130
+ },
131
+ ]
132
+
133
+ value = lhngrid(
134
+ rows=session.table("TASKS"),
135
+ columns=columns,
136
+ row_key="ID",
137
+ page_index=st.session_state[PAGE_INDEX_KEY],
138
+ page_size=st.session_state[PAGE_SIZE_KEY],
139
+ sorting=st.session_state.get(SORTING_KEY, []),
140
+ key=TABLE_KEY,
141
+ )
142
+ ```
143
+
144
+ Callbacks receive the component key so the handler can read the current
145
+ component value from `st.session_state[key]`. Exposed callbacks are
146
+ `on_pageIndex_change`, `on_pageSize_change`, `on_sorting_change`,
147
+ `on_columnFilters_change`, `on_selectedRowId_change`,
148
+ `on_selectedRowIds_change`, `on_rowOffset_change`, `on_rowLimit_change`, and
149
+ `on_rowAction_change`. The legacy `onIndexChange` parameter still aliases
150
+ `on_pageIndex_change`.
151
+
152
+ ### Styling hooks
153
+
154
+ Available themes are `default` and `streamlit-dataframe`. The
155
+ `streamlit-dataframe` theme only changes styling so the grid visually matches
156
+ Streamlit's default DataFrame component; it does not add DataFrame features.
157
+ Set `show_row_selection=False` to hide the checkbox selection column.
158
+ Use `pagination_position` to place the pagination controls. Supported values are
159
+ `top-left`, `top-middle`, `top-right`, `bottom-left`, `bottom-middle`, and
160
+ `bottom-right`. The default is `bottom-right`.
161
+
162
+ The component uses readable, scoped class names so app-level CSS can target the
163
+ grid safely. Common hooks include `lhn-grid`, `lhn-grid-table-container`,
164
+ `lhn-grid-table`, `lhn-grid-header-cell`, `lhn-grid-cell`,
165
+ `lhn-grid-table-row`, `lhn-grid-table-row-selected`,
166
+ `lhn-grid-row-selection-checkbox`, `lhn-grid-badge`,
167
+ `lhn-grid-pagination`, `lhn-grid-page-size-select`, and
168
+ `lhn-grid-pagination-button`.
169
+
170
+ ## Build a wheel
171
+
172
+ To package this component for distribution:
173
+
174
+ 1. Build the frontend assets (from `lhngrid/frontend`):
175
+
176
+ ```sh
177
+ npm i
178
+ npm run build
179
+ ```
180
+
181
+ 2. Build the Python wheel using UV (from the project root):
182
+ ```sh
183
+ uv build
184
+ ```
185
+
186
+ This will create a `dist/` directory containing your wheel. The wheel includes the compiled frontend from `lhngrid/frontend/build`.
187
+
188
+ ### Requirements
189
+
190
+ - Python >= 3.10
191
+ - Node.js >= 24 (LTS)
192
+
193
+ ### Expected output
194
+
195
+ - `dist/lhngrid-0.0.1-py3-none-any.whl`
196
+ - If you run `uv run --with build python -m build` (without `--wheel`), you’ll also get an sdist: `dist/lhngrid-0.0.1.tar.gz`
@@ -0,0 +1,175 @@
1
+ # lhngrid
2
+
3
+ A custom grid to handle tabular data
4
+
5
+ ## Installation instructions
6
+
7
+ ```sh
8
+ uv pip install lhngrid
9
+ ```
10
+
11
+ ### Development install (editable)
12
+
13
+ When developing this component locally, install it in editable mode so Streamlit picks up code changes without rebuilding a wheel. Run this from the directory that contains `pyproject.toml`:
14
+
15
+ ```sh
16
+ uv pip install -e . --force-reinstall
17
+ ```
18
+
19
+ ## Usage instructions
20
+
21
+ ```python
22
+ import streamlit as st
23
+
24
+ from lhngrid import lhngrid
25
+
26
+ TABLE_KEY = "tasks"
27
+ PAGE_INDEX_KEY = f"{TABLE_KEY}_page_index"
28
+ PAGE_SIZE_KEY = f"{TABLE_KEY}_page_size"
29
+
30
+ st.session_state.setdefault(PAGE_INDEX_KEY, 0)
31
+ st.session_state.setdefault(PAGE_SIZE_KEY, 10)
32
+
33
+
34
+ def sync_grid_state(key: str) -> None:
35
+ component = st.session_state.get(key)
36
+
37
+ if component is None:
38
+ return
39
+
40
+ st.session_state[PAGE_INDEX_KEY] = component.pageIndex
41
+ st.session_state[PAGE_SIZE_KEY] = component.pageSize
42
+
43
+
44
+ value = lhngrid(
45
+ rows=rows,
46
+ columns=columns,
47
+ row_key="id",
48
+ page_index=st.session_state[PAGE_INDEX_KEY],
49
+ page_size=st.session_state[PAGE_SIZE_KEY],
50
+ theme="streamlit-dataframe",
51
+ show_row_selection=False,
52
+ pagination_position="top-left",
53
+ key=TABLE_KEY,
54
+ on_pageIndex_change=sync_grid_state,
55
+ on_pageSize_change=sync_grid_state,
56
+ )
57
+
58
+ st.write(value)
59
+ ```
60
+
61
+ ### Snowpark DataFrames
62
+
63
+ LHNGrid can also accept a Snowpark DataFrame through the same `rows`
64
+ parameter. Install the optional Snowpark extra when using this path:
65
+
66
+ ```sh
67
+ uv pip install "lhngrid[snowpark]"
68
+ ```
69
+
70
+ When `rows` is a Snowpark DataFrame, LHNGrid queries only the current page,
71
+ applies supported sorting/filtering in Snowpark, and sends page-sized JSON rows
72
+ to the frontend.
73
+
74
+ ```python
75
+ value = lhngrid(
76
+ rows=session.table("TASKS"),
77
+ columns=None, # infer columns from the Snowpark schema
78
+ row_key="ID",
79
+ page_index=st.session_state[PAGE_INDEX_KEY],
80
+ page_size=st.session_state[PAGE_SIZE_KEY],
81
+ sorting=st.session_state.get(SORTING_KEY, []),
82
+ key=TABLE_KEY,
83
+ on_pageIndex_change=sync_grid_state,
84
+ on_pageSize_change=sync_grid_state,
85
+ on_sorting_change=sync_grid_state,
86
+ )
87
+ ```
88
+
89
+ Explicit columns remain the source of truth when provided. Use `sourceKey` when
90
+ the Snowpark column name differs from the frontend row key.
91
+
92
+ ```python
93
+ columns = [
94
+ {
95
+ "id": "task",
96
+ "header": "Task",
97
+ "accessorKey": "task",
98
+ "sourceKey": "TASK",
99
+ "type": "text",
100
+ "sortable": True,
101
+ },
102
+ {
103
+ "id": "created_at",
104
+ "header": "Created At",
105
+ "accessorKey": "createdAt",
106
+ "sourceKey": "CREATED_AT",
107
+ "type": "date",
108
+ "sortable": True,
109
+ },
110
+ ]
111
+
112
+ value = lhngrid(
113
+ rows=session.table("TASKS"),
114
+ columns=columns,
115
+ row_key="ID",
116
+ page_index=st.session_state[PAGE_INDEX_KEY],
117
+ page_size=st.session_state[PAGE_SIZE_KEY],
118
+ sorting=st.session_state.get(SORTING_KEY, []),
119
+ key=TABLE_KEY,
120
+ )
121
+ ```
122
+
123
+ Callbacks receive the component key so the handler can read the current
124
+ component value from `st.session_state[key]`. Exposed callbacks are
125
+ `on_pageIndex_change`, `on_pageSize_change`, `on_sorting_change`,
126
+ `on_columnFilters_change`, `on_selectedRowId_change`,
127
+ `on_selectedRowIds_change`, `on_rowOffset_change`, `on_rowLimit_change`, and
128
+ `on_rowAction_change`. The legacy `onIndexChange` parameter still aliases
129
+ `on_pageIndex_change`.
130
+
131
+ ### Styling hooks
132
+
133
+ Available themes are `default` and `streamlit-dataframe`. The
134
+ `streamlit-dataframe` theme only changes styling so the grid visually matches
135
+ Streamlit's default DataFrame component; it does not add DataFrame features.
136
+ Set `show_row_selection=False` to hide the checkbox selection column.
137
+ Use `pagination_position` to place the pagination controls. Supported values are
138
+ `top-left`, `top-middle`, `top-right`, `bottom-left`, `bottom-middle`, and
139
+ `bottom-right`. The default is `bottom-right`.
140
+
141
+ The component uses readable, scoped class names so app-level CSS can target the
142
+ grid safely. Common hooks include `lhn-grid`, `lhn-grid-table-container`,
143
+ `lhn-grid-table`, `lhn-grid-header-cell`, `lhn-grid-cell`,
144
+ `lhn-grid-table-row`, `lhn-grid-table-row-selected`,
145
+ `lhn-grid-row-selection-checkbox`, `lhn-grid-badge`,
146
+ `lhn-grid-pagination`, `lhn-grid-page-size-select`, and
147
+ `lhn-grid-pagination-button`.
148
+
149
+ ## Build a wheel
150
+
151
+ To package this component for distribution:
152
+
153
+ 1. Build the frontend assets (from `lhngrid/frontend`):
154
+
155
+ ```sh
156
+ npm i
157
+ npm run build
158
+ ```
159
+
160
+ 2. Build the Python wheel using UV (from the project root):
161
+ ```sh
162
+ uv build
163
+ ```
164
+
165
+ This will create a `dist/` directory containing your wheel. The wheel includes the compiled frontend from `lhngrid/frontend/build`.
166
+
167
+ ### Requirements
168
+
169
+ - Python >= 3.10
170
+ - Node.js >= 24 (LTS)
171
+
172
+ ### Expected output
173
+
174
+ - `dist/lhngrid-0.0.1-py3-none-any.whl`
175
+ - If you run `uv run --with build python -m build` (without `--wheel`), you’ll also get an sdist: `dist/lhngrid-0.0.1.tar.gz`
@@ -0,0 +1,270 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from functools import partial
5
+ from typing import Any, Callable, Literal, cast
6
+
7
+ import streamlit as st
8
+
9
+ from ._pagination import normalize_pagination
10
+ from ._snowpark import is_snowpark_dataframe, prepare_snowpark_data
11
+
12
+ _component = st.components.v2.component(
13
+ "lhngrid.lhngrid",
14
+ js="index-*.js",
15
+ css="index-*.css",
16
+ html='<div class="react-root"></div>',
17
+ )
18
+
19
+
20
+ def _noop() -> None:
21
+ return None
22
+
23
+
24
+ GridCallback = Callable[[str], None]
25
+ GridTheme = Literal["default", "streamlit-dataframe"]
26
+ GridPaginationPosition = Literal[
27
+ "top-left",
28
+ "top-middle",
29
+ "top-right",
30
+ "bottom-left",
31
+ "bottom-middle",
32
+ "bottom-right",
33
+ ]
34
+ _GRID_THEMES: set[str] = {"default", "streamlit-dataframe"}
35
+ _GRID_PAGINATION_POSITIONS: set[str] = {
36
+ "top-left",
37
+ "top-middle",
38
+ "top-right",
39
+ "bottom-left",
40
+ "bottom-middle",
41
+ "bottom-right",
42
+ }
43
+
44
+
45
+ def _bind_callback(callback: GridCallback | None, key: str) -> Callable[[], None]:
46
+ if callback is None:
47
+ return _noop
48
+
49
+ return partial(callback, key)
50
+
51
+
52
+ def _normalize_size(value: int | float | str | None) -> str | None:
53
+ if value is None:
54
+ return None
55
+
56
+ if isinstance(value, bool):
57
+ raise TypeError("width and height must be a number, string, or None.")
58
+
59
+ if isinstance(value, (int, float)):
60
+ if value < 0:
61
+ raise ValueError("width and height must be non-negative.")
62
+
63
+ return f"{value}px"
64
+
65
+ if isinstance(value, str):
66
+ return value
67
+
68
+ raise TypeError("width and height must be a number, string, or None.")
69
+
70
+
71
+ def _normalize_theme(value: str) -> GridTheme:
72
+ if value not in _GRID_THEMES:
73
+ themes = ", ".join(sorted(_GRID_THEMES))
74
+ raise ValueError(f"theme must be one of: {themes}.")
75
+
76
+ return cast(GridTheme, value)
77
+
78
+
79
+ def _normalize_pagination_position(value: str) -> GridPaginationPosition:
80
+ if value not in _GRID_PAGINATION_POSITIONS:
81
+ positions = ", ".join(sorted(_GRID_PAGINATION_POSITIONS))
82
+ raise ValueError(f"pagination_position must be one of: {positions}.")
83
+
84
+ return cast(GridPaginationPosition, value)
85
+
86
+
87
+ def _paginate_rows(
88
+ rows: Sequence[dict[str, Any]],
89
+ page_index: int,
90
+ page_size: int,
91
+ total_rows: int | None,
92
+ ) -> tuple[list[dict[str, Any]], int, int, int, int, int]:
93
+ total_rows_value = len(rows) if total_rows is None else max(0, int(total_rows))
94
+ page_index_value, page_size_value, row_offset, row_limit = normalize_pagination(
95
+ page_index,
96
+ page_size,
97
+ total_rows_value,
98
+ )
99
+
100
+ if total_rows is not None and total_rows_value > len(rows):
101
+ return (
102
+ list(rows),
103
+ page_index_value,
104
+ page_size_value,
105
+ total_rows_value,
106
+ row_offset,
107
+ row_limit,
108
+ )
109
+
110
+ page_start = row_offset
111
+ page_end = page_start + row_limit
112
+
113
+ return (
114
+ list(rows[page_start:page_end]),
115
+ page_index_value,
116
+ page_size_value,
117
+ total_rows_value,
118
+ row_offset,
119
+ row_limit,
120
+ )
121
+
122
+
123
+ def _normalize_sequence_rows(rows: Any) -> Sequence[dict[str, Any]]:
124
+ if isinstance(rows, (str, bytes)) or not isinstance(rows, Sequence):
125
+ raise TypeError(
126
+ "rows must be a sequence of dictionaries or a Snowpark DataFrame."
127
+ )
128
+
129
+ return cast(Sequence[dict[str, Any]], rows)
130
+
131
+
132
+ def lhngrid(
133
+ *,
134
+ rows: Any,
135
+ columns: list[dict[str, Any]] | None = None,
136
+ row_key: str,
137
+ page_index: int = 0,
138
+ page_size: int = 10,
139
+ total_rows: int | None = None,
140
+ sorting: list[dict[str, Any]] | None = None,
141
+ column_filters: list[dict[str, Any]] | None = None,
142
+ loading: bool = False,
143
+ width: int | float | str | None = None,
144
+ height: int | float | str | None = None,
145
+ page_size_options: Sequence[int] = (5, 10, 25, 50, 100),
146
+ pagination_position: GridPaginationPosition = "bottom-right",
147
+ show_row_selection: bool = True,
148
+ show_actions: bool = False,
149
+ theme: GridTheme = "default",
150
+ key: str | None = None,
151
+ on_pageIndex_change: GridCallback | None = None,
152
+ on_pageSize_change: GridCallback | None = None,
153
+ on_sorting_change: GridCallback | None = None,
154
+ on_columnFilters_change: GridCallback | None = None,
155
+ on_selectedRowId_change: GridCallback | None = None,
156
+ on_selectedRowIds_change: GridCallback | None = None,
157
+ on_rowOffset_change: GridCallback | None = None,
158
+ on_rowLimit_change: GridCallback | None = None,
159
+ on_rowAction_change: GridCallback | None = None,
160
+ onIndexChange: GridCallback | None = None,
161
+ ) -> Any:
162
+ """Render an LHNGrid Streamlit component."""
163
+ if key is None:
164
+ raise ValueError("lhngrid requires a stable key.")
165
+
166
+ page_index_callback = on_pageIndex_change or onIndexChange
167
+ sorting_value = sorting or []
168
+ column_filters_value = column_filters or []
169
+ theme_value = _normalize_theme(theme)
170
+ pagination_position_value = _normalize_pagination_position(pagination_position)
171
+
172
+ if is_snowpark_dataframe(rows):
173
+ prepared_grid_data = prepare_snowpark_data(
174
+ dataframe=rows,
175
+ columns=columns,
176
+ row_key=row_key,
177
+ page_index=page_index,
178
+ page_size=page_size,
179
+ total_rows=total_rows,
180
+ sorting=sorting_value,
181
+ column_filters=column_filters_value,
182
+ )
183
+ paged_rows = prepared_grid_data.rows
184
+ component_columns = prepared_grid_data.columns
185
+ page_index_value = prepared_grid_data.page_index
186
+ page_size_value = prepared_grid_data.page_size
187
+ total_rows_value = prepared_grid_data.total_rows
188
+ row_offset = prepared_grid_data.row_offset
189
+ row_limit = prepared_grid_data.row_limit
190
+ else:
191
+ if columns is None:
192
+ raise ValueError(
193
+ "columns is required when rows is not a Snowpark DataFrame."
194
+ )
195
+
196
+ sequence_rows = _normalize_sequence_rows(rows)
197
+ component_columns = columns
198
+ (
199
+ paged_rows,
200
+ page_index_value,
201
+ page_size_value,
202
+ total_rows_value,
203
+ row_offset,
204
+ row_limit,
205
+ ) = _paginate_rows(sequence_rows, page_index, page_size, total_rows)
206
+
207
+ page_size_options_value = list(
208
+ dict.fromkeys(
209
+ max(1, int(option))
210
+ for option in (*page_size_options, page_size_value)
211
+ )
212
+ )
213
+
214
+ return _component(
215
+ key=key,
216
+ data={
217
+ "rows": paged_rows,
218
+ "columns": component_columns,
219
+ "rowKey": row_key,
220
+ "pageIndex": page_index_value,
221
+ "pageSize": page_size_value,
222
+ "totalRows": total_rows_value,
223
+ "sorting": sorting_value,
224
+ "columnFilters": column_filters_value,
225
+ "loading": loading,
226
+ "width": _normalize_size(width),
227
+ "height": _normalize_size(height),
228
+ "rowOffset": row_offset,
229
+ "rowLimit": row_limit,
230
+ "pageSizeOptions": page_size_options_value,
231
+ "paginationPosition": pagination_position_value,
232
+ "showRowSelection": show_row_selection,
233
+ "showActions": show_actions,
234
+ "theme": theme_value,
235
+ "enabledCallbacks": {
236
+ "pageIndex": page_index_callback is not None,
237
+ "pageSize": on_pageSize_change is not None,
238
+ "sorting": on_sorting_change is not None,
239
+ "columnFilters": on_columnFilters_change is not None,
240
+ "selectedRowId": on_selectedRowId_change is not None,
241
+ "selectedRowIds": on_selectedRowIds_change is not None,
242
+ "rowOffset": on_rowOffset_change is not None,
243
+ "rowLimit": on_rowLimit_change is not None,
244
+ "rowAction": show_actions or on_rowAction_change is not None,
245
+ },
246
+ },
247
+ default={
248
+ "pageIndex": page_index_value,
249
+ "pageSize": page_size_value,
250
+ "sorting": sorting_value,
251
+ "columnFilters": column_filters_value,
252
+ "selectedRowId": None,
253
+ "selectedRowIds": [],
254
+ "rowOffset": row_offset,
255
+ "rowLimit": row_limit,
256
+ "rowAction": None,
257
+ },
258
+ on_pageIndex_change=_bind_callback(page_index_callback, key),
259
+ on_pageSize_change=_bind_callback(on_pageSize_change, key),
260
+ on_sorting_change=_bind_callback(on_sorting_change, key),
261
+ on_columnFilters_change=_bind_callback(on_columnFilters_change, key),
262
+ on_selectedRowId_change=_bind_callback(on_selectedRowId_change, key),
263
+ on_selectedRowIds_change=_bind_callback(on_selectedRowIds_change, key),
264
+ on_rowOffset_change=_bind_callback(on_rowOffset_change, key),
265
+ on_rowLimit_change=_bind_callback(on_rowLimit_change, key),
266
+ on_rowAction_change=_bind_callback(on_rowAction_change, key),
267
+ )
268
+
269
+
270
+ __all__ = ["lhngrid"]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def normalize_pagination(
5
+ page_index: int,
6
+ page_size: int,
7
+ total_rows: int,
8
+ ) -> tuple[int, int, int, int]:
9
+ page_index_value = max(0, int(page_index))
10
+ page_size_value = max(1, int(page_size))
11
+ total_rows_value = max(0, int(total_rows))
12
+ max_page_index = max((total_rows_value - 1) // page_size_value, 0)
13
+ page_index_value = min(page_index_value, max_page_index)
14
+ row_offset = page_index_value * page_size_value
15
+ row_limit = page_size_value
16
+
17
+ return page_index_value, page_size_value, row_offset, row_limit