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.

Files changed (45) hide show
  1. tinybird/__cli__.py +8 -0
  2. tinybird/ch_utils/constants.py +244 -0
  3. tinybird/ch_utils/engine.py +855 -0
  4. tinybird/check_pypi.py +25 -0
  5. tinybird/client.py +1281 -0
  6. tinybird/config.py +117 -0
  7. tinybird/connectors.py +428 -0
  8. tinybird/context.py +23 -0
  9. tinybird/datafile.py +5589 -0
  10. tinybird/datatypes.py +434 -0
  11. tinybird/feedback_manager.py +1022 -0
  12. tinybird/git_settings.py +145 -0
  13. tinybird/sql.py +865 -0
  14. tinybird/sql_template.py +2343 -0
  15. tinybird/sql_template_fmt.py +281 -0
  16. tinybird/sql_toolset.py +350 -0
  17. tinybird/syncasync.py +682 -0
  18. tinybird/tb_cli.py +25 -0
  19. tinybird/tb_cli_modules/auth.py +252 -0
  20. tinybird/tb_cli_modules/branch.py +1043 -0
  21. tinybird/tb_cli_modules/cicd.py +434 -0
  22. tinybird/tb_cli_modules/cli.py +1571 -0
  23. tinybird/tb_cli_modules/common.py +2082 -0
  24. tinybird/tb_cli_modules/config.py +344 -0
  25. tinybird/tb_cli_modules/connection.py +803 -0
  26. tinybird/tb_cli_modules/datasource.py +900 -0
  27. tinybird/tb_cli_modules/exceptions.py +91 -0
  28. tinybird/tb_cli_modules/fmt.py +91 -0
  29. tinybird/tb_cli_modules/job.py +85 -0
  30. tinybird/tb_cli_modules/pipe.py +858 -0
  31. tinybird/tb_cli_modules/regions.py +9 -0
  32. tinybird/tb_cli_modules/tag.py +100 -0
  33. tinybird/tb_cli_modules/telemetry.py +310 -0
  34. tinybird/tb_cli_modules/test.py +107 -0
  35. tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
  36. tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
  37. tinybird/tb_cli_modules/token.py +349 -0
  38. tinybird/tb_cli_modules/workspace.py +269 -0
  39. tinybird/tb_cli_modules/workspace_members.py +212 -0
  40. tinybird/tornado_template.py +1194 -0
  41. tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
  42. tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
  43. tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
  44. tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
  45. 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())