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.
Files changed (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. 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"))