writer 0.8.3rc4__py3-none-any.whl → 1.25.1rc1__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 (378) hide show
  1. writer/__init__.py +1 -1
  2. writer/abstract.py +1 -1
  3. writer/{ai.py → ai/__init__.py} +867 -163
  4. writer/app_runner.py +596 -241
  5. writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
  6. writer/app_templates/default/.wf/components-blueprints_root.jsonl +1 -0
  7. writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +27 -0
  8. writer/app_templates/default/.wf/components-root.jsonl +1 -0
  9. writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
  10. writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
  11. writer/app_templates/default/.wf/metadata.json +3 -0
  12. writer/app_templates/default/README.md +3 -0
  13. writer/app_templates/default/main.py +16 -0
  14. writer/app_templates/default/requirements.txt +1 -0
  15. writer/app_templates/default/static/README.md +8 -0
  16. writer/app_templates/default/static/agent_builder_demo.png +0 -0
  17. writer/app_templates/default/static/favicon.png +0 -0
  18. writer/app_templates/hello/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +18 -0
  19. writer/app_templates/hello/.wf/components-blueprints_root.jsonl +1 -0
  20. writer/app_templates/hello/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +15 -0
  21. writer/app_templates/hello/.wf/components-root.jsonl +1 -0
  22. writer/app_templates/hello/.wf/metadata.json +3 -0
  23. writer/app_templates/hello/main.py +16 -0
  24. writer/app_templates/hello/static/README.md +8 -0
  25. writer/app_templates/hello/static/favicon.png +0 -0
  26. writer/app_templates/hello/static/welcome.svg +40 -0
  27. writer/auth.py +7 -2
  28. writer/autogen.py +352 -0
  29. writer/blocks/__init__.py +51 -17
  30. writer/blocks/addtostatelist.py +10 -9
  31. writer/blocks/apitrigger.py +45 -0
  32. writer/blocks/base_block.py +332 -21
  33. writer/blocks/base_trigger.py +14 -0
  34. writer/blocks/calleventhandler.py +39 -35
  35. writer/blocks/changepage.py +48 -0
  36. writer/blocks/code.py +102 -0
  37. writer/blocks/crontrigger.py +49 -0
  38. writer/blocks/foreach.py +70 -53
  39. writer/blocks/httprequest.py +112 -99
  40. writer/blocks/ifelse.py +71 -0
  41. writer/blocks/logmessage.py +34 -39
  42. writer/blocks/parsejson.py +30 -29
  43. writer/blocks/returnvalue.py +7 -7
  44. writer/blocks/runblueprint.py +63 -0
  45. writer/blocks/setstate.py +43 -33
  46. writer/blocks/sharedblueprint.py +86 -0
  47. writer/blocks/uieventtrigger.py +49 -0
  48. writer/blocks/writeraddchatmessage.py +50 -12
  49. writer/blocks/writeraddtokg.py +38 -11
  50. writer/blocks/writeraskkg.py +123 -0
  51. writer/blocks/writerchat.py +80 -61
  52. writer/blocks/writerchatreply.py +279 -0
  53. writer/blocks/writerchatreplywithtoolconfig.py +393 -0
  54. writer/blocks/writerclassification.py +78 -39
  55. writer/blocks/writercompletion.py +49 -44
  56. writer/blocks/writerfileapi.py +85 -0
  57. writer/blocks/writerinitchat.py +24 -12
  58. writer/blocks/writerkeyvaluestorage.py +106 -0
  59. writer/blocks/writernocodeapp.py +35 -37
  60. writer/blocks/writerparsepdf.py +73 -0
  61. writer/blocks/writerstructuredoutput.py +105 -0
  62. writer/blocks/writertoolcalling.py +251 -0
  63. writer/blocks/writervision.py +141 -0
  64. writer/blocks/writerwebsearch.py +175 -0
  65. writer/blueprints.py +839 -0
  66. writer/command_line.py +52 -16
  67. writer/core.py +562 -290
  68. writer/core_ui.py +6 -2
  69. writer/evaluator.py +98 -46
  70. writer/journal.py +227 -0
  71. writer/keyvalue_storage.py +93 -0
  72. writer/logs.py +277 -0
  73. writer/serve.py +625 -327
  74. writer/ss_types.py +101 -12
  75. writer/static/assets/Arrow.dom-GBJpMYQS.js +1 -0
  76. writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
  77. writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
  78. writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
  79. writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
  80. writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
  81. writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
  82. writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
  83. writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
  84. writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
  85. writer/static/assets/BuilderEmbeddedCodeEditor-B0bcjlhk.css +1 -0
  86. writer/static/assets/BuilderEmbeddedCodeEditor-Dn7eDICN.js +726 -0
  87. writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
  88. writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
  89. writer/static/assets/BuilderInsertionLabel-BhyL9wgn.js +1 -0
  90. writer/static/assets/BuilderInsertionLabel-_YS5WPfq.css +1 -0
  91. writer/static/assets/BuilderInsertionOverlay-D2XS0ij9.css +1 -0
  92. writer/static/assets/BuilderInsertionOverlay-MkAIVruY.js +1 -0
  93. writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
  94. writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
  95. writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
  96. writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
  97. writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
  98. writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
  99. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
  100. writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
  101. writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
  102. writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
  103. writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
  104. writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
  105. writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
  106. writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
  107. writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
  108. writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
  109. writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
  110. writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
  111. writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
  112. writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
  113. writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
  114. writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
  115. writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
  116. writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
  117. writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
  118. writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
  119. writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
  120. writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
  121. writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
  122. writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
  123. writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
  124. writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
  125. writer/static/assets/abap-D8nrxEjS.js +6 -0
  126. writer/static/assets/apex-BrXDlLUW.js +6 -0
  127. writer/static/assets/art-paper-D70v1WMA.svg +180 -0
  128. writer/static/assets/azcli-CElzELwZ.js +6 -0
  129. writer/static/assets/bat-CUsyEhik.js +6 -0
  130. writer/static/assets/bicep-BtxyJn6H.js +7 -0
  131. writer/static/assets/cameligo-ClBCoF8h.js +6 -0
  132. writer/static/assets/clojure-B9TqLHAk.js +6 -0
  133. writer/static/assets/codicon-BA2IlpFX.ttf +0 -0
  134. writer/static/assets/coffee-DYsfeylR.js +6 -0
  135. writer/static/assets/cpp-VVGvvgir.js +6 -0
  136. writer/static/assets/csharp-Z6z2stHy.js +6 -0
  137. writer/static/assets/csp-DgZoLDI1.js +6 -0
  138. writer/static/assets/css-KqQ96-gC.js +8 -0
  139. writer/static/assets/css.worker-DvNUQFd1.js +84 -0
  140. writer/static/assets/cssMode-BYq4oZGq.js +9 -0
  141. writer/static/assets/cypher-CYoSlgTu.js +6 -0
  142. writer/static/assets/dart-BGDl7St1.js +6 -0
  143. writer/static/assets/dockerfile-CuCtxA7T.js +6 -0
  144. writer/static/assets/ecl-BCTFAUpS.js +6 -0
  145. writer/static/assets/editor.worker-BVwmgLrR.js +11 -0
  146. writer/static/assets/elixir-C7hRTYZ9.js +6 -0
  147. writer/static/assets/flow9-Bi_qi707.js +6 -0
  148. writer/static/assets/freemarker2-CnNourkO.js +8 -0
  149. writer/static/assets/fsharp-CxaaEKKi.js +6 -0
  150. writer/static/assets/go-DUImKuGY.js +6 -0
  151. writer/static/assets/graphql-D5sGVkLV.js +6 -0
  152. writer/static/assets/handlebars-Bm22yapJ.js +6 -0
  153. writer/static/assets/hcl-zD_CCkZ1.js +6 -0
  154. writer/static/assets/html-CAKAfoZF.js +6 -0
  155. writer/static/assets/html.worker-BJMlcbMU.js +458 -0
  156. writer/static/assets/htmlMode-BGZ97n-V.js +9 -0
  157. writer/static/assets/index-5u5REPT4.js +16 -0
  158. writer/static/assets/index-BKNuk68o.css +1 -0
  159. writer/static/assets/index-BQNXU3IR.js +17 -0
  160. writer/static/assets/index-BQr1pfrb.js +1 -0
  161. writer/static/assets/index-DHXAd5Yn.js +4 -0
  162. writer/static/assets/index-Zki-pfO-.js +8525 -0
  163. writer/static/assets/index.esm-B1ZQtduY.js +17 -0
  164. writer/static/assets/ini-8kKHd4ZL.js +6 -0
  165. writer/static/assets/java-De1axCfe.js +6 -0
  166. writer/static/assets/javascript-X1f02eyK.js +6 -0
  167. writer/static/assets/json.worker-BwvX8PuZ.js +42 -0
  168. writer/static/assets/jsonMode-hT0bNgT8.js +11 -0
  169. writer/static/assets/julia-D3ApGBxz.js +6 -0
  170. writer/static/assets/kotlin-GbSrCElU.js +6 -0
  171. writer/static/assets/less-DNUaDNdz.js +7 -0
  172. writer/static/assets/lexon-Bg9QKxBu.js +6 -0
  173. writer/static/assets/liquid-KmCCiJw2.js +6 -0
  174. writer/static/assets/lua-Crkvc3mc.js +6 -0
  175. writer/static/assets/m3-DsrzVyM1.js +6 -0
  176. writer/static/assets/mapbox-gl-C0cyFYYW.js +2329 -0
  177. writer/static/assets/markdown-CY5IOZuu.js +6 -0
  178. writer/static/assets/marked.esm-273vDTCT.js +45 -0
  179. writer/static/assets/mdx-DtRFauUw.js +6 -0
  180. writer/static/assets/mips-BE8RsGBA.js +6 -0
  181. writer/static/assets/msdax-N5ajIiFQ.js +6 -0
  182. writer/static/assets/mysql-DRxbB97D.js +6 -0
  183. writer/static/assets/objective-c-BHUZy23s.js +6 -0
  184. writer/static/assets/pascal-BemVzBTY.js +6 -0
  185. writer/static/assets/pascaligo-BACCcnx_.js +6 -0
  186. writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
  187. writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
  188. writer/static/assets/perl-CuU66Ptk.js +6 -0
  189. writer/static/assets/pgsql-CQ6TMH2r.js +6 -0
  190. writer/static/assets/php-BvyzZa65.js +6 -0
  191. writer/static/assets/pla-DrIuu9u1.js +6 -0
  192. writer/static/assets/plotly.min-DutuuatZ.js +4030 -0
  193. writer/static/assets/poppins-latin-300-italic-4WBEAciR.woff +0 -0
  194. writer/static/assets/poppins-latin-300-italic-EWCPeN2Y.woff2 +0 -0
  195. writer/static/assets/poppins-latin-300-normal-DCNuMXUj.woff +0 -0
  196. writer/static/assets/poppins-latin-300-normal-Dku2WoCh.woff2 +0 -0
  197. writer/static/assets/poppins-latin-400-italic-B4GYq972.woff2 +0 -0
  198. writer/static/assets/poppins-latin-400-italic-BPejoDS-.woff +0 -0
  199. writer/static/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
  200. writer/static/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
  201. writer/static/assets/poppins-latin-500-italic-Ce_qjtl5.woff +0 -0
  202. writer/static/assets/poppins-latin-500-italic-o28Otv0U.woff2 +0 -0
  203. writer/static/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
  204. writer/static/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
  205. writer/static/assets/poppins-latin-600-italic-BhOZippK.woff +0 -0
  206. writer/static/assets/poppins-latin-600-italic-CZ4wqKBi.woff2 +0 -0
  207. writer/static/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
  208. writer/static/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
  209. writer/static/assets/poppins-latin-700-italic-CW91C-LJ.woff +0 -0
  210. writer/static/assets/poppins-latin-700-italic-RKf6esGj.woff2 +0 -0
  211. writer/static/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
  212. writer/static/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
  213. writer/static/assets/poppins-latin-ext-300-italic-CBzyU4Pf.woff +0 -0
  214. writer/static/assets/poppins-latin-ext-300-italic-DdDvTq5-.woff2 +0 -0
  215. writer/static/assets/poppins-latin-ext-300-normal-7Zg2msWE.woff2 +0 -0
  216. writer/static/assets/poppins-latin-ext-300-normal-C9p7gvmA.woff +0 -0
  217. writer/static/assets/poppins-latin-ext-400-italic-BiCGV3eO.woff2 +0 -0
  218. writer/static/assets/poppins-latin-ext-400-italic-gsPYOGqV.woff +0 -0
  219. writer/static/assets/poppins-latin-ext-400-normal-CIpeJEZw.woff2 +0 -0
  220. writer/static/assets/poppins-latin-ext-400-normal-Ce_uWq1Z.woff +0 -0
  221. writer/static/assets/poppins-latin-ext-500-italic-CwrTHwbn.woff2 +0 -0
  222. writer/static/assets/poppins-latin-ext-500-italic-jdc8Bv4M.woff +0 -0
  223. writer/static/assets/poppins-latin-ext-500-normal-Bl1-S02S.woff +0 -0
  224. writer/static/assets/poppins-latin-ext-500-normal-H4Q0z8D2.woff2 +0 -0
  225. writer/static/assets/poppins-latin-ext-600-italic-BqeDa496.woff2 +0 -0
  226. writer/static/assets/poppins-latin-ext-600-italic-C7MQPb_A.woff +0 -0
  227. writer/static/assets/poppins-latin-ext-600-normal-Cn4C8475.woff2 +0 -0
  228. writer/static/assets/poppins-latin-ext-600-normal-DB6FJURc.woff +0 -0
  229. writer/static/assets/poppins-latin-ext-700-italic-BAdhB_WS.woff2 +0 -0
  230. writer/static/assets/poppins-latin-ext-700-italic-WKTwQMp8.woff +0 -0
  231. writer/static/assets/poppins-latin-ext-700-normal-CE2WFKmF.woff +0 -0
  232. writer/static/assets/poppins-latin-ext-700-normal-DDaViAzG.woff2 +0 -0
  233. writer/static/assets/postiats-BR_hrfni.js +6 -0
  234. writer/static/assets/powerquery-CKDUeRmd.js +6 -0
  235. writer/static/assets/powershell-Dsa4rhA_.js +6 -0
  236. writer/static/assets/protobuf-CGsvhooB.js +7 -0
  237. writer/static/assets/pug-D2p3uOX2.js +6 -0
  238. writer/static/assets/python-DVhxg746.js +6 -0
  239. writer/static/assets/qsharp-B7F3HtPF.js +6 -0
  240. writer/static/assets/r-3aLoi2fs.js +6 -0
  241. writer/static/assets/razor-DR5Ns_BC.js +6 -0
  242. writer/static/assets/redis-jqFeRM5s.js +6 -0
  243. writer/static/assets/redshift-BriwQgXR.js +6 -0
  244. writer/static/assets/restructuredtext-hbBFZ0w9.js +6 -0
  245. writer/static/assets/ruby-ByThyB2Q.js +6 -0
  246. writer/static/assets/rust-DIEZMp5R.js +6 -0
  247. writer/static/assets/sb-C6Gjjw_x.js +6 -0
  248. writer/static/assets/scala-DZNw3jJB.js +6 -0
  249. writer/static/assets/scheme-55eqh71t.js +6 -0
  250. writer/static/assets/scss-D-OVkc4F.js +8 -0
  251. writer/static/assets/serialization-DJC7NP0N.js +20 -0
  252. writer/static/assets/shell-DSpi8_qN.js +6 -0
  253. writer/static/assets/solidity-BHddiNFS.js +6 -0
  254. writer/static/assets/sophia-D6taVZFb.js +6 -0
  255. writer/static/assets/sparql-LA0C7mUc.js +6 -0
  256. writer/static/assets/sql-C3-3IcFM.js +6 -0
  257. writer/static/assets/st-C4g7059C.js +6 -0
  258. writer/static/assets/swift-DNI1vH3h.js +8 -0
  259. writer/static/assets/systemverilog-DL_FVbcQ.js +6 -0
  260. writer/static/assets/tcl-DVJXmIwd.js +6 -0
  261. writer/static/assets/ts.worker-CwG1rUES.js +37021 -0
  262. writer/static/assets/tsMode-BNUEZzir.js +16 -0
  263. writer/static/assets/twig-BVWDLtw5.js +6 -0
  264. writer/static/assets/typescript-CRVt7Hx0.js +6 -0
  265. writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
  266. writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
  267. writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
  268. writer/static/assets/vb-Btz91-7U.js +6 -0
  269. writer/static/assets/vega-embed.module-SNP5iNdJ.js +201 -0
  270. writer/static/assets/wgsl-D8V_buCG.js +303 -0
  271. writer/static/assets/xml-C_6-t1tb.js +6 -0
  272. writer/static/assets/yaml-DIw8G7jk.js +6 -0
  273. writer/static/components/annotatedtext.svg +4 -0
  274. writer/static/components/avatar.svg +4 -0
  275. writer/static/components/blueprints_addtostatelist.svg +4 -0
  276. writer/static/components/blueprints_apitrigger.svg +4 -0
  277. writer/static/components/blueprints_calleventhandler.svg +9 -0
  278. writer/static/components/blueprints_category_Logic.svg +4 -0
  279. writer/static/components/blueprints_category_Other.svg +4 -0
  280. writer/static/components/blueprints_category_Triggers.svg +4 -0
  281. writer/static/components/blueprints_category_Writer.svg +25 -0
  282. writer/static/components/blueprints_code.svg +9 -0
  283. writer/static/components/blueprints_crontrigger.svg +6 -0
  284. writer/static/components/blueprints_foreach.svg +4 -0
  285. writer/static/components/blueprints_httprequest.svg +11 -0
  286. writer/static/components/blueprints_logmessage.svg +11 -0
  287. writer/static/components/blueprints_parsejson.svg +4 -0
  288. writer/static/components/blueprints_returnvalue.svg +4 -0
  289. writer/static/components/blueprints_runblueprint.svg +4 -0
  290. writer/static/components/blueprints_setstate.svg +4 -0
  291. writer/static/components/blueprints_uieventtrigger.svg +4 -0
  292. writer/static/components/blueprints_writeraddchatmessage.svg +19 -0
  293. writer/static/components/blueprints_writeraddtokg.svg +19 -0
  294. writer/static/components/blueprints_writerchat.svg +11 -0
  295. writer/static/components/blueprints_writerchatreply.svg +19 -0
  296. writer/static/components/blueprints_writerclassification.svg +24 -0
  297. writer/static/components/blueprints_writercompletion.svg +14 -0
  298. writer/static/components/blueprints_writerinitchat.svg +11 -0
  299. writer/static/components/blueprints_writernocodeapp.svg +14 -0
  300. writer/static/components/button.svg +4 -0
  301. writer/static/components/category_Content.svg +4 -0
  302. writer/static/components/category_Embed.svg +4 -0
  303. writer/static/components/category_Input.svg +5 -0
  304. writer/static/components/category_Layout.svg +9 -0
  305. writer/static/components/category_Other.svg +4 -0
  306. writer/static/components/chatbot.svg +4 -0
  307. writer/static/components/checkboxinput.svg +4 -0
  308. writer/static/components/colorinput.svg +11 -0
  309. writer/static/components/column.svg +4 -0
  310. writer/static/components/columns.svg +4 -0
  311. writer/static/components/dataframe.svg +4 -0
  312. writer/static/components/dateinput.svg +4 -0
  313. writer/static/components/dropdowninput.svg +5 -0
  314. writer/static/components/fileinput.svg +4 -0
  315. writer/static/components/googlemaps.svg +4 -0
  316. writer/static/components/header.svg +4 -0
  317. writer/static/components/heading.svg +9 -0
  318. writer/static/components/horizontalstack.svg +4 -0
  319. writer/static/components/html.svg +9 -0
  320. writer/static/components/icon.svg +4 -0
  321. writer/static/components/iframe.svg +4 -0
  322. writer/static/components/image.svg +11 -0
  323. writer/static/components/jsonviewer.svg +4 -0
  324. writer/static/components/link.svg +12 -0
  325. writer/static/components/mapbox.svg +4 -0
  326. writer/static/components/message.svg +4 -0
  327. writer/static/components/metric.svg +4 -0
  328. writer/static/components/multiselectinput.svg +4 -0
  329. writer/static/components/numberinput.svg +4 -0
  330. writer/static/components/page.svg +50 -0
  331. writer/static/components/pagination.svg +4 -0
  332. writer/static/components/pdf.svg +4 -0
  333. writer/static/components/plotlygraph.svg +7 -0
  334. writer/static/components/progressbar.svg +5 -0
  335. writer/static/components/radioinput.svg +4 -0
  336. writer/static/components/rangeinput.svg +4 -0
  337. writer/static/components/ratinginput.svg +4 -0
  338. writer/static/components/repeater.svg +4 -0
  339. writer/static/components/reuse.svg +4 -0
  340. writer/static/components/section.svg +4 -0
  341. writer/static/components/selectinput.svg +5 -0
  342. writer/static/components/separator.svg +4 -0
  343. writer/static/components/sidebar.svg +4 -0
  344. writer/static/components/sliderinput.svg +4 -0
  345. writer/static/components/step.svg +4 -0
  346. writer/static/components/steps.svg +4 -0
  347. writer/static/components/switchinput.svg +4 -0
  348. writer/static/components/tab.svg +4 -0
  349. writer/static/components/tabs.svg +4 -0
  350. writer/static/components/tags.svg +11 -0
  351. writer/static/components/text.svg +4 -0
  352. writer/static/components/textareainput.svg +11 -0
  353. writer/static/components/textinput.svg +4 -0
  354. writer/static/components/timeinput.svg +4 -0
  355. writer/static/components/timer.svg +4 -0
  356. writer/static/components/vegalitechart.svg +7 -0
  357. writer/static/components/videoplayer.svg +11 -0
  358. writer/static/components/webcamcapture.svg +4 -0
  359. writer/static/favicon.png +0 -0
  360. writer/static/index.html +84 -0
  361. writer/static/status/cancelled.svg +5 -0
  362. writer/static/status/error.svg +5 -0
  363. writer/static/status/skipped.svg +4 -0
  364. writer/static/status/stopped.svg +4 -0
  365. writer/static/status/success.svg +4 -0
  366. writer/sync.py +431 -0
  367. writer/ui.py +2268 -0
  368. writer/vault.py +48 -0
  369. writer/wf_project.py +90 -66
  370. writer-1.25.1rc1.dist-info/METADATA +92 -0
  371. writer-1.25.1rc1.dist-info/RECORD +382 -0
  372. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
  373. writer/blocks/runworkflow.py +0 -59
  374. writer/workflows.py +0 -183
  375. writer-0.8.3rc4.dist-info/METADATA +0 -117
  376. writer-0.8.3rc4.dist-info/RECORD +0 -44
  377. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
  378. {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info/licenses}/LICENSE.txt +0 -0
