tinybird 0.0.1.dev246__py3-none-any.whl → 0.0.1.dev248__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 (30) hide show
  1. tinybird/ch_utils/constants.py +2 -0
  2. tinybird/prompts.py +2 -0
  3. tinybird/tb/__cli__.py +2 -2
  4. tinybird/tb/modules/agent/agent.py +107 -18
  5. tinybird/tb/modules/agent/models.py +6 -0
  6. tinybird/tb/modules/agent/prompts.py +57 -29
  7. tinybird/tb/modules/agent/tools/append.py +55 -0
  8. tinybird/tb/modules/agent/tools/build.py +1 -0
  9. tinybird/tb/modules/agent/tools/create_datafile.py +8 -3
  10. tinybird/tb/modules/agent/tools/deploy.py +1 -1
  11. tinybird/tb/modules/agent/tools/mock.py +59 -0
  12. tinybird/tb/modules/agent/tools/plan.py +1 -1
  13. tinybird/tb/modules/agent/tools/read_fixture_data.py +28 -0
  14. tinybird/tb/modules/agent/utils.py +296 -3
  15. tinybird/tb/modules/build.py +4 -1
  16. tinybird/tb/modules/build_common.py +2 -3
  17. tinybird/tb/modules/cli.py +9 -1
  18. tinybird/tb/modules/create.py +1 -1
  19. tinybird/tb/modules/feedback_manager.py +1 -0
  20. tinybird/tb/modules/llm.py +1 -1
  21. tinybird/tb/modules/login.py +6 -301
  22. tinybird/tb/modules/login_common.py +310 -0
  23. tinybird/tb/modules/mock.py +3 -69
  24. tinybird/tb/modules/mock_common.py +71 -0
  25. tinybird/tb/modules/project.py +9 -0
  26. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/METADATA +1 -1
  27. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/RECORD +30 -25
  28. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/WHEEL +0 -0
  29. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/entry_points.txt +0 -0
  30. {tinybird-0.0.1.dev246.dist-info → tinybird-0.0.1.dev248.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,7 @@
1
+ import difflib
1
2
  import os
2
3
  from contextlib import contextmanager
3
- from typing import Any, Callable, List, Optional
4
+ from typing import Any, Callable, List, Optional, Tuple
4
5
 
5
6
  import click
6
7
  from prompt_toolkit.application import Application, get_app
@@ -13,9 +14,17 @@ from prompt_toolkit.layout.dimension import LayoutDimension as D
13
14
  from prompt_toolkit.mouse_events import MouseEventType
14
15
  from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
15
16
  from prompt_toolkit.shortcuts import PromptSession
16
- from prompt_toolkit.styles import Style
17
+ from prompt_toolkit.styles import Style as PromptStyle
17
18
  from pydantic import BaseModel, Field
18
19
 
20
+ try:
21
+ from colorama import Back, Fore, Style, init
22
+
23
+ init(autoreset=True)
24
+ COLORAMA_AVAILABLE = True
25
+ except ImportError:
26
+ COLORAMA_AVAILABLE = False
27
+
19
28
 
20
29
  class TinybirdAgentContext(BaseModel):
21
30
  folder: str
@@ -26,9 +35,13 @@ class TinybirdAgentContext(BaseModel):
26
35
  build_project: Callable[..., None]
27
36
  deploy_project: Callable[[], None]
28
37
  deploy_check_project: Callable[[], None]
38
+ mock_data: Callable[..., list[dict[str, Any]]]
39
+ append_data: Callable[..., None]
40
+ analyze_fixture: Callable[..., dict[str, Any]]
41
+ dangerously_skip_permissions: bool
29
42
 
30
43
 
31
- default_style = Style.from_dict(
44
+ default_style = PromptStyle.from_dict(
32
45
  {
33
46
  "separator": "#6C6C6C",
34
47
  "questionmark": "#FF9D00 bold",
@@ -384,3 +397,283 @@ class Datafile(BaseModel):
384
397
  description: str
385
398
  pathname: str
386
399
  dependencies: List[str] = Field(default_factory=list)
400
+
401
+
402
+ def create_terminal_box(content: str, new_content: Optional[str] = None, title: Optional[str] = None) -> str:
403
+ """
404
+ Create a formatted box with automatic line numbers that fills the terminal width.
405
+ Optionally shows a diff between content and new_content.
406
+
407
+ Args:
408
+ content: The original text content to display in the box (without line numbers)
409
+ new_content: Optional new content to show as a diff against the original
410
+ title: Optional title to display as header, if not provided will use first line of content
411
+
412
+ Returns:
413
+ A string containing the formatted box with line numbers added
414
+ """
415
+ # Get terminal width, default to 80 if can't determine
416
+ try:
417
+ terminal_width = os.get_terminal_size().columns
418
+ except:
419
+ terminal_width = 80
420
+
421
+ # Box characters
422
+ top_left = "╭"
423
+ top_right = "╮"
424
+ bottom_left = "╰"
425
+ bottom_right = "╯"
426
+ horizontal = "─"
427
+ vertical = "│"
428
+
429
+ # Calculate available width for content (terminal_width - 2 borders - 2 spaces padding)
430
+ available_width = terminal_width - 4
431
+
432
+ # Split content into lines
433
+ lines = content.strip().split("\n")
434
+ new_lines = new_content.strip().split("\n") if new_content else []
435
+
436
+ # Check if we have a title parameter or should use first line as header
437
+ header = title
438
+ content_lines = lines
439
+ new_content_lines = new_lines
440
+
441
+ if header is None and lines:
442
+ # Use first line as header if no title provided
443
+ header = lines[0]
444
+ content_lines = lines[1:] if len(lines) > 1 else []
445
+ if new_lines:
446
+ # Skip header in new content too
447
+ new_content_lines = new_lines[1:] if len(new_lines) > 1 else []
448
+ elif header is not None:
449
+ # Title provided, use all content lines as-is
450
+ content_lines = lines
451
+ new_content_lines = new_lines
452
+
453
+ # Process content lines
454
+ processed_lines = []
455
+
456
+ if new_content is None:
457
+ # No diff, just add line numbers
458
+ line_number = 1
459
+ for line in content_lines:
460
+ processed_lines.extend(_process_line(line, line_number, available_width, None))
461
+ line_number += 1
462
+ else:
463
+ # Create diff and process it properly
464
+ diff = list(
465
+ difflib.unified_diff(
466
+ content_lines,
467
+ new_content_lines,
468
+ lineterm="",
469
+ n=3, # Add some context lines
470
+ )
471
+ )
472
+
473
+ # Process the unified diff output
474
+ old_line_num = 1
475
+ new_line_num = 1
476
+ old_index = 0
477
+ new_index = 0
478
+
479
+ # Parse the diff output
480
+ i = 0
481
+ while i < len(diff):
482
+ line = diff[i]
483
+ if line.startswith("@@"):
484
+ # Parse hunk header to get line numbers
485
+ import re
486
+
487
+ match = re.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
488
+ if match:
489
+ old_line_num = int(match.group(1))
490
+ new_line_num = int(match.group(2))
491
+ old_index = old_line_num - 1
492
+ new_index = new_line_num - 1
493
+ elif line.startswith("---") or line.startswith("+++"):
494
+ # Skip file headers
495
+ pass
496
+ elif line.startswith(" "):
497
+ # Context line (unchanged)
498
+ content = line[1:] # Remove the leading space
499
+ if old_index < len(content_lines) and content_lines[old_index] == content:
500
+ processed_lines.extend(_process_line(content, old_line_num, available_width, None))
501
+ old_line_num += 1
502
+ new_line_num += 1
503
+ old_index += 1
504
+ new_index += 1
505
+ elif line.startswith("-"):
506
+ # Removed line
507
+ content = line[1:] # Remove the leading minus
508
+ processed_lines.extend(_process_line(content, old_line_num, available_width, "-"))
509
+ old_line_num += 1
510
+ old_index += 1
511
+ elif line.startswith("+"):
512
+ # Added line
513
+ content = line[1:] # Remove the leading plus
514
+ processed_lines.extend(_process_line(content, new_line_num, available_width, "+"))
515
+ new_line_num += 1
516
+ new_index += 1
517
+ i += 1
518
+
519
+ # Add any remaining unchanged lines that weren't in the diff
520
+ while old_index < len(content_lines) and new_index < len(new_content_lines):
521
+ if content_lines[old_index] == new_content_lines[new_index]:
522
+ processed_lines.extend(_process_line(content_lines[old_index], old_line_num, available_width, None))
523
+ old_line_num += 1
524
+ new_line_num += 1
525
+ old_index += 1
526
+ new_index += 1
527
+ else:
528
+ break
529
+
530
+ # Build the box
531
+ result = []
532
+
533
+ # Top border
534
+ result.append(top_left + horizontal * (terminal_width - 2) + top_right)
535
+
536
+ # Add header if exists
537
+ if header:
538
+ # Center the header
539
+ header_padding = (available_width - len(header)) // 2
540
+ header_line = (
541
+ vertical
542
+ + " "
543
+ + " " * header_padding
544
+ + header
545
+ + " " * (available_width - len(header) - header_padding)
546
+ + " "
547
+ + vertical
548
+ )
549
+ result.append(header_line)
550
+ # Empty line after header
551
+ result.append(vertical + " " * (terminal_width - 2) + vertical)
552
+
553
+ # Content lines
554
+ for line_num, content, diff_marker in processed_lines:
555
+ if line_num is not None:
556
+ # Line with number
557
+ if COLORAMA_AVAILABLE:
558
+ line_num_str = f"{Fore.LIGHTBLACK_EX}{line_num:>4}{Style.RESET_ALL}"
559
+ else:
560
+ line_num_str = f"{line_num:>4}"
561
+
562
+ if diff_marker:
563
+ if COLORAMA_AVAILABLE:
564
+ if diff_marker == "-":
565
+ # Fill the entire content area with red background
566
+ content_with_bg = f"{Back.RED}{diff_marker} {content}{Style.RESET_ALL}"
567
+ # Calculate padding needed for the content area
568
+ content_area_width = available_width - 9 # 9 is reduced prefix length
569
+ content_padding = content_area_width - len(f"{diff_marker} {content}")
570
+ if content_padding > 0:
571
+ content_with_bg = (
572
+ f"{Back.RED}{diff_marker} {content}{' ' * content_padding}{Style.RESET_ALL}"
573
+ )
574
+ line = f"{vertical} {line_num_str} {content_with_bg}"
575
+ elif diff_marker == "+":
576
+ # Fill the entire content area with green background
577
+ content_with_bg = f"{Back.GREEN}{diff_marker} {content}{Style.RESET_ALL}"
578
+ # Calculate padding needed for the content area
579
+ content_area_width = available_width - 9 # 9 is reduced prefix length
580
+ content_padding = content_area_width - len(f"{diff_marker} {content}")
581
+ if content_padding > 0:
582
+ content_with_bg = (
583
+ f"{Back.GREEN}{diff_marker} {content}{' ' * content_padding}{Style.RESET_ALL}"
584
+ )
585
+ line = f"{vertical} {line_num_str} {content_with_bg}"
586
+ else:
587
+ line = f"{vertical} {line_num:>4} {diff_marker} {content}"
588
+ else:
589
+ line = f"{vertical} {line_num_str} {content}"
590
+ else:
591
+ # Continuation line without number - fill background starting from where symbol would be
592
+ if diff_marker and COLORAMA_AVAILABLE:
593
+ if diff_marker == "-":
594
+ # Calculate how much space we need to fill with background
595
+ content_area_width = available_width - 9 # 9 is reduced prefix length
596
+ content_padding = content_area_width - len(
597
+ content
598
+ ) # Don't subtract spaces, they're in the background
599
+ if content_padding > 0:
600
+ line = f"{vertical} {Back.RED} {content}{' ' * content_padding}{Style.RESET_ALL}"
601
+ else:
602
+ line = f"{vertical} {Back.RED} {content}{Style.RESET_ALL}"
603
+ elif diff_marker == "+":
604
+ # Calculate how much space we need to fill with background
605
+ content_area_width = available_width - 9 # 9 is reduced prefix length
606
+ content_padding = content_area_width - len(
607
+ content
608
+ ) # Don't subtract spaces, they're in the background
609
+ if content_padding > 0:
610
+ line = f"{vertical} {Back.GREEN} {content}{' ' * content_padding}{Style.RESET_ALL}"
611
+ else:
612
+ line = f"{vertical} {Back.GREEN} {content}{Style.RESET_ALL}"
613
+ else:
614
+ line = f"{vertical} {content}"
615
+
616
+ # Pad to terminal width
617
+ # Need to account for ANSI escape sequences not taking visual space
618
+ if COLORAMA_AVAILABLE:
619
+ # Calculate visible length (excluding ANSI codes)
620
+ visible_line = line
621
+ # Remove all ANSI escape sequences for length calculation
622
+ import re
623
+
624
+ ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
625
+ visible_line = ansi_escape.sub("", visible_line)
626
+ padding_needed = terminal_width - len(visible_line) - 1
627
+ else:
628
+ padding_needed = terminal_width - len(line) - 1
629
+
630
+ line += " " * padding_needed + vertical
631
+ result.append(line)
632
+
633
+ # Empty line before bottom (only if we have content)
634
+ if processed_lines:
635
+ result.append(vertical + " " * (terminal_width - 2) + vertical)
636
+
637
+ # Bottom border
638
+ result.append(bottom_left + horizontal * (terminal_width - 2) + bottom_right)
639
+
640
+ return "\n".join(result)
641
+
642
+
643
+ def _process_line(
644
+ line: str, line_number: int, available_width: int, diff_marker: Optional[str]
645
+ ) -> List[Tuple[Optional[int], str, Optional[str]]]:
646
+ """
647
+ Process a single line, handling wrapping if necessary.
648
+
649
+ Returns a list of tuples (line_number, content, diff_marker)
650
+ """
651
+ # Calculate space needed for line number and spacing
652
+ # " 9999 " for normal lines or " 9999 + " for diff lines
653
+ prefix_length = 9 # Reduced from 13 to 9
654
+
655
+ # Available width for actual content
656
+ content_width = available_width - prefix_length
657
+
658
+ processed: List[Tuple[Optional[int], str, Optional[str]]] = []
659
+
660
+ if len(line) <= content_width:
661
+ # Line fits, add it as is
662
+ processed.append((line_number, line, diff_marker))
663
+ else:
664
+ # Line needs wrapping
665
+ # First line with line number
666
+ first_part = line[:content_width]
667
+ processed.append((line_number, first_part, diff_marker))
668
+
669
+ # Remaining wrapped lines without line numbers
670
+ remaining = line[content_width:]
671
+ while remaining:
672
+ if len(remaining) <= content_width:
673
+ processed.append((None, remaining, diff_marker))
674
+ break
675
+ else:
676
+ processed.append((None, remaining[:content_width], diff_marker))
677
+ remaining = remaining[content_width:]
678
+
679
+ return processed
@@ -3,7 +3,7 @@ import time
3
3
  from copy import deepcopy
4
4
  from functools import partial
5
5
  from pathlib import Path
6
- from typing import Callable, List
6
+ from typing import Any, Callable, Dict, List
7
7
  from urllib.parse import urlencode
8
8
 
9
9
  import click
@@ -32,8 +32,11 @@ def build(ctx: click.Context, watch: bool) -> None:
32
32
  """
33
33
  Validate and build the project server side.
34
34
  """
35
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
35
36
  project: Project = ctx.ensure_object(dict)["project"]
36
37
  tb_client: TinyB = ctx.ensure_object(dict)["client"]
38
+ if obj["env"] == "cloud":
39
+ raise click.ClickException(FeedbackManager.error_build_only_supported_in_local())
37
40
 
38
41
  if project.has_deeper_level():
39
42
  click.echo(
@@ -3,7 +3,7 @@ import logging
3
3
  import time
4
4
  from pathlib import Path
5
5
  from typing import Optional
6
- from urllib.parse import urlencode
6
+ from urllib.parse import urlencode, urljoin
7
7
 
8
8
  import click
9
9
  import requests
@@ -25,7 +25,6 @@ def process(
25
25
  file_changed: Optional[str] = None,
26
26
  diff: Optional[str] = None,
27
27
  silent: bool = False,
28
- error: bool = False,
29
28
  build_status: Optional[BuildStatus] = None,
30
29
  exit_on_error: bool = True,
31
30
  ) -> Optional[str]:
@@ -190,7 +189,7 @@ def build_project(project: Project, tb_client: TinyB, silent: bool = False) -> O
190
189
  ".pipe": "text/plain",
191
190
  ".connection": "text/plain",
192
191
  }
193
- TINYBIRD_API_URL = tb_client.host + "/v1/build"
192
+ TINYBIRD_API_URL = urljoin(tb_client.host, "/v1/build")
194
193
  logging.debug(TINYBIRD_API_URL)
195
194
  TINYBIRD_API_KEY = tb_client.token
196
195
  error: Optional[str] = None
@@ -79,6 +79,13 @@ agent_mode_flag = os.environ.get("TB_AGENT_MODE", "false") == "true"
79
79
  "--output", type=click.Choice(["human", "json", "csv"], case_sensitive=False), default="human", help="Output format"
80
80
  )
81
81
  @click.option("--max-depth", type=int, default=3, help="Maximum depth of the project files.")
82
+ @click.option(
83
+ "--dangerously-skip-permissions",
84
+ is_flag=True,
85
+ default=False,
86
+ help="Skip permissions check in agent mode.",
87
+ hidden=True,
88
+ )
82
89
  @click.version_option(version=VERSION)
83
90
  @click.pass_context
84
91
  def cli(
@@ -93,6 +100,7 @@ def cli(
93
100
  staging: bool,
94
101
  output: str,
95
102
  max_depth: int,
103
+ dangerously_skip_permissions: bool,
96
104
  ) -> None:
97
105
  """
98
106
  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.
@@ -196,7 +204,7 @@ def cli(
196
204
  is_agent_mode = agent_mode_flag and ctx.invoked_subcommand is None
197
205
 
198
206
  if is_agent_mode:
199
- run_agent(config, project)
207
+ run_agent(config, project, dangerously_skip_permissions)
200
208
 
201
209
 
202
210
  @cli.command(hidden=True)
@@ -21,7 +21,7 @@ from tinybird.tb.modules.feedback_manager import FeedbackManager
21
21
  from tinybird.tb.modules.llm import LLM
22
22
  from tinybird.tb.modules.llm_utils import extract_xml, parse_xml
23
23
  from tinybird.tb.modules.local_common import get_tinybird_local_client
24
- from tinybird.tb.modules.mock import create_mock_data
24
+ from tinybird.tb.modules.mock_common import create_mock_data
25
25
  from tinybird.tb.modules.project import Project
26
26
 
27
27
 
@@ -444,6 +444,7 @@ class FeedbackManager:
444
444
  error_invalid_output_format = error_message(
445
445
  "Invalid output format for this command. Supported formats are: {formats}"
446
446
  )
447
+ error_build_only_supported_in_local = error_message("Builds are only supported in Tinybird Local")
447
448
 
448
449
  info_incl_relative_path = info_message("** Relative path {path} does not exist, skipping.")
449
450
  info_ignoring_incl_file = info_message(
@@ -3,7 +3,7 @@ from typing import Optional
3
3
 
4
4
  import requests
5
5
 
6
- from tinybird.tb.modules.cli import CLIConfig
6
+ from tinybird.tb.modules.config import CLIConfig
7
7
  from tinybird.tb.modules.feedback_manager import FeedbackManager
8
8
 
9
9