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/types.py
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""Type definitions and protocols for bootstack datasource module.
|
|
2
|
+
|
|
3
|
+
This module defines the core protocol that all datasource implementations must follow,
|
|
4
|
+
enabling consistent data management across different backends (memory, SQLite, web, etc.).
|
|
5
|
+
|
|
6
|
+
The DataSourceProtocol provides a unified interface for:
|
|
7
|
+
- Data loading and configuration (load, where, order)
|
|
8
|
+
- Pagination (page, next_page, prev_page, has_next_page)
|
|
9
|
+
- CRUD operations (insert, get, update, delete)
|
|
10
|
+
- Selection management (is_selected, select/deselect records)
|
|
11
|
+
- Data export (CSV export)
|
|
12
|
+
- Index-based access (page_slice)
|
|
13
|
+
- Lifecycle and reorder (reload, move)
|
|
14
|
+
|
|
15
|
+
All datasource implementations should conform to this protocol to ensure
|
|
16
|
+
compatibility with datasource-aware widgets and utilities.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import (
|
|
22
|
+
TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Sequence, Mapping,
|
|
23
|
+
runtime_checkable,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from bootstack.data.query import Column, Condition, SortKey
|
|
28
|
+
|
|
29
|
+
# Type aliases for clarity
|
|
30
|
+
Primitive = Any # Can be str, int, float, bool, None, etc.
|
|
31
|
+
Record = Dict[str, Any]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@runtime_checkable
|
|
35
|
+
class DataSourceProtocol(Protocol):
|
|
36
|
+
"""Protocol defining the interface for data source implementations.
|
|
37
|
+
|
|
38
|
+
This protocol establishes a contract that all datasource backends must implement,
|
|
39
|
+
ensuring consistent behavior across different storage mechanisms (memory, database,
|
|
40
|
+
web API, etc.).
|
|
41
|
+
|
|
42
|
+
All datasource implementations must support:
|
|
43
|
+
- Pagination with configurable page size
|
|
44
|
+
- Filtering with `col`-based conditions (see `bootstack.data.query`)
|
|
45
|
+
- Sorting by one or more columns
|
|
46
|
+
- Full CRUD operations on records
|
|
47
|
+
- Selection tracking for multi-select scenarios
|
|
48
|
+
- CSV export capabilities
|
|
49
|
+
- Direct index-based data access
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
- Records are represented as Dict[str, Any] with at least 'id' and 'selected' fields
|
|
53
|
+
- Filtering and sorting are expressed with the `col` expression API, not SQL
|
|
54
|
+
- All methods preserve immutability - operations return new data or modify in-place
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# public attrs
|
|
58
|
+
page_size: int
|
|
59
|
+
"""Number of records per page."""
|
|
60
|
+
|
|
61
|
+
# ---------- data & view config ----------
|
|
62
|
+
def load(self, records: Sequence[Primitive] | Sequence[Mapping[str, Any]]) -> DataSourceProtocol:
|
|
63
|
+
"""Load data records into the datasource.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
records: Sequence of records (dicts) or primitives (auto-wrapped)
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Self for method chaining
|
|
70
|
+
"""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
def where(self, condition: "Condition | None" = None) -> DataSourceProtocol:
|
|
74
|
+
"""Filter rows by a condition built with `col`.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
condition: A filter condition (e.g. `col("age") >= 25`), or `None`
|
|
78
|
+
/ no argument to clear the filter.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Self for method chaining
|
|
82
|
+
"""
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
def order(self, *keys: "str | Column | SortKey") -> DataSourceProtocol:
|
|
86
|
+
"""Sort rows by one or more keys.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
keys: Column names (`"name"`), descending names (`"-name"`), or
|
|
90
|
+
`col(...)` specs (`col("salary").desc()`). With no arguments,
|
|
91
|
+
clears sorting.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Self for method chaining
|
|
95
|
+
"""
|
|
96
|
+
...
|
|
97
|
+
|
|
98
|
+
# ---------- pagination ----------
|
|
99
|
+
def page(self, page: Optional[int] = None) -> List[Record]:
|
|
100
|
+
"""Get records for specified page (or current page if None).
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
page: Page number (0-indexed); updates current page if provided
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of record dictionaries for the page
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
def next_page(self) -> List[Record]:
|
|
111
|
+
"""Advance to next page and return its records.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of record dictionaries for the new page
|
|
115
|
+
"""
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
def prev_page(self) -> List[Record]:
|
|
119
|
+
"""Move to previous page and return its records.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
List of record dictionaries for the new page
|
|
123
|
+
"""
|
|
124
|
+
...
|
|
125
|
+
|
|
126
|
+
def has_next_page(self) -> bool:
|
|
127
|
+
"""Check if more pages exist after current page.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
True if next page exists, False otherwise
|
|
131
|
+
"""
|
|
132
|
+
...
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def count(self) -> int:
|
|
136
|
+
"""Total number of records matching the current filter."""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
# ---------- CRUD ----------
|
|
140
|
+
def insert(self, record: Dict[str, Any]) -> int:
|
|
141
|
+
"""Create new record and return its ID.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
record: Dictionary with record data
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
The ID assigned to the new record
|
|
148
|
+
"""
|
|
149
|
+
...
|
|
150
|
+
|
|
151
|
+
def get(self, record_id: Any) -> Optional[Record]:
|
|
152
|
+
"""Retrieve single record by ID.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
record_id: Unique identifier of the record
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Record dictionary or None if not found
|
|
159
|
+
"""
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
def update(self, record_id: Any, updates: Dict[str, Any]) -> bool:
|
|
163
|
+
"""Update record fields by ID.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
record_id: Unique identifier of the record
|
|
167
|
+
updates: Dictionary with fields to update
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if record was updated, False if not found
|
|
171
|
+
"""
|
|
172
|
+
...
|
|
173
|
+
|
|
174
|
+
def delete(self, record_id: Any) -> bool:
|
|
175
|
+
"""Delete record by ID.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
record_id: Unique identifier of the record
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
True if record was deleted, False if not found
|
|
182
|
+
"""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
# ---------- selection ----------
|
|
186
|
+
def is_selected(self, record_id: Any) -> bool:
|
|
187
|
+
"""Check whether a record is currently selected.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
record_id: Unique identifier of the record
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if the record is selected, False otherwise (including missing records)
|
|
194
|
+
"""
|
|
195
|
+
...
|
|
196
|
+
|
|
197
|
+
def select(self, record_id: Any) -> bool:
|
|
198
|
+
"""Mark record as selected.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
record_id: Unique identifier of the record
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
True if record was selected, False if not found
|
|
205
|
+
"""
|
|
206
|
+
...
|
|
207
|
+
|
|
208
|
+
def deselect(self, record_id: Any) -> bool:
|
|
209
|
+
"""Mark record as unselected.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
record_id: Unique identifier of the record
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
True if record was deselected, False if not found
|
|
216
|
+
"""
|
|
217
|
+
...
|
|
218
|
+
|
|
219
|
+
def select_all(self, current_page_only: bool = False) -> int:
|
|
220
|
+
"""Select all records (optionally only current page).
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
current_page_only: If True, select only records on current page
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Number of records selected
|
|
227
|
+
"""
|
|
228
|
+
...
|
|
229
|
+
|
|
230
|
+
def deselect_all(self, current_page_only: bool = False) -> int:
|
|
231
|
+
"""Deselect all records (optionally only current page).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
current_page_only: If True, deselect only records on current page
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Number of records deselected
|
|
238
|
+
"""
|
|
239
|
+
...
|
|
240
|
+
|
|
241
|
+
def selected(self, page: Optional[int] = None) -> List[Record]:
|
|
242
|
+
"""Get selected records, optionally paginated.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
page: Optional page number for paginating selected records
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
List of selected record dictionaries
|
|
249
|
+
"""
|
|
250
|
+
...
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def selected_count(self) -> int:
|
|
254
|
+
"""Number of selected records."""
|
|
255
|
+
...
|
|
256
|
+
|
|
257
|
+
# ---------- lifecycle / reorder ----------
|
|
258
|
+
def reload(self) -> None:
|
|
259
|
+
"""Re-read data from the underlying source.
|
|
260
|
+
|
|
261
|
+
For in-memory implementations this is typically a no-op. For
|
|
262
|
+
file- or database-backed sources, this re-queries or re-reads.
|
|
263
|
+
"""
|
|
264
|
+
...
|
|
265
|
+
|
|
266
|
+
def close(self) -> None:
|
|
267
|
+
"""Release any resources held by the source (a connection, file handle).
|
|
268
|
+
|
|
269
|
+
A no-op for in-memory sources. Sources are also context managers, so a
|
|
270
|
+
`with` block closes automatically. Safe to call more than once.
|
|
271
|
+
"""
|
|
272
|
+
...
|
|
273
|
+
|
|
274
|
+
def move(self, record_id: Any, target_index: int) -> bool:
|
|
275
|
+
"""Reorder a record to a new position.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
record_id: Unique identifier of the record to move
|
|
279
|
+
target_index: Zero-based destination index (clamped to valid range)
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
True if the record was moved, False if not supported or not found
|
|
283
|
+
"""
|
|
284
|
+
...
|
|
285
|
+
|
|
286
|
+
# ---------- export ----------
|
|
287
|
+
def save(self, path: str, *, selected_only: bool = False, format: Optional[str] = None, config: Any = None) -> None:
|
|
288
|
+
"""Export records to a file, format chosen by the path extension (or `format`).
|
|
289
|
+
|
|
290
|
+
Records are streamed into the writer; the active `where`/`order` view is
|
|
291
|
+
respected. Built-in formats: CSV, TSV, JSON, JSONL, XML — plus Parquet,
|
|
292
|
+
Feather, and HDF5 with the matching extra installed.
|
|
293
|
+
"""
|
|
294
|
+
...
|
|
295
|
+
|
|
296
|
+
def export_csv(self, filepath: str, include_all: bool = True) -> None:
|
|
297
|
+
"""Export records to CSV file.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
filepath: Path to output CSV file
|
|
301
|
+
include_all: If True, export all records; if False, export only selected
|
|
302
|
+
"""
|
|
303
|
+
...
|
|
304
|
+
|
|
305
|
+
# ---------- index-based paging ----------
|
|
306
|
+
def page_slice(self, start_index: int, count: int) -> List[Record]:
|
|
307
|
+
"""Get records by start index and count (respects filter/sort).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
start_index: Starting record index
|
|
311
|
+
count: Number of records to retrieve
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
List of record dictionaries
|
|
315
|
+
"""
|
|
316
|
+
...
|
|
317
|
+
|
|
318
|
+
# ---------- row identity ----------
|
|
319
|
+
@property
|
|
320
|
+
def id_field(self) -> str:
|
|
321
|
+
"""Name of the record field that holds the stable row identity."""
|
|
322
|
+
...
|
|
323
|
+
|
|
324
|
+
def _internal_fields(self) -> "frozenset[str]":
|
|
325
|
+
"""Raw-record keys that are implementation-internal and hidden from
|
|
326
|
+
users. Datasource-aware widgets strip these from columns and exports."""
|
|
327
|
+
...
|
|
328
|
+
|
|
329
|
+
def _record_id(self, record: Any) -> Any:
|
|
330
|
+
"""Stable identity of a raw record — what `get`/`update`/`delete`/
|
|
331
|
+
`select` accept and what is surfaced publicly as `id`."""
|
|
332
|
+
...
|
|
333
|
+
|
|
334
|
+
def _public_record(self, record: Any) -> Record:
|
|
335
|
+
"""User-facing copy of a raw record: internal fields stripped, `id`
|
|
336
|
+
surfaced."""
|
|
337
|
+
...
|
|
338
|
+
|
|
339
|
+
# ---------- change broadcasting ----------
|
|
340
|
+
def on_change(self, handler: Any = None) -> Any:
|
|
341
|
+
"""Subscribe to changes. Returns a `Stream` (no handler) or a
|
|
342
|
+
cancellable subscription (with handler). The handler receives a
|
|
343
|
+
`DataChangeEvent`."""
|
|
344
|
+
...
|
|
345
|
+
|
|
346
|
+
def observe(self, condition: "Condition | None" = None, *order: Any) -> Any:
|
|
347
|
+
"""Observe a live result set for a `where`/`order` query. Returns a
|
|
348
|
+
`Stream` of result sets that re-emits on relevant changes."""
|
|
349
|
+
...
|
|
350
|
+
|
|
351
|
+
def _query(self, condition: "Condition | None", sort_keys: List[Any]) -> List[Record]:
|
|
352
|
+
"""Run a one-off filtered/sorted read without disturbing the source's
|
|
353
|
+
active `where`/`order` view state. Backs `observe()`."""
|
|
354
|
+
...
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Streaming record writers — the export counterpart of `readers`.
|
|
2
|
+
|
|
3
|
+
A *writer* consumes a lazy iterable of record dicts and writes them to a file,
|
|
4
|
+
streaming where the format allows so a large export does not materialize the
|
|
5
|
+
whole dataset. Writers are keyed by file extension in a registry: the stdlib
|
|
6
|
+
formats (CSV / TSV / JSON / JSONL / XML) are always available, and the optional
|
|
7
|
+
columnar/scientific formats (Parquet / Feather / HDF5) register too but raise a
|
|
8
|
+
clear "install the extra" error when used without their dependency.
|
|
9
|
+
|
|
10
|
+
The contract mirrors the readers — `writer(path, records, config) -> None` — so
|
|
11
|
+
it pairs with `read_records` for round-trips, and backs the source-level
|
|
12
|
+
`save()` / `export_csv()`.
|
|
13
|
+
|
|
14
|
+
Register a custom writer for your own extension::
|
|
15
|
+
|
|
16
|
+
from bootstack.data.writers import register_writer
|
|
17
|
+
|
|
18
|
+
@register_writer(".myfmt")
|
|
19
|
+
def write_myfmt(path, records, config=None):
|
|
20
|
+
with open(path, "w") as f:
|
|
21
|
+
for record in records:
|
|
22
|
+
f.write(serialize(record))
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import csv
|
|
28
|
+
import importlib
|
|
29
|
+
import json
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional
|
|
32
|
+
from xml.sax.saxutils import escape as _xml_escape
|
|
33
|
+
|
|
34
|
+
from bootstack.data.types import Record
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from bootstack.data.file_source import FileSourceConfig
|
|
38
|
+
|
|
39
|
+
# A writer consumes record dicts: (path, records, config) -> None.
|
|
40
|
+
Writer = Callable[..., None]
|
|
41
|
+
|
|
42
|
+
_WRITERS: Dict[str, Writer] = {}
|
|
43
|
+
|
|
44
|
+
# Format-name aliases (FileSourceConfig.file_format) → canonical extension.
|
|
45
|
+
_FORMAT_ALIASES: Dict[str, str] = {
|
|
46
|
+
"csv": ".csv",
|
|
47
|
+
"tsv": ".tsv",
|
|
48
|
+
"json": ".json",
|
|
49
|
+
"jsonl": ".jsonl",
|
|
50
|
+
"ndjson": ".jsonl",
|
|
51
|
+
"xml": ".xml",
|
|
52
|
+
"parquet": ".parquet",
|
|
53
|
+
"feather": ".feather",
|
|
54
|
+
"hdf5": ".h5",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _norm_ext(ext: str) -> str:
|
|
59
|
+
ext = ext.lower()
|
|
60
|
+
return ext if ext.startswith(".") else "." + ext
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def register_writer(*extensions: str, writer: Optional[Writer] = None):
|
|
64
|
+
"""Register a writer for one or more file extensions (decorator or direct)."""
|
|
65
|
+
|
|
66
|
+
def _apply(fn: Writer) -> Writer:
|
|
67
|
+
for ext in extensions:
|
|
68
|
+
_WRITERS[_norm_ext(ext)] = fn
|
|
69
|
+
return fn
|
|
70
|
+
|
|
71
|
+
return _apply(writer) if writer is not None else _apply
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def supported_write_extensions() -> List[str]:
|
|
75
|
+
"""Return the sorted list of registered writer extensions."""
|
|
76
|
+
return sorted(_WRITERS)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_writer(path_or_ext: "str | Path") -> Writer:
|
|
80
|
+
"""Resolve the writer for a path or extension.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
ValueError: If no writer is registered for the extension.
|
|
84
|
+
"""
|
|
85
|
+
suffix = Path(str(path_or_ext)).suffix or str(path_or_ext)
|
|
86
|
+
ext = _norm_ext(suffix)
|
|
87
|
+
try:
|
|
88
|
+
return _WRITERS[ext]
|
|
89
|
+
except KeyError:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"No writer registered for {ext!r}. Supported formats: "
|
|
92
|
+
f"{', '.join(supported_write_extensions())}."
|
|
93
|
+
) from None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def write_records(
|
|
97
|
+
path: "str | Path",
|
|
98
|
+
records: "Iterable[Record]",
|
|
99
|
+
config: "Optional[FileSourceConfig]" = None,
|
|
100
|
+
*,
|
|
101
|
+
format: Optional[str] = None,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Write records to a file, choosing the writer by `format` or extension.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
path: Destination file path.
|
|
107
|
+
records: An iterable of record dicts (consumed lazily).
|
|
108
|
+
config: Optional formatting configuration (encoding, delimiter, …).
|
|
109
|
+
format: Explicit format name overriding the path extension.
|
|
110
|
+
"""
|
|
111
|
+
if format:
|
|
112
|
+
writer = get_writer(_FORMAT_ALIASES.get(format, format))
|
|
113
|
+
else:
|
|
114
|
+
writer = get_writer(path)
|
|
115
|
+
writer(path, records, config)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _require(module: str, extra: str):
|
|
119
|
+
"""Import an optional dependency or raise a clear install hint."""
|
|
120
|
+
try:
|
|
121
|
+
return importlib.import_module(module)
|
|
122
|
+
except ImportError as exc:
|
|
123
|
+
raise ImportError(
|
|
124
|
+
f"Writing this format needs the optional {module!r} dependency. "
|
|
125
|
+
f"Install it with: pip install bootstack[{extra}]"
|
|
126
|
+
) from exc
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _encoding(config: "Optional[FileSourceConfig]") -> str:
|
|
130
|
+
return getattr(config, "encoding", "utf-8") or "utf-8"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------------------------------- stdlib
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@register_writer(".csv", ".tsv", ".txt")
|
|
137
|
+
def write_csv(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
138
|
+
"""Write records to CSV/TSV, streaming a row at a time.
|
|
139
|
+
|
|
140
|
+
Column headers are taken from the first record; later records are written with
|
|
141
|
+
those columns (missing fields blank, extra fields ignored).
|
|
142
|
+
"""
|
|
143
|
+
delimiter = getattr(config, "delimiter", None)
|
|
144
|
+
if not delimiter:
|
|
145
|
+
delimiter = "\t" if _norm_ext(Path(path).suffix) == ".tsv" else ","
|
|
146
|
+
|
|
147
|
+
it = iter(records)
|
|
148
|
+
try:
|
|
149
|
+
first = next(it)
|
|
150
|
+
except StopIteration:
|
|
151
|
+
open(path, "w", encoding=_encoding(config)).close() # empty export
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
fieldnames = list(first.keys())
|
|
155
|
+
with open(path, "w", newline="", encoding=_encoding(config)) as f:
|
|
156
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=delimiter, extrasaction="ignore")
|
|
157
|
+
writer.writeheader()
|
|
158
|
+
writer.writerow(first)
|
|
159
|
+
for record in it:
|
|
160
|
+
writer.writerow(record)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@register_writer(".jsonl", ".ndjson")
|
|
164
|
+
def write_jsonl(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
165
|
+
"""Write records as line-delimited JSON (one object per line) — fully streamed."""
|
|
166
|
+
with open(path, "w", encoding=_encoding(config)) as f:
|
|
167
|
+
for record in records:
|
|
168
|
+
f.write(json.dumps(record))
|
|
169
|
+
f.write("\n")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@register_writer(".json")
|
|
173
|
+
def write_json(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
174
|
+
"""Write records as a JSON array, streamed element by element."""
|
|
175
|
+
with open(path, "w", encoding=_encoding(config)) as f:
|
|
176
|
+
f.write("[")
|
|
177
|
+
first = True
|
|
178
|
+
for record in records:
|
|
179
|
+
if not first:
|
|
180
|
+
f.write(", ")
|
|
181
|
+
f.write(json.dumps(record))
|
|
182
|
+
first = False
|
|
183
|
+
f.write("]")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@register_writer(".xml")
|
|
187
|
+
def write_xml(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
188
|
+
"""Write records as XML, streaming one record element at a time.
|
|
189
|
+
|
|
190
|
+
Each record becomes a `<record>` element (or `config.xml_record_tag`) whose
|
|
191
|
+
child elements are the record's fields.
|
|
192
|
+
"""
|
|
193
|
+
record_tag = getattr(config, "xml_record_tag", None) or "record"
|
|
194
|
+
root_tag = "records"
|
|
195
|
+
with open(path, "w", encoding=_encoding(config)) as f:
|
|
196
|
+
f.write(f"<{root_tag}>")
|
|
197
|
+
for record in records:
|
|
198
|
+
f.write(f"<{record_tag}>")
|
|
199
|
+
for key, value in record.items():
|
|
200
|
+
text = "" if value is None else _xml_escape(str(value))
|
|
201
|
+
f.write(f"<{key}>{text}</{key}>")
|
|
202
|
+
f.write(f"</{record_tag}>")
|
|
203
|
+
f.write(f"</{root_tag}>")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# --------------------------------------------------------------------------- optional
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@register_writer(".parquet")
|
|
210
|
+
def write_parquet(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
211
|
+
"""Write records to a Parquet file (needs pyarrow)."""
|
|
212
|
+
pa = _require("pyarrow", "parquet")
|
|
213
|
+
import pyarrow.parquet as pq
|
|
214
|
+
|
|
215
|
+
pq.write_table(pa.Table.from_pylist(list(records)), str(path))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@register_writer(".feather", ".arrow")
|
|
219
|
+
def write_feather(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
220
|
+
"""Write records to a Feather/Arrow IPC file (needs pyarrow)."""
|
|
221
|
+
pa = _require("pyarrow", "parquet")
|
|
222
|
+
import pyarrow.feather as feather
|
|
223
|
+
|
|
224
|
+
feather.write_feather(pa.Table.from_pylist(list(records)), str(path))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@register_writer(".h5", ".hdf5", ".hdf")
|
|
228
|
+
def write_hdf5(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
|
|
229
|
+
"""Write records to a tabular HDF5 file (needs pandas + tables)."""
|
|
230
|
+
pd = _require("pandas", "hdf5")
|
|
231
|
+
key = getattr(config, "hdf5_key", None) or "data"
|
|
232
|
+
pd.DataFrame(list(records)).to_hdf(str(path), key=key, format="table")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Developer tooling for bootstack — hot reload.
|
|
2
|
+
|
|
3
|
+
.. warning::
|
|
4
|
+
|
|
5
|
+
**Provisional.** This module ships in 0.1.0 but is *carved out of the API
|
|
6
|
+
freeze*: unlike the ``bootstack`` compose surface, it may change in a minor
|
|
7
|
+
release while the dev workflow settles. Pin a version if you depend on it.
|
|
8
|
+
|
|
9
|
+
Run an app with live reload::
|
|
10
|
+
|
|
11
|
+
bootstack dev app.py
|
|
12
|
+
|
|
13
|
+
On save, a module-level ``with bs.App()`` app updates in place — the window,
|
|
14
|
+
your signals, and any module-level datasources survive; only the UI the body
|
|
15
|
+
built is rebuilt. State that should persist across reloads lives **above** the
|
|
16
|
+
``with`` block (module level); the block body is what reloads.
|
|
17
|
+
|
|
18
|
+
For multi-file apps, mark a page builder so only its region rebuilds when its
|
|
19
|
+
file changes::
|
|
20
|
+
|
|
21
|
+
from bootstack.dev import reloadable
|
|
22
|
+
|
|
23
|
+
@reloadable
|
|
24
|
+
def build_dashboard(db):
|
|
25
|
+
bs.Label("Dashboard", font="heading-lg")
|
|
26
|
+
bs.DataTable(data_source=db)
|
|
27
|
+
|
|
28
|
+
Outside ``bootstack dev`` everything here is inert: :func:`reloadable` returns
|
|
29
|
+
the function unchanged and :func:`is_dev_mode` is False.
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from bootstack.dev._env import is_dev_mode
|
|
34
|
+
from bootstack.dev._registry import reloadable
|
|
35
|
+
|
|
36
|
+
__all__ = ["reloadable", "is_dev_mode"]
|