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,1103 @@
1
+ """Workspace service - business logic for workspace lifecycle management.
2
+
3
+ Orchestrates workspace CRUD, table loading, SQL query execution via Query Service,
4
+ and high-level from-transformation workflow. Provides multi-project list and
5
+ single-project operations.
6
+ """
7
+
8
+ import csv
9
+ import io
10
+ import json
11
+ import logging
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from cryptography.hazmat.primitives import serialization
16
+ from cryptography.hazmat.primitives.asymmetric import rsa
17
+
18
+ from ..client import _collect_inline_results
19
+ from ..constants import (
20
+ BIGQUERY_WORKSPACE_LOGIN_TYPE,
21
+ QUERY_RESULTS_DEFAULT_LIMIT,
22
+ QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES,
23
+ QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES_BIGQUERY,
24
+ SNOWFLAKE_WORKSPACE_LOGIN_TYPE,
25
+ )
26
+ from ..errors import ConfigError, ErrorCode, KeboolaApiError
27
+ from ..models import ProjectConfig
28
+ from .base import BaseService
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class SnowflakeWorkspaceKeyPair:
35
+ """PEM key material for Snowflake key-pair workspace authentication."""
36
+
37
+ private_pem: str
38
+ public_pem: str
39
+
40
+
41
+ def _classify_qs_compatibility(login_type: str, backend: str) -> bool:
42
+ """Map a workspace ``(loginType, backend)`` pair to Query-Service compat.
43
+
44
+ Compatibility is keyed by BOTH backend and loginType because the same
45
+ ``default`` string means opposite things per backend: a BigQuery workspace's
46
+ ``default`` loginType IS Query-Service-compatible (verified against project
47
+ 9621 on connection.keboola.com), whereas a Snowflake legacy ``default``
48
+ workspace is NOT ('JWT token is invalid'). See the two whitelists in
49
+ ``constants`` for the empirical rationale.
50
+
51
+ Conservative whitelist semantics: returns True only for ``loginType``s
52
+ confirmed to work with POST /v2/storage/branch/{ID}/workspaces/{WS}/query.
53
+ Unknown backends fall through to the Snowflake whitelist (false negatives
54
+ over false positives).
55
+ """
56
+ if backend.lower() == "bigquery":
57
+ return login_type in QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES_BIGQUERY
58
+ return login_type in QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES
59
+
60
+
61
+ def _workspace_login_type_for_backend(backend: str) -> str | None:
62
+ """Return the loginType kbagent should request for newly created workspaces."""
63
+ normalized = backend.lower()
64
+ if normalized == "snowflake":
65
+ return SNOWFLAKE_WORKSPACE_LOGIN_TYPE
66
+ if normalized == "bigquery":
67
+ # BigQuery's Query-Service-compatible loginType. Omitting it lets the
68
+ # backend default to the same value, but requesting it explicitly keeps
69
+ # parity with keboola-mcp-server and is robust to a server-side change
70
+ # of the implicit default.
71
+ return BIGQUERY_WORKSPACE_LOGIN_TYPE
72
+ return None
73
+
74
+
75
+ def _generate_snowflake_workspace_key_pair() -> SnowflakeWorkspaceKeyPair:
76
+ """Generate the unencrypted PEM key pair required by Snowflake workspaces."""
77
+ private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
78
+ private_key_pem = private_key.private_bytes(
79
+ encoding=serialization.Encoding.PEM,
80
+ format=serialization.PrivateFormat.PKCS8,
81
+ encryption_algorithm=serialization.NoEncryption(),
82
+ ).decode("ascii")
83
+ public_key_pem = (
84
+ private_key.public_key()
85
+ .public_bytes(
86
+ encoding=serialization.Encoding.PEM,
87
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
88
+ )
89
+ .decode("ascii")
90
+ )
91
+ return SnowflakeWorkspaceKeyPair(private_pem=private_key_pem, public_pem=public_key_pem)
92
+
93
+
94
+ def _workspace_key_pair_for_backend(backend: str) -> SnowflakeWorkspaceKeyPair | None:
95
+ """Return private/public key material for backends that require it."""
96
+ if backend.lower() == "snowflake":
97
+ return _generate_snowflake_workspace_key_pair()
98
+ return None
99
+
100
+
101
+ def find_storage_workspace_for_sandbox_config(
102
+ workspaces: list[dict[str, Any]],
103
+ config_id: str,
104
+ ) -> int | None:
105
+ """Pure-function lookup: find the Storage workspace that backs a sandbox config.
106
+
107
+ A ``keboola.sandboxes`` configuration's ``parameters.id`` is the
108
+ sandbox-service internal ID, not a Storage workspace ID -- passing it to
109
+ ``GET /v2/storage/workspaces/{ID}`` returns 404 (issue #304). The real
110
+ relation goes the other direction: each Storage workspace exposes
111
+ ``configurationId`` pointing back at its sandbox config.
112
+
113
+ Extracted from ``WorkspaceService.resolve_sandbox_workspace_id`` so
114
+ ``ConfigService.get_config_detail`` can call it with a workspace list it
115
+ already has (avoiding a circular ``ConfigService -> WorkspaceService``
116
+ dependency and the extra HTTP round-trip that would otherwise pile up
117
+ in HTTP and web-UI consumers -- see issue #312).
118
+
119
+ Args:
120
+ workspaces: Raw output of ``KeboolaClient.list_workspaces()`` -- each
121
+ entry is the Storage API workspace dict (not the normalised CLI
122
+ shape).
123
+ config_id: ``keboola.sandboxes`` configuration ID.
124
+
125
+ Returns:
126
+ Storage workspace ID (int), or None if no workspace currently backs
127
+ this config (orphan sandbox, or workspace deleted but config kept
128
+ around).
129
+ """
130
+ for ws in workspaces:
131
+ if ws.get("component") == "keboola.sandboxes" and str(ws.get("configurationId", "")) == str(
132
+ config_id
133
+ ):
134
+ ws_id = ws.get("id")
135
+ if isinstance(ws_id, int):
136
+ return ws_id
137
+ try:
138
+ return int(ws_id) if ws_id is not None else None
139
+ except (TypeError, ValueError):
140
+ return None
141
+ return None
142
+
143
+
144
+ def _is_orphaned_workspace(ws: dict[str, Any], config_names: dict[str, str]) -> bool:
145
+ """Return True if a workspace has no backing keboola.sandboxes config.
146
+
147
+ A workspace is orphaned when it is tied to keboola.sandboxes (the normal
148
+ kbagent creation path) but the sandbox config no longer exists — either it
149
+ was deleted separately or was never created.
150
+ """
151
+ component_id = ws.get("component_id", "")
152
+ config_id = str(ws.get("config_id", ""))
153
+ if component_id != "keboola.sandboxes":
154
+ return False
155
+ # config_names keys are sandbox config IDs; absence means orphan
156
+ return not config_id or config_id not in config_names
157
+
158
+
159
+ def _csv_cell(value: Any) -> Any:
160
+ """Coerce one `/results` JSON cell to its CSV representation.
161
+
162
+ ``None`` -> empty field (matches the warehouse CSV export). VARIANT/ARRAY/
163
+ OBJECT (Snowflake) and STRUCT/ARRAY (BigQuery) columns arrive as native
164
+ Python ``dict``/``list``; ``csv.writer`` would otherwise emit their Python
165
+ ``repr`` (``{'k': 'v'}``), so we serialize them as compact JSON
166
+ (``{"k":"v"}``) to match the warehouse's CSV serialization. Scalars pass
167
+ through and are stringified by ``csv.writer``.
168
+ """
169
+ if value is None:
170
+ return ""
171
+ if isinstance(value, (dict, list)):
172
+ return json.dumps(value, separators=(",", ":"), ensure_ascii=False)
173
+ return value
174
+
175
+
176
+ def _rows_to_csv(columns: list[dict[str, Any]], rows: list[list[Any]]) -> str:
177
+ """Render structured columns+rows as an RFC-4180 CSV string.
178
+
179
+ Synthesized so the inline `/results` payload stays drop-in compatible with
180
+ consumers that still read ``csv_data`` (CLI preview, web UI table + export
181
+ buttons, REST). ``csv.writer`` handles quoting/escaping.
182
+ """
183
+ buffer = io.StringIO()
184
+ writer = csv.writer(buffer, lineterminator="\n")
185
+ writer.writerow([col.get("name", "") for col in columns])
186
+ for row in rows:
187
+ writer.writerow([_csv_cell(value) for value in row])
188
+ return buffer.getvalue()
189
+
190
+
191
+ class WorkspaceService(BaseService):
192
+ """Business logic for managing Keboola workspaces.
193
+
194
+ Supports:
195
+ - Workspace CRUD (create, list, detail, delete, password reset)
196
+ - Table loading into workspaces
197
+ - SQL query execution via Query Service
198
+ - High-level from-transformation workflow
199
+
200
+ Uses dependency injection for config_store and client_factory.
201
+ """
202
+
203
+ def _resolve_branch_id(self, alias: str, project: ProjectConfig) -> int:
204
+ """Resolve the effective branch ID for a project.
205
+
206
+ Uses active_branch_id if set, otherwise fetches main branch from API.
207
+
208
+ Returns:
209
+ Branch ID (int).
210
+ """
211
+ if project.active_branch_id is not None:
212
+ return project.active_branch_id
213
+
214
+ client = self._client_factory(project.stack_url, project.token)
215
+ try:
216
+ branches = client.list_dev_branches()
217
+ for branch in branches:
218
+ if branch.get("isDefault", False):
219
+ return int(branch["id"])
220
+ raise ConfigError(
221
+ f"No default branch found for project '{alias}'. "
222
+ "Set an active branch with 'kbagent branch use'."
223
+ )
224
+ finally:
225
+ client.close()
226
+
227
+ def _fetch_sandbox_config_names(
228
+ self, client: Any, branch_id: int | None = None
229
+ ) -> dict[str, str]:
230
+ """Fetch sandbox configuration names for workspace name resolution.
231
+
232
+ The workspace API returns internal names (WORKSPACE_xxxxx). The user-given
233
+ name lives on the keboola.sandboxes configuration. This method fetches those
234
+ configs and builds a config_id -> name mapping.
235
+
236
+ Returns:
237
+ Dict mapping config_id (str) to config name.
238
+ """
239
+ try:
240
+ configs = client.list_component_configs("keboola.sandboxes", branch_id=branch_id)
241
+ return {str(cfg.get("id", "")): cfg.get("name", "") for cfg in configs}
242
+ except Exception:
243
+ # Non-critical: fall back to internal workspace names
244
+ return {}
245
+
246
+ def _detect_backend(self, client: Any) -> str:
247
+ """Detect the project's default backend via token verification.
248
+
249
+ Returns:
250
+ Backend string (e.g. 'snowflake', 'bigquery').
251
+ """
252
+ token_info = client.verify_token()
253
+ return token_info.default_backend
254
+
255
+ def create_workspace(
256
+ self,
257
+ alias: str,
258
+ name: str = "",
259
+ backend: str | None = None,
260
+ read_only: bool = True,
261
+ ui_mode: bool = False,
262
+ ) -> dict[str, Any]:
263
+ """Create a new workspace.
264
+
265
+ Two modes:
266
+ - Default (headless): fast (~1s) via Storage API. Not visible in Keboola UI.
267
+ - UI mode (--ui): slower (~15s) via Queue job. Visible in UI Workspaces tab.
268
+
269
+ IMPORTANT: Credentials are only available on creation (headless mode).
270
+ Snowflake returns a generated private key; password-based workspaces
271
+ return a password. In UI mode, password must be retrieved via
272
+ 'workspace password' command.
273
+
274
+ Args:
275
+ alias: Project alias.
276
+ name: Human-readable name for the workspace.
277
+ backend: Workspace backend. Auto-detected from project if None.
278
+ read_only: Whether the workspace has read-only storage access.
279
+ ui_mode: If True, create via Queue job (visible in Keboola UI).
280
+
281
+ Returns:
282
+ Dict with workspace details including connection credentials.
283
+ """
284
+ projects = self.resolve_projects([alias])
285
+ project = projects[alias]
286
+ branch_id = self._resolve_branch_id(alias, project)
287
+ effective_name = name or f"kbagent-{alias}"
288
+
289
+ client = self._client_factory(project.stack_url, project.token)
290
+ try:
291
+ effective_backend = backend or self._detect_backend(client)
292
+ # Step 1: Create keboola.sandboxes config (in the correct branch)
293
+ sandbox_config = client.create_sandbox_config(
294
+ name=effective_name,
295
+ description="Created by kbagent CLI",
296
+ branch_id=branch_id,
297
+ )
298
+ config_id = sandbox_config.get("id", "")
299
+
300
+ if ui_mode:
301
+ return self._create_workspace_via_job(
302
+ client,
303
+ alias,
304
+ effective_name,
305
+ config_id,
306
+ effective_backend,
307
+ )
308
+ else:
309
+ return self._create_workspace_direct(
310
+ client,
311
+ alias,
312
+ effective_name,
313
+ config_id,
314
+ branch_id,
315
+ effective_backend,
316
+ read_only,
317
+ )
318
+ finally:
319
+ client.close()
320
+
321
+ def _create_workspace_direct(
322
+ self,
323
+ client: Any,
324
+ alias: str,
325
+ name: str,
326
+ config_id: str,
327
+ branch_id: int,
328
+ backend: str,
329
+ read_only: bool,
330
+ ) -> dict[str, Any]:
331
+ """Create workspace via Storage API (fast, headless)."""
332
+ key_pair = _workspace_key_pair_for_backend(backend)
333
+ ws_data = client.create_config_workspace(
334
+ branch_id=branch_id,
335
+ component_id="keboola.sandboxes",
336
+ config_id=config_id,
337
+ backend=backend,
338
+ login_type=_workspace_login_type_for_backend(backend),
339
+ public_key=key_pair.public_pem if key_pair else None,
340
+ )
341
+
342
+ connection = ws_data.get("connection", {})
343
+ credential_label = "private key" if key_pair else "password"
344
+ result = {
345
+ "project_alias": alias,
346
+ "workspace_id": ws_data.get("id"),
347
+ "name": name,
348
+ "config_id": config_id,
349
+ "backend": connection.get("backend", backend),
350
+ "host": connection.get("host", ""),
351
+ "warehouse": connection.get("warehouse", ""),
352
+ "database": connection.get("database", ""),
353
+ "schema": connection.get("schema", ""),
354
+ "user": connection.get("user", ""),
355
+ "password": connection.get("password", ""),
356
+ "read_only": read_only,
357
+ "ui_mode": False,
358
+ "message": f"Workspace '{name}' created in project '{alias}'. "
359
+ f"Save the {credential_label} -- it cannot be retrieved later!",
360
+ }
361
+ if key_pair is not None:
362
+ result["private_key"] = key_pair.private_pem
363
+ return result
364
+
365
+ def _create_workspace_via_job(
366
+ self,
367
+ client: Any,
368
+ alias: str,
369
+ name: str,
370
+ config_id: str,
371
+ backend: str,
372
+ ) -> dict[str, Any]:
373
+ """Create workspace via Queue job (slower, visible in UI)."""
374
+ # The Queue job path does not expose a publicKey/loginType input. Keep
375
+ # returning a reset password here; headless Snowflake creates use the
376
+ # key-pair path in _create_workspace_direct().
377
+ job = client.create_job(
378
+ component_id="keboola.sandboxes",
379
+ config_id=config_id,
380
+ config_data={
381
+ "parameters": {
382
+ "task": "create",
383
+ "type": backend,
384
+ "shared": False,
385
+ },
386
+ },
387
+ )
388
+ job_id = str(job.get("id", ""))
389
+
390
+ # Wait for the job to complete
391
+ client.wait_for_queue_job(job_id)
392
+
393
+ # Find the workspace created by the job
394
+ workspaces = client.list_config_workspaces(
395
+ branch_id=int(job.get("branchId", 0)),
396
+ component_id="keboola.sandboxes",
397
+ config_id=config_id,
398
+ )
399
+
400
+ if not workspaces:
401
+ raise KeboolaApiError(
402
+ message=f"Sandbox job completed but no workspace found for config {config_id}",
403
+ status_code=500,
404
+ error_code=ErrorCode.WORKSPACE_NOT_FOUND,
405
+ retryable=False,
406
+ )
407
+
408
+ ws_data = workspaces[0]
409
+ connection = ws_data.get("connection", {})
410
+ workspace_id = ws_data.get("id")
411
+
412
+ # Reset password so we can return it (job doesn't expose the initial password)
413
+ password = ""
414
+ try:
415
+ pw_data = client.reset_workspace_password(workspace_id)
416
+ password = pw_data.get("password", "")
417
+ except KeboolaApiError:
418
+ logger.debug("Could not reset password for workspace %s", workspace_id)
419
+
420
+ return {
421
+ "project_alias": alias,
422
+ "workspace_id": workspace_id,
423
+ "name": name,
424
+ "config_id": config_id,
425
+ "backend": connection.get("backend", backend),
426
+ "host": connection.get("host", ""),
427
+ "warehouse": connection.get("warehouse", ""),
428
+ "database": connection.get("database", ""),
429
+ "schema": connection.get("schema", ""),
430
+ "user": connection.get("user", ""),
431
+ "password": password,
432
+ "read_only": True,
433
+ "ui_mode": True,
434
+ "message": (
435
+ f"Workspace '{name}' ({workspace_id}) created in project '{alias}' (visible in UI). "
436
+ "Save the password -- it cannot be retrieved later!"
437
+ ),
438
+ }
439
+
440
+ def resolve_sandbox_workspace_id(
441
+ self,
442
+ alias: str,
443
+ config_id: str,
444
+ branch_id: int | None = None,
445
+ ) -> int | None:
446
+ """Map a ``keboola.sandboxes`` config ID to its Storage workspace ID.
447
+
448
+ The sandbox config's ``parameters.id`` field looks like a Storage
449
+ workspace ID but is actually a sandbox-service-internal handle --
450
+ passing it to ``GET /v2/storage/workspaces/{ID}`` returns 404 (issue
451
+ #304). The real mapping lives the other way around: each Storage
452
+ workspace exposes ``configurationId`` pointing back at its sandbox
453
+ config. This helper walks the workspace list to find the matching
454
+ one.
455
+
456
+ Args:
457
+ alias: Project alias.
458
+ config_id: ``keboola.sandboxes`` configuration ID.
459
+ branch_id: Branch to query. When None, uses the project's
460
+ resolved branch (production fallback).
461
+
462
+ Returns:
463
+ Storage workspace ID, or None if no workspace is currently
464
+ backed by this config (orphan sandbox, or workspace deleted but
465
+ config kept around).
466
+ """
467
+ projects = self.resolve_projects([alias])
468
+ project = projects[alias]
469
+ effective_branch = (
470
+ branch_id if branch_id is not None else self._resolve_branch_id(alias, project)
471
+ )
472
+
473
+ client = self._client_factory(project.stack_url, project.token)
474
+ try:
475
+ workspaces = client.list_workspaces(branch_id=effective_branch)
476
+ finally:
477
+ client.close()
478
+
479
+ return find_storage_workspace_for_sandbox_config(workspaces, config_id)
480
+
481
+ def list_workspaces(
482
+ self,
483
+ aliases: list[str] | None = None,
484
+ orphaned_only: bool = False,
485
+ branch_id: int | None = None,
486
+ qs_compatible_only: bool = False,
487
+ ) -> dict[str, Any]:
488
+ """List workspaces across one or multiple projects.
489
+
490
+ Args:
491
+ aliases: Project aliases to query. None means all projects.
492
+ orphaned_only: If True, return only orphaned workspaces — those
493
+ whose keboola.sandboxes config no longer exists.
494
+ branch_id: When set, list workspaces from this specific dev branch
495
+ (`/v2/storage/branch/{ID}/workspaces`). When None, the
496
+ production endpoint is used; callers wanting to honour the
497
+ alias's pinned branch should resolve it via
498
+ ``resolve_branch()`` in the command layer before calling.
499
+ Only valid with a single alias (mirrors storage commands).
500
+ qs_compatible_only: If True, return only workspaces whose
501
+ ``login_type`` is in ``QUERY_SERVICE_COMPATIBLE_LOGIN_TYPES``
502
+ AND that are read-only -- the canonical shape for a Streamlit
503
+ / Quix data-app reading via the Query Service.
504
+
505
+ Returns:
506
+ Dict with "workspaces" and "errors" lists. Each workspace entry
507
+ carries ``login_type``, ``read_only``, ``qs_compatible``,
508
+ ``database`` and ``warehouse`` so callers can pick a
509
+ data-app-compatible workspace without spawning a probe query
510
+ (closes #304).
511
+ """
512
+ if branch_id is not None and (aliases is None or len(aliases) != 1):
513
+ raise ConfigError(
514
+ "branch_id requires exactly one alias (a branch ID is scoped to a single project)."
515
+ )
516
+
517
+ projects = self.resolve_projects(aliases)
518
+
519
+ def worker(
520
+ alias: str, project: ProjectConfig
521
+ ) -> tuple[str, list[dict[str, Any]], bool] | tuple[str, dict[str, str]]:
522
+ client = self._client_factory(project.stack_url, project.token)
523
+ try:
524
+ effective_branch = (
525
+ branch_id if branch_id is not None else self._resolve_branch_id(alias, project)
526
+ )
527
+ raw_workspaces = client.list_workspaces(branch_id=effective_branch)
528
+
529
+ # Fetch sandbox configs to resolve user-given names
530
+ config_names = self._fetch_sandbox_config_names(client, effective_branch)
531
+
532
+ workspaces: list[dict[str, Any]] = []
533
+ for ws in raw_workspaces:
534
+ connection = ws.get("connection", {})
535
+ config_id = ws.get("configurationId") or ""
536
+ component_id = ws.get("component") or ""
537
+ login_type = connection.get("loginType", "") or ""
538
+ backend = connection.get("backend", "") or ""
539
+ read_only = bool(ws.get("readOnlyStorageAccess", False))
540
+ entry = {
541
+ "project_alias": alias,
542
+ "id": ws.get("id"),
543
+ "name": config_names.get(str(config_id), ws.get("name", "")),
544
+ "backend": backend,
545
+ "host": connection.get("host", ""),
546
+ "database": connection.get("database", ""),
547
+ "warehouse": connection.get("warehouse", ""),
548
+ "schema": connection.get("schema", ""),
549
+ "user": connection.get("user", ""),
550
+ "created": ws.get("created", ""),
551
+ "component_id": component_id,
552
+ "config_id": config_id,
553
+ "login_type": login_type,
554
+ "read_only": read_only,
555
+ "qs_compatible": _classify_qs_compatibility(login_type, backend),
556
+ }
557
+ if orphaned_only:
558
+ if _is_orphaned_workspace(entry, config_names):
559
+ workspaces.append(entry)
560
+ elif qs_compatible_only:
561
+ if entry["qs_compatible"] and entry["read_only"]:
562
+ workspaces.append(entry)
563
+ else:
564
+ workspaces.append(entry)
565
+ return (alias, workspaces, True)
566
+ except KeboolaApiError as exc:
567
+ return (
568
+ alias,
569
+ {
570
+ "project_alias": alias,
571
+ "error_code": exc.error_code,
572
+ "message": exc.message,
573
+ },
574
+ )
575
+ except Exception as exc:
576
+ return (
577
+ alias,
578
+ {
579
+ "project_alias": alias,
580
+ "error_code": "UNEXPECTED_ERROR",
581
+ "message": str(exc),
582
+ },
583
+ )
584
+ finally:
585
+ client.close()
586
+
587
+ successes, errors = self._run_parallel(projects, worker)
588
+
589
+ all_workspaces: list[dict[str, Any]] = []
590
+ for _alias, workspaces, _ok in successes:
591
+ all_workspaces.extend(workspaces)
592
+
593
+ all_workspaces.sort(key=lambda w: (w["project_alias"], w.get("id", 0)))
594
+ errors.sort(key=lambda e: e.get("project_alias", ""))
595
+
596
+ return {
597
+ "workspaces": all_workspaces,
598
+ "errors": errors,
599
+ }
600
+
601
+ def gc_workspaces(
602
+ self,
603
+ aliases: list[str] | None = None,
604
+ dry_run: bool = False,
605
+ ) -> dict[str, Any]:
606
+ """Delete all orphaned workspaces (workspace GC).
607
+
608
+ An orphan is a keboola.sandboxes-backed workspace whose config no longer
609
+ exists. Reuses delete_workspace for each orphan so the sandbox config
610
+ cleanup path is also exercised.
611
+
612
+ Args:
613
+ aliases: Project aliases to query. None means all projects.
614
+ dry_run: If True, list orphans without deleting.
615
+
616
+ Returns:
617
+ Dict with dry_run flag, would_delete/deleted list, errors, count.
618
+ """
619
+ orphan_result = self.list_workspaces(aliases=aliases, orphaned_only=True)
620
+ orphans = orphan_result["workspaces"]
621
+ list_errors = orphan_result["errors"]
622
+
623
+ if dry_run:
624
+ return {
625
+ "dry_run": True,
626
+ "would_delete": orphans,
627
+ "count": len(orphans),
628
+ "errors": list_errors,
629
+ "message": (
630
+ f"DRY RUN: {len(orphans)} orphaned workspace(s) would be deleted."
631
+ + (" No errors." if not list_errors else f" {len(list_errors)} list error(s).")
632
+ ),
633
+ }
634
+
635
+ deleted: list[dict[str, Any]] = []
636
+ delete_errors: list[dict[str, Any]] = []
637
+ for ws in orphans:
638
+ try:
639
+ self.delete_workspace(alias=ws["project_alias"], workspace_id=ws["id"])
640
+ deleted.append(ws)
641
+ except Exception as exc:
642
+ # Full traceback goes to the logger (observability for unexpected
643
+ # errors like AttributeError); user-facing flow is unchanged.
644
+ logger.exception(
645
+ "Failed to delete orphaned workspace %s in project %s",
646
+ ws["id"],
647
+ ws["project_alias"],
648
+ )
649
+ delete_errors.append(
650
+ {
651
+ "workspace_id": ws["id"],
652
+ "project_alias": ws["project_alias"],
653
+ "error": str(exc),
654
+ }
655
+ )
656
+
657
+ all_errors = list_errors + delete_errors
658
+ return {
659
+ "dry_run": False,
660
+ "deleted": deleted,
661
+ "errors": all_errors,
662
+ "count_deleted": len(deleted),
663
+ "count_errors": len(all_errors),
664
+ "message": (
665
+ f"GC complete: {len(deleted)} orphaned workspace(s) deleted"
666
+ + (f", {len(all_errors)} error(s)." if all_errors else ".")
667
+ ),
668
+ }
669
+
670
+ def get_workspace(
671
+ self,
672
+ alias: str,
673
+ workspace_id: int,
674
+ branch_id: int | None = None,
675
+ ) -> dict[str, Any]:
676
+ """Get workspace details (password NOT included).
677
+
678
+ Args:
679
+ alias: Project alias.
680
+ workspace_id: Workspace ID.
681
+ branch_id: When set, query the branch-scoped endpoint
682
+ ``/v2/storage/branch/{ID}/workspaces/{WS}``. When None, falls
683
+ back to the project's active branch (or the production
684
+ endpoint if no active branch is pinned). Explicit None vs.
685
+ resolved value lets the command layer surface a "production
686
+ branch used" notice without changing the service signature.
687
+
688
+ Returns:
689
+ Dict with workspace details including ``login_type``,
690
+ ``read_only`` and ``qs_compatible`` so callers can pick a
691
+ Query-Service-compatible workspace without firing a probe query
692
+ (closes #304).
693
+ """
694
+ projects = self.resolve_projects([alias])
695
+ project = projects[alias]
696
+ effective_branch = (
697
+ branch_id if branch_id is not None else self._resolve_branch_id(alias, project)
698
+ )
699
+
700
+ client = self._client_factory(project.stack_url, project.token)
701
+ try:
702
+ ws_data = client.get_workspace(workspace_id, branch_id=effective_branch)
703
+ finally:
704
+ client.close()
705
+
706
+ connection = ws_data.get("connection", {})
707
+ login_type = connection.get("loginType", "") or ""
708
+ backend = connection.get("backend", "") or ""
709
+ return {
710
+ "project_alias": alias,
711
+ "workspace_id": ws_data.get("id"),
712
+ "backend": backend,
713
+ "host": connection.get("host", ""),
714
+ "warehouse": connection.get("warehouse", ""),
715
+ "database": connection.get("database", ""),
716
+ "schema": connection.get("schema", ""),
717
+ "user": connection.get("user", ""),
718
+ "created": ws_data.get("created", ""),
719
+ "login_type": login_type,
720
+ "read_only": bool(ws_data.get("readOnlyStorageAccess", False)),
721
+ "qs_compatible": _classify_qs_compatibility(login_type, backend),
722
+ "component_id": ws_data.get("component", "") or "",
723
+ "config_id": ws_data.get("configurationId", "") or "",
724
+ }
725
+
726
+ def delete_workspace(self, alias: str, workspace_id: int) -> dict[str, Any]:
727
+ """Delete a workspace and its associated sandboxes config (if any).
728
+
729
+ Args:
730
+ alias: Project alias.
731
+ workspace_id: Workspace ID.
732
+
733
+ Returns:
734
+ Dict confirming the deletion.
735
+ """
736
+ projects = self.resolve_projects([alias])
737
+ project = projects[alias]
738
+ branch_id = self._resolve_branch_id(alias, project)
739
+
740
+ client = self._client_factory(project.stack_url, project.token)
741
+ try:
742
+ # Get workspace details to find associated config
743
+ config_id = None
744
+ component = None
745
+ try:
746
+ ws_data = client.get_workspace(workspace_id, branch_id=branch_id)
747
+ component = ws_data.get("component")
748
+ config_id = ws_data.get("configurationId")
749
+ except KeboolaApiError:
750
+ pass # Workspace might not exist, proceed with delete
751
+
752
+ # Delete the workspace
753
+ client.delete_workspace(workspace_id, branch_id=branch_id)
754
+
755
+ # Clean up associated sandboxes config (in the correct branch)
756
+ if config_id and component == "keboola.sandboxes":
757
+ try:
758
+ client.delete_config("keboola.sandboxes", config_id, branch_id=branch_id)
759
+ except KeboolaApiError:
760
+ logger.debug("Could not delete sandbox config %s", config_id)
761
+ finally:
762
+ client.close()
763
+
764
+ return {
765
+ "project_alias": alias,
766
+ "workspace_id": workspace_id,
767
+ "message": f"Workspace {workspace_id} deleted from project '{alias}'.",
768
+ }
769
+
770
+ def reset_password(self, alias: str, workspace_id: int) -> dict[str, Any]:
771
+ """Reset workspace password.
772
+
773
+ Args:
774
+ alias: Project alias.
775
+ workspace_id: Workspace ID.
776
+
777
+ Returns:
778
+ Dict with the new password.
779
+ """
780
+ projects = self.resolve_projects([alias])
781
+ project = projects[alias]
782
+ branch_id = self._resolve_branch_id(alias, project)
783
+
784
+ client = self._client_factory(project.stack_url, project.token)
785
+ try:
786
+ result = client.reset_workspace_password(workspace_id, branch_id=branch_id)
787
+ finally:
788
+ client.close()
789
+
790
+ return {
791
+ "project_alias": alias,
792
+ "workspace_id": workspace_id,
793
+ "password": result.get("password", ""),
794
+ "message": (
795
+ f"Password reset for workspace {workspace_id} in project '{alias}'. "
796
+ "Save the new password -- it cannot be retrieved later!"
797
+ ),
798
+ }
799
+
800
+ def load_tables(
801
+ self,
802
+ alias: str,
803
+ workspace_id: int,
804
+ tables: list[str],
805
+ preserve: bool = False,
806
+ ) -> dict[str, Any]:
807
+ """Load tables into a workspace.
808
+
809
+ Builds table mapping from table IDs, using the last segment as the
810
+ destination name. Waits for the async storage job to complete.
811
+
812
+ Args:
813
+ alias: Project alias.
814
+ workspace_id: Workspace ID.
815
+ tables: List of table IDs (e.g. "in.c-bucket.table-name").
816
+ preserve: If True, keep existing tables in the workspace.
817
+
818
+ Returns:
819
+ Dict with load job results.
820
+ """
821
+ projects = self.resolve_projects([alias])
822
+ project = projects[alias]
823
+ branch_id = self._resolve_branch_id(alias, project)
824
+
825
+ # Build table load definitions
826
+ table_defs: list[dict[str, Any]] = []
827
+ for table_id in tables:
828
+ # Use last part of table ID as destination name
829
+ parts = table_id.split(".")
830
+ destination = parts[-1] if parts else table_id
831
+ table_defs.append(
832
+ {
833
+ "source": table_id,
834
+ "destination": destination,
835
+ }
836
+ )
837
+
838
+ client = self._client_factory(project.stack_url, project.token)
839
+ try:
840
+ job_result = client.load_workspace_tables(
841
+ workspace_id, table_defs, branch_id=branch_id, preserve=preserve
842
+ )
843
+ finally:
844
+ client.close()
845
+
846
+ return {
847
+ "project_alias": alias,
848
+ "workspace_id": workspace_id,
849
+ "tables_loaded": len(tables),
850
+ "table_ids": tables,
851
+ "job_id": job_result.get("id"),
852
+ "job_status": job_result.get("status", ""),
853
+ "message": f"Loaded {len(tables)} table(s) into workspace {workspace_id}.",
854
+ }
855
+
856
+ def execute_query(
857
+ self,
858
+ alias: str,
859
+ workspace_id: int,
860
+ sql: str,
861
+ transactional: bool = False,
862
+ full: bool = False,
863
+ limit: int = QUERY_RESULTS_DEFAULT_LIMIT,
864
+ ) -> dict[str, Any]:
865
+ """Execute SQL query in a workspace via Query Service.
866
+
867
+ Submits the query, polls until complete, and fetches results for each
868
+ statement. Two retrieval paths:
869
+
870
+ * Default (``full=False``): the fast inline ``GET .../results`` path.
871
+ Reads the already-computed result set as JSON (no warehouse UNLOAD /
872
+ CSV-file materialization), paginated up to ``limit`` rows. Each
873
+ statement carries structured ``columns`` + ``rows`` and a synthesized
874
+ ``csv_data`` (drop-in for the legacy CSV-string consumers).
875
+ * ``full=True``: the legacy ``GET .../export?fileType=csv`` path, which
876
+ materializes the *complete* result set as a CSV file. Slower, but not
877
+ capped at ``limit`` -- use it when you need every row.
878
+
879
+ Args:
880
+ alias: Project alias.
881
+ workspace_id: Workspace ID.
882
+ sql: SQL statement(s) to execute.
883
+ transactional: Whether to wrap in a transaction.
884
+ full: Fetch the complete result set via the CSV export path instead
885
+ of the fast inline path.
886
+ limit: Max rows to fetch via the fast inline path (ignored when
887
+ ``full`` is True).
888
+
889
+ Returns:
890
+ Dict with query results.
891
+ """
892
+ projects = self.resolve_projects([alias])
893
+ project = projects[alias]
894
+ branch_id = self._resolve_branch_id(alias, project)
895
+
896
+ client = self._client_factory(project.stack_url, project.token)
897
+ try:
898
+ # Submit query
899
+ query_job = client.submit_query(
900
+ branch_id=branch_id,
901
+ workspace_id=workspace_id,
902
+ statements=[sql],
903
+ transactional=transactional,
904
+ )
905
+
906
+ query_job_id = str(query_job.get("queryJobId", query_job.get("id", "")))
907
+
908
+ # Wait for completion
909
+ completed_job = client.wait_for_query_job(query_job_id)
910
+
911
+ # Fetch results for each statement
912
+ results: list[dict[str, Any]] = []
913
+ statements = completed_job.get("statements", [])
914
+ for stmt in statements:
915
+ stmt_id = str(stmt.get("id", ""))
916
+ status = stmt.get("status", "")
917
+ num_rows = stmt.get("numberOfRows", stmt.get("resultRows", 0))
918
+ result_entry: dict[str, Any] = {
919
+ "statement_id": stmt_id,
920
+ "status": status,
921
+ "rows_affected": num_rows,
922
+ }
923
+
924
+ # Only statements that produced a result set carry rows.
925
+ if status == "completed" and num_rows > 0:
926
+ self._attach_statement_results(
927
+ result_entry, client, query_job_id, stmt_id, full=full, limit=limit
928
+ )
929
+
930
+ results.append(result_entry)
931
+
932
+ return {
933
+ "project_alias": alias,
934
+ "workspace_id": workspace_id,
935
+ "branch_id": branch_id,
936
+ "query_job_id": query_job_id,
937
+ "status": completed_job.get("status", ""),
938
+ "statements": results,
939
+ "message": f"Query executed in workspace {workspace_id}.",
940
+ }
941
+ finally:
942
+ client.close()
943
+
944
+ @staticmethod
945
+ def _attach_statement_results(
946
+ result_entry: dict[str, Any],
947
+ client: Any,
948
+ query_job_id: str,
949
+ stmt_id: str,
950
+ *,
951
+ full: bool,
952
+ limit: int,
953
+ ) -> None:
954
+ """Populate a statement's result entry, fast inline path or full export.
955
+
956
+ Failures are swallowed (debug-logged): a result-fetch error must not
957
+ sink an otherwise-successful query -- the statement still reports its
958
+ status and row count, just without the data payload.
959
+ """
960
+ try:
961
+ if full:
962
+ result_entry["csv_data"] = client.export_query_results(query_job_id, stmt_id)
963
+ return
964
+ inline = _collect_inline_results(client, query_job_id, stmt_id, limit)
965
+ result_entry["columns"] = inline.columns
966
+ result_entry["rows"] = inline.rows
967
+ result_entry["row_count"] = len(inline.rows)
968
+ result_entry["total_rows"] = inline.total_rows
969
+ result_entry["truncated"] = inline.truncated
970
+ # Synthesize csv_data so legacy consumers (web UI table/export, CLI
971
+ # preview) keep working without a format change.
972
+ result_entry["csv_data"] = _rows_to_csv(inline.columns, inline.rows)
973
+ except KeboolaApiError:
974
+ logger.debug("Could not fetch results for statement %s", stmt_id)
975
+
976
+ def create_from_transformation(
977
+ self,
978
+ alias: str,
979
+ component_id: str,
980
+ config_id: str,
981
+ row_id: str | None = None,
982
+ backend: str | None = None,
983
+ preserve: bool = False,
984
+ ) -> dict[str, Any]:
985
+ """Create a workspace from a transformation config.
986
+
987
+ Reads the transformation configuration, extracts input table mappings,
988
+ creates a config-tied workspace, and loads the input tables.
989
+
990
+ Args:
991
+ alias: Project alias.
992
+ component_id: Transformation component ID (e.g. keboola.snowflake-transformation).
993
+ config_id: Configuration ID.
994
+ row_id: Optional row ID for row-based transformations.
995
+ backend: Workspace backend. Auto-detected from project if None.
996
+ preserve: If True, keep existing tables in the workspace during load.
997
+
998
+ Returns:
999
+ Dict with workspace details and loaded tables.
1000
+ """
1001
+ projects = self.resolve_projects([alias])
1002
+ project = projects[alias]
1003
+ branch_id = self._resolve_branch_id(alias, project)
1004
+
1005
+ client = self._client_factory(project.stack_url, project.token)
1006
+ try:
1007
+ effective_backend = backend or self._detect_backend(client)
1008
+
1009
+ # Read the transformation config
1010
+ config_data = client.get_config_detail(component_id, config_id)
1011
+
1012
+ # Extract input mapping from configuration
1013
+ configuration = config_data.get("configuration", {})
1014
+
1015
+ # If row_id specified, find the row
1016
+ if row_id:
1017
+ rows = config_data.get("rows", [])
1018
+ target_row = None
1019
+ for row in rows:
1020
+ if str(row.get("id", "")) == str(row_id):
1021
+ target_row = row
1022
+ break
1023
+ if target_row is None:
1024
+ raise ConfigError(
1025
+ f"Row '{row_id}' not found in config '{config_id}' "
1026
+ f"of component '{component_id}'."
1027
+ )
1028
+ configuration = target_row.get("configuration", {})
1029
+
1030
+ storage = configuration.get("storage", {})
1031
+ input_tables = storage.get("input", {}).get("tables", [])
1032
+
1033
+ if not input_tables:
1034
+ raise ConfigError(
1035
+ f"No input tables found in transformation config '{config_id}'. "
1036
+ "The configuration may not have input mapping defined."
1037
+ )
1038
+
1039
+ # Create config-tied workspace
1040
+ key_pair = _workspace_key_pair_for_backend(effective_backend)
1041
+ ws_data = client.create_config_workspace(
1042
+ branch_id=branch_id,
1043
+ component_id=component_id,
1044
+ config_id=config_id,
1045
+ backend=effective_backend,
1046
+ login_type=_workspace_login_type_for_backend(effective_backend),
1047
+ public_key=key_pair.public_pem if key_pair else None,
1048
+ )
1049
+
1050
+ workspace_id = ws_data.get("id")
1051
+ # create_config_workspace always returns an id for the new workspace
1052
+ assert workspace_id is not None
1053
+ connection = ws_data.get("connection", {})
1054
+
1055
+ # Build table load definitions from input mapping
1056
+ table_defs: list[dict[str, Any]] = []
1057
+ source_tables: list[str] = []
1058
+ for table in input_tables:
1059
+ source = table.get("source", "")
1060
+ destination = table.get("destination", "")
1061
+ if source:
1062
+ source_tables.append(source)
1063
+ entry: dict[str, Any] = {"source": source, "destination": destination}
1064
+ # Pass through columns, where_column, where_values if present
1065
+ if table.get("columns"):
1066
+ entry["columns"] = table["columns"]
1067
+ if table.get("where_column"):
1068
+ entry["where_column"] = table["where_column"]
1069
+ if table.get("where_values"):
1070
+ entry["where_values"] = table["where_values"]
1071
+ table_defs.append(entry)
1072
+
1073
+ # Load tables into workspace
1074
+ if table_defs:
1075
+ client.load_workspace_tables(
1076
+ workspace_id, table_defs, branch_id=branch_id, preserve=preserve
1077
+ )
1078
+
1079
+ credential_label = "private key" if key_pair else "password"
1080
+ result = {
1081
+ "project_alias": alias,
1082
+ "workspace_id": workspace_id,
1083
+ "branch_id": branch_id,
1084
+ "component_id": component_id,
1085
+ "config_id": config_id,
1086
+ "row_id": row_id,
1087
+ "backend": connection.get("backend", backend),
1088
+ "host": connection.get("host", ""),
1089
+ "warehouse": connection.get("warehouse", ""),
1090
+ "database": connection.get("database", ""),
1091
+ "schema": connection.get("schema", ""),
1092
+ "user": connection.get("user", ""),
1093
+ "password": connection.get("password", ""),
1094
+ "tables_loaded": source_tables,
1095
+ "message": f"Workspace {workspace_id} created from transformation "
1096
+ f"'{config_id}' with {len(source_tables)} table(s) loaded. "
1097
+ f"Save the {credential_label} -- it cannot be retrieved later!",
1098
+ }
1099
+ if key_pair is not None:
1100
+ result["private_key"] = key_pair.private_pem
1101
+ return result
1102
+ finally:
1103
+ client.close()