Skip to content

Commit 6f4d0ba

Browse files
committed
feat(config): fallback to .tinyb auth when token is missing
1 parent 264f262 commit 6f4d0ba

3 files changed

Lines changed: 117 additions & 5 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,12 @@ config = {
476476
| Option | Type | Default | Description |
477477
|--------|------|---------|-------------|
478478
| `include` | `list[str]` | *required* | File paths or glob patterns for Python and raw datafiles |
479-
| `token` | `str` | *required* | API token; supports `${ENV_VAR}` interpolation |
479+
| `token` | `str` | *required* | API token; supports `${ENV_VAR}` interpolation. If missing, SDK falls back to `TINYBIRD_TOKEN`, then `.tinyb` |
480480
| `base_url` | `str` | `"https://api.tinybird.co"` | Tinybird API URL |
481481
| `dev_mode` | `"branch"` \| `"local"` | `"branch"` | Development mode |
482482

483+
If `base_url` is omitted, SDK resolves it from `TINYBIRD_URL`, then `TINYBIRD_HOST`, then `.tinyb` (`host`), and finally defaults to `https://api.tinybird.co`.
484+
483485
### Local Development Mode
484486

485487
Use a local Tinybird container for development without affecting cloud workspaces:

src/tinybird_sdk/cli/config.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,40 @@ def replacer(match: re.Match[str]) -> str:
9797
return pattern.sub(replacer, value)
9898

9999

100+
def _try_interpolate_env_vars(value: str | None) -> str | None:
101+
if not value:
102+
return None
103+
try:
104+
return _interpolate_env_vars(value)
105+
except ValueError:
106+
return None
107+
108+
109+
def _resolve_base_url_from_env() -> str | None:
110+
return os.getenv("TINYBIRD_URL") or os.getenv("TINYBIRD_HOST")
111+
112+
113+
def _read_tinyb_auth(config_dir: str) -> dict[str, str | None]:
114+
tinyb_path = Path(config_dir) / ".tinyb"
115+
if not tinyb_path.exists() or not tinyb_path.is_file():
116+
return {"token": None, "host": None}
117+
118+
try:
119+
raw = json.loads(tinyb_path.read_text(encoding="utf-8"))
120+
except Exception:
121+
return {"token": None, "host": None}
122+
123+
if not isinstance(raw, dict):
124+
return {"token": None, "host": None}
125+
126+
token = raw.get("token")
127+
host = raw.get("host")
128+
return {
129+
"token": token if isinstance(token, str) and token else None,
130+
"host": host if isinstance(host, str) and host else None,
131+
}
132+
133+
100134
def find_config_file(start_dir: str) -> dict[str, str] | None:
101135
current = Path(start_dir).resolve()
102136

@@ -117,16 +151,26 @@ def _resolve_config(config: TinybirdConfig, config_path: str) -> ResolvedConfig:
117151
raise ValueError(
118152
f"Missing 'include' field in {config_path}. Add an array of files to scan for datasources and pipes."
119153
)
120-
if not config.token:
121-
raise ValueError(f"Missing 'token' field in {config_path}")
122154

123155
include = config.include or [config.schema] # type: ignore[list-item]
124156

125157
config_dir = str(Path(config_path).parent)
126158
load_env_files(config_dir)
159+
tinyb_auth = _read_tinyb_auth(config_dir)
127160

128-
token = _interpolate_env_vars(config.token)
129-
base_url = _interpolate_env_vars(config.base_url) if config.base_url else DEFAULT_BASE_URL
161+
token = _try_interpolate_env_vars(config.token) or os.getenv("TINYBIRD_TOKEN") or tinyb_auth["token"]
162+
if not token:
163+
raise ValueError(
164+
f"Missing Tinybird token in {config_path}. "
165+
"Set 'token' in config, TINYBIRD_TOKEN, or authenticate with Tinybird CLI to create .tinyb."
166+
)
167+
168+
base_url = (
169+
_try_interpolate_env_vars(config.base_url)
170+
or _resolve_base_url_from_env()
171+
or tinyb_auth["host"]
172+
or DEFAULT_BASE_URL
173+
)
130174

131175
return ResolvedConfig(
132176
include=include,

tests/test_cli_config_parity.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,72 @@ def test_load_config_async_supports_python(tmp_path: Path, monkeypatch: pytest.M
8888
assert loaded["include"] == ["lib/datasources.py"]
8989

9090

91+
def test_load_config_falls_back_to_env_when_token_and_base_url_are_missing(
92+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
93+
) -> None:
94+
project = tmp_path / "project"
95+
project.mkdir()
96+
97+
(project / "tinybird.config.json").write_text(
98+
json.dumps({"include": ["lib/datasources.py"]}),
99+
encoding="utf-8",
100+
)
101+
102+
monkeypatch.setenv("TINYBIRD_TOKEN", "p.from_env")
103+
monkeypatch.setenv("TINYBIRD_HOST", "https://api.us-east-1.aws.tinybird.co")
104+
monkeypatch.delenv("TINYBIRD_URL", raising=False)
105+
106+
loaded = load_config(str(project))
107+
assert loaded["token"] == "p.from_env"
108+
assert loaded["base_url"] == "https://api.us-east-1.aws.tinybird.co"
109+
110+
111+
def test_load_config_falls_back_to_tinyb_file_when_config_and_env_are_missing(
112+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
113+
) -> None:
114+
project = tmp_path / "project"
115+
project.mkdir()
116+
117+
(project / "tinybird.config.json").write_text(
118+
json.dumps({"include": ["lib/datasources.py"]}),
119+
encoding="utf-8",
120+
)
121+
(project / ".tinyb").write_text(
122+
json.dumps(
123+
{
124+
"token": "p.from_tinyb",
125+
"host": "https://api.eu-central-1.aws.tinybird.co",
126+
}
127+
),
128+
encoding="utf-8",
129+
)
130+
131+
monkeypatch.delenv("TINYBIRD_TOKEN", raising=False)
132+
monkeypatch.delenv("TINYBIRD_URL", raising=False)
133+
monkeypatch.delenv("TINYBIRD_HOST", raising=False)
134+
135+
loaded = load_config(str(project))
136+
assert loaded["token"] == "p.from_tinyb"
137+
assert loaded["base_url"] == "https://api.eu-central-1.aws.tinybird.co"
138+
139+
140+
def test_load_config_raises_when_no_token_sources_are_available(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
141+
project = tmp_path / "project"
142+
project.mkdir()
143+
144+
(project / "tinybird.config.json").write_text(
145+
json.dumps({"include": ["lib/datasources.py"]}),
146+
encoding="utf-8",
147+
)
148+
149+
monkeypatch.delenv("TINYBIRD_TOKEN", raising=False)
150+
monkeypatch.delenv("TINYBIRD_URL", raising=False)
151+
monkeypatch.delenv("TINYBIRD_HOST", raising=False)
152+
153+
with pytest.raises(ValueError, match="Missing Tinybird token"):
154+
load_config(str(project))
155+
156+
91157
def test_path_helpers_with_and_without_src_folder(tmp_path: Path) -> None:
92158
no_src = tmp_path / "no-src"
93159
no_src.mkdir()

0 commit comments

Comments
 (0)