abstra 3.24.3__py3-none-any.whl → 3.24.4__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.
Potentially problematic release.
This version of abstra might be problematic. Click here for more details.
- abstra/ai.py +2 -0
- {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/METADATA +1 -1
- {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/RECORD +190 -187
- abstra_internals/contracts_generated.py +3737 -2560
- abstra_internals/controllers/workflows.py +42 -0
- abstra_internals/interface/sdk/ai.py +69 -0
- abstra_internals/repositories/git/native.py +90 -3
- abstra_internals/repositories/git/types.py +10 -0
- abstra_internals/repositories/linter/rules/env_in_bundle.py +2 -0
- abstra_internals/repositories/project/json_migrations/__init__.py +2 -0
- abstra_internals/repositories/project/json_migrations/migration_016.py +17 -0
- abstra_internals/repositories/project/json_migrations/migration_016_test.py +141 -0
- abstra_internals/repositories/project/project.py +62 -17
- abstra_internals/repositories/project/project_test.py +279 -0
- abstra_internals/services/fs.py +311 -32
- abstra_internals/services/fs_test.py +28 -5
- abstra_internals/utils/file.py +7 -3
- abstra_internals/utils/fs_cache.py +173 -0
- abstra_statics/dist/assets/{AbstraButton.vue_vue_type_script_setup_true_lang.13670ae7.js → AbstraButton.vue_vue_type_script_setup_true_lang.779f608b.js} +2 -2
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.9c4020b5.js +2 -0
- abstra_statics/dist/assets/{ApiKeys.9b0b18b5.js → ApiKeys.fbd3ff63.js} +2 -2
- abstra_statics/dist/assets/App.cdc99dd8.js +2 -0
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.c5b57972.js +2 -0
- abstra_statics/dist/assets/BaseLayout.c2cb2f91.js +2 -0
- abstra_statics/dist/assets/Billing.f53186a3.js +2 -0
- abstra_statics/dist/assets/{Breadcrumb.e54636d6.js → Breadcrumb.1a138bb1.js} +2 -2
- abstra_statics/dist/assets/{Builds.c7363e1b.js → Builds.f3c7442c.js} +2 -2
- abstra_statics/dist/assets/{Card.4a8a30bb.js → Card.0899e9af.js} +2 -2
- abstra_statics/dist/assets/{CircularLoading.d81a4cac.js → CircularLoading.9f9e733d.js} +2 -2
- abstra_statics/dist/assets/{CloseCircleOutlined.39b5ab06.js → CloseCircleOutlined.36d9f25b.js} +2 -2
- abstra_statics/dist/assets/{ConnectorsView.d4b67e2e.js → ConnectorsView.9d99a93b.js} +2 -2
- abstra_statics/dist/assets/{ConsoleOmniChat.vue_vue_type_script_setup_true_lang.5360224e.js → ConsoleOmniChat.vue_vue_type_script_setup_true_lang.7061772f.js} +2 -2
- abstra_statics/dist/assets/{ContentLayout.10f24838.js → ContentLayout.da1fff81.js} +2 -2
- abstra_statics/dist/assets/CrudView.5b250a71.js +2 -0
- abstra_statics/dist/assets/{DocsButton.vue_vue_type_script_setup_true_lang.108b18e1.js → DocsButton.vue_vue_type_script_setup_true_lang.88300c0e.js} +2 -2
- abstra_statics/dist/assets/{EditorLogin.2f00deb7.js → EditorLogin.440d3dc5.js} +2 -2
- abstra_statics/dist/assets/{EditorsView.eb87a2d8.js → EditorsView.c81d5c0a.js} +2 -2
- abstra_statics/dist/assets/EnvVars.688f662d.js +2 -0
- abstra_statics/dist/assets/{Error.98b8036c.js → Error.8fd45945.js} +2 -2
- abstra_statics/dist/assets/ExclamationCircleOutlined.ea15dcd1.js +2 -0
- abstra_statics/dist/assets/{Files.9fc8199a.js → Files.2ef1fd75.js} +2 -2
- abstra_statics/dist/assets/Form.4047a0fe.js +2 -0
- abstra_statics/dist/assets/Form.7d1b0423.css +1 -0
- abstra_statics/dist/assets/FormRunner.8d0c448a.js +2 -0
- abstra_statics/dist/assets/Home.586b0b6c.js +2 -0
- abstra_statics/dist/assets/{Home.191a6dce.js → Home.c4610516.js} +2 -2
- abstra_statics/dist/assets/LoadingContainer.12120ff7.js +2 -0
- abstra_statics/dist/assets/LoadingOutlined.3c83d190.js +2 -0
- abstra_statics/dist/assets/{Login.edfbdaea.js → Login.2d19f80c.js} +2 -2
- abstra_statics/dist/assets/Login.de9c56a5.js +2 -0
- abstra_statics/dist/assets/{Login.vue_vue_type_script_setup_true_lang.02acef81.js → Login.vue_vue_type_script_setup_true_lang.8d4054f1.js} +2 -2
- abstra_statics/dist/assets/Logo.3f68eae2.js +2 -0
- abstra_statics/dist/assets/{Logs.4c6c0b3a.js → Logs.1f1770c9.js} +2 -2
- abstra_statics/dist/assets/{LogsController.a58ca42a.js → LogsController.e88bddfb.js} +2 -2
- abstra_statics/dist/assets/Main.a79ded11.js +2 -0
- abstra_statics/dist/assets/MockForm.025d99f9.css +1 -0
- abstra_statics/dist/assets/{MockForm.091aa4ce.js → MockForm.aa5ad3bb.js} +2 -2
- abstra_statics/dist/assets/Navbar.2529c5ae.js +2 -0
- abstra_statics/dist/assets/NewEditor.2603174c.js +8 -0
- abstra_statics/dist/assets/{NewEditor.f2d1c0c3.css → NewEditor.5ebf7c09.css} +1 -1
- abstra_statics/dist/assets/OidcLoginCallback.7ed0c484.js +2 -0
- abstra_statics/dist/assets/OidcLogoutCallback.7303c2ab.js +2 -0
- abstra_statics/dist/assets/{OmniChat.c78c1e51.js → OmniChat.3d03d97a.js} +2 -2
- abstra_statics/dist/assets/{OnboardingView.687780ed.js → OnboardingView.4b747af0.js} +2 -2
- abstra_statics/dist/assets/Organization.aab680aa.js +2 -0
- abstra_statics/dist/assets/{Organizations.fc123489.js → Organizations.2340795a.js} +2 -2
- abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.6ab1b899.js → PhArrowCounterClockwise.vue.d9dd0137.js} +2 -2
- abstra_statics/dist/assets/{PhArrowSquareOut.vue.1cebb708.js → PhArrowSquareOut.vue.93df49c1.js} +2 -2
- abstra_statics/dist/assets/{PhClockCounterClockwise.vue.dae2e135.js → PhClockCounterClockwise.vue.f1f419a1.js} +2 -2
- abstra_statics/dist/assets/{PhCopy.vue.71703533.js → PhCopy.vue.e59eaa52.js} +2 -2
- abstra_statics/dist/assets/{PhCopySimple.vue.369eb629.js → PhCopySimple.vue.11467aaf.js} +2 -2
- abstra_statics/dist/assets/{PhCube.vue.f8549a9b.js → PhCube.vue.e29a6bae.js} +2 -2
- abstra_statics/dist/assets/PhDatabase.vue.e22926ed.js +2 -0
- abstra_statics/dist/assets/{PhDotsThreeVertical.vue.9d76c4de.js → PhDotsThreeVertical.vue.fff5caa8.js} +2 -2
- abstra_statics/dist/assets/PhDownloadSimple.vue.e11671c2.js +2 -0
- abstra_statics/dist/assets/{PhFileArrowUp.vue.406b22e3.js → PhFileArrowUp.vue.9f743e50.js} +2 -2
- abstra_statics/dist/assets/{PhFilePlus.vue.b180df90.js → PhFilePlus.vue.b2e51e09.js} +2 -2
- abstra_statics/dist/assets/{PhFolderPlus.vue.b18fd061.js → PhFolderPlus.vue.8742ea4d.js} +2 -2
- abstra_statics/dist/assets/{PhGear.vue.bed38929.js → PhGear.vue.1c3eb148.js} +2 -2
- abstra_statics/dist/assets/{PhKey.vue.6ef5fdd3.js → PhKey.vue.8702106e.js} +2 -2
- abstra_statics/dist/assets/{PhPencil.vue.0fc0fcc0.js → PhPencil.vue.74eafe52.js} +2 -2
- abstra_statics/dist/assets/{PhPencilSimple.vue.0707effd.js → PhPencilSimple.vue.87355169.js} +2 -2
- abstra_statics/dist/assets/{PhRocket.vue.761192f5.js → PhRocket.vue.d4a6ad6a.js} +2 -2
- abstra_statics/dist/assets/{PhSignOut.vue.8d8dfd96.js → PhSignOut.vue.83e5f761.js} +2 -2
- abstra_statics/dist/assets/{PhSparkle.vue.18ed0427.js → PhSparkle.vue.d2009d46.js} +2 -2
- abstra_statics/dist/assets/{PhTranslate.vue.00a17a08.js → PhTranslate.vue.bec980b1.js} +2 -2
- abstra_statics/dist/assets/{PhUsersThree.vue.d69f0723.js → PhUsersThree.vue.dd23f9fb.js} +2 -2
- abstra_statics/dist/assets/{PhWarningCircle.vue.20bfeba7.js → PhWarningCircle.vue.27414f28.js} +2 -2
- abstra_statics/dist/assets/{PhWebhooksLogo.vue.58a98824.js → PhWebhooksLogo.vue.ff084558.js} +2 -2
- abstra_statics/dist/assets/{PlayerConfigProvider.ad360920.js → PlayerConfigProvider.ca40f824.js} +2 -2
- abstra_statics/dist/assets/{PlayerNavbar.97e8dee9.js → PlayerNavbar.393e1a48.js} +2 -2
- abstra_statics/dist/assets/{Project.6c4642b5.js → Project.72b53439.js} +2 -2
- abstra_statics/dist/assets/{ProjectLogin.f92a038d.js → ProjectLogin.26c92806.js} +2 -2
- abstra_statics/dist/assets/{ProjectSettings.582746dc.js → ProjectSettings.70e7668b.js} +2 -2
- abstra_statics/dist/assets/{ProjectsView.a6b3674b.js → ProjectsView.83667357.js} +2 -2
- abstra_statics/dist/assets/{SaveButton.c3ad6e9b.js → SaveButton.56d96f71.js} +2 -2
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.a29d9bc5.js +2 -0
- abstra_statics/dist/assets/{Sidebar.69f9369e.js → Sidebar.fd8a9f17.js} +2 -2
- abstra_statics/dist/assets/{Sql.cdefe5b9.js → Sql.92e57cd8.js} +4 -4
- abstra_statics/dist/assets/Steps.8d0493a8.js +2 -0
- abstra_statics/dist/assets/{TableCard.5462c89d.js → TableCard.75d256c8.js} +2 -2
- abstra_statics/dist/assets/{TableEditor.fcfa13de.js → TableEditor.de62b5ae.js} +2 -2
- abstra_statics/dist/assets/{Tables.4ee84a7c.js → Tables.f33c00ab.js} +2 -2
- abstra_statics/dist/assets/{TablesDiagram.b1d1579e.js → TablesDiagram.621aac9c.js} +3 -3
- abstra_statics/dist/assets/{TablesTabs.vue_vue_type_script_setup_true_lang.3e5206e0.js → TablesTabs.vue_vue_type_script_setup_true_lang.db87820d.js} +2 -2
- abstra_statics/dist/assets/{Tasks.fd2605bd.js → Tasks.e7e8affd.js} +2 -2
- abstra_statics/dist/assets/{UploadOutlined.64837788.js → UploadOutlined.76665096.js} +2 -2
- abstra_statics/dist/assets/{View.b144c5e3.js → View.2d181255.js} +2 -2
- abstra_statics/dist/assets/{View.vue_vue_type_script_setup_true_lang.c79117ce.js → View.vue_vue_type_script_setup_true_lang.c02bf815.js} +2 -2
- abstra_statics/dist/assets/{Watermark.c0756030.js → Watermark.34db0ee5.js} +2 -2
- abstra_statics/dist/assets/{WebEditor.774989ad.js → WebEditor.615c5ed3.js} +2 -2
- abstra_statics/dist/assets/WidgetPreview.d7362a8d.js +2 -0
- abstra_statics/dist/assets/WorkflowViewer.0a209003.css +1 -0
- abstra_statics/dist/assets/WorkflowViewer.e89c0824.js +2 -0
- abstra_statics/dist/assets/ant-design.24becb3a.js +2 -0
- abstra_statics/dist/assets/{apiKey.ee792d72.js → apiKey.846016a7.js} +2 -2
- abstra_statics/dist/assets/asyncComputed.78dd1715.js +2 -0
- abstra_statics/dist/assets/{build.6e7d77b3.js → build.8774fc90.js} +2 -2
- abstra_statics/dist/assets/colorHelpers.d8c19ea3.js +2 -0
- abstra_statics/dist/assets/{console.38bda98e.js → console.984bbe98.js} +2 -2
- abstra_statics/dist/assets/constants.3b7395d7.js +2 -0
- abstra_statics/dist/assets/contracts.generated.31740563.js +2 -0
- abstra_statics/dist/assets/{cssMode.408206bf.js → cssMode.f4a00eca.js} +2 -2
- abstra_statics/dist/assets/datetime.01b86df2.js +2 -0
- abstra_statics/dist/assets/dayjs.26942e0e.js +2 -0
- abstra_statics/dist/assets/editor.a6369f16.js +2 -0
- abstra_statics/dist/assets/editor.main.a0763b31.js +2 -0
- abstra_statics/dist/assets/fetch.33e85d9b.js +2 -0
- abstra_statics/dist/assets/{files.1c1692f5.js → files.c547743b.js} +2 -2
- abstra_statics/dist/assets/{folder.1b74b12c.js → folder.57131245.js} +2 -2
- abstra_statics/dist/assets/{freemarker2.e62e067c.js → freemarker2.7a4cfae0.js} +2 -2
- abstra_statics/dist/assets/{handlebars.604fc901.js → handlebars.db4a27de.js} +2 -2
- abstra_statics/dist/assets/{html.c02f177e.js → html.f4b3970c.js} +2 -2
- abstra_statics/dist/assets/{htmlMode.64078e03.js → htmlMode.631923d5.js} +2 -2
- abstra_statics/dist/assets/index.0a1e5d8b.js +2 -0
- abstra_statics/dist/assets/{index.015caad7.js → index.12c03275.js} +2 -2
- abstra_statics/dist/assets/{index.b91afb03.js → index.2141f0e8.js} +2 -2
- abstra_statics/dist/assets/{index.82590a75.js → index.2f74579e.js} +2 -2
- abstra_statics/dist/assets/{index.bec0ecd0.js → index.30fbc3f5.js} +2 -2
- abstra_statics/dist/assets/{index.a12eba98.js → index.6f45b384.js} +5 -5
- abstra_statics/dist/assets/{index.2ec95eae.js → index.7f04c017.js} +2 -2
- abstra_statics/dist/assets/{index.82842143.js → index.8e10d0e4.js} +2 -2
- abstra_statics/dist/assets/{index.b72cb2b3.js → index.fb17f22c.js} +2 -2
- abstra_statics/dist/assets/{javascript.57026f87.js → javascript.b2197abc.js} +3 -3
- abstra_statics/dist/assets/{jsonMode.9b45b375.js → jsonMode.8f2810a6.js} +2 -2
- abstra_statics/dist/assets/{jwt-decode.c5760184.css → jwt-decode.cfe2994b.css} +1 -1
- abstra_statics/dist/assets/{jwt-decode.esm.3348bca5.js → jwt-decode.esm.5ee65524.js} +88 -54
- abstra_statics/dist/assets/{linters.903f3240.js → linters.7d520e27.js} +2 -2
- abstra_statics/dist/assets/{liquid.233d5164.js → liquid.d3e68b2e.js} +3 -3
- abstra_statics/dist/assets/{member.d878cf3f.js → member.0ebe904c.js} +2 -2
- abstra_statics/dist/assets/{metadata.9f7495db.js → metadata.db332d21.js} +2 -2
- abstra_statics/dist/assets/{omniChatStore.40ad0b1b.js → omniChatStore.cf2158f0.js} +2 -2
- abstra_statics/dist/assets/{organization.8f08e075.js → organization.23b0aa74.js} +2 -2
- abstra_statics/dist/assets/{os.8ffdbf05.js → os.e0510e90.js} +2 -2
- abstra_statics/dist/assets/player.78bcc85c.js +2 -0
- abstra_statics/dist/assets/{plotly.min.da87d61b.js → plotly.min.f771780a.js} +2 -2
- abstra_statics/dist/assets/polling.5339a00f.js +2 -0
- abstra_statics/dist/assets/{project.2483de10.js → project.afe4bf99.js} +2 -2
- abstra_statics/dist/assets/{python.1bdbd404.js → python.d8c220ed.js} +3 -3
- abstra_statics/dist/assets/{razor.be821b87.js → razor.97fa5198.js} +3 -3
- abstra_statics/dist/assets/{record.a108da5a.js → record.dd367e66.js} +2 -2
- abstra_statics/dist/assets/redirect.970a0b6b.js +2 -0
- abstra_statics/dist/assets/{repository.48119e01.js → repository.214607cb.js} +2 -2
- abstra_statics/dist/assets/{repository.353e892d.js → repository.c874615c.js} +2 -2
- abstra_statics/dist/assets/{repository.677ca13c.js → repository.d889eafa.js} +2 -2
- abstra_statics/dist/assets/{router.c6e27700.js → router.9781de48.js} +5 -5
- abstra_statics/dist/assets/router.e3b4de3c.js +2 -0
- abstra_statics/dist/assets/{string.998fa621.js → string.0d721ad6.js} +2 -2
- abstra_statics/dist/assets/{tables.9701f90c.js → tables.45712b3f.js} +2 -2
- abstra_statics/dist/assets/tasksController.538cacf5.js +4 -0
- abstra_statics/dist/assets/{toggleHighContrast.23d5a1ab.js → toggleHighContrast.daf44fef.js} +7 -7
- abstra_statics/dist/assets/{tsMode.4558d65a.js → tsMode.b785363f.js} +2 -2
- abstra_statics/dist/assets/{typescript.4445d2fa.js → typescript.8bb42736.js} +3 -3
- abstra_statics/dist/assets/url.804625c6.js +2 -0
- abstra_statics/dist/assets/{useCodebaseEvents.6ebbc5a2.js → useCodebaseEvents.e9e5d343.js} +2 -2
- abstra_statics/dist/assets/useTables.2441f2b4.js +2 -0
- abstra_statics/dist/assets/userStore.9eb65729.js +2 -0
- abstra_statics/dist/assets/uuid.bc394306.js +2 -0
- abstra_statics/dist/assets/{vue-flow-background.f1022925.js → vue-flow-background.a4e5e1cd.js} +2 -2
- abstra_statics/dist/assets/{vue-flow-core.0de753a6.js → vue-flow-core.bc9175da.js} +2 -2
- abstra_statics/dist/assets/{vue-quill.esm-bundler.8f4ad2b3.js → vue-quill.esm-bundler.12c58800.js} +2 -2
- abstra_statics/dist/assets/{workspaceStore.5d3f2aec.js → workspaceStore.18d1ed9a.js} +2 -2
- abstra_statics/dist/assets/{xml.8a25758b.js → xml.1dacd023.js} +3 -3
- abstra_statics/dist/assets/{yaml.e466330b.js → yaml.e841ac1c.js} +3 -3
- abstra_statics/dist/console.html +15 -15
- abstra_statics/dist/editor.html +14 -14
- abstra_statics/dist/player.html +10 -10
- abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.1035457c.js +0 -2
- abstra_statics/dist/assets/App.9ab9cabb.js +0 -2
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.6713c9c9.js +0 -2
- abstra_statics/dist/assets/BaseLayout.28c01b5b.js +0 -2
- abstra_statics/dist/assets/Billing.f9062d88.js +0 -2
- abstra_statics/dist/assets/CrudView.57e8b29a.js +0 -2
- abstra_statics/dist/assets/EnvVars.883a4a57.js +0 -2
- abstra_statics/dist/assets/ExclamationCircleOutlined.2441b96e.js +0 -2
- abstra_statics/dist/assets/Form.5d562f15.js +0 -2
- abstra_statics/dist/assets/Form.7493bc0a.css +0 -1
- abstra_statics/dist/assets/FormRunner.2b1b3c45.js +0 -2
- abstra_statics/dist/assets/Home.8502aa41.js +0 -2
- abstra_statics/dist/assets/LoadingContainer.ac03ea28.js +0 -2
- abstra_statics/dist/assets/LoadingOutlined.4c40acc4.js +0 -2
- abstra_statics/dist/assets/Login.8bd6a07a.js +0 -2
- abstra_statics/dist/assets/Logo.fc8ace6c.js +0 -2
- abstra_statics/dist/assets/Main.e6b2d2d5.js +0 -2
- abstra_statics/dist/assets/MockForm.e410c2c1.css +0 -1
- abstra_statics/dist/assets/Navbar.24019fd6.js +0 -2
- abstra_statics/dist/assets/NewEditor.2b6f4ed3.js +0 -8
- abstra_statics/dist/assets/OidcLoginCallback.987cebba.js +0 -2
- abstra_statics/dist/assets/OidcLogoutCallback.6c00d878.js +0 -2
- abstra_statics/dist/assets/Organization.0ac1bf79.js +0 -2
- abstra_statics/dist/assets/PhDatabase.vue.0d3246d7.js +0 -2
- abstra_statics/dist/assets/PhDownloadSimple.vue.21156b6d.js +0 -2
- abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.62178939.js +0 -2
- abstra_statics/dist/assets/Steps.82252fc0.js +0 -2
- abstra_statics/dist/assets/WidgetPreview.4fd6afc0.js +0 -2
- abstra_statics/dist/assets/WorkflowViewer.2666936e.js +0 -2
- abstra_statics/dist/assets/WorkflowViewer.3b6aee8e.css +0 -1
- abstra_statics/dist/assets/ant-design.b3eefa58.js +0 -2
- abstra_statics/dist/assets/asyncComputed.c73d027a.js +0 -2
- abstra_statics/dist/assets/colorHelpers.5ee17d14.js +0 -2
- abstra_statics/dist/assets/constants.be8ad36c.js +0 -2
- abstra_statics/dist/assets/contracts.generated.f01de5a3.js +0 -2
- abstra_statics/dist/assets/datetime.a6d58ce1.js +0 -2
- abstra_statics/dist/assets/dayjs.703ebc20.js +0 -2
- abstra_statics/dist/assets/editor.a77b56bd.js +0 -2
- abstra_statics/dist/assets/editor.main.a1ebf0ab.js +0 -2
- abstra_statics/dist/assets/fetch.cd29ef4c.js +0 -2
- abstra_statics/dist/assets/index.5197afb2.js +0 -2
- abstra_statics/dist/assets/player.7112583e.js +0 -2
- abstra_statics/dist/assets/polling.f547718c.js +0 -2
- abstra_statics/dist/assets/redirect.eedb2bf6.js +0 -2
- abstra_statics/dist/assets/router.c7abfb0c.js +0 -2
- abstra_statics/dist/assets/tasksController.5db769f7.js +0 -4
- abstra_statics/dist/assets/url.5d02a63f.js +0 -2
- abstra_statics/dist/assets/useTables.4d5edd80.js +0 -2
- abstra_statics/dist/assets/userStore.34b8f1eb.js +0 -2
- abstra_statics/dist/assets/uuid.6980e2bb.js +0 -2
- {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/WHEEL +0 -0
- {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/entry_points.txt +0 -0
- {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/top_level.txt +0 -0
|
@@ -368,6 +368,48 @@ class WorkflowController:
|
|
|
368
368
|
- Changes are persisted immediately to the project configuration
|
|
369
369
|
- Use with caution as it can completely restructure the workflow
|
|
370
370
|
"""
|
|
371
|
+
# Deduplicate transitions from frontend to prevent corrupted state
|
|
372
|
+
# This handles cases where the client may send duplicate transitions
|
|
373
|
+
# Rules:
|
|
374
|
+
# 1. No duplicate IDs (keep first occurrence)
|
|
375
|
+
# 2. No duplicate source→target pairs (keep last occurrence as an update)
|
|
376
|
+
# 3. Allow bidirectional: a→b and b→a are both valid
|
|
377
|
+
if "transitions" in workflow_state_dto:
|
|
378
|
+
original_count = len(workflow_state_dto["transitions"])
|
|
379
|
+
|
|
380
|
+
# Use dict to track transitions by (source, target) pair
|
|
381
|
+
# This ensures only one transition per direction
|
|
382
|
+
transitions_by_direction = {}
|
|
383
|
+
|
|
384
|
+
for transition in workflow_state_dto["transitions"]:
|
|
385
|
+
source_id = transition.get("sourceStageId")
|
|
386
|
+
target_id = transition.get("targetStageId")
|
|
387
|
+
|
|
388
|
+
# Skip invalid transitions
|
|
389
|
+
if not source_id or not target_id:
|
|
390
|
+
continue
|
|
391
|
+
|
|
392
|
+
# Key by (source, target) to enforce one transition per direction
|
|
393
|
+
# Note: (a, b) != (b, a), so bidirectional edges are allowed
|
|
394
|
+
direction_key = (source_id, target_id)
|
|
395
|
+
|
|
396
|
+
# Later occurrences replace earlier ones (update semantics)
|
|
397
|
+
transitions_by_direction[direction_key] = transition
|
|
398
|
+
|
|
399
|
+
workflow_state_dto["transitions"] = list(transitions_by_direction.values())
|
|
400
|
+
|
|
401
|
+
# Log if duplicates were found and removed
|
|
402
|
+
deduped_count = len(workflow_state_dto["transitions"])
|
|
403
|
+
if original_count > deduped_count:
|
|
404
|
+
from abstra_internals.logger import AbstraLogger
|
|
405
|
+
|
|
406
|
+
removed_count = original_count - deduped_count
|
|
407
|
+
AbstraLogger.warning(
|
|
408
|
+
f"Removed {removed_count} duplicate transitions from workflow update "
|
|
409
|
+
f"(had {original_count}, now {deduped_count}). "
|
|
410
|
+
f"Multiple transitions with same source→target were treated as updates."
|
|
411
|
+
)
|
|
412
|
+
|
|
371
413
|
project = self.repos.project.load(include_disabled_stages=True)
|
|
372
414
|
|
|
373
415
|
for stage_dto in workflow_state_dto["stages"]:
|
|
@@ -4,6 +4,7 @@ from typing import Dict, List, Optional, TypeVar, Union
|
|
|
4
4
|
from abstra_internals.contracts_generated import (
|
|
5
5
|
CloudApiCliModelsBankStatementResponse,
|
|
6
6
|
CloudApiCliModelsBoletoResponse,
|
|
7
|
+
CloudApiCliModelsInvoiceResponse,
|
|
7
8
|
CloudApiCliModelsNfeResponse,
|
|
8
9
|
CloudApiCliModelsNfseResponse,
|
|
9
10
|
CloudApiCliModelsUsDriverLicenseResponse,
|
|
@@ -174,6 +175,74 @@ def parse_nfe(document_path: Union["Path", str]) -> CloudApiCliModelsNfeResponse
|
|
|
174
175
|
return CloudApiCliModelsNfeResponse.from_dict(data)
|
|
175
176
|
|
|
176
177
|
|
|
178
|
+
def parse_invoice(
|
|
179
|
+
document_path: Union["Path", str],
|
|
180
|
+
) -> CloudApiCliModelsInvoiceResponse:
|
|
181
|
+
"""
|
|
182
|
+
Parse an invoice document using AI-powered OCR to extract comprehensive invoice information including supplier and receiver details, financial data, shipping information, and line items.
|
|
183
|
+
|
|
184
|
+
The parser extracts 35+ fields including:
|
|
185
|
+
|
|
186
|
+
**Supplier Information:**
|
|
187
|
+
- supplier_name: Name of the supplier/vendor
|
|
188
|
+
- supplier_address: Supplier's full address
|
|
189
|
+
- supplier_email: Supplier's email address
|
|
190
|
+
- supplier_phone: Supplier's phone number
|
|
191
|
+
- supplier_tax_id: Supplier's tax identification number
|
|
192
|
+
- supplier_registration: Supplier's registration number
|
|
193
|
+
- supplier_iban: Supplier's IBAN for payments
|
|
194
|
+
- supplier_payment_ref: Payment reference number
|
|
195
|
+
- supplier_website: Supplier's website URL
|
|
196
|
+
|
|
197
|
+
**Receiver Information:**
|
|
198
|
+
- receiver_name: Name of the receiver/customer
|
|
199
|
+
- receiver_address: Receiver's full address
|
|
200
|
+
- receiver_email: Receiver's email address
|
|
201
|
+
- receiver_phone: Receiver's phone number
|
|
202
|
+
- receiver_tax_id: Receiver's tax identification number
|
|
203
|
+
- receiver_website: Receiver's website URL
|
|
204
|
+
|
|
205
|
+
**Financial Information:**
|
|
206
|
+
- invoice_id: Invoice number/identifier
|
|
207
|
+
- invoice_date: Date the invoice was issued
|
|
208
|
+
- due_date: Payment due date
|
|
209
|
+
- total_amount: Total invoice amount
|
|
210
|
+
- net_amount: Net amount (after tax/discount)
|
|
211
|
+
- total_tax_amount: Total tax amount
|
|
212
|
+
- freight_amount: Freight/shipping cost
|
|
213
|
+
- amount_paid_since_last_invoice: Amount paid since last invoice
|
|
214
|
+
- currency: Currency code (e.g., USD, EUR, BRL)
|
|
215
|
+
- currency_exchange_rate: Exchange rate if applicable
|
|
216
|
+
- payment_terms: Payment terms and conditions
|
|
217
|
+
|
|
218
|
+
**Shipping Information:**
|
|
219
|
+
- ship_from_name: Ship-from party name
|
|
220
|
+
- ship_from_address: Ship-from address
|
|
221
|
+
- ship_to_name: Ship-to party name
|
|
222
|
+
- ship_to_address: Ship-to destination address
|
|
223
|
+
- remit_to_name: Remit-to party name
|
|
224
|
+
- remit_to_address: Remit-to address for payment
|
|
225
|
+
- carrier: Shipping carrier name
|
|
226
|
+
- delivery_date: Expected or actual delivery date
|
|
227
|
+
|
|
228
|
+
**Other Information:**
|
|
229
|
+
- purchase_order: Purchase order number reference
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
document_path (Union[Path, str]): The path to the invoice document to be parsed.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
dict: The parsed invoice data.
|
|
236
|
+
|
|
237
|
+
Raises:
|
|
238
|
+
ValueError: If document path is invalid or parsing fails.
|
|
239
|
+
"""
|
|
240
|
+
data = SDKContextStore.get_by_thread().ai_sdk.parse_document(
|
|
241
|
+
document_path, "invoice"
|
|
242
|
+
)
|
|
243
|
+
return CloudApiCliModelsInvoiceResponse.from_dict(data)
|
|
244
|
+
|
|
245
|
+
|
|
177
246
|
def parse_boleto(document_path: Union["Path", str]) -> CloudApiCliModelsBoletoResponse:
|
|
178
247
|
"""
|
|
179
248
|
Parse a Brazilian Boleto (bank slip) document using AI-powered OCR for automated payment processing.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import shutil
|
|
2
2
|
import subprocess
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import List, Optional, Tuple
|
|
4
|
+
from typing import Dict, List, Optional, Tuple
|
|
5
5
|
|
|
6
6
|
from abstra_internals.environment import REMOTE_NAME
|
|
7
7
|
|
|
@@ -19,7 +19,10 @@ class NativeGitRepository(GitRepositoryInterface):
|
|
|
19
19
|
self._git_available = None
|
|
20
20
|
|
|
21
21
|
def _run_git_command(
|
|
22
|
-
self,
|
|
22
|
+
self,
|
|
23
|
+
command: List[str],
|
|
24
|
+
cwd: Optional[Path] = None,
|
|
25
|
+
input: Optional[str] = None,
|
|
23
26
|
) -> Tuple[bool, str, str]:
|
|
24
27
|
"""Run a git command and return success, stdout, stderr"""
|
|
25
28
|
try:
|
|
@@ -27,7 +30,12 @@ class NativeGitRepository(GitRepositoryInterface):
|
|
|
27
30
|
cwd = self.working_directory
|
|
28
31
|
|
|
29
32
|
result = subprocess.run(
|
|
30
|
-
["git"] + command,
|
|
33
|
+
["git"] + command,
|
|
34
|
+
cwd=cwd,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
timeout=30,
|
|
38
|
+
input=input,
|
|
31
39
|
)
|
|
32
40
|
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
|
|
33
41
|
except (
|
|
@@ -582,3 +590,82 @@ class NativeGitRepository(GitRepositoryInterface):
|
|
|
582
590
|
return "<<<<<<< " in output or "=======" in output or ">>>>>>> " in output
|
|
583
591
|
|
|
584
592
|
return False
|
|
593
|
+
|
|
594
|
+
def check_ignore(self, path: Path) -> bool:
|
|
595
|
+
"""
|
|
596
|
+
Check if a path should be ignored according to .gitignore rules.
|
|
597
|
+
Uses git check-ignore for accurate gitignore handling.
|
|
598
|
+
"""
|
|
599
|
+
if not self.is_git_repository():
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
try:
|
|
603
|
+
# Get path relative to working directory
|
|
604
|
+
try:
|
|
605
|
+
relative_path = path.relative_to(self.working_directory)
|
|
606
|
+
except ValueError:
|
|
607
|
+
# Path is outside working directory, not ignored
|
|
608
|
+
return False
|
|
609
|
+
|
|
610
|
+
# Use git check-ignore -q (quiet mode, exit code only)
|
|
611
|
+
success, _, _ = self._run_git_command(
|
|
612
|
+
["check-ignore", "-q", str(relative_path)]
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Exit code 0 means the path is ignored
|
|
616
|
+
return success
|
|
617
|
+
|
|
618
|
+
except Exception:
|
|
619
|
+
return False
|
|
620
|
+
|
|
621
|
+
def check_ignore_batch(self, paths: List[Path]) -> Dict[Path, bool]:
|
|
622
|
+
"""
|
|
623
|
+
Check multiple paths at once using git check-ignore --stdin.
|
|
624
|
+
Much more efficient than checking one path at a time.
|
|
625
|
+
"""
|
|
626
|
+
if not self.is_git_repository():
|
|
627
|
+
return {path: False for path in paths}
|
|
628
|
+
|
|
629
|
+
results = {}
|
|
630
|
+
relative_paths = []
|
|
631
|
+
path_map = {}
|
|
632
|
+
|
|
633
|
+
# Convert to relative paths
|
|
634
|
+
for path in paths:
|
|
635
|
+
try:
|
|
636
|
+
rel_path = path.relative_to(self.working_directory)
|
|
637
|
+
rel_path_str = str(rel_path)
|
|
638
|
+
relative_paths.append(rel_path_str)
|
|
639
|
+
path_map[rel_path_str] = path
|
|
640
|
+
except ValueError:
|
|
641
|
+
# Path outside working directory, not ignored
|
|
642
|
+
results[path] = False
|
|
643
|
+
|
|
644
|
+
if not relative_paths:
|
|
645
|
+
return results
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
# Use git check-ignore with stdin for batch processing
|
|
649
|
+
input_data = "\n".join(relative_paths)
|
|
650
|
+
success, stdout, _ = self._run_git_command(
|
|
651
|
+
["check-ignore", "--stdin"],
|
|
652
|
+
cwd=self.working_directory,
|
|
653
|
+
input=input_data,
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
# git check-ignore --stdin returns the paths that ARE ignored in stdout
|
|
657
|
+
ignored_paths_set = (
|
|
658
|
+
set(stdout.strip().split("\n")) if stdout.strip() else set()
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
# Map results back to original paths
|
|
662
|
+
for rel_path_str, path in path_map.items():
|
|
663
|
+
results[path] = rel_path_str in ignored_paths_set
|
|
664
|
+
|
|
665
|
+
except Exception:
|
|
666
|
+
# On error, mark all remaining as not ignored
|
|
667
|
+
for path in paths:
|
|
668
|
+
if path not in results:
|
|
669
|
+
results[path] = False
|
|
670
|
+
|
|
671
|
+
return results
|
|
@@ -271,3 +271,13 @@ class GitRepositoryInterface(ABC):
|
|
|
271
271
|
) -> Tuple[int, int]:
|
|
272
272
|
"""Calculate ahead/behind count between local and remote commits"""
|
|
273
273
|
pass
|
|
274
|
+
|
|
275
|
+
@abstractmethod
|
|
276
|
+
def check_ignore(self, path: Path) -> bool:
|
|
277
|
+
"""Check if a path should be ignored according to .gitignore rules"""
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
@abstractmethod
|
|
281
|
+
def check_ignore_batch(self, paths: List[Path]) -> Dict[Path, bool]:
|
|
282
|
+
"""Check multiple paths at once for better performance"""
|
|
283
|
+
pass
|
|
@@ -17,6 +17,8 @@ class AddEnvToGitIgnore(LinterFix):
|
|
|
17
17
|
abstraignore_file = Settings.root_path / GITIGNORE_FILEPATH
|
|
18
18
|
with abstraignore_file.open("a") as file:
|
|
19
19
|
file.write("\n.env")
|
|
20
|
+
# Clear cache after modifying .gitignore
|
|
21
|
+
FileSystemService.clear_gitignore_cache()
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class EnvInBundleFound(LinterIssue):
|
|
@@ -23,6 +23,7 @@ from .migration_012 import Migration012
|
|
|
23
23
|
from .migration_013 import Migration013
|
|
24
24
|
from .migration_014 import Migration014
|
|
25
25
|
from .migration_015 import Migration015
|
|
26
|
+
from .migration_016 import Migration016
|
|
26
27
|
|
|
27
28
|
MIGRATIONS: List[Type[Migration]] = [
|
|
28
29
|
Migration001,
|
|
@@ -40,6 +41,7 @@ MIGRATIONS: List[Type[Migration]] = [
|
|
|
40
41
|
Migration013,
|
|
41
42
|
Migration014,
|
|
42
43
|
Migration015,
|
|
44
|
+
Migration016,
|
|
43
45
|
]
|
|
44
46
|
|
|
45
47
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .base_migration import Migration
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Migration016(Migration):
|
|
5
|
+
@staticmethod
|
|
6
|
+
def target_version() -> str:
|
|
7
|
+
return "16.0"
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def source_version() -> str:
|
|
11
|
+
return "15.0"
|
|
12
|
+
|
|
13
|
+
def _migrate(self) -> None:
|
|
14
|
+
for form_data in self.data.get("forms", []):
|
|
15
|
+
form_data.pop("allow_restart", None)
|
|
16
|
+
form_data.pop("restart_button_text", None)
|
|
17
|
+
form_data.pop("welcome_title", None)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from unittest import TestCase
|
|
2
|
+
|
|
3
|
+
from abstra_internals.repositories.project.json_migrations.migration_016 import (
|
|
4
|
+
Migration016,
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestMigration016(TestCase):
|
|
9
|
+
def test_removes_deprecated_form_fields(self):
|
|
10
|
+
data = {
|
|
11
|
+
"forms": [
|
|
12
|
+
{
|
|
13
|
+
"id": "f1",
|
|
14
|
+
"title": "Form A",
|
|
15
|
+
"file": "form_a.py",
|
|
16
|
+
"path": "/form-a",
|
|
17
|
+
"is_initial": True,
|
|
18
|
+
"auto_start": False,
|
|
19
|
+
"allow_restart": True,
|
|
20
|
+
"restart_button_text": "Start Over",
|
|
21
|
+
"welcome_title": "Welcome!",
|
|
22
|
+
"end_message": None,
|
|
23
|
+
"start_message": None,
|
|
24
|
+
"error_message": None,
|
|
25
|
+
"timeout_message": None,
|
|
26
|
+
"start_button_text": "Begin",
|
|
27
|
+
"notification_trigger": {
|
|
28
|
+
"variable_name": "notify",
|
|
29
|
+
"enabled": False,
|
|
30
|
+
},
|
|
31
|
+
"access_control": {"is_public": True, "required_roles": []},
|
|
32
|
+
"other_field": "should remain",
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"scripts": [],
|
|
36
|
+
"jobs": [],
|
|
37
|
+
"hooks": [],
|
|
38
|
+
"version": "15.0",
|
|
39
|
+
}
|
|
40
|
+
m = Migration016(data)
|
|
41
|
+
m.apply()
|
|
42
|
+
|
|
43
|
+
self.assertEqual(m.data["version"], "16.0")
|
|
44
|
+
|
|
45
|
+
form = m.data["forms"][0]
|
|
46
|
+
self.assertNotIn("allow_restart", form)
|
|
47
|
+
self.assertNotIn("restart_button_text", form)
|
|
48
|
+
self.assertNotIn("welcome_title", form)
|
|
49
|
+
|
|
50
|
+
self.assertEqual(form["other_field"], "should remain")
|
|
51
|
+
self.assertEqual(form["title"], "Form A")
|
|
52
|
+
self.assertEqual(form["path"], "/form-a")
|
|
53
|
+
self.assertEqual(form["is_initial"], True)
|
|
54
|
+
self.assertEqual(form["auto_start"], False)
|
|
55
|
+
|
|
56
|
+
def test_handles_multiple_forms(self):
|
|
57
|
+
data = {
|
|
58
|
+
"forms": [
|
|
59
|
+
{
|
|
60
|
+
"id": "f1",
|
|
61
|
+
"title": "Form 1",
|
|
62
|
+
"file": "form_1.py",
|
|
63
|
+
"path": "/form-1",
|
|
64
|
+
"is_initial": True,
|
|
65
|
+
"auto_start": False,
|
|
66
|
+
"allow_restart": True,
|
|
67
|
+
"restart_button_text": "Restart 1",
|
|
68
|
+
"welcome_title": "Welcome 1",
|
|
69
|
+
"end_message": None,
|
|
70
|
+
"start_message": None,
|
|
71
|
+
"error_message": None,
|
|
72
|
+
"timeout_message": None,
|
|
73
|
+
"start_button_text": None,
|
|
74
|
+
"notification_trigger": {"variable_name": "n", "enabled": False},
|
|
75
|
+
"access_control": {"is_public": True, "required_roles": []},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "f2",
|
|
79
|
+
"title": "Form 2",
|
|
80
|
+
"file": "form_2.py",
|
|
81
|
+
"path": "/form-2",
|
|
82
|
+
"is_initial": False,
|
|
83
|
+
"auto_start": True,
|
|
84
|
+
"allow_restart": False,
|
|
85
|
+
"restart_button_text": None,
|
|
86
|
+
"welcome_title": "Hello",
|
|
87
|
+
"end_message": None,
|
|
88
|
+
"start_message": None,
|
|
89
|
+
"error_message": None,
|
|
90
|
+
"timeout_message": None,
|
|
91
|
+
"start_button_text": None,
|
|
92
|
+
"notification_trigger": {"variable_name": "n", "enabled": False},
|
|
93
|
+
"access_control": {"is_public": False, "required_roles": ["admin"]},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
"scripts": [],
|
|
97
|
+
"jobs": [],
|
|
98
|
+
"hooks": [],
|
|
99
|
+
"version": "15.0",
|
|
100
|
+
}
|
|
101
|
+
m = Migration016(data)
|
|
102
|
+
m.apply()
|
|
103
|
+
|
|
104
|
+
for form in m.data["forms"]:
|
|
105
|
+
self.assertNotIn("allow_restart", form)
|
|
106
|
+
self.assertNotIn("restart_button_text", form)
|
|
107
|
+
self.assertNotIn("welcome_title", form)
|
|
108
|
+
|
|
109
|
+
self.assertIn("path", form)
|
|
110
|
+
self.assertIn("is_initial", form)
|
|
111
|
+
self.assertIn("auto_start", form)
|
|
112
|
+
|
|
113
|
+
def test_handles_forms_without_deprecated_fields(self):
|
|
114
|
+
data = {
|
|
115
|
+
"forms": [
|
|
116
|
+
{
|
|
117
|
+
"id": "f1",
|
|
118
|
+
"title": "Form Without Deprecated Fields",
|
|
119
|
+
"file": "form.py",
|
|
120
|
+
"path": "/form",
|
|
121
|
+
"is_initial": True,
|
|
122
|
+
"auto_start": False,
|
|
123
|
+
"end_message": None,
|
|
124
|
+
"start_message": None,
|
|
125
|
+
"error_message": None,
|
|
126
|
+
"timeout_message": None,
|
|
127
|
+
"start_button_text": None,
|
|
128
|
+
"notification_trigger": {"variable_name": "n", "enabled": False},
|
|
129
|
+
"access_control": {"is_public": True, "required_roles": []},
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"scripts": [],
|
|
133
|
+
"jobs": [],
|
|
134
|
+
"hooks": [],
|
|
135
|
+
"version": "15.0",
|
|
136
|
+
}
|
|
137
|
+
m = Migration016(data)
|
|
138
|
+
m.apply()
|
|
139
|
+
|
|
140
|
+
self.assertEqual(m.data["version"], "16.0")
|
|
141
|
+
self.assertEqual(len(m.data["forms"]), 1)
|
|
@@ -496,11 +496,8 @@ class FormStage(StageWithFile):
|
|
|
496
496
|
auto_start: Optional[bool] = False
|
|
497
497
|
start_message: Optional[str] = None
|
|
498
498
|
error_message: Optional[str] = None
|
|
499
|
-
welcome_title: Optional[str] = None
|
|
500
|
-
allow_restart: Optional[bool] = False
|
|
501
499
|
timeout_message: Optional[str] = None
|
|
502
500
|
start_button_text: Optional[str] = None
|
|
503
|
-
restart_button_text: Optional[str] = None
|
|
504
501
|
access_control: AccessSettings = field(
|
|
505
502
|
default_factory=lambda: AccessSettings(is_public=False, required_roles=[])
|
|
506
503
|
)
|
|
@@ -541,11 +538,8 @@ class FormStage(StageWithFile):
|
|
|
541
538
|
auto_start=data["auto_start"],
|
|
542
539
|
start_message=data["start_message"],
|
|
543
540
|
error_message=data["error_message"],
|
|
544
|
-
welcome_title=data["welcome_title"],
|
|
545
|
-
allow_restart=data["allow_restart"],
|
|
546
541
|
timeout_message=data["timeout_message"],
|
|
547
542
|
start_button_text=data["start_button_text"],
|
|
548
|
-
restart_button_text=data["restart_button_text"],
|
|
549
543
|
workflow_position=(x, y),
|
|
550
544
|
is_initial=data["is_initial"],
|
|
551
545
|
workflow_transitions=[
|
|
@@ -569,22 +563,17 @@ class FormStage(StageWithFile):
|
|
|
569
563
|
|
|
570
564
|
@property
|
|
571
565
|
def browser_runner_dto(self):
|
|
572
|
-
allow_restart = self.allow_restart if self.is_initial else False
|
|
573
|
-
|
|
574
566
|
return {
|
|
575
567
|
"id": self.id,
|
|
576
568
|
"path": self.path,
|
|
577
569
|
"title": self.title,
|
|
578
570
|
"is_initial": self.is_initial,
|
|
579
571
|
"auto_start": self.auto_start,
|
|
580
|
-
"allow_restart": allow_restart,
|
|
581
572
|
"end_message": self.end_message,
|
|
582
573
|
"start_message": self.start_message,
|
|
583
574
|
"error_message": self.error_message,
|
|
584
|
-
"welcome_title": self.welcome_title,
|
|
585
575
|
"timeout_message": self.timeout_message,
|
|
586
576
|
"start_button_text": self.start_button_text,
|
|
587
|
-
"restart_button_text": self.restart_button_text,
|
|
588
577
|
}
|
|
589
578
|
|
|
590
579
|
@property
|
|
@@ -618,11 +607,8 @@ class FormStage(StageWithFile):
|
|
|
618
607
|
"auto_start",
|
|
619
608
|
"start_message",
|
|
620
609
|
"error_message",
|
|
621
|
-
"welcome_title",
|
|
622
|
-
"allow_restart",
|
|
623
610
|
"timeout_message",
|
|
624
611
|
"start_button_text",
|
|
625
|
-
"restart_button_text",
|
|
626
612
|
]:
|
|
627
613
|
setattr(self, attr, value)
|
|
628
614
|
elif attr == "file":
|
|
@@ -657,11 +643,8 @@ class FormStage(StageWithFile):
|
|
|
657
643
|
auto_start=self.auto_start or False,
|
|
658
644
|
start_message=self.start_message,
|
|
659
645
|
error_message=self.error_message,
|
|
660
|
-
welcome_title=self.welcome_title,
|
|
661
|
-
allow_restart=self.allow_restart or False,
|
|
662
646
|
timeout_message=self.timeout_message,
|
|
663
647
|
start_button_text=self.start_button_text,
|
|
664
|
-
restart_button_text=self.restart_button_text,
|
|
665
648
|
workflow_position=[self.workflow_position[0], self.workflow_position[1]],
|
|
666
649
|
transitions=[t.to_abstra_json_dto() for t in self.workflow_transitions],
|
|
667
650
|
is_initial=self.is_initial,
|
|
@@ -1222,6 +1205,50 @@ class Project:
|
|
|
1222
1205
|
self.jobs = [j for j in self.jobs if j.id != id]
|
|
1223
1206
|
self.scripts = [s for s in self.scripts if s.id != id]
|
|
1224
1207
|
|
|
1208
|
+
@staticmethod
|
|
1209
|
+
def __deduplicate_transitions(transitions: List[dict]) -> List[dict]:
|
|
1210
|
+
"""
|
|
1211
|
+
Remove duplicate transitions based on ID and target, keeping only the first occurrence.
|
|
1212
|
+
|
|
1213
|
+
This is necessary to handle corrupted project files that may have accumulated
|
|
1214
|
+
duplicate transitions over time, which can cause performance issues when
|
|
1215
|
+
loading large workflows.
|
|
1216
|
+
|
|
1217
|
+
Rules:
|
|
1218
|
+
1. No duplicate IDs (keeps first occurrence)
|
|
1219
|
+
2. No duplicate targets (keeps first occurrence - since source is implicit from stage)
|
|
1220
|
+
3. Allows bidirectional: a→b and b→a are in different stages, so both valid
|
|
1221
|
+
|
|
1222
|
+
Args:
|
|
1223
|
+
transitions: List of transition dictionaries from the JSON data
|
|
1224
|
+
(all from the same source stage)
|
|
1225
|
+
|
|
1226
|
+
Returns:
|
|
1227
|
+
List of unique transitions, preserving order
|
|
1228
|
+
"""
|
|
1229
|
+
seen_ids = set()
|
|
1230
|
+
seen_targets = set()
|
|
1231
|
+
unique_transitions = []
|
|
1232
|
+
|
|
1233
|
+
for transition in transitions:
|
|
1234
|
+
transition_id = transition.get("id")
|
|
1235
|
+
target_id = transition.get("target_id")
|
|
1236
|
+
|
|
1237
|
+
# Skip if duplicate ID
|
|
1238
|
+
if transition_id in seen_ids:
|
|
1239
|
+
continue
|
|
1240
|
+
|
|
1241
|
+
# Skip if duplicate target (same source→target)
|
|
1242
|
+
if target_id in seen_targets:
|
|
1243
|
+
continue
|
|
1244
|
+
|
|
1245
|
+
# Valid unique transition
|
|
1246
|
+
seen_ids.add(transition_id)
|
|
1247
|
+
seen_targets.add(target_id)
|
|
1248
|
+
unique_transitions.append(transition)
|
|
1249
|
+
|
|
1250
|
+
return unique_transitions
|
|
1251
|
+
|
|
1225
1252
|
@staticmethod
|
|
1226
1253
|
def __from_dict(data: dict):
|
|
1227
1254
|
target_stages = set()
|
|
@@ -1230,6 +1257,24 @@ class Project:
|
|
|
1230
1257
|
|
|
1231
1258
|
stage_keys = ["forms", "hooks", "scripts", "jobs"]
|
|
1232
1259
|
|
|
1260
|
+
# Deduplicate transitions in each stage before processing
|
|
1261
|
+
for key in stage_keys:
|
|
1262
|
+
for stage in data[key]:
|
|
1263
|
+
if "transitions" in stage:
|
|
1264
|
+
original_count = len(stage["transitions"])
|
|
1265
|
+
stage["transitions"] = Project.__deduplicate_transitions(
|
|
1266
|
+
stage["transitions"]
|
|
1267
|
+
)
|
|
1268
|
+
deduped_count = len(stage["transitions"])
|
|
1269
|
+
|
|
1270
|
+
# Log warning if duplicates were found
|
|
1271
|
+
if original_count > deduped_count:
|
|
1272
|
+
removed_count = original_count - deduped_count
|
|
1273
|
+
AbstraLogger.warning(
|
|
1274
|
+
f"Removed {removed_count} duplicate transitions from stage {stage.get('id', 'unknown')} "
|
|
1275
|
+
f"(had {original_count}, now {deduped_count})"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1233
1278
|
for key in stage_keys:
|
|
1234
1279
|
for stage in data[key]:
|
|
1235
1280
|
nodes.append(Node(id=stage["id"]))
|