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,655 @@
1
+ """Extract and merge embedded code from Keboola configurations.
2
+
3
+ On pull: extracts SQL/Python code from config parameters into separate files.
4
+ On push: reads code files back into config parameters.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Any, cast
11
+
12
+ from keboola_agent_cli.sync.sql_split import split_statements
13
+
14
+
15
+ def _strip_trailing_empty(lines: list[str]) -> list[str]:
16
+ """Remove trailing empty lines but preserve leading whitespace."""
17
+ result = list(lines)
18
+ while result and result[-1].strip() == "":
19
+ result.pop()
20
+ return result
21
+
22
+
23
+ def _lines_to_script(lines: list[str], *, is_sql: bool = False) -> list[str]:
24
+ """Convert collected lines back into the ``script[]`` array.
25
+
26
+ For SQL transformations: splits on semicolons using a state machine,
27
+ producing one element per statement (matching Keboola runtime semantics).
28
+ For Python/other: joins all lines into a single element.
29
+ """
30
+ stripped = _strip_trailing_empty(lines)
31
+ if not stripped:
32
+ return []
33
+ content = "\n".join(stripped)
34
+ if is_sql:
35
+ return split_statements(content)
36
+ return [content]
37
+
38
+
39
+ # Component patterns that contain SQL transformations.
40
+ # Used for exact-match dispatch in pull/push code-file extraction.
41
+ # For runtime-shape detection (i.e. "does this component's script[]
42
+ # need to be split on statement boundaries?") prefer
43
+ # :func:`is_sql_transformation_component` -- it adds fragment-based
44
+ # fallback so newer/variant SQL backends are covered without code edits.
45
+ SQL_TRANSFORMATION_COMPONENTS: set[str] = {
46
+ "keboola.snowflake-transformation",
47
+ "keboola.synapse-transformation",
48
+ "keboola.oracle-transformation",
49
+ "keboola.redshift-sql-transformation",
50
+ "keboola.google-bigquery-transformation",
51
+ "keboola.duckdb-transformation",
52
+ }
53
+
54
+ # Component patterns that contain Python transformations
55
+ PYTHON_TRANSFORMATION_COMPONENTS: set[str] = {
56
+ "keboola.python-transformation-v2",
57
+ }
58
+
59
+ # Components with embedded Python code (custom apps)
60
+ PYTHON_APP_COMPONENTS: set[str] = {
61
+ "kds-team.app-custom-python",
62
+ }
63
+
64
+ # Fragment-based detection for SQL transformations. Keeps
65
+ # is_sql_transformation_component robust against newer/variant
66
+ # component IDs (e.g. ``keboola.snowflake-transformation-v2``,
67
+ # self-hosted ``keboola.exasol-transformation``) without requiring
68
+ # an edit to SQL_TRANSFORMATION_COMPONENTS for every new backend.
69
+ _SQL_TRANSFORMATION_FRAGMENTS: tuple[str, ...] = (
70
+ "snowflake-transformation",
71
+ "synapse-transformation",
72
+ "oracle-transformation",
73
+ "redshift-sql-transformation",
74
+ "redshift-transformation",
75
+ "google-bigquery-transformation",
76
+ "bigquery-transformation",
77
+ "duckdb-transformation",
78
+ "exasol-transformation",
79
+ "teradata-transformation",
80
+ )
81
+
82
+ SQL_BLOCK_MARKER = "/* ===== BLOCK: {name} ===== */"
83
+ SQL_CODE_MARKER = "/* ===== CODE: {name} ===== */"
84
+ PYTHON_BLOCK_MARKER = "# ===== BLOCK: {name} ====="
85
+ PYTHON_CODE_MARKER = "# ===== CODE: {name} ====="
86
+ DESCRIPTION_FILENAME = "_description.md"
87
+
88
+
89
+ def is_sql_transformation_component(component_id: str) -> bool:
90
+ """Return True if the component's script[] elements are SQL statements.
91
+
92
+ The Keboola runtime treats each ``parameters.blocks[].codes[].script``
93
+ element of a SQL transformation as ONE logical statement. Pushing a
94
+ string instead of an array passes the Storage API (lax validator) but
95
+ crashes at job runtime ("Expected array, got string").
96
+
97
+ Combines exact-match (``SQL_TRANSFORMATION_COMPONENTS``) with a
98
+ fragment fallback so newer/variant SQL backends -- including ones
99
+ not yet enumerated in the exact set -- still get correct treatment.
100
+ """
101
+ if component_id in SQL_TRANSFORMATION_COMPONENTS:
102
+ return True
103
+ return any(fragment in component_id for fragment in _SQL_TRANSFORMATION_FRAGMENTS)
104
+
105
+
106
+ def normalize_blocks_codes_script(
107
+ component_id: str,
108
+ config: dict[str, Any],
109
+ ) -> tuple[dict[str, Any], list[dict[str, Any]]]:
110
+ """Normalize ``parameters.blocks[].codes[].script`` for runtime safety.
111
+
112
+ Closes two gaps between the Storage API (lax shape validator) and the
113
+ runtime (strict per-element semantics):
114
+
115
+ 1. **String -> array** (since 0.28.0). The Keboola runtime schema
116
+ validator requires ``script`` to be an array. The Storage API
117
+ silently accepts a string and the runtime crashes at job
118
+ execution with::
119
+
120
+ Invalid type for path "root.parameters.blocks.0.codes.X.script".
121
+ Expected "array", but got "string"
122
+
123
+ For SQL transformations the string is split on statement
124
+ boundaries via :func:`split_statements` (state machine respecting
125
+ comments and string literals); for Python / R / custom-Python
126
+ apps and other components with the same schema, the string is
127
+ wrapped as a single-element array.
128
+
129
+ 2. **Per-element re-split for SQL** (since 0.30.8, closes issue
130
+ #274). When ``script`` is already a list but an element packs
131
+ multiple ``;``-separated statements, the Snowflake/BigQuery/etc.
132
+ runtime crashes at ``odbc_prepare`` with ``Actual statement
133
+ count N did not match the desired statement count 1`` (SQL
134
+ state ``0A000``). Storage API accepts this shape silently
135
+ because it only checks "list of strings". This pass re-splits
136
+ each SQL-transformation element through :func:`split_statements`
137
+ and replaces in place when the element produces more than one
138
+ statement.
139
+
140
+ Components that don't match either case are passed through
141
+ unchanged.
142
+
143
+ Args:
144
+ component_id: The configuration's component ID. Used to choose
145
+ split-vs-wrap-vs-resplit and to passthrough components that
146
+ don't have this schema at all.
147
+ config: The configuration dict to normalize. Mutated in place.
148
+
149
+ Returns:
150
+ ``(config, normalizations)``. ``normalizations`` is a list of
151
+ per-element change records of shape::
152
+
153
+ # case 1 (string -> array):
154
+ {
155
+ "path": "parameters.blocks[0].codes[1].script",
156
+ "action": "sql_split" | "wrap_array",
157
+ "before_type": "str",
158
+ "after_type": "list",
159
+ "after_length": 3,
160
+ }
161
+ # case 2 (list element -> multiple list elements):
162
+ {
163
+ "path": "parameters.blocks[0].codes[1].script[2]",
164
+ "action": "sql_resplit",
165
+ "before_type": "str",
166
+ "after_type": "list",
167
+ "before_length": 1,
168
+ "after_length": 2,
169
+ }
170
+
171
+ Empty when nothing was normalized (already-valid input).
172
+
173
+ The caller is responsible for surfacing the normalization records to
174
+ the user (stderr in human mode, JSON envelope in JSON mode) so the
175
+ silent fix is observable.
176
+ """
177
+ normalizations: list[dict[str, Any]] = []
178
+ if not isinstance(config, dict):
179
+ return config, normalizations
180
+ parameters = config.get("parameters")
181
+ if not isinstance(parameters, dict):
182
+ return config, normalizations
183
+ blocks = parameters.get("blocks")
184
+ if not isinstance(blocks, list):
185
+ return config, normalizations
186
+
187
+ is_sql = is_sql_transformation_component(component_id)
188
+
189
+ for block_idx, block in enumerate(blocks):
190
+ if not isinstance(block, dict):
191
+ continue
192
+ block_d = cast("dict[str, Any]", block)
193
+ codes = block_d.get("codes")
194
+ if not isinstance(codes, list):
195
+ continue
196
+ for code_idx, code in enumerate(codes):
197
+ if not isinstance(code, dict):
198
+ continue
199
+ code_d = cast("dict[str, Any]", code)
200
+ script = code_d.get("script")
201
+ if isinstance(script, str):
202
+ if is_sql:
203
+ new_script = split_statements(script)
204
+ action = "sql_split"
205
+ else:
206
+ # Single-element wrap for Python / R / custom-Python
207
+ # apps and any unknown component sharing the schema.
208
+ # Empty strings collapse to ``[]`` -- the runtime
209
+ # treats both as no-op.
210
+ new_script = [script] if script.strip() else []
211
+ action = "wrap_array"
212
+ code_d["script"] = new_script
213
+ normalizations.append(
214
+ {
215
+ "path": f"parameters.blocks[{block_idx}].codes[{code_idx}].script",
216
+ "action": action,
217
+ "before_type": "str",
218
+ "after_type": "list",
219
+ "after_length": len(new_script),
220
+ }
221
+ )
222
+ elif is_sql and isinstance(script, list):
223
+ rebuilt: list[Any] = []
224
+ resplits: list[tuple[int, int]] = []
225
+ for el_idx, element in enumerate(script):
226
+ if isinstance(element, str):
227
+ parts = split_statements(element)
228
+ if len(parts) > 1:
229
+ rebuilt.extend(parts)
230
+ resplits.append((el_idx, len(parts)))
231
+ continue
232
+ rebuilt.append(element)
233
+ if resplits:
234
+ code_d["script"] = rebuilt
235
+ for el_idx, after_count in resplits:
236
+ normalizations.append(
237
+ {
238
+ "path": (
239
+ f"parameters.blocks[{block_idx}]"
240
+ f".codes[{code_idx}].script[{el_idx}]"
241
+ ),
242
+ "action": "sql_resplit",
243
+ "before_type": "str",
244
+ "after_type": "list",
245
+ "before_length": 1,
246
+ "after_length": after_count,
247
+ }
248
+ )
249
+
250
+ return config, normalizations
251
+
252
+
253
+ def _extract_description(config_data: dict[str, Any], config_dir: Path) -> None:
254
+ """Extract description field into _description.md."""
255
+ description = config_data.get("description", "")
256
+ if not description:
257
+ return
258
+ config_dir.mkdir(parents=True, exist_ok=True)
259
+ (config_dir / DESCRIPTION_FILENAME).write_text(description, encoding="utf-8")
260
+ del config_data["description"]
261
+
262
+
263
+ def _merge_description(config_data: dict[str, Any], config_dir: Path) -> None:
264
+ """Read _description.md back into config_data."""
265
+ desc_file = config_dir / DESCRIPTION_FILENAME
266
+ if desc_file.exists():
267
+ config_data["description"] = desc_file.read_text(encoding="utf-8")
268
+
269
+
270
+ def extract_code_files(
271
+ component_id: str,
272
+ config_data: dict[str, Any],
273
+ config_dir: Path,
274
+ ) -> dict[str, Any]:
275
+ """Extract embedded code from config into separate files.
276
+
277
+ Modifies config_data in place to remove extracted code.
278
+ Writes code files to config_dir.
279
+ Returns the modified config_data.
280
+ """
281
+ _extract_description(config_data, config_dir)
282
+ if component_id in SQL_TRANSFORMATION_COMPONENTS:
283
+ return _extract_sql_transformation(config_data, config_dir)
284
+ if component_id in PYTHON_TRANSFORMATION_COMPONENTS:
285
+ return _extract_python_transformation(config_data, config_dir)
286
+ if component_id in PYTHON_APP_COMPONENTS:
287
+ return _extract_python_app(config_data, config_dir)
288
+ return config_data
289
+
290
+
291
+ def merge_code_files(
292
+ component_id: str,
293
+ config_data: dict[str, Any],
294
+ config_dir: Path,
295
+ ) -> dict[str, Any]:
296
+ """Read code files and merge them back into config_data.
297
+
298
+ Reverse of extract_code_files. Called before push.
299
+ Returns the modified config_data.
300
+ """
301
+ _merge_description(config_data, config_dir)
302
+ if component_id in SQL_TRANSFORMATION_COMPONENTS:
303
+ return _merge_sql_transformation(config_data, config_dir)
304
+ if component_id in PYTHON_TRANSFORMATION_COMPONENTS:
305
+ return _merge_python_transformation(config_data, config_dir)
306
+ if component_id in PYTHON_APP_COMPONENTS:
307
+ return _merge_python_app(config_data, config_dir)
308
+ return config_data
309
+
310
+
311
+ # ---- SQL Transformations ----
312
+
313
+
314
+ def _extract_sql_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
315
+ """Extract SQL blocks from parameters.blocks into transform.sql."""
316
+ parameters = config_data.get("parameters", {})
317
+ blocks = parameters.get("blocks", [])
318
+
319
+ if not blocks:
320
+ return config_data
321
+
322
+ lines: list[str] = []
323
+ for block in blocks:
324
+ block_name = block.get("name", "unnamed")
325
+ lines.append(SQL_BLOCK_MARKER.format(name=block_name))
326
+ lines.append("")
327
+
328
+ for code in block.get("codes", []):
329
+ code_name = code.get("name", "unnamed")
330
+ lines.append(SQL_CODE_MARKER.format(name=code_name))
331
+
332
+ scripts = code.get("script") or []
333
+ for si, script in enumerate(scripts):
334
+ if si > 0:
335
+ lines.append("") # blank line between statements
336
+ if isinstance(script, str) and "\n" in script:
337
+ lines.extend(script.split("\n"))
338
+ else:
339
+ lines.append(script)
340
+ lines.append("")
341
+
342
+ sql_content = "\n".join(lines).rstrip() + "\n"
343
+
344
+ config_dir.mkdir(parents=True, exist_ok=True)
345
+ (config_dir / "transform.sql").write_text(sql_content, encoding="utf-8")
346
+
347
+ # Remove blocks from parameters (they're now in the SQL file)
348
+ parameters.pop("blocks", None)
349
+
350
+ return config_data
351
+
352
+
353
+ def _merge_sql_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
354
+ """Read transform.sql and parse block markers back into parameters.blocks."""
355
+ sql_file = config_dir / "transform.sql"
356
+ if not sql_file.exists():
357
+ return config_data
358
+
359
+ content = sql_file.read_text(encoding="utf-8")
360
+ blocks = _parse_sql_blocks(content)
361
+
362
+ parameters = config_data.setdefault("parameters", {})
363
+ parameters["blocks"] = blocks
364
+
365
+ return config_data
366
+
367
+
368
+ def _parse_sql_blocks(content: str) -> list[dict[str, Any]]:
369
+ """Parse SQL content with block/code markers into blocks structure."""
370
+ blocks: list[dict[str, Any]] = []
371
+ current_block: dict[str, Any] | None = None
372
+ current_code: dict[str, Any] | None = None
373
+ current_script_lines: list[str] = []
374
+ orphan_lines: list[str] = []
375
+
376
+ for line in content.split("\n"):
377
+ stripped = line.strip()
378
+
379
+ # Check for block marker
380
+ if stripped.startswith("/* ===== BLOCK:") and stripped.endswith("===== */"):
381
+ # Save previous code if any
382
+ if current_code is not None and current_block is not None:
383
+ current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
384
+ current_block.setdefault("codes", []).append(current_code)
385
+ current_code = None
386
+ current_script_lines = []
387
+
388
+ block_name = stripped[len("/* ===== BLOCK:") :].rstrip(" =*/").strip()
389
+ current_block = {"name": block_name, "codes": []}
390
+ blocks.append(current_block)
391
+ orphan_lines = []
392
+ continue
393
+
394
+ # Check for code marker
395
+ if stripped.startswith("/* ===== CODE:") and stripped.endswith("===== */"):
396
+ # Save previous code if any
397
+ if current_code is not None and current_block is not None:
398
+ current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
399
+ current_block.setdefault("codes", []).append(current_code)
400
+ current_script_lines = []
401
+
402
+ code_name = stripped[len("/* ===== CODE:") :].rstrip(" =*/").strip()
403
+ current_code = {"name": code_name}
404
+ # Prepend any orphan lines collected between BLOCK and this CODE marker
405
+ if orphan_lines:
406
+ current_script_lines = list(orphan_lines)
407
+ orphan_lines = []
408
+ continue
409
+
410
+ # Regular line
411
+ if current_code is not None:
412
+ current_script_lines.append(line)
413
+ elif current_block is not None and stripped:
414
+ # Lines between BLOCK marker and first CODE marker — preserve them
415
+ # so they're not silently discarded on roundtrip.
416
+ orphan_lines.append(line)
417
+
418
+ # Don't forget the last code block
419
+ if current_code is not None and current_block is not None:
420
+ current_code["script"] = _lines_to_script(current_script_lines, is_sql=True)
421
+ current_block.setdefault("codes", []).append(current_code)
422
+
423
+ # If no markers found, treat entire content as single block/code
424
+ if not blocks and content.strip():
425
+ blocks = [
426
+ {
427
+ "name": "Block 1",
428
+ "codes": [
429
+ {"name": "Code 1", "script": _lines_to_script(content.split("\n"), is_sql=True)}
430
+ ],
431
+ }
432
+ ]
433
+
434
+ return blocks
435
+
436
+
437
+ # ---- Python Transformations ----
438
+
439
+
440
+ def _extract_python_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
441
+ """Extract Python blocks from parameters.blocks into transform.py, packages into pyproject.toml."""
442
+ parameters = config_data.get("parameters", {})
443
+ blocks = parameters.get("blocks", [])
444
+
445
+ if blocks:
446
+ lines: list[str] = []
447
+ for block in blocks:
448
+ block_name = block.get("name", "unnamed")
449
+ lines.append(PYTHON_BLOCK_MARKER.format(name=block_name))
450
+ lines.append("")
451
+
452
+ for code in block.get("codes", []):
453
+ code_name = code.get("name", "unnamed")
454
+ lines.append(PYTHON_CODE_MARKER.format(name=code_name))
455
+
456
+ scripts = code.get("script") or []
457
+ for si, script in enumerate(scripts):
458
+ if si > 0:
459
+ lines.append("") # blank line between script elements
460
+ if isinstance(script, str) and "\n" in script:
461
+ lines.extend(script.split("\n"))
462
+ else:
463
+ lines.append(script)
464
+ lines.append("")
465
+
466
+ py_content = "\n".join(lines).rstrip() + "\n"
467
+ config_dir.mkdir(parents=True, exist_ok=True)
468
+ (config_dir / "transform.py").write_text(py_content, encoding="utf-8")
469
+
470
+ # Remove blocks from parameters
471
+ parameters.pop("blocks", None)
472
+
473
+ # Extract packages to pyproject.toml
474
+ packages = parameters.get("packages", [])
475
+ if packages:
476
+ config_name = config_data.get("name", "transformation")
477
+ _write_pyproject_toml(config_dir, config_name, packages, component_id=None, config_id=None)
478
+ parameters.pop("packages", None)
479
+
480
+ return config_data
481
+
482
+
483
+ def _merge_python_transformation(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
484
+ """Read transform.py and pyproject.toml back into config_data."""
485
+ py_file = config_dir / "transform.py"
486
+ if py_file.exists():
487
+ content = py_file.read_text(encoding="utf-8")
488
+ blocks = _parse_python_blocks(content)
489
+ parameters = config_data.setdefault("parameters", {})
490
+ parameters["blocks"] = blocks
491
+
492
+ # Read packages from pyproject.toml
493
+ packages = _read_pyproject_packages(config_dir)
494
+ if packages:
495
+ parameters = config_data.setdefault("parameters", {})
496
+ parameters["packages"] = packages
497
+
498
+ return config_data
499
+
500
+
501
+ def _parse_python_blocks(content: str) -> list[dict[str, Any]]:
502
+ """Parse Python content with block/code markers into blocks structure."""
503
+ blocks: list[dict[str, Any]] = []
504
+ current_block: dict[str, Any] | None = None
505
+ current_code: dict[str, Any] | None = None
506
+ current_script_lines: list[str] = []
507
+
508
+ for line in content.split("\n"):
509
+ stripped = line.strip()
510
+
511
+ if stripped.startswith("# ===== BLOCK:") and stripped.endswith("====="):
512
+ if current_code is not None and current_block is not None:
513
+ current_code["script"] = _lines_to_script(current_script_lines)
514
+ current_block.setdefault("codes", []).append(current_code)
515
+ current_code = None
516
+ current_script_lines = []
517
+
518
+ block_name = stripped[len("# ===== BLOCK:") :].rstrip(" =").strip()
519
+ current_block = {"name": block_name, "codes": []}
520
+ blocks.append(current_block)
521
+ continue
522
+
523
+ if stripped.startswith("# ===== CODE:") and stripped.endswith("====="):
524
+ if current_code is not None and current_block is not None:
525
+ current_code["script"] = _lines_to_script(current_script_lines)
526
+ current_block.setdefault("codes", []).append(current_code)
527
+ current_script_lines = []
528
+
529
+ code_name = stripped[len("# ===== CODE:") :].rstrip(" =").strip()
530
+ current_code = {"name": code_name}
531
+ continue
532
+
533
+ if current_code is not None:
534
+ current_script_lines.append(line)
535
+
536
+ if current_code is not None and current_block is not None:
537
+ current_code["script"] = _lines_to_script(current_script_lines)
538
+ current_block.setdefault("codes", []).append(current_code)
539
+
540
+ if not blocks and content.strip():
541
+ blocks = [
542
+ {
543
+ "name": "Block 1",
544
+ "codes": [{"name": "Code 1", "script": _lines_to_script(content.split("\n"))}],
545
+ }
546
+ ]
547
+
548
+ return blocks
549
+
550
+
551
+ # ---- Python Apps ----
552
+
553
+
554
+ def _extract_python_app(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
555
+ """Extract parameters.code into code.py and packages into pyproject.toml."""
556
+ parameters = config_data.get("parameters", {})
557
+
558
+ code = parameters.get("code")
559
+ if code and isinstance(code, str):
560
+ config_dir.mkdir(parents=True, exist_ok=True)
561
+ (config_dir / "code.py").write_text(code, encoding="utf-8")
562
+ parameters.pop("code", None)
563
+
564
+ packages = parameters.get("packages", [])
565
+ if packages:
566
+ keboola_meta = config_data.get("_keboola", {})
567
+ _write_pyproject_toml(
568
+ config_dir,
569
+ config_data.get("name", "app"),
570
+ packages,
571
+ component_id=keboola_meta.get("component_id"),
572
+ config_id=keboola_meta.get("config_id"),
573
+ )
574
+ parameters.pop("packages", None)
575
+
576
+ return config_data
577
+
578
+
579
+ def _merge_python_app(config_data: dict[str, Any], config_dir: Path) -> dict[str, Any]:
580
+ """Read code.py and pyproject.toml back into config_data."""
581
+ code_file = config_dir / "code.py"
582
+ if code_file.exists():
583
+ parameters = config_data.setdefault("parameters", {})
584
+ parameters["code"] = code_file.read_text(encoding="utf-8")
585
+
586
+ packages = _read_pyproject_packages(config_dir)
587
+ if packages:
588
+ parameters = config_data.setdefault("parameters", {})
589
+ parameters["packages"] = packages
590
+
591
+ return config_data
592
+
593
+
594
+ # ---- pyproject.toml helpers ----
595
+
596
+
597
+ def _write_pyproject_toml(
598
+ config_dir: Path,
599
+ name: str,
600
+ packages: list[str],
601
+ component_id: str | None = None,
602
+ config_id: str | None = None,
603
+ ) -> None:
604
+ """Write packages to a pyproject.toml file."""
605
+ config_dir.mkdir(parents=True, exist_ok=True)
606
+
607
+ # Sanitize name for pyproject
608
+ safe_name = name.lower().replace(" ", "-").replace("_", "-")
609
+
610
+ lines = [
611
+ "[project]",
612
+ f'name = "{safe_name}"',
613
+ 'requires-python = ">=3.11"',
614
+ "dependencies = [",
615
+ ]
616
+ for pkg in packages:
617
+ lines.append(f' "{pkg}",')
618
+ lines.append("]")
619
+
620
+ if component_id or config_id:
621
+ lines.append("")
622
+ lines.append("[tool.keboola]")
623
+ if component_id:
624
+ lines.append(f'component_id = "{component_id}"')
625
+ if config_id:
626
+ lines.append(f'config_id = "{config_id}"')
627
+
628
+ lines.append("") # trailing newline
629
+ (config_dir / "pyproject.toml").write_text("\n".join(lines), encoding="utf-8")
630
+
631
+
632
+ def _read_pyproject_packages(config_dir: Path) -> list[str]:
633
+ """Read packages from pyproject.toml dependencies."""
634
+ toml_file = config_dir / "pyproject.toml"
635
+ if not toml_file.exists():
636
+ return []
637
+
638
+ content = toml_file.read_text(encoding="utf-8")
639
+ packages: list[str] = []
640
+ in_deps = False
641
+
642
+ for line in content.split("\n"):
643
+ stripped = line.strip()
644
+ if stripped == "dependencies = [":
645
+ in_deps = True
646
+ continue
647
+ if in_deps:
648
+ if stripped == "]":
649
+ break
650
+ # Strip quotes and trailing comma
651
+ pkg = stripped.strip('", ')
652
+ if pkg:
653
+ packages.append(pkg)
654
+
655
+ return packages