tinybird 4.5.3.dev0__tar.gz → 4.5.4__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 (126) hide show
  1. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/PKG-INFO +12 -1
  2. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/common.py +3 -2
  3. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datatypes.py +2 -2
  4. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/service_datasources.py +3 -0
  5. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql.py +13 -7
  6. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_template.py +1 -1
  7. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_toolset.py +1 -19
  8. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/__cli__.py +2 -2
  9. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/branch.py +0 -1
  10. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/cicd.py +4 -4
  11. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/cli.py +6 -6
  12. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/common.py +10 -8
  13. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/create.py +0 -12
  14. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build.py +2 -2
  15. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_datasource.py +1 -1
  16. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_pipe.py +1 -1
  17. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/playground.py +2 -2
  18. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deployment.py +162 -4
  19. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deployment_common.py +122 -28
  20. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/job.py +1 -3
  21. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local.py +10 -11
  22. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/login_common.py +2 -4
  23. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/query_output.py +1 -3
  24. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/workspace.py +10 -11
  25. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/workspace_members.py +1 -3
  26. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/branch.py +4 -6
  27. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/cli.py +1 -3
  28. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/common.py +5 -4
  29. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/job.py +1 -3
  30. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tag.py +2 -3
  31. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/workspace.py +10 -11
  32. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/PKG-INFO +12 -1
  33. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/setup.cfg +0 -0
  34. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/__cli__.py +0 -0
  35. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/ch_utils/constants.py +0 -0
  36. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/ch_utils/engine.py +0 -0
  37. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/check_pypi.py +0 -0
  38. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/client.py +0 -0
  39. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/config.py +0 -0
  40. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/context.py +0 -0
  41. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/exceptions.py +0 -0
  42. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_connection.py +0 -0
  43. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_datasource.py +0 -0
  44. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_pipe.py +0 -0
  45. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/feedback_manager.py +0 -0
  46. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/git_settings.py +0 -0
  47. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/prompts.py +0 -0
  48. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_template_fmt.py +0 -0
  49. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/syncasync.py +0 -0
  50. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/check_pypi.py +0 -0
  51. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/cli.py +0 -0
  52. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/client.py +0 -0
  53. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/config.py +0 -0
  54. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/build.py +0 -0
  55. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/build_common.py +0 -0
  56. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/config.py +0 -0
  57. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection.py +0 -0
  58. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection_kafka.py +0 -0
  59. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection_s3.py +0 -0
  60. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/copy.py +0 -0
  61. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_common.py +0 -0
  62. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/diff.py +0 -0
  63. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/fixture.py +0 -0
  64. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_common.py +0 -0
  65. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  66. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  67. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  68. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  69. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/pull.py +0 -0
  70. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datasource.py +0 -0
  71. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deprecations.py +0 -0
  72. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/endpoint.py +0 -0
  73. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/exceptions.py +0 -0
  74. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/feedback_manager.py +0 -0
  75. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/fmt.py +0 -0
  76. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/info.py +0 -0
  77. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/infra.py +0 -0
  78. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/job_common.py +0 -0
  79. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/llm.py +0 -0
  80. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/llm_utils.py +0 -0
  81. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local_common.py +0 -0
  82. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local_logs.py +0 -0
  83. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/login.py +0 -0
  84. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/logout.py +0 -0
  85. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/logs.py +0 -0
  86. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/materialization.py +0 -0
  87. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/open.py +0 -0
  88. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/pipe.py +0 -0
  89. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/preview.py +0 -0
  90. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/project.py +0 -0
  91. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/project_commands.py +0 -0
  92. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/py_project.py +0 -0
  93. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/regions.py +0 -0
  94. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/secret.py +0 -0
  95. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/secret_common.py +0 -0
  96. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/sink.py +0 -0
  97. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/table.py +0 -0
  98. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/telemetry.py +0 -0
  99. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/test.py +0 -0
  100. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/test_common.py +0 -0
  101. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  102. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  103. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/token.py +0 -0
  104. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/ts_project.py +0 -0
  105. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/watch.py +0 -0
  106. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli.py +0 -0
  107. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/auth.py +0 -0
  108. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/cicd.py +0 -0
  109. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/config.py +0 -0
  110. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/connection.py +0 -0
  111. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/datasource.py +0 -0
  112. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/exceptions.py +0 -0
  113. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/fmt.py +0 -0
  114. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/pipe.py +0 -0
  115. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/regions.py +0 -0
  116. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/telemetry.py +0 -0
  117. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/test.py +0 -0
  118. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  119. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  120. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  121. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tornado_template.py +0 -0
  122. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/SOURCES.txt +0 -0
  123. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/dependency_links.txt +0 -0
  124. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/entry_points.txt +0 -0
  125. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/requires.txt +0 -0
  126. {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.5.3.dev0
3
+ Version: 4.5.4
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -52,6 +52,17 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.5.4
56
+ *******
57
+
58
+ - `Added` `tb migrate-to-forward` to help users migrate Classic workspaces to Forward
59
+
60
+ 4.5.3
61
+ *******
62
+
63
+ - `Changed` `tb init` now defaults to CLI projects when `--type` is not provided.
64
+ - `Fixed` GitHub and GitLab CI templates generated by `tb init` now run `tb --local build` and `tb --local test run`.
65
+
55
66
  4.5.2
56
67
  ***********
57
68
 
@@ -1394,7 +1394,8 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
1394
1394
  if match := _PATTERN_SIMPLE_AGG_FUNC.search(t):
1395
1395
  fn = match.group(1)
1396
1396
  inner_type = match.group(2)
1397
- result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
1397
+ if "Nullable(" not in inner_type:
1398
+ result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
1398
1399
  return result
1399
1400
 
1400
1401
 
@@ -2366,7 +2367,7 @@ def get_project_fixtures(folder: str) -> List[str]:
2366
2367
  def has_internal_datafiles(folder: str) -> bool:
2367
2368
  folder = folder or "."
2368
2369
  filenames = get_project_filenames(folder)
2369
- return any([f for f in filenames if "spans" in str(f) and "vendor" not in str(f)])
2370
+ return any(f for f in filenames if "spans" in str(f) and "vendor" not in str(f))
2370
2371
 
2371
2372
 
2372
2373
  def peek(iterable):
@@ -117,11 +117,11 @@ def date_test(x: str) -> bool:
117
117
 
118
118
 
119
119
  def datetime64_test(x: str) -> bool:
120
- return any([p.match(x) for p in datetime64_patterns])
120
+ return any(p.match(x) for p in datetime64_patterns)
121
121
 
122
122
 
123
123
  def datetime_test(x: str) -> bool:
124
- return any([p.match(x) for p in datetime_patterns])
124
+ return any(p.match(x) for p in datetime_patterns)
125
125
 
126
126
 
127
127
  def int_8_test(x: str) -> bool:
@@ -45,6 +45,7 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
45
45
  {"name": "method", "type": "String"},
46
46
  {"name": "release", "type": "String"},
47
47
  {"name": "user_agent", "type": "Nullable(String)"},
48
+ {"name": "client_ip", "type": "Nullable(String)"},
48
49
  {"name": "resource_tags", "type": "Array(String)"},
49
50
  {"name": "memory_usage", "type": "UInt64"},
50
51
  ],
