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,582 @@
1
+ """Persistent configuration store for Keboola Agent CLI.
2
+
3
+ Manages reading and writing of config.json with project connections.
4
+ File permissions are set to 0600 to protect stored tokens.
5
+ Uses atomic writes to prevent TOCTOU race conditions.
6
+ File locking (fcntl) prevents corruption from concurrent access.
7
+ """
8
+
9
+ import contextlib
10
+ import json
11
+ import logging
12
+ import os
13
+ from pathlib import Path
14
+
15
+ import platformdirs
16
+ from pydantic import ValidationError
17
+
18
+ from .constants import (
19
+ ENV_CONFIG_DIR,
20
+ ENV_KBC_STORAGE_API_URL,
21
+ ENV_KBC_TOKEN,
22
+ ENV_PROJECT_ALIAS,
23
+ ENV_PROJECT_FROM_ENV,
24
+ LOCAL_CONFIG_DIR_NAME,
25
+ )
26
+ from .errors import ConfigError
27
+ from .models import AppConfig, DeveloperPortalIdentity, ProjectConfig
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ CURRENT_CONFIG_VERSION = 1
32
+
33
+ # Prepended to every config.json write as a first-position field. Claude Code
34
+ # (or any LLM reading the file) sees this before it sees any token. The field
35
+ # is silently ignored by AppConfig on load (Pydantic default: extra = ignore).
36
+ # Intent: nudge agents away from copying tokens into direct REST calls.
37
+ CLAUDE_CONFIG_WARNING = (
38
+ "THESE ARE KEBOOLA STORAGE API TOKENS. NEVER use them to call the "
39
+ "Keboola REST API directly (no curl, httpx, requests, fetch against "
40
+ "*.keboola.com). Always use `kbagent <command>` -- it wraps the same "
41
+ "API with retries, permission checks, and an audit trail. If you "
42
+ "need a command kbagent does not cover, run `kbagent serve` and call "
43
+ "the equivalent REST endpoint instead of touching the API directly. "
44
+ "See plugins/kbagent/skills/kbagent/SKILL.md. "
45
+ "Developer Portal credentials stored here have the SAME risk profile -- "
46
+ "never call apps-api.keboola.com directly; use `kbagent dev-portal ...`."
47
+ )
48
+
49
+ # File-lock constants (fcntl is POSIX-only; on Windows we skip locking).
50
+ try:
51
+ import fcntl
52
+
53
+ _LOCK_SH = fcntl.LOCK_SH
54
+ _LOCK_EX = fcntl.LOCK_EX
55
+ _LOCK_UN = fcntl.LOCK_UN
56
+ _HAS_FCNTL = True
57
+ except ImportError:
58
+ _LOCK_SH = 0
59
+ _LOCK_EX = 0
60
+ _LOCK_UN = 0
61
+ _HAS_FCNTL = False
62
+
63
+
64
+ def _try_flock(fd: int, operation: int) -> None:
65
+ """Try to apply a file lock. Silently skip on unsupported platforms (Windows)."""
66
+ if not _HAS_FCNTL:
67
+ return
68
+ with contextlib.suppress(OSError):
69
+ fcntl.flock(fd, operation)
70
+
71
+
72
+ def resolve_config_dir(cli_config_dir: str | None = None) -> tuple[Path, str]:
73
+ """Resolve the config directory using the priority chain.
74
+
75
+ Priority:
76
+ 1. --config-dir CLI flag (explicit override)
77
+ 2. KBAGENT_CONFIG_DIR environment variable
78
+ 3. Walk up from CWD looking for .kbagent/config.json (like git)
79
+ 4. Global default (~/.config/keboola-agent-cli/)
80
+
81
+ Returns:
82
+ Tuple of (resolved_path, source_label).
83
+ source_label is one of: "cli-flag", "env-var", "local", "global".
84
+ """
85
+ if cli_config_dir:
86
+ return Path(cli_config_dir), "cli-flag"
87
+
88
+ env_val = os.environ.get(ENV_CONFIG_DIR)
89
+ if env_val:
90
+ return Path(env_val), "env-var"
91
+
92
+ try:
93
+ current = Path.cwd().resolve()
94
+ except OSError:
95
+ return Path(platformdirs.user_config_dir("keboola-agent-cli")), "global"
96
+
97
+ home = Path.home().resolve()
98
+ while True:
99
+ candidate = current / LOCAL_CONFIG_DIR_NAME / "config.json"
100
+ if candidate.is_file():
101
+ return current / LOCAL_CONFIG_DIR_NAME, "local"
102
+ if current == home or current == current.parent:
103
+ break
104
+ current = current.parent
105
+
106
+ return Path(platformdirs.user_config_dir("keboola-agent-cli")), "global"
107
+
108
+
109
+ class ConfigStore:
110
+ """Handles persistence of application configuration to disk.
111
+
112
+ Configuration is stored as JSON at the platform-appropriate config directory,
113
+ defaulting to ~/.config/keboola-agent-cli/config.json on Linux/macOS.
114
+ """
115
+
116
+ CONFIG_FILENAME = "config.json"
117
+
118
+ def __init__(self, config_dir: Path | None = None, source: str = "global") -> None:
119
+ if config_dir is None:
120
+ self._config_dir = Path(platformdirs.user_config_dir("keboola-agent-cli"))
121
+ else:
122
+ self._config_dir = config_dir
123
+ self._config_path = self._config_dir / self.CONFIG_FILENAME
124
+ self._source = source
125
+
126
+ @property
127
+ def config_path(self) -> Path:
128
+ """Return the path to the config file."""
129
+ return self._config_path
130
+
131
+ @property
132
+ def config_dir(self) -> Path:
133
+ """Return the directory holding ``config.json`` (and sibling state files)."""
134
+ return self._config_dir
135
+
136
+ @property
137
+ def source(self) -> str:
138
+ """Return the config source label (cli-flag, env-var, local, global)."""
139
+ return self._source
140
+
141
+ def load(self) -> AppConfig:
142
+ """Load configuration from disk.
143
+
144
+ Returns an empty AppConfig if the file does not exist.
145
+ Validates the config version and raises ConfigError on mismatch or corruption.
146
+
147
+ Raises:
148
+ ConfigError: If the config file is corrupted or has an unsupported version.
149
+ """
150
+ logger.debug("Loading config from %s", self._config_path)
151
+ if not self._config_path.exists():
152
+ logger.debug("Config file does not exist, returning empty config")
153
+ return self._inject_env_project(AppConfig())
154
+
155
+ fd: int | None = None
156
+ try:
157
+ fd = os.open(str(self._config_path), os.O_RDONLY)
158
+ _try_flock(fd, _LOCK_SH)
159
+ raw = self._config_path.read_text(encoding="utf-8")
160
+ except OSError as exc:
161
+ raise ConfigError(f"Cannot read config file {self._config_path}: {exc}") from exc
162
+ except UnicodeDecodeError as exc:
163
+ raise ConfigError(f"Config file is not valid UTF-8 text: {exc}") from exc
164
+ finally:
165
+ if fd is not None:
166
+ _try_flock(fd, _LOCK_UN)
167
+ os.close(fd)
168
+
169
+ try:
170
+ data = json.loads(raw)
171
+ except json.JSONDecodeError as exc:
172
+ raise ConfigError(f"Config file is not valid JSON: {exc}") from exc
173
+
174
+ if not isinstance(data, dict):
175
+ raise ConfigError(
176
+ f"Config file has invalid structure: expected JSON object, got {type(data).__name__}"
177
+ )
178
+
179
+ version = data.get("version", 1)
180
+ if version > CURRENT_CONFIG_VERSION:
181
+ raise ConfigError(
182
+ f"Config file version {version} is newer than supported version "
183
+ f"{CURRENT_CONFIG_VERSION}. Please upgrade keboola-cli."
184
+ )
185
+
186
+ try:
187
+ config = AppConfig.model_validate(data)
188
+ except Exception as exc:
189
+ raise ConfigError(f"Config file has invalid structure: {exc}") from exc
190
+
191
+ return self._inject_env_project(config)
192
+
193
+ def _inject_env_project(self, config: AppConfig) -> AppConfig:
194
+ """Synthesize an in-memory project from env vars when opted in (issue #359).
195
+
196
+ When ``KBAGENT_PROJECT_FROM_ENV`` is truthy, read ``KBC_TOKEN`` and
197
+ ``KBC_STORAGE_API_URL`` and inject a project under the reserved alias
198
+ ``__env__`` so a headless daemon / container / CI can run kbagent with
199
+ no ``project add`` and no config.json on disk. Both CLI and ``serve``
200
+ funnel through ``load()``, so this single chokepoint covers both.
201
+
202
+ The injected project is marked ``ephemeral=True``; ``save()`` strips it
203
+ so the env token is never persisted. Opt-in is explicit (the flag), not
204
+ the mere presence of ``KBC_TOKEN``, to avoid a phantom project on a dev
205
+ machine that exported the token only for ``project add``.
206
+
207
+ A real project already registered under ``__env__`` is left untouched.
208
+
209
+ Raises:
210
+ ConfigError: If the flag is set but the credential env vars are
211
+ missing (fail fast rather than silently skip).
212
+ """
213
+ flag = os.environ.get(ENV_PROJECT_FROM_ENV, "").strip().lower()
214
+ if flag not in ("1", "true", "yes", "on"):
215
+ return config
216
+
217
+ if ENV_PROJECT_ALIAS in config.projects:
218
+ return config
219
+
220
+ token = os.environ.get(ENV_KBC_TOKEN)
221
+ url = os.environ.get(ENV_KBC_STORAGE_API_URL)
222
+ if not token or not url:
223
+ missing = [
224
+ name
225
+ for name, value in ((ENV_KBC_TOKEN, token), (ENV_KBC_STORAGE_API_URL, url))
226
+ if not value
227
+ ]
228
+ raise ConfigError(
229
+ f"{ENV_PROJECT_FROM_ENV} is set but {' and '.join(missing)} "
230
+ f"{'is' if len(missing) == 1 else 'are'} missing. Set both "
231
+ f"{ENV_KBC_TOKEN} and {ENV_KBC_STORAGE_API_URL}, or unset "
232
+ f"{ENV_PROJECT_FROM_ENV}."
233
+ )
234
+
235
+ # Keboola Storage tokens are `{projectId}-{tokenId}-{secret}`, so we can
236
+ # recover the project_id offline from the prefix. The real project_name
237
+ # needs an API call (verify_token) -- load() must stay offline, so it is
238
+ # left blank here; `project status` / `project info` show the verified
239
+ # name when a command actually talks to the API.
240
+ prefix = token.split("-", 1)[0]
241
+ project_id = int(prefix) if prefix.isdigit() else None
242
+ try:
243
+ config.projects[ENV_PROJECT_ALIAS] = ProjectConfig(
244
+ stack_url=url,
245
+ token=token,
246
+ project_id=project_id,
247
+ ephemeral=True,
248
+ )
249
+ except ValidationError as exc:
250
+ # Convert pydantic's raw error into a clean fail-fast message --
251
+ # this runs inside load(), which callers only guard for ConfigError.
252
+ reason = "; ".join(e.get("msg", "") for e in exc.errors()) or str(exc)
253
+ raise ConfigError(
254
+ f"{ENV_KBC_STORAGE_API_URL}={url!r} is not a usable stack URL: {reason}"
255
+ ) from exc
256
+ if not config.default_project:
257
+ config.default_project = ENV_PROJECT_ALIAS
258
+ logger.debug("Injected ephemeral '%s' project from environment", ENV_PROJECT_ALIAS)
259
+ return config
260
+
261
+ @staticmethod
262
+ def _strip_ephemeral_projects(config: AppConfig) -> AppConfig:
263
+ """Return a copy of ``config`` with ephemeral (env-synthesized) projects removed.
264
+
265
+ Defends against persisting an env token to disk: mutation methods do
266
+ ``load() -> mutate -> save()``, and ``load()`` may have injected the
267
+ ``__env__`` project. The original object is left intact because callers
268
+ keep using it after ``save()`` returns. If ``default_project`` pointed
269
+ at a stripped ephemeral alias, it is blanked (the next ``load()``
270
+ re-injects and re-defaults it).
271
+ """
272
+ ephemeral_aliases = {alias for alias, p in config.projects.items() if p.ephemeral}
273
+ if not ephemeral_aliases:
274
+ return config
275
+ clean = config.model_copy(deep=True)
276
+ for alias in ephemeral_aliases:
277
+ clean.projects.pop(alias, None)
278
+ if clean.default_project in ephemeral_aliases:
279
+ clean.default_project = next(iter(clean.projects), "")
280
+ return clean
281
+
282
+ @staticmethod
283
+ def _reject_ephemeral_mutation(config: AppConfig, alias: str, operation: str) -> None:
284
+ """Block mutations targeting an env-synthesized project (issue #359).
285
+
286
+ A `__env__` project injected from `KBAGENT_PROJECT_FROM_ENV` exists only
287
+ in memory and is stripped on save, so `remove`/`edit`/`rename`/branch
288
+ ops would otherwise report success and then silently vanish on the next
289
+ `load()`. Reject them with a clear, actionable message instead. A real
290
+ persisted project that happens to use the alias (``ephemeral=False``) is
291
+ unaffected.
292
+ """
293
+ project = config.projects.get(alias)
294
+ if project is not None and project.ephemeral:
295
+ raise ConfigError(
296
+ f"Project '{alias}' is synthesized from environment variables "
297
+ f"({ENV_PROJECT_FROM_ENV}) and cannot be {operation} -- it lives "
298
+ f"only in memory. To change it, update {ENV_KBC_TOKEN} / "
299
+ f"{ENV_KBC_STORAGE_API_URL}; to manage a persisted project, unset "
300
+ f"{ENV_PROJECT_FROM_ENV} and use 'project add'."
301
+ )
302
+
303
+ def save(self, config: AppConfig) -> None:
304
+ """Save configuration to disk with secure file permissions (0600).
305
+
306
+ Creates the config directory if it does not exist.
307
+ Uses atomic write to ensure the file is never on disk with
308
+ permissions broader than 0600 (prevents TOCTOU race condition).
309
+
310
+ Raises:
311
+ ConfigError: If the file cannot be written.
312
+ """
313
+ logger.debug("Saving config to %s", self._config_path)
314
+ lock_fd: int | None = None
315
+ try:
316
+ self._config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
317
+ self._ensure_gitignore()
318
+ # Never persist env-synthesized projects (issue #359): strip any
319
+ # ephemeral entry so the KBC_TOKEN from the environment stays in
320
+ # memory only. Operate on a copy -- callers reuse the AppConfig.
321
+ config = self._strip_ephemeral_projects(config)
322
+ # Prepend the agent-facing warning as the first field so any LLM
323
+ # that reads config.json sees it BEFORE any token value.
324
+ payload = {
325
+ "_warning": CLAUDE_CONFIG_WARNING,
326
+ **config.model_dump(mode="json"),
327
+ }
328
+ json_str = json.dumps(payload, indent=2, ensure_ascii=False)
329
+ data = (json_str + "\n").encode("utf-8")
330
+
331
+ # Acquire an exclusive lock on the target file before writing.
332
+ # The lock file is opened (or created) with 0600 permissions.
333
+ lock_fd = os.open(str(self._config_path), os.O_RDONLY | os.O_CREAT, 0o600)
334
+ _try_flock(lock_fd, _LOCK_EX)
335
+
336
+ # Write to a temp file created with 0600 from the start,
337
+ # then atomically rename into place. This avoids any window
338
+ # where the config file exists with world-readable permissions.
339
+ tmp_path = self._config_path.with_suffix(".tmp")
340
+ fd = os.open(str(tmp_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
341
+ try:
342
+ os.write(fd, data)
343
+ finally:
344
+ os.close(fd)
345
+ # On Windows (no fcntl), close the lock fd before os.replace —
346
+ # Windows cannot atomically replace a file that is currently open.
347
+ # On POSIX the fd stays open (flock is still held) until the finally block.
348
+ if not _HAS_FCNTL and lock_fd is not None:
349
+ os.close(lock_fd)
350
+ lock_fd = None
351
+ os.replace(str(tmp_path), str(self._config_path))
352
+ except OSError as exc:
353
+ raise ConfigError(f"Cannot write config file {self._config_path}: {exc}") from exc
354
+ finally:
355
+ if lock_fd is not None:
356
+ _try_flock(lock_fd, _LOCK_UN)
357
+ os.close(lock_fd)
358
+
359
+ def _ensure_gitignore(self) -> None:
360
+ """Create a .gitignore inside the config directory to protect tokens.
361
+
362
+ Defense in depth: even if the parent .gitignore covers this directory,
363
+ a local .gitignore prevents accidental commits if the parent rule is
364
+ removed or the config dir is copied elsewhere.
365
+ """
366
+ gitignore_path = self._config_dir / ".gitignore"
367
+ if gitignore_path.exists():
368
+ return
369
+ try:
370
+ gitignore_path.write_text(
371
+ "# Auto-generated by kbagent -- protects stored API tokens\n*\n",
372
+ encoding="utf-8",
373
+ )
374
+ except OSError:
375
+ logger.debug("Could not create .gitignore in %s", self._config_dir)
376
+
377
+ def add_project(self, alias: str, project: ProjectConfig) -> None:
378
+ """Add a project to the configuration.
379
+
380
+ Sets it as default if no default is set yet.
381
+
382
+ Args:
383
+ alias: Human-friendly project name.
384
+ project: Project configuration with stack URL, token, and project info.
385
+
386
+ Raises:
387
+ ConfigError: If the alias already exists.
388
+ """
389
+ config = self.load()
390
+ if alias in config.projects:
391
+ raise ConfigError(f"Project '{alias}' already exists. Use 'project edit' to modify it.")
392
+ config.projects[alias] = project
393
+ if not config.default_project:
394
+ config.default_project = alias
395
+ self.save(config)
396
+
397
+ def remove_project(self, alias: str) -> None:
398
+ """Remove a project from the configuration.
399
+
400
+ Updates the default project if the removed project was the default.
401
+
402
+ Args:
403
+ alias: The project alias to remove.
404
+
405
+ Raises:
406
+ ConfigError: If the alias does not exist.
407
+ """
408
+ config = self.load()
409
+ if alias not in config.projects:
410
+ raise ConfigError(f"Project '{alias}' not found.")
411
+ self._reject_ephemeral_mutation(config, alias, "removed")
412
+ del config.projects[alias]
413
+ if config.default_project == alias:
414
+ config.default_project = next(iter(config.projects), "")
415
+ self.save(config)
416
+
417
+ def get_project(self, alias: str) -> ProjectConfig | None:
418
+ """Get a project by alias, or None if not found."""
419
+ config = self.load()
420
+ return config.projects.get(alias)
421
+
422
+ def set_project_branch(self, alias: str, branch_id: int | None) -> None:
423
+ """Set or clear the active development branch for a project.
424
+
425
+ Args:
426
+ alias: The project alias.
427
+ branch_id: Branch ID to activate, or None to reset to main.
428
+
429
+ Raises:
430
+ ConfigError: If the alias does not exist.
431
+ """
432
+ config = self.load()
433
+ if alias not in config.projects:
434
+ raise ConfigError(f"Project '{alias}' not found.")
435
+ self._reject_ephemeral_mutation(config, alias, "modified")
436
+ config.projects[alias].active_branch_id = branch_id
437
+ self.save(config)
438
+
439
+ def edit_project(self, alias: str, **kwargs: str | int | None) -> None:
440
+ """Update fields on an existing project.
441
+
442
+ Only non-None keyword arguments are applied.
443
+
444
+ Args:
445
+ alias: The project alias to edit.
446
+ **kwargs: Fields to update (stack_url, token, project_name, project_id).
447
+
448
+ Raises:
449
+ ConfigError: If the alias does not exist.
450
+ """
451
+ config = self.load()
452
+ if alias not in config.projects:
453
+ raise ConfigError(f"Project '{alias}' not found.")
454
+ self._reject_ephemeral_mutation(config, alias, "edited")
455
+ project = config.projects[alias]
456
+ for key, value in kwargs.items():
457
+ if hasattr(project, key) and value is not None:
458
+ setattr(project, key, value)
459
+ config.projects[alias] = project
460
+ self.save(config)
461
+
462
+ def rename_project(self, old_alias: str, new_alias: str) -> None:
463
+ """Rename a project alias in the persisted config.
464
+
465
+ Pops ``old_alias`` from the projects dict and re-inserts the same
466
+ ``ProjectConfig`` under ``new_alias``. If ``default_project`` was
467
+ set to ``old_alias``, it is updated to ``new_alias`` so the pin
468
+ survives the rename. Both mutations are applied to the same
469
+ in-memory ``AppConfig`` and saved as one transaction.
470
+
471
+ Args:
472
+ old_alias: The current alias to rename from.
473
+ new_alias: The target alias to rename to.
474
+
475
+ Raises:
476
+ ConfigError: If ``old_alias`` does not exist or ``new_alias``
477
+ is already in use by another project.
478
+ """
479
+ config = self.load()
480
+ if old_alias not in config.projects:
481
+ raise ConfigError(f"Project '{old_alias}' not found.")
482
+ self._reject_ephemeral_mutation(config, old_alias, "renamed")
483
+ if new_alias in config.projects:
484
+ raise ConfigError(
485
+ f"Cannot rename '{old_alias}' to '{new_alias}': "
486
+ f"alias '{new_alias}' is already in use."
487
+ )
488
+ config.projects[new_alias] = config.projects.pop(old_alias)
489
+ if config.default_project == old_alias:
490
+ config.default_project = new_alias
491
+ self.save(config)
492
+
493
+ def add_dev_portal_identity(self, alias: str, identity: DeveloperPortalIdentity) -> None:
494
+ """Add a Developer Portal identity to the configuration.
495
+
496
+ Sets it as default if no default identity is set.
497
+
498
+ Raises:
499
+ ConfigError: If the alias already exists.
500
+ """
501
+ config = self.load()
502
+ if alias in config.dev_portal_identities:
503
+ raise ConfigError(
504
+ f"Developer Portal identity '{alias}' already exists. "
505
+ "Use 'dev-portal identity edit' to modify it."
506
+ )
507
+ config.dev_portal_identities[alias] = identity
508
+ if not config.default_dev_portal_identity:
509
+ config.default_dev_portal_identity = alias
510
+ self.save(config)
511
+
512
+ def remove_dev_portal_identity(self, alias: str) -> None:
513
+ """Remove a Developer Portal identity.
514
+
515
+ Falls the default through to the next available identity (or "" if none).
516
+
517
+ Raises:
518
+ ConfigError: If the alias does not exist.
519
+ """
520
+ config = self.load()
521
+ if alias not in config.dev_portal_identities:
522
+ raise ConfigError(f"Developer Portal identity '{alias}' not found.")
523
+ del config.dev_portal_identities[alias]
524
+ if config.default_dev_portal_identity == alias:
525
+ config.default_dev_portal_identity = next(iter(config.dev_portal_identities), "")
526
+ self.save(config)
527
+
528
+ def get_dev_portal_identity(self, alias: str) -> DeveloperPortalIdentity | None:
529
+ """Get a Developer Portal identity by alias, or None if not found."""
530
+ config = self.load()
531
+ return config.dev_portal_identities.get(alias)
532
+
533
+ def edit_dev_portal_identity(self, alias: str, **kwargs: str | None) -> None:
534
+ """Update fields on an existing Developer Portal identity.
535
+
536
+ Only non-None keyword arguments are applied.
537
+
538
+ Raises:
539
+ ConfigError: If the alias does not exist.
540
+ """
541
+ config = self.load()
542
+ if alias not in config.dev_portal_identities:
543
+ raise ConfigError(f"Developer Portal identity '{alias}' not found.")
544
+ ident = config.dev_portal_identities[alias]
545
+ for key, value in kwargs.items():
546
+ if hasattr(ident, key) and value is not None:
547
+ setattr(ident, key, value)
548
+ config.dev_portal_identities[alias] = ident
549
+ self.save(config)
550
+
551
+ def rename_dev_portal_identity(self, old_alias: str, new_alias: str) -> None:
552
+ """Rename a Developer Portal identity alias.
553
+
554
+ If the default was set to the old alias, it follows the rename.
555
+
556
+ Raises:
557
+ ConfigError: If old alias does not exist, or new alias is in use.
558
+ """
559
+ config = self.load()
560
+ if old_alias not in config.dev_portal_identities:
561
+ raise ConfigError(f"Developer Portal identity '{old_alias}' not found.")
562
+ if new_alias in config.dev_portal_identities:
563
+ raise ConfigError(
564
+ f"Cannot rename '{old_alias}' to '{new_alias}': "
565
+ f"alias '{new_alias}' is already in use."
566
+ )
567
+ config.dev_portal_identities[new_alias] = config.dev_portal_identities.pop(old_alias)
568
+ if config.default_dev_portal_identity == old_alias:
569
+ config.default_dev_portal_identity = new_alias
570
+ self.save(config)
571
+
572
+ def set_default_dev_portal_identity(self, alias: str) -> None:
573
+ """Set the default Developer Portal identity.
574
+
575
+ Raises:
576
+ ConfigError: If the alias does not exist.
577
+ """
578
+ config = self.load()
579
+ if alias not in config.dev_portal_identities:
580
+ raise ConfigError(f"Developer Portal identity '{alias}' not found.")
581
+ config.default_dev_portal_identity = alias
582
+ self.save(config)