From 4e460712c55f65012309d782ed9d5d50dbaacac1 Mon Sep 17 00:00:00 2001 From: Vlad Gramuzov Date: Sun, 10 Oct 2021 02:12:43 +0300 Subject: [PATCH] Add Tinybird query runner Co-authored-by: Thomas Rausch Co-authored-by: Vlad Gramuzov " --- README.md | 1 + .../app/assets/images/db-logos/tinybird.png | Bin 0 -> 18169 bytes redash/query_runner/clickhouse.py | 6 +- redash/query_runner/tinybird.py | 113 ++++++++++++++++ redash/settings/__init__.py | 1 + tests/query_runner/test_tinybird.py | 123 ++++++++++++++++++ 6 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 client/app/assets/images/db-logos/tinybird.png create mode 100644 redash/query_runner/tinybird.py create mode 100644 tests/query_runner/test_tinybird.py diff --git a/README.md b/README.md index 5eacf7b4d8..07a4de3e71 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ Redash supports more than 35 SQL and NoSQL [data sources](https://redash.io/help - SPARQL - SQLite - TiDB +- Tinybird - TreasureData - Trino - Uptycs diff --git a/client/app/assets/images/db-logos/tinybird.png b/client/app/assets/images/db-logos/tinybird.png new file mode 100644 index 0000000000000000000000000000000000000000..129555e39fe8e52f9865b9c5464b714f6aac2265 GIT binary patch literal 18169 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 Lw3Q0)TLk_eO!(lj literal 0 HcmV?d00001 diff --git a/redash/query_runner/clickhouse.py b/redash/query_runner/clickhouse.py index 0d5afa25b0..dddff2712e 100644 --- a/redash/query_runner/clickhouse.py +++ b/redash/query_runner/clickhouse.py @@ -53,10 +53,6 @@ def configuration_schema(cls): "secret": ["password"], } - @classmethod - def type(cls): - return "clickhouse" - @property def _url(self): return urlparse(self.configuration["url"]) @@ -162,7 +158,7 @@ def _define_column_type(column): return TYPE_STRING def _clickhouse_query(self, query, session_id=None, session_check=None): - logger.debug("Clickhouse is about to execute query: %s", query) + logger.debug(f"{self.name()} is about to execute query: %s", query) query += "\nFORMAT JSON" diff --git a/redash/query_runner/tinybird.py b/redash/query_runner/tinybird.py new file mode 100644 index 0000000000..f29a45a8cb --- /dev/null +++ b/redash/query_runner/tinybird.py @@ -0,0 +1,113 @@ +import logging + +import requests + +from redash.query_runner import register +from redash.query_runner.clickhouse import ClickHouse + +logger = logging.getLogger(__name__) + + +class Tinybird(ClickHouse): + noop_query = "SELECT count() FROM tinybird.pipe_stats LIMIT 1" + + DEFAULT_URL = "https://api.tinybird.co" + + SQL_ENDPOINT = "/v0/sql" + DATASOURCES_ENDPOINT = "/v0/datasources" + PIPES_ENDPOINT = "/v0/pipes" + + @classmethod + def configuration_schema(cls): + return { + "type": "object", + "properties": { + "url": {"type": "string", "default": cls.DEFAULT_URL}, + "token": {"type": "string", "title": "Auth Token"}, + "timeout": { + "type": "number", + "title": "Request Timeout", + "default": 30, + }, + "verify": { + "type": "boolean", + "title": "Verify SSL certificate", + "default": True, + }, + }, + "order": ["url", "token"], + "required": ["token"], + "extra_options": ["timeout", "verify"], + "secret": ["token"], + } + + def _get_tables(self, schema): + self._collect_tinybird_schema( + schema, + self.DATASOURCES_ENDPOINT, + "datasources", + ) + + self._collect_tinybird_schema( + schema, + self.PIPES_ENDPOINT, + "pipes", + ) + + return list(schema.values()) + + def _send_query(self, data, session_id=None, session_check=None): + return self._get_from_tinybird( + self.SQL_ENDPOINT, + params={"q": data.encode("utf-8", "ignore")}, + ) + + def _collect_tinybird_schema(self, schema, endpoint, resource_type): + response = self._get_from_tinybird(endpoint) + resources = response.get(resource_type, []) + + for r in resources: + if r["name"] not in schema: + schema[r["name"]] = {"name": r["name"], "columns": []} + + if resource_type == "pipes" and not r.get("endpoint"): + continue + + query = f"SELECT * FROM {r['name']} LIMIT 1 FORMAT JSON" + try: + query_result = self._send_query(query) + except Exception: + logger.exception(f"error in schema {r['name']}") + continue + + columns = [meta["name"] for meta in query_result["meta"]] + schema[r["name"]]["columns"].extend(columns) + + return schema + + def _get_from_tinybird(self, endpoint, params=None): + url = f"{self.configuration.get('url', self.DEFAULT_URL)}{endpoint}" + authorization = f"Bearer {self.configuration.get('token')}" + + try: + response = requests.get( + url, + timeout=self.configuration.get("timeout", 30), + params=params, + headers={"Authorization": authorization}, + verify=self.configuration.get("verify", True), + ) + except requests.RequestException as e: + if e.response: + details = f"({e.__class__.__name__}, Status Code: {e.response.status_code})" + else: + details = f"({e.__class__.__name__})" + raise Exception(f"Connection error to: {url} {details}.") + + if response.status_code >= 400: + raise Exception(response.text) + + return response.json() + + +register(Tinybird) diff --git a/redash/settings/__init__.py b/redash/settings/__init__.py index 2eedbc7599..fb7e398c00 100644 --- a/redash/settings/__init__.py +++ b/redash/settings/__init__.py @@ -296,6 +296,7 @@ def email_server_is_configured(): "redash.query_runner.impala_ds", "redash.query_runner.vertica", "redash.query_runner.clickhouse", + "redash.query_runner.tinybird", "redash.query_runner.yandex_metrica", "redash.query_runner.rockset", "redash.query_runner.treasuredata", diff --git a/tests/query_runner/test_tinybird.py b/tests/query_runner/test_tinybird.py new file mode 100644 index 0000000000..79fbf4c4bc --- /dev/null +++ b/tests/query_runner/test_tinybird.py @@ -0,0 +1,123 @@ +import json +from unittest import TestCase +from unittest.mock import Mock, patch + +from redash.query_runner import TYPE_DATETIME, TYPE_INTEGER, TYPE_STRING +from redash.query_runner.tinybird import Tinybird + +DATASOURCES_RESPONSE = { + "datasources": [ + { + "id": "t_datasource_id", + "name": "test_datasource", + "columns": [ + { + "name": "string_attribute", + "type": "String", + "nullable": False, + "normalized_name": "string_attribute", + }, + { + "name": "number_attribute", + "type": "Int32", + "nullable": False, + "normalized_name": "number_attribute", + }, + {"name": "date_attribute", "type": "DateTime", "nullable": False, "normalized_name": "date_attribute"}, + ], + } + ] +} + +PIPES_RESPONSE = {"pipes": [{"id": "t_pipe_id", "name": "test_pipe", "endpoint": "t_endpoint_id", "type": "endpoint"}]} + +SCHEMA_RESPONSE = { + "meta": [ + {"name": "string_attribute", "type": "String"}, + {"name": "number_attribute", "type": "Int32"}, + {"name": "date_attribute", "type": "DateTime"}, + ] +} + +QUERY_RESPONSE = { + **SCHEMA_RESPONSE, + "data": [ + {"string_attribute": "hello world", "number_attribute": 123, "date_attribute": "2023-01-01 00:00:03.001000"}, + ], + "rows": 1, + "statistics": {"elapsed": 0.011556914, "rows_read": 87919, "bytes_read": 17397219}, +} + + +class TestTinybird(TestCase): + @patch("requests.get") + def test_get_schema_scans_pipes_and_datasources(self, get_request): + query_runner = self._build_query_runner() + + get_request.side_effect = self._mock_tinybird_schema_requests + + schema = query_runner.get_schema() + + self.assertEqual( + schema, + [ + {"name": "test_datasource", "columns": ["string_attribute", "number_attribute", "date_attribute"]}, + {"name": "test_pipe", "columns": ["string_attribute", "number_attribute", "date_attribute"]}, + ], + ) + + (url,), kwargs = get_request.call_args + + self.assertEqual(kwargs["timeout"], 60) + self.assertEqual(kwargs["headers"], {"Authorization": "Bearer p.test.token"}) + + @patch("requests.get") + def test_run_query(self, get_request): + query_runner = self._build_query_runner() + + get_request.return_value = Mock( + status_code=200, text=json.dumps(QUERY_RESPONSE), json=Mock(return_value=QUERY_RESPONSE) + ) + + data, error = query_runner.run_query("SELECT * FROM test_datasource LIMIT 1", None) + + self.assertIsNone(error) + self.assertEqual( + json.loads(data), + { + "columns": [ + {"name": "string_attribute", "friendly_name": "string_attribute", "type": TYPE_STRING}, + {"name": "number_attribute", "friendly_name": "number_attribute", "type": TYPE_INTEGER}, + {"name": "date_attribute", "friendly_name": "date_attribute", "type": TYPE_DATETIME}, + ], + "rows": [ + { + "string_attribute": "hello world", + "number_attribute": 123, + "date_attribute": "2023-01-01 00:00:03.001000", + } + ], + }, + ) + + (url,), kwargs = get_request.call_args + + self.assertEqual(url, "https://api.tinybird.co/v0/sql") + self.assertEqual(kwargs["timeout"], 60) + self.assertEqual(kwargs["headers"], {"Authorization": "Bearer p.test.token"}) + self.assertEqual(kwargs["params"], {"q": b"SELECT * FROM test_datasource LIMIT 1\nFORMAT JSON"}) + + def _mock_tinybird_schema_requests(self, endpoint, **kwargs): + response = {} + + if endpoint.endswith(Tinybird.PIPES_ENDPOINT): + response = PIPES_RESPONSE + if endpoint.endswith(Tinybird.DATASOURCES_ENDPOINT): + response = DATASOURCES_RESPONSE + if endpoint.endswith(Tinybird.SQL_ENDPOINT): + response = SCHEMA_RESPONSE + + return Mock(status_code=200, text=json.dumps(response), json=Mock(return_value=response)) + + def _build_query_runner(self): + return Tinybird({"url": "https://api.tinybird.co", "token": "p.test.token", "timeout": 60})