@@ -506,6 +507,8 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
506
507
  {"name": "ch_written_bytes", "type": "UInt64"},
507
508
  {"name": "ch_cpu_time", "type": "Float32"},
508
509
  {"name": "rate_limited", "type": "UInt8"},
510
+ {"name": "ips", "type": "SimpleAggregateFunction(groupUniqArrayArray, Array(String))"},
511
+ {"name": "tokens", "type": "SimpleAggregateFunction(groupUniqArrayArray, Array(String))"},
509
512
  ],
510
513
  },
511
514
  {
@@ -233,10 +233,21 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
233
233
  if match := _RE_TRY_FIX_NULLABLE_SAF.search(t):
234
234
  fn = match.group(1)
235
235
  inner_type = match.group(2)
236
- result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
236
+ if "Nullable(" not in inner_type:
237
+ result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
237
238
  return result
238
239
 
239
240
 
241
+ def wrap_nullable(col: dict[str, Any]):
242
+ if col["nullable"]:
243
+ if (col_type := try_to_fix_nullable_in_simple_aggregating_function(col["type"])) is None:
244
+ # Skip wrapping if Nullable already present, e.g. LowCardinality(Nullable(String))
245
+ col_type = col["type"] if "Nullable(" in col["type"] else "Nullable(%s)" % col["type"]
246
+ else:
247
+ col_type = col["type"]
248
+ return col_type
249
+
250
+
240
251
  def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = False) -> List[str]:
