tinybird 0.0.1.dev247__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.

tinybird/prompts.py CHANGED
@@ -776,6 +776,8 @@ datasource_instructions = """
776
776
  - Use always json paths to define the schema. Example: `user_id` String `json:$.user_id`,
777
777
  - Array columns are supported with a special syntax. Example: `items` Array(String) `json:$.items[:]`
778
778
  - If the datasource is using an S3 or GCS connection, they need to set IMPORT_CONNECTION_NAME, IMPORT_BUCKET_URI and IMPORT_SCHEDULE (GCS @on-demand only, S3 supports @auto too)
779
+ - Unless the user asks for them, do not include ENGINE_PARTITION_KEY and ENGINE_PRIMARY_KEY.
780
+ - DateTime64 type without precision is not supported. Use DateTime64(3) instead.
779
781
  </datasource_file_instructions>
780
782
  """
781
783
 
tinybird/tb/__cli__.py CHANGED
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/forward/commands'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '0.0.1.dev247'
8
- __revision__ = '379a827'
7
+ __version__ = '0.0.1.dev248'
8
+ __revision__ = '8eed30e'
@@ -54,6 +54,7 @@ from tinybird.tb.modules.deployment_common import create_deployment
54
54
  from tinybird.tb.modules.exceptions import CLIBuildException, CLIMockException
55
55
  from tinybird.tb.modules.feedback_manager import FeedbackManager
56
56
  from tinybird.tb.modules.local_common import get_tinybird_local_client
57
+ from tinybird.tb.modules.login_common import login
57
58
  from tinybird.tb.modules.mock_common import append_mock_data, create_mock_data
58
59
  from tinybird.tb.modules.project import Project
59
60
 
@@ -75,7 +76,7 @@ You are an interactive CLI tool that helps users with data engineering tasks. Us
75
76
 
76
77
  # Tone and style
77
78
  You should be concise, direct, and to the point.
78
- Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting.
79
+ Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting. Do not use emojis.
79
80
  Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like Bash or code comments as means to communicate with the user during the session.
80
81
  If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
81
82
  IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
@@ -116,6 +117,7 @@ You have access to the following tools:
116
117
  8. If the datafile was created successfully, but the built failed, try to fix the error and repeat the process.
117
118
 
118
119
  IMPORTANT: If the user cancels some of the steps or there is an error in file creation, DO NOT continue with the plan. Stop the process and wait for the user before using any other tool.
120
+ IMPORTANT: Every time you finish a plan and start a new resource creation or update process, create a new plan before starting with the changes.
119
121
 
120
122
  # When planning the creation or update of resources:
121
123
  {plan_instructions}
@@ -217,18 +219,39 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
217
219
 
218
220
 
219
221
  def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permissions: bool):
220
- display_banner()
222
+ token = config.get("token", None)
223
+ host = config.get("host", None)
221
224
 
222
225
  try:
223
- token = config["token"]
224
- host = config["host"]
226
+ if not token or not host:
227
+ yes = click.confirm(
228
+ FeedbackManager.warning(
229
+ message="Tinybird Code requires authentication. Do you want to authenticate now? [Y/n]"
230
+ ),
231
+ prompt_suffix="",
232
+ show_default=False,
233
+ default=True,
234
+ )
235
+ if yes:
236
+ click.echo()
237
+ login(host, auth_host="https://cloud.tinybird.co", workspace=None, interactive=False, method="browser")
238
+ click.echo()
239
+ cli_config = CLIConfig.get_project_config()
240
+ token = cli_config.get_token()
241
+ host = cli_config.get_host()
242
+
243
+ if not token or not host:
244
+ click.echo(
245
+ FeedbackManager.error(message="Tinybird Code requires authentication. Run 'tb login' first.")
246
+ )
247
+ return
248
+
249
+ display_banner()
225
250
  agent = TinybirdAgent(token, host, project, dangerously_skip_permissions)
226
251
  click.echo()
227
- if config.get("token"):
228
- click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
229
- click.echo(FeedbackManager.info(message="Run /help for more commands"))
230
- else:
231
- click.echo(FeedbackManager.info(message="Run /login to authenticate"))
252
+ click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
253
+ click.echo(FeedbackManager.info(message="Run /help for more commands"))
254
+
232
255
  click.echo()
233
256
 
234
257
  except Exception as e:
@@ -240,7 +263,7 @@ def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permiss
240
263
  while True:
241
264
  try:
