tinybird 0.0.1.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.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/__cli__.py +8 -0
- tinybird/ch_utils/constants.py +244 -0
- tinybird/ch_utils/engine.py +855 -0
- tinybird/check_pypi.py +25 -0
- tinybird/client.py +1281 -0
- tinybird/config.py +117 -0
- tinybird/connectors.py +428 -0
- tinybird/context.py +23 -0
- tinybird/datafile.py +5589 -0
- tinybird/datatypes.py +434 -0
- tinybird/feedback_manager.py +1022 -0
- tinybird/git_settings.py +145 -0
- tinybird/sql.py +865 -0
- tinybird/sql_template.py +2343 -0
- tinybird/sql_template_fmt.py +281 -0
- tinybird/sql_toolset.py +350 -0
- tinybird/syncasync.py +682 -0
- tinybird/tb_cli.py +25 -0
- tinybird/tb_cli_modules/auth.py +252 -0
- tinybird/tb_cli_modules/branch.py +1043 -0
- tinybird/tb_cli_modules/cicd.py +434 -0
- tinybird/tb_cli_modules/cli.py +1571 -0
- tinybird/tb_cli_modules/common.py +2082 -0
- tinybird/tb_cli_modules/config.py +344 -0
- tinybird/tb_cli_modules/connection.py +803 -0
- tinybird/tb_cli_modules/datasource.py +900 -0
- tinybird/tb_cli_modules/exceptions.py +91 -0
- tinybird/tb_cli_modules/fmt.py +91 -0
- tinybird/tb_cli_modules/job.py +85 -0
- tinybird/tb_cli_modules/pipe.py +858 -0
- tinybird/tb_cli_modules/regions.py +9 -0
- tinybird/tb_cli_modules/tag.py +100 -0
- tinybird/tb_cli_modules/telemetry.py +310 -0
- tinybird/tb_cli_modules/test.py +107 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit.py +340 -0
- tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +71 -0
- tinybird/tb_cli_modules/token.py +349 -0
- tinybird/tb_cli_modules/workspace.py +269 -0
- tinybird/tb_cli_modules/workspace_members.py +212 -0
- tinybird/tornado_template.py +1194 -0
- tinybird-0.0.1.dev0.dist-info/METADATA +2815 -0
- tinybird-0.0.1.dev0.dist-info/RECORD +45 -0
- tinybird-0.0.1.dev0.dist-info/WHEEL +5 -0
- tinybird-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- tinybird-0.0.1.dev0.dist-info/top_level.txt +4 -0
|
@@ -0,0 +1,900 @@
|
|
|
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 asyncio
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import humanfriendly
|
|
14
|
+
from click import Context
|
|
15
|
+
|
|
16
|
+
from tinybird.client import AuthNoTokenException, CanNotBeDeletedException, DoesNotExistException, TinyB
|
|
17
|
+
from tinybird.config import get_display_host
|
|
18
|
+
from tinybird.tb_cli_modules.config import CLIConfig
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from tinybird.connectors import Connector
|
|
22
|
+
|
|
23
|
+
from tinybird.datafile import get_name_version, wait_job
|
|
24
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
25
|
+
from tinybird.tb_cli_modules.branch import warn_if_in_live
|
|
26
|
+
from tinybird.tb_cli_modules.cli import cli
|
|
27
|
+
from tinybird.tb_cli_modules.common import (
|
|
28
|
+
_analyze,
|
|
29
|
+
_generate_datafile,
|
|
30
|
+
ask_for_user_token,
|
|
31
|
+
autocomplete_topics,
|
|
32
|
+
check_user_token,
|
|
33
|
+
coro,
|
|
34
|
+
echo_safe_humanfriendly_tables_format_smart_table,
|
|
35
|
+
get_format_from_filename_or_url,
|
|
36
|
+
load_connector_config,
|
|
37
|
+
push_data,
|
|
38
|
+
sync_data,
|
|
39
|
+
validate_datasource_name,
|
|
40
|
+
validate_kafka_auto_offset_reset,
|
|
41
|
+
validate_kafka_group,
|
|
42
|
+
validate_kafka_topic,
|
|
43
|
+
)
|
|
44
|
+
from tinybird.tb_cli_modules.exceptions import CLIDatasourceException
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@cli.group()
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def datasource(ctx):
|
|
50
|
+
"""Data Sources commands"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@datasource.command(name="ls")
|
|
54
|
+
@click.option("--match", default=None, help="Retrieve any resources matching the pattern. eg --match _test")
|
|
55
|
+
@click.option(
|
|
56
|
+
"--format",
|
|
57
|
+
"format_",
|
|
58
|
+
type=click.Choice(["json"], case_sensitive=False),
|
|
59
|
+
default=None,
|
|
60
|
+
help="Force a type of the output",
|
|
61
|
+
)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
@coro
|
|
64
|
+
async def datasource_ls(ctx: Context, match: Optional[str], format_: str):
|
|
65
|
+
"""List data sources"""
|
|
66
|
+
|
|
67
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
68
|
+
ds = await client.datasources()
|
|
69
|
+
columns = ["version", "shared from", "name", "row_count", "size", "created at", "updated at", "connection"]
|
|
70
|
+
table_human_readable = []
|
|
71
|
+
table_machine_readable = []
|
|
72
|
+
pattern = re.compile(match) if match else None
|
|
73
|
+
|
|
74
|
+
for t in ds:
|
|
75
|
+
stats = t.get("stats", None)
|
|
76
|
+
if not stats:
|
|
77
|
+
stats = t.get("statistics", {"bytes": ""})
|
|
78
|
+
if not stats:
|
|
79
|
+
stats = {"bytes": ""}
|
|
80
|
+
|
|
81
|
+
tk = get_name_version(t["name"])
|
|
82
|
+
if pattern and not pattern.search(tk["name"]):
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
if "." in tk["name"]:
|
|
86
|
+
shared_from, name = tk["name"].split(".")
|
|
87
|
+
else:
|
|
88
|
+
shared_from, name = "", tk["name"]
|
|
89
|
+
|
|
90
|
+
table_human_readable.append(
|
|
91
|
+
(
|
|
92
|
+
tk["version"] if tk["version"] is not None else "",
|
|
93
|
+
shared_from,
|
|
94
|
+
name,
|
|
95
|
+
humanfriendly.format_number(stats.get("row_count")) if stats.get("row_count", None) else "-",
|
|
96
|
+
humanfriendly.format_size(int(stats.get("bytes"))) if stats.get("bytes", None) else "-",
|
|
97
|
+
t["created_at"][:-7],
|
|
98
|
+
t["updated_at"][:-7],
|
|
99
|
+
t.get("service", ""),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
table_machine_readable.append(
|
|
103
|
+
{
|
|
104
|
+
"version": tk["version"] if tk["version"] is not None else "",
|
|
105
|
+
"shared from": shared_from,
|
|
106
|
+
"name": name,
|
|
107
|
+
"row_count": stats.get("row_count", None) or "-",
|
|
108
|
+
"size": stats.get("bytes", None) or "-",
|
|
109
|
+
"created at": t["created_at"][:-7],
|
|
110
|
+
"updated at": t["updated_at"][:-7],
|
|
111
|
+
"connection": t.get("service", ""),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not format_:
|
|
116
|
+
click.echo(FeedbackManager.info_datasources())
|
|
117
|
+
echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
|
|
118
|
+
click.echo("\n")
|
|
119
|
+
elif format_ == "json":
|
|
120
|
+
click.echo(json.dumps({"datasources": table_machine_readable}, indent=2))
|
|
121
|
+
else:
|
|
122
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_ls_type())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@datasource.command(name="append")
|
|
126
|
+
@click.argument("datasource_name")
|
|
127
|
+
@click.argument("url", nargs=-1)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--connector",
|
|
130
|
+
type=click.Choice(["bigquery", "snowflake"], case_sensitive=True),
|
|
131
|
+
help="Import from one of the selected connectors",
|
|
132
|
+
hidden=True,
|
|
133
|
+
)
|
|
134
|
+
@click.option("--sql", default=None, help="Query to extract data from one of the SQL connectors", hidden=True)
|
|
135
|
+
@click.option(
|
|
136
|
+
"--incremental",
|
|
137
|
+
default=None,
|
|
138
|
+
help="It does an incremental append, taking the max value for the date column name provided as a parameter. It only works when the `connector` parameter is passed.",
|
|
139
|
+
hidden=True,
|
|
140
|
+
)
|
|
141
|
+
@click.option(
|
|
142
|
+
"--ignore-empty",
|
|
143
|
+
help="Wheter or not to ignore empty results from the connector",
|
|
144
|
+
is_flag=True,
|
|
145
|
+
default=False,
|
|
146
|
+
hidden=True,
|
|
147
|
+
)
|
|
148
|
+
@click.option("--concurrency", help="How many files to submit concurrently", default=1, hidden=True)
|
|
149
|
+
@click.pass_context
|
|
150
|
+
@coro
|
|
151
|
+
async def datasource_append(
|
|
152
|
+
ctx: Context,
|
|
153
|
+
datasource_name: str,
|
|
154
|
+
url,
|
|
155
|
+
connector: Optional[str],
|
|
156
|
+
sql: Optional[str],
|
|
157
|
+
incremental: Optional[str],
|
|
158
|
+
ignore_empty: bool,
|
|
159
|
+
concurrency: int,
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
Appends data to an existing Data Source from URL, local file or a connector
|
|
163
|
+
|
|
164
|
+
- Load from URL `tb datasource append [datasource_name] https://url_to_csv`
|
|
165
|
+
|
|
166
|
+
- Load from local file `tb datasource append [datasource_name] /path/to/local/file`
|
|
167
|
+
|
|
168
|
+
- Load from connector `tb datasource append [datasource_name] --connector [connector_name] --sql [the_sql_to_extract_from]`
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
if not url and not connector:
|
|
172
|
+
raise CLIDatasourceException(FeedbackManager.error_missing_url_or_connector(datasource=datasource_name))
|
|
173
|
+
|
|
174
|
+
if incremental and not connector:
|
|
175
|
+
raise CLIDatasourceException(FeedbackManager.error_incremental_not_supported())
|
|
176
|
+
|
|
177
|
+
if incremental:
|
|
178
|
+
date = None
|
|
179
|
+
source_column = incremental.split(":")[0]
|
|
180
|
+
dest_column = incremental.split(":")[-1]
|
|
181
|
+
client: TinyB = ctx.obj["client"]
|
|
182
|
+
result = await client.query(f"SELECT max({dest_column}) as inc from {datasource_name} FORMAT JSON")
|
|
183
|
+
try:
|
|
184
|
+
date = result["data"][0]["inc"]
|
|
185
|
+
except Exception as e:
|
|
186
|
+
raise CLIDatasourceException(f"{str(e)}")
|
|
187
|
+
if date:
|
|
188
|
+
sql = f"{sql} WHERE {source_column} > '{date}'"
|
|
189
|
+
await push_data(
|
|
190
|
+
ctx, datasource_name, url, connector, sql, mode="append", ignore_empty=ignore_empty, concurrency=concurrency
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@datasource.command(name="replace")
|
|
195
|
+
@click.argument("datasource_name")
|
|
196
|
+
@click.argument("url", nargs=-1)
|
|
197
|
+
@click.option(
|
|
198
|
+
"--connector",
|
|
199
|
+
type=click.Choice(["bigquery", "snowflake"], case_sensitive=True),
|
|
200
|
+
help="Import from one of the selected connectors",
|
|
201
|
+
hidden=True,
|
|
202
|
+
)
|
|
203
|
+
@click.option("--sql", default=None, help="Query to extract data from one of the SQL connectors", hidden=True)
|
|
204
|
+
@click.option("--sql-condition", default=None, help="SQL WHERE condition to replace data", hidden=True)
|
|
205
|
+
@click.option("--skip-incompatible-partition-key", is_flag=True, default=False, hidden=True)
|
|
206
|
+
@click.option(
|
|
207
|
+
"--ignore-empty",
|
|
208
|
+
help="Wheter or not to ignore empty results from the connector",
|
|
209
|
+
is_flag=True,
|
|
210
|
+
default=False,
|
|
211
|
+
hidden=True,
|
|
212
|
+
)
|
|
213
|
+
@click.pass_context
|
|
214
|
+
@coro
|
|
215
|
+
async def datasource_replace(
|
|
216
|
+
ctx,
|
|
217
|
+
datasource_name,
|
|
218
|
+
url,
|
|
219
|
+
connector,
|
|
220
|
+
sql,
|
|
221
|
+
sql_condition,
|
|
222
|
+
skip_incompatible_partition_key,
|
|
223
|
+
ignore_empty: bool,
|
|
224
|
+
):
|
|
225
|
+
"""
|
|
226
|
+
Replaces the data in a data source from a URL, local file or a connector
|
|
227
|
+
|
|
228
|
+
- Replace from URL `tb datasource replace [datasource_name] https://url_to_csv --sql-condition "country='ES'"`
|
|
229
|
+
|
|
230
|
+
- Replace from local file `tb datasource replace [datasource_name] /path/to/local/file --sql-condition "country='ES'"`
|
|
231
|
+
|
|
232
|
+
- Replace from connector `tb datasource replace [datasource_name] --connector [connector_name] --sql [the_sql_to_extract_from] --sql-condition "country='ES'"`
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
if not url and not connector:
|
|
236
|
+
raise CLIDatasourceException(FeedbackManager.error_missing_url_or_connector(datasource=datasource_name))
|
|
237
|
+
|
|
238
|
+
replace_options = set()
|
|
239
|
+
if skip_incompatible_partition_key:
|
|
240
|
+
replace_options.add("skip_incompatible_partition_key")
|
|
241
|
+
await push_data(
|
|
242
|
+
ctx,
|
|
243
|
+
datasource_name,
|
|
244
|
+
url,
|
|
245
|
+
connector,
|
|
246
|
+
sql,
|
|
247
|
+
mode="replace",
|
|
248
|
+
sql_condition=sql_condition,
|
|
249
|
+
replace_options=replace_options,
|
|
250
|
+
ignore_empty=ignore_empty,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@datasource.command(name="analyze")
|
|
255
|
+
@click.argument("url_or_file")
|
|
256
|
+
@click.option(
|
|
257
|
+
"--connector",
|
|
258
|
+
type=click.Choice(["bigquery", "snowflake"], case_sensitive=True),
|
|
259
|
+
help="Use from one of the selected connectors. In this case pass a table name as a parameter instead of a file name or an URL",
|
|
260
|
+
hidden=True,
|
|
261
|
+
)
|
|
262
|
+
@click.pass_context
|
|
263
|
+
@coro
|
|
264
|
+
async def datasource_analyze(ctx, url_or_file, connector):
|
|
265
|
+
"""Analyze a URL or a file before creating a new data source"""
|
|
266
|
+
client = ctx.obj["client"]
|
|
267
|
+
|
|
268
|
+
_connector = None
|
|
269
|
+
if connector:
|
|
270
|
+
load_connector_config(ctx, connector, False, check_uninstalled=False)
|
|
271
|
+
if connector not in ctx.obj:
|
|
272
|
+
raise CLIDatasourceException(FeedbackManager.error_connector_not_configured(connector=connector))
|
|
273
|
+
else:
|
|
274
|
+
_connector = ctx.obj[connector]
|
|
275
|
+
|
|
276
|
+
def _table(title, columns, data):
|
|
277
|
+
row_format = "{:<25}" * len(columns)
|
|
278
|
+
click.echo(FeedbackManager.info_datasource_title(title=title))
|
|
279
|
+
click.echo(FeedbackManager.info_datasource_row(row=row_format.format(*columns)))
|
|
280
|
+
for t in data:
|
|
281
|
+
click.echo(FeedbackManager.info_datasource_row(row=row_format.format(*[str(element) for element in t])))
|
|
282
|
+
|
|
283
|
+
analysis, _ = await _analyze(
|
|
284
|
+
url_or_file, client, format=get_format_from_filename_or_url(url_or_file), connector=_connector
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
columns = ("name", "type", "nullable")
|
|
288
|
+
if "columns" in analysis["analysis"]:
|
|
289
|
+
_table(
|
|
290
|
+
"columns",
|
|
291
|
+
columns,
|
|
292
|
+
[
|
|
293
|
+
(t["name"], t["recommended_type"], "false" if t["present_pct"] == 1 else "true")
|
|
294
|
+
for t in analysis["analysis"]["columns"]
|
|
295
|
+
],
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
click.echo(FeedbackManager.info_datasource_title(title="SQL Schema"))
|
|
299
|
+
click.echo(analysis["analysis"]["schema"])
|
|
300
|
+
|
|
301
|
+
values = []
|
|
302
|
+
|
|
303
|
+
if "dialect" in analysis:
|
|
304
|
+
for x in analysis["dialect"].items():
|
|
305
|
+
if x[1] == " ":
|
|
306
|
+
values.append((x[0], '" "'))
|
|
307
|
+
elif type(x[1]) == str and ("\n" in x[1] or "\r" in x[1]): # noqa: E721
|
|
308
|
+
values.append((x[0], x[1].replace("\n", "\\n").replace("\r", "\\r")))
|
|
309
|
+
else:
|
|
310
|
+
values.append(x)
|
|
311
|
+
|
|
312
|
+
_table("dialect", ("name", "value"), values)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@datasource.command(name="rm")
|
|
316
|
+
@click.argument("datasource_name")
|
|
317
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
318
|
+
@click.pass_context
|
|
319
|
+
@coro
|
|
320
|
+
async def datasource_delete(ctx: Context, datasource_name: str, yes: bool):
|
|
321
|
+
"""Delete a data source"""
|
|
322
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
323
|
+
try:
|
|
324
|
+
datasource = await client.get_datasource(datasource_name)
|
|
325
|
+
except AuthNoTokenException:
|
|
326
|
+
raise
|
|
327
|
+
except DoesNotExistException:
|
|
328
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
329
|
+
except Exception as e:
|
|
330
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
331
|
+
connector = datasource.get("service", False)
|
|
332
|
+
|
|
333
|
+
if connector:
|
|
334
|
+
click.echo(FeedbackManager.warning_datasource_is_connected(datasource=datasource_name, connector=connector))
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
response = await client.datasource_delete(datasource_name, dry_run=True)
|
|
338
|
+
dependencies_information = f'The Data Source is used in => Pipes="{response["dependent_pipes"]}", nodes="{response["dependent_pipes"]}"'
|
|
339
|
+
dependencies_information = (
|
|
340
|
+
dependencies_information if response["dependent_pipes"] else "The Data Source is not used in any Pipe"
|
|
341
|
+
)
|
|
342
|
+
warning_message = f"\nDo you want to delete {datasource_name}? Once deleted, it can't be recovered."
|
|
343
|
+
except CanNotBeDeletedException as e:
|
|
344
|
+
if "downstream" not in str(e):
|
|
345
|
+
dependencies_information = str(e)
|
|
346
|
+
warning_message = f"\nDo you want to unlink and delete {datasource_name}? This action can't be undone."
|
|
347
|
+
else:
|
|
348
|
+
raise CLIDatasourceException(
|
|
349
|
+
FeedbackManager.error_datasource_can_not_be_deleted(datasource=datasource_name, error=e)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
353
|
+
await warn_if_in_live(semver)
|
|
354
|
+
|
|
355
|
+
if yes or click.confirm(
|
|
356
|
+
FeedbackManager.warning_confirm_delete_datasource(
|
|
357
|
+
warning_message=warning_message, dependencies_information=dependencies_information
|
|
358
|
+
)
|
|
359
|
+
):
|
|
360
|
+
try:
|
|
361
|
+
await client.datasource_delete(datasource_name, force=True)
|
|
362
|
+
except DoesNotExistException:
|
|
363
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
364
|
+
except CanNotBeDeletedException as e:
|
|
365
|
+
raise CLIDatasourceException(
|
|
366
|
+
FeedbackManager.error_datasource_can_not_be_deleted(datasource=datasource_name, error=e)
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
370
|
+
|
|
371
|
+
click.echo(FeedbackManager.success_delete_datasource(datasource=datasource_name))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@datasource.command(name="truncate")
|
|
375
|
+
@click.argument("datasource_name", required=True)
|
|
376
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
377
|
+
@click.option(
|
|
378
|
+
"--cascade", is_flag=True, default=False, help="Truncate dependent DS attached in cascade to the given DS"
|
|
379
|
+
)
|
|
380
|
+
@click.pass_context
|
|
381
|
+
@coro
|
|
382
|
+
async def datasource_truncate(ctx, datasource_name, yes, cascade):
|
|
383
|
+
"""Truncate a data source"""
|
|
384
|
+
|
|
385
|
+
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
386
|
+
await warn_if_in_live(semver)
|
|
387
|
+
|
|
388
|
+
client = ctx.obj["client"]
|
|
389
|
+
if yes or click.confirm(FeedbackManager.warning_confirm_truncate_datasource(datasource=datasource_name)):
|
|
390
|
+
try:
|
|
391
|
+
await client.datasource_truncate(datasource_name)
|
|
392
|
+
except AuthNoTokenException:
|
|
393
|
+
raise
|
|
394
|
+
except DoesNotExistException:
|
|
395
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
396
|
+
except Exception as e:
|
|
397
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
398
|
+
|
|
399
|
+
click.echo(FeedbackManager.success_truncate_datasource(datasource=datasource_name))
|
|
400
|
+
|
|
401
|
+
if cascade:
|
|
402
|
+
try:
|
|
403
|
+
ds_cascade_dependencies = await client.datasource_dependencies(
|
|
404
|
+
no_deps=False,
|
|
405
|
+
match=None,
|
|
406
|
+
pipe=None,
|
|
407
|
+
datasource=datasource_name,
|
|
408
|
+
check_for_partial_replace=True,
|
|
409
|
+
recursive=False,
|
|
410
|
+
)
|
|
411
|
+
except Exception as e:
|
|
412
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
413
|
+
|
|
414
|
+
cascade_dependent_ds = list(ds_cascade_dependencies.get("dependencies", {}).keys()) + list(
|
|
415
|
+
ds_cascade_dependencies.get("incompatible_datasources", {}).keys()
|
|
416
|
+
)
|
|
417
|
+
for cascade_ds in cascade_dependent_ds:
|
|
418
|
+
if yes or click.confirm(FeedbackManager.warning_confirm_truncate_datasource(datasource=cascade_ds)):
|
|
419
|
+
try:
|
|
420
|
+
await client.datasource_truncate(cascade_ds)
|
|
421
|
+
except DoesNotExistException:
|
|
422
|
+
raise CLIDatasourceException(
|
|
423
|
+
FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name)
|
|
424
|
+
)
|
|
425
|
+
except Exception as e:
|
|
426
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
427
|
+
click.echo(FeedbackManager.success_truncate_datasource(datasource=cascade_ds))
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@datasource.command(name="delete")
|
|
431
|
+
@click.argument("datasource_name")
|
|
432
|
+
@click.option("--sql-condition", default=None, help="SQL WHERE condition to remove rows", hidden=True, required=True)
|
|
433
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
434
|
+
@click.option("--wait", is_flag=True, default=False, help="Wait for delete job to finish, disabled by default")
|
|
435
|
+
@click.option("--dry-run", is_flag=True, default=False, help="Run the command without deleting anything")
|
|
436
|
+
@click.pass_context
|
|
437
|
+
@coro
|
|
438
|
+
async def datasource_delete_rows(ctx, datasource_name, sql_condition, yes, wait, dry_run):
|
|
439
|
+
"""
|
|
440
|
+
Delete rows from a datasource
|
|
441
|
+
|
|
442
|
+
- Delete rows with SQL condition: `tb datasource delete [datasource_name] --sql-condition "country='ES'"`
|
|
443
|
+
|
|
444
|
+
- Delete rows with SQL condition and wait for the job to finish: `tb datasource delete [datasource_name] --sql-condition "country='ES'" --wait`
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
448
|
+
await warn_if_in_live(semver)
|
|
449
|
+
|
|
450
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
451
|
+
if (
|
|
452
|
+
dry_run
|
|
453
|
+
or yes
|
|
454
|
+
or click.confirm(
|
|
455
|
+
FeedbackManager.warning_confirm_delete_rows_datasource(
|
|
456
|
+
datasource=datasource_name, delete_condition=sql_condition
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
):
|
|
460
|
+
try:
|
|
461
|
+
res = await client.datasource_delete_rows(datasource_name, sql_condition, dry_run)
|
|
462
|
+
if dry_run:
|
|
463
|
+
click.echo(
|
|
464
|
+
FeedbackManager.success_dry_run_delete_rows_datasource(
|
|
465
|
+
rows=res["rows_to_be_deleted"], datasource=datasource_name, delete_condition=sql_condition
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
return
|
|
469
|
+
job_id = res["job_id"]
|
|
470
|
+
job_url = res["job_url"]
|
|
471
|
+
click.echo(FeedbackManager.info_datasource_delete_rows_job_url(url=job_url))
|
|
472
|
+
if wait:
|
|
473
|
+
progress_symbols = ["-", "\\", "|", "/"]
|
|
474
|
+
progress_str = "Waiting for the job to finish"
|
|
475
|
+
# TODO: Use click.echo instead of print and see if the behavior is the same
|
|
476
|
+
print(f"\n{progress_str}", end="") # noqa: T201
|
|
477
|
+
|
|
478
|
+
def progress_line(n):
|
|
479
|
+
print(f"\r{progress_str} {progress_symbols[n % len(progress_symbols)]}", end="") # noqa: T201
|
|
480
|
+
|
|
481
|
+
i = 0
|
|
482
|
+
while True:
|
|
483
|
+
try:
|
|
484
|
+
res = await client._req(f"v0/jobs/{job_id}")
|
|
485
|
+
except Exception:
|
|
486
|
+
raise CLIDatasourceException(FeedbackManager.error_job_status(url=job_url))
|
|
487
|
+
if res["status"] == "done":
|
|
488
|
+
print("\n") # noqa: T201
|
|
489
|
+
click.echo(
|
|
490
|
+
FeedbackManager.success_delete_rows_datasource(
|
|
491
|
+
datasource=datasource_name, delete_condition=sql_condition
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
break
|
|
495
|
+
elif res["status"] == "error":
|
|
496
|
+
print("\n") # noqa: T201
|
|
497
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=res["error"]))
|
|
498
|
+
await asyncio.sleep(1)
|
|
499
|
+
i += 1
|
|
500
|
+
progress_line(i)
|
|
501
|
+
|
|
502
|
+
except AuthNoTokenException:
|
|
503
|
+
raise
|
|
504
|
+
except DoesNotExistException:
|
|
505
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
506
|
+
except Exception as e:
|
|
507
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@datasource.command(
|
|
511
|
+
name="generate",
|
|
512
|
+
short_help="Generates a .datasource file based on a sample CSV, NDJSON or Parquet file from local disk or url",
|
|
513
|
+
)
|
|
514
|
+
@click.argument("filenames", nargs=-1, default=None)
|
|
515
|
+
@click.option("--force", is_flag=True, default=False, help="Override existing files")
|
|
516
|
+
@click.option(
|
|
517
|
+
"--connector",
|
|
518
|
+
type=click.Choice(["bigquery", "snowflake"], case_sensitive=True),
|
|
519
|
+
help="Use from one of the selected connectors. In this case pass a table name as a parameter instead of a file name",
|
|
520
|
+
hidden=True,
|
|
521
|
+
)
|
|
522
|
+
@click.pass_context
|
|
523
|
+
@coro
|
|
524
|
+
async def generate_datasource(ctx: Context, connector: str, filenames, force: bool):
|
|
525
|
+
"""Generate a data source file based on a sample CSV file from local disk or url"""
|
|
526
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
527
|
+
|
|
528
|
+
_connector: Optional["Connector"] = None
|
|
529
|
+
if connector:
|
|
530
|
+
load_connector_config(ctx, connector, False, check_uninstalled=False)
|
|
531
|
+
if connector not in ctx.ensure_object(dict):
|
|
532
|
+
raise CLIDatasourceException(FeedbackManager.error_connector_not_configured(connector=connector))
|
|
533
|
+
else:
|
|
534
|
+
_connector = ctx.ensure_object(dict)[connector]
|
|
535
|
+
|
|
536
|
+
for filename in filenames:
|
|
537
|
+
await _generate_datafile(
|
|
538
|
+
filename, client, force=force, format=get_format_from_filename_or_url(filename), connector=_connector
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@datasource.command(name="connect")
|
|
543
|
+
@click.argument("connection")
|
|
544
|
+
@click.argument("datasource_name")
|
|
545
|
+
@click.option("--kafka-topic", "topic", help="For Kafka connections: topic", shell_complete=autocomplete_topics)
|
|
546
|
+
@click.option("--topic", "topic", hidden=True)
|
|
547
|
+
@click.option("--kafka-group", "group", help="For Kafka connections: group ID")
|
|
548
|
+
@click.option("--group", "group", hidden=True)
|
|
549
|
+
@click.option(
|
|
550
|
+
"--kafka-auto-offset-reset",
|
|
551
|
+
"auto_offset_reset",
|
|
552
|
+
default=None,
|
|
553
|
+
type=click.Choice(["latest", "earliest"], case_sensitive=False),
|
|
554
|
+
help='Kafka auto.offset.reset config. Valid values are: ["latest", "earliest"]',
|
|
555
|
+
)
|
|
556
|
+
@click.option("--auto-offset-reset", "auto_offset_reset", hidden=True)
|
|
557
|
+
@click.pass_context
|
|
558
|
+
@coro
|
|
559
|
+
# Example usage: tb datasource connect 776824da-ac64-4de4-b8b8-b909f69d5ed5 new_ds --topic a --group b --auto-offset-reset latest
|
|
560
|
+
async def datasource_connect(ctx, connection, datasource_name, topic, group, auto_offset_reset):
|
|
561
|
+
"""Create a new datasource from an existing connection"""
|
|
562
|
+
|
|
563
|
+
validate_datasource_name(datasource_name)
|
|
564
|
+
|
|
565
|
+
client: TinyB = ctx.obj["client"]
|
|
566
|
+
|
|
567
|
+
connector = await client.get_connector(connection, key="name") or await client.get_connector(connection, key="id")
|
|
568
|
+
if not connector:
|
|
569
|
+
raise CLIDatasourceException(FeedbackManager.error_connection_does_not_exists(connection=connection))
|
|
570
|
+
|
|
571
|
+
service: str = connector.get("service", "")
|
|
572
|
+
if service == "kafka":
|
|
573
|
+
topic and validate_kafka_topic(topic)
|
|
574
|
+
group and validate_kafka_group(group)
|
|
575
|
+
auto_offset_reset and validate_kafka_auto_offset_reset(auto_offset_reset)
|
|
576
|
+
|
|
577
|
+
if not topic:
|
|
578
|
+
try:
|
|
579
|
+
topics = await client.kafka_list_topics(connection)
|
|
580
|
+
click.echo("We've discovered the following topics:")
|
|
581
|
+
for t in topics:
|
|
582
|
+
click.echo(f" {t}")
|
|
583
|
+
except Exception as e:
|
|
584
|
+
logging.debug(f"Error listing topics: {e}")
|
|
585
|
+
topic = click.prompt("Kafka topic")
|
|
586
|
+
validate_kafka_topic(topic)
|
|
587
|
+
if not group:
|
|
588
|
+
group = click.prompt("Kafka group")
|
|
589
|
+
validate_kafka_group(group)
|
|
590
|
+
if not auto_offset_reset:
|
|
591
|
+
click.echo("Kafka doesn't seem to have prior commits on this topic and group ID")
|
|
592
|
+
click.echo("Setting auto.offset.reset is required. Valid values:")
|
|
593
|
+
click.echo(" latest Skip earlier messages and ingest only new messages")
|
|
594
|
+
click.echo(" earliest Start ingestion from the first message")
|
|
595
|
+
auto_offset_reset = click.prompt("Kafka auto.offset.reset config")
|
|
596
|
+
validate_kafka_auto_offset_reset(auto_offset_reset)
|
|
597
|
+
if not click.confirm("Proceed?"):
|
|
598
|
+
return
|
|
599
|
+
resp = await client.datasource_kafka_connect(connection, datasource_name, topic, group, auto_offset_reset)
|
|
600
|
+
datasource_id = resp["datasource"]["id"]
|
|
601
|
+
click.echo(FeedbackManager.success_datasource_kafka_connected(id=datasource_id))
|
|
602
|
+
else:
|
|
603
|
+
raise CLIDatasourceException(FeedbackManager.error_unknown_connection_service(service=service))
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
@datasource.command(name="share")
|
|
607
|
+
@click.argument("datasource_name")
|
|
608
|
+
@click.argument("workspace_name_or_id")
|
|
609
|
+
@click.option("--user_token", default=None, help="When passed, we won't prompt asking for it")
|
|
610
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
611
|
+
@click.pass_context
|
|
612
|
+
@coro
|
|
613
|
+
async def datasource_share(ctx: Context, datasource_name: str, workspace_name_or_id: str, user_token: str, yes: bool):
|
|
614
|
+
"""Share a datasource"""
|
|
615
|
+
|
|
616
|
+
config = CLIConfig.get_project_config()
|
|
617
|
+
client = config.get_client()
|
|
618
|
+
host = config.get_host() or CLIConfig.DEFAULTS["host"]
|
|
619
|
+
ui_host = get_display_host(host)
|
|
620
|
+
|
|
621
|
+
_datasource: Dict[str, Any] = {}
|
|
622
|
+
try:
|
|
623
|
+
_datasource = await client.get_datasource(datasource_name)
|
|
624
|
+
except AuthNoTokenException:
|
|
625
|
+
raise
|
|
626
|
+
except DoesNotExistException:
|
|
627
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
628
|
+
except Exception as e:
|
|
629
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=str(e)))
|
|
630
|
+
|
|
631
|
+
workspaces: List[Dict[str, Any]] = (await client.user_workspaces()).get("workspaces", [])
|
|
632
|
+
destination_workspace = next(
|
|
633
|
+
(
|
|
634
|
+
workspace
|
|
635
|
+
for workspace in workspaces
|
|
636
|
+
if workspace["name"] == workspace_name_or_id or workspace["id"] == workspace_name_or_id
|
|
637
|
+
),
|
|
638
|
+
None,
|
|
639
|
+
)
|
|
640
|
+
current_workspace = next((workspace for workspace in workspaces if workspace["id"] == config["id"]), None)
|
|
641
|
+
|
|
642
|
+
if not destination_workspace:
|
|
643
|
+
raise CLIDatasourceException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
|
|
644
|
+
|
|
645
|
+
if not current_workspace:
|
|
646
|
+
raise CLIDatasourceException(FeedbackManager.error_not_authenticated())
|
|
647
|
+
|
|
648
|
+
if not user_token:
|
|
649
|
+
user_token = ask_for_user_token("share a Data Source", ui_host)
|
|
650
|
+
await check_user_token(ctx, user_token)
|
|
651
|
+
|
|
652
|
+
client.token = user_token
|
|
653
|
+
|
|
654
|
+
if yes or click.confirm(
|
|
655
|
+
FeedbackManager.warning_datasource_share(
|
|
656
|
+
datasource=datasource_name,
|
|
657
|
+
source_workspace=current_workspace.get("name"),
|
|
658
|
+
destination_workspace=destination_workspace["name"],
|
|
659
|
+
)
|
|
660
|
+
):
|
|
661
|
+
try:
|
|
662
|
+
await client.datasource_share(
|
|
663
|
+
datasource_id=_datasource.get("id", ""),
|
|
664
|
+
current_workspace_id=current_workspace.get("id", ""),
|
|
665
|
+
destination_workspace_id=destination_workspace.get("id", ""),
|
|
666
|
+
)
|
|
667
|
+
click.echo(
|
|
668
|
+
FeedbackManager.success_datasource_shared(
|
|
669
|
+
datasource=datasource_name, workspace=destination_workspace["name"]
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
except Exception as e:
|
|
673
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=str(e)))
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@datasource.command(name="unshare")
|
|
677
|
+
@click.argument("datasource_name")
|
|
678
|
+
@click.argument("workspace_name_or_id")
|
|
679
|
+
@click.option("--user_token", default=None, help="When passed, we won't prompt asking for it")
|
|
680
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
681
|
+
@click.pass_context
|
|
682
|
+
@coro
|
|
683
|
+
async def datasource_unshare(ctx: Context, datasource_name: str, workspace_name_or_id: str, user_token: str, yes: bool):
|
|
684
|
+
"""Unshare a datasource"""
|
|
685
|
+
|
|
686
|
+
config = CLIConfig.get_project_config()
|
|
687
|
+
client = config.get_client()
|
|
688
|
+
host = config.get_host() or CLIConfig.DEFAULTS["host"]
|
|
689
|
+
ui_host = get_display_host(host)
|
|
690
|
+
|
|
691
|
+
_datasource: Dict[str, Any] = {}
|
|
692
|
+
try:
|
|
693
|
+
_datasource = await client.get_datasource(datasource_name)
|
|
694
|
+
except AuthNoTokenException:
|
|
695
|
+
raise
|
|
696
|
+
except DoesNotExistException:
|
|
697
|
+
raise CLIDatasourceException(FeedbackManager.error_datasource_does_not_exist(datasource=datasource_name))
|
|
698
|
+
except Exception as e:
|
|
699
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=str(e)))
|
|
700
|
+
|
|
701
|
+
workspaces: List[Dict[str, Any]] = (await client.user_workspaces()).get("workspaces", [])
|
|
702
|
+
destination_workspace = next(
|
|
703
|
+
(
|
|
704
|
+
workspace
|
|
705
|
+
for workspace in workspaces
|
|
706
|
+
if workspace["name"] == workspace_name_or_id or workspace["id"] == workspace_name_or_id
|
|
707
|
+
),
|
|
708
|
+
None,
|
|
709
|
+
)
|
|
710
|
+
current_workspace = next((workspace for workspace in workspaces if workspace["id"] == config["id"]), None)
|
|
711
|
+
|
|
712
|
+
if not destination_workspace:
|
|
713
|
+
raise CLIDatasourceException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
|
|
714
|
+
|
|
715
|
+
if not current_workspace:
|
|
716
|
+
raise CLIDatasourceException(FeedbackManager.error_not_authenticated())
|
|
717
|
+
|
|
718
|
+
if not user_token:
|
|
719
|
+
user_token = ask_for_user_token("unshare a Data Source", ui_host)
|
|
720
|
+
await check_user_token(ctx, user_token)
|
|
721
|
+
|
|
722
|
+
client.token = user_token
|
|
723
|
+
|
|
724
|
+
if yes or click.confirm(
|
|
725
|
+
FeedbackManager.warning_datasource_unshare(
|
|
726
|
+
datasource=datasource_name,
|
|
727
|
+
source_workspace=current_workspace.get("name"),
|
|
728
|
+
destination_workspace=destination_workspace["name"],
|
|
729
|
+
)
|
|
730
|
+
):
|
|
731
|
+
try:
|
|
732
|
+
await client.datasource_unshare(
|
|
733
|
+
datasource_id=_datasource.get("id", ""),
|
|
734
|
+
current_workspace_id=current_workspace.get("id", ""),
|
|
735
|
+
destination_workspace_id=destination_workspace.get("id", ""),
|
|
736
|
+
)
|
|
737
|
+
click.echo(
|
|
738
|
+
FeedbackManager.success_datasource_unshared(
|
|
739
|
+
datasource=datasource_name, workspace=destination_workspace["name"]
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
except Exception as e:
|
|
743
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=str(e)))
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@datasource.command(name="sync")
|
|
747
|
+
@click.argument("datasource_name")
|
|
748
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
749
|
+
@click.pass_context
|
|
750
|
+
@coro
|
|
751
|
+
async def datasource_sync(ctx, datasource_name: str, yes: bool):
|
|
752
|
+
"""Sync from connector defined in .datasource file"""
|
|
753
|
+
|
|
754
|
+
try:
|
|
755
|
+
await sync_data(ctx, datasource_name, yes)
|
|
756
|
+
except AuthNoTokenException:
|
|
757
|
+
raise
|
|
758
|
+
except Exception as e:
|
|
759
|
+
raise CLIDatasourceException(FeedbackManager.error_syncing_datasource(datasource=datasource_name, error=str(e)))
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
@datasource.command(name="exchange", hidden=True)
|
|
763
|
+
@click.argument("datasource_a", required=True)
|
|
764
|
+
@click.argument("datasource_b", required=True)
|
|
765
|
+
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
766
|
+
@click.pass_context
|
|
767
|
+
@coro
|
|
768
|
+
async def datasource_exchange(ctx, datasource_a: str, datasource_b: str, yes: bool):
|
|
769
|
+
"""Exchange two data sources"""
|
|
770
|
+
|
|
771
|
+
client = ctx.obj["client"]
|
|
772
|
+
|
|
773
|
+
try:
|
|
774
|
+
if yes or click.confirm(FeedbackManager.warning_exchange(datasource_a=datasource_a, datasource_b=datasource_b)):
|
|
775
|
+
await client.datasource_exchange(datasource_a, datasource_b)
|
|
776
|
+
except Exception as e:
|
|
777
|
+
raise CLIDatasourceException(FeedbackManager.error_exception(error=e))
|
|
778
|
+
|
|
779
|
+
click.echo(FeedbackManager.success_exchange_datasources(datasource_a=datasource_a, datasource_b=datasource_b))
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@datasource.command(name="copy")
|
|
783
|
+
@click.argument("datasource_name")
|
|
784
|
+
@click.option(
|
|
785
|
+
"--sql",
|
|
786
|
+
default=None,
|
|
787
|
+
help="Freeform SQL query to select what is copied from Main into the Branch Data Source",
|
|
788
|
+
required=False,
|
|
789
|
+
)
|
|
790
|
+
@click.option(
|
|
791
|
+
"--sql-from-main",
|
|
792
|
+
is_flag=True,
|
|
793
|
+
default=False,
|
|
794
|
+
help="SQL query selecting * from the same Data Source in Main",
|
|
795
|
+
required=False,
|
|
796
|
+
)
|
|
797
|
+
@click.option("--wait", is_flag=True, default=False, help="Wait for copy job to finish, disabled by default")
|
|
798
|
+
@click.pass_context
|
|
799
|
+
@coro
|
|
800
|
+
async def datasource_copy_from_main(
|
|
801
|
+
ctx: Context, datasource_name: str, sql: str, sql_from_main: bool, wait: bool
|
|
802
|
+
) -> None:
|
|
803
|
+
"""Copy data source from Main."""
|
|
804
|
+
|
|
805
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
806
|
+
|
|
807
|
+
if sql and sql_from_main:
|
|
808
|
+
click.echo(FeedbackManager.error_exception(error="Use --sql or --sql-from-main but not both"))
|
|
809
|
+
return
|
|
810
|
+
|
|
811
|
+
if not sql and not sql_from_main:
|
|
812
|
+
click.echo(FeedbackManager.error_exception(error="Use --sql or --sql-from-main"))
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
response = await client.datasource_query_copy(
|
|
816
|
+
datasource_name, sql if sql else f"SELECT * FROM main.{datasource_name}"
|
|
817
|
+
)
|
|
818
|
+
if "job" not in response:
|
|
819
|
+
raise Exception(response)
|
|
820
|
+
job_id = response["job"]["job_id"]
|
|
821
|
+
job_url = response["job"]["job_url"]
|
|
822
|
+
if sql:
|
|
823
|
+
click.echo(FeedbackManager.info_copy_with_sql_job_url(sql=sql, datasource_name=datasource_name, url=job_url))
|
|
824
|
+
else:
|
|
825
|
+
click.echo(FeedbackManager.info_copy_from_main_job_url(datasource_name=datasource_name, url=job_url))
|
|
826
|
+
if wait:
|
|
827
|
+
base_msg = "Copy from Main Workspace" if sql_from_main else f"Copy from {sql}"
|
|
828
|
+
await wait_job(client, job_id, job_url, f"{base_msg} to {datasource_name}")
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
@datasource.group(name="scheduling")
|
|
832
|
+
@click.pass_context
|
|
833
|
+
def datasource_scheduling(ctx: Context) -> None:
|
|
834
|
+
"""Data Source scheduling commands."""
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
@datasource_scheduling.command(name="state")
|
|
838
|
+
@click.argument("datasource_name")
|
|
839
|
+
@click.pass_context
|
|
840
|
+
@coro
|
|
841
|
+
async def datasource_scheduling_state(ctx: Context, datasource_name: str) -> None:
|
|
842
|
+
"""Get the scheduling state of a Data Source."""
|
|
843
|
+
client: TinyB = ctx.obj["client"]
|
|
844
|
+
try:
|
|
845
|
+
await client.get_datasource(datasource_name) # Check if datasource exists
|
|
846
|
+
state = await client.datasource_scheduling_state(datasource_name)
|
|
847
|
+
click.echo(FeedbackManager.info_datasource_scheduling_state(datasource=datasource_name, state=state))
|
|
848
|
+
|
|
849
|
+
except AuthNoTokenException:
|
|
850
|
+
raise
|
|
851
|
+
except Exception as e:
|
|
852
|
+
raise CLIDatasourceException(
|
|
853
|
+
FeedbackManager.error_datasource_scheduling_state(datasource=datasource_name, error=e)
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
@datasource_scheduling.command(name="pause")
|
|
858
|
+
@click.argument("datasource_name")
|
|
859
|
+
@click.pass_context
|
|
860
|
+
@coro
|
|
861
|
+
async def datasource_scheduling_pause(ctx: Context, datasource_name: str) -> None:
|
|
862
|
+
"""Pause the scheduling of a Data Source."""
|
|
863
|
+
|
|
864
|
+
click.echo(FeedbackManager.info_datasource_scheduling_pause())
|
|
865
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
866
|
+
|
|
867
|
+
try:
|
|
868
|
+
await client.get_datasource(datasource_name) # Check if datasource exists
|
|
869
|
+
await client.datasource_scheduling_pause(datasource_name)
|
|
870
|
+
click.echo(FeedbackManager.success_datasource_scheduling_paused(datasource=datasource_name))
|
|
871
|
+
|
|
872
|
+
except AuthNoTokenException:
|
|
873
|
+
raise
|
|
874
|
+
except Exception as e:
|
|
875
|
+
raise CLIDatasourceException(
|
|
876
|
+
FeedbackManager.error_pausing_datasource_scheduling(datasource=datasource_name, error=e)
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
@datasource_scheduling.command(name="resume")
|
|
881
|
+
@click.argument("datasource_name")
|
|
882
|
+
@click.pass_context
|
|
883
|
+
@coro
|
|
884
|
+
async def datasource_scheduling_resume(ctx: Context, datasource_name: str) -> None:
|
|
885
|
+
"""Resume the scheduling of a Data Source."""
|
|
886
|
+
|
|
887
|
+
click.echo(FeedbackManager.info_datasource_scheduling_resume())
|
|
888
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
889
|
+
|
|
890
|
+
try:
|
|
891
|
+
await client.get_datasource(datasource_name) # Check if datasource exists
|
|
892
|
+
await client.datasource_scheduling_resume(datasource_name)
|
|
893
|
+
click.echo(FeedbackManager.success_datasource_scheduling_resumed(datasource=datasource_name))
|
|
894
|
+
|
|
895
|
+
except AuthNoTokenException:
|
|
896
|
+
raise
|
|
897
|
+
except Exception as e:
|
|
898
|
+
raise CLIDatasourceException(
|
|
899
|
+
FeedbackManager.error_resuming_datasource_scheduling(datasource=datasource_name, error=e)
|
|
900
|
+
)
|