tinybird 4.5.12.dev0__tar.gz → 4.6.1__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.5.12.dev0 → tinybird-4.6.1}/PKG-INFO +12 -3
  2. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datafile/common.py +17 -1
  3. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/service_datasources.py +4 -0
  4. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/sql.py +8 -0
  5. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/sql_template.py +2 -2
  6. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/__cli__.py +2 -2
  7. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/client.py +20 -1
  8. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/build_common.py +25 -0
  9. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/common.py +69 -29
  10. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/connection.py +46 -0
  11. tinybird-4.6.1/tinybird/tb/modules/connection_dynamodb.py +240 -0
  12. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/connection_s3.py +2 -2
  13. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/create.py +12 -0
  14. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/build_pipe.py +2 -2
  15. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/pull.py +29 -6
  16. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datasource.py +166 -1
  17. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/local_common.py +2 -2
  18. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/project.py +10 -0
  19. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/query_output.py +3 -1
  20. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/secret.py +15 -1
  21. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/test_common.py +1 -1
  22. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/common.py +3 -1
  23. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/PKG-INFO +12 -3
  24. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/SOURCES.txt +1 -0
  25. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/requires.txt +1 -1
  26. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/setup.cfg +0 -0
  27. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/__cli__.py +0 -0
  28. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/ch_utils/constants.py +0 -0
  29. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/ch_utils/engine.py +0 -0
  30. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/check_pypi.py +0 -0
  31. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/client.py +0 -0
  32. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/config.py +0 -0
  33. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/context.py +0 -0
  34. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datafile/exceptions.py +0 -0
  35. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datafile/parse_connection.py +0 -0
  36. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datafile/parse_datasource.py +0 -0
  37. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datafile/parse_pipe.py +0 -0
  38. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/datatypes.py +0 -0
  39. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/feedback_manager.py +0 -0
  40. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/git_settings.py +0 -0
  41. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/prompts.py +0 -0
  42. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/sql_template_fmt.py +0 -0
  43. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/sql_toolset.py +0 -0
  44. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/syncasync.py +0 -0
  45. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/check_pypi.py +0 -0
  46. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/cli.py +0 -0
  47. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/config.py +0 -0
  48. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/branch.py +0 -0
  49. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/build.py +0 -0
  50. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/cicd.py +0 -0
  51. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/cli.py +0 -0
  52. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/config.py +0 -0
  53. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/connection_kafka.py +0 -0
  54. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/copy.py +0 -0
  55. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/build.py +0 -0
  56. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/build_common.py +0 -0
  57. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  58. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/diff.py +0 -0
  59. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/fixture.py +0 -0
  60. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/format_common.py +0 -0
  61. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/format_connection.py +0 -0
  62. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  63. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  64. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  65. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/datafile/playground.py +0 -0
  66. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/deployment.py +0 -0
  67. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/deployment_common.py +0 -0
  68. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/deprecations.py +0 -0
  69. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/endpoint.py +0 -0
  70. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/exceptions.py +0 -0
  71. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/feedback_manager.py +0 -0
  72. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/fmt.py +0 -0
  73. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/info.py +0 -0
  74. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/infra.py +0 -0
  75. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/job.py +0 -0
  76. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/job_common.py +0 -0
  77. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/llm.py +0 -0
  78. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/llm_utils.py +0 -0
  79. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/local.py +0 -0
  80. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/local_logs.py +0 -0
  81. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/login.py +0 -0
  82. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/login_common.py +0 -0
  83. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/logout.py +0 -0
  84. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/logs.py +0 -0
  85. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/materialization.py +0 -0
  86. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/open.py +0 -0
  87. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/pipe.py +0 -0
  88. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/preview.py +0 -0
  89. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/project_commands.py +0 -0
  90. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/py_project.py +0 -0
  91. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/regions.py +0 -0
  92. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/secret_common.py +0 -0
  93. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/sink.py +0 -0
  94. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/table.py +0 -0
  95. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/telemetry.py +0 -0
  96. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/test.py +0 -0
  97. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  98. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  99. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/token.py +0 -0
  100. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/ts_project.py +0 -0
  101. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/watch.py +0 -0
  102. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/workspace.py +0 -0
  103. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb/modules/workspace_members.py +0 -0
  104. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli.py +0 -0
  105. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/auth.py +0 -0
  106. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/branch.py +0 -0
  107. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/cicd.py +0 -0
  108. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/cli.py +0 -0
  109. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/config.py +0 -0
  110. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/connection.py +0 -0
  111. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/datasource.py +0 -0
  112. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/exceptions.py +0 -0
  113. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/fmt.py +0 -0
  114. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/job.py +0 -0
  115. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/pipe.py +0 -0
  116. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/regions.py +0 -0
  117. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/tag.py +0 -0
  118. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/telemetry.py +0 -0
  119. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/test.py +0 -0
  120. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  121. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  122. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/workspace.py +0 -0
  123. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  124. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird/tornado_template.py +0 -0
  125. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/dependency_links.txt +0 -0
  126. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/entry_points.txt +0 -0
  127. {tinybird-4.5.12.dev0 → tinybird-4.6.1}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.5.12.dev0
