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,593 @@
|
|
|
1
|
+
"""Project membership and invitation lifecycle service.
|
|
2
|
+
|
|
3
|
+
Wraps the Manage API endpoints under ``/manage/projects/{id}/{users,invitations}``
|
|
4
|
+
behind a layer that:
|
|
5
|
+
|
|
6
|
+
- resolves a project alias to its numeric ID via ``ConfigStore``;
|
|
7
|
+
- looks up members + invitations by email (the public-facing key) so callers
|
|
8
|
+
never need to deal with raw user/invitation IDs;
|
|
9
|
+
- treats the Manage API's "already invited / already a member" 400 response as
|
|
10
|
+
a no-op rather than an error (mirrors the heuristic from the orchestrator
|
|
11
|
+
scripts, but typed to status_code + message substring rather than guessed
|
|
12
|
+
HTTP code);
|
|
13
|
+
- parallelises bulk invitation via :class:`ThreadPoolExecutor`, accumulating
|
|
14
|
+
per-row results so one bad row never aborts the rest.
|
|
15
|
+
|
|
16
|
+
Endpoints + payload shapes were verified empirically on 2026-05-01 against
|
|
17
|
+
``connection.us-east4.gcp.keboola.com``; see the plan-of-record for the full
|
|
18
|
+
verification log.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import csv
|
|
24
|
+
import logging
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from ..config_store import ConfigStore
|
|
31
|
+
from ..constants import DEFAULT_INVITE_WORKERS, PROJECT_ROLES
|
|
32
|
+
from ..errors import ConfigError, ErrorCode, KeboolaApiError
|
|
33
|
+
from ..manage_client import ManageClient
|
|
34
|
+
from ..models import (
|
|
35
|
+
BulkInviteResult,
|
|
36
|
+
MemberInviteRow,
|
|
37
|
+
ProjectInvitation,
|
|
38
|
+
ProjectMember,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
ManageClientFactory = Callable[[str, str], ManageClient]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def default_manage_client_factory(stack_url: str, manage_token: str) -> ManageClient:
|
|
47
|
+
"""Construct a :class:`ManageClient` bound to ``stack_url``."""
|
|
48
|
+
return ManageClient(stack_url=stack_url, manage_token=manage_token)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# The Manage API returns HTTP 400 (not 422) with one of these substrings when
|
|
52
|
+
# a duplicate invitation/member is created. Treated as success-with-note in
|
|
53
|
+
# the service layer so bulk imports don't fail on idempotent re-runs.
|
|
54
|
+
_ALREADY_INVITED_MARKER = "already been invited"
|
|
55
|
+
_ALREADY_MEMBER_MARKER = "already a member"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class MemberService:
|
|
59
|
+
"""Business logic for project members and invitations."""
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
config_store: ConfigStore,
|
|
64
|
+
manage_client_factory: ManageClientFactory | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self._config_store = config_store
|
|
67
|
+
self._manage_client_factory = manage_client_factory or default_manage_client_factory
|
|
68
|
+
|
|
69
|
+
# ------------------------------------------------------------------
|
|
70
|
+
# Public API: single-shot operations
|
|
71
|
+
# ------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def invite(
|
|
74
|
+
self,
|
|
75
|
+
*,
|
|
76
|
+
manage_token: str,
|
|
77
|
+
alias: str,
|
|
78
|
+
email: str,
|
|
79
|
+
role: str,
|
|
80
|
+
reason: str | None = None,
|
|
81
|
+
dry_run: bool = False,
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
"""Invite ``email`` to the project registered under ``alias``."""
|
|
84
|
+
self._validate_role(role)
|
|
85
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
86
|
+
|
|
87
|
+
if dry_run:
|
|
88
|
+
return {
|
|
89
|
+
"status": "dry_run",
|
|
90
|
+
"alias": alias,
|
|
91
|
+
"project_id": project_id,
|
|
92
|
+
"email": email,
|
|
93
|
+
"role": role,
|
|
94
|
+
"reason": reason or "",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
98
|
+
try:
|
|
99
|
+
return self._invite_one(manage_client, alias, project_id, email, role, reason)
|
|
100
|
+
finally:
|
|
101
|
+
manage_client.close()
|
|
102
|
+
|
|
103
|
+
def invite_bulk(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
manage_token: str,
|
|
107
|
+
csv_path: Path,
|
|
108
|
+
default_role: str | None = None,
|
|
109
|
+
workers: int = DEFAULT_INVITE_WORKERS,
|
|
110
|
+
dry_run: bool = False,
|
|
111
|
+
) -> BulkInviteResult:
|
|
112
|
+
"""Invite every row of ``csv_path`` in parallel.
|
|
113
|
+
|
|
114
|
+
CSV must have a header row. Recognised columns (case-insensitive):
|
|
115
|
+
``email`` (required), ``project`` or ``project_id`` (one required),
|
|
116
|
+
``role`` (optional if ``default_role`` is given), ``reason`` (optional).
|
|
117
|
+
Extra columns are ignored. ``project`` values that are all-digits are
|
|
118
|
+
resolved as numeric project IDs without an alias lookup.
|
|
119
|
+
"""
|
|
120
|
+
if default_role is not None:
|
|
121
|
+
self._validate_role(default_role)
|
|
122
|
+
rows = self._parse_invite_csv(csv_path, default_role)
|
|
123
|
+
if not rows:
|
|
124
|
+
return BulkInviteResult(total=0, succeeded=0, noop=0, failed=0, dry_run=dry_run)
|
|
125
|
+
|
|
126
|
+
if dry_run:
|
|
127
|
+
return self._bulk_dry_run(rows)
|
|
128
|
+
|
|
129
|
+
# Resolve every row's (stack_url, project_id) up front. A row that
|
|
130
|
+
# fails resolution (unknown alias, unregistered project_id) becomes a
|
|
131
|
+
# per-row "failed" entry; the rest of the batch still runs. Mirrors
|
|
132
|
+
# the partial-success contract enforced by `OrgService.refresh_tokens`.
|
|
133
|
+
resolved: list[tuple[dict[str, Any], str, int]] = []
|
|
134
|
+
upfront_failures: list[MemberInviteRow] = []
|
|
135
|
+
for row in rows:
|
|
136
|
+
try:
|
|
137
|
+
stack_url, project_id = self._stack_for_row(row)
|
|
138
|
+
resolved.append((row, stack_url, project_id))
|
|
139
|
+
except ConfigError as exc:
|
|
140
|
+
upfront_failures.append(
|
|
141
|
+
MemberInviteRow(
|
|
142
|
+
email=row["email"],
|
|
143
|
+
project=str(row["project"]),
|
|
144
|
+
role=row["role"],
|
|
145
|
+
status="failed",
|
|
146
|
+
note=str(getattr(exc, "message", exc)),
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not resolved:
|
|
151
|
+
return BulkInviteResult(
|
|
152
|
+
total=len(upfront_failures),
|
|
153
|
+
succeeded=0,
|
|
154
|
+
noop=0,
|
|
155
|
+
failed=len(upfront_failures),
|
|
156
|
+
rows=upfront_failures,
|
|
157
|
+
dry_run=False,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# All resolved rows must share a single stack URL; sending invitations
|
|
161
|
+
# for project A on stack X via a client bound to stack Y is a security
|
|
162
|
+
# bug, not a "partial-success" path.
|
|
163
|
+
resolved_stacks = {t[1] for t in resolved}
|
|
164
|
+
if len(resolved_stacks) != 1:
|
|
165
|
+
raise ConfigError(
|
|
166
|
+
f"CSV references multiple stack URLs ({sorted(resolved_stacks)}); "
|
|
167
|
+
"split the file by stack and run --from-csv per stack."
|
|
168
|
+
)
|
|
169
|
+
stack_url = resolved_stacks.pop()
|
|
170
|
+
|
|
171
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
172
|
+
results: list[MemberInviteRow] = list(upfront_failures)
|
|
173
|
+
try:
|
|
174
|
+
worker_count = max(1, min(workers, len(resolved)))
|
|
175
|
+
with ThreadPoolExecutor(max_workers=worker_count) as pool:
|
|
176
|
+
futures = [
|
|
177
|
+
pool.submit(self._invoke_resolved_row, manage_client, row, project_id)
|
|
178
|
+
for row, _, project_id in resolved
|
|
179
|
+
]
|
|
180
|
+
for fut in as_completed(futures):
|
|
181
|
+
results.append(fut.result())
|
|
182
|
+
finally:
|
|
183
|
+
manage_client.close()
|
|
184
|
+
|
|
185
|
+
return BulkInviteResult(
|
|
186
|
+
total=len(results),
|
|
187
|
+
succeeded=sum(1 for r in results if r.status == "ok"),
|
|
188
|
+
noop=sum(1 for r in results if r.status == "noop"),
|
|
189
|
+
failed=sum(1 for r in results if r.status == "failed"),
|
|
190
|
+
rows=results,
|
|
191
|
+
dry_run=False,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def list_members(
|
|
195
|
+
self,
|
|
196
|
+
*,
|
|
197
|
+
manage_token: str,
|
|
198
|
+
alias: str,
|
|
199
|
+
include_pending: bool = False,
|
|
200
|
+
) -> dict[str, Any]:
|
|
201
|
+
"""Return active members (and, optionally, pending invitations)."""
|
|
202
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
203
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
204
|
+
try:
|
|
205
|
+
members_raw = manage_client.list_project_members(project_id)
|
|
206
|
+
members = [ProjectMember.model_validate(m) for m in members_raw]
|
|
207
|
+
payload: dict[str, Any] = {
|
|
208
|
+
"alias": alias,
|
|
209
|
+
"project_id": project_id,
|
|
210
|
+
"members": [m.model_dump(by_alias=False) for m in members],
|
|
211
|
+
}
|
|
212
|
+
if include_pending:
|
|
213
|
+
inv_raw = manage_client.list_project_invitations(project_id)
|
|
214
|
+
payload["pending_invitations"] = [
|
|
215
|
+
ProjectInvitation.model_validate(i).model_dump(by_alias=False) for i in inv_raw
|
|
216
|
+
]
|
|
217
|
+
return payload
|
|
218
|
+
finally:
|
|
219
|
+
manage_client.close()
|
|
220
|
+
|
|
221
|
+
def list_invitations(
|
|
222
|
+
self,
|
|
223
|
+
*,
|
|
224
|
+
manage_token: str,
|
|
225
|
+
alias: str,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Return pending invitations for ``alias``."""
|
|
228
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
229
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
230
|
+
try:
|
|
231
|
+
raw = manage_client.list_project_invitations(project_id)
|
|
232
|
+
return {
|
|
233
|
+
"alias": alias,
|
|
234
|
+
"project_id": project_id,
|
|
235
|
+
"invitations": [
|
|
236
|
+
ProjectInvitation.model_validate(i).model_dump(by_alias=False) for i in raw
|
|
237
|
+
],
|
|
238
|
+
}
|
|
239
|
+
finally:
|
|
240
|
+
manage_client.close()
|
|
241
|
+
|
|
242
|
+
def cancel_invitation(
|
|
243
|
+
self,
|
|
244
|
+
*,
|
|
245
|
+
manage_token: str,
|
|
246
|
+
alias: str,
|
|
247
|
+
email: str,
|
|
248
|
+
invitation_id: int | None = None,
|
|
249
|
+
) -> dict[str, Any]:
|
|
250
|
+
"""Cancel a pending invitation. Resolves by email if no ID is supplied."""
|
|
251
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
252
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
253
|
+
try:
|
|
254
|
+
if invitation_id is None:
|
|
255
|
+
invitation_id = self._resolve_invitation_id(manage_client, project_id, email)
|
|
256
|
+
manage_client.cancel_project_invitation(project_id, invitation_id)
|
|
257
|
+
return {
|
|
258
|
+
"status": "cancelled",
|
|
259
|
+
"alias": alias,
|
|
260
|
+
"project_id": project_id,
|
|
261
|
+
"email": email,
|
|
262
|
+
"invitation_id": invitation_id,
|
|
263
|
+
}
|
|
264
|
+
finally:
|
|
265
|
+
manage_client.close()
|
|
266
|
+
|
|
267
|
+
def remove_member(
|
|
268
|
+
self,
|
|
269
|
+
*,
|
|
270
|
+
manage_token: str,
|
|
271
|
+
alias: str,
|
|
272
|
+
email: str,
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
"""Remove an active member from a project."""
|
|
275
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
276
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
277
|
+
try:
|
|
278
|
+
user_id = self._resolve_member_id(manage_client, project_id, email)
|
|
279
|
+
manage_client.remove_project_member(project_id, user_id)
|
|
280
|
+
return {
|
|
281
|
+
"status": "removed",
|
|
282
|
+
"alias": alias,
|
|
283
|
+
"project_id": project_id,
|
|
284
|
+
"email": email,
|
|
285
|
+
"user_id": user_id,
|
|
286
|
+
}
|
|
287
|
+
finally:
|
|
288
|
+
manage_client.close()
|
|
289
|
+
|
|
290
|
+
def set_member_role(
|
|
291
|
+
self,
|
|
292
|
+
*,
|
|
293
|
+
manage_token: str,
|
|
294
|
+
alias: str,
|
|
295
|
+
email: str,
|
|
296
|
+
role: str,
|
|
297
|
+
) -> dict[str, Any]:
|
|
298
|
+
"""Change an existing member's role via PATCH."""
|
|
299
|
+
self._validate_role(role)
|
|
300
|
+
stack_url, project_id = self._resolve_alias(alias)
|
|
301
|
+
manage_client = self._manage_client_factory(stack_url, manage_token)
|
|
302
|
+
try:
|
|
303
|
+
user_id = self._resolve_member_id(manage_client, project_id, email)
|
|
304
|
+
updated = manage_client.update_project_member_role(project_id, user_id, role)
|
|
305
|
+
return {
|
|
306
|
+
"status": "updated",
|
|
307
|
+
"alias": alias,
|
|
308
|
+
"project_id": project_id,
|
|
309
|
+
"email": email,
|
|
310
|
+
"user_id": user_id,
|
|
311
|
+
"role": updated.get("role", role),
|
|
312
|
+
}
|
|
313
|
+
finally:
|
|
314
|
+
manage_client.close()
|
|
315
|
+
|
|
316
|
+
# ------------------------------------------------------------------
|
|
317
|
+
# Internal helpers
|
|
318
|
+
# ------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
@staticmethod
|
|
321
|
+
def _validate_role(role: str) -> None:
|
|
322
|
+
"""Defence-in-depth: command layer enforces the same whitelist via Choice."""
|
|
323
|
+
if role not in PROJECT_ROLES:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"Invalid role {role!r}. Allowed roles are: {', '.join(PROJECT_ROLES)}."
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def _resolve_alias(self, alias: str) -> tuple[str, int]:
|
|
329
|
+
"""Look up ``alias`` in the config store and return ``(stack_url, project_id)``."""
|
|
330
|
+
project = self._config_store.get_project(alias)
|
|
331
|
+
if project is None:
|
|
332
|
+
raise ConfigError(
|
|
333
|
+
f"Project alias '{alias}' is not registered. Run `kbagent project list`."
|
|
334
|
+
)
|
|
335
|
+
if project.project_id is None:
|
|
336
|
+
raise ConfigError(
|
|
337
|
+
f"Project alias '{alias}' has no numeric project_id; "
|
|
338
|
+
"re-add it via `kbagent project add` to populate it."
|
|
339
|
+
)
|
|
340
|
+
return project.stack_url, project.project_id
|
|
341
|
+
|
|
342
|
+
def _stack_for_row(self, row: dict[str, Any]) -> tuple[str, int]:
|
|
343
|
+
"""Resolve a CSV row's project field to ``(stack_url, project_id)``."""
|
|
344
|
+
project_field = str(row["project"]).strip()
|
|
345
|
+
if project_field.isdigit():
|
|
346
|
+
# Numeric project_id rows still need a stack_url; we infer from the
|
|
347
|
+
# currently-registered projects sharing that ID, falling back to
|
|
348
|
+
# any default.
|
|
349
|
+
project_id = int(project_field)
|
|
350
|
+
for cfg in self._config_store.load().projects.values():
|
|
351
|
+
if cfg.project_id == project_id:
|
|
352
|
+
return cfg.stack_url, project_id
|
|
353
|
+
raise ConfigError(
|
|
354
|
+
f"CSV row references project_id={project_id}, which is not registered "
|
|
355
|
+
"in this kbagent config; add it via `kbagent project add` so we know "
|
|
356
|
+
"which stack URL to use."
|
|
357
|
+
)
|
|
358
|
+
return self._resolve_alias(project_field)
|
|
359
|
+
|
|
360
|
+
@staticmethod
|
|
361
|
+
def _resolve_member_id(manage_client: ManageClient, project_id: int, email: str) -> int:
|
|
362
|
+
"""Find an active member's numeric ID by email (case-insensitive match)."""
|
|
363
|
+
members = manage_client.list_project_members(project_id)
|
|
364
|
+
normalised = email.casefold()
|
|
365
|
+
for member in members:
|
|
366
|
+
if str(member.get("email", "")).casefold() == normalised:
|
|
367
|
+
return int(member["id"])
|
|
368
|
+
raise KeboolaApiError(
|
|
369
|
+
message=f"No active member with email {email!r} on project {project_id}.",
|
|
370
|
+
status_code=404,
|
|
371
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
372
|
+
retryable=False,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def _resolve_invitation_id(manage_client: ManageClient, project_id: int, email: str) -> int:
|
|
377
|
+
"""Find a pending invitation by email."""
|
|
378
|
+
invitations = manage_client.list_project_invitations(project_id)
|
|
379
|
+
normalised = email.casefold()
|
|
380
|
+
for inv in invitations:
|
|
381
|
+
if str(inv.get("user", {}).get("email", "")).casefold() == normalised:
|
|
382
|
+
return int(inv["id"])
|
|
383
|
+
raise KeboolaApiError(
|
|
384
|
+
message=f"No pending invitation for email {email!r} on project {project_id}.",
|
|
385
|
+
status_code=404,
|
|
386
|
+
error_code=ErrorCode.NOT_FOUND,
|
|
387
|
+
retryable=False,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def _invite_one(
|
|
391
|
+
self,
|
|
392
|
+
manage_client: ManageClient,
|
|
393
|
+
project_label: str,
|
|
394
|
+
project_id: int,
|
|
395
|
+
email: str,
|
|
396
|
+
role: str,
|
|
397
|
+
reason: str | None,
|
|
398
|
+
) -> dict[str, Any]:
|
|
399
|
+
"""Single-row invitation logic shared by ``invite`` and ``invite_bulk``.
|
|
400
|
+
|
|
401
|
+
``project_label`` is the human-readable project identifier surfaced in
|
|
402
|
+
the result dict's ``alias`` field. Single-shot mode passes the
|
|
403
|
+
registered alias; bulk mode passes the raw CSV ``project`` cell, which
|
|
404
|
+
may be either an alias or a numeric project ID string -- whatever the
|
|
405
|
+
user wrote.
|
|
406
|
+
"""
|
|
407
|
+
try:
|
|
408
|
+
invitation = manage_client.create_project_invitation(
|
|
409
|
+
project_id=project_id,
|
|
410
|
+
email=email,
|
|
411
|
+
role=role,
|
|
412
|
+
reason=reason,
|
|
413
|
+
)
|
|
414
|
+
return {
|
|
415
|
+
"status": "ok",
|
|
416
|
+
"alias": project_label,
|
|
417
|
+
"project_id": project_id,
|
|
418
|
+
"email": email,
|
|
419
|
+
"role": role,
|
|
420
|
+
"invitation_id": invitation.get("id"),
|
|
421
|
+
}
|
|
422
|
+
except KeboolaApiError as exc:
|
|
423
|
+
note = self._noop_note_for(exc)
|
|
424
|
+
if note is None:
|
|
425
|
+
raise
|
|
426
|
+
return {
|
|
427
|
+
"status": "noop",
|
|
428
|
+
"alias": project_label,
|
|
429
|
+
"project_id": project_id,
|
|
430
|
+
"email": email,
|
|
431
|
+
"role": role,
|
|
432
|
+
"note": note,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
def _invoke_resolved_row(
|
|
436
|
+
self,
|
|
437
|
+
manage_client: ManageClient,
|
|
438
|
+
row: dict[str, Any],
|
|
439
|
+
project_id: int,
|
|
440
|
+
) -> MemberInviteRow:
|
|
441
|
+
"""Execute one CSV row inside the bulk-invite executor.
|
|
442
|
+
|
|
443
|
+
Called only on rows whose (stack_url, project_id) was already resolved
|
|
444
|
+
by ``invite_bulk`` -- so the only failure path here is the API call
|
|
445
|
+
itself (e.g. invalid email, network error, role rejection).
|
|
446
|
+
"""
|
|
447
|
+
email = row["email"]
|
|
448
|
+
role = row["role"]
|
|
449
|
+
reason = row.get("reason")
|
|
450
|
+
project_field = str(row["project"]).strip()
|
|
451
|
+
try:
|
|
452
|
+
outcome = self._invite_one(
|
|
453
|
+
manage_client, project_field, project_id, email, role, reason
|
|
454
|
+
)
|
|
455
|
+
return MemberInviteRow(
|
|
456
|
+
email=email,
|
|
457
|
+
project=project_field,
|
|
458
|
+
project_id=project_id,
|
|
459
|
+
role=role,
|
|
460
|
+
status=outcome["status"],
|
|
461
|
+
note=outcome.get("note", ""),
|
|
462
|
+
invitation_id=outcome.get("invitation_id"),
|
|
463
|
+
)
|
|
464
|
+
except KeboolaApiError as exc:
|
|
465
|
+
return MemberInviteRow(
|
|
466
|
+
email=email,
|
|
467
|
+
project=project_field,
|
|
468
|
+
project_id=project_id,
|
|
469
|
+
role=role,
|
|
470
|
+
status="failed",
|
|
471
|
+
note=str(getattr(exc, "message", exc)),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
@staticmethod
|
|
475
|
+
def _noop_note_for(exc: KeboolaApiError) -> str | None:
|
|
476
|
+
"""Return a noop reason if ``exc`` is the "already invited / member" 400."""
|
|
477
|
+
if exc.status_code != 400:
|
|
478
|
+
return None
|
|
479
|
+
message = exc.message or ""
|
|
480
|
+
if _ALREADY_INVITED_MARKER in message:
|
|
481
|
+
return "already_invited"
|
|
482
|
+
if _ALREADY_MEMBER_MARKER in message:
|
|
483
|
+
return "already_member"
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
def _parse_invite_csv(self, csv_path: Path, default_role: str | None) -> list[dict[str, Any]]:
|
|
487
|
+
"""Parse + validate a bulk-invite CSV. Returns a list of normalised dicts."""
|
|
488
|
+
if not csv_path.exists():
|
|
489
|
+
raise ConfigError(f"CSV file not found: {csv_path}")
|
|
490
|
+
|
|
491
|
+
# `utf-8-sig` strips a leading BOM if Excel produced the CSV (otherwise
|
|
492
|
+
# the first header reads as `email`, which fails the email-column
|
|
493
|
+
# check with a misleading message).
|
|
494
|
+
with csv_path.open("r", newline="", encoding="utf-8-sig") as fh:
|
|
495
|
+
reader = csv.DictReader(fh)
|
|
496
|
+
if reader.fieldnames is None:
|
|
497
|
+
raise ConfigError(f"CSV file {csv_path} has no header row.")
|
|
498
|
+
headers = {h.strip().lower(): h for h in reader.fieldnames if h}
|
|
499
|
+
if "email" not in headers:
|
|
500
|
+
raise ConfigError(
|
|
501
|
+
f"CSV file {csv_path} is missing an 'email' column. "
|
|
502
|
+
f"Found columns: {list(reader.fieldnames)}."
|
|
503
|
+
)
|
|
504
|
+
project_key = (
|
|
505
|
+
"project"
|
|
506
|
+
if "project" in headers
|
|
507
|
+
else ("project_id" if "project_id" in headers else None)
|
|
508
|
+
)
|
|
509
|
+
if project_key is None:
|
|
510
|
+
raise ConfigError(
|
|
511
|
+
f"CSV file {csv_path} must have a 'project' or 'project_id' column. "
|
|
512
|
+
f"Found columns: {list(reader.fieldnames)}."
|
|
513
|
+
)
|
|
514
|
+
has_role = "role" in headers
|
|
515
|
+
if not has_role and default_role is None:
|
|
516
|
+
raise ConfigError(
|
|
517
|
+
f"CSV file {csv_path} has no 'role' column and --default-role was not given."
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
rows: list[dict[str, Any]] = []
|
|
521
|
+
for line_no, raw in enumerate(reader, start=2): # header is line 1
|
|
522
|
+
email = (raw.get(headers["email"]) or "").strip()
|
|
523
|
+
project = (raw.get(headers[project_key]) or "").strip()
|
|
524
|
+
role = (raw.get(headers["role"]) if has_role else None) or default_role or ""
|
|
525
|
+
role = role.strip()
|
|
526
|
+
reason = (raw.get(headers["reason"]) or "").strip() if "reason" in headers else ""
|
|
527
|
+
if not email or not project:
|
|
528
|
+
raise ConfigError(
|
|
529
|
+
f"CSV {csv_path} line {line_no}: 'email' and '{project_key}' are both required."
|
|
530
|
+
)
|
|
531
|
+
if not role:
|
|
532
|
+
raise ConfigError(
|
|
533
|
+
f"CSV {csv_path} line {line_no}: missing role and no --default-role."
|
|
534
|
+
)
|
|
535
|
+
self._validate_role(role)
|
|
536
|
+
rows.append(
|
|
537
|
+
{
|
|
538
|
+
"email": email,
|
|
539
|
+
"project": project,
|
|
540
|
+
"role": role,
|
|
541
|
+
"reason": reason or None,
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
return rows
|
|
545
|
+
|
|
546
|
+
def _bulk_dry_run(self, rows: list[dict[str, Any]]) -> BulkInviteResult:
|
|
547
|
+
"""Render a dry-run result without hitting the network.
|
|
548
|
+
|
|
549
|
+
Mirrors the live path: per-row resolution failures become per-row
|
|
550
|
+
failed entries; multi-stack-URL CSVs raise (matches the real-run
|
|
551
|
+
invariant so users don't get a "preview said ok, real run aborted"
|
|
552
|
+
surprise).
|
|
553
|
+
"""
|
|
554
|
+
previewed: list[MemberInviteRow] = []
|
|
555
|
+
resolved_stacks: set[str] = set()
|
|
556
|
+
for row in rows:
|
|
557
|
+
try:
|
|
558
|
+
stack_url, project_id = self._stack_for_row(row)
|
|
559
|
+
resolved_stacks.add(stack_url)
|
|
560
|
+
except ConfigError as exc:
|
|
561
|
+
previewed.append(
|
|
562
|
+
MemberInviteRow(
|
|
563
|
+
email=row["email"],
|
|
564
|
+
project=str(row["project"]),
|
|
565
|
+
role=row["role"],
|
|
566
|
+
status="failed",
|
|
567
|
+
note=str(getattr(exc, "message", exc)),
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
continue
|
|
571
|
+
previewed.append(
|
|
572
|
+
MemberInviteRow(
|
|
573
|
+
email=row["email"],
|
|
574
|
+
project=str(row["project"]),
|
|
575
|
+
project_id=project_id,
|
|
576
|
+
role=row["role"],
|
|
577
|
+
status="ok",
|
|
578
|
+
note="dry_run",
|
|
579
|
+
)
|
|
580
|
+
)
|
|
581
|
+
if len(resolved_stacks) > 1:
|
|
582
|
+
raise ConfigError(
|
|
583
|
+
f"CSV references multiple stack URLs ({sorted(resolved_stacks)}); "
|
|
584
|
+
"split the file by stack and run --from-csv per stack."
|
|
585
|
+
)
|
|
586
|
+
return BulkInviteResult(
|
|
587
|
+
total=len(previewed),
|
|
588
|
+
succeeded=sum(1 for r in previewed if r.status == "ok"),
|
|
589
|
+
noop=0,
|
|
590
|
+
failed=sum(1 for r in previewed if r.status == "failed"),
|
|
591
|
+
rows=previewed,
|
|
592
|
+
dry_run=True,
|
|
593
|
+
)
|