From aac246a517b7b7172020cad9e49287c506c9b813 Mon Sep 17 00:00:00 2001 From: Raphael Fakhri <153192858+RaphaelFakhri@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:58:18 +0000 Subject: [PATCH] fix(api): name the multipart field after the detected format in append_datasource append_datasource always sent local files under the csv form field. The API requires the field name to be ndjson or parquet for those formats, so NDJSON and Parquet uploads failed. Use the format detected from the file extension and keep csv as the fallback. Closes #23 --- CHANGELOG.md | 6 ++++++ src/tinybird_sdk/api/api.py | 2 +- tests/test_api_parity.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1ecb0..c6b23c3 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] + +### Fixed + +- `append_datasource` with a local `file` now names the multipart form field after the detected format (`csv`, `ndjson` or `parquet`) instead of always using `csv`, so NDJSON and Parquet uploads are accepted by the API. + ## [0.4.0] - 2026-06-29 ### Added diff --git a/src/tinybird_sdk/api/api.py b/src/tinybird_sdk/api/api.py index 8ee18b5..2c4cdf5 100644 --- a/src/tinybird_sdk/api/api.py +++ b/src/tinybird_sdk/api/api.py @@ -272,7 +272,7 @@ def append_datasource( with open(file_path_str, "rb") as fp: file_content = fp.read() content_type, multipart = create_multipart_body( - files=[("csv", file_path_str, file_content, None)], + files=[(detected_format or "csv", file_path_str, file_content, None)], ) response = self.request( f"/v0/datasources?{urlencode(query)}", diff --git a/tests/test_api_parity.py b/tests/test_api_parity.py index 6f13da0..095092f 100644 --- a/tests/test_api_parity.py +++ b/tests/test_api_parity.py @@ -124,6 +124,35 @@ def fake_fetch(url: str, **kwargs: Any) -> _FakeResponse: assert api.truncate_datasource("events") == {} +@pytest.mark.parametrize( + ("filename", "field_name"), + [ + ("events.csv", "csv"), + ("events.ndjson", "ndjson"), + ("events.jsonl", "ndjson"), + ("events.parquet", "parquet"), + ("events.dat", "csv"), + ], +) +def test_append_file_uses_format_as_multipart_field_name( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, filename: str, field_name: str +) -> None: + calls: list[dict[str, Any]] = [] + + def fake_fetch(_url: str, **kwargs: Any) -> _FakeResponse: + calls.append(kwargs) + return _FakeResponse(200, {"ok": True}) + + monkeypatch.setattr(api_module, "tinybird_fetch", fake_fetch) + api = TinybirdApi({"base_url": "https://api.tinybird.co", "token": "p.token"}) + + local_file = tmp_path / filename + local_file.write_bytes(b"data") + api.append_datasource("events", {"file": str(local_file)}) + + assert f'name="{field_name}"; filename="{filename}"'.encode() in calls[0]["body"] + + def test_append_requires_either_url_or_file() -> None: api = TinybirdApi({"base_url": "https://api.tinybird.co", "token": "p.token"}) with pytest.raises(ValueError, match="Either 'url' or 'file'"):