solara-ui 1.45.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 (464) hide show
  1. prefix/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  2. prefix/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  3. solara/__init__.py +124 -0
  4. solara/__main__.py +765 -0
  5. solara/_stores.py +297 -0
  6. solara/alias.py +6 -0
  7. solara/autorouting.py +555 -0
  8. solara/cache.py +305 -0
  9. solara/checks.html +71 -0
  10. solara/checks.py +227 -0
  11. solara/comm.py +28 -0
  12. solara/components/__init__.py +77 -0
  13. solara/components/alert.py +155 -0
  14. solara/components/applayout.py +397 -0
  15. solara/components/button.py +85 -0
  16. solara/components/card.py +87 -0
  17. solara/components/checkbox.py +50 -0
  18. solara/components/code_highlight_css.py +11 -0
  19. solara/components/code_highlight_css.vue +63 -0
  20. solara/components/columns.py +159 -0
  21. solara/components/component_vue.py +134 -0
  22. solara/components/cross_filter.py +335 -0
  23. solara/components/dataframe.py +546 -0
  24. solara/components/datatable.py +214 -0
  25. solara/components/datatable.vue +175 -0
  26. solara/components/details.py +56 -0
  27. solara/components/download.vue +35 -0
  28. solara/components/echarts.py +86 -0
  29. solara/components/echarts.vue +139 -0
  30. solara/components/figure_altair.py +39 -0
  31. solara/components/file_browser.py +181 -0
  32. solara/components/file_download.py +199 -0
  33. solara/components/file_drop.py +159 -0
  34. solara/components/file_drop.vue +83 -0
  35. solara/components/file_list_widget.vue +78 -0
  36. solara/components/head.py +27 -0
  37. solara/components/head_tag.py +49 -0
  38. solara/components/head_tag.vue +60 -0
  39. solara/components/image.py +173 -0
  40. solara/components/input.py +456 -0
  41. solara/components/input_text_area.py +86 -0
  42. solara/components/link.py +55 -0
  43. solara/components/markdown.py +441 -0
  44. solara/components/markdown_editor.py +33 -0
  45. solara/components/markdown_editor.vue +359 -0
  46. solara/components/matplotlib.py +74 -0
  47. solara/components/meta.py +47 -0
  48. solara/components/misc.py +333 -0
  49. solara/components/pivot_table.py +258 -0
  50. solara/components/pivot_table.vue +158 -0
  51. solara/components/progress.py +47 -0
  52. solara/components/select.py +182 -0
  53. solara/components/select.vue +27 -0
  54. solara/components/slider.py +442 -0
  55. solara/components/slider_date.vue +56 -0
  56. solara/components/spinner-solara.vue +105 -0
  57. solara/components/spinner.py +45 -0
  58. solara/components/sql_code.py +41 -0
  59. solara/components/sql_code.vue +125 -0
  60. solara/components/style.py +105 -0
  61. solara/components/switch.py +71 -0
  62. solara/components/tab_navigation.py +37 -0
  63. solara/components/title.py +90 -0
  64. solara/components/title.vue +38 -0
  65. solara/components/togglebuttons.py +200 -0
  66. solara/components/tooltip.py +61 -0
  67. solara/core.py +42 -0
  68. solara/datatypes.py +143 -0
  69. solara/express.py +241 -0
  70. solara/hooks/__init__.py +4 -0
  71. solara/hooks/dataframe.py +99 -0
  72. solara/hooks/misc.py +263 -0
  73. solara/hooks/use_reactive.py +151 -0
  74. solara/hooks/use_thread.py +129 -0
  75. solara/kitchensink.py +8 -0
  76. solara/lab/__init__.py +34 -0
  77. solara/lab/components/__init__.py +7 -0
  78. solara/lab/components/chat.py +215 -0
  79. solara/lab/components/confirmation_dialog.py +163 -0
  80. solara/lab/components/cross_filter.py +7 -0
  81. solara/lab/components/input_date.py +339 -0
  82. solara/lab/components/input_time.py +133 -0
  83. solara/lab/components/menu.py +181 -0
  84. solara/lab/components/menu.vue +38 -0
  85. solara/lab/components/tabs.py +274 -0
  86. solara/lab/components/theming.py +98 -0
  87. solara/lab/components/theming.vue +72 -0
  88. solara/lab/hooks/__init__.py +0 -0
  89. solara/lab/hooks/dataframe.py +2 -0
  90. solara/lab/toestand.py +3 -0
  91. solara/lab/utils/__init__.py +2 -0
  92. solara/lab/utils/cookies.py +5 -0
  93. solara/lab/utils/dataframe.py +165 -0
  94. solara/lab/utils/headers.py +5 -0
  95. solara/layout.py +44 -0
  96. solara/lifecycle.py +46 -0
  97. solara/minisettings.py +141 -0
  98. solara/py.typed +0 -0
  99. solara/reactive.py +99 -0
  100. solara/routing.py +268 -0
  101. solara/scope/__init__.py +88 -0
  102. solara/scope/types.py +55 -0
  103. solara/server/__init__.py +0 -0
  104. solara/server/app.py +527 -0
  105. solara/server/assets/custom.css +1 -0
  106. solara/server/assets/custom.js +1 -0
  107. solara/server/assets/favicon.png +0 -0
  108. solara/server/assets/favicon.svg +5 -0
  109. solara/server/assets/style.css +1681 -0
  110. solara/server/assets/theme-dark.css +437 -0
  111. solara/server/assets/theme-light.css +420 -0
  112. solara/server/assets/theme.js +3 -0
  113. solara/server/cdn_helper.py +91 -0
  114. solara/server/esm.py +71 -0
  115. solara/server/fastapi.py +5 -0
  116. solara/server/flask.py +297 -0
  117. solara/server/jupyter/__init__.py +2 -0
  118. solara/server/jupyter/cdn_handler.py +28 -0
  119. solara/server/jupyter/server_extension.py +40 -0
  120. solara/server/jupyter/solara.py +91 -0
  121. solara/server/jupytertools.py +46 -0
  122. solara/server/kernel.py +388 -0
  123. solara/server/kernel_context.py +467 -0
  124. solara/server/patch.py +564 -0
  125. solara/server/pyinstaller/__init__.py +9 -0
  126. solara/server/pyinstaller/hook-ipyreact.py +5 -0
  127. solara/server/pyinstaller/hook-ipyvuetify.py +5 -0
  128. solara/server/pyinstaller/hook-solara.py +9 -0
  129. solara/server/qt.py +113 -0
  130. solara/server/reload.py +251 -0
  131. solara/server/server.py +484 -0
  132. solara/server/settings.py +249 -0
  133. solara/server/shell.py +269 -0
  134. solara/server/starlette.py +770 -0
  135. solara/server/static/ansi.js +270 -0
  136. solara/server/static/highlight-dark.css +82 -0
  137. solara/server/static/highlight.css +43 -0
  138. solara/server/static/main-vuetify.js +272 -0
  139. solara/server/static/main.js +163 -0
  140. solara/server/static/solara_bootstrap.py +129 -0
  141. solara/server/static/sun.svg +23 -0
  142. solara/server/static/webworker.js +42 -0
  143. solara/server/telemetry.py +212 -0
  144. solara/server/templates/index.html.j2 +1 -0
  145. solara/server/templates/loader-plain.css +11 -0
  146. solara/server/templates/loader-plain.html +20 -0
  147. solara/server/templates/loader-solara.css +111 -0
  148. solara/server/templates/loader-solara.html +40 -0
  149. solara/server/templates/plain.html +82 -0
  150. solara/server/templates/solara.html.j2 +486 -0
  151. solara/server/threaded.py +84 -0
  152. solara/server/utils.py +44 -0
  153. solara/server/websocket.py +45 -0
  154. solara/settings.py +86 -0
  155. solara/tasks.py +893 -0
  156. solara/template/button.py +16 -0
  157. solara/template/markdown.py +42 -0
  158. solara/template/portal/.flake8 +6 -0
  159. solara/template/portal/.pre-commit-config.yaml +28 -0
  160. solara/template/portal/LICENSE +21 -0
  161. solara/template/portal/Procfile +7 -0
  162. solara/template/portal/mypy.ini +3 -0
  163. solara/template/portal/pyproject.toml +26 -0
  164. solara/template/portal/solara_portal/__init__.py +4 -0
  165. solara/template/portal/solara_portal/components/__init__.py +2 -0
  166. solara/template/portal/solara_portal/components/article.py +28 -0
  167. solara/template/portal/solara_portal/components/data.py +28 -0
  168. solara/template/portal/solara_portal/components/header.py +6 -0
  169. solara/template/portal/solara_portal/components/layout.py +6 -0
  170. solara/template/portal/solara_portal/content/articles/equis-in-vidi.md +85 -0
  171. solara/template/portal/solara_portal/content/articles/substiterat-vati.md +70 -0
  172. solara/template/portal/solara_portal/data.py +60 -0
  173. solara/template/portal/solara_portal/pages/__init__.py +67 -0
  174. solara/template/portal/solara_portal/pages/article/__init__.py +26 -0
  175. solara/template/portal/solara_portal/pages/tabular.py +29 -0
  176. solara/template/portal/solara_portal/pages/viz/__init__.py +70 -0
  177. solara/template/portal/solara_portal/pages/viz/overview.py +14 -0
  178. solara/test/__init__.py +0 -0
  179. solara/test/pytest_plugin.py +783 -0
  180. solara/toestand.py +998 -0
  181. solara/util.py +348 -0
  182. solara/validate_hooks.py +258 -0
  183. solara/website/__init__.py +0 -0
  184. solara/website/assets/custom.css +444 -0
  185. solara/website/assets/images/logo-small.png +0 -0
  186. solara/website/assets/images/logo.svg +17 -0
  187. solara/website/assets/images/logo_white.svg +50 -0
  188. solara/website/assets/theme.js +8 -0
  189. solara/website/components/__init__.py +5 -0
  190. solara/website/components/algolia.py +6 -0
  191. solara/website/components/algolia.vue +24 -0
  192. solara/website/components/algolia_api.vue +202 -0
  193. solara/website/components/breadcrumbs.py +28 -0
  194. solara/website/components/contact.py +144 -0
  195. solara/website/components/docs.py +143 -0
  196. solara/website/components/header.py +75 -0
  197. solara/website/components/mailchimp.py +12 -0
  198. solara/website/components/mailchimp.vue +47 -0
  199. solara/website/components/markdown.py +99 -0
  200. solara/website/components/markdown_nav.vue +34 -0
  201. solara/website/components/notebook.py +171 -0
  202. solara/website/components/sidebar.py +105 -0
  203. solara/website/pages/__init__.py +370 -0
  204. solara/website/pages/about/__init__.py +9 -0
  205. solara/website/pages/about/about.md +3 -0
  206. solara/website/pages/apps/__init__.py +16 -0
  207. solara/website/pages/apps/authorization/__init__.py +119 -0
  208. solara/website/pages/apps/authorization/admin.py +12 -0
  209. solara/website/pages/apps/authorization/users.py +12 -0
  210. solara/website/pages/apps/jupyter-dashboard-1.py +116 -0
  211. solara/website/pages/apps/layout-demo.py +40 -0
  212. solara/website/pages/apps/multipage/__init__.py +38 -0
  213. solara/website/pages/apps/multipage/page1.py +26 -0
  214. solara/website/pages/apps/multipage/page2.py +34 -0
  215. solara/website/pages/apps/scatter.py +136 -0
  216. solara/website/pages/apps/scrolling.py +63 -0
  217. solara/website/pages/apps/tutorial-streamlit.py +18 -0
  218. solara/website/pages/careers/__init__.py +27 -0
  219. solara/website/pages/changelog/__init__.py +10 -0
  220. solara/website/pages/changelog/changelog.md +372 -0
  221. solara/website/pages/contact/__init__.py +34 -0
  222. solara/website/pages/doc_use_download.py +85 -0
  223. solara/website/pages/documentation/__init__.py +90 -0
  224. solara/website/pages/documentation/advanced/__init__.py +9 -0
  225. solara/website/pages/documentation/advanced/content/00-overview.md +1 -0
  226. solara/website/pages/documentation/advanced/content/10-howto/00-overview.md +6 -0
  227. solara/website/pages/documentation/advanced/content/10-howto/10-multipage.md +196 -0
  228. solara/website/pages/documentation/advanced/content/10-howto/20-layout.md +125 -0
  229. solara/website/pages/documentation/advanced/content/10-howto/30-testing.md +417 -0
  230. solara/website/pages/documentation/advanced/content/10-howto/31-debugging.md +69 -0
  231. solara/website/pages/documentation/advanced/content/10-howto/40-embed.md +50 -0
  232. solara/website/pages/documentation/advanced/content/10-howto/50-ipywidget_libraries.md +124 -0
  233. solara/website/pages/documentation/advanced/content/15-reference/00-overview.md +3 -0
  234. solara/website/pages/documentation/advanced/content/15-reference/40-static_files.md +31 -0
  235. solara/website/pages/documentation/advanced/content/15-reference/41-asset-files.md +72 -0
  236. solara/website/pages/documentation/advanced/content/15-reference/60-static-site-generation.md +59 -0
  237. solara/website/pages/documentation/advanced/content/15-reference/70-search.md +34 -0
  238. solara/website/pages/documentation/advanced/content/15-reference/80-reloading.md +34 -0
  239. solara/website/pages/documentation/advanced/content/15-reference/90-notebook-support.md +7 -0
  240. solara/website/pages/documentation/advanced/content/15-reference/95-caching.md +148 -0
  241. solara/website/pages/documentation/advanced/content/20-understanding/00-introduction.md +10 -0
  242. solara/website/pages/documentation/advanced/content/20-understanding/05-ipywidgets.md +35 -0
  243. solara/website/pages/documentation/advanced/content/20-understanding/06-ipyvuetify.md +42 -0
  244. solara/website/pages/documentation/advanced/content/20-understanding/10-reacton.md +28 -0
  245. solara/website/pages/documentation/advanced/content/20-understanding/12-reacton-basics.md +108 -0
  246. solara/website/pages/documentation/advanced/content/20-understanding/15-anatomy.md +23 -0
  247. solara/website/pages/documentation/advanced/content/20-understanding/17-rules-of-hooks.md +192 -0
  248. solara/website/pages/documentation/advanced/content/20-understanding/18-containers.md +166 -0
  249. solara/website/pages/documentation/advanced/content/20-understanding/20-solara.md +18 -0
  250. solara/website/pages/documentation/advanced/content/20-understanding/40-routing.md +256 -0
  251. solara/website/pages/documentation/advanced/content/20-understanding/50-solara-server.md +108 -0
  252. solara/website/pages/documentation/advanced/content/20-understanding/60-voila.md +12 -0
  253. solara/website/pages/documentation/advanced/content/30-enterprise/00-overview.md +7 -0
  254. solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md +187 -0
  255. solara/website/pages/documentation/advanced/content/40-development/00-overview.md +0 -0
  256. solara/website/pages/documentation/advanced/content/40-development/01-contribute.md +45 -0
  257. solara/website/pages/documentation/advanced/content/40-development/10-setup.md +76 -0
  258. solara/website/pages/documentation/api/__init__.py +19 -0
  259. solara/website/pages/documentation/api/cross_filter/__init__.py +9 -0
  260. solara/website/pages/documentation/api/cross_filter/cross_filter_dataframe.py +22 -0
  261. solara/website/pages/documentation/api/cross_filter/cross_filter_report.py +20 -0
  262. solara/website/pages/documentation/api/cross_filter/cross_filter_select.py +20 -0
  263. solara/website/pages/documentation/api/cross_filter/cross_filter_slider.py +20 -0
  264. solara/website/pages/documentation/api/hooks/__init__.py +9 -0
  265. solara/website/pages/documentation/api/hooks/use_cross_filter.py +23 -0
  266. solara/website/pages/documentation/api/hooks/use_dark_effective.py +12 -0
  267. solara/website/pages/documentation/api/hooks/use_effect.md +43 -0
  268. solara/website/pages/documentation/api/hooks/use_effect.py +9 -0
  269. solara/website/pages/documentation/api/hooks/use_exception.py +31 -0
  270. solara/website/pages/documentation/api/hooks/use_memo.md +16 -0
  271. solara/website/pages/documentation/api/hooks/use_memo.py +9 -0
  272. solara/website/pages/documentation/api/hooks/use_previous.py +30 -0
  273. solara/website/pages/documentation/api/hooks/use_reactive.py +16 -0
  274. solara/website/pages/documentation/api/hooks/use_state.py +10 -0
  275. solara/website/pages/documentation/api/hooks/use_state_or_update.py +66 -0
  276. solara/website/pages/documentation/api/hooks/use_thread.md +64 -0
  277. solara/website/pages/documentation/api/hooks/use_thread.py +42 -0
  278. solara/website/pages/documentation/api/hooks/use_trait_observe.py +12 -0
  279. solara/website/pages/documentation/api/routing/__init__.py +9 -0
  280. solara/website/pages/documentation/api/routing/generate_routes.py +10 -0
  281. solara/website/pages/documentation/api/routing/generate_routes_directory.py +10 -0
  282. solara/website/pages/documentation/api/routing/resolve_path.py +35 -0
  283. solara/website/pages/documentation/api/routing/route.py +29 -0
  284. solara/website/pages/documentation/api/routing/use_route.py +76 -0
  285. solara/website/pages/documentation/api/routing/use_router.py +16 -0
  286. solara/website/pages/documentation/api/utilities/__init__.py +9 -0
  287. solara/website/pages/documentation/api/utilities/component_vue.py +10 -0
  288. solara/website/pages/documentation/api/utilities/computed.py +16 -0
  289. solara/website/pages/documentation/api/utilities/display.py +16 -0
  290. solara/website/pages/documentation/api/utilities/get_kernel_id.py +16 -0
  291. solara/website/pages/documentation/api/utilities/get_session_id.py +16 -0
  292. solara/website/pages/documentation/api/utilities/memoize.py +35 -0
  293. solara/website/pages/documentation/api/utilities/on_kernel_start.py +44 -0
  294. solara/website/pages/documentation/api/utilities/reactive.py +16 -0
  295. solara/website/pages/documentation/api/utilities/widget.py +104 -0
  296. solara/website/pages/documentation/components/__init__.py +12 -0
  297. solara/website/pages/documentation/components/advanced/__init__.py +9 -0
  298. solara/website/pages/documentation/components/advanced/link.py +25 -0
  299. solara/website/pages/documentation/components/advanced/meta.py +17 -0
  300. solara/website/pages/documentation/components/advanced/style.py +43 -0
  301. solara/website/pages/documentation/components/common.py +9 -0
  302. solara/website/pages/documentation/components/data/__init__.py +9 -0
  303. solara/website/pages/documentation/components/data/dataframe.py +44 -0
  304. solara/website/pages/documentation/components/data/pivot_table.py +81 -0
  305. solara/website/pages/documentation/components/enterprise/__init__.py +9 -0
  306. solara/website/pages/documentation/components/enterprise/avatar.py +24 -0
  307. solara/website/pages/documentation/components/enterprise/avatar_menu.py +25 -0
  308. solara/website/pages/documentation/components/input/__init__.py +9 -0
  309. solara/website/pages/documentation/components/input/button.py +23 -0
  310. solara/website/pages/documentation/components/input/checkbox.py +10 -0
  311. solara/website/pages/documentation/components/input/file_browser.py +30 -0
  312. solara/website/pages/documentation/components/input/file_drop.py +76 -0
  313. solara/website/pages/documentation/components/input/input.py +43 -0
  314. solara/website/pages/documentation/components/input/select.py +22 -0
  315. solara/website/pages/documentation/components/input/slider.py +29 -0
  316. solara/website/pages/documentation/components/input/switch.py +10 -0
  317. solara/website/pages/documentation/components/input/togglebuttons.py +21 -0
  318. solara/website/pages/documentation/components/lab/__init__.py +9 -0
  319. solara/website/pages/documentation/components/lab/chat.py +109 -0
  320. solara/website/pages/documentation/components/lab/confirmation_dialog.py +55 -0
  321. solara/website/pages/documentation/components/lab/cookies_headers.py +48 -0
  322. solara/website/pages/documentation/components/lab/input_date.py +20 -0
  323. solara/website/pages/documentation/components/lab/input_time.py +15 -0
  324. solara/website/pages/documentation/components/lab/menu.py +22 -0
  325. solara/website/pages/documentation/components/lab/tab.py +25 -0
  326. solara/website/pages/documentation/components/lab/tabs.py +45 -0
  327. solara/website/pages/documentation/components/lab/task.py +11 -0
  328. solara/website/pages/documentation/components/lab/theming.py +74 -0
  329. solara/website/pages/documentation/components/lab/use_task.py +11 -0
  330. solara/website/pages/documentation/components/layout/__init__.py +9 -0
  331. solara/website/pages/documentation/components/layout/app_bar.py +16 -0
  332. solara/website/pages/documentation/components/layout/app_bar_title.py +16 -0
  333. solara/website/pages/documentation/components/layout/app_layout.py +24 -0
  334. solara/website/pages/documentation/components/layout/card.py +15 -0
  335. solara/website/pages/documentation/components/layout/card_actions.py +16 -0
  336. solara/website/pages/documentation/components/layout/column.py +30 -0
  337. solara/website/pages/documentation/components/layout/columns.py +27 -0
  338. solara/website/pages/documentation/components/layout/columns_responsive.py +66 -0
  339. solara/website/pages/documentation/components/layout/details.py +13 -0
  340. solara/website/pages/documentation/components/layout/griddraggable.py +62 -0
  341. solara/website/pages/documentation/components/layout/gridfixed.py +19 -0
  342. solara/website/pages/documentation/components/layout/hbox.py +18 -0
  343. solara/website/pages/documentation/components/layout/row.py +30 -0
  344. solara/website/pages/documentation/components/layout/sidebar.py +24 -0
  345. solara/website/pages/documentation/components/layout/vbox.py +19 -0
  346. solara/website/pages/documentation/components/output/__init__.py +9 -0
  347. solara/website/pages/documentation/components/output/file_download.py +11 -0
  348. solara/website/pages/documentation/components/output/html.py +19 -0
  349. solara/website/pages/documentation/components/output/image.py +11 -0
  350. solara/website/pages/documentation/components/output/markdown.py +57 -0
  351. solara/website/pages/documentation/components/output/markdown_editor.py +51 -0
  352. solara/website/pages/documentation/components/output/sql_code.py +83 -0
  353. solara/website/pages/documentation/components/output/tooltip.py +11 -0
  354. solara/website/pages/documentation/components/page/__init__.py +9 -0
  355. solara/website/pages/documentation/components/page/head.py +15 -0
  356. solara/website/pages/documentation/components/page/title.py +25 -0
  357. solara/website/pages/documentation/components/status/__init__.py +9 -0
  358. solara/website/pages/documentation/components/status/error.py +39 -0
  359. solara/website/pages/documentation/components/status/info.py +39 -0
  360. solara/website/pages/documentation/components/status/progress.py +10 -0
  361. solara/website/pages/documentation/components/status/spinner.py +11 -0
  362. solara/website/pages/documentation/components/status/success.py +40 -0
  363. solara/website/pages/documentation/components/status/warning.py +47 -0
  364. solara/website/pages/documentation/components/viz/__init__.py +9 -0
  365. solara/website/pages/documentation/components/viz/altair.py +42 -0
  366. solara/website/pages/documentation/components/viz/echarts.py +77 -0
  367. solara/website/pages/documentation/components/viz/matplotlib.py +30 -0
  368. solara/website/pages/documentation/components/viz/plotly.py +63 -0
  369. solara/website/pages/documentation/components/viz/plotly_express.py +41 -0
  370. solara/website/pages/documentation/examples/__init__.py +54 -0
  371. solara/website/pages/documentation/examples/ai/__init__.py +11 -0
  372. solara/website/pages/documentation/examples/ai/chatbot.py +113 -0
  373. solara/website/pages/documentation/examples/ai/tokenizer.py +107 -0
  374. solara/website/pages/documentation/examples/basics/__init__.py +10 -0
  375. solara/website/pages/documentation/examples/basics/sine.py +28 -0
  376. solara/website/pages/documentation/examples/fullscreen/__init__.py +10 -0
  377. solara/website/pages/documentation/examples/fullscreen/authorization.py +3 -0
  378. solara/website/pages/documentation/examples/fullscreen/layout_demo.py +3 -0
  379. solara/website/pages/documentation/examples/fullscreen/multipage.py +3 -0
  380. solara/website/pages/documentation/examples/fullscreen/scatter.py +3 -0
  381. solara/website/pages/documentation/examples/fullscreen/scrolling.py +3 -0
  382. solara/website/pages/documentation/examples/fullscreen/tutorial_streamlit.py +3 -0
  383. solara/website/pages/documentation/examples/general/__init__.py +10 -0
  384. solara/website/pages/documentation/examples/general/custom_storage.py +70 -0
  385. solara/website/pages/documentation/examples/general/deploy_model.py +115 -0
  386. solara/website/pages/documentation/examples/general/live_update.py +32 -0
  387. solara/website/pages/documentation/examples/general/login_oauth.py +81 -0
  388. solara/website/pages/documentation/examples/general/mycard.vue +58 -0
  389. solara/website/pages/documentation/examples/general/pokemon_search.py +51 -0
  390. solara/website/pages/documentation/examples/general/vue_component.py +50 -0
  391. solara/website/pages/documentation/examples/ipycanvas.py +49 -0
  392. solara/website/pages/documentation/examples/libraries/__init__.py +10 -0
  393. solara/website/pages/documentation/examples/libraries/altair.py +65 -0
  394. solara/website/pages/documentation/examples/libraries/bqplot.py +39 -0
  395. solara/website/pages/documentation/examples/libraries/ipyleaflet.py +33 -0
  396. solara/website/pages/documentation/examples/libraries/ipyleaflet_advanced.py +66 -0
  397. solara/website/pages/documentation/examples/utilities/__init__.py +10 -0
  398. solara/website/pages/documentation/examples/utilities/calculator.py +157 -0
  399. solara/website/pages/documentation/examples/utilities/countdown_timer.py +62 -0
  400. solara/website/pages/documentation/examples/utilities/todo.py +196 -0
  401. solara/website/pages/documentation/examples/visualization/__init__.py +6 -0
  402. solara/website/pages/documentation/examples/visualization/annotator.py +67 -0
  403. solara/website/pages/documentation/examples/visualization/linked_views.py +81 -0
  404. solara/website/pages/documentation/examples/visualization/plotly.py +44 -0
  405. solara/website/pages/documentation/faq/__init__.py +12 -0
  406. solara/website/pages/documentation/faq/content/99-faq.md +112 -0
  407. solara/website/pages/documentation/getting_started/__init__.py +9 -0
  408. solara/website/pages/documentation/getting_started/content/00-quickstart.md +107 -0
  409. solara/website/pages/documentation/getting_started/content/01-introduction.md +125 -0
  410. solara/website/pages/documentation/getting_started/content/02-installing.md +134 -0
  411. solara/website/pages/documentation/getting_started/content/04-tutorials/00-overview.md +14 -0
  412. solara/website/pages/documentation/getting_started/content/04-tutorials/10_data_science.py +13 -0
  413. solara/website/pages/documentation/getting_started/content/04-tutorials/20-web-app.md +89 -0
  414. solara/website/pages/documentation/getting_started/content/04-tutorials/30-ipywidgets.md +124 -0
  415. solara/website/pages/documentation/getting_started/content/04-tutorials/40-streamlit.md +146 -0
  416. solara/website/pages/documentation/getting_started/content/04-tutorials/50-dash.md +144 -0
  417. solara/website/pages/documentation/getting_started/content/04-tutorials/60-jupyter-dashboard-part1.py +65 -0
  418. solara/website/pages/documentation/getting_started/content/04-tutorials/SF_crime_sample.csv.gz +0 -0
  419. solara/website/pages/documentation/getting_started/content/04-tutorials/_data_science.ipynb +445 -0
  420. solara/website/pages/documentation/getting_started/content/04-tutorials/_jupyter_dashboard_1.ipynb +1021 -0
  421. solara/website/pages/documentation/getting_started/content/05-fundamentals/00-overview.md +11 -0
  422. solara/website/pages/documentation/getting_started/content/05-fundamentals/10-components.md +228 -0
  423. solara/website/pages/documentation/getting_started/content/05-fundamentals/50-state-management.md +278 -0
  424. solara/website/pages/documentation/getting_started/content/07-deploying/00-overview.md +7 -0
  425. solara/website/pages/documentation/getting_started/content/07-deploying/10-self-hosted.md +305 -0
  426. solara/website/pages/documentation/getting_started/content/07-deploying/20-cloud-hosted.md +80 -0
  427. solara/website/pages/documentation/getting_started/content/80-what-is-lab.md +7 -0
  428. solara/website/pages/documentation/getting_started/content/90-troubleshoot.md +26 -0
  429. solara/website/pages/docutils.py +38 -0
  430. solara/website/pages/home.vue +1199 -0
  431. solara/website/pages/our_team/__init__.py +83 -0
  432. solara/website/pages/pricing/__init__.py +31 -0
  433. solara/website/pages/roadmap/__init__.py +11 -0
  434. solara/website/pages/roadmap/roadmap.md +47 -0
  435. solara/website/pages/scale_ipywidgets.py +45 -0
  436. solara/website/pages/showcase/__init__.py +105 -0
  437. solara/website/pages/showcase/domino_code_assist.py +60 -0
  438. solara/website/pages/showcase/planeto_tessa.py +19 -0
  439. solara/website/pages/showcase/solara_dev.py +54 -0
  440. solara/website/pages/showcase/solarathon_2023_team_2.py +22 -0
  441. solara/website/pages/showcase/solarathon_2023_team_4.py +22 -0
  442. solara/website/pages/showcase/solarathon_2023_team_5.py +23 -0
  443. solara/website/pages/showcase/solarathon_2023_team_6.py +34 -0
  444. solara/website/pages/showcase/wanderlust.py +27 -0
  445. solara/website/public/beach.jpeg +0 -0
  446. solara/website/public/logo.svg +6 -0
  447. solara/website/public/social/discord.svg +1 -0
  448. solara/website/public/social/github.svg +1 -0
  449. solara/website/public/social/twitter.svg +3 -0
  450. solara/website/public/success.html +25 -0
  451. solara/website/templates/index.html.j2 +117 -0
  452. solara/website/utils.py +51 -0
  453. solara/widgets/__init__.py +1 -0
  454. solara/widgets/vue/gridlayout.vue +107 -0
  455. solara/widgets/vue/html.vue +4 -0
  456. solara/widgets/vue/navigator.vue +134 -0
  457. solara/widgets/vue/vegalite.vue +130 -0
  458. solara/widgets/widgets.py +74 -0
  459. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_notebook_config.d/solara.json +7 -0
  460. solara_ui-1.45.0.data/data/etc/jupyter/jupyter_server_config.d/solara.json +7 -0
  461. solara_ui-1.45.0.dist-info/METADATA +162 -0
  462. solara_ui-1.45.0.dist-info/RECORD +464 -0
  463. solara_ui-1.45.0.dist-info/WHEEL +4 -0
  464. solara_ui-1.45.0.dist-info/licenses/LICENSE +21 -0
