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,376 @@
|
|
|
1
|
+
"""Pull-side storage-metadata, jobs, and sample writers (from sync_service.py).
|
|
2
|
+
|
|
3
|
+
Free functions that materialise a project's storage metadata (buckets/tables),
|
|
4
|
+
per-config job history, and table data samples to the filesystem during
|
|
5
|
+
``sync pull``. Only :func:`fetch_jobs_per_config` needs the ``SyncService`` (for
|
|
6
|
+
its ``_resolve_max_workers`` helper); the rest are pure. ``_ensure_path_within``
|
|
7
|
+
(the storage-write path-traversal guard) moved here with its only callers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import csv
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import logging
|
|
16
|
+
import threading
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import TYPE_CHECKING, Any
|
|
20
|
+
|
|
21
|
+
from ..constants import (
|
|
22
|
+
ENCRYPTED_COLUMN_MASK,
|
|
23
|
+
ENCRYPTED_COLUMN_PREFIX,
|
|
24
|
+
JOBS_FILENAME,
|
|
25
|
+
STORAGE_BUCKETS_FILENAME,
|
|
26
|
+
STORAGE_DIR_NAME,
|
|
27
|
+
STORAGE_SAMPLES_DIR_NAME,
|
|
28
|
+
)
|
|
29
|
+
from ..errors import ConfigError
|
|
30
|
+
from ..sync.manifest import ManifestConfiguration
|
|
31
|
+
from ..sync.naming import sanitize_path_segment
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .sync_service import SyncService
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
# Storage API sync-export column ceiling (avoids the API 400 on wide tables).
|
|
39
|
+
_MAX_SYNC_COLUMNS = 30
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ensure_path_within(base_dir: Path, target: Path, what: str) -> None:
|
|
43
|
+
"""Reject a write whose path escapes *base_dir* (defense-in-depth).
|
|
44
|
+
|
|
45
|
+
Mirrors ``_ensure_within_branch`` for non-config writes (storage metadata +
|
|
46
|
+
samples) whose path segments derive from API-controlled bucket ids / table
|
|
47
|
+
names (GHSA-833q-c5wv-26r7). Raises ConfigError on escape so a malformed or
|
|
48
|
+
compromised Storage response cannot write outside the sync workspace.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
base_resolved = base_dir.resolve()
|
|
52
|
+
target_resolved = target.resolve()
|
|
53
|
+
except OSError as exc:
|
|
54
|
+
raise ConfigError(f"Cannot resolve sync path: {exc}") from exc
|
|
55
|
+
if not target_resolved.is_relative_to(base_resolved):
|
|
56
|
+
raise ConfigError(
|
|
57
|
+
f"Storage path escapes sync workspace ({what}). Refusing to write "
|
|
58
|
+
f"outside '{base_resolved}'. This indicates a malformed or compromised "
|
|
59
|
+
f"API response or a path-sanitization regression."
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def write_storage_metadata(
|
|
64
|
+
project_root: Path,
|
|
65
|
+
buckets: list[dict[str, Any]],
|
|
66
|
+
tables: list[dict[str, Any]],
|
|
67
|
+
samples: dict[str, str],
|
|
68
|
+
) -> dict[str, int]:
|
|
69
|
+
"""Write storage bucket and table metadata to the filesystem.
|
|
70
|
+
|
|
71
|
+
Creates:
|
|
72
|
+
storage/buckets.json - list of all buckets
|
|
73
|
+
storage/tables/{bucket_id}/{table_name}.json - per-table metadata
|
|
74
|
+
storage/samples/{bucket}/{table}/sample.csv - data samples (if any)
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict with counts: buckets, tables, samples written.
|
|
78
|
+
"""
|
|
79
|
+
storage_dir = project_root / STORAGE_DIR_NAME
|
|
80
|
+
storage_dir.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
# Write buckets index. API may return null for tablesCount / dataSizeBytes
|
|
83
|
+
# on empty buckets; coerce to 0 (dict.get default only fires when key is
|
|
84
|
+
# missing, not when the value is explicitly null).
|
|
85
|
+
bucket_summaries = [
|
|
86
|
+
{
|
|
87
|
+
"id": b.get("id", ""),
|
|
88
|
+
"name": b.get("name", ""),
|
|
89
|
+
"stage": b.get("stage", ""),
|
|
90
|
+
"description": b.get("description", ""),
|
|
91
|
+
"tables_count": b.get("tablesCount") or 0,
|
|
92
|
+
"data_size_bytes": b.get("dataSizeBytes") or 0,
|
|
93
|
+
"metadata": b.get("metadata", []),
|
|
94
|
+
}
|
|
95
|
+
for b in buckets
|
|
96
|
+
]
|
|
97
|
+
buckets_file = storage_dir / STORAGE_BUCKETS_FILENAME
|
|
98
|
+
buckets_file.write_text(
|
|
99
|
+
json.dumps(bucket_summaries, indent=2, ensure_ascii=False),
|
|
100
|
+
encoding="utf-8",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Group tables by bucket
|
|
104
|
+
tables_by_bucket: dict[str, list[dict[str, Any]]] = {}
|
|
105
|
+
for t in tables:
|
|
106
|
+
bucket_id = (
|
|
107
|
+
t.get("bucket", {}).get("id", "")
|
|
108
|
+
if isinstance(t.get("bucket"), dict)
|
|
109
|
+
else t.get("bucketId", "")
|
|
110
|
+
)
|
|
111
|
+
if not bucket_id:
|
|
112
|
+
continue
|
|
113
|
+
tables_by_bucket.setdefault(bucket_id, []).append(t)
|
|
114
|
+
|
|
115
|
+
tables_written = 0
|
|
116
|
+
tables_dir = storage_dir / "tables"
|
|
117
|
+
for bucket_id, bucket_tables in tables_by_bucket.items():
|
|
118
|
+
# Sanitize bucket_id for filesystem. sanitize_path_segment first kills
|
|
119
|
+
# traversal (`/`, `..`, absolute paths); the trailing replace keeps the
|
|
120
|
+
# legacy `in.c-foo` -> `in-c-foo` directory naming for legitimate ids
|
|
121
|
+
# (GHSA-833q-c5wv-26r7).
|
|
122
|
+
safe_bucket = sanitize_path_segment(bucket_id).replace(".", "-")
|
|
123
|
+
bucket_dir = tables_dir / safe_bucket
|
|
124
|
+
_ensure_path_within(storage_dir, bucket_dir, f"bucket_id={bucket_id!r}")
|
|
125
|
+
bucket_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
for t in bucket_tables:
|
|
128
|
+
table_name = t.get("name", "unknown")
|
|
129
|
+
table_meta = {
|
|
130
|
+
"id": t.get("id", ""),
|
|
131
|
+
"name": table_name,
|
|
132
|
+
"primary_key": t.get("primaryKey", []),
|
|
133
|
+
"columns": t.get("columns", []),
|
|
134
|
+
# API may return null for rowsCount / dataSizeBytes on
|
|
135
|
+
# newly-created or empty tables; coerce to 0 explicitly
|
|
136
|
+
# (dict.get default only fires when the key is missing).
|
|
137
|
+
"rows_count": t.get("rowsCount") or 0,
|
|
138
|
+
"data_size_bytes": t.get("dataSizeBytes") or 0,
|
|
139
|
+
"last_import_date": t.get("lastImportDate", ""),
|
|
140
|
+
"last_change_date": t.get("lastChangeDate", ""),
|
|
141
|
+
"description": t.get("description", ""),
|
|
142
|
+
"metadata": t.get("metadata", []),
|
|
143
|
+
"column_metadata": t.get("columnMetadata", {}),
|
|
144
|
+
}
|
|
145
|
+
# The table name comes from the API; sanitize it for the filename
|
|
146
|
+
# and assert containment so a crafted name cannot escape the bucket
|
|
147
|
+
# dir (GHSA-833q-c5wv-26r7). The original name stays verbatim in the
|
|
148
|
+
# metadata body above.
|
|
149
|
+
safe_table = sanitize_path_segment(table_name)
|
|
150
|
+
table_file = bucket_dir / f"{safe_table}.json"
|
|
151
|
+
_ensure_path_within(storage_dir, table_file, f"table={table_name!r}")
|
|
152
|
+
table_file.write_text(
|
|
153
|
+
json.dumps(table_meta, indent=2, ensure_ascii=False),
|
|
154
|
+
encoding="utf-8",
|
|
155
|
+
)
|
|
156
|
+
tables_written += 1
|
|
157
|
+
|
|
158
|
+
# Write samples
|
|
159
|
+
samples_written = 0
|
|
160
|
+
if samples:
|
|
161
|
+
samples_dir = storage_dir / STORAGE_SAMPLES_DIR_NAME
|
|
162
|
+
for table_id, csv_data in samples.items():
|
|
163
|
+
# table_id format: "in.c-bucket.table" -> samples/in-c-bucket/table/
|
|
164
|
+
# Every segment derives from the API table_id; sanitize each and
|
|
165
|
+
# assert containment so a crafted id cannot escape (GHSA-833q).
|
|
166
|
+
parts = table_id.split(".", 2)
|
|
167
|
+
if len(parts) >= 3:
|
|
168
|
+
safe_bucket = sanitize_path_segment(f"{parts[0]}-{parts[1]}")
|
|
169
|
+
safe_table = sanitize_path_segment(parts[2])
|
|
170
|
+
else:
|
|
171
|
+
safe_bucket = sanitize_path_segment(table_id.replace(".", "-"))
|
|
172
|
+
safe_table = "data"
|
|
173
|
+
sample_dir = samples_dir / safe_bucket / safe_table
|
|
174
|
+
_ensure_path_within(storage_dir, sample_dir, f"table_id={table_id!r}")
|
|
175
|
+
sample_dir.mkdir(parents=True, exist_ok=True)
|
|
176
|
+
|
|
177
|
+
masked_csv = mask_encrypted_columns(csv_data)
|
|
178
|
+
(sample_dir / "sample.csv").write_text(masked_csv, encoding="utf-8")
|
|
179
|
+
samples_written += 1
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"buckets": len(buckets),
|
|
183
|
+
"tables": tables_written,
|
|
184
|
+
"samples": samples_written,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def fetch_jobs_per_config(
|
|
189
|
+
service: SyncService,
|
|
190
|
+
client: Any,
|
|
191
|
+
components: list[dict[str, Any]],
|
|
192
|
+
job_limit: int,
|
|
193
|
+
) -> list[dict[str, Any]]:
|
|
194
|
+
"""Fetch jobs per config via /search/jobs in parallel.
|
|
195
|
+
|
|
196
|
+
Used as fallback when the grouped-jobs API cannot return all configs in a
|
|
197
|
+
single call (jobsPerGroup * limit <= 500 constraint). Returns data in the
|
|
198
|
+
same format as ``list_jobs_grouped()`` so :func:`write_per_config_jobs`
|
|
199
|
+
works unchanged.
|
|
200
|
+
"""
|
|
201
|
+
config_pairs: list[tuple[str, str]] = []
|
|
202
|
+
for comp in components:
|
|
203
|
+
comp_id = comp.get("id", "")
|
|
204
|
+
for cfg in comp.get("configurations", []):
|
|
205
|
+
cfg_id = str(cfg.get("id", ""))
|
|
206
|
+
if comp_id and cfg_id:
|
|
207
|
+
config_pairs.append((comp_id, cfg_id))
|
|
208
|
+
|
|
209
|
+
if not config_pairs:
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
results: list[dict[str, Any]] = []
|
|
213
|
+
lock = threading.Lock()
|
|
214
|
+
max_workers = min(len(config_pairs), service._resolve_max_workers())
|
|
215
|
+
|
|
216
|
+
def _fetch_one(pair: tuple[str, str]) -> None:
|
|
217
|
+
comp_id, cfg_id = pair
|
|
218
|
+
try:
|
|
219
|
+
jobs = client.list_jobs(
|
|
220
|
+
component_id=comp_id,
|
|
221
|
+
config_id=cfg_id,
|
|
222
|
+
limit=job_limit,
|
|
223
|
+
)
|
|
224
|
+
if jobs:
|
|
225
|
+
with lock:
|
|
226
|
+
results.append(
|
|
227
|
+
{
|
|
228
|
+
"group": {"componentId": comp_id, "configId": cfg_id},
|
|
229
|
+
"jobs": jobs,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
except Exception:
|
|
233
|
+
logger.debug("Failed to fetch jobs for %s/%s", comp_id, cfg_id, exc_info=True)
|
|
234
|
+
|
|
235
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
236
|
+
futures = [executor.submit(_fetch_one, pair) for pair in config_pairs]
|
|
237
|
+
for future in as_completed(futures):
|
|
238
|
+
future.result() # propagate unexpected errors
|
|
239
|
+
|
|
240
|
+
return results
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def write_per_config_jobs(
|
|
244
|
+
branch_dir: Path,
|
|
245
|
+
configurations: list[ManifestConfiguration],
|
|
246
|
+
jobs_grouped: list[dict[str, Any]],
|
|
247
|
+
) -> int:
|
|
248
|
+
"""Write ``_jobs.jsonl`` files next to each configuration.
|
|
249
|
+
|
|
250
|
+
Matches grouped jobs to configs by componentId+configId, then writes a JSONL
|
|
251
|
+
file with light job records. Returns the number of files written.
|
|
252
|
+
"""
|
|
253
|
+
jobs_by_config: dict[tuple[str, str], list[dict[str, Any]]] = {}
|
|
254
|
+
for group in jobs_grouped:
|
|
255
|
+
group_key = group.get("group", {})
|
|
256
|
+
component_id = group_key.get("componentId", "")
|
|
257
|
+
config_id = group_key.get("configId", "")
|
|
258
|
+
if component_id and config_id:
|
|
259
|
+
jobs_by_config[(component_id, config_id)] = group.get("jobs", [])
|
|
260
|
+
|
|
261
|
+
files_written = 0
|
|
262
|
+
for cfg in configurations:
|
|
263
|
+
key = (cfg.component_id, cfg.id)
|
|
264
|
+
jobs = jobs_by_config.get(key)
|
|
265
|
+
if not jobs:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
config_dir = branch_dir / cfg.path
|
|
269
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
270
|
+
jobs_file = config_dir / JOBS_FILENAME
|
|
271
|
+
|
|
272
|
+
lines: list[str] = []
|
|
273
|
+
for job in jobs:
|
|
274
|
+
light_job: dict[str, Any] = {
|
|
275
|
+
"id": str(job.get("id", "")),
|
|
276
|
+
"status": job.get("status", ""),
|
|
277
|
+
"start_time": job.get("startTime", ""),
|
|
278
|
+
"end_time": job.get("endTime", ""),
|
|
279
|
+
"duration_seconds": job.get("durationSeconds", 0),
|
|
280
|
+
}
|
|
281
|
+
if job.get("mode") and job["mode"] != "run":
|
|
282
|
+
light_job["mode"] = job["mode"]
|
|
283
|
+
# Include error message for failed/warning jobs
|
|
284
|
+
status = job.get("status", "")
|
|
285
|
+
if status in ("error", "warning", "terminated", "cancelled"):
|
|
286
|
+
result = job.get("result", {})
|
|
287
|
+
if isinstance(result, dict) and result.get("message"):
|
|
288
|
+
light_job["error_message"] = result["message"]
|
|
289
|
+
lines.append(json.dumps(light_job, ensure_ascii=False))
|
|
290
|
+
|
|
291
|
+
jobs_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
292
|
+
files_written += 1
|
|
293
|
+
|
|
294
|
+
return files_written
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def fetch_samples(
|
|
298
|
+
client: Any,
|
|
299
|
+
tables: list[dict[str, Any]],
|
|
300
|
+
sample_limit: int,
|
|
301
|
+
max_samples: int,
|
|
302
|
+
) -> dict[str, str]:
|
|
303
|
+
"""Fetch CSV data previews for tables, respecting limits.
|
|
304
|
+
|
|
305
|
+
Selects tables sorted by rowsCount descending (largest first), limited to
|
|
306
|
+
``max_samples`` tables. Returns a dict mapping table_id -> CSV string.
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
# Storage API may return rowsCount=None for empty/newly-created tables on
|
|
310
|
+
# some backends; dict.get() does not coerce None to the default value.
|
|
311
|
+
def _rows(t: dict[str, Any]) -> int:
|
|
312
|
+
return t.get("rowsCount") or 0
|
|
313
|
+
|
|
314
|
+
sorted_tables = sorted(
|
|
315
|
+
[t for t in tables if _rows(t) > 0],
|
|
316
|
+
key=_rows,
|
|
317
|
+
reverse=True,
|
|
318
|
+
)[:max_samples]
|
|
319
|
+
|
|
320
|
+
samples: dict[str, str] = {}
|
|
321
|
+
for t in sorted_tables:
|
|
322
|
+
table_id = t.get("id", "")
|
|
323
|
+
if not table_id:
|
|
324
|
+
continue
|
|
325
|
+
try:
|
|
326
|
+
# Limit columns to avoid the API 400 error on wide tables.
|
|
327
|
+
all_columns = t.get("columns", [])
|
|
328
|
+
columns = (
|
|
329
|
+
all_columns[:_MAX_SYNC_COLUMNS] if len(all_columns) > _MAX_SYNC_COLUMNS else None
|
|
330
|
+
)
|
|
331
|
+
csv_data = client.get_table_data_preview(table_id, limit=sample_limit, columns=columns)
|
|
332
|
+
samples[table_id] = csv_data
|
|
333
|
+
except Exception:
|
|
334
|
+
logger.warning("Failed to fetch sample for %s", table_id, exc_info=True)
|
|
335
|
+
|
|
336
|
+
return samples
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def mask_encrypted_columns(csv_data: str) -> str:
|
|
340
|
+
"""Mask encrypted column values in CSV data.
|
|
341
|
+
|
|
342
|
+
Encrypted columns in Keboola start with '#' in the column name; their values
|
|
343
|
+
are replaced with the masked placeholder.
|
|
344
|
+
"""
|
|
345
|
+
if not csv_data:
|
|
346
|
+
return csv_data
|
|
347
|
+
|
|
348
|
+
lines = csv_data.split("\n")
|
|
349
|
+
if not lines:
|
|
350
|
+
return csv_data
|
|
351
|
+
|
|
352
|
+
reader = csv.reader(io.StringIO(lines[0]))
|
|
353
|
+
try:
|
|
354
|
+
header = next(reader)
|
|
355
|
+
except StopIteration:
|
|
356
|
+
return csv_data
|
|
357
|
+
|
|
358
|
+
encrypted_indices = [
|
|
359
|
+
i for i, col in enumerate(header) if col.startswith(ENCRYPTED_COLUMN_PREFIX)
|
|
360
|
+
]
|
|
361
|
+
if not encrypted_indices:
|
|
362
|
+
return csv_data
|
|
363
|
+
|
|
364
|
+
output = io.StringIO()
|
|
365
|
+
writer = csv.writer(output)
|
|
366
|
+
full_reader = csv.reader(io.StringIO(csv_data))
|
|
367
|
+
for row_idx, row in enumerate(full_reader):
|
|
368
|
+
if row_idx == 0:
|
|
369
|
+
writer.writerow(row) # header unchanged
|
|
370
|
+
else:
|
|
371
|
+
for idx in encrypted_indices:
|
|
372
|
+
if idx < len(row):
|
|
373
|
+
row[idx] = ENCRYPTED_COLUMN_MASK
|
|
374
|
+
writer.writerow(row)
|
|
375
|
+
|
|
376
|
+
return output.getvalue()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Manifest + local-file writeback after a push (extracted from sync_service.py).
|
|
2
|
+
|
|
3
|
+
Free functions that record a freshly-created config/row in the manifest, write
|
|
4
|
+
the API-assigned ULID + encrypted secrets back into the local ``_config.yml``,
|
|
5
|
+
and propagate ``KBC.*`` metadata. Only :func:`writeback_after_push` needs the
|
|
6
|
+
``SyncService`` (for its ``_write_config_file`` helper); the rest are pure.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from ..errors import KeboolaApiError
|
|
16
|
+
from ..sync.manifest import ManifestConfigRow, ManifestConfiguration
|
|
17
|
+
from ._encryption import apply_encrypted_to_local
|
|
18
|
+
from ._sync_models import WritebackResult
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..sync.manifest import Manifest
|
|
22
|
+
from .sync_service import SyncService
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def writeback_create_config_in_manifest(
|
|
28
|
+
*,
|
|
29
|
+
manifest: Manifest,
|
|
30
|
+
component_id: str,
|
|
31
|
+
branch_id: int | None,
|
|
32
|
+
config_path_str: str,
|
|
33
|
+
new_id: str,
|
|
34
|
+
file_hash: str,
|
|
35
|
+
cfg_hash: str,
|
|
36
|
+
) -> WritebackResult:
|
|
37
|
+
"""Record a freshly-created config in the manifest.
|
|
38
|
+
|
|
39
|
+
If a placeholder entry already exists at ``(branch_id, component_id, path)``
|
|
40
|
+
-- the FIIA / scaffold emit pattern -- update it in place, preserving any
|
|
41
|
+
user-declared metadata (e.g. ``KBC.configuration.folderName``) and
|
|
42
|
+
refreshing only the bookkeeping hashes. Otherwise append a new entry.
|
|
43
|
+
|
|
44
|
+
Matching includes ``branch_id`` because a single manifest can hold entries
|
|
45
|
+
from multiple branches in git-branching mode; matching on
|
|
46
|
+
``(component_id, path)`` alone would risk updating the wrong branch's entry
|
|
47
|
+
when the same logical path exists under two branches.
|
|
48
|
+
|
|
49
|
+
Returns a :class:`WritebackResult` carrying the entry and its pre-overwrite
|
|
50
|
+
``previous_id`` so the create pass can remap any child row parents /
|
|
51
|
+
transformation variable links from the placeholder id to the freshly-assigned
|
|
52
|
+
ULID.
|
|
53
|
+
"""
|
|
54
|
+
target_branch = branch_id or 0
|
|
55
|
+
for entry in manifest.configurations:
|
|
56
|
+
if (
|
|
57
|
+
entry.branch_id == target_branch
|
|
58
|
+
and entry.component_id == component_id
|
|
59
|
+
and entry.path == config_path_str
|
|
60
|
+
):
|
|
61
|
+
previous_id = entry.id
|
|
62
|
+
entry.id = new_id
|
|
63
|
+
entry.metadata["pull_hash"] = file_hash
|
|
64
|
+
entry.metadata["pull_config_hash"] = cfg_hash
|
|
65
|
+
return WritebackResult(entry=entry, previous_id=previous_id)
|
|
66
|
+
new_entry = ManifestConfiguration(
|
|
67
|
+
branchId=target_branch,
|
|
68
|
+
componentId=component_id,
|
|
69
|
+
id=new_id,
|
|
70
|
+
path=config_path_str,
|
|
71
|
+
metadata={"pull_hash": file_hash, "pull_config_hash": cfg_hash},
|
|
72
|
+
)
|
|
73
|
+
manifest.configurations.append(new_entry)
|
|
74
|
+
return WritebackResult(entry=new_entry, previous_id="")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def writeback_create_row_in_manifest(
|
|
78
|
+
*,
|
|
79
|
+
parent: ManifestConfiguration,
|
|
80
|
+
row_path_str: str,
|
|
81
|
+
new_row_id: str,
|
|
82
|
+
file_hash: str,
|
|
83
|
+
cfg_hash: str,
|
|
84
|
+
) -> ManifestConfigRow:
|
|
85
|
+
"""Record a freshly-created row under its parent in the manifest.
|
|
86
|
+
|
|
87
|
+
Mirrors :func:`writeback_create_config_in_manifest` for rows: update any
|
|
88
|
+
placeholder row entry in place, otherwise append.
|
|
89
|
+
"""
|
|
90
|
+
for row in parent.rows:
|
|
91
|
+
if row.path == row_path_str:
|
|
92
|
+
row.id = new_row_id
|
|
93
|
+
row.metadata["pull_hash"] = file_hash
|
|
94
|
+
row.metadata["pull_config_hash"] = cfg_hash
|
|
95
|
+
return row
|
|
96
|
+
new_row = ManifestConfigRow(
|
|
97
|
+
id=new_row_id,
|
|
98
|
+
path=row_path_str,
|
|
99
|
+
metadata={"pull_hash": file_hash, "pull_config_hash": cfg_hash},
|
|
100
|
+
)
|
|
101
|
+
parent.rows.append(new_row)
|
|
102
|
+
return new_row
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def propagate_kbc_metadata(
|
|
106
|
+
client: Any,
|
|
107
|
+
entry: ManifestConfiguration,
|
|
108
|
+
branch_id: int | None,
|
|
109
|
+
) -> str | None:
|
|
110
|
+
"""POST any ``KBC.*`` keys from the manifest entry to the metadata API.
|
|
111
|
+
|
|
112
|
+
Bookkeeping keys (``pull_hash``, ``pull_config_hash``, ...) live in the same
|
|
113
|
+
metadata dict but are filtered by the ``KBC.`` prefix. Called only on CREATE;
|
|
114
|
+
updates use ``kbagent config set-metadata`` explicitly. The metadata API
|
|
115
|
+
stores configuration-level annotations only -- this is **not** a secret
|
|
116
|
+
store; do not place tokens or passwords under ``KBC.*`` keys.
|
|
117
|
+
|
|
118
|
+
Returns ``None`` on success (or when there are no KBC.* keys to propagate).
|
|
119
|
+
Returns the API error message on a non-fatal write failure: the config is
|
|
120
|
+
already created on the remote and the manifest writeback is complete, so a
|
|
121
|
+
single failed metadata POST is reported back to the push loop as an
|
|
122
|
+
accumulated error rather than aborting the rest of the push.
|
|
123
|
+
"""
|
|
124
|
+
entries = [(key, str(value)) for key, value in entry.metadata.items() if key.startswith("KBC.")]
|
|
125
|
+
if not entries:
|
|
126
|
+
return None
|
|
127
|
+
try:
|
|
128
|
+
client.set_config_metadata(
|
|
129
|
+
component_id=entry.component_id,
|
|
130
|
+
config_id=entry.id,
|
|
131
|
+
entries=entries,
|
|
132
|
+
branch_id=branch_id,
|
|
133
|
+
)
|
|
134
|
+
except KeboolaApiError as exc:
|
|
135
|
+
logger.warning(
|
|
136
|
+
"Failed to propagate KBC.* metadata for %s/%s: %s",
|
|
137
|
+
entry.component_id,
|
|
138
|
+
entry.id,
|
|
139
|
+
exc,
|
|
140
|
+
)
|
|
141
|
+
return exc.message
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def writeback_after_push(
|
|
146
|
+
service: SyncService,
|
|
147
|
+
local_data: dict[str, Any],
|
|
148
|
+
config_dir: Path,
|
|
149
|
+
config_id: str,
|
|
150
|
+
pushed_configuration: dict[str, Any],
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Update local ``_config.yml`` after a successful push.
|
|
153
|
+
|
|
154
|
+
Writes back the API-assigned ``_keboola.config_id`` (on first create) and
|
|
155
|
+
the encrypted secret values (so local matches remote state).
|
|
156
|
+
"""
|
|
157
|
+
keboola_meta = local_data.setdefault("_keboola", {})
|
|
158
|
+
if config_id:
|
|
159
|
+
keboola_meta["config_id"] = config_id
|
|
160
|
+
|
|
161
|
+
pushed_params = pushed_configuration.get("parameters", {})
|
|
162
|
+
local_params = local_data.get("parameters", {})
|
|
163
|
+
if pushed_params and local_params:
|
|
164
|
+
apply_encrypted_to_local(local_params, pushed_params)
|
|
165
|
+
|
|
166
|
+
service._write_config_file(config_dir, local_data)
|
|
167
|
+
logger.debug("Updated local config at %s after push", config_dir)
|