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.
Files changed (306) hide show
  1. keboola_agent_cli/__init__.py +34 -0
  2. keboola_agent_cli/__main__.py +5 -0
  3. keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
  4. keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
  5. keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
  6. keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
  7. keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
  8. keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
  9. keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
  10. keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
  11. keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
  12. keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
  13. keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
  14. keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
  15. keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
  16. keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
  17. keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
  18. keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
  19. keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
  20. keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
  21. keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
  22. keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
  23. keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
  24. keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
  25. keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
  26. keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
  27. keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
  28. keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
  29. keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
  30. keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
  31. keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
  32. keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
  33. keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
  34. keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
  35. keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
  36. keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
  37. keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
  38. keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
  39. keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
  40. keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
  41. keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
  42. keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
  43. keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
  44. keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
  45. keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
  46. keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
  47. keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
  48. keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
  49. keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
  50. keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
  51. keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
  52. keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
  53. keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
  54. keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
  55. keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
  56. keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
  57. keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
  58. keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
  59. keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
  60. keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
  61. keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
  62. keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
  63. keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
  64. keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
  65. keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
  66. keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
  67. keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
  68. keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
  69. keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
  70. keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
  71. keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
  72. keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
  73. keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
  74. keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
  75. keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
  76. keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
  77. keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
  78. keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
  79. keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
  80. keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
  81. keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
  82. keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
  83. keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
  84. keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
  85. keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
  86. keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
  87. keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
  88. keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
  89. keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
  90. keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
  91. keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
  92. keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
  93. keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
  94. keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
  95. keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
  96. keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
  97. keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
  98. keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
  99. keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
  100. keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
  101. keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
  102. keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
  103. keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
  104. keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
  105. keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
  106. keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
  107. keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
  108. keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
  109. keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
  110. keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
  111. keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
  112. keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
  113. keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
  114. keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
  115. keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
  116. keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
  117. keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
  118. keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
  119. keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
  120. keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
  121. keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
  122. keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
  123. keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
  124. keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
  125. keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
  126. keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
  127. keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
  128. keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
  129. keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
  130. keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
  131. keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
  132. keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
  133. keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
  134. keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
  135. keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
  136. keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
  137. keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
  138. keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
  139. keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
  140. keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
  141. keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
  142. keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
  143. keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
  144. keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
  145. keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
  146. keboola_agent_cli/_ui_dist/index.html +50 -0
  147. keboola_agent_cli/ai_client.py +83 -0
  148. keboola_agent_cli/auto_update.py +550 -0
  149. keboola_agent_cli/changelog.py +1198 -0
  150. keboola_agent_cli/cli.py +448 -0
  151. keboola_agent_cli/client.py +3422 -0
  152. keboola_agent_cli/commands/__init__.py +0 -0
  153. keboola_agent_cli/commands/_data_app_git.py +343 -0
  154. keboola_agent_cli/commands/_helpers.py +377 -0
  155. keboola_agent_cli/commands/_metadata_input.py +49 -0
  156. keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
  157. keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
  158. keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
  159. keboola_agent_cli/commands/agent.py +968 -0
  160. keboola_agent_cli/commands/branch.py +423 -0
  161. keboola_agent_cli/commands/changelog.py +168 -0
  162. keboola_agent_cli/commands/component.py +216 -0
  163. keboola_agent_cli/commands/config.py +2442 -0
  164. keboola_agent_cli/commands/context.py +1481 -0
  165. keboola_agent_cli/commands/data_app.py +1279 -0
  166. keboola_agent_cli/commands/dev_portal.py +584 -0
  167. keboola_agent_cli/commands/doctor.py +37 -0
  168. keboola_agent_cli/commands/encrypt.py +145 -0
  169. keboola_agent_cli/commands/feature.py +311 -0
  170. keboola_agent_cli/commands/flow.py +948 -0
  171. keboola_agent_cli/commands/http_client.py +157 -0
  172. keboola_agent_cli/commands/init.py +279 -0
  173. keboola_agent_cli/commands/job.py +661 -0
  174. keboola_agent_cli/commands/kai.py +301 -0
  175. keboola_agent_cli/commands/lineage.py +1464 -0
  176. keboola_agent_cli/commands/org.py +292 -0
  177. keboola_agent_cli/commands/permissions.py +360 -0
  178. keboola_agent_cli/commands/project.py +1192 -0
  179. keboola_agent_cli/commands/repl.py +243 -0
  180. keboola_agent_cli/commands/schedule.py +340 -0
  181. keboola_agent_cli/commands/search.py +178 -0
  182. keboola_agent_cli/commands/semantic_layer.py +939 -0
  183. keboola_agent_cli/commands/serve.py +272 -0
  184. keboola_agent_cli/commands/sharing.py +340 -0
  185. keboola_agent_cli/commands/storage.py +2630 -0
  186. keboola_agent_cli/commands/stream.py +266 -0
  187. keboola_agent_cli/commands/sync.py +1277 -0
  188. keboola_agent_cli/commands/tool.py +206 -0
  189. keboola_agent_cli/commands/version.py +186 -0
  190. keboola_agent_cli/commands/workspace.py +635 -0
  191. keboola_agent_cli/config_store.py +582 -0
  192. keboola_agent_cli/constants.py +528 -0
  193. keboola_agent_cli/data_science_client.py +342 -0
  194. keboola_agent_cli/dev_portal_client.py +323 -0
  195. keboola_agent_cli/errors.py +248 -0
  196. keboola_agent_cli/http_base.py +315 -0
  197. keboola_agent_cli/json_utils.py +126 -0
  198. keboola_agent_cli/lib.py +536 -0
  199. keboola_agent_cli/manage_client.py +324 -0
  200. keboola_agent_cli/metastore_client.py +214 -0
  201. keboola_agent_cli/models.py +427 -0
  202. keboola_agent_cli/output.py +1084 -0
  203. keboola_agent_cli/permissions.py +469 -0
  204. keboola_agent_cli/py.typed +3 -0
  205. keboola_agent_cli/result_models.py +271 -0
  206. keboola_agent_cli/server/__init__.py +34 -0
  207. keboola_agent_cli/server/agent_runner.py +1289 -0
  208. keboola_agent_cli/server/agents_store.py +325 -0
  209. keboola_agent_cli/server/app.py +764 -0
  210. keboola_agent_cli/server/auth.py +117 -0
  211. keboola_agent_cli/server/dependencies.py +149 -0
  212. keboola_agent_cli/server/pricing.py +303 -0
  213. keboola_agent_cli/server/routers/__init__.py +1 -0
  214. keboola_agent_cli/server/routers/agents.py +616 -0
  215. keboola_agent_cli/server/routers/ai_chat.py +129 -0
  216. keboola_agent_cli/server/routers/branches.py +133 -0
  217. keboola_agent_cli/server/routers/components.py +48 -0
  218. keboola_agent_cli/server/routers/configs.py +507 -0
  219. keboola_agent_cli/server/routers/data_apps.py +384 -0
  220. keboola_agent_cli/server/routers/dev_portal.py +67 -0
  221. keboola_agent_cli/server/routers/encrypt.py +35 -0
  222. keboola_agent_cli/server/routers/feature.py +179 -0
  223. keboola_agent_cli/server/routers/flows.py +204 -0
  224. keboola_agent_cli/server/routers/health.py +53 -0
  225. keboola_agent_cli/server/routers/jobs.py +175 -0
  226. keboola_agent_cli/server/routers/kai.py +80 -0
  227. keboola_agent_cli/server/routers/lineage.py +226 -0
  228. keboola_agent_cli/server/routers/mcp.py +70 -0
  229. keboola_agent_cli/server/routers/members.py +170 -0
  230. keboola_agent_cli/server/routers/org.py +96 -0
  231. keboola_agent_cli/server/routers/projects.py +106 -0
  232. keboola_agent_cli/server/routers/schedules.py +54 -0
  233. keboola_agent_cli/server/routers/search.py +30 -0
  234. keboola_agent_cli/server/routers/semantic_layer.py +650 -0
  235. keboola_agent_cli/server/routers/sharing.py +86 -0
  236. keboola_agent_cli/server/routers/storage.py +574 -0
  237. keboola_agent_cli/server/routers/stream.py +100 -0
  238. keboola_agent_cli/server/routers/workspaces.py +302 -0
  239. keboola_agent_cli/server/run_broadcaster.py +329 -0
  240. keboola_agent_cli/server/sse.py +25 -0
  241. keboola_agent_cli/services/__init__.py +0 -0
  242. keboola_agent_cli/services/_encryption.py +217 -0
  243. keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
  244. keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
  245. keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
  246. keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
  247. keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
  248. keboola_agent_cli/services/_sync_bindings.py +456 -0
  249. keboola_agent_cli/services/_sync_branch.py +191 -0
  250. keboola_agent_cli/services/_sync_bulk.py +228 -0
  251. keboola_agent_cli/services/_sync_clone.py +163 -0
  252. keboola_agent_cli/services/_sync_models.py +97 -0
  253. keboola_agent_cli/services/_sync_push_ops.py +369 -0
  254. keboola_agent_cli/services/_sync_storage.py +376 -0
  255. keboola_agent_cli/services/_sync_writeback.py +167 -0
  256. keboola_agent_cli/services/agent_service.py +458 -0
  257. keboola_agent_cli/services/base.py +175 -0
  258. keboola_agent_cli/services/branch_service.py +588 -0
  259. keboola_agent_cli/services/component_service.py +694 -0
  260. keboola_agent_cli/services/config_service.py +2099 -0
  261. keboola_agent_cli/services/data_app_git_service.py +224 -0
  262. keboola_agent_cli/services/data_app_service.py +2082 -0
  263. keboola_agent_cli/services/deep_lineage_service.py +1322 -0
  264. keboola_agent_cli/services/dev_portal_service.py +345 -0
  265. keboola_agent_cli/services/doctor_service.py +445 -0
  266. keboola_agent_cli/services/encrypt_service.py +87 -0
  267. keboola_agent_cli/services/feature_service.py +268 -0
  268. keboola_agent_cli/services/flow_service.py +769 -0
  269. keboola_agent_cli/services/flow_validation.py +188 -0
  270. keboola_agent_cli/services/http_forwarder_service.py +236 -0
  271. keboola_agent_cli/services/job_idempotency_store.py +285 -0
  272. keboola_agent_cli/services/job_service.py +797 -0
  273. keboola_agent_cli/services/kai_service.py +367 -0
  274. keboola_agent_cli/services/lineage_service.py +274 -0
  275. keboola_agent_cli/services/mcp_service.py +1498 -0
  276. keboola_agent_cli/services/mcp_transport.py +259 -0
  277. keboola_agent_cli/services/member_service.py +593 -0
  278. keboola_agent_cli/services/org_service.py +619 -0
  279. keboola_agent_cli/services/project_service.py +947 -0
  280. keboola_agent_cli/services/repo_validate_service.py +767 -0
  281. keboola_agent_cli/services/schedule_service.py +731 -0
  282. keboola_agent_cli/services/search_service.py +331 -0
  283. keboola_agent_cli/services/semantic_layer_service.py +1497 -0
  284. keboola_agent_cli/services/sharing_service.py +307 -0
  285. keboola_agent_cli/services/storage_service.py +2524 -0
  286. keboola_agent_cli/services/stream_service.py +395 -0
  287. keboola_agent_cli/services/sync_service.py +2244 -0
  288. keboola_agent_cli/services/variables_service.py +447 -0
  289. keboola_agent_cli/services/version_service.py +1038 -0
  290. keboola_agent_cli/services/workspace_service.py +1103 -0
  291. keboola_agent_cli/stream_client.py +217 -0
  292. keboola_agent_cli/sync/__init__.py +1 -0
  293. keboola_agent_cli/sync/branch_mapping.py +174 -0
  294. keboola_agent_cli/sync/clone.py +211 -0
  295. keboola_agent_cli/sync/code_extraction.py +655 -0
  296. keboola_agent_cli/sync/config_format.py +290 -0
  297. keboola_agent_cli/sync/diff_engine.py +566 -0
  298. keboola_agent_cli/sync/git_utils.py +93 -0
  299. keboola_agent_cli/sync/manifest.py +162 -0
  300. keboola_agent_cli/sync/naming.py +90 -0
  301. keboola_agent_cli/sync/secrets.py +62 -0
  302. keboola_agent_cli/sync/sql_split.py +134 -0
  303. keboola_cli-0.63.4.dist-info/METADATA +308 -0
  304. keboola_cli-0.63.4.dist-info/RECORD +306 -0
  305. keboola_cli-0.63.4.dist-info/WHEEL +4 -0
  306. 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
+ )