bootstack 0.1.0a1__py3-none-any.whl
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.
- bootstack/__init__.py +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -0
- bootstack/assets/bootstack.ico +0 -0
- bootstack/assets/bootstack.png +0 -0
- bootstack/assets/elements/__init__.py +0 -0
- bootstack/assets/elements/badge-pill.png +0 -0
- bootstack/assets/elements/badge-square.png +0 -0
- bootstack/assets/elements/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
- bootstack/assets/elements/checkbox-checked.png +0 -0
- bootstack/assets/elements/checkbox-indeterminate.png +0 -0
- bootstack/assets/elements/checkbox-unchecked.png +0 -0
- bootstack/assets/elements/field.png +0 -0
- bootstack/assets/elements/input-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-default.png +0 -0
- bootstack/assets/elements/input-compact.png +0 -0
- bootstack/assets/elements/input-default.png +0 -0
- bootstack/assets/elements/list-item-separated.png +0 -0
- bootstack/assets/elements/list-item.png +0 -0
- bootstack/assets/elements/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -0
- bootstack/core/mixins/__init__.py +1 -0
- bootstack/core/mixins/ttk_state.py +35 -0
- bootstack/core/mixins/widget.py +132 -0
- bootstack/core/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -0
- bootstack/style/builders_tk/__init__.py +16 -0
- bootstack/style/builders_tk/defaults.py +229 -0
- bootstack/style/element.py +173 -0
- bootstack/style/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""In-memory data source implementation with filtering, sorting, and pagination.
|
|
2
|
+
|
|
3
|
+
Provides a pure-Python in-memory data manager that supports:
|
|
4
|
+
- Pagination with configurable page size
|
|
5
|
+
- SQL-like filtering with WHERE clause syntax
|
|
6
|
+
- Multi-column sorting with ASC/DESC
|
|
7
|
+
- Full CRUD operations (create, read, update, delete)
|
|
8
|
+
- Record selection tracking
|
|
9
|
+
- CSV export
|
|
10
|
+
- Inferred schema from data
|
|
11
|
+
|
|
12
|
+
The MemoryDataSource stores all data in memory and provides fast access for
|
|
13
|
+
small to medium datasets. For larger datasets or persistence requirements,
|
|
14
|
+
consider using SqliteDataSource instead.
|
|
15
|
+
|
|
16
|
+
Filtering Syntax:
|
|
17
|
+
Supports SQL-like WHERE conditions:
|
|
18
|
+
- Comparisons: =, !=, >, >=, <, <=
|
|
19
|
+
- String operations: CONTAINS, STARTSWITH, ENDSWITH
|
|
20
|
+
- Set operations: IN ('val1', 'val2')
|
|
21
|
+
- Pattern matching: LIKE with % and _ wildcards
|
|
22
|
+
- Logical operators: AND, OR
|
|
23
|
+
- Literals: 'string', "string", 123, 3.14, true, false, null
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```python
|
|
27
|
+
set_filter("status = 'active' AND age >= 18")
|
|
28
|
+
set_filter("name LIKE 'John%'")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Sorting Syntax:
|
|
32
|
+
Multi-column sorting with ASC/DESC:
|
|
33
|
+
set_sort("last_name ASC, age DESC")
|
|
34
|
+
set_sort("priority DESC, created_at ASC")
|
|
35
|
+
|
|
36
|
+
Data Format:
|
|
37
|
+
- Records must be dictionaries or will be auto-wrapped as {"text": str(value)}
|
|
38
|
+
- Each record automatically gets an 'id' field (integer, auto-generated if missing)
|
|
39
|
+
- Each record automatically gets a 'selected' field (0 or 1)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import csv
|
|
45
|
+
import re
|
|
46
|
+
from collections.abc import Sequence
|
|
47
|
+
from typing import Any, Dict, List, Optional, Union, Mapping, Iterable, Tuple
|
|
48
|
+
|
|
49
|
+
from bootstack.datasource.base import BaseDataSource
|
|
50
|
+
from bootstack.datasource.types import Primitive
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MemoryDataSource(BaseDataSource):
|
|
54
|
+
"""In-memory data manager with pagination, filtering, sorting, and CRUD operations.
|
|
55
|
+
|
|
56
|
+
Stores all records in memory as dictionaries with automatic ID generation and
|
|
57
|
+
selection tracking. Provides SQL-like filtering and sorting syntax for intuitive
|
|
58
|
+
data manipulation.
|
|
59
|
+
|
|
60
|
+
The datasource maintains an internal index for O(1) ID lookups and supports
|
|
61
|
+
dynamic schema inference from provided data.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
page_size: Number of records per page (default: 10)
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
page_size: Current page size setting
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
```python
|
|
71
|
+
ds = MemoryDataSource(page_size=20)
|
|
72
|
+
ds.set_data([
|
|
73
|
+
{"name": "Alice", "age": 30},
|
|
74
|
+
{"name": "Bob", "age": 25},
|
|
75
|
+
])
|
|
76
|
+
ds.set_filter("age >= 30")
|
|
77
|
+
page = ds.get_page(0)
|
|
78
|
+
```
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, page_size: int = 10):
|
|
82
|
+
"""Initialize the in-memory datasource with defaults.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
page_size: Number of records returned per page when paginating.
|
|
86
|
+
"""
|
|
87
|
+
super().__init__(page_size)
|
|
88
|
+
self._table = "records"
|
|
89
|
+
self._columns: List[str] = []
|
|
90
|
+
self._data: List[Dict[str, Any]] = []
|
|
91
|
+
self._id_index: Dict[Any, int] = {}
|
|
92
|
+
self._where_sql: str = ""
|
|
93
|
+
self._order_by_sql: str = ""
|
|
94
|
+
self._filter_predicate = None
|
|
95
|
+
self._sort_keys: List[Tuple[str, bool]] = []
|
|
96
|
+
|
|
97
|
+
def _rebuild_id_index(self) -> None:
|
|
98
|
+
"""Rebuild the ID-to-position index for fast lookups."""
|
|
99
|
+
self._id_index.clear()
|
|
100
|
+
for i, rec in enumerate(self._data):
|
|
101
|
+
self._id_index[rec.get("id")] = i
|
|
102
|
+
|
|
103
|
+
def _ensure_selected_column(self) -> None:
|
|
104
|
+
"""Ensure all records have a 'selected' field."""
|
|
105
|
+
if "selected" not in self._columns:
|
|
106
|
+
self._columns.append("selected")
|
|
107
|
+
for r in self._data:
|
|
108
|
+
r.setdefault("selected", 0)
|
|
109
|
+
|
|
110
|
+
def _ensure_id(self) -> None:
|
|
111
|
+
"""Ensure all records have unique integer IDs."""
|
|
112
|
+
used = set()
|
|
113
|
+
max_id = 0
|
|
114
|
+
for r in self._data:
|
|
115
|
+
if "id" in r and isinstance(r["id"], int):
|
|
116
|
+
used.add(r["id"])
|
|
117
|
+
max_id = max(max_id, r["id"])
|
|
118
|
+
for r in self._data:
|
|
119
|
+
if "id" not in r or not isinstance(r["id"], int) or r["id"] in used:
|
|
120
|
+
max_id += 1
|
|
121
|
+
r["id"] = max_id
|
|
122
|
+
used.add(max_id)
|
|
123
|
+
self._rebuild_id_index()
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _like_to_regex(pattern: str) -> re.Pattern:
|
|
127
|
+
"""Convert SQL LIKE pattern to regex (% -> .*, _ -> .)."""
|
|
128
|
+
esc = ""
|
|
129
|
+
for ch in pattern:
|
|
130
|
+
if ch in ".^$*+?{}[]\\|()":
|
|
131
|
+
esc += "\\" + ch
|
|
132
|
+
else:
|
|
133
|
+
esc += ch
|
|
134
|
+
esc = esc.replace("%", ".*").replace("_", ".")
|
|
135
|
+
return re.compile("^" + esc + "$", re.IGNORECASE)
|
|
136
|
+
|
|
137
|
+
def _parse_filter(self, where_sql: str):
|
|
138
|
+
"""Parse WHERE clause into a predicate function.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
where_sql: SQL WHERE clause
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Predicate function that evaluates rows, or None if no filter
|
|
145
|
+
"""
|
|
146
|
+
if not where_sql:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
tokens = re.split(r"\s+(AND|OR)\s+", where_sql, flags=re.IGNORECASE)
|
|
150
|
+
terms: List[Tuple[str, str, Any]] = []
|
|
151
|
+
ops_between: List[str] = []
|
|
152
|
+
|
|
153
|
+
def parse_term(t: str) -> Tuple[str, str, Any]:
|
|
154
|
+
m_in = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s+IN\s*\((.*)\)\s*$", t, flags=re.IGNORECASE)
|
|
155
|
+
if m_in:
|
|
156
|
+
col, inner = m_in.group(1), m_in.group(2)
|
|
157
|
+
parts = [p.strip() for p in inner.split(",") if p.strip()]
|
|
158
|
+
values = [self._coerce_literal(p) for p in parts]
|
|
159
|
+
return col, "IN", values
|
|
160
|
+
|
|
161
|
+
m_like = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s+LIKE\s+(.+)\s*$", t, flags=re.IGNORECASE)
|
|
162
|
+
if m_like:
|
|
163
|
+
col, val = m_like.group(1), self._coerce_literal(m_like.group(2))
|
|
164
|
+
return col, "LIKE", val
|
|
165
|
+
|
|
166
|
+
for op in ("CONTAINS", "STARTSWITH", "ENDSWITH"):
|
|
167
|
+
m = re.match(rf"^\s*([A-Za-z_][A-Za-z0-9_]*)\s+{op}\s+(.+)\s*$", t, flags=re.IGNORECASE)
|
|
168
|
+
if m:
|
|
169
|
+
col, val = m.group(1), self._coerce_literal(m.group(2))
|
|
170
|
+
return col, op.upper(), val
|
|
171
|
+
|
|
172
|
+
m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(=|!=|>=|>|<=|<)\s*(.+)\s*$", t)
|
|
173
|
+
if m:
|
|
174
|
+
col, op, val = m.group(1), m.group(2), self._coerce_literal(m.group(3))
|
|
175
|
+
return col, op, val
|
|
176
|
+
|
|
177
|
+
m = re.match(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*$", t)
|
|
178
|
+
if m:
|
|
179
|
+
col = m.group(1)
|
|
180
|
+
return col, "truthy", True
|
|
181
|
+
|
|
182
|
+
raise ValueError(f"Unrecognized filter term: {t!r}")
|
|
183
|
+
|
|
184
|
+
for i, tok in enumerate(tokens):
|
|
185
|
+
if i % 2 == 0:
|
|
186
|
+
if not tok.strip():
|
|
187
|
+
continue
|
|
188
|
+
terms.append(parse_term(tok))
|
|
189
|
+
else:
|
|
190
|
+
ops_between.append(tok.strip().upper())
|
|
191
|
+
|
|
192
|
+
if not terms:
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
prepared: List[Tuple[str, str, Any]] = []
|
|
196
|
+
for col, op, val in terms:
|
|
197
|
+
if op == "LIKE" and isinstance(val, str):
|
|
198
|
+
prepared.append((col, op, self._like_to_regex(val)))
|
|
199
|
+
else:
|
|
200
|
+
prepared.append((col, op, val))
|
|
201
|
+
|
|
202
|
+
def predicate(row: Mapping[str, Any]) -> bool:
|
|
203
|
+
def eval_term(col: str, op: str, val: Any) -> bool:
|
|
204
|
+
rv = row.get(col, None)
|
|
205
|
+
try:
|
|
206
|
+
if op == "=": return rv == val
|
|
207
|
+
if op == "!=": return rv != val
|
|
208
|
+
if op == ">": return (rv is not None) and (val is not None) and rv > val
|
|
209
|
+
if op == ">=": return (rv is not None) and (val is not None) and rv >= val
|
|
210
|
+
if op == "<": return (rv is not None) and (val is not None) and rv < val
|
|
211
|
+
if op == "<=": return (rv is not None) and (val is not None) and rv <= val
|
|
212
|
+
if op == "CONTAINS":
|
|
213
|
+
return (rv is not None) and (val is not None) and (str(val).lower() in str(rv).lower())
|
|
214
|
+
if op == "STARTSWITH":
|
|
215
|
+
return (rv is not None) and (val is not None) and str(rv).lower().startswith(str(val).lower())
|
|
216
|
+
if op == "ENDSWITH":
|
|
217
|
+
return (rv is not None) and (val is not None) and str(rv).lower().endswith(str(val).lower())
|
|
218
|
+
if op == "IN":
|
|
219
|
+
return rv in val
|
|
220
|
+
if op == "LIKE" and isinstance(val, re.Pattern):
|
|
221
|
+
return (rv is not None) and bool(val.match(str(rv)))
|
|
222
|
+
if op == "truthy":
|
|
223
|
+
return bool(rv)
|
|
224
|
+
except Exception:
|
|
225
|
+
return False
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
result = eval_term(*prepared[0])
|
|
229
|
+
for j, t in enumerate(prepared[1:], start=0):
|
|
230
|
+
op_between = ops_between[j] if j < len(ops_between) else "AND"
|
|
231
|
+
if op_between == "AND":
|
|
232
|
+
result = result and eval_term(*t)
|
|
233
|
+
else:
|
|
234
|
+
result = result or eval_term(*t)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
return predicate
|
|
238
|
+
|
|
239
|
+
def _parse_sort(self, order_by_sql: str) -> List[Tuple[str, bool]]:
|
|
240
|
+
"""Parse ORDER BY clause into list of (column, reverse_bool) tuples."""
|
|
241
|
+
if not order_by_sql:
|
|
242
|
+
return []
|
|
243
|
+
parts = [p.strip() for p in order_by_sql.split(",") if p.strip()]
|
|
244
|
+
out: List[Tuple[str, bool]] = []
|
|
245
|
+
for p in parts:
|
|
246
|
+
m = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)(?:\s+(ASC|DESC))?$", p, flags=re.IGNORECASE)
|
|
247
|
+
if not m:
|
|
248
|
+
continue
|
|
249
|
+
col = m.group(1)
|
|
250
|
+
dir_tok = (m.group(2) or "ASC").upper()
|
|
251
|
+
out.append((col, dir_tok == "DESC"))
|
|
252
|
+
return out
|
|
253
|
+
|
|
254
|
+
def _apply_filter_and_sort(self, rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
255
|
+
"""Apply current filter and sort to row collection."""
|
|
256
|
+
if self._filter_predicate:
|
|
257
|
+
rows = [r for r in rows if self._filter_predicate(r)]
|
|
258
|
+
else:
|
|
259
|
+
rows = list(rows)
|
|
260
|
+
|
|
261
|
+
if self._sort_keys:
|
|
262
|
+
def key_func(r: Dict[str, Any]):
|
|
263
|
+
key_parts = []
|
|
264
|
+
for col, reverse in self._sort_keys:
|
|
265
|
+
v = r.get(col)
|
|
266
|
+
key_parts.append((v is None, v))
|
|
267
|
+
return tuple(key_parts)
|
|
268
|
+
|
|
269
|
+
for col, rev in reversed(self._sort_keys):
|
|
270
|
+
rows.sort(key=lambda r, c=col: (r.get(c) is None, r.get(c)), reverse=rev)
|
|
271
|
+
|
|
272
|
+
return rows
|
|
273
|
+
|
|
274
|
+
def set_data(self, records: Union[Sequence[Primitive], Sequence[Dict[str, Any]]]) -> "MemoryDataSource":
|
|
275
|
+
"""Load records into datasource.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
records: Sequence of dicts or primitives (auto-wrapped as {"text": str(x)})
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Self for method chaining
|
|
282
|
+
"""
|
|
283
|
+
if not records:
|
|
284
|
+
self._data = []
|
|
285
|
+
self._columns = []
|
|
286
|
+
self._rebuild_id_index()
|
|
287
|
+
return self
|
|
288
|
+
|
|
289
|
+
if records and not self._is_mapping(records[0]):
|
|
290
|
+
records = [dict(text=str(x)) for x in records]
|
|
291
|
+
|
|
292
|
+
data: List[Dict[str, Any]] = []
|
|
293
|
+
for i, rec in enumerate(records):
|
|
294
|
+
r = dict(rec)
|
|
295
|
+
r.setdefault("id", i)
|
|
296
|
+
r.setdefault("selected", 0)
|
|
297
|
+
data.append(r)
|
|
298
|
+
|
|
299
|
+
self._data = data
|
|
300
|
+
self._columns = list(self._data[0].keys())
|
|
301
|
+
self._ensure_id()
|
|
302
|
+
self._ensure_selected_column()
|
|
303
|
+
return self
|
|
304
|
+
|
|
305
|
+
def set_filter(self, where_sql: str = ""):
|
|
306
|
+
"""Apply SQL-like WHERE filter to data."""
|
|
307
|
+
self._where_sql = where_sql or ""
|
|
308
|
+
self._filter_predicate = self._parse_filter(self._where_sql)
|
|
309
|
+
|
|
310
|
+
def set_sort(self, order_by_sql: str = ""):
|
|
311
|
+
"""Apply SQL-like ORDER BY sorting to data."""
|
|
312
|
+
self._order_by_sql = order_by_sql or ""
|
|
313
|
+
self._sort_keys = self._parse_sort(self._order_by_sql)
|
|
314
|
+
|
|
315
|
+
def _filtered_sorted_rows(self) -> List[Dict[str, Any]]:
|
|
316
|
+
"""Get all rows with current filter and sort applied."""
|
|
317
|
+
return self._apply_filter_and_sort(self._data)
|
|
318
|
+
|
|
319
|
+
def get_page(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
320
|
+
"""Get records for specified page."""
|
|
321
|
+
if page is not None:
|
|
322
|
+
self._page = max(0, int(page))
|
|
323
|
+
rows = self._filtered_sorted_rows()
|
|
324
|
+
start = self._page * self.page_size
|
|
325
|
+
end = start + self.page_size
|
|
326
|
+
return [dict(r) for r in rows[start:end]]
|
|
327
|
+
|
|
328
|
+
def next_page(self) -> List[Dict[str, Any]]:
|
|
329
|
+
"""Advance to next page and return its records."""
|
|
330
|
+
if self.has_next_page():
|
|
331
|
+
self._page += 1
|
|
332
|
+
return self.get_page()
|
|
333
|
+
|
|
334
|
+
def prev_page(self) -> List[Dict[str, Any]]:
|
|
335
|
+
"""Move to previous page and return its records."""
|
|
336
|
+
self._page = max(0, self._page - 1)
|
|
337
|
+
return self.get_page()
|
|
338
|
+
|
|
339
|
+
def has_next_page(self) -> bool:
|
|
340
|
+
"""Check if more pages exist after current page."""
|
|
341
|
+
return (self._page + 1) * self.page_size < self.total_count()
|
|
342
|
+
|
|
343
|
+
def total_count(self) -> int:
|
|
344
|
+
"""Get total number of records matching current filter."""
|
|
345
|
+
return len(self._filtered_sorted_rows())
|
|
346
|
+
|
|
347
|
+
def _generate_new_id(self) -> int:
|
|
348
|
+
"""Generate next available integer ID."""
|
|
349
|
+
if not self._data:
|
|
350
|
+
return 1
|
|
351
|
+
return max(int(r.get("id", 0)) for r in self._data) + 1
|
|
352
|
+
|
|
353
|
+
def create_record(self, record: Dict[str, Any]) -> int:
|
|
354
|
+
"""Create new record and return its ID."""
|
|
355
|
+
r = dict(record)
|
|
356
|
+
if "id" not in r:
|
|
357
|
+
r["id"] = self._generate_new_id()
|
|
358
|
+
if "selected" not in r:
|
|
359
|
+
r["selected"] = 0
|
|
360
|
+
self._data.append(r)
|
|
361
|
+
self._columns = list(set(self._columns) | set(r.keys()))
|
|
362
|
+
self._id_index[r["id"]] = len(self._data) - 1
|
|
363
|
+
return r["id"]
|
|
364
|
+
|
|
365
|
+
def read_record(self, record_id: Any) -> Optional[Dict[str, Any]]:
|
|
366
|
+
"""Retrieve single record by ID."""
|
|
367
|
+
idx = self._id_index.get(record_id)
|
|
368
|
+
if idx is None:
|
|
369
|
+
return None
|
|
370
|
+
return dict(self._data[idx])
|
|
371
|
+
|
|
372
|
+
def update_record(self, record_id: Any, updates: Dict[str, Any]) -> bool:
|
|
373
|
+
"""Update record fields by ID."""
|
|
374
|
+
if not updates:
|
|
375
|
+
return False
|
|
376
|
+
idx = self._id_index.get(record_id)
|
|
377
|
+
if idx is None:
|
|
378
|
+
return False
|
|
379
|
+
self._data[idx].update(updates)
|
|
380
|
+
self._columns = list(set(self._columns) | set(updates.keys()))
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
def delete_record(self, record_id: Any) -> bool:
|
|
384
|
+
"""Delete record by ID."""
|
|
385
|
+
idx = self._id_index.get(record_id)
|
|
386
|
+
if idx is None:
|
|
387
|
+
return False
|
|
388
|
+
self._data.pop(idx)
|
|
389
|
+
self._rebuild_id_index()
|
|
390
|
+
return True
|
|
391
|
+
|
|
392
|
+
def select_record(self, record_id: Any) -> bool:
|
|
393
|
+
"""Mark record as selected."""
|
|
394
|
+
return self._set_selected_flag(record_id, 1)
|
|
395
|
+
|
|
396
|
+
def unselect_record(self, record_id: Any) -> bool:
|
|
397
|
+
"""Mark record as unselected."""
|
|
398
|
+
return self._set_selected_flag(record_id, 0)
|
|
399
|
+
|
|
400
|
+
def select_all(self, current_page_only: bool = False) -> int:
|
|
401
|
+
"""Select all records (optionally only current page)."""
|
|
402
|
+
self._ensure_selected_column()
|
|
403
|
+
if current_page_only:
|
|
404
|
+
ids = [r["id"] for r in self.get_page()]
|
|
405
|
+
count = 0
|
|
406
|
+
idset = set(ids)
|
|
407
|
+
for r in self._data:
|
|
408
|
+
if r["id"] in idset and r.get("selected") != 1:
|
|
409
|
+
r["selected"] = 1
|
|
410
|
+
count += 1
|
|
411
|
+
return count
|
|
412
|
+
else:
|
|
413
|
+
count = 0
|
|
414
|
+
for r in self._data:
|
|
415
|
+
if r.get("selected") != 1:
|
|
416
|
+
r["selected"] = 1
|
|
417
|
+
count += 1
|
|
418
|
+
return count
|
|
419
|
+
|
|
420
|
+
def unselect_all(self, current_page_only: bool = False) -> int:
|
|
421
|
+
"""Unselect all records (optionally only current page)."""
|
|
422
|
+
self._ensure_selected_column()
|
|
423
|
+
if current_page_only:
|
|
424
|
+
ids = [r["id"] for r in self.get_page()]
|
|
425
|
+
count = 0
|
|
426
|
+
idset = set(ids)
|
|
427
|
+
for r in self._data:
|
|
428
|
+
if r["id"] in idset and r.get("selected") != 0:
|
|
429
|
+
r["selected"] = 0
|
|
430
|
+
count += 1
|
|
431
|
+
return count
|
|
432
|
+
else:
|
|
433
|
+
count = 0
|
|
434
|
+
for r in self._data:
|
|
435
|
+
if r.get("selected") != 0:
|
|
436
|
+
r["selected"] = 0
|
|
437
|
+
count += 1
|
|
438
|
+
return count
|
|
439
|
+
|
|
440
|
+
def _set_selected_flag(self, record_id: Any, flag: int) -> bool:
|
|
441
|
+
"""Set selection flag for record by ID."""
|
|
442
|
+
self._ensure_selected_column()
|
|
443
|
+
idx = self._id_index.get(record_id)
|
|
444
|
+
if idx is None:
|
|
445
|
+
return False
|
|
446
|
+
self._data[idx]["selected"] = 1 if flag else 0
|
|
447
|
+
return True
|
|
448
|
+
|
|
449
|
+
def get_selected(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
450
|
+
"""Get selected records, optionally paginated."""
|
|
451
|
+
self._ensure_selected_column()
|
|
452
|
+
rows = [r for r in self._data if r.get("selected") == 1]
|
|
453
|
+
rows = self._apply_filter_and_sort(rows)
|
|
454
|
+
if page is None:
|
|
455
|
+
return [dict(r) for r in rows]
|
|
456
|
+
start = max(0, int(page)) * self.page_size
|
|
457
|
+
end = start + self.page_size
|
|
458
|
+
return [dict(r) for r in rows[start:end]]
|
|
459
|
+
|
|
460
|
+
def selected_count(self) -> int:
|
|
461
|
+
"""Get total number of selected records."""
|
|
462
|
+
self._ensure_selected_column()
|
|
463
|
+
return sum(1 for r in self._data if r.get("selected") == 1)
|
|
464
|
+
|
|
465
|
+
def export_to_csv(self, filepath: str, include_all: bool = True) -> None:
|
|
466
|
+
"""Export records to CSV file."""
|
|
467
|
+
rows = self._data if include_all else [r for r in self._data if r.get("selected") == 1]
|
|
468
|
+
if not rows:
|
|
469
|
+
return
|
|
470
|
+
fieldnames = list(self._columns) if self._columns else list(rows[0].keys())
|
|
471
|
+
with open(filepath, mode="w", newline="", encoding="utf-8") as f:
|
|
472
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
473
|
+
writer.writeheader()
|
|
474
|
+
for r in rows:
|
|
475
|
+
writer.writerow({k: r.get(k) for k in fieldnames})
|
|
476
|
+
|
|
477
|
+
def get_page_from_index(self, start_index: int, count: int) -> List[Dict[str, Any]]:
|
|
478
|
+
"""Get records by start index and count (respects filter/sort)."""
|
|
479
|
+
rows = self._filtered_sorted_rows()
|
|
480
|
+
start = max(0, int(start_index))
|
|
481
|
+
end = start + max(0, int(count))
|
|
482
|
+
return [dict(r) for r in rows[start:end]]
|