tinybird 4.6.15.dev0__tar.gz → 4.6.17.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.15.dev0 → tinybird-4.6.17.dev0}/PKG-INFO +20 -1
  2. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/ch_utils/engine.py +6 -1
  3. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/client.py +22 -43
  4. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/config.py +0 -4
  5. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datafile/common.py +21 -0
  6. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datafile/parse_pipe.py +8 -1
  7. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/feedback_manager.py +4 -3
  8. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/service_datasources.py +43 -19
  9. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/sql_template.py +50 -1
  10. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/sql_toolset.py +0 -5
  11. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/__cli__.py +2 -2
  12. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/client.py +33 -35
  13. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/build_common.py +10 -1
  14. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/common.py +18 -10
  15. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/pipe_checker.py +4 -0
  16. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/pull.py +1 -1
  17. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datasource.py +87 -71
  18. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/deployment.py +5 -8
  19. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/deployment_common.py +83 -13
  20. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/feedback_manager.py +0 -3
  21. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/common.py +59 -10
  22. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/datasource.py +13 -53
  23. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/workspace.py +10 -6
  24. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird.egg-info/PKG-INFO +20 -1
  25. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/setup.cfg +0 -0
  26. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/__cli__.py +0 -0
  27. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/ch_utils/constants.py +0 -0
  28. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/check_pypi.py +0 -0
  29. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/context.py +0 -0
  30. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datafile/exceptions.py +0 -0
  31. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datafile/parse_connection.py +0 -0
  32. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datafile/parse_datasource.py +0 -0
  33. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/datatypes.py +0 -0
  34. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/git_settings.py +0 -0
  35. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/iterating/__init__.py +0 -0
  36. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/iterating/data_branch_modes.py +0 -0
  37. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/prompts.py +0 -0
  38. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/sql.py +0 -0
  39. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/sql_template_fmt.py +0 -0
  40. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/syncasync.py +0 -0
  41. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/check_pypi.py +0 -0
  42. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/cli.py +0 -0
  43. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/config.py +0 -0
  44. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/branch.py +0 -0
  45. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/build.py +0 -0
  46. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/cicd.py +0 -0
  47. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/cli.py +0 -0
  48. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/config.py +0 -0
  49. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/connection.py +0 -0
  50. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/connection_dynamodb.py +0 -0
  51. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/connection_kafka.py +0 -0
  52. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/connection_s3.py +0 -0
  53. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/copy.py +0 -0
  54. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/create.py +0 -0
  55. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/build.py +0 -0
  56. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/build_common.py +0 -0
  57. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  58. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/build_pipe.py +0 -0
  59. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/diff.py +0 -0
  60. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/fixture.py +0 -0
  61. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/format_common.py +0 -0
  62. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  63. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  64. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  65. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/datafile/playground.py +0 -0
  66. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/deprecations.py +0 -0
  67. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/endpoint.py +0 -0
  68. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/exceptions.py +0 -0
  69. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/fmt.py +0 -0
  70. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/info.py +0 -0
  71. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/infra.py +0 -0
  72. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/job.py +0 -0
  73. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/job_common.py +0 -0
  74. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/llm.py +0 -0
  75. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/llm_utils.py +0 -0
  76. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/local.py +0 -0
  77. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/local_common.py +0 -0
  78. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/local_logs.py +0 -0
  79. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/login.py +0 -0
  80. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/login_common.py +0 -0
  81. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/logout.py +0 -0
  82. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/logs.py +0 -0
  83. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/materialization.py +0 -0
  84. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/open.py +0 -0
  85. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/pipe.py +0 -0
  86. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/preview.py +0 -0
  87. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/project.py +0 -0
  88. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/project_commands.py +0 -0
  89. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/py_project.py +0 -0
  90. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/query_output.py +0 -0
  91. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/regions.py +0 -0
  92. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/secret.py +0 -0
  93. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/secret_common.py +0 -0
  94. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/sink.py +0 -0
  95. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/table.py +0 -0
  96. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/telemetry.py +0 -0
  97. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/test.py +0 -0
  98. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/test_common.py +0 -0
  99. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  100. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/token.py +0 -0
  101. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/ts_project.py +0 -0
  102. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/watch.py +0 -0
  103. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/workspace.py +0 -0
  104. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb/modules/workspace_members.py +0 -0
  105. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli.py +0 -0
  106. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/auth.py +0 -0
  107. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/branch.py +0 -0
  108. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/cicd.py +0 -0
  109. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/cli.py +0 -0
  110. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/config.py +0 -0
  111. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/connection.py +0 -0
  112. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  113. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/fmt.py +0 -0
  114. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/job.py +0 -0
  115. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/pipe.py +0 -0
  116. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/regions.py +0 -0
  117. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/tag.py +0 -0
  118. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  119. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/test.py +0 -0
  120. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  121. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  122. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird/tornado_template.py +0 -0
  123. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird.egg-info/SOURCES.txt +0 -0
  124. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird.egg-info/dependency_links.txt +0 -0
  125. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird.egg-info/entry_points.txt +0 -0
  126. {tinybird-4.6.15.dev0 → tinybird-4.6.17.dev0}/tinybird.egg-info/requires.txt +0 -0
  127. {tinybird-4.6.15.dev0 → tinybird-4.6.17.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.15.dev0
3
+ Version: 4.6.17.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -52,10 +52,27 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.15
56
+ ********
57
+
58
+ - `Fixed` ``tb deploy`` waits for the old deployment to delete to avoid clashing with a new deployment submission.
59
+
60
+ 4.6.14
61
+ ********
62
+
63
+ - `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.
64
+
65
+ 4.6.14
66
+ ********
67
+
68
+ - `Fixed` `tb migrate-to-forward` correctly finds and offers to delete all branches, not just those created by the user running the migration.
69
+
55
70
  4.6.13
56
71
  ********
57
72
 
73
+ - `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.
58
74
  - `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.
75
+ - `Fixed` Forward builds branch with vendored files without incorrectly detecting drift.
59
76
 
60
77
  4.6.12
61
78
  ********
@@ -74,6 +91,7 @@ Changelog
74
91
 
75
92
  - `Removed` `tinybird.bi_stats_rt` from `tb logs` sources. The BI Connector `bi_stats` / `bi_stats_rt` service datasources are deprecated.
76
93
 
94
+
77
95
  4.6.9
78
96
  *******
79
97
 
@@ -82,6 +100,7 @@ Changelog
82
100
  4.6.8
83
101
  *******
84
102
 
103
+ - `Changed` Upgraded Tornado to 6.5.5.
85
104
  - `Fixed` Tinybird Local CLI commands to respect Git branch local workspaces correctly.
86
105
 
87
106
  4.6.7
@@ -476,6 +476,9 @@ def engine_options(columns, options: List[EngineOption], args: Dict):
476
476
  if option_value:
477
477
  if o.sql.lower() == "settings":
478
478
  engine_settings = f"{o.sql} {option_value}"
479
+ # Conditional TTLs fail if we wrap the value in parentheses
480
+ elif o.sql.lower() == "ttl":
481
+ options_values.append(f"{o.sql} {option_value}")
479
482
  else:
480
483
  options_values.append(f"{o.sql} ({option_value})")
481
484
 
@@ -536,7 +539,9 @@ def engine_full_from_dict(
536
539
  >>> engine_full_from_dict('MergeTree', {'sorting_key': 'local_date, cod_store'}, schema=schema)
537
540
  'MergeTree() ORDER BY (local_date, cod_store)'
538
541
  >>> engine_full_from_dict('MergeTree', {'partition_key': 'toDate(timestamp)', 'sorting_key': 'local_date, cod_store', 'settings': 'index_granularity = 32, index_granularity_bytes = 2048', 'ttl': 'toDate(local_date) + INTERVAL 1 DAY'}, schema=schema)
539
- 'MergeTree() PARTITION BY (toDate(timestamp)) ORDER BY (local_date, cod_store) TTL (toDate(local_date) + INTERVAL 1 DAY) SETTINGS index_granularity = 32, index_granularity_bytes = 2048'
542
+ 'MergeTree() PARTITION BY (toDate(timestamp)) ORDER BY (local_date, cod_store) TTL toDate(local_date) + INTERVAL 1 DAY SETTINGS index_granularity = 32, index_granularity_bytes = 2048'
543
+ >>> engine_full_from_dict('MergeTree', {'sorting_key': 'timestamp', 'ttl': 'timestamp + INTERVAL 1 DAY DELETE WHERE is_deleted = 1'}, schema=schema)
544
+ 'MergeTree() ORDER BY (timestamp) TTL timestamp + INTERVAL 1 DAY DELETE WHERE is_deleted = 1'
540
545
 
541
546
  >>> schema = ''
542
547
  >>> engine_full_from_dict('CollapsingMergeTree', {'sign': 'sign_column'}, schema=schema)
@@ -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,58 +437,37 @@ 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
- use_v1: bool = False,
445
444
  ):
446
- params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
447
-
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}
448
450
  if sql_condition:
449
- params["replace_condition"] = sql_condition
451
+ v1_params["replace_condition"] = sql_condition
450
452
  if replace_options:
451
453
  for option in list(replace_options):
452
- params[option] = "true"
453
-
454
- async with aiofiles.open(file, "rb") as content:
455
- file_content = await content.read()
456
-
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,
476
- method="POST",
477
- )
478
-
479
- if format == "csv":
480
- files = {"csv": ("csv", file_content)}
481
- else:
482
- files = {"ndjson": ("ndjson", file_content)}
483
-
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"
484
463
  res = await self._req(
485
- f"v0/datasources?{urlencode(params, safe='')}",
486
- files=files,
464
+ f"/v1/datasources/{quote(datasource_name, safe='')}/{mode}?{urlencode(v1_params, safe='')}",
465
+ data=file_content,
466
+ headers=headers,
487
467
  method="POST",
488
468
  )
489
- if status_callback:
490
- status_callback(res)
491
-
469
+ if not isinstance(res, dict):
470
+ raise RuntimeError("We couldn't confirm that your import started. Please try again.")
492
471
  return res
493
472
 
494
473
  async def datasource_truncate(self, datasource_name: str):
@@ -49,11 +49,7 @@ LEGACY_HOSTS = {
49
49
  "https://ui.europe-west2.gcp.tinybird.co": "https://cloud.tinybird.co/gcp/europe-west2",
50
50
  "https://ui.ap-east.aws.tinybird.co": "https://app.tinybird.co/aws/ap-east-1",
51
51
  "https://ui.ap-east-1.aws.tinybird.co": "https://app.tinybird.co/aws/ap-east-1",
52
- "https://ui.split.tinybird.co": "https://app.tinybird.co/aws/split-us-east",
53
- "https://ui.split.us-west-2.aws.tinybird.co": "https://app.tinybird.co/aws/split-us-west-2",
54
52
  "https://ui.northamerica-northeast2.gcp.tinybird.co": "https://app.tinybird.co/gcp/northamerica-northeast2",
55
- "https://api.split.tinybird.co": "https://app.tinybird.co/aws/split-us-east",
56
- "https://api.split.us-west-2.aws.tinybird.co": "https://app.tinybird.co/aws/split-us-west-2",
57
53
  "https://ui.wadus1.gcp.tinybird.co": "https://app.wadus.tinybird.co/gcp/wadus1",
58
54
  "https://ui.wadus2.gcp.tinybird.co": "https://app.wadus.tinybird.co/gcp/wadus2",
59
55
  "https://ui.wadus3.gcp.tinybird.co": "https://app.wadus.tinybird.co/gcp/wadus3",
@@ -278,6 +278,12 @@ REQUIRED_DYNAMODB_PARAMS = {
278
278
  }
279
279
  DYNAMODB_PARAMS = REQUIRED_DYNAMODB_PARAMS
280
280
 
281
+ REQUIRED_POSTGRES_PARAMS = {
282
+ "pg_connection_name",
283
+ "pg_table",
284
+ }
285
+ POSTGRES_PARAMS = REQUIRED_POSTGRES_PARAMS
286
+
281
287
 
282
288
  def extract_column_names_from_sorting_key_part(part: str) -> List[str]:
283
289
  """
@@ -602,6 +608,13 @@ class Datafile:
602
608
  raise DatafileValidationError(
603
609
  f"Invalid import schedule '{node['import_schedule']}'. Only {sorted(VALID_BLOB_STORAGE_CRON_VALUES)} values are allowed"
604
610
  )
611
+ # Validate Postgres params
612
+ if any(param in node for param in POSTGRES_PARAMS) and (
613
+ missing := [param for param in REQUIRED_POSTGRES_PARAMS if param not in node]
614
+ ):
615
+ raise DatafileValidationError(
616
+ f"Some Postgres params have been provided, but the following required ones are missing: {missing}"
617
+ )
605
618
 
606
619
  else:
607
620
  # We cannot validate a datafile whose kind is unknown
@@ -2068,6 +2081,8 @@ def parse(
2068
2081
  "import_query": assign_var("import_query"), # Deprecated, BQ and SFK
2069
2082
  "import_table_arn": assign_var("import_table_arn"), # Only for DynamoDB
2070
2083
  "import_export_bucket": assign_var("import_export_bucket"), # For DynamoDB
2084
+ "pg_connection_name": assign_var("pg_connection_name"),
2085
+ "pg_table": assign_var("pg_table"),
2071
2086
  "shared_with": shared_with,
2072
2087
  "export_service": export_service, # Deprecated
2073
2088
  "forward_query": forward_query,
@@ -2128,6 +2143,12 @@ def parse(
2128
2143
  "gcs_secret": assign_var("gcs_hmac_secret"),
2129
2144
  "gcs_hmac_access_id": assign_var("gcs_hmac_access_id"),
2130
2145
  "gcs_hmac_secret": assign_var("gcs_hmac_secret"),
2146
+ "pg_host": assign_var("pg_host"),
2147
+ "pg_port": assign_var("pg_port"),
2148
+ "pg_database": assign_var("pg_database"),
2149
+ "pg_user": assign_var("pg_user"),
2150
+ "pg_password": assign_var("pg_password"),
2151
+ "pg_ssl_mode": assign_var("pg_ssl_mode"),
2131
2152
  "include": include,
2132
2153
  },
2133
2154
  }
@@ -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
 
@@ -61,6 +65,9 @@ def parse_pipe(
61
65
  secrets_list: Optional[List[str]] = None
62
66
  if secrets:
63
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"])
64
71
  # Setting test_mode=True to ignore errors on required parameters and
65
72
  # secrets_in_test_mode=False to raise errors on missing secrets
66
73
  sql, _, variable_warnings = render_sql_template(
@@ -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
  )
@@ -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"},
@@ -48,6 +48,7 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
48
48
  {"name": "client_ip", "type": "Nullable(String)"},
49
49
  {"name": "resource_tags", "type": "Array(String)"},
50
50
  {"name": "memory_usage", "type": "UInt64"},
51
+ {"name": "replica", "type": "String"},
51
52
  ],
52
53
  },
53
54
  {
@@ -333,6 +334,25 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
333
334
  {"name": "job_metadata", "type": "String"},
334
335
  ],
335
336
  },
337
+ {
338
+ "name": "tinybird.rate_limits_log",
339
+ "description": "Endpoint concurrency limiter transitions in your workspace.",
340
+ "dateColumn": "timestamp",
341
+ "engine": {
342
+ "engine": "MergeTree",
343
+ "sorting_key": "timestamp, pipe_name",
344
+ "partition_key": "toYYYYMM(timestamp)",
345
+ },
346
+ "columns": [
347
+ {"name": "timestamp", "type": "DateTime64(3)"},
348
+ {"name": "workspace_id", "type": "String"},
349
+ {"name": "workspace_name", "type": "String"},
350
+ {"name": "pipe_name", "type": "String"},
351
+ {"name": "rate_limit_type", "type": "LowCardinality(String)"},
352
+ {"name": "action", "type": "LowCardinality(String)"},
353
+ {"name": "limit_metadata", "type": "String"},
354
+ ],
355
+ },
336
356
  {
337
357
  "name": "tinybird.hook_log",
338
358
  "description": "Log of hook executions and their results.",
@@ -518,22 +538,6 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
518
538
  {"name": "origin", "type": "String"},
519
539
  ],
520
540
  },
521
- {
522
- "name": "organization.processed_data",
523
- "description": "Information related to all processed data per day per workspace.",
524
- "dateColumn": "date",
525
- "engine": {
526
- "engine": "SummingMergeTree",
527
- "sorting_key": "database, date",
528
- "partition_key": "toYYYYMM(date)",
529
- },
530
- "columns": [
531
- {"name": "date", "type": "Date"},
532
- {"name": "database", "type": "String"},
533
- {"name": "read_bytes", "type": "UInt64"},
534
- {"name": "written_bytes", "type": "UInt64"},
535
- ],
536
- },
537
541
  {
538
542
  "name": "organization.datasources_storage",
539
543
  "description": "Similar to tinybird.datasources_storage but with data for all Organization Workspaces.",
@@ -659,7 +663,7 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
659
663
  "engine": {
660
664
  "engine": "MergeTree",
661
665
  "sorting_key": "workspace_id, pipe_id, start_datetime",
662
- "ttl": "start_datetime + toIntervalDay(7)",
666
+ "ttl": "start_datetime + toIntervalMonth(3)",
663
667
  },
664
668
  "columns": [
665
669
  {"name": "workspace_id", "type": "String"},
@@ -683,6 +687,7 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
683
687
  {"name": "user_agent", "type": "Nullable(String)"},
684
688
  {"name": "resource_tags", "type": "Array(String)"},
685
689
  {"name": "memory_usage", "type": "UInt64"},
690
+ {"name": "replica", "type": "String"},
686
691
  ],
687
692
  },
688
693
  {
@@ -726,6 +731,25 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
726
731
  {"name": "job_metadata", "type": "String"},
727
732
  ],
728
733
  },
734
+ {
735
+ "name": "organization.rate_limits_log",
736
+ "description": "Endpoint concurrency limiter transitions across the organization.",
737
+ "dateColumn": "timestamp",
738
+ "engine": {
739
+ "engine": "MergeTree",
740
+ "sorting_key": "workspace_id, timestamp, pipe_name",
741
+ "partition_key": "toYYYYMM(timestamp)",
742
+ },
743
+ "columns": [
744
+ {"name": "timestamp", "type": "DateTime64(3)"},
745
+ {"name": "workspace_id", "type": "String"},
746
+ {"name": "workspace_name", "type": "String"},
747
+ {"name": "pipe_name", "type": "String"},
748
+ {"name": "rate_limit_type", "type": "LowCardinality(String)"},
749
+ {"name": "action", "type": "LowCardinality(String)"},
750
+ {"name": "limit_metadata", "type": "String"},
751
+ ],
752
+ },
729
753
  {
730
754
  "name": "organization.sinks_ops_log",
731
755
  "description": "Historic Logs for all Sink job executions across the organization",
@@ -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):
@@ -493,7 +493,6 @@ def replace_tables_chquery_cached(
493
493
  sorted_replacements: Optional[tuple] = None,
494
494
  default_database: str = "",
495
495
  output_one_line: bool = False,
496
- timestamp: Optional[datetime] = None,
497
496
  function_allow_list: Optional[FrozenSet[str]] = None,
498
497
  settings_allow_list: Optional[FrozenSet[str]] = None,
499
498
  ) -> str:
@@ -536,7 +535,6 @@ def replace_tables(
536
535
  sql,
537
536
  None,
538
537
  output_one_line=output_one_line,
539
- timestamp=timestamp,
540
538
  function_allow_list=hashable_list,
541
539
  settings_allow_list=hashable_settings_list,
542
540
  )
@@ -610,7 +608,6 @@ def replace_tables(
610
608
  sql,
611
609
  None,
612
610
  output_one_line=output_one_line,
613
- timestamp=timestamp,
614
611
  function_allow_list=hashable_list,
615
612
  settings_allow_list=hashable_settings_list,
616
613
  )
@@ -649,7 +646,6 @@ def replace_tables(
649
646
  r,
650
647
  default_database=default_database,
651
648
  output_one_line=output_one_line,
652
- timestamp=timestamp,
653
649
  function_allow_list=hashable_list,
654
650
  settings_allow_list=hashable_settings_list,
655
651
  )
@@ -658,7 +654,6 @@ def replace_tables(
658
654
  sql,
659
655
  None,
660
656
  output_one_line=output_one_line,
661
- timestamp=timestamp,
662
657
  function_allow_list=hashable_list,
663
658
  settings_allow_list=hashable_settings_list,
664
659
  )
@@ -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.15.dev0'
8
- __revision__ = '8311ae9'
7
+ __version__ = '4.6.17.dev0'
8
+ __revision__ = '026318e'
@@ -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:
@@ -465,16 +465,9 @@ 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,
469
468
  ):
470
- params = {"name": datasource_name, "mode": mode, "format": format, "debug": "blocks_block_log"}
471
-
472
- if sql_condition:
473
- params["replace_condition"] = sql_condition
474
- if replace_options:
475
- for option in list(replace_options):
476
- params[option] = "true"
477
-
469
+ # Keep this argument for SDK compatibility; v1 returns before import completion.
470
+ del status_callback
478
471
  with open(file, "rb") as content:
479
472
  file_content = content.read()
480
473
  content_types = {
@@ -485,29 +478,34 @@ class TinyB:
485
478
  headers = {"Content-Type": content_types[format]}
486
479
  if str(file).endswith(".gz"):
487
480
  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
504
- if format == "csv":
505
- files = {"csv": ("csv", file_content)}
506
- else:
507
- files = {"ndjson": ("ndjson", file_content)}
508
- res = self._req(f"v0/datasources?{urlencode(params, safe='')}", files=files, method="POST")
509
- if status_callback:
510
- status_callback(res)
481
+ v1_params = {"format": format}
482
+ if sql_condition:
483
+ v1_params["replace_condition"] = sql_condition
484
+ if replace_options:
485
+ for option in list(replace_options):
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
498
+
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"])
511
509
  return res
512
510
 
513
511
  def datasource_truncate(self, datasource_name: str):
@@ -835,8 +833,8 @@ class TinyB:
835
833
  def user_workspace_branches(self, version: str = "v0"):
836
834
  return self._req(f"/{version}/user/workspaces/?with_environments=true&only_environments=true")
837
835
 
838
- def branches(self):
839
- return self._req("/v1/environments")
836
+ def branches(self, version: str = "v1"):
837
+ return self._req(f"/{version}/environments")
840
838
 
841
839
  def releases(self, workspace_id):
842
840
  return self._req(f"/v0/workspaces/{workspace_id}/releases")
@@ -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