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
bootstack/data/base.py ADDED
@@ -0,0 +1,780 @@
1
+ """Abstract base class for datasource implementations.
2
+
3
+ Provides a common foundation for all datasource implementations with:
4
+ - Abstract method definitions enforcing the DataSourceProtocol interface
5
+ - Shared utility methods for type inference, literal parsing, and validation
6
+ - Template methods for common operations
7
+ - Hook methods for extensibility
8
+
9
+ Subclasses must implement storage-specific operations (load, page, CRUD, etc.)
10
+ while inheriting common utilities and patterns.
11
+
12
+ This base class makes it easier to:
13
+ - Create custom datasource implementations
14
+ - Reduce code duplication across implementations
15
+ - Ensure consistent behavior across all datasources
16
+ - Extend datasource functionality through hooks
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from abc import ABC, abstractmethod
22
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Sequence, Mapping
23
+
24
+ from bootstack.data.types import Primitive, Record
25
+
26
+ if TYPE_CHECKING:
27
+ from bootstack.data.query import Column, Condition, SortKey
28
+
29
+
30
+ class BaseDataSource(ABC):
31
+ """Abstract base class for datasource implementations.
32
+
33
+ Provides shared utilities and enforces the DataSourceProtocol interface through
34
+ abstract methods. Subclasses implement storage-specific logic while inheriting
35
+ common functionality.
36
+
37
+ Args:
38
+ page_size(int): Number of records per page (default: 10)
39
+
40
+ Attributes:
41
+ page_size: Current page size setting
42
+
43
+ Example:
44
+ .. code-block:: python
45
+
46
+ class RedisDataSource(BaseDataSource):
47
+ def __init__(self, redis_client, page_size=10):
48
+ super().__init__(page_size)
49
+ self.redis = redis_client
50
+
51
+ def load(self, records):
52
+ pass # Redis-specific implementation
53
+
54
+ def page(self, page=None):
55
+ pass # Redis-specific implementation
56
+
57
+ """
58
+
59
+ def __init__(self, page_size: int = 10):
60
+ """Initialize base datasource with pagination settings.
61
+
62
+ Args:
63
+ page_size: Number of records returned per page when paginating.
64
+ """
65
+ self.page_size = page_size
66
+ self._page = 0
67
+ # Reactive change broadcasting (on_change / observe). Lazy import keeps
68
+ # the data layer free of a load-time dependency on the runtime layer.
69
+ from bootstack.data._observable import _ChangeHub
70
+
71
+ self._hub = _ChangeHub(self)
72
+
73
+ # ========== ABSTRACT METHODS - MUST BE IMPLEMENTED ==========
74
+
75
+ # Data & View Configuration
76
+ @abstractmethod
77
+ def load(self, records: Sequence[Primitive] | Sequence[Mapping[str, Any]]) -> 'BaseDataSource':
78
+ """Load data records into the datasource.
79
+
80
+ Args:
81
+ records: Sequence of records (dicts) or primitives (auto-wrapped)
82
+
83
+ Returns:
84
+ Self for method chaining
85
+ """
86
+ ...
87
+
88
+ @abstractmethod
89
+ def where(self, condition: "Condition | None" = None) -> "BaseDataSource":
90
+ """Filter rows by a condition built with `col`.
91
+
92
+ Args:
93
+ condition: A filter condition (e.g. `col("age") >= 25`), or `None`
94
+ / no argument to clear the filter.
95
+
96
+ Returns:
97
+ Self for method chaining
98
+ """
99
+ ...
100
+
101
+ @abstractmethod
102
+ def order(self, *keys: "str | Column | SortKey") -> "BaseDataSource":
103
+ """Sort rows by one or more keys.
104
+
105
+ Args:
106
+ keys: Column names (`"name"`), descending names (`"-name"`), or
107
+ `col(...)` specs. With no arguments, clears sorting.
108
+
109
+ Returns:
110
+ Self for method chaining
111
+ """
112
+ ...
113
+
114
+ # Pagination
115
+ @abstractmethod
116
+ def page(self, page: Optional[int] = None) -> List[Record]:
117
+ """Get records for specified page (or current page if None).
118
+
119
+ Args:
120
+ page: Page number (0-indexed); updates current page if provided
121
+
122
+ Returns:
123
+ List of record dictionaries for the page
124
+ """
125
+ ...
126
+
127
+ @abstractmethod
128
+ def next_page(self) -> List[Record]:
129
+ """Advance to next page and return its records.
130
+
131
+ Returns:
132
+ List of record dictionaries for the new page
133
+ """
134
+ ...
135
+
136
+ @abstractmethod
137
+ def prev_page(self) -> List[Record]:
138
+ """Move to previous page and return its records.
139
+
140
+ Returns:
141
+ List of record dictionaries for the new page
142
+ """
143
+ ...
144
+
145
+ @abstractmethod
146
+ def has_next_page(self) -> bool:
147
+ """Check if more pages exist after current page.
148
+
149
+ Returns:
150
+ True if next page exists, False otherwise
151
+ """
152
+ ...
153
+
154
+ @property
155
+ @abstractmethod
156
+ def count(self) -> int:
157
+ """Total number of records matching the current filter."""
158
+ ...
159
+
160
+ # CRUD Operations
161
+ @abstractmethod
162
+ def insert(self, record: Dict[str, Any]) -> int:
163
+ """Create new record and return its ID.
164
+
165
+ Args:
166
+ record: Dictionary with record data
167
+
168
+ Returns:
169
+ The ID assigned to the new record
170
+ """
171
+ ...
172
+
173
+ @abstractmethod
174
+ def get(self, record_id: Any) -> Optional[Record]:
175
+ """Retrieve single record by ID.
176
+
177
+ Args:
178
+ record_id: Unique identifier of the record
179
+
180
+ Returns:
181
+ Record dictionary or None if not found
182
+ """
183
+ ...
184
+
185
+ @abstractmethod
186
+ def update(self, record_id: Any, updates: Dict[str, Any]) -> bool:
187
+ """Update record fields by ID.
188
+
189
+ Args:
190
+ record_id: Unique identifier of the record
191
+ updates: Dictionary with fields to update
192
+
193
+ Returns:
194
+ True if record was updated, False if not found
195
+ """
196
+ ...
197
+
198
+ @abstractmethod
199
+ def delete(self, record_id: Any) -> bool:
200
+ """Delete record by ID.
201
+
202
+ Args:
203
+ record_id: Unique identifier of the record
204
+
205
+ Returns:
206
+ True if record was deleted, False if not found
207
+ """
208
+ ...
209
+
210
+ # Selection Management
211
+ @abstractmethod
212
+ def is_selected(self, record_id: Any) -> bool:
213
+ """Check whether a record is currently selected.
214
+
215
+ Args:
216
+ record_id: Unique identifier of the record
217
+
218
+ Returns:
219
+ True if the record is selected, False otherwise (including missing records)
220
+ """
221
+ ...
222
+
223
+ @abstractmethod
224
+ def select(self, record_id: Any) -> bool:
225
+ """Mark record as selected.
226
+
227
+ Args:
228
+ record_id: Unique identifier of the record
229
+
230
+ Returns:
231
+ True if record was selected, False if not found
232
+ """
233
+ ...
234
+
235
+ @abstractmethod
236
+ def deselect(self, record_id: Any) -> bool:
237
+ """Mark record as unselected.
238
+
239
+ Args:
240
+ record_id: Unique identifier of the record
241
+
242
+ Returns:
243
+ True if record was deselected, False if not found
244
+ """
245
+ ...
246
+
247
+ @abstractmethod
248
+ def select_all(self, current_page_only: bool = False) -> int:
249
+ """Select all records (optionally only current page).
250
+
251
+ Args:
252
+ current_page_only: If True, select only records on current page
253
+
254
+ Returns:
255
+ Number of records selected
256
+ """
257
+ ...
258
+
259
+ @abstractmethod
260
+ def deselect_all(self, current_page_only: bool = False) -> int:
261
+ """Deselect all records (optionally only current page).
262
+
263
+ Args:
264
+ current_page_only: If True, deselect only records on current page
265
+
266
+ Returns:
267
+ Number of records deselected
268
+ """
269
+ ...
270
+
271
+ @abstractmethod
272
+ def selected(self, page: Optional[int] = None) -> List[Record]:
273
+ """Get selected records, optionally paginated.
274
+
275
+ Args:
276
+ page: Optional page number for paginating selected records
277
+
278
+ Returns:
279
+ List of selected record dictionaries
280
+ """
281
+ ...
282
+
283
+ @property
284
+ @abstractmethod
285
+ def selected_count(self) -> int:
286
+ """Number of selected records."""
287
+ ...
288
+
289
+ # Export
290
+ def save(
291
+ self,
292
+ path: str,
293
+ *,
294
+ selected_only: bool = False,
295
+ format: Optional[str] = None,
296
+ config: Any = None,
297
+ ) -> None:
298
+ """Export records to a file, choosing the format by extension.
299
+
300
+ Records are streamed into the writer, so a large export does not
301
+ materialize the whole dataset. The active `where`/`order` view is
302
+ respected — what you export is what the source currently shows.
303
+
304
+ Args:
305
+ path: Destination file path; its extension selects the format (CSV,
306
+ TSV, JSON, JSONL, XML, and — with the extras — Parquet, Feather,
307
+ HDF5).
308
+ selected_only: Export only selected records instead of all.
309
+ format: Explicit format name overriding the path extension.
310
+ config: Optional `FileSourceConfig` controlling encoding/delimiter/etc.
311
+ """
312
+ from bootstack.data.writers import write_records
313
+
314
+ write_records(
315
+ path, self._export_records(selected_only=selected_only), config, format=format
316
+ )
317
+
318
+ def export_csv(self, filepath: str, include_all: bool = True) -> None:
319
+ """Export records to a CSV file (streamed).
320
+
321
+ Args:
322
+ filepath: Path to output CSV file
323
+ include_all: If True, export all records; if False, export only selected
324
+ """
325
+ self.save(filepath, selected_only=not include_all, format="csv")
326
+
327
+ def _export_records(self, *, selected_only: bool = False, chunk_size: int = 1000):
328
+ """Yield user-facing records for export, paging in chunks (bounded memory)."""
329
+ if selected_only:
330
+ for record in self.selected():
331
+ yield self._public_record(record)
332
+ return
333
+ offset = 0
334
+ while True:
335
+ chunk = self.page_slice(offset, chunk_size)
336
+ if not chunk:
337
+ break
338
+ for record in chunk:
339
+ yield self._public_record(record)
340
+ offset += len(chunk)
341
+
342
+ # Index-based Access
343
+ @abstractmethod
344
+ def page_slice(self, start_index: int, count: int) -> List[Record]:
345
+ """Get records by start index and count (respects filter/sort).
346
+
347
+ Args:
348
+ start_index: Starting record index
349
+ count: Number of records to retrieve
350
+
351
+ Returns:
352
+ List of record dictionaries
353
+ """
354
+ ...
355
+
356
+ # Observable query — read-only filtered/sorted read
357
+ @abstractmethod
358
+ def _query(self, condition: "Condition | None", sort_keys: "List[SortKey]") -> List[Record]:
359
+ """Run a one-off filtered and sorted read, returning fresh records.
360
+
361
+ Unlike `page`, this does NOT use or modify the source's active
362
+ `where`/`order` view state — it answers an `observe()` query in
363
+ isolation so pagination is undisturbed.
364
+
365
+ Args:
366
+ condition: Filter condition (or `None` for all rows).
367
+ sort_keys: Ordering to apply (empty for source/natural order).
368
+
369
+ Returns:
370
+ List of matching record dictionaries.
371
+ """
372
+ ...
373
+
374
+ # Lifecycle / reorder — concrete defaults; subclasses may override
375
+ def reload(self) -> None:
376
+ """Re-read data from the underlying source.
377
+
378
+ Default is a no-op suitable for in-memory implementations.
379
+ File- and database-backed sources should override to re-query.
380
+ """
381
+ return None
382
+
383
+ def move(self, record_id: Any, target_index: int) -> bool:
384
+ """Reorder a record to a new position.
385
+
386
+ Default returns False (not supported). Subclasses that maintain
387
+ an explicit ordering should override.
388
+
389
+ Args:
390
+ record_id: Unique identifier of the record to move
391
+ target_index: Zero-based destination index (clamped to valid range)
392
+
393
+ Returns:
394
+ True if the record was moved, False if not supported or not found
395
+ """
396
+ return False
397
+
398
+ # ========== RESOURCE LIFECYCLE (close / context manager) ==========
399
+
400
+ def close(self) -> None:
401
+ """Release any resources held by this data source.
402
+
403
+ The default is a no-op — in-memory sources hold none. Sources backed by
404
+ a database connection or file handle override this to release it. Safe to
405
+ call more than once.
406
+ """
407
+ return None
408
+
409
+ def __enter__(self) -> "BaseDataSource":
410
+ """Enter a `with` block, returning the data source itself."""
411
+ return self
412
+
413
+ def __exit__(self, exc_type, exc, tb) -> bool:
414
+ """Exit a `with` block, releasing resources via `close()`.
415
+
416
+ Returns False so any exception raised inside the block propagates.
417
+ """
418
+ self.close()
419
+ return False
420
+
421
+ # ========== ROW IDENTITY (for datasource-aware widgets) ==========
422
+
423
+ @property
424
+ def id_field(self) -> str:
425
+ """Name of the record field that holds the stable row identity."""
426
+ return getattr(self, "_id_field", "id")
427
+
428
+ def _internal_fields(self) -> "frozenset[str]":
429
+ """Raw-record keys that are implementation-internal and hidden from users.
430
+
431
+ Datasource-aware widgets (for example `Table`) strip these from displayed
432
+ columns, exports, and the records they hand back. The default is none;
433
+ sources that carry bookkeeping columns override this.
434
+
435
+ Returns:
436
+ A set of record keys to treat as private.
437
+ """
438
+ return frozenset()
439
+
440
+ def _record_id(self, record: "Mapping[str, Any] | None") -> Any:
441
+ """Return the stable identity of a raw record.
442
+
443
+ The returned value is what `get`/`update`/`delete`/`select` accept and
444
+ what is surfaced publicly as the record's `id`. The default reads the
445
+ configured `id_field`; sources with a separate identity column override.
446
+
447
+ Args:
448
+ record: A raw record as returned by `page`/`page_slice`.
449
+
450
+ Returns:
451
+ The record's identity, or `None` when it cannot be determined.
452
+ """
453
+ if not record:
454
+ return None
455
+ return record.get(self.id_field)
456
+
457
+ def _public_record(self, record: "Mapping[str, Any] | None") -> Record:
458
+ """Return a user-facing copy of a raw record.
459
+
460
+ Internal bookkeeping fields are stripped and the stable identity is
461
+ surfaced as `id`, so widgets and callers see a clean record regardless
462
+ of how the source stores it.
463
+
464
+ Args:
465
+ record: A raw record as returned by `page`/`page_slice`.
466
+
467
+ Returns:
468
+ A new dict with internal fields removed and `id` populated.
469
+ """
470
+ if not record:
471
+ return {}
472
+ internal = self._internal_fields()
473
+ out = {k: v for k, v in record.items() if k not in internal}
474
+ rid = self._record_id(record)
475
+ if rid is not None:
476
+ out["id"] = rid
477
+ return out
478
+
479
+ # ========== CHANGE BROADCASTING (on_change / observe) ==========
480
+
481
+ def on_change(self, handler: "Callable[[Any], Any] | None" = None) -> Any:
482
+ """Subscribe to changes to this source.
483
+
484
+ Call with no argument to get a composable `Stream` of coarse change
485
+ events; chain `map`/`filter`/`debounce` and `listen` to drive any
486
+ widget (for example, a dashboard badge bound to the row count). Call
487
+ with a handler to subscribe directly and get back a cancellable
488
+ subscription.
489
+
490
+ The handler receives a `DataChangeEvent`. Rapid mutations are coalesced
491
+ into a single notification per event-loop turn, and mutations made from
492
+ a background thread are delivered on the main thread automatically — so
493
+ a bound widget can refresh from a worker-thread feed with no extra work.
494
+
495
+ Args:
496
+ handler: Change handler. Omit to receive a `Stream` instead.
497
+
498
+ Returns:
499
+ A `Stream` when `handler` is omitted, otherwise a cancellable
500
+ subscription handle.
501
+
502
+ Example:
503
+ .. code-block:: python
504
+
505
+ ds.on_change(lambda e: print("changed:", e.kind))
506
+
507
+ # Feed a dashboard badge with the live row count.
508
+ ds.on_change().map(lambda e: ds.count).listen(badge.set_value)
509
+ """
510
+ from bootstack.streams import Handle, Stream
511
+
512
+ hub = self._hub
513
+ if handler is not None:
514
+ return Handle(hub.add_listener(handler))
515
+
516
+ from bootstack._runtime.app import get_current_app, has_current_app
517
+
518
+ if not has_current_app():
519
+ raise RuntimeError(
520
+ "on_change() with no handler returns a Stream, which requires an "
521
+ "active App. Pass a handler — on_change(fn) — for headless use."
522
+ )
523
+ owner = get_current_app()
524
+
525
+ def _source(downstream):
526
+ return Handle(hub.add_listener(downstream))
527
+
528
+ return Stream(owner, _source=_source)
529
+
530
+ def observe(self, condition: "Condition | None" = None, *order: "str | Column | SortKey") -> Any:
531
+ """Observe a live result set for a `where`/`order` query.
532
+
533
+ Returns a `Stream` that emits the matching records immediately, then a
534
+ fresh result set whenever a relevant change occurs. Each subscriber
535
+ observes its own slice — declare the query once, react to its results
536
+ over time (the "observable query" pattern).
537
+
538
+ Selection toggles do not re-emit (selection is not a row-set change).
539
+ Unlike `where`/`order`, observing does not disturb the source's own
540
+ pagination view.
541
+
542
+ Performance: each relevant change re-runs the whole query and re-emits
543
+ the full result set. Use `observe` for *small* derived sets — dashboard
544
+ metrics, a short pinned list, a filtered side panel. For large or
545
+ virtualized views (`Table`, `ListView`) do NOT observe the full set;
546
+ bind those widgets to the source directly — they listen via `on_change`
547
+ and refetch only their visible window with `page`/`page_slice`.
548
+
549
+ Args:
550
+ condition: Filter condition built with `col` (or `None` for all rows).
551
+ order: Sort keys — column names, `"-name"` for descending, or
552
+ `col(...)` specs.
553
+
554
+ Returns:
555
+ A `Stream` of result sets (each a list of record dictionaries).
556
+
557
+ Example:
558
+ .. code-block:: python
559
+
560
+ ds.observe(col("status") == "active", "-created").listen(
561
+ lambda rows: gauge.set_value(len(rows))
562
+ )
563
+ """
564
+ from bootstack.data._observable import _ObservedQuery
565
+ from bootstack.data.query import normalize_sort_keys
566
+ from bootstack.streams import Handle, Stream
567
+ from bootstack._runtime.app import get_current_app, has_current_app
568
+
569
+ if not has_current_app():
570
+ raise RuntimeError(
571
+ "observe() returns a Stream, which requires an active App."
572
+ )
573
+ owner = get_current_app()
574
+ sort_keys = normalize_sort_keys(order)
575
+ hub = self._hub
576
+ source = self
577
+
578
+ def _source(downstream):
579
+ query = _ObservedQuery(source, condition, sort_keys, downstream)
580
+ remove = hub.add_query(query)
581
+ try:
582
+ downstream(query.start())
583
+ except Exception:
584
+ pass
585
+ return Handle(remove)
586
+
587
+ return Stream(owner, _source=_source)
588
+
589
+ def _silence(self):
590
+ """Context manager that suppresses change emission within its block.
591
+
592
+ Used internally by widgets to mutate their own bound source without
593
+ broadcasting the change back to themselves.
594
+ """
595
+ return self._hub.silence()
596
+
597
+ # ========== SHARED UTILITY METHODS ==========
598
+
599
+ @staticmethod
600
+ def _infer_type(value: Any) -> str:
601
+ """Infer SQL-compatible type from Python value.
602
+
603
+ Args:
604
+ value: Python value to infer type from
605
+
606
+ Returns:
607
+ SQL type string ('INTEGER', 'REAL', 'BLOB', or 'TEXT')
608
+
609
+ Examples:
610
+ >>> BaseDataSource._infer_type(42)
611
+ 'INTEGER'
612
+ >>> BaseDataSource._infer_type(3.14)
613
+ 'REAL'
614
+ >>> BaseDataSource._infer_type("hello")
615
+ 'TEXT'
616
+ """
617
+ if isinstance(value, int):
618
+ return "INTEGER"
619
+ elif isinstance(value, float):
620
+ return "REAL"
621
+ elif isinstance(value, (bytes, bytearray)):
622
+ return "BLOB"
623
+ return "TEXT"
624
+
625
+ @staticmethod
626
+ def _is_mapping(x: Any) -> bool:
627
+ """Check if value is a mapping (dict-like).
628
+
629
+ Args:
630
+ x: Value to check
631
+
632
+ Returns:
633
+ True if value is a mapping, False otherwise
634
+
635
+ Examples:
636
+ >>> BaseDataSource._is_mapping({'a': 1})
637
+ True
638
+ >>> BaseDataSource._is_mapping([1, 2, 3])
639
+ False
640
+ """
641
+ return isinstance(x, Mapping)
642
+
643
+ @staticmethod
644
+ def _coerce_literal(s: str) -> Any:
645
+ """Parse string literal into Python value (int, float, bool, None, or str).
646
+
647
+ Converts SQL/JSON-like literal strings into appropriate Python types:
648
+ - Quoted strings ('foo', "bar") -> str
649
+ - true/false -> bool
650
+ - null/none -> None
651
+ - Numeric strings -> int or float
652
+ - Everything else -> str
653
+
654
+ Args:
655
+ s: String literal to parse
656
+
657
+ Returns:
658
+ Parsed Python value
659
+
660
+ Examples:
661
+ >>> BaseDataSource._coerce_literal("'hello'")
662
+ 'hello'
663
+ >>> BaseDataSource._coerce_literal("42")
664
+ 42
665
+ >>> BaseDataSource._coerce_literal("3.14")
666
+ 3.14
667
+ >>> BaseDataSource._coerce_literal("true")
668
+ True
669
+ >>> BaseDataSource._coerce_literal("null")
670
+ None
671
+ """
672
+ t = s.strip()
673
+
674
+ # Handle quoted strings
675
+ if len(t) >= 2 and ((t[0] == t[-1] == "'") or (t[0] == t[-1] == '"')):
676
+ return t[1:-1]
677
+
678
+ # Handle booleans
679
+ if t.lower() == "true":
680
+ return True
681
+ if t.lower() == "false":
682
+ return False
683
+
684
+ # Handle null/none
685
+ if t.lower() in ("null", "none"):
686
+ return None
687
+
688
+ # Try numeric conversion
689
+ try:
690
+ return int(t)
691
+ except Exception:
692
+ pass
693
+
694
+ try:
695
+ return float(t)
696
+ except Exception:
697
+ pass
698
+
699
+ # Default to string
700
+ return t
701
+
702
+ @staticmethod
703
+ def _validate_record(record: Any) -> None:
704
+ """Validate that a record is a dictionary.
705
+
706
+ Args:
707
+ record: Value to validate
708
+
709
+ Raises:
710
+ ValueError: If record is not a dictionary
711
+ """
712
+ if not isinstance(record, dict):
713
+ raise ValueError(f"Record must be a dictionary, got {type(record).__name__}")
714
+
715
+ # ========== HOOK METHODS (Can be overridden by subclasses) ==========
716
+
717
+ def _before_create(self, record: Dict[str, Any]) -> Dict[str, Any]:
718
+ """Hook called before creating a record.
719
+
720
+ Subclasses can override this to perform validation, transformation,
721
+ or side effects before record creation.
722
+
723
+ Args:
724
+ record: Record about to be created
725
+
726
+ Returns:
727
+ Modified record (or original if no changes)
728
+ """
729
+ return record
730
+
731
+ def _after_create(self, record_id: int, record: Dict[str, Any]) -> None:
732
+ """Hook called after creating a record.
733
+
734
+ Subclasses can override this to perform logging, caching updates,
735
+ or other side effects after record creation.
736
+
737
+ Args:
738
+ record_id: ID of the created record
739
+ record: The created record
740
+ """
741
+ pass
742
+
743
+ def _before_update(self, record_id: Any, updates: Dict[str, Any]) -> Dict[str, Any]:
744
+ """Hook called before updating a record.
745
+
746
+ Args:
747
+ record_id: ID of record being updated
748
+ updates: Updates to apply
749
+
750
+ Returns:
751
+ Modified updates (or original if no changes)
752
+ """
753
+ return updates
754
+
755
+ def _after_update(self, record_id: Any, updates: Dict[str, Any], success: bool) -> None:
756
+ """Hook called after updating a record.
757
+
758
+ Args:
759
+ record_id: ID of updated record
760
+ updates: Updates that were applied
761
+ success: Whether the update succeeded
762
+ """
763
+ pass
764
+
765
+ def _before_delete(self, record_id: Any) -> None:
766
+ """Hook called before deleting a record.
767
+
768
+ Args:
769
+ record_id: ID of record about to be deleted
770
+ """
771
+ pass
772
+
773
+ def _after_delete(self, record_id: Any, success: bool) -> None:
774
+ """Hook called after deleting a record.
775
+
776
+ Args:
777
+ record_id: ID of deleted record
778
+ success: Whether the deletion succeeded
779
+ """
780
+ pass