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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""File-backed data source — streams a file into a SQLite working store.
|
|
2
|
+
|
|
3
|
+
`FileDataSource` reads CSV / TSV / JSON / JSONL (and, with the optional extras,
|
|
4
|
+
Parquet / Feather / HDF5) and ingests it — a chunk at a time, with bounded
|
|
5
|
+
memory — into a SQLite working store. After loading it *is* a
|
|
6
|
+
`SqliteDataSource`, so paging, filtering, sorting, and CRUD are all fast SQL at
|
|
7
|
+
flat memory, no matter how large the file.
|
|
8
|
+
|
|
9
|
+
The original file is treated as **read-only input**: edits live in the working
|
|
10
|
+
store, never written back. Save changes by exporting to a new file.
|
|
11
|
+
|
|
12
|
+
Working store (where the ingested data lives):
|
|
13
|
+
- default: a temporary on-disk SQLite file, removed on `close()` — bounded
|
|
14
|
+
memory even for millions of rows.
|
|
15
|
+
- `cache="path.db"`: a named, persistent store. Edits survive restarts, and
|
|
16
|
+
re-opening skips re-ingest while the cache is newer than the source file.
|
|
17
|
+
- `cache=":memory:"`: an in-memory store — compact, but RAM-bound.
|
|
18
|
+
|
|
19
|
+
Transformation pipeline (applied per record during ingest):
|
|
20
|
+
column selection / renames / default values / type conversions / per-column
|
|
21
|
+
transforms / row filter / row transform.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
.. code-block:: python
|
|
25
|
+
|
|
26
|
+
ds = FileDataSource("data.csv")
|
|
27
|
+
ds.load()
|
|
28
|
+
ds.where(col("age") > 25)
|
|
29
|
+
first = ds.page(0)
|
|
30
|
+
ds.close() # or use `with FileDataSource(...) as ds:`
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import sqlite3
|
|
37
|
+
import tempfile
|
|
38
|
+
import weakref
|
|
39
|
+
from dataclasses import dataclass
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Type
|
|
42
|
+
|
|
43
|
+
from bootstack.data.sqlite_source import SqliteDataSource
|
|
44
|
+
from bootstack.data.types import Record
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cleanup_temp_store(conn: "sqlite3.Connection", path: str) -> None:
|
|
48
|
+
"""Close the connection and remove a temporary working-store file.
|
|
49
|
+
|
|
50
|
+
Registered as a `weakref.finalize` callback so a `FileDataSource` whose
|
|
51
|
+
`close()` was never called still cleans up its temp file when garbage
|
|
52
|
+
collected or at interpreter exit — no orphaned files left behind. Takes the
|
|
53
|
+
connection and path directly (never the source) so it holds no strong
|
|
54
|
+
reference that would keep the source alive.
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
conn.close()
|
|
58
|
+
except Exception:
|
|
59
|
+
pass
|
|
60
|
+
try:
|
|
61
|
+
os.remove(path)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class FileSourceConfig:
|
|
68
|
+
"""Configuration for file parsing and the per-record transformation pipeline.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
.. code-block:: python
|
|
72
|
+
|
|
73
|
+
config = FileSourceConfig(
|
|
74
|
+
column_renames={'emp_id': 'id'},
|
|
75
|
+
column_types={'age': int},
|
|
76
|
+
)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
file_format: Literal[
|
|
80
|
+
'auto', 'csv', 'tsv', 'json', 'jsonl', 'ndjson', 'xml', 'parquet', 'feather', 'hdf5'
|
|
81
|
+
] = 'auto'
|
|
82
|
+
"""Format override; auto-detected from the extension when `'auto'`."""
|
|
83
|
+
|
|
84
|
+
encoding: str = 'utf-8'
|
|
85
|
+
"""Character encoding for reading the file."""
|
|
86
|
+
|
|
87
|
+
delimiter: Optional[str] = None
|
|
88
|
+
"""Field separator. `None` auto-selects `','` for CSV and a tab for TSV."""
|
|
89
|
+
|
|
90
|
+
quotechar: str = '"'
|
|
91
|
+
"""Quote character for fields containing the delimiter."""
|
|
92
|
+
|
|
93
|
+
skip_rows: int = 0
|
|
94
|
+
"""Number of leading rows to skip before the header (CSV/TSV)."""
|
|
95
|
+
|
|
96
|
+
header_row: Optional[int] = 0
|
|
97
|
+
"""Row index containing column names (`None` = no header)."""
|
|
98
|
+
|
|
99
|
+
has_header: bool = True
|
|
100
|
+
"""Whether the first row contains column names."""
|
|
101
|
+
|
|
102
|
+
json_lines: bool = False
|
|
103
|
+
"""`True` for line-delimited JSON (JSONL/NDJSON)."""
|
|
104
|
+
|
|
105
|
+
json_records_key: Optional[str] = None
|
|
106
|
+
"""Key whose value is the records list in a JSON object (e.g. `'data'` for
|
|
107
|
+
`{'data': [...]}`); `None` = a top-level array, or the object itself as one
|
|
108
|
+
record."""
|
|
109
|
+
|
|
110
|
+
xml_record_tag: Optional[str] = None
|
|
111
|
+
"""Element tag that marks one record (`None` = direct children of the root)."""
|
|
112
|
+
|
|
113
|
+
hdf5_key: Optional[str] = None
|
|
114
|
+
"""Dataset/table key to read from an HDF5 file (`None` = the first key)."""
|
|
115
|
+
|
|
116
|
+
column_renames: Optional[Dict[str, str]] = None
|
|
117
|
+
"""Mapping from each existing column name to its replacement."""
|
|
118
|
+
|
|
119
|
+
column_types: Optional[Dict[str, Type]] = None
|
|
120
|
+
"""Mapping from a column name to the target type to convert its values to."""
|
|
121
|
+
|
|
122
|
+
column_transforms: Optional[Dict[str, Callable[[Any], Any]]] = None
|
|
123
|
+
"""Mapping from a column name to a transform applied to its values."""
|
|
124
|
+
|
|
125
|
+
columns_to_load: Optional[List[str]] = None
|
|
126
|
+
"""List of columns to keep (`None` = all columns)."""
|
|
127
|
+
|
|
128
|
+
default_values: Optional[Dict[str, Any]] = None
|
|
129
|
+
"""Mapping from a column name to a fill value for missing or null entries."""
|
|
130
|
+
|
|
131
|
+
row_filter: Optional[Callable[[Dict[str, Any]], bool]] = None
|
|
132
|
+
"""Function `(row_dict) -> bool` to filter rows during load."""
|
|
133
|
+
|
|
134
|
+
row_transform: Optional[Callable[[Dict[str, Any]], Dict[str, Any]]] = None
|
|
135
|
+
"""Function `(row_dict) -> row_dict` for row-level transforms."""
|
|
136
|
+
|
|
137
|
+
chunk_size: int = 10_000
|
|
138
|
+
"""Rows ingested per batch (bounds memory during load)."""
|
|
139
|
+
|
|
140
|
+
progress_callback: Optional[Callable[[int], None]] = None
|
|
141
|
+
"""Function `(count)` called after each ingested chunk with the running total
|
|
142
|
+
of rows loaded so far."""
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class FileDataSource(SqliteDataSource):
|
|
146
|
+
"""A `SqliteDataSource` whose data is streamed in from a file.
|
|
147
|
+
|
|
148
|
+
Reads a CSV / TSV / JSON / JSONL file (or an optional Parquet / Feather /
|
|
149
|
+
HDF5 file) and ingests it chunk-by-chunk into a SQLite working store, so even
|
|
150
|
+
a multi-million-row file loads with bounded memory. Once loaded it behaves
|
|
151
|
+
exactly like a `SqliteDataSource` — fast SQL paging, filtering, sorting, CRUD.
|
|
152
|
+
|
|
153
|
+
The original file is read-only input; edits live in the working store and are
|
|
154
|
+
never written back. Export to save changes.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
filepath: Path to the data file.
|
|
158
|
+
config: Optional `FileSourceConfig` for parsing and transforms.
|
|
159
|
+
page_size: Number of records returned per page.
|
|
160
|
+
cache: Working store location. None (default) uses a temporary on-disk
|
|
161
|
+
file removed on `close()`. A path names a persistent store whose data
|
|
162
|
+
survives restarts (and is reused without re-ingest while it is newer
|
|
163
|
+
than the source). `":memory:"` keeps the store in memory.
|
|
164
|
+
id_field: Record field used as the stable row identity.
|
|
165
|
+
|
|
166
|
+
Attributes:
|
|
167
|
+
filepath: Path to the data file.
|
|
168
|
+
config: Active configuration.
|
|
169
|
+
is_loaded: Whether the file has been ingested (or a fresh cache adopted).
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
.. code-block:: python
|
|
173
|
+
|
|
174
|
+
with FileDataSource("people.csv") as ds:
|
|
175
|
+
ds.load()
|
|
176
|
+
ds.where(col("age") > 25)
|
|
177
|
+
first = ds.page(0)
|
|
178
|
+
|
|
179
|
+
Note:
|
|
180
|
+
- `reload()` re-ingests from the file.
|
|
181
|
+
- Background-thread ingest and progressive display are a planned follow-up;
|
|
182
|
+
`load()` is currently synchronous (but streamed, so memory stays bounded).
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
filepath: str | Path,
|
|
188
|
+
config: Optional[FileSourceConfig] = None,
|
|
189
|
+
page_size: int = 10,
|
|
190
|
+
*,
|
|
191
|
+
cache: Optional[str] = None,
|
|
192
|
+
id_field: str = "id",
|
|
193
|
+
):
|
|
194
|
+
"""Configure a file-backed source and open its SQLite working store.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
FileNotFoundError: If the supplied file path cannot be found.
|
|
198
|
+
"""
|
|
199
|
+
self.filepath = Path(filepath)
|
|
200
|
+
self.config = config or FileSourceConfig()
|
|
201
|
+
if not self.filepath.exists():
|
|
202
|
+
raise FileNotFoundError(f"File not found: {self.filepath}")
|
|
203
|
+
|
|
204
|
+
# Choose the working store.
|
|
205
|
+
self._is_temp = False
|
|
206
|
+
self._cache_path: Optional[Path] = None
|
|
207
|
+
if cache is None:
|
|
208
|
+
fd, tmp = tempfile.mkstemp(suffix=".bsdb")
|
|
209
|
+
os.close(fd)
|
|
210
|
+
store_name = tmp
|
|
211
|
+
self._is_temp = True
|
|
212
|
+
elif cache == ":memory:":
|
|
213
|
+
store_name = ":memory:"
|
|
214
|
+
else:
|
|
215
|
+
store_name = str(cache)
|
|
216
|
+
self._cache_path = Path(cache)
|
|
217
|
+
self._store_name = store_name
|
|
218
|
+
|
|
219
|
+
super().__init__(name=store_name, page_size=page_size, id_field=id_field)
|
|
220
|
+
|
|
221
|
+
# Safety net: a temp store is removed even if close() is never called —
|
|
222
|
+
# the finalizer runs on garbage collection and at interpreter exit, so no
|
|
223
|
+
# orphaned temp files accumulate.
|
|
224
|
+
self._finalizer: Optional[weakref.finalize] = None
|
|
225
|
+
if self._is_temp:
|
|
226
|
+
self._finalizer = weakref.finalize(
|
|
227
|
+
self, _cleanup_temp_store, self.conn, store_name
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Reuse a fresh persistent cache without re-ingesting.
|
|
231
|
+
self.is_loaded = False
|
|
232
|
+
if self._cache_is_fresh():
|
|
233
|
+
self._adopt_existing_schema()
|
|
234
|
+
self.is_loaded = True
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------ loading
|
|
237
|
+
|
|
238
|
+
def load(self, *, force: bool = False) -> "FileDataSource":
|
|
239
|
+
"""Ingest the file into the working store (streamed, in chunks).
|
|
240
|
+
|
|
241
|
+
Unlike other sources, a `FileDataSource` draws its records from the file
|
|
242
|
+
given at construction, so `load()` takes no records. It is a no-op when a
|
|
243
|
+
fresh persistent cache was adopted, unless `force=True`.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
force: Re-ingest even if data is already present (used by `reload`).
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Self for method chaining.
|
|
250
|
+
"""
|
|
251
|
+
if self.is_loaded and not force:
|
|
252
|
+
return self
|
|
253
|
+
super().load(
|
|
254
|
+
self._iter_records(),
|
|
255
|
+
chunk_size=self.config.chunk_size,
|
|
256
|
+
on_progress=self._on_progress,
|
|
257
|
+
)
|
|
258
|
+
self.is_loaded = True
|
|
259
|
+
return self
|
|
260
|
+
|
|
261
|
+
def reload(self) -> None:
|
|
262
|
+
"""Re-ingest the file from disk, replacing the working store's contents."""
|
|
263
|
+
self.load(force=True)
|
|
264
|
+
|
|
265
|
+
def _iter_records(self) -> Iterator[Record]:
|
|
266
|
+
"""Stream parsed, transformed records from the file."""
|
|
267
|
+
from bootstack.data.readers import read_records
|
|
268
|
+
|
|
269
|
+
for raw in read_records(self.filepath, self.config):
|
|
270
|
+
record = self._apply_transformations(raw)
|
|
271
|
+
if record is not None:
|
|
272
|
+
yield record
|
|
273
|
+
|
|
274
|
+
def _on_progress(self, count: int) -> None:
|
|
275
|
+
if self.config.progress_callback:
|
|
276
|
+
self.config.progress_callback(count)
|
|
277
|
+
|
|
278
|
+
# ------------------------------------------------------------------ cache
|
|
279
|
+
|
|
280
|
+
def _cache_is_fresh(self) -> bool:
|
|
281
|
+
"""Whether a named cache already holds up-to-date data for the source."""
|
|
282
|
+
if self._cache_path is None:
|
|
283
|
+
return False
|
|
284
|
+
try:
|
|
285
|
+
if not self._cache_path.exists() or self._cache_path.stat().st_size == 0:
|
|
286
|
+
return False
|
|
287
|
+
if self._cache_path.stat().st_mtime < self.filepath.stat().st_mtime:
|
|
288
|
+
return False
|
|
289
|
+
count = self.conn.execute(f"SELECT COUNT(*) FROM {self._table}").fetchone()[0]
|
|
290
|
+
return count > 0
|
|
291
|
+
except sqlite3.Error:
|
|
292
|
+
return False
|
|
293
|
+
except OSError:
|
|
294
|
+
return False
|
|
295
|
+
|
|
296
|
+
def _adopt_existing_schema(self) -> None:
|
|
297
|
+
"""Populate the column list from an existing cache table (no re-ingest)."""
|
|
298
|
+
try:
|
|
299
|
+
cur = self.conn.execute(f"PRAGMA table_info({self._table})")
|
|
300
|
+
self._columns = [row["name"] for row in cur.fetchall()]
|
|
301
|
+
except sqlite3.Error:
|
|
302
|
+
self._columns = []
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------ lifecycle
|
|
305
|
+
|
|
306
|
+
def close(self) -> None:
|
|
307
|
+
"""Close the working store, removing the temporary file if one was used.
|
|
308
|
+
|
|
309
|
+
Idempotent. A temporary store is also cleaned automatically if this is
|
|
310
|
+
never called (see the finalizer registered in `__init__`).
|
|
311
|
+
"""
|
|
312
|
+
if self._finalizer is not None:
|
|
313
|
+
self._finalizer() # closes the connection and removes the temp file (once)
|
|
314
|
+
else:
|
|
315
|
+
super().close()
|
|
316
|
+
|
|
317
|
+
# ------------------------------------------------------------------ transforms
|
|
318
|
+
|
|
319
|
+
def _apply_transformations(self, record: Record) -> Optional[Record]:
|
|
320
|
+
"""Apply the configured transformation pipeline to a single record.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
The transformed record, or None if filtered out.
|
|
324
|
+
"""
|
|
325
|
+
# Apply row filter first.
|
|
326
|
+
if self.config.row_filter and not self.config.row_filter(record):
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# Column selection.
|
|
330
|
+
if self.config.columns_to_load:
|
|
331
|
+
record = {k: v for k, v in record.items() if k in self.config.columns_to_load}
|
|
332
|
+
|
|
333
|
+
# Column renames.
|
|
334
|
+
if self.config.column_renames:
|
|
335
|
+
for old_name, new_name in self.config.column_renames.items():
|
|
336
|
+
if old_name in record:
|
|
337
|
+
record[new_name] = record.pop(old_name)
|
|
338
|
+
|
|
339
|
+
# Default values.
|
|
340
|
+
if self.config.default_values:
|
|
341
|
+
for col, default in self.config.default_values.items():
|
|
342
|
+
if col not in record or record[col] is None:
|
|
343
|
+
record[col] = default
|
|
344
|
+
|
|
345
|
+
# Type conversions.
|
|
346
|
+
if self.config.column_types:
|
|
347
|
+
for col, col_type in self.config.column_types.items():
|
|
348
|
+
if col in record and record[col] is not None:
|
|
349
|
+
try:
|
|
350
|
+
record[col] = col_type(record[col])
|
|
351
|
+
except (ValueError, TypeError):
|
|
352
|
+
pass # Keep original value if conversion fails
|
|
353
|
+
|
|
354
|
+
# Per-column transforms.
|
|
355
|
+
if self.config.column_transforms:
|
|
356
|
+
for col, transform_func in self.config.column_transforms.items():
|
|
357
|
+
if col in record:
|
|
358
|
+
try:
|
|
359
|
+
record[col] = transform_func(record[col])
|
|
360
|
+
except Exception:
|
|
361
|
+
pass # Keep original value if transform fails
|
|
362
|
+
|
|
363
|
+
# Row-level transform.
|
|
364
|
+
if self.config.row_transform:
|
|
365
|
+
record = self.config.row_transform(record)
|
|
366
|
+
|
|
367
|
+
return record
|