tinybird 4.6.6.dev0__tar.gz → 4.6.7.dev0__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 (127) hide show
  1. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/PKG-INFO +8 -1
  2. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/__cli__.py +2 -2
  3. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/build.py +1 -46
  4. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/config.py +0 -3
  5. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/playground.py +1 -329
  6. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/local_common.py +124 -22
  7. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/watch.py +1 -100
  8. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/config.py +0 -6
  9. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird.egg-info/PKG-INFO +8 -1
  10. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/setup.cfg +0 -0
  11. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/__cli__.py +0 -0
  12. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/ch_utils/constants.py +0 -0
  13. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/ch_utils/engine.py +0 -0
  14. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/check_pypi.py +0 -0
  15. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/client.py +0 -0
  16. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/config.py +0 -0
  17. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/context.py +0 -0
  18. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datafile/common.py +0 -0
  19. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datafile/exceptions.py +0 -0
  20. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datafile/parse_connection.py +0 -0
  21. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datafile/parse_datasource.py +0 -0
  22. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datafile/parse_pipe.py +0 -0
  23. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/datatypes.py +0 -0
  24. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/feedback_manager.py +0 -0
  25. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/git_settings.py +0 -0
  26. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/iterating/__init__.py +0 -0
  27. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/iterating/data_branch_modes.py +0 -0
  28. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/prompts.py +0 -0
  29. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/service_datasources.py +0 -0
  30. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/sql.py +0 -0
  31. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/sql_template.py +0 -0
  32. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/sql_template_fmt.py +0 -0
  33. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/sql_toolset.py +0 -0
  34. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/syncasync.py +0 -0
  35. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/check_pypi.py +0 -0
  36. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/cli.py +0 -0
  37. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/client.py +0 -0
  38. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/config.py +0 -0
  39. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/branch.py +0 -0
  40. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/build_common.py +0 -0
  41. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/cicd.py +0 -0
  42. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/cli.py +0 -0
  43. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/common.py +0 -0
  44. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/connection.py +0 -0
  45. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/connection_dynamodb.py +0 -0
  46. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/connection_kafka.py +0 -0
  47. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/connection_s3.py +0 -0
  48. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/copy.py +0 -0
  49. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/create.py +0 -0
  50. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/build.py +0 -0
  51. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/build_common.py +0 -0
  52. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  53. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  54. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/diff.py +0 -0
  55. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/fixture.py +0 -0
  56. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/format_common.py +0 -0
  57. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  58. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  59. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  60. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  61. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datafile/pull.py +0 -0
  62. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/datasource.py +0 -0
  63. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/deployment.py +0 -0
  64. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/deployment_common.py +0 -0
  65. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/deprecations.py +0 -0
  66. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/endpoint.py +0 -0
  67. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/exceptions.py +0 -0
  68. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/feedback_manager.py +0 -0
  69. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/fmt.py +0 -0
  70. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/info.py +0 -0
  71. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/infra.py +0 -0
  72. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/job.py +0 -0
  73. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/job_common.py +0 -0
  74. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/llm.py +0 -0
  75. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/llm_utils.py +0 -0
  76. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/local.py +0 -0
  77. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/local_logs.py +0 -0
  78. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/login.py +0 -0
  79. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/login_common.py +0 -0
  80. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/logout.py +0 -0
  81. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/logs.py +0 -0
  82. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/materialization.py +0 -0
  83. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/open.py +0 -0
  84. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/pipe.py +0 -0
  85. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/preview.py +0 -0
  86. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/project.py +0 -0
  87. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/project_commands.py +0 -0
  88. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/py_project.py +0 -0
  89. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/query_output.py +0 -0
  90. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/regions.py +0 -0
  91. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/secret.py +0 -0
  92. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/secret_common.py +0 -0
  93. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/sink.py +0 -0
  94. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/table.py +0 -0
  95. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/telemetry.py +0 -0
  96. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/test.py +0 -0
  97. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/test_common.py +0 -0
  98. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  99. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/token.py +0 -0
  100. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/ts_project.py +0 -0
  101. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/workspace.py +0 -0
  102. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb/modules/workspace_members.py +0 -0
  103. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli.py +0 -0
  104. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  105. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  106. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  107. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  108. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/common.py +0 -0
  109. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  110. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/datasource.py +0 -0
  111. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  112. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  113. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  114. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  115. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  116. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  117. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  118. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  119. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  120. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
  121. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  122. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird/tornado_template.py +0 -0
  123. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird.egg-info/SOURCES.txt +0 -0
  124. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird.egg-info/dependency_links.txt +0 -0
  125. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird.egg-info/entry_points.txt +0 -0
  126. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/tinybird.egg-info/requires.txt +0 -0
  127. {tinybird-4.6.6.dev0 → tinybird-4.6.7.dev0}/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.6.6.dev0
