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,1078 @@
1
+ """Internal helpers for :mod:`semantic_layer_service`.
2
+
3
+ Split out so the orchestrator class stays under the
4
+ CONTRIBUTING.md services hard ceiling (1,500 LOC). Each helper here is
5
+ pure-functional or operates on an externally-provided
6
+ :class:`MetastoreClient` -- there is no in-module state.
7
+
8
+ Helpers grouped by feature:
9
+
10
+ - Validation (basic + deep) :func:`validate_basic`,
11
+ :func:`validate_deep`
12
+ - Diff :func:`collect_side_from_file`,
13
+ :func:`diff_one_type`,
14
+ :func:`compare_attrs`
15
+ - Build (heuristic) :func:`heuristic_generate_model`,
16
+ :func:`fetch_table_schemas`
17
+ - Import / promote :func:`run_import_loop`,
18
+ :func:`run_promote_loop`
19
+ - Export I/O :func:`write_snapshot_to_file`,
20
+ :func:`default_export_path`,
21
+ :func:`build_export_snapshot`
22
+ - Constants reused across modules :data:`DIFF_IGNORED_KEYS`,
23
+ :data:`PUSH_ORDER`
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import json
29
+ import logging
30
+ import os
31
+ import re
32
+ from collections.abc import Callable
33
+ from concurrent.futures import ThreadPoolExecutor
34
+ from dataclasses import dataclass
35
+ from datetime import UTC, datetime
36
+ from pathlib import Path
37
+ from typing import TYPE_CHECKING, Any
38
+
39
+ from ..errors import ConfigError, ErrorCode, KeboolaApiError
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ if TYPE_CHECKING:
44
+ from ..metastore_client import SemanticType
45
+ from .storage_service import StorageService
46
+
47
+ # Re-exported from the main module to avoid a circular import; pulled in
48
+ # locally for type hints / runtime constants.
49
+
50
+ # Column reference regex (Snowflake-style ``"SCHEMA"."COLUMN"``).
51
+ _COLUMN_REF_RE = re.compile(r'"[^"]+"\."([^"]+)"')
52
+
53
+ # SUM(... PCT ...) heuristic.
54
+ _SUM_ON_PCT_RE = re.compile(r"\bSUM\s*\(\s*[^)]*\b(PCT|PERCENT|RATE)\b", re.IGNORECASE)
55
+
56
+ # AGG-on-STRING detection for --deep validate.
57
+ _AGG_ON_STRING_FUNCS = ("SUM", "AVG")
58
+
59
+ # Severity-band suffixes recognised by downstream pipelines.
60
+ _SEVERITY_SUFFIXES = ("_critical", "_warning", "_healthy", "_review")
61
+
62
+ # Keys ignored when deep-equality-comparing two items (the modelUUID is
63
+ # rewritten when promoting across projects, and the server-side
64
+ # timestamps drift every fetch).
65
+ DIFF_IGNORED_KEYS: tuple[str, ...] = (
66
+ "modelUUID",
67
+ "createdAt",
68
+ "lastUpdated",
69
+ "revision",
70
+ )
71
+
72
+ # Push order for both `import` and `promote` -- datasets before metrics
73
+ # (metric.dataset references a tableId), constraints last (their
74
+ # `metrics[]` list references metric names).
75
+ PUSH_ORDER: tuple[tuple[str, SemanticType], ...] = (
76
+ ("datasets", "semantic-dataset"),
77
+ ("metrics", "semantic-metric"),
78
+ ("relationships", "semantic-relationship"),
79
+ ("glossary", "semantic-glossary"),
80
+ ("constraints", "semantic-constraint"),
81
+ )
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class WorkerResult:
86
+ """Outcome of a per-table parallel fetch worker.
87
+
88
+ Shared between the deep-validation pass and the build heuristic.
89
+ Exactly one of ``detail`` / ``error`` is non-None.
90
+ """
91
+
92
+ table_id: str
93
+ detail: dict[str, Any] | None
94
+ error: str | None
95
+
96
+
97
+ # ── validate (basic + deep) ─────────────────────────────────────────
98
+
99
+
100
+ def validate_basic(
101
+ *,
102
+ datasets: list[dict[str, Any]],
103
+ metrics: list[dict[str, Any]],
104
+ relationships: list[dict[str, Any]],
105
+ constraints: list[dict[str, Any]],
106
+ glossary: list[dict[str, Any]],
107
+ errors: list[dict[str, str]],
108
+ warnings: list[dict[str, str]],
109
+ ) -> None:
110
+ """Pure in-memory validation (no API calls).
111
+
112
+ Mutates ``errors`` and ``warnings`` in place. See the orchestrator's
113
+ docstring for the full list of checks.
114
+ """
115
+ # DUPLICATES per type (name)
116
+ for label, items, key in (
117
+ ("dataset", datasets, "name"),
118
+ ("metric", metrics, "name"),
119
+ ("relationship", relationships, "name"),
120
+ ("constraint", constraints, "name"),
121
+ ("glossary", glossary, "term"),
122
+ ):
123
+ seen: set[str] = set()
124
+ dups: set[str] = set()
125
+ for it in items:
126
+ name = it.get(key, "")
127
+ if not name:
128
+ continue
129
+ if name in seen:
130
+ dups.add(name)
131
+ seen.add(name)
132
+ for dup in sorted(dups):
133
+ errors.append(
134
+ {"type": "DUPLICATE", "item": f"{label}:{dup}", "detail": f"duplicate {key}"}
135
+ )
136
+
137
+ dataset_tids = {d.get("tableId", "") for d in datasets if d.get("tableId")}
138
+ dataset_names = {d.get("name", "") for d in datasets if d.get("name")}
139
+
140
+ # DANGLING REL: from/to tableId must be in datasets
141
+ for rel in relationships:
142
+ for endpoint in ("from", "to"):
143
+ tid = rel.get(endpoint, "")
144
+ if tid and tid not in dataset_tids and tid not in dataset_names:
145
+ errors.append(
146
+ {
147
+ "type": "DANGLING_RELATIONSHIP",
148
+ "item": rel.get("name", "?"),
149
+ "detail": f"{endpoint}={tid!r} not in model datasets",
150
+ }
151
+ )
152
+
153
+ # DANGLING METRIC: metric.dataset is a tableId
154
+ for m in metrics:
155
+ ds = m.get("dataset", "")
156
+ if ds and ds not in dataset_tids and ds not in dataset_names:
157
+ errors.append(
158
+ {
159
+ "type": "DANGLING_METRIC",
160
+ "item": m.get("name", "?"),
161
+ "detail": f"dataset={ds!r} not in model datasets",
162
+ }
163
+ )
164
+
165
+ # SUM ON PCT
166
+ for m in metrics:
167
+ sql = m.get("sql", "") or ""
168
+ if _SUM_ON_PCT_RE.search(sql):
169
+ warnings.append(
170
+ {
171
+ "type": "SUM_ON_PCT",
172
+ "item": m.get("name", "?"),
173
+ "detail": "SUM() over a percentage/rate column is usually incorrect",
174
+ }
175
+ )
176
+
177
+ # CONSTRAINT ORPHAN + SEVERITY SUFFIX
178
+ metric_names = {m.get("name", "") for m in metrics if m.get("name")}
179
+ for c in constraints:
180
+ cname = c.get("name", "")
181
+ for mname in c.get("metrics", []) or []:
182
+ if mname not in metric_names:
183
+ errors.append(
184
+ {
185
+ "type": "CONSTRAINT_ORPHAN",
186
+ "item": cname or "?",
187
+ "detail": f"references missing metric {mname!r}",
188
+ }
189
+ )
190
+ if cname and not any(cname.endswith(suf) for suf in _SEVERITY_SUFFIXES):
191
+ warnings.append(
192
+ {
193
+ "type": "SEVERITY_SUFFIX",
194
+ "item": cname,
195
+ "detail": (
196
+ "constraint name should end with one of "
197
+ "_critical/_warning/_healthy/_review for downstream parsing"
198
+ ),
199
+ }
200
+ )
201
+
202
+
203
+ def validate_deep(
204
+ *,
205
+ alias: str,
206
+ storage: StorageService,
207
+ datasets: list[dict[str, Any]],
208
+ metrics: list[dict[str, Any]],
209
+ errors: list[dict[str, str]],
210
+ warnings: list[dict[str, str]],
211
+ ) -> None:
212
+ """Add deep checks that require a Snowflake schema fetch per dataset.
213
+
214
+ Mutates ``errors`` and ``warnings`` in place.
215
+ """
216
+
217
+ def _worker(ds: dict[str, Any]) -> WorkerResult:
218
+ tid = ds.get("tableId", "")
219
+ if not tid:
220
+ return WorkerResult(table_id=tid, detail=None, error="missing tableId")
221
+ try:
222
+ detail = storage.get_table_detail(alias, tid)
223
+ return WorkerResult(table_id=tid, detail=detail, error=None)
224
+ except (KeboolaApiError, ConfigError) as exc:
225
+ return WorkerResult(table_id=tid, detail=None, error=str(exc))
226
+
227
+ details_by_tid: dict[str, dict[str, Any]] = {}
228
+ with ThreadPoolExecutor(max_workers=8) as pool:
229
+ for outcome in pool.map(_worker, datasets):
230
+ if outcome.error is not None:
231
+ warnings.append(
232
+ {
233
+ "type": "DEEP_FETCH_FAILED",
234
+ "item": outcome.table_id or "?",
235
+ "detail": outcome.error,
236
+ }
237
+ )
238
+ continue
239
+ if outcome.detail is not None:
240
+ details_by_tid[outcome.table_id] = outcome.detail
241
+
242
+ # PHANTOM FIELD (declared field not in actual columns)
243
+ for ds in datasets:
244
+ tid = ds.get("tableId", "")
245
+ detail = details_by_tid.get(tid)
246
+ if detail is None:
247
+ continue
248
+ actual_columns = set(detail.get("columns", []))
249
+ declared = ds.get("fields", []) or []
250
+ for field in declared:
251
+ fname = field.get("name", "") if isinstance(field, dict) else str(field)
252
+ if fname and fname not in actual_columns:
253
+ errors.append(
254
+ {
255
+ "type": "PHANTOM_FIELD",
256
+ "item": f"{ds.get('name', '?')}.{fname}",
257
+ "detail": f"field {fname!r} not in storage table columns",
258
+ }
259
+ )
260
+
261
+ # METRIC PHANTOM + AGG ON STRING -- build a quick lookup: tableId -> {col_name: basetype}
262
+ col_types_by_tid: dict[str, dict[str, str]] = {}
263
+ for tid, detail in details_by_tid.items():
264
+ cols: dict[str, str] = {}
265
+ for col_info in detail.get("column_details", []) or []:
266
+ cols[col_info.get("name", "")] = (col_info.get("type", "") or "").upper()
267
+ col_types_by_tid[tid] = cols
268
+
269
+ for m in metrics:
270
+ sql = m.get("sql", "") or ""
271
+ mdataset = m.get("dataset", "")
272
+ col_types = col_types_by_tid.get(mdataset, {})
273
+ refs = set(_COLUMN_REF_RE.findall(sql))
274
+ if col_types:
275
+ actual = set(col_types.keys())
276
+ for ref in refs:
277
+ if ref not in actual:
278
+ errors.append(
279
+ {
280
+ "type": "METRIC_PHANTOM",
281
+ "item": m.get("name", "?"),
282
+ "detail": (
283
+ f"column {ref!r} referenced in sql not in dataset {mdataset!r}"
284
+ ),
285
+ }
286
+ )
287
+ # AGG ON STRING
288
+ for func in _AGG_ON_STRING_FUNCS:
289
+ pattern = re.compile(
290
+ rf'\b{func}\s*\(\s*[^)]*?"([^"]+)"\."([^"]+)"',
291
+ re.IGNORECASE,
292
+ )
293
+ for match in pattern.finditer(sql):
294
+ col = match.group(2)
295
+ col_type = col_types.get(col, "").upper()
296
+ if col_type == "STRING":
297
+ errors.append(
298
+ {
299
+ "type": "AGG_ON_STRING",
300
+ "item": m.get("name", "?"),
301
+ "detail": f"{func}() on STRING column {col!r}",
302
+ }
303
+ )
304
+
305
+
306
+ # ── diff ────────────────────────────────────────────────────────────
307
+
308
+
309
+ def compare_attrs(a: dict[str, Any], b: dict[str, Any]) -> list[str]:
310
+ """Return the sorted list of attribute keys that differ between two items."""
311
+ ignored = set(DIFF_IGNORED_KEYS)
312
+ keys = (set(a) | set(b)) - ignored - {"id"}
313
+ diff_keys: list[str] = []
314
+ for key in sorted(keys):
315
+ if a.get(key) != b.get(key):
316
+ diff_keys.append(key)
317
+ return diff_keys
318
+
319
+
320
+ def diff_one_type(
321
+ left: list[dict[str, Any]],
322
+ right: list[dict[str, Any]],
323
+ *,
324
+ id_key: str,
325
+ ) -> dict[str, Any]:
326
+ """Compute added/removed/changed for one entity type."""
327
+ left_by_key = {item.get(id_key, ""): item for item in left if item.get(id_key)}
328
+ right_by_key = {item.get(id_key, ""): item for item in right if item.get(id_key)}
329
+
330
+ added = sorted(k for k in right_by_key if k not in left_by_key)
331
+ removed = sorted(k for k in left_by_key if k not in right_by_key)
332
+ changed: list[dict[str, Any]] = []
333
+ for key in sorted(set(left_by_key) & set(right_by_key)):
334
+ diff_keys = compare_attrs(left_by_key[key], right_by_key[key])
335
+ if diff_keys:
336
+ changed.append({id_key: key, "diff_keys": diff_keys})
337
+ return {"added": added, "removed": removed, "changed": changed}
338
+
339
+
340
+ def collect_side_from_file(file: Path) -> dict[str, Any]:
341
+ """Resolve one diff side from a snapshot file.
342
+
343
+ Returns the shape consumed by the diff orchestrator:
344
+ ``{"ref": {"source": "file", "ref": <path>, "model": {...}},
345
+ "data": {<plural>: [attrs_dict, ...]}}``.
346
+ """
347
+ try:
348
+ payload = json.loads(file.read_text(encoding="utf-8"))
349
+ except OSError as exc:
350
+ raise KeboolaApiError(
351
+ message=f"Cannot read --file {file}: {exc}",
352
+ error_code=ErrorCode.READ_ERROR,
353
+ ) from exc
354
+ except json.JSONDecodeError as exc:
355
+ raise KeboolaApiError(
356
+ message=f"File {file} is not valid JSON: {exc}",
357
+ error_code=ErrorCode.INVALID_FORMAT,
358
+ ) from exc
359
+
360
+ def _attrs(items: list[Any]) -> list[dict[str, Any]]:
361
+ out: list[dict[str, Any]] = []
362
+ for item in items or []:
363
+ if isinstance(item, dict) and "attributes" in item:
364
+ merged = dict(item.get("attributes") or {})
365
+ merged["id"] = item.get("id", "")
366
+ out.append(merged)
367
+ elif isinstance(item, dict):
368
+ out.append(item)
369
+ return out
370
+
371
+ return {
372
+ "ref": {
373
+ "source": "file",
374
+ "ref": str(file),
375
+ "model": payload.get("model", {}),
376
+ },
377
+ "data": {
378
+ "datasets": _attrs(payload.get("datasets", [])),
379
+ "metrics": _attrs(payload.get("metrics", [])),
380
+ "relationships": _attrs(payload.get("relationships", [])),
381
+ "constraints": _attrs(payload.get("constraints", [])),
382
+ "glossary": _attrs(payload.get("glossary", [])),
383
+ },
384
+ }
385
+
386
+
387
+ # ── build (heuristic generator) ─────────────────────────────────────
388
+
389
+
390
+ def run_import_loop(
391
+ client: Any,
392
+ *,
393
+ snapshot: dict[str, Any],
394
+ target_model_uuid: str,
395
+ existing_by_type: dict[Any, list[dict[str, Any]]],
396
+ type_filter: set[str] | None,
397
+ dry_run: bool,
398
+ overwrite: bool,
399
+ ) -> dict[str, Any]:
400
+ """Replay a snapshot into the target model.
401
+
402
+ Returns the per-type stats dict matching the orchestrator's contract.
403
+ Push order is :data:`PUSH_ORDER`. Errors are accumulated per item;
404
+ one failure does not abort the rest.
405
+ """
406
+ imported: dict[str, Any] = {}
407
+ for plural, type_slug in PUSH_ORDER:
408
+ if type_filter is not None and plural not in type_filter:
409
+ continue
410
+ source_items = snapshot.get(plural, []) or []
411
+ id_key = "term" if plural == "glossary" else "name"
412
+ existing_by_name: dict[str, dict[str, Any]] = {
413
+ (item.get("attributes") or {}).get(id_key, ""): item
414
+ for item in existing_by_type.get(type_slug, [])
415
+ if (item.get("attributes") or {}).get(id_key)
416
+ }
417
+
418
+ per_type: dict[str, Any] = {
419
+ "created": 0,
420
+ "skipped": 0,
421
+ "overwritten": 0,
422
+ "failed": [],
423
+ }
424
+ for raw_item in source_items:
425
+ if not isinstance(raw_item, dict):
426
+ continue
427
+ attrs = dict(raw_item.get("attributes") or {})
428
+ attrs["modelUUID"] = target_model_uuid # rewrite
429
+ key = attrs.get(id_key, "")
430
+ if not key:
431
+ per_type["failed"].append({"name": "?", "reason": f"missing {id_key} on item"})
432
+ continue
433
+
434
+ if key in existing_by_name:
435
+ if not overwrite:
436
+ per_type["skipped"] += 1
437
+ continue
438
+ if dry_run:
439
+ per_type["overwritten"] += 1
440
+ continue
441
+ try:
442
+ client.delete_item(type_slug, existing_by_name[key]["id"])
443
+ client.post_item(type_slug, name=key, data=attrs)
444
+ per_type["overwritten"] += 1
445
+ except KeboolaApiError as exc:
446
+ per_type["failed"].append({"name": key, "reason": exc.message})
447
+ continue
448
+
449
+ if dry_run:
450
+ per_type["created"] += 1
451
+ continue
452
+ try:
453
+ client.post_item(type_slug, name=key, data=attrs)
454
+ per_type["created"] += 1
455
+ except KeboolaApiError as exc:
456
+ per_type["failed"].append({"name": key, "reason": exc.message})
457
+
458
+ imported[plural] = per_type
459
+ return imported
460
+
461
+
462
+ def run_promote_loop(
463
+ target_client: Any,
464
+ *,
465
+ src_children: dict[Any, list[dict[str, Any]]],
466
+ tgt_children: dict[Any, list[dict[str, Any]]],
467
+ target_model_uuid: str,
468
+ type_filter: set[str] | None,
469
+ dry_run: bool,
470
+ ) -> dict[str, Any]:
471
+ """Run the additive + overwrite promote loop.
472
+
473
+ NEW: POST to target. CHANGED: DELETE+POST. IDENTICAL: skip.
474
+ Items only in target are never deleted (additive only).
475
+ Returns ``{plural: {new, overwritten, identical, failed, changes}}``.
476
+ """
477
+ result: dict[str, Any] = {}
478
+ for plural, type_slug in PUSH_ORDER:
479
+ if type_filter is not None and plural not in type_filter:
480
+ continue
481
+ id_key = "term" if plural == "glossary" else "name"
482
+ src_items = src_children.get(type_slug, []) or []
483
+ tgt_by_key: dict[str, dict[str, Any]] = {
484
+ (it.get("attributes") or {}).get(id_key, ""): it
485
+ for it in tgt_children.get(type_slug, [])
486
+ if (it.get("attributes") or {}).get(id_key)
487
+ }
488
+
489
+ stats: dict[str, Any] = {
490
+ "new": 0,
491
+ "overwritten": 0,
492
+ "identical": 0,
493
+ "failed": [],
494
+ "changes": [],
495
+ }
496
+
497
+ for src in src_items:
498
+ src_attrs = dict(src.get("attributes") or {})
499
+ src_attrs["modelUUID"] = target_model_uuid # rewrite
500
+ key = src_attrs.get(id_key, "")
501
+ if not key:
502
+ stats["failed"].append({"name": "?", "reason": f"missing {id_key} on source item"})
503
+ continue
504
+
505
+ if key in tgt_by_key:
506
+ diff_keys = compare_attrs(src_attrs, dict(tgt_by_key[key].get("attributes") or {}))
507
+ if not diff_keys:
508
+ stats["identical"] += 1
509
+ continue
510
+ if dry_run:
511
+ stats["overwritten"] += 1
512
+ stats["changes"].append({id_key: key, "diff_keys": diff_keys})
513
+ continue
514
+ try:
515
+ target_client.delete_item(type_slug, tgt_by_key[key]["id"])
516
+ target_client.post_item(type_slug, name=key, data=src_attrs)
517
+ stats["overwritten"] += 1
518
+ stats["changes"].append({id_key: key, "diff_keys": diff_keys})
519
+ except KeboolaApiError as exc:
520
+ stats["failed"].append({"name": key, "reason": exc.message})
521
+ continue
522
+
523
+ if dry_run:
524
+ stats["new"] += 1
525
+ continue
526
+ try:
527
+ target_client.post_item(type_slug, name=key, data=src_attrs)
528
+ stats["new"] += 1
529
+ except KeboolaApiError as exc:
530
+ stats["failed"].append({"name": key, "reason": exc.message})
531
+
532
+ result[plural] = stats
533
+ return result
534
+
535
+
536
+ def push_built_model(
537
+ client: Any,
538
+ *,
539
+ generated: dict[str, Any],
540
+ model_name_or_uuid: str | None,
541
+ resolve_model_fn: Callable[[Any, str], tuple[str, dict[str, Any]]],
542
+ keep_on_failure: bool = False,
543
+ ) -> tuple[dict[str, int], str, dict[str, Any] | None]:
544
+ """Push a heuristic-generated model + children to the metastore.
545
+
546
+ Returns ``(counts, model_uuid, model_item)`` where ``model_item``
547
+ is the freshly-created model record (``None`` when updating an
548
+ existing model). ``counts`` maps each plural to the number of
549
+ successfully POSTed children.
550
+
551
+ Rollback semantics (issue #295): on any POST failure during the
552
+ children loop, every successfully-POSTed child is DELETEd in
553
+ reverse order, then the model itself is DELETEd **only when this
554
+ call created it** (i.e. ``model_name_or_uuid`` was None). The
555
+ original exception is re-raised wrapped in a KeboolaApiError that
556
+ carries the rollback summary in ``details``. Pass
557
+ ``keep_on_failure=True`` to skip cleanup for forensic inspection.
558
+ """
559
+ if model_name_or_uuid is None:
560
+ # Create the model.
561
+ model_attrs: dict[str, Any] = {
562
+ "name": generated["name"],
563
+ "sql_dialect": generated.get("sql_dialect", "Snowflake"),
564
+ }
565
+ if generated.get("description"):
566
+ model_attrs["description"] = generated["description"]
567
+ model_item = client.post_item("semantic-model", name=generated["name"], data=model_attrs)
568
+ model_uuid = model_item["id"]
569
+ model_created_here = True
570
+ else:
571
+ model_uuid, _ = resolve_model_fn(client, model_name_or_uuid)
572
+ model_item = None
573
+ model_created_here = False
574
+
575
+ posted_children: list[tuple[SemanticType, str, str]] = []
576
+ counts: dict[str, int] = {plural: 0 for plural, _ in PUSH_ORDER}
577
+ # Sentinel: the except clause references `plural` and `name` to tag the
578
+ # failing row in the wrapped error message; pre-init so they're defined
579
+ # even on the rare path where the loop never started a row (e.g. the
580
+ # first POST raised before either was set inside the loop body).
581
+ plural = ""
582
+ name = ""
583
+ try:
584
+ for plural, type_slug in PUSH_ORDER:
585
+ for item in generated.get(plural, []) or []:
586
+ # Resolve `name` BEFORE `dict(item)` so an exception during
587
+ # the dict-copy (or in the POST itself) tags the wrapped
588
+ # error with the current row, not the previous one.
589
+ name = (
590
+ (item.get("name") if isinstance(item, dict) else "")
591
+ or (item.get("term") if isinstance(item, dict) else "")
592
+ or ""
593
+ )
594
+ if not name:
595
+ continue
596
+ attrs = dict(item)
597
+ attrs["modelUUID"] = model_uuid
598
+ posted = client.post_item(type_slug, name=name, data=attrs)
599
+ posted_id = str(posted.get("id", "") or "") if isinstance(posted, dict) else ""
600
+ if not posted_id:
601
+ # Defensive: the metastore always returns `id` in a
602
+ # dict body, but a proxy / middleware that mangles
603
+ # the response would otherwise leave this child
604
+ # untrackable for rollback. Skip the counts increment
605
+ # so the visible state matches what we can actually
606
+ # clean up.
607
+ logger.warning(
608
+ "POST %s name=%s returned no id (response type=%s); "
609
+ "skipping rollback tracking",
610
+ type_slug,
611
+ name,
612
+ type(posted).__name__,
613
+ )
614
+ continue
615
+ posted_children.append((type_slug, posted_id, name))
616
+ counts[plural] += 1
617
+ except (KeyboardInterrupt, SystemExit):
618
+ raise
619
+ except Exception as exc:
620
+ # Catch broadly: client.post_item can raise httpx.RequestError
621
+ # subclasses (ReadError, ProtocolError, ...) that are NOT
622
+ # KeboolaApiError; without the rollback path on those, the partial
623
+ # state this PR exists to clean up would still leak. Re-raise of
624
+ # KeyboardInterrupt / SystemExit above keeps Ctrl-C honest.
625
+ api_exc = exc if isinstance(exc, KeboolaApiError) else None
626
+ error_code = api_exc.error_code if api_exc else ErrorCode.INTERNAL_ERROR
627
+ status_code = api_exc.status_code if api_exc else 500
628
+ exc_message = api_exc.message if api_exc else str(exc) or type(exc).__name__
629
+
630
+ if keep_on_failure:
631
+ logger.info(
632
+ "build_model push failed for %s/%s; preserving %d posted children + model %s "
633
+ "(--keep-on-failure set)",
634
+ plural,
635
+ name,
636
+ len(posted_children),
637
+ model_uuid,
638
+ )
639
+ raise KeboolaApiError(
640
+ message=(
641
+ f"build_model push failed at {plural}/{name!r}: {exc_message}. "
642
+ f"Partial state preserved (--keep-on-failure): {len(posted_children)} "
643
+ f"children posted, model {model_uuid} kept."
644
+ ),
645
+ error_code=error_code,
646
+ status_code=status_code,
647
+ details={
648
+ "rollback": {
649
+ "attempted": False,
650
+ "reason": "keep_on_failure",
651
+ "posted_children": len(posted_children),
652
+ "model_created_here": model_created_here,
653
+ "model_uuid": model_uuid,
654
+ }
655
+ },
656
+ ) from exc
657
+ deleted = 0
658
+ failed_deletes: list[dict[str, str]] = []
659
+ for type_slug, child_id, child_name in reversed(posted_children):
660
+ try:
661
+ client.delete_item(type_slug, child_id)
662
+ deleted += 1
663
+ except (KeyboardInterrupt, SystemExit):
664
+ raise
665
+ except Exception as del_exc:
666
+ # Same rationale as the outer broad catch: a non-API
667
+ # exception from delete_item must not abort the cleanup or
668
+ # mask the original POST failure. logger.warning (no
669
+ # traceback) keeps log volume bounded when the metastore
670
+ # is fully down -- 200+ ERROR-level stack traces would
671
+ # otherwise drown out the original POST failure. The
672
+ # consolidated summary line below carries the full count.
673
+ failed_deletes.append(
674
+ {
675
+ "type": type_slug,
676
+ "id": child_id,
677
+ "name": child_name,
678
+ "error": (
679
+ del_exc.message
680
+ if isinstance(del_exc, KeboolaApiError)
681
+ else str(del_exc) or type(del_exc).__name__
682
+ ),
683
+ }
684
+ )
685
+ logger.warning(
686
+ "Rollback DELETE failed for %s id=%s name=%s: %s",
687
+ type_slug,
688
+ child_id,
689
+ child_name,
690
+ failed_deletes[-1]["error"],
691
+ )
692
+ model_deleted = False
693
+ model_delete_error: str | None = None
694
+ if model_created_here:
695
+ try:
696
+ client.delete_item("semantic-model", model_uuid)
697
+ model_deleted = True
698
+ except (KeyboardInterrupt, SystemExit):
699
+ raise
700
+ except Exception as del_exc:
701
+ model_delete_error = (
702
+ del_exc.message
703
+ if isinstance(del_exc, KeboolaApiError)
704
+ else str(del_exc) or type(del_exc).__name__
705
+ )
706
+ # logger.exception here (with traceback) because there's
707
+ # only ever ONE model-delete; the volume concern that
708
+ # applies to the per-child loop does not.
709
+ logger.exception(
710
+ "Rollback DELETE of created model %s failed during build_model cleanup",
711
+ model_uuid,
712
+ )
713
+ if failed_deletes:
714
+ logger.error(
715
+ "build_model rollback summary: %d/%d child DELETEs failed during cleanup",
716
+ len(failed_deletes),
717
+ len(posted_children),
718
+ )
719
+ raise KeboolaApiError(
720
+ message=(
721
+ f"build_model push failed at {plural}/{name!r}: {exc_message}. "
722
+ f"Rollback deleted {deleted}/{len(posted_children)} child(ren)"
723
+ + (
724
+ f" and the model ({model_uuid})."
725
+ if model_deleted
726
+ else (
727
+ f"; model ({model_uuid}) delete failed: {model_delete_error}."
728
+ if model_created_here
729
+ else f"; existing model ({model_uuid}) preserved."
730
+ )
731
+ )
732
+ ),
733
+ error_code=error_code,
734
+ status_code=status_code,
735
+ details={
736
+ "rollback": {
737
+ "attempted": True,
738
+ "posted_children": len(posted_children),
739
+ "deleted": deleted,
740
+ "failed_deletes": failed_deletes,
741
+ "model_created_here": model_created_here,
742
+ "model_deleted": model_deleted,
743
+ "model_delete_error": model_delete_error,
744
+ "model_uuid": model_uuid,
745
+ }
746
+ },
747
+ ) from exc
748
+ return counts, model_uuid, model_item
749
+
750
+
751
+ def synthesize_role_classified_fields(
752
+ storage: StorageService,
753
+ alias: str,
754
+ table_id: str,
755
+ classify_role: Callable[[str, str], str],
756
+ ) -> list[dict[str, Any]]:
757
+ """Fetch storage schema for ``table_id`` and synthesise ``fields[]`` with role heuristics.
758
+
759
+ Used by ``add_dataset --deep-fields``. Returns the list of
760
+ ``{name, type, role}`` dicts (one per column); empty when the
761
+ storage table has no columns.
762
+ """
763
+ detail = storage.get_table_detail(alias, table_id)
764
+ fields: list[dict[str, Any]] = []
765
+ for col in detail.get("column_details", []) or []:
766
+ cname = col.get("name", "")
767
+ basetype = col.get("type", "") or col.get("native_type", "")
768
+ fields.append(
769
+ {
770
+ "name": cname,
771
+ "type": basetype,
772
+ "role": classify_role(cname, basetype),
773
+ }
774
+ )
775
+ return fields
776
+
777
+
778
+ def fetch_table_schemas(
779
+ storage: StorageService,
780
+ alias: str,
781
+ table_ids: list[str],
782
+ ) -> tuple[dict[str, dict[str, Any]], list[dict[str, str]]]:
783
+ """Fetch storage schemas for a list of tableIds in parallel.
784
+
785
+ Returns ``(schemas_by_tid, fetch_errors)`` where ``fetch_errors`` is
786
+ a list of ``{"table_id", "error"}`` dicts -- one per missing /
787
+ failed table. Errors are returned (not raised) so the build pass
788
+ can decide whether to abort.
789
+ """
790
+
791
+ def _worker(tid: str) -> WorkerResult:
792
+ try:
793
+ detail = storage.get_table_detail(alias, tid)
794
+ return WorkerResult(table_id=tid, detail=detail, error=None)
795
+ except (KeboolaApiError, ConfigError) as exc:
796
+ return WorkerResult(table_id=tid, detail=None, error=str(exc))
797
+
798
+ schemas_by_tid: dict[str, dict[str, Any]] = {}
799
+ fetch_errors: list[dict[str, str]] = []
800
+ with ThreadPoolExecutor(max_workers=8) as pool:
801
+ for outcome in pool.map(_worker, table_ids):
802
+ if outcome.error is not None:
803
+ fetch_errors.append({"table_id": outcome.table_id, "error": outcome.error})
804
+ continue
805
+ if outcome.detail is not None:
806
+ schemas_by_tid[outcome.table_id] = outcome.detail
807
+ return schemas_by_tid, fetch_errors
808
+
809
+
810
+ def resolve_model_uuid(
811
+ client: Any,
812
+ model_name_or_uuid: str | None,
813
+ ) -> tuple[str, dict[str, Any]]:
814
+ """Resolve a model selector to ``(uuid, attributes_dict)``.
815
+
816
+ See :meth:`SemanticLayerService._resolve_model` for the contract.
817
+ Lives here so the orchestrator class stays under the file-size
818
+ budget; the class method is a one-line delegate.
819
+
820
+ Raises ``ConfigError`` when the selector is ambiguous or missing.
821
+ """
822
+ models = client.list_items("semantic-model")
823
+ if not models:
824
+ raise ConfigError(
825
+ "Project has no semantic-layer models. Use "
826
+ "'kbagent semantic-layer model create' to create one."
827
+ )
828
+
829
+ if model_name_or_uuid is None:
830
+ if len(models) == 1:
831
+ return models[0]["id"], dict(models[0].get("attributes") or {})
832
+ names = ", ".join(sorted((m.get("attributes") or {}).get("name", "?") for m in models))
833
+ raise ConfigError(f"Project has {len(models)} models — specify --model. Available: {names}")
834
+
835
+ # Try UUID match first (exact ID match)
836
+ for m in models:
837
+ if m.get("id") == model_name_or_uuid:
838
+ return m["id"], dict(m.get("attributes") or {})
839
+
840
+ # Then name match
841
+ name_matches = [
842
+ m for m in models if (m.get("attributes") or {}).get("name") == model_name_or_uuid
843
+ ]
844
+ if len(name_matches) == 1:
845
+ return name_matches[0]["id"], dict(name_matches[0].get("attributes") or {})
846
+ if len(name_matches) > 1:
847
+ raise ConfigError(
848
+ f"Multiple models found with name '{model_name_or_uuid}'. Specify the UUID instead."
849
+ )
850
+
851
+ names = ", ".join(sorted((m.get("attributes") or {}).get("name", "?") for m in models))
852
+ raise ConfigError(f"Model '{model_name_or_uuid}' not found. Available: {names}")
853
+
854
+
855
+ def unpack_attrs_with_id(
856
+ items: list[dict[str, Any]], *, id_field: str = "_id"
857
+ ) -> list[dict[str, Any]]:
858
+ """Flatten ``{type, id, attributes}`` items to attributes dicts + injected id field.
859
+
860
+ Used by validate_model and other callers that need attribute-only
861
+ lists with the server id preserved (under ``_id`` by default so it
862
+ doesn't collide with the attribute namespace).
863
+ """
864
+ return [dict(i.get("attributes") or {}, **{id_field: i.get("id", "")}) for i in items]
865
+
866
+
867
+ def unpack_children_by_plural(
868
+ raw_by_type: dict[Any, list[dict[str, Any]]],
869
+ *,
870
+ id_field: str = "id",
871
+ ) -> dict[str, list[dict[str, Any]]]:
872
+ """Convert ``raw_by_type[semantic-X]`` lists to attribute dicts keyed by plural.
873
+
874
+ Returns a dict with keys ``datasets``, ``metrics``, ``relationships``,
875
+ ``constraints``, ``glossary`` -- ready to splat into a result envelope.
876
+ """
877
+ return {
878
+ "datasets": unpack_attrs_with_id(
879
+ raw_by_type.get("semantic-dataset", []), id_field=id_field
880
+ ),
881
+ "metrics": unpack_attrs_with_id(raw_by_type.get("semantic-metric", []), id_field=id_field),
882
+ "relationships": unpack_attrs_with_id(
883
+ raw_by_type.get("semantic-relationship", []), id_field=id_field
884
+ ),
885
+ "constraints": unpack_attrs_with_id(
886
+ raw_by_type.get("semantic-constraint", []), id_field=id_field
887
+ ),
888
+ "glossary": unpack_attrs_with_id(
889
+ raw_by_type.get("semantic-glossary", []), id_field=id_field
890
+ ),
891
+ }
892
+
893
+
894
+ def default_export_path(model_name: str) -> Path:
895
+ """Default ``./sl_export_{model_name}_{YYYYMMDD_HHMMSS}.json`` path."""
896
+ ts = datetime.now(tz=UTC).strftime("%Y%m%d_%H%M%S")
897
+ safe_name = re.sub(r"[^A-Za-z0-9._-]+", "_", str(model_name)) or "model"
898
+ return Path.cwd() / f"sl_export_{safe_name}_{ts}.json"
899
+
900
+
901
+ def build_export_snapshot(
902
+ *,
903
+ alias: str,
904
+ model_uuid: str,
905
+ model_attrs: dict[str, Any],
906
+ raw_by_type: dict[Any, list[dict[str, Any]]],
907
+ ) -> dict[str, Any]:
908
+ """Assemble the self-describing export envelope."""
909
+ exported_at = datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
910
+ return {
911
+ "exported_at": exported_at,
912
+ "project": alias,
913
+ "model": dict(model_attrs, id=model_uuid),
914
+ "datasets": raw_by_type.get("semantic-dataset", []),
915
+ "metrics": raw_by_type.get("semantic-metric", []),
916
+ "relationships": raw_by_type.get("semantic-relationship", []),
917
+ "constraints": raw_by_type.get("semantic-constraint", []),
918
+ "glossary": raw_by_type.get("semantic-glossary", []),
919
+ }
920
+
921
+
922
+ def write_snapshot_to_file(snapshot: dict[str, Any], output_path: Path) -> None:
923
+ """Atomic-write a JSON snapshot with 0o644 perms and O_NOFOLLOW.
924
+
925
+ O_NOFOLLOW refuses to follow a pre-existing symlink at the chosen
926
+ path so a malicious --output (or a planted symlink in CWD) cannot
927
+ redirect the write to a sensitive file.
928
+ """
929
+ payload = json.dumps(snapshot, indent=2).encode("utf-8")
930
+ fd = os.open(
931
+ str(output_path),
932
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW,
933
+ 0o644,
934
+ )
935
+ try:
936
+ os.write(fd, payload)
937
+ finally:
938
+ os.close(fd)
939
+
940
+
941
+ # Map warehouse-native column types onto the closed set the metastore
942
+ # accepts for `fields[*].type`. Anything not matched falls through to
943
+ # "string" — safest default for legacy untyped Storage tables.
944
+ _FIELD_TYPE_MAP: dict[str, str] = {
945
+ # strings
946
+ "varchar": "string",
947
+ "char": "string",
948
+ "string": "string",
949
+ "text": "string",
950
+ "nvarchar": "string",
951
+ "nchar": "string",
952
+ # integers
953
+ "int": "integer",
954
+ "integer": "integer",
955
+ "bigint": "integer",
956
+ "smallint": "integer",
957
+ "tinyint": "integer",
958
+ # decimals
959
+ "decimal": "decimal",
960
+ "numeric": "decimal",
961
+ "number": "decimal",
962
+ "float": "decimal",
963
+ "double": "decimal",
964
+ "real": "decimal",
965
+ "money": "decimal",
966
+ # booleans
967
+ "boolean": "boolean",
968
+ "bool": "boolean",
969
+ "bit": "boolean",
970
+ # date / datetime
971
+ "date": "date",
972
+ "datetime": "datetime",
973
+ "datetime2": "datetime",
974
+ "timestamp": "datetime",
975
+ "timestamptz": "datetime",
976
+ "timestamp_ntz": "datetime",
977
+ "timestamp_ltz": "datetime",
978
+ "timestamp_tz": "datetime",
979
+ # json
980
+ "json": "json",
981
+ "jsonb": "json",
982
+ "variant": "json",
983
+ "object": "json",
984
+ "array": "json",
985
+ }
986
+
987
+
988
+ def _normalize_field_type(basetype: str) -> str:
989
+ """Coerce a warehouse-native type into the metastore's closed vocabulary."""
990
+ if not basetype:
991
+ return "string"
992
+ # strip parens (e.g. VARCHAR(255), DECIMAL(38, 9))
993
+ head = basetype.split("(", 1)[0].strip().lower()
994
+ return _FIELD_TYPE_MAP.get(head, "string")
995
+
996
+
997
+ def heuristic_generate_model(
998
+ *,
999
+ schemas: dict[str, dict[str, Any]],
1000
+ model_name: str,
1001
+ derive_fqn: Callable[[str], str],
1002
+ classify_role: Callable[[str, str], str],
1003
+ ) -> dict[str, Any]:
1004
+ """Deterministic stand-in for the AI generator (see ``build_model``).
1005
+
1006
+ Builds: one dataset per table (with classified fields[]), one
1007
+ COUNT(*) metric per dataset as a placeholder, no relationships
1008
+ (cross-table FKs are not inferrable from columns alone), an empty
1009
+ constraints list, and a glossary entry per dataset.
1010
+
1011
+ Accepts the FQN-derivation and role-classification helpers as
1012
+ callables so the helper module stays free of import-time coupling
1013
+ to the main service module's regex constants.
1014
+ """
1015
+ datasets: list[dict[str, Any]] = []
1016
+ metrics: list[dict[str, Any]] = []
1017
+ glossary: list[dict[str, Any]] = []
1018
+
1019
+ for tid, detail in schemas.items():
1020
+ ds_name = (
1021
+ (detail.get("display_name") or detail.get("name") or tid.split(".")[-1])
1022
+ .replace(" ", "_")
1023
+ .lower()
1024
+ )
1025
+ fields: list[dict[str, Any]] = []
1026
+ for col in detail.get("column_details", []) or []:
1027
+ cname = col.get("name", "")
1028
+ basetype = col.get("type", "") or col.get("native_type", "")
1029
+ # Metastore validates field types against a closed lowercase set:
1030
+ # {string, integer, decimal, boolean, date, datetime, json}
1031
+ # Storage returns warehouse-native types (VARCHAR, INTEGER, ...)
1032
+ # or empty strings for untyped legacy tables. Map both to the
1033
+ # metastore vocabulary; fall back to "string" for unknown /
1034
+ # empty types so the builder doesn't 422 on legacy buckets.
1035
+ fields.append(
1036
+ {
1037
+ "name": cname,
1038
+ "type": _normalize_field_type(basetype),
1039
+ "role": classify_role(cname, basetype),
1040
+ }
1041
+ )
1042
+ datasets.append(
1043
+ {
1044
+ "name": ds_name,
1045
+ "tableId": tid,
1046
+ "fqn": derive_fqn(tid),
1047
+ "fields": fields,
1048
+ "description": detail.get("description", "") or "",
1049
+ }
1050
+ )
1051
+ metrics.append(
1052
+ {
1053
+ "name": f"{ds_name}_row_count",
1054
+ "sql": f"COUNT(*) FROM {derive_fqn(tid)}",
1055
+ "dataset": tid,
1056
+ "description": f"Row count of {ds_name}.",
1057
+ }
1058
+ )
1059
+ glossary.append(
1060
+ {
1061
+ "term": ds_name,
1062
+ "definition": f"Table {tid}: {detail.get('description', '') or 'auto-generated'}.",
1063
+ }
1064
+ )
1065
+
1066
+ return {
1067
+ "name": model_name,
1068
+ "description": (
1069
+ f"Heuristic-generated model from {len(schemas)} table(s). "
1070
+ "Iterate with `kbagent semantic-layer add/edit`."
1071
+ ),
1072
+ "sql_dialect": "Snowflake",
1073
+ "datasets": datasets,
1074
+ "metrics": metrics,
1075
+ "relationships": [],
1076
+ "constraints": [],
1077
+ "glossary": glossary,
1078
+ }