3
+ Version: 4.6.1
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -27,7 +27,7 @@ Requires-Dist: requests<3,>=2.28.1
27
27
  Requires-Dist: shandy-sqlfmt==0.11.1
28
28
  Requires-Dist: shandy-sqlfmt[jinjafmt]==0.11.1
29
29
  Requires-Dist: toposort==1.10
30
- Requires-Dist: tornado~=6.0.0
30
+ Requires-Dist: tornado~=6.5.5
31
31
  Requires-Dist: urllib3<2,>=1.26.14
32
32
  Requires-Dist: watchdog==6.0.0
33
33
  Requires-Dist: wheel
@@ -52,12 +52,21 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.1
56
+ *******
57
+
58
+ - `Changed` `tb pull` to overwrite existing local files when pulling from a Tinybird workspace.
59
+
60
+ 4.6.0
61
+ *******
62
+
63
+ - `Added` Support for AWS DynamoDB datasource creation and management.
64
+
55
65
  4.5.12
56
66
  *******
57
67
 
58
68
  - `Fixed` `tb info` now lists branches and marks the current branch correctly when running on a branch.
59
69
 
60
-
61
70
  4.5.11
62
71
  *******
63
72
 
@@ -277,6 +277,13 @@ VALID_BLOB_STORAGE_CRON_VALUES = {
277
277
  "@auto",
278
278
  }
279
279
 
280
+ REQUIRED_DYNAMODB_PARAMS = {
281
+ "import_connection_name",
282
+ "import_table_arn",
283
+ "import_export_bucket",
284
+ }
285
+ DYNAMODB_PARAMS = REQUIRED_DYNAMODB_PARAMS
286
+
280
287
 
281
288
  def extract_column_names_from_sorting_key_part(part: str) -> List[str]:
