tinybird 0.0.1.dev15__tar.gz → 0.0.1.dev17__tar.gz

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 (94) hide show
  1. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/PKG-INFO +1 -1
  2. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/client.py +3 -1
  3. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/feedback_manager.py +12 -1
  4. {tinybird-0.0.1.dev15/tinybird/tb/modules → tinybird-0.0.1.dev17/tinybird}/prompts.py +26 -64
  5. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/cli.py +2 -1
  6. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/build.py +37 -153
  7. tinybird-0.0.1.dev17/tinybird/tb/modules/build_shell.py +149 -0
  8. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/cli.py +9 -87
  9. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/common.py +7 -98
  10. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/config.py +0 -10
  11. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/create.py +82 -8
  12. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/build_pipe.py +1 -1
  13. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datasource.py +0 -10
  14. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/llm.py +35 -3
  15. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/local.py +2 -46
  16. tinybird-0.0.1.dev17/tinybird/tb/modules/local_common.py +54 -0
  17. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/mock.py +1 -1
  18. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/pipe.py +0 -4
  19. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/test.py +23 -12
  20. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/PKG-INFO +1 -1
  21. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/SOURCES.txt +3 -2
  22. tinybird-0.0.1.dev15/tinybird/tb/modules/branch.py +0 -1023
  23. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/setup.cfg +0 -0
  24. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/__cli__.py +0 -0
  25. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/ch_utils/constants.py +0 -0
  26. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/ch_utils/engine.py +0 -0
  27. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/check_pypi.py +0 -0
  28. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/config.py +0 -0
  29. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/connectors.py +0 -0
  30. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/context.py +0 -0
  31. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/datafile.py +0 -0
  32. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/datatypes.py +0 -0
  33. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/git_settings.py +0 -0
  34. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/sql.py +0 -0
  35. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/sql_template.py +0 -0
  36. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/sql_template_fmt.py +0 -0
  37. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/sql_toolset.py +0 -0
  38. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/syncasync.py +0 -0
  39. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/auth.py +0 -0
  40. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/cicd.py +0 -0
  41. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/connection.py +0 -0
  42. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/build.py +0 -0
  43. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/build_common.py +0 -0
  44. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/build_datasource.py +0 -0
  45. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/common.py +0 -0
  46. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/diff.py +0 -0
  47. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/exceptions.py +0 -0
  48. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/fixture.py +0 -0
  49. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/format_common.py +0 -0
  50. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/format_datasource.py +0 -0
  51. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/format_pipe.py +0 -0
  52. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/parse_datasource.py +0 -0
  53. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/parse_pipe.py +0 -0
  54. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/pipe_checker.py +0 -0
  55. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/datafile/pull.py +0 -0
  56. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/exceptions.py +0 -0
  57. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/fmt.py +0 -0
  58. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/job.py +0 -0
  59. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/login.py +0 -0
  60. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/regions.py +0 -0
  61. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/table.py +0 -0
  62. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/tag.py +0 -0
  63. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/telemetry.py +0 -0
  64. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/tinyunit/tinyunit.py +0 -0
  65. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/tinyunit/tinyunit_lib.py +0 -0
  66. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/token.py +0 -0
  67. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/workspace.py +0 -0
  68. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb/modules/workspace_members.py +0 -0
  69. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli.py +0 -0
  70. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/auth.py +0 -0
  71. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/branch.py +0 -0
  72. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/cicd.py +0 -0
  73. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/cli.py +0 -0
  74. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/common.py +0 -0
  75. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/config.py +0 -0
  76. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/connection.py +0 -0
  77. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/datasource.py +0 -0
  78. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/exceptions.py +0 -0
  79. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/fmt.py +0 -0
  80. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/job.py +0 -0
  81. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/pipe.py +0 -0
  82. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/regions.py +0 -0
  83. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/tag.py +0 -0
  84. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/telemetry.py +0 -0
  85. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/test.py +0 -0
  86. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  87. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  88. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/workspace.py +0 -0
  89. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  90. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird/tornado_template.py +0 -0
  91. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/dependency_links.txt +0 -0
  92. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/entry_points.txt +0 -0
  93. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/requires.txt +0 -0
  94. {tinybird-0.0.1.dev15 → tinybird-0.0.1.dev17}/tinybird.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird
