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,1464 @@
1
+ """Lineage commands - column-level dependency analysis across projects.
2
+
3
+ Thin CLI layer: parses arguments, calls DeepLineageService, formats output.
4
+ No business logic belongs here.
5
+
6
+ Four subcommands:
7
+ build -- scan sync'd projects, build lineage graph, save cache
8
+ show -- query upstream/downstream from cached graph
9
+ serve -- start local web server with interactive lineage browser
10
+ """
11
+
12
+ import http.server
13
+ import json
14
+ import re
15
+ import threading
16
+ import webbrowser
17
+ from pathlib import Path
18
+ from urllib.parse import parse_qs, urlparse
19
+
20
+ import typer
21
+
22
+ from ..errors import ErrorCode
23
+ from ..services.deep_lineage_service import DeepLineageService, LineageGraph
24
+ from ._helpers import (
25
+ check_cli_permission,
26
+ get_formatter,
27
+ get_service,
28
+ )
29
+
30
+ lineage_app = typer.Typer(
31
+ help="Column-level data lineage across projects.\n\n"
32
+ "Build a dependency graph from sync'd data, then query upstream/downstream."
33
+ )
34
+
35
+
36
+ @lineage_app.callback(invoke_without_command=True)
37
+ def _lineage_callback(ctx: typer.Context) -> None:
38
+ check_cli_permission(ctx, "lineage")
39
+ if ctx.invoked_subcommand is None:
40
+ # No subcommand -> show help
41
+ click_cmd = typer.main.get_command(lineage_app)
42
+ ctx_help = click_cmd.make_context("lineage", [])
43
+ typer.echo(click_cmd.get_help(ctx_help))
44
+
45
+
46
+ # -- lineage build ---------------------------------------------------------
47
+
48
+
49
+ @lineage_app.command("build")
50
+ def lineage_build(
51
+ ctx: typer.Context,
52
+ directory: Path = typer.Option(
53
+ Path("."),
54
+ "--directory",
55
+ "-d",
56
+ help="Root directory with sync'd projects (default: current directory).",
57
+ ),
58
+ output: Path = typer.Option(
59
+ ...,
60
+ "--output",
61
+ "-o",
62
+ help="Output JSON file for the lineage graph (required).",
63
+ ),
64
+ ai: bool = typer.Option(
65
+ False,
66
+ "--ai",
67
+ help="Generate AI task file for SQL/Python analysis (2-step: AI processes, then re-build).",
68
+ ),
69
+ refresh: bool = typer.Option(
70
+ False,
71
+ "--refresh",
72
+ help="Sync pull all projects first, then rebuild.",
73
+ ),
74
+ ) -> None:
75
+ """Build column-level lineage graph from sync'd data.
76
+
77
+ Scans all sync'd projects (from `sync pull --all-projects`), detects
78
+ table dependencies via config mappings and SQL parsing, and saves the
79
+ graph to a JSON cache file for fast queries with `lineage show`.
80
+
81
+ AI-enhanced analysis is a 2-step process:
82
+
83
+ 1. kbagent lineage build -d /path -o lineage.json --ai
84
+ (builds deterministic graph + generates .lineage_ai_tasks.json)
85
+
86
+ 2. AI agent reads the task file, analyzes each code file from disk,
87
+ writes results to .lineage_ai_results.json
88
+
89
+ 3. kbagent lineage build -d /path -o lineage.json
90
+ (automatically applies AI results if .lineage_ai_results.json exists)
91
+ """
92
+ formatter = get_formatter(ctx)
93
+ service = get_service(ctx, "deep_lineage_service")
94
+
95
+ root = directory.resolve()
96
+ if not root.is_dir():
97
+ formatter.error(message=f"Directory not found: {root}", error_code=ErrorCode.DIR_NOT_FOUND)
98
+ raise typer.Exit(code=1)
99
+
100
+ # --refresh: sync pull all projects first
101
+ if refresh:
102
+ if not formatter.json_mode:
103
+ formatter.console.print("[bold]Syncing all projects...[/bold]")
104
+ sync_service = get_service(ctx, "sync_service")
105
+ sync_result = sync_service.pull_all(base_dir=root)
106
+ summary = sync_result.get("summary", {})
107
+ if not formatter.json_mode:
108
+ formatter.console.print(
109
+ f" Synced {summary.get('success', 0)}/{summary.get('total', 0)} projects"
110
+ f" ({summary.get('failed', 0)} failed)\n"
111
+ )
112
+
113
+ result = service.build_lineage(root, generate_ai_tasks=ai)
114
+
115
+ try:
116
+ with open(output, "w") as f:
117
+ json.dump(result, f, indent=2)
118
+ except OSError as exc:
119
+ formatter.error(
120
+ message=f"Cannot write output file '{output}': {exc}",
121
+ error_code=ErrorCode.WRITE_ERROR,
122
+ )
123
+ raise typer.Exit(code=1) from None
124
+
125
+ if formatter.json_mode:
126
+ formatter.output(result)
127
+ else:
128
+ summary = result.get("summary", {})
129
+ formatter.console.print("\n[bold]Lineage graph built[/bold]")
130
+ formatter.console.print(f" Tables: {summary.get('tables', 0)}")
131
+ formatter.console.print(f" Configurations: {summary.get('configurations', 0)}")
132
+ formatter.console.print(f" Edges: {summary.get('edges', 0)}")
133
+ if summary.get("detection_methods"):
134
+ formatter.console.print("\n Detection methods:")
135
+ for k, v in sorted(summary["detection_methods"].items(), key=lambda x: -x[1]):
136
+ formatter.console.print(f" {k}: {v}")
137
+ # AI status
138
+ ai_status = result.get("ai_status", {})
139
+ if ai_status.get("ai_results_applied"):
140
+ formatter.console.print(
141
+ f"\n AI results applied: {ai_status.get('ai_edges_added', 0)} edges added"
142
+ )
143
+ if ai_status.get("ai_tasks_generated"):
144
+ formatter.console.print(
145
+ f"\n [bold]AI tasks generated: {ai_status['ai_tasks_generated']}[/bold]"
146
+ f" (already done: {ai_status.get('ai_already_done', 0)})"
147
+ )
148
+ formatter.console.print(f" Task file: {ai_status['ai_tasks_file']}")
149
+ formatter.console.print(
150
+ " Next: let your AI agent process the tasks, then re-run this command."
151
+ )
152
+ # Surface warnings (e.g. empty scan) so users notice layout issues.
153
+ for warning in result.get("warnings", []) or []:
154
+ formatter.console.print(f"\n[yellow]Warning:[/yellow] {warning}")
155
+ formatter.console.print(f"\n Saved to: {output}")
156
+
157
+
158
+ # -- lineage info ----------------------------------------------------------
159
+
160
+
161
+ @lineage_app.command("info")
162
+ def lineage_info(
163
+ ctx: typer.Context,
164
+ load: Path = typer.Option(
165
+ ...,
166
+ "--load",
167
+ "-l",
168
+ help="Lineage JSON cache file (from `lineage build`).",
169
+ ),
170
+ ) -> None:
171
+ """Show what's in a cached lineage graph.
172
+
173
+ Displays per-project breakdown (tables, configs) and lists the most
174
+ connected tables -- good starting points for upstream/downstream queries.
175
+
176
+ Example:
177
+
178
+ kbagent lineage info -l lineage.json
179
+ """
180
+ formatter = get_formatter(ctx)
181
+ service = get_service(ctx, "deep_lineage_service")
182
+
183
+ if not load.exists():
184
+ formatter.error(
185
+ message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
186
+ )
187
+ raise typer.Exit(code=1)
188
+
189
+ graph = service.load_from_cache(load)
190
+
191
+ if formatter.json_mode:
192
+ formatter.output(graph.to_dict())
193
+ return
194
+
195
+ summary = graph.summary()
196
+ formatter.console.print("\n[bold]Lineage Graph Contents[/bold]")
197
+ formatter.console.print(f" Tables: {summary.get('tables', 0)}")
198
+ formatter.console.print(f" Configurations: {summary.get('configurations', 0)}")
199
+ formatter.console.print(f" Edges: {summary.get('edges', 0)}")
200
+ if summary.get("detection_methods"):
201
+ formatter.console.print("\n Detection methods:")
202
+ for k, v in sorted(summary["detection_methods"].items(), key=lambda x: -x[1]):
203
+ formatter.console.print(f" {k}: {v}")
204
+
205
+ # Per-project breakdown
206
+ proj_tables: dict[str, int] = {}
207
+ proj_configs: dict[str, int] = {}
208
+ for t in graph.tables.values():
209
+ proj_tables[t.project_alias] = proj_tables.get(t.project_alias, 0) + 1
210
+ for c in graph.configurations.values():
211
+ proj_configs[c.project_alias] = proj_configs.get(c.project_alias, 0) + 1
212
+ all_projects = sorted(set(proj_tables) | set(proj_configs))
213
+ if all_projects:
214
+ formatter.console.print("\n [bold]Projects:[/bold]")
215
+ for proj in all_projects:
216
+ nt = proj_tables.get(proj, 0)
217
+ nc = proj_configs.get(proj, 0)
218
+ formatter.console.print(f" {proj:40s} {nt:4d} tables, {nc:4d} configs")
219
+
220
+ # Most connected tables
221
+ edge_counts: dict[str, int] = {}
222
+ for e in graph.edges:
223
+ for fqn in (e.source_fqn, e.target_fqn):
224
+ if fqn in graph.tables:
225
+ edge_counts[fqn] = edge_counts.get(fqn, 0) + 1
226
+ top = sorted(edge_counts.items(), key=lambda x: -x[1])[:15]
227
+ if top:
228
+ formatter.console.print(
229
+ "\n [bold]Most connected tables[/bold]"
230
+ " (use with [cyan]lineage show --upstream/--downstream[/cyan]):"
231
+ )
232
+ for fqn, count in top:
233
+ t = graph.tables[fqn]
234
+ formatter.console.print(f" {fqn:60s} {count:3d} edges, {t.rows_count:>12,} rows")
235
+
236
+
237
+ # -- lineage show ----------------------------------------------------------
238
+
239
+
240
+ @lineage_app.command("show")
241
+ def lineage_show(
242
+ ctx: typer.Context,
243
+ load: Path = typer.Option(
244
+ ...,
245
+ "--load",
246
+ "-l",
247
+ help="Lineage JSON cache file (from `lineage build`).",
248
+ ),
249
+ upstream: str | None = typer.Option(
250
+ None,
251
+ "--upstream",
252
+ help="Show upstream dependencies. Use 'project:table_id' or just 'table_id'.",
253
+ ),
254
+ downstream: str | None = typer.Option(
255
+ None,
256
+ "--downstream",
257
+ help="Show downstream dependents. Use 'project:table_id' or just 'table_id'.",
258
+ ),
259
+ column: str | None = typer.Option(
260
+ None,
261
+ "--column",
262
+ "-c",
263
+ help="Trace a specific column (use with --upstream/--downstream).",
264
+ ),
265
+ columns: bool = typer.Option(
266
+ False,
267
+ "--columns",
268
+ help="Show column-level mapping detail on edges.",
269
+ ),
270
+ project: str | None = typer.Option(
271
+ None,
272
+ "--project",
273
+ "-p",
274
+ help="Project alias filter for queries.",
275
+ ),
276
+ depth: int = typer.Option(10, "--depth", help="Max traversal depth (default: 10)."),
277
+ format: str = typer.Option(
278
+ "text",
279
+ "--format",
280
+ "-f",
281
+ help="Output format: text, mermaid, html, or er (entity-relationship).",
282
+ ),
283
+ ) -> None:
284
+ """Query upstream/downstream dependencies from a cached lineage graph.
285
+
286
+ Requires a lineage cache file built with `lineage build`.
287
+
288
+ Node identifiers for --upstream/--downstream:
289
+
290
+ Full FQN: project-alias:bucket_id.table_name
291
+
292
+ Table only: bucket_id.table_name (auto-resolves, warns if ambiguous)
293
+
294
+ Output formats (--format):
295
+
296
+ text Rich tree (default)
297
+
298
+ mermaid Mermaid flowchart source code
299
+
300
+ html Self-contained HTML file with embedded mermaid diagram
301
+
302
+ Examples:
303
+
304
+ kbagent lineage show -l lineage.json --downstream "project:table"
305
+
306
+ kbagent lineage show -l lineage.json --upstream "project:table" --columns
307
+
308
+ kbagent lineage show -l lineage.json --upstream "project:table" -c "col_name"
309
+
310
+ kbagent lineage show -l lineage.json --downstream "project:table" -f mermaid
311
+
312
+ kbagent lineage show -l lineage.json --downstream "project:table" -f html
313
+ """
314
+ formatter = get_formatter(ctx)
315
+ service = get_service(ctx, "deep_lineage_service")
316
+
317
+ valid_formats = ("text", "mermaid", "html", "er")
318
+ if format not in valid_formats:
319
+ formatter.error(
320
+ message=f"Invalid format '{format}'. Must be one of: {', '.join(valid_formats)}",
321
+ error_code=ErrorCode.INVALID_FORMAT,
322
+ )
323
+ raise typer.Exit(code=2)
324
+
325
+ if not load.exists():
326
+ formatter.error(
327
+ message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
328
+ )
329
+ raise typer.Exit(code=1)
330
+
331
+ graph = service.load_from_cache(load)
332
+
333
+ if not upstream and not downstream:
334
+ formatter.error(
335
+ message="Specify --upstream or --downstream to query.\n"
336
+ "Use `kbagent lineage info -l FILE` to see what's in the graph.",
337
+ error_code=ErrorCode.MISSING_QUERY,
338
+ )
339
+ raise typer.Exit(code=2)
340
+
341
+ display_opts = {"show_columns": columns, "filter_column": column}
342
+
343
+ if upstream:
344
+ query_result = service.query_upstream(graph, upstream, project or "", depth)
345
+ if "error" in query_result:
346
+ suggestions = query_result.get("suggestions", [])
347
+ msg = query_result["error"]
348
+ if suggestions:
349
+ msg += "\nDid you mean: " + ", ".join(suggestions[:5])
350
+ formatter.error(message=msg, error_code=ErrorCode.NODE_NOT_FOUND)
351
+ raise typer.Exit(code=1)
352
+
353
+ if formatter.json_mode:
354
+ if column:
355
+ query_result = _filter_column_json(query_result, column)
356
+ formatter.output(query_result)
357
+ elif format in ("mermaid", "html", "er"):
358
+ _output_mermaid_or_html(formatter, service, graph, query_result, "upstream", format)
359
+ else:
360
+ _format_lineage_tree(formatter, graph, query_result, "upstream", **display_opts)
361
+
362
+ if downstream:
363
+ query_result = service.query_downstream(graph, downstream, project or "", depth)
364
+ if "error" in query_result:
365
+ suggestions = query_result.get("suggestions", [])
366
+ msg = query_result["error"]
367
+ if suggestions:
368
+ msg += "\nDid you mean: " + ", ".join(suggestions[:5])
369
+ formatter.error(message=msg, error_code=ErrorCode.NODE_NOT_FOUND)
370
+ raise typer.Exit(code=1)
371
+
372
+ if formatter.json_mode:
373
+ if column:
374
+ query_result = _filter_column_json(query_result, column)
375
+ formatter.output(query_result)
376
+ elif format in ("mermaid", "html", "er"):
377
+ _output_mermaid_or_html(formatter, service, graph, query_result, "downstream", format)
378
+ else:
379
+ _format_lineage_tree(formatter, graph, query_result, "downstream", **display_opts)
380
+
381
+
382
+ # -- Output formatting helpers ----------------------------------------------
383
+
384
+
385
+ def _output_mermaid_or_html(
386
+ formatter,
387
+ service,
388
+ graph,
389
+ query_result: dict,
390
+ direction: str,
391
+ output_format: str,
392
+ ) -> None:
393
+ """Render query result as mermaid, html, or er diagram and output it."""
394
+ from ..services.deep_lineage_service import DeepLineageService
395
+
396
+ node_fqn = query_result["node"]
397
+ edges = query_result.get("edges", [])
398
+
399
+ if output_format == "er":
400
+ er_code = DeepLineageService.render_er_diagram(edges, graph, node_fqn)
401
+ typer.echo(er_code)
402
+ return
403
+
404
+ mermaid_code = DeepLineageService.render_mermaid(edges, graph, direction, node_fqn)
405
+
406
+ if output_format == "mermaid":
407
+ typer.echo(mermaid_code)
408
+ elif output_format == "html":
409
+ title = f"Lineage {direction} of {node_fqn}"
410
+ html_content = DeepLineageService.render_html(mermaid_code, title)
411
+ sanitized_node = re.sub(r"[^a-zA-Z0-9_]", "_", node_fqn)
412
+ filename = f"lineage_{direction}_{sanitized_node}.html"
413
+ try:
414
+ with open(filename, "w") as f:
415
+ f.write(html_content)
416
+ if not formatter.json_mode:
417
+ formatter.console.print(f"HTML lineage diagram saved to: {filename}")
418
+ except OSError as exc:
419
+ formatter.error(
420
+ message=f"Cannot write HTML file '{filename}': {exc}",
421
+ error_code=ErrorCode.WRITE_ERROR,
422
+ )
423
+ raise typer.Exit(code=1) from None
424
+
425
+
426
+ def _filter_column_json(result: dict, column_name: str) -> dict:
427
+ """Filter JSON query result to only edges relevant to a specific column."""
428
+ filtered_edges = []
429
+ col_lower = column_name.lower()
430
+ for edge in result.get("edges", []):
431
+ col_map = edge.get("column_mapping", {})
432
+ edge_columns = edge.get("columns", [])
433
+ mapped_keys = [k for k in col_map if k.lower() == col_lower]
434
+ mapped_vals = [k for k, v in col_map.items() if v.lower().endswith(f".{col_lower}")]
435
+ col_match = any(c.lower() == col_lower for c in edge_columns)
436
+ if mapped_keys or mapped_vals or col_match:
437
+ relevant_map = {
438
+ k: v for k, v in col_map.items() if k in mapped_keys or k in mapped_vals
439
+ }
440
+ edge_copy = dict(edge)
441
+ if relevant_map:
442
+ edge_copy["column_mapping"] = relevant_map
443
+ filtered_edges.append(edge_copy)
444
+ elif not col_map and not edge_columns:
445
+ filtered_edges.append(edge)
446
+ result_copy = dict(result)
447
+ result_copy["edges"] = filtered_edges
448
+ result_copy["column_filter"] = column_name
449
+ return result_copy
450
+
451
+
452
+ def _format_lineage_tree(
453
+ formatter,
454
+ graph,
455
+ result: dict,
456
+ direction: str,
457
+ show_columns: bool = False,
458
+ filter_column: str | None = None,
459
+ ) -> None:
460
+ """Format lineage query result as a human-readable tree."""
461
+ from ..services.deep_lineage_service import LineageGraph
462
+
463
+ node_fqn = result["node"]
464
+ node_info = result.get("node_info", {})
465
+ edges = result.get("edges", [])
466
+
467
+ arrow = "<-" if direction == "upstream" else "->"
468
+ label = "Upstream dependencies" if direction == "upstream" else "Downstream dependents"
469
+
470
+ node_type = node_info.get("type", "unknown")
471
+ if node_type == "table":
472
+ n_cols = node_info.get("columns", 0)
473
+ rows = node_info.get("rows", 0)
474
+ desc = f"[table] {node_fqn} ({n_cols} cols, {rows:,} rows)"
475
+ elif node_info.get("name"):
476
+ desc = f"[{node_type}] {node_info['name']} ({node_info.get('component', '')})"
477
+ else:
478
+ desc = f"[{node_type}] {node_fqn}"
479
+
480
+ header = f"\n[bold]{label} of {desc}[/bold]"
481
+ if filter_column:
482
+ header += f" [dim](column: {filter_column})[/dim]"
483
+ formatter.console.print(header + "\n")
484
+
485
+ if not edges:
486
+ formatter.console.print(" (none found)")
487
+ return
488
+
489
+ col_lower = filter_column.lower() if filter_column else None
490
+
491
+ for edge in sorted(edges, key=lambda e: e["depth"]):
492
+ col_map = edge.get("column_mapping", {})
493
+ edge_columns = edge.get("columns", [])
494
+
495
+ if col_lower:
496
+ has_in_map = any(
497
+ k.lower() == col_lower or v.lower().endswith(f".{col_lower}")
498
+ for k, v in col_map.items()
499
+ )
500
+ has_in_cols = any(c.lower() == col_lower for c in edge_columns)
501
+ is_structural = not col_map and not edge_columns
502
+ if not has_in_map and not has_in_cols and not is_structural:
503
+ continue
504
+
505
+ indent = " " * edge["depth"]
506
+ target_fqn = edge["source"] if direction == "upstream" else edge["target"]
507
+
508
+ if isinstance(graph, LineageGraph):
509
+ if target_fqn in graph.tables:
510
+ t = graph.tables[target_fqn]
511
+ node_desc = f"[table] {target_fqn} ({len(t.columns)} cols, {t.rows_count:,} rows)"
512
+ elif target_fqn in graph.configurations:
513
+ c = graph.configurations[target_fqn]
514
+ node_desc = (
515
+ f"[{c.component_type}] {c.project_alias}:{c.config_name} ({c.component_id})"
516
+ )
517
+ else:
518
+ node_desc = target_fqn
519
+ else:
520
+ node_desc = target_fqn
521
+
522
+ col_hint = ""
523
+ if not show_columns and edge_columns:
524
+ col_list = edge_columns[:5]
525
+ suffix = f"... +{len(edge_columns) - 5}" if len(edge_columns) > 5 else ""
526
+ col_hint = f" [{', '.join(col_list)}{suffix}]"
527
+
528
+ formatter.console.print(f"{indent}{arrow} ({edge['detection']}) {node_desc}{col_hint}")
529
+
530
+ if show_columns and col_map:
531
+ map_indent = " " * (edge["depth"] + 1)
532
+ items = list(col_map.items())
533
+ if col_lower:
534
+ items = [
535
+ (k, v)
536
+ for k, v in items
537
+ if k.lower() == col_lower or v.lower().endswith(f".{col_lower}")
538
+ ]
539
+ for out_col, src_expr in items:
540
+ src_short = src_expr.split(".")[-1] if "." in src_expr else src_expr
541
+ src_table = ".".join(src_expr.split(".")[:-1]) if "." in src_expr else ""
542
+ if src_table:
543
+ formatter.console.print(
544
+ f"{map_indent}[dim]{out_col}[/dim] <- {src_table}.[bold]{src_short}[/bold]"
545
+ )
546
+ else:
547
+ formatter.console.print(f"{map_indent}[dim]{out_col}[/dim] <- {src_expr}")
548
+ elif show_columns and edge_columns:
549
+ map_indent = " " * (edge["depth"] + 1)
550
+ show_cols = edge_columns
551
+ if col_lower:
552
+ show_cols = [c for c in edge_columns if c.lower() == col_lower]
553
+ for c in show_cols[:10]:
554
+ formatter.console.print(f"{map_indent}[dim]{c}[/dim]")
555
+ if len(show_cols) > 10:
556
+ formatter.console.print(f"{map_indent}[dim]... +{len(show_cols) - 10} more[/dim]")
557
+
558
+
559
+ # -- lineage serve ---------------------------------------------------------
560
+
561
+ _LINEAGE_HTML_TEMPLATE = r"""<!DOCTYPE html>
562
+ <html lang="en">
563
+ <head>
564
+ <meta charset="utf-8">
565
+ <title>Keboola Lineage Browser</title>
566
+ <style>
567
+ * { margin: 0; padding: 0; box-sizing: border-box; }
568
+ body {
569
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
570
+ height: 100vh; overflow: hidden; display: flex; background: #fff; color: #333;
571
+ }
572
+ /* -- Sidebar -- */
573
+ #sidebar {
574
+ width: 280px; min-width: 280px; background: #f8f9fa;
575
+ border-right: 1px solid #e0e0e0; display: flex; flex-direction: column;
576
+ height: 100vh; overflow: hidden;
577
+ }
578
+ #sidebar-header {
579
+ padding: 16px; border-bottom: 1px solid #e0e0e0;
580
+ }
581
+ #sidebar-header h1 {
582
+ font-size: 15px; font-weight: 700; color: #1a73e8; margin-bottom: 12px;
583
+ }
584
+ #project-select {
585
+ width: 100%; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px;
586
+ font-size: 13px; background: #fff; color: #333; outline: none;
587
+ }
588
+ #project-select:focus { border-color: #1a73e8; }
589
+
590
+ /* Tabs */
591
+ #tabs {
592
+ display: flex; border-bottom: 1px solid #e0e0e0;
593
+ }
594
+ .tab {
595
+ flex: 1; padding: 8px 0; text-align: center; font-size: 12px; font-weight: 600;
596
+ cursor: pointer; color: #5f6368; border-bottom: 2px solid transparent;
597
+ background: none; border-top: none; border-left: none; border-right: none;
598
+ }
599
+ .tab.active { color: #1a73e8; border-bottom-color: #1a73e8; }
600
+ .tab:hover { background: #e8eaed; }
601
+
602
+ /* Search */
603
+ #node-search {
604
+ margin: 8px 12px; padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px;
605
+ font-size: 13px; outline: none; width: calc(100% - 24px);
606
+ }
607
+ #node-search:focus { border-color: #1a73e8; }
608
+
609
+ /* Node list */
610
+ #node-list {
611
+ flex: 1; overflow-y: auto; padding: 0;
612
+ }
613
+ .node-item {
614
+ padding: 6px 12px; cursor: pointer; font-size: 12px; color: #333;
615
+ border-bottom: 1px solid #f0f0f0; white-space: nowrap; overflow: hidden;
616
+ text-overflow: ellipsis;
617
+ }
618
+ .node-item:hover { background: #e8f0fe; }
619
+ .node-item.selected { background: #d2e3fc; font-weight: 600; }
620
+ .node-item .node-meta {
621
+ font-size: 11px; color: #888; margin-top: 1px;
622
+ }
623
+
624
+ /* Controls below list */
625
+ #query-controls {
626
+ padding: 12px; border-top: 1px solid #e0e0e0; background: #f8f9fa;
627
+ }
628
+ #query-controls label { font-size: 12px; font-weight: 600; color: #5f6368; }
629
+ .radio-group {
630
+ display: flex; gap: 12px; margin: 4px 0 8px 0;
631
+ }
632
+ .radio-group label { font-weight: 400; font-size: 12px; cursor: pointer; }
633
+ .radio-group input { margin-right: 3px; }
634
+ #depth-row { display: flex; align-items: center; gap: 8px; }
635
+ #depth-slider { flex: 1; }
636
+ #depth-value { font-size: 12px; color: #333; min-width: 16px; }
637
+
638
+ /* -- Main area -- */
639
+ #main {
640
+ flex: 1; display: flex; flex-direction: column; overflow: hidden;
641
+ }
642
+ #main-header {
643
+ padding: 12px 20px; border-bottom: 1px solid #e0e0e0;
644
+ display: flex; align-items: center; justify-content: space-between;
645
+ min-height: 48px; background: #fff;
646
+ }
647
+ #main-header h2 {
648
+ font-size: 14px; font-weight: 600; color: #333; margin: 0;
649
+ }
650
+ #main-stats {
651
+ font-size: 12px; color: #888;
652
+ }
653
+ #export-buttons {
654
+ display: flex; gap: 6px;
655
+ }
656
+ #export-buttons button {
657
+ padding: 4px 10px; font-size: 11px; border: 1px solid #dadce0;
658
+ border-radius: 4px; background: #fff; color: #333; cursor: pointer;
659
+ }
660
+ #export-buttons button:hover { background: #f1f3f4; }
661
+
662
+ /* Diagram area */
663
+ #diagram-area {
664
+ flex: 1; overflow: auto; padding: 20px; display: flex;
665
+ align-items: flex-start; justify-content: center;
666
+ }
667
+ #diagram-area .mermaid-container {
668
+ max-width: 100%; overflow: auto;
669
+ }
670
+ #diagram-area .mermaid-container svg {
671
+ max-width: none;
672
+ }
673
+ #placeholder {
674
+ color: #999; font-size: 14px; text-align: center; margin-top: 100px;
675
+ }
676
+ #placeholder p { margin: 8px 0; }
677
+
678
+ /* Loading indicator */
679
+ #loading {
680
+ display: none; color: #1a73e8; font-size: 13px; text-align: center;
681
+ margin-top: 80px;
682
+ }
683
+
684
+ /* Responsive: collapse sidebar on narrow screens */
685
+ @media (max-width: 700px) {
686
+ #sidebar { width: 220px; min-width: 220px; }
687
+ }
688
+ @media (max-width: 500px) {
689
+ body { flex-direction: column; }
690
+ #sidebar { width: 100%; min-width: 100%; height: 40vh; }
691
+ #main { height: 60vh; }
692
+ }
693
+ </style>
694
+ </head>
695
+ <body>
696
+
697
+ <!-- Sidebar -->
698
+ <div id="sidebar">
699
+ <div id="sidebar-header">
700
+ <h1>Keboola Lineage Browser</h1>
701
+ <select id="project-select"><option value="">Loading...</option></select>
702
+ </div>
703
+ <div id="tabs">
704
+ <button class="tab active" data-type="tables">Tables</button>
705
+ <button class="tab" data-type="configs">Configs</button>
706
+ </div>
707
+ <input id="node-search" type="text" placeholder="Filter nodes..." autocomplete="off">
708
+ <div id="node-list"></div>
709
+ <div id="query-controls">
710
+ <label>Direction</label>
711
+ <div class="radio-group">
712
+ <label><input type="radio" name="direction" value="upstream" checked> Upstream</label>
713
+ <label><input type="radio" name="direction" value="downstream"> Downstream</label>
714
+ <label><input type="radio" name="direction" value="both"> Both</label>
715
+ </div>
716
+ <label>Depth</label>
717
+ <div id="depth-row">
718
+ <input type="range" id="depth-slider" min="1" max="10" value="3">
719
+ <span id="depth-value">3</span>
720
+ </div>
721
+ </div>
722
+ </div>
723
+
724
+ <!-- Main -->
725
+ <div id="main">
726
+ <div id="main-header">
727
+ <h2 id="diagram-title">Select a node to explore lineage</h2>
728
+ <span id="main-stats"></span>
729
+ <div id="export-buttons" style="display:none">
730
+ <label style="font-size:12px;margin-right:8px;cursor:pointer">
731
+ <input type="radio" name="view-mode" value="flow" checked> Flow
732
+ </label>
733
+ <label style="font-size:12px;margin-right:12px;cursor:pointer">
734
+ <input type="radio" name="view-mode" value="er"> ER
735
+ </label>
736
+ <label style="font-size:12px;margin-right:12px;cursor:pointer">
737
+ <input type="checkbox" id="show-columns"> Columns
738
+ </label>
739
+ <button id="btn-mermaid">Download Mermaid</button>
740
+ <button id="btn-json">Download JSON</button>
741
+ <button id="btn-html">Download HTML</button>
742
+ </div>
743
+ </div>
744
+ <div id="legend" style="display:none;padding:4px 16px;font-size:11px;background:#f8f9fa;border-bottom:1px solid #e0e0e0;gap:16px;flex-wrap:wrap;align-items:center">
745
+ <span><span style="display:inline-block;width:12px;height:12px;background:#e1f5fe;border:2px solid #0288d1;border-radius:2px;vertical-align:middle"></span> Table</span>
746
+ <span><span style="display:inline-block;width:12px;height:12px;background:#e8f5e9;border:2px solid #388e3c;border-radius:2px;vertical-align:middle"></span> Configuration</span>
747
+ <span><span style="display:inline-block;width:12px;height:12px;background:#f3e5f5;border:2px solid #7b1fa2;border-radius:2px;vertical-align:middle"></span> Table from another project</span>
748
+ <span style="color:#888">Edges: input_mapping | output_mapping | sql_tokenizer | bucket_sharing | ai</span>
749
+ </div>
750
+ <div id="diagram-area">
751
+ <div id="placeholder">
752
+ <p>Choose a project and click a table or configuration to visualize its lineage.</p>
753
+ <p style="font-size:12px;color:#bbb">
754
+ Use the sidebar to browse nodes, then click to query upstream or downstream dependencies.
755
+ </p>
756
+ </div>
757
+ <div id="loading">Querying lineage...</div>
758
+ <div id="zoom-controls" style="display:none;position:absolute;top:60px;right:20px;z-index:10">
759
+ <button onclick="zoomDiagram(1.2)" title="Zoom in" style="padding:4px 10px;font-size:18px;cursor:pointer">+</button>
760
+ <button onclick="zoomDiagram(1/1.2)" title="Zoom out" style="padding:4px 10px;font-size:18px;cursor:pointer">&minus;</button>
761
+ <button onclick="zoomDiagram(0)" title="Reset zoom" style="padding:4px 10px;font-size:12px;cursor:pointer">Reset</button>
762
+ </div>
763
+ <div id="mermaid-output" class="mermaid-container" style="overflow:auto;transform-origin:top left"></div>
764
+ </div>
765
+ </div>
766
+
767
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
768
+ <script>
769
+ (function() {
770
+ // State
771
+ var allData = null; // full lineage data from /data.json
772
+ var edgeCounts = {}; // fqn -> number of connections
773
+ var currentTab = "tables";
774
+ var selectedProject = "";
775
+ var selectedNode = null; // FQN of the selected node
776
+ var lastQueryResult = null;
777
+ var lastMermaidCode = null;
778
+ var renderCounter = 0; // unique ID for mermaid renders
779
+
780
+ // DOM refs
781
+ var projectSelect = document.getElementById("project-select");
782
+ var nodeSearch = document.getElementById("node-search");
783
+ var nodeList = document.getElementById("node-list");
784
+ var depthSlider = document.getElementById("depth-slider");
785
+ var depthValue = document.getElementById("depth-value");
786
+ var diagramTitle = document.getElementById("diagram-title");
787
+ var mainStats = document.getElementById("main-stats");
788
+ var exportBtns = document.getElementById("export-buttons");
789
+ var mermaidOutput = document.getElementById("mermaid-output");
790
+ var placeholder = document.getElementById("placeholder");
791
+ var loading = document.getElementById("loading");
792
+
793
+ // Initialize mermaid
794
+ mermaid.initialize({
795
+ startOnLoad: false,
796
+ theme: "default",
797
+ flowchart: { useMaxWidth: false, htmlLabels: true },
798
+ securityLevel: "loose"
799
+ });
800
+
801
+ // -- Data loading --
802
+ fetch("/data.json")
803
+ .then(function(r) { return r.json(); })
804
+ .then(function(data) {
805
+ allData = data;
806
+ // Pre-compute edge counts per node
807
+ edgeCounts = {};
808
+ (data.edges || []).forEach(function(e) {
809
+ edgeCounts[e.source_fqn] = (edgeCounts[e.source_fqn] || 0) + 1;
810
+ edgeCounts[e.target_fqn] = (edgeCounts[e.target_fqn] || 0) + 1;
811
+ });
812
+ populateProjects();
813
+ })
814
+ .catch(function(err) {
815
+ placeholder.innerHTML = "<p style='color:#d93025'>Failed to load lineage data: " +
816
+ err.message + "</p>";
817
+ });
818
+
819
+ function populateProjects() {
820
+ var projects = {};
821
+ var tables = allData.tables || {};
822
+ var configs = allData.configurations || {};
823
+ for (var fqn in tables) {
824
+ var pa = tables[fqn].project_alias || fqn.split(":")[0] || "";
825
+ if (pa) projects[pa] = true;
826
+ }
827
+ for (var cfqn in configs) {
828
+ var cpa = configs[cfqn].project_alias || cfqn.split(":")[0] || "";
829
+ if (cpa) projects[cpa] = true;
830
+ }
831
+ var sorted = Object.keys(projects).sort();
832
+ projectSelect.innerHTML = '<option value="">-- Select project --</option>';
833
+ for (var i = 0; i < sorted.length; i++) {
834
+ var opt = document.createElement("option");
835
+ opt.value = sorted[i];
836
+ opt.textContent = sorted[i];
837
+ projectSelect.appendChild(opt);
838
+ }
839
+ // Auto-select first project if only one
840
+ if (sorted.length === 1) {
841
+ projectSelect.value = sorted[0];
842
+ selectedProject = sorted[0];
843
+ renderNodeList();
844
+ }
845
+ }
846
+
847
+ // -- Event handlers --
848
+ projectSelect.addEventListener("change", function() {
849
+ selectedProject = this.value;
850
+ selectedNode = null;
851
+ nodeSearch.value = "";
852
+ renderNodeList();
853
+ });
854
+
855
+ document.querySelectorAll(".tab").forEach(function(tab) {
856
+ tab.addEventListener("click", function() {
857
+ document.querySelectorAll(".tab").forEach(function(t) { t.classList.remove("active"); });
858
+ tab.classList.add("active");
859
+ currentTab = tab.getAttribute("data-type");
860
+ selectedNode = null;
861
+ renderNodeList();
862
+ });
863
+ });
864
+
865
+ nodeSearch.addEventListener("input", function() {
866
+ renderNodeList();
867
+ });
868
+
869
+ depthSlider.addEventListener("input", function() {
870
+ depthValue.textContent = this.value;
871
+ });
872
+
873
+ // Re-query when direction or depth changes (if a node is selected)
874
+ document.querySelectorAll('input[name="direction"]').forEach(function(radio) {
875
+ radio.addEventListener("change", function() {
876
+ if (selectedNode) queryNode(selectedNode);
877
+ });
878
+ });
879
+ depthSlider.addEventListener("change", function() {
880
+ if (selectedNode) queryNode(selectedNode);
881
+ });
882
+
883
+ // Export buttons
884
+ document.getElementById("btn-mermaid").addEventListener("click", function() {
885
+ if (lastMermaidCode) downloadFile("lineage.mmd", lastMermaidCode, "text/plain");
886
+ });
887
+ document.getElementById("btn-json").addEventListener("click", function() {
888
+ if (lastQueryResult) {
889
+ downloadFile("lineage.json", JSON.stringify(lastQueryResult, null, 2), "application/json");
890
+ }
891
+ });
892
+ document.getElementById("btn-html").addEventListener("click", function() {
893
+ if (lastMermaidCode) {
894
+ var html = buildStandaloneHtml(lastMermaidCode, diagramTitle.textContent);
895
+ downloadFile("lineage.html", html, "text/html");
896
+ }
897
+ });
898
+ document.getElementById("show-columns").addEventListener("change", function() {
899
+ if (selectedNode) queryNode(selectedNode);
900
+ });
901
+ document.querySelectorAll('input[name="view-mode"]').forEach(function(r) {
902
+ r.addEventListener("change", function() { if (selectedNode) queryNode(selectedNode); });
903
+ });
904
+
905
+ // -- Render node list (grouped by bucket / component type) --
906
+ function renderNodeList() {
907
+ nodeList.innerHTML = "";
908
+ if (!allData || !selectedProject) return;
909
+
910
+ var query = (nodeSearch.value || "").toLowerCase().trim();
911
+ // groups: { groupName: [items] }
912
+ var groups = {};
913
+
914
+ if (currentTab === "tables") {
915
+ var tables = allData.tables || {};
916
+ for (var fqn in tables) {
917
+ var t = tables[fqn];
918
+ var pa = t.project_alias || fqn.split(":")[0] || "";
919
+ if (pa !== selectedProject) continue;
920
+ var tableId = t.table_id || fqn.split(":").slice(1).join(":") || fqn;
921
+ if (query && tableId.toLowerCase().indexOf(query) < 0) continue;
922
+ var colCount = Array.isArray(t.columns) ? t.columns.length : (t.columns || 0);
923
+ var ec = edgeCounts[fqn] || 0;
924
+ // Group by bucket: "in.c-bucket" or "out.c-bucket"
925
+ var bucketId = t.bucket_id || tableId.split(".").slice(0, -1).join(".") || "other";
926
+ var tableName = t.name || tableId.split(".").pop() || tableId;
927
+ if (!groups[bucketId]) groups[bucketId] = [];
928
+ groups[bucketId].push({
929
+ fqn: fqn, name: tableName, edges: ec,
930
+ meta: ec + " edges, " + colCount + " cols, " + (t.rows_count || 0).toLocaleString() + " rows"
931
+ });
932
+ }
933
+ } else {
934
+ var configs = allData.configurations || {};
935
+ for (var cfqn in configs) {
936
+ var c = configs[cfqn];
937
+ var cpa = c.project_alias || cfqn.split(":")[0] || "";
938
+ if (cpa !== selectedProject) continue;
939
+ var configName = c.config_name || c.name || cfqn;
940
+ if (query && configName.toLowerCase().indexOf(query) < 0 &&
941
+ (c.component_id || "").toLowerCase().indexOf(query) < 0) continue;
942
+ var cec = edgeCounts[cfqn] || 0;
943
+ // Group by component_id
944
+ var compId = c.component_id || "other";
945
+ if (!groups[compId]) groups[compId] = [];
946
+ groups[compId].push({
947
+ fqn: cfqn, name: configName, edges: cec,
948
+ meta: cec + " edges"
949
+ });
950
+ }
951
+ }
952
+
953
+ // Sort groups: by total edges in group descending
954
+ var groupNames = Object.keys(groups);
955
+ groupNames.sort(function(a, b) {
956
+ var sumA = groups[a].reduce(function(s, i) { return s + i.edges; }, 0);
957
+ var sumB = groups[b].reduce(function(s, i) { return s + i.edges; }, 0);
958
+ return sumB - sumA || a.localeCompare(b);
959
+ });
960
+
961
+ for (var g = 0; g < groupNames.length; g++) {
962
+ var gName = groupNames[g];
963
+ var gItems = groups[gName];
964
+ gItems.sort(function(a, b) { return (b.edges || 0) - (a.edges || 0) || a.name.localeCompare(b.name); });
965
+
966
+ // Foldable group header
967
+ var totalEdges = gItems.reduce(function(s, i) { return s + i.edges; }, 0);
968
+ var header = document.createElement("div");
969
+ header.style.cssText = "padding:8px 8px;font-size:11px;font-weight:600;color:#1a73e8;" +
970
+ "background:#e8f0fe;border-bottom:1px solid #d2e3fc;cursor:pointer;user-select:none;" +
971
+ "display:flex;justify-content:space-between;align-items:center";
972
+ header.innerHTML = '<span>\u25BC ' + escapeHtml(gName) + '</span>' +
973
+ '<span style="color:#5f6368;font-weight:400">' + gItems.length + ', ' + totalEdges + ' edges</span>';
974
+ var groupContainer = document.createElement("div");
975
+ header.addEventListener("click", (function(container, hdr) {
976
+ return function() {
977
+ var hidden = container.style.display === "none";
978
+ container.style.display = hidden ? "block" : "none";
979
+ hdr.querySelector("span").textContent = (hidden ? "\u25BC " : "\u25B6 ") +
980
+ hdr.querySelector("span").textContent.substring(2);
981
+ };
982
+ })(groupContainer, header));
983
+ nodeList.appendChild(header);
984
+
985
+ for (var i = 0; i < gItems.length; i++) {
986
+ var item = gItems[i];
987
+ var div = document.createElement("div");
988
+ div.className = "node-item" + (item.fqn === selectedNode ? " selected" : "");
989
+ div.setAttribute("data-fqn", item.fqn);
990
+ div.innerHTML = '<div>' + escapeHtml(item.name) + '</div>' +
991
+ '<div class="node-meta">' + escapeHtml(item.meta) + '</div>';
992
+ div.addEventListener("click", (function(fqn) {
993
+ return function() { onNodeClick(fqn); };
994
+ })(item.fqn));
995
+ groupContainer.appendChild(div);
996
+ }
997
+ nodeList.appendChild(groupContainer);
998
+ }
999
+ }
1000
+
1001
+ function onNodeClick(fqn) {
1002
+ selectedNode = fqn;
1003
+ // Update selection highlight
1004
+ document.querySelectorAll(".node-item").forEach(function(el) {
1005
+ el.classList.toggle("selected", el.getAttribute("data-fqn") === fqn);
1006
+ });
1007
+ queryNode(fqn);
1008
+ }
1009
+
1010
+ // -- Query API --
1011
+ function getDirection() {
1012
+ var radios = document.querySelectorAll('input[name="direction"]');
1013
+ for (var i = 0; i < radios.length; i++) {
1014
+ if (radios[i].checked) return radios[i].value;
1015
+ }
1016
+ return "upstream";
1017
+ }
1018
+
1019
+ function getDepth() {
1020
+ return parseInt(depthSlider.value, 10) || 3;
1021
+ }
1022
+
1023
+ function queryNode(fqn) {
1024
+ var direction = getDirection();
1025
+ var depth = getDepth();
1026
+
1027
+ var showCols = document.getElementById("show-columns").checked;
1028
+ var viewMode = document.querySelector('input[name="view-mode"]:checked').value;
1029
+ var cp = (showCols ? "&columns=true" : "") + (viewMode === "er" ? "&view=er" : "");
1030
+
1031
+ placeholder.style.display = "none";
1032
+ mermaidOutput.innerHTML = "";
1033
+ loading.style.display = "block";
1034
+ exportBtns.style.display = "none";
1035
+ mainStats.textContent = "";
1036
+
1037
+ if (direction === "both") {
1038
+ // Fetch both directions and merge
1039
+ diagramTitle.textContent = "Both directions of " + fqn + ", depth " + depth;
1040
+ var enc = encodeURIComponent(fqn);
1041
+ Promise.all([
1042
+ fetch("/api/query?node=" + enc + "&direction=upstream&depth=" + depth).then(function(r){return r.json();}),
1043
+ fetch("/api/query?node=" + enc + "&direction=downstream&depth=" + depth).then(function(r){return r.json();}),
1044
+ fetch("/api/mermaid?node=" + enc + "&direction=upstream&depth=" + depth + cp).then(function(r){return r.text();}),
1045
+ fetch("/api/mermaid?node=" + enc + "&direction=downstream&depth=" + depth + cp).then(function(r){return r.text();})
1046
+ ]).then(function(res) {
1047
+ loading.style.display = "none";
1048
+ var upQ = res[0], downQ = res[1], upM = res[2], downM = res[3];
1049
+ var allEdges = (upQ.edges || []).concat(downQ.edges || []);
1050
+ var merged = {node: fqn, edges: allEdges};
1051
+ // Merge mermaid from both directions
1052
+ var mermaidCode;
1053
+ if (viewMode === "er") {
1054
+ // ER: merge entity/relationship lines, deduplicate
1055
+ var allL = (upM + "\n" + downM).split("\n");
1056
+ var seenER = {}; var erL = ["erDiagram"];
1057
+ allL.forEach(function(l) {
1058
+ var t = l.trim();
1059
+ if (!t || t === "erDiagram") return;
1060
+ if (!seenER[t]) { seenER[t] = true; erL.push(l); }
1061
+ });
1062
+ mermaidCode = erL.join("\n");
1063
+ } else {
1064
+ // Flowchart: merge node/edge lines, deduplicate
1065
+ var upLines = upM.split("\n"); var downLines = downM.split("\n");
1066
+ var seen = {}; upLines.forEach(function(l){seen[l.trim()]=true;});
1067
+ var extra = downLines.filter(function(l){return l.trim() && !seen[l.trim()] && !l.trim().startsWith("graph ") && !l.trim().startsWith("classDef ");});
1068
+ var combined = upLines.slice(0,-2).concat(extra).concat(upLines.slice(-2));
1069
+ combined[0] = "graph LR";
1070
+ mermaidCode = combined.join("\n");
1071
+ }
1072
+ lastQueryResult = merged; lastMermaidCode = mermaidCode;
1073
+ var nodeSet = {}; if(merged.node) nodeSet[merged.node]=true;
1074
+ allEdges.forEach(function(e){nodeSet[e.source]=true;nodeSet[e.target]=true;});
1075
+ mainStats.textContent = Object.keys(nodeSet).length+" nodes, "+allEdges.length+" edges";
1076
+ exportBtns.style.display = "flex";
1077
+ if(allEdges.length===0){mermaidOutput.innerHTML='<p style="color:#888;padding:20px">No dependencies found in either direction.</p>';return;}
1078
+ buildIdMap(merged); renderMermaid(mermaidCode);
1079
+ });
1080
+ return;
1081
+ }
1082
+
1083
+ diagramTitle.textContent = direction.charAt(0).toUpperCase() + direction.slice(1) +
1084
+ " of " + fqn + ", depth " + depth;
1085
+
1086
+ var queryUrl = "/api/query?node=" + encodeURIComponent(fqn) +
1087
+ "&direction=" + direction + "&depth=" + depth;
1088
+ var mermaidUrl = "/api/mermaid?node=" + encodeURIComponent(fqn) +
1089
+ "&direction=" + direction + "&depth=" + depth + cp;
1090
+
1091
+ Promise.all([
1092
+ fetch(queryUrl).then(function(r) { return r.json(); }),
1093
+ fetch(mermaidUrl).then(function(r) { return r.text(); })
1094
+ ]).then(function(results) {
1095
+ var queryResult = results[0];
1096
+ var mermaidCode = results[1];
1097
+ loading.style.display = "none";
1098
+
1099
+ if (queryResult.error) {
1100
+ mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">' +
1101
+ escapeHtml(queryResult.error) + '</p>';
1102
+ return;
1103
+ }
1104
+
1105
+ lastQueryResult = queryResult;
1106
+ lastMermaidCode = mermaidCode;
1107
+
1108
+ var edges = queryResult.edges || [];
1109
+ var nodeSet = {};
1110
+ if (queryResult.node) nodeSet[queryResult.node] = true;
1111
+ for (var i = 0; i < edges.length; i++) {
1112
+ nodeSet[edges[i].source] = true;
1113
+ nodeSet[edges[i].target] = true;
1114
+ }
1115
+ var nodeCount = Object.keys(nodeSet).length;
1116
+ mainStats.textContent = nodeCount + " nodes, " + edges.length + " edges";
1117
+ exportBtns.style.display = "flex";
1118
+
1119
+ if (edges.length === 0) {
1120
+ var opposite = direction === "upstream" ? "downstream" : "upstream";
1121
+ mermaidOutput.innerHTML = '<p style="color:#888;padding:20px">No ' +
1122
+ direction + ' dependencies found.</p>' +
1123
+ '<p style="padding:0 20px"><a href="#" style="color:#1a73e8" onclick="' +
1124
+ "document.querySelector('input[name=direction][value=" + opposite + "]').checked=true;" +
1125
+ "document.querySelector('.node-item.selected').click();return false;" +
1126
+ '">Try ' + opposite + ' direction</a> ' +
1127
+ 'or <a href="#" style="color:#1a73e8" onclick="' +
1128
+ "document.querySelector('input[name=direction][value=both]').checked=true;" +
1129
+ "document.querySelector('.node-item.selected').click();return false;" +
1130
+ '">show both directions</a></p>';
1131
+ return;
1132
+ }
1133
+
1134
+ // Build ID map for click traversal, then render
1135
+ buildIdMap(queryResult);
1136
+ renderMermaid(mermaidCode);
1137
+ }).catch(function(err) {
1138
+ loading.style.display = "none";
1139
+ mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">Query failed: ' +
1140
+ escapeHtml(err.message) + '</p>';
1141
+ });
1142
+ }
1143
+
1144
+ // Map sanitized mermaid node IDs back to FQNs for click traversal
1145
+ var lastIdToFqn = {};
1146
+
1147
+ function sanitizeFqn(fqn) {
1148
+ return fqn.replace(/[^a-zA-Z0-9_]/g, "_");
1149
+ }
1150
+
1151
+ function buildIdMap(queryResult) {
1152
+ lastIdToFqn = {};
1153
+ if (!queryResult || !queryResult.edges) return;
1154
+ var fqns = {};
1155
+ if (queryResult.node) fqns[queryResult.node] = true;
1156
+ queryResult.edges.forEach(function(e) {
1157
+ fqns[e.source] = true;
1158
+ fqns[e.target] = true;
1159
+ });
1160
+ for (var fqn in fqns) {
1161
+ lastIdToFqn[sanitizeFqn(fqn)] = fqn;
1162
+ }
1163
+ }
1164
+
1165
+ function attachDiagramClickHandlers() {
1166
+ var nodes = mermaidOutput.querySelectorAll(".node");
1167
+ nodes.forEach(function(el) {
1168
+ el.style.cursor = "pointer";
1169
+ el.addEventListener("click", function() {
1170
+ // Extract the mermaid node ID from the element
1171
+ var nodeId = el.id || "";
1172
+ // Mermaid wraps IDs: "flowchart-{id}-{n}" or just the id
1173
+ for (var sid in lastIdToFqn) {
1174
+ if (nodeId.indexOf(sid) >= 0) {
1175
+ var fqn = lastIdToFqn[sid];
1176
+ // Navigate to this node
1177
+ selectedNode = fqn;
1178
+ queryNode(fqn);
1179
+ // Update sidebar selection
1180
+ var proj = fqn.split(":")[0] || "";
1181
+ if (proj !== selectedProject) {
1182
+ projectSelect.value = proj;
1183
+ selectedProject = proj;
1184
+ renderNodeList();
1185
+ }
1186
+ document.querySelectorAll(".node-item").forEach(function(item) {
1187
+ item.classList.toggle("selected", item.getAttribute("data-fqn") === fqn);
1188
+ });
1189
+ break;
1190
+ }
1191
+ }
1192
+ });
1193
+ });
1194
+ }
1195
+
1196
+ function renderMermaid(code) {
1197
+ renderCounter++;
1198
+ var id = "mermaid-diagram-" + renderCounter;
1199
+ mermaidOutput.innerHTML = "";
1200
+ mermaid.render(id, code).then(function(result) {
1201
+ mermaidOutput.innerHTML = result.svg;
1202
+ // Fit SVG to fill the diagram area
1203
+ var svg = mermaidOutput.querySelector("svg");
1204
+ if (svg) {
1205
+ var area = document.getElementById("diagram-area");
1206
+ var w = area.clientWidth - 20;
1207
+ var h = area.clientHeight - 20;
1208
+ svg.setAttribute("width", w);
1209
+ svg.setAttribute("height", h);
1210
+ svg.style.display = "block";
1211
+ }
1212
+ currentZoom = 1;
1213
+ document.getElementById("zoom-controls").style.display = "block";
1214
+ document.getElementById("legend").style.display = "flex";
1215
+ attachDiagramClickHandlers();
1216
+ }).catch(function(err) {
1217
+ mermaidOutput.innerHTML = '<p style="color:#d93025;padding:20px">' +
1218
+ 'Mermaid render error: ' + escapeHtml(err.message) + '</p>' +
1219
+ '<pre style="padding:12px;background:#f5f5f5;border-radius:4px;' +
1220
+ 'font-size:11px;overflow:auto;max-height:300px">' +
1221
+ escapeHtml(code) + '</pre>';
1222
+ });
1223
+ }
1224
+
1225
+ // -- Helpers --
1226
+ var currentZoom = 1;
1227
+ window.zoomDiagram = function(factor) {
1228
+ if (factor === 0) { currentZoom = 1; } else { currentZoom *= factor; }
1229
+ currentZoom = Math.max(0.2, Math.min(3, currentZoom));
1230
+ var svg = mermaidOutput.querySelector("svg");
1231
+ if (svg) { svg.style.transform = "scale(" + currentZoom + ")"; svg.style.transformOrigin = "top left"; }
1232
+ };
1233
+
1234
+ function escapeHtml(text) {
1235
+ var div = document.createElement("div");
1236
+ div.appendChild(document.createTextNode(text || ""));
1237
+ return div.innerHTML;
1238
+ }
1239
+
1240
+ function downloadFile(filename, content, mimeType) {
1241
+ var blob = new Blob([content], { type: mimeType });
1242
+ var url = URL.createObjectURL(blob);
1243
+ var a = document.createElement("a");
1244
+ a.href = url;
1245
+ a.download = filename;
1246
+ document.body.appendChild(a);
1247
+ a.click();
1248
+ document.body.removeChild(a);
1249
+ URL.revokeObjectURL(url);
1250
+ }
1251
+
1252
+ function buildStandaloneHtml(mermaidCode, title) {
1253
+ return '<!DOCTYPE html>\n<html>\n<head>\n' +
1254
+ ' <title>' + escapeHtml(title) + '</title>\n' +
1255
+ ' <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"><' + '/script>\n' +
1256
+ ' <style>\n' +
1257
+ ' body { font-family: system-ui, -apple-system, sans-serif;\n' +
1258
+ ' max-width: 100%; padding: 20px; color: #333; }\n' +
1259
+ ' h2 { margin-bottom: 12px; }\n' +
1260
+ ' .mermaid { text-align: center; margin-top: 16px; }\n' +
1261
+ ' .legend { margin: 16px 0; padding: 12px 16px; background: #f5f5f5;\n' +
1262
+ ' border-radius: 8px; font-size: 13px; display: inline-block; }\n' +
1263
+ ' .legend-swatch { display: inline-block; width: 14px; height: 14px;\n' +
1264
+ ' border-radius: 3px; vertical-align: middle; margin-right: 4px; }\n' +
1265
+ ' </style>\n</head>\n<body>\n' +
1266
+ ' <h2>' + escapeHtml(title) + '</h2>\n' +
1267
+ ' <div class="legend">\n' +
1268
+ ' <strong>Legend</strong><br/>\n' +
1269
+ ' <span class="legend-swatch" style="background:#e1f5fe;border:2px solid #0288d1"></span> Table\n' +
1270
+ ' &nbsp;&nbsp;\n' +
1271
+ ' <span class="legend-swatch" style="background:#e8f5e9;border:2px solid #388e3c"></span> Configuration\n' +
1272
+ ' <br/><span style="color:#888;font-size:12px">' +
1273
+ ' Edge labels: input_mapping / output_mapping | sql_tokenizer | bucket_sharing | ai</span>\n' +
1274
+ ' </div>\n' +
1275
+ ' <div class="mermaid">\n' + mermaidCode + '\n </div>\n' +
1276
+ ' <script>mermaid.initialize({startOnLoad:true, theme:"default", ' +
1277
+ 'flowchart:{useMaxWidth:false,htmlLabels:true}, securityLevel:"loose"});<' + '/script>\n' +
1278
+ '</body>\n</html>';
1279
+ }
1280
+ })();
1281
+ </script>
1282
+ </body>
1283
+ </html>"""
1284
+
1285
+
1286
+ class _LineageHandler(http.server.BaseHTTPRequestHandler):
1287
+ """HTTP handler for the lineage browser - serves HTML, data JSON, and query APIs."""
1288
+
1289
+ html_content: str = ""
1290
+ json_content: str = ""
1291
+ service: DeepLineageService | None = None
1292
+ graph: LineageGraph | None = None
1293
+
1294
+ def do_GET(self) -> None:
1295
+ parsed = urlparse(self.path)
1296
+ path = parsed.path
1297
+
1298
+ if path == "/" or path == "/index.html":
1299
+ self._serve(self.html_content, "text/html")
1300
+ elif path == "/data.json":
1301
+ self._serve(self.json_content, "application/json")
1302
+ elif path == "/api/query":
1303
+ self._handle_query(parsed)
1304
+ elif path == "/api/mermaid":
1305
+ self._handle_mermaid(parsed)
1306
+ else:
1307
+ self.send_error(404)
1308
+
1309
+ def _handle_query(self, parsed) -> None:
1310
+ """Handle /api/query?node=FQN&direction=upstream&depth=3."""
1311
+ params = parse_qs(parsed.query)
1312
+ node = params.get("node", [""])[0]
1313
+ direction = params.get("direction", ["downstream"])[0]
1314
+ depth = int(params.get("depth", ["3"])[0])
1315
+
1316
+ if not node:
1317
+ self._serve(json.dumps({"error": "Missing 'node' parameter"}), "application/json")
1318
+ return
1319
+
1320
+ assert self.service is not None and self.graph is not None
1321
+ if direction == "upstream":
1322
+ result = self.service.query_upstream(self.graph, node, depth=depth)
1323
+ else:
1324
+ result = self.service.query_downstream(self.graph, node, depth=depth)
1325
+
1326
+ self._serve(json.dumps(result), "application/json")
1327
+
1328
+ def _handle_mermaid(self, parsed) -> None:
1329
+ """Handle /api/mermaid?node=FQN&direction=upstream&depth=3."""
1330
+ from ..services.deep_lineage_service import DeepLineageService
1331
+
1332
+ params = parse_qs(parsed.query)
1333
+ node = params.get("node", [""])[0]
1334
+ direction = params.get("direction", ["downstream"])[0]
1335
+ depth = int(params.get("depth", ["3"])[0])
1336
+
1337
+ if not node:
1338
+ self._serve("graph LR\n empty[No node specified]", "text/plain")
1339
+ return
1340
+
1341
+ assert self.service is not None and self.graph is not None
1342
+ if direction == "upstream":
1343
+ result = self.service.query_upstream(self.graph, node, depth=depth)
1344
+ else:
1345
+ result = self.service.query_downstream(self.graph, node, depth=depth)
1346
+
1347
+ if "error" in result:
1348
+ self._serve(
1349
+ "graph LR\n error[" + result["error"].replace('"', "'") + "]", "text/plain"
1350
+ )
1351
+ return
1352
+
1353
+ edges = result.get("edges", [])
1354
+ view = params.get("view", ["flow"])[0]
1355
+ show_cols = params.get("columns", [""])[0] == "true"
1356
+
1357
+ if view == "er":
1358
+ mermaid_code = DeepLineageService.render_er_diagram(
1359
+ edges,
1360
+ self.graph,
1361
+ node,
1362
+ show_columns=show_cols,
1363
+ )
1364
+ else:
1365
+ mermaid_code = DeepLineageService.render_mermaid(
1366
+ edges,
1367
+ self.graph,
1368
+ direction,
1369
+ node,
1370
+ show_columns=show_cols,
1371
+ )
1372
+ self._serve(mermaid_code, "text/plain")
1373
+
1374
+ def _serve(self, content: str, content_type: str) -> None:
1375
+ encoded = content.encode("utf-8")
1376
+ self.send_response(200)
1377
+ self.send_header("Content-Type", f"{content_type}; charset=utf-8")
1378
+ self.send_header("Content-Length", str(len(encoded)))
1379
+ self.send_header("Cache-Control", "no-cache")
1380
+ self.end_headers()
1381
+ self.wfile.write(encoded)
1382
+
1383
+ def log_message(self, format: str, *args: object) -> None:
1384
+ """Silence default stderr logging."""
1385
+
1386
+
1387
+ @lineage_app.command("server")
1388
+ def lineage_serve(
1389
+ ctx: typer.Context,
1390
+ load: Path = typer.Option(
1391
+ ...,
1392
+ "--load",
1393
+ "-l",
1394
+ help="Lineage JSON cache file (from `lineage build`).",
1395
+ ),
1396
+ port: int = typer.Option(
1397
+ 8088,
1398
+ "--port",
1399
+ help="Port to serve on.",
1400
+ ),
1401
+ host: str = typer.Option(
1402
+ "127.0.0.1",
1403
+ "--host",
1404
+ help="Host to bind to.",
1405
+ ),
1406
+ ) -> None:
1407
+ """Start a local web server with interactive lineage browser.
1408
+
1409
+ Serves an interactive lineage browser from a cached lineage file.
1410
+ Browse projects, tables, and configurations in the sidebar, then
1411
+ click a node to query and visualize its upstream or downstream
1412
+ dependencies as a mermaid diagram.
1413
+
1414
+ Example:
1415
+
1416
+ kbagent lineage server -l lineage.json
1417
+ kbagent lineage server -l lineage.json --port 9000
1418
+ """
1419
+ formatter = get_formatter(ctx)
1420
+
1421
+ if not load.exists():
1422
+ formatter.error(
1423
+ message=f"Cache file not found: {load}", error_code=ErrorCode.FILE_NOT_FOUND
1424
+ )
1425
+ raise typer.Exit(code=1)
1426
+
1427
+ try:
1428
+ raw_data = json.loads(load.read_text(encoding="utf-8"))
1429
+ except (json.JSONDecodeError, OSError) as exc:
1430
+ formatter.error(message=f"Cannot read lineage file: {exc}", error_code=ErrorCode.READ_ERROR)
1431
+ raise typer.Exit(code=1) from None
1432
+
1433
+ # Load the graph via the service for API queries
1434
+ service = get_service(ctx, "deep_lineage_service")
1435
+ graph = service.load_from_cache(load)
1436
+
1437
+ # Attach content + service/graph to the handler class
1438
+ _LineageHandler.html_content = _LINEAGE_HTML_TEMPLATE
1439
+ _LineageHandler.json_content = json.dumps(raw_data)
1440
+ _LineageHandler.service = service
1441
+ _LineageHandler.graph = graph
1442
+
1443
+ server = http.server.HTTPServer((host, port), _LineageHandler)
1444
+ url = f"http://{host}:{port}"
1445
+
1446
+ if formatter.json_mode:
1447
+ formatter.output({"url": url, "host": host, "port": port})
1448
+ else:
1449
+ formatter.console.print("\n[bold]Lineage browser server[/bold]")
1450
+ formatter.console.print(f" URL: {url}")
1451
+ formatter.console.print(f" Data: {load.resolve()}")
1452
+ formatter.console.print(" Press Ctrl+C to stop.\n")
1453
+
1454
+ # Open browser in a separate thread to avoid blocking
1455
+ threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
1456
+
1457
+ try:
1458
+ server.serve_forever()
1459
+ except KeyboardInterrupt:
1460
+ pass
1461
+ finally:
1462
+ server.server_close()
1463
+ if not formatter.json_mode:
1464
+ formatter.console.print("\nServer stopped.")