tinybird 4.6.9.dev0__py3-none-any.whl → 4.6.13.dev0__py3-none-any.whl

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.
@@ -121,6 +121,12 @@ class TableDetails:
121
121
  def is_mergetree_family(self) -> bool:
122
122
  return self.engine is not None and "mergetree" in self.engine.lower()
123
123
 
124
+ def is_view(self) -> bool:
125
+ return self.original_engine == "View"
126
+
127
+ def is_merge_final_engine(self) -> bool:
128
+ return self.original_engine == "MergeFinal"
129
+
124
130
  def supports_alter_add_column(self) -> bool:
125
131
  return self.is_mergetree_family() or (self.engine is not None and self.engine.lower() == "null")
126
132
 
@@ -221,8 +221,6 @@ INTERNAL_TABLES: Tuple[str, ...] = (
221
221
  "kafka_ops_log",
222
222
  "datasources_storage",
223
223
  "endpoint_errors",
224
- "bi_stats_rt",
225
- "bi_stats",
226
224
  )
227
225
 
228
226
  PREVIEW_CONNECTOR_SERVICES = ["s3", "s3_iamrole", "gcs"]
@@ -403,12 +401,15 @@ class Datafile:
403
401
  if node.get("mode") and node["mode"] not in ["append", "replace"]:
404
402
  raise DatafileValidationError("COPY node mode must be append or replace")
405
403
  # copy schedule must be @on-demand or a cron-expression
406
- if (
407
- node.get("copy_schedule")
408
- and node["copy_schedule"] != ON_DEMAND
409
- and not croniter.is_valid(node["copy_schedule"])
410
- ):
411
- raise DatafileValidationError("COPY node schedule must be @on-demand or a valid cron expression.")
404
+ copy_schedule = node.get("copy_schedule")
405
+ if copy_schedule and copy_schedule != ON_DEMAND:
406
+ if isinstance(copy_schedule, str) and "tb_secret" in copy_schedule:
407
+ raise DatafileValidationError(
408
+ "COPY_SCHEDULE does not support { tb_secret(...) }. "
409
+ 'Use a literal cron expression (for example "0 * * * *") or @on-demand.'
410
+ )
411
+ if not croniter.is_valid(copy_schedule):
412
+ raise DatafileValidationError("COPY node schedule must be @on-demand or a valid cron expression.")
412
413
  for key in node.keys():
413
414
  if key not in CopyParameters.valid_params():
414
415
  raise DatafileValidationError(
@@ -505,10 +506,16 @@ class Datafile:
505
506
 
506
507
  # Validate schedule format (common for both Kafka and S3/GCS)
507
508
  export_schedule = node.get("export_schedule")
508
- if export_schedule and export_schedule != ON_DEMAND and not croniter.is_valid(export_schedule):
509
- raise DatafileValidationError(
510
- f"Sink node {repr(node['name'])} has invalid export_schedule '{export_schedule}'. Must be @on-demand or a valid cron expression."
511
- )
509
+ if export_schedule and export_schedule != ON_DEMAND:
510
+ if isinstance(export_schedule, str) and "tb_secret" in export_schedule:
511
+ raise DatafileValidationError(
512
+ f"Sink node {repr(node['name'])}: EXPORT_SCHEDULE does not support {{ tb_secret(...) }}. "
513
+ 'Use a literal cron expression (for example "0 * * * *") or @on-demand.'
514
+ )
515
+ if not croniter.is_valid(export_schedule):
516
+ raise DatafileValidationError(
517
+ f"Sink node {repr(node['name'])} has invalid export_schedule '{export_schedule}'. Must be @on-demand or a valid cron expression."
518
+ )
512
519
 
513
520
  def validate(self):
514
521
  if self.kind == DatafileKind.pipe:
@@ -1725,7 +1732,9 @@ def parse(
1725
1732
 
1726
1733
  parser_state.current_node["indexes"] = indexes
1727
1734
 
1728
- def assign_var(v: str, allowed_values: Optional[set[str]] = None) -> Callable[[VarArg(str), KwArg(Any)], None]:
1735
+ def assign_var(
1736
+ v: str, allowed_values: Optional[set[str]] = None, lowercase: bool = False
1737
+ ) -> Callable[[VarArg(str), KwArg(Any)], None]:
1729
1738
  @multiline_not_supported
1730
1739
  def _f(*args: str, **kwargs: Any):
1731
1740
  s = _unquote((" ".join(args)).strip())
@@ -1736,6 +1745,8 @@ def parse(
1736
1745
  lineno=kwargs["lineno"],
1737
1746
  pos=1,
1738
1747
  )
1748
+ if lowercase:
1749
+ val = val.lower()
1739
1750
  parser_state.current_node[v.lower()] = val
1740
1751
 
1741
1752
  return _f
@@ -2048,7 +2059,9 @@ def parse(
2048
2059
  "import_schedule": assign_var("import_schedule"),
2049
2060
  "import_strategy": import_strategy_deprecated, # Deprecated, always append
2050
2061
  "import_bucket_uri": assign_var("import_bucket_uri"),
2051
- "import_format": assign_var("import_format"),
2062
+ # Lowercase: format checks and executor routing downstream expect
2063
+ # lowercase values (ANALYTICS-A8M)
2064
+ "import_format": assign_var("import_format", lowercase=True),
2052
2065
  "import_from_timestamp": assign_var("import_from_timestamp"),
2053
2066
  "import_service": import_service_deprecated, # Deprecated
2054
2067
  "import_external_datasource": import_external_datasource_deprecated, # Deprecated, BQ and SFK
@@ -244,49 +244,6 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
244
244
  {"name": "rows_quarantine", "type": "SimpleAggregateFunction(max, UInt64)"},
245
245
  ],
246
246
  },
247
- {
248
- "name": "tinybird.bi_stats_rt",
249
- "description": "Contains information about all requests to your BI Connector interface in real time.",
250
- "dateColumn": "start_datetime",
251
- "engine": {
252
- "engine": "MergeTree",
253
- "sorting_key": "cityHash64(query_normalized), start_datetime",
254
- "ttl": "start_datetime + toIntervalDay(7)",
255
- },
256
- "columns": [
257
- {"name": "start_datetime", "type": "DateTime"},
258
- {"name": "query", "type": "String"},
259
- {"name": "query_normalized", "type": "String"},
260
- {"name": "error_code", "type": "Int32"},
261
- {"name": "error", "type": "Nullable(String)"},
262
- {"name": "duration", "type": "UInt64"},
263
- {"name": "read_rows", "type": "UInt64"},
264
- {"name": "read_bytes", "type": "UInt64"},
265
- {"name": "result_rows", "type": "UInt64"},
266
- {"name": "result_bytes", "type": "UInt64"},
267
- ],
268
- },
269
- {
270
- "name": "tinybird.bi_stats",
271
- "description": "Aggregates the stats in tinybird.bi_stats_rt by day.",
272
- "dateColumn": "date",
273
- "engine": {"engine": "MergeTree", "sorting_key": "cityHash64(query_normalized), start_datetime"},
274
- "columns": [
275
- {"name": "date", "type": "Date"},
276
- {"name": "query_normalized", "type": "String"},
277
- {"name": "view_count", "type": "UInt64"},
278
- {"name": "error_count", "type": "UInt64"},
279
- {"name": "avg_duration_state", "type": "AggregateFunction(avg, Float32)"},
280
- {
281
- "name": "quantile_timing_state",
282
- "type": "AggregateFunction(quantilesTiming(0.9, 0.95, 0.99), Float64)",
283
- },
284
- {"name": "read_bytes_sum", "type": "UInt64"},
285
- {"name": "read_rows_sum", "type": "UInt64"},
286
- {"name": "avg_result_rows_state", "type": "AggregateFunction(avg, Float32)"},
287
- {"name": "avg_result_bytes_state", "type": "AggregateFunction(avg, Float32)"},
288
- ],
289
- },
290
247
  {
291
248
  "name": "tinybird.sinks_ops_log",
292
249
  "description": "Contains information about your Sink pipes.",
@@ -483,6 +440,7 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
483
440
  {"name": "error_code", "type": "Int16"},
484
441
  {"name": "error", "type": "String"},
485
442
  {"name": "fix_suggestion", "type": "String"},
443
+ {"name": "fix_suggestion_ai", "type": "String"},
486
444
  {"name": "run_validation", "type": "DateTime"},
487
445
  ],
488
446
  },
