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.

Files changed (43) hide show
  1. tinybird/__cli__.py +7 -8
  2. tinybird/check_pypi.py +1 -1
  3. tinybird/feedback_manager.py +2 -2
  4. tinybird/tb/cli.py +28 -0
  5. tinybird/{tb_cli_modules → tb/modules}/auth.py +5 -5
  6. tinybird/{tb_cli_modules → tb/modules}/branch.py +6 -5
  7. tinybird/{tb_cli_modules → tb/modules}/build.py +8 -26
  8. tinybird/tb/modules/cicd.py +271 -0
  9. tinybird/{tb_cli_modules → tb/modules}/cli.py +24 -91
  10. tinybird/tb/modules/common.py +2098 -0
  11. tinybird/tb/modules/config.py +352 -0
  12. tinybird/{tb_cli_modules → tb/modules}/connection.py +4 -4
  13. tinybird/{tb_cli_modules → tb/modules}/create.py +11 -7
  14. tinybird/{datafile.py → tb/modules/datafile.py} +6 -7
  15. tinybird/{tb_cli_modules → tb/modules}/datasource.py +7 -6
  16. tinybird/tb/modules/exceptions.py +91 -0
  17. tinybird/{tb_cli_modules → tb/modules}/fmt.py +3 -3
  18. tinybird/{tb_cli_modules → tb/modules}/job.py +3 -3
  19. tinybird/{tb_cli_modules → tb/modules}/llm.py +14 -13
  20. tinybird/{tb_cli_modules → tb/modules}/local.py +11 -6
  21. tinybird/tb/modules/mock.py +53 -0
  22. tinybird/{tb_cli_modules → tb/modules}/pipe.py +5 -5
  23. tinybird/{tb_cli_modules → tb/modules}/prompts.py +3 -0
  24. tinybird/tb/modules/regions.py +9 -0
  25. tinybird/tb/modules/table.py +185 -0
  26. tinybird/{tb_cli_modules → tb/modules}/tag.py +2 -2
  27. tinybird/tb/modules/telemetry.py +310 -0
  28. tinybird/{tb_cli_modules → tb/modules}/test.py +5 -5
  29. tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit.py +1 -1
  30. tinybird/{tb_cli_modules → tb/modules}/token.py +3 -3
  31. tinybird/{tb_cli_modules → tb/modules}/workspace.py +5 -5
  32. tinybird/{tb_cli_modules → tb/modules}/workspace_members.py +4 -4
  33. tinybird/tb_cli_modules/common.py +9 -7
  34. tinybird/tb_cli_modules/config.py +0 -8
  35. {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev6.dist-info}/METADATA +1 -1
  36. tinybird-0.0.1.dev6.dist-info/RECORD +58 -0
  37. tinybird-0.0.1.dev6.dist-info/entry_points.txt +2 -0
  38. tinybird/tb_cli.py +0 -27
  39. tinybird-0.0.1.dev4.dist-info/RECORD +0 -50
  40. tinybird-0.0.1.dev4.dist-info/entry_points.txt +0 -2
  41. /tinybird/{tb_cli_modules → tb/modules}/tinyunit/tinyunit_lib.py +0 -0
  42. {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev6.dist-info}/WHEEL +0 -0
  43. {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.tb_cli_modules.branch import warn_if_in_live
22
- from tinybird.tb_cli_modules.cli import cli
23
- from tinybird.tb_cli_modules.common import coro, create_tb_client, echo_safe_humanfriendly_tables_format_smart_table
24
- from tinybird.tb_cli_modules.exceptions import CLIPipeException
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()
@@ -130,4 +130,7 @@ FROM numbers({row_count})
130
130
  - Do NOT add a semicolon at the end of the query
131
131
  - Do NOT add any FORMAT at the end of the query, because it will be added later by Tinybird.
132
132
 
133
+ # Extra context:
134
+ {context}
135
+
133
136
  """
@@ -0,0 +1,9 @@
1
+ from typing import Optional, TypedDict
2
+
3
+
4
+ class Region(TypedDict):
5
+ name: str
6
+ provider: str
7
+ api_host: str
8
+ host: str
9
+ default_password: Optional[str]
@@ -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.tb_cli_modules.cli import cli
8
- from tinybird.tb_cli_modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table
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.tb_cli_modules.cli import cli
14
- from tinybird.tb_cli_modules.common import coro, create_tb_client, gather_with_concurrency
15
- from tinybird.tb_cli_modules.config import CLIConfig
16
- from tinybird.tb_cli_modules.exceptions import CLIException
17
- from tinybird.tb_cli_modules.tinyunit.tinyunit import (
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.tb_cli_modules.common import CLIException
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.tb_cli_modules.cli import cli
12
- from tinybird.tb_cli_modules.common import (
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.tb_cli_modules.exceptions import CLITokenException
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.tb_cli_modules.cli import cli
16
- from tinybird.tb_cli_modules.common import (
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.tb_cli_modules.config import CLIConfig
30
- from tinybird.tb_cli_modules.exceptions import CLIWorkspaceException
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()