tinybird-cli 5.9.1.dev2__tar.gz → 5.10.0__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 (48) hide show
  1. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/PKG-INFO +5 -3
  2. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/ch_utils/engine.py +21 -1
  4. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/client.py +5 -3
  5. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/datafile.py +29 -20
  6. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/feedback_manager.py +2 -0
  7. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/sql.py +32 -4
  8. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/sql_template.py +26 -9
  9. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/syncasync.py +4 -4
  10. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/cli.py +4 -4
  11. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/common.py +33 -10
  12. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/connection.py +12 -1
  13. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/datasource.py +1 -1
  14. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tornado_template.py +1 -1
  15. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird_cli.egg-info/PKG-INFO +5 -3
  16. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/setup.cfg +0 -0
  17. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/ch_utils/constants.py +0 -0
  18. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/check_pypi.py +0 -0
  19. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/config.py +0 -0
  20. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/connectors.py +0 -0
  21. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/context.py +0 -0
  22. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/datatypes.py +0 -0
  23. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/git_settings.py +0 -0
  24. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/sql_template_fmt.py +0 -0
  25. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/sql_toolset.py +0 -0
  26. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli.py +0 -0
  27. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/auth.py +0 -0
  28. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/branch.py +0 -0
  29. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/cicd.py +0 -0
  30. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/exceptions.py +0 -0
  32. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/fmt.py +0 -0
  33. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/tag.py +0 -0
  37. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  40. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  41. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/token.py +0 -0
  42. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/workspace.py +0 -0
  43. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  44. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  45. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  46. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird_cli.egg-info/entry_points.txt +0 -0
  47. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/tinybird_cli.egg-info/requires.txt +0 -0
  48. {tinybird-cli-5.9.1.dev2 → tinybird-cli-5.10.0}/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.9.1.dev2
3
+ Version: 5.10.0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,11 +18,13 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
- 5.9.1.dev2
21
+ 5.10.0
22
22
  ***********
23
23
 
24
- - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
25
24
  - `Added` new "File not found" error to `tb check` when including files from missing paths.
25
+ - `Added` support for Kafka Data Sources with CA certificate.
26
+ - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
27
+ - `Fixed` Correctly parse lambda expressions in indexes
26
28
 
27
29
  5.9.0
28
30
  ***********
@@ -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.9.1.dev2'
8
- __revision__ = '4f458c3'
7
+ __version__ = '5.10.0'
8
+ __revision__ = 'f8aa90b'
@@ -4,7 +4,14 @@ from collections import defaultdict
4
4
  from dataclasses import asdict
5
5
  from typing import Any, Callable, Dict, Iterable, List, Optional
6
6
 
7
- from ..sql import TableIndex, col_name, engine_replicated_to_local, parse_indexes_structure, parse_table_structure
7
+ from ..sql import (
8
+ TableIndex,
9
+ TableProjection,
10
+ col_name,
11
+ engine_replicated_to_local,
12
+ parse_indexes_structure,
13
+ parse_table_structure,
14
+ )
8
15
 
9
16
  DEFAULT_EMPTY_PARAMETERS = ["ttl", "partition_key", "sorting_key"]
10
17
  DEFAULT_JOIN_EMPTY_PARAMETERS = ["join_strictness", "join_type", "key_columns"]
@@ -237,6 +244,10 @@ class TableDetails:
237
244
  def indexes(self) -> List[TableIndex]:
238
245
  return _parse_indexes(str(self.details.get("create_table_query", "")))
239
246
 
247
+ @property
248
+ def projections(self) -> List[TableProjection]:
249
+ return _parse_projections(self.details.get("create_table_query", ""))
250
+
240
251
  def to_json(self, exclude: Optional[List[str]] = None, include_empty_details: bool = False):
241
252
  # name, database are not exported since they are not part of the engine
242
253
  d: Dict[str, Any] = {
@@ -833,3 +844,12 @@ def _parse_indexes(create_table_query_expr: str) -> List[TableIndex]:
833
844
  return []
834
845
 
835
846
  return parse_indexes_structure(indexes)
847
+
848
+
849
+ def _parse_projections(create_table_query_expr: str) -> List[TableProjection]:
850
+ return [
851
+ TableProjection(name, expr)
852
+ for name, expr in re.findall(
853
+ r"PROJECTION\s+(\w+)\s*\(((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)", create_table_query_expr
854
+ )
855
+ ]
@@ -111,7 +111,7 @@ class TinyB(object):
111
111
  retries: int = LIMIT_RETRIES,
112
112
  use_token: Optional[str] = None,
113
113
  **kwargs,
114
- ): # noqa: C901
114
+ ):
115
115
  url = f"{self.host.strip('/')}/{endpoint.strip('/')}"
116
116
 
117
117
  token_to_use = use_token if use_token else self.token
@@ -275,7 +275,7 @@ class TinyB(object):
275
275
  bigquery_connection = (
276
276
  await self.bigquery_connection() if connector == "bigquery" or connector is None else None
277
277
  )