3
- Version: 0.0.1.dev15
3
+ Version: 0.0.1.dev17
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -188,7 +188,9 @@ class TinyB:
188
188
  if response.status_code == 599:
189
189
  raise TimeoutException("timeout")
190
190
  if "Content-Type" in response.headers and (
191
- response.headers["Content-Type"] == "text/plain" or "text/csv" in response.headers["Content-Type"]
191
+ response.headers["Content-Type"] == "text/plain"
192
+ or "text/csv" in response.headers["Content-Type"]
193
+ or "ndjson" in response.headers["Content-Type"]
192
194
  ):
193
195
  return response.content.decode("utf-8")
194
196
  if response.status_code >= 400 and response.status_code not in [400, 403, 404, 409, 429]:
@@ -1,5 +1,6 @@
1
1
  from collections import namedtuple
2
2
  from typing import Any, Callable
3
+ import os
3
4
 
4
5
  FeedbackMessage = namedtuple("FeedbackMessage", "message")
5
6
 
@@ -26,6 +27,16 @@ def print_message(message: str, color: str = bcolors.ENDC) -> Callable[..., str]
26
27
  def error_message(message: str) -> Callable[..., str]:
27
28
  return print_message(f"\n** {message}", bcolors.FAIL)
28
29
 
30
+ def error_exception(message: Exception) -> Callable[..., str]:
31
+ def formatter(**kwargs: Any) -> str:
32
+ color = bcolors.FAIL
33
+ if isinstance(kwargs.get('error', ''), Exception) and os.environ.get('TB_DEBUG', '') != '':
34
+ import traceback
35
+ stack_trace = f"Traceback:\n{''.join(traceback.format_tb(kwargs['error'].__traceback__))}"
36
+ return f"{color}{message.format(**kwargs)}{stack_trace}{bcolors.ENDC}"
37
+ return f"{color}{message.format(**kwargs)}{bcolors.ENDC}"
38
+ return formatter
39
+
29
40
 
30
41
  def simple_error_message(message: str) -> Callable[..., str]:
31
42
  return print_message(f"{message}", bcolors.FAIL)
@@ -56,7 +67,7 @@ def gray_message(message: str) -> Callable[..., str]:
56
67
 
57
68
 
58
69
  class FeedbackManager:
59
- error_exception = error_message("{error}")
70
+ error_exception = error_exception("{error}")
60
71
  simple_error_exception = simple_error_message("{error}")
61
72
  error_exception_trace = error_message("{error}\n** Trace:\n{trace}")
