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,566 @@
1
+ """Diff engine for comparing local configs against remote API state.
2
+
3
+ Produces a changeset describing what needs to be created, updated,
4
+ or deleted when pushing local changes to Keboola.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import copy
10
+ import hashlib
11
+ import json
12
+ from typing import Any
13
+
14
+ from ..constants import DIFF_MAX_DEPTH, DIFF_MAX_LINES, ENCRYPTED_PLACEHOLDER
15
+ from .secrets import is_encrypted_value
16
+
17
+ # Keys that are internal bookkeeping and should be excluded from comparison.
18
+ # NOTE: _configuration_extra is NOT ignored -- it carries the actual config
19
+ # payload for components like keboola.flow (phases, tasks, conditions) that
20
+ # don't use the standard "parameters" key.
21
+ _IGNORED_KEYS: frozenset[str] = frozenset({"_keboola", "version"})
22
+
23
+
24
+ class ConfigChange:
25
+ """Represents a single configuration or configuration-row change.
26
+
27
+ ``change_type`` values:
28
+
29
+ - ``"added"`` -- new local config/row not yet in remote
30
+ - ``"modified"`` -- local changed, remote unchanged (safe to push)
31
+ - ``"remote_modified"`` -- remote changed, local unchanged (run pull)
32
+ - ``"conflict"`` -- both sides changed since last pull
33
+ - ``"deleted"`` -- local file removed, wants to delete from remote
34
+
35
+ Row changes set ``is_row=True`` and carry ``parent_config_id`` so
36
+ ``sync push`` can dispatch them to the row-specific client methods
37
+ (``create_config_row`` / ``update_config_row`` / ``delete_config_row``).
38
+ """
39
+
40
+ def __init__(
41
+ self,
42
+ change_type: str,
43
+ component_id: str,
44
+ config_id: str, # empty string for new configs/rows
45
+ config_name: str,
46
+ path: str,
47
+ local_data: dict[str, Any] | None = None,
48
+ remote_data: dict[str, Any] | None = None,
49
+ details: list[str] | None = None,
50
+ is_row: bool = False,
51
+ parent_config_id: str = "",
52
+ ):
53
+ self.change_type = change_type
54
+ self.component_id = component_id
55
+ self.config_id = config_id
56
+ self.config_name = config_name
57
+ self.path = path
58
+ self.local_data = local_data
59
+ self.remote_data = remote_data
60
+ self.details = details or []
61
+ self.is_row = is_row
62
+ self.parent_config_id = parent_config_id
63
+
64
+ def to_dict(self) -> dict[str, Any]:
65
+ """Serialize to dict for JSON output."""
66
+ data: dict[str, Any] = {
67
+ "change_type": self.change_type,
68
+ "component_id": self.component_id,
69
+ "config_id": self.config_id,
70
+ "config_name": self.config_name,
71
+ "path": self.path,
72
+ "details": self.details,
73
+ }
74
+ if self.is_row:
75
+ data["is_row"] = True
76
+ data["parent_config_id"] = self.parent_config_id
77
+ return data
78
+
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Normalization helpers
82
+ # ---------------------------------------------------------------------------
83
+
84
+
85
+ def normalize_for_comparison(obj: Any) -> Any:
86
+ """Normalize a config dict for comparison.
87
+
88
+ - Replace all encrypted values (KBC::*Secure::*) with a fixed placeholder
89
+ to avoid false diffs from encryption nonces.
90
+ - Sort dict keys for consistent hashing.
91
+ - Strip ``_keboola`` metadata block (internal, not part of config content).
92
+ - Strip ``version`` key (local format marker).
93
+
94
+ Note: ``_configuration_extra`` is **not** stripped -- it carries real
95
+ config payload (e.g. ``keboola.flow`` phases/tasks, and a transformation's
96
+ ``variables_id`` / ``variables_values_id`` links). It is part of
97
+ :data:`config_hash`, so any code that mutates it (e.g. the fresh-CREATE
98
+ variable-link backfill in ``sync push``) must refresh the stored
99
+ ``pull_config_hash`` afterwards or ``sync diff`` will report a conflict.
100
+
101
+ Returns a deep copy -- the original object is never mutated.
102
+ """
103
+ return _normalize(copy.deepcopy(obj))
104
+
105
+
106
+ def _normalize(obj: Any) -> Any:
107
+ """Recursively normalize *obj* in place (operates on a deep copy)."""
108
+ if isinstance(obj, dict):
109
+ # Remove ignored keys
110
+ for key in _IGNORED_KEYS:
111
+ obj.pop(key, None)
112
+
113
+ # Recurse into remaining values, replacing encrypted strings
114
+ normalized: dict[str, Any] = {}
115
+ for key in sorted(obj.keys()):
116
+ normalized[key] = _normalize(obj[key])
117
+ return normalized
118
+
119
+ if isinstance(obj, list):
120
+ return [_normalize(item) for item in obj]
121
+
122
+ if is_encrypted_value(obj):
123
+ return ENCRYPTED_PLACEHOLDER
124
+
125
+ return obj
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Hashing
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ def config_hash(config_data: dict[str, Any]) -> str:
134
+ """Compute a normalized hash of a config for fast change detection."""
135
+ normalized = normalize_for_comparison(config_data)
136
+ return hashlib.sha256(json.dumps(normalized, sort_keys=True).encode()).hexdigest()
137
+
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Deep diff
141
+ # ---------------------------------------------------------------------------
142
+
143
+
144
+ def deep_diff(
145
+ local: dict[str, Any],
146
+ remote: dict[str, Any],
147
+ path: str = "",
148
+ ) -> list[str]:
149
+ """Compute human-readable diff descriptions between two config dicts.
150
+
151
+ Returns a list of strings like:
152
+
153
+ - ``"parameters.api_url changed: 'old' -> 'new'"``
154
+ - ``"output.tables[0].destination added"``
155
+ - ``"description removed"``
156
+
157
+ Skips encrypted values (shows ``'changed (encrypted)'`` instead of
158
+ actual values). Limits comparison depth to ``DIFF_MAX_DEPTH`` levels
159
+ and caps the output at ``DIFF_MAX_LINES`` entries.
160
+ """
161
+ results: list[str] = []
162
+ _deep_diff_recurse(
163
+ normalize_for_comparison(local),
164
+ normalize_for_comparison(remote),
165
+ path=path,
166
+ depth=0,
167
+ results=results,
168
+ )
169
+ return results[:DIFF_MAX_LINES]
170
+
171
+
172
+ def _deep_diff_recurse(
173
+ local_val: Any,
174
+ remote_val: Any,
175
+ *,
176
+ path: str,
177
+ depth: int,
178
+ results: list[str],
179
+ ) -> None:
180
+ """Recursive worker for :func:`deep_diff`."""
181
+ # Early exit when we've already collected enough detail lines.
182
+ if len(results) >= DIFF_MAX_LINES:
183
+ return
184
+
185
+ if type(local_val) != type(remote_val): # noqa: E721
186
+ results.append(_format_changed(path, remote_val, local_val))
187
+ return
188
+
189
+ if isinstance(local_val, dict) and isinstance(remote_val, dict):
190
+ _diff_dicts(local_val, remote_val, path=path, depth=depth, results=results)
191
+ return
192
+
193
+ if isinstance(local_val, list) and isinstance(remote_val, list):
194
+ _diff_lists(local_val, remote_val, path=path, depth=depth, results=results)
195
+ return
196
+
197
+ # Scalar comparison
198
+ if local_val != remote_val:
199
+ results.append(_format_changed(path, remote_val, local_val))
200
+
201
+
202
+ def _diff_dicts(
203
+ local_dict: dict[str, Any],
204
+ remote_dict: dict[str, Any],
205
+ *,
206
+ path: str,
207
+ depth: int,
208
+ results: list[str],
209
+ ) -> None:
210
+ """Compare two dicts key by key."""
211
+ all_keys = sorted(set(local_dict.keys()) | set(remote_dict.keys()))
212
+
213
+ for key in all_keys:
214
+ if len(results) >= DIFF_MAX_LINES:
215
+ return
216
+
217
+ child_path = f"{path}.{key}" if path else key
218
+ in_local = key in local_dict
219
+ in_remote = key in remote_dict
220
+
221
+ if in_local and not in_remote:
222
+ results.append(f"{child_path} added")
223
+ continue
224
+
225
+ if not in_local and in_remote:
226
+ results.append(f"{child_path} removed")
227
+ continue
228
+
229
+ # Both sides have the key -- recurse if within depth budget.
230
+ if depth < DIFF_MAX_DEPTH:
231
+ _deep_diff_recurse(
232
+ local_dict[key],
233
+ remote_dict[key],
234
+ path=child_path,
235
+ depth=depth + 1,
236
+ results=results,
237
+ )
238
+ else:
239
+ # Beyond depth limit, fall back to equality check.
240
+ if local_dict[key] != remote_dict[key]:
241
+ results.append(f"{child_path} changed")
242
+
243
+
244
+ def _diff_lists(
245
+ local_list: list[Any],
246
+ remote_list: list[Any],
247
+ *,
248
+ path: str,
249
+ depth: int,
250
+ results: list[str],
251
+ ) -> None:
252
+ """Compare two lists element by element."""
253
+ if len(local_list) != len(remote_list):
254
+ results.append(f"{path} list length changed: {len(remote_list)} -> {len(local_list)}")
255
+ return
256
+
257
+ for idx, (local_item, remote_item) in enumerate(zip(local_list, remote_list, strict=True)):
258
+ if len(results) >= DIFF_MAX_LINES:
259
+ return
260
+
261
+ child_path = f"{path}[{idx}]"
262
+
263
+ if depth < DIFF_MAX_DEPTH:
264
+ _deep_diff_recurse(
265
+ local_item,
266
+ remote_item,
267
+ path=child_path,
268
+ depth=depth + 1,
269
+ results=results,
270
+ )
271
+ else:
272
+ if local_item != remote_item:
273
+ results.append(f"{child_path} changed")
274
+
275
+
276
+ def _format_changed(path: str, old_val: Any, new_val: Any) -> str:
277
+ """Format a single scalar change, masking encrypted placeholders."""
278
+ label = path if path else "(root)"
279
+
280
+ if old_val == ENCRYPTED_PLACEHOLDER or new_val == ENCRYPTED_PLACEHOLDER:
281
+ return f"{label} changed (encrypted)"
282
+
283
+ return f"{label} changed: {_repr_short(old_val)} -> {_repr_short(new_val)}"
284
+
285
+
286
+ def _repr_short(value: Any, max_length: int = 60) -> str:
287
+ """Short repr of a value, truncated if too long."""
288
+ text = repr(value)
289
+ if len(text) > max_length:
290
+ return text[: max_length - 3] + "..."
291
+ return text
292
+
293
+
294
+ # ---------------------------------------------------------------------------
295
+ # Changeset computation
296
+ # ---------------------------------------------------------------------------
297
+
298
+
299
+ def compute_changeset(
300
+ local_configs: list[dict[str, Any]],
301
+ remote_configs: dict[str, dict[str, Any]],
302
+ tracked_keys: set[str] | None = None,
303
+ base_hashes: dict[str, str] | None = None,
304
+ local_override_hashes: dict[str, str] | None = None,
305
+ ) -> list[ConfigChange]:
306
+ """3-way diff: compare local vs base (pull_hash) vs remote.
307
+
308
+ Args:
309
+ local_configs: List of dicts with keys:
310
+ ``component_id``, ``config_id``, ``config_name``, ``path``, ``data``.
311
+ remote_configs: Dict keyed by ``"{component_id}/{config_id}"`` with
312
+ API config data (already converted to local format).
313
+ tracked_keys: Optional set of ``"{component_id}/{config_id}"`` keys
314
+ from the manifest. Only manifest-tracked remote configs can be
315
+ flagged as ``"deleted"``.
316
+ base_hashes: Optional dict of ``"{component_id}/{config_id}"`` ->
317
+ normalized config hash at last pull time. Enables 3-way diff:
318
+
319
+ - local changed, remote unchanged → ``"modified"`` (safe push)
320
+ - local unchanged, remote changed → ``"remote_modified"`` (pull)
321
+ - both changed → ``"conflict"``
322
+
323
+ When ``None``, falls back to 2-way diff (any difference →
324
+ ``"modified"``).
325
+ local_override_hashes: Optional dict of ``"{component_id}/{config_id}"``
326
+ -> hash to use for local side instead of computing from data.
327
+ Used when local files haven't changed since pull to avoid lossy
328
+ code-merge roundtrip.
329
+
330
+ Returns:
331
+ List of :class:`ConfigChange` objects.
332
+ """
333
+ changes: list[ConfigChange] = []
334
+ seen_remote_keys: set[str] = set()
335
+
336
+ for entry in local_configs:
337
+ component_id: str = entry["component_id"]
338
+ config_id: str = entry.get("config_id", "")
339
+ config_name: str = entry.get("config_name", "")
340
+ path: str = entry.get("path", "")
341
+ local_data: dict[str, Any] = entry.get("data", {})
342
+
343
+ remote_key = f"{component_id}/{config_id}" if config_id else ""
344
+
345
+ # New config (no id yet, or not in remote)
346
+ if not config_id or remote_key not in remote_configs:
347
+ changes.append(
348
+ ConfigChange(
349
+ change_type="added",
350
+ component_id=component_id,
351
+ config_id=config_id,
352
+ config_name=config_name,
353
+ path=path,
354
+ local_data=local_data,
355
+ )
356
+ )
357
+ if remote_key:
358
+ seen_remote_keys.add(remote_key)
359
+ continue
360
+
361
+ # Existing config -- 3-way compare
362
+ seen_remote_keys.add(remote_key)
363
+ remote_data: dict[str, Any] = remote_configs[remote_key]
364
+
365
+ # Use override hash if available (avoids lossy code roundtrip)
366
+ override_h = (local_override_hashes or {}).get(remote_key)
367
+ local_h = override_h if override_h else config_hash(local_data)
368
+ remote_h = config_hash(remote_data)
369
+
370
+ if local_h == remote_h:
371
+ # Unchanged -- skip
372
+ continue
373
+
374
+ # Determine change direction using base hash (pull-time snapshot)
375
+ base_h = (base_hashes or {}).get(remote_key)
376
+ if base_h is not None:
377
+ local_changed = local_h != base_h
378
+ remote_changed = remote_h != base_h
379
+ else:
380
+ # No base hash available -- fall back to 2-way
381
+ local_changed = True
382
+ remote_changed = False
383
+
384
+ if local_changed and remote_changed:
385
+ change_type = "conflict"
386
+ details = deep_diff(local_data, remote_data)
387
+ elif remote_changed and not local_changed:
388
+ change_type = "remote_modified"
389
+ details = deep_diff(remote_data, local_data)
390
+ else:
391
+ # local_changed (and not remote_changed), or fallback
392
+ change_type = "modified"
393
+ details = deep_diff(local_data, remote_data)
394
+
395
+ changes.append(
396
+ ConfigChange(
397
+ change_type=change_type,
398
+ component_id=component_id,
399
+ config_id=config_id,
400
+ config_name=config_name,
401
+ path=path,
402
+ local_data=local_data,
403
+ remote_data=remote_data,
404
+ details=details,
405
+ )
406
+ )
407
+
408
+ # Detect deleted configs: remote configs that were previously tracked
409
+ # in the manifest but whose local file was removed.
410
+ for remote_key, remote_data in remote_configs.items():
411
+ if remote_key in seen_remote_keys:
412
+ continue
413
+
414
+ # Only flag configs the manifest knows about.
415
+ # Unknown remote configs are new on the server side.
416
+ if tracked_keys is not None and remote_key not in tracked_keys:
417
+ continue
418
+
419
+ parts = remote_key.split("/", 1)
420
+ component_id = parts[0] if len(parts) > 0 else ""
421
+ config_id = parts[1] if len(parts) > 1 else ""
422
+
423
+ changes.append(
424
+ ConfigChange(
425
+ change_type="deleted",
426
+ component_id=component_id,
427
+ config_id=config_id,
428
+ config_name=remote_data.get("name", ""),
429
+ path="",
430
+ remote_data=remote_data,
431
+ )
432
+ )
433
+
434
+ return changes
435
+
436
+
437
+ def compute_row_changeset(
438
+ local_rows: list[dict[str, Any]],
439
+ remote_rows: dict[str, dict[str, Any]],
440
+ tracked_row_keys: set[str] | None = None,
441
+ base_hashes: dict[str, str] | None = None,
442
+ ) -> list[ConfigChange]:
443
+ """3-way diff for configuration rows, parallel to :func:`compute_changeset`.
444
+
445
+ Row keys are ``"{component_id}/{parent_config_id}/rows/{row_id}"`` so they
446
+ cannot collide with parent-config keys. Results carry ``is_row=True`` and
447
+ ``parent_config_id`` so ``sync push`` can route them to
448
+ ``client.create_config_row`` / ``update_config_row`` / ``delete_config_row``.
449
+
450
+ Args:
451
+ local_rows: List of dicts with keys:
452
+ ``component_id``, ``parent_config_id``, ``row_id``,
453
+ ``row_name``, ``path``, ``data``.
454
+ remote_rows: Dict keyed by ``"{component_id}/{parent_config_id}/rows/{row_id}"``
455
+ with API row data (already converted to local format via
456
+ :func:`api_row_to_local`).
457
+ tracked_row_keys: Optional set of row keys from the manifest.
458
+ Only manifest-tracked remote rows can be flagged as ``"deleted"``.
459
+ base_hashes: Optional dict of row_key -> normalized config hash at last
460
+ pull time. Enables 3-way diff (same semantics as parent configs).
461
+
462
+ Returns:
463
+ List of :class:`ConfigChange` objects, each with ``is_row=True``.
464
+ """
465
+ changes: list[ConfigChange] = []
466
+ seen_remote_keys: set[str] = set()
467
+
468
+ for entry in local_rows:
469
+ component_id: str = entry["component_id"]
470
+ parent_config_id: str = entry.get("parent_config_id", "")
471
+ row_id: str = entry.get("row_id", "")
472
+ row_name: str = entry.get("row_name", "")
473
+ path: str = entry.get("path", "")
474
+ local_data: dict[str, Any] = entry.get("data", {})
475
+
476
+ remote_key = (
477
+ f"{component_id}/{parent_config_id}/rows/{row_id}"
478
+ if row_id and parent_config_id
479
+ else ""
480
+ )
481
+
482
+ # New row (no id yet, or not in remote)
483
+ if not row_id or remote_key not in remote_rows:
484
+ changes.append(
485
+ ConfigChange(
486
+ change_type="added",
487
+ component_id=component_id,
488
+ config_id=row_id,
489
+ config_name=row_name,
490
+ path=path,
491
+ local_data=local_data,
492
+ is_row=True,
493
+ parent_config_id=parent_config_id,
494
+ )
495
+ )
496
+ if remote_key:
497
+ seen_remote_keys.add(remote_key)
498
+ continue
499
+
500
+ seen_remote_keys.add(remote_key)
501
+ remote_data: dict[str, Any] = remote_rows[remote_key]
502
+
503
+ local_h = config_hash(local_data)
504
+ remote_h = config_hash(remote_data)
505
+
506
+ if local_h == remote_h:
507
+ continue
508
+
509
+ base_h = (base_hashes or {}).get(remote_key)
510
+ if base_h is not None:
511
+ local_changed = local_h != base_h
512
+ remote_changed = remote_h != base_h
513
+ else:
514
+ local_changed = True
515
+ remote_changed = False
516
+
517
+ if local_changed and remote_changed:
518
+ change_type = "conflict"
519
+ details = deep_diff(local_data, remote_data)
520
+ elif remote_changed and not local_changed:
521
+ change_type = "remote_modified"
522
+ details = deep_diff(remote_data, local_data)
523
+ else:
524
+ change_type = "modified"
525
+ details = deep_diff(local_data, remote_data)
526
+
527
+ changes.append(
528
+ ConfigChange(
529
+ change_type=change_type,
530
+ component_id=component_id,
531
+ config_id=row_id,
532
+ config_name=row_name,
533
+ path=path,
534
+ local_data=local_data,
535
+ remote_data=remote_data,
536
+ details=details,
537
+ is_row=True,
538
+ parent_config_id=parent_config_id,
539
+ )
540
+ )
541
+
542
+ # Deleted rows: tracked in manifest but missing from local filesystem
543
+ for remote_key, remote_data in remote_rows.items():
544
+ if remote_key in seen_remote_keys:
545
+ continue
546
+ if tracked_row_keys is not None and remote_key not in tracked_row_keys:
547
+ continue
548
+
549
+ # remote_key shape: "{component_id}/{parent_config_id}/rows/{row_id}"
550
+ comp_and_parent, _, row_id = remote_key.rpartition("/rows/")
551
+ component_id, _, parent_config_id = comp_and_parent.partition("/")
552
+
553
+ changes.append(
554
+ ConfigChange(
555
+ change_type="deleted",
556
+ component_id=component_id,
557
+ config_id=row_id,
558
+ config_name=remote_data.get("name", ""),
559
+ path="",
560
+ remote_data=remote_data,
561
+ is_row=True,
562
+ parent_config_id=parent_config_id,
563
+ )
564
+ )
565
+
566
+ return changes
@@ -0,0 +1,93 @@
1
+ """Git helper functions for sync operations.
2
+
3
+ All functions are safe to call on machines without git installed -- they
4
+ return ``None`` or sensible defaults when the ``git`` binary is missing
5
+ or the directory is not a repository.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+
14
+ def is_git_repo(path: Path) -> bool:
15
+ """Return ``True`` if *path* is inside a git working tree."""
16
+ result = subprocess.run(
17
+ ["git", "rev-parse", "--is-inside-work-tree"],
18
+ cwd=path,
19
+ capture_output=True,
20
+ text=True,
21
+ check=False,
22
+ )
23
+ return result.returncode == 0 and result.stdout.strip() == "true"
24
+
25
+
26
+ def get_current_branch(path: Path) -> str | None:
27
+ """Return the current git branch name, or ``None`` on failure.
28
+
29
+ Uses ``git branch --show-current`` which works even in a freshly
30
+ initialized repo (no commits yet), unlike ``git rev-parse``.
31
+ """
32
+ result = subprocess.run(
33
+ ["git", "branch", "--show-current"],
34
+ cwd=path,
35
+ capture_output=True,
36
+ text=True,
37
+ check=False,
38
+ )
39
+ if result.returncode != 0:
40
+ return None
41
+ branch = result.stdout.strip()
42
+ return branch if branch else None
43
+
44
+
45
+ def get_default_branch(path: Path) -> str:
46
+ """Detect the default branch name (``main`` or ``master``).
47
+
48
+ Strategy:
49
+ 1. Check ``git config init.defaultBranch`` (user/repo setting).
50
+ 2. Check if ``refs/remotes/origin/main`` exists.
51
+ 3. Check if ``refs/remotes/origin/master`` exists.
52
+ 4. Fall back to ``"main"``.
53
+ """
54
+ # 1. git config
55
+ result = subprocess.run(
56
+ ["git", "config", "init.defaultBranch"],
57
+ cwd=path,
58
+ capture_output=True,
59
+ text=True,
60
+ check=False,
61
+ )
62
+ if result.returncode == 0 and result.stdout.strip():
63
+ return result.stdout.strip()
64
+
65
+ # 2. origin/main
66
+ result = subprocess.run(
67
+ ["git", "rev-parse", "--verify", "refs/remotes/origin/main"],
68
+ cwd=path,
69
+ capture_output=True,
70
+ text=True,
71
+ check=False,
72
+ )
73
+ if result.returncode == 0:
74
+ return "main"
75
+
76
+ # 3. origin/master
77
+ result = subprocess.run(
78
+ ["git", "rev-parse", "--verify", "refs/remotes/origin/master"],
79
+ cwd=path,
80
+ capture_output=True,
81
+ text=True,
82
+ check=False,
83
+ )
84
+ if result.returncode == 0:
85
+ return "master"
86
+
87
+ # 4. Use current branch name (works in fresh repos with no commits)
88
+ current = get_current_branch(path)
89
+ if current:
90
+ return current
91
+
92
+ # 5. Default
93
+ return "main"