tinybird-cli 4.0.2.dev0__tar.gz → 4.1.1__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 (47) hide show
  1. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/PKG-INFO +15 -4
  2. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/client.py +17 -2
  4. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/context.py +1 -0
  5. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/datafile.py +5 -5
  6. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/feedback_manager.py +10 -0
  7. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/sql_template.py +92 -66
  8. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/cicd.py +1 -1
  9. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/common.py +2 -1
  10. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/datasource.py +4 -2
  11. tinybird-cli-4.1.1/tinybird/tb_cli_modules/token.py +335 -0
  12. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tornado_template.py +9 -1
  13. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird_cli.egg-info/PKG-INFO +15 -4
  14. tinybird-cli-4.0.2.dev0/tinybird/tb_cli_modules/token.py +0 -127
  15. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/setup.cfg +0 -0
  16. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/ch_utils/constants.py +0 -0
  17. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/ch_utils/engine.py +0 -0
  18. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/check_pypi.py +0 -0
  19. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/config.py +0 -0
  20. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/connectors.py +0 -0
  21. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/datatypes.py +0 -0
  22. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/git_settings.py +0 -0
  23. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/sql.py +0 -0
  24. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/sql_template_fmt.py +0 -0
  25. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/sql_toolset.py +0 -0
  26. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/syncasync.py +0 -0
  27. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli.py +0 -0
  28. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/auth.py +0 -0
  29. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/branch.py +0 -0
  30. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/cli.py +0 -0
  31. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/config.py +0 -0
  32. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/connection.py +0 -0
  33. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  34. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/job.py +0 -0
  35. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/pipe.py +0 -0
  36. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/regions.py +0 -0
  37. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  40. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  41. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/workspace.py +0 -0
  42. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  43. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  44. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  45. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird_cli.egg-info/entry_points.txt +0 -0
  46. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/tinybird_cli.egg-info/requires.txt +0 -0
  47. {tinybird-cli-4.0.2.dev0 → tinybird-cli-4.1.1}/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.0.2.dev0