241
252
  """return an array with each column in SQL
242
253
  >>> schema_to_sql_columns([{'name': 'temperature', 'type': 'Float32', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'temperature'}, {'name': 'temperature_delta', 'type': 'Float32', 'codec': 'CODEC(Delta(4), LZ4))', 'default_value': 'MATERIALIZED temperature', 'nullable': False, 'normalized_name': 'temperature_delta'}])
@@ -255,12 +266,7 @@ def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = F
255
266
  columns: List[str] = []
256
267
  for x in schema:
257
268
  name = x["normalized_name"] if "normalized_name" in x else x["name"]
258
- if x["nullable"]:
259
- if (_type := try_to_fix_nullable_in_simple_aggregating_function(x["type"])) is None:
260
- # Skip wrapping if Nullable already present, e.g. LowCardinality(Nullable(String))
261
- _type = x["type"] if "Nullable(" in x["type"] else "Nullable(%s)" % x["type"]
262
- else:
263
- _type = x["type"]
269
+ _type = wrap_nullable(x)
264
270
  parts = [col_name(name, backquotes=True), _type]
265
271
  if x.get("jsonpath", None) and not skip_jsonpaths:
266
272
  parts.append(f"`json:{x['jsonpath']}`")
@@ -2826,7 +2826,7 @@ def render_sql_template(
2826
2826
  return Comment("error launched")
2827
2827
 
2828
2828
  v: dict = {x["name"]: Placeholder(x["name"], x["line"]) for x in template_variables}
2829
- is_tb_secret = any([s for s in template_variables if s["name"] == "tb_secret" or s["name"] == "tb_var"])
2829
+ is_tb_secret = any(s for s in template_variables if s["name"] == "tb_secret" or s["name"] == "tb_var")
2830
2830
 
2831
2831
  if variables:
2832
2832
  v.update(variables)
@@ -22,7 +22,7 @@ VALID_REMOTE = "VALID_REMOTE"
22
22
 
23
23
  class InvalidFunction(ValueError):
24
24
  def __init__(self, msg: str = "", table_function_name: str = ""):
25
- if any([fn for fn in COPY_ENABLED_TABLE_FUNCTIONS if fn in msg]):
25
+ if any(fn for fn in COPY_ENABLED_TABLE_FUNCTIONS if fn in msg):
26
26
  msg = msg.replace("is restricted", "is restricted to Copy Pipes")
27
27
 
28
28
  if table_function_name:
@@ -75,19 +75,6 @@ def explain_plan(sql: str) -> str:
75
75
  return chquery.explain_ast(sql)
76
76
 
77
77
 
78
- @dataclass(frozen=True)
79
- class ColumnInfo:
80
- name: str
81
- type: str
82
- nullable: bool
83
- default_specifier: str = ""
84
- default_expression: str | None = None
85
- codec: str | None = None
86
- comment: str | None = None
87
- ttl: str | None = None
88
- is_primary_key: bool = False
89
-
90
-
91
78
  @dataclass
92
79
  class MaterializedViewTarget:
93
80
  database: Optional[str]
@@ -118,11 +105,6 @@ def parse_materialized_view_target(create_table_query: str) -> Optional[Material
118
105
  )
119
106
 
120
107
 
121
- def get_columns_from_create_query(sql_schema: str) -> list[ColumnInfo]:
122
- columns = chquery.get_columns_from_create_query(sql_schema)
123
- return [ColumnInfo(**col) for col in columns]
124
-
125
-
126
108
  def has_join(sql: str) -> bool:
127
109
  return any(line.rstrip().startswith("TableJoin") for line in explain_plan(sql).split())
128
110
 
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '4.5.3.dev0'
8
- __revision__ = 'afc5f42'
7
+ __version__ = '4.5.4'
8
+ __revision__ = 'eebce0e'
@@ -27,7 +27,6 @@ from tinybird.tb.modules.feedback_manager import FeedbackManager, get_cli_name
27
27
  @cli.group()
28
28
  def branch() -> None:
29
29
  """Branch commands. Custom branches is an experimental feature in beta."""
30
- pass
31
30
 
32
31
 
33
32
  @branch.command(name="ls")
@@ -51,9 +51,9 @@ jobs:
51
51
  - name: Install Tinybird CLI
52
52
  run: curl https://tinybird.co | sh
53
53
  - name: Build project
54
- run: tb build
54
+ run: tb --local build
55
55
  - name: Test project
56
- run: tb test run
56
+ run: tb --local test run
57
57
  - name: Deployment check
58
58
  run: tb --cloud --host ${{! env.TINYBIRD_HOST }} --token ${{! env.TINYBIRD_TOKEN }} deploy --check
59
59
  """