62
73
  error_notoken = error_message(
@@ -76,68 +76,7 @@ SQL >
76
76
  </instructions>
77
77
  """
78
78
 
79
- create_test_calls_prompt = """
80
- You are a Tinybird expert. You will be given a pipe endpoint containing different nodes with SQL and Tinybird templating syntax. You will generate URLs to test it with different parameters combinations.
81
-
82
- <test>
83
- <test_1>:
84
- name: <test_name_1>
85
- description: <description_1>
86
- parameters: <url_encoded_parameters_1>
87
- <test_2>:
88
- name: <test_name_2>
89
- description: <description_2>
90
- parameters: <url_encoded_parameters_2>
91
- </test>
92
- <instructions>
93
- - The test name must be unique.
94
- - The test command must be a valid Tinybird command that can be run in the terminal.
95
- - The test command can have as many parameters as are needed to test the pipe.
96
- - The parameter within Tinybird templating syntax looks like this one {{String(my_param_name, default_value)}}.
97
- - If there are no parameters in the , you can omit parametrs and generate a single test command.
98
- - Extra context: {context}
99
- </instructions>
100
- """
101
-
102
- sample_data_prompt = """Your role is to generate sample data for a given data source. A data source is a table in Tinybird. You will be given a schema for the data source, and you will need to generate a set of rows that fit that schema.
103
-
104
- The schema will be provided to you at the end of this prompt in the following format:
105
-
106
- ```
107
- SCHEMA >
108
- `span_id` String `json:$.span_id`,
109
- `operation_id` String `json:$.operation_name`,
110
- `duration` Nullable(Float32) `json:$.duration`,
111
- ```
112
-
113
- In the example above, `span_id` are column names, String and Float32 are column types, and `json:$.duration` is a JSON path expression that points to the duration value in the JSON data. Use this information to generate valid data.
114
-
115
- You will generate {row_count} rows of data in JSON format, newlines separated. Your reponse will only contain the {row_count} rows of data, without any other text. Do not format the data in any way, and do not place it inside any markers for a code block of any kind. Most importantly, do not use backticks on the first or last line or anywhere else in the response. This is extremely important, ensure that your response doesn't include any backticks.
116
-
117
- Remember that for a JSON to be valid, there can't be a trailing comma after the last value in an object.
118
-
119
- Make sure to include all columns in the schema, and use valid JSON. You may leave some columns empty only if the column type allows it (e.g. Nullable(String)). Use realistic values for the data, and make sure to include a variety of values in each column.
120
-
121
- For columns that represent timestamps, use values that are close to the current date and time. The current date and time is {current_datetime}"""
122
-
123
- sample_data_with_errors_prompt = """Your role is to generate incorrect sample data for a given data source. A data source is a table in Tinybird. You will be given a schema for the data source, and you will need to generate a set of rows that mostly fit that schema, but with some errors.
124
-
125
- The schema will be provided to you at the end of this prompt in the following format:
126
-
127
- ```
128
- SCHEMA >
129
- `span_id` String `json:$.span_id`,
130
- `operation_id` String `json:$.operation_name`,
131
- `duration` Nullable(Float32) `json:$.duration`,
132
- ```
133
-
134
- In the example above, `span_id` are column names, String and Float32 are column types, and `json:$.duration` is a JSON path expression that points to the duration value in the JSON data. Use this information to generate valid data.
135
-
136
- You will generate {row_count} rows of data in JSON format, newlines separated. Your reponse will be a single string containing all {row_count} rows, without any other text. Make sure to include all columns in the schema, and use valid JSON. Use realistic values for the data, and make sure to include a variety of values in each column. In every set of {row_count} rows, there should be at least one row that has an error. The error could be in any column, and it could be a type error (e.g. a string where a number is expected), or it could be an error in the structure of the JSON (e.g. a value in the wrong place in the document).
137
-
138
- For columns that represent timestamps, use values that are close to the current date and time. The current date and time is {current_datetime}"""
139
-
140
- sample_data_sql_prompt = """
79
+ generate_sql_mock_data_prompt = """
141
80
  Given the schema for a Tinybird datasource, return a can you create a clickhouse sql query to generate some random data that matches that schema.
142
81
 
143
82
  Response format MUST be just a valid clickhouse sql query.
@@ -164,13 +103,13 @@ SELECT
164
103
  rand() % 50 AS pvp_kills, -- Random PvP kills between 0 and 49
165
104
  rand() % 200 AS quest_completions, -- Random quest completions between 0 and 199
166
105
  now() - rand() % 86400 AS timestamp -- Random timestamp within the last day
167
- FROM numbers({row_count})
106
+ FROM numbers({rows})
168
107
 
169
108
  # Instructions:
170
109
 
171
110
  - The query MUST return a random sample of data that matches the schema.
172
111
  - The query MUST return a valid clickhouse sql query.
173
- - The query MUST return a sample of EXACTLY {row_count} rows.
112
+ - The query MUST return a sample of EXACTLY {rows} rows.
174
113
  - The query MUST be valid for clickhouse and Tinybird.
175
114
  - Return JUST the sql query, without any other text or symbols.
176
115
  - Do NOT include ```clickhouse or ```sql or any other wrapping text.
@@ -182,3 +121,26 @@ FROM numbers({row_count})
182
121
  {context}
183
122
 
184
123
  """
124
+
125
+ create_test_calls_prompt = """
126
+ You are a Tinybird expert. You will be given a pipe endpoint containing different nodes with SQL and Tinybird templating syntax. You will generate URLs to test it with different parameters combinations.
127
+
128
+ <test>
129
+ <test_1>:
130
+ name: <test_name_1>
131
+ description: <description_1>
132
+ parameters: <url_encoded_parameters_1>
133
+ <test_2>:
134
+ name: <test_name_2>
135
+ description: <description_2>
136
+ parameters: <url_encoded_parameters_2>
137
+ </test>
138
+ <instructions>
139
+ - The test name must be unique.
140
+ - The test command must be a valid Tinybird command that can be run in the terminal.
141
+ - The test command can have as many parameters as are needed to test the pipe.
142
+ - The parameter within Tinybird templating syntax looks like this one {{String(my_param_name, default_value)}}.
143
+ - If there are no parameters in the , you can omit parametrs and generate a single test command.
144
+ - Extra context: {context}
145
+ </instructions>
146
+ """
@@ -5,7 +5,6 @@ if sys.platform == "win32":
5
5
  asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
6
6
 
7
7
  import tinybird.tb.modules.auth
8
- import tinybird.tb.modules.branch
9
8
  import tinybird.tb.modules.build
10
9
  import tinybird.tb.modules.cli
11
10
  import tinybird.tb.modules.common
@@ -14,10 +13,12 @@ import tinybird.tb.modules.create
14
13
  import tinybird.tb.modules.datasource
15
14
  import tinybird.tb.modules.fmt
16
15
  import tinybird.tb.modules.job
16
+ import tinybird.tb.modules.local
17
17
  import tinybird.tb.modules.login
18
18
  import tinybird.tb.modules.mock
19
19
  import tinybird.tb.modules.pipe
20
20
  import tinybird.tb.modules.tag
21
+ import tinybird.tb.modules.test
21
22
  import tinybird.tb.modules.token
22
23
  import tinybird.tb.modules.workspace
23
24
  import tinybird.tb.modules.workspace_members
@@ -1,26 +1,20 @@
1
1
  import asyncio
2
- import cmd
3
2
  import os
4
- import random
5
- import subprocess
6
- import sys
7
3
  import threading
8
4
  import time
9
5
  from pathlib import Path
10
6
  from typing import Any, Awaitable, Callable, List, Union
11
7
 
12
8
  import click
13
- import humanfriendly
14
9
  from click import Context
15
10
  from watchdog.events import FileSystemEventHandler
16
11
  from watchdog.observers import Observer
17
12
 
18
13
  import tinybird.context as context
19
- from tinybird.syncasync import async_to_sync
20
- from tinybird.client import TinyB, AuthNoTokenException
21
- from tinybird.tb.modules.common import CLIException
14
+ from tinybird.client import TinyB
22
15
  from tinybird.config import FeatureFlags
23
- from tinybird.feedback_manager import FeedbackManager, bcolors
16
+ from tinybird.feedback_manager import FeedbackManager
17
+ from tinybird.tb.modules.build_shell import BuildShell, print_table_formatted
24
18
  from tinybird.tb.modules.cli import cli
25
19
  from tinybird.tb.modules.common import coro, push_data
26
20
  from tinybird.tb.modules.datafile.build import folder_build
@@ -29,54 +23,14 @@ from tinybird.tb.modules.datafile.exceptions import ParseException
29
23
  from tinybird.tb.modules.datafile.fixture import build_fixture_name, get_fixture_dir
30
24
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
31
25
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
32
- from tinybird.tb.modules.local import get_tinybird_local_client
33
- from tinybird.tb.modules.table import format_table
34
-
35
-
36
- class BuildShell(cmd.Cmd):
37
- prompt = "\n\001\033[1;32m\002TB > \001\033[0m\002"
38
-
39
- def __init__(self, folder: str, client: TinyB):
40
- super().__init__()
41
- self.folder = folder
42
- self.client = client
43
- def do_exit(self, arg):
44
- sys.exit(0)
45
-
46
- def do_quit(self, arg):
47
- sys.exit(0)
48
-
49
- def default(self, argline):
50
- click.echo("")
51
- if argline.startswith("tb build"):
52
- click.echo(FeedbackManager.error(message="Build command is already running"))
53
- else:
54
- arg_stripped = argline.strip().lower()
55
- if not arg_stripped:
56
- return
57
- if arg_stripped.startswith("tb"):
58
- extra_args = f" --folder {self.folder}" if arg_stripped.startswith("tb mock") else ""
59
- subprocess.run(arg_stripped + extra_args, shell=True, text=True)
60
- elif arg_stripped.startswith("with") or arg_stripped.startswith("select"):
61
- try:
62
- run_sql(self.client, argline)
63
- except Exception as e:
64
- click.echo(FeedbackManager.error(message=str(e)))
65
-
66
- elif arg_stripped.startswith("mock "):
67
- subprocess.run(f"tb {arg_stripped} --folder {self.folder}", shell=True, text=True)
68
- else:
69
- click.echo(FeedbackManager.error(message="Invalid command"))
70
-
71
- def reprint_prompt(self):
72
- self.stdout.write(self.prompt)
73
- self.stdout.flush()
26
+ from tinybird.tb.modules.local_common import get_tinybird_local_client
74
27
 
75
28
 
76
29
  class FileChangeHandler(FileSystemEventHandler):
77
- def __init__(self, filenames: List[str], process: Callable[[List[str]], None]):
30
+ def __init__(self, filenames: List[str], process: Callable[[List[str]], None], build_ok: bool):
78
31
  self.filenames = filenames
79
32
  self.process = process
33
+ self.build_ok = build_ok
80
34
 
81
35
  def on_modified(self, event: Any) -> None:
82
36
  is_not_vendor = "vendor/" not in event.src_path
@@ -88,7 +42,9 @@ class FileChangeHandler(FileSystemEventHandler):
88
42
  filename = event.src_path.split("/")[-1]
89
43
  click.echo(FeedbackManager.highlight(message=f"\n\n⟲ Changes detected in {filename}\n"))
90
44
  try:
91
- self.process([event.src_path])
45
+ to_process = [event.src_path] if self.build_ok else self.filenames
46
+ self.process(to_process)
47
+ self.build_ok = True
92
48
  except Exception as e:
93
49
  click.echo(FeedbackManager.error_exception(error=e))
94
50
 
@@ -98,6 +54,7 @@ def watch_files(
98
54
  process: Union[Callable[[List[str]], None], Callable[[List[str]], Awaitable[None]]],
99
55
  shell: BuildShell,
100
56
  folder: str,
57
+ build_ok: bool,
101
58
  ) -> None:
102
59
  # Handle both sync and async process functions
103
60
  async def process_wrapper(files: List[str]) -> None:
@@ -115,7 +72,7 @@ def watch_files(
115
72
  )
116
73
  shell.reprint_prompt()
117
74
 
118
- event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)))
75
+ event_handler = FileChangeHandler(filenames, lambda f: asyncio.run(process_wrapper(f)), build_ok)
119
76
  observer = Observer()