3
+ Version: 4.1.1
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,15 +18,26 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 4.1.1
22
+ ************
23
+
24
+ - `Fixed` workspace info gathering when switching branches. We were prioritizing the general workspace properties over the user ones.
25
+
26
+ 4.1.0
27
+ ************
28
+
29
+ - `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.
31
+
21
32
  4.0.0
22
33
  ************
23
34
 
24
- This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to these version.
35
+ This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to this version.
25
36
 
26
37
  - `Deprecated` `--semver` flag and `tb release` commands are now deprecated. You can keep using `tb deploy` to integrate and deploy from git. Changes are deployed to the main Workspace instead of to a Release.
27
- - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an exmple https://github.com/tinybirdco/ci
38
+ - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an example https://github.com/tinybirdco/ci
28
39
  - `Removed` `tb env` command is removed, use `tb branch` instead.
29
- - `Deprecated` .datasource files with `ENGINE "Join"` are deprecated, use `Engine "MergeeTree"` instead.
40
+ - `Deprecated` .datasource files with `ENGINE "Join"` is deprecated, use `Engine "MergeeTree"` instead.
30
41
  - `Deprecated` `tb materialize`
31
42
  - `Removed` Drop the `--timeout` flag from `tb push` which made the populate job to timeout. You can use now `--wait` to wait for the job to finish or nothing to just create the job and return.
32
43
  - `Removed` Support for `KEY` directive is removed. The `KEY` was used to create a Data Source with Join engine by the given `KEY` column name. Join engines are also deprecated, you can use a regular `MergeTree` Data Source instead and adapt the pipes SQL accordingly.
@@ -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.0.2.dev0'
8
- __revision__ = 'dbd0d5c'
7
+ __version__ = '4.1.1'
8
+ __revision__ = 'b6a6857'
@@ -209,11 +209,21 @@ class TinyB(object):
209
209
  return None
210
210
 
211
211
  async def create_token(
212
- self, name: str, scope: str, origin_code: Optional[str], origin_resource_name_or_id: Optional[str]
212
+ self, name: str, scope: List[str], origin_code: Optional[str], origin_resource_name_or_id: Optional[str] = None
213
213
  ):
214
214
  origin = origin_code or "C" # == Origins.CUSTOM if none specified
215
+ params = {
216
+ "name": name,
217
+ "origin": origin,
218
+ }
219
+ if origin_resource_name_or_id:
220
+ params["resource_id"] = origin_resource_name_or_id
221
+
222
+ # TODO: We should support sending multiple scopes in the body of the request
223
+ url = f"/v0/tokens?{urlencode(params)}"
224
+ url = url + "&" + "&".join([f"scope={scope}" for scope in scope])
215
225
  return await self._req(
216
- f"/v0/tokens?name={name}&scope={scope}&origin={origin}&resource_id={origin_resource_name_or_id}",
226
+ url,
217
227
  method="POST",
218
228
  data="",
219
229
  )
@@ -1163,6 +1173,11 @@ class TinyB(object):
1163
1173
  params = self._token_to_params(token)
1164
1174
  return await self._req(f"/v0/tokens?{params}", method="POST", data="")
1165
1175
 
1176
+ async def create_jwt_token(self, name: str, expiration_time: int, scopes: List[Dict[str, Any]]):
1177
+ url_params = {"name": name, "expiration_time": expiration_time}
1178
+ body = json.dumps({"scopes": scopes})
1179
+ return await self._req(f"/v0/tokens?{urlencode(url_params)}", method="POST", data=body)
1180
+
1166
1181
  async def token_update(self, token: Dict[str, Any]):
1167
1182
  name = token["name"]
1168
1183
  params = self._token_to_params(token)
@@ -7,6 +7,7 @@ hfi_frequency: ContextVar[float] = ContextVar("hfi_frequency")
7
7
  hfi_frequency_gatherer: ContextVar[float] = ContextVar("hfi_frequency_gatherer")
8
8
  use_gatherer: ContextVar[bool] = ContextVar("use_gatherer")
9
9
  allow_gatherer_fallback: ContextVar[bool] = ContextVar("allow_gatherer_fallback")
10
+ gatherer_allow_s3_backup_on_user_errors: ContextVar[bool] = ContextVar("gatherer_allow_s3_backup_on_user_errors")
10
11
  disable_template_security_validation: ContextVar[bool] = ContextVar("disable_template_security_validation")
11
12
  origin: ContextVar[str] = ContextVar("origin")
12
13
  request_id: ContextVar[str] = ContextVar("request_id")
@@ -773,7 +773,7 @@ def parse_pipe(
773
773
  for node in doc.nodes:
774
774
  sql = node.get("sql", "")
775
775
  if sql.strip()[0] == "%":
776
- sql, _ = render_sql_template(sql[1:], test_mode=True, name=node["name"])
776
+ sql, _, _ = render_sql_template(sql[1:], test_mode=True, name=node["name"])
777
777
  # it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
778
778
  from tinybird.sql_toolset import format_sql as toolset_format_sql
779
779
 
@@ -785,7 +785,7 @@ def parse_pipe(
785
785
  )
786
786
  )
787
787
  except ValueError as e:
788
- t, template_variables = get_template_and_variables(sql, name=node["name"])
788
+ t, template_variables, _ = get_template_and_variables(sql, name=node["name"])
789
789
 
790
790
  if sql.strip()[0] != "%" and len(template_variables) > 0:
791
791
  raise click.ClickException(FeedbackManager.error_template_start(filename=filename))
@@ -1492,7 +1492,7 @@ async def process_file(
1492
1492
  is_template = False
1493
1493
  if sql[0] == "%":
1494
1494
  try:
1495
- sql_rendered, _ = render_sql_template(sql[1:], test_mode=True)
1495
+ sql_rendered, _, _ = render_sql_template(sql[1:], test_mode=True)
1496
1496
  except Exception as e:
1497
1497
  raise click.ClickException(
1498
1498
  FeedbackManager.error_parsing_node(node=node["name"], pipe=name, error=str(e))
@@ -2785,7 +2785,7 @@ async def new_pipe(
2785
2785
  click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
2786
2786
  try:
2787
2787
  r = await tb_client.create_token(
2788
- token_name, f"PIPES:{tk['permissions']}:{p['name']}", "P", p["name"]
2788
+ token_name, [f"PIPES:{tk['permissions']}:{p['name']}"], "P", p["name"]
2789
2789
  )
2790
2790
  token = r["token"] # type: ignore
2791
2791
  except Exception as e:
@@ -2896,7 +2896,7 @@ async def new_ds(
2896
2896
  token_name = tk["token_name"]
2897
2897
  click.echo(FeedbackManager.info_create_not_found_token(token=token_name))
2898
2898
  # DS == token_origin.Origins.DATASOURCE
2899
- await client.create_token(token_name, f"DATASOURCES:{tk['permissions']}:{ds_name}", "DS", ds_name)
2899
+ await client.create_token(token_name, [f"DATASOURCES:{tk['permissions']}:{ds_name}"], "DS", ds_name)
2900
2900
  else:
2901
2901
  click.echo(FeedbackManager.info_create_found_token(token=token_name))
2902
2902
  scopes = [f"DATASOURCES:{tk['permissions']}:{ds_name}"]
@@ -348,6 +348,13 @@ class FeedbackManager:
348
348
  "{connector} Data sources require a post-release deployment. Increment the post-release number of the semver (for example: 0.0.1 -> 0.0.1-1) to do so. You can read more about post-releases at https://www.tinybird.co/docs/production/deployment-strategies"
349
349
  )
350
350
 
351
+ error_number_of_scopes_and_resources_mismatch = error_message(
352
+ "The number of --scope and --resource options must be the same"
353
+ )
354
+ error_number_of_fixed_params_and_resources_mismatch = error_message(
355
+ "The number of --fixed-params options must not exceed the number of --scope and --resource options."
356
+ )
357
+
351
358
  info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
352
359
  info_ignoring_incl_file = info_message(
353
360
  "** Ignoring file {filename}. .incl files are not checked independently. They are checked as part of the file that includes them. Please check the file that includes this .incl file."
@@ -422,6 +429,9 @@ Ready? """
422
429
  "You are going to manually update workspace commit reference manually, this is just for special occasions. Do you want to update current commit reference '{current_commit}' to '{new_commit}'?"
423
430
  )
424
431
 
432
+ warning_exchange = warning_message(
433
+ "Warning: Do you want to exchange Data Source {datasource_a} by Data Source {datasource_b}?"
434
+ )
425
435
  warning_no_test_results = warning_message("Warning: No test results to show")
426
436
  warning_using_branch_token = warning_message("** You're using the token defined in $TB_TOKEN.")
427
437
  warning_using_branch_host = warning_message("** You're using the token defined in $TB_HOST.")
@@ -42,6 +42,24 @@ class SQLTemplateException(ValueError):
42
42
 
43
43
 
44
44
  DEFAULT_PARAM_NAMES = ["format", "q"]
45
+ RESERVED_PARAM_NAMES = [
46
+ "__tb__semver",
47
+ "debug_source_tables",
48
+ "debug",
49
+ "explain",
50
+ "finalize_aggregations",
51
+ "output_format_json_quote_64bit_integers",
52
+ "output_format_json_quote_denormals",
53
+ "output_format_parquet_string_as_string",
54
+ "pipeline",
55
+ "playground",
56
+ "q",
57
+ "query_id",
58
+ "release_replacements",
59
+ "tag",
60
+ "template_parameters",
61
+ "token",
62
+ ]
45
63
 
46
64
  parameter_types = [
47
65
  "String",
@@ -1771,6 +1789,8 @@ def get_template_and_variables(sql: str, name: Optional[str], escape_arrays: boo
1771
1789
  it is important to NOT MODIFY THESE OBJECTS.
1772
1790
  Neither render_sql_template() or generate() modify them, so neither should you
1773
1791
  """
1792
+ variable_warnings = []
1793
+
1774
1794
  try:
1775
1795
  t = Template(sql, name)
1776
1796
  template_variables = get_var_names(t)
@@ -1780,10 +1800,12 @@ def get_template_and_variables(sql: str, name: Optional[str], escape_arrays: boo
1780
1800
  name = variable["name"]
1781
1801
  line = variable["line"]
1782
1802
  raise ValueError(f'"{name}" can not be used as a variable name, line {line}')
1803
+ if variable["name"] in RESERVED_PARAM_NAMES:
1804
+ variable_warnings.append(variable["name"])
1783
1805
 
1784
1806
  wrap_vars(t, escape_arrays=escape_arrays)
1785
1807
 
1786
- return t, template_variables
1808
+ return t, template_variables, variable_warnings
1787
1809
  except SecurityException as e:
1788
1810
  raise SQLTemplateException(e)
1789
1811
 
@@ -1836,96 +1858,96 @@ def render_sql_template(
1836
1858
  variables: Optional[dict] = None,
1837
1859
  test_mode: bool = False,
1838
1860
  name: Optional[str] = None,
1839
- ) -> Tuple[str, dict]:
1861
+ ) -> Tuple[str, dict, list]:
1840
1862
  """
1841
1863
  >>> render_sql_template("select * from table where f = {{Float32(foo)}}", { 'foo': -1 })
1842
- ("select * from table where f = toFloat32('-1.0')", {})
1864
+ ("select * from table where f = toFloat32('-1.0')", {}, [])
1843
1865
  >>> render_sql_template("{% if defined(open) %}ERROR{% else %}YEAH!{% end %}")
1844
- ('YEAH!', {})
1866
+ ('YEAH!', {}, [])
1845
1867
  >>> render_sql_template("{% if defined(close) %}ERROR{% else %}YEAH!{% end %}")
1846
- ('YEAH!', {})
1868
+ ('YEAH!', {}, [])
1847
1869
  >>> render_sql_template("{% if defined(input) %}ERROR{% else %}YEAH!{% end %}")
1848
- ('YEAH!', {})
1870
+ ('YEAH!', {}, [])
1849
1871
  >>> render_sql_template("{% if defined(print) %}ERROR{% else %}YEAH!{% end %}")
1850
- ('YEAH!', {})
1872
+ ('YEAH!', {}, [])
1851
1873
  >>> render_sql_template("select * from table where str = {{foo}}", { 'foo': 'test' })
1852
- ("select * from table where str = 'test'", {})
1874
+ ("select * from table where str = 'test'", {}, [])
1853
1875
  >>> render_sql_template("select * from table where f = {{foo}}", { 'foo': 1.0 })
1854
- ('select * from table where f = 1.0', {})
1876
+ ('select * from table where f = 1.0', {}, [])
1855
1877
  >>> render_sql_template("select {{Boolean(foo)}} from table", { 'foo': True })
1856
- ('select 1 from table', {})
1878
+ ('select 1 from table', {}, [])
1857
1879
  >>> render_sql_template("select {{Boolean(foo)}} from table", { 'foo': False })
1858
- ('select 0 from table', {})
1880
+ ('select 0 from table', {}, [])
1859
1881
  >>> render_sql_template("select * from table where f = {{Float32(foo)}}", { 'foo': 1 })
1860
- ("select * from table where f = toFloat32('1.0')", {})
1882
+ ("select * from table where f = toFloat32('1.0')", {}, [])
1861
1883
  >>> render_sql_template("select * from table where f = {{foo}}", { 'foo': "';drop table users;" })
1862
- ("select * from table where f = '\\\\';drop table users;'", {})
1884
+ ("select * from table where f = '\\\\';drop table users;'", {}, [])
1863
1885
  >>> render_sql_template("select * from {{symbol(foo)}}", { 'foo': 'table-name' })
1864
- ('select * from `table-name`', {})
1886
+ ('select * from `table-name`', {}, [])
1865
1887
  >>> render_sql_template("select * from {{symbol(foo)}}", { 'foo': '"table-name"' })
1866
- ('select * from `table-name`', {})
1888
+ ('select * from `table-name`', {}, [])
1867
1889
  >>> render_sql_template("select * from {{table(foo)}}", { 'foo': '"table-name"' })
1868
- ('select * from table-name', {})
1890
+ ('select * from table-name', {}, [])
1869
1891
  >>> render_sql_template("select * from {{Int32(foo)}}", { 'foo': 'non_int' })
1870
1892
  Traceback (most recent call last):
1871
1893
  ...
1872
1894
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error validating 'non_int' to type Int32
1873
1895
  >>> render_sql_template("select * from table where f = {{Float32(foo)}}", test_mode=True)
1874
- ("select * from table where f = toFloat32('0.0')", {})
1896
+ ("select * from table where f = toFloat32('0.0')", {}, [])
1875
1897
  >>> render_sql_template("SELECT * FROM query_log__dev where a = {{test}}", test_mode=True)
1876
- ("SELECT * FROM query_log__dev where a = '__placeholder__'", {})
1898
+ ("SELECT * FROM query_log__dev where a = '__placeholder__'", {}, [])
1877
1899
  >>> render_sql_template("SELECT {{test}}", {'token':'testing'})
1878
1900
  Traceback (most recent call last):
1879
1901
  ...
1880
1902
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: expression "test" evaluated to null
1881
1903
  >>> render_sql_template('{% if test %}SELECT 1{% else %} select 2 {% end %}')
1882
- (' select 2 ', {})
1904
+ (' select 2 ', {}, [])
1883
1905
  >>> render_sql_template('{% if Int32(test, 1) %}SELECT 1{% else %} select 2 {% end %}')
1884
- ('SELECT 1', {})
1906
+ ('SELECT 1', {}, [])
1885
1907
  >>> render_sql_template('{% for v in test %}SELECT {{v}} {% end %}',test_mode=True)
1886
- ("SELECT '__placeholder__' SELECT '__placeholder__' SELECT '__placeholder__' ", {})
1908
+ ("SELECT '__placeholder__' SELECT '__placeholder__' SELECT '__placeholder__' ", {}, [])
1887
1909
  >>> render_sql_template("select {{Int32(foo, 1)}}", test_mode=True)
1888
- ("select toInt32('1')", {})
1910
+ ("select toInt32('1')", {}, [])
1889
1911
  >>> render_sql_template("SELECT count() c FROM test_table where a > {{Float32(myvar)}} {% if defined(my_condition) %} and c = Int32({{my_condition}}){% end %}", {'myvar': 1.0})
1890
- ("SELECT count() c FROM test_table where a > toFloat32('1.0') ", {})
1912
+ ("SELECT count() c FROM test_table where a > toFloat32('1.0') ", {}, [])
1891
1913
  >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=a, b=b)}}", {'a': '1', 'b': '2'})
1892
- ("SELECT count() c FROM where a = '1' and b = '2'", {})
1914
+ ("SELECT count() c FROM where a = '1' and b = '2'", {}, [])
1893
1915
  >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=a, b=b)}}", {'b': '2'})
1894
- ("SELECT count() c FROM where b = '2'", {})
1916
+ ("SELECT count() c FROM where b = '2'", {}, [])
1895
1917
  >>> render_sql_template("SELECT count() c FROM where {{sql_and(a=Int(a, defined=False), b=Int(b, defined=False))}}", {'b': '2'})
1896
- ('SELECT count() c FROM where b = 2', {})
1918
+ ('SELECT count() c FROM where b = 2', {}, [])
1897
1919
  >>> render_sql_template("SELECT count() c FROM where {{sql_and(a__in=Array(a), b=b)}}", {'a': 'a,b,c','b': '2'})
1898
- ("SELECT count() c FROM where a in ['a','b','c'] and b = '2'", {})
1920
+ ("SELECT count() c FROM where a in ['a','b','c'] and b = '2'", {}, [])
1899
1921
  >>> render_sql_template("SELECT count() c FROM where {{sql_and(a__not_in=Array(a), b=b)}}", {'a': 'a,b,c','b': '2'})
1900
- ("SELECT count() c FROM where a not in ['a','b','c'] and b = '2'", {})
1922
+ ("SELECT count() c FROM where a not in ['a','b','c'] and b = '2'", {}, [])
1901
1923
  >>> render_sql_template("SELECT c FROM where a > {{Date(start)}}", test_mode=True)
1902
- ("SELECT c FROM where a > '2019-01-01'", {})
1924
+ ("SELECT c FROM where a > '2019-01-01'", {}, [])
1903
1925
  >>> render_sql_template("SELECT c FROM where a > {{DateTime(start)}}", test_mode=True)
1904
- ("SELECT c FROM where a > '2019-01-01 00:00:00'", {})
1926
+ ("SELECT c FROM where a > '2019-01-01 00:00:00'", {}, [])
1905
1927
  >>> render_sql_template("SELECT c FROM where a > {{DateTime(start)}}", {'start': '2018-09-07 23:55:00'})
1906
- ("SELECT c FROM where a > '2018-09-07 23:55:00'", {})
1928
+ ("SELECT c FROM where a > '2018-09-07 23:55:00'", {}, [])
1907
1929
  >>> render_sql_template('SELECT * FROM tracker {% if defined(start) %} {{DateTime(start)}} and {{DateTime(end)}} {% end %}', {'start': '2019-08-01 00:00:00', 'end': '2019-08-02 00:00:00'})
1908
- ("SELECT * FROM tracker '2019-08-01 00:00:00' and '2019-08-02 00:00:00' ", {})
1930
+ ("SELECT * FROM tracker '2019-08-01 00:00:00' and '2019-08-02 00:00:00' ", {}, [])
1909
1931
  >>> render_sql_template('SELECT * from test limit {{Int(limit)}}', test_mode=True)
1910
- ('SELECT * from test limit 0', {})
1932
+ ('SELECT * from test limit 0', {}, [])
1911
1933
  >>> render_sql_template('SELECT {{symbol(attr)}} from test', test_mode=True)
1912
- ('SELECT `placeholder` from test', {})
1934
+ ('SELECT `placeholder` from test', {}, [])
1913
1935
  >>> render_sql_template('SELECT {{Array(foo)}}', {'foo': 'a,b,c,d'})
1914
- ("SELECT ['a','b','c','d']", {})
1936
+ ("SELECT ['a','b','c','d']", {}, [])
1915
1937
  >>> render_sql_template("SELECT {{Array(foo, 'Int32')}}", {'foo': '1,2,3,4'})
1916
- ('SELECT [1,2,3,4]', {})
1938
+ ('SELECT [1,2,3,4]', {}, [])
1917
1939
  >>> render_sql_template("SELECT {{Array(foo, 'Int32')}}", test_mode=True)
1918
- ('SELECT [0,0]', {})
1940
+ ('SELECT [0,0]', {}, [])
1919
1941
  >>> render_sql_template("SELECT {{Array(foo)}}", test_mode=True)
1920
- ("SELECT ['__placeholder__0','__placeholder__1']", {})
1942
+ ("SELECT ['__placeholder__0','__placeholder__1']", {}, [])
1921
1943
  >>> render_sql_template("{{max_threads(2)}} SELECT 1")
1922
- ('-- max_threads 2\\n SELECT 1', {'max_threads': 2})
1944
+ ('-- max_threads 2\\n SELECT 1', {'max_threads': 2}, [])
1923
1945
  >>> render_sql_template("SELECT {{String(foo)}}", test_mode=True)
1924
- ("SELECT '__placeholder__'", {})
1946
+ ("SELECT '__placeholder__'", {}, [])
1925
1947
  >>> render_sql_template("SELECT {{String(foo, 'test')}}", test_mode=True)
1926
- ("SELECT 'test'", {})
1948
+ ("SELECT 'test'", {}, [])
1927
1949
  >>> render_sql_template("SELECT {{String(foo, 'test')}}", {'foo': 'tt'})
1928
- ("SELECT 'tt'", {})
1950
+ ("SELECT 'tt'", {}, [])
1929
1951
  >>> render_sql_template("SELECT {{String(format, 'test')}}", {'format': 'tt'})
1930
1952
  Traceback (most recent call last):
1931
1953
  ...
@@ -1943,25 +1965,25 @@ def render_sql_template(
1943
1965
  ...
1944
1966
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Missing column() default value, use `column(column_name, 'default_column_name')`
1945
1967
  >>> render_sql_template("SELECT {{column(agg)}}", {'agg': 'foo'})
1946
- ('SELECT `foo`', {})
1968
+ ('SELECT `foo`', {}, [])
1947
1969
  >>> render_sql_template("SELECT {{column(agg)}}", {'agg': '"foo"'})
1948
- ('SELECT `foo`', {})
1970
+ ('SELECT `foo`', {}, [])
1949
1971
  >>> render_sql_template('{% if not defined(test) %}error("This is an error"){% end %}', {})
1950
- ('error("This is an error")', {})
1972
+ ('error("This is an error")', {}, [])
1951
1973
  >>> render_sql_template('{% if not defined(test) %}custom_error({error: "This is an error"}){% end %}', {})
1952
- ('custom_error({error: "This is an error"})', {})
1974
+ ('custom_error({error: "This is an error"})', {}, [])
1953
1975
  >>> render_sql_template("SELECT {{String(foo + 'abcd')}}", test_mode=True)
1954
- ("SELECT '__placeholder__'", {})
1976
+ ("SELECT '__placeholder__'", {}, [])
1955
1977
  >>> render_sql_template("SELECT {{columns(agg)}}", {})
1956
1978
  Traceback (most recent call last):
1957
1979
  ...
1958
1980
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Missing columns() default value, use `columns(column_names, 'default_column_name')`
1959
1981
  >>> render_sql_template("SELECT {{columns(agg, 'a,b,c')}} FROM table", {})
1960
- ('SELECT `a`,`b`,`c` FROM table', {})
1982
+ ('SELECT `a`,`b`,`c` FROM table', {}, [])
1961
1983
  >>> render_sql_template("SELECT {{columns(agg, 'a,b,c')}} FROM table", {'agg': 'foo'})
1962
- ('SELECT `foo` FROM table', {})
1984
+ ('SELECT `foo` FROM table', {}, [])
1963
1985
  >>> render_sql_template("SELECT {{columns('a,b,c')}} FROM table", {})
1964
- ('SELECT `a`,`b`,`c` FROM table', {})
1986
+ ('SELECT `a`,`b`,`c` FROM table', {}, [])
1965
1987
  >>> render_sql_template("% {% if whatever(passenger_count) %}{% end %}", test_mode=True)
1966
1988
  Traceback (most recent call last):
1967
1989
  ...
@@ -1971,13 +1993,13 @@ def render_sql_template(
1971
1993
  ...
1972
1994
  SyntaxError: invalid syntax
1973
1995
  >>> render_sql_template("SELECT * FROM dim_fecha_evento where foo like {{sql_unescape(String(pepe), '%')}}", {"pepe": 'raul_el_bueno_is_the_best_%'})
1974
- ("SELECT * FROM dim_fecha_evento where foo like 'raul_el_bueno_is_the_best_%'", {})
1996
+ ("SELECT * FROM dim_fecha_evento where foo like 'raul_el_bueno_is_the_best_%'", {}, [])
1975
1997
  >>> render_sql_template("SELECT * FROM table WHERE field={{String(field_filter)}}", {"field_filter": 'action."test run"'})
1976
- ('SELECT * FROM table WHERE field=\\'action.\\\\"test run\\\\"\\'', {})
1998
+ ('SELECT * FROM table WHERE field=\\'action.\\\\"test run\\\\"\\'', {}, [])
1977
1999
  >>> render_sql_template("SELECT {{Int128(foo)}} as x, {{Int128(bar)}} as y", {'foo': -170141183460469231731687303715884105728, 'bar': 170141183460469231731687303715884105727})
1978
- ("SELECT toInt128('-170141183460469231731687303715884105728') as x, toInt128('170141183460469231731687303715884105727') as y", {})
2000
+ ("SELECT toInt128('-170141183460469231731687303715884105728') as x, toInt128('170141183460469231731687303715884105727') as y", {}, [])
1979
2001
  >>> render_sql_template("SELECT {{Int256(foo)}} as x, {{Int256(bar)}} as y", {'foo': -57896044618658097711785492504343953926634992332820282019728792003956564819968, 'bar': 57896044618658097711785492504343953926634992332820282019728792003956564819967})
1980
- ("SELECT toInt256('-57896044618658097711785492504343953926634992332820282019728792003956564819968') as x, toInt256('57896044618658097711785492504343953926634992332820282019728792003956564819967') as y", {})
2002
+ ("SELECT toInt256('-57896044618658097711785492504343953926634992332820282019728792003956564819968') as x, toInt256('57896044618658097711785492504343953926634992332820282019728792003956564819967') as y", {}, [])
1981
2003
  >>> render_sql_template('% SELECT * FROM {% import os %}{{ os.popen("whoami").read() }}')
1982
2004
  Traceback (most recent call last):
1983
2005
  ...
@@ -1999,21 +2021,21 @@ def render_sql_template(
1999
2021
  ...
2000
2022
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Invalid BinOp: Pow()
2001
2023
  >>> render_sql_template("% SELECT {{Array(click_selector, 'String', 'pre,pro')}}")
2002
- ("% SELECT ['pre','pro']", {})
2024
+ ("% SELECT ['pre','pro']", {}, [])
2003
2025
  >>> render_sql_template("% SELECT {{Array(click_selector, 'String', 'pre,pro')}}", {'click_selector': 'hi,hello'})
2004
- ("% SELECT ['hi','hello']", {})
2026
+ ("% SELECT ['hi','hello']", {}, [])
2005
2027
  >>> render_sql_template("% SELECT now() > {{DateTime64(variable, '2020-09-09 10:10:10.000')}}", {})
2006
- ("% SELECT now() > '2020-09-09 10:10:10.000'", {})
2028
+ ("% SELECT now() > '2020-09-09 10:10:10.000'", {}, [])
2007
2029
  >>> render_sql_template("% SELECT {% if defined(x) %} x, 1", {})
2008
2030
  Traceback (most recent call last):
2009
2031
  ...
2010
2032
  tinybird.tornado_template.UnClosedIfError: Missing {% end %} block for if at line 1
2011
2033
  >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload) %} OR department = {{kv['dp']}} {% end %}")
2012
- ('% SELECT * FROM employees WHERE 0 ', {})
2034
+ ('% SELECT * FROM employees WHERE 0 ', {}, [])
2013
2035
  >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload, '[{\\"dp\\":\\"Sales\\"}]') %} OR department = {{kv['dp']}} {% end %}")
2014
- ("% SELECT * FROM employees WHERE 0 OR department = 'Sales' ", {})
2036
+ ("% SELECT * FROM employees WHERE 0 OR department = 'Sales' ", {}, [])
2015
2037
  >>> render_sql_template("% SELECT * FROM employees WHERE 0 {% for kv in JSON(payload) %} OR department = {{kv['dp']}} {% end %}", { 'payload': '[{"dp":"Design"},{"dp":"Marketing"}]'})
2016
- ("% SELECT * FROM employees WHERE 0 OR department = 'Design' OR department = 'Marketing' ", {})
2038
+ ("% SELECT * FROM employees WHERE 0 OR department = 'Design' OR department = 'Marketing' ", {}, [])
2017
2039
  >>> render_sql_template("% {% for kv in JSON(payload) %} department = {{kv['dp']}} {% end %}", test_mode=True)
2018
2040
  Traceback (most recent call last):
2019
2041
  ...
@@ -2023,22 +2045,26 @@ def render_sql_template(
2023
2045
  ...
2024
2046
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Error parsing JSON: '' - Expecting value: line 1 column 1 (char 0)
2025
2047
  >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": "1,2"})
2026
- ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {})
2048
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2027
2049
  >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": ["1","2"]})
2028
- ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {})
2050
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2029
2051
  >>> render_sql_template("% {% if defined(test) %}{% set _total = sum(test) %} SELECT test as aa, {{Array(test, 'Int32')}} as test, {{_total}} as a {% end %}", {"test": "1,2"})
2030
- ('% SELECT test as aa, [1,2] as test, 3 as a ', {})
2052
+ ('% SELECT test as aa, [1,2] as test, 3 as a ', {}, [])
2031
2053
  >>> render_sql_template("% {% if defined(test) %}{% set _groupByCSV = ','.join(test) %} SELECT test as aa, {{Array(test, 'String')}} as test, {{_groupByCSV}} as a {% end %}", {"test": ["1","2"]})
2032
- ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {})
2054
+ ("% SELECT test as aa, ['1','2'] as test, '1,2' as a ", {}, [])
2033
2055
  >>> render_sql_template("% SELECT {% if defined(x) %} x, 1")
2034
2056
  Traceback (most recent call last):
2035
2057
  ...
2036
2058
  tinybird.tornado_template.UnClosedIfError: Missing {% end %} block for if at line 1
2059
+ >>> render_sql_template("select * from table where str = {{pipeline}}", { 'pipeline': 'test' })
2060
+ ("select * from table where str = 'test'", {}, ['pipeline'])
2037
2061
  """
2038
2062
  escape_split_to_array = ff_split_to_array_escape.get(False)
2039
2063
  bypass_preprocess_variables = ff_preprocess_parameters_circuit_breaker.get(False)
2040
2064
 
2041
- t, template_variables = get_template_and_variables(sql, name, escape_arrays=escape_split_to_array)
2065
+ t, template_variables, variable_warnings = get_template_and_variables(
2066
+ sql, name, escape_arrays=escape_split_to_array
2067
+ )
2042
2068
 
2043
2069
  if not bypass_preprocess_variables and variables is not None:
2044
2070
  processed_variables = preprocess_variables(variables, t)
@@ -2070,7 +2096,7 @@ def render_sql_template(
2070
2096
  v.update(type_fns)
2071
2097
 
2072
2098
  try:
2073
- return generate(t, **v)
2099
+ return *generate(t, **v), variable_warnings
2074
2100
  except NameError as e:
2075
2101
  raise SQLTemplateException(e, documentation="/cli/advanced-templates.html#defined")
2076
2102
  except SQLTemplateException:
@@ -17,7 +17,7 @@ class Provider(Enum):
17
17
 
18
18
  WORKFLOW_VERSION = "v3.1.0"
19
19
 
20
- DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=3,<4"
20
+ DEFAULT_REQUIREMENTS_FILE = "tinybird-cli>=4,<5"
21
21
 
22
22
  GITHUB_CI_YML = """
23
23
  ##################################################
@@ -222,7 +222,8 @@ async def get_current_workspace_branches(config: CLIConfig) -> List[Dict[str, An
222
222
  client = config.get_client()
223
223
  user_branches: List[Dict[str, Any]] = (await client.user_workspace_branches()).get("workspaces", [])
224
224
  all_branches: List[Dict[str, Any]] = (await client.branches()).get("environments", [])
225
- branches = all_branches + [branch for branch in user_branches if branch not in all_branches]
225
+ branches = user_branches + [branch for branch in all_branches if branch not in user_branches]
226
+
226
227
  return [branch for branch in branches if branch.get("main") == current_main_workspace["id"]]
227
228
 
228
229
 
@@ -738,15 +738,17 @@ async def datasource_sync(ctx, datasource_name: str, yes: bool):
738
738
  @datasource.command(name="exchange", hidden=True)
739
739
  @click.argument("datasource_a", required=True)
740
740
  @click.argument("datasource_b", required=True)
741
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
741
742
  @click.pass_context
742
743
  @coro
743
- async def datasource_exchange(ctx, datasource_a, datasource_b):
744
+ async def datasource_exchange(ctx, datasource_a: str, datasource_b: str, yes: bool):
744
745
  """Exchange two data sources"""
745
746
 
746
747
  client = ctx.obj["client"]
747
748
 
748
749
  try:
749
- await client.datasource_exchange(datasource_a, datasource_b)
750
+ if yes or click.confirm(FeedbackManager.warning_exchange(datasource_a=datasource_a, datasource_b=datasource_b)):
751
+ await client.datasource_exchange(datasource_a, datasource_b)
750
752
  except Exception as e:
751
753
  raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
752
754
 
@@ -0,0 +1,335 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ import click
5
+ import pyperclip
6
+ from click import Context
7
+ from humanfriendly import parse_timespan
8
+
9
+ from tinybird.client import TinyB
10
+ from tinybird.feedback_manager import FeedbackManager
11
+ from tinybird.tb_cli_modules.cli import cli
12
+ from tinybird.tb_cli_modules.common import (
13
+ DoesNotExistException,
14
+ coro,
15
+ echo_safe_humanfriendly_tables_format_smart_table,
16
+ )
17
+ from tinybird.tb_cli_modules.exceptions import CLITokenException
18
+
19
+
20
+ @cli.group()
21
+ @click.pass_context
22
+ def token(ctx: Context) -> None:
23
+ """Token commands."""
24
+
25
+
26
+ @token.command(name="ls")
27
+ @click.option("--match", default=None, help="Retrieve any token matching the pattern. eg --match _test")
28
+ @click.pass_context
29
+ @coro
30
+ async def token_ls(
31
+ ctx: Context,
32
+ match: Optional[str] = None,
33
+ ) -> None:
34
+ """List static tokens."""
35
+
36
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
37
+ client: TinyB = obj["client"]
38
+
39
+ try:
40
+ tokens = await client.token_list(match)
41
+ columns = ["id", "name", "description"]
42
+ table = list(map(lambda token: [token.get(key, "") for key in columns], tokens))
43
+
44
+ click.echo(FeedbackManager.info_tokens())
45
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
46
+ click.echo("\n")
47
+ except Exception as e:
48
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
49
+
50
+
51
+ @token.command(name="rm")
52
+ @click.argument("token_id")
53
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
54
+ @click.pass_context
55
+ @coro
56
+ async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
57
+ """Remove a static token."""
58
+
59
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
60
+ client: TinyB = obj["client"]
61
+ if yes or click.confirm(FeedbackManager.warning_confirm_delete_token(token=token_id)):
62
+ try:
63
+ await client.token_delete(token_id)
64
+ except DoesNotExistException:
65
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
66
+ except Exception as e:
67
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
68
+ click.echo(FeedbackManager.success_delete_token(token=token_id))
69
+
70
+
71
+ @token.command(name="refresh")
72
+ @click.argument("token_id")
73
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
74
+ @click.pass_context
75
+ @coro
76
+ async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
77
+ """Refresh a static token."""
78
+
79
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
80
+ client: TinyB = obj["client"]
81
+ if yes or click.confirm(FeedbackManager.warning_confirm_refresh_token(token=token_id)):
82
+ try:
83
+ await client.token_refresh(token_id)
84
+ except DoesNotExistException:
85
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
86
+ except Exception as e:
87
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
88
+ click.echo(FeedbackManager.success_refresh_token(token=token_id))
89
+
90
+
91
+ @token.command(name="scopes")
92
+ @click.argument("token_id")
93
+ @click.pass_context
94
+ @coro
95
+ async def token_scopes(ctx: Context, token_id: str) -> None:
96
+ """List static token scopes."""
97
+
98
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
99
+ client: TinyB = obj["client"]
100
+
101
+ try:
102
+ scopes = await client.token_scopes(token_id)
103
+ columns = ["type", "resource", "filter"]
104
+ table = list(map(lambda scope: [scope.get(key, "") for key in columns], scopes))
105
+ click.echo(FeedbackManager.info_token_scopes(token=token_id))
106
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
107
+ click.echo("\n")
108
+ except Exception as e:
109
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
110
+
111
+
112
+ @token.command(name="copy")
113
+ @click.argument("token_id")
114
+ @click.pass_context
115
+ @coro
116
+ async def token_copy(ctx: Context, token_id: str) -> None:
117
+ """Copy a static token."""
118
+
119
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
120
+ client: TinyB = obj["client"]
121
+
122
+ try:
123
+ token = await client.token_get(token_id)
124
+ pyperclip.copy(token["token"].strip())
125
+ except DoesNotExistException:
126
+ raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
127
+ except Exception as e:
128
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
129
+ click.echo(FeedbackManager.success_copy_token(token=token_id))
130
+
131
+
132
+ def parse_ttl(ctx, param, value):
133
+ if value is None:
134
+ return None
135
+ try:
136
+ seconds = parse_timespan(value)
137
+ return timedelta(seconds=seconds)
138
+ except ValueError:
139
+ raise click.BadParameter(f"Invalid time to live format: {value}")
140
+
141
+
142
+ def parse_fixed_params(fixed_params_list):
143
+ parsed_params = []
144
+ for fixed_param in fixed_params_list:
145
+ param_dict = {}
146
+ for param in fixed_param.split(","):
147
+ key, value = param.split("=")
148
+ param_dict[key] = value
149
+ parsed_params.append(param_dict)
150
+ return parsed_params
151
+
152
+
153
+ @token.group()
154
+ @click.pass_context
155
+ def create(ctx: Context) -> None:
156
+ """Token creation commands.
157
+
158
+ You can create two types of tokens: JWT or Static.
159
+
160
+ * JWT tokens have a TTL and can only have the PIPES:READ scope.Their main use case is allow your users to call your endpoints without exposing your API key.
161
+
162
+ * Static tokens do not have a TTL and can have any valid scope (DATASOURCES:READ, DATASOURCES:APPEND, DATASOURCES:CREATE, DATASOURCES:DROP, PIPES:CREATE, PIPES:READ, PIPES:DROP).
163
+
164
+ Examples:
165
+
166
+ tb token create jwt my_jwt_token --ttl 1h --scope PIPES:READ --resource my_pipe
167
+
168
+ tb token create static my_static_token --scope PIPES:READ --resource my_pipe
169
+
170
+ tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource
171
+
172
+ tb token create static my_static_token --scope DATASOURCES:READ --resource my_datasource --filters "column_name=value"
173
+
174
+ """
175
+
176
+
177
+ @create.command(name="jwt")
178
+ @click.argument("name")
179
+ @click.option("--ttl", type=str, callback=parse_ttl, required=True, help="Time to live (e.g., '1h', '30min', '1d')")
180
+ @click.option(
181
+ "--scope",
182
+ multiple=True,
183
+ type=click.Choice(["PIPES:READ"]),
184
+ required=True,
185
+ help="Scope of the token (only PIPES:READ is allowed for JWT tokens)",
186
+ )
187
+ @click.option("--resource", multiple=True, required=True, help="Resource associated with the scope")
188
+ @click.option(
189
+ "--fixed-params", multiple=True, help="Fixed parameters in key=value format, multiple values separated by commas"
190
+ )
191
+ @click.pass_context
192
+ @coro
193
+ async def create_jwt_token(ctx: Context, name: str, ttl: timedelta, scope, resource, fixed_params) -> None:
194
+ """Create a JWT token with a TTL specify."""
195
+
196
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
197
+ client: TinyB = obj["client"]
198
+
199
+ expiration_time = int((ttl + datetime.now(timezone.utc)).timestamp())
200
+ if len(scope) != len(resource):
201
+ raise CLITokenException(FeedbackManager.error_number_of_scopes_and_resources_mismatch())
202
+
203
+ # Ensure the number of fixed-params does not exceed the number of scope/resource pairs
204
+ if fixed_params and len(fixed_params) > len(scope):
205
+ raise CLITokenException(FeedbackManager.error_number_of_fixed_params_and_resources_mismatch())
206
+
207
+ # Parse fixed params
208
+ parsed_fixed_params = parse_fixed_params(fixed_params) if fixed_params else []
209
+
210
+ # Create a list of fixed params for each scope/resource pair, defaulting to empty dict if not provided
211
+ fixed_params_list: List[Dict[str, Any]] = [{}] * len(scope)
212
+ for i, params in enumerate(parsed_fixed_params):
213
+ fixed_params_list[i] = params
214
+
215
+ scopes = []
216
+ for sc, res, fparams in zip(scope, resource, fixed_params_list):
217
+ scopes.append(
218
+ {
219
+ "type": sc,
220
+ "resource": res,
221
+ "fixed_params": fparams,
222
+ }
223
+ )
224
+
225
+ try:
226
+ response = await client.create_jwt_token(name, expiration_time, scopes)
227
+ except Exception as e:
228
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
229
+
230
+ click.echo("The token has been generated successfully.")
231
+ click.echo(
232
+ f"The token will expire at: {datetime.fromtimestamp(expiration_time).strftime('%Y-%m-%d %H:%M:%S')} UTC "
233
+ )
234
+ click.echo(f"The token is: {response['token']}")
235
+
236
+
237
+ # Valid scopes for static tokens
238
+ valid_scopes = [
239
+ "DATASOURCES:READ",
240
+ "DATASOURCES:APPEND",
241
+ "DATASOURCES:CREATE",
242
+ "DATASOURCES:DROP",
243
+ "PIPES:CREATE",
244
+ "PIPES:READ",
245
+ "PIPES:DROP",
246
+ ]
247
+
248
+
249
+ # As we are passing dynamic options to the command, we need to create a custom class to handle the help message
250
+ class DynamicOptionsCommand(click.Command):
251
+ def get_help(self, ctx):
252
+ # Usage
253
+ usage = "Usage: tb token create static [OPTIONS] NAME\n\n"
254
+ dynamic_options_help = usage
255
+
256
+ # Description
257
+ dynamic_options_help += " Create a static token that will live forever.\n\n"
258
+
259
+ # Options
260
+ dynamic_options_help += "Options:\n"
261
+ dynamic_options_help += f" --scope [{','.join(valid_scopes)}] Scope for the token [Required]\n"
262
+ dynamic_options_help += " --resource TEXT Resource you want to associate the scope with\n"
263
+ dynamic_options_help += " --filter TEXT SQL condition used to filter the values when calling with this token (eg. --filter=value > 0) \n"
264
+ dynamic_options_help += " -h, --help Show this message and exit.\n"
265
+
266
+ return dynamic_options_help
267
+
268
+
269
+ @create.command(
270
+ name="static", context_settings=dict(ignore_unknown_options=True, allow_extra_args=True), cls=DynamicOptionsCommand
271
+ )
272
+ @click.argument("name")
273
+ @click.pass_context
274
+ @coro
275
+ async def create_static_token(ctx, name: str):
276
+ """Create a static token."""
277
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
278
+ client: TinyB = obj["client"]
279
+
280
+ args = ctx.args
281
+ scopes: List[Dict[str, str]] = []
282
+ current_scope = None
283
+
284
+ # We parse the arguments to get the scopes, resources and filters
285
+ # The arguments should be in the format --scope <scope> --resource <resource> --filter <filter>
286
+ i = 0
287
+ while i < len(args):
288
+ if args[i] == "--scope":
289
+ if current_scope:
290
+ scopes.append(current_scope)
291
+ current_scope = {}
292
+ current_scope = {"scope": args[i + 1]}
293
+ i += 2
294
+ elif args[i] == "--resource":
295
+ if current_scope is None:
296
+ raise click.BadParameter("Resource must follow a scope")
297
+ if "resource" in current_scope:
298
+ raise click.BadParameter(
299
+ "Resource already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
300
+ )
301
+ current_scope["resource"] = args[i + 1]
302
+ i += 2
303
+ elif args[i] == "--filter":
304
+ if current_scope is None:
305
+ raise click.BadParameter("Filter must follow a scope")
306
+ if "filter" in current_scope:
307
+ raise click.BadParameter(
308
+ "Filter already defined for this scope. The format is --scope <scope> --resource <resource> --filter <filter>"
309
+ )
310
+ current_scope["filter"] = args[i + 1]
311
+ i += 2
312
+ else:
313
+ raise click.BadParameter(f"Unknown parameter {args[i]}")
314
+
315
+ if current_scope:
316
+ scopes.append(current_scope)
317
+
318
+ # Parse the scopes like `SCOPE:RESOURCE:FILTER` or `SCOPE:RESOURCE` or `SCOPE` as that's what the API expsects
319
+ scoped_parsed: List[str] = []
320
+ for scope in scopes:
321
+ if scope.get("resource") and scope.get("filter"):
322
+ scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}:{scope.get('filter')}")
323
+ elif scope.get("resource"):
324
+ scoped_parsed.append(f"{scope.get('scope')}:{scope.get('resource')}")
325
+ elif "scope" in scope:
326
+ scoped_parsed.append(scope.get("scope", ""))
327
+ else:
328
+ raise CLITokenException("Unknown error")
329
+
330
+ try:
331
+ await client.create_token(name, scoped_parsed, origin_code=None)
332
+ except Exception as e:
333
+ raise CLITokenException(FeedbackManager.error_exception(error=e))
334
+
335
+ click.echo("The token has been generated successfully.")
@@ -892,7 +892,15 @@ def _parse(reader: _TemplateReader, template, in_block=None, in_loop=None):
892
892
  reader.consume(2)
893
893
  if not contents:
894
894
  reader.raise_parse_error("Empty expression")
895
- body.chunks.append(_Expression(contents, line))
895
+
896
+ try:
897
+ body.chunks.append(_Expression(contents, line))
898
+ except SyntaxError:
899
+ operator, _, _ = contents.partition(" ")
900
+
901
+ if "from" in operator:
902
+ reader.raise_parse_error('"from" is a forbidden word')
903
+ raise
896
904
  continue
897
905
 
898
906
  # Block
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 4.0.2.dev0
3
+ Version: 4.1.1
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,15 +18,26 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 4.1.1
22
+ ************
23
+
24
+ - `Fixed` workspace info gathering when switching branches. We were prioritizing the general workspace properties over the user ones.
25
+
26
+ 4.1.0
27
+ ************
28
+
29
+ - `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.
31
+
21
32
  4.0.0
