bootstack 0.1.0__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 +157 -0
- bootstack/__main__.py +5 -0
- bootstack/_core/__init__.py +21 -0
- bootstack/_core/capabilities/__init__.py +45 -0
- bootstack/_core/capabilities/after.py +103 -0
- bootstack/_core/capabilities/bind.py +192 -0
- bootstack/_core/capabilities/bindtags.py +112 -0
- bootstack/_core/capabilities/busy.py +72 -0
- bootstack/_core/capabilities/clipboard.py +89 -0
- bootstack/_core/capabilities/focus.py +118 -0
- bootstack/_core/capabilities/grab.py +65 -0
- bootstack/_core/capabilities/grid.py +211 -0
- bootstack/_core/capabilities/localization.py +231 -0
- bootstack/_core/capabilities/pack.py +140 -0
- bootstack/_core/capabilities/place.py +113 -0
- bootstack/_core/capabilities/selection.py +136 -0
- bootstack/_core/capabilities/signals.py +244 -0
- bootstack/_core/capabilities/winfo.py +315 -0
- bootstack/_core/colorutils.py +234 -0
- bootstack/_core/exceptions.py +25 -0
- bootstack/_core/images.py +463 -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/paths.py +49 -0
- bootstack/_core/publisher.py +149 -0
- bootstack/_core/variables.py +62 -0
- bootstack/_runtime/__init__.py +3 -0
- bootstack/_runtime/app.py +930 -0
- bootstack/_runtime/base_window.py +945 -0
- bootstack/_runtime/events.py +399 -0
- bootstack/_runtime/shortcuts.py +496 -0
- bootstack/_runtime/tk_patch.py +43 -0
- bootstack/_runtime/toplevel.py +276 -0
- bootstack/_runtime/utility.py +457 -0
- bootstack/_runtime/visual_focus.py +240 -0
- bootstack/_runtime/window_utilities.py +1123 -0
- bootstack/assets/__init__.py +21 -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/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
- bootstack/assets/elements/card.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-addon-compact.png +0 -0
- bootstack/assets/elements/input-addon-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/listrow-compact.png +0 -0
- bootstack/assets/elements/listrow-default.png +0 -0
- bootstack/assets/elements/manifest.toml +361 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/navitem-compact.png +0 -0
- bootstack/assets/elements/navitem-default.png +0 -0
- bootstack/assets/elements/progressbar-h-compact.png +0 -0
- bootstack/assets/elements/progressbar-h-default.png +0 -0
- bootstack/assets/elements/progressbar-v-compact.png +0 -0
- bootstack/assets/elements/progressbar-v-default.png +0 -0
- bootstack/assets/elements/radiobutton.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-h.png +0 -0
- bootstack/assets/elements/slider-track-v.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tab-h.png +0 -0
- bootstack/assets/elements/tab-v.png +0 -0
- bootstack/assets/icons/bootstrap.ttf +0 -0
- bootstack/assets/icons/glyphmap.json +2080 -0
- bootstack/assets/icons/icon_metrics.json +12470 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
- bootstack/cli/__init__.py +133 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +395 -0
- bootstack/cli/appicon.py +285 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +313 -0
- bootstack/cli/demo.py +564 -0
- bootstack/cli/dev.py +153 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/icons.py +98 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +268 -0
- bootstack/cli/run.py +95 -0
- bootstack/cli/start.py +117 -0
- bootstack/cli/templates/__init__.py +931 -0
- bootstack/clipboard.py +48 -0
- bootstack/constants.py +318 -0
- bootstack/data/README.md +615 -0
- bootstack/data/__init__.py +78 -0
- bootstack/data/_observable.py +276 -0
- bootstack/data/base.py +780 -0
- bootstack/data/file_source.py +367 -0
- bootstack/data/memory_source.py +450 -0
- bootstack/data/query.py +367 -0
- bootstack/data/readers.py +289 -0
- bootstack/data/sqlite_source.py +869 -0
- bootstack/data/types.py +354 -0
- bootstack/data/writers.py +232 -0
- bootstack/dev/__init__.py +36 -0
- bootstack/dev/_capture.py +141 -0
- bootstack/dev/_env.py +43 -0
- bootstack/dev/_registry.py +140 -0
- bootstack/dev/_reloader.py +351 -0
- bootstack/dev/_reset.py +50 -0
- bootstack/dev/_watcher.py +91 -0
- bootstack/dialogs/__init__.py +948 -0
- bootstack/dialogs/_impl/__init__.py +47 -0
- bootstack/dialogs/_impl/colorchooser.py +588 -0
- bootstack/dialogs/_impl/datedialog.py +450 -0
- bootstack/dialogs/_impl/dialog.py +594 -0
- bootstack/dialogs/_impl/filterdialog.py +358 -0
- bootstack/dialogs/_impl/fontdialog.py +364 -0
- bootstack/dialogs/_impl/formdialog.py +564 -0
- bootstack/dialogs/_impl/message.py +486 -0
- bootstack/dialogs/_impl/query.py +570 -0
- bootstack/errors.py +67 -0
- bootstack/events/__init__.py +111 -0
- bootstack/events/_event.py +135 -0
- bootstack/events/_payloads.py +539 -0
- bootstack/events/_subscription.py +38 -0
- bootstack/i18n/README.md +77 -0
- bootstack/i18n/__init__.py +23 -0
- bootstack/i18n/catalog.py +121 -0
- bootstack/i18n/intl_format.py +584 -0
- bootstack/i18n/msgcat.py +425 -0
- bootstack/i18n/specs.py +156 -0
- bootstack/images.py +563 -0
- bootstack/py.typed +1 -0
- bootstack/scheduling/__init__.py +11 -0
- bootstack/scheduling/_schedule.py +218 -0
- bootstack/shortcuts.py +21 -0
- bootstack/signals/README.md +98 -0
- bootstack/signals/__init__.py +10 -0
- bootstack/signals/integration.py +100 -0
- bootstack/signals/signal.py +353 -0
- bootstack/signals/types.py +6 -0
- bootstack/store.py +286 -0
- bootstack/streams/__init__.py +12 -0
- bootstack/streams/_stream.py +321 -0
- bootstack/style/__init__.py +38 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +46 -0
- bootstack/style/builders/button.py +339 -0
- bootstack/style/builders/buttongroup.py +311 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +110 -0
- bootstack/style/builders/combobox.py +113 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +82 -0
- bootstack/style/builders/expander.py +148 -0
- bootstack/style/builders/field.py +335 -0
- bootstack/style/builders/frame.py +50 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +34 -0
- bootstack/style/builders/listview.py +369 -0
- bootstack/style/builders/menubar.py +91 -0
- bootstack/style/builders/menubutton.py +359 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +67 -0
- bootstack/style/builders/radiobutton.py +99 -0
- bootstack/style/builders/scale.py +64 -0
- bootstack/style/builders/scrollbar.py +225 -0
- bootstack/style/builders/separator.py +49 -0
- bootstack/style/builders/sidenav.py +643 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +70 -0
- bootstack/style/builders/tabitem.py +204 -0
- bootstack/style/builders/togglegroup.py +294 -0
- bootstack/style/builders/toolbutton.py +275 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +193 -0
- bootstack/style/builders/utils.py +455 -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/fonts.py +123 -0
- bootstack/style/style.py +609 -0
- bootstack/style/style_builder_base.py +716 -0
- bootstack/style/style_builder_mixed.py +93 -0
- bootstack/style/style_builder_tk.py +109 -0
- bootstack/style/style_builder_ttk.py +353 -0
- bootstack/style/style_resolver.py +447 -0
- bootstack/style/theme.py +245 -0
- bootstack/style/theme_provider.py +471 -0
- bootstack/style/themes/__init__.py +128 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +41 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +523 -0
- bootstack/style/utility.py +746 -0
- bootstack/types.py +39 -0
- bootstack/validation/__init__.py +6 -0
- bootstack/validation/types.py +15 -0
- bootstack/validation/validation_result.py +17 -0
- bootstack/validation/validation_rules.py +205 -0
- bootstack/widgets/__init__.py +74 -0
- bootstack/widgets/_core/__init__.py +31 -0
- bootstack/widgets/_core/app_config.py +266 -0
- bootstack/widgets/_core/base.py +461 -0
- bootstack/widgets/_core/container.py +334 -0
- bootstack/widgets/_core/context.py +35 -0
- bootstack/widgets/_core/events.py +130 -0
- bootstack/widgets/_core/field_mixin.py +353 -0
- bootstack/widgets/_core/icon_image_props.py +72 -0
- bootstack/widgets/_core/image_binding.py +93 -0
- bootstack/widgets/_core/navmodel.py +345 -0
- bootstack/widgets/_core/options.py +210 -0
- bootstack/widgets/_core/selection_group.py +130 -0
- bootstack/widgets/_core/window_controls.py +96 -0
- bootstack/widgets/_core/window_menu.py +246 -0
- bootstack/widgets/_impl/__init__.py +1 -0
- bootstack/widgets/_impl/_internal/__init__.py +0 -0
- bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
- bootstack/widgets/_impl/_parts/__init__.py +11 -0
- bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
- bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
- bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
- bootstack/widgets/_impl/composites/__init__.py +33 -0
- bootstack/widgets/_impl/composites/_dateutils.py +33 -0
- bootstack/widgets/_impl/composites/_image_fit.py +105 -0
- bootstack/widgets/_impl/composites/accordion.py +381 -0
- bootstack/widgets/_impl/composites/avatar.py +191 -0
- bootstack/widgets/_impl/composites/buttongroup.py +371 -0
- bootstack/widgets/_impl/composites/calendar.py +972 -0
- bootstack/widgets/_impl/composites/carousel.py +567 -0
- bootstack/widgets/_impl/composites/chart.py +882 -0
- bootstack/widgets/_impl/composites/compositeframe.py +298 -0
- bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
- bootstack/widgets/_impl/composites/dateentry.py +404 -0
- bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
- bootstack/widgets/_impl/composites/expander.py +515 -0
- bootstack/widgets/_impl/composites/field.py +670 -0
- bootstack/widgets/_impl/composites/form.py +1066 -0
- bootstack/widgets/_impl/composites/gallery.py +551 -0
- bootstack/widgets/_impl/composites/list/__init__.py +15 -0
- bootstack/widgets/_impl/composites/list/listitem.py +802 -0
- bootstack/widgets/_impl/composites/list/listview.py +1433 -0
- bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
- bootstack/widgets/_impl/composites/menu/model.py +358 -0
- bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
- bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
- bootstack/widgets/_impl/composites/meter.py +860 -0
- bootstack/widgets/_impl/composites/numericentry.py +201 -0
- bootstack/widgets/_impl/composites/pagestack.py +395 -0
- bootstack/widgets/_impl/composites/passwordentry.py +142 -0
- bootstack/widgets/_impl/composites/pathentry.py +168 -0
- bootstack/widgets/_impl/composites/picture.py +289 -0
- bootstack/widgets/_impl/composites/radiogroup.py +511 -0
- bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
- bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/_impl/composites/scrollview.py +764 -0
- bootstack/widgets/_impl/composites/selectbox.py +1026 -0
- bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
- bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
- bootstack/widgets/_impl/composites/shell/layout.py +335 -0
- bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
- bootstack/widgets/_impl/composites/shell/providers.py +558 -0
- bootstack/widgets/_impl/composites/shell/rail.py +117 -0
- bootstack/widgets/_impl/composites/shell/shell.py +581 -0
- bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
- bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
- bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
- bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
- bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
- bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
- bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
- bootstack/widgets/_impl/composites/slider/slider.py +851 -0
- bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
- bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
- bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
- bootstack/widgets/_impl/composites/tableview/types.py +169 -0
- bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
- bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
- bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
- bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
- bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
- bootstack/widgets/_impl/composites/textarea/change.py +46 -0
- bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
- bootstack/widgets/_impl/composites/textarea/core.py +495 -0
- bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
- bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
- bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
- bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
- bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
- bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
- bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
- bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
- bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
- bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
- bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
- bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
- bootstack/widgets/_impl/composites/textentry.py +57 -0
- bootstack/widgets/_impl/composites/timeentry.py +176 -0
- bootstack/widgets/_impl/composites/toast.py +390 -0
- bootstack/widgets/_impl/composites/toast_stack.py +156 -0
- bootstack/widgets/_impl/composites/togglegroup.py +418 -0
- bootstack/widgets/_impl/composites/toolbar.py +608 -0
- bootstack/widgets/_impl/composites/tooltip.py +491 -0
- bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
- bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
- bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
- bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
- bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
- bootstack/widgets/_impl/mixins/__init__.py +24 -0
- bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
- bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
- bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
- bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
- bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
- bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
- bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
- bootstack/widgets/_impl/primitives/__init__.py +49 -0
- bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
- bootstack/widgets/_impl/primitives/badge.py +45 -0
- bootstack/widgets/_impl/primitives/button.py +76 -0
- bootstack/widgets/_impl/primitives/card.py +45 -0
- bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
- bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
- bootstack/widgets/_impl/primitives/combobox.py +156 -0
- bootstack/widgets/_impl/primitives/entry.py +87 -0
- bootstack/widgets/_impl/primitives/flexframe.py +448 -0
- bootstack/widgets/_impl/primitives/frame.py +185 -0
- bootstack/widgets/_impl/primitives/gridframe.py +546 -0
- bootstack/widgets/_impl/primitives/label.py +84 -0
- bootstack/widgets/_impl/primitives/labelframe.py +54 -0
- bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
- bootstack/widgets/_impl/primitives/packframe.py +227 -0
- bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
- bootstack/widgets/_impl/primitives/progressbar.py +83 -0
- bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
- bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
- bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
- bootstack/widgets/_impl/primitives/separator.py +43 -0
- bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
- bootstack/widgets/_impl/primitives/spinbox.py +95 -0
- bootstack/widgets/_impl/primitives/switch.py +44 -0
- bootstack/widgets/_impl/primitives/treeview.py +69 -0
- bootstack/widgets/app.py +371 -0
- bootstack/widgets/appshell.py +1179 -0
- bootstack/widgets/avatar.py +140 -0
- bootstack/widgets/boolean_controls.py +455 -0
- bootstack/widgets/button.py +224 -0
- bootstack/widgets/buttongroup.py +239 -0
- bootstack/widgets/calendar.py +195 -0
- bootstack/widgets/card.py +159 -0
- bootstack/widgets/carousel.py +241 -0
- bootstack/widgets/chart.py +302 -0
- bootstack/widgets/codeeditor.py +675 -0
- bootstack/widgets/contextmenu.py +371 -0
- bootstack/widgets/datatable.py +688 -0
- bootstack/widgets/datefield.py +395 -0
- bootstack/widgets/divider.py +60 -0
- bootstack/widgets/expander.py +579 -0
- bootstack/widgets/form.py +200 -0
- bootstack/widgets/gallery.py +250 -0
- bootstack/widgets/gauge.py +168 -0
- bootstack/widgets/grid.py +121 -0
- bootstack/widgets/groupbox.py +162 -0
- bootstack/widgets/label.py +229 -0
- bootstack/widgets/listview.py +354 -0
- bootstack/widgets/menubutton.py +419 -0
- bootstack/widgets/numberfield.py +431 -0
- bootstack/widgets/pagestack.py +345 -0
- bootstack/widgets/passwordfield.py +383 -0
- bootstack/widgets/pathfield.py +454 -0
- bootstack/widgets/picture.py +251 -0
- bootstack/widgets/progressbar.py +106 -0
- bootstack/widgets/radio_variants.py +271 -0
- bootstack/widgets/radiogroup.py +228 -0
- bootstack/widgets/scrollbar.py +64 -0
- bootstack/widgets/scrollview.py +186 -0
- bootstack/widgets/select.py +302 -0
- bootstack/widgets/selectbutton.py +182 -0
- bootstack/widgets/sidebar_toggle.py +130 -0
- bootstack/widgets/sizegrip.py +42 -0
- bootstack/widgets/slider.py +408 -0
- bootstack/widgets/spinbox.py +147 -0
- bootstack/widgets/spinnerfield.py +413 -0
- bootstack/widgets/splash.py +367 -0
- bootstack/widgets/splitview.py +482 -0
- bootstack/widgets/stacks.py +249 -0
- bootstack/widgets/statusbar.py +189 -0
- bootstack/widgets/tabs.py +425 -0
- bootstack/widgets/textarea.py +459 -0
- bootstack/widgets/textfield.py +410 -0
- bootstack/widgets/theme_toggle.py +92 -0
- bootstack/widgets/timefield.py +340 -0
- bootstack/widgets/toast.py +314 -0
- bootstack/widgets/togglegroup.py +231 -0
- bootstack/widgets/toolbar.py +347 -0
- bootstack/widgets/tooltip.py +94 -0
- bootstack/widgets/tree.py +648 -0
- bootstack/widgets/types.py +305 -0
- bootstack/widgets/window.py +307 -0
- bootstack-0.1.0.dist-info/METADATA +301 -0
- bootstack-0.1.0.dist-info/RECORD +471 -0
- bootstack-0.1.0.dist-info/WHEEL +5 -0
- bootstack-0.1.0.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0.dist-info/top_level.txt +1 -0
bootstack/data/README.md
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
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 load(self, records): ...
|
|
27
|
+
def 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.data import MemoryDataSource
|
|
55
|
+
from bootstack import col
|
|
56
|
+
|
|
57
|
+
ds = MemoryDataSource(page_size=20)
|
|
58
|
+
ds.load([
|
|
59
|
+
{"name": "Alice", "age": 30, "city": "NYC"},
|
|
60
|
+
{"name": "Bob", "age": 25, "city": "LA"},
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
ds.where(col("age") >= 25)
|
|
64
|
+
ds.order("name")
|
|
65
|
+
page = ds.page(0)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Features:**
|
|
69
|
+
- Expressive filtering with the `col` expression API
|
|
70
|
+
- Multi-column sorting
|
|
71
|
+
- O(1) ID lookups via internal index
|
|
72
|
+
- Automatic ID generation
|
|
73
|
+
|
|
74
|
+
### SqliteDataSource
|
|
75
|
+
|
|
76
|
+
Best for large datasets requiring persistence or SQL capabilities.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from bootstack.data import SqliteDataSource
|
|
80
|
+
|
|
81
|
+
# Persistent database
|
|
82
|
+
ds = SqliteDataSource("mydata.db", page_size=50)
|
|
83
|
+
ds.load([...])
|
|
84
|
+
|
|
85
|
+
# In-memory database
|
|
86
|
+
ds = SqliteDataSource(":memory:", page_size=50)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Features:**
|
|
90
|
+
- SQL-native filtering and sorting
|
|
91
|
+
- Persistent storage across sessions
|
|
92
|
+
- Efficient for large datasets
|
|
93
|
+
- Automatic schema inference
|
|
94
|
+
|
|
95
|
+
### FileDataSource
|
|
96
|
+
|
|
97
|
+
Best for loading data from files with preprocessing needs.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from bootstack.data import FileDataSource, FileSourceConfig
|
|
101
|
+
|
|
102
|
+
# Simple CSV loading
|
|
103
|
+
ds = FileDataSource("data.csv")
|
|
104
|
+
ds.load()
|
|
105
|
+
|
|
106
|
+
# With transformations
|
|
107
|
+
config = FileSourceConfig(
|
|
108
|
+
column_renames={'old_name': 'new_name'},
|
|
109
|
+
column_types={'age': int, 'salary': float},
|
|
110
|
+
row_filter=lambda r: r['status'] == 'active'
|
|
111
|
+
)
|
|
112
|
+
ds = FileDataSource("data.json", config=config)
|
|
113
|
+
ds.load()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Features:**
|
|
117
|
+
- Supports CSV, TSV, JSON, JSONL
|
|
118
|
+
- Multiple loading strategies (eager, lazy, chunked, hybrid)
|
|
119
|
+
- Transformation pipeline
|
|
120
|
+
- Progress callbacks for large files
|
|
121
|
+
|
|
122
|
+
## Creating Custom DataSources
|
|
123
|
+
|
|
124
|
+
### Method 1: Inherit from BaseDataSource (Recommended)
|
|
125
|
+
|
|
126
|
+
This is the easiest and most common approach. You get utility methods for free and only need to implement storage-specific logic.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
from bootstack.data import BaseDataSource
|
|
130
|
+
from typing import Any, Dict, List, Optional
|
|
131
|
+
|
|
132
|
+
class RedisDataSource(BaseDataSource):
|
|
133
|
+
"""Custom datasource backed by Redis."""
|
|
134
|
+
|
|
135
|
+
def __init__(self, redis_client, page_size=10):
|
|
136
|
+
super().__init__(page_size)
|
|
137
|
+
self.redis = redis_client
|
|
138
|
+
self._key_prefix = "myapp:records:"
|
|
139
|
+
|
|
140
|
+
def load(self, records):
|
|
141
|
+
"""Store records in Redis."""
|
|
142
|
+
for record in records:
|
|
143
|
+
record_id = record.get('id', self._generate_id())
|
|
144
|
+
key = f"{self._key_prefix}{record_id}"
|
|
145
|
+
self.redis.set(key, json.dumps(record))
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def page(self, page=None):
|
|
149
|
+
"""Retrieve a page of records."""
|
|
150
|
+
if page is not None:
|
|
151
|
+
self._page = page
|
|
152
|
+
|
|
153
|
+
# Get all keys and paginate
|
|
154
|
+
keys = self.redis.keys(f"{self._key_prefix}*")
|
|
155
|
+
start = self._page * self.page_size
|
|
156
|
+
end = start + self.page_size
|
|
157
|
+
|
|
158
|
+
records = []
|
|
159
|
+
for key in keys[start:end]:
|
|
160
|
+
data = self.redis.get(key)
|
|
161
|
+
records.append(json.loads(data))
|
|
162
|
+
|
|
163
|
+
return records
|
|
164
|
+
|
|
165
|
+
def insert(self, record):
|
|
166
|
+
"""Create new record in Redis."""
|
|
167
|
+
record_id = record.get('id', self._generate_id())
|
|
168
|
+
record['id'] = record_id
|
|
169
|
+
|
|
170
|
+
# Use hook for logging/validation
|
|
171
|
+
record = self._before_create(record)
|
|
172
|
+
|
|
173
|
+
key = f"{self._key_prefix}{record_id}"
|
|
174
|
+
self.redis.set(key, json.dumps(record))
|
|
175
|
+
|
|
176
|
+
# Use hook for post-creation tasks
|
|
177
|
+
self._after_create(record_id, record)
|
|
178
|
+
|
|
179
|
+
return record_id
|
|
180
|
+
|
|
181
|
+
def get(self, record_id):
|
|
182
|
+
"""Read single record from Redis."""
|
|
183
|
+
key = f"{self._key_prefix}{record_id}"
|
|
184
|
+
data = self.redis.get(key)
|
|
185
|
+
return json.loads(data) if data else None
|
|
186
|
+
|
|
187
|
+
def update(self, record_id, updates):
|
|
188
|
+
"""Update record in Redis."""
|
|
189
|
+
record = self.get(record_id)
|
|
190
|
+
if not record:
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
updates = self._before_update(record_id, updates)
|
|
194
|
+
record.update(updates)
|
|
195
|
+
|
|
196
|
+
key = f"{self._key_prefix}{record_id}"
|
|
197
|
+
self.redis.set(key, json.dumps(record))
|
|
198
|
+
|
|
199
|
+
self._after_update(record_id, updates, True)
|
|
200
|
+
return True
|
|
201
|
+
|
|
202
|
+
def delete(self, record_id):
|
|
203
|
+
"""Delete record from Redis."""
|
|
204
|
+
self._before_delete(record_id)
|
|
205
|
+
key = f"{self._key_prefix}{record_id}"
|
|
206
|
+
result = self.redis.delete(key) > 0
|
|
207
|
+
self._after_delete(record_id, result)
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
# Implement remaining abstract methods...
|
|
211
|
+
# (where, order, next_page, prev_page, has_next_page,
|
|
212
|
+
# count, selection methods, export_csv, etc.)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Method 2: Implement the Protocol (Advanced)
|
|
216
|
+
|
|
217
|
+
For maximum flexibility, implement the `DataSourceProtocol` without inheritance. This is useful when you need to integrate with existing classes or have complex inheritance requirements.
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from bootstack.data import DataSourceProtocol
|
|
221
|
+
from typing import List, Dict, Any, Optional
|
|
222
|
+
|
|
223
|
+
class APIDataSource:
|
|
224
|
+
"""Datasource that fetches data from a REST API."""
|
|
225
|
+
|
|
226
|
+
def __init__(self, api_url: str, page_size: int = 10):
|
|
227
|
+
self.api_url = api_url
|
|
228
|
+
self.page_size = page_size
|
|
229
|
+
self._page = 0
|
|
230
|
+
self._cache = []
|
|
231
|
+
|
|
232
|
+
def load(self, records):
|
|
233
|
+
"""Cache data locally."""
|
|
234
|
+
self._cache = list(records)
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def page(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
238
|
+
"""Fetch page from API or cache."""
|
|
239
|
+
if page is not None:
|
|
240
|
+
self._page = page
|
|
241
|
+
|
|
242
|
+
# Make API request with pagination
|
|
243
|
+
response = requests.get(
|
|
244
|
+
self.api_url,
|
|
245
|
+
params={'page': self._page, 'per_page': self.page_size}
|
|
246
|
+
)
|
|
247
|
+
return response.json()['data']
|
|
248
|
+
|
|
249
|
+
# Implement all other protocol methods...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Using Utility Methods
|
|
253
|
+
|
|
254
|
+
The `BaseDataSource` provides several utility methods you can use:
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
class MyDataSource(BaseDataSource):
|
|
258
|
+
def process_record(self, raw_data: str):
|
|
259
|
+
# Parse literals
|
|
260
|
+
age = self._coerce_literal("25") # Returns int 25
|
|
261
|
+
active = self._coerce_literal("true") # Returns bool True
|
|
262
|
+
name = self._coerce_literal("'Alice'") # Returns str "Alice"
|
|
263
|
+
|
|
264
|
+
# Infer SQL types
|
|
265
|
+
age_type = self._infer_type(25) # Returns "INTEGER"
|
|
266
|
+
name_type = self._infer_type("Alice") # Returns "TEXT"
|
|
267
|
+
|
|
268
|
+
# Validate records
|
|
269
|
+
record = {"name": name, "age": age}
|
|
270
|
+
self._validate_record(record) # Raises ValueError if not dict
|
|
271
|
+
|
|
272
|
+
return record
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Using Hook Methods
|
|
276
|
+
|
|
277
|
+
Hook methods let you add custom behavior without overriding core logic:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
class AuditedDataSource(BaseDataSource):
|
|
281
|
+
def __init__(self):
|
|
282
|
+
super().__init__()
|
|
283
|
+
self.audit_log = []
|
|
284
|
+
|
|
285
|
+
def _before_create(self, record):
|
|
286
|
+
"""Add timestamp before creating."""
|
|
287
|
+
record['created_at'] = datetime.now().isoformat()
|
|
288
|
+
return record
|
|
289
|
+
|
|
290
|
+
def _after_create(self, record_id, record):
|
|
291
|
+
"""Log creation to audit trail."""
|
|
292
|
+
self.audit_log.append({
|
|
293
|
+
'action': 'CREATE',
|
|
294
|
+
'record_id': record_id,
|
|
295
|
+
'timestamp': datetime.now()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
def _before_update(self, record_id, updates):
|
|
299
|
+
"""Add modified timestamp."""
|
|
300
|
+
updates['modified_at'] = datetime.now().isoformat()
|
|
301
|
+
return updates
|
|
302
|
+
|
|
303
|
+
def _after_update(self, record_id, updates, success):
|
|
304
|
+
"""Log update to audit trail."""
|
|
305
|
+
if success:
|
|
306
|
+
self.audit_log.append({
|
|
307
|
+
'action': 'UPDATE',
|
|
308
|
+
'record_id': record_id,
|
|
309
|
+
'changes': updates,
|
|
310
|
+
'timestamp': datetime.now()
|
|
311
|
+
})
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## API Reference
|
|
315
|
+
|
|
316
|
+
### Required Methods (All Implementations)
|
|
317
|
+
|
|
318
|
+
#### Data & View Configuration
|
|
319
|
+
- `load(records)` - Load data into the datasource
|
|
320
|
+
- `where(condition)` - Filter rows by a `col` condition (None clears)
|
|
321
|
+
- `order(*keys)` - Sort by column names ("name", "-age") or `col` specs
|
|
322
|
+
|
|
323
|
+
#### Pagination
|
|
324
|
+
- `page(page=None)` - Get records for specified page
|
|
325
|
+
- `next_page()` - Move to next page
|
|
326
|
+
- `prev_page()` - Move to previous page
|
|
327
|
+
- `has_next_page()` - Check if next page exists
|
|
328
|
+
- `count` - Get total record count
|
|
329
|
+
|
|
330
|
+
#### CRUD Operations
|
|
331
|
+
- `insert(record)` - Create new record, returns ID
|
|
332
|
+
- `get(record_id)` - Read single record by ID
|
|
333
|
+
- `update(record_id, updates)` - Update record fields
|
|
334
|
+
- `delete(record_id)` - Delete record by ID
|
|
335
|
+
|
|
336
|
+
#### Selection Management
|
|
337
|
+
- `is_selected(record_id)` - Check whether a record is selected
|
|
338
|
+
- `select(record_id)` - Mark record as selected
|
|
339
|
+
- `deselect(record_id)` - Unmark record
|
|
340
|
+
- `select_all(current_page_only=False)` - Select all/page records
|
|
341
|
+
- `deselect_all(current_page_only=False)` - Deselect all/page records
|
|
342
|
+
- `selected(page=None)` - Get selected records
|
|
343
|
+
- `selected_count` - Count selected records
|
|
344
|
+
|
|
345
|
+
#### Lifecycle / reorder
|
|
346
|
+
- `reload()` - Re-read from source (no-op for in-memory)
|
|
347
|
+
- `move(record_id, target_index)` - Reorder a record
|
|
348
|
+
|
|
349
|
+
#### Export
|
|
350
|
+
- `export_csv(filepath, include_all=True)` - Export to CSV
|
|
351
|
+
|
|
352
|
+
#### Index-based Access
|
|
353
|
+
- `page_slice(start_index, count)` - Get records by index range
|
|
354
|
+
|
|
355
|
+
### Utility Methods (Inherited from BaseDataSource)
|
|
356
|
+
|
|
357
|
+
- `_infer_type(value)` - Infer SQL type from Python value
|
|
358
|
+
- `_is_mapping(x)` - Check if value is dict-like
|
|
359
|
+
- `_coerce_literal(s)` - Parse string literal to Python value
|
|
360
|
+
- `_validate_record(record)` - Validate record is a dictionary
|
|
361
|
+
|
|
362
|
+
### Hook Methods (Inherited from BaseDataSource)
|
|
363
|
+
|
|
364
|
+
Override these to add custom behavior:
|
|
365
|
+
|
|
366
|
+
- `_before_create(record)` - Called before creating record
|
|
367
|
+
- `_after_create(record_id, record)` - Called after creating record
|
|
368
|
+
- `_before_update(record_id, updates)` - Called before updating
|
|
369
|
+
- `_after_update(record_id, updates, success)` - Called after updating
|
|
370
|
+
- `_before_delete(record_id)` - Called before deleting
|
|
371
|
+
- `_after_delete(record_id, success)` - Called after deleting
|
|
372
|
+
|
|
373
|
+
## Examples
|
|
374
|
+
|
|
375
|
+
### Example 1: MongoDB DataSource
|
|
376
|
+
|
|
377
|
+
```python
|
|
378
|
+
from bootstack.data import BaseDataSource
|
|
379
|
+
from pymongo import MongoClient
|
|
380
|
+
|
|
381
|
+
class MongoDataSource(BaseDataSource):
|
|
382
|
+
def __init__(self, connection_string, database, collection, page_size=10):
|
|
383
|
+
super().__init__(page_size)
|
|
384
|
+
self.client = MongoClient(connection_string)
|
|
385
|
+
self.db = self.client[database]
|
|
386
|
+
self.collection = self.db[collection]
|
|
387
|
+
self._filter = {}
|
|
388
|
+
self._sort = []
|
|
389
|
+
|
|
390
|
+
def load(self, records):
|
|
391
|
+
"""Insert records into MongoDB."""
|
|
392
|
+
if records:
|
|
393
|
+
self.collection.insert_many(list(records))
|
|
394
|
+
return self
|
|
395
|
+
|
|
396
|
+
def where(self, condition=None):
|
|
397
|
+
"""Store the active filter condition (a `col` expression, or None).
|
|
398
|
+
|
|
399
|
+
For a query backend you translate the condition tree (Compare/Match/
|
|
400
|
+
And/Or/... nodes) into a native query. As a simple fallback you can
|
|
401
|
+
evaluate `condition.matches(row)` in Python over fetched rows.
|
|
402
|
+
"""
|
|
403
|
+
self._filter = condition
|
|
404
|
+
return self
|
|
405
|
+
|
|
406
|
+
def order(self, *keys):
|
|
407
|
+
"""Store the active sort keys (column names; "-name" for descending)."""
|
|
408
|
+
self._sort = [
|
|
409
|
+
(k[1:], -1) if k.startswith("-") else (k, 1)
|
|
410
|
+
for k in keys
|
|
411
|
+
]
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
def page(self, page=None):
|
|
415
|
+
if page is not None:
|
|
416
|
+
self._page = page
|
|
417
|
+
|
|
418
|
+
skip = self._page * self.page_size
|
|
419
|
+
cursor = self.collection.find(self._filter).sort(self._sort).skip(skip).limit(self.page_size)
|
|
420
|
+
return list(cursor)
|
|
421
|
+
|
|
422
|
+
def insert(self, record):
|
|
423
|
+
result = self.collection.insert_one(record)
|
|
424
|
+
return result.inserted_id
|
|
425
|
+
|
|
426
|
+
def get(self, record_id):
|
|
427
|
+
return self.collection.find_one({"_id": record_id})
|
|
428
|
+
|
|
429
|
+
def update(self, record_id, updates):
|
|
430
|
+
result = self.collection.update_one({"_id": record_id}, {"$set": updates})
|
|
431
|
+
return result.modified_count > 0
|
|
432
|
+
|
|
433
|
+
def delete(self, record_id):
|
|
434
|
+
result = self.collection.delete_one({"_id": record_id})
|
|
435
|
+
return result.deleted_count > 0
|
|
436
|
+
|
|
437
|
+
# ... implement remaining methods
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Example 2: Cached API DataSource
|
|
441
|
+
|
|
442
|
+
```python
|
|
443
|
+
from bootstack.data import BaseDataSource
|
|
444
|
+
import requests
|
|
445
|
+
from functools import lru_cache
|
|
446
|
+
from datetime import datetime, timedelta
|
|
447
|
+
|
|
448
|
+
class CachedAPIDataSource(BaseDataSource):
|
|
449
|
+
def __init__(self, api_url, cache_ttl=300, page_size=10):
|
|
450
|
+
super().__init__(page_size)
|
|
451
|
+
self.api_url = api_url
|
|
452
|
+
self.cache_ttl = cache_ttl
|
|
453
|
+
self._cache = {}
|
|
454
|
+
self._cache_time = {}
|
|
455
|
+
|
|
456
|
+
def _is_cache_valid(self, key):
|
|
457
|
+
"""Check if cached data is still valid."""
|
|
458
|
+
if key not in self._cache_time:
|
|
459
|
+
return False
|
|
460
|
+
age = datetime.now() - self._cache_time[key]
|
|
461
|
+
return age < timedelta(seconds=self.cache_ttl)
|
|
462
|
+
|
|
463
|
+
def page(self, page=None):
|
|
464
|
+
if page is not None:
|
|
465
|
+
self._page = page
|
|
466
|
+
|
|
467
|
+
cache_key = f"page_{self._page}"
|
|
468
|
+
|
|
469
|
+
# Return cached data if valid
|
|
470
|
+
if self._is_cache_valid(cache_key):
|
|
471
|
+
return self._cache[cache_key]
|
|
472
|
+
|
|
473
|
+
# Fetch from API
|
|
474
|
+
response = requests.get(
|
|
475
|
+
f"{self.api_url}/records",
|
|
476
|
+
params={'page': self._page, 'per_page': self.page_size}
|
|
477
|
+
)
|
|
478
|
+
data = response.json()
|
|
479
|
+
|
|
480
|
+
# Cache the result
|
|
481
|
+
self._cache[cache_key] = data
|
|
482
|
+
self._cache_time[cache_key] = datetime.now()
|
|
483
|
+
|
|
484
|
+
return data
|
|
485
|
+
|
|
486
|
+
def insert(self, record):
|
|
487
|
+
# Invalidate cache on write
|
|
488
|
+
self._cache.clear()
|
|
489
|
+
self._cache_time.clear()
|
|
490
|
+
|
|
491
|
+
response = requests.post(f"{self.api_url}/records", json=record)
|
|
492
|
+
return response.json()['id']
|
|
493
|
+
|
|
494
|
+
# ... implement remaining methods
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Example 3: Multi-Source DataSource
|
|
498
|
+
|
|
499
|
+
```python
|
|
500
|
+
from bootstack.data import BaseDataSource
|
|
501
|
+
|
|
502
|
+
class MultiSourceDataSource(BaseDataSource):
|
|
503
|
+
"""Aggregates data from multiple datasources."""
|
|
504
|
+
|
|
505
|
+
def __init__(self, sources, page_size=10):
|
|
506
|
+
super().__init__(page_size)
|
|
507
|
+
self.sources = sources # List of datasources
|
|
508
|
+
self._aggregated_data = []
|
|
509
|
+
|
|
510
|
+
def load(self, records):
|
|
511
|
+
"""Not applicable for multi-source."""
|
|
512
|
+
raise NotImplementedError("Use add_source() instead")
|
|
513
|
+
|
|
514
|
+
def add_source(self, datasource):
|
|
515
|
+
"""Add a datasource to aggregate."""
|
|
516
|
+
self.sources.append(datasource)
|
|
517
|
+
|
|
518
|
+
def _aggregate_data(self):
|
|
519
|
+
"""Combine data from all sources."""
|
|
520
|
+
all_records = []
|
|
521
|
+
for source in self.sources:
|
|
522
|
+
source_records = source.page_slice(0, source.count)
|
|
523
|
+
all_records.extend(source_records)
|
|
524
|
+
return all_records
|
|
525
|
+
|
|
526
|
+
def page(self, page=None):
|
|
527
|
+
if page is not None:
|
|
528
|
+
self._page = page
|
|
529
|
+
|
|
530
|
+
all_data = self._aggregate_data()
|
|
531
|
+
start = self._page * self.page_size
|
|
532
|
+
end = start + self.page_size
|
|
533
|
+
return all_data[start:end]
|
|
534
|
+
|
|
535
|
+
# ... implement remaining methods
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Best Practices
|
|
539
|
+
|
|
540
|
+
1. **Always call `super().__init__(page_size)`** in your `__init__` method
|
|
541
|
+
2. **Use hook methods** for cross-cutting concerns (logging, validation, caching)
|
|
542
|
+
3. **Inherit utility methods** instead of reimplementing them
|
|
543
|
+
4. **Handle errors gracefully** - return False/None for failures rather than raising
|
|
544
|
+
5. **Document your datasource** - explain what backend it uses and any special requirements
|
|
545
|
+
6. **Test thoroughly** - ensure all protocol methods work correctly
|
|
546
|
+
7. **Consider performance** - implement efficient filtering and pagination for large datasets
|
|
547
|
+
|
|
548
|
+
## Testing Your DataSource
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
def test_custom_datasource():
|
|
552
|
+
ds = MyCustomDataSource()
|
|
553
|
+
|
|
554
|
+
# Test basic operations
|
|
555
|
+
ds.load([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])
|
|
556
|
+
assert ds.count == 2
|
|
557
|
+
|
|
558
|
+
# Test CRUD
|
|
559
|
+
new_id = ds.insert({"name": "Charlie"})
|
|
560
|
+
assert ds.get(new_id) is not None
|
|
561
|
+
|
|
562
|
+
# Test pagination
|
|
563
|
+
page = ds.page(0)
|
|
564
|
+
assert isinstance(page, list)
|
|
565
|
+
|
|
566
|
+
# Test filtering
|
|
567
|
+
from bootstack import col
|
|
568
|
+
ds.where(col("name") == "Alice")
|
|
569
|
+
filtered = ds.page(0)
|
|
570
|
+
assert len(filtered) == 1
|
|
571
|
+
|
|
572
|
+
# Test it's an instance of BaseDataSource
|
|
573
|
+
assert isinstance(ds, BaseDataSource)
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Migration Guide
|
|
577
|
+
|
|
578
|
+
If you have an existing datasource implementation, here's how to migrate to use `BaseDataSource`:
|
|
579
|
+
|
|
580
|
+
### Before (Standalone Class)
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
class MyDataSource:
|
|
584
|
+
def __init__(self, page_size=10):
|
|
585
|
+
self.page_size = page_size
|
|
586
|
+
self._page = 0
|
|
587
|
+
|
|
588
|
+
@staticmethod
|
|
589
|
+
def _infer_type(value):
|
|
590
|
+
# Your implementation
|
|
591
|
+
pass
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
### After (Inheriting BaseDataSource)
|
|
595
|
+
|
|
596
|
+
```python
|
|
597
|
+
from bootstack.data import BaseDataSource
|
|
598
|
+
|
|
599
|
+
class MyDataSource(BaseDataSource):
|
|
600
|
+
def __init__(self, page_size=10):
|
|
601
|
+
super().__init__(page_size) # Initialize base class
|
|
602
|
+
# BaseDataSource now provides _page and utility methods
|
|
603
|
+
|
|
604
|
+
# Remove _infer_type - inherited from BaseDataSource
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
## Support
|
|
608
|
+
|
|
609
|
+
For questions, issues, or feature requests, please visit:
|
|
610
|
+
- GitHub Issues: https://github.com/israel-dryer/bootstack/issues
|
|
611
|
+
- Documentation: https://bootstack.org/
|
|
612
|
+
|
|
613
|
+
## License
|
|
614
|
+
|
|
615
|
+
This module is part of bootstack and follows the same license terms.
|
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
- Filtering and sorting with the `col` expression API
|
|
11
|
+
- Full CRUD operations (insert, get, update, delete)
|
|
12
|
+
- Record selection tracking
|
|
13
|
+
- CSV export
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from bootstack.data import MemoryDataSource, SqliteDataSource, FileDataSource, col
|
|
17
|
+
|
|
18
|
+
# In-memory datasource
|
|
19
|
+
ds = MemoryDataSource(page_size=20)
|
|
20
|
+
ds.load([{"name": "Alice", "age": 30}, ...])
|
|
21
|
+
|
|
22
|
+
# SQLite datasource (persistent)
|
|
23
|
+
db = SqliteDataSource("mydata.db", page_size=50)
|
|
24
|
+
db.load([{"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.where(col("age") >= 25)
|
|
32
|
+
ds.order("name")
|
|
33
|
+
first = ds.page(0)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from bootstack.data.base import BaseDataSource
|
|
37
|
+
from bootstack.data.memory_source import MemoryDataSource
|
|
38
|
+
from bootstack.data.sqlite_source import SqliteDataSource
|
|
39
|
+
from bootstack.data.file_source import FileDataSource, FileSourceConfig
|
|
40
|
+
from bootstack.data.query import col, Column, Condition, SortKey, any_of, all_of
|
|
41
|
+
from bootstack.data.types import DataSourceProtocol, Record, Primitive
|
|
42
|
+
from bootstack.data.readers import (
|
|
43
|
+
register_reader,
|
|
44
|
+
read_records,
|
|
45
|
+
get_reader,
|
|
46
|
+
supported_read_extensions,
|
|
47
|
+
)
|
|
48
|
+
from bootstack.data.writers import (
|
|
49
|
+
register_writer,
|
|
50
|
+
write_records,
|
|
51
|
+
get_writer,
|
|
52
|
+
supported_write_extensions,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
'BaseDataSource',
|
|
57
|
+
'MemoryDataSource',
|
|
58
|
+
'SqliteDataSource',
|
|
59
|
+
'FileDataSource',
|
|
60
|
+
'FileSourceConfig',
|
|
61
|
+
'DataSourceProtocol',
|
|
62
|
+
'Record',
|
|
63
|
+
'Primitive',
|
|
64
|
+
'col',
|
|
65
|
+
'Column',
|
|
66
|
+
'Condition',
|
|
67
|
+
'SortKey',
|
|
68
|
+
'any_of',
|
|
69
|
+
'all_of',
|
|
70
|
+
'register_reader',
|
|
71
|
+
'read_records',
|
|
72
|
+
'get_reader',
|
|
73
|
+
'supported_read_extensions',
|
|
74
|
+
'register_writer',
|
|
75
|
+
'write_records',
|
|
76
|
+
'get_writer',
|
|
77
|
+
'supported_write_extensions',
|
|
78
|
+
]
|