tinybird 0.0.1.dev34__py3-none-any.whl → 0.0.1.dev36__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/context.py +1 -1
- tinybird/feedback_manager.py +6 -0
- tinybird/prompts.py +6 -0
- tinybird/sql_toolset.py +9 -2
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/cli.py +2 -2
- tinybird/tb/modules/build.py +53 -12
- tinybird/tb/modules/cli.py +7 -94
- tinybird/tb/modules/create.py +4 -4
- tinybird/tb/modules/datafile/build.py +2 -2
- tinybird/tb/modules/datafile/common.py +17 -1
- tinybird/tb/modules/datasource.py +3 -471
- tinybird/tb/modules/{deploy.py → deployment.py} +22 -12
- tinybird/tb/modules/endpoint.py +187 -0
- tinybird/tb/modules/llm.py +10 -16
- tinybird/tb/modules/llm_utils.py +87 -0
- tinybird/tb/modules/local.py +4 -1
- tinybird/tb/modules/local_common.py +2 -2
- tinybird/tb/modules/mock.py +3 -4
- tinybird/tb/modules/pipe.py +1 -254
- tinybird/tb/modules/shell.py +8 -1
- tinybird/tb/modules/test.py +2 -2
- tinybird/tb/modules/update.py +4 -4
- tinybird/tb/modules/watch.py +4 -4
- tinybird/tb/modules/workspace.py +0 -96
- tinybird/tb_cli_modules/common.py +19 -17
- {tinybird-0.0.1.dev34.dist-info → tinybird-0.0.1.dev36.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev34.dist-info → tinybird-0.0.1.dev36.dist-info}/RECORD +31 -31
- tinybird/tb/modules/connection.py +0 -803
- {tinybird-0.0.1.dev34.dist-info → tinybird-0.0.1.dev36.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev34.dist-info → tinybird-0.0.1.dev36.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev34.dist-info → tinybird-0.0.1.dev36.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# This is a command file for our CLI. Please keep it clean.
|
|
2
|
+
#
|
|
3
|
+
# - If it makes sense and only when strictly necessary, you can create utility functions in this file.
|
|
4
|
+
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
import humanfriendly
|
|
13
|
+
import requests
|
|
14
|
+
from click import Context
|
|
15
|
+
|
|
16
|
+
from tinybird.client import AuthNoTokenException, DoesNotExistException, TinyB
|
|
17
|
+
from tinybird.tb.modules.cli import cli
|
|
18
|
+
from tinybird.tb.modules.common import coro, echo_safe_humanfriendly_tables_format_smart_table
|
|
19
|
+
from tinybird.tb.modules.datafile.common import get_name_version
|
|
20
|
+
from tinybird.tb.modules.exceptions import CLIPipeException
|
|
21
|
+
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@cli.group(hidden=True)
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def endpoint(ctx):
|
|
27
|
+
"""Endpoint commands"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@endpoint.command(name="ls")
|
|
31
|
+
@click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. eg --match _test")
|
|
32
|
+
@click.option(
|
|
33
|
+
"--format",
|
|
34
|
+
"format_",
|
|
35
|
+
type=click.Choice(["json"], case_sensitive=False),
|
|
36
|
+
default=None,
|
|
37
|
+
help="Force a type of the output",
|
|
38
|
+
)
|
|
39
|
+
@click.pass_context
|
|
40
|
+
@coro
|
|
41
|
+
async def endpoint_ls(ctx: Context, match: str, format_: str):
|
|
42
|
+
"""List endpoints"""
|
|
43
|
+
|
|
44
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
45
|
+
pipes = await client.pipes(dependencies=False, node_attrs="name", attrs="name,updated_at,endpoint,url")
|
|
46
|
+
endpoints = [p for p in pipes if p.get("endpoint")]
|
|
47
|
+
endpoints = sorted(endpoints, key=lambda p: p["updated_at"])
|
|
48
|
+
tokens = await client.tokens()
|
|
49
|
+
columns = ["name", "updated at", "nodes", "url"]
|
|
50
|
+
table_human_readable = []
|
|
51
|
+
table_machine_readable = []
|
|
52
|
+
pattern = re.compile(match) if match else None
|
|
53
|
+
for t in endpoints:
|
|
54
|
+
tk = get_name_version(t["name"])
|
|
55
|
+
if pattern and not pattern.search(tk["name"]):
|
|
56
|
+
continue
|
|
57
|
+
token = get_endpoint_token(tokens, tk["name"])
|
|
58
|
+
endpoint_url = build_endpoint_url(client, tk["name"], token)
|
|
59
|
+
table_human_readable.append((tk["name"], t["updated_at"][:-7], len(t["nodes"]), endpoint_url))
|
|
60
|
+
table_machine_readable.append(
|
|
61
|
+
{
|
|
62
|
+
"name": tk["name"],
|
|
63
|
+
"updated at": t["updated_at"][:-7],
|
|
64
|
+
"nodes": len(t["nodes"]),
|
|
65
|
+
"url": endpoint_url,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not format_:
|
|
70
|
+
click.echo(FeedbackManager.info_pipes())
|
|
71
|
+
echo_safe_humanfriendly_tables_format_smart_table(table_human_readable, column_names=columns)
|
|
72
|
+
click.echo("\n")
|
|
73
|
+
elif format_ == "json":
|
|
74
|
+
click.echo(json.dumps({"pipes": table_machine_readable}, indent=2))
|
|
75
|
+
else:
|
|
76
|
+
raise CLIPipeException(FeedbackManager.error_pipe_ls_type())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@endpoint.command(name="token")
|
|
80
|
+
@click.argument("pipe_name")
|
|
81
|
+
@click.pass_context
|
|
82
|
+
@coro
|
|
83
|
+
async def endpoint_token(ctx: click.Context, pipe_name: str):
|
|
84
|
+
"""Retrieve a token to read an endpoint"""
|
|
85
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
await client.pipe_file(pipe_name)
|
|
89
|
+
except DoesNotExistException:
|
|
90
|
+
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name))
|
|
91
|
+
|
|
92
|
+
tokens = await client.tokens()
|
|
93
|
+
token = get_endpoint_token(tokens, pipe_name)
|
|
94
|
+
if token:
|
|
95
|
+
click.echo(token)
|
|
96
|
+
else:
|
|
97
|
+
click.echo(FeedbackManager.warning_token_pipe(pipe=pipe_name))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@endpoint.command(
|
|
101
|
+
name="data",
|
|
102
|
+
context_settings=dict(
|
|
103
|
+
allow_extra_args=True,
|
|
104
|
+
ignore_unknown_options=True,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
@click.argument("pipe")
|
|
108
|
+
@click.option("--query", default=None, help="Run SQL over endpoint results")
|
|
109
|
+
@click.option(
|
|
110
|
+
"--format", "format_", type=click.Choice(["json", "csv"], case_sensitive=False), help="Return format (CSV, JSON)"
|
|
111
|
+
)
|
|
112
|
+
@click.pass_context
|
|
113
|
+
@coro
|
|
114
|
+
async def endpoint_data(ctx: Context, pipe: str, query: str, format_: str):
|
|
115
|
+
"""Print data returned by an endpoint
|
|
116
|
+
|
|
117
|
+
Syntax: tb endpoint data <pipe_name> --param_name value --param2_name value2 ...
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
121
|
+
params = {ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, len(ctx.args), 2)}
|
|
122
|
+
req_format = "json" if not format_ else format_.lower()
|
|
123
|
+
try:
|
|
124
|
+
res = await client.pipe_data(pipe, format=req_format, sql=query, params=params)
|
|
125
|
+
except AuthNoTokenException:
|
|
126
|
+
raise
|
|
127
|
+
except Exception as e:
|
|
128
|
+
raise CLIPipeException(FeedbackManager.error_exception(error=str(e)))
|
|
129
|
+
|
|
130
|
+
if not format_:
|
|
131
|
+
stats = res["statistics"]
|
|
132
|
+
seconds = stats["elapsed"]
|
|
133
|
+
rows_read = humanfriendly.format_number(stats["rows_read"])
|
|
134
|
+
bytes_read = humanfriendly.format_size(stats["bytes_read"])
|
|
135
|
+
|
|
136
|
+
click.echo(FeedbackManager.success_print_pipe(pipe=pipe))
|
|
137
|
+
click.echo(FeedbackManager.info_query_stats(seconds=seconds, rows=rows_read, bytes=bytes_read))
|
|
138
|
+
|
|
139
|
+
if not res["data"]:
|
|
140
|
+
click.echo(FeedbackManager.info_no_rows())
|
|
141
|
+
else:
|
|
142
|
+
echo_safe_humanfriendly_tables_format_smart_table(
|
|
143
|
+
data=[d.values() for d in res["data"]], column_names=res["data"][0].keys()
|
|
144
|
+
)
|
|
145
|
+
click.echo("\n")
|
|
146
|
+
elif req_format == "json":
|
|
147
|
+
click.echo(json.dumps(res))
|
|
148
|
+
else:
|
|
149
|
+
click.echo(res)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@endpoint.command(name="url")
|
|
153
|
+
@click.argument("pipe")
|
|
154
|
+
@click.pass_context
|
|
155
|
+
@coro
|
|
156
|
+
async def endpoint_url(ctx: Context, pipe: str):
|
|
157
|
+
"""Print the URL of an endpoint"""
|
|
158
|
+
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
159
|
+
tokens = await client.tokens()
|
|
160
|
+
token = get_endpoint_token(tokens, pipe)
|
|
161
|
+
click.echo(build_endpoint_url(client, pipe, token))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def build_endpoint_url(tb_client: TinyB, pipe_name: str, token: Optional[str]) -> str:
|
|
165
|
+
try:
|
|
166
|
+
token = token or tb_client.token
|
|
167
|
+
example_params = {
|
|
168
|
+
"format": "json",
|
|
169
|
+
"pipe": pipe_name,
|
|
170
|
+
"q": "",
|
|
171
|
+
"token": token,
|
|
172
|
+
}
|
|
173
|
+
response = requests.get(f"{tb_client.host}/examples/query.http?{urlencode(example_params)}")
|
|
174
|
+
return response.text.replace("http://localhost:8001", tb_client.host)
|
|
175
|
+
except Exception:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_endpoint_token(tokens: List[Dict[str, Any]], pipe_name: str) -> Optional[str]:
|
|
180
|
+
token = None
|
|
181
|
+
for t in tokens:
|
|
182
|
+
for scope in t["scopes"]:
|
|
183
|
+
if scope["type"] == "PIPES:READ" and scope["resource"] == pipe_name:
|
|
184
|
+
token = t["token"]
|
|
185
|
+
break
|
|
186
|
+
|
|
187
|
+
return token
|
tinybird/tb/modules/llm.py
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from tinybird.client import TinyB
|
|
1
|
+
import requests
|
|
5
2
|
|
|
6
3
|
|
|
7
4
|
class LLM:
|
|
8
5
|
def __init__(
|
|
9
6
|
self,
|
|
7
|
+
host: str,
|
|
10
8
|
user_token: str,
|
|
11
|
-
client: TinyB,
|
|
12
9
|
):
|
|
13
|
-
self.
|
|
14
|
-
self.
|
|
10
|
+
self.host = host
|
|
11
|
+
self.user_token = user_token
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
def ask(self, system_prompt: str, prompt: str) -> str:
|
|
17
14
|
"""
|
|
18
15
|
Calls the model with the given prompt and returns the response.
|
|
19
16
|
|
|
@@ -25,14 +22,11 @@ class LLM:
|
|
|
25
22
|
str: The response from the language model.
|
|
26
23
|
"""
|
|
27
24
|
|
|
28
|
-
data = {"system": system_prompt}
|
|
29
|
-
|
|
30
|
-
if prompt:
|
|
31
|
-
data["prompt"] = prompt
|
|
25
|
+
data = {"system": system_prompt, "prompt": prompt}
|
|
32
26
|
|
|
33
|
-
response =
|
|
34
|
-
"/v0/llm",
|
|
35
|
-
|
|
27
|
+
response = requests.post(
|
|
28
|
+
f"{self.host}/v0/llm",
|
|
29
|
+
headers={"Authorization": f"Bearer {self.user_token}"},
|
|
36
30
|
data=data,
|
|
37
31
|
)
|
|
38
|
-
return response.get("result", "")
|
|
32
|
+
return response.json().get("result", "")
|
tinybird/tb/modules/llm_utils.py
CHANGED
|
@@ -22,3 +22,90 @@ def parse_xml(text: str, tag: str) -> List[str]:
|
|
|
22
22
|
Parses the text for the specified XML tag and returns a list of the contents of each tag.
|
|
23
23
|
"""
|
|
24
24
|
return re.findall(f"<{tag}.*?>(.*?)</{tag}>", text, re.DOTALL)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def generate(llm_call, task: str, context: str = "") -> tuple[str, str]:
|
|
28
|
+
"""Generate and improve a solution based on feedback."""
|
|
29
|
+
task = f"<task>\n{task}\n</task>"
|
|
30
|
+
full_prompt = (
|
|
31
|
+
f"{generator_prompt}\n<context>\n{context}\n</context>\n{task}" if context else f"{generator_prompt}\n{task}"
|
|
32
|
+
)
|
|
33
|
+
response = llm_call(full_prompt)
|
|
34
|
+
thoughts = extract_xml(response, "thoughts")
|
|
35
|
+
result = extract_xml(response, "response")
|
|
36
|
+
|
|
37
|
+
return thoughts, result
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def evaluate(llm_call, content: str, task: str) -> tuple[str, str]:
|
|
41
|
+
"""Evaluate if a solution meets requirements."""
|
|
42
|
+
full_prompt = f"{evaluator_prompt}\n<original_task>\n{task}\n</original_task>\n<content_to_evaluate>\n{content}\n</content_to_evaluate>"
|
|
43
|
+
response = llm_call(full_prompt)
|
|
44
|
+
evaluation = extract_xml(response, "evaluation")
|
|
45
|
+
feedback = extract_xml(response, "feedback")
|
|
46
|
+
|
|
47
|
+
return evaluation, feedback
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def loop(llm_call, task: str) -> str:
|
|
51
|
+
"""Keep generating and evaluating until requirements are met."""
|
|
52
|
+
memory = []
|
|
53
|
+
chain_of_thought = []
|
|
54
|
+
|
|
55
|
+
thoughts, result = generate(llm_call, task)
|
|
56
|
+
memory.append(result)
|
|
57
|
+
chain_of_thought.append({"thoughts": thoughts, "result": result})
|
|
58
|
+
|
|
59
|
+
attempts = 0
|
|
60
|
+
while True:
|
|
61
|
+
if attempts > 5:
|
|
62
|
+
raise Exception("Failed to generate a valid solution")
|
|
63
|
+
|
|
64
|
+
evaluation, feedback = evaluate(llm_call, result, task)
|
|
65
|
+
if evaluation == "PASS":
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
context = "\n".join(
|
|
69
|
+
[
|
|
70
|
+
"Previous attempts:",
|
|
71
|
+
*[f"- {m}" for m in memory],
|
|
72
|
+
f"\nFeedback: {feedback}",
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
thoughts, result = generate(llm_call, task, context)
|
|
77
|
+
memory.append(result)
|
|
78
|
+
chain_of_thought.append({"thoughts": thoughts, "result": result})
|
|
79
|
+
|
|
80
|
+
attempts += 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
evaluator_prompt = """
|
|
84
|
+
Evaluate this following code implementation for code correctness taking into account the user prompt and the instructions provided.
|
|
85
|
+
|
|
86
|
+
You should be evaluating only and not attemping to solve the task.
|
|
87
|
+
Only output "PASS" if all criteria are met and the code won't fail at runtime.
|
|
88
|
+
Output your evaluation concisely in the following format:
|
|
89
|
+
|
|
90
|
+
<evaluation>PASS or FAIL</evaluation>
|
|
91
|
+
<feedback>
|
|
92
|
+
[What is wrong with the code and how to fix it]
|
|
93
|
+
</feedback>
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
generator_prompt = """
|
|
97
|
+
Your goal is to complete the task based on <task> tag. If there are feedback
|
|
98
|
+
from your previous generations, you should reflect on them to improve your solution
|
|
99
|
+
|
|
100
|
+
Output your answer concisely in the following format:
|
|
101
|
+
|
|
102
|
+
<thoughts>
|
|
103
|
+
[Your understanding of the task and feedback and how you plan to improve]
|
|
104
|
+
</thoughts>
|
|
105
|
+
|
|
106
|
+
<response>
|
|
107
|
+
[Your code implementation here]
|
|
108
|
+
</response>
|
|
109
|
+
|
|
110
|
+
The code implementation should not be wrapped in any markdown format.
|
|
111
|
+
"""
|
tinybird/tb/modules/local.py
CHANGED
|
@@ -30,7 +30,10 @@ def start_tinybird_local(
|
|
|
30
30
|
|
|
31
31
|
if (
|
|
32
32
|
pull_show_prompt
|
|
33
|
-
and click.prompt(
|
|
33
|
+
and click.prompt(
|
|
34
|
+
FeedbackManager.warning(message="△ New version detected, download? [y/N]"), default="n"
|
|
35
|
+
).lower()
|
|
36
|
+
== "y"
|
|
34
37
|
):
|
|
35
38
|
click.echo(FeedbackManager.info(message="* Downloading latest version of Tinybird Local..."))
|
|
36
39
|
pull_required = True
|
|
@@ -17,11 +17,11 @@ TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
|
|
|
17
17
|
|
|
18
18
|
async def get_tinybird_local_client(path: Optional[str] = None) -> TinyB:
|
|
19
19
|
"""Get a Tinybird client connected to the local environment."""
|
|
20
|
-
config = await
|
|
20
|
+
config = await get_tinybird_local_config(path)
|
|
21
21
|
return config.get_client(host=TB_LOCAL_HOST)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
async def
|
|
24
|
+
async def get_tinybird_local_config(path: Optional[str] = None) -> CLIConfig:
|
|
25
25
|
"""Craft a client config with a workspace name based on the path of the project files
|
|
26
26
|
|
|
27
27
|
It uses the tokens from tinybird local
|
tinybird/tb/modules/mock.py
CHANGED
|
@@ -24,7 +24,7 @@ from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
|
24
24
|
default="Use the datasource schema to generate sample data",
|
|
25
25
|
help="Extra context to use for data generation",
|
|
26
26
|
)
|
|
27
|
-
@click.option("--folder", type=str, default=
|
|
27
|
+
@click.option("--folder", type=str, default=os.getcwd(), help="Folder where datafiles will be placed")
|
|
28
28
|
@coro
|
|
29
29
|
async def mock(datasource: str, rows: int, prompt: str, folder: str) -> None:
|
|
30
30
|
"""Load sample data into a Data Source.
|
|
@@ -67,11 +67,10 @@ async def mock(datasource: str, rows: int, prompt: str, folder: str) -> None:
|
|
|
67
67
|
except Exception:
|
|
68
68
|
click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
|
|
69
69
|
return
|
|
70
|
-
|
|
71
|
-
llm = LLM(user_token=user_token, client=user_client)
|
|
70
|
+
llm = LLM(user_token=user_token, host=user_client.host)
|
|
72
71
|
tb_client = await get_tinybird_local_client(os.path.abspath(folder))
|
|
73
72
|
prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
|
|
74
|
-
response =
|
|
73
|
+
response = llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
|
|
75
74
|
sql = extract_xml(response, "sql")
|
|
76
75
|
if os.environ.get("TB_DEBUG", "") != "":
|
|
77
76
|
logging.debug(sql)
|
tinybird/tb/modules/pipe.py
CHANGED
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
# - But please, **do not** interleave utility functions and command definitions.
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
-
import os
|
|
8
7
|
import re
|
|
9
|
-
from pathlib import Path
|
|
10
8
|
from typing import Dict, List, Optional, Tuple
|
|
11
9
|
|
|
12
10
|
import click
|
|
@@ -14,7 +12,6 @@ import humanfriendly
|
|
|
14
12
|
from click import Context
|
|
15
13
|
|
|
16
14
|
from tinybird.client import AuthNoTokenException, DoesNotExistException, TinyB
|
|
17
|
-
from tinybird.config import DEFAULT_API_HOST
|
|
18
15
|
from tinybird.tb.modules.cli import cli
|
|
19
16
|
from tinybird.tb.modules.common import (
|
|
20
17
|
coro,
|
|
@@ -22,8 +19,7 @@ from tinybird.tb.modules.common import (
|
|
|
22
19
|
echo_safe_humanfriendly_tables_format_smart_table,
|
|
23
20
|
wait_job,
|
|
24
21
|
)
|
|
25
|
-
from tinybird.tb.modules.datafile.
|
|
26
|
-
from tinybird.tb.modules.datafile.common import PipeNodeTypes, PipeTypes, get_name_version
|
|
22
|
+
from tinybird.tb.modules.datafile.common import PipeTypes, get_name_version
|
|
27
23
|
from tinybird.tb.modules.exceptions import CLIPipeException
|
|
28
24
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
29
25
|
|
|
@@ -46,37 +42,6 @@ def pipe_sink(ctx: Context) -> None:
|
|
|
46
42
|
"""Sink Pipe commands"""
|
|
47
43
|
|
|
48
44
|
|
|
49
|
-
@pipe.command(
|
|
50
|
-
name="generate",
|
|
51
|
-
short_help="Generates a pipe file based on a sql query. Example: tb pipe generate my_pipe 'select * from existing_datasource'",
|
|
52
|
-
)
|
|
53
|
-
@click.argument("name")
|
|
54
|
-
@click.argument("query")
|
|
55
|
-
@click.option("--force", is_flag=True, default=False, help="Override existing files")
|
|
56
|
-
@click.pass_context
|
|
57
|
-
def generate_pipe(ctx: click.Context, name: str, query: str, force: bool):
|
|
58
|
-
pipefile = f"""
|
|
59
|
-
NODE endpoint
|
|
60
|
-
DESCRIPTION >
|
|
61
|
-
Generated from the command line
|
|
62
|
-
SQL >
|
|
63
|
-
{query}
|
|
64
|
-
|
|
65
|
-
"""
|
|
66
|
-
base = Path("endpoints")
|
|
67
|
-
if not base.exists():
|
|
68
|
-
base = Path()
|
|
69
|
-
f = base / (f"{name}.pipe")
|
|
70
|
-
if not f.exists() or force:
|
|
71
|
-
with open(f"{f}", "w") as file:
|
|
72
|
-
file.write(pipefile)
|
|
73
|
-
click.echo(FeedbackManager.success_generated_pipe(file=f))
|
|
74
|
-
else:
|
|
75
|
-
raise CLIPipeException(
|
|
76
|
-
FeedbackManager.error_exception(error=f"File {f} already exists, use --force to override")
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
45
|
@pipe.command(name="stats")
|
|
81
46
|
@click.argument("pipes", nargs=-1)
|
|
82
47
|
@click.option(
|
|
@@ -288,224 +253,6 @@ async def pipe_populate(
|
|
|
288
253
|
await wait_job(cl, job_id, job_url, "Populating")
|
|
289
254
|
|
|
290
255
|
|
|
291
|
-
@pipe.command(name="unlink")
|
|
292
|
-
@click.argument("pipe_name_or_id")
|
|
293
|
-
@click.argument("node_uid", default=None, required=False)
|
|
294
|
-
@click.pass_context
|
|
295
|
-
@coro
|
|
296
|
-
async def pipe_unlink_output_node(
|
|
297
|
-
ctx: click.Context,
|
|
298
|
-
pipe_name_or_id: str,
|
|
299
|
-
node_uid: Optional[str] = None,
|
|
300
|
-
):
|
|
301
|
-
"""Unlink the output of a pipe. Works for Materialized Views, Copy Pipes, and Sinks."""
|
|
302
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
303
|
-
|
|
304
|
-
try:
|
|
305
|
-
pipe = await client.pipe(pipe_name_or_id)
|
|
306
|
-
|
|
307
|
-
if pipe["type"] not in [PipeTypes.MATERIALIZED, PipeTypes.COPY, PipeTypes.DATA_SINK]:
|
|
308
|
-
raise CLIPipeException(FeedbackManager.error_unlinking_pipe_not_linked(pipe=pipe_name_or_id))
|
|
309
|
-
|
|
310
|
-
if pipe["type"] == PipeTypes.MATERIALIZED:
|
|
311
|
-
click.echo(FeedbackManager.info_unlinking_materialized_pipe(pipe=pipe["name"]))
|
|
312
|
-
|
|
313
|
-
if not node_uid:
|
|
314
|
-
for node in pipe["nodes"]:
|
|
315
|
-
if "materialized" in node and node["materialized"] is not None:
|
|
316
|
-
node_uid = node["id"]
|
|
317
|
-
break
|
|
318
|
-
|
|
319
|
-
if not node_uid:
|
|
320
|
-
raise CLIPipeException(FeedbackManager.error_unlinking_pipe_not_linked(pipe=pipe_name_or_id))
|
|
321
|
-
else:
|
|
322
|
-
await client.pipe_unlink_materialized(pipe["name"], node_uid)
|
|
323
|
-
click.echo(FeedbackManager.success_pipe_unlinked(pipe=pipe["name"]))
|
|
324
|
-
|
|
325
|
-
if pipe["type"] == PipeTypes.COPY:
|
|
326
|
-
click.echo(FeedbackManager.info_unlinking_copy_pipe(pipe=pipe["name"]))
|
|
327
|
-
|
|
328
|
-
if not node_uid:
|
|
329
|
-
for node in pipe["nodes"]:
|
|
330
|
-
if node["node_type"] == "copy":
|
|
331
|
-
node_uid = node["id"]
|
|
332
|
-
break
|
|
333
|
-
|
|
334
|
-
if not node_uid:
|
|
335
|
-
raise CLIPipeException(FeedbackManager.error_unlinking_pipe_not_linked(pipe=pipe_name_or_id))
|
|
336
|
-
else:
|
|
337
|
-
await client.pipe_remove_copy(pipe["name"], node_uid)
|
|
338
|
-
click.echo(FeedbackManager.success_pipe_unlinked(pipe=pipe["name"]))
|
|
339
|
-
|
|
340
|
-
if pipe["type"] == PipeTypes.DATA_SINK:
|
|
341
|
-
click.echo(FeedbackManager.info_unlinking_sink_pipe(pipe=pipe["name"]))
|
|
342
|
-
|
|
343
|
-
if not node_uid:
|
|
344
|
-
for node in pipe["nodes"]:
|
|
345
|
-
if node["node_type"] == "sink":
|
|
346
|
-
node_uid = node["id"]
|
|
347
|
-
break
|
|
348
|
-
|
|
349
|
-
if not node_uid:
|
|
350
|
-
raise CLIPipeException(FeedbackManager.error_unlinking_pipe_not_linked(pipe=pipe_name_or_id))
|
|
351
|
-
else:
|
|
352
|
-
await client.pipe_remove_sink(pipe["name"], node_uid)
|
|
353
|
-
click.echo(FeedbackManager.success_pipe_unlinked(pipe=pipe["name"]))
|
|
354
|
-
|
|
355
|
-
if pipe["type"] == PipeTypes.STREAM:
|
|
356
|
-
click.echo(FeedbackManager.info_unlinking_stream_pipe(pipe=pipe["name"]))
|
|
357
|
-
node_uid = next((node["id"] for node in pipe["nodes"] if node["node_type"] == PipeNodeTypes.STREAM), None)
|
|
358
|
-
|
|
359
|
-
if not node_uid:
|
|
360
|
-
raise CLIPipeException(FeedbackManager.error_unlinking_pipe_not_linked(pipe=pipe_name_or_id))
|
|
361
|
-
else:
|
|
362
|
-
await client.pipe_remove_stream(pipe["name"], node_uid)
|
|
363
|
-
click.echo(FeedbackManager.success_pipe_unlinked(pipe=pipe["name"]))
|
|
364
|
-
|
|
365
|
-
except AuthNoTokenException:
|
|
366
|
-
raise
|
|
367
|
-
except Exception as e:
|
|
368
|
-
raise CLIPipeException(FeedbackManager.error_exception(error=e))
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
@pipe.command(name="append")
|
|
372
|
-
@click.argument("pipe_name_or_uid")
|
|
373
|
-
@click.argument("sql")
|
|
374
|
-
@click.pass_context
|
|
375
|
-
@coro
|
|
376
|
-
async def pipe_append_node(
|
|
377
|
-
ctx: click.Context,
|
|
378
|
-
pipe_name_or_uid: str,
|
|
379
|
-
sql: str,
|
|
380
|
-
):
|
|
381
|
-
"""Append a node to a pipe"""
|
|
382
|
-
|
|
383
|
-
client = ctx.ensure_object(dict)["client"]
|
|
384
|
-
try:
|
|
385
|
-
res = await client.pipe_append_node(pipe_name_or_uid, sql)
|
|
386
|
-
click.echo(
|
|
387
|
-
FeedbackManager.success_node_changed(
|
|
388
|
-
pipe_name_or_uid=pipe_name_or_uid, node_name=res["name"], node_id=res["id"]
|
|
389
|
-
)
|
|
390
|
-
)
|
|
391
|
-
except DoesNotExistException:
|
|
392
|
-
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name_or_uid))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async def common_pipe_publish_node(ctx: click.Context, pipe_name_or_id: str, node_uid: Optional[str] = None):
|
|
396
|
-
"""Change the published node of a pipe"""
|
|
397
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
398
|
-
host = ctx.ensure_object(dict)["config"].get("host", DEFAULT_API_HOST)
|
|
399
|
-
|
|
400
|
-
try:
|
|
401
|
-
pipe = await client.pipe(pipe_name_or_id)
|
|
402
|
-
if not node_uid:
|
|
403
|
-
node = pipe["nodes"][-1]["name"]
|
|
404
|
-
click.echo(FeedbackManager.info_using_node(node=node))
|
|
405
|
-
else:
|
|
406
|
-
node = node_uid
|
|
407
|
-
|
|
408
|
-
await client.pipe_set_endpoint(pipe_name_or_id, node)
|
|
409
|
-
click.echo(FeedbackManager.success_node_published(pipe=pipe_name_or_id, host=host))
|
|
410
|
-
except AuthNoTokenException:
|
|
411
|
-
raise
|
|
412
|
-
except DoesNotExistException:
|
|
413
|
-
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name_or_id))
|
|
414
|
-
except Exception as e:
|
|
415
|
-
raise CLIPipeException(FeedbackManager.error_exception(error=e))
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
@pipe.command(name="publish")
|
|
419
|
-
@click.argument("pipe_name_or_id")
|
|
420
|
-
@click.argument("node_uid", default=None, required=False)
|
|
421
|
-
@click.pass_context
|
|
422
|
-
@coro
|
|
423
|
-
async def pipe_publish_node(
|
|
424
|
-
ctx: click.Context,
|
|
425
|
-
pipe_name_or_id: str,
|
|
426
|
-
node_uid: Optional[str] = None,
|
|
427
|
-
):
|
|
428
|
-
"""Change the published node of a pipe"""
|
|
429
|
-
|
|
430
|
-
await common_pipe_publish_node(ctx, pipe_name_or_id, node_uid)
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
@pipe.command(name="unpublish")
|
|
434
|
-
@click.argument("pipe_name_or_id")
|
|
435
|
-
@click.argument("node_uid", default=None, required=False)
|
|
436
|
-
@click.pass_context
|
|
437
|
-
@coro
|
|
438
|
-
async def pipe_unpublish_node(
|
|
439
|
-
ctx: click.Context,
|
|
440
|
-
pipe_name_or_id: str,
|
|
441
|
-
node_uid: Optional[str] = None,
|
|
442
|
-
):
|
|
443
|
-
"""Unpublish the endpoint of a pipe"""
|
|
444
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
445
|
-
host = ctx.ensure_object(dict)["config"].get("host", DEFAULT_API_HOST)
|
|
446
|
-
|
|
447
|
-
try:
|
|
448
|
-
pipe = await client.pipe(pipe_name_or_id)
|
|
449
|
-
|
|
450
|
-
if not pipe["endpoint"]:
|
|
451
|
-
raise CLIPipeException(FeedbackManager.error_remove_no_endpoint())
|
|
452
|
-
|
|
453
|
-
if not node_uid:
|
|
454
|
-
node = pipe["endpoint"]
|
|
455
|
-
click.echo(FeedbackManager.info_using_node(node=node))
|
|
456
|
-
else:
|
|
457
|
-
node = node_uid
|
|
458
|
-
|
|
459
|
-
await client.pipe_remove_endpoint(pipe_name_or_id, node)
|
|
460
|
-
click.echo(FeedbackManager.success_node_unpublished(pipe=pipe_name_or_id, host=host))
|
|
461
|
-
except AuthNoTokenException:
|
|
462
|
-
raise
|
|
463
|
-
except DoesNotExistException:
|
|
464
|
-
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name_or_id))
|
|
465
|
-
except Exception as e:
|
|
466
|
-
raise CLIPipeException(FeedbackManager.error_exception(error=e))
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
@pipe.command(name="set_endpoint")
|
|
470
|
-
@click.argument("pipe_name_or_id")
|
|
471
|
-
@click.argument("node_uid", default=None, required=False)
|
|
472
|
-
@click.pass_context
|
|
473
|
-
@coro
|
|
474
|
-
async def pipe_published_node(
|
|
475
|
-
ctx: click.Context,
|
|
476
|
-
pipe_name_or_id: str,
|
|
477
|
-
node_uid: Optional[str] = None,
|
|
478
|
-
no_live_warning: bool = False,
|
|
479
|
-
):
|
|
480
|
-
"""Same as 'publish', change the published node of a pipe"""
|
|
481
|
-
|
|
482
|
-
await common_pipe_publish_node(ctx, pipe_name_or_id, node_uid)
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
@pipe.command(name="rm")
|
|
486
|
-
@click.argument("pipe_name_or_id")
|
|
487
|
-
@click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
|
|
488
|
-
@click.pass_context
|
|
489
|
-
@coro
|
|
490
|
-
async def pipe_delete(ctx: click.Context, pipe_name_or_id: str, yes: bool):
|
|
491
|
-
"""Delete a pipe. pipe_name_or_id can be either a Pipe name or id in the Workspace or a local path to a .pipe file"""
|
|
492
|
-
|
|
493
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
494
|
-
|
|
495
|
-
file_path = pipe_name_or_id
|
|
496
|
-
if os.path.exists(file_path):
|
|
497
|
-
result = await process_file(file_path, client)
|
|
498
|
-
pipe_name_or_id = result[0]["name"]
|
|
499
|
-
|
|
500
|
-
if yes or click.confirm(FeedbackManager.warning_confirm_delete_pipe(pipe=pipe_name_or_id)):
|
|
501
|
-
try:
|
|
502
|
-
await client.pipe_delete(pipe_name_or_id)
|
|
503
|
-
except DoesNotExistException:
|
|
504
|
-
raise CLIPipeException(FeedbackManager.error_pipe_does_not_exist(pipe=pipe_name_or_id))
|
|
505
|
-
|
|
506
|
-
click.echo(FeedbackManager.success_delete_pipe(pipe=pipe_name_or_id))
|
|
507
|
-
|
|
508
|
-
|
|
509
256
|
@pipe.command(name="token_read")
|
|
510
257
|
@click.argument("pipe_name")
|
|
511
258
|
@click.pass_context
|
tinybird/tb/modules/shell.py
CHANGED
|
@@ -196,6 +196,14 @@ class Shell:
|
|
|
196
196
|
history=self.history,
|
|
197
197
|
)
|
|
198
198
|
|
|
199
|
+
@property
|
|
200
|
+
def datasources(self):
|
|
201
|
+
return [Path(f).stem for f in glob.glob(f"{self.folder}/**/*.datasource", recursive=True)]
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def pipes(self):
|
|
205
|
+
return [Path(f).stem for f in glob.glob(f"{self.folder}/**/*.pipe", recursive=True)]
|
|
206
|
+
|
|
199
207
|
def get_history(self):
|
|
200
208
|
try:
|
|
201
209
|
history_file = os.path.expanduser("~/.tb_history")
|
|
@@ -222,7 +230,6 @@ class Shell:
|
|
|
222
230
|
line = argline.strip()
|
|
223
231
|
if not line:
|
|
224
232
|
return
|
|
225
|
-
|
|
226
233
|
# Implement the command logic here
|
|
227
234
|
# Replace do_* methods with equivalent logic:
|
|
228
235
|
command_parts = line.split(maxsplit=1)
|