tinybird 4.5.3.dev0__tar.gz → 4.5.4__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.
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/PKG-INFO +12 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/common.py +3 -2
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datatypes.py +2 -2
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/service_datasources.py +3 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql.py +13 -7
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_template.py +1 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_toolset.py +1 -19
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/__cli__.py +2 -2
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/branch.py +0 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/cicd.py +4 -4
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/cli.py +6 -6
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/common.py +10 -8
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/create.py +0 -12
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build.py +2 -2
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_datasource.py +1 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_pipe.py +1 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/playground.py +2 -2
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deployment.py +162 -4
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deployment_common.py +122 -28
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/job.py +1 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local.py +10 -11
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/login_common.py +2 -4
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/query_output.py +1 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/workspace.py +10 -11
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/workspace_members.py +1 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/branch.py +4 -6
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/cli.py +1 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/common.py +5 -4
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/job.py +1 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tag.py +2 -3
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/workspace.py +10 -11
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/PKG-INFO +12 -1
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/setup.cfg +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/__cli__.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/ch_utils/constants.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/ch_utils/engine.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/check_pypi.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/client.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/config.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/context.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/exceptions.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_connection.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_datasource.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/datafile/parse_pipe.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/feedback_manager.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/git_settings.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/prompts.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/sql_template_fmt.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/syncasync.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/check_pypi.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/cli.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/client.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/config.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/build.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/build_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/config.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection_kafka.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/connection_s3.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/copy.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/build_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/diff.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/fixture.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_connection.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datafile/pull.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/datasource.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/deprecations.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/endpoint.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/exceptions.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/feedback_manager.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/fmt.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/info.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/infra.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/job_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/llm.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/llm_utils.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/local_logs.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/login.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/logout.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/logs.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/materialization.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/open.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/pipe.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/preview.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/project.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/project_commands.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/py_project.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/regions.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/secret.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/secret_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/sink.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/table.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/telemetry.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/test.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/test_common.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/token.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/ts_project.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb/modules/watch.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/auth.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/cicd.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/config.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/connection.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/datasource.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/exceptions.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/fmt.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/pipe.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/regions.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/telemetry.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/test.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tb_cli_modules/workspace_members.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird/tornado_template.py +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/SOURCES.txt +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/dependency_links.txt +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/entry_points.txt +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/requires.txt +0 -0
- {tinybird-4.5.3.dev0 → tinybird-4.5.4}/tinybird.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: tinybird
|
|
3
|
-
Version: 4.5.
|
|
3
|
+
Version: 4.5.4
|
|
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,17 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
|
|
|
52
52
|
Changelog
|
|
53
53
|
----------
|
|
54
54
|
|
|
55
|
+
4.5.4
|
|
56
|
+
*******
|
|
57
|
+
|
|
58
|
+
- `Added` `tb migrate-to-forward` to help users migrate Classic workspaces to Forward
|
|
59
|
+
|
|
60
|
+
4.5.3
|
|
61
|
+
*******
|
|
62
|
+
|
|
63
|
+
- `Changed` `tb init` now defaults to CLI projects when `--type` is not provided.
|
|
64
|
+
- `Fixed` GitHub and GitLab CI templates generated by `tb init` now run `tb --local build` and `tb --local test run`.
|
|
65
|
+
|
|
55
66
|
4.5.2
|
|
56
67
|
***********
|
|
57
68
|
|
|
@@ -1394,7 +1394,8 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
|
|
|
1394
1394
|
if match := _PATTERN_SIMPLE_AGG_FUNC.search(t):
|
|
1395
1395
|
fn = match.group(1)
|
|
1396
1396
|
inner_type = match.group(2)
|
|
1397
|
-
|
|
1397
|
+
if "Nullable(" not in inner_type:
|
|
1398
|
+
result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
|
|
1398
1399
|
return result
|
|
1399
1400
|
|
|
1400
1401
|
|
|
@@ -2366,7 +2367,7 @@ def get_project_fixtures(folder: str) -> List[str]:
|
|
|
2366
2367
|
def has_internal_datafiles(folder: str) -> bool:
|
|
2367
2368
|
folder = folder or "."
|
|
2368
2369
|
filenames = get_project_filenames(folder)
|
|
2369
|
-
return any(
|
|
2370
|
+
return any(f for f in filenames if "spans" in str(f) and "vendor" not in str(f))
|
|
2370
2371
|
|
|
2371
2372
|
|
|
2372
2373
|
def peek(iterable):
|
|
@@ -117,11 +117,11 @@ def date_test(x: str) -> bool:
|
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
def datetime64_test(x: str) -> bool:
|
|
120
|
-
return any(
|
|
120
|
+
return any(p.match(x) for p in datetime64_patterns)
|
|
121
121
|
|
|
122
122
|
|
|
123
123
|
def datetime_test(x: str) -> bool:
|
|
124
|
-
return any(
|
|
124
|
+
return any(p.match(x) for p in datetime_patterns)
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
def int_8_test(x: str) -> bool:
|
|
@@ -45,6 +45,7 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
|
|
|
45
45
|
{"name": "method", "type": "String"},
|
|
46
46
|
{"name": "release", "type": "String"},
|
|
47
47
|
{"name": "user_agent", "type": "Nullable(String)"},
|
|
48
|
+
{"name": "client_ip", "type": "Nullable(String)"},
|
|
48
49
|
{"name": "resource_tags", "type": "Array(String)"},
|
|
49
50
|
{"name": "memory_usage", "type": "UInt64"},
|
|
50
51
|
],
|
|
@@ -506,6 +507,8 @@ def get_tinybird_service_datasources() -> List[Dict[str, Any]]:
|
|
|
506
507
|
{"name": "ch_written_bytes", "type": "UInt64"},
|
|
507
508
|
{"name": "ch_cpu_time", "type": "Float32"},
|
|
508
509
|
{"name": "rate_limited", "type": "UInt8"},
|
|
510
|
+
{"name": "ips", "type": "SimpleAggregateFunction(groupUniqArrayArray, Array(String))"},
|
|
511
|
+
{"name": "tokens", "type": "SimpleAggregateFunction(groupUniqArrayArray, Array(String))"},
|
|
509
512
|
],
|
|
510
513
|
},
|
|
511
514
|
{
|
|
@@ -233,10 +233,21 @@ def try_to_fix_nullable_in_simple_aggregating_function(t: str) -> Optional[str]:
|
|
|
233
233
|
if match := _RE_TRY_FIX_NULLABLE_SAF.search(t):
|
|
234
234
|
fn = match.group(1)
|
|
235
235
|
inner_type = match.group(2)
|
|
236
|
-
|
|
236
|
+
if "Nullable(" not in inner_type:
|
|
237
|
+
result = f"SimpleAggregateFunction({fn}, Nullable({inner_type}))"
|
|
237
238
|
return result
|
|
238
239
|
|
|
239
240
|
|
|
241
|
+
def wrap_nullable(col: dict[str, Any]):
|
|
242
|
+
if col["nullable"]:
|
|
243
|
+
if (col_type := try_to_fix_nullable_in_simple_aggregating_function(col["type"])) is None:
|
|
244
|
+
# Skip wrapping if Nullable already present, e.g. LowCardinality(Nullable(String))
|
|
245
|
+
col_type = col["type"] if "Nullable(" in col["type"] else "Nullable(%s)" % col["type"]
|
|
246
|
+
else:
|
|
247
|
+
col_type = col["type"]
|
|
248
|
+
return col_type
|
|
249
|
+
|
|
250
|
+
|
|
240
251
|
def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = False) -> List[str]:
|
|
241
252
|
"""return an array with each column in SQL
|
|
242
253
|
>>> schema_to_sql_columns([{'name': 'temperature', 'type': 'Float32', 'codec': None, 'default_value': None, 'nullable': False, 'normalized_name': 'temperature'}, {'name': 'temperature_delta', 'type': 'Float32', 'codec': 'CODEC(Delta(4), LZ4))', 'default_value': 'MATERIALIZED temperature', 'nullable': False, 'normalized_name': 'temperature_delta'}])
|
|
@@ -255,12 +266,7 @@ def schema_to_sql_columns(schema: List[Dict[str, Any]], skip_jsonpaths: bool = F
|
|
|
255
266
|
columns: List[str] = []
|
|
256
267
|
for x in schema:
|
|
257
268
|
name = x["normalized_name"] if "normalized_name" in x else x["name"]
|
|
258
|
-
|
|
259
|
-
if (_type := try_to_fix_nullable_in_simple_aggregating_function(x["type"])) is None:
|
|
260
|
-
# Skip wrapping if Nullable already present, e.g. LowCardinality(Nullable(String))
|
|
261
|
-
_type = x["type"] if "Nullable(" in x["type"] else "Nullable(%s)" % x["type"]
|
|
262
|
-
else:
|
|
263
|
-
_type = x["type"]
|
|
269
|
+
_type = wrap_nullable(x)
|
|
264
270
|
parts = [col_name(name, backquotes=True), _type]
|
|
265
271
|
if x.get("jsonpath", None) and not skip_jsonpaths:
|
|
266
272
|
parts.append(f"`json:{x['jsonpath']}`")
|
|
@@ -2826,7 +2826,7 @@ def render_sql_template(
|
|
|
2826
2826
|
return Comment("error launched")
|
|
2827
2827
|
|
|
2828
2828
|
v: dict = {x["name"]: Placeholder(x["name"], x["line"]) for x in template_variables}
|
|
2829
|
-
is_tb_secret = any(
|
|
2829
|
+
is_tb_secret = any(s for s in template_variables if s["name"] == "tb_secret" or s["name"] == "tb_var")
|
|
2830
2830
|
|
|
2831
2831
|
if variables:
|
|
2832
2832
|
v.update(variables)
|
|
@@ -22,7 +22,7 @@ VALID_REMOTE = "VALID_REMOTE"
|
|
|
22
22
|
|
|
23
23
|
class InvalidFunction(ValueError):
|
|
24
24
|
def __init__(self, msg: str = "", table_function_name: str = ""):
|
|
25
|
-
if any(
|
|
25
|
+
if any(fn for fn in COPY_ENABLED_TABLE_FUNCTIONS if fn in msg):
|
|
26
26
|
msg = msg.replace("is restricted", "is restricted to Copy Pipes")
|
|
27
27
|
|
|
28
28
|
if table_function_name:
|
|
@@ -75,19 +75,6 @@ def explain_plan(sql: str) -> str:
|
|
|
75
75
|
return chquery.explain_ast(sql)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
@dataclass(frozen=True)
|
|
79
|
-
class ColumnInfo:
|
|
80
|
-
name: str
|
|
81
|
-
type: str
|
|
82
|
-
nullable: bool
|
|
83
|
-
default_specifier: str = ""
|
|
84
|
-
default_expression: str | None = None
|
|
85
|
-
codec: str | None = None
|
|
86
|
-
comment: str | None = None
|
|
87
|
-
ttl: str | None = None
|
|
88
|
-
is_primary_key: bool = False
|
|
89
|
-
|
|
90
|
-
|
|
91
78
|
@dataclass
|
|
92
79
|
class MaterializedViewTarget:
|
|
93
80
|
database: Optional[str]
|
|
@@ -118,11 +105,6 @@ def parse_materialized_view_target(create_table_query: str) -> Optional[Material
|
|
|
118
105
|
)
|
|
119
106
|
|
|
120
107
|
|
|
121
|
-
def get_columns_from_create_query(sql_schema: str) -> list[ColumnInfo]:
|
|
122
|
-
columns = chquery.get_columns_from_create_query(sql_schema)
|
|
123
|
-
return [ColumnInfo(**col) for col in columns]
|
|
124
|
-
|
|
125
|
-
|
|
126
108
|
def has_join(sql: str) -> bool:
|
|
127
109
|
return any(line.rstrip().startswith("TableJoin") for line in explain_plan(sql).split())
|
|
128
110
|
|
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/forward/commands'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '4.5.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '4.5.4'
|
|
8
|
+
__revision__ = 'eebce0e'
|
|
@@ -51,9 +51,9 @@ jobs:
|
|
|
51
51
|
- name: Install Tinybird CLI
|
|
52
52
|
run: curl https://tinybird.co | sh
|
|
53
53
|
- name: Build project
|
|
54
|
-
run: tb build
|
|
54
|
+
run: tb --local build
|
|
55
55
|
- name: Test project
|
|
56
|
-
run: tb test run
|
|
56
|
+
run: tb --local test run
|
|
57
57
|
- name: Deployment check
|
|
58
58
|
run: tb --cloud --host ${{! env.TINYBIRD_HOST }} --token ${{! env.TINYBIRD_TOKEN }} deploy --check
|
|
59
59
|
"""
|
|
@@ -115,8 +115,8 @@ tinybird_ci_workflow:
|
|
|
115
115
|
script:
|
|
116
116
|
- export PATH="$HOME/.local/bin:$PATH"
|
|
117
117
|
- cd $CI_PROJECT_DIR/{{ data_project_dir }}
|
|
118
|
-
- tb build
|
|
119
|
-
- tb test run
|
|
118
|
+
- tb --local build
|
|
119
|
+
- tb --local test run
|
|
120
120
|
- tb --cloud --host "$TINYBIRD_HOST" --token "$TINYBIRD_TOKEN" deploy --check
|
|
121
121
|
services:
|
|
122
122
|
- name: tinybirdco/tinybird-local:latest
|
|
@@ -63,7 +63,7 @@ DEV_MODE_VALUES = {DEV_MODE_MANUAL, DEV_MODE_LOCAL, DEV_MODE_BRANCH}
|
|
|
63
63
|
DEV_MODE_ROUTED_COMMANDS = {"build", "deploy"}
|
|
64
64
|
SDK_PROJECT_ROUTED_COMMANDS = {"build", "deploy", "preview"}
|
|
65
65
|
TS_PROJECT_ROUTED_COMMANDS = SDK_PROJECT_ROUTED_COMMANDS
|
|
66
|
-
COMMANDS_ALWAYS_CLOUD = {"infra", "branch", "environment", "workspace", "preview"}
|
|
66
|
+
COMMANDS_ALWAYS_CLOUD = {"infra", "branch", "environment", "workspace", "preview", "migrate-to-forward"}
|
|
67
67
|
PROJECT_TYPE_TYPESCRIPT = "ts-sdk"
|
|
68
68
|
PROJECT_TYPE_PYTHON = "python-sdk"
|
|
69
69
|
PROJECT_TYPE_CLI = "cli"
|
|
@@ -625,8 +625,10 @@ def cli(
|
|
|
625
625
|
config["dev_mode"] = tinybird_dev_mode
|
|
626
626
|
|
|
627
627
|
# Resolve project folder from tinybird.config.json (preferred) or legacy .tinyb cwd.
|
|
628
|
-
|
|
629
|
-
if
|
|
628
|
+
folder_from_config = get_project_folder_from_tinybird_config(os.getcwd())
|
|
629
|
+
if folder_from_config:
|
|
630
|
+
folder = folder_from_config
|
|
631
|
+
else:
|
|
630
632
|
tinyb_dir = os.path.dirname(config_temp._path) # Directory containing .tinyb file
|
|
631
633
|
cwd_config = config.get("cwd", ".")
|
|
632
634
|
|
|
@@ -870,9 +872,7 @@ def sql(
|
|
|
870
872
|
if output == "json":
|
|
871
873
|
echo_json(res, indent=8)
|
|
872
874
|
else:
|
|
873
|
-
dd = []
|
|
874
|
-
for d in res["data"]:
|
|
875
|
-
dd.append(d.values())
|
|
875
|
+
dd = [d.values() for d in res["data"]]
|
|
876
876
|
echo_safe_format_table(dd, columns=res["meta"])
|
|
877
877
|
else:
|
|
878
878
|
click.echo(FeedbackManager.info_no_rows())
|
|
@@ -699,10 +699,12 @@ def create_workspace_interactive(
|
|
|
699
699
|
def print_data_branch_summary(client, job_id, response=None):
|
|
700
700
|
response = client.job(job_id) if job_id else response or {"partitions": []}
|
|
701
701
|
columns = ["Data Source", "Partition", "Status", "Error"]
|
|
702
|
-
table = []
|
|
702
|
+
table: list[list] = []
|
|
703
703
|
for partition in response["partitions"]:
|
|
704
|
-
|
|
705
|
-
|
|
704
|
+
table.extend(
|
|
705
|
+
[partition["datasource"]["name"], p["partition"], p["status"], p.get("error", "")]
|
|
706
|
+
for p in partition["partitions"]
|
|
707
|
+
)
|
|
706
708
|
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
707
709
|
|
|
708
710
|
|
|
@@ -2262,10 +2264,11 @@ def create_organization_and_add_workspaces(
|
|
|
2262
2264
|
|
|
2263
2265
|
# Add existing orphan workspaces to the organization - this is only needed for backwards compatibility
|
|
2264
2266
|
user_workspaces = client.user_workspaces_with_organization(version="v1")
|
|
2265
|
-
workspaces_to_migrate = [
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2267
|
+
workspaces_to_migrate = [
|
|
2268
|
+
workspace["id"]
|
|
2269
|
+
for workspace in user_workspaces["workspaces"]
|
|
2270
|
+
if workspace.get("organization") is None and workspace.get("role") == "admin"
|
|
2271
|
+
]
|
|
2269
2272
|
client.add_workspaces_to_organization(organization["id"], workspaces_to_migrate)
|
|
2270
2273
|
|
|
2271
2274
|
return organization
|
|
@@ -2283,7 +2286,6 @@ def get_user_token(config: CLIConfig, user_token: Optional[str] = None) -> str:
|
|
|
2283
2286
|
check_user_token_with_client(client, user_token)
|
|
2284
2287
|
except Exception:
|
|
2285
2288
|
user_token = None
|
|
2286
|
-
pass
|
|
2287
2289
|
if not user_token:
|
|
2288
2290
|
user_token = ask_for_user_token("delete a workspace", ui_host)
|
|
2289
2291
|
if not user_token:
|
|
@@ -293,18 +293,6 @@ def _prompt_sdk(sdk: Optional[str]) -> str:
|
|
|
293
293
|
if sdk:
|
|
294
294
|
return sdk.lower()
|
|
295
295
|
|
|
296
|
-
click.echo(FeedbackManager.highlight(message="\n? Select project type:"))
|
|
297
|
-
click.echo(" [1] typescript - Tinybird TypeScript SDK")
|
|
298
|
-
click.echo(" [2] python - Tinybird Python SDK")
|
|
299
|
-
click.echo(" [3] cli - Tinybird CLI datafiles project")
|
|
300
|
-
choice = click.prompt("\nSelect option", default=3, type=int)
|
|
301
|
-
if choice == 1:
|
|
302
|
-
return "typescript"
|
|
303
|
-
if choice == 2:
|
|
304
|
-
return "python"
|
|
305
|
-
if choice == 3:
|
|
306
|
-
return "cli"
|
|
307
|
-
click.echo(FeedbackManager.warning(message=f"Invalid option '{choice}'. Defaulting to {DEFAULT_SDK}."))
|
|
308
296
|
return DEFAULT_SDK
|
|
309
297
|
|
|
310
298
|
|
|
@@ -544,7 +544,7 @@ def process(
|
|
|
544
544
|
if (
|
|
545
545
|
fork_downstream
|
|
546
546
|
and r.get("resource", "") == "pipes"
|
|
547
|
-
and any(
|
|
547
|
+
and any("engine" in x.get("params", {}) for x in r.get("nodes", []))
|
|
548
548
|
):
|
|
549
549
|
raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
|
|
550
550
|
|
|
@@ -1087,7 +1087,7 @@ def process_file(
|
|
|
1087
1087
|
deps = []
|
|
1088
1088
|
nodes: List[Dict[str, Any]] = []
|
|
1089
1089
|
|
|
1090
|
-
is_copy = any(
|
|
1090
|
+
is_copy = any(node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY)
|
|
1091
1091
|
for node in doc.nodes:
|
|
1092
1092
|
sql = node["sql"]
|
|
1093
1093
|
node_type = node.get("type", "standard").lower()
|
|
@@ -166,7 +166,7 @@ def new_ds(
|
|
|
166
166
|
existing = existing_ds.get("indexes", [])
|
|
167
167
|
new.sort(key=lambda x: x["name"])
|
|
168
168
|
existing.sort(key=lambda x: x["name"])
|
|
169
|
-
if len(existing) != len(new) or any(
|
|
169
|
+
if len(existing) != len(new) or any((d, d2) for d, d2 in zip(new, existing) if d != d2):
|
|
170
170
|
new_indices = ds.get("params", {}).get("indexes") or "0"
|
|
171
171
|
if (
|
|
172
172
|
new_description
|
|
@@ -253,7 +253,7 @@ def is_materialized(resource: Optional[Dict[str, Any]]) -> bool:
|
|
|
253
253
|
return False
|
|
254
254
|
|
|
255
255
|
is_materialized = any(
|
|
256
|
-
|
|
256
|
+
node.get("params", {}).get("type", None) == "materialized" for node in resource.get("nodes", []) or []
|
|
257
257
|
)
|
|
258
258
|
return is_materialized
|
|
259
259
|
|
|
@@ -702,7 +702,7 @@ def process(
|
|
|
702
702
|
if (
|
|
703
703
|
fork_downstream
|
|
704
704
|
and r.get("resource", "") == "pipes"
|
|
705
|
-
and any(
|
|
705
|
+
and any("engine" in x.get("params", {}) for x in r.get("nodes", []))
|
|
706
706
|
):
|
|
707
707
|
raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
|
|
708
708
|
|
|
@@ -1239,7 +1239,7 @@ def process_file(
|
|
|
1239
1239
|
deps = []
|
|
1240
1240
|
nodes: List[Dict[str, Any]] = []
|
|
1241
1241
|
|
|
1242
|
-
is_copy = any(
|
|
1242
|
+
is_copy = any(node for node in doc.nodes if node.get("type", "standard").lower() == PipeNodeTypes.COPY)
|
|
1243
1243
|
for node in doc.nodes:
|
|
1244
1244
|
sql = node["sql"]
|
|
1245
1245
|
node_type = node.get("type", "standard").lower()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Dict, Optional
|
|
@@ -7,14 +8,17 @@ from typing import Any, Dict, Optional
|
|
|
7
8
|
import click
|
|
8
9
|
import requests
|
|
9
10
|
|
|
11
|
+
from tinybird.tb.client import TinyB
|
|
10
12
|
from tinybird.tb.modules.cli import cli
|
|
11
13
|
from tinybird.tb.modules.common import (
|
|
12
14
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
13
15
|
sys_exit,
|
|
14
16
|
)
|
|
17
|
+
from tinybird.tb.modules.create import persist_tinybird_config
|
|
15
18
|
from tinybird.tb.modules.deployment_common import (
|
|
16
19
|
create_deployment,
|
|
17
20
|
discard_deployment,
|
|
21
|
+
migrate_to_forward_workspace,
|
|
18
22
|
promote_deployment,
|
|
19
23
|
)
|
|
20
24
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
@@ -151,12 +155,68 @@ def api_fetch(url: str, headers: dict, request_from: Optional[str] = None) -> di
|
|
|
151
155
|
return {}
|
|
152
156
|
|
|
153
157
|
|
|
158
|
+
def _get_classic_workspace_branches(client: TinyB, workspace_id: str) -> list[dict[str, Any]]:
|
|
159
|
+
branches: list[dict[str, Any]] = client.user_workspace_branches(version="v0").get("workspaces", [])
|
|
160
|
+
return [branch for branch in branches if str(branch.get("main")) == workspace_id]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_non_live_classic_releases(client: TinyB, workspace_id: str) -> list[dict[str, Any]]:
|
|
164
|
+
releases: list[dict[str, Any]] = client.releases(workspace_id=workspace_id).get("releases", [])
|
|
165
|
+
return [release for release in releases if release.get("status") != "live"]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _cleanup_classic_migration_blockers(client: TinyB, config: Dict[str, Any]) -> None:
|
|
169
|
+
workspace_id = str(config["id"])
|
|
170
|
+
workspace_name = str(config["name"])
|
|
171
|
+
branches = _get_classic_workspace_branches(client, workspace_id)
|
|
172
|
+
releases = _get_non_live_classic_releases(client, workspace_id)
|
|
173
|
+
|
|
174
|
+
if not branches and not releases:
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
for branch in branches:
|
|
179
|
+
client.delete_branch(id=str(branch["id"]))
|
|
180
|
+
|
|
181
|
+
for release in releases:
|
|
182
|
+
client.release_rm(
|
|
183
|
+
workspace_id=workspace_id,
|
|
184
|
+
semver=str(release["semver"]),
|
|
185
|
+
confirmation=workspace_name,
|
|
186
|
+
dry_run=False,
|
|
187
|
+
force=False,
|
|
188
|
+
)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
message = f"Error cleaning up Classic branches or releases before migration: {str(e)}"
|
|
191
|
+
click.echo(FeedbackManager.error(message=message))
|
|
192
|
+
sys_exit("migration_error", message)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _persist_migrate_to_forward_config(project: Project) -> None:
|
|
196
|
+
root_folder = os.getcwd()
|
|
197
|
+
project_folder = os.path.relpath(project.path.resolve(), root_folder)
|
|
198
|
+
|
|
199
|
+
config_changed, config_created = persist_tinybird_config(
|
|
200
|
+
root_folder=root_folder,
|
|
201
|
+
project_type="cli",
|
|
202
|
+
dev_mode="manual",
|
|
203
|
+
folder=project_folder,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if not config_changed:
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
message = "Created tinybird.config.json for the Forward CLI"
|
|
210
|
+
if not config_created:
|
|
211
|
+
message = "Updated tinybird.config.json for the Forward CLI"
|
|
212
|
+
click.echo(FeedbackManager.info(message=message))
|
|
213
|
+
|
|
214
|
+
|
|
154
215
|
@cli.group(name="deployment")
|
|
155
216
|
def deployment_group() -> None:
|
|
156
217
|
"""
|
|
157
218
|
Deployment commands.
|
|
158
219
|
"""
|
|
159
|
-
pass
|
|
160
220
|
|
|
161
221
|
|
|
162
222
|
@deployment_group.command(name="create")
|
|
@@ -262,9 +322,7 @@ def deployment_ls(ctx: click.Context, include_deleted: bool) -> None:
|
|
|
262
322
|
# Handle different output formats
|
|
263
323
|
if output == "json":
|
|
264
324
|
# Create JSON structure
|
|
265
|
-
deployments_json = []
|
|
266
|
-
for row in table:
|
|
267
|
-
deployments_json.append({"id": row[0], "status": row[1], "created_at": row[2]})
|
|
325
|
+
deployments_json = [{"id": row[0], "status": row[1], "created_at": row[2]} for row in table]
|
|
268
326
|
from tinybird.tb.modules.common import echo_json
|
|
269
327
|
|
|
270
328
|
echo_json({"deployments": deployments_json})
|
|
@@ -374,6 +432,104 @@ def deploy(
|
|
|
374
432
|
create_deployment_cmd(ctx, wait, auto, check, allow_destructive_operations, template, verbose)
|
|
375
433
|
|
|
376
434
|
|
|
435
|
+
@cli.command(name="migrate-to-forward")
|
|
436
|
+
@click.option(
|
|
437
|
+
"--allow-destructive-operations/--no-allow-destructive-operations",
|
|
438
|
+
is_flag=True,
|
|
439
|
+
default=False,
|
|
440
|
+
help="Allow destructive operations in deployments (for example replacing a Pipe with a Data Source).",
|
|
441
|
+
)
|
|
442
|
+
@click.pass_context
|
|
443
|
+
def migrate_to_forward(ctx: click.Context, allow_destructive_operations: bool) -> None:
|
|
444
|
+
"""Migrate a Tinybird Classic cloud workspace to Tinybird Forward."""
|
|
445
|
+
client = ctx.ensure_object(dict)["client"]
|
|
446
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
447
|
+
config: Dict[str, Any] = ctx.ensure_object(dict)["config"]
|
|
448
|
+
env = ctx.ensure_object(dict)["env"]
|
|
449
|
+
output = ctx.ensure_object(dict)["output"]
|
|
450
|
+
|
|
451
|
+
try:
|
|
452
|
+
workspace_info = client.workspace_info(version="v0")
|
|
453
|
+
except Exception as e:
|
|
454
|
+
message = f"Error checking workspace status: {str(e)}"
|
|
455
|
+
click.echo(FeedbackManager.error(message=message))
|
|
456
|
+
sys_exit("migration_error", message)
|
|
457
|
+
|
|
458
|
+
if workspace_info.get("is_forward", False):
|
|
459
|
+
message = "This command is unavailable for Tinybird Forward workspaces."
|
|
460
|
+
click.echo(FeedbackManager.error(message=message))
|
|
461
|
+
sys_exit("migration_error", message)
|
|
462
|
+
|
|
463
|
+
click.echo(
|
|
464
|
+
FeedbackManager.warning(
|
|
465
|
+
message=(
|
|
466
|
+
"This operation is irreversible: once your workspace is migrated to Tinybird Forward, "
|
|
467
|
+
"you cannot switch it back to Tinybird Classic. It will also run your first Forward deployment."
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
if not click.confirm("Do you want to proceed and run the deployment check now?", default=False):
|
|
473
|
+
click.echo(FeedbackManager.info(message="Migration cancelled."))
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
check_result = create_deployment(
|
|
477
|
+
project,
|
|
478
|
+
client,
|
|
479
|
+
config,
|
|
480
|
+
wait=False,
|
|
481
|
+
auto=False,
|
|
482
|
+
verbose=False,
|
|
483
|
+
check=True,
|
|
484
|
+
allow_destructive_operations=allow_destructive_operations,
|
|
485
|
+
output=output,
|
|
486
|
+
env=env,
|
|
487
|
+
show_migrate_to_forward_hint=False,
|
|
488
|
+
return_check_result=True,
|
|
489
|
+
skip_forward_workspace_validation=True,
|
|
490
|
+
)
|
|
491
|
+
if not check_result:
|
|
492
|
+
message = "Deployment check did not complete. Migration cancelled."
|
|
493
|
+
click.echo(FeedbackManager.error(message=message))
|
|
494
|
+
sys_exit("migration_error", message)
|
|
495
|
+
|
|
496
|
+
if check_result and check_result.get("status") == "no_changes":
|
|
497
|
+
click.echo(
|
|
498
|
+
FeedbackManager.warning(
|
|
499
|
+
message=(
|
|
500
|
+
"No deployment changes were detected. Add this dummy pipe to your workspace and run "
|
|
501
|
+
"`tb migrate-to-forward` again:"
|
|
502
|
+
)
|
|
503
|
+
)
|
|
504
|
+
)
|
|
505
|
+
click.echo("NODE n\nSQL >\n select 'Forward'")
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
if not click.confirm(
|
|
509
|
+
"Do you want to continue with the migration? This will also delete your branches, releases and switch your workspace from Classic to Forward.",
|
|
510
|
+
default=False,
|
|
511
|
+
):
|
|
512
|
+
click.echo(FeedbackManager.info(message="Migration cancelled."))
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
_persist_migrate_to_forward_config(project)
|
|
516
|
+
_cleanup_classic_migration_blockers(client, config)
|
|
517
|
+
migrate_to_forward_workspace(client=client, output=output, dry_run=False)
|
|
518
|
+
create_deployment(
|
|
519
|
+
project,
|
|
520
|
+
client,
|
|
521
|
+
config,
|
|
522
|
+
wait=True,
|
|
523
|
+
auto=True,
|
|
524
|
+
verbose=False,
|
|
525
|
+
check=False,
|
|
526
|
+
allow_destructive_operations=allow_destructive_operations,
|
|
527
|
+
output=output,
|
|
528
|
+
env=env,
|
|
529
|
+
skip_forward_workspace_validation=True,
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
377
533
|
def create_deployment_cmd(
|
|
378
534
|
ctx: click.Context,
|
|
379
535
|
wait: bool,
|
|
@@ -384,6 +540,7 @@ def create_deployment_cmd(
|
|
|
384
540
|
verbose: bool = False,
|
|
385
541
|
) -> None:
|
|
386
542
|
output = ctx.ensure_object(dict)["output"]
|
|
543
|
+
env = ctx.ensure_object(dict)["env"]
|
|
387
544
|
project: Project = ctx.ensure_object(dict)["project"]
|
|
388
545
|
if template:
|
|
389
546
|
if project.get_project_files():
|
|
@@ -419,6 +576,7 @@ def create_deployment_cmd(
|
|
|
419
576
|
allow_destructive_operations,
|
|
420
577
|
ingest_hint=not is_web_analytics_starter_kit,
|
|
421
578
|
output=output,
|
|
579
|
+
env=env,
|
|
422
580
|
)
|
|
423
581
|
show_web_analytics_starter_kit_hints(client, is_web_analytics_starter_kit)
|
|
424
582
|
|