writer/app_runner.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import asyncio
2
2
  import concurrent.futures
3
3
  import importlib.util
4
+ import io
4
5
  import logging
5
6
  import logging.handlers
6
7
  import multiprocessing
@@ -11,15 +12,17 @@ import shutil
11
12
  import signal
12
13
  import subprocess
13
14
  import sys
15
+ import tempfile
14
16
  import threading
17
+ import zipfile
15
18
  from types import ModuleType
16
- from typing import Callable, Dict, List, Optional, cast
19
+ from typing import Any, Callable, Dict, List, Optional, Union, cast
17
20
 
18
21
  import watchdog.events
19
22
  from pydantic import ValidationError
20
23
  from watchdog.observers.polling import PollingObserver
21
24
 
22
- from writer import VERSION, audit_and_fix, core_ui, crypto, wf_project
25
+ from writer import VERSION, audit_and_fix, core_ui, crypto, vault, wf_project
23
26
  from writer.core import (
24
27
  Config,
25
28
  EventHandlerRegistry,
@@ -28,6 +31,7 @@ from writer.core import (
28
31
  use_request_context,
29
32
  )
30
33
  from writer.core_ui import ingest_bmc_component_tree
34
+ from writer.logs import use_logging_redirect, use_stdout_redirect
31
35
  from writer.ss_types import (
32
36
  AppProcessServerRequest,
33
37
  AppProcessServerRequestPacket,
@@ -44,17 +48,22 @@ from writer.ss_types import (
44
48
  InitSessionRequest,
45
49
  InitSessionRequestPayload,
46
50
  InitSessionResponsePayload,
51
+ ListResourcesRequest,
52
+ ListResourcesRequestPayload,
53
+ QueueMessageRequest,
47
54
  ServeMode,
48
55
  SourceFilesDirectory,
49
56
  StateContentRequest,
50
57
  StateContentResponsePayload,
51
58
  StateEnquiryRequest,
52
59
  StateEnquiryResponsePayload,
60
+ WriterApplicationInformation,
53
61
  WriterEvent,
62
+ WriterVaultUpdateRequest,
54
63
  )
55
64
  from writer.wf_project import WfProjectContext
56
65
 
57
- logging.basicConfig(level=logging.INFO, format='%(message)s')
66
+ user_code_logger = logging.getLogger("user_code")
58
67
 
59
68
 
60
69
  class MessageHandlingException(Exception):
@@ -62,15 +71,13 @@ class MessageHandlingException(Exception):
62
71
 
63
72
 
64
73
  class SessionPruner(threading.Thread):
65
-
66
74
  """
67
75
  Prunes sessions in intervals, without interfering with the AppProcess server thread.
68
76
  """
69
77
 
70
78
  PRUNE_SESSIONS_INTERVAL_SECONDS = 60
71
79
 
72
- def __init__(self,
73
- is_session_pruner_terminated: threading.Event):
80
+ def __init__(self, is_session_pruner_terminated: threading.Event):
74
81
  super().__init__(name="SessionPrunerThread")
75
82
  self.is_session_pruner_terminated = is_session_pruner_terminated
76
83
 
@@ -79,28 +86,30 @@ class SessionPruner(threading.Thread):
79
86
 
80
87
  while True:
81
88
  self.is_session_pruner_terminated.wait(
82
- timeout=SessionPruner.PRUNE_SESSIONS_INTERVAL_SECONDS)
89
+ timeout=SessionPruner.PRUNE_SESSIONS_INTERVAL_SECONDS
90
+ )
83
91
  if self.is_session_pruner_terminated.is_set():
84
92
  return
85
93
  writer.session_manager.prune_sessions()
86
94
 
87
95
 
88
96
  class AppProcess(multiprocessing.Process):
89
-
90
97
  """
91
98
  Writer Framework runs the user's app code using an isolated process, based on this class.
92
99
  The main process is able to communicate with the user app process via app messages (e.g. event, componentUpdate).
93
100
  """
94
101
 
95
- def __init__(self,
96
- client_conn: multiprocessing.connection.Connection,
97
- server_conn: multiprocessing.connection.Connection,
98
- app_path: str,
99
- mode: ServeMode,
100
- run_code: str,
101
- bmc_components: Dict,
102
- is_app_process_server_ready: multiprocessing.synchronize.Event,
103
- is_app_process_server_failed: multiprocessing.synchronize.Event):
102
+ def __init__(
103
+ self,
104
+ client_conn: multiprocessing.connection.Connection,
105
+ server_conn: multiprocessing.connection.Connection,
106
+ app_path: str,
107
+ mode: ServeMode,
108
+ run_code: str,
109
+ bmc_components: Dict,
110
+ is_app_process_server_ready: multiprocessing.synchronize.Event,
111
+ is_app_process_server_failed: multiprocessing.synchronize.Event,
112
+ ):
104
113
  super().__init__(name="AppProcess")
105
114
  self.client_conn = client_conn
106
115
  self.server_conn = server_conn
@@ -109,11 +118,11 @@ class AppProcess(multiprocessing.Process):
109
118
  self.run_code = run_code
110
119
  self.bmc_components = bmc_components
111
120
  self.is_app_process_server_ready = is_app_process_server_ready
112
- self.is_app_process_server_failed = is_app_process_server_failed
121
+ self.is_app_process_server_failed = is_app_process_server_failed
113
122
  self.logger = logging.getLogger("app")
114
123
  self.handler_registry = EventHandlerRegistry()
115
124
  self.middleware_registry = MiddlewareRegistry()
116
-
125
+ self.executor: Optional[concurrent.futures.ThreadPoolExecutor] = None
117
126
 
118
127
  def _load_module(self) -> ModuleType:
119
128
  """
@@ -137,7 +146,9 @@ class AppProcess(multiprocessing.Process):
137
146
  """
138
147
  return self.handler_registry.gather_handler_meta()
139
148
 
140
- def _handle_session_init(self, payload: InitSessionRequestPayload) -> InitSessionResponsePayload:
149
+ def _handle_session_init(
150
+ self, payload: InitSessionRequestPayload
151
+ ) -> InitSessionResponsePayload:
141
152
  """
142
153
  Handles session initialisation and provides a starter pack.
143
154
  """
@@ -146,9 +157,13 @@ class AppProcess(multiprocessing.Process):
146
157
 
147
158
  import writer
148
159
 
149
- session = writer.session_manager.get_session(payload.proposedSessionId, restore_initial_mail=True)
160
+ session = writer.session_manager.get_session(
161
+ payload.proposedSessionId, restore_initial_mail=True
162
+ )
150
163
  if session is None:
151
- session = writer.session_manager.get_new_session(payload.cookies, payload.headers, payload.proposedSessionId)
164
+ session = writer.session_manager.get_new_session(
165
+ payload.cookies, payload.headers, payload.proposedSessionId
166
+ )
152
167
 
153
168
  if session is None:
154
169
  raise MessageHandlingException("Session rejected.")
@@ -157,11 +172,25 @@ class AppProcess(multiprocessing.Process):
157
172
  try:
158
173
  user_state = session.session_state.user_state.to_dict()
159
174
  except BaseException:
160
- session.session_state.add_log_entry(
161
- "error", "Serialisation error", tb.format_exc())
175
+ session.session_state.add_log_entry("error", "Serialisation error", tb.format_exc())
162
176
 
163
177
  ui_component_tree = core_ui.export_component_tree(
164
- session.session_component_tree, mode=writer.Config.mode)
178
+ session.session_component_tree, mode=writer.Config.mode
179
+ )
180
+
181
+ headers = session.headers or {}
182
+ writer_application: Optional[WriterApplicationInformation] = None
183
+ writer_app_id = headers.get("x-agent-id") or os.getenv("WRITER_APP_ID")
184
+ writer_org_id = headers.get("x-organization-id") or os.getenv("WRITER_ORG_ID")
185
+ writer_base_url = os.getenv("WRITER_BASE_URL", "https://api.writer.com")
186
+ if writer_app_id is not None and writer_org_id is not None:
187
+ writer_application = WriterApplicationInformation(
188
+ id=writer_app_id,
189
+ organizationId=writer_org_id,
190
+ baseUrl=writer_base_url
191
+ )
192
+ if writer.Config.mode == "edit":
193
+ writer_application.apiKey = os.getenv("WRITER_API_KEY")
165
194
 
