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,566 @@
|
|
|
1
|
+
"""Diff engine for comparing local configs against remote API state.
|
|
2
|
+
|
|
3
|
+
Produces a changeset describing what needs to be created, updated,
|
|
4
|
+
or deleted when pushing local changes to Keboola.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..constants import DIFF_MAX_DEPTH, DIFF_MAX_LINES, ENCRYPTED_PLACEHOLDER
|
|
15
|
+
from .secrets import is_encrypted_value
|
|
16
|
+
|
|
17
|
+
# Keys that are internal bookkeeping and should be excluded from comparison.
|
|
18
|
+
# NOTE: _configuration_extra is NOT ignored -- it carries the actual config
|
|
19
|
+
# payload for components like keboola.flow (phases, tasks, conditions) that
|
|
20
|
+
# don't use the standard "parameters" key.
|
|
21
|
+
_IGNORED_KEYS: frozenset[str] = frozenset({"_keboola", "version"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigChange:
|
|
25
|
+
"""Represents a single configuration or configuration-row change.
|
|
26
|
+
|
|
27
|
+
``change_type`` values:
|
|
28
|
+
|
|
29
|
+
- ``"added"`` -- new local config/row not yet in remote
|
|
30
|
+
- ``"modified"`` -- local changed, remote unchanged (safe to push)
|
|
31
|
+
- ``"remote_modified"`` -- remote changed, local unchanged (run pull)
|
|
32
|
+
- ``"conflict"`` -- both sides changed since last pull
|
|
33
|
+
- ``"deleted"`` -- local file removed, wants to delete from remote
|
|
34
|
+
|
|
35
|
+
Row changes set ``is_row=True`` and carry ``parent_config_id`` so
|
|
36
|
+
``sync push`` can dispatch them to the row-specific client methods
|
|
37
|
+
(``create_config_row`` / ``update_config_row`` / ``delete_config_row``).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
change_type: str,
|
|
43
|
+
component_id: str,
|
|
44
|
+
config_id: str, # empty string for new configs/rows
|
|
45
|
+
config_name: str,
|
|
46
|
+
path: str,
|
|
47
|
+
local_data: dict[str, Any] | None = None,
|
|
48
|
+
remote_data: dict[str, Any] | None = None,
|
|
49
|
+
details: list[str] | None = None,
|
|
50
|
+
is_row: bool = False,
|
|
51
|
+
parent_config_id: str = "",
|
|
52
|
+
):
|
|
53
|
+
self.change_type = change_type
|
|
54
|
+
self.component_id = component_id
|
|
55
|
+
self.config_id = config_id
|
|
56
|
+
self.config_name = config_name
|
|
57
|
+
self.path = path
|
|
58
|
+
self.local_data = local_data
|
|
59
|
+
self.remote_data = remote_data
|
|
60
|
+
self.details = details or []
|
|
61
|
+
self.is_row = is_row
|
|
62
|
+
self.parent_config_id = parent_config_id
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> dict[str, Any]:
|
|
65
|
+
"""Serialize to dict for JSON output."""
|
|
66
|
+
data: dict[str, Any] = {
|
|
67
|
+
"change_type": self.change_type,
|
|
68
|
+
"component_id": self.component_id,
|
|
69
|
+
"config_id": self.config_id,
|
|
70
|
+
"config_name": self.config_name,
|
|
71
|
+
"path": self.path,
|
|
72
|
+
"details": self.details,
|
|
73
|
+
}
|
|
74
|
+
if self.is_row:
|
|
75
|
+
data["is_row"] = True
|
|
76
|
+
data["parent_config_id"] = self.parent_config_id
|
|
77
|
+
return data
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ---------------------------------------------------------------------------
|
|
81
|
+
# Normalization helpers
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def normalize_for_comparison(obj: Any) -> Any:
|
|
86
|
+
"""Normalize a config dict for comparison.
|
|
87
|
+
|
|
88
|
+
- Replace all encrypted values (KBC::*Secure::*) with a fixed placeholder
|
|
89
|
+
to avoid false diffs from encryption nonces.
|
|
90
|
+
- Sort dict keys for consistent hashing.
|
|
91
|
+
- Strip ``_keboola`` metadata block (internal, not part of config content).
|
|
92
|
+
- Strip ``version`` key (local format marker).
|
|
93
|
+
|
|
94
|
+
Note: ``_configuration_extra`` is **not** stripped -- it carries real
|
|
95
|
+
config payload (e.g. ``keboola.flow`` phases/tasks, and a transformation's
|
|
96
|
+
``variables_id`` / ``variables_values_id`` links). It is part of
|
|
97
|
+
:data:`config_hash`, so any code that mutates it (e.g. the fresh-CREATE
|
|
98
|
+
variable-link backfill in ``sync push``) must refresh the stored
|
|
99
|
+
``pull_config_hash`` afterwards or ``sync diff`` will report a conflict.
|
|
100
|
+
|
|
101
|
+
Returns a deep copy -- the original object is never mutated.
|
|
102
|
+
"""
|
|
103
|
+
return _normalize(copy.deepcopy(obj))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _normalize(obj: Any) -> Any:
|
|
107
|
+
"""Recursively normalize *obj* in place (operates on a deep copy)."""
|
|
108
|
+
if isinstance(obj, dict):
|
|
109
|
+
# Remove ignored keys
|
|
110
|
+
for key in _IGNORED_KEYS:
|
|
111
|
+
obj.pop(key, None)
|
|
112
|
+
|
|
113
|
+
# Recurse into remaining values, replacing encrypted strings
|
|
114
|
+
normalized: dict[str, Any] = {}
|
|
115
|
+
for key in sorted(obj.keys()):
|
|
116
|
+
normalized[key] = _normalize(obj[key])
|
|
117
|
+
return normalized
|
|
118
|
+
|
|
119
|
+
if isinstance(obj, list):
|
|
120
|
+
return [_normalize(item) for item in obj]
|
|
121
|
+
|
|
122
|
+
if is_encrypted_value(obj):
|
|
123
|
+
return ENCRYPTED_PLACEHOLDER
|
|
124
|
+
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ---------------------------------------------------------------------------
|
|
129
|
+
# Hashing
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def config_hash(config_data: dict[str, Any]) -> str:
|
|
134
|
+
"""Compute a normalized hash of a config for fast change detection."""
|
|
135
|
+
normalized = normalize_for_comparison(config_data)
|
|
136
|
+
return hashlib.sha256(json.dumps(normalized, sort_keys=True).encode()).hexdigest()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Deep diff
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def deep_diff(
|
|
145
|
+
local: dict[str, Any],
|
|
146
|
+
remote: dict[str, Any],
|
|
147
|
+
path: str = "",
|
|
148
|
+
) -> list[str]:
|
|
149
|
+
"""Compute human-readable diff descriptions between two config dicts.
|
|
150
|
+
|
|
151
|
+
Returns a list of strings like:
|
|
152
|
+
|
|
153
|
+
- ``"parameters.api_url changed: 'old' -> 'new'"``
|
|
154
|
+
- ``"output.tables[0].destination added"``
|
|
155
|
+
- ``"description removed"``
|
|
156
|
+
|
|
157
|
+
Skips encrypted values (shows ``'changed (encrypted)'`` instead of
|
|
158
|
+
actual values). Limits comparison depth to ``DIFF_MAX_DEPTH`` levels
|
|
159
|
+
and caps the output at ``DIFF_MAX_LINES`` entries.
|
|
160
|
+
"""
|
|
161
|
+
results: list[str] = []
|
|
162
|
+
_deep_diff_recurse(
|
|
163
|
+
normalize_for_comparison(local),
|
|
164
|
+
normalize_for_comparison(remote),
|
|
165
|
+
path=path,
|
|
166
|
+
depth=0,
|
|
167
|
+
results=results,
|
|
168
|
+
)
|
|
169
|
+
return results[:DIFF_MAX_LINES]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _deep_diff_recurse(
|
|
173
|
+
local_val: Any,
|
|
174
|
+
remote_val: Any,
|
|
175
|
+
*,
|
|
176
|
+
path: str,
|
|
177
|
+
depth: int,
|
|
178
|
+
results: list[str],
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Recursive worker for :func:`deep_diff`."""
|
|
181
|
+
# Early exit when we've already collected enough detail lines.
|
|
182
|
+
if len(results) >= DIFF_MAX_LINES:
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
if type(local_val) != type(remote_val): # noqa: E721
|
|
186
|
+
results.append(_format_changed(path, remote_val, local_val))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if isinstance(local_val, dict) and isinstance(remote_val, dict):
|
|
190
|
+
_diff_dicts(local_val, remote_val, path=path, depth=depth, results=results)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
if isinstance(local_val, list) and isinstance(remote_val, list):
|
|
194
|
+
_diff_lists(local_val, remote_val, path=path, depth=depth, results=results)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Scalar comparison
|
|
198
|
+
if local_val != remote_val:
|
|
199
|
+
results.append(_format_changed(path, remote_val, local_val))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _diff_dicts(
|
|
203
|
+
local_dict: dict[str, Any],
|
|
204
|
+
remote_dict: dict[str, Any],
|
|
205
|
+
*,
|
|
206
|
+
path: str,
|
|
207
|
+
depth: int,
|
|
208
|
+
results: list[str],
|
|
209
|
+
) -> None:
|
|
210
|
+
"""Compare two dicts key by key."""
|
|
211
|
+
all_keys = sorted(set(local_dict.keys()) | set(remote_dict.keys()))
|
|
212
|
+
|
|
213
|
+
for key in all_keys:
|
|
214
|
+
if len(results) >= DIFF_MAX_LINES:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
child_path = f"{path}.{key}" if path else key
|
|
218
|
+
in_local = key in local_dict
|
|
219
|
+
in_remote = key in remote_dict
|
|
220
|
+
|
|
221
|
+
if in_local and not in_remote:
|
|
222
|
+
results.append(f"{child_path} added")
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if not in_local and in_remote:
|
|
226
|
+
results.append(f"{child_path} removed")
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Both sides have the key -- recurse if within depth budget.
|
|
230
|
+
if depth < DIFF_MAX_DEPTH:
|
|
231
|
+
_deep_diff_recurse(
|
|
232
|
+
local_dict[key],
|
|
233
|
+
remote_dict[key],
|
|
234
|
+
path=child_path,
|
|
235
|
+
depth=depth + 1,
|
|
236
|
+
results=results,
|
|
237
|
+
)
|
|
238
|
+
else:
|
|
239
|
+
# Beyond depth limit, fall back to equality check.
|
|
240
|
+
if local_dict[key] != remote_dict[key]:
|
|
241
|
+
results.append(f"{child_path} changed")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _diff_lists(
|
|
245
|
+
local_list: list[Any],
|
|
246
|
+
remote_list: list[Any],
|
|
247
|
+
*,
|
|
248
|
+
path: str,
|
|
249
|
+
depth: int,
|
|
250
|
+
results: list[str],
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Compare two lists element by element."""
|
|
253
|
+
if len(local_list) != len(remote_list):
|
|
254
|
+
results.append(f"{path} list length changed: {len(remote_list)} -> {len(local_list)}")
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
for idx, (local_item, remote_item) in enumerate(zip(local_list, remote_list, strict=True)):
|
|
258
|
+
if len(results) >= DIFF_MAX_LINES:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
child_path = f"{path}[{idx}]"
|
|
262
|
+
|
|
263
|
+
if depth < DIFF_MAX_DEPTH:
|
|
264
|
+
_deep_diff_recurse(
|
|
265
|
+
local_item,
|
|
266
|
+
remote_item,
|
|
267
|
+
path=child_path,
|
|
268
|
+
depth=depth + 1,
|
|
269
|
+
results=results,
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
if local_item != remote_item:
|
|
273
|
+
results.append(f"{child_path} changed")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _format_changed(path: str, old_val: Any, new_val: Any) -> str:
|
|
277
|
+
"""Format a single scalar change, masking encrypted placeholders."""
|
|
278
|
+
label = path if path else "(root)"
|
|
279
|
+
|
|
280
|
+
if old_val == ENCRYPTED_PLACEHOLDER or new_val == ENCRYPTED_PLACEHOLDER:
|
|
281
|
+
return f"{label} changed (encrypted)"
|
|
282
|
+
|
|
283
|
+
return f"{label} changed: {_repr_short(old_val)} -> {_repr_short(new_val)}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _repr_short(value: Any, max_length: int = 60) -> str:
|
|
287
|
+
"""Short repr of a value, truncated if too long."""
|
|
288
|
+
text = repr(value)
|
|
289
|
+
if len(text) > max_length:
|
|
290
|
+
return text[: max_length - 3] + "..."
|
|
291
|
+
return text
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ---------------------------------------------------------------------------
|
|
295
|
+
# Changeset computation
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def compute_changeset(
|
|
300
|
+
local_configs: list[dict[str, Any]],
|
|
301
|
+
remote_configs: dict[str, dict[str, Any]],
|
|
302
|
+
tracked_keys: set[str] | None = None,
|
|
303
|
+
base_hashes: dict[str, str] | None = None,
|
|
304
|
+
local_override_hashes: dict[str, str] | None = None,
|
|
305
|
+
) -> list[ConfigChange]:
|
|
306
|
+
"""3-way diff: compare local vs base (pull_hash) vs remote.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
local_configs: List of dicts with keys:
|
|
310
|
+
``component_id``, ``config_id``, ``config_name``, ``path``, ``data``.
|
|
311
|
+
remote_configs: Dict keyed by ``"{component_id}/{config_id}"`` with
|
|
312
|
+
API config data (already converted to local format).
|
|
313
|
+
tracked_keys: Optional set of ``"{component_id}/{config_id}"`` keys
|
|
314
|
+
from the manifest. Only manifest-tracked remote configs can be
|
|
315
|
+
flagged as ``"deleted"``.
|
|
316
|
+
base_hashes: Optional dict of ``"{component_id}/{config_id}"`` ->
|
|
317
|
+
normalized config hash at last pull time. Enables 3-way diff:
|
|
318
|
+
|
|
319
|
+
- local changed, remote unchanged → ``"modified"`` (safe push)
|
|
320
|
+
- local unchanged, remote changed → ``"remote_modified"`` (pull)
|
|
321
|
+
- both changed → ``"conflict"``
|
|
322
|
+
|
|
323
|
+
When ``None``, falls back to 2-way diff (any difference →
|
|
324
|
+
``"modified"``).
|
|
325
|
+
local_override_hashes: Optional dict of ``"{component_id}/{config_id}"``
|
|
326
|
+
-> hash to use for local side instead of computing from data.
|
|
327
|
+
Used when local files haven't changed since pull to avoid lossy
|
|
328
|
+
code-merge roundtrip.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
List of :class:`ConfigChange` objects.
|
|
332
|
+
"""
|
|
333
|
+
changes: list[ConfigChange] = []
|
|
334
|
+
seen_remote_keys: set[str] = set()
|
|
335
|
+
|
|
336
|
+
for entry in local_configs:
|
|
337
|
+
component_id: str = entry["component_id"]
|
|
338
|
+
config_id: str = entry.get("config_id", "")
|
|
339
|
+
config_name: str = entry.get("config_name", "")
|
|
340
|
+
path: str = entry.get("path", "")
|
|
341
|
+
local_data: dict[str, Any] = entry.get("data", {})
|
|
342
|
+
|
|
343
|
+
remote_key = f"{component_id}/{config_id}" if config_id else ""
|
|
344
|
+
|
|
345
|
+
# New config (no id yet, or not in remote)
|
|
346
|
+
if not config_id or remote_key not in remote_configs:
|
|
347
|
+
changes.append(
|
|
348
|
+
ConfigChange(
|
|
349
|
+
change_type="added",
|
|
350
|
+
component_id=component_id,
|
|
351
|
+
config_id=config_id,
|
|
352
|
+
config_name=config_name,
|
|
353
|
+
path=path,
|
|
354
|
+
local_data=local_data,
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
if remote_key:
|
|
358
|
+
seen_remote_keys.add(remote_key)
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Existing config -- 3-way compare
|
|
362
|
+
seen_remote_keys.add(remote_key)
|
|
363
|
+
remote_data: dict[str, Any] = remote_configs[remote_key]
|
|
364
|
+
|
|
365
|
+
# Use override hash if available (avoids lossy code roundtrip)
|
|
366
|
+
override_h = (local_override_hashes or {}).get(remote_key)
|
|
367
|
+
local_h = override_h if override_h else config_hash(local_data)
|
|
368
|
+
remote_h = config_hash(remote_data)
|
|
369
|
+
|
|
370
|
+
if local_h == remote_h:
|
|
371
|
+
# Unchanged -- skip
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
# Determine change direction using base hash (pull-time snapshot)
|
|
375
|
+
base_h = (base_hashes or {}).get(remote_key)
|
|
376
|
+
if base_h is not None:
|
|
377
|
+
local_changed = local_h != base_h
|
|
378
|
+
remote_changed = remote_h != base_h
|
|
379
|
+
else:
|
|
380
|
+
# No base hash available -- fall back to 2-way
|
|
381
|
+
local_changed = True
|
|
382
|
+
remote_changed = False
|
|
383
|
+
|
|
384
|
+
if local_changed and remote_changed:
|
|
385
|
+
change_type = "conflict"
|
|
386
|
+
details = deep_diff(local_data, remote_data)
|
|
387
|
+
elif remote_changed and not local_changed:
|
|
388
|
+
change_type = "remote_modified"
|
|
389
|
+
details = deep_diff(remote_data, local_data)
|
|
390
|
+
else:
|
|
391
|
+
# local_changed (and not remote_changed), or fallback
|
|
392
|
+
change_type = "modified"
|
|
393
|
+
details = deep_diff(local_data, remote_data)
|
|
394
|
+
|
|
395
|
+
changes.append(
|
|
396
|
+
ConfigChange(
|
|
397
|
+
change_type=change_type,
|
|
398
|
+
component_id=component_id,
|
|
399
|
+
config_id=config_id,
|
|
400
|
+
config_name=config_name,
|
|
401
|
+
path=path,
|
|
402
|
+
local_data=local_data,
|
|
403
|
+
remote_data=remote_data,
|
|
404
|
+
details=details,
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
# Detect deleted configs: remote configs that were previously tracked
|
|
409
|
+
# in the manifest but whose local file was removed.
|
|
410
|
+
for remote_key, remote_data in remote_configs.items():
|
|
411
|
+
if remote_key in seen_remote_keys:
|
|
412
|
+
continue
|
|
413
|
+
|
|
414
|
+
# Only flag configs the manifest knows about.
|
|
415
|
+
# Unknown remote configs are new on the server side.
|
|
416
|
+
if tracked_keys is not None and remote_key not in tracked_keys:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
parts = remote_key.split("/", 1)
|
|
420
|
+
component_id = parts[0] if len(parts) > 0 else ""
|
|
421
|
+
config_id = parts[1] if len(parts) > 1 else ""
|
|
422
|
+
|
|
423
|
+
changes.append(
|
|
424
|
+
ConfigChange(
|
|
425
|
+
change_type="deleted",
|
|
426
|
+
component_id=component_id,
|
|
427
|
+
config_id=config_id,
|
|
428
|
+
config_name=remote_data.get("name", ""),
|
|
429
|
+
path="",
|
|
430
|
+
remote_data=remote_data,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
return changes
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def compute_row_changeset(
|
|
438
|
+
local_rows: list[dict[str, Any]],
|
|
439
|
+
remote_rows: dict[str, dict[str, Any]],
|
|
440
|
+
tracked_row_keys: set[str] | None = None,
|
|
441
|
+
base_hashes: dict[str, str] | None = None,
|
|
442
|
+
) -> list[ConfigChange]:
|
|
443
|
+
"""3-way diff for configuration rows, parallel to :func:`compute_changeset`.
|
|
444
|
+
|
|
445
|
+
Row keys are ``"{component_id}/{parent_config_id}/rows/{row_id}"`` so they
|
|
446
|
+
cannot collide with parent-config keys. Results carry ``is_row=True`` and
|
|
447
|
+
``parent_config_id`` so ``sync push`` can route them to
|
|
448
|
+
``client.create_config_row`` / ``update_config_row`` / ``delete_config_row``.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
local_rows: List of dicts with keys:
|
|
452
|
+
``component_id``, ``parent_config_id``, ``row_id``,
|
|
453
|
+
``row_name``, ``path``, ``data``.
|
|
454
|
+
remote_rows: Dict keyed by ``"{component_id}/{parent_config_id}/rows/{row_id}"``
|
|
455
|
+
with API row data (already converted to local format via
|
|
456
|
+
:func:`api_row_to_local`).
|
|
457
|
+
tracked_row_keys: Optional set of row keys from the manifest.
|
|
458
|
+
Only manifest-tracked remote rows can be flagged as ``"deleted"``.
|
|
459
|
+
base_hashes: Optional dict of row_key -> normalized config hash at last
|
|
460
|
+
pull time. Enables 3-way diff (same semantics as parent configs).
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
List of :class:`ConfigChange` objects, each with ``is_row=True``.
|
|
464
|
+
"""
|
|
465
|
+
changes: list[ConfigChange] = []
|
|
466
|
+
seen_remote_keys: set[str] = set()
|
|
467
|
+
|
|
468
|
+
for entry in local_rows:
|
|
469
|
+
component_id: str = entry["component_id"]
|
|
470
|
+
parent_config_id: str = entry.get("parent_config_id", "")
|
|
471
|
+
row_id: str = entry.get("row_id", "")
|
|
472
|
+
row_name: str = entry.get("row_name", "")
|
|
473
|
+
path: str = entry.get("path", "")
|
|
474
|
+
local_data: dict[str, Any] = entry.get("data", {})
|
|
475
|
+
|
|
476
|
+
remote_key = (
|
|
477
|
+
f"{component_id}/{parent_config_id}/rows/{row_id}"
|
|
478
|
+
if row_id and parent_config_id
|
|
479
|
+
else ""
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# New row (no id yet, or not in remote)
|
|
483
|
+
if not row_id or remote_key not in remote_rows:
|
|
484
|
+
changes.append(
|
|
485
|
+
ConfigChange(
|
|
486
|
+
change_type="added",
|
|
487
|
+
component_id=component_id,
|
|
488
|
+
config_id=row_id,
|
|
489
|
+
config_name=row_name,
|
|
490
|
+
path=path,
|
|
491
|
+
local_data=local_data,
|
|
492
|
+
is_row=True,
|
|
493
|
+
parent_config_id=parent_config_id,
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
if remote_key:
|
|
497
|
+
seen_remote_keys.add(remote_key)
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
seen_remote_keys.add(remote_key)
|
|
501
|
+
remote_data: dict[str, Any] = remote_rows[remote_key]
|
|
502
|
+
|
|
503
|
+
local_h = config_hash(local_data)
|
|
504
|
+
remote_h = config_hash(remote_data)
|
|
505
|
+
|
|
506
|
+
if local_h == remote_h:
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
base_h = (base_hashes or {}).get(remote_key)
|
|
510
|
+
if base_h is not None:
|
|
511
|
+
local_changed = local_h != base_h
|
|
512
|
+
remote_changed = remote_h != base_h
|
|
513
|
+
else:
|
|
514
|
+
local_changed = True
|
|
515
|
+
remote_changed = False
|
|
516
|
+
|
|
517
|
+
if local_changed and remote_changed:
|
|
518
|
+
change_type = "conflict"
|
|
519
|
+
details = deep_diff(local_data, remote_data)
|
|
520
|
+
elif remote_changed and not local_changed:
|
|
521
|
+
change_type = "remote_modified"
|
|
522
|
+
details = deep_diff(remote_data, local_data)
|
|
523
|
+
else:
|
|
524
|
+
change_type = "modified"
|
|
525
|
+
details = deep_diff(local_data, remote_data)
|
|
526
|
+
|
|
527
|
+
changes.append(
|
|
528
|
+
ConfigChange(
|
|
529
|
+
change_type=change_type,
|
|
530
|
+
component_id=component_id,
|
|
531
|
+
config_id=row_id,
|
|
532
|
+
config_name=row_name,
|
|
533
|
+
path=path,
|
|
534
|
+
local_data=local_data,
|
|
535
|
+
remote_data=remote_data,
|
|
536
|
+
details=details,
|
|
537
|
+
is_row=True,
|
|
538
|
+
parent_config_id=parent_config_id,
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
# Deleted rows: tracked in manifest but missing from local filesystem
|
|
543
|
+
for remote_key, remote_data in remote_rows.items():
|
|
544
|
+
if remote_key in seen_remote_keys:
|
|
545
|
+
continue
|
|
546
|
+
if tracked_row_keys is not None and remote_key not in tracked_row_keys:
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
# remote_key shape: "{component_id}/{parent_config_id}/rows/{row_id}"
|
|
550
|
+
comp_and_parent, _, row_id = remote_key.rpartition("/rows/")
|
|
551
|
+
component_id, _, parent_config_id = comp_and_parent.partition("/")
|
|
552
|
+
|
|
553
|
+
changes.append(
|
|
554
|
+
ConfigChange(
|
|
555
|
+
change_type="deleted",
|
|
556
|
+
component_id=component_id,
|
|
557
|
+
config_id=row_id,
|
|
558
|
+
config_name=remote_data.get("name", ""),
|
|
559
|
+
path="",
|
|
560
|
+
remote_data=remote_data,
|
|
561
|
+
is_row=True,
|
|
562
|
+
parent_config_id=parent_config_id,
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
return changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Git helper functions for sync operations.
|
|
2
|
+
|
|
3
|
+
All functions are safe to call on machines without git installed -- they
|
|
4
|
+
return ``None`` or sensible defaults when the ``git`` binary is missing
|
|
5
|
+
or the directory is not a repository.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_git_repo(path: Path) -> bool:
|
|
15
|
+
"""Return ``True`` if *path* is inside a git working tree."""
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
18
|
+
cwd=path,
|
|
19
|
+
capture_output=True,
|
|
20
|
+
text=True,
|
|
21
|
+
check=False,
|
|
22
|
+
)
|
|
23
|
+
return result.returncode == 0 and result.stdout.strip() == "true"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_current_branch(path: Path) -> str | None:
|
|
27
|
+
"""Return the current git branch name, or ``None`` on failure.
|
|
28
|
+
|
|
29
|
+
Uses ``git branch --show-current`` which works even in a freshly
|
|
30
|
+
initialized repo (no commits yet), unlike ``git rev-parse``.
|
|
31
|
+
"""
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
["git", "branch", "--show-current"],
|
|
34
|
+
cwd=path,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
check=False,
|
|
38
|
+
)
|
|
39
|
+
if result.returncode != 0:
|
|
40
|
+
return None
|
|
41
|
+
branch = result.stdout.strip()
|
|
42
|
+
return branch if branch else None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_default_branch(path: Path) -> str:
|
|
46
|
+
"""Detect the default branch name (``main`` or ``master``).
|
|
47
|
+
|
|
48
|
+
Strategy:
|
|
49
|
+
1. Check ``git config init.defaultBranch`` (user/repo setting).
|
|
50
|
+
2. Check if ``refs/remotes/origin/main`` exists.
|
|
51
|
+
3. Check if ``refs/remotes/origin/master`` exists.
|
|
52
|
+
4. Fall back to ``"main"``.
|
|
53
|
+
"""
|
|
54
|
+
# 1. git config
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
["git", "config", "init.defaultBranch"],
|
|
57
|
+
cwd=path,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
check=False,
|
|
61
|
+
)
|
|
62
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
63
|
+
return result.stdout.strip()
|
|
64
|
+
|
|
65
|
+
# 2. origin/main
|
|
66
|
+
result = subprocess.run(
|
|
67
|
+
["git", "rev-parse", "--verify", "refs/remotes/origin/main"],
|
|
68
|
+
cwd=path,
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
check=False,
|
|
72
|
+
)
|
|
73
|
+
if result.returncode == 0:
|
|
74
|
+
return "main"
|
|
75
|
+
|
|
76
|
+
# 3. origin/master
|
|
77
|
+
result = subprocess.run(
|
|
78
|
+
["git", "rev-parse", "--verify", "refs/remotes/origin/master"],
|
|
79
|
+
cwd=path,
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
check=False,
|
|
83
|
+
)
|
|
84
|
+
if result.returncode == 0:
|
|
85
|
+
return "master"
|
|
86
|
+
|
|
87
|
+
# 4. Use current branch name (works in fresh repos with no commits)
|
|
88
|
+
current = get_current_branch(path)
|
|
89
|
+
if current:
|
|
90
|
+
return current
|
|
91
|
+
|
|
92
|
+
# 5. Default
|
|
93
|
+
return "main"
|