bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,354 @@
1
+ """Type definitions and protocols for bootstack datasource module.
2
+
3
+ This module defines the core protocol that all datasource implementations must follow,
4
+ enabling consistent data management across different backends (memory, SQLite, web, etc.).
5
+
6
+ The DataSourceProtocol provides a unified interface for:
7
+ - Data loading and configuration (load, where, order)
8
+ - Pagination (page, next_page, prev_page, has_next_page)
9
+ - CRUD operations (insert, get, update, delete)
10
+ - Selection management (is_selected, select/deselect records)
11
+ - Data export (CSV export)
12
+ - Index-based access (page_slice)
13
+ - Lifecycle and reorder (reload, move)
14
+
15
+ All datasource implementations should conform to this protocol to ensure
16
+ compatibility with datasource-aware widgets and utilities.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import (
22
+ TYPE_CHECKING, Any, Dict, List, Optional, Protocol, Sequence, Mapping,
23
+ runtime_checkable,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from bootstack.data.query import Column, Condition, SortKey
28
+
29
+ # Type aliases for clarity
30
+ Primitive = Any # Can be str, int, float, bool, None, etc.
31
+ Record = Dict[str, Any]
32
+
33
+
34
+ @runtime_checkable
35
+ class DataSourceProtocol(Protocol):
36
+ """Protocol defining the interface for data source implementations.
37
+
38
+ This protocol establishes a contract that all datasource backends must implement,
39
+ ensuring consistent behavior across different storage mechanisms (memory, database,
40
+ web API, etc.).
41
+
42
+ All datasource implementations must support:
43
+ - Pagination with configurable page size
44
+ - Filtering with `col`-based conditions (see `bootstack.data.query`)
45
+ - Sorting by one or more columns
46
+ - Full CRUD operations on records
47
+ - Selection tracking for multi-select scenarios
48
+ - CSV export capabilities
49
+ - Direct index-based data access
50
+
51
+ Notes:
52
+ - Records are represented as Dict[str, Any] with at least 'id' and 'selected' fields
53
+ - Filtering and sorting are expressed with the `col` expression API, not SQL
54
+ - All methods preserve immutability - operations return new data or modify in-place
55
+ """
56
+
57
+ # public attrs
58
+ page_size: int
59
+ """Number of records per page."""
60
+
61
+ # ---------- data & view config ----------
62
+ def load(self, records: Sequence[Primitive] | Sequence[Mapping[str, Any]]) -> DataSourceProtocol:
63
+ """Load data records into the datasource.
64
+
65
+ Args:
66
+ records: Sequence of records (dicts) or primitives (auto-wrapped)
67
+
68
+ Returns:
69
+ Self for method chaining
70
+ """
71
+ ...
72
+
73
+ def where(self, condition: "Condition | None" = None) -> DataSourceProtocol:
74
+ """Filter rows by a condition built with `col`.
75
+
76
+ Args:
77
+ condition: A filter condition (e.g. `col("age") >= 25`), or `None`
78
+ / no argument to clear the filter.
79
+
80
+ Returns:
81
+ Self for method chaining
82
+ """
83
+ ...
84
+
85
+ def order(self, *keys: "str | Column | SortKey") -> DataSourceProtocol:
86
+ """Sort rows by one or more keys.
87
+
88
+ Args:
89
+ keys: Column names (`"name"`), descending names (`"-name"`), or
90
+ `col(...)` specs (`col("salary").desc()`). With no arguments,
91
+ clears sorting.
92
+
93
+ Returns:
94
+ Self for method chaining
95
+ """
96
+ ...
97
+
98
+ # ---------- pagination ----------
99
+ def page(self, page: Optional[int] = None) -> List[Record]:
100
+ """Get records for specified page (or current page if None).
101
+
102
+ Args:
103
+ page: Page number (0-indexed); updates current page if provided
104
+
105
+ Returns:
106
+ List of record dictionaries for the page
107
+ """
108
+ ...
109
+
110
+ def next_page(self) -> List[Record]:
111
+ """Advance to next page and return its records.
112
+
113
+ Returns:
114
+ List of record dictionaries for the new page
115
+ """
116
+ ...
117
+
118
+ def prev_page(self) -> List[Record]:
119
+ """Move to previous page and return its records.
120
+
121
+ Returns:
122
+ List of record dictionaries for the new page
123
+ """
124
+ ...
125
+
126
+ def has_next_page(self) -> bool:
127
+ """Check if more pages exist after current page.
128
+
129
+ Returns:
130
+ True if next page exists, False otherwise
131
+ """
132
+ ...
133
+
134
+ @property
135
+ def count(self) -> int:
136
+ """Total number of records matching the current filter."""
137
+ ...
138
+
139
+ # ---------- CRUD ----------
140
+ def insert(self, record: Dict[str, Any]) -> int:
141
+ """Create new record and return its ID.
142
+
143
+ Args:
144
+ record: Dictionary with record data
145
+
146
+ Returns:
147
+ The ID assigned to the new record
148
+ """
149
+ ...
150
+
151
+ def get(self, record_id: Any) -> Optional[Record]:
152
+ """Retrieve single record by ID.
153
+
154
+ Args:
155
+ record_id: Unique identifier of the record
156
+
157
+ Returns:
158
+ Record dictionary or None if not found
159
+ """
160
+ ...
161
+
162
+ def update(self, record_id: Any, updates: Dict[str, Any]) -> bool:
163
+ """Update record fields by ID.
164
+
165
+ Args:
166
+ record_id: Unique identifier of the record
167
+ updates: Dictionary with fields to update
168
+
169
+ Returns:
170
+ True if record was updated, False if not found
171
+ """
172
+ ...
173
+
174
+ def delete(self, record_id: Any) -> bool:
175
+ """Delete record by ID.
176
+
177
+ Args:
178
+ record_id: Unique identifier of the record
179
+
180
+ Returns:
181
+ True if record was deleted, False if not found
182
+ """
183
+ ...
184
+
185
+ # ---------- selection ----------
186
+ def is_selected(self, record_id: Any) -> bool:
187
+ """Check whether a record is currently selected.
188
+
189
+ Args:
190
+ record_id: Unique identifier of the record
191
+
192
+ Returns:
193
+ True if the record is selected, False otherwise (including missing records)
194
+ """
195
+ ...
196
+
197
+ def select(self, record_id: Any) -> bool:
198
+ """Mark record as selected.
199
+
200
+ Args:
201
+ record_id: Unique identifier of the record
202
+
203
+ Returns:
204
+ True if record was selected, False if not found
205
+ """
206
+ ...
207
+
208
+ def deselect(self, record_id: Any) -> bool:
209
+ """Mark record as unselected.
210
+
211
+ Args:
212
+ record_id: Unique identifier of the record
213
+
214
+ Returns:
215
+ True if record was deselected, False if not found
216
+ """
217
+ ...
218
+
219
+ def select_all(self, current_page_only: bool = False) -> int:
220
+ """Select all records (optionally only current page).
221
+
222
+ Args:
223
+ current_page_only: If True, select only records on current page
224
+
225
+ Returns:
226
+ Number of records selected
227
+ """
228
+ ...
229
+
230
+ def deselect_all(self, current_page_only: bool = False) -> int:
231
+ """Deselect all records (optionally only current page).
232
+
233
+ Args:
234
+ current_page_only: If True, deselect only records on current page
235
+
236
+ Returns:
237
+ Number of records deselected
238
+ """
239
+ ...
240
+
241
+ def selected(self, page: Optional[int] = None) -> List[Record]:
242
+ """Get selected records, optionally paginated.
243
+
244
+ Args:
245
+ page: Optional page number for paginating selected records
246
+
247
+ Returns:
248
+ List of selected record dictionaries
249
+ """
250
+ ...
251
+
252
+ @property
253
+ def selected_count(self) -> int:
254
+ """Number of selected records."""
255
+ ...
256
+
257
+ # ---------- lifecycle / reorder ----------
258
+ def reload(self) -> None:
259
+ """Re-read data from the underlying source.
260
+
261
+ For in-memory implementations this is typically a no-op. For
262
+ file- or database-backed sources, this re-queries or re-reads.
263
+ """
264
+ ...
265
+
266
+ def close(self) -> None:
267
+ """Release any resources held by the source (a connection, file handle).
268
+
269
+ A no-op for in-memory sources. Sources are also context managers, so a
270
+ `with` block closes automatically. Safe to call more than once.
271
+ """
272
+ ...
273
+
274
+ def move(self, record_id: Any, target_index: int) -> bool:
275
+ """Reorder a record to a new position.
276
+
277
+ Args:
278
+ record_id: Unique identifier of the record to move
279
+ target_index: Zero-based destination index (clamped to valid range)
280
+
281
+ Returns:
282
+ True if the record was moved, False if not supported or not found
283
+ """
284
+ ...
285
+
286
+ # ---------- export ----------
287
+ def save(self, path: str, *, selected_only: bool = False, format: Optional[str] = None, config: Any = None) -> None:
288
+ """Export records to a file, format chosen by the path extension (or `format`).
289
+
290
+ Records are streamed into the writer; the active `where`/`order` view is
291
+ respected. Built-in formats: CSV, TSV, JSON, JSONL, XML — plus Parquet,
292
+ Feather, and HDF5 with the matching extra installed.
293
+ """
294
+ ...
295
+
296
+ def export_csv(self, filepath: str, include_all: bool = True) -> None:
297
+ """Export records to CSV file.
298
+
299
+ Args:
300
+ filepath: Path to output CSV file
301
+ include_all: If True, export all records; if False, export only selected
302
+ """
303
+ ...
304
+
305
+ # ---------- index-based paging ----------
306
+ def page_slice(self, start_index: int, count: int) -> List[Record]:
307
+ """Get records by start index and count (respects filter/sort).
308
+
309
+ Args:
310
+ start_index: Starting record index
311
+ count: Number of records to retrieve
312
+
313
+ Returns:
314
+ List of record dictionaries
315
+ """
316
+ ...
317
+
318
+ # ---------- row identity ----------
319
+ @property
320
+ def id_field(self) -> str:
321
+ """Name of the record field that holds the stable row identity."""
322
+ ...
323
+
324
+ def _internal_fields(self) -> "frozenset[str]":
325
+ """Raw-record keys that are implementation-internal and hidden from
326
+ users. Datasource-aware widgets strip these from columns and exports."""
327
+ ...
328
+
329
+ def _record_id(self, record: Any) -> Any:
330
+ """Stable identity of a raw record — what `get`/`update`/`delete`/
331
+ `select` accept and what is surfaced publicly as `id`."""
332
+ ...
333
+
334
+ def _public_record(self, record: Any) -> Record:
335
+ """User-facing copy of a raw record: internal fields stripped, `id`
336
+ surfaced."""
337
+ ...
338
+
339
+ # ---------- change broadcasting ----------
340
+ def on_change(self, handler: Any = None) -> Any:
341
+ """Subscribe to changes. Returns a `Stream` (no handler) or a
342
+ cancellable subscription (with handler). The handler receives a
343
+ `DataChangeEvent`."""
344
+ ...
345
+
346
+ def observe(self, condition: "Condition | None" = None, *order: Any) -> Any:
347
+ """Observe a live result set for a `where`/`order` query. Returns a
348
+ `Stream` of result sets that re-emits on relevant changes."""
349
+ ...
350
+
351
+ def _query(self, condition: "Condition | None", sort_keys: List[Any]) -> List[Record]:
352
+ """Run a one-off filtered/sorted read without disturbing the source's
353
+ active `where`/`order` view state. Backs `observe()`."""
354
+ ...
@@ -0,0 +1,232 @@
1
+ """Streaming record writers — the export counterpart of `readers`.
2
+
3
+ A *writer* consumes a lazy iterable of record dicts and writes them to a file,
4
+ streaming where the format allows so a large export does not materialize the
5
+ whole dataset. Writers are keyed by file extension in a registry: the stdlib
6
+ formats (CSV / TSV / JSON / JSONL / XML) are always available, and the optional
7
+ columnar/scientific formats (Parquet / Feather / HDF5) register too but raise a
8
+ clear "install the extra" error when used without their dependency.
9
+
10
+ The contract mirrors the readers — `writer(path, records, config) -> None` — so
11
+ it pairs with `read_records` for round-trips, and backs the source-level
12
+ `save()` / `export_csv()`.
13
+
14
+ Register a custom writer for your own extension::
15
+
16
+ from bootstack.data.writers import register_writer
17
+
18
+ @register_writer(".myfmt")
19
+ def write_myfmt(path, records, config=None):
20
+ with open(path, "w") as f:
21
+ for record in records:
22
+ f.write(serialize(record))
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import csv
28
+ import importlib
29
+ import json
30
+ from pathlib import Path
31
+ from typing import TYPE_CHECKING, Callable, Dict, Iterable, List, Optional
32
+ from xml.sax.saxutils import escape as _xml_escape
33
+
34
+ from bootstack.data.types import Record
35
+
36
+ if TYPE_CHECKING:
37
+ from bootstack.data.file_source import FileSourceConfig
38
+
39
+ # A writer consumes record dicts: (path, records, config) -> None.
40
+ Writer = Callable[..., None]
41
+
42
+ _WRITERS: Dict[str, Writer] = {}
43
+
44
+ # Format-name aliases (FileSourceConfig.file_format) → canonical extension.
45
+ _FORMAT_ALIASES: Dict[str, str] = {
46
+ "csv": ".csv",
47
+ "tsv": ".tsv",
48
+ "json": ".json",
49
+ "jsonl": ".jsonl",
50
+ "ndjson": ".jsonl",
51
+ "xml": ".xml",
52
+ "parquet": ".parquet",
53
+ "feather": ".feather",
54
+ "hdf5": ".h5",
55
+ }
56
+
57
+
58
+ def _norm_ext(ext: str) -> str:
59
+ ext = ext.lower()
60
+ return ext if ext.startswith(".") else "." + ext
61
+
62
+
63
+ def register_writer(*extensions: str, writer: Optional[Writer] = None):
64
+ """Register a writer for one or more file extensions (decorator or direct)."""
65
+
66
+ def _apply(fn: Writer) -> Writer:
67
+ for ext in extensions:
68
+ _WRITERS[_norm_ext(ext)] = fn
69
+ return fn
70
+
71
+ return _apply(writer) if writer is not None else _apply
72
+
73
+
74
+ def supported_write_extensions() -> List[str]:
75
+ """Return the sorted list of registered writer extensions."""
76
+ return sorted(_WRITERS)
77
+
78
+
79
+ def get_writer(path_or_ext: "str | Path") -> Writer:
80
+ """Resolve the writer for a path or extension.
81
+
82
+ Raises:
83
+ ValueError: If no writer is registered for the extension.
84
+ """
85
+ suffix = Path(str(path_or_ext)).suffix or str(path_or_ext)
86
+ ext = _norm_ext(suffix)
87
+ try:
88
+ return _WRITERS[ext]
89
+ except KeyError:
90
+ raise ValueError(
91
+ f"No writer registered for {ext!r}. Supported formats: "
92
+ f"{', '.join(supported_write_extensions())}."
93
+ ) from None
94
+
95
+
96
+ def write_records(
97
+ path: "str | Path",
98
+ records: "Iterable[Record]",
99
+ config: "Optional[FileSourceConfig]" = None,
100
+ *,
101
+ format: Optional[str] = None,
102
+ ) -> None:
103
+ """Write records to a file, choosing the writer by `format` or extension.
104
+
105
+ Args:
106
+ path: Destination file path.
107
+ records: An iterable of record dicts (consumed lazily).
108
+ config: Optional formatting configuration (encoding, delimiter, …).
109
+ format: Explicit format name overriding the path extension.
110
+ """
111
+ if format:
112
+ writer = get_writer(_FORMAT_ALIASES.get(format, format))
113
+ else:
114
+ writer = get_writer(path)
115
+ writer(path, records, config)
116
+
117
+
118
+ def _require(module: str, extra: str):
119
+ """Import an optional dependency or raise a clear install hint."""
120
+ try:
121
+ return importlib.import_module(module)
122
+ except ImportError as exc:
123
+ raise ImportError(
124
+ f"Writing this format needs the optional {module!r} dependency. "
125
+ f"Install it with: pip install bootstack[{extra}]"
126
+ ) from exc
127
+
128
+
129
+ def _encoding(config: "Optional[FileSourceConfig]") -> str:
130
+ return getattr(config, "encoding", "utf-8") or "utf-8"
131
+
132
+
133
+ # --------------------------------------------------------------------------- stdlib
134
+
135
+
136
+ @register_writer(".csv", ".tsv", ".txt")
137
+ def write_csv(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
138
+ """Write records to CSV/TSV, streaming a row at a time.
139
+
140
+ Column headers are taken from the first record; later records are written with
141
+ those columns (missing fields blank, extra fields ignored).
142
+ """
143
+ delimiter = getattr(config, "delimiter", None)
144
+ if not delimiter:
145
+ delimiter = "\t" if _norm_ext(Path(path).suffix) == ".tsv" else ","
146
+
147
+ it = iter(records)
148
+ try:
149
+ first = next(it)
150
+ except StopIteration:
151
+ open(path, "w", encoding=_encoding(config)).close() # empty export
152
+ return
153
+
154
+ fieldnames = list(first.keys())
155
+ with open(path, "w", newline="", encoding=_encoding(config)) as f:
156
+ writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter=delimiter, extrasaction="ignore")
157
+ writer.writeheader()
158
+ writer.writerow(first)
159
+ for record in it:
160
+ writer.writerow(record)
161
+
162
+
163
+ @register_writer(".jsonl", ".ndjson")
164
+ def write_jsonl(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
165
+ """Write records as line-delimited JSON (one object per line) — fully streamed."""
166
+ with open(path, "w", encoding=_encoding(config)) as f:
167
+ for record in records:
168
+ f.write(json.dumps(record))
169
+ f.write("\n")
170
+
171
+
172
+ @register_writer(".json")
173
+ def write_json(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
174
+ """Write records as a JSON array, streamed element by element."""
175
+ with open(path, "w", encoding=_encoding(config)) as f:
176
+ f.write("[")
177
+ first = True
178
+ for record in records:
179
+ if not first:
180
+ f.write(", ")
181
+ f.write(json.dumps(record))
182
+ first = False
183
+ f.write("]")
184
+
185
+
186
+ @register_writer(".xml")
187
+ def write_xml(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
188
+ """Write records as XML, streaming one record element at a time.
189
+
190
+ Each record becomes a `<record>` element (or `config.xml_record_tag`) whose
191
+ child elements are the record's fields.
192
+ """
193
+ record_tag = getattr(config, "xml_record_tag", None) or "record"
194
+ root_tag = "records"
195
+ with open(path, "w", encoding=_encoding(config)) as f:
196
+ f.write(f"<{root_tag}>")
197
+ for record in records:
198
+ f.write(f"<{record_tag}>")
199
+ for key, value in record.items():
200
+ text = "" if value is None else _xml_escape(str(value))
201
+ f.write(f"<{key}>{text}</{key}>")
202
+ f.write(f"</{record_tag}>")
203
+ f.write(f"</{root_tag}>")
204
+
205
+
206
+ # --------------------------------------------------------------------------- optional
207
+
208
+
209
+ @register_writer(".parquet")
210
+ def write_parquet(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
211
+ """Write records to a Parquet file (needs pyarrow)."""
212
+ pa = _require("pyarrow", "parquet")
213
+ import pyarrow.parquet as pq
214
+
215
+ pq.write_table(pa.Table.from_pylist(list(records)), str(path))
216
+
217
+
218
+ @register_writer(".feather", ".arrow")
219
+ def write_feather(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
220
+ """Write records to a Feather/Arrow IPC file (needs pyarrow)."""
221
+ pa = _require("pyarrow", "parquet")
222
+ import pyarrow.feather as feather
223
+
224
+ feather.write_feather(pa.Table.from_pylist(list(records)), str(path))
225
+
226
+
227
+ @register_writer(".h5", ".hdf5", ".hdf")
228
+ def write_hdf5(path: "str | Path", records: "Iterable[Record]", config: "Optional[FileSourceConfig]" = None) -> None:
229
+ """Write records to a tabular HDF5 file (needs pandas + tables)."""
230
+ pd = _require("pandas", "hdf5")
231
+ key = getattr(config, "hdf5_key", None) or "data"
232
+ pd.DataFrame(list(records)).to_hdf(str(path), key=key, format="table")
@@ -0,0 +1,36 @@
1
+ """Developer tooling for bootstack — hot reload.
2
+
3
+ .. warning::
4
+
5
+ **Provisional.** This module ships in 0.1.0 but is *carved out of the API
6
+ freeze*: unlike the ``bootstack`` compose surface, it may change in a minor
7
+ release while the dev workflow settles. Pin a version if you depend on it.
8
+
9
+ Run an app with live reload::
10
+
11
+ bootstack dev app.py
12
+
13
+ On save, a module-level ``with bs.App()`` app updates in place — the window,
14
+ your signals, and any module-level datasources survive; only the UI the body
15
+ built is rebuilt. State that should persist across reloads lives **above** the
16
+ ``with`` block (module level); the block body is what reloads.
17
+
18
+ For multi-file apps, mark a page builder so only its region rebuilds when its
19
+ file changes::
20
+
21
+ from bootstack.dev import reloadable
22
+
23
+ @reloadable
24
+ def build_dashboard(db):
25
+ bs.Label("Dashboard", font="heading-lg")
26
+ bs.DataTable(data_source=db)
27
+
28
+ Outside ``bootstack dev`` everything here is inert: :func:`reloadable` returns
29
+ the function unchanged and :func:`is_dev_mode` is False.
30
+ """
31
+ from __future__ import annotations
32
+
33
+ from bootstack.dev._env import is_dev_mode
34
+ from bootstack.dev._registry import reloadable
35
+
36
+ __all__ = ["reloadable", "is_dev_mode"]