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,1192 @@
|
|
|
1
|
+
"""Project management commands - add, list, remove, edit, status.
|
|
2
|
+
|
|
3
|
+
Thin CLI layer: parses arguments, calls ProjectService, formats output.
|
|
4
|
+
No business logic belongs here.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from ..constants import (
|
|
17
|
+
DEFAULT_INVITE_WORKERS,
|
|
18
|
+
DEFAULT_STACK_URL,
|
|
19
|
+
DEFAULT_TOKEN_DESCRIPTION,
|
|
20
|
+
ENV_KBC_STORAGE_API_URL,
|
|
21
|
+
ENV_KBC_TOKEN,
|
|
22
|
+
PROJECT_ROLES,
|
|
23
|
+
)
|
|
24
|
+
from ..errors import ConfigError, ErrorCode, KeboolaApiError
|
|
25
|
+
from ._helpers import (
|
|
26
|
+
check_cli_permission,
|
|
27
|
+
get_formatter,
|
|
28
|
+
get_service,
|
|
29
|
+
map_error_to_exit_code,
|
|
30
|
+
resolve_manage_token,
|
|
31
|
+
)
|
|
32
|
+
from ._metadata_input import resolve_text_input
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProjectRole(StrEnum):
|
|
36
|
+
"""Project membership role."""
|
|
37
|
+
|
|
38
|
+
admin = "admin"
|
|
39
|
+
guest = "guest"
|
|
40
|
+
readOnly = "readOnly"
|
|
41
|
+
share = "share"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
project_app = typer.Typer(help="Manage connected Keboola projects")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@project_app.callback(invoke_without_command=True)
|
|
48
|
+
def _project_permission_check(ctx: typer.Context) -> None:
|
|
49
|
+
check_cli_permission(ctx, "project")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _format_project_table(console: Console, projects: list[dict[str, Any]]) -> None:
|
|
53
|
+
"""Render a Rich table of projects for human output."""
|
|
54
|
+
if not projects:
|
|
55
|
+
console.print("No projects configured. Use [bold]kbagent project add[/bold] to add one.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
table = Table(title="Connected Projects")
|
|
59
|
+
table.add_column("Alias", style="bold cyan")
|
|
60
|
+
table.add_column("Project Name")
|
|
61
|
+
table.add_column("Project ID", justify="right")
|
|
62
|
+
table.add_column("Stack URL")
|
|
63
|
+
table.add_column("Token", style="dim")
|
|
64
|
+
table.add_column("Default", justify="center")
|
|
65
|
+
table.add_column("Branch", justify="center")
|
|
66
|
+
|
|
67
|
+
for p in projects:
|
|
68
|
+
default_marker = "*" if p.get("is_default") else ""
|
|
69
|
+
branch_id = p.get("active_branch_id")
|
|
70
|
+
branch_display = str(branch_id) if branch_id is not None else "[dim]main[/dim]"
|
|
71
|
+
table.add_row(
|
|
72
|
+
p["alias"],
|
|
73
|
+
p.get("project_name", ""),
|
|
74
|
+
str(p.get("project_id", "")),
|
|
75
|
+
p["stack_url"],
|
|
76
|
+
p["token"],
|
|
77
|
+
default_marker,
|
|
78
|
+
branch_display,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
console.print(table)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _format_status_table(console: Console, statuses: list[dict[str, Any]]) -> None:
|
|
85
|
+
"""Render a Rich table of project connectivity statuses."""
|
|
86
|
+
if not statuses:
|
|
87
|
+
console.print("No projects configured.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
table = Table(title="Project Status")
|
|
91
|
+
table.add_column("Alias", style="bold cyan")
|
|
92
|
+
table.add_column("Status")
|
|
93
|
+
table.add_column("Response Time", justify="right")
|
|
94
|
+
table.add_column("Project Name")
|
|
95
|
+
table.add_column("Stack URL")
|
|
96
|
+
table.add_column("Branch", justify="center")
|
|
97
|
+
|
|
98
|
+
for s in statuses:
|
|
99
|
+
if s["status"] == "ok":
|
|
100
|
+
status_str = "[bold green]OK[/bold green]"
|
|
101
|
+
else:
|
|
102
|
+
status_str = f"[bold red]ERROR[/bold red]: {s.get('error', 'Unknown')}"
|
|
103
|
+
response_time = f"{s.get('response_time_ms', 0)}ms"
|
|
104
|
+
branch_id = s.get("active_branch_id")
|
|
105
|
+
branch_display = str(branch_id) if branch_id is not None else "[dim]main[/dim]"
|
|
106
|
+
table.add_row(
|
|
107
|
+
s["alias"],
|
|
108
|
+
status_str,
|
|
109
|
+
response_time,
|
|
110
|
+
s.get("project_name", ""),
|
|
111
|
+
s["stack_url"],
|
|
112
|
+
branch_display,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_token(token: str | None) -> str:
|
|
119
|
+
"""Resolve the Storage API token, falling back to interactive prompt.
|
|
120
|
+
|
|
121
|
+
Token resolution order (Typer handles steps 1-2 automatically via envvar):
|
|
122
|
+
1. --token CLI argument
|
|
123
|
+
2. KBC_TOKEN env var (handled by Typer's envvar parameter)
|
|
124
|
+
3. Interactive prompt with hidden input (if TTY)
|
|
125
|
+
4. Error if none available
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
token: Token from --token or KBC_TOKEN env var (resolved by Typer), or None.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The Storage API token.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
typer.Exit: If no token can be resolved.
|
|
135
|
+
"""
|
|
136
|
+
if token:
|
|
137
|
+
return token
|
|
138
|
+
|
|
139
|
+
is_tty = hasattr(sys.stdin, "isatty") and sys.stdin.isatty()
|
|
140
|
+
if is_tty:
|
|
141
|
+
return typer.prompt("Storage API token", hide_input=True)
|
|
142
|
+
|
|
143
|
+
typer.echo(
|
|
144
|
+
f"Error: No token available. Pass --token, set {ENV_KBC_TOKEN} env var, "
|
|
145
|
+
"or run interactively.",
|
|
146
|
+
err=True,
|
|
147
|
+
)
|
|
148
|
+
raise typer.Exit(code=2)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@project_app.command("add")
|
|
152
|
+
def project_add(
|
|
153
|
+
ctx: typer.Context,
|
|
154
|
+
alias: str = typer.Option(..., "--project", help="Human-friendly name for this project"),
|
|
155
|
+
url: str = typer.Option(
|
|
156
|
+
DEFAULT_STACK_URL,
|
|
157
|
+
help="Keboola stack URL",
|
|
158
|
+
envvar=ENV_KBC_STORAGE_API_URL,
|
|
159
|
+
),
|
|
160
|
+
token: str | None = typer.Option(
|
|
161
|
+
None,
|
|
162
|
+
help="Storage API token (also via KBC_TOKEN env var)",
|
|
163
|
+
envvar=ENV_KBC_TOKEN,
|
|
164
|
+
),
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Add a new Keboola project connection.
|
|
167
|
+
|
|
168
|
+
Token is read from --token, KBC_TOKEN env var, or prompted interactively.
|
|
169
|
+
"""
|
|
170
|
+
formatter = get_formatter(ctx)
|
|
171
|
+
service = get_service(ctx, "project_service")
|
|
172
|
+
resolved_token = _resolve_token(token)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
result = service.add_project(alias=alias, stack_url=url, token=resolved_token)
|
|
176
|
+
formatter.output(
|
|
177
|
+
result,
|
|
178
|
+
lambda c, d: c.print(
|
|
179
|
+
f"[bold green]Success:[/bold green] Project [bold]{d['alias']}[/bold] added "
|
|
180
|
+
f"(project: {d['project_name']}, id: {d['project_id']})"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
except KeboolaApiError as exc:
|
|
184
|
+
exit_code = map_error_to_exit_code(exc)
|
|
185
|
+
formatter.error(
|
|
186
|
+
message=exc.message,
|
|
187
|
+
error_code=exc.error_code,
|
|
188
|
+
retryable=exc.retryable,
|
|
189
|
+
)
|
|
190
|
+
raise typer.Exit(code=exit_code) from None
|
|
191
|
+
except ConfigError as exc:
|
|
192
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
193
|
+
raise typer.Exit(code=5) from None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@project_app.command("list")
|
|
197
|
+
def project_list(ctx: typer.Context) -> None:
|
|
198
|
+
"""List all connected Keboola projects."""
|
|
199
|
+
formatter = get_formatter(ctx)
|
|
200
|
+
service = get_service(ctx, "project_service")
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
projects = service.list_projects()
|
|
204
|
+
formatter.output(projects, _format_project_table)
|
|
205
|
+
except ConfigError as exc:
|
|
206
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
207
|
+
raise typer.Exit(code=5) from None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@project_app.command("remove")
|
|
211
|
+
def project_remove(
|
|
212
|
+
ctx: typer.Context,
|
|
213
|
+
alias: str = typer.Option(..., "--project", help="Alias of the project to remove"),
|
|
214
|
+
) -> None:
|
|
215
|
+
"""Remove a Keboola project connection."""
|
|
216
|
+
formatter = get_formatter(ctx)
|
|
217
|
+
service = get_service(ctx, "project_service")
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
result = service.remove_project(alias=alias)
|
|
221
|
+
formatter.output(
|
|
222
|
+
result, lambda c, d: c.print(f"[bold green]Success:[/bold green] {d['message']}")
|
|
223
|
+
)
|
|
224
|
+
except ConfigError as exc:
|
|
225
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
226
|
+
raise typer.Exit(code=5) from None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@project_app.command("edit")
|
|
230
|
+
def project_edit(
|
|
231
|
+
ctx: typer.Context,
|
|
232
|
+
alias: str = typer.Option(..., "--project", help="Alias of the project to edit"),
|
|
233
|
+
url: str | None = typer.Option(None, help="New Keboola stack URL"),
|
|
234
|
+
token: str | None = typer.Option(
|
|
235
|
+
None,
|
|
236
|
+
help="New Storage API token",
|
|
237
|
+
),
|
|
238
|
+
new_alias: str | None = typer.Option(
|
|
239
|
+
None,
|
|
240
|
+
"--new-alias",
|
|
241
|
+
help=(
|
|
242
|
+
"Rename the project alias. Updates the config.json projects key "
|
|
243
|
+
"AND the default_project field if it matched. Renames the nested "
|
|
244
|
+
"sync directory <cwd>/<old-alias>/ when present (with -2-suffix "
|
|
245
|
+
"collision handling). Lineage cache (if any) is NOT auto-updated; "
|
|
246
|
+
"rebuild with 'kbagent lineage build' after the rename."
|
|
247
|
+
),
|
|
248
|
+
),
|
|
249
|
+
dry_run: bool = typer.Option(
|
|
250
|
+
False,
|
|
251
|
+
"--dry-run",
|
|
252
|
+
help=(
|
|
253
|
+
"Preview the edit without mutating state. Validates --new-alias, "
|
|
254
|
+
"detects collision against existing projects, predicts the disk-"
|
|
255
|
+
"rename method (git_mv vs shutil_move), and surfaces the lineage-"
|
|
256
|
+
"cache warning if any -- all read-only. Errors (collision, "
|
|
257
|
+
"invalid format) raise the same exit codes as the live path. No "
|
|
258
|
+
"API call is made for --token in dry-run mode."
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
) -> None:
|
|
262
|
+
"""Edit an existing Keboola project connection.
|
|
263
|
+
|
|
264
|
+
If --token is provided, the token is re-verified against the API.
|
|
265
|
+
Combined with --new-alias, the rename is applied first and any
|
|
266
|
+
--url / --token mutation lands on the new alias key. Pass --dry-run
|
|
267
|
+
to preview without mutating state.
|
|
268
|
+
"""
|
|
269
|
+
formatter = get_formatter(ctx)
|
|
270
|
+
service = get_service(ctx, "project_service")
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
result = service.edit_project(
|
|
274
|
+
alias=alias,
|
|
275
|
+
stack_url=url,
|
|
276
|
+
token=token,
|
|
277
|
+
new_alias=new_alias,
|
|
278
|
+
dry_run=dry_run,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def _human(c: Console, d: dict) -> None:
|
|
282
|
+
if d.get("dry_run"):
|
|
283
|
+
planned = d.get("planned", {})
|
|
284
|
+
p_new = planned.get("new_alias")
|
|
285
|
+
if p_new:
|
|
286
|
+
c.print(
|
|
287
|
+
f"[bold yellow]DRY RUN:[/bold yellow] Project "
|
|
288
|
+
f"[bold]{d['alias']}[/bold] would be renamed to "
|
|
289
|
+
f"[bold]{p_new}[/bold]. No state mutated."
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
c.print(
|
|
293
|
+
f"[bold yellow]DRY RUN:[/bold yellow] Project "
|
|
294
|
+
f"[bold]{d['alias']}[/bold] would be updated. "
|
|
295
|
+
"No state mutated."
|
|
296
|
+
)
|
|
297
|
+
return
|
|
298
|
+
if "old_alias" in d:
|
|
299
|
+
c.print(
|
|
300
|
+
f"[bold green]Success:[/bold green] Project "
|
|
301
|
+
f"[bold]{d['old_alias']}[/bold] renamed to "
|
|
302
|
+
f"[bold]{d['alias']}[/bold]."
|
|
303
|
+
)
|
|
304
|
+
else:
|
|
305
|
+
c.print(
|
|
306
|
+
f"[bold green]Success:[/bold green] Project [bold]{d['alias']}[/bold] updated."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
formatter.output(result, _human)
|
|
310
|
+
except KeboolaApiError as exc:
|
|
311
|
+
exit_code = map_error_to_exit_code(exc)
|
|
312
|
+
formatter.error(
|
|
313
|
+
message=exc.message,
|
|
314
|
+
error_code=exc.error_code,
|
|
315
|
+
retryable=exc.retryable,
|
|
316
|
+
)
|
|
317
|
+
raise typer.Exit(code=exit_code) from None
|
|
318
|
+
except ConfigError as exc:
|
|
319
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
320
|
+
raise typer.Exit(code=5) from None
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _format_refresh_result(console: Console, data: dict) -> None:
|
|
324
|
+
"""Render token refresh results as Rich tables with summary."""
|
|
325
|
+
dry_run = data.get("dry_run", False)
|
|
326
|
+
mode_label = "[bold yellow]DRY RUN[/bold yellow] " if dry_run else ""
|
|
327
|
+
console.print(f"\n{mode_label}Token Refresh\n")
|
|
328
|
+
|
|
329
|
+
# Refreshed projects
|
|
330
|
+
refreshed = data.get("projects_refreshed", [])
|
|
331
|
+
if refreshed:
|
|
332
|
+
action_label = "Projects to Refresh" if dry_run else "Projects Refreshed"
|
|
333
|
+
table = Table(title=action_label)
|
|
334
|
+
table.add_column("Alias", style="bold cyan")
|
|
335
|
+
table.add_column("Project ID", justify="right")
|
|
336
|
+
table.add_column("Project Name")
|
|
337
|
+
if not dry_run:
|
|
338
|
+
table.add_column("Token", style="dim")
|
|
339
|
+
|
|
340
|
+
for p in refreshed:
|
|
341
|
+
if dry_run:
|
|
342
|
+
table.add_row(p["alias"], str(p["project_id"]), p["project_name"])
|
|
343
|
+
else:
|
|
344
|
+
table.add_row(
|
|
345
|
+
p["alias"], str(p["project_id"]), p["project_name"], p.get("token", "")
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
console.print(table)
|
|
349
|
+
console.print()
|
|
350
|
+
|
|
351
|
+
# Valid projects (tokens that were fine)
|
|
352
|
+
valid = data.get("projects_valid", [])
|
|
353
|
+
if valid:
|
|
354
|
+
table = Table(title="Projects Valid")
|
|
355
|
+
table.add_column("Alias", style="bold cyan")
|
|
356
|
+
table.add_column("Project ID", justify="right")
|
|
357
|
+
table.add_column("Project Name")
|
|
358
|
+
|
|
359
|
+
for p in valid:
|
|
360
|
+
table.add_row(p["alias"], str(p["project_id"]), p["project_name"])
|
|
361
|
+
|
|
362
|
+
console.print(table)
|
|
363
|
+
console.print()
|
|
364
|
+
|
|
365
|
+
# Skipped projects
|
|
366
|
+
skipped = data.get("projects_skipped", [])
|
|
367
|
+
if skipped:
|
|
368
|
+
table = Table(title="Projects Skipped")
|
|
369
|
+
table.add_column("Alias", style="bold cyan")
|
|
370
|
+
table.add_column("Reason", style="dim")
|
|
371
|
+
|
|
372
|
+
for p in skipped:
|
|
373
|
+
table.add_row(p["alias"], p["reason"])
|
|
374
|
+
|
|
375
|
+
console.print(table)
|
|
376
|
+
console.print()
|
|
377
|
+
|
|
378
|
+
# Failed projects
|
|
379
|
+
failed = data.get("projects_failed", [])
|
|
380
|
+
if failed:
|
|
381
|
+
table = Table(title="Projects Failed")
|
|
382
|
+
table.add_column("Alias", style="bold cyan")
|
|
383
|
+
table.add_column("Error", style="bold red")
|
|
384
|
+
|
|
385
|
+
for p in failed:
|
|
386
|
+
table.add_row(p["alias"], p["error"])
|
|
387
|
+
|
|
388
|
+
console.print(table)
|
|
389
|
+
console.print()
|
|
390
|
+
|
|
391
|
+
# Summary line
|
|
392
|
+
summary_parts = []
|
|
393
|
+
if refreshed:
|
|
394
|
+
verb = "to refresh" if dry_run else "refreshed"
|
|
395
|
+
summary_parts.append(f"[bold green]{len(refreshed)}[/bold green] {verb}")
|
|
396
|
+
if valid:
|
|
397
|
+
summary_parts.append(f"[bold green]{len(valid)}[/bold green] valid")
|
|
398
|
+
if skipped:
|
|
399
|
+
summary_parts.append(f"[dim]{len(skipped)} skipped[/dim]")
|
|
400
|
+
if failed:
|
|
401
|
+
summary_parts.append(f"[bold red]{len(failed)} failed[/bold red]")
|
|
402
|
+
|
|
403
|
+
console.print("Summary: " + ", ".join(summary_parts) if summary_parts else "No changes.")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
@project_app.command("status")
|
|
407
|
+
def project_status(
|
|
408
|
+
ctx: typer.Context,
|
|
409
|
+
project: str | None = typer.Option(
|
|
410
|
+
None, "--project", help="Check only this project (default: all)"
|
|
411
|
+
),
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Test connectivity to connected Keboola projects."""
|
|
414
|
+
formatter = get_formatter(ctx)
|
|
415
|
+
service = get_service(ctx, "project_service")
|
|
416
|
+
|
|
417
|
+
aliases = [project] if project else None
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
statuses = service.get_status(aliases=aliases)
|
|
421
|
+
formatter.output(statuses, _format_status_table)
|
|
422
|
+
except ConfigError as exc:
|
|
423
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
424
|
+
raise typer.Exit(code=5) from None
|
|
425
|
+
except KeboolaApiError as exc:
|
|
426
|
+
exit_code = map_error_to_exit_code(exc)
|
|
427
|
+
formatter.error(
|
|
428
|
+
message=exc.message,
|
|
429
|
+
error_code=exc.error_code,
|
|
430
|
+
retryable=exc.retryable,
|
|
431
|
+
)
|
|
432
|
+
raise typer.Exit(code=exit_code) from None
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@project_app.command("refresh")
|
|
436
|
+
def project_refresh(
|
|
437
|
+
ctx: typer.Context,
|
|
438
|
+
project: str | None = typer.Option(
|
|
439
|
+
None, "--project", "-p", help="Refresh token for a specific project"
|
|
440
|
+
),
|
|
441
|
+
all_projects: bool = typer.Option(
|
|
442
|
+
False, "--all", help="Refresh all projects with invalid tokens"
|
|
443
|
+
),
|
|
444
|
+
dry_run: bool = typer.Option(
|
|
445
|
+
False, "--dry-run", help="Preview what would be refreshed without making changes"
|
|
446
|
+
),
|
|
447
|
+
force: bool = typer.Option(False, "--force", help="Refresh even if token is valid"),
|
|
448
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
449
|
+
token_description: str = typer.Option(
|
|
450
|
+
DEFAULT_TOKEN_DESCRIPTION,
|
|
451
|
+
"--token-description",
|
|
452
|
+
help="Description prefix for created Storage API tokens",
|
|
453
|
+
),
|
|
454
|
+
token_expires_in: int | None = typer.Option(
|
|
455
|
+
None,
|
|
456
|
+
"--token-expires-in",
|
|
457
|
+
min=1,
|
|
458
|
+
help="Token lifetime in seconds. If not set, tokens never expire.",
|
|
459
|
+
),
|
|
460
|
+
) -> None:
|
|
461
|
+
"""Refresh expired or invalid Storage API tokens.
|
|
462
|
+
|
|
463
|
+
Creates new tokens via the Manage API and updates the local config.
|
|
464
|
+
Requires a Manage API token: interactive hidden prompt by default
|
|
465
|
+
(since 0.28.0); pass top-level --allow-env-manage-token to read
|
|
466
|
+
KBC_MANAGE_API_TOKEN from env (CI/CD).
|
|
467
|
+
|
|
468
|
+
\b
|
|
469
|
+
Examples:
|
|
470
|
+
kbagent project refresh --project prod
|
|
471
|
+
kbagent project refresh --all
|
|
472
|
+
kbagent project refresh --all --dry-run
|
|
473
|
+
kbagent project refresh --all --force
|
|
474
|
+
"""
|
|
475
|
+
formatter = get_formatter(ctx)
|
|
476
|
+
service = get_service(ctx, "org_service")
|
|
477
|
+
|
|
478
|
+
# Validate: must have --project or --all, not both, not neither
|
|
479
|
+
if project and all_projects:
|
|
480
|
+
formatter.error(
|
|
481
|
+
message="Provide --project or --all, not both",
|
|
482
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
483
|
+
)
|
|
484
|
+
raise typer.Exit(code=2)
|
|
485
|
+
if not project and not all_projects:
|
|
486
|
+
formatter.error(
|
|
487
|
+
message="Provide --project or --all",
|
|
488
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
489
|
+
)
|
|
490
|
+
raise typer.Exit(code=2)
|
|
491
|
+
|
|
492
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
493
|
+
|
|
494
|
+
aliases = [project] if project else None
|
|
495
|
+
|
|
496
|
+
# Build kwargs shared by preview and real call
|
|
497
|
+
refresh_kwargs: dict = {
|
|
498
|
+
"manage_token": manage_token,
|
|
499
|
+
"aliases": aliases,
|
|
500
|
+
"token_description": token_description,
|
|
501
|
+
"token_expires_in": token_expires_in,
|
|
502
|
+
"force": force,
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Interactive safety: show preview first, then confirm
|
|
506
|
+
interactive = not formatter.json_mode and not yes and not dry_run
|
|
507
|
+
if interactive:
|
|
508
|
+
try:
|
|
509
|
+
preview = service.refresh_tokens(**refresh_kwargs, dry_run=True)
|
|
510
|
+
except KeboolaApiError as exc:
|
|
511
|
+
exit_code = map_error_to_exit_code(exc)
|
|
512
|
+
formatter.error(
|
|
513
|
+
message=exc.message,
|
|
514
|
+
error_code=exc.error_code,
|
|
515
|
+
retryable=exc.retryable,
|
|
516
|
+
)
|
|
517
|
+
raise typer.Exit(code=exit_code) from None
|
|
518
|
+
|
|
519
|
+
_format_refresh_result(formatter.console, preview)
|
|
520
|
+
|
|
521
|
+
would_refresh = len(preview.get("projects_refreshed", []))
|
|
522
|
+
if would_refresh == 0:
|
|
523
|
+
formatter.console.print("\nAll tokens are valid.")
|
|
524
|
+
return
|
|
525
|
+
|
|
526
|
+
if not typer.confirm(f"\nProceed to refresh {would_refresh} token(s)?"):
|
|
527
|
+
formatter.console.print("Aborted.")
|
|
528
|
+
raise typer.Exit(code=0)
|
|
529
|
+
|
|
530
|
+
# Execute the actual refresh
|
|
531
|
+
try:
|
|
532
|
+
result = service.refresh_tokens(**refresh_kwargs, dry_run=dry_run)
|
|
533
|
+
except KeboolaApiError as exc:
|
|
534
|
+
exit_code = map_error_to_exit_code(exc)
|
|
535
|
+
formatter.error(
|
|
536
|
+
message=exc.message,
|
|
537
|
+
error_code=exc.error_code,
|
|
538
|
+
retryable=exc.retryable,
|
|
539
|
+
)
|
|
540
|
+
raise typer.Exit(code=exit_code) from None
|
|
541
|
+
|
|
542
|
+
formatter.output(result, _format_refresh_result)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# ── Project pin (default project) ─────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@project_app.command("use")
|
|
549
|
+
def project_use(
|
|
550
|
+
ctx: typer.Context,
|
|
551
|
+
alias: str = typer.Argument(..., help="Project alias to pin as default"),
|
|
552
|
+
) -> None:
|
|
553
|
+
"""Pin <alias> as the default project for subsequent commands.
|
|
554
|
+
|
|
555
|
+
The pin persists in config.json. ``KBAGENT_PROJECT`` overrides it for a
|
|
556
|
+
single invocation; an explicit ``--project`` flag overrides both.
|
|
557
|
+
"""
|
|
558
|
+
formatter = get_formatter(ctx)
|
|
559
|
+
service = get_service(ctx, "project_service")
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
result = service.use_project(alias=alias)
|
|
563
|
+
except ConfigError as exc:
|
|
564
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
565
|
+
raise typer.Exit(code=5) from None
|
|
566
|
+
|
|
567
|
+
def _human(c: Console, d: dict[str, Any]) -> None:
|
|
568
|
+
previous = d.get("previous")
|
|
569
|
+
if previous and previous != d["alias"]:
|
|
570
|
+
c.print(
|
|
571
|
+
f"[bold green]Pinned:[/bold green] default project is now "
|
|
572
|
+
f"[bold]{d['alias']}[/bold] (was [dim]{previous}[/dim])"
|
|
573
|
+
)
|
|
574
|
+
else:
|
|
575
|
+
c.print(
|
|
576
|
+
f"[bold green]Pinned:[/bold green] default project is [bold]{d['alias']}[/bold]"
|
|
577
|
+
)
|
|
578
|
+
env_override = d.get("env_override")
|
|
579
|
+
if env_override and env_override != d["alias"]:
|
|
580
|
+
c.print(
|
|
581
|
+
f"[yellow]Note:[/yellow] KBAGENT_PROJECT='{env_override}' is set "
|
|
582
|
+
"and overrides this pin for the current shell."
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
formatter.output(result, _human)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@project_app.command("current")
|
|
589
|
+
def project_current(ctx: typer.Context) -> None:
|
|
590
|
+
"""Show the effective default project.
|
|
591
|
+
|
|
592
|
+
Reports whether the value comes from the ``KBAGENT_PROJECT`` env var
|
|
593
|
+
(``env``) or the persisted pin (``pin``). Prints nothing but a hint if
|
|
594
|
+
neither is set.
|
|
595
|
+
"""
|
|
596
|
+
formatter = get_formatter(ctx)
|
|
597
|
+
service = get_service(ctx, "project_service")
|
|
598
|
+
|
|
599
|
+
result = service.current_project()
|
|
600
|
+
|
|
601
|
+
def _human(c: Console, d: dict[str, Any]) -> None:
|
|
602
|
+
alias = d.get("alias")
|
|
603
|
+
source = d.get("source")
|
|
604
|
+
if alias is None:
|
|
605
|
+
c.print(
|
|
606
|
+
"[dim](no default project set)[/dim] -- pass --project, set "
|
|
607
|
+
"KBAGENT_PROJECT, or run 'kbagent project use <alias>'"
|
|
608
|
+
)
|
|
609
|
+
return
|
|
610
|
+
if source == "env":
|
|
611
|
+
c.print(f"[bold cyan]{alias}[/bold cyan] [dim](source: KBAGENT_PROJECT env var)[/dim]")
|
|
612
|
+
if d.get("env_points_to_configured_project") is False:
|
|
613
|
+
c.print(
|
|
614
|
+
f"[yellow]Warning:[/yellow] '{alias}' is NOT in your "
|
|
615
|
+
"configured projects. Commands that use this pin will fail."
|
|
616
|
+
)
|
|
617
|
+
pinned = d.get("pinned")
|
|
618
|
+
if pinned:
|
|
619
|
+
c.print(f"[dim] (pinned in config: {pinned}, overridden)[/dim]")
|
|
620
|
+
else:
|
|
621
|
+
c.print(f"[bold cyan]{alias}[/bold cyan] [dim](source: pinned default)[/dim]")
|
|
622
|
+
|
|
623
|
+
formatter.output(result, _human)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# ── Project description (dashboard KBC.projectDescription) ────────────
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
@project_app.command("description-get")
|
|
630
|
+
def project_description_get(
|
|
631
|
+
ctx: typer.Context,
|
|
632
|
+
project: str = typer.Option(
|
|
633
|
+
...,
|
|
634
|
+
"--project",
|
|
635
|
+
help="Project alias to query",
|
|
636
|
+
),
|
|
637
|
+
) -> None:
|
|
638
|
+
"""Get the Keboola dashboard project description.
|
|
639
|
+
|
|
640
|
+
Reads the ``KBC.projectDescription`` metadata value from the default
|
|
641
|
+
branch - this is what the Keboola UI shows on the project dashboard.
|
|
642
|
+
Returns an empty string if no description has been set.
|
|
643
|
+
"""
|
|
644
|
+
formatter = get_formatter(ctx)
|
|
645
|
+
service = get_service(ctx, "branch_service")
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
result = service.get_project_description(alias=project)
|
|
649
|
+
except KeboolaApiError as exc:
|
|
650
|
+
exit_code = map_error_to_exit_code(exc)
|
|
651
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
652
|
+
raise typer.Exit(code=exit_code) from None
|
|
653
|
+
except ConfigError as exc:
|
|
654
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
655
|
+
raise typer.Exit(code=5) from None
|
|
656
|
+
|
|
657
|
+
formatter.output(
|
|
658
|
+
result,
|
|
659
|
+
lambda c, d: c.print(d["description"] or "[dim](no description set)[/dim]"),
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@project_app.command("description-set")
|
|
664
|
+
def project_description_set(
|
|
665
|
+
ctx: typer.Context,
|
|
666
|
+
project: str = typer.Option(
|
|
667
|
+
...,
|
|
668
|
+
"--project",
|
|
669
|
+
help="Project alias to update",
|
|
670
|
+
),
|
|
671
|
+
text: str | None = typer.Option(None, "--text", help="Inline description string"),
|
|
672
|
+
file: Path | None = typer.Option(
|
|
673
|
+
None,
|
|
674
|
+
"--file",
|
|
675
|
+
help="Read description from a UTF-8 markdown file",
|
|
676
|
+
),
|
|
677
|
+
stdin: bool = typer.Option(
|
|
678
|
+
False,
|
|
679
|
+
"--stdin",
|
|
680
|
+
help="Read description from standard input",
|
|
681
|
+
),
|
|
682
|
+
) -> None:
|
|
683
|
+
"""Set the Keboola dashboard project description (markdown).
|
|
684
|
+
|
|
685
|
+
Writes to ``KBC.projectDescription`` on the default branch. Provide the
|
|
686
|
+
content via exactly one of --text, --file, or --stdin.
|
|
687
|
+
"""
|
|
688
|
+
formatter = get_formatter(ctx)
|
|
689
|
+
|
|
690
|
+
try:
|
|
691
|
+
description = resolve_text_input(text=text, file=file, stdin=stdin)
|
|
692
|
+
except ConfigError as exc:
|
|
693
|
+
formatter.error(message=exc.message, error_code=ErrorCode.INVALID_ARGUMENT)
|
|
694
|
+
raise typer.Exit(code=2) from None
|
|
695
|
+
|
|
696
|
+
service = get_service(ctx, "branch_service")
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
result = service.set_project_description(alias=project, description=description)
|
|
700
|
+
formatter.output(
|
|
701
|
+
result,
|
|
702
|
+
lambda c, d: c.print(f"[bold green]Success:[/bold green] {d['message']}"),
|
|
703
|
+
)
|
|
704
|
+
except KeboolaApiError as exc:
|
|
705
|
+
exit_code = map_error_to_exit_code(exc)
|
|
706
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
707
|
+
raise typer.Exit(code=exit_code) from None
|
|
708
|
+
except ConfigError as exc:
|
|
709
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
710
|
+
raise typer.Exit(code=5) from None
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _format_info_table(console: Console, data: dict[str, Any]) -> None:
|
|
714
|
+
"""Render detailed project metadata as a Rich table."""
|
|
715
|
+
from rich.panel import Panel
|
|
716
|
+
|
|
717
|
+
table = Table(show_header=False, box=None, padding=(0, 1))
|
|
718
|
+
table.add_column("Field", style="bold cyan", no_wrap=True)
|
|
719
|
+
table.add_column("Value")
|
|
720
|
+
|
|
721
|
+
table.add_row("Alias", str(data.get("alias", "")))
|
|
722
|
+
table.add_row("Project ID", str(data.get("project_id", "")))
|
|
723
|
+
table.add_row("Project Name", str(data.get("project_name", "")))
|
|
724
|
+
table.add_row("Stack URL", str(data.get("stack_url", "")))
|
|
725
|
+
table.add_row("Default Backend", str(data.get("default_backend", "")))
|
|
726
|
+
table.add_row("Token ID", str(data.get("token_id", "")))
|
|
727
|
+
table.add_row("Token Description", str(data.get("token_description", "")))
|
|
728
|
+
table.add_row("Master Token", "Yes" if data.get("is_master_token") else "No")
|
|
729
|
+
|
|
730
|
+
expires = data.get("token_expires")
|
|
731
|
+
table.add_row("Token Expires", str(expires) if expires else "[dim]never[/dim]")
|
|
732
|
+
|
|
733
|
+
features = data.get("features", [])
|
|
734
|
+
if features:
|
|
735
|
+
table.add_row("Features", ", ".join(sorted(features)))
|
|
736
|
+
else:
|
|
737
|
+
table.add_row("Features", "[dim](none)[/dim]")
|
|
738
|
+
|
|
739
|
+
limits = data.get("limits", {})
|
|
740
|
+
if limits:
|
|
741
|
+
limit_lines = [f"{k}: {v}" for k, v in sorted(limits.items())]
|
|
742
|
+
table.add_row("Limits", "\n".join(limit_lines))
|
|
743
|
+
|
|
744
|
+
metrics = data.get("metrics", {})
|
|
745
|
+
if metrics:
|
|
746
|
+
metric_lines = [f"{k}: {v}" for k, v in sorted(metrics.items())]
|
|
747
|
+
table.add_row("Metrics", "\n".join(metric_lines))
|
|
748
|
+
|
|
749
|
+
console.print(Panel(table, title=f"Project Info: {data.get('alias', '')}", expand=False))
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@project_app.command("info")
|
|
753
|
+
def project_info(
|
|
754
|
+
ctx: typer.Context,
|
|
755
|
+
project: str = typer.Option(
|
|
756
|
+
...,
|
|
757
|
+
"--project",
|
|
758
|
+
help="Project alias to query",
|
|
759
|
+
),
|
|
760
|
+
) -> None:
|
|
761
|
+
"""Show detailed project metadata.
|
|
762
|
+
|
|
763
|
+
Returns project name, ID, stack URL, default backend, feature flags,
|
|
764
|
+
storage limits and metrics, and token information.
|
|
765
|
+
"""
|
|
766
|
+
formatter = get_formatter(ctx)
|
|
767
|
+
service = get_service(ctx, "project_service")
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
result = service.get_info(alias=project)
|
|
771
|
+
except KeboolaApiError as exc:
|
|
772
|
+
exit_code = map_error_to_exit_code(exc)
|
|
773
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
774
|
+
raise typer.Exit(code=exit_code) from None
|
|
775
|
+
except ConfigError as exc:
|
|
776
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
777
|
+
raise typer.Exit(code=5) from None
|
|
778
|
+
|
|
779
|
+
formatter.output(result, _format_info_table)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# ── Project members & invitations (since v0.26.1) ─────────────────────
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
def _format_invite_result(console: Console, data: dict[str, Any]) -> None:
|
|
786
|
+
"""Single-shot invite result."""
|
|
787
|
+
status = data.get("status", "")
|
|
788
|
+
if status == "ok":
|
|
789
|
+
console.print(
|
|
790
|
+
f"[bold green]Invited[/bold green] {data['email']} to "
|
|
791
|
+
f"[cyan]{data['alias']}[/cyan] as [yellow]{data['role']}[/yellow] "
|
|
792
|
+
f"(invitation_id={data.get('invitation_id')})."
|
|
793
|
+
)
|
|
794
|
+
elif status == "noop":
|
|
795
|
+
console.print(
|
|
796
|
+
f"[yellow]No-op[/yellow]: {data['email']} on [cyan]{data['alias']}[/cyan] "
|
|
797
|
+
f"-- {data.get('note', '')}."
|
|
798
|
+
)
|
|
799
|
+
elif status == "dry_run":
|
|
800
|
+
console.print(
|
|
801
|
+
f"[dim]Would invite[/dim] {data['email']} to [cyan]{data['alias']}[/cyan] "
|
|
802
|
+
f"as [yellow]{data['role']}[/yellow]."
|
|
803
|
+
)
|
|
804
|
+
else:
|
|
805
|
+
console.print(f"[bold red]Unexpected status[/bold red]: {data!r}")
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def _format_bulk_invite_result(console: Console, data: dict[str, Any]) -> None:
|
|
809
|
+
"""Render the bulk-invite summary table."""
|
|
810
|
+
console.print(
|
|
811
|
+
f"\n[bold]Bulk invite:[/bold] total={data['total']} "
|
|
812
|
+
f"succeeded={data['succeeded']} noop={data['noop']} failed={data['failed']}"
|
|
813
|
+
+ (" [dim](dry-run)[/dim]" if data.get("dry_run") else "")
|
|
814
|
+
)
|
|
815
|
+
rows = data.get("rows") or []
|
|
816
|
+
if not rows:
|
|
817
|
+
return
|
|
818
|
+
table = Table(title="Per-row results")
|
|
819
|
+
table.add_column("Status", style="bold")
|
|
820
|
+
table.add_column("Email")
|
|
821
|
+
table.add_column("Project")
|
|
822
|
+
table.add_column("Role")
|
|
823
|
+
table.add_column("Note")
|
|
824
|
+
status_style = {"ok": "green", "noop": "yellow", "failed": "red"}
|
|
825
|
+
for row in rows:
|
|
826
|
+
status = row.get("status", "")
|
|
827
|
+
style = status_style.get(status, "white")
|
|
828
|
+
table.add_row(
|
|
829
|
+
f"[{style}]{status}[/{style}]",
|
|
830
|
+
row.get("email", ""),
|
|
831
|
+
row.get("project", ""),
|
|
832
|
+
row.get("role", ""),
|
|
833
|
+
row.get("note", ""),
|
|
834
|
+
)
|
|
835
|
+
console.print(table)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _format_member_list(console: Console, data: dict[str, Any]) -> None:
|
|
839
|
+
members = data.get("members") or []
|
|
840
|
+
table = Table(title=f"Members of {data.get('alias')} (project_id={data.get('project_id')})")
|
|
841
|
+
table.add_column("ID", justify="right", style="dim")
|
|
842
|
+
table.add_column("Email")
|
|
843
|
+
table.add_column("Role", style="yellow")
|
|
844
|
+
table.add_column("Status")
|
|
845
|
+
table.add_column("MFA", justify="center")
|
|
846
|
+
for m in members:
|
|
847
|
+
table.add_row(
|
|
848
|
+
str(m.get("id", "")),
|
|
849
|
+
m.get("email", ""),
|
|
850
|
+
m.get("role", ""),
|
|
851
|
+
m.get("status", ""),
|
|
852
|
+
"yes" if m.get("mfa_enabled") else "no",
|
|
853
|
+
)
|
|
854
|
+
console.print(table)
|
|
855
|
+
pending = data.get("pending_invitations")
|
|
856
|
+
if pending:
|
|
857
|
+
ptable = Table(title="Pending invitations")
|
|
858
|
+
ptable.add_column("ID", justify="right", style="dim")
|
|
859
|
+
ptable.add_column("Email")
|
|
860
|
+
ptable.add_column("Role", style="yellow")
|
|
861
|
+
ptable.add_column("Reason")
|
|
862
|
+
for p in pending:
|
|
863
|
+
ptable.add_row(
|
|
864
|
+
str(p.get("id", "")),
|
|
865
|
+
p.get("user", {}).get("email", ""),
|
|
866
|
+
p.get("role", ""),
|
|
867
|
+
p.get("reason", ""),
|
|
868
|
+
)
|
|
869
|
+
console.print(ptable)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _format_invitation_list(console: Console, data: dict[str, Any]) -> None:
|
|
873
|
+
invitations = data.get("invitations") or []
|
|
874
|
+
if not invitations:
|
|
875
|
+
console.print(f"No pending invitations for [cyan]{data.get('alias')}[/cyan].")
|
|
876
|
+
return
|
|
877
|
+
table = Table(
|
|
878
|
+
title=f"Pending invitations for {data.get('alias')} (project_id={data.get('project_id')})"
|
|
879
|
+
)
|
|
880
|
+
table.add_column("ID", justify="right", style="dim")
|
|
881
|
+
table.add_column("Email")
|
|
882
|
+
table.add_column("Role", style="yellow")
|
|
883
|
+
table.add_column("Reason")
|
|
884
|
+
for inv in invitations:
|
|
885
|
+
table.add_row(
|
|
886
|
+
str(inv.get("id", "")),
|
|
887
|
+
inv.get("user", {}).get("email", ""),
|
|
888
|
+
inv.get("role", ""),
|
|
889
|
+
inv.get("reason", ""),
|
|
890
|
+
)
|
|
891
|
+
console.print(table)
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@project_app.command("invite")
|
|
895
|
+
def project_invite(
|
|
896
|
+
ctx: typer.Context,
|
|
897
|
+
project: str | None = typer.Option(
|
|
898
|
+
None, "--project", "-p", help="Project alias to invite the user to (single-shot mode)"
|
|
899
|
+
),
|
|
900
|
+
email: str | None = typer.Option(
|
|
901
|
+
None, "--email", "-e", help="Email address of the user to invite"
|
|
902
|
+
),
|
|
903
|
+
role: ProjectRole | None = typer.Option(
|
|
904
|
+
None,
|
|
905
|
+
"--role",
|
|
906
|
+
"-r",
|
|
907
|
+
help="Role to grant: " + " | ".join(PROJECT_ROLES),
|
|
908
|
+
),
|
|
909
|
+
reason: str | None = typer.Option(
|
|
910
|
+
None, "--reason", help="Optional human-readable reason attached to the invitation"
|
|
911
|
+
),
|
|
912
|
+
from_csv: Path | None = typer.Option(
|
|
913
|
+
None,
|
|
914
|
+
"--from-csv",
|
|
915
|
+
help="CSV file with columns email, project (alias or numeric ID), role[, reason]",
|
|
916
|
+
),
|
|
917
|
+
default_role: ProjectRole | None = typer.Option(
|
|
918
|
+
None,
|
|
919
|
+
"--default-role",
|
|
920
|
+
help="Role to apply when a CSV row has no role column",
|
|
921
|
+
),
|
|
922
|
+
workers: int = typer.Option(
|
|
923
|
+
DEFAULT_INVITE_WORKERS,
|
|
924
|
+
"--workers",
|
|
925
|
+
min=1,
|
|
926
|
+
max=32,
|
|
927
|
+
help="Parallel workers for --from-csv (default 8)",
|
|
928
|
+
),
|
|
929
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview without sending invitations"),
|
|
930
|
+
) -> None:
|
|
931
|
+
"""Invite a user (or many users via CSV) to one or more projects.
|
|
932
|
+
|
|
933
|
+
\b
|
|
934
|
+
Single-shot:
|
|
935
|
+
kbagent project invite --project prod --email a@b.com --role admin
|
|
936
|
+
|
|
937
|
+
\b
|
|
938
|
+
Bulk (one row per email; CSV header required):
|
|
939
|
+
kbagent project invite --from-csv participants.csv --default-role guest
|
|
940
|
+
"""
|
|
941
|
+
formatter = get_formatter(ctx)
|
|
942
|
+
|
|
943
|
+
if from_csv and (project or email):
|
|
944
|
+
formatter.error(
|
|
945
|
+
message="--from-csv is mutually exclusive with --project / --email",
|
|
946
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
947
|
+
)
|
|
948
|
+
raise typer.Exit(code=2)
|
|
949
|
+
if not from_csv and not (project and email and role):
|
|
950
|
+
formatter.error(
|
|
951
|
+
message="Provide --project, --email, and --role for single-shot invite "
|
|
952
|
+
"(or use --from-csv for bulk).",
|
|
953
|
+
error_code=ErrorCode.USAGE_ERROR,
|
|
954
|
+
)
|
|
955
|
+
raise typer.Exit(code=2)
|
|
956
|
+
|
|
957
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
958
|
+
service = get_service(ctx, "member_service")
|
|
959
|
+
|
|
960
|
+
try:
|
|
961
|
+
if from_csv:
|
|
962
|
+
result = service.invite_bulk(
|
|
963
|
+
manage_token=manage_token,
|
|
964
|
+
csv_path=from_csv,
|
|
965
|
+
default_role=default_role,
|
|
966
|
+
workers=workers,
|
|
967
|
+
dry_run=dry_run,
|
|
968
|
+
)
|
|
969
|
+
payload = result.model_dump()
|
|
970
|
+
formatter.output(payload, _format_bulk_invite_result)
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
result = service.invite(
|
|
974
|
+
manage_token=manage_token,
|
|
975
|
+
alias=project,
|
|
976
|
+
email=email,
|
|
977
|
+
role=role,
|
|
978
|
+
reason=reason,
|
|
979
|
+
dry_run=dry_run,
|
|
980
|
+
)
|
|
981
|
+
except ConfigError as exc:
|
|
982
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
983
|
+
raise typer.Exit(code=5) from None
|
|
984
|
+
except ValueError as exc:
|
|
985
|
+
formatter.error(message=str(exc), error_code=ErrorCode.VALIDATION_ERROR)
|
|
986
|
+
raise typer.Exit(code=2) from None
|
|
987
|
+
except KeboolaApiError as exc:
|
|
988
|
+
exit_code = map_error_to_exit_code(exc)
|
|
989
|
+
formatter.error(
|
|
990
|
+
message=exc.message,
|
|
991
|
+
error_code=exc.error_code,
|
|
992
|
+
retryable=exc.retryable,
|
|
993
|
+
)
|
|
994
|
+
raise typer.Exit(code=exit_code) from None
|
|
995
|
+
|
|
996
|
+
formatter.output(result, _format_invite_result)
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
@project_app.command("member-list")
|
|
1000
|
+
def project_member_list(
|
|
1001
|
+
ctx: typer.Context,
|
|
1002
|
+
project: str = typer.Option(..., "--project", "-p", help="Project alias to list members for"),
|
|
1003
|
+
include_pending: bool = typer.Option(
|
|
1004
|
+
False, "--include-pending", help="Also list pending (unaccepted) invitations"
|
|
1005
|
+
),
|
|
1006
|
+
) -> None:
|
|
1007
|
+
"""List active members of a project (and optionally pending invitations)."""
|
|
1008
|
+
formatter = get_formatter(ctx)
|
|
1009
|
+
|
|
1010
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
1011
|
+
service = get_service(ctx, "member_service")
|
|
1012
|
+
try:
|
|
1013
|
+
result = service.list_members(
|
|
1014
|
+
manage_token=manage_token,
|
|
1015
|
+
alias=project,
|
|
1016
|
+
include_pending=include_pending,
|
|
1017
|
+
)
|
|
1018
|
+
except ConfigError as exc:
|
|
1019
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1020
|
+
raise typer.Exit(code=5) from None
|
|
1021
|
+
except KeboolaApiError as exc:
|
|
1022
|
+
exit_code = map_error_to_exit_code(exc)
|
|
1023
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1024
|
+
raise typer.Exit(code=exit_code) from None
|
|
1025
|
+
|
|
1026
|
+
formatter.output(result, _format_member_list)
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
@project_app.command("invitation-list")
|
|
1030
|
+
def project_invitation_list(
|
|
1031
|
+
ctx: typer.Context,
|
|
1032
|
+
project: str = typer.Option(
|
|
1033
|
+
..., "--project", "-p", help="Project alias to list pending invitations for"
|
|
1034
|
+
),
|
|
1035
|
+
) -> None:
|
|
1036
|
+
"""List pending project invitations."""
|
|
1037
|
+
formatter = get_formatter(ctx)
|
|
1038
|
+
|
|
1039
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
1040
|
+
service = get_service(ctx, "member_service")
|
|
1041
|
+
try:
|
|
1042
|
+
result = service.list_invitations(manage_token=manage_token, alias=project)
|
|
1043
|
+
except ConfigError as exc:
|
|
1044
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1045
|
+
raise typer.Exit(code=5) from None
|
|
1046
|
+
except KeboolaApiError as exc:
|
|
1047
|
+
exit_code = map_error_to_exit_code(exc)
|
|
1048
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1049
|
+
raise typer.Exit(code=exit_code) from None
|
|
1050
|
+
|
|
1051
|
+
formatter.output(result, _format_invitation_list)
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
@project_app.command("invitation-cancel")
|
|
1055
|
+
def project_invitation_cancel(
|
|
1056
|
+
ctx: typer.Context,
|
|
1057
|
+
project: str = typer.Option(..., "--project", "-p", help="Project alias"),
|
|
1058
|
+
email: str = typer.Option(
|
|
1059
|
+
...,
|
|
1060
|
+
"--email",
|
|
1061
|
+
"-e",
|
|
1062
|
+
help="Invitee's email address (used to look up the invitation if --invitation-id is omitted)",
|
|
1063
|
+
),
|
|
1064
|
+
invitation_id: int | None = typer.Option(
|
|
1065
|
+
None,
|
|
1066
|
+
"--invitation-id",
|
|
1067
|
+
help="Numeric invitation ID; bypass the email lookup",
|
|
1068
|
+
),
|
|
1069
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
1070
|
+
) -> None:
|
|
1071
|
+
"""Cancel a pending invitation."""
|
|
1072
|
+
formatter = get_formatter(ctx)
|
|
1073
|
+
|
|
1074
|
+
if (
|
|
1075
|
+
not formatter.json_mode
|
|
1076
|
+
and not yes
|
|
1077
|
+
and not typer.confirm(f"Cancel pending invitation for {email} on {project}?")
|
|
1078
|
+
):
|
|
1079
|
+
formatter.console.print("Aborted.")
|
|
1080
|
+
raise typer.Exit(code=0)
|
|
1081
|
+
|
|
1082
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
1083
|
+
service = get_service(ctx, "member_service")
|
|
1084
|
+
try:
|
|
1085
|
+
result = service.cancel_invitation(
|
|
1086
|
+
manage_token=manage_token,
|
|
1087
|
+
alias=project,
|
|
1088
|
+
email=email,
|
|
1089
|
+
invitation_id=invitation_id,
|
|
1090
|
+
)
|
|
1091
|
+
except ConfigError as exc:
|
|
1092
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1093
|
+
raise typer.Exit(code=5) from None
|
|
1094
|
+
except KeboolaApiError as exc:
|
|
1095
|
+
exit_code = map_error_to_exit_code(exc)
|
|
1096
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1097
|
+
raise typer.Exit(code=exit_code) from None
|
|
1098
|
+
|
|
1099
|
+
formatter.output(
|
|
1100
|
+
result,
|
|
1101
|
+
lambda c, d: c.print(
|
|
1102
|
+
f"[bold green]Cancelled[/bold green] invitation_id={d.get('invitation_id')} "
|
|
1103
|
+
f"for {d.get('email')} on [cyan]{d.get('alias')}[/cyan]."
|
|
1104
|
+
),
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
@project_app.command("member-remove")
|
|
1109
|
+
def project_member_remove(
|
|
1110
|
+
ctx: typer.Context,
|
|
1111
|
+
project: str = typer.Option(..., "--project", "-p", help="Project alias"),
|
|
1112
|
+
email: str = typer.Option(..., "--email", "-e", help="Email of the member to remove"),
|
|
1113
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
1114
|
+
) -> None:
|
|
1115
|
+
"""Remove an active member from a project (destructive)."""
|
|
1116
|
+
formatter = get_formatter(ctx)
|
|
1117
|
+
|
|
1118
|
+
if (
|
|
1119
|
+
not formatter.json_mode
|
|
1120
|
+
and not yes
|
|
1121
|
+
and not typer.confirm(f"Remove member {email} from project {project}? This is destructive.")
|
|
1122
|
+
):
|
|
1123
|
+
formatter.console.print("Aborted.")
|
|
1124
|
+
raise typer.Exit(code=0)
|
|
1125
|
+
|
|
1126
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
1127
|
+
service = get_service(ctx, "member_service")
|
|
1128
|
+
try:
|
|
1129
|
+
result = service.remove_member(
|
|
1130
|
+
manage_token=manage_token,
|
|
1131
|
+
alias=project,
|
|
1132
|
+
email=email,
|
|
1133
|
+
)
|
|
1134
|
+
except ConfigError as exc:
|
|
1135
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1136
|
+
raise typer.Exit(code=5) from None
|
|
1137
|
+
except KeboolaApiError as exc:
|
|
1138
|
+
exit_code = map_error_to_exit_code(exc)
|
|
1139
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1140
|
+
raise typer.Exit(code=exit_code) from None
|
|
1141
|
+
|
|
1142
|
+
formatter.output(
|
|
1143
|
+
result,
|
|
1144
|
+
lambda c, d: c.print(
|
|
1145
|
+
f"[bold red]Removed[/bold red] {d.get('email')} (user_id={d.get('user_id')}) "
|
|
1146
|
+
f"from [cyan]{d.get('alias')}[/cyan]."
|
|
1147
|
+
),
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
@project_app.command("member-set-role")
|
|
1152
|
+
def project_member_set_role(
|
|
1153
|
+
ctx: typer.Context,
|
|
1154
|
+
project: str = typer.Option(..., "--project", "-p", help="Project alias"),
|
|
1155
|
+
email: str = typer.Option(..., "--email", "-e", help="Email of the member to update"),
|
|
1156
|
+
role: ProjectRole = typer.Option(
|
|
1157
|
+
...,
|
|
1158
|
+
"--role",
|
|
1159
|
+
"-r",
|
|
1160
|
+
help="New role: " + " | ".join(PROJECT_ROLES),
|
|
1161
|
+
),
|
|
1162
|
+
) -> None:
|
|
1163
|
+
"""Change an existing member's role (PATCH)."""
|
|
1164
|
+
formatter = get_formatter(ctx)
|
|
1165
|
+
|
|
1166
|
+
manage_token = resolve_manage_token(allow_env=ctx.obj["allow_env_manage_token"])
|
|
1167
|
+
service = get_service(ctx, "member_service")
|
|
1168
|
+
try:
|
|
1169
|
+
result = service.set_member_role(
|
|
1170
|
+
manage_token=manage_token,
|
|
1171
|
+
alias=project,
|
|
1172
|
+
email=email,
|
|
1173
|
+
role=role,
|
|
1174
|
+
)
|
|
1175
|
+
except ConfigError as exc:
|
|
1176
|
+
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
|
|
1177
|
+
raise typer.Exit(code=5) from None
|
|
1178
|
+
except ValueError as exc:
|
|
1179
|
+
formatter.error(message=str(exc), error_code=ErrorCode.VALIDATION_ERROR)
|
|
1180
|
+
raise typer.Exit(code=2) from None
|
|
1181
|
+
except KeboolaApiError as exc:
|
|
1182
|
+
exit_code = map_error_to_exit_code(exc)
|
|
1183
|
+
formatter.error(message=exc.message, error_code=exc.error_code, retryable=exc.retryable)
|
|
1184
|
+
raise typer.Exit(code=exit_code) from None
|
|
1185
|
+
|
|
1186
|
+
formatter.output(
|
|
1187
|
+
result,
|
|
1188
|
+
lambda c, d: c.print(
|
|
1189
|
+
f"[bold green]Updated[/bold green] {d.get('email')} role on "
|
|
1190
|
+
f"[cyan]{d.get('alias')}[/cyan] -> [yellow]{d.get('role')}[/yellow]."
|
|
1191
|
+
),
|
|
1192
|
+
)
|