tinybird 4.6.13.dev0__tar.gz → 4.6.14__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.13.dev0 → tinybird-4.6.14}/PKG-INFO +14 -1
  2. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/client.py +26 -23
  3. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datafile/common.py +1 -0
  4. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datafile/parse_pipe.py +30 -6
  5. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/feedback_manager.py +7 -3
  6. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/service_datasources.py +3 -3
  7. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/sql_template.py +51 -2
  8. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/sql_toolset.py +1 -1
  9. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/__cli__.py +2 -2
  10. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/client.py +37 -14
  11. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/build_common.py +10 -1
  12. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/common.py +25 -3
  13. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/config.py +1 -1
  14. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/build.py +1 -1
  15. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/pipe_checker.py +6 -2
  16. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/playground.py +1 -1
  17. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/pull.py +1 -1
  18. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datasource.py +120 -36
  19. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/feedback_manager.py +0 -3
  20. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/common.py +66 -3
  21. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/config.py +1 -1
  22. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/datasource.py +27 -3
  23. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/workspace.py +10 -6
  24. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tornado_template.py +5 -5
  25. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird.egg-info/PKG-INFO +14 -1
  26. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/setup.cfg +0 -0
  27. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/__cli__.py +0 -0
  28. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/ch_utils/constants.py +0 -0
  29. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/ch_utils/engine.py +0 -0
  30. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/check_pypi.py +0 -0
  31. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/config.py +0 -0
  32. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/context.py +0 -0
  33. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datafile/exceptions.py +0 -0
  34. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datafile/parse_connection.py +0 -0
  35. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datafile/parse_datasource.py +0 -0
  36. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/datatypes.py +0 -0
  37. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/git_settings.py +0 -0
  38. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/iterating/__init__.py +0 -0
  39. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/iterating/data_branch_modes.py +0 -0
  40. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/prompts.py +0 -0
  41. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/sql.py +0 -0
  42. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/sql_template_fmt.py +0 -0
  43. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/syncasync.py +0 -0
  44. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/check_pypi.py +0 -0
  45. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/cli.py +0 -0
  46. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/config.py +0 -0
  47. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/branch.py +0 -0
  48. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/build.py +0 -0
  49. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/cicd.py +0 -0
  50. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/cli.py +0 -0
  51. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/connection.py +0 -0
  52. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/connection_dynamodb.py +0 -0
  53. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/connection_kafka.py +0 -0
  54. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/connection_s3.py +0 -0
  55. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/copy.py +0 -0
  56. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/create.py +0 -0
  57. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/build_common.py +0 -0
  58. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  59. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  60. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/diff.py +0 -0
  61. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/fixture.py +0 -0
  62. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/format_common.py +0 -0
  63. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  64. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  65. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  66. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/deployment.py +0 -0
  67. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/deployment_common.py +0 -0
  68. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/deprecations.py +0 -0
  69. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/endpoint.py +0 -0
  70. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/exceptions.py +0 -0
  71. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/fmt.py +0 -0
  72. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/info.py +0 -0
  73. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/infra.py +0 -0
  74. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/job.py +0 -0
  75. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/job_common.py +0 -0
  76. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/llm.py +0 -0
  77. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/llm_utils.py +0 -0
  78. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/local.py +0 -0
  79. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/local_common.py +0 -0
  80. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/local_logs.py +0 -0
  81. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/login.py +0 -0
  82. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/login_common.py +0 -0
  83. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/logout.py +0 -0
  84. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/logs.py +0 -0
  85. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/materialization.py +0 -0
  86. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/open.py +0 -0
  87. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/pipe.py +0 -0
  88. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/preview.py +0 -0
  89. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/project.py +0 -0
  90. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/project_commands.py +0 -0
  91. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/py_project.py +0 -0
  92. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/query_output.py +0 -0
  93. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/regions.py +0 -0
  94. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/secret.py +0 -0
  95. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/secret_common.py +0 -0
  96. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/sink.py +0 -0
  97. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/table.py +0 -0
  98. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/telemetry.py +0 -0
  99. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/test.py +0 -0
  100. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/test_common.py +0 -0
  101. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  102. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/token.py +0 -0
  103. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/ts_project.py +0 -0
  104. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/watch.py +0 -0
  105. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/workspace.py +0 -0
  106. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb/modules/workspace_members.py +0 -0
  107. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli.py +0 -0
  108. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/auth.py +0 -0
  109. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/branch.py +0 -0
  110. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/cicd.py +0 -0
  111. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/cli.py +0 -0
  112. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/connection.py +0 -0
  113. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/exceptions.py +0 -0
  114. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/fmt.py +0 -0
  115. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/job.py +0 -0
  116. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/pipe.py +0 -0
  117. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/regions.py +0 -0
  118. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/tag.py +0 -0
  119. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/telemetry.py +0 -0
  120. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/test.py +0 -0
  121. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  122. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  123. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird.egg-info/SOURCES.txt +0 -0
  124. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird.egg-info/dependency_links.txt +0 -0
  125. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird.egg-info/entry_points.txt +0 -0
  126. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/tinybird.egg-info/requires.txt +0 -0
  127. {tinybird-4.6.13.dev0 → tinybird-4.6.14}/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.13.dev0
