tinybird 0.0.1.dev18__py3-none-any.whl → 0.0.1.dev20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tinybird might be problematic. Click here for more details.

Files changed (49) hide show
  1. tinybird/client.py +24 -4
  2. tinybird/config.py +1 -1
  3. tinybird/feedback_manager.py +8 -30
  4. tinybird/tb/__cli__.py +8 -0
  5. tinybird/tb/modules/auth.py +1 -1
  6. tinybird/tb/modules/build.py +26 -80
  7. tinybird/tb/modules/cicd.py +1 -1
  8. tinybird/tb/modules/cli.py +11 -837
  9. tinybird/tb/modules/common.py +1 -55
  10. tinybird/tb/modules/connection.py +1 -1
  11. tinybird/tb/modules/create.py +18 -7
  12. tinybird/tb/modules/datafile/build.py +142 -971
  13. tinybird/tb/modules/datafile/build_common.py +1 -1
  14. tinybird/tb/modules/datafile/build_datasource.py +1 -1
  15. tinybird/tb/modules/datafile/build_pipe.py +1 -1
  16. tinybird/tb/modules/datafile/common.py +12 -11
  17. tinybird/tb/modules/datafile/diff.py +1 -1
  18. tinybird/tb/modules/datafile/fixture.py +1 -1
  19. tinybird/tb/modules/datafile/format_common.py +0 -7
  20. tinybird/tb/modules/datafile/format_datasource.py +0 -2
  21. tinybird/tb/modules/datafile/format_pipe.py +0 -2
  22. tinybird/tb/modules/datafile/parse_datasource.py +1 -1
  23. tinybird/tb/modules/datafile/parse_pipe.py +1 -1
  24. tinybird/tb/modules/datafile/pull.py +1 -1
  25. tinybird/tb/modules/datasource.py +4 -75
  26. tinybird/tb/modules/feedback_manager.py +1048 -0
  27. tinybird/tb/modules/fmt.py +1 -1
  28. tinybird/tb/modules/job.py +1 -1
  29. tinybird/tb/modules/llm.py +6 -4
  30. tinybird/tb/modules/local.py +26 -21
  31. tinybird/tb/modules/local_common.py +2 -1
  32. tinybird/tb/modules/login.py +18 -11
  33. tinybird/tb/modules/mock.py +18 -7
  34. tinybird/tb/modules/pipe.py +4 -126
  35. tinybird/tb/modules/{build_shell.py → shell.py} +66 -36
  36. tinybird/tb/modules/table.py +88 -5
  37. tinybird/tb/modules/tag.py +2 -2
  38. tinybird/tb/modules/test.py +45 -29
  39. tinybird/tb/modules/tinyunit/tinyunit.py +1 -1
  40. tinybird/tb/modules/token.py +2 -2
  41. tinybird/tb/modules/watch.py +72 -0
  42. tinybird/tb/modules/workspace.py +1 -1
  43. tinybird/tb/modules/workspace_members.py +1 -1
  44. {tinybird-0.0.1.dev18.dist-info → tinybird-0.0.1.dev20.dist-info}/METADATA +1 -1
  45. tinybird-0.0.1.dev20.dist-info/RECORD +76 -0
  46. tinybird-0.0.1.dev18.dist-info/RECORD +0 -73
  47. {tinybird-0.0.1.dev18.dist-info → tinybird-0.0.1.dev20.dist-info}/WHEEL +0 -0
  48. {tinybird-0.0.1.dev18.dist-info → tinybird-0.0.1.dev20.dist-info}/entry_points.txt +0 -0
  49. {tinybird-0.0.1.dev18.dist-info → tinybird-0.0.1.dev20.dist-info}/top_level.txt +0 -0
@@ -7,69 +7,55 @@ import json
7
7
  import logging
8
8
  import os
9
9
  import pprint
10
- import re
11
- import shutil
12
- import sys
13
10
  from os import getcwd
14
11
  from pathlib import Path
15
- from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
12
+ from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
16
13
 
17
14
  import click
18
15
  import humanfriendly
19
16
  from click import Context
20
- from packaging import version
21
17
 
22
- import tinybird.context as context
23
18
  from tinybird.client import (
24
19
  AuthException,
25
20
  AuthNoTokenException,
26
- DoesNotExistException,
27
21
  TinyB,
28
22
  )
29
- from tinybird.config import SUPPORTED_CONNECTORS, VERSION, FeatureFlags, get_config
30
- from tinybird.feedback_manager import FeedbackManager
23
+ from tinybird.config import SUPPORTED_CONNECTORS, get_config
24
+ from tinybird.tb import __cli__
31
25
  from tinybird.tb.modules.common import (
32
- OLDEST_ROLLBACK,
33
26
  CatchAuthExceptions,
34
27
  CLIException,
35
28
  _get_tb_client,
36
29
  coro,
37
- create_tb_client,
38
30
  echo_safe_format_table,
39
31
  get_current_main_workspace,
40
32
  getenv_bool,
41
- is_major_semver,
42
- is_post_semver,
43
33
  load_connector_config,
44
- remove_release,
45
34
  try_update_config_with_remote,
46
- wait_job,
47
35
  )
48
36
  from tinybird.tb.modules.config import CLIConfig
