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,767 @@
1
+ """Pre-flight validation for Keboola data-app git repositories.
2
+
3
+ Walks a GitHub repo via the public Contents + Trees API and verifies the
4
+ "Golden Rule" repository structure documented at
5
+ https://help.keboola.com/data-apps/python-js/. Each check emits one of
6
+ ``BLOCKING`` / ``WARN`` / ``OK`` with a citation to the canon page that
7
+ defines the rule.
8
+
9
+ Rate-limit-aware fetch strategy:
10
+
11
+ 1. ONE ``GET /repos/{owner}/{repo}/git/trees/{ref}?recursive=1`` to
12
+ resolve every existence check against the same response.
13
+ 2. UP TO 4 ``GET /repos/{owner}/{repo}/contents/{path}`` for files whose
14
+ contents the rules need to inspect: setup.sh, pyproject.toml, plus
15
+ nginx/default.conf and supervisord/app.conf (both fetched when both
16
+ exist so the port-match check can compare them).
17
+
18
+ A typical run spends 1-5 GitHub API calls (1 tree + 0-4 contents) regardless of repo size; the
19
+ 60/hour unauth limit is no longer the common-case failure mode it
20
+ otherwise would be. Pass ``--git-pat-env`` (resolved to a plaintext PAT)
21
+ to raise the limit to 5,000/hour.
22
+
23
+ Scope of this PR: ``--type python-js`` only. Streamlit / pure-Python /
24
+ R repo layouts differ and need their own per-type canon citations -- a
25
+ follow-up PR adds them.
26
+
27
+ TODO: extract ``GitHubContentsClient`` (defined below) into a top-level
28
+ ``src/keboola_agent_cli/github_client.py`` module that inherits from
29
+ ``BaseHttpClient`` so this stays consistent with the 3-layer architecture
30
+ (LAYER 3 = clients, LAYER 2 = services). The ``github_client_factory``
31
+ dependency-injection pattern in ``RepoValidateService`` already isolates
32
+ the client for testing; the extraction is a pure refactor with no
33
+ behaviour change.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import base64
39
+ import logging
40
+ import re
41
+ from dataclasses import dataclass, field
42
+ from typing import Any
43
+ from urllib.parse import quote, urlparse
44
+
45
+ import httpx
46
+
47
+ from ..constants import DEFAULT_TIMEOUT
48
+ from ..errors import ConfigError, ErrorCode, KeboolaApiError
49
+ from .base import BaseService
50
+
51
+ logger = logging.getLogger(__name__)
52
+
53
+
54
+ # Canonical citations for every rule the validator emits. Each enumerated
55
+ # rule includes the help-doc anchor users can click to read why we flag
56
+ # the issue. Kept here so commands and tests can reference them by name.
57
+ HELP_PYTHON_JS = "https://help.keboola.com/data-apps/python-js/"
58
+ HELP_BACKEND_VERSIONS = "https://help.keboola.com/components/data-apps/backend-versions/"
59
+ HELP_STORAGE_ACCESS = "https://help.keboola.com/data-apps/storage-access/"
60
+
61
+
62
+ # Files we may need to fetch *contents* for (not just existence).
63
+ _NGINX_CONF = "keboola-config/nginx/sites/default.conf"
64
+ _APP_CONF = "keboola-config/supervisord/services/app.conf"
65
+ _SETUP_SH = "keboola-config/setup.sh"
66
+ _PYPROJECT = "pyproject.toml"
67
+
68
+ # Heuristic regexes -- tightening any of these is preferred over a real
69
+ # parser because every Python framework names POST handlers differently
70
+ # (Flask blueprints, FastAPI decorators, dynamic registration, etc.) and
71
+ # false positives are operationally cheap (a WARN, not a BLOCKING).
72
+ _PIP_INSTALL_RE = re.compile(r"\bpip\s+install\b")
73
+ _UV_SYNC_RE = re.compile(r"\buv\s+sync\b")
74
+ _REQUIRES_PYTHON_RE = re.compile(r'^\s*requires-python\s*=\s*["\']([^"\']+)["\']\s*$', re.MULTILINE)
75
+ _NGINX_PROXY_PASS_PORT_RE = re.compile(r"proxy_pass\s+http://[^:]+:(\d+)")
76
+ _APP_CONF_PORT_RE = re.compile(r"--port[=\s]+(\d+)|:(\d+)\b")
77
+
78
+
79
+ # Severities. Order matters for verdict aggregation (worst wins).
80
+ SEVERITY_OK = "OK"
81
+ SEVERITY_WARN = "WARN"
82
+ SEVERITY_BLOCKING = "BLOCKING"
83
+
84
+
85
+ @dataclass
86
+ class CheckResult:
87
+ """One validate-repo check outcome."""
88
+
89
+ name: str
90
+ severity: str
91
+ citation: str
92
+ message: str = ""
93
+ details: dict[str, Any] = field(default_factory=dict)
94
+
95
+ def to_dict(self) -> dict[str, Any]:
96
+ out: dict[str, Any] = {
97
+ "name": self.name,
98
+ "severity": self.severity,
99
+ "citation": self.citation,
100
+ }
101
+ if self.message:
102
+ out["message"] = self.message
103
+ if self.details:
104
+ out["details"] = self.details
105
+ return out
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # GitHub client (intentionally minimal -- no fancy retry, just the calls
110
+ # validate-repo needs)
111
+ # ---------------------------------------------------------------------------
112
+
113
+
114
+ class GitHubContentsClient:
115
+ """Read-only GitHub Contents + Trees client.
116
+
117
+ Only used by :class:`RepoValidateService`. Refuses non-GitHub hosts
118
+ (this PR ships GitHub support; GitLab / Bitbucket / etc. are tracked
119
+ as a follow-up). PAT is sent only when present and never logged.
120
+ """
121
+
122
+ BASE_URL = "https://api.github.com"
123
+
124
+ def __init__(self, token: str | None = None, timeout: Any = DEFAULT_TIMEOUT) -> None:
125
+ headers: dict[str, str] = {
126
+ "Accept": "application/vnd.github+json",
127
+ "X-GitHub-Api-Version": "2022-11-28",
128
+ }
129
+ if token:
130
+ headers["Authorization"] = f"token {token}"
131
+ self._client = httpx.Client(
132
+ base_url=self.BASE_URL,
133
+ headers=headers,
134
+ timeout=timeout,
135
+ follow_redirects=True,
136
+ )
137
+
138
+ def __enter__(self) -> GitHubContentsClient:
139
+ return self
140
+
141
+ def __exit__(self, *args: Any) -> None:
142
+ self.close()
143
+
144
+ def close(self) -> None:
145
+ self._client.close()
146
+
147
+ def get_tree_recursive(self, owner: str, repo: str, ref: str) -> dict[str, Any]:
148
+ """``GET /repos/{owner}/{repo}/git/trees/{ref}?recursive=1``.
149
+
150
+ Single call returns every path + blob SHA in the repo at ``ref``.
151
+ Tree may be ``truncated`` for very large repos (~100k entries);
152
+ callers fall back to per-file content fetches in that case.
153
+
154
+ Slash-bearing refs (``feature/foo``) are URL-encoded so GitHub
155
+ does not mis-route the request to a 404.
156
+ """
157
+ encoded_ref = quote(ref, safe="")
158
+ path = f"/repos/{owner}/{repo}/git/trees/{encoded_ref}?recursive=1"
159
+ return self._json_get(path, action=f"GET tree for {owner}/{repo}@{ref}")
160
+
161
+ def get_file_content(self, owner: str, repo: str, path: str, ref: str) -> str | None:
162
+ """Return decoded text content of one file, or ``None`` if absent.
163
+
164
+ Raises ``KeboolaApiError`` on auth / rate-limit / 5xx. ``ref`` is
165
+ sent as a query param (httpx URL-encodes), so slash-bearing branches
166
+ like ``feature/foo`` round-trip correctly.
167
+ """
168
+ url = f"/repos/{owner}/{repo}/contents/{path}"
169
+ params = {"ref": ref}
170
+ try:
171
+ response = self._client.get(url, params=params)
172
+ except httpx.HTTPError as exc:
173
+ raise KeboolaApiError(
174
+ message=f"GitHub fetch failed for {owner}/{repo}/{path}: {exc}",
175
+ status_code=0,
176
+ error_code=ErrorCode.CONNECTION_ERROR,
177
+ retryable=True,
178
+ ) from exc
179
+ if response.status_code == 404:
180
+ return None
181
+ self._raise_for_status(response, action=f"GET contents {owner}/{repo}/{path}")
182
+ body = response.json()
183
+ if not isinstance(body, dict):
184
+ return None
185
+ encoding = body.get("encoding", "")
186
+ content = body.get("content", "")
187
+ if encoding == "base64" and isinstance(content, str):
188
+ try:
189
+ return base64.b64decode(content).decode("utf-8", errors="replace")
190
+ except (ValueError, TypeError) as exc:
191
+ logger.warning("Failed to decode %s/%s/%s: %s", owner, repo, path, exc)
192
+ return ""
193
+ if isinstance(content, str):
194
+ return content
195
+ return ""
196
+
197
+ def _json_get(self, path: str, *, action: str) -> dict[str, Any]:
198
+ try:
199
+ response = self._client.get(path)
200
+ except httpx.HTTPError as exc:
201
+ raise KeboolaApiError(
202
+ message=f"GitHub call failed ({action}): {exc}",
203
+ status_code=0,
204
+ error_code=ErrorCode.CONNECTION_ERROR,
205
+ retryable=True,
206
+ ) from exc
207
+ self._raise_for_status(response, action=action)
208
+ body = response.json()
209
+ return body if isinstance(body, dict) else {}
210
+
211
+ @staticmethod
212
+ def _raise_for_status(response: httpx.Response, *, action: str) -> None:
213
+ if 200 <= response.status_code < 300:
214
+ return
215
+ # Surface rate limit explicitly so the operator can act on the
216
+ # actionable hint (use a PAT). The unauthenticated GitHub limit is
217
+ # 60/hour; even with the trees-recursive optimisation a CI loop
218
+ # can exhaust it.
219
+ rate_limit_remaining = response.headers.get("X-RateLimit-Remaining")
220
+ message = (
221
+ f"GitHub API returned {response.status_code} for {action}. Body: {response.text[:200]}"
222
+ )
223
+ if response.status_code == 403 and rate_limit_remaining == "0":
224
+ message = (
225
+ f"GitHub API rate-limit exceeded ({action}). The unauthenticated "
226
+ "limit is 60 requests per hour per IP. Pass --git-pat-env to use a "
227
+ "PAT (raises the limit to 5000/hour)."
228
+ )
229
+ raise KeboolaApiError(
230
+ message=message,
231
+ status_code=response.status_code,
232
+ error_code=ErrorCode.API_ERROR,
233
+ retryable=response.status_code >= 500,
234
+ )
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Pure validation function (no I/O -- I/O lives in the service)
239
+ # ---------------------------------------------------------------------------
240
+
241
+
242
+ @dataclass
243
+ class _RepoSnapshot:
244
+ """Just-enough subset of a GitHub repo for the validator to work on."""
245
+
246
+ paths: set[str]
247
+ truncated: bool
248
+ setup_sh: str | None = None
249
+ pyproject_toml: str | None = None
250
+ nginx_conf: str | None = None
251
+ app_conf: str | None = None
252
+
253
+
254
+ def validate_keboola_repo(
255
+ snapshot: _RepoSnapshot,
256
+ *,
257
+ type_: str,
258
+ runtime_python_pin: str | None = None,
259
+ ) -> list[CheckResult]:
260
+ """Run every validate-repo check against an in-memory snapshot.
261
+
262
+ Pure: no I/O, no client. Tests pass hand-crafted snapshots; the
263
+ service constructs snapshots from real GitHub responses.
264
+
265
+ ``runtime_python_pin`` is optional; if absent the requires-python
266
+ consistency check downgrades to a soft skip. The §1b probe locks the
267
+ canonical pinned version live; offline fallback skips the check
268
+ rather than fabricating one.
269
+ """
270
+ if type_ != "python-js":
271
+ # The command layer rejects non-python-js types; the service
272
+ # boundary check is defence-in-depth.
273
+ return [
274
+ CheckResult(
275
+ name="meta.type-supported",
276
+ severity=SEVERITY_BLOCKING,
277
+ citation=HELP_PYTHON_JS,
278
+ message=(
279
+ f"validate-repo currently supports --type python-js only "
280
+ f"(got: {type_!r}). Other types tracked as follow-up."
281
+ ),
282
+ )
283
+ ]
284
+
285
+ results: list[CheckResult] = []
286
+ paths = snapshot.paths
287
+
288
+ if snapshot.truncated:
289
+ results.append(
290
+ CheckResult(
291
+ name="meta.tree-truncated",
292
+ severity=SEVERITY_WARN,
293
+ citation=HELP_PYTHON_JS,
294
+ message=(
295
+ "Repo tree returned >100k entries (GitHub truncated the "
296
+ "recursive response). File-existence checks may have "
297
+ "false negatives; run with --git-pat-env to raise the "
298
+ "rate limit and retry."
299
+ ),
300
+ )
301
+ )
302
+
303
+ # 1. Golden-Rule existence checks ----------------------------------------
304
+ for required, name in (
305
+ (_NGINX_CONF, "golden-rule.nginx-default-conf"),
306
+ (_APP_CONF, "golden-rule.supervisord-app-conf"),
307
+ (_PYPROJECT, "golden-rule.pyproject-toml"),
308
+ ):
309
+ if required in paths:
310
+ results.append(CheckResult(name=name, severity=SEVERITY_OK, citation=HELP_PYTHON_JS))
311
+ else:
312
+ results.append(
313
+ CheckResult(
314
+ name=name,
315
+ severity=SEVERITY_BLOCKING,
316
+ citation=HELP_PYTHON_JS,
317
+ message=f"Required file not found at {required}.",
318
+ )
319
+ )
320
+
321
+ # 2. setup.sh checks (depend on file presence + content) -----------------
322
+ pyproject_has_deps = _pyproject_declares_deps(snapshot.pyproject_toml)
323
+ setup_sh_present = _SETUP_SH in paths
324
+ if setup_sh_present:
325
+ results.append(
326
+ CheckResult(
327
+ name="golden-rule.setup-sh-present",
328
+ severity=SEVERITY_OK,
329
+ citation=HELP_PYTHON_JS,
330
+ )
331
+ )
332
+ elif pyproject_has_deps:
333
+ results.append(
334
+ CheckResult(
335
+ name="golden-rule.setup-sh-present",
336
+ severity=SEVERITY_BLOCKING,
337
+ citation=HELP_PYTHON_JS,
338
+ message=(
339
+ "pyproject.toml declares dependencies but keboola-config/setup.sh "
340
+ "is missing; the runtime cannot install them without `uv sync`."
341
+ ),
342
+ )
343
+ )
344
+ else:
345
+ results.append(
346
+ CheckResult(
347
+ name="golden-rule.setup-sh-present",
348
+ severity=SEVERITY_WARN,
349
+ citation=HELP_PYTHON_JS,
350
+ message=(
351
+ "keboola-config/setup.sh is absent; intentional only if your "
352
+ "app has zero runtime dependencies."
353
+ ),
354
+ )
355
+ )
356
+
357
+ if setup_sh_present and snapshot.setup_sh is not None:
358
+ if _PIP_INSTALL_RE.search(snapshot.setup_sh):
359
+ results.append(
360
+ CheckResult(
361
+ name="golden-rule.setup-sh-no-pip",
362
+ severity=SEVERITY_BLOCKING,
363
+ citation=HELP_PYTHON_JS,
364
+ message=(
365
+ "keboola-config/setup.sh contains `pip install`. The runtime "
366
+ "blocks pip; replace with `uv sync`."
367
+ ),
368
+ )
369
+ )
370
+ else:
371
+ results.append(
372
+ CheckResult(
373
+ name="golden-rule.setup-sh-no-pip",
374
+ severity=SEVERITY_OK,
375
+ citation=HELP_PYTHON_JS,
376
+ )
377
+ )
378
+
379
+ if pyproject_has_deps:
380
+ if _UV_SYNC_RE.search(snapshot.setup_sh):
381
+ results.append(
382
+ CheckResult(
383
+ name="golden-rule.setup-sh-uv-sync",
384
+ severity=SEVERITY_OK,
385
+ citation=HELP_PYTHON_JS,
386
+ )
387
+ )
388
+ else:
389
+ results.append(
390
+ CheckResult(
391
+ name="golden-rule.setup-sh-uv-sync",
392
+ severity=SEVERITY_WARN,
393
+ citation=HELP_PYTHON_JS,
394
+ message=(
395
+ "pyproject.toml declares dependencies but setup.sh does "
396
+ "not invoke `uv sync`; deps will not install."
397
+ ),
398
+ )
399
+ )
400
+
401
+ # 3. requires-python <= runtime pin --------------------------------------
402
+ if snapshot.pyproject_toml is not None and runtime_python_pin:
403
+ declared = _extract_requires_python(snapshot.pyproject_toml)
404
+ if declared and _requires_python_above_pin(declared, runtime_python_pin):
405
+ results.append(
406
+ CheckResult(
407
+ name="golden-rule.requires-python",
408
+ severity=SEVERITY_BLOCKING,
409
+ citation=HELP_BACKEND_VERSIONS,
410
+ message=(
411
+ f"pyproject.toml requires-python={declared!r} is above the "
412
+ f"data-app runtime pin ({runtime_python_pin}); `uv sync` will fail."
413
+ ),
414
+ details={"declared": declared, "runtime_pin": runtime_python_pin},
415
+ )
416
+ )
417
+ else:
418
+ results.append(
419
+ CheckResult(
420
+ name="golden-rule.requires-python",
421
+ severity=SEVERITY_OK,
422
+ citation=HELP_BACKEND_VERSIONS,
423
+ )
424
+ )
425
+ elif snapshot.pyproject_toml is not None:
426
+ # No runtime pin available offline; skip silently rather than
427
+ # fabricating a constraint.
428
+ results.append(
429
+ CheckResult(
430
+ name="golden-rule.requires-python",
431
+ severity=SEVERITY_WARN,
432
+ citation=HELP_BACKEND_VERSIONS,
433
+ message=(
434
+ "Runtime Python pin not available offline; skipped requires-python "
435
+ "consistency check. Re-run after the §1b probe locks the runtime version."
436
+ ),
437
+ )
438
+ )
439
+
440
+ # 4. nginx proxy_pass port matches app.conf port -------------------------
441
+ if snapshot.nginx_conf is not None and snapshot.app_conf is not None:
442
+ nginx_match = _NGINX_PROXY_PASS_PORT_RE.search(snapshot.nginx_conf)
443
+ app_match = _APP_CONF_PORT_RE.search(snapshot.app_conf)
444
+ if nginx_match and app_match:
445
+ nginx_port = nginx_match.group(1)
446
+ app_port = next((g for g in app_match.groups() if g), "")
447
+ if nginx_port == app_port:
448
+ results.append(
449
+ CheckResult(
450
+ name="golden-rule.nginx-app-port-match",
451
+ severity=SEVERITY_OK,
452
+ citation=HELP_PYTHON_JS,
453
+ details={"port": nginx_port},
454
+ )
455
+ )
456
+ else:
457
+ results.append(
458
+ CheckResult(
459
+ name="golden-rule.nginx-app-port-match",
460
+ severity=SEVERITY_WARN,
461
+ citation=HELP_PYTHON_JS,
462
+ message=(
463
+ f"nginx proxy_pass port ({nginx_port}) does not match "
464
+ f"app.conf port ({app_port}); requests will reach a "
465
+ "different process than configured."
466
+ ),
467
+ details={"nginx_port": nginx_port, "app_port": app_port},
468
+ )
469
+ )
470
+
471
+ return results
472
+
473
+
474
+ def aggregate_verdict(results: list[CheckResult]) -> dict[str, Any]:
475
+ """Return ``{verdict, blocking_count, warn_count, ok_count}`` from results."""
476
+ counts = {SEVERITY_OK: 0, SEVERITY_WARN: 0, SEVERITY_BLOCKING: 0}
477
+ for r in results:
478
+ counts[r.severity] = counts.get(r.severity, 0) + 1
479
+ if counts[SEVERITY_BLOCKING] > 0:
480
+ verdict = SEVERITY_BLOCKING
481
+ elif counts[SEVERITY_WARN] > 0:
482
+ verdict = SEVERITY_WARN
483
+ else:
484
+ verdict = SEVERITY_OK
485
+ return {
486
+ "verdict": verdict,
487
+ "blocking_count": counts[SEVERITY_BLOCKING],
488
+ "warn_count": counts[SEVERITY_WARN],
489
+ "ok_count": counts[SEVERITY_OK],
490
+ }
491
+
492
+
493
+ # ---------------------------------------------------------------------------
494
+ # Service: glue between git URL + GitHub client + pure validator
495
+ # ---------------------------------------------------------------------------
496
+
497
+
498
+ @dataclass
499
+ class _GitHubLocator:
500
+ owner: str
501
+ repo: str
502
+ ref: str
503
+
504
+
505
+ class RepoValidateService(BaseService):
506
+ """Pre-flight validation of a Keboola data-app git repo.
507
+
508
+ Read-only. The service only fetches from GitHub; it never touches a
509
+ Keboola project. ``ConfigStore`` is accepted to match the
510
+ :class:`BaseService` constructor signature but is unused.
511
+ """
512
+
513
+ GITHUB_HOSTS: tuple[str, ...] = ("github.com", "www.github.com")
514
+
515
+ def __init__(
516
+ self,
517
+ config_store: Any,
518
+ github_client_factory: Any | None = None,
519
+ ) -> None:
520
+ super().__init__(config_store=config_store)
521
+ self._github_client_factory = github_client_factory or self._default_github_client
522
+
523
+ @staticmethod
524
+ def _default_github_client(token: str | None) -> GitHubContentsClient:
525
+ return GitHubContentsClient(token=token)
526
+
527
+ def validate_repo(
528
+ self,
529
+ *,
530
+ git_repo: str,
531
+ git_branch: str = "main",
532
+ git_public: bool = True,
533
+ git_pat: str | None = None,
534
+ type_: str = "python-js",
535
+ strict: bool = False,
536
+ ) -> dict[str, Any]:
537
+ del git_public # kwarg accepted for command-layer call shape; unused here.
538
+ if type_ != "python-js":
539
+ raise KeboolaApiError(
540
+ message=(
541
+ f"--type currently supports python-js only (got: {type_!r}). "
542
+ "Other types are tracked as a follow-up."
543
+ ),
544
+ status_code=0,
545
+ error_code=ErrorCode.INVALID_ARGUMENT,
546
+ retryable=False,
547
+ )
548
+
549
+ locator = self._parse_github_url(git_repo, git_branch)
550
+ # The command layer enforces "PAT requires --no-git-public"; here
551
+ # we just forward whatever was supplied. Empty PAT -> anonymous.
552
+ client = self._github_client_factory(git_pat or None)
553
+
554
+ try:
555
+ try:
556
+ tree_response = client.get_tree_recursive(locator.owner, locator.repo, locator.ref)
557
+ except KeboolaApiError as exc:
558
+ # 404 from the trees endpoint usually means private repo
559
+ # without a PAT, or a typo in the URL.
560
+ if exc.status_code == 404 and not git_pat:
561
+ raise KeboolaApiError(
562
+ message=(
563
+ f"GitHub returned 404 for {locator.owner}/{locator.repo}@"
564
+ f"{locator.ref}. If this is a private repo, pass "
565
+ "--git-pat-env / --git-pat-file with a PAT that has "
566
+ "`repo` scope."
567
+ ),
568
+ status_code=404,
569
+ error_code=ErrorCode.VALIDATION_ERROR,
570
+ retryable=False,
571
+ ) from exc
572
+ raise
573
+
574
+ tree_entries = tree_response.get("tree", [])
575
+ paths: set[str] = set()
576
+ if isinstance(tree_entries, list):
577
+ for entry in tree_entries:
578
+ if isinstance(entry, dict):
579
+ path = entry.get("path")
580
+ if isinstance(path, str):
581
+ paths.add(path)
582
+ truncated = bool(tree_response.get("truncated"))
583
+
584
+ # Up to 4 content fetches: setup.sh, pyproject.toml,
585
+ # nginx-conf and app-conf -- capped at 4. Combined with the
586
+ # tree fetch above the worst case is 5 GitHub calls, still
587
+ # bounded regardless of repo size.
588
+ setup_sh = (
589
+ client.get_file_content(locator.owner, locator.repo, _SETUP_SH, locator.ref)
590
+ if _SETUP_SH in paths
591
+ else None
592
+ )
593
+ pyproject = (
594
+ client.get_file_content(locator.owner, locator.repo, _PYPROJECT, locator.ref)
595
+ if _PYPROJECT in paths
596
+ else None
597
+ )
598
+ nginx_conf = (
599
+ client.get_file_content(locator.owner, locator.repo, _NGINX_CONF, locator.ref)
600
+ if _NGINX_CONF in paths
601
+ else None
602
+ )
603
+ app_conf = (
604
+ client.get_file_content(locator.owner, locator.repo, _APP_CONF, locator.ref)
605
+ if _APP_CONF in paths
606
+ else None
607
+ )
608
+
609
+ snapshot = _RepoSnapshot(
610
+ paths=paths,
611
+ truncated=truncated,
612
+ setup_sh=setup_sh,
613
+ pyproject_toml=pyproject,
614
+ nginx_conf=nginx_conf,
615
+ app_conf=app_conf,
616
+ )
617
+ results = validate_keboola_repo(snapshot, type_=type_, runtime_python_pin=None)
618
+ finally:
619
+ client.close()
620
+
621
+ verdict = aggregate_verdict(results)
622
+ is_failure = verdict["verdict"] == SEVERITY_BLOCKING or (
623
+ strict and verdict["verdict"] == SEVERITY_WARN
624
+ )
625
+
626
+ return {
627
+ "git_repo": git_repo,
628
+ "git_branch": git_branch,
629
+ "type": type_,
630
+ "checks": [r.to_dict() for r in results],
631
+ **verdict,
632
+ "strict": strict,
633
+ "is_failure": is_failure,
634
+ "message": _format_verdict_message(verdict, strict=strict),
635
+ }
636
+
637
+ def _parse_github_url(self, url: str, ref: str) -> _GitHubLocator:
638
+ if not url:
639
+ raise KeboolaApiError(
640
+ message="--git-repo is required.",
641
+ status_code=0,
642
+ error_code=ErrorCode.MISSING_PARAMETER,
643
+ retryable=False,
644
+ )
645
+ # Allow common forms: https://github.com/owner/repo,
646
+ # https://github.com/owner/repo.git, http://github.com/...
647
+ try:
648
+ parsed = urlparse(url)
649
+ except (ValueError, TypeError) as exc:
650
+ raise KeboolaApiError(
651
+ message=f"Cannot parse --git-repo {url!r}: {exc}",
652
+ status_code=0,
653
+ error_code=ErrorCode.INVALID_FORMAT,
654
+ retryable=False,
655
+ ) from exc
656
+
657
+ host = (parsed.hostname or "").lower()
658
+ if host not in self.GITHUB_HOSTS:
659
+ raise KeboolaApiError(
660
+ message=(
661
+ f"validate-repo currently supports github.com only "
662
+ f"(got: {parsed.hostname or url!r}). GitLab / Bitbucket "
663
+ "tracked as follow-up."
664
+ ),
665
+ status_code=0,
666
+ error_code=ErrorCode.INVALID_ARGUMENT,
667
+ retryable=False,
668
+ )
669
+
670
+ path = (parsed.path or "").strip("/")
671
+ if path.endswith(".git"):
672
+ path = path[: -len(".git")]
673
+ parts = path.split("/")
674
+ if len(parts) < 2 or not parts[0] or not parts[1]:
675
+ raise KeboolaApiError(
676
+ message=(
677
+ f"Cannot extract owner/repo from {url!r}; expected "
678
+ "https://github.com/<owner>/<repo>."
679
+ ),
680
+ status_code=0,
681
+ error_code=ErrorCode.INVALID_FORMAT,
682
+ retryable=False,
683
+ )
684
+ return _GitHubLocator(owner=parts[0], repo=parts[1], ref=ref)
685
+
686
+
687
+ def _pyproject_declares_deps(content: str | None) -> bool:
688
+ """Heuristic: does the pyproject.toml declare runtime dependencies?
689
+
690
+ Looks for a non-empty ``dependencies`` array under ``[project]``,
691
+ ``[tool.poetry.dependencies]``, or ``[tool.uv]`` — the three places
692
+ that matter for `uv sync`. False positives are operationally cheap
693
+ (a WARN nudge); false negatives would skip the setup.sh check.
694
+ """
695
+ if not content:
696
+ return False
697
+ # Cheap regex match for any non-empty dependencies = [...] declaration.
698
+ return bool(
699
+ re.search(r"^\s*dependencies\s*=\s*\[\s*[^\]\s]", content, re.MULTILINE)
700
+ or re.search(
701
+ r"^\[tool\.poetry\.dependencies\]\s*\n[^\[]*?\b\w+\s*=",
702
+ content,
703
+ re.MULTILINE,
704
+ )
705
+ )
706
+
707
+
708
+ def _extract_requires_python(content: str) -> str | None:
709
+ match = _REQUIRES_PYTHON_RE.search(content)
710
+ return match.group(1).strip() if match else None
711
+
712
+
713
+ def _requires_python_above_pin(declared: str, pin: str) -> bool:
714
+ """Return True if ``declared`` requires a Python newer than ``pin``.
715
+
716
+ Heuristic only -- declared is a PEP 440 spec like ``>=3.13`` and pin
717
+ is a concrete release like ``3.12.10``. We compare numeric major.minor
718
+ pairs; if the declared lower bound's minor exceeds the pin's minor,
719
+ we flag. False positives on patch-level constraints (``>=3.12.20``
720
+ against pin ``3.12.10``) are accepted as a known limitation -- the
721
+ Keboola runtime pin moves with patch bumps but the help canon does
722
+ not commit to them.
723
+ """
724
+ bound_match = re.search(r">=\s*(\d+)\.(\d+)", declared)
725
+ if not bound_match:
726
+ return False
727
+ declared_major = int(bound_match.group(1))
728
+ declared_minor = int(bound_match.group(2))
729
+ pin_match = re.match(r"(\d+)\.(\d+)", pin)
730
+ if not pin_match:
731
+ return False
732
+ pin_major = int(pin_match.group(1))
733
+ pin_minor = int(pin_match.group(2))
734
+ if declared_major > pin_major:
735
+ return True
736
+ return declared_major == pin_major and declared_minor > pin_minor
737
+
738
+
739
+ def _format_verdict_message(verdict: dict[str, Any], *, strict: bool) -> str:
740
+ blocking = verdict.get("blocking_count", 0)
741
+ warn = verdict.get("warn_count", 0)
742
+ if blocking:
743
+ return (
744
+ f"{blocking} BLOCKING and {warn} WARN check(s). Fix the BLOCKING "
745
+ "entries before `kbagent data-app create`."
746
+ )
747
+ if warn and strict:
748
+ return f"0 BLOCKING and {warn} WARN check(s); --strict treats WARNs as failures."
749
+ if warn:
750
+ return f"0 BLOCKING and {warn} WARN check(s). Repo is deployable; review WARNs."
751
+ return "All checks passed."
752
+
753
+
754
+ # Re-export ConfigError so callers don't import from .errors twice (the
755
+ # service module is the natural seam between command and the validation
756
+ # function).
757
+ __all__ = [
758
+ "SEVERITY_BLOCKING",
759
+ "SEVERITY_OK",
760
+ "SEVERITY_WARN",
761
+ "CheckResult",
762
+ "ConfigError",
763
+ "GitHubContentsClient",
764
+ "RepoValidateService",
765
+ "aggregate_verdict",
766
+ "validate_keboola_repo",
767
+ ]