From cf1b0b242b411b950f65d427b1ea07565934f920 Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Thu, 1 Oct 2026 17:13:23 +0200 Subject: [PATCH 1/2] Wire native branch/info/preview commands to CLI entry point `branch`, `info`, and `preview` previously shelled out to the upstream Forward `tb` package. Wire them to the SDK's existing native implementations (run_branch_list/status/delete, run_info, run_preview) so these commands work without `tb` installed alongside the SDK. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 + README.md | 13 ++ src/tinybird_sdk/cli/index.py | 106 ++++++++++++++++- tests/test_cli_entrypoint.py | 218 ++++++++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1ecb0..4cdf133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- `tinybird branch` (`list`/`status`/`delete`), `tinybird info`, and `tinybird preview` now run the SDK's own native implementations instead of delegating to the installed Forward `tb` package, so these commands work without `tb` installed alongside the SDK. + ## [0.4.0] - 2026-06-29 ### Added diff --git a/README.md b/README.md index e42f34b..78b3bca 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,19 @@ tinybird info tinybird info --json ``` +### `tinybird preview` + +Builds the project and deploys it to a temporary preview branch (named `tmp_ci_` by default), mirroring what CI preview environments do. + +```bash +tinybird preview +tinybird preview --dry-run +tinybird preview --check +tinybird preview --name my-preview +tinybird preview --local +tinybird preview --branch +``` + ## Configuration Create a `tinybird.config.json` (or `tinybird.config.py` / `tinybird_config.py` for dynamic logic) in your project root: diff --git a/src/tinybird_sdk/cli/index.py b/src/tinybird_sdk/cli/index.py index 1183961..72a525b 100644 --- a/src/tinybird_sdk/cli/index.py +++ b/src/tinybird_sdk/cli/index.py @@ -5,11 +5,16 @@ import json import sys +from .commands.branch import run_branch_delete, run_branch_list, run_branch_status from .commands.generate import run_generate +from .commands.info import run_info from .commands.init import run_init from .commands.migrate import run_migrate +from .commands.preview import run_preview from .output import output +_SDK_OWNED_COMMANDS = {"init", "generate", "migrate", "branch", "info", "preview"} + def _print_json(payload: object) -> None: print(json.dumps(payload, indent=2, default=str)) @@ -81,6 +86,45 @@ def create_cli() -> argparse.ArgumentParser: ) migrate_cmd.add_argument("--json", action="store_true", help="Print migration result as JSON") + branch_cmd = sub.add_parser("branch", help="Manage Tinybird branches") + branch_sub = branch_cmd.add_subparsers(dest="branch_command", required=True) + branch_sub.add_parser("list", help="List branches") + branch_status_cmd = branch_sub.add_parser("status", help="Show a branch's status") + branch_status_cmd.add_argument( + "name", nargs="?", help="Branch name (defaults to the current project branch)" + ) + branch_delete_cmd = branch_sub.add_parser("delete", help="Delete a branch") + branch_delete_cmd.add_argument("name", help="Branch name to delete") + + info_cmd = sub.add_parser("info", help="Show project and workspace info") + info_cmd.add_argument("--json", action="store_true", help="Print info as JSON") + + preview_cmd = sub.add_parser( + "preview", help="Build and deploy resources to a temporary preview branch" + ) + preview_cmd.add_argument( + "--dry-run", action="store_true", help="Build without creating a preview branch" + ) + preview_cmd.add_argument( + "--check", action="store_true", help="Validate the preview deploy without applying it" + ) + preview_cmd.add_argument("--name", help="Preview branch name override") + preview_mode = preview_cmd.add_mutually_exclusive_group() + preview_mode.add_argument( + "--local", + action="store_const", + dest="dev_mode", + const="local", + help="Preview against Tinybird Local", + ) + preview_mode.add_argument( + "--branch", + action="store_const", + dest="dev_mode", + const="branch", + help="Preview against a cloud branch", + ) + return parser @@ -88,7 +132,7 @@ def main(argv: list[str] | None = None) -> int: normalized_argv = list(argv) if argv is not None else list(sys.argv[1:]) # SDK-owned commands stay local; all other commands are delegated to Tinybird CLI. - if not normalized_argv or normalized_argv[0] not in {"init", "generate", "migrate"}: + if not normalized_argv or normalized_argv[0] not in _SDK_OWNED_COMMANDS: return _run_installed_tinybird_cli(normalized_argv) parser = create_cli() @@ -142,6 +186,66 @@ def main(argv: list[str] | None = None) -> int: print(f"Completed in {output.format_duration(generate_result.duration_ms)}") return 0 + if args.command == "branch": + if args.branch_command == "list": + list_result = run_branch_list() + if not list_result.success: + output.error(list_result.error or "Failed to list branches") + return 1 + for branch in list_result.branches: + print(branch.get("name") or branch.get("id")) + return 0 + + if args.branch_command == "status": + status_result = run_branch_status(args.name) + if not status_result.success: + output.error(status_result.error or "Failed to get branch status") + return 1 + _print_json(status_result.branch) + return 0 + + delete_result = run_branch_delete(args.name) + if not delete_result.success: + output.error(delete_result.error or "Failed to delete branch") + return 1 + output.success(f"\n✓ Branch '{args.name}' deleted") + return 0 + + if args.command == "info": + info_result = run_info() + if not info_result.success: + output.error(info_result.error or "Info failed") + return 1 + + info_payload = asdict(info_result) + if args.json: + _print_json(info_payload) + return 0 + + output.show_info(info_payload) + return 0 + + if args.command == "preview": + preview_result = run_preview( + { + "dry_run": args.dry_run, + "check": args.check, + "name": args.name, + "dev_mode_override": args.dev_mode, + } + ) + if not preview_result.success: + output.error(preview_result.error or "Preview failed") + return 1 + + output.success( + f"\n✓ Preview completed in {output.format_duration(preview_result.duration_ms)}" + ) + if preview_result.branch: + output.info(f"Branch: {preview_result.branch['name']}") + output.info(f"URL: {preview_result.branch['url']}") + return 0 + migrate_result = run_migrate( { "cwd": args.cwd, diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index fe2c493..70404e2 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -15,6 +15,16 @@ def _mute_output(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(cli_index.output, "error", lambda *args, **kwargs: None) +def _deny_delegation(monkeypatch: pytest.MonkeyPatch, command: str) -> None: + monkeypatch.setattr( + cli_index, + "_run_installed_tinybird_cli", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError(f"should not delegate {command}") + ), + ) + + def _install_fake_tinybird_cli(monkeypatch: pytest.MonkeyPatch, main_impl) -> None: tinybird_pkg = types.ModuleType("tinybird") tinybird_pkg.__path__ = [] @@ -210,3 +220,211 @@ def test_cli_entrypoint_migrate_failure_returns_error(monkeypatch: pytest.Monkey lambda *_args, **_kwargs: {"success": False, "errors": ["boom"]}, ) assert cli_index.main(["migrate", "legacy.datasource"]) == 1 + + +def test_cli_entrypoint_branch_list( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _deny_delegation(monkeypatch, "branch") + monkeypatch.setattr( + cli_index, + "run_branch_list", + lambda *_args, **_kwargs: SimpleNamespace( + success=True, branches=[{"name": "main"}, {"name": "feature_x"}], error=None + ), + ) + assert cli_index.main(["branch", "list"]) == 0 + out = capsys.readouterr().out + assert "main" in out + assert "feature_x" in out + + +def test_cli_entrypoint_branch_list_failure_returns_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _mute_output(monkeypatch) + _deny_delegation(monkeypatch, "branch") + monkeypatch.setattr( + cli_index, + "run_branch_list", + lambda *_args, **_kwargs: SimpleNamespace(success=False, branches=[], error="boom"), + ) + assert cli_index.main(["branch", "list"]) == 1 + + +def test_cli_entrypoint_branch_status( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _deny_delegation(monkeypatch, "branch") + calls: list[str | None] = [] + + def fake_run_branch_status(name: str | None = None, *_args, **_kwargs) -> SimpleNamespace: + calls.append(name) + return SimpleNamespace(success=True, branch={"name": name, "id": "br_1"}, error=None) + + monkeypatch.setattr(cli_index, "run_branch_status", fake_run_branch_status) + + assert cli_index.main(["branch", "status", "feature_x"]) == 0 + assert calls == ["feature_x"] + out = capsys.readouterr().out + assert '"name": "feature_x"' in out + + +def test_cli_entrypoint_branch_status_defaults_to_no_name( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _deny_delegation(monkeypatch, "branch") + calls: list[str | None] = [] + + def fake_run_branch_status(name: str | None = None, *_args, **_kwargs) -> SimpleNamespace: + calls.append(name) + return SimpleNamespace(success=True, branch={"name": "main"}, error=None) + + monkeypatch.setattr(cli_index, "run_branch_status", fake_run_branch_status) + + assert cli_index.main(["branch", "status"]) == 0 + assert calls == [None] + + +def test_cli_entrypoint_branch_delete( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _deny_delegation(monkeypatch, "branch") + calls: list[str] = [] + + def fake_run_branch_delete(name: str, *_args, **_kwargs) -> SimpleNamespace: + calls.append(name) + return SimpleNamespace(success=True, deleted=True, error=None) + + monkeypatch.setattr(cli_index, "run_branch_delete", fake_run_branch_delete) + + assert cli_index.main(["branch", "delete", "feature_x"]) == 0 + assert calls == ["feature_x"] + out = capsys.readouterr().out + assert "feature_x" in out + + +def test_cli_entrypoint_branch_delete_failure_returns_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + _mute_output(monkeypatch) + _deny_delegation(monkeypatch, "branch") + monkeypatch.setattr( + cli_index, + "run_branch_delete", + lambda *_args, **_kwargs: SimpleNamespace(success=False, deleted=False, error="boom"), + ) + assert cli_index.main(["branch", "delete", "feature_x"]) == 1 + + +def test_cli_entrypoint_info( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + from tinybird_sdk.cli.commands.info import InfoCommandResult + + _deny_delegation(monkeypatch, "info") + monkeypatch.setattr( + cli_index, + "run_info", + lambda *_args, **_kwargs: InfoCommandResult( + success=True, + cloud={"base_url": "https://api.tinybird.co"}, + local={"running": False}, + branch={"git_branch": "main"}, + project={"resources": {"datasources": 1, "pipes": 2}}, + branches=[], + ), + ) + assert cli_index.main(["info"]) == 0 + out = capsys.readouterr().out + assert "api.tinybird.co" in out + + +def test_cli_entrypoint_info_json( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + from tinybird_sdk.cli.commands.info import InfoCommandResult + + _deny_delegation(monkeypatch, "info") + monkeypatch.setattr( + cli_index, + "run_info", + lambda *_args, **_kwargs: InfoCommandResult( + success=True, cloud=None, local=None, branch=None, project=None, branches=None + ), + ) + assert cli_index.main(["info", "--json"]) == 0 + out = capsys.readouterr().out + assert '"success": true' in out + + +def test_cli_entrypoint_info_failure_returns_error(monkeypatch: pytest.MonkeyPatch) -> None: + from tinybird_sdk.cli.commands.info import InfoCommandResult + + _mute_output(monkeypatch) + _deny_delegation(monkeypatch, "info") + monkeypatch.setattr( + cli_index, + "run_info", + lambda *_args, **_kwargs: InfoCommandResult( + success=False, + cloud=None, + local=None, + branch=None, + project=None, + branches=None, + error="boom", + ), + ) + assert cli_index.main(["info"]) == 1 + + +def test_cli_entrypoint_preview( + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + _deny_delegation(monkeypatch, "preview") + calls: list[dict] = [] + + def fake_run_preview(options: dict) -> SimpleNamespace: + calls.append(options) + return SimpleNamespace( + success=True, + duration_ms=10, + error=None, + branch={"name": "tmp_ci_feature_x", "url": "https://api.tinybird.co"}, + build=None, + deploy=None, + ) + + monkeypatch.setattr(cli_index, "run_preview", fake_run_preview) + + assert cli_index.main(["preview", "--check", "--name", "custom", "--local"]) == 0 + assert calls == [ + {"dry_run": False, "check": True, "name": "custom", "dev_mode_override": "local"} + ] + out = capsys.readouterr().out + assert "Preview completed" in out + assert "tmp_ci_feature_x" in out + + +def test_cli_entrypoint_preview_failure_returns_error(monkeypatch: pytest.MonkeyPatch) -> None: + _mute_output(monkeypatch) + _deny_delegation(monkeypatch, "preview") + monkeypatch.setattr( + cli_index, + "run_preview", + lambda *_args, **_kwargs: SimpleNamespace( + success=False, duration_ms=1, error="boom", branch=None, build=None, deploy=None + ), + ) + assert cli_index.main(["preview"]) == 1 + + +def test_cli_entrypoint_preview_rejects_local_and_branch_together() -> None: + with pytest.raises(SystemExit): + cli_index.main(["preview", "--local", "--branch"]) + + +def test_cli_entrypoint_branch_requires_subcommand() -> None: + with pytest.raises(SystemExit): + cli_index.main(["branch"]) From 96f22128b214c42800938fa1e741d7c2ccd81093 Mon Sep 17 00:00:00 2001 From: Tommy Healy Date: Fri, 2 Oct 2026 11:19:20 +0200 Subject: [PATCH 2/2] Revert native branch/info/preview wiring; keep delegating to the installed Tinybird CLI branch, info, and preview already work correctly via the real tb CLI delegation path (main's SDK_OWNED_COMMANDS policy). Reimplementing them against the raw API duplicates logic the installed CLI already owns and can drift from its actual behavior. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 6 - README.md | 13 -- src/tinybird_sdk/cli/index.py | 106 +---------------- tests/test_cli_entrypoint.py | 218 ---------------------------------- 4 files changed, 1 insertion(+), 342 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cdf133..0c1ecb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Changed - -- `tinybird branch` (`list`/`status`/`delete`), `tinybird info`, and `tinybird preview` now run the SDK's own native implementations instead of delegating to the installed Forward `tb` package, so these commands work without `tb` installed alongside the SDK. - ## [0.4.0] - 2026-06-29 ### Added diff --git a/README.md b/README.md index 78b3bca..e42f34b 100644 --- a/README.md +++ b/README.md @@ -429,19 +429,6 @@ tinybird info tinybird info --json ``` -### `tinybird preview` - -Builds the project and deploys it to a temporary preview branch (named `tmp_ci_` by default), mirroring what CI preview environments do. - -```bash -tinybird preview -tinybird preview --dry-run -tinybird preview --check -tinybird preview --name my-preview -tinybird preview --local -tinybird preview --branch -``` - ## Configuration Create a `tinybird.config.json` (or `tinybird.config.py` / `tinybird_config.py` for dynamic logic) in your project root: diff --git a/src/tinybird_sdk/cli/index.py b/src/tinybird_sdk/cli/index.py index 72a525b..1183961 100644 --- a/src/tinybird_sdk/cli/index.py +++ b/src/tinybird_sdk/cli/index.py @@ -5,16 +5,11 @@ import json import sys -from .commands.branch import run_branch_delete, run_branch_list, run_branch_status from .commands.generate import run_generate -from .commands.info import run_info from .commands.init import run_init from .commands.migrate import run_migrate -from .commands.preview import run_preview from .output import output -_SDK_OWNED_COMMANDS = {"init", "generate", "migrate", "branch", "info", "preview"} - def _print_json(payload: object) -> None: print(json.dumps(payload, indent=2, default=str)) @@ -86,45 +81,6 @@ def create_cli() -> argparse.ArgumentParser: ) migrate_cmd.add_argument("--json", action="store_true", help="Print migration result as JSON") - branch_cmd = sub.add_parser("branch", help="Manage Tinybird branches") - branch_sub = branch_cmd.add_subparsers(dest="branch_command", required=True) - branch_sub.add_parser("list", help="List branches") - branch_status_cmd = branch_sub.add_parser("status", help="Show a branch's status") - branch_status_cmd.add_argument( - "name", nargs="?", help="Branch name (defaults to the current project branch)" - ) - branch_delete_cmd = branch_sub.add_parser("delete", help="Delete a branch") - branch_delete_cmd.add_argument("name", help="Branch name to delete") - - info_cmd = sub.add_parser("info", help="Show project and workspace info") - info_cmd.add_argument("--json", action="store_true", help="Print info as JSON") - - preview_cmd = sub.add_parser( - "preview", help="Build and deploy resources to a temporary preview branch" - ) - preview_cmd.add_argument( - "--dry-run", action="store_true", help="Build without creating a preview branch" - ) - preview_cmd.add_argument( - "--check", action="store_true", help="Validate the preview deploy without applying it" - ) - preview_cmd.add_argument("--name", help="Preview branch name override") - preview_mode = preview_cmd.add_mutually_exclusive_group() - preview_mode.add_argument( - "--local", - action="store_const", - dest="dev_mode", - const="local", - help="Preview against Tinybird Local", - ) - preview_mode.add_argument( - "--branch", - action="store_const", - dest="dev_mode", - const="branch", - help="Preview against a cloud branch", - ) - return parser @@ -132,7 +88,7 @@ def main(argv: list[str] | None = None) -> int: normalized_argv = list(argv) if argv is not None else list(sys.argv[1:]) # SDK-owned commands stay local; all other commands are delegated to Tinybird CLI. - if not normalized_argv or normalized_argv[0] not in _SDK_OWNED_COMMANDS: + if not normalized_argv or normalized_argv[0] not in {"init", "generate", "migrate"}: return _run_installed_tinybird_cli(normalized_argv) parser = create_cli() @@ -186,66 +142,6 @@ def main(argv: list[str] | None = None) -> int: print(f"Completed in {output.format_duration(generate_result.duration_ms)}") return 0 - if args.command == "branch": - if args.branch_command == "list": - list_result = run_branch_list() - if not list_result.success: - output.error(list_result.error or "Failed to list branches") - return 1 - for branch in list_result.branches: - print(branch.get("name") or branch.get("id")) - return 0 - - if args.branch_command == "status": - status_result = run_branch_status(args.name) - if not status_result.success: - output.error(status_result.error or "Failed to get branch status") - return 1 - _print_json(status_result.branch) - return 0 - - delete_result = run_branch_delete(args.name) - if not delete_result.success: - output.error(delete_result.error or "Failed to delete branch") - return 1 - output.success(f"\n✓ Branch '{args.name}' deleted") - return 0 - - if args.command == "info": - info_result = run_info() - if not info_result.success: - output.error(info_result.error or "Info failed") - return 1 - - info_payload = asdict(info_result) - if args.json: - _print_json(info_payload) - return 0 - - output.show_info(info_payload) - return 0 - - if args.command == "preview": - preview_result = run_preview( - { - "dry_run": args.dry_run, - "check": args.check, - "name": args.name, - "dev_mode_override": args.dev_mode, - } - ) - if not preview_result.success: - output.error(preview_result.error or "Preview failed") - return 1 - - output.success( - f"\n✓ Preview completed in {output.format_duration(preview_result.duration_ms)}" - ) - if preview_result.branch: - output.info(f"Branch: {preview_result.branch['name']}") - output.info(f"URL: {preview_result.branch['url']}") - return 0 - migrate_result = run_migrate( { "cwd": args.cwd, diff --git a/tests/test_cli_entrypoint.py b/tests/test_cli_entrypoint.py index 70404e2..fe2c493 100644 --- a/tests/test_cli_entrypoint.py +++ b/tests/test_cli_entrypoint.py @@ -15,16 +15,6 @@ def _mute_output(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(cli_index.output, "error", lambda *args, **kwargs: None) -def _deny_delegation(monkeypatch: pytest.MonkeyPatch, command: str) -> None: - monkeypatch.setattr( - cli_index, - "_run_installed_tinybird_cli", - lambda *_args, **_kwargs: (_ for _ in ()).throw( - AssertionError(f"should not delegate {command}") - ), - ) - - def _install_fake_tinybird_cli(monkeypatch: pytest.MonkeyPatch, main_impl) -> None: tinybird_pkg = types.ModuleType("tinybird") tinybird_pkg.__path__ = [] @@ -220,211 +210,3 @@ def test_cli_entrypoint_migrate_failure_returns_error(monkeypatch: pytest.Monkey lambda *_args, **_kwargs: {"success": False, "errors": ["boom"]}, ) assert cli_index.main(["migrate", "legacy.datasource"]) == 1 - - -def test_cli_entrypoint_branch_list( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _deny_delegation(monkeypatch, "branch") - monkeypatch.setattr( - cli_index, - "run_branch_list", - lambda *_args, **_kwargs: SimpleNamespace( - success=True, branches=[{"name": "main"}, {"name": "feature_x"}], error=None - ), - ) - assert cli_index.main(["branch", "list"]) == 0 - out = capsys.readouterr().out - assert "main" in out - assert "feature_x" in out - - -def test_cli_entrypoint_branch_list_failure_returns_error( - monkeypatch: pytest.MonkeyPatch, -) -> None: - _mute_output(monkeypatch) - _deny_delegation(monkeypatch, "branch") - monkeypatch.setattr( - cli_index, - "run_branch_list", - lambda *_args, **_kwargs: SimpleNamespace(success=False, branches=[], error="boom"), - ) - assert cli_index.main(["branch", "list"]) == 1 - - -def test_cli_entrypoint_branch_status( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _deny_delegation(monkeypatch, "branch") - calls: list[str | None] = [] - - def fake_run_branch_status(name: str | None = None, *_args, **_kwargs) -> SimpleNamespace: - calls.append(name) - return SimpleNamespace(success=True, branch={"name": name, "id": "br_1"}, error=None) - - monkeypatch.setattr(cli_index, "run_branch_status", fake_run_branch_status) - - assert cli_index.main(["branch", "status", "feature_x"]) == 0 - assert calls == ["feature_x"] - out = capsys.readouterr().out - assert '"name": "feature_x"' in out - - -def test_cli_entrypoint_branch_status_defaults_to_no_name( - monkeypatch: pytest.MonkeyPatch, -) -> None: - _deny_delegation(monkeypatch, "branch") - calls: list[str | None] = [] - - def fake_run_branch_status(name: str | None = None, *_args, **_kwargs) -> SimpleNamespace: - calls.append(name) - return SimpleNamespace(success=True, branch={"name": "main"}, error=None) - - monkeypatch.setattr(cli_index, "run_branch_status", fake_run_branch_status) - - assert cli_index.main(["branch", "status"]) == 0 - assert calls == [None] - - -def test_cli_entrypoint_branch_delete( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _deny_delegation(monkeypatch, "branch") - calls: list[str] = [] - - def fake_run_branch_delete(name: str, *_args, **_kwargs) -> SimpleNamespace: - calls.append(name) - return SimpleNamespace(success=True, deleted=True, error=None) - - monkeypatch.setattr(cli_index, "run_branch_delete", fake_run_branch_delete) - - assert cli_index.main(["branch", "delete", "feature_x"]) == 0 - assert calls == ["feature_x"] - out = capsys.readouterr().out - assert "feature_x" in out - - -def test_cli_entrypoint_branch_delete_failure_returns_error( - monkeypatch: pytest.MonkeyPatch, -) -> None: - _mute_output(monkeypatch) - _deny_delegation(monkeypatch, "branch") - monkeypatch.setattr( - cli_index, - "run_branch_delete", - lambda *_args, **_kwargs: SimpleNamespace(success=False, deleted=False, error="boom"), - ) - assert cli_index.main(["branch", "delete", "feature_x"]) == 1 - - -def test_cli_entrypoint_info( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - from tinybird_sdk.cli.commands.info import InfoCommandResult - - _deny_delegation(monkeypatch, "info") - monkeypatch.setattr( - cli_index, - "run_info", - lambda *_args, **_kwargs: InfoCommandResult( - success=True, - cloud={"base_url": "https://api.tinybird.co"}, - local={"running": False}, - branch={"git_branch": "main"}, - project={"resources": {"datasources": 1, "pipes": 2}}, - branches=[], - ), - ) - assert cli_index.main(["info"]) == 0 - out = capsys.readouterr().out - assert "api.tinybird.co" in out - - -def test_cli_entrypoint_info_json( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - from tinybird_sdk.cli.commands.info import InfoCommandResult - - _deny_delegation(monkeypatch, "info") - monkeypatch.setattr( - cli_index, - "run_info", - lambda *_args, **_kwargs: InfoCommandResult( - success=True, cloud=None, local=None, branch=None, project=None, branches=None - ), - ) - assert cli_index.main(["info", "--json"]) == 0 - out = capsys.readouterr().out - assert '"success": true' in out - - -def test_cli_entrypoint_info_failure_returns_error(monkeypatch: pytest.MonkeyPatch) -> None: - from tinybird_sdk.cli.commands.info import InfoCommandResult - - _mute_output(monkeypatch) - _deny_delegation(monkeypatch, "info") - monkeypatch.setattr( - cli_index, - "run_info", - lambda *_args, **_kwargs: InfoCommandResult( - success=False, - cloud=None, - local=None, - branch=None, - project=None, - branches=None, - error="boom", - ), - ) - assert cli_index.main(["info"]) == 1 - - -def test_cli_entrypoint_preview( - monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] -) -> None: - _deny_delegation(monkeypatch, "preview") - calls: list[dict] = [] - - def fake_run_preview(options: dict) -> SimpleNamespace: - calls.append(options) - return SimpleNamespace( - success=True, - duration_ms=10, - error=None, - branch={"name": "tmp_ci_feature_x", "url": "https://api.tinybird.co"}, - build=None, - deploy=None, - ) - - monkeypatch.setattr(cli_index, "run_preview", fake_run_preview) - - assert cli_index.main(["preview", "--check", "--name", "custom", "--local"]) == 0 - assert calls == [ - {"dry_run": False, "check": True, "name": "custom", "dev_mode_override": "local"} - ] - out = capsys.readouterr().out - assert "Preview completed" in out - assert "tmp_ci_feature_x" in out - - -def test_cli_entrypoint_preview_failure_returns_error(monkeypatch: pytest.MonkeyPatch) -> None: - _mute_output(monkeypatch) - _deny_delegation(monkeypatch, "preview") - monkeypatch.setattr( - cli_index, - "run_preview", - lambda *_args, **_kwargs: SimpleNamespace( - success=False, duration_ms=1, error="boom", branch=None, build=None, deploy=None - ), - ) - assert cli_index.main(["preview"]) == 1 - - -def test_cli_entrypoint_preview_rejects_local_and_branch_together() -> None: - with pytest.raises(SystemExit): - cli_index.main(["preview", "--local", "--branch"]) - - -def test_cli_entrypoint_branch_requires_subcommand() -> None: - with pytest.raises(SystemExit): - cli_index.main(["branch"])