tinybird 0.0.1.dev29__py3-none-any.whl → 0.0.1.dev30__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.

@@ -175,7 +175,7 @@ def generate_datafile(
175
175
  if not f.exists() or force:
176
176
  with open(f"{f}", "w") as ds_file:
177
177
  ds_file.write(datafile)
178
- click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
178
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder or ".")))
179
179
 
180
180
  if data and (base / "fixtures").exists():
181
181
  # Generating a fixture for Parquet files is not so trivial, since Parquet format
@@ -1177,31 +1177,6 @@ async def print_current_workspace(config: CLIConfig) -> None:
1177
1177
  echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
1178
1178
 
1179
1179
 
1180
- async def print_current_branch(config: CLIConfig) -> None:
1181
- _ = await try_update_config_with_remote(config, only_if_needed=True)
1182
-
1183
- response = await config.get_client().user_workspaces_and_branches()
1184
-
1185
- columns = ["name", "id", "workspace"]
1186
- table = []
1187
-
1188
- for workspace in response["workspaces"]:
1189
- if config["id"] == workspace["id"]:
1190
- click.echo(FeedbackManager.info_current_branch())
1191
- if workspace.get("is_branch"):
1192
- name = workspace["name"]
1193
- main_workspace = await get_current_main_workspace(config)
1194
- assert isinstance(main_workspace, dict)
1195
- main_name = main_workspace["name"]
1196
- else:
1197
- name = MAIN_BRANCH
1198
- main_name = workspace["name"]
1199
- table.append([name, workspace["id"], main_name])
1200
- break
1201
-
1202
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
1203
-
1204
-
1205
1180
  class ConnectionReplacements:
