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.
- keboola_agent_cli/__init__.py +34 -0
- keboola_agent_cli/__main__.py +5 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/arc-DhFYIddx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/architecture-7EHR7CIX-hNCijx_H.js +1 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js +37 -0
- keboola_agent_cli/_ui_dist/assets/architectureDiagram-3BPJPVTR-C6hUlprM.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js +2 -0
- keboola_agent_cli/_ui_dist/assets/array-BifhSqXX.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js +133 -0
- keboola_agent_cli/_ui_dist/assets/blockDiagram-GPEHLZMM-DC7qY9i4.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js +11 -0
- keboola_agent_cli/_ui_dist/assets/c4Diagram-AAUBKEIU-5Lh44evt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js +2 -0
- keboola_agent_cli/_ui_dist/assets/channel-DBMrXlxx.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-2J33WTMH-Coy82EBh.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js +63 -0
- keboola_agent_cli/_ui_dist/assets/chunk-3OPIFGDE-BQC5CRHI.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-4BX2VUAB-DUuEt70o.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-55IACEB6-BvR-6chF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js +3 -0
- keboola_agent_cli/_ui_dist/assets/chunk-5ZQYHXKU-BjcTN7ul.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js +207 -0
- keboola_agent_cli/_ui_dist/assets/chunk-727SXJPM-C0zxqqRN.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js +232 -0
- keboola_agent_cli/_ui_dist/assets/chunk-AQP2D5EJ-CXf7rIlZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-BSJP7CBP-Oj_FO9Q7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js +124 -0
- keboola_agent_cli/_ui_dist/assets/chunk-CSCIHK7Q-CcTsLrFc.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js +16 -0
- keboola_agent_cli/_ui_dist/assets/chunk-FMBD7UC4-FH-zLkkW.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-L5ZTLDWV-B1Ky_e7O.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-ND2GUHAM-BHz1rpbm.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js +160 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NNHCCRGN-DlpIbxXb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-NZK2D7GU-tnrSoegS.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js +71 -0
- keboola_agent_cli/_ui_dist/assets/chunk-O5CBEL6O-DxxqDH0l.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js +2 -0
- keboola_agent_cli/_ui_dist/assets/chunk-QZHKN3VN-CSjc2gjj.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-4FO5ZUOK-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js +2 -0
- keboola_agent_cli/_ui_dist/assets/classDiagram-v2-Q7XG4LA2-BuZcZu85.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js +2 -0
- keboola_agent_cli/_ui_dist/assets/cose-bilkent-S5V4N54A-Y0L8LDMa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js +322 -0
- keboola_agent_cli/_ui_dist/assets/cytoscape.esm-C8YCVR3_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js +5 -0
- keboola_agent_cli/_ui_dist/assets/dagre-BM42HDAG-UZ-9BTqF.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dagre-Bx709z4p.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js +2 -0
- keboola_agent_cli/_ui_dist/assets/defaultLocale-C8Fc0cco.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js +44 -0
- keboola_agent_cli/_ui_dist/assets/diagram-2AECGRRQ-DoDQ60wi.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js +11 -0
- keboola_agent_cli/_ui_dist/assets/diagram-5GNKFQAL-CMGFxpUs.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js +4 -0
- keboola_agent_cli/_ui_dist/assets/diagram-KO2AKTUF-1uGDa-Iu.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-LMA3HP47-XtFH7B51.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js +25 -0
- keboola_agent_cli/_ui_dist/assets/diagram-OG6HWLK6-B4_Te1T5.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/dist-Di6zmlv0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js +86 -0
- keboola_agent_cli/_ui_dist/assets/erDiagram-TEJ5UH35-NjQkrdFt.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/eventmodeling-FCH6USID-BrJMIks8.js +1 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js +163 -0
- keboola_agent_cli/_ui_dist/assets/flowDiagram-I6XJVG4X-CIr8DWl7.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js +293 -0
- keboola_agent_cli/_ui_dist/assets/ganttDiagram-6RSMTGT7-C1VY_xbQ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraph-WXDBUCRP-COacYjo-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js +107 -0
- keboola_agent_cli/_ui_dist/assets/gitGraphDiagram-PVQCEYII-DQT8-kg2.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js +2 -0
- keboola_agent_cli/_ui_dist/assets/graphlib-B8gBHxth.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/index-CMq50kkV.css +1 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js +118 -0
- keboola_agent_cli/_ui_dist/assets/index-D8W97DAz.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/info-J43DQDTF-DdCTRIzU.js +1 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js +3 -0
- keboola_agent_cli/_ui_dist/assets/infoDiagram-5YYISTIA-C77rsoTp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/init-D6jRqBbL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js +71 -0
- keboola_agent_cli/_ui_dist/assets/ishikawaDiagram-YF4QCWOH-BcTbXaLy.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js +140 -0
- keboola_agent_cli/_ui_dist/assets/journeyDiagram-JHISSGLW-BejeAJQ_.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js +90 -0
- keboola_agent_cli/_ui_dist/assets/kanban-definition-UN3LZRKU-BRNz_UrH.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js +258 -0
- keboola_agent_cli/_ui_dist/assets/katex-C4eR7coU.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js +2 -0
- keboola_agent_cli/_ui_dist/assets/line-CzAQKFbJ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js +2 -0
- keboola_agent_cli/_ui_dist/assets/linear-DUNFFdck.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js +5 -0
- keboola_agent_cli/_ui_dist/assets/mermaid-parser.core-CpuBOkFa.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js +97 -0
- keboola_agent_cli/_ui_dist/assets/mindmap-definition-RKZ34NQL-9EJQNjH0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js +2 -0
- keboola_agent_cli/_ui_dist/assets/ordinal-hYBb2elL.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/packet-YPE3B663-DLiiw_B2.js +1 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js +2 -0
- keboola_agent_cli/_ui_dist/assets/path-BWPyau1x.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/pie-LRSECV5Y-CRoO8G1g.js +1 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js +31 -0
- keboola_agent_cli/_ui_dist/assets/pieDiagram-4H26LBE5-XH4cy6Cb.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js +8 -0
- keboola_agent_cli/_ui_dist/assets/quadrantDiagram-W4KKPZXB-fdhc93U8.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/radar-GUYGQ44K-DAlLVJHm.js +1 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js +85 -0
- keboola_agent_cli/_ui_dist/assets/requirementDiagram-4Y6WPE33-a94eP3R9.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js +2 -0
- keboola_agent_cli/_ui_dist/assets/rough.esm-CSKSodPl.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js +41 -0
- keboola_agent_cli/_ui_dist/assets/sankeyDiagram-5OEKKPKP-jcBa02sp.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js +163 -0
- keboola_agent_cli/_ui_dist/assets/sequenceDiagram-3UESZ5HK-A5-GGM-e.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js +2 -0
- keboola_agent_cli/_ui_dist/assets/src-ZI-V_AF0.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-AJRCARHV-BKAA5rqE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js +2 -0
- keboola_agent_cli/_ui_dist/assets/stateDiagram-v2-BHNVJYJU-DnJwJBsE.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js +121 -0
- keboola_agent_cli/_ui_dist/assets/timeline-definition-PNZ67QCA-Cy39jp8b.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/treeView-BLDUP644-DbLYl23-.js +1 -0
- keboola_agent_cli/_ui_dist/assets/treemap-LRROVOQU-Bp0eGlOt.js +1 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js +35 -0
- keboola_agent_cli/_ui_dist/assets/vennDiagram-CIIHVFJN-BGECKubd.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/wardley-L42UT6IY-D4yH4jqS.js +1 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js +79 -0
- keboola_agent_cli/_ui_dist/assets/wardleyDiagram-YWT4CUSO-D6XRG3cZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js +8 -0
- keboola_agent_cli/_ui_dist/assets/xychartDiagram-2RQKCTM6-DRre-pfZ.js.map +1 -0
- keboola_agent_cli/_ui_dist/index.html +50 -0
- keboola_agent_cli/ai_client.py +83 -0
- keboola_agent_cli/auto_update.py +550 -0
- keboola_agent_cli/changelog.py +1198 -0
- keboola_agent_cli/cli.py +448 -0
- keboola_agent_cli/client.py +3422 -0
- keboola_agent_cli/commands/__init__.py +0 -0
- keboola_agent_cli/commands/_data_app_git.py +343 -0
- keboola_agent_cli/commands/_helpers.py +377 -0
- keboola_agent_cli/commands/_metadata_input.py +49 -0
- keboola_agent_cli/commands/_semantic_layer_crud.py +632 -0
- keboola_agent_cli/commands/_semantic_layer_helpers.py +44 -0
- keboola_agent_cli/commands/_semantic_layer_reference_data.py +247 -0
- keboola_agent_cli/commands/agent.py +968 -0
- keboola_agent_cli/commands/branch.py +423 -0
- keboola_agent_cli/commands/changelog.py +168 -0
- keboola_agent_cli/commands/component.py +216 -0
- keboola_agent_cli/commands/config.py +2442 -0
- keboola_agent_cli/commands/context.py +1481 -0
- keboola_agent_cli/commands/data_app.py +1279 -0
- keboola_agent_cli/commands/dev_portal.py +584 -0
- keboola_agent_cli/commands/doctor.py +37 -0
- keboola_agent_cli/commands/encrypt.py +145 -0
- keboola_agent_cli/commands/feature.py +311 -0
- keboola_agent_cli/commands/flow.py +948 -0
- keboola_agent_cli/commands/http_client.py +157 -0
- keboola_agent_cli/commands/init.py +279 -0
- keboola_agent_cli/commands/job.py +661 -0
- keboola_agent_cli/commands/kai.py +301 -0
- keboola_agent_cli/commands/lineage.py +1464 -0
- keboola_agent_cli/commands/org.py +292 -0
- keboola_agent_cli/commands/permissions.py +360 -0
- keboola_agent_cli/commands/project.py +1192 -0
- keboola_agent_cli/commands/repl.py +243 -0
- keboola_agent_cli/commands/schedule.py +340 -0
- keboola_agent_cli/commands/search.py +178 -0
- keboola_agent_cli/commands/semantic_layer.py +939 -0
- keboola_agent_cli/commands/serve.py +272 -0
- keboola_agent_cli/commands/sharing.py +340 -0
- keboola_agent_cli/commands/storage.py +2630 -0
- keboola_agent_cli/commands/stream.py +266 -0
- keboola_agent_cli/commands/sync.py +1277 -0
- keboola_agent_cli/commands/tool.py +206 -0
- keboola_agent_cli/commands/version.py +186 -0
- keboola_agent_cli/commands/workspace.py +635 -0
- keboola_agent_cli/config_store.py +582 -0
- keboola_agent_cli/constants.py +528 -0
- keboola_agent_cli/data_science_client.py +342 -0
- keboola_agent_cli/dev_portal_client.py +323 -0
- keboola_agent_cli/errors.py +248 -0
- keboola_agent_cli/http_base.py +315 -0
- keboola_agent_cli/json_utils.py +126 -0
- keboola_agent_cli/lib.py +536 -0
- keboola_agent_cli/manage_client.py +324 -0
- keboola_agent_cli/metastore_client.py +214 -0
- keboola_agent_cli/models.py +427 -0
- keboola_agent_cli/output.py +1084 -0
- keboola_agent_cli/permissions.py +469 -0
- keboola_agent_cli/py.typed +3 -0
- keboola_agent_cli/result_models.py +271 -0
- keboola_agent_cli/server/__init__.py +34 -0
- keboola_agent_cli/server/agent_runner.py +1289 -0
- keboola_agent_cli/server/agents_store.py +325 -0
- keboola_agent_cli/server/app.py +764 -0
- keboola_agent_cli/server/auth.py +117 -0
- keboola_agent_cli/server/dependencies.py +149 -0
- keboola_agent_cli/server/pricing.py +303 -0
- keboola_agent_cli/server/routers/__init__.py +1 -0
- keboola_agent_cli/server/routers/agents.py +616 -0
- keboola_agent_cli/server/routers/ai_chat.py +129 -0
- keboola_agent_cli/server/routers/branches.py +133 -0
- keboola_agent_cli/server/routers/components.py +48 -0
- keboola_agent_cli/server/routers/configs.py +507 -0
- keboola_agent_cli/server/routers/data_apps.py +384 -0
- keboola_agent_cli/server/routers/dev_portal.py +67 -0
- keboola_agent_cli/server/routers/encrypt.py +35 -0
- keboola_agent_cli/server/routers/feature.py +179 -0
- keboola_agent_cli/server/routers/flows.py +204 -0
- keboola_agent_cli/server/routers/health.py +53 -0
- keboola_agent_cli/server/routers/jobs.py +175 -0
- keboola_agent_cli/server/routers/kai.py +80 -0
- keboola_agent_cli/server/routers/lineage.py +226 -0
- keboola_agent_cli/server/routers/mcp.py +70 -0
- keboola_agent_cli/server/routers/members.py +170 -0
- keboola_agent_cli/server/routers/org.py +96 -0
- keboola_agent_cli/server/routers/projects.py +106 -0
- keboola_agent_cli/server/routers/schedules.py +54 -0
- keboola_agent_cli/server/routers/search.py +30 -0
- keboola_agent_cli/server/routers/semantic_layer.py +650 -0
- keboola_agent_cli/server/routers/sharing.py +86 -0
- keboola_agent_cli/server/routers/storage.py +574 -0
- keboola_agent_cli/server/routers/stream.py +100 -0
- keboola_agent_cli/server/routers/workspaces.py +302 -0
- keboola_agent_cli/server/run_broadcaster.py +329 -0
- keboola_agent_cli/server/sse.py +25 -0
- keboola_agent_cli/services/__init__.py +0 -0
- keboola_agent_cli/services/_encryption.py +217 -0
- keboola_agent_cli/services/_semantic_layer_cascade.py +147 -0
- keboola_agent_cli/services/_semantic_layer_crud.py +382 -0
- keboola_agent_cli/services/_semantic_layer_internals.py +1078 -0
- keboola_agent_cli/services/_semantic_layer_lookup.py +181 -0
- keboola_agent_cli/services/_semantic_layer_reference_data.py +217 -0
- keboola_agent_cli/services/_sync_bindings.py +456 -0
- keboola_agent_cli/services/_sync_branch.py +191 -0
- keboola_agent_cli/services/_sync_bulk.py +228 -0
- keboola_agent_cli/services/_sync_clone.py +163 -0
- keboola_agent_cli/services/_sync_models.py +97 -0
- keboola_agent_cli/services/_sync_push_ops.py +369 -0
- keboola_agent_cli/services/_sync_storage.py +376 -0
- keboola_agent_cli/services/_sync_writeback.py +167 -0
- keboola_agent_cli/services/agent_service.py +458 -0
- keboola_agent_cli/services/base.py +175 -0
- keboola_agent_cli/services/branch_service.py +588 -0
- keboola_agent_cli/services/component_service.py +694 -0
- keboola_agent_cli/services/config_service.py +2099 -0
- keboola_agent_cli/services/data_app_git_service.py +224 -0
- keboola_agent_cli/services/data_app_service.py +2082 -0
- keboola_agent_cli/services/deep_lineage_service.py +1322 -0
- keboola_agent_cli/services/dev_portal_service.py +345 -0
- keboola_agent_cli/services/doctor_service.py +445 -0
- keboola_agent_cli/services/encrypt_service.py +87 -0
- keboola_agent_cli/services/feature_service.py +268 -0
- keboola_agent_cli/services/flow_service.py +769 -0
- keboola_agent_cli/services/flow_validation.py +188 -0
- keboola_agent_cli/services/http_forwarder_service.py +236 -0
- keboola_agent_cli/services/job_idempotency_store.py +285 -0
- keboola_agent_cli/services/job_service.py +797 -0
- keboola_agent_cli/services/kai_service.py +367 -0
- keboola_agent_cli/services/lineage_service.py +274 -0
- keboola_agent_cli/services/mcp_service.py +1498 -0
- keboola_agent_cli/services/mcp_transport.py +259 -0
- keboola_agent_cli/services/member_service.py +593 -0
- keboola_agent_cli/services/org_service.py +619 -0
- keboola_agent_cli/services/project_service.py +947 -0
- keboola_agent_cli/services/repo_validate_service.py +767 -0
- keboola_agent_cli/services/schedule_service.py +731 -0
- keboola_agent_cli/services/search_service.py +331 -0
- keboola_agent_cli/services/semantic_layer_service.py +1497 -0
- keboola_agent_cli/services/sharing_service.py +307 -0
- keboola_agent_cli/services/storage_service.py +2524 -0
- keboola_agent_cli/services/stream_service.py +395 -0
- keboola_agent_cli/services/sync_service.py +2244 -0
- keboola_agent_cli/services/variables_service.py +447 -0
- keboola_agent_cli/services/version_service.py +1038 -0
- keboola_agent_cli/services/workspace_service.py +1103 -0
- keboola_agent_cli/stream_client.py +217 -0
- keboola_agent_cli/sync/__init__.py +1 -0
- keboola_agent_cli/sync/branch_mapping.py +174 -0
- keboola_agent_cli/sync/clone.py +211 -0
- keboola_agent_cli/sync/code_extraction.py +655 -0
- keboola_agent_cli/sync/config_format.py +290 -0
- keboola_agent_cli/sync/diff_engine.py +566 -0
- keboola_agent_cli/sync/git_utils.py +93 -0
- keboola_agent_cli/sync/manifest.py +162 -0
- keboola_agent_cli/sync/naming.py +90 -0
- keboola_agent_cli/sync/secrets.py +62 -0
- keboola_agent_cli/sync/sql_split.py +134 -0
- keboola_cli-0.63.4.dist-info/METADATA +308 -0
- keboola_cli-0.63.4.dist-info/RECORD +306 -0
- keboola_cli-0.63.4.dist-info/WHEEL +4 -0
- 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
|
+
]
|