tinybird 0.0.1.dev291__py3-none-any.whl → 1.0.5__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.
- tinybird/ch_utils/constants.py +5 -0
- tinybird/connectors.py +1 -7
- tinybird/context.py +3 -3
- tinybird/datafile/common.py +10 -8
- tinybird/datafile/parse_pipe.py +2 -2
- tinybird/feedback_manager.py +3 -0
- tinybird/prompts.py +1 -0
- tinybird/service_datasources.py +223 -0
- tinybird/sql_template.py +26 -11
- tinybird/sql_template_fmt.py +14 -4
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +1 -0
- tinybird/tb/client.py +104 -26
- tinybird/tb/config.py +24 -0
- tinybird/tb/modules/agent/agent.py +103 -67
- tinybird/tb/modules/agent/banner.py +15 -15
- tinybird/tb/modules/agent/explore_agent.py +5 -0
- tinybird/tb/modules/agent/mock_agent.py +5 -1
- tinybird/tb/modules/agent/models.py +6 -2
- tinybird/tb/modules/agent/prompts.py +49 -2
- tinybird/tb/modules/agent/tools/deploy.py +1 -1
- tinybird/tb/modules/agent/tools/execute_query.py +15 -18
- tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
- tinybird/tb/modules/agent/tools/run_command.py +9 -0
- tinybird/tb/modules/agent/utils.py +38 -48
- tinybird/tb/modules/branch.py +150 -0
- tinybird/tb/modules/build.py +58 -13
- tinybird/tb/modules/build_common.py +209 -25
- tinybird/tb/modules/cli.py +129 -16
- tinybird/tb/modules/common.py +172 -146
- tinybird/tb/modules/connection.py +125 -194
- tinybird/tb/modules/connection_kafka.py +382 -0
- tinybird/tb/modules/copy.py +3 -1
- tinybird/tb/modules/create.py +83 -150
- tinybird/tb/modules/datafile/build.py +27 -38
- tinybird/tb/modules/datafile/build_datasource.py +21 -25
- tinybird/tb/modules/datafile/diff.py +1 -1
- tinybird/tb/modules/datafile/format_pipe.py +46 -7
- tinybird/tb/modules/datafile/playground.py +59 -68
- tinybird/tb/modules/datafile/pull.py +2 -3
- tinybird/tb/modules/datasource.py +477 -308
- tinybird/tb/modules/deployment.py +2 -0
- tinybird/tb/modules/deployment_common.py +84 -44
- tinybird/tb/modules/deprecations.py +4 -4
- tinybird/tb/modules/dev_server.py +33 -12
- tinybird/tb/modules/exceptions.py +14 -0
- tinybird/tb/modules/feedback_manager.py +1 -1
- tinybird/tb/modules/info.py +69 -12
- tinybird/tb/modules/infra.py +4 -5
- tinybird/tb/modules/job_common.py +15 -0
- tinybird/tb/modules/local.py +143 -23
- tinybird/tb/modules/local_common.py +347 -19
- tinybird/tb/modules/local_logs.py +209 -0
- tinybird/tb/modules/login.py +21 -2
- tinybird/tb/modules/login_common.py +254 -12
- tinybird/tb/modules/mock.py +5 -54
- tinybird/tb/modules/mock_common.py +0 -54
- tinybird/tb/modules/open.py +10 -5
- tinybird/tb/modules/project.py +14 -5
- tinybird/tb/modules/shell.py +15 -7
- tinybird/tb/modules/sink.py +3 -1
- tinybird/tb/modules/telemetry.py +11 -3
- tinybird/tb/modules/test.py +13 -9
- tinybird/tb/modules/test_common.py +13 -87
- tinybird/tb/modules/tinyunit/tinyunit.py +0 -14
- tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -6
- tinybird/tb/modules/watch.py +5 -3
- tinybird/tb_cli_modules/common.py +2 -2
- tinybird/tb_cli_modules/telemetry.py +1 -1
- tinybird/tornado_template.py +6 -7
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/METADATA +32 -6
- tinybird-1.0.5.dist-info/RECORD +132 -0
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
- tinybird-0.0.1.dev291.dist-info/RECORD +0 -128
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# This is a command file for our CLI. Please keep it clean.
|
|
2
|
+
#
|
|
3
|
+
# - If it makes sense and only when strictly necessary, you can create utility functions in this file.
|
|
4
|
+
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from click import Context
|
|
12
|
+
from confluent_kafka.admin import AdminClient
|
|
13
|
+
|
|
14
|
+
from tinybird.tb.client import TinyB
|
|
15
|
+
from tinybird.tb.modules.common import (
|
|
16
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
17
|
+
get_kafka_connection_name,
|
|
18
|
+
validate_kafka_bootstrap_servers,
|
|
19
|
+
)
|
|
20
|
+
from tinybird.tb.modules.create import generate_kafka_connection_with_secrets
|
|
21
|
+
from tinybird.tb.modules.exceptions import CLIConnectionException, CLIException
|
|
22
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
23
|
+
from tinybird.tb.modules.project import Project
|
|
24
|
+
from tinybird.tb.modules.secret import save_secret_to_env_file
|
|
25
|
+
from tinybird.tb.modules.telemetry import add_telemetry_event
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def connection_create_kafka(
|
|
29
|
+
ctx: Context,
|
|
30
|
+
connection_name: Optional[str] = None,
|
|
31
|
+
bootstrap_servers: Optional[str] = None,
|
|
32
|
+
key: Optional[str] = None,
|
|
33
|
+
secret: Optional[str] = None,
|
|
34
|
+
auto_offset_reset: Optional[str] = None,
|
|
35
|
+
schema_registry_url: Optional[str] = None,
|
|
36
|
+
sasl_mechanism: Optional[str] = None,
|
|
37
|
+
security_protocol: Optional[str] = None,
|
|
38
|
+
ssl_ca_pem: Optional[str] = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
obj: Dict[str, Any] = ctx.ensure_object(dict)
|
|
41
|
+
click.echo(FeedbackManager.gray(message="\n» Creating Kafka connection..."))
|
|
42
|
+
project: Project = ctx.ensure_object(dict)["project"]
|
|
43
|
+
name = get_kafka_connection_name(project.folder, connection_name)
|
|
44
|
+
error: Optional[str] = None
|
|
45
|
+
ssl_ca_pem_required = False
|
|
46
|
+
|
|
47
|
+
if not bootstrap_servers:
|
|
48
|
+
default_bootstrap_servers = "localhost:9092"
|
|
49
|
+
bootstrap_servers = click.prompt(
|
|
50
|
+
FeedbackManager.highlight(
|
|
51
|
+
message=f"? Bootstrap servers (comma-separated list of host:port pairs) [{default_bootstrap_servers}]"
|
|
52
|
+
),
|
|
53
|
+
default=default_bootstrap_servers,
|
|
54
|
+
show_default=False,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert isinstance(bootstrap_servers, str)
|
|
58
|
+
|
|
59
|
+
servers_with_ssl_ca_pem_required = "aivencloud.com"
|
|
60
|
+
|
|
61
|
+
ssl_ca_pem_required = any(server in bootstrap_servers for server in servers_with_ssl_ca_pem_required)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
validate_kafka_bootstrap_servers(bootstrap_servers)
|
|
65
|
+
click.echo(FeedbackManager.success(message="✓ Server is valid"))
|
|
66
|
+
except CLIException as e:
|
|
67
|
+
error = str(e)
|
|
68
|
+
click.echo(FeedbackManager.error(message=error))
|
|
69
|
+
click.echo(FeedbackManager.warning(message="Process will continue, but the connection might not be valid."))
|
|
70
|
+
add_telemetry_event("connection_error", error=error)
|
|
71
|
+
|
|
72
|
+
secret_required = click.confirm(
|
|
73
|
+
FeedbackManager.info(message=" ? Do you want to store the bootstrap server in a .env.local file? [Y/n]"),
|
|
74
|
+
default=True,
|
|
75
|
+
show_default=False,
|
|
76
|
+
)
|
|
77
|
+
tb_secret_bootstrap_servers: Optional[str] = None
|
|
78
|
+
tb_secret_key: Optional[str] = None
|
|
79
|
+
tb_secret_secret: Optional[str] = None
|
|
80
|
+
tb_secret_ssl_ca_pem: Optional[str] = None
|
|
81
|
+
|
|
82
|
+
if secret_required:
|
|
83
|
+
tb_secret_bootstrap_servers = str(click.prompt(FeedbackManager.info(message=" ? Secret name")))
|
|
84
|
+
try:
|
|
85
|
+
save_secret_to_env_file(project=project, name=tb_secret_bootstrap_servers, value=bootstrap_servers)
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise CLIConnectionException(FeedbackManager.error(message=str(e)))
|
|
88
|
+
|
|
89
|
+
key = click.prompt(FeedbackManager.highlight(message="? Kafka key"))
|
|
90
|
+
|
|
91
|
+
assert isinstance(key, str)
|
|
92
|
+
|
|
93
|
+
secret_required = click.confirm(
|
|
94
|
+
FeedbackManager.info(message=" ? Do you want to store the Kafka key in a .env.local file? [Y/n]"),
|
|
95
|
+
default=True,
|
|
96
|
+
show_default=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if secret_required:
|
|
100
|
+
tb_secret_key = str(click.prompt(FeedbackManager.info(message=" ? Secret name")))
|
|
101
|
+
try:
|
|
102
|
+
save_secret_to_env_file(project=project, name=tb_secret_key, value=key)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
raise CLIConnectionException(FeedbackManager.error(message=str(e)))
|
|
105
|
+
|
|
106
|
+
secret = secret or click.prompt(FeedbackManager.highlight(message="? Kafka secret"), hide_input=True)
|
|
107
|
+
|
|
108
|
+
assert isinstance(secret, str)
|
|
109
|
+
|
|
110
|
+
secret_required = click.confirm(
|
|
111
|
+
FeedbackManager.info(message=" ? Do you want to store the Kafka secret in a .env.local file? [Y/n]"),
|
|
112
|
+
default=True,
|
|
113
|
+
show_default=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if secret_required:
|
|
117
|
+
tb_secret_secret = str(click.prompt(FeedbackManager.info(message=" ? Secret name")))
|
|
118
|
+
try:
|
|
119
|
+
save_secret_to_env_file(project=project, name=tb_secret_secret, value=secret)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise CLIConnectionException(FeedbackManager.error(message=str(e)))
|
|
122
|
+
|
|
123
|
+
security_protocol_options = ["SASL_SSL", "PLAINTEXT"]
|
|
124
|
+
security_protocol = security_protocol or click.prompt(
|
|
125
|
+
FeedbackManager.highlight(message="? Security Protocol (SASL_SSL, PLAINTEXT) [SASL_SSL]"),
|
|
126
|
+
type=click.Choice(security_protocol_options),
|
|
127
|
+
show_default=False,
|
|
128
|
+
show_choices=False,
|
|
129
|
+
default="SASL_SSL",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if security_protocol not in security_protocol_options:
|
|
133
|
+
raise CLIConnectionException(FeedbackManager.error(message=f"Invalid security protocol: {security_protocol}"))
|
|
134
|
+
|
|
135
|
+
sasl_mechanism_options = ["PLAIN", "SCRAM-SHA-256", "SCRAM-SHA-512"]
|
|
136
|
+
sasl_mechanism = sasl_mechanism or click.prompt(
|
|
137
|
+
FeedbackManager.highlight(message="? SASL Mechanism (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512) [PLAIN]"),
|
|
138
|
+
type=click.Choice(sasl_mechanism_options),
|
|
139
|
+
show_default=False,
|
|
140
|
+
show_choices=False,
|
|
141
|
+
default="PLAIN",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if sasl_mechanism not in sasl_mechanism_options:
|
|
145
|
+
raise CLIConnectionException(FeedbackManager.error(message=f"Invalid SASL mechanism: {sasl_mechanism}"))
|
|
146
|
+
|
|
147
|
+
if not schema_registry_url:
|
|
148
|
+
schema_registry_url = click.prompt(
|
|
149
|
+
FeedbackManager.highlight(message="? Schema Registry URL (optional)"),
|
|
150
|
+
default="",
|
|
151
|
+
show_default=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if ssl_ca_pem_required and not ssl_ca_pem:
|
|
155
|
+
yes = click.confirm(
|
|
156
|
+
FeedbackManager.highlight(
|
|
157
|
+
message="? CA certificate in PEM format (optional)", default=True, show_default=False
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
if yes:
|
|
161
|
+
ssl_ca_pem = click.edit(
|
|
162
|
+
"IMPORTANT: THIS LINE MUST BE DELETED. Enter your CA certificate value.", extension=".txt"
|
|
163
|
+
)
|
|
164
|
+
secret_required = click.confirm(
|
|
165
|
+
FeedbackManager.info(message=" ? Do you want to store the Kafka key in a .env.local file? [Y/n]"),
|
|
166
|
+
default=True,
|
|
167
|
+
show_default=False,
|
|
168
|
+
)
|
|
169
|
+
if secret_required and ssl_ca_pem:
|
|
170
|
+
tb_secret_ssl_ca_pem = str(click.prompt(FeedbackManager.info(message=" ? Secret name")))
|
|
171
|
+
try:
|
|
172
|
+
save_secret_to_env_file(project=project, name=tb_secret_ssl_ca_pem, value=ssl_ca_pem)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
raise CLIConnectionException(FeedbackManager.error(message=str(e)))
|
|
175
|
+
|
|
176
|
+
create_in_cloud = (
|
|
177
|
+
click.confirm(
|
|
178
|
+
FeedbackManager.highlight(
|
|
179
|
+
message="? Would you like to create this connection in the cloud environment as well? [Y/n]"
|
|
180
|
+
),
|
|
181
|
+
default=True,
|
|
182
|
+
show_default=False,
|
|
183
|
+
)
|
|
184
|
+
if obj["env"] == "local"
|
|
185
|
+
and (tb_secret_bootstrap_servers or tb_secret_key or tb_secret_secret or tb_secret_ssl_ca_pem)
|
|
186
|
+
else False
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if create_in_cloud:
|
|
190
|
+
click.echo(FeedbackManager.gray(message="» Creating Secrets in cloud environment..."))
|
|
191
|
+
prod_config = obj["config"]
|
|
192
|
+
host = prod_config["host"]
|
|
193
|
+
token = prod_config["token"]
|
|
194
|
+
prod_client = TinyB(
|
|
195
|
+
token=token,
|
|
196
|
+
host=host,
|
|
197
|
+
staging=False,
|
|
198
|
+
)
|
|
199
|
+
if tb_secret_bootstrap_servers:
|
|
200
|
+
prod_client.create_secret(name=tb_secret_bootstrap_servers, value=bootstrap_servers)
|
|
201
|
+
if tb_secret_key:
|
|
202
|
+
prod_client.create_secret(name=tb_secret_key, value=key)
|
|
203
|
+
if tb_secret_secret:
|
|
204
|
+
prod_client.create_secret(name=tb_secret_secret, value=secret)
|
|
205
|
+
if tb_secret_ssl_ca_pem and ssl_ca_pem:
|
|
206
|
+
prod_client.create_secret(name=tb_secret_ssl_ca_pem, value=ssl_ca_pem)
|
|
207
|
+
click.echo(FeedbackManager.success(message="✓ Secrets created!"))
|
|
208
|
+
|
|
209
|
+
click.echo(FeedbackManager.gray(message="» Validating connection..."))
|
|
210
|
+
|
|
211
|
+
topics: list[str] = []
|
|
212
|
+
try:
|
|
213
|
+
topics = list_kafka_topics(bootstrap_servers, key, secret, security_protocol, sasl_mechanism, ssl_ca_pem)
|
|
214
|
+
click.echo(FeedbackManager.success(message="✓ Connection is valid"))
|
|
215
|
+
except Exception as e:
|
|
216
|
+
error = str(e)
|
|
217
|
+
click.echo(FeedbackManager.error(message=f"Connection is not valid: {e}"))
|
|
218
|
+
add_telemetry_event("connection_error", error=error)
|
|
219
|
+
|
|
220
|
+
generate_kafka_connection_with_secrets(
|
|
221
|
+
name=name,
|
|
222
|
+
bootstrap_servers=bootstrap_servers,
|
|
223
|
+
tb_secret_bootstrap_servers=tb_secret_bootstrap_servers,
|
|
224
|
+
key=key,
|
|
225
|
+
tb_secret_key=tb_secret_key,
|
|
226
|
+
secret=secret,
|
|
227
|
+
tb_secret_secret=tb_secret_secret,
|
|
228
|
+
security_protocol=security_protocol,
|
|
229
|
+
sasl_mechanism=sasl_mechanism,
|
|
230
|
+
ssl_ca_pem=ssl_ca_pem,
|
|
231
|
+
tb_secret_ssl_ca_pem=tb_secret_ssl_ca_pem,
|
|
232
|
+
schema_registry_url=schema_registry_url,
|
|
233
|
+
folder=project.folder,
|
|
234
|
+
)
|
|
235
|
+
click.echo(FeedbackManager.info_file_created(file=f"connections/{name}.connection"))
|
|
236
|
+
if error:
|
|
237
|
+
click.echo(
|
|
238
|
+
FeedbackManager.warning(
|
|
239
|
+
message="Connection created, but some credentials are missing or invalid. Check https://www.tinybird.co/docs/forward/get-data-in/connectors/kafka#kafka-connection-settings for more details."
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
click.echo(FeedbackManager.success(message="✓ Connection created!"))
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
"name": name,
|
|
247
|
+
"bootstrap_servers": bootstrap_servers,
|
|
248
|
+
"key": key,
|
|
249
|
+
"secret": secret,
|
|
250
|
+
"sasl_mechanism": sasl_mechanism,
|
|
251
|
+
"security_protocol": security_protocol,
|
|
252
|
+
"topics": topics,
|
|
253
|
+
"error": error,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def list_kafka_topics(
|
|
258
|
+
bootstrap_servers, sasl_username, sasl_password, security_protocol, sasl_mechanism, ssl_ca_pem
|
|
259
|
+
) -> list[str]:
|
|
260
|
+
conf = {
|
|
261
|
+
"bootstrap.servers": bootstrap_servers,
|
|
262
|
+
"security.protocol": security_protocol,
|
|
263
|
+
"sasl.mechanism": sasl_mechanism,
|
|
264
|
+
"sasl.username": sasl_username,
|
|
265
|
+
"sasl.password": sasl_password,
|
|
266
|
+
"log_level": 0,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if ssl_ca_pem:
|
|
270
|
+
conf["ssl.ca.pem"] = re.sub(r"\\n", r"\n", ssl_ca_pem)
|
|
271
|
+
|
|
272
|
+
client = AdminClient(conf)
|
|
273
|
+
metadata = client.list_topics(timeout=5)
|
|
274
|
+
return list(metadata.topics.keys())
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def generate_kafka_group_id(topic: str):
|
|
278
|
+
return f"{topic}_{int(datetime.timestamp(datetime.now()))}"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def select_topic(kafka_topic: Optional[str], connection_id: str, client: TinyB) -> str:
|
|
282
|
+
if kafka_topic:
|
|
283
|
+
topics = client.kafka_list_topics(connection_id)
|
|
284
|
+
if kafka_topic not in topics:
|
|
285
|
+
raise CLIConnectionException(
|
|
286
|
+
FeedbackManager.error(message=f"Topic '{kafka_topic}' not found. Topics available: {', '.join(topics)}")
|
|
287
|
+
)
|
|
288
|
+
topic = kafka_topic
|
|
289
|
+
else:
|
|
290
|
+
topics = client.kafka_list_topics(connection_id)
|
|
291
|
+
click.echo(FeedbackManager.highlight(message="? Select a Kafka topic:"))
|
|
292
|
+
topic_index = -1
|
|
293
|
+
while topic_index == -1:
|
|
294
|
+
for index, topic in enumerate(topics):
|
|
295
|
+
click.echo(f" [{index + 1}] {topic}")
|
|
296
|
+
topic_index = click.prompt("\nSelect topic", default=1)
|
|
297
|
+
try:
|
|
298
|
+
topic = topics[int(topic_index) - 1]
|
|
299
|
+
except Exception:
|
|
300
|
+
topic_index = -1
|
|
301
|
+
|
|
302
|
+
if not topic:
|
|
303
|
+
raise CLIConnectionException(FeedbackManager.error(message="Topic is required."))
|
|
304
|
+
|
|
305
|
+
return topic
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def select_group_id(kafka_group_id: Optional[str], topic: str, connection_id: str, client: TinyB) -> str:
|
|
309
|
+
group_id = kafka_group_id
|
|
310
|
+
is_valid = False
|
|
311
|
+
while not is_valid:
|
|
312
|
+
if not group_id:
|
|
313
|
+
default_group_id = generate_kafka_group_id(topic)
|
|
314
|
+
else:
|
|
315
|
+
default_group_id = group_id
|
|
316
|
+
|
|
317
|
+
group_id = click.prompt(
|
|
318
|
+
FeedbackManager.highlight(message="? Enter a Kafka group ID"),
|
|
319
|
+
default=default_group_id,
|
|
320
|
+
show_default=True,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert isinstance(group_id, str)
|
|
324
|
+
|
|
325
|
+
click.echo(FeedbackManager.gray(message=f"» Validating group ID '{group_id}'..."))
|
|
326
|
+
try:
|
|
327
|
+
client.kafka_preview_group(connection_id, topic, group_id)
|
|
328
|
+
is_valid = True
|
|
329
|
+
click.echo(FeedbackManager.success(message=f"✓ Group ID '{group_id}' is valid."))
|
|
330
|
+
except Exception as e:
|
|
331
|
+
click.echo(FeedbackManager.error(message=str(e)))
|
|
332
|
+
group_id = None # Reset to prompt again
|
|
333
|
+
|
|
334
|
+
if not group_id:
|
|
335
|
+
raise CLIConnectionException(FeedbackManager.error(message="Group ID is required."))
|
|
336
|
+
|
|
337
|
+
return group_id
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def echo_kafka_data(connection_id: str, connection_name: str, topic: str, group_id: str, client: TinyB) -> None:
|
|
341
|
+
click.echo(
|
|
342
|
+
FeedbackManager.highlight(
|
|
343
|
+
message=f"» Previewing data for connection '{connection_name}' with topic '{topic}' and group ID '{group_id}'..."
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
response = client.kafka_preview_topic(connection_id, topic, group_id)
|
|
347
|
+
preview = response.get("preview", {})
|
|
348
|
+
data = preview.get("data", [])
|
|
349
|
+
column_names = [col["name"] for col in preview.get("meta", [])]
|
|
350
|
+
|
|
351
|
+
# Convert each row dictionary to a list of values ordered by column names
|
|
352
|
+
data_as_lists = []
|
|
353
|
+
for row in data:
|
|
354
|
+
if isinstance(row, dict):
|
|
355
|
+
# Convert dict to list of values in column order
|
|
356
|
+
row_values = [row.get(col_name, "") for col_name in column_names]
|
|
357
|
+
data_as_lists.append(row_values)
|
|
358
|
+
else:
|
|
359
|
+
# If it's already a list, keep it as is
|
|
360
|
+
data_as_lists.append(row)
|
|
361
|
+
|
|
362
|
+
if not data_as_lists and not column_names:
|
|
363
|
+
click.echo(FeedbackManager.warning(message="No data to preview."))
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
echo_safe_humanfriendly_tables_format_smart_table(data_as_lists, column_names)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def select_connection(
|
|
370
|
+
connection_id: Optional[str], connection_type: str, connections: list[dict[str, Any]], client: TinyB
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
click.echo(FeedbackManager.highlight(message=f"? Select a {connection_type.capitalize()} connection:"))
|
|
373
|
+
connection_index = -1
|
|
374
|
+
while connection_index == -1:
|
|
375
|
+
for index, conn in enumerate(connections):
|
|
376
|
+
click.echo(f" [{index + 1}] {conn['name']}")
|
|
377
|
+
connection_index = click.prompt("\nSelect connection", default=1)
|
|
378
|
+
try:
|
|
379
|
+
connection = connections[int(connection_index) - 1]
|
|
380
|
+
except Exception:
|
|
381
|
+
connection_index = -1
|
|
382
|
+
return connection
|
tinybird/tb/modules/copy.py
CHANGED
|
@@ -16,6 +16,7 @@ from tinybird.tb.modules.cli import cli
|
|
|
16
16
|
from tinybird.tb.modules.common import echo_safe_humanfriendly_tables_format_smart_table, wait_job
|
|
17
17
|
from tinybird.tb.modules.exceptions import CLIPipeException
|
|
18
18
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
19
|
+
from tinybird.tb.modules.job_common import echo_job_url
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
@cli.group()
|
|
@@ -89,15 +90,16 @@ def copy_run(ctx: click.Context, pipe_name_or_id: str, wait: bool, mode: str, pa
|
|
|
89
90
|
params = dict(key_value.split("=") for key_value in param) if param else {}
|
|
90
91
|
click.echo(FeedbackManager.highlight(message=f"\n» Running on-demand copy '{pipe_name_or_id}'"))
|
|
91
92
|
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
93
|
+
config = ctx.ensure_object(dict)["config"]
|
|
92
94
|
|
|
93
95
|
try:
|
|
94
96
|
response = client.pipe_run(pipe_name_or_id, "copy", params, mode)
|
|
95
97
|
job_id = response["job"]["job_id"]
|
|
96
98
|
job_url = response["job"]["job_url"]
|
|
99
|
+
echo_job_url(client.token, client.host, config.get("name") or "", job_url)
|
|
97
100
|
target_datasource_id = response["tags"]["copy_target_datasource"]
|
|
98
101
|
target_datasource = client.get_datasource(target_datasource_id)
|
|
99
102
|
target_datasource_name = target_datasource["name"]
|
|
100
|
-
click.echo(FeedbackManager.gray(message="Job URL: ") + FeedbackManager.info(message=f"{job_url}"))
|
|
101
103
|
click.echo(FeedbackManager.success(message=f"✓ Copy to '{target_datasource_name}' job created"))
|
|
102
104
|
|
|
103
105
|
if wait:
|