1206
1181
  _PARAMS_REPLACEMENTS: Dict[str, Dict[str, str]] = {
1207
1182
  "s3": {
@@ -323,14 +323,6 @@ class CLIConfig:
323
323
  path: str = os.path.join(working_dir, ".tinyb")
324
324
  return CLIConfig(path, parent=CLIConfig.get_global_config())
325
325
 
326
- @staticmethod
327
- def get_llm_config(working_dir: Optional[str] = None) -> Dict[str, Any]:
328
- return (
329
- CLIConfig.get_project_config(working_dir)
330
- .get("llms", {})
331
- .get("openai", {"model": "gpt-4o-mini", "api_key": None})
332
- )
333
-
334
326
  @staticmethod
335
327
  def reset() -> None:
336
328
  CLIConfig._global = None
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  from os import getcwd
3
4
  from pathlib import Path
4
5
  from typing import Optional
@@ -260,7 +261,25 @@ def init_git(folder: str):
260
261
 
261
262
 
262
263
  def generate_pipe_file(name: str, content: str, folder: str):
263
- base = Path(folder) / "endpoints"
264
+ def is_copy(content: str) -> bool:
265
+ return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
266
+
267
+ def is_materialization(content: str) -> bool:
268
+ return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
269
+
270
+ def is_sink(content: str) -> bool:
271
+ return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
272
+
273
+ if is_copy(content):
274
+ pathname = "copies"
275
+ elif is_materialization(content):
276
+ pathname = "materializations"
277
+ elif is_sink(content):
278
+ pathname = "sinks"
279
+ else:
280
+ pathname = "endpoints"
281
+
282
+ base = Path(folder) / pathname
264
283
  if not base.exists():
265
284
  base = Path()
266
285
  f = base / (f"{name}.pipe")
@@ -768,13 +768,13 @@ async def process(
768
768
  raise click.ClickException(FeedbackManager.error_forkdownstream_pipes_with_engine(pipe=resource_name))
769
769
 
770
770
  to_run[resource_name] = r
771
- file_deps = r.get("deps", [])
771
+ file_deps: List[str] = r.get("deps", [])
772
772
  deps += file_deps
773
773
  # calculate and look for deps
774
774
  dep_list = []
775
775
  for x in file_deps:
776
776
  if x not in INTERNAL_TABLES or is_internal:
777
- f, ds = find_file_by_name(dir_path, x, verbose, vendor_paths=vendor_paths, resource=r)
777
+ f, ds = find_file_by_name(dir_path or ".", x, verbose, vendor_paths=vendor_paths, resource=r)
778
778
  if f:
779
779
  dep_list.append(f.rsplit(".", 1)[0])
780
780
  if ds:
@@ -266,7 +266,19 @@ async def new_pipe(
266
266
 
267
267
  if data.get("type") == "endpoint":
268
268
  token = tb_client.token
269
- click.echo(f"""** => Test endpoint with:\n** $ curl {host}/v0/pipes/{p["name"]}.json?token={token}""")
269
+ try:
270
+ example_params = {
271
+ "format": "json",
272
+ "pipe": p["name"],
273
+ "q": "",
274
+ "token": token,
275
+ }
276
+ endpoint_url = await tb_client._req(f"/examples/query.http?{urlencode(example_params)}")
277
+ if endpoint_url:
278
+ endpoint_url = endpoint_url.replace("http://localhost:8001", host)
279
+ click.echo(f"""** => Test endpoint with:\n** $ curl {endpoint_url}""")
280
+ except Exception:
281
+ pass
270
282
 
271
283
 
272
284
  async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
@@ -542,7 +542,7 @@ async def datasource_share(ctx: Context, datasource_name: str, workspace_name_or
542
542
  """Share a datasource"""
543
543
 
544
544
  config = CLIConfig.get_project_config()
545
- client = config.get_client()
545
+ client: TinyB = ctx.ensure_object(dict)["client"]
546
546
  host = config.get_host() or CLIConfig.DEFAULTS["host"]
547
547
  ui_host = get_display_host(host)
548
548
 
@@ -1,8 +1,10 @@
1
1
  import json
2
+ import os
2
3
  import urllib.parse
3
4
  from copy import deepcopy
4
- from typing import List
5
+ from typing import Any, List
5
6
 
7
+ from anthropic import AnthropicVertex
6
8
  from pydantic import BaseModel
7
9
 
8
10
  from tinybird.client import TinyB
@@ -37,7 +39,7 @@ class LLM:
37
39
  self.user_client = deepcopy(client)
38
40
  self.user_client.token = user_token
39
41
 
40
- async def ask(self, prompt: str, system_prompt: str = "", model: str = "o1-mini") -> str:
42
+ async def ask(self, prompt: str, system_prompt: str = "") -> str:
41
43
  """
42
44
  Calls the model with the given prompt and returns the response.
43
45
 
@@ -47,7 +49,7 @@ class LLM:
47
49
  Returns:
48
50
  str: The response from the language model.
49
51
  """
50
- messages = []
52
+ messages: List[Any] = []
51
53
 
52
54
  if system_prompt:
53
55
  messages.append({"role": "user", "content": system_prompt})
@@ -55,8 +57,21 @@ class LLM:
55
57
  if prompt:
56
58
  messages.append({"role": "user", "content": prompt})
57
59
 
60
+ if gcloud_access_token := os.getenv("GCLOUD_ACCESS_TOKEN"):
61
+ client = AnthropicVertex(
62
+ region="europe-west1",
63
+ project_id="gen-lang-client-0705305160",
64
+ access_token=gcloud_access_token,
65
+ )
66
+ message = client.messages.create(
67
+ max_tokens=8000,
68
+ messages=messages,
69
+ model="claude-3-5-sonnet-v2@20241022",
70
+ )
71
+ return message.content[0].text or "" # type: ignore
72
+
58
73
  data = {
59
- "model": model,
74
+ "model": "o1-mini",
60
75
  "messages": messages,
61
76
  }
62
77
  response = await self.user_client._req(
@@ -82,7 +82,7 @@ def get_docker_client():
82
82
  client.ping()
83
83
  return client
84
84
  except Exception:
85
- raise CLIException("Docker is not running or installed. Make sure Docker is installed and running.")
85
+ raise CLIException("No container runtime is running. Make sure a Docker-compatible runtime is installed and running.")
86
86
 
87
87
 
88
88
  def stop_tinybird_local(docker_client):
@@ -135,7 +135,12 @@ async def login(host: str, workspace: str):
135
135
  server_thread.start()
136
136
 
137
137
  # Open the browser to the auth page
138
- client_id = "T6excMo8IKguvUw4vFNYfqlt9pe6msCU"
138
+ if "wadus" in host:
139
+ client_id = "Rpl7Uy9aSjqoPCSvHgGl3zNQuZcSOXBe"
140
+ base_auth_url = "https://auth.wadus1.tinybird.co"
141
+ else:
142
+ client_id = "T6excMo8IKguvUw4vFNYfqlt9pe6msCU"
143
+ base_auth_url = "https://auth.tinybird.co"
139
144
  callback_url = f"http://localhost:{AUTH_SERVER_PORT}"
140
145
  params = {
141
146
  "client_id": client_id,
@@ -143,7 +148,7 @@ async def login(host: str, workspace: str):
143
148
  "response_type": "token",
144
149
  "scope": "openid profile email",
145
150
  }
146
- auth_url = f"https://auth.tinybird.co/authorize?{urlencode(params)}"
151
+ auth_url = f"{base_auth_url}/authorize?{urlencode(params)}"
147
152
  webbrowser.open(auth_url)
148
153
 
149
154
  # Wait for the authentication to complete or timeout
@@ -274,7 +274,7 @@ class Shell:
274
274
  elif arg.startswith("mock"):
275
275
  self.handle_mock(arg)
276
276
  else:
277
- subprocess.run(f"tb --local {arg}", shell=True, text=True)
277
+ subprocess.run(f"tb {arg}", shell=True, text=True)
278
278
 
279
279
  def default(self, argline):
280
280
  click.echo("")
@@ -286,7 +286,7 @@ class Shell:
286
286
  elif len(arg.split()) == 1 and arg in self.endpoints + self.pipes + self.datasources + self.shared_datasources:
287
287
  self.run_sql(f"select * from {arg}")
288
288
  else:
289
- subprocess.run(f"tb --local {arg}", shell=True, text=True)
289
+ subprocess.run(f"tb {arg}", shell=True, text=True)
290
290
 
291
291
  def run_sql(self, query, rows_limit=20):
292
292
  try:
@@ -6,11 +6,13 @@
6
6
  import difflib
7
7
  import glob
8
8
  import os
9
+ import urllib.parse
9
10
  from pathlib import Path
10
- from typing import Any, Dict, Iterable, List, Optional, Tuple
11
+ from typing import Any, Dict, List, Optional, Tuple
11
12
 
12
13
  import click
13
14
  import yaml
15
+ from requests import Response
14
16
 
15
17
  from tinybird.prompts import test_create_prompt
16
18
  from tinybird.tb.modules.cli import cli
@@ -66,9 +68,9 @@ def test(ctx: click.Context) -> None:
66
68
  type=click.Path(exists=True, file_okay=False),
67
69
  help="Folder where datafiles will be placed",
68
70
  )
69
- @click.option("--prompt", type=str, default=None, help="Prompt to be used to create the test")
71
+ @click.option("--prompt", type=str, default="", help="Prompt to be used to create the test")
70
72
  @coro
71
- async def test_create(name_or_filename: str, prompt: Optional[str], folder: str) -> None:
73
+ async def test_create(name_or_filename: str, prompt: str, folder: str) -> None:
72
74
  """
73
75
  Create a test for an existing pipe
74
76
  """
@@ -80,15 +82,13 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
80
82
  raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
81
83
  else:
82
84
  pipe_folders = ("endpoints", "copies", "materializations", "sinks", "pipes")
83
- pipe_path = next(
84
- (
85
+ try:
86
+ pipe_path = next(
85
87
  root_path / folder / f"{name_or_filename}.pipe"
86
88
  for folder in pipe_folders
87
89
  if (root_path / folder / f"{name_or_filename}.pipe").exists()
88
- ),
89
- None,
90
- )
91
- if not pipe_path:
90
+ )
91
+ except Exception:
92
92
  raise CLIException(FeedbackManager.error(message=f"Pipe {name_or_filename} not found"))
93
93
 
94
94
  pipe_name = pipe_path.stem
@@ -97,8 +97,8 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
97
97
  pipe_content = pipe_path.read_text()
98
98
 
99
99
  client = await get_tinybird_local_client(os.path.abspath(folder))
100
- pipe_nodes = await client._req(f"/v0/pipes/{pipe_name}")
101
- parameters = set([param["name"] for node in pipe_nodes["nodes"] for param in node["params"]])
100
+ pipe = await client._req(f"/v0/pipes/{pipe_name}")
101
+ parameters = set([param["name"] for node in pipe["nodes"] for param in node["params"]])
102
102
 
103
103
  system_prompt = test_create_prompt.format(
104
104
  name=pipe_name,
@@ -107,24 +107,26 @@ async def test_create(name_or_filename: str, prompt: Optional[str], folder: str)
107
107
  )
108
108
  config = CLIConfig.get_project_config(folder)
109
109
  user_token = config.get_user_token()
110
+ if not user_token:
111
+ raise CLIException(FeedbackManager.error(message="No user token found"))
110
112
  llm = LLM(user_token=user_token, client=config.get_client())
111
113
 
112
- response = await llm.ask(prompt, system_prompt=system_prompt)
113
- response = extract_xml(response, "response")
114
- tests_content = parse_xml(response, "test")
114
+ response_llm = await llm.ask(prompt, system_prompt=system_prompt)
115
+ response_xml = extract_xml(response_llm, "response")
116
+ tests_content = parse_xml(response_xml, "test")
115
117
 
116
118
  tests: List[Dict[str, Any]] = []
117
119
  for test_content in tests_content:
118
- test = {}
120
+ test: Dict[str, Any] = {}
119
121
  test["name"] = extract_xml(test_content, "name")
120
122
  test["description"] = extract_xml(test_content, "description")
121
- parameters = extract_xml(test_content, "parameters")
122
- test["parameters"] = parameters.split("?")[1] if "?" in parameters else parameters
123
+ parameters_api = extract_xml(test_content, "parameters")
124
+ test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
123
125
  test["expected_result"] = ""
124
126
 
125
127
  response = None
126
128
  try:
127
- response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{parameters}")
129
+ response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
128
130
  except Exception:
129
131
  pass
130
132
 
@@ -181,7 +183,7 @@ async def test_update(pipe: str, folder: str) -> None:
181
183
  test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
182
184
  response = None
183
185
  try:
184
- response = await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
186
+ response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test_params)
185
187
  except Exception:
186
188
  continue
187
189
 
@@ -221,20 +223,21 @@ async def run_tests(name: Tuple[str, ...], folder: str) -> None:
221
223
  client = await get_tinybird_local_client(os.path.abspath(folder))
222
224
  paths = [Path(n) for n in name]
223
225
  endpoints = [f"./tests/{p.stem}.yaml" for p in paths]
224
- test_files: Iterable[str] = endpoints if len(endpoints) > 0 else glob.glob("./tests/**/*.y*ml", recursive=True)
226
+ test_files: List[str] = endpoints if len(endpoints) > 0 else glob.glob("./tests/**/*.y*ml", recursive=True)
225
227
 
226
228
  async def run_test(test_file):
227
229
  test_file_path = Path(test_file)
228
230
  click.echo(FeedbackManager.info(message=f"\n* {test_file_path.stem}{test_file_path.suffix}"))
229
231
  test_file_content = yaml.safe_load(test_file_path.read_text())
232
+
230
233
  for test in test_file_content:
231
234
  try:
232
235
  test_params = test["parameters"].split("?")[1] if "?" in test["parameters"] else test["parameters"]
233
236
  response = None
234
237
  try:
235
- response = await client._req_raw(f"/v0/pipes/{test_file_path.stem}.ndjson?{test_params}")
238
+ response = await get_pipe_data(client, pipe_name=test_file_path.stem, test_params=test_params)
236
239
  except Exception:
237
- raise Exception("Expected to not fail but got an error")
240
+ continue
238
241
 
239
242
  expected_result = response.text
240
243
  if response.status_code >= 400:
@@ -271,3 +274,19 @@ async def run_tests(name: Tuple[str, ...], folder: str) -> None:
271
274
  exit(1)
272
275
  else:
273
276
  click.echo(FeedbackManager.success(message=f"\n✓ {test_count}/{test_count} passed"))
277
+
278
+
279
+ async def get_pipe_data(client, pipe_name: str, test_params: str) -> Response:
280
+ pipe = await client._req(f"/v0/pipes/{pipe_name}")
281
+ output_node = next(
282
+ (node for node in pipe["nodes"] if node["node_type"] != "default" and node["node_type"] != "standard"),
283
+ {"name": "not_found"},
284
+ )
285
+ if output_node["node_type"] == "endpoint":
286
+ return await client._req_raw(f"/v0/pipes/{pipe_name}.ndjson?{test_params}")
287
+
288
+ params = {
289
+ "q": output_node["sql"],
290
+ "pipeline": pipe_name,
291
+ }
292
+ return await client._req_raw(f"""/v0/sql?{urllib.parse.urlencode(params)}&{test_params}""")
@@ -0,0 +1,182 @@
1
+ import os
2
+ import re
3
+ from os import getcwd
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import click
8
+
9
+ from tinybird.client import TinyB
10
+ from tinybird.prompts import mock_prompt, update_prompt
11
+ from tinybird.tb.modules.cli import cli
12
+ from tinybird.tb.modules.common import check_user_token_with_client, coro, generate_datafile
13
+ from tinybird.tb.modules.config import CLIConfig
14
+ from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
15
+ from tinybird.tb.modules.exceptions import CLIException
16
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
17
+ from tinybird.tb.modules.llm import LLM
18
+ from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
19
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
20
+
21
+
22
+ @cli.command()
23
+ @click.argument("prompt")
24
+ @click.option(
25
+ "--folder",
26
+ default=".",
27
+ type=click.Path(exists=False, file_okay=False),
28
+ help="Folder where project files will be placed",
29
+ )
30
+ @coro
31
+ async def update(
32
+ prompt: str,
33
+ folder: str,
34
+ ) -> None:
35
+ """Update resources in the project."""
36
+ folder = folder or getcwd()
37
+ folder_path = Path(folder)
38
+ if not folder_path.exists():
39
+ folder_path.mkdir()
40
+
41
+ try:
42
+ config = CLIConfig.get_project_config(folder)
43
+ tb_client = config.get_client()
44
+ user_token: Optional[str] = None
45
+ try:
46
+ user_token = config.get_user_token()
47
+ if not user_token:
48
+ raise CLIException("No user token found")
49
+ await check_user_token_with_client(tb_client, token=user_token)
50
+ except Exception as e:
51
+ click.echo(
52
+ FeedbackManager.error(message=f"This action requires authentication. Run 'tb login' first. Error: {e}")
53
+ )
54
+ return
55
+
56
+ local_client = await get_tinybird_local_client(folder)
57
+
58
+ click.echo(FeedbackManager.highlight(message="\n» Updating resources..."))
59
+ datasources_updated = await update_resources(tb_client, user_token, prompt, folder)
60
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
61
+
62
+ if datasources_updated and user_token:
63
+ click.echo(FeedbackManager.highlight(message="\n» Generating fixtures..."))
64
+
65
+ datasource_files = [f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")]
66
+ for datasource_file in datasource_files:
67
+ datasource_path = Path(folder) / "datasources" / datasource_file
68
+ llm = LLM(user_token=user_token, client=tb_client)
69
+ datasource_name = datasource_path.stem
70
+ datasource_content = datasource_path.read_text()
71
+ has_json_path = "`json:" in datasource_content
72
+ if has_json_path:
73
+ prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
74
+ response = await llm.ask(prompt, system_prompt=mock_prompt(rows=20))
75
+ sql = extract_xml(response, "sql")
76
+ sql = sql.split("FORMAT")[0]
77
+ result = await local_client.query(f"{sql} FORMAT JSON")
78
+ data = result.get("data", [])
79
+ fixture_name = build_fixture_name(
80
+ datasource_path.absolute().as_posix(), datasource_name, datasource_content
81
+ )
82
+ if data:
83
+ persist_fixture(fixture_name, data, folder)
84
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}"))
85
+
86
+ click.echo(FeedbackManager.success(message="✓ Done!\n"))
87
+ except Exception as e:
88
+ click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
89
+
90
+
91
+ async def update_resources(
92
+ tb_client: TinyB,
93
+ user_token: str,
94
+ prompt: str,
95
+ folder: str,
96
+ ):
97
+ datasource_paths = [
98
+ Path(folder) / "datasources" / f for f in os.listdir(Path(folder) / "datasources") if f.endswith(".datasource")
99
+ ]
100
+ pipes_paths = [
101
+ Path(folder) / "endpoints" / f for f in os.listdir(Path(folder) / "endpoints") if f.endswith(".pipe")
102
+ ]
103
+ resources_xml = "\n".join(
104
+ [
105
+ f"<resource><type>{resource_type}</type><name>{resource_name}</name><content>{resource_content}</content></resource>"
106
+ for resource_type, resource_name, resource_content in [
107
+ ("datasource", ds.stem, ds.read_text()) for ds in datasource_paths
108
+ ]
109
+ + [
110
+ (
111
+ "pipe",
112
+ pipe.stem,
113
+ pipe.read_text(),
114
+ )
115
+ for pipe in pipes_paths
116
+ ]
117
+ ]
118
+ )
119
+ llm = LLM(user_token=user_token, client=tb_client)
120
+ result = await llm.ask(prompt, system_prompt=update_prompt(resources_xml))
121
+ result = extract_xml(result, "response")
122
+ resources = parse_xml(result, "resource")
123
+ datasources = []
124
+ pipes = []
125
+ for resource_xml in resources:
126
+ resource_type = extract_xml(resource_xml, "type")
127
+ name = extract_xml(resource_xml, "name")
128
+ content = extract_xml(resource_xml, "content")
129
+ resource = {
130
+ "name": name,
131
+ "content": content,
132
+ }
133
+ if resource_type.lower() == "datasource":
134
+ datasources.append(resource)
135
+ elif resource_type.lower() == "pipe":
136
+ pipes.append(resource)
137
+
138
+ for ds in datasources:
139
+ content = ds["content"].replace("```", "")
140
+ filename = f"{ds['name']}.datasource"
141
+ generate_datafile(
142
+ content,
143
+ filename=filename,
144
+ data=None,
145
+ _format="ndjson",
146
+ force=True,
147
+ folder=folder,
148
+ )
149
+
150
+ for pipe in pipes:
151
+ content = pipe["content"].replace("```", "")
152
+ generate_pipe_file(pipe["name"], content, folder)
153
+
154
+ return len(datasources) > 0
155
+
156
+
157
+ def generate_pipe_file(name: str, content: str, folder: str):
158
+ def is_copy(content: str) -> bool:
159
+ return re.search(r"TYPE copy", content, re.IGNORECASE) is not None
160
+
161
+ def is_materialization(content: str) -> bool:
162
+ return re.search(r"TYPE materialized", content, re.IGNORECASE) is not None
163
+
164
+ def is_sink(content: str) -> bool:
165
+ return re.search(r"TYPE sink", content, re.IGNORECASE) is not None
166
+
167
+ if is_copy(content):
168
+ pathname = "copies"
169
+ elif is_materialization(content):
170
+ pathname = "materializations"
171
+ elif is_sink(content):
172
+ pathname = "sinks"
173
+ else:
174
+ pathname = "endpoints"
175
+
176
+ base = Path(folder) / pathname
177
+ if not base.exists():
178
+ base = Path()
179
+ f = base / (f"{name}.pipe")
180
+ with open(f"{f}", "w") as file:
181
+ file.write(content)
182
+ click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev29
3
+ Version: 0.0.1.dev30
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -8,6 +8,7 @@ Author-email: support@tinybird.co
8
8
  Requires-Python: >=3.9, <3.13
9
9
  Description-Content-Type: text/x-rst
10
10
  Requires-Dist: aiofiles (==24.1.0)
11
+ Requires-Dist: anthropic (==0.42.0)
11
12
  Requires-Dist: click (<8.2,>=8.1.6)
12
13
  Requires-Dist: clickhouse-toolset (==0.33.dev0)
13
14
  Requires-Dist: colorama (==0.4.6)