tinybird 0.0.1.dev291__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. tinybird/ch_utils/constants.py +5 -0
  2. tinybird/connectors.py +1 -7
  3. tinybird/context.py +3 -3
  4. tinybird/datafile/common.py +10 -8
  5. tinybird/datafile/parse_pipe.py +2 -2
  6. tinybird/feedback_manager.py +3 -0
  7. tinybird/prompts.py +1 -0
  8. tinybird/service_datasources.py +223 -0
  9. tinybird/sql_template.py +26 -11
  10. tinybird/sql_template_fmt.py +14 -4
  11. tinybird/tb/__cli__.py +2 -2
  12. tinybird/tb/cli.py +1 -0
  13. tinybird/tb/client.py +104 -26
  14. tinybird/tb/config.py +24 -0
  15. tinybird/tb/modules/agent/agent.py +103 -67
  16. tinybird/tb/modules/agent/banner.py +15 -15
  17. tinybird/tb/modules/agent/explore_agent.py +5 -0
  18. tinybird/tb/modules/agent/mock_agent.py +5 -1
  19. tinybird/tb/modules/agent/models.py +6 -2
  20. tinybird/tb/modules/agent/prompts.py +49 -2
  21. tinybird/tb/modules/agent/tools/deploy.py +1 -1
  22. tinybird/tb/modules/agent/tools/execute_query.py +15 -18
  23. tinybird/tb/modules/agent/tools/request_endpoint.py +1 -1
  24. tinybird/tb/modules/agent/tools/run_command.py +9 -0
  25. tinybird/tb/modules/agent/utils.py +38 -48
  26. tinybird/tb/modules/branch.py +150 -0
  27. tinybird/tb/modules/build.py +58 -13
  28. tinybird/tb/modules/build_common.py +209 -25
  29. tinybird/tb/modules/cli.py +129 -16
  30. tinybird/tb/modules/common.py +172 -146
  31. tinybird/tb/modules/connection.py +125 -194
  32. tinybird/tb/modules/connection_kafka.py +382 -0
  33. tinybird/tb/modules/copy.py +3 -1
  34. tinybird/tb/modules/create.py +83 -150
  35. tinybird/tb/modules/datafile/build.py +27 -38
  36. tinybird/tb/modules/datafile/build_datasource.py +21 -25
  37. tinybird/tb/modules/datafile/diff.py +1 -1
  38. tinybird/tb/modules/datafile/format_pipe.py +46 -7
  39. tinybird/tb/modules/datafile/playground.py +59 -68
  40. tinybird/tb/modules/datafile/pull.py +2 -3
  41. tinybird/tb/modules/datasource.py +477 -308
  42. tinybird/tb/modules/deployment.py +2 -0
  43. tinybird/tb/modules/deployment_common.py +84 -44
  44. tinybird/tb/modules/deprecations.py +4 -4
  45. tinybird/tb/modules/dev_server.py +33 -12
  46. tinybird/tb/modules/exceptions.py +14 -0
  47. tinybird/tb/modules/feedback_manager.py +1 -1
  48. tinybird/tb/modules/info.py +69 -12
  49. tinybird/tb/modules/infra.py +4 -5
  50. tinybird/tb/modules/job_common.py +15 -0
  51. tinybird/tb/modules/local.py +143 -23
  52. tinybird/tb/modules/local_common.py +347 -19
  53. tinybird/tb/modules/local_logs.py +209 -0
  54. tinybird/tb/modules/login.py +21 -2
  55. tinybird/tb/modules/login_common.py +254 -12
  56. tinybird/tb/modules/mock.py +5 -54
  57. tinybird/tb/modules/mock_common.py +0 -54
  58. tinybird/tb/modules/open.py +10 -5
  59. tinybird/tb/modules/project.py +14 -5
  60. tinybird/tb/modules/shell.py +15 -7
  61. tinybird/tb/modules/sink.py +3 -1
  62. tinybird/tb/modules/telemetry.py +11 -3
  63. tinybird/tb/modules/test.py +13 -9
  64. tinybird/tb/modules/test_common.py +13 -87
  65. tinybird/tb/modules/tinyunit/tinyunit.py +0 -14
  66. tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -6
  67. tinybird/tb/modules/watch.py +5 -3
  68. tinybird/tb_cli_modules/common.py +2 -2
  69. tinybird/tb_cli_modules/telemetry.py +1 -1
  70. tinybird/tornado_template.py +6 -7
  71. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/METADATA +32 -6
  72. tinybird-1.0.5.dist-info/RECORD +132 -0
  73. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/WHEEL +1 -1
  74. tinybird-0.0.1.dev291.dist-info/RECORD +0 -128
  75. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/entry_points.txt +0 -0
  76. {tinybird-0.0.1.dev291.dist-info → tinybird-1.0.5.dist-info}/top_level.txt +0 -0
@@ -3,25 +3,22 @@ import os
3
3
  import re
4
4
  import shutil
5
5
  from pathlib import Path
6
- from typing import Any, Dict, List, Optional
6
+ from typing import Any, Dict, List, Optional, Set
7
7
  from urllib.parse import urlparse
8
8
 
9
9
  import click
10
10
  import requests
11
11
 
12
- from tinybird.prompts import claude_rules_prompt, create_prompt, readme_prompt, rules_prompt
13
- from tinybird.tb.client import TinyB
12
+ from tinybird.prompts import claude_rules_prompt, rules_prompt
13
+ from tinybird.tb.modules.agent import run_agent
14
14
  from tinybird.tb.modules.cicd import init_cicd
15
15
  from tinybird.tb.modules.cli import cli
16
- from tinybird.tb.modules.common import _generate_datafile, generate_datafile
16
+ from tinybird.tb.modules.common import _generate_datafile
17
17
  from tinybird.tb.modules.config import CLIConfig
18
18
  from tinybird.tb.modules.datafile.fixture import persist_fixture
19
19
  from tinybird.tb.modules.exceptions import CLICreateException
20
20
  from tinybird.tb.modules.feedback_manager import FeedbackManager
21
- from tinybird.tb.modules.llm import LLM
22
- from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
23
21
  from tinybird.tb.modules.local_common import get_tinybird_local_client
24
- from tinybird.tb.modules.mock_common import create_mock_data
25
22
  from tinybird.tb.modules.project import Project
26
23
 
27
24
 
@@ -67,13 +64,9 @@ def create(
67
64
  folder_path.mkdir()
68
65
 
69
66
  try:
70
- tb_client = config.get_client(token=ctx_config.get("token"), host=ctx_config.get("host"))
71
- user_token: str = ""
72
67
  created_something = False
73
- if prompt:
74
- user_token = ctx_config.get("user_token")
75
- if not user_token:
76
- raise Exception("This action requires authentication. Run 'tb login' first.")
68
+ if prompt and not ctx_config.get("user_token"):
69
+ raise Exception("This action requires authentication. Run 'tb login' first.")
77
70
 
78
71
  if not validate_project_structure(project):
79
72
  click.echo(FeedbackManager.highlight(message="\n» Creating new project structure..."))
@@ -99,28 +92,20 @@ def create(
99
92
 
100
93
  prompt_result: List[Path] = []
101
94
  if prompt:
102
- prompt_result = create_resources_from_prompt(tb_client, user_token, prompt, project)
103
- result.extend(prompt_result)
104
- readme_path = folder_path / "README.md"
105
- if readme_path.exists():
106
- click.echo(FeedbackManager.highlight(message="\n» Updating project description..."))
107
- else:
108
- click.echo(FeedbackManager.highlight(message="\n» Creating project description..."))
109
- readme_path.touch()
110
- llm = LLM(user_token=str(user_token), host=tb_client.host)
111
- readme_user_prompt = prompt or ""
112
- all_resources_xml = get_resources_xml(project)
113
- readme_response = llm.ask(
114
- system_prompt=readme_prompt(
115
- readme_path.read_text(), tb_client.host, "$TB_ADMIN_TOKEN", all_resources_xml
116
- ),
117
- prompt=readme_user_prompt,
118
- feature="tb_create_readme",
95
+ prompt_instructions = (
96
+ "Create or update the Tinybird datasources, pipes, and connections required to satisfy the following request. "
97
+ "Do not generate mock data or append data; those steps will run later programmatically."
119
98
  )
120
- readme_result = extract_xml(readme_response, "readme")
121
- readme_path.write_text(readme_result)
122
- click.echo(FeedbackManager.info_file_created(file="README.md"))
123
- created_something = True
99
+ prompt_result = create_resources_from_prompt(
100
+ ctx_config,
101
+ project,
102
+ prompt,
103
+ feature="tb_create",
104
+ instructions=prompt_instructions,
105
+ )
106
+ result.extend(prompt_result)
107
+ if prompt_result:
108
+ created_something = True
124
109
 
125
110
  if data or prompt:
126
111
  click.echo(FeedbackManager.success(message="✓ Resources created!\n"))
@@ -180,24 +165,30 @@ def create(
180
165
  datasource_name = datasource_path.stem
181
166
  datasource_content = datasource_path.read_text()
182
167
  has_json_path = "`json:" in datasource_content
183
- if has_json_path:
184
- mock_data = create_mock_data(
185
- datasource_name,
186
- datasource_content,
187
- rows,
188
- prompt,
189
- config,
190
- ctx_config,
191
- user_token,
192
- tb_client,
193
- format_="ndjson",
194
- folder=project.folder,
195
- )
196
- if mock_data:
197
- persist_fixture(datasource_name, mock_data, folder, format="ndjson")
198
- click.echo(FeedbackManager.info_file_created(file=f"fixtures/{datasource_name}.ndjson"))
199
- click.echo(FeedbackManager.success(message="✓ Done!"))
200
- created_something = True
168
+ if not has_json_path:
169
+ continue
170
+
171
+ fixture_path = Path(folder) / "fixtures" / f"{datasource_name}.ndjson"
172
+ fixture_existed = fixture_path.exists()
173
+ fixture_prompt = (
174
+ f"Generate {rows} rows of representative sample data for the Tinybird datasource defined in {datasource_path}. "
175
+ f"Store the data in ndjson format at fixtures/{datasource_name}.ndjson."
176
+ )
177
+ if prompt.strip():
178
+ fixture_prompt += f"\n\nOriginal project request:\n{prompt.strip()}"
179
+
180
+ run_agent(
181
+ ctx_config,
182
+ project,
183
+ True,
184
+ prompt=fixture_prompt,
185
+ feature="tb_mock",
186
+ )
187
+
188
+ if fixture_path.exists() and not fixture_existed:
189
+ click.echo(FeedbackManager.info_file_created(file=f"fixtures/{datasource_name}.ndjson"))
190
+ click.echo(FeedbackManager.success(message="✓ Done!"))
191
+ created_something = True
201
192
 
202
193
  if not created_something and not len(result) > 0:
203
194
  click.echo(FeedbackManager.warning(message="△ No resources created\n"))
@@ -291,86 +282,39 @@ def create_project_structure(folder: str):
291
282
 
292
283
 
293
284
  def create_resources_from_prompt(
294
- tb_client: TinyB,
295
- user_token: str,
296
- prompt: str,
285
+ config: Dict[str, Any],
297
286
  project: Project,
298
- feature: str = "tb_create_resources",
287
+ prompt: str,
288
+ feature: str = "tb_create",
289
+ instructions: Optional[str] = None,
299
290
  ) -> List[Path]:
300
- result: List[Path] = []
301
- datasource_paths = [Path(ds_file) for ds_file in project.get_datasource_files()]
302
- pipes_paths = [Path(pipe_file) for pipe_file in project.get_pipe_files()]
303
- connections_paths = [Path(conn_file) for conn_file in project.get_connection_files()]
304
- resources_xml = "\n".join(
305
- [
306
- f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
307
- for resource_type, resource_name, resource_content in [
308
- ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
309
- ]
310
- + [
311
- (
312
- "pipe",
313
- pipe.stem,
314
- pipe.read_text(),
315
- )
316
- for pipe in pipes_paths
317
- ]
318
- + [
319
- (
320
- "connection",
321
- conn.stem,
322
- conn.read_text(),
323
- )
324
- for conn in connections_paths
325
- ]
326
- ]
327
- )
328
- llm = LLM(user_token=user_token, host=tb_client.host)
329
- prompt_result = llm.ask(system_prompt=create_prompt(resources_xml), prompt=prompt, feature=feature)
330
- prompt_result = extract_xml(prompt_result, "response")
331
- resources = parse_xml(prompt_result, "resource")
332
- datasources = []
333
- pipes = []
334
- connections = []
335
- for resource_xml in resources:
336
- resource_type = extract_xml(resource_xml, "type")
337
- name = extract_xml(resource_xml, "name")
338
- content = extract_xml(resource_xml, "content")
339
- resource = {
340
- "name": name,
341
- "content": content,
342
- }
343
- if resource_type.lower() == "datasource":
344
- datasources.append(resource)
345
- elif resource_type.lower() == "pipe":
346
- pipes.append(resource)
347
- elif resource_type.lower() == "connection":
348
- connections.append(resource)
349
-
350
- for ds in datasources:
351
- content = ds["content"].replace("```", "")
352
- filename = f"{ds['name']}.datasource"
353
- ds_file = generate_datafile(
354
- content,
355
- filename=filename,
356
- data=None,
357
- _format="ndjson",
358
- force=True,
359
- folder=project.folder,
360
- )
361
- result.append(ds_file)
362
- for pipe in pipes:
363
- content = pipe["content"].replace("```", "")
364
- pipe_file = generate_pipe_file(pipe["name"], content, project.folder)
365
- result.append(pipe_file)
291
+ """Run the agent in prompt mode and report newly created project resources."""
366
292
 
367
- for conn in connections:
368
- content = conn["content"].replace("```", "")
369
- filename = f"{conn['name']}.connection"
370
- conn_file = generate_connection_file(conn["name"], content, project.folder)
371
- result.append(conn_file)
293
+ agent_prompt = prompt.strip()
294
+ if instructions:
295
+ instructions = instructions.strip()
296
+ if agent_prompt:
297
+ agent_prompt = f"{instructions}\n\n{agent_prompt}"
298
+ else:
299
+ agent_prompt = instructions
300
+
301
+ if not agent_prompt:
302
+ return []
303
+
304
+ resources_before = _collect_project_resource_paths(project)
305
+ run_agent(config, project, True, prompt=agent_prompt, feature=feature)
306
+ resources_after = _collect_project_resource_paths(project)
307
+
308
+ created_resources = [Path(path) for path in sorted(resources_after - resources_before)]
309
+ return created_resources
372
310
 
373
- return result
311
+
312
+ def _collect_project_resource_paths(project: Project) -> Set[Path]:
313
+ resources: Set[Path] = set()
314
+ resources.update(Path(path) for path in project.get_datasource_files())
315
+ resources.update(Path(path) for path in project.get_pipe_files())
316
+ resources.update(Path(path) for path in project.get_connection_files())
317
+ return resources
374
318
 
375
319
 
376
320
  def init_git(folder: str):
@@ -513,28 +457,6 @@ def save_context(prompt: str, feedback: str):
513
457
  context_file.write_text(f"- {prompt}\n{feedback}")
514
458
 
515
459
 
516
- def get_resources_xml(project: Project) -> str:
517
- datasource_paths = [Path(f) for f in project.get_datasource_files()]
518
- pipes_paths = [Path(f) for f in project.get_pipe_files()]
519
- resources_xml = "\n".join(
520
- [
521
- f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
522
- for resource_type, resource_name, resource_content in [
523
- ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
524
- ]
525
- + [
526
- (
527
- "pipe",
528
- pipe.stem,
529
- pipe.read_text(),
530
- )
531
- for pipe in pipes_paths
532
- ]
533
- ]
534
- )
535
- return resources_xml
536
-
537
-
538
460
  def create_resources_from_data(
539
461
  data: str,
540
462
  project: Project,
@@ -602,6 +524,9 @@ def generate_kafka_connection_with_secrets(
602
524
  tb_secret_secret: Optional[str],
603
525
  security_protocol: str,
604
526
  sasl_mechanism: str,
527
+ ssl_ca_pem: Optional[str],
528
+ tb_secret_ssl_ca_pem: Optional[str],
529
+ schema_registry_url: Optional[str],
605
530
  folder: str,
606
531
  ) -> Path:
607
532
  kafka_bootstrap_servers = (
@@ -609,6 +534,7 @@ def generate_kafka_connection_with_secrets(
609
534
  )
610
535
  kafka_key = inject_tb_secret(tb_secret_key) if tb_secret_key else key
611
536
  kafka_secret = inject_tb_secret(tb_secret_secret) if tb_secret_secret else secret
537
+ kafka_ssl_ca_pem = inject_tb_secret(tb_secret_ssl_ca_pem) if tb_secret_ssl_ca_pem else ssl_ca_pem
612
538
  content = f"""TYPE kafka
613
539
  KAFKA_BOOTSTRAP_SERVERS {kafka_bootstrap_servers}
614
540
  KAFKA_SECURITY_PROTOCOL {security_protocol or "SASL_SSL"}
@@ -616,6 +542,13 @@ KAFKA_SASL_MECHANISM {sasl_mechanism or "PLAIN"}
616
542
  KAFKA_KEY {kafka_key}
617
543
  KAFKA_SECRET {kafka_secret}
618
544
  """
545
+ if schema_registry_url:
546
+ content += f"""KAFKA_SCHEMA_REGISTRY_URL {schema_registry_url}\n"""
547
+ if kafka_ssl_ca_pem:
548
+ content += f"""KAFKA_SSL_CA_PEM >\n {kafka_ssl_ca_pem}\n"""
549
+ content += """# Learn more at https://www.tinybird.co/docs/forward/get-data-in/connectors/kafka#kafka-connection-settings
550
+ """
551
+
619
552
  return generate_connection_file(name, content, folder, skip_feedback=True)
620
553
 
621
554
 
@@ -128,41 +128,33 @@ def folder_build(
128
128
  filename = to_run[name]["filename"]
129
129
  filename = filename.replace(f"{folder}/", "")
130
130
  click.echo(FeedbackManager.info(message=f"✓ {filename}"))
131
- else:
132
- if raise_on_exists:
133
- raise AlreadyExistsException(
134
- FeedbackManager.warning_name_already_exists(
135
- name=name if to_run[name]["version"] is None else f"{name}__v{to_run[name]['version']}"
136
- )
131
+ elif raise_on_exists:
132
+ raise AlreadyExistsException(
133
+ FeedbackManager.warning_name_already_exists(
134
+ name=name if to_run[name]["version"] is None else f"{name}__v{to_run[name]['version']}"
137
135
  )
136
+ )
137
+ elif name_matches_existing_resource(resource, name, tb_client):
138
+ if resource == "pipes":
139
+ click.echo(FeedbackManager.error_pipe_cannot_be_pushed(name=name))
138
140
  else:
139
- if name_matches_existing_resource(resource, name, tb_client):
140
- if resource == "pipes":
141
- click.echo(FeedbackManager.error_pipe_cannot_be_pushed(name=name))
142
- else:
143
- click.echo(FeedbackManager.error_datasource_cannot_be_pushed(name=name))
144
- else:
145
- click.echo(
146
- FeedbackManager.warning_name_already_exists(
147
- name=(
148
- name
149
- if to_run[name]["version"] is None
150
- else f"{name}__v{to_run[name]['version']}"
151
- )
152
- )
153
- )
154
- else:
155
- if should_push_file(name, remote_resource_names, force, run_tests):
156
- extension = "pipe" if resource == "pipes" else "datasource"
157
- click.echo(FeedbackManager.info_building_resource(name=f"{name}.{extension}", version=""))
141
+ click.echo(FeedbackManager.error_datasource_cannot_be_pushed(name=name))
158
142
  else:
159
- if name_matches_existing_resource(resource, name, tb_client):
160
- if resource == "pipes":
161
- click.echo(FeedbackManager.warning_pipe_cannot_be_pushed(name=name))
162
- else:
163
- click.echo(FeedbackManager.warning_datasource_cannot_be_pushed(name=name))
164
- else:
165
- click.echo(FeedbackManager.warning_dry_name_already_exists(name=name))
143
+ click.echo(
144
+ FeedbackManager.warning_name_already_exists(
145
+ name=(name if to_run[name]["version"] is None else f"{name}__v{to_run[name]['version']}")
146
+ )
147
+ )
148
+ elif should_push_file(name, remote_resource_names, force, run_tests):
149
+ extension = "pipe" if resource == "pipes" else "datasource"
150
+ click.echo(FeedbackManager.info_building_resource(name=f"{name}.{extension}", version=""))
151
+ elif name_matches_existing_resource(resource, name, tb_client):
152
+ if resource == "pipes":
153
+ click.echo(FeedbackManager.warning_pipe_cannot_be_pushed(name=name))
154
+ else:
155
+ click.echo(FeedbackManager.warning_datasource_cannot_be_pushed(name=name))
156
+ else:
157
+ click.echo(FeedbackManager.warning_dry_name_already_exists(name=name))
166
158
 
167
159
  def push_files(
168
160
  dependency_graph: GraphDependencies,
@@ -932,11 +924,8 @@ def process_file(
932
924
  raise click.ClickException(FeedbackManager.error_missing_table_arn(datasource=datasource["name"]))
933
925
  if not params.get("import_export_bucket", None):
934
926
  raise click.ClickException(FeedbackManager.error_missing_export_bucket(datasource=datasource["name"]))
935
- else:
936
- if not params.get("import_external_datasource", None):
937
- raise click.ClickException(
938
- FeedbackManager.error_missing_external_datasource(datasource=datasource["name"])
939
- )
927
+ elif not params.get("import_external_datasource", None):
928
+ raise click.ClickException(FeedbackManager.error_missing_external_datasource(datasource=datasource["name"]))
940
929
 
941
930
  return params
942
931
 
@@ -1059,7 +1048,7 @@ def process_file(
1059
1048
  #
1060
1049
  # Note: any unknown import_ parameter is leaved as is.
1061
1050
  for key in ImportReplacements.get_datafile_parameter_keys():
1062
- replacement, default_value = ImportReplacements.get_api_param_for_datafile_param(service, key)
1051
+ replacement, default_value = ImportReplacements.get_api_param_for_datafile_param(key)
1063
1052
  if not replacement:
1064
1053
  continue # We should not reach this never, but just in case...
1065
1054
 
@@ -301,32 +301,28 @@ def new_ds(
301
301
  if alter_response and make_changes:
302
302
  # alter operation finished
303
303
  pass
304
+ elif (
305
+ os.getenv("TB_I_KNOW_WHAT_I_AM_DOING")
306
+ and click.prompt(FeedbackManager.info_ask_for_datasource_confirmation()) == ds_name
307
+ ): # TODO move to CLI
308
+ try:
309
+ client.datasource_delete(ds_name)
310
+ click.echo(FeedbackManager.success_delete_datasource(datasource=ds_name))
311
+ except Exception:
312
+ raise click.ClickException(FeedbackManager.error_removing_datasource(datasource=ds_name))
313
+ return
314
+ elif alter_error_message:
315
+ raise click.ClickException(
316
+ FeedbackManager.error_datasource_already_exists_and_alter_failed(
317
+ datasource=ds_name, alter_error_message=alter_error_message
318
+ )
319
+ )
320
+ elif promote_error_message:
321
+ raise click.ClickException(
322
+ FeedbackManager.error_promoting_datasource(datasource=ds_name, error=promote_error_message)
323
+ )
304
324
  else:
305
- # removed replacing by default. When a datasource is removed data is
306
- # removed and all the references needs to be updated
307
- if (
308
- os.getenv("TB_I_KNOW_WHAT_I_AM_DOING")
309
- and click.prompt(FeedbackManager.info_ask_for_datasource_confirmation()) == ds_name
310
- ): # TODO move to CLI
311
- try:
312
- client.datasource_delete(ds_name)
313
- click.echo(FeedbackManager.success_delete_datasource(datasource=ds_name))
314
- except Exception:
315
- raise click.ClickException(FeedbackManager.error_removing_datasource(datasource=ds_name))
316
- return
317
- else:
318
- if alter_error_message:
319
- raise click.ClickException(
320
- FeedbackManager.error_datasource_already_exists_and_alter_failed(
321
- datasource=ds_name, alter_error_message=alter_error_message
322
- )
323
- )
324
- if promote_error_message:
325
- raise click.ClickException(
326
- FeedbackManager.error_promoting_datasource(datasource=ds_name, error=promote_error_message)
327
- )
328
- else:
329
- click.echo(FeedbackManager.warning_datasource_already_exists(datasource=ds_name))
325
+ click.echo(FeedbackManager.warning_datasource_already_exists(datasource=ds_name))
330
326
 
331
327
 
332
328
  def share_and_unshare_datasource(
@@ -168,7 +168,7 @@ def diff_command(
168
168
  sys.stdout.writelines(diff_lines)
169
169
  click.echo("")
170
170
 
171
- for rfilename, _ in local_resources.items():
171
+ for rfilename in local_resources.keys():
172
172
  if rfilename not in changed:
173
173
  for resource in remote_datasources + remote_pipes:
174
174
  properties = get_name_version(resource["name"])
@@ -25,11 +25,25 @@ from tinybird.tb.modules.datafile.format_datasource import format_engine
25
25
 
26
26
 
27
27
  def format_node_sql(
28
- file_parts: List[str], node: Dict[str, Any], line_length: Optional[int] = None, lower_keywords: bool = False
28
+ file_parts: List[str],
29
+ node: Dict[str, Any],
30
+ line_length: Optional[int] = None,
31
+ lower_keywords: bool = False,
32
+ resource_name: Optional[str] = None,
33
+ resource_source: Optional[str] = None,
29
34
  ) -> List[str]:
30
35
  file_parts.append("SQL >")
31
36
  file_parts.append(DATAFILE_NEW_LINE)
32
- file_parts.append(format_sql(node["sql"], DATAFILE_INDENT, line_length=line_length, lower_keywords=lower_keywords))
37
+ file_parts.append(
38
+ format_sql(
39
+ node["sql"],
40
+ DATAFILE_INDENT,
41
+ line_length=line_length,
42
+ lower_keywords=lower_keywords,
43
+ resource_name=resource_name,
44
+ resource_source=resource_source,
45
+ )
46
+ )
33
47
  file_parts.append(DATAFILE_NEW_LINE)
34
48
  file_parts.append(DATAFILE_NEW_LINE)
35
49
  return file_parts
@@ -93,8 +107,21 @@ def format_pipe_include(file_parts: List[str], node: Dict[str, Any], includes: D
93
107
  return file_parts
94
108
 
95
109
 
96
- def format_sql(sql: str, DATAFILE_INDENT: str, line_length: Optional[int] = None, lower_keywords: bool = False) -> str:
97
- sql = format_sql_template(sql.strip(), line_length=line_length, lower_keywords=lower_keywords)
110
+ def format_sql(
111
+ sql: str,
112
+ DATAFILE_INDENT: str,
113
+ line_length: Optional[int] = None,
114
+ lower_keywords: bool = False,
115
+ resource_name: Optional[str] = None,
116
+ resource_source: Optional[str] = None,
117
+ ) -> str:
118
+ sql = format_sql_template(
119
+ sql.strip(),
120
+ line_length=line_length,
121
+ lower_keywords=lower_keywords,
122
+ resource_name=resource_name,
123
+ resource_source=resource_source,
124
+ )
98
125
  return "\n".join([f"{DATAFILE_INDENT}{part}" for part in sql.split("\n") if len(part.strip())])
99
126
 
100
127
 
@@ -105,6 +132,8 @@ def format_node(
105
132
  line_length: Optional[int] = None,
106
133
  unroll_includes: bool = False,
107
134
  lower_keywords: bool = False,
135
+ resource_name: Optional[str] = None,
136
+ resource_source: Optional[str] = None,
108
137
  ) -> None:
109
138
  if not unroll_includes:
110
139
  format_pipe_include(file_parts, node, includes)
@@ -119,7 +148,14 @@ def format_node(
119
148
 
120
149
  Doc = namedtuple("Doc", ["description"])
121
150
  format_description(file_parts, Doc(node.get("description", "")))
122
- format_node_sql(file_parts, node, line_length=line_length, lower_keywords=lower_keywords)
151
+ format_node_sql(
152
+ file_parts,
153
+ node,
154
+ line_length=line_length,
155
+ lower_keywords=lower_keywords,
156
+ resource_name=resource_name,
157
+ resource_source=resource_source,
158
+ )
123
159
  format_node_type(file_parts, node)
124
160
 
125
161
 
@@ -132,6 +168,7 @@ def format_pipe(
132
168
  for_deploy_diff: bool = False,
133
169
  skip_eval: bool = False,
134
170
  content: Optional[str] = None,
171
+ resource_source: Optional[str] = None,
135
172
  ) -> str:
136
173
  if datafile:
137
174
  doc = datafile
@@ -162,7 +199,7 @@ def format_pipe(
162
199
  if "." in include_file
163
200
  else eval_var(include_file)
164
201
  )
165
- included_pipe = parse_pipe(include_file, skip_eval=skip_eval).datafile
202
+ included_pipe = parse_pipe(str(include_file), skip_eval=skip_eval).datafile
166
203
  pipe_nodes = doc.nodes.copy()
167
204
  for included_node in included_pipe.nodes.copy():
168
205
  unrolled_included_node = next(
@@ -178,10 +215,12 @@ def format_pipe(
178
215
  line_length=line_length,
179
216
  unroll_includes=unroll_includes,
180
217
  lower_keywords=bool(for_deploy_diff),
218
+ resource_name=filename,
219
+ resource_source=resource_source,
181
220
  )
182
221
 
183
222
  if not unroll_includes:
184
- for k, _ in doc.includes.items():
223
+ for k in doc.includes.keys():
185
224
  if ".incl" not in k:
186
225
  continue
187
226
  file_parts.append(f"INCLUDE {k}")