282
289
  """
@@ -575,8 +582,15 @@ class Datafile:
575
582
  )
576
583
  if "kafka_group_id" in node and not str(node["kafka_group_id"]).strip():
577
584
  raise DatafileValidationError("KAFKA_GROUP_ID cannot be empty")
585
+ # Validate DynamoDB params first since import_connection_name is shared with blob storage.
586
+ is_dynamodb = any(param in node for param in ("import_table_arn", "import_export_bucket"))
587
+ if is_dynamodb:
588
+ if missing := [param for param in REQUIRED_DYNAMODB_PARAMS if param not in node]:
589
+ raise DatafileValidationError(
590
+ f"Some DynamoDB params have been provided, but the following required ones are missing: {missing}"
591
+ )
578
592
  # Validate S3 params
579
- if any(param in node for param in BLOB_STORAGE_PARAMS):
593
+ elif any(param in node for param in BLOB_STORAGE_PARAMS):
580
594
  if missing := [param for param in REQUIRED_BLOB_STORAGE_PARAMS if param not in node]:
581
595
  raise DatafileValidationError(
582
596
  f"Some connection params have been provided, but the following required ones are missing: {missing}"
@@ -2097,6 +2111,8 @@ def parse(
2097
2111
  "s3_arn": assign_var("s3_arn"),
2098
2112
  "s3_access_key": assign_var("s3_access_key"),
2099
2113
  "s3_secret": assign_var("s3_secret"),
2114
+ "dynamodb_arn": assign_var("dynamodb_arn"),
2115
+ "dynamodb_region": assign_var("dynamodb_region"),
2100
2116
  "gcs_service_account_credentials_json": assign_var_json("gcs_service_account_credentials_json"),
2101
2117
  "gcs_access_id": assign_var("gcs_hmac_access_id"),
2102
2118
  "gcs_secret": assign_var("gcs_hmac_secret"),
@@ -219,7 +219,9 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
219
219
  {"name": "processed_messages", "type": "Int32"},
220
220
  {"name": "processed_bytes", "type": "Int32"},
221
221
  {"name": "committed_messages", "type": "Int32"},
222
+ {"name": "time_read", "type": "Float32"},
222
223
  {"name": "time_process", "type": "Float32"},
224
+ {"name": "time_write", "type": "Float32"},
223
225
  {"name": "msg", "type": "String"},
224
226
  ],
225
227
  },
@@ -880,7 +882,9 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
880
882
  {"name": "processed_messages", "type": "Int32"},
881
883
  {"name": "processed_bytes", "type": "Int32"},
882
884
  {"name": "committed_messages", "type": "Int32"},
885
+ {"name": "time_read", "type": "Float32"},
883
886
  {"name": "time_process", "type": "Float32"},
887
+ {"name": "time_write", "type": "Float32"},
884
888
  {"name": "msg", "type": "String"},
885
889
  ],
886
890
  },
@@ -398,6 +398,14 @@ def parse_table_structure(schema: str) -> List[Dict[str, Any]]:
398
398
  >>> parse_table_structure('c Nullable(Float32) DEFAULT NULL')
399
399
  [{'name': 'c', 'type': 'Float32', 'codec': None, 'default_value': None, 'jsonpath': None, 'nullable': True, 'normalized_name': 'c'}]
400
400
 
401
+ >>> parse_table_structure('c LowCardinality(Nullable(String))')
402
+ [{'name': 'c', 'type': 'LowCardinality(Nullable(String))', 'codec': None, 'default_value': None, 'jsonpath': None, 'nullable': False, 'normalized_name': 'c'}]
403
+
404
+ >>> parse_table_structure('c Nullable(LowCardinality(Nullable(String)))')
405
+ Traceback (most recent call last):
406
+ ...
407
+ ValueError: Nested type LowCardinality(Nullable(String)) cannot be inside Nullable type
408
+
401
409
  >>> parse_table_structure("c String DEFAULT 'bla'")
402
410
  [{'name': 'c', 'type': 'String', 'codec': None, 'default_value': "DEFAULT 'bla'", 'jsonpath': None, 'nullable': False, 'normalized_name': 'c'}]
403
411
 
@@ -7,7 +7,7 @@ from collections import deque
7
7
  from datetime import datetime
8
8
  from functools import lru_cache
9
9
  from json import loads
10
- from typing import Any, Callable, Dict, List, Optional, Tuple, Union
10
+ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast
11
11
 
12
12
  from tornado import escape
13
13
  from tornado.util import ObjectDict, exec_in, unicode_type
@@ -1499,7 +1499,7 @@ def generate(self, **kwargs) -> Tuple[str, TemplateExecutionResults]:
1499
1499
  )
1500
1500
 
1501
1501
  exec_in(self.compiled, namespace)
1502
- execute = namespace["_tt_execute"]
1502
+ execute = cast(Callable[[], bytes], namespace["_tt_execute"])
1503
1503
  # Clear the traceback module's cache of source data now that
1504
1504
  # we've generated a new template (mainly for this module's
1505
1505
  # unittests, where different tests reuse the same name).
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '4.5.12.dev0'
8
- __revision__ = '6a70694'
7
+ __version__ = '4.6.1'
8
+ __revision__ = '41da03f'
@@ -1183,6 +1183,21 @@ class TinyB:
1183
1183
  except Exception:
1184
1184
  return False
1185
1185
 
1186
+ def validate_dynamodb(
1187
+ self, table_arn: str, region: str, role_arn: str, external_id_seed: Optional[str] = None
1188
+ ) -> Dict[str, Any]:
1189
+ body: Dict[str, Any] = {"table_arn": table_arn, "region": region, "role_arn": role_arn}
1190
+ if external_id_seed:
1191
+ body["external_id_seed"] = external_id_seed
1192
+ response = self._req_raw(
1193
+ "/v1/integrations/dynamodb/validate",
1194
+ method="POST",
1195
+ data=json.dumps(body),
1196
+ )
1197
+ if response.status_code >= 400:
1198
+ raise requests.HTTPError(parse_error_response(response), response=response)
1199
+ return response.json()
1200
+
1186
1201
  def get_trust_policy(self, service: str, external_id_seed: Optional[str] = None) -> Dict[str, Any]:
1187
1202
  params = {}
1188
1203
  if external_id_seed:
@@ -1195,11 +1210,15 @@ class TinyB:
1195
1210
  params["bucket"] = bucket
1196
1211
  return self._req(f"/v0/integrations/{service}/policies/write-access-policy?{urlencode(params)}")
1197
1212
 
1198
- def get_access_read_policy(self, service: str, bucket: Optional[str] = None) -> Dict[str, Any]:
1213
+ def get_access_read_policy(
1214
+ self, service: str, bucket: Optional[str] = None, table_name: Optional[str] = None
1215
+ ) -> Dict[str, Any]:
1199
1216
  params = {}
1200
1217
  if bucket:
1201
1218
  # The Kafka endpoint scopes the policy via `msk_cluster_arn`, not `bucket`.
1202
1219
  params["msk_cluster_arn" if service == "kafka" else "bucket"] = bucket
1220
+ if table_name:
1221
+ params["table_name"] = table_name
1203
1222
  return self._req(f"/v0/integrations/{service}/policies/read-access-policy?{urlencode(params)}")
1204
1223
 
1205
1224
  def sql_get_format(self, sql: str, with_clickhouse_format: bool = False) -> str:
@@ -247,6 +247,8 @@ def build_project(
247
247
  if load_fixtures:
248
248
  append_project_fixtures(project, tb_client, project_files)
249
249
  echo_build_feedback(build.get("feedback", []))
250
+ if with_connections:
251
+ echo_dynamodb_local_backfill_feedback(build)
250
252
  return True
251
253
  elif build_result == "failed":
252
254
  error = format_build_errors(result.get("errors", []))
@@ -461,6 +463,29 @@ def echo_build_feedback(feedback: list[dict[str, Any]]) -> None:
461
463
  )
462
464
 
463
465
 
466
+ def echo_dynamodb_local_backfill_feedback(build: dict[str, Any]) -> None:
467
+ datasources_by_id = {datasource.get("id"): datasource.get("name") for datasource in build.get("datasources", [])}
468
+ for data_linker in build.get("data_linkers", []):
469
+ if data_linker.get("service") != "dynamodb":
470
+ continue
471
+
472
+ settings = data_linker.get("settings") or {}
473
+ export_arn = settings.get("initial_export_arn")
474
+ if not export_arn:
475
+ continue
476
+
477
+ datasource_name = datasources_by_id.get(data_linker.get("datasource_id")) or "unknown"
478
+ click.echo(
479
+ FeedbackManager.warning(
480
+ message=(
481
+ f"△ DynamoDB initial export backfill started for datasource '{datasource_name}'. "
482
+ "AWS exports can stay in progress for several minutes; Tinybird Local will keep retrying "
483
+ f"the import until AWS marks the export as completed. Export ARN: {export_arn}."
484
+ )
485
+ )
486
+ )
487
+
488
+
464
489
  def format_build_errors(build_errors: list[dict[str, Any]]) -> str:
465
490
  full_error_msg = ""
466
491
  for build_error in build_errors:
@@ -105,7 +105,9 @@ def echo_safe_humanfriendly_tables_format_smart_table(data: Iterable[Any], colum
105
105
  try:
106
106
  click.echo(humanfriendly.tables.format_smart_table(data, column_names=column_names))
107
107
  except ValueError as exc:
108
- if str(exc) == "max() arg is an empty sequence":
108
+ # Python 3.11 wording: "max() arg is an empty sequence"
109
+ # Python 3.12 wording: "max() iterable argument is empty"
110
+ if str(exc) in ("max() arg is an empty sequence", "max() iterable argument is empty"):
109
111
  click.echo("------------")
110
112
  click.echo("Empty")
111
113
  click.echo("------------")
@@ -122,7 +124,9 @@ def echo_safe_humanfriendly_tables_format_pretty_table(data: Iterable[Any], colu
122
124
  try:
123
125
  click.echo(humanfriendly.tables.format_pretty_table(data, column_names=column_names))
124
126
  except ValueError as exc:
125
- if str(exc) == "max() arg is an empty sequence":
127
+ # Python 3.11 wording: "max() arg is an empty sequence"
128
+ # Python 3.12 wording: "max() iterable argument is empty"
129
+ if str(exc) in ("max() arg is an empty sequence", "max() iterable argument is empty"):
126
130
  click.echo("------------")
127
131
  click.echo("Empty")
128
132
  click.echo("------------")
@@ -139,7 +143,9 @@ def echo_safe_format_table(data: Iterable[Any], columns) -> None:
139
143
  try:
140
144
  click.echo(format_table(data, columns))
141
145
  except ValueError as exc:
142
- if str(exc) == "max() arg is an empty sequence":
146
+ # Python 3.11 wording: "max() arg is an empty sequence"
147
+ # Python 3.12 wording: "max() iterable argument is empty"
148
+ if str(exc) in ("max() arg is an empty sequence", "max() iterable argument is empty"):
143
149
  click.echo("------------")
144
150
  click.echo("Empty")
145
151
  click.echo("------------")
@@ -1623,7 +1629,7 @@ def run_aws_iamrole_connection_flow(
1623
1629
  local_unavailable: bool = False,
1624
1630
  ) -> Tuple[str, str, Optional[TinyB], Optional[TinyB]]:
1625
1631
  """
