tinybird-cli 4.1.2.dev0__tar.gz → 5.0.0__tar.gz

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.
Files changed (46) hide show
  1. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/PKG-INFO +17 -3
  2. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/ch_utils/constants.py +1 -0
  4. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/client.py +21 -9
  5. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/datafile.py +44 -7
  6. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/feedback_manager.py +8 -1
  7. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/sql_toolset.py +69 -16
  8. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/pipe.py +8 -0
  9. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/regions.py +5 -5
  10. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/workspace_members.py +8 -3
  11. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/PKG-INFO +17 -3
  12. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/setup.cfg +0 -0
  13. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/ch_utils/engine.py +0 -0
  14. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/check_pypi.py +0 -0
  15. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/config.py +0 -0
  16. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/connectors.py +0 -0
  17. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/context.py +0 -0
  18. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/datatypes.py +0 -0
  19. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/git_settings.py +0 -0
  20. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/sql.py +0 -0
  21. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/sql_template.py +0 -0
  22. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/sql_template_fmt.py +0 -0
  23. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/syncasync.py +0 -0
  24. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli.py +0 -0
  25. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/auth.py +0 -0
  26. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/branch.py +0 -0
  27. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/cicd.py +0 -0
  28. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/cli.py +0 -0
  29. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/common.py +0 -0
  30. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/connection.py +0 -0
  32. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/datasource.py +0 -0
  33. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  34. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/job.py +0 -0
  35. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  36. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/test.py +0 -0
  37. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  38. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  39. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/token.py +0 -0
  40. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tb_cli_modules/workspace.py +0 -0
  41. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird/tornado_template.py +0 -0
  42. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  43. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  44. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  45. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/requires.txt +0 -0
  46. {tinybird-cli-4.1.2.dev0 → tinybird-cli-5.0.0}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 4.1.2.dev0
3
+ Version: 5.0.0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,6 +18,20 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.0.0
22
+ ******
23
+
24
+ - `Breaking change` Make `insertion_date` column explicit. This column is no longer inferred, it must be present in the Data Source file.
25
+
26
+ Detailed info at https://www.tinybird.co/docs/changelog/2024-06-17-insertion_date-deprecation
27
+
28
+ - `Added` parameter to `tb pipe regression-test` --relative-change
29
+ - `Added` `tb push` displays warnings when using a reserved parameter in a pipe.
30
+ - `Added` --role parameter to `tb workspace members add`
31
+ - `Changed` Point region hosts to new `app.tinybird.co` domain
32
+ - `Improved` help message of `tb workspace members set-role` now displays the 3 valid roles: viewer|guest|admin.
33
+ - `Improved` syntax error messages when parsing endpoints.
34
+
21
35
  4.1.1
22
36
  ************
23
37
 
@@ -27,7 +41,7 @@ Changelog
27
41
  ************
28
42
 
29
43
  - `Added` `tb token create` command to be able to create static and JWT tokens from the CLI. You can check more information at https://www.tinybird.co/blog-posts/jwt-api-endpoints-public-beta
30
- - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
44
+ - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
31
45
 
32
46
  4.0.0
33
47
  ************
@@ -85,7 +99,7 @@ This is a major release, please read the commands affected below and consider up
85
99
  ************
86
100
 
87
101
  - `Changed` fixed major version of tinybird-cli to lower than 4 when using `tb init --git`
88
- - `Changed` behavior when running `tb deploy` on a branch to push the connection settings to the backend. This change is the backend that decides what to do.
102
+ - `Changed` behavior when running `tb deploy` on a branch to push the connection settings to the backend. This change is the backend that decides what to do.
89
103
  - `Changed` `tb pipe sink` commands are now available
90
104
  - `Fixed` regression tests query when filtering by specific parameter
91
105
  - `Fixed` Avoid `tb fmt` to error if there's a `CASE` in the sql
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '4.1.2.dev0'
8
- __revision__ = 'b6a6857'
7
+ __version__ = '5.0.0'
8
+ __revision__ = '2af0ad5'
@@ -4,6 +4,7 @@ LIVE_WS_NAME = "live"
4
4
  SNAPSHOT_WS_NAME = "snapshot"
5
5
 
6
6
  ENABLED_TABLE_FUNCTIONS = {"generateRandom", "null", "numbers", "numbers_mt", "values", "zeros", "zeros_mt"}
7
+ COPY_ENABLED_TABLE_FUNCTIONS = frozenset(["postgresql"])
7
8
 
