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,248 @@
1
+ """Error types and helpers for Keboola Agent CLI."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ErrorCode(StrEnum):
7
+ """Stable machine-readable error codes emitted by kbagent.
8
+
9
+ ``str`` mixin means values compare equal to their plain-string equivalents
10
+ and serialise as plain strings in JSON output -- no wire-format change.
11
+
12
+ Versioning: adding a new code = minor bump; renaming / removing = major bump.
13
+ """
14
+
15
+ # Auth / access
16
+ INVALID_TOKEN = "INVALID_TOKEN"
17
+ ACCESS_DENIED = "ACCESS_DENIED"
18
+ PERMISSION_DENIED = "PERMISSION_DENIED"
19
+ MISSING_MASTER_TOKEN = "MISSING_MASTER_TOKEN"
20
+ UNAUTHORIZED = "UNAUTHORIZED" # Bearer-auth rejection by `kbagent serve` (0.40.0+)
21
+
22
+ # Network / transport
23
+ TIMEOUT = "TIMEOUT"
24
+ CONNECTION_ERROR = "CONNECTION_ERROR"
25
+ RETRY_EXHAUSTED = "RETRY_EXHAUSTED"
26
+
27
+ # API / generic
28
+ API_ERROR = "API_ERROR"
29
+ NOT_FOUND = "NOT_FOUND"
30
+ ALREADY_EXISTS = "ALREADY_EXISTS"
31
+ VALIDATION_ERROR = "VALIDATION_ERROR"
32
+ INVALID_ARGUMENT = "INVALID_ARGUMENT"
33
+ INVALID_FORMAT = "INVALID_FORMAT"
34
+ USAGE_ERROR = "USAGE_ERROR"
35
+ MISSING_PARAMETER = "MISSING_PARAMETER"
36
+ UNKNOWN_ERROR = "UNKNOWN_ERROR"
37
+ # `kbagent serve` HTTP envelope (0.40.0+)
38
+ HTTP_ERROR = "HTTP_ERROR" # Generic HTTP-layer error (Starlette HTTPException passthrough)
39
+ INTERNAL_ERROR = "INTERNAL_ERROR" # Uncaught exception inside a route handler
40
+
41
+ # Configuration
42
+ CONFIG_ERROR = "CONFIG_ERROR"
43
+ NOT_INITIALIZED = "NOT_INITIALIZED"
44
+ INIT_ERROR = "INIT_ERROR"
45
+
46
+ # Jobs
47
+ QUEUE_JOB_FAILED = "QUEUE_JOB_FAILED"
48
+ QUEUE_JOB_TIMEOUT = "QUEUE_JOB_TIMEOUT"
49
+ STORAGE_JOB_FAILED = "STORAGE_JOB_FAILED"
50
+ STORAGE_JOB_TIMEOUT = "STORAGE_JOB_TIMEOUT"
51
+ QUERY_JOB_FAILED = "QUERY_JOB_FAILED"
52
+ QUERY_JOB_TIMEOUT = "QUERY_JOB_TIMEOUT"
53
+
54
+ # Variables
55
+ NO_VARIABLE_ROWS = "NO_VARIABLE_ROWS"
56
+ MALFORMED_VARIABLES_ROW = "MALFORMED_VARIABLES_ROW"
57
+
58
+ # Storage
59
+ UPLOAD_FAILED = "UPLOAD_FAILED"
60
+ EXPORT_EMPTY_MANIFEST = "EXPORT_EMPTY_MANIFEST"
61
+ EXPORT_NO_FILE = "EXPORT_NO_FILE"
62
+ EXPORT_NO_URL = "EXPORT_NO_URL"
63
+ NOT_SLICED = "NOT_SLICED"
64
+ FILE_NO_URL = "FILE_NO_URL"
65
+
66
+ # I/O
67
+ FILE_NOT_FOUND = "FILE_NOT_FOUND"
68
+ DIR_NOT_FOUND = "DIR_NOT_FOUND"
69
+ READ_ERROR = "READ_ERROR"
70
+ WRITE_ERROR = "WRITE_ERROR"
71
+ INPUT_ERROR = "INPUT_ERROR"
72
+
73
+ # Lineage
74
+ NODE_NOT_FOUND = "NODE_NOT_FOUND"
75
+
76
+ # Sharing
77
+ INVALID_SHARING_TYPE = "INVALID_SHARING_TYPE"
78
+ NOT_LINKED_BUCKET = "NOT_LINKED_BUCKET"
79
+
80
+ # KAI (AI Service)
81
+ KAI_ERROR = "KAI_ERROR"
82
+ KAI_NOT_ENABLED = "KAI_NOT_ENABLED"
83
+
84
+ # Workspace / Query
85
+ MISSING_QUERY = "MISSING_QUERY"
86
+ WORKSPACE_NOT_FOUND = "WORKSPACE_NOT_FOUND"
87
+
88
+ # Sync
89
+ PARENT_CONFIG_NOT_TRACKED = "PARENT_CONFIG_NOT_TRACKED"
90
+ VARIABLE_LINK_UNRESOLVED = "VARIABLE_LINK_UNRESOLVED"
91
+ SYNC_CONFLICT = "SYNC_CONFLICT"
92
+
93
+ # Encryption
94
+ ENCRYPTION_FAILED = "ENCRYPTION_FAILED"
95
+
96
+ # Job / queue (extensions from 0.22.0)
97
+ JOB_TIMEOUT_TERMINATED = "JOB_TIMEOUT_TERMINATED"
98
+
99
+ # Flow (new in 0.22.0)
100
+ SCHEDULE_DELETE_FAILED = "SCHEDULE_DELETE_FAILED"
101
+ # Conditional-flow validation (replaces INVALID_FLOW_DAG; since 0.57.0)
102
+ INVALID_FLOW_DEFINITION = "INVALID_FLOW_DEFINITION"
103
+
104
+ # Data apps (new in 0.27.0)
105
+ DATA_APP_BUILD_FAILED = "DATA_APP_BUILD_FAILED"
106
+ DATA_APP_DEPLOY_TIMEOUT = "DATA_APP_DEPLOY_TIMEOUT"
107
+ DATA_APP_INVALID_GIT = "DATA_APP_INVALID_GIT"
108
+
109
+ # Data apps - secrets + validate-repo (new in 0.28.0)
110
+ DATA_APP_INVALID_SECRET = "DATA_APP_INVALID_SECRET"
111
+ DATA_APP_INVALID_REPO = "DATA_APP_INVALID_REPO"
112
+ DATA_APP_REPO_VALIDATION_BLOCKING = "DATA_APP_REPO_VALIDATION_BLOCKING"
113
+
114
+ # Developer Portal (since 0.48.0)
115
+ DP_LOGIN_FAILED = "DP_LOGIN_FAILED"
116
+ DP_MFA_REQUIRED = "DP_MFA_REQUIRED"
117
+ DP_APP_NOT_FOUND = "DP_APP_NOT_FOUND"
118
+ DP_PUBLISH_REQUIREMENTS_MISSING = "DP_PUBLISH_REQUIREMENTS_MISSING"
119
+ DP_ICON_UPLOAD_FAILED = "DP_ICON_UPLOAD_FAILED"
120
+
121
+
122
+ def mask_token(token: str) -> str:
123
+ """Mask a Keboola Storage API token for safe display.
124
+
125
+ Preserves the prefix (part before the first dash) and the last 4 characters,
126
+ replacing the middle with '...'.
127
+
128
+ Examples:
129
+ mask_token("901-55555-fakeTestTokenDoNotUseXXXXXXXX")
130
+ -> "901-...XXXX"
131
+
132
+ mask_token("abc") -> "***"
133
+ mask_token("") -> "***"
134
+ """
135
+ if len(token) < 8:
136
+ return "***"
137
+
138
+ dash_index = token.find("-")
139
+ if dash_index == -1 or dash_index >= len(token) - 4:
140
+ return "***"
141
+
142
+ prefix = token[:dash_index]
143
+ last4 = token[-4:]
144
+ return f"{prefix}-...{last4}"
145
+
146
+
147
+ class KeboolaApiError(Exception):
148
+ """Raised when a Keboola API call fails.
149
+
150
+ Optional ``details`` payload lets the service layer attach structured
151
+ context (e.g. a fetched log tail, the remote-cancelled job dict) that
152
+ the command layer surfaces in ``--json`` mode without changing the
153
+ stable top-level error envelope. Keep keys small and side-effect-free;
154
+ PR9 will lock this schema down with a versioned enum.
155
+ """
156
+
157
+ def __init__(
158
+ self,
159
+ message: str,
160
+ status_code: int = 0,
161
+ error_code: str | ErrorCode = ErrorCode.UNKNOWN_ERROR,
162
+ retryable: bool = False,
163
+ details: dict | None = None,
164
+ ) -> None:
165
+ super().__init__(message)
166
+ self.message = message
167
+ self.status_code = status_code
168
+ self.error_code = error_code
169
+ self.retryable = retryable
170
+ self.details: dict = details if details is not None else {}
171
+
172
+
173
+ class ConfigError(Exception):
174
+ """Raised when there is a configuration problem."""
175
+
176
+ def __init__(self, message: str) -> None:
177
+ super().__init__(message)
178
+ self.message = message
179
+
180
+
181
+ class SyncConflictError(Exception):
182
+ """Raised when ``sync pull --force`` would overwrite locally-modified
183
+ configs whose remote **also** changed since the last pull -- a true 3-way
184
+ merge conflict (local and remote both diverged from the synced base).
185
+
186
+ ``--force`` deliberately bypasses the "preserve locally-modified files"
187
+ guard, so without this check it would silently adopt the edited on-disk
188
+ file as the new synced baseline (issue: force-pull baseline corruption).
189
+ Rather than discard un-pushed work, the pull aborts *before writing
190
+ anything* and asks the user to resolve each conflict (push or discard
191
+ local edits, then pull again).
192
+
193
+ ``conflicts`` carries one dict per conflicting config/row so the command
194
+ layer can list them. Each dict has ``component_id``, ``config_id``,
195
+ ``config_name``, ``path``, ``scope`` (``"config"`` or ``"row"``), and an
196
+ optional ``row_id``.
197
+ """
198
+
199
+ def __init__(self, conflicts: list[dict[str, str]]) -> None:
200
+ self.conflicts = conflicts
201
+ n = len(conflicts)
202
+ plural = "s" if n != 1 else ""
203
+ message = (
204
+ f"{n} config{plural} ha{'ve' if n != 1 else 's'} un-pushed local "
205
+ f"edits AND changed on the remote since the last pull (merge "
206
+ f"conflict). `sync pull --force` refuses to overwrite them so your "
207
+ f"local work is not lost. Resolve each conflict first: review with "
208
+ f"`kbagent sync diff`, then either `kbagent sync push` your local "
209
+ f"edits or discard them, and pull again."
210
+ )
211
+ super().__init__(message)
212
+ self.message = message
213
+ self.error_code = ErrorCode.SYNC_CONFLICT
214
+
215
+
216
+ class PermissionDeniedError(Exception):
217
+ """Raised when an operation is blocked by the permission policy."""
218
+
219
+ def __init__(self, operation: str, message: str = "") -> None:
220
+ if not message:
221
+ message = f"Operation '{operation}' is blocked by the active permission policy."
222
+ super().__init__(message)
223
+ self.operation = operation
224
+ self.message = message
225
+
226
+
227
+ _ERROR_CODE_TO_TYPE: dict[str, str] = {
228
+ ErrorCode.INVALID_TOKEN: "authentication",
229
+ ErrorCode.MISSING_MASTER_TOKEN: "authentication",
230
+ ErrorCode.TIMEOUT: "network",
231
+ ErrorCode.CONNECTION_ERROR: "network",
232
+ ErrorCode.RETRY_EXHAUSTED: "network",
233
+ ErrorCode.NOT_FOUND: "not_found",
234
+ ErrorCode.CONFIG_ERROR: "configuration",
235
+ ErrorCode.VALIDATION_ERROR: "validation",
236
+ ErrorCode.SYNC_CONFLICT: "conflict",
237
+ ErrorCode.PERMISSION_DENIED: "authorization",
238
+ ErrorCode.DP_LOGIN_FAILED: "authentication",
239
+ ErrorCode.DP_MFA_REQUIRED: "authentication",
240
+ ErrorCode.DP_APP_NOT_FOUND: "not_found",
241
+ ErrorCode.DP_PUBLISH_REQUIREMENTS_MISSING: "validation",
242
+ ErrorCode.DP_ICON_UPLOAD_FAILED: "api",
243
+ }
244
+
245
+
246
+ def map_error_code_to_type(error_code: str) -> str:
247
+ """Map a machine-readable error code to a broad error type category."""
248
+ return _ERROR_CODE_TO_TYPE.get(error_code, "api")
@@ -0,0 +1,315 @@
1
+ """Base HTTP client with shared retry, timeout, and error handling logic.
2
+
3
+ Both KeboolaClient (Storage API) and ManageClient (Manage API) inherit
4
+ from BaseHttpClient to avoid duplicating the retry loop, error mapping,
5
+ and message sanitization code.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ import platform
12
+ import time
13
+ from typing import Any, Self
14
+ from urllib.parse import urlparse, urlunparse
15
+
16
+ import httpx
17
+
18
+ from .constants import (
19
+ APP_NAME,
20
+ BACKOFF_BASE,
21
+ ENV_CONVERSATION_ID,
22
+ MAX_API_ERROR_LENGTH,
23
+ MAX_RETRIES,
24
+ MAX_RETRY_AFTER_SECONDS,
25
+ RETRYABLE_STATUS_CODES,
26
+ )
27
+ from .errors import ErrorCode, KeboolaApiError, mask_token
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def build_user_agent() -> str:
33
+ """Build the User-Agent that signs every Keboola API call.
34
+
35
+ Format (RFC 7231 product + comment):
36
+
37
+ keboola-cli/<version> (<os> <release>; <arch>; <impl> <pyver>)
38
+ e.g. keboola-cli/0.45.0 (Darwin 25.3.0; arm64; CPython 3.12.7)
39
+
40
+ Keboola's edge logs this verbatim (DataDog access logs), so the fleet can
41
+ be segmented by version and OS/arch. Only neutral host metadata is sent --
42
+ never ``platform.node()`` (the hostname is PII). Identity ("which project /
43
+ user") is resolved server-side from the token, never derived client-side.
44
+ """
45
+ from . import __version__
46
+
47
+ return (
48
+ f"{APP_NAME}/{__version__} "
49
+ f"({platform.system()} {platform.release()}; "
50
+ f"{platform.machine()}; "
51
+ f"{platform.python_implementation()} {platform.python_version()})"
52
+ )
53
+
54
+
55
+ class BaseHttpClient:
56
+ """Shared HTTP client with retry, timeout, and error handling.
57
+
58
+ Provides:
59
+ - _do_request(method, path, **kwargs): HTTP request with retry + backoff
60
+ - _raise_api_error(response, base_url=None): error mapping with truncation
61
+ - Context manager support (close, __enter__, __exit__)
62
+
63
+ Subclasses must call super().__init__() with base_url, token, headers,
64
+ and optional timeout.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ base_url: str,
70
+ token: str,
71
+ headers: dict[str, str],
72
+ timeout: httpx.Timeout | None = None,
73
+ ) -> None:
74
+ from .constants import DEFAULT_TIMEOUT
75
+
76
+ self._base_url = base_url.rstrip("/")
77
+ self._token = token
78
+ self._masked_token = mask_token(token)
79
+ # Sign every request centrally so all subclasses share one UA string
80
+ # (and OS/version enrichment) instead of hardcoding it five times.
81
+ headers["User-Agent"] = build_user_agent()
82
+ conversation_id = os.environ.get(ENV_CONVERSATION_ID, "")
83
+ if conversation_id:
84
+ headers["X-Conversation-ID"] = conversation_id
85
+ self._client = httpx.Client(
86
+ base_url=self._base_url,
87
+ timeout=timeout or DEFAULT_TIMEOUT,
88
+ headers=headers,
89
+ )
90
+
91
+ @staticmethod
92
+ def _derive_service_url(stack_url: str, service_prefix: str) -> str:
93
+ """Derive a service base URL by replacing 'connection.' in the hostname.
94
+
95
+ E.g. _derive_service_url("https://connection.keboola.com", "queue")
96
+ -> "https://queue.keboola.com"
97
+ """
98
+ parsed = urlparse(stack_url)
99
+ hostname = parsed.hostname or ""
100
+ new_host = hostname.replace("connection.", f"{service_prefix}.", 1)
101
+ if new_host == hostname:
102
+ logger.warning(
103
+ "%s URL derivation did not change hostname: %s",
104
+ service_prefix,
105
+ hostname,
106
+ )
107
+ return urlunparse(parsed._replace(netloc=new_host))
108
+
109
+ def close(self) -> None:
110
+ """Close the underlying HTTP client."""
111
+ self._client.close()
112
+
113
+ def __enter__(self) -> Self:
114
+ return self
115
+
116
+ def __exit__(self, *args: Any) -> None:
117
+ self.close()
118
+
119
+ def _do_request(
120
+ self,
121
+ method: str,
122
+ path: str,
123
+ *,
124
+ client: httpx.Client | None = None,
125
+ base_url: str | None = None,
126
+ **kwargs: Any,
127
+ ) -> httpx.Response:
128
+ """Execute an HTTP request with retry and exponential backoff.
129
+
130
+ Retries on status codes 429, 500, 502, 503, 504 up to MAX_RETRIES times
131
+ with exponential backoff (1s, 2s, 4s).
132
+
133
+ Args:
134
+ method: HTTP method (GET, POST, etc.).
135
+ path: URL path relative to base_url.
136
+ client: Optional httpx.Client to use (defaults to self._client).
137
+ Useful for subclasses that maintain multiple clients (e.g. queue client).
138
+ base_url: Optional base URL for error messages (defaults to self._base_url).
139
+ **kwargs: Additional arguments passed to httpx.Client.request().
140
+
141
+ Returns:
142
+ The HTTP response on success.
143
+
144
+ Raises:
145
+ KeboolaApiError: On HTTP errors (with masked token) or after retries exhausted.
146
+ """
147
+ http_client = client or self._client
148
+ url_label = base_url or self._base_url
149
+ last_response: httpx.Response | None = None
150
+
151
+ for attempt in range(MAX_RETRIES):
152
+ try:
153
+ response = http_client.request(method, path, **kwargs)
154
+
155
+ if response.status_code < 400:
156
+ return response
157
+
158
+ if response.status_code in RETRYABLE_STATUS_CODES and attempt < MAX_RETRIES - 1:
159
+ if response.status_code == 429:
160
+ retry_after = response.headers.get("Retry-After")
161
+ if retry_after:
162
+ try:
163
+ delay = min(float(retry_after), MAX_RETRY_AFTER_SECONDS)
164
+ except ValueError:
165
+ delay = BACKOFF_BASE * (2**attempt)
166
+ else:
167
+ delay = BACKOFF_BASE * (2**attempt)
168
+ else:
169
+ delay = BACKOFF_BASE * (2**attempt)
170
+ logger.debug(
171
+ "Retry attempt %d/%d for %s %s (status %d), delay %.1fs",
172
+ attempt + 1,
173
+ MAX_RETRIES,
174
+ method,
175
+ path,
176
+ response.status_code,
177
+ delay,
178
+ )
179
+ time.sleep(delay)
180
+ last_response = response
181
+ continue
182
+
183
+ self._raise_api_error(response, url_label)
184
+
185
+ except httpx.TimeoutException as exc:
186
+ if attempt < MAX_RETRIES - 1:
187
+ delay = BACKOFF_BASE * (2**attempt)
188
+ logger.debug(
189
+ "Retry attempt %d/%d for %s %s (timeout), delay %.1fs",
190
+ attempt + 1,
191
+ MAX_RETRIES,
192
+ method,
193
+ path,
194
+ delay,
195
+ )
196
+ time.sleep(delay)
197
+ continue
198
+ raise KeboolaApiError(
199
+ message=f"Request timed out connecting to {url_label} (token: {self._masked_token})",
200
+ status_code=0,
201
+ error_code=ErrorCode.TIMEOUT,
202
+ retryable=True,
203
+ ) from exc
204
+
205
+ except httpx.ConnectError as exc:
206
+ if attempt < MAX_RETRIES - 1:
207
+ delay = BACKOFF_BASE * (2**attempt)
208
+ logger.debug(
209
+ "Retry attempt %d/%d for %s %s (connection error), delay %.1fs",
210
+ attempt + 1,
211
+ MAX_RETRIES,
212
+ method,
213
+ path,
214
+ delay,
215
+ )
216
+ time.sleep(delay)
217
+ continue
218
+ raise KeboolaApiError(
219
+ message=f"Cannot connect to {url_label} (token: {self._masked_token})",
220
+ status_code=0,
221
+ error_code=ErrorCode.CONNECTION_ERROR,
222
+ retryable=True,
223
+ ) from exc
224
+
225
+ if last_response is not None:
226
+ self._raise_api_error(last_response, url_label)
227
+
228
+ raise KeboolaApiError(
229
+ message=f"Request failed after {MAX_RETRIES} retries to {url_label} (token: {self._masked_token})",
230
+ status_code=0,
231
+ error_code=ErrorCode.RETRY_EXHAUSTED,
232
+ retryable=True,
233
+ )
234
+
235
+ def _raise_api_error(self, response: httpx.Response, base_url: str | None = None) -> None:
236
+ """Convert an HTTP error response into a KeboolaApiError.
237
+
238
+ Parses the response body for error messages, truncates long messages
239
+ to MAX_API_ERROR_LENGTH characters, and maps status codes to
240
+ appropriate error codes.
241
+
242
+ Args:
243
+ response: The HTTP error response.
244
+ base_url: Optional URL label for error messages.
245
+
246
+ Raises:
247
+ KeboolaApiError: Always raised with appropriate error code and message.
248
+ """
249
+ status = response.status_code
250
+ url_label = base_url or self._base_url
251
+
252
+ try:
253
+ body = response.json()
254
+ # Real Keboola APIs answer with one of these keys in priority
255
+ # order. Two caveats:
256
+ # 1. Keboola Metastore puts the HTTP status code into `error`
257
+ # as an int (e.g. {"error": 422}) -- using `or` would
258
+ # shadow the actual error message in `errors`/`exception`.
259
+ # So we only accept `error` if it's a non-empty string.
260
+ # 2. `errors`/`detail` are lists of dicts (FastAPI / metastore
261
+ # 422 shape); we json.dumps them so the f-string render
262
+ # below doesn't print `[{...}]` repr.
263
+ err_field = body.get("error")
264
+ api_message = (
265
+ err_field
266
+ if isinstance(err_field, str) and err_field
267
+ else (
268
+ body.get("exception")
269
+ or body.get("message")
270
+ or body.get("description")
271
+ or body.get("detail")
272
+ or body.get("errors")
273
+ or json.dumps(body)
274
+ )
275
+ )
276
+ if not isinstance(api_message, str):
277
+ api_message = json.dumps(api_message)
278
+ except Exception:
279
+ api_message = response.text
280
+
281
+ # Truncate to prevent Rich markup injection and excessive output
282
+ if isinstance(api_message, str) and len(api_message) > MAX_API_ERROR_LENGTH:
283
+ api_message = api_message[:MAX_API_ERROR_LENGTH] + "..."
284
+
285
+ if status == 401:
286
+ raise KeboolaApiError(
287
+ message=f"Invalid or expired token (token: {self._masked_token}): {api_message}",
288
+ status_code=status,
289
+ error_code=ErrorCode.INVALID_TOKEN,
290
+ retryable=False,
291
+ )
292
+
293
+ if status == 403:
294
+ raise KeboolaApiError(
295
+ message=f"Access denied (token: {self._masked_token}): {api_message}",
296
+ status_code=status,
297
+ error_code=ErrorCode.ACCESS_DENIED,
298
+ retryable=False,
299
+ )
300
+
301
+ if status == 404:
302
+ raise KeboolaApiError(
303
+ message=f"Resource not found: {api_message}",
304
+ status_code=status,
305
+ error_code=ErrorCode.NOT_FOUND,
306
+ retryable=False,
307
+ )
308
+
309
+ retryable = status in RETRYABLE_STATUS_CODES
310
+ raise KeboolaApiError(
311
+ message=f"API error {status} from {url_label} (token: {self._masked_token}): {api_message}",
312
+ status_code=status,
313
+ error_code=ErrorCode.API_ERROR,
314
+ retryable=retryable,
315
+ )
@@ -0,0 +1,126 @@
1
+ """JSON deep-merge and nested-path utilities.
2
+
3
+ Used by ``config update`` to patch configuration content without
4
+ losing sibling keys -- the exact problem that MCP server's
5
+ ``update_config`` tool has (keboola/mcp-server#468).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import copy
11
+ from typing import Any
12
+
13
+
14
+ def deep_merge(target: dict[str, Any], source: dict[str, Any]) -> dict[str, Any]:
15
+ """Recursively merge *source* into *target* (non-mutating).
16
+
17
+ Rules:
18
+ * dict + dict → recursively merged
19
+ * anything else → *source* wins (including list replaces list)
20
+
21
+ Returns a new dict; neither *target* nor *source* is mutated.
22
+ """
23
+ result = copy.deepcopy(target)
24
+ for key, src_value in source.items():
25
+ if key in result and isinstance(result[key], dict) and isinstance(src_value, dict):
26
+ result[key] = deep_merge(result[key], src_value)
27
+ else:
28
+ result[key] = copy.deepcopy(src_value)
29
+ return result
30
+
31
+
32
+ def get_nested_value(obj: Any, path: str) -> Any:
33
+ """Retrieve a value from a nested structure using a dot-separated path.
34
+
35
+ Supports integer segments for list indexing (e.g. ``"tables.0.name"``).
36
+
37
+ Raises ``KeyError`` or ``IndexError`` if the path does not exist.
38
+ """
39
+ for segment in path.split("."):
40
+ if isinstance(obj, dict):
41
+ obj = obj[segment]
42
+ elif isinstance(obj, list):
43
+ obj = obj[int(segment)]
44
+ else:
45
+ raise KeyError(f"Cannot traverse into {type(obj).__name__} with key '{segment}'")
46
+ return obj
47
+
48
+
49
+ def set_nested_value(obj: dict[str, Any], path: str, value: Any) -> dict[str, Any]:
50
+ """Set a value at a dot-separated path, creating intermediate dicts.
51
+
52
+ Returns a deep-copied dict with the value set — *obj* is not mutated.
53
+
54
+ Supports integer segments for list indexing on **existing** lists
55
+ (new intermediate containers are always dicts).
56
+ """
57
+ result = copy.deepcopy(obj)
58
+ segments = path.split(".")
59
+ current: Any = result
60
+ for segment in segments[:-1]:
61
+ if isinstance(current, dict):
62
+ if segment not in current:
63
+ current[segment] = {}
64
+ current = current[segment]
65
+ elif isinstance(current, list):
66
+ current = current[int(segment)]
67
+ else:
68
+ raise KeyError(f"Cannot traverse into {type(current).__name__} with key '{segment}'")
69
+
70
+ last = segments[-1]
71
+ if isinstance(current, dict):
72
+ current[last] = copy.deepcopy(value)
73
+ elif isinstance(current, list):
74
+ current[int(last)] = copy.deepcopy(value)
75
+ else:
76
+ raise KeyError(f"Cannot set key '{last}' on {type(current).__name__}")
77
+ return result
78
+
79
+
80
+ def compute_diff(
81
+ old: dict[str, Any],
82
+ new: dict[str, Any],
83
+ path: str = "",
84
+ ) -> list[str]:
85
+ """Produce a human-readable list of changes between two dicts.
86
+
87
+ Each entry looks like:
88
+ ``"parameters.tables.count: 5 -> 10"``
89
+ ``"parameters.newKey: (absent) -> 'hello'"``
90
+ ``"parameters.removed: 42 -> (absent)"``
91
+ """
92
+ changes: list[str] = []
93
+ all_keys = sorted(set(list(old.keys()) + list(new.keys())))
94
+
95
+ for key in all_keys:
96
+ full_path = f"{path}.{key}" if path else key
97
+ in_old = key in old
98
+ in_new = key in new
99
+
100
+ if in_old and in_new:
101
+ old_val = old[key]
102
+ new_val = new[key]
103
+ if isinstance(old_val, dict) and isinstance(new_val, dict):
104
+ changes.extend(compute_diff(old_val, new_val, full_path))
105
+ elif old_val != new_val:
106
+ changes.append(f"{full_path}: {_fmt(old_val)} -> {_fmt(new_val)}")
107
+ elif in_old and not in_new:
108
+ changes.append(f"{full_path}: {_fmt(old[key])} -> (absent)")
109
+ else:
110
+ changes.append(f"{full_path}: (absent) -> {_fmt(new[key])}")
111
+
112
+ return changes
113
+
114
+
115
+ def _fmt(value: Any) -> str:
116
+ """Format a value for diff display — truncate long representations."""
117
+ if isinstance(value, str):
118
+ s = repr(value)
119
+ elif isinstance(value, dict):
120
+ s = f"{{...}} ({len(value)} keys)"
121
+ elif isinstance(value, list):
122
+ s = f"[...] ({len(value)} items)"
123
+ else:
124
+ s = repr(value)
125
+ max_len = 80
126
+ return s if len(s) <= max_len else s[: max_len - 3] + "..."