@@ -115,8 +115,8 @@ tinybird_ci_workflow:
115
115
  script:
116
116
  - export PATH="$HOME/.local/bin:$PATH"
117
117
  - cd $CI_PROJECT_DIR/{{ data_project_dir }}
118
- - tb build
119
- - tb test run
118
+ - tb --local build
119
+ - tb --local test run
120
120
  - tb --cloud --host "$TINYBIRD_HOST" --token "$TINYBIRD_TOKEN" deploy --check
121
121
  services:
122
122
  - name: tinybirdco/tinybird-local:latest
@@ -63,7 +63,7 @@ DEV_MODE_VALUES = {DEV_MODE_MANUAL, DEV_MODE_LOCAL, DEV_MODE_BRANCH}
63
63
  DEV_MODE_ROUTED_COMMANDS = {"build", "deploy"}
64
64
  SDK_PROJECT_ROUTED_COMMANDS = {"build", "deploy", "preview"}
65
65
  TS_PROJECT_ROUTED_COMMANDS = SDK_PROJECT_ROUTED_COMMANDS
66
- COMMANDS_ALWAYS_CLOUD = {"infra", "branch", "environment", "workspace", "preview"}
66
+ COMMANDS_ALWAYS_CLOUD = {"infra", "branch", "environment", "workspace", "preview", "migrate-to-forward"}
67
67
  PROJECT_TYPE_TYPESCRIPT = "ts-sdk"
68
68
  PROJECT_TYPE_PYTHON = "python-sdk"
69
69
  PROJECT_TYPE_CLI = "cli"
