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,593 @@
1
+ """Project membership and invitation lifecycle service.
2
+
3
+ Wraps the Manage API endpoints under ``/manage/projects/{id}/{users,invitations}``
4
+ behind a layer that:
5
+
6
+ - resolves a project alias to its numeric ID via ``ConfigStore``;
7
+ - looks up members + invitations by email (the public-facing key) so callers
8
+ never need to deal with raw user/invitation IDs;
9
+ - treats the Manage API's "already invited / already a member" 400 response as
10
+ a no-op rather than an error (mirrors the heuristic from the orchestrator
11
+ scripts, but typed to status_code + message substring rather than guessed
12
+ HTTP code);
13
+ - parallelises bulk invitation via :class:`ThreadPoolExecutor`, accumulating
14
+ per-row results so one bad row never aborts the rest.
15
+
16
+ Endpoints + payload shapes were verified empirically on 2026-05-01 against
17
+ ``connection.us-east4.gcp.keboola.com``; see the plan-of-record for the full
18
+ verification log.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import csv
24
+ import logging
25
+ from collections.abc import Callable
26
+ from concurrent.futures import ThreadPoolExecutor, as_completed
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ from ..config_store import ConfigStore
31
+ from ..constants import DEFAULT_INVITE_WORKERS, PROJECT_ROLES
32
+ from ..errors import ConfigError, ErrorCode, KeboolaApiError
33
+ from ..manage_client import ManageClient
34
+ from ..models import (
35
+ BulkInviteResult,
36
+ MemberInviteRow,
37
+ ProjectInvitation,
38
+ ProjectMember,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ ManageClientFactory = Callable[[str, str], ManageClient]
44
+
45
+
46
+ def default_manage_client_factory(stack_url: str, manage_token: str) -> ManageClient:
47
+ """Construct a :class:`ManageClient` bound to ``stack_url``."""
48
+ return ManageClient(stack_url=stack_url, manage_token=manage_token)
49
+
50
+
51
+ # The Manage API returns HTTP 400 (not 422) with one of these substrings when
52
+ # a duplicate invitation/member is created. Treated as success-with-note in
53
+ # the service layer so bulk imports don't fail on idempotent re-runs.
54
+ _ALREADY_INVITED_MARKER = "already been invited"
55
+ _ALREADY_MEMBER_MARKER = "already a member"
56
+
57
+
58
+ class MemberService:
59
+ """Business logic for project members and invitations."""
60
+
61
+ def __init__(
62
+ self,
63
+ config_store: ConfigStore,
64
+ manage_client_factory: ManageClientFactory | None = None,
65
+ ) -> None:
66
+ self._config_store = config_store
67
+ self._manage_client_factory = manage_client_factory or default_manage_client_factory
68
+
69
+ # ------------------------------------------------------------------
70
+ # Public API: single-shot operations
71
+ # ------------------------------------------------------------------
72
+
73
+ def invite(
74
+ self,
75
+ *,
76
+ manage_token: str,
77
+ alias: str,
78
+ email: str,
79
+ role: str,
80
+ reason: str | None = None,
81
+ dry_run: bool = False,
82
+ ) -> dict[str, Any]:
83
+ """Invite ``email`` to the project registered under ``alias``."""
84
+ self._validate_role(role)
85
+ stack_url, project_id = self._resolve_alias(alias)
86
+
87
+ if dry_run:
88
+ return {
89
+ "status": "dry_run",
90
+ "alias": alias,
91
+ "project_id": project_id,
92
+ "email": email,
93
+ "role": role,
94
+ "reason": reason or "",
95
+ }
96
+
97
+ manage_client = self._manage_client_factory(stack_url, manage_token)
98
+ try:
99
+ return self._invite_one(manage_client, alias, project_id, email, role, reason)
100
+ finally:
101
+ manage_client.close()
102
+
103
+ def invite_bulk(
104
+ self,
105
+ *,
106
+ manage_token: str,
107
+ csv_path: Path,
108
+ default_role: str | None = None,
109
+ workers: int = DEFAULT_INVITE_WORKERS,
110
+ dry_run: bool = False,
111
+ ) -> BulkInviteResult:
112
+ """Invite every row of ``csv_path`` in parallel.
113
+
114
+ CSV must have a header row. Recognised columns (case-insensitive):
115
+ ``email`` (required), ``project`` or ``project_id`` (one required),
116
+ ``role`` (optional if ``default_role`` is given), ``reason`` (optional).
117
+ Extra columns are ignored. ``project`` values that are all-digits are
118
+ resolved as numeric project IDs without an alias lookup.
119
+ """
120
+ if default_role is not None:
121
+ self._validate_role(default_role)
122
+ rows = self._parse_invite_csv(csv_path, default_role)
123
+ if not rows:
124
+ return BulkInviteResult(total=0, succeeded=0, noop=0, failed=0, dry_run=dry_run)
125
+
126
+ if dry_run:
127
+ return self._bulk_dry_run(rows)
128
+
129
+ # Resolve every row's (stack_url, project_id) up front. A row that
130
+ # fails resolution (unknown alias, unregistered project_id) becomes a
131
+ # per-row "failed" entry; the rest of the batch still runs. Mirrors
132
+ # the partial-success contract enforced by `OrgService.refresh_tokens`.
133
+ resolved: list[tuple[dict[str, Any], str, int]] = []
134
+ upfront_failures: list[MemberInviteRow] = []
135
+ for row in rows:
136
+ try:
137
+ stack_url, project_id = self._stack_for_row(row)
138
+ resolved.append((row, stack_url, project_id))
139
+ except ConfigError as exc:
140
+ upfront_failures.append(
141
+ MemberInviteRow(
142
+ email=row["email"],
143
+ project=str(row["project"]),
144
+ role=row["role"],
145
+ status="failed",
146
+ note=str(getattr(exc, "message", exc)),
147
+ )
148
+ )
149
+
150
+ if not resolved:
151
+ return BulkInviteResult(
152
+ total=len(upfront_failures),
153
+ succeeded=0,
154
+ noop=0,
155
+ failed=len(upfront_failures),
156
+ rows=upfront_failures,
157
+ dry_run=False,
158
+ )
159
+
160
+ # All resolved rows must share a single stack URL; sending invitations
161
+ # for project A on stack X via a client bound to stack Y is a security
162
+ # bug, not a "partial-success" path.
163
+ resolved_stacks = {t[1] for t in resolved}
164
+ if len(resolved_stacks) != 1:
165
+ raise ConfigError(
166
+ f"CSV references multiple stack URLs ({sorted(resolved_stacks)}); "
167
+ "split the file by stack and run --from-csv per stack."
168
+ )
169
+ stack_url = resolved_stacks.pop()
170
+
171
+ manage_client = self._manage_client_factory(stack_url, manage_token)
172
+ results: list[MemberInviteRow] = list(upfront_failures)
173
+ try:
174
+ worker_count = max(1, min(workers, len(resolved)))
175
+ with ThreadPoolExecutor(max_workers=worker_count) as pool:
176
+ futures = [
177
+ pool.submit(self._invoke_resolved_row, manage_client, row, project_id)
178
+ for row, _, project_id in resolved
179
+ ]
180
+ for fut in as_completed(futures):
181
+ results.append(fut.result())
182
+ finally:
183
+ manage_client.close()
184
+
185
+ return BulkInviteResult(
186
+ total=len(results),
187
+ succeeded=sum(1 for r in results if r.status == "ok"),
188
+ noop=sum(1 for r in results if r.status == "noop"),
189
+ failed=sum(1 for r in results if r.status == "failed"),
190
+ rows=results,
191
+ dry_run=False,
192
+ )
193
+
194
+ def list_members(
195
+ self,
196
+ *,
197
+ manage_token: str,
198
+ alias: str,
199
+ include_pending: bool = False,
200
+ ) -> dict[str, Any]:
201
+ """Return active members (and, optionally, pending invitations)."""
202
+ stack_url, project_id = self._resolve_alias(alias)
203
+ manage_client = self._manage_client_factory(stack_url, manage_token)
204
+ try:
205
+ members_raw = manage_client.list_project_members(project_id)
206
+ members = [ProjectMember.model_validate(m) for m in members_raw]
207
+ payload: dict[str, Any] = {
208
+ "alias": alias,
209
+ "project_id": project_id,
210
+ "members": [m.model_dump(by_alias=False) for m in members],
211
+ }
212
+ if include_pending:
213
+ inv_raw = manage_client.list_project_invitations(project_id)
214
+ payload["pending_invitations"] = [
215
+ ProjectInvitation.model_validate(i).model_dump(by_alias=False) for i in inv_raw
216
+ ]
217
+ return payload
218
+ finally:
219
+ manage_client.close()
220
+
221
+ def list_invitations(
222
+ self,
223
+ *,
224
+ manage_token: str,
225
+ alias: str,
226
+ ) -> dict[str, Any]:
227
+ """Return pending invitations for ``alias``."""
228
+ stack_url, project_id = self._resolve_alias(alias)
229
+ manage_client = self._manage_client_factory(stack_url, manage_token)
230
+ try:
231
+ raw = manage_client.list_project_invitations(project_id)
232
+ return {
233
+ "alias": alias,
234
+ "project_id": project_id,
235
+ "invitations": [
236
+ ProjectInvitation.model_validate(i).model_dump(by_alias=False) for i in raw
237
+ ],
238
+ }
239
+ finally:
240
+ manage_client.close()
241
+
242
+ def cancel_invitation(
243
+ self,
244
+ *,
245
+ manage_token: str,
246
+ alias: str,
247
+ email: str,
248
+ invitation_id: int | None = None,
249
+ ) -> dict[str, Any]:
250
+ """Cancel a pending invitation. Resolves by email if no ID is supplied."""
251
+ stack_url, project_id = self._resolve_alias(alias)
252
+ manage_client = self._manage_client_factory(stack_url, manage_token)
253
+ try:
254
+ if invitation_id is None:
255
+ invitation_id = self._resolve_invitation_id(manage_client, project_id, email)
256
+ manage_client.cancel_project_invitation(project_id, invitation_id)
257
+ return {
258
+ "status": "cancelled",
259
+ "alias": alias,
260
+ "project_id": project_id,
261
+ "email": email,
262
+ "invitation_id": invitation_id,
263
+ }
264
+ finally:
265
+ manage_client.close()
266
+
267
+ def remove_member(
268
+ self,
269
+ *,
270
+ manage_token: str,
271
+ alias: str,
272
+ email: str,
273
+ ) -> dict[str, Any]:
274
+ """Remove an active member from a project."""
275
+ stack_url, project_id = self._resolve_alias(alias)
276
+ manage_client = self._manage_client_factory(stack_url, manage_token)
277
+ try:
278
+ user_id = self._resolve_member_id(manage_client, project_id, email)
279
+ manage_client.remove_project_member(project_id, user_id)
280
+ return {
281
+ "status": "removed",
282
+ "alias": alias,
283
+ "project_id": project_id,
284
+ "email": email,
285
+ "user_id": user_id,
286
+ }
287
+ finally:
288
+ manage_client.close()
289
+
290
+ def set_member_role(
291
+ self,
292
+ *,
293
+ manage_token: str,
294
+ alias: str,
295
+ email: str,
296
+ role: str,
297
+ ) -> dict[str, Any]:
298
+ """Change an existing member's role via PATCH."""
299
+ self._validate_role(role)
300
+ stack_url, project_id = self._resolve_alias(alias)
301
+ manage_client = self._manage_client_factory(stack_url, manage_token)
302
+ try:
303
+ user_id = self._resolve_member_id(manage_client, project_id, email)
304
+ updated = manage_client.update_project_member_role(project_id, user_id, role)
305
+ return {
306
+ "status": "updated",
307
+ "alias": alias,
308
+ "project_id": project_id,
309
+ "email": email,
310
+ "user_id": user_id,
311
+ "role": updated.get("role", role),
312
+ }
313
+ finally:
314
+ manage_client.close()
315
+
316
+ # ------------------------------------------------------------------
317
+ # Internal helpers
318
+ # ------------------------------------------------------------------
319
+
320
+ @staticmethod
321
+ def _validate_role(role: str) -> None:
322
+ """Defence-in-depth: command layer enforces the same whitelist via Choice."""
323
+ if role not in PROJECT_ROLES:
324
+ raise ValueError(
325
+ f"Invalid role {role!r}. Allowed roles are: {', '.join(PROJECT_ROLES)}."
326
+ )
327
+
328
+ def _resolve_alias(self, alias: str) -> tuple[str, int]:
329
+ """Look up ``alias`` in the config store and return ``(stack_url, project_id)``."""
330
+ project = self._config_store.get_project(alias)
331
+ if project is None:
332
+ raise ConfigError(
333
+ f"Project alias '{alias}' is not registered. Run `kbagent project list`."
334
+ )
335
+ if project.project_id is None:
336
+ raise ConfigError(
337
+ f"Project alias '{alias}' has no numeric project_id; "
338
+ "re-add it via `kbagent project add` to populate it."
339
+ )
340
+ return project.stack_url, project.project_id
341
+
342
+ def _stack_for_row(self, row: dict[str, Any]) -> tuple[str, int]:
343
+ """Resolve a CSV row's project field to ``(stack_url, project_id)``."""
344
+ project_field = str(row["project"]).strip()
345
+ if project_field.isdigit():
346
+ # Numeric project_id rows still need a stack_url; we infer from the
347
+ # currently-registered projects sharing that ID, falling back to
348
+ # any default.
349
+ project_id = int(project_field)
350
+ for cfg in self._config_store.load().projects.values():
351
+ if cfg.project_id == project_id:
352
+ return cfg.stack_url, project_id
353
+ raise ConfigError(
354
+ f"CSV row references project_id={project_id}, which is not registered "
355
+ "in this kbagent config; add it via `kbagent project add` so we know "
356
+ "which stack URL to use."
357
+ )
358
+ return self._resolve_alias(project_field)
359
+
360
+ @staticmethod
361
+ def _resolve_member_id(manage_client: ManageClient, project_id: int, email: str) -> int:
362
+ """Find an active member's numeric ID by email (case-insensitive match)."""
363
+ members = manage_client.list_project_members(project_id)
364
+ normalised = email.casefold()
365
+ for member in members:
366
+ if str(member.get("email", "")).casefold() == normalised:
367
+ return int(member["id"])
368
+ raise KeboolaApiError(
369
+ message=f"No active member with email {email!r} on project {project_id}.",
370
+ status_code=404,
371
+ error_code=ErrorCode.NOT_FOUND,
372
+ retryable=False,
373
+ )
374
+
375
+ @staticmethod
376
+ def _resolve_invitation_id(manage_client: ManageClient, project_id: int, email: str) -> int:
377
+ """Find a pending invitation by email."""
378
+ invitations = manage_client.list_project_invitations(project_id)
379
+ normalised = email.casefold()
380
+ for inv in invitations:
381
+ if str(inv.get("user", {}).get("email", "")).casefold() == normalised:
382
+ return int(inv["id"])
383
+ raise KeboolaApiError(
384
+ message=f"No pending invitation for email {email!r} on project {project_id}.",
385
+ status_code=404,
386
+ error_code=ErrorCode.NOT_FOUND,
387
+ retryable=False,
388
+ )
389
+
390
+ def _invite_one(
391
+ self,
392
+ manage_client: ManageClient,
393
+ project_label: str,
394
+ project_id: int,
395
+ email: str,
396
+ role: str,
397
+ reason: str | None,
398
+ ) -> dict[str, Any]:
399
+ """Single-row invitation logic shared by ``invite`` and ``invite_bulk``.
400
+
401
+ ``project_label`` is the human-readable project identifier surfaced in
402
+ the result dict's ``alias`` field. Single-shot mode passes the
403
+ registered alias; bulk mode passes the raw CSV ``project`` cell, which
404
+ may be either an alias or a numeric project ID string -- whatever the
405
+ user wrote.
406
+ """
407
+ try:
408
+ invitation = manage_client.create_project_invitation(
409
+ project_id=project_id,
410
+ email=email,
411
+ role=role,
412
+ reason=reason,
413
+ )
414
+ return {
415
+ "status": "ok",
416
+ "alias": project_label,
417
+ "project_id": project_id,
418
+ "email": email,
419
+ "role": role,
420
+ "invitation_id": invitation.get("id"),
421
+ }
422
+ except KeboolaApiError as exc:
423
+ note = self._noop_note_for(exc)
424
+ if note is None:
425
+ raise
426
+ return {
427
+ "status": "noop",
428
+ "alias": project_label,
429
+ "project_id": project_id,
430
+ "email": email,
431
+ "role": role,
432
+ "note": note,
433
+ }
434
+
435
+ def _invoke_resolved_row(
436
+ self,
437
+ manage_client: ManageClient,
438
+ row: dict[str, Any],
439
+ project_id: int,
440
+ ) -> MemberInviteRow:
441
+ """Execute one CSV row inside the bulk-invite executor.
442
+
443
+ Called only on rows whose (stack_url, project_id) was already resolved
444
+ by ``invite_bulk`` -- so the only failure path here is the API call
445
+ itself (e.g. invalid email, network error, role rejection).
446
+ """
447
+ email = row["email"]
448
+ role = row["role"]
449
+ reason = row.get("reason")
450
+ project_field = str(row["project"]).strip()
451
+ try:
452
+ outcome = self._invite_one(
453
+ manage_client, project_field, project_id, email, role, reason
454
+ )
455
+ return MemberInviteRow(
456
+ email=email,
457
+ project=project_field,
458
+ project_id=project_id,
459
+ role=role,
460
+ status=outcome["status"],
461
+ note=outcome.get("note", ""),
462
+ invitation_id=outcome.get("invitation_id"),
463
+ )
464
+ except KeboolaApiError as exc:
465
+ return MemberInviteRow(
466
+ email=email,
467
+ project=project_field,
468
+ project_id=project_id,
469
+ role=role,
470
+ status="failed",
471
+ note=str(getattr(exc, "message", exc)),
472
+ )
473
+
474
+ @staticmethod
475
+ def _noop_note_for(exc: KeboolaApiError) -> str | None:
476
+ """Return a noop reason if ``exc`` is the "already invited / member" 400."""
477
+ if exc.status_code != 400:
478
+ return None
479
+ message = exc.message or ""
480
+ if _ALREADY_INVITED_MARKER in message:
481
+ return "already_invited"
482
+ if _ALREADY_MEMBER_MARKER in message:
483
+ return "already_member"
484
+ return None
485
+
486
+ def _parse_invite_csv(self, csv_path: Path, default_role: str | None) -> list[dict[str, Any]]:
487
+ """Parse + validate a bulk-invite CSV. Returns a list of normalised dicts."""
488
+ if not csv_path.exists():
489
+ raise ConfigError(f"CSV file not found: {csv_path}")
490
+
491
+ # `utf-8-sig` strips a leading BOM if Excel produced the CSV (otherwise
492
+ # the first header reads as `email`, which fails the email-column
493
+ # check with a misleading message).
494
+ with csv_path.open("r", newline="", encoding="utf-8-sig") as fh:
495
+ reader = csv.DictReader(fh)
496
+ if reader.fieldnames is None:
497
+ raise ConfigError(f"CSV file {csv_path} has no header row.")
498
+ headers = {h.strip().lower(): h for h in reader.fieldnames if h}
499
+ if "email" not in headers:
500
+ raise ConfigError(
501
+ f"CSV file {csv_path} is missing an 'email' column. "
502
+ f"Found columns: {list(reader.fieldnames)}."
503
+ )
504
+ project_key = (
505
+ "project"
506
+ if "project" in headers
507
+ else ("project_id" if "project_id" in headers else None)
508
+ )
509
+ if project_key is None:
510
+ raise ConfigError(
511
+ f"CSV file {csv_path} must have a 'project' or 'project_id' column. "
512
+ f"Found columns: {list(reader.fieldnames)}."
513
+ )
514
+ has_role = "role" in headers
515
+ if not has_role and default_role is None:
516
+ raise ConfigError(
517
+ f"CSV file {csv_path} has no 'role' column and --default-role was not given."
518
+ )
519
+
520
+ rows: list[dict[str, Any]] = []
521
+ for line_no, raw in enumerate(reader, start=2): # header is line 1
522
+ email = (raw.get(headers["email"]) or "").strip()
523
+ project = (raw.get(headers[project_key]) or "").strip()
524
+ role = (raw.get(headers["role"]) if has_role else None) or default_role or ""
525
+ role = role.strip()
526
+ reason = (raw.get(headers["reason"]) or "").strip() if "reason" in headers else ""
527
+ if not email or not project:
528
+ raise ConfigError(
529
+ f"CSV {csv_path} line {line_no}: 'email' and '{project_key}' are both required."
530
+ )
531
+ if not role:
532
+ raise ConfigError(
533
+ f"CSV {csv_path} line {line_no}: missing role and no --default-role."
534
+ )
535
+ self._validate_role(role)
536
+ rows.append(
537
+ {
538
+ "email": email,
539
+ "project": project,
540
+ "role": role,
541
+ "reason": reason or None,
542
+ }
543
+ )
544
+ return rows
545
+
546
+ def _bulk_dry_run(self, rows: list[dict[str, Any]]) -> BulkInviteResult:
547
+ """Render a dry-run result without hitting the network.
548
+
549
+ Mirrors the live path: per-row resolution failures become per-row
550
+ failed entries; multi-stack-URL CSVs raise (matches the real-run
551
+ invariant so users don't get a "preview said ok, real run aborted"
552
+ surprise).
553
+ """
554
+ previewed: list[MemberInviteRow] = []
555
+ resolved_stacks: set[str] = set()
556
+ for row in rows:
557
+ try:
558
+ stack_url, project_id = self._stack_for_row(row)
559
+ resolved_stacks.add(stack_url)
560
+ except ConfigError as exc:
561
+ previewed.append(
562
+ MemberInviteRow(
563
+ email=row["email"],
564
+ project=str(row["project"]),
565
+ role=row["role"],
566
+ status="failed",
567
+ note=str(getattr(exc, "message", exc)),
568
+ )
569
+ )
570
+ continue
571
+ previewed.append(
572
+ MemberInviteRow(
573
+ email=row["email"],
574
+ project=str(row["project"]),
575
+ project_id=project_id,
576
+ role=row["role"],
577
+ status="ok",
578
+ note="dry_run",
579
+ )
580
+ )
581
+ if len(resolved_stacks) > 1:
582
+ raise ConfigError(
583
+ f"CSV references multiple stack URLs ({sorted(resolved_stacks)}); "
584
+ "split the file by stack and run --from-csv per stack."
585
+ )
586
+ return BulkInviteResult(
587
+ total=len(previewed),
588
+ succeeded=sum(1 for r in previewed if r.status == "ok"),
589
+ noop=0,
590
+ failed=sum(1 for r in previewed if r.status == "failed"),
591
+ rows=previewed,
592
+ dry_run=True,
593
+ )