tinybird-cli 5.2.2.dev2__tar.gz → 5.2.2.dev4__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 (46) hide show
  1. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/PKG-INFO +14 -1
  2. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/client.py +17 -9
  4. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/datafile.py +30 -3
  5. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/feedback_manager.py +15 -2
  6. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/common.py +192 -0
  7. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/connection.py +69 -158
  8. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/PKG-INFO +14 -1
  9. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/setup.cfg +0 -0
  10. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/ch_utils/constants.py +0 -0
  11. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/ch_utils/engine.py +0 -0
  12. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/check_pypi.py +0 -0
  13. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/config.py +0 -0
  14. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/connectors.py +0 -0
  15. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/context.py +0 -0
  16. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/datatypes.py +0 -0
  17. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/git_settings.py +0 -0
  18. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/sql.py +0 -0
  19. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/sql_template.py +0 -0
  20. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/sql_template_fmt.py +0 -0
  21. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/sql_toolset.py +0 -0
  22. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/syncasync.py +0 -0
  23. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli.py +0 -0
  24. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/auth.py +0 -0
  25. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/branch.py +0 -0
  26. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/cicd.py +0 -0
  27. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/cli.py +0 -0
  28. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/config.py +0 -0
  29. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/datasource.py +0 -0
  30. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/exceptions.py +0 -0
  31. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/job.py +0 -0
  32. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/pipe.py +0 -0
  33. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/regions.py +0 -0
  34. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/telemetry.py +0 -0
  35. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/test.py +0 -0
  36. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  37. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  38. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/token.py +0 -0
  39. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/workspace.py +0 -0
  40. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  41. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird/tornado_template.py +0 -0
  42. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  43. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  44. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/entry_points.txt +0 -0
  45. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/requires.txt +0 -0
  46. {tinybird-cli-5.2.2.dev2 → tinybird-cli-5.2.2.dev4}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.2.2.dev2
3
+ Version: 5.2.2.dev4
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,12 +18,25 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+
22
+ 5.2.2.dev4
23
+ **********
24
+
25
+ - `Added` `tb push` now supports `dynamodb` as service type
26
+
27
+ 5.2.2.dev3
28
+ **********
29
+
30
+ - `Added` `tb connection create` now supports `dynamodb` as service type
31
+
21
32
  5.2.2.dev2
22
33
  **********
34
+
23
35
  - `Added` error when trying to push a data source with `SETTINGS` instead of `ENGINE_SETTINGS`
24
36
 
25
37
  5.2.2.dev1
26
38
  **********
39
+
27
40
  - `Added` support for buckets with gzip files when creating S3 Data Sources
28
41
 
29
42
  5.2.1
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '5.2.2.dev2'
8
- __revision__ = '372e585'
7
+ __version__ = '5.2.2.dev4'
8
+ __revision__ = '06332a3'
@@ -260,10 +260,14 @@ class TinyB(object):
260
260
  response = await self._req(f"/v0/connectors?{urlencode(params)}")
261
261
  return response["connectors"]
262
262
 
263
- async def connections(self, connector: Optional[str] = None):
263
+ async def connections(self, connector: Optional[str] = None, skip_bigquery: Optional[bool] = False):
264
264
  response = await self._req("/v0/connectors")
265
265
  connectors = response["connectors"]
266
- bigquery_connection = await self.bigquery_connection() if connector == "bigquery" or connector is None else None
266
+ bigquery_connection = None
267
+ if not skip_bigquery:
268
+ bigquery_connection = (
269
+ await self.bigquery_connection() if connector == "bigquery" or connector is None else None
270
+ )
267
271
  connectors = connectors + [bigquery_connection] if bigquery_connection else connectors
268
272
  if connector:
269
273
  return [
@@ -885,8 +889,12 @@ class TinyB(object):
885
889
  name_or_id: str,
886
890
  service: Optional[str] = None,
887
891
  key: Optional[str] = "name",
892
+ skip_bigquery: Optional[bool] = False,
888
893
  ) -> Optional[Dict[str, Any]]:
889
- return next((c for c in await self.connections(connector=service) if c[key] == name_or_id), None)
894
+ return next(
895
+ (c for c in await self.connections(connector=service, skip_bigquery=skip_bigquery) if c[key] == name_or_id),
896
+ None,
897
+ )
890
898
 
891
899
  async def get_connector_by_id(self, connector_id: Optional[str] = None):
892
900
  return await self._req(f"/v0/connectors/{connector_id}")
@@ -1011,14 +1019,14 @@ class TinyB(object):
1011
1019
  response = await self._req(f"/v0/connectors/snowflake/warehouses?{urlencode(params)}", method="POST", data="")
