tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev31__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/sql_template.py CHANGED
@@ -12,7 +12,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
  from tornado import escape
13
13
  from tornado.util import ObjectDict, exec_in, unicode_type
14
14
 
15
- from tinybird.context import ff_preprocess_parameters_circuit_breaker, ff_split_to_array_escape
15
+ from tinybird.context import (
16
+ ff_column_json_backticks_circuit_breaker,
17
+ ff_preprocess_parameters_circuit_breaker,
18
+ ff_split_to_array_escape,
19
+ )
16
20
 
17
21
  from .datatypes import testers
18
22
  from .tornado_template import VALID_CUSTOM_FUNCTION_NAMES, SecurityException, Template
@@ -271,6 +275,8 @@ def columns(x, default=None, fn=None):
271
275
 
272
276
 
273
277
  def column(x, default=None):
278
+ bypass_colunn_json_backticks = ff_column_json_backticks_circuit_breaker.get(False)
279
+
274
280
  if x is None or isinstance(x, Placeholder):
275
281
  if default is None:
276
282
  raise SQLTemplateException(
@@ -278,7 +284,18 @@ def column(x, default=None):
278
284
  documentation="/cli/advanced-templates.html#column",
279
285
  )
280
286
  x = default
281
- return Symbol("`" + sqlescape(x) + "`")
287
+
288
+ quote = "`"
289
+ if bypass_colunn_json_backticks:
290
+ return Symbol(quote + sqlescape(x) + quote)
291
+
292
+ try:
293
+ slices = x.split(".")
294
+ escaped_slices = [quote + sqlescape(s) + quote for s in slices]
295
+ escaped = ".".join(escaped_slices)
296
+ return Symbol(escaped)
297
+ except Exception: # in case there's a problem with .split
298
+ return Symbol(quote + sqlescape(x) + quote)
282
299
 
283
300
 
284
301
  def symbol(x, quote="`"):