22
33
  ************
23
34
 
24
- This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to these version.
35
+ This is a major release, please read the commands affected below and consider updating your scripts and workflow before upgrading to this version.
25
36
 
26
37
  - `Deprecated` `--semver` flag and `tb release` commands are now deprecated. You can keep using `tb deploy` to integrate and deploy from git. Changes are deployed to the main Workspace instead of to a Release.
27
- - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an exmple https://github.com/tinybirdco/ci
38
+ - `Removed` `--cicd` flag and CI/CD templates generation from `tb init`. You can still use the git integration, just create your own pipelines. You can use the ones in this repo as an example https://github.com/tinybirdco/ci
28
39
  - `Removed` `tb env` command is removed, use `tb branch` instead.
29
- - `Deprecated` .datasource files with `ENGINE "Join"` are deprecated, use `Engine "MergeeTree"` instead.
40
+ - `Deprecated` .datasource files with `ENGINE "Join"` is deprecated, use `Engine "MergeeTree"` instead.
30
41
  - `Deprecated` `tb materialize`
31
42
  - `Removed` Drop the `--timeout` flag from `tb push` which made the populate job to timeout. You can use now `--wait` to wait for the job to finish or nothing to just create the job and return.
32
43
  - `Removed` Support for `KEY` directive is removed. The `KEY` was used to create a Data Source with Join engine by the given `KEY` column name. Join engines are also deprecated, you can use a regular `MergeTree` Data Source instead and adapt the pipes SQL accordingly.
