tinybird 0.0.1.dev15__py3-none-any.whl → 0.0.1.dev17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinybird might be problematic. Click here for more details.
- tinybird/client.py +3 -1
- tinybird/feedback_manager.py +12 -1
- tinybird/{tb/modules/prompts.py → prompts.py} +26 -64
- tinybird/tb/cli.py +2 -1
- tinybird/tb/modules/build.py +37 -153
- tinybird/tb/modules/build_shell.py +149 -0
- tinybird/tb/modules/cli.py +9 -87
- tinybird/tb/modules/common.py +7 -98
- tinybird/tb/modules/config.py +0 -10
- tinybird/tb/modules/create.py +82 -8
- tinybird/tb/modules/datafile/build_pipe.py +1 -1
- tinybird/tb/modules/datasource.py +0 -10
- tinybird/tb/modules/llm.py +35 -3
- tinybird/tb/modules/local.py +2 -46
- tinybird/tb/modules/local_common.py +54 -0
- tinybird/tb/modules/mock.py +1 -1
- tinybird/tb/modules/pipe.py +0 -4
- tinybird/tb/modules/test.py +23 -12
- {tinybird-0.0.1.dev15.dist-info → tinybird-0.0.1.dev17.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev15.dist-info → tinybird-0.0.1.dev17.dist-info}/RECORD +23 -22
- tinybird/tb/modules/branch.py +0 -1023
- {tinybird-0.0.1.dev15.dist-info → tinybird-0.0.1.dev17.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev15.dist-info → tinybird-0.0.1.dev17.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev15.dist-info → tinybird-0.0.1.dev17.dist-info}/top_level.txt +0 -0
tinybird/tb/modules/cli.py
CHANGED
|
@@ -36,7 +36,6 @@ from tinybird.tb.modules.common import (
|
|
|
36
36
|
coro,
|
|
37
37
|
create_tb_client,
|
|
38
38
|
echo_safe_format_table,
|
|
39
|
-
folder_init,
|
|
40
39
|
get_current_main_workspace,
|
|
41
40
|
getenv_bool,
|
|
42
41
|
is_major_semver,
|
|
@@ -63,6 +62,7 @@ from tinybird.tb.modules.datafile.exceptions import (
|
|
|
63
62
|
from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
|
|
64
63
|
from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
|
|
65
64
|
from tinybird.tb.modules.datafile.pull import folder_pull
|
|
65
|
+
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
66
66
|
from tinybird.tb.modules.telemetry import add_telemetry_event
|
|
67
67
|
|
|
68
68
|
__old_click_echo = click.echo
|
|
@@ -80,7 +80,6 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
|
|
|
80
80
|
)
|
|
81
81
|
@click.option("--token", help="Use auth token, defaults to TB_TOKEN envvar, then to the .tinyb file")
|
|
82
82
|
@click.option("--host", help="Use custom host, defaults to TB_HOST envvar, then to https://api.tinybird.co")
|
|
83
|
-
@click.option("--semver", help="Semver of a Release to run the command. Example: 1.0.0", hidden=True)
|
|
84
83
|
@click.option("--gcp-project-id", help="The Google Cloud project ID", hidden=True)
|
|
85
84
|
@click.option(
|
|
86
85
|
"--gcs-bucket", help="The Google Cloud Storage bucket to write temp files when using the connectors", hidden=True
|
|
@@ -108,6 +107,7 @@ DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
|
|
|
108
107
|
"--with-headers", help="Flag to enable connector to export with headers", is_flag=True, default=False, hidden=True
|
|
109
108
|
)
|
|
110
109
|
@click.option("--show-tokens", is_flag=True, default=False, help="Enable the output of tokens")
|
|
110
|
+
@click.option("--local", is_flag=True, default=False, help="Run in local mode")
|
|
111
111
|
@click.version_option(version=VERSION)
|
|
112
112
|
@click.pass_context
|
|
113
113
|
@coro
|
|
@@ -116,7 +116,6 @@ async def cli(
|
|
|
116
116
|
debug: bool,
|
|
117
117
|
token: str,
|
|
118
118
|
host: str,
|
|
119
|
-
semver: str,
|
|
120
119
|
gcp_project_id: str,
|
|
121
120
|
gcs_bucket: str,
|
|
122
121
|
google_application_credentials: str,
|
|
@@ -131,13 +130,14 @@ async def cli(
|
|
|
131
130
|
sf_stage,
|
|
132
131
|
with_headers: bool,
|
|
133
132
|
show_tokens: bool,
|
|
133
|
+
local: bool,
|
|
134
134
|
) -> None:
|
|
135
135
|
"""
|
|
136
136
|
Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output.
|
|
137
137
|
"""
|
|
138
138
|
|
|
139
139
|
# We need to unpatch for our tests not to break
|
|
140
|
-
if show_tokens:
|
|
140
|
+
if show_tokens or local or ctx.invoked_subcommand == "build":
|
|
141
141
|
__unpatch_click_output()
|
|
142
142
|
else:
|
|
143
143
|
__patch_click_output()
|
|
@@ -153,9 +153,7 @@ async def cli(
|
|
|
153
153
|
config_temp.set_token(token)
|
|
154
154
|
if host:
|
|
155
155
|
config_temp.set_host(host)
|
|
156
|
-
if
|
|
157
|
-
config_temp.set_semver(semver)
|
|
158
|
-
if token or host or semver:
|
|
156
|
+
if token or host:
|
|
159
157
|
await try_update_config_with_remote(config_temp, auto_persist=False, raise_on_errors=False)
|
|
160
158
|
|
|
161
159
|
# Overwrite token and host with env vars manually, without resorting to click.
|
|
@@ -167,10 +165,8 @@ async def cli(
|
|
|
167
165
|
token = os.environ.get("TB_TOKEN", "")
|
|
168
166
|
if not host and "TB_HOST" in os.environ:
|
|
169
167
|
host = os.environ.get("TB_HOST", "")
|
|
170
|
-
if not semver and "TB_SEMVER" in os.environ:
|
|
171
|
-
semver = os.environ.get("TB_SEMVER", "")
|
|
172
168
|
|
|
173
|
-
config = await get_config(host, token
|
|
169
|
+
config = await get_config(host, token)
|
|
174
170
|
client = _get_tb_client(config.get("token", None), config["host"])
|
|
175
171
|
|
|
176
172
|
# If they have passed a token or host as paramter and it's different that record in .tinyb, refresh the workspace id
|
|
@@ -228,85 +224,14 @@ async def cli(
|
|
|
228
224
|
|
|
229
225
|
logging.debug("debug enabled")
|
|
230
226
|
|
|
231
|
-
ctx.ensure_object(dict)["client"] =
|
|
227
|
+
ctx.ensure_object(dict)["client"] = (
|
|
228
|
+
await get_tinybird_local_client() if local else _get_tb_client(config.get("token", None), config["host"])
|
|
229
|
+
)
|
|
232
230
|
|
|
233
231
|
for connector in SUPPORTED_CONNECTORS:
|
|
234
232
|
load_connector_config(ctx, connector, debug, check_uninstalled=True)
|
|
235
233
|
|
|
236
234
|
|
|
237
|
-
@cli.command()
|
|
238
|
-
@click.option(
|
|
239
|
-
"--generate-datasources",
|
|
240
|
-
is_flag=True,
|
|
241
|
-
default=False,
|
|
242
|
-
help="Generate datasources based on CSV, NDJSON and Parquet files in this folder",
|
|
243
|
-
)
|
|
244
|
-
@click.option(
|
|
245
|
-
"--folder",
|
|
246
|
-
default=None,
|
|
247
|
-
type=click.Path(exists=True, file_okay=False),
|
|
248
|
-
help="Folder where datafiles will be placed",
|
|
249
|
-
)
|
|
250
|
-
@click.option("-f", "--force", is_flag=True, default=False, help="Overrides existing files")
|
|
251
|
-
@click.option(
|
|
252
|
-
"-ir",
|
|
253
|
-
"--ignore-remote",
|
|
254
|
-
is_flag=True,
|
|
255
|
-
default=False,
|
|
256
|
-
help="Ignores remote files not present in the local data project on git init",
|
|
257
|
-
)
|
|
258
|
-
@click.option(
|
|
259
|
-
"--git",
|
|
260
|
-
is_flag=True,
|
|
261
|
-
default=False,
|
|
262
|
-
help="Init workspace with git releases. Generates CI/CD files for your git provider",
|
|
263
|
-
)
|
|
264
|
-
@click.option(
|
|
265
|
-
"--override-commit",
|
|
266
|
-
default=None,
|
|
267
|
-
help="Use this option to manually override the reference commit of your workspace. This is useful if a commit is not recognized in your git log, such as after a force push (git push -f).",
|
|
268
|
-
)
|
|
269
|
-
@click.option(
|
|
270
|
-
"--cicd", is_flag=True, default=False, help="Generates only CI/CD files for your git provider", hidden=True
|
|
271
|
-
)
|
|
272
|
-
@click.pass_context
|
|
273
|
-
@coro
|
|
274
|
-
async def init(
|
|
275
|
-
ctx: Context,
|
|
276
|
-
generate_datasources: bool,
|
|
277
|
-
folder: Optional[str],
|
|
278
|
-
force: bool,
|
|
279
|
-
ignore_remote: bool,
|
|
280
|
-
git: bool,
|
|
281
|
-
override_commit: Optional[str],
|
|
282
|
-
cicd: Optional[bool],
|
|
283
|
-
) -> None:
|
|
284
|
-
"""Initialize folder layout."""
|
|
285
|
-
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
286
|
-
config = CLIConfig.get_project_config()
|
|
287
|
-
if config.get("token") is None:
|
|
288
|
-
raise AuthNoTokenException
|
|
289
|
-
folder = folder if folder else getcwd()
|
|
290
|
-
|
|
291
|
-
workspaces: List[Dict[str, Any]] = (await client.user_workspaces_and_branches()).get("workspaces", [])
|
|
292
|
-
current_ws: Dict[str, Any] = next(
|
|
293
|
-
(workspace for workspace in workspaces if config and workspace.get("id", ".") == config.get("id", "..")), {}
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
if current_ws.get("is_branch"):
|
|
297
|
-
raise CLIException(FeedbackManager.error_not_allowed_in_branch())
|
|
298
|
-
|
|
299
|
-
await folder_init(client, folder, generate_datasources, generate_releases=True, force=force)
|
|
300
|
-
|
|
301
|
-
error = False
|
|
302
|
-
final_response = None
|
|
303
|
-
|
|
304
|
-
if final_response:
|
|
305
|
-
if error:
|
|
306
|
-
raise CLIException(final_response)
|
|
307
|
-
click.echo(final_response)
|
|
308
|
-
|
|
309
|
-
|
|
310
235
|
@cli.command()
|
|
311
236
|
@click.argument("filenames", type=click.Path(exists=True), nargs=-1, default=None)
|
|
312
237
|
@click.option("--debug", is_flag=True, default=False, help="Print internal representation")
|
|
@@ -1340,9 +1265,6 @@ async def deploy(
|
|
|
1340
1265
|
check_backfill_required = getenv_bool("TB_CHECK_BACKFILL_REQUIRED", True)
|
|
1341
1266
|
try:
|
|
1342
1267
|
tb_client = create_tb_client(ctx)
|
|
1343
|
-
if dry_run:
|
|
1344
|
-
config.set_semver(None)
|
|
1345
|
-
tb_client.semver = None
|
|
1346
1268
|
await folder_push(
|
|
1347
1269
|
tb_client=tb_client,
|
|
1348
1270
|
dry_run=dry_run,
|
tinybird/tb/modules/common.py
CHANGED
|
@@ -218,19 +218,6 @@ async def get_current_environment(client, config):
|
|
|
218
218
|
return next((workspace for workspace in workspaces if workspace["id"] == config["id"]), None)
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
async def get_current_workspace_branches(config: CLIConfig) -> List[Dict[str, Any]]:
|
|
222
|
-
current_main_workspace: Optional[Dict[str, Any]] = await get_current_main_workspace(config)
|
|
223
|
-
if not current_main_workspace:
|
|
224
|
-
raise CLIException(FeedbackManager.error_unable_to_identify_main_workspace())
|
|
225
|
-
|
|
226
|
-
client = config.get_client()
|
|
227
|
-
user_branches: List[Dict[str, Any]] = (await client.user_workspace_branches()).get("workspaces", [])
|
|
228
|
-
all_branches: List[Dict[str, Any]] = (await client.branches()).get("environments", [])
|
|
229
|
-
branches = user_branches + [branch for branch in all_branches if branch not in user_branches]
|
|
230
|
-
|
|
231
|
-
return [branch for branch in branches if branch.get("main") == current_main_workspace["id"]]
|
|
232
|
-
|
|
233
|
-
|
|
234
221
|
class AliasedGroup(click.Group):
|
|
235
222
|
def get_command(self, ctx, cmd_name):
|
|
236
223
|
# Step one: built-in commands as normal
|
|
@@ -320,16 +307,15 @@ def getenv_bool(key: str, default: bool) -> bool:
|
|
|
320
307
|
return v.lower() == "true" or v == "1"
|
|
321
308
|
|
|
322
309
|
|
|
323
|
-
def _get_tb_client(token: str, host: str
|
|
310
|
+
def _get_tb_client(token: str, host: str) -> TinyB:
|
|
324
311
|
disable_ssl: bool = getenv_bool("TB_DISABLE_SSL_CHECKS", False)
|
|
325
|
-
return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True
|
|
312
|
+
return TinyB(token, host, version=VERSION, disable_ssl_checks=disable_ssl, send_telemetry=True)
|
|
326
313
|
|
|
327
314
|
|
|
328
315
|
def create_tb_client(ctx: Context) -> TinyB:
|
|
329
316
|
token = ctx.ensure_object(dict)["config"].get("token", "")
|
|
330
317
|
host = ctx.ensure_object(dict)["config"].get("host", DEFAULT_API_HOST)
|
|
331
|
-
|
|
332
|
-
return _get_tb_client(token, host, semver=semver)
|
|
318
|
+
return _get_tb_client(token, host)
|
|
333
319
|
|
|
334
320
|
|
|
335
321
|
async def _analyze(filename: str, client: TinyB, format: str, connector: Optional["Connector"] = None):
|
|
@@ -776,77 +762,6 @@ async def create_workspace_interactive(
|
|
|
776
762
|
await create_workspace_non_interactive(ctx, workspace_name, starterkit, user_token, fork) # type: ignore
|
|
777
763
|
|
|
778
764
|
|
|
779
|
-
async def create_workspace_branch(
|
|
780
|
-
branch_name: Optional[str],
|
|
781
|
-
last_partition: bool,
|
|
782
|
-
all: bool,
|
|
783
|
-
ignore_datasources: Optional[List[str]],
|
|
784
|
-
wait: Optional[bool],
|
|
785
|
-
) -> None:
|
|
786
|
-
"""
|
|
787
|
-
Creates a workspace branch
|
|
788
|
-
"""
|
|
789
|
-
config = CLIConfig.get_project_config()
|
|
790
|
-
_ = await try_update_config_with_remote(config)
|
|
791
|
-
|
|
792
|
-
try:
|
|
793
|
-
workspace = await get_current_workspace(config)
|
|
794
|
-
if not workspace:
|
|
795
|
-
raise CLIWorkspaceException(FeedbackManager.error_workspace())
|
|
796
|
-
|
|
797
|
-
if not branch_name:
|
|
798
|
-
click.echo(FeedbackManager.info_workspace_branch_create_greeting())
|
|
799
|
-
default_name = f"{workspace['name']}_{uuid.uuid4().hex[0:4]}"
|
|
800
|
-
branch_name = click.prompt("\Branch name", default=default_name, err=True, type=str)
|
|
801
|
-
assert isinstance(branch_name, str)
|
|
802
|
-
|
|
803
|
-
response = await config.get_client().create_workspace_branch(
|
|
804
|
-
branch_name,
|
|
805
|
-
last_partition,
|
|
806
|
-
all,
|
|
807
|
-
ignore_datasources,
|
|
808
|
-
)
|
|
809
|
-
assert isinstance(response, dict)
|
|
810
|
-
|
|
811
|
-
is_job: bool = "job" in response
|
|
812
|
-
is_summary: bool = "partitions" in response
|
|
813
|
-
|
|
814
|
-
if not is_job and not is_summary:
|
|
815
|
-
raise CLIException(str(response))
|
|
816
|
-
|
|
817
|
-
if all and not is_job:
|
|
818
|
-
raise CLIException(str(response))
|
|
819
|
-
|
|
820
|
-
click.echo(
|
|
821
|
-
FeedbackManager.success_workspace_branch_created(workspace_name=workspace["name"], branch_name=branch_name)
|
|
822
|
-
)
|
|
823
|
-
|
|
824
|
-
job_id: Optional[str] = None
|
|
825
|
-
|
|
826
|
-
if is_job:
|
|
827
|
-
job_id = response["job"]["job_id"]
|
|
828
|
-
job_url = response["job"]["job_url"]
|
|
829
|
-
click.echo(FeedbackManager.info_data_branch_job_url(url=job_url))
|
|
830
|
-
|
|
831
|
-
if wait and is_job:
|
|
832
|
-
assert isinstance(job_id, str)
|
|
833
|
-
|
|
834
|
-
# Await the job to finish and get the result dict
|
|
835
|
-
job_response = await wait_job(config.get_client(), job_id, job_url, "Branch creation")
|
|
836
|
-
if job_response is None:
|
|
837
|
-
raise CLIException(f"Empty job API response (job_id: {job_id}, job_url: {job_url})")
|
|
838
|
-
else:
|
|
839
|
-
response = job_response.get("result", {})
|
|
840
|
-
is_summary = "partitions" in response
|
|
841
|
-
|
|
842
|
-
await switch_workspace(config, branch_name, only_environments=True)
|
|
843
|
-
if is_summary and (bool(last_partition) or bool(all)):
|
|
844
|
-
await print_data_branch_summary(config.get_client(), None, response)
|
|
845
|
-
|
|
846
|
-
except Exception as e:
|
|
847
|
-
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
848
|
-
|
|
849
|
-
|
|
850
765
|
async def print_data_branch_summary(client, job_id, response=None):
|
|
851
766
|
response = await client.job(job_id) if job_id else response or {"partitions": []}
|
|
852
767
|
columns = ["Data Source", "Partition", "Status", "Error"]
|
|
@@ -1327,13 +1242,10 @@ def _get_setting_value(connection, setting, sensitive_settings):
|
|
|
1327
1242
|
return connection.get(setting, "")
|
|
1328
1243
|
|
|
1329
1244
|
|
|
1330
|
-
async def switch_workspace(config: CLIConfig, workspace_name_or_id: str
|
|
1245
|
+
async def switch_workspace(config: CLIConfig, workspace_name_or_id: str) -> None:
|
|
1331
1246
|
try:
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
else:
|
|
1335
|
-
response = await config.get_client().user_workspaces()
|
|
1336
|
-
workspaces = response["workspaces"]
|
|
1247
|
+
response = await config.get_client().user_workspaces()
|
|
1248
|
+
workspaces = response["workspaces"]
|
|
1337
1249
|
|
|
1338
1250
|
workspace = next(
|
|
1339
1251
|
(
|
|
@@ -1345,10 +1257,7 @@ async def switch_workspace(config: CLIConfig, workspace_name_or_id: str, only_en
|
|
|
1345
1257
|
)
|
|
1346
1258
|
|
|
1347
1259
|
if not workspace:
|
|
1348
|
-
|
|
1349
|
-
raise CLIException(FeedbackManager.error_branch(branch=workspace_name_or_id))
|
|
1350
|
-
else:
|
|
1351
|
-
raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
|
|
1260
|
+
raise CLIException(FeedbackManager.error_workspace(workspace=workspace_name_or_id))
|
|
1352
1261
|
|
|
1353
1262
|
config.set_token(workspace["token"])
|
|
1354
1263
|
config.set_token_for_host(workspace["token"], config.get_host())
|
tinybird/tb/modules/config.py
CHANGED
|
@@ -72,7 +72,6 @@ class CLIConfig:
|
|
|
72
72
|
"token": "TB_TOKEN",
|
|
73
73
|
"user_token": "TB_USER_TOKEN",
|
|
74
74
|
"host": "TB_HOST",
|
|
75
|
-
"semver": "TB_SEMVER",
|
|
76
75
|
}
|
|
77
76
|
|
|
78
77
|
DEFAULTS: Dict[str, str] = {"host": DEFAULT_API_HOST if not FeatureFlags.is_localhost() else DEFAULT_LOCALHOST}
|
|
@@ -178,15 +177,6 @@ class CLIConfig:
|
|
|
178
177
|
except KeyError:
|
|
179
178
|
return None
|
|
180
179
|
|
|
181
|
-
def set_semver(self, semver: Optional[str]) -> None:
|
|
182
|
-
self["semver"] = semver
|
|
183
|
-
|
|
184
|
-
def get_semver(self) -> Optional[str]:
|
|
185
|
-
try:
|
|
186
|
-
return self["semver"]
|
|
187
|
-
except KeyError:
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
180
|
def set_token_for_host(self, token: Optional[str], host: Optional[str]) -> None:
|
|
191
181
|
"""Sets the token for the specified host.
|
|
192
182
|
|
tinybird/tb/modules/create.py
CHANGED
|
@@ -5,13 +5,13 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
|
-
|
|
8
|
+
import requests
|
|
9
9
|
|
|
10
10
|
from tinybird.client import TinyB
|
|
11
11
|
from tinybird.feedback_manager import FeedbackManager
|
|
12
12
|
from tinybird.tb.modules.cicd import init_cicd
|
|
13
13
|
from tinybird.tb.modules.cli import cli
|
|
14
|
-
from tinybird.tb.modules.common import _generate_datafile, coro, generate_datafile
|
|
14
|
+
from tinybird.tb.modules.common import _generate_datafile, check_user_token, coro, generate_datafile
|
|
15
15
|
from tinybird.tb.modules.config import CLIConfig
|
|
16
16
|
from tinybird.tb.modules.datafile.fixture import build_fixture_name, persist_fixture
|
|
17
17
|
from tinybird.tb.modules.exceptions import CLIException
|
|
@@ -19,6 +19,11 @@ from tinybird.tb.modules.llm import LLM
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
@cli.command()
|
|
22
|
+
@click.option(
|
|
23
|
+
"--demo",
|
|
24
|
+
is_flag=True,
|
|
25
|
+
help="Demo data and files to get started",
|
|
26
|
+
)
|
|
22
27
|
@click.option(
|
|
23
28
|
"--data",
|
|
24
29
|
type=click.Path(exists=True),
|
|
@@ -41,7 +46,8 @@ from tinybird.tb.modules.llm import LLM
|
|
|
41
46
|
@click.pass_context
|
|
42
47
|
@coro
|
|
43
48
|
async def create(
|
|
44
|
-
ctx: Context,
|
|
49
|
+
ctx: click.Context,
|
|
50
|
+
demo: bool,
|
|
45
51
|
data: Optional[str],
|
|
46
52
|
prompt: Optional[str],
|
|
47
53
|
folder: Optional[str],
|
|
@@ -50,7 +56,16 @@ async def create(
|
|
|
50
56
|
"""Initialize a new project."""
|
|
51
57
|
folder = folder or getcwd()
|
|
52
58
|
try:
|
|
53
|
-
config = CLIConfig.get_project_config()
|
|
59
|
+
config = CLIConfig.get_project_config(folder)
|
|
60
|
+
|
|
61
|
+
if prompt:
|
|
62
|
+
user_token = config.get_user_token()
|
|
63
|
+
try:
|
|
64
|
+
await check_user_token(ctx, token=user_token)
|
|
65
|
+
except Exception:
|
|
66
|
+
click.echo(FeedbackManager.error(message="This action requires authentication. Run 'tb login' first."))
|
|
67
|
+
return
|
|
68
|
+
|
|
54
69
|
tb_client = config.get_client()
|
|
55
70
|
click.echo(FeedbackManager.gray(message="Creating new project structure..."))
|
|
56
71
|
await project_create(tb_client, data, prompt, folder)
|
|
@@ -63,7 +78,60 @@ async def create(
|
|
|
63
78
|
|
|
64
79
|
click.echo(FeedbackManager.gray(message="Building fixtures..."))
|
|
65
80
|
|
|
66
|
-
if
|
|
81
|
+
if demo:
|
|
82
|
+
# Users datasource
|
|
83
|
+
ds_name = "users"
|
|
84
|
+
datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
|
|
85
|
+
datasource_content = fetch_gist_content(
|
|
86
|
+
"https://gist.githubusercontent.com/gnzjgo/b48fb9c92825ed27c04e3104b9e871e1/raw/1f33c20eefbabc4903f38e234329e028d8ef9def/users.datasource"
|
|
87
|
+
)
|
|
88
|
+
datasource_path.write_text(datasource_content)
|
|
89
|
+
click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
|
|
90
|
+
|
|
91
|
+
# Users fixtures
|
|
92
|
+
fixture_content = fetch_gist_content(
|
|
93
|
+
"https://gist.githubusercontent.com/gnzjgo/8e8f66a39d7576ce3a2529bf773334a8/raw/9cab636767990e97d44a141867e5f226e992de8c/users.ndjson"
|
|
94
|
+
)
|
|
95
|
+
fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
|
|
96
|
+
persist_fixture(fixture_name, fixture_content)
|
|
97
|
+
click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
|
|
98
|
+
|
|
99
|
+
# Events datasource
|
|
100
|
+
ds_name = "events"
|
|
101
|
+
datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
|
|
102
|
+
datasource_content = fetch_gist_content(
|
|
103
|
+
"https://gist.githubusercontent.com/gnzjgo/f8ca37b5b1f6707c75206b618de26bc9/raw/cd625da0dcd1ba8de29f12bc1c8600b9ff7c809c/events.datasource"
|
|
104
|
+
)
|
|
105
|
+
datasource_path.write_text(datasource_content)
|
|
106
|
+
click.echo(FeedbackManager.info(message=f"✓ /datasources/{ds_name}.datasource"))
|
|
107
|
+
|
|
108
|
+
# Events fixtures
|
|
109
|
+
fixture_content = fetch_gist_content(
|
|
110
|
+
"https://gist.githubusercontent.com/gnzjgo/859ab9439c17e77241d0c14a5a532809/raw/251f2f3f00a968f8759ec4068cebde915256b054/events.ndjson"
|
|
111
|
+
)
|
|
112
|
+
fixture_name = build_fixture_name(datasource_path.absolute(), ds_name, datasource_path.read_text())
|
|
113
|
+
persist_fixture(fixture_name, fixture_content)
|
|
114
|
+
click.echo(FeedbackManager.info(message=f"✓ /fixtures/{ds_name}"))
|
|
115
|
+
|
|
116
|
+
# Create sample endpoint
|
|
117
|
+
pipe_name = "api_token_usage"
|
|
118
|
+
pipe_path = Path(folder) / "endpoints" / f"{pipe_name}.pipe"
|
|
119
|
+
pipe_content = fetch_gist_content(
|
|
120
|
+
"https://gist.githubusercontent.com/gnzjgo/68ecc47472c2b754b0ae0c1187022963/raw/52cc3aa3afdf939e58d43355bfe4ddc739989ddd/api_token_usage.pipe"
|
|
121
|
+
)
|
|
122
|
+
pipe_path.write_text(pipe_content)
|
|
123
|
+
click.echo(FeedbackManager.info(message=f"✓ /endpoints/{pipe_name}.pipe"))
|
|
124
|
+
|
|
125
|
+
# Create sample test
|
|
126
|
+
test_name = "api_token_usage"
|
|
127
|
+
test_path = Path(folder) / "tests" / f"{test_name}.yaml"
|
|
128
|
+
test_content = fetch_gist_content(
|
|
129
|
+
"https://gist.githubusercontent.com/gnzjgo/e58620bbb977d6f42f1d0c2a7b46ac8f/raw/a5f61d5019111f937484f941111829dfce69f648/api_token_usage.yaml"
|
|
130
|
+
)
|
|
131
|
+
test_path.write_text(test_content)
|
|
132
|
+
click.echo(FeedbackManager.info(message=f"✓ /tests/{test_name}.yaml"))
|
|
133
|
+
|
|
134
|
+
elif data:
|
|
67
135
|
ds_name = os.path.basename(data.split(".")[0])
|
|
68
136
|
data_content = Path(data).read_text()
|
|
69
137
|
datasource_path = Path(folder) / "datasources" / f"{ds_name}.datasource"
|
|
@@ -79,7 +147,7 @@ async def create(
|
|
|
79
147
|
datasource_content = datasource_path.read_text()
|
|
80
148
|
has_json_path = "`json:" in datasource_content
|
|
81
149
|
if has_json_path:
|
|
82
|
-
sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows)
|
|
150
|
+
sql = await llm.generate_sql_sample_data(schema=datasource_content, rows=rows, context=prompt)
|
|
83
151
|
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
84
152
|
data = result.get("data", [])
|
|
85
153
|
fixture_name = build_fixture_name(datasource_path.absolute(), datasource_name, datasource_content)
|
|
@@ -97,7 +165,7 @@ async def project_create(
|
|
|
97
165
|
prompt: Optional[str],
|
|
98
166
|
folder: str,
|
|
99
167
|
):
|
|
100
|
-
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures"]
|
|
168
|
+
project_paths = ["datasources", "endpoints", "materializations", "copies", "sinks", "fixtures", "tests"]
|
|
101
169
|
force = True
|
|
102
170
|
for x in project_paths:
|
|
103
171
|
try:
|
|
@@ -113,7 +181,7 @@ async def project_create(
|
|
|
113
181
|
try:
|
|
114
182
|
await _generate_datafile(str(path), client, format=format, force=force)
|
|
115
183
|
except Exception as e:
|
|
116
|
-
click.echo(FeedbackManager.error(message=f"
|
|
184
|
+
click.echo(FeedbackManager.error(message=f"Error: {str(e)}"))
|
|
117
185
|
name = data.split(".")[0]
|
|
118
186
|
generate_pipe_file(
|
|
119
187
|
f"{name}_endpoint",
|
|
@@ -170,3 +238,9 @@ def generate_pipe_file(name: str, content: str, folder: str):
|
|
|
170
238
|
with open(f"{f}", "w") as file:
|
|
171
239
|
file.write(content)
|
|
172
240
|
click.echo(FeedbackManager.info_file_created(file=f.relative_to(folder)))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def fetch_gist_content(url: str) -> str: # TODO: replace this with a function that fetches the content from a repo
|
|
244
|
+
response = requests.get(url)
|
|
245
|
+
response.raise_for_status()
|
|
246
|
+
return response.text
|
|
@@ -266,7 +266,7 @@ async def new_pipe(
|
|
|
266
266
|
|
|
267
267
|
if data.get("type") == "endpoint":
|
|
268
268
|
token = tb_client.token
|
|
269
|
-
|
|
269
|
+
click.echo(f"""** => Test endpoint with:\n** $ curl {host}/v0/pipes/{p["name"]}.json?token={token}""")
|
|
270
270
|
|
|
271
271
|
|
|
272
272
|
async def get_token_from_main_branch(branch_tb_client: TinyB) -> Optional[str]:
|
|
@@ -21,7 +21,6 @@ if TYPE_CHECKING:
|
|
|
21
21
|
from tinybird.connectors import Connector
|
|
22
22
|
|
|
23
23
|
from tinybird.feedback_manager import FeedbackManager
|
|
24
|
-
from tinybird.tb.modules.branch import warn_if_in_live
|
|
25
24
|
from tinybird.tb.modules.cli import cli
|
|
26
25
|
from tinybird.tb.modules.common import (
|
|
27
26
|
_analyze,
|
|
@@ -358,9 +357,6 @@ async def datasource_delete(ctx: Context, datasource_name: str, yes: bool):
|
|
|
358
357
|
FeedbackManager.error_datasource_can_not_be_deleted(datasource=datasource_name, error=e)
|
|
359
358
|
)
|
|
360
359
|
|
|
361
|
-
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
362
|
-
await warn_if_in_live(semver)
|
|
363
|
-
|
|
364
360
|
if yes or click.confirm(
|
|
365
361
|
FeedbackManager.warning_confirm_delete_datasource(
|
|
366
362
|
warning_message=warning_message, dependencies_information=dependencies_information
|
|
@@ -391,9 +387,6 @@ async def datasource_delete(ctx: Context, datasource_name: str, yes: bool):
|
|
|
391
387
|
async def datasource_truncate(ctx, datasource_name, yes, cascade):
|
|
392
388
|
"""Truncate a data source"""
|
|
393
389
|
|
|
394
|
-
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
395
|
-
await warn_if_in_live(semver)
|
|
396
|
-
|
|
397
390
|
client = ctx.obj["client"]
|
|
398
391
|
if yes or click.confirm(FeedbackManager.warning_confirm_truncate_datasource(datasource=datasource_name)):
|
|
399
392
|
try:
|
|
@@ -453,9 +446,6 @@ async def datasource_delete_rows(ctx, datasource_name, sql_condition, yes, wait,
|
|
|
453
446
|
- Delete rows with SQL condition and wait for the job to finish: `tb datasource delete [datasource_name] --sql-condition "country='ES'" --wait`
|
|
454
447
|
"""
|
|
455
448
|
|
|
456
|
-
semver: str = ctx.ensure_object(dict)["config"]["semver"]
|
|
457
|
-
await warn_if_in_live(semver)
|
|
458
|
-
|
|
459
449
|
client: TinyB = ctx.ensure_object(dict)["client"]
|
|
460
450
|
if (
|
|
461
451
|
dry_run
|
tinybird/tb/modules/llm.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import json
|
|
2
3
|
import urllib.parse
|
|
3
4
|
from copy import deepcopy
|
|
4
|
-
from typing import Awaitable, Callable, List
|
|
5
|
+
from typing import Awaitable, Callable, List, Optional
|
|
5
6
|
|
|
7
|
+
from openai import OpenAI
|
|
6
8
|
from pydantic import BaseModel
|
|
7
9
|
|
|
8
10
|
from tinybird.client import TinyB
|
|
11
|
+
from tinybird.prompts import create_test_calls_prompt
|
|
9
12
|
from tinybird.tb.modules.config import CLIConfig
|
|
10
13
|
|
|
11
14
|
|
|
@@ -19,13 +22,25 @@ class DataProject(BaseModel):
|
|
|
19
22
|
pipes: List[DataFile]
|
|
20
23
|
|
|
21
24
|
|
|
25
|
+
class TestExpectation(BaseModel):
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
parameters: str
|
|
29
|
+
expected_result: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestExpectations(BaseModel):
|
|
33
|
+
tests: List[TestExpectation]
|
|
34
|
+
|
|
35
|
+
|
|
22
36
|
class LLM:
|
|
23
|
-
def __init__(self, client: TinyB):
|
|
37
|
+
def __init__(self, client: TinyB, api_key: Optional[str] = None):
|
|
24
38
|
self.client = client
|
|
25
39
|
user_token = CLIConfig.get_project_config().get_user_token()
|
|
26
40
|
user_client = deepcopy(client)
|
|
27
41
|
user_client.token = user_token
|
|
28
42
|
self.user_client = user_client
|
|
43
|
+
self.openai = OpenAI(api_key=api_key) if api_key else None
|
|
29
44
|
|
|
30
45
|
async def _execute(self, action_fn: Callable[[], Awaitable[str]], checker_fn: Callable[[str], bool]):
|
|
31
46
|
is_valid = False
|
|
@@ -46,9 +61,10 @@ class LLM:
|
|
|
46
61
|
response = await self.user_client._req(
|
|
47
62
|
"/v0/llm/create",
|
|
48
63
|
method="POST",
|
|
49
|
-
data=f'{{"prompt":
|
|
64
|
+
data=f'{{"prompt": {json.dumps(prompt)}}}',
|
|
50
65
|
headers={"Content-Type": "application/json"},
|
|
51
66
|
)
|
|
67
|
+
|
|
52
68
|
return DataProject.model_validate(response.get("result", {}))
|
|
53
69
|
except Exception:
|
|
54
70
|
return DataProject(datasources=[], pipes=[])
|
|
@@ -61,3 +77,19 @@ class LLM:
|
|
|
61
77
|
headers={"Content-Type": "application/json"},
|
|
62
78
|
)
|
|
63
79
|
return response.get("result", "")
|
|
80
|
+
|
|
81
|
+
async def create_test_commands(
|
|
82
|
+
self, pipe_content: str, pipe_params: set[str], context: str = ""
|
|
83
|
+
) -> TestExpectations:
|
|
84
|
+
if not self.openai:
|
|
85
|
+
raise ValueError("OpenAI API key is not set")
|
|
86
|
+
|
|
87
|
+
completion = self.openai.beta.chat.completions.parse(
|
|
88
|
+
model="gpt-4o",
|
|
89
|
+
messages=[
|
|
90
|
+
{"role": "system", "content": create_test_calls_prompt.format(context=context)},
|
|
91
|
+
{"role": "user", "content": f"Pipe content: {pipe_content}\nPipe params: {pipe_params}"},
|
|
92
|
+
],
|
|
93
|
+
response_format=TestExpectations,
|
|
94
|
+
)
|
|
95
|
+
return completion.choices[0].message.parsed or TestExpectations(tests=[])
|