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,1084 @@
|
|
|
1
|
+
"""Output formatting with JSON and Rich dual mode support."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.panel import Panel
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
|
|
14
|
+
from .models import ErrorResponse, SuccessResponse
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OutputFormatter:
|
|
18
|
+
"""Formats CLI output as either JSON (for machines/agents) or Rich (for humans).
|
|
19
|
+
|
|
20
|
+
In JSON mode, all output goes to stdout as valid JSON.
|
|
21
|
+
In human mode, Rich console is used for formatted tables and panels.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
json_mode: bool = False,
|
|
27
|
+
no_color: bool = False,
|
|
28
|
+
verbose: bool = False,
|
|
29
|
+
) -> None:
|
|
30
|
+
self.json_mode = json_mode
|
|
31
|
+
self.verbose = verbose
|
|
32
|
+
is_tty = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
33
|
+
force_terminal = None if is_tty and not no_color else False
|
|
34
|
+
self.console = Console(
|
|
35
|
+
no_color=no_color,
|
|
36
|
+
force_terminal=force_terminal,
|
|
37
|
+
)
|
|
38
|
+
self.err_console = Console(
|
|
39
|
+
stderr=True,
|
|
40
|
+
no_color=no_color,
|
|
41
|
+
force_terminal=force_terminal,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def output(
|
|
45
|
+
self, data: Any, human_formatter: Callable[[Console, Any], object] | None = None
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Output data in the appropriate format.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
data: The data to output. In JSON mode, this is serialized directly.
|
|
51
|
+
In human mode, it's passed to human_formatter.
|
|
52
|
+
human_formatter: A callable that takes (Console, data) and prints
|
|
53
|
+
human-friendly output. If None in human mode, prints repr.
|
|
54
|
+
"""
|
|
55
|
+
if self.json_mode:
|
|
56
|
+
response = SuccessResponse(status="ok", data=data)
|
|
57
|
+
sys.stdout.write(response.model_dump_json(indent=2) + "\n")
|
|
58
|
+
else:
|
|
59
|
+
if human_formatter is not None:
|
|
60
|
+
human_formatter(self.console, data)
|
|
61
|
+
else:
|
|
62
|
+
self.console.print(data)
|
|
63
|
+
|
|
64
|
+
def error(
|
|
65
|
+
self,
|
|
66
|
+
message: str,
|
|
67
|
+
error_code: str = "ERROR",
|
|
68
|
+
project: str = "",
|
|
69
|
+
retryable: bool = False,
|
|
70
|
+
error_type: str = "",
|
|
71
|
+
details: dict | None = None,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Output an error message.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
message: Human-readable error description.
|
|
77
|
+
error_code: Machine-readable error code.
|
|
78
|
+
project: Project alias related to the error.
|
|
79
|
+
retryable: Whether the operation can be retried.
|
|
80
|
+
error_type: Broad error category. If empty, derived from error_code.
|
|
81
|
+
details: Optional structured context (e.g. {"logTail": [...]}).
|
|
82
|
+
When empty/None, the field is omitted from JSON output so
|
|
83
|
+
consumers can key off presence.
|
|
84
|
+
"""
|
|
85
|
+
if self.json_mode:
|
|
86
|
+
if not error_type:
|
|
87
|
+
from .errors import map_error_code_to_type
|
|
88
|
+
|
|
89
|
+
error_type = map_error_code_to_type(error_code)
|
|
90
|
+
err = ErrorResponse(
|
|
91
|
+
code=error_code,
|
|
92
|
+
error_type=error_type,
|
|
93
|
+
message=message,
|
|
94
|
+
project=project,
|
|
95
|
+
retryable=retryable,
|
|
96
|
+
details=details if details else None,
|
|
97
|
+
)
|
|
98
|
+
error_envelope = {
|
|
99
|
+
"status": "error",
|
|
100
|
+
"error": err.model_dump(exclude_none=True),
|
|
101
|
+
}
|
|
102
|
+
sys.stdout.write(json.dumps(error_envelope, indent=2) + "\n")
|
|
103
|
+
else:
|
|
104
|
+
self.err_console.print(f"[bold red]Error:[/bold red] {message}")
|
|
105
|
+
|
|
106
|
+
def success(self, message: str) -> None:
|
|
107
|
+
"""Output a success message.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
message: The success message to display.
|
|
111
|
+
"""
|
|
112
|
+
if self.json_mode:
|
|
113
|
+
response = SuccessResponse(status="ok", data={"message": message})
|
|
114
|
+
sys.stdout.write(response.model_dump_json(indent=2) + "\n")
|
|
115
|
+
else:
|
|
116
|
+
self.console.print(f"[bold green]Success:[/bold green] {message}")
|
|
117
|
+
|
|
118
|
+
def warning(self, message: str) -> None:
|
|
119
|
+
"""Output a warning message to stderr (human mode only).
|
|
120
|
+
|
|
121
|
+
In JSON mode, warnings are not printed separately -- they are
|
|
122
|
+
embedded in the structured response via the errors list.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
message: The warning message to display.
|
|
126
|
+
"""
|
|
127
|
+
if not self.json_mode:
|
|
128
|
+
self.err_console.print(f"[bold yellow]Warning:[/bold yellow] {message}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def format_configs_table(console: Console, data: dict[str, Any]) -> None:
|
|
132
|
+
"""Render a Rich table of configurations grouped by project alias.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
console: Rich Console instance.
|
|
136
|
+
data: Dict with "configs" (list of config dicts) and "errors" (list of error dicts).
|
|
137
|
+
"""
|
|
138
|
+
configs = data.get("configs", [])
|
|
139
|
+
errors = data.get("errors", [])
|
|
140
|
+
|
|
141
|
+
# Show per-project errors as warnings
|
|
142
|
+
for err in errors:
|
|
143
|
+
console.print(
|
|
144
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
145
|
+
f"{err['message']}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if not configs:
|
|
149
|
+
if not errors:
|
|
150
|
+
console.print(
|
|
151
|
+
"No configurations found. Use [bold]kbagent project add[/bold] to connect a project first."
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
console.print("No configurations retrieved (all projects failed).")
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
# Group configs by project alias
|
|
158
|
+
projects_order: list[str] = []
|
|
159
|
+
grouped: dict[str, list[dict[str, Any]]] = {}
|
|
160
|
+
for cfg in configs:
|
|
161
|
+
alias = cfg["project_alias"]
|
|
162
|
+
if alias not in grouped:
|
|
163
|
+
projects_order.append(alias)
|
|
164
|
+
grouped[alias] = []
|
|
165
|
+
grouped[alias].append(cfg)
|
|
166
|
+
|
|
167
|
+
for alias in projects_order:
|
|
168
|
+
project_configs = grouped[alias]
|
|
169
|
+
table = Table(title=f"Configurations - {alias}")
|
|
170
|
+
table.add_column("Component", style="bold cyan")
|
|
171
|
+
table.add_column("Type", style="dim")
|
|
172
|
+
table.add_column("Config ID", justify="right")
|
|
173
|
+
table.add_column("Config Name")
|
|
174
|
+
table.add_column("Folder", style="dim")
|
|
175
|
+
table.add_column("Last Modified", style="dim")
|
|
176
|
+
table.add_column("Modified By", style="dim")
|
|
177
|
+
|
|
178
|
+
for cfg in project_configs:
|
|
179
|
+
# Format last_modified to shorter form if present
|
|
180
|
+
last_mod = cfg.get("last_modified", "")
|
|
181
|
+
if last_mod and "T" in last_mod:
|
|
182
|
+
last_mod = last_mod.split("T")[0] # just date part
|
|
183
|
+
|
|
184
|
+
table.add_row(
|
|
185
|
+
cfg["component_id"],
|
|
186
|
+
cfg["component_type"],
|
|
187
|
+
cfg["config_id"],
|
|
188
|
+
cfg["config_name"],
|
|
189
|
+
cfg.get("folder", ""),
|
|
190
|
+
last_mod,
|
|
191
|
+
cfg.get("last_modified_by", ""),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
console.print(table)
|
|
195
|
+
console.print()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def format_config_detail(console: Console, data: dict[str, Any]) -> None:
|
|
199
|
+
"""Render detailed configuration information.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
console: Rich Console instance.
|
|
203
|
+
data: Configuration detail dict from the API.
|
|
204
|
+
"""
|
|
205
|
+
alias = data.get("project_alias", "unknown")
|
|
206
|
+
name = data.get("name", "Unknown")
|
|
207
|
+
config_id = data.get("id", "")
|
|
208
|
+
description = data.get("description", "")
|
|
209
|
+
component_id = data.get("component_id", data.get("componentId", ""))
|
|
210
|
+
|
|
211
|
+
header = f"Configuration Detail - {alias}"
|
|
212
|
+
|
|
213
|
+
lines = [
|
|
214
|
+
f"[bold]Name:[/bold] {name}",
|
|
215
|
+
f"[bold]Config ID:[/bold] {config_id}",
|
|
216
|
+
f"[bold]Component:[/bold] {component_id}",
|
|
217
|
+
]
|
|
218
|
+
if description:
|
|
219
|
+
lines.append(f"[bold]Description:[/bold] {description}")
|
|
220
|
+
|
|
221
|
+
# Show configuration parameters if present
|
|
222
|
+
configuration = data.get("configuration", {})
|
|
223
|
+
if configuration:
|
|
224
|
+
config_str = json.dumps(configuration, indent=2)
|
|
225
|
+
lines.append(f"\n[bold]Configuration:[/bold]\n{config_str}")
|
|
226
|
+
|
|
227
|
+
# keboola.sandboxes annotation (issue #304 bod #3): make explicit that
|
|
228
|
+
# ``parameters.id`` is NOT the Storage workspace ID, and show the real
|
|
229
|
+
# mapping resolved by the command layer.
|
|
230
|
+
annotation = data.get("sandbox_annotation")
|
|
231
|
+
if annotation:
|
|
232
|
+
sb_id = annotation.get("sandbox_service_id")
|
|
233
|
+
ws_id = annotation.get("storage_workspace_id")
|
|
234
|
+
lines.append("\n[bold yellow]Sandbox / Workspace mapping:[/bold yellow]")
|
|
235
|
+
lines.append(
|
|
236
|
+
f" [dim]parameters.id (sandbox-service):[/dim] {sb_id if sb_id is not None else '[dim](absent)[/dim]'}"
|
|
237
|
+
)
|
|
238
|
+
if ws_id is not None:
|
|
239
|
+
lines.append(
|
|
240
|
+
f" [bold]Storage workspace ID:[/bold] [green]{ws_id}[/green] "
|
|
241
|
+
f"[dim](use with `kbagent workspace detail --workspace-id {ws_id}`)[/dim]"
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
lines.append(
|
|
245
|
+
" [bold]Storage workspace ID:[/bold] [dim]none[/dim] "
|
|
246
|
+
"[yellow](no workspace currently backed by this config -- orphan sandbox)[/yellow]"
|
|
247
|
+
)
|
|
248
|
+
lines.append(f" [dim]{annotation.get('note', '')}[/dim]")
|
|
249
|
+
|
|
250
|
+
# Show rows if present
|
|
251
|
+
rows = data.get("rows", [])
|
|
252
|
+
if rows:
|
|
253
|
+
lines.append(f"\n[bold]Rows:[/bold] {len(rows)} row(s)")
|
|
254
|
+
for row in rows[:10]: # Show at most 10 rows
|
|
255
|
+
row_name = row.get("name", row.get("id", ""))
|
|
256
|
+
lines.append(f" - {row_name}")
|
|
257
|
+
if len(rows) > 10:
|
|
258
|
+
lines.append(f" ... and {len(rows) - 10} more")
|
|
259
|
+
|
|
260
|
+
panel = Panel("\n".join(lines), title=header, expand=False)
|
|
261
|
+
console.print(panel)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
_JOB_STATUS_STYLES = {
|
|
265
|
+
"success": "bold green",
|
|
266
|
+
"error": "bold red",
|
|
267
|
+
"processing": "bold blue",
|
|
268
|
+
"cancelled": "bold yellow",
|
|
269
|
+
"terminated": "bold yellow",
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def format_jobs_table(console: Console, data: dict[str, Any]) -> None:
|
|
274
|
+
"""Render a Rich table of jobs grouped by project alias.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
console: Rich Console instance.
|
|
278
|
+
data: Dict with "jobs" (list of job dicts) and "errors" (list of error dicts).
|
|
279
|
+
"""
|
|
280
|
+
jobs = data.get("jobs", [])
|
|
281
|
+
errors = data.get("errors", [])
|
|
282
|
+
|
|
283
|
+
for err in errors:
|
|
284
|
+
console.print(
|
|
285
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
286
|
+
f"{err['message']}"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if not jobs:
|
|
290
|
+
if not errors:
|
|
291
|
+
console.print(
|
|
292
|
+
"No jobs found. Use [bold]kbagent project add[/bold] to connect a project first."
|
|
293
|
+
)
|
|
294
|
+
else:
|
|
295
|
+
console.print("No jobs retrieved (all projects failed).")
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
# Detect if jobs span multiple projects
|
|
299
|
+
project_aliases = {job.get("project_alias", "unknown") for job in jobs}
|
|
300
|
+
multi_project = len(project_aliases) > 1
|
|
301
|
+
|
|
302
|
+
table = Table(title="Jobs")
|
|
303
|
+
table.add_column("Project", style="bold magenta")
|
|
304
|
+
table.add_column("Job ID", justify="right")
|
|
305
|
+
table.add_column("Status")
|
|
306
|
+
table.add_column("Component", style="bold cyan")
|
|
307
|
+
table.add_column("Config ID", justify="right")
|
|
308
|
+
table.add_column("Created", style="dim")
|
|
309
|
+
table.add_column("Duration", justify="right")
|
|
310
|
+
|
|
311
|
+
# Track previous alias for visual grouping (show alias only on change)
|
|
312
|
+
prev_alias = None
|
|
313
|
+
for job in jobs:
|
|
314
|
+
alias = job.get("project_alias", "unknown")
|
|
315
|
+
status = job.get("status", "unknown")
|
|
316
|
+
style = _JOB_STATUS_STYLES.get(status, "")
|
|
317
|
+
status_display = f"[{style}]{status}[/{style}]" if style else status
|
|
318
|
+
|
|
319
|
+
duration = _format_duration(job)
|
|
320
|
+
|
|
321
|
+
# In multi-project mode, show alias only on first row of each group
|
|
322
|
+
if multi_project:
|
|
323
|
+
display_alias = alias if alias != prev_alias else ""
|
|
324
|
+
else:
|
|
325
|
+
display_alias = alias if prev_alias is None else ""
|
|
326
|
+
prev_alias = alias
|
|
327
|
+
|
|
328
|
+
table.add_row(
|
|
329
|
+
display_alias,
|
|
330
|
+
str(job.get("id", "")),
|
|
331
|
+
status_display,
|
|
332
|
+
job.get("component", ""),
|
|
333
|
+
str(job.get("configId", job.get("config_id", ""))),
|
|
334
|
+
job.get("createdTime", ""),
|
|
335
|
+
duration,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
console.print(table)
|
|
339
|
+
console.print()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _format_duration(job: dict[str, Any]) -> str:
|
|
343
|
+
"""Format job duration from startTime/endTime or durationSeconds."""
|
|
344
|
+
duration_sec = job.get("durationSeconds")
|
|
345
|
+
if duration_sec is not None:
|
|
346
|
+
return _seconds_to_human(int(duration_sec))
|
|
347
|
+
|
|
348
|
+
start = job.get("startTime")
|
|
349
|
+
end = job.get("endTime")
|
|
350
|
+
if start and end:
|
|
351
|
+
try:
|
|
352
|
+
start_dt = datetime.fromisoformat(start)
|
|
353
|
+
end_dt = datetime.fromisoformat(end)
|
|
354
|
+
delta = int((end_dt - start_dt).total_seconds())
|
|
355
|
+
return _seconds_to_human(delta)
|
|
356
|
+
except (ValueError, TypeError):
|
|
357
|
+
pass
|
|
358
|
+
|
|
359
|
+
return "-"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _seconds_to_human(seconds: int) -> str:
|
|
363
|
+
"""Convert seconds to a human-readable duration string."""
|
|
364
|
+
if seconds < 60:
|
|
365
|
+
return f"{seconds}s"
|
|
366
|
+
minutes, secs = divmod(seconds, 60)
|
|
367
|
+
if minutes < 60:
|
|
368
|
+
return f"{minutes}m {secs}s"
|
|
369
|
+
hours, mins = divmod(minutes, 60)
|
|
370
|
+
return f"{hours}h {mins}m"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def format_job_detail(console: Console, data: dict[str, Any]) -> None:
|
|
374
|
+
"""Render detailed job information as a Rich panel.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
console: Rich Console instance.
|
|
378
|
+
data: Job detail dict from the Queue API with project_alias.
|
|
379
|
+
"""
|
|
380
|
+
alias = data.get("project_alias", "unknown")
|
|
381
|
+
job_id = data.get("id", "")
|
|
382
|
+
status = data.get("status", "unknown")
|
|
383
|
+
style = _JOB_STATUS_STYLES.get(status, "")
|
|
384
|
+
status_display = f"[{style}]{status}[/{style}]" if style else status
|
|
385
|
+
|
|
386
|
+
lines = [
|
|
387
|
+
f"[bold]Job ID:[/bold] {job_id}",
|
|
388
|
+
f"[bold]Project:[/bold] {alias}",
|
|
389
|
+
f"[bold]Status:[/bold] {status_display}",
|
|
390
|
+
f"[bold]Component:[/bold] {data.get('component', '')}",
|
|
391
|
+
f"[bold]Config ID:[/bold] {data.get('config', data.get('configId', ''))}",
|
|
392
|
+
f"[bold]Mode:[/bold] {data.get('mode', '')}",
|
|
393
|
+
f"[bold]Type:[/bold] {data.get('type', '')}",
|
|
394
|
+
]
|
|
395
|
+
|
|
396
|
+
# Timing
|
|
397
|
+
created = data.get("createdTime", "")
|
|
398
|
+
start = data.get("startTime", "")
|
|
399
|
+
end = data.get("endTime", "")
|
|
400
|
+
duration = _format_duration(data)
|
|
401
|
+
|
|
402
|
+
if created:
|
|
403
|
+
lines.append(f"[bold]Created:[/bold] {created}")
|
|
404
|
+
if start:
|
|
405
|
+
lines.append(f"[bold]Started:[/bold] {start}")
|
|
406
|
+
if end:
|
|
407
|
+
lines.append(f"[bold]Ended:[/bold] {end}")
|
|
408
|
+
lines.append(f"[bold]Duration:[/bold] {duration}")
|
|
409
|
+
|
|
410
|
+
# Branch and orchestration
|
|
411
|
+
branch_id = data.get("branchId")
|
|
412
|
+
if branch_id:
|
|
413
|
+
lines.append(f"[bold]Branch ID:[/bold] {branch_id}")
|
|
414
|
+
orch_job = data.get("orchestrationJobId")
|
|
415
|
+
if orch_job:
|
|
416
|
+
lines.append(f"[bold]Orchestration Job:[/bold] {orch_job}")
|
|
417
|
+
|
|
418
|
+
# URL
|
|
419
|
+
url = data.get("url", "")
|
|
420
|
+
if url:
|
|
421
|
+
lines.append(f"[bold]URL:[/bold] {url}")
|
|
422
|
+
|
|
423
|
+
# Result message
|
|
424
|
+
result = data.get("result", {})
|
|
425
|
+
if isinstance(result, dict):
|
|
426
|
+
message = result.get("message", "")
|
|
427
|
+
if message:
|
|
428
|
+
lines.append(f"\n[bold]Result Message:[/bold]\n{message}")
|
|
429
|
+
|
|
430
|
+
error_info = result.get("error", {})
|
|
431
|
+
if isinstance(error_info, dict) and error_info:
|
|
432
|
+
error_type = error_info.get("type", "")
|
|
433
|
+
if error_type:
|
|
434
|
+
lines.append(f"[bold]Error Type:[/bold] {error_type}")
|
|
435
|
+
|
|
436
|
+
panel = Panel("\n".join(lines), title=f"Job Detail - {job_id}", expand=False)
|
|
437
|
+
console.print(panel)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _format_tool_params(schema: dict[str, Any]) -> str:
|
|
441
|
+
"""Format inputSchema properties as a compact param string.
|
|
442
|
+
|
|
443
|
+
Required params are marked with *, optional are dim.
|
|
444
|
+
Example: "sql_query*, query_name*"
|
|
445
|
+
"""
|
|
446
|
+
props = schema.get("properties", {})
|
|
447
|
+
required = set(schema.get("required", []))
|
|
448
|
+
if not props:
|
|
449
|
+
return "[dim](none)[/dim]"
|
|
450
|
+
parts = []
|
|
451
|
+
for name in props:
|
|
452
|
+
if name in required:
|
|
453
|
+
parts.append(f"[bold]{name}[/bold]*")
|
|
454
|
+
else:
|
|
455
|
+
parts.append(f"[dim]{name}[/dim]")
|
|
456
|
+
return ", ".join(parts)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def format_tools_table(console: Console, data: dict[str, Any]) -> None:
|
|
460
|
+
"""Render a Rich table of MCP tools.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
console: Rich Console instance.
|
|
464
|
+
data: Dict with "tools" list and "errors" list.
|
|
465
|
+
"""
|
|
466
|
+
tools = data.get("tools", [])
|
|
467
|
+
errors = data.get("errors", [])
|
|
468
|
+
|
|
469
|
+
for err in errors:
|
|
470
|
+
console.print(
|
|
471
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
472
|
+
f"{err['message']}"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if not tools:
|
|
476
|
+
if not errors:
|
|
477
|
+
console.print(
|
|
478
|
+
"No MCP tools found. Ensure keboola-mcp-server is installed and a project is connected."
|
|
479
|
+
)
|
|
480
|
+
else:
|
|
481
|
+
console.print("No tools retrieved (all projects failed).")
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
table = Table(title="MCP Tools")
|
|
485
|
+
table.add_column("Tool Name", style="bold cyan")
|
|
486
|
+
table.add_column("Parameters")
|
|
487
|
+
table.add_column("Multi-Project", justify="center")
|
|
488
|
+
table.add_column("Description", max_width=60)
|
|
489
|
+
|
|
490
|
+
for tool in tools:
|
|
491
|
+
multi = "[green]yes[/green]" if tool.get("multi_project") else "[dim]no[/dim]"
|
|
492
|
+
params_str = _format_tool_params(tool.get("inputSchema", {}))
|
|
493
|
+
table.add_row(
|
|
494
|
+
tool["name"],
|
|
495
|
+
params_str,
|
|
496
|
+
multi,
|
|
497
|
+
tool.get("description", ""),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
console.print(table)
|
|
501
|
+
console.print()
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _extract_result_text(result: dict[str, Any]) -> str:
|
|
505
|
+
"""Extract a text representation of a tool result's content for comparison."""
|
|
506
|
+
parts = []
|
|
507
|
+
for item in result.get("content", []):
|
|
508
|
+
if isinstance(item, str):
|
|
509
|
+
parts.append(item)
|
|
510
|
+
elif isinstance(item, dict):
|
|
511
|
+
parts.append(json.dumps(item, sort_keys=True))
|
|
512
|
+
else:
|
|
513
|
+
parts.append(str(item))
|
|
514
|
+
return "\n".join(parts)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def format_tool_result(console: Console, data: dict[str, Any]) -> None:
|
|
518
|
+
"""Render MCP tool call results as Rich panels.
|
|
519
|
+
|
|
520
|
+
When all results are errors with the same message (e.g. missing parameter),
|
|
521
|
+
consolidates them into a single error panel instead of repeating N times.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
console: Rich Console instance.
|
|
525
|
+
data: Dict with "results" list and "errors" list.
|
|
526
|
+
"""
|
|
527
|
+
results = data.get("results", [])
|
|
528
|
+
errors = data.get("errors", [])
|
|
529
|
+
|
|
530
|
+
for err in errors:
|
|
531
|
+
console.print(
|
|
532
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
533
|
+
f"{err['message']}"
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
if not results:
|
|
537
|
+
if not errors:
|
|
538
|
+
console.print("No results returned.")
|
|
539
|
+
else:
|
|
540
|
+
console.print("Tool call failed for all projects.")
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
# Detect all-same-error pattern: all results are errors with identical content
|
|
544
|
+
all_errors = all(r.get("isError", False) for r in results)
|
|
545
|
+
if all_errors and len(results) > 1:
|
|
546
|
+
unique_messages = {_extract_result_text(r) for r in results}
|
|
547
|
+
if len(unique_messages) == 1:
|
|
548
|
+
# Consolidate: show one error panel + count
|
|
549
|
+
affected = [r.get("project_alias", "unknown") for r in results]
|
|
550
|
+
content = results[0].get("content", [])
|
|
551
|
+
|
|
552
|
+
lines = [
|
|
553
|
+
f"[bold]Status:[/bold] [bold red]ERROR[/bold red] (same error across {len(results)} projects)",
|
|
554
|
+
]
|
|
555
|
+
for item in content:
|
|
556
|
+
if isinstance(item, str):
|
|
557
|
+
lines.append(item)
|
|
558
|
+
elif isinstance(item, dict):
|
|
559
|
+
lines.append(json.dumps(item, indent=2))
|
|
560
|
+
else:
|
|
561
|
+
lines.append(str(item))
|
|
562
|
+
|
|
563
|
+
lines.append(f"\n[dim]Affected projects: {', '.join(affected)}[/dim]")
|
|
564
|
+
|
|
565
|
+
panel = Panel("\n".join(lines), title="Tool Error", expand=False)
|
|
566
|
+
console.print(panel)
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
# Normal rendering: one panel per result
|
|
570
|
+
for result in results:
|
|
571
|
+
alias = result.get("project_alias", "unknown")
|
|
572
|
+
is_error = result.get("isError", False)
|
|
573
|
+
content = result.get("content", [])
|
|
574
|
+
|
|
575
|
+
status_label = "[bold red]ERROR[/bold red]" if is_error else "[bold green]OK[/bold green]"
|
|
576
|
+
lines: list[str] = [f"[bold]Status:[/bold] {status_label}"]
|
|
577
|
+
|
|
578
|
+
for item in content:
|
|
579
|
+
if isinstance(item, str):
|
|
580
|
+
lines.append(item)
|
|
581
|
+
elif isinstance(item, dict):
|
|
582
|
+
lines.append(json.dumps(item, indent=2))
|
|
583
|
+
else:
|
|
584
|
+
lines.append(str(item))
|
|
585
|
+
|
|
586
|
+
panel = Panel("\n".join(lines), title=f"Result - {alias}", expand=False)
|
|
587
|
+
console.print(panel)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
_SHARING_TYPE_STYLES = {
|
|
591
|
+
"organization": "bold cyan",
|
|
592
|
+
"organization-project": "bold green",
|
|
593
|
+
"data-science": "bold magenta",
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _project_label(alias: str, project_id: int, project_name: str) -> Text:
|
|
598
|
+
"""Create a styled project label. Unknown projects shown as dimmed #id."""
|
|
599
|
+
if alias:
|
|
600
|
+
return Text(alias, style="bold")
|
|
601
|
+
label = Text(f"#{project_id}", style="dim")
|
|
602
|
+
if project_name:
|
|
603
|
+
label.append(f" ({project_name})", style="dim")
|
|
604
|
+
return label
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def format_lineage_table(console: Console, data: dict[str, Any]) -> None:
|
|
608
|
+
"""Render cross-project lineage data as Rich tables.
|
|
609
|
+
|
|
610
|
+
Args:
|
|
611
|
+
console: Rich Console instance.
|
|
612
|
+
data: Dict with "edges", "shared_buckets", "linked_buckets", "summary", "errors".
|
|
613
|
+
"""
|
|
614
|
+
edges = data.get("edges", [])
|
|
615
|
+
shared_buckets = data.get("shared_buckets", [])
|
|
616
|
+
linked_buckets = data.get("linked_buckets", [])
|
|
617
|
+
summary = data.get("summary", {})
|
|
618
|
+
errors = data.get("errors", [])
|
|
619
|
+
|
|
620
|
+
# Show per-project errors
|
|
621
|
+
for err in errors:
|
|
622
|
+
console.print(
|
|
623
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
624
|
+
f"{err['message']}"
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
# Render summary
|
|
628
|
+
_render_lineage_summary(console, summary)
|
|
629
|
+
|
|
630
|
+
# Render edges table if available
|
|
631
|
+
if edges:
|
|
632
|
+
_render_edges_table(console, edges)
|
|
633
|
+
elif shared_buckets or linked_buckets:
|
|
634
|
+
# No edges but buckets exist -- show details
|
|
635
|
+
if shared_buckets:
|
|
636
|
+
_render_shared_buckets_table(console, shared_buckets)
|
|
637
|
+
if linked_buckets:
|
|
638
|
+
_render_linked_buckets_table(console, linked_buckets)
|
|
639
|
+
elif not errors:
|
|
640
|
+
console.print("\nNo bucket sharing detected across queried projects.")
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _render_lineage_summary(console: Console, summary: dict[str, Any]) -> None:
|
|
644
|
+
"""Render a summary line for lineage results."""
|
|
645
|
+
shared = summary.get("total_shared_buckets", 0)
|
|
646
|
+
linked = summary.get("total_linked_buckets", 0)
|
|
647
|
+
edge_count = summary.get("total_edges", 0)
|
|
648
|
+
queried = summary.get("projects_queried", 0)
|
|
649
|
+
|
|
650
|
+
console.print(
|
|
651
|
+
f"\nFound [bold]{shared}[/bold] shared bucket(s) with "
|
|
652
|
+
f"[bold]{edge_count}[/bold] link(s) across "
|
|
653
|
+
f"[bold]{queried}[/bold] project(s)."
|
|
654
|
+
)
|
|
655
|
+
if linked:
|
|
656
|
+
console.print(f" [dim]{linked} linked bucket(s) detected.[/dim]")
|
|
657
|
+
console.print()
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _render_edges_table(console: Console, edges: list[dict[str, Any]]) -> None:
|
|
661
|
+
"""Render the main edges table showing data flow between projects."""
|
|
662
|
+
table = Table(title="Data Flow Edges")
|
|
663
|
+
table.add_column("Source Project", style="bold magenta")
|
|
664
|
+
table.add_column("Source Bucket", style="cyan")
|
|
665
|
+
table.add_column("Sharing Type", style="dim")
|
|
666
|
+
table.add_column("Target Project", style="bold magenta")
|
|
667
|
+
table.add_column("Target Bucket", style="cyan")
|
|
668
|
+
|
|
669
|
+
for edge in edges:
|
|
670
|
+
source_label = _project_label(
|
|
671
|
+
edge.get("source_project_alias", ""),
|
|
672
|
+
edge.get("source_project_id", 0),
|
|
673
|
+
edge.get("source_project_name", ""),
|
|
674
|
+
)
|
|
675
|
+
target_label = _project_label(
|
|
676
|
+
edge.get("target_project_alias", ""),
|
|
677
|
+
edge.get("target_project_id", 0),
|
|
678
|
+
edge.get("target_project_name", ""),
|
|
679
|
+
)
|
|
680
|
+
sharing_type = edge.get("sharing_type", "")
|
|
681
|
+
style = _SHARING_TYPE_STYLES.get(sharing_type, "")
|
|
682
|
+
sharing_display = Text(sharing_type, style=style) if style else Text(sharing_type)
|
|
683
|
+
|
|
684
|
+
table.add_row(
|
|
685
|
+
source_label,
|
|
686
|
+
edge.get("source_bucket_id", ""),
|
|
687
|
+
sharing_display,
|
|
688
|
+
target_label,
|
|
689
|
+
edge.get("target_bucket_id", ""),
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
console.print(table)
|
|
693
|
+
console.print()
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _render_shared_buckets_table(console: Console, shared_buckets: list[dict[str, Any]]) -> None:
|
|
697
|
+
"""Render a table of shared buckets (no linked targets found)."""
|
|
698
|
+
table = Table(title="Shared Buckets (no linked targets found)")
|
|
699
|
+
table.add_column("Project", style="bold magenta")
|
|
700
|
+
table.add_column("Bucket ID", style="cyan")
|
|
701
|
+
table.add_column("Bucket Name")
|
|
702
|
+
table.add_column("Sharing Type", style="dim")
|
|
703
|
+
|
|
704
|
+
for sb in shared_buckets:
|
|
705
|
+
table.add_row(
|
|
706
|
+
sb.get("project_alias", ""),
|
|
707
|
+
sb.get("bucket_id", ""),
|
|
708
|
+
sb.get("bucket_name", ""),
|
|
709
|
+
sb.get("sharing_type", ""),
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
console.print(table)
|
|
713
|
+
console.print()
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _render_linked_buckets_table(console: Console, linked_buckets: list[dict[str, Any]]) -> None:
|
|
717
|
+
"""Render a table of linked buckets (incoming links)."""
|
|
718
|
+
table = Table(title="Linked Buckets (incoming)")
|
|
719
|
+
table.add_column("Project", style="bold magenta")
|
|
720
|
+
table.add_column("Bucket ID", style="cyan")
|
|
721
|
+
table.add_column("Source Bucket", style="dim")
|
|
722
|
+
table.add_column("Source Project", style="dim")
|
|
723
|
+
table.add_column("Read-only", justify="center")
|
|
724
|
+
|
|
725
|
+
for lb in linked_buckets:
|
|
726
|
+
readonly = "[green]yes[/green]" if lb.get("is_readonly") else "[dim]no[/dim]"
|
|
727
|
+
table.add_row(
|
|
728
|
+
lb.get("project_alias", ""),
|
|
729
|
+
lb.get("bucket_id", ""),
|
|
730
|
+
lb.get("source_bucket_id", ""),
|
|
731
|
+
lb.get("source_project_name", ""),
|
|
732
|
+
readonly,
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
console.print(table)
|
|
736
|
+
console.print()
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def format_branches_table(console: Console, data: dict[str, Any]) -> None:
|
|
740
|
+
"""Render a Rich table of development branches grouped by project alias.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
console: Rich Console instance.
|
|
744
|
+
data: Dict with "branches" (list of branch dicts) and "errors" (list of error dicts).
|
|
745
|
+
"""
|
|
746
|
+
branches = data.get("branches", [])
|
|
747
|
+
errors = data.get("errors", [])
|
|
748
|
+
|
|
749
|
+
for err in errors:
|
|
750
|
+
console.print(
|
|
751
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
752
|
+
f"{err['message']}"
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if not branches:
|
|
756
|
+
if not errors:
|
|
757
|
+
console.print(
|
|
758
|
+
"No branches found. Use [bold]kbagent project add[/bold] to connect a project first."
|
|
759
|
+
)
|
|
760
|
+
else:
|
|
761
|
+
console.print("No branches retrieved (all projects failed).")
|
|
762
|
+
return
|
|
763
|
+
|
|
764
|
+
active_branches = data.get("active_branches", {})
|
|
765
|
+
|
|
766
|
+
table = Table(title="Development Branches")
|
|
767
|
+
table.add_column("Project", style="bold magenta")
|
|
768
|
+
table.add_column("Branch ID", justify="right")
|
|
769
|
+
table.add_column("Name", style="bold cyan")
|
|
770
|
+
table.add_column("Default", justify="center")
|
|
771
|
+
table.add_column("Active", justify="center")
|
|
772
|
+
table.add_column("Description", style="dim", max_width=40)
|
|
773
|
+
table.add_column("Created", style="dim")
|
|
774
|
+
|
|
775
|
+
prev_alias = None
|
|
776
|
+
for branch in branches:
|
|
777
|
+
alias = branch.get("project_alias", "unknown")
|
|
778
|
+
is_default = branch.get("isDefault", False)
|
|
779
|
+
default_display = "[green]yes[/green]" if is_default else "[dim]no[/dim]"
|
|
780
|
+
|
|
781
|
+
branch_id = branch.get("id")
|
|
782
|
+
active_id = active_branches.get(alias)
|
|
783
|
+
# Compare as int to handle potential type mismatch from API
|
|
784
|
+
is_active = (
|
|
785
|
+
branch_id is not None and active_id is not None and int(branch_id) == int(active_id)
|
|
786
|
+
)
|
|
787
|
+
active_display = "[bold green]>>>[/bold green]" if is_active else ""
|
|
788
|
+
|
|
789
|
+
display_alias = alias if alias != prev_alias else ""
|
|
790
|
+
prev_alias = alias
|
|
791
|
+
|
|
792
|
+
table.add_row(
|
|
793
|
+
display_alias,
|
|
794
|
+
str(branch.get("id", "")),
|
|
795
|
+
branch.get("name", ""),
|
|
796
|
+
default_display,
|
|
797
|
+
active_display,
|
|
798
|
+
branch.get("description", ""),
|
|
799
|
+
branch.get("created", ""),
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
console.print(table)
|
|
803
|
+
console.print()
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def format_branch_metadata_table(console: Console, data: dict[str, Any]) -> None:
|
|
807
|
+
"""Render branch metadata entries as a Rich table.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
console: Rich Console instance.
|
|
811
|
+
data: Dict with project_alias, branch_id, and metadata (list of entries).
|
|
812
|
+
"""
|
|
813
|
+
entries = data.get("metadata", [])
|
|
814
|
+
alias = data.get("project_alias", "unknown")
|
|
815
|
+
branch_id = data.get("branch_id", "default")
|
|
816
|
+
|
|
817
|
+
if not entries:
|
|
818
|
+
console.print(
|
|
819
|
+
f"No metadata on branch [bold]{branch_id}[/bold] of project "
|
|
820
|
+
f"[bold magenta]{alias}[/bold magenta]."
|
|
821
|
+
)
|
|
822
|
+
return
|
|
823
|
+
|
|
824
|
+
table = Table(title=f"Branch Metadata - project: {alias} - branch: {branch_id}")
|
|
825
|
+
table.add_column("ID", justify="right", style="dim")
|
|
826
|
+
table.add_column("Key", style="bold cyan")
|
|
827
|
+
table.add_column("Value", max_width=60)
|
|
828
|
+
table.add_column("Provider", style="dim")
|
|
829
|
+
table.add_column("Timestamp", style="dim")
|
|
830
|
+
|
|
831
|
+
for entry in entries:
|
|
832
|
+
value = str(entry.get("value", ""))
|
|
833
|
+
if "\n" in value:
|
|
834
|
+
first_line = value.splitlines()[0]
|
|
835
|
+
value = f"{first_line} [dim](+{len(value.splitlines()) - 1} more lines)[/dim]"
|
|
836
|
+
table.add_row(
|
|
837
|
+
str(entry.get("id", "")),
|
|
838
|
+
str(entry.get("key", "")),
|
|
839
|
+
value,
|
|
840
|
+
str(entry.get("provider", "")),
|
|
841
|
+
str(entry.get("timestamp", "")),
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
console.print(table)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def format_doctor_panel(console: Console, data: dict[str, Any]) -> None:
|
|
848
|
+
"""Render doctor check results as a Rich panel with colored status indicators.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
console: Rich Console instance.
|
|
852
|
+
data: Dict with "checks" list and "summary" dict from DoctorService.
|
|
853
|
+
"""
|
|
854
|
+
status_icons = {
|
|
855
|
+
"pass": "[bold green]PASS[/bold green]",
|
|
856
|
+
"fail": "[bold red]FAIL[/bold red]",
|
|
857
|
+
"warn": "[bold yellow]WARN[/bold yellow]",
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
checks = data.get("checks", [])
|
|
861
|
+
lines = [
|
|
862
|
+
f" {status_icons.get(c['status'], '[dim]SKIP[/dim]')} {c['name']}: {c['message']}"
|
|
863
|
+
for c in checks
|
|
864
|
+
]
|
|
865
|
+
|
|
866
|
+
summary = data.get("summary", {})
|
|
867
|
+
parts = [f"{summary.get('total', 0)} checks"]
|
|
868
|
+
if summary.get("passed"):
|
|
869
|
+
parts.append(f"[green]{summary['passed']} passed[/green]")
|
|
870
|
+
if summary.get("failed"):
|
|
871
|
+
parts.append(f"[red]{summary['failed']} failed[/red]")
|
|
872
|
+
if summary.get("warnings"):
|
|
873
|
+
parts.append(f"[yellow]{summary['warnings']} warnings[/yellow]")
|
|
874
|
+
|
|
875
|
+
lines.append("")
|
|
876
|
+
lines.append(f" Summary: {', '.join(parts)}")
|
|
877
|
+
|
|
878
|
+
console.print(Panel("\n".join(lines), title="kbagent doctor", expand=False))
|
|
879
|
+
|
|
880
|
+
|
|
881
|
+
def format_workspaces_table(console: Console, data: dict[str, Any]) -> None:
|
|
882
|
+
"""Render a Rich table of workspaces grouped by project alias.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
console: Rich Console instance.
|
|
886
|
+
data: Dict with "workspaces" and "errors" lists.
|
|
887
|
+
"""
|
|
888
|
+
workspaces = data.get("workspaces", [])
|
|
889
|
+
errors = data.get("errors", [])
|
|
890
|
+
|
|
891
|
+
for err in errors:
|
|
892
|
+
console.print(
|
|
893
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
894
|
+
f"{err['message']}"
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
if not workspaces:
|
|
898
|
+
if not errors:
|
|
899
|
+
console.print(
|
|
900
|
+
"No workspaces found. Use [bold]kbagent workspace create[/bold] to create one."
|
|
901
|
+
)
|
|
902
|
+
else:
|
|
903
|
+
console.print("No workspaces retrieved (all projects failed).")
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
table = Table(title="Workspaces")
|
|
907
|
+
table.add_column("Project", style="bold magenta")
|
|
908
|
+
table.add_column("ID", justify="right")
|
|
909
|
+
table.add_column("Name", style="bold cyan")
|
|
910
|
+
table.add_column("Backend")
|
|
911
|
+
table.add_column("Schema")
|
|
912
|
+
# Login type + RO + QS surface the three Query-Service compatibility signals
|
|
913
|
+
# that were previously invisible to data-app developers (issue #304).
|
|
914
|
+
# Width-clamped because ``snowflake-service-keypair`` is the longest known
|
|
915
|
+
# value; clamp leaves room for the wider table on standard terminals.
|
|
916
|
+
table.add_column("Login Type", max_width=24)
|
|
917
|
+
table.add_column("RO", justify="center")
|
|
918
|
+
table.add_column("QS", justify="center")
|
|
919
|
+
table.add_column("Created", style="dim")
|
|
920
|
+
|
|
921
|
+
prev_alias = None
|
|
922
|
+
for ws in workspaces:
|
|
923
|
+
alias = ws.get("project_alias", "unknown")
|
|
924
|
+
display_alias = alias if alias != prev_alias else ""
|
|
925
|
+
prev_alias = alias
|
|
926
|
+
|
|
927
|
+
login_type = ws.get("login_type", "") or "[dim]?[/dim]"
|
|
928
|
+
ro_cell = "[green]yes[/green]" if ws.get("read_only") else "no"
|
|
929
|
+
if ws.get("qs_compatible") is True:
|
|
930
|
+
qs_cell = "[green]yes[/green]"
|
|
931
|
+
elif ws.get("login_type"):
|
|
932
|
+
# We have a loginType but it is not on the confirmed whitelist.
|
|
933
|
+
# Yellow rather than red: the policy varies per stack (see
|
|
934
|
+
# snowflake-legacy-service in constants.py) and we do not want to
|
|
935
|
+
# actively discourage a workspace that might in fact work.
|
|
936
|
+
qs_cell = "[yellow]?[/yellow]"
|
|
937
|
+
else:
|
|
938
|
+
qs_cell = "[dim]?[/dim]"
|
|
939
|
+
|
|
940
|
+
table.add_row(
|
|
941
|
+
display_alias,
|
|
942
|
+
str(ws.get("workspace_id", ws.get("id", ""))),
|
|
943
|
+
ws.get("name", ""),
|
|
944
|
+
ws.get("backend", ""),
|
|
945
|
+
ws.get("schema", ""),
|
|
946
|
+
login_type,
|
|
947
|
+
ro_cell,
|
|
948
|
+
qs_cell,
|
|
949
|
+
ws.get("created", ""),
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
console.print(table)
|
|
953
|
+
console.print(
|
|
954
|
+
"[dim]RO = read-only storage access. QS = Query Service compatible "
|
|
955
|
+
"(yes = confirmed whitelist, ? = loginType not on confirmed list, "
|
|
956
|
+
"may still work). See `kbagent workspace list --qs-compatible` to "
|
|
957
|
+
"filter to data-app-ready workspaces.[/dim]"
|
|
958
|
+
)
|
|
959
|
+
console.print()
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
def format_query_results(console: Console, data: dict[str, Any]) -> None:
|
|
963
|
+
"""Render SQL query results.
|
|
964
|
+
|
|
965
|
+
Shows query status plus, per statement, a Rich table built from the
|
|
966
|
+
structured ``columns``/``rows`` returned by the fast inline path. Falls back
|
|
967
|
+
to a CSV preview when only ``csv_data`` is present (the ``--full`` export
|
|
968
|
+
path, which returns a CSV string without structured columns).
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
console: Rich Console instance.
|
|
972
|
+
data: Dict with query execution results.
|
|
973
|
+
"""
|
|
974
|
+
alias = data.get("project_alias", "unknown")
|
|
975
|
+
workspace_id = data.get("workspace_id", "")
|
|
976
|
+
status = data.get("status", "unknown")
|
|
977
|
+
|
|
978
|
+
console.print(
|
|
979
|
+
Panel(
|
|
980
|
+
f"[bold]Project:[/bold] {alias}\n"
|
|
981
|
+
f"[bold]Workspace:[/bold] {workspace_id}\n"
|
|
982
|
+
f"[bold]Status:[/bold] {status}",
|
|
983
|
+
title=f"Query Results - Workspace {workspace_id}",
|
|
984
|
+
expand=False,
|
|
985
|
+
)
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
statements = data.get("statements", [])
|
|
989
|
+
for i, stmt in enumerate(statements):
|
|
990
|
+
console.print(
|
|
991
|
+
f"\n[bold]Statement {i + 1}:[/bold] "
|
|
992
|
+
f"{stmt.get('status', 'unknown')} ・ {stmt.get('rows_affected', 0)} rows"
|
|
993
|
+
)
|
|
994
|
+
_render_statement_result(console, stmt)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
def _render_statement_result(console: Console, stmt: dict[str, Any]) -> None:
|
|
998
|
+
"""Render a single statement's result set (structured table or CSV preview)."""
|
|
999
|
+
columns = stmt.get("columns")
|
|
1000
|
+
rows = stmt.get("rows")
|
|
1001
|
+
if columns and rows is not None:
|
|
1002
|
+
table = Table(show_lines=False)
|
|
1003
|
+
for col in columns:
|
|
1004
|
+
table.add_column(str(col.get("name", "")))
|
|
1005
|
+
for row in rows:
|
|
1006
|
+
table.add_row(*["" if value is None else str(value) for value in row])
|
|
1007
|
+
console.print(table)
|
|
1008
|
+
if stmt.get("truncated"):
|
|
1009
|
+
total = stmt.get("total_rows")
|
|
1010
|
+
shown = stmt.get("row_count", len(rows))
|
|
1011
|
+
suffix = f" of {total}" if total is not None else ""
|
|
1012
|
+
console.print(
|
|
1013
|
+
f" [dim]Showing first {shown}{suffix} rows. "
|
|
1014
|
+
f"Use --full for the complete result set.[/dim]"
|
|
1015
|
+
)
|
|
1016
|
+
return
|
|
1017
|
+
|
|
1018
|
+
# Fallback: --full export path returns a CSV string with no structured columns.
|
|
1019
|
+
csv_data = stmt.get("csv_data", "")
|
|
1020
|
+
if not csv_data:
|
|
1021
|
+
return
|
|
1022
|
+
csv_lines = csv_data.strip().split("\n")
|
|
1023
|
+
preview_count = min(len(csv_lines), 11) # header + 10 rows
|
|
1024
|
+
console.print(" [bold]Results:[/bold]")
|
|
1025
|
+
for csv_line in csv_lines[:preview_count]:
|
|
1026
|
+
console.print(f" {csv_line}")
|
|
1027
|
+
if len(csv_lines) > preview_count:
|
|
1028
|
+
console.print(f" ... ({len(csv_lines) - preview_count} more rows)")
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def format_search_results(console: Console, data: dict[str, Any]) -> None:
|
|
1032
|
+
"""Render search results as a Rich table with match locations.
|
|
1033
|
+
|
|
1034
|
+
Args:
|
|
1035
|
+
console: Rich Console instance.
|
|
1036
|
+
data: Dict with "matches", "errors", and "stats".
|
|
1037
|
+
"""
|
|
1038
|
+
matches = data.get("matches", [])
|
|
1039
|
+
errors = data.get("errors", [])
|
|
1040
|
+
stats = data.get("stats", {})
|
|
1041
|
+
|
|
1042
|
+
for err in errors:
|
|
1043
|
+
console.print(
|
|
1044
|
+
f"[bold yellow]Warning:[/bold yellow] Project [bold]{err['project_alias']}[/bold]: "
|
|
1045
|
+
f"{err['message']}"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
if not matches:
|
|
1049
|
+
console.print(
|
|
1050
|
+
f"No matches found. Searched {stats.get('configs_searched', 0)} "
|
|
1051
|
+
f"configurations across {stats.get('projects_searched', 0)} project(s)."
|
|
1052
|
+
)
|
|
1053
|
+
return
|
|
1054
|
+
|
|
1055
|
+
table = Table(title="Search Results")
|
|
1056
|
+
table.add_column("Project", style="bold cyan")
|
|
1057
|
+
table.add_column("Component", style="dim")
|
|
1058
|
+
table.add_column("Config ID", justify="right")
|
|
1059
|
+
table.add_column("Config Name")
|
|
1060
|
+
table.add_column("Hits", justify="right", style="bold yellow")
|
|
1061
|
+
table.add_column("Match Locations", style="dim", max_width=60)
|
|
1062
|
+
|
|
1063
|
+
for match in matches:
|
|
1064
|
+
locations = match.get("match_locations", [])
|
|
1065
|
+
# Show first 3 locations, truncate the rest
|
|
1066
|
+
display_locations = ", ".join(locations[:3])
|
|
1067
|
+
if len(locations) > 3:
|
|
1068
|
+
display_locations += f", ... (+{len(locations) - 3})"
|
|
1069
|
+
|
|
1070
|
+
table.add_row(
|
|
1071
|
+
match["project_alias"],
|
|
1072
|
+
match["component_id"],
|
|
1073
|
+
match["config_id"],
|
|
1074
|
+
match["config_name"],
|
|
1075
|
+
str(match.get("match_count", len(locations))),
|
|
1076
|
+
display_locations,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
console.print(table)
|
|
1080
|
+
console.print(
|
|
1081
|
+
f"\n[bold]{stats.get('matches_found', 0)}[/bold] matching config(s) "
|
|
1082
|
+
f"in {stats.get('configs_searched', 0)} searched "
|
|
1083
|
+
f"across {stats.get('projects_searched', 0)} project(s)."
|
|
1084
|
+
)
|