From d2310f3dca26c4c66c48e51c8a0807fc47e3301e Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Thu, 23 Apr 2026 20:27:49 +0200 Subject: [PATCH 1/7] add tinybird.config.json support --- src/tinybird_sdk/api/__init__.py | 1 + src/tinybird_sdk/api/branches.py | 28 +++++++--- src/tinybird_sdk/cli/commands/build.py | 10 +++- src/tinybird_sdk/cli/commands/preview.py | 13 ++++- src/tinybird_sdk/cli/config.py | 38 ++++++++++++- src/tinybird_sdk/cli/config_types.py | 9 ++++ tests/test_api_branches_options.py | 69 ++++++++++++++++++++++++ tests/test_cli_branch_config.py | 56 +++++++++++++++++++ 8 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 tests/test_api_branches_options.py create mode 100644 tests/test_cli_branch_config.py diff --git a/src/tinybird_sdk/api/__init__.py b/src/tinybird_sdk/api/__init__.py index 799ffbe..96832c5 100644 --- a/src/tinybird_sdk/api/__init__.py +++ b/src/tinybird_sdk/api/__init__.py @@ -13,6 +13,7 @@ ) from .branches import ( TinybirdBranch, + CreateBranchOptions, BranchApiConfig, BranchApiError, create_branch, diff --git a/src/tinybird_sdk/api/branches.py b/src/tinybird_sdk/api/branches.py index 64f19e8..c7efaaa 100644 --- a/src/tinybird_sdk/api/branches.py +++ b/src/tinybird_sdk/api/branches.py @@ -1,12 +1,13 @@ from __future__ import annotations import time -from dataclasses import asdict -from dataclasses import dataclass +from dataclasses import asdict, dataclass from typing import Any from urllib.parse import urlencode from .fetcher import tinybird_fetch +LAST_PARTITION = "last_partition" +ALL_PARTITIONS = "all_partitions" LAST_PARTITION = "last_partition" @@ -25,6 +26,12 @@ class TinybirdBranch: token: str | None = None +@dataclass(frozen=True, slots=True) +class CreateBranchOptions: + last_partition: bool = False + all_partitions: bool = False + + class BranchApiError(Exception): def __init__(self, message: str, status: int, body: Any = None): super().__init__(message) @@ -68,9 +75,16 @@ def _poll_job( raise BranchApiError(f"Job '{job_id}' timed out after {max_attempts} attempts", 408) -def create_branch(config: BranchApiConfig | dict[str, Any], name: str) -> TinybirdBranch: +def create_branch( + config: BranchApiConfig | dict[str, Any], name: str, options: CreateBranchOptions | None = None +) -> TinybirdBranch: normalized = config if isinstance(config, BranchApiConfig) else BranchApiConfig(**config) - url = f"{normalized.base_url.rstrip('/')}/v1/environments?{urlencode({'name': name})}" + params = {"name": name} + if options and options.last_partition: + params["data"] = LAST_PARTITION + elif options and options.all_partitions: + params["data"] = ALL_PARTITIONS + url = f"{normalized.base_url.rstrip('/')}/v1/environments?{urlencode(params)}" response = tinybird_fetch(url, method="POST", headers=_headers(normalized.token)) if not response.ok: @@ -152,14 +166,16 @@ def branch_exists(config: BranchApiConfig | dict[str, Any], name: str) -> bool: return any(branch.name == name for branch in branches) -def get_or_create_branch(config: BranchApiConfig | dict[str, Any], name: str) -> dict[str, Any]: +def get_or_create_branch( + config: BranchApiConfig | dict[str, Any], name: str, options: CreateBranchOptions | None = None +) -> dict[str, Any]: normalized = config if isinstance(config, BranchApiConfig) else BranchApiConfig(**config) try: branch = get_branch(normalized, name) return {**asdict(branch), "was_created": False} except BranchApiError as error: if error.status == 404: - branch = create_branch(normalized, name) + branch = create_branch(normalized, name, options=options) return {**asdict(branch), "was_created": True} raise diff --git a/src/tinybird_sdk/cli/commands/build.py b/src/tinybird_sdk/cli/commands/build.py index 457a6d3..ccdf8d6 100644 --- a/src/tinybird_sdk/cli/commands/build.py +++ b/src/tinybird_sdk/cli/commands/build.py @@ -5,7 +5,7 @@ import time from typing import Any -from ...api.branches import get_or_create_branch +from ...api.branches import CreateBranchOptions, get_or_create_branch from ...api.build import build_to_tinybird from ...api.dashboard import get_branch_dashboard_url, get_local_dashboard_url from ...api.local import ( @@ -138,9 +138,17 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu if not normalized.token_override: try: + branch_options = None + branch_value = config.get("branch_data_on_create") + if branch_value and config.get("dev_mode") != "local": + branch_options = CreateBranchOptions( + last_partition=(branch_value == "last_partition"), + all_partitions=(branch_value == "all_partitions"), + ) branch = get_or_create_branch( {"base_url": config["base_url"], "token": config["token"]}, config["tinybird_branch"], + options=branch_options, ) if not branch.get("token"): return BuildCommandResult( diff --git a/src/tinybird_sdk/cli/commands/preview.py b/src/tinybird_sdk/cli/commands/preview.py index 9a52a31..e304e9b 100644 --- a/src/tinybird_sdk/cli/commands/preview.py +++ b/src/tinybird_sdk/cli/commands/preview.py @@ -5,7 +5,7 @@ import time from typing import Any -from ...api.branches import create_branch, delete_branch, get_branch +from ...api.branches import CreateBranchOptions, create_branch, delete_branch, get_branch from ...api.build import build_to_tinybird from ...api.deploy import deploy_to_main from ...api.local import LocalNotRunningError, get_local_tokens, get_or_create_local_workspace @@ -151,8 +151,17 @@ def run_preview( except Exception: pass + branch_options = None + branch_value = config.get("branch_data_on_create") + if branch_value and config.get("dev_mode") != "local": + branch_options = CreateBranchOptions( + last_partition=(branch_value == "last_partition"), + all_partitions=(branch_value == "all_partitions"), + ) branch = create_branch( - {"base_url": config["base_url"], "token": config["token"]}, preview_branch_name + {"base_url": config["base_url"], "token": config["token"]}, + preview_branch_name, + options=branch_options, ) except Exception as error: return PreviewCommandResult( diff --git a/src/tinybird_sdk/cli/config.py b/src/tinybird_sdk/cli/config.py index 49a0b59..f5bdec2 100644 --- a/src/tinybird_sdk/cli/config.py +++ b/src/tinybird_sdk/cli/config.py @@ -8,7 +8,12 @@ from typing import Any from .config_loader import load_config_file -from .config_types import DevMode, TinybirdConfig +from .config_types import ( + BRANCH_DATA_ON_CREATE_VALUES, + BranchDataOnCreateMode, + DevMode, + TinybirdConfig, +) from .git import get_current_git_branch, get_tinybird_branch_name, is_main_branch DEFAULT_BASE_URL = "https://api.tinybird.co" @@ -34,6 +39,26 @@ class ResolvedConfig: tinybird_branch: str | None is_main_branch: bool dev_mode: DevMode + branch_data_on_create: str | None + + +def _resolve_branch_data_on_create(raw: dict[str, Any]) -> str | None: + value = raw.get("branch_data_on_create") + if value is None: + return BranchDataOnCreateMode.LAST_PARTITION.value + if not isinstance(value, str): + raise ValueError("branch_data_on_create must be a string.") + + mode = value.strip().lower() + if not mode: + return BranchDataOnCreateMode.LAST_PARTITION.value + if mode not in BRANCH_DATA_ON_CREATE_VALUES: + raise ValueError( + f"Invalid branch_data_on_create '{value}'. Allowed values are: {', '.join(BRANCH_DATA_ON_CREATE_VALUES)}." + ) + if mode == BranchDataOnCreateMode.ALL_PARTITIONS.value: + raise ValueError("branch_data_on_create 'all_partitions' is currently disabled.") + return mode def load_env_files(directory: str) -> None: @@ -176,6 +201,14 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig: or DEFAULT_BASE_URL ) + branch_data_on_create = _resolve_branch_data_on_create(asdict(config)) + dev_mode = config.dev_mode or "branch" + if branch_data_on_create and dev_mode == "local": + print( + "Warning: branch_data_on_create is set in tinybird.config.json but dev_mode='local'. " + "Branch data settings only apply to cloud branches." + ) + return ResolvedConfig( include=include, token=token, @@ -185,7 +218,8 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig: git_branch=get_current_git_branch(), tinybird_branch=get_tinybird_branch_name(), is_main_branch=is_main_branch(), - dev_mode=config.dev_mode or "branch", + dev_mode=dev_mode, + branch_data_on_create=branch_data_on_create, ) diff --git a/src/tinybird_sdk/cli/config_types.py b/src/tinybird_sdk/cli/config_types.py index 56c02e4..06da69f 100644 --- a/src/tinybird_sdk/cli/config_types.py +++ b/src/tinybird_sdk/cli/config_types.py @@ -1,9 +1,17 @@ from __future__ import annotations from dataclasses import dataclass, field +from enum import StrEnum from typing import Literal DevMode = Literal["branch", "local"] +BranchDataOnCreate = Literal["last_partition", "all_partitions"] +BRANCH_DATA_ON_CREATE_VALUES: tuple[str, ...] = ("last_partition", "all_partitions") + + +class BranchDataOnCreateMode(StrEnum): + LAST_PARTITION = "last_partition" + ALL_PARTITIONS = "all_partitions" @dataclass(frozen=True, slots=True) @@ -13,3 +21,4 @@ class TinybirdConfig: token: str | None = None base_url: str | None = None dev_mode: DevMode | None = None + branch_data_on_create: str | None = None diff --git a/tests/test_api_branches_options.py b/tests/test_api_branches_options.py new file mode 100644 index 0000000..80b3ff8 --- /dev/null +++ b/tests/test_api_branches_options.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import Any +from urllib.parse import parse_qs, urlparse + +import pytest + +import tinybird_sdk.api.branches as branches_module +from tinybird_sdk.api.branches import CreateBranchOptions, create_branch + + +class _FakeResponse: + def __init__(self, status_code: int, payload: dict[str, Any]): + self.status_code = status_code + self._payload = payload + self.text = "" + + @property + def ok(self) -> bool: + return 200 <= self.status_code < 300 + + def json(self) -> dict[str, Any]: + return self._payload + + +def test_create_branch_uses_last_partition_data_query(monkeypatch: pytest.MonkeyPatch) -> None: + called_urls: list[str] = [] + + def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: + called_urls.append(url) + if "/v1/environments?" in url: + return _FakeResponse(200, {"job": {"id": "job-1"}}) + if "/v0/jobs/" in url: + return _FakeResponse(200, {"status": "done"}) + return _FakeResponse(200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"}) + + monkeypatch.setattr(branches_module, "tinybird_fetch", fake_fetch) + create_branch( + {"base_url": "https://api.tinybird.co", "token": "p.test"}, + "x", + options=CreateBranchOptions(last_partition=True), + ) + + parsed = urlparse(called_urls[0]) + query = parse_qs(parsed.query) + assert parsed.path == "/v1/environments" + assert query == {"name": ["x"], "data": ["last_partition"]} + + +def test_create_branch_without_options_keeps_default_query(monkeypatch: pytest.MonkeyPatch) -> None: + called_urls: list[str] = [] + + def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: + called_urls.append(url) + if "/v1/environments?" in url: + return _FakeResponse(200, {"job": {"id": "job-1"}}) + if "/v0/jobs/" in url: + return _FakeResponse(200, {"status": "done"}) + return _FakeResponse(200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"}) + + monkeypatch.setattr(branches_module, "tinybird_fetch", fake_fetch) + create_branch({"base_url": "https://api.tinybird.co", "token": "p.test"}, "x") + + parsed = urlparse(called_urls[0]) + query = parse_qs(parsed.query) + assert parsed.path == "/v1/environments" + assert query == {"name": ["x"]} + assert "data" not in query + assert "ignore_datasources" not in query diff --git a/tests/test_cli_branch_config.py b/tests/test_cli_branch_config.py new file mode 100644 index 0000000..ba522cf --- /dev/null +++ b/tests/test_cli_branch_config.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from tinybird_sdk.cli.config import _resolve_branch_data_on_create, load_config + + +def test_branch_data_on_create_last_partition() -> None: + assert _resolve_branch_data_on_create({"branch_data_on_create": "last_partition"}) == "last_partition" + + +def test_branch_data_on_create_missing_returns_none() -> None: + assert _resolve_branch_data_on_create({}) == "last_partition" + + +def test_branch_data_on_create_empty_defaults_to_last_partition() -> None: + assert _resolve_branch_data_on_create({"branch_data_on_create": " "}) == "last_partition" + + +def test_branch_data_on_create_all_partitions_disabled() -> None: + with pytest.raises(ValueError, match="disabled"): + _resolve_branch_data_on_create({"branch_data_on_create": "all_partitions"}) + + +def test_branch_data_on_create_invalid_value() -> None: + with pytest.raises(ValueError, match="Invalid branch_data_on_create"): + _resolve_branch_data_on_create({"branch_data_on_create": "invalid"}) + + +def test_branch_data_on_create_non_string() -> None: + with pytest.raises(ValueError, match="must be a string"): + _resolve_branch_data_on_create({"branch_data_on_create": 1}) + + +def test_load_config_warns_when_local_mode_uses_branch_data(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "tinybird.config.json").write_text( + json.dumps( + { + "include": ["lib/datasources.py"], + "token": "p.test", + "base_url": "https://api.tinybird.co", + "dev_mode": "local", + "branch_data_on_create": "last_partition", + } + ), + encoding="utf-8", + ) + + load_config(str(project)) + captured = capsys.readouterr() + assert "branch_data_on_create is set" in captured.out From d20170bf57dc545a37d6d5e180e4b860b4f31ce9 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Tue, 9 Jun 2026 12:15:55 +0200 Subject: [PATCH 2/7] improve workflow --- src/tinybird_sdk/api/branches.py | 10 ++-- src/tinybird_sdk/cli/commands/build.py | 3 +- src/tinybird_sdk/cli/commands/clear.py | 11 +++- src/tinybird_sdk/cli/commands/preview.py | 3 +- src/tinybird_sdk/cli/config.py | 35 ++++++------ src/tinybird_sdk/cli/config_types.py | 9 ++- src/tinybird_sdk/client/base.py | 8 ++- tests/test_api_branches_options.py | 24 +++++++- tests/test_cli_branch_config.py | 71 ++++++++++++++++++------ tests/test_client_parity.py | 16 ++++-- 10 files changed, 131 insertions(+), 59 deletions(-) diff --git a/src/tinybird_sdk/api/branches.py b/src/tinybird_sdk/api/branches.py index c7efaaa..e7bee59 100644 --- a/src/tinybird_sdk/api/branches.py +++ b/src/tinybird_sdk/api/branches.py @@ -7,7 +7,6 @@ from .fetcher import tinybird_fetch LAST_PARTITION = "last_partition" -ALL_PARTITIONS = "all_partitions" LAST_PARTITION = "last_partition" @@ -29,7 +28,6 @@ class TinybirdBranch: @dataclass(frozen=True, slots=True) class CreateBranchOptions: last_partition: bool = False - all_partitions: bool = False class BranchApiError(Exception): @@ -82,8 +80,6 @@ def create_branch( params = {"name": name} if options and options.last_partition: params["data"] = LAST_PARTITION - elif options and options.all_partitions: - params["data"] = ALL_PARTITIONS url = f"{normalized.base_url.rstrip('/')}/v1/environments?{urlencode(params)}" response = tinybird_fetch(url, method="POST", headers=_headers(normalized.token)) @@ -180,7 +176,9 @@ def get_or_create_branch( raise -def clear_branch(config: BranchApiConfig | dict[str, Any], name: str) -> TinybirdBranch: +def clear_branch( + config: BranchApiConfig | dict[str, Any], name: str, options: CreateBranchOptions | None = None +) -> TinybirdBranch: normalized = config if isinstance(config, BranchApiConfig) else BranchApiConfig(**config) delete_branch(normalized, name) - return create_branch(normalized, name) + return create_branch(normalized, name, options=options) diff --git a/src/tinybird_sdk/cli/commands/build.py b/src/tinybird_sdk/cli/commands/build.py index ccdf8d6..0dfe265 100644 --- a/src/tinybird_sdk/cli/commands/build.py +++ b/src/tinybird_sdk/cli/commands/build.py @@ -139,11 +139,10 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu if not normalized.token_override: try: branch_options = None - branch_value = config.get("branch_data_on_create") + branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": branch_options = CreateBranchOptions( last_partition=(branch_value == "last_partition"), - all_partitions=(branch_value == "all_partitions"), ) branch = get_or_create_branch( {"base_url": config["base_url"], "token": config["token"]}, diff --git a/src/tinybird_sdk/cli/commands/clear.py b/src/tinybird_sdk/cli/commands/clear.py index 176a446..600a276 100644 --- a/src/tinybird_sdk/cli/commands/clear.py +++ b/src/tinybird_sdk/cli/commands/clear.py @@ -5,7 +5,7 @@ import time from typing import Any -from ...api.branches import clear_branch +from ...api.branches import CreateBranchOptions, clear_branch from ...api.local import clear_local_workspace, get_local_tokens, get_local_workspace_name from ..config import load_config_async @@ -60,8 +60,15 @@ def run_clear(options: ClearCommandOptions | dict[str, Any] | None = None) -> Cl duration_ms=int(time.time() * 1000) - start, ) + branch_options = None + branch_value = config.get("branch_data_mode") + if branch_value and config.get("dev_mode") != "local": + branch_options = CreateBranchOptions(last_partition=(branch_value == "last_partition")) + clear_branch( - {"base_url": config["base_url"], "token": config["token"]}, config["tinybird_branch"] + {"base_url": config["base_url"], "token": config["token"]}, + config["tinybird_branch"], + options=branch_options, ) return ClearResult( success=True, diff --git a/src/tinybird_sdk/cli/commands/preview.py b/src/tinybird_sdk/cli/commands/preview.py index e304e9b..0200efe 100644 --- a/src/tinybird_sdk/cli/commands/preview.py +++ b/src/tinybird_sdk/cli/commands/preview.py @@ -152,11 +152,10 @@ def run_preview( pass branch_options = None - branch_value = config.get("branch_data_on_create") + branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": branch_options = CreateBranchOptions( last_partition=(branch_value == "last_partition"), - all_partitions=(branch_value == "all_partitions"), ) branch = create_branch( {"base_url": config["base_url"], "token": config["token"]}, diff --git a/src/tinybird_sdk/cli/config.py b/src/tinybird_sdk/cli/config.py index f5bdec2..990ff7b 100644 --- a/src/tinybird_sdk/cli/config.py +++ b/src/tinybird_sdk/cli/config.py @@ -9,8 +9,8 @@ from .config_loader import load_config_file from .config_types import ( - BRANCH_DATA_ON_CREATE_VALUES, - BranchDataOnCreateMode, + BRANCH_DATA_MODE_VALUES, + BranchDataModeEnum, DevMode, TinybirdConfig, ) @@ -39,26 +39,27 @@ class ResolvedConfig: tinybird_branch: str | None is_main_branch: bool dev_mode: DevMode - branch_data_on_create: str | None + branch_data_mode: str | None -def _resolve_branch_data_on_create(raw: dict[str, Any]) -> str | None: - value = raw.get("branch_data_on_create") +def _resolve_branch_data_mode(raw: dict[str, Any]) -> tuple[str | None, bool]: + if "branch_data_on_create" in raw: + raise ValueError("`branch_data_on_create` has been renamed to `branch_data_mode`.") + + value = raw.get("branch_data_mode") if value is None: - return BranchDataOnCreateMode.LAST_PARTITION.value + return BranchDataModeEnum.LAST_PARTITION.value, False if not isinstance(value, str): - raise ValueError("branch_data_on_create must be a string.") + raise ValueError("branch_data_mode must be a string.") mode = value.strip().lower() if not mode: - return BranchDataOnCreateMode.LAST_PARTITION.value - if mode not in BRANCH_DATA_ON_CREATE_VALUES: + return BranchDataModeEnum.LAST_PARTITION.value, False + if mode not in BRANCH_DATA_MODE_VALUES: raise ValueError( - f"Invalid branch_data_on_create '{value}'. Allowed values are: {', '.join(BRANCH_DATA_ON_CREATE_VALUES)}." + f"Invalid branch_data_mode '{value}'. Allowed values are: {', '.join(BRANCH_DATA_MODE_VALUES)}." ) - if mode == BranchDataOnCreateMode.ALL_PARTITIONS.value: - raise ValueError("branch_data_on_create 'all_partitions' is currently disabled.") - return mode + return mode, True def load_env_files(directory: str) -> None: @@ -201,11 +202,11 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig: or DEFAULT_BASE_URL ) - branch_data_on_create = _resolve_branch_data_on_create(asdict(config)) + branch_data_mode, branch_data_mode_explicit = _resolve_branch_data_mode(asdict(config)) dev_mode = config.dev_mode or "branch" - if branch_data_on_create and dev_mode == "local": + if branch_data_mode_explicit and branch_data_mode and dev_mode == "local": print( - "Warning: branch_data_on_create is set in tinybird.config.json but dev_mode='local'. " + "Warning: branch_data_mode is set in tinybird.config.json but dev_mode='local'. " "Branch data settings only apply to cloud branches." ) @@ -219,7 +220,7 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig: tinybird_branch=get_tinybird_branch_name(), is_main_branch=is_main_branch(), dev_mode=dev_mode, - branch_data_on_create=branch_data_on_create, + branch_data_mode=branch_data_mode, ) diff --git a/src/tinybird_sdk/cli/config_types.py b/src/tinybird_sdk/cli/config_types.py index 06da69f..8e998a2 100644 --- a/src/tinybird_sdk/cli/config_types.py +++ b/src/tinybird_sdk/cli/config_types.py @@ -5,13 +5,12 @@ from typing import Literal DevMode = Literal["branch", "local"] -BranchDataOnCreate = Literal["last_partition", "all_partitions"] -BRANCH_DATA_ON_CREATE_VALUES: tuple[str, ...] = ("last_partition", "all_partitions") +BranchDataMode = Literal["last_partition"] +BRANCH_DATA_MODE_VALUES: tuple[str, ...] = ("last_partition",) -class BranchDataOnCreateMode(StrEnum): +class BranchDataModeEnum(StrEnum): LAST_PARTITION = "last_partition" - ALL_PARTITIONS = "all_partitions" @dataclass(frozen=True, slots=True) @@ -21,4 +20,4 @@ class TinybirdConfig: token: str | None = None base_url: str | None = None dev_mode: DevMode | None = None - branch_data_on_create: str | None = None + branch_data_mode: str | None = None diff --git a/src/tinybird_sdk/client/base.py b/src/tinybird_sdk/client/base.py index 6f0fb36..d97b819 100644 --- a/src/tinybird_sdk/client/base.py +++ b/src/tinybird_sdk/client/base.py @@ -4,7 +4,7 @@ from typing import Any, cast from ..api.api import TinybirdApi, TinybirdApiError -from ..api.branches import get_or_create_branch +from ..api.branches import CreateBranchOptions, get_or_create_branch from ..cli.config import load_config_async from .preview import get_preview_branch_name, is_preview_environment from .tokens import TokensNamespace @@ -159,12 +159,18 @@ def _resolve_branch_context(self) -> ClientContext: ) branch_name = config["tinybird_branch"] + branch_options = None + branch_value = config.get("branch_data_mode") + if branch_value and config.get("dev_mode") != "local": + branch_options = CreateBranchOptions(last_partition=(branch_value == "last_partition")) + branch = get_or_create_branch( { "base_url": self._config["base_url"], "token": self._config["token"], }, branch_name, + options=branch_options, ) if not branch.get("token"): diff --git a/tests/test_api_branches_options.py b/tests/test_api_branches_options.py index 80b3ff8..c722ef4 100644 --- a/tests/test_api_branches_options.py +++ b/tests/test_api_branches_options.py @@ -6,7 +6,7 @@ import pytest import tinybird_sdk.api.branches as branches_module -from tinybird_sdk.api.branches import CreateBranchOptions, create_branch +from tinybird_sdk.api.branches import CreateBranchOptions, clear_branch, create_branch class _FakeResponse: @@ -67,3 +67,25 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: assert query == {"name": ["x"]} assert "data" not in query assert "ignore_datasources" not in query + + +def test_clear_branch_forwards_create_options(monkeypatch: pytest.MonkeyPatch) -> None: + captured_options: list[CreateBranchOptions | None] = [] + + monkeypatch.setattr(branches_module, "delete_branch", lambda *_args, **_kwargs: None) + + def fake_create_branch(_config: dict[str, Any], _name: str, options: CreateBranchOptions | None = None) -> Any: + captured_options.append(options) + return {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"} + + monkeypatch.setattr(branches_module, "create_branch", fake_create_branch) + + clear_branch( + {"base_url": "https://api.tinybird.co", "token": "p.test"}, + "x", + options=CreateBranchOptions(last_partition=True), + ) + + assert len(captured_options) == 1 + assert captured_options[0] is not None + assert captured_options[0].last_partition is True diff --git a/tests/test_cli_branch_config.py b/tests/test_cli_branch_config.py index ba522cf..98e4ee0 100644 --- a/tests/test_cli_branch_config.py +++ b/tests/test_cli_branch_config.py @@ -5,37 +5,73 @@ import pytest -from tinybird_sdk.cli.config import _resolve_branch_data_on_create, load_config +from tinybird_sdk.cli.config import _resolve_branch_data_mode, load_config -def test_branch_data_on_create_last_partition() -> None: - assert _resolve_branch_data_on_create({"branch_data_on_create": "last_partition"}) == "last_partition" +def test_branch_data_mode_last_partition() -> None: + mode, explicit = _resolve_branch_data_mode({"branch_data_mode": "last_partition"}) + assert mode == "last_partition" + assert explicit is True -def test_branch_data_on_create_missing_returns_none() -> None: - assert _resolve_branch_data_on_create({}) == "last_partition" +def test_branch_data_mode_missing_defaults_to_last_partition() -> None: + mode, explicit = _resolve_branch_data_mode({}) + assert mode == "last_partition" + assert explicit is False -def test_branch_data_on_create_empty_defaults_to_last_partition() -> None: - assert _resolve_branch_data_on_create({"branch_data_on_create": " "}) == "last_partition" +def test_branch_data_mode_empty_defaults_to_last_partition() -> None: + mode, explicit = _resolve_branch_data_mode({"branch_data_mode": " "}) + assert mode == "last_partition" + assert explicit is False -def test_branch_data_on_create_all_partitions_disabled() -> None: - with pytest.raises(ValueError, match="disabled"): - _resolve_branch_data_on_create({"branch_data_on_create": "all_partitions"}) +def test_branch_data_mode_rejects_legacy_key() -> None: + with pytest.raises(ValueError, match="renamed to `branch_data_mode`"): + _resolve_branch_data_mode({"branch_data_on_create": "last_partition"}) -def test_branch_data_on_create_invalid_value() -> None: - with pytest.raises(ValueError, match="Invalid branch_data_on_create"): - _resolve_branch_data_on_create({"branch_data_on_create": "invalid"}) +def test_branch_data_mode_rejects_all_partitions() -> None: + with pytest.raises(ValueError, match="Invalid branch_data_mode"): + _resolve_branch_data_mode({"branch_data_mode": "all_partitions"}) -def test_branch_data_on_create_non_string() -> None: +def test_branch_data_mode_invalid_value() -> None: + with pytest.raises(ValueError, match="Invalid branch_data_mode"): + _resolve_branch_data_mode({"branch_data_mode": "invalid"}) + + +def test_branch_data_mode_non_string() -> None: with pytest.raises(ValueError, match="must be a string"): - _resolve_branch_data_on_create({"branch_data_on_create": 1}) + _resolve_branch_data_mode({"branch_data_mode": 1}) + + +def test_load_config_warns_when_local_mode_explicit_branch_data_mode( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + project = tmp_path / "project" + project.mkdir() + (project / "tinybird.config.json").write_text( + json.dumps( + { + "include": ["lib/datasources.py"], + "token": "p.test", + "base_url": "https://api.tinybird.co", + "dev_mode": "local", + "branch_data_mode": "last_partition", + } + ), + encoding="utf-8", + ) + + load_config(str(project)) + captured = capsys.readouterr() + assert "branch_data_mode is set" in captured.out -def test_load_config_warns_when_local_mode_uses_branch_data(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: +def test_load_config_does_not_warn_when_branch_data_mode_is_implicit( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: project = tmp_path / "project" project.mkdir() (project / "tinybird.config.json").write_text( @@ -45,7 +81,6 @@ def test_load_config_warns_when_local_mode_uses_branch_data(tmp_path: Path, caps "token": "p.test", "base_url": "https://api.tinybird.co", "dev_mode": "local", - "branch_data_on_create": "last_partition", } ), encoding="utf-8", @@ -53,4 +88,4 @@ def test_load_config_warns_when_local_mode_uses_branch_data(tmp_path: Path, caps load_config(str(project)) captured = capsys.readouterr() - assert "branch_data_on_create is set" in captured.out + assert "branch_data_mode is set" not in captured.out diff --git a/tests/test_client_parity.py b/tests/test_client_parity.py index 434626f..cd52a8d 100644 --- a/tests/test_client_parity.py +++ b/tests/test_client_parity.py @@ -43,6 +43,8 @@ def query( def test_client_branch_context_uses_branch_token(monkeypatch: pytest.MonkeyPatch) -> None: + captured: dict[str, Any] = {} + monkeypatch.setattr(client_base, "is_preview_environment", lambda: False) monkeypatch.setattr( client_base, @@ -51,13 +53,16 @@ def test_client_branch_context_uses_branch_token(monkeypatch: pytest.MonkeyPatch "git_branch": "feature/alpha", "tinybird_branch": "feature_alpha", "is_main_branch": False, + "dev_mode": "branch", + "branch_data_mode": "last_partition", }, ) - monkeypatch.setattr( - client_base, - "get_or_create_branch", - lambda *_args, **_kwargs: {"token": "branch_token"}, - ) + + def fake_get_or_create_branch(*_args: Any, **kwargs: Any) -> dict[str, Any]: + captured.update(kwargs) + return {"token": "branch_token"} + + monkeypatch.setattr(client_base, "get_or_create_branch", fake_get_or_create_branch) client = TinybirdClient( { @@ -71,6 +76,7 @@ def test_client_branch_context_uses_branch_token(monkeypatch: pytest.MonkeyPatch assert context["token"] == "branch_token" assert context["is_branch_token"] is True assert context["branch_name"] == "feature_alpha" + assert captured["options"].last_partition is True def test_tokens_namespace_wraps_token_api_errors(monkeypatch: pytest.MonkeyPatch) -> None: From 51b5c14a2047c35336e43650afa5c4ac0da4f0d7 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Tue, 9 Jun 2026 12:18:31 +0200 Subject: [PATCH 3/7] bump version --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 39bc80f..f87e0a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tinybird-sdk" -version = "0.2.0" +version = "0.3.0" description = "Python SDK for Tinybird Forward" readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 9b5f8a4..140f430 100644 --- a/uv.lock +++ b/uv.lock @@ -1190,7 +1190,7 @@ wheels = [ [[package]] name = "tinybird-sdk" -version = "0.2.0" +version = "0.3.0" source = { editable = "." } dependencies = [ { name = "tinybird" }, From c0a26ed90ce63aa297b21a62dc027056ae4cc193 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Wed, 10 Jun 2026 12:19:51 +0200 Subject: [PATCH 4/7] format code --- src/tinybird_sdk/api/branches.py | 1 + src/tinybird_sdk/client/base.py | 4 +++- tests/test_api_branches_options.py | 12 +++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/tinybird_sdk/api/branches.py b/src/tinybird_sdk/api/branches.py index e7bee59..afaa06e 100644 --- a/src/tinybird_sdk/api/branches.py +++ b/src/tinybird_sdk/api/branches.py @@ -6,6 +6,7 @@ from urllib.parse import urlencode from .fetcher import tinybird_fetch + LAST_PARTITION = "last_partition" LAST_PARTITION = "last_partition" diff --git a/src/tinybird_sdk/client/base.py b/src/tinybird_sdk/client/base.py index d97b819..c570770 100644 --- a/src/tinybird_sdk/client/base.py +++ b/src/tinybird_sdk/client/base.py @@ -162,7 +162,9 @@ def _resolve_branch_context(self) -> ClientContext: branch_options = None branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": - branch_options = CreateBranchOptions(last_partition=(branch_value == "last_partition")) + branch_options = CreateBranchOptions( + last_partition=(branch_value == "last_partition") + ) branch = get_or_create_branch( { diff --git a/tests/test_api_branches_options.py b/tests/test_api_branches_options.py index c722ef4..f04194b 100644 --- a/tests/test_api_branches_options.py +++ b/tests/test_api_branches_options.py @@ -32,7 +32,9 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: return _FakeResponse(200, {"job": {"id": "job-1"}}) if "/v0/jobs/" in url: return _FakeResponse(200, {"status": "done"}) - return _FakeResponse(200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"}) + return _FakeResponse( + 200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"} + ) monkeypatch.setattr(branches_module, "tinybird_fetch", fake_fetch) create_branch( @@ -56,7 +58,9 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: return _FakeResponse(200, {"job": {"id": "job-1"}}) if "/v0/jobs/" in url: return _FakeResponse(200, {"status": "done"}) - return _FakeResponse(200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"}) + return _FakeResponse( + 200, {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"} + ) monkeypatch.setattr(branches_module, "tinybird_fetch", fake_fetch) create_branch({"base_url": "https://api.tinybird.co", "token": "p.test"}, "x") @@ -74,7 +78,9 @@ def test_clear_branch_forwards_create_options(monkeypatch: pytest.MonkeyPatch) - monkeypatch.setattr(branches_module, "delete_branch", lambda *_args, **_kwargs: None) - def fake_create_branch(_config: dict[str, Any], _name: str, options: CreateBranchOptions | None = None) -> Any: + def fake_create_branch( + _config: dict[str, Any], _name: str, options: CreateBranchOptions | None = None + ) -> Any: captured_options.append(options) return {"id": "b1", "name": "x", "created_at": "2024-01-01T00:00:00Z", "token": "p.test"} From ad40f24e5e84e846348f7d97ffbdd603a14d49e3 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Mon, 15 Jun 2026 11:03:47 +0200 Subject: [PATCH 5/7] add changelog --- CHANGELOG.md | 17 +++++++++-------- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 615b9a7..a4b114d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +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] - -### Added -- Professional repository baseline: - - CI workflow with lint, type checks, tests, and secret scanning gates. - - `pre-commit` with `ruff`, `mypy`, and `gitleaks`. - - `Makefile`, `CODEOWNERS`, and PR template. - - Initial `SECURITY.md` and packaging metadata improvements. +## [0.1.11] - 2026-06-15 + +### Changed + +- Bumped bundled `tinybird` CLI dependency to `4.6.1`. +- Updated branch data config handling to use `branch_data_mode`; legacy `branch_data_on_create` now triggers an explicit migration error. +- `branch_data_mode` now only accepts `last_partition` as a user-facing value. +- In `dev_mode=local`, branch data mode warnings are now shown only when `branch_data_mode` is explicitly set in `tinybird.config.json`. +- `tinybird branch create` and `tinybird branch clear` now show a deprecation warning (instead of failing) when `--ignore-datasource` is passed, then continue by ignoring that flag. diff --git a/pyproject.toml b/pyproject.toml index f87e0a5..9251c1e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] requires-python = ">=3.11" dependencies = [ - "tinybird==4.6.0", + "tinybird==4.6.1", ] classifiers = [ "Development Status :: 4 - Beta", From 042a77fa246aecc22f2d5ef18be920d6d3fb5242 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Mon, 15 Jun 2026 11:09:26 +0200 Subject: [PATCH 6/7] fix tb version --- CHANGELOG.md | 2 +- pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4b114d..037580b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht ### Changed -- Bumped bundled `tinybird` CLI dependency to `4.6.1`. +- Relaxed bundled `tinybird` CLI dependency to `>=4.6.0,<4.7.0` to avoid resolution failures while keeping the SDK on the `4.6.x` line. - Updated branch data config handling to use `branch_data_mode`; legacy `branch_data_on_create` now triggers an explicit migration error. - `branch_data_mode` now only accepts `last_partition` as a user-facing value. - In `dev_mode=local`, branch data mode warnings are now shown only when `branch_data_mode` is explicitly set in `tinybird.config.json`. diff --git a/pyproject.toml b/pyproject.toml index 9251c1e..660ca16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ ] requires-python = ">=3.11" dependencies = [ - "tinybird==4.6.1", + "tinybird>=4.6.0,<4.7.0", ] classifiers = [ "Development Status :: 4 - Beta", diff --git a/uv.lock b/uv.lock index 140f430..5d81094 100644 --- a/uv.lock +++ b/uv.lock @@ -1205,7 +1205,7 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "tinybird", specifier = "==4.6.0" }] +requires-dist = [{ name = "tinybird", specifier = ">=4.6.0,<4.7.0" }] [package.metadata.requires-dev] dev = [ From fe8293a25110f4ca197e5ec37e4e64b5ede30992 Mon Sep 17 00:00:00 2001 From: Manuel Martin Morante Date: Fri, 19 Jun 2026 12:17:36 -0700 Subject: [PATCH 7/7] fix error in the API params to create a branch --- src/tinybird_sdk/api/branches.py | 11 ++++------- src/tinybird_sdk/cli/commands/build.py | 4 +--- src/tinybird_sdk/cli/commands/clear.py | 2 +- src/tinybird_sdk/cli/commands/preview.py | 4 +--- src/tinybird_sdk/client/base.py | 4 +--- tests/test_api_branches_options.py | 6 +++--- tests/test_client_parity.py | 2 +- 7 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/tinybird_sdk/api/branches.py b/src/tinybird_sdk/api/branches.py index afaa06e..4facaab 100644 --- a/src/tinybird_sdk/api/branches.py +++ b/src/tinybird_sdk/api/branches.py @@ -5,12 +5,9 @@ from typing import Any from urllib.parse import urlencode +from ..cli.config_types import BranchDataMode from .fetcher import tinybird_fetch -LAST_PARTITION = "last_partition" - -LAST_PARTITION = "last_partition" - @dataclass(frozen=True, slots=True) class BranchApiConfig: @@ -28,7 +25,7 @@ class TinybirdBranch: @dataclass(frozen=True, slots=True) class CreateBranchOptions: - last_partition: bool = False + branch_data_mode: BranchDataMode | None = None class BranchApiError(Exception): @@ -79,8 +76,8 @@ def create_branch( ) -> TinybirdBranch: normalized = config if isinstance(config, BranchApiConfig) else BranchApiConfig(**config) params = {"name": name} - if options and options.last_partition: - params["data"] = LAST_PARTITION + if options and options.branch_data_mode: + params["data"] = options.branch_data_mode url = f"{normalized.base_url.rstrip('/')}/v1/environments?{urlencode(params)}" response = tinybird_fetch(url, method="POST", headers=_headers(normalized.token)) diff --git a/src/tinybird_sdk/cli/commands/build.py b/src/tinybird_sdk/cli/commands/build.py index 0dfe265..935bd43 100644 --- a/src/tinybird_sdk/cli/commands/build.py +++ b/src/tinybird_sdk/cli/commands/build.py @@ -141,9 +141,7 @@ def run_build(options: BuildCommandOptions | dict[str, Any] | None = None) -> Bu branch_options = None branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": - branch_options = CreateBranchOptions( - last_partition=(branch_value == "last_partition"), - ) + branch_options = CreateBranchOptions(branch_data_mode=branch_value) branch = get_or_create_branch( {"base_url": config["base_url"], "token": config["token"]}, config["tinybird_branch"], diff --git a/src/tinybird_sdk/cli/commands/clear.py b/src/tinybird_sdk/cli/commands/clear.py index 600a276..c230546 100644 --- a/src/tinybird_sdk/cli/commands/clear.py +++ b/src/tinybird_sdk/cli/commands/clear.py @@ -63,7 +63,7 @@ def run_clear(options: ClearCommandOptions | dict[str, Any] | None = None) -> Cl branch_options = None branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": - branch_options = CreateBranchOptions(last_partition=(branch_value == "last_partition")) + branch_options = CreateBranchOptions(branch_data_mode=branch_value) clear_branch( {"base_url": config["base_url"], "token": config["token"]}, diff --git a/src/tinybird_sdk/cli/commands/preview.py b/src/tinybird_sdk/cli/commands/preview.py index 0200efe..053b2b0 100644 --- a/src/tinybird_sdk/cli/commands/preview.py +++ b/src/tinybird_sdk/cli/commands/preview.py @@ -154,9 +154,7 @@ def run_preview( branch_options = None branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": - branch_options = CreateBranchOptions( - last_partition=(branch_value == "last_partition"), - ) + branch_options = CreateBranchOptions(branch_data_mode=branch_value) branch = create_branch( {"base_url": config["base_url"], "token": config["token"]}, preview_branch_name, diff --git a/src/tinybird_sdk/client/base.py b/src/tinybird_sdk/client/base.py index c570770..b48ba09 100644 --- a/src/tinybird_sdk/client/base.py +++ b/src/tinybird_sdk/client/base.py @@ -162,9 +162,7 @@ def _resolve_branch_context(self) -> ClientContext: branch_options = None branch_value = config.get("branch_data_mode") if branch_value and config.get("dev_mode") != "local": - branch_options = CreateBranchOptions( - last_partition=(branch_value == "last_partition") - ) + branch_options = CreateBranchOptions(branch_data_mode=branch_value) branch = get_or_create_branch( { diff --git a/tests/test_api_branches_options.py b/tests/test_api_branches_options.py index f04194b..a04e1f0 100644 --- a/tests/test_api_branches_options.py +++ b/tests/test_api_branches_options.py @@ -40,7 +40,7 @@ def fake_fetch(url: str, **_kwargs: Any) -> _FakeResponse: create_branch( {"base_url": "https://api.tinybird.co", "token": "p.test"}, "x", - options=CreateBranchOptions(last_partition=True), + options=CreateBranchOptions(branch_data_mode="last_partition"), ) parsed = urlparse(called_urls[0]) @@ -89,9 +89,9 @@ def fake_create_branch( clear_branch( {"base_url": "https://api.tinybird.co", "token": "p.test"}, "x", - options=CreateBranchOptions(last_partition=True), + options=CreateBranchOptions(branch_data_mode="last_partition"), ) assert len(captured_options) == 1 assert captured_options[0] is not None - assert captured_options[0].last_partition is True + assert captured_options[0].branch_data_mode == "last_partition" diff --git a/tests/test_client_parity.py b/tests/test_client_parity.py index cd52a8d..70b132f 100644 --- a/tests/test_client_parity.py +++ b/tests/test_client_parity.py @@ -76,7 +76,7 @@ def fake_get_or_create_branch(*_args: Any, **kwargs: Any) -> dict[str, Any]: assert context["token"] == "branch_token" assert context["is_branch_token"] is True assert context["branch_name"] == "feature_alpha" - assert captured["options"].last_partition is True + assert captured["options"].branch_data_mode == "last_partition" def test_tokens_namespace_wraps_token_api_errors(monkeypatch: pytest.MonkeyPatch) -> None: