tinybird 0.0.1.dev41__py3-none-any.whl → 0.0.1.dev43__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/client.py CHANGED
@@ -72,7 +72,7 @@ def parse_error_response(response: Response) -> str:
72
72
  if content.get("error", None):
73
73
  error = content["error"]
74
74
  if content.get("errors", None):
75
- error += f' -> errors: {content.get("errors")}'
75
+ error += f" -> errors: {content.get('errors')}"
76
76
  else:
77
77
  error = json.dumps(response, indent=4)
78
78
  return error
tinybird/connectors.py CHANGED
@@ -246,7 +246,7 @@ class BigQuery(Connector):
246
246
  uri='{self.gcs.gs_url()}{destination}*.csv',
247
247
  format='CSV',
248
248
  overwrite=true,
249
- header={ "true" if with_headers else "false" },
249
+ header={"true" if with_headers else "false"},
250
250
  field_delimiter=',') AS
251
251
  {sql}
252
252
  """
@@ -319,7 +319,7 @@ class Snowflake(Connector):
319
319
 
320
320
  def create_stage(self):
321
321
  sql = f"""
322
- create stage "{self.options['schema']}".{self.stage()}
322
+ create stage "{self.options["schema"]}".{self.stage()}
323
323
  url='{self.gcs.gcs_url()}'
324
324
  storage_integration = {self.storage_integration()};
325
325
  """
@@ -337,7 +337,7 @@ class Snowflake(Connector):
337
337
  from ({sql})
338
338
  overwrite = true
339
339
  file_format = (TYPE=CSV COMPRESSION=NONE ESCAPE_UNENCLOSED_FIELD=NONE FIELD_DELIMITER='|' FIELD_OPTIONALLY_ENCLOSED_BY='"' null_if=())
340
- header = {"true" if with_headers else "false" }
340
+ header = {"true" if with_headers else "false"}
341
341
  max_file_size = 2500000000;
342
342
  """
343
343
  self.execute(sql)
@@ -951,7 +951,7 @@ Ready? """
951
951
  )
952
952
  success_datasource_alter = success_message("** The Data Source has been correctly updated.")
953
953
  success_datasource_kafka_connected = success_message(
954
- "** Data Source '{id}' created\n" "** Kafka streaming connection configured successfully!"
954
+ "** Data Source '{id}' created\n** Kafka streaming connection configured successfully!"
955
955
  )
956
956
  success_datasource_shared = success_message(
957
957
  "** The Data Source {datasource} has been correctly shared with {workspace}"
tinybird/prompts.py CHANGED
@@ -371,10 +371,20 @@ You are a Tinybird expert. You will be given a pipe containing different nodes w
371
371
  - The test command must be a valid Tinybird command that can be run in the terminal.
372
372
  - The test command can have as many parameters as are needed to test the pipe.
373
373
  - The parameter within Tinybird templating syntax looks like this one {{String(my_param_name, default_value)}}.
374
- - If there are no parameters, you can omit parameters and generate a single test command.
374
+ - If there are no parameters, you can omit parameters and generate a single test.
375
375
  - The format of the parameters is the following: ?param1=value1&param2=value2&param3=value3
376
+ - If some parameters are provided by the user and you need to use them, preserve in the same format as they were provided, like case sensitive.
376
377
  </instructions>
377
378
 
379
+ This is an example of a test with parameters:
380
+ <example>
381
+ <test>
382
+ <name>kpis_date_range</name>
383
+ <description>Test specific date range with daily granularity</description>
384
+ <parameters>?date_from=2024-01-01&date_to=2024-01-10</parameters>
385
+ </test>
386
+ </example>
387
+
378
388
  Follow the instructions and generate the following response with no additional text:
379
389
 
380
390
  <response>
tinybird/sql.py CHANGED
@@ -241,7 +241,7 @@ def format_parse_error(
241
241
  message += f" found at position {adjusted_position - len(keyword)}"
242
242
  else:
243
243
  message += (
244
- f" found {repr(table_structure[i]) if len(table_structure)>i else 'EOF'} at position {adjusted_position}"
244
+ f" found {repr(table_structure[i]) if len(table_structure) > i else 'EOF'} at position {adjusted_position}"
245
245
  )
246
246
  return message
247
247
 
@@ -100,7 +100,7 @@ def _format_jinja_node(self, node: Node, max_length: int) -> bool:
100
100
  parts = tag.code.split("\n")
101
101
  prefix = INDENT * (node.depth[0] + node.depth[1])
102
102
  if len(parts) > 1:
103
- tag.code = "\n".join([f'{prefix if i != 0 else ""}{part}' for i, part in enumerate(parts)])
103
+ tag.code = "\n".join([f"{prefix if i != 0 else ''}{part}" for i, part in enumerate(parts)])
104
104
 
105
105
  node.value = str(tag)
106
106
 
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.dev41'
8
- __revision__ = '86b5c65'
7
+ __version__ = '0.0.1.dev43'
8
+ __revision__ = 'ecb1311'
@@ -31,18 +31,16 @@ def build(ctx: click.Context, watch: bool) -> None:
31
31
  project: Project = ctx.ensure_object(dict)["project"]
32
32
  tb_client = asyncio.run(get_tinybird_local_client(str(project.path)))
33
33
  click.echo(FeedbackManager.highlight(message="\n» Building project..."))
34
-
35
34
  time_start = time.time()
36
35
 
37
- def process(file_changed: Optional[str] = None) -> None:
36
+ def process(file_changed: Optional[str] = None, diff: Optional[str] = None) -> None:
38
37
  if file_changed and file_changed.endswith(".ndjson"):
39
- rebuild_fixture(project, file_changed)
38
+ rebuild_fixture(project, tb_client, file_changed)
40
39
  else:
41
40
  build_project(project, tb_client)
42
- new_tb_client = asyncio.run(get_tinybird_local_client(str(project.path)))
43
41
  try:
44
42
  if file_changed:
45
- build_and_print_resource(new_tb_client, file_changed)
43
+ build_and_print_resource(tb_client, file_changed, diff)
46
44
  except Exception:
47
45
  pass
48
46
 
@@ -52,7 +50,7 @@ def build(ctx: click.Context, watch: bool) -> None:
52
50
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s"))
53
51
 
54
52
  if watch:
55
- shell = Shell(project=project)
53
+ shell = Shell(project=project, tb_client=tb_client)
56
54
  click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
57
55
  watcher_thread = threading.Thread(
58
56
  target=watch_project,
@@ -80,6 +78,9 @@ def build_project(project: Project, tb_client: TinyB) -> None:
80
78
  project_path = project.path
81
79
  project_files = project.get_project_files()
82
80
 
81
+ if not project_files:
82
+ return
83
+
83
84
  for file_path in project_files:
84
85
  relative_path = str(Path(file_path).relative_to(project_path))
85
86
  fd = open(file_path, "rb")
@@ -88,7 +89,7 @@ def build_project(project: Project, tb_client: TinyB) -> None:
88
89
  files.append((MULTIPART_BOUNDARY_DATA_PROJECT, (relative_path, fd.read().decode("utf-8"), content_type)))
89
90
  HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
90
91
 
91
- r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS, data={"folder": str(project_path)})
92
+ r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS)
92
93
  try:
93
94
  result = r.json()
94
95
  except Exception as e:
@@ -103,19 +104,21 @@ def build_project(project: Project, tb_client: TinyB) -> None:
103
104
  datasources = result.get("datasources", [])
104
105
  pipes = result.get("pipes", [])
105
106
  for ds in datasources:
106
- ds_path = next((p for p in project_files if p.endswith(ds.get("name") + ".datasource")), None)
107
- if ds_path:
108
- ds_path = ds_path.replace(f"{project.folder}/", "")
109
- click.echo(FeedbackManager.info(message=f"✓ {ds_path} created"))
107
+ ds_path_str: Optional[str] = next(
108
+ (p for p in project_files if p.endswith(ds.get("name") + ".datasource")), None
109
+ )
110
+ if ds_path_str:
111
+ ds_path = Path(ds_path_str)
112
+ ds_path_str = ds_path_str.replace(f"{project.folder}/", "")
113
+ click.echo(FeedbackManager.info(message=f"✓ {ds_path_str} created"))
110
114
  for pipe in pipes:
111
115
  pipe_name = pipe.get("name")
112
- pipe_path = next((p for p in project_files if p.endswith(pipe_name + ".pipe")), None)
113
- if pipe_path:
114
- pipe_path = pipe_path.replace(f"{project.folder}/", "")
115
- click.echo(FeedbackManager.info(message=f"✓ {pipe_path} created"))
116
+ pipe_path_str: Optional[str] = next((p for p in project_files if p.endswith(pipe_name + ".pipe")), None)
117
+ if pipe_path_str:
118
+ pipe_path_str = pipe_path_str.replace(f"{project.folder}/", "")
119
+ click.echo(FeedbackManager.info(message=f"✓ {pipe_path_str} created"))
116
120
 
117
121
  try:
118
- new_tb_client = asyncio.run(get_tinybird_local_client(str(project.path)))
119
122
  for filename in project_files:
120
123
  if filename.endswith(".datasource"):
121
124
  ds_path = Path(filename)
@@ -128,7 +131,7 @@ def build_project(project: Project, tb_client: TinyB) -> None:
128
131
  fixture_path = fixture_folder / f"{ds_name}.ndjson"
129
132
 
130
133
  if fixture_path.exists():
131
- append_fixture(new_tb_client, ds_name, str(fixture_path))
134
+ append_fixture(tb_client, ds_name, str(fixture_path))
132
135
  except Exception:
133
136
  pass
134
137
 
@@ -166,9 +169,8 @@ def append_fixture(
166
169
  )
167
170
 
168
171
 
169
- def rebuild_fixture(project: Project, fixture: str) -> None:
172
+ def rebuild_fixture(project: Project, tb_client: TinyB, fixture: str) -> None:
170
173
  try:
171
- tb_client = asyncio.run(get_tinybird_local_client(str(project.path)))
172
174
  fixture_path = Path(fixture)
173
175
  datasources_path = Path(project.folder) / "datasources"
174
176
  ds_name = fixture_path.stem
@@ -188,9 +190,17 @@ def rebuild_fixture(project: Project, fixture: str) -> None:
188
190
  click.echo(FeedbackManager.error_exception(error=e))
189
191
 
190
192
 
191
- def build_and_print_resource(tb_client: TinyB, filename: str):
193
+ def build_and_print_resource(tb_client: TinyB, filename: str, diff: Optional[str] = None):
194
+ table_name = diff
192
195
  resource_path = Path(filename)
193
- name = resource_path.stem
194
- pipeline = name if filename.endswith(".pipe") else None
195
- res = asyncio.run(tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline))
196
- print_table_formatted(res, name)
196
+ resource_name = resource_path.stem
197
+
198
+ pipeline = resource_name if filename.endswith(".pipe") else None
199
+
200
+ if not table_name:
201
+ table_name = resource_name
202
+
203
+ sql = f"SELECT * FROM {table_name} FORMAT JSON"
204
+
205
+ res = asyncio.run(tb_client.query(sql, pipeline=pipeline))
206
+ print_table_formatted(res, table_name)
@@ -166,7 +166,7 @@ def build_client(
166
166
  build_ok = asyncio.run(build_once(filenames))
167
167
 
168
168
  if watch:
169
- shell = Shell(project=project, client=tb_client)
169
+ shell = Shell(project=project, tb_client=tb_client)
170
170
  click.echo(FeedbackManager.gray(message="\nWatching for changes..."))
171
171
  watcher_thread = threading.Thread(
172
172
  target=watch_files, args=(filenames, process, shell, project, build_ok), daemon=True
@@ -39,7 +39,7 @@ jobs:
39
39
  working-directory: '{{ data_project_dir }}'
40
40
  services:
41
41
  tinybird:
42
- image: tinybirdco/tinybird-local:latest
42
+ image: tinybirdco/tinybird-local:beta
43
43
  ports:
44
44
  - 80:80
45
45
  steps:
@@ -83,7 +83,7 @@ tinybird_ci_workflow:
83
83
  - tb build
84
84
  - tb test run
85
85
  services:
86
- - name: tinybirdco/tinybird-local:latest
86
+ - name: tinybirdco/tinybird-local:beta
87
87
  alias: tinybird-local
88
88
  """
