tinybird 0.0.1.dev67__py3-none-any.whl → 0.0.1.dev69__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.

Files changed (36) hide show
  1. tinybird/ch_utils/engine.py +2 -4
  2. tinybird/context.py +0 -1
  3. tinybird/prompts.py +3 -3
  4. tinybird/sql_template.py +1 -3
  5. tinybird/sql_toolset.py +3 -3
  6. tinybird/tb/__cli__.py +2 -2
  7. tinybird/tb/modules/auth.py +1 -1
  8. tinybird/tb/modules/cli.py +5 -5
  9. tinybird/tb/modules/common.py +9 -9
  10. tinybird/tb/modules/create.py +42 -80
  11. tinybird/tb/modules/datafile/build_common.py +1 -1
  12. tinybird/tb/modules/datafile/build_datasource.py +1 -1
  13. tinybird/tb/modules/datafile/common.py +18 -3
  14. tinybird/tb/modules/datafile/pipe_checker.py +1 -1
  15. tinybird/tb/modules/datasource.py +2 -2
  16. tinybird/tb/modules/deployment.py +60 -11
  17. tinybird/tb/modules/endpoint.py +1 -1
  18. tinybird/tb/modules/fmt.py +1 -1
  19. tinybird/tb/modules/materialization.py +5 -5
  20. tinybird/tb/modules/mock.py +9 -38
  21. tinybird/tb/modules/pipe.py +1 -1
  22. tinybird/tb/modules/project.py +3 -3
  23. tinybird/tb/modules/shell.py +13 -21
  24. tinybird/tb/modules/test.py +39 -71
  25. tinybird/tb/modules/token.py +4 -4
  26. tinybird/tb/modules/watch.py +25 -12
  27. tinybird/tb/modules/workspace.py +6 -6
  28. tinybird/tb/modules/workspace_members.py +6 -6
  29. tinybird/tb_cli_modules/common.py +2 -2
  30. tinybird/tornado_template.py +2 -1
  31. tinybird-0.0.1.dev69.dist-info/METADATA +73 -0
  32. {tinybird-0.0.1.dev67.dist-info → tinybird-0.0.1.dev69.dist-info}/RECORD +35 -35
  33. {tinybird-0.0.1.dev67.dist-info → tinybird-0.0.1.dev69.dist-info}/WHEEL +1 -1
  34. tinybird-0.0.1.dev67.dist-info/METADATA +0 -64
  35. {tinybird-0.0.1.dev67.dist-info → tinybird-0.0.1.dev69.dist-info}/entry_points.txt +0 -0
  36. {tinybird-0.0.1.dev67.dist-info → tinybird-0.0.1.dev69.dist-info}/top_level.txt +0 -0
@@ -44,12 +44,15 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
44
44
  if candidate_deployment.get("live"):
45
45
  click.echo(FeedbackManager.error(message="Candidate deployment is already live"))
46
46
  else:
47
- click.echo(FeedbackManager.success(message="Promoting deployment"))
47
+ click.echo(FeedbackManager.success(message="Setting candidate deployment as live"))
48
48
 
49
49
  TINYBIRD_API_URL = f"{host}/v1/deployments/{candidate_deployment.get('id')}/set-live"
50
50
  r = requests.post(TINYBIRD_API_URL, headers=headers)
51
51
  result = r.json()
52
52
  logging.debug(json.dumps(result, indent=2))
53
+ if result.get("error"):
54
+ click.echo(FeedbackManager.error(message=result.get("error")))
55
+ sys.exit(1)
53
56
 
54
57
  click.echo(FeedbackManager.success(message="Removing old deployment"))
55
58
 
@@ -57,6 +60,11 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
57
60
  r = requests.delete(TINYBIRD_API_URL, headers=headers)
58
61
  result = r.json()
59
62
  logging.debug(json.dumps(result, indent=2))
63
+ if result.get("error"):
64
+ click.echo(FeedbackManager.error(message=result.get("error")))
65
+ sys.exit(1)
66
+
67
+ click.echo(FeedbackManager.success(message="Deployment promotion successfully started"))
60
68
 
61
69
  if wait:
