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.
- writer/__init__.py +1 -1
- writer/abstract.py +1 -1
- writer/{ai.py → ai/__init__.py} +867 -163
- writer/app_runner.py +596 -241
- writer/app_templates/default/.wf/components-blueprints_blueprint-0-0decp3w5erhvl0nw.jsonl +11 -0
- writer/app_templates/default/.wf/components-blueprints_root.jsonl +1 -0
- writer/app_templates/default/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +27 -0
- writer/app_templates/default/.wf/components-root.jsonl +1 -0
- writer/app_templates/default/.wf/components-workflows_root.jsonl +1 -0
- writer/app_templates/default/.wf/components-workflows_workflow-0-lfltcky7l1fsm6j2.jsonl +1 -0
- writer/app_templates/default/.wf/metadata.json +3 -0
- writer/app_templates/default/README.md +3 -0
- writer/app_templates/default/main.py +16 -0
- writer/app_templates/default/requirements.txt +1 -0
- writer/app_templates/default/static/README.md +8 -0
- writer/app_templates/default/static/agent_builder_demo.png +0 -0
- writer/app_templates/default/static/favicon.png +0 -0
- writer/app_templates/hello/.wf/components-blueprints_blueprint-0-t84xyhxau9ej3823.jsonl +18 -0
- writer/app_templates/hello/.wf/components-blueprints_root.jsonl +1 -0
- writer/app_templates/hello/.wf/components-page-0-c0f99a9e-5004-4e75-a6c6-36f17490b134.jsonl +15 -0
- writer/app_templates/hello/.wf/components-root.jsonl +1 -0
- writer/app_templates/hello/.wf/metadata.json +3 -0
- writer/app_templates/hello/main.py +16 -0
- writer/app_templates/hello/static/README.md +8 -0
- writer/app_templates/hello/static/favicon.png +0 -0
- writer/app_templates/hello/static/welcome.svg +40 -0
- writer/auth.py +7 -2
- writer/autogen.py +352 -0
- writer/blocks/__init__.py +51 -17
- writer/blocks/addtostatelist.py +10 -9
- writer/blocks/apitrigger.py +45 -0
- writer/blocks/base_block.py +332 -21
- writer/blocks/base_trigger.py +14 -0
- writer/blocks/calleventhandler.py +39 -35
- writer/blocks/changepage.py +48 -0
- writer/blocks/code.py +102 -0
- writer/blocks/crontrigger.py +49 -0
- writer/blocks/foreach.py +70 -53
- writer/blocks/httprequest.py +112 -99
- writer/blocks/ifelse.py +71 -0
- writer/blocks/logmessage.py +34 -39
- writer/blocks/parsejson.py +30 -29
- writer/blocks/returnvalue.py +7 -7
- writer/blocks/runblueprint.py +63 -0
- writer/blocks/setstate.py +43 -33
- writer/blocks/sharedblueprint.py +86 -0
- writer/blocks/uieventtrigger.py +49 -0
- writer/blocks/writeraddchatmessage.py +50 -12
- writer/blocks/writeraddtokg.py +38 -11
- writer/blocks/writeraskkg.py +123 -0
- writer/blocks/writerchat.py +80 -61
- writer/blocks/writerchatreply.py +279 -0
- writer/blocks/writerchatreplywithtoolconfig.py +393 -0
- writer/blocks/writerclassification.py +78 -39
- writer/blocks/writercompletion.py +49 -44
- writer/blocks/writerfileapi.py +85 -0
- writer/blocks/writerinitchat.py +24 -12
- writer/blocks/writerkeyvaluestorage.py +106 -0
- writer/blocks/writernocodeapp.py +35 -37
- writer/blocks/writerparsepdf.py +73 -0
- writer/blocks/writerstructuredoutput.py +105 -0
- writer/blocks/writertoolcalling.py +251 -0
- writer/blocks/writervision.py +141 -0
- writer/blocks/writerwebsearch.py +175 -0
- writer/blueprints.py +839 -0
- writer/command_line.py +52 -16
- writer/core.py +562 -290
- writer/core_ui.py +6 -2
- writer/evaluator.py +98 -46
- writer/journal.py +227 -0
- writer/keyvalue_storage.py +93 -0
- writer/logs.py +277 -0
- writer/serve.py +625 -327
- writer/ss_types.py +101 -12
- writer/static/assets/Arrow.dom-GBJpMYQS.js +1 -0
- writer/static/assets/BaseMarkdown-Wrvby5J8.js +1 -0
- writer/static/assets/BlueprintToolbar-BuXNRxWT.js +1 -0
- writer/static/assets/BlueprintToolbar-wpfX0jo_.css +1 -0
- writer/static/assets/BuilderApp-PTOI76jZ.js +8 -0
- writer/static/assets/BuilderApp-WimUfNZr.css +1 -0
- writer/static/assets/BuilderApplicationSelect-DXzy4e_h.js +7 -0
- writer/static/assets/BuilderApplicationSelect-XaM1D5fv.css +1 -0
- writer/static/assets/BuilderBlueprintLibraryPanel-Ckrhknlh.css +1 -0
- writer/static/assets/BuilderBlueprintLibraryPanel-DBDzhTlc.js +1 -0
- writer/static/assets/BuilderEmbeddedCodeEditor-B0bcjlhk.css +1 -0
- writer/static/assets/BuilderEmbeddedCodeEditor-Dn7eDICN.js +726 -0
- writer/static/assets/BuilderGraphSelect-C-LRsO8W.js +7 -0
- writer/static/assets/BuilderGraphSelect-D7B61d5s.css +1 -0
- writer/static/assets/BuilderInsertionLabel-BhyL9wgn.js +1 -0
- writer/static/assets/BuilderInsertionLabel-_YS5WPfq.css +1 -0
- writer/static/assets/BuilderInsertionOverlay-D2XS0ij9.css +1 -0
- writer/static/assets/BuilderInsertionOverlay-MkAIVruY.js +1 -0
- writer/static/assets/BuilderJournal-A0LcEwGI.js +7 -0
- writer/static/assets/BuilderJournal-DHv3Pvvm.css +1 -0
- writer/static/assets/BuilderModelSelect-CdSo_sih.js +7 -0
- writer/static/assets/BuilderModelSelect-Dc4IPLp2.css +1 -0
- writer/static/assets/BuilderSettings-BDwZBveu.js +16 -0
- writer/static/assets/BuilderSettings-lZkOXEYw.css +1 -0
- writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-3O6jKBXD.js +4 -0
- writer/static/assets/BuilderSettingsArtifactAPITriggerDetails-DnX66iRg.css +1 -0
- writer/static/assets/BuilderSettingsDeploySharedBlueprint-BR_3ptsd.js +1 -0
- writer/static/assets/BuilderSettingsDeploySharedBlueprint-KJTl8gxP.css +1 -0
- writer/static/assets/BuilderSettingsHandlers-CBtEQFSo.js +1 -0
- writer/static/assets/BuilderSettingsHandlers-DJPeASfz.css +1 -0
- writer/static/assets/BuilderSidebarComponentTree-DLltgas5.js +1 -0
- writer/static/assets/BuilderSidebarComponentTree-DYu1F793.css +1 -0
- writer/static/assets/BuilderSidebarToolkit-CApZNTAq.js +7 -0
- writer/static/assets/BuilderSidebarToolkit-CwqbjRv8.css +1 -0
- writer/static/assets/BuilderTemplateEditor-CYSDeWgV.css +1 -0
- writer/static/assets/BuilderTemplateEditor-DnRDRcA0.js +87 -0
- writer/static/assets/BuilderVault-2vGoV0sx.js +1 -0
- writer/static/assets/BuilderVault-Cx6oQSES.css +1 -0
- writer/static/assets/ComponentRenderer-72hqvEvI.css +1 -0
- writer/static/assets/ComponentRenderer-D4Pj1i3s.js +1 -0
- writer/static/assets/SharedCopyClipboardButton-BipJKGtz.css +1 -0
- writer/static/assets/SharedCopyClipboardButton-DNI9kLe6.js +1 -0
- writer/static/assets/WdsCheckbox-DKvpPA4D.css +1 -0
- writer/static/assets/WdsCheckbox-edQcn1cf.js +1 -0
- writer/static/assets/WdsDropdownMenu-CzzPN9Wg.css +1 -0
- writer/static/assets/WdsDropdownMenu-DQnrRBNV.js +1 -0
- writer/static/assets/WdsFieldWrapper-Cmufx5Nj.js +1 -0
- writer/static/assets/WdsFieldWrapper-CsemOh8D.css +1 -0
- writer/static/assets/WdsTabs-DKj7BqI0.css +1 -0
- writer/static/assets/WdsTabs-DcfY_zn5.js +1 -0
- writer/static/assets/abap-D8nrxEjS.js +6 -0
- writer/static/assets/apex-BrXDlLUW.js +6 -0
- writer/static/assets/art-paper-D70v1WMA.svg +180 -0
- writer/static/assets/azcli-CElzELwZ.js +6 -0
- writer/static/assets/bat-CUsyEhik.js +6 -0
- writer/static/assets/bicep-BtxyJn6H.js +7 -0
- writer/static/assets/cameligo-ClBCoF8h.js +6 -0
- writer/static/assets/clojure-B9TqLHAk.js +6 -0
- writer/static/assets/codicon-BA2IlpFX.ttf +0 -0
- writer/static/assets/coffee-DYsfeylR.js +6 -0
- writer/static/assets/cpp-VVGvvgir.js +6 -0
- writer/static/assets/csharp-Z6z2stHy.js +6 -0
- writer/static/assets/csp-DgZoLDI1.js +6 -0
- writer/static/assets/css-KqQ96-gC.js +8 -0
- writer/static/assets/css.worker-DvNUQFd1.js +84 -0
- writer/static/assets/cssMode-BYq4oZGq.js +9 -0
- writer/static/assets/cypher-CYoSlgTu.js +6 -0
- writer/static/assets/dart-BGDl7St1.js +6 -0
- writer/static/assets/dockerfile-CuCtxA7T.js +6 -0
- writer/static/assets/ecl-BCTFAUpS.js +6 -0
- writer/static/assets/editor.worker-BVwmgLrR.js +11 -0
- writer/static/assets/elixir-C7hRTYZ9.js +6 -0
- writer/static/assets/flow9-Bi_qi707.js +6 -0
- writer/static/assets/freemarker2-CnNourkO.js +8 -0
- writer/static/assets/fsharp-CxaaEKKi.js +6 -0
- writer/static/assets/go-DUImKuGY.js +6 -0
- writer/static/assets/graphql-D5sGVkLV.js +6 -0
- writer/static/assets/handlebars-Bm22yapJ.js +6 -0
- writer/static/assets/hcl-zD_CCkZ1.js +6 -0
- writer/static/assets/html-CAKAfoZF.js +6 -0
- writer/static/assets/html.worker-BJMlcbMU.js +458 -0
- writer/static/assets/htmlMode-BGZ97n-V.js +9 -0
- writer/static/assets/index-5u5REPT4.js +16 -0
- writer/static/assets/index-BKNuk68o.css +1 -0
- writer/static/assets/index-BQNXU3IR.js +17 -0
- writer/static/assets/index-BQr1pfrb.js +1 -0
- writer/static/assets/index-DHXAd5Yn.js +4 -0
- writer/static/assets/index-Zki-pfO-.js +8525 -0
- writer/static/assets/index.esm-B1ZQtduY.js +17 -0
- writer/static/assets/ini-8kKHd4ZL.js +6 -0
- writer/static/assets/java-De1axCfe.js +6 -0
- writer/static/assets/javascript-X1f02eyK.js +6 -0
- writer/static/assets/json.worker-BwvX8PuZ.js +42 -0
- writer/static/assets/jsonMode-hT0bNgT8.js +11 -0
- writer/static/assets/julia-D3ApGBxz.js +6 -0
- writer/static/assets/kotlin-GbSrCElU.js +6 -0
- writer/static/assets/less-DNUaDNdz.js +7 -0
- writer/static/assets/lexon-Bg9QKxBu.js +6 -0
- writer/static/assets/liquid-KmCCiJw2.js +6 -0
- writer/static/assets/lua-Crkvc3mc.js +6 -0
- writer/static/assets/m3-DsrzVyM1.js +6 -0
- writer/static/assets/mapbox-gl-C0cyFYYW.js +2329 -0
- writer/static/assets/markdown-CY5IOZuu.js +6 -0
- writer/static/assets/marked.esm-273vDTCT.js +45 -0
- writer/static/assets/mdx-DtRFauUw.js +6 -0
- writer/static/assets/mips-BE8RsGBA.js +6 -0
- writer/static/assets/msdax-N5ajIiFQ.js +6 -0
- writer/static/assets/mysql-DRxbB97D.js +6 -0
- writer/static/assets/objective-c-BHUZy23s.js +6 -0
- writer/static/assets/pascal-BemVzBTY.js +6 -0
- writer/static/assets/pascaligo-BACCcnx_.js +6 -0
- writer/static/assets/pdf-B6-yWJ-Y.js +12 -0
- writer/static/assets/pdf.worker.min-CyUfim15.mjs +21 -0
- writer/static/assets/perl-CuU66Ptk.js +6 -0
- writer/static/assets/pgsql-CQ6TMH2r.js +6 -0
- writer/static/assets/php-BvyzZa65.js +6 -0
- writer/static/assets/pla-DrIuu9u1.js +6 -0
- writer/static/assets/plotly.min-DutuuatZ.js +4030 -0
- writer/static/assets/poppins-latin-300-italic-4WBEAciR.woff +0 -0
- writer/static/assets/poppins-latin-300-italic-EWCPeN2Y.woff2 +0 -0
- writer/static/assets/poppins-latin-300-normal-DCNuMXUj.woff +0 -0
- writer/static/assets/poppins-latin-300-normal-Dku2WoCh.woff2 +0 -0
- writer/static/assets/poppins-latin-400-italic-B4GYq972.woff2 +0 -0
- writer/static/assets/poppins-latin-400-italic-BPejoDS-.woff +0 -0
- writer/static/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- writer/static/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- writer/static/assets/poppins-latin-500-italic-Ce_qjtl5.woff +0 -0
- writer/static/assets/poppins-latin-500-italic-o28Otv0U.woff2 +0 -0
- writer/static/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- writer/static/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- writer/static/assets/poppins-latin-600-italic-BhOZippK.woff +0 -0
- writer/static/assets/poppins-latin-600-italic-CZ4wqKBi.woff2 +0 -0
- writer/static/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- writer/static/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- writer/static/assets/poppins-latin-700-italic-CW91C-LJ.woff +0 -0
- writer/static/assets/poppins-latin-700-italic-RKf6esGj.woff2 +0 -0
- writer/static/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- writer/static/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-italic-CBzyU4Pf.woff +0 -0
- writer/static/assets/poppins-latin-ext-300-italic-DdDvTq5-.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-normal-7Zg2msWE.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-300-normal-C9p7gvmA.woff +0 -0
- writer/static/assets/poppins-latin-ext-400-italic-BiCGV3eO.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-400-italic-gsPYOGqV.woff +0 -0
- writer/static/assets/poppins-latin-ext-400-normal-CIpeJEZw.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-400-normal-Ce_uWq1Z.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-italic-CwrTHwbn.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-500-italic-jdc8Bv4M.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-normal-Bl1-S02S.woff +0 -0
- writer/static/assets/poppins-latin-ext-500-normal-H4Q0z8D2.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-italic-BqeDa496.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-italic-C7MQPb_A.woff +0 -0
- writer/static/assets/poppins-latin-ext-600-normal-Cn4C8475.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-600-normal-DB6FJURc.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-italic-BAdhB_WS.woff2 +0 -0
- writer/static/assets/poppins-latin-ext-700-italic-WKTwQMp8.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-normal-CE2WFKmF.woff +0 -0
- writer/static/assets/poppins-latin-ext-700-normal-DDaViAzG.woff2 +0 -0
- writer/static/assets/postiats-BR_hrfni.js +6 -0
- writer/static/assets/powerquery-CKDUeRmd.js +6 -0
- writer/static/assets/powershell-Dsa4rhA_.js +6 -0
- writer/static/assets/protobuf-CGsvhooB.js +7 -0
- writer/static/assets/pug-D2p3uOX2.js +6 -0
- writer/static/assets/python-DVhxg746.js +6 -0
- writer/static/assets/qsharp-B7F3HtPF.js +6 -0
- writer/static/assets/r-3aLoi2fs.js +6 -0
- writer/static/assets/razor-DR5Ns_BC.js +6 -0
- writer/static/assets/redis-jqFeRM5s.js +6 -0
- writer/static/assets/redshift-BriwQgXR.js +6 -0
- writer/static/assets/restructuredtext-hbBFZ0w9.js +6 -0
- writer/static/assets/ruby-ByThyB2Q.js +6 -0
- writer/static/assets/rust-DIEZMp5R.js +6 -0
- writer/static/assets/sb-C6Gjjw_x.js +6 -0
- writer/static/assets/scala-DZNw3jJB.js +6 -0
- writer/static/assets/scheme-55eqh71t.js +6 -0
- writer/static/assets/scss-D-OVkc4F.js +8 -0
- writer/static/assets/serialization-DJC7NP0N.js +20 -0
- writer/static/assets/shell-DSpi8_qN.js +6 -0
- writer/static/assets/solidity-BHddiNFS.js +6 -0
- writer/static/assets/sophia-D6taVZFb.js +6 -0
- writer/static/assets/sparql-LA0C7mUc.js +6 -0
- writer/static/assets/sql-C3-3IcFM.js +6 -0
- writer/static/assets/st-C4g7059C.js +6 -0
- writer/static/assets/swift-DNI1vH3h.js +8 -0
- writer/static/assets/systemverilog-DL_FVbcQ.js +6 -0
- writer/static/assets/tcl-DVJXmIwd.js +6 -0
- writer/static/assets/ts.worker-CwG1rUES.js +37021 -0
- writer/static/assets/tsMode-BNUEZzir.js +16 -0
- writer/static/assets/twig-BVWDLtw5.js +6 -0
- writer/static/assets/typescript-CRVt7Hx0.js +6 -0
- writer/static/assets/useBlueprintRun-C00bCxh-.js +1 -0
- writer/static/assets/useKeyValueEditor-nDmI7cTJ.js +1 -0
- writer/static/assets/useListResources-DLkZhRSJ.js +1 -0
- writer/static/assets/vb-Btz91-7U.js +6 -0
- writer/static/assets/vega-embed.module-SNP5iNdJ.js +201 -0
- writer/static/assets/wgsl-D8V_buCG.js +303 -0
- writer/static/assets/xml-C_6-t1tb.js +6 -0
- writer/static/assets/yaml-DIw8G7jk.js +6 -0
- writer/static/components/annotatedtext.svg +4 -0
- writer/static/components/avatar.svg +4 -0
- writer/static/components/blueprints_addtostatelist.svg +4 -0
- writer/static/components/blueprints_apitrigger.svg +4 -0
- writer/static/components/blueprints_calleventhandler.svg +9 -0
- writer/static/components/blueprints_category_Logic.svg +4 -0
- writer/static/components/blueprints_category_Other.svg +4 -0
- writer/static/components/blueprints_category_Triggers.svg +4 -0
- writer/static/components/blueprints_category_Writer.svg +25 -0
- writer/static/components/blueprints_code.svg +9 -0
- writer/static/components/blueprints_crontrigger.svg +6 -0
- writer/static/components/blueprints_foreach.svg +4 -0
- writer/static/components/blueprints_httprequest.svg +11 -0
- writer/static/components/blueprints_logmessage.svg +11 -0
- writer/static/components/blueprints_parsejson.svg +4 -0
- writer/static/components/blueprints_returnvalue.svg +4 -0
- writer/static/components/blueprints_runblueprint.svg +4 -0
- writer/static/components/blueprints_setstate.svg +4 -0
- writer/static/components/blueprints_uieventtrigger.svg +4 -0
- writer/static/components/blueprints_writeraddchatmessage.svg +19 -0
- writer/static/components/blueprints_writeraddtokg.svg +19 -0
- writer/static/components/blueprints_writerchat.svg +11 -0
- writer/static/components/blueprints_writerchatreply.svg +19 -0
- writer/static/components/blueprints_writerclassification.svg +24 -0
- writer/static/components/blueprints_writercompletion.svg +14 -0
- writer/static/components/blueprints_writerinitchat.svg +11 -0
- writer/static/components/blueprints_writernocodeapp.svg +14 -0
- writer/static/components/button.svg +4 -0
- writer/static/components/category_Content.svg +4 -0
- writer/static/components/category_Embed.svg +4 -0
- writer/static/components/category_Input.svg +5 -0
- writer/static/components/category_Layout.svg +9 -0
- writer/static/components/category_Other.svg +4 -0
- writer/static/components/chatbot.svg +4 -0
- writer/static/components/checkboxinput.svg +4 -0
- writer/static/components/colorinput.svg +11 -0
- writer/static/components/column.svg +4 -0
- writer/static/components/columns.svg +4 -0
- writer/static/components/dataframe.svg +4 -0
- writer/static/components/dateinput.svg +4 -0
- writer/static/components/dropdowninput.svg +5 -0
- writer/static/components/fileinput.svg +4 -0
- writer/static/components/googlemaps.svg +4 -0
- writer/static/components/header.svg +4 -0
- writer/static/components/heading.svg +9 -0
- writer/static/components/horizontalstack.svg +4 -0
- writer/static/components/html.svg +9 -0
- writer/static/components/icon.svg +4 -0
- writer/static/components/iframe.svg +4 -0
- writer/static/components/image.svg +11 -0
- writer/static/components/jsonviewer.svg +4 -0
- writer/static/components/link.svg +12 -0
- writer/static/components/mapbox.svg +4 -0
- writer/static/components/message.svg +4 -0
- writer/static/components/metric.svg +4 -0
- writer/static/components/multiselectinput.svg +4 -0
- writer/static/components/numberinput.svg +4 -0
- writer/static/components/page.svg +50 -0
- writer/static/components/pagination.svg +4 -0
- writer/static/components/pdf.svg +4 -0
- writer/static/components/plotlygraph.svg +7 -0
- writer/static/components/progressbar.svg +5 -0
- writer/static/components/radioinput.svg +4 -0
- writer/static/components/rangeinput.svg +4 -0
- writer/static/components/ratinginput.svg +4 -0
- writer/static/components/repeater.svg +4 -0
- writer/static/components/reuse.svg +4 -0
- writer/static/components/section.svg +4 -0
- writer/static/components/selectinput.svg +5 -0
- writer/static/components/separator.svg +4 -0
- writer/static/components/sidebar.svg +4 -0
- writer/static/components/sliderinput.svg +4 -0
- writer/static/components/step.svg +4 -0
- writer/static/components/steps.svg +4 -0
- writer/static/components/switchinput.svg +4 -0
- writer/static/components/tab.svg +4 -0
- writer/static/components/tabs.svg +4 -0
- writer/static/components/tags.svg +11 -0
- writer/static/components/text.svg +4 -0
- writer/static/components/textareainput.svg +11 -0
- writer/static/components/textinput.svg +4 -0
- writer/static/components/timeinput.svg +4 -0
- writer/static/components/timer.svg +4 -0
- writer/static/components/vegalitechart.svg +7 -0
- writer/static/components/videoplayer.svg +11 -0
- writer/static/components/webcamcapture.svg +4 -0
- writer/static/favicon.png +0 -0
- writer/static/index.html +84 -0
- writer/static/status/cancelled.svg +5 -0
- writer/static/status/error.svg +5 -0
- writer/static/status/skipped.svg +4 -0
- writer/static/status/stopped.svg +4 -0
- writer/static/status/success.svg +4 -0
- writer/sync.py +431 -0
- writer/ui.py +2268 -0
- writer/vault.py +48 -0
- writer/wf_project.py +90 -66
- writer-1.25.1rc1.dist-info/METADATA +92 -0
- writer-1.25.1rc1.dist-info/RECORD +382 -0
- {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/WHEEL +1 -1
- writer/blocks/runworkflow.py +0 -59
- writer/workflows.py +0 -183
- writer-0.8.3rc4.dist-info/METADATA +0 -117
- writer-0.8.3rc4.dist-info/RECORD +0 -44
- {writer-0.8.3rc4.dist-info → writer-1.25.1rc1.dist-info}/entry_points.txt +0 -0
- {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
|
-
|
|
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__(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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.
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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",
|
|
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",
|
|
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(
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
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 =
|
|
492
|
-
|
|
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__(
|
|
510
|
-
|
|
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__(
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
624
|
-
self.
|
|
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
|
|
637
|
-
Should be performed from the event loop which will consume the
|
|
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.
|
|
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("
|
|
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
|
|
651
|
-
|
|
652
|
-
self.observer.schedule(
|
|
653
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
922
|
+
if session_id != response_session_id:
|
|
725
923
|
raise PermissionError("Session mismatch.")
|
|
726
|
-
if
|
|
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
|
-
|
|
737
|
-
|
|
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 ==
|
|
948
|
+
if from_path == "main.py":
|
|
743
949
|
raise PermissionError("cannot rename main script")
|
|
744
|
-
if to_path ==
|
|
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
|
-
|
|
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 ==
|
|
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
|
|
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(
|
|
988
|
+
logger = logging.getLogger("writer")
|
|
777
989
|
try:
|
|
778
990
|
contents = None
|
|
779
|
-
with open(path, "r", encoding=
|
|
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
|
-
|
|
791
|
-
|
|
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(
|
|
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(
|
|
800
|
-
|
|
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(
|
|
812
|
-
type="checkSession",
|
|
813
|
-
|
|
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(
|
|
820
|
-
type="sessionInit",
|
|
821
|
-
|
|
822
|
-
))
|
|
1051
|
+
return await self.dispatch_message(
|
|
1052
|
+
"anonymous", InitSessionRequest(type="sessionInit", payload=payload)
|
|
1053
|
+
)
|
|
823
1054
|
|
|
824
|
-
async def update_components(
|
|
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(
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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] = [
|
|
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(
|
|
924
|
-
|
|
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
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
|