tinybird 0.0.1.dev247__py3-none-any.whl → 0.0.1.dev249__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/ch_utils/engine.py +3 -2
- tinybird/prompts.py +2 -0
- tinybird/tb/__cli__.py +2 -2
- tinybird/tb/modules/agent/agent.py +53 -15
- tinybird/tb/modules/agent/models.py +2 -1
- 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/deployment.py +22 -1
- 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.dev249.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev249.dist-info}/RECORD +16 -15
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev249.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev249.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev247.dist-info → tinybird-0.0.1.dev249.dist-info}/top_level.txt +0 -0
tinybird/ch_utils/engine.py
CHANGED
|
@@ -134,8 +134,9 @@ class TableDetails:
|
|
|
134
134
|
_version = self.details.get("version", None)
|
|
135
135
|
return _version
|
|
136
136
|
|
|
137
|
-
def is_replicated(self):
|
|
138
|
-
|
|
137
|
+
def is_replicated(self) -> bool:
|
|
138
|
+
engine: Optional[str] = self.details.get("engine", None)
|
|
139
|
+
return engine is not None and "Replicated" in engine
|
|
139
140
|
|
|
140
141
|
def is_mergetree_family(self) -> bool:
|
|
141
142
|
return self.engine is not None and "mergetree" in self.engine.lower()
|
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.dev249'
|
|
8
|
+
__revision__ = 'ef4e98c'
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import shlex
|
|
1
2
|
import subprocess
|
|
2
3
|
import sys
|
|
3
4
|
from datetime import datetime
|
|
@@ -54,19 +55,29 @@ from tinybird.tb.modules.deployment_common import create_deployment
|
|
|
54
55
|
from tinybird.tb.modules.exceptions import CLIBuildException, CLIMockException
|
|
55
56
|
from tinybird.tb.modules.feedback_manager import FeedbackManager
|
|
56
57
|
from tinybird.tb.modules.local_common import get_tinybird_local_client
|
|
58
|
+
from tinybird.tb.modules.login_common import login
|
|
57
59
|
from tinybird.tb.modules.mock_common import append_mock_data, create_mock_data
|
|
58
60
|
from tinybird.tb.modules.project import Project
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
class TinybirdAgent:
|
|
62
|
-
def __init__(
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
token: str,
|
|
67
|
+
user_token: str,
|
|
68
|
+
host: str,
|
|
69
|
+
workspace_id: str,
|
|
70
|
+
project: Project,
|
|
71
|
+
dangerously_skip_permissions: bool,
|
|
72
|
+
):
|
|
63
73
|
self.token = token
|
|
74
|
+
self.user_token = user_token
|
|
64
75
|
self.host = host
|
|
65
76
|
self.dangerously_skip_permissions = dangerously_skip_permissions
|
|
66
77
|
self.project = project
|
|
67
78
|
self.messages: list[ModelMessage] = []
|
|
68
79
|
self.agent = Agent(
|
|
69
|
-
model=create_model(
|
|
80
|
+
model=create_model(user_token, host, workspace_id),
|
|
70
81
|
deps_type=TinybirdAgentContext,
|
|
71
82
|
system_prompt=f"""
|
|
72
83
|
You are a Tinybird Code, an agentic CLI that can help users to work with Tinybird.
|
|
@@ -75,7 +86,7 @@ You are an interactive CLI tool that helps users with data engineering tasks. Us
|
|
|
75
86
|
|
|
76
87
|
# Tone and style
|
|
77
88
|
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.
|
|
89
|
+
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
90
|
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
91
|
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
92
|
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 +127,7 @@ You have access to the following tools:
|
|
|
116
127
|
8. If the datafile was created successfully, but the built failed, try to fix the error and repeat the process.
|
|
117
128
|
|
|
118
129
|
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.
|
|
130
|
+
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
131
|
|
|
120
132
|
# When planning the creation or update of resources:
|
|
121
133
|
{plan_instructions}
|
|
@@ -217,18 +229,41 @@ Today is {datetime.now().strftime("%Y-%m-%d")}
|
|
|
217
229
|
|
|
218
230
|
|
|
219
231
|
def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permissions: bool):
|
|
220
|
-
|
|
221
|
-
|
|
232
|
+
token = config.get("token", None)
|
|
233
|
+
host = config.get("host", None)
|
|
234
|
+
user_token = config.get("user_token", None)
|
|
235
|
+
workspace_id = config.get("id", None)
|
|
222
236
|
try:
|
|
223
|
-
token
|
|
224
|
-
|
|
225
|
-
|
|
237
|
+
if not token or not host or not workspace_id or not user_token:
|
|
238
|
+
yes = click.confirm(
|
|
239
|
+
FeedbackManager.warning(
|
|
240
|
+
message="Tinybird Code requires authentication. Do you want to authenticate now? [Y/n]"
|
|
241
|
+
),
|
|
242
|
+
prompt_suffix="",
|
|
243
|
+
show_default=False,
|
|
244
|
+
default=True,
|
|
245
|
+
)
|
|
246
|
+
if yes:
|
|
247
|
+
click.echo()
|
|
248
|
+
login(host, auth_host="https://cloud.tinybird.co", workspace=None, interactive=False, method="browser")
|
|
249
|
+
click.echo()
|
|
250
|
+
cli_config = CLIConfig.get_project_config()
|
|
251
|
+
token = cli_config.get_token()
|
|
252
|
+
user_token = cli_config.get_user_token()
|
|
253
|
+
host = cli_config.get_host()
|
|
254
|
+
|
|
255
|
+
if not token or not host or not user_token:
|
|
256
|
+
click.echo(
|
|
257
|
+
FeedbackManager.error(message="Tinybird Code requires authentication. Run 'tb login' first.")
|
|
258
|
+
)
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
display_banner()
|
|
262
|
+
agent = TinybirdAgent(token, user_token, host, workspace_id, project, dangerously_skip_permissions)
|
|
226
263
|
click.echo()
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
else:
|
|
231
|
-
click.echo(FeedbackManager.info(message="Run /login to authenticate"))
|
|
264
|
+
click.echo(FeedbackManager.info(message="Describe what you want to create and I'll help you build it"))
|
|
265
|
+
click.echo(FeedbackManager.info(message="Run /help for more commands"))
|
|
266
|
+
|
|
232
267
|
click.echo()
|
|
233
268
|
|
|
234
269
|
except Exception as e:
|
|
@@ -240,7 +275,7 @@ def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permiss
|
|
|
240
275
|
while True:
|
|
241
276
|
try:
|
|
242
277
|
user_input = prompt(
|
|
243
|
-
[("class:prompt", "tb » ")],
|
|
278
|
+
[("class:prompt", f"tb ({project.workspace_name}) » ")],
|
|
244
279
|
history=load_history(),
|
|
245
280
|
cursor=CursorShape.BLOCK,
|
|
246
281
|
style=Style.from_dict(
|
|
@@ -250,7 +285,10 @@ def run_agent(config: dict[str, Any], project: Project, dangerously_skip_permiss
|
|
|
250
285
|
}
|
|
251
286
|
),
|
|
252
287
|
)
|
|
253
|
-
|
|
288
|
+
if user_input.startswith("tb "):
|
|
289
|
+
cmd_parts = shlex.split(user_input)
|
|
290
|
+
subprocess.run(cmd_parts, check=True)
|
|
291
|
+
continue
|
|
254
292
|
if user_input.lower() in ["/exit", "/quit"]:
|
|
255
293
|
click.echo(FeedbackManager.info(message="Goodbye!"))
|
|
256
294
|
break
|
|
@@ -7,11 +7,12 @@ from pydantic_ai.providers.anthropic import AnthropicProvider
|
|
|
7
7
|
def create_model(
|
|
8
8
|
token: str,
|
|
9
9
|
base_url: str,
|
|
10
|
+
workspace_id: str,
|
|
10
11
|
model: AnthropicModelName = "claude-4-sonnet-20250514",
|
|
11
12
|
):
|
|
12
13
|
client = AsyncAnthropic(
|
|
13
14
|
base_url=base_url,
|
|
14
|
-
http_client=AsyncClient(params={"token": token}),
|
|
15
|
+
http_client=AsyncClient(params={"token": token, "workspace_id": workspace_id}),
|
|
15
16
|
auth_token=token,
|
|
16
17
|
)
|
|
17
18
|
return AnthropicModel(
|
|
@@ -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
|
|
@@ -211,6 +211,7 @@ def deployment_ls(ctx: click.Context, include_deleted: bool) -> None:
|
|
|
211
211
|
List all the deployments you have in the project.
|
|
212
212
|
"""
|
|
213
213
|
client = ctx.ensure_object(dict)["client"]
|
|
214
|
+
output = ctx.ensure_object(dict)["output"]
|
|
214
215
|
|
|
215
216
|
TINYBIRD_API_KEY = client.token
|
|
216
217
|
HEADERS = {"Authorization": f"Bearer {TINYBIRD_API_KEY}"}
|
|
@@ -243,7 +244,27 @@ def deployment_ls(ctx: click.Context, include_deleted: bool) -> None:
|
|
|
243
244
|
)
|
|
244
245
|
|
|
245
246
|
table.reverse()
|
|
246
|
-
|
|
247
|
+
|
|
248
|
+
# Handle different output formats
|
|
249
|
+
if output == "json":
|
|
250
|
+
# Create JSON structure
|
|
251
|
+
deployments_json = []
|
|
252
|
+
for row in table:
|
|
253
|
+
deployments_json.append({"id": row[0], "status": row[1], "created_at": row[2]})
|
|
254
|
+
from tinybird.tb.modules.common import echo_json
|
|
255
|
+
|
|
256
|
+
echo_json({"deployments": deployments_json})
|
|
257
|
+
elif output == "csv":
|
|
258
|
+
# Create CSV output
|
|
259
|
+
csv_output = f"{columns[0]},{columns[1]},{columns[2]}\n"
|
|
260
|
+
for row in table:
|
|
261
|
+
csv_output += f"{row[0]},{row[1]},{row[2]}\n"
|
|
262
|
+
from tinybird.tb.modules.common import force_echo
|
|
263
|
+
|
|
264
|
+
force_echo(csv_output)
|
|
265
|
+
else:
|
|
266
|
+
# Default human-readable output
|
|
267
|
+
echo_safe_humanfriendly_tables_format_smart_table(table, column_names=columns)
|
|
247
268
|
|
|
248
269
|
|
|
249
270
|
@deployment_group.command(name="promote")
|