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,342 @@
1
+ """Keboola Data Science API client (data-app deployment records).
2
+
3
+ The Data Science API owns the *deployment* side of a data app — id, state,
4
+ desiredState, url, configVersion. The Storage API
5
+ (``keboola.data-apps`` configs) owns the *configuration* side — git block,
6
+ encrypted secrets, slug, runtime size. Both must stay in sync; see
7
+ ``services/data_app_service.py`` for the orchestration.
8
+
9
+ URL derivation: ``https://data-science.<stack-suffix>`` from the project's
10
+ connection URL via ``BaseHttpClient._derive_service_url``. Auth: same
11
+ ``X-StorageApi-Token`` as the Storage API. The single exception is
12
+ ``GET /apps/{id}/password`` which additionally requires
13
+ ``X-KBC-ManageApiToken`` -- the manage token is passed per-call so the
14
+ client itself stays project-scoped.
15
+
16
+ Verified shapes (writeup §2 / §6 / §9, replayed in this PR's live
17
+ validation):
18
+
19
+ POST /apps -> 201, {id, configId, ...}
20
+ GET /apps -> 200, [{id, configId, state, desiredState, url}, ...]
21
+ GET /apps/{id} -> 200, full deployment record
22
+ PATCH /apps/{id} -> 200, deployment record (only
23
+ desiredState / configVersion /
24
+ restartIfRunning persist;
25
+ ``config:{...}`` is silently
26
+ dropped)
27
+ DELETE /apps/{id} -> 202, cascades to Storage config
28
+ GET /apps/{id}/password -> 200, {password: "<20 hex>"}
29
+ (requires both Storage and
30
+ Manage tokens)
31
+ GET /apps/{id}/logs/tail -> 200, text/plain container log
32
+ tail. ``lines=N`` and
33
+ ``since=ISO8601`` are mutually
34
+ exclusive on the server.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import json
40
+ import logging
41
+ from typing import Any
42
+ from urllib.parse import quote
43
+
44
+ from .constants import DEFAULT_TIMEOUT
45
+ from .http_base import BaseHttpClient
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class DataScienceClient(BaseHttpClient):
51
+ """HTTP client for the Keboola Data Science API (``/apps``).
52
+
53
+ Inherits retry / backoff / token-masking from ``BaseHttpClient``.
54
+ """
55
+
56
+ def __init__(self, stack_url: str, token: str) -> None:
57
+ self._stack_url = stack_url.rstrip("/")
58
+ ds_base_url = self._derive_service_url(self._stack_url, "data-science")
59
+ headers = {
60
+ "X-StorageApi-Token": token,
61
+ }
62
+ super().__init__(
63
+ base_url=ds_base_url,
64
+ token=token,
65
+ headers=headers,
66
+ timeout=DEFAULT_TIMEOUT,
67
+ )
68
+
69
+ def __enter__(self) -> DataScienceClient:
70
+ return self
71
+
72
+ def __exit__(self, *args: Any) -> None:
73
+ self.close()
74
+
75
+ def list_apps(self) -> list[dict[str, Any]]:
76
+ """Return the thin index of data apps in the project (no body filter).
77
+
78
+ The Data Science API scopes responses by the token's project; there
79
+ is no ``branchId`` query parameter on the list endpoint.
80
+ """
81
+ response = self._do_request("GET", "/apps")
82
+ body = response.json()
83
+ # Some stacks wrap the list in {"data": [...]}; fall back gracefully.
84
+ apps = (body.get("data") or body.get("apps") or []) if isinstance(body, dict) else body
85
+ return apps if isinstance(apps, list) else []
86
+
87
+ def get_app(self, app_id: str) -> dict[str, Any]:
88
+ """Fetch a single deployment record by numeric app id."""
89
+ response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}")
90
+ return response.json()
91
+
92
+ def create_app(
93
+ self,
94
+ *,
95
+ type_: str,
96
+ name: str,
97
+ description: str,
98
+ config: dict[str, Any],
99
+ branch_id: int | None = None,
100
+ ) -> dict[str, Any]:
101
+ """Create the deployment shell + linked Storage config in one call.
102
+
103
+ Server-generated identifiers: ``id`` (numeric) and ``configId``
104
+ (ULID). The ``configId`` field in the request body is silently
105
+ ignored (writeup §5) -- callers must accept whatever ULID the
106
+ server assigns and round-trip it on subsequent updates.
107
+
108
+ ``config`` carries the *initial* Storage configuration body. The
109
+ full config (git block, encrypted secrets, etc.) is added via
110
+ ``KeboolaClient.update_config`` after creation; sending it here is
111
+ possible but the encryption step depends on knowing
112
+ ``config_id`` first, so the canonical flow is:
113
+ ``create_app`` -> encrypt secrets -> ``update_config``.
114
+ """
115
+ payload: dict[str, Any] = {
116
+ "branchId": branch_id,
117
+ "type": type_,
118
+ "name": name,
119
+ "description": description,
120
+ "config": config,
121
+ }
122
+ response = self._do_request(
123
+ "POST",
124
+ "/apps",
125
+ content=json.dumps(payload).encode("utf-8"),
126
+ headers={"Content-Type": "application/json"},
127
+ )
128
+ return response.json()
129
+
130
+ def patch_app(
131
+ self,
132
+ app_id: str,
133
+ *,
134
+ desired_state: str | None = None,
135
+ config_version: str | None = None,
136
+ restart_if_running: bool | None = None,
137
+ ) -> dict[str, Any]:
138
+ """Update the deployment record (state / pinned config version).
139
+
140
+ IMPORTANT: never sends a ``config`` block — that surface is owned
141
+ by the Storage API (writeup §2.1, §8 pitfall row 3). Updating
142
+ size / autoSuspend / git settings goes through ``update_config``
143
+ on the Storage API.
144
+
145
+ The §9 redeploy contract requires
146
+ ``desired_state="running"`` + ``config_version=<N>``
147
+ + ``restart_if_running=True`` together when bumping the deployed
148
+ config version; sending ``config_version`` alone yields HTTP 422.
149
+ """
150
+ payload: dict[str, Any] = {}
151
+ if desired_state is not None:
152
+ payload["desiredState"] = desired_state
153
+ if config_version is not None:
154
+ payload["configVersion"] = config_version
155
+ if restart_if_running is not None:
156
+ payload["restartIfRunning"] = restart_if_running
157
+ response = self._do_request(
158
+ "PATCH",
159
+ f"/apps/{quote(str(app_id), safe='')}",
160
+ content=json.dumps(payload).encode("utf-8"),
161
+ headers={"Content-Type": "application/json"},
162
+ )
163
+ return response.json()
164
+
165
+ def delete_app(self, app_id: str) -> None:
166
+ """Delete the deployment AND the linked Storage config (cascade).
167
+
168
+ Returns HTTP 202 on success; the body is empty.
169
+ """
170
+ self._do_request("DELETE", f"/apps/{quote(str(app_id), safe='')}")
171
+
172
+ def get_app_password(self, app_id: str, manage_token: str) -> dict[str, Any]:
173
+ """Retrieve the auto-generated simpleAuth password.
174
+
175
+ Requires both the project's Storage token (already on
176
+ ``self._client``) AND a Manage API token, supplied per-call so the
177
+ manage token never lives on the client instance.
178
+
179
+ The 20-character hex password is auto-generated at app create time
180
+ and is NOT rotatable -- to change it you must delete and recreate
181
+ the app (writeup §11.2).
182
+ """
183
+ path = f"/apps/{quote(str(app_id), safe='')}/password"
184
+ # Pass the Manage token via per-request `headers=`. httpx merges these
185
+ # with the client's persistent headers for this call only, so the
186
+ # manage token never lives on `self._client`. Using `_do_request`
187
+ # gives us the same retry/backoff and uniform error mapping as every
188
+ # other call in this client (no bespoke try/except needed).
189
+ response = self._do_request(
190
+ "GET",
191
+ path,
192
+ headers={"X-KBC-ManageApiToken": manage_token},
193
+ )
194
+ return response.json()
195
+
196
+ def tail_app_logs(
197
+ self,
198
+ app_id: str,
199
+ *,
200
+ lines: int | None = None,
201
+ since: str | None = None,
202
+ ) -> str:
203
+ """Fetch the container log tail from ``/apps/{id}/logs/tail``.
204
+
205
+ Returns the response body verbatim as plain text (``text/plain``)
206
+ -- one log line per ``\\n``, trailing newline preserved as the
207
+ server sent it. Callers that want a list of lines should call
208
+ ``text.splitlines()``.
209
+
210
+ ``lines`` and ``since`` are mutually exclusive on the server
211
+ (400 ``Only one of "since" or "lines" can be set``); the caller
212
+ MUST enforce that constraint before invoking. Passing neither
213
+ returns the full current container buffer. ``lines=0`` and
214
+ negative values are rejected by the server with a 400 -- callers
215
+ opting into the full buffer should pass ``lines=None``.
216
+
217
+ ``since`` must be an ISO 8601 timestamp WITH timezone (``Z`` or
218
+ ``+00:00``); naive datetimes and date-only values are rejected
219
+ by the server with a 400.
220
+ """
221
+ path = f"/apps/{quote(str(app_id), safe='')}/logs/tail"
222
+ params: dict[str, Any] = {}
223
+ if lines is not None:
224
+ params["lines"] = lines
225
+ if since is not None:
226
+ params["since"] = since
227
+ # ``params or None`` keeps the URL clean (no trailing ``?``) when
228
+ # the caller wants the server's default buffer-all behavior.
229
+ response = self._do_request("GET", path, params=params or None)
230
+ return response.text
231
+
232
+ # ------------------------------------------------------------------
233
+ # Git repository (sandboxes-service /apps/{id}/git-repo/*)
234
+ #
235
+ # These endpoints introspect and manage the git repository a data app
236
+ # is deployed from. Ground truth: keboola/sandboxes-service server
237
+ # source + docs/swagger.yaml. Two functional groups:
238
+ #
239
+ # * Repo introspection (git-repo, /branches, /entrypoints) -- works
240
+ # for ANY configured repo (managed or external); auth = the same
241
+ # X-StorageApi-Token, permission CanManageApp.
242
+ # * Credential management (/credentials GET + POST) -- ONLY for a
243
+ # *managed* git repo (app.managedGitRepoId set); a repo configured
244
+ # via `data-app create --git-repo <url>` is *external*, so these
245
+ # return 409. Auth needs an admin storage token
246
+ # (CanManageAppRepoCredentials).
247
+ #
248
+ # IMPORTANT response-shape gotchas (verified in both sources):
249
+ # * /branches returns a RAW top-level JSON array (NOT wrapped in
250
+ # {branches: [...]}).
251
+ # * /entrypoints returns a RAW top-level array<string>.
252
+ # * /credentials (GET) IS wrapped: {"credentials": [...]}.
253
+ # * POST /credentials returns the created credential; the one-time
254
+ # ``secret`` is present ONLY for type=http_token and ONLY here.
255
+ # ------------------------------------------------------------------
256
+
257
+ def get_git_repo(self, app_id: str) -> dict[str, Any]:
258
+ """Return the clone URLs of the app's configured git repository.
259
+
260
+ Shape: ``{"sshUrl": str|None, "httpsUrl": str|None,
261
+ "isManagedGitRepo": bool}``. For external repos only the URL
262
+ matching the configured protocol is populated (the other is
263
+ ``None``) and embedded credentials are stripped. ``409`` if the
264
+ app has no git repository configured.
265
+ """
266
+ response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}/git-repo")
267
+ body = response.json()
268
+ return body if isinstance(body, dict) else {}
269
+
270
+ def list_git_branches(self, app_id: str) -> list[dict[str, Any]]:
271
+ """List the remote branches of the app's configured git repository.
272
+
273
+ Returns the server's RAW top-level array of branch objects
274
+ ``[{"branch", "comment", "sha", "author": {"name", "email"},
275
+ "date"}]`` (HEAD/origin/HEAD filtered, sorted by name). Works for
276
+ managed and external repos alike.
277
+ """
278
+ response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/branches")
279
+ body = response.json()
280
+ return body if isinstance(body, list) else []
281
+
282
+ def list_git_entrypoints(self, app_id: str) -> list[str]:
283
+ """List root-level ``.py`` entrypoint files of the app's repo.
284
+
285
+ Returns the server's RAW top-level ``array<string>`` of root
286
+ filenames on the configured branch (or the repo default).
287
+ Extension is hardcoded to ``py`` server-side, so non-Python
288
+ entrypoints are not listable here.
289
+ """
290
+ response = self._do_request(
291
+ "GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/entrypoints"
292
+ )
293
+ body = response.json()
294
+ return [str(item) for item in body] if isinstance(body, list) else []
295
+
296
+ def list_git_credentials(self, app_id: str) -> dict[str, Any]:
297
+ """List the credentials of the app's MANAGED git repository.
298
+
299
+ Shape: ``{"credentials": [{"id", "type", "name", "permissions",
300
+ "ownerAdminId", "createdAt"}]}``. The ``secret`` is NEVER returned
301
+ here. ``409`` if the app has no managed git repository; requires an
302
+ admin storage token.
303
+ """
304
+ response = self._do_request(
305
+ "GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/credentials"
306
+ )
307
+ body = response.json()
308
+ return body if isinstance(body, dict) else {}
309
+
310
+ def create_git_credential(
311
+ self,
312
+ app_id: str,
313
+ *,
314
+ type_: str,
315
+ permissions: str,
316
+ public_key: str | None = None,
317
+ name: str | None = None,
318
+ ) -> dict[str, Any]:
319
+ """Create a credential for the app's MANAGED git repository.
320
+
321
+ ``type_`` is ``"ssh_key"`` or ``"http_token"``; ``permissions`` is
322
+ ``"readOnly"`` or ``"readWrite"``. ``public_key`` is required IFF
323
+ ``type_ == "ssh_key"`` and MUST be absent otherwise (the server
324
+ returns 400 on a wrong combination).
325
+
326
+ Returns the created credential. The one-time ``secret`` field is
327
+ present ONLY when ``type_ == "http_token"`` and is never retrievable
328
+ again. ``409`` if the app has no managed git repository; requires an
329
+ admin storage token.
330
+ """
331
+ payload: dict[str, Any] = {"type": type_, "permissions": permissions}
332
+ if public_key is not None:
333
+ payload["publicKey"] = public_key
334
+ if name is not None:
335
+ payload["name"] = name
336
+ response = self._do_request(
337
+ "POST",
338
+ f"/apps/{quote(str(app_id), safe='')}/git-repo/credentials",
339
+ content=json.dumps(payload).encode("utf-8"),
340
+ headers={"Content-Type": "application/json"},
341
+ )
342
+ return response.json()
@@ -0,0 +1,323 @@
1
+ """Keboola Developer Portal HTTP client (apps-api.keboola.com).
2
+
3
+ Auth model:
4
+ - Login (email + password) returns a bearer token. On a personal account, the
5
+ first login returns an MFA session; we prompt the user via /dev/tty and
6
+ re-login with {email, session, code} to obtain the bearer.
7
+ - The bearer lives ONLY on this client instance (in self._bearer). It is
8
+ never written to disk, never logged, and discarded when the client closes.
9
+ - Each kbagent invocation logs in fresh; there is no token cache.
10
+
11
+ The client is intentionally dumb: dry-run, diff, and confirm logic belong to
12
+ the service and command layers.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import urllib.error
19
+ import urllib.request
20
+ from typing import Any
21
+
22
+ import httpx
23
+
24
+ from .constants import DP_MFA_CHALLENGE_TYPE, MAX_API_ERROR_LENGTH
25
+ from .errors import ErrorCode, KeboolaApiError
26
+ from .http_base import BaseHttpClient
27
+ from .models import DeveloperPortalIdentity
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ def _tty_prompt(label: str, *, secret: bool = False) -> str | None:
33
+ """Prompt via the controlling terminal so a redirected stdin can't break it.
34
+
35
+ Returns None when no /dev/tty is available (non-interactive shell, no
36
+ controlling terminal). Caller must treat None as "cannot prompt".
37
+ """
38
+ try:
39
+ with open("/dev/tty", "w") as out:
40
+ if secret:
41
+ import getpass
42
+
43
+ return getpass.getpass(label, stream=out)
44
+ out.write(label)
45
+ out.flush()
46
+ with open("/dev/tty") as tin:
47
+ return tin.readline().rstrip("\n")
48
+ except OSError:
49
+ return None
50
+
51
+
52
+ class DeveloperPortalClient(BaseHttpClient):
53
+ """HTTP client for the Keboola Developer Portal."""
54
+
55
+ def __init__(self, identity: DeveloperPortalIdentity) -> None:
56
+ # We don't have a bearer yet — pass empty token. Login populates it.
57
+ super().__init__(
58
+ base_url=identity.portal_url,
59
+ token="",
60
+ headers={"Accept": "application/json"},
61
+ )
62
+ self._identity = identity
63
+ self._bearer: str | None = None
64
+
65
+ @property
66
+ def bearer(self) -> str | None:
67
+ """The active bearer token, or None if not yet authenticated.
68
+
69
+ In-memory only; never written to disk. Exposed so the service can
70
+ reuse one login across a prepare/apply pair (see seed_bearer) instead
71
+ of re-authenticating — which, on a personal MFA account, would prompt
72
+ for a second MFA code on a single write.
73
+ """
74
+ return self._bearer
75
+
76
+ def seed_bearer(self, bearer: str) -> None:
77
+ """Reuse a bearer obtained by an earlier client for the same identity.
78
+
79
+ Lets the service carry one authenticated session across the
80
+ prepare -> (random-code confirm) -> apply flow without a second login.
81
+ """
82
+ self._bearer = bearer
83
+ self._client.headers["Authorization"] = bearer
84
+
85
+ def _ensure_authenticated(self) -> None:
86
+ """Log in if not already authenticated. Idempotent on the instance."""
87
+ if self._bearer is not None:
88
+ return
89
+ self._bearer = self._login(self._identity.username, self._identity.password)
90
+ self._client.headers["Authorization"] = self._bearer
91
+
92
+ def _login(self, username: str, password: str) -> str:
93
+ try:
94
+ resp = self._client.post(
95
+ "/auth/login",
96
+ json={"email": username, "password": password},
97
+ )
98
+ except httpx.HTTPError as exc:
99
+ raise KeboolaApiError(
100
+ message=f"Developer Portal login transport error: {exc}",
101
+ error_code=ErrorCode.CONNECTION_ERROR,
102
+ ) from exc
103
+ if resp.status_code != 200:
104
+ raise KeboolaApiError(
105
+ message=(
106
+ f"Developer Portal login failed (HTTP {resp.status_code}). "
107
+ "Check the identity credentials."
108
+ ),
109
+ error_code=ErrorCode.DP_LOGIN_FAILED,
110
+ )
111
+ payload = resp.json()
112
+ if isinstance(payload, dict) and payload.get("token"):
113
+ return payload["token"]
114
+ # MFA path — implemented in Task 7.
115
+ if isinstance(payload, dict) and payload.get("session"):
116
+ return self._login_with_mfa(username, payload["session"])
117
+ raise KeboolaApiError(
118
+ message="Developer Portal login response missing token and session",
119
+ error_code=ErrorCode.DP_LOGIN_FAILED,
120
+ )
121
+
122
+ def _login_with_mfa(self, username: str, session: str) -> str:
123
+ """Confirm an MFA-gated login.
124
+
125
+ Per the Keboola Developer Portal apiary spec, the same POST /auth/login
126
+ endpoint accepts {email, session, code, challenge}. The `challenge`
127
+ field is documented as optional with default SOFTWARE_TOKEN_MFA, but
128
+ in practice the server rejects calls that omit it (404 with the
129
+ misleading "must be one of" enum message attached to the admin schema).
130
+ Send it explicitly. Single attempt only -- /auth/login consumes the
131
+ session, so any retry on the same session always 404s with "Invalid
132
+ code or auth state for the user" regardless of the new challenge type.
133
+ """
134
+ code = _tty_prompt("MFA code: ")
135
+ if not code:
136
+ raise KeboolaApiError(
137
+ message=(
138
+ "Developer Portal identity requires an MFA code, but no "
139
+ "interactive terminal is available. Run from a real "
140
+ "terminal, or switch to a service.{vendor}.{id} "
141
+ "account (no MFA)."
142
+ ),
143
+ error_code=ErrorCode.DP_MFA_REQUIRED,
144
+ )
145
+ body = {
146
+ "email": username,
147
+ "session": session,
148
+ "code": code.strip(),
149
+ "challenge": DP_MFA_CHALLENGE_TYPE,
150
+ }
151
+ try:
152
+ resp = self._client.post("/auth/login", json=body)
153
+ except httpx.HTTPError as exc:
154
+ raise KeboolaApiError(
155
+ message=f"Developer Portal MFA login transport error: {exc}",
156
+ error_code=ErrorCode.CONNECTION_ERROR,
157
+ ) from exc
158
+ if resp.status_code == 200:
159
+ payload = resp.json()
160
+ if isinstance(payload, dict) and payload.get("token"):
161
+ return payload["token"]
162
+ raise KeboolaApiError(
163
+ message=(
164
+ "Developer Portal MFA login returned HTTP 200 but no "
165
+ f"'token' field in response: {payload!r}"
166
+ ),
167
+ error_code=ErrorCode.DP_LOGIN_FAILED,
168
+ )
169
+ try:
170
+ body_text = resp.text[:MAX_API_ERROR_LENGTH]
171
+ except (UnicodeDecodeError, AttributeError):
172
+ body_text = "<unreadable>"
173
+ raise KeboolaApiError(
174
+ message=(
175
+ f"Developer Portal MFA login failed (HTTP {resp.status_code}): "
176
+ f"{body_text}. If your TOTP code rotates every 30s, this is "
177
+ "often a stale code -- retry promptly. If the server says "
178
+ "'Invalid code or auth state' on a fresh session, the code "
179
+ "itself was wrong."
180
+ ),
181
+ error_code=ErrorCode.DP_LOGIN_FAILED,
182
+ )
183
+
184
+ # ----- Reads -----
185
+
186
+ def list_apps(self, vendor: str) -> list[dict[str, Any]]:
187
+ self._ensure_authenticated()
188
+ resp = self._do_request("GET", f"/vendors/{vendor}/apps?limit=1000")
189
+ if resp.status_code != 200:
190
+ self._raise_dp_error(resp, action="list apps", vendor=vendor)
191
+ payload = resp.json()
192
+ if isinstance(payload, dict) and "apps" in payload:
193
+ return list(payload["apps"])
194
+ if isinstance(payload, list):
195
+ return payload
196
+ return []
197
+
198
+ def get_app(self, vendor: str, app_id: str) -> dict[str, Any]:
199
+ self._ensure_authenticated()
200
+ try:
201
+ resp = self._do_request("GET", f"/vendors/{vendor}/apps/{app_id}")
202
+ except KeboolaApiError as exc:
203
+ if exc.error_code == ErrorCode.NOT_FOUND:
204
+ raise KeboolaApiError(
205
+ message=f"Developer Portal app '{app_id}' not found in vendor '{vendor}'",
206
+ error_code=ErrorCode.DP_APP_NOT_FOUND,
207
+ ) from exc
208
+ raise
209
+ return resp.json()
210
+
211
+ # ----- Writes -----
212
+
213
+ def create_app(self, vendor: str, payload: dict[str, Any]) -> dict[str, Any]:
214
+ self._ensure_authenticated()
215
+ resp = self._do_request("POST", f"/vendors/{vendor}/apps", json=payload)
216
+ if resp.status_code not in (200, 201):
217
+ self._raise_dp_error(resp, action="create app", vendor=vendor)
218
+ return resp.json()
219
+
220
+ def patch_app(self, vendor: str, app_id: str, payload: dict[str, Any]) -> dict[str, Any]:
221
+ """PATCH an app. Routes by identity role:
222
+ - admin -> PATCH /admin/apps/{app_id} (permissive schema, accepts the
223
+ 9 fields forbidden() on the vendor schema: complexity, categories,
224
+ forwardToken, forwardTokenDetails, injectEnvironment, processTimeout,
225
+ requiredMemory, features, category).
226
+ - vendor -> PATCH /vendors/{vendor}/apps/{app_id} (default, restricted
227
+ schema). The `vendor` arg is still required for the path.
228
+ """
229
+ self._ensure_authenticated()
230
+ if self._identity.role_hint == "admin":
231
+ path = f"/admin/apps/{app_id}"
232
+ else:
233
+ path = f"/vendors/{vendor}/apps/{app_id}"
234
+ resp = self._do_request("PATCH", path, json=payload)
235
+ if resp.status_code not in (200, 204):
236
+ self._raise_dp_error(resp, action="patch app", vendor=vendor, app_id=app_id)
237
+ return resp.json() if resp.content else {}
238
+
239
+ def publish_app(self, vendor: str, app_id: str) -> dict[str, Any]:
240
+ self._ensure_authenticated()
241
+ resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/publish")
242
+ if resp.status_code not in (200, 202):
243
+ self._raise_dp_error(resp, action="publish app", vendor=vendor, app_id=app_id)
244
+ return resp.json() if resp.content else {"status": "submitted"}
245
+
246
+ def deprecate_app(self, vendor: str, app_id: str) -> dict[str, Any]:
247
+ self._ensure_authenticated()
248
+ resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/deprecate")
249
+ if resp.status_code not in (200, 202):
250
+ self._raise_dp_error(resp, action="deprecate app", vendor=vendor, app_id=app_id)
251
+ return resp.json() if resp.content else {"status": "deprecated"}
252
+
253
+ def upload_icon(self, vendor: str, app_id: str, png_bytes: bytes) -> None:
254
+ """Two-hop icon upload: ask the portal for a presigned S3 URL, then PUT bytes there.
255
+
256
+ The S3 PUT does NOT use this client's httpx instance (no retry, no auth,
257
+ no User-Agent injection). We use urllib directly so the wire shape stays
258
+ exactly what S3 expects.
259
+ """
260
+ self._ensure_authenticated()
261
+ try:
262
+ resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/icon")
263
+ except KeboolaApiError as exc:
264
+ raise KeboolaApiError(
265
+ message=(f"Developer Portal failed to mint icon-upload URL: {exc.message}"),
266
+ error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
267
+ ) from exc
268
+ if resp.status_code != 200:
269
+ raise KeboolaApiError(
270
+ message=(
271
+ f"Developer Portal failed to mint icon-upload URL (HTTP {resp.status_code})"
272
+ ),
273
+ error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
274
+ )
275
+ payload = resp.json()
276
+ link = payload.get("link") if isinstance(payload, dict) else None
277
+ if not link:
278
+ raise KeboolaApiError(
279
+ message="Developer Portal icon-upload response missing 'link'",
280
+ error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
281
+ )
282
+ req = urllib.request.Request(
283
+ link,
284
+ data=png_bytes,
285
+ headers={"Content-Type": "image/png"},
286
+ method="PUT",
287
+ )
288
+ try:
289
+ with urllib.request.urlopen(req) as s3_resp:
290
+ if getattr(s3_resp, "status", 200) >= 300:
291
+ raise KeboolaApiError(
292
+ message=f"Icon S3 PUT failed (HTTP {s3_resp.status})",
293
+ error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
294
+ )
295
+ except urllib.error.HTTPError as exc:
296
+ raise KeboolaApiError(
297
+ message=f"Icon S3 PUT failed (HTTP {exc.code}): {exc.reason}",
298
+ error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
299
+ ) from exc
300
+
301
+ # ----- Error mapping -----
302
+
303
+ def _raise_dp_error(
304
+ self,
305
+ resp: httpx.Response,
306
+ *,
307
+ action: str,
308
+ vendor: str | None = None,
309
+ app_id: str | None = None,
310
+ ) -> None:
311
+ try:
312
+ body = resp.json()
313
+ except ValueError:
314
+ body = resp.text
315
+ ctx = f"{action}"
316
+ if vendor:
317
+ ctx += f" (vendor={vendor})"
318
+ if app_id:
319
+ ctx += f" (app={app_id})"
320
+ raise KeboolaApiError(
321
+ message=f"Developer Portal {ctx} failed (HTTP {resp.status_code}): {body}",
322
+ error_code=ErrorCode.API_ERROR,
323
+ )