@@ -625,8 +625,10 @@ def cli(
625
625
  config["dev_mode"] = tinybird_dev_mode
626
626
 
627
627
  # Resolve project folder from tinybird.config.json (preferred) or legacy .tinyb cwd.
628
- folder = get_project_folder_from_tinybird_config(os.getcwd())
629
- if not folder:
628
+ folder_from_config = get_project_folder_from_tinybird_config(os.getcwd())
629
+ if folder_from_config:
630
+ folder = folder_from_config
631
+ else:
630
632
  tinyb_dir = os.path.dirname(config_temp._path) # Directory containing .tinyb file
631
633
  cwd_config = config.get("cwd", ".")
632
634
 
@@ -870,9 +872,7 @@ def sql(
870
872
  if output == "json":
871
873
  echo_json(res, indent=8)
872
874
  else:
873
- dd = []
874
- for d in res["data"]:
875
- dd.append(d.values())
875
+ dd = [d.values() for d in res["data"]]
876
876
  echo_safe_format_table(dd, columns=res["meta"])
877
877
  else:
878
878
  click.echo(FeedbackManager.info_no_rows())
@@ -699,10 +699,12 @@ def create_workspace_interactive(
699
699
  def print_data_branch_summary(client, job_id, response=None):
700
700
  response = client.job(job_id) if job_id else response or {"partitions": []}
701
701
  columns = ["Data Source", "Partition", "Status", "Error"]
702
- table = []
702
+ table: list[list] = []
703
703
  for partition in response["partitions"]:
704
- for p in partition["partitions"]:
705
- table.append([partition["datasource"]["name"], p["partition"], p["status"], p.get("error", "")])
704
+ table.extend(
705
+ [partition["datasource"]["name"], p["partition"], p["status"], p.get("error", "")]
706
+ for p in partition["partitions"]
707
+ )
706
708
  echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
707
709
 
708
710
 
@@ -2262,10 +2264,11 @@ def create_organization_and_add_workspaces(
2262
2264
 
2263
2265
  # Add existing orphan workspaces to the organization - this is only needed for backwards compatibility
2264
2266
  user_workspaces = client.user_workspaces_with_organization(version="v1")
2265
- workspaces_to_migrate = []
2266
- for workspace in user_workspaces["workspaces"]:
2267
- if workspace.get("organization") is None and workspace.get("role") == "admin":
2268
- workspaces_to_migrate.append(workspace["id"])
2267
+ workspaces_to_migrate = [
2268
+ workspace["id"]
2269
+ for workspace in user_workspaces["workspaces"]
2270
+ if workspace.get("organization") is None and workspace.get("role") == "admin"
2271
+ ]
2269
2272
  client.add_workspaces_to_organization(organization["id"], workspaces_to_migrate)
2270
2273
 
2271
2274
  return organization
@@ -2283,7 +2286,6 @@ def get_user_token(config: CLIConfig, user_token: Optional[str] = None) -> str:
2283
2286
  check_user_token_with_client(client, user_token)
2284
2287
  except Exception:
2285
2288
  user_token = None
2286
- pass
2287
2289
  if not user_token:
2288
2290
  user_token = ask_for_user_token("delete a workspace", ui_host)
2289
2291
  if not user_token:
@@ -293,18 +293,6 @@ def _prompt_sdk(sdk: Optional[str]) -> str:
293
293
  if sdk:
294
294
  return sdk.lower()
295
295
 
296
- click.echo(FeedbackManager.highlight(message="\n? Select project type:"))
297
- click.echo(" [1] typescript - Tinybird TypeScript SDK")
298
- click.echo(" [2] python - Tinybird Python SDK")
299
- click.echo(" [3] cli - Tinybird CLI datafiles project")
300
- choice = click.prompt("\nSelect option", default=3, type=int)
301
- if choice == 1:
302
- return "typescript"
303
- if choice == 2:
304
- return "python"
305
- if choice == 3:
306
- return "cli"
307
- click.echo(FeedbackManager.warning(message=f"Invalid option '{choice}'. Defaulting to {DEFAULT_SDK}."))
308
296
  return DEFAULT_SDK
309
297
 
310
298
 
@@ -544,7 +544,7 @@ def process(
544
544
  if (
545
545
  fork_downstream
546
546
  and r.get("resource", "") == "pipes"
547
- and any(["engine" in x.get("params", {}) for x in r.get("nodes", [])])
547
+ and any("engine" in x.get("params", {}) for x in r.get("nodes", []))
548
548
  ):
549
549
  raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
550
550
 
@@ -1087,7 +1087,7 @@ def process_file(
1087
1087
  deps = []
1088
1088
  nodes: List[Dict[str, Any]] = []
1089
1089
 
1090
- is_copy = any([node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY])
1090
+ is_copy = any(node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY)
1091
1091
  for node in doc.nodes:
1092
1092
  sql = node["sql"]
1093
1093
  node_type = node.get("type", "standard").lower()
@@ -166,7 +166,7 @@ def new_ds(
166
166
  existing = existing_ds.get("indexes", [])
167
167
  new.sort(key=lambda x: x["name"])
168
168
  existing.sort(key=lambda x: x["name"])
169
- if len(existing) != len(new) or any([(d, d2) for d, d2 in zip(new, existing) if d != d2]):
169
+ if len(existing) != len(new) or any((d, d2) for d, d2 in zip(new, existing) if d != d2):
170
170
  new_indices = ds.get("params", {}).get("indexes") or "0"
171
171
  if (
172
172
  new_description
@@ -253,7 +253,7 @@ def is_materialized(resource: Optional[Dict[str, Any]]) -> bool:
253
253
  return False
254
254
 
255
255
  is_materialized = any(
256
- [node.get("params", {}).get("type", None) == "materialized" for node in resource.get("nodes", []) or []]
256
+ node.get("params", {}).get("type", None) == "materialized" for node in resource.get("nodes", []) or []
257
257
  )
258
258
  return is_materialized
259
259
 
@@ -702,7 +702,7 @@ def process(
702
702
  if (
703
703
  fork_downstream
704
704
  and r.get("resource", "") == "pipes"
705
- and any(["engine" in x.get("params", {}) for x in r.get("nodes", [])])
705
+ and any("engine" in x.get("params", {}) for x in r.get("nodes", []))
706
706
  ):
707
707
  raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
708
708
 
@@ -1239,7 +1239,7 @@ def process_file(
1239
1239
  deps = []
1240
1240
  nodes: List[Dict[str, Any]] = []
1241
1241
 
1242
- is_copy = any([node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY])
1242
+ is_copy = any(node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY)
1243
1243
  for node in doc.nodes:
1244
1244
  sql = node["sql"]
1245
1245
  node_type = node.get("type", "standard").lower()
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import os
3
4
  from datetime import datetime, timedelta, timezone
4
5
  from pathlib import Path
5
6
  from typing import Any, Dict, Optional
@@ -7,14 +8,17 @@ from typing import Any, Dict, Optional
7
8
  import click
8
9
  import requests
9
10
 
11
+ from tinybird.tb.client import TinyB
10
12
  from tinybird.tb.modules.cli import cli
11
13
  from tinybird.tb.modules.common import (
12
14
  echo_safe_humanfriendly_tables_format_smart_table,
13
15
  sys_exit,
14
16
  )
17
+ from tinybird.tb.modules.create import persist_tinybird_config
15
18
  from tinybird.tb.modules.deployment_common import (
16
19
  create_deployment,
17
20
  discard_deployment,
21
+ migrate_to_forward_workspace,
18
22
  promote_deployment,
19
23
  )
20
24
  from tinybird.tb.modules.feedback_manager import FeedbackManager
@@ -151,12 +155,68 @@ def api_fetch(url: str, headers: dict, request_from: Optional[str] = None) -> di
151
155
  return {}
152
156
 
153
157
 
158
+ def _get_classic_workspace_branches(client: TinyB, workspace_id: str) -> list[dict[str, Any]]:
159
+ branches: list[dict[str, Any]] = client.user_workspace_branches(version="v0").get("workspaces", [])
160
+ return [branch for branch in branches if str(branch.get("main")) == workspace_id]
161
+
162
+
163
+ def _get_non_live_classic_releases(client: TinyB, workspace_id: str) -> list[dict[str, Any]]:
164
+ releases: list[dict[str, Any]] = client.releases(workspace_id=workspace_id).get("releases", [])
165
+ return [release for release in releases if release.get("status") != "live"]
166
+
167
+
168
+ def _cleanup_classic_migration_blockers(client: TinyB, config: Dict[str, Any]) -> None:
169
+ workspace_id = str(config["id"])
170
+ workspace_name = str(config["name"])
171
+ branches = _get_classic_workspace_branches(client, workspace_id)
172
+ releases = _get_non_live_classic_releases(client, workspace_id)
173
+
174
+ if not branches and not releases:
175
+ return
176
+
177
+ try:
178
+ for branch in branches:
179
+ client.delete_branch(id=str(branch["id"]))
180
+
181
+ for release in releases:
182
+ client.release_rm(
183
+ workspace_id=workspace_id,
184
+ semver=str(release["semver"]),
185
+ confirmation=workspace_name,
186
+ dry_run=False,
187
+ force=False,
188
+ )
189
+ except Exception as e:
190
+ message = f"Error cleaning up Classic branches or releases before migration: {str(e)}"
191
+ click.echo(FeedbackManager.error(message=message))
192
+ sys_exit("migration_error", message)
193
+
194
+
195
+ def _persist_migrate_to_forward_config(project: Project) -> None:
196
+ root_folder = os.getcwd()
197
+ project_folder = os.path.relpath(project.path.resolve(), root_folder)
198
+
199
+ config_changed, config_created = persist_tinybird_config(
200
+ root_folder=root_folder,
201
+ project_type="cli",
202
+ dev_mode="manual",
203
+ folder=project_folder,
204
+ )
205
+
206
+ if not config_changed:
207
+ return
208
+
209
+ message = "Created tinybird.config.json for the Forward CLI"
210
+ if not config_created:
211
+ message = "Updated tinybird.config.json for the Forward CLI"
212
+ click.echo(FeedbackManager.info(message=message))
213
+
214
+
154
215
  @cli.group(name="deployment")
155
216
  def deployment_group() -> None:
156
217
  """
157
218
  Deployment commands.
158
219
  """
159
- pass
160
220
 
161
221
 
162
222
  @deployment_group.command(name="create")
@@ -262,9 +322,7 @@ def deployment_ls(ctx: click.Context, include_deleted: bool) -> None:
262
322
  # Handle different output formats
263
323
  if output == "json":
264
324
  # Create JSON structure
265
- deployments_json = []
266
- for row in table:
267
- deployments_json.append({"id": row[0], "status": row[1], "created_at": row[2]})
325
+ deployments_json = [{"id": row[0], "status": row[1], "created_at": row[2]} for row in table]
268
326
  from tinybird.tb.modules.common import echo_json
269
327
 
270
328
  echo_json({"deployments": deployments_json})
@@ -374,6 +432,104 @@ def deploy(
374
432
  create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template, verbose)
375
433
 
376
434
 
435
+ @cli.command(name="migrate-to-forward")
436
+ @click.option(
437
+ "--allow-destructive-operations/--no-allow-destructive-operations",
438
+ is_flag=True,
439
+ default=False,
440
+ help="Allow destructive operations in deployments (for example replacing a Pipe with a Data Source).",
441
+ )
442
+ @click.pass_context
443
+ def migrate_to_forward(ctx: click.Context, allow_destructive_operations: bool) -> None:
444
+ """Migrate a Tinybird Classic cloud workspace to Tinybird Forward."""
445
+ client = ctx.ensure_object(dict)["client"]
446
+ project: Project = ctx.ensure_object(dict)["project"]
447
+ config: Dict[str, Any] = ctx.ensure_object(dict)["config"]
448
+ env = ctx.ensure_object(dict)["env"]
449
+ output = ctx.ensure_object(dict)["output"]
450
+
451
+ try:
452
+ workspace_info = client.workspace_info(version="v0")
453
+ except Exception as e:
454
+ message = f"Error checking workspace status: {str(e)}"
455
+ click.echo(FeedbackManager.error(message=message))
456
+ sys_exit("migration_error", message)
457
+
458
+ if workspace_info.get("is_forward", False):
459
+ message = "This command is unavailable for Tinybird Forward workspaces."
460
+ click.echo(FeedbackManager.error(message=message))
461
+ sys_exit("migration_error", message)
462
+
463
+ click.echo(
464
+ FeedbackManager.warning(
465
+ message=(
466
+ "This operation is irreversible: once your workspace is migrated to Tinybird Forward, "
467
+ "you cannot switch it back to Tinybird Classic. It will also run your first Forward deployment."
468
+ )
469
+ )
470
+ )
471
+
472
+ if not click.confirm("Do you want to proceed and run the deployment check now?", default=False):
473
+ click.echo(FeedbackManager.info(message="Migration cancelled."))
474
+ return
475
+
476
+ check_result = create_deployment(
477
+ project,
478
+ client,
479
+ config,
480
+ wait=False,
481
+ auto=False,
482
+ verbose=False,
483
+ check=True,
484
+ allow_destructive_operations=allow_destructive_operations,
485
+ output=output,
486
+ env=env,
487
+ show_migrate_to_forward_hint=False,
488
+ return_check_result=True,
489
+ skip_forward_workspace_validation=True,
490
+ )
491
+ if not check_result:
492
+ message = "Deployment check did not complete. Migration cancelled."
493
+ click.echo(FeedbackManager.error(message=message))
494
+ sys_exit("migration_error", message)
495
+
496
+ if check_result and check_result.get("status") == "no_changes":
497
+ click.echo(
498
+ FeedbackManager.warning(
499
+ message=(
500
+ "No deployment changes were detected. Add this dummy pipe to your workspace and run "
501
+ "`tb migrate-to-forward` again:"
502
+ )
503
+ )
504
+ )
505
+ click.echo("NODE n\nSQL >\n select 'Forward'")
506
+ return
507
+
508
+ if not click.confirm(
509
+ "Do you want to continue with the migration? This will also delete your branches, releases and switch your workspace from Classic to Forward.",
510
+ default=False,
511
+ ):
512
+ click.echo(FeedbackManager.info(message="Migration cancelled."))
513
+ return
514
+
515
+ _persist_migrate_to_forward_config(project)
516
+ _cleanup_classic_migration_blockers(client, config)
517
+ migrate_to_forward_workspace(client=client, output=output, dry_run=False)
518
+ create_deployment(
519
+ project,
520
+ client,
521
+ config,
522
+ wait=True,
523
+ auto=True,
524
+ verbose=False,
525
+ check=False,
526
+ allow_destructive_operations=allow_destructive_operations,
527
+ output=output,
528
+ env=env,
529
+ skip_forward_workspace_validation=True,
530
+ )
531
+
532
+
377
533
  def create_deployment_cmd(
378
534
  ctx: click.Context,
379
535
  wait: bool,
@@ -384,6 +540,7 @@ def create_deployment_cmd(
384
540
  verbose: bool = False,
385
541
  ) -> None:
386
542
  output = ctx.ensure_object(dict)["output"]
543
+ env = ctx.ensure_object(dict)["env"]
387
544
  project: Project = ctx.ensure_object(dict)["project"]
388
545
  if template:
389
546
  if project.get_project_files():
@@ -419,6 +576,7 @@ def create_deployment_cmd(
419
576
  allow_destructive_operations,
420
577
  ingest_hint=not is_web_analytics_starter_kit,
421
578
  output=output,
579
+ env=env,
422
580
  )
423
581
  show_web_analytics_starter_kit_hints(client, is_web_analytics_starter_kit)
424
582