solara/server/flask.py ADDED
@@ -0,0 +1,297 @@
1
+ import asyncio
2
+ import logging
3
+ import mimetypes
4
+ import os
5
+ import threading
6
+ from http.server import HTTPServer
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List
9
+ from urllib.parse import urlparse
10
+ from uuid import uuid4
11
+
12
+ import flask
13
+ import simple_websocket
14
+ from flask import Blueprint, Flask, abort, request, send_from_directory, url_for
15
+ from flask_sock import Sock
16
+
17
+ try:
18
+ import solara_enterprise # type: ignore
19
+
20
+ del solara_enterprise
21
+
22
+ has_solara_enterprise = True
23
+ except ImportError:
24
+ has_solara_enterprise = False
25
+
26
+ if has_solara_enterprise:
27
+ from solara_enterprise.auth.flask import allowed # type: ignore
28
+ from solara_enterprise.auth.flask import (
29
+ authorize,
30
+ get_user,
31
+ init_flask,
32
+ login,
33
+ logout,
34
+ )
35
+ else:
36
+
37
+ def allowed():
38
+ return True
39
+
40
+
41
+ import solara
42
+ import solara.settings
43
+ from solara.server.threaded import ServerBase
44
+
45
+ from . import app as appmod
46
+ from . import cdn_helper, kernel_context, server, settings, websocket
47
+
48
+ os.environ["SERVER_SOFTWARE"] = "solara/" + str(solara.__version__)
49
+
50
+ logger = logging.getLogger("solara.server.flask")
51
+ blueprint = Blueprint("blueprint-solara", __name__)
52
+ websocket_extension = Sock()
53
+
54
+
55
+ class WebsocketWrapper(websocket.WebsocketWrapper):
56
+ ws: simple_websocket.Server
57
+
58
+ def __init__(self, ws: simple_websocket.Server) -> None:
59
+ self.ws = ws
60
+ self.lock = threading.Lock()
61
+
62
+ def close(self):
63
+ with self.lock:
64
+ self.ws.close()
65
+
66
+ def send_text(self, data: str) -> None:
67
+ with self.lock:
68
+ try:
69
+ self.ws.send(data)
70
+ except simple_websocket.ws.ConnectionClosed:
71
+ raise websocket.WebSocketDisconnect()
72
+
73
+ def send_bytes(self, data: bytes) -> None:
74
+ with self.lock:
75
+ try:
76
+ self.ws.send(data)
77
+ except simple_websocket.ws.ConnectionClosed:
78
+ raise websocket.WebSocketDisconnect()
79
+
80
+ async def receive(self):
81
+ from anyio import to_thread
82
+
83
+ try:
84
+ return await to_thread.run_sync(lambda: self.ws.receive()) # type: ignore
85
+ except simple_websocket.ws.ConnectionClosed:
86
+ raise websocket.WebSocketDisconnect()
87
+
88
+
89
+ class ServerFlask(ServerBase):
90
+ server: Any
91
+ name = "flask"
92
+
93
+ def __init__(self, port: int, host: str = "localhost", flask_app=None, url_prefix="", **kwargs):
94
+ super().__init__(port, host, **kwargs)
95
+ self.app = flask_app or app
96
+ self.url_prefix = url_prefix
97
+ self.server = None
98
+
99
+ def has_started(self):
100
+ return server.is_ready(f"http://{self.host}:{self.port}{self.url_prefix}")
101
+
102
+ def signal_stop(self):
103
+ if self.server:
104
+ assert isinstance(self.server, HTTPServer)
105
+ self.server.shutdown() # type: ignore
106
+
107
+ def serve(self):
108
+ from werkzeug.serving import make_server
109
+
110
+ self.server = make_server(self.host, self.port, self.app, threaded=True) # type: ignore
111
+ assert isinstance(self.server, HTTPServer)
112
+ self.started.set()
113
+ self.server.serve_forever(poll_interval=0.05) # type: ignore
114
+
115
+
116
+ @blueprint.route("/jupyter/api/kernels/<id>")
117
+ def kernels(id):
118
+ if not allowed():
119
+ abort(401)
120
+ return {"name": "lala", "id": "dsa"}
121
+
122
+
123
+ @websocket_extension.route("/jupyter/api/kernels/<kernel_id>/<name>")
124
+ def kernels_connection(ws: simple_websocket.Server, kernel_id: str, name: str):
125
+ if not settings.main.base_url:
126
+ settings.main.base_url = url_for("blueprint-solara.read_root", _external=True)
127
+ if settings.oauth.private and not has_solara_enterprise:
128
+ raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
129
+ if has_solara_enterprise:
130
+ user = get_user()
131
+ if user is None and settings.oauth.private:
132
+ logger.error("app is private, requires login")
133
+ ws.close(1008, "app is private, requires login") # policy violation
134
+ return
135
+ else:
136
+ user = None
137
+
138
+ try:
139
+ page_id = request.args["session_id"]
140
+ session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID)
141
+ logger.info("Solara kernel requested for session_id=%s kernel_id=%s", session_id, kernel_id)
142
+ if session_id is None:
143
+ logger.warning("no session cookie")
144
+ session_id = "session-id-cookie-unavailable:" + str(uuid4())
145
+ ws_wrapper = WebsocketWrapper(ws)
146
+ headers_dict: Dict[str, List[str]] = {}
147
+ for k, v in request.headers.__iter__():
148
+ if k not in headers_dict.keys():
149
+ headers_dict[k] = [v]
150
+ else:
151
+ headers_dict[k].append(v)
152
+ asyncio.run(
153
+ server.app_loop(
154
+ ws_wrapper,
155
+ cookies=request.cookies.to_dict(),
156
+ headers=headers_dict,
157
+ session_id=session_id,
158
+ kernel_id=kernel_id,
159
+ page_id=page_id,
160
+ user=user,
161
+ )
162
+ )
163
+ except: # noqa
164
+ logger.exception("Error in kernel handler")
165
+ raise
166
+
167
+
168
+ @blueprint.route("/_solara/api/close/<kernel_id>", methods=["GET", "POST"])
169
+ def close(kernel_id: str):
170
+ page_id = request.args["session_id"]
171
+ context = kernel_context.contexts.get(kernel_id, None)
172
+ if context is not None:
173
+ context.page_close(page_id)
174
+ return ""
175
+
176
+
177
+ @blueprint.route("/static/public/<path:path>")
178
+ def public(path):
179
+ if not allowed():
180
+ abort(401)
181
+ directories = [app.directory.parent / "public" for app in appmod.apps.values()]
182
+ for directory in directories:
183
+ file = directory / path
184
+ if file.exists():
185
+ return send_from_directory(directory, path)
186
+ return flask.Response("not found", status=404)
187
+
188
+
189
+ @blueprint.route("/static/assets/<path:path>")
190
+ def assets(path):
191
+ if not allowed():
192
+ abort(401)
193
+ directories = server.asset_directories()
194
+ for directory in directories:
195
+ file = directory / path
196
+ if file.exists():
197
+ return send_from_directory(directory, path)
198
+ return flask.Response("not found", status=404)
199
+
200
+
201
+ @blueprint.route("/jupyter/nbextensions/<dir>/<filename>")
202
+ def nbext(dir, filename):
203
+ if not allowed():
204
+ abort(401)
205
+ for directory in server.nbextensions_directories:
206
+ file = directory / dir / filename
207
+ if file.exists():
208
+ return send_from_directory(directory, dir + "/" + filename)
209
+ return flask.Response("not found", status=404)
210
+
211
+
212
+ @blueprint.route("/static/<path:path>")
213
+ def serve_static(path):
214
+ if not allowed():
215
+ abort(401)
216
+ return send_from_directory(server.solara_static, path)
217
+
218
+
219
+ if solara.settings.assets.proxy:
220
+
221
+ @blueprint.route(f"/{cdn_helper.cdn_url_path}/<path:path>")
222
+ def cdn(path):
223
+ if not allowed():
224
+ abort(401)
225
+ cache_directory = settings.assets.proxy_cache_dir
226
+ try:
227
+ content = cdn_helper.get_data(Path(cache_directory), path)
228
+ except PermissionError:
229
+ return flask.Response("not found", status=404)
230
+ mime = mimetypes.guess_type(path)
231
+ return flask.Response(content, mimetype=mime[0])
232
+
233
+
234
+ @blueprint.route("/", defaults={"path": ""})
235
+ @blueprint.route("/<path:path>")
236
+ def read_root(path):
237
+ root_path = url_for(".read_root")
238
+ if root_path.endswith("/"):
239
+ root_path = root_path[:-1]
240
+
241
+ if not settings.main.base_url:
242
+ settings.main.base_url = url_for("blueprint-solara.read_root", _external=True)
243
+
244
+ session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
245
+ if root_path:
246
+ path = flask.request.path[len(root_path) :]
247
+ content = server.read_root(path, root_path=root_path)
248
+ if content is None:
249
+ if not allowed():
250
+ abort(401)
251
+ return flask.Response("not found", status=404)
252
+
253
+ if not allowed():
254
+ return login()
255
+
256
+ samesite = "lax"
257
+ secure = False
258
+ # we want samesite, so we can set a cookie when embedded in an iframe, such as on huggingface
259
+ # however, samesite=none requires Secure https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
260
+ # when hosted on the localhost domain we can always set the Secure flag
261
+ # to allow samesite https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies
262
+ o = urlparse(request.base_url)
263
+ if request.headers.get("x-forwarded-proto", "http") == "https" or o.hostname == "localhost":
264
+ samesite = "none"
265
+ secure = True
266
+
267
+ assert session_id is not None
268
+ response = flask.Response(content, mimetype="text/html")
269
+ response.set_cookie(server.COOKIE_KEY_SESSION_ID, value=session_id, secure=secure, samesite=samesite)
270
+ return response
271
+
272
+
273
+ if has_solara_enterprise:
274
+ blueprint.route("/_solara/auth/authorize")(authorize)
275
+ blueprint.route("/_solara/auth/logout")(logout)
276
+ blueprint.route("/_solara/auth/login")(login)
277
+
278
+
279
+ @blueprint.route("/readyz")
280
+ def readyz():
281
+ json, status = server.readyz()
282
+ return flask.Response(json, mimetype="application/json", status=status)
283
+
284
+
285
+ # using the blueprint and websocket blueprint makes it easier to integrate into other applications
286
+ websocket_extension.init_app(blueprint)
287
+ app = Flask(__name__, static_url_path="/_static") # do not intervere with out static files
288
+ app.register_blueprint(blueprint)
289
+ if has_solara_enterprise:
290
+ init_flask(app)
291
+
292
+ if __name__ == "__main__":
293
+ app.run(debug=False, port=8765)
294
+
295
+ # we can only call this at the module level, which means that the solara script cannot import this
296
+ # module. This is a difference with the asgi standard, which provides a lifecycle hook (see starlette.py)
297
+ appmod.ensure_apps_initialized()
@@ -0,0 +1,2 @@
1
+ from .server_extension import _load_jupyter_server_extension # noqa
2
+ from .server_extension import load_jupyter_server_extension # noqa
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import mimetypes
3
+
4
+ import tornado.web
5
+ from jupyter_server.base.handlers import JupyterHandler
6
+
7
+ from solara.server import settings
8
+ from solara.server.cdn_helper import get_data
9
+
10
+ logger = logging.getLogger("Solara.cdn")
11
+
12
+
13
+ class CdnHandler(JupyterHandler):
14
+ def initialize(self, cache_directory=settings.assets.proxy_cache_dir):
15
+ self.cache_directory = cache_directory
16
+ logging.info("Using %r as cache directory", self.cache_directory)
17
+
18
+ async def get(self, path=None):
19
+ try:
20
+ content = get_data(self.cache_directory, path)
21
+ except Exception as e:
22
+ logger.warning(e)
23
+ raise tornado.web.HTTPError(500)
24
+
25
+ mime = mimetypes.guess_type(path)
26
+ if mime[0] is not None:
27
+ self.set_header("Content-Type", mime[0])
28
+ self.write(content)
@@ -0,0 +1,40 @@
1
+ from jupyter_server.utils import url_path_join
2
+
3
+ from solara.server.cdn_helper import cdn_url_path
4
+ from solara.server.jupyter.cdn_handler import CdnHandler
5
+ from .solara import SolaraHandler, Assets, ReadyZ
6
+
7
+
8
+ def _jupyter_server_extension_paths():
9
+ return [{"module": "solara.server.jupyter.server_extension"}]
10
+
11
+
12
+ def _load_jupyter_server_extension(server_app):
13
+ # a dummy app, so that server.read_root can be used
14
+ import solara.server.app
15
+
16
+ solara.server.app.apps["__default__"] = solara.server.app.AppScript("solara.server.jupyter.solara:Page")
17
+ solara.server.app.apps["__default__"].init()
18
+
19
+ web_app = server_app.web_app
20
+
21
+ host_pattern = ".*$"
22
+ base_url = url_path_join(web_app.settings["base_url"])
23
+
24
+ web_app.add_handlers(
25
+ host_pattern,
26
+ [
27
+ (url_path_join(base_url, f"/{cdn_url_path}/(.*)"), CdnHandler, {}), # kept for backward compatibility
28
+ (url_path_join(base_url, f"/solara/{cdn_url_path}/(.*)"), CdnHandler, {}),
29
+ (url_path_join(base_url, "/solara/static/assets/(.*)"), Assets, {}),
30
+ (url_path_join(base_url, "/solara/readyz"), ReadyZ, {}),
31
+ (url_path_join(base_url, "/solara(.*)"), SolaraHandler, {}),
32
+ ],
33
+ )
34
+
35
+
36
+ # For backward compatibility
37
+ load_jupyter_server_extension = _load_jupyter_server_extension
38
+
39
+ # For future compatibility
40
+ _jupyter_server_extension_points = _jupyter_server_extension_paths
@@ -0,0 +1,91 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import tornado.web
7
+ from jupyter_server.base.handlers import JupyterHandler
8
+ import solara.server.server as server
9
+
10
+ from solara.server.utils import path_is_child_of
11
+ import solara
12
+
13
+ logger = logging.getLogger("solara.server.jupyter.solara")
14
+
15
+
16
+ @solara.component
17
+ def Page():
18
+ solara.Error("Hi, you should not see this, we only support ipypopout for now")
19
+
20
+
21
+ class SolaraHandler(JupyterHandler):
22
+ async def get(self, path=None):
23
+ try:
24
+ # base url ends with /
25
+ base_url = self.settings["base_url"]
26
+ # root_path's do not end with /
27
+ jupyter_root_path = ""
28
+ if base_url and base_url.endswith("/"):
29
+ jupyter_root_path = base_url[:-1]
30
+ root_path = f"{jupyter_root_path}/solara"
31
+ content = server.read_root(path="", root_path=root_path, jupyter_root_path=jupyter_root_path)
32
+ except Exception as e:
33
+ logger.exception(e)
34
+ raise tornado.web.HTTPError(500)
35
+
36
+ if content is None:
37
+ raise tornado.web.HTTPError(404)
38
+
39
+ self.set_header("Content-Type", "text/html")
40
+ self.write(content)
41
+
42
+
43
+ # similar to voila
44
+ class MultiStaticFileHandler(tornado.web.StaticFileHandler):
45
+ """A static file handler that 'merges' a list of directories
46
+
47
+ If initialized like this::
48
+
49
+ application = web.Application([
50
+ (r"/content/(.*)", web.MultiStaticFileHandler, {"paths": ["/var/1", "/var/2"]}),
51
+ ])
52
+
53
+ A file will be looked up in /var/1 first, then in /var/2.
54
+
55
+ """
56
+
57
+ def initialize(self, paths, default_filename=None): # type: ignore
58
+ self.roots = paths
59
+ super().initialize(path=paths[0], default_filename=default_filename)
60
+
61
+ def get_absolute_path(self, root: str, path: str) -> str: # type: ignore
62
+ # find the first absolute path that exists
63
+ self.root = self.roots[0]
64
+ abspath = os.path.abspath(os.path.join(root, path))
65
+ for root in self.roots[1:]:
66
+ abspath = os.path.abspath(os.path.join(root, path))
67
+ if os.path.exists(abspath):
68
+ self.root = root # make sure all the other methods in the base class know how to find the file
69
+ break
70
+
71
+ # tornado probably already does a version of this, to make sure it behaves as the rest of the solara
72
+ # server, we do it again
73
+ if not path_is_child_of(Path(abspath), Path(self.root)):
74
+ raise PermissionError(f"Trying to read from outside of cache directory: {abspath} is not a subdir of {self.root}")
75
+
76
+ return abspath
77
+
78
+
79
+ class Assets(MultiStaticFileHandler):
80
+ def initialize(self): # type: ignore
81
+ super().initialize(server.asset_directories())
82
+ logging.error("Using %r as assets directories", self.roots)
83
+
84
+
85
+ class ReadyZ(JupyterHandler):
86
+ def get(self):
87
+ json_data, status = server.readyz()
88
+ json_response = json.dumps(json_data)
89
+ self.set_header("Content-Type", "application/json")
90
+ self.set_status(status)
91
+ self.write(json_response)
@@ -0,0 +1,46 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ # based on notebook config, but to reduce the dependencies
6
+ # and with modern typing and use of pathlib
7
+
8
+
9
+ def recursive_update(target, new):
10
+ """Recursively update one dictionary using another.
11
+
12
+ None values will delete their keys.
13
+ """
14
+ for k, v in new.items():
15
+ if isinstance(v, dict):
16
+ if k not in target:
17
+ target[k] = {}
18
+ recursive_update(target[k], v)
19
+ if not target[k]:
20
+ # Prune empty subdicts
21
+ del target[k]
22
+
23
+ elif v is None:
24
+ target.pop(k, None)
25
+
26
+ else:
27
+ target[k] = v
28
+
29
+
30
+ def _get_config(directory: Path, config_name: str):
31
+ paths = [directory / f"{config_name}.json"]
32
+ paths.extend((directory / f"{config_name}.d/").glob("*.json"))
33
+ data: dict = {}
34
+ for path in paths:
35
+ if path.is_file():
36
+ with open(path, encoding="utf-8") as f:
37
+ recursive_update(data, json.load(f))
38
+ return data
39
+
40
+
41
+ def get_config(directories: List[Path], config_name: str):
42
+ config: dict = {}
43
+ # step through back to front, to ensure front of the list is top priority
44
+ for directory in directories[::-1]:
45
+ recursive_update(config, _get_config(directory, config_name))
46
+ return config