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 +2 -0
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +33 -10
- tinybird/tb/modules/agent/tools/create_datafile.py +3 -77
- tinybird/tb/modules/agent/tools/read_fixture_data.py +2 -2
- tinybird/tb/modules/agent/utils.py +292 -3
- tinybird/tb/modules/login.py +6 -301
- tinybird/tb/modules/login_common.py +310 -0
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev248.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev248.dist-info}/RECORD +13 -12
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev248.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev248.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev248.dist-info}/top_level.txt +0 -0
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.
|
|
8
|
-
__revision__ = '
|
|
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
|
-
|
|
222
|
+
token = config.get("token", None)
|
|
223
|
+
host = config.get("host", None)
|
|
221
224
|
|
|
222
225
|
try:
|
|
223
|
-
token
|
|
224
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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 =
|
|
47
|
+
content = create_terminal_box(path.read_text(), resource.content, title=resource.pathname)
|
|
122
48
|
else:
|
|
123
|
-
content =
|
|
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
|
-
|
|
26
|
+
columns = response["analysis"]["columns"]
|
|
27
27
|
|
|
28
|
-
return f"#Result of analysis of {fixture_pathname}:\n##
|
|
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 =
|
|
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
|
tinybird/tb/modules/login.py
CHANGED
|
@@ -1,112 +1,12 @@
|
|
|
1
|
-
import
|
|
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.
|
|
21
|
-
from tinybird.tb.modules.
|
|
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
|
|
139
|
-
|
|
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!"))
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
109
|
-
tinybird-0.0.1.
|
|
110
|
-
tinybird-0.0.1.
|
|
111
|
-
tinybird-0.0.1.
|
|
112
|
-
tinybird-0.0.1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|