166
195
  res_payload = InitSessionResponsePayload(
167
196
  userState=user_state,
@@ -169,7 +198,8 @@ class AppProcess(multiprocessing.Process):
169
198
  mail=session.session_state.mail,
170
199
  components=ui_component_tree,
171
200
  userFunctions=self._get_user_functions(),
172
- featureFlags=writer.Config.feature_flags
201
+ featureFlags=writer.Config.feature_flags,
202
+ writerApplication=writer_application,
173
203
  )
174
204
 
175
205
  session.session_state.clear_mail()
@@ -186,21 +216,21 @@ class AppProcess(multiprocessing.Process):
186
216
  try:
187
217
  mutations = session.session_state.user_state.get_mutations_as_dict()
188
218
  except BaseException:
189
- session.session_state.add_log_entry("error",
190
- "Serialisation Error",
191
- "An exception was raised during serialisation.",
192
- tb.format_exc())
219
+ session.session_state.add_log_entry(
220
+ "error",
221
+ "Serialisation Error",
222
+ "An exception was raised during serialisation.",
223
+ tb.format_exc(),
224
+ )
193
225
 
194
226
  mail = session.session_state.mail
195
227
 
196
228
  ui_component_tree = core_ui.export_component_tree(
197
- session.session_component_tree, mode=Config.mode, only_update=True)
229
+ session.session_component_tree, mode=Config.mode, only_update=True
230
+ )
198
231
 