@@ -1,127 +0,0 @@
1
- from typing import Any, Dict, Optional
2
-
3
- import click
4
- import pyperclip
5
- from click import Context
6
-
7
- from tinybird.client import TinyB
8
- from tinybird.feedback_manager import FeedbackManager
9
- from tinybird.tb_cli_modules.cli import cli
10
- from tinybird.tb_cli_modules.common import (
11
- DoesNotExistException,
12
- coro,
13
- echo_safe_humanfriendly_tables_format_smart_table,
14
- )
15
- from tinybird.tb_cli_modules.exceptions import CLITokenException
16
-
17
-
18
- @cli.group()
19
- @click.pass_context
20
- def token(ctx: Context) -> None:
21
- """Token commands."""
22
-
23
-
24
- @token.command(name="ls")
25
- @click.option("--match", default=None, help="Retrieve any token matching the pattern. eg --match _test")
26
- @click.pass_context
27
- @coro
28
- async def token_ls(
29
- ctx: Context,
30
- match: Optional[str] = None,
31
- ) -> None:
32
- """List tokens."""
33
-
34
- obj: Dict[str, Any] = ctx.ensure_object(dict)
35
- client: TinyB = obj["client"]
36
-
37
- try:
38
- tokens = await client.token_list(match)
39
- columns = ["id", "name", "description"]
40
- table = list(map(lambda token: [token.get(key, "") for key in columns], tokens))
41
-
42
- click.echo(FeedbackManager.info_tokens())
43
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
44
- click.echo("\n")
45
- except Exception as e:
46
- raise CLITokenException(FeedbackManager.error_exception(error=e))
47
-
48
-
49
- @token.command(name="rm")
50
- @click.argument("token_id")
51
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
52
- @click.pass_context
53
- @coro
54
- async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
55
- """Remove a token."""
56
-
57
- obj: Dict[str, Any] = ctx.ensure_object(dict)
58
- client: TinyB = obj["client"]
59
- if yes or click.confirm(FeedbackManager.warning_confirm_delete_token(token=token_id)):
60
- try:
61
- await client.token_delete(token_id)
62
- except DoesNotExistException:
63
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token_id=token_id))
64
- except Exception as e:
65
- raise CLITokenException(FeedbackManager.error_exception(error=e))
66
- click.echo(FeedbackManager.success_delete_token(token=token_id))
67
-
68
-
69
- @token.command(name="refresh")
70
- @click.argument("token_id")
71
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
72
- @click.pass_context
73
- @coro
74
- async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
75
- """Refresh a token."""
76
-
77
- obj: Dict[str, Any] = ctx.ensure_object(dict)
78
- client: TinyB = obj["client"]
79
- if yes or click.confirm(FeedbackManager.warning_confirm_refresh_token(token=token_id)):
80
- try:
81
- await client.token_refresh(token_id)
82
- except DoesNotExistException:
83
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
84
- except Exception as e:
85
- raise CLITokenException(FeedbackManager.error_exception(error=e))
86
- click.echo(FeedbackManager.success_refresh_token(token=token_id))
87
-
88
-
89
- @token.command(name="scopes")
90
- @click.argument("token_id")
91
- @click.pass_context
92
- @coro
93
- async def token_scopes(ctx: Context, token_id: str) -> None:
94
- """List token scopes."""
95
-
96
- obj: Dict[str, Any] = ctx.ensure_object(dict)
97
- client: TinyB = obj["client"]
98
-
99
- try:
100
- scopes = await client.token_scopes(token_id)
101
- columns = ["type", "resource", "filter"]
102
- table = list(map(lambda scope: [scope.get(key, "") for key in columns], scopes))
103
- click.echo(FeedbackManager.info_token_scopes(token=token_id))
104
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
105
- click.echo("\n")
106
- except Exception as e:
107
- raise CLITokenException(FeedbackManager.error_exception(error=e))
108
-
109
-
110
- @token.command(name="copy")
111
- @click.argument("token_id")
112
- @click.pass_context
113
- @coro
114
- async def token_copy(ctx: Context, token_id: str) -> None:
115
- """Copy a token."""
116
-
117
- obj: Dict[str, Any] = ctx.ensure_object(dict)
118
- client: TinyB = obj["client"]
119
-
120
- try:
121
- token = await client.token_get(token_id)
122
- pyperclip.copy(token["token"].strip())
123
- except DoesNotExistException:
124
- raise CLITokenException(FeedbackManager.error_token_does_not_exist(token=token_id))
125
- except Exception as e:
126
- raise CLITokenException(FeedbackManager.error_exception(error=e))
127
- click.echo(FeedbackManager.success_copy_token(token=token_id))