3
+ Version: 4.6.7.dev0
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,13 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.6
56
+ *******
57
+
58
+ - `Fixed` `tb local` now starts the Tinybird Local container using the host's native architecture (``linux/arm64`` on Apple Silicon, ``linux/amd64`` elsewhere) instead of always forcing ``linux/amd64``, so it no longer runs under emulation on arm64 machines.
59
+ - `Added` ``TB_LOCAL_IMAGE_PLATFORM`` environment variable to override the auto-detected image platform (e.g. ``linux/amd64`` to run the amd64 image emulated on an arm64 host, or to run a locally built single-arch image that does not match the host).
60
+ - `Fixed` `tb local start` new-version detection now normalizes Docker Hub image digests and no longer pulls when the registry is unreachable.
61
+
55
62
  4.6.5
56
63
  *******
57
64
 
@@ -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.6.6.dev0'
8
- __revision__ = '939e465'
7
+ __version__ = '4.6.7.dev0'
8
+ __revision__ = 'd3cbd87'
@@ -1,22 +1,15 @@
1
- from copy import deepcopy
2
1
  from functools import partial
3
2
  from pathlib import Path
4
- from typing import Any, Callable, Dict, List, Optional
5
- from urllib.parse import urlencode
3
+ from typing import Any, Callable, Dict, Optional
6
4
 
7
5
  import click
8
6
 
9
- from tinybird.datafile.exceptions import ParseException
10
- from tinybird.datafile.parse_datasource import parse_datasource
11
- from tinybird.datafile.parse_pipe import parse_pipe
12
7
  from tinybird.tb.client import TinyB
13
8
  from tinybird.tb.config import CLOUD_HOSTS
14
9
  from tinybird.tb.modules.build_common import process
15
10
  from tinybird.tb.modules.cli import cli, get_current_git_branch
16
- from tinybird.tb.modules.config import CLIConfig
17
11
  from tinybird.tb.modules.feedback_manager import FeedbackManager
18
12
  from tinybird.tb.modules.project import Project
19
- from tinybird.tb.modules.query_output import print_table_formatted
20
13
  from tinybird.tb.modules.watch import watch_project
21
14
 
22
15
 
@@ -182,41 +175,3 @@ def is_endpoint(f: Path) -> bool:
182
175
 
183
176
  def is_pipe(f: Path) -> bool:
184
177
  return f.suffix == ".pipe" and not is_vendor(f)
185
-
186
-
187
- def check_filenames(filenames: List[str]):
188
- parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
189
- incl_suffix = ".incl"
190
-
191
- for filename in filenames:
192
- file_suffix = Path(filename).suffix
193
- if file_suffix == incl_suffix:
194
- continue
195
-
196
- parser = parser_matrix.get(file_suffix)
197
- if not parser:
198
- raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
199
-
200
- parser(filename)
201
-
202
-
203
- def build_and_print_resource(config: CLIConfig, tb_client: TinyB, filename: str):
204
- resource_path = Path(filename)
205
- name = resource_path.stem
206
- playground_name = name if filename.endswith(".pipe") else None
207
- user_client = deepcopy(tb_client)
208
- user_client.token = config.get_user_token() or ""
209
- cli_params = {}
210
- cli_params["workspace_id"] = config.get("id", None)
211
- data = user_client._req(f"/v0/playgrounds?{urlencode(cli_params)}")
212
- playgrounds = data["playgrounds"]
213
- playground = next((p for p in playgrounds if p["name"] == (f"{playground_name}" + "__tb__playground")), None)
214
- if not playground:
215
- return
216
- playground_id = playground["id"]
217
- last_node = playground["nodes"][-1]
218
- if not last_node:
219
- return
220
- node_sql = last_node["sql"]
221
- res = tb_client.query(f"{node_sql} FORMAT JSON", playground=playground_id)
222
- print_table_formatted(res, name)
@@ -197,9 +197,6 @@ class CLIConfig:
197
197
  netloc: str = url_info.netloc.decode() if isinstance(url_info.netloc, bytes) else url_info.netloc
