tinybird 0.0.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/__cli__.py +8 -0
- tinybird/ch_utils/constants.py +244 -0
- tinybird/ch_utils/engine.py +855 -0
- tinybird/check_pypi.py +25 -0
- tinybird/client.py +1281 -0
- tinybird/config.py +117 -0
- tinybird/connectors.py +428 -0
- tinybird/context.py +23 -0
- tinybird/datafile.py +5589 -0
- tinybird/datatypes.py +434 -0
- tinybird/feedback_manager.py +1022 -0
- tinybird/git_settings.py +145 -0
- tinybird/sql.py +865 -0
- tinybird/sql_template.py +2343 -0
- tinybird/sql_template_fmt.py +281 -0
- tinybird/sql_toolset.py +350 -0
- tinybird/syncasync.py +682 -0
- tinybird/tb_cli.py +25 -0
- tinybird/tb_cli_modules/auth.py +252 -0
- tinybird/tb_cli_modules/branch.py +1043 -0
- tinybird/tb_cli_modules/cicd.py +434 -0
- tinybird/tb_cli_modules/cli.py +1571 -0
- tinybird/tb_cli_modules/common.py +2082 -0
- tinybird/tb_cli_modules/config.py +344 -0
- tinybird/tb_cli_modules/connection.py +803 -0
- tinybird/tb_cli_modules/datasource.py +900 -0
- tinybird/tb_cli_modules/exceptions.py +91 -0
- tinybird/tb_cli_modules/fmt.py +91 -0
- tinybird/tb_cli_modules/job.py +85 -0
- tinybird/tb_cli_modules/pipe.py +858 -0
- tinybird/tb_cli_modules/regions.py +9 -0
- tinybird/tb_cli_modules/tag.py +100 -0
- tinybird/tb_cli_modules/telemetry.py +310 -0
- tinybird/tb_cli_modules/test.py +107 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
- tinybird/tb_cli_modules/token.py +349 -0
- tinybird/tb_cli_modules/workspace.py +269 -0
- tinybird/tb_cli_modules/workspace_members.py +212 -0
- tinybird/tornado_template.py +1194 -0
- tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
- tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
- tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
- tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
import yaml
|
|
7
|
+
from humanfriendly.tables import format_smart_table
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from tinybird.client import TinyB
|
|
11
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
12
|
+
from tinybird.tb_cli_modules.common import CLIException
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class TestPipe:
|
|
17
|
+
name: str
|
|
18
|
+
sql: Optional[str]
|
|
19
|
+
params: Optional[Dict[str, Any]]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TestCase:
|
|
24
|
+
name: str
|
|
25
|
+
sql: str
|
|
26
|
+
max_time: Optional[float]
|
|
27
|
+
max_bytes_read: Optional[int]
|
|
28
|
+
pipe: Optional[TestPipe]
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
name: str,
|
|
33
|
+
sql: str,
|
|
34
|
+
max_time: Optional[float] = None,
|
|
35
|
+
max_bytes_read: Optional[int] = None,
|
|
36
|
+
pipe: Optional[Dict[str, Any]] = None,
|
|
37
|
+
):
|
|
38
|
+
self.name = name
|
|
39
|
+
self.sql = sql
|
|
40
|
+
self.max_time = max_time
|
|
41
|
+
self.max_bytes_read = max_bytes_read
|
|
42
|
+
self.pipe = None
|
|
43
|
+
if pipe and pipe.get("name"):
|
|
44
|
+
params: Optional[Dict[str, Any]] = pipe.get("params", None)
|
|
45
|
+
pipe_name: str = pipe.get("name", "")
|
|
46
|
+
self.pipe: TestPipe = TestPipe(pipe_name, sql, params)
|
|
47
|
+
|
|
48
|
+
def __iter__(self):
|
|
49
|
+
yield (
|
|
50
|
+
self.name,
|
|
51
|
+
{"sql": self.sql, "max_time": self.max_time, "max_bytes_read": self.max_bytes_read, "pipe": self.pipe},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class Status:
|
|
57
|
+
name: str
|
|
58
|
+
abbreviation: str
|
|
59
|
+
color: str
|
|
60
|
+
description: str
|
|
61
|
+
msg: Optional[str] = None
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def build_error_with_msg(cls, msg: str) -> "Status":
|
|
65
|
+
return Status(name="ERROR", abbreviation="E", description="Error", color="bright_yellow", msg=msg)
|
|
66
|
+
|
|
67
|
+
def __eq__(self, other):
|
|
68
|
+
return self.name == other.name
|
|
69
|
+
|
|
70
|
+
def __hash__(self):
|
|
71
|
+
return hash(self.name)
|
|
72
|
+
|
|
73
|
+
def show(self):
|
|
74
|
+
return f"{self.description}: {self.msg}" if self.msg else self.description
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
PASS = Status(name="PASS", abbreviation="P", description="Pass", color="green")
|
|
78
|
+
PASS_OVER_TIME = Status(name="PASS_OVER_TIME", abbreviation="P*OT", description="Pass Over Time", color="cyan")
|
|
79
|
+
PASS_OVER_READ_BYTES = Status(
|
|
80
|
+
name="PASS_OVER_READ_BYTES", abbreviation="P*OB", description="Pass Over Read Bytes", color="cyan"
|
|
81
|
+
)
|
|
82
|
+
PASS_OVER_TIME_AND_READ_BYTES = Status(
|
|
83
|
+
name="PASS_OVER_TIME_AND_OVER_BYTES",
|
|
84
|
+
abbreviation="P*OT*OB",
|
|
85
|
+
description="Pass Over Time and Over Read Bytes",
|
|
86
|
+
color="cyan",
|
|
87
|
+
)
|
|
88
|
+
FAILED = Status(name="FAIL", abbreviation="F", description="Fail", color="red")
|
|
89
|
+
ERROR = Status(name="ERROR", abbreviation="E", description="Error", color="bright_yellow")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass()
|
|
93
|
+
class TestResult:
|
|
94
|
+
name: str
|
|
95
|
+
data: List[Dict]
|
|
96
|
+
elapsed_time: float
|
|
97
|
+
read_bytes: int
|
|
98
|
+
max_elapsed_time: Optional[float]
|
|
99
|
+
max_bytes_read: Optional[int]
|
|
100
|
+
error: Optional[str]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def status(self) -> Status:
|
|
104
|
+
if len(self.data) > 0:
|
|
105
|
+
return FAILED
|
|
106
|
+
elif self.error:
|
|
107
|
+
return Status.build_error_with_msg(self.error)
|
|
108
|
+
elif (
|
|
109
|
+
self.max_bytes_read is not None
|
|
110
|
+
and self.max_elapsed_time is not None
|
|
111
|
+
and self.read_bytes > self.max_bytes_read
|
|
112
|
+
and self.elapsed_time > self.max_elapsed_time
|
|
113
|
+
):
|
|
114
|
+
return PASS_OVER_TIME_AND_READ_BYTES
|
|
115
|
+
elif self.max_bytes_read is not None and self.read_bytes > self.max_bytes_read:
|
|
116
|
+
return PASS_OVER_READ_BYTES
|
|
117
|
+
elif self.max_elapsed_time is not None and self.elapsed_time > self.max_elapsed_time:
|
|
118
|
+
return PASS_OVER_TIME
|
|
119
|
+
return PASS
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
def __dict__(self):
|
|
123
|
+
return {
|
|
124
|
+
"name": self.name,
|
|
125
|
+
"data": self.data,
|
|
126
|
+
"elapsed_time": self.elapsed_time,
|
|
127
|
+
"read_bytes": self.read_bytes,
|
|
128
|
+
"max_elapsed_time": self.max_elapsed_time,
|
|
129
|
+
"max_bytes_read": self.max_bytes_read,
|
|
130
|
+
"error": self.error,
|
|
131
|
+
"status": self.status.name,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass()
|
|
136
|
+
class TestSummaryResults:
|
|
137
|
+
filename: str
|
|
138
|
+
results: List[TestResult]
|
|
139
|
+
semver: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_file(file: str) -> Iterable[TestCase]:
|
|
143
|
+
with Path(file).open("r") as f:
|
|
144
|
+
definitions: List[Dict[str, Any]] = yaml.safe_load(f)
|
|
145
|
+
|
|
146
|
+
for definition in definitions:
|
|
147
|
+
try:
|
|
148
|
+
for name, properties in definition.items():
|
|
149
|
+
yield TestCase(
|
|
150
|
+
name,
|
|
151
|
+
properties.get("sql"),
|
|
152
|
+
properties.get("max_time"),
|
|
153
|
+
properties.get("max_bytes_read"),
|
|
154
|
+
properties.get("pipe", None),
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
raise CLIException(
|
|
158
|
+
f"""Error: {FeedbackManager.error_exception(error=e)} reading file, check "{file}"->"{definition.get('name')}" """
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def generate_file(file: str, overwrite: bool = False) -> None:
|
|
163
|
+
definitions = [
|
|
164
|
+
dict(TestCase("this_test_should_pass", sql="SELECT * FROM numbers(5) WHERE 0")),
|
|
165
|
+
dict(TestCase("this_test_should_fail", "SELECT * FROM numbers(5) WHERE 1")),
|
|
166
|
+
dict(TestCase("this_test_should_pass_over_time", "SELECT * FROM numbers(5) WHERE 0", max_time=0.0000001)),
|
|
167
|
+
dict(
|
|
168
|
+
TestCase(
|
|
169
|
+
"this_test_should_pass_over_bytes",
|
|
170
|
+
"SELECT sum(number) AS total FROM numbers(5) HAVING total>1000",
|
|
171
|
+
max_bytes_read=5,
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
dict(
|
|
175
|
+
TestCase(
|
|
176
|
+
"this_test_should_pass_over_time_and_bytes",
|
|
177
|
+
"SELECT sum(number) AS total FROM numbers(5) HAVING total>1000",
|
|
178
|
+
max_time=0.0000001,
|
|
179
|
+
max_bytes_read=5,
|
|
180
|
+
)
|
|
181
|
+
),
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
p = Path(file)
|
|
185
|
+
if (not p.exists()) or overwrite:
|
|
186
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
with p.open("w") as f:
|
|
188
|
+
yaml.safe_dump(definitions, f)
|
|
189
|
+
click.echo(FeedbackManager.success_generated_local_file(file=p))
|
|
190
|
+
else:
|
|
191
|
+
raise CLIException(FeedbackManager.error_file_already_exists(file=p))
|
|
192
|
+
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def run_test_file(tb_client: TinyB, file: str) -> List[TestResult]:
|
|
197
|
+
results: List[TestResult] = []
|
|
198
|
+
for test_case in parse_file(file):
|
|
199
|
+
if not test_case.sql and not test_case.pipe:
|
|
200
|
+
results.append(
|
|
201
|
+
TestResult(
|
|
202
|
+
name=test_case.name,
|
|
203
|
+
data=[],
|
|
204
|
+
elapsed_time=0,
|
|
205
|
+
read_bytes=0,
|
|
206
|
+
max_elapsed_time=test_case.max_time,
|
|
207
|
+
max_bytes_read=test_case.max_bytes_read,
|
|
208
|
+
error="'sql' or 'pipe' attribute not found",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
if test_case.sql and not test_case.pipe:
|
|
214
|
+
q = f"SELECT * FROM ({test_case.sql}) LIMIT 20 FORMAT JSON"
|
|
215
|
+
try:
|
|
216
|
+
test_response = await tb_client.query(q)
|
|
217
|
+
results.append(
|
|
218
|
+
TestResult(
|
|
219
|
+
name=test_case.name,
|
|
220
|
+
data=test_response["data"],
|
|
221
|
+
elapsed_time=test_response.get("statistics", {}).get("elapsed", 0),
|
|
222
|
+
read_bytes=test_response.get("statistics", {}).get("bytes_read", 0),
|
|
223
|
+
max_elapsed_time=test_case.max_time,
|
|
224
|
+
max_bytes_read=test_case.max_bytes_read,
|
|
225
|
+
error=None,
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
except Exception as e:
|
|
230
|
+
results.append(
|
|
231
|
+
TestResult(
|
|
232
|
+
name=test_case.name,
|
|
233
|
+
data=[],
|
|
234
|
+
elapsed_time=0,
|
|
235
|
+
read_bytes=0,
|
|
236
|
+
max_elapsed_time=test_case.max_time,
|
|
237
|
+
max_bytes_read=test_case.max_bytes_read,
|
|
238
|
+
error=str(e),
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
if test_case.pipe:
|
|
242
|
+
pipe = test_case.pipe.name
|
|
243
|
+
params = test_case.pipe.params
|
|
244
|
+
try:
|
|
245
|
+
sql = test_case.sql if test_case.sql else None
|
|
246
|
+
test_response = await tb_client.pipe_data(pipe, format="json", params=params, sql=sql)
|
|
247
|
+
results.append(
|
|
248
|
+
TestResult(
|
|
249
|
+
name=test_case.name,
|
|
250
|
+
data=test_response["data"],
|
|
251
|
+
elapsed_time=test_response.get("statistics", {}).get("elapsed", 0),
|
|
252
|
+
read_bytes=test_response.get("statistics", {}).get("bytes_read", 0),
|
|
253
|
+
max_elapsed_time=test_case.max_time,
|
|
254
|
+
max_bytes_read=test_case.max_bytes_read,
|
|
255
|
+
error=None,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
except Exception as e:
|
|
259
|
+
results.append(
|
|
260
|
+
TestResult(
|
|
261
|
+
name=test_case.name,
|
|
262
|
+
data=[],
|
|
263
|
+
elapsed_time=0,
|
|
264
|
+
read_bytes=0,
|
|
265
|
+
max_elapsed_time=test_case.max_time,
|
|
266
|
+
max_bytes_read=test_case.max_bytes_read,
|
|
267
|
+
error=str(e),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return results
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_run_summary(results: List[TestSummaryResults], only_fail: bool = False, verbose_level: int = 0):
|
|
275
|
+
total_counts: Dict[Status, int] = {}
|
|
276
|
+
for result in results:
|
|
277
|
+
summary: List[Dict] = []
|
|
278
|
+
for test in result.results:
|
|
279
|
+
total_counts[test.status] = total_counts.get(test.status, 0) + 1
|
|
280
|
+
|
|
281
|
+
# Skip the PASS tests if we only want the failed ones
|
|
282
|
+
if only_fail and test.status in [PASS]:
|
|
283
|
+
continue
|
|
284
|
+
|
|
285
|
+
summary.append(
|
|
286
|
+
{
|
|
287
|
+
"semver": result.semver or "live",
|
|
288
|
+
"file": result.filename,
|
|
289
|
+
"test": test.name,
|
|
290
|
+
"status": test.status.show(),
|
|
291
|
+
"elapsed": f"{test.elapsed_time} ms",
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
click.echo(
|
|
296
|
+
format_smart_table(
|
|
297
|
+
data=[d.values() for d in summary], column_names=list(summary[0].keys()) if len(summary) > 0 else []
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
click.echo("\n")
|
|
301
|
+
|
|
302
|
+
# Only display the data for debugging when wanting verbose
|
|
303
|
+
if verbose_level == 0:
|
|
304
|
+
continue
|
|
305
|
+
failed_tests = [test for test in result.results if test.status is not PASS]
|
|
306
|
+
|
|
307
|
+
for test in failed_tests:
|
|
308
|
+
click.secho(f"{result.filename}::{test.name}", fg=test.status.color, bold=True, nl=True)
|
|
309
|
+
|
|
310
|
+
if test.data:
|
|
311
|
+
click.echo(
|
|
312
|
+
format_smart_table(
|
|
313
|
+
data=[d.values() for d in test.data],
|
|
314
|
+
column_names=list(test.data[0].keys()) if len(test.data) else [],
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
click.echo("\n")
|
|
318
|
+
|
|
319
|
+
if test.error:
|
|
320
|
+
click.secho(test.error, fg=test.status.color, bold=True, nl=True, err=True)
|
|
321
|
+
|
|
322
|
+
if len(total_counts):
|
|
323
|
+
click.echo("\nTotals:")
|
|
324
|
+
for key_status, value_total in total_counts.items():
|
|
325
|
+
code_summary = f"Total {key_status.description}: {value_total}"
|
|
326
|
+
click.secho(code_summary, fg=key_status.color, bold=True, nl=True)
|
|
327
|
+
|
|
328
|
+
if total_counts.get(FAILED, 0) > 0:
|
|
329
|
+
raise CLIException(FeedbackManager.error_some_data_validation_have_failed())
|
|
330
|
+
if total_counts.get(ERROR, 0) > 0:
|
|
331
|
+
raise CLIException(FeedbackManager.error_some_tests_have_errors())
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_bare_url(url: str) -> str:
|
|
335
|
+
if url.startswith("http://"):
|
|
336
|
+
return url[7:]
|
|
337
|
+
elif url.startswith("https://"):
|
|
338
|
+
return url[8:]
|
|
339
|
+
else:
|
|
340
|
+
return url
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import namedtuple
|
|
3
|
+
from json import JSONEncoder
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from typing_extensions import override
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MyJSONEncoder(JSONEncoder):
|
|
10
|
+
# def default(self, in_obj):
|
|
11
|
+
# loaded_data = [
|
|
12
|
+
# DataUnitTest(
|
|
13
|
+
# getattr(unitDataTest, 'name'),
|
|
14
|
+
# getattr(unitDataTest, 'description'),
|
|
15
|
+
# getattr(unitDataTest, 'enabled'),
|
|
16
|
+
# getattr(unitDataTest, 'endpoint'),
|
|
17
|
+
# getattr(unitDataTest, 'result'),
|
|
18
|
+
# getattr(unitDataTest, 'time'),
|
|
19
|
+
# getattr(unitDataTest, 'sql'))
|
|
20
|
+
# for unitDataTest in in_obj]
|
|
21
|
+
# return loaded_data
|
|
22
|
+
def default(self, obj):
|
|
23
|
+
return obj.to_json()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DataUnitTest:
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
name: str,
|
|
30
|
+
description: str,
|
|
31
|
+
enabled: bool,
|
|
32
|
+
endpoint: Optional[str],
|
|
33
|
+
result: Optional[str],
|
|
34
|
+
time: int,
|
|
35
|
+
sql: str,
|
|
36
|
+
):
|
|
37
|
+
self.name = name
|
|
38
|
+
self.description = description
|
|
39
|
+
self.enabled = enabled
|
|
40
|
+
self.endpoint = endpoint
|
|
41
|
+
self.result = result
|
|
42
|
+
self.time = time
|
|
43
|
+
self.sql = sql
|
|
44
|
+
|
|
45
|
+
def __iter__(self):
|
|
46
|
+
yield from {
|
|
47
|
+
"name": self.name,
|
|
48
|
+
"description": self.description,
|
|
49
|
+
"enabled": self.enabled,
|
|
50
|
+
"endpoint": self.endpoint,
|
|
51
|
+
"sql": self.sql,
|
|
52
|
+
"result": self.result,
|
|
53
|
+
"time": self.time,
|
|
54
|
+
}.items()
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
return json.dumps(dict(self), ensure_ascii=False)
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
def __dict__(self):
|
|
61
|
+
return dict(self)
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
return self.__str__()
|
|
65
|
+
|
|
66
|
+
def to_json(self):
|
|
67
|
+
return self.__str__()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def customDataUnitTestDecoder(dataUnitTestDict):
|
|
71
|
+
return namedtuple("X", dataUnitTestDict.keys())(*dataUnitTestDict.values())
|