@@ -798,51 +756,6 @@ def get_organization_service_datasources() -> List[Dict[str, Any]]:
798
756
  {"name": "token_name", "type": "String"},
799
757
  ],
800
758
  },
801
- {
802
- "name": "organization.bi_stats_rt",
803
- "description": "Contains information about all requests to the BI Connector interface for the whole Organization in real time.",
804
- "dateColumn": "start_datetime",
805
- "engine": {
806
- "engine": "MergeTree",
807
- "sorting_key": "cityHash64(query_normalized), start_datetime",
808
- "ttl": "start_datetime + toIntervalDay(7)",
809
- },
810
- "columns": [
811
- {"name": "database", "type": "String"},
812
- {"name": "start_datetime", "type": "DateTime"},
813
- {"name": "query", "type": "String"},
814
- {"name": "query_normalized", "type": "String"},
815
- {"name": "error_code", "type": "Int32"},
816
- {"name": "error", "type": "Nullable(String)"},
817
- {"name": "duration", "type": "UInt64"},
818
- {"name": "read_rows", "type": "UInt64"},
819
- {"name": "read_bytes", "type": "UInt64"},
820
- {"name": "result_rows", "type": "UInt64"},
821
- {"name": "result_bytes", "type": "UInt64"},
822
- ],
823
- },
824
- {
825
- "name": "organization.bi_stats",
826
- "description": "Aggregates the stats in organization.bi_stats_rt by day.",
827
- "dateColumn": "date",
828
- "engine": {"engine": "MergeTree", "sorting_key": "database, cityHash64(query_normalized), date"},
829
- "columns": [
830
- {"name": "database", "type": "String"},
831
- {"name": "date", "type": "Date"},
832
- {"name": "query_normalized", "type": "String"},
833
- {"name": "view_count", "type": "UInt64"},
834
- {"name": "error_count", "type": "UInt64"},
835
- {"name": "avg_duration_state", "type": "AggregateFunction(avg, Float32)"},
836
- {
837
- "name": "quantile_timing_state",
838
- "type": "AggregateFunction(quantilesTiming(0.9, 0.95, 0.99), Float64)",
839
- },
840
- {"name": "read_bytes_sum", "type": "UInt64"},
841
- {"name": "read_rows_sum", "type": "UInt64"},
842
- {"name": "avg_result_rows_state", "type": "AggregateFunction(avg, Float32)"},
843
- {"name": "avg_result_bytes_state", "type": "AggregateFunction(avg, Float32)"},
844
- ],
845
- },
846
759
  {
847
760
  "name": "organization.metrics_logs",
848
761
  "description": "Metrics of your organization's dedicated clusters",
tinybird/sql_template.py CHANGED
@@ -2911,13 +2911,21 @@ def extract_variables_from_sql(sql: str, params: List[Dict[str, Any]]) -> Dict[s
2911
2911
  return defaults
2912
2912
 
2913
2913
 
2914
- def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict[str, str]] = None) -> str:
2914
+ def render_template_with_secrets(
2915
+ name: str,
2916
+ content: str,
2917
+ secrets: Optional[Dict[str, str]] = None,
2918
+ empty_secret_raises: bool = False,
2919
+ ) -> str:
2915
2920
  """Renders a template with secrets, allowing for default values.
2916
2921
 
2917
2922
  Args:
2918
2923
  name: The name of the template
2919
2924
  content: The template content
2920
2925
  secrets: A dictionary mapping secret names to their values
2926
+ empty_secret_raises: When True, empty secret values or empty defaults raise
2927
+ instead of returning '""'. Used for sink metadata (Forward deploy) where
2928
+ an empty bucket path must not be baked as s3://"".
2921
2929
 
2922
2930
  Returns:
2923
2931
  The rendered template
@@ -2959,6 +2967,26 @@ def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict
2959
2967
  Traceback (most recent call last):
2960
2968
  ...
2961
2969
  tinybird.sql_template.SQLTemplateException: Template Syntax Error: Cannot access secret 'MISSING_SECRET'. Check the secret exists in the Workspace and the token has the required scope.
2970
+
2971
+ >>> render_template_with_secrets(
2972
+ ... "sink_bucket",
2973
+ ... "s3://{{ tb_secret('MISSING_SECRET', '') }}",
2974
+ ... secrets = {},
2975
+ ... empty_secret_raises=True,
2976
+ ... )
2977
+ Traceback (most recent call last):
2978
+ ...
2979
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Secret 'MISSING_SECRET' resolves to an empty value and cannot be used in sink metadata.
2980
+
2981
+ >>> render_template_with_secrets(
2982
+ ... "sink_bucket",
2983
+ ... "s3://{{ tb_secret('EMPTY_SECRET', 'fallback') }}",
2984
+ ... secrets = {'EMPTY_SECRET': ''},
2985
+ ... empty_secret_raises=True,
2986
+ ... )
2987
+ Traceback (most recent call last):
2988
+ ...
2989
+ tinybird.sql_template.SQLTemplateException: Template Syntax Error: Secret 'EMPTY_SECRET' resolves to an empty value and cannot be used in sink metadata.
2962
2990
  """
