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,342 @@
|
|
|
1
|
+
"""Keboola Data Science API client (data-app deployment records).
|
|
2
|
+
|
|
3
|
+
The Data Science API owns the *deployment* side of a data app — id, state,
|
|
4
|
+
desiredState, url, configVersion. The Storage API
|
|
5
|
+
(``keboola.data-apps`` configs) owns the *configuration* side — git block,
|
|
6
|
+
encrypted secrets, slug, runtime size. Both must stay in sync; see
|
|
7
|
+
``services/data_app_service.py`` for the orchestration.
|
|
8
|
+
|
|
9
|
+
URL derivation: ``https://data-science.<stack-suffix>`` from the project's
|
|
10
|
+
connection URL via ``BaseHttpClient._derive_service_url``. Auth: same
|
|
11
|
+
``X-StorageApi-Token`` as the Storage API. The single exception is
|
|
12
|
+
``GET /apps/{id}/password`` which additionally requires
|
|
13
|
+
``X-KBC-ManageApiToken`` -- the manage token is passed per-call so the
|
|
14
|
+
client itself stays project-scoped.
|
|
15
|
+
|
|
16
|
+
Verified shapes (writeup §2 / §6 / §9, replayed in this PR's live
|
|
17
|
+
validation):
|
|
18
|
+
|
|
19
|
+
POST /apps -> 201, {id, configId, ...}
|
|
20
|
+
GET /apps -> 200, [{id, configId, state, desiredState, url}, ...]
|
|
21
|
+
GET /apps/{id} -> 200, full deployment record
|
|
22
|
+
PATCH /apps/{id} -> 200, deployment record (only
|
|
23
|
+
desiredState / configVersion /
|
|
24
|
+
restartIfRunning persist;
|
|
25
|
+
``config:{...}`` is silently
|
|
26
|
+
dropped)
|
|
27
|
+
DELETE /apps/{id} -> 202, cascades to Storage config
|
|
28
|
+
GET /apps/{id}/password -> 200, {password: "<20 hex>"}
|
|
29
|
+
(requires both Storage and
|
|
30
|
+
Manage tokens)
|
|
31
|
+
GET /apps/{id}/logs/tail -> 200, text/plain container log
|
|
32
|
+
tail. ``lines=N`` and
|
|
33
|
+
``since=ISO8601`` are mutually
|
|
34
|
+
exclusive on the server.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
from typing import Any
|
|
42
|
+
from urllib.parse import quote
|
|
43
|
+
|
|
44
|
+
from .constants import DEFAULT_TIMEOUT
|
|
45
|
+
from .http_base import BaseHttpClient
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DataScienceClient(BaseHttpClient):
|
|
51
|
+
"""HTTP client for the Keboola Data Science API (``/apps``).
|
|
52
|
+
|
|
53
|
+
Inherits retry / backoff / token-masking from ``BaseHttpClient``.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, stack_url: str, token: str) -> None:
|
|
57
|
+
self._stack_url = stack_url.rstrip("/")
|
|
58
|
+
ds_base_url = self._derive_service_url(self._stack_url, "data-science")
|
|
59
|
+
headers = {
|
|
60
|
+
"X-StorageApi-Token": token,
|
|
61
|
+
}
|
|
62
|
+
super().__init__(
|
|
63
|
+
base_url=ds_base_url,
|
|
64
|
+
token=token,
|
|
65
|
+
headers=headers,
|
|
66
|
+
timeout=DEFAULT_TIMEOUT,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def __enter__(self) -> DataScienceClient:
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *args: Any) -> None:
|
|
73
|
+
self.close()
|
|
74
|
+
|
|
75
|
+
def list_apps(self) -> list[dict[str, Any]]:
|
|
76
|
+
"""Return the thin index of data apps in the project (no body filter).
|
|
77
|
+
|
|
78
|
+
The Data Science API scopes responses by the token's project; there
|
|
79
|
+
is no ``branchId`` query parameter on the list endpoint.
|
|
80
|
+
"""
|
|
81
|
+
response = self._do_request("GET", "/apps")
|
|
82
|
+
body = response.json()
|
|
83
|
+
# Some stacks wrap the list in {"data": [...]}; fall back gracefully.
|
|
84
|
+
apps = (body.get("data") or body.get("apps") or []) if isinstance(body, dict) else body
|
|
85
|
+
return apps if isinstance(apps, list) else []
|
|
86
|
+
|
|
87
|
+
def get_app(self, app_id: str) -> dict[str, Any]:
|
|
88
|
+
"""Fetch a single deployment record by numeric app id."""
|
|
89
|
+
response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}")
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
def create_app(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
type_: str,
|
|
96
|
+
name: str,
|
|
97
|
+
description: str,
|
|
98
|
+
config: dict[str, Any],
|
|
99
|
+
branch_id: int | None = None,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Create the deployment shell + linked Storage config in one call.
|
|
102
|
+
|
|
103
|
+
Server-generated identifiers: ``id`` (numeric) and ``configId``
|
|
104
|
+
(ULID). The ``configId`` field in the request body is silently
|
|
105
|
+
ignored (writeup §5) -- callers must accept whatever ULID the
|
|
106
|
+
server assigns and round-trip it on subsequent updates.
|
|
107
|
+
|
|
108
|
+
``config`` carries the *initial* Storage configuration body. The
|
|
109
|
+
full config (git block, encrypted secrets, etc.) is added via
|
|
110
|
+
``KeboolaClient.update_config`` after creation; sending it here is
|
|
111
|
+
possible but the encryption step depends on knowing
|
|
112
|
+
``config_id`` first, so the canonical flow is:
|
|
113
|
+
``create_app`` -> encrypt secrets -> ``update_config``.
|
|
114
|
+
"""
|
|
115
|
+
payload: dict[str, Any] = {
|
|
116
|
+
"branchId": branch_id,
|
|
117
|
+
"type": type_,
|
|
118
|
+
"name": name,
|
|
119
|
+
"description": description,
|
|
120
|
+
"config": config,
|
|
121
|
+
}
|
|
122
|
+
response = self._do_request(
|
|
123
|
+
"POST",
|
|
124
|
+
"/apps",
|
|
125
|
+
content=json.dumps(payload).encode("utf-8"),
|
|
126
|
+
headers={"Content-Type": "application/json"},
|
|
127
|
+
)
|
|
128
|
+
return response.json()
|
|
129
|
+
|
|
130
|
+
def patch_app(
|
|
131
|
+
self,
|
|
132
|
+
app_id: str,
|
|
133
|
+
*,
|
|
134
|
+
desired_state: str | None = None,
|
|
135
|
+
config_version: str | None = None,
|
|
136
|
+
restart_if_running: bool | None = None,
|
|
137
|
+
) -> dict[str, Any]:
|
|
138
|
+
"""Update the deployment record (state / pinned config version).
|
|
139
|
+
|
|
140
|
+
IMPORTANT: never sends a ``config`` block — that surface is owned
|
|
141
|
+
by the Storage API (writeup §2.1, §8 pitfall row 3). Updating
|
|
142
|
+
size / autoSuspend / git settings goes through ``update_config``
|
|
143
|
+
on the Storage API.
|
|
144
|
+
|
|
145
|
+
The §9 redeploy contract requires
|
|
146
|
+
``desired_state="running"`` + ``config_version=<N>``
|
|
147
|
+
+ ``restart_if_running=True`` together when bumping the deployed
|
|
148
|
+
config version; sending ``config_version`` alone yields HTTP 422.
|
|
149
|
+
"""
|
|
150
|
+
payload: dict[str, Any] = {}
|
|
151
|
+
if desired_state is not None:
|
|
152
|
+
payload["desiredState"] = desired_state
|
|
153
|
+
if config_version is not None:
|
|
154
|
+
payload["configVersion"] = config_version
|
|
155
|
+
if restart_if_running is not None:
|
|
156
|
+
payload["restartIfRunning"] = restart_if_running
|
|
157
|
+
response = self._do_request(
|
|
158
|
+
"PATCH",
|
|
159
|
+
f"/apps/{quote(str(app_id), safe='')}",
|
|
160
|
+
content=json.dumps(payload).encode("utf-8"),
|
|
161
|
+
headers={"Content-Type": "application/json"},
|
|
162
|
+
)
|
|
163
|
+
return response.json()
|
|
164
|
+
|
|
165
|
+
def delete_app(self, app_id: str) -> None:
|
|
166
|
+
"""Delete the deployment AND the linked Storage config (cascade).
|
|
167
|
+
|
|
168
|
+
Returns HTTP 202 on success; the body is empty.
|
|
169
|
+
"""
|
|
170
|
+
self._do_request("DELETE", f"/apps/{quote(str(app_id), safe='')}")
|
|
171
|
+
|
|
172
|
+
def get_app_password(self, app_id: str, manage_token: str) -> dict[str, Any]:
|
|
173
|
+
"""Retrieve the auto-generated simpleAuth password.
|
|
174
|
+
|
|
175
|
+
Requires both the project's Storage token (already on
|
|
176
|
+
``self._client``) AND a Manage API token, supplied per-call so the
|
|
177
|
+
manage token never lives on the client instance.
|
|
178
|
+
|
|
179
|
+
The 20-character hex password is auto-generated at app create time
|
|
180
|
+
and is NOT rotatable -- to change it you must delete and recreate
|
|
181
|
+
the app (writeup §11.2).
|
|
182
|
+
"""
|
|
183
|
+
path = f"/apps/{quote(str(app_id), safe='')}/password"
|
|
184
|
+
# Pass the Manage token via per-request `headers=`. httpx merges these
|
|
185
|
+
# with the client's persistent headers for this call only, so the
|
|
186
|
+
# manage token never lives on `self._client`. Using `_do_request`
|
|
187
|
+
# gives us the same retry/backoff and uniform error mapping as every
|
|
188
|
+
# other call in this client (no bespoke try/except needed).
|
|
189
|
+
response = self._do_request(
|
|
190
|
+
"GET",
|
|
191
|
+
path,
|
|
192
|
+
headers={"X-KBC-ManageApiToken": manage_token},
|
|
193
|
+
)
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
def tail_app_logs(
|
|
197
|
+
self,
|
|
198
|
+
app_id: str,
|
|
199
|
+
*,
|
|
200
|
+
lines: int | None = None,
|
|
201
|
+
since: str | None = None,
|
|
202
|
+
) -> str:
|
|
203
|
+
"""Fetch the container log tail from ``/apps/{id}/logs/tail``.
|
|
204
|
+
|
|
205
|
+
Returns the response body verbatim as plain text (``text/plain``)
|
|
206
|
+
-- one log line per ``\\n``, trailing newline preserved as the
|
|
207
|
+
server sent it. Callers that want a list of lines should call
|
|
208
|
+
``text.splitlines()``.
|
|
209
|
+
|
|
210
|
+
``lines`` and ``since`` are mutually exclusive on the server
|
|
211
|
+
(400 ``Only one of "since" or "lines" can be set``); the caller
|
|
212
|
+
MUST enforce that constraint before invoking. Passing neither
|
|
213
|
+
returns the full current container buffer. ``lines=0`` and
|
|
214
|
+
negative values are rejected by the server with a 400 -- callers
|
|
215
|
+
opting into the full buffer should pass ``lines=None``.
|
|
216
|
+
|
|
217
|
+
``since`` must be an ISO 8601 timestamp WITH timezone (``Z`` or
|
|
218
|
+
``+00:00``); naive datetimes and date-only values are rejected
|
|
219
|
+
by the server with a 400.
|
|
220
|
+
"""
|
|
221
|
+
path = f"/apps/{quote(str(app_id), safe='')}/logs/tail"
|
|
222
|
+
params: dict[str, Any] = {}
|
|
223
|
+
if lines is not None:
|
|
224
|
+
params["lines"] = lines
|
|
225
|
+
if since is not None:
|
|
226
|
+
params["since"] = since
|
|
227
|
+
# ``params or None`` keeps the URL clean (no trailing ``?``) when
|
|
228
|
+
# the caller wants the server's default buffer-all behavior.
|
|
229
|
+
response = self._do_request("GET", path, params=params or None)
|
|
230
|
+
return response.text
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# Git repository (sandboxes-service /apps/{id}/git-repo/*)
|
|
234
|
+
#
|
|
235
|
+
# These endpoints introspect and manage the git repository a data app
|
|
236
|
+
# is deployed from. Ground truth: keboola/sandboxes-service server
|
|
237
|
+
# source + docs/swagger.yaml. Two functional groups:
|
|
238
|
+
#
|
|
239
|
+
# * Repo introspection (git-repo, /branches, /entrypoints) -- works
|
|
240
|
+
# for ANY configured repo (managed or external); auth = the same
|
|
241
|
+
# X-StorageApi-Token, permission CanManageApp.
|
|
242
|
+
# * Credential management (/credentials GET + POST) -- ONLY for a
|
|
243
|
+
# *managed* git repo (app.managedGitRepoId set); a repo configured
|
|
244
|
+
# via `data-app create --git-repo <url>` is *external*, so these
|
|
245
|
+
# return 409. Auth needs an admin storage token
|
|
246
|
+
# (CanManageAppRepoCredentials).
|
|
247
|
+
#
|
|
248
|
+
# IMPORTANT response-shape gotchas (verified in both sources):
|
|
249
|
+
# * /branches returns a RAW top-level JSON array (NOT wrapped in
|
|
250
|
+
# {branches: [...]}).
|
|
251
|
+
# * /entrypoints returns a RAW top-level array<string>.
|
|
252
|
+
# * /credentials (GET) IS wrapped: {"credentials": [...]}.
|
|
253
|
+
# * POST /credentials returns the created credential; the one-time
|
|
254
|
+
# ``secret`` is present ONLY for type=http_token and ONLY here.
|
|
255
|
+
# ------------------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
def get_git_repo(self, app_id: str) -> dict[str, Any]:
|
|
258
|
+
"""Return the clone URLs of the app's configured git repository.
|
|
259
|
+
|
|
260
|
+
Shape: ``{"sshUrl": str|None, "httpsUrl": str|None,
|
|
261
|
+
"isManagedGitRepo": bool}``. For external repos only the URL
|
|
262
|
+
matching the configured protocol is populated (the other is
|
|
263
|
+
``None``) and embedded credentials are stripped. ``409`` if the
|
|
264
|
+
app has no git repository configured.
|
|
265
|
+
"""
|
|
266
|
+
response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}/git-repo")
|
|
267
|
+
body = response.json()
|
|
268
|
+
return body if isinstance(body, dict) else {}
|
|
269
|
+
|
|
270
|
+
def list_git_branches(self, app_id: str) -> list[dict[str, Any]]:
|
|
271
|
+
"""List the remote branches of the app's configured git repository.
|
|
272
|
+
|
|
273
|
+
Returns the server's RAW top-level array of branch objects
|
|
274
|
+
``[{"branch", "comment", "sha", "author": {"name", "email"},
|
|
275
|
+
"date"}]`` (HEAD/origin/HEAD filtered, sorted by name). Works for
|
|
276
|
+
managed and external repos alike.
|
|
277
|
+
"""
|
|
278
|
+
response = self._do_request("GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/branches")
|
|
279
|
+
body = response.json()
|
|
280
|
+
return body if isinstance(body, list) else []
|
|
281
|
+
|
|
282
|
+
def list_git_entrypoints(self, app_id: str) -> list[str]:
|
|
283
|
+
"""List root-level ``.py`` entrypoint files of the app's repo.
|
|
284
|
+
|
|
285
|
+
Returns the server's RAW top-level ``array<string>`` of root
|
|
286
|
+
filenames on the configured branch (or the repo default).
|
|
287
|
+
Extension is hardcoded to ``py`` server-side, so non-Python
|
|
288
|
+
entrypoints are not listable here.
|
|
289
|
+
"""
|
|
290
|
+
response = self._do_request(
|
|
291
|
+
"GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/entrypoints"
|
|
292
|
+
)
|
|
293
|
+
body = response.json()
|
|
294
|
+
return [str(item) for item in body] if isinstance(body, list) else []
|
|
295
|
+
|
|
296
|
+
def list_git_credentials(self, app_id: str) -> dict[str, Any]:
|
|
297
|
+
"""List the credentials of the app's MANAGED git repository.
|
|
298
|
+
|
|
299
|
+
Shape: ``{"credentials": [{"id", "type", "name", "permissions",
|
|
300
|
+
"ownerAdminId", "createdAt"}]}``. The ``secret`` is NEVER returned
|
|
301
|
+
here. ``409`` if the app has no managed git repository; requires an
|
|
302
|
+
admin storage token.
|
|
303
|
+
"""
|
|
304
|
+
response = self._do_request(
|
|
305
|
+
"GET", f"/apps/{quote(str(app_id), safe='')}/git-repo/credentials"
|
|
306
|
+
)
|
|
307
|
+
body = response.json()
|
|
308
|
+
return body if isinstance(body, dict) else {}
|
|
309
|
+
|
|
310
|
+
def create_git_credential(
|
|
311
|
+
self,
|
|
312
|
+
app_id: str,
|
|
313
|
+
*,
|
|
314
|
+
type_: str,
|
|
315
|
+
permissions: str,
|
|
316
|
+
public_key: str | None = None,
|
|
317
|
+
name: str | None = None,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""Create a credential for the app's MANAGED git repository.
|
|
320
|
+
|
|
321
|
+
``type_`` is ``"ssh_key"`` or ``"http_token"``; ``permissions`` is
|
|
322
|
+
``"readOnly"`` or ``"readWrite"``. ``public_key`` is required IFF
|
|
323
|
+
``type_ == "ssh_key"`` and MUST be absent otherwise (the server
|
|
324
|
+
returns 400 on a wrong combination).
|
|
325
|
+
|
|
326
|
+
Returns the created credential. The one-time ``secret`` field is
|
|
327
|
+
present ONLY when ``type_ == "http_token"`` and is never retrievable
|
|
328
|
+
again. ``409`` if the app has no managed git repository; requires an
|
|
329
|
+
admin storage token.
|
|
330
|
+
"""
|
|
331
|
+
payload: dict[str, Any] = {"type": type_, "permissions": permissions}
|
|
332
|
+
if public_key is not None:
|
|
333
|
+
payload["publicKey"] = public_key
|
|
334
|
+
if name is not None:
|
|
335
|
+
payload["name"] = name
|
|
336
|
+
response = self._do_request(
|
|
337
|
+
"POST",
|
|
338
|
+
f"/apps/{quote(str(app_id), safe='')}/git-repo/credentials",
|
|
339
|
+
content=json.dumps(payload).encode("utf-8"),
|
|
340
|
+
headers={"Content-Type": "application/json"},
|
|
341
|
+
)
|
|
342
|
+
return response.json()
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Keboola Developer Portal HTTP client (apps-api.keboola.com).
|
|
2
|
+
|
|
3
|
+
Auth model:
|
|
4
|
+
- Login (email + password) returns a bearer token. On a personal account, the
|
|
5
|
+
first login returns an MFA session; we prompt the user via /dev/tty and
|
|
6
|
+
re-login with {email, session, code} to obtain the bearer.
|
|
7
|
+
- The bearer lives ONLY on this client instance (in self._bearer). It is
|
|
8
|
+
never written to disk, never logged, and discarded when the client closes.
|
|
9
|
+
- Each kbagent invocation logs in fresh; there is no token cache.
|
|
10
|
+
|
|
11
|
+
The client is intentionally dumb: dry-run, diff, and confirm logic belong to
|
|
12
|
+
the service and command layers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from .constants import DP_MFA_CHALLENGE_TYPE, MAX_API_ERROR_LENGTH
|
|
25
|
+
from .errors import ErrorCode, KeboolaApiError
|
|
26
|
+
from .http_base import BaseHttpClient
|
|
27
|
+
from .models import DeveloperPortalIdentity
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _tty_prompt(label: str, *, secret: bool = False) -> str | None:
|
|
33
|
+
"""Prompt via the controlling terminal so a redirected stdin can't break it.
|
|
34
|
+
|
|
35
|
+
Returns None when no /dev/tty is available (non-interactive shell, no
|
|
36
|
+
controlling terminal). Caller must treat None as "cannot prompt".
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
with open("/dev/tty", "w") as out:
|
|
40
|
+
if secret:
|
|
41
|
+
import getpass
|
|
42
|
+
|
|
43
|
+
return getpass.getpass(label, stream=out)
|
|
44
|
+
out.write(label)
|
|
45
|
+
out.flush()
|
|
46
|
+
with open("/dev/tty") as tin:
|
|
47
|
+
return tin.readline().rstrip("\n")
|
|
48
|
+
except OSError:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DeveloperPortalClient(BaseHttpClient):
|
|
53
|
+
"""HTTP client for the Keboola Developer Portal."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, identity: DeveloperPortalIdentity) -> None:
|
|
56
|
+
# We don't have a bearer yet — pass empty token. Login populates it.
|
|
57
|
+
super().__init__(
|
|
58
|
+
base_url=identity.portal_url,
|
|
59
|
+
token="",
|
|
60
|
+
headers={"Accept": "application/json"},
|
|
61
|
+
)
|
|
62
|
+
self._identity = identity
|
|
63
|
+
self._bearer: str | None = None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def bearer(self) -> str | None:
|
|
67
|
+
"""The active bearer token, or None if not yet authenticated.
|
|
68
|
+
|
|
69
|
+
In-memory only; never written to disk. Exposed so the service can
|
|
70
|
+
reuse one login across a prepare/apply pair (see seed_bearer) instead
|
|
71
|
+
of re-authenticating — which, on a personal MFA account, would prompt
|
|
72
|
+
for a second MFA code on a single write.
|
|
73
|
+
"""
|
|
74
|
+
return self._bearer
|
|
75
|
+
|
|
76
|
+
def seed_bearer(self, bearer: str) -> None:
|
|
77
|
+
"""Reuse a bearer obtained by an earlier client for the same identity.
|
|
78
|
+
|
|
79
|
+
Lets the service carry one authenticated session across the
|
|
80
|
+
prepare -> (random-code confirm) -> apply flow without a second login.
|
|
81
|
+
"""
|
|
82
|
+
self._bearer = bearer
|
|
83
|
+
self._client.headers["Authorization"] = bearer
|
|
84
|
+
|
|
85
|
+
def _ensure_authenticated(self) -> None:
|
|
86
|
+
"""Log in if not already authenticated. Idempotent on the instance."""
|
|
87
|
+
if self._bearer is not None:
|
|
88
|
+
return
|
|
89
|
+
self._bearer = self._login(self._identity.username, self._identity.password)
|
|
90
|
+
self._client.headers["Authorization"] = self._bearer
|
|
91
|
+
|
|
92
|
+
def _login(self, username: str, password: str) -> str:
|
|
93
|
+
try:
|
|
94
|
+
resp = self._client.post(
|
|
95
|
+
"/auth/login",
|
|
96
|
+
json={"email": username, "password": password},
|
|
97
|
+
)
|
|
98
|
+
except httpx.HTTPError as exc:
|
|
99
|
+
raise KeboolaApiError(
|
|
100
|
+
message=f"Developer Portal login transport error: {exc}",
|
|
101
|
+
error_code=ErrorCode.CONNECTION_ERROR,
|
|
102
|
+
) from exc
|
|
103
|
+
if resp.status_code != 200:
|
|
104
|
+
raise KeboolaApiError(
|
|
105
|
+
message=(
|
|
106
|
+
f"Developer Portal login failed (HTTP {resp.status_code}). "
|
|
107
|
+
"Check the identity credentials."
|
|
108
|
+
),
|
|
109
|
+
error_code=ErrorCode.DP_LOGIN_FAILED,
|
|
110
|
+
)
|
|
111
|
+
payload = resp.json()
|
|
112
|
+
if isinstance(payload, dict) and payload.get("token"):
|
|
113
|
+
return payload["token"]
|
|
114
|
+
# MFA path — implemented in Task 7.
|
|
115
|
+
if isinstance(payload, dict) and payload.get("session"):
|
|
116
|
+
return self._login_with_mfa(username, payload["session"])
|
|
117
|
+
raise KeboolaApiError(
|
|
118
|
+
message="Developer Portal login response missing token and session",
|
|
119
|
+
error_code=ErrorCode.DP_LOGIN_FAILED,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _login_with_mfa(self, username: str, session: str) -> str:
|
|
123
|
+
"""Confirm an MFA-gated login.
|
|
124
|
+
|
|
125
|
+
Per the Keboola Developer Portal apiary spec, the same POST /auth/login
|
|
126
|
+
endpoint accepts {email, session, code, challenge}. The `challenge`
|
|
127
|
+
field is documented as optional with default SOFTWARE_TOKEN_MFA, but
|
|
128
|
+
in practice the server rejects calls that omit it (404 with the
|
|
129
|
+
misleading "must be one of" enum message attached to the admin schema).
|
|
130
|
+
Send it explicitly. Single attempt only -- /auth/login consumes the
|
|
131
|
+
session, so any retry on the same session always 404s with "Invalid
|
|
132
|
+
code or auth state for the user" regardless of the new challenge type.
|
|
133
|
+
"""
|
|
134
|
+
code = _tty_prompt("MFA code: ")
|
|
135
|
+
if not code:
|
|
136
|
+
raise KeboolaApiError(
|
|
137
|
+
message=(
|
|
138
|
+
"Developer Portal identity requires an MFA code, but no "
|
|
139
|
+
"interactive terminal is available. Run from a real "
|
|
140
|
+
"terminal, or switch to a service.{vendor}.{id} "
|
|
141
|
+
"account (no MFA)."
|
|
142
|
+
),
|
|
143
|
+
error_code=ErrorCode.DP_MFA_REQUIRED,
|
|
144
|
+
)
|
|
145
|
+
body = {
|
|
146
|
+
"email": username,
|
|
147
|
+
"session": session,
|
|
148
|
+
"code": code.strip(),
|
|
149
|
+
"challenge": DP_MFA_CHALLENGE_TYPE,
|
|
150
|
+
}
|
|
151
|
+
try:
|
|
152
|
+
resp = self._client.post("/auth/login", json=body)
|
|
153
|
+
except httpx.HTTPError as exc:
|
|
154
|
+
raise KeboolaApiError(
|
|
155
|
+
message=f"Developer Portal MFA login transport error: {exc}",
|
|
156
|
+
error_code=ErrorCode.CONNECTION_ERROR,
|
|
157
|
+
) from exc
|
|
158
|
+
if resp.status_code == 200:
|
|
159
|
+
payload = resp.json()
|
|
160
|
+
if isinstance(payload, dict) and payload.get("token"):
|
|
161
|
+
return payload["token"]
|
|
162
|
+
raise KeboolaApiError(
|
|
163
|
+
message=(
|
|
164
|
+
"Developer Portal MFA login returned HTTP 200 but no "
|
|
165
|
+
f"'token' field in response: {payload!r}"
|
|
166
|
+
),
|
|
167
|
+
error_code=ErrorCode.DP_LOGIN_FAILED,
|
|
168
|
+
)
|
|
169
|
+
try:
|
|
170
|
+
body_text = resp.text[:MAX_API_ERROR_LENGTH]
|
|
171
|
+
except (UnicodeDecodeError, AttributeError):
|
|
172
|
+
body_text = "<unreadable>"
|
|
173
|
+
raise KeboolaApiError(
|
|
174
|
+
message=(
|
|
175
|
+
f"Developer Portal MFA login failed (HTTP {resp.status_code}): "
|
|
176
|
+
f"{body_text}. If your TOTP code rotates every 30s, this is "
|
|
177
|
+
"often a stale code -- retry promptly. If the server says "
|
|
178
|
+
"'Invalid code or auth state' on a fresh session, the code "
|
|
179
|
+
"itself was wrong."
|
|
180
|
+
),
|
|
181
|
+
error_code=ErrorCode.DP_LOGIN_FAILED,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# ----- Reads -----
|
|
185
|
+
|
|
186
|
+
def list_apps(self, vendor: str) -> list[dict[str, Any]]:
|
|
187
|
+
self._ensure_authenticated()
|
|
188
|
+
resp = self._do_request("GET", f"/vendors/{vendor}/apps?limit=1000")
|
|
189
|
+
if resp.status_code != 200:
|
|
190
|
+
self._raise_dp_error(resp, action="list apps", vendor=vendor)
|
|
191
|
+
payload = resp.json()
|
|
192
|
+
if isinstance(payload, dict) and "apps" in payload:
|
|
193
|
+
return list(payload["apps"])
|
|
194
|
+
if isinstance(payload, list):
|
|
195
|
+
return payload
|
|
196
|
+
return []
|
|
197
|
+
|
|
198
|
+
def get_app(self, vendor: str, app_id: str) -> dict[str, Any]:
|
|
199
|
+
self._ensure_authenticated()
|
|
200
|
+
try:
|
|
201
|
+
resp = self._do_request("GET", f"/vendors/{vendor}/apps/{app_id}")
|
|
202
|
+
except KeboolaApiError as exc:
|
|
203
|
+
if exc.error_code == ErrorCode.NOT_FOUND:
|
|
204
|
+
raise KeboolaApiError(
|
|
205
|
+
message=f"Developer Portal app '{app_id}' not found in vendor '{vendor}'",
|
|
206
|
+
error_code=ErrorCode.DP_APP_NOT_FOUND,
|
|
207
|
+
) from exc
|
|
208
|
+
raise
|
|
209
|
+
return resp.json()
|
|
210
|
+
|
|
211
|
+
# ----- Writes -----
|
|
212
|
+
|
|
213
|
+
def create_app(self, vendor: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
214
|
+
self._ensure_authenticated()
|
|
215
|
+
resp = self._do_request("POST", f"/vendors/{vendor}/apps", json=payload)
|
|
216
|
+
if resp.status_code not in (200, 201):
|
|
217
|
+
self._raise_dp_error(resp, action="create app", vendor=vendor)
|
|
218
|
+
return resp.json()
|
|
219
|
+
|
|
220
|
+
def patch_app(self, vendor: str, app_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
221
|
+
"""PATCH an app. Routes by identity role:
|
|
222
|
+
- admin -> PATCH /admin/apps/{app_id} (permissive schema, accepts the
|
|
223
|
+
9 fields forbidden() on the vendor schema: complexity, categories,
|
|
224
|
+
forwardToken, forwardTokenDetails, injectEnvironment, processTimeout,
|
|
225
|
+
requiredMemory, features, category).
|
|
226
|
+
- vendor -> PATCH /vendors/{vendor}/apps/{app_id} (default, restricted
|
|
227
|
+
schema). The `vendor` arg is still required for the path.
|
|
228
|
+
"""
|
|
229
|
+
self._ensure_authenticated()
|
|
230
|
+
if self._identity.role_hint == "admin":
|
|
231
|
+
path = f"/admin/apps/{app_id}"
|
|
232
|
+
else:
|
|
233
|
+
path = f"/vendors/{vendor}/apps/{app_id}"
|
|
234
|
+
resp = self._do_request("PATCH", path, json=payload)
|
|
235
|
+
if resp.status_code not in (200, 204):
|
|
236
|
+
self._raise_dp_error(resp, action="patch app", vendor=vendor, app_id=app_id)
|
|
237
|
+
return resp.json() if resp.content else {}
|
|
238
|
+
|
|
239
|
+
def publish_app(self, vendor: str, app_id: str) -> dict[str, Any]:
|
|
240
|
+
self._ensure_authenticated()
|
|
241
|
+
resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/publish")
|
|
242
|
+
if resp.status_code not in (200, 202):
|
|
243
|
+
self._raise_dp_error(resp, action="publish app", vendor=vendor, app_id=app_id)
|
|
244
|
+
return resp.json() if resp.content else {"status": "submitted"}
|
|
245
|
+
|
|
246
|
+
def deprecate_app(self, vendor: str, app_id: str) -> dict[str, Any]:
|
|
247
|
+
self._ensure_authenticated()
|
|
248
|
+
resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/deprecate")
|
|
249
|
+
if resp.status_code not in (200, 202):
|
|
250
|
+
self._raise_dp_error(resp, action="deprecate app", vendor=vendor, app_id=app_id)
|
|
251
|
+
return resp.json() if resp.content else {"status": "deprecated"}
|
|
252
|
+
|
|
253
|
+
def upload_icon(self, vendor: str, app_id: str, png_bytes: bytes) -> None:
|
|
254
|
+
"""Two-hop icon upload: ask the portal for a presigned S3 URL, then PUT bytes there.
|
|
255
|
+
|
|
256
|
+
The S3 PUT does NOT use this client's httpx instance (no retry, no auth,
|
|
257
|
+
no User-Agent injection). We use urllib directly so the wire shape stays
|
|
258
|
+
exactly what S3 expects.
|
|
259
|
+
"""
|
|
260
|
+
self._ensure_authenticated()
|
|
261
|
+
try:
|
|
262
|
+
resp = self._do_request("POST", f"/vendors/{vendor}/apps/{app_id}/icon")
|
|
263
|
+
except KeboolaApiError as exc:
|
|
264
|
+
raise KeboolaApiError(
|
|
265
|
+
message=(f"Developer Portal failed to mint icon-upload URL: {exc.message}"),
|
|
266
|
+
error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
|
|
267
|
+
) from exc
|
|
268
|
+
if resp.status_code != 200:
|
|
269
|
+
raise KeboolaApiError(
|
|
270
|
+
message=(
|
|
271
|
+
f"Developer Portal failed to mint icon-upload URL (HTTP {resp.status_code})"
|
|
272
|
+
),
|
|
273
|
+
error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
|
|
274
|
+
)
|
|
275
|
+
payload = resp.json()
|
|
276
|
+
link = payload.get("link") if isinstance(payload, dict) else None
|
|
277
|
+
if not link:
|
|
278
|
+
raise KeboolaApiError(
|
|
279
|
+
message="Developer Portal icon-upload response missing 'link'",
|
|
280
|
+
error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
|
|
281
|
+
)
|
|
282
|
+
req = urllib.request.Request(
|
|
283
|
+
link,
|
|
284
|
+
data=png_bytes,
|
|
285
|
+
headers={"Content-Type": "image/png"},
|
|
286
|
+
method="PUT",
|
|
287
|
+
)
|
|
288
|
+
try:
|
|
289
|
+
with urllib.request.urlopen(req) as s3_resp:
|
|
290
|
+
if getattr(s3_resp, "status", 200) >= 300:
|
|
291
|
+
raise KeboolaApiError(
|
|
292
|
+
message=f"Icon S3 PUT failed (HTTP {s3_resp.status})",
|
|
293
|
+
error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
|
|
294
|
+
)
|
|
295
|
+
except urllib.error.HTTPError as exc:
|
|
296
|
+
raise KeboolaApiError(
|
|
297
|
+
message=f"Icon S3 PUT failed (HTTP {exc.code}): {exc.reason}",
|
|
298
|
+
error_code=ErrorCode.DP_ICON_UPLOAD_FAILED,
|
|
299
|
+
) from exc
|
|
300
|
+
|
|
301
|
+
# ----- Error mapping -----
|
|
302
|
+
|
|
303
|
+
def _raise_dp_error(
|
|
304
|
+
self,
|
|
305
|
+
resp: httpx.Response,
|
|
306
|
+
*,
|
|
307
|
+
action: str,
|
|
308
|
+
vendor: str | None = None,
|
|
309
|
+
app_id: str | None = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
try:
|
|
312
|
+
body = resp.json()
|
|
313
|
+
except ValueError:
|
|
314
|
+
body = resp.text
|
|
315
|
+
ctx = f"{action}"
|
|
316
|
+
if vendor:
|
|
317
|
+
ctx += f" (vendor={vendor})"
|
|
318
|
+
if app_id:
|
|
319
|
+
ctx += f" (app={app_id})"
|
|
320
|
+
raise KeboolaApiError(
|
|
321
|
+
message=f"Developer Portal {ctx} failed (HTTP {resp.status_code}): {body}",
|
|
322
|
+
error_code=ErrorCode.API_ERROR,
|
|
323
|
+
)
|