keboola-cli 0.63.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.
- keboola_agent_cli/__init__.py +34 -0
- keboola_agent_cli/__main__.py +5 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/index.html +50 -0
- keboola_agent_cli/ai_client.py +83 -0
- keboola_agent_cli/auto_update.py +550 -0
- keboola_agent_cli/changelog.py +1198 -0
- keboola_agent_cli/cli.py +448 -0
- keboola_agent_cli/client.py +3422 -0
- keboola_agent_cli/commands/__init__.py +0 -0
- keboola_agent_cli/commands/_data_app_git.py +343 -0
- keboola_agent_cli/commands/_helpers.py +377 -0
- keboola_agent_cli/commands/_metadata_input.py +49 -0
- keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
- keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
- keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
- keboola_agent_cli/commands/agent.py +968 -0
- keboola_agent_cli/commands/branch.py +423 -0
- keboola_agent_cli/commands/changelog.py +168 -0
- keboola_agent_cli/commands/component.py +216 -0
- keboola_agent_cli/commands/config.py +2442 -0
- keboola_agent_cli/commands/context.py +1481 -0
- keboola_agent_cli/commands/data_app.py +1279 -0
- keboola_agent_cli/commands/dev_portal.py +584 -0
- keboola_agent_cli/commands/doctor.py +37 -0
- keboola_agent_cli/commands/encrypt.py +145 -0
- keboola_agent_cli/commands/feature.py +311 -0
- keboola_agent_cli/commands/flow.py +948 -0
- keboola_agent_cli/commands/http_client.py +157 -0
- keboola_agent_cli/commands/init.py +279 -0
- keboola_agent_cli/commands/job.py +661 -0
- keboola_agent_cli/commands/kai.py +301 -0
- keboola_agent_cli/commands/lineage.py +1464 -0
- keboola_agent_cli/commands/org.py +292 -0
- keboola_agent_cli/commands/permissions.py +360 -0
- keboola_agent_cli/commands/project.py +1192 -0
- keboola_agent_cli/commands/repl.py +243 -0
- keboola_agent_cli/commands/schedule.py +340 -0
- keboola_agent_cli/commands/search.py +178 -0
- keboola_agent_cli/commands/semantic_layer.py +939 -0
- keboola_agent_cli/commands/serve.py +272 -0
- keboola_agent_cli/commands/sharing.py +340 -0
- keboola_agent_cli/commands/storage.py +2630 -0
- keboola_agent_cli/commands/stream.py +266 -0
- keboola_agent_cli/commands/sync.py +1277 -0
- keboola_agent_cli/commands/tool.py +206 -0
- keboola_agent_cli/commands/version.py +186 -0
- keboola_agent_cli/commands/workspace.py +635 -0
- keboola_agent_cli/config_store.py +582 -0
- keboola_agent_cli/constants.py +528 -0
- keboola_agent_cli/data_science_client.py +342 -0
- keboola_agent_cli/dev_portal_client.py +323 -0
- keboola_agent_cli/errors.py +248 -0
- keboola_agent_cli/http_base.py +315 -0
- keboola_agent_cli/json_utils.py +126 -0
- keboola_agent_cli/lib.py +536 -0
- keboola_agent_cli/manage_client.py +324 -0
- keboola_agent_cli/metastore_client.py +214 -0
- keboola_agent_cli/models.py +427 -0
- keboola_agent_cli/output.py +1084 -0
- keboola_agent_cli/permissions.py +469 -0
- keboola_agent_cli/py.typed +3 -0
- keboola_agent_cli/result_models.py +271 -0
- keboola_agent_cli/server/__init__.py +34 -0
- keboola_agent_cli/server/agent_runner.py +1289 -0
- keboola_agent_cli/server/agents_store.py +325 -0
- keboola_agent_cli/server/app.py +764 -0
- keboola_agent_cli/server/auth.py +117 -0
- keboola_agent_cli/server/dependencies.py +149 -0
- keboola_agent_cli/server/pricing.py +303 -0
- keboola_agent_cli/server/routers/__init__.py +1 -0
- keboola_agent_cli/server/routers/agents.py +616 -0
- keboola_agent_cli/server/routers/ai_chat.py +129 -0
- keboola_agent_cli/server/routers/branches.py +133 -0
- keboola_agent_cli/server/routers/components.py +48 -0
- keboola_agent_cli/server/routers/configs.py +507 -0
- keboola_agent_cli/server/routers/data_apps.py +384 -0
- keboola_agent_cli/server/routers/dev_portal.py +67 -0
- keboola_agent_cli/server/routers/encrypt.py +35 -0
- keboola_agent_cli/server/routers/feature.py +179 -0
- keboola_agent_cli/server/routers/flows.py +204 -0
- keboola_agent_cli/server/routers/health.py +53 -0
- keboola_agent_cli/server/routers/jobs.py +175 -0
- keboola_agent_cli/server/routers/kai.py +80 -0
- keboola_agent_cli/server/routers/lineage.py +226 -0
- keboola_agent_cli/server/routers/mcp.py +70 -0
- keboola_agent_cli/server/routers/members.py +170 -0
- keboola_agent_cli/server/routers/org.py +96 -0
- keboola_agent_cli/server/routers/projects.py +106 -0
- keboola_agent_cli/server/routers/schedules.py +54 -0
- keboola_agent_cli/server/routers/search.py +30 -0
- keboola_agent_cli/server/routers/semantic_layer.py +650 -0
- keboola_agent_cli/server/routers/sharing.py +86 -0
- keboola_agent_cli/server/routers/storage.py +574 -0
- keboola_agent_cli/server/routers/stream.py +100 -0
- keboola_agent_cli/server/routers/workspaces.py +302 -0
- keboola_agent_cli/server/run_broadcaster.py +329 -0
- keboola_agent_cli/server/sse.py +25 -0
- keboola_agent_cli/services/__init__.py +0 -0
- keboola_agent_cli/services/_encryption.py +217 -0
- keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
- keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
- keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
- keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
- keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
- keboola_agent_cli/services/_sync_bindings.py +456 -0
- keboola_agent_cli/services/_sync_branch.py +191 -0
- keboola_agent_cli/services/_sync_bulk.py +228 -0
- keboola_agent_cli/services/_sync_clone.py +163 -0
- keboola_agent_cli/services/_sync_models.py +97 -0
- keboola_agent_cli/services/_sync_push_ops.py +369 -0
- keboola_agent_cli/services/_sync_storage.py +376 -0
- keboola_agent_cli/services/_sync_writeback.py +167 -0
- keboola_agent_cli/services/agent_service.py +458 -0
- keboola_agent_cli/services/base.py +175 -0
- keboola_agent_cli/services/branch_service.py +588 -0
- keboola_agent_cli/services/component_service.py +694 -0
- keboola_agent_cli/services/config_service.py +2099 -0
- keboola_agent_cli/services/data_app_git_service.py +224 -0
- keboola_agent_cli/services/data_app_service.py +2082 -0
- keboola_agent_cli/services/deep_lineage_service.py +1322 -0
- keboola_agent_cli/services/dev_portal_service.py +345 -0
- keboola_agent_cli/services/doctor_service.py +445 -0
- keboola_agent_cli/services/encrypt_service.py +87 -0
- keboola_agent_cli/services/feature_service.py +268 -0
- keboola_agent_cli/services/flow_service.py +769 -0
- keboola_agent_cli/services/flow_validation.py +188 -0
- keboola_agent_cli/services/http_forwarder_service.py +236 -0
- keboola_agent_cli/services/job_idempotency_store.py +285 -0
- keboola_agent_cli/services/job_service.py +797 -0
- keboola_agent_cli/services/kai_service.py +367 -0
- keboola_agent_cli/services/lineage_service.py +274 -0
- keboola_agent_cli/services/mcp_service.py +1498 -0
- keboola_agent_cli/services/mcp_transport.py +259 -0
- keboola_agent_cli/services/member_service.py +593 -0
- keboola_agent_cli/services/org_service.py +619 -0
- keboola_agent_cli/services/project_service.py +947 -0
- keboola_agent_cli/services/repo_validate_service.py +767 -0
- keboola_agent_cli/services/schedule_service.py +731 -0
- keboola_agent_cli/services/search_service.py +331 -0
- keboola_agent_cli/services/semantic_layer_service.py +1497 -0
- keboola_agent_cli/services/sharing_service.py +307 -0
- keboola_agent_cli/services/storage_service.py +2524 -0
- keboola_agent_cli/services/stream_service.py +395 -0
- keboola_agent_cli/services/sync_service.py +2244 -0
- keboola_agent_cli/services/variables_service.py +447 -0
- keboola_agent_cli/services/version_service.py +1038 -0
- keboola_agent_cli/services/workspace_service.py +1103 -0
- keboola_agent_cli/stream_client.py +217 -0
- keboola_agent_cli/sync/__init__.py +1 -0
- keboola_agent_cli/sync/branch_mapping.py +174 -0
- keboola_agent_cli/sync/clone.py +211 -0
- keboola_agent_cli/sync/code_extraction.py +655 -0
- keboola_agent_cli/sync/config_format.py +290 -0
- keboola_agent_cli/sync/diff_engine.py +566 -0
- keboola_agent_cli/sync/git_utils.py +93 -0
- keboola_agent_cli/sync/manifest.py +162 -0
- keboola_agent_cli/sync/naming.py +90 -0
- keboola_agent_cli/sync/secrets.py +62 -0
- keboola_agent_cli/sync/sql_split.py +134 -0
- keboola_cli-0.63.4.dist-info/METADATA +308 -0
- keboola_cli-0.63.4.dist-info/RECORD +306 -0
- keboola_cli-0.63.4.dist-info/WHEEL +4 -0
- keboola_cli-0.63.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
"""Extract and merge embedded code from Keboola configurations.
|
|
2
|
+
|
|
3
|
+
On pull: extracts SQL/Python code from config parameters into separate files.
|
|
4
|
+
On push: reads code files back into config parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, cast
|
|
11
|
+
|
|
12
|
+
from keboola_agent_cli.sync.sql_split import split_statements
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _strip_trailing_empty(lines: list[str]) -> list[str]:
|
|
16
|
+
"""Remove trailing empty lines but preserve leading whitespace."""
|
|
17
|
+
result = list(lines)
|
|
18
|
+
while result and result[-1].strip() == "":
|
|
19
|
+
result.pop()
|
|
20
|
+
return result
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _lines_to_script(lines: list[str], *, is_sql: bool = False) -> list[str]:
|
|
24
|
+
"""Convert collected lines back into the ``script[]`` array.
|
|
25
|
+
|
|
26
|
+
For SQL transformations: splits on semicolons using a state machine,
|
|
27
|
+
producing one element per statement (matching Keboola runtime semantics).
|
|
28
|
+
For Python/other: joins all lines into a single element.
|
|
29
|
+
"""
|
|
30
|
+
stripped = _strip_trailing_empty(lines)
|
|
31
|
+
if not stripped:
|
|
32
|
+
return []
|
|
33
|
+
content = "\n".join(stripped)
|
|
34
|
+
if is_sql:
|
|
35
|
+
return split_statements(content)
|
|
36
|
+
return [content]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Component patterns that contain SQL transformations.
|
|
40
|
+
# Used for exact-match dispatch in pull/push code-file extraction.
|
|
41
|
+
# For runtime-shape detection (i.e. "does this component's script[]
|
|
42
|
+
# need to be split on statement boundaries?") prefer
|
|
43
|
+
# :func:`is_sql_transformation_component` -- it adds fragment-based
|
|
44
|
+
# fallback so newer/variant SQL backends are covered without code edits.
|
|
45
|
+
SQL_TRANSFORMATION_COMPONENTS: set[str] = {
|
|
46
|
+
"keboola.snowflake-transformation",
|
|
47
|
+
"keboola.synapse-transformation",
|
|
48
|
+
"keboola.oracle-transformation",
|
|
49
|
+
"keboola.redshift-sql-transformation",
|
|
50
|
+
"keboola.google-bigquery-transformation",
|
|
51
|
+
"keboola.duckdb-transformation",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Component patterns that contain Python transformations
|
|
55
|
+
PYTHON_TRANSFORMATION_COMPONENTS: set[str] = {
|
|
56
|
+
"keboola.python-transformation-v2",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Components with embedded Python code (custom apps)
|
|
60
|
+
PYTHON_APP_COMPONENTS: set[str] = {
|
|
61
|
+
"kds-team.app-custom-python",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Fragment-based detection for SQL transformations. Keeps
|
|
65
|
+
# is_sql_transformation_component robust against newer/variant
|
|
66
|
+
# component IDs (e.g. ``keboola.snowflake-transformation-v2``,
|
|
67
|
+
# self-hosted ``keboola.exasol-transformation``) without requiring
|
|
68
|
+
# an edit to SQL_TRANSFORMATION_COMPONENTS for every new backend.
|
|
69
|
+
_SQL_TRANSFORMATION_FRAGMENTS: tuple[str, ...] = (
|
|
70
|
+
"snowflake-transformation",
|
|
71
|
+
"synapse-transformation",
|
|
72
|
+
"oracle-transformation",
|
|
73
|
+
"redshift-sql-transformation",
|
|
74
|
+
"redshift-transformation",
|
|
75
|
+
"google-bigquery-transformation",
|
|
76
|
+
"bigquery-transformation",
|
|
77
|
+
"duckdb-transformation",
|
|
78
|
+
"exasol-transformation",
|
|
79
|
+
"teradata-transformation",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
SQL_BLOCK_MARKER = "/* ===== BLOCK: {name} ===== */"
|
|
83
|
+
SQL_CODE_MARKER = "/* ===== CODE: {name} ===== */"
|
|
84
|
+
PYTHON_BLOCK_MARKER = "# ===== BLOCK: {name} ====="
|
|
85
|
+
PYTHON_CODE_MARKER = "# ===== CODE: {name} ====="
|
|
86
|
+
DESCRIPTION_FILENAME = "_description.md"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def is_sql_transformation_component(component_id: str) -> bool:
|
|
90
|
+
"""Return True if the component's script[] elements are SQL statements.
|
|
91
|
+
|
|
92
|
+
The Keboola runtime treats each ``parameters.blocks[].codes[].script``
|
|
93
|
+
element of a SQL transformation as ONE logical statement. Pushing a
|
|
94
|
+
string instead of an array passes the Storage API (lax validator) but
|
|
95
|
+
crashes at job runtime ("Expected array, got string").
|
|
96
|
+
|
|
97
|
+
Combines exact-match (``SQL_TRANSFORMATION_COMPONENTS``) with a
|
|
98
|
+
fragment fallback so newer/variant SQL backends -- including ones
|
|
99
|
+
not yet enumerated in the exact set -- still get correct treatment.
|
|
100
|
+
"""
|
|
101
|
+
if component_id in SQL_TRANSFORMATION_COMPONENTS:
|
|
102
|
+
return True
|
|
103
|
+
return any(fragment in component_id for fragment in _SQL_TRANSFORMATION_FRAGMENTS)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def normalize_blocks_codes_script(
|
|
107
|
+
component_id: str,
|
|
108
|
+
config: dict[str, Any],
|
|
109
|
+
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
110
|
+
"""Normalize ``parameters.blocks[].codes[].script`` for runtime safety.
|
|
111
|
+
|
|
112
|
+
Closes two gaps between the Storage API (lax shape validator) and the
|
|
113
|
+
runtime (strict per-element semantics):
|
|
114
|
+
|
|
115
|
+
1. **String -> array** (since 0.28.0). The Keboola runtime schema
|
|
116
|
+
validator requires ``script`` to be an array. The Storage API
|
|
117
|
+
silently accepts a string and the runtime crashes at job
|
|
118
|
+
execution with::
|
|
119
|
+
|
|
120
|
+
Invalid type for path "root.parameters.blocks.0.codes.X.script".
|
|
121
|
+
Expected "array", but got "string"
|
|
122
|
+
|
|
123
|
+
For SQL transformations the string is split on statement
|
|
124
|
+
boundaries via :func:`split_statements` (state machine respecting
|
|
125
|
+
comments and string literals); for Python / R / custom-Python
|
|
126
|
+
apps and other components with the same schema, the string is
|
|
127
|
+
wrapped as a single-element array.
|
|
128
|
+
|
|
129
|
+
2. **Per-element re-split for SQL** (since 0.30.8, closes issue
|
|
130
|
+
#274). When ``script`` is already a list but an element packs
|
|
131
|
+
multiple ``;``-separated statements, the Snowflake/BigQuery/etc.
|
|
132
|
+
runtime crashes at ``odbc_prepare`` with ``Actual statement
|
|
133
|
+
count N did not match the desired statement count 1`` (SQL
|
|
134
|
+
state ``0A000``). Storage API accepts this shape silently
|
|
135
|
+
because it only checks "list of strings". This pass re-splits
|
|
136
|
+
each SQL-transformation element through :func:`split_statements`
|
|
137
|
+
and replaces in place when the element produces more than one
|
|
138
|
+
statement.
|
|
139
|
+
|
|
140
|
+
Components that don't match either case are passed through
|
|
141
|
+
unchanged.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
component_id: The configuration's component ID. Used to choose
|
|
145
|
+
split-vs-wrap-vs-resplit and to passthrough components that
|
|
146
|
+
don't have this schema at all.
|
|
147
|
+
config: The configuration dict to normalize. Mutated in place.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
``(config, normalizations)``. ``normalizations`` is a list of
|
|
151
|
+
per-element change records of shape::
|
|
152
|
+
|
|
153
|
+
# case 1 (string -> array):
|
|
154
|
+
{
|
|
155
|
+
"path": "parameters.blocks[0].codes[1].script",
|
|
156
|
+
"action": "sql_split" | "wrap_array",
|
|
157
|
+
"before_type": "str",
|
|
158
|
+
"after_type": "list",
|
|
159
|
+
"after_length": 3,
|
|
160
|
+
}
|
|
161
|
+
# case 2 (list element -> multiple list elements):
|
|
162
|
+
{
|
|
163
|
+
"path": "parameters.blocks[0].codes[1].script[2]",
|
|
164
|
+
"action": "sql_resplit",
|
|
165
|
+
"before_type": "str",
|
|
166
|
+
"after_type": "list",
|
|
167
|
+
"before_length": 1,
|
|
168
|
+
"after_length": 2,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Empty when nothing was normalized (already-valid input).
|
|
172
|
+
|
|
173
|
+
The caller is responsible for surfacing the normalization records to
|
|
174
|
+
the user (stderr in human mode, JSON envelope in JSON mode) so the
|
|
175
|
+
silent fix is observable.
|
|
176
|
+
"""
|
|
177
|
+
normalizations: list[dict[str, Any]] = []
|
|
178
|
+
if not isinstance(config, dict):
|
|
179
|
+
return config, normalizations
|
|
180
|
+
parameters = config.get("parameters")
|
|
181
|
+
if not isinstance(parameters, dict):
|
|
182
|
+
return config, normalizations
|
|
183
|
+
blocks = parameters.get("blocks")
|
|
184
|
+
if not isinstance(blocks, list):
|
|
185
|
+
return config, normalizations
|
|
186
|
+
|
|
187
|
+
is_sql = is_sql_transformation_component(component_id)
|
|
188
|
+
|
|
189
|
+
for block_idx, block in enumerate(blocks):
|
|
190
|
+
if not isinstance(block, dict):
|
|
191
|
+
continue
|
|
192
|
+
block_d = cast("dict[str, Any]", block)
|
|
193
|
+
codes = block_d.get("codes")
|
|
194
|
+
if not isinstance(codes, list):
|
|
195
|
+
continue
|
|
196
|
+
for code_idx, code in enumerate(codes):
|
|
197
|
+
if not isinstance(code, dict):
|
|
198
|
+
continue
|
|
199
|
+
code_d = cast("dict[str, Any]", code)
|
|
200
|
+
script = code_d.get("script")
|
|
201
|
+
if isinstance(script, str):
|
|
202
|
+
if is_sql:
|
|
203
|
+
new_script = split_statements(script)
|
|
204
|
+
action = "sql_split"
|
|
205
|
+
else:
|
|
206
|
+
# Single-element wrap for Python / R / custom-Python
|
|
207
|
+
# apps and any unknown component sharing the schema.
|
|
208
|
+
# Empty strings collapse to ``[]`` -- the runtime
|
|
209
|
+
# treats both as no-op.
|
|
210
|
+
new_script = [script] if script.strip() else []
|
|
211
|
+
action = "wrap_array"
|
|
212
|
+
code_d["script"] = new_script
|
|
213
|
+
normalizations.append(
|
|
214
|
+
{
|
|
215
|
+
"path": f"parameters.blocks[{block_idx}].codes[{code_idx}].script",
|
|
216
|
+
"action": action,
|
|
217
|
+
"before_type": "str",
|
|
218
|
+
"after_type": "list",
|
|
219
|
+
"after_length": len(new_script),
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
elif is_sql and isinstance(script, list):
|
|
223
|
+
rebuilt: list[Any] = []
|
|
224
|
+
resplits: list[tuple[int, int]] = []
|
|
225
|
+
for el_idx, element in enumerate(script):
|
|
226
|
+
if isinstance(element, str):
|
|
227
|
+
parts = split_statements(element)
|
|
228
|
+
if len(parts) > 1:
|
|
229
|
+
rebuilt.extend(parts)
|
|
230
|
+
resplits.append((el_idx, len(parts)))
|
|
231
|
+
continue
|
|
232
|
+
rebuilt.append(element)
|
|
233
|
+
if resplits:
|
|
234
|
+
code_d["script"] = rebuilt
|
|
235
|
+
for el_idx, after_count in resplits:
|
|
236
|
+
normalizations.append(
|
|
237
|
+
{
|
|
238
|
+
"path": (
|
|
239
|
+
f"parameters.blocks[{block_idx}]"
|
|
240
|
+
f".codes[{code_idx}].script[{el_idx}]"
|
|
241
|
+
),
|
|
242
|
+
"action": "sql_resplit",
|
|
243
|
+
"before_type": "str",
|
|
244
|
+
"after_type": "list",
|
|
245
|
+
"before_length": 1,
|
|
246
|
+
"after_length": after_count,
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
return config, normalizations
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _extract_description(config_data: dict[str, Any], config_dir: Path) -> None:
|
|
254
|
+
"""Extract description field into _description.md."""
|
|
255
|
+
description = config_data.get("description", "")
|
|
256
|
+
if not description:
|
|
257
|
+
return
|
|
258
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
(config_dir / DESCRIPTION_FILENAME).write_text(description, encoding="utf-8")
|
|
260
|
+
del config_data["description"]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _merge_description(config_data: dict[str, Any], config_dir: Path) -> None:
|
|
264
|
+
"""Read _description.md back into config_data."""
|
|
265
|
+
desc_file = config_dir / DESCRIPTION_FILENAME
|
|
266
|
+
if desc_file.exists():
|
|
267
|
+
config_data["description"] = desc_file.read_text(encoding="utf-8")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def extract_code_files(
|
|
271
|
+
component_id: str,
|
|
272
|
+
config_data: dict[str, Any],
|
|
273
|
+
config_dir: Path,
|
|
274
|
+
) -> dict[str, Any]:
|
|
275
|
+
"""Extract embedded code from config into separate files.
|
|
276
|
+
|
|
277
|
+
Modifies config_data in place to remove extracted code.
|
|
278
|
+
Writes code files to config_dir.
|
|
279
|
+
Returns the modified config_data.
|
|
280
|
+
"""
|
|
281
|
+
_extract_description(config_data, config_dir)
|
|
282
|
+
if component_id in SQL_TRANSFORMATION_COMPONENTS:
|
|
283
|
+
return _extract_sql_transformation(config_data, config_dir)
|
|
284
|
+
if component_id in PYTHON_TRANSFORMATION_COMPONENTS:
|
|
285
|
+
return _extract_python_transformation(config_data, config_dir)
|
|
286
|
+
if component_id in PYTHON_APP_COMPONENTS:
|
|
287
|
+
return _extract_python_app(config_data, config_dir)
|
|
288
|
+
return config_data
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def merge_code_files(
|
|
292
|
+
component_id: str,
|
|
293
|
+
config_data: dict[str, Any],
|
|
294
|
+
config_dir: Path,
|
|
295
|
+
) -> dict[str, Any]:
|
|
296
|
+
"""Read code files and merge them back into config_data.
|
|
297
|
+
|
|
298
|
+
Reverse of extract_code_files. Called before push.
|
|
299
|
+
Returns the modified config_data.
|
|
300
|
+
"""
|
|
301
|
+
_merge_description(config_data, config_dir)
|
|
302
|
+
if component_id in SQL_TRANSFORMATION_COMPONENTS:
|
|
303
|
+
return _merge_sql_transformation(config_data, config_dir)
|
|
304
|
+
if component_id in PYTHON_TRANSFORMATION_COMPONENTS:
|
|
305
|
+
return _merge_python_transformation(config_data, config_dir)
|
|
306
|
+
if component_id in PYTHON_APP_COMPONENTS:
|
|
307
|
+
return _merge_python_app(config_data, config_dir)
|
|
308
|
+
return config_data
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ---- SQL Transformations ----
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _extract_sql_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
315
|
+
"""Extract SQL blocks from parameters.blocks into transform.sql."""
|
|
316
|
+
parameters = config_data.get("parameters", {})
|
|
317
|
+
blocks = parameters.get("blocks", [])
|
|
318
|
+
|
|
319
|
+
if not blocks:
|
|
320
|
+
return config_data
|
|
321
|
+
|
|
322
|
+
lines: list[str] = []
|
|
323
|
+
for block in blocks:
|
|
324
|
+
block_name = block.get("name", "unnamed")
|
|
325
|
+
lines.append(SQL_BLOCK_MARKER.format(name=block_name))
|
|
326
|
+
lines.append("")
|
|
327
|
+
|
|
328
|
+
for code in block.get("codes", []):
|
|
329
|
+
code_name = code.get("name", "unnamed")
|
|
330
|
+
lines.append(SQL_CODE_MARKER.format(name=code_name))
|
|
331
|
+
|
|
332
|
+
scripts = code.get("script") or []
|
|
333
|
+
for si, script in enumerate(scripts):
|
|
334
|
+
if si > 0:
|
|
335
|
+
lines.append("") # blank line between statements
|
|
336
|
+
if isinstance(script, str) and "\n" in script:
|
|
337
|
+
lines.extend(script.split("\n"))
|
|
338
|
+
else:
|
|
339
|
+
lines.append(script)
|
|
340
|
+
lines.append("")
|
|
341
|
+
|
|
342
|
+
sql_content = "\n".join(lines).rstrip() + "\n"
|
|
343
|
+
|
|
344
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
345
|
+
(config_dir / "transform.sql").write_text(sql_content, encoding="utf-8")
|
|
346
|
+
|
|
347
|
+
# Remove blocks from parameters (they're now in the SQL file)
|
|
348
|
+
parameters.pop("blocks", None)
|
|
349
|
+
|
|
350
|
+
return config_data
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _merge_sql_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
354
|
+
"""Read transform.sql and parse block markers back into parameters.blocks."""
|
|
355
|
+
sql_file = config_dir / "transform.sql"
|
|
356
|
+
if not sql_file.exists():
|
|
357
|
+
return config_data
|
|
358
|
+
|
|
359
|
+
content = sql_file.read_text(encoding="utf-8")
|
|
360
|
+
blocks = _parse_sql_blocks(content)
|
|
361
|
+
|
|
362
|
+
parameters = config_data.setdefault("parameters", {})
|
|
363
|
+
parameters["blocks"] = blocks
|
|
364
|
+
|
|
365
|
+
return config_data
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _parse_sql_blocks(content: str) -> list[dict[str, Any]]:
|
|
369
|
+
"""Parse SQL content with block/code markers into blocks structure."""
|
|
370
|
+
blocks: list[dict[str, Any]] = []
|
|
371
|
+
current_block: dict[str, Any] | None = None
|
|
372
|
+
current_code: dict[str, Any] | None = None
|
|
373
|
+
current_script_lines: list[str] = []
|
|
374
|
+
orphan_lines: list[str] = []
|
|
375
|
+
|
|
376
|
+
for line in content.split("\n"):
|
|
377
|
+
stripped = line.strip()
|
|
378
|
+
|
|
379
|
+
# Check for block marker
|
|
380
|
+
if stripped.startswith("/* ===== BLOCK:") and stripped.endswith("===== */"):
|
|
381
|
+
# Save previous code if any
|
|
382
|
+
if current_code is not None and current_block is not None:
|
|
383
|
+
current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
|
|
384
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
385
|
+
current_code = None
|
|
386
|
+
current_script_lines = []
|
|
387
|
+
|
|
388
|
+
block_name = stripped[len("/* ===== BLOCK:") :].rstrip(" =*/").strip()
|
|
389
|
+
current_block = {"name": block_name, "codes": []}
|
|
390
|
+
blocks.append(current_block)
|
|
391
|
+
orphan_lines = []
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
# Check for code marker
|
|
395
|
+
if stripped.startswith("/* ===== CODE:") and stripped.endswith("===== */"):
|
|
396
|
+
# Save previous code if any
|
|
397
|
+
if current_code is not None and current_block is not None:
|
|
398
|
+
current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
|
|
399
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
400
|
+
current_script_lines = []
|
|
401
|
+
|
|
402
|
+
code_name = stripped[len("/* ===== CODE:") :].rstrip(" =*/").strip()
|
|
403
|
+
current_code = {"name": code_name}
|
|
404
|
+
# Prepend any orphan lines collected between BLOCK and this CODE marker
|
|
405
|
+
if orphan_lines:
|
|
406
|
+
current_script_lines = list(orphan_lines)
|
|
407
|
+
orphan_lines = []
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
# Regular line
|
|
411
|
+
if current_code is not None:
|
|
412
|
+
current_script_lines.append(line)
|
|
413
|
+
elif current_block is not None and stripped:
|
|
414
|
+
# Lines between BLOCK marker and first CODE marker — preserve them
|
|
415
|
+
# so they're not silently discarded on roundtrip.
|
|
416
|
+
orphan_lines.append(line)
|
|
417
|
+
|
|
418
|
+
# Don't forget the last code block
|
|
419
|
+
if current_code is not None and current_block is not None:
|
|
420
|
+
current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
|
|
421
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
422
|
+
|
|
423
|
+
# If no markers found, treat entire content as single block/code
|
|
424
|
+
if not blocks and content.strip():
|
|
425
|
+
blocks = [
|
|
426
|
+
{
|
|
427
|
+
"name": "Block 1",
|
|
428
|
+
"codes": [
|
|
429
|
+
{"name": "Code 1", "script": _lines_to_script(content.split("\n"), is_sql=True)}
|
|
430
|
+
],
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
|
|
434
|
+
return blocks
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
# ---- Python Transformations ----
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _extract_python_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
441
|
+
"""Extract Python blocks from parameters.blocks into transform.py, packages into pyproject.toml."""
|
|
442
|
+
parameters = config_data.get("parameters", {})
|
|
443
|
+
blocks = parameters.get("blocks", [])
|
|
444
|
+
|
|
445
|
+
if blocks:
|
|
446
|
+
lines: list[str] = []
|
|
447
|
+
for block in blocks:
|
|
448
|
+
block_name = block.get("name", "unnamed")
|
|
449
|
+
lines.append(PYTHON_BLOCK_MARKER.format(name=block_name))
|
|
450
|
+
lines.append("")
|
|
451
|
+
|
|
452
|
+
for code in block.get("codes", []):
|
|
453
|
+
code_name = code.get("name", "unnamed")
|
|
454
|
+
lines.append(PYTHON_CODE_MARKER.format(name=code_name))
|
|
455
|
+
|
|
456
|
+
scripts = code.get("script") or []
|
|
457
|
+
for si, script in enumerate(scripts):
|
|
458
|
+
if si > 0:
|
|
459
|
+
lines.append("") # blank line between script elements
|
|
460
|
+
if isinstance(script, str) and "\n" in script:
|
|
461
|
+
lines.extend(script.split("\n"))
|
|
462
|
+
else:
|
|
463
|
+
lines.append(script)
|
|
464
|
+
lines.append("")
|
|
465
|
+
|
|
466
|
+
py_content = "\n".join(lines).rstrip() + "\n"
|
|
467
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
468
|
+
(config_dir / "transform.py").write_text(py_content, encoding="utf-8")
|
|
469
|
+
|
|
470
|
+
# Remove blocks from parameters
|
|
471
|
+
parameters.pop("blocks", None)
|
|
472
|
+
|
|
473
|
+
# Extract packages to pyproject.toml
|
|
474
|
+
packages = parameters.get("packages", [])
|
|
475
|
+
if packages:
|
|
476
|
+
config_name = config_data.get("name", "transformation")
|
|
477
|
+
_write_pyproject_toml(config_dir, config_name, packages, component_id=None, config_id=None)
|
|
478
|
+
parameters.pop("packages", None)
|
|
479
|
+
|
|
480
|
+
return config_data
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _merge_python_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
484
|
+
"""Read transform.py and pyproject.toml back into config_data."""
|
|
485
|
+
py_file = config_dir / "transform.py"
|
|
486
|
+
if py_file.exists():
|
|
487
|
+
content = py_file.read_text(encoding="utf-8")
|
|
488
|
+
blocks = _parse_python_blocks(content)
|
|
489
|
+
parameters = config_data.setdefault("parameters", {})
|
|
490
|
+
parameters["blocks"] = blocks
|
|
491
|
+
|
|
492
|
+
# Read packages from pyproject.toml
|
|
493
|
+
packages = _read_pyproject_packages(config_dir)
|
|
494
|
+
if packages:
|
|
495
|
+
parameters = config_data.setdefault("parameters", {})
|
|
496
|
+
parameters["packages"] = packages
|
|
497
|
+
|
|
498
|
+
return config_data
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _parse_python_blocks(content: str) -> list[dict[str, Any]]:
|
|
502
|
+
"""Parse Python content with block/code markers into blocks structure."""
|
|
503
|
+
blocks: list[dict[str, Any]] = []
|
|
504
|
+
current_block: dict[str, Any] | None = None
|
|
505
|
+
current_code: dict[str, Any] | None = None
|
|
506
|
+
current_script_lines: list[str] = []
|
|
507
|
+
|
|
508
|
+
for line in content.split("\n"):
|
|
509
|
+
stripped = line.strip()
|
|
510
|
+
|
|
511
|
+
if stripped.startswith("# ===== BLOCK:") and stripped.endswith("====="):
|
|
512
|
+
if current_code is not None and current_block is not None:
|
|
513
|
+
current_code["script"] = _lines_to_script(current_script_lines)
|
|
514
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
515
|
+
current_code = None
|
|
516
|
+
current_script_lines = []
|
|
517
|
+
|
|
518
|
+
block_name = stripped[len("# ===== BLOCK:") :].rstrip(" =").strip()
|
|
519
|
+
current_block = {"name": block_name, "codes": []}
|
|
520
|
+
blocks.append(current_block)
|
|
521
|
+
continue
|
|
522
|
+
|
|
523
|
+
if stripped.startswith("# ===== CODE:") and stripped.endswith("====="):
|
|
524
|
+
if current_code is not None and current_block is not None:
|
|
525
|
+
current_code["script"] = _lines_to_script(current_script_lines)
|
|
526
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
527
|
+
current_script_lines = []
|
|
528
|
+
|
|
529
|
+
code_name = stripped[len("# ===== CODE:") :].rstrip(" =").strip()
|
|
530
|
+
current_code = {"name": code_name}
|
|
531
|
+
continue
|
|
532
|
+
|
|
533
|
+
if current_code is not None:
|
|
534
|
+
current_script_lines.append(line)
|
|
535
|
+
|
|
536
|
+
if current_code is not None and current_block is not None:
|
|
537
|
+
current_code["script"] = _lines_to_script(current_script_lines)
|
|
538
|
+
current_block.setdefault("codes", []).append(current_code)
|
|
539
|
+
|
|
540
|
+
if not blocks and content.strip():
|
|
541
|
+
blocks = [
|
|
542
|
+
{
|
|
543
|
+
"name": "Block 1",
|
|
544
|
+
"codes": [{"name": "Code 1", "script": _lines_to_script(content.split("\n"))}],
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
|
|
548
|
+
return blocks
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
# ---- Python Apps ----
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _extract_python_app(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
555
|
+
"""Extract parameters.code into code.py and packages into pyproject.toml."""
|
|
556
|
+
parameters = config_data.get("parameters", {})
|
|
557
|
+
|
|
558
|
+
code = parameters.get("code")
|
|
559
|
+
if code and isinstance(code, str):
|
|
560
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
561
|
+
(config_dir / "code.py").write_text(code, encoding="utf-8")
|
|
562
|
+
parameters.pop("code", None)
|
|
563
|
+
|
|
564
|
+
packages = parameters.get("packages", [])
|
|
565
|
+
if packages:
|
|
566
|
+
keboola_meta = config_data.get("_keboola", {})
|
|
567
|
+
_write_pyproject_toml(
|
|
568
|
+
config_dir,
|
|
569
|
+
config_data.get("name", "app"),
|
|
570
|
+
packages,
|
|
571
|
+
component_id=keboola_meta.get("component_id"),
|
|
572
|
+
config_id=keboola_meta.get("config_id"),
|
|
573
|
+
)
|
|
574
|
+
parameters.pop("packages", None)
|
|
575
|
+
|
|
576
|
+
return config_data
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _merge_python_app(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
|
|
580
|
+
"""Read code.py and pyproject.toml back into config_data."""
|
|
581
|
+
code_file = config_dir / "code.py"
|
|
582
|
+
if code_file.exists():
|
|
583
|
+
parameters = config_data.setdefault("parameters", {})
|
|
584
|
+
parameters["code"] = code_file.read_text(encoding="utf-8")
|
|
585
|
+
|
|
586
|
+
packages = _read_pyproject_packages(config_dir)
|
|
587
|
+
if packages:
|
|
588
|
+
parameters = config_data.setdefault("parameters", {})
|
|
589
|
+
parameters["packages"] = packages
|
|
590
|
+
|
|
591
|
+
return config_data
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ---- pyproject.toml helpers ----
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _write_pyproject_toml(
|
|
598
|
+
config_dir: Path,
|
|
599
|
+
name: str,
|
|
600
|
+
packages: list[str],
|
|
601
|
+
component_id: str | None = None,
|
|
602
|
+
config_id: str | None = None,
|
|
603
|
+
) -> None:
|
|
604
|
+
"""Write packages to a pyproject.toml file."""
|
|
605
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
606
|
+
|
|
607
|
+
# Sanitize name for pyproject
|
|
608
|
+
safe_name = name.lower().replace(" ", "-").replace("_", "-")
|
|
609
|
+
|
|
610
|
+
lines = [
|
|
611
|
+
"[project]",
|
|
612
|
+
f'name = "{safe_name}"',
|
|
613
|
+
'requires-python = ">=3.11"',
|
|
614
|
+
"dependencies = [",
|
|
615
|
+
]
|
|
616
|
+
for pkg in packages:
|
|
617
|
+
lines.append(f' "{pkg}",')
|
|
618
|
+
lines.append("]")
|
|
619
|
+
|
|
620
|
+
if component_id or config_id:
|
|
621
|
+
lines.append("")
|
|
622
|
+
lines.append("[tool.keboola]")
|
|
623
|
+
if component_id:
|
|
624
|
+
lines.append(f'component_id = "{component_id}"')
|
|
625
|
+
if config_id:
|
|
626
|
+
lines.append(f'config_id = "{config_id}"')
|
|
627
|
+
|
|
628
|
+
lines.append("") # trailing newline
|
|
629
|
+
(config_dir / "pyproject.toml").write_text("\n".join(lines), encoding="utf-8")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _read_pyproject_packages(config_dir: Path) -> list[str]:
|
|
633
|
+
"""Read packages from pyproject.toml dependencies."""
|
|
634
|
+
toml_file = config_dir / "pyproject.toml"
|
|
635
|
+
if not toml_file.exists():
|
|
636
|
+
return []
|
|
637
|
+
|
|
638
|
+
content = toml_file.read_text(encoding="utf-8")
|
|
639
|
+
packages: list[str] = []
|
|
640
|
+
in_deps = False
|
|
641
|
+
|
|
642
|
+
for line in content.split("\n"):
|
|
643
|
+
stripped = line.strip()
|
|
644
|
+
if stripped == "dependencies = [":
|
|
645
|
+
in_deps = True
|
|
646
|
+
continue
|
|
647
|
+
if in_deps:
|
|
648
|
+
if stripped == "]":
|
|
649
|
+
break
|
|
650
|
+
# Strip quotes and trailing comma
|
|
651
|
+
pkg = stripped.strip('", ')
|
|
652
|
+
if pkg:
|
|
653
|
+
packages.append(pkg)
|
|
654
|
+
|
|
655
|
+
return packages
|