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,367 @@
1
+ """Expressive, SQL-free filtering and sorting for data sources.
2
+
3
+ Build filter conditions with `col` and combine them with the usual operators::
4
+
5
+ from bootstack.data import col
6
+
7
+ ds.where(col("age") >= 25)
8
+ ds.where((col("status") == "active") & col("name").contains("ada"))
9
+ ds.where(col("department").is_in(["Sales", "Engineering"]))
10
+ ds.order("-salary", "name") # '-' prefix sorts descending
11
+
12
+ A condition is backend-agnostic: in-memory sources evaluate it with
13
+ `matches()`, while SQLite renders it to a parameterized query with `to_sql()`
14
+ (values are always bound, never interpolated, so user input can't inject SQL).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from dataclasses import dataclass, field
19
+ from typing import Any, List, Mapping, Sequence, Tuple, Union
20
+
21
+ __all__ = [
22
+ "col",
23
+ "Column",
24
+ "Condition",
25
+ "SortKey",
26
+ "any_of",
27
+ "all_of",
28
+ ]
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # SQL rendering helpers
33
+ # ---------------------------------------------------------------------------
34
+
35
+ _SQL_OPS = {"eq": "=", "ne": "!=", "lt": "<", "le": "<=", "gt": ">", "ge": ">="}
36
+
37
+
38
+ def _quote(identifier: str) -> str:
39
+ """Quote a column identifier for safe use in SQL."""
40
+ return '"' + str(identifier).replace('"', '""') + '"'
41
+
42
+
43
+ def _escape_like(value: str) -> str:
44
+ """Escape LIKE wildcards so they match literally (paired with ESCAPE '\\')."""
45
+ return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
46
+
47
+
48
+ SqlFragment = Tuple[str, List[Any]]
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # Conditions
53
+ # ---------------------------------------------------------------------------
54
+
55
+ class Condition:
56
+ """A filter expression. Combine with `&` (and), `|` (or), and `~` (not)."""
57
+
58
+ def __and__(self, other: "Condition") -> "Condition":
59
+ return And([self, other])
60
+
61
+ def __or__(self, other: "Condition") -> "Condition":
62
+ return Or([self, other])
63
+
64
+ def __invert__(self) -> "Condition":
65
+ return Not(self)
66
+
67
+ def matches(self, row: Mapping[str, Any]) -> bool:
68
+ """Return whether `row` satisfies this condition (in-memory evaluation)."""
69
+ raise NotImplementedError
70
+
71
+ def to_sql(self) -> SqlFragment:
72
+ """Render to a parameterized SQL fragment: `(clause, params)`."""
73
+ raise NotImplementedError
74
+
75
+
76
+ @dataclass
77
+ class Compare(Condition):
78
+ """A value comparison: `column <op> value`."""
79
+
80
+ column: str
81
+ op: str # eq, ne, lt, le, gt, ge
82
+ value: Any
83
+
84
+ def matches(self, row: Mapping[str, Any]) -> bool:
85
+ actual = row.get(self.column)
86
+ if self.op == "eq":
87
+ return actual == self.value
88
+ if self.op == "ne":
89
+ return actual != self.value
90
+ if actual is None or self.value is None:
91
+ return False
92
+ try:
93
+ if self.op == "lt":
94
+ return actual < self.value
95
+ if self.op == "le":
96
+ return actual <= self.value
97
+ if self.op == "gt":
98
+ return actual > self.value
99
+ if self.op == "ge":
100
+ return actual >= self.value
101
+ except TypeError:
102
+ return False
103
+ return False
104
+
105
+ def to_sql(self) -> SqlFragment:
106
+ q = _quote(self.column)
107
+ if self.value is None and self.op in ("eq", "ne"):
108
+ return (f"{q} IS NULL" if self.op == "eq" else f"{q} IS NOT NULL", [])
109
+ return (f"{q} {_SQL_OPS[self.op]} ?", [self.value])
110
+
111
+
112
+ @dataclass
113
+ class Match(Condition):
114
+ """A case-insensitive text match: contains / startswith / endswith."""
115
+
116
+ column: str
117
+ kind: str # contains, startswith, endswith
118
+ value: str
119
+
120
+ def matches(self, row: Mapping[str, Any]) -> bool:
121
+ actual = row.get(self.column)
122
+ if actual is None:
123
+ return False
124
+ haystack = str(actual).lower()
125
+ needle = str(self.value).lower()
126
+ if self.kind == "contains":
127
+ return needle in haystack
128
+ if self.kind == "startswith":
129
+ return haystack.startswith(needle)
130
+ if self.kind == "endswith":
131
+ return haystack.endswith(needle)
132
+ return False
133
+
134
+ def to_sql(self) -> SqlFragment:
135
+ esc = _escape_like(str(self.value))
136
+ pattern = {
137
+ "contains": f"%{esc}%",
138
+ "startswith": f"{esc}%",
139
+ "endswith": f"%{esc}",
140
+ }[self.kind]
141
+ return (f"{_quote(self.column)} LIKE ? ESCAPE '\\'", [pattern])
142
+
143
+
144
+ @dataclass
145
+ class In(Condition):
146
+ """Membership test: `column IN (values)`."""
147
+
148
+ column: str
149
+ values: List[Any]
150
+
151
+ def matches(self, row: Mapping[str, Any]) -> bool:
152
+ return row.get(self.column) in self.values
153
+
154
+ def to_sql(self) -> SqlFragment:
155
+ if not self.values:
156
+ return ("0", []) # IN () matches nothing
157
+ placeholders = ", ".join("?" for _ in self.values)
158
+ return (f"{_quote(self.column)} IN ({placeholders})", list(self.values))
159
+
160
+
161
+ @dataclass
162
+ class IsNull(Condition):
163
+ """Null test: `column IS NULL`."""
164
+
165
+ column: str
166
+
167
+ def matches(self, row: Mapping[str, Any]) -> bool:
168
+ return row.get(self.column) is None
169
+
170
+ def to_sql(self) -> SqlFragment:
171
+ return (f"{_quote(self.column)} IS NULL", [])
172
+
173
+
174
+ @dataclass
175
+ class And(Condition):
176
+ """All sub-conditions must hold."""
177
+
178
+ parts: List[Condition] = field(default_factory=list)
179
+
180
+ def matches(self, row: Mapping[str, Any]) -> bool:
181
+ return all(p.matches(row) for p in self.parts)
182
+
183
+ def to_sql(self) -> SqlFragment:
184
+ return _combine(self.parts, " AND ", empty="1")
185
+
186
+
187
+ @dataclass
188
+ class Or(Condition):
189
+ """At least one sub-condition must hold."""
190
+
191
+ parts: List[Condition] = field(default_factory=list)
192
+
193
+ def matches(self, row: Mapping[str, Any]) -> bool:
194
+ return any(p.matches(row) for p in self.parts)
195
+
196
+ def to_sql(self) -> SqlFragment:
197
+ return _combine(self.parts, " OR ", empty="0")
198
+
199
+
200
+ @dataclass
201
+ class Not(Condition):
202
+ """Negates a condition."""
203
+
204
+ part: Condition
205
+
206
+ def matches(self, row: Mapping[str, Any]) -> bool:
207
+ return not self.part.matches(row)
208
+
209
+ def to_sql(self) -> SqlFragment:
210
+ clause, params = self.part.to_sql()
211
+ return (f"NOT ({clause})", params)
212
+
213
+
214
+ def _combine(parts: Sequence[Condition], joiner: str, *, empty: str) -> SqlFragment:
215
+ if not parts:
216
+ return (empty, [])
217
+ clauses: List[str] = []
218
+ params: List[Any] = []
219
+ for part in parts:
220
+ clause, p = part.to_sql()
221
+ clauses.append(clause)
222
+ params.extend(p)
223
+ return ("(" + joiner.join(clauses) + ")", params)
224
+
225
+
226
+ # ---------------------------------------------------------------------------
227
+ # Column reference (builds conditions / sort keys)
228
+ # ---------------------------------------------------------------------------
229
+
230
+ class Column:
231
+ """A reference to a column. Build conditions and sort keys from it.
232
+
233
+ Created by `col(name)`. Supports the comparison operators (`==`, `!=`,
234
+ `<`, `<=`, `>`, `>=`), text matching (`contains`, `startswith`, `endswith`),
235
+ membership (`is_in`), null tests (`is_null`, `is_not_null`), and sort
236
+ direction (`asc`, `desc`).
237
+ """
238
+
239
+ __slots__ = ("name",)
240
+
241
+ def __init__(self, name: str) -> None:
242
+ self.name = name
243
+
244
+ # comparisons
245
+ def __eq__(self, other: Any) -> Condition: # type: ignore[override]
246
+ return Compare(self.name, "eq", other)
247
+
248
+ def __ne__(self, other: Any) -> Condition: # type: ignore[override]
249
+ return Compare(self.name, "ne", other)
250
+
251
+ def __lt__(self, other: Any) -> Condition:
252
+ return Compare(self.name, "lt", other)
253
+
254
+ def __le__(self, other: Any) -> Condition:
255
+ return Compare(self.name, "le", other)
256
+
257
+ def __gt__(self, other: Any) -> Condition:
258
+ return Compare(self.name, "gt", other)
259
+
260
+ def __ge__(self, other: Any) -> Condition:
261
+ return Compare(self.name, "ge", other)
262
+
263
+ __hash__ = None # type: ignore[assignment]
264
+
265
+ # text
266
+ def contains(self, value: str) -> Condition:
267
+ """Case-insensitive substring match."""
268
+ return Match(self.name, "contains", value)
269
+
270
+ def startswith(self, value: str) -> Condition:
271
+ """Case-insensitive prefix match."""
272
+ return Match(self.name, "startswith", value)
273
+
274
+ def endswith(self, value: str) -> Condition:
275
+ """Case-insensitive suffix match."""
276
+ return Match(self.name, "endswith", value)
277
+
278
+ # set / null
279
+ def is_in(self, values: Sequence[Any]) -> Condition:
280
+ """Match any value in `values`."""
281
+ return In(self.name, list(values))
282
+
283
+ def is_null(self) -> Condition:
284
+ """Match rows where this column is null/missing."""
285
+ return IsNull(self.name)
286
+
287
+ def is_not_null(self) -> Condition:
288
+ """Match rows where this column is not null."""
289
+ return Not(IsNull(self.name))
290
+
291
+ # sort
292
+ def asc(self) -> "SortKey":
293
+ """Sort by this column, ascending."""
294
+ return SortKey(self.name, False)
295
+
296
+ def desc(self) -> "SortKey":
297
+ """Sort by this column, descending."""
298
+ return SortKey(self.name, True)
299
+
300
+ def __repr__(self) -> str:
301
+ return f"col({self.name!r})"
302
+
303
+
304
+ def col(name: str) -> Column:
305
+ """Reference a column by name for use in `where()` / `order()`."""
306
+ return Column(name)
307
+
308
+
309
+ # ---------------------------------------------------------------------------
310
+ # Sorting
311
+ # ---------------------------------------------------------------------------
312
+
313
+ @dataclass
314
+ class SortKey:
315
+ """A single sort term: a column and a direction."""
316
+
317
+ column: str
318
+ descending: bool = False
319
+
320
+
321
+ SortSpec = Union[str, Column, SortKey]
322
+
323
+
324
+ def normalize_sort_keys(keys: Sequence[SortSpec]) -> List[SortKey]:
325
+ """Normalize mixed sort specs into a list of `SortKey`.
326
+
327
+ Accepts `"name"` (ascending), `"-name"` (descending), a `Column`
328
+ (ascending), or a `SortKey`.
329
+ """
330
+ result: List[SortKey] = []
331
+ for key in keys:
332
+ if isinstance(key, SortKey):
333
+ result.append(key)
334
+ elif isinstance(key, Column):
335
+ result.append(SortKey(key.name, False))
336
+ elif isinstance(key, str):
337
+ if key.startswith("-"):
338
+ result.append(SortKey(key[1:], True))
339
+ else:
340
+ result.append(SortKey(key, False))
341
+ else:
342
+ raise TypeError(f"Invalid sort key: {key!r}")
343
+ return result
344
+
345
+
346
+ # ---------------------------------------------------------------------------
347
+ # Combinators
348
+ # ---------------------------------------------------------------------------
349
+
350
+ def any_of(*conditions: Condition) -> Condition | None:
351
+ """Combine conditions with OR. Returns None if none are given."""
352
+ parts = [c for c in conditions if c is not None]
353
+ if not parts:
354
+ return None
355
+ if len(parts) == 1:
356
+ return parts[0]
357
+ return Or(parts)
358
+
359
+
360
+ def all_of(*conditions: Condition) -> Condition | None:
361
+ """Combine conditions with AND. Returns None if none are given."""
362
+ parts = [c for c in conditions if c is not None]
363
+ if not parts:
364
+ return None
365
+ if len(parts) == 1:
366
+ return parts[0]
367
+ return And(parts)
@@ -0,0 +1,289 @@
1
+ """Streaming record readers for file-backed data sources.
2
+
3
+ A *reader* turns a file into a lazy iterator of record dicts, parsed a row at a
4
+ time so large files stream with bounded memory rather than loading all at once.
5
+ Readers are keyed by file extension in a registry: the stdlib formats
6
+ (CSV / TSV / JSON / JSONL / XML) are always available, and the optional columnar
7
+ and scientific formats (Parquet / Feather / HDF5) register too but raise a clear
8
+ "install the extra" error when used without their dependency.
9
+
10
+ The reader contract is intentionally small — `reader(path, config) ->
11
+ Iterator[Record]` — so it composes directly with the streaming
12
+ `SqliteDataSource.load(...)`, which chunks the iterator itself.
13
+
14
+ Register a custom reader for your own extension::
15
+
16
+ from bootstack.data.readers import register_reader
17
+
18
+ @register_reader(".myfmt")
19
+ def read_myfmt(path, config):
20
+ for record in parse(path):
21
+ yield record
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import csv
27
+ import importlib
28
+ import json
29
+ from pathlib import Path
30
+ from typing import TYPE_CHECKING, Callable, Dict, Iterator, List, Optional
31
+
32
+ from bootstack.data.types import Record
33
+
34
+ if TYPE_CHECKING:
35
+ from bootstack.data.file_source import FileSourceConfig
36
+
37
+ # A reader yields record dicts lazily: (path, config) -> Iterator[Record].
38
+ Reader = Callable[..., Iterator[Record]]
39
+
40
+ _READERS: Dict[str, Reader] = {}
41
+
42
+ # Format-name aliases (FileSourceConfig.file_format) → canonical extension.
43
+ _FORMAT_ALIASES: Dict[str, str] = {
44
+ "csv": ".csv",
45
+ "tsv": ".tsv",
46
+ "json": ".json",
47
+ "jsonl": ".jsonl",
48
+ "ndjson": ".jsonl",
49
+ "xml": ".xml",
50
+ "parquet": ".parquet",
51
+ "feather": ".feather",
52
+ "hdf5": ".h5",
53
+ }
54
+
55
+
56
+ def _norm_ext(ext: str) -> str:
57
+ """Normalize an extension to lowercase with a leading dot."""
58
+ ext = ext.lower()
59
+ return ext if ext.startswith(".") else "." + ext
60
+
61
+
62
+ def register_reader(*extensions: str, reader: Optional[Reader] = None):
63
+ """Register a reader for one or more file extensions.
64
+
65
+ Use as a decorator (`@register_reader(".csv", ".tsv")`) or call directly with
66
+ `reader=`. Extensions may be given with or without the leading dot.
67
+
68
+ Args:
69
+ extensions: File extensions the reader handles.
70
+ reader: The reader callable, when not used as a decorator.
71
+
72
+ Returns:
73
+ The reader (so it works as a decorator).
74
+ """
75
+
76
+ def _apply(fn: Reader) -> Reader:
77
+ for ext in extensions:
78
+ _READERS[_norm_ext(ext)] = fn
79
+ return fn
80
+
81
+ return _apply(reader) if reader is not None else _apply
82
+
83
+
84
+ def supported_read_extensions() -> List[str]:
85
+ """Return the sorted list of registered readable file extensions."""
86
+ return sorted(_READERS)
87
+
88
+
89
+ def get_reader(path_or_ext: "str | Path") -> Reader:
90
+ """Resolve the reader for a path or extension.
91
+
92
+ Args:
93
+ path_or_ext: A file path or a bare extension (e.g. `".csv"` or `"csv"`).
94
+
95
+ Returns:
96
+ The registered reader callable.
97
+
98
+ Raises:
99
+ ValueError: If no reader is registered for the extension.
100
+ """
101
+ suffix = Path(str(path_or_ext)).suffix or str(path_or_ext)
102
+ ext = _norm_ext(suffix)
103
+ try:
104
+ return _READERS[ext]
105
+ except KeyError:
106
+ raise ValueError(
107
+ f"No reader registered for {ext!r}. Supported formats: "
108
+ f"{', '.join(supported_read_extensions())}."
109
+ ) from None
110
+
111
+
112
+ def read_records(path: "str | Path", config: "Optional[FileSourceConfig]" = None) -> Iterator[Record]:
113
+ """Stream records from a file, choosing the reader by format or extension.
114
+
115
+ Honors `config.file_format` when set to something other than `'auto'`,
116
+ otherwise resolves the reader from the path's extension.
117
+
118
+ Args:
119
+ path: Path to the data file.
120
+ config: Parsing configuration (a default `FileSourceConfig` is used if
121
+ omitted).
122
+
123
+ Returns:
124
+ A lazy iterator of record dicts.
125
+ """
126
+ from bootstack.data.file_source import FileSourceConfig
127
+
128
+ cfg = config or FileSourceConfig()
129
+ fmt = getattr(cfg, "file_format", "auto")
130
+ if fmt and fmt != "auto":
131
+ reader = get_reader(_FORMAT_ALIASES.get(fmt, fmt))
132
+ else:
133
+ reader = get_reader(path)
134
+ return reader(path, cfg)
135
+
136
+
137
+ def _require(module: str, extra: str):
138
+ """Import an optional dependency or raise a clear install hint."""
139
+ try:
140
+ return importlib.import_module(module)
141
+ except ImportError as exc:
142
+ raise ImportError(
143
+ f"Reading this format needs the optional {module!r} dependency. "
144
+ f"Install it with: pip install bootstack[{extra}]"
145
+ ) from exc
146
+
147
+
148
+ # --------------------------------------------------------------------------- stdlib
149
+
150
+
151
+ @register_reader(".csv", ".tsv", ".txt")
152
+ def read_csv(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
153
+ """Stream records from a CSV/TSV file, one row at a time."""
154
+ delimiter = config.delimiter
155
+ if delimiter is None:
156
+ delimiter = "\t" if _norm_ext(Path(path).suffix) == ".tsv" else ","
157
+ with open(path, "r", encoding=config.encoding, newline="") as f:
158
+ for _ in range(config.skip_rows or 0):
159
+ next(f, None)
160
+ reader = csv.DictReader(f, delimiter=delimiter, quotechar=config.quotechar)
161
+ for row in reader:
162
+ yield dict(row)
163
+
164
+
165
+ @register_reader(".jsonl", ".ndjson")
166
+ def read_jsonl(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
167
+ """Stream records from a line-delimited JSON file (one object per line)."""
168
+ with open(path, "r", encoding=config.encoding) as f:
169
+ for line in f:
170
+ line = line.strip()
171
+ if not line:
172
+ continue
173
+ obj = json.loads(line)
174
+ yield obj if isinstance(obj, dict) else {"value": obj}
175
+
176
+
177
+ @register_reader(".json")
178
+ def read_json(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
179
+ """Read records from a JSON file.
180
+
181
+ Supported shapes:
182
+
183
+ - a top-level **array of objects** → one record per element;
184
+ - a top-level **object** with its records under `config.json_records_key`
185
+ (e.g. an API response like `{"data": [...]}`) → that list;
186
+ - any other top-level **object** → a single record.
187
+
188
+ A JSON array is parsed in full — the stdlib parser cannot stream an array, so
189
+ prefer JSONL / `.ndjson` (or set `config.json_lines`) for very large data.
190
+ """
191
+ if getattr(config, "json_lines", False):
192
+ yield from read_jsonl(path, config)
193
+ return
194
+ with open(path, "r", encoding=config.encoding) as f:
195
+ data = json.load(f)
196
+
197
+ key = getattr(config, "json_records_key", None)
198
+ if key is not None:
199
+ items = data.get(key) if isinstance(data, dict) else None
200
+ if not isinstance(items, list):
201
+ raise ValueError(
202
+ f"json_records_key={key!r} did not resolve to a list of records in {path}."
203
+ )
204
+ for item in items:
205
+ yield item if isinstance(item, dict) else {"value": item}
206
+ return
207
+
208
+ if isinstance(data, list):
209
+ for item in data:
210
+ yield item if isinstance(item, dict) else {"value": item}
211
+ elif isinstance(data, dict):
212
+ yield data
213
+
214
+
215
+ @register_reader(".xml")
216
+ def read_xml(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
217
+ """Stream records from an XML file using incremental parsing.
218
+
219
+ Each record element (those matching `config.xml_record_tag`, or the direct
220
+ children of the root when it is unset) becomes one record: its attributes and
221
+ the text of its leaf child elements become fields. Parsed elements are cleared
222
+ so memory stays bounded. Nested/repeated children are skipped in this version.
223
+ """
224
+ import xml.etree.ElementTree as ET
225
+
226
+ record_tag = getattr(config, "xml_record_tag", None)
227
+ depth = 0
228
+ root = None
229
+ for event, elem in ET.iterparse(path, events=("start", "end")):
230
+ if event == "start":
231
+ if depth == 0:
232
+ root = elem
233
+ depth += 1
234
+ continue
235
+ # end event
236
+ depth -= 1
237
+ is_record = (elem.tag == record_tag) if record_tag else (depth == 1)
238
+ if not is_record:
239
+ continue
240
+ record: Record = dict(elem.attrib)
241
+ for child in elem:
242
+ if len(child) == 0: # leaf element → a scalar field
243
+ record[child.tag] = child.text
244
+ yield record
245
+ elem.clear()
246
+ if root is not None:
247
+ root.clear() # drop processed siblings to keep memory bounded
248
+
249
+
250
+ # --------------------------------------------------------------------------- optional
251
+
252
+
253
+ @register_reader(".parquet")
254
+ def read_parquet(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
255
+ """Stream records from a Parquet file in row-group batches (needs pyarrow)."""
256
+ pq = _require("pyarrow.parquet", "parquet")
257
+ parquet_file = pq.ParquetFile(str(path))
258
+ for batch in parquet_file.iter_batches():
259
+ for row in batch.to_pylist():
260
+ yield row
261
+
262
+
263
+ @register_reader(".feather", ".arrow")
264
+ def read_feather(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
265
+ """Stream records from a Feather/Arrow IPC file in batches (needs pyarrow)."""
266
+ feather = _require("pyarrow.feather", "parquet")
267
+ table = feather.read_table(str(path))
268
+ for batch in table.to_batches():
269
+ for row in batch.to_pylist():
270
+ yield row
271
+
272
+
273
+ @register_reader(".h5", ".hdf5", ".hdf")
274
+ def read_hdf5(path: "str | Path", config: "FileSourceConfig") -> Iterator[Record]:
275
+ """Stream records from a tabular HDF5 file in chunks (needs pandas + tables)."""
276
+ pd = _require("pandas", "hdf5")
277
+ key = getattr(config, "hdf5_key", None)
278
+ store = pd.HDFStore(str(path), mode="r")
279
+ try:
280
+ if key is None:
281
+ keys = store.keys()
282
+ if not keys:
283
+ return
284
+ key = keys[0]
285
+ for chunk in store.select(key, chunksize=10_000):
286
+ for record in chunk.to_dict(orient="records"):
287
+ yield record
288
+ finally:
289
+ store.close()