199
232
  res_payload = EventResponsePayload(
200
- result=result,
201
- mutations=mutations,
202
- components=ui_component_tree,
203
- mail=mail
233
+ result=result, mutations=mutations, components=ui_component_tree, mail=mail
204
234
  )
205
235
  session.session_state.clear_mail()
206
236
 
@@ -214,17 +244,16 @@ class AppProcess(multiprocessing.Process):
214
244
  try:
215
245
  mutations = session.session_state.user_state.get_mutations_as_dict()
216
246
  except BaseException:
217
- session.session_state.add_log_entry("error",
218
- "Serialisation Error",
219
- "An exception was raised during serialisation.",
220
- tb.format_exc())
247
+ session.session_state.add_log_entry(
248
+ "error",
249
+ "Serialisation Error",
250
+ "An exception was raised during serialisation.",
251
+ tb.format_exc(),
252
+ )
221
253
 
222
254
  mail = session.session_state.mail
223
255
 
224
- res_payload = StateEnquiryResponsePayload(
225
- mutations=mutations,
226
- mail=mail
227
- )
256
+ res_payload = StateEnquiryResponsePayload(mutations=mutations, mail=mail)
228
257
 
229
258
  session.session_state.clear_mail()
230
259
 
@@ -236,25 +265,109 @@ class AppProcess(multiprocessing.Process):
236
265
  serialized_state = session.session_state.user_state.to_raw_state()
237
266
  except BaseException:
238
267
  import traceback as tb
239
- session.session_state.add_log_entry("error",
240
- "Serialisation Error",
241
- "An exception was raised during serialisation.",
242
- tb.format_exc())
268
+
269
+ session.session_state.add_log_entry(
270
+ "error",
271
+ "Serialisation Error",
272
+ "An exception was raised during serialisation.",
273
+ tb.format_exc(),
274
+ )
243
275
 
244
276
  return StateContentResponsePayload(state=serialized_state)
245
277
 
246
278
  def _handle_hash_request(self, req_payload: HashRequestPayload) -> HashRequestResponsePayload:
247
- res_payload = HashRequestResponsePayload(
248
- message=crypto.get_hash(req_payload.message)
249
- )
279
+ res_payload = HashRequestResponsePayload(message=crypto.get_hash(req_payload.message))
250
280
  return res_payload
251
281
 
252
- def _handle_component_update(self, session: WriterSession, payload: ComponentUpdateRequestPayload) -> None:
282
+ def _handle_component_update(
283
+ self, session: WriterSession, payload: ComponentUpdateRequestPayload
284
+ ) -> None:
253
285
  import writer
286
+
254
287
  ingest_bmc_component_tree(writer.base_component_tree, payload.components)
255
288
  ingest_bmc_component_tree(session.session_component_tree, payload.components, True)
256
289
 
257
- def _handle_message(self, session_id: str, request: AppProcessServerRequest) -> AppProcessServerResponse:
290
+ def _handle_list_resources(
291
+ self, session: WriterSession, req: ListResourcesRequestPayload
292
+ ) -> AppProcessServerResponse:
293
+ organization_id = os.environ.get("WRITER_ORG_ID", None)
294
+ if req.resource_type == "graphs":
295
+ from writerai import APIConnectionError
296
+
297
+ from writer.ai import list_graphs
298
+
299
+ try:
300
+ graphs = list_graphs()
301
+ raw_graphs = [
302
+ {
303
+ "name": graph.name,
304
+ "id": graph.id,
305
+ "description": graph.description,
306
+ "organization_id": organization_id,
307
+ }
308
+ for graph in graphs
309
+ ]
310
+ return AppProcessServerResponse(
311
+ status="ok", status_message=None, payload={"data": raw_graphs}
312
+ )
313
+ except (RuntimeError, APIConnectionError) as e:
314
+ return AppProcessServerResponse(status="error", status_message=str(e), payload=None)
315
+
316
+ if req.resource_type == "applications":
317
+ from writerai import APIConnectionError
318
+
319
+ from writer.ai import apps
320
+
321
+ try:
322
+ applications = apps.list()
323
+ raw_apps = [
324
+ {
325
+ "name": app.name,
326
+ "id": app.id,
327
+ "type": app.type,
328
+ "status": app.status,
329
+ "organization_id": organization_id,
330
+ "inputs": {i.name: "" for i in app.inputs},
331
+ }
332
+ for app in applications
333
+ ]
334
+ return AppProcessServerResponse(
335
+ status="ok", status_message=None, payload={"data": raw_apps}
336
+ )
337
+ except (RuntimeError, APIConnectionError) as e:
338
+ return AppProcessServerResponse(status="error", status_message=str(e), payload=None)
339
+
340
+ if req.resource_type == "models":
341
+ from writerai import APIConnectionError
342
+
343
+ from writer.ai import WriterAIManager
344
+
345
+ try:
346
+ client = WriterAIManager.acquire_client()
347
+ models_response = client.models.list()
348
+ models = models_response.models
349
+ raw_models = [
350
+ {
351
+ "name": model.name,
352
+ "id": model.id
353
+ }
354
+ for model in models
355
+ ]
356
+ return AppProcessServerResponse(
357
+ status="ok", status_message=None, payload={"data": raw_models}
358
+ )
359
+ except (RuntimeError, APIConnectionError) as e:
360
+ return AppProcessServerResponse(status="error", status_message=str(e), payload=None)
361
+
362
+ return AppProcessServerResponse(
363
+ status="error",
364
+ status_message=f"could not load unknow resources {req.resource_type}",
365
+ payload=None,
366
+ )
367
+
368
+ def _handle_message(
369
+ self, session_id: str, request: AppProcessServerRequest
370
+ ) -> AppProcessServerResponse:
258
371
  """
259
372
  Handles messages from the main process to the app's isolated process.
260
373
  """
@@ -265,12 +378,11 @@ class AppProcess(multiprocessing.Process):
265
378
  type = request.type
266
379
 
267
380
  if type == "sessionInit":
268
- si_req_payload = InitSessionRequestPayload.parse_obj(
269
- request.payload)
381
+ si_req_payload = InitSessionRequestPayload.model_validate(request.payload)
270
382
  return AppProcessServerResponse(
271
383
  status="ok",
272
384
  status_message=None,
273
- payload=self._handle_session_init(si_req_payload)
385
+ payload=self._handle_session_init(si_req_payload),
274
386
  )
275
387
 
276
388
  session = writer.session_manager.get_session(session_id)
@@ -279,58 +391,64 @@ class AppProcess(multiprocessing.Process):
279
391
  session.update_last_active_timestamp()
280
392
 
281
393
  if type == "checkSession":
282
- return AppProcessServerResponse(
283
- status="ok",
284
- status_message=None,
285
- payload=None
286
- )
394
+ return AppProcessServerResponse(status="ok", status_message=None, payload=None)
287
395
 
288
396
  if type == "event":
289
- ev_req_payload = WriterEvent.parse_obj(request.payload)
397
+ ev_req_payload = WriterEvent.model_validate(request.payload)
290
398
  return AppProcessServerResponse(
291
399
  status="ok",
292
400
  status_message=None,
293
- payload=self._handle_event(session, ev_req_payload)
401
+ payload=self._handle_event(session, ev_req_payload),
294
402
  )
295
403
 
296
404
  if type == "stateEnquiry":
297
405
  return AppProcessServerResponse(
298
- status="ok",
299
- status_message=None,
300
- payload=self._handle_state_enquiry(session)
406
+ status="ok", status_message=None, payload=self._handle_state_enquiry(session)
301
407
  )
302
408
 
303
409
  if type == "stateContent":
304
410
  return AppProcessServerResponse(
305
- status="ok",
306
- status_message=None,
307
- payload=self._handle_state_content(session)
411
+ status="ok", status_message=None, payload=self._handle_state_content(session)
308
412
  )
309
413
 
310
414
  if type == "setUserinfo":
311
415
  session.userinfo = request.payload
312
- return AppProcessServerResponse(
313
- status="ok",
314
- status_message=None,
315
- payload=None
316
- )
416
+ return AppProcessServerResponse(status="ok", status_message=None, payload=None)
417
+
418
+ if type == "queueMessage":
419
+ session.queued_messages.append(request.payload)
420
+ return AppProcessServerResponse(status="ok", status_message=None, payload=None)
421
+
422
+ if type == "retrieveMessages":
423
+ return AppProcessServerResponse(status="ok", status_message=None, payload=session.queued_messages)
424
+
425
+ if type == "clearMessages":
426
+ session.queued_messages = []
427
+ return AppProcessServerResponse(status="ok", status_message=None, payload=None)
317
428
 
318
429
  if self.mode == "edit" and type == "hashRequest":
319
430
  hash_request_payload = HashRequestPayload.model_validate(request.payload)
320
431
  return AppProcessServerResponse(
321
432
  status="ok",
322
433
  status_message=None,
323
- payload=self._handle_hash_request(hash_request_payload)
434
+ payload=self._handle_hash_request(hash_request_payload),
324
435
  )