3
+ Version: 4.6.14
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -51,6 +51,17 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
51
51
 
52
52
  Changelog
53
53
  ----------
54
+ 4.6.14
55
+ ********
56
+
57
+ - `Changed` ``tb datasource append`` and ``tb datasource replace`` submit local CSV, NDJSON, and Parquet files through the v1 import endpoint by default. ``tb datasource append --events`` also submits its request body through v1. These asynchronous imports print the queued job ID; pass ``--wait`` to wait for completion. Remote URL imports continue to use their existing route.
58
+
59
+ 4.6.13
60
+ ********
61
+
62
+ - `Fixed` `tb deploy` and `tb build` now reject pipes that declare a parameter without a valid name (e.g. a numeric-literal cast like ``{{Float32(1682892000.0)}}``), instead of creating an unusable parameter.
63
+ - `Added` ``tb datasource append`` and ``tb datasource replace`` support the experimental ``--experimental=use_v1`` option for local CSV, NDJSON, and Parquet files. It submits the import through the v1 endpoint and prints the queued job ID; pass ``--wait`` to wait for completion.
64
+ - `Fixed` Forward builds branch with vendored files without incorrectly detecting drift.
54
65
 
55
66
  4.6.12
56
67
  ********
@@ -69,6 +80,7 @@ Changelog
69
80
 
70
81
  - `Removed` `tinybird.bi_stats_rt` from `tb logs` sources. The BI Connector `bi_stats` / `bi_stats_rt` service datasources are deprecated.
71
82
 
83
+
72
84
  4.6.9
73
85
  *******
74
86
 
@@ -77,6 +89,7 @@ Changelog
77
89
  4.6.8
78
90
  *******
79
91
 
92
+ - `Changed` Upgraded Tornado to 6.5.5.
80
93
  - `Fixed` Tinybird Local CLI commands to respect Git branch local workspaces correctly.
81
94
 
82
95
  4.6.7
@@ -411,7 +411,7 @@ class TinyB:
411
411
  sql_condition: Optional[str] = None,
412
412
  format: str = "csv",
413
413
  replace_options: Optional[Set[str]] = None,
414
- ):
414
+ ) -> Dict[str, Any]:
415
415
  params = {"name": table_name, "url": url, "mode": mode, "debug": "blocks_block_log", "format": format}
416
416
 
417
417
  if sql_condition:
@@ -437,35 +437,38 @@ class TinyB:
437
437
  datasource_name: str,
438
438
  file: Union[str, Path],
439
439
  mode: str = "append",
440
- status_callback=None,
440
+ status_callback=None, # noqa: ARG002 - kept for v0 caller compatibility
441
441
  sql_condition: Optional[str] = None,
442
442
  format: str = "csv",
443
443
  replace_options: Optional[Set[str]] = None,
444
444
  ):
445
- params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
446
-
445
+ # V1 returns the import job immediately, so waiting and progress reporting
446
+ # belong to the caller.
447
+ async with aiofiles.open(file, "rb") as content:
448
+ file_content = await content.read()
449
+ v1_params = {"format": format}
447
450
  if sql_condition:
448
- params["replace_condition"] = sql_condition
451
+ v1_params["replace_condition"] = sql_condition
449
452
  if replace_options:
