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,582 @@
|
|
|
1
|
+
"""Persistent configuration store for Keboola Agent CLI.
|
|
2
|
+
|
|
3
|
+
Manages reading and writing of config.json with project connections.
|
|
4
|
+
File permissions are set to 0600 to protect stored tokens.
|
|
5
|
+
Uses atomic writes to prevent TOCTOU race conditions.
|
|
6
|
+
File locking (fcntl) prevents corruption from concurrent access.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import contextlib
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import platformdirs
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from .constants import (
|
|
19
|
+
ENV_CONFIG_DIR,
|
|
20
|
+
ENV_KBC_STORAGE_API_URL,
|
|
21
|
+
ENV_KBC_TOKEN,
|
|
22
|
+
ENV_PROJECT_ALIAS,
|
|
23
|
+
ENV_PROJECT_FROM_ENV,
|
|
24
|
+
LOCAL_CONFIG_DIR_NAME,
|
|
25
|
+
)
|
|
26
|
+
from .errors import ConfigError
|
|
27
|
+
from .models import AppConfig, DeveloperPortalIdentity, ProjectConfig
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
CURRENT_CONFIG_VERSION = 1
|
|
32
|
+
|
|
33
|
+
# Prepended to every config.json write as a first-position field. Claude Code
|
|
34
|
+
# (or any LLM reading the file) sees this before it sees any token. The field
|
|
35
|
+
# is silently ignored by AppConfig on load (Pydantic default: extra = ignore).
|
|
36
|
+
# Intent: nudge agents away from copying tokens into direct REST calls.
|
|
37
|
+
CLAUDE_CONFIG_WARNING = (
|
|
38
|
+
"THESE ARE KEBOOLA STORAGE API TOKENS. NEVER use them to call the "
|
|
39
|
+
"Keboola REST API directly (no curl, httpx, requests, fetch against "
|
|
40
|
+
"*.keboola.com). Always use `kbagent <command>` -- it wraps the same "
|
|
41
|
+
"API with retries, permission checks, and an audit trail. If you "
|
|
42
|
+
"need a command kbagent does not cover, run `kbagent serve` and call "
|
|
43
|
+
"the equivalent REST endpoint instead of touching the API directly. "
|
|
44
|
+
"See plugins/kbagent/skills/kbagent/SKILL.md. "
|
|
45
|
+
"Developer Portal credentials stored here have the SAME risk profile -- "
|
|
46
|
+
"never call apps-api.keboola.com directly; use `kbagent dev-portal ...`."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# File-lock constants (fcntl is POSIX-only; on Windows we skip locking).
|
|
50
|
+
try:
|
|
51
|
+
import fcntl
|
|
52
|
+
|
|
53
|
+
_LOCK_SH = fcntl.LOCK_SH
|
|
54
|
+
_LOCK_EX = fcntl.LOCK_EX
|
|
55
|
+
_LOCK_UN = fcntl.LOCK_UN
|
|
56
|
+
_HAS_FCNTL = True
|
|
57
|
+
except ImportError:
|
|
58
|
+
_LOCK_SH = 0
|
|
59
|
+
_LOCK_EX = 0
|
|
60
|
+
_LOCK_UN = 0
|
|
61
|
+
_HAS_FCNTL = False
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _try_flock(fd: int, operation: int) -> None:
|
|
65
|
+
"""Try to apply a file lock. Silently skip on unsupported platforms (Windows)."""
|
|
66
|
+
if not _HAS_FCNTL:
|
|
67
|
+
return
|
|
68
|
+
with contextlib.suppress(OSError):
|
|
69
|
+
fcntl.flock(fd, operation)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve_config_dir(cli_config_dir: str | None = None) -> tuple[Path, str]:
|
|
73
|
+
"""Resolve the config directory using the priority chain.
|
|
74
|
+
|
|
75
|
+
Priority:
|
|
76
|
+
1. --config-dir CLI flag (explicit override)
|
|
77
|
+
2. KBAGENT_CONFIG_DIR environment variable
|
|
78
|
+
3. Walk up from CWD looking for .kbagent/config.json (like git)
|
|
79
|
+
4. Global default (~/.config/keboola-agent-cli/)
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Tuple of (resolved_path, source_label).
|
|
83
|
+
source_label is one of: "cli-flag", "env-var", "local", "global".
|
|
84
|
+
"""
|
|
85
|
+
if cli_config_dir:
|
|
86
|
+
return Path(cli_config_dir), "cli-flag"
|
|
87
|
+
|
|
88
|
+
env_val = os.environ.get(ENV_CONFIG_DIR)
|
|
89
|
+
if env_val:
|
|
90
|
+
return Path(env_val), "env-var"
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
current = Path.cwd().resolve()
|
|
94
|
+
except OSError:
|
|
95
|
+
return Path(platformdirs.user_config_dir("keboola-agent-cli")), "global"
|
|
96
|
+
|
|
97
|
+
home = Path.home().resolve()
|
|
98
|
+
while True:
|
|
99
|
+
candidate = current / LOCAL_CONFIG_DIR_NAME / "config.json"
|
|
100
|
+
if candidate.is_file():
|
|
101
|
+
return current / LOCAL_CONFIG_DIR_NAME, "local"
|
|
102
|
+
if current == home or current == current.parent:
|
|
103
|
+
break
|
|
104
|
+
current = current.parent
|
|
105
|
+
|
|
106
|
+
return Path(platformdirs.user_config_dir("keboola-agent-cli")), "global"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ConfigStore:
|
|
110
|
+
"""Handles persistence of application configuration to disk.
|
|
111
|
+
|
|
112
|
+
Configuration is stored as JSON at the platform-appropriate config directory,
|
|
113
|
+
defaulting to ~/.config/keboola-agent-cli/config.json on Linux/macOS.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
CONFIG_FILENAME = "config.json"
|
|
117
|
+
|
|
118
|
+
def __init__(self, config_dir: Path | None = None, source: str = "global") -> None:
|
|
119
|
+
if config_dir is None:
|
|
120
|
+
self._config_dir = Path(platformdirs.user_config_dir("keboola-agent-cli"))
|
|
121
|
+
else:
|
|
122
|
+
self._config_dir = config_dir
|
|
123
|
+
self._config_path = self._config_dir / self.CONFIG_FILENAME
|
|
124
|
+
self._source = source
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def config_path(self) -> Path:
|
|
128
|
+
"""Return the path to the config file."""
|
|
129
|
+
return self._config_path
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def config_dir(self) -> Path:
|
|
133
|
+
"""Return the directory holding ``config.json`` (and sibling state files)."""
|
|
134
|
+
return self._config_dir
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def source(self) -> str:
|
|
138
|
+
"""Return the config source label (cli-flag, env-var, local, global)."""
|
|
139
|
+
return self._source
|
|
140
|
+
|
|
141
|
+
def load(self) -> AppConfig:
|
|
142
|
+
"""Load configuration from disk.
|
|
143
|
+
|
|
144
|
+
Returns an empty AppConfig if the file does not exist.
|
|
145
|
+
Validates the config version and raises ConfigError on mismatch or corruption.
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
ConfigError: If the config file is corrupted or has an unsupported version.
|
|
149
|
+
"""
|
|
150
|
+
logger.debug("Loading config from %s", self._config_path)
|
|
151
|
+
if not self._config_path.exists():
|
|
152
|
+
logger.debug("Config file does not exist, returning empty config")
|
|
153
|
+
return self._inject_env_project(AppConfig())
|
|
154
|
+
|
|
155
|
+
fd: int | None = None
|
|
156
|
+
try:
|
|
157
|
+
fd = os.open(str(self._config_path), os.O_RDONLY)
|
|
158
|
+
_try_flock(fd, _LOCK_SH)
|
|
159
|
+
raw = self._config_path.read_text(encoding="utf-8")
|
|
160
|
+
except OSError as exc:
|
|
161
|
+
raise ConfigError(f"Cannot read config file {self._config_path}: {exc}") from exc
|
|
162
|
+
except UnicodeDecodeError as exc:
|
|
163
|
+
raise ConfigError(f"Config file is not valid UTF-8 text: {exc}") from exc
|
|
164
|
+
finally:
|
|
165
|
+
if fd is not None:
|
|
166
|
+
_try_flock(fd, _LOCK_UN)
|
|
167
|
+
os.close(fd)
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
data = json.loads(raw)
|
|
171
|
+
except json.JSONDecodeError as exc:
|
|
172
|
+
raise ConfigError(f"Config file is not valid JSON: {exc}") from exc
|
|
173
|
+
|
|
174
|
+
if not isinstance(data, dict):
|
|
175
|
+
raise ConfigError(
|
|
176
|
+
f"Config file has invalid structure: expected JSON object, got {type(data).__name__}"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
version = data.get("version", 1)
|
|
180
|
+
if version > CURRENT_CONFIG_VERSION:
|
|
181
|
+
raise ConfigError(
|
|
182
|
+
f"Config file version {version} is newer than supported version "
|
|
183
|
+
f"{CURRENT_CONFIG_VERSION}. Please upgrade keboola-cli."
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
config = AppConfig.model_validate(data)
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
raise ConfigError(f"Config file has invalid structure: {exc}") from exc
|
|
190
|
+
|
|
191
|
+
return self._inject_env_project(config)
|
|
192
|
+
|
|
193
|
+
def _inject_env_project(self, config: AppConfig) -> AppConfig:
|
|
194
|
+
"""Synthesize an in-memory project from env vars when opted in (issue #359).
|
|
195
|
+
|
|
196
|
+
When ``KBAGENT_PROJECT_FROM_ENV`` is truthy, read ``KBC_TOKEN`` and
|
|
197
|
+
``KBC_STORAGE_API_URL`` and inject a project under the reserved alias
|
|
198
|
+
``__env__`` so a headless daemon / container / CI can run kbagent with
|
|
199
|
+
no ``project add`` and no config.json on disk. Both CLI and ``serve``
|
|
200
|
+
funnel through ``load()``, so this single chokepoint covers both.
|
|
201
|
+
|
|
202
|
+
The injected project is marked ``ephemeral=True``; ``save()`` strips it
|
|
203
|
+
so the env token is never persisted. Opt-in is explicit (the flag), not
|
|
204
|
+
the mere presence of ``KBC_TOKEN``, to avoid a phantom project on a dev
|
|
205
|
+
machine that exported the token only for ``project add``.
|
|
206
|
+
|
|
207
|
+
A real project already registered under ``__env__`` is left untouched.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
ConfigError: If the flag is set but the credential env vars are
|
|
211
|
+
missing (fail fast rather than silently skip).
|
|
212
|
+
"""
|
|
213
|
+
flag = os.environ.get(ENV_PROJECT_FROM_ENV, "").strip().lower()
|
|
214
|
+
if flag not in ("1", "true", "yes", "on"):
|
|
215
|
+
return config
|
|
216
|
+
|
|
217
|
+
if ENV_PROJECT_ALIAS in config.projects:
|
|
218
|
+
return config
|
|
219
|
+
|
|
220
|
+
token = os.environ.get(ENV_KBC_TOKEN)
|
|
221
|
+
url = os.environ.get(ENV_KBC_STORAGE_API_URL)
|
|
222
|
+
if not token or not url:
|
|
223
|
+
missing = [
|
|
224
|
+
name
|
|
225
|
+
for name, value in ((ENV_KBC_TOKEN, token), (ENV_KBC_STORAGE_API_URL, url))
|
|
226
|
+
if not value
|
|
227
|
+
]
|
|
228
|
+
raise ConfigError(
|
|
229
|
+
f"{ENV_PROJECT_FROM_ENV} is set but {' and '.join(missing)} "
|
|
230
|
+
f"{'is' if len(missing) == 1 else 'are'} missing. Set both "
|
|
231
|
+
f"{ENV_KBC_TOKEN} and {ENV_KBC_STORAGE_API_URL}, or unset "
|
|
232
|
+
f"{ENV_PROJECT_FROM_ENV}."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Keboola Storage tokens are `{projectId}-{tokenId}-{secret}`, so we can
|
|
236
|
+
# recover the project_id offline from the prefix. The real project_name
|
|
237
|
+
# needs an API call (verify_token) -- load() must stay offline, so it is
|
|
238
|
+
# left blank here; `project status` / `project info` show the verified
|
|
239
|
+
# name when a command actually talks to the API.
|
|
240
|
+
prefix = token.split("-", 1)[0]
|
|
241
|
+
project_id = int(prefix) if prefix.isdigit() else None
|
|
242
|
+
try:
|
|
243
|
+
config.projects[ENV_PROJECT_ALIAS] = ProjectConfig(
|
|
244
|
+
stack_url=url,
|
|
245
|
+
token=token,
|
|
246
|
+
project_id=project_id,
|
|
247
|
+
ephemeral=True,
|
|
248
|
+
)
|
|
249
|
+
except ValidationError as exc:
|
|
250
|
+
# Convert pydantic's raw error into a clean fail-fast message --
|
|
251
|
+
# this runs inside load(), which callers only guard for ConfigError.
|
|
252
|
+
reason = "; ".join(e.get("msg", "") for e in exc.errors()) or str(exc)
|
|
253
|
+
raise ConfigError(
|
|
254
|
+
f"{ENV_KBC_STORAGE_API_URL}={url!r} is not a usable stack URL: {reason}"
|
|
255
|
+
) from exc
|
|
256
|
+
if not config.default_project:
|
|
257
|
+
config.default_project = ENV_PROJECT_ALIAS
|
|
258
|
+
logger.debug("Injected ephemeral '%s' project from environment", ENV_PROJECT_ALIAS)
|
|
259
|
+
return config
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def _strip_ephemeral_projects(config: AppConfig) -> AppConfig:
|
|
263
|
+
"""Return a copy of ``config`` with ephemeral (env-synthesized) projects removed.
|
|
264
|
+
|
|
265
|
+
Defends against persisting an env token to disk: mutation methods do
|
|
266
|
+
``load() -> mutate -> save()``, and ``load()`` may have injected the
|
|
267
|
+
``__env__`` project. The original object is left intact because callers
|
|
268
|
+
keep using it after ``save()`` returns. If ``default_project`` pointed
|
|
269
|
+
at a stripped ephemeral alias, it is blanked (the next ``load()``
|
|
270
|
+
re-injects and re-defaults it).
|
|
271
|
+
"""
|
|
272
|
+
ephemeral_aliases = {alias for alias, p in config.projects.items() if p.ephemeral}
|
|
273
|
+
if not ephemeral_aliases:
|
|
274
|
+
return config
|
|
275
|
+
clean = config.model_copy(deep=True)
|
|
276
|
+
for alias in ephemeral_aliases:
|
|
277
|
+
clean.projects.pop(alias, None)
|
|
278
|
+
if clean.default_project in ephemeral_aliases:
|
|
279
|
+
clean.default_project = next(iter(clean.projects), "")
|
|
280
|
+
return clean
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _reject_ephemeral_mutation(config: AppConfig, alias: str, operation: str) -> None:
|
|
284
|
+
"""Block mutations targeting an env-synthesized project (issue #359).
|
|
285
|
+
|
|
286
|
+
A `__env__` project injected from `KBAGENT_PROJECT_FROM_ENV` exists only
|
|
287
|
+
in memory and is stripped on save, so `remove`/`edit`/`rename`/branch
|
|
288
|
+
ops would otherwise report success and then silently vanish on the next
|
|
289
|
+
`load()`. Reject them with a clear, actionable message instead. A real
|
|
290
|
+
persisted project that happens to use the alias (``ephemeral=False``) is
|
|
291
|
+
unaffected.
|
|
292
|
+
"""
|
|
293
|
+
project = config.projects.get(alias)
|
|
294
|
+
if project is not None and project.ephemeral:
|
|
295
|
+
raise ConfigError(
|
|
296
|
+
f"Project '{alias}' is synthesized from environment variables "
|
|
297
|
+
f"({ENV_PROJECT_FROM_ENV}) and cannot be {operation} -- it lives "
|
|
298
|
+
f"only in memory. To change it, update {ENV_KBC_TOKEN} / "
|
|
299
|
+
f"{ENV_KBC_STORAGE_API_URL}; to manage a persisted project, unset "
|
|
300
|
+
f"{ENV_PROJECT_FROM_ENV} and use 'project add'."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
def save(self, config: AppConfig) -> None:
|
|
304
|
+
"""Save configuration to disk with secure file permissions (0600).
|
|
305
|
+
|
|
306
|
+
Creates the config directory if it does not exist.
|
|
307
|
+
Uses atomic write to ensure the file is never on disk with
|
|
308
|
+
permissions broader than 0600 (prevents TOCTOU race condition).
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
ConfigError: If the file cannot be written.
|
|
312
|
+
"""
|
|
313
|
+
logger.debug("Saving config to %s", self._config_path)
|
|
314
|
+
lock_fd: int | None = None
|
|
315
|
+
try:
|
|
316
|
+
self._config_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
317
|
+
self._ensure_gitignore()
|
|
318
|
+
# Never persist env-synthesized projects (issue #359): strip any
|
|
319
|
+
# ephemeral entry so the KBC_TOKEN from the environment stays in
|
|
320
|
+
# memory only. Operate on a copy -- callers reuse the AppConfig.
|
|
321
|
+
config = self._strip_ephemeral_projects(config)
|
|
322
|
+
# Prepend the agent-facing warning as the first field so any LLM
|
|
323
|
+
# that reads config.json sees it BEFORE any token value.
|
|
324
|
+
payload = {
|
|
325
|
+
"_warning": CLAUDE_CONFIG_WARNING,
|
|
326
|
+
**config.model_dump(mode="json"),
|
|
327
|
+
}
|
|
328
|
+
json_str = json.dumps(payload, indent=2, ensure_ascii=False)
|
|
329
|
+
data = (json_str + "\n").encode("utf-8")
|
|
330
|
+
|
|
331
|
+
# Acquire an exclusive lock on the target file before writing.
|
|
332
|
+
# The lock file is opened (or created) with 0600 permissions.
|
|
333
|
+
lock_fd = os.open(str(self._config_path), os.O_RDONLY | os.O_CREAT, 0o600)
|
|
334
|
+
_try_flock(lock_fd, _LOCK_EX)
|
|
335
|
+
|
|
336
|
+
# Write to a temp file created with 0600 from the start,
|
|
337
|
+
# then atomically rename into place. This avoids any window
|
|
338
|
+
# where the config file exists with world-readable permissions.
|
|
339
|
+
tmp_path = self._config_path.with_suffix(".tmp")
|
|
340
|
+
fd = os.open(str(tmp_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
341
|
+
try:
|
|
342
|
+
os.write(fd, data)
|
|
343
|
+
finally:
|
|
344
|
+
os.close(fd)
|
|
345
|
+
# On Windows (no fcntl), close the lock fd before os.replace —
|
|
346
|
+
# Windows cannot atomically replace a file that is currently open.
|
|
347
|
+
# On POSIX the fd stays open (flock is still held) until the finally block.
|
|
348
|
+
if not _HAS_FCNTL and lock_fd is not None:
|
|
349
|
+
os.close(lock_fd)
|
|
350
|
+
lock_fd = None
|
|
351
|
+
os.replace(str(tmp_path), str(self._config_path))
|
|
352
|
+
except OSError as exc:
|
|
353
|
+
raise ConfigError(f"Cannot write config file {self._config_path}: {exc}") from exc
|
|
354
|
+
finally:
|
|
355
|
+
if lock_fd is not None:
|
|
356
|
+
_try_flock(lock_fd, _LOCK_UN)
|
|
357
|
+
os.close(lock_fd)
|
|
358
|
+
|
|
359
|
+
def _ensure_gitignore(self) -> None:
|
|
360
|
+
"""Create a .gitignore inside the config directory to protect tokens.
|
|
361
|
+
|
|
362
|
+
Defense in depth: even if the parent .gitignore covers this directory,
|
|
363
|
+
a local .gitignore prevents accidental commits if the parent rule is
|
|
364
|
+
removed or the config dir is copied elsewhere.
|
|
365
|
+
"""
|
|
366
|
+
gitignore_path = self._config_dir / ".gitignore"
|
|
367
|
+
if gitignore_path.exists():
|
|
368
|
+
return
|
|
369
|
+
try:
|
|
370
|
+
gitignore_path.write_text(
|
|
371
|
+
"# Auto-generated by kbagent -- protects stored API tokens\n*\n",
|
|
372
|
+
encoding="utf-8",
|
|
373
|
+
)
|
|
374
|
+
except OSError:
|
|
375
|
+
logger.debug("Could not create .gitignore in %s", self._config_dir)
|
|
376
|
+
|
|
377
|
+
def add_project(self, alias: str, project: ProjectConfig) -> None:
|
|
378
|
+
"""Add a project to the configuration.
|
|
379
|
+
|
|
380
|
+
Sets it as default if no default is set yet.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
alias: Human-friendly project name.
|
|
384
|
+
project: Project configuration with stack URL, token, and project info.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ConfigError: If the alias already exists.
|
|
388
|
+
"""
|
|
389
|
+
config = self.load()
|
|
390
|
+
if alias in config.projects:
|
|
391
|
+
raise ConfigError(f"Project '{alias}' already exists. Use 'project edit' to modify it.")
|
|
392
|
+
config.projects[alias] = project
|
|
393
|
+
if not config.default_project:
|
|
394
|
+
config.default_project = alias
|
|
395
|
+
self.save(config)
|
|
396
|
+
|
|
397
|
+
def remove_project(self, alias: str) -> None:
|
|
398
|
+
"""Remove a project from the configuration.
|
|
399
|
+
|
|
400
|
+
Updates the default project if the removed project was the default.
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
alias: The project alias to remove.
|
|
404
|
+
|
|
405
|
+
Raises:
|
|
406
|
+
ConfigError: If the alias does not exist.
|
|
407
|
+
"""
|
|
408
|
+
config = self.load()
|
|
409
|
+
if alias not in config.projects:
|
|
410
|
+
raise ConfigError(f"Project '{alias}' not found.")
|
|
411
|
+
self._reject_ephemeral_mutation(config, alias, "removed")
|
|
412
|
+
del config.projects[alias]
|
|
413
|
+
if config.default_project == alias:
|
|
414
|
+
config.default_project = next(iter(config.projects), "")
|
|
415
|
+
self.save(config)
|
|
416
|
+
|
|
417
|
+
def get_project(self, alias: str) -> ProjectConfig | None:
|
|
418
|
+
"""Get a project by alias, or None if not found."""
|
|
419
|
+
config = self.load()
|
|
420
|
+
return config.projects.get(alias)
|
|
421
|
+
|
|
422
|
+
def set_project_branch(self, alias: str, branch_id: int | None) -> None:
|
|
423
|
+
"""Set or clear the active development branch for a project.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
alias: The project alias.
|
|
427
|
+
branch_id: Branch ID to activate, or None to reset to main.
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
ConfigError: If the alias does not exist.
|
|
431
|
+
"""
|
|
432
|
+
config = self.load()
|
|
433
|
+
if alias not in config.projects:
|
|
434
|
+
raise ConfigError(f"Project '{alias}' not found.")
|
|
435
|
+
self._reject_ephemeral_mutation(config, alias, "modified")
|
|
436
|
+
config.projects[alias].active_branch_id = branch_id
|
|
437
|
+
self.save(config)
|
|
438
|
+
|
|
439
|
+
def edit_project(self, alias: str, **kwargs: str | int | None) -> None:
|
|
440
|
+
"""Update fields on an existing project.
|
|
441
|
+
|
|
442
|
+
Only non-None keyword arguments are applied.
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
alias: The project alias to edit.
|
|
446
|
+
**kwargs: Fields to update (stack_url, token, project_name, project_id).
|
|
447
|
+
|
|
448
|
+
Raises:
|
|
449
|
+
ConfigError: If the alias does not exist.
|
|
450
|
+
"""
|
|
451
|
+
config = self.load()
|
|
452
|
+
if alias not in config.projects:
|
|
453
|
+
raise ConfigError(f"Project '{alias}' not found.")
|
|
454
|
+
self._reject_ephemeral_mutation(config, alias, "edited")
|
|
455
|
+
project = config.projects[alias]
|
|
456
|
+
for key, value in kwargs.items():
|
|
457
|
+
if hasattr(project, key) and value is not None:
|
|
458
|
+
setattr(project, key, value)
|
|
459
|
+
config.projects[alias] = project
|
|
460
|
+
self.save(config)
|
|
461
|
+
|
|
462
|
+
def rename_project(self, old_alias: str, new_alias: str) -> None:
|
|
463
|
+
"""Rename a project alias in the persisted config.
|
|
464
|
+
|
|
465
|
+
Pops ``old_alias`` from the projects dict and re-inserts the same
|
|
466
|
+
``ProjectConfig`` under ``new_alias``. If ``default_project`` was
|
|
467
|
+
set to ``old_alias``, it is updated to ``new_alias`` so the pin
|
|
468
|
+
survives the rename. Both mutations are applied to the same
|
|
469
|
+
in-memory ``AppConfig`` and saved as one transaction.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
old_alias: The current alias to rename from.
|
|
473
|
+
new_alias: The target alias to rename to.
|
|
474
|
+
|
|
475
|
+
Raises:
|
|
476
|
+
ConfigError: If ``old_alias`` does not exist or ``new_alias``
|
|
477
|
+
is already in use by another project.
|
|
478
|
+
"""
|
|
479
|
+
config = self.load()
|
|
480
|
+
if old_alias not in config.projects:
|
|
481
|
+
raise ConfigError(f"Project '{old_alias}' not found.")
|
|
482
|
+
self._reject_ephemeral_mutation(config, old_alias, "renamed")
|
|
483
|
+
if new_alias in config.projects:
|
|
484
|
+
raise ConfigError(
|
|
485
|
+
f"Cannot rename '{old_alias}' to '{new_alias}': "
|
|
486
|
+
f"alias '{new_alias}' is already in use."
|
|
487
|
+
)
|
|
488
|
+
config.projects[new_alias] = config.projects.pop(old_alias)
|
|
489
|
+
if config.default_project == old_alias:
|
|
490
|
+
config.default_project = new_alias
|
|
491
|
+
self.save(config)
|
|
492
|
+
|
|
493
|
+
def add_dev_portal_identity(self, alias: str, identity: DeveloperPortalIdentity) -> None:
|
|
494
|
+
"""Add a Developer Portal identity to the configuration.
|
|
495
|
+
|
|
496
|
+
Sets it as default if no default identity is set.
|
|
497
|
+
|
|
498
|
+
Raises:
|
|
499
|
+
ConfigError: If the alias already exists.
|
|
500
|
+
"""
|
|
501
|
+
config = self.load()
|
|
502
|
+
if alias in config.dev_portal_identities:
|
|
503
|
+
raise ConfigError(
|
|
504
|
+
f"Developer Portal identity '{alias}' already exists. "
|
|
505
|
+
"Use 'dev-portal identity edit' to modify it."
|
|
506
|
+
)
|
|
507
|
+
config.dev_portal_identities[alias] = identity
|
|
508
|
+
if not config.default_dev_portal_identity:
|
|
509
|
+
config.default_dev_portal_identity = alias
|
|
510
|
+
self.save(config)
|
|
511
|
+
|
|
512
|
+
def remove_dev_portal_identity(self, alias: str) -> None:
|
|
513
|
+
"""Remove a Developer Portal identity.
|
|
514
|
+
|
|
515
|
+
Falls the default through to the next available identity (or "" if none).
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
ConfigError: If the alias does not exist.
|
|
519
|
+
"""
|
|
520
|
+
config = self.load()
|
|
521
|
+
if alias not in config.dev_portal_identities:
|
|
522
|
+
raise ConfigError(f"Developer Portal identity '{alias}' not found.")
|
|
523
|
+
del config.dev_portal_identities[alias]
|
|
524
|
+
if config.default_dev_portal_identity == alias:
|
|
525
|
+
config.default_dev_portal_identity = next(iter(config.dev_portal_identities), "")
|
|
526
|
+
self.save(config)
|
|
527
|
+
|
|
528
|
+
def get_dev_portal_identity(self, alias: str) -> DeveloperPortalIdentity | None:
|
|
529
|
+
"""Get a Developer Portal identity by alias, or None if not found."""
|
|
530
|
+
config = self.load()
|
|
531
|
+
return config.dev_portal_identities.get(alias)
|
|
532
|
+
|
|
533
|
+
def edit_dev_portal_identity(self, alias: str, **kwargs: str | None) -> None:
|
|
534
|
+
"""Update fields on an existing Developer Portal identity.
|
|
535
|
+
|
|
536
|
+
Only non-None keyword arguments are applied.
|
|
537
|
+
|
|
538
|
+
Raises:
|
|
539
|
+
ConfigError: If the alias does not exist.
|
|
540
|
+
"""
|
|
541
|
+
config = self.load()
|
|
542
|
+
if alias not in config.dev_portal_identities:
|
|
543
|
+
raise ConfigError(f"Developer Portal identity '{alias}' not found.")
|
|
544
|
+
ident = config.dev_portal_identities[alias]
|
|
545
|
+
for key, value in kwargs.items():
|
|
546
|
+
if hasattr(ident, key) and value is not None:
|
|
547
|
+
setattr(ident, key, value)
|
|
548
|
+
config.dev_portal_identities[alias] = ident
|
|
549
|
+
self.save(config)
|
|
550
|
+
|
|
551
|
+
def rename_dev_portal_identity(self, old_alias: str, new_alias: str) -> None:
|
|
552
|
+
"""Rename a Developer Portal identity alias.
|
|
553
|
+
|
|
554
|
+
If the default was set to the old alias, it follows the rename.
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
ConfigError: If old alias does not exist, or new alias is in use.
|
|
558
|
+
"""
|
|
559
|
+
config = self.load()
|
|
560
|
+
if old_alias not in config.dev_portal_identities:
|
|
561
|
+
raise ConfigError(f"Developer Portal identity '{old_alias}' not found.")
|
|
562
|
+
if new_alias in config.dev_portal_identities:
|
|
563
|
+
raise ConfigError(
|
|
564
|
+
f"Cannot rename '{old_alias}' to '{new_alias}': "
|
|
565
|
+
f"alias '{new_alias}' is already in use."
|
|
566
|
+
)
|
|
567
|
+
config.dev_portal_identities[new_alias] = config.dev_portal_identities.pop(old_alias)
|
|
568
|
+
if config.default_dev_portal_identity == old_alias:
|
|
569
|
+
config.default_dev_portal_identity = new_alias
|
|
570
|
+
self.save(config)
|
|
571
|
+
|
|
572
|
+
def set_default_dev_portal_identity(self, alias: str) -> None:
|
|
573
|
+
"""Set the default Developer Portal identity.
|
|
574
|
+
|
|
575
|
+
Raises:
|
|
576
|
+
ConfigError: If the alias does not exist.
|
|
577
|
+
"""
|
|
578
|
+
config = self.load()
|
|
579
|
+
if alias not in config.dev_portal_identities:
|
|
580
|
+
raise ConfigError(f"Developer Portal identity '{alias}' not found.")
|
|
581
|
+
config.default_dev_portal_identity = alias
|
|
582
|
+
self.save(config)
|