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/core.py
CHANGED
|
@@ -46,31 +46,38 @@ import writer.blocks
|
|
|
46
46
|
import writer.evaluator
|
|
47
47
|
from writer import core_ui
|
|
48
48
|
from writer.core_ui import Component
|
|
49
|
+
from writer.logs import use_stdout_redirect
|
|
49
50
|
from writer.ss_types import (
|
|
51
|
+
BlueprintExecutionError,
|
|
52
|
+
BlueprintExecutionLog,
|
|
50
53
|
InstancePath,
|
|
51
54
|
Readable,
|
|
52
55
|
ServeMode,
|
|
53
|
-
WorkflowExecutionLog,
|
|
54
56
|
WriterEvent,
|
|
55
57
|
WriterEventResult,
|
|
56
58
|
WriterFileItem,
|
|
57
59
|
)
|
|
60
|
+
from writer.vault import writer_vault
|
|
58
61
|
|
|
59
62
|
if TYPE_CHECKING:
|
|
60
63
|
import pandas
|
|
61
64
|
|
|
62
65
|
from writer.app_runner import AppProcess
|
|
66
|
+
from writer.blueprints import BlueprintRunner
|
|
63
67
|
from writer.ss_types import AppProcessServerRequest
|
|
64
68
|
|
|
69
|
+
|
|
65
70
|
@dataclasses.dataclass
|
|
66
71
|
class CurrentRequest:
|
|
67
72
|
session_id: str
|
|
68
|
-
request:
|
|
73
|
+
request: "AppProcessServerRequest"
|
|
74
|
+
|
|
69
75
|
|
|
70
76
|
_current_request: ContextVar[Optional[CurrentRequest]] = ContextVar("current_request", default=None)
|
|
71
77
|
|
|
78
|
+
|
|
72
79
|
@contextlib.contextmanager
|
|
73
|
-
def use_request_context(session_id: str, request:
|
|
80
|
+
def use_request_context(session_id: str, request: "AppProcessServerRequest"):
|
|
74
81
|
"""
|
|
75
82
|
Context manager to set the current request context.
|
|
76
83
|
|
|
@@ -85,7 +92,8 @@ def use_request_context(session_id: str, request: 'AppProcessServerRequest'):
|
|
|
85
92
|
finally:
|
|
86
93
|
_current_request.set(None)
|
|
87
94
|
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
def get_app_process() -> "AppProcess":
|
|
89
97
|
"""
|
|
90
98
|
Retrieves the Writer Framework process context.
|
|
91
99
|
|
|
@@ -93,11 +101,12 @@ def get_app_process() -> 'AppProcess':
|
|
|
93
101
|
>>> _current_process.bmc_components # get the component tree
|
|
94
102
|
"""
|
|
95
103
|
from writer.app_runner import AppProcess # Needed during runtime
|
|
104
|
+
|
|
96
105
|
raw_process: BaseProcess = multiprocessing.current_process()
|
|
97
106
|
if isinstance(raw_process, AppProcess):
|
|
98
107
|
return raw_process
|
|
99
108
|
|
|
100
|
-
raise RuntimeError(
|
|
109
|
+
raise RuntimeError("Failed to retrieve the AppProcess: running in wrong context")
|
|
101
110
|
|
|
102
111
|
|
|
103
112
|
def import_failure(rvalue: Any = None):
|
|
@@ -113,6 +122,7 @@ def import_failure(rvalue: Any = None):
|
|
|
113
122
|
|
|
114
123
|
:param rvalue: the value to return
|
|
115
124
|
"""
|
|
125
|
+
|
|
116
126
|
def decorator(func):
|
|
117
127
|
@functools.wraps(func)
|
|
118
128
|
def wrapper(*args, **kwargs):
|
|
@@ -120,12 +130,13 @@ def import_failure(rvalue: Any = None):
|
|
|
120
130
|
return func(*args, **kwargs)
|
|
121
131
|
except ImportError:
|
|
122
132
|
return rvalue
|
|
133
|
+
|
|
123
134
|
return wrapper
|
|
135
|
+
|
|
124
136
|
return decorator
|
|
125
137
|
|
|
126
138
|
|
|
127
139
|
class Config:
|
|
128
|
-
|
|
129
140
|
is_mail_enabled_for_log: bool = False
|
|
130
141
|
mode: ServeMode = "run"
|
|
131
142
|
logger: Optional[logging.Logger] = None
|
|
@@ -133,12 +144,13 @@ class Config:
|
|
|
133
144
|
|
|
134
145
|
|
|
135
146
|
class WriterSession:
|
|
136
|
-
|
|
137
147
|
"""
|
|
138
148
|
Represents a session.
|
|
139
149
|
"""
|
|
140
150
|
|
|
141
|
-
def __init__(
|
|
151
|
+
def __init__(
|
|
152
|
+
self, session_id: str, cookies: Optional[Dict[str, str]], headers: Optional[Dict[str, str]]
|
|
153
|
+
) -> None:
|
|
142
154
|
self.session_id = session_id
|
|
143
155
|
self.cookies = cookies
|
|
144
156
|
self.headers = headers
|
|
@@ -149,6 +161,7 @@ class WriterSession:
|
|
|
149
161
|
self.session_component_tree = core_ui.build_session_component_tree(base_component_tree)
|
|
150
162
|
self.event_handler = EventHandler(self)
|
|
151
163
|
self.userinfo: Optional[dict] = None
|
|
164
|
+
self.queued_messages: List[Any] = []
|
|
152
165
|
|
|
153
166
|
def update_last_active_timestamp(self) -> None:
|
|
154
167
|
self.last_active_timestamp = int(time.time())
|
|
@@ -167,16 +180,17 @@ class MutationSubscription:
|
|
|
167
180
|
|
|
168
181
|
>>> m = MutationSubscription(path="a.c", handler=myhandler)
|
|
169
182
|
"""
|
|
170
|
-
|
|
183
|
+
|
|
184
|
+
type: Literal["subscription", "property"]
|
|
171
185
|
path: str
|
|
172
|
-
handler: Callable
|
|
173
|
-
state:
|
|
186
|
+
handler: Callable # Handler to execute when mutation happens
|
|
187
|
+
state: "State"
|
|
174
188
|
property_name: Optional[str] = None
|
|
175
189
|
|
|
176
190
|
def __post_init__(self):
|
|
177
191
|
if len(self.path) == 0:
|
|
178
192
|
raise ValueError("path cannot be empty.")
|
|
179
|
-
|
|
193
|
+
|
|
180
194
|
path_parts = parse_state_variable_expression(self.path)
|
|
181
195
|
for part in path_parts:
|
|
182
196
|
if len(part) == 0:
|
|
@@ -194,14 +208,17 @@ class MutationSubscription:
|
|
|
194
208
|
path_parts = parse_state_variable_expression(self.path)
|
|
195
209
|
return path_parts[-1]
|
|
196
210
|
|
|
197
|
-
|
|
211
|
+
|
|
212
|
+
class StateRecursionWatcher:
|
|
198
213
|
limit = 128
|
|
199
214
|
|
|
200
215
|
def __init__(self):
|
|
201
216
|
self.counter_recursion = 0
|
|
202
217
|
|
|
218
|
+
|
|
203
219
|
_state_recursion_watcher = ContextVar("state_recursion_watcher", default=StateRecursionWatcher())
|
|
204
220
|
|
|
221
|
+
|
|
205
222
|
@contextlib.contextmanager
|
|
206
223
|
def state_recursion_new(key: str):
|
|
207
224
|
"""
|
|
@@ -219,6 +236,7 @@ def state_recursion_new(key: str):
|
|
|
219
236
|
finally:
|
|
220
237
|
recursion_watcher.counter_recursion -= 1
|
|
221
238
|
|
|
239
|
+
|
|
222
240
|
class FileWrapper:
|
|
223
241
|
"""
|
|
224
242
|
A wrapper for either a string pointing to a file or a file-like object with a read() method.
|
|
@@ -229,17 +247,18 @@ class FileWrapper:
|
|
|
229
247
|
def __init__(self, file: Union[Readable, str], mime_type: Optional[str] = None):
|
|
230
248
|
if not file:
|
|
231
249
|
raise ValueError("Must specify a file.")
|
|
232
|
-
if not (
|
|
233
|
-
callable(getattr(file, "read", None)) or
|
|
234
|
-
isinstance(file, str)):
|
|
250
|
+
if not (callable(getattr(file, "read", None)) or isinstance(file, str)):
|
|
235
251
|
raise ValueError(
|
|
236
|
-
"File must provide a read() method or contain a string with a path to a local file."
|
|
252
|
+
"File must provide a read() method or contain a string with a path to a local file."
|
|
253
|
+
)
|
|
237
254
|
self.file = file
|
|
238
255
|
self.mime_type = mime_type
|
|
239
256
|
|
|
240
257
|
def _get_file_stream_as_dataurl(self, f_stream: Readable) -> str:
|
|
241
258
|
base64_str = base64.b64encode(f_stream.read()).decode("latin-1")
|
|
242
|
-
dataurl =
|
|
259
|
+
dataurl = (
|
|
260
|
+
f"data:{self.mime_type if self.mime_type is not None else ''};base64,{ base64_str }"
|
|
261
|
+
)
|
|
243
262
|
return dataurl
|
|
244
263
|
|
|
245
264
|
def get_as_dataurl(self) -> str:
|
|
@@ -253,7 +272,6 @@ class FileWrapper:
|
|
|
253
272
|
|
|
254
273
|
|
|
255
274
|
class BytesWrapper:
|
|
256
|
-
|
|
257
275
|
"""
|
|
258
276
|
A wrapper for raw byte data.
|
|
259
277
|
Provides a method for retrieving the data as data URL.
|
|
@@ -279,6 +297,7 @@ class StateSerialiser:
|
|
|
279
297
|
Serialises user state values before sending them to the front end.
|
|
280
298
|
Provides JSON-compatible values, including data URLs for binary data.
|
|
281
299
|
"""
|
|
300
|
+
|
|
282
301
|
def serialise(self, v: Any) -> Union[Dict, List, str, bool, int, float, None]:
|
|
283
302
|
from writer.ai import Conversation
|
|
284
303
|
from writer.core_df import EditableDataFrame
|
|
@@ -308,8 +327,7 @@ class StateSerialiser:
|
|
|
308
327
|
# Checking the MRO allows to determine object type without creating dependencies
|
|
309
328
|
# to these packages
|
|
310
329
|
|
|
311
|
-
v_mro = [
|
|
312
|
-
f"{x.__module__}.{x.__name__}" for x in inspect.getmro(type(v))]
|
|
330
|
+
v_mro = [f"{x.__module__}.{x.__name__}" for x in inspect.getmro(type(v))]
|
|
313
331
|
|
|
314
332
|
if isinstance(v, (int, float)):
|
|
315
333
|
if "numpy.float64" in v_mro:
|
|
@@ -339,7 +357,8 @@ class StateSerialiser:
|
|
|
339
357
|
return self._serialise_dict_recursively(v.to_dict())
|
|
340
358
|
|
|
341
359
|
raise StateSerialiserException(
|
|
342
|
-
f"Object of type { type(v) } (MRO: {v_mro}) cannot be serialised."
|
|
360
|
+
f"Object of type { type(v) } (MRO: {v_mro}) cannot be serialised."
|
|
361
|
+
)
|
|
343
362
|
|
|
344
363
|
def _serialise_dict_recursively(self, d: Dict) -> Dict:
|
|
345
364
|
return {str(k): self.serialise(v) for k, v in d.items()}
|
|
@@ -371,11 +390,13 @@ class StateSerialiser:
|
|
|
371
390
|
:return: a arrow file as a dataurl (application/vnd.apache.arrow.file)
|
|
372
391
|
"""
|
|
373
392
|
import pyarrow.interchange # type: ignore
|
|
393
|
+
|
|
374
394
|
table = pyarrow.interchange.from_dataframe(df)
|
|
375
395
|
return self._serialise_pyarrow_table(table)
|
|
376
396
|
|
|
377
397
|
def _serialise_pandas_dataframe(self, df):
|
|
378
398
|
import pyarrow as pa # type: ignore
|
|
399
|
+
|
|
379
400
|
pa_table = pa.Table.from_pandas(df, preserve_index=True)
|
|
380
401
|
return self._serialise_pyarrow_table(pa_table)
|
|
381
402
|
|
|
@@ -391,6 +412,7 @@ class StateSerialiser:
|
|
|
391
412
|
bw = BytesWrapper(buf, "application/vnd.apache.arrow.file")
|
|
392
413
|
return self.serialise(bw)
|
|
393
414
|
|
|
415
|
+
|
|
394
416
|
class MutableValue:
|
|
395
417
|
"""
|
|
396
418
|
MutableValue allows you to implement a value whose modification
|
|
@@ -405,6 +427,7 @@ class MutableValue:
|
|
|
405
427
|
>>> self.value = new_value
|
|
406
428
|
>>> self.mutate()
|
|
407
429
|
"""
|
|
430
|
+
|
|
408
431
|
def __init__(self):
|
|
409
432
|
self._mutated = False
|
|
410
433
|
|
|
@@ -430,8 +453,8 @@ class MutableValue:
|
|
|
430
453
|
"""
|
|
431
454
|
self._mutated = False
|
|
432
455
|
|
|
433
|
-
class StateProxy:
|
|
434
456
|
|
|
457
|
+
class StateProxy:
|
|
435
458
|
"""
|
|
436
459
|
The root user state and its children (nested states) are instances of this class.
|
|
437
460
|
Provides proxy functionality to detect state mutations via assignment.
|
|
@@ -466,33 +489,31 @@ class StateProxy:
|
|
|
466
489
|
def __setitem__(self, key: str, raw_value: Any) -> None:
|
|
467
490
|
with state_recursion_new(key):
|
|
468
491
|
if not isinstance(key, str):
|
|
469
|
-
raise ValueError(
|
|
470
|
-
f"State keys must be strings. Received {str(key)} ({type(key)}).")
|
|
492
|
+
raise ValueError(f"State keys must be strings. Received {str(key)} ({type(key)}).")
|
|
471
493
|
old_value = self.state.get(key)
|
|
472
494
|
self.state[key] = raw_value
|
|
473
495
|
|
|
474
496
|
for local_mutation in self.local_mutation_subscriptions:
|
|
475
497
|
if local_mutation.local_path == key:
|
|
476
|
-
if local_mutation.type ==
|
|
477
|
-
context_data = {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"ui": _event_handler_ui_manager()
|
|
492
|
-
})
|
|
493
|
-
elif local_mutation.type == 'property':
|
|
498
|
+
if local_mutation.type == "subscription":
|
|
499
|
+
context_data = {"event": "mutation", "mutation": local_mutation.path}
|
|
500
|
+
payload = {"previous_value": old_value, "new_value": raw_value}
|
|
501
|
+
|
|
502
|
+
EventHandlerExecutor.invoke(
|
|
503
|
+
local_mutation.handler,
|
|
504
|
+
{
|
|
505
|
+
"state": local_mutation.state,
|
|
506
|
+
"context": context_data,
|
|
507
|
+
"payload": payload,
|
|
508
|
+
"session": _event_handler_session_info(),
|
|
509
|
+
"ui": _event_handler_ui_manager(),
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
elif local_mutation.type == "property":
|
|
494
513
|
assert local_mutation.property_name is not None
|
|
495
|
-
self[local_mutation.property_name] = local_mutation.handler(
|
|
514
|
+
self[local_mutation.property_name] = local_mutation.handler(
|
|
515
|
+
local_mutation.state
|
|
516
|
+
)
|
|
496
517
|
|
|
497
518
|
self._apply_raw(f"+{key}")
|
|
498
519
|
|
|
@@ -544,7 +565,7 @@ class StateProxy:
|
|
|
544
565
|
for key, value in list(self.state.items()):
|
|
545
566
|
if key.startswith("_"):
|
|
546
567
|
continue
|
|
547
|
-
|
|
568
|
+
|
|
548
569
|
escaped_key = self.escape_key(key)
|
|
549
570
|
serialised_value = None
|
|
550
571
|
|
|
@@ -562,20 +583,21 @@ class StateProxy:
|
|
|
562
583
|
try:
|
|
563
584
|
serialised_value = state_serialiser.serialise(value)
|
|
564
585
|
except BaseException:
|
|
565
|
-
raise ValueError(
|
|
586
|
+
raise ValueError(
|
|
587
|
+
f"""Couldn't serialise value of type "{ type(value) }" for key "{ key }"."""
|
|
588
|
+
)
|
|
566
589
|
serialised_mutations[f"+{escaped_key}"] = serialised_value
|
|
567
590
|
elif isinstance(value, MutableValue) is True and value.mutated():
|
|
568
591
|
try:
|
|
569
592
|
serialised_value = state_serialiser.serialise(value)
|
|
570
593
|
value.reset_mutation()
|
|
571
594
|
except BaseException:
|
|
572
|
-
raise ValueError(
|
|
595
|
+
raise ValueError(
|
|
596
|
+
f"""Couldn't serialise value of type "{ type(value) }" for key "{ key }"."""
|
|
597
|
+
)
|
|
573
598
|
serialised_mutations[f"+{escaped_key}"] = serialised_value
|
|
574
599
|
|
|
575
|
-
deleted_keys =
|
|
576
|
-
{self.escape_key(key)
|
|
577
|
-
for key in self.mutated
|
|
578
|
-
if key.startswith("-")}
|
|
600
|
+
deleted_keys = {self.escape_key(key) for key in self.mutated if key.startswith("-")}
|
|
579
601
|
for key in deleted_keys:
|
|
580
602
|
serialised_mutations[f"{key}"] = None
|
|
581
603
|
|
|
@@ -592,7 +614,8 @@ class StateProxy:
|
|
|
592
614
|
serialised_value = state_serialiser.serialise(value)
|
|
593
615
|
except BaseException:
|
|
594
616
|
raise ValueError(
|
|
595
|
-
f"""Couldn't serialise value of type "{ type(value) }" for key "{ key }"."""
|
|
617
|
+
f"""Couldn't serialise value of type "{ type(value) }" for key "{ key }"."""
|
|
618
|
+
)
|
|
596
619
|
serialised[key] = serialised_value
|
|
597
620
|
return serialised
|
|
598
621
|
|
|
@@ -620,14 +643,15 @@ def get_annotations(instance) -> Dict[str, Any]:
|
|
|
620
643
|
Returns the annotations of the class in a way that works on python 3.9 and python 3.10
|
|
621
644
|
"""
|
|
622
645
|
if isinstance(instance, type):
|
|
623
|
-
ann = instance.__dict__.get(
|
|
646
|
+
ann = instance.__dict__.get("__annotations__", None)
|
|
624
647
|
else:
|
|
625
|
-
ann = getattr(instance,
|
|
648
|
+
ann = getattr(instance, "__annotations__", None)
|
|
626
649
|
|
|
627
650
|
if ann is None:
|
|
628
651
|
ann = {}
|
|
629
652
|
return ann
|
|
630
653
|
|
|
654
|
+
|
|
631
655
|
class StateMeta(type):
|
|
632
656
|
"""
|
|
633
657
|
Constructs a class at runtime that extends WriterState or State
|
|
@@ -672,15 +696,16 @@ class StateMeta(type):
|
|
|
672
696
|
annotations = get_annotations(klass)
|
|
673
697
|
for key, expected_type in annotations.items():
|
|
674
698
|
if key == "_state_proxy":
|
|
675
|
-
raise AttributeError(
|
|
699
|
+
raise AttributeError(
|
|
700
|
+
"_state_proxy is an reserved keyword for Writer Framework, don't use it in annotation."
|
|
701
|
+
)
|
|
676
702
|
|
|
677
|
-
if not(inspect.isclass(expected_type) and issubclass(expected_type, State)):
|
|
703
|
+
if not (inspect.isclass(expected_type) and issubclass(expected_type, State)):
|
|
678
704
|
proxy = DictPropertyProxy("_state_proxy", key)
|
|
679
705
|
setattr(klass, key, proxy)
|
|
680
706
|
|
|
681
707
|
|
|
682
708
|
class State(metaclass=StateMeta):
|
|
683
|
-
|
|
684
709
|
def __init__(self, raw_state: Optional[Dict[str, Any]] = None):
|
|
685
710
|
final_raw_state = raw_state if raw_state is not None else {}
|
|
686
711
|
|
|
@@ -703,7 +728,9 @@ class State(metaclass=StateMeta):
|
|
|
703
728
|
"""
|
|
704
729
|
self._state_proxy.state = {}
|
|
705
730
|
for key, value in raw_state.items():
|
|
706
|
-
assert not isinstance(
|
|
731
|
+
assert not isinstance(
|
|
732
|
+
value, StateProxy
|
|
733
|
+
), f"state proxy datatype is not expected in ingest operation, {locals()}"
|
|
707
734
|
self._set_state_item(key, value)
|
|
708
735
|
|
|
709
736
|
def to_dict(self) -> dict:
|
|
@@ -717,7 +744,6 @@ class State(metaclass=StateMeta):
|
|
|
717
744
|
"""
|
|
718
745
|
return self._state_proxy.to_dict()
|
|
719
746
|
|
|
720
|
-
|
|
721
747
|
def to_raw_state(self) -> dict:
|
|
722
748
|
"""
|
|
723
749
|
Converts a StateProxy and its children into a python dictionary that can be used to recreate the
|
|
@@ -735,7 +761,6 @@ class State(metaclass=StateMeta):
|
|
|
735
761
|
return self._state_proxy.__repr__()
|
|
736
762
|
|
|
737
763
|
def __getitem__(self, key: str) -> Any:
|
|
738
|
-
|
|
739
764
|
# Essential to support operation like
|
|
740
765
|
# state['item']['a'] = state['item']['b']
|
|
741
766
|
if hasattr(self, key):
|
|
@@ -746,7 +771,9 @@ class State(metaclass=StateMeta):
|
|
|
746
771
|
return self._state_proxy.__getitem__(key)
|
|
747
772
|
|
|
748
773
|
def __setitem__(self, key: str, raw_value: Any) -> None:
|
|
749
|
-
assert not isinstance(
|
|
774
|
+
assert not isinstance(
|
|
775
|
+
raw_value, StateProxy
|
|
776
|
+
), f"state proxy datatype is not expected, {locals()}"
|
|
750
777
|
|
|
751
778
|
self._set_state_item(key, raw_value)
|
|
752
779
|
|
|
@@ -769,15 +796,14 @@ class State(metaclass=StateMeta):
|
|
|
769
796
|
return self._state_proxy.__contains__(key)
|
|
770
797
|
|
|
771
798
|
def _set_state_item(self, key: str, value: Any):
|
|
772
|
-
"""
|
|
773
|
-
"""
|
|
799
|
+
""" """
|
|
774
800
|
|
|
775
801
|
"""
|
|
776
802
|
At this level, the values that arrive are either States which encapsulate a StateProxy, or another datatype.
|
|
777
803
|
If there is a StateProxy, it is a fault in the code.
|
|
778
804
|
"""
|
|
779
805
|
annotations = get_annotations(self)
|
|
780
|
-
expected_type = annotations.get(key, None)
|
|
806
|
+
expected_type = cast(Type, annotations.get(key, None))
|
|
781
807
|
expect_dict = _type_match_dict(expected_type)
|
|
782
808
|
if isinstance(value, dict) and not expect_dict:
|
|
783
809
|
"""
|
|
@@ -786,7 +812,9 @@ class State(metaclass=StateMeta):
|
|
|
786
812
|
"""
|
|
787
813
|
state = annotations[key](value) if key in annotations else State()
|
|
788
814
|
if not isinstance(state, State):
|
|
789
|
-
raise ValueError(
|
|
815
|
+
raise ValueError(
|
|
816
|
+
f"Attribute {key} must inherit of State or requires a dict to accept dictionary"
|
|
817
|
+
)
|
|
790
818
|
|
|
791
819
|
setattr(self, key, state)
|
|
792
820
|
state.ingest(value)
|
|
@@ -798,11 +826,12 @@ class State(metaclass=StateMeta):
|
|
|
798
826
|
else:
|
|
799
827
|
self._state_proxy[key] = value
|
|
800
828
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
829
|
+
def subscribe_mutation(
|
|
830
|
+
self,
|
|
831
|
+
path: Union[str, List[str]],
|
|
832
|
+
handler: Callable[..., Union[None, Awaitable[None]]],
|
|
833
|
+
initial_triggered: bool = False,
|
|
834
|
+
) -> None:
|
|
806
835
|
r"""
|
|
807
836
|
Automatically triggers a handler when a mutation occurs in the state.
|
|
808
837
|
|
|
@@ -847,30 +876,35 @@ class State(metaclass=StateMeta):
|
|
|
847
876
|
path_parts = parse_state_variable_expression(p)
|
|
848
877
|
for i, path_part in enumerate(path_parts):
|
|
849
878
|
if i == len(path_parts) - 1:
|
|
850
|
-
local_mutation = MutationSubscription(
|
|
879
|
+
local_mutation = MutationSubscription("subscription", p, handler, self)
|
|
851
880
|
state_proxy.local_mutation_subscriptions.append(local_mutation)
|
|
852
881
|
|
|
853
882
|
# At startup, the application must be informed of the
|
|
854
883
|
# existing states. To cause this, we trigger manually
|
|
855
884
|
# the handler.
|
|
856
885
|
if initial_triggered is True:
|
|
857
|
-
EventHandlerExecutor.invoke(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
886
|
+
EventHandlerExecutor.invoke(
|
|
887
|
+
handler,
|
|
888
|
+
{
|
|
889
|
+
"state": self,
|
|
890
|
+
"context": {"event": "init"},
|
|
891
|
+
"payload": {},
|
|
892
|
+
"session": {},
|
|
893
|
+
"ui": _event_handler_ui_manager(),
|
|
894
|
+
},
|
|
895
|
+
)
|
|
864
896
|
|
|
865
897
|
elif path_part in state_proxy:
|
|
866
898
|
state_proxy = state_proxy[path_part]
|
|
867
899
|
else:
|
|
868
900
|
raise ValueError(f"Mutation subscription failed - {p} not found in state")
|
|
869
901
|
|
|
870
|
-
def calculated_property(
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
902
|
+
def calculated_property(
|
|
903
|
+
self,
|
|
904
|
+
property_name: str,
|
|
905
|
+
path: Union[str, List[str]],
|
|
906
|
+
handler: Callable[..., Union[None, Awaitable[None]]],
|
|
907
|
+
) -> None:
|
|
874
908
|
"""
|
|
875
909
|
Update a calculated property when a mutation triggers
|
|
876
910
|
|
|
@@ -900,7 +934,9 @@ class State(metaclass=StateMeta):
|
|
|
900
934
|
path_parts = parse_state_variable_expression(p)
|
|
901
935
|
for i, path_part in enumerate(path_parts):
|
|
902
936
|
if i == len(path_parts) - 1:
|
|
903
|
-
local_mutation = MutationSubscription(
|
|
937
|
+
local_mutation = MutationSubscription(
|
|
938
|
+
"property", p, handler, self, property_name
|
|
939
|
+
)
|
|
904
940
|
state_proxy.local_mutation_subscriptions.append(local_mutation)
|
|
905
941
|
state_proxy[property_name] = handler(self)
|
|
906
942
|
elif path_part in state_proxy:
|
|
@@ -927,11 +963,11 @@ class WriterState(State):
|
|
|
927
963
|
|
|
928
964
|
@classmethod
|
|
929
965
|
def get_new(cls):
|
|
930
|
-
"""
|
|
966
|
+
"""Returns a new WriterState instance set to the initial state."""
|
|
931
967
|
|
|
932
968
|
return initial_state.get_clone()
|
|
933
969
|
|
|
934
|
-
def get_clone(self) ->
|
|
970
|
+
def get_clone(self) -> "WriterState":
|
|
935
971
|
"""
|
|
936
972
|
get_clone clones the destination application state for the session.
|
|
937
973
|
|
|
@@ -949,10 +985,12 @@ class WriterState(State):
|
|
|
949
985
|
cloned_mail = copy.deepcopy(self.mail)
|
|
950
986
|
except BaseException:
|
|
951
987
|
substitute_state = WriterState()
|
|
952
|
-
substitute_state.add_log_entry(
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
988
|
+
substitute_state.add_log_entry(
|
|
989
|
+
"error",
|
|
990
|
+
"Cannot clone state",
|
|
991
|
+
"The state may contain unpickable objects, such as modules.",
|
|
992
|
+
traceback.format_exc(),
|
|
993
|
+
)
|
|
956
994
|
return substitute_state
|
|
957
995
|
|
|
958
996
|
cloned_state = self.__class__(cloned_user_state, cloned_mail)
|
|
@@ -960,20 +998,29 @@ class WriterState(State):
|
|
|
960
998
|
return cloned_state
|
|
961
999
|
|
|
962
1000
|
def add_mail(self, type: str, payload: Any) -> None:
|
|
963
|
-
mail_item = {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
self
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1001
|
+
mail_item = {"type": type, "payload": payload}
|
|
1002
|
+
self.mail.append(mail_item)
|
|
1003
|
+
|
|
1004
|
+
def add_notification(
|
|
1005
|
+
self, type: Literal["info", "success", "warning", "error"], title: str, message: str
|
|
1006
|
+
) -> None:
|
|
1007
|
+
self.add_mail(
|
|
1008
|
+
"notification",
|
|
1009
|
+
{
|
|
1010
|
+
"type": type,
|
|
1011
|
+
"title": title,
|
|
1012
|
+
"message": message,
|
|
1013
|
+
},
|
|
1014
|
+
)
|
|
975
1015
|
|
|
976
|
-
def _log_entry_in_logger(
|
|
1016
|
+
def _log_entry_in_logger(
|
|
1017
|
+
self,
|
|
1018
|
+
type: Literal["debug", "info", "warning", "error", "critical"],
|
|
1019
|
+
title: str,
|
|
1020
|
+
message: str,
|
|
1021
|
+
code: Optional[str] = None,
|
|
1022
|
+
blueprint_execution: Optional[BlueprintExecutionLog] = None,
|
|
1023
|
+
) -> None:
|
|
977
1024
|
if not Config.logger:
|
|
978
1025
|
return
|
|
979
1026
|
log_args: Tuple[str, ...] = ()
|
|
@@ -984,11 +1031,11 @@ class WriterState(State):
|
|
|
984
1031
|
log_args = (title, message)
|
|
985
1032
|
|
|
986
1033
|
log_colors = {
|
|
987
|
-
"debug": "\x1b[36;20m",
|
|
988
|
-
"info": "\x1b[34;20m",
|
|
1034
|
+
"debug": "\x1b[36;20m", # Cyan for debug
|
|
1035
|
+
"info": "\x1b[34;20m", # Blue for info
|
|
989
1036
|
"warning": "\x1b[33;20m", # Yellow for warning
|
|
990
|
-
"error": "\x1b[31;20m",
|
|
991
|
-
"critical": "\x1b[35;20m" # Magenta for critical
|
|
1037
|
+
"error": "\x1b[31;20m", # Red for error
|
|
1038
|
+
"critical": "\x1b[35;20m", # Magenta for critical
|
|
992
1039
|
}
|
|
993
1040
|
|
|
994
1041
|
log_methods = {
|
|
@@ -996,41 +1043,69 @@ class WriterState(State):
|
|
|
996
1043
|
"info": Config.logger.info,
|
|
997
1044
|
"warning": Config.logger.warning,
|
|
998
1045
|
"error": Config.logger.error,
|
|
999
|
-
"critical": Config.logger.critical
|
|
1046
|
+
"critical": Config.logger.critical,
|
|
1000
1047
|
}
|
|
1001
1048
|
|
|
1002
1049
|
log_message = "From app log: " + ("\n%s" * len(log_args))
|
|
1003
1050
|
|
|
1051
|
+
if blueprint_execution:
|
|
1052
|
+
log_message += "\n"
|
|
1053
|
+
for entry in blueprint_execution.summary:
|
|
1054
|
+
outcome = entry.get("outcome")
|
|
1055
|
+
if outcome is None:
|
|
1056
|
+
continue
|
|
1057
|
+
component_id = entry.get("componentId")
|
|
1058
|
+
entry_message = entry.get("message")
|
|
1059
|
+
log_message += f"- Id: {component_id} | Outcome: {outcome} | {entry_message}\n"
|
|
1060
|
+
|
|
1004
1061
|
color = log_colors.get(type, "\x1b[0m") # Default to no color if type not found
|
|
1005
|
-
log_method = log_methods.get(
|
|
1062
|
+
log_method = log_methods.get(
|
|
1063
|
+
type, Config.logger.info
|
|
1064
|
+
) # Default to info level if type not found
|
|
1006
1065
|
|
|
1007
1066
|
log_method(f"{color}{log_message}\x1b[0m", *log_args)
|
|
1008
1067
|
|
|
1009
|
-
def add_log_entry(
|
|
1010
|
-
self
|
|
1068
|
+
def add_log_entry(
|
|
1069
|
+
self,
|
|
1070
|
+
type: Literal["info", "error"],
|
|
1071
|
+
title: str,
|
|
1072
|
+
message: str,
|
|
1073
|
+
code: Optional[str] = None,
|
|
1074
|
+
blueprint_execution: Optional[BlueprintExecutionLog] = None,
|
|
1075
|
+
id: Optional[str] = None,
|
|
1076
|
+
) -> None:
|
|
1077
|
+
self._log_entry_in_logger(type, title, message, code, blueprint_execution)
|
|
1011
1078
|
if not Config.is_mail_enabled_for_log:
|
|
1012
1079
|
return
|
|
1013
1080
|
shortened_message = None
|
|
1014
1081
|
if len(message) > WriterState.LOG_ENTRY_MAX_LEN:
|
|
1015
|
-
shortened_message = message[0:WriterState.LOG_ENTRY_MAX_LEN] + "..."
|
|
1082
|
+
shortened_message = message[0 : WriterState.LOG_ENTRY_MAX_LEN] + "..."
|
|
1016
1083
|
else:
|
|
1017
1084
|
shortened_message = message
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
"
|
|
1024
|
-
|
|
1085
|
+
|
|
1086
|
+
if id is not None:
|
|
1087
|
+
self._remove_duplicates_in_mail(id)
|
|
1088
|
+
|
|
1089
|
+
self.add_mail(
|
|
1090
|
+
"logEntry",
|
|
1091
|
+
{
|
|
1092
|
+
"type": type,
|
|
1093
|
+
"title": title,
|
|
1094
|
+
"message": shortened_message,
|
|
1095
|
+
"code": code,
|
|
1096
|
+
"blueprintExecution": blueprint_execution,
|
|
1097
|
+
"id": id,
|
|
1098
|
+
},
|
|
1099
|
+
)
|
|
1025
1100
|
|
|
1026
1101
|
def file_download(self, data: Any, file_name: str):
|
|
1027
1102
|
if not isinstance(data, (bytes, FileWrapper, BytesWrapper)):
|
|
1028
1103
|
raise ValueError(
|
|
1029
|
-
"Data for a fileDownload mail must be bytes, a FileWrapper or a BytesWrapper."
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
"fileName": file_name
|
|
1033
|
-
|
|
1104
|
+
"Data for a fileDownload mail must be bytes, a FileWrapper or a BytesWrapper."
|
|
1105
|
+
)
|
|
1106
|
+
self.add_mail(
|
|
1107
|
+
"fileDownload", {"data": state_serialiser.serialise(data), "fileName": file_name}
|
|
1108
|
+
)
|
|
1034
1109
|
|
|
1035
1110
|
def open_url(self, url: str):
|
|
1036
1111
|
self.add_mail("openUrl", url)
|
|
@@ -1038,6 +1113,18 @@ class WriterState(State):
|
|
|
1038
1113
|
def clear_mail(self) -> None:
|
|
1039
1114
|
self.mail = []
|
|
1040
1115
|
|
|
1116
|
+
def _remove_duplicates_in_mail(self, id: str) -> None:
|
|
1117
|
+
new_mail = []
|
|
1118
|
+
for entry in self.mail:
|
|
1119
|
+
if entry["type"] != "logEntry":
|
|
1120
|
+
new_mail.append(entry)
|
|
1121
|
+
continue
|
|
1122
|
+
log_id = entry["payload"].get("id")
|
|
1123
|
+
if log_id != id:
|
|
1124
|
+
new_mail.append(entry)
|
|
1125
|
+
continue
|
|
1126
|
+
self.mail = new_mail
|
|
1127
|
+
|
|
1041
1128
|
def set_page(self, active_page_key: str) -> None:
|
|
1042
1129
|
self.add_mail("pageChange", active_page_key)
|
|
1043
1130
|
|
|
@@ -1045,10 +1132,7 @@ class WriterState(State):
|
|
|
1045
1132
|
self.add_mail("routeVarsChange", route_vars)
|
|
1046
1133
|
|
|
1047
1134
|
def import_stylesheet(self, stylesheet_key: str, path: str) -> None:
|
|
1048
|
-
self.add_mail("importStylesheet", {
|
|
1049
|
-
"stylesheetKey": stylesheet_key,
|
|
1050
|
-
"path": path
|
|
1051
|
-
})
|
|
1135
|
+
self.add_mail("importStylesheet", {"stylesheetKey": stylesheet_key, "path": path})
|
|
1052
1136
|
|
|
1053
1137
|
def import_script(self, script_key: str, path: str) -> None:
|
|
1054
1138
|
"""
|
|
@@ -1060,25 +1144,18 @@ class WriterState(State):
|
|
|
1060
1144
|
>>>
|
|
1061
1145
|
>>> initial_state.import_script("my_script", "/static/script.js")
|
|
1062
1146
|
"""
|
|
1063
|
-
self.add_mail("importScript", {
|
|
1064
|
-
"scriptKey": script_key,
|
|
1065
|
-
"path": path
|
|
1066
|
-
})
|
|
1147
|
+
self.add_mail("importScript", {"scriptKey": script_key, "path": path})
|
|
1067
1148
|
|
|
1068
1149
|
def import_frontend_module(self, module_key: str, specifier: str) -> None:
|
|
1069
|
-
self.add_mail("importModule", {
|
|
1070
|
-
"moduleKey": module_key,
|
|
1071
|
-
"specifier": specifier
|
|
1072
|
-
})
|
|
1150
|
+
self.add_mail("importModule", {"moduleKey": module_key, "specifier": specifier})
|
|
1073
1151
|
|
|
1074
1152
|
def call_frontend_function(self, module_key: str, function_name: str, args: List) -> None:
|
|
1075
|
-
self.add_mail(
|
|
1076
|
-
"moduleKey": module_key,
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
})
|
|
1153
|
+
self.add_mail(
|
|
1154
|
+
"functionCall", {"moduleKey": module_key, "functionName": function_name, "args": args}
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1080
1157
|
|
|
1081
|
-
class MiddlewareExecutor
|
|
1158
|
+
class MiddlewareExecutor:
|
|
1082
1159
|
"""
|
|
1083
1160
|
A MiddlewareExecutor executes middleware in a controlled context. It allows writer framework
|
|
1084
1161
|
to manage different implementations of middleware.
|
|
@@ -1114,7 +1191,6 @@ class MiddlewareExecutor():
|
|
|
1114
1191
|
|
|
1115
1192
|
|
|
1116
1193
|
class MiddlewareRegistry:
|
|
1117
|
-
|
|
1118
1194
|
def __init__(self) -> None:
|
|
1119
1195
|
self.registry: List[MiddlewareExecutor] = []
|
|
1120
1196
|
|
|
@@ -1131,6 +1207,7 @@ class MiddlewareRegistry:
|
|
|
1131
1207
|
"""
|
|
1132
1208
|
return self.registry
|
|
1133
1209
|
|
|
1210
|
+
|
|
1134
1211
|
class EventHandlerRegistry:
|
|
1135
1212
|
"""
|
|
1136
1213
|
Maps functions registered as event handlers from the user app's core
|
|
@@ -1143,10 +1220,138 @@ class EventHandlerRegistry:
|
|
|
1143
1220
|
|
|
1144
1221
|
class HandlerEntry(TypedDict):
|
|
1145
1222
|
callable: Callable
|
|
1146
|
-
meta:
|
|
1223
|
+
meta: "EventHandlerRegistry.HandlerMeta"
|
|
1224
|
+
|
|
1225
|
+
# === BLUEPRINT HANLDERS ===
|
|
1226
|
+
@staticmethod
|
|
1227
|
+
def stop_blueprint_run(payload: dict, blueprint_runner: "BlueprintRunner"):
|
|
1228
|
+
run_id = payload.pop("run_id", None)
|
|
1229
|
+
if not run_id:
|
|
1230
|
+
raise ValueError("Missing run_id in payload")
|
|
1231
|
+
return blueprint_runner.cancel_blueprint_execution(run_id=run_id)
|
|
1232
|
+
|
|
1233
|
+
@staticmethod
|
|
1234
|
+
def run_blueprint_by_id(
|
|
1235
|
+
payload: dict,
|
|
1236
|
+
context: dict,
|
|
1237
|
+
session: dict,
|
|
1238
|
+
blueprint_runner: "BlueprintRunner",
|
|
1239
|
+
vault: Dict,
|
|
1240
|
+
):
|
|
1241
|
+
blueprint_id = payload.pop("blueprint_id", None)
|
|
1242
|
+
if not blueprint_id:
|
|
1243
|
+
raise ValueError("Missing blueprint_id in payload")
|
|
1244
|
+
execution_environment = EventHandler._get_blueprint_execution_environment(
|
|
1245
|
+
payload, context, session, vault
|
|
1246
|
+
)
|
|
1247
|
+
return blueprint_runner.run_blueprint(
|
|
1248
|
+
component_id=blueprint_id,
|
|
1249
|
+
execution_environment=execution_environment,
|
|
1250
|
+
title="Blueprint execution triggered on demand",
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
@staticmethod
|
|
1254
|
+
def run_blueprint_by_key(
|
|
1255
|
+
payload: dict,
|
|
1256
|
+
context: dict,
|
|
1257
|
+
session: dict,
|
|
1258
|
+
blueprint_runner: "BlueprintRunner",
|
|
1259
|
+
vault: Dict,
|
|
1260
|
+
):
|
|
1261
|
+
blueprint_key = payload.pop("blueprint_key", None)
|
|
1262
|
+
if not blueprint_key:
|
|
1263
|
+
raise ValueError("Missing blueprint_key in payload")
|
|
1264
|
+
execution_environment = EventHandler._get_blueprint_execution_environment(
|
|
1265
|
+
payload, context, session, vault
|
|
1266
|
+
)
|
|
1267
|
+
return blueprint_runner.run_blueprint_by_key(
|
|
1268
|
+
blueprint_key=blueprint_key, execution_environment=execution_environment
|
|
1269
|
+
)
|
|
1270
|
+
|
|
1271
|
+
@staticmethod
|
|
1272
|
+
def run_blueprint_via_api(
|
|
1273
|
+
payload: dict,
|
|
1274
|
+
context: dict,
|
|
1275
|
+
session: dict,
|
|
1276
|
+
blueprint_runner: "BlueprintRunner",
|
|
1277
|
+
vault: Dict,
|
|
1278
|
+
):
|
|
1279
|
+
"""
|
|
1280
|
+
This handler is used to run a blueprint via the API.
|
|
1281
|
+
It is used by the frontend to run a blueprint when the user clicks on a button.
|
|
1282
|
+
"""
|
|
1283
|
+
blueprint_id = payload.pop("blueprint_id", None)
|
|
1284
|
+
if not blueprint_id:
|
|
1285
|
+
raise ValueError("Missing blueprint_id in payload")
|
|
1286
|
+
trigger_type = payload.pop("trigger_type", None)
|
|
1287
|
+
if not trigger_type:
|
|
1288
|
+
raise ValueError("Missing trigger_type in payload")
|
|
1289
|
+
execution_environment = EventHandler._get_blueprint_execution_environment(
|
|
1290
|
+
payload, context, session, vault
|
|
1291
|
+
)
|
|
1292
|
+
return blueprint_runner.run_blueprint_via_api(
|
|
1293
|
+
blueprint_id=blueprint_id,
|
|
1294
|
+
trigger_type=trigger_type,
|
|
1295
|
+
branch_id=payload.pop("branch_id", None),
|
|
1296
|
+
execution_environment=execution_environment,
|
|
1297
|
+
)
|
|
1298
|
+
|
|
1299
|
+
@staticmethod
|
|
1300
|
+
def run_blueprint_branch(
|
|
1301
|
+
payload: dict,
|
|
1302
|
+
context: dict,
|
|
1303
|
+
session: dict,
|
|
1304
|
+
blueprint_runner: "BlueprintRunner",
|
|
1305
|
+
vault: Dict,
|
|
1306
|
+
):
|
|
1307
|
+
branch_id = payload.pop("branch_id", None)
|
|
1308
|
+
if not branch_id:
|
|
1309
|
+
raise ValueError("Missing branch_id in payload")
|
|
1310
|
+
execution_environment = EventHandler._get_blueprint_execution_environment(
|
|
1311
|
+
payload, context, session, vault
|
|
1312
|
+
)
|
|
1313
|
+
return blueprint_runner.run_branch(
|
|
1314
|
+
start_node_id=branch_id,
|
|
1315
|
+
branch_out_id=None,
|
|
1316
|
+
execution_environment=execution_environment,
|
|
1317
|
+
title="Branch execution triggered by demand",
|
|
1318
|
+
)
|
|
1147
1319
|
|
|
1148
1320
|
def __init__(self):
|
|
1149
|
-
self.handler_map: Dict[str,
|
|
1321
|
+
self.handler_map: Dict[str, "EventHandlerRegistry.HandlerEntry"] = {
|
|
1322
|
+
"stop_blueprint_run": {
|
|
1323
|
+
"callable": self.stop_blueprint_run,
|
|
1324
|
+
"meta": {"name": "stop_blueprint_run", "args": ["payload", "blueprint_runner"]},
|
|
1325
|
+
},
|
|
1326
|
+
"run_blueprint_by_key": {
|
|
1327
|
+
"callable": self.run_blueprint_by_key,
|
|
1328
|
+
"meta": {
|
|
1329
|
+
"name": "run_blueprint_by_key",
|
|
1330
|
+
"args": ["payload", "context", "session", "blueprint_runner", "vault"],
|
|
1331
|
+
},
|
|
1332
|
+
},
|
|
1333
|
+
"run_blueprint_by_id": {
|
|
1334
|
+
"callable": self.run_blueprint_by_id,
|
|
1335
|
+
"meta": {
|
|
1336
|
+
"name": "run_blueprint_by_id",
|
|
1337
|
+
"args": ["payload", "context", "session", "blueprint_runner", "vault"],
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
"run_blueprint_via_api": {
|
|
1341
|
+
"callable": self.run_blueprint_via_api,
|
|
1342
|
+
"meta": {
|
|
1343
|
+
"name": "run_blueprint_via_api",
|
|
1344
|
+
"args": ["payload", "context", "session", "blueprint_runner", "vault"],
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
"run_blueprint_branch": {
|
|
1348
|
+
"callable": self.run_blueprint_branch,
|
|
1349
|
+
"meta": {
|
|
1350
|
+
"name": "run_blueprint_branch",
|
|
1351
|
+
"args": ["payload", "context", "session", "blueprint_runner", "vault"],
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
}
|
|
1150
1355
|
|
|
1151
1356
|
def __iter__(self):
|
|
1152
1357
|
return iter(self.handler_map.keys())
|
|
@@ -1165,23 +1370,17 @@ class EventHandlerRegistry:
|
|
|
1165
1370
|
# and handler __qualname__by a dot
|
|
1166
1371
|
access_name = f"{module_name}.{handler.__qualname__}"
|
|
1167
1372
|
|
|
1168
|
-
entry: EventHandlerRegistry.HandlerEntry =
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
"name": access_name,
|
|
1173
|
-
"args": inspect.getfullargspec(handler).args
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1373
|
+
entry: EventHandlerRegistry.HandlerEntry = {
|
|
1374
|
+
"callable": handler,
|
|
1375
|
+
"meta": {"name": access_name, "args": inspect.getfullargspec(handler).args},
|
|
1376
|
+
}
|
|
1176
1377
|
|
|
1177
1378
|
self.handler_map[access_name] = entry
|
|
1178
1379
|
|
|
1179
1380
|
def register_module(self, module: ModuleType):
|
|
1180
1381
|
if isinstance(module, ModuleType):
|
|
1181
|
-
all_fn_names = (x[0] for x in inspect.getmembers(
|
|
1182
|
-
|
|
1183
|
-
exposed_fn_names = list(
|
|
1184
|
-
filter(lambda x: not x.startswith("_"), all_fn_names))
|
|
1382
|
+
all_fn_names = (x[0] for x in inspect.getmembers(module, inspect.isfunction))
|
|
1383
|
+
exposed_fn_names = list(filter(lambda x: not x.startswith("_"), all_fn_names))
|
|
1185
1384
|
|
|
1186
1385
|
for fn_name in exposed_fn_names:
|
|
1187
1386
|
fn_callable = getattr(module, fn_name)
|
|
@@ -1189,25 +1388,18 @@ class EventHandlerRegistry:
|
|
|
1189
1388
|
continue
|
|
1190
1389
|
self.register_handler(fn_callable)
|
|
1191
1390
|
else:
|
|
1192
|
-
raise ValueError(
|
|
1193
|
-
f"Attempted to register a non-module object: {module}"
|
|
1194
|
-
)
|
|
1391
|
+
raise ValueError(f"Attempted to register a non-module object: {module}")
|
|
1195
1392
|
|
|
1196
1393
|
def find_handler_callable(self, handler_name: str) -> Optional[Callable]:
|
|
1197
1394
|
if handler_name not in self.handler_map:
|
|
1198
1395
|
return None
|
|
1199
|
-
handler_entry: EventHandlerRegistry.HandlerEntry =
|
|
1200
|
-
self.handler_map[handler_name]
|
|
1396
|
+
handler_entry: EventHandlerRegistry.HandlerEntry = self.handler_map[handler_name]
|
|
1201
1397
|
return handler_entry["callable"]
|
|
1202
1398
|
|
|
1203
|
-
def get_handler_meta(
|
|
1204
|
-
self,
|
|
1205
|
-
handler_name: str
|
|
1206
|
-
) -> "EventHandlerRegistry.HandlerMeta":
|
|
1399
|
+
def get_handler_meta(self, handler_name: str) -> "EventHandlerRegistry.HandlerMeta":
|
|
1207
1400
|
if handler_name not in self.handler_map:
|
|
1208
1401
|
raise RuntimeError(f"Handler {handler_name} is not registered")
|
|
1209
|
-
entry: EventHandlerRegistry.HandlerEntry =
|
|
1210
|
-
self.handler_map[handler_name]
|
|
1402
|
+
entry: EventHandlerRegistry.HandlerEntry = self.handler_map[handler_name]
|
|
1211
1403
|
return entry["meta"]
|
|
1212
1404
|
|
|
1213
1405
|
def gather_handler_meta(self) -> List["EventHandlerRegistry.HandlerMeta"]:
|
|
@@ -1215,7 +1407,6 @@ class EventHandlerRegistry:
|
|
|
1215
1407
|
|
|
1216
1408
|
|
|
1217
1409
|
class EventDeserialiser:
|
|
1218
|
-
|
|
1219
1410
|
"""Applies transformations to the payload of an incoming event, depending on its type.
|
|
1220
1411
|
|
|
1221
1412
|
The transformation happens in place: the event passed to the transform method is mutated.
|
|
@@ -1224,7 +1415,9 @@ class EventDeserialiser:
|
|
|
1224
1415
|
applying sanitisation of inputs where relevant."""
|
|
1225
1416
|
|
|
1226
1417
|
def __init__(self, session: "WriterSession"):
|
|
1227
|
-
self.evaluator = writer.evaluator.Evaluator(
|
|
1418
|
+
self.evaluator = writer.evaluator.Evaluator(
|
|
1419
|
+
session.session_state, session.session_component_tree
|
|
1420
|
+
)
|
|
1228
1421
|
|
|
1229
1422
|
def transform(self, ev: WriterEvent) -> None:
|
|
1230
1423
|
# Events without payloads are safe
|
|
@@ -1243,8 +1436,7 @@ class EventDeserialiser:
|
|
|
1243
1436
|
if ev.isSafe:
|
|
1244
1437
|
return
|
|
1245
1438
|
ev.payload = {}
|
|
1246
|
-
raise ValueError(
|
|
1247
|
-
"No payload transformer available for custom event type.")
|
|
1439
|
+
raise ValueError("No payload transformer available for custom event type.")
|
|
1248
1440
|
tf_func = getattr(self, func_name)
|
|
1249
1441
|
try:
|
|
1250
1442
|
tf_payload = tf_func(ev)
|
|
@@ -1259,8 +1451,7 @@ class EventDeserialiser:
|
|
|
1259
1451
|
instance_path = ev.instancePath
|
|
1260
1452
|
if not instance_path:
|
|
1261
1453
|
raise ValueError("This event cannot be run as a global event.")
|
|
1262
|
-
options = self.evaluator.evaluate_field(
|
|
1263
|
-
instance_path, "tags", True, "{ }")
|
|
1454
|
+
options = self.evaluator.evaluate_field(instance_path, "tags", True, "{ }")
|
|
1264
1455
|
if not isinstance(options, dict):
|
|
1265
1456
|
raise ValueError("Invalid value for tags")
|
|
1266
1457
|
if payload not in options.keys():
|
|
@@ -1273,7 +1464,8 @@ class EventDeserialiser:
|
|
|
1273
1464
|
if not instance_path:
|
|
1274
1465
|
raise ValueError("This event cannot be run as a global event.")
|
|
1275
1466
|
options = self.evaluator.evaluate_field(
|
|
1276
|
-
instance_path, "options", True, """{ "a": "Option A", "b": "Option B" }"""
|
|
1467
|
+
instance_path, "options", True, """{ "a": "Option A", "b": "Option B" }"""
|
|
1468
|
+
)
|
|
1277
1469
|
if not isinstance(options, dict):
|
|
1278
1470
|
raise ValueError("Invalid value for options")
|
|
1279
1471
|
if payload not in options.keys():
|
|
@@ -1286,12 +1478,12 @@ class EventDeserialiser:
|
|
|
1286
1478
|
if not instance_path:
|
|
1287
1479
|
raise ValueError("This event cannot be run as a global event.")
|
|
1288
1480
|
options = self.evaluator.evaluate_field(
|
|
1289
|
-
instance_path, "options", True, """{ "a": "Option A", "b": "Option B" }"""
|
|
1481
|
+
instance_path, "options", True, """{ "a": "Option A", "b": "Option B" }"""
|
|
1482
|
+
)
|
|
1290
1483
|
if not isinstance(options, dict):
|
|
1291
1484
|
raise ValueError("Invalid value for options")
|
|
1292
1485
|
if not isinstance(payload, list):
|
|
1293
|
-
raise ValueError(
|
|
1294
|
-
"Invalid multiple options payload. Expected a list.")
|
|
1486
|
+
raise ValueError("Invalid multiple options payload. Expected a list.")
|
|
1295
1487
|
if not all(item in options.keys() for item in payload):
|
|
1296
1488
|
raise ValueError("Unauthorised option")
|
|
1297
1489
|
return payload
|
|
@@ -1310,7 +1502,7 @@ class EventDeserialiser:
|
|
|
1310
1502
|
"key": key,
|
|
1311
1503
|
"ctrl_key": ctrl_key,
|
|
1312
1504
|
"shift_key": shift_key,
|
|
1313
|
-
"meta_key": meta_key
|
|
1505
|
+
"meta_key": meta_key,
|
|
1314
1506
|
}
|
|
1315
1507
|
return tf_payload
|
|
1316
1508
|
|
|
@@ -1319,21 +1511,14 @@ class EventDeserialiser:
|
|
|
1319
1511
|
ctrl_key = bool(payload.get("ctrlKey"))
|
|
1320
1512
|
shift_key = bool(payload.get("shiftKey"))
|
|
1321
1513
|
meta_key = bool(payload.get("metaKey"))
|
|
1322
|
-
tf_payload = {
|
|
1323
|
-
"ctrl_key": ctrl_key,
|
|
1324
|
-
"shift_key": shift_key,
|
|
1325
|
-
"meta_key": meta_key
|
|
1326
|
-
}
|
|
1514
|
+
tf_payload = {"ctrl_key": ctrl_key, "shift_key": shift_key, "meta_key": meta_key}
|
|
1327
1515
|
return tf_payload
|
|
1328
1516
|
|
|
1329
1517
|
def _transform_hashchange(self, ev) -> Dict:
|
|
1330
1518
|
payload = ev.payload
|
|
1331
1519
|
page_key = payload.get("pageKey")
|
|
1332
1520
|
route_vars = dict(payload.get("routeVars"))
|
|
1333
|
-
tf_payload = {
|
|
1334
|
-
"page_key": page_key,
|
|
1335
|
-
"route_vars": route_vars
|
|
1336
|
-
}
|
|
1521
|
+
tf_payload = {"page_key": page_key, "route_vars": route_vars}
|
|
1337
1522
|
return tf_payload
|
|
1338
1523
|
|
|
1339
1524
|
def _transform_page_open(self, ev) -> str:
|
|
@@ -1344,10 +1529,7 @@ class EventDeserialiser:
|
|
|
1344
1529
|
payload = ev.payload
|
|
1345
1530
|
page_key = payload.get("pageKey")
|
|
1346
1531
|
route_vars = dict(payload.get("routeVars"))
|
|
1347
|
-
tf_payload = {
|
|
1348
|
-
"page_key": page_key,
|
|
1349
|
-
"route_vars": route_vars
|
|
1350
|
-
}
|
|
1532
|
+
tf_payload = {"page_key": page_key, "route_vars": route_vars}
|
|
1351
1533
|
return tf_payload
|
|
1352
1534
|
|
|
1353
1535
|
def _transform_chatbot_message(self, ev) -> dict:
|
|
@@ -1384,7 +1566,7 @@ class EventDeserialiser:
|
|
|
1384
1566
|
return {
|
|
1385
1567
|
"name": file_item.get("name"),
|
|
1386
1568
|
"type": file_item.get("type"),
|
|
1387
|
-
"data": urllib.request.urlopen(data).read()
|
|
1569
|
+
"data": urllib.request.urlopen(data).read(),
|
|
1388
1570
|
}
|
|
1389
1571
|
|
|
1390
1572
|
def _transform_file_change(self, ev) -> List[Dict]:
|
|
@@ -1401,8 +1583,7 @@ class EventDeserialiser:
|
|
|
1401
1583
|
try:
|
|
1402
1584
|
datetime.date.fromisoformat(payload)
|
|
1403
1585
|
except ValueError:
|
|
1404
|
-
raise ValueError(
|
|
1405
|
-
"Date must be in YYYY-MM-DD format or another valid ISO 8601 format.")
|
|
1586
|
+
raise ValueError("Date must be in YYYY-MM-DD format or another valid ISO 8601 format.")
|
|
1406
1587
|
|
|
1407
1588
|
return payload
|
|
1408
1589
|
|
|
@@ -1412,10 +1593,11 @@ class EventDeserialiser:
|
|
|
1412
1593
|
if not isinstance(payload, str):
|
|
1413
1594
|
raise ValueError("Time must be a string.")
|
|
1414
1595
|
try:
|
|
1415
|
-
time.strptime(payload,
|
|
1596
|
+
time.strptime(payload, "%H:%M")
|
|
1416
1597
|
except ValueError:
|
|
1417
1598
|
raise ValueError(
|
|
1418
|
-
"Time must be in hh:mm format (in 24-hour format that includes leading zeros)."
|
|
1599
|
+
"Time must be in hh:mm format (in 24-hour format that includes leading zeros)."
|
|
1600
|
+
)
|
|
1419
1601
|
|
|
1420
1602
|
return payload
|
|
1421
1603
|
|
|
@@ -1455,7 +1637,7 @@ class EventDeserialiser:
|
|
|
1455
1637
|
payload = ev.payload
|
|
1456
1638
|
if not isinstance(payload, dict):
|
|
1457
1639
|
return None
|
|
1458
|
-
|
|
1640
|
+
|
|
1459
1641
|
payload = _deserialize_bigint_format(payload)
|
|
1460
1642
|
return payload
|
|
1461
1643
|
|
|
@@ -1475,17 +1657,21 @@ class EventDeserialiser:
|
|
|
1475
1657
|
payload = _deserialize_bigint_format(payload)
|
|
1476
1658
|
return payload
|
|
1477
1659
|
|
|
1660
|
+
def _transform_tab_change(self, ev: WriterEvent) -> Optional[str]:
|
|
1661
|
+
payload = ev.payload
|
|
1662
|
+
if not isinstance(payload, str):
|
|
1663
|
+
return None
|
|
1664
|
+
return payload
|
|
1478
1665
|
|
|
1479
|
-
class SessionManager:
|
|
1480
1666
|
|
|
1667
|
+
class SessionManager:
|
|
1481
1668
|
"""
|
|
1482
1669
|
Stores and manages sessions.
|
|
1483
1670
|
"""
|
|
1484
1671
|
|
|
1485
1672
|
IDLE_SESSION_MAX_SECONDS = 3600
|
|
1486
1673
|
TOKEN_SIZE_BYTES = 32
|
|
1487
|
-
hex_pattern = re.compile(
|
|
1488
|
-
r"^[0-9a-fA-F]{" + str(TOKEN_SIZE_BYTES*2) + r"}$")
|
|
1674
|
+
hex_pattern = re.compile(r"^[0-9a-fA-F]{" + str(TOKEN_SIZE_BYTES * 2) + r"}$")
|
|
1489
1675
|
|
|
1490
1676
|
def __init__(self) -> None:
|
|
1491
1677
|
self.sessions: Dict[str, WriterSession] = {}
|
|
@@ -1494,7 +1680,9 @@ class SessionManager:
|
|
|
1494
1680
|
def add_verifier(self, verifier: Callable) -> None:
|
|
1495
1681
|
self.verifiers.append(verifier)
|
|
1496
1682
|
|
|
1497
|
-
def _verify_before_new_session(
|
|
1683
|
+
def _verify_before_new_session(
|
|
1684
|
+
self, cookies: Optional[Dict] = None, headers: Optional[Dict] = None
|
|
1685
|
+
) -> bool:
|
|
1498
1686
|
for verifier in self.verifiers:
|
|
1499
1687
|
args = inspect.getfullargspec(verifier).args
|
|
1500
1688
|
arg_values = []
|
|
@@ -1509,8 +1697,7 @@ class SessionManager:
|
|
|
1509
1697
|
elif verifier_result is True:
|
|
1510
1698
|
pass
|
|
1511
1699
|
else:
|
|
1512
|
-
raise ValueError(
|
|
1513
|
-
"Invalid verifier return value. Must be True or False.")
|
|
1700
|
+
raise ValueError("Invalid verifier return value. Must be True or False.")
|
|
1514
1701
|
return True
|
|
1515
1702
|
|
|
1516
1703
|
def _check_proposed_session_id(self, proposed_session_id: Optional[str]) -> bool:
|
|
@@ -1520,7 +1707,12 @@ class SessionManager:
|
|
|
1520
1707
|
return True
|
|
1521
1708
|
return False
|
|
1522
1709
|
|
|
1523
|
-
def get_new_session(
|
|
1710
|
+
def get_new_session(
|
|
1711
|
+
self,
|
|
1712
|
+
cookies: Optional[Dict] = None,
|
|
1713
|
+
headers: Optional[Dict] = None,
|
|
1714
|
+
proposed_session_id: Optional[str] = None,
|
|
1715
|
+
) -> Optional[WriterSession]:
|
|
1524
1716
|
if not self._check_proposed_session_id(proposed_session_id):
|
|
1525
1717
|
return None
|
|
1526
1718
|
if not self._verify_before_new_session(cookies, headers):
|
|
@@ -1534,9 +1726,13 @@ class SessionManager:
|
|
|
1534
1726
|
self.sessions[new_id] = new_session
|
|
1535
1727
|
return new_session
|
|
1536
1728
|
|
|
1537
|
-
def get_session(
|
|
1729
|
+
def get_session(
|
|
1730
|
+
self, session_id: Optional[str], restore_initial_mail: bool = False
|
|
1731
|
+
) -> Optional[WriterSession]:
|
|
1538
1732
|
if session_id is None:
|
|
1539
1733
|
return None
|
|
1734
|
+
if session_id == "anonymous":
|
|
1735
|
+
return None
|
|
1540
1736
|
|
|
1541
1737
|
session = self.sessions.get(session_id)
|
|
1542
1738
|
if session is not None and restore_initial_mail is True:
|
|
@@ -1556,8 +1752,7 @@ class SessionManager:
|
|
|
1556
1752
|
del self.sessions[session_id]
|
|
1557
1753
|
|
|
1558
1754
|
def prune_sessions(self) -> None:
|
|
1559
|
-
cutoff_timestamp = int(time.time()) -
|
|
1560
|
-
SessionManager.IDLE_SESSION_MAX_SECONDS
|
|
1755
|
+
cutoff_timestamp = int(time.time()) - SessionManager.IDLE_SESSION_MAX_SECONDS
|
|
1561
1756
|
prune_sessions = []
|
|
1562
1757
|
for session_id, session in self.sessions.items():
|
|
1563
1758
|
if session.last_active_timestamp < cutoff_timestamp:
|
|
@@ -1575,21 +1770,21 @@ class SessionManager:
|
|
|
1575
1770
|
|
|
1576
1771
|
|
|
1577
1772
|
class EventHandler:
|
|
1578
|
-
|
|
1579
1773
|
"""
|
|
1580
1774
|
Handles events in the context of a Session.
|
|
1581
1775
|
"""
|
|
1582
1776
|
|
|
1583
1777
|
def __init__(self, session: WriterSession) -> None:
|
|
1584
|
-
import writer.
|
|
1778
|
+
import writer.blueprints
|
|
1585
1779
|
|
|
1586
1780
|
self.session = session
|
|
1587
1781
|
self.session_state = session.session_state
|
|
1588
1782
|
self.session_component_tree = session.session_component_tree
|
|
1589
1783
|
self.deser = EventDeserialiser(session)
|
|
1590
|
-
self.evaluator = writer.evaluator.Evaluator(
|
|
1591
|
-
|
|
1592
|
-
|
|
1784
|
+
self.evaluator = writer.evaluator.Evaluator(
|
|
1785
|
+
session.session_state, session.session_component_tree
|
|
1786
|
+
)
|
|
1787
|
+
self.blueprint_runner = writer.blueprints.BlueprintRunner(session)
|
|
1593
1788
|
|
|
1594
1789
|
def _handle_binding(self, event_type, target_component, instance_path, payload) -> None:
|
|
1595
1790
|
if not target_component.binding:
|
|
@@ -1599,28 +1794,44 @@ class EventHandler:
|
|
|
1599
1794
|
return
|
|
1600
1795
|
self.evaluator.set_state(binding["stateRef"], instance_path, payload)
|
|
1601
1796
|
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
return self.workflow_runner.run_workflow_by_key(workflow_key, execution_environment)
|
|
1611
|
-
elif workflow_id:
|
|
1612
|
-
return self.workflow_runner.run_workflow(workflow_id, execution_environment, "Workflow execution triggered on demand")
|
|
1613
|
-
return fn
|
|
1797
|
+
@staticmethod
|
|
1798
|
+
def _get_blueprint_execution_environment(payload, context, session, vault):
|
|
1799
|
+
return {
|
|
1800
|
+
"payload": payload,
|
|
1801
|
+
"context": context,
|
|
1802
|
+
"session": session,
|
|
1803
|
+
"vault": vault,
|
|
1804
|
+
}
|
|
1614
1805
|
|
|
1615
|
-
def
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1806
|
+
def _get_blueprint_callable(
|
|
1807
|
+
self,
|
|
1808
|
+
blueprint_key: Optional[str] = None,
|
|
1809
|
+
blueprint_id: Optional[str] = None,
|
|
1810
|
+
branch_id: Optional[str] = None,
|
|
1811
|
+
):
|
|
1812
|
+
def fn(payload, context, session, vault):
|
|
1813
|
+
execution_environment = self._get_blueprint_execution_environment(
|
|
1814
|
+
payload, context, session, vault
|
|
1815
|
+
)
|
|
1816
|
+
if blueprint_key:
|
|
1817
|
+
return self.blueprint_runner.run_blueprint_by_key(
|
|
1818
|
+
blueprint_key, execution_environment
|
|
1819
|
+
)
|
|
1820
|
+
elif blueprint_id:
|
|
1821
|
+
return self.blueprint_runner.run_blueprint(
|
|
1822
|
+
blueprint_id, execution_environment, "Blueprint execution triggered on demand"
|
|
1823
|
+
)
|
|
1824
|
+
elif branch_id:
|
|
1825
|
+
return self.blueprint_runner.run_branch(
|
|
1826
|
+
branch_id,
|
|
1827
|
+
None,
|
|
1828
|
+
execution_environment,
|
|
1829
|
+
"Blueprint branch execution triggered on demand",
|
|
1830
|
+
)
|
|
1619
1831
|
|
|
1620
|
-
|
|
1621
|
-
workflow_id = handler[17:]
|
|
1622
|
-
return self._get_workflow_callable(workflow_id=workflow_id)
|
|
1832
|
+
return fn
|
|
1623
1833
|
|
|
1834
|
+
def _get_handler_callable(self, handler: str) -> Optional[Callable]:
|
|
1624
1835
|
current_app_process = get_app_process()
|
|
1625
1836
|
handler_registry = current_app_process.handler_registry
|
|
1626
1837
|
callable_handler = handler_registry.find_handler_callable(handler)
|
|
@@ -1633,51 +1844,73 @@ class EventHandler:
|
|
|
1633
1844
|
"state": self.session_state,
|
|
1634
1845
|
"payload": ev.payload,
|
|
1635
1846
|
"context": context_data,
|
|
1636
|
-
"session":_event_handler_session_info(),
|
|
1637
|
-
"ui": _event_handler_ui_manager()
|
|
1847
|
+
"session": _event_handler_session_info(),
|
|
1848
|
+
"ui": _event_handler_ui_manager(),
|
|
1849
|
+
"blueprint_runner": self.blueprint_runner,
|
|
1850
|
+
"vault": writer_vault.get_secrets(),
|
|
1638
1851
|
}
|
|
1639
1852
|
|
|
1640
|
-
def _call_handler_callable(
|
|
1641
|
-
self,
|
|
1642
|
-
handler_callable: Callable,
|
|
1643
|
-
calling_arguments: Dict
|
|
1644
|
-
) -> Any:
|
|
1853
|
+
def _call_handler_callable(self, handler_callable: Callable, calling_arguments: Dict) -> Any:
|
|
1645
1854
|
current_app_process = get_app_process()
|
|
1646
1855
|
result = None
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1856
|
+
with (
|
|
1857
|
+
core_ui.use_component_tree(self.session.session_component_tree),
|
|
1858
|
+
use_stdout_redirect(
|
|
1859
|
+
lambda entry: self.session_state.add_log_entry("info", "Stdout message", entry)
|
|
1860
|
+
),
|
|
1861
|
+
):
|
|
1650
1862
|
middlewares_executors = current_app_process.middleware_registry.executors()
|
|
1651
|
-
result = EventHandlerExecutor.invoke_with_middlewares(
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
if captured_stdout:
|
|
1655
|
-
self.session_state.add_log_entry("info", "Stdout message", captured_stdout)
|
|
1863
|
+
result = EventHandlerExecutor.invoke_with_middlewares(
|
|
1864
|
+
middlewares_executors, handler_callable, calling_arguments
|
|
1865
|
+
)
|
|
1656
1866
|
|
|
1657
1867
|
return result
|
|
1658
1868
|
|
|
1659
1869
|
def _deserialize(self, ev: WriterEvent):
|
|
1660
1870
|
try:
|
|
1661
1871
|
self.deser.transform(ev)
|
|
1662
|
-
except BaseException as e:
|
|
1872
|
+
except BaseException as e:
|
|
1663
1873
|
self.session_state.add_notification(
|
|
1664
|
-
"error",
|
|
1665
|
-
|
|
1666
|
-
|
|
1874
|
+
"error",
|
|
1875
|
+
"Error",
|
|
1876
|
+
f"A deserialization error occurred when handling event '{ ev.type }'.",
|
|
1877
|
+
)
|
|
1878
|
+
self.session_state.add_log_entry(
|
|
1879
|
+
"error",
|
|
1880
|
+
"Deserialization Failed",
|
|
1881
|
+
f"The data sent might be corrupt. A runtime exception was raised when deserializing event '{ ev.type }'.",
|
|
1882
|
+
traceback.format_exc(),
|
|
1883
|
+
)
|
|
1667
1884
|
raise e
|
|
1668
1885
|
|
|
1669
1886
|
def _handle_global_event(self, ev: WriterEvent):
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1887
|
+
try:
|
|
1888
|
+
if not ev.isSafe:
|
|
1889
|
+
raise PermissionError("Attempted executing a global event in an unsafe context.")
|
|
1890
|
+
if not ev.handler:
|
|
1891
|
+
raise ValueError("Handler not specified when attempting to execute global event.")
|
|
1892
|
+
handler_callable = self._get_handler_callable(ev.handler)
|
|
1893
|
+
if not handler_callable:
|
|
1894
|
+
return
|
|
1895
|
+
calling_arguments = self._get_calling_arguments(ev, instance_path=None)
|
|
1896
|
+
return self._call_handler_callable(handler_callable, calling_arguments)
|
|
1897
|
+
except BaseException as e:
|
|
1898
|
+
if not isinstance(e, BlueprintExecutionError):
|
|
1899
|
+
# Only create a notification and log entry
|
|
1900
|
+
# for non-blueprint errors, as blueprint errors
|
|
1901
|
+
# have their own logging mechanism
|
|
1902
|
+
self.session_state.add_notification(
|
|
1903
|
+
"error",
|
|
1904
|
+
"Runtime Error",
|
|
1905
|
+
f"An error occurred when processing event '{ ev.type }'.",
|
|
1906
|
+
)
|
|
1907
|
+
self.session_state.add_log_entry(
|
|
1908
|
+
"error",
|
|
1909
|
+
"Runtime Exception",
|
|
1910
|
+
f"A runtime exception was raised when processing event '{ ev.type }'.",
|
|
1911
|
+
traceback.format_exc(),
|
|
1912
|
+
)
|
|
1913
|
+
raise e
|
|
1681
1914
|
|
|
1682
1915
|
def _handle_component_event(self, ev: WriterEvent):
|
|
1683
1916
|
instance_path = ev.instancePath
|
|
@@ -1687,6 +1920,14 @@ class EventHandler:
|
|
|
1687
1920
|
target_id = instance_path[-1]["componentId"]
|
|
1688
1921
|
target_component = cast(Component, self.session_component_tree.get_component(target_id))
|
|
1689
1922
|
self._handle_binding(ev.type, target_component, instance_path, ev.payload)
|
|
1923
|
+
calling_arguments = self._get_calling_arguments(ev, instance_path)
|
|
1924
|
+
execution_environment = self._get_blueprint_execution_environment(
|
|
1925
|
+
calling_arguments.get("payload"),
|
|
1926
|
+
calling_arguments.get("context"),
|
|
1927
|
+
calling_arguments.get("session"),
|
|
1928
|
+
calling_arguments.get("vault"),
|
|
1929
|
+
)
|
|
1930
|
+
self.blueprint_runner.execute_ui_trigger(target_id, ev.type, execution_environment)
|
|
1690
1931
|
if not target_component.handlers:
|
|
1691
1932
|
return None
|
|
1692
1933
|
handler = target_component.handlers.get(ev.type)
|
|
@@ -1695,13 +1936,19 @@ class EventHandler:
|
|
|
1695
1936
|
handler_callable = self._get_handler_callable(handler)
|
|
1696
1937
|
if not handler_callable:
|
|
1697
1938
|
return
|
|
1698
|
-
calling_arguments = self._get_calling_arguments(ev, instance_path)
|
|
1699
1939
|
return self._call_handler_callable(handler_callable, calling_arguments)
|
|
1700
1940
|
except BaseException as e:
|
|
1701
|
-
self.session_state.add_notification(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1941
|
+
self.session_state.add_notification(
|
|
1942
|
+
"error",
|
|
1943
|
+
"Runtime Error",
|
|
1944
|
+
f"An error occurred when processing event '{ ev.type }'.",
|
|
1945
|
+
)
|
|
1946
|
+
self.session_state.add_log_entry(
|
|
1947
|
+
"error",
|
|
1948
|
+
"Runtime Exception",
|
|
1949
|
+
f"A runtime exception was raised when processing event '{ ev.type }'.",
|
|
1950
|
+
traceback.format_exc(),
|
|
1951
|
+
)
|
|
1705
1952
|
raise e
|
|
1706
1953
|
|
|
1707
1954
|
def handle(self, ev: WriterEvent) -> WriterEventResult:
|
|
@@ -1718,7 +1965,6 @@ class EventHandler:
|
|
|
1718
1965
|
|
|
1719
1966
|
|
|
1720
1967
|
class EventHandlerExecutor:
|
|
1721
|
-
|
|
1722
1968
|
@staticmethod
|
|
1723
1969
|
def build_arguments(func: Callable, writer_args: dict) -> List[Any]:
|
|
1724
1970
|
"""
|
|
@@ -1756,7 +2002,7 @@ class EventHandlerExecutor:
|
|
|
1756
2002
|
>>> EventHandlerExecutor.invoke(my_handler, {'state': {'a': 1}, 'payload': None, 'context': None, 'session': None, 'ui': None})
|
|
1757
2003
|
"""
|
|
1758
2004
|
is_async_handler = inspect.iscoroutinefunction(callable_handler)
|
|
1759
|
-
if
|
|
2005
|
+
if not callable(callable_handler) and not is_async_handler:
|
|
1760
2006
|
raise ValueError("Invalid handler. The handler isn't a callable object.")
|
|
1761
2007
|
|
|
1762
2008
|
handler_args = EventHandlerExecutor.build_arguments(callable_handler, writer_args)
|
|
@@ -1770,7 +2016,11 @@ class EventHandlerExecutor:
|
|
|
1770
2016
|
return result
|
|
1771
2017
|
|
|
1772
2018
|
@staticmethod
|
|
1773
|
-
def invoke_with_middlewares(
|
|
2019
|
+
def invoke_with_middlewares(
|
|
2020
|
+
middlewares_executors: List[MiddlewareExecutor],
|
|
2021
|
+
callable_handler: Callable,
|
|
2022
|
+
writer_args: dict,
|
|
2023
|
+
) -> Any:
|
|
1774
2024
|
"""
|
|
1775
2025
|
Runs the middlewares then the handler. This function allows you to manage exceptions that are triggered in middleware
|
|
1776
2026
|
|
|
@@ -1789,7 +2039,9 @@ class EventHandlerExecutor:
|
|
|
1789
2039
|
else:
|
|
1790
2040
|
executor = middlewares_executors[0]
|
|
1791
2041
|
with executor.execute(writer_args):
|
|
1792
|
-
return EventHandlerExecutor.invoke_with_middlewares(
|
|
2042
|
+
return EventHandlerExecutor.invoke_with_middlewares(
|
|
2043
|
+
middlewares_executors[1:], callable_handler, writer_args
|
|
2044
|
+
)
|
|
1793
2045
|
|
|
1794
2046
|
|
|
1795
2047
|
class DictPropertyProxy:
|
|
@@ -1840,6 +2092,7 @@ class DictPropertyProxy:
|
|
|
1840
2092
|
|
|
1841
2093
|
S = TypeVar("S", bound=WriterState)
|
|
1842
2094
|
|
|
2095
|
+
|
|
1843
2096
|
def new_initial_state(klass: Type[S], raw_state: dict) -> S:
|
|
1844
2097
|
"""
|
|
1845
2098
|
Initializes the initial state of the application and makes it globally accessible.
|
|
@@ -1859,6 +2112,7 @@ def new_initial_state(klass: Type[S], raw_state: dict) -> S:
|
|
|
1859
2112
|
|
|
1860
2113
|
return initial_state
|
|
1861
2114
|
|
|
2115
|
+
|
|
1862
2116
|
"""
|
|
1863
2117
|
This variable contains the list of properties calculated for each class
|
|
1864
2118
|
that inherits from State.
|
|
@@ -1868,6 +2122,7 @@ these properties when loading an application.
|
|
|
1868
2122
|
"""
|
|
1869
2123
|
calculated_properties_per_state_type: Dict[Type[State], List[str]] = {}
|
|
1870
2124
|
|
|
2125
|
+
|
|
1871
2126
|
def writerproperty(path: Union[str, List[str]]):
|
|
1872
2127
|
"""
|
|
1873
2128
|
Mechanism for declaring a calculated property whenever an attribute changes
|
|
@@ -1891,8 +2146,7 @@ def writerproperty(path: Union[str, List[str]]):
|
|
|
1891
2146
|
>>> return self.counterA + self.counterB
|
|
1892
2147
|
"""
|
|
1893
2148
|
|
|
1894
|
-
class Property
|
|
1895
|
-
|
|
2149
|
+
class Property:
|
|
1896
2150
|
def __init__(self, func):
|
|
1897
2151
|
self.func = func
|
|
1898
2152
|
self.initialized = False
|
|
@@ -1917,17 +2171,22 @@ def writerproperty(path: Union[str, List[str]]):
|
|
|
1917
2171
|
"""
|
|
1918
2172
|
args = inspect.getfullargspec(self.func)
|
|
1919
2173
|
if len(args.args) > 1:
|
|
1920
|
-
logging.warning(
|
|
2174
|
+
logging.warning(
|
|
2175
|
+
f"Wrong signature for calculated property '{instance.__class__.__name__}:{self.property_name}'. It must declare only self argument."
|
|
2176
|
+
)
|
|
1921
2177
|
return None
|
|
1922
2178
|
|
|
1923
2179
|
if self.initialized is False:
|
|
1924
|
-
instance.calculated_property(
|
|
2180
|
+
instance.calculated_property(
|
|
2181
|
+
property_name=self.property_name, path=path, handler=self.func
|
|
2182
|
+
)
|
|
1925
2183
|
self.initialized = True
|
|
1926
2184
|
|
|
1927
2185
|
return self.func(instance)
|
|
1928
2186
|
|
|
1929
2187
|
return Property
|
|
1930
2188
|
|
|
2189
|
+
|
|
1931
2190
|
def session_verifier(func: Callable) -> Callable:
|
|
1932
2191
|
"""
|
|
1933
2192
|
Decorator for marking session verifiers.
|
|
@@ -1967,7 +2226,10 @@ def reset_base_component_tree() -> None:
|
|
|
1967
2226
|
global base_component_tree
|
|
1968
2227
|
base_component_tree = core_ui.build_base_component_tree()
|
|
1969
2228
|
|
|
1970
|
-
|
|
2229
|
+
|
|
2230
|
+
def _clone_mutation_subscriptions(
|
|
2231
|
+
session_state: State, app_state: State, root_state: Optional["State"] = None
|
|
2232
|
+
) -> None:
|
|
1971
2233
|
"""
|
|
1972
2234
|
clone subscriptions on mutations between the initial state of the application and the state created for the session
|
|
1973
2235
|
|
|
@@ -1988,7 +2250,9 @@ def _clone_mutation_subscriptions(session_state: State, app_state: State, root_s
|
|
|
1988
2250
|
_root_state = root_state if root_state is not None else session_state
|
|
1989
2251
|
for mutation_subscription in state_proxy_app.local_mutation_subscriptions:
|
|
1990
2252
|
new_mutation_subscription = copy.copy(mutation_subscription)
|
|
1991
|
-
new_mutation_subscription.state =
|
|
2253
|
+
new_mutation_subscription.state = (
|
|
2254
|
+
_root_state if new_mutation_subscription.type == "subscription" else session_state
|
|
2255
|
+
)
|
|
1992
2256
|
session_state._state_proxy.local_mutation_subscriptions.append(new_mutation_subscription)
|
|
1993
2257
|
|
|
1994
2258
|
|
|
@@ -2006,19 +2270,19 @@ def parse_state_variable_expression(p: str):
|
|
|
2006
2270
|
it = 0
|
|
2007
2271
|
last_split = 0
|
|
2008
2272
|
while it < len(p):
|
|
2009
|
-
if p[it] ==
|
|
2273
|
+
if p[it] == "\\":
|
|
2010
2274
|
it += 2
|
|
2011
|
-
elif p[it] ==
|
|
2012
|
-
new_part = p[last_split:
|
|
2013
|
-
parts.append(new_part.replace(
|
|
2275
|
+
elif p[it] == ".":
|
|
2276
|
+
new_part = p[last_split:it]
|
|
2277
|
+
parts.append(new_part.replace("\\.", "."))
|
|
2014
2278
|
|
|
2015
2279
|
last_split = it + 1
|
|
2016
2280
|
it += 1
|
|
2017
2281
|
else:
|
|
2018
2282
|
it += 1
|
|
2019
2283
|
|
|
2020
|
-
new_part = p[last_split: len(p)]
|
|
2021
|
-
parts.append(new_part.replace(
|
|
2284
|
+
new_part = p[last_split : len(p)]
|
|
2285
|
+
parts.append(new_part.replace("\\.", "."))
|
|
2022
2286
|
return parts
|
|
2023
2287
|
|
|
2024
2288
|
|
|
@@ -2026,31 +2290,36 @@ async def _async_wrapper_internal(callable_handler: Callable, arg_values: List[A
|
|
|
2026
2290
|
result = await callable_handler(*arg_values)
|
|
2027
2291
|
return result
|
|
2028
2292
|
|
|
2293
|
+
|
|
2029
2294
|
def _event_handler_session_info() -> Dict[str, Any]:
|
|
2030
2295
|
"""
|
|
2031
2296
|
Returns the session information for the current event handler.
|
|
2032
2297
|
|
|
2033
2298
|
This information is exposed in the session parameter of a handler
|
|
2034
|
-
|
|
2299
|
+
|
|
2035
2300
|
"""
|
|
2036
2301
|
current_session = get_session()
|
|
2037
2302
|
session_info: Dict[str, Any] = {}
|
|
2038
2303
|
if current_session is not None:
|
|
2039
|
-
session_info[
|
|
2040
|
-
session_info[
|
|
2041
|
-
session_info[
|
|
2042
|
-
session_info[
|
|
2304
|
+
session_info["id"] = current_session.session_id
|
|
2305
|
+
session_info["cookies"] = current_session.cookies
|
|
2306
|
+
session_info["headers"] = current_session.headers
|
|
2307
|
+
session_info["userinfo"] = current_session.userinfo or {}
|
|
2043
2308
|
|
|
2044
2309
|
return session_info
|
|
2045
2310
|
|
|
2311
|
+
|
|
2046
2312
|
def _event_handler_ui_manager():
|
|
2047
2313
|
from writer import PROPER_UI_INIT, _get_ui_runtime_error_message
|
|
2314
|
+
|
|
2048
2315
|
if PROPER_UI_INIT:
|
|
2049
2316
|
from writer.ui import WriterUIManager
|
|
2317
|
+
|
|
2050
2318
|
return WriterUIManager()
|
|
2051
2319
|
else:
|
|
2052
2320
|
raise RuntimeError(_get_ui_runtime_error_message())
|
|
2053
2321
|
|
|
2322
|
+
|
|
2054
2323
|
def _deserialize_bigint_format(payload: Optional[Union[dict, list]]):
|
|
2055
2324
|
"""
|
|
2056
2325
|
Decodes the payload of a big int serialization
|
|
@@ -2117,9 +2386,11 @@ def _type_match_dict(expected_type: Type):
|
|
|
2117
2386
|
>>>
|
|
2118
2387
|
>>> _type_match_dict(SpecifcDict) # True
|
|
2119
2388
|
"""
|
|
2120
|
-
if
|
|
2121
|
-
|
|
2122
|
-
|
|
2389
|
+
if (
|
|
2390
|
+
expected_type is not None
|
|
2391
|
+
and inspect.isclass(expected_type)
|
|
2392
|
+
and issubclass(expected_type, dict)
|
|
2393
|
+
):
|
|
2123
2394
|
return True
|
|
2124
2395
|
|
|
2125
2396
|
if typing.get_origin(expected_type) == dict:
|
|
@@ -2151,6 +2422,7 @@ def unescape_bigint_matching_string(string: str) -> str:
|
|
|
2151
2422
|
|
|
2152
2423
|
return result
|
|
2153
2424
|
|
|
2425
|
+
|
|
2154
2426
|
state_serialiser = StateSerialiser()
|
|
2155
2427
|
initial_state = WriterState()
|
|
2156
2428
|
base_component_tree = core_ui.build_base_component_tree()
|