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.

@@ -1,1023 +0,0 @@
1
- # This is a command file for our CLI. Please keep it clean.
2
- #
3
- # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
- # - But please, **do not** interleave utility functions and command definitions.
5
-
6
- import os
7
- from typing import List, Optional, Tuple
8
-
9
- import aiofiles
10
- import click
11
- import yaml
12
-
13
- from tinybird.feedback_manager import FeedbackManager
14
- from tinybird.tb.modules.cli import cli
15
- from tinybird.tb.modules.common import (
16
- MAIN_BRANCH,
17
- OLDEST_ROLLBACK,
18
- coro,
19
- create_workspace_branch,
20
- echo_safe_humanfriendly_tables_format_smart_table,
21
- get_current_main_workspace,
22
- get_current_workspace,
23
- get_current_workspace_branches,
24
- get_oldest_rollback,
25
- get_workspace_member_email,
26
- getenv_bool,
27
- print_branch_regression_tests_summary,
28
- print_current_branch,
29
- print_current_workspace,
30
- print_data_branch_summary,
31
- print_release_summary,
32
- remove_release,
33
- switch_to_workspace_by_user_workspace_data,
34
- switch_workspace,
35
- try_update_config_with_remote,
36
- wait_job,
37
- )
38
- from tinybird.tb.modules.config import CLIConfig
39
- from tinybird.tb.modules.exceptions import CLIBranchException, CLIException, CLIReleaseException
40
-
41
-
42
- @cli.group(hidden=True)
43
- def release() -> None:
44
- """Release commands"""
45
-
46
-
47
- @release.command(name="ls", short_help="Lists Releases for the current Workspace")
48
- @coro
49
- async def release_ls() -> None:
50
- """List current available Releases in the Workspace"""
51
- config = CLIConfig.get_project_config()
52
- _ = await try_update_config_with_remote(config, only_if_needed=True)
53
-
54
- await print_releases(config)
55
-
56
-
57
- async def print_releases(config: CLIConfig):
58
- response = await config.get_client().releases(config["id"])
59
-
60
- table: List[Tuple[str, str, str, str, str]] = []
61
- for release in response["releases"]:
62
- table.append(
63
- (release["created_at"], release["semver"], release["status"], release["commit"], release["rollback"])
64
- )
65
-
66
- columns = ["created_at", "semver", "status", "commit", "rollback release"]
67
- click.echo(FeedbackManager.info_releases())
68
- echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
69
-
70
-
71
- @release.command(name="generate", short_help="Generates a custom deployment for a Release")
72
- @click.option(
73
- "--semver",
74
- is_flag=False,
75
- required=True,
76
- type=str,
77
- help="Semver of the new Release. Example: 1.0.0",
78
- )
79
- @coro
80
- async def release_generate(semver: str) -> None:
81
- click.echo(FeedbackManager.warning_deprecated_releases())
82
- if os.path.exists(".tinyenv"):
83
- async with aiofiles.open(".tinyenv", "r") as env_file:
84
- lines = await env_file.readlines()
85
-
86
- updated_lines = []
87
- for line in lines:
88
- if line.startswith("VERSION="):
89
- updated_lines.append(f"VERSION={semver}\n")
90
- else:
91
- updated_lines.append(line)
92
-
93
- async with aiofiles.open(".tinyenv", "w") as env_file:
94
- await env_file.writelines(updated_lines)
95
- else:
96
- async with aiofiles.open(".tinyenv", "w") as env_file:
97
- await env_file.write(f"VERSION={semver}\n")
98
-
99
- deploy_dir = os.path.join("deploy", semver)
100
- os.makedirs(deploy_dir, exist_ok=True)
101
-
102
- deploy_file = os.path.join(deploy_dir, "deploy.sh")
103
- async with aiofiles.open(deploy_file, "w") as deploy:
104
- await deploy.write(
105
- """\
106
- #!/bin/bash
107
- set -euxo pipefail
108
-
109
- # tb --semver $VERSION deploy
110
- """
111
- )
112
-
113
- os.chmod(deploy_file, 0o755)
114
-
115
- post_deploy_file = os.path.join(deploy_dir, "postdeploy.sh")
116
- async with aiofiles.open(post_deploy_file, "w") as post_deploy:
117
- await post_deploy.write(
118
- """\
119
- #!/bin/bash
120
- set -euxo pipefail
121
-
122
- # tb --semver $VERSION pipe populate <pipe_name> --node <node_name> --sql-condition <sql> --wait
123
- """
124
- )
125
-
126
- os.chmod(post_deploy_file, 0o755)
127
-
128
- click.echo(FeedbackManager.info_release_generated(semver=semver))
129
-
130
-
131
- @release.command(name="promote", short_help="Promotes to live status a preview Release")
132
- @click.option(
133
- "--semver", required=True, type=str, help="Semver of a preview Release to promote to live. Example: 1.0.0"
134
- )
135
- @coro
136
- async def release_promote(semver: str) -> None:
137
- """
138
- The oldest rollback Release will be automatically removed if no usage, otherwise export TB_FORCE_REMOVE_OLDEST_ROLLBACK="1" to force deletion
139
- """
140
- click.echo(FeedbackManager.warning_deprecated_releases())
141
- config = CLIConfig.get_project_config()
142
- _ = await try_update_config_with_remote(config, only_if_needed=True)
143
-
144
- client = config.get_client()
145
-
146
- try:
147
- force_remove = getenv_bool("TB_FORCE_REMOVE_OLDEST_ROLLBACK", False)
148
- click.echo(FeedbackManager.warning_remove_oldest_rollback(semver=semver))
149
- try:
150
- await remove_release(False, config, OLDEST_ROLLBACK, client, force=force_remove)
151
- except Exception as e:
152
- click.echo(FeedbackManager.error_remove_oldest_rollback(error=str(e), semver=semver))
153
- release = await client.release_promote(config["id"], semver)
154
- click.echo(FeedbackManager.success_release_promote(semver=semver))
155
- click.echo(FeedbackManager.success_git_release(release_commit=release["commit"]))
156
- except Exception as e:
157
- raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
158
-
159
-
160
- @release.command(name="preview", short_help="Updates the status of a deploying Release to preview")
161
- @click.option(
162
- "--semver", is_flag=False, required=True, type=str, help="Semver of a preview Release to preview. Example: 1.0.0"
163
- )
164
- @coro
165
- async def release_preview(semver: str) -> None:
166
- click.echo(FeedbackManager.warning_deprecated_releases())
167
- config = CLIConfig.get_project_config()
168
- _ = await try_update_config_with_remote(config, only_if_needed=True)
169
-
170
- client = config.get_client()
171
-
172
- try:
173
- await client.release_preview(config["id"], semver)
174
- click.echo(FeedbackManager.success_release_preview(semver=semver))
175
- except Exception as e:
176
- raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
177
-
178
-
179
- @release.command(name="rollback", short_help="Rollbacks to a previous Release")
180
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
181
- @coro
182
- async def release_rollback(yes: bool) -> None:
183
- click.echo(FeedbackManager.warning_deprecated_releases())
184
- config = CLIConfig.get_project_config()
185
- _ = await try_update_config_with_remote(config, only_if_needed=False)
186
-
187
- client = config.get_client()
188
-
189
- releases_response = await config.get_client().releases(config["id"])
190
-
191
- try:
192
- release_to_rollback = next(
193
- (release for release in releases_response["releases"] if release["status"] == "live"), None
194
- )
195
- if not release_to_rollback:
196
- raise CLIBranchException(FeedbackManager.error_release_rollback_live_not_found())
197
-
198
- await print_releases(config)
199
- semver = release_to_rollback.get("semver")
200
- rollback_version = release_to_rollback.get("rollback")
201
- await print_release_summary(config, rollback_version)
202
- if yes or click.confirm(
203
- FeedbackManager.warning_confirm_rollback_release(semver=semver, rollback=rollback_version)
204
- ):
205
- release = await client.release_rollback(config["id"], semver=semver)
206
- click.echo(FeedbackManager.success_release_rollback(semver=release["semver"]))
207
- except Exception as e:
208
- raise CLIReleaseException(FeedbackManager.error_exception(error=str(e)))
209
-
210
-
211
- @release.command(name="rm", short_help="Removes a preview or failed Release. This action is irreversible")
212
- @click.option(
213
- "--semver", required=False, type=str, help="Semver of a preview or failed Release to delete. Example: 1.0.0"
214
- )
215
- @click.option(
216
- "--oldest-rollback", is_flag=True, default=False, help="Removes the oldest rollback Release by creation date"
217
- )
218
- @click.option(
219
- "--force", is_flag=True, default=False, help="USE WITH CAUTION! Allows to delete a Release that is currently in use"
220
- )
221
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
222
- @click.option(
223
- "--dry-run", is_flag=True, default=False, help="Checks the Release could be deleted without actually deleting it"
224
- )
225
- @coro
226
- async def release_rm(semver: str, oldest_rollback: bool, force: bool, yes: bool, dry_run: bool) -> None:
227
- click.echo(FeedbackManager.warning_deprecated_releases())
228
- if (not semver and not oldest_rollback) or (semver and oldest_rollback):
229
- raise CLIException(FeedbackManager.error_release_rm_param())
230
-
231
- config = CLIConfig.get_project_config()
232
- client = config.get_client()
233
- _ = await try_update_config_with_remote(config)
234
-
235
- if oldest_rollback:
236
- oldest_rollback_semver = await get_oldest_rollback(config, client)
237
- if not oldest_rollback_semver:
238
- click.echo(FeedbackManager.info_release_no_rollback())
239
- return
240
- else:
241
- semver = oldest_rollback_semver
242
- await print_release_summary(config, semver, info=True, dry_run=dry_run)
243
- if dry_run or yes or click.confirm(FeedbackManager.warning_confirm_delete_release(semver=semver)):
244
- try:
245
- await remove_release(dry_run, config, semver, client, force, show_print=False)
246
- except Exception as e:
247
- raise CLIReleaseException(FeedbackManager.simple_error_exception(error=str(e)))
248
-
249
-
250
- @cli.group()
251
- def branch() -> None:
252
- """Branch commands. Branches are an experimental feature only available in beta. Running branch commands without activation will return an error"""
253
- pass
254
-
255
-
256
- @branch.command(name="ls")
257
- @click.option("--sort/--no-sort", default=False, help="Sort the table rows by name")
258
- @coro
259
- async def branch_ls(sort: bool) -> None:
260
- """List all the branches available using the workspace token"""
261
-
262
- config = CLIConfig.get_project_config()
263
- _ = await try_update_config_with_remote(config, only_if_needed=True)
264
-
265
- client = config.get_client()
266
-
267
- current_main_workspace = await get_current_main_workspace(config)
268
- assert isinstance(current_main_workspace, dict)
269
-
270
- if current_main_workspace["id"] != config["id"]:
271
- client = config.get_client(token=current_main_workspace["token"])
272
-
273
- current_main_owner_email = get_workspace_member_email(current_main_workspace, current_main_workspace["owner"])
274
-
275
- response = await client.branches()
276
-
277
- columns = ["name", "id", "created_at", "owner", "current"]
278
-
279
- table: List[Tuple[str, str, str, str, bool]] = [
280
- (
281
- MAIN_BRANCH,
282
- current_main_workspace["id"],
283
- current_main_workspace["created_at"],
284
- current_main_owner_email,
285
- config["id"] == current_main_workspace["id"],
286
- )
287
- ]
288
-
289
- for branch in response["environments"]:
290
- branch_owner_email = get_workspace_member_email(branch, branch["owner"])
291
-
292
- table.append(
293
- (branch["name"], branch["id"], branch["created_at"], branch_owner_email, config["id"] == branch["id"])
294
- )
295
-
296
- current_branch = [row for row in table if row[4]]
297
- other_branches = [row for row in table if not row[4]]
298
-
299
- if sort:
300
- other_branches.sort(key=lambda x: x[0])
301
-
302
- sorted_table = current_branch + other_branches
303
-
304
- await print_current_workspace(config)
305
-
306
- click.echo(FeedbackManager.info_branches())
307
- echo_safe_humanfriendly_tables_format_smart_table(sorted_table, column_names=columns)
308
-
309
-
310
- @branch.command(name="use")
311
- @click.argument("branch_name_or_id")
312
- @coro
313
- async def branch_use(branch_name_or_id: str) -> None:
314
- """Switch to another Branch (requires an admin token associated with a user). Use 'tb branch ls' to list the Branches you can access"""
315
-
316
- config = CLIConfig.get_project_config()
317
- _ = await try_update_config_with_remote(config, only_if_needed=True)
318
-
319
- if branch_name_or_id == MAIN_BRANCH:
320
- current_main_workspace = await get_current_main_workspace(config)
321
- assert isinstance(current_main_workspace, dict)
322
- await switch_to_workspace_by_user_workspace_data(config, current_main_workspace)
323
- else:
324
- await switch_workspace(config, branch_name_or_id, only_environments=True)
325
-
326
-
327
- @branch.command(name="current")
328
- @coro
329
- async def branch_current() -> None:
330
- """Show the Branch you're currently authenticated to"""
331
- config = CLIConfig.get_project_config()
332
- await print_current_branch(config)
333
-
334
-
335
- @branch.command(name="create", short_help="Create a new Branch in the current 'main' Workspace")
336
- @click.argument("branch_name", required=False)
337
- @click.option(
338
- "--last-partition",
339
- is_flag=True,
340
- default=False,
341
- help="Attach the last modified partition from 'main' to the new Branch",
342
- )
343
- @click.option(
344
- "--all",
345
- is_flag=True,
346
- default=False,
347
- help="Attach all data from 'main' to the new Branch. Use only if you actually need all the data in the Branch",
348
- hidden=True,
349
- )
350
- @click.option(
351
- "-i",
352
- "--ignore-datasource",
353
- "ignore_datasources",
354
- type=str,
355
- multiple=True,
356
- help="Ignore specified data source partitions",
357
- )
358
- @click.option(
359
- "--wait/--no-wait",
360
- is_flag=True,
361
- default=True,
362
- help="Wait for data branch jobs to finish, showing a progress bar. Disabled by default.",
363
- )
364
- @coro
365
- async def create_branch(
366
- branch_name: Optional[str], last_partition: bool, all: bool, ignore_datasources: List[str], wait: bool
367
- ) -> None:
368
- if last_partition and all:
369
- raise CLIException(FeedbackManager.error_exception(error="Use --last-partition or --all but not both"))
370
- await create_workspace_branch(branch_name, last_partition, all, list(ignore_datasources), wait)
371
-
372
-
373
- @branch.command(name="rm", short_help="Removes a Branch from the Workspace. It can't be recovered.")
374
- @click.argument("branch_name_or_id")
375
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
376
- @coro
377
- async def delete_branch(branch_name_or_id: str, yes: bool) -> None:
378
- """Remove an Branch (not Main)"""
379
-
380
- config = CLIConfig.get_project_config()
381
- _ = await try_update_config_with_remote(config)
382
-
383
- client = config.get_client()
384
-
385
- if branch_name_or_id == MAIN_BRANCH:
386
- raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
387
-
388
- try:
389
- workspace_branches = await get_current_workspace_branches(config)
390
- workspace_to_delete = next(
391
- (
392
- workspace
393
- for workspace in workspace_branches
394
- if workspace["name"] == branch_name_or_id or workspace["id"] == branch_name_or_id
395
- ),
396
- None,
397
- )
398
- except Exception as e:
399
- raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
400
-
401
- if not workspace_to_delete:
402
- raise CLIBranchException(FeedbackManager.error_branch(branch=branch_name_or_id))
403
-
404
- if yes or click.confirm(FeedbackManager.warning_confirm_delete_branch(branch=workspace_to_delete["name"])):
405
- need_to_switch_to_main = workspace_to_delete.get("main") and config["id"] == workspace_to_delete["id"]
406
- # get origin workspace if deleting current branch
407
- if need_to_switch_to_main:
408
- try:
409
- workspaces = (await client.user_workspaces()).get("workspaces", [])
410
- workspace_main = next(
411
- (workspace for workspace in workspaces if workspace["id"] == workspace_to_delete["main"]), None
412
- )
413
- except Exception:
414
- workspace_main = None
415
- try:
416
- await client.delete_branch(workspace_to_delete["id"])
417
- click.echo(FeedbackManager.success_branch_deleted(branch_name=workspace_to_delete["name"]))
418
- except Exception as e:
419
- raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
420
- else:
421
- if need_to_switch_to_main:
422
- if workspace_main:
423
- await switch_to_workspace_by_user_workspace_data(config, workspace_main)
424
- else:
425
- raise CLIException(FeedbackManager.error_switching_to_main())
426
-
427
-
428
- @branch.command(
429
- name="data",
430
- short_help="Perform a data branch operation to bring data into the current Branch. Check flags for details",
431
- )
432
- @click.option(
433
- "--last-partition",
434
- is_flag=True,
435
- default=False,
436
- help="Attach the last modified partition from 'main' to the new Branch",
437
- )
438
- @click.option(
439
- "--all",
440
- is_flag=True,
441
- default=False,
442
- help="Attach all data from 'main' to the new Branch. Use only if you actually need all the data in the Branch",
443
- hidden=True,
444
- )
445
- @click.option(
446
- "-i",
447
- "--ignore-datasource",
448
- "ignore_datasources",
449
- type=str,
450
- multiple=True,
451
- help="Ignore specified data source partitions",
452
- )
453
- @click.option(
454
- "--wait",
455
- is_flag=True,
456
- default=False,
457
- help="Wait for data branch jobs to finish, showing a progress bar. Disabled by default.",
458
- )
459
- @coro
460
- async def data_branch(last_partition: bool, all: bool, ignore_datasources: List[str], wait: bool) -> None:
461
- if last_partition and all:
462
- raise CLIException(FeedbackManager.error_exception(error="Use --last-partition or --all but not both"))
463
-
464
- if not last_partition and not all:
465
- raise CLIException(FeedbackManager.error_exception(error="Use --last-partition or --all"))
466
-
467
- config = CLIConfig.get_project_config()
468
- client = config.get_client()
469
-
470
- current_main_workspace = await get_current_main_workspace(config)
471
- assert isinstance(current_main_workspace, dict)
472
-
473
- if current_main_workspace["id"] == config["id"]:
474
- raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
475
-
476
- try:
477
- response = await client.branch_workspace_data(config["id"], last_partition, all, ignore_datasources)
478
-
479
- is_job: bool = "job" in response
480
- is_summary: bool = "partitions" in response
481
-
482
- if not is_job and not is_summary:
483
- raise CLIBranchException(str(response))
484
-
485
- if all and not is_job:
486
- raise CLIBranchException(str(response))
487
-
488
- if wait and is_job:
489
- job_id = response["job"]["job_id"]
490
- job_url = response["job"]["job_url"]
491
- click.echo(FeedbackManager.info_data_branch_job_url(url=job_url))
492
- job_response = await wait_job(client, job_id, job_url, "Branch creation")
493
- response = job_response["result"]
494
- is_job = False
495
- is_summary = "partitions" in response
496
-
497
- if is_job:
498
- click.echo(FeedbackManager.success_workspace_data_branch_in_progress(job_url=response["job"]["job_url"]))
499
- else:
500
- if not is_job and not is_summary:
501
- FeedbackManager.warning_unknown_response(response=response)
502
- elif is_summary and (bool(last_partition) or bool(all)):
503
- await print_data_branch_summary(client, None, response)
504
- click.echo(FeedbackManager.success_workspace_data_branch())
505
-
506
- except Exception as e:
507
- raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
508
-
509
-
510
- @branch.group("regression-tests", invoke_without_command=True)
511
- @click.option(
512
- "-f",
513
- "--filename",
514
- type=click.Path(exists=True),
515
- required=False,
516
- help="The yaml file with the regression-tests definition",
517
- )
518
- @click.option(
519
- "--wait",
520
- is_flag=True,
521
- default=False,
522
- help="Wait for regression job to finish, showing a progress bar. Disabled by default.",
523
- )
524
- @click.option(
525
- "--skip-regression-tests/--no-skip-regression-tests",
526
- envvar="TB_SKIP_REGRESSION",
527
- default=False,
528
- help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
529
- )
530
- @click.option(
531
- "--main",
532
- is_flag=True,
533
- default=False,
534
- help="Run regression tests in the main Branch. For this flag to work all the resources in the Branch pipe endpoints need to exist in the main Branch.",
535
- )
536
- @click.pass_context
537
- @coro
538
- async def regression_tests(
539
- ctx, filename: str, wait: bool, skip_regression_tests: Optional[bool] = False, main: Optional[bool] = False
540
- ):
541
- """Regression test commands for Branches"""
542
- if skip_regression_tests:
543
- click.echo(FeedbackManager.warning_regression_skipped())
544
- return
545
-
546
- if filename:
547
- try:
548
- with open(filename, "r") as file: # noqa: ASYNC230
549
- regression_tests_commands = yaml.safe_load(file)
550
- except Exception as exc:
551
- raise CLIBranchException(FeedbackManager.error_regression_yaml_not_valid(filename=filename, error=exc))
552
- if not isinstance(regression_tests_commands, List):
553
- raise CLIBranchException(
554
- FeedbackManager.error_regression_yaml_not_valid(filename=filename, error="not a list of pipes")
555
- )
556
-
557
- config = CLIConfig.get_project_config()
558
- client = config.get_client()
559
-
560
- current_main_workspace = await get_current_main_workspace(config)
561
- assert isinstance(current_main_workspace, dict)
562
-
563
- if current_main_workspace["id"] == config["id"]:
564
- raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
565
- return
566
- try:
567
- response = await client.branch_regression_tests_file(
568
- config["id"], regression_tests_commands, run_in_main=main
569
- )
570
- if "job" not in response:
571
- raise CLIBranchException(str(response))
572
- job_id = response["job"]["job_id"]
573
- job_url = response["job"]["job_url"]
574
- click.echo(FeedbackManager.info_regression_tests_branch_job_url(url=job_url))
575
- if wait:
576
- await wait_job(client, job_id, job_url, "Regression tests")
577
- await print_branch_regression_tests_summary(client, job_id, config["host"])
578
- except Exception as e:
579
- raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
580
- else:
581
- if not ctx.invoked_subcommand:
582
- await _run_regression(type="coverage", wait=wait, run_in_main=main)
583
-
584
-
585
- async def _run_regression(
586
- type: str,
587
- pipe_name: Optional[str] = None,
588
- assert_result: Optional[bool] = True,
589
- assert_result_no_error: Optional[bool] = True,
590
- assert_result_rows_count: Optional[bool] = True,
591
- assert_result_ignore_order: Optional[bool] = False,
592
- assert_time_increase_percentage: Optional[int] = 25,
593
- assert_bytes_read_increase_percentage: Optional[int] = 25,
594
- assert_max_time: Optional[float] = 0.3,
595
- failfast: Optional[bool] = False,
596
- wait: Optional[bool] = False,
597
- skip: Optional[bool] = False,
598
- run_in_main: Optional[bool] = False,
599
- **kwargs,
600
- ):
601
- if skip:
602
- click.echo(FeedbackManager.warning_regression_skipped())
603
- return
604
-
605
- config = CLIConfig.get_project_config()
606
- client = config.get_client()
607
-
608
- current_main_workspace = await get_current_main_workspace(config)
609
- assert isinstance(current_main_workspace, dict)
610
-
611
- if current_main_workspace["id"] == config["id"]:
612
- raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
613
- try:
614
- response = await client.branch_regression_tests(
615
- config["id"],
616
- pipe_name,
617
- type,
618
- failfast=failfast,
619
- assert_result=assert_result,
620
- assert_result_no_error=assert_result_no_error,
621
- assert_result_rows_count=assert_result_rows_count,
622
- assert_result_ignore_order=assert_result_ignore_order,
623
- assert_time_increase_percentage=assert_time_increase_percentage,
624
- assert_bytes_read_increase_percentage=assert_bytes_read_increase_percentage,
625
- assert_max_time=assert_max_time,
626
- run_in_main=run_in_main,
627
- **kwargs,
628
- )
629
- if "job" not in response:
630
- raise CLIBranchException(str(response))
631
- job_id = response["job"]["job_id"]
632
- job_url = response["job"]["job_url"]
633
- click.echo(FeedbackManager.info_regression_tests_branch_job_url(url=job_url))
634
- if wait:
635
- await wait_job(client, job_id, job_url, "Regression tests")
636
- await print_branch_regression_tests_summary(client, job_id, config["host"])
637
- except Exception as e:
638
- raise CLIBranchException(FeedbackManager.error_exception(error=str(e)))
639
-
640
-
641
- @regression_tests.command(
642
- name="coverage",
643
- short_help="Run regression tests using coverage requests for Branch vs Main Workspace. It creates a regression-tests job. The argument pipe_name supports regular expressions. Using '.*' if no pipe_name is provided",
644
- )
645
- @click.argument("pipe_name", required=False)
646
- @click.option(
647
- "--assert-result/--no-assert-result",
648
- is_flag=True,
649
- default=True,
650
- help="Whether to perform an assertion on the results returned by the endpoint. Enabled by default. Use --no-assert-result if you expect the endpoint output is different from current version",
651
- )
652
- @click.option(
653
- "--assert-result-no-error/--no-assert-result-no-error",
654
- is_flag=True,
655
- default=True,
656
- help="Whether to verify that the endpoint does not return errors. Enabled by default. Use --no-assert-result-no-error if you expect errors from the endpoint",
657
- )
658
- @click.option(
659
- "--assert-result-rows-count/--no-assert-result-rows-count",
660
- is_flag=True,
661
- default=True,
662
- help="Whether to verify that the correct number of elements are returned in the results. Enabled by default. Use --no-assert-result-rows-count if you expect the numbers of elements in the endpoint output is different from current version",
663
- )
664
- @click.option(
665
- "--assert-result-ignore-order/--no-assert-result-ignore-order",
666
- is_flag=True,
667
- default=False,
668
- help="Whether to ignore the order of the elements in the results. Disabled by default. Use --assert-result-ignore-order if you expect the endpoint output is returning same elements but in different order",
669
- )
670
- @click.option(
671
- "--assert-time-increase-percentage",
672
- type=int,
673
- required=False,
674
- default=25,
675
- help="Allowed percentage increase in endpoint response time. Default value is 25%. Use -1 to disable assert.",
676
- )
677
- @click.option(
678
- "--assert-bytes-read-increase-percentage",
679
- type=int,
680
- required=False,
681
- default=25,
682
- help="Allowed percentage increase in the amount of bytes read by the endpoint. Default value is 25%. Use -1 to disable assert",
683
- )
684
- @click.option(
685
- "--assert-max-time",
686
- type=float,
687
- required=False,
688
- default=0.3,
689
- help="Max time allowed for the endpoint response time. If the response time is lower than this value then the --assert-time-increase-percentage is not taken into account.",
690
- )
691
- @click.option(
692
- "-ff", "--failfast", is_flag=True, default=False, help="When set, the checker will exit as soon one test fails"
693
- )
694
- @click.option(
695
- "--wait",
696
- is_flag=True,
697
- default=False,
698
- help="Waits for regression job to finish, showing a progress bar. Disabled by default.",
699
- )
700
- @click.option(
701
- "--skip-regression-tests/--no-skip-regression-tests",
702
- envvar="TB_SKIP_REGRESSION",
703
- default=False,
704
- help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
705
- )
706
- @click.option(
707
- "--main",
708
- is_flag=True,
709
- default=False,
710
- help="Run regression tests in the main Branch. For this flag to work all the resources in the Branch pipe endpoints need to exist in the main Branch.",
711
- )
712
- @coro
713
- async def coverage(
714
- pipe_name: str,
715
- assert_result: bool,
716
- assert_result_no_error: bool,
717
- assert_result_rows_count: bool,
718
- assert_result_ignore_order: bool,
719
- assert_time_increase_percentage: int,
720
- assert_bytes_read_increase_percentage: int,
721
- assert_max_time: float,
722
- failfast: bool,
723
- wait: bool,
724
- skip_regression_tests: Optional[bool] = False,
725
- main: Optional[bool] = False,
726
- ):
727
- await _run_regression(
728
- "coverage",
729
- pipe_name,
730
- assert_result,
731
- assert_result_no_error,
732
- assert_result_rows_count,
733
- assert_result_ignore_order,
734
- assert_time_increase_percentage,
735
- assert_bytes_read_increase_percentage,
736
- assert_max_time,
737
- failfast,
738
- wait,
739
- skip_regression_tests,
740
- run_in_main=main,
741
- )
742
-
743
-
744
- @regression_tests.command(
745
- name="last",
746
- short_help="Run regression tests using last requests for Branch vs Main Workspace. It creates a regression-tests job. The argument pipe_name supports regular expressions. Using '.*' if no pipe_name is provided",
747
- )
748
- @click.argument("pipe_name", required=False)
749
- @click.option(
750
- "-l",
751
- "--limit",
752
- type=click.IntRange(1, 100),
753
- default=10,
754
- required=False,
755
- help="Number of requests to validate. Default is 10",
756
- )
757
- @click.option(
758
- "--assert-result/--no-assert-result",
759
- is_flag=True,
760
- default=True,
761
- help="Whether to perform an assertion on the results returned by the endpoint. Enabled by default. Use --no-assert-result if you expect the endpoint output is different from current version",
762
- )
763
- @click.option(
764
- "--assert-result-no-error/--no-assert-result-no-error",
765
- is_flag=True,
766
- default=True,
767
- help="Whether to verify that the endpoint does not return errors. Enabled by default. Use --no-assert-result-no-error if you expect errors from the endpoint",
768
- )
769
- @click.option(
770
- "--assert-result-rows-count/--no-assert-result-rows-count",
771
- is_flag=True,
772
- default=True,
773
- help="Whether to verify that the correct number of elements are returned in the results. Enabled by default. Use --no-assert-result-rows-count if you expect the numbers of elements in the endpoint output is different from current version",
774
- )
775
- @click.option(
776
- "--assert-result-ignore-order/--no-assert-result-ignore-order",
777
- is_flag=True,
778
- default=False,
779
- help="Whether to ignore the order of the elements in the results. Disabled by default. Use --assert-result-ignore-order if you expect the endpoint output is returning same elements but in different order",
780
- )
781
- @click.option(
782
- "--assert-time-increase-percentage",
783
- type=int,
784
- required=False,
785
- default=25,
786
- help="Allowed percentage increase in endpoint response time. Default value is 25%. Use -1 to disable assert.",
787
- )
788
- @click.option(
789
- "--assert-bytes-read-increase-percentage",
790
- type=int,
791
- required=False,
792
- default=25,
793
- help="Allowed percentage increase in the amount of bytes read by the endpoint. Default value is 25%. Use -1 to disable assert",
794
- )
795
- @click.option(
796
- "--assert-max-time",
797
- type=float,
798
- required=False,
799
- default=0.3,
800
- help="Max time allowed for the endpoint response time. If the response time is lower than this value then the --assert-time-increase-percentage is not taken into account.",
801
- )
802
- @click.option(
803
- "-ff", "--failfast", is_flag=True, default=False, help="When set, the checker will exit as soon one test fails"
804
- )
805
- @click.option(
806
- "--wait",
807
- is_flag=True,
808
- default=False,
809
- help="Waits for regression job to finish, showing a progress bar. Disabled by default.",
810
- )
811
- @click.option(
812
- "--skip-regression-tests/--no-skip-regression-tests",
813
- envvar="TB_SKIP_REGRESSION",
814
- default=False,
815
- help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
816
- )
817
- @coro
818
- async def last(
819
- pipe_name: str,
820
- limit: int,
821
- assert_result: bool,
822
- assert_result_no_error: bool,
823
- assert_result_rows_count: bool,
824
- assert_result_ignore_order: bool,
825
- assert_time_increase_percentage: int,
826
- assert_bytes_read_increase_percentage: int,
827
- assert_max_time: float,
828
- failfast: bool,
829
- wait: bool,
830
- skip_regression_tests: Optional[bool] = False,
831
- ):
832
- await _run_regression(
833
- "last",
834
- pipe_name,
835
- assert_result,
836
- assert_result_no_error,
837
- assert_result_rows_count,
838
- assert_result_ignore_order,
839
- assert_time_increase_percentage,
840
- assert_bytes_read_increase_percentage,
841
- assert_max_time,
842
- failfast,
843
- wait,
844
- skip_regression_tests,
845
- limit=limit,
846
- )
847
-
848
-
849
- @regression_tests.command(
850
- name="manual",
851
- short_help="Run regression tests using manual requests for Branch vs Main Workspace. It creates a regression-tests job. The argument pipe_name supports regular expressions. Using '.*' if no pipe_name is provided",
852
- context_settings=dict(allow_extra_args=True, ignore_unknown_options=True),
853
- )
854
- @click.argument("pipe_name", required=False)
855
- @click.option(
856
- "--assert-result/--no-assert-result",
857
- is_flag=True,
858
- default=True,
859
- help="Whether to perform an assertion on the results returned by the endpoint. Enabled by default. Use --no-assert-result if you expect the endpoint output is different from current version",
860
- )
861
- @click.option(
862
- "--assert-result-no-error/--no-assert-result-no-error",
863
- is_flag=True,
864
- default=True,
865
- help="Whether to verify that the endpoint does not return errors. Enabled by default. Use --no-assert-result-no-error if you expect errors from the endpoint",
866
- )
867
- @click.option(
868
- "--assert-result-rows-count/--no-assert-result-rows-count",
869
- is_flag=True,
870
- default=True,
871
- help="Whether to verify that the correct number of elements are returned in the results. Enabled by default. Use --no-assert-result-rows-count if you expect the numbers of elements in the endpoint output is different from current version",
872
- )
873
- @click.option(
874
- "--assert-result-ignore-order/--no-assert-result-ignore-order",
875
- is_flag=True,
876
- default=False,
877
- help="Whether to ignore the order of the elements in the results. Disabled by default. Use --assert-result-ignore-order if you expect the endpoint output is returning same elements but in different order",
878
- )
879
- @click.option(
880
- "--assert-time-increase-percentage",
881
- type=int,
882
- required=False,
883
- default=25,
884
- help="Allowed percentage increase in endpoint response time. Default value is 25%. Use -1 to disable assert.",
885
- )
886
- @click.option(
887
- "--assert-bytes-read-increase-percentage",
888
- type=int,
889
- required=False,
890
- default=25,
891
- help="Allowed percentage increase in the amount of bytes read by the endpoint. Default value is 25%. Use -1 to disable assert",
892
- )
893
- @click.option(
894
- "--assert-max-time",
895
- type=float,
896
- required=False,
897
- default=0.3,
898
- help="Max time allowed for the endpoint response time. If the response time is lower than this value then the --assert-time-increase-percentage is not taken into account.",
899
- )
900
- @click.option(
901
- "-ff", "--failfast", is_flag=True, default=False, help="When set, the checker will exit as soon one test fails"
902
- )
903
- @click.option(
904
- "--wait",
905
- is_flag=True,
906
- default=False,
907
- help="Waits for regression job to finish, showing a progress bar. Disabled by default.",
908
- )
909
- @click.option(
910
- "--skip-regression-tests/--no-skip-regression-tests",
911
- envvar="TB_SKIP_REGRESSION",
912
- default=False,
913
- help="Flag to skip execution of regression tests. This is handy for CI branches where regression might be flaky",
914
- )
915
- @click.pass_context
916
- @coro
917
- async def manual(
918
- ctx: click.Context,
919
- pipe_name: str,
920
- assert_result: bool,
921
- assert_result_no_error: bool,
922
- assert_result_rows_count: bool,
923
- assert_result_ignore_order: bool,
924
- assert_time_increase_percentage: int,
925
- assert_bytes_read_increase_percentage: int,
926
- assert_max_time: float,
927
- failfast: bool,
928
- wait: bool,
929
- skip_regression_tests: Optional[bool] = False,
930
- ):
931
- params = [{ctx.args[i][2:]: ctx.args[i + 1] for i in range(0, len(ctx.args), 2)}]
932
- await _run_regression(
933
- "manual",
934
- pipe_name,
935
- assert_result,
936
- assert_result_no_error,
937
- assert_result_rows_count,
938
- assert_result_ignore_order,
939
- assert_time_increase_percentage,
940
- assert_bytes_read_increase_percentage,
941
- assert_max_time,
942
- failfast,
943
- wait,
944
- skip_regression_tests,
945
- params=params,
946
- )
947
-
948
-
949
- @branch.group()
950
- def datasource() -> None:
951
- """Branch data source commands."""
952
-
953
-
954
- @datasource.command(name="copy")
955
- @click.argument("datasource_name")
956
- @click.option(
957
- "--sql",
958
- default=None,
959
- help="Freeform SQL query to select what is copied from Main into the Branch Data Source",
960
- required=False,
961
- )
962
- @click.option(
963
- "--sql-from-main",
964
- is_flag=True,
965
- default=False,
966
- help="SQL query selecting * from the same Data Source in Main",
967
- required=False,
968
- )
969
- @click.option("--wait", is_flag=True, default=False, help="Wait for copy job to finish, disabled by default")
970
- @coro
971
- async def datasource_copy_from_main(datasource_name: str, sql: str, sql_from_main: bool, wait: bool) -> None:
972
- """Copy data source from Main."""
973
-
974
- if sql and sql_from_main:
975
- raise CLIException(FeedbackManager.error_exception(error="Use --sql or --sql-from-main but not both"))
976
-
977
- if not sql and not sql_from_main:
978
- raise CLIException(FeedbackManager.error_exception(error="Use --sql or --sql-from-main"))
979
-
980
- config = CLIConfig.get_project_config()
981
-
982
- current_main_workspace = await get_current_main_workspace(config)
983
- assert isinstance(current_main_workspace, dict)
984
-
985
- if current_main_workspace["id"] == config["id"] and sql_from_main:
986
- raise CLIException(FeedbackManager.error_not_allowed_in_main_branch())
987
-
988
- client = config.get_client()
989
-
990
- response = await client.datasource_query_copy(
991
- datasource_name, sql if sql else f"SELECT * FROM main.{datasource_name}"
992
- )
993
- if "job" not in response:
994
- raise CLIBranchException(response)
995
- job_id = response["job"]["job_id"]
996
- job_url = response["job"]["job_url"]
997
- if sql:
998
- click.echo(FeedbackManager.info_copy_with_sql_job_url(sql=sql, datasource_name=datasource_name, url=job_url))
999
- else:
1000
- click.echo(FeedbackManager.info_copy_from_main_job_url(datasource_name=datasource_name, url=job_url))
1001
- if wait:
1002
- base_msg = "Copy from Main Workspace" if sql_from_main else f"Copy from {sql}"
1003
- await wait_job(client, job_id, job_url, f"{base_msg} to {datasource_name}")
1004
-
1005
-
1006
- async def warn_if_in_live(semver: str) -> None:
1007
- if not semver:
1008
- return
1009
-
1010
- config = CLIConfig.get_project_config()
1011
- current_workspace = await get_current_workspace(config)
1012
- assert isinstance(current_workspace, dict)
1013
-
1014
- is_branch = current_workspace.get("is_branch", False)
1015
- release = current_workspace.get("release", {})
1016
- live_semver = release and release.get("semver", None)
1017
-
1018
- if is_branch:
1019
- return
1020
-
1021
- if live_semver and live_semver == semver:
1022
- click.echo(FeedbackManager.warning_release_risky_operation_in_live())
1023
- click.echo("")