abstra 3.24.2__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.
- abstra/ai.py +2 -0
- {abstra-3.24.2.dist-info → abstra-3.24.4.dist-info}/METADATA +1 -1
- {abstra-3.24.2.dist-info → abstra-3.24.4.dist-info}/RECORD +193 -190
- abstra_internals/contracts_generated.py +3737 -2560
- abstra_internals/controllers/main.py +62 -0
- abstra_internals/controllers/workflows.py +42 -0
- abstra_internals/interface/sdk/ai.py +69 -0
- abstra_internals/repositories/git/native.py +92 -5
- 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 +63 -18
- abstra_internals/repositories/project/project_test.py +279 -0
- abstra_internals/server/routes/mcp.py +1 -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.a3ba2a31.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.b1a71740.js → AbstraLogo.vue_vue_type_script_setup_true_lang.9c4020b5.js} +2 -2
- abstra_statics/dist/assets/{ApiKeys.dafa1dc2.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.8a82fa01.js → Breadcrumb.1a138bb1.js} +2 -2
- abstra_statics/dist/assets/{Builds.c3fd9633.js → Builds.f3c7442c.js} +2 -2
- abstra_statics/dist/assets/{Card.38a860a4.js → Card.0899e9af.js} +2 -2
- abstra_statics/dist/assets/{CircularLoading.217756fb.js → CircularLoading.9f9e733d.js} +2 -2
- abstra_statics/dist/assets/CloseCircleOutlined.36d9f25b.js +2 -0
- abstra_statics/dist/assets/{ConnectorsView.121898a9.js → ConnectorsView.9d99a93b.js} +2 -2
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.7061772f.js +2 -0
- abstra_statics/dist/assets/ContentLayout.da1fff81.js +2 -0
- abstra_statics/dist/assets/CrudView.5b250a71.js +2 -0
- abstra_statics/dist/assets/{DocsButton.vue_vue_type_script_setup_true_lang.a9c8118b.js → DocsButton.vue_vue_type_script_setup_true_lang.88300c0e.js} +2 -2
- abstra_statics/dist/assets/{EditorLogin.fba78ce9.js → EditorLogin.440d3dc5.js} +2 -2
- abstra_statics/dist/assets/{EditorsView.1a9ccb13.js → EditorsView.c81d5c0a.js} +2 -2
- abstra_statics/dist/assets/EnvVars.688f662d.js +2 -0
- abstra_statics/dist/assets/{Error.5bd293cc.js → Error.8fd45945.js} +2 -2
- abstra_statics/dist/assets/{ExclamationCircleOutlined.5d5c3f30.js → ExclamationCircleOutlined.ea15dcd1.js} +2 -2
- abstra_statics/dist/assets/{Files.c4ce443f.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.4eff6ce9.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.ecec1ff2.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.ada1c6c9.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.c1f01b05.js → Logs.1f1770c9.js} +2 -2
- abstra_statics/dist/assets/LogsController.61f8e22d.css +1 -0
- abstra_statics/dist/assets/LogsController.e88bddfb.js +2 -0
- abstra_statics/dist/assets/Main.a79ded11.js +2 -0
- abstra_statics/dist/assets/MockForm.025d99f9.css +1 -0
- abstra_statics/dist/assets/{MockForm.9b7a0df3.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.e3cfeb2c.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.c3de8733.js → OmniChat.3d03d97a.js} +2 -2
- abstra_statics/dist/assets/{OnboardingView.6cda1bc5.js → OnboardingView.4b747af0.js} +2 -2
- abstra_statics/dist/assets/{Organization.c36206b7.js → Organization.aab680aa.js} +2 -2
- abstra_statics/dist/assets/Organizations.2340795a.js +2 -0
- abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.9e570570.js → PhArrowCounterClockwise.vue.d9dd0137.js} +2 -2
- abstra_statics/dist/assets/{PhArrowSquareOut.vue.bcbdb6e7.js → PhArrowSquareOut.vue.93df49c1.js} +2 -2
- abstra_statics/dist/assets/{PhClockCounterClockwise.vue.4bd682d8.js → PhClockCounterClockwise.vue.f1f419a1.js} +2 -2
- abstra_statics/dist/assets/{PhCopy.vue.29934bc2.js → PhCopy.vue.e59eaa52.js} +2 -2
- abstra_statics/dist/assets/PhCopySimple.vue.11467aaf.js +2 -0
- abstra_statics/dist/assets/{PhCube.vue.0fe2c514.js → PhCube.vue.e29a6bae.js} +2 -2
- abstra_statics/dist/assets/{PhDatabase.vue.fdfb515c.js → PhDatabase.vue.e22926ed.js} +2 -2
- abstra_statics/dist/assets/{PhDotsThreeVertical.vue.7a0e0638.js → PhDotsThreeVertical.vue.fff5caa8.js} +2 -2
- abstra_statics/dist/assets/{PhDownloadSimple.vue.f1245c40.js → PhDownloadSimple.vue.e11671c2.js} +2 -2
- abstra_statics/dist/assets/{PhFileArrowUp.vue.c292afe1.js → PhFileArrowUp.vue.9f743e50.js} +2 -2
- abstra_statics/dist/assets/{PhFilePlus.vue.c39ff1a9.js → PhFilePlus.vue.b2e51e09.js} +2 -2
- abstra_statics/dist/assets/{PhFolderPlus.vue.bc40161e.js → PhFolderPlus.vue.8742ea4d.js} +2 -2
- abstra_statics/dist/assets/{PhGear.vue.0feed515.js → PhGear.vue.1c3eb148.js} +2 -2
- abstra_statics/dist/assets/{PhKey.vue.15a9e64e.js → PhKey.vue.8702106e.js} +2 -2
- abstra_statics/dist/assets/{PhPencil.vue.a7219766.js → PhPencil.vue.74eafe52.js} +2 -2
- abstra_statics/dist/assets/{PhPencilSimple.vue.15a2b403.js → PhPencilSimple.vue.87355169.js} +2 -2
- abstra_statics/dist/assets/{PhRocket.vue.7155b91f.js → PhRocket.vue.d4a6ad6a.js} +2 -2
- abstra_statics/dist/assets/{PhSignOut.vue.2af17bd7.js → PhSignOut.vue.83e5f761.js} +2 -2
- abstra_statics/dist/assets/{PhSparkle.vue.c7f06cac.js → PhSparkle.vue.d2009d46.js} +2 -2
- abstra_statics/dist/assets/{PhTranslate.vue.2ce651a6.js → PhTranslate.vue.bec980b1.js} +2 -2
- abstra_statics/dist/assets/{PhUsersThree.vue.2942df75.js → PhUsersThree.vue.dd23f9fb.js} +2 -2
- abstra_statics/dist/assets/{PhWarningCircle.vue.05a40bc4.js → PhWarningCircle.vue.27414f28.js} +2 -2
- abstra_statics/dist/assets/{PhWebhooksLogo.vue.e4752384.js → PhWebhooksLogo.vue.ff084558.js} +2 -2
- abstra_statics/dist/assets/{PlayerConfigProvider.00af5968.js → PlayerConfigProvider.ca40f824.js} +2 -2
- abstra_statics/dist/assets/{PlayerNavbar.117f184b.js → PlayerNavbar.393e1a48.js} +2 -2
- abstra_statics/dist/assets/{Project.66111161.js → Project.72b53439.js} +2 -2
- abstra_statics/dist/assets/{ProjectLogin.d9bb1f86.js → ProjectLogin.26c92806.js} +2 -2
- abstra_statics/dist/assets/ProjectSettings.70e7668b.js +2 -0
- abstra_statics/dist/assets/ProjectsView.83667357.js +2 -0
- abstra_statics/dist/assets/{SaveButton.c3f2a4bb.js → SaveButton.56d96f71.js} +2 -2
- abstra_statics/dist/assets/{ScrollArea.vue_vue_type_script_setup_true_lang.cb5567cd.js → ScrollArea.vue_vue_type_script_setup_true_lang.a29d9bc5.js} +2 -2
- abstra_statics/dist/assets/{Sidebar.1c4e35be.js → Sidebar.fd8a9f17.js} +2 -2
- abstra_statics/dist/assets/{Sql.8d31ec23.js → Sql.92e57cd8.js} +3 -3
- abstra_statics/dist/assets/Steps.8d0493a8.js +2 -0
- abstra_statics/dist/assets/TableCard.75d256c8.js +2 -0
- abstra_statics/dist/assets/{TableEditor.ba7a8b6a.js → TableEditor.de62b5ae.js} +2 -2
- abstra_statics/dist/assets/{Tables.113960f2.js → Tables.f33c00ab.js} +2 -2
- abstra_statics/dist/assets/{TablesDiagram.8e6d1e89.js → TablesDiagram.621aac9c.js} +3 -3
- abstra_statics/dist/assets/{TablesTabs.vue_vue_type_script_setup_true_lang.5cc96b0d.js → TablesTabs.vue_vue_type_script_setup_true_lang.db87820d.js} +2 -2
- abstra_statics/dist/assets/{Tasks.90846020.js → Tasks.e7e8affd.js} +2 -2
- abstra_statics/dist/assets/{UploadOutlined.518baf9a.js → UploadOutlined.76665096.js} +2 -2
- abstra_statics/dist/assets/{View.ded6b355.js → View.2d181255.js} +2 -2
- abstra_statics/dist/assets/{View.vue_vue_type_script_setup_true_lang.285b5e2c.js → View.vue_vue_type_script_setup_true_lang.c02bf815.js} +2 -2
- abstra_statics/dist/assets/{Watermark.5071a4b2.js → Watermark.34db0ee5.js} +2 -2
- abstra_statics/dist/assets/{WebEditor.18ece735.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.846016a7.js +2 -0
- abstra_statics/dist/assets/asyncComputed.78dd1715.js +2 -0
- abstra_statics/dist/assets/{build.a8637e29.js → build.8774fc90.js} +2 -2
- abstra_statics/dist/assets/{colorHelpers.8ba18214.js → colorHelpers.d8c19ea3.js} +2 -2
- abstra_statics/dist/assets/{console.2a5ed51a.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.6c4ccf50.js → cssMode.f4a00eca.js} +2 -2
- abstra_statics/dist/assets/{datetime.89495471.js → datetime.01b86df2.js} +2 -2
- 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.8999afd5.js → files.c547743b.js} +2 -2
- abstra_statics/dist/assets/{folder.81ef8619.js → folder.57131245.js} +2 -2
- abstra_statics/dist/assets/{freemarker2.559f77f2.js → freemarker2.7a4cfae0.js} +2 -2
- abstra_statics/dist/assets/{handlebars.8d101b7c.js → handlebars.db4a27de.js} +2 -2
- abstra_statics/dist/assets/{html.b3e7d3ab.js → html.f4b3970c.js} +3 -3
- abstra_statics/dist/assets/{htmlMode.2305b1bb.js → htmlMode.631923d5.js} +2 -2
- abstra_statics/dist/assets/{index.bc97991a.js → index.0a1e5d8b.js} +2 -2
- abstra_statics/dist/assets/{index.da4f9d54.js → index.12c03275.js} +2 -2
- abstra_statics/dist/assets/{index.e5cb42a1.js → index.2141f0e8.js} +2 -2
- abstra_statics/dist/assets/{index.f2beb20d.js → index.2f74579e.js} +2 -2
- abstra_statics/dist/assets/{index.b74c262c.js → index.30fbc3f5.js} +2 -2
- abstra_statics/dist/assets/{index.f6171691.js → index.6f45b384.js} +5 -5
- abstra_statics/dist/assets/{index.d809956c.js → index.7f04c017.js} +2 -2
- abstra_statics/dist/assets/{index.71eb83f3.js → index.8e10d0e4.js} +2 -2
- abstra_statics/dist/assets/{index.90acf038.js → index.fb17f22c.js} +2 -2
- abstra_statics/dist/assets/{javascript.3000fc25.js → javascript.b2197abc.js} +3 -3
- abstra_statics/dist/assets/{jsonMode.7bbb508d.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.c9c37cdc.js → jwt-decode.esm.5ee65524.js} +133 -99
- abstra_statics/dist/assets/{linters.7fec18d9.js → linters.7d520e27.js} +2 -2
- abstra_statics/dist/assets/{liquid.b4ac9aaf.js → liquid.d3e68b2e.js} +3 -3
- abstra_statics/dist/assets/member.0ebe904c.js +2 -0
- abstra_statics/dist/assets/{metadata.e627ddda.js → metadata.db332d21.js} +2 -2
- abstra_statics/dist/assets/{omniChatStore.508e8ece.js → omniChatStore.cf2158f0.js} +2 -2
- abstra_statics/dist/assets/{organization.cd03f9a8.js → organization.23b0aa74.js} +2 -2
- abstra_statics/dist/assets/{os.f08724fb.js → os.e0510e90.js} +2 -2
- abstra_statics/dist/assets/player.78bcc85c.js +2 -0
- abstra_statics/dist/assets/{plotly.min.50ebb925.js → plotly.min.f771780a.js} +2 -2
- abstra_statics/dist/assets/polling.5339a00f.js +2 -0
- abstra_statics/dist/assets/{project.9a068e8d.js → project.afe4bf99.js} +2 -2
- abstra_statics/dist/assets/{python.51a7c648.js → python.d8c220ed.js} +3 -3
- abstra_statics/dist/assets/{razor.99323f5f.js → razor.97fa5198.js} +2 -2
- abstra_statics/dist/assets/{record.a33d29b1.js → record.dd367e66.js} +2 -2
- abstra_statics/dist/assets/{redirect.42bf4f0a.js → redirect.970a0b6b.js} +2 -2
- abstra_statics/dist/assets/{repository.94fb77c7.js → repository.214607cb.js} +2 -2
- abstra_statics/dist/assets/{repository.c0d70cb2.js → repository.c874615c.js} +2 -2
- abstra_statics/dist/assets/{repository.5190b94f.js → repository.d889eafa.js} +2 -2
- abstra_statics/dist/assets/{router.a8616541.js → router.9781de48.js} +3 -3
- abstra_statics/dist/assets/router.e3b4de3c.js +2 -0
- abstra_statics/dist/assets/{string.8fab6b53.js → string.0d721ad6.js} +2 -2
- abstra_statics/dist/assets/{tables.2e1c934b.js → tables.45712b3f.js} +2 -2
- abstra_statics/dist/assets/tasksController.538cacf5.js +4 -0
- abstra_statics/dist/assets/{toggleHighContrast.6544a728.js → toggleHighContrast.daf44fef.js} +7 -7
- abstra_statics/dist/assets/{tsMode.922e04bb.js → tsMode.b785363f.js} +2 -2
- abstra_statics/dist/assets/{typescript.1b4f8286.js → typescript.8bb42736.js} +2 -2
- abstra_statics/dist/assets/url.804625c6.js +2 -0
- abstra_statics/dist/assets/{useCodebaseEvents.ffe057d1.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.3e9183ec.js → vue-flow-background.a4e5e1cd.js} +2 -2
- abstra_statics/dist/assets/{vue-flow-core.41c647da.js → vue-flow-core.bc9175da.js} +2 -2
- abstra_statics/dist/assets/{vue-quill.esm-bundler.36e79a95.js → vue-quill.esm-bundler.12c58800.js} +2 -2
- abstra_statics/dist/assets/{workspaceStore.5a435520.js → workspaceStore.18d1ed9a.js} +2 -2
- abstra_statics/dist/assets/{xml.c1692f52.js → xml.1dacd023.js} +3 -3
- abstra_statics/dist/assets/{yaml.244444c1.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/App.45396b70.js +0 -2
- abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.db5596a1.js +0 -2
- abstra_statics/dist/assets/BaseLayout.135a51d9.js +0 -2
- abstra_statics/dist/assets/Billing.622c9155.js +0 -2
- abstra_statics/dist/assets/CloseCircleOutlined.4c4707d8.js +0 -2
- abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.502d779a.js +0 -2
- abstra_statics/dist/assets/ContentLayout.b1c94c2b.js +0 -2
- abstra_statics/dist/assets/CrudView.75a430a4.js +0 -2
- abstra_statics/dist/assets/EnvVars.d77957ea.js +0 -2
- abstra_statics/dist/assets/Form.7493bc0a.css +0 -1
- abstra_statics/dist/assets/Form.a4787001.js +0 -2
- abstra_statics/dist/assets/FormRunner.73650f9e.js +0 -2
- abstra_statics/dist/assets/Home.19a2303b.js +0 -2
- abstra_statics/dist/assets/LoadingContainer.97fa8f2e.js +0 -2
- abstra_statics/dist/assets/LoadingOutlined.e309ab16.js +0 -2
- abstra_statics/dist/assets/Login.632cada3.js +0 -2
- abstra_statics/dist/assets/Logo.d77d5637.js +0 -2
- abstra_statics/dist/assets/LogsController.0ff97ed4.css +0 -1
- abstra_statics/dist/assets/LogsController.2dceb3d3.js +0 -2
- abstra_statics/dist/assets/Main.44b7640e.js +0 -2
- abstra_statics/dist/assets/MockForm.e410c2c1.css +0 -1
- abstra_statics/dist/assets/Navbar.0951ed6d.js +0 -2
- abstra_statics/dist/assets/NewEditor.d65a400f.js +0 -8
- abstra_statics/dist/assets/OidcLoginCallback.66b0f38a.js +0 -2
- abstra_statics/dist/assets/OidcLogoutCallback.48d8429a.js +0 -2
- abstra_statics/dist/assets/Organizations.2b1c6c65.js +0 -2
- abstra_statics/dist/assets/PhCopySimple.vue.0241af8c.js +0 -2
- abstra_statics/dist/assets/ProjectSettings.f8c6f60a.js +0 -2
- abstra_statics/dist/assets/ProjectsView.32f6ccff.js +0 -2
- abstra_statics/dist/assets/Steps.687763a5.js +0 -2
- abstra_statics/dist/assets/TableCard.8c99a870.js +0 -2
- abstra_statics/dist/assets/WidgetPreview.88a4f27f.js +0 -2
- abstra_statics/dist/assets/WorkflowViewer.3b6aee8e.css +0 -1
- abstra_statics/dist/assets/WorkflowViewer.778c401d.js +0 -2
- abstra_statics/dist/assets/ant-design.4efc9ccd.js +0 -2
- abstra_statics/dist/assets/apiKey.bd946d8c.js +0 -2
- abstra_statics/dist/assets/asyncComputed.7bc1692e.js +0 -2
- abstra_statics/dist/assets/constants.534f67bc.js +0 -2
- abstra_statics/dist/assets/contracts.generated.8ad36e63.js +0 -2
- abstra_statics/dist/assets/dayjs.304f38f8.js +0 -2
- abstra_statics/dist/assets/editor.c1a1bd33.js +0 -2
- abstra_statics/dist/assets/editor.main.84e237cf.js +0 -2
- abstra_statics/dist/assets/fetch.452c58e5.js +0 -2
- abstra_statics/dist/assets/member.3c12efee.js +0 -2
- abstra_statics/dist/assets/player.7362caf4.js +0 -2
- abstra_statics/dist/assets/polling.4db5ee9a.js +0 -2
- abstra_statics/dist/assets/router.4168cc71.js +0 -2
- abstra_statics/dist/assets/tasksController.1feffcfe.js +0 -4
- abstra_statics/dist/assets/url.9e033350.js +0 -2
- abstra_statics/dist/assets/useTables.5fffa3f1.js +0 -2
- abstra_statics/dist/assets/userStore.d962fba4.js +0 -2
- abstra_statics/dist/assets/uuid.8581bc03.js +0 -2
- {abstra-3.24.2.dist-info → abstra-3.24.4.dist-info}/WHEEL +0 -0
- {abstra-3.24.2.dist-info → abstra-3.24.4.dist-info}/entry_points.txt +0 -0
- {abstra-3.24.2.dist-info → abstra-3.24.4.dist-info}/top_level.txt +0 -0
|
@@ -205,3 +205,282 @@ class ProjectTests(TestCase):
|
|
|
205
205
|
self.assertEqual(p.get_next_stages_ids(form2.id), [])
|
|
206
206
|
|
|
207
207
|
self.assertEqual(p.get_previous_stages_ids(form2.id), [form1.id])
|
|
208
|
+
|
|
209
|
+
def test_deduplicate_transitions_on_load(self):
|
|
210
|
+
"""Test that duplicate transitions are removed when loading a project from JSON"""
|
|
211
|
+
# Create a project with a script that has duplicate transitions
|
|
212
|
+
script1 = ScriptStage(
|
|
213
|
+
file="script1.py",
|
|
214
|
+
id="script1",
|
|
215
|
+
is_initial=True,
|
|
216
|
+
title="Script 1",
|
|
217
|
+
workflow_position=(0, 0),
|
|
218
|
+
workflow_transitions=[],
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
script2 = ScriptStage(
|
|
222
|
+
file="script2.py",
|
|
223
|
+
id="script2",
|
|
224
|
+
is_initial=False,
|
|
225
|
+
title="Script 2",
|
|
226
|
+
workflow_position=(100, 100),
|
|
227
|
+
workflow_transitions=[],
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Add the same transition multiple times (simulating corrupted data)
|
|
231
|
+
duplicate_transition = WorkflowTransition(
|
|
232
|
+
id="transition1",
|
|
233
|
+
target_id=script2.id,
|
|
234
|
+
target_type=script2.type_name,
|
|
235
|
+
type="task",
|
|
236
|
+
task_type="test_task",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
script1.workflow_transitions.append(duplicate_transition)
|
|
240
|
+
script1.workflow_transitions.append(duplicate_transition)
|
|
241
|
+
script1.workflow_transitions.append(duplicate_transition)
|
|
242
|
+
|
|
243
|
+
project = self.project_repository.load()
|
|
244
|
+
project.add_stage(script1)
|
|
245
|
+
project.add_stage(script2)
|
|
246
|
+
self.project_repository.save(project)
|
|
247
|
+
|
|
248
|
+
# Manually add duplicates to the JSON file to simulate corrupted data
|
|
249
|
+
import json
|
|
250
|
+
|
|
251
|
+
json_path = self.project_repository.get_file_path()
|
|
252
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
253
|
+
data = json.load(f)
|
|
254
|
+
|
|
255
|
+
# Verify duplicates were saved (as expected from the in-memory structure)
|
|
256
|
+
self.assertEqual(len(data["scripts"][0]["transitions"]), 3)
|
|
257
|
+
|
|
258
|
+
# Load the project - this should deduplicate the transitions
|
|
259
|
+
loaded_project = self.project_repository.load()
|
|
260
|
+
|
|
261
|
+
loaded_script1 = loaded_project.get_script(script1.id)
|
|
262
|
+
self.assertIsNotNone(loaded_script1)
|
|
263
|
+
assert loaded_script1 is not None # Type narrowing for pyright
|
|
264
|
+
|
|
265
|
+
# Verify that only one transition remains after deduplication
|
|
266
|
+
self.assertEqual(len(loaded_script1.workflow_transitions), 1)
|
|
267
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].id, "transition1")
|
|
268
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
|
|
269
|
+
|
|
270
|
+
def test_deduplicate_multiple_unique_transitions(self):
|
|
271
|
+
"""Test that unique transitions are preserved while duplicates are removed"""
|
|
272
|
+
script1 = ScriptStage(
|
|
273
|
+
file="script1.py",
|
|
274
|
+
id="script1",
|
|
275
|
+
is_initial=True,
|
|
276
|
+
title="Script 1",
|
|
277
|
+
workflow_position=(0, 0),
|
|
278
|
+
workflow_transitions=[],
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
script2 = ScriptStage(
|
|
282
|
+
file="script2.py",
|
|
283
|
+
id="script2",
|
|
284
|
+
is_initial=False,
|
|
285
|
+
title="Script 2",
|
|
286
|
+
workflow_position=(100, 100),
|
|
287
|
+
workflow_transitions=[],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
script3 = ScriptStage(
|
|
291
|
+
file="script3.py",
|
|
292
|
+
id="script3",
|
|
293
|
+
is_initial=False,
|
|
294
|
+
title="Script 3",
|
|
295
|
+
workflow_position=(200, 200),
|
|
296
|
+
workflow_transitions=[],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Create unique and duplicate transitions
|
|
300
|
+
transition1 = WorkflowTransition(
|
|
301
|
+
id="transition1",
|
|
302
|
+
target_id=script2.id,
|
|
303
|
+
target_type=script2.type_name,
|
|
304
|
+
type="task",
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
transition2 = WorkflowTransition(
|
|
308
|
+
id="transition2",
|
|
309
|
+
target_id=script3.id,
|
|
310
|
+
target_type=script3.type_name,
|
|
311
|
+
type="task",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Add transitions: 2 unique, but transition1 is duplicated
|
|
315
|
+
script1.workflow_transitions.append(transition1)
|
|
316
|
+
script1.workflow_transitions.append(transition2)
|
|
317
|
+
script1.workflow_transitions.append(transition1) # duplicate
|
|
318
|
+
|
|
319
|
+
project = self.project_repository.load()
|
|
320
|
+
project.add_stage(script1)
|
|
321
|
+
project.add_stage(script2)
|
|
322
|
+
project.add_stage(script3)
|
|
323
|
+
self.project_repository.save(project)
|
|
324
|
+
|
|
325
|
+
# Load and verify deduplication
|
|
326
|
+
loaded_project = self.project_repository.load()
|
|
327
|
+
loaded_script1 = loaded_project.get_script(script1.id)
|
|
328
|
+
|
|
329
|
+
self.assertIsNotNone(loaded_script1)
|
|
330
|
+
assert loaded_script1 is not None # Type narrowing for pyright
|
|
331
|
+
self.assertEqual(len(loaded_script1.workflow_transitions), 2)
|
|
332
|
+
|
|
333
|
+
# Verify both unique transitions are present
|
|
334
|
+
transition_ids = [t.id for t in loaded_script1.workflow_transitions]
|
|
335
|
+
self.assertIn("transition1", transition_ids)
|
|
336
|
+
self.assertIn("transition2", transition_ids)
|
|
337
|
+
|
|
338
|
+
def test_deduplicate_transitions_with_same_source_target(self):
|
|
339
|
+
"""Test that multiple transitions with same source→target (but different IDs) are deduplicated"""
|
|
340
|
+
# Create stages
|
|
341
|
+
script1 = ScriptStage(
|
|
342
|
+
file="script1.py",
|
|
343
|
+
id="script1",
|
|
344
|
+
is_initial=True,
|
|
345
|
+
title="Script 1",
|
|
346
|
+
workflow_position=(0, 0),
|
|
347
|
+
workflow_transitions=[],
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
script2 = ScriptStage(
|
|
351
|
+
file="script2.py",
|
|
352
|
+
id="script2",
|
|
353
|
+
is_initial=False,
|
|
354
|
+
title="Script 2",
|
|
355
|
+
workflow_position=(100, 100),
|
|
356
|
+
workflow_transitions=[],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Add DIFFERENT transitions with SAME source→target
|
|
360
|
+
# This simulates corrupted data where multiple transitions point same direction
|
|
361
|
+
transition_a = WorkflowTransition(
|
|
362
|
+
id="trans-a",
|
|
363
|
+
target_id=script2.id,
|
|
364
|
+
target_type=script2.type_name,
|
|
365
|
+
type="task",
|
|
366
|
+
task_type="type1",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
transition_b = WorkflowTransition(
|
|
370
|
+
id="trans-b",
|
|
371
|
+
target_id=script2.id, # SAME target as trans-a
|
|
372
|
+
target_type=script2.type_name,
|
|
373
|
+
type="task",
|
|
374
|
+
task_type="type2",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
transition_c = WorkflowTransition(
|
|
378
|
+
id="trans-c",
|
|
379
|
+
target_id=script2.id, # SAME target as trans-a
|
|
380
|
+
target_type=script2.type_name,
|
|
381
|
+
type="task",
|
|
382
|
+
task_type="type3",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
script1.workflow_transitions.append(transition_a)
|
|
386
|
+
script1.workflow_transitions.append(transition_b)
|
|
387
|
+
script1.workflow_transitions.append(transition_c)
|
|
388
|
+
|
|
389
|
+
project = self.project_repository.load()
|
|
390
|
+
project.add_stage(script1)
|
|
391
|
+
project.add_stage(script2)
|
|
392
|
+
self.project_repository.save(project)
|
|
393
|
+
|
|
394
|
+
# Manually verify JSON has all 3 transitions
|
|
395
|
+
import json
|
|
396
|
+
|
|
397
|
+
json_path = self.project_repository.get_file_path()
|
|
398
|
+
with open(json_path, "r", encoding="utf-8") as f:
|
|
399
|
+
data = json.load(f)
|
|
400
|
+
|
|
401
|
+
# All 3 transitions are saved
|
|
402
|
+
self.assertEqual(len(data["scripts"][0]["transitions"]), 3)
|
|
403
|
+
|
|
404
|
+
# Load the project - should deduplicate by source→target
|
|
405
|
+
loaded_project = self.project_repository.load()
|
|
406
|
+
loaded_script1 = loaded_project.get_script(script1.id)
|
|
407
|
+
|
|
408
|
+
self.assertIsNotNone(loaded_script1)
|
|
409
|
+
assert loaded_script1 is not None # Type narrowing for pyright
|
|
410
|
+
|
|
411
|
+
# Should keep only ONE transition (first occurrence)
|
|
412
|
+
self.assertEqual(
|
|
413
|
+
len(loaded_script1.workflow_transitions),
|
|
414
|
+
1,
|
|
415
|
+
f"Expected 1 transition after deduplication, got {len(loaded_script1.workflow_transitions)}",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Should be the first one (trans-a)
|
|
419
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].id, "trans-a")
|
|
420
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
|
|
421
|
+
|
|
422
|
+
def test_deduplicate_allows_bidirectional_transitions(self):
|
|
423
|
+
"""Test that bidirectional transitions (a→b and b→a) are both preserved"""
|
|
424
|
+
# Create stages
|
|
425
|
+
script1 = ScriptStage(
|
|
426
|
+
file="script1.py",
|
|
427
|
+
id="script1",
|
|
428
|
+
is_initial=True,
|
|
429
|
+
title="Script 1",
|
|
430
|
+
workflow_position=(0, 0),
|
|
431
|
+
workflow_transitions=[],
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
script2 = ScriptStage(
|
|
435
|
+
file="script2.py",
|
|
436
|
+
id="script2",
|
|
437
|
+
is_initial=False,
|
|
438
|
+
title="Script 2",
|
|
439
|
+
workflow_position=(100, 100),
|
|
440
|
+
workflow_transitions=[],
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Add bidirectional transitions: script1→script2 and script2→script1
|
|
444
|
+
forward = WorkflowTransition(
|
|
445
|
+
id="forward",
|
|
446
|
+
target_id=script2.id,
|
|
447
|
+
target_type=script2.type_name,
|
|
448
|
+
type="task",
|
|
449
|
+
task_type="forward_type",
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
backward = WorkflowTransition(
|
|
453
|
+
id="backward",
|
|
454
|
+
target_id=script1.id,
|
|
455
|
+
target_type=script1.type_name,
|
|
456
|
+
type="task",
|
|
457
|
+
task_type="backward_type",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
script1.workflow_transitions.append(forward)
|
|
461
|
+
script2.workflow_transitions.append(backward)
|
|
462
|
+
|
|
463
|
+
project = self.project_repository.load()
|
|
464
|
+
project.add_stage(script1)
|
|
465
|
+
project.add_stage(script2)
|
|
466
|
+
self.project_repository.save(project)
|
|
467
|
+
|
|
468
|
+
# Load and verify both directions are preserved
|
|
469
|
+
loaded_project = self.project_repository.load()
|
|
470
|
+
loaded_script1 = loaded_project.get_script(script1.id)
|
|
471
|
+
loaded_script2 = loaded_project.get_script(script2.id)
|
|
472
|
+
|
|
473
|
+
self.assertIsNotNone(loaded_script1)
|
|
474
|
+
self.assertIsNotNone(loaded_script2)
|
|
475
|
+
assert loaded_script1 is not None
|
|
476
|
+
assert loaded_script2 is not None
|
|
477
|
+
|
|
478
|
+
# script1 should have forward transition
|
|
479
|
+
self.assertEqual(len(loaded_script1.workflow_transitions), 1)
|
|
480
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].id, "forward")
|
|
481
|
+
self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
|
|
482
|
+
|
|
483
|
+
# script2 should have backward transition
|
|
484
|
+
self.assertEqual(len(loaded_script2.workflow_transitions), 1)
|
|
485
|
+
self.assertEqual(loaded_script2.workflow_transitions[0].id, "backward")
|
|
486
|
+
self.assertEqual(loaded_script2.workflow_transitions[0].target_id, script1.id)
|
|
@@ -59,5 +59,6 @@ def get_editor_bp(main_controller: MainController):
|
|
|
59
59
|
requires_approval(workflow_controller.fix_positions_with_autolayout),
|
|
60
60
|
pysa_controller.analyze_python_syntax,
|
|
61
61
|
requires_approval(main_controller.linter_repository.fix_issue_in_codebase),
|
|
62
|
+
requires_approval(main_controller.add_and_install_requirement),
|
|
62
63
|
]
|
|
63
64
|
)
|
abstra_internals/services/fs.py
CHANGED
|
@@ -1,13 +1,131 @@
|
|
|
1
1
|
import fnmatch
|
|
2
2
|
import os
|
|
3
|
+
import threading
|
|
3
4
|
from pathlib import Path
|
|
4
|
-
from typing import List, Optional, Tuple
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
5
6
|
|
|
6
7
|
from abstra_internals.consts.filepaths import GITIGNORE_FILEPATH
|
|
8
|
+
from abstra_internals.repositories.git import create_git_repository
|
|
7
9
|
from abstra_internals.settings import Settings
|
|
10
|
+
from abstra_internals.utils.fs_cache import get_path_cache
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
class FileSystemService:
|
|
14
|
+
# Cache for git check-ignore results
|
|
15
|
+
_git_ignore_cache: Dict[Path, bool] = {}
|
|
16
|
+
_git_ignore_cache_lock = threading.Lock()
|
|
17
|
+
_git_repository = None
|
|
18
|
+
|
|
19
|
+
# Fallback cache for Python .gitignore parsing (when git unavailable)
|
|
20
|
+
_gitignore_cache: Dict[Path, List[str]] = {}
|
|
21
|
+
_gitignore_cache_lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _get_git_repository():
|
|
25
|
+
"""Get or create the git repository instance."""
|
|
26
|
+
if FileSystemService._git_repository is None:
|
|
27
|
+
FileSystemService._git_repository = create_git_repository(
|
|
28
|
+
Settings.root_path
|
|
29
|
+
)
|
|
30
|
+
return FileSystemService._git_repository
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def _is_git_available() -> bool:
|
|
34
|
+
"""Check if git is available and we're in a git repository."""
|
|
35
|
+
git_repo = FileSystemService._get_git_repository()
|
|
36
|
+
return git_repo.is_git_repository()
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _check_git_ignore_batch(paths: List[Path]) -> Dict[Path, bool]:
|
|
40
|
+
"""
|
|
41
|
+
Check multiple paths at once using git check-ignore --stdin.
|
|
42
|
+
Much more efficient than checking one at a time.
|
|
43
|
+
"""
|
|
44
|
+
if not FileSystemService._is_git_available():
|
|
45
|
+
return {path: False for path in paths}
|
|
46
|
+
|
|
47
|
+
results = {}
|
|
48
|
+
uncached_paths = []
|
|
49
|
+
|
|
50
|
+
# Check cache first
|
|
51
|
+
with FileSystemService._git_ignore_cache_lock:
|
|
52
|
+
for path in paths:
|
|
53
|
+
if path in FileSystemService._git_ignore_cache:
|
|
54
|
+
results[path] = FileSystemService._git_ignore_cache[path]
|
|
55
|
+
else:
|
|
56
|
+
uncached_paths.append(path)
|
|
57
|
+
|
|
58
|
+
if not uncached_paths:
|
|
59
|
+
return results
|
|
60
|
+
|
|
61
|
+
# Use git repository to check in batch
|
|
62
|
+
git_repo = FileSystemService._get_git_repository()
|
|
63
|
+
batch_results = git_repo.check_ignore_batch(uncached_paths)
|
|
64
|
+
|
|
65
|
+
# Update cache
|
|
66
|
+
with FileSystemService._git_ignore_cache_lock:
|
|
67
|
+
for path, is_ignored in batch_results.items():
|
|
68
|
+
FileSystemService._git_ignore_cache[path] = is_ignored
|
|
69
|
+
results[path] = is_ignored
|
|
70
|
+
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _check_git_ignore(path: Path) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Use git check-ignore to determine if a path should be ignored.
|
|
77
|
+
Much faster than parsing .gitignore files manually.
|
|
78
|
+
"""
|
|
79
|
+
# Quick cache check
|
|
80
|
+
if path in FileSystemService._git_ignore_cache:
|
|
81
|
+
return FileSystemService._git_ignore_cache[path]
|
|
82
|
+
|
|
83
|
+
# Use batch method for single path (still efficient due to caching)
|
|
84
|
+
result = FileSystemService._check_git_ignore_batch([path])
|
|
85
|
+
return result.get(path, False)
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _get_gitignore_patterns(directory: Path) -> Optional[List[str]]:
|
|
89
|
+
"""
|
|
90
|
+
Get cached .gitignore patterns for a directory.
|
|
91
|
+
Returns None if no .gitignore file exists.
|
|
92
|
+
"""
|
|
93
|
+
# Quick check without lock
|
|
94
|
+
if directory in FileSystemService._gitignore_cache:
|
|
95
|
+
return FileSystemService._gitignore_cache[directory]
|
|
96
|
+
|
|
97
|
+
with FileSystemService._gitignore_cache_lock:
|
|
98
|
+
# Double-check after acquiring lock
|
|
99
|
+
if directory in FileSystemService._gitignore_cache:
|
|
100
|
+
return FileSystemService._gitignore_cache[directory]
|
|
101
|
+
|
|
102
|
+
ignore_file = directory / GITIGNORE_FILEPATH
|
|
103
|
+
patterns = None
|
|
104
|
+
|
|
105
|
+
# Check if .gitignore exists and read it
|
|
106
|
+
try:
|
|
107
|
+
if ignore_file.exists():
|
|
108
|
+
patterns = ignore_file.read_text().splitlines()
|
|
109
|
+
except (IOError, UnicodeDecodeError):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
# Cache the result (None if file doesn't exist)
|
|
113
|
+
FileSystemService._gitignore_cache[directory] = patterns if patterns else []
|
|
114
|
+
return FileSystemService._gitignore_cache[directory]
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def clear_gitignore_cache():
|
|
118
|
+
"""Clear all gitignore-related caches. Use when .gitignore files are modified."""
|
|
119
|
+
with FileSystemService._git_ignore_cache_lock:
|
|
120
|
+
FileSystemService._git_ignore_cache.clear()
|
|
121
|
+
|
|
122
|
+
# Also clear the old pattern cache if it exists (for fallback mode)
|
|
123
|
+
if hasattr(FileSystemService, "_gitignore_cache"):
|
|
124
|
+
FileSystemService._gitignore_cache.clear()
|
|
125
|
+
|
|
126
|
+
# Clear git repository cache to avoid using stale repository instances
|
|
127
|
+
FileSystemService._git_repository = None
|
|
128
|
+
|
|
11
129
|
@staticmethod
|
|
12
130
|
def venv_path() -> Optional[Path]:
|
|
13
131
|
str_path = os.getenv("VIRTUAL_ENV") or os.getenv("CONDA_PREFIX")
|
|
@@ -66,6 +184,99 @@ class FileSystemService:
|
|
|
66
184
|
if not dirpath.is_dir():
|
|
67
185
|
raise ValueError(f"Provided path {dirpath} is not a directory.")
|
|
68
186
|
|
|
187
|
+
# If using git ignore, collect all paths first and check in one batch
|
|
188
|
+
if use_ignore and FileSystemService._is_git_available():
|
|
189
|
+
return FileSystemService._list_paths_with_git(
|
|
190
|
+
dirpath,
|
|
191
|
+
include_dirs=include_dirs,
|
|
192
|
+
allowed_suffixes=allowed_suffixes,
|
|
193
|
+
recursive=recursive,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Fallback to old method if git not available
|
|
197
|
+
return FileSystemService._list_paths_recursive(
|
|
198
|
+
dirpath,
|
|
199
|
+
include_dirs=include_dirs,
|
|
200
|
+
use_ignore=use_ignore,
|
|
201
|
+
allowed_suffixes=allowed_suffixes,
|
|
202
|
+
recursive=recursive,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _list_paths_with_git(
|
|
207
|
+
dirpath: Path,
|
|
208
|
+
*,
|
|
209
|
+
include_dirs: bool = True,
|
|
210
|
+
allowed_suffixes: Optional[List[str]] = None,
|
|
211
|
+
recursive: bool = True,
|
|
212
|
+
) -> List[Path]:
|
|
213
|
+
"""
|
|
214
|
+
Optimized version that collects all paths first, then checks git ignore in one batch.
|
|
215
|
+
"""
|
|
216
|
+
all_paths = []
|
|
217
|
+
path_info = {} # Map path to (is_directory, should_check_suffix)
|
|
218
|
+
|
|
219
|
+
# Collect all paths recursively without checking ignore
|
|
220
|
+
def collect_paths(current_dir: Path):
|
|
221
|
+
try:
|
|
222
|
+
with os.scandir(current_dir) as entries:
|
|
223
|
+
for entry in entries:
|
|
224
|
+
try:
|
|
225
|
+
is_directory = entry.is_dir(follow_symlinks=False)
|
|
226
|
+
except OSError:
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
child = Path(entry.path)
|
|
230
|
+
all_paths.append(child)
|
|
231
|
+
path_info[child] = (
|
|
232
|
+
is_directory,
|
|
233
|
+
not is_directory or include_dirs,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
if is_directory and recursive:
|
|
237
|
+
collect_paths(child)
|
|
238
|
+
except PermissionError:
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
collect_paths(dirpath)
|
|
242
|
+
|
|
243
|
+
# Check all paths at once with git
|
|
244
|
+
ignore_results = FileSystemService._check_git_ignore_batch(all_paths)
|
|
245
|
+
|
|
246
|
+
# Filter results
|
|
247
|
+
matches = []
|
|
248
|
+
if include_dirs and not dirpath.is_symlink():
|
|
249
|
+
matches.append(dirpath)
|
|
250
|
+
|
|
251
|
+
for path in all_paths:
|
|
252
|
+
# Skip ignored
|
|
253
|
+
if ignore_results.get(path, False):
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
is_directory, should_include = path_info[path]
|
|
257
|
+
|
|
258
|
+
# Check suffix filter
|
|
259
|
+
if not is_directory and not FileSystemService._suffix_allowed(
|
|
260
|
+
path, allowed_suffixes
|
|
261
|
+
):
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Include based on type
|
|
265
|
+
if should_include:
|
|
266
|
+
matches.append(path)
|
|
267
|
+
|
|
268
|
+
return sorted(matches, key=lambda p: p.as_posix())
|
|
269
|
+
|
|
270
|
+
@staticmethod
|
|
271
|
+
def _list_paths_recursive(
|
|
272
|
+
dirpath: Path,
|
|
273
|
+
*,
|
|
274
|
+
include_dirs: bool = True,
|
|
275
|
+
use_ignore: bool = True,
|
|
276
|
+
allowed_suffixes: Optional[List[str]] = None,
|
|
277
|
+
recursive: bool = True,
|
|
278
|
+
) -> List[Path]:
|
|
279
|
+
"""Fallback recursive implementation when git is not available."""
|
|
69
280
|
if use_ignore and (FileSystemService.is_ignored(dirpath)):
|
|
70
281
|
return []
|
|
71
282
|
|
|
@@ -73,23 +284,42 @@ class FileSystemService:
|
|
|
73
284
|
if include_dirs and not dirpath.is_symlink():
|
|
74
285
|
matches.append(dirpath)
|
|
75
286
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
287
|
+
try:
|
|
288
|
+
with os.scandir(dirpath) as entries:
|
|
289
|
+
for entry in entries:
|
|
290
|
+
try:
|
|
291
|
+
is_directory = entry.is_dir(follow_symlinks=False)
|
|
292
|
+
except OSError:
|
|
293
|
+
continue
|
|
294
|
+
|
|
295
|
+
child = Path(entry.path)
|
|
296
|
+
|
|
297
|
+
# Check if ignored
|
|
298
|
+
if use_ignore and FileSystemService._is_ignored_with_stat(
|
|
299
|
+
child, is_directory
|
|
300
|
+
):
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Check if we should include this entry
|
|
304
|
+
if is_directory or FileSystemService._suffix_allowed(
|
|
305
|
+
child, allowed_suffixes
|
|
306
|
+
):
|
|
307
|
+
matches.append(child)
|
|
308
|
+
|
|
309
|
+
# Recurse into directories
|
|
310
|
+
if is_directory and recursive:
|
|
311
|
+
matches.extend(
|
|
312
|
+
FileSystemService._list_paths_recursive(
|
|
313
|
+
child,
|
|
314
|
+
include_dirs=include_dirs,
|
|
315
|
+
use_ignore=use_ignore,
|
|
316
|
+
allowed_suffixes=allowed_suffixes,
|
|
317
|
+
recursive=True,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
except PermissionError:
|
|
321
|
+
pass
|
|
322
|
+
|
|
93
323
|
return sorted(matches, key=lambda p: p.as_posix())
|
|
94
324
|
|
|
95
325
|
@staticmethod
|
|
@@ -168,10 +398,60 @@ class FileSystemService:
|
|
|
168
398
|
return True
|
|
169
399
|
return any(path.suffix.lower() == suffix.lower() for suffix in allowed_suffixes)
|
|
170
400
|
|
|
401
|
+
@staticmethod
|
|
402
|
+
def _is_ignored_with_stat(path: Path, is_directory: bool):
|
|
403
|
+
"""
|
|
404
|
+
Optimized version of is_ignored that accepts pre-computed is_directory flag
|
|
405
|
+
to avoid redundant stat calls when using os.scandir().
|
|
406
|
+
Uses git check-ignore when available for best performance.
|
|
407
|
+
"""
|
|
408
|
+
# Try using git check-ignore first (much faster)
|
|
409
|
+
# Don't resolve path yet - let git handle it as-is
|
|
410
|
+
if FileSystemService._is_git_available():
|
|
411
|
+
return FileSystemService._check_git_ignore(path)
|
|
412
|
+
|
|
413
|
+
# Fallback: resolve path for Python implementation
|
|
414
|
+
path = get_path_cache().get_resolved_path(path)
|
|
415
|
+
|
|
416
|
+
# Fallback to Python implementation
|
|
417
|
+
# Use the pre-computed is_directory flag instead of calling path.is_file()
|
|
418
|
+
current_dir = path if is_directory else path.parent
|
|
419
|
+
|
|
420
|
+
project_root = Settings.root_path
|
|
421
|
+
while (
|
|
422
|
+
current_dir != current_dir.parent and current_dir != project_root.parent
|
|
423
|
+
): # Stop at project root
|
|
424
|
+
# Get cached patterns for this directory
|
|
425
|
+
patterns = FileSystemService._get_gitignore_patterns(current_dir)
|
|
426
|
+
|
|
427
|
+
if patterns:
|
|
428
|
+
for pattern in patterns:
|
|
429
|
+
pattern = pattern.strip()
|
|
430
|
+
if pattern and not pattern.startswith("#"):
|
|
431
|
+
if FileSystemService._matches_gitignore_pattern(
|
|
432
|
+
path, pattern, current_dir
|
|
433
|
+
):
|
|
434
|
+
return True
|
|
435
|
+
|
|
436
|
+
current_dir = current_dir.parent
|
|
437
|
+
|
|
438
|
+
return False
|
|
439
|
+
|
|
171
440
|
@staticmethod
|
|
172
441
|
def is_ignored(path: Path):
|
|
173
|
-
|
|
442
|
+
"""
|
|
443
|
+
Check if a path should be ignored according to .gitignore rules.
|
|
444
|
+
Uses git check-ignore for performance when available, falls back to Python implementation.
|
|
445
|
+
"""
|
|
446
|
+
# Try using git check-ignore first (much faster)
|
|
447
|
+
# Don't resolve path yet - let git handle it as-is
|
|
448
|
+
if FileSystemService._is_git_available():
|
|
449
|
+
return FileSystemService._check_git_ignore(path)
|
|
450
|
+
|
|
451
|
+
# Fallback: resolve path for Python implementation
|
|
452
|
+
path = get_path_cache().get_resolved_path(path)
|
|
174
453
|
|
|
454
|
+
# Fallback to Python implementation if git is not available
|
|
175
455
|
# Look for ignore file starting from the path's directory up to the root
|
|
176
456
|
# For files that don't exist, we need to check if it would be a file based on the path structure
|
|
177
457
|
# If the path has a suffix or ends with a filename-like component, treat it as a file
|
|
@@ -187,19 +467,18 @@ class FileSystemService:
|
|
|
187
467
|
while (
|
|
188
468
|
current_dir != current_dir.parent and current_dir != project_root.parent
|
|
189
469
|
): # Stop at project root
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
continue
|
|
470
|
+
# Get cached patterns for this directory
|
|
471
|
+
patterns = FileSystemService._get_gitignore_patterns(current_dir)
|
|
472
|
+
|
|
473
|
+
if patterns:
|
|
474
|
+
for pattern in patterns:
|
|
475
|
+
pattern = pattern.strip()
|
|
476
|
+
if pattern and not pattern.startswith("#"):
|
|
477
|
+
if FileSystemService._matches_gitignore_pattern(
|
|
478
|
+
path, pattern, current_dir
|
|
479
|
+
):
|
|
480
|
+
return True
|
|
481
|
+
|
|
203
482
|
current_dir = current_dir.parent
|
|
204
483
|
|
|
205
484
|
return False
|