450
453
  for option in list(replace_options):
451
- params[option] = "true"
452
-
453
- async with aiofiles.open(file, "rb") as content:
454
- file_content = await content.read()
455
- if format == "csv":
456
- files = {"csv": ("csv", file_content)}
457
- else:
458
- files = {"ndjson": ("ndjson", file_content)}
459
-
460
- res = await self._req(
461
- f"v0/datasources?{urlencode(params, safe='')}",
462
- files=files,
463
- method="POST",
464
- )
465
- if status_callback:
466
- status_callback(res)
467
-
468
- return res
454
+ v1_params[option] = "true"
455
+ content_types = {
456
+ "csv": "text/csv",
457
+ "ndjson": "application/x-ndjson",
458
+ "parquet": "application/vnd.apache.parquet",
459
+ }
460
+ headers = {"Content-Type": content_types[format]}
461
+ if str(file).endswith(".gz"):
462
+ headers["Content-Encoding"] = "gzip"
463
+ res = await self._req(
464
+ f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
465
+ data=file_content,
466
+ headers=headers,
467
+ method="POST",
468
+ )
469
+ if not isinstance(res, dict):
470
+ raise RuntimeError("We couldn't confirm that your import started. Please try again.")
471
+ return res
469
472
 
470
473
  async def datasource_truncate(self, datasource_name: str):
471
474
  return await self._req(f"/v0/datasources/{datasource_name}/truncate", method="POST", data="")
