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,1277 @@
|
|
|
1
|
+
"""Sync commands - init, pull, push, diff, and status for local filesystem sync.
|
|
2
|
+
|
|
3
|
+
Thin CLI layer: parses arguments, calls SyncService, formats output.
|
|
4
|
+
No business logic belongs here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from ..errors import ConfigError, ErrorCode, KeboolaApiError, SyncConflictError
|
|
13
|
+
from ._helpers import check_cli_permission, get_formatter, get_service, map_error_to_exit_code
|
|
14
|
+
|
|
15
|
+
sync_app = typer.Typer(help="Sync project configurations with local filesystem")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@sync_app.callback(invoke_without_command=True)
|
|
19
|
+
def _sync_permission_check(ctx: typer.Context) -> None:
|
|
20
|
+
check_cli_permission(ctx, "sync")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
KEBOOLA_DIR = ".keboola"
|
|
24
|
+
MANIFEST_FILE = "manifest.json"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _safe_resolve_dir(directory: Path) -> Path:
|
|
28
|
+
"""Resolve a directory path safely (handles deleted CWD)."""
|
|
29
|
+
try:
|
|
30
|
+
return directory.resolve()
|
|
31
|
+
except (OSError, ValueError):
|
|
32
|
+
return Path(str(directory)).expanduser()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_project_root(directory: Path, alias: str | None = None) -> Path:
|
|
36
|
+
"""Find the project root directory containing .keboola/manifest.json.
|
|
37
|
+
|
|
38
|
+
Tries in order:
|
|
39
|
+
1. directory itself (explicit --directory or current dir)
|
|
40
|
+
2. directory/{alias}/ (auto-detect subdirectory from --all-projects layout)
|
|
41
|
+
"""
|
|
42
|
+
root = _safe_resolve_dir(directory)
|
|
43
|
+
if (root / KEBOOLA_DIR / MANIFEST_FILE).exists():
|
|
44
|
+
return root
|
|
45
|
+
if alias:
|
|
46
|
+
sub = root / alias
|
|
47
|
+
if (sub / KEBOOLA_DIR / MANIFEST_FILE).exists():
|
|
48
|
+
return sub
|
|
49
|
+
return root # let caller handle the error
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_override_file(path: Path) -> dict[str, str]:
|
|
53
|
+
"""Load a clone override map (JSON or YAML) as a flat ``{str: str}`` dict.
|
|
54
|
+
|
|
55
|
+
YAML's loader also parses JSON, so a single path handles both. Every value
|
|
56
|
+
is coerced to ``str`` (bucket ids, variable values, and path prefixes are
|
|
57
|
+
all strings).
|
|
58
|
+
"""
|
|
59
|
+
import yaml
|
|
60
|
+
|
|
61
|
+
if not path.exists():
|
|
62
|
+
raise ConfigError(f"Override file not found: {path}")
|
|
63
|
+
try:
|
|
64
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
65
|
+
except yaml.YAMLError as exc:
|
|
66
|
+
raise ConfigError(f"Cannot parse override file {path}: {exc}") from exc
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
raise ConfigError(f"Override file {path} must contain a JSON/YAML object (mapping).")
|
|
69
|
+
return {str(key): str(value) for key, value in data.items()}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _change_label(change: dict) -> str:
|
|
73
|
+
"""Build a human-readable label for a config change entry."""
|
|
74
|
+
path = change.get("path", "")
|
|
75
|
+
name = change.get("config_name", "")
|
|
76
|
+
component = change["component_id"]
|
|
77
|
+
if path:
|
|
78
|
+
return f"{component}/{path}"
|
|
79
|
+
if name:
|
|
80
|
+
return f"{component}/{name}"
|
|
81
|
+
config_id = change.get("config_id", "")
|
|
82
|
+
return f"{component}/{config_id}" if config_id else component
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@sync_app.command("init")
|
|
86
|
+
def sync_init(
|
|
87
|
+
ctx: typer.Context,
|
|
88
|
+
project: str = typer.Option(
|
|
89
|
+
...,
|
|
90
|
+
"--project",
|
|
91
|
+
help="Project alias to initialize sync for",
|
|
92
|
+
),
|
|
93
|
+
directory: Path = typer.Option(
|
|
94
|
+
Path("."),
|
|
95
|
+
"--directory",
|
|
96
|
+
"-d",
|
|
97
|
+
help="Target directory for the project files",
|
|
98
|
+
),
|
|
99
|
+
git_branching: bool = typer.Option(
|
|
100
|
+
False,
|
|
101
|
+
"--git-branching",
|
|
102
|
+
help="Enable git-branching mode (maps git branches to Keboola branches)",
|
|
103
|
+
),
|
|
104
|
+
adopt_existing: bool = typer.Option(
|
|
105
|
+
False,
|
|
106
|
+
"--adopt-existing",
|
|
107
|
+
help="Adopt an existing .keboola/manifest.json (e.g. written by kbc) "
|
|
108
|
+
"instead of failing. Validates the manifest's project_id against the alias "
|
|
109
|
+
"and normalises the file. Idempotent.",
|
|
110
|
+
),
|
|
111
|
+
) -> None:
|
|
112
|
+
"""Initialize a sync working directory for a Keboola project.
|
|
113
|
+
|
|
114
|
+
Creates the .keboola/ directory with manifest.json containing
|
|
115
|
+
project metadata and naming conventions. Optionally enables
|
|
116
|
+
git-branching mode for branch-to-branch mapping.
|
|
117
|
+
|
|
118
|
+
Use --adopt-existing to register a directory that was already initialised
|
|
119
|
+
by the official kbc CLI without overwriting the manifest.
|
|
120
|
+
"""
|
|
121
|
+
formatter = get_formatter(ctx)
|
|
122
|
+
service = get_service(ctx, "sync_service")
|
|
123
|
+
project_root = directory.resolve()
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
result = service.init_sync(
|
|
127
|
+
alias=project,
|
|
128
|
+
project_root=project_root,
|
|
129
|
+
git_branching=git_branching,
|
|
130
|
+
adopt_existing=adopt_existing,
|
|
131
|
+
)
|
|
132
|
+
except ConfigError as exc:
|
|
133
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
134
|
+
raise typer.Exit(code=5) from None
|
|
135
|
+
except FileExistsError as exc:
|
|
136
|
+
formatter.error(message=str(exc), error_code=ErrorCode.ALREADY_EXISTS)
|
|
137
|
+
raise typer.Exit(code=1) from None
|
|
138
|
+
except KeboolaApiError as exc:
|
|
139
|
+
formatter.error(
|
|
140
|
+
message=exc.message,
|
|
141
|
+
error_code=exc.error_code,
|
|
142
|
+
retryable=exc.retryable,
|
|
143
|
+
)
|
|
144
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
145
|
+
|
|
146
|
+
if formatter.json_mode:
|
|
147
|
+
formatter.output(result)
|
|
148
|
+
else:
|
|
149
|
+
status = result.get("status", "initialized")
|
|
150
|
+
if status == "adopted":
|
|
151
|
+
formatter.success(
|
|
152
|
+
f"Adopted manifest for project '{result['project_alias']}' "
|
|
153
|
+
f"(ID: {result['project_id']})"
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
formatter.success(
|
|
157
|
+
f"Initialized sync for project '{result['project_alias']}' "
|
|
158
|
+
f"(ID: {result['project_id']})"
|
|
159
|
+
)
|
|
160
|
+
formatter.console.print(f" API host: {result['api_host']}")
|
|
161
|
+
if result["git_branching"]:
|
|
162
|
+
formatter.console.print(
|
|
163
|
+
f" Git-branching: enabled (default branch: {result['default_branch']})"
|
|
164
|
+
)
|
|
165
|
+
for f in result["files_created"]:
|
|
166
|
+
formatter.console.print(f" Created: {f}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _format_pull_result(formatter: Any, result: dict) -> None:
|
|
170
|
+
"""Format a single-project pull result for human output."""
|
|
171
|
+
is_dry = result.get("status") == "dry_run"
|
|
172
|
+
details = result.get("details", [])
|
|
173
|
+
new_cfgs = [d for d in details if d["action"] == "new"]
|
|
174
|
+
updated_cfgs = [d for d in details if d["action"] == "updated"]
|
|
175
|
+
removed_cfgs = [d for d in details if d["action"] == "removed"]
|
|
176
|
+
renamed_cfgs = [d for d in details if d["action"] == "renamed"]
|
|
177
|
+
skipped_cfgs = [d for d in details if d["action"] == "skipped"]
|
|
178
|
+
|
|
179
|
+
has_changes = bool(new_cfgs or updated_cfgs or removed_cfgs or renamed_cfgs)
|
|
180
|
+
|
|
181
|
+
storage = result.get("storage", {})
|
|
182
|
+
jobs_written = result.get("jobs_written", 0)
|
|
183
|
+
has_extra = bool(storage.get("buckets") or storage.get("tables") or jobs_written)
|
|
184
|
+
|
|
185
|
+
if not has_changes and not skipped_cfgs and not has_extra:
|
|
186
|
+
formatter.console.print("[green]Already up to date.[/green] No changes from remote.")
|
|
187
|
+
return
|
|
188
|
+
elif is_dry:
|
|
189
|
+
formatter.console.print("[yellow]Dry run -- no files written:[/yellow]")
|
|
190
|
+
formatter.console.print(
|
|
191
|
+
f" Would pull {result['configs_pulled']} configurations "
|
|
192
|
+
f"({result['rows_pulled']} rows), "
|
|
193
|
+
f"write {result['files_written']} files"
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
formatter.success(
|
|
197
|
+
f"Pulled {result['configs_pulled']} configurations "
|
|
198
|
+
f"({result['rows_pulled']} rows) "
|
|
199
|
+
f"into {result['branch_dir']}/"
|
|
200
|
+
)
|
|
201
|
+
formatter.console.print(f" Files written: {result['files_written']}")
|
|
202
|
+
if storage.get("buckets") or storage.get("tables"):
|
|
203
|
+
formatter.console.print(
|
|
204
|
+
f" Storage: {storage.get('buckets', 0)} buckets, {storage.get('tables', 0)} tables"
|
|
205
|
+
)
|
|
206
|
+
if storage.get("samples"):
|
|
207
|
+
formatter.console.print(f" Samples: {storage['samples']} tables")
|
|
208
|
+
if jobs_written:
|
|
209
|
+
formatter.console.print(f" Jobs: {jobs_written} configs with job history")
|
|
210
|
+
|
|
211
|
+
if renamed_cfgs:
|
|
212
|
+
formatter.console.print(f" [magenta]Renamed ({len(renamed_cfgs)}):[/magenta]")
|
|
213
|
+
for d in renamed_cfgs:
|
|
214
|
+
formatter.console.print(
|
|
215
|
+
f" > {d.get('old_path', '?')} -> {d['component_id']}/{d['config_name']}"
|
|
216
|
+
)
|
|
217
|
+
if new_cfgs:
|
|
218
|
+
formatter.console.print(f" [green]New ({len(new_cfgs)}):[/green]")
|
|
219
|
+
for d in new_cfgs:
|
|
220
|
+
formatter.console.print(f" + {d['component_id']}/{d['config_name']}")
|
|
221
|
+
if updated_cfgs:
|
|
222
|
+
formatter.console.print(f" [yellow]Updated ({len(updated_cfgs)}):[/yellow]")
|
|
223
|
+
for d in updated_cfgs:
|
|
224
|
+
formatter.console.print(f" ~ {d['component_id']}/{d['config_name']}")
|
|
225
|
+
if removed_cfgs:
|
|
226
|
+
formatter.console.print(f" [red]Removed from remote ({len(removed_cfgs)}):[/red]")
|
|
227
|
+
for d in removed_cfgs:
|
|
228
|
+
formatter.console.print(f" - {d['path']}")
|
|
229
|
+
if skipped_cfgs:
|
|
230
|
+
formatter.console.print(
|
|
231
|
+
f" [cyan]Skipped ({len(skipped_cfgs)}) -- locally modified:[/cyan]"
|
|
232
|
+
)
|
|
233
|
+
for d in skipped_cfgs:
|
|
234
|
+
formatter.console.print(f" ! {d['component_id']}/{d['config_name']}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _format_diff_result(formatter: Any, result: dict) -> None:
|
|
238
|
+
"""Format a single-project diff result for human output."""
|
|
239
|
+
changes = result["changes"]
|
|
240
|
+
summary = result["summary"]
|
|
241
|
+
remote_only = result.get("remote_only", [])
|
|
242
|
+
|
|
243
|
+
if not changes and not remote_only:
|
|
244
|
+
formatter.console.print("[green]No differences.[/green]")
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
local_changes = [c for c in changes if c["change_type"] in ("added", "modified", "deleted")]
|
|
248
|
+
remote_changes = [c for c in changes if c["change_type"] == "remote_modified"]
|
|
249
|
+
conflict_changes = [c for c in changes if c["change_type"] == "conflict"]
|
|
250
|
+
|
|
251
|
+
if local_changes:
|
|
252
|
+
for change in local_changes:
|
|
253
|
+
ct = change["change_type"]
|
|
254
|
+
label = _change_label(change)
|
|
255
|
+
prefix = {"added": "+", "modified": "~", "deleted": "-"}.get(ct, "?")
|
|
256
|
+
formatter.console.print(f" {prefix} {ct.upper()} {label}")
|
|
257
|
+
formatter.console.print(
|
|
258
|
+
f" {summary['added']} to create, {summary['modified']} to update, "
|
|
259
|
+
f"{summary['deleted']} to delete"
|
|
260
|
+
)
|
|
261
|
+
if remote_changes:
|
|
262
|
+
for change in remote_changes:
|
|
263
|
+
formatter.console.print(f" ~ REMOTE MODIFIED {_change_label(change)}")
|
|
264
|
+
if conflict_changes:
|
|
265
|
+
for change in conflict_changes:
|
|
266
|
+
formatter.console.print(f" ! CONFLICT {_change_label(change)}")
|
|
267
|
+
if remote_only:
|
|
268
|
+
formatter.console.print(f" {len(remote_only)} new remote-only config(s)")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _format_conflict_list(formatter: Any, conflicts: list[dict[str, str]]) -> None:
|
|
272
|
+
"""Print the per-config force-pull conflict list (human mode only)."""
|
|
273
|
+
if not conflicts:
|
|
274
|
+
return
|
|
275
|
+
n = len(conflicts)
|
|
276
|
+
formatter.console.print(
|
|
277
|
+
f"\n[bold red]Merge conflict:[/bold red] {n} config(s) changed BOTH "
|
|
278
|
+
f"locally and on the remote since the last pull:"
|
|
279
|
+
)
|
|
280
|
+
for c in conflicts:
|
|
281
|
+
label = "row" if c.get("scope") == "row" else "config"
|
|
282
|
+
formatter.console.print(
|
|
283
|
+
f" [red]![/red] {c.get('component_id')}/{c.get('config_id')} "
|
|
284
|
+
f"[dim]({label})[/dim] {c.get('config_name', '')}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _format_push_result(formatter: Any, result: dict) -> None:
|
|
289
|
+
"""Format a single-project push result for human output."""
|
|
290
|
+
status = result.get("status", "")
|
|
291
|
+
if status == "no_changes":
|
|
292
|
+
formatter.console.print(" No changes to push.")
|
|
293
|
+
return
|
|
294
|
+
if status == "dry_run":
|
|
295
|
+
summary = result.get("summary", {})
|
|
296
|
+
formatter.console.print(
|
|
297
|
+
f" Would create {summary.get('added', 0)}, "
|
|
298
|
+
f"update {summary.get('modified', 0)}, "
|
|
299
|
+
f"delete {summary.get('deleted', 0)}"
|
|
300
|
+
)
|
|
301
|
+
return
|
|
302
|
+
formatter.console.print(
|
|
303
|
+
f" {result.get('created', 0)} created, "
|
|
304
|
+
f"{result.get('updated', 0)} updated, "
|
|
305
|
+
f"{result.get('deleted', 0)} deleted"
|
|
306
|
+
)
|
|
307
|
+
# Show name drift warnings
|
|
308
|
+
drift_warnings = result.get("name_drift_warnings", [])
|
|
309
|
+
if drift_warnings:
|
|
310
|
+
formatter.console.print(
|
|
311
|
+
f"\n [yellow]Warning: {len(drift_warnings)} config(s) have "
|
|
312
|
+
f"local directory names that don't match their config name:[/yellow]"
|
|
313
|
+
)
|
|
314
|
+
for w in drift_warnings:
|
|
315
|
+
formatter.console.print(
|
|
316
|
+
f" '{w['local_dirname']}' should be "
|
|
317
|
+
f"'{w['expected_dirname']}' (config: {w['config_name']})"
|
|
318
|
+
)
|
|
319
|
+
formatter.console.print(" Run 'kbagent config rename' or 'kbagent sync pull' to fix.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _pull_one_liner(result: dict) -> str:
|
|
323
|
+
"""One-line summary of a single pull result."""
|
|
324
|
+
details = result.get("details", [])
|
|
325
|
+
new_n = sum(1 for d in details if d["action"] == "new")
|
|
326
|
+
upd_n = sum(1 for d in details if d["action"] == "updated")
|
|
327
|
+
rem_n = sum(1 for d in details if d["action"] == "removed")
|
|
328
|
+
ren_n = sum(1 for d in details if d["action"] == "renamed")
|
|
329
|
+
skip_n = sum(1 for d in details if d["action"] == "skipped")
|
|
330
|
+
if not new_n and not upd_n and not rem_n and not ren_n and not skip_n:
|
|
331
|
+
return "[green]up to date[/green]"
|
|
332
|
+
parts = []
|
|
333
|
+
if ren_n:
|
|
334
|
+
parts.append(f"[magenta]>{ren_n} renamed[/magenta]")
|
|
335
|
+
if new_n:
|
|
336
|
+
parts.append(f"[green]+{new_n} new[/green]")
|
|
337
|
+
if upd_n:
|
|
338
|
+
parts.append(f"[yellow]~{upd_n} updated[/yellow]")
|
|
339
|
+
if rem_n:
|
|
340
|
+
parts.append(f"[red]-{rem_n} removed[/red]")
|
|
341
|
+
if skip_n:
|
|
342
|
+
parts.append(f"[cyan]!{skip_n} skipped[/cyan]")
|
|
343
|
+
return ", ".join(parts)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _diff_one_liner(result: dict) -> str:
|
|
347
|
+
"""One-line summary of a single diff result."""
|
|
348
|
+
s = result.get("summary", {})
|
|
349
|
+
mod = s.get("modified", 0)
|
|
350
|
+
add = s.get("added", 0)
|
|
351
|
+
dlt = s.get("deleted", 0)
|
|
352
|
+
rmod = s.get("remote_modified", 0)
|
|
353
|
+
conf = s.get("conflict", 0)
|
|
354
|
+
ro = s.get("remote_only", 0)
|
|
355
|
+
if not any([mod, add, dlt, rmod, conf, ro]):
|
|
356
|
+
return "[green]in sync[/green]"
|
|
357
|
+
parts = []
|
|
358
|
+
if add:
|
|
359
|
+
parts.append(f"[green]{add} to create[/green]")
|
|
360
|
+
if mod:
|
|
361
|
+
parts.append(f"[yellow]{mod} to push[/yellow]")
|
|
362
|
+
if dlt:
|
|
363
|
+
parts.append(f"[red]{dlt} to delete[/red]")
|
|
364
|
+
if rmod:
|
|
365
|
+
parts.append(f"[cyan]{rmod} to pull[/cyan]")
|
|
366
|
+
if conf:
|
|
367
|
+
parts.append(f"[red]{conf} conflicts[/red]")
|
|
368
|
+
if ro:
|
|
369
|
+
parts.append(f"[cyan]{ro} new remote[/cyan]")
|
|
370
|
+
return ", ".join(parts)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _push_one_liner(result: dict) -> str:
|
|
374
|
+
"""One-line summary of a single push result."""
|
|
375
|
+
status = result.get("status", "")
|
|
376
|
+
if status == "no_changes":
|
|
377
|
+
return "[green]nothing to push[/green]"
|
|
378
|
+
if status == "dry_run":
|
|
379
|
+
s = result.get("summary", {})
|
|
380
|
+
return f"would: +{s.get('added', 0)} ~{s.get('modified', 0)} -{s.get('deleted', 0)}"
|
|
381
|
+
c = result.get("created", 0)
|
|
382
|
+
u = result.get("updated", 0)
|
|
383
|
+
d = result.get("deleted", 0)
|
|
384
|
+
return f"+{c} created, ~{u} updated, -{d} deleted"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _format_all_results(
|
|
388
|
+
formatter: Any,
|
|
389
|
+
data: dict,
|
|
390
|
+
per_project_formatter: Any = None,
|
|
391
|
+
one_liner: Any = None,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Format multi-project results for human output.
|
|
394
|
+
|
|
395
|
+
In default mode: one line per project (compact summary).
|
|
396
|
+
With --verbose: full detail per project.
|
|
397
|
+
Errors always shown.
|
|
398
|
+
"""
|
|
399
|
+
summary = data["summary"]
|
|
400
|
+
projects = data["projects"]
|
|
401
|
+
skipped = data.get("skipped", [])
|
|
402
|
+
verbose = getattr(formatter, "verbose", False)
|
|
403
|
+
|
|
404
|
+
for alias in sorted(projects):
|
|
405
|
+
proj_result = projects[alias]
|
|
406
|
+
if "error" in proj_result:
|
|
407
|
+
formatter.console.print(f" [red]x[/red] {alias}: [red]{proj_result['error']}[/red]")
|
|
408
|
+
elif verbose and per_project_formatter:
|
|
409
|
+
formatter.console.print(f"\n[bold]{alias}:[/bold]")
|
|
410
|
+
per_project_formatter(formatter, proj_result)
|
|
411
|
+
elif one_liner:
|
|
412
|
+
formatter.console.print(f" [green]OK[/green] {alias}: {one_liner(proj_result)}")
|
|
413
|
+
else:
|
|
414
|
+
formatter.console.print(f" [green]OK[/green] {alias}")
|
|
415
|
+
|
|
416
|
+
if skipped:
|
|
417
|
+
formatter.console.print(f"\n[dim]Skipped (no manifest): {', '.join(skipped)}[/dim]")
|
|
418
|
+
|
|
419
|
+
formatter.console.print(
|
|
420
|
+
f"\n{summary['total']} projects: "
|
|
421
|
+
f"[green]{summary['success']} OK[/green], "
|
|
422
|
+
f"[red]{summary['failed']} failed[/red]"
|
|
423
|
+
+ (f", {summary.get('skipped', 0)} skipped" if summary.get("skipped") else "")
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@sync_app.command("pull")
|
|
428
|
+
def sync_pull(
|
|
429
|
+
ctx: typer.Context,
|
|
430
|
+
project: str | None = typer.Option(
|
|
431
|
+
None,
|
|
432
|
+
"--project",
|
|
433
|
+
help="Project alias to pull configurations from",
|
|
434
|
+
),
|
|
435
|
+
all_projects: bool = typer.Option(
|
|
436
|
+
False,
|
|
437
|
+
"--all-projects",
|
|
438
|
+
help="Pull all configured projects in parallel",
|
|
439
|
+
),
|
|
440
|
+
directory: Path = typer.Option(
|
|
441
|
+
Path("."),
|
|
442
|
+
"--directory",
|
|
443
|
+
"-d",
|
|
444
|
+
help="Project root directory (must contain .keboola/)",
|
|
445
|
+
),
|
|
446
|
+
force: bool = typer.Option(
|
|
447
|
+
False,
|
|
448
|
+
"--force",
|
|
449
|
+
help=(
|
|
450
|
+
"Force re-pull. Locally-modified configs whose remote is unchanged "
|
|
451
|
+
"are PRESERVED (kept as pending changes for `sync push`); a true "
|
|
452
|
+
"merge conflict (local AND remote both changed since the last pull) "
|
|
453
|
+
"aborts the pull so you can resolve it."
|
|
454
|
+
),
|
|
455
|
+
),
|
|
456
|
+
dry_run: bool = typer.Option(
|
|
457
|
+
False,
|
|
458
|
+
"--dry-run",
|
|
459
|
+
help="Show what would be pulled without writing any files",
|
|
460
|
+
),
|
|
461
|
+
job_limit: int = typer.Option(
|
|
462
|
+
5,
|
|
463
|
+
"--job-limit",
|
|
464
|
+
help="Max recent jobs to pull per configuration (default 5)",
|
|
465
|
+
),
|
|
466
|
+
no_storage: bool = typer.Option(
|
|
467
|
+
False,
|
|
468
|
+
"--no-storage",
|
|
469
|
+
help="Skip downloading storage bucket/table metadata",
|
|
470
|
+
),
|
|
471
|
+
no_jobs: bool = typer.Option(
|
|
472
|
+
False,
|
|
473
|
+
"--no-jobs",
|
|
474
|
+
help="Skip downloading per-config job history",
|
|
475
|
+
),
|
|
476
|
+
with_samples: bool = typer.Option(
|
|
477
|
+
False,
|
|
478
|
+
"--with-samples",
|
|
479
|
+
help="Download table data samples (CSV previews)",
|
|
480
|
+
),
|
|
481
|
+
sample_limit: int = typer.Option(
|
|
482
|
+
100,
|
|
483
|
+
"--sample-limit",
|
|
484
|
+
help="Max rows per table sample (default 100)",
|
|
485
|
+
),
|
|
486
|
+
max_samples: int = typer.Option(
|
|
487
|
+
50,
|
|
488
|
+
"--max-samples",
|
|
489
|
+
help="Max number of tables to sample (default 50)",
|
|
490
|
+
),
|
|
491
|
+
branch: int | None = typer.Option(
|
|
492
|
+
None,
|
|
493
|
+
"--branch",
|
|
494
|
+
help=(
|
|
495
|
+
"Dev branch ID. Overrides the manifest / 'branch use' active "
|
|
496
|
+
"branch for this single invocation. Requires exactly one --project."
|
|
497
|
+
),
|
|
498
|
+
),
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Download configurations from a Keboola project to local files.
|
|
501
|
+
|
|
502
|
+
Use --project for a single project or --all-projects for all configured
|
|
503
|
+
projects in parallel (each in its own subdirectory).
|
|
504
|
+
"""
|
|
505
|
+
formatter = get_formatter(ctx)
|
|
506
|
+
service = get_service(ctx, "sync_service")
|
|
507
|
+
|
|
508
|
+
if all_projects and project:
|
|
509
|
+
formatter.error(
|
|
510
|
+
message="Cannot use --project with --all-projects",
|
|
511
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
512
|
+
)
|
|
513
|
+
raise typer.Exit(code=2)
|
|
514
|
+
if not all_projects and not project:
|
|
515
|
+
formatter.error(
|
|
516
|
+
message="Specify --project ALIAS or --all-projects",
|
|
517
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
518
|
+
)
|
|
519
|
+
raise typer.Exit(code=2)
|
|
520
|
+
if branch is not None and all_projects:
|
|
521
|
+
formatter.error(
|
|
522
|
+
message="--branch requires --project (branch id is per-project)",
|
|
523
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
524
|
+
)
|
|
525
|
+
raise typer.Exit(code=2)
|
|
526
|
+
|
|
527
|
+
if all_projects:
|
|
528
|
+
base_dir = _safe_resolve_dir(directory)
|
|
529
|
+
try:
|
|
530
|
+
data = service.pull_all(
|
|
531
|
+
base_dir,
|
|
532
|
+
force=force,
|
|
533
|
+
dry_run=dry_run,
|
|
534
|
+
job_limit=job_limit,
|
|
535
|
+
no_storage=no_storage,
|
|
536
|
+
no_jobs=no_jobs,
|
|
537
|
+
with_samples=with_samples,
|
|
538
|
+
sample_limit=sample_limit,
|
|
539
|
+
max_samples=max_samples,
|
|
540
|
+
)
|
|
541
|
+
except ConfigError as exc:
|
|
542
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
543
|
+
raise typer.Exit(code=5) from None
|
|
544
|
+
|
|
545
|
+
if formatter.json_mode:
|
|
546
|
+
formatter.output(data)
|
|
547
|
+
else:
|
|
548
|
+
_format_all_results(formatter, data, _format_pull_result, _pull_one_liner)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
project_root = _resolve_project_root(directory, project)
|
|
552
|
+
|
|
553
|
+
# Auto-init if no manifest exists (same as --all-projects behavior)
|
|
554
|
+
manifest_path = project_root / KEBOOLA_DIR / MANIFEST_FILE
|
|
555
|
+
if not manifest_path.exists():
|
|
556
|
+
try:
|
|
557
|
+
service.init_sync(project, project_root)
|
|
558
|
+
except Exception as exc:
|
|
559
|
+
formatter.error(message=str(exc), error_code=ErrorCode.INIT_ERROR)
|
|
560
|
+
raise typer.Exit(code=1) from None
|
|
561
|
+
|
|
562
|
+
try:
|
|
563
|
+
result = service.pull(
|
|
564
|
+
alias=project,
|
|
565
|
+
project_root=project_root,
|
|
566
|
+
force=force,
|
|
567
|
+
dry_run=dry_run,
|
|
568
|
+
job_limit=job_limit,
|
|
569
|
+
no_storage=no_storage,
|
|
570
|
+
no_jobs=no_jobs,
|
|
571
|
+
with_samples=with_samples,
|
|
572
|
+
sample_limit=sample_limit,
|
|
573
|
+
max_samples=max_samples,
|
|
574
|
+
branch_override=branch,
|
|
575
|
+
)
|
|
576
|
+
except SyncConflictError as exc:
|
|
577
|
+
if not formatter.json_mode:
|
|
578
|
+
_format_conflict_list(formatter, exc.conflicts)
|
|
579
|
+
formatter.error(
|
|
580
|
+
message=exc.message,
|
|
581
|
+
error_code=exc.error_code,
|
|
582
|
+
project=project or "",
|
|
583
|
+
details={"conflicts": exc.conflicts},
|
|
584
|
+
)
|
|
585
|
+
raise typer.Exit(code=1) from None
|
|
586
|
+
except FileNotFoundError as exc:
|
|
587
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
588
|
+
raise typer.Exit(code=1) from None
|
|
589
|
+
except ConfigError as exc:
|
|
590
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
591
|
+
raise typer.Exit(code=5) from None
|
|
592
|
+
except KeboolaApiError as exc:
|
|
593
|
+
formatter.error(
|
|
594
|
+
message=exc.message,
|
|
595
|
+
error_code=exc.error_code,
|
|
596
|
+
retryable=exc.retryable,
|
|
597
|
+
)
|
|
598
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
599
|
+
|
|
600
|
+
if formatter.json_mode:
|
|
601
|
+
formatter.output(result)
|
|
602
|
+
else:
|
|
603
|
+
_format_pull_result(formatter, result)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@sync_app.command("status")
|
|
607
|
+
def sync_status(
|
|
608
|
+
ctx: typer.Context,
|
|
609
|
+
directory: Path = typer.Option(
|
|
610
|
+
Path("."),
|
|
611
|
+
"--directory",
|
|
612
|
+
"-d",
|
|
613
|
+
help="Project root directory (must contain .keboola/)",
|
|
614
|
+
),
|
|
615
|
+
) -> None:
|
|
616
|
+
"""Show which local configurations have been modified, added, or deleted.
|
|
617
|
+
|
|
618
|
+
Compares the local filesystem state against the manifest to detect
|
|
619
|
+
changes since the last pull.
|
|
620
|
+
"""
|
|
621
|
+
formatter = get_formatter(ctx)
|
|
622
|
+
service = get_service(ctx, "sync_service")
|
|
623
|
+
project_root = directory.resolve()
|
|
624
|
+
|
|
625
|
+
try:
|
|
626
|
+
result = service.status(project_root=project_root)
|
|
627
|
+
except FileNotFoundError as exc:
|
|
628
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
629
|
+
raise typer.Exit(code=1) from None
|
|
630
|
+
|
|
631
|
+
if formatter.json_mode:
|
|
632
|
+
formatter.output(result)
|
|
633
|
+
else:
|
|
634
|
+
modified = result["modified"]
|
|
635
|
+
added = result["added"]
|
|
636
|
+
deleted = result["deleted"]
|
|
637
|
+
unchanged = result["unchanged"]
|
|
638
|
+
secret_warnings = result.get("plaintext_secret_warnings", [])
|
|
639
|
+
|
|
640
|
+
if not modified and not added and not deleted:
|
|
641
|
+
formatter.console.print(
|
|
642
|
+
f"[green]No changes detected.[/green] ({unchanged} configurations tracked)"
|
|
643
|
+
)
|
|
644
|
+
else:
|
|
645
|
+
if modified:
|
|
646
|
+
formatter.console.print(f"\n[yellow]Modified ({len(modified)}):[/yellow]")
|
|
647
|
+
for m in modified:
|
|
648
|
+
formatter.console.print(f" M {m['path']}")
|
|
649
|
+
|
|
650
|
+
if added:
|
|
651
|
+
formatter.console.print(f"\n[green]Added ({len(added)}):[/green]")
|
|
652
|
+
for a in added:
|
|
653
|
+
formatter.console.print(f" A {a['path']}")
|
|
654
|
+
|
|
655
|
+
if deleted:
|
|
656
|
+
formatter.console.print(f"\n[red]Deleted ({len(deleted)}):[/red]")
|
|
657
|
+
for d in deleted:
|
|
658
|
+
formatter.console.print(f" D {d['path']}")
|
|
659
|
+
|
|
660
|
+
formatter.console.print(
|
|
661
|
+
f"\n{len(modified)} modified, {len(added)} added, "
|
|
662
|
+
f"{len(deleted)} deleted, {unchanged} unchanged"
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Plaintext secret warning -- independent of local change state (issue #378).
|
|
666
|
+
if secret_warnings:
|
|
667
|
+
formatter.console.print(
|
|
668
|
+
f"\n[bold red]PLAINTEXT SECRETS in {len(secret_warnings)} synced "
|
|
669
|
+
f"config(s)[/bold red] (issue #378) -- in sync with the remote but NOT encrypted:"
|
|
670
|
+
)
|
|
671
|
+
for w in secret_warnings:
|
|
672
|
+
keys = ", ".join(w["secret_keys"])
|
|
673
|
+
formatter.console.print(f" [red]![/red] {w['path']} ({keys})")
|
|
674
|
+
formatter.console.print(
|
|
675
|
+
"[dim] Re-push on kbagent >=0.54.0 to encrypt, then ROTATE the credential "
|
|
676
|
+
"-- config version history keeps the old plaintext.[/dim]"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
@sync_app.command("diff")
|
|
681
|
+
def sync_diff(
|
|
682
|
+
ctx: typer.Context,
|
|
683
|
+
project: str | None = typer.Option(
|
|
684
|
+
None,
|
|
685
|
+
"--project",
|
|
686
|
+
help="Project alias to diff against",
|
|
687
|
+
),
|
|
688
|
+
all_projects: bool = typer.Option(
|
|
689
|
+
False,
|
|
690
|
+
"--all-projects",
|
|
691
|
+
help="Diff all configured projects in parallel",
|
|
692
|
+
),
|
|
693
|
+
directory: Path = typer.Option(
|
|
694
|
+
Path("."),
|
|
695
|
+
"--directory",
|
|
696
|
+
"-d",
|
|
697
|
+
help="Project root directory (must contain .keboola/)",
|
|
698
|
+
),
|
|
699
|
+
branch: int | None = typer.Option(
|
|
700
|
+
None,
|
|
701
|
+
"--branch",
|
|
702
|
+
help=(
|
|
703
|
+
"Dev branch ID. Overrides the manifest / 'branch use' active "
|
|
704
|
+
"branch for this single invocation. Requires exactly one --project."
|
|
705
|
+
),
|
|
706
|
+
),
|
|
707
|
+
) -> None:
|
|
708
|
+
"""Show detailed diff between local and remote configurations.
|
|
709
|
+
|
|
710
|
+
Use --project for a single project or --all-projects for all configured
|
|
711
|
+
projects in parallel.
|
|
712
|
+
"""
|
|
713
|
+
formatter = get_formatter(ctx)
|
|
714
|
+
service = get_service(ctx, "sync_service")
|
|
715
|
+
|
|
716
|
+
if all_projects and project:
|
|
717
|
+
formatter.error(
|
|
718
|
+
message="Cannot use --project with --all-projects",
|
|
719
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
720
|
+
)
|
|
721
|
+
raise typer.Exit(code=2)
|
|
722
|
+
if not all_projects and not project:
|
|
723
|
+
formatter.error(
|
|
724
|
+
message="Specify --project ALIAS or --all-projects",
|
|
725
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
726
|
+
)
|
|
727
|
+
raise typer.Exit(code=2)
|
|
728
|
+
if branch is not None and all_projects:
|
|
729
|
+
formatter.error(
|
|
730
|
+
message="--branch requires --project (branch id is per-project)",
|
|
731
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
732
|
+
)
|
|
733
|
+
raise typer.Exit(code=2)
|
|
734
|
+
|
|
735
|
+
if all_projects:
|
|
736
|
+
base_dir = _safe_resolve_dir(directory)
|
|
737
|
+
try:
|
|
738
|
+
data = service.diff_all(base_dir)
|
|
739
|
+
except ConfigError as exc:
|
|
740
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
741
|
+
raise typer.Exit(code=5) from None
|
|
742
|
+
|
|
743
|
+
if formatter.json_mode:
|
|
744
|
+
formatter.output(data)
|
|
745
|
+
else:
|
|
746
|
+
_format_all_results(formatter, data, _format_diff_result, _diff_one_liner)
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
project_root = _resolve_project_root(directory, project)
|
|
750
|
+
|
|
751
|
+
try:
|
|
752
|
+
result = service.diff(alias=project, project_root=project_root, branch_override=branch)
|
|
753
|
+
except FileNotFoundError as exc:
|
|
754
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
755
|
+
raise typer.Exit(code=1) from None
|
|
756
|
+
except ConfigError as exc:
|
|
757
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
758
|
+
raise typer.Exit(code=5) from None
|
|
759
|
+
except KeboolaApiError as exc:
|
|
760
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
761
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
762
|
+
|
|
763
|
+
if formatter.json_mode:
|
|
764
|
+
formatter.output(result)
|
|
765
|
+
else:
|
|
766
|
+
changes = result["changes"]
|
|
767
|
+
summary = result["summary"]
|
|
768
|
+
remote_only = result.get("remote_only", [])
|
|
769
|
+
|
|
770
|
+
if not changes and not remote_only:
|
|
771
|
+
formatter.console.print(
|
|
772
|
+
"[green]No differences found.[/green] Local and remote are in sync."
|
|
773
|
+
)
|
|
774
|
+
return
|
|
775
|
+
|
|
776
|
+
# Categorize changes by direction
|
|
777
|
+
prefix_map = {
|
|
778
|
+
"added": "[green]+ ",
|
|
779
|
+
"modified": "[yellow]~ ",
|
|
780
|
+
"remote_modified": "[cyan]~ ",
|
|
781
|
+
"conflict": "[red]! ",
|
|
782
|
+
"deleted": "[red]- ",
|
|
783
|
+
}
|
|
784
|
+
suffix_map = {
|
|
785
|
+
"added": "[/green]",
|
|
786
|
+
"modified": "[/yellow]",
|
|
787
|
+
"remote_modified": "[/cyan]",
|
|
788
|
+
"conflict": "[/red]",
|
|
789
|
+
"deleted": "[/red]",
|
|
790
|
+
}
|
|
791
|
+
label_map = {
|
|
792
|
+
"added": "ADDED",
|
|
793
|
+
"modified": "MODIFIED",
|
|
794
|
+
"remote_modified": "REMOTE MODIFIED",
|
|
795
|
+
"conflict": "CONFLICT",
|
|
796
|
+
"deleted": "DELETED",
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
local_changes = [c for c in changes if c["change_type"] in ("added", "modified", "deleted")]
|
|
800
|
+
remote_changes = [c for c in changes if c["change_type"] == "remote_modified"]
|
|
801
|
+
conflict_changes = [c for c in changes if c["change_type"] == "conflict"]
|
|
802
|
+
|
|
803
|
+
# Local changes (what push would do)
|
|
804
|
+
if local_changes:
|
|
805
|
+
formatter.console.print("[bold]Local changes (push would apply):[/bold]")
|
|
806
|
+
for change in local_changes:
|
|
807
|
+
ct = change["change_type"]
|
|
808
|
+
label = _change_label(change)
|
|
809
|
+
formatter.console.print(
|
|
810
|
+
f" {prefix_map[ct]}{label_map[ct]} {label}{suffix_map[ct]}"
|
|
811
|
+
)
|
|
812
|
+
for detail in change.get("details", []):
|
|
813
|
+
formatter.console.print(f" {detail}")
|
|
814
|
+
formatter.console.print(
|
|
815
|
+
f"\n{summary['added']} to create, {summary['modified']} to update, "
|
|
816
|
+
f"{summary['deleted']} to delete"
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# Remote changes (need pull)
|
|
820
|
+
if remote_changes:
|
|
821
|
+
if local_changes:
|
|
822
|
+
formatter.console.print()
|
|
823
|
+
formatter.console.print("[bold]Remote changes (run 'sync pull' to fetch):[/bold]")
|
|
824
|
+
for change in remote_changes:
|
|
825
|
+
label = _change_label(change)
|
|
826
|
+
formatter.console.print(f" [cyan]~ REMOTE MODIFIED {label}[/cyan]")
|
|
827
|
+
for detail in change.get("details", []):
|
|
828
|
+
formatter.console.print(f" {detail}")
|
|
829
|
+
|
|
830
|
+
# Conflicts (both sides changed)
|
|
831
|
+
if conflict_changes:
|
|
832
|
+
if local_changes or remote_changes:
|
|
833
|
+
formatter.console.print()
|
|
834
|
+
formatter.console.print(
|
|
835
|
+
"[bold red]Conflicts (both local and remote changed):[/bold red]"
|
|
836
|
+
)
|
|
837
|
+
for change in conflict_changes:
|
|
838
|
+
label = _change_label(change)
|
|
839
|
+
formatter.console.print(f" [red]! CONFLICT {label}[/red]")
|
|
840
|
+
for detail in change.get("details", []):
|
|
841
|
+
formatter.console.print(f" {detail}")
|
|
842
|
+
|
|
843
|
+
# Remote-only configs (new on server, not yet pulled)
|
|
844
|
+
if remote_only:
|
|
845
|
+
if changes:
|
|
846
|
+
formatter.console.print()
|
|
847
|
+
formatter.console.print(
|
|
848
|
+
f"[bold]Remote only ({len(remote_only)} new, run 'sync pull' to fetch):[/bold]"
|
|
849
|
+
)
|
|
850
|
+
for cfg in remote_only:
|
|
851
|
+
name = cfg.get("config_name", cfg.get("config_id", ""))
|
|
852
|
+
formatter.console.print(f" [cyan]+ NEW {cfg['component_id']}/{name}[/cyan]")
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@sync_app.command("push")
|
|
856
|
+
def sync_push(
|
|
857
|
+
ctx: typer.Context,
|
|
858
|
+
project: str | None = typer.Option(
|
|
859
|
+
None,
|
|
860
|
+
"--project",
|
|
861
|
+
help="Project alias to push changes to",
|
|
862
|
+
),
|
|
863
|
+
all_projects: bool = typer.Option(
|
|
864
|
+
False,
|
|
865
|
+
"--all-projects",
|
|
866
|
+
help="Push all configured projects in parallel",
|
|
867
|
+
),
|
|
868
|
+
directory: Path = typer.Option(
|
|
869
|
+
Path("."),
|
|
870
|
+
"--directory",
|
|
871
|
+
"-d",
|
|
872
|
+
help="Project root directory (must contain .keboola/)",
|
|
873
|
+
),
|
|
874
|
+
dry_run: bool = typer.Option(
|
|
875
|
+
False,
|
|
876
|
+
"--dry-run",
|
|
877
|
+
help="Show what would be pushed without actually pushing",
|
|
878
|
+
),
|
|
879
|
+
force: bool = typer.Option(
|
|
880
|
+
False,
|
|
881
|
+
"--force",
|
|
882
|
+
help="Allow deletion of remote configs that were removed locally",
|
|
883
|
+
),
|
|
884
|
+
allow_plaintext: bool = typer.Option(
|
|
885
|
+
False,
|
|
886
|
+
"--allow-plaintext-on-encrypt-failure",
|
|
887
|
+
help="Allow push even if secret encryption fails (DANGEROUS: secrets stored as plaintext)",
|
|
888
|
+
),
|
|
889
|
+
branch: int | None = typer.Option(
|
|
890
|
+
None,
|
|
891
|
+
"--branch",
|
|
892
|
+
help=(
|
|
893
|
+
"Dev branch ID. Overrides the manifest / 'branch use' active "
|
|
894
|
+
"branch for this single invocation. Requires exactly one --project. "
|
|
895
|
+
"When no '<branch_name>/' subtree exists on disk, the default tree "
|
|
896
|
+
"(main/) is promoted to this branch."
|
|
897
|
+
),
|
|
898
|
+
),
|
|
899
|
+
no_name_drift_warnings: bool = typer.Option(
|
|
900
|
+
False,
|
|
901
|
+
"--no-name-drift-warnings",
|
|
902
|
+
help=(
|
|
903
|
+
"Suppress the cosmetic name_drift_warnings array in the result "
|
|
904
|
+
"envelope (the underlying detection still runs)."
|
|
905
|
+
),
|
|
906
|
+
),
|
|
907
|
+
) -> None:
|
|
908
|
+
"""Push local configuration changes to a Keboola project.
|
|
909
|
+
|
|
910
|
+
Use --project for a single project or --all-projects for all configured
|
|
911
|
+
projects in parallel.
|
|
912
|
+
"""
|
|
913
|
+
formatter = get_formatter(ctx)
|
|
914
|
+
service = get_service(ctx, "sync_service")
|
|
915
|
+
|
|
916
|
+
if all_projects and project:
|
|
917
|
+
formatter.error(
|
|
918
|
+
message="Cannot use --project with --all-projects",
|
|
919
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
920
|
+
)
|
|
921
|
+
raise typer.Exit(code=2)
|
|
922
|
+
if not all_projects and not project:
|
|
923
|
+
formatter.error(
|
|
924
|
+
message="Specify --project ALIAS or --all-projects",
|
|
925
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
926
|
+
)
|
|
927
|
+
raise typer.Exit(code=2)
|
|
928
|
+
if branch is not None and all_projects:
|
|
929
|
+
formatter.error(
|
|
930
|
+
message="--branch requires --project (branch id is per-project)",
|
|
931
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
932
|
+
)
|
|
933
|
+
raise typer.Exit(code=2)
|
|
934
|
+
|
|
935
|
+
if all_projects:
|
|
936
|
+
base_dir = _safe_resolve_dir(directory)
|
|
937
|
+
try:
|
|
938
|
+
data = service.push_all(
|
|
939
|
+
base_dir,
|
|
940
|
+
dry_run=dry_run,
|
|
941
|
+
force=force,
|
|
942
|
+
allow_plaintext_fallback=allow_plaintext,
|
|
943
|
+
)
|
|
944
|
+
except ConfigError as exc:
|
|
945
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
946
|
+
raise typer.Exit(code=5) from None
|
|
947
|
+
|
|
948
|
+
if formatter.json_mode:
|
|
949
|
+
formatter.output(data)
|
|
950
|
+
else:
|
|
951
|
+
_format_all_results(formatter, data, _format_push_result, _push_one_liner)
|
|
952
|
+
return
|
|
953
|
+
|
|
954
|
+
project_root = _resolve_project_root(directory, project)
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
result = service.push(
|
|
958
|
+
alias=project,
|
|
959
|
+
project_root=project_root,
|
|
960
|
+
dry_run=dry_run,
|
|
961
|
+
force=force,
|
|
962
|
+
allow_plaintext_fallback=allow_plaintext,
|
|
963
|
+
branch_override=branch,
|
|
964
|
+
no_name_drift_warnings=no_name_drift_warnings,
|
|
965
|
+
)
|
|
966
|
+
except FileNotFoundError as exc:
|
|
967
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
968
|
+
raise typer.Exit(code=1) from None
|
|
969
|
+
except ConfigError as exc:
|
|
970
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
971
|
+
raise typer.Exit(code=5) from None
|
|
972
|
+
except KeboolaApiError as exc:
|
|
973
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
974
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
975
|
+
|
|
976
|
+
if formatter.json_mode:
|
|
977
|
+
formatter.output(result)
|
|
978
|
+
else:
|
|
979
|
+
status = result.get("status", "")
|
|
980
|
+
|
|
981
|
+
if status == "no_changes":
|
|
982
|
+
formatter.console.print("[green]No changes to push.[/green]")
|
|
983
|
+
skipped_reason = result.get("skipped_reason")
|
|
984
|
+
if skipped_reason:
|
|
985
|
+
formatter.console.print(f" [yellow]{skipped_reason}[/yellow]")
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
if status == "dry_run":
|
|
989
|
+
formatter.console.print("[yellow]Dry run -- no changes applied:[/yellow]")
|
|
990
|
+
for change in result.get("changes", []):
|
|
991
|
+
label = _change_label(change)
|
|
992
|
+
formatter.console.print(f" {change['change_type'].upper()} {label}")
|
|
993
|
+
summary = result["summary"]
|
|
994
|
+
formatter.console.print(
|
|
995
|
+
f"\nWould create {summary['added']}, update {summary['modified']}, "
|
|
996
|
+
f"delete {summary['deleted']}"
|
|
997
|
+
)
|
|
998
|
+
return
|
|
999
|
+
|
|
1000
|
+
formatter.success(
|
|
1001
|
+
f"Pushed: {result['created']} created, "
|
|
1002
|
+
f"{result['updated']} updated, "
|
|
1003
|
+
f"{result['deleted']} deleted"
|
|
1004
|
+
)
|
|
1005
|
+
for change in result.get("pushed_details", []):
|
|
1006
|
+
label = _change_label(change)
|
|
1007
|
+
action = change["change_type"].upper()
|
|
1008
|
+
formatter.console.print(f" {action} {label}")
|
|
1009
|
+
for err in result.get("errors", []):
|
|
1010
|
+
formatter.warning(
|
|
1011
|
+
f" Error: {err['change_type']} {err['component_id']}/{err['config_id']}: "
|
|
1012
|
+
f"{err['message']}"
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
@sync_app.command("clone")
|
|
1017
|
+
def sync_clone(
|
|
1018
|
+
ctx: typer.Context,
|
|
1019
|
+
source: Path = typer.Option(
|
|
1020
|
+
...,
|
|
1021
|
+
"--source",
|
|
1022
|
+
help="Reference synced project directory (must contain .keboola/manifest.json)",
|
|
1023
|
+
),
|
|
1024
|
+
target: str = typer.Option(
|
|
1025
|
+
...,
|
|
1026
|
+
"--target",
|
|
1027
|
+
help="Target project alias to clone INTO (a fresh/empty project on first clone)",
|
|
1028
|
+
),
|
|
1029
|
+
target_dir: Path = typer.Option(
|
|
1030
|
+
...,
|
|
1031
|
+
"--target-dir",
|
|
1032
|
+
help="Directory to materialise the clone into (must not exist on first clone)",
|
|
1033
|
+
),
|
|
1034
|
+
bucket_map: Path | None = typer.Option(
|
|
1035
|
+
None,
|
|
1036
|
+
"--bucket-map",
|
|
1037
|
+
help="JSON/YAML file mapping {old_bucket_id: new_bucket_id} for input/output rewrites",
|
|
1038
|
+
),
|
|
1039
|
+
variable_values: Path | None = typer.Option(
|
|
1040
|
+
None,
|
|
1041
|
+
"--variable-values",
|
|
1042
|
+
help="JSON/YAML file mapping {variable_name: value} to override keboola.variables rows",
|
|
1043
|
+
),
|
|
1044
|
+
instance_rename: Path | None = typer.Option(
|
|
1045
|
+
None,
|
|
1046
|
+
"--instance-rename",
|
|
1047
|
+
help="JSON/YAML file mapping {old_path_prefix: new_path_prefix} to rename config dirs",
|
|
1048
|
+
),
|
|
1049
|
+
dry_run: bool = typer.Option(
|
|
1050
|
+
False,
|
|
1051
|
+
"--dry-run",
|
|
1052
|
+
help="Apply overrides and show the would-be diff without pushing",
|
|
1053
|
+
),
|
|
1054
|
+
branch: int | None = typer.Option(
|
|
1055
|
+
None,
|
|
1056
|
+
"--branch",
|
|
1057
|
+
help="Target dev branch id (defaults to the target project's production branch)",
|
|
1058
|
+
),
|
|
1059
|
+
) -> None:
|
|
1060
|
+
"""Clone a reference project into a fresh target, parameterised by overrides.
|
|
1061
|
+
|
|
1062
|
+
Copies the reference tree, applies declarative overrides (bucket_map,
|
|
1063
|
+
variable_values, instance_rename), and pushes so every config is CREATEd
|
|
1064
|
+
fresh -- keboola.flow task configIds and transformation variable links are
|
|
1065
|
+
remapped reference->ULID automatically. Idempotent: re-running with an
|
|
1066
|
+
existing --target-dir just pushes and reports no_changes.
|
|
1067
|
+
"""
|
|
1068
|
+
formatter = get_formatter(ctx)
|
|
1069
|
+
service = get_service(ctx, "sync_service")
|
|
1070
|
+
|
|
1071
|
+
try:
|
|
1072
|
+
overrides: dict[str, Any] = {}
|
|
1073
|
+
if bucket_map is not None:
|
|
1074
|
+
overrides["bucket_map"] = _load_override_file(bucket_map)
|
|
1075
|
+
if variable_values is not None:
|
|
1076
|
+
overrides["variable_values"] = _load_override_file(variable_values)
|
|
1077
|
+
if instance_rename is not None:
|
|
1078
|
+
overrides["instance_rename"] = _load_override_file(instance_rename)
|
|
1079
|
+
|
|
1080
|
+
result = service.clone_project(
|
|
1081
|
+
source=source,
|
|
1082
|
+
target_alias=target,
|
|
1083
|
+
target_dir=target_dir,
|
|
1084
|
+
overrides=overrides,
|
|
1085
|
+
dry_run=dry_run,
|
|
1086
|
+
branch_override=branch,
|
|
1087
|
+
)
|
|
1088
|
+
except FileNotFoundError as exc:
|
|
1089
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
1090
|
+
raise typer.Exit(code=1) from None
|
|
1091
|
+
except FileExistsError as exc:
|
|
1092
|
+
formatter.error(message=str(exc), error_code=ErrorCode.CONFIG_ERROR)
|
|
1093
|
+
raise typer.Exit(code=5) from None
|
|
1094
|
+
except ConfigError as exc:
|
|
1095
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1096
|
+
raise typer.Exit(code=5) from None
|
|
1097
|
+
except KeboolaApiError as exc:
|
|
1098
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1099
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
1100
|
+
|
|
1101
|
+
if formatter.json_mode:
|
|
1102
|
+
formatter.output(result)
|
|
1103
|
+
else:
|
|
1104
|
+
_format_clone_result(formatter, result)
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def _format_clone_result(formatter: Any, result: dict[str, Any]) -> None:
|
|
1108
|
+
"""Human-mode rendering for ``sync clone``."""
|
|
1109
|
+
status = result.get("status", "")
|
|
1110
|
+
overrides = (
|
|
1111
|
+
f"buckets={result.get('bucket_rewrites', 0)}, "
|
|
1112
|
+
f"variables={result.get('variable_overrides', 0)}, "
|
|
1113
|
+
f"renamed={result.get('renamed_instances', 0)}"
|
|
1114
|
+
)
|
|
1115
|
+
if status == "dry_run":
|
|
1116
|
+
summary = result.get("summary", {})
|
|
1117
|
+
formatter.console.print("[yellow]Dry run -- nothing pushed.[/yellow]")
|
|
1118
|
+
formatter.console.print(f" Overrides applied: {overrides}")
|
|
1119
|
+
formatter.console.print(
|
|
1120
|
+
f" Would create {summary.get('added', 0)} config(s) in "
|
|
1121
|
+
f"[cyan]{result.get('target_alias')}[/cyan]."
|
|
1122
|
+
)
|
|
1123
|
+
return
|
|
1124
|
+
if status == "no_changes":
|
|
1125
|
+
formatter.console.print(
|
|
1126
|
+
f"[green]Already cloned[/green] -- no changes to push into "
|
|
1127
|
+
f"[cyan]{result.get('target_alias')}[/cyan]."
|
|
1128
|
+
)
|
|
1129
|
+
return
|
|
1130
|
+
formatter.success(
|
|
1131
|
+
f"Cloned into {result.get('target_alias')}: {result.get('created', 0)} created "
|
|
1132
|
+
f"({overrides}, flow_task_remaps={result.get('flow_task_remaps', 0)})"
|
|
1133
|
+
)
|
|
1134
|
+
for err in result.get("errors", []):
|
|
1135
|
+
formatter.warning(
|
|
1136
|
+
f" Error: {err.get('change_type')} "
|
|
1137
|
+
f"{err.get('component_id')}/{err.get('config_id')}: {err.get('message')}"
|
|
1138
|
+
)
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
@sync_app.command("branch-link")
|
|
1142
|
+
def sync_branch_link(
|
|
1143
|
+
ctx: typer.Context,
|
|
1144
|
+
project: str = typer.Option(..., "--project", help="Project alias"),
|
|
1145
|
+
directory: Path = typer.Option(Path("."), "--directory", "-d", help="Project root directory"),
|
|
1146
|
+
branch_id: int | None = typer.Option(
|
|
1147
|
+
None, "--branch-id", help="Link to existing Keboola branch by ID"
|
|
1148
|
+
),
|
|
1149
|
+
branch_name: str | None = typer.Option(
|
|
1150
|
+
None, "--branch-name", help="Create/find branch with this name"
|
|
1151
|
+
),
|
|
1152
|
+
) -> None:
|
|
1153
|
+
"""Link the current git branch to a Keboola development branch.
|
|
1154
|
+
|
|
1155
|
+
Creates a new Keboola dev branch if one doesn't exist with the same name
|
|
1156
|
+
as the current git branch.
|
|
1157
|
+
"""
|
|
1158
|
+
formatter = get_formatter(ctx)
|
|
1159
|
+
service = get_service(ctx, "sync_service")
|
|
1160
|
+
project_root = directory.resolve()
|
|
1161
|
+
|
|
1162
|
+
try:
|
|
1163
|
+
result = service.branch_link(
|
|
1164
|
+
alias=project,
|
|
1165
|
+
project_root=project_root,
|
|
1166
|
+
branch_id=branch_id,
|
|
1167
|
+
branch_name=branch_name,
|
|
1168
|
+
)
|
|
1169
|
+
except FileNotFoundError as exc:
|
|
1170
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
1171
|
+
raise typer.Exit(code=1) from None
|
|
1172
|
+
except ConfigError as exc:
|
|
1173
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1174
|
+
raise typer.Exit(code=5) from None
|
|
1175
|
+
except KeboolaApiError as exc:
|
|
1176
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1177
|
+
raise typer.Exit(code=map_error_to_exit_code(exc)) from None
|
|
1178
|
+
|
|
1179
|
+
if formatter.json_mode:
|
|
1180
|
+
formatter.output(result)
|
|
1181
|
+
else:
|
|
1182
|
+
status = result["status"]
|
|
1183
|
+
if status == "already_linked":
|
|
1184
|
+
formatter.console.print(
|
|
1185
|
+
f"Already linked: {result['git_branch']} -> "
|
|
1186
|
+
f"Keboola branch {result['keboola_branch_id']} ({result['keboola_branch_name']})"
|
|
1187
|
+
)
|
|
1188
|
+
else:
|
|
1189
|
+
formatter.success(
|
|
1190
|
+
f"Linked {result['git_branch']} -> "
|
|
1191
|
+
f"Keboola branch {result['keboola_branch_id']} ({result['keboola_branch_name']})"
|
|
1192
|
+
)
|
|
1193
|
+
|
|
1194
|
+
|
|
1195
|
+
@sync_app.command("branch-unlink")
|
|
1196
|
+
def sync_branch_unlink(
|
|
1197
|
+
ctx: typer.Context,
|
|
1198
|
+
directory: Path = typer.Option(Path("."), "--directory", "-d", help="Project root directory"),
|
|
1199
|
+
) -> None:
|
|
1200
|
+
"""Remove the branch mapping for the current git branch.
|
|
1201
|
+
|
|
1202
|
+
Does NOT delete the Keboola branch itself.
|
|
1203
|
+
"""
|
|
1204
|
+
formatter = get_formatter(ctx)
|
|
1205
|
+
service = get_service(ctx, "sync_service")
|
|
1206
|
+
project_root = directory.resolve()
|
|
1207
|
+
|
|
1208
|
+
try:
|
|
1209
|
+
result = service.branch_unlink(project_root=project_root)
|
|
1210
|
+
except FileNotFoundError as exc:
|
|
1211
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
1212
|
+
raise typer.Exit(code=1) from None
|
|
1213
|
+
except ConfigError as exc:
|
|
1214
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1215
|
+
raise typer.Exit(code=5) from None
|
|
1216
|
+
|
|
1217
|
+
if formatter.json_mode:
|
|
1218
|
+
formatter.output(result)
|
|
1219
|
+
else:
|
|
1220
|
+
if result["status"] == "not_linked":
|
|
1221
|
+
formatter.console.print(f"Branch '{result['git_branch']}' is not linked.")
|
|
1222
|
+
else:
|
|
1223
|
+
formatter.success(
|
|
1224
|
+
f"Unlinked {result['git_branch']} from Keboola branch {result['keboola_branch_id']}"
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
@sync_app.command("branch-status")
|
|
1229
|
+
def sync_branch_status(
|
|
1230
|
+
ctx: typer.Context,
|
|
1231
|
+
directory: Path = typer.Option(Path("."), "--directory", "-d", help="Project root directory"),
|
|
1232
|
+
) -> None:
|
|
1233
|
+
"""Show the branch mapping status for the current git branch."""
|
|
1234
|
+
formatter = get_formatter(ctx)
|
|
1235
|
+
service = get_service(ctx, "sync_service")
|
|
1236
|
+
project_root = directory.resolve()
|
|
1237
|
+
|
|
1238
|
+
try:
|
|
1239
|
+
result = service.branch_status(project_root=project_root)
|
|
1240
|
+
except FileNotFoundError as exc:
|
|
1241
|
+
formatter.error(message=str(exc), error_code=ErrorCode.NOT_INITIALIZED)
|
|
1242
|
+
raise typer.Exit(code=1) from None
|
|
1243
|
+
except ConfigError as exc:
|
|
1244
|
+
# Corrupted .keboola/branch-mapping.json -- surface as clean
|
|
1245
|
+
# exit-5 envelope so the user sees the descriptive message
|
|
1246
|
+
# ("Failed to parse ...: Invalid branch ID ...") instead of a
|
|
1247
|
+
# Python traceback. Mirrors the handler other sync commands
|
|
1248
|
+
# already have (issue #269 sec-20 follow-up).
|
|
1249
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1250
|
+
raise typer.Exit(code=5) from None
|
|
1251
|
+
|
|
1252
|
+
if formatter.json_mode:
|
|
1253
|
+
formatter.output(result)
|
|
1254
|
+
else:
|
|
1255
|
+
if not result.get("git_branching"):
|
|
1256
|
+
formatter.console.print("Git-branching mode is not enabled.")
|
|
1257
|
+
return
|
|
1258
|
+
|
|
1259
|
+
git_branch = result.get("git_branch", "unknown")
|
|
1260
|
+
if result.get("linked"):
|
|
1261
|
+
if result.get("is_production"):
|
|
1262
|
+
formatter.console.print(
|
|
1263
|
+
f"Branch: {git_branch}\nKeboola: production\nStatus: [green]Linked[/green]"
|
|
1264
|
+
)
|
|
1265
|
+
else:
|
|
1266
|
+
formatter.console.print(
|
|
1267
|
+
f"Branch: {git_branch}\n"
|
|
1268
|
+
f"Keboola: {result['keboola_branch_id']} ({result['keboola_branch_name']})\n"
|
|
1269
|
+
f"Status: [green]Linked[/green]"
|
|
1270
|
+
)
|
|
1271
|
+
else:
|
|
1272
|
+
formatter.console.print(
|
|
1273
|
+
f"Branch: {git_branch}\n"
|
|
1274
|
+
f"Keboola: (none)\n"
|
|
1275
|
+
f"Status: [red]Not linked[/red]\n\n"
|
|
1276
|
+
f"Run 'kbagent sync branch-link --project ALIAS' to link."
|
|
1277
|
+
)
|