49
- from tinybird.tb.modules.datafile.build import build_graph, folder_push
37
+ from tinybird.tb.modules.datafile.build import build_graph
50
38
  from tinybird.tb.modules.datafile.common import (
51
39
  Datafile,
52
40
  DatafileSyntaxError,
53
41
  get_project_filenames,
54
- get_resource_versions,
55
- has_internal_datafiles,
56
42
  )
57
43
  from tinybird.tb.modules.datafile.diff import diff_command
58
44
  from tinybird.tb.modules.datafile.exceptions import (
59
- AlreadyExistsException,
60
45
  ParseException,
61
46
  )
62
47
  from tinybird.tb.modules.datafile.parse_datasource import parse_datasource
63
48
  from tinybird.tb.modules.datafile.parse_pipe import parse_pipe
64
49
  from tinybird.tb.modules.datafile.pull import folder_pull
50
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
65
51
  from tinybird.tb.modules.local_common import get_tinybird_local_client
66
- from tinybird.tb.modules.telemetry import add_telemetry_event
67
52
 
68
53
  __old_click_echo = click.echo
69
54
  __old_click_secho = click.secho
70
55
  DEFAULT_PATTERNS: List[Tuple[str, Union[str, Callable[[str], str]]]] = [
71
56
  (r"p\.ey[A-Za-z0-9-_\.]+", lambda v: f"{v[:4]}...{v[-8:]}")
72
57
  ]
58
+ VERSION = f"{__cli__.__version__} (rev {__cli__.__revision__})"
73
59
 
74
60
 
75
61
  @click.group(cls=CatchAuthExceptions, context_settings={"help_option_names": ["-h", "--help"]})