1626
- Run the interactive AWS IAM Role connection flow for S3.
1632
+ Run the interactive AWS IAM Role connection flow for S3 or DynamoDB.
1627
1633
 
1628
1634
  Guides the user through creating an IAM policy and role with the appropriate
1629
1635
  trust policy that includes AWS account IDs from the selected environments.
@@ -1631,7 +1637,7 @@ def run_aws_iamrole_connection_flow(
1631
1637
  Args:
1632
1638
  config: The CLI configuration dictionary.
1633
1639
  client: The TinyB client instance.
1634
- service: The data connector service type (e.g., 's3').
1640
+ service: The data connector service type (e.g., 's3' or 'dynamodb').
1635
1641
  connection_name: The name for the connection being created.
1636
1642
  policy: The access policy type ('read' or 'write').
1637
1643
  local_unavailable: If True, local environment is unavailable (e.g., missing AWS credentials).
@@ -1639,23 +1645,41 @@ def run_aws_iamrole_connection_flow(
1639
1645
  Returns:
1640
1646
  A tuple containing:
1641
1647
  - role_arn (str): The AWS IAM Role ARN entered by the user.
1642
- - region (str): The AWS region where the bucket is located.
1648
+ - region (str): The AWS region where the resource is located.
1643
1649
  - cloud_client (Optional[TinyB]): The TinyB client instance for the cloud environment.
1644
1650
  - local_client (Optional[TinyB]): The TinyB client instance for the local environment.
1645
1651
  """
1652
+ bucket_name: Optional[str]
1653
+ table_name: Optional[str] = None
1654
+ region_prompt: str
1646
1655
  if service == DataConnectorType.AMAZON_DYNAMODB:
1647
- raise NotImplementedError("DynamoDB is not supported")
1648
-
1649
- bucket_name = click.prompt(
1650
- FeedbackManager.highlight(
1651
- message="? Bucket name (specific name recommended, use '*' for unrestricted access in IAM policy)"
1652
- ),
1653
- prompt_suffix="\n> ",
1654
- )
1655
- validate_string_connector_param("Bucket", bucket_name)
1656
+ table_name = click.prompt(
1657
+ FeedbackManager.highlight(
1658
+ message="? DynamoDB table name (specific name recommended, use '*' for unrestricted access in IAM policy)"
1659
+ ),
1660
+ prompt_suffix="\n> ",
1661
+ )
1662
+ validate_string_connector_param("DynamoDB table name", table_name)
1663
+ bucket_name = click.prompt(
1664
+ FeedbackManager.highlight(
1665
+ message="? Export bucket name (specific name recommended, use '*' for unrestricted access in IAM policy)"
1666
+ ),
1667
+ prompt_suffix="\n> ",
1668
+ )
1669
+ validate_string_connector_param("Export bucket name", bucket_name)
1670
+ region_prompt = "? Region (the region where the DynamoDB table is located)"
1671
+ else:
1672
+ bucket_name = click.prompt(
1673
+ FeedbackManager.highlight(
1674
+ message="? Bucket name (specific name recommended, use '*' for unrestricted access in IAM policy)"
1675
+ ),
1676
+ prompt_suffix="\n> ",
1677
+ )
1678
+ validate_string_connector_param("Bucket", bucket_name)
1679
+ region_prompt = "? Region (the region where the bucket is located)"
1656
1680
 
1657
1681
  region = click.prompt(
1658
- FeedbackManager.highlight(message="? Region (the region where the bucket is located)"),
1682
+ FeedbackManager.highlight(message=region_prompt),
1659
1683
  default="us-east-1",
1660
1684
  show_default=True,
1661
1685
  prompt_suffix="\n> ",
@@ -1738,15 +1762,27 @@ def run_aws_iamrole_connection_flow(
1738
1762
  # Use cloud_client as the main client if local is unavailable
1739
1763
  policy_client = cloud_client if local_unavailable and cloud_client else client
1740
1764
 
1741
- access_policy, trust_policy, _ = get_aws_iamrole_policies(
1742
- policy_client,
1743
- service=service,
1744
- policy=policy,
1745
- bucket=bucket_name,
1746
- external_id_seed=connection_name,
1747
- cloud_client=cloud_client if not local_unavailable else None,
1748
- local_client=local_client,
1749
- )
1765
+ if table_name is not None:
1766
+ access_policy, trust_policy, _ = get_aws_iamrole_policies(
1767
+ policy_client,
1768
+ service=service,
1769
+ policy=policy,
1770
+ bucket=bucket_name,
1771
+ table_name=table_name,
1772
+ external_id_seed=connection_name,
1773
+ cloud_client=cloud_client if not local_unavailable else None,
1774
+ local_client=local_client,
1775
+ )
1776
+ else:
1777
+ access_policy, trust_policy, _ = get_aws_iamrole_policies(
1778
+ policy_client,
1779
+ service=service,
1780
+ policy=policy,
1781
+ bucket=bucket_name,
1782
+ external_id_seed=connection_name,
1783
+ cloud_client=cloud_client if not local_unavailable else None,
1784
+ local_client=local_client,
1785
+ )
1750
1786
 
1751
1787
  click.echo(FeedbackManager.gray(message="\n» Step 1: AWS Authentication"))
1752
1788
  click.echo(
@@ -1756,7 +1792,10 @@ def run_aws_iamrole_connection_flow(
1756
1792
  )
1757
1793
  click.echo(
1758
1794
  FeedbackManager.info(
1759
- message="You'll be creating a single IAM Policy and Role to access your S3 data. Using IAM Roles improves security by providing temporary credentials and following least privilege principles."
1795
+ message=(
1796
+ "You'll be creating a single IAM Policy and Role to access your data. "
1797
+ "Using IAM Roles improves security by providing temporary credentials and following least privilege principles."
1798
+ )
1760
1799
  )
1761
1800
  )
1762
1801
  click.echo(FeedbackManager.click_enter_to_continue())
@@ -1781,7 +1820,7 @@ def run_aws_iamrole_connection_flow(
1781
1820
  click.echo(FeedbackManager.info(message="3. Copy and paste the following policy:"))
1782
1821
  click.echo(FeedbackManager.highlight(message=f"\n{access_policy}\n"))
1783
1822
  click.echo(
1784
- FeedbackManager.info(message=f"4. Name the policy something meaningful (e.g., TinybirdS3Access-{bucket_name})")
1823
+ FeedbackManager.info(message=f"4. Name the policy something meaningful (e.g., TinybirdAccess-{bucket_name})")
1785
1824
  )
1786
1825
  click.echo(FeedbackManager.info(message="5. Click 'Create policy'"))
1787
1826
  click.echo(FeedbackManager.click_enter_to_continue())
@@ -1807,7 +1846,7 @@ def run_aws_iamrole_connection_flow(
1807
1846
  click.echo(FeedbackManager.highlight(message=f"\n{trust_policy}\n"))
1808
1847
  click.echo(FeedbackManager.info(message="4. Click Next, search for and select the policy you just created"))
1809
1848
  click.echo(
1810
- FeedbackManager.info(message=f"5. Name the role something meaningful (e.g., TinybirdS3Role-{bucket_name})")
1849
+ FeedbackManager.info(message=f"5. Name the role something meaningful (e.g., TinybirdRole-{bucket_name})")
1811
1850
  )
1812
1851
  click.echo(FeedbackManager.info(message="6. Click 'Create role'"))
1813
1852
  click.echo(FeedbackManager.info(message="7. Copy the Role ARN from the role details page"))
@@ -1954,6 +1993,7 @@ def get_aws_iamrole_policies(
1954
1993
  service: str,
1955
1994
  policy: str = "write",
1956
1995
  bucket: Optional[str] = None,
1996
+ table_name: Optional[str] = None,
1957
1997
  external_id_seed: Optional[str] = None,
1958
1998
  cloud_client: Optional[TinyB] = None,
1959
1999
  local_client: Optional[TinyB] = None,
@@ -1979,7 +2019,7 @@ def get_aws_iamrole_policies(
1979
2019
  if policy == "write":
1980
2020
  access_policy = client.get_access_write_policy(service, bucket)
1981
2021
  elif policy == "read":
1982
- access_policy = client.get_access_read_policy(service, bucket)
2022
+ access_policy = client.get_access_read_policy(service, bucket, table_name=table_name)
1983
2023
  else:
1984
2024
  raise Exception(f"Access policy {policy} not supported. Choose from 'read' or 'write'")
1985
2025
  if not len(access_policy) > 0:
@@ -20,6 +20,7 @@ from tinybird.tb.modules.common import (
20
20
  get_gcs_svc_account_creds,
21
21
  run_gcp_svc_account_connection_flow,
22
22
  )
23
+ from tinybird.tb.modules.connection_dynamodb import connection_create_dynamodb
23
24
  from tinybird.tb.modules.connection_kafka import (
24
25
  connection_create_kafka,
25
26
  echo_kafka_data,
@@ -248,6 +249,51 @@ def connection_create_gcs(ctx: Context) -> None:
248
249
  )
249
250
 
250
251
 
252
+ @connection_create.command(
253
+ name="dynamodb", short_help="Creates a AWS DynamoDB connection using IAM role authentication."
254
+ )
255
+ @click.option("--connection-name", default=None, help="The name of the connection to identify it in Tinybird")
256
+ @click.option("--table-arn", default=None, help="Optional. Validate the connection against this DynamoDB table ARN")
257
+ @click.pass_context
258
+ def connection_create_dynamodb_cmd(ctx: Context, connection_name: Optional[str], table_arn: Optional[str]) -> None:
259
+ """
260
+ Creates a AWS DynamoDB connection using IAM role authentication.
261
+
262
+ \b
263
+ $ tb connection create dynamodb
264
+ """
265
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
266
+ project: Project = obj["project"]
267
+ client: TinyB = obj["client"]
268
+ env: str = obj["env"]
269
+ config = obj["config"]
270
+
271
+ local_aws_unavailable = env == "local" and not client.check_aws_credentials()
272
+
273
+ if env == "local" and not local_aws_unavailable:
274
+ click.echo(FeedbackManager.gray(message="» Building project before continue..."))
275
+ error = build_project(project=project, tb_client=client, watch=False, config=config, silent=True)
276
+ if error:
277
+ click.echo(FeedbackManager.error(message=error))
278
+ else:
279
+ click.echo(FeedbackManager.success(message="✓ Build completed"))
280
+
281
+ result = connection_create_dynamodb(ctx, connection_name=connection_name, table_arn=table_arn)
282
+
283
+ if env == "local" and not local_aws_unavailable and not result["error"]:
284
+ click.echo(FeedbackManager.gray(message="» Building project to access the new connection..."))
285
+ error = build_project(project=project, tb_client=client, watch=False, config=config, silent=True)
286
+ if error:
287
+ click.echo(FeedbackManager.error(message=error))
288
+ else:
289
+ click.echo(FeedbackManager.success(message="✓ Build completed"))
290
+
291
+ # connection_create_dynamodb already prints the failure details; exit non-zero so scripted/CI
292
+ # usage does not treat a connection with missing secrets as a success.
293
+ if result["error"]:
294
+ ctx.exit(1)
295
+
296
+
251
297
  @connection_create.command(name="kafka", help="Create a Kafka connection.")
252
298
  @click.option("--connection-name", default=None, help="The name of the connection to identify it in Tinybird")
253
299
  @click.option("--bootstrap-servers", default=None, help="Kafka Bootstrap Server in form mykafka.mycloud.com:9092")