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/base.py
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
"""Abstract base class for datasource implementations.
|
|
2
|
+
|
|
3
|
+
Provides a common foundation for all datasource implementations with:
|
|
4
|
+
- Abstract method definitions enforcing the DataSourceProtocol interface
|
|
5
|
+
- Shared utility methods for type inference, literal parsing, and validation
|
|
6
|
+
- Template methods for common operations
|
|
7
|
+
- Hook methods for extensibility
|
|
8
|
+
|
|
9
|
+
Subclasses must implement storage-specific operations (load, page, CRUD, etc.)
|
|
10
|
+
while inheriting common utilities and patterns.
|
|
11
|
+
|
|
12
|
+
This base class makes it easier to:
|
|
13
|
+
- Create custom datasource implementations
|
|
14
|
+
- Reduce code duplication across implementations
|
|
15
|
+
- Ensure consistent behavior across all datasources
|
|
16
|
+
- Extend datasource functionality through hooks
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from abc import ABC, abstractmethod
|
|
22
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Mapping
|
|
23
|
+
|
|
24
|
+
from bootstack.data.types import Primitive, Record
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from bootstack.data.query import Column, Condition, SortKey
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseDataSource(ABC):
|
|
31
|
+
"""Abstract base class for datasource implementations.
|
|
32
|
+
|
|
33
|
+
Provides shared utilities and enforces the DataSourceProtocol interface through
|
|
34
|
+
abstract methods. Subclasses implement storage-specific logic while inheriting
|
|
35
|
+
common functionality.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
page_size(int): Number of records per page (default: 10)
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
page_size: Current page size setting
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
.. code-block:: python
|
|
45
|
+
|
|
46
|
+
class RedisDataSource(BaseDataSource):
|
|
47
|
+
def __init__(self, redis_client, page_size=10):
|
|
48
|
+
super().__init__(page_size)
|
|
49
|
+
self.redis = redis_client
|
|
50
|
+
|
|
51
|
+
def load(self, records):
|
|
52
|
+
pass # Redis-specific implementation
|
|
53
|
+
|
|
54
|
+
def page(self, page=None):
|
|
55
|
+
pass # Redis-specific implementation
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, page_size: int = 10):
|
|
60
|
+
"""Initialize base datasource with pagination settings.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
page_size: Number of records returned per page when paginating.
|
|
64
|
+
"""
|
|
65
|
+
self.page_size = page_size
|
|
66
|
+
self._page = 0
|
|
67
|
+
# Reactive change broadcasting (on_change / observe). Lazy import keeps
|
|
68
|
+
# the data layer free of a load-time dependency on the runtime layer.
|
|
69
|
+
from bootstack.data._observable import _ChangeHub
|
|
70
|
+
|
|
71
|
+
self._hub = _ChangeHub(self)
|
|
72
|
+
|
|
73
|
+
# ========== ABSTRACT METHODS - MUST BE IMPLEMENTED ==========
|
|
74
|
+
|
|
75
|
+
# Data & View Configuration
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def load(self, records: Sequence[Primitive] | Sequence[Mapping[str, Any]]) -> 'BaseDataSource':
|
|
78
|
+
"""Load data records into the datasource.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
records: Sequence of records (dicts) or primitives (auto-wrapped)
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Self for method chaining
|
|
85
|
+
"""
|
|
86
|
+
...
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def where(self, condition: "Condition | None" = None) -> "BaseDataSource":
|
|
90
|
+
"""Filter rows by a condition built with `col`.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
condition: A filter condition (e.g. `col("age") >= 25`), or `None`
|
|
94
|
+
/ no argument to clear the filter.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Self for method chaining
|
|
98
|
+
"""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def order(self, *keys: "str | Column | SortKey") -> "BaseDataSource":
|
|
103
|
+
"""Sort rows by one or more keys.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
keys: Column names (`"name"`), descending names (`"-name"`), or
|
|
107
|
+
`col(...)` specs. With no arguments, clears sorting.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Self for method chaining
|
|
111
|
+
"""
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
# Pagination
|
|
115
|
+
@abstractmethod
|
|
116
|
+
def page(self, page: Optional[int] = None) -> List[Record]:
|
|
117
|
+
"""Get records for specified page (or current page if None).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
page: Page number (0-indexed); updates current page if provided
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
List of record dictionaries for the page
|
|
124
|
+
"""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
@abstractmethod
|
|
128
|
+
def next_page(self) -> List[Record]:
|
|
129
|
+
"""Advance to next page and return its records.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of record dictionaries for the new page
|
|
133
|
+
"""
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def prev_page(self) -> List[Record]:
|
|
138
|
+
"""Move to previous page and return its records.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of record dictionaries for the new page
|
|
142
|
+
"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def has_next_page(self) -> bool:
|
|
147
|
+
"""Check if more pages exist after current page.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if next page exists, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def count(self) -> int:
|
|
157
|
+
"""Total number of records matching the current filter."""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
# CRUD Operations
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def insert(self, record: Dict[str, Any]) -> int:
|
|
163
|
+
"""Create new record and return its ID.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
record: Dictionary with record data
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The ID assigned to the new record
|
|
170
|
+
"""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def get(self, record_id: Any) -> Optional[Record]:
|
|
175
|
+
"""Retrieve single record by ID.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
record_id: Unique identifier of the record
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Record dictionary or None if not found
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def update(self, record_id: Any, updates: Dict[str, Any]) -> bool:
|
|
187
|
+
"""Update record fields by ID.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
record_id: Unique identifier of the record
|
|
191
|
+
updates: Dictionary with fields to update
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if record was updated, False if not found
|
|
195
|
+
"""
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
@abstractmethod
|
|
199
|
+
def delete(self, record_id: Any) -> bool:
|
|
200
|
+
"""Delete record by ID.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
record_id: Unique identifier of the record
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
True if record was deleted, False if not found
|
|
207
|
+
"""
|
|
208
|
+
...
|
|
209
|
+
|
|
210
|
+
# Selection Management
|
|
211
|
+
@abstractmethod
|
|
212
|
+
def is_selected(self, record_id: Any) -> bool:
|
|
213
|
+
"""Check whether a record is currently selected.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
record_id: Unique identifier of the record
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
True if the record is selected, False otherwise (including missing records)
|
|
220
|
+
"""
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
@abstractmethod
|
|
224
|
+
def select(self, record_id: Any) -> bool:
|
|
225
|
+
"""Mark record as selected.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
record_id: Unique identifier of the record
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if record was selected, False if not found
|
|
232
|
+
"""
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
def deselect(self, record_id: Any) -> bool:
|
|
237
|
+
"""Mark record as unselected.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
record_id: Unique identifier of the record
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if record was deselected, False if not found
|
|
244
|
+
"""
|
|
245
|
+
...
|
|
246
|
+
|
|
247
|
+
@abstractmethod
|
|
248
|
+
def select_all(self, current_page_only: bool = False) -> int:
|
|
249
|
+
"""Select all records (optionally only current page).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
current_page_only: If True, select only records on current page
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Number of records selected
|
|
256
|
+
"""
|
|
257
|
+
...
|
|
258
|
+
|
|
259
|
+
@abstractmethod
|
|
260
|
+
def deselect_all(self, current_page_only: bool = False) -> int:
|
|
261
|
+
"""Deselect all records (optionally only current page).
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
current_page_only: If True, deselect only records on current page
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Number of records deselected
|
|
268
|
+
"""
|
|
269
|
+
...
|
|
270
|
+
|
|
271
|
+
@abstractmethod
|
|
272
|
+
def selected(self, page: Optional[int] = None) -> List[Record]:
|
|
273
|
+
"""Get selected records, optionally paginated.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
page: Optional page number for paginating selected records
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
List of selected record dictionaries
|
|
280
|
+
"""
|
|
281
|
+
...
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
@abstractmethod
|
|
285
|
+
def selected_count(self) -> int:
|
|
286
|
+
"""Number of selected records."""
|
|
287
|
+
...
|
|
288
|
+
|
|
289
|
+
# Export
|
|
290
|
+
def save(
|
|
291
|
+
self,
|
|
292
|
+
path: str,
|
|
293
|
+
*,
|
|
294
|
+
selected_only: bool = False,
|
|
295
|
+
format: Optional[str] = None,
|
|
296
|
+
config: Any = None,
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Export records to a file, choosing the format by extension.
|
|
299
|
+
|
|
300
|
+
Records are streamed into the writer, so a large export does not
|
|
301
|
+
materialize the whole dataset. The active `where`/`order` view is
|
|
302
|
+
respected — what you export is what the source currently shows.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
path: Destination file path; its extension selects the format (CSV,
|
|
306
|
+
TSV, JSON, JSONL, XML, and — with the extras — Parquet, Feather,
|
|
307
|
+
HDF5).
|
|
308
|
+
selected_only: Export only selected records instead of all.
|
|
309
|
+
format: Explicit format name overriding the path extension.
|
|
310
|
+
config: Optional `FileSourceConfig` controlling encoding/delimiter/etc.
|
|
311
|
+
"""
|
|
312
|
+
from bootstack.data.writers import write_records
|
|
313
|
+
|
|
314
|
+
write_records(
|
|
315
|
+
path, self._export_records(selected_only=selected_only), config, format=format
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
def export_csv(self, filepath: str, include_all: bool = True) -> None:
|
|
319
|
+
"""Export records to a CSV file (streamed).
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
filepath: Path to output CSV file
|
|
323
|
+
include_all: If True, export all records; if False, export only selected
|
|
324
|
+
"""
|
|
325
|
+
self.save(filepath, selected_only=not include_all, format="csv")
|
|
326
|
+
|
|
327
|
+
def _export_records(self, *, selected_only: bool = False, chunk_size: int = 1000):
|
|
328
|
+
"""Yield user-facing records for export, paging in chunks (bounded memory)."""
|
|
329
|
+
if selected_only:
|
|
330
|
+
for record in self.selected():
|
|
331
|
+
yield self._public_record(record)
|
|
332
|
+
return
|
|
333
|
+
offset = 0
|
|
334
|
+
while True:
|
|
335
|
+
chunk = self.page_slice(offset, chunk_size)
|
|
336
|
+
if not chunk:
|
|
337
|
+
break
|
|
338
|
+
for record in chunk:
|
|
339
|
+
yield self._public_record(record)
|
|
340
|
+
offset += len(chunk)
|
|
341
|
+
|
|
342
|
+
# Index-based Access
|
|
343
|
+
@abstractmethod
|
|
344
|
+
def page_slice(self, start_index: int, count: int) -> List[Record]:
|
|
345
|
+
"""Get records by start index and count (respects filter/sort).
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
start_index: Starting record index
|
|
349
|
+
count: Number of records to retrieve
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
List of record dictionaries
|
|
353
|
+
"""
|
|
354
|
+
...
|
|
355
|
+
|
|
356
|
+
# Observable query — read-only filtered/sorted read
|
|
357
|
+
@abstractmethod
|
|
358
|
+
def _query(self, condition: "Condition | None", sort_keys: "List[SortKey]") -> List[Record]:
|
|
359
|
+
"""Run a one-off filtered and sorted read, returning fresh records.
|
|
360
|
+
|
|
361
|
+
Unlike `page`, this does NOT use or modify the source's active
|
|
362
|
+
`where`/`order` view state — it answers an `observe()` query in
|
|
363
|
+
isolation so pagination is undisturbed.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
condition: Filter condition (or `None` for all rows).
|
|
367
|
+
sort_keys: Ordering to apply (empty for source/natural order).
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
List of matching record dictionaries.
|
|
371
|
+
"""
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
# Lifecycle / reorder — concrete defaults; subclasses may override
|
|
375
|
+
def reload(self) -> None:
|
|
376
|
+
"""Re-read data from the underlying source.
|
|
377
|
+
|
|
378
|
+
Default is a no-op suitable for in-memory implementations.
|
|
379
|
+
File- and database-backed sources should override to re-query.
|
|
380
|
+
"""
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
def move(self, record_id: Any, target_index: int) -> bool:
|
|
384
|
+
"""Reorder a record to a new position.
|
|
385
|
+
|
|
386
|
+
Default returns False (not supported). Subclasses that maintain
|
|
387
|
+
an explicit ordering should override.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
record_id: Unique identifier of the record to move
|
|
391
|
+
target_index: Zero-based destination index (clamped to valid range)
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
True if the record was moved, False if not supported or not found
|
|
395
|
+
"""
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
# ========== RESOURCE LIFECYCLE (close / context manager) ==========
|
|
399
|
+
|
|
400
|
+
def close(self) -> None:
|
|
401
|
+
"""Release any resources held by this data source.
|
|
402
|
+
|
|
403
|
+
The default is a no-op — in-memory sources hold none. Sources backed by
|
|
404
|
+
a database connection or file handle override this to release it. Safe to
|
|
405
|
+
call more than once.
|
|
406
|
+
"""
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
def __enter__(self) -> "BaseDataSource":
|
|
410
|
+
"""Enter a `with` block, returning the data source itself."""
|
|
411
|
+
return self
|
|
412
|
+
|
|
413
|
+
def __exit__(self, exc_type, exc, tb) -> bool:
|
|
414
|
+
"""Exit a `with` block, releasing resources via `close()`.
|
|
415
|
+
|
|
416
|
+
Returns False so any exception raised inside the block propagates.
|
|
417
|
+
"""
|
|
418
|
+
self.close()
|
|
419
|
+
return False
|
|
420
|
+
|
|
421
|
+
# ========== ROW IDENTITY (for datasource-aware widgets) ==========
|
|
422
|
+
|
|
423
|
+
@property
|
|
424
|
+
def id_field(self) -> str:
|
|
425
|
+
"""Name of the record field that holds the stable row identity."""
|
|
426
|
+
return getattr(self, "_id_field", "id")
|
|
427
|
+
|
|
428
|
+
def _internal_fields(self) -> "frozenset[str]":
|
|
429
|
+
"""Raw-record keys that are implementation-internal and hidden from users.
|
|
430
|
+
|
|
431
|
+
Datasource-aware widgets (for example `Table`) strip these from displayed
|
|
432
|
+
columns, exports, and the records they hand back. The default is none;
|
|
433
|
+
sources that carry bookkeeping columns override this.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
A set of record keys to treat as private.
|
|
437
|
+
"""
|
|
438
|
+
return frozenset()
|
|
439
|
+
|
|
440
|
+
def _record_id(self, record: "Mapping[str, Any] | None") -> Any:
|
|
441
|
+
"""Return the stable identity of a raw record.
|
|
442
|
+
|
|
443
|
+
The returned value is what `get`/`update`/`delete`/`select` accept and
|
|
444
|
+
what is surfaced publicly as the record's `id`. The default reads the
|
|
445
|
+
configured `id_field`; sources with a separate identity column override.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
record: A raw record as returned by `page`/`page_slice`.
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
The record's identity, or `None` when it cannot be determined.
|
|
452
|
+
"""
|
|
453
|
+
if not record:
|
|
454
|
+
return None
|
|
455
|
+
return record.get(self.id_field)
|
|
456
|
+
|
|
457
|
+
def _public_record(self, record: "Mapping[str, Any] | None") -> Record:
|
|
458
|
+
"""Return a user-facing copy of a raw record.
|
|
459
|
+
|
|
460
|
+
Internal bookkeeping fields are stripped and the stable identity is
|
|
461
|
+
surfaced as `id`, so widgets and callers see a clean record regardless
|
|
462
|
+
of how the source stores it.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
record: A raw record as returned by `page`/`page_slice`.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
A new dict with internal fields removed and `id` populated.
|
|
469
|
+
"""
|
|
470
|
+
if not record:
|
|
471
|
+
return {}
|
|
472
|
+
internal = self._internal_fields()
|
|
473
|
+
out = {k: v for k, v in record.items() if k not in internal}
|
|
474
|
+
rid = self._record_id(record)
|
|
475
|
+
if rid is not None:
|
|
476
|
+
out["id"] = rid
|
|
477
|
+
return out
|
|
478
|
+
|
|
479
|
+
# ========== CHANGE BROADCASTING (on_change / observe) ==========
|
|
480
|
+
|
|
481
|
+
def on_change(self, handler: "Callable[[Any], Any] | None" = None) -> Any:
|
|
482
|
+
"""Subscribe to changes to this source.
|
|
483
|
+
|
|
484
|
+
Call with no argument to get a composable `Stream` of coarse change
|
|
485
|
+
events; chain `map`/`filter`/`debounce` and `listen` to drive any
|
|
486
|
+
widget (for example, a dashboard badge bound to the row count). Call
|
|
487
|
+
with a handler to subscribe directly and get back a cancellable
|
|
488
|
+
subscription.
|
|
489
|
+
|
|
490
|
+
The handler receives a `DataChangeEvent`. Rapid mutations are coalesced
|
|
491
|
+
into a single notification per event-loop turn, and mutations made from
|
|
492
|
+
a background thread are delivered on the main thread automatically — so
|
|
493
|
+
a bound widget can refresh from a worker-thread feed with no extra work.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
handler: Change handler. Omit to receive a `Stream` instead.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
A `Stream` when `handler` is omitted, otherwise a cancellable
|
|
500
|
+
subscription handle.
|
|
501
|
+
|
|
502
|
+
Example:
|
|
503
|
+
.. code-block:: python
|
|
504
|
+
|
|
505
|
+
ds.on_change(lambda e: print("changed:", e.kind))
|
|
506
|
+
|
|
507
|
+
# Feed a dashboard badge with the live row count.
|
|
508
|
+
ds.on_change().map(lambda e: ds.count).listen(badge.set_value)
|
|
509
|
+
"""
|
|
510
|
+
from bootstack.streams import Handle, Stream
|
|
511
|
+
|
|
512
|
+
hub = self._hub
|
|
513
|
+
if handler is not None:
|
|
514
|
+
return Handle(hub.add_listener(handler))
|
|
515
|
+
|
|
516
|
+
from bootstack._runtime.app import get_current_app, has_current_app
|
|
517
|
+
|
|
518
|
+
if not has_current_app():
|
|
519
|
+
raise RuntimeError(
|
|
520
|
+
"on_change() with no handler returns a Stream, which requires an "
|
|
521
|
+
"active App. Pass a handler — on_change(fn) — for headless use."
|
|
522
|
+
)
|
|
523
|
+
owner = get_current_app()
|
|
524
|
+
|
|
525
|
+
def _source(downstream):
|
|
526
|
+
return Handle(hub.add_listener(downstream))
|
|
527
|
+
|
|
528
|
+
return Stream(owner, _source=_source)
|
|
529
|
+
|
|
530
|
+
def observe(self, condition: "Condition | None" = None, *order: "str | Column | SortKey") -> Any:
|
|
531
|
+
"""Observe a live result set for a `where`/`order` query.
|
|
532
|
+
|
|
533
|
+
Returns a `Stream` that emits the matching records immediately, then a
|
|
534
|
+
fresh result set whenever a relevant change occurs. Each subscriber
|
|
535
|
+
observes its own slice — declare the query once, react to its results
|
|
536
|
+
over time (the "observable query" pattern).
|
|
537
|
+
|
|
538
|
+
Selection toggles do not re-emit (selection is not a row-set change).
|
|
539
|
+
Unlike `where`/`order`, observing does not disturb the source's own
|
|
540
|
+
pagination view.
|
|
541
|
+
|
|
542
|
+
Performance: each relevant change re-runs the whole query and re-emits
|
|
543
|
+
the full result set. Use `observe` for *small* derived sets — dashboard
|
|
544
|
+
metrics, a short pinned list, a filtered side panel. For large or
|
|
545
|
+
virtualized views (`Table`, `ListView`) do NOT observe the full set;
|
|
546
|
+
bind those widgets to the source directly — they listen via `on_change`
|
|
547
|
+
and refetch only their visible window with `page`/`page_slice`.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
condition: Filter condition built with `col` (or `None` for all rows).
|
|
551
|
+
order: Sort keys — column names, `"-name"` for descending, or
|
|
552
|
+
`col(...)` specs.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
A `Stream` of result sets (each a list of record dictionaries).
|
|
556
|
+
|
|
557
|
+
Example:
|
|
558
|
+
.. code-block:: python
|
|
559
|
+
|
|
560
|
+
ds.observe(col("status") == "active", "-created").listen(
|
|
561
|
+
lambda rows: gauge.set_value(len(rows))
|
|
562
|
+
)
|
|
563
|
+
"""
|
|
564
|
+
from bootstack.data._observable import _ObservedQuery
|
|
565
|
+
from bootstack.data.query import normalize_sort_keys
|
|
566
|
+
from bootstack.streams import Handle, Stream
|
|
567
|
+
from bootstack._runtime.app import get_current_app, has_current_app
|
|
568
|
+
|
|
569
|
+
if not has_current_app():
|
|
570
|
+
raise RuntimeError(
|
|
571
|
+
"observe() returns a Stream, which requires an active App."
|
|
572
|
+
)
|
|
573
|
+
owner = get_current_app()
|
|
574
|
+
sort_keys = normalize_sort_keys(order)
|
|
575
|
+
hub = self._hub
|
|
576
|
+
source = self
|
|
577
|
+
|
|
578
|
+
def _source(downstream):
|
|
579
|
+
query = _ObservedQuery(source, condition, sort_keys, downstream)
|
|
580
|
+
remove = hub.add_query(query)
|
|
581
|
+
try:
|
|
582
|
+
downstream(query.start())
|
|
583
|
+
except Exception:
|
|
584
|
+
pass
|
|
585
|
+
return Handle(remove)
|
|
586
|
+
|
|
587
|
+
return Stream(owner, _source=_source)
|
|
588
|
+
|
|
589
|
+
def _silence(self):
|
|
590
|
+
"""Context manager that suppresses change emission within its block.
|
|
591
|
+
|
|
592
|
+
Used internally by widgets to mutate their own bound source without
|
|
593
|
+
broadcasting the change back to themselves.
|
|
594
|
+
"""
|
|
595
|
+
return self._hub.silence()
|
|
596
|
+
|
|
597
|
+
# ========== SHARED UTILITY METHODS ==========
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _infer_type(value: Any) -> str:
|
|
601
|
+
"""Infer SQL-compatible type from Python value.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
value: Python value to infer type from
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
SQL type string ('INTEGER', 'REAL', 'BLOB', or 'TEXT')
|
|
608
|
+
|
|
609
|
+
Examples:
|
|
610
|
+
>>> BaseDataSource._infer_type(42)
|
|
611
|
+
'INTEGER'
|
|
612
|
+
>>> BaseDataSource._infer_type(3.14)
|
|
613
|
+
'REAL'
|
|
614
|
+
>>> BaseDataSource._infer_type("hello")
|
|
615
|
+
'TEXT'
|
|
616
|
+
"""
|
|
617
|
+
if isinstance(value, int):
|
|
618
|
+
return "INTEGER"
|
|
619
|
+
elif isinstance(value, float):
|
|
620
|
+
return "REAL"
|
|
621
|
+
elif isinstance(value, (bytes, bytearray)):
|
|
622
|
+
return "BLOB"
|
|
623
|
+
return "TEXT"
|
|
624
|
+
|
|
625
|
+
@staticmethod
|
|
626
|
+
def _is_mapping(x: Any) -> bool:
|
|
627
|
+
"""Check if value is a mapping (dict-like).
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
x: Value to check
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
True if value is a mapping, False otherwise
|
|
634
|
+
|
|
635
|
+
Examples:
|
|
636
|
+
>>> BaseDataSource._is_mapping({'a': 1})
|
|
637
|
+
True
|
|
638
|
+
>>> BaseDataSource._is_mapping([1, 2, 3])
|
|
639
|
+
False
|
|
640
|
+
"""
|
|
641
|
+
return isinstance(x, Mapping)
|
|
642
|
+
|
|
643
|
+
@staticmethod
|
|
644
|
+
def _coerce_literal(s: str) -> Any:
|
|
645
|
+
"""Parse string literal into Python value (int, float, bool, None, or str).
|
|
646
|
+
|
|
647
|
+
Converts SQL/JSON-like literal strings into appropriate Python types:
|
|
648
|
+
- Quoted strings ('foo', "bar") -> str
|
|
649
|
+
- true/false -> bool
|
|
650
|
+
- null/none -> None
|
|
651
|
+
- Numeric strings -> int or float
|
|
652
|
+
- Everything else -> str
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
s: String literal to parse
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
Parsed Python value
|
|
659
|
+
|
|
660
|
+
Examples:
|
|
661
|
+
>>> BaseDataSource._coerce_literal("'hello'")
|
|
662
|
+
'hello'
|
|
663
|
+
>>> BaseDataSource._coerce_literal("42")
|
|
664
|
+
42
|
|
665
|
+
>>> BaseDataSource._coerce_literal("3.14")
|
|
666
|
+
3.14
|
|
667
|
+
>>> BaseDataSource._coerce_literal("true")
|
|
668
|
+
True
|
|
669
|
+
>>> BaseDataSource._coerce_literal("null")
|
|
670
|
+
None
|
|
671
|
+
"""
|
|
672
|
+
t = s.strip()
|
|
673
|
+
|
|
674
|
+
# Handle quoted strings
|
|
675
|
+
if len(t) >= 2 and ((t[0] == t[-1] == "'") or (t[0] == t[-1] == '"')):
|
|
676
|
+
return t[1:-1]
|
|
677
|
+
|
|
678
|
+
# Handle booleans
|
|
679
|
+
if t.lower() == "true":
|
|
680
|
+
return True
|
|
681
|
+
if t.lower() == "false":
|
|
682
|
+
return False
|
|
683
|
+
|
|
684
|
+
# Handle null/none
|
|
685
|
+
if t.lower() in ("null", "none"):
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
# Try numeric conversion
|
|
689
|
+
try:
|
|
690
|
+
return int(t)
|
|
691
|
+
except Exception:
|
|
692
|
+
pass
|
|
693
|
+
|
|
694
|
+
try:
|
|
695
|
+
return float(t)
|
|
696
|
+
except Exception:
|
|
697
|
+
pass
|
|
698
|
+
|
|
699
|
+
# Default to string
|
|
700
|
+
return t
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def _validate_record(record: Any) -> None:
|
|
704
|
+
"""Validate that a record is a dictionary.
|
|
705
|
+
|
|
706
|
+
Args:
|
|
707
|
+
record: Value to validate
|
|
708
|
+
|
|
709
|
+
Raises:
|
|
710
|
+
ValueError: If record is not a dictionary
|
|
711
|
+
"""
|
|
712
|
+
if not isinstance(record, dict):
|
|
713
|
+
raise ValueError(f"Record must be a dictionary, got {type(record).__name__}")
|
|
714
|
+
|
|
715
|
+
# ========== HOOK METHODS (Can be overridden by subclasses) ==========
|
|
716
|
+
|
|
717
|
+
def _before_create(self, record: Dict[str, Any]) -> Dict[str, Any]:
|
|
718
|
+
"""Hook called before creating a record.
|
|
719
|
+
|
|
720
|
+
Subclasses can override this to perform validation, transformation,
|
|
721
|
+
or side effects before record creation.
|
|
722
|
+
|
|
723
|
+
Args:
|
|
724
|
+
record: Record about to be created
|
|
725
|
+
|
|
726
|
+
Returns:
|
|
727
|
+
Modified record (or original if no changes)
|
|
728
|
+
"""
|
|
729
|
+
return record
|
|
730
|
+
|
|
731
|
+
def _after_create(self, record_id: int, record: Dict[str, Any]) -> None:
|
|
732
|
+
"""Hook called after creating a record.
|
|
733
|
+
|
|
734
|
+
Subclasses can override this to perform logging, caching updates,
|
|
735
|
+
or other side effects after record creation.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
record_id: ID of the created record
|
|
739
|
+
record: The created record
|
|
740
|
+
"""
|
|
741
|
+
pass
|
|
742
|
+
|
|
743
|
+
def _before_update(self, record_id: Any, updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
744
|
+
"""Hook called before updating a record.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
record_id: ID of record being updated
|
|
748
|
+
updates: Updates to apply
|
|
749
|
+
|
|
750
|
+
Returns:
|
|
751
|
+
Modified updates (or original if no changes)
|
|
752
|
+
"""
|
|
753
|
+
return updates
|
|
754
|
+
|
|
755
|
+
def _after_update(self, record_id: Any, updates: Dict[str, Any], success: bool) -> None:
|
|
756
|
+
"""Hook called after updating a record.
|
|
757
|
+
|
|
758
|
+
Args:
|
|
759
|
+
record_id: ID of updated record
|
|
760
|
+
updates: Updates that were applied
|
|
761
|
+
success: Whether the update succeeded
|
|
762
|
+
"""
|
|
763
|
+
pass
|
|
764
|
+
|
|
765
|
+
def _before_delete(self, record_id: Any) -> None:
|
|
766
|
+
"""Hook called before deleting a record.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
record_id: ID of record about to be deleted
|
|
770
|
+
"""
|
|
771
|
+
pass
|
|
772
|
+
|
|
773
|
+
def _after_delete(self, record_id: Any, success: bool) -> None:
|
|
774
|
+
"""Hook called after deleting a record.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
record_id: ID of deleted record
|
|
778
|
+
success: Whether the deletion succeeded
|
|
779
|
+
"""
|
|
780
|
+
pass
|