120
77
 
121
78
  observer.schedule(event_handler, path=folder, recursive=True)
@@ -131,44 +88,6 @@ def watch_files(
131
88
  observer.join()
132
89
 
133
90
 
134
-
135
- def run_sql(client: TinyB, query, rows_limit=20):
136
- try:
137
- q = query.strip()
138
- if q.startswith("insert"):
139
- click.echo(FeedbackManager.info_append_data())
140
- raise CLIException(FeedbackManager.error_invalid_query())
141
- if q.startswith("delete"):
142
- raise CLIException(FeedbackManager.error_invalid_query())
143
-
144
- # fuck my life
145
- def run_query_in_thread():
146
- loop = asyncio.new_event_loop()
147
- asyncio.set_event_loop(loop)
148
- try:
149
- return loop.run_until_complete(client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON"))
150
- finally:
151
- loop.close()
152
-
153
- # Run the query in a separate thread
154
- import concurrent.futures
155
- with concurrent.futures.ThreadPoolExecutor() as executor:
156
- res = executor.submit(run_query_in_thread).result()
157
-
158
- except AuthNoTokenException:
159
- raise
160
- except Exception as e:
161
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
162
-
163
- if isinstance(res, dict) and "error" in res:
164
- raise CLIException(FeedbackManager.error_exception(error=res["error"]))
165
-
166
- if isinstance(res, dict) and "data" in res and res["data"]:
167
- print_table_formatted(res, 'QUERY')
168
- else:
169
- click.echo(FeedbackManager.info_no_rows())
170
-
171
-
172
91
  @cli.command()
173
92
  @click.option(
174
93
  "--folder",
@@ -195,7 +114,6 @@ async def build(
195
114
  ignore_sql_errors = FeatureFlags.ignore_sql_errors()
196
115
  context.disable_template_security_validation.set(True)
197
116
  is_internal = has_internal_datafiles(folder)
198
-
199
117
  folder_path = os.path.abspath(folder)
200
118
  tb_client = await get_tinybird_local_client(folder_path)
201
119
 
@@ -228,30 +146,31 @@ async def build(
228
146
  is_internal=is_internal,
229
147
  watch=watch,
230
148
  )
231
-
232
- filename = filenames[0]
233
- if filename.endswith(".ndjson"):
234
- fixture_path = Path(filename)
235
- name = "_".join(fixture_path.stem.split("_")[:-1])
236
- ds_path = Path(folder) / "datasources" / f"{name}.datasource"
237
- if ds_path.exists():
238
- await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
239
-
240
- if watch:
241
- if filename.endswith(".datasource"):
242
- ds_path = Path(filename)
243
- name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
244
- fixture_path = get_fixture_dir() / f"{name}.ndjson"
245
- if fixture_path.exists():
246
- await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
247
- if not filename.endswith(".ndjson"):
248
- await build_and_print_resource(tb_client, filename)
149
+ if len(filenames) > 0:
150
+ filename = filenames[0]
151
+ if filename.endswith(".ndjson"):
152
+ fixture_path = Path(filename)
153
+ name = "_".join(fixture_path.stem.split("_")[:-1])
154
+ ds_path = Path(folder) / "datasources" / f"{name}.datasource"
155
+ if ds_path.exists():
156
+ await append_datasource({}, tb_client, name, str(fixture_path), silent=True)
157
+
158
+ if watch:
159
+ if filename.endswith(".datasource"):
160
+ ds_path = Path(filename)
161
+ name = build_fixture_name(filename, ds_path.stem, ds_path.read_text())
162
+ fixture_path = get_fixture_dir() / f"{name}.ndjson"
163
+ if fixture_path.exists():
164
+ await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
165
+ if not filename.endswith(".ndjson"):
166
+ await build_and_print_resource(tb_client, filename)
249
167
 
250
168
  datafiles = get_project_filenames(folder)
251
169
  fixtures = get_project_fixtures(folder)
252
170
  filenames = datafiles + fixtures
253
171
 
254
172
  async def build_once(filenames: List[str]):
173
+ ok = False
255
174
  try:
256
175
  click.echo("⚡ Building project...\n")
257
176
  time_start = time.time()
@@ -266,15 +185,20 @@ async def build(
266
185
  if fixture_path.exists():
267
186
  await append_datasource({}, tb_client, ds_path.stem, str(fixture_path), silent=True)
268
187
  click.echo(FeedbackManager.success(message=f"\n✓ Build completed in {elapsed_time:.1f}s\n"))
188
+ ok = True
269
189
  except Exception as e:
270
190
  click.echo(FeedbackManager.error(message=str(e)))
191
+ ok = False
192
+ return ok
271
193
 
272
- await build_once(filenames)
194
+ build_ok = await build_once(filenames)
273
195
 
274
196
  if watch:
275
197
  shell = BuildShell(folder=folder, client=tb_client)
276
198
  click.echo(FeedbackManager.highlight(message="◎ Watching for changes..."))
277
- watcher_thread = threading.Thread(target=watch_files, args=(filenames, process, shell, folder), daemon=True)
199
+ watcher_thread = threading.Thread(
200
+ target=watch_files, args=(filenames, process, shell, folder, build_ok), daemon=True
201
+ )
278
202
  watcher_thread.start()
279
203
  shell.cmdloop()
280
204
 
@@ -286,46 +210,6 @@ async def build_and_print_resource(tb_client: TinyB, filename: str):
286
210
  res = await tb_client.query(f"SELECT * FROM {name} FORMAT JSON", pipeline=pipeline)
287
211
  print_table_formatted(res, name)
288
212
 
289
- def print_table_formatted(res: dict, name: str):
290
- rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
291
- rebuild_index = random.randint(0, len(rebuild_colors) - 1)
292
- rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
293
- data = []
294
- limit = 5
295
- for d in res["data"][:5]:
296
- data.append(d.values())
297
- meta = res["meta"]
298
- row_count = res.get("rows", 0)
299
- stats = res.get("statistics", {})
300
- elapsed = stats.get("elapsed", 0)
301
- cols = len(meta)
302
- try:
303
-
304
- def print_message(message: str, color=bcolors.CGREY):
305
- return f"{color}{message}{bcolors.ENDC}"
306
-
307
- table = format_table(data, meta)
308
- colored_char = print_message("│", rebuild_color)
309
- table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
310
- click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
311
- click.echo(colored_char)
312
- click.echo(table_with_marker)
313
- click.echo(colored_char)
314
- rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
315
- bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
316
- elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
317
- stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
318
- rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
319
- click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
320
- click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
321
- except ValueError as exc:
322
- if str(exc) == "max() arg is an empty sequence":
323
- click.echo("------------")
324
- click.echo("Empty")
325
- click.echo("------------")
326
- else:
327
- raise exc
328
-
329
213
 
330
214
  async def append_datasource(
331
215
  ctx: click.Context,
@@ -0,0 +1,149 @@
1
+ import asyncio
2
+ import cmd
3
+ import random
4
+ import subprocess
5
+ import sys
6
+
7
+ import click
8
+ import humanfriendly
9
+
10
+ from tinybird.client import TinyB
11
+ from tinybird.feedback_manager import FeedbackManager, bcolors
12
+ from tinybird.tb.modules.exceptions import CLIException
13
+ from tinybird.tb.modules.table import format_table
14
+
15
+
16
+ class BuildShell(cmd.Cmd):
17
+ prompt = "\n\001\033[1;32m\002tb > \001\033[0m\002"
18
+
19
+ def __init__(self, folder: str, client: TinyB):
20
+ super().__init__()
21
+ self.folder = folder
22
+ self.client = client
23
+
24
+ def do_exit(self, arg):
25
+ sys.exit(0)
26
+
27
+ def do_quit(self, arg):
28
+ sys.exit(0)
29
+
30
+ def do_build(self, arg):
31
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
32
+
33
+ def do_auth(self, arg):
34
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
35
+
36
+ def do_workspace(self, arg):
37
+ click.echo(FeedbackManager.error(message=f"'tb {arg}' command is not available in watch mode"))
38
+
39
+ def do_mock(self, arg):
40
+ subprocess.run(f"tb mock {arg} --folder {self.folder}", shell=True, text=True)
41
+
42
+ def do_tb(self, arg):
43
+ click.echo("")
44
+ arg = arg.strip().lower()
45
+ if arg.startswith("build"):
46
+ self.do_build(arg)
47
+ elif arg.startswith("auth"):
48
+ self.do_auth(arg)
49
+ elif arg.startswith("workspace"):
50
+ self.do_workspace(arg)
51
+ elif arg.startswith("mock"):
52
+ self.do_mock(arg)
53
+ else:
54
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
55
+
56
+ def default(self, argline):
57
+ click.echo("")
58
+ arg = argline.strip().lower()
59
+ if not arg:
60
+ return
61
+ if arg.startswith("with") or arg.startswith("select"):
62
+ try:
63
+ self.run_sql(argline)
64
+ except Exception as e:
65
+ click.echo(FeedbackManager.error(message=str(e)))
66
+ else:
67
+ subprocess.run(f"tb --local {arg}", shell=True, text=True)
68
+
69
+ def reprint_prompt(self):
70
+ self.stdout.write(self.prompt)
71
+ self.stdout.flush()
72
+
73
+ def run_sql(self, query, rows_limit=20):
74
+ try:
75
+ q = query.strip()
76
+ if q.startswith("insert"):
77
+ click.echo(FeedbackManager.info_append_data())
78
+ raise CLIException(FeedbackManager.error_invalid_query())
79
+ if q.startswith("delete"):
80
+ raise CLIException(FeedbackManager.error_invalid_query())
81
+
82
+ # fuck my life
83
+ def run_query_in_thread():
84
+ loop = asyncio.new_event_loop()
85
+ asyncio.set_event_loop(loop)
86
+ try:
87
+ return loop.run_until_complete(
88
+ self.client.query(f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT JSON")
89
+ )
90
+ finally:
91
+ loop.close()
92
+
93
+ # Run the query in a separate thread
94
+ import concurrent.futures
95
+
96
+ with concurrent.futures.ThreadPoolExecutor() as executor:
97
+ res = executor.submit(run_query_in_thread).result()
98
+
99
+ except Exception as e:
100
+ raise CLIException(FeedbackManager.error_exception(error=str(e)))
101
+
102
+ if isinstance(res, dict) and "error" in res:
103
+ raise CLIException(FeedbackManager.error_exception(error=res["error"]))
104
+
105
+ if isinstance(res, dict) and "data" in res and res["data"]:
106
+ print_table_formatted(res, "QUERY")
107
+ else:
108
+ click.echo(FeedbackManager.info_no_rows())
109
+
110
+
111
+ def print_table_formatted(res: dict, name: str):
112
+ rebuild_colors = [bcolors.FAIL, bcolors.OKBLUE, bcolors.WARNING, bcolors.OKGREEN, bcolors.HEADER]
113
+ rebuild_index = random.randint(0, len(rebuild_colors) - 1)
114
+ rebuild_color = rebuild_colors[rebuild_index % len(rebuild_colors)]
115
+ data = []
116
+ limit = 5
117
+ for d in res["data"][:5]:
118
+ data.append(d.values())
119
+ meta = res["meta"]
120
+ row_count = res.get("rows", 0)
121
+ stats = res.get("statistics", {})
122
+ elapsed = stats.get("elapsed", 0)
123
+ cols = len(meta)
124
+ try:
125
+
126
+ def print_message(message: str, color=bcolors.CGREY):
127
+ return f"{color}{message}{bcolors.ENDC}"
128
+
129
+ table = format_table(data, meta)
130
+ colored_char = print_message("│", rebuild_color)
131
+ table_with_marker = "\n".join(f"{colored_char} {line}" for line in table.split("\n"))
132
+ click.echo(f"\n{colored_char} {print_message('⚡', rebuild_color)} Running {name}")
133
+ click.echo(colored_char)
134
+ click.echo(table_with_marker)
135
+ click.echo(colored_char)
136
+ rows_read = humanfriendly.format_number(stats.get("rows_read", 0))
137
+ bytes_read = humanfriendly.format_size(stats.get("bytes_read", 0))
138
+ elapsed = humanfriendly.format_timespan(elapsed) if elapsed >= 1 else f"{elapsed * 1000:.2f}ms"
139
+ stats_message = f"» {bytes_read} ({rows_read} rows x {cols} cols) in {elapsed}"
140
+ rows_message = f"» Showing {limit} first rows" if row_count > limit else "» Showing all rows"
141
+ click.echo(f"{colored_char} {print_message(stats_message, bcolors.OKGREEN)}")
142
+ click.echo(f"{colored_char} {print_message(rows_message, bcolors.CGREY)}")
143
+ except ValueError as exc:
144
+ if str(exc) == "max() arg is an empty sequence":
145
+ click.echo("------------")
146
+ click.echo("Empty")
147
+ click.echo("------------")
148
+ else:
149
+ raise exc