tinybird 4.6.13.dev0__tar.gz → 4.6.15.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.13.dev0 → tinybird-4.6.15.dev0}/PKG-INFO +6 -1
  2. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/client.py +34 -10
  3. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/common.py +1 -0
  4. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_pipe.py +22 -5
  5. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/feedback_manager.py +3 -0
  6. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_template.py +1 -1
  7. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_toolset.py +1 -1
  8. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/__cli__.py +2 -2
  9. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/client.py +25 -0
  10. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/common.py +15 -1
  11. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/config.py +1 -1
  12. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build.py +1 -1
  13. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/pipe_checker.py +2 -2
  14. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/playground.py +1 -1
  15. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datasource.py +72 -4
  16. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/common.py +15 -1
  17. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/config.py +1 -1
  18. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/datasource.py +68 -4
  19. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tornado_template.py +5 -5
  20. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/PKG-INFO +6 -1
  21. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/setup.cfg +0 -0
  22. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/__cli__.py +0 -0
  23. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/ch_utils/constants.py +0 -0
  24. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/ch_utils/engine.py +0 -0
  25. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/check_pypi.py +0 -0
  26. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/config.py +0 -0
  27. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/context.py +0 -0
  28. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/exceptions.py +0 -0
  29. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_connection.py +0 -0
  30. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datafile/parse_datasource.py +0 -0
  31. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/datatypes.py +0 -0
  32. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/git_settings.py +0 -0
  33. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/iterating/__init__.py +0 -0
  34. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/iterating/data_branch_modes.py +0 -0
  35. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/prompts.py +0 -0
  36. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/service_datasources.py +0 -0
  37. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/sql.py +0 -0
  38. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/sql_template_fmt.py +0 -0
  39. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/syncasync.py +0 -0
  40. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/check_pypi.py +0 -0
  41. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/cli.py +0 -0
  42. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/config.py +0 -0
  43. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/branch.py +0 -0
  44. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/build.py +0 -0
  45. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/build_common.py +0 -0
  46. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/cicd.py +0 -0
  47. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/cli.py +0 -0
  48. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection.py +0 -0
  49. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_dynamodb.py +0 -0
  50. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_kafka.py +0 -0
  51. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/connection_s3.py +0 -0
  52. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/copy.py +0 -0
  53. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/create.py +0 -0
  54. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_common.py +0 -0
  55. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  56. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  57. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/diff.py +0 -0
  58. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/fixture.py +0 -0
  59. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_common.py +0 -0
  60. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  61. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  62. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  63. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/datafile/pull.py +0 -0
  64. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deployment.py +0 -0
  65. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deployment_common.py +0 -0
  66. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/deprecations.py +0 -0
  67. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/endpoint.py +0 -0
  68. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/exceptions.py +0 -0
  69. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/feedback_manager.py +0 -0
  70. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/fmt.py +0 -0
  71. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/info.py +0 -0
  72. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/infra.py +0 -0
  73. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/job.py +0 -0
  74. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/job_common.py +0 -0
  75. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/llm.py +0 -0
  76. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/llm_utils.py +0 -0
  77. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local.py +0 -0
  78. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local_common.py +0 -0
  79. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/local_logs.py +0 -0
  80. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/login.py +0 -0
  81. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/login_common.py +0 -0
  82. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/logout.py +0 -0
  83. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/logs.py +0 -0
  84. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/materialization.py +0 -0
  85. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/open.py +0 -0
  86. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/pipe.py +0 -0
  87. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/preview.py +0 -0
  88. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/project.py +0 -0
  89. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/project_commands.py +0 -0
  90. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/py_project.py +0 -0
  91. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/query_output.py +0 -0
  92. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/regions.py +0 -0
  93. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/secret.py +0 -0
  94. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/secret_common.py +0 -0
  95. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/sink.py +0 -0
  96. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/table.py +0 -0
  97. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/telemetry.py +0 -0
  98. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/test.py +0 -0
  99. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/test_common.py +0 -0
  100. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  101. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/token.py +0 -0
  102. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/ts_project.py +0 -0
  103. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/watch.py +0 -0
  104. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/workspace.py +0 -0
  105. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb/modules/workspace_members.py +0 -0
  106. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli.py +0 -0
  107. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  108. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  109. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  110. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  111. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  112. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  113. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  114. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  115. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  116. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  117. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  118. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  119. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  120. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  121. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/workspace.py +0 -0
  122. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  123. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/SOURCES.txt +0 -0
  124. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/dependency_links.txt +0 -0
  125. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/entry_points.txt +0 -0
  126. {tinybird-4.6.13.dev0 → tinybird-4.6.15.dev0}/tinybird.egg-info/requires.txt +0 -0
  127. {tinybird-4.6.13.dev0 → tinybird-4.6.15.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.13.dev0
3
+ Version: 4.6.15.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,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.13
56
+ ********
57
+
58
+ - `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.
59
+
55
60
  4.6.12
56
61
  ********
57
62
 
@@ -441,6 +441,7 @@ class TinyB:
441
441
  sql_condition: Optional[str] = None,
442
442
  format: str = "csv",
443
443
  replace_options: Optional[Set[str]] = None,
444
+ use_v1: bool = False,
444
445
  ):
445
446
  params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
446
447
 
@@ -452,20 +453,43 @@ class TinyB:
452
453
 
453
454
  async with aiofiles.open(file, "rb") as content:
454
455
  file_content = await content.read()
455
- if format == "csv":
456
- files = {"csv": ("csv", file_content)}
457
- else:
458
- files = {"ndjson": ("ndjson", file_content)}
459
456
 
460
- res = await self._req(
461
- f"v0/datasources?{urlencode(params, safe='')}",
462
- files=files,
457
+ if use_v1:
458
+ v1_params = {"format": format}
459
+ if sql_condition:
460
+ v1_params["replace_condition"] = sql_condition
461
+ if replace_options:
462
+ for option in list(replace_options):
463
+ v1_params[option] = "true"
464
+ content_types = {
465
+ "csv": "text/csv",
466
+ "ndjson": "application/x-ndjson",
467
+ "parquet": "application/vnd.apache.parquet",
468
+ }
469
+ headers = {"Content-Type": content_types[format]}
470
+ if str(file).endswith(".gz"):
471
+ headers["Content-Encoding"] = "gzip"
472
+ return await self._req(
473
+ f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
474
+ data=file_content,
475
+ headers=headers,
463
476
  method="POST",
464
477
  )
465
- if status_callback:
466
- status_callback(res)
467
478
 
468
- return res
479
+ if format == "csv":
480
+ files = {"csv": ("csv", file_content)}
481
+ else:
482
+ files = {"ndjson": ("ndjson", file_content)}
483
+
484
+ res = await self._req(
485
+ f"v0/datasources?{urlencode(params, safe='')}",
486
+ files=files,
487
+ method="POST",
488
+ )
489
+ if status_callback:
490
+ status_callback(res)
491
+
492
+ return res
469
493
 
470
494
  async def datasource_truncate(self, datasource_name: str):
471
495
  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"),
@@ -52,20 +52,35 @@ def parse_pipe(
52
52
  if "type" in node:
53
53
  node["type"] = node["type"].lower()
54
54
  sql = node.get("sql", "")
55
- if sql.strip()[0] == "%":
55
+ source_sql = sql.strip()
56
+ if not source_sql:
57
+ raise click.ClickException(
58
+ FeedbackManager.error_parsing_node(node=node["name"], pipe=filename, error="Empty query")
59
+ )
60
+ if source_sql.startswith("%"):
56
61
  secrets_list: Optional[List[str]] = None
57
62
  if secrets:
58
63
  secrets_list = list(secrets.keys())
59
64
  # Setting test_mode=True to ignore errors on required parameters and
60
65
  # secrets_in_test_mode=False to raise errors on missing secrets
61
66
  sql, _, variable_warnings = render_sql_template(
62
- sql[1:],
67
+ source_sql[1:],
63
68
  name=node["name"],
64
69
  secrets=secrets_list,
65
70
  test_mode=True,
66
71
  secrets_in_test_mode=ignore_secrets,
67
72
  )
68
73
  doc.warnings = variable_warnings
74
+ if not sql.strip():
75
+ raise click.ClickException(
76
+ FeedbackManager.error_parsing_node(
77
+ node=node["name"],
78
+ pipe=filename,
79
+ error="Template renders to an empty query without parameters",
80
+ )
81
+ )
82
+ else:
83
+ sql = source_sql
69
84
  # it'll fail with a ModuleNotFoundError when the toolset is not available but it returns the parsed doc
70
85
  from tinybird.sql_toolset import format_sql as toolset_format_sql
71
86
 
@@ -79,13 +94,15 @@ def parse_pipe(
79
94
  )
80
95
  )
81
96
  except ValueError as e:
82
- t, template_variables, _ = get_template_and_variables(sql, name=node["name"])
97
+ source_sql = node.get("sql", "")
98
+ source_sql_stripped = source_sql.strip()
99
+ t, template_variables, _ = get_template_and_variables(source_sql, name=node["name"])
83
100
 
84
- if sql.strip()[0] != "%" and len(template_variables) > 0:
101
+ if not source_sql_stripped.startswith("%") and len(template_variables) > 0:
85
102
  raise click.ClickException(FeedbackManager.error_template_start(filename=filename))
86
103
  raise click.ClickException(
87
104
  FeedbackManager.error_parsing_file(
88
- filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {sql}"
105
+ filename=filename, lineno="", error=f"{str(e)} + SQL(value error): {source_sql}"
89
106
  )
90
107
  )
91
108
  except UnClosedIfError as e:
@@ -864,6 +864,9 @@ class FeedbackManager:
864
864
  success_total_rows = success_message("** Total rows in {datasource}: {total_rows}")
865
865
  success_appended_datasource = success_message("** Data appended to Data Source '{datasource}' successfully!")
866
866
  success_replaced_datasource = success_message("** Data replaced in Data Source '{datasource}' successfully!")
867
+ success_import_job_queued = success_message("** {operation} import job queued: {job_id}")
868
+ success_import_job_completed = success_message("** {operation} import completed: {job_id}")
869
+ info_import_job_status = info_message("Check status: tb job details {job_id}")
867
870
  success_auth = success_message(
868
871
  "** Auth successful! \n** Configuration written to .tinyb file, consider adding it to .gitignore"
869
872
  )
@@ -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
@@ -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.15.dev0'
8
+ __revision__ = '8311ae9'
@@ -465,6 +465,7 @@ class TinyB:
465
465
  sql_condition: Optional[str] = None,
466
466
  format: str = "csv",
467
467
  replace_options: Optional[Set[str]] = None,
468
+ use_v1: bool = False,
468
469
  ):
469
470
  params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
470
471
 
@@ -476,6 +477,30 @@ class TinyB:
476
477
 
477
478
  with open(file, "rb") as content:
478
479
  file_content = content.read()
480
+ content_types = {
481
+ "csv": "text/csv",
482
+ "ndjson": "application/x-ndjson",
483
+ "parquet": "application/vnd.apache.parquet",
484
+ }
485
+ headers = {"Content-Type": content_types[format]}
486
+ if str(file).endswith(".gz"):
487
+ headers["Content-Encoding"] = "gzip"
488
+ if use_v1:
489
+ v1_params = {"format": format}
490
+ if sql_condition:
491
+ v1_params["replace_condition"] = sql_condition
492
+ if replace_options:
493
+ for option in list(replace_options):
494
+ v1_params[option] = "true"
495
+ res = self._req(
496
+ f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
497
+ data=file_content,
498
+ headers=headers,
499
+ method="POST",
500
+ )
501
+ if "error" in res:
502
+ raise Exception(res["error"])
503
+ return res
479
504
  if format == "csv":
480
505
  files = {"csv": ("csv", file_content)}
481
506
  else:
@@ -925,7 +925,8 @@ def push_data(
925
925
  replace_options=None,
926
926
  concurrency: int = 1,
927
927
  silent: bool = False,
928
- ):
928
+ use_v1: bool = False,
929
+ ) -> Optional[list[str]]:
929
930
  if url and type(url) is tuple:
930
931
  url = url[0]
931
932
 
@@ -962,6 +963,8 @@ 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
+ if use_v1 and parsed.scheme in ("http", "https"):
967
+ raise CLIException("--experimental=use_v1 only supports local files.")
965
968
  # poor man's format detection
966
969
  _format = get_format_from_filename_or_url(url)
967
970
  if parsed.scheme in ("http", "https"):
@@ -982,8 +985,15 @@ def push_data(
982
985
  sql_condition=sql_condition,
983
986
  format=_format,
984
987
  replace_options=replace_options,
988
+ use_v1=use_v1,
985
989
  )
986
990
 
991
+ if use_v1:
992
+ job_id = res.get("id") or res.get("import_id")
993
+ if not isinstance(job_id, str):
994
+ raise CLIException("The v1 import response did not include a job ID.")
995
+ return job_id
996
+
987
997
  datasource_name = res["datasource"]["name"]
988
998
  try:
989
999
  datasource = client.get_datasource(datasource_name)
@@ -1014,6 +1024,8 @@ def push_data(
1014
1024
  try:
1015
1025
  tasks = [process_url(datasource_name, url, mode, sql_condition, replace_options) for url in urls]
1016
1026
  output = gather_with_concurrency(concurrency, *tasks)
1027
+ if use_v1:
1028
+ return list(output)
1017
1029
  parser, total_rows, appended_rows = list(output)[-1]
1018
1030
  except AuthNoTokenException:
1019
1031
  raise
@@ -1031,6 +1043,8 @@ def push_data(
1031
1043
 
1032
1044
  click.echo(FeedbackManager.success_progress_blocks())
1033
1045
 
1046
+ return None
1047
+
1034
1048
 
1035
1049
  def sync_data(ctx, datasource_name: str, yes: bool):
1036
1050
  client: TinyB = ctx.obj["client"]
@@ -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
  )
@@ -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,
@@ -35,6 +35,7 @@ from tinybird.tb.modules.common import (
35
35
  get_format_from_filename_or_url,
36
36
  normalize_datasource_name,
37
37
  push_data,
38
+ wait_job,
38
39
  )
39
40
  from tinybird.tb.modules.config import CLIConfig
40
41
  from tinybird.tb.modules.connection_dynamodb import connection_create_dynamodb, validate_dynamodb_table
@@ -58,7 +59,7 @@ from tinybird.tb.modules.create import (
58
59
  generate_gcs_connection_file_with_secrets,
59
60
  )
60
61
  from tinybird.tb.modules.datafile.fixture import persist_fixture
61
- from tinybird.tb.modules.exceptions import CLIDatasourceException
62
+ from tinybird.tb.modules.exceptions import CLIDatasourceException, CLIException
62
63
  from tinybird.tb.modules.feedback_manager import FeedbackManager, get_cli_name
63
64
  from tinybird.tb.modules.llm import LLM
64
65
  from tinybird.tb.modules.llm_utils import extract_xml
@@ -66,6 +67,36 @@ from tinybird.tb.modules.project import Project
66
67
  from tinybird.tb.modules.secret import save_secret_to_env_file
67
68
  from tinybird.tb.modules.telemetry import add_telemetry_event
68
69
 
70
+ EXPERIMENTAL_FEATURE_USE_V1 = "use_v1"
71
+
72
+
73
+ def _echo_v1_import_jobs_queued(job_ids: list[str], operation: str) -> None:
74
+ for job_id in job_ids:
75
+ click.echo(FeedbackManager.success(message=f"✓ {operation} import job queued: {job_id}"))
76
+ click.echo(FeedbackManager.gray(message=f"Check status: tb job details {job_id}"))
77
+
78
+
79
+ def _wait_for_v1_import_jobs(client: TinyB, job_ids: list[str], operation: str) -> None:
80
+ for job_id in job_ids:
81
+ try:
82
+ wait_job(client, job_id, f"/v0/jobs/{job_id}", f"{operation} import")
83
+ except CLIException:
84
+ if _echo_v1_import_job_details(client, job_id):
85
+ raise click.exceptions.Exit(1)
86
+ raise
87
+ click.echo(FeedbackManager.success(message=f"✓ {operation} import completed: {job_id}"))
88
+
89
+
90
+ def _echo_v1_import_job_details(client: TinyB, job_id: str) -> bool:
91
+ try:
92
+ job = client.job(job_id)
93
+ except Exception:
94
+ return False
95
+ click.echo(FeedbackManager.info_job(job=job_id))
96
+ echo_safe_humanfriendly_tables_format_smart_table([job.values()], column_names=job.keys())
97
+ click.echo("\n")
98
+ return True
99
+
69
100
 
70
101
  def _dynamodb_key_schema_sort_key(key_schema: dict[str, str]) -> int:
71
102
  if key_schema.get("key_type") == "HASH":
@@ -213,7 +244,14 @@ def datasource_ls(ctx: Context, match: Optional[str], format_: str):
213
244
  @click.option("--url", type=str, help="URL to append data from")
214
245
  @click.option("--file", type=str, help="Local file to append data from")
215
246
  @click.option("--events", type=str, help="Events to append data from")
247
+ @click.option(
248
+ "--experimental",
249
+ type=click.Choice([EXPERIMENTAL_FEATURE_USE_V1]),
250
+ multiple=True,
251
+ help="Enable an experimental feature. May be specified multiple times.",
252
+ )
216
253
  @click.option("--concurrency", help="How many files to submit concurrently", default=1, hidden=True)
254
+ @click.option("--wait", is_flag=True, default=False, help="Wait for a v1 import job to finish.")
217
255
  @click.pass_context
218
256
  def datasource_append(
219
257
  ctx: Context,
@@ -222,7 +260,9 @@ def datasource_append(
222
260
  url: str,
223
261
  file: str,
224
262
  events: str,
263
+ experimental: tuple[str, ...],
225
264
  concurrency: int,
265
+ wait: bool,
226
266
  ):
227
267
  """
228
268
  Appends data to an existing data source from URL, local file or a connector
@@ -237,6 +277,9 @@ def datasource_append(
237
277
  env: str = ctx.ensure_object(dict)["env"]
238
278
  client: TinyB = ctx.obj["client"]
239
279
  project: Project = ctx.ensure_object(dict)["project"]
280
+ use_v1 = EXPERIMENTAL_FEATURE_USE_V1 in experimental
281
+ if wait and not use_v1:
282
+ raise CLIDatasourceException("--wait requires --experimental=use_v1.")
240
283
 
241
284
  # If data is passed as argument, we detect if it's a JSON object, a URL or a file
242
285
  if data:
@@ -346,6 +389,8 @@ def datasource_append(
346
389
  raise CLIDatasourceException(FeedbackManager.error(message="Invalid ingestion option"))
347
390
 
348
391
  if events:
392
+ if use_v1:
393
+ raise CLIDatasourceException("--experimental=use_v1 only supports local files.")
349
394
  click.echo(FeedbackManager.highlight(message=f"\n» Sending events to {datasource_name}"))
350
395
  events_params = {"name": datasource_name}
351
396
  request_from = getattr(client, "request_from", None)
@@ -382,13 +427,14 @@ def datasource_append(
382
427
  else:
383
428
  click.echo(FeedbackManager.highlight(message=f"\n» Appending data to {datasource_name}"))
384
429
  try:
385
- push_data(
430
+ job_ids = push_data(
386
431
  client,
387
432
  datasource_name,
388
433
  data,
389
434
  mode="append",
390
435
  concurrency=concurrency,
391
436
  silent=True,
437
+ use_v1=use_v1,
392
438
  )
393
439
  except Exception as e:
394
440
  is_quarantined = "quarantine" in str(e)
@@ -398,7 +444,12 @@ def datasource_append(
398
444
  return
399
445
  else:
400
446
  raise e
401
- click.echo(FeedbackManager.success(message="✓ Rows appended!"))
447
+ if use_v1:
448
+ _echo_v1_import_jobs_queued(job_ids or [], "Append")
449
+ if wait:
450
+ _wait_for_v1_import_jobs(client, job_ids or [], "Append")
451
+ else:
452
+ click.echo(FeedbackManager.success(message="✓ Rows appended!"))
402
453
 
403
454
 
404
455
  @datasource.command(name="replace")
@@ -406,6 +457,13 @@ def datasource_append(
406
457
  @click.argument("url", nargs=-1, required=True)
407
458
  @click.option("--sql-condition", default=None, help="SQL WHERE condition to replace data", hidden=True)
408
459
  @click.option("--skip-incompatible-partition-key", is_flag=True, default=False, hidden=True)
460
+ @click.option("--wait", is_flag=True, default=False, help="Wait for a v1 import job to finish.")
461
+ @click.option(
462
+ "--experimental",
463
+ type=click.Choice([EXPERIMENTAL_FEATURE_USE_V1]),
464
+ multiple=True,
465
+ help="Enable an experimental feature. May be specified multiple times.",
466
+ )
409
467
  @click.pass_context
410
468
  def datasource_replace(
411
469
  ctx: Context,
@@ -413,6 +471,8 @@ def datasource_replace(
413
471
  url,
414
472
  sql_condition,
415
473
  skip_incompatible_partition_key,
474
+ experimental: tuple[str, ...],
475
+ wait: bool,
416
476
  ):
417
477
  """
418
478
  Replaces the data in a data source from a URL, local file or a connector
@@ -426,14 +486,22 @@ def datasource_replace(
426
486
  if skip_incompatible_partition_key:
427
487
  replace_options.add("skip_incompatible_partition_key")
428
488
  client: TinyB = ctx.obj["client"]
429
- push_data(
489
+ use_v1 = EXPERIMENTAL_FEATURE_USE_V1 in experimental
490
+ if wait and not use_v1:
491
+ raise CLIDatasourceException("--wait requires --experimental=use_v1.")
492
+ job_ids = push_data(
430
493
  client,
431
494
  datasource_name,
432
495
  url,
433
496
  mode="replace",
434
497
  sql_condition=sql_condition,
435
498
  replace_options=replace_options,
499
+ use_v1=use_v1,
436
500
  )
501
+ if use_v1:
502
+ _echo_v1_import_jobs_queued(job_ids or [], "Replace")
503
+ if wait:
504
+ _wait_for_v1_import_jobs(client, job_ids or [], "Replace")
437
505
 
438
506
 
439
507
  @datasource.command(name="analyze")
@@ -963,7 +963,8 @@ async def push_data(
963
963
  sql_condition: Optional[str] = None,
964
964
  replace_options=None,
965
965
  concurrency: int = 1,
966
- ):
966
+ use_v1: bool = False,
967
+ ) -> Optional[list[str]]:
967
968
  if url and type(url) is tuple:
968
969
  url = url[0]
969
970
  client: TinyB = ctx.obj["client"]
@@ -1000,6 +1001,8 @@ async def push_data(
1000
1001
  datasource_name: str, url: str, mode: str, sql_condition: Optional[str], replace_options: Optional[Set[str]]
1001
1002
  ):
1002
1003
  parsed = urlparse(url)
1004
+ if use_v1 and parsed.scheme in ("http", "https"):
1005
+ raise CLIException("--experimental=use_v1 only supports local files.")
1003
1006
  # poor man's format detection
1004
1007
  _format = get_format_from_filename_or_url(url)
1005
1008
  if parsed.scheme in ("http", "https"):
@@ -1020,8 +1023,15 @@ async def push_data(
1020
1023
  sql_condition=sql_condition,
1021
1024
  format=_format,
1022
1025
  replace_options=replace_options,
1026
+ use_v1=use_v1,
1023
1027
  )
1024
1028
 
1029
+ if use_v1:
1030
+ job_id = res.get("id") or res.get("import_id")
1031
+ if not isinstance(job_id, str):
1032
+ raise CLIException("The v1 import response did not include a job ID.")
1033
+ return job_id
1034
+
1025
1035
  datasource_name = res["datasource"]["name"]
1026
1036
  try:
1027
1037
  datasource = await client.get_datasource(datasource_name)
@@ -1052,6 +1062,8 @@ async def push_data(
1052
1062
  try:
1053
1063
  tasks = [process_url(datasource_name, url, mode, sql_condition, replace_options) for url in urls]
1054
1064
  output = await gather_with_concurrency(concurrency, *tasks)
1065
+ if use_v1:
1066
+ return list(output)
1055
1067
  parser, total_rows, appended_rows = list(output)[-1]
1056
1068
  except AuthNoTokenException:
1057
1069
  raise
@@ -1072,6 +1084,8 @@ async def push_data(
1072
1084
  click.echo(FeedbackManager.success_appended_datasource(datasource=datasource_name))
1073
1085
  click.echo(FeedbackManager.info_data_pushed(datasource=datasource_name))
1074
1086
 
1087
+ return None
1088
+
1075
1089
 
1076
1090
  async def sync_data(ctx, datasource_name: str, yes: bool):
1077
1091
  client: TinyB = ctx.obj["client"]
@@ -88,7 +88,7 @@ class CLIConfig:
88
88
  def to_dict(self) -> Dict[str, Any]:
89
89
  """Helper to ease"""
90
90
  result: Dict[str, Any] = self._parent.to_dict() if self._parent else {}
91
- result.update(dict((v.name, deepcopy(v.value)) for v in self._values.values()))
91
+ result.update({v.name: deepcopy(v.value) for v in self._values.values()})
92
92
  return result
93
93
 
94
94
  def __getitem__(self, key) -> Any:
@@ -15,7 +15,7 @@ from click import Context
15
15
 
16
16
  from tinybird.client import AuthNoTokenException, CanNotBeDeletedException, DoesNotExistException, TinyB
17
17
  from tinybird.config import get_display_host
18
- from tinybird.datafile_common import get_name_version, wait_job
18
+ from tinybird.datafile_common import get_name_version
19
19
  from tinybird.feedback_manager import FeedbackManager
20
20
  from tinybird.tb_cli_modules.branch import warn_if_in_live
21
21
  from tinybird.tb_cli_modules.cli import cli
@@ -34,9 +34,12 @@ from tinybird.tb_cli_modules.common import (
34
34
  validate_kafka_auto_offset_reset,
35
35
  validate_kafka_group,
36
36
  validate_kafka_topic,
37
+ wait_job,
37
38
  )
38
39
  from tinybird.tb_cli_modules.config import CLIConfig
39
- from tinybird.tb_cli_modules.exceptions import CLIDatasourceException
40
+ from tinybird.tb_cli_modules.exceptions import CLIDatasourceException, CLIException
41
+
42
+ EXPERIMENTAL_FEATURE_USE_V1 = "use_v1"
40
43
 
41
44
 
42
45
  @cli.group()
@@ -45,6 +48,34 @@ def datasource(ctx):
45
48
  """Data Sources commands"""
46
49
 
47
50
 
51
+ def _echo_v1_import_jobs_queued(job_ids: list[str], operation: str) -> None:
52
+ for job_id in job_ids:
53
+ click.echo(FeedbackManager.success_import_job_queued(operation=operation, job_id=job_id))
54
+ click.echo(FeedbackManager.info_import_job_status(job_id=job_id))
55
+
56
+
57
+ async def _wait_for_v1_import_jobs(client: TinyB, job_ids: list[str], operation: str) -> None:
58
+ for job_id in job_ids:
59
+ try:
60
+ await wait_job(client, job_id, f"/v0/jobs/{job_id}", f"{operation} import")
61
+ except CLIException:
62
+ if await _echo_v1_import_job_details(client, job_id):
63
+ raise click.exceptions.Exit(1)
64
+ raise
65
+ click.echo(FeedbackManager.success_import_job_completed(operation=operation, job_id=job_id))
66
+
67
+
68
+ async def _echo_v1_import_job_details(client: TinyB, job_id: str) -> bool:
69
+ try:
70
+ job = await client.job(job_id)
71
+ except Exception:
72
+ return False
73
+ click.echo(FeedbackManager.info_job(job=job_id))
74
+ echo_safe_humanfriendly_tables_format_smart_table([job.values()], column_names=job.keys())
75
+ click.echo("\n")
76
+ return True
77
+
78
+
48
79
  @datasource.command(name="ls")
49
80
  @click.option("--match", default=None, help="Retrieve any resources matching the pattern. eg --match _test")
50
81
  @click.option(
@@ -135,6 +166,13 @@ async def datasource_ls(ctx: Context, match: Optional[str], format_: str):
135
166
  hidden=True,
136
167
  )
137
168
  @click.option("--concurrency", help="How many files to submit concurrently", default=1, hidden=True)
169
+ @click.option("--wait", is_flag=True, default=False, help="Wait for a v1 import job to finish.")
170
+ @click.option(
171
+ "--experimental",
172
+ type=click.Choice([EXPERIMENTAL_FEATURE_USE_V1]),
173
+ multiple=True,
174
+ help="Enable an experimental feature. May be specified multiple times.",
175
+ )
138
176
  @click.pass_context
139
177
  @coro
140
178
  async def datasource_append(
@@ -145,6 +183,8 @@ async def datasource_append(
145
183
  incremental: Optional[str],
146
184
  ignore_empty: bool,
147
185
  concurrency: int,
186
+ experimental: tuple[str, ...],
187
+ wait: bool,
148
188
  ):
149
189
  """
150
190
  Appends data to an existing Data Source from URL, local file or a connector
@@ -157,7 +197,14 @@ async def datasource_append(
157
197
 
158
198
  if not url:
159
199
  raise CLIDatasourceException(FeedbackManager.error_missing_url(datasource=datasource_name))
160
- await push_data(ctx, datasource_name, url, mode="append", concurrency=concurrency)
200
+ use_v1 = EXPERIMENTAL_FEATURE_USE_V1 in experimental
201
+ if wait and not use_v1:
202
+ raise CLIDatasourceException("--wait requires --experimental=use_v1.")
203
+ job_ids = await push_data(ctx, datasource_name, url, mode="append", concurrency=concurrency, use_v1=use_v1)
204
+ if use_v1:
205
+ _echo_v1_import_jobs_queued(job_ids or [], "Append")
206
+ if wait:
207
+ await _wait_for_v1_import_jobs(ctx.obj["client"], job_ids or [], "Append")
161
208
 
162
209
 
163
210
  @datasource.command(name="replace")
@@ -165,6 +212,13 @@ async def datasource_append(
165
212
  @click.argument("url", nargs=-1)
166
213
  @click.option("--sql-condition", default=None, help="SQL WHERE condition to replace data", hidden=True)
167
214
  @click.option("--skip-incompatible-partition-key", is_flag=True, default=False, hidden=True)
215
+ @click.option("--wait", is_flag=True, default=False, help="Wait for a v1 import job to finish.")
216
+ @click.option(
217
+ "--experimental",
218
+ type=click.Choice([EXPERIMENTAL_FEATURE_USE_V1]),
219
+ multiple=True,
220
+ help="Enable an experimental feature. May be specified multiple times.",
221
+ )
168
222
  @click.pass_context
169
223
  @coro
170
224
  async def datasource_replace(
@@ -173,6 +227,8 @@ async def datasource_replace(
173
227
  url,
174
228
  sql_condition,
175
229
  skip_incompatible_partition_key,
230
+ experimental: tuple[str, ...],
231
+ wait: bool,
176
232
  ):
177
233
  """
178
234
  Replaces the data in a data source from a URL, local file or a connector
@@ -189,14 +245,22 @@ async def datasource_replace(
189
245
  replace_options = set()
190
246
  if skip_incompatible_partition_key:
191
247
  replace_options.add("skip_incompatible_partition_key")
192
- await push_data(
248
+ use_v1 = EXPERIMENTAL_FEATURE_USE_V1 in experimental
249
+ if wait and not use_v1:
250
+ raise CLIDatasourceException("--wait requires --experimental=use_v1.")
251
+ job_ids = await push_data(
193
252
  ctx,
194
253
  datasource_name,
195
254
  url,
196
255
  mode="replace",
197
256
  sql_condition=sql_condition,
198
257
  replace_options=replace_options,
258
+ use_v1=use_v1,
199
259
  )
260
+ if use_v1:
261
+ _echo_v1_import_jobs_queued(job_ids or [], "Replace")
262
+ if wait:
263
+ await _wait_for_v1_import_jobs(ctx.obj["client"], job_ids or [], "Replace")
200
264
 
201
265
 
202
266
  @datasource.command(name="analyze")
@@ -916,10 +916,10 @@ def _parse(reader: _TemplateReader, template, in_block=None, in_loop=None):
916
916
 
917
917
  # Intermediate ("else", "elif", etc) blocks
918
918
  intermediate_blocks = {
919
- "else": set(["if", "for", "while", "try"]),
920
- "elif": set(["if"]),
921
- "except": set(["try"]),
922
- "finally": set(["try"]),
919
+ "else": {"if", "for", "while", "try"},
920
+ "elif": {"if"},
921
+ "except": {"try"},
922
+ "finally": {"try"},
923
923
  }
924
924
  allowed_parents = intermediate_blocks.get(operator)
925
925
 
@@ -997,7 +997,7 @@ def _parse(reader: _TemplateReader, template, in_block=None, in_loop=None):
997
997
 
998
998
  elif operator in ("break", "continue"):
999
999
  if not in_loop:
1000
- reader.raise_parse_error("%s outside %s block" % (operator, set(["for", "while"])))
1000
+ reader.raise_parse_error("%s outside %s block" % (operator, {"for", "while"}))
1001
1001
  body.chunks.append(_Statement(contents, line))
1002
1002
  continue
1003
1003
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.6.13.dev0
3
+ Version: 4.6.15.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,11 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.13
56
+ ********
57
+
58
+ - `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.
59
+
55
60
  4.6.12
56
61
  ********
57
62
 
File without changes