@@ -2091,6 +2091,7 @@ def parse(
2091
2091
  "sql": sql("sql"),
2092
2092
  "version": version,
2093
2093
  "deployment_method": assign_var("deployment_method", allowed_values={"alter"}),
2094
+ "export_service": export_service, # Deprecated
2094
2095
  "export_connection_name": assign_var("export_connection_name"),
2095
2096
  "export_schedule": assign_var("export_schedule"),
2096
2097
  "export_bucket_uri": assign_var("export_bucket_uri"),
@@ -11,7 +11,11 @@ from tinybird.datafile.common import (
11
11
  parse,
12
12
  )
13
13
  from tinybird.datafile.exceptions import IncludeFileNotFoundException, ParseException
14
- from tinybird.sql_template import get_template_and_variables, render_sql_template
14
+ from tinybird.sql_template import (
15
+ get_template_and_variables,
16
+ render_sql_template,
17
+ validate_template_parameter_names,
18
+ )
15
19
  from tinybird.tb.modules.feedback_manager import FeedbackManager
16
20
  from tinybird.tornado_template import UnClosedIfError
17
21
 
@@ -52,20 +56,38 @@ def parse_pipe(
52
56
  if "type" in node:
53
57
  node["type"] = node["type"].lower()
54
58
  sql = node.get("sql", "")
55
- if sql.strip()[0] == "%":
59
+ source_sql = sql.strip()
60
+ if not source_sql:
61
+ raise click.ClickException(
62
+ FeedbackManager.error_parsing_node(node=node["name"], pipe=filename, error="Empty query")
63
+ )
64
+ if source_sql.startswith("%"):
56
65
  secrets_list: Optional[List[str]] = None
57
66
  if secrets:
58
67
  secrets_list = list(secrets.keys())
68
+ # Reject parameters declared without a usable name (e.g. a numeric
69
+ # literal cast like `{{Float32(1682892000.0)}}`) before rendering.
70
+ validate_template_parameter_names(sql[1:], node_id=node["name"])
59
71
  # Setting test_mode=True to ignore errors on required parameters and
60
72
  # secrets_in_test_mode=False to raise errors on missing secrets
61
73
  sql, _, variable_warnings = render_sql_template(
62
- sql[1:],
74
+ source_sql[1:],
63
75
  name=node["name"],
64
76
  secrets=secrets_list,
65
77
  test_mode=True,
66
78
  secrets_in_test_mode=ignore_secrets,
67
79
  )
68
80
  doc.warnings = variable_warnings
81
+ if not sql.strip():
82
+ raise click.ClickException(
83
+ FeedbackManager.error_parsing_node(
84
+ node=node["name"],
85
+ pipe=filename,
86
+ error="Template renders to an empty query without parameters",
87
+ )
88
+ )
89
+ else:
90
+ sql = source_sql
69
91
  # it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
70
92
  from tinybird.sql_toolset import format_sql as toolset_format_sql
71
93
 
@@ -79,13 +101,15 @@ def parse_pipe(
79
101
  )
80
102
  )
81
103
  except ValueError as e:
82
- t, template_variables, _ = get_template_and_variables(sql, name=node["name"])
104
+ source_sql = node.get("sql", "")
105
+ source_sql_stripped = source_sql.strip()
106
+ t, template_variables, _ = get_template_and_variables(source_sql, name=node["name"])
83
107
 
84
- if sql.strip()[0] != "%" and len(template_variables) > 0:
108
+ if not source_sql_stripped.startswith("%") and len(template_variables) > 0:
85
109
  raise click.ClickException(FeedbackManager.error_template_start(filename=filename))
86
110
  raise click.ClickException(
87
111
  FeedbackManager.error_parsing_file(
88
- filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {sql}"
112
+ filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {source_sql}"
89
113
  )
90
114
  )
91
115
  except UnClosedIfError as e:
@@ -242,10 +242,11 @@ class FeedbackManager:
242
242
  error_organization_index = error_message(
243
243
  "Error selecting organization '{organization_index}'. Select a valid index or 0 to cancel"
244
244
  )
245
- warning_none_organization = warning_message(
246
- "Tinybird is now based on organizations. Please, go to the UI ({ui_host}) to follow the migration process. \nYour workspace will be created any way."
247
- )
248
245
  error_while_fetching_orgs = error_message("Error while fetching organizations: {error}")
246
+ error_classic_workspace_creation_deprecated = error_message(
247
+ "The Classic experience is deprecated. New workspaces should be created in Forward. "
248
+ "Migration guide: https://www.tinybird.co/docs/forward/guides/migrate-from-classic"
249
+ )
249
250
  error_deleted_include = error_message(
250
251
  "Related include file {include_file} was deleted and it's used in {filename}. Delete or remove dependency from {filename}."
251
252
  )
@@ -864,6 +865,9 @@ class FeedbackManager:
864
865
  success_total_rows = success_message("** Total rows in {datasource}: {total_rows}")
865
866
  success_appended_datasource = success_message("** Data appended to Data Source '{datasource}' successfully!")
866
867
  success_replaced_datasource = success_message("** Data replaced in Data Source '{datasource}' successfully!")
868
+ success_import_job_queued = success_message("** {operation} import job queued: {job_id}")
869
+ success_import_job_completed = success_message("** {operation} import completed: {job_id}")
870
+ info_import_job_status = info_message("Check status: tb job details {job_id}")
867
871
  success_auth = success_message(
868
872
  "** Auth successful! \n** Configuration written to .tinyb file, consider adding it to .gitignore"
869
873
  )
@@ -19,12 +19,12 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
19
19
  return [
20
20
  {
21
21
  "name": "tinybird.pipe_stats_rt",
22
- "description": "Contains information about all requests made to your API endpoints in real time. This data source has a TTL of 7 days. If you need to query data older than 7 days you must use the aggregated by day data available at tinybird.pipe_stats.",
22
+ "description": "Contains information about all requests made to your API endpoints in real time. This data source has a TTL of 3 months. If you need to query data older than 3 months you must use the aggregated by day data available at tinybird.pipe_stats.",
23
23
  "dateColumn": "start_datetime",
24
24
  "engine": {
25
25
  "engine": "MergeTree",
26
26
  "sorting_key": "pipe_id, start_datetime",
27
- "ttl": "start_datetime + toIntervalDay(7)",
27
+ "ttl": "start_datetime + toIntervalMonth(3)",
28
28
  },
29
29
  "columns": [
30
30
  {"name": "start_datetime", "type": "DateTime"},
@@ -659,7 +659,7 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
659
659
  "engine": {
660
660
  "engine": "MergeTree",
661
661
  "sorting_key": "workspace_id, pipe_id, start_datetime",
662
- "ttl": "start_datetime + toIntervalDay(7)",
662
+ "ttl": "start_datetime + toIntervalMonth(3)",
663
663
  },
664
664
  "columns": [
665
665
  {"name": "workspace_id", "type": "String"},
@@ -1396,7 +1396,7 @@ _namespace = {
1396
1396
  }
1397
1397
 
1398
1398
 
1399
- reserved_vars = set(["_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *list(vars(builtins))])
1399
+ reserved_vars = {"_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *list(vars(builtins))}
1400
1400
  for p in DEFAULT_PARAM_NAMES: # we handle these in an specific manner
1401
1401
  reserved_vars.discard(p) # `format` is part of builtins
1402
1402
  # Allow 'id' to be used as a template parameter - https://gitlab.com/tinybird/analytics/-/issues/19119
@@ -2263,7 +2263,12 @@ def get_var_names_and_types(t: Template, node_id: Optional[str] = None) -> List[
2263
2263
  vars_out.extend(
2264
2264
  parse_statement_code_if_new_vars(statement_code, skip_names=typed_names | statement_expr_names)
2265
2265
  )
2266
- return vars_out
2266
+ # Inside a {% %} statement a type function with a literal first arg
2267
+ # (e.g. {% set x = Float32(1.5) %}) is a plain value cast: the
2268
+ # assigned variable is the parameter, the literal is not. Drop
2269
+ # these artifacts so they don't surface as bogus params nor get
2270
+ # rejected by validate_template_parameter_names.
2271
+ return [vd for vd in vars_out if isinstance(vd["name"], str) and vd["name"]]
2267
2272
 
2268
2273
  def _n(chunks: list, vars_out: list[dict[str, Any]]) -> None:
2269
2274
  for x in chunks:
@@ -2310,6 +2315,43 @@ def get_var_names_and_types_cached(t: Template):
2310
2315
  return get_var_names_and_types(t)
2311
2316
 
2312
2317
 
2318
+ def validate_template_parameter_names(sql: str, node_id: Optional[str] = None) -> None:
2319
+ """Reject template parameters that don't have a usable name.
2320
+
2321
+ Type functions such as ``{{Float32(x)}}`` double as casts, so a literal
2322
+ first argument (e.g. ``{{Float32(1682892000.0)}}`` or ``{{String('')}}``)
2323
+ is parsed as a parameter whose name is a number or an empty string instead
2324
+ of an identifier. Such a "parameter" can never be supplied by a caller and
2325
+ breaks parameter metadata consumers, so it must be rejected at write time.
2326
+
2327
+ Raises ``ValueError`` with the same wording used elsewhere for invalid names.
2328
+
2329
+ Templates that fail to parse are left untouched: there are no parameters to
2330
+ validate, and reporting/deferring those errors is the job of the existing
2331
+ render/validation flow (some paths, e.g. ``ignore_sql_errors``, tolerate
2332
+ unparseable SQL on purpose). This keeps the check strictly additive. The
2333
+ only exception is the invalid-name ``ValueError`` that extraction itself
2334
+ raises (e.g. for a list literal used as first argument), which is exactly
2335
+ what this validator exists to report.
2336
+ """
2337
+ try:
2338
+ params = get_var_names_and_types(Template(sql), node_id=node_id)
2339
+ except SQLTemplateException:
2340
+ # Template-level problem (e.g. a SecurityException converted by
2341
+ # get_var_names_and_types), not an invalid parameter name.
2342
+ return
2343
+ except ValueError:
2344
+ # Extraction itself rejects some invalid names (e.g. a list literal
2345
+ # as first argument) with the same ValueError this function raises.
2346
+ raise
2347
+ except Exception:
2348
+ return
2349
+ for param in params:
2350
+ name = param.get("name")
2351
+ if not isinstance(name, str) or not name:
2352
+ raise ValueError(f'"{name}" can not be used as a variable name')
2353
+
2354
+
2313
2355
  def wrap_vars(t, escape_arrays: bool = False):
2314
2356
  def _n(chunks, v):
2315
2357
  for x in chunks:
@@ -2851,6 +2893,13 @@ def render_sql_template(
2851
2893
  documentation="/cli/advanced-templates.html",
2852
2894
  )
2853
2895
  raise SQLTemplateException(str(e), documentation="/cli/advanced-templates.html")
2896
+ except KeyError as e:
2897
+ key = e.args[0] if e.args else "unknown"
2898
+ raise SQLTemplateException(
2899
+ f"Missing key '{key}' while rendering the template. "
2900
+ "Ensure every object accessed in the template has this key.",
2901
+ documentation="/cli/advanced-templates.html",
2902
+ )
2854
2903
  except IndexError as e:
2855
2904
  # This happens when trying to access string indices on empty strings
2856
2905
  if "string index out of range" in str(e):
@@ -643,7 +643,7 @@ def replace_tables(
643
643
 
644
644
  if current_replacements:
645
645
  # We need to transform the dictionary into something cacheable, so a sorted tuple of tuples it is
646
- r = tuple(sorted([(k, v) for k, v in current_replacements.items()]))
646
+ r = tuple(sorted(current_replacements.items()))
647
647
  sql = replace_tables_chquery_cached(
648
648
  sql,
649
649
  r,
@@ -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.13.dev0'
8
- __revision__ = '9f77ad2'
7
+ __version__ = '4.6.14'
8
+ __revision__ = '532ee82'
@@ -435,7 +435,7 @@ class TinyB:
435
435
  sql_condition: Optional[str] = None,
436
436
  format: str = "csv",
437
437
  replace_options: Optional[Set[str]] = None,
438
- ):
438
+ ) -> Dict[str, Any]:
439
439
  params = {"name": table_name, "url": url, "mode": mode, "debug": "blocks_block_log", "format": format}
440
440
 
441
441
  if sql_condition:
@@ -466,23 +466,46 @@ class TinyB:
466
466
  format: str = "csv",
467
467
  replace_options: Optional[Set[str]] = None,
468
468
  ):
469
- params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
470
-
469
+ # Keep this argument for SDK compatibility; v1 returns before import completion.
470
+ del status_callback
471
+ with open(file, "rb") as content:
472
+ file_content = content.read()
473
+ content_types = {
474
+ "csv": "text/csv",
475
+ "ndjson": "application/x-ndjson",
476
+ "parquet": "application/vnd.apache.parquet",
477
+ }
478
+ headers = {"Content-Type": content_types[format]}
479
+ if str(file).endswith(".gz"):
480
+ headers["Content-Encoding"] = "gzip"
481
+ v1_params = {"format": format}
471
482
  if sql_condition:
472
- params["replace_condition"] = sql_condition
483
+ v1_params["replace_condition"] = sql_condition
473
484
  if replace_options:
474
485
  for option in list(replace_options):
475
- params[option] = "true"
486
+ v1_params[option] = "true"
487
+ res = self._req(
488
+ f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
489
+ data=file_content,
490
+ headers=headers,
491
+ method="POST",
492
+ )
493
+ if not isinstance(res, dict):
494
+ raise RuntimeError("We couldn't confirm that your import started. Please try again.")
495
+ if "error" in res:
496
+ raise Exception(res["error"])
497
+ return res
476
498
 
477
- with open(file, "rb") as content:
478
- file_content = content.read()
479
- if format == "csv":
480
- files = {"csv": ("csv", file_content)}
481
- else:
482
- files = {"ndjson": ("ndjson", file_content)}
483
- res = self._req(f"v0/datasources?{urlencode(params, safe='')}", files=files, method="POST")
484
- if status_callback:
485
- status_callback(res)
499
+ def datasource_append_events(self, datasource_name: str, events: str) -> dict:
500
+ """Submit NDJSON event data through the asynchronous v1 append route."""
501
+ res = self._req(
502
+ f"/v1/datasources/{quote(datasource_name, safe='')}/append?format=ndjson",
503
+ data=events.encode(),
504
+ headers={"Content-Type": "application/x-ndjson"},
505
+ method="POST",
506
+ )
507
+ if "error" in res:
508
+ raise Exception(res["error"])
486
509
  return res
487
510
 
488
511
  def datasource_truncate(self, datasource_name: str):
@@ -300,7 +300,8 @@ def build_project_with_deploy_api(
300
300
  if build_result == "no_changes":
301
301
  return False
302
302
  if build_result != "success":
303
- deployment_errors = deployment.get("errors", []) if deployment else result.get("errors", [])
303
+ deployment_errors = list(deployment.get("errors", []) if deployment else result.get("errors", []))
304
+ deployment_errors.extend(item for item in deployment.get("feedback", []) if item.get("level") == "ERROR")
304
305
  raise click.ClickException(result.get("error") or format_build_errors(deployment_errors))
305
306
  if not deployment:
306
307
  raise click.ClickException("Couldn't parse deployment response from server")
@@ -328,6 +329,7 @@ def get_build_request_files(
328
329
  project_with_vendors: Optional[Project] = None,
329
330
  ) -> tuple[list[tuple[str, tuple[str, str, str]]], list[str]]:
330
331
  multipart_boundary_data_project = "data_project://"
332
+ multipart_boundary_data_project_vendored = "data_project_vendored://"
331
333
  datafile_type_to_content_type = {
332
334
  ".datasource": "text/plain",
333
335
  ".pipe": "text/plain",
@@ -350,6 +352,13 @@ def get_build_request_files(
350
352
 
351
353
  files.append((multipart_boundary_data_project, (relative_path, content, content_type)))
352
354
 
355
+ for file_path in project.get_vendored_files():
356
+ relative_path = Path(file_path).relative_to(project_path).as_posix()
357
+ with open(file_path, "rb") as fd:
358
+ content_type = datafile_type_to_content_type.get(Path(file_path).suffix, "application/unknown")
359
+ content = fd.read().decode("utf-8")
360
+ files.append((multipart_boundary_data_project_vendored, (relative_path, content, content_type)))
361
+
353
362
  return files, project_files
354
363
 
355
364
 
@@ -925,7 +925,8 @@ def push_data(
925
925
  replace_options=None,
926
926
  concurrency: int = 1,
927
927
  silent: bool = False,
928
- ):
928
+ wait: bool = True,
929
+ ) -> Optional[list[str]]:
929
930
  if url and type(url) is tuple:
930
931
  url = url[0]
931
932
 
@@ -962,9 +963,11 @@ def push_data(
962
963
  datasource_name: str, url: str, mode: str, sql_condition: Optional[str], replace_options: Optional[Set[str]]
963
964
  ):
964
965
  parsed = urlparse(url)
966
+ # Remote URL imports retain the v0 API; local files use v1 body imports.
967
+ is_remote_url = parsed.scheme in ("http", "https")
965
968
  # poor man's format detection
966
969
  _format = get_format_from_filename_or_url(url)
967
- if parsed.scheme in ("http", "https"):
970
+ if is_remote_url:
968
971
  res = client.datasource_create_from_url(
969
972
  datasource_name,
970
973
  url,
@@ -984,6 +987,12 @@ def push_data(
984
987
  replace_options=replace_options,
985
988
  )
986
989
 
990
+ if not is_remote_url:
991
+ job_id = res.get("id") or res.get("import_id")
992
+ if not isinstance(job_id, str):
993
+ raise CLIException("We couldn't confirm that your import started. Please try again.")
994
+ return job_id
995
+
987
996
  datasource_name = res["datasource"]["name"]
988
997
  try:
989
998
  datasource = client.get_datasource(datasource_name)
@@ -1014,6 +1023,17 @@ def push_data(
1014
1023
  try:
1015
1024
  tasks = [process_url(datasource_name, url, mode, sql_condition, replace_options) for url in urls]
1016
1025
  output = gather_with_concurrency(concurrency, *tasks)
1026
+ v1_job_ids = [result for result in output if isinstance(result, str)]
1027
+ if v1_job_ids:
1028
+ if not wait:
1029
+ return v1_job_ids
1030
+ for job_id in v1_job_ids:
1031
+ wait_job_no_ui(client, job_id)
1032
+ if not silent:
1033
+ if mode == "replace":
1034
+ click.echo(FeedbackManager.success_replaced_datasource(datasource=datasource_name))
1035
+ click.echo(FeedbackManager.success_progress_blocks())
1036
+ return None
1017
1037
  parser, total_rows, appended_rows = list(output)[-1]
1018
1038
  except AuthNoTokenException:
1019
1039
  raise
@@ -1031,6 +1051,8 @@ def push_data(
1031
1051
 
1032
1052
  click.echo(FeedbackManager.success_progress_blocks())
1033
1053
 
1054
+ return None
1055
+
1034
1056
 
1035
1057
  def sync_data(ctx, datasource_name: str, yes: bool):
1036
1058
  client: TinyB = ctx.obj["client"]
@@ -2366,7 +2388,7 @@ def force_echo(string: str) -> None:
2366
2388
  click.echo(string, force_output=True) # type: ignore
2367
2389
 
2368
2390
 
2369
- def echo_json(data: Dict[str, Any], indent: Union[None, int, str] = None) -> None:
2391
+ def echo_json(data: Dict[str, Any], indent: Union[int, str, None] = None) -> None:
2370
2392
  force_echo(json.dumps(data, indent=indent))
2371
2393
 
2372
2394
 
@@ -85,7 +85,7 @@ class CLIConfig:
85
85
  def to_dict(self) -> Dict[str, Any]:
86
86
  """Helper to ease"""
87
87
  result: Dict[str, Any] = {}
88
- result.update(dict((v.name, deepcopy(v.value)) for v in self._values.values()))
88
+ result.update({v.name: deepcopy(v.value) for v in self._values.values()})
89
89
  return result
90
90
 
91
91
  def __getitem__(self, key) -> Any:
@@ -951,7 +951,7 @@ def process_file(
951
951
  "filename": filename,
952
952
  "name": name + version,
953
953
  "nodes": nodes,
954
- "deps": [x for x in set(deps)],
954
+ "deps": list(set(deps)),
955
955
  "tokens": doc.tokens,
956
956
  "description": description,
957
957
  "warnings": doc.warnings,
@@ -162,12 +162,12 @@ class PipeChecker(unittest.TestCase):
162
162
 
163
163
  if self.ignore_order:
164
164
  current_data = (
165
- sorted(normalize_array(current_data), key=itemgetter(*[k for k in current_data[0].keys()]))
165
+ sorted(normalize_array(current_data), key=itemgetter(*list(current_data[0].keys())))
166
166
  if len(current_data) > 0
167
167
  else current_data
168
168
  )
169
169
  checker_data = (
170
- sorted(normalize_array(checker_data), key=itemgetter(*[k for k in checker_data[0].keys()]))
170
+ sorted(normalize_array(checker_data), key=itemgetter(*list(checker_data[0].keys())))
171
171
  if len(checker_data) > 0
172
172
  else checker_data
173
173
  )
@@ -282,6 +282,8 @@ class PipeCheckerRunner:
282
282
  FROM {pipe_stats_rt}
283
283
  WHERE
284
284
  pipe_name = '{self.pipe_name}'
285
+ -- pipe_stats_rt retains 3 months; recent requests are enough for checks
286
+ AND start_datetime >= now() - INTERVAL 7 DAY
285
287
  AND url IS NOT NULL
286
288
  AND extractURLParameter(assumeNotNull(url), 'from') <> 'ui'
287
289
  AND extractURLParameter(assumeNotNull(url), 'pipe_checker') <> 'true'
@@ -309,6 +311,8 @@ class PipeCheckerRunner:
309
311
  FROM {pipe_stats_rt}
310
312
  WHERE
311
313
  pipe_name = '{self.pipe_name}'
314
+ -- pipe_stats_rt retains 3 months; recent requests are enough for checks
315
+ AND start_datetime >= now() - INTERVAL 7 DAY
312
316
  AND url IS NOT NULL
313
317
  AND extractURLParameter(assumeNotNull(url), 'from') <> 'ui'
314
318
  AND extractURLParameter(assumeNotNull(url), 'pipe_checker') <> 'true'
@@ -992,7 +992,7 @@ def process_file(
992
992
  "filename": filename,
993
993
  "name": name + version,
994
994
  "nodes": nodes,
995
- "deps": [x for x in set(deps)],
995
+ "deps": list(set(deps)),
996
996
  "tokens": doc.tokens,
997
997
  "description": description,
998
998
  "warnings": doc.warnings,
@@ -53,7 +53,7 @@ def folder_pull(
53
53
  if not candidates:
54
54
  return None
55
55
 
56
- return sorted(candidates, key=lambda path: (len(path.parent.relative_to(dest_folder).parts), str(path)))[0]
56
+ return min(candidates, key=lambda path: (len(path.parent.relative_to(dest_folder).parts), str(path)))
57
57
 
58
58
  def write_files(
59
59
  resources: list[dict[str, Any]],