325
436
 
326
437
  if self.mode == "edit" and type == "componentUpdate":
327
- cu_req_payload = ComponentUpdateRequestPayload.parse_obj(
328
- request.payload)
438
+ cu_req_payload = ComponentUpdateRequestPayload.model_validate(request.payload)
329
439
  self._handle_component_update(session, cu_req_payload)
440
+ return AppProcessServerResponse(status="ok", status_message=None, payload=None)
441
+
442
+ if self.mode == "edit" and type == "listResources":
443
+ list_req_payload = ListResourcesRequestPayload.model_validate(request.payload)
444
+ return self._handle_list_resources(session, list_req_payload)
445
+
446
+ if self.mode == "edit" and type == "writerVaultUpdate":
447
+ vault.writer_vault.refresh()
330
448
  return AppProcessServerResponse(
331
449
  status="ok",
332
450
  status_message=None,
333
- payload=None
451
+ payload=None,
334
452
  )
335
453
 
336
454
  raise MessageHandlingException("Invalid event.")
@@ -341,7 +459,6 @@ class AppProcess(multiprocessing.Process):
341
459
  """
342
460
 
343
461
  import io
344
- from contextlib import redirect_stdout
345
462
 
346
463
  import writer
347
464
 
@@ -350,18 +467,65 @@ class AppProcess(multiprocessing.Process):
350
467
  raise ValueError("Couldn't find app module (writeruserapp).")
351
468
 
352
469
  code_path = os.path.join(self.app_path, "main.py")
353
- with redirect_stdout(io.StringIO()) as f:
354
- code = compile(self.run_code, code_path, "exec")
355
- exec(code, writeruserapp.__dict__)
356
- captured_stdout = f.getvalue()
357
-
358
- if captured_stdout:
359
- writer.core.initial_state.add_log_entry(
360
- "info", "Stdout message during initialization", captured_stdout)
470
+
471
+ # Containers to capture logs for KV storage
472
+ init_stdout_container = ['']
473
+ init_logs_container = ['']
474
+
475
+ try:
476
+ with (
477
+ use_stdout_redirect([
478
+ lambda entry: writer.core.initial_state.add_log_entry("info", "Stdout message during initialization", entry),
479
+ lambda entry: init_stdout_container.__setitem__(0, entry)
480
+ ]),
481
+ use_logging_redirect([
482
+ lambda entry: writer.core.initial_state.add_log_entry("info", "Logs during initialization", entry),
483
+ lambda entry: init_logs_container.__setitem__(0, entry)
484
+ ]),
485
+ ):
486
+ writeruserapp.__dict__["logger"] = user_code_logger
487
+ code = compile(self.run_code, code_path, "exec")
488
+ exec(code, writeruserapp.__dict__)
489
+ finally:
490
+ self._save_initialization_logs(init_stdout_container[0], init_logs_container[0])
361
491
 
362
492
  # Register non-private functions as handlers
363
493
  self.handler_registry.register_module(writeruserapp)
364
494
 
495
+ def _save_initialization_logs(self, stdout: str, logs: str) -> None:
496
+ """Save main.py initialization logs to KV storage."""
497
+ if not stdout and not logs:
498
+ return
499
+
500
+ from datetime import datetime, timezone
501
+
502
+ from writer.core import Config
503
+ from writer.journal import INIT_LOGS_KEY_PREFIX
504
+ from writer.keyvalue_storage import writer_kv_storage
505
+
506
+ if "journal" not in Config.feature_flags or not writer_kv_storage.is_accessible():
507
+ return
508
+
509
+ timestamp = datetime.now(timezone.utc)
510
+ # Match JournalRecord.instance_type logic: 'e' for editor, 'a' for agent
511
+ instance_type = "editor" if self.mode == "edit" else "agent"
512
+ instance_type_letter = instance_type[0] # 'e' or 'a'
513
+
514
+ key = f"{INIT_LOGS_KEY_PREFIX}{instance_type_letter}-{int(timestamp.timestamp() * 1000)}"
515
+ data = {
516
+ "timestamp": timestamp.isoformat(),
517
+ "instanceType": instance_type,
518
+ "mode": self.mode,
519
+ "stdout": stdout,
520
+ "logs": logs
521
+ }
522
+ try:
523
+ writer_kv_storage.save(key, data)
524
+ except Exception as e:
525
+ # Don't fail initialization if log saving fails
526
+ app_logger = logging.getLogger("app_runner")
527
+ app_logger.warning(f"Failed to save initialization logs to KV storage: {e}")
528
+
365
529
  def _apply_configuration(self) -> None:
366
530
  import writer
367
531
 
@@ -382,6 +546,7 @@ class AppProcess(multiprocessing.Process):
382
546
  def _main(self) -> None:
383
547
  self._apply_configuration()
384
548
  import os
549
+
385
550
  os.chdir(self.app_path)
386
551
  self._load_module()
387
552
  # Allows for relative imports from the app's path
@@ -397,7 +562,11 @@ class AppProcess(multiprocessing.Process):
397
562
  ingest_bmc_component_tree(writer.base_component_tree, self.bmc_components)
398
563
  except BaseException:
399
564
  writer.core.initial_state.add_log_entry(
400
- "error", "UI Components Error", "Couldn't load components. An exception was raised.", tb.format_exc())
565
+ "error",
566
+ "UI Components Error",
567
+ "Couldn't load components. An exception was raised.",
568
+ tb.format_exc(),
569
+ )
401
570
  if self.mode == "run":
402
571
  terminate_early = True
403
572
 
@@ -407,10 +576,14 @@ class AppProcess(multiprocessing.Process):
407
576
  # Initialisation errors will be sent to all sessions via mail during session initialisation
408
577
 
409
578
  writer.core.initial_state.add_log_entry(
410
- "error", "Code Error", "Couldn't execute code. An exception was raised.", tb.format_exc())
411
-
579
+ "error",
580
+ "Code Error",
581
+ "Couldn't execute code. An exception was raised.",
582
+ tb.format_exc(),
583
+ )
584
+
412
585
  # Exit if in run mode
413
-
586
+
414
587
  if self.mode == "run":
415
588
  terminate_early = True
416
589
 
@@ -420,19 +593,18 @@ class AppProcess(multiprocessing.Process):
420
593
 
421
594
  self._run_app_process_server()
422
595
 
423
- def _handle_message_and_get_packet(self, message_id: int, session_id: str, request: AppProcessServerRequest) -> AppProcessServerResponsePacket:
596
+ def _handle_message_and_get_packet(
597
+ self, message_id: int, session_id: str, request: AppProcessServerRequest
598
+ ) -> AppProcessServerResponsePacket:
424
599
  response = None
425
600
  try:
426
601
  response = self._handle_message(session_id, request)
427
602
  except (MessageHandlingException, ValidationError) as e:
428
603
  response = AppProcessServerResponse(
429
- status="error",
430
- status_message=repr(e),
431
- payload=None
604
+ status="error", status_message=repr(e), payload=None
432
605
  )
433
606
 
434
- packet: AppProcessServerResponsePacket = (
435
- message_id, session_id, response)
607
+ packet: AppProcessServerResponsePacket = (message_id, session_id, response)
436
608
  return packet
437
609
 
438
610
  def _send_packet(self, packet_future: concurrent.futures.Future) -> None:
@@ -443,13 +615,13 @@ class AppProcess(multiprocessing.Process):
443
615
 
444
616
  def _run_app_process_server(self) -> None:
445
617
  is_app_process_server_terminated = threading.Event()
446
- session_pruner = SessionPruner(
447
- is_app_process_server_terminated)
618
+ session_pruner = SessionPruner(is_app_process_server_terminated)
448
619
  session_pruner.start()
449
620
 
450
621
  def terminate_server():
451
622
  if is_app_process_server_terminated.is_set():
452
623
  return
624
+ self.executor.shutdown(wait=False)
453
625
  with self.server_conn_lock:
454
626
  self.server_conn.send(None)
455
627
  is_app_process_server_terminated.set()
@@ -465,49 +637,54 @@ class AppProcess(multiprocessing.Process):
465
637
  # No need to handle signal as not main thread
466
638
  pass
467
639
 
468
- with concurrent.futures.ThreadPoolExecutor(100) as thread_pool:
469
- self.is_app_process_server_ready.set()
470
- while True: # Starts app message server
471
- try:
472
- if not self.server_conn.poll(1):
473
- continue
474
- packet = self.server_conn.recv()
475
- if packet is None: # An empty packet terminates the process
476
- # Send empty packet to client for it to close
477
- terminate_server()
478
- return
479
- self._handle_app_process_server_packet(packet, thread_pool)
480
- except InterruptedError:
481
- terminate_server()
482
- return
483
- except BaseException as e:
484
- logging.error(
485
- f"Unexpected exception in AppProcess server.\n{repr(e)}")
640
+ self.is_app_process_server_ready.set()
641
+ while True and not is_app_process_server_terminated.is_set(): # Starts app message server
642
+ try:
643
+ if not self.server_conn.poll(1):
644
+ continue
645
+ packet = self.server_conn.recv()
646
+ if packet is None: # An empty packet terminates the process
647
+ # Send empty packet to client for it to close
486
648
  terminate_server()
487
649
  return
650
+ self._handle_app_process_server_packet(packet)
651
+ except Exception as e:
652
+ self.logger.error(f"Unexpected exception in AppProcess server.\n{repr(e)}")
653
+ terminate_server()
654
+ return
488
655
 
489
- def _handle_app_process_server_packet(self, packet: AppProcessServerRequestPacket, thread_pool) -> None:
656
+ def _handle_app_process_server_packet(self, packet: AppProcessServerRequestPacket) -> None:
657
+ if not self.executor:
658
+ return
490
659
  (message_id, session_id, request) = packet
491
- thread_pool_future = thread_pool.submit(self._handle_message_and_get_packet,
492
- message_id, session_id, request)
660
+ thread_pool_future = self.executor.submit(
661
+ self._handle_message_and_get_packet, message_id, session_id, request
662
+ )
493
663
  thread_pool_future.add_done_callback(self._send_packet)
494
664
 
495
665
  def run(self) -> None:
666
+ max_workers = int(os.getenv("WRITER_MAX_WORKERS", (os.cpu_count() or 4) * 10))
667
+ self.executor = concurrent.futures.ThreadPoolExecutor(
668
+ max_workers=max_workers,
669
+ )
496
670
  self.server_conn_lock = threading.Lock()
497
671
  self.client_conn.close()
498
672
  self._main()
499
673
 
500
674
 
501
675
  class FileEventHandler(watchdog.events.PatternMatchingEventHandler):
502
-
503
676
  """