62
70
  while True:
@@ -67,12 +75,11 @@ def promote_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
67
75
 
68
76
  last_deployment = result.get("deployment")
69
77
  if last_deployment.get("status") == "deleted":
78
+ click.echo(FeedbackManager.success(message="Deployment promoted successfully"))
70
79
  break
71
80
 
72
81
  time.sleep(5)
73
82
 
74
- click.echo(FeedbackManager.success(message="Deployment promoted successfully"))
75
-
76
83
 
77
84
  # TODO(eclbg): This logic should be in the server, and there should be a dedicated endpoint for rolling back a
78
85
  # deployment
@@ -109,6 +116,9 @@ def rollback_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
109
116
  r = requests.post(TINYBIRD_API_URL, headers=headers)
110
117
  result = r.json()
111
118
  logging.debug(json.dumps(result, indent=2))
119
+ if result.get("error"):
120
+ click.echo(FeedbackManager.error(message=result.get("error")))
121
+ sys.exit(1)
112
122
 
113
123
  click.echo(FeedbackManager.success(message="Removing current deployment"))
114
124
 
@@ -116,6 +126,11 @@ def rollback_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
116
126
  r = requests.delete(TINYBIRD_API_URL, headers=headers)
117
127
  result = r.json()
118
128
  logging.debug(json.dumps(result, indent=2))
129
+ if result.get("error"):
130
+ click.echo(FeedbackManager.error(message=result.get("error")))
131
+ sys.exit(1)
132
+
133
+ click.echo(FeedbackManager.success(message="Deployment rollback successfully started"))
119
134
 
120
135
  if wait:
121
136
  while True:
@@ -126,11 +141,10 @@ def rollback_deployment(host: Optional[str], headers: dict, wait: bool) -> None:
126
141
 
127
142
  current_deployment = result.get("deployment")
128
143
  if current_deployment.get("status") == "deleted":
144
+ click.echo(FeedbackManager.success(message="Deployment rolled back successfully"))
129
145
  break
130
146
  time.sleep(5)
131
147
 
132
- click.echo(FeedbackManager.success(message="Deployment rolled back successfully"))
133
-
134
148
 
135
149
  @cli.group(name="deployment")
136
150
  def deployment_group() -> None:
@@ -159,12 +173,18 @@ def deployment_group() -> None:
159
173
  default=False,
160
174
  help="Validate the deployment before creating it. Disabled by default.",
161
175
  )
176
+ @click.option(
177
+ "--allow-remove-datasources/--no-allow-remove-datasources",
178
+ is_flag=True,
179
+ default=False,
180
+ help="Allow removing datasources. Disabled by default.",
181
+ )
162
182
  @click.pass_context
163
- def deployment_create(ctx: click.Context, wait: bool, auto: bool, check: bool) -> None:
183
+ def deployment_create(ctx: click.Context, wait: bool, auto: bool, check: bool, allow_remove_datasources: bool) -> None:
164
184
  """
165
185
  Validate and deploy the project server side.
166
186
  """
167
- create_deployment(ctx, wait, auto, check)
187
+ create_deployment(ctx, wait, auto, check, allow_remove_datasources)
168
188
 
169
189
 
170
190
  @deployment_group.command(name="ls")
@@ -187,6 +207,9 @@ def deployment_ls(ctx: click.Context) -> None:
187
207
  columns = ["ID", "Status", "Created at", "Live"]
188
208
  table = []
189
209
  for deployment in result.get("deployments"):