@@ -232,7 +218,7 @@ async def cli(
232
218
  load_connector_config(ctx, connector, debug, check_uninstalled=True)
233
219
 
234
220
 
235
- @cli.command()
221
+ @cli.command(hidden=True)
236
222
  @click.argument("filenames", type=click.Path(exists=True), nargs=-1, default=None)
237
223
  @click.option("--debug", is_flag=True, default=False, help="Print internal representation")
238
224
  def check(filenames: List[str], debug: bool) -> None:
@@ -271,208 +257,14 @@ def check(filenames: List[str], debug: bool) -> None:
271
257
 
272
258
  except DatafileSyntaxError as e:
273
259
  # TODO(eclbg): add the filename to the error message
274
- raise CLIException(e)
260
+ raise CLIException(str(e))
275
261
  except ParseException as e:
276
262
  raise CLIException(FeedbackManager.error_exception(error=e))
277
263
 
278
264
  process(filenames=filenames)
279
265
 
280
266
 
281
- @cli.command()
282
- @click.option(
283
- "--dry-run",
284
- is_flag=True,
285
- default=False,
286
- help="Run the command without creating resources on the Tinybird account or any side effect",
287
- )
288
- @click.option(
289
- "--check/--no-check", is_flag=True, default=True, help="Enable/Disable output checking, enabled by default"
290
- )
291
- @click.option("--push-deps", is_flag=True, default=False, help="Push dependencies, disabled by default")
292
- @click.option(
293
- "--only-changes",
294
- is_flag=True,
295
- default=False,
296
- help="Push only the resources that have changed compared to the destination workspace",
297
- )
298
- @click.option(
299
- "--debug",
300
- is_flag=True,
301
- default=False,
302
- help="Prints internal representation, can be combined with any command to get more information.",
303
- )
304
- @click.option("-f", "--force", is_flag=True, default=False, help="Override pipes when they already exist")
305
- @click.option(
306
- "--override-datasource",
307
- is_flag=True,
308
- default=False,
309
- help="When pushing a pipe with a Materialized node if the target Data Source exists it will try to override it.",
310
- )
311
- @click.option("--populate", is_flag=True, default=False, help="Populate materialized nodes when pushing them")
312
- @click.option(
313
- "--subset",
314
- type=float,
315
- default=None,
316
- help="Populate with a subset percent of the data (limited to a maximum of 2M rows), this is useful to quickly test a materialized node with some data. The subset must be greater than 0 and lower than 0.1. A subset of 0.1 means a 10 percent of the data in the source Data Source will be used to populate the materialized view. Use it together with --populate, it has precedence over --sql-condition",
317
- )
318
- @click.option(
319
- "--sql-condition",
320
- type=str,
321
- default=None,
322
- help="Populate with a SQL condition to be applied to the trigger Data Source of the Materialized View. For instance, `--sql-condition='date == toYYYYMM(now())'` it'll populate taking all the rows from the trigger Data Source which `date` is the current month. Use it together with --populate. --sql-condition is not taken into account if the --subset param is present. Including in the ``sql_condition`` any column present in the Data Source ``engine_sorting_key`` will make the populate job process less data.",
323
- )
324
- @click.option(
325
- "--unlink-on-populate-error",
326
- is_flag=True,
327
- default=False,
328
- help="If the populate job fails the Materialized View is unlinked and new data won't be ingested in the Materialized View. First time a populate job fails, the Materialized View is always unlinked.",
329
- )
330
- @click.option("--fixtures", is_flag=True, default=False, help="Append fixtures to data sources")
331
- @click.option(
332
- "--wait",
333
- is_flag=True,
334
- default=False,
335
- help="To be used along with --populate command. Waits for populate jobs to finish, showing a progress bar. Disabled by default.",
336
- )
337
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
338
- @click.option(
339
- "--only-response-times", is_flag=True, default=False, help="Checks only response times, when --force push a pipe"
340
- )
341
- @click.argument("filenames", type=click.Path(exists=True), nargs=-1, default=None)
342
- @click.option("--workspace_map", nargs=2, type=str, multiple=True)
343
- @click.option(
344
- "--workspace",
345
- nargs=2,
346
- type=str,
347
- multiple=True,
348
- help="add a workspace path to the list of external workspaces, usage: --workspace name path/to/folder",
349
- )
350
- @click.option(
351
- "--no-versions",
352
- is_flag=True,
353
- default=False,
354
- help="when set, resource dependency versions are not used, it pushes the dependencies as-is",
355
- )
356
- @click.option(
357
- "-l", "--limit", type=click.IntRange(0, 100), default=0, required=False, help="Number of requests to validate"
358
- )
359
- @click.option(
360
- "--sample-by-params",
361
- type=click.IntRange(1, 100),
362
- default=1,
363
- required=False,
364
- help="When set, we will aggregate the pipe_stats_rt requests by extractURLParameterNames(assumeNotNull(url)) and for each combination we will take a sample of N requests",
365
- )
366
- @click.option(
367
- "-ff", "--failfast", is_flag=True, default=False, help="When set, the checker will exit as soon one test fails"
368
- )
369
- @click.option(
370
- "--ignore-order", is_flag=True, default=False, help="When set, the checker will ignore the order of list properties"
371
- )
372
- @click.option(
373
- "--validate-processed-bytes",
374
- is_flag=True,
375
- default=False,
376
- help="When set, the checker will validate that the new version doesn't process more than 25% than the current version",
377
- )
378
- @click.option(
379
- "--check-requests-from-main",
380
- is_flag=True,
381
- default=False,
382
- help="When set, the checker will get Main Workspace requests",
383
- hidden=True,
384
- )
385
- @click.option(
386
- "--folder",
387
- default=".",
388
- help="Folder from where to execute the command. By default the current folder",
389
- hidden=True,
390
- type=click.types.STRING,
391
- )
392
- @click.option(
393
- "--user_token",
394
- is_flag=False,
395
- default=None,
396
- help="The user token is required for sharing a datasource that contains the SHARED_WITH entry.",
397
- type=click.types.STRING,
398
- )
399
- @click.pass_context
400
- @coro
401
- async def push(
402
- ctx: Context,
403
- filenames: Optional[List[str]],
404
- dry_run: bool,
405
- check: bool,
406
- push_deps: bool,
407
- only_changes: bool,
408
- debug: bool,
409
- force: bool,
410
- override_datasource: bool,
411
- populate: bool,
412
- subset: Optional[float],
413
- sql_condition: Optional[str],
414
- unlink_on_populate_error: bool,
415
- fixtures: bool,
416
- wait: bool,
417
- yes: bool,
418
- only_response_times: bool,
419
- workspace_map,
420
- workspace,
421
- no_versions: bool,
422
- limit: int,
423
- sample_by_params: int,
424
- failfast: bool,
425
- ignore_order: bool,
426
- validate_processed_bytes: bool,
427
- check_requests_from_main: bool,
428
- folder: str,
429
- user_token: Optional[str],
430
- ) -> None:
431
- """Push files to Tinybird."""
432
-
433
- ignore_sql_errors = FeatureFlags.ignore_sql_errors()
434
- context.disable_template_security_validation.set(True)
435
-
436
- is_internal = has_internal_datafiles(folder)
437
-
438
- await folder_push(
439
- create_tb_client(ctx),
440
- filenames,
441
- dry_run,
442
- check,
443
- push_deps,
444
- only_changes,
445
- debug=debug,
446
- force=force,
447
- override_datasource=override_datasource,
448
- populate=populate,
449
- populate_subset=subset,
450
- populate_condition=sql_condition,
451
- unlink_on_populate_error=unlink_on_populate_error,
452
- upload_fixtures=fixtures,
453
- wait=wait,
454
- ignore_sql_errors=ignore_sql_errors,
455
- skip_confirmation=yes,
456
- only_response_times=only_response_times,
457
- workspace_map=dict(workspace_map),
458
- workspace_lib_paths=workspace,
459
- no_versions=no_versions,
460
- run_tests=False,
461
- tests_to_run=limit,
462
- tests_sample_by_params=sample_by_params,
463
- tests_failfast=failfast,
464
- tests_ignore_order=ignore_order,
465
- tests_validate_processed_bytes=validate_processed_bytes,
466
- tests_check_requests_from_branch=check_requests_from_main,
467
- folder=folder,
468
- config=CLIConfig.get_project_config(),
469
- user_token=user_token,
470
- is_internal=is_internal,
471
- )
472
- return
473
-
474
-
475
- @cli.command()
267
+ @cli.command(hidden=True)
476
268
  @click.option(
477
269
  "--folder", default=None, type=click.Path(exists=True, file_okay=False), help="Folder where files will be placed"
478
270
  )
@@ -489,7 +281,7 @@ async def pull(ctx: Context, folder: str, force: bool, fmt: bool) -> None:
489
281
  return await folder_pull(client, folder, force, fmt=fmt)
490
282
 
491
283
 
492
- @cli.command()
284
+ @cli.command(hidden=True)
493
285
  @click.option("--no-deps", is_flag=True, default=False, help="Print only data sources with no pipes using them")
494
286
  @click.option("--match", default=None, help="Retrieve any resource matching the pattern")
495
287
  @click.option("--pipe", default=None, help="Retrieve any resource used by pipe")
@@ -641,11 +433,6 @@ async def sql(
641
433
  f"SELECT * FROM ({query}) LIMIT {rows_limit} FORMAT {req_format}", pipeline=pipeline
642
434
  )
643
435
  elif pipe and node:
644
- datasources: List[Dict[str, Any]] = await client.datasources()
645
- pipes: List[Dict[str, Any]] = await client.pipes()
646
-
647
- existing_resources: List[str] = [x["name"] for x in datasources] + [x["name"] for x in pipes]
648
- resource_versions = get_resource_versions(existing_resources)
649
436
  filenames = [pipe]
650
437
 
651
438
  # build graph to get new versions for all the files involved in the query
@@ -658,22 +445,6 @@ async def sql(
658
445
  skip_connectors=True,
659
446
  )
660
447
 
661
- # update existing versions
662
- latest_datasource_versions = resource_versions.copy()
663
-
664
- for dep in dependencies_graph.to_run.values():
665
- ds = dep["resource_name"]
666
- if dep["version"] is not None:
667
- latest_datasource_versions[ds] = dep["version"]
668
-
669
- # build the graph again with the rigth version
670
- dependencies_graph = await build_graph(
671
- filenames,
672
- client,
673
- dir_path=".",
674
- resource_versions=latest_datasource_versions,
675
- verbose=False,
676
- )
677
448
  query = ""
678
449
  for _, elem in dependencies_graph.to_run.items():
679
450
  for _node in elem["nodes"]:
@@ -715,291 +486,6 @@ async def sql(
715
486
  click.echo(FeedbackManager.info_no_rows())
716
487
 
717
488
 
718
- @cli.command(
719
- name="materialize",
720
- short_help="Given a local Pipe datafile (.pipe) and a node name it generates the target Data Source and materialized Pipe ready to be pushed and guides you through the process to create the materialized view",
721
- )
722
- @click.argument("filename", type=click.Path(exists=True))
723
- @click.argument("target_datasource", default=None, required=False)
724
- @click.option("--push-deps", is_flag=True, default=False, help="Push dependencies, disabled by default")
725
- @click.option("--workspace_map", nargs=2, type=str, multiple=True, hidden=True)
726
- @click.option(
727
- "--workspace",
728
- nargs=2,
729
- type=str,
730
- multiple=True,
731
- help="add a workspace path to the list of external workspaces, usage: --workspace name path/to/folder",
732
- )
733
- @click.option(
734
- "--no-versions",
735
- is_flag=True,
736
- default=False,
737
- help="when set, resource dependency versions are not used, it pushes the dependencies as-is",
738
- )
739
- @click.option("--verbose", is_flag=True, default=False, help="Prints more log")
740
- @click.option("--force-populate", default=None, required=False, help="subset or full", hidden=True)
741
- @click.option(
742
- "--unlink-on-populate-error",
743
- is_flag=True,
744
- default=False,
745
- help="If the populate job fails the Materialized View is unlinked and new data won't be ingested in the Materialized View. First time a populate job fails, the Materialized View is always unlinked.",
746
- )
747
- @click.option("--override-pipe", is_flag=True, default=False, help="Override pipe if exists or prompt", hidden=True)
748
- @click.option(
749
- "--override-datasource", is_flag=True, default=False, help="Override data source if exists or prompt", hidden=True
750
- )
751
- @click.pass_context
752
- @coro
753
- async def materialize(
754
- ctx: Context,
755
- filename: str,
756
- push_deps: bool,
757
- workspace_map: List[str],
758
- workspace: List[str],
759
- no_versions: bool,
760
- verbose: bool,
761
- force_populate: Optional[str],
762
- unlink_on_populate_error: bool,
763
- override_pipe: bool,
764
- override_datasource: bool,
765
- target_datasource: Optional[str] = None,
766
- ) -> None:
767
- deprecation_notice = FeedbackManager.warning_deprecated(
768
- warning="'tb materialize' is deprecated. To create a Materialized View in a guided way you can use the UI: `Create Pipe` and use the `Create Materialized View` wizard. Finally download the resulting `.pipe` and `.datasource` files."
769
- )
770
- click.echo(deprecation_notice)
771
- cl = create_tb_client(ctx)
772
-
773
- async def _try_push_pipe_to_analyze(pipe_name):
774
- try:
775
- to_run = await folder_push(
776
- cl,
777
- filenames=[filename],
778
- dry_run=False,
779
- check=False,
780
- push_deps=push_deps,
781
- debug=False,
782
- force=False,
783
- workspace_map=dict(workspace_map),
784
- workspace_lib_paths=workspace,
785
- no_versions=no_versions,
786
- run_tests=False,
787
- as_standard=True,
788
- raise_on_exists=True,
789
- verbose=verbose,
790
- )
791
- except AlreadyExistsException as e:
792
- if "Datasource" in str(e):
793
- click.echo(str(e))
794
- return
795
- if override_pipe or click.confirm(FeedbackManager.info_pipe_exists(name=pipe_name)):
796
- to_run = await folder_push(
797
- cl,
798
- filenames=[filename],
799
- dry_run=False,
800
- check=False,
801
- push_deps=push_deps,
802
- debug=False,
803
- force=True,
804
- workspace_map=dict(workspace_map),
805
- workspace_lib_paths=workspace,
806
- no_versions=no_versions,
807
- run_tests=False,
808
- as_standard=True,
809
- verbose=verbose,
810
- )
811
- else:
812
- return
813
- except click.ClickException as ex:
814
- # HACK: By now, datafile raises click.ClickException instead of
815
- # CLIException to avoid circular imports. Thats we need to trace
816
- # the error here.
817
- #
818
- # Once we do a big refactor in datafile, we can get rid of this
819
- # snippet.
820
- msg: str = str(ex)
821
- add_telemetry_event("datafile_error", error=msg)
822
- click.echo(msg)
823
-
824
- return to_run
825
-
826
- def _choose_node_name(pipe):
827
- node = pipe["nodes"][0]
828
- materialized_nodes = [node for node in pipe["nodes"] if node["type"].lower() == "materialized"]
829
-
830
- if len(materialized_nodes) == 1:
831
- node = materialized_nodes[0]
832
-
833
- if len(pipe["nodes"]) > 1 and len(materialized_nodes) != 1:
834
- for index, node in enumerate(pipe["nodes"], start=1):
835
- click.echo(f" [{index}] Materialize node with name => {node['name']}")
836
- option = click.prompt(FeedbackManager.prompt_choose_node(), default=len(pipe["nodes"]))
837
- node = pipe["nodes"][option - 1]
838
- node_name = node["name"]
839
- return node, node_name
840
-
841
- def _choose_target_datasource_name(pipe, node, node_name):
842
- return target_datasource or node.get("datasource", None) or f'mv_{pipe["resource_name"]}_{node_name}'
843
-
844
- def _save_local_backup_pipe(pipe):
845
- pipe_bak = f"{filename}_bak"
846
- shutil.copyfile(filename, pipe_bak)
847
- pipe_file_name = f"{pipe['resource_name']}.pipe"
848
- click.echo(FeedbackManager.info_pipe_backup_created(name=pipe_bak))
849
- return pipe_file_name
850
-
851
- def _save_local_datasource(datasource_name, ds_datafile):
852
- base = Path("datasources")
853
- if not base.exists():
854
- base = Path()
855
- file_name = f"{datasource_name}.datasource"
856
- f = base / file_name
857
- with open(f"{f}", "w") as file:
858
- file.write(ds_datafile)
859
-
860
- click.echo(FeedbackManager.success_generated_local_file(file=f))
861
- return f
862
-
863
- async def _try_push_datasource(datasource_name, f):
864
- exists = False
865
- try:
866
- exists = await cl.get_datasource(datasource_name)
867
- except Exception:
868
- pass
869
-
870
- if exists:
871
- click.echo(FeedbackManager.info_materialize_push_datasource_exists(name=f.name))
872
- if override_datasource or click.confirm(FeedbackManager.info_materialize_push_datasource_override(name=f)):
873
- try:
874
- await cl.datasource_delete(datasource_name, force=True)
875
- except DoesNotExistException:
876
- pass
877
-
878
- filename = str(f.absolute())
879
- to_run = await folder_push(
880
- cl,
881
- filenames=[filename],
882
- push_deps=push_deps,
883
- workspace_map=dict(workspace_map),
884
- workspace_lib_paths=workspace,
885
- no_versions=no_versions,
886
- verbose=verbose,
887
- )
888
- return to_run
889
-
890
- def _save_local_pipe(pipe_file_name, pipe_datafile, pipe):
891
- base = Path("pipes")
892
- if not base.exists():
893
- base = Path()
894
- f_pipe = base / pipe_file_name
895
-
896
- with open(f"{f_pipe}", "w") as file:
897
- if pipe["version"] is not None and pipe["version"] >= 0:
898
- pipe_datafile = f"VERSION {pipe['version']} \n {pipe_datafile}"
899
- matches = re.findall(r"([^\s\.]*__v\d+)", pipe_datafile)
900
- for match in set(matches):
901
- m = match.split("__v")[0]
902
- if m in pipe_datafile:
903
- pipe_datafile = pipe_datafile.replace(match, m)
904
- file.write(pipe_datafile)
905
-
906
- click.echo(FeedbackManager.success_generated_local_file(file=f_pipe))
907
- return f_pipe
908
-
909
- async def _try_push_pipe(f_pipe):
910
- if override_pipe:
911
- option = 2
912
- else:
913
- click.echo(FeedbackManager.info_materialize_push_pipe_skip(name=f_pipe.name))
914
- click.echo(FeedbackManager.info_materialize_push_pipe_override(name=f_pipe.name))
915
- option = click.prompt(FeedbackManager.prompt_choose(), default=1)
916
- force = True
917
- check = option == 1
918
-
919
- filename = str(f_pipe.absolute())
920
- to_run = await folder_push(
921
- cl,
922
- filenames=[filename],
923
- dry_run=False,
924
- check=check,
925
- push_deps=push_deps,
926
- debug=False,
927
- force=force,
928
- unlink_on_populate_error=unlink_on_populate_error,
929
- workspace_map=dict(workspace_map),
930
- workspace_lib_paths=workspace,
931
- no_versions=no_versions,
932
- run_tests=False,
933
- verbose=verbose,
934
- )
935
- return to_run
936
-
937
- async def _populate(pipe, node_name, f_pipe):
938
- if force_populate or click.confirm(FeedbackManager.prompt_populate(file=f_pipe)):
939
- if not force_populate:
940
- click.echo(FeedbackManager.info_materialize_populate_partial())
941
- click.echo(FeedbackManager.info_materialize_populate_full())
942
- option = click.prompt(FeedbackManager.prompt_choose(), default=1)
943
- else:
944
- option = 1 if force_populate == "subset" else 2
945
- populate = False
946
- populate_subset = False
947
- if option == 1:
948
- populate_subset = 0.1
949
- populate = True
950
- elif option == 2:
951
- populate = True
952
-
953
- if populate:
954
- response = await cl.populate_node(
955
- pipe["name"],
956
- node_name,
957
- populate_subset=populate_subset,
958
- unlink_on_populate_error=unlink_on_populate_error,
959
- )
960
- if "job" not in response:
961
- raise CLIException(response)
962
-
963
- job_url = response.get("job", {}).get("job_url", None)
964
- job_id = response.get("job", {}).get("job_id", None)
965
- click.echo(FeedbackManager.info_populate_job_url(url=job_url))
966
- wait_populate = True
967
- if wait_populate:
968
- await wait_job(cl, job_id, job_url, "Populating")
969
-
970
- click.echo(FeedbackManager.warning_beta_tester())
971
- pipe_name = os.path.basename(filename).rsplit(".", 1)[0]
972
- click.echo(FeedbackManager.info_before_push_materialize(name=filename))
973
- try:
974
- # extracted the materialize logic to local functions so the workflow is more readable
975
- to_run = await _try_push_pipe_to_analyze(pipe_name)
976
-
977
- if to_run is None:
978
- return
979
-
980
- pipe = to_run[pipe_name.split("/")[-1]]
981
- node, node_name = _choose_node_name(pipe)
982
- datasource_name = _choose_target_datasource_name(pipe, node, node_name)
983
-
984
- click.echo(FeedbackManager.info_before_materialize(name=pipe["name"]))
985
- analysis = await cl.analyze_pipe_node(pipe["name"], node, datasource_name=datasource_name)
986
- ds_datafile = analysis["analysis"]["datasource"]["datafile"]
987
- pipe_datafile = analysis["analysis"]["pipe"]["datafile"]
988
-
989
- pipe_file_name = _save_local_backup_pipe(pipe)
990
- f = _save_local_datasource(datasource_name, ds_datafile)
991
- await _try_push_datasource(datasource_name, f)
992
-
993
- f_pipe = _save_local_pipe(pipe_file_name, pipe_datafile, pipe)
994
- await _try_push_pipe(f_pipe)
995
- await _populate(pipe, node_name, f_pipe)
996
- click.echo(FeedbackManager.success_created_matview(name=datasource_name))
997
- except AuthNoTokenException:
998
- raise
999
- except Exception as e:
1000
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
1001
-
1002
-
1003
489
  def __patch_click_output():
1004
490
  import re
1005
491
 
@@ -1034,316 +520,4 @@ def __patch_click_output():
1034
520
 
1035
521
  def __unpatch_click_output():
1036
522
  click.echo = __old_click_echo
1037
- click.secho = __old_click_echo
1038
-
1039
-
1040
- @cli.command(short_help="Learn how to include info about the CLI in your shell PROMPT")
1041
- @click.pass_context
1042
- @coro
1043
- async def prompt(_ctx: Context) -> None:
1044
- click.secho("Follow these instructions => https://www.tinybird.co/docs/cli.html#configure-the-shell-prompt")
1045
-
1046
-
1047
- @cli.command()
1048
- @click.option(
1049
- "--dry-run",
1050
- is_flag=True,
1051
- default=False,
1052
- help="Run the command without creating resources on the Tinybird account or any side effect",
1053
- )
1054
- @click.option(
1055
- "--debug",
1056
- is_flag=True,
1057
- default=False,
1058
- help="Prints internal representation, can be combined with any command to get more information.",
1059
- )
1060
- @click.option(
1061
- "--override-datasource",
1062
- is_flag=True,
1063
- default=False,
1064
- help="When pushing a pipe with a Materialized node if the target Data Source exists it will try to override it.",
1065
- )
1066
- @click.option("--populate", is_flag=True, default=False, help="Populate materialized nodes when pushing them")
1067
- @click.option(
1068
- "--subset",
1069
- type=float,
1070
- default=None,
1071
- help="Populate with a subset percent of the data (limited to a maximum of 2M rows), this is useful to quickly test a materialized node with some data. The subset must be greater than 0 and lower than 0.1. A subset of 0.1 means a 10 percent of the data in the source Data Source will be used to populate the materialized view. Use it together with --populate, it has precedence over --sql-condition",
1072
- )
1073
- @click.option(
1074
- "--sql-condition",
1075
- type=str,
1076
- default=None,
1077
- help="Populate with a SQL condition to be applied to the trigger Data Source of the Materialized View. For instance, `--sql-condition='date == toYYYYMM(now())'` it'll populate taking all the rows from the trigger Data Source which `date` is the current month. Use it together with --populate. --sql-condition is not taken into account if the --subset param is present. Including in the ``sql_condition`` any column present in the Data Source ``engine_sorting_key`` will make the populate job process less data.",
1078
- )
1079
- @click.option(
1080
- "--unlink-on-populate-error",
1081
- is_flag=True,
1082
- default=False,
1083
- help="If the populate job fails the Materialized View is unlinked and new data won't be ingested in the Materialized View. First time a populate job fails, the Materialized View is always unlinked.",
1084
- )
1085
- @click.option(
1086
- "--wait",
1087
- is_flag=True,
1088
- default=False,
1089
- help="To be used along with --populate command. Waits for populate jobs to finish, showing a progress bar. Disabled by default.",
1090
- )
1091
- @click.option("--yes", is_flag=True, default=False, help="Do not ask for confirmation")
1092
- @click.option("--workspace_map", nargs=2, type=str, multiple=True)
1093
- @click.option(
1094
- "--workspace",
1095
- nargs=2,
1096
- type=str,
1097
- multiple=True,
1098
- help="add a workspace path to the list of external workspaces, usage: --workspace name path/to/folder",
1099
- )
1100
- @click.option(
1101
- "--folder",
1102
- default=".",
1103
- help="Folder from where to execute the command. By default the current folder",
1104
- hidden=True,
1105
- type=click.types.STRING,
1106
- )
1107
- @click.option(
1108
- "--user_token",
1109
- is_flag=False,
1110
- default=None,
1111
- help="The user token is required for sharing a datasource that contains the SHARED_WITH entry.",
1112
- type=click.types.STRING,
1113
- )
1114
- @click.option(
1115
- "--fork-downstream",
1116
- is_flag=True,
1117
- default=False,
1118
- help="Creates new versions of the dependent downstream resources to the changed resources.",
1119
- hidden=True,
1120
- )
1121
- @click.option(
1122
- "--fork",
1123
- is_flag=True,
1124
- default=False,
1125
- help="Creates new versions of the changed resources. Use --fork-downstream to fork also the downstream dependencies of the changed resources.",
1126
- hidden=True,
1127
- )
1128
- # this is added to use tb deploy in dry run mode. It's temprary => https://gitlab.com/tinybird/analytics/-/issues/12551
1129
- @click.option(
1130
- "--use-main",
1131
- is_flag=True,
1132
- default=False,
1133
- help="Use main commit instead of release commit",
1134
- hidden=True,
1135
- )
1136
- @click.option(
1137
- "--skip-head-outdated",
1138
- is_flag=True,
1139
- default=False,
1140
- help="Allows to deploy any commit without checking if the branch is rebased to the workspace commit",
1141
- hidden=True,
1142
- )
1143
- @click.pass_context
1144
- @coro
1145
- async def deploy(
1146
- ctx: Context,
1147
- dry_run: bool,
1148
- debug: bool,
1149
- override_datasource: bool,
1150
- populate: bool,
1151
- subset: Optional[float],
1152
- sql_condition: Optional[str],
1153
- unlink_on_populate_error: bool,
1154
- wait: bool,
1155
- yes: bool,
1156
- workspace_map,
1157
- workspace,
1158
- folder: str,
1159
- user_token: Optional[str],
1160
- fork_downstream: bool,
1161
- fork: bool,
1162
- use_main: bool,
1163
- skip_head_outdated: bool,
1164
- ) -> None:
1165
- """Deploy in Tinybird pushing resources changed from last git commit deployed.
1166
-
1167
- Usage: tb deploy
1168
- """
1169
- try:
1170
- ignore_sql_errors = FeatureFlags.ignore_sql_errors()
1171
- context.disable_template_security_validation.set(True)
1172
-
1173
- is_internal = has_internal_datafiles(folder)
1174
-
1175
- client: TinyB = ctx.ensure_object(dict)["client"]
1176
- config = CLIConfig.get_project_config()
1177
- workspaces: List[Dict[str, Any]] = (await client.user_workspaces_and_branches()).get("workspaces", [])
1178
- current_ws: Dict[str, Any] = next(
1179
- (workspace for workspace in workspaces if config and workspace.get("id", ".") == config.get("id", "..")), {}
1180
- )
1181
-
1182
- semver = config.get("semver")
1183
- auto_promote = getenv_bool("TB_AUTO_PROMOTE", True)
1184
- release = current_ws.get("release", {})
1185
- current_semver: Optional[str] = None
1186
- if release and isinstance(release, dict):
1187
- current_semver = release.get("semver")
1188
-
1189
- if not current_semver:
1190
- click.echo(FeedbackManager.error_init_release(workspace=current_ws.get("name")))
1191
- sys.exit(1)
1192
-
1193
- release_created = False
1194
- new_release = False
1195
- check_backfill_required = False
1196
-
1197
- # semver is now optional, if not sent force the bump
1198
- if not semver:
1199
- semver = current_semver
1200
- else:
1201
- click.echo(FeedbackManager.warning_deprecated_releases())
1202
-
1203
- if semver and current_semver:
1204
- new_version = version.parse(semver.split("-snapshot")[0])
1205
- current_version = version.parse(current_semver.split("-snapshot")[0])
1206
- show_feedback = new_version != current_version
1207
- if show_feedback:
1208
- if dry_run:
1209
- click.echo(
1210
- FeedbackManager.info_dry_releases_detected(current_semver=current_version, semver=new_version)
1211
- )
1212
- else:
1213
- click.echo(
1214
- FeedbackManager.info_releases_detected(current_semver=current_version, semver=new_version)
1215
- )
1216
-
1217
- if new_version == current_version:
1218
- if current_version.post is None:
1219
- semver = str(current_version) + "-1"
1220
- new_version = version.Version(semver)
1221
- else:
1222
- semver = f"{current_version.major}.{current_version.minor}.{current_version.micro}-{current_version.post + 1}"
1223
- new_version = version.Version(semver)
1224
-
1225
- if is_post_semver(new_version, current_version):
1226
- if show_feedback:
1227
- if dry_run:
1228
- click.echo(FeedbackManager.info_dry_local_release(version=new_version))
1229
- else:
1230
- click.echo(FeedbackManager.info_local_release(version=new_version))
1231
- yes = True
1232
- auto_promote = False
1233
- new_release = False
1234
- elif is_major_semver(new_version, current_version):
1235
- if show_feedback:
1236
- if dry_run:
1237
- click.echo(FeedbackManager.info_dry_major_release(version=new_version))
1238
- else:
1239
- click.echo(FeedbackManager.info_major_release(version=new_version))
1240
- auto_promote = False
1241
- new_release = True
1242
- else:
1243
- # allows TB_AUTO_PROMOTE=0 so release is left in preview
1244
- auto_promote = getenv_bool("TB_AUTO_PROMOTE", True)
1245
- if auto_promote:
1246
- if show_feedback:
1247
- if dry_run:
1248
- click.echo(
1249
- FeedbackManager.info_dry_minor_patch_release_with_autopromote(version=new_version)
1250
- )
1251
- else:
1252
- click.echo(FeedbackManager.info_minor_patch_release_with_autopromote(version=new_version))
1253
- else:
1254
- if show_feedback:
1255
- if dry_run:
1256
- click.echo(FeedbackManager.info_dry_minor_patch_release_no_autopromote(version=new_version))
1257
- else:
1258
- click.echo(FeedbackManager.info_minor_patch_release_no_autopromote(version=new_version))
1259
- new_release = True
1260
-
1261
- if new_release:
1262
- release_created = True
1263
- fork_downstream = True
1264
- # allows TB_CHECK_BACKFILL_REQUIRED=0 so it is not checked
1265
- check_backfill_required = getenv_bool("TB_CHECK_BACKFILL_REQUIRED", True)
1266
- try:
1267
- tb_client = create_tb_client(ctx)
1268
- await folder_push(
1269
- tb_client=tb_client,
1270
- dry_run=dry_run,
1271
- check=False,
1272
- push_deps=True,
1273
- debug=debug,
1274
- force=True,
1275
- git_release=True,
1276
- override_datasource=override_datasource,
1277
- populate=populate,
1278
- populate_subset=subset,
1279
- populate_condition=sql_condition,
1280
- unlink_on_populate_error=unlink_on_populate_error,
1281
- upload_fixtures=False,
1282
- wait=wait,
1283
- ignore_sql_errors=ignore_sql_errors,
1284
- skip_confirmation=yes,
1285
- workspace_map=dict(workspace_map),
1286
- workspace_lib_paths=workspace,
1287
- run_tests=False,
1288
- folder=folder,
1289
- config=config,
1290
- user_token=user_token,
1291
- fork_downstream=fork_downstream,
1292
- fork=fork,
1293
- is_internal=is_internal,
1294
- release_created=release_created,
1295
- auto_promote=auto_promote,
1296
- check_backfill_required=check_backfill_required,
1297
- use_main=use_main,
1298
- check_outdated=not skip_head_outdated,
1299
- )
1300
- except Exception as e:
1301
- if release_created and not dry_run:
1302
- await client.release_failed(config["id"], semver)
1303
- raise e
1304
-
1305
- if release_created:
1306
- try:
1307
- if not dry_run:
1308
- await client.release_preview(config["id"], semver)
1309
- click.echo(FeedbackManager.success_release_preview(semver=semver))
1310
- else:
1311
- click.echo(FeedbackManager.success_dry_release_preview(semver=semver))
1312
- if auto_promote:
1313
- try:
1314
- force_remove = getenv_bool("TB_FORCE_REMOVE_OLDEST_ROLLBACK", False)
1315
- if dry_run:
1316
- click.echo(FeedbackManager.warning_dry_remove_oldest_rollback(semver=semver))
1317
- else:
1318
- click.echo(FeedbackManager.warning_remove_oldest_rollback(semver=semver))
1319
- try:
1320
- await remove_release(dry_run, config, OLDEST_ROLLBACK, client, force=force_remove)
1321
- except Exception as e:
1322
- click.echo(FeedbackManager.error_remove_oldest_rollback(error=str(e), semver=semver))
1323
- sys.exit(1)
1324
-
1325
- if not dry_run:
1326
- release = await client.release_promote(config["id"], semver)
1327
- click.echo(FeedbackManager.success_release_promote(semver=semver))
1328
- click.echo(FeedbackManager.success_git_release(release_commit=release["commit"]))
1329
- else:
1330
- click.echo(FeedbackManager.success_dry_release_promote(semver=semver))
1331
- click.echo(FeedbackManager.success_dry_git_release(release_commit=release["commit"]))
1332
- except Exception as e:
1333
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
1334
- except Exception as e:
1335
- raise CLIException(FeedbackManager.error_exception(error=str(e)))
1336
-
1337
- if not new_release:
1338
- if dry_run:
1339
- if show_feedback:
1340
- click.echo(FeedbackManager.success_dry_release_update(semver=current_semver, new_semver=semver))
1341
- else:
1342
- client.semver = None
1343
- await client.update_release_semver(config["id"], current_semver, semver)
1344
- if show_feedback:
1345
- click.echo(FeedbackManager.success_release_update(semver=current_semver, new_semver=semver))
1346
- except AuthNoTokenException:
1347
- raise
1348
- except Exception as e:
1349
- raise CLIException(str(e))
523
+ click.secho = __old_click_secho