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,248 @@
|
|
|
1
|
+
"""Error types and helpers for Keboola Agent CLI."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ErrorCode(StrEnum):
|
|
7
|
+
"""Stable machine-readable error codes emitted by kbagent.
|
|
8
|
+
|
|
9
|
+
``str`` mixin means values compare equal to their plain-string equivalents
|
|
10
|
+
and serialise as plain strings in JSON output -- no wire-format change.
|
|
11
|
+
|
|
12
|
+
Versioning: adding a new code = minor bump; renaming / removing = major bump.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# Auth / access
|
|
16
|
+
INVALID_TOKEN = "INVALID_TOKEN"
|
|
17
|
+
ACCESS_DENIED = "ACCESS_DENIED"
|
|
18
|
+
PERMISSION_DENIED = "PERMISSION_DENIED"
|
|
19
|
+
MISSING_MASTER_TOKEN = "MISSING_MASTER_TOKEN"
|
|
20
|
+
UNAUTHORIZED = "UNAUTHORIZED" # Bearer-auth rejection by `kbagent serve` (0.40.0+)
|
|
21
|
+
|
|
22
|
+
# Network / transport
|
|
23
|
+
TIMEOUT = "TIMEOUT"
|
|
24
|
+
CONNECTION_ERROR = "CONNECTION_ERROR"
|
|
25
|
+
RETRY_EXHAUSTED = "RETRY_EXHAUSTED"
|
|
26
|
+
|
|
27
|
+
# API / generic
|
|
28
|
+
API_ERROR = "API_ERROR"
|
|
29
|
+
NOT_FOUND = "NOT_FOUND"
|
|
30
|
+
ALREADY_EXISTS = "ALREADY_EXISTS"
|
|
31
|
+
VALIDATION_ERROR = "VALIDATION_ERROR"
|
|
32
|
+
INVALID_ARGUMENT = "INVALID_ARGUMENT"
|
|
33
|
+
INVALID_FORMAT = "INVALID_FORMAT"
|
|
34
|
+
USAGE_ERROR = "USAGE_ERROR"
|
|
35
|
+
MISSING_PARAMETER = "MISSING_PARAMETER"
|
|
36
|
+
UNKNOWN_ERROR = "UNKNOWN_ERROR"
|
|
37
|
+
# `kbagent serve` HTTP envelope (0.40.0+)
|
|
38
|
+
HTTP_ERROR = "HTTP_ERROR" # Generic HTTP-layer error (Starlette HTTPException passthrough)
|
|
39
|
+
INTERNAL_ERROR = "INTERNAL_ERROR" # Uncaught exception inside a route handler
|
|
40
|
+
|
|
41
|
+
# Configuration
|
|
42
|
+
CONFIG_ERROR = "CONFIG_ERROR"
|
|
43
|
+
NOT_INITIALIZED = "NOT_INITIALIZED"
|
|
44
|
+
INIT_ERROR = "INIT_ERROR"
|
|
45
|
+
|
|
46
|
+
# Jobs
|
|
47
|
+
QUEUE_JOB_FAILED = "QUEUE_JOB_FAILED"
|
|
48
|
+
QUEUE_JOB_TIMEOUT = "QUEUE_JOB_TIMEOUT"
|
|
49
|
+
STORAGE_JOB_FAILED = "STORAGE_JOB_FAILED"
|
|
50
|
+
STORAGE_JOB_TIMEOUT = "STORAGE_JOB_TIMEOUT"
|
|
51
|
+
QUERY_JOB_FAILED = "QUERY_JOB_FAILED"
|
|
52
|
+
QUERY_JOB_TIMEOUT = "QUERY_JOB_TIMEOUT"
|
|
53
|
+
|
|
54
|
+
# Variables
|
|
55
|
+
NO_VARIABLE_ROWS = "NO_VARIABLE_ROWS"
|
|
56
|
+
MALFORMED_VARIABLES_ROW = "MALFORMED_VARIABLES_ROW"
|
|
57
|
+
|
|
58
|
+
# Storage
|
|
59
|
+
UPLOAD_FAILED = "UPLOAD_FAILED"
|
|
60
|
+
EXPORT_EMPTY_MANIFEST = "EXPORT_EMPTY_MANIFEST"
|
|
61
|
+
EXPORT_NO_FILE = "EXPORT_NO_FILE"
|
|
62
|
+
EXPORT_NO_URL = "EXPORT_NO_URL"
|
|
63
|
+
NOT_SLICED = "NOT_SLICED"
|
|
64
|
+
FILE_NO_URL = "FILE_NO_URL"
|
|
65
|
+
|
|
66
|
+
# I/O
|
|
67
|
+
FILE_NOT_FOUND = "FILE_NOT_FOUND"
|
|
68
|
+
DIR_NOT_FOUND = "DIR_NOT_FOUND"
|
|
69
|
+
READ_ERROR = "READ_ERROR"
|
|
70
|
+
WRITE_ERROR = "WRITE_ERROR"
|
|
71
|
+
INPUT_ERROR = "INPUT_ERROR"
|
|
72
|
+
|
|
73
|
+
# Lineage
|
|
74
|
+
NODE_NOT_FOUND = "NODE_NOT_FOUND"
|
|
75
|
+
|
|
76
|
+
# Sharing
|
|
77
|
+
INVALID_SHARING_TYPE = "INVALID_SHARING_TYPE"
|
|
78
|
+
NOT_LINKED_BUCKET = "NOT_LINKED_BUCKET"
|
|
79
|
+
|
|
80
|
+
# KAI (AI Service)
|
|
81
|
+
KAI_ERROR = "KAI_ERROR"
|
|
82
|
+
KAI_NOT_ENABLED = "KAI_NOT_ENABLED"
|
|
83
|
+
|
|
84
|
+
# Workspace / Query
|
|
85
|
+
MISSING_QUERY = "MISSING_QUERY"
|
|
86
|
+
WORKSPACE_NOT_FOUND = "WORKSPACE_NOT_FOUND"
|
|
87
|
+
|
|
88
|
+
# Sync
|
|
89
|
+
PARENT_CONFIG_NOT_TRACKED = "PARENT_CONFIG_NOT_TRACKED"
|
|
90
|
+
VARIABLE_LINK_UNRESOLVED = "VARIABLE_LINK_UNRESOLVED"
|
|
91
|
+
SYNC_CONFLICT = "SYNC_CONFLICT"
|
|
92
|
+
|
|
93
|
+
# Encryption
|
|
94
|
+
ENCRYPTION_FAILED = "ENCRYPTION_FAILED"
|
|
95
|
+
|
|
96
|
+
# Job / queue (extensions from 0.22.0)
|
|
97
|
+
JOB_TIMEOUT_TERMINATED = "JOB_TIMEOUT_TERMINATED"
|
|
98
|
+
|
|
99
|
+
# Flow (new in 0.22.0)
|
|
100
|
+
SCHEDULE_DELETE_FAILED = "SCHEDULE_DELETE_FAILED"
|
|
101
|
+
# Conditional-flow validation (replaces INVALID_FLOW_DAG; since 0.57.0)
|
|
102
|
+
INVALID_FLOW_DEFINITION = "INVALID_FLOW_DEFINITION"
|
|
103
|
+
|
|
104
|
+
# Data apps (new in 0.27.0)
|
|
105
|
+
DATA_APP_BUILD_FAILED = "DATA_APP_BUILD_FAILED"
|
|
106
|
+
DATA_APP_DEPLOY_TIMEOUT = "DATA_APP_DEPLOY_TIMEOUT"
|
|
107
|
+
DATA_APP_INVALID_GIT = "DATA_APP_INVALID_GIT"
|
|
108
|
+
|
|
109
|
+
# Data apps - secrets + validate-repo (new in 0.28.0)
|
|
110
|
+
DATA_APP_INVALID_SECRET = "DATA_APP_INVALID_SECRET"
|
|
111
|
+
DATA_APP_INVALID_REPO = "DATA_APP_INVALID_REPO"
|
|
112
|
+
DATA_APP_REPO_VALIDATION_BLOCKING = "DATA_APP_REPO_VALIDATION_BLOCKING"
|
|
113
|
+
|
|
114
|
+
# Developer Portal (since 0.48.0)
|
|
115
|
+
DP_LOGIN_FAILED = "DP_LOGIN_FAILED"
|
|
116
|
+
DP_MFA_REQUIRED = "DP_MFA_REQUIRED"
|
|
117
|
+
DP_APP_NOT_FOUND = "DP_APP_NOT_FOUND"
|
|
118
|
+
DP_PUBLISH_REQUIREMENTS_MISSING = "DP_PUBLISH_REQUIREMENTS_MISSING"
|
|
119
|
+
DP_ICON_UPLOAD_FAILED = "DP_ICON_UPLOAD_FAILED"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def mask_token(token: str) -> str:
|
|
123
|
+
"""Mask a Keboola Storage API token for safe display.
|
|
124
|
+
|
|
125
|
+
Preserves the prefix (part before the first dash) and the last 4 characters,
|
|
126
|
+
replacing the middle with '...'.
|
|
127
|
+
|
|
128
|
+
Examples:
|
|
129
|
+
mask_token("901-55555-fakeTestTokenDoNotUseXXXXXXXX")
|
|
130
|
+
-> "901-...XXXX"
|
|
131
|
+
|
|
132
|
+
mask_token("abc") -> "***"
|
|
133
|
+
mask_token("") -> "***"
|
|
134
|
+
"""
|
|
135
|
+
if len(token) < 8:
|
|
136
|
+
return "***"
|
|
137
|
+
|
|
138
|
+
dash_index = token.find("-")
|
|
139
|
+
if dash_index == -1 or dash_index >= len(token) - 4:
|
|
140
|
+
return "***"
|
|
141
|
+
|
|
142
|
+
prefix = token[:dash_index]
|
|
143
|
+
last4 = token[-4:]
|
|
144
|
+
return f"{prefix}-...{last4}"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class KeboolaApiError(Exception):
|
|
148
|
+
"""Raised when a Keboola API call fails.
|
|
149
|
+
|
|
150
|
+
Optional ``details`` payload lets the service layer attach structured
|
|
151
|
+
context (e.g. a fetched log tail, the remote-cancelled job dict) that
|
|
152
|
+
the command layer surfaces in ``--json`` mode without changing the
|
|
153
|
+
stable top-level error envelope. Keep keys small and side-effect-free;
|
|
154
|
+
PR9 will lock this schema down with a versioned enum.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
message: str,
|
|
160
|
+
status_code: int = 0,
|
|
161
|
+
error_code: str | ErrorCode = ErrorCode.UNKNOWN_ERROR,
|
|
162
|
+
retryable: bool = False,
|
|
163
|
+
details: dict | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
super().__init__(message)
|
|
166
|
+
self.message = message
|
|
167
|
+
self.status_code = status_code
|
|
168
|
+
self.error_code = error_code
|
|
169
|
+
self.retryable = retryable
|
|
170
|
+
self.details: dict = details if details is not None else {}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ConfigError(Exception):
|
|
174
|
+
"""Raised when there is a configuration problem."""
|
|
175
|
+
|
|
176
|
+
def __init__(self, message: str) -> None:
|
|
177
|
+
super().__init__(message)
|
|
178
|
+
self.message = message
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SyncConflictError(Exception):
|
|
182
|
+
"""Raised when ``sync pull --force`` would overwrite locally-modified
|
|
183
|
+
configs whose remote **also** changed since the last pull -- a true 3-way
|
|
184
|
+
merge conflict (local and remote both diverged from the synced base).
|
|
185
|
+
|
|
186
|
+
``--force`` deliberately bypasses the "preserve locally-modified files"
|
|
187
|
+
guard, so without this check it would silently adopt the edited on-disk
|
|
188
|
+
file as the new synced baseline (issue: force-pull baseline corruption).
|
|
189
|
+
Rather than discard un-pushed work, the pull aborts *before writing
|
|
190
|
+
anything* and asks the user to resolve each conflict (push or discard
|
|
191
|
+
local edits, then pull again).
|
|
192
|
+
|
|
193
|
+
``conflicts`` carries one dict per conflicting config/row so the command
|
|
194
|
+
layer can list them. Each dict has ``component_id``, ``config_id``,
|
|
195
|
+
``config_name``, ``path``, ``scope`` (``"config"`` or ``"row"``), and an
|
|
196
|
+
optional ``row_id``.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
def __init__(self, conflicts: list[dict[str, str]]) -> None:
|
|
200
|
+
self.conflicts = conflicts
|
|
201
|
+
n = len(conflicts)
|
|
202
|
+
plural = "s" if n != 1 else ""
|
|
203
|
+
message = (
|
|
204
|
+
f"{n} config{plural} ha{'ve' if n != 1 else 's'} un-pushed local "
|
|
205
|
+
f"edits AND changed on the remote since the last pull (merge "
|
|
206
|
+
f"conflict). `sync pull --force` refuses to overwrite them so your "
|
|
207
|
+
f"local work is not lost. Resolve each conflict first: review with "
|
|
208
|
+
f"`kbagent sync diff`, then either `kbagent sync push` your local "
|
|
209
|
+
f"edits or discard them, and pull again."
|
|
210
|
+
)
|
|
211
|
+
super().__init__(message)
|
|
212
|
+
self.message = message
|
|
213
|
+
self.error_code = ErrorCode.SYNC_CONFLICT
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
class PermissionDeniedError(Exception):
|
|
217
|
+
"""Raised when an operation is blocked by the permission policy."""
|
|
218
|
+
|
|
219
|
+
def __init__(self, operation: str, message: str = "") -> None:
|
|
220
|
+
if not message:
|
|
221
|
+
message = f"Operation '{operation}' is blocked by the active permission policy."
|
|
222
|
+
super().__init__(message)
|
|
223
|
+
self.operation = operation
|
|
224
|
+
self.message = message
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
_ERROR_CODE_TO_TYPE: dict[str, str] = {
|
|
228
|
+
ErrorCode.INVALID_TOKEN: "authentication",
|
|
229
|
+
ErrorCode.MISSING_MASTER_TOKEN: "authentication",
|
|
230
|
+
ErrorCode.TIMEOUT: "network",
|
|
231
|
+
ErrorCode.CONNECTION_ERROR: "network",
|
|
232
|
+
ErrorCode.RETRY_EXHAUSTED: "network",
|
|
233
|
+
ErrorCode.NOT_FOUND: "not_found",
|
|
234
|
+
ErrorCode.CONFIG_ERROR: "configuration",
|
|
235
|
+
ErrorCode.VALIDATION_ERROR: "validation",
|
|
236
|
+
ErrorCode.SYNC_CONFLICT: "conflict",
|
|
237
|
+
ErrorCode.PERMISSION_DENIED: "authorization",
|
|
238
|
+
ErrorCode.DP_LOGIN_FAILED: "authentication",
|
|
239
|
+
ErrorCode.DP_MFA_REQUIRED: "authentication",
|
|
240
|
+
ErrorCode.DP_APP_NOT_FOUND: "not_found",
|
|
241
|
+
ErrorCode.DP_PUBLISH_REQUIREMENTS_MISSING: "validation",
|
|
242
|
+
ErrorCode.DP_ICON_UPLOAD_FAILED: "api",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def map_error_code_to_type(error_code: str) -> str:
|
|
247
|
+
"""Map a machine-readable error code to a broad error type category."""
|
|
248
|
+
return _ERROR_CODE_TO_TYPE.get(error_code, "api")
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Base HTTP client with shared retry, timeout, and error handling logic.
|
|
2
|
+
|
|
3
|
+
Both KeboolaClient (Storage API) and ManageClient (Manage API) inherit
|
|
4
|
+
from BaseHttpClient to avoid duplicating the retry loop, error mapping,
|
|
5
|
+
and message sanitization code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
import time
|
|
13
|
+
from typing import Any, Self
|
|
14
|
+
from urllib.parse import urlparse, urlunparse
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from .constants import (
|
|
19
|
+
APP_NAME,
|
|
20
|
+
BACKOFF_BASE,
|
|
21
|
+
ENV_CONVERSATION_ID,
|
|
22
|
+
MAX_API_ERROR_LENGTH,
|
|
23
|
+
MAX_RETRIES,
|
|
24
|
+
MAX_RETRY_AFTER_SECONDS,
|
|
25
|
+
RETRYABLE_STATUS_CODES,
|
|
26
|
+
)
|
|
27
|
+
from .errors import ErrorCode, KeboolaApiError, mask_token
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_user_agent() -> str:
|
|
33
|
+
"""Build the User-Agent that signs every Keboola API call.
|
|
34
|
+
|
|
35
|
+
Format (RFC 7231 product + comment):
|
|
36
|
+
|
|
37
|
+
keboola-cli/<version> (<os> <release>; <arch>; <impl> <pyver>)
|
|
38
|
+
e.g. keboola-cli/0.45.0 (Darwin 25.3.0; arm64; CPython 3.12.7)
|
|
39
|
+
|
|
40
|
+
Keboola's edge logs this verbatim (DataDog access logs), so the fleet can
|
|
41
|
+
be segmented by version and OS/arch. Only neutral host metadata is sent --
|
|
42
|
+
never ``platform.node()`` (the hostname is PII). Identity ("which project /
|
|
43
|
+
user") is resolved server-side from the token, never derived client-side.
|
|
44
|
+
"""
|
|
45
|
+
from . import __version__
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
f"{APP_NAME}/{__version__} "
|
|
49
|
+
f"({platform.system()} {platform.release()}; "
|
|
50
|
+
f"{platform.machine()}; "
|
|
51
|
+
f"{platform.python_implementation()} {platform.python_version()})"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class BaseHttpClient:
|
|
56
|
+
"""Shared HTTP client with retry, timeout, and error handling.
|
|
57
|
+
|
|
58
|
+
Provides:
|
|
59
|
+
- _do_request(method, path, **kwargs): HTTP request with retry + backoff
|
|
60
|
+
- _raise_api_error(response, base_url=None): error mapping with truncation
|
|
61
|
+
- Context manager support (close, __enter__, __exit__)
|
|
62
|
+
|
|
63
|
+
Subclasses must call super().__init__() with base_url, token, headers,
|
|
64
|
+
and optional timeout.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
base_url: str,
|
|
70
|
+
token: str,
|
|
71
|
+
headers: dict[str, str],
|
|
72
|
+
timeout: httpx.Timeout | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
from .constants import DEFAULT_TIMEOUT
|
|
75
|
+
|
|
76
|
+
self._base_url = base_url.rstrip("/")
|
|
77
|
+
self._token = token
|
|
78
|
+
self._masked_token = mask_token(token)
|
|
79
|
+
# Sign every request centrally so all subclasses share one UA string
|
|
80
|
+
# (and OS/version enrichment) instead of hardcoding it five times.
|
|
81
|
+
headers["User-Agent"] = build_user_agent()
|
|
82
|
+
conversation_id = os.environ.get(ENV_CONVERSATION_ID, "")
|
|
83
|
+
if conversation_id:
|
|
84
|
+
headers["X-Conversation-ID"] = conversation_id
|
|
85
|
+
self._client = httpx.Client(
|
|
86
|
+
base_url=self._base_url,
|
|
87
|
+
timeout=timeout or DEFAULT_TIMEOUT,
|
|
88
|
+
headers=headers,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def _derive_service_url(stack_url: str, service_prefix: str) -> str:
|
|
93
|
+
"""Derive a service base URL by replacing 'connection.' in the hostname.
|
|
94
|
+
|
|
95
|
+
E.g. _derive_service_url("https://connection.keboola.com", "queue")
|
|
96
|
+
-> "https://queue.keboola.com"
|
|
97
|
+
"""
|
|
98
|
+
parsed = urlparse(stack_url)
|
|
99
|
+
hostname = parsed.hostname or ""
|
|
100
|
+
new_host = hostname.replace("connection.", f"{service_prefix}.", 1)
|
|
101
|
+
if new_host == hostname:
|
|
102
|
+
logger.warning(
|
|
103
|
+
"%s URL derivation did not change hostname: %s",
|
|
104
|
+
service_prefix,
|
|
105
|
+
hostname,
|
|
106
|
+
)
|
|
107
|
+
return urlunparse(parsed._replace(netloc=new_host))
|
|
108
|
+
|
|
109
|
+
def close(self) -> None:
|
|
110
|
+
"""Close the underlying HTTP client."""
|
|
111
|
+
self._client.close()
|
|
112
|
+
|
|
113
|
+
def __enter__(self) -> Self:
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def __exit__(self, *args: Any) -> None:
|
|
117
|
+
self.close()
|
|
118
|
+
|
|
119
|
+
def _do_request(
|
|
120
|
+
self,
|
|
121
|
+
method: str,
|
|
122
|
+
path: str,
|
|
123
|
+
*,
|
|
124
|
+
client: httpx.Client | None = None,
|
|
125
|
+
base_url: str | None = None,
|
|
126
|
+
**kwargs: Any,
|
|
127
|
+
) -> httpx.Response:
|
|
128
|
+
"""Execute an HTTP request with retry and exponential backoff.
|
|
129
|
+
|
|
130
|
+
Retries on status codes 429, 500, 502, 503, 504 up to MAX_RETRIES times
|
|
131
|
+
with exponential backoff (1s, 2s, 4s).
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
method: HTTP method (GET, POST, etc.).
|
|
135
|
+
path: URL path relative to base_url.
|
|
136
|
+
client: Optional httpx.Client to use (defaults to self._client).
|
|
137
|
+
Useful for subclasses that maintain multiple clients (e.g. queue client).
|
|
138
|
+
base_url: Optional base URL for error messages (defaults to self._base_url).
|
|
139
|
+
**kwargs: Additional arguments passed to httpx.Client.request().
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The HTTP response on success.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
KeboolaApiError: On HTTP errors (with masked token) or after retries exhausted.
|
|
146
|
+
"""
|
|
147
|
+
http_client = client or self._client
|
|
148
|
+
url_label = base_url or self._base_url
|
|
149
|
+
last_response: httpx.Response | None = None
|
|
150
|
+
|
|
151
|
+
for attempt in range(MAX_RETRIES):
|
|
152
|
+
try:
|
|
153
|
+
response = http_client.request(method, path, **kwargs)
|
|
154
|
+
|
|
155
|
+
if response.status_code < 400:
|
|
156
|
+
return response
|
|
157
|
+
|
|
158
|
+
if response.status_code in RETRYABLE_STATUS_CODES and attempt < MAX_RETRIES - 1:
|
|
159
|
+
if response.status_code == 429:
|
|
160
|
+
retry_after = response.headers.get("Retry-After")
|
|
161
|
+
if retry_after:
|
|
162
|
+
try:
|
|
163
|
+
delay = min(float(retry_after), MAX_RETRY_AFTER_SECONDS)
|
|
164
|
+
except ValueError:
|
|
165
|
+
delay = BACKOFF_BASE * (2**attempt)
|
|
166
|
+
else:
|
|
167
|
+
delay = BACKOFF_BASE * (2**attempt)
|
|
168
|
+
else:
|
|
169
|
+
delay = BACKOFF_BASE * (2**attempt)
|
|
170
|
+
logger.debug(
|
|
171
|
+
"Retry attempt %d/%d for %s %s (status %d), delay %.1fs",
|
|
172
|
+
attempt + 1,
|
|
173
|
+
MAX_RETRIES,
|
|
174
|
+
method,
|
|
175
|
+
path,
|
|
176
|
+
response.status_code,
|
|
177
|
+
delay,
|
|
178
|
+
)
|
|
179
|
+
time.sleep(delay)
|
|
180
|
+
last_response = response
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
self._raise_api_error(response, url_label)
|
|
184
|
+
|
|
185
|
+
except httpx.TimeoutException as exc:
|
|
186
|
+
if attempt < MAX_RETRIES - 1:
|
|
187
|
+
delay = BACKOFF_BASE * (2**attempt)
|
|
188
|
+
logger.debug(
|
|
189
|
+
"Retry attempt %d/%d for %s %s (timeout), delay %.1fs",
|
|
190
|
+
attempt + 1,
|
|
191
|
+
MAX_RETRIES,
|
|
192
|
+
method,
|
|
193
|
+
path,
|
|
194
|
+
delay,
|
|
195
|
+
)
|
|
196
|
+
time.sleep(delay)
|
|
197
|
+
continue
|
|
198
|
+
raise KeboolaApiError(
|
|
199
|
+
message=f"Request timed out connecting to {url_label} (token: {self._masked_token})",
|
|
200
|
+
status_code=0,
|
|
201
|
+
error_code=ErrorCode.TIMEOUT,
|
|
202
|
+
retryable=True,
|
|
203
|
+
) from exc
|
|
204
|
+
|
|
205
|
+
except httpx.ConnectError as exc:
|
|
206
|
+
if attempt < MAX_RETRIES - 1:
|
|
207
|
+
delay = BACKOFF_BASE * (2**attempt)
|
|
208
|
+
logger.debug(
|
|
209
|
+
"Retry attempt %d/%d for %s %s (connection error), delay %.1fs",
|
|
210
|
+
attempt + 1,
|
|
211
|
+
MAX_RETRIES,
|
|
212
|
+
method,
|
|
213
|
+
path,
|
|
214
|
+
delay,
|
|
215
|
+
)
|
|
216
|
+
time.sleep(delay)
|
|
217
|
+
continue
|
|
218
|
+
raise KeboolaApiError(
|
|
219
|
+
message=f"Cannot connect to {url_label} (token: {self._masked_token})",
|
|
220
|
+
status_code=0,
|
|
221
|
+
error_code=ErrorCode.CONNECTION_ERROR,
|
|
222
|
+
retryable=True,
|
|
223
|
+
) from exc
|
|
224
|
+
|
|
225
|
+
if last_response is not None:
|
|
226
|
+
self._raise_api_error(last_response, url_label)
|
|
227
|
+
|
|
228
|
+
raise KeboolaApiError(
|
|
229
|
+
message=f"Request failed after {MAX_RETRIES} retries to {url_label} (token: {self._masked_token})",
|
|
230
|
+
status_code=0,
|
|
231
|
+
error_code=ErrorCode.RETRY_EXHAUSTED,
|
|
232
|
+
retryable=True,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
def _raise_api_error(self, response: httpx.Response, base_url: str | None = None) -> None:
|
|
236
|
+
"""Convert an HTTP error response into a KeboolaApiError.
|
|
237
|
+
|
|
238
|
+
Parses the response body for error messages, truncates long messages
|
|
239
|
+
to MAX_API_ERROR_LENGTH characters, and maps status codes to
|
|
240
|
+
appropriate error codes.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
response: The HTTP error response.
|
|
244
|
+
base_url: Optional URL label for error messages.
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
KeboolaApiError: Always raised with appropriate error code and message.
|
|
248
|
+
"""
|
|
249
|
+
status = response.status_code
|
|
250
|
+
url_label = base_url or self._base_url
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
body = response.json()
|
|
254
|
+
# Real Keboola APIs answer with one of these keys in priority
|
|
255
|
+
# order. Two caveats:
|
|
256
|
+
# 1. Keboola Metastore puts the HTTP status code into `error`
|
|
257
|
+
# as an int (e.g. {"error": 422}) -- using `or` would
|
|
258
|
+
# shadow the actual error message in `errors`/`exception`.
|
|
259
|
+
# So we only accept `error` if it's a non-empty string.
|
|
260
|
+
# 2. `errors`/`detail` are lists of dicts (FastAPI / metastore
|
|
261
|
+
# 422 shape); we json.dumps them so the f-string render
|
|
262
|
+
# below doesn't print `[{...}]` repr.
|
|
263
|
+
err_field = body.get("error")
|
|
264
|
+
api_message = (
|
|
265
|
+
err_field
|
|
266
|
+
if isinstance(err_field, str) and err_field
|
|
267
|
+
else (
|
|
268
|
+
body.get("exception")
|
|
269
|
+
or body.get("message")
|
|
270
|
+
or body.get("description")
|
|
271
|
+
or body.get("detail")
|
|
272
|
+
or body.get("errors")
|
|
273
|
+
or json.dumps(body)
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
if not isinstance(api_message, str):
|
|
277
|
+
api_message = json.dumps(api_message)
|
|
278
|
+
except Exception:
|
|
279
|
+
api_message = response.text
|
|
280
|
+
|
|
281
|
+
# Truncate to prevent Rich markup injection and excessive output
|
|
282
|
+
if isinstance(api_message, str) and len(api_message) > MAX_API_ERROR_LENGTH:
|
|
283
|
+
api_message = api_message[:MAX_API_ERROR_LENGTH] + "..."
|
|
284
|
+
|
|
285
|
+
if status == 401:
|
|
286
|
+
raise KeboolaApiError(
|
|
287
|
+
message=f"Invalid or expired token (token: {self._masked_token}): {api_message}",
|
|
288
|
+
status_code=status,
|
|
289
|
+
error_code=ErrorCode.INVALID_TOKEN,
|
|
290
|
+
retryable=False,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
if status == 403:
|
|
294
|
+
raise KeboolaApiError(
|
|
295
|
+
message=f"Access denied (token: {self._masked_token}): {api_message}",
|
|
296
|
+
status_code=status,
|
|
297
|
+
error_code=ErrorCode.ACCESS_DENIED,
|
|
298
|
+
retryable=False,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
if status == 404:
|
|
302
|
+
raise KeboolaApiError(
|
|
303
|
+
message=f"Resource not found: {api_message}",
|
|
304
|
+
status_code=status,
|
|
305
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
306
|
+
retryable=False,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
retryable = status in RETRYABLE_STATUS_CODES
|
|
310
|
+
raise KeboolaApiError(
|
|
311
|
+
message=f"API error {status} from {url_label} (token: {self._masked_token}): {api_message}",
|
|
312
|
+
status_code=status,
|
|
313
|
+
error_code=ErrorCode.API_ERROR,
|
|
314
|
+
retryable=retryable,
|
|
315
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""JSON deep-merge and nested-path utilities.
|
|
2
|
+
|
|
3
|
+
Used by ``config update`` to patch configuration content without
|
|
4
|
+
losing sibling keys -- the exact problem that MCP server's
|
|
5
|
+
``update_config`` tool has (keboola/mcp-server#468).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def deep_merge(target: dict[str, Any], source: dict[str, Any]) -> dict[str, Any]:
|
|
15
|
+
"""Recursively merge *source* into *target* (non-mutating).
|
|
16
|
+
|
|
17
|
+
Rules:
|
|
18
|
+
* dict + dict → recursively merged
|
|
19
|
+
* anything else → *source* wins (including list replaces list)
|
|
20
|
+
|
|
21
|
+
Returns a new dict; neither *target* nor *source* is mutated.
|
|
22
|
+
"""
|
|
23
|
+
result = copy.deepcopy(target)
|
|
24
|
+
for key, src_value in source.items():
|
|
25
|
+
if key in result and isinstance(result[key], dict) and isinstance(src_value, dict):
|
|
26
|
+
result[key] = deep_merge(result[key], src_value)
|
|
27
|
+
else:
|
|
28
|
+
result[key] = copy.deepcopy(src_value)
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_nested_value(obj: Any, path: str) -> Any:
|
|
33
|
+
"""Retrieve a value from a nested structure using a dot-separated path.
|
|
34
|
+
|
|
35
|
+
Supports integer segments for list indexing (e.g. ``"tables.0.name"``).
|
|
36
|
+
|
|
37
|
+
Raises ``KeyError`` or ``IndexError`` if the path does not exist.
|
|
38
|
+
"""
|
|
39
|
+
for segment in path.split("."):
|
|
40
|
+
if isinstance(obj, dict):
|
|
41
|
+
obj = obj[segment]
|
|
42
|
+
elif isinstance(obj, list):
|
|
43
|
+
obj = obj[int(segment)]
|
|
44
|
+
else:
|
|
45
|
+
raise KeyError(f"Cannot traverse into {type(obj).__name__} with key '{segment}'")
|
|
46
|
+
return obj
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def set_nested_value(obj: dict[str, Any], path: str, value: Any) -> dict[str, Any]:
|
|
50
|
+
"""Set a value at a dot-separated path, creating intermediate dicts.
|
|
51
|
+
|
|
52
|
+
Returns a deep-copied dict with the value set — *obj* is not mutated.
|
|
53
|
+
|
|
54
|
+
Supports integer segments for list indexing on **existing** lists
|
|
55
|
+
(new intermediate containers are always dicts).
|
|
56
|
+
"""
|
|
57
|
+
result = copy.deepcopy(obj)
|
|
58
|
+
segments = path.split(".")
|
|
59
|
+
current: Any = result
|
|
60
|
+
for segment in segments[:-1]:
|
|
61
|
+
if isinstance(current, dict):
|
|
62
|
+
if segment not in current:
|
|
63
|
+
current[segment] = {}
|
|
64
|
+
current = current[segment]
|
|
65
|
+
elif isinstance(current, list):
|
|
66
|
+
current = current[int(segment)]
|
|
67
|
+
else:
|
|
68
|
+
raise KeyError(f"Cannot traverse into {type(current).__name__} with key '{segment}'")
|
|
69
|
+
|
|
70
|
+
last = segments[-1]
|
|
71
|
+
if isinstance(current, dict):
|
|
72
|
+
current[last] = copy.deepcopy(value)
|
|
73
|
+
elif isinstance(current, list):
|
|
74
|
+
current[int(last)] = copy.deepcopy(value)
|
|
75
|
+
else:
|
|
76
|
+
raise KeyError(f"Cannot set key '{last}' on {type(current).__name__}")
|
|
77
|
+
return result
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def compute_diff(
|
|
81
|
+
old: dict[str, Any],
|
|
82
|
+
new: dict[str, Any],
|
|
83
|
+
path: str = "",
|
|
84
|
+
) -> list[str]:
|
|
85
|
+
"""Produce a human-readable list of changes between two dicts.
|
|
86
|
+
|
|
87
|
+
Each entry looks like:
|
|
88
|
+
``"parameters.tables.count: 5 -> 10"``
|
|
89
|
+
``"parameters.newKey: (absent) -> 'hello'"``
|
|
90
|
+
``"parameters.removed: 42 -> (absent)"``
|
|
91
|
+
"""
|
|
92
|
+
changes: list[str] = []
|
|
93
|
+
all_keys = sorted(set(list(old.keys()) + list(new.keys())))
|
|
94
|
+
|
|
95
|
+
for key in all_keys:
|
|
96
|
+
full_path = f"{path}.{key}" if path else key
|
|
97
|
+
in_old = key in old
|
|
98
|
+
in_new = key in new
|
|
99
|
+
|
|
100
|
+
if in_old and in_new:
|
|
101
|
+
old_val = old[key]
|
|
102
|
+
new_val = new[key]
|
|
103
|
+
if isinstance(old_val, dict) and isinstance(new_val, dict):
|
|
104
|
+
changes.extend(compute_diff(old_val, new_val, full_path))
|
|
105
|
+
elif old_val != new_val:
|
|
106
|
+
changes.append(f"{full_path}: {_fmt(old_val)} -> {_fmt(new_val)}")
|
|
107
|
+
elif in_old and not in_new:
|
|
108
|
+
changes.append(f"{full_path}: {_fmt(old[key])} -> (absent)")
|
|
109
|
+
else:
|
|
110
|
+
changes.append(f"{full_path}: (absent) -> {_fmt(new[key])}")
|
|
111
|
+
|
|
112
|
+
return changes
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _fmt(value: Any) -> str:
|
|
116
|
+
"""Format a value for diff display — truncate long representations."""
|
|
117
|
+
if isinstance(value, str):
|
|
118
|
+
s = repr(value)
|
|
119
|
+
elif isinstance(value, dict):
|
|
120
|
+
s = f"{{...}} ({len(value)} keys)"
|
|
121
|
+
elif isinstance(value, list):
|
|
122
|
+
s = f"[...] ({len(value)} items)"
|
|
123
|
+
else:
|
|
124
|
+
s = repr(value)
|
|
125
|
+
max_len = 80
|
|
126
|
+
return s if len(s) <= max_len else s[: max_len - 3] + "..."
|