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.

@@ -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
@@ -1,19 +1,16 @@
1
- from copy import deepcopy
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.user_client = deepcopy(client)
14
- self.user_client.token = user_token
10
+ self.host = host
11
+ self.user_token = user_token
15
12
 
16
- async def ask(self, system_prompt: str, prompt: Optional[str] = None) -> str:
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 = await self.user_client._req(
34
- "/v0/llm",
35
- method="POST",
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", "")
@@ -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
+ """
@@ -30,7 +30,10 @@ def start_tinybird_local(
30
30
 
31
31
  if (
32
32
  pull_show_prompt
33
- and click.prompt(FeedbackManager.warning(message="△ New version detected, download? [y/N]")).lower() == "y"
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 get_client_config_for_build(path)
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 get_client_config_for_build(path: Optional[str] = None) -> CLIConfig:
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
@@ -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=".", help="Folder where datafiles will be placed")
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
- user_client.token = user_token
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 = await llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
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)
@@ -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.build import process_file
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
@@ -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)