1012
1020
  return response["warehouses"]
1013
1021
 
1014
- async def get_s3_trust_policy(self) -> Dict[str, Any]:
1015
- return await self._req("/v0/integrations/s3/policies/trust-policy")
1022
+ async def get_trust_policy(self, service: str) -> Dict[str, Any]:
1023
+ return await self._req(f"/v0/integrations/{service}/policies/trust-policy")
1016
1024
 
1017
- async def get_s3_access_write_policy(self) -> Dict[str, Any]:
1018
- return await self._req("/v0/integrations/s3/policies/write-access-policy")
1025
+ async def get_access_write_policy(self, service: str) -> Dict[str, Any]:
1026
+ return await self._req(f"/v0/integrations/{service}/policies/write-access-policy")
1019
1027
 
1020
- async def get_s3_access_read_policy(self) -> Dict[str, Any]:
1021
- return await self._req("/v0/integrations/s3/policies/read-access-policy")
1028
+ async def get_access_read_policy(self, service: str) -> Dict[str, Any]:
1029
+ return await self._req(f"/v0/integrations/{service}/policies/read-access-policy")
1022
1030
 
1023
1031
  async def sql_get_format(self, sql: str, with_clickhouse_format: bool = False) -> str:
1024
1032
  try:
@@ -146,6 +146,8 @@ class ImportReplacements:
146
146
  ("import_connector", "connector", None),
147
147
  ("import_external_datasource", "external_data_source", None),
148
148
  ("import_bucket_uri", "bucket_uri", None),
149
+ ("import_table_arn", "dynamodb_table_arn", None),
150
+ ("import_export_bucket", "dynamodb_export_bucket", None),
149
151
  )
150
152
 
151
153
  @staticmethod