242
265
  user_input = prompt(
243
- [("class:prompt", "tb » ")],
266
+ [("class:prompt", f"tb ({project.workspace_name}) » ")],
244
267
  history=load_history(),
245
268
  cursor=CursorShape.BLOCK,
246
269
  style=Style.from_dict(
@@ -1,87 +1,13 @@
1
- import difflib
2
1
  from pathlib import Path
3
2
 
4
- try:
5
- from colorama import Back, Fore, Style, init
6
-
7
- init()
8
- except ImportError: # fallback so that the imported classes always exist
9
-
10
- class ColorFallback:
11
- def __getattr__(self, name):
12
- return ""
13
-
14
- Fore = Back = Style = ColorFallback()
15
-
16
3
  import click
17
4
  from pydantic_ai import RunContext
18
5
 
19
- from tinybird.tb.modules.agent.utils import Datafile, TinybirdAgentContext, show_options
6
+ from tinybird.tb.modules.agent.utils import Datafile, TinybirdAgentContext, create_terminal_box, show_options
20
7
  from tinybird.tb.modules.exceptions import CLIBuildException
21
8
  from tinybird.tb.modules.feedback_manager import FeedbackManager
22
9
 
23
10
 
24
- def create_line_numbered_diff(original_content: str, new_content: str, filename: str) -> str:
25
- """Create a diff with line numbers similar to the example format"""
26
- original_lines = original_content.splitlines()
27
- new_lines = new_content.splitlines()
28
-
29
- # Create a SequenceMatcher to find the differences
30
- matcher = difflib.SequenceMatcher(None, original_lines, new_lines)
31
-
32
- result = []
33
- result.append(f"╭{'─' * 88}╮")
34
- result.append(f"│ {filename:<86} │")
35
- result.append(f"│{' ' * 88}│")
36
-
37
- # Process the opcodes to build the diff
38
- for tag, i1, i2, j1, j2 in matcher.get_opcodes():
39
- if tag == "equal":
40
- # Show context lines
41
- for i, line in enumerate(original_lines[i1:i2]):
42
- line_num = i1 + i + 1
43
- result.append(f"│ {line_num:4} {line:<74} │")
44
- elif tag == "replace":
45
- # Show removed lines
46
- for i, line in enumerate(original_lines[i1:i2]):
47
- line_num = i1 + i + 1
48
- result.append(f"│ {Back.RED}{line_num:4} - {line:<74}{Back.RESET} │")
49
- # Show added lines
50
- for i, line in enumerate(new_lines[j1:j2]):
51
- line_num = i1 + i + 1
52
- result.append(f"│ {Back.GREEN}{line_num:4} + {line:<74}{Back.RESET} │")
53
- elif tag == "delete":
54
- # Show removed lines
55
- for i, line in enumerate(original_lines[i1:i2]):
56
- line_num = i1 + i + 1
57
- result.append(f"│ {Back.RED}{line_num:4} - {line:<74}{Back.RESET} │")
58
- elif tag == "insert":
59
- # Show added lines
60
- for i, line in enumerate(new_lines[j1:j2]):
61
- # Use the line number from the original position
62
- line_num = i1 + i + 1
63
- result.append(f"│ {Back.GREEN}{line_num:4} + {line:<74}{Back.RESET} │")
64
-
65
- result.append(f"╰{'─' * 88}╯")
66
- return "\n".join(result)
67
-
68
-
69
- def create_line_numbered_content(content: str, filename: str) -> str:
70
- """Create a formatted display of file content with line numbers"""
71
- lines = content.splitlines()
72
-
73
- result = []
74
- result.append(f"╭{'─' * 88}╮")
75
- result.append(f"│ {filename:<86} │")
76
- result.append(f"│{' ' * 88}│")
77
-
78
- for i, line in enumerate(lines, 1):
79
- result.append(f"│ {i:4} {line:<74} │")
80
-
81
- result.append(f"╰{'─' * 88}╯")
82
- return "\n".join(result)
83
-
84
-
85
11
  def get_resource_confirmation(resource: Datafile, exists: bool) -> bool:
86
12
  """Get user confirmation for creating a resource"""
87
13
  while True:
@@ -118,9 +44,9 @@ def create_datafile(ctx: RunContext[TinybirdAgentContext], resource: Datafile) -
118
44
  content = resource.content
119
45
  exists = str(path) in ctx.deps.get_project_files()
120
46
  if exists:
121
- content = create_line_numbered_diff(path.read_text(), resource.content, resource.pathname)
47
+ content = create_terminal_box(path.read_text(), resource.content, title=resource.pathname)
122
48
  else:
123
- content = create_line_numbered_content(resource.content, resource.pathname)
49
+ content = create_terminal_box(resource.content, title=resource.pathname)
124
50
  click.echo(content)
125
51
  confirmation = ctx.deps.dangerously_skip_permissions or get_resource_confirmation(resource, exists)
126
52
 
@@ -23,6 +23,6 @@ def read_fixture_data(ctx: RunContext[TinybirdAgentContext], fixture_pathname: s
23
23
  response = ctx.deps.analyze_fixture(fixture_path=str(fixture_path))
24
24
  # limit content to first 10 rows
25
25
  data = response["preview"]["data"][:10]
26
- schema = response["analysis"]["schema"]
26
+ columns = response["analysis"]["columns"]
27
27
 
28
- return f"#Result of analysis of {fixture_pathname}:\n##Data sample:\n{json.dumps(data)}\n##Schema:\n{schema}"
28
+ return f"#Result of analysis of {fixture_pathname}:\n##Columns:\n{json.dumps(columns)}\n##Data sample:\n{json.dumps(data)}"
@@ -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
@@ -32,7 +41,7 @@ class TinybirdAgentContext(BaseModel):
32
41
  dangerously_skip_permissions: bool
33
42
 
34
43
 
35
- default_style = Style.from_dict(
44
+ default_style = PromptStyle.from_dict(
36
45
  {
37
46
  "separator": "#6C6C6C",
38
47
  "questionmark": "#FF9D00 bold",
@@ -388,3 +397,283 @@ class Datafile(BaseModel):
388
397
  description: str
389
398
  pathname: str
390
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
@@ -1,112 +1,12 @@
1
- import http.server
2
- import os
3
- import platform
4
- import random
5
- import shutil
6
- import socketserver
7
- import string
8
- import subprocess
9
- import sys
10
- import threading
11
- import time
12
- import urllib.parse
13
- import webbrowser
14
- from typing import Any, Dict, Optional
15
- from urllib.parse import urlencode
1
+ from typing import Optional
16
2
 
17
3
  import click
18
- import requests
19
4
 
20
- from tinybird.tb.config import DEFAULT_API_HOST
21
- from tinybird.tb.modules.cli import CLIConfig, cli
22
- from tinybird.tb.modules.common import ask_for_region_interactively, get_regions
23
- from tinybird.tb.modules.exceptions import CLILoginException
24
- from tinybird.tb.modules.feedback_manager import FeedbackManager
5
+ from tinybird.tb.modules.cli import cli
6
+ from tinybird.tb.modules.login_common import login
25
7
 
26
- SERVER_MAX_WAIT_TIME = 180
27
8
 
28
-
29
- class AuthHandler(http.server.SimpleHTTPRequestHandler):
30
- def do_GET(self):
31
- # The access_token is in the URL fragment, which is not sent to the server
32
- # We'll send a small HTML page that extracts the token and sends it back to the server
33
- self.send_response(200)
34
- self.send_header("Content-type", "text/html")
35
- self.end_headers()
36
- self.wfile.write(
37
- """
38
- <html>
39
- <head>
40
- <style>
41
- body {{
42
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
43
- background: #f5f5f5;
44
- display: flex;
45
- align-items: center;
46
- justify-content: center;
47
- height: 100vh;
48
- margin: 0;
49
- }}
50
- </style>
51
- </head>
52
- <body>
53
- <script>
54
- const searchParams = new URLSearchParams(window.location.search);
55
- const code = searchParams.get('code');
56
- const workspace = searchParams.get('workspace');
57
- const region = searchParams.get('region');
58
- const provider = searchParams.get('provider');
59
- const host = "{auth_host}";
60
- fetch('/?code=' + code, {{method: 'POST'}})
61
- .then(() => {{
62
- window.location.href = host + "/" + provider + "/" + region + "/cli-login?workspace=" + workspace;
63
- }});
64
- </script>
65
- </body>
66
- </html>
67
- """.format(auth_host=self.server.auth_host).encode() # type: ignore
68
- )
69
-
70
- def do_POST(self):
71
- parsed_path = urllib.parse.urlparse(self.path)
72
- query_params = urllib.parse.parse_qs(parsed_path.query)
73
-
74
- if "code" in query_params:
75
- code = query_params["code"][0]
76
- self.server.auth_callback(code) # type: ignore
77
- self.send_response(200)
78
- self.end_headers()
79
- else:
80
- self.send_error(400, "Missing 'code' parameter")
81
-
82
- self.server.shutdown()
83
-
84
- def log_message(self, format, *args):
85
- # Suppress log messages
86
- return
87
-
88
-
89
- AUTH_SERVER_PORT = 49160
90
-
91
-
92
- class AuthServer(socketserver.TCPServer):
93
- allow_reuse_address = True
94
-
95
- def __init__(self, server_address, RequestHandlerClass, auth_callback, auth_host):
96
- self.auth_callback = auth_callback
97
- self.auth_host = auth_host
98
- super().__init__(server_address, RequestHandlerClass)
99
-
100
-
101
- def start_server(auth_callback, auth_host):
102
- with AuthServer(("", AUTH_SERVER_PORT), AuthHandler, auth_callback, auth_host) as httpd:
103
- httpd.timeout = 30
104
- start_time = time.time()
105
- while time.time() - start_time < SERVER_MAX_WAIT_TIME: # Run for a maximum of 180 seconds
106
- httpd.handle_request()
107
-
108
-
109
- @cli.command()
9
+ @cli.command("login", help="Authenticate using the browser.")
110
10
  @click.option(
111
11
  "--host",
112
12
  type=str,
@@ -135,200 +35,5 @@ def start_server(auth_callback, auth_host):
135
35
  default="browser",
136
36
  help="Set the authentication method to use. Default: browser.",
137
37
  )
138
- def login(host: Optional[str], auth_host: str, workspace: str, interactive: bool, method: str):
139
- """Authenticate using the browser."""
140
- try:
141
- cli_config = CLIConfig.get_project_config()
142
- if not host and cli_config.get_token():
143
- host = cli_config.get_host(use_defaults_if_needed=False)
144
- if not host or interactive:
145
- if interactive:
146
- click.echo(FeedbackManager.highlight(message="» Select one region from the list below:"))
147
- else:
148
- click.echo(FeedbackManager.highlight(message="» No region detected, select one from the list below:"))
149
-
150
- regions = get_regions(cli_config)
151
- selected_region = ask_for_region_interactively(regions)
152
-
153
- # If the user cancels the selection, we'll exit
154
- if not selected_region:
155
- sys.exit(1)
156
- host = selected_region.get("api_host")
157
-
158
- if not host:
159
- host = DEFAULT_API_HOST
160
-
161
- host = host.rstrip("/")
162
- auth_host = auth_host.rstrip("/")
163
-
164
- if method == "code":
165
- display_code, one_time_code = create_one_time_code()
166
- click.echo(FeedbackManager.info(message=f"First, copy your one-time code: {display_code}"))
167
- click.echo(FeedbackManager.info(message="Press [Enter] to continue in the browser..."))
168
- input()
169
- click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
170
- params = {
171
- "apiHost": host,
172
- "code": one_time_code,
173
- "method": "code",
174
- }
175
- auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
176
- open_url(auth_url)
177
- click.echo(
178
- FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:")
179
- )
180
- click.echo(FeedbackManager.info(message=auth_url))
181
-
182
- def poll_for_tokens():
183
- while True:
184
- params = {
185
- "apiHost": host,
186
- "cliCode": one_time_code,
187
- "method": "code",
188
- }
189
- response = requests.get(f"{auth_host}/api/cli-login?{urlencode(params)}")
190
-
191
- try:
192
- if response.status_code == 200:
193
- data = response.json()
194
- user_token = data.get("user_token", "")
195
- workspace_token = data.get("workspace_token", "")
196
- if user_token and workspace_token:
197
- authenticate_with_tokens(data, host, cli_config)
198
- break
199
- except Exception:
200
- pass
201
-
202
- time.sleep(2)
203
-
204
- poll_for_tokens()
205
- return
206
-
207
- auth_event = threading.Event()
208
- auth_code: list[str] = [] # Using a list to store the code, as it's mutable
209
-
210
- def auth_callback(code):
211
- auth_code.append(code)
212
- auth_event.set()
213
-
214
- click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
215
- # Start the local server in a separate thread
216
- server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
217
- server_thread.daemon = True
218
- server_thread.start()
219
-
220
- # Open the browser to the auth page
221
- params = {
222
- "apiHost": host,
223
- }
224
-
225
- if workspace:
226
- params["workspace"] = workspace
227
-
228
- auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
229
- open_url(auth_url)
230
-
231
- click.echo(FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:"))
232
- click.echo(FeedbackManager.info(message=auth_url))
233
-
234
- # Wait for the authentication to complete or timeout
235
- if auth_event.wait(timeout=SERVER_MAX_WAIT_TIME): # Wait for up to 180 seconds
236
- params = {}
237
- params["code"] = auth_code[0]
238
- response = requests.get(
239
- f"{auth_host}/api/cli-login?{urlencode(params)}",
240
- )
241
-
242
- data = response.json()
243
- authenticate_with_tokens(data, host, cli_config)
244
- else:
245
- raise Exception("Authentication failed or timed out.")
246
- except Exception as e:
247
- raise CLILoginException(FeedbackManager.error(message=str(e)))
248
-
249
-
250
- def _running_in_wsl() -> bool:
251
- """Return True when Python is executing inside a WSL distro."""
252
- # Fast positive check (modern WSL always sets at least one of these):
253
- if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
254
- return True
255
-
256
- # Fall back to kernel /proc data
257
- release = platform.uname().release.lower()
258
- if "microsoft" in release: # covers stock WSL kernels
259
- return True
260
- try:
261
- if "microsoft" in open("/proc/version").read().lower():
262
- return True
263
- except FileNotFoundError:
264
- pass
265
- return False
266
-
267
-
268
- def open_url(url: str, *, new_tab: bool = False) -> bool:
269
- # 1. Try the standard library first on CPython ≥ 3.11 this already
270
- # recognises WSL and fires up the Windows default browser for us.
271
- try:
272
- wb: Any = webbrowser.get() # mypy: Any for Py < 3.10
273
- if new_tab:
274
- if wb.open_new_tab(url):
275
- return True
276
- else:
277
- if wb.open(url):
278
- return True
279
- except webbrowser.Error:
280
- pass # keep going
281
-
282
- # 2. Inside WSL, prefer `wslview` if the user has it (wslu package).
283
- if _running_in_wsl() and shutil.which("wslview"):
284
- subprocess.Popen(["wslview", url])
285
- return True
286
-
287
- # 3. Secondary WSL fallback use Windows **start** through cmd.exe.
288
- # Empty "" argument is required so long URLs are not treated as a window title.
289
- if _running_in_wsl():
290
- subprocess.Popen(["cmd.exe", "/c", "start", "", url])
291
- return True
292
-
293
- # 4. Unix last-ditch fallback xdg-open (most minimal container images have it)
294
- if shutil.which("xdg-open"):
295
- subprocess.Popen(["xdg-open", url])
296
- return True
297
-
298
- # 5. If everything failed, let the caller know.
299
- return False
300
-
301
-
302
- def create_one_time_code():
303
- """Create a random one-time code for the authentication process in the format of A2C4-D2G4 (only uppercase letters and digits)"""
304
- seperator = "-"
305
- full_code = "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
306
- parts = [full_code[:4], full_code[4:]]
307
- return seperator.join(parts), full_code
308
-
309
-
310
- def authenticate_with_tokens(data: Dict[str, Any], host: Optional[str], cli_config: CLIConfig):
311
- cli_config.set_token(data.get("workspace_token", ""))
312
- host = host or data.get("api_host", "")
313
- cli_config.set_token_for_host(data.get("workspace_token", ""), host)
314
- cli_config.set_user_token(data.get("user_token", ""))
315
- cli_config.set_host(host)
316
- ws = cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(version="v1")
317
- for k in ("id", "name", "user_email", "user_id", "scope"):
318
- if k in ws:
319
- cli_config[k] = ws[k]
320
-
321
- path = os.path.join(os.getcwd(), ".tinyb")
322
- cli_config.persist_to_file(override_with_path=path)
323
-
324
- auth_info: Dict[str, Any] = cli_config.get_user_client().check_auth_login()
325
- if not auth_info.get("is_valid", False):
326
- raise Exception(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
327
-
328
- if not auth_info.get("is_user", False):
329
- raise Exception(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
330
-
331
- click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
332
- click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
333
- click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
334
- click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
38
+ def login_cmd(host: Optional[str], auth_host: str, workspace: str, interactive: bool, method: str):
39
+ login(host, auth_host, workspace, interactive, method)
@@ -0,0 +1,310 @@
1
+ import http.server
2
+ import os
3
+ import platform
4
+ import random
5
+ import shutil
6
+ import socketserver
7
+ import string
8
+ import subprocess
9
+ import sys
10
+ import threading
11
+ import time
12
+ import urllib.parse
13
+ import webbrowser
14
+ from typing import Any, Dict, Optional
15
+ from urllib.parse import urlencode
16
+
17
+ import click
18
+ import requests
19
+
20
+ from tinybird.tb.config import DEFAULT_API_HOST
21
+ from tinybird.tb.modules.common import ask_for_region_interactively, get_regions
22
+ from tinybird.tb.modules.config import CLIConfig
23
+ from tinybird.tb.modules.exceptions import CLILoginException
24
+ from tinybird.tb.modules.feedback_manager import FeedbackManager
25
+
26
+ SERVER_MAX_WAIT_TIME = 180
27
+
28
+
29
+ class AuthHandler(http.server.SimpleHTTPRequestHandler):
30
+ def do_GET(self):
31
+ # The access_token is in the URL fragment, which is not sent to the server
32
+ # We'll send a small HTML page that extracts the token and sends it back to the server
33
+ self.send_response(200)
34
+ self.send_header("Content-type", "text/html")
35
+ self.end_headers()
36
+ self.wfile.write(
37
+ """
38
+ <html>
39
+ <head>
40
+ <style>
41
+ body {{
42
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
43
+ background: #f5f5f5;
44
+ display: flex;
45
+ align-items: center;
46
+ justify-content: center;
47
+ height: 100vh;
48
+ margin: 0;
49
+ }}
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <script>
54
+ const searchParams = new URLSearchParams(window.location.search);
55
+ const code = searchParams.get('code');
56
+ const workspace = searchParams.get('workspace');
57
+ const region = searchParams.get('region');
58
+ const provider = searchParams.get('provider');
59
+ const host = "{auth_host}";
60
+ fetch('/?code=' + code, {{method: 'POST'}})
61
+ .then(() => {{
62
+ window.location.href = host + "/" + provider + "/" + region + "/cli-login?workspace=" + workspace;
63
+ }});
64
+ </script>
65
+ </body>
66
+ </html>
67
+ """.format(auth_host=self.server.auth_host).encode() # type: ignore
68
+ )
69
+
70
+ def do_POST(self):
71
+ parsed_path = urllib.parse.urlparse(self.path)
72
+ query_params = urllib.parse.parse_qs(parsed_path.query)
73
+
74
+ if "code" in query_params:
75
+ code = query_params["code"][0]
76
+ self.server.auth_callback(code) # type: ignore
77
+ self.send_response(200)
78
+ self.end_headers()
79
+ else:
80
+ self.send_error(400, "Missing 'code' parameter")
81
+
82
+ self.server.shutdown()
83
+
84
+ def log_message(self, format, *args):
85
+ # Suppress log messages
86
+ return
87
+
88
+
89
+ AUTH_SERVER_PORT = 49160
90
+
91
+
92
+ class AuthServer(socketserver.TCPServer):
93
+ allow_reuse_address = True
94
+
95
+ def __init__(self, server_address, RequestHandlerClass, auth_callback, auth_host):
96
+ self.auth_callback = auth_callback
97
+ self.auth_host = auth_host
98
+ super().__init__(server_address, RequestHandlerClass)
99
+
100
+
101
+ def start_server(auth_callback, auth_host):
102
+ with AuthServer(("", AUTH_SERVER_PORT), AuthHandler, auth_callback, auth_host) as httpd:
103
+ httpd.timeout = 30
104
+ start_time = time.time()
105
+ while time.time() - start_time < SERVER_MAX_WAIT_TIME: # Run for a maximum of 180 seconds
106
+ httpd.handle_request()
107
+
108
+
109
+ def login(
110
+ host: Optional[str],
111
+ auth_host: str = "https://cloud.tinybird.co",
112
+ workspace: Optional[str] = None,
113
+ interactive: bool = False,
114
+ method: str = "browser",
115
+ ):
116
+ try:
117
+ cli_config = CLIConfig.get_project_config()
118
+ if not host and cli_config.get_token():
119
+ host = cli_config.get_host(use_defaults_if_needed=False)
120
+ if not host or interactive:
121
+ if interactive:
122
+ click.echo(FeedbackManager.highlight(message="» Select one region from the list below:"))
123
+ else:
124
+ click.echo(FeedbackManager.highlight(message="» No region detected, select one from the list below:"))
125
+
126
+ regions = get_regions(cli_config)
127
+ selected_region = ask_for_region_interactively(regions)
128
+
129
+ # If the user cancels the selection, we'll exit
130
+ if not selected_region:
131
+ sys.exit(1)
132
+ host = selected_region.get("api_host")
133
+
134
+ if not host:
135
+ host = DEFAULT_API_HOST
136
+
137
+ host = host.rstrip("/")
138
+ auth_host = auth_host.rstrip("/")
139
+
140
+ if method == "code":
141
+ display_code, one_time_code = create_one_time_code()
142
+ click.echo(FeedbackManager.info(message=f"First, copy your one-time code: {display_code}"))
143
+ click.echo(FeedbackManager.info(message="Press [Enter] to continue in the browser..."))
144
+ input()
145
+ click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
146
+ params = {
147
+ "apiHost": host,
148
+ "code": one_time_code,
149
+ "method": "code",
150
+ }
151
+ auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
152
+ open_url(auth_url)
153
+ click.echo(
154
+ FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:")
155
+ )
156
+ click.echo(FeedbackManager.info(message=auth_url))
157
+
158
+ def poll_for_tokens():
159
+ while True:
160
+ params = {
161
+ "apiHost": host,
162
+ "cliCode": one_time_code,
163
+ "method": "code",
164
+ }
165
+ response = requests.get(f"{auth_host}/api/cli-login?{urlencode(params)}")
166
+
167
+ try:
168
+ if response.status_code == 200:
169
+ data = response.json()
170
+ user_token = data.get("user_token", "")
171
+ workspace_token = data.get("workspace_token", "")
172
+ if user_token and workspace_token:
173
+ authenticate_with_tokens(data, host, cli_config)
174
+ break
175
+ except Exception:
176
+ pass
177
+
178
+ time.sleep(2)
179
+
180
+ poll_for_tokens()
181
+ return
182
+
183
+ auth_event = threading.Event()
184
+ auth_code: list[str] = [] # Using a list to store the code, as it's mutable
185
+
186
+ def auth_callback(code):
187
+ auth_code.append(code)
188
+ auth_event.set()
189
+
190
+ click.echo(FeedbackManager.highlight(message="» Opening browser for authentication..."))
191
+ # Start the local server in a separate thread
192
+ server_thread = threading.Thread(target=start_server, args=(auth_callback, auth_host))
193
+ server_thread.daemon = True
194
+ server_thread.start()
195
+
196
+ # Open the browser to the auth page
197
+ params = {
198
+ "apiHost": host,
199
+ }
200
+
201
+ if workspace:
202
+ params["workspace"] = workspace
203
+
204
+ auth_url = f"{auth_host}/api/cli-login?{urlencode(params)}"
205
+ open_url(auth_url)
206
+
207
+ click.echo(FeedbackManager.info(message="\nIf browser does not open, please open the following URL manually:"))
208
+ click.echo(FeedbackManager.info(message=auth_url))
209
+
210
+ # Wait for the authentication to complete or timeout
211
+ if auth_event.wait(timeout=SERVER_MAX_WAIT_TIME): # Wait for up to 180 seconds
212
+ params = {}
213
+ params["code"] = auth_code[0]
214
+ response = requests.get(
215
+ f"{auth_host}/api/cli-login?{urlencode(params)}",
216
+ )
217
+
218
+ data = response.json()
219
+ authenticate_with_tokens(data, host, cli_config)
220
+ else:
221
+ raise Exception("Authentication failed or timed out.")
222
+ except Exception as e:
223
+ raise CLILoginException(FeedbackManager.error(message=str(e)))
224
+
225
+
226
+ def _running_in_wsl() -> bool:
227
+ """Return True when Python is executing inside a WSL distro."""
228
+ # Fast positive check (modern WSL always sets at least one of these):
229
+ if "WSL_DISTRO_NAME" in os.environ or "WSL_INTEROP" in os.environ:
230
+ return True
231
+
232
+ # Fall back to kernel /proc data
233
+ release = platform.uname().release.lower()
234
+ if "microsoft" in release: # covers stock WSL kernels
235
+ return True
236
+ try:
237
+ if "microsoft" in open("/proc/version").read().lower():
238
+ return True
239
+ except FileNotFoundError:
240
+ pass
241
+ return False
242
+
243
+
244
+ def open_url(url: str, *, new_tab: bool = False) -> bool:
245
+ # 1. Try the standard library first on CPython ≥ 3.11 this already
246
+ # recognises WSL and fires up the Windows default browser for us.
247
+ try:
248
+ wb: Any = webbrowser.get() # mypy: Any for Py < 3.10
249
+ if new_tab:
250
+ if wb.open_new_tab(url):
251
+ return True
252
+ else:
253
+ if wb.open(url):
254
+ return True
255
+ except webbrowser.Error:
256
+ pass # keep going
257
+
258
+ # 2. Inside WSL, prefer `wslview` if the user has it (wslu package).
259
+ if _running_in_wsl() and shutil.which("wslview"):
260
+ subprocess.Popen(["wslview", url])
261
+ return True
262
+
263
+ # 3. Secondary WSL fallback use Windows **start** through cmd.exe.
264
+ # Empty "" argument is required so long URLs are not treated as a window title.
265
+ if _running_in_wsl():
266
+ subprocess.Popen(["cmd.exe", "/c", "start", "", url])
267
+ return True
268
+
269
+ # 4. Unix last-ditch fallback xdg-open (most minimal container images have it)
270
+ if shutil.which("xdg-open"):
271
+ subprocess.Popen(["xdg-open", url])
272
+ return True
273
+
274
+ # 5. If everything failed, let the caller know.
275
+ return False
276
+
277
+
278
+ def create_one_time_code():
279
+ """Create a random one-time code for the authentication process in the format of A2C4-D2G4 (only uppercase letters and digits)"""
280
+ seperator = "-"
281
+ full_code = "".join(random.choices(string.ascii_uppercase + string.digits, k=8))
282
+ parts = [full_code[:4], full_code[4:]]
283
+ return seperator.join(parts), full_code
284
+
285
+
286
+ def authenticate_with_tokens(data: Dict[str, Any], host: Optional[str], cli_config: CLIConfig):
287
+ cli_config.set_token(data.get("workspace_token", ""))
288
+ host = host or data.get("api_host", "")
289
+ cli_config.set_token_for_host(data.get("workspace_token", ""), host)
290
+ cli_config.set_user_token(data.get("user_token", ""))
291
+ cli_config.set_host(host)
292
+ ws = cli_config.get_client(token=data.get("workspace_token", ""), host=host).workspace_info(version="v1")
293
+ for k in ("id", "name", "user_email", "user_id", "scope"):
294
+ if k in ws:
295
+ cli_config[k] = ws[k]
296
+
297
+ path = os.path.join(os.getcwd(), ".tinyb")
298
+ cli_config.persist_to_file(override_with_path=path)
299
+
300
+ auth_info: Dict[str, Any] = cli_config.get_user_client().check_auth_login()
301
+ if not auth_info.get("is_valid", False):
302
+ raise Exception(FeedbackManager.error_auth_login_not_valid(host=cli_config.get_host()))
303
+
304
+ if not auth_info.get("is_user", False):
305
+ raise Exception(FeedbackManager.error_auth_login_not_user(host=cli_config.get_host()))
306
+
307
+ click.echo(FeedbackManager.gray(message="\nWorkspace: ") + FeedbackManager.info(message=ws["name"]))
308
+ click.echo(FeedbackManager.gray(message="User: ") + FeedbackManager.info(message=ws["user_email"]))
309
+ click.echo(FeedbackManager.gray(message="Host: ") + FeedbackManager.info(message=host))
310
+ click.echo(FeedbackManager.success(message="\n✓ Authentication successful!"))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: tinybird
3
- Version: 0.0.1.dev247
3
+ Version: 0.0.1.dev248
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/forward/commands
6
6
  Author: Tinybird
@@ -3,7 +3,7 @@ tinybird/context.py,sha256=FfqYfrGX_I7PKGTQo93utaKPDNVYWelg4Hsp3evX5wM,1291
3
3
  tinybird/datatypes.py,sha256=r4WCvspmrXTJHiPjjyOTiZyZl31FO3Ynkwq4LQsYm6E,11059
4
4
  tinybird/feedback_manager.py,sha256=1INQFfRfuMCb9lfB8KNf4r6qC2khW568hoHjtk-wshI,69305
5
5
  tinybird/git_settings.py,sha256=Sw_8rGmribEFJ4Z_6idrVytxpFYk7ez8ei0qHULzs3E,3934
6
- tinybird/prompts.py,sha256=dg8Q8Q29X69j-PPP8KGooMz7-M3FNXEYqC0Fj0LcUbw,45316
6
+ tinybird/prompts.py,sha256=Dx9pD5kyXdbzkGOcWdULWUg_aN5F1CUPAISm5PyMzOM,45498
7
7
  tinybird/sql.py,sha256=BufnOgclQokDyihtuXesOwHBsebN6wRXIxO5wKRkOwE,48299
8
8
  tinybird/sql_template.py,sha256=LChDztXUUrNO4Qukv2RMsdjQ-vhmepWiHVoX6yr140E,99983
9
9
  tinybird/sql_template_fmt.py,sha256=KUHdj5rYCYm_rKKdXYSJAE9vIyXUQLB0YSZnUXHeBlY,10196
@@ -17,7 +17,7 @@ tinybird/datafile/exceptions.py,sha256=8rw2umdZjtby85QbuRKFO5ETz_eRHwUY5l7eHsy1w
17
17
  tinybird/datafile/parse_connection.py,sha256=tRyn2Rpr1TeWet5BXmMoQgaotbGdYep1qiTak_OqC5E,1825
18
18
  tinybird/datafile/parse_datasource.py,sha256=ssW8QeFSgglVFi3sDZj_HgkJiTJ2069v2JgqnH3CkDE,1825
19
19
  tinybird/datafile/parse_pipe.py,sha256=xf4m0Tw44QWJzHzAm7Z7FwUoUUtr7noMYjU1NiWnX0k,3880
20
- tinybird/tb/__cli__.py,sha256=vsGGI9kP2i8TgwhV7ZNt0lpfebdoTNrbxYl00oYXMjE,247
20
+ tinybird/tb/__cli__.py,sha256=XU7dQJoJUS04mQcPTcpDweJQbUllPEGhxSfkfVitr2Y,247
21
21
  tinybird/tb/check_pypi.py,sha256=Gp0HkHHDFMSDL6nxKlOY51z7z1Uv-2LRexNTZSHHGmM,552
22
22
  tinybird/tb/cli.py,sha256=FdDFEIayjmsZEVsVSSvRiVYn_FHOVg_zWQzchnzfWho,1008
23
23
  tinybird/tb/client.py,sha256=pJbdkWMXGAqKseNAvdsRRnl_c7I-DCMB0dWCQnG82nU,54146
@@ -46,7 +46,8 @@ tinybird/tb/modules/llm.py,sha256=CpTq2YAk88E8ENpQA94-mas3UDN1aqa--9Al8GdwQtc,15
46
46
  tinybird/tb/modules/llm_utils.py,sha256=nS9r4FAElJw8yXtmdYrx-rtI2zXR8qXfi1QqUDCfxvg,3469
47
47
  tinybird/tb/modules/local.py,sha256=tpiw_F_qOIp42h3kTBwTm5GQDyuVLF0QNF1jmB0zR94,6845
48
48
  tinybird/tb/modules/local_common.py,sha256=_WODjW3oPshgsZ1jDFFx2nr0zrLi3Gxz5dlahWPobM8,17464
49
- tinybird/tb/modules/login.py,sha256=glqj5RWH26AseEoBl8XfrSDEjQTdko17i_pVWOIMoGc,12497
49
+ tinybird/tb/modules/login.py,sha256=zerXZqIv15pbFk5XRt746xGcVnp01YmL_403byBf4jQ,1245
50
+ tinybird/tb/modules/login_common.py,sha256=IfthYbHmC7EtsCXCB1iF4TngPOwfaHJ6Dfi_t7oBXnI,11640
50
51
  tinybird/tb/modules/logout.py,sha256=sniI4JNxpTrVeRCp0oGJuQ3yRerG4hH5uz6oBmjv724,1009
51
52
  tinybird/tb/modules/materialization.py,sha256=0O2JUCxLzz-DrXTUewVHlIyC6-Kyymw0hGXXDicMSHE,5403
52
53
  tinybird/tb/modules/mock.py,sha256=ET8sRpmXnQsd2sSJXH_KCdREU1_XQgkORru6T357Akc,3260
@@ -67,24 +68,24 @@ tinybird/tb/modules/watch.py,sha256=No0bK1M1_3CYuMaIgylxf7vYFJ72lTJe3brz6xQ-mJo,
67
68
  tinybird/tb/modules/workspace.py,sha256=Q_8HcxMsNg8QG9aBlwcWS2umrDP5IkTIHqqz3sfmGuc,11341
68
69
  tinybird/tb/modules/workspace_members.py,sha256=5JdkJgfuEwbq-t6vxkBhYwgsiTDxF790wsa6Xfif9nk,8608
69
70
  tinybird/tb/modules/agent/__init__.py,sha256=i3oe3vDIWWPaicdCM0zs7D7BJ1W0k7th93ooskHAV00,54
70
- tinybird/tb/modules/agent/agent.py,sha256=3mP1HwjEGWuyuUByyA7xe2FrW2l-ZiChRKDxMs7vJIg,16887
71
+ tinybird/tb/modules/agent/agent.py,sha256=8NBSwQuu3AagieLfbNOXSWeud3WGyThBuyPuzZTtxJQ,17932
71
72
  tinybird/tb/modules/agent/animations.py,sha256=z0MNLf8TnUO8qAjgYvth_wc9a9283pNVz1Z4jl15Ggs,2558
72
73
  tinybird/tb/modules/agent/banner.py,sha256=KX_e467uiy1gWOZ4ofTZt0GCFGQqHQ_8Ob27XLQqda0,3053
73
74
  tinybird/tb/modules/agent/memory.py,sha256=H6SJK--2L5C87B7AJd_jMqsq3sCvFvZwZXmajuT0GBE,1171
74
75
  tinybird/tb/modules/agent/models.py,sha256=Of74wcU8oX05ricTqmhMHVHfeYo_pQbnbCI_q3mlx5E,682
75
76
  tinybird/tb/modules/agent/prompts.py,sha256=fZMTbTbq8SHWob8-wA5fQFnZ9lJa7Y_66_9JvJT3xuc,6818
76
- tinybird/tb/modules/agent/utils.py,sha256=K0mszvO27wqsmxjWZOjrshUIVb3qxpYr1hM_enlbrxw,13441
77
+ tinybird/tb/modules/agent/utils.py,sha256=7Y8bq_rZlqre8_OvLVjIvE8ZLOBpuKmXamyaNs02zzc,25231
77
78
  tinybird/tb/modules/agent/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
79
  tinybird/tb/modules/agent/tools/append.py,sha256=2q5y32jeNHJgbMsVmABn7y-KCoKwkqbMFJAm8OZ4zQc,2161
79
80
  tinybird/tb/modules/agent/tools/build.py,sha256=LhzJMx6tbxC7gogIrxhfKJc-SDgoSR-FC6IunfaCdn8,758
80
- tinybird/tb/modules/agent/tools/create_datafile.py,sha256=IeLB81HNQ5k8fy1x3ZDkKbi0iPqSMZz-xNi2Kga0I1M,5431
81
+ tinybird/tb/modules/agent/tools/create_datafile.py,sha256=ZVLj5VT8udfYDqfbYu9U3f2IG2wSB3POwC-zB_JvfsA,2692
81
82
  tinybird/tb/modules/agent/tools/deploy.py,sha256=Vv1SHalxZsl5QttaON0jBwJenj1cVOQiQ-cMiK2ULZg,1443
82
83
  tinybird/tb/modules/agent/tools/deploy_check.py,sha256=VqMYC7l3_cihmmM_pi8w1t8rJ3P0xDc7pHs_st9k-9Q,684
83
84
  tinybird/tb/modules/agent/tools/explore.py,sha256=ihALc_kBcsjrKT3hZyicqyIowB0g_K3AtNNi-5uz9-8,412
84
85
  tinybird/tb/modules/agent/tools/mock.py,sha256=c4fY8_D92tOUBr0DoqoA5lEE3FgvUQHP6JE75mfTBko,2491
85
86
  tinybird/tb/modules/agent/tools/plan.py,sha256=wQY4gNtFTOEy2yZUGf8VqefPUbbz5DgMZdrzGRk-wiE,1365
86
87
  tinybird/tb/modules/agent/tools/preview_datafile.py,sha256=e9q5fR0afApcrntzFrnuHmd10ex7MG_GM6T0Pwc9bRI,850
87
- tinybird/tb/modules/agent/tools/read_fixture_data.py,sha256=QCmdXccVVRqkDYmUdPst94qr29dmlE20KSMyBHpzDjs,961
88
+ tinybird/tb/modules/agent/tools/read_fixture_data.py,sha256=XgeDld6YTOjWNcJ7cr8bHD2phG6W-h5UuC2amGSBnQw,977
88
89
  tinybird/tb/modules/datafile/build.py,sha256=NFKBrusFLU0WJNCXePAFWiEDuTaXpwc0lHlOQWEJ43s,51117
89
90
  tinybird/tb/modules/datafile/build_common.py,sha256=2yNdxe49IMA9wNvl25NemY2Iaz8L66snjOdT64dm1is,4511
90
91
  tinybird/tb/modules/datafile/build_datasource.py,sha256=Ra8pVQBDafbFRUKlhpgohhTsRyp_ADKZJVG8Gd69idY,17227
@@ -105,8 +106,8 @@ tinybird/tb_cli_modules/config.py,sha256=IsgdtFRnUrkY8-Zo32lmk6O7u3bHie1QCxLwgp4
105
106
  tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
106
107
  tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
107
108
  tinybird/tb_cli_modules/telemetry.py,sha256=Hh2Io8ZPROSunbOLuMvuIFU4TqwWPmQTqal4WS09K1A,10449
108
- tinybird-0.0.1.dev247.dist-info/METADATA,sha256=IBLQijubQLYO67wrCCvUpdLPbEqv1JBnf7RCCEOiKG8,1733
109
- tinybird-0.0.1.dev247.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
110
- tinybird-0.0.1.dev247.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
111
- tinybird-0.0.1.dev247.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
112
- tinybird-0.0.1.dev247.dist-info/RECORD,,
109
+ tinybird-0.0.1.dev248.dist-info/METADATA,sha256=Q2bYzjGQkmxcw_IutVCHU3qK6Au414Yse04ys2JugeA,1733
110
+ tinybird-0.0.1.dev248.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
111
+ tinybird-0.0.1.dev248.dist-info/entry_points.txt,sha256=LwdHU6TfKx4Qs7BqqtaczEZbImgU7Abe9Lp920zb_fo,43
112
+ tinybird-0.0.1.dev248.dist-info/top_level.txt,sha256=VqqqEmkAy7UNaD8-V51FCoMMWXjLUlR0IstvK7tJYVY,54
113
+ tinybird-0.0.1.dev248.dist-info/RECORD,,