504
677
  Watches for changes in files and triggers code reloads.
505
678
  """
506
679
 
507
680
  def __init__(self, update_callback: Callable, patterns: List[str]):
508
681
  self.update_callback = update_callback
509
- super().__init__(patterns=patterns, ignore_patterns=[
510
- ".*"], ignore_directories=False, case_sensitive=False)
682
+ super().__init__(
683
+ patterns=patterns,
684
+ ignore_patterns=[".*"],
685
+ ignore_directories=False,
686
+ case_sensitive=False,
687
+ )
511
688
 
512
689
  def on_any_event(self, event) -> None:
513
690
  if event.event_type not in ("modified", "deleted", "created"):
@@ -516,8 +693,7 @@ class FileEventHandler(watchdog.events.PatternMatchingEventHandler):
516
693
 
517
694
 
518
695
  class ThreadSafeAsyncEvent(asyncio.Event):
519
-
520
- """ Asyncio event adapted to be thread-safe."""
696
+ """Asyncio event adapted to be thread-safe."""
521
697
 
522
698
  def __init__(self):
523
699
  super().__init__()
@@ -529,22 +705,24 @@ class ThreadSafeAsyncEvent(asyncio.Event):
529
705
 
530
706
 
531
707
  class AppProcessListener(threading.Thread):
532
-
533
708
  """
534
709
  Listens to messages from the AppProcess server.
535
- Notifies receipt via events in response_events and makes the responses available in response_packets.
710
+ Notifies receipt via events in response_events and makes the responses available in response_packets.
536
711
  """
537
712
 
538
- def __init__(self,
539
- client_conn: multiprocessing.connection.Connection,
540
- is_app_process_server_ready: multiprocessing.synchronize.Event,
541
- response_packets: Dict,
542
- response_events: Dict):
713
+ def __init__(
714
+ self,
715
+ client_conn: multiprocessing.connection.Connection,
716
+ is_app_process_server_ready: multiprocessing.synchronize.Event,
717
+ response_packets: Dict,
718
+ response_events: Dict,
719
+ ):
543
720
  super().__init__(name="AppProcessListenerThread")
544
721
  self.client_conn = client_conn
545
722
  self.is_app_process_server_ready = is_app_process_server_ready
546
723
  self.response_packets = response_packets
547
724
  self.response_events = response_events
725
+ self.logger = logging.getLogger("writer")
548
726
 
549
727
  def run(self) -> None:
550
728
  self.is_app_process_server_ready.wait()
@@ -554,7 +732,7 @@ class AppProcessListener(threading.Thread):
554
732
  try:
555
733
  packet = self.client_conn.recv()
556
734
  except OSError:
557
- logging.error("Connection to AppProcess closed.")
735
+ self.logger.error("Connection to AppProcess closed.")
558
736
  return
559
737
  if packet is None:
560
738
  return
@@ -564,24 +742,19 @@ class AppProcessListener(threading.Thread):
564
742
  if response_event:
565
743
  response_event.set()
566
744
  else:
567
- raise ValueError(
568
- f"No response event found for message {message_id}.")
745
+ raise ValueError(f"No response event found for message {message_id}.")
569
746
 
570
747
 
571
748
  class LogListener(threading.Thread):
572
-
573
749
  """
574
750
  Logs messages stored in the multiprocessing queue.
575
- This allows log messages from the AppProcess to be safely managed.
751
+ This allows log messages from the AppProcess to be safely managed.
576
752
  """
577
753
 
578
- def __init__(self,
579
- log_queue: multiprocessing.Queue):
754
+ def __init__(self, log_queue: multiprocessing.Queue):
580
755
  super().__init__(name="LogListenerThread")
581
756
  self.log_queue = log_queue
582
757
  self.logger = logging.getLogger("from_app")
583
- self.logger.setLevel(logging.INFO)
584
- self.logger.addHandler(logging.StreamHandler())
585
758
 
586
759
  def run(self) -> None:
587
760
  while True:
@@ -592,7 +765,6 @@ class LogListener(threading.Thread):
592
765
 
593
766
 
594
767
  class AppRunner:
595
-
596
768
  """
597
769
  Starts a given user app in a separate process.
598
770
  Manages changes to the app.
@@ -600,8 +772,8 @@ class AppRunner:
600
772
  """
601
773
 
602
774
  UPDATE_CHECK_INTERVAL_SECONDS = 0.2
603
- WF_PROJECT_SAVE_INTERVAL = 0.2
604
- MAX_WAIT_NOTIFY_SECONDS = 10
775
+ WF_PROJECT_SAVE_INTERVAL = float(os.getenv("WRITER_SAVE_INTERVAL", "0.2"))
776
+ MAX_WAIT_NOTIFY_SECONDS = 30
605
777
 
606
778
  def __init__(self, app_path: str, mode: str):
607
779
  self.server_conn: Optional[multiprocessing.connection.Connection] = None
@@ -620,8 +792,8 @@ class AppRunner:
620
792
  self.message_counter = 0
621
793
  self.log_queue: multiprocessing.Queue = multiprocessing.Queue()
622
794
  self.log_listener: Optional[LogListener] = None
623
- self.code_update_loop: Optional[asyncio.AbstractEventLoop] = None
624
- self.code_update_condition: Optional[asyncio.Condition] = None
795
+ self.serve_loop: Optional[asyncio.AbstractEventLoop] = None
796
+ self.announcement_queues: Dict[str, asyncio.Queue] = {}
625
797
  self.wf_project_context = WfProjectContext(app_path=app_path)
626
798
 
627
799
  if mode not in ("edit", "run"):
@@ -631,37 +803,52 @@ class AppRunner:
631
803
  self._set_logger()
632
804
 
633
805
  def hook_to_running_event_loop(self):
634
-
635
806
  """
636
- Sets the properties required to notify the web server of the code update.
637
- Should be performed from the event loop which will consume the notification.
807
+ Sets the properties required to notify the web server of the announcements.
808
+ Should be performed from the event loop which will consume the notifications.
638
809
  """
639
810
 
640
- self.code_update_loop = asyncio.get_running_loop()
641
- self.code_update_condition = asyncio.Condition()
811
+ self.serve_loop = asyncio.get_running_loop()
642
812
 
643
813
  def _set_logger(self):
644
- logger = logging.getLogger("app")
814
+ logger = logging.getLogger("app_runner")
645
815
  logger.addHandler(logging.handlers.QueueHandler(self.log_queue))
646
816
  self.log_listener = LogListener(self.log_queue)
647
817
  self.log_listener.start()
648
818
 
649
819
  def _start_fs_observer(self):
650
- self.observer = PollingObserver(AppRunner.UPDATE_CHECK_INTERVAL_SECONDS)
651
- self.observer.schedule(FileEventHandler(self.reload_code_from_saved, patterns=["*.py"]), path=self.app_path, recursive=True)
652
- self.observer.schedule(FileEventHandler(self._install_requirements, patterns=["requirements.txt"]), path=self.app_path)
653
- self.observer.start()
820
+ if self.observer is None:
821
+ self.observer = PollingObserver(AppRunner.UPDATE_CHECK_INTERVAL_SECONDS)
822
+ self.observer.schedule(
823
+ FileEventHandler(self.reload_code_from_saved, patterns=["*.py"]),
824
+ path=self.app_path,
825
+ recursive=True,
826
+ )
827
+ # See _install_requirements docstring for info
828
+ # self.observer.schedule(
829
+ # FileEventHandler(self._install_requirements, patterns=["requirements.txt"]),
830
+ # path=self.app_path,
831
+ # )
832
+ if not self.observer.is_alive():
833
+ self.observer.start()
654
834
 
655
835
  def _start_wf_project_process_write_files(self):
656
- wf_project.start_process_write_files_async(self.wf_project_context, AppRunner.WF_PROJECT_SAVE_INTERVAL)
836
+ wf_project.start_process_write_files_async(
837
+ self.wf_project_context, AppRunner.WF_PROJECT_SAVE_INTERVAL
838
+ )
657
839
 
658
840
  def _install_requirements(self) -> None:
659
- logger = logging.getLogger('writer')
841
+ """
842
+ Not used anywhere anymore as this method of installing dependencies is not supported.
843
+ Left because might change in the future.
844
+ """
845
+
846
+ logger = logging.getLogger("writer")
660
847
  logger.debug("\nDetected changes in requirements.txt. Installing dependencies...")
661
848
  try:
662
849
  # Run pip install command
663
850
  subprocess.run(
664
- ["pip", "install", "-r", "requirements.txt"],
851
+ ["pip", "install", "-r", "requirements.txt"],
665
852
  check=True,
666
853
  capture_output=True,
667
854
  text=True,
@@ -672,7 +859,20 @@ class AppRunner:
672
859
  self.reload_code_from_saved()
673
860
  except subprocess.CalledProcessError as e:
674
861
  logger.warning(f"Error installing dependencies: {e.stderr}")
675
- # TODO(WF-170): find a way to dispatch log
862
+ self.queue_announcement(
863
+ "mail",
864
+ [
865
+ {
866
+ "type": "logEntry",
867
+ "payload": {
868
+ "type": "error",
869
+ "title": "Error installing dependencies",
870
+ "message": "The dependencies specified on `requirements.txt` could not be installed.",
871
+ "code": e.stderr,
872
+ },
873
+ }
874
+ ],
875
+ )
676
876
  except Exception as e:
677
877
  logger.warning(f"Unexpected error: {e}")
678
878
 
@@ -694,8 +894,9 @@ class AppRunner:
694
894
  # parent pid and pid.
695
895
  self._subscribe_terminal_signal()
696
896
 
697
- async def dispatch_message(self, session_id: Optional[str], request: AppProcessServerRequest) -> AppProcessServerResponse:
698
-
897
+ async def dispatch_message(
898
+ self, session_id: str, request: AppProcessServerRequest
899
+ ) -> AppProcessServerResponse:
699
900
  """
700
901
  Sends a message to the AppProcess server, waits for the listener to obtain a response and returns it.