@@ -1115,6 +1117,8 @@ def parse(
1115
1117
  "import_external_datasource": assign_var("import_external_datasource"),
1116
1118
  "import_bucket_uri": assign_var("import_bucket_uri"),
1117
1119
  "import_query": assign_var("import_query"),
1120
+ "import_table_arn": assign_var("import_table_arn"),
1121
+ "import_export_bucket": assign_var("import_export_bucket"),
1118
1122
  "shared_with": shared_with,
1119
1123
  "export_service": assign_var("export_service"),
1120
1124
  "export_connection_name": assign_var("export_connection_name"),
@@ -1319,6 +1323,11 @@ async def process_file(
1319
1323
  if service in PREVIEW_CONNECTOR_SERVICES:
1320
1324
  if not params.get("import_bucket_uri", None):
1321
1325
  raise click.ClickException(FeedbackManager.error_missing_bucket_uri(datasource=datasource["name"]))
1326
+ elif service == "dynamodb":
1327
+ if not params.get("import_table_arn", None):
1328
+ raise click.ClickException(FeedbackManager.error_missing_table_arn(datasource=datasource["name"]))
1329
+ if not params.get("import_export_bucket", None):
1330
+ raise click.ClickException(FeedbackManager.error_missing_export_bucket(datasource=datasource["name"]))
1322
1331
  else:
1323
1332
  if not params.get("import_external_datasource", None):
1324
1333
  raise click.ClickException(
@@ -1388,7 +1397,7 @@ async def process_file(
1388
1397
  params.update(get_engine_params(node))
1389
1398
 
1390
1399
  if "import_service" in node or "import_connection_name" in node:
1391
- VALID_SERVICES: Tuple[str, ...] = ("bigquery", "snowflake", "s3", "s3_iamrole", "gcs")
1400
+ VALID_SERVICES: Tuple[str, ...] = ("bigquery", "snowflake", "s3", "s3_iamrole", "gcs", "dynamodb")
1392
1401
 
1393
1402
  import_params = await get_import_params(params, node)
1394
1403
 
@@ -2958,7 +2967,16 @@ async def new_ds(
2958
2967
  except DoesNotExistException:
2959
2968
  datasource_exists = False
2960
2969
 
2961
- if ds["params"].get("engine", "").lower() == "join":
2970
+ engine_param = ds["params"].get("engine", "")
2971
+
2972
+ if (
2973
+ ds["params"].get("service") == "dynamodb"
2974
+ and engine_param != ""
2975
+ and engine_param.lower() != "replacingmergetree"
2976
+ ):
2977
+ raise click.ClickException(FeedbackManager.error_dynamodb_engine_not_supported(engine=engine_param))
2978
+
2979
+ if engine_param.lower() == "join":
2962
2980
  deprecation_notice = FeedbackManager.warning_deprecated(
2963
2981
  warning="Data Sources with Join engine are deprecated and will be removed in the next major release of tinybird-cli. Use MergeTree instead."
2964
2982
  )
@@ -2988,8 +3006,17 @@ async def new_ds(
2988
3006
  FeedbackManager.error_format(extension=extension, valid_formats=valid_formats)
2989
3007
  )
2990
3008
  params["format"] = extension
3009
+ datasource_response = await client.datasource_create_from_definition(params)
3010
+ datasource = datasource_response.get("datasource", {})
3011
+
3012
+ if datasource.get("service") == "dynamodb":
3013
+ job_id = datasource_response.get("import_id", None)
3014
+ if job_id:
3015
+ jobs = await client.jobs(status=["waiting", "working"])
3016
+ job_url = next((job["job_url"] for job in jobs if job["id"] == job_id), None)
3017
+ if job_url:
3018
+ click.echo(FeedbackManager.success_dynamodb_initial_load(job_url=job_url))
2991
3019
 
2992
- datasource = (await client.datasource_create_from_definition(params)).get("datasource", {})
2993
3020
  if "tokens" in ds and ds["tokens"]:
2994
3021
  await manage_tokens()
2995
3022
 
@@ -119,6 +119,9 @@ class FeedbackManager:
119
119
  error_file_extension = error_message(
120
120
  "File extension for {filename} not supported. It should be one of .datasource or .pipe"
121
121
  )
122
+ error_dynamodb_engine_not_supported = error_message(
123
+ "Engine {engine} not supported for DynamoDB Data Sources. Only ReplacingMergeTree is supported."
124
+ )
122
125
  error_format = error_message("Format {extension} not supported. It should be one of {valid_formats}")
123
126
  error_remove_endpoint = error_message("Failed removing pipe endpoint {error}")
124
127
  error_remove_no_endpoint = error_message("Pipe does not have any endpoint")
@@ -296,6 +299,12 @@ class FeedbackManager:
296
299
  error_missing_bucket_uri = error_message(
297
300
  "Missing IMPORT_BUCKET_URI in '{datasource}'.\n** See https://www.tinybird.co/docs/ingest/s3 to learn more."
298
301
  )
302
+ error_missing_table_arn = error_message(
303
+ "Missing IMPORT_TABLE_ARN in '{datasource}'.\n** See https://www.tinybird.co/docs/ingest/dynamodb to learn more."
304
+ )
305
+ error_missing_export_bucket = error_message(
306
+ "Missing IMPORT_EXPORT_BUCKET in '{datasource}'.\n** See https://www.tinybird.co/docs/ingest/dynamodb to learn more."
307
+ )
299
308
  error_some_data_validation_have_failed = error_message("The data validation has failed")
300
309
  error_some_tests_have_errors = error_message("Tests with errors")
301
310
  error_regression_yaml_not_valid = error_message(
@@ -431,7 +440,7 @@ Ready? """
431
440
 
432
441
  prompt_s3_iamrole_connection_login_aws = prompt_message("""[1] Log into your AWS Console\n\n""")
433
442
  prompt_s3_iamrole_connection_policy = prompt_message(
434
- """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, replace <bucket> with your bucket name:\n\n{access_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
443
+ """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, replace {replacements}:\n\n{access_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
435
444
  )
436
445
  prompt_s3_iamrole_connection_policy_not_copied = prompt_message(
437
446
  """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, copy this policy and replace <bucket> with your bucket name:\n\n{access_policy}\n\n"""
@@ -476,6 +485,7 @@ Ready? """
476
485
  )
477
486
  info_creating_kafka_connection = info_message("** Creating new Kafka connection '{connection_name}'")
478
487
  info_creating_s3_iamrole_connection = info_message("** Creating new S3 IAM Role connection '{connection_name}'")
488
+ info_creating_dynamodb_connection = info_message("** Creating new DynamoDB connection '{connection_name}'")
479
489
 
480
490
  warning_remove_oldest_rollback = warning_message(
481
491
  "** [WARNING] Will try to remove oldest rollback Release before promoting to live Release {semver}."
@@ -881,6 +891,7 @@ Ready? """
881
891
  success_print_pipe = success_message("** Pipe: {pipe}")
882
892
  success_create = success_message("** '{name}' created")
883
893
  success_delete = success_message("** '{name}' deleted")
894
+ success_dynamodb_initial_load = success_message("** Initial load of DynamoDB table started: {job_url}")
884
895
  success_progress_blocks = success_message("** \N{front-facing baby chick} done")
885
896
  success_now_using_config = success_message("** Now using {name} ({id})")
886
897
  success_connector_config = success_message(
@@ -910,7 +921,9 @@ Ready? """
910
921
  success_s3_iam_connection_created = success_message(
911
922
  "** Info associated with this connection:\n** External ID: {external_id}\n** Role ARN: {role_arn}"
912
923
  )
913
-
924
+ success_dynamodb_connection_created = success_message(
925
+ "** Info associated with this DynamoDB connection:\n** Region: {region}\n** Role ARN: {role_arn}"
926
+ )
914
927
  success_delete_connection = success_message("** Connection {connection_id} removed successfully")
915
928
  success_connection_using = success_message("** Using connection '{connection_name}'")
916
929
  success_using_host = success_message("** Using host: {host} ({name})")
@@ -7,6 +7,7 @@
7
7
 
8
8
  import asyncio
9
9
  import json
10
+ import os
10
11
  import re
11
12
  import socket
12
13
  import sys
@@ -24,6 +25,7 @@ import click
24
25
  import click.formatting
25
26
  import humanfriendly
26
27
  import humanfriendly.tables
28
+ import pyperclip
27
29
  from click import Context
28
30
  from click._termui_impl import ProgressBar
29
31
  from humanfriendly.tables import format_pretty_table
@@ -1659,6 +1661,12 @@ class ConnectionReplacements:
1659
1661
  "private_key_id": "gcs_private_key_id",
1660
1662
  "connection_name": "name",
1661
1663
  },
1664
+ "dynamodb": {
1665
+ "service": "service",
1666
+ "connection_name": "name",
1667
+ "role_arn": "dynamodb_iamrole_arn",
1668
+ "region": "dynamodb_iamrole_region",
1669
+ },
1662
1670
  }
1663
1671
 
1664
1672
  @staticmethod
@@ -2109,3 +2117,187 @@ async def remove_release(
2109
2117
  click.echo(FeedbackManager.success_release_delete(semver=response.get("semver")))
2110
2118
  else:
2111
2119
  click.echo(FeedbackManager.info_no_release_deleted())
2120
+
2121
+
2122
+ async def validate_aws_iamrole_integration(
2123
+ client: TinyB,
2124
+ service: str,
2125
+ role_arn: Optional[str],
2126
+ region: Optional[str],
2127
+ policy: str = "write",
2128
+ no_validate: Optional[bool] = False,
2129
+ ):
2130
+ if no_validate is False:
2131
+ access_policy, trust_policy, external_id = await get_aws_iamrole_policies(
2132
+ client, service=service, policy=policy
2133
+ )
2134
+
2135
+ if not role_arn:
2136
+ if not click.confirm(
2137
+ FeedbackManager.prompt_s3_iamrole_connection_login_aws(),
2138
+ show_default=False,
2139
+ prompt_suffix="Press y to continue:",
2140
+ ):
2141
+ sys.exit(1)
2142
+
2143
+ access_policy_copied = True
2144
+ try:
2145
+ pyperclip.copy(access_policy)
2146
+ except Exception:
2147
+ access_policy_copied = False
2148
+
2149
+ replacements_dict = {
2150
+ "<bucket>": "<bucket> with your bucket name",
2151
+ "<table_name>": "<table_name> with your DynamoDB table name",
2152
+ }
2153
+
2154
+ replacements = [
2155
+ replacements_dict.get(replacement, "")
2156
+ for replacement in replacements_dict.keys()
2157
+ if replacement in access_policy
2158
+ ]
2159
+
2160
+ if not click.confirm(
2161
+ (
2162
+ FeedbackManager.prompt_s3_iamrole_connection_policy(
2163
+ access_policy=access_policy, replacements=", ".join(replacements)
2164
+ )
2165
+ if access_policy_copied
2166
+ else FeedbackManager.prompt_s3_iamrole_connection_policy_not_copied(access_policy=access_policy)
2167
+ ),
2168
+ show_default=False,
2169
+ prompt_suffix="Press y to continue:",
2170
+ ):
2171
+ sys.exit(1)
2172
+
2173
+ trust_policy_copied = True
2174
+ try:
2175
+ pyperclip.copy(trust_policy)
2176
+ except Exception:
2177
+ trust_policy_copied = False
2178
+
2179
+ if not click.confirm(
2180
+ (
2181
+ FeedbackManager.prompt_s3_iamrole_connection_role(trust_policy=trust_policy)
2182
+ if trust_policy_copied
2183
+ else FeedbackManager.prompt_s3_iamrole_connection_role_not_copied(trust_policy=trust_policy)
2184
+ ),
2185
+ show_default=False,
2186
+ prompt_suffix="Press y to continue:",
2187
+ ):
2188
+ sys.exit(1)
2189
+ else:
2190
+ try:
2191
+ trust_policy = await client.get_trust_policy(service)
2192
+ external_id = trust_policy["Statement"][0]["Condition"]["StringEquals"]["sts:ExternalId"]
2193
+ except Exception:
2194
+ external_id = ""
2195
+
2196
+ if not role_arn:
2197
+ role_arn = click.prompt("Enter the ARN of the role you just created")
2198
+ validate_string_connector_param("Role ARN", role_arn)
2199
+
2200
+ if not region:
2201
+ region_resource = "table" if service == DataConnectorType.AMAZON_DYNAMODB else "bucket"
2202
+ region = click.prompt(f"Enter the region where the {region_resource} is located")
2203
+ validate_string_connector_param("Region", region)
2204
+
2205
+ return role_arn, region, external_id
2206
+
2207
+
2208
+ async def get_aws_iamrole_policies(client: TinyB, service: str, policy: str = "write"):
2209
+ access_policy: Dict[str, Any] = {}
2210
+ if service == DataConnectorType.AMAZON_S3_IAMROLE:
2211
+ service = DataConnectorType.AMAZON_S3
2212
+ try:
2213
+ if policy == "write":
2214
+ access_policy = await client.get_access_write_policy(service)
2215
+ elif policy == "read":
2216
+ access_policy = await client.get_access_read_policy(service)
2217
+ else:
2218
+ raise Exception(f"Access policy {policy} not supported. Choose from 'read' or 'write'")
2219
+ if not len(access_policy) > 0:
2220
+ raise Exception(f"{service.upper()} Integration not supported in this region")
2221
+ except Exception as e:
2222
+ raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
2223
+
2224
+ trust_policy: Dict[str, Any] = {}
2225
+ try:
2226
+ trust_policy = await client.get_trust_policy(service)
2227
+ if not len(trust_policy) > 0:
2228
+ raise Exception(f"{service.upper()} Integration not supported in this region")
2229
+ except Exception as e:
2230
+ raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
2231
+ try:
2232
+ external_id = trust_policy["Statement"][0]["Condition"]["StringEquals"]["sts:ExternalId"]
2233
+ except Exception:
2234
+ external_id = ""
2235
+ return json.dumps(access_policy, indent=4), json.dumps(trust_policy, indent=4), external_id
2236
+
2237
+
2238
+ async def validate_aws_iamrole_connection_name(
2239
+ client: TinyB, connection_name: Optional[str], no_validate: Optional[bool] = False
2240
+ ) -> str:
2241
+ if connection_name and no_validate is False:
2242
+ if await client.get_connector(connection_name, skip_bigquery=True) is not None:
2243
+ raise CLIConnectionException(FeedbackManager.info_connection_already_exists(name=connection_name))
2244
+ else:
2245
+ while not connection_name:
2246
+ connection_name = click.prompt("Enter the name for this connection", default=None, show_default=False)
2247
+ assert isinstance(connection_name, str)
2248
+
2249
+ if no_validate is False:
2250
+ if await client.get_connector(connection_name) is not None:
2251
+ click.echo(FeedbackManager.info_connection_already_exists(name=connection_name))
2252
+ connection_name = None
2253
+ assert isinstance(connection_name, str)
2254
+ return connection_name
2255
+
2256
+
2257
+ class DataConnectorType(str, Enum):
2258
+ KAFKA = "kafka"
2259
+ GCLOUD_SCHEDULER = "gcscheduler"
2260
+ SNOWFLAKE = "snowflake"
2261
+ BIGQUERY = "bigquery"
2262
+ GCLOUD_STORAGE = "gcs"
2263
+ GCLOUD_STORAGE_HMAC = "gcs_hmac"
2264
+ GCLOUD_STORAGE_SA = "gcs_service_account"
2265
+ AMAZON_S3 = "s3"
2266
+ AMAZON_S3_IAMROLE = "s3_iamrole"
2267
+ AMAZON_DYNAMODB = "dynamodb"
2268
+
2269
+ def __str__(self) -> str:
2270
+ return self.value
2271
+
2272
+
2273
+ async def create_aws_iamrole_connection(client: TinyB, service: str, connection_name, role_arn, region) -> None:
2274
+ conn_file_name = f"{connection_name}.connection"
2275
+ conn_file_path = Path(getcwd(), conn_file_name)
2276
+
2277
+ if os.path.isfile(conn_file_path):
2278
+ raise CLIConnectionException(FeedbackManager.error_connection_file_already_exists(name=conn_file_name))
2279
+
2280
+ if service == DataConnectorType.AMAZON_S3_IAMROLE:
2281
+ click.echo(FeedbackManager.info_creating_s3_iamrole_connection(connection_name=connection_name))
2282
+ if service == DataConnectorType.AMAZON_DYNAMODB:
2283
+ click.echo(FeedbackManager.info_creating_dynamodb_connection(connection_name=connection_name))
2284
+
2285
+ params = ConnectionReplacements.map_api_params_from_prompt_params(
2286
+ service, connection_name=connection_name, role_arn=role_arn, region=region
2287
+ )
2288
+
2289
+ click.echo("** Creating connection...")
2290
+ try:
2291
+ _ = await client.connection_create(params)
2292
+ except Exception as e:
2293
+ raise CLIConnectionException(
2294
+ FeedbackManager.error_connection_create(connection_name=connection_name, error=str(e))
2295
+ )
2296
+
2297
+ with open(conn_file_path, "w") as f:
2298
+ f.write(
2299
+ f"""TYPE {service}
2300
+
2301
+ """
2302
+ )
2303
+ click.echo(FeedbackManager.success_connection_file_created(name=conn_file_name))
@@ -3,16 +3,12 @@
3
3
  # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
4
  # - But please, **do not** interleave utility functions and command definitions.
5
5
 
6
- import json
7
6
  import os
8
- import sys
9
- from enum import Enum
10
7
  from os import getcwd
11
8
  from pathlib import Path
12
9
  from typing import Any, Dict, List, Optional
13
10
 
14
11
  import click
15
- import pyperclip
16
12
  from click import Context
17
13
 
18
14
  from tinybird.client import DoesNotExistException, TinyB
@@ -20,9 +16,13 @@ from tinybird.feedback_manager import FeedbackManager
20
16
  from tinybird.tb_cli_modules.cli import cli
21
17
  from tinybird.tb_cli_modules.common import (
22
18
  ConnectionReplacements,
19
+ DataConnectorType,
23
20
  _get_setting_value,
24
21
  coro,
22
+ create_aws_iamrole_connection,
25
23
  echo_safe_humanfriendly_tables_format_smart_table,
24
+ validate_aws_iamrole_connection_name,
25
+ validate_aws_iamrole_integration,
26
26
  validate_connection_name,
27
27
  validate_kafka_auto_offset_reset,
28
28
  validate_kafka_bootstrap_servers,
@@ -34,22 +34,6 @@ from tinybird.tb_cli_modules.common import (
34
34
  from tinybird.tb_cli_modules.exceptions import CLIConnectionException
35
35
  from tinybird.tb_cli_modules.telemetry import is_ci_environment
36
36
 
37
-
38
- class DataConnectorType(str, Enum):
39
- KAFKA = "kafka"
40
- GCLOUD_SCHEDULER = "gcscheduler"
41
- SNOWFLAKE = "snowflake"
42
- BIGQUERY = "bigquery"
43
- GCLOUD_STORAGE = "gcs"
44
- GCLOUD_STORAGE_HMAC = "gcs_hmac"
45
- GCLOUD_STORAGE_SA = "gcs_service_account"
46
- AMAZON_S3 = "s3"
47
- AMAZON_S3_IAMROLE = "s3_iamrole"
48
-
49
- def __str__(self) -> str:
50
- return self.value
51
-
52
-
53
37
  DATA_CONNECTOR_SETTINGS: Dict[DataConnectorType, List[str]] = {
54
38
  DataConnectorType.KAFKA: [
55
39
  "kafka_bootstrap_servers",
@@ -96,6 +80,11 @@ DATA_CONNECTOR_SETTINGS: Dict[DataConnectorType, List[str]] = {
96
80
  "s3_iamrole_region",
97
81
  "s3_iamrole_external_id",
98
82
  ],
83
+ DataConnectorType.AMAZON_DYNAMODB: [
84
+ "dynamodb_iamrole_arn",
85
+ "dynamodb_iamrole_region",
86
+ "dynamodb_iamrole_external_id",
87
+ ],
99
88
  }
100
89
 
101
90
  SENSITIVE_CONNECTOR_SETTINGS = {
@@ -108,6 +97,7 @@ SENSITIVE_CONNECTOR_SETTINGS = {
108
97
  DataConnectorType.GCLOUD_STORAGE_HMAC: ["gcs_hmac_secret"],
109
98
  DataConnectorType.AMAZON_S3: ["s3_secret_access_key"],
110
99
  DataConnectorType.AMAZON_S3_IAMROLE: ["s3_iamrole_arn"],
100
+ DataConnectorType.AMAZON_DYNAMODB: ["dynamodb_iamrole_arn"],
111
101
  }
112
102
 
113
103
 
@@ -735,147 +725,68 @@ async def connection_create_s3_iamrole(
735
725
 
736
726
  obj: Dict[str, Any] = ctx.ensure_object(dict)
737
727
  client: TinyB = obj["client"]
738
-
739
- async def get_s3_policies():
740
- s3_access_policy: Dict[str, Any] = {}
741
- try:
742
- if policy == "write":
743
- s3_access_policy = await client.get_s3_access_write_policy()
744
- elif policy == "read":
745
- s3_access_policy = await client.get_s3_access_read_policy()
746
- else:
747
- raise Exception(f"Access policy {policy} not supported. Choose from 'read' or 'write'")
748
- if not len(s3_access_policy) > 0:
749
- raise Exception("S3 Integration not supported in this region")
750
- except Exception as e:
751
- raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
752
-
753
- s3_trust_policy: Dict[str, Any] = {}
754
- try:
755
- s3_trust_policy = await client.get_s3_trust_policy()
756
- if not len(s3_trust_policy) > 0:
757
- raise Exception("S3 Integration not supported in this region")
758
- except Exception as e:
759
- raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
760
- try:
761
- external_id = s3_trust_policy["Statement"][0]["Condition"]["StringEquals"]["sts:ExternalId"]
762
- except Exception:
763
- external_id = ""
764
- return json.dumps(s3_access_policy, indent=4), json.dumps(s3_trust_policy, indent=4), external_id
765
-
766
- async def validate_s3_integration(role_arn: Optional[str], region: Optional[str]):
767
- if no_validate is False:
768
- access_policy, trust_policy, external_id = await get_s3_policies()
769
-
770
- if not role_arn:
771
- if not click.confirm(
772
- FeedbackManager.prompt_s3_iamrole_connection_login_aws(),
773
- show_default=False,
774
- prompt_suffix="Press y to continue:",
775
- ):
776
- sys.exit(1)
777
-
778
- access_policy_copied = True
779
- try:
780
- pyperclip.copy(access_policy)
781
- except Exception:
782
- access_policy_copied = False
783
-
784
- if not click.confirm(
785
- (
786
- FeedbackManager.prompt_s3_iamrole_connection_policy(access_policy=access_policy)
787
- if access_policy_copied
788
- else FeedbackManager.prompt_s3_iamrole_connection_policy_not_copied(access_policy=access_policy)
789
- ),
790
- show_default=False,
791
- prompt_suffix="Press y to continue:",
792
- ):
793
- sys.exit(1)
794
-
795
- trust_policy_copied = True
796
- try:
797
- pyperclip.copy(trust_policy)
798
- except Exception:
799
- trust_policy_copied = False
800
-
801
- if not click.confirm(
802
- (
803
- FeedbackManager.prompt_s3_iamrole_connection_role(trust_policy=trust_policy)
804
- if trust_policy_copied
805
- else FeedbackManager.prompt_s3_iamrole_connection_role_not_copied(trust_policy=trust_policy)
806
- ),
807
- show_default=False,
808
- prompt_suffix="Press y to continue:",
809
- ):
810
- sys.exit(1)
811
- else:
812
- try:
813
- trust_policy = await client.get_s3_trust_policy()
814
- external_id = trust_policy["Statement"][0]["Condition"]["StringEquals"]["sts:ExternalId"]
815
- except Exception:
816
- external_id = ""
817
- if not role_arn:
818
- role_arn = click.prompt("Enter the ARN of the role you just created")
819
- validate_string_connector_param("Role ARN", role_arn)
820
-
821
- if not region:
822
- region = click.prompt("Enter the region where the bucket is located")
823
- validate_string_connector_param("Region", region)
824
-
825
- return role_arn, region, external_id
826
-
827
- async def validate_connection_name(connection_name: Optional[str]) -> str:
828
- if connection_name and no_validate is False:
829
- if await client.get_connector(connection_name) is not None:
830
- raise CLIConnectionException(FeedbackManager.info_connection_already_exists(name=connection_name))
831
- else:
832
- while not connection_name:
833
- connection_name = click.prompt("Enter the name for this connection", default=None, show_default=False)
834
- assert isinstance(connection_name, str)
835
-
836
- if no_validate is False:
837
- if await client.get_connector(connection_name) is not None:
838
- click.echo(FeedbackManager.info_connection_already_exists(name=connection_name))
839
- connection_name = None
840
- assert isinstance(connection_name, str)
841
- return connection_name
842
-
843
- async def create_connection(connection_name, role_arn, region) -> None:
844
- conn_file_name = f"{connection_name}.connection"
845
- conn_file_path = Path(getcwd(), conn_file_name)
846
- service = DataConnectorType.AMAZON_S3_IAMROLE
847
-
848
- if os.path.isfile(conn_file_path):
849
- raise CLIConnectionException(FeedbackManager.error_connection_file_already_exists(name=conn_file_name))
850
-
851
- click.echo(FeedbackManager.info_creating_s3_iamrole_connection(connection_name=connection_name))
852
-
853
- params = ConnectionReplacements.map_api_params_from_prompt_params(
854
- service, connection_name=connection_name, role_arn=role_arn, region=region
855
- )
856
-
857
- click.echo("** Creating connection...")
858
- try:
859
- _ = await client.connection_create(params)
860
- except Exception as e:
861
- raise CLIConnectionException(
862
- FeedbackManager.error_connection_create(connection_name=connection_name, error=str(e))
863
- )
864
-
865
- with open(conn_file_path, "w") as f:
866
- f.write(
867
- f"""TYPE {service}
868
-
869
- """
870
- )
871
- click.echo(FeedbackManager.success_connection_file_created(name=conn_file_name))
872
-
873
- role_arn, region, external_id = await validate_s3_integration(role_arn, region)
874
- connection_name = await validate_connection_name(connection_name)
875
- await create_connection(connection_name, role_arn, region)
728
+ service = DataConnectorType.AMAZON_S3_IAMROLE
729
+ role_arn, region, external_id = await validate_aws_iamrole_integration(
730
+ client,
731
+ service=service,
732
+ role_arn=role_arn,
733
+ region=region,
734
+ policy=policy,
735
+ no_validate=no_validate,
736
+ )
737
+ connection_name = await validate_aws_iamrole_connection_name(client, connection_name, no_validate)
738
+ await create_aws_iamrole_connection(
739
+ client, service=service, connection_name=connection_name, role_arn=role_arn, region=region
740
+ )
876
741
  if external_id:
877
742
  click.echo(
878
743
  FeedbackManager.success_s3_iam_connection_created(
879
744
  connection_name=connection_name, external_id=external_id, role_arn=role_arn
880
745
  )
881
746
  )
747
+
748
+
749
+ @connection_create.command(
750
+ name="dynamodb", short_help="Creates a AWS DynamoDB connection using IAM role authentication", hidden=True
751
+ )
752
+ @click.option("--connection-name", default=None, help="The name of the connection to identify it in Tinybird")
753
+ @click.option("--role-arn", default=None, help="The ARN of the IAM role to use for the connection")
754
+ @click.option("--region", default=None, help="The AWS region where DynamoDB is located")
755
+ @click.option("--no-validate", is_flag=True, default=False, help="Do not validate DynamoDB connection during creation")
756
+ @click.pass_context
757
+ @coro
758
+ async def connection_create_dynamodb(
759
+ ctx: Context,
760
+ connection_name: Optional[str] = "",
761
+ role_arn: Optional[str] = "",
762
+ region: Optional[str] = "",
763
+ no_validate: Optional[bool] = False,
764
+ ) -> None:
765
+ """
766
+ Creates a DynamoDB connection using IAM role authentication in the current workspace
767
+
768
+ \b
769
+ $ tb connection create dynamodb
770
+ """
771
+
772
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
773
+ client: TinyB = obj["client"]
774
+
775
+ service = DataConnectorType.AMAZON_DYNAMODB
776
+ role_arn, region, _external_id = await validate_aws_iamrole_integration(
777
+ client,
778
+ service=service,
779
+ role_arn=role_arn,
780
+ region=region,
781
+ policy="read",
782
+ no_validate=no_validate,
783
+ )
784
+ connection_name = await validate_aws_iamrole_connection_name(client, connection_name, no_validate)
785
+ await create_aws_iamrole_connection(
786
+ client, service=service, connection_name=connection_name, role_arn=role_arn, region=region
787
+ )
788
+ click.echo(
789
+ FeedbackManager.success_dynamodb_connection_created(
790
+ connection_name=connection_name, region=region, role_arn=role_arn
791
+ )
792
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.2.2.dev2
3
+ Version: 5.2.2.dev4
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,12 +18,25 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+
22
+ 5.2.2.dev4
23
+ **********
24
+
25
+ - `Added` `tb push` now supports `dynamodb` as service type
26
+
27
+ 5.2.2.dev3
28
+ **********
29
+
30
+ - `Added` `tb connection create` now supports `dynamodb` as service type
31
+
21
32
  5.2.2.dev2
22
33
  **********
34
+
23
35
  - `Added` error when trying to push a data source with `SETTINGS` instead of `ENGINE_SETTINGS`
24
36
 
25
37
  5.2.2.dev1
26
38
  **********
39
+
27
40
  - `Added` support for buckets with gzip files when creating S3 Data Sources
28
41
 
29
42
  5.2.1