@@ -2109,6 +2126,8 @@ def render_sql_template(
2109
2126
  ('SELECT `foo`', {}, [])
2110
2127
  >>> render_sql_template("SELECT {{column(agg)}}", {'agg': '"foo"'})
2111
2128
  ('SELECT `foo`', {}, [])
2129
+ >>> render_sql_template("SELECT {{column(agg)}}", {'agg': 'json.a'})
2130
+ ('SELECT `json`.`a`', {}, [])
2112
2131
  >>> render_sql_template('{% if not defined(test) %}error("This is an error"){% end %}', {})
2113
2132
  ('error("This is an error")', {}, [])
2114
2133
  >>> render_sql_template('{% if not defined(test) %}custom_error({error: "This is an error"}){% end %}', {})
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev29'
8
- __revision__ = '1ff2823'
7
+ __version__ = '0.0.1.dev31'
8
+ __revision__ = '0e056e8'
tinybird/tb/cli.py CHANGED
@@ -6,7 +6,7 @@ if sys.platform == "win32":
6
6
 
7
7
  import tinybird.tb.modules.auth
8
8
  import tinybird.tb.modules.build
9
- import tinybird.tb.modules.build_server
9
+ import tinybird.tb.modules.build_client
10
10
  import tinybird.tb.modules.cli
11
11
  import tinybird.tb.modules.common
12
12
  import tinybird.tb.modules.connection
@@ -22,6 +22,7 @@ import tinybird.tb.modules.pipe
22
22
  import tinybird.tb.modules.tag
23
23
  import tinybird.tb.modules.test
24
24
  import tinybird.tb.modules.token
25
+ import tinybird.tb.modules.update
25
26
  import tinybird.tb.modules.workspace
26
27
  import tinybird.tb.modules.workspace_members
27
28
 
@@ -1,214 +1,101 @@
1
1
  import asyncio
2
- import os
2
+ import glob
3
+ import json
4
+ import logging
3
5
  import threading
4
- import time
5
6
  from pathlib import Path
6
7
  from typing import List
7
8
 
8
9
  import click
10
+ import requests
9
11
 
10
- import tinybird.context as context
11
12
  from tinybird.client import TinyB
12
- from tinybird.config import FeatureFlags
13
13
  from tinybird.tb.modules.cli import cli
14
- from tinybird.tb.modules.common import push_data
15
- from tinybird.tb.modules.datafile.build import folder_build
16
- from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
17
- from tinybird.tb.modules.datafile.exceptions import ParseException
18
- from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
19
- from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
20
- from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
21
14
  from tinybird.tb.modules.feedback_manager import FeedbackManager
22
15
  from tinybird.tb.modules.local_common import get_tinybird_local_client
23
- from tinybird.tb.modules.shell import Shell, print_table_formatted
24
- from tinybird.tb.modules.watch import watch_files
16
+ from tinybird.tb.modules.shell import Shell
17
+ from tinybird.tb.modules.watch import watch_project
25
18
 
26
19
 
27
20
  @cli.command()
28
- @click.option(
29
- "--folder",
30
- default=".",
31
- help="Folder from where to execute the command. By default the current folder.",
32
- hidden=True,
33
- type=click.types.STRING,
34
- )
35
- @click.option(
36
- "--watch",
37
- is_flag=True,
38
- help="Watch for changes in the files and rebuild them.",
39
- )
40
- def build(
41
- folder: str,
42
- watch: bool,
43
- ) -> None:
44
- """Build the project in Tinybird Local."""
45
- ignore_sql_errors = FeatureFlags.ignore_sql_errors()
46
- context.disable_template_security_validation.set(True)
47
- is_internal = has_internal_datafiles(folder)
48
- folder_path = os.path.abspath(folder)
49
- tb_client = asyncio.run(get_tinybird_local_client(folder_path))
21
+ @click.option("--folder", type=str, default=".")
22
+ @click.option("--watch", is_flag=True, default=False, help="Watch for changes and rebuild automatically")
23
+ def build(folder: str, watch: bool) -> None:
24
+ """
25
+ Validate and build the project server side.
26
+ """
50
27
 
51
- def check_filenames(filenames: List[str]):
52
- parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
53
- incl_suffix = ".incl"
28
+ tb_client = asyncio.run(get_tinybird_local_client(folder))
54
29
 
55
- for filename in filenames:
56
- if os.path.isdir(filename):
57
- check_filenames(filenames=get_project_filenames(filename))
30
+ def process() -> None:
31
+ build_project(folder, tb_client)
58
32
 
59
- file_suffix = Path(filename).suffix
60
- if file_suffix == incl_suffix:
61
- continue
62
-
63
- parser = parser_matrix.get(file_suffix)
64
- if not parser:
65
- raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
66
-
67
- parser(filename)
68
-
69
- async def process(filenames: List[str], watch: bool = False):
70
- datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
71
- if len(datafiles) > 0:
72
- check_filenames(filenames=datafiles)
73
- await folder_build(
74
- tb_client,
75
- filenames=datafiles,
76
- ignore_sql_errors=ignore_sql_errors,
77
- is_internal=is_internal,
78
- watch=watch,
79
- folder=folder,
80
- )
81
- if len(filenames) > 0:
82
- filename = filenames[0]
83
- if filename.endswith(".ndjson"):
84
- fixture_path = Path(filename)
85
- datasources_path = Path(folder) / "datasources"
86
- ds_name = fixture_path.stem
87
- ds_path = datasources_path / f"{ds_name}.datasource"
88
-
89
- if not ds_path.exists():
90
- try:
91
- ds_name = "_".join(fixture_path.stem.split("_")[:-1])
92
- ds_path = datasources_path / f"{ds_name}.datasource"
93
- except Exception:
94
- pass
95
-
96
- if ds_path.exists():
97
- await append_datasource(tb_client, ds_name, str(fixture_path))
98
-
99
- if watch:
100
- if filename.endswith(".datasource"):
101
- ds_path = Path(filename)
102
- ds_name = ds_path.stem
103
- name = build_fixture_name(filename, ds_name, ds_path.read_text())
104
- fixture_folder = get_fixture_dir(folder)
105
- fixture_path = fixture_folder / f"{name}.ndjson"
106
-
107
- if not fixture_path.exists():
108
- fixture_path = fixture_folder / f"{ds_name}.ndjson"
109
-
110
- if fixture_path.exists():
111
- await append_datasource(tb_client, ds_name, str(fixture_path))
112
-
113
- if not filename.endswith(".ndjson"):
114
- await build_and_print_resource(tb_client, filename)
115
-
116
- datafiles = get_project_filenames(folder)
117
- fixtures = get_project_fixtures(folder)
118
- filenames = datafiles + fixtures
119
-
120
- async def build_once(filenames: List[str]):
121
- ok = False
122
- try:
123
- click.echo(FeedbackManager.highlight(message="» Building project...\n"))
124
- time_start = time.time()
125
- await process(filenames=filenames, watch=False)
126
- time_end = time.time()
127
- elapsed_time = time_end - time_start
128
- for filename in filenames:
129
- if filename.endswith(".datasource"):
130
- ds_path = Path(filename)
131
- ds_name = ds_path.stem
132
- name = build_fixture_name(filename, ds_name, ds_path.read_text())
133
- fixture_folder = get_fixture_dir(folder)
134
- fixture_path = fixture_folder / f"{name}.ndjson"
135
-
136
- if not fixture_path.exists():
137
- fixture_path = fixture_folder / f"{ds_name}.ndjson"
138
-
139
- if fixture_path.exists():
140
- await append_datasource(tb_client, ds_name, str(fixture_path))
141
- click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
142
- ok = True
143
- except Exception as e:
144
- error_path = Path(".tb_error.txt")
145
- if error_path.exists():
146
- content = error_path.read_text()
147
- content += f"\n\n{str(e)}"
148
- error_path.write_text(content)
149
- else:
150
- error_path.write_text(str(e))
151
- click.echo(FeedbackManager.error_exception(error=e))
152
- ok = False
153
- return ok
154
-
155
- build_ok = asyncio.run(build_once(filenames))
33
+ process()
156
34
 
157
35
  if watch:
158
- paths = [Path(f) for f in get_project_filenames(folder, with_vendor=True)]
159
-
160
- def is_vendor(f: Path) -> bool:
161
- return f.parts[0] == "vendor"
162
-
163
- def get_vendor_workspace(f: Path) -> str:
164
- return f.parts[1]
165
-
166
- def is_endpoint(f: Path) -> bool:
167
- return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
168
-
169
- def is_pipe(f: Path) -> bool:
170
- return f.suffix == ".pipe" and not is_vendor(f)
171
-
172
- datasource_paths = [f for f in paths if f.suffix == ".datasource"]
173
- datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
174
- shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
175
- pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
176
- endpoints = [f.stem for f in paths if is_endpoint(f)]
177
- shell = Shell(
178
- folder=folder,
179
- client=tb_client,
180
- datasources=datasources,
181
- shared_datasources=shared_datasources,
182
- pipes=pipes,
183
- endpoints=endpoints,
184
- )
36
+ shell = Shell(folder=folder, client=tb_client)
185
37
  click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
186
38
  watcher_thread = threading.Thread(
187
- target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
39
+ target=watch_project,
40
+ args=(shell, process, folder),
41
+ daemon=True,
188
42
  )
189
43
  watcher_thread.start()
190
44
  shell.run()
191
45
 
192
46
 
193
- async def build_and_print_resource(tb_client: TinyB, filename: str):
194
- resource_path = Path(filename)
195
- name = resource_path.stem
196
- pipeline = name if filename.endswith(".pipe") else None
197
- res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
198
- print_table_formatted(res, name)
199
-
200
-
201
- async def append_datasource(
202
- tb_client: TinyB,
203
- datasource_name: str,
204
- url: str,
205
- ):
206
- await tb_client.datasource_truncate(datasource_name)
207
- await push_data(
208
- tb_client,
209
- datasource_name,
210
- url,
211
- mode="append",
212
- concurrency=1,
213
- silent=True,
214
- )
47
+ def get_project_files(project_path: Path) -> List[str]:
48
+ project_file_extensions = ("datasource", "pipe")
49
+ project_files = []
50
+ for extension in project_file_extensions:
51
+ for project_file in glob.glob(f"{project_path}/**/*.{extension}", recursive=True):
52
+ logging.debug(f"Found project file: {project_file}")
53
+ project_files.append(project_file)
54
+ return project_files
55
+
56
+
57
+ def build_project(folder: str, tb_client: TinyB) -> None:
58
+ MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
59
+ DATAFILE_TYPE_TO_CONTENT_TYPE = {
60
+ ".datasource": "text/plain",
61
+ ".pipe": "text/plain",
62
+ }
63
+ TINYBIRD_API_URL = tb_client.host + "/v1/build"
64
+ TINYBIRD_API_KEY = tb_client.token
65
+ try:
66
+ files = [
67
+ ("context://", ("cli-version", "1.0.0", "text/plain")),
68
+ ]
69
+ fds = []
70
+ project_path = Path(folder)
71
+ project_files = get_project_files(project_path)
72
+ for file_path in project_files:
73
+ relative_path = str(Path(file_path).relative_to(project_path))
74
+ fd = open(file_path, "rb")
75
+ fds.append(fd)
76
+ content_type = DATAFILE_TYPE_TO_CONTENT_TYPE.get(Path(file_path).suffix, "application/unknown")
77
+ files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
78
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
79
+
80
+ r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
81
+ result = r.json()
82
+
83
+ logging.debug(json.dumps(result, indent=2))
84
+
85
+ build_result = result.get("result")
86
+ if build_result == "success":
87
+ click.echo(FeedbackManager.success(message="Build completed successfully"))
88
+ elif build_result == "failed":
89
+ click.echo(FeedbackManager.error(message="Build failed"))
90
+ build_errors = result.get("errors")
91
+ for build_error in build_errors:
92
+ filename_bit = f"{build_error.get('filename', '')}"
93
+ error_msg = (filename_bit + "\n\n") if filename_bit else "" + build_error.get("error")
94
+ click.echo(FeedbackManager.error(message=error_msg))
95
+ else:
96
+ click.echo(FeedbackManager.error(message=f"Unknown build result. Error: {result.get('error')}"))
97
+ except Exception as e:
98
+ click.echo(FeedbackManager.error_exception(error="Error building project: " + str(e)))
99
+ finally:
100
+ for fd in fds:
101
+ fd.close()
@@ -0,0 +1,219 @@
1
+ import asyncio
2
+ import os
3
+ import threading
4
+ import time
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ import click
9
+
10
+ import tinybird.context as context
11
+ from tinybird.client import TinyB
12
+ from tinybird.config import FeatureFlags
13
+ from tinybird.tb.modules.cli import cli
14
+ from tinybird.tb.modules.common import push_data
15
+ from tinybird.tb.modules.datafile.build import folder_build
16
+ from tinybird.tb.modules.datafile.common import get_project_filenames, get_project_fixtures, has_internal_datafiles
17
+ from tinybird.tb.modules.datafile.exceptions import ParseException
18
+ from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
19
+ from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
20
+ from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
21
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
22
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
23
+ from tinybird.tb.modules.shell import Shell, print_table_formatted
24
+ from tinybird.tb.modules.watch import watch_files
25
+
26
+
27
+ def is_vendor(f: Path) -> bool:
28
+ return f.parts[0] == "vendor"
29
+
30
+
31
+ def get_vendor_workspace(f: Path) -> str:
32
+ return f.parts[1]
33
+
34
+
35
+ def is_endpoint(f: Path) -> bool:
36
+ return f.suffix == ".pipe" and not is_vendor(f) and f.parts[0] == "endpoints"
37
+
38
+
39
+ def is_pipe(f: Path) -> bool:
40
+ return f.suffix == ".pipe" and not is_vendor(f)
41
+
42
+
43
+ def check_filenames(filenames: List[str]):
44
+ parser_matrix = {".pipe": parse_pipe, ".datasource": parse_datasource}
45
+ incl_suffix = ".incl"
46
+
47
+ for filename in filenames:
48
+ if os.path.isdir(filename):
49
+ check_filenames(filenames=get_project_filenames(filename))
50
+
51
+ file_suffix = Path(filename).suffix
52
+ if file_suffix == incl_suffix:
53
+ continue
54
+
55
+ parser = parser_matrix.get(file_suffix)
56
+ if not parser:
57
+ raise ParseException(FeedbackManager.error_unsupported_datafile(extension=file_suffix))
58
+
59
+ parser(filename)
60
+
61
+
62
+ @cli.command()
63
+ @click.option(
64
+ "--folder",
65
+ default=".",
66
+ help="Folder from where to execute the command. By default the current folder.",
67
+ hidden=True,
68
+ type=click.types.STRING,
69
+ )
70
+ @click.option(
71
+ "--watch",
72
+ is_flag=True,
73
+ help="Watch for changes in the files and rebuild them.",
74
+ )
75
+ def build_client(
76
+ folder: str,
77
+ watch: bool,
78
+ ) -> None:
79
+ """Build the project in Tinybird Local."""
80
+ ignore_sql_errors = FeatureFlags.ignore_sql_errors()
81
+ context.disable_template_security_validation.set(True)
82
+ is_internal = has_internal_datafiles(folder)
83
+ folder_path = os.path.abspath(folder)
84
+ tb_client = asyncio.run(get_tinybird_local_client(folder_path))
85
+
86
+ async def process(filenames: List[str], watch: bool = False):
87
+ datafiles = [f for f in filenames if f.endswith(".datasource") or f.endswith(".pipe")]
88
+ if len(datafiles) > 0:
89
+ check_filenames(filenames=datafiles)
90
+ await folder_build(
91
+ tb_client,
92
+ filenames=datafiles,
93
+ ignore_sql_errors=ignore_sql_errors,
94
+ is_internal=is_internal,
95
+ watch=watch,
96
+ folder=folder,
97
+ )
98
+ if len(filenames) > 0:
99
+ filename = filenames[0]
100
+ if filename.endswith(".ndjson"):
101
+ fixture_path = Path(filename)
102
+ datasources_path = Path(folder) / "datasources"
103
+ ds_name = fixture_path.stem
104
+ ds_path = datasources_path / f"{ds_name}.datasource"
105
+
106
+ if not ds_path.exists():
107
+ try:
108
+ ds_name = "_".join(fixture_path.stem.split("_")[:-1])
109
+ ds_path = datasources_path / f"{ds_name}.datasource"
110
+ except Exception:
111
+ pass
112
+
113
+ if ds_path.exists():
114
+ await append_datasource(tb_client, ds_name, str(fixture_path))
115
+
116
+ if watch:
117
+ if filename.endswith(".datasource"):
118
+ ds_path = Path(filename)
119
+ ds_name = ds_path.stem
120
+ name = build_fixture_name(filename, ds_name, ds_path.read_text())
121
+ fixture_folder = get_fixture_dir(folder)
122
+ fixture_path = fixture_folder / f"{name}.ndjson"
123
+
124
+ if not fixture_path.exists():
125
+ fixture_path = fixture_folder / f"{ds_name}.ndjson"
126
+
127
+ if fixture_path.exists():
128
+ await append_datasource(tb_client, ds_name, str(fixture_path))
129
+
130
+ if not filename.endswith(".ndjson"):
131
+ await build_and_print_resource(tb_client, filename)
132
+
133
+ datafiles = get_project_filenames(folder)
134
+ fixtures = get_project_fixtures(folder)
135
+ filenames = datafiles + fixtures
136
+
137
+ async def build_once(filenames: List[str]):
138
+ ok = False
139
+ try:
140
+ click.echo(FeedbackManager.highlight(message="» Building project...\n"))
141
+ time_start = time.time()
142
+ await process(filenames=filenames, watch=False)
143
+ time_end = time.time()
144
+ elapsed_time = time_end - time_start
145
+ for filename in filenames:
146
+ if filename.endswith(".datasource"):
147
+ ds_path = Path(filename)
148
+ ds_name = ds_path.stem
149
+ name = build_fixture_name(filename, ds_name, ds_path.read_text())
150
+ fixture_folder = get_fixture_dir(folder)
151
+ fixture_path = fixture_folder / f"{name}.ndjson"
152
+
153
+ if not fixture_path.exists():
154
+ fixture_path = fixture_folder / f"{ds_name}.ndjson"
155
+
156
+ if fixture_path.exists():
157
+ await append_datasource(tb_client, ds_name, str(fixture_path))
158
+ click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
159
+ ok = True
160
+ except Exception as e:
161
+ error_path = Path(".tb_error.txt")
162
+ if error_path.exists():
163
+ content = error_path.read_text()
164
+ content += f"\n\n{str(e)}"
165
+ error_path.write_text(content)
166
+ else:
167
+ error_path.write_text(str(e))
168
+ click.echo(FeedbackManager.error_exception(error=e))
169
+ ok = False
170
+ return ok
171
+
172
+ build_ok = asyncio.run(build_once(filenames))
173
+
174
+ if watch:
175
+ paths = [Path(f) for f in get_project_filenames(folder, with_vendor=True)]
176
+
177
+ datasource_paths = [f for f in paths if f.suffix == ".datasource"]
178
+ datasources = [f.stem for f in datasource_paths if not is_vendor(f)]
179
+ shared_datasources = [f"{get_vendor_workspace(f)}.{f.stem}" for f in datasource_paths if is_vendor(f)]
180
+ pipes = [f.stem for f in paths if is_pipe(f) and not is_endpoint(f)]
181
+ endpoints = [f.stem for f in paths if is_endpoint(f)]
182
+ shell = Shell(
183
+ folder=folder,
184
+ client=tb_client,
185
+ datasources=datasources,
186
+ shared_datasources=shared_datasources,
187
+ pipes=pipes,
188
+ endpoints=endpoints,
189
+ )
190
+ click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
191
+ watcher_thread = threading.Thread(
192
+ target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
193
+ )
194
+ watcher_thread.start()
195
+ shell.run()
196
+
197
+
198
+ async def build_and_print_resource(tb_client: TinyB, filename: str):
199
+ resource_path = Path(filename)
200
+ name = resource_path.stem
201
+ pipeline = name if filename.endswith(".pipe") else None
202
+ res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
203
+ print_table_formatted(res, name)
204
+
205
+
206
+ async def append_datasource(
207
+ tb_client: TinyB,
208
+ datasource_name: str,
209
+ url: str,
210
+ ):
211
+ await tb_client.datasource_truncate(datasource_name)
212
+ await push_data(
213
+ tb_client,
214
+ datasource_name,
215
+ url,
216
+ mode="append",
217
+ concurrency=1,
218
+ silent=True,
219
+ )