198
198
  self["host"] = f"{scheme}://{netloc}"
199
199
 
200
- def set_cwd(self, cwd: str) -> None:
201
- self["cwd"] = cwd
202
-
203
200
  def get_host(self, use_defaults_if_needed: bool = False) -> Optional[str]:
204
201
  result: Optional[str] = self.get("host", None)
205
202
  if result:
@@ -9,7 +9,6 @@ from pathlib import Path
9
9
  from typing import Any, Dict, Iterable, List, Optional, Set, Tuple
10
10
 
11
11
  import click
12
- from toposort import toposort
13
12
 
14
13
  from tinybird.datafile.common import (
15
14
  DEFAULT_CRON_PERIOD,
@@ -27,7 +26,7 @@ from tinybird.datafile.common import (
27
26
  get_project_filenames,
28
27
  pp,
29
28
  )
30
- from tinybird.datafile.exceptions import AlreadyExistsException, IncludeFileNotFoundException
29
+ from tinybird.datafile.exceptions import IncludeFileNotFoundException
31
30
  from tinybird.datafile.parse_datasource import parse_datasource
32
31
  from tinybird.datafile.parse_pipe import parse_pipe
33
32
  from tinybird.sql import parse_table_structure, schema_to_sql_columns
@@ -35,339 +34,12 @@ from tinybird.sql_template import get_used_tables_in_template, render_sql_templa
35
34
  from tinybird.tb.client import TinyB
36
35
  from tinybird.tb.modules.common import get_ca_pem_content
37
36
  from tinybird.tb.modules.config import CLIConfig
38
- from tinybird.tb.modules.datafile.build_datasource import is_datasource
39
37
  from tinybird.tb.modules.datafile.build_pipe import (
40
38
  get_target_materialized_data_source_name,
41
- is_endpoint,
42
39
  is_endpoint_with_no_dependencies,
43
- is_materialized,
44
40
  new_pipe,
45
41
  )
46
42
  from tinybird.tb.modules.feedback_manager import FeedbackManager, get_cli_name
47
- from tinybird.tb.modules.project import Project
48
-
49
-
50
- def folder_playground(
51
- project: Project,
52
- config: CLIConfig,
53
- tb_client: TinyB,
54
- filenames: Optional[List[str]] = None,
55
- is_internal: bool = False,
56
- current_ws: Optional[Dict[str, Any]] = None,
57
- local_ws: Optional[Dict[str, Any]] = None,
58
- ):
59
- build = True
60
- dry_run = False
61
- force = True
62
- only_changes = True
63
- debug = False
64
- run_tests = False
65
- verbose = False
66
- raise_on_exists = False
67
- fork_downstream = True
68
- fork = False
69
- release_created = False
70
- folder = str(project.path)
71
- datasources: List[Dict[str, Any]] = tb_client.datasources()
72
- pipes: List[Dict[str, Any]] = tb_client.pipes(dependencies=True)
73
- build = True
74
- dry_run = False
75
- force = True
76
- only_changes = True
77
- debug = False
78
- check = True
79
- populate = False
80
- populate_subset = None
81
- populate_condition = None
82
- tests_to_run = 0
83
- override_datasource = False
84
- skip_confirmation = True
85
- wait = False
86
- unlink_on_populate_error = False
87
- only_response_times = False
88
- run_tests = False
89
- verbose = False
90
- as_standard = False
91
- raise_on_exists = False
92
- fork_downstream = True
93
- fork = False
94
- release_created = False
95
- tests_relative_change = 0.01
96
- tests_sample_by_params = 0
97
- tests_filter_by = None
98
- tests_failfast = False
99
- tests_ignore_order = False
100
- tests_validate_processed_bytes = False
101
- tests_check_requests_from_branch = False
102
- user_token = None
103
- ignore_sql_errors = False
104
- is_vendor = False
105
- current_ws = current_ws or local_ws
106
-
107
- existing_resources: List[str] = [x["name"] for x in datasources] + [x["name"] for x in pipes]
108
- remote_resource_names = [get_remote_resource_name_without_version(x) for x in existing_resources]
109
-
110
- if not filenames:
111
- filenames = get_project_filenames(folder)
112
-
113
- # build graph to get new versions for all the files involved in the query
114
- # dependencies need to be processed always to get the versions
115
- dependencies_graph = build_graph(
116
- filenames,
117
- tb_client,
118
- dir_path=folder,
119
- process_dependencies=True,
120
- skip_connectors=True,
121
- vendor_paths=[],
122
- current_ws=current_ws,
123
- only_changes=only_changes,
124
- fork_downstream=fork_downstream,
125
- is_internal=is_internal,
126
- build=build,
127
- )
128
-
129
- if debug:
130
- pp.pprint(dependencies_graph.to_run)
131
-
132
- def should_push_file(
133
- name: str,
134
- remote_resource_names: List[str],
135
- force: bool,
136
- run_tests: bool,
137
- ) -> bool:
138
- """
139
- Function to know if we need to run a file or not
140
- """
141
- if name not in remote_resource_names:
142
- return True
143
- # When we need to try to push a file when it doesn't exist and the version is different that the existing one
144
- resource_full_name = name
145
- if resource_full_name not in existing_resources:
146
- return True
147
- return force or run_tests
148
-
149
- def push(
150
- name: str,
151
- to_run: Dict[str, Dict[str, Any]],
152
- dry_run: bool,
153
- fork_downstream: Optional[bool] = False,
154
- fork: Optional[bool] = False,
155
- ):
156
- if name in to_run:
157
- resource = to_run[name]["resource"]
158
- if resource == "datasources":
159
- return
160
- if not dry_run:
161
- if should_push_file(name, remote_resource_names, force, run_tests):
162
- click.echo(FeedbackManager.info_processing_new_resource(name=name, version=""))
163
- try:
164
- exec_file(
165
- to_run[name],
166
- config,
167
- tb_client,
168
- force,
169
- check,
170
- debug and verbose,
171
- populate,
172
- populate_subset,
173
- populate_condition,
174
- unlink_on_populate_error,
175
- wait,
176
- user_token,
177
- override_datasource,
178
- ignore_sql_errors,
179
- skip_confirmation,
180
- only_response_times,
181
- run_tests,
182
- as_standard,
183
- tests_to_run,
184
- tests_relative_change,
185
- tests_sample_by_params,
186
- tests_filter_by,
187
- tests_failfast,
188
- tests_ignore_order,
189
- tests_validate_processed_bytes,
190
- tests_check_requests_from_branch,
191
- current_ws,
192
- local_ws,
193
- fork_downstream,
194
- fork,
195
- build,
196
- is_vendor,
197
- )
198
- if not run_tests:
199
- click.echo(
200
- FeedbackManager.success_create(
201
- name=(
202
- name
203
- if to_run[name]["version"] is None
204
- else f"{name}__v{to_run[name]['version']}"
205
- )
206
- )
207
- )
208
- except Exception as e:
209
- filename = to_run[name]["filename"]
210
- exception = FeedbackManager.error_push_file_exception(
211
- filename=filename,
212
- error=e,
213
- )
214
- raise click.ClickException(exception)
215
- elif raise_on_exists:
216
- raise AlreadyExistsException(
217
- FeedbackManager.warning_name_already_exists(
218
- name=name if to_run[name]["version"] is None else f"{name}__v{to_run[name]['version']}"
219
- )
220
- )
221
- elif name_matches_existing_resource(resource, name, tb_client):
222
- if resource == "pipes":
223
- click.echo(FeedbackManager.error_pipe_cannot_be_pushed(name=name))
224
- else:
225
- click.echo(FeedbackManager.error_datasource_cannot_be_pushed(name=name))
226
- else:
227
- click.echo(
228
- FeedbackManager.warning_name_already_exists(
229
- name=(name if to_run[name]["version"] is None else f"{name}__v{to_run[name]['version']}")
230
- )
231
- )
232
- if dry_run:
233
- if should_push_file(name, remote_resource_names, force, run_tests):
234
- extension = "pipe" if resource == "pipes" else "datasource"
235
- click.echo(FeedbackManager.info_building_resource(name=f"{name}.{extension}", version=""))
236
- elif name_matches_existing_resource(resource, name, tb_client):
237
- if resource == "pipes":
238
- click.echo(FeedbackManager.warning_pipe_cannot_be_pushed(name=name))
239
- else:
240
- click.echo(FeedbackManager.warning_datasource_cannot_be_pushed(name=name))
241
- else:
242
- click.echo(FeedbackManager.warning_dry_name_already_exists(name=name))
243
-
244
- def push_files(
245
- dependency_graph: GraphDependencies,
246
- dry_run: bool = False,
247
- ):
248
- endpoints_dep_map = dict()
249
- processed = set()
250
-
251
- resources_to_run = dependency_graph.to_run
252
-
253
- # This will generate the graph from right to left and will fill the gaps of the dependencies
254
- # If we have a graph like this:
255
- # A -> B -> C
256
- # If we only modify A, the normal dependencies graph will only contain a node like _{A => B}
257
- # But we need a graph that contains A, B and C and the dependencies between them to deploy them in the right order
258
- dependencies_graph_fork_downstream, resources_to_run_fork_downstream = generate_forkdownstream_graph(
259
- dependency_graph.all_dep_map,
260
- dependency_graph.all_resources,
261
- resources_to_run,
262
- list(dependency_graph.dep_map.keys()),
263
- )
264
-
265
- # First, we will deploy the datasources that need to be deployed.
266
- # We need to deploy the datasources from left to right as some datasources might have MV that depend on the column types of previous datasources. Ex: `test_change_column_type_landing_datasource` test
267
- groups = [group for group in toposort(dependencies_graph_fork_downstream)]
268
-
269
- groups.reverse()
270
- for group in groups:
271
- for name in group:
272
- if name in processed or not is_datasource(resources_to_run_fork_downstream[name]):
273
- continue
274
-
275
- # If we are trying to modify a Kafka or CDK datasource, we need to inform the user that the resource needs to be post-released
276
- kafka_connection_name = (
277
- resources_to_run_fork_downstream[name].get("params", {}).get("kafka_connection_name")
278
- )
279
- service = resources_to_run_fork_downstream[name].get("params", {}).get("import_service")
280
- if release_created and (kafka_connection_name or service):
281
- connector = "Kafka" if kafka_connection_name else service
282
- error_msg = FeedbackManager.error_connector_require_post_release(connector=connector)
283
- raise click.ClickException(error_msg)
284
-
285
- push(
286
- name,
287
- resources_to_run_fork_downstream,
288
- dry_run,
289
- fork_downstream,
290
- fork,
291
- )
292
- processed.add(name)
293
-
294
- # Now, we will create a map of all the endpoints and there dependencies
295
- # We are using the forkdownstream graph to get the dependencies of the endpoints as the normal dependencies graph only contains the resources that are going to be deployed
296
- # But does not include the missing gaps
297
- # If we have ENDPOINT_A ----> MV_PIPE_B -----> DATASOURCE_B ------> ENDPOINT_C
298
- # Where endpoint A is being used in the MV_PIPE_B, if we only modify the endpoint A
299
- # The dependencies graph will only contain the endpoint A and the MV_PIPE_B, but not the DATASOURCE_B and the ENDPOINT_C
300
- groups = [group for group in toposort(dependencies_graph_fork_downstream)]
301
- for group in groups:
302
- for name in group:
303
- if name in processed or not is_endpoint(resources_to_run_fork_downstream[name]):
304
- continue
305
-
306
- endpoints_dep_map[name] = dependencies_graph_fork_downstream[name]
307
-
308
- # Now that we have the dependencies of the endpoints, we need to check that the resources has not been deployed yet and only care about the endpoints that depend on endpoints
309
- groups = [group for group in toposort(endpoints_dep_map)]
310
-
311
- # As we have used the forkdownstream graph to get the dependencies of the endpoints, we have all the dependencies of the endpoints
312
- # But we need to deploy the endpoints and the dependencies of the endpoints from left to right
313
- # So we need to reverse the groups
314
- groups.reverse()
315
- for group in groups:
316
- for name in group:
317
- if name in processed or not is_endpoint(resources_to_run_fork_downstream[name]):
318
- continue
319
-
320
- push(
321
- name,
322
- resources_to_run_fork_downstream,
323
- dry_run,
324
- fork_downstream,
325
- fork,
326
- )
327
- processed.add(name)
328
-
329
- # Now we should have the endpoints and datasources deployed, we can deploy the rest of the pipes (copy & sinks)
330
- # We need to rely on the forkdownstream graph as it contains all the modified pipes as well as the dependencies of the pipes
331
- # In this case, we don't need to generate a new graph as we did for the endpoints as the pipes are not going to be used as dependencies and the datasources are already deployed
332
- groups = [group for group in toposort(dependencies_graph_fork_downstream)]
333
- for group in groups:
334
- for name in group:
335
- if name in processed or is_materialized(resources_to_run_fork_downstream.get(name)):
336
- continue
337
-
338
- push(
339
- name,
340
- resources_to_run_fork_downstream,
341
- dry_run,
342
- fork_downstream,
343
- fork,
344
- )
345
- processed.add(name)
346
-
347
- # Finally, we need to deploy the materialized views from right to left.
348
- # We need to rely on the forkdownstream graph as it contains all the modified materialized views as well as the dependencies of the materialized views
349
- # In this case, we don't need to generate a new graph as we did for the endpoints as the pipes are not going to be used as dependencies and the datasources are already deployed
350
- groups = [group for group in toposort(dependencies_graph_fork_downstream)]
351
- for group in groups:
352
- for name in group:
353
- if name in processed or not is_materialized(resources_to_run_fork_downstream.get(name)):
354
- continue
355
-
356
- push(
357
- name,
358
- resources_to_run_fork_downstream,
359
- dry_run,
360
- fork_downstream,
361
- fork,
362
- )
363
- processed.add(name)
364
-
365
- push_files(dependencies_graph, dry_run)
366
-
367
- if not dry_run and not run_tests and verbose:
368
- click.echo(FeedbackManager.info_not_pushing_fixtures())
369
-
370
- return dependencies_graph.to_run
371
43
 
372
44
 
373
45
  def name_matches_existing_resource(resource: str, name: str, tb_client: TinyB):
@@ -2,6 +2,7 @@ import hashlib
2
2
  import json
3
3
  import logging
4
4
  import os
5
+ import platform
5
6
  import re
6
7
  import subprocess
7
8
  import threading
@@ -13,6 +14,7 @@ import boto3
13
14
  import click
14
15
  import requests
15
16
  from docker.client import DockerClient
17
+ from docker.errors import ImageNotFound
16
18
  from docker.models.containers import Container
17
19
 
18
20
  import docker
@@ -39,6 +41,11 @@ _PATTERN_MESSAGE = re.compile(r'message="([^"]*)"')
39
41
 
40
42
  TB_IMAGE_NAME = "tinybirdco/tinybird-local:latest"
41
43
  TB_CONTAINER_NAME = "tinybird-local"
44
+
45
+ # Docker Hub registry hostnames that Docker may prepend to a repository name in
46
+ # RepoDigests (e.g. ``docker.io/tinybirdco/tinybird-local@sha256:…``). They all
47
+ # refer to the same registry, so we strip them before comparing repositories.
48
+ _DOCKER_HUB_REGISTRY_PREFIXES = ("docker.io/", "index.docker.io/", "registry-1.docker.io/")
42
49
  TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 7181))