2963
2991
  if not secrets:
2964
2992
  secrets = {}
@@ -2974,15 +3002,24 @@ def render_template_with_secrets(name: str, content: str, secrets: Optional[Dict
2974
3002
  The secret value or default
2975
3003
 
2976
3004
  Raises:
2977
- SQLTemplateException: If the secret is not found and no default is provided
3005
+ SQLTemplateException: If the secret is not found and no default is provided,
3006
+ or if empty_secret_raises is True and the resolved value is empty
2978
3007
  """
2979
3008
  if secret_name in secrets:
2980
3009
  value = secrets[secret_name]
2981
3010
  if isinstance(value, str) and len(value) == 0:
3011
+ if empty_secret_raises:
3012
+ raise SQLTemplateException(
3013
+ f"Secret '{secret_name}' resolves to an empty value and cannot be used in sink metadata."
3014
+ )
2982
3015
  return '""'
2983
3016
  return value
2984
3017
  elif default is not None:
2985
3018
  if isinstance(default, str) and len(default) == 0:
3019
+ if empty_secret_raises:
3020
+ raise SQLTemplateException(
3021
+ f"Secret '{secret_name}' resolves to an empty value and cannot be used in sink metadata."
3022
+ )
2986
3023
  return '""'
2987
3024
  return default
2988
3025
  else:
tinybird/sql_toolset.py CHANGED
@@ -158,7 +158,20 @@ def has_unoptimized_join(sql: str, left_table: Optional[Union[Tuple[str, str], T
158
158
  raise UnoptimizedJoinException(sql)
159
159
 
160
160
 
161
- def format_where_for_mutation_command(where_clause: str, lightweight: bool = False) -> str:
161
+ def _format_fragment_for_mutation_command(fragment: str) -> str:
162
+ """Normalize a SQL fragment the way CH stores it in the
163
+ `system.mutations.command` column: format the expression with CH's own
164
+ formatter, then escape it as it appears inside the serialized command
165
+ (string literals keep backslash-escaped quotes)."""
166
+ formatted = chquery.format(f"""SELECT {fragment}""").split("SELECT ")[1]
167
+ formatted = formatted.replace("\\", "\\\\").replace("'", "''")
168
+ quoted = chquery.format(f"SELECT '{formatted}'").split("SELECT ")[1]
169
+ return quoted[1:-1]
170
+
171
+
172
+ def format_where_for_mutation_command(
173
+ where_clause: str, lightweight: bool = False, partition: Optional[str] = None
174
+ ) -> str:
162
175
  """
163
176
  >>> format_where_for_mutation_command("numnights = 99")
164
177
  'DELETE WHERE numnights = 99'
@@ -172,12 +185,18 @@ def format_where_for_mutation_command(where_clause: str, lightweight: bool = Fal
172
185
  "DELETE WHERE reservationid = \\\\'\\\\\\\\\\\\'foo\\\\'"
173
186
  >>> format_where_for_mutation_command("number < 3", lightweight=True)
174
187
  'UPDATE _row_exists = 0 WHERE number < 3'
188
+ >>> format_where_for_mutation_command("number < 3", lightweight=True, partition="201901")
189
+ 'UPDATE _row_exists = 0 IN PARTITION 201901 WHERE number < 3'
190
+ >>> format_where_for_mutation_command("number < 3", lightweight=True, partition="'2019-01-01'")
191
+ "UPDATE _row_exists = 0 IN PARTITION \\\\'2019-01-01\\\\' WHERE number < 3"
175
192
  """
176
- formatted_condition = chquery.format(f"""SELECT {where_clause}""").split("SELECT ")[1]
177
- formatted_condition = formatted_condition.replace("\\", "\\\\").replace("'", "''")
178
- quoted_condition = chquery.format(f"SELECT '{formatted_condition}'").split("SELECT ")[1]
179
- prefix = "UPDATE _row_exists = 0 WHERE" if lightweight else "DELETE WHERE"
180
- return f"{prefix} {quoted_condition[1:-1]}"
193
+ quoted_condition = _format_fragment_for_mutation_command(where_clause)
194
+ prefix = "UPDATE _row_exists = 0" if lightweight else "DELETE"
195
+ # Partition-scoped mutations store the partition expression in the
196
+ # command (`… IN PARTITION <expr> WHERE <cond>`), normalized by the same
197
+ # CH formatter, so it must be part of the match.
198
+ partition_clause = f" IN PARTITION {_format_fragment_for_mutation_command(partition)}" if partition else ""
199
+ return f"{prefix}{partition_clause} WHERE {quoted_condition}"
181
200
 
182
201
 
183
202
  # Functions that take table/dictionary names as string literal arguments.
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '4.6.9.dev0'
8
- __revision__ = '13f02ee'
7
+ __version__ = '4.6.13.dev0'
8
+ __revision__ = '9f77ad2'
tinybird/tb/client.py CHANGED
@@ -546,20 +546,36 @@ class TinyB:
546
546
  def datasource_sync(self, datasource_id: str):
547
547
  return self._req(f"/v0/datasources/{datasource_id}/scheduling/runs", method="POST", data="")
548
548
 
549
- def datasource_sample(self, datasource_name: str, max_files: int = 1) -> Dict[str, Any]:
550
- """Start a sample import job for an S3/GCS connected datasource.
549
+ def datasource_sample(
550
+ self,
551
+ datasource_name: str,
552
+ max_files: int = 1,
553
+ rows: Optional[int] = None,
554
+ max_bytes: Optional[str] = None,
555
+ full_export: bool = False,
556
+ ) -> Dict[str, Any]:
557
+ """Start a sample import job for an S3/GCS/DynamoDB connected datasource.
551
558
 
552
559
  Args:
553
560
  datasource_name: Name of the datasource to import sample data into
554
- max_files: Maximum number of files to import (default 1, max 10)
561
+ max_files: Maximum number of files to import for blob storage connectors (default 1, max 10)
562
+ rows: For DynamoDB, the maximum number of rows to scan and import (mutually exclusive with max_bytes)
563
+ max_bytes: For DynamoDB, the maximum approximate JSONEachRow bytes to import, capped by the server's
564
+ workspace limit (mutually exclusive with rows)
565
+ full_export: For DynamoDB, trigger a full PITR export instead of a bounded scan
555
566
 
556
567
  Returns:
557
568
  dict with job info including id, job_id, job_url, job, status
558
569
  """
570
+ payload: Dict[str, Any] = {"max_files": max_files, "full_export": full_export}
571
+ if rows is not None:
572
+ payload["rows"] = rows
573
+ if max_bytes is not None:
574
+ payload["max_bytes"] = max_bytes
559
575
  return self._req(
560
576
  f"/v0/datasources/{datasource_name}/sample",
561
577
  method="POST",
562
- data=json.dumps({"max_files": max_files}),
578
+ data=json.dumps(payload),
563
579
  )
564
580
 
565
581
  def datasource_scheduling_state(self, datasource_id: str):
@@ -1153,7 +1153,8 @@ def create_ctx_client(
1153
1153
  if show_warnings and command:
1154
1154
  click.echo(FeedbackManager.gray(message="Running against Tinybird Local"))
1155
1155
  local_branch = None
1156
- if not test:
1156
+ explicit_token_passed = bool(config.get("token_passed"))
1157
+ if not test and not explicit_token_passed:
1157
1158
  git_branch = get_current_git_branch()
1158
1159
  if git_branch and not is_main_git_branch(git_branch):
1159
1160
  local_branch = get_tinybird_branch_name_from_git_branch(git_branch)
@@ -1614,6 +1614,7 @@ def run_aws_iamrole_connection_flow(
1614
1614
  connection_name: str,
1615
1615
  policy: str,
1616
1616
  local_unavailable: bool = False,
1617
+ env: str = "local",
1617
1618
  ) -> Tuple[str, str, Optional[TinyB], Optional[TinyB]]:
1618
1619
  """
1619
1620
  Run the interactive AWS IAM Role connection flow for S3 or DynamoDB.
@@ -1628,6 +1629,9 @@ def run_aws_iamrole_connection_flow(
1628
1629
  connection_name: The name for the connection being created.
1629
1630
  policy: The access policy type ('read' or 'write').
1630
1631
  local_unavailable: If True, local environment is unavailable (e.g., missing AWS credentials).
1632
+ env: The active target environment ('local' or 'cloud'). When not local, the
1633
+ passed-in `client` is reused as the cloud client so secrets land in the
1634
+ workspace the command targets (including branches).
1631
1635
 
1632
1636
  Returns:
1633
1637
  A tuple containing:
@@ -1736,12 +1740,15 @@ def run_aws_iamrole_connection_flow(
1736
1740
 
1737
1741
  if use_cloud:
1738
1742
  try:
1739
- cloud_client = TinyB(
1740
- token=config.get("token", ""),
1741
- host=config.get("host", ""),
1742
- staging=False,
1743
- request_from=getattr(client, "request_from", None),
1744
- )
1743
+ if env != "local":
1744
+ cloud_client = client
1745
+ else:
1746
+ cloud_client = TinyB(
1747
+ token=config.get("token", ""),
1748
+ host=config.get("host", ""),
1749
+ staging=False,
1750
+ request_from=getattr(client, "request_from", None),
1751
+ )
1745
1752
  except Exception as e:
1746
1753
  click.echo(FeedbackManager.warning(message=f"Failed to initialize cloud client: {e}"))
1747
1754
  click.echo(FeedbackManager.warning(message="Continuing without cloud environment."))
@@ -158,6 +158,7 @@ def connection_create_dynamodb(
158
158
  connection_name=connection_name,
159
159
  policy="read",
160
160
  local_unavailable=local_aws_unavailable,
161
+ env=env,
161
162
  )
162
163
 
163
164
  unique_suffix = uuid.uuid4().hex[:8]
@@ -363,6 +363,7 @@ def connection_create_s3(
363
363
  connection_name=connection_name,
364
364
  policy=access_type.lower(),
365
365
  local_unavailable=local_aws_unavailable,
366
+ env=env,
366
367
  )
367
368
  unique_suffix = uuid.uuid4().hex[:8] # Use first 8 chars of a UUID for brevity
368
369
  secret_name = f"s3_role_arn_{connection_name}_{unique_suffix}"
@@ -862,20 +862,50 @@ def datasource_sync(ctx: Context, datasource_name: str, yes: bool):
862
862
  default=False,
863
863
  help="Wait for the import job to finish",
864
864
  )
865
+ @click.option(
866
+ "--rows",
867
+ default=None,
868
+ type=int,
869
+ help="For DynamoDB, the maximum number of rows to scan and import (default 1500; mutually exclusive with --max-bytes)",
870
+ )
871
+ @click.option(
872
+ "--max-bytes",
873
+ default=None,
874
+ type=str,
875
+ help="For DynamoDB, the maximum approximate JSONEachRow bytes to import, e.g. 500MB (capped at 10GB by default; mutually exclusive with --rows)",
876
+ )
877
+ @click.option(
878
+ "--full-export",
879
+ is_flag=True,
880
+ default=False,
881
+ help="For DynamoDB, trigger a full PITR export instead of a bounded sample (mutually exclusive with --rows and --max-bytes)",
882
+ )
865
883
  @click.pass_context
866
- def datasource_sample(ctx: Context, datasource_name: str, max_files: int, wait: bool) -> None:
867
- """Import sample data from a datasource connected to an S3 or GCS bucket.
868
-
869
- This command only works with datasources that have IMPORT_CONNECTION_NAME and
870
- IMPORT_BUCKET_URI configured (i.e., datasources ingesting from S3 or GCS).
871
- It imports a limited number of files from the bucket URI pattern, useful for
872
- testing data pipelines in branches without importing the entire dataset.
884
+ def datasource_sample(
885
+ ctx: Context,
886
+ datasource_name: str,
887
+ max_files: int,
888
+ wait: bool,
889
+ rows: Optional[int],
890
+ max_bytes: Optional[str],
891
+ full_export: bool,
892
+ ) -> None:
893
+ """Import sample data from a datasource connected to S3, GCS, or DynamoDB.
894
+
895
+ For S3 and GCS, this imports a limited number of files from the bucket URI
896
+ pattern. For DynamoDB, this scans and imports a bounded sample limited by
897
+ either --rows or --max-bytes (defaulting to 1500 rows), or --full-export to
898
+ trigger a PITR export of the whole table.
873
899
 
874
900
  By default, returns immediately with job info. Use --wait to block until complete.
875
901
 
876
902
  Examples:
877
903
  tb --branch=my_branch datasource sample my_s3_ds
878
904
  tb --branch=my_branch datasource sample my_s3_ds --max-files 3 --wait
905
+ tb --branch=my_branch datasource sample my_dynamodb_ds --wait
906
+ tb --branch=my_branch datasource sample my_dynamodb_ds --rows 100000 --wait
907
+ tb --branch=my_branch datasource sample my_dynamodb_ds --max-bytes 1GB --wait
908
+ tb --branch=my_branch datasource sample my_dynamodb_ds --full-export --wait
879
909
  """
880
910
  from tinybird.tb.modules.common import wait_job
881
911
  from tinybird.tb.modules.job_common import echo_job_url
@@ -884,10 +914,32 @@ def datasource_sample(ctx: Context, datasource_name: str, max_files: int, wait:
884
914
  client: TinyB = ctx.obj["client"]
885
915
  config = ctx.obj.get("config", {})
886
916
 
917
+ if rows is not None and max_bytes is not None:
918
+ raise CLIDatasourceException(
919
+ FeedbackManager.error(message="--rows and --max-bytes are mutually exclusive; pass only one.")
920
+ )
921
+
922
+ if full_export and (rows is not None or max_bytes is not None):
923
+ raise CLIDatasourceException(
924
+ FeedbackManager.error(message="--full-export cannot be combined with --rows or --max-bytes.")
925
+ )
926
+
927
+ if full_export:
928
+ click.echo(
929
+ FeedbackManager.warning(
930
+ message=(
931
+ "DynamoDB full export samples will import the whole table. "
932
+ "Use --rows or --max-bytes without --full-export for a bounded sample."
933
+ )
934
+ )
935
+ )
936
+
887
937
  click.echo(FeedbackManager.info(message=f"Starting sample import for {datasource_name}..."))
888
938
 
889
939
  # Start the job
890
- result = client.datasource_sample(datasource_name, max_files=max_files)
940
+ result = client.datasource_sample(
941
+ datasource_name, max_files=max_files, rows=rows, max_bytes=max_bytes, full_export=full_export
942
+ )
891
943
 
892
944
  job_id = result.get("job_id") or result.get("id")
893
945
  if not job_id:
@@ -21,7 +21,6 @@ from tinybird.tb.modules.feedback_manager import FeedbackManager
21
21
 
22
22
  LOG_SOURCES: Tuple[str, ...] = (
23
23
  "tinybird.pipe_stats_rt",
24
- "tinybird.bi_stats_rt",
25
24
  "tinybird.block_log",
26
25
  "tinybird.datasources_ops_log",
27
26
  "tinybird.endpoint_errors",
@@ -39,7 +38,6 @@ DEFAULT_LOG_SOURCES: Tuple[str, ...] = (
39
38
 
40
39
  TIMESTAMP_COLUMNS: Dict[str, str] = {
41
40
  "tinybird.pipe_stats_rt": "start_datetime",
42
- "tinybird.bi_stats_rt": "start_datetime",
43
41
  "tinybird.block_log": "timestamp",
44
42
  "tinybird.datasources_ops_log": "timestamp",
45
43
  "tinybird.endpoint_errors": "start_datetime",
@@ -61,16 +59,6 @@ RELEVANT_DETAIL_FIELDS: Dict[str, Tuple[str, ...]] = {
61
59
  "read_bytes",
62
60
  "result_rows",
63
61
  ),
64
- "tinybird.bi_stats_rt": (
65
- "query_normalized",
66
- "error_code",
67
- "error",
68
- "duration",
69
- "read_rows",
70
- "read_bytes",
71
- "result_rows",
72
- "result_bytes",
73
- ),
74
62
  "tinybird.block_log": (
75
63
  "datasource_name",
76
64
  "status",
@@ -207,7 +195,7 @@ _TEMPORAL_DETAIL_FIELDS = {
207
195
  _DURATION_DETAIL_FIELDS = {"duration", "elapsed_time", "processing_time"}
208
196
  _ROW_COUNT_DETAIL_FIELDS = {"rows", "read_rows", "written_rows", "result_rows", "quarantine_lines"}
209
197
  _BYTE_COUNT_DETAIL_FIELDS = {"bytes", "read_bytes", "result_bytes", "written_bytes"}
210
- _MILLISECOND_DURATION_SOURCES = {"tinybird.bi_stats_rt"}
198
+ _MILLISECOND_DURATION_SOURCES: Set[str] = set()
211
199
 
212
200
 
213
201
  def _to_iso_utc(dt: datetime) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 4.6.9.dev0
3
+ Version: 4.6.13.dev0
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -52,6 +52,28 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
52
52
  Changelog
53
53
  ----------
54
54
 
55
+ 4.6.12
56
+ ********
57
+
58
+ - `Added` Forward sink pipes support ``{{ tb_secret(...) }}`` in ``EXPORT_*`` metadata (for example ``EXPORT_BUCKET_URI``). Secrets are resolved at deploy time and baked into the sink settings; changing a secret requires a redeploy.
59
+ - `Fixed` Forward deploy now raises an error when a secret used in sink metadata resolves to an empty value.
60
+ - `Fixed` Forward deploy now rejects ``{{ tb_secret(...) }}`` in ``EXPORT_SCHEDULE`` and ``COPY_SCHEDULE``. Use a literal cron expression (for example ``0 * * * *``) or ``@on-demand``.
61
+
62
+ 4.6.11
63
+ ********
64
+
65
+ - `Added` `tb datasource sample` support for DynamoDB connectors.
66
+
67
+ 4.6.10
68
+ ********
69
+
70
+ - `Removed` `tinybird.bi_stats_rt` from `tb logs` sources. The BI Connector `bi_stats` / `bi_stats_rt` service datasources are deprecated.
71
+
72
+ 4.6.9
73
+ *******
74
+
75
+ - `Fixed` Commands run against Tinybird Local with an explicit token (``--token``) now target the token's workspace.
76
+
55
77
  4.6.8
56
78
  *******
57
79
 
@@ -3,41 +3,41 @@ tinybird/datatypes.py,sha256=DRScTI1-lgYjbC7u_4qxOLFleWL-vief_eBg_9WU37U,11304
3
3
  tinybird/feedback_manager.py,sha256=ZnRsExCkEZKuslmSxn3m706-0UIZ_qOpdek9ar16KOI,68001
4
4
  tinybird/git_settings.py,sha256=mqWgeboOlOFsSo97qyv595UCR2R1QCAqT4GTawBNPBg,3935
5
5
  tinybird/prompts.py,sha256=FNYpphDYlrXpm5AQM3yfQPbj4gd_TVnwoaoeswtx0OE,32699
6
- tinybird/service_datasources.py,sha256=Fp3a_Evte1u1U4f4D8fchgbtv6BgJIIyBbmOLtgLu4Q,58162
6
+ tinybird/service_datasources.py,sha256=asBTXONMv7sf7LFBx1a4eTcZCHPUq-I3r030AtorRzY,53643
7
7
  tinybird/sql.py,sha256=L3tTmm62iTrf6WQdcV8mmwrCMitLFUvJ3OfDzyRTMGw,47385
8
- tinybird/sql_template.py,sha256=8A5Jfzej7OHXzbfFMGpbq-YUzSEOoDHy5u7uy0vLZo4,127947
8
+ tinybird/sql_template.py,sha256=Mus34RE6DBcq57mGRu6vibXuCgov9htTPqqSkNknmR0,129666
9
9
  tinybird/sql_template_fmt.py,sha256=Ma4qcs-2r8ZXQC4GUmrCqYz34DsnGF8k5lE2Jwnr314,10638
10
- tinybird/sql_toolset.py,sha256=TvWeH3DsB13SfJ97V_t_ZaFTJ1IAtsrxQ8E3FLL8_tE,27442
10
+ tinybird/sql_toolset.py,sha256=I_RHAfhpYuIc3bNY9VcNjQ3zESAawb3D9ZMjb9-ev4M,28519
11
11
  tinybird/syncasync.py,sha256=rIPmCvygWSFqfnlVqhZH4N9gVVTvD6DEPsfoxGizYrI,27776
12
12
  tinybird/tornado_template.py,sha256=1_0nYFk_xJh_TMHh6AKkJILvnNY6xYmaM-uJ3Ofv7e8,42085
13
13
  tinybird/ch_utils/constants.py,sha256=yTNizMzgYNBzUc2EV3moBfdrDIggOe9hiuAgWF7sv2c,4333
14
- tinybird/ch_utils/engine.py,sha256=rZ2f5QsG3iDB60a1IwSJmTIqcCkjTHe5GzBPqTGvvOc,36711
15
- tinybird/datafile/common.py,sha256=pe9en0Kr8Bat_DyQjSjO2185BnEHkXGwrGN-tlaX68A,110244
14
+ tinybird/ch_utils/engine.py,sha256=OkR86FOOUPRpCsRxlR-kunykg-K4Daqo3uS06X1uvYQ,36887
15
+ tinybird/datafile/common.py,sha256=pLB5GmCCDLnafDEGp_-HnS8z_dYLTclYSk_bBpEhpro,111128
16
16
  tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1wnI,556
17
17
  tinybird/datafile/parse_connection.py,sha256=GxmGp_XnWbDZPDbh_PBxitlIMqZRYfDwxMBw-JQBp1g,1890
18
18
  tinybird/datafile/parse_datasource.py,sha256=yd58HrUF4yNJXLn6OsvKGpZJpvrcjLGAeJG1lgBe_zk,1891
19
19
  tinybird/datafile/parse_pipe.py,sha256=-9bbgVuiWRyDYydrLVflDBt8GstZotMy6dklsrc6MUY,3859
20
20
  tinybird/iterating/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  tinybird/iterating/data_branch_modes.py,sha256=5YuDa-gr8mKwmES8Xro6TRKbQtaOtIw4GbC606Qhu3o,184
22
- tinybird/tb/__cli__.py,sha256=Q6C-ebNNuhW7FQDE0FltpvE9ga2KHukMPoTpKGjTJAE,245
22
+ tinybird/tb/__cli__.py,sha256=GqDS3d3b9YPNF68yqEjPdtNXJzS_oz7KOt2Mo9nxlCk,246
23
23
  tinybird/tb/check_pypi.py,sha256=Gp0HkHHDFMSDL6nxKlOY51z7z1Uv-2LRexNTZSHHGmM,552
24
24
  tinybird/tb/cli.py,sha256=IjiGfNIpxSxi1odK1kMj9s8lEhx3sAUgGA263XdmyR0,1119
25
- tinybird/tb/client.py,sha256=wgXYa_CSKi-ymWnX-j8-CNHCNdQrUWQA6ezT0REMPOs,56830
25
+ tinybird/tb/client.py,sha256=LQ470JkU9NQcmM_rjElD_aV2l8nlWHTKuqzC3Yx1IzM,57605
26
26
  tinybird/tb/config.py,sha256=l6NtYvSPZDVOtt1W4G4ExXXDIf0hm6lvRnRoDMbdrsY,5429
27
27
  tinybird/tb/modules/branch.py,sha256=U50nj2kZllCkOHQfJWQ-YQ260pvlcssgStMSovmqMiU,10157
28
28
  tinybird/tb/modules/build.py,sha256=q7-aVM7q1tApiax_y6vYSCZq4cJ9ATSAm9D-elpNp7I,6543
29
29
  tinybird/tb/modules/build_common.py,sha256=o04aeaoyGTnwhR0cEXAgQzc7SJya97YECoEihsi4SyU,24979
30
30
  tinybird/tb/modules/cicd.py,sha256=IO4qqsoLRXcubALb7vx_QnRpg3zIIxfaVO9bGomlESY,8267
31
- tinybird/tb/modules/cli.py,sha256=3cOuWvSSa6MDebgTT1g1YaxvB57JRmfFKtnS_vJDbwc,43947
32
- tinybird/tb/modules/common.py,sha256=8S_Z49hY4n-uZ_rdEZTFvzbQVG7aV0PBhZx3OBfgN3g,92987
31
+ tinybird/tb/modules/cli.py,sha256=oerpcsZ3vLsLZdCjgtVa6F9D3OFj-cqWFJfX-M40N2c,44038
32
+ tinybird/tb/modules/common.py,sha256=XWuEIpMo3-JkplFqPzgxQqJDKAZD7rujY2VOXerGPPQ,93355
33
33
  tinybird/tb/modules/config.py,sha256=kZsMBf_qrGXPvhx1GW46DXS62ClVIKFFPjDXEZyGeyE,11181
34
34
  tinybird/tb/modules/connection.py,sha256=HwHn0YgwqcKEYCLr2OqDzJzAUFd65Wv8MZiSUw_TLII,18452
35
- tinybird/tb/modules/connection_dynamodb.py,sha256=j1Z0tXrlTZJU1LJ0rEzv3ezo0pS4PJJKfVjUTGrH3nk,9473
35
+ tinybird/tb/modules/connection_dynamodb.py,sha256=vaB28d6t8nOd6kcUQGMUkl3IZin5DYSBN2TBPm2968Q,9490
36
36
  tinybird/tb/modules/connection_kafka.py,sha256=Qc8uUNl5SvUcUGb_aG3AirYKw6673FoX1Sfdlzgz6Y8,33530
37
- tinybird/tb/modules/connection_s3.py,sha256=YsE5AttknPlGVX7ebP2J0djVVJAqXRwH2fQ6dXu7yoQ,16482
37
+ tinybird/tb/modules/connection_s3.py,sha256=eR7lsrCYbVWZ8LI7F5MUBxm8_DAvuaVSKqUDq9CUUks,16499
38
38
  tinybird/tb/modules/copy.py,sha256=Apzjxiyinp6KmgamypPKEe3BAnpxG7MwYcSYkHiG8sA,6033
39
39
  tinybird/tb/modules/create.py,sha256=G3_wCUjNVzBiowo70NgwzmUUXXOxYO2LWjF9eEfcffc,22947
40
- tinybird/tb/modules/datasource.py,sha256=tblNurkQXi6fps2izOjsg4xmGkPKd4FhBHPvrrN3PE8,74779
40
+ tinybird/tb/modules/datasource.py,sha256=baxfUQd7vDyEDh65fyOa2X5Xwf4OS3ligyznbbpAo6Q,76671
41
41
  tinybird/tb/modules/deployment.py,sha256=TNDlvaYmk-zouvaAdMXlyFPbgbnMKQOt76xpZBggJI0,21184
42
42
  tinybird/tb/modules/deployment_common.py,sha256=fLPWNNs8ZwSkWHJjLeqQUqTbKqnnhW1y8yKffBdDCo0,31948
43
43
  tinybird/tb/modules/deprecations.py,sha256=XXekrzPO9v12F1ToQDUGzLYJJ2wrEUlGKOkLCSdfHiM,4935
@@ -57,7 +57,7 @@ tinybird/tb/modules/local_logs.py,sha256=TTrfEQr3wVoddrQiKslSWJQ8BE4FOlMQZbH1yJ5
57
57
  tinybird/tb/modules/login.py,sha256=fxgcsN0fMhjzT_6C8VrwGTxnLxLVuWlZvASFr8I9yUU,1885
58
58
  tinybird/tb/modules/login_common.py,sha256=oRIAJm1cwJ4t4hZb1RKRmyylZuYz0mrEBo0xTOZHSQ8,22185
59
59
  tinybird/tb/modules/logout.py,sha256=sniI4JNxpTrVeRCp0oGJuQ3yRerG4hH5uz6oBmjv724,1009
60
- tinybird/tb/modules/logs.py,sha256=uSQi_A6QIFOjAoj1h8XOI_51AQ3uk5UEg2TjYbZxDJA,21462
60
+ tinybird/tb/modules/logs.py,sha256=M05V2OgWqv2suaCzz8c5IDfCnXhVqZ9WIh8rNQpolSg,21165
61
61
  tinybird/tb/modules/materialization.py,sha256=SaomNeaAzLWtcnsZdetYBxEq0ihY1cRzh23n3Z1P_c4,5643
62
62
  tinybird/tb/modules/open.py,sha256=ddABA4guIrBhrCKXYpkH4cfiAcFOG2B1eVePk5BEXto,1799
63
63
  tinybird/tb/modules/pipe.py,sha256=xPKtezhnWZ6k_g82r4XpgKslofhuIxb_PvynH4gdUzI,2393
@@ -99,8 +99,8 @@ tinybird/tb_cli_modules/config.py,sha256=Ey9yqM27C4Irglm5-63RXte8K0bFh8ConKdIoAa
99
99
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
100
100
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
101
101
  tinybird/tb_cli_modules/telemetry.py,sha256=W098H6jmS4kpE7hN3tadaREBTf7oMocel-lkKWN0pU8,10466
102
- tinybird-4.6.9.dev0.dist-info/METADATA,sha256=T5FGarUwhK0x2IVlkyyN-7JYmMPXUUWZIsU97s6ZXA8,14789
103
- tinybird-4.6.9.dev0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
104
- tinybird-4.6.9.dev0.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
105
- tinybird-4.6.9.dev0.dist-info/top_level.txt,sha256=ZIQJTPCzMqnfDzM_hEGZrJqDSEcKnIK_49T86DGWpyQ,78
106
- tinybird-4.6.9.dev0.dist-info/RECORD,,
102
+ tinybird-4.6.13.dev0.dist-info/METADATA,sha256=gJeFvTk8TuQQGSnCdwvIHXhQSPJhzgXW3PHVqHmDFyM,15709
103
+ tinybird-4.6.13.dev0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
104
+ tinybird-4.6.13.dev0.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
105
+ tinybird-4.6.13.dev0.dist-info/top_level.txt,sha256=ZIQJTPCzMqnfDzM_hEGZrJqDSEcKnIK_49T86DGWpyQ,78
106
+ tinybird-4.6.13.dev0.dist-info/RECORD,,