701
902
  """
@@ -704,44 +905,49 @@ class AppRunner:
704
905
  self.message_counter += 1
705
906
  is_response_ready = ThreadSafeAsyncEvent()
706
907
  self.response_events[message_id] = is_response_ready
707
- packet: AppProcessServerRequestPacket = (
708
- message_id, session_id, request)
908
+ packet: AppProcessServerRequestPacket = (message_id, session_id, request)
709
909
 
710
910
  if self.client_conn is None:
711
- raise ValueError(
712
- "Cannot dispatch message. No connection to AppProcess server is set.")
911
+ raise ValueError("Cannot dispatch message. No connection to AppProcess server is set.")
713
912
  self.client_conn.send(packet)
714
913
 
715
914
  await is_response_ready.wait() # Set by the listener thread
716
915
 
717
916
  response_packet = self.response_packets.get(message_id)
718
917
  if response_packet is None:
719
- raise ValueError(
720
- f"Empty packet received in response to message {message_id}.")
918
+ raise ValueError(f"Empty packet received in response to message {message_id}.")
721
919
  response_message_id, response_session_id, response = response_packet
722
920
  del self.response_packets[message_id]
723
921
  del self.response_events[message_id]
724
- if (session_id != response_session_id):
922
+ if session_id != response_session_id:
725
923
  raise PermissionError("Session mismatch.")
726
- if (message_id != response_message_id):
924
+ if message_id != response_message_id:
727
925
  raise PermissionError("Message mismatch.")
728
926
 
729
927
  return response
730
928
 
731
-
732
- def create_persisted_script(self, file = "main.py"):
929
+ def create_persisted_script(self, file="main.py", content: Union[str, bytes] = ""):
733
930
  path = os.path.join(self.app_path, file)
734
931
  self._check_file_in_app_path(path)
735
932
 
736
- with open(path, "x", encoding='utf-8') as f:
737
- f.write('')
933
+ if isinstance(content, str):
934
+ mode = "w"
935
+ encoding = "utf-8"
936
+ elif isinstance(content, bytes):
937
+ mode = "wb"
938
+ encoding = None
939
+
940
+ with open(path, mode, encoding=encoding) as f:
941
+ f.write(content)
942
+ f.flush()
943
+ os.fsync(f.fileno())
738
944
 
739
945
  self.source_files = wf_project.build_source_files(self.app_path)
740
946
 
741
947
  def rename_persisted_script(self, from_path: str, to_path: str):
742
- if from_path == 'main.py':
948
+ if from_path == "main.py":
743
949
  raise PermissionError("cannot rename main script")
744
- if to_path == 'main.py':
950
+ if to_path == "main.py":
745
951
  raise PermissionError("cannot overwrite main script")
746
952
 
747
953
  from_path_abs = os.path.join(self.app_path, from_path)
@@ -751,12 +957,18 @@ class AppRunner:
751
957
  self._check_file_in_app_path(to_path_abs)
752
958
 
753
959
  os.makedirs(os.path.dirname(to_path_abs), exist_ok=True)
754
- os.rename(from_path_abs, to_path_abs)
960
+
961
+ try:
962
+ os.rename(from_path_abs, to_path_abs)
963
+ except OSError:
964
+ # If the error is due to the function not being implemented (like S3/Fuse), we fallback to copy/delete
965
+ shutil.copyfile(from_path_abs, to_path_abs)
966
+ os.remove(from_path_abs)
755
967
 
756
968
  self.source_files = wf_project.build_source_files(self.app_path)
757
969
 
758
970
  def delete_persisted_script(self, file: str):
759
- if file == 'main.py':
971
+ if file == "main.py":
760
972
  raise PermissionError("cannot delete main script")
761
973
 
762
974
  path = os.path.join(self.app_path, file)
@@ -769,14 +981,14 @@ class AppRunner:
769
981
 
770
982
  self.source_files = wf_project.build_source_files(self.app_path)
771
983
 
772
- def load_persisted_script(self, file = "main.py") -> str:
984
+ def load_persisted_script(self, file="main.py") -> str:
773
985
  path = os.path.join(self.app_path, file)
774
986
  self._check_file_in_app_path(path)
775
987
 
776
- logger = logging.getLogger('writer')
988
+ logger = logging.getLogger("writer")
777
989
  try:
778
990
  contents = None
779
- with open(path, "r", encoding='utf-8') as f:
991
+ with open(path, "r", encoding="utf-8") as f:
780
992
  contents = f.read()
781
993
  return contents
782
994
  except FileNotFoundError as error:
@@ -787,17 +999,21 @@ class AppRunner:
787
999
  raise error
788
1000
 
789
1001
  def _check_file_in_app_path(self, path):
790
- if not os.path.abspath(path).startswith(os.path.abspath((self.app_path))):
791
- raise PermissionError(f"{path} is outside of application ({self.app_path})")
792
-
1002
+ app_path = os.path.abspath(self.app_path)
1003
+ file_path = os.path.abspath(path)
1004
+ if file_path == app_path or not file_path.startswith(app_path):
1005
+ raise PermissionError(f"{path} should be inside of application ({self.app_path})")
1006
+ wf_path = os.path.abspath(os.path.join(self.app_path, ".wf"))
1007
+ if file_path.startswith(wf_path):
1008
+ raise PermissionError(f"{path} should not be inside of Writer Framework files ({wf_path})")
793
1009
 
794
1010
  def _load_persisted_components(self) -> Dict[str, ComponentDefinition]:
795
- logger = logging.getLogger('writer')
796
- if os.path.isfile(os.path.join(self.app_path, "ui.json")):
797
- wf_project.migrate_obsolete_ui_json(self.app_path, metadata={"writer_version": VERSION})
1011
+ logger = logging.getLogger("writer")
798
1012
 
799
- if not os.path.isfile(os.path.join(self.app_path, ".wf", 'components-workflows_root.jsonl')):
800
- wf_project.create_default_workflows_root(self.app_path)
1013
+ if not os.path.isfile(
1014
+ os.path.join(self.app_path, ".wf", "components-blueprints_root.jsonl")
1015
+ ):
1016
+ wf_project.create_default_blueprints_root(self.app_path)
801
1017
 
802
1018
  if not os.path.isdir(os.path.join(self.app_path, ".wf")):
803
1019
  logger.error("Couldn't find .wf in the path provided: %s.", self.app_path)
@@ -807,49 +1023,75 @@ class AppRunner:
807
1023
  components = audit_and_fix.fix_components(components)
808
1024
  return components
809
1025
 
1026
+ async def queue_message(self, session_id: str, data: Any) -> AppProcessServerResponse:
1027
+ return await self.dispatch_message(session_id, QueueMessageRequest(type="queueMessage", payload=data))
1028
+
1029
+ async def retrieve_messages(self, session_id: str) -> list:
1030
+ response = await self.dispatch_message(
1031
+ session_id, AppProcessServerRequest(type="retrieveMessages", payload=None)
1032
+ )
1033
+ if isinstance(response.payload, list):
1034
+ return response.payload
1035
+ return []
1036
+
1037
+ async def clear_messages(self, session_id: str) -> AppProcessServerResponse:
1038
+ response = await self.dispatch_message(
1039
+ session_id, AppProcessServerRequest(type="clearMessages", payload=None)
1040
+ )
1041
+ return response
1042
+
810
1043
  async def check_session(self, session_id: str) -> bool:
811
- response = await self.dispatch_message(session_id, AppProcessServerRequest(
812
- type="checkSession",
813
- payload=None
814
- ))
1044
+ response = await self.dispatch_message(
1045
+ session_id, AppProcessServerRequest(type="checkSession", payload=None)
1046
+ )
815
1047
  is_ok: bool = response.status == "ok"
816
1048
  return is_ok
817
1049
 
818
1050
  async def init_session(self, payload: InitSessionRequestPayload) -> AppProcessServerResponse:
819
- return await self.dispatch_message(None, InitSessionRequest(
820
- type="sessionInit",
821
- payload=payload
822
- ))
1051
+ return await self.dispatch_message(
1052
+ "anonymous", InitSessionRequest(type="sessionInit", payload=payload)
1053
+ )
823
1054
 
824
- async def update_components(self, session_id: str, payload: ComponentUpdateRequestPayload) -> AppProcessServerResponse:
1055
+ async def update_components(
1056
+ self, session_id: str, payload: ComponentUpdateRequestPayload
1057
+ ) -> AppProcessServerResponse:
825
1058
  if self.mode != "edit":
826
- raise PermissionError(
827
- "Cannot update components in non-update mode.")
1059
+ raise PermissionError("Cannot update components in non-update mode.")
828
1060
  self.bmc_components = payload.components
829
1061
 
830
- wf_project.write_files_async(self.wf_project_context, metadata={"writer_version": VERSION}, components=payload.components)
1062
+ wf_project.write_files_async(
1063
+ self.wf_project_context,
1064
+ metadata={"writer_version": VERSION},
1065
+ components=payload.components,
1066
+ )
1067
+
1068
+ return await self.dispatch_message(
1069
+ session_id, ComponentUpdateRequest(type="componentUpdate", payload=payload)
1070
+ )
1071
+
1072
+ async def list_resources(self, session_id: str, resource_type: str) -> AppProcessServerResponse:
1073
+ if self.mode != "edit":
1074
+ raise PermissionError("Cannot update components in non-update mode.")
1075
+ message_payload = ListResourcesRequestPayload(resource_type=resource_type)
1076
+ message = ListResourcesRequest(type="listResources", payload=message_payload)
1077
+ return await self.dispatch_message(session_id, message)
831
1078
 
832
- return await self.dispatch_message(session_id, ComponentUpdateRequest(
833
- type="componentUpdate",
834
- payload=payload
835
- ))
1079
+ async def writer_vault_refresh(self, session_id: str) -> AppProcessServerResponse:
1080
+ message = WriterVaultUpdateRequest(type="writerVaultUpdate")
1081
+ return await self.dispatch_message(session_id, message)
836
1082
 
837
1083
  async def handle_event(self, session_id: str, event: WriterEvent) -> AppProcessServerResponse:
838
- return await self.dispatch_message(session_id, EventRequest(
839
- type="event",
840
- payload=event
841
- ))
1084
+ return await self.dispatch_message(session_id, EventRequest(type="event", payload=event))
842
1085
 
843
- async def handle_hash_request(self, session_id: str, payload: HashRequestPayload) -> AppProcessServerResponse:
844
- return await self.dispatch_message(session_id, HashRequest(
845
- type="hashRequest",
846
- payload=payload
847
- ))
1086
+ async def handle_hash_request(
1087
+ self, session_id: str, payload: HashRequestPayload
1088
+ ) -> AppProcessServerResponse:
1089
+ return await self.dispatch_message(
1090
+ session_id, HashRequest(type="hashRequest", payload=payload)
1091
+ )
848
1092
 
849
1093
  async def handle_state_enquiry(self, session_id: str) -> AppProcessServerResponse:
850
- return await self.dispatch_message(session_id, StateEnquiryRequest(
851
- type="stateEnquiry"
852
- ))
1094
+ return await self.dispatch_message(session_id, StateEnquiryRequest(type="stateEnquiry"))
853
1095
 
854
1096
  async def handle_state_content(self, session_id: str) -> AppProcessServerResponse:
855
1097
  """