89
89
 
@@ -66,15 +66,17 @@ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
66
66
  @click.option("--host", help="Use custom host, defaults to TB_HOST envvar, then to https://api.tinybird.co")
67
67
  @click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
68
68
  @click.option("--prod/--local", is_flag=True, default=False, help="Run against production or local")
69
- @click.option("--folder", type=str, default=os.getcwd(), help="Folder where files will be placed")
69
+ @click.option("--folder", type=str, help="Folder where files will be placed")
70
70
  @click.version_option(version=VERSION)
71
71
  @click.pass_context
72
72
  @coro
73
- async def cli(ctx: Context, debug: bool, token: str, host: str, show_tokens: bool, prod: bool, folder: str) -> None:
73
+ async def cli(
74
+ ctx: Context, debug: bool, token: str, host: str, show_tokens: bool, prod: bool, folder: Optional[str]
75
+ ) -> None:
74
76
  """
75
77
  Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
76
78
  """
77
- project = Project(folder=folder)
79
+ project = Project(folder=folder or os.getcwd())
78
80
  # We need to unpatch for our tests not to break
79
81
  if show_tokens or not prod or ctx.invoked_subcommand == "build":
80
82
  __unpatch_click_output()
@@ -87,7 +89,7 @@ async def cli(ctx: Context, debug: bool, token: str, host: str, show_tokens: boo
87
89
  if debug:
88
90
  logging.basicConfig(level=logging.DEBUG)
89
91
 
90
- config_temp = CLIConfig.get_project_config(project.path)
92
+ config_temp = CLIConfig.get_project_config(str(project.path))
91
93
  if token:
92
94
  config_temp.set_token(token)
93
95
  if host:
@@ -5,13 +5,14 @@
5
5
 
6
6
  import json
7
7
  import re
8
+ from typing import Optional, Tuple
8
9
 
9
10
  import click
10
11
  from click import Context
11
12
 
12
- from tinybird.client import TinyB
13
+ from tinybird.client import AuthNoTokenException, TinyB
13
14
  from tinybird.tb.modules.cli import cli
14
- from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table
15
+ from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table, wait_job
15
16
  from tinybird.tb.modules.datafile.common import get_name_version
16
17
  from tinybird.tb.modules.exceptions import CLIPipeException
17
18
  from tinybird.tb.modules.feedback_manager import FeedbackManager
@@ -38,10 +39,10 @@ async def copy_ls(ctx: Context, match: str, format_: str):
38
39
  """List copy pipes"""
39
40
 
40
41
  client: TinyB = ctx.ensure_object(dict)["client"]
41
- pipes = await client.pipes(dependencies=False, node_attrs="name", attrs="name,updated_at")
42
+ pipes = await client.pipes(dependencies=False, node_attrs="name", attrs="name,updated_at,type")
42
43
  copies = [p for p in pipes if p.get("type") == "copy"]
43
44
  copies = sorted(copies, key=lambda p: p["updated_at"])
44
- columns = ["name", "updated at", "nodes", "url"]
45
+ columns = ["name", "updated at", "nodes"]
45
46
  table_human_readable = []
46
47
  table_machine_readable = []
47
48
  pattern = re.compile(match) if match else None
@@ -66,3 +67,93 @@ async def copy_ls(ctx: Context, match: str, format_: str):
66
67
  click.echo(json.dumps({"pipes": table_machine_readable}, indent=2))
67
68
  else:
68
69
  raise CLIPipeException(FeedbackManager.error_pipe_ls_type())
70
+
71
+
72
+ @copy.command(name="run", short_help="Run an on-demand copy job")
73
+ @click.argument("pipe_name_or_id")
74
+ @click.option("--wait", is_flag=True, default=False, help="Wait for the copy job to finish")
75
+ @click.option(
76
+ "--mode", type=click.Choice(["append", "replace"], case_sensitive=True), default=None, help="Copy strategy"
77
+ )
78
+ @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
79
+ @click.option(
80
+ "--param",
81
+ nargs=1,
82
+ type=str,
83
+ multiple=True,
84
+ default=None,
85
+ help="Key and value of the params you want the Copy pipe to be called with. For example: tb pipe copy run <my_copy_pipe> --param foo=bar",
86
+ )
87
+ @click.pass_context
88
+ @coro
89
+ async def copy_run(
90
+ ctx: click.Context, pipe_name_or_id: str, wait: bool, mode: str, yes: bool, param: Optional[Tuple[str]]
91
+ ):
92
+ """Run an on-demand copy job"""
93
+
94
+ params = dict(key_value.split("=") for key_value in param) if param else {}
95
+
96
+ if yes or click.confirm(FeedbackManager.warning_confirm_copy_pipe(pipe=pipe_name_or_id)):
97
+ click.echo(FeedbackManager.info_copy_job_running(pipe=pipe_name_or_id))
98
+ client: TinyB = ctx.ensure_object(dict)["client"]
99
+
100
+ try:
101
+ response = await client.pipe_run_copy(pipe_name_or_id, params, mode)
102
+
103
+ job_id = response["job"]["job_id"]
104
+ job_url = response["job"]["job_url"]
105
+ target_datasource_id = response["tags"]["copy_target_datasource"]
106
+ target_datasource = await client.get_datasource(target_datasource_id)
107
+ target_datasource_name = target_datasource["name"]
108
+ click.echo(
109
+ FeedbackManager.success_copy_job_created(target_datasource=target_datasource_name, job_url=job_url)
110
+ )
111
+
112
+ if wait:
113
+ await wait_job(client, job_id, job_url, "** Copying data")
114
+ click.echo(FeedbackManager.success_data_copied_to_ds(target_datasource=target_datasource_name))
115
+
116
+ except AuthNoTokenException:
117
+ raise
118
+ except Exception as e:
119
+ raise CLIPipeException(FeedbackManager.error_creating_copy_job(error=e))
120
+
121
+
122
+ @copy.command(name="resume", short_help="Resume a paused copy pipe")
123
+ @click.argument("pipe_name_or_id")
124
+ @click.pass_context
125
+ @coro
126
+ async def copy_resume(ctx: click.Context, pipe_name_or_id: str):
127
+ """Resume a paused copy pipe"""
128
+
129
+ click.echo(FeedbackManager.info_copy_pipe_resuming(pipe=pipe_name_or_id))
130
+ client: TinyB = ctx.ensure_object(dict)["client"]
131
+
132
+ try:
133
+ await client.pipe_resume_copy(pipe_name_or_id)
134
+ click.echo(FeedbackManager.success_copy_pipe_resumed(pipe=pipe_name_or_id))
135
+
136
+ except AuthNoTokenException:
137
+ raise
138
+ except Exception as e:
139
+ raise CLIPipeException(FeedbackManager.error_resuming_copy_pipe(error=e))
140
+
141
+
142
+ @copy.command(name="pause", short_help="Pause a running copy pipe")
143
+ @click.argument("pipe_name_or_id")
144
+ @click.pass_context
145
+ @coro
146
+ async def copy_pause(ctx: click.Context, pipe_name_or_id: str):
147
+ """Pause a running copy pipe"""
148
+
149
+ click.echo(FeedbackManager.info_copy_pipe_pausing(pipe=pipe_name_or_id))
150
+ client: TinyB = ctx.ensure_object(dict)["client"]
151
+
152
+ try:
153
+ await client.pipe_pause_copy(pipe_name_or_id)
154
+ click.echo(FeedbackManager.success_copy_pipe_paused(pipe=pipe_name_or_id))
155
+
156
+ except AuthNoTokenException:
157
+ raise
158
+ except Exception as e:
159
+ raise CLIPipeException(FeedbackManager.error_pausing_copy_pipe(error=e))
@@ -707,6 +707,7 @@ async def process(
707
707
  ):
708
708
  name, kind = filename.rsplit(".", 1)
709
709
  warnings = []
710
+ embedded_datasources = {} if embedded_datasources is None else embedded_datasources
710
711
 
711
712
  try:
712
713
  res = await process_file(
@@ -806,31 +807,45 @@ async def get_processed(
806
807
  to_run: Optional[Dict[str, Any]] = None,
807
808
  vendor_paths: Optional[List[Tuple[str, str]]] = None,
808
809
  processed: Optional[Set[str]] = None,
809
- tb_client: TinyB = None,
810
+ tb_client: Optional[TinyB] = None,
810
811
  skip_connectors: bool = False,
811
812
  current_ws: Optional[Dict[str, Any]] = None,
812
813
  fork_downstream: Optional[bool] = False,
813
814
  is_internal: Optional[bool] = False,
814
815
  dir_path: Optional[str] = None,
815
- embedded_datasources: Optional[Dict[str, Any]] = None,
816
+ embedded_datasources: Optional[Dict[str, Dict[str, Any]]] = None,
816
817
  ):
817
- if deps is None:
818
- deps = []
819
- if dep_map is None:
820
- dep_map = {}
821
- if to_run is None:
822
- to_run = {}
823
- if processed is None:
824
- processed = set()
818
+ # Initialize with proper type annotations
819
+ deps_list: List[str] = [] if deps is None else deps
820
+ dep_map_dict: Dict[str, Any] = {} if dep_map is None else dep_map
821
+ to_run_dict: Dict[str, Any] = {} if to_run is None else to_run
822
+ processed_set: Set[str] = set() if processed is None else processed
823
+ embedded_ds: Dict[str, Dict[str, Any]] = {} if embedded_datasources is None else embedded_datasources
825
824
 
826
825
  for filename in filenames:
827
826
  # just process changed filenames (tb deploy and --only-changes)
828
- if changed:
827
+ if changed is not None:
829
828
  resource = Path(filename).resolve().stem
830
829
  if resource in changed and (not changed[resource] or changed[resource] in ["shared", "remote"]):
831
830
  continue
832
831
  if os.path.isdir(filename):
833
- await get_processed(filenames=get_project_filenames(filename))
832
+ await get_processed(
833
+ filenames=get_project_filenames(filename),
834
+ changed=changed,
835
+ verbose=verbose,
836
+ deps=deps_list,
837
+ dep_map=dep_map_dict,
838
+ to_run=to_run_dict,
839
+ vendor_paths=vendor_paths,
840
+ processed=processed_set,
841
+ tb_client=tb_client,
842
+ skip_connectors=skip_connectors,
843
+ current_ws=current_ws,
844
+ fork_downstream=fork_downstream,
845
+ is_internal=is_internal,
846
+ dir_path=dir_path,
847
+ embedded_datasources=embedded_ds,
848
+ )
834
849
  else:
835
850
  if verbose:
836
851
  click.echo(FeedbackManager.info_processing_file(filename=filename))
@@ -838,12 +853,15 @@ async def get_processed(
838
853
  if ".incl" in filename:
839
854
  click.echo(FeedbackManager.warning_skipping_include_file(file=filename))
840
855
 
856
+ if tb_client is None:
857
+ raise ValueError("tb_client cannot be None")
858
+
841
859
  name, warnings = await process(
842
860
  filename=filename,
843
861
  tb_client=tb_client,
844
- deps=deps,
845
- dep_map=dep_map,
846
- to_run=to_run,
862
+ deps=deps_list,
863
+ dep_map=dep_map_dict,
864
+ to_run=to_run_dict,
847
865
  vendor_paths=vendor_paths,
848
866
  skip_connectors=skip_connectors,
849
867
  current_ws=current_ws,
@@ -852,9 +870,9 @@ async def get_processed(
852
870
  is_internal=is_internal,
853
871
  dir_path=dir_path,
854
872
  verbose=verbose,
855
- embedded_datasources=embedded_datasources,
873
+ embedded_datasources=embedded_ds,
856
874
  )
857
- processed.add(name)
875
+ processed_set.add(name)
858
876
 
859
877
  if verbose:
860
878
  if len(warnings) == 1:
@@ -890,7 +908,7 @@ async def build_graph(
890
908
  to_run: Dict[str, Any] = {}
891
909
  deps: List[str] = []
892
910
  dep_map: Dict[str, Any] = {}
893
- embedded_datasources = {}
911
+ embedded_datasources: Dict[str, Dict[str, Any]] = {}
894
912
 
895
913
  # These dictionaries are used to store all the resources and there dependencies for the whole project
896
914
  # This is used for the downstream dependency graph
@@ -919,17 +937,18 @@ async def build_graph(
919
937
  all_dep_map = all_dependencies_graph.dep_map
920
938
  all_resources = all_dependencies_graph.to_run
921
939
 
922
- processed = set()
940
+ processed: Set[str] = set()
923
941
 
924
942
  await get_processed(
925
943
  filenames=filenames,
926
- tb_client=tb_client,
927
944
  changed=changed,
945
+ verbose=verbose,
928
946
  deps=deps,
929
947
  dep_map=dep_map,
930
948
  to_run=to_run,
931
949
  vendor_paths=vendor_paths,
932
950
  processed=processed,
951
+ tb_client=tb_client,
933
952
  skip_connectors=skip_connectors,
934
953
  current_ws=current_ws,
935
954
  fork_downstream=fork_downstream,
@@ -1198,18 +1217,22 @@ async def process_file(
1198
1217
  raise Exception(f"Invalid import schedule: '{cron}'. Valid values are: {valid_values}")
1199
1218
 
1200
1219
  if cron == ON_DEMAND_CRON:
1220
+ if import_params is None:
1221
+ import_params = {}
1201
1222
  import_params["import_schedule"] = ON_DEMAND_CRON_EXPECTED_BY_THE_API
1223
+
1202
1224
  if cron == AUTO_CRON:
1203
1225
  period: int = DEFAULT_CRON_PERIOD
1204
1226
 
1205
- if current_ws:
1227
+ if current_ws is not None:
1206
1228
  workspaces = (await tb_client.user_workspaces()).get("workspaces", [])
1207
1229
  workspace_rate_limits: Dict[str, Dict[str, int]] = next(
1208
1230
  (w.get("rate_limits", {}) for w in workspaces if w["id"] == current_ws["id"]), {}
1209
1231
  )
1210
- period = workspace_rate_limits.get("api_datasources_create_append_replace", {}).get(
1211
- "period", DEFAULT_CRON_PERIOD
1212
- )
1232
+ if workspace_rate_limits:
1233
+ rate_limit_config = workspace_rate_limits.get("api_datasources_create_append_replace", {})
1234
+ if rate_limit_config:
1235
+ period = rate_limit_config.get("period", DEFAULT_CRON_PERIOD)
1213
1236
 
1214
1237
  def seconds_to_cron_expression(seconds: int) -> str:
1215
1238
  minutes = seconds // 60
@@ -1223,10 +1246,13 @@ async def process_file(
1223
1246
  return f"*/{minutes} * * * *"
1224
1247
  return f"*/{seconds} * * * *"
1225
1248
 
1249
+ if import_params is None:
1250
+ import_params = {}
1226
1251
  import_params["import_schedule"] = seconds_to_cron_expression(period)
1227
1252
 
1228
- # Include all import_ parameters in the datasource params
1229
- params.update(import_params)
1253
+ # Include all import_ parameters in the datasource params
1254
+ if import_params is not None:
1255
+ params.update(import_params)
1230
1256
 
1231
1257
  # Substitute the import parameters with the ones used by the
1232
1258
  # import API:
@@ -124,10 +124,95 @@ def deployment_group() -> None:
124
124
  help="Auto-promote the deployment. Only works if --wait is enabled. Disabled by default.",
125
125
  )
126
126
  @click.pass_context
127
- def create(ctx: click.Context, wait: bool, auto: bool) -> None:
127
+ def deployment_create(ctx: click.Context, wait: bool, auto: bool) -> None:
128
128
  """
129
129
  Validate and deploy the project server side.
130
130
  """
131
+ create_deployment(ctx, wait, auto)
132
+
133
+
134
+ @deployment_group.command(name="ls")
135
+ @click.pass_context
136
+ def deployment_ls(ctx: click.Context) -> None:
137
+ """
138
+ List all the deployments you have in the project.
139
+ """
140
+ client = ctx.ensure_object(dict)["client"]
141
+
142
+ TINYBIRD_API_KEY = client.token
143
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
144
+ TINYBIRD_API_URL = f"{client.host}/v1/deployments"
145
+
146
+ r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
147
+ result = r.json()
148
+ logging.debug(json.dumps(result, indent=2))
149
+
150
+ status_map = {"data_ready": "Ready", "failed": "Failed"}
151
+ columns = ["ID", "Status", "Created at", "Live"]
152
+ table = []
153
+ for deployment in result.get("deployments"):
154
+ table.append(
155
+ [
156
+ deployment.get("id"),
157
+ status_map.get(deployment.get("status"), "In progress"),
158
+ datetime.fromisoformat(deployment.get("created_at")).strftime("%Y-%m-%d %H:%M:%S"),
159
+ deployment.get("live"),
160
+ ]
161
+ )
162
+
163
+ echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
164
+
165
+
166
+ @deployment_group.command(name="promote")
167
+ @click.pass_context
168
+ def deployment_promote(ctx: click.Context) -> None:
169
+ """
170
+ Promote last deploy to ready and remove old one.
171
+ """
172
+ client = ctx.ensure_object(dict)["client"]
173
+
174
+ TINYBIRD_API_KEY = client.token
175
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
176
+
177
+ promote_deployment(client.host, HEADERS)
178
+
179
+
180
+ @deployment_group.command(name="rollback")
181
+ @click.pass_context
182
+ def deployment_rollback(ctx: click.Context) -> None:
183
+ """
184
+ Rollback to the previous deployment.
185
+ """
186
+ client = ctx.ensure_object(dict)["client"]
187
+
188
+ TINYBIRD_API_KEY = client.token
189
+ HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
190
+
191
+ rollback_deployment(client.host, HEADERS)
192
+
193
+
194
+ @cli.command(name="deploy")
195
+ @click.option(
196
+ "--wait/--no-wait",
197
+ is_flag=True,
198
+ default=False,
199
+ help="Wait for deploy to finish. Disabled by default.",
200
+ )
201
+ @click.option(
202
+ "--auto/--no-auto",
203
+ is_flag=True,
204
+ default=False,
205
+ help="Auto-promote the deployment. Only works if --wait is enabled. Disabled by default.",
206
+ )
207
+ @click.pass_context
208
+ def deploy(ctx: click.Context, wait: bool, auto: bool) -> None:
209
+ """
210
+ Deploy the project.
211
+ """
212
+ create_deployment(ctx, wait, auto)
213
+
214
+
215
+ def create_deployment(ctx: click.Context, wait: bool, auto: bool) -> None:
131
216
  # TODO: This code is duplicated in build_server.py
132
217
  # Should be refactored to be shared
133
218
  MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
@@ -203,63 +288,3 @@ def create(ctx: click.Context, wait: bool, auto: bool) -> None:
203
288
 
204
289
  if auto:
205
290
  promote_deployment(client.host, HEADERS)
206
-
207
-
208
- @deployment_group.command(name="ls")
209
- @click.pass_context
210
- def deployment_ls(ctx: click.Context) -> None:
211
- """
212
- List all the deployments you have in the project.
213
- """
214
- client = ctx.ensure_object(dict)["client"]
215
-
216
- TINYBIRD_API_KEY = client.token
217
- HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
218
- TINYBIRD_API_URL = f"{client.host}/v1/deployments"
219
-
220
- r = requests.get(TINYBIRD_API_URL, headers=HEADERS)
221
- result = r.json()
222
- logging.debug(json.dumps(result, indent=2))
223
-
224
- status_map = {"data_ready": "Ready", "failed": "Failed"}
225
- columns = ["ID", "Status", "Created at", "Live"]
226
- table = []
227
- for deployment in result.get("deployments"):
228
- table.append(
229
- [
230
- deployment.get("id"),
231
- status_map.get(deployment.get("status"), "In progress"),
232
- datetime.fromisoformat(deployment.get("created_at")).strftime("%Y-%m-%d %H:%M:%S"),
233
- deployment.get("live"),
234
- ]
235
- )
236
-
237
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
238
-
239
-
240
- @deployment_group.command(name="promote")
241
- @click.pass_context
242
- def deployment_promote(ctx: click.Context) -> None:
243
- """
244
- Promote last deploy to ready and remove old one.
245
- """
246
- client = ctx.ensure_object(dict)["client"]
247
-
248
- TINYBIRD_API_KEY = client.token
249
- HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
250
-
251
- promote_deployment(client.host, HEADERS)
252
-
253
-
254
- @deployment_group.command(name="rollback")
255
- @click.pass_context
256
- def deployment_rollback(ctx: click.Context) -> None:
257
- """
258
- Rollback to the previous deployment.
259
- """
260
- client = ctx.ensure_object(dict)["client"]
261
-
262
- TINYBIRD_API_KEY = client.token
263
- HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
264
-
265
- rollback_deployment(client.host, HEADERS)
@@ -161,7 +161,7 @@ async def endpoint_url(ctx: Context, pipe: str):
161
161
  click.echo(build_endpoint_url(client, pipe, token))
162
162
 
163
163
 
164
- def build_endpoint_url(tb_client: TinyB, pipe_name: str, token: Optional[str]) -> str:
164
+ def build_endpoint_url(tb_client: TinyB, pipe_name: str, token: Optional[str]) -> Optional[str]:
165
165
  try:
166
166
  token = token or tb_client.token
167
167
  example_params = {
@@ -36,7 +36,7 @@ def generate(llm_call, task: str, feedback: str = "") -> tuple[str, str]:
36
36
  thoughts = extract_xml(response, "thoughts")
37
37
  result = extract_xml(response, "response")
38
38
 
39
- return thoughts, result, response
39
+ return thoughts, result
40
40
 
41
41
 
42
42
  def evaluate(llm_call, content: str, task: str) -> tuple[str, str]:
@@ -96,7 +96,7 @@ Output your evaluation concisely in the following format:
96
96
  """
97
97
 
98
98
  generator_prompt = """
99
- Your goal is to complete the task based on <task> tag. If there are feedback
99
+ Your goal is to complete the task based on <task> tag. If there are feedback
100
100
  from your previous generations, you should reflect on them to solve the task.
101
101
  All xml tags MUST be closed.
102
102
 
@@ -326,96 +326,6 @@ async def print_pipe(ctx: Context, pipe: str, query: str, format_: str):
326
326
  click.echo(res)
327
327
 
328
328
 
329
- @pipe_copy.command(name="run", short_help="Run an on-demand copy job")
330
- @click.argument("pipe_name_or_id")
331
- @click.option("--wait", is_flag=True, default=False, help="Wait for the copy job to finish")
332
- @click.option(
333
- "--mode", type=click.Choice(["append", "replace"], case_sensitive=True), default=None, help="Copy strategy"
334
- )
335
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
336
- @click.option(
337
- "--param",
338
- nargs=1,
339
- type=str,
340
- multiple=True,
341
- default=None,
342
- help="Key and value of the params you want the Copy pipe to be called with. For example: tb pipe copy run <my_copy_pipe> --param foo=bar",
343
- )
344
- @click.pass_context
345
- @coro
346
- async def pipe_copy_run(
347
- ctx: click.Context, pipe_name_or_id: str, wait: bool, mode: str, yes: bool, param: Optional[Tuple[str]]
348
- ):
349
- """Run an on-demand copy job"""
350
-
351
- params = dict(key_value.split("=") for key_value in param) if param else {}
352
-
353
- if yes or click.confirm(FeedbackManager.warning_confirm_copy_pipe(pipe=pipe_name_or_id)):
354
- click.echo(FeedbackManager.info_copy_job_running(pipe=pipe_name_or_id))
355
- client: TinyB = ctx.ensure_object(dict)["client"]
356
-
357
- try:
358
- response = await client.pipe_run_copy(pipe_name_or_id, params, mode)
359
-
360
- job_id = response["job"]["job_id"]
361
- job_url = response["job"]["job_url"]
362
- target_datasource_id = response["tags"]["copy_target_datasource"]
363
- target_datasource = await client.get_datasource(target_datasource_id)
364
- target_datasource_name = target_datasource["name"]
365
- click.echo(
366
- FeedbackManager.success_copy_job_created(target_datasource=target_datasource_name, job_url=job_url)
367
- )
368
-
369
- if wait:
370
- await wait_job(client, job_id, job_url, "** Copying data")
371
- click.echo(FeedbackManager.success_data_copied_to_ds(target_datasource=target_datasource_name))
372
-
373
- except AuthNoTokenException:
374
- raise
375
- except Exception as e:
376
- raise CLIPipeException(FeedbackManager.error_creating_copy_job(error=e))
377
-
378
-
379
- @pipe_copy.command(name="resume", short_help="Resume a paused copy pipe")
380
- @click.argument("pipe_name_or_id")
381
- @click.pass_context
382
- @coro
383
- async def pipe_copy_resume(ctx: click.Context, pipe_name_or_id: str):
384
- """Resume a paused copy pipe"""
385
-
386
- click.echo(FeedbackManager.info_copy_pipe_resuming(pipe=pipe_name_or_id))
387
- client: TinyB = ctx.ensure_object(dict)["client"]
388
-
389
- try:
390
- await client.pipe_resume_copy(pipe_name_or_id)
391
- click.echo(FeedbackManager.success_copy_pipe_resumed(pipe=pipe_name_or_id))
392
-
393
- except AuthNoTokenException:
394
- raise
395
- except Exception as e:
396
- raise CLIPipeException(FeedbackManager.error_resuming_copy_pipe(error=e))
397
-
398
-
399
- @pipe_copy.command(name="pause", short_help="Pause a running copy pipe")
400
- @click.argument("pipe_name_or_id")
401
- @click.pass_context
402
- @coro
403
- async def pipe_copy_pause(ctx: click.Context, pipe_name_or_id: str):
404
- """Pause a running copy pipe"""
405
-
406
- click.echo(FeedbackManager.info_copy_pipe_pausing(pipe=pipe_name_or_id))
407
- client: TinyB = ctx.ensure_object(dict)["client"]
408
-
409
- try:
410
- await client.pipe_pause_copy(pipe_name_or_id)
411
- click.echo(FeedbackManager.success_copy_pipe_paused(pipe=pipe_name_or_id))
412
-
413
- except AuthNoTokenException:
414
- raise
415
- except Exception as e:
416
- raise CLIPipeException(FeedbackManager.error_pausing_copy_pipe(error=e))
417
-
418
-
419
329
  @pipe_sink.command(name="run", short_help="Run an on-demand sink job")
420
330
  @click.argument("pipe_name_or_id")
421
331
  @click.option("--wait", is_flag=True, default=False, help="Wait for the sink job to finish")
@@ -1,9 +1,12 @@
1
1
  import glob
2
2
  import os
3
3
  from pathlib import Path
4
- from typing import List, Optional
4
+ from typing import Dict, List, Optional
5
5
 
6
6
  from tinybird.tb.modules.config import CLIConfig
7
+ from tinybird.tb.modules.datafile.common import Datafile
8
+ from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
9
+ from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
7
10
 
8
11
 
9
12
  class Project:
@@ -42,3 +45,24 @@ class Project:
42
45
  @property
43
46
  def pipes(self) -> List[str]:
44
47
  return [Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)]
48
+
49
+ def get_pipe_datafile(self, filename: str) -> Datafile:
50
+ return parse_pipe(filename)
51
+
52
+ def get_datasource_datafile(self, filename: str) -> Datafile:
53
+ return parse_datasource(filename)
54
+
55
+ def get_datafile(self, filename: str) -> Optional[Datafile]:
56
+ if filename.endswith(".pipe"):
57
+ return self.get_pipe_datafile(filename)
58
+ elif filename.endswith(".datasource"):
59
+ return self.get_datasource_datafile(filename)
60
+ return None
61
+
62
+ def get_project_datafiles(self) -> Dict[str, Datafile]:
63
+ project_filenames = self.get_project_files()
64
+ datafiles: Dict[str, Datafile] = {}
65
+ for filename in project_filenames:
66
+ if datafile := self.get_datafile(filename):
67
+ datafiles[filename] = datafile
68
+ return datafiles
@@ -14,9 +14,9 @@ from prompt_toolkit.key_binding import KeyBindings
14
14
  from prompt_toolkit.shortcuts import CompleteStyle
15
15
  from prompt_toolkit.styles import Style
16
16
 
17
+ from tinybird.client import TinyB
17
18
  from tinybird.tb.modules.exceptions import CLIException
18
19
  from tinybird.tb.modules.feedback_manager import FeedbackManager, bcolors
19
- from tinybird.tb.modules.local_common import get_tinybird_local_client
20
20
  from tinybird.tb.modules.project import Project
21
21
  from tinybird.tb.modules.table import format_table
22
22
 
@@ -174,9 +174,10 @@ def _(event):
174
174
 
175
175
 
176
176
  class Shell:
177
- def __init__(self, project: Project):
177
+ def __init__(self, project: Project, tb_client: TinyB):
178
178
  self.history = self.get_history()
179
179
  self.project = project
180
+ self.tb_client = tb_client
180
181
  self.prompt_message = "\ntb > "
181
182
  self.commands = ["create", "mock", "test", "tb", "select"]
182
183
  self.session: PromptSession = PromptSession(
@@ -285,9 +286,8 @@ class Shell:
285
286
  loop = asyncio.new_event_loop()
286
287
  asyncio.set_event_loop(loop)
287
288
  try:
288
- tb_client = asyncio.run(get_tinybird_local_client(str(self.project.path)))
289
289
  return loop.run_until_complete(
290
- tb_client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
290
+ self.tb_client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
291
291
  )
292
292
  finally:
293
293
  loop.close()
@@ -15,6 +15,7 @@ from watchdog.events import (
15
15
  )
16
16
  from watchdog.observers import Observer
17
17
 
18
+ from tinybird.tb.modules.datafile.common import Datafile, DatafileKind
18
19
  from tinybird.tb.modules.feedback_manager import FeedbackManager
19
20
  from tinybird.tb.modules.project import Project
20
21
  from tinybird.tb.modules.shell import Shell
@@ -108,7 +109,7 @@ def watch_files(
108
109
  event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)), build_ok)
109
110
  observer = Observer()
110
111
 
111
- observer.schedule(event_handler, path=project.path, recursive=True)
112
+ observer.schedule(event_handler, path=str(project.path), recursive=True)
112
113
 
113
114
  observer.start()
114
115
 
@@ -122,10 +123,16 @@ def watch_files(
122
123
 
123
124
 
124
125
  class WatchProjectHandler(PatternMatchingEventHandler):
125
- def __init__(self, shell: Shell, project: Project, process: Callable[[Optional[str]], None]):
126
+ def __init__(
127
+ self,
128
+ shell: Shell,
129
+ project: Project,
130
+ process: Callable[[Optional[str], Optional[str]], None],
131
+ ):
126
132
  self.shell = shell
127
133
  self.project = project
128
134
  self.process = process
135
+ self.datafiles = project.get_project_datafiles()
129
136
  super().__init__(
130
137
  patterns=[
131
138
  f"{project.path}/**/*.datasource",
@@ -150,7 +157,7 @@ class WatchProjectHandler(PatternMatchingEventHandler):
150
157
  def _process(self, path: Optional[str] = None) -> None:
151
158
  click.echo(FeedbackManager.highlight(message="» Rebuilding project..."))
152
159
  time_start = time.time()
153
- self.process(path)
160
+ self.process(path, self.diff(path))
154
161
  time_end = time.time()
155
162
  elapsed_time = time_end - time_start
156
163
  click.echo(
@@ -159,6 +166,48 @@ class WatchProjectHandler(PatternMatchingEventHandler):
159
166
  )
160
167
  self.shell.reprint_prompt()
161
168
 
169
+ def diff(self, path: Optional[str] = None) -> Optional[str]:
170
+ if not path:
171
+ return None
172
+
173
+ current_datafile = self.datafiles.get(path, None)
174
+ new_datafile = self.project.get_datafile(path)
175
+ table_name = None
176
+ if current_datafile and new_datafile:
177
+ if current_datafile.kind == DatafileKind.datasource:
178
+ table_name = self.datasource_diff(current_datafile, new_datafile)
179
+ elif current_datafile.kind == DatafileKind.pipe:
180
+ table_name = self.pipe_diff(current_datafile, new_datafile)
181
+
182
+ self.refresh_datafiles()
183
+ return table_name
184
+
185
+ def refresh_datafiles(self) -> None:
186
+ self.datafiles = self.project.get_project_datafiles()
187
+
188
+ def datasource_diff(self, current_datafile: Datafile, new_datafile: Datafile) -> Optional[str]:
189
+ current_schema = current_datafile.nodes[0].get("schema")
190
+ new_schema = new_datafile.nodes[0].get("schema")
191
+ if current_schema != new_schema:
192
+ return current_datafile.nodes[0].get("name")
193
+ return None
194
+
195
+ def pipe_diff(self, current_datafile: Datafile, new_datafile: Datafile) -> Optional[str]:
196
+ current_nodes = current_datafile.nodes
197
+ current_sql_dict = {node.get("name"): node.get("sql") for node in current_nodes}
198
+ new_nodes = new_datafile.nodes
199
+ new_sql_dict = {node.get("name"): node.get("sql") for node in new_nodes}
200
+ for node in new_sql_dict.keys():
201
+ if node and node not in current_sql_dict:
202
+ return node
203
+
204
+ for node_name, sql in new_sql_dict.items():
205
+ current_sql = current_sql_dict.get(node_name)
206
+ if current_sql and current_sql != sql:
207
+ return node_name
208
+
209
+ return None
210
+
162
211
  def on_modified(self, event: Any) -> None:
163
212
  if path := self.should_process(event):
164
213
  filename = Path(path).name
@@ -176,12 +225,12 @@ class WatchProjectHandler(PatternMatchingEventHandler):
176
225
 
177
226
  def watch_project(
178
227
  shell: Shell,
179
- process: Callable[[Optional[str]], None],
228
+ process: Callable[[Optional[str], Optional[str]], None],
180
229
  project: Project,
181
230
  ) -> None:
182
231
  event_handler = WatchProjectHandler(shell=shell, project=project, process=process)
183
232
  observer = Observer()
184
- observer.schedule(event_handler, path=project.path, recursive=True)
233
+ observer.schedule(event_handler, path=str(project.path), recursive=True)
185
234
  observer.start()
186
235
 
187
236
  try:
@@ -1590,7 +1590,7 @@ async def try_update_config_with_remote(
1590
1590
  def ask_for_admin_token_interactively(ui_host: str, default_token: Optional[str]) -> str:
1591
1591
  return (
1592
1592
  click.prompt(
1593
- f"\nCopy the \"admin your@email\" token from {ui_host}/tokens and paste it here { 'OR press enter to use the token from .tinyb file' if default_token else ''}",
1593
+ f'\nCopy the "admin your@email" token from {ui_host}/tokens and paste it here {"OR press enter to use the token from .tinyb file" if default_token else ""}',
1594
1594
  hide_input=True,
1595
1595
  show_default=False,
1596
1596
  default=default_token,
@@ -370,7 +370,7 @@ class Template:
370
370
  for chunk in self.file.body.chunks:
371
371
  if isinstance(chunk, _ExtendsBlock):
372
372
  if not loader:
373
- raise ParseError("{% extends %} block found, but no " "template loader")
373
+ raise ParseError("{% extends %} block found, but no template loader")
374
374
  template = loader.load(chunk.name, self.name)
375
375
  ancestors.extend(template._get_ancestors(loader))
376
376
  return ancestors
@@ -633,7 +633,7 @@ class _Expression(_Node):
633
633
 
634
634
  def generate(self, writer):
635
635
  writer.write_line("_tt_tmp = %s" % self.expression, self.line)
636
- writer.write_line("if isinstance(_tt_tmp, _tt_string_types):" " _tt_tmp = _tt_utf8(_tt_tmp)", self.line)
636
+ writer.write_line("if isinstance(_tt_tmp, _tt_string_types): _tt_tmp = _tt_utf8(_tt_tmp)", self.line)
637
637
  writer.write_line("else: _tt_tmp = _tt_utf8(str(_tt_tmp))", self.line)
638
638
  if not self.raw and writer.current_template.autoescape is not None:
639
639
  # In python3 functions like xhtml_escape return unicode,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev41
3
+ Version: 0.0.1.dev43
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -1,58 +1,58 @@
1
1
  tinybird/__cli__.py,sha256=esPl5QDTzuQgHe5FuxWLm-fURFigGGwjnYLh9GuWUw4,232
2
- tinybird/client.py,sha256=oqe6mQbH_EC18o5jbiI12QEuAdTZ9yHCSHT-aQ6Pb-0,51517
2
+ tinybird/client.py,sha256=p3tpPC0QbObihnwGdmw1bRjKmQS2kOebJ9Me3ioM-Ww,51517
3
3
  tinybird/config.py,sha256=ENRNyEMXHj_P882o31iFz0hTveziLabVRrxiWE5RRBE,6233
4
- tinybird/connectors.py,sha256=lkpVSUmSuViEZBa4QjTK7YmPHUop0a5UFoTrSmlVq6k,15244
4
+ tinybird/connectors.py,sha256=7Gjms7b5MAaBFGi3xytsJurCylprONpFcYrzp4Fw2Rc,15241
5
5
  tinybird/context.py,sha256=A3GBApac9xO6hrAMJ1s9dMrI_ou9aKF84CdEjtPddMk,1417
6
6
  tinybird/datatypes.py,sha256=XNypumfqNjsvLJ5iNXnbVHRvAJe0aQwI3lS6Cxox-e0,10979
7
- tinybird/feedback_manager.py,sha256=ON5Zu-G3-QDVfH2i_P-V4EtyhlNtAzyp1YDZsnce0_U,67826
7
+ tinybird/feedback_manager.py,sha256=g1r9NcFfKXdk_13soaiTZLvdoUGleVfawl6Yfj3zmRw,67823
8
8
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
9
- tinybird/prompts.py,sha256=2UdLOCjIWbLINI1TuUSDXOW2T0YaPD0RHZAa26j6VlE,25247
10
- tinybird/sql.py,sha256=eulpRe05ZFrKFrxYawgxDxxrktFE8uL6hSL1gHIWKyg,46166
9
+ tinybird/prompts.py,sha256=GEf-14SZZIAYcNUIbwnmmCrhylrRy6EuDyGo8-_Z6jo,25672
10
+ tinybird/sql.py,sha256=igHaRIeEREN5XYowwpIGYG8gDB5kn5p2cDNL1t8uc40,46168
11
11
  tinybird/sql_template.py,sha256=GmMLAI10MTqjQo9qztuQHLRWs67teozsWDxUBdvkAn4,93668
12
- tinybird/sql_template_fmt.py,sha256=1z-PuqSZXtzso8Z_mPqUc-NxIxUrNUcVIPezNieZk-M,10196
12
+ tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
13
13
  tinybird/sql_toolset.py,sha256=NEUj8Ro5x9XlfVLlGr6nWt9o7OLWVxlqs6TIpgumUNs,14678
14
14
  tinybird/syncasync.py,sha256=IPnOx6lMbf9SNddN1eBtssg8vCLHMt76SuZ6YNYm-Yk,27761
15
- tinybird/tornado_template.py,sha256=oflXyoL2LSCegvl6bAzqw2JIqRaN5WPjhYYDtQcfuOE,41869
15
+ tinybird/tornado_template.py,sha256=FL85SMPq2dH4JqKovmSbaolGdEzwOO91NqOzqXo2Qr0,41863
16
16
  tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
17
17
  tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,40454
18
- tinybird/tb/__cli__.py,sha256=Py43Okm6MRi1REBHdZBS-rEI97DI49FulIJwEZXDzhs,251
18
+ tinybird/tb/__cli__.py,sha256=ezvYUt2sERQfnRCQPlsLhgBYbA8MgSBu2-Y-q-WNVug,251
19
19
  tinybird/tb/cli.py,sha256=_kYDnDS3a45MMKJFZnYZx1gLuVqs4N_Rt8GO4sueAeg,957
20
20
  tinybird/tb/modules/auth.py,sha256=EzRWFmwRkXNhUmRaruEVFLdkbUg8xMSix0cAWl5D4Jg,9029
21
- tinybird/tb/modules/build.py,sha256=N1I_MWPChBdfrFMcHdIbsTbMrGSNDBPBmhv2xSLsoWw,7500
22
- tinybird/tb/modules/build_client.py,sha256=na4MH0D4_yXMkNEW2a9bslW6t1fBRnr8JTN68MfRvvw,7229
23
- tinybird/tb/modules/cicd.py,sha256=SjCyvvy0WUnsjFs2biwwXvcf0Ddpmghhd8-SnMyfsRM,5355
24
- tinybird/tb/modules/cli.py,sha256=hD_mUAXtUofU20UC9j8Pzc4QI1gV0Y9Zefwe24T-qZA,19427
21
+ tinybird/tb/modules/build.py,sha256=mdry4XN8rIOomgOXBJmJwF53y2xzY8sej58PQEdSzz0,7647
22
+ tinybird/tb/modules/build_client.py,sha256=3gKxFFNjgUDDPiBpGeLhmm6RB2TNj6e3Z8yUuEg3DEc,7232
23
+ tinybird/tb/modules/cicd.py,sha256=xxXwy-QekJcG14kkJeGNl7LkHduhZXfvBZE8WrU6-t4,5351
24
+ tinybird/tb/modules/cli.py,sha256=8pyz442S-s_Xdr-jPCfVeBwQ4RJC09B4BDKaVhYfpR4,19442
25
25
  tinybird/tb/modules/common.py,sha256=e4U7AT0dUBG6O-7Iq2CVN1UHPd6-ZCFucyW0L5gBi4g,70592
26
26
  tinybird/tb/modules/config.py,sha256=mie3oMVTf5YOUFEiLs88P16U4LkJafJjSpjwyAkFHog,10979
27
- tinybird/tb/modules/copy.py,sha256=qrtU1PFXmu-xhq1LR2uo7-1MoCOOQeVb_j10vcasXmI,2415
27
+ tinybird/tb/modules/copy.py,sha256=QwEloYVSM4zIQ8iTRyLx355-GvZ-BEJ-kzSX81PcRXw,5927
28
28
  tinybird/tb/modules/create.py,sha256=iUYt5XG-GPwE3LnHrWqlOHmke0Bv8VMPxjAffxRFYoQ,11438
29
29
  tinybird/tb/modules/datasource.py,sha256=-VG2qKlu0fmkhsIB5bPiTp3XuktB_r-ZkIoohEBEXtI,13713
30
- tinybird/tb/modules/deployment.py,sha256=fCWHlfMrAbHLst_-pveH6oPHPnDsXqv1bAkzoFtxvEM,9597
31
- tinybird/tb/modules/endpoint.py,sha256=iYSWzi3_VJzHcq1_j_Hv4cfG1GFKXKxqEY4jLjKhxag,6488
30
+ tinybird/tb/modules/deployment.py,sha256=TaFpS6aowGaZ-XjvMdof2sO7yJS2MCsZRqmB3v3_eSs,10228
31
+ tinybird/tb/modules/endpoint.py,sha256=tR0_NEZd0INJEPO6e4manWc6Qw5FT4IZFW6muxTCnrI,6498
32
32
  tinybird/tb/modules/exceptions.py,sha256=4A2sSjCEqKUMqpP3WI00zouCWW4uLaghXXLZBSw04mY,3363
33
33
  tinybird/tb/modules/feedback_manager.py,sha256=e8tqehRR0Buhs8O0n8N2Sg2vnnBVb1NLtnZqkPrYD_A,68379
34
34
  tinybird/tb/modules/fmt.py,sha256=poh6_cwVGSf-sBu6LKWuO2TANL_J8Sgm25sPpwxa3Aw,3558
35
35
  tinybird/tb/modules/job.py,sha256=956Pj8BEEsiD2GZsV9RKKVM3I_CveOLgS82lykO5ukk,2963
36
36
  tinybird/tb/modules/llm.py,sha256=AC0VSphTOM2t-v1_3NLvNN_FIbgMo4dTyMqIv5nniPo,835
37
- tinybird/tb/modules/llm_utils.py,sha256=zUwcF8hgBsxWVWgCzSRh--O7lWVZ_wzj7bxN8Yl5sXo,3480
37
+ tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
38
38
  tinybird/tb/modules/local.py,sha256=x4xuCGVkoa8KLYGZEJnFUP8HUkKX05Frp_djRVjVjTs,5669
39
39
  tinybird/tb/modules/local_common.py,sha256=afPW6bSc-YI7Q4ngvBV53sM7QxnPGBMe5N91iETM2yE,2783
40
40
  tinybird/tb/modules/login.py,sha256=0cS-f3MsQFHc6xjw8FRWJm4EJBH9C7Ri68EcO_tiwes,6508
41
41
  tinybird/tb/modules/mock.py,sha256=XoCaFFroJf2jWVxEztPelwXYNle__KilON1e81Mxkd4,3764
42
- tinybird/tb/modules/pipe.py,sha256=Kay7AZVf_M5biIvX5hi-Vaz4l9C7AV-s0C2Nle_gkJo,17528
43
- tinybird/tb/modules/project.py,sha256=EyrX5-5i3vhWk6v3KzbrZg8im4ey23hpzM7U651u6OE,1432
42
+ tinybird/tb/modules/pipe.py,sha256=fQAlH5V00rKl9kvBWTjgAC3scvNA00ogANGT5hozxkI,14051
43
+ tinybird/tb/modules/project.py,sha256=9bP6G4LUMkkeC_M47d89fKt2Bg_yEMPsO9u5eQW68uo,2450
44
44
  tinybird/tb/modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
45
- tinybird/tb/modules/shell.py,sha256=ycovF7PQLpzy27qYp_IsP6KO8LZMO5hnex3INMhtebM,13068
45
+ tinybird/tb/modules/shell.py,sha256=iCX9HsvNhVDecop-s4zDPjXfL9GQ56-QODqwHsN5oT8,12994
46
46
  tinybird/tb/modules/table.py,sha256=4XrtjM-N0zfNtxVkbvLDQQazno1EPXnxTyo7llivfXk,11035
47
47
  tinybird/tb/modules/tag.py,sha256=anPmMUBc-TbFovlpFi8GPkKA18y7Y0GczMsMms5TZsU,3502
48
48
  tinybird/tb/modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
49
49
  tinybird/tb/modules/test.py,sha256=zciS9klk2dNBiyncqtXgGTVao1B7p4S9hg_Oat_FJIY,11616
50
50
  tinybird/tb/modules/token.py,sha256=sPdJoBE-6dd3Sd6W-prst7VOoJ0NbvP0uTaB6dXHs5s,12711
51
51
  tinybird/tb/modules/update.py,sha256=RCOGhxJPhlwCzRzPPYFfbvpJeDKu3yEISWUJinr953M,6821
52
- tinybird/tb/modules/watch.py,sha256=TVfCbZbWLuUZSrdNsWH35E0G7PVVAHvZ7K-u_GMzm9A,6591
52
+ tinybird/tb/modules/watch.py,sha256=Kredt5C7OOiI6YOivuR5QBdiDY4J_xLiwqHOROnfcsU,8591
53
53
  tinybird/tb/modules/workspace.py,sha256=M0RtXCaw7RdZ3c_fqtmjbVb7HqlV742Drn4OiyZlp3M,6345
54
54
  tinybird/tb/modules/workspace_members.py,sha256=Ai6iCOzXX1zQ8q9iXIFSFHsBJlT-8Q28DaG5Ie-UweY,8726
55
- tinybird/tb/modules/datafile/build.py,sha256=pwgsIuvHwb2cdsl3IWOAPyj6S9vB3jn_BXGRcKT7I2Y,57577
55
+ tinybird/tb/modules/datafile/build.py,sha256=bq_As61bOqPF0NzeUq8sx3EnCCte4KDoDQZYW6n6UIw,59060
56
56
  tinybird/tb/modules/datafile/build_common.py,sha256=IXl-Z51zUi1dypV7meNenX0iu2UmowNeqgG6WHyMHlk,4562
57
57
  tinybird/tb/modules/datafile/build_datasource.py,sha256=4aP8_DYCRGghXntZSeWDNJxjps1QRVa7WHoYCzQwQts,17355
58
58
  tinybird/tb/modules/datafile/build_pipe.py,sha256=Jgv3YKIvMfjPiSIdw1k2mpaoDdAWMiMRaSHwRgyI97E,28258
@@ -70,13 +70,13 @@ tinybird/tb/modules/datafile/pull.py,sha256=vcjMUbjnZ9XQMGmL33J3ElpbXBTat8Yzp-ha
70
70
  tinybird/tb/modules/tinyunit/tinyunit.py,sha256=3EBqKzNCfyDuZiO4H61ihanFBRLFUGeuXf3nDXnYFcU,11727
71
71
  tinybird/tb/modules/tinyunit/tinyunit_lib.py,sha256=hGh1ZaXC1af7rKnX7222urkj0QJMhMWclqMy59dOqwE,1922
72
72
  tinybird/tb_cli_modules/cicd.py,sha256=0lMkb6CVOFZl5HOwgY8mK4T4mgI7O8335UngLXtCc-c,13851
73
- tinybird/tb_cli_modules/common.py,sha256=18LDc3au8K6NO-mN_5jtETMKHhGJOnZRfP0P7oKI2Eg,78836
73
+ tinybird/tb_cli_modules/common.py,sha256=SnC_PLCJHwaFaNg7LsUtc1s5jgiD_vkglbva0D3w8RA,78833
74
74
  tinybird/tb_cli_modules/config.py,sha256=6u6B5QCdiQLbJkCkwtnKGs9H3nP-KXXhC75mF7B-1DQ,11464
75
75
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
76
76
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
77
77
  tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
78
- tinybird-0.0.1.dev41.dist-info/METADATA,sha256=VrfRatTvL5BvgKELZsK-vEW6vu_4_5cbWnWf7Penawo,2482
79
- tinybird-0.0.1.dev41.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
80
- tinybird-0.0.1.dev41.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
81
- tinybird-0.0.1.dev41.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
82
- tinybird-0.0.1.dev41.dist-info/RECORD,,
78
+ tinybird-0.0.1.dev43.dist-info/METADATA,sha256=8vZ7c2enj_Q8TUAt2lEOvC4KgA3VZ3Ony-v89LGotmc,2482
79
+ tinybird-0.0.1.dev43.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
80
+ tinybird-0.0.1.dev43.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
81
+ tinybird-0.0.1.dev43.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
82
+ tinybird-0.0.1.dev43.dist-info/RECORD,,