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,453 @@
|
|
|
1
|
+
"""SQLite-backed data source implementation with persistence, filtering, and pagination.
|
|
2
|
+
|
|
3
|
+
Provides a database-backed data manager that supports:
|
|
4
|
+
- Persistent storage using SQLite
|
|
5
|
+
- All features of MemoryDataSource (pagination, filtering, sorting, CRUD)
|
|
6
|
+
- Efficient handling of large datasets
|
|
7
|
+
- SQL-native filtering and sorting
|
|
8
|
+
- Automatic schema inference
|
|
9
|
+
- Data persistence across application restarts
|
|
10
|
+
|
|
11
|
+
The SqliteDataSource is ideal for:
|
|
12
|
+
- Large datasets that don't fit comfortably in memory
|
|
13
|
+
- Applications requiring data persistence
|
|
14
|
+
- Scenarios needing SQL query capabilities
|
|
15
|
+
- Multi-user or multi-session data sharing
|
|
16
|
+
|
|
17
|
+
For in-memory, lightweight scenarios, consider MemoryDataSource instead.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
```python
|
|
21
|
+
ds = SqliteDataSource("mydata.db", page_size=50)
|
|
22
|
+
ds.set_data([{"name": "Alice", "age": 30}])
|
|
23
|
+
ds.set_filter("age >= 25")
|
|
24
|
+
page = ds.get_page(0)
|
|
25
|
+
```
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import csv
|
|
31
|
+
import sqlite3
|
|
32
|
+
from typing import Any, Dict, List, Optional, Union, Sequence
|
|
33
|
+
|
|
34
|
+
from bootstack.datasource.base import BaseDataSource
|
|
35
|
+
from bootstack.datasource.types import Primitive, Record
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SqliteDataSource(BaseDataSource):
|
|
39
|
+
"""SQLite-backed data manager with pagination, filtering, sorting, and CRUD operations.
|
|
40
|
+
|
|
41
|
+
Provides persistent storage using SQLite database with automatic schema inference
|
|
42
|
+
and SQL-native filtering/sorting. Supports all operations defined in DataSourceProtocol.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
name: Database file path or ":memory:" for in-memory database (default: ":memory:")
|
|
46
|
+
page_size: Number of records per page (default: 10)
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
conn: SQLite database connection
|
|
50
|
+
page_size: Current page size setting
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
```python
|
|
54
|
+
ds = SqliteDataSource("data.db", page_size=20)
|
|
55
|
+
ds.set_data([{"name": "Alice", "age": 30}])
|
|
56
|
+
ds.set_filter("age > 25")
|
|
57
|
+
page = ds.get_page(0)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Note:
|
|
61
|
+
- The database connection persists for the lifetime of the object
|
|
62
|
+
- Close the connection explicitly with conn.close() if needed
|
|
63
|
+
- Schema is inferred from first record's data types
|
|
64
|
+
- 'id' field is automatically set as PRIMARY KEY
|
|
65
|
+
- 'selected' field is added automatically for selection tracking
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, name: str = ":memory:", page_size: int = 10):
|
|
69
|
+
"""Create SQLite datasource and set initial pagination state.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Database file path or ':memory:' for an in-memory database.
|
|
73
|
+
page_size: Number of records returned per page during pagination.
|
|
74
|
+
"""
|
|
75
|
+
super().__init__(page_size)
|
|
76
|
+
self.conn = sqlite3.connect(name)
|
|
77
|
+
self.conn.row_factory = sqlite3.Row
|
|
78
|
+
self._table = "records"
|
|
79
|
+
self._where = ""
|
|
80
|
+
self._order_by = ""
|
|
81
|
+
self._columns = []
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _quote_identifier(name: str) -> str:
|
|
85
|
+
"""Safely quote an identifier (column/table) for SQLite."""
|
|
86
|
+
text = str(name).replace('"', '""')
|
|
87
|
+
return f'"{text}"'
|
|
88
|
+
|
|
89
|
+
def set_data(
|
|
90
|
+
self,
|
|
91
|
+
records: Union[Sequence[Primitive], Sequence[dict[str, Any]], Sequence[Sequence[Any]]],
|
|
92
|
+
column_keys: Optional[Sequence[str]] = None,
|
|
93
|
+
) -> "SqliteDataSource":
|
|
94
|
+
"""Load records into database, creating table with inferred schema.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
records: Sequence of dicts, primitives, or row sequences.
|
|
98
|
+
column_keys: Optional column names when supplying row sequences (lists/tuples).
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Self for method chaining
|
|
102
|
+
"""
|
|
103
|
+
if not records:
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
# Apply fast, in-memory friendly pragmas when using ":memory:" to speed up bulk loads
|
|
107
|
+
try:
|
|
108
|
+
if self.conn.execute("PRAGMA database_list").fetchone()[2] == ":memory:":
|
|
109
|
+
self.conn.execute("PRAGMA synchronous = OFF")
|
|
110
|
+
self.conn.execute("PRAGMA journal_mode = MEMORY")
|
|
111
|
+
self.conn.execute("PRAGMA temp_store = MEMORY")
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
# Normalize into a common structure: either dicts or row tuples with provided keys
|
|
116
|
+
first = records[0]
|
|
117
|
+
using_dicts = isinstance(first, dict)
|
|
118
|
+
# Turn primitives into dicts
|
|
119
|
+
if not using_dicts and not isinstance(first, (list, tuple)):
|
|
120
|
+
records = [dict(text=str(x)) for x in records]
|
|
121
|
+
using_dicts = True
|
|
122
|
+
|
|
123
|
+
if using_dicts:
|
|
124
|
+
# Ensure helper columns
|
|
125
|
+
for i, record in enumerate(records):
|
|
126
|
+
if "id" not in record:
|
|
127
|
+
record["id"] = i
|
|
128
|
+
if "selected" not in record:
|
|
129
|
+
record["selected"] = 0
|
|
130
|
+
|
|
131
|
+
self._columns = list(records[0].keys())
|
|
132
|
+
col_types = {col: self._infer_type(records[0][col]) for col in self._columns}
|
|
133
|
+
rows_to_insert = [tuple(row.get(col) for col in self._columns) for row in records]
|
|
134
|
+
else:
|
|
135
|
+
# Sequence rows with provided column keys
|
|
136
|
+
keys = list(column_keys or [])
|
|
137
|
+
if not keys:
|
|
138
|
+
keys = [str(i) for i in range(len(first))]
|
|
139
|
+
need_id = "id" not in keys
|
|
140
|
+
need_selected = "selected" not in keys
|
|
141
|
+
if need_id:
|
|
142
|
+
keys.append("id")
|
|
143
|
+
if need_selected:
|
|
144
|
+
keys.append("selected")
|
|
145
|
+
self._columns = keys
|
|
146
|
+
|
|
147
|
+
# Infer types from first row (pad to keys length)
|
|
148
|
+
padded_first = list(first) + [None] * (len(keys) - len(first))
|
|
149
|
+
if need_id and "id" in keys:
|
|
150
|
+
padded_first[keys.index("id")] = 0
|
|
151
|
+
if need_selected and "selected" in keys:
|
|
152
|
+
padded_first[keys.index("selected")] = 0
|
|
153
|
+
col_types = {col: self._infer_type(padded_first[idx]) for idx, col in enumerate(keys)}
|
|
154
|
+
|
|
155
|
+
rows_to_insert = []
|
|
156
|
+
id_idx = keys.index("id") if "id" in keys else None
|
|
157
|
+
sel_idx = keys.index("selected") if "selected" in keys else None
|
|
158
|
+
value_len = len(keys)
|
|
159
|
+
for i, row in enumerate(records):
|
|
160
|
+
base_values = list(row[: value_len]) + [""] * (value_len - len(row))
|
|
161
|
+
if id_idx is not None:
|
|
162
|
+
base_values[id_idx] = i
|
|
163
|
+
if sel_idx is not None:
|
|
164
|
+
base_values[sel_idx] = 0
|
|
165
|
+
rows_to_insert.append(tuple(base_values))
|
|
166
|
+
|
|
167
|
+
col_definitions = ", ".join(
|
|
168
|
+
f"{self._quote_identifier(col)} {col_types[col]}" + (" PRIMARY KEY" if col == "id" else "")
|
|
169
|
+
for col in self._columns
|
|
170
|
+
)
|
|
171
|
+
placeholders = ", ".join("?" for _ in self._columns)
|
|
172
|
+
|
|
173
|
+
# Recreate table and bulk insert in a single transaction for speed
|
|
174
|
+
original_row_factory = self.conn.row_factory
|
|
175
|
+
try:
|
|
176
|
+
self.conn.row_factory = None # avoid row wrapping overhead during inserts
|
|
177
|
+
with self.conn:
|
|
178
|
+
self.conn.execute(f"DROP TABLE IF EXISTS {self._table}")
|
|
179
|
+
self.conn.execute(f"CREATE TABLE {self._table} ({col_definitions})")
|
|
180
|
+
self.conn.executemany(f"INSERT INTO {self._table} VALUES ({placeholders})", rows_to_insert)
|
|
181
|
+
finally:
|
|
182
|
+
self.conn.row_factory = original_row_factory
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def set_filter(self, where_sql: str = ""):
|
|
186
|
+
"""Apply SQL WHERE clause filter.
|
|
187
|
+
|
|
188
|
+
The fragment is interpolated into the query unmodified, so the caller
|
|
189
|
+
is responsible for ensuring it is trusted/author-controlled. Do not
|
|
190
|
+
pass strings built from end-user input — use parameterized queries
|
|
191
|
+
directly via `self.conn` instead.
|
|
192
|
+
"""
|
|
193
|
+
self._where = where_sql
|
|
194
|
+
|
|
195
|
+
def set_sort(self, order_by_sql: str = ""):
|
|
196
|
+
"""Apply SQL ORDER BY clause for sorting.
|
|
197
|
+
|
|
198
|
+
Same trust contract as `set_filter`: the fragment is interpolated
|
|
199
|
+
verbatim, so it must be author-controlled, not user input.
|
|
200
|
+
"""
|
|
201
|
+
self._order_by = order_by_sql
|
|
202
|
+
|
|
203
|
+
def get_page(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
204
|
+
"""Get records for specified page."""
|
|
205
|
+
if page is not None:
|
|
206
|
+
self._page = page
|
|
207
|
+
offset = self._page * self.page_size
|
|
208
|
+
|
|
209
|
+
query = f"SELECT * FROM {self._table}"
|
|
210
|
+
if self._where:
|
|
211
|
+
query += f" WHERE {self._where}"
|
|
212
|
+
if self._order_by:
|
|
213
|
+
query += f" ORDER BY {self._order_by}"
|
|
214
|
+
query += f" LIMIT {self.page_size} OFFSET {offset}"
|
|
215
|
+
|
|
216
|
+
cursor = self.conn.execute(query)
|
|
217
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
218
|
+
|
|
219
|
+
def next_page(self) -> List[Dict[str, Any]]:
|
|
220
|
+
"""Advance to next page and return its records."""
|
|
221
|
+
self._page += 1
|
|
222
|
+
return self.get_page()
|
|
223
|
+
|
|
224
|
+
def prev_page(self) -> List[Dict[str, Any]]:
|
|
225
|
+
"""Move to previous page and return its records."""
|
|
226
|
+
self._page = max(0, self._page - 1)
|
|
227
|
+
return self.get_page()
|
|
228
|
+
|
|
229
|
+
def has_next_page(self) -> bool:
|
|
230
|
+
"""Check if more pages exist after current page."""
|
|
231
|
+
return (self._page + 1) * self.page_size < self.total_count()
|
|
232
|
+
|
|
233
|
+
def total_count(self) -> int:
|
|
234
|
+
"""Get total number of records matching current filter."""
|
|
235
|
+
query = f"SELECT COUNT(*) FROM {self._table}"
|
|
236
|
+
if self._where:
|
|
237
|
+
query += f" WHERE {self._where}"
|
|
238
|
+
return self.conn.execute(query).fetchone()[0]
|
|
239
|
+
|
|
240
|
+
# === CRUD OPERATIONS ===
|
|
241
|
+
|
|
242
|
+
def create_record(self, record: Dict[str, Any]) -> int:
|
|
243
|
+
"""Create new record and return its ID."""
|
|
244
|
+
if "id" not in record:
|
|
245
|
+
record["id"] = self._generate_new_id()
|
|
246
|
+
|
|
247
|
+
if "selected" not in record:
|
|
248
|
+
record["selected"] = 0
|
|
249
|
+
|
|
250
|
+
# Ensure table exists (handles empty datasources)
|
|
251
|
+
self._ensure_table_for_record(record)
|
|
252
|
+
|
|
253
|
+
keys = list(record.keys())
|
|
254
|
+
cols = ", ".join(self._quote_identifier(k) for k in keys)
|
|
255
|
+
placeholders = ", ".join("?" for _ in keys)
|
|
256
|
+
values = tuple(record[col] for col in keys)
|
|
257
|
+
|
|
258
|
+
with self.conn:
|
|
259
|
+
self.conn.execute(f"INSERT INTO {self._table} ({cols}) VALUES ({placeholders})", values)
|
|
260
|
+
return record["id"]
|
|
261
|
+
|
|
262
|
+
def read_record(self, record_id: Any) -> Optional[Dict[str, Any]]:
|
|
263
|
+
"""Retrieve single record by ID."""
|
|
264
|
+
cursor = self.conn.execute(f"SELECT * FROM {self._table} WHERE id = ?", (record_id,))
|
|
265
|
+
row = cursor.fetchone()
|
|
266
|
+
return dict(row) if row else None
|
|
267
|
+
|
|
268
|
+
def update_record(self, record_id: Any, updates: Dict[str, Any]) -> bool:
|
|
269
|
+
"""Update record fields by ID."""
|
|
270
|
+
if not updates:
|
|
271
|
+
return False
|
|
272
|
+
set_clause = ", ".join(f"{self._quote_identifier(k)} = ?" for k in updates)
|
|
273
|
+
values = tuple(updates.values()) + (record_id,)
|
|
274
|
+
with self.conn:
|
|
275
|
+
cur = self.conn.execute(f"UPDATE {self._table} SET {set_clause} WHERE id = ?", values)
|
|
276
|
+
return cur.rowcount > 0
|
|
277
|
+
|
|
278
|
+
def delete_record(self, record_id: Any) -> bool:
|
|
279
|
+
"""Delete record by ID."""
|
|
280
|
+
with self.conn:
|
|
281
|
+
cur = self.conn.execute(f"DELETE FROM {self._table} WHERE id = ?", (record_id,))
|
|
282
|
+
return cur.rowcount > 0
|
|
283
|
+
|
|
284
|
+
def _generate_new_id(self) -> int:
|
|
285
|
+
"""Generate next available integer ID."""
|
|
286
|
+
try:
|
|
287
|
+
cursor = self.conn.execute(f"SELECT MAX(id) FROM {self._table}")
|
|
288
|
+
max_id = cursor.fetchone()[0]
|
|
289
|
+
except Exception:
|
|
290
|
+
max_id = 0
|
|
291
|
+
return (max_id or 0) + 1
|
|
292
|
+
|
|
293
|
+
# ------------------------------------------------------------------ helpers
|
|
294
|
+
def _ensure_table_for_record(self, record: Dict[str, Any]) -> None:
|
|
295
|
+
"""Create the table if it does not yet exist, inferring columns from record."""
|
|
296
|
+
try:
|
|
297
|
+
# Quick existence check
|
|
298
|
+
self.conn.execute(f"SELECT 1 FROM {self._table} LIMIT 1")
|
|
299
|
+
return
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
|
|
303
|
+
cols = list(self._columns) if self._columns else list(record.keys())
|
|
304
|
+
if "id" not in cols:
|
|
305
|
+
cols.append("id")
|
|
306
|
+
if "selected" not in cols:
|
|
307
|
+
cols.append("selected")
|
|
308
|
+
self._columns = cols
|
|
309
|
+
|
|
310
|
+
col_types = {}
|
|
311
|
+
for c in cols:
|
|
312
|
+
if c == "id":
|
|
313
|
+
col_types[c] = "INTEGER"
|
|
314
|
+
elif c == "selected":
|
|
315
|
+
col_types[c] = "INTEGER"
|
|
316
|
+
else:
|
|
317
|
+
col_types[c] = self._infer_type(record.get(c))
|
|
318
|
+
|
|
319
|
+
col_definitions = ", ".join(
|
|
320
|
+
f"{self._quote_identifier(col)} {col_types[col]}" + (" PRIMARY KEY" if col == "id" else "")
|
|
321
|
+
for col in cols
|
|
322
|
+
)
|
|
323
|
+
with self.conn:
|
|
324
|
+
self.conn.execute(f"CREATE TABLE IF NOT EXISTS {self._table} ({col_definitions})")
|
|
325
|
+
|
|
326
|
+
# === SELECTION ====
|
|
327
|
+
|
|
328
|
+
def select_record(self, record_id: Any) -> bool:
|
|
329
|
+
"""Mark record as selected."""
|
|
330
|
+
return self._set_selected_flag(record_id, 1)
|
|
331
|
+
|
|
332
|
+
def unselect_record(self, record_id: Any) -> bool:
|
|
333
|
+
"""Mark record as unselected."""
|
|
334
|
+
return self._set_selected_flag(record_id, 0)
|
|
335
|
+
|
|
336
|
+
def select_all(self, current_page_only: bool = False) -> int:
|
|
337
|
+
"""Select all records (optionally only current page)."""
|
|
338
|
+
self._ensure_selected_column()
|
|
339
|
+
if current_page_only:
|
|
340
|
+
ids = [row["id"] for row in self.get_page()]
|
|
341
|
+
if not ids:
|
|
342
|
+
return 0
|
|
343
|
+
placeholders = ", ".join("?" for _ in ids)
|
|
344
|
+
query = f"UPDATE {self._table} SET selected = 1 WHERE id IN ({placeholders})"
|
|
345
|
+
with self.conn:
|
|
346
|
+
cur = self.conn.execute(query, ids)
|
|
347
|
+
return cur.rowcount
|
|
348
|
+
else:
|
|
349
|
+
with self.conn:
|
|
350
|
+
cur = self.conn.execute(f"UPDATE {self._table} SET selected = 1")
|
|
351
|
+
return cur.rowcount
|
|
352
|
+
|
|
353
|
+
def unselect_all(self, current_page_only: bool = False) -> int:
|
|
354
|
+
"""Unselect all records (optionally only current page)."""
|
|
355
|
+
self._ensure_selected_column()
|
|
356
|
+
if current_page_only:
|
|
357
|
+
ids = [row["id"] for row in self.get_page()]
|
|
358
|
+
if not ids:
|
|
359
|
+
return 0
|
|
360
|
+
placeholders = ", ".join("?" for _ in ids)
|
|
361
|
+
query = f"UPDATE {self._table} SET selected = 0 WHERE id IN ({placeholders})"
|
|
362
|
+
with self.conn:
|
|
363
|
+
cur = self.conn.execute(query, ids)
|
|
364
|
+
return cur.rowcount
|
|
365
|
+
else:
|
|
366
|
+
with self.conn:
|
|
367
|
+
cur = self.conn.execute(f"UPDATE {self._table} SET selected = 0")
|
|
368
|
+
return cur.rowcount
|
|
369
|
+
|
|
370
|
+
def get_selected(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
371
|
+
"""Get selected records, optionally paginated."""
|
|
372
|
+
self._ensure_selected_column()
|
|
373
|
+
query = f"SELECT * FROM {self._table} WHERE selected = 1"
|
|
374
|
+
|
|
375
|
+
if page is not None:
|
|
376
|
+
offset = page * self.page_size
|
|
377
|
+
query += f" LIMIT {self.page_size} OFFSET {offset}"
|
|
378
|
+
|
|
379
|
+
cursor = self.conn.execute(query)
|
|
380
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
381
|
+
|
|
382
|
+
def _ensure_selected_column(self):
|
|
383
|
+
"""Add 'selected' column to table if it doesn't exist."""
|
|
384
|
+
if "selected" not in self._columns:
|
|
385
|
+
with self.conn:
|
|
386
|
+
self.conn.execute(f"ALTER TABLE {self._table} ADD COLUMN selected INTEGER DEFAULT 0")
|
|
387
|
+
self._columns.append("selected")
|
|
388
|
+
|
|
389
|
+
def selected_count(self) -> int:
|
|
390
|
+
"""Get total number of selected records."""
|
|
391
|
+
self._ensure_selected_column()
|
|
392
|
+
query = f"SELECT COUNT(*) FROM {self._table} WHERE selected = 1"
|
|
393
|
+
return self.conn.execute(query).fetchone()[0]
|
|
394
|
+
|
|
395
|
+
def _set_selected_flag(self, record_id: Any, flag: int) -> bool:
|
|
396
|
+
"""Set selection flag for record by ID."""
|
|
397
|
+
if "selected" not in self._columns:
|
|
398
|
+
# Add selected column if it doesn't exist
|
|
399
|
+
with self.conn:
|
|
400
|
+
self.conn.execute(f"ALTER TABLE {self._table} ADD COLUMN selected INTEGER DEFAULT 0")
|
|
401
|
+
self._columns.append("selected")
|
|
402
|
+
|
|
403
|
+
with self.conn:
|
|
404
|
+
cur = self.conn.execute(f"UPDATE {self._table} SET selected = ? WHERE id = ?", (flag, record_id))
|
|
405
|
+
return cur.rowcount > 0
|
|
406
|
+
|
|
407
|
+
# === DATA EXPORT ===
|
|
408
|
+
|
|
409
|
+
def export_to_csv(self, filepath: str, include_all: bool = True):
|
|
410
|
+
"""Export records to CSV file."""
|
|
411
|
+
self._ensure_selected_column()
|
|
412
|
+
query = f"SELECT * FROM {self._table}"
|
|
413
|
+
if not include_all:
|
|
414
|
+
query += " WHERE selected = 1"
|
|
415
|
+
|
|
416
|
+
cursor = self.conn.execute(query)
|
|
417
|
+
rows = cursor.fetchall()
|
|
418
|
+
|
|
419
|
+
if not rows:
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
with open(filepath, mode='w', newline='', encoding='utf-8') as f:
|
|
423
|
+
writer = csv.DictWriter(f, fieldnames=rows[0].keys())
|
|
424
|
+
writer.writeheader()
|
|
425
|
+
for row in rows:
|
|
426
|
+
writer.writerow(dict(row))
|
|
427
|
+
|
|
428
|
+
def get_page_from_index(self, start_index: int, count: int) -> List[Dict[str, Any]]:
|
|
429
|
+
"""Get records by start index and count (respects filter/sort)."""
|
|
430
|
+
query = f"SELECT * FROM {self._table}"
|
|
431
|
+
if self._where:
|
|
432
|
+
query += f" WHERE {self._where}"
|
|
433
|
+
if self._order_by:
|
|
434
|
+
query += f" ORDER BY {self._order_by}"
|
|
435
|
+
query += f" LIMIT {count} OFFSET {start_index}"
|
|
436
|
+
cursor = self.conn.execute(query)
|
|
437
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
438
|
+
|
|
439
|
+
def get_distinct_values(self, column: str, limit: int = 1000) -> List[Any]:
|
|
440
|
+
"""Get distinct values for a column.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
column: Column name to get distinct values from.
|
|
444
|
+
limit: Maximum number of distinct values to return.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
List of distinct values sorted alphabetically.
|
|
448
|
+
"""
|
|
449
|
+
quoted_col = self._quote_identifier(column)
|
|
450
|
+
query = f"SELECT DISTINCT {quoted_col} FROM {self._table}"
|
|
451
|
+
query += f" ORDER BY {quoted_col} LIMIT {limit}"
|
|
452
|
+
cursor = self.conn.execute(query)
|
|
453
|
+
return [row[0] for row in cursor.fetchall()]
|