8
9
  ENABLED_SYSTEM_TABLES = {
9
10
  "functions",
@@ -11,6 +11,7 @@ import requests.adapters
11
11
  from requests import Response
12
12
  from urllib3 import Retry
13
13
 
14
+ from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS
14
15
  from tinybird.syncasync import sync_to_async
15
16
  from tinybird.tb_cli_modules.regions import fill_with_public_regions
16
17
  from tinybird.tb_cli_modules.telemetry import add_telemetry_event
@@ -749,10 +750,12 @@ class TinyB(object):
749
750
  async def delete_branch(self, id: str):
750
751
  return await self._req(f"/v0/environments/{id}", method="DELETE")
751
752
 
752
- async def add_users_to_workspace(self, workspace: Dict[str, Any], users_emails: List[str]):
753
+ async def add_users_to_workspace(self, workspace: Dict[str, Any], users_emails: List[str], role: Optional[str]):
753
754
  users = ",".join(users_emails)
754
755
  return await self._req(
755
- f"/v0/workspaces/{workspace['id']}/users/", method="PUT", data={"operation": "add", "users": users}
756
+ f"/v0/workspaces/{workspace['id']}/users/",
757
+ method="PUT",
758
+ data={"operation": "add", "users": users, "role": role},
756
759
  )
757
760
 
758
761
  async def remove_users_from_workspace(self, workspace: Dict[str, Any], users_emails: List[str]):
@@ -1033,23 +1036,32 @@ class TinyB(object):
1033
1036
  return result["q"]
1034
1037
 
1035
1038
  @staticmethod
1036
- def _sql_get_used_tables_local(sql: str, raising: bool = False) -> List[str]:
1039
+ def _sql_get_used_tables_local(sql: str, raising: bool = False, is_copy: Optional[bool] = False) -> List[str]:
1037
1040
  from tinybird.sql_toolset import sql_get_used_tables
1038
1041
 
1039
- tables = sql_get_used_tables(sql, raising, table_functions=False)
1042
+ tables = sql_get_used_tables(
1043
+ sql, raising, table_functions=False, function_allow_list=COPY_ENABLED_TABLE_FUNCTIONS if is_copy else None
1044
+ )
1040
1045
  return [t[1] if t[0] == "" else f"{t[0]}.{t[1]}" for t in tables]
1041
1046
 
1042
- async def _sql_get_used_tables_remote(self, sql: str, raising: bool = False) -> List[str]:
1043
- params = {"q": sql, "raising": "true" if raising else "false", "table_functions": "false"}
1047
+ async def _sql_get_used_tables_remote(
1048
+ self, sql: str, raising: bool = False, is_copy: Optional[bool] = False
1049
+ ) -> List[str]:
1050
+ params = {
1051
+ "q": sql,
1052
+ "raising": "true" if raising else "false",
1053
+ "table_functions": "false",
1054
+ "is_copy": "true" if is_copy else "false",
1055
+ }
1044
1056
  result = await self._req("/v0/sql_tables", data=params, method="POST")
1045
1057
  return [t[1] if t[0] == "" else f"{t[0]}.{t[1]}" for t in result["tables"]]
1046
1058
 
1047
1059
  # Get used tables from a query. Does not include table functions
1048
- async def sql_get_used_tables(self, sql: str, raising: bool = False) -> List[str]:
1060
+ async def sql_get_used_tables(self, sql: str, raising: bool = False, is_copy: Optional[bool] = False) -> List[str]:
1049
1061
  try:
1050
- return self._sql_get_used_tables_local(sql, raising)
1062
+ return self._sql_get_used_tables_local(sql, raising, is_copy)
1051
1063
  except ModuleNotFoundError:
1052
- return await self._sql_get_used_tables_remote(sql, raising)
1064
+ return await self._sql_get_used_tables_remote(sql, raising, is_copy)
1053
1065
 
1054
1066
  @staticmethod
1055
1067
  def _replace_tables_local(q: str, replacements):
@@ -711,6 +711,7 @@ class Datafile:
711
711
  self.raw: Optional[List[str]] = None
712
712
  self.includes: Dict[str, Any] = {}
713
713
  self.shared_with: List[str] = []
714
+ self.warnings: List[str] = []
714
715
 
715
716
  def validate(self) -> None:
716
717
  for x in self.nodes:
@@ -773,7 +774,8 @@ def parse_pipe(
773
774
  for node in doc.nodes:
774
775
  sql = node.get("sql", "")
775
776
  if sql.strip()[0] == "%":
776
- sql, _, _ = render_sql_template(sql[1:], test_mode=True, name=node["name"])
777
+ sql, _, variable_warnings = render_sql_template(sql[1:], test_mode=True, name=node["name"])
778
+ doc.warnings = variable_warnings
777
779
  # it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
778
780
  from tinybird.sql_toolset import format_sql as toolset_format_sql
779
781
 
@@ -1474,6 +1476,7 @@ async def process_file(
1474
1476
  deps = []
1475
1477
  nodes: List[Dict[str, Any]] = []
1476
1478
 
1479
+ is_copy = any([node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY])
1477
1480
  for node in doc.nodes:
1478
1481
  sql = node["sql"]
1479
1482
  node_type = node.get("type", "standard").lower()
@@ -1502,7 +1505,7 @@ async def process_file(
1502
1505
  sql_rendered = sql
1503
1506
 
1504
1507
  try:
1505
- dependencies = await tb_client.sql_get_used_tables(sql_rendered, raising=True)
1508
+ dependencies = await tb_client.sql_get_used_tables(sql_rendered, raising=True, is_copy=is_copy)
1506
1509
  deps += [t for t in dependencies if t not in [n["name"] for n in doc.nodes]]
1507
1510
 
1508
1511
  except Exception as e:
@@ -1565,6 +1568,7 @@ async def process_file(
1565
1568
  "deps": [x for x in set(deps)],
1566
1569
  "tokens": doc.tokens,
1567
1570
  "description": description,
1571
+ "warnings": doc.warnings,
1568
1572
  }
1569
1573
  ]
1570
1574
  elif DataFileExtensions.TOKEN in filename:
@@ -1766,6 +1770,7 @@ class PipeChecker(unittest.TestCase):
1766
1770
  only_response_times: bool,
1767
1771
  ignore_order: bool,
1768
1772
  validate_processed_bytes: bool,
1773
+ relative_change: float,
1769
1774
  *args,
1770
1775
  **kwargs,
1771
1776
  ) -> None:
@@ -1784,6 +1789,7 @@ class PipeChecker(unittest.TestCase):
1784
1789
  self.only_response_times = only_response_times
1785
1790
  self.ignore_order = ignore_order
1786
1791
  self.validate_processed_bytes = validate_processed_bytes
1792
+ self.relative_change = relative_change
1787
1793
 
1788
1794
  parsed = urlparse(self.current_pipe_url)
1789
1795
  self.checker_pipe_url = f"{parsed.scheme}://{parsed.netloc}/v0/pipes/{self.checker_pipe_name}.json"
@@ -1901,12 +1907,13 @@ class PipeChecker(unittest.TestCase):
1901
1907
  for _, (current_data_e, check_fixtures_data_e) in enumerate(zip(current_data, checker_data)):
1902
1908
  self.assertEqual(list(current_data_e.keys()), list(check_fixtures_data_e.keys()))
1903
1909
  for x in current_data_e.keys():
1904
- if isinstance(current_data_e[x], float):
1910
+ if isinstance(current_data_e[x], (float, int)):
1905
1911
  d = abs(current_data_e[x] - check_fixtures_data_e[x])
1912
+
1906
1913
  try:
1907
1914
  self.assertLessEqual(
1908
1915
  d / current_data_e[x],
1909
- 0.01,
1916
+ self.relative_change,
1910
1917
  f"key {x}. old value: {current_data_e[x]}, new value: {check_fixtures_data_e[x]}\n{self.diff(current_data_e, check_fixtures_data_e)}",
1911
1918
  )
1912
1919
  except ZeroDivisionError:
@@ -2057,6 +2064,7 @@ class PipeCheckerRunner:
2057
2064
  only_response_times: bool,
2058
2065
  ignore_order: bool,
2059
2066
  validate_processed_bytes: bool,
2067
+ relative_change: float,
2060
2068
  ) -> PipeChecker:
2061
2069
  return PipeChecker(
2062
2070
  request,
@@ -2066,6 +2074,7 @@ class PipeCheckerRunner:
2066
2074
  only_response_times,
2067
2075
  ignore_order,
2068
2076
  validate_processed_bytes,
2077
+ relative_change,
2069
2078
  )
2070
2079
 
2071
2080
  def _delta_percentage(self, checker: float, current: float) -> float:
@@ -2085,6 +2094,7 @@ class PipeCheckerRunner:
2085
2094
  only_response_times: bool,
2086
2095
  ignore_order: bool,
2087
2096
  validate_processed_bytes: bool,
2097
+ relative_change: float,
2088
2098
  failfast: bool,
2089
2099
  custom_output: bool = False,
2090
2100
  debug: bool = False,
@@ -2123,7 +2133,13 @@ class PipeCheckerRunner:
2123
2133
  for _, request in enumerate(pipe_requests_to_check):
2124
2134
  suite.addTest(
2125
2135
  self._get_checker(
2126
- request, checker_pipe_name, token, only_response_times, ignore_order, validate_processed_bytes
2136
+ request,
2137
+ checker_pipe_name,
2138
+ token,
2139
+ only_response_times,
2140
+ ignore_order,
2141
+ validate_processed_bytes,
2142
+ relative_change,
2127
2143
  )
2128
2144
  )
2129
2145
 
@@ -2264,6 +2280,7 @@ async def check_pipe(
2264
2280
  populate: bool,
2265
2281
  cl: TinyB,
2266
2282
  limit: int = 0,
2283
+ relative_change: float = 0.01,
2267
2284
  sample_by_params: int = 0,
2268
2285
  only_response_times=False,
2269
2286
  matches: Optional[List[str]] = None,
@@ -2342,6 +2359,7 @@ async def check_pipe(
2342
2359
  only_response_times,
2343
2360
  ignore_order,
2344
2361
  validate_processed_bytes,
2362
+ relative_change,
2345
2363
  failfast,
2346
2364
  )
2347
2365
 
@@ -2556,6 +2574,7 @@ async def new_pipe(
2556
2574
  run_tests: bool = False,
2557
2575
  as_standard: bool = False,
2558
2576
  tests_to_run: int = 0,
2577
+ tests_relative_change: float = 0.01,
2559
2578
  tests_to_sample_by_params: int = 0,
2560
2579
  tests_filter_by: Optional[List[str]] = None,
2561
2580
  tests_failfast: bool = False,
@@ -2600,6 +2619,7 @@ async def new_pipe(
2600
2619
  tb_client,
2601
2620
  only_response_times=only_response_times,
2602
2621
  limit=tests_to_run,
2622
+ relative_change=tests_relative_change,
2603
2623
  sample_by_params=tests_to_sample_by_params,
2604
2624
  matches=tests_filter_by,
2605
2625
  failfast=tests_failfast,
@@ -3206,6 +3226,7 @@ async def exec_file(
3206
3226
  run_tests=False,
3207
3227
  as_standard=False,
3208
3228
  tests_to_run: int = 0,
3229
+ tests_relative_change: float = 0.01,
3209
3230
  tests_to_sample_by_params: int = 0,
3210
3231
  tests_filter_by: Optional[List[str]] = None,
3211
3232
  tests_failfast: bool = False,
@@ -3235,6 +3256,7 @@ async def exec_file(
3235
3256
  run_tests=run_tests,
3236
3257
  as_standard=as_standard,
3237
3258
  tests_to_run=tests_to_run,
3259
+ tests_relative_change=tests_relative_change,
3238
3260
  tests_to_sample_by_params=tests_to_sample_by_params,
3239
3261
  tests_filter_by=tests_filter_by,
3240
3262
  tests_failfast=tests_failfast,
@@ -3587,6 +3609,7 @@ async def build_graph(
3587
3609
  workspace_lib_paths: Optional[List[Tuple[str, str]]],
3588
3610
  ):
3589
3611
  name, kind = filename.rsplit(".", 1)
3612
+ warnings = []
3590
3613
 
3591
3614
  try:
3592
3615
  res = await process_file(
@@ -3607,6 +3630,7 @@ async def build_graph(
3607
3630
 
3608
3631
  for r in res:
3609
3632
  fn = r["resource_name"]
3633
+ warnings = r.get("warnings", [])
3610
3634
  if changed and fn in changed and (not changed[fn] or changed[fn] in ["shared", "remote"]):
3611
3635
  continue
3612
3636
 
@@ -3655,7 +3679,7 @@ async def build_graph(
3655
3679
  r["shared_with"] = mapped_workspaces
3656
3680
 
3657
3681
  dep_map[fn] = set(dep_list)
3658
- return os.path.basename(name)
3682
+ return os.path.basename(name), warnings
3659
3683
 
3660
3684
  processed = set()
3661
3685
 
@@ -3675,9 +3699,20 @@ async def build_graph(
3675
3699
  if ".incl" in filename:
3676
3700
  click.echo(FeedbackManager.warning_skipping_include_file(file=filename))
3677
3701
 
3678
- name = await process(filename, deps, dep_map, to_run, workspace_lib_paths)
3702
+ name, warnings = await process(filename, deps, dep_map, to_run, workspace_lib_paths)
3679
3703
  processed.add(name)
3680
3704
 
3705
+ if verbose:
3706
+ if len(warnings) == 1:
3707
+ click.echo(FeedbackManager.warning_pipe_restricted_param(word=warnings[0]))
3708
+ elif len(warnings) > 1:
3709
+ click.echo(
3710
+ FeedbackManager.warning_pipe_restricted_params(
3711
+ words=", ".join(["'{}'".format(param) for param in warnings[:-1]]),
3712
+ last_word=warnings[-1],
3713
+ )
3714
+ )
3715
+
3681
3716
  await get_processed(filenames=filenames)
3682
3717
 
3683
3718
  if process_dependencies:
@@ -3805,6 +3840,7 @@ async def folder_push(
3805
3840
  raise_on_exists: bool = False,
3806
3841
  verbose: bool = True,
3807
3842
  tests_to_run: int = 0,
3843
+ tests_relative_change: float = 0.01,
3808
3844
  tests_sample_by_params: int = 0,
3809
3845
  tests_filter_by: Optional[List[str]] = None,
3810
3846
  tests_failfast: bool = False,
@@ -3991,6 +4027,7 @@ async def folder_push(
3991
4027
  run_tests,
3992
4028
  as_standard,
3993
4029
  tests_to_run,
4030
+ tests_relative_change,
3994
4031
  tests_sample_by_params,
3995
4032
  tests_filter_by,
3996
4033
  tests_failfast,
@@ -261,7 +261,7 @@ class FeedbackManager:
261
261
  "** Unable to switch to 'main' Workspace. Need to authenticate again, use 'tb auth"
262
262
  )
263
263
  error_parsing_node_with_unclosed_if = error_message(
264
- 'Parsing node "{node}" from pipe "{pipe}":\n Missing in line {lineno} of the SQL:\n {sql}'
264
+ "Missing {{%end%}} block in parsing node '{node}' from pipe '{pipe}':\n Missing in line '{lineno}' of the SQL:\n '{sql}'"
265
265
  )
266
266
  error_unknown_connection = error_message("Unknown connection '{connection}' in Data Source '{datasource}'.")
267
267
  error_unknown_kafka_connection = error_message(
@@ -554,6 +554,13 @@ Ready? """
554
554
  warning_sink_no_connection = warning_message(
555
555
  "** Warning: pipe '{pipe_name}' does not have an associated Sink connection."
556
556
  )
557
+ warning_pipe_restricted_param = warning_message(
558
+ "** The parameter name '{word}' is a reserved word. Please, choose another name or the pipe will not work as expected."
559
+ )
560
+ warning_pipe_restricted_params = warning_message(
561
+ "** The parameter names {words} and '{last_word}' are reserved words. Please, choose another name or the pipe will not work as expected."
562
+ )
563
+
557
564
  info_fixtures_branch = info_message("** Data Fixtures are only pushed to Branches")
558
565
  info_materialize_push_datasource_exists = warning_message("** Data Source {name} already exists")
559
566
  info_materialize_push_datasource_override = prompt_message(
@@ -3,20 +3,26 @@ import logging
3
3
  from collections import defaultdict
4
4
  from datetime import datetime
5
5
  from functools import lru_cache
6
- from typing import Any, List, Optional, Set, Tuple
6
+ from typing import Any, FrozenSet, List, Optional, Set, Tuple
7
7
 
8
8
  from chtoolset import query as chquery
9
9
  from toposort import toposort
10
10
 
11
- from tinybird.ch_utils.constants import ENABLED_TABLE_FUNCTIONS
11
+ from tinybird.ch_utils.constants import COPY_ENABLED_TABLE_FUNCTIONS, ENABLED_TABLE_FUNCTIONS
12
12
 
13
13
  VALID_REMOTE = "VALID_REMOTE"
14
14
 
15
15
 
16
16
  class InvalidFunction(ValueError):
17
17
  def __init__(self, msg: str = "", table_function_names: str = ""):
18
+ if any([fn for fn in COPY_ENABLED_TABLE_FUNCTIONS if fn in msg]):
19
+ msg = msg.replace("is restricted", "is restricted to Copy Pipes")
20
+
18
21
  if table_function_names:
19
- self.msg = f"The query uses disabled table functions: '{table_function_names}'"
22
+ if "postgresql" in table_function_names:
23
+ self.msg = "The postgresql table function is only allowed in Copy Pipes"
24
+ else:
25
+ self.msg = f"The query uses disabled table functions: '{table_function_names}'"
20
26
  else:
21
27
  self.msg = msg
22
28
  super().__init__(self.msg)
@@ -58,7 +64,11 @@ def format_where_for_mutation_command(where_clause: str) -> str:
58
64
 
59
65
  @lru_cache(maxsize=2**13)
60
66
  def sql_get_used_tables_cached(
61
- sql: str, raising: bool = False, default_database: str = "", table_functions: bool = True
67
+ sql: str,
68
+ raising: bool = False,
69
+ default_database: str = "",
70
+ table_functions: bool = True,
71
+ function_allow_list: Optional[FrozenSet[str]] = None,
62
72
  ) -> List[Tuple[str, str, str]]:
63
73
  """
64
74
  >>> sql_get_used_tables("SELECT 1 FROM the_table")
@@ -79,7 +89,11 @@ def sql_get_used_tables_cached(
79
89
  [('d_d3926a', 't_976af08ec4b547419e729c63e754b17b', '')]
80
90
  """
81
91
  try:
82
- tables: List[Tuple[str, str, str]] = chquery.tables(sql, default_database=default_database)
92
+ _function_allow_list = list() if function_allow_list is None else list(function_allow_list)
93
+
94
+ tables: List[Tuple[str, str, str]] = chquery.tables(
95
+ sql, default_database=default_database, function_allow_list=_function_allow_list
96
+ )
83
97
  if not table_functions:
84
98
  return [(t[0], t[1], "") for t in tables if t[0] or t[1]]
85
99
  return tables
@@ -93,10 +107,17 @@ def sql_get_used_tables_cached(
93
107
 
94
108
 
95
109
  def sql_get_used_tables(
96
- sql: str, raising: bool = False, default_database: str = "", table_functions: bool = True
110
+ sql: str,
111
+ raising: bool = False,
112
+ default_database: str = "",
113
+ table_functions: bool = True,
114
+ function_allow_list: Optional[FrozenSet[str]] = None,
97
115
  ) -> List[Tuple[str, str, str]]:
98
- # We return a copy here since the callers might (and replace_tables does) edit the resulting object
99
- return copy.copy(sql_get_used_tables_cached(sql, raising, default_database, table_functions))
116
+ hashable_list = frozenset() if function_allow_list is None else function_allow_list
117
+
118
+ return copy.copy(
119
+ sql_get_used_tables_cached(sql, raising, default_database, table_functions, function_allow_list=hashable_list)
120
+ )
100
121
 
101
122
 
102
123
  class ReplacementsDict(dict):
@@ -150,9 +171,18 @@ def replace_tables_chquery_cached(
150
171
  default_database: str = "",
151
172
  output_one_line: bool = False,
152
173
  timestamp: Optional[datetime] = None,
174
+ function_allow_list: Optional[FrozenSet[str]] = None,
153
175
  ) -> str:
154
176
  replacements = dict(sorted_replacements) if sorted_replacements else {}
155
- return chquery.replace_tables(sql, replacements, default_database=default_database, one_line=output_one_line)
177
+ _function_allow_list = list() if function_allow_list is None else list(function_allow_list)
178
+
179
+ return chquery.replace_tables(
180
+ sql,
181
+ replacements,
182
+ default_database=default_database,
183
+ one_line=output_one_line,
184
+ function_allow_list=_function_allow_list,
185
+ )
156
186
 
157
187
 
158
188
  def replace_tables(
@@ -164,15 +194,19 @@ def replace_tables(
164
194
  valid_tables: Optional[Set[Tuple[str, str]]] = None,
165
195
  output_one_line: bool = False,
166
196
  timestamp: Optional[datetime] = None,
197
+ function_allow_list: Optional[FrozenSet[str]] = None,
167
198
  ) -> str:
168
199
  """
169
200
  Given a query and a list of table replacements, returns the query after applying the table replacements.
170
201
  It takes into account dependencies between replacement subqueries (if any)
171
202
  It also validates the sql to verify it's valid and doesn't use unknown or prohibited functions
172
203
  """
204
+ hashable_list = frozenset() if function_allow_list is None else function_allow_list
173
205
  if not replacements:
174
206
  # Always call replace_tables to do validation and formatting
175
- return replace_tables_chquery_cached(sql, None, output_one_line=output_one_line, timestamp=timestamp)
207
+ return replace_tables_chquery_cached(
208
+ sql, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
209
+ )
176
210
 
177
211
  _replaced_with = set()
178
212
  _replacements = ReplacementsDict()
@@ -182,14 +216,24 @@ def replace_tables(
182
216
  _replaced_with.add(r)
183
217
 
184
218
  deps: defaultdict = defaultdict(set)
185
- _tables = sql_get_used_tables(sql, default_database=default_database, raising=True, table_functions=check_functions)
219
+ _tables = sql_get_used_tables(
220
+ sql,
221
+ default_database=default_database,
222
+ raising=True,
223
+ table_functions=check_functions,
224
+ function_allow_list=function_allow_list,
225
+ )
186
226
  seen_tables = set()
187
227
  table: Tuple[str, str] | Tuple[str, str, str]
228
+ if function_allow_list is None:
229
+ _enabled_table_functions = ENABLED_TABLE_FUNCTIONS
230
+ else:
231
+ _enabled_table_functions = ENABLED_TABLE_FUNCTIONS.union(set(function_allow_list))
188
232
  while _tables:
189
233
  table = _tables.pop()
190
234
  if len(table) == 3:
191
235
  first_table, second_table, last_table = table
192
- if last_table and last_table not in ENABLED_TABLE_FUNCTIONS:
236
+ if last_table and last_table not in _enabled_table_functions:
193
237
  raise InvalidFunction(table_function_names=last_table)
194
238
  if first_table or second_table:
195
239
  table = (first_table, second_table)
@@ -204,7 +248,7 @@ def replace_tables(
204
248
  if len(dependent_table) == 3:
205
249
  if (
206
250
  dependent_table[2]
207
- and dependent_table[2] not in ENABLED_TABLE_FUNCTIONS
251
+ and dependent_table[2] not in _enabled_table_functions
208
252
  and not (dependent_table[2] in ["cluster"] and replacement[0] == VALID_REMOTE)
209
253
  ):
210
254
  raise InvalidFunction(table_function_names=dependent_table[2])
@@ -219,7 +263,9 @@ def replace_tables(
219
263
  deps_sorted = list(reversed(list(toposort(deps))))
220
264
 
221
265
  if not deps_sorted:
222
- return replace_tables_chquery_cached(sql, None, output_one_line=output_one_line, timestamp=timestamp)
266
+ return replace_tables_chquery_cached(
267
+ sql, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
268
+ )
223
269
 
224
270
  for current_deps in deps_sorted:
225
271
  current_replacements = {}
@@ -251,10 +297,17 @@ def replace_tables(
251
297
  # We need to transform the dictionary into something cacheable, so a sorted tuple of tuples it is
252
298
  r = tuple(sorted([(k, v) for k, v in current_replacements.items()]))
253
299
  sql = replace_tables_chquery_cached(
254
- sql, r, default_database=default_database, output_one_line=output_one_line, timestamp=timestamp
300
+ sql,
301
+ r,
302
+ default_database=default_database,
303
+ output_one_line=output_one_line,
304
+ timestamp=timestamp,
305
+ function_allow_list=hashable_list,
255
306
  )
256
307
  else:
257
- sql = replace_tables_chquery_cached(sql, None, output_one_line=output_one_line, timestamp=timestamp)
308
+ sql = replace_tables_chquery_cached(
309
+ sql, None, output_one_line=output_one_line, timestamp=timestamp, function_allow_list=hashable_list
310
+ )
258
311
 
259
312
  return sql
260
313
 
@@ -539,6 +539,12 @@ async def print_pipe(ctx: Context, pipe: str, query: str, format_: str):
539
539
  help="When set, the checker will get Main Workspace requests",
540
540
  hidden=True,
541
541
  )
542
+ @click.option(
543
+ "--relative-change",
544
+ type=float,
545
+ default=0.01,
546
+ help="When set, the checker will validate the new version has less than this distance with the current version",
547
+ )
542
548
  @click.pass_context
543
549
  @coro
544
550
  async def regression_test(
@@ -556,6 +562,7 @@ async def regression_test(
556
562
  ignore_order: bool,
557
563
  validate_processed_bytes: bool,
558
564
  check_requests_from_main: bool,
565
+ relative_change: float,
559
566
  ):
560
567
  """
561
568
  Run regression tests on Tinybird
@@ -583,6 +590,7 @@ async def regression_test(
583
590
  no_versions=no_versions,
584
591
  run_tests=True,
585
592
  tests_to_run=limit,
593
+ tests_relative_change=relative_change,
586
594
  tests_sample_by_params=sample_by_params,
587
595
  tests_filter_by=match,
588
596
  tests_failfast=failfast,
@@ -16,35 +16,35 @@ regions_list: List[Region] = [
16
16
  "name": "europe-west3",
17
17
  "provider": "gcp",
18
18
  "api_host": "https://api.tinybird.co",
19
- "host": "https://ui.tinybird.co",
19
+ "host": "https://app.tinybird.co/gcp/europe-west3",
20
20
  "default_password": "",
21
21
  },
22
22
  {
23
23
  "name": "us-east4",
24
24
  "provider": "gcp",
25
25
  "api_host": "https://api.us-east.tinybird.co",
26
- "host": "https://ui.us-east.tinybird.co",
26
+ "host": "https://app.tinybird.co/gcp/us-east4",
27
27
  "default_password": "",
28
28
  },
29
29
  {
30
30
  "name": "us-east-1",
31
31
  "provider": "aws",
32
32
  "api_host": "https://api.us-east.aws.tinybird.co",
33
- "host": "https://ui.us-east.aws.tinybird.co",
33
+ "host": "https://app.tinybird.co/aws/us-east-1",
34
34
  "default_password": "",
35
35
  },
36
36
  {
37
37
  "name": "us-west-2",
38
38
  "provider": "aws",
39
39
  "api_host": "https://api.us-west-2.aws.tinybird.co",
40
- "host": "https://ui.us-west-2.aws.tinybird.co",
40
+ "host": "https://app.tinybird.co/aws/us-west-2",
41
41
  "default_password": "",
42
42
  },
43
43
  {
44
44
  "name": "eu-central-1",
45
45
  "provider": "aws",
46
46
  "api_host": "https://api.eu-central-1.aws.tinybird.co",
47
- "host": "https://ui.eu-central-1.aws.tinybird.co",
47
+ "host": "https://app.tinybird.co/aws/eu-central-1",
48
48
  "default_password": "",
49
49
  },
50
50
  ]
@@ -22,6 +22,8 @@ from tinybird.tb_cli_modules.common import (
22
22
  from tinybird.tb_cli_modules.exceptions import CLIWorkspaceMembersException
23
23
  from tinybird.tb_cli_modules.workspace import workspace
24
24
 
25
+ ROLES = ["viewer", "guest", "admin"]
26
+
25
27
 
26
28
  @dataclass
27
29
  class WorkspaceMemberCommandContext:
@@ -55,10 +57,13 @@ def members(ctx: Context) -> None:
55
57
 
56
58
  @members.command(name="add", short_help="Adds members to the current Workspace")
57
59
  @click.argument("members_emails")
60
+ @click.option("--role", is_flag=False, default=None, help="Role for the members being added", type=click.Choice(ROLES))
58
61
  @click.option("--user_token", is_flag=False, default=None, help="When passed, we won't prompt asking for it")
59
62
  @click.pass_context
60
63
  @coro
61
- async def add_members_to_workspace(ctx: Context, members_emails: str, user_token: Optional[str]) -> None:
64
+ async def add_members_to_workspace(
65
+ ctx: Context, members_emails: str, user_token: Optional[str], role: Optional[str]
66
+ ) -> None:
62
67
  """Adds members to the current Workspace."""
63
68
 
64
69
  cmd_ctx = await get_command_context(ctx)
@@ -79,7 +84,7 @@ async def add_members_to_workspace(ctx: Context, members_emails: str, user_token
79
84
 
80
85
  user_client: TinyB = deepcopy(cmd_ctx.client)
81
86
  user_client.token = user_token
82
- await user_client.add_users_to_workspace(cmd_ctx.workspace, users_to_add)
87
+ await user_client.add_users_to_workspace(cmd_ctx.workspace, users_to_add, role)
83
88
  msg = (
84
89
  FeedbackManager.success_workspace_user_added(user=users_to_add[0], workspace_name=cmd_ctx.workspace["name"])
85
90
  if len(users_to_add) == 1
@@ -149,7 +154,7 @@ async def remove_members_from_workspace(ctx: Context, members_emails: str, user_
149
154
 
150
155
 
151
156
  @members.command(name="set-role", short_help="Sets the role for existing workspace members")
152
- @click.argument("role", required=True, type=click.Choice(["guest", "admin"]))
157
+ @click.argument("role", required=True, type=click.Choice(ROLES))
153
158
  @click.argument("members_emails", required=True, type=str)
154
159
  @click.option("--user_token", is_flag=False, default=None, help="When passed, we won't prompt asking for it")
155
160
  @click.pass_context
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 4.1.2.dev0
3
+ Version: 5.0.0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,6 +18,20 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.0.0
22
+ ******
23
+
24
+ - `Breaking change` Make `insertion_date` column explicit. This column is no longer inferred, it must be present in the Data Source file.
25
+
26
+ Detailed info at https://www.tinybird.co/docs/changelog/2024-06-17-insertion_date-deprecation
27
+
28
+ - `Added` parameter to `tb pipe regression-test` --relative-change
29
+ - `Added` `tb push` displays warnings when using a reserved parameter in a pipe.
30
+ - `Added` --role parameter to `tb workspace members add`
31
+ - `Changed` Point region hosts to new `app.tinybird.co` domain
32
+ - `Improved` help message of `tb workspace members set-role` now displays the 3 valid roles: viewer|guest|admin.
33
+ - `Improved` syntax error messages when parsing endpoints.
34
+
21
35
  4.1.1
22
36
  ************
23
37
 
@@ -27,7 +41,7 @@ Changelog
27
41
  ************
28
42
 
29
43
  - `Added` `tb token create` command to be able to create static and JWT tokens from the CLI. You can check more information at https://www.tinybird.co/blog-posts/jwt-api-endpoints-public-beta
30
- - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
44
+ - `Fixed` `tb init --git` to pin `tinybird-cli>=4,<5` in `requirements.txt` to avoid issues with the latest version of the CLI.
31
45
 
32
46
  4.0.0
33
47
  ************
@@ -85,7 +99,7 @@ This is a major release, please read the commands affected below and consider up
85
99
  ************
86
100
 
87
101
  - `Changed` fixed major version of tinybird-cli to lower than 4 when using `tb init --git`
88
- - `Changed` behavior when running `tb deploy` on a branch to push the connection settings to the backend. This change is the backend that decides what to do.
102
+ - `Changed` behavior when running `tb deploy` on a branch to push the connection settings to the backend. This change is the backend that decides what to do.
89
103
  - `Changed` `tb pipe sink` commands are now available
90
104
  - `Fixed` regression tests query when filtering by specific parameter
91
105
  - `Fixed` Avoid `tb fmt` to error if there's a `CASE` in the sql