278
- connectors = connectors + [bigquery_connection] if bigquery_connection else connectors
278
+ connectors = [*connectors, bigquery_connection] if bigquery_connection else connectors
279
279
  if connector:
280
280
  return [
281
281
  {
@@ -865,6 +865,7 @@ class TinyB(object):
865
865
  kafka_auto_offset_reset=None,
866
866
  kafka_schema_registry_url=None,
867
867
  kafka_sasl_mechanism="PLAIN",
868
+ kafka_ssl_ca_pem=None,
868
869
  ):
869
870
  params = {
870
871
  "service": "kafka",
@@ -880,7 +881,8 @@ class TinyB(object):
880
881
  params["kafka_schema_registry_url"] = kafka_schema_registry_url
881
882
  if kafka_auto_offset_reset:
882
883
  params["kafka_auto_offset_reset"] = kafka_auto_offset_reset
883
-
884
+ if kafka_ssl_ca_pem:
885
+ params["kafka_ssl_ca_pem"] = kafka_ssl_ca_pem
884
886
  connection_params = {key: value for key, value in params.items() if value is not None}
885
887
 
886
888
  return await self._req(
@@ -60,7 +60,13 @@ from toposort import toposort
60
60
  from tinybird.config import PROJECT_PATHS
61
61
  from tinybird.sql_template_fmt import DEFAULT_FMT_LINE_LENGTH, format_sql_template
62
62
  from tinybird.syncasync import sync_to_async
63
- from tinybird.tb_cli_modules.common import _get_tb_client, get_current_main_workspace, getenv_bool, wait_job
63
+ from tinybird.tb_cli_modules.common import (
64
+ _get_tb_client,
65
+ get_ca_pem_content,
66
+ get_current_main_workspace,
67
+ getenv_bool,
68
+ wait_job,
69
+ )
64
70
  from tinybird.tb_cli_modules.config import CLIConfig
65
71
  from tinybird.tb_cli_modules.exceptions import CLIGitReleaseException, CLIPipeException
66
72
 
@@ -72,7 +78,7 @@ from .sql_template import get_template_and_variables, get_used_tables_in_templat
72
78
  from .tornado_template import UnClosedIfError
73
79
 
74
80
  os.environ["GIT_PYTHON_REFRESH"] = "quiet"
75
- from git import HEAD, Diff, GitCommandError, InvalidGitRepositoryError, Repo # noqa: E402
81
+ from git import HEAD, Diff, GitCommandError, InvalidGitRepositoryError, Repo
76
82
 
77
83
  INTERNAL_TABLES: Tuple[str, ...] = (
78
84
  "datasources_ops_log",
@@ -586,7 +592,7 @@ class Deployment:
586
592
  self.cli_git_release.validate_local_for_release(self.current_release, check_outdated=check_outdated)
587
593
  click.echo(FeedbackManager.info_deployment_detecting_changes_header())
588
594
  commit = self.cli_git_release.get_main_branch_commit() if use_main else self.current_release["commit"]
589
- diffs = self.cli_git_release.diff_datafiles(commit)
595
+ diffs = self.cli_git_release.diff_datafiles(commit) # type: ignore
590
596
  click.echo(
591
597
  FeedbackManager.info_git_release_diffs(
592
598
  workspace=self.current_ws["name"],
@@ -960,7 +966,7 @@ def parse(
960
966
  basepath: str = ".",
961
967
  replace_includes: bool = True,
962
968
  skip_eval: bool = False,
963
- ) -> Datafile: # noqa: C901
969
+ ) -> Datafile:
964
970
  """
965
971
  Parses `s` string into a document
966
972
  >>> d = parse("FROM SCRATCH\\nSOURCE 'https://example.com'\\n#this is a comment\\nMAINTAINER 'rambo' #this is me\\nNODE \\"test_01\\"\\n DESCRIPTION this is a node that does whatever\\nSQL >\\n\\n SELECT * from test_00\\n\\n\\nNODE \\"test_02\\"\\n DESCRIPTION this is a node that does whatever\\nSQL >\\n\\n SELECT * from test_01\\n WHERE a > 1\\n GROUP by a\\n")
@@ -1110,9 +1116,10 @@ def parse(
1110
1116
  pass
1111
1117
  finally:
1112
1118
  file.seek(0)
1113
- lines[lineno : lineno + 1] = [""] + list(
1114
- StringIO(Template(file.read()).safe_substitute(attrs), newline=None)
1115
- )
1119
+ lines[lineno : lineno + 1] = [
1120
+ "",
1121
+ *list(StringIO(Template(file.read()).safe_substitute(attrs), newline=None)),
1122
+ ]
1116
1123
  except FileNotFoundError:
1117
1124
  raise IncludeFileNotFoundException(f, lineno)
1118
1125
 
@@ -1211,6 +1218,7 @@ def parse(
1211
1218
  "kafka_store_headers": assign_var("kafka_store_headers"),
1212
1219
  "kafka_store_binary_headers": assign_var("kafka_store_binary_headers"),
1213
1220
  "kafka_key_avro_deserialization": assign_var("kafka_key_avro_deserialization"),
1221
+ "kafka_ssl_ca_pem": assign_var("kafka_ssl_ca_pem"),
1214
1222
  "import_service": assign_var("import_service"),
1215
1223
  "import_connection_name": assign_var("import_connection_name"),
1216
1224
  "import_schedule": assign_var("import_schedule"),
@@ -1318,7 +1326,7 @@ async def process_file(
1318
1326
  workspace_map: Optional[Dict] = None,
1319
1327
  workspace_lib_paths: Optional[List[Tuple[str, str]]] = None,
1320
1328
  current_ws: Optional[Dict[str, Any]] = None,
1321
- ): # noqa: C901 B006
1329
+ ):
1322
1330
  if workspace_map is None:
1323
1331
  workspace_map = {}
1324
1332
 
@@ -1349,6 +1357,7 @@ async def process_file(
1349
1357
  "kafka_connection_name": params.get("kafka_connection_name", None),
1350
1358
  "kafka_auto_offset_reset": params.get("kafka_auto_offset_reset", None),
1351
1359
  "kafka_schema_registry_url": params.get("kafka_schema_registry_url", None),
1360
+ "kafka_ssl_ca_pem": get_ca_pem_content(params.get("kafka_ssl_ca_pem", None), filename),
1352
1361
  }
1353
1362
 
1354
1363
  connector = await tb_client.get_connection(**connector_params)
@@ -2118,7 +2127,7 @@ class PipeCheckerRunnerResponse:
2118
2127
 
2119
2128
 
2120
2129
  class PipeCheckerRunner:
2121
- checker_stream_result_class = unittest.runner._WritelnDecorator # type: ignore
2130
+ checker_stream_result_class = unittest.runner._WritelnDecorator
2122
2131
 
2123
2132
  def __init__(self, pipe_name: str, host: str):
2124
2133
  self.pipe_name = pipe_name
@@ -2281,7 +2290,7 @@ class PipeCheckerRunner:
2281
2290
  )
2282
2291
 
2283
2292
  result = PipeCheckerTextTestResult(
2284
- self.checker_stream_result_class(sys.stdout), descriptions=True, verbosity=2, custom_output=custom_output
2293
+ self.checker_stream_result_class(sys.stdout), descriptions=True, verbosity=2, custom_output=custom_output # type: ignore
2285
2294
  )
2286
2295
  result.failfast = failfast
2287
2296
  suite.run(result)
@@ -2868,7 +2877,7 @@ async def new_pipe(
2868
2877
  config: Any = None,
2869
2878
  fork_downstream: Optional[bool] = False,
2870
2879
  fork: Optional[bool] = False,
2871
- ): # noqa: C901
2880
+ ):
2872
2881
  # TODO use tb_client instead of calling the urls directly.
2873
2882
  host = tb_client.host
2874
2883
  token = tb_client.token
@@ -3270,10 +3279,10 @@ async def new_ds(
3270
3279
  if job_url:
3271
3280
  click.echo(FeedbackManager.success_dynamodb_initial_load(job_url=job_url))
3272
3281
 
3273
- if "tokens" in ds and ds["tokens"]:
3282
+ if ds.get("tokens"):
3274
3283
  await manage_tokens()
3275
3284
 
3276
- if "shared_with" in ds and ds["shared_with"]:
3285
+ if ds.get("shared_with"):
3277
3286
  if not user_token:
3278
3287
  click.echo(FeedbackManager.info_skipping_shared_with_entry())
3279
3288
  else:
@@ -3989,7 +3998,7 @@ async def build_graph(
3989
3998
  mapped_workspaces.append(
3990
3999
  workspace_map.get(shared_with)
3991
4000
  if workspace_map.get(shared_with, None) is not None
3992
- else shared_with
4001
+ else shared_with # type: ignore
3993
4002
  )
3994
4003
  r["shared_with"] = mapped_workspaces
3995
4004
 
@@ -4189,7 +4198,7 @@ async def folder_push(
4189
4198
  use_main: bool = False,
4190
4199
  check_outdated: bool = True,
4191
4200
  hide_folders: bool = False,
4192
- ): # noqa: C901
4201
+ ):
4193
4202
  workspaces: List[Dict[str, Any]] = (await tb_client.user_workspaces_and_branches()).get("workspaces", [])
4194
4203
  current_ws: Dict[str, Any] = next(
4195
4204
  (workspace for workspace in workspaces if config and workspace.get("id", ".") == config.get("id", "..")), {}
@@ -4754,7 +4763,7 @@ DATAFILE_INDENT = " " * 4
4754
4763
 
4755
4764
 
4756
4765
  def format_schema(file_parts: List[str], node: Dict[str, Any]) -> List[str]:
4757
- if "schema" in node and node["schema"]:
4766
+ if node.get("schema"):
4758
4767
  file_parts.append("SCHEMA >")
4759
4768
  file_parts.append(DATAFILE_NEW_LINE)
4760
4769
  columns = schema_to_sql_columns(node["columns"])
@@ -4766,7 +4775,7 @@ def format_schema(file_parts: List[str], node: Dict[str, Any]) -> List[str]:
4766
4775
 
4767
4776
 
4768
4777
  def format_indices(file_parts: List[str], node: Dict[str, Any]) -> List[str]:
4769
- if "indexes" in node and node["indexes"]:
4778
+ if node.get("indexes"):
4770
4779
  indexes = node["indexes"]
4771
4780
  file_parts.append("INDEXES >")
4772
4781
  file_parts.append(DATAFILE_NEW_LINE)
@@ -4995,7 +5004,7 @@ async def format_node_type(file_parts: List[str], node: Dict[str, Any]) -> List[
4995
5004
  file_parts.append(DATAFILE_NEW_LINE)
4996
5005
  file_parts.append(f'COPY_MODE {node.get("mode")}')
4997
5006
 
4998
- if CopyParameters.COPY_SCHEDULE in node and node[CopyParameters.COPY_SCHEDULE]:
5007
+ if node.get(CopyParameters.COPY_SCHEDULE):
4999
5008
  is_ondemand = node[CopyParameters.COPY_SCHEDULE].lower() == ON_DEMAND
5000
5009
  file_parts.append(DATAFILE_NEW_LINE)
5001
5010
  file_parts.append(
@@ -5154,7 +5163,7 @@ async def folder_pull(
5154
5163
  verbose: bool = True,
5155
5164
  progress_bar: bool = False,
5156
5165
  fmt: bool = False,
5157
- ): # noqa: C901
5166
+ ):
5158
5167
  pattern = re.compile(match) if match else None
5159
5168
 
5160
5169
  def _get_latest_versions(resources: List[str]):
@@ -5321,7 +5330,7 @@ async def diff_command(
5321
5330
 
5322
5331
  if filenames:
5323
5332
  if len(filenames) == 1:
5324
- filenames = [filenames[0]] + get_project_filenames(filenames[0])
5333
+ filenames = [filenames[0], *get_project_filenames(filenames[0])]
5325
5334
  await folder_pull(client, target_dir, False, None, True, verbose=False)
5326
5335
  else:
5327
5336
  filenames = get_project_filenames(".")
@@ -238,6 +238,8 @@ class FeedbackManager:
238
238
  error_connection_does_not_exists = error_message("Connection {connection_id} does not exist")
239
239
  error_connection_create = error_message("Connection {connection_name} could not be created: {error}")
240
240
  error_connection_integration_not_available = error_message("Connection could not be created: {error}")
241
+ error_connection_invalid_ca_pem = error_message("Invalid CA certificate in PEM format")
242
+ error_connection_ca_pem_not_found = error_message("CA certificate in PEM format not found at {ca_pem}")
241
243
  error_workspace = error_message("Workspace {workspace} not found. use 'tb workspace ls' to list your workspaces")
242
244
  error_deleted_include = error_message(
243
245
  "Related include file {include_file} was deleted and it's used in {filename}. Delete or remove dependency from {filename}."
@@ -38,11 +38,37 @@ class TableIndex:
38
38
  return f"CLEAR INDEX IF EXISTS {self.name}"
39
39
 
40
40
 
41
+ @dataclass
42
+ class TableProjection:
43
+ """Defines a CH table PROJECTION"""
44
+
45
+ name: str
46
+ expr: str
47
+
48
+ def to_datafile(self):
49
+ return f"{self.name} ({self.expr})"
50
+
51
+ def to_sql(self):
52
+ return f"PROJECTION {self.to_datafile()}"
53
+
54
+ def add_index_sql(self):
55
+ return f"ADD {self.to_sql()}"
56
+
57
+ def drop_index_sql(self):
58
+ return f"DROP PROJECTION IF EXISTS {self.name}"
59
+
60
+ def materialize_index_sql(self):
61
+ return f"MATERIALIZE PROJECTION IF EXISTS {self.name}"
62
+
63
+ def clear_index_sql(self):
64
+ return f"CLEAR PROJECTION IF EXISTS {self.name}"
65
+
66
+
41
67
  def as_subquery(sql: str) -> str:
42
68
  return f"""(\n{sql}\n)"""
43
69
 
44
70
 
45
- def get_format(sql: str) -> str:
71
+ def get_format(sql: str) -> Optional[str]:
46
72
  """
47
73
  retrieves FORMAT from CH sql
48
74
  >>> get_format('select * from test')
@@ -211,7 +237,7 @@ def format_parse_error(
211
237
  return message
212
238
 
213
239
 
214
- def parse_indexes_structure(indexes: List[str]) -> List[TableIndex]:
240
+ def parse_indexes_structure(indexes: Optional[List[str]]) -> List[TableIndex]:
215
241
  """
216
242
  >>> parse_indexes_structure(["index_name a TYPE set(100) GRANULARITY 100", "index_name_bf mapValues(d) TYPE bloom_filter(0.001) GRANULARITY 16"])
217
243
  [TableIndex(name='index_name', expr='a', type_full='set(100)', granularity='100'), TableIndex(name='index_name_bf', expr='mapValues(d)', type_full='bloom_filter(0.001)', granularity='16')]
@@ -239,6 +265,8 @@ def parse_indexes_structure(indexes: List[str]) -> List[TableIndex]:
239
265
  ValueError: invalid INDEX format. Usage: `name expr TYPE type_full GRANULARITY granularity`
240
266
  >>> parse_indexes_structure(["my_index m['key'] TYPE ngrambf_v1(1, 1024, 1, 42) GRANULARITY 1"])
241
267
  [TableIndex(name='my_index', expr="m['key']", type_full='ngrambf_v1(1, 1024, 1, 42)', granularity='1')]
268
+ >>> parse_indexes_structure(["my_index_lambda arrayMap(x -> tupleElement(x,'message'), column_name) TYPE ngrambf_v1(1, 1024, 1, 42) GRANULARITY 1"])
269
+ [TableIndex(name='my_index_lambda', expr="arrayMap(x -> tupleElement(x,'message'), column_name)", type_full='ngrambf_v1(1, 1024, 1, 42)', granularity='1')]
242
270
  >>> parse_indexes_structure(["ip_range_minmax_idx (toIPv6(ip_range_start), toIPv6(ip_range_end)) TYPE minmax GRANULARITY 1"])
243
271
  [TableIndex(name='ip_range_minmax_idx', expr='(toIPv6(ip_range_start), toIPv6(ip_range_end))', type_full='minmax', granularity='1')]
244
272
  """
@@ -253,7 +281,7 @@ def parse_indexes_structure(indexes: List[str]) -> List[TableIndex]:
253
281
  raise ValueError("invalid INDEX format. Usage: `name expr TYPE type_full GRANULARITY granularity`")
254
282
 
255
283
  match = re.match(
256
- r"(\w+)\s+([\w\s*\[\]\*\(\),\'\".]+)\s+TYPE\s+(\w+)(?:\(([\w\s*.,]+)\))?(?:\s+GRANULARITY\s+(\d+))?",
284
+ r"(\w+)\s+([\w\s*\[\]\*\(\),\'\"-><.]+)\s+TYPE\s+(\w+)(?:\(([\w\s*.,]+)\))?(?:\s+GRANULARITY\s+(\d+))?",
257
285
  index,
258
286
  )
259
287
  if match:
@@ -498,7 +526,7 @@ REGEX_WHITESPACE = re.compile(r"\s*")
498
526
  REGEX_COMMENT = re.compile(r"\-\-[^\n\r]*[\n\r]")
499
527
 
500
528
 
501
- def _parse_table_structure(schema: str) -> List[Dict[str, Any]]: # noqa: C901
529
+ def _parse_table_structure(schema: str) -> List[Dict[str, Any]]:
502
530
  # CH syntax from https://clickhouse.com/docs/en/sql-reference/statements/create/table/
503
531
  # name1 [type1] [NULL|NOT NULL] [DEFAULT|MATERIALIZED|ALIAS expr1] [compression_codec] [TTL expr1]
504
532
  try:
@@ -359,7 +359,7 @@ def defined(x=None):
359
359
  return True
360
360
 
361
361
 
362
- def array_type(types): # noqa: C901
362
+ def array_type(types):
363
363
  def _f(
364
364
  x, _type=None, default=None, defined=True, required=None, description=None, enum=None, example=None, format=None
365
365
  ):
@@ -1370,7 +1370,7 @@ _namespace = {
1370
1370
  }
1371
1371
 
1372
1372
 
1373
- reserved_vars = set(["_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error"] + list(vars(builtins)))
1373
+ reserved_vars = set(["_tt_tmp", "_tt_append", "isinstance", "str", "error", "custom_error", *list(vars(builtins))])
1374
1374
  for p in DEFAULT_PARAM_NAMES: # we handle these in an specific manner
1375
1375
  reserved_vars.discard(p) # `format` is part of builtins
1376
1376
  error_vars = ["error", "custom_error"]
@@ -1552,7 +1552,7 @@ def get_var_names(t):
1552
1552
  raise SQLTemplateException(e)
1553
1553
 
1554
1554
 
1555
- def get_var_data(content, node_id=None): # noqa: C901
1555
+ def get_var_data(content, node_id=None):
1556
1556
  def node_to_value(x):
1557
1557
  if type(x) in (ast.Bytes, ast.Str):
1558
1558
  return x.s
@@ -1619,8 +1619,10 @@ def get_var_data(content, node_id=None): # noqa: C901
1619
1619
 
1620
1620
  kwargs = {}
1621
1621
  for x in node.keywords:
1622
- kwargs[x.arg] = node_to_value(x.value)
1623
-
1622
+ value = node_to_value(x.value)
1623
+ kwargs[x.arg] = value
1624
+ if x.arg == "default":
1625
+ kwargs["default"] = check_default_value(value)
1624
1626
  if func in VALID_CUSTOM_FUNCTION_NAMES:
1625
1627
  # Type definition here is set to 'String' because it comes from a
1626
1628
  # `defined(variable)` expression that does not contain any type hint.
@@ -1628,11 +1630,15 @@ def get_var_data(content, node_id=None): # noqa: C901
1628
1630
  # args[0] check is used to avoid adding unnamed parameters found in
1629
1631
  # templates like: `split_to_array('')`
1630
1632
  if len(args) > 0 and args[0] not in vars and args[0]:
1631
- vars[args[0]] = {"type": "String", "default": None, "used_in": "function_call"}
1633
+ vars[args[0]] = {
1634
+ "type": "String",
1635
+ "default": None,
1636
+ "used_in": "function_call",
1637
+ }
1632
1638
  elif func == "Array":
1633
1639
  if "default" not in kwargs:
1634
1640
  default = kwargs.get("default", args[2] if len(args) > 2 and args[2] else None)
1635
- kwargs["default"] = default
1641
+ kwargs["default"] = check_default_value(default)
1636
1642
  if len(args):
1637
1643
  vars[args[0]] = {
1638
1644
  "type": f"Array({args[1]})" if len(args) > 1 else "Array(String)",
@@ -1644,9 +1650,11 @@ def get_var_data(content, node_id=None): # noqa: C901
1644
1650
  # if this is a cast use the function name to get the type
1645
1651
  if "default" not in kwargs:
1646
1652
  default = kwargs.get("default", args[1] if len(args) > 1 else None)
1647
- kwargs["default"] = default
1653
+ kwargs["default"] = check_default_value(default)
1648
1654
  try:
1649
1655
  vars[args[0]] = {"type": func, **kwargs}
1656
+ if "default" in kwargs:
1657
+ kwargs["default"] = check_default_value(kwargs["default"])
1650
1658
  except TypeError as e:
1651
1659
  logging.exception(f"pipe parsing problem {content} (node '{node_id}'): {e}")
1652
1660
  except Exception as e:
@@ -1665,6 +1673,13 @@ def get_var_data(content, node_id=None): # noqa: C901
1665
1673
 
1666
1674
  return vars
1667
1675
 
1676
+ def check_default_value(value):
1677
+ if isinstance(value, int):
1678
+ MAX_SAFE_INTEGER = 9007199254740991
1679
+ if value > MAX_SAFE_INTEGER:
1680
+ return str(value)
1681
+ return value
1682
+
1668
1683
  def parse_content(content, retries=0):
1669
1684
  try:
1670
1685
  parsed = ast.parse(content)
@@ -1688,7 +1703,7 @@ def get_var_data(content, node_id=None): # noqa: C901
1688
1703
  return [dict(name=k, **v) for k, v in vars.items()]
1689
1704
 
1690
1705
 
1691
- def get_var_names_and_types(t, node_id=None): # noqa: C901
1706
+ def get_var_names_and_types(t, node_id=None):
1692
1707
  """
1693
1708
  >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE description = {{Float32(with_value, 0.0)}}"))
1694
1709
  [{'name': 'with_value', 'type': 'Float32', 'default': 0.0}]
@@ -1744,6 +1759,8 @@ def get_var_names_and_types(t, node_id=None): # noqa: C901
1744
1759
  [{'name': 'symbol_id', 'type': 'Int128', 'description': 'Symbol Id', 'required': True, 'default': 11111}, {'name': 'user_id', 'type': 'Int256', 'description': 'User Id', 'default': 3555}]
1745
1760
  >>> get_var_names_and_types(Template("SELECT now() > {{DateTime64(timestamp, '2020-09-09 10:10:10.000')}}"))
1746
1761
  [{'name': 'timestamp', 'type': 'DateTime64', 'default': '2020-09-09 10:10:10.000'}]
1762
+ >>> get_var_names_and_types(Template("SELECT * FROM filter_value WHERE symbol = {{Int64(symbol_id, 9223372036854775807)}}"))
1763
+ [{'name': 'symbol_id', 'type': 'Int64', 'default': '9223372036854775807'}]
1747
1764
  """
1748
1765
  try:
1749
1766
 
@@ -177,7 +177,7 @@ class _WorkItem:
177
177
  return
178
178
  try:
179
179
  result = self.fn(*self.args, **self.kwargs)
180
- except BaseException as exc: # noqa: B036
180
+ except BaseException as exc:
181
181
  self.future.set_exception(exc)
182
182
  # Break a reference cycle with the exception 'exc'
183
183
  self = None
@@ -473,11 +473,11 @@ class AsyncToSync:
473
473
  if exc_info[1]:
474
474
  try:
475
475
  raise exc_info[1]
476
- except BaseException: # noqa: B036
476
+ except BaseException:
477
477
  result = await self.awaitable(*args, **kwargs)
478
478
  else:
479
479
  result = await self.awaitable(*args, **kwargs)
480
- except BaseException as e: # noqa: B036
480
+ except BaseException as e:
481
481
  call_result.set_exception(e)
482
482
  else:
483
483
  call_result.set_result(result)
@@ -639,7 +639,7 @@ class SyncToAsync:
639
639
  if exc_info[1]:
640
640
  try:
641
641
  raise exc_info[1]
642
- except BaseException: # noqa: B036
642
+ except BaseException:
643
643
  return func(*args, **kwargs)
644
644
  else:
645
645
  return func(*args, **kwargs)
@@ -76,7 +76,7 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
76
76
  ]
77
77
 
78
78
 
79
- @click.group(cls=CatchAuthExceptions, context_settings={"help_option_names": ["-h", "--help"]}) # noqa: C901
79
+ @click.group(cls=CatchAuthExceptions, context_settings={"help_option_names": ["-h", "--help"]})
80
80
  @click.option(
81
81
  "--debug/--no-debug",
82
82
  default=False,
@@ -212,7 +212,7 @@ async def cli(
212
212
  if ctx.invoked_subcommand == "auth":
213
213
  return
214
214
 
215
- from tinybird.connectors import create_connector # noqa
215
+ from tinybird.connectors import create_connector
216
216
 
217
217
  if gcp_project_id and gcs_bucket and google_application_credentials and not sf_account:
218
218
  bq_config = {
@@ -679,7 +679,7 @@ async def push(
679
679
  return
680
680
 
681
681
 
682
- @cli.command() # noqa: C901
682
+ @cli.command()
683
683
  @click.option(
684
684
  "--folder", default=None, type=click.Path(exists=True, file_okay=False), help="Folder where files will be placed"
685
685
  )
@@ -1251,7 +1251,7 @@ def __unpatch_click_output():
1251
1251
  click.secho = __old_click_echo
1252
1252
 
1253
1253
 
1254
- @cli.command(short_help="Learn how to include info about the CLI in your shell PROMPT") # noqa: C901
1254
+ @cli.command(short_help="Learn how to include info about the CLI in your shell PROMPT")
1255
1255
  @click.pass_context
1256
1256
  @coro
1257
1257
  async def prompt(_ctx: Context) -> None:
@@ -1133,17 +1133,15 @@ async def push_data(
1133
1133
  appended_rows = 0
1134
1134
  parser = None
1135
1135
 
1136
- if "error" in res and res["error"]:
1136
+ if res.get("error"):
1137
1137
  raise CLIException(FeedbackManager.error_exception(error=res["error"]))
1138
- if "errors" in res and res["errors"]:
1138
+ if res.get("errors"):
1139
1139
  raise CLIException(FeedbackManager.error_exception(error=res["errors"]))
1140
- if "blocks" in res and res["blocks"]:
1140
+ if res.get("blocks"):
1141
1141
  for block in res["blocks"]:
1142
1142
  if "process_return" in block and block["process_return"] is not None:
1143
1143
  process_return = block["process_return"][0]
1144
- parser = (
1145
- process_return["parser"] if "parser" in process_return and process_return["parser"] else parser
1146
- )
1144
+ parser = process_return["parser"] if process_return.get("parser") else parser
1147
1145
  if parser and parser != "clickhouse":
1148
1146
  parser = process_return["parser"]
1149
1147
  appended_rows += process_return["lines"]
@@ -1214,12 +1212,12 @@ def autocomplete_topics(ctx: Context, args, incomplete):
1214
1212
 
1215
1213
 
1216
1214
  def validate_datasource_name(name):
1217
- if not isinstance(name, str) or str == "":
1215
+ if not isinstance(name, str) or name == "":
1218
1216
  raise CLIException(FeedbackManager.error_datasource_name())
1219
1217
 
1220
1218
 
1221
1219
  def validate_connection_id(connection_id):
1222
- if not isinstance(connection_id, str) or str == "":
1220
+ if not isinstance(connection_id, str) or connection_id == "":
1223
1221
  raise CLIException(FeedbackManager.error_datasource_connection_id())
1224
1222
 
1225
1223
 
@@ -1235,7 +1233,7 @@ def validate_kafka_group(group):
1235
1233
 
1236
1234
  def validate_kafka_auto_offset_reset(auto_offset_reset):
1237
1235
  valid_values = {"latest", "earliest", "none"}
1238
- if not (auto_offset_reset in valid_values):
1236
+ if auto_offset_reset not in valid_values:
1239
1237
  raise CLIException(FeedbackManager.error_kafka_auto_offset_reset())
1240
1238
 
1241
1239
 
@@ -1588,7 +1586,7 @@ async def try_update_config_with_remote(
1588
1586
  def ask_for_admin_token_interactively(ui_host: str, default_token: Optional[str]) -> str:
1589
1587
  return (
1590
1588
  click.prompt(
1591
- f"\nCopy the \"admin your@email\" token from {ui_host}/tokens and paste it here { f'OR press enter to use the token from .tinyb file' if default_token else ''}",
1589
+ f"\nCopy the \"admin your@email\" token from {ui_host}/tokens and paste it here { 'OR press enter to use the token from .tinyb file' if default_token else ''}",
1592
1590
  hide_input=True,
1593
1591
  show_default=False,
1594
1592
  default=default_token,
@@ -2056,3 +2054,28 @@ async def create_aws_iamrole_connection(client: TinyB, service: str, connection_
2056
2054
  """
2057
2055
  )
2058
2056
  click.echo(FeedbackManager.success_connection_file_created(name=conn_file_name))
2057
+
2058
+
2059
+ def get_ca_pem_content(ca_pem: Optional[str], filename: Optional[str] = None) -> Optional[str]:
2060
+ if not ca_pem:
2061
+ return None
2062
+
2063
+ def is_valid_content(text_content: str) -> bool:
2064
+ return text_content.startswith("-----BEGIN CERTIFICATE-----")
2065
+
2066
+ ca_pem_content = ca_pem
2067
+ base_path = Path(getcwd(), filename).parent if filename else Path(getcwd())
2068
+ ca_pem_path = Path(base_path, ca_pem)
2069
+ path_exists = os.path.exists(ca_pem_path)
2070
+
2071
+ if not path_exists:
2072
+ raise CLIConnectionException(FeedbackManager.error_connection_ca_pem_not_found(ca_pem=ca_pem))
2073
+
2074
+ if ca_pem.endswith(".pem") and path_exists:
2075
+ with open(ca_pem_path, "r") as f:
2076
+ ca_pem_content = f.read()
2077
+
2078
+ if not is_valid_content(ca_pem_content):
2079
+ raise CLIConnectionException(FeedbackManager.error_connection_invalid_ca_pem())
2080
+
2081
+ return ca_pem_content
@@ -21,6 +21,7 @@ from tinybird.tb_cli_modules.common import (
21
21
  coro,
22
22
  create_aws_iamrole_connection,
23
23
  echo_safe_humanfriendly_tables_format_smart_table,
24
+ get_ca_pem_content,
24
25
  validate_aws_iamrole_connection_name,
25
26
  validate_aws_iamrole_integration,
26
27
  validate_connection_name,
@@ -44,6 +45,7 @@ DATA_CONNECTOR_SETTINGS: Dict[DataConnectorType, List[str]] = {
44
45
  "kafka_security_protocol",
45
46
  "kafka_sasl_mechanism",
46
47
  "kafka_schema_registry_url",
48
+ "kafka_ssl_ca_pem",
47
49
  ],
48
50
  DataConnectorType.GCLOUD_SCHEDULER: ["gcscheduler_region"],
49
51
  DataConnectorType.SNOWFLAKE: [
@@ -131,6 +133,7 @@ def connection_create(ctx: Context) -> None:
131
133
  default="PLAIN",
132
134
  help="Authentication method for connection-based protocols. Defaults to 'PLAIN'",
133
135
  )
136
+ @click.option("--ssl-ca-pem", default=None, help="Path or content of the CA Certificate file in PEM format")
134
137
  @click.pass_context
135
138
  @coro
136
139
  async def connection_create_kafka(
@@ -142,6 +145,7 @@ async def connection_create_kafka(
142
145
  auto_offset_reset: Optional[str],
143
146
  schema_registry_url: Optional[str],
144
147
  sasl_mechanism: Optional[str],
148
+ ssl_ca_pem: Optional[str],
145
149
  ) -> None:
146
150
  """
147
151
  Add a Kafka connection
@@ -174,7 +178,14 @@ async def connection_create_kafka(
174
178
  client: TinyB = obj["client"]
175
179
 
176
180
  result = await client.connection_create_kafka(
177
- bootstrap_servers, key, secret, connection_name, auto_offset_reset, schema_registry_url, sasl_mechanism
181
+ bootstrap_servers,
182
+ key,
183
+ secret,
184
+ connection_name,
185
+ auto_offset_reset,
186
+ schema_registry_url,
187
+ sasl_mechanism,
188
+ get_ca_pem_content(ssl_ca_pem),
178
189
  )
179
190
 
180
191
  id = result["id"]
@@ -229,7 +229,7 @@ async def datasource_replace(
229
229
 
230
230
  - Replace from local file `tb datasource replace [datasource_name] /path/to/local/file --sql-condition "country='ES'"`
231
231
 
232
- - Replace from connector `tb datasource replace [datasource_name] --connector [connector_name] --sql [the_sql_to_extract_from] --sql-condition "country='ES'"`
232
+ - Replace from connector `tb datasource replace [datasource_name] --connector [connector_name] --sql [the_sql_to_extract_from] --sql-condition "country='ES'"`
233
233
  """
234
234
 
235
235
  if not url and not connector:
@@ -848,7 +848,7 @@ def _parse(reader: _TemplateReader, template, in_block=None, in_loop=None):
848
848
  # When there are more than 2 curlies in a row, use the
849
849
  # innermost ones. This is useful when generating languages
850
850
  # like latex where curlies are also meaningful
851
- if curly + 2 < reader.remaining() and reader[curly + 1] == "{" and reader[curly + 2] == "{": # noqa: W504
851
+ if curly + 2 < reader.remaining() and reader[curly + 1] == "{" and reader[curly + 2] == "{":
852
852
  curly += 1
853
853
  continue
854
854
  break
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.9.1.dev2
3
+ Version: 5.10.0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,11 +18,13 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
- 5.9.1.dev2
21
+ 5.10.0
22
22
  ***********
23
23
 
24
- - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
25
24
  - `Added` new "File not found" error to `tb check` when including files from missing paths.
25
+ - `Added` support for Kafka Data Sources with CA certificate.
26
+ - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
27
+ - `Fixed` Correctly parse lambda expressions in indexes
26
28
 
27
29
  5.9.0
28
30
  ***********