@@ -857,11 +1099,9 @@ class AppRunner:
857
1099
 
858
1100
  It is only accessible through tests
859
1101
  """
860
- return await self.dispatch_message(session_id, StateContentRequest(
861
- type="stateContent"
862
- ))
1102
+ return await self.dispatch_message(session_id, StateContentRequest(type="stateContent"))
863
1103
 
864
- def save_code(self, session_id: str, code: str, path: List[str] = ['main.py']) -> None:
1104
+ def save_code(self, session_id: str, code: str, path: List[str] = ["main.py"]) -> None:
865
1105
  if self.mode != "edit":
866
1106
  raise PermissionError("Cannot save code in non-edit mode.")
867
1107
 
@@ -873,9 +1113,108 @@ class AppRunner:
873
1113
 
874
1114
  with open(filepath, "w") as f:
875
1115
  f.write(code)
1116
+ f.flush()
1117
+ os.fsync(f.fileno())
876
1118
 
877
1119
  self.source_files = wf_project.build_source_files(self.app_path)
878
1120
 
1121
+ def export_zip(self):
1122
+ if self.mode != "edit":
1123
+ raise PermissionError("Cannot export in non-edit mode.")
1124
+ zip_buffer = io.BytesIO()
1125
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
1126
+ for root, dirs, files in os.walk(self.app_path):
1127
+ for file in files:
1128
+ if file.endswith('.pyc'):
1129
+ continue
1130
+ full_path = os.path.join(root, file)
1131
+ arcname = os.path.relpath(full_path, start=self.app_path)
1132
+ zipf.write(full_path, arcname=arcname)
1133
+ zip_buffer.seek(0)
1134
+ return zip_buffer
1135
+
1136
+ def _sync_folders(self, src: str, dst: str):
1137
+ """
1138
+ Synchronizes the contents of the source folder to the destination folder in a one-way manner.
1139
+
1140
+ - Copies all files and subdirectories from `src` to `dst`.
1141
+ - Creates any missing directories in `dst` to match `src`.
1142
+ - Removes any files or directories in `dst` that do not exist in `src`.
1143
+ """
1144
+ # Create dst if it doesn't exist
1145
+ os.makedirs(dst, exist_ok=True)
1146
+
1147
+ # Copy files and folders from src to dst
1148
+ for root, dirs, files in os.walk(src):
1149
+ rel_path = os.path.relpath(root, src)
1150
+ dst_path = os.path.join(dst, rel_path)
1151
+
1152
+ # Create directories in dst
1153
+ os.makedirs(dst_path, exist_ok=True)
1154
+
1155
+ # Copy files
1156
+ for file in files:
1157
+ src_file = os.path.join(root, file)
1158
+ dst_file = os.path.join(dst_path, file)
1159
+ shutil.copy2(src_file, dst_file)
1160
+
1161
+ # Remove files and folders in dst that don't exist in src
1162
+ for root, dirs, files in os.walk(dst):
1163
+ rel_path = os.path.relpath(root, dst)
1164
+ src_path = os.path.join(src, rel_path)
1165
+
1166
+ # Remove files not in src
1167
+ for file in files:
1168
+ dst_file = os.path.join(root, file)
1169
+ src_file = os.path.join(src_path, file)
1170
+ if not os.path.exists(src_file):
1171
+ os.remove(dst_file)
1172
+
1173
+ # Remove empty directories not in src
1174
+ for dir in dirs:
1175
+ dst_dir = os.path.join(root, dir)
1176
+ src_dir = os.path.join(src_path, dir)
1177
+ if not os.path.exists(src_dir):
1178
+ shutil.rmtree(dst_dir)
1179
+
1180
+ async def import_zip(self, zip_path: str):
1181
+ if self.mode != "edit":
1182
+ raise PermissionError("Cannot import in non-edit mode.")
1183
+
1184
+ try:
1185
+ with tempfile.TemporaryDirectory() as tmpdir:
1186
+ extracted_path = os.path.join(tmpdir, "imported_agent")
1187
+ os.makedirs(extracted_path, exist_ok=True)
1188
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
1189
+ zip_ref.extractall(extracted_path)
1190
+
1191
+ main_py_dir = None
1192
+ for root, _, files in os.walk(extracted_path):
1193
+ if "main.py" in files:
1194
+ main_py_dir = root
1195
+ break
1196
+
1197
+ if main_py_dir is None:
1198
+ raise ValueError("main.py not found in the imported archive.")
1199
+
1200
+ wf_dir_path = os.path.join(main_py_dir, ".wf")
1201
+ if not os.path.isdir(wf_dir_path):
1202
+ raise ValueError(".wf directory not found alongside main.py in the archive.")
1203
+
1204
+ # Passed all checks; replace current app contents
1205
+
1206
+ logging.info("Copying app at %s", main_py_dir)
1207
+ if self.observer is not None:
1208
+ self.observer.unschedule_all()
1209
+
1210
+ self._sync_folders(main_py_dir, self.app_path)
1211
+
1212
+ self._start_fs_observer()
1213
+ self.bmc_components = self._load_persisted_components()
1214
+ self.reload_code_from_saved()
1215
+ except zipfile.BadZipFile:
1216
+ raise ValueError("Uploaded file is not a valid ZIP.")
1217
+
879
1218
  def _clean_process(self) -> None:
880
1219
  # Terminate the AppProcess server by sending an empty message
881
1220
  # The empty message will bounce an empty message and terminate the client too
@@ -916,12 +1255,15 @@ class AppRunner:
916
1255
  if self.run_code is None:
917
1256
  raise ValueError("Cannot start app process. Code hasn't been set.")
918
1257
  if self.bmc_components is None:
919
- raise ValueError(
920
- "Cannot start app process. Components haven't been set.")
1258
+ raise ValueError("Cannot start app process. Components haven't been set.")
921
1259
  self.is_app_process_server_ready.clear()
922
1260
  client_conn, server_conn = multiprocessing.Pipe(duplex=True)
923
- self.client_conn = cast(multiprocessing.connection.Connection, client_conn) # for mypy type checking on windows
924
- self.server_conn = cast(multiprocessing.connection.Connection, server_conn) # for mypy type checking on windows
1261
+ self.client_conn = cast(
1262
+ multiprocessing.connection.Connection, client_conn
1263
+ ) # for mypy type checking on windows
1264
+ self.server_conn = cast(
1265
+ multiprocessing.connection.Connection, server_conn
1266
+ ) # for mypy type checking on windows
925
1267
 
926
1268
  self.app_process = AppProcess(
927
1269
  client_conn=self.client_conn,
@@ -931,13 +1273,15 @@ class AppRunner:
931
1273
  run_code=self.run_code,
932
1274
  bmc_components=self.bmc_components,
933
1275
  is_app_process_server_ready=self.is_app_process_server_ready,
934
- is_app_process_server_failed=self.is_app_process_server_failed)
1276
+ is_app_process_server_failed=self.is_app_process_server_failed,
1277
+ )
935
1278
  self.app_process.start()
936
1279
  self.app_process_listener = AppProcessListener(
937
1280
  self.client_conn,
938
1281
  self.is_app_process_server_ready,
939
1282
  self.response_packets,
940
- self.response_events)
1283
+ self.response_events,
1284
+ )
941
1285
  self.app_process_listener.start()
942
1286
  self.is_app_process_server_ready.wait()
943
1287
  if self.mode == "run" and self.is_app_process_server_failed.is_set():
@@ -950,7 +1294,6 @@ class AppRunner:
950
1294
  self.update_code(None, self.load_persisted_script())
951
1295
 
952
1296
  def update_code(self, session_id: Optional[str], run_code: str) -> None:
953
-
954
1297
  """
955
1298
  Updates the running code and notifies the update.
956
1299
  In order to notify of the update, the event loop and asyncio.Condition need
@@ -966,24 +1309,36 @@ class AppRunner:
966
1309
  self._clean_process()
967
1310
  self._start_app_process()
968
1311
  self.is_app_process_server_ready.wait()
969
-
970
- if self.code_update_loop is not None and self.code_update_condition is not None:
971
- future = asyncio.run_coroutine_threadsafe(self.notify_of_code_update(), self.code_update_loop)
972
- future.result(AppRunner.MAX_WAIT_NOTIFY_SECONDS)
1312
+ self.queue_announcement("codeUpdate", None)
973
1313
 
974
- async def notify_of_code_update(self):
975
- await self.code_update_condition.acquire()
976
- try:
977
- self.code_update_condition.notify_all()
978
- finally:
979
- self.code_update_condition.release()
1314
+ async def queue_announcement_async(
1315
+ self, type, payload, exclude_session_id: Optional[str] = None
1316
+ ):
1317
+ for session_id, announcement_queue in self.announcement_queues.items():
1318
+ if session_id == exclude_session_id:
1319
+ continue
1320
+ await announcement_queue.put({"type": type, "payload": payload})
1321
+
1322
+ def queue_announcement(self, type, payload):
1323
+ async def announce(type: str, payload: Any):
1324
+ for announcement_queue in self.announcement_queues.values():
1325
+ await announcement_queue.put({"type": type, "payload": payload})
1326
+
1327
+ if self.serve_loop is not None:
1328
+ try:
1329
+ future = asyncio.run_coroutine_threadsafe(announce(type, payload), self.serve_loop)
1330
+ future.result(AppRunner.MAX_WAIT_NOTIFY_SECONDS)
1331
+ except (
1332
+ RuntimeError,
1333
+ concurrent.futures.CancelledError,
1334
+ concurrent.futures.TimeoutError,
1335
+ ):
1336
+ # Ignore errors that occur during pytest runs where serve_loop may be closed
1337
+ pass
980
1338
 
981
1339
  def set_userinfo(self, session_id: str, userinfo: dict) -> None:
982
1340
  def run_async_in_thread():
983
- message = AppProcessServerRequest(
984
- type="setUserinfo",
985
- payload=userinfo
986
- )
1341
+ message = AppProcessServerRequest(type="setUserinfo", payload=userinfo)
987
1342
 
988
1343
  asyncio.run(self.dispatch_message(session_id, message))
989
1344