tinybird 0.0.1.dev4__py3-none-any.whl → 0.0.1.dev6__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 +7 -8
- tinybird/check_pypi.py +1 -1
- tinybird/feedback_manager.py +2 -2
- tinybird/tb/cli.py +28 -0
- tinybird/{tb_cli_modules → tb/modules}/auth.py +5 -5
- tinybird/{tb_cli_modules → tb/modules}/branch.py +6 -5
- tinybird/{tb_cli_modules → tb/modules}/build.py +8 -26
- tinybird/tb/modules/cicd.py +271 -0
- tinybird/{tb_cli_modules → tb/modules}/cli.py +24 -91
- tinybird/tb/modules/common.py +2098 -0
- tinybird/tb/modules/config.py +352 -0
- tinybird/{tb_cli_modules → tb/modules}/connection.py +4 -4
- tinybird/{tb_cli_modules → tb/modules}/create.py +11 -7
- tinybird/{datafile.py → tb/modules/datafile.py} +6 -7
- tinybird/{tb_cli_modules → tb/modules}/datasource.py +7 -6
- tinybird/tb/modules/exceptions.py +91 -0
- tinybird/{tb_cli_modules → tb/modules}/fmt.py +3 -3
- tinybird/{tb_cli_modules → tb/modules}/job.py +3 -3
- tinybird/{tb_cli_modules → tb/modules}/llm.py +14 -13
- tinybird/{tb_cli_modules → tb/modules}/local.py +11 -6
- tinybird/tb/modules/mock.py +53 -0
- tinybird/{tb_cli_modules → tb/modules}/pipe.py +5 -5
- tinybird/{tb_cli_modules → tb/modules}/prompts.py +3 -0
- tinybird/tb/modules/regions.py +9 -0
- tinybird/tb/modules/table.py +185 -0
- tinybird/{tb_cli_modules → tb/modules}/tag.py +2 -2
- tinybird/tb/modules/telemetry.py +310 -0
- tinybird/{tb_cli_modules → tb/modules}/test.py +5 -5
- tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit.py +1 -1
- tinybird/{tb_cli_modules → tb/modules}/token.py +3 -3
- tinybird/{tb_cli_modules → tb/modules}/workspace.py +5 -5
- tinybird/{tb_cli_modules → tb/modules}/workspace_members.py +4 -4
- tinybird/tb_cli_modules/common.py +9 -7
- tinybird/tb_cli_modules/config.py +0 -8
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev6.dist-info}/METADATA +1 -1
- tinybird-0.0.1.dev6.dist-info/RECORD +58 -0
- tinybird-0.0.1.dev6.dist-info/entry_points.txt +2 -0
- tinybird/tb_cli.py +0 -27
- tinybird-0.0.1.dev4.dist-info/RECORD +0 -50
- tinybird-0.0.1.dev4.dist-info/entry_points.txt +0 -2
- /tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev6.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
7
|
+
from tinybird.tb.modules.cli import cli
|
|
8
|
+
from tinybird.tb.modules.common import CLIException, coro
|
|
9
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
10
|
+
from tinybird.tb.modules.llm import LLM
|
|
11
|
+
from tinybird.tb.modules.local import get_tinybird_local_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command()
|
|
15
|
+
@click.argument("datasource", type=str)
|
|
16
|
+
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
17
|
+
@click.option("--context", type=str, default="", help="Extra context to use for data generation")
|
|
18
|
+
@coro
|
|
19
|
+
async def mock(datasource: str, rows: int, context: str) -> None:
|
|
20
|
+
"""Load sample data into a datasource.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
ctx: Click context object
|
|
24
|
+
datasource_file: Path to the datasource file to load sample data into
|
|
25
|
+
"""
|
|
26
|
+
import llm
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
datasource_path = Path(datasource)
|
|
30
|
+
datasource_name = datasource
|
|
31
|
+
if datasource_path.suffix == ".datasource":
|
|
32
|
+
datasource_name = datasource_path.stem
|
|
33
|
+
else:
|
|
34
|
+
datasource_path = Path("datasources", f"{datasource}.datasource")
|
|
35
|
+
|
|
36
|
+
datasource_content = datasource_path.read_text()
|
|
37
|
+
llm_config = CLIConfig.get_llm_config()
|
|
38
|
+
llm = LLM(key=llm_config["api_key"])
|
|
39
|
+
tb_client = get_tinybird_local_client()
|
|
40
|
+
sql = await llm.generate_sql_sample_data(tb_client, datasource_content, rows, context)
|
|
41
|
+
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
42
|
+
data = result.get("data", [])
|
|
43
|
+
max_rows_per_request = 100
|
|
44
|
+
sent_rows = 0
|
|
45
|
+
for i in range(0, len(data), max_rows_per_request):
|
|
46
|
+
batch = data[i : i + max_rows_per_request]
|
|
47
|
+
ndjson_data = "\n".join([json.dumps(row) for row in batch])
|
|
48
|
+
await tb_client.datasource_events(datasource_name, ndjson_data)
|
|
49
|
+
sent_rows += len(batch)
|
|
50
|
+
click.echo(f"Sent {sent_rows} events to datasource '{datasource_name}'")
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
@@ -16,12 +16,12 @@ from click import Context
|
|
|
16
16
|
import tinybird.context as context
|
|
17
17
|
from tinybird.client import AuthNoTokenException, DoesNotExistException, TinyB
|
|
18
18
|
from tinybird.config import DEFAULT_API_HOST, FeatureFlags
|
|
19
|
-
from tinybird.datafile import PipeNodeTypes, PipeTypes, folder_push, get_name_version, process_file, wait_job
|
|
20
19
|
from tinybird.feedback_manager import FeedbackManager
|
|
21
|
-
from tinybird.
|
|
22
|
-
from tinybird.
|
|
23
|
-
from tinybird.
|
|
24
|
-
from tinybird.
|
|
20
|
+
from tinybird.tb.modules.branch import warn_if_in_live
|
|
21
|
+
from tinybird.tb.modules.cli import cli
|
|
22
|
+
from tinybird.tb.modules.common import coro, create_tb_client, echo_safe_humanfriendly_tables_format_smart_table
|
|
23
|
+
from tinybird.tb.modules.datafile import PipeNodeTypes, PipeTypes, folder_push, get_name_version, process_file, wait_job
|
|
24
|
+
from tinybird.tb.modules.exceptions import CLIPipeException
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@cli.group()
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Standard library modules.
|
|
2
|
+
import collections
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Modules included in our package.
|
|
6
|
+
from humanfriendly.compat import coerce_string
|
|
7
|
+
from humanfriendly.tables import format_robust_table
|
|
8
|
+
from humanfriendly.terminal import (
|
|
9
|
+
ansi_strip,
|
|
10
|
+
ansi_width,
|
|
11
|
+
ansi_wrap,
|
|
12
|
+
find_terminal_size,
|
|
13
|
+
terminal_supports_colors,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from tinybird.feedback_manager import bcolors
|
|
17
|
+
|
|
18
|
+
NUMERIC_DATA_PATTERN = re.compile(r"^\d+(\.\d+)?$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_table(data, columns):
|
|
22
|
+
"""
|
|
23
|
+
Render tabular data using the most appropriate representation.
|
|
24
|
+
|
|
25
|
+
:param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
|
|
26
|
+
containing the rows of the table, where each row is an
|
|
27
|
+
iterable containing the columns of the table (strings).
|
|
28
|
+
:param column_names: An iterable of column names (strings).
|
|
29
|
+
:returns: The rendered table (a string).
|
|
30
|
+
|
|
31
|
+
If you want an easy way to render tabular data on a terminal in a human
|
|
32
|
+
friendly format then this function is for you! It works as follows:
|
|
33
|
+
|
|
34
|
+
- If the input data doesn't contain any line breaks the function
|
|
35
|
+
:func:`format_pretty_table()` is used to render a pretty table. If the
|
|
36
|
+
resulting table fits in the terminal without wrapping the rendered pretty
|
|
37
|
+
table is returned.
|
|
38
|
+
|
|
39
|
+
- If the input data does contain line breaks or if a pretty table would
|
|
40
|
+
wrap (given the width of the terminal) then the function
|
|
41
|
+
:func:`format_robust_table()` is used to render a more robust table that
|
|
42
|
+
can deal with data containing line breaks and long text.
|
|
43
|
+
"""
|
|
44
|
+
# Normalize the input in case we fall back from a pretty table to a robust
|
|
45
|
+
# table (in which case we'll definitely iterate the input more than once).
|
|
46
|
+
data = [normalize_columns(r) for r in data]
|
|
47
|
+
|
|
48
|
+
column_names = []
|
|
49
|
+
column_types = []
|
|
50
|
+
|
|
51
|
+
for c in columns:
|
|
52
|
+
column_names.append(c["name"])
|
|
53
|
+
column_types.append(c["type"])
|
|
54
|
+
|
|
55
|
+
column_names = normalize_columns(column_names)
|
|
56
|
+
# Make sure the input data doesn't contain any line breaks (because pretty
|
|
57
|
+
# tables break horribly when a column's text contains a line break :-).
|
|
58
|
+
if not any(any("\n" in c for c in r) for r in data):
|
|
59
|
+
# Render a pretty table.
|
|
60
|
+
pretty_table = format_pretty_table(data, column_names, column_types)
|
|
61
|
+
# Check if the pretty table fits in the terminal.
|
|
62
|
+
table_width = max(map(ansi_width, pretty_table.splitlines()))
|
|
63
|
+
num_rows, num_columns = find_terminal_size()
|
|
64
|
+
if table_width <= num_columns:
|
|
65
|
+
# The pretty table fits in the terminal without wrapping!
|
|
66
|
+
return pretty_table
|
|
67
|
+
# Fall back to a robust table when a pretty table won't work.
|
|
68
|
+
return format_robust_table(data, column_names)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_pretty_table(data, column_names=None, column_types=None, horizontal_bar="─", vertical_bar=" "):
|
|
72
|
+
"""
|
|
73
|
+
Render a table using characters like dashes and vertical bars to emulate borders.
|
|
74
|
+
|
|
75
|
+
:param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
|
|
76
|
+
containing the rows of the table, where each row is an
|
|
77
|
+
iterable containing the columns of the table (strings).
|
|
78
|
+
:param column_names: An iterable of column names (strings).
|
|
79
|
+
:param column_types: An iterable of column types (strings).
|
|
80
|
+
:param horizontal_bar: The character used to represent a horizontal bar (a
|
|
81
|
+
string).
|
|
82
|
+
:param vertical_bar: The character used to represent a vertical bar (a
|
|
83
|
+
string).
|
|
84
|
+
:returns: The rendered table (a string).
|
|
85
|
+
|
|
86
|
+
Here's an example:
|
|
87
|
+
|
|
88
|
+
>>> from humanfriendly.tables import format_pretty_table
|
|
89
|
+
>>> column_names = ['Version', 'Uploaded on', 'Downloads']
|
|
90
|
+
>>> humanfriendly_releases = [
|
|
91
|
+
... ['1.23', '2015-05-25', '218'],
|
|
92
|
+
... ['1.23.1', '2015-05-26', '1354'],
|
|
93
|
+
... ['1.24', '2015-05-26', '223'],
|
|
94
|
+
... ['1.25', '2015-05-26', '4319'],
|
|
95
|
+
... ['1.25.1', '2015-06-02', '197'],
|
|
96
|
+
... ]
|
|
97
|
+
>>> print(format_pretty_table(humanfriendly_releases, column_names))
|
|
98
|
+
-------------------------------------
|
|
99
|
+
| Version | Uploaded on | Downloads |
|
|
100
|
+
-------------------------------------
|
|
101
|
+
| 1.23 | 2015-05-25 | 218 |
|
|
102
|
+
| 1.23.1 | 2015-05-26 | 1354 |
|
|
103
|
+
| 1.24 | 2015-05-26 | 223 |
|
|
104
|
+
| 1.25 | 2015-05-26 | 4319 |
|
|
105
|
+
| 1.25.1 | 2015-06-02 | 197 |
|
|
106
|
+
-------------------------------------
|
|
107
|
+
|
|
108
|
+
Notes about the resulting table:
|
|
109
|
+
|
|
110
|
+
- If a column contains numeric data (integer and/or floating point
|
|
111
|
+
numbers) in all rows (ignoring column names of course) then the content
|
|
112
|
+
of that column is right-aligned, as can be seen in the example above. The
|
|
113
|
+
idea here is to make it easier to compare the numbers in different
|
|
114
|
+
columns to each other.
|
|
115
|
+
|
|
116
|
+
- The column names are highlighted in color so they stand out a bit more
|
|
117
|
+
(see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what
|
|
118
|
+
that looks like (my terminals are always set to white text on a black
|
|
119
|
+
background):
|
|
120
|
+
|
|
121
|
+
.. image:: images/pretty-table.png
|
|
122
|
+
"""
|
|
123
|
+
# Normalize the input because we'll have to iterate it more than once.
|
|
124
|
+
data = [normalize_columns(r, expandtabs=True) for r in data]
|
|
125
|
+
if column_names is not None:
|
|
126
|
+
column_names = normalize_columns(column_names)
|
|
127
|
+
if column_names:
|
|
128
|
+
if terminal_supports_colors():
|
|
129
|
+
column_names = [highlight_column_name(n) for n in column_names]
|
|
130
|
+
data.insert(0, column_names)
|
|
131
|
+
if column_types is not None:
|
|
132
|
+
column_types = normalize_columns(column_types)
|
|
133
|
+
column_types = [highlight_column_type(t) for t in column_types]
|
|
134
|
+
data.insert(1, column_types)
|
|
135
|
+
# Calculate the maximum width of each column.
|
|
136
|
+
widths = collections.defaultdict(int)
|
|
137
|
+
numeric_data = collections.defaultdict(list)
|
|
138
|
+
for row_index, row in enumerate(data):
|
|
139
|
+
for column_index, column in enumerate(row):
|
|
140
|
+
widths[column_index] = max(widths[column_index], ansi_width(column))
|
|
141
|
+
if not (column_names and row_index <= 1): # Skip both header rows for numeric check
|
|
142
|
+
numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column))))
|
|
143
|
+
|
|
144
|
+
horizontal_bar = highlight_horizontal_bar(horizontal_bar)
|
|
145
|
+
# Create a horizontal bar of dashes as a delimiter.
|
|
146
|
+
line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1)
|
|
147
|
+
# Start the table with a vertical bar.
|
|
148
|
+
lines = []
|
|
149
|
+
# Format the rows and columns.
|
|
150
|
+
for row_index, row in enumerate(data):
|
|
151
|
+
line = [vertical_bar]
|
|
152
|
+
for column_index, column in enumerate(row):
|
|
153
|
+
padding = " " * (widths[column_index] - ansi_width(column))
|
|
154
|
+
if all(numeric_data[column_index]):
|
|
155
|
+
line.append(" " + padding + column + " ")
|
|
156
|
+
else:
|
|
157
|
+
line.append(" " + column + padding + " ")
|
|
158
|
+
line.append(vertical_bar)
|
|
159
|
+
lines.append("".join(line))
|
|
160
|
+
if column_names and column_types and row_index > 0 and row_index < len(data) - 1:
|
|
161
|
+
lines.append(line_delimiter)
|
|
162
|
+
# Join the lines, returning a single string.
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def normalize_columns(row, expandtabs=False):
|
|
167
|
+
results = []
|
|
168
|
+
for value in row:
|
|
169
|
+
text = coerce_string(value)
|
|
170
|
+
if expandtabs:
|
|
171
|
+
text = text.expandtabs()
|
|
172
|
+
results.append(text)
|
|
173
|
+
return results
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def highlight_column_name(name):
|
|
177
|
+
return ansi_wrap(name)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def highlight_column_type(type):
|
|
181
|
+
return f"{bcolors.CGREY}\033[3m{type}\033[23m{bcolors.ENDC}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def highlight_horizontal_bar(horizontal_bar):
|
|
185
|
+
return f"{bcolors.CGREY}\033[3m{horizontal_bar}\033[23m{bcolors.ENDC}"
|
|
@@ -4,8 +4,8 @@ import click
|
|
|
4
4
|
from click import Context
|
|
5
5
|
|
|
6
6
|
from tinybird.feedback_manager import FeedbackManager
|
|
7
|
-
from tinybird.
|
|
8
|
-
from tinybird.
|
|
7
|
+
from tinybird.tb.modules.cli import cli
|
|
8
|
+
from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@cli.group()
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
12
|
+
from urllib.parse import urlencode
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from tinybird.config import CURRENT_VERSION
|
|
17
|
+
|
|
18
|
+
TELEMETRY_TIMEOUT: int = 1
|
|
19
|
+
TELEMETRY_DATASOURCE: str = "tb_cli_telemetry"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_ci_product_name() -> Optional[str]:
|
|
23
|
+
if _is_env_true("TB_DISABLE_CI_DETECTION"):
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
CI_CHECKS: List[Tuple[str, Callable[[], bool]]] = [
|
|
27
|
+
("Azure pipelines", lambda: _is_env_true("TF_BUILD")),
|
|
28
|
+
("GitHub Actions", lambda: _is_env_true("GITHUB_ACTIONS")),
|
|
29
|
+
("Appveyor", lambda: _is_env_true("APPVEYOR")),
|
|
30
|
+
("Travis CI", lambda: _is_env_true("TRAVIS")),
|
|
31
|
+
("Circle CI", lambda: _is_env_true("CIRCLECI")),
|
|
32
|
+
("Amazon Web Services CodeBuild", lambda: _is_env_present(["CODEBUILD_BUILD_ID", "AWS_REGION"])),
|
|
33
|
+
("Jenkins", lambda: _is_env_present(["BUILD_ID", "BUILD_URL"])),
|
|
34
|
+
("Google Cloud Build", lambda: _is_env_present(["BUILD_ID", "PROJECT_ID"])),
|
|
35
|
+
("TeamCity", lambda: _is_env_present(["TEAMCITY_VERSION"])),
|
|
36
|
+
("JetBrains Space", lambda: _is_env_present(["JB_SPACE_API_URL"])),
|
|
37
|
+
("Generic CI", lambda: _is_env_true("CI")),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
return next((check[0] for check in CI_CHECKS if check[1]()), None)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_ci_environment() -> bool:
|
|
44
|
+
ci_product: Optional[str] = get_ci_product_name()
|
|
45
|
+
return ci_product is not None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def silence_errors(f: Callable) -> Callable:
|
|
49
|
+
"""Decorator to silence all errors in the decorated
|
|
50
|
+
function.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@functools.wraps(f)
|
|
54
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
55
|
+
try:
|
|
56
|
+
return f(*args, **kwargs)
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
return wrapper
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_env_true(env_var: str) -> bool:
|
|
64
|
+
"""Checks if `env_var` is `true` or `1`."""
|
|
65
|
+
return os.getenv(env_var, "").lower() in ("true", "1")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _is_env_present(envs: List[str]) -> bool:
|
|
69
|
+
"""Checks if all of the variables passed in `envs`
|
|
70
|
+
are defined (ie: not empty)
|
|
71
|
+
"""
|
|
72
|
+
return all(os.getenv(env_var, None) is not None for env_var in envs)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _hide_tokens(text: str) -> str:
|
|
76
|
+
"""Cuts any token in a way that they get unusable if leaked,
|
|
77
|
+
but we still can use them for debugging if needed.
|
|
78
|
+
"""
|
|
79
|
+
return re.sub(r"p\.ey[A-Za-z0-9-_\.]+", lambda s: f"{s[0][:10]}...{s[0][-10:]}", text)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TelemetryHelper:
|
|
83
|
+
def __init__(self, tb_host: Optional[str] = None, max_enqueued_events: int = 5) -> None:
|
|
84
|
+
self.tb_host = tb_host or os.getenv("TB_CLI_TELEMETRY_HOST", "https://api.tinybird.co")
|
|
85
|
+
self.max_enqueued_events: int = max_enqueued_events
|
|
86
|
+
|
|
87
|
+
self.enabled: bool = True
|
|
88
|
+
self.events: List[Dict[str, Any]] = []
|
|
89
|
+
self.telemetry_token: Optional[str] = None
|
|
90
|
+
|
|
91
|
+
run_id = str(uuid.uuid4())
|
|
92
|
+
|
|
93
|
+
self._defaults: Dict[str, Any] = {
|
|
94
|
+
# Per-event values
|
|
95
|
+
"event": "<the event>",
|
|
96
|
+
"event_data": "<the event data>",
|
|
97
|
+
"timestamp": "<the timestamp>",
|
|
98
|
+
# Static values
|
|
99
|
+
"run_id": run_id,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
self._threads: List[threading.Thread] = []
|
|
103
|
+
self.log(f"Telemetry initialized with run_id: {run_id}")
|
|
104
|
+
|
|
105
|
+
@silence_errors
|
|
106
|
+
def add_event(self, event: str, event_data: Dict[str, Any]) -> None:
|
|
107
|
+
if not self.enabled:
|
|
108
|
+
self.log("Helper is disabled")
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
if "x.y.z" in CURRENT_VERSION and not _is_env_true("TB_CLI_TELEMETRY_SEND_IN_LOCAL"):
|
|
112
|
+
self.log("Not sending events in local development mode")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Let's save deep copies to not interfere with original objects
|
|
116
|
+
event_dict: Dict[str, Any] = deepcopy(self._defaults)
|
|
117
|
+
event_dict["event"] = event
|
|
118
|
+
event_dict["event_data"] = json.dumps(event_data)
|
|
119
|
+
event_dict["timestamp"] = datetime.utcnow().isoformat()
|
|
120
|
+
|
|
121
|
+
self.events.append(event_dict)
|
|
122
|
+
if len(self.events) >= self.max_enqueued_events:
|
|
123
|
+
self.flush()
|
|
124
|
+
|
|
125
|
+
@silence_errors
|
|
126
|
+
def flush(self, wait: bool = False) -> None:
|
|
127
|
+
if self.enabled and len(self.events) > 0:
|
|
128
|
+
# Take the ownership for the pending events.
|
|
129
|
+
#
|
|
130
|
+
# We need this because the proper flush() is done in
|
|
131
|
+
# a thread to avoid blocking the user and we could send
|
|
132
|
+
# the same event twice if we maintain the same list after
|
|
133
|
+
# during the sending.
|
|
134
|
+
|
|
135
|
+
events: List[Dict[str, Any]] = self.events
|
|
136
|
+
self.events = []
|
|
137
|
+
|
|
138
|
+
self.log(f"Flusing {len(events)} events in a new thread...")
|
|
139
|
+
thread: threading.Thread = threading.Thread(target=self._flush, args=[events])
|
|
140
|
+
self._threads.append(thread)
|
|
141
|
+
thread.start()
|
|
142
|
+
|
|
143
|
+
if wait:
|
|
144
|
+
for t in self._threads:
|
|
145
|
+
t.join()
|
|
146
|
+
if t.is_alive():
|
|
147
|
+
self.log(f"Couldn't wait for the end of the thread {t.name}")
|
|
148
|
+
self._threads.clear()
|
|
149
|
+
|
|
150
|
+
@silence_errors
|
|
151
|
+
def _flush(self, events: List[Dict[str, Any]]) -> None:
|
|
152
|
+
"""Actual flush. This is where we use HFI to ingest events."""
|
|
153
|
+
|
|
154
|
+
timeout: int
|
|
155
|
+
try:
|
|
156
|
+
timeout = int(os.getenv("TB_CLI_TELEMETRY_TIMEOUT", TELEMETRY_TIMEOUT))
|
|
157
|
+
timeout = max(TELEMETRY_TIMEOUT, timeout)
|
|
158
|
+
except ValueError:
|
|
159
|
+
timeout = TELEMETRY_TIMEOUT
|
|
160
|
+
|
|
161
|
+
if not self.telemetry_token:
|
|
162
|
+
self.telemetry_token = os.getenv("TB_CLI_TELEMETRY_TOKEN")
|
|
163
|
+
if self.telemetry_token:
|
|
164
|
+
self.log("Got telemetry token from environment TB_CLI_TELEMETRY_TOKEN")
|
|
165
|
+
|
|
166
|
+
with requests.Session() as session:
|
|
167
|
+
if not self.telemetry_token:
|
|
168
|
+
url: str = f"{self.tb_host}/v0/regions"
|
|
169
|
+
self.log(f"Requesting token from {url}...")
|
|
170
|
+
try:
|
|
171
|
+
r = session.get(url, timeout=timeout)
|
|
172
|
+
regions: List[Dict[str, Any]] = json.loads(r.content.decode())["regions"]
|
|
173
|
+
self.telemetry_token = next(
|
|
174
|
+
(r.get("telemetry_token", None) for r in regions if r["api_host"] == self.tb_host), None
|
|
175
|
+
)
|
|
176
|
+
if self.telemetry_token:
|
|
177
|
+
self.log(f"Got telemetry token from {url}")
|
|
178
|
+
except requests.exceptions.Timeout:
|
|
179
|
+
self.log(f"Disabling due to timeout after {timeout} seconds")
|
|
180
|
+
self.enabled = False
|
|
181
|
+
return
|
|
182
|
+
except Exception as ex:
|
|
183
|
+
self.log(str(ex))
|
|
184
|
+
|
|
185
|
+
if not self.telemetry_token:
|
|
186
|
+
self.log("Disabling due to lack of token")
|
|
187
|
+
self.enabled = False
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
self.log(f"token={self.telemetry_token}")
|
|
191
|
+
|
|
192
|
+
data: str = _hide_tokens("\n".join(json.dumps(e) for e in events))
|
|
193
|
+
|
|
194
|
+
# Note we don't use `wait` as this telemetry isn't a critical
|
|
195
|
+
# operation to support and we don't want to generate overhead
|
|
196
|
+
params: Dict[str, Any] = {"name": TELEMETRY_DATASOURCE, "token": self.telemetry_token}
|
|
197
|
+
url = f"{self.tb_host}/v0/events?{urlencode(params)}"
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
self.log(f"Sending data to {url}...")
|
|
201
|
+
r = session.post(url, data=data, timeout=timeout)
|
|
202
|
+
except requests.exceptions.Timeout:
|
|
203
|
+
self.log(f"Disabling due to timeout after {timeout} seconds")
|
|
204
|
+
self.enabled = False
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
self.log(f"Received status {r.status_code}: {r.text}")
|
|
208
|
+
|
|
209
|
+
if r.status_code == 200 or r.status_code == 202:
|
|
210
|
+
self.log(f"Successfully sent {len(events)} events to {self.tb_host}")
|
|
211
|
+
self.events.clear()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if r.status_code in (403, 404):
|
|
215
|
+
self.log(f"Disabling due to {r.status_code} errors")
|
|
216
|
+
self.enabled = False
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
if r.status_code >= 500:
|
|
220
|
+
self.log(f"Disabling telemetry and discarding {len(events)} events")
|
|
221
|
+
self.enabled = False
|
|
222
|
+
|
|
223
|
+
@silence_errors
|
|
224
|
+
def log(self, msg: str) -> None:
|
|
225
|
+
"""Internal logging function to help with development and debugging."""
|
|
226
|
+
if not _is_env_true("TB_CLI_TELEMETRY_DEBUG"):
|
|
227
|
+
return
|
|
228
|
+
print(f"> Telemetry: {msg}") # noqa: T201
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
_helper_instance: Optional[TelemetryHelper] = None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@silence_errors
|
|
235
|
+
def init_telemetry() -> None:
|
|
236
|
+
"""Setups the telemetry helper with the config present in `config`.
|
|
237
|
+
If no config is provided, it tries to get it from the passed Click context.
|
|
238
|
+
|
|
239
|
+
We need to call this method any time we suspect the config changes any value.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
telemetry = _get_helper()
|
|
243
|
+
if telemetry:
|
|
244
|
+
telemetry.log("Initialized")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@silence_errors
|
|
248
|
+
def add_telemetry_event(event: str, **kw_event_data: Any) -> None:
|
|
249
|
+
"""Adds a new telemetry event."""
|
|
250
|
+
|
|
251
|
+
telemetry = _get_helper()
|
|
252
|
+
if not telemetry:
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
telemetry.add_event(event, dict(**kw_event_data))
|
|
257
|
+
except Exception as ex:
|
|
258
|
+
telemetry.log(str(ex))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@silence_errors
|
|
262
|
+
def add_telemetry_sysinfo_event() -> None:
|
|
263
|
+
"""Collects system info and sends a `system_info` event
|
|
264
|
+
with the data.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
ci_product: Optional[str] = get_ci_product_name()
|
|
268
|
+
|
|
269
|
+
add_telemetry_event(
|
|
270
|
+
"system_info",
|
|
271
|
+
platform=platform.platform(),
|
|
272
|
+
system=platform.system(),
|
|
273
|
+
arch=platform.machine(),
|
|
274
|
+
processor=platform.processor(),
|
|
275
|
+
python_runtime=platform.python_implementation(),
|
|
276
|
+
python_version=platform.python_version(),
|
|
277
|
+
is_ci=ci_product is not None,
|
|
278
|
+
ci_product=ci_product,
|
|
279
|
+
cli_version=CURRENT_VERSION,
|
|
280
|
+
cli_args=sys.argv[1:] if len(sys.argv) > 1 else [],
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@silence_errors
|
|
285
|
+
def flush_telemetry(wait: bool = False) -> None:
|
|
286
|
+
"""Flushes all pending telemetry events."""
|
|
287
|
+
|
|
288
|
+
telemetry = _get_helper()
|
|
289
|
+
if not telemetry:
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
telemetry.flush(wait=wait)
|
|
294
|
+
except Exception as ex:
|
|
295
|
+
telemetry.log(str(ex))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@silence_errors
|
|
299
|
+
def _get_helper() -> Optional[TelemetryHelper]:
|
|
300
|
+
"""Returns the shared TelemetryHelper instance."""
|
|
301
|
+
|
|
302
|
+
if _is_env_true("TB_CLI_TELEMETRY_OPTOUT"):
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
global _helper_instance
|
|
306
|
+
|
|
307
|
+
if not _helper_instance:
|
|
308
|
+
_helper_instance = TelemetryHelper()
|
|
309
|
+
|
|
310
|
+
return _helper_instance
|
|
@@ -10,11 +10,11 @@ import click
|
|
|
10
10
|
|
|
11
11
|
from tinybird.client import AuthNoTokenException
|
|
12
12
|
from tinybird.feedback_manager import FeedbackManager
|
|
13
|
-
from tinybird.
|
|
14
|
-
from tinybird.
|
|
15
|
-
from tinybird.
|
|
16
|
-
from tinybird.
|
|
17
|
-
from tinybird.
|
|
13
|
+
from tinybird.tb.modules.cli import cli
|
|
14
|
+
from tinybird.tb.modules.common import coro, create_tb_client, gather_with_concurrency
|
|
15
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
16
|
+
from tinybird.tb.modules.exceptions import CLIException
|
|
17
|
+
from tinybird.tb.modules.tinyunit.tinyunit import (
|
|
18
18
|
TestSummaryResults,
|
|
19
19
|
generate_file,
|
|
20
20
|
parse_file,
|
|
@@ -9,7 +9,7 @@ from typing_extensions import override
|
|
|
9
9
|
|
|
10
10
|
from tinybird.client import TinyB
|
|
11
11
|
from tinybird.feedback_manager import FeedbackManager
|
|
12
|
-
from tinybird.
|
|
12
|
+
from tinybird.tb.modules.common import CLIException
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
@dataclass
|
|
@@ -8,13 +8,13 @@ from humanfriendly import parse_timespan
|
|
|
8
8
|
|
|
9
9
|
from tinybird.client import AuthNoTokenException, TinyB
|
|
10
10
|
from tinybird.feedback_manager import FeedbackManager
|
|
11
|
-
from tinybird.
|
|
12
|
-
from tinybird.
|
|
11
|
+
from tinybird.tb.modules.cli import cli
|
|
12
|
+
from tinybird.tb.modules.common import (
|
|
13
13
|
DoesNotExistException,
|
|
14
14
|
coro,
|
|
15
15
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
16
16
|
)
|
|
17
|
-
from tinybird.
|
|
17
|
+
from tinybird.tb.modules.exceptions import CLITokenException
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@cli.group()
|
|
@@ -10,10 +10,9 @@ from click import Context
|
|
|
10
10
|
|
|
11
11
|
from tinybird.client import CanNotBeDeletedException, DoesNotExistException, TinyB
|
|
12
12
|
from tinybird.config import get_display_host
|
|
13
|
-
from tinybird.datafile import PipeTypes
|
|
14
13
|
from tinybird.feedback_manager import FeedbackManager
|
|
15
|
-
from tinybird.
|
|
16
|
-
from tinybird.
|
|
14
|
+
from tinybird.tb.modules.cli import cli
|
|
15
|
+
from tinybird.tb.modules.common import (
|
|
17
16
|
_get_workspace_plan_name,
|
|
18
17
|
ask_for_user_token,
|
|
19
18
|
check_user_token,
|
|
@@ -26,8 +25,9 @@ from tinybird.tb_cli_modules.common import (
|
|
|
26
25
|
print_current_workspace,
|
|
27
26
|
switch_workspace,
|
|
28
27
|
)
|
|
29
|
-
from tinybird.
|
|
30
|
-
from tinybird.
|
|
28
|
+
from tinybird.tb.modules.config import CLIConfig
|
|
29
|
+
from tinybird.tb.modules.datafile import PipeTypes
|
|
30
|
+
from tinybird.tb.modules.exceptions import CLIWorkspaceException
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
@cli.group()
|