bootstack 0.1.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bootstack/__init__.py +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -0
- bootstack/assets/bootstack.ico +0 -0
- bootstack/assets/bootstack.png +0 -0
- bootstack/assets/elements/__init__.py +0 -0
- bootstack/assets/elements/badge-pill.png +0 -0
- bootstack/assets/elements/badge-square.png +0 -0
- bootstack/assets/elements/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
- bootstack/assets/elements/checkbox-checked.png +0 -0
- bootstack/assets/elements/checkbox-indeterminate.png +0 -0
- bootstack/assets/elements/checkbox-unchecked.png +0 -0
- bootstack/assets/elements/field.png +0 -0
- bootstack/assets/elements/input-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-default.png +0 -0
- bootstack/assets/elements/input-compact.png +0 -0
- bootstack/assets/elements/input-default.png +0 -0
- bootstack/assets/elements/list-item-separated.png +0 -0
- bootstack/assets/elements/list-item.png +0 -0
- bootstack/assets/elements/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -0
- bootstack/core/mixins/__init__.py +1 -0
- bootstack/core/mixins/ttk_state.py +35 -0
- bootstack/core/mixins/widget.py +132 -0
- bootstack/core/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -0
- bootstack/style/builders_tk/__init__.py +16 -0
- bootstack/style/builders_tk/defaults.py +229 -0
- bootstack/style/element.py +173 -0
- bootstack/style/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Localization integration for bootstack.
|
|
3
|
+
|
|
4
|
+
Exports MessageCatalog, which bridges Tcl msgcat lookups with compiled
|
|
5
|
+
gettext (.mo) catalogs built via Babel.
|
|
6
|
+
"""
|
|
7
|
+
from .msgcat import MessageCatalog
|
|
8
|
+
from .intl_format import IntlFormatter
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"MessageCatalog",
|
|
12
|
+
"IntlFormatter",
|
|
13
|
+
]
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
"""International number/date formatting and parsing utilities.
|
|
2
|
+
|
|
3
|
+
Provides locale-aware formatting using Babel and pragmatic parsing that
|
|
4
|
+
prefers `dateparser` with a `python-dateutil` fallback. This module is
|
|
5
|
+
independent of translation and complements MessageCatalog by handling
|
|
6
|
+
locale-sensitive values.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import locale
|
|
12
|
+
import re
|
|
13
|
+
import warnings
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import date, datetime, time
|
|
16
|
+
from typing import Any, Literal, Mapping, Optional, TypedDict, Union, cast
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import dateparser # type: ignore
|
|
20
|
+
except Exception: # optional dependency
|
|
21
|
+
dateparser = None # type: ignore
|
|
22
|
+
from babel.core import Locale, UnknownLocaleError
|
|
23
|
+
from babel.dates import format_date, format_datetime, format_time, get_date_format
|
|
24
|
+
# Babel: formatting only (and parse_decimal for numbers)
|
|
25
|
+
from babel.numbers import (format_currency, format_decimal, format_percent, format_scientific, parse_decimal, get_territory_currencies)
|
|
26
|
+
# Parsing stack
|
|
27
|
+
from dateutil import parser as duparser
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ----------------------------
|
|
31
|
+
# Locale detection helper
|
|
32
|
+
# ----------------------------
|
|
33
|
+
|
|
34
|
+
def detect_locale(default: str = "en_US") -> str:
|
|
35
|
+
"""
|
|
36
|
+
Return a Babel-friendly locale like 'de_DE' or 'en_US'.
|
|
37
|
+
- Tries current process locale, then system default (if available).
|
|
38
|
+
- Strips encoding suffixes (e.g. '.UTF-8').
|
|
39
|
+
- Validates with Babel; falls back to `default` on failure.
|
|
40
|
+
"""
|
|
41
|
+
lang: Optional[str]
|
|
42
|
+
enc: Optional[str]
|
|
43
|
+
|
|
44
|
+
# 1) Current process locale
|
|
45
|
+
lang, enc = locale.getlocale() # type: ignore[assignment]
|
|
46
|
+
|
|
47
|
+
# 2) Fallback to system default if unset and API exists
|
|
48
|
+
if not lang:
|
|
49
|
+
get_def = getattr(locale, "getdefaultlocale", None)
|
|
50
|
+
if callable(get_def):
|
|
51
|
+
try:
|
|
52
|
+
lang2, _enc2 = get_def() # deprecated but present on 3.13
|
|
53
|
+
except (ValueError, TypeError, locale.Error):
|
|
54
|
+
lang2 = None
|
|
55
|
+
if lang2:
|
|
56
|
+
lang = lang2
|
|
57
|
+
|
|
58
|
+
if not lang:
|
|
59
|
+
return default
|
|
60
|
+
|
|
61
|
+
# Normalize 'de_DE.UTF-8' -> 'de_DE'
|
|
62
|
+
lang = lang.split(".", 1)[0]
|
|
63
|
+
|
|
64
|
+
# 3) Validate for Babel
|
|
65
|
+
try:
|
|
66
|
+
Locale.parse(lang)
|
|
67
|
+
return lang
|
|
68
|
+
except (UnknownLocaleError, ValueError):
|
|
69
|
+
return default
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ----------------------------
|
|
73
|
+
# DevExtreme-like format specs
|
|
74
|
+
# ----------------------------
|
|
75
|
+
|
|
76
|
+
NumberPreset = Literal[
|
|
77
|
+
"fixedPoint",
|
|
78
|
+
"decimal",
|
|
79
|
+
"percent",
|
|
80
|
+
"currency",
|
|
81
|
+
"exponential",
|
|
82
|
+
"thousands",
|
|
83
|
+
"millions",
|
|
84
|
+
"billions",
|
|
85
|
+
"trillions",
|
|
86
|
+
"largeNumber",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
DatePreset = Literal[
|
|
90
|
+
"longDate",
|
|
91
|
+
"shortDate",
|
|
92
|
+
"longTime",
|
|
93
|
+
"shortTime",
|
|
94
|
+
"longDateLongTime",
|
|
95
|
+
"shortDateShortTime",
|
|
96
|
+
"monthAndDay",
|
|
97
|
+
"monthAndYear",
|
|
98
|
+
"quarterAndYear",
|
|
99
|
+
"millisecond",
|
|
100
|
+
"second",
|
|
101
|
+
"minute",
|
|
102
|
+
"hour",
|
|
103
|
+
"day",
|
|
104
|
+
"dayOfWeek",
|
|
105
|
+
"month",
|
|
106
|
+
"quarter",
|
|
107
|
+
"year",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class NumberFormatOptions(TypedDict, total=False):
|
|
112
|
+
type: NumberPreset | Literal["custom"]
|
|
113
|
+
precision: int
|
|
114
|
+
currency: str
|
|
115
|
+
pattern: str
|
|
116
|
+
use_grouping: bool
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
NumberFormatSpec = Union[str, NumberFormatOptions]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DateFormatOptions(TypedDict, total=False):
|
|
123
|
+
type: DatePreset | Literal["custom"]
|
|
124
|
+
pattern: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
DateFormatSpec = Union[str, DateFormatOptions]
|
|
128
|
+
|
|
129
|
+
# Loosen API: allow str | dict for convenience
|
|
130
|
+
LooseSpec = Union[str, Mapping[str, Any]]
|
|
131
|
+
FormatSpec = LooseSpec
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ----------------------------
|
|
135
|
+
# Compact number suffix config
|
|
136
|
+
# ----------------------------
|
|
137
|
+
|
|
138
|
+
@dataclass(frozen=True)
|
|
139
|
+
class Suffix:
|
|
140
|
+
threshold: int
|
|
141
|
+
symbol: str
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
SUFFIXES_EN = (
|
|
145
|
+
Suffix(1_000_000_000_000, "T"),
|
|
146
|
+
Suffix(1_000_000_000, "B"),
|
|
147
|
+
Suffix(1_000_000, "M"),
|
|
148
|
+
Suffix(1_000, "K"),
|
|
149
|
+
)
|
|
150
|
+
_SUFFIX_TO_FACTOR = {"K": 1_000, "M": 1_000_000, "B": 1_000_000_000, "T": 1_000_000_000_000}
|
|
151
|
+
_NUMBERISH = (int, float)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _locale_to_languages(loc: str) -> list[str]:
|
|
155
|
+
"""
|
|
156
|
+
'en_US' -> ['en']; 'pt_BR' -> ['pt']; 'fr' -> ['fr'].
|
|
157
|
+
dateparser wants base languages, not 'de-DE'.
|
|
158
|
+
"""
|
|
159
|
+
return [loc.replace("_", "-").split("-")[0].lower()]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class IntlFormatter:
|
|
163
|
+
"""DevExtreme-like number/date/datetime formatter.
|
|
164
|
+
|
|
165
|
+
Provides locale-aware formatting and parsing capabilities:
|
|
166
|
+
|
|
167
|
+
- FORMAT with Babel (locale-aware).
|
|
168
|
+
- PARSE dates/times with dateparser -> dateutil fallback.
|
|
169
|
+
- PARSE numbers with Babel's parse_decimal (+ compact K/M/B/T).
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
locale: str | None = None,
|
|
175
|
+
*,
|
|
176
|
+
day_first: bool = False,
|
|
177
|
+
year_first: bool = False,
|
|
178
|
+
):
|
|
179
|
+
"""Create an IntlFormatter.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
locale: Locale code like 'en_US' or 'de_DE'. If None, detect
|
|
183
|
+
from the current process/system settings.
|
|
184
|
+
day_first: Whether day precedes month when parsing dates (e.g., D/M/Y).
|
|
185
|
+
year_first: Whether year precedes month/day when parsing dates (e.g., Y/M/D).
|
|
186
|
+
"""
|
|
187
|
+
self.locale = locale or detect_locale()
|
|
188
|
+
self.day_first = day_first
|
|
189
|
+
self.year_first = year_first
|
|
190
|
+
|
|
191
|
+
# ---------- Public API ----------
|
|
192
|
+
def format(self, value: Any, spec: FormatSpec) -> str:
|
|
193
|
+
"""Format a number/date/time/datetime according to spec.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
value: The value to format. Numbers, date, time, datetime are
|
|
197
|
+
handled specially; anything else is converted with str().
|
|
198
|
+
spec: Format specification. For numbers, a preset string like
|
|
199
|
+
'decimal', 'percent', 'currency', 'largeNumber', or a dict
|
|
200
|
+
with options (e.g., {'type': 'percent', 'precision': 2}).
|
|
201
|
+
For dates/times, use presets like 'longDate', 'shortTime',
|
|
202
|
+
or a CLDR pattern via {'type': 'custom', 'pattern': 'yyyy-MM-dd'}
|
|
203
|
+
(or simply the pattern string).
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
The formatted string.
|
|
207
|
+
"""
|
|
208
|
+
if value is None:
|
|
209
|
+
return ""
|
|
210
|
+
if isinstance(value, _NUMBERISH):
|
|
211
|
+
return self._format_number(float(value), spec)
|
|
212
|
+
if isinstance(value, datetime):
|
|
213
|
+
return self._format_datetime(value, spec)
|
|
214
|
+
if isinstance(value, date):
|
|
215
|
+
return self._format_date(value, spec)
|
|
216
|
+
if isinstance(value, time):
|
|
217
|
+
return self._format_time(value, spec)
|
|
218
|
+
return str(value)
|
|
219
|
+
|
|
220
|
+
def parse(self, text: str, spec: FormatSpec) -> Any:
|
|
221
|
+
"""Parse a string into a number/date/time/datetime per spec.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
text: Input string to parse.
|
|
225
|
+
spec: A number or temporal spec, same shape as for format().
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Parsed Python object (float, date, time, datetime), or None if empty.
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
ValueError: If parsing fails for temporal values.
|
|
232
|
+
"""
|
|
233
|
+
s = (text or "").strip()
|
|
234
|
+
if s == "":
|
|
235
|
+
return None
|
|
236
|
+
if self._is_number_spec(spec):
|
|
237
|
+
return self._parse_number(s, spec)
|
|
238
|
+
return self._parse_temporal(s, spec)
|
|
239
|
+
|
|
240
|
+
# ---------- Numbers ----------
|
|
241
|
+
def _format_number(self, x: float, spec: LooseSpec) -> str:
|
|
242
|
+
opt = self._normalize_number_spec(spec)
|
|
243
|
+
|
|
244
|
+
if opt["type"] == "custom":
|
|
245
|
+
pat = opt.get("pattern") or "#,##0.###"
|
|
246
|
+
return format_decimal(x, format=pat, locale=self.locale)
|
|
247
|
+
|
|
248
|
+
if opt["type"] in ("thousands", "millions", "billions", "trillions"):
|
|
249
|
+
threshold = {
|
|
250
|
+
"thousands": 1_000,
|
|
251
|
+
"millions": 1_000_000,
|
|
252
|
+
"billions": 1_000_000_000,
|
|
253
|
+
"trillions": 1_000_000_000_000,
|
|
254
|
+
}[opt["type"]]
|
|
255
|
+
symbol = {1_000: "K", 1_000_000: "M", 1_000_000_000: "B", 1_000_000_000_000: "T"}[threshold]
|
|
256
|
+
return self._format_with_suffix(x, threshold, symbol, opt.get("precision"))
|
|
257
|
+
|
|
258
|
+
if opt["type"] == "largeNumber":
|
|
259
|
+
return self._format_large_number(x, opt.get("precision"))
|
|
260
|
+
|
|
261
|
+
pattern = self._build_pattern_from_options(opt) if opt["type"] in ("decimal", "fixedPoint") else None
|
|
262
|
+
|
|
263
|
+
if opt["type"] in ("decimal", "fixedPoint"):
|
|
264
|
+
return format_decimal(x, format=pattern, locale=self.locale)
|
|
265
|
+
|
|
266
|
+
if opt["type"] == "percent":
|
|
267
|
+
if opt.get("precision") is None:
|
|
268
|
+
return format_percent(x, locale=self.locale)
|
|
269
|
+
p = max(0, int(opt["precision"]))
|
|
270
|
+
frac = "" if p == 0 else ("." + "0" * p)
|
|
271
|
+
percent_pattern = f"#,##0{frac}%"
|
|
272
|
+
return format_percent(x, format=percent_pattern, locale=self.locale)
|
|
273
|
+
|
|
274
|
+
if opt["type"] == "currency":
|
|
275
|
+
curr = opt.get("currency") or self._get_default_currency()
|
|
276
|
+
prec = opt.get("precision")
|
|
277
|
+
if prec is None:
|
|
278
|
+
# Use locale default currency pattern (includes symbol & spacing)
|
|
279
|
+
return format_currency(x, curr, locale=self.locale)
|
|
280
|
+
# Build a currency pattern with required precision; symbol first is a sane default
|
|
281
|
+
p = max(0, int(prec))
|
|
282
|
+
frac = "" if p == 0 else ("." + "0" * p)
|
|
283
|
+
currency_pattern = f"\\u00A4#,##0{frac}"
|
|
284
|
+
return format_currency(x, curr, format=currency_pattern, locale=self.locale)
|
|
285
|
+
|
|
286
|
+
if opt["type"] == "exponential":
|
|
287
|
+
return format_scientific(x, locale=self.locale)
|
|
288
|
+
|
|
289
|
+
return format_decimal(x, locale=self.locale)
|
|
290
|
+
|
|
291
|
+
def _parse_number(self, s: str, spec: LooseSpec) -> float:
|
|
292
|
+
opt = self._normalize_number_spec(spec)
|
|
293
|
+
m = re.match(r"^\s*([\-+]?[\d.,Ee ]+)\s*([KMBT])?\s*%?\s*$", s, re.IGNORECASE)
|
|
294
|
+
suffix = None
|
|
295
|
+
core = s
|
|
296
|
+
if m:
|
|
297
|
+
core = m.group(1)
|
|
298
|
+
suffix = m.group(2).upper() if m.group(2) else None
|
|
299
|
+
|
|
300
|
+
if opt["type"] in ("currency", "percent"):
|
|
301
|
+
core = re.sub(r"[^\d\-+.,Ee ]", "", core)
|
|
302
|
+
|
|
303
|
+
if "E" in core.upper():
|
|
304
|
+
if self._locale_decimal_mark_is_comma():
|
|
305
|
+
core = core.replace(".", "").replace(",", ".")
|
|
306
|
+
val = float(core)
|
|
307
|
+
else:
|
|
308
|
+
val = float(parse_decimal(core, locale=self.locale))
|
|
309
|
+
|
|
310
|
+
if opt["type"] == "percent" and s.strip().endswith("%"):
|
|
311
|
+
val /= 100.0
|
|
312
|
+
if suffix:
|
|
313
|
+
val *= _SUFFIX_TO_FACTOR[suffix]
|
|
314
|
+
|
|
315
|
+
return val
|
|
316
|
+
|
|
317
|
+
def _format_large_number(self, x: float, precision: Optional[int]) -> str:
|
|
318
|
+
abs_x = abs(x)
|
|
319
|
+
for suf in SUFFIXES_EN:
|
|
320
|
+
if abs_x >= suf.threshold:
|
|
321
|
+
return self._format_with_suffix(x, suf.threshold, suf.symbol, precision)
|
|
322
|
+
return format_decimal(x, format=self._build_pattern(precision), locale=self.locale)
|
|
323
|
+
|
|
324
|
+
def _format_with_suffix(self, x: float, threshold: int, symbol: str, precision: Optional[int]) -> str:
|
|
325
|
+
scaled = x / threshold
|
|
326
|
+
num = format_decimal(scaled, format=self._build_pattern(precision), locale=self.locale)
|
|
327
|
+
return f"{num}{symbol}"
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def _build_pattern(precision: Optional[int]) -> Optional[str]:
|
|
331
|
+
if precision is None:
|
|
332
|
+
return "#,##0.###"
|
|
333
|
+
p = max(0, int(precision))
|
|
334
|
+
frac = "" if p == 0 else ("." + "0" * p)
|
|
335
|
+
return f"#,##0{frac}"
|
|
336
|
+
|
|
337
|
+
def _build_pattern_from_options(self, opt: NumberFormatOptions) -> Optional[str]:
|
|
338
|
+
return self._build_pattern(opt.get("precision"))
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
def _normalize_number_spec(spec: LooseSpec) -> NumberFormatOptions:
|
|
342
|
+
if isinstance(spec, str):
|
|
343
|
+
if any(ch in spec for ch in "#0"):
|
|
344
|
+
return {"type": "custom", "pattern": spec}
|
|
345
|
+
return {"type": cast(NumberPreset, spec)}
|
|
346
|
+
return cast(NumberFormatOptions, dict(spec))
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _is_number_spec(spec: LooseSpec) -> bool:
|
|
350
|
+
if isinstance(spec, dict):
|
|
351
|
+
t = spec.get("type")
|
|
352
|
+
return t in {
|
|
353
|
+
"fixedPoint", "decimal", "percent", "currency", "exponential",
|
|
354
|
+
"thousands", "millions", "billions", "trillions", "largeNumber", "custom"
|
|
355
|
+
}
|
|
356
|
+
if isinstance(spec, str):
|
|
357
|
+
return (
|
|
358
|
+
spec in {
|
|
359
|
+
"fixedPoint", "decimal", "percent", "currency", "exponential",
|
|
360
|
+
"thousands", "millions", "billions", "trillions", "largeNumber"
|
|
361
|
+
} or any(ch in spec for ch in "#0")
|
|
362
|
+
)
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
def _locale_decimal_mark_is_comma(self) -> bool:
|
|
366
|
+
return "," in format_decimal(1.1, locale=self.locale)
|
|
367
|
+
|
|
368
|
+
def _get_default_currency(self) -> str:
|
|
369
|
+
"""
|
|
370
|
+
Get the default currency for the current locale based on its territory.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Currency code (e.g., 'JPY', 'USD', 'EUR') or 'USD' as fallback.
|
|
374
|
+
"""
|
|
375
|
+
# Map common language codes to their primary territories when no territory is specified
|
|
376
|
+
LANG_TO_TERRITORY = {
|
|
377
|
+
'ja': 'JP', 'en': 'US', 'de': 'DE', 'fr': 'FR', 'es': 'ES',
|
|
378
|
+
'it': 'IT', 'pt': 'BR', 'zh': 'CN', 'ko': 'KR', 'ru': 'RU',
|
|
379
|
+
'ar': 'SA', 'nl': 'NL', 'sv': 'SE', 'pl': 'PL', 'tr': 'TR',
|
|
380
|
+
'da': 'DK', 'fi': 'FI', 'no': 'NO', 'cs': 'CZ', 'hu': 'HU',
|
|
381
|
+
'ro': 'RO', 'th': 'TH', 'vi': 'VN', 'id': 'ID', 'he': 'IL',
|
|
382
|
+
'el': 'GR', 'uk': 'UA', 'bg': 'BG', 'hr': 'HR', 'sk': 'SK',
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
loc = Locale.parse(self.locale)
|
|
387
|
+
|
|
388
|
+
# If no territory specified, infer from language
|
|
389
|
+
if not loc.territory and loc.language:
|
|
390
|
+
territory = LANG_TO_TERRITORY.get(loc.language)
|
|
391
|
+
if territory:
|
|
392
|
+
currencies = get_territory_currencies(territory)
|
|
393
|
+
if currencies:
|
|
394
|
+
return currencies[0]
|
|
395
|
+
|
|
396
|
+
# Try with the territory if present
|
|
397
|
+
if loc.territory:
|
|
398
|
+
currencies = get_territory_currencies(loc.territory)
|
|
399
|
+
if currencies:
|
|
400
|
+
# Return the first (primary/official) currency
|
|
401
|
+
return currencies[0]
|
|
402
|
+
except (UnknownLocaleError, ValueError):
|
|
403
|
+
pass
|
|
404
|
+
return "USD" # fallback
|
|
405
|
+
|
|
406
|
+
# ---------- Dates / Times ----------
|
|
407
|
+
def _format_date(self, d: date, spec: LooseSpec) -> str:
|
|
408
|
+
opt = self._normalize_date_spec(spec)
|
|
409
|
+
t = opt["type"]
|
|
410
|
+
if t == "custom": return format_date(d, format=opt["pattern"], locale=self.locale)
|
|
411
|
+
if t == "longDate": return format_date(d, "long", self.locale)
|
|
412
|
+
if t == "shortDate": return format_date(d, "short", self.locale)
|
|
413
|
+
if t == "monthAndDay": return format_date(d, "MMMM d", self.locale)
|
|
414
|
+
if t == "monthAndYear": return format_date(d, "MMMM y", self.locale)
|
|
415
|
+
if t == "quarterAndYear": return format_date(d, "QQQ y", self.locale)
|
|
416
|
+
if t == "day": return format_date(d, "d", self.locale)
|
|
417
|
+
if t == "dayOfWeek": return format_date(d, "EEEE", self.locale)
|
|
418
|
+
if t == "month": return format_date(d, "MMMM", self.locale)
|
|
419
|
+
if t == "quarter": return format_date(d, "QQQ", self.locale)
|
|
420
|
+
if t == "year": return format_date(d, "y", self.locale)
|
|
421
|
+
if t in ("longTime", "shortTime"):
|
|
422
|
+
return format_time(time(0, 0), "long" if t == "longTime" else "short", self.locale)
|
|
423
|
+
if t in ("longDateLongTime", "shortDateShortTime"):
|
|
424
|
+
return format_datetime(
|
|
425
|
+
datetime.combine(d, time(0, 0)), "long" if t == "longDateLongTime" else "short", self.locale)
|
|
426
|
+
return format_date(d, "short", self.locale)
|
|
427
|
+
|
|
428
|
+
def _format_time(self, t: time, spec: LooseSpec) -> str:
|
|
429
|
+
opt = self._normalize_date_spec(spec)
|
|
430
|
+
typ = opt["type"]
|
|
431
|
+
if typ == "custom": return format_time(t, format=opt["pattern"], locale=self.locale)
|
|
432
|
+
if typ == "longTime": return format_time(t, "long", self.locale)
|
|
433
|
+
if typ == "shortTime": return format_time(t, "short", self.locale)
|
|
434
|
+
if typ == "hour": return format_time(t, "H", self.locale)
|
|
435
|
+
if typ == "minute": return format_time(t, "m", self.locale)
|
|
436
|
+
if typ == "second": return format_time(t, "s", self.locale)
|
|
437
|
+
if typ == "millisecond": return format_datetime(datetime.combine(date.today(), t), "S", self.locale)
|
|
438
|
+
if typ in ("longDate", "shortDate", "monthAndDay", "monthAndYear", "quarterAndYear", "day", "dayOfWeek",
|
|
439
|
+
"month", "quarter", "year"):
|
|
440
|
+
return format_time(t, "short", self.locale)
|
|
441
|
+
if typ in ("longDateLongTime", "shortDateShortTime"):
|
|
442
|
+
return format_datetime(
|
|
443
|
+
datetime.combine(date.today(), t), "long" if typ == "longDateLongTime" else "short", self.locale)
|
|
444
|
+
return format_time(t, "short", self.locale)
|
|
445
|
+
|
|
446
|
+
def _format_datetime(self, dt: datetime, spec: LooseSpec) -> str:
|
|
447
|
+
opt = self._normalize_date_spec(spec)
|
|
448
|
+
typ = opt["type"]
|
|
449
|
+
if typ == "custom": return format_datetime(dt, format=opt["pattern"], locale=self.locale)
|
|
450
|
+
if typ == "longDateLongTime": return format_datetime(dt, "long", self.locale)
|
|
451
|
+
if typ == "shortDateShortTime": return format_datetime(dt, "short", self.locale)
|
|
452
|
+
if typ == "longDate": return format_date(dt.date(), "long", self.locale)
|
|
453
|
+
if typ == "shortDate": return format_date(dt.date(), "short", self.locale)
|
|
454
|
+
if typ == "longTime": return format_time(dt.time(), "long", self.locale)
|
|
455
|
+
if typ == "shortTime": return format_time(dt.time(), "short", self.locale)
|
|
456
|
+
if typ == "monthAndDay": return format_date(dt.date(), "MMMM d", self.locale)
|
|
457
|
+
if typ == "monthAndYear": return format_date(dt.date(), "MMMM y", self.locale)
|
|
458
|
+
if typ == "quarterAndYear": return format_date(dt.date(), "QQQ y", self.locale)
|
|
459
|
+
if typ == "millisecond": return format_datetime(dt, "SSS", self.locale)
|
|
460
|
+
if typ == "second": return format_time(dt.time(), "s", self.locale)
|
|
461
|
+
if typ == "minute": return format_time(dt.time(), "m", self.locale)
|
|
462
|
+
if typ == "hour": return format_time(dt.time(), "H", self.locale)
|
|
463
|
+
if typ == "day": return format_date(dt.date(), "d", self.locale)
|
|
464
|
+
if typ == "dayOfWeek": return format_date(dt.date(), "EEEE", self.locale)
|
|
465
|
+
if typ == "month": return format_date(dt.date(), "MMMM", self.locale)
|
|
466
|
+
if typ == "quarter": return format_date(dt.date(), "QQQ", self.locale)
|
|
467
|
+
if typ == "year": return format_date(dt.date(), "y", self.locale)
|
|
468
|
+
return format_datetime(dt, "short", self.locale)
|
|
469
|
+
|
|
470
|
+
# ---------- Temporal parsing ----------
|
|
471
|
+
def _parse_temporal(self, s: str, spec: LooseSpec) -> Any:
|
|
472
|
+
opt = self._normalize_date_spec(spec)
|
|
473
|
+
t = opt["type"]
|
|
474
|
+
|
|
475
|
+
if t == "millisecond" and re.fullmatch(r"\d{1,3}", s):
|
|
476
|
+
ms = max(0, min(999, int(s)))
|
|
477
|
+
return datetime.now().replace(microsecond=ms * 1000)
|
|
478
|
+
|
|
479
|
+
languages = _locale_to_languages(self.locale)
|
|
480
|
+
settings = {
|
|
481
|
+
"PREFER_DAY_OF_MONTH": "first" if self.day_first else "current",
|
|
482
|
+
"PREFER_DATES_FROM": "current_period",
|
|
483
|
+
"RELATIVE_BASE": datetime.now(),
|
|
484
|
+
"RETURN_AS_TIMEZONE_AWARE": False,
|
|
485
|
+
"DATE_ORDER": self._date_order_from_locale(),
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
dp = None
|
|
489
|
+
try:
|
|
490
|
+
with warnings.catch_warnings():
|
|
491
|
+
warnings.filterwarnings(
|
|
492
|
+
"ignore", category=DeprecationWarning,
|
|
493
|
+
message=r"Parsing dates involving a day of month without a year specified.*")
|
|
494
|
+
dp = dateparser.parse(s, languages=languages, settings=settings)
|
|
495
|
+
except ValueError:
|
|
496
|
+
dp = None
|
|
497
|
+
|
|
498
|
+
if dp is None:
|
|
499
|
+
with warnings.catch_warnings():
|
|
500
|
+
warnings.filterwarnings(
|
|
501
|
+
"ignore", category=DeprecationWarning,
|
|
502
|
+
message=r"Parsing dates involving a day of month without a year specified.*")
|
|
503
|
+
dp = dateparser.parse(s, settings=settings)
|
|
504
|
+
|
|
505
|
+
if dp is None:
|
|
506
|
+
base = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
|
507
|
+
try:
|
|
508
|
+
dp = duparser.parse(
|
|
509
|
+
s, dayfirst=self.day_first, yearfirst=self.year_first,
|
|
510
|
+
fuzzy=True, default=base)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
raise ValueError(f"Could not parse temporal value: {s!r}") from e
|
|
513
|
+
|
|
514
|
+
if t in ("longDate", "shortDate", "monthAndDay", "monthAndYear", "quarterAndYear", "day", "dayOfWeek", "month",
|
|
515
|
+
"quarter", "year"):
|
|
516
|
+
return dp.date()
|
|
517
|
+
if t in ("longTime", "shortTime", "second", "minute", "hour", "millisecond"):
|
|
518
|
+
return dp.time().replace(microsecond=(dp.microsecond // 1000) * 1000)
|
|
519
|
+
return dp
|
|
520
|
+
|
|
521
|
+
@staticmethod
|
|
522
|
+
def _normalize_date_spec(spec: LooseSpec) -> DateFormatOptions:
|
|
523
|
+
if isinstance(spec, str):
|
|
524
|
+
known = {
|
|
525
|
+
"longDate", "shortDate", "longTime", "shortTime",
|
|
526
|
+
"longDateLongTime", "shortDateShortTime",
|
|
527
|
+
"monthAndDay", "monthAndYear", "quarterAndYear",
|
|
528
|
+
"millisecond", "second", "minute", "hour",
|
|
529
|
+
"day", "dayOfWeek", "month", "quarter", "year",
|
|
530
|
+
}
|
|
531
|
+
if spec in known:
|
|
532
|
+
return {"type": cast(DatePreset, spec)}
|
|
533
|
+
# Otherwise treat as CLDR custom pattern for formatting
|
|
534
|
+
return {"type": "custom", "pattern": spec}
|
|
535
|
+
return cast(DateFormatOptions, dict(spec))
|
|
536
|
+
|
|
537
|
+
def _date_order_from_locale(self) -> str:
|
|
538
|
+
"""
|
|
539
|
+
Infer DATE_ORDER for dateparser from the locale's short date pattern.
|
|
540
|
+
Returns 'DMY', 'MDY', or 'YMD'.
|
|
541
|
+
"""
|
|
542
|
+
try:
|
|
543
|
+
pat = str(get_date_format("short", locale=self.locale))
|
|
544
|
+
order: list[str] = []
|
|
545
|
+
for ch in pat:
|
|
546
|
+
if ch in "yMd":
|
|
547
|
+
if ch == "y" and "Y" not in order:
|
|
548
|
+
order.append("Y")
|
|
549
|
+
elif ch == "M" and "M" not in order:
|
|
550
|
+
order.append("M")
|
|
551
|
+
elif ch == "d" and "D" not in order:
|
|
552
|
+
order.append("D")
|
|
553
|
+
joined = "".join(order)
|
|
554
|
+
if joined.startswith("DMY"):
|
|
555
|
+
return "DMY"
|
|
556
|
+
if joined.startswith("MDY"):
|
|
557
|
+
return "MDY"
|
|
558
|
+
if joined.startswith("YMD"):
|
|
559
|
+
return "YMD"
|
|
560
|
+
return "MDY" # fallback
|
|
561
|
+
except (UnknownLocaleError, ValueError):
|
|
562
|
+
return "MDY"
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
# -------------- optional quick demo --------------
|
|
566
|
+
if __name__ == "__main__":
|
|
567
|
+
fmt = IntlFormatter() # auto-detects system locale
|
|
568
|
+
print(fmt.locale)
|
|
569
|
+
|
|
570
|
+
# Numbers
|
|
571
|
+
print(fmt.format(1234.56, "decimal"))
|
|
572
|
+
print(fmt.format(0.42, {"type": "percent", "precision": 0}))
|
|
573
|
+
print(fmt.format(1234.5, {"type": "currency", "currency": "EUR", "precision": 2}))
|
|
574
|
+
print(fmt.format(1_234_000, "largeNumber"))
|
|
575
|
+
print(fmt.parse("1.2M", "largeNumber"))
|
|
576
|
+
|
|
577
|
+
# Dates/times
|
|
578
|
+
print(fmt.format(date(2025, 9, 2), "longDate"))
|
|
579
|
+
print(IntlFormatter(locale="de_DE", day_first=True).parse("15 juillet 2025", "longDate")) # auto-detect FR text
|
|
580
|
+
print(IntlFormatter(locale="fr_FR", day_first=True).parse("15 juillet 2025", "longDate"))
|