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,607 @@
|
|
|
1
|
+
# bootstack DataSource Module
|
|
2
|
+
|
|
3
|
+
A flexible, extensible data management system providing unified interfaces for working with data from various sources (memory, databases, files) with built-in support for pagination, filtering, sorting, and CRUD operations.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Architecture Overview](#architecture-overview)
|
|
8
|
+
- [Built-in Implementations](#built-in-implementations)
|
|
9
|
+
- [Creating Custom DataSources](#creating-custom-datasources)
|
|
10
|
+
- [API Reference](#api-reference)
|
|
11
|
+
- [Examples](#examples)
|
|
12
|
+
|
|
13
|
+
## Architecture Overview
|
|
14
|
+
|
|
15
|
+
The datasource module is built on three key components:
|
|
16
|
+
|
|
17
|
+
### 1. DataSourceProtocol (types.py)
|
|
18
|
+
|
|
19
|
+
A Protocol (duck-typing interface) that defines the contract all datasources must follow. This enables type checking and IDE support without requiring inheritance.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from typing import Protocol
|
|
23
|
+
|
|
24
|
+
class DataSourceProtocol(Protocol):
|
|
25
|
+
page_size: int
|
|
26
|
+
def set_data(self, records): ...
|
|
27
|
+
def get_page(self, page=None): ...
|
|
28
|
+
# ... other methods
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. BaseDataSource (base.py)
|
|
32
|
+
|
|
33
|
+
An abstract base class that provides:
|
|
34
|
+
- **Abstract method definitions** - Enforces implementation of required methods
|
|
35
|
+
- **Shared utility methods** - Common functionality like type inference and literal parsing
|
|
36
|
+
- **Hook methods** - Extension points for customization (e.g., `_before_create`, `_after_update`)
|
|
37
|
+
|
|
38
|
+
This is the **recommended base class** for creating custom datasources.
|
|
39
|
+
|
|
40
|
+
### 3. Concrete Implementations
|
|
41
|
+
|
|
42
|
+
Built-in datasources that inherit from `BaseDataSource`:
|
|
43
|
+
- **MemoryDataSource** - Fast in-memory storage
|
|
44
|
+
- **SqliteDataSource** - Persistent SQLite storage
|
|
45
|
+
- **FileDataSource** - File-based storage (CSV, JSON, JSONL, TSV)
|
|
46
|
+
|
|
47
|
+
## Built-in Implementations
|
|
48
|
+
|
|
49
|
+
### MemoryDataSource
|
|
50
|
+
|
|
51
|
+
Best for small to medium datasets that fit comfortably in memory.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from bootstack.datasource import MemoryDataSource
|
|
55
|
+
|
|
56
|
+
ds = MemoryDataSource(page_size=20)
|
|
57
|
+
ds.set_data([
|
|
58
|
+
{"name": "Alice", "age": 30, "city": "NYC"},
|
|
59
|
+
{"name": "Bob", "age": 25, "city": "LA"},
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
ds.set_filter("age >= 25")
|
|
63
|
+
ds.set_sort("name ASC")
|
|
64
|
+
page = ds.get_page(0)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Features:**
|
|
68
|
+
- SQL-like filtering (WHERE syntax)
|
|
69
|
+
- Multi-column sorting (ORDER BY syntax)
|
|
70
|
+
- O(1) ID lookups via internal index
|
|
71
|
+
- Automatic ID generation
|
|
72
|
+
|
|
73
|
+
### SqliteDataSource
|
|
74
|
+
|
|
75
|
+
Best for large datasets requiring persistence or SQL capabilities.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from bootstack.datasource import SqliteDataSource
|
|
79
|
+
|
|
80
|
+
# Persistent database
|
|
81
|
+
ds = SqliteDataSource("mydata.db", page_size=50)
|
|
82
|
+
ds.set_data([...])
|
|
83
|
+
|
|
84
|
+
# In-memory database
|
|
85
|
+
ds = SqliteDataSource(":memory:", page_size=50)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Features:**
|
|
89
|
+
- SQL-native filtering and sorting
|
|
90
|
+
- Persistent storage across sessions
|
|
91
|
+
- Efficient for large datasets
|
|
92
|
+
- Automatic schema inference
|
|
93
|
+
|
|
94
|
+
### FileDataSource
|
|
95
|
+
|
|
96
|
+
Best for loading data from files with preprocessing needs.
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from bootstack.datasource import FileDataSource, FileSourceConfig
|
|
100
|
+
|
|
101
|
+
# Simple CSV loading
|
|
102
|
+
ds = FileDataSource("data.csv")
|
|
103
|
+
ds.load()
|
|
104
|
+
|
|
105
|
+
# With transformations
|
|
106
|
+
config = FileSourceConfig(
|
|
107
|
+
column_renames={'old_name': 'new_name'},
|
|
108
|
+
column_types={'age': int, 'salary': float},
|
|
109
|
+
row_filter=lambda r: r['status'] == 'active'
|
|
110
|
+
)
|
|
111
|
+
ds = FileDataSource("data.json", config=config)
|
|
112
|
+
ds.load()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Features:**
|
|
116
|
+
- Supports CSV, TSV, JSON, JSONL
|
|
117
|
+
- Multiple loading strategies (eager, lazy, chunked, hybrid)
|
|
118
|
+
- Transformation pipeline
|
|
119
|
+
- Progress callbacks for large files
|
|
120
|
+
|
|
121
|
+
## Creating Custom DataSources
|
|
122
|
+
|
|
123
|
+
### Method 1: Inherit from BaseDataSource (Recommended)
|
|
124
|
+
|
|
125
|
+
This is the easiest and most common approach. You get utility methods for free and only need to implement storage-specific logic.
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from bootstack.datasource import BaseDataSource
|
|
129
|
+
from typing import Any, Dict, List, Optional
|
|
130
|
+
|
|
131
|
+
class RedisDataSource(BaseDataSource):
|
|
132
|
+
"""Custom datasource backed by Redis."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, redis_client, page_size=10):
|
|
135
|
+
super().__init__(page_size)
|
|
136
|
+
self.redis = redis_client
|
|
137
|
+
self._key_prefix = "myapp:records:"
|
|
138
|
+
|
|
139
|
+
def set_data(self, records):
|
|
140
|
+
"""Store records in Redis."""
|
|
141
|
+
for record in records:
|
|
142
|
+
record_id = record.get('id', self._generate_id())
|
|
143
|
+
key = f"{self._key_prefix}{record_id}"
|
|
144
|
+
self.redis.set(key, json.dumps(record))
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
def get_page(self, page=None):
|
|
148
|
+
"""Retrieve a page of records."""
|
|
149
|
+
if page is not None:
|
|
150
|
+
self._page = page
|
|
151
|
+
|
|
152
|
+
# Get all keys and paginate
|
|
153
|
+
keys = self.redis.keys(f"{self._key_prefix}*")
|
|
154
|
+
start = self._page * self.page_size
|
|
155
|
+
end = start + self.page_size
|
|
156
|
+
|
|
157
|
+
records = []
|
|
158
|
+
for key in keys[start:end]:
|
|
159
|
+
data = self.redis.get(key)
|
|
160
|
+
records.append(json.loads(data))
|
|
161
|
+
|
|
162
|
+
return records
|
|
163
|
+
|
|
164
|
+
def create_record(self, record):
|
|
165
|
+
"""Create new record in Redis."""
|
|
166
|
+
record_id = record.get('id', self._generate_id())
|
|
167
|
+
record['id'] = record_id
|
|
168
|
+
|
|
169
|
+
# Use hook for logging/validation
|
|
170
|
+
record = self._before_create(record)
|
|
171
|
+
|
|
172
|
+
key = f"{self._key_prefix}{record_id}"
|
|
173
|
+
self.redis.set(key, json.dumps(record))
|
|
174
|
+
|
|
175
|
+
# Use hook for post-creation tasks
|
|
176
|
+
self._after_create(record_id, record)
|
|
177
|
+
|
|
178
|
+
return record_id
|
|
179
|
+
|
|
180
|
+
def read_record(self, record_id):
|
|
181
|
+
"""Read single record from Redis."""
|
|
182
|
+
key = f"{self._key_prefix}{record_id}"
|
|
183
|
+
data = self.redis.get(key)
|
|
184
|
+
return json.loads(data) if data else None
|
|
185
|
+
|
|
186
|
+
def update_record(self, record_id, updates):
|
|
187
|
+
"""Update record in Redis."""
|
|
188
|
+
record = self.read_record(record_id)
|
|
189
|
+
if not record:
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
updates = self._before_update(record_id, updates)
|
|
193
|
+
record.update(updates)
|
|
194
|
+
|
|
195
|
+
key = f"{self._key_prefix}{record_id}"
|
|
196
|
+
self.redis.set(key, json.dumps(record))
|
|
197
|
+
|
|
198
|
+
self._after_update(record_id, updates, True)
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
def delete_record(self, record_id):
|
|
202
|
+
"""Delete record from Redis."""
|
|
203
|
+
self._before_delete(record_id)
|
|
204
|
+
key = f"{self._key_prefix}{record_id}"
|
|
205
|
+
result = self.redis.delete(key) > 0
|
|
206
|
+
self._after_delete(record_id, result)
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
# Implement remaining abstract methods...
|
|
210
|
+
# (set_filter, set_sort, next_page, prev_page, has_next_page,
|
|
211
|
+
# total_count, selection methods, export_to_csv, etc.)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Method 2: Implement the Protocol (Advanced)
|
|
215
|
+
|
|
216
|
+
For maximum flexibility, implement the `DataSourceProtocol` without inheritance. This is useful when you need to integrate with existing classes or have complex inheritance requirements.
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from bootstack.datasource import DataSourceProtocol
|
|
220
|
+
from typing import List, Dict, Any, Optional
|
|
221
|
+
|
|
222
|
+
class APIDataSource:
|
|
223
|
+
"""Datasource that fetches data from a REST API."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, api_url: str, page_size: int = 10):
|
|
226
|
+
self.api_url = api_url
|
|
227
|
+
self.page_size = page_size
|
|
228
|
+
self._page = 0
|
|
229
|
+
self._cache = []
|
|
230
|
+
|
|
231
|
+
def set_data(self, records):
|
|
232
|
+
"""Cache data locally."""
|
|
233
|
+
self._cache = list(records)
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
def get_page(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
237
|
+
"""Fetch page from API or cache."""
|
|
238
|
+
if page is not None:
|
|
239
|
+
self._page = page
|
|
240
|
+
|
|
241
|
+
# Make API request with pagination
|
|
242
|
+
response = requests.get(
|
|
243
|
+
self.api_url,
|
|
244
|
+
params={'page': self._page, 'per_page': self.page_size}
|
|
245
|
+
)
|
|
246
|
+
return response.json()['data']
|
|
247
|
+
|
|
248
|
+
# Implement all other protocol methods...
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Using Utility Methods
|
|
252
|
+
|
|
253
|
+
The `BaseDataSource` provides several utility methods you can use:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
class MyDataSource(BaseDataSource):
|
|
257
|
+
def process_record(self, raw_data: str):
|
|
258
|
+
# Parse literals
|
|
259
|
+
age = self._coerce_literal("25") # Returns int 25
|
|
260
|
+
active = self._coerce_literal("true") # Returns bool True
|
|
261
|
+
name = self._coerce_literal("'Alice'") # Returns str "Alice"
|
|
262
|
+
|
|
263
|
+
# Infer SQL types
|
|
264
|
+
age_type = self._infer_type(25) # Returns "INTEGER"
|
|
265
|
+
name_type = self._infer_type("Alice") # Returns "TEXT"
|
|
266
|
+
|
|
267
|
+
# Validate records
|
|
268
|
+
record = {"name": name, "age": age}
|
|
269
|
+
self._validate_record(record) # Raises ValueError if not dict
|
|
270
|
+
|
|
271
|
+
return record
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Using Hook Methods
|
|
275
|
+
|
|
276
|
+
Hook methods let you add custom behavior without overriding core logic:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
class AuditedDataSource(BaseDataSource):
|
|
280
|
+
def __init__(self):
|
|
281
|
+
super().__init__()
|
|
282
|
+
self.audit_log = []
|
|
283
|
+
|
|
284
|
+
def _before_create(self, record):
|
|
285
|
+
"""Add timestamp before creating."""
|
|
286
|
+
record['created_at'] = datetime.now().isoformat()
|
|
287
|
+
return record
|
|
288
|
+
|
|
289
|
+
def _after_create(self, record_id, record):
|
|
290
|
+
"""Log creation to audit trail."""
|
|
291
|
+
self.audit_log.append({
|
|
292
|
+
'action': 'CREATE',
|
|
293
|
+
'record_id': record_id,
|
|
294
|
+
'timestamp': datetime.now()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
def _before_update(self, record_id, updates):
|
|
298
|
+
"""Add modified timestamp."""
|
|
299
|
+
updates['modified_at'] = datetime.now().isoformat()
|
|
300
|
+
return updates
|
|
301
|
+
|
|
302
|
+
def _after_update(self, record_id, updates, success):
|
|
303
|
+
"""Log update to audit trail."""
|
|
304
|
+
if success:
|
|
305
|
+
self.audit_log.append({
|
|
306
|
+
'action': 'UPDATE',
|
|
307
|
+
'record_id': record_id,
|
|
308
|
+
'changes': updates,
|
|
309
|
+
'timestamp': datetime.now()
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## API Reference
|
|
314
|
+
|
|
315
|
+
### Required Methods (All Implementations)
|
|
316
|
+
|
|
317
|
+
#### Data & View Configuration
|
|
318
|
+
- `set_data(records)` - Load data into the datasource
|
|
319
|
+
- `set_filter(where_sql)` - Apply SQL-like WHERE filter
|
|
320
|
+
- `set_sort(order_by_sql)` - Apply SQL-like ORDER BY sorting
|
|
321
|
+
|
|
322
|
+
#### Pagination
|
|
323
|
+
- `get_page(page=None)` - Get records for specified page
|
|
324
|
+
- `next_page()` - Move to next page
|
|
325
|
+
- `prev_page()` - Move to previous page
|
|
326
|
+
- `has_next_page()` - Check if next page exists
|
|
327
|
+
- `total_count()` - Get total record count
|
|
328
|
+
|
|
329
|
+
#### CRUD Operations
|
|
330
|
+
- `create_record(record)` - Create new record, returns ID
|
|
331
|
+
- `read_record(record_id)` - Read single record by ID
|
|
332
|
+
- `update_record(record_id, updates)` - Update record fields
|
|
333
|
+
- `delete_record(record_id)` - Delete record by ID
|
|
334
|
+
|
|
335
|
+
#### Selection Management
|
|
336
|
+
- `select_record(record_id)` - Mark record as selected
|
|
337
|
+
- `unselect_record(record_id)` - Unmark record
|
|
338
|
+
- `select_all(current_page_only=False)` - Select all/page records
|
|
339
|
+
- `unselect_all(current_page_only=False)` - Unselect all/page records
|
|
340
|
+
- `get_selected(page=None)` - Get selected records
|
|
341
|
+
- `selected_count()` - Count selected records
|
|
342
|
+
|
|
343
|
+
#### Export
|
|
344
|
+
- `export_to_csv(filepath, include_all=True)` - Export to CSV
|
|
345
|
+
|
|
346
|
+
#### Index-based Access
|
|
347
|
+
- `get_page_from_index(start_index, count)` - Get records by index range
|
|
348
|
+
|
|
349
|
+
### Utility Methods (Inherited from BaseDataSource)
|
|
350
|
+
|
|
351
|
+
- `_infer_type(value)` - Infer SQL type from Python value
|
|
352
|
+
- `_is_mapping(x)` - Check if value is dict-like
|
|
353
|
+
- `_coerce_literal(s)` - Parse string literal to Python value
|
|
354
|
+
- `_validate_record(record)` - Validate record is a dictionary
|
|
355
|
+
|
|
356
|
+
### Hook Methods (Inherited from BaseDataSource)
|
|
357
|
+
|
|
358
|
+
Override these to add custom behavior:
|
|
359
|
+
|
|
360
|
+
- `_before_create(record)` - Called before creating record
|
|
361
|
+
- `_after_create(record_id, record)` - Called after creating record
|
|
362
|
+
- `_before_update(record_id, updates)` - Called before updating
|
|
363
|
+
- `_after_update(record_id, updates, success)` - Called after updating
|
|
364
|
+
- `_before_delete(record_id)` - Called before deleting
|
|
365
|
+
- `_after_delete(record_id, success)` - Called after deleting
|
|
366
|
+
|
|
367
|
+
## Examples
|
|
368
|
+
|
|
369
|
+
### Example 1: MongoDB DataSource
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from bootstack.datasource import BaseDataSource
|
|
373
|
+
from pymongo import MongoClient
|
|
374
|
+
|
|
375
|
+
class MongoDataSource(BaseDataSource):
|
|
376
|
+
def __init__(self, connection_string, database, collection, page_size=10):
|
|
377
|
+
super().__init__(page_size)
|
|
378
|
+
self.client = MongoClient(connection_string)
|
|
379
|
+
self.db = self.client[database]
|
|
380
|
+
self.collection = self.db[collection]
|
|
381
|
+
self._filter = {}
|
|
382
|
+
self._sort = []
|
|
383
|
+
|
|
384
|
+
def set_data(self, records):
|
|
385
|
+
"""Insert records into MongoDB."""
|
|
386
|
+
if records:
|
|
387
|
+
self.collection.insert_many(list(records))
|
|
388
|
+
return self
|
|
389
|
+
|
|
390
|
+
def set_filter(self, where_sql=""):
|
|
391
|
+
"""Convert SQL-like filter to MongoDB query."""
|
|
392
|
+
# Simple implementation - in production, use a proper parser
|
|
393
|
+
if "age >= 30" in where_sql:
|
|
394
|
+
self._filter = {"age": {"$gte": 30}}
|
|
395
|
+
else:
|
|
396
|
+
self._filter = {}
|
|
397
|
+
|
|
398
|
+
def set_sort(self, order_by_sql=""):
|
|
399
|
+
"""Convert SQL ORDER BY to MongoDB sort."""
|
|
400
|
+
if "name ASC" in order_by_sql:
|
|
401
|
+
self._sort = [("name", 1)]
|
|
402
|
+
elif "age DESC" in order_by_sql:
|
|
403
|
+
self._sort = [("age", -1)]
|
|
404
|
+
else:
|
|
405
|
+
self._sort = []
|
|
406
|
+
|
|
407
|
+
def get_page(self, page=None):
|
|
408
|
+
if page is not None:
|
|
409
|
+
self._page = page
|
|
410
|
+
|
|
411
|
+
skip = self._page * self.page_size
|
|
412
|
+
cursor = self.collection.find(self._filter).sort(self._sort).skip(skip).limit(self.page_size)
|
|
413
|
+
return list(cursor)
|
|
414
|
+
|
|
415
|
+
def create_record(self, record):
|
|
416
|
+
result = self.collection.insert_one(record)
|
|
417
|
+
return result.inserted_id
|
|
418
|
+
|
|
419
|
+
def read_record(self, record_id):
|
|
420
|
+
return self.collection.find_one({"_id": record_id})
|
|
421
|
+
|
|
422
|
+
def update_record(self, record_id, updates):
|
|
423
|
+
result = self.collection.update_one({"_id": record_id}, {"$set": updates})
|
|
424
|
+
return result.modified_count > 0
|
|
425
|
+
|
|
426
|
+
def delete_record(self, record_id):
|
|
427
|
+
result = self.collection.delete_one({"_id": record_id})
|
|
428
|
+
return result.deleted_count > 0
|
|
429
|
+
|
|
430
|
+
# ... implement remaining methods
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Example 2: Cached API DataSource
|
|
434
|
+
|
|
435
|
+
```python
|
|
436
|
+
from bootstack.datasource import BaseDataSource
|
|
437
|
+
import requests
|
|
438
|
+
from functools import lru_cache
|
|
439
|
+
from datetime import datetime, timedelta
|
|
440
|
+
|
|
441
|
+
class CachedAPIDataSource(BaseDataSource):
|
|
442
|
+
def __init__(self, api_url, cache_ttl=300, page_size=10):
|
|
443
|
+
super().__init__(page_size)
|
|
444
|
+
self.api_url = api_url
|
|
445
|
+
self.cache_ttl = cache_ttl
|
|
446
|
+
self._cache = {}
|
|
447
|
+
self._cache_time = {}
|
|
448
|
+
|
|
449
|
+
def _is_cache_valid(self, key):
|
|
450
|
+
"""Check if cached data is still valid."""
|
|
451
|
+
if key not in self._cache_time:
|
|
452
|
+
return False
|
|
453
|
+
age = datetime.now() - self._cache_time[key]
|
|
454
|
+
return age < timedelta(seconds=self.cache_ttl)
|
|
455
|
+
|
|
456
|
+
def get_page(self, page=None):
|
|
457
|
+
if page is not None:
|
|
458
|
+
self._page = page
|
|
459
|
+
|
|
460
|
+
cache_key = f"page_{self._page}"
|
|
461
|
+
|
|
462
|
+
# Return cached data if valid
|
|
463
|
+
if self._is_cache_valid(cache_key):
|
|
464
|
+
return self._cache[cache_key]
|
|
465
|
+
|
|
466
|
+
# Fetch from API
|
|
467
|
+
response = requests.get(
|
|
468
|
+
f"{self.api_url}/records",
|
|
469
|
+
params={'page': self._page, 'per_page': self.page_size}
|
|
470
|
+
)
|
|
471
|
+
data = response.json()
|
|
472
|
+
|
|
473
|
+
# Cache the result
|
|
474
|
+
self._cache[cache_key] = data
|
|
475
|
+
self._cache_time[cache_key] = datetime.now()
|
|
476
|
+
|
|
477
|
+
return data
|
|
478
|
+
|
|
479
|
+
def create_record(self, record):
|
|
480
|
+
# Invalidate cache on write
|
|
481
|
+
self._cache.clear()
|
|
482
|
+
self._cache_time.clear()
|
|
483
|
+
|
|
484
|
+
response = requests.post(f"{self.api_url}/records", json=record)
|
|
485
|
+
return response.json()['id']
|
|
486
|
+
|
|
487
|
+
# ... implement remaining methods
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Example 3: Multi-Source DataSource
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from bootstack.datasource import BaseDataSource
|
|
494
|
+
|
|
495
|
+
class MultiSourceDataSource(BaseDataSource):
|
|
496
|
+
"""Aggregates data from multiple datasources."""
|
|
497
|
+
|
|
498
|
+
def __init__(self, sources, page_size=10):
|
|
499
|
+
super().__init__(page_size)
|
|
500
|
+
self.sources = sources # List of datasources
|
|
501
|
+
self._aggregated_data = []
|
|
502
|
+
|
|
503
|
+
def set_data(self, records):
|
|
504
|
+
"""Not applicable for multi-source."""
|
|
505
|
+
raise NotImplementedError("Use add_source() instead")
|
|
506
|
+
|
|
507
|
+
def add_source(self, datasource):
|
|
508
|
+
"""Add a datasource to aggregate."""
|
|
509
|
+
self.sources.append(datasource)
|
|
510
|
+
|
|
511
|
+
def _aggregate_data(self):
|
|
512
|
+
"""Combine data from all sources."""
|
|
513
|
+
all_records = []
|
|
514
|
+
for source in self.sources:
|
|
515
|
+
source_records = source.get_page_from_index(0, source.total_count())
|
|
516
|
+
all_records.extend(source_records)
|
|
517
|
+
return all_records
|
|
518
|
+
|
|
519
|
+
def get_page(self, page=None):
|
|
520
|
+
if page is not None:
|
|
521
|
+
self._page = page
|
|
522
|
+
|
|
523
|
+
all_data = self._aggregate_data()
|
|
524
|
+
start = self._page * self.page_size
|
|
525
|
+
end = start + self.page_size
|
|
526
|
+
return all_data[start:end]
|
|
527
|
+
|
|
528
|
+
# ... implement remaining methods
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
## Best Practices
|
|
532
|
+
|
|
533
|
+
1. **Always call `super().__init__(page_size)`** in your `__init__` method
|
|
534
|
+
2. **Use hook methods** for cross-cutting concerns (logging, validation, caching)
|
|
535
|
+
3. **Inherit utility methods** instead of reimplementing them
|
|
536
|
+
4. **Handle errors gracefully** - return False/None for failures rather than raising
|
|
537
|
+
5. **Document your datasource** - explain what backend it uses and any special requirements
|
|
538
|
+
6. **Test thoroughly** - ensure all protocol methods work correctly
|
|
539
|
+
7. **Consider performance** - implement efficient filtering and pagination for large datasets
|
|
540
|
+
|
|
541
|
+
## Testing Your DataSource
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
def test_custom_datasource():
|
|
545
|
+
ds = MyCustomDataSource()
|
|
546
|
+
|
|
547
|
+
# Test basic operations
|
|
548
|
+
ds.set_data([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])
|
|
549
|
+
assert ds.total_count() == 2
|
|
550
|
+
|
|
551
|
+
# Test CRUD
|
|
552
|
+
new_id = ds.create_record({"name": "Charlie"})
|
|
553
|
+
assert ds.read_record(new_id) is not None
|
|
554
|
+
|
|
555
|
+
# Test pagination
|
|
556
|
+
page = ds.get_page(0)
|
|
557
|
+
assert isinstance(page, list)
|
|
558
|
+
|
|
559
|
+
# Test filtering
|
|
560
|
+
ds.set_filter("name = 'Alice'")
|
|
561
|
+
filtered = ds.get_page(0)
|
|
562
|
+
assert len(filtered) == 1
|
|
563
|
+
|
|
564
|
+
# Test it's an instance of BaseDataSource
|
|
565
|
+
assert isinstance(ds, BaseDataSource)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Migration Guide
|
|
569
|
+
|
|
570
|
+
If you have an existing datasource implementation, here's how to migrate to use `BaseDataSource`:
|
|
571
|
+
|
|
572
|
+
### Before (Standalone Class)
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
class MyDataSource:
|
|
576
|
+
def __init__(self, page_size=10):
|
|
577
|
+
self.page_size = page_size
|
|
578
|
+
self._page = 0
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def _infer_type(value):
|
|
582
|
+
# Your implementation
|
|
583
|
+
pass
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### After (Inheriting BaseDataSource)
|
|
587
|
+
|
|
588
|
+
```python
|
|
589
|
+
from bootstack.datasource import BaseDataSource
|
|
590
|
+
|
|
591
|
+
class MyDataSource(BaseDataSource):
|
|
592
|
+
def __init__(self, page_size=10):
|
|
593
|
+
super().__init__(page_size) # Initialize base class
|
|
594
|
+
# BaseDataSource now provides _page and utility methods
|
|
595
|
+
|
|
596
|
+
# Remove _infer_type - inherited from BaseDataSource
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
## Support
|
|
600
|
+
|
|
601
|
+
For questions, issues, or feature requests, please visit:
|
|
602
|
+
- GitHub Issues: https://github.com/israel-dryer/bootstack/issues
|
|
603
|
+
- Documentation: https://bootstack.org/
|
|
604
|
+
|
|
605
|
+
## License
|
|
606
|
+
|
|
607
|
+
This module is part of bootstack and follows the same license terms.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Data source abstraction for bootstack widgets.
|
|
2
|
+
|
|
3
|
+
Provides unified interface for data management with multiple backend implementations:
|
|
4
|
+
- MemoryDataSource: Fast in-memory storage for small to medium datasets
|
|
5
|
+
- SqliteDataSource: Persistent SQLite storage for large datasets
|
|
6
|
+
- FileDataSource: File-based storage with support for CSV, JSON, and various formats
|
|
7
|
+
|
|
8
|
+
All datasources support:
|
|
9
|
+
- Pagination with configurable page size
|
|
10
|
+
- SQL-like filtering and sorting
|
|
11
|
+
- Full CRUD operations (create, read, update, delete)
|
|
12
|
+
- Record selection tracking
|
|
13
|
+
- CSV export
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from bootstack.datasource import MemoryDataSource, SqliteDataSource, FileDataSource
|
|
17
|
+
|
|
18
|
+
# In-memory datasource
|
|
19
|
+
ds = MemoryDataSource(page_size=20)
|
|
20
|
+
ds.set_data([{"name": "Alice", "age": 30}, ...])
|
|
21
|
+
|
|
22
|
+
# SQLite datasource (persistent)
|
|
23
|
+
db = SqliteDataSource("mydata.db", page_size=50)
|
|
24
|
+
db.set_data([{"name": "Bob", "age": 25}, ...])
|
|
25
|
+
|
|
26
|
+
# File datasource (CSV, JSON, etc.)
|
|
27
|
+
file_ds = FileDataSource("data.csv", page_size=25)
|
|
28
|
+
file_ds.load()
|
|
29
|
+
|
|
30
|
+
# Common operations (work with all)
|
|
31
|
+
ds.set_filter("age >= 25")
|
|
32
|
+
ds.set_sort("name ASC")
|
|
33
|
+
page1 = ds.get_page(0)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from bootstack.datasource.base import BaseDataSource
|
|
37
|
+
from bootstack.datasource.memory_source import MemoryDataSource
|
|
38
|
+
from bootstack.datasource.sqlite_source import SqliteDataSource
|
|
39
|
+
from bootstack.datasource.file_source import FileDataSource, FileSourceConfig
|
|
40
|
+
from bootstack.datasource.types import DataSourceProtocol, Record, Primitive
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
'BaseDataSource',
|
|
44
|
+
'MemoryDataSource',
|
|
45
|
+
'SqliteDataSource',
|
|
46
|
+
'FileDataSource',
|
|
47
|
+
'FileSourceConfig',
|
|
48
|
+
'DataSourceProtocol',
|
|
49
|
+
'Record',
|
|
50
|
+
'Primitive',
|
|
51
|
+
]
|