43
50
  TB_LOCAL_CLICKHOUSE_INTERFACE_PORT = int(os.getenv("TB_LOCAL_CLICKHOUSE_INTERFACE_PORT", 7182))
44
51
  TB_LOCAL_HOST = _PATTERN_HTTP_PREFIX.sub("", os.getenv("TB_LOCAL_HOST", "localhost"))
@@ -270,6 +277,107 @@ def get_local_tokens(silent: bool = False) -> Dict[str, str]:
270
277
  )
271
278
 
272
279
 
280
+ def get_local_image_platform() -> str:
281
+ """Return the Docker platform for the Tinybird Local image matching the host.
282
+
283
+ Tinybird Local is published as a multi-arch image, so we select the native
284
+ architecture to avoid emulating linux/amd64 on arm64 hosts (e.g. Apple
285
+ Silicon). Defaults to linux/amd64 for unknown architectures.
286
+
287
+ ``TB_LOCAL_IMAGE_PLATFORM`` overrides the auto-detected platform. This is an
288
+ escape hatch for running a single-arch image that does not match the host
289
+ (e.g. deliberately running the amd64 image emulated on an arm64 Mac, or when
290
+ a locally built image only provides one architecture).
291
+ """
292
+ override = os.getenv("TB_LOCAL_IMAGE_PLATFORM")
293
+ if override:
294
+ return override
295
+ machine = platform.machine().lower()
296
+ if machine in ("arm64", "aarch64"):
297
+ return "linux/arm64"
298
+ return "linux/amd64"
299
+
300
+
301
+ def _normalize_repo(name: str) -> str:
302
+ """Strip the Docker Hub registry host from a repository name so digests
303
+ recorded as ``docker.io/tinybirdco/tinybird-local`` and
304
+ ``tinybirdco/tinybird-local`` compare equal."""
305
+ for prefix in _DOCKER_HUB_REGISTRY_PREFIXES:
306
+ if name.startswith(prefix):
307
+ return name[len(prefix) :]
308
+ return name
309
+
310
+
311
+ def _remote_manifest_digests(docker_client: DockerClient, image_name: str) -> set[str]:
312
+ remote_image = docker_client.images.get_registry_data(image_name)
313
+ remote_digests: set[str] = set()
314
+ if isinstance(remote_image.id, str):
315
+ remote_digests.add(remote_image.id)
316
+
317
+ attrs = remote_image.attrs if isinstance(remote_image.attrs, dict) else {}
318
+ descriptor = attrs.get("Descriptor", {})
319
+ if isinstance(descriptor, dict):
320
+ descriptor_digest = descriptor.get("digest")
321
+ if isinstance(descriptor_digest, str):
322
+ remote_digests.add(descriptor_digest)
323
+
324
+ return remote_digests
325
+
326
+
327
+ def is_new_local_image_available(docker_client: DockerClient, check_new_version: bool = True) -> tuple[bool, bool]:
328
+ """Decide whether to pull the Tinybird Local image before starting it.
329
+
330
+ Returns ``(show_prompt, pull_required)``.
331
+
332
+ Two local-only checks always run, even with ``check_new_version=False`` (the
333
+ ``--skip-new-version`` / prompt-to-start paths), because they decide whether
334
+ the cached image is *usable* at all:
335
+ - image missing -> pull it;
336
+ - wrong platform cached -> re-pull. An arm64 user who pulled ``:latest`` with
337
+ the old (amd64-forcing) CLI has the tag cached as ``linux/amd64``; we must
338
+ replace it with the native image instead of running it emulated.
339
+
340
+ Only when ``check_new_version`` is set do we make the registry round-trip to
341
+ detect a newer published image. Docker Engine exposes the tag's registry
342
+ digest through ``get_registry_data``; we compare that digest with the
343
+ normalized local ``RepoDigests`` for this image.
344
+
345
+ We fail safe everywhere: if the registry is unreachable or the local image
346
+ cannot be inspected we keep what is there. A locally built image (no matching
347
+ registry digest) will prompt; developers use ``--skip-new-version`` for that.
348
+ """
349
+ try:
350
+ local_image = docker_client.images.get(TB_IMAGE_NAME)
351
+ except ImageNotFound:
352
+ return False, True # nothing local yet, we must pull
353
+ except Exception:
354
+ return False, False # cannot inspect locally, keep whatever is there
355
+
356
+ local_platform = f"{local_image.attrs.get('Os', '')}/{local_image.attrs.get('Architecture', '')}"
357
+ if local_platform != get_local_image_platform():
358
+ return False, True # wrong platform cached, re-pull the native image
359
+
360
+ if not check_new_version:
361
+ return False, False # cached image is usable; don't touch the registry
362
+
363
+ repo = _PATTERN_TAG_SUFFIX.sub("", TB_IMAGE_NAME)
364
+ local_digests = {
365
+ repo_digest.split("@", 1)[1]
366
+ for repo_digest in (local_image.attrs.get("RepoDigests") or [])
367
+ if "@" in repo_digest and _normalize_repo(repo_digest.split("@", 1)[0]) == repo
368
+ }
369
+
370
+ try:
371
+ remote_digests = _remote_manifest_digests(docker_client, TB_IMAGE_NAME)
372
+ except Exception:
373
+ return False, False # registry unreachable, keep the local image
374
+
375
+ if not remote_digests:
376
+ return False, False # registry metadata unavailable, keep the local image
377
+
378
+ return local_digests.isdisjoint(remote_digests), False
379
+
380
+
273
381
  def start_tinybird_local(
274
382
  docker_client: DockerClient,
275
383
  use_aws_creds: bool,
@@ -280,29 +388,23 @@ def start_tinybird_local(
280
388
  watch: bool = False,
281
389
  ) -> None:
282
390
  """Start the Tinybird container."""
283
- pull_show_prompt = False
284
- pull_required = False
285
-
286
- if not skip_new_version:
287
- try:
288
- local_image = docker_client.images.get(TB_IMAGE_NAME)
289
- local_image_id = local_image.attrs["RepoDigests"][0].split("@")[1]
290
- remote_image = docker_client.images.get_registry_data(TB_IMAGE_NAME)
291
- pull_show_prompt = local_image_id != remote_image.id
292
- except Exception:
293
- pull_show_prompt = False
294
- pull_required = True
391
+ # Always honors a forced pull (missing / wrong-platform image) even when the
392
+ # new-version check is skipped, so an arm64 host never keeps running the
393
+ # emulated amd64 image.
394
+ pull_show_prompt, pull_required = is_new_local_image_available(
395
+ docker_client, check_new_version=not skip_new_version
396
+ )
295
397
 
296
- if pull_show_prompt and click.confirm(
297
- FeedbackManager.warning(message="△ New version detected, download? [y/N]:"),
298
- show_default=False,
299
- prompt_suffix="",
300
- ):
301
- click.echo(FeedbackManager.info(message="* Downloading latest version of Tinybird Local..."))
302
- pull_required = True
398
+ if pull_show_prompt and click.confirm(
399
+ FeedbackManager.warning(message="△ New version detected, download? [y/N]:"),
400
+ show_default=False,
401
+ prompt_suffix="",
402
+ ):
403
+ click.echo(FeedbackManager.info(message="* Downloading latest version of Tinybird Local..."))
404
+ pull_required = True
303
405
 
304
- if pull_required:
305
- docker_client.images.pull(TB_IMAGE_NAME, platform="linux/amd64")
406
+ if pull_required:
407
+ docker_client.images.pull(TB_IMAGE_NAME, platform=get_local_image_platform())
306
408
 
307
409
  environment = {}
308
410
  if use_aws_creds:
@@ -334,7 +436,7 @@ def start_tinybird_local(
334
436
  detach=True,
335
437
  ports={"7181/tcp": TB_LOCAL_PORT, "7182/tcp": TB_LOCAL_CLICKHOUSE_INTERFACE_PORT},
336
438
  remove=False,
337
- platform="linux/amd64",
439
+ platform=get_local_image_platform(),
338
440
  environment=environment,
339
441
  volumes=volumes,
340
442
  )