210
+ if deployment.get("id") == "0":
211
+ continue
212
+
190
213
  table.append(
191
214
  [
192
215
  deployment.get("id"),
@@ -258,15 +281,27 @@ def deployment_rollback(ctx: click.Context, wait: bool) -> None:
258
281
  default=False,
259
282
  help="Validate the deployment before creating it. Disabled by default.",
260
283
  )
284
+ @click.option(
285
+ "--allow-remove-datasources/--no-allow-remove-datasources",
286
+ is_flag=True,
287
+ default=False,
288
+ help="Allow removing datasources. Disabled by default.",
289
+ )
261
290
  @click.pass_context
262
- def deploy(ctx: click.Context, wait: bool, auto: bool, check: bool) -> None:
291
+ def deploy(ctx: click.Context, wait: bool, auto: bool, check: bool, allow_remove_datasources: bool) -> None:
263
292
  """
264
293
  Deploy the project.
265
294
  """
266
- create_deployment(ctx, wait, auto, check)
295
+ create_deployment(ctx, wait, auto, check, allow_remove_datasources)
267
296
 
268
297
 
269
- def create_deployment(ctx: click.Context, wait: bool, auto: bool, check: Optional[bool] = None) -> None:
298
+ def create_deployment(
299
+ ctx: click.Context,
300
+ wait: bool,
301
+ auto: bool,
302
+ check: Optional[bool] = None,
303
+ allow_remove_datasources: Optional[bool] = None,
304
+ ) -> None:
270
305
  # TODO: This code is duplicated in build_server.py
271
306
  # Should be refactored to be shared
272
307
  MULTIPART_BOUNDARY_DATA_PROJECT = "data_project://"
@@ -297,21 +332,31 @@ def create_deployment(ctx: click.Context, wait: bool, auto: bool, check: Optiona
297
332
  if check:
298
333
  click.echo(FeedbackManager.highlight(message="\n» Validating deployment...\n"))
299
334
  params["check"] = "true"
335
+ if allow_remove_datasources:
336
+ params["allow_remove_datasources"] = "true"
300
337
  r = requests.post(TINYBIRD_API_URL, files=files, headers=HEADERS, params=params)
301
338
  result = r.json()
302
339
  logging.debug(json.dumps(result, indent=2))
303
340
 
304
341
  if check:
305
342
  print_changes(result, project)
343
+ feedback = result.get("deployment", {}).get("feedback", [])
344
+ for f in feedback:
345
+ click.echo(
346
+ FeedbackManager.warning(message=f"△ {f.get('level')}: {f.get('resource')}: {f.get('message')}")
347
+ )
348
+
306
349
  status = result.get("result")
307
350
  if status == "success":
308
351
  click.echo(FeedbackManager.success(message="\n✓ Deployment is valid"))
309
352
  else:
310
353
  click.echo(FeedbackManager.error(message="\n✗ Deployment is not valid"))
354
+
311
355
  return
312
356
 
313
357
  deploy_result = result.get("result")
314
358
  if deploy_result == "success":
359
+ print_changes(result, project)
315
360
  click.echo(FeedbackManager.success(message="Deployment submitted successfully"))
316
361
  deployment = result.get("deployment")
317
362
  feedback = deployment.get("feedback", [])
@@ -394,4 +439,8 @@ def print_changes(result: dict, project: Project) -> None:
394
439
  for p in deployment.get("deleted_pipe_names", []):
395
440
  resources.append(["deleted", p, project.get_resource_path(p, "pipe")])
396
441
 
397
- echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=columns)
442
+ if resources:
443
+ click.echo(FeedbackManager.highlight(message="\n» Changes to be deployed...\n"))
444
+ echo_safe_humanfriendly_tables_format_smart_table(resources, column_names=columns)
445
+ else:
446
+ click.echo(FeedbackManager.highlight(message="\n» No changes to be deployed\n"))
@@ -29,7 +29,7 @@ def endpoint(ctx):
29
29
 
30
30
 
31
31
  @endpoint.command(name="ls")
32
- @click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. eg --match _test")
32
+ @click.option("--match", default=None, help="Retrieve any resource matching the pattern. For example, --match _test")
33
33
  @click.option(
34
34
  "--format",
35
35
  "format_",
@@ -58,7 +58,7 @@ async def fmt(
58
58
  elif (".pipe" in extensions) or (".incl" in extensions):
59
59
  result = await format_pipe(filename, line_length, skip_eval=True)
60
60
  else:
61
- click.echo("Unsupported file type. Supported files types are: .pipe, .incl and .datasource")
61
+ click.echo("Unsupported file type. Supported files types are: .pipe and .datasource")
62
62
  return None
63
63
 
64
64
  if diff:
@@ -23,7 +23,7 @@ def materialization(ctx):
23
23
 
24
24
 
25
25
  @materialization.command(name="ls")
26
- @click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. eg --match _test")
26
+ @click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. For example, --match _test")
27
27
  @click.option(
28
28
  "--format",
29
29
  "format_",
@@ -79,16 +79,16 @@ async def materialization_ls(ctx: click.Context, match: str, format_: str):
79
79
  "--sql-condition",
80
80
  type=str,
81
81
  default=None,
82
- help="Populate with a SQL condition to be applied to the trigger Data Source of the Materialized View. For instance, `--sql-condition='date == toYYYYMM(now())'` it'll populate taking all the rows from the trigger Data Source which `date` is the current month. Use it together with --populate. --sql-condition is not taken into account if the --subset param is present. Including in the ``sql_condition`` any column present in the Data Source ``engine_sorting_key`` will make the populate job process less data.",
82
+ help="Populate with a SQL condition to be applied to the trigger data source of the materialized view. For instance, `--sql-condition='date == toYYYYMM(now())'` it'll populate taking all the rows from the trigger data source which `date` is the current month. Use it together with --populate. --sql-condition is not taken into account if the --subset param is present. Including in the ``sql_condition`` any column present in the data source ``engine_sorting_key`` will make the populate job process less data.",
83
83
  )
84
84
  @click.option(
85
- "--truncate", is_flag=True, default=False, help="Truncates the materialized Data Source before populating it."
85
+ "--truncate", is_flag=True, default=False, help="Truncates the materialized data source before populating it."
86
86
  )
87
87
  @click.option(
88
88
  "--unlink-on-populate-error",
89
89
  is_flag=True,
90
90
  default=False,
91
- help="If the populate job fails the Materialized View is unlinked and new data won't be ingested in the Materialized View. First time a populate job fails, the Materialized View is always unlinked.",
91
+ help="If the populate job fails the materialized view is unlinked and new data won't be ingested in the materialized view. First time a populate job fails, the materialized view is always unlinked.",
92
92
  )
93
93
  @click.option(
94
94
  "--wait",
@@ -107,7 +107,7 @@ async def pipe_populate(
107
107
  unlink_on_populate_error: bool,
108
108
  wait: bool,
109
109
  ):
110
- """Populate the result of a Materialized Node into the target Materialized View"""
110
+ """Populate the result of a Materialized Node into the target materialized view"""
111
111
  cl = create_tb_client(ctx)
112
112
 
113
113
  pipe = await cl.pipe(pipe_name)
@@ -2,7 +2,6 @@ import glob
2
2
  import logging
3
3
  import os
4
4
  from pathlib import Path
5
- from typing import Optional
6
5
 
7
6
  import click
8
7
 
@@ -27,10 +26,9 @@ from tinybird.tb.modules.project import Project
27
26
  default="",
28
27
  help="Extra context to use for data generation",
29
28
  )
30
- @click.option("--skip", is_flag=True, default=False, help="Skip following up on the generated data")
31
29
  @click.pass_context
32
30
  @coro
33
- async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, skip: bool) -> None:
31
+ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str) -> None:
34
32
  """Generate sample data for a datasource.
35
33
 
36
34
  Args:
@@ -80,44 +78,17 @@ async def mock(ctx: click.Context, datasource: str, rows: int, prompt: str, skip
80
78
  return
81
79
  llm = LLM(user_token=user_token, host=user_client.host)
82
80
  prompt = f"<datasource_schema>{datasource_content}</datasource_schema>\n<user_input>{prompt}</user_input>"
83
- iterations = 0
84
- history = ""
85
- fixture_path: Optional[Path] = None
86
81
  sql = ""
87
- while iterations < 10:
88
- feedback = ""
89
- if iterations > 0:
90
- feedback = click.prompt("\nFollow-up instructions or continue", default="continue")
91
- if iterations > 0 and (not feedback or feedback in ("continue", "ok", "exit", "quit", "q")):
92
- break
93
- else:
94
- if iterations > 0:
95
- if fixture_path:
96
- fixture_path.unlink()
97
- fixture_path = None
98
- click.echo(FeedbackManager.highlight(message=f"\n» Creating fixture for {datasource_name}..."))
99
82
 
100
- response = llm.ask(system_prompt=mock_prompt(rows, feedback, history), prompt=prompt)
101
- sql = extract_xml(response, "sql")
102
- result = await tb_client.query(f"{sql} FORMAT JSON")
103
- data = result.get("data", [])[:rows]
104
- fixture_path = persist_fixture(datasource_name, data, folder)
105
- click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}.ndjson created"))
83
+ response = llm.ask(system_prompt=mock_prompt(rows), prompt=prompt)
84
+ sql = extract_xml(response, "sql")
85
+ result = await tb_client.query(f"{sql} FORMAT JSON")
86
+ data = result.get("data", [])[:rows]
87
+ persist_fixture(datasource_name, data, folder)
88
+ click.echo(FeedbackManager.info(message=f"✓ /fixtures/{datasource_name}.ndjson created"))
106
89
 
107
- if os.environ.get("TB_DEBUG", "") != "":
108
- logging.debug(sql)
109
-
110
- history = (
111
- history
112
- + f"""
113
- <result_iteration_{iterations}>
114
- {response}
115
- </result_iteration_{iterations}>
116
- """
117
- )
118
- if skip:
119
- break
120
- iterations += 1
90
+ if os.environ.get("TB_DEBUG", "") != "":
91
+ logging.debug(sql)
121
92
 
122
93
  click.echo(FeedbackManager.success(message=f"✓ Sample data for {datasource_name} created with {rows} rows"))
123
94
 
@@ -27,7 +27,7 @@ def pipe(ctx):
27
27
 
28
28
 
29
29
  @pipe.command(name="ls")
30
- @click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. eg --match _test")
30
+ @click.option("--match", default=None, help="Retrieve any resourcing matching the pattern. For example, --match _test")
31
31
  @click.option(
32
32
  "--format",
33
33
  "format_",
@@ -40,17 +40,17 @@ class Project:
40
40
 
41
41
  def get_vendor_files(self) -> List[str]:
42
42
  vendor_files: List[str] = []
43
- for project_file in glob.glob(f"{self.vendor_path}/**/*.datasource", recursive=True):
43
+ for project_file in glob.glob(f"{self.vendor_path}/**/*.datasource", recursive=False):
44
44
  vendor_files.append(project_file)
45
45
  return vendor_files
46
46
 
47
47
  @property
48
48
  def datasources(self) -> List[str]:
49
- return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.datasource", recursive=True)])
49
+ return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.datasource", recursive=False)])
50
50
 
51
51
  @property
52
52
  def pipes(self) -> List[str]:
53
- return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=True)])
53
+ return sorted([Path(f).stem for f in glob.glob(f"{self.path}/**/*.pipe", recursive=False)])
54
54
 
55
55
  def get_pipe_datafile(self, filename: str) -> Optional[Datafile]:
56
56
  try:
@@ -24,10 +24,17 @@ from tinybird.tb.modules.table import format_table
24
24
  class DynamicCompleter(Completer):
25
25
  def __init__(self, project: Project):
26
26
  self.project = project
27
- self.static_commands = ["create", "mock", "test", "select"]
27
+ self.static_commands = [
28
+ "create",
29
+ "mock",
30
+ "test",
31
+ "select",
32
+ "datasource",
33
+ "pipe",
34
+ "endpoint",
35
+ "copy",
36
+ ]
28
37
  self.test_commands = ["create", "run", "update"]
29
- self.mock_flags = ["--prompt", "--rows"]
30
- self.common_rows = ["10", "50", "100", "500", "1000"]
31
38
  self.sql_keywords = ["select", "from", "where", "group by", "order by", "limit"]
32
39
 
33
40
  def get_completions(self, document, complete_event):
@@ -114,20 +121,6 @@ class DynamicCompleter(Completer):
114
121
  )
115
122
  return
116
123
 
117
- if len(words) == 3 or len(words) == 4:
118
- # After datasource or after a flag value, show available flags
119
- available_flags = [f for f in self.mock_flags if f not in words]
120
- for flag in available_flags:
121
- yield Completion(flag, start_position=0, display=flag, style="class:completion.cmd")
122
- return
123
-
124
- last_word = words[-1]
125
- if last_word == "--prompt":
126
- yield Completion('""', start_position=0, display='"Enter your prompt..."', style="class:completion.cmd")
127
- elif last_word == "--rows":
128
- for rows in self.common_rows:
129
- yield Completion(rows, start_position=0, display=rows, style="class:completion.cmd")
130
-
131
124
  def _handle_test_completions(self, words: List[str]):
132
125
  if len(words) == 1:
133
126
  for cmd in self.test_commands:
@@ -206,7 +199,6 @@ class Shell:
206
199
  self.project = project
207
200
  self.tb_client = tb_client
208
201
  self.prompt_message = "\ntb > "
209
- self.commands = ["create", "mock", "test", "tb", "select"]
210
202
  self.session: PromptSession = PromptSession(
211
203
  completer=DynamicCompleter(project),
212
204
  complete_style=CompleteStyle.COLUMN,
@@ -274,7 +266,7 @@ class Shell:
274
266
  def handle_mock(self, arg):
275
267
  if "mock" in arg.strip().lower():
276
268
  arg = arg.replace("mock", "")
277
- subprocess.run(f"tb --build mock {arg} --skip", shell=True, text=True)
269
+ subprocess.run(f"tb --build mock {arg}", shell=True, text=True)
278
270
 
279
271
  def handle_tb(self, argline):
280
272
  click.echo("")
@@ -290,7 +282,7 @@ class Shell:
290
282
  else:
291
283
  need_skip = ("mock", "test create", "create")
292
284
  if any(arg.startswith(cmd) for cmd in need_skip):
293
- argline = f"{argline} --skip"
285
+ argline = f"{argline}"
294
286
  subprocess.run(f"tb --build {argline}", shell=True, text=True)
295
287
 
296
288
  def default(self, argline):
@@ -305,7 +297,7 @@ class Shell:
305
297
  else:
306
298
  need_skip = ("mock", "test create", "create")
307
299
  if any(arg.startswith(cmd) for cmd in need_skip):
308
- argline = f"{argline} --skip"
300
+ argline = f"{argline}"
309
301
  subprocess.run(f"tb --build {argline}", shell=True, text=True)
310
302
 
311
303
  def run_sql(self, query, rows_limit=20):
@@ -66,14 +66,9 @@ def test(ctx: click.Context) -> None:
66
66
  @click.option(
67
67
  "--prompt", type=str, default="Create a test for the selected pipe", help="Prompt to be used to create the test"
68
68
  )
69
- @click.option(
70
- "--skip",
71
- is_flag=True,
72
- help="Skip the test creation process and only generate the test file",
73
- )
74
69
  @click.pass_context
75
70
  @coro
76
- async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, skip: bool) -> None:
71
+ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str) -> None:
77
72
  """
78
73
  Create a test for an existing pipe
79
74
  """
@@ -114,74 +109,47 @@ async def test_create(ctx: click.Context, name_or_filename: str, prompt: str, sk
114
109
  user_token = config.get_user_token()
115
110
  if not user_token:
116
111
  raise CLIException(FeedbackManager.error(message="No user token found"))
112
+
117
113
  llm = LLM(user_token=user_token, host=config.get_client().host)
114
+ click.echo(FeedbackManager.highlight(message=f"\n» Creating test for {pipe_name} endpoint..."))
115
+ response_llm = llm.ask(system_prompt=system_prompt, prompt=prompt)
116
+ response_xml = extract_xml(response_llm, "response")
117
+ tests_content = parse_xml(response_xml, "test")
118
118
 
119
- iterations = 0
120
- history = ""
121
- test_path: Optional[Path] = None
119
+ tests: List[Dict[str, Any]] = []
122
120
 
123
- while iterations < 10:
124
- feedback = ""
125
- if iterations > 0:
126
- feedback = click.prompt("\nFollow-up instructions or continue", default="continue")
127
- if iterations > 0 and (not feedback or feedback in ("continue", "ok", "exit", "quit", "q")):
128
- break
129
- else:
130
- if iterations > 0 and test_path:
131
- test_path.unlink()
132
- test_path = None
133
- click.echo(FeedbackManager.highlight(message=f"\n» Creating test for {pipe_name} endpoint..."))
134
-
135
- response_llm = llm.ask(system_prompt=system_prompt, prompt=prompt)
136
- response_xml = extract_xml(response_llm, "response")
137
- tests_content = parse_xml(response_xml, "test")
138
-
139
- tests: List[Dict[str, Any]] = []
140
-
141
- for test_content in tests_content:
142
- test: Dict[str, Any] = {}
143
- test["name"] = extract_xml(test_content, "name")
144
- test["description"] = extract_xml(test_content, "description")
145
- parameters_api = extract_xml(test_content, "parameters")
146
- test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
147
- test["expected_result"] = ""
148
-
149
- response = None
150
- try:
151
- response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
152
- except Exception:
153
- pass
154
-
155
- if response:
156
- if response.status_code >= 400:
157
- test["expected_http_status"] = response.status_code
158
- test["expected_result"] = response.json()["error"]
159
- else:
160
- if "expected_http_status" in test:
161
- del test["expected_http_status"]
162
- test["expected_result"] = response.text or ""
163
-
164
- tests.append(test)
165
-
166
- if len(tests) > 0:
167
- test_path = generate_test_file(pipe_name, tests, folder, mode="a")
168
- for test in tests:
169
- test_name = test["name"]
170
- click.echo(FeedbackManager.info(message=f"✓ {test_name} created"))
171
-
172
- history = (
173
- history
174
- + f"""
175
- <result_iteration_{iterations}>
176
- {response_xml}
177
- </result_iteration_{iterations}>
178
- """
179
- )
180
- if skip:
181
- break
182
- iterations += 1
121
+ for test_content in tests_content:
122
+ test: Dict[str, Any] = {}
123
+ test["name"] = extract_xml(test_content, "name")
124
+ test["description"] = extract_xml(test_content, "description")
125
+ parameters_api = extract_xml(test_content, "parameters")
126
+ test["parameters"] = parameters_api.split("?")[1] if "?" in parameters_api else parameters_api
127
+ test["expected_result"] = ""
128
+
129
+ response = None
130
+ try:
131
+ response = await get_pipe_data(client, pipe_name=pipe_name, test_params=test["parameters"])
132
+ except Exception:
133
+ pass
134
+
135
+ if response:
136
+ if response.status_code >= 400:
137
+ test["expected_http_status"] = response.status_code
138
+ test["expected_result"] = response.json()["error"]
183
139
  else:
184
- click.echo(FeedbackManager.info(message="* No tests created"))
140
+ if "expected_http_status" in test:
141
+ del test["expected_http_status"]
142
+ test["expected_result"] = response.text or ""
143
+
144
+ tests.append(test)
145
+
146
+ if len(tests) > 0:
147
+ generate_test_file(pipe_name, tests, folder, mode="a")
148
+ for test in tests:
149
+ test_name = test["name"]
150
+ click.echo(FeedbackManager.info(message=f"✓ {test_name} created"))
151
+ else:
152
+ click.echo(FeedbackManager.info(message="* No tests created"))
185
153
 
186
154
  click.echo(FeedbackManager.success(message="✓ Done!\n"))
187
155
  except Exception as e:
@@ -239,7 +207,7 @@ async def test_update(ctx: click.Context, pipe: str) -> None:
239
207
 
240
208
  @test.command(
241
209
  name="run",
242
- help="Run the test suite, a file, or a test.",
210
+ help="Run the test suite, a file, or a test",
243
211
  )
244
212
  @click.argument("name", nargs=-1)
245
213
  @click.pass_context
@@ -24,7 +24,7 @@ def token(ctx: Context) -> None:
24
24
 
25
25
 
26
26
  @token.command(name="ls")
27
- @click.option("--match", default=None, help="Retrieve any token matching the pattern. eg --match _test")
27
+ @click.option("--match", default=None, help="Retrieve any token matching the pattern. For example, --match _test")
28
28
  @click.pass_context
29
29
  @coro
30
30
  async def token_ls(
@@ -52,7 +52,7 @@ async def token_ls(
52
52
 
53
53
  @token.command(name="rm")
54
54
  @click.argument("token_id")
55
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
55
+ @click.option("--yes", is_flag=True, default=False, help="Don't ask for confirmation")
56
56
  @click.pass_context
57
57
  @coro
58
58
  async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
@@ -74,7 +74,7 @@ async def token_rm(ctx: Context, token_id: str, yes: bool) -> None:
74
74
 
75
75
  @token.command(name="refresh")
76
76
  @click.argument("token_id")
77
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
77
+ @click.option("--yes", is_flag=True, default=False, help="Don't ask for confirmation")
78
78
  @click.pass_context
79
79
  @coro
80
80
  async def token_refresh(ctx: Context, token_id: str, yes: bool) -> None:
@@ -347,4 +347,4 @@ async def create_static_token(ctx, name: str):
347
347
  except Exception as e:
348
348
  raise CLITokenException(FeedbackManager.error_exception(error=e))
349
349
 
350
- click.echo("The token has been generated successfully.")
350
+ click.echo("Token has been generated successfully.")
@@ -7,7 +7,7 @@ import click
7
7
  from watchdog.events import (
8
8
  DirDeletedEvent,
9
9
  FileDeletedEvent,
10
- PatternMatchingEventHandler,
10
+ FileSystemEventHandler,
11
11
  )
12
12
  from watchdog.observers import Observer
13
13
 
@@ -17,25 +17,23 @@ from tinybird.tb.modules.project import Project
17
17
  from tinybird.tb.modules.shell import Shell
18
18
 
19
19
 
20
- class WatchProjectHandler(PatternMatchingEventHandler):
20
+ class WatchProjectHandler(FileSystemEventHandler):
21
21
  def __init__(self, shell: Shell, project: Project, process: Callable):
22
22
  self.shell = shell
23
23
  self.project = project
24
24
  self.process = process
25
25
  self.datafiles = project.get_project_datafiles()
26
- super().__init__(
27
- patterns=[
28
- f"{project.path}/**/*.datasource",
29
- f"{project.path}/**/*.pipe",
30
- f"{project.path}/fixtures/*.ndjson",
31
- ],
32
- ignore_patterns=[f"{project.path}/vendor/"],
33
- )
26
+ super().__init__()
34
27
 
35
28
  def should_process(self, event: Any) -> Optional[str]:
36
29
  if event.is_directory:
37
30
  return None
38
31
 
32
+ valid_extensions = [".datasource", ".pipe", ".ndjson"]
33
+
34
+ if not any(event.src_path.endswith(ext) for ext in valid_extensions):
35
+ return None
36
+
39
37
  if os.path.exists(event.src_path):
40
38
  return event.src_path
41
39
 
@@ -91,13 +89,28 @@ class WatchProjectHandler(PatternMatchingEventHandler):
91
89
 
92
90
  return None
93
91
 
94
- def on_modified(self, event: Any) -> None:
92
+ def on_any_event(self, event):
93
+ if str(event.src_path).endswith("~"):
94
+ return None
95
+
96
+ if event.event_type == "modified":
97
+ self.modified(event)
98
+ elif event.event_type == "deleted":
99
+ self.deleted(event)
100
+
101
+ def created(self, event: Any) -> None:
102
+ if path := self.should_process(event):
103
+ filename = Path(path).name
104
+ click.echo(FeedbackManager.highlight(message=f"\n\n⟲ New file detected: {filename}\n"))
105
+ self._process(path)
106
+
107
+ def modified(self, event: Any) -> None:
95
108
  if path := self.should_process(event):
96
109
  filename = Path(path).name
97
110
  click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
98
111
  self._process(path)
99
112
 
100
- def on_deleted(self, event: Union[DirDeletedEvent, FileDeletedEvent]) -> None:
113
+ def deleted(self, event: Union[DirDeletedEvent, FileDeletedEvent]) -> None:
101
114
  filename = Path(str(event.src_path)).name
102
115
  if event.is_directory:
103
116
  click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Deleted directory: {filename}\n"))