tinybird 0.0.1.dev4__py3-none-any.whl → 0.0.1.dev5__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/__cli__.py +2 -2
- tinybird/check_pypi.py +1 -1
- tinybird/feedback_manager.py +2 -2
- tinybird/tb_cli.py +1 -0
- tinybird/tb_cli_modules/build.py +0 -18
- tinybird/tb_cli_modules/cli.py +2 -69
- tinybird/tb_cli_modules/common.py +18 -0
- tinybird/tb_cli_modules/llm.py +13 -12
- tinybird/tb_cli_modules/local.py +2 -1
- tinybird/tb_cli_modules/mock.py +53 -0
- tinybird/tb_cli_modules/prompts.py +3 -0
- tinybird/tb_cli_modules/table.py +185 -0
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev5.dist-info}/METADATA +1 -1
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev5.dist-info}/RECORD +17 -15
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev5.dist-info}/WHEEL +0 -0
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev5.dist-info}/entry_points.txt +0 -0
- {tinybird-0.0.1.dev4.dist-info → tinybird-0.0.1.dev5.dist-info}/top_level.txt +0 -0
tinybird/__cli__.py
CHANGED
|
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
|
|
|
4
4
|
__url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
|
|
5
5
|
__author__ = 'Tinybird'
|
|
6
6
|
__author_email__ = 'support@tinybird.co'
|
|
7
|
-
__version__ = '0.0.1.
|
|
8
|
-
__revision__ = '
|
|
7
|
+
__version__ = '0.0.1.dev5'
|
|
8
|
+
__revision__ = 'b69dd20'
|
tinybird/check_pypi.py
CHANGED
|
@@ -6,7 +6,7 @@ from tinybird.feedback_manager import FeedbackManager
|
|
|
6
6
|
from tinybird.syncasync import sync_to_async
|
|
7
7
|
from tinybird.tb_cli_modules.common import CLIException, getenv_bool
|
|
8
8
|
|
|
9
|
-
PYPY_URL = "https://pypi.org/pypi/tinybird
|
|
9
|
+
PYPY_URL = "https://pypi.org/pypi/tinybird/json"
|
|
10
10
|
requests_get = sync_to_async(requests.get, thread_sensitive=False)
|
|
11
11
|
|
|
12
12
|
|
tinybird/feedback_manager.py
CHANGED
|
@@ -565,9 +565,9 @@ Ready? """
|
|
|
565
565
|
)
|
|
566
566
|
warning_fixture_not_found = warning_message("** Warning: No fixture found for the datasource {datasource_name}")
|
|
567
567
|
warning_update_version = warning_message(
|
|
568
|
-
'** UPDATE AVAILABLE: please run "pip install tinybird
|
|
568
|
+
'** UPDATE AVAILABLE: please run "pip install tinybird=={latest_version}" to update or `export TB_VERSION_WARNING=0` to skip the check.'
|
|
569
569
|
)
|
|
570
|
-
warning_current_version = warning_message("** current: tinybird
|
|
570
|
+
warning_current_version = warning_message("** current: tinybird {current_version}\n")
|
|
571
571
|
warning_confirm_truncate_datasource = prompt_message(
|
|
572
572
|
"Do you want to truncate {datasource}? Once truncated, your data can't be recovered"
|
|
573
573
|
)
|
tinybird/tb_cli.py
CHANGED
|
@@ -14,6 +14,7 @@ import tinybird.tb_cli_modules.create
|
|
|
14
14
|
import tinybird.tb_cli_modules.datasource
|
|
15
15
|
import tinybird.tb_cli_modules.fmt
|
|
16
16
|
import tinybird.tb_cli_modules.job
|
|
17
|
+
import tinybird.tb_cli_modules.mock
|
|
17
18
|
import tinybird.tb_cli_modules.pipe
|
|
18
19
|
import tinybird.tb_cli_modules.tag
|
|
19
20
|
import tinybird.tb_cli_modules.test
|
tinybird/tb_cli_modules/build.py
CHANGED
|
@@ -26,8 +26,6 @@ from tinybird.tb_cli_modules.cli import cli
|
|
|
26
26
|
from tinybird.tb_cli_modules.common import (
|
|
27
27
|
coro,
|
|
28
28
|
)
|
|
29
|
-
from tinybird.tb_cli_modules.config import CLIConfig
|
|
30
|
-
from tinybird.tb_cli_modules.llm import LLM
|
|
31
29
|
from tinybird.tb_cli_modules.local import (
|
|
32
30
|
get_tinybird_local_client,
|
|
33
31
|
)
|
|
@@ -153,22 +151,6 @@ async def build(
|
|
|
153
151
|
only_pipes=only_pipes,
|
|
154
152
|
)
|
|
155
153
|
|
|
156
|
-
for filename in filenames:
|
|
157
|
-
if filename.endswith(".datasource") and not skip_datasources:
|
|
158
|
-
ds_path = Path(filename)
|
|
159
|
-
ds_name = ds_path.stem
|
|
160
|
-
datasource_content = ds_path.read_text()
|
|
161
|
-
llm_config = CLIConfig.get_llm_config()
|
|
162
|
-
llm = LLM(key=llm_config["api_key"])
|
|
163
|
-
has_json_path = "`json:" in datasource_content
|
|
164
|
-
|
|
165
|
-
if has_json_path:
|
|
166
|
-
await llm.generate_sql_sample_data(
|
|
167
|
-
tb_client=tb_client,
|
|
168
|
-
datasource_name=ds_name,
|
|
169
|
-
datasource_content=datasource_content,
|
|
170
|
-
)
|
|
171
|
-
|
|
172
154
|
if watch:
|
|
173
155
|
filename = filenames[0]
|
|
174
156
|
if filename.endswith(".pipe"):
|
tinybird/tb_cli_modules/cli.py
CHANGED
|
@@ -10,14 +10,12 @@ import pprint
|
|
|
10
10
|
import re
|
|
11
11
|
import shutil
|
|
12
12
|
import sys
|
|
13
|
-
from datetime import datetime
|
|
14
13
|
from os import environ, getcwd
|
|
15
14
|
from pathlib import Path
|
|
16
15
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
|
|
17
16
|
|
|
18
17
|
import click
|
|
19
18
|
import humanfriendly
|
|
20
|
-
import requests
|
|
21
19
|
from click import Context
|
|
22
20
|
from packaging import version
|
|
23
21
|
|
|
@@ -58,7 +56,7 @@ from tinybird.tb_cli_modules.common import (
|
|
|
58
56
|
_get_tb_client,
|
|
59
57
|
coro,
|
|
60
58
|
create_tb_client,
|
|
61
|
-
|
|
59
|
+
echo_safe_format_table,
|
|
62
60
|
folder_init,
|
|
63
61
|
get_current_main_workspace,
|
|
64
62
|
getenv_bool,
|
|
@@ -69,7 +67,6 @@ from tinybird.tb_cli_modules.common import (
|
|
|
69
67
|
try_update_config_with_remote,
|
|
70
68
|
)
|
|
71
69
|
from tinybird.tb_cli_modules.config import CLIConfig
|
|
72
|
-
from tinybird.tb_cli_modules.prompts import sample_data_prompt
|
|
73
70
|
from tinybird.tb_cli_modules.telemetry import add_telemetry_event
|
|
74
71
|
|
|
75
72
|
__old_click_echo = click.echo
|
|
@@ -927,7 +924,7 @@ async def sql(
|
|
|
927
924
|
dd = []
|
|
928
925
|
for d in res["data"]:
|
|
929
926
|
dd.append(d.values())
|
|
930
|
-
|
|
927
|
+
echo_safe_format_table(dd, columns=res["meta"])
|
|
931
928
|
else:
|
|
932
929
|
click.echo(FeedbackManager.info_no_rows())
|
|
933
930
|
|
|
@@ -1572,67 +1569,3 @@ async def deploy(
|
|
|
1572
1569
|
raise
|
|
1573
1570
|
except Exception as e:
|
|
1574
1571
|
raise CLIException(str(e))
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
@cli.command()
|
|
1578
|
-
@click.argument("datasource_file", type=click.Path(exists=True))
|
|
1579
|
-
@click.option("--count", type=int, default=10, help="Number of events to send")
|
|
1580
|
-
@click.option("--model", type=str, default=None, help="Model to use for data generation")
|
|
1581
|
-
@click.option("--print-data", is_flag=True, default=False, help="Print the data being sent")
|
|
1582
|
-
@click.pass_context
|
|
1583
|
-
def load_sample_data(ctx: Context, datasource_file: str, count: int, model: Optional[str], print_data: bool) -> None:
|
|
1584
|
-
"""Load sample data into a datasource.
|
|
1585
|
-
|
|
1586
|
-
Args:
|
|
1587
|
-
ctx: Click context object
|
|
1588
|
-
datasource_file: Path to the datasource file to load sample data into
|
|
1589
|
-
"""
|
|
1590
|
-
import llm
|
|
1591
|
-
|
|
1592
|
-
try:
|
|
1593
|
-
# TODO(eclbg): allow passing a datasource name instead of a file
|
|
1594
|
-
datasource_path = Path(datasource_file)
|
|
1595
|
-
if datasource_path.suffix != ".datasource":
|
|
1596
|
-
raise CLIException(FeedbackManager.error_file_extension(filename=datasource_file))
|
|
1597
|
-
|
|
1598
|
-
datasource_name = datasource_path.stem
|
|
1599
|
-
|
|
1600
|
-
response = requests.get("http://localhost:80/tokens")
|
|
1601
|
-
token = response.json()["workspace_admin_token"]
|
|
1602
|
-
|
|
1603
|
-
with open(datasource_file) as f:
|
|
1604
|
-
content = f.read()
|
|
1605
|
-
schema_start = next(i for i, line in enumerate(content.splitlines()) if line.strip().startswith("SCHEMA >"))
|
|
1606
|
-
schema_end = next(
|
|
1607
|
-
i
|
|
1608
|
-
for i, line in enumerate(content.splitlines()[schema_start + 1 :], schema_start + 1)
|
|
1609
|
-
if not line.strip()
|
|
1610
|
-
)
|
|
1611
|
-
schema = "\n".join(content.splitlines()[schema_start:schema_end])
|
|
1612
|
-
llm_model = llm.get_model(model)
|
|
1613
|
-
click.echo(f"Using model: {model}")
|
|
1614
|
-
prompt = sample_data_prompt.format(current_datetime=datetime.now().isoformat(), row_count=count)
|
|
1615
|
-
# prompt = sample_data_with_errors_prompt.format(current_datetime=datetime.now().isoformat()) # This prompt will generate data with errors
|
|
1616
|
-
full_prompt = prompt + "\n\n" + schema
|
|
1617
|
-
sent_events = 0
|
|
1618
|
-
while sent_events < count:
|
|
1619
|
-
click.echo(f"Generating data for '{datasource_name}'")
|
|
1620
|
-
data = llm_model.prompt(full_prompt)
|
|
1621
|
-
|
|
1622
|
-
click.echo(f"Sending data to '{datasource_name}'")
|
|
1623
|
-
headers = {"Authorization": f"Bearer {token}"}
|
|
1624
|
-
if print_data:
|
|
1625
|
-
click.echo(f"Data: {data}")
|
|
1626
|
-
response = requests.post(
|
|
1627
|
-
f"http://localhost:80/v0/events?name={datasource_name}",
|
|
1628
|
-
data=data,
|
|
1629
|
-
headers=headers,
|
|
1630
|
-
)
|
|
1631
|
-
if response.status_code not in (200, 202):
|
|
1632
|
-
raise CLIException(f"Failed to send data: {response.text}")
|
|
1633
|
-
click.echo(f"Response: {response.text}")
|
|
1634
|
-
sent_events += 10
|
|
1635
|
-
click.echo(f"Sent 10 events to datasource '{datasource_name}'")
|
|
1636
|
-
|
|
1637
|
-
except Exception as e:
|
|
1638
|
-
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
@@ -53,6 +53,7 @@ from tinybird.config import (
|
|
|
53
53
|
get_display_host,
|
|
54
54
|
write_config,
|
|
55
55
|
)
|
|
56
|
+
from tinybird.tb_cli_modules.table import format_table
|
|
56
57
|
|
|
57
58
|
if TYPE_CHECKING:
|
|
58
59
|
from tinybird.connectors import Connector
|
|
@@ -134,6 +135,23 @@ def echo_safe_humanfriendly_tables_format_smart_table(data: Iterable[Any], colum
|
|
|
134
135
|
raise exc
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
def echo_safe_format_table(data: Iterable[Any], columns) -> None:
|
|
139
|
+
"""
|
|
140
|
+
There is a bug in the humanfriendly library: it breaks to render the small table for small terminals
|
|
141
|
+
(`format_robust_table`) if we call format_smart_table with an empty dataset. This catches the error and prints
|
|
142
|
+
what we would call an empty "robust_table".
|
|
143
|
+
"""
|
|
144
|
+
try:
|
|
145
|
+
click.echo(format_table(data, columns))
|
|
146
|
+
except ValueError as exc:
|
|
147
|
+
if str(exc) == "max() arg is an empty sequence":
|
|
148
|
+
click.echo("------------")
|
|
149
|
+
click.echo("Empty")
|
|
150
|
+
click.echo("------------")
|
|
151
|
+
else:
|
|
152
|
+
raise exc
|
|
153
|
+
|
|
154
|
+
|
|
137
155
|
def normalize_datasource_name(s: str) -> str:
|
|
138
156
|
s = re.sub(r"[^0-9a-zA-Z_]", "_", s)
|
|
139
157
|
if s[0] in "0123456789":
|
tinybird/tb_cli_modules/llm.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import
|
|
3
|
-
from typing import
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Awaitable, Callable, List
|
|
4
4
|
|
|
5
5
|
from openai import OpenAI
|
|
6
6
|
from pydantic import BaseModel
|
|
@@ -46,26 +46,27 @@ class LLM:
|
|
|
46
46
|
return completion.choices[0].message.parsed or DataProject(datasources=[], pipes=[])
|
|
47
47
|
|
|
48
48
|
async def generate_sql_sample_data(
|
|
49
|
-
self, tb_client: TinyB,
|
|
49
|
+
self, tb_client: TinyB, datasource_content: str, row_count: int = 20, context: str = ""
|
|
50
50
|
) -> str:
|
|
51
51
|
async def action_fn():
|
|
52
52
|
response = self.client.chat.completions.create(
|
|
53
53
|
model="gpt-4o-mini",
|
|
54
54
|
messages=[
|
|
55
|
-
{
|
|
55
|
+
{
|
|
56
|
+
"role": "system",
|
|
57
|
+
"content": sample_data_sql_prompt.format(
|
|
58
|
+
current_datetime=datetime.now().isoformat(), row_count=row_count, context=context
|
|
59
|
+
),
|
|
60
|
+
},
|
|
56
61
|
{"role": "user", "content": datasource_content},
|
|
57
62
|
],
|
|
58
63
|
)
|
|
64
|
+
return response.choices[0].message.content or ""
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
62
|
-
return result.get("data", [])
|
|
63
|
-
|
|
64
|
-
async def checker_fn(sample_data: List[Dict[str, Any]]):
|
|
65
|
-
ndjson_data = "\n".join([json.dumps(row) for row in sample_data])
|
|
66
|
+
async def checker_fn(sql: str):
|
|
66
67
|
try:
|
|
67
|
-
result = await tb_client.
|
|
68
|
-
return result.get("
|
|
68
|
+
result = await tb_client.query(f"DESCRIBE ({sql}) FORMAT JSON")
|
|
69
|
+
return len(result.get("data", [])) > 0
|
|
69
70
|
except Exception:
|
|
70
71
|
return False
|
|
71
72
|
|
tinybird/tb_cli_modules/local.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import time
|
|
2
3
|
|
|
3
4
|
import click
|
|
@@ -15,7 +16,7 @@ from tinybird.tb_cli_modules.exceptions import CLIException
|
|
|
15
16
|
# TODO: Use the official Tinybird image once it's available 'tinybirdco/tinybird-local:latest'
|
|
16
17
|
TB_IMAGE_NAME = "registry.gitlab.com/tinybird/analytics/tinybird-local-jammy-3.11:latest"
|
|
17
18
|
TB_CONTAINER_NAME = "tinybird-local"
|
|
18
|
-
TB_LOCAL_PORT = 80
|
|
19
|
+
TB_LOCAL_PORT = int(os.getenv("TB_LOCAL_PORT", 80))
|
|
19
20
|
TB_LOCAL_HOST = f"http://localhost:{TB_LOCAL_PORT}"
|
|
20
21
|
|
|
21
22
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from tinybird.feedback_manager import FeedbackManager
|
|
7
|
+
from tinybird.tb_cli_modules.cli import cli
|
|
8
|
+
from tinybird.tb_cli_modules.common import CLIException, coro
|
|
9
|
+
from tinybird.tb_cli_modules.config import CLIConfig
|
|
10
|
+
from tinybird.tb_cli_modules.llm import LLM
|
|
11
|
+
from tinybird.tb_cli_modules.local import get_tinybird_local_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@cli.command()
|
|
15
|
+
@click.argument("datasource", type=str)
|
|
16
|
+
@click.option("--rows", type=int, default=10, help="Number of events to send")
|
|
17
|
+
@click.option("--context", type=str, default="", help="Extra context to use for data generation")
|
|
18
|
+
@coro
|
|
19
|
+
async def mock(datasource: str, rows: int, context: str) -> None:
|
|
20
|
+
"""Load sample data into a datasource.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
ctx: Click context object
|
|
24
|
+
datasource_file: Path to the datasource file to load sample data into
|
|
25
|
+
"""
|
|
26
|
+
import llm
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
datasource_path = Path(datasource)
|
|
30
|
+
datasource_name = datasource
|
|
31
|
+
if datasource_path.suffix == ".datasource":
|
|
32
|
+
datasource_name = datasource_path.stem
|
|
33
|
+
else:
|
|
34
|
+
datasource_path = Path("datasources", f"{datasource}.datasource")
|
|
35
|
+
|
|
36
|
+
datasource_content = datasource_path.read_text()
|
|
37
|
+
llm_config = CLIConfig.get_llm_config()
|
|
38
|
+
llm = LLM(key=llm_config["api_key"])
|
|
39
|
+
tb_client = get_tinybird_local_client()
|
|
40
|
+
sql = await llm.generate_sql_sample_data(tb_client, datasource_content, rows, context)
|
|
41
|
+
result = await tb_client.query(f"{sql} FORMAT JSON")
|
|
42
|
+
data = result.get("data", [])
|
|
43
|
+
max_rows_per_request = 100
|
|
44
|
+
sent_rows = 0
|
|
45
|
+
for i in range(0, len(data), max_rows_per_request):
|
|
46
|
+
batch = data[i : i + max_rows_per_request]
|
|
47
|
+
ndjson_data = "\n".join([json.dumps(row) for row in batch])
|
|
48
|
+
await tb_client.datasource_events(datasource_name, ndjson_data)
|
|
49
|
+
sent_rows += len(batch)
|
|
50
|
+
click.echo(f"Sent {sent_rows} events to datasource '{datasource_name}'")
|
|
51
|
+
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise CLIException(FeedbackManager.error_exception(error=str(e)))
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# Standard library modules.
|
|
2
|
+
import collections
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
# Modules included in our package.
|
|
6
|
+
from humanfriendly.compat import coerce_string
|
|
7
|
+
from humanfriendly.tables import format_robust_table
|
|
8
|
+
from humanfriendly.terminal import (
|
|
9
|
+
ansi_strip,
|
|
10
|
+
ansi_width,
|
|
11
|
+
ansi_wrap,
|
|
12
|
+
find_terminal_size,
|
|
13
|
+
terminal_supports_colors,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from tinybird.feedback_manager import bcolors
|
|
17
|
+
|
|
18
|
+
NUMERIC_DATA_PATTERN = re.compile(r"^\d+(\.\d+)?$")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def format_table(data, columns):
|
|
22
|
+
"""
|
|
23
|
+
Render tabular data using the most appropriate representation.
|
|
24
|
+
|
|
25
|
+
:param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
|
|
26
|
+
containing the rows of the table, where each row is an
|
|
27
|
+
iterable containing the columns of the table (strings).
|
|
28
|
+
:param column_names: An iterable of column names (strings).
|
|
29
|
+
:returns: The rendered table (a string).
|
|
30
|
+
|
|
31
|
+
If you want an easy way to render tabular data on a terminal in a human
|
|
32
|
+
friendly format then this function is for you! It works as follows:
|
|
33
|
+
|
|
34
|
+
- If the input data doesn't contain any line breaks the function
|
|
35
|
+
:func:`format_pretty_table()` is used to render a pretty table. If the
|
|
36
|
+
resulting table fits in the terminal without wrapping the rendered pretty
|
|
37
|
+
table is returned.
|
|
38
|
+
|
|
39
|
+
- If the input data does contain line breaks or if a pretty table would
|
|
40
|
+
wrap (given the width of the terminal) then the function
|
|
41
|
+
:func:`format_robust_table()` is used to render a more robust table that
|
|
42
|
+
can deal with data containing line breaks and long text.
|
|
43
|
+
"""
|
|
44
|
+
# Normalize the input in case we fall back from a pretty table to a robust
|
|
45
|
+
# table (in which case we'll definitely iterate the input more than once).
|
|
46
|
+
data = [normalize_columns(r) for r in data]
|
|
47
|
+
|
|
48
|
+
column_names = []
|
|
49
|
+
column_types = []
|
|
50
|
+
|
|
51
|
+
for c in columns:
|
|
52
|
+
column_names.append(c["name"])
|
|
53
|
+
column_types.append(c["type"])
|
|
54
|
+
|
|
55
|
+
column_names = normalize_columns(column_names)
|
|
56
|
+
# Make sure the input data doesn't contain any line breaks (because pretty
|
|
57
|
+
# tables break horribly when a column's text contains a line break :-).
|
|
58
|
+
if not any(any("\n" in c for c in r) for r in data):
|
|
59
|
+
# Render a pretty table.
|
|
60
|
+
pretty_table = format_pretty_table(data, column_names, column_types)
|
|
61
|
+
# Check if the pretty table fits in the terminal.
|
|
62
|
+
table_width = max(map(ansi_width, pretty_table.splitlines()))
|
|
63
|
+
num_rows, num_columns = find_terminal_size()
|
|
64
|
+
if table_width <= num_columns:
|
|
65
|
+
# The pretty table fits in the terminal without wrapping!
|
|
66
|
+
return pretty_table
|
|
67
|
+
# Fall back to a robust table when a pretty table won't work.
|
|
68
|
+
return format_robust_table(data, column_names)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_pretty_table(data, column_names=None, column_types=None, horizontal_bar="─", vertical_bar=" "):
|
|
72
|
+
"""
|
|
73
|
+
Render a table using characters like dashes and vertical bars to emulate borders.
|
|
74
|
+
|
|
75
|
+
:param data: An iterable (e.g. a :func:`tuple` or :class:`list`)
|
|
76
|
+
containing the rows of the table, where each row is an
|
|
77
|
+
iterable containing the columns of the table (strings).
|
|
78
|
+
:param column_names: An iterable of column names (strings).
|
|
79
|
+
:param column_types: An iterable of column types (strings).
|
|
80
|
+
:param horizontal_bar: The character used to represent a horizontal bar (a
|
|
81
|
+
string).
|
|
82
|
+
:param vertical_bar: The character used to represent a vertical bar (a
|
|
83
|
+
string).
|
|
84
|
+
:returns: The rendered table (a string).
|
|
85
|
+
|
|
86
|
+
Here's an example:
|
|
87
|
+
|
|
88
|
+
>>> from humanfriendly.tables import format_pretty_table
|
|
89
|
+
>>> column_names = ['Version', 'Uploaded on', 'Downloads']
|
|
90
|
+
>>> humanfriendly_releases = [
|
|
91
|
+
... ['1.23', '2015-05-25', '218'],
|
|
92
|
+
... ['1.23.1', '2015-05-26', '1354'],
|
|
93
|
+
... ['1.24', '2015-05-26', '223'],
|
|
94
|
+
... ['1.25', '2015-05-26', '4319'],
|
|
95
|
+
... ['1.25.1', '2015-06-02', '197'],
|
|
96
|
+
... ]
|
|
97
|
+
>>> print(format_pretty_table(humanfriendly_releases, column_names))
|
|
98
|
+
-------------------------------------
|
|
99
|
+
| Version | Uploaded on | Downloads |
|
|
100
|
+
-------------------------------------
|
|
101
|
+
| 1.23 | 2015-05-25 | 218 |
|
|
102
|
+
| 1.23.1 | 2015-05-26 | 1354 |
|
|
103
|
+
| 1.24 | 2015-05-26 | 223 |
|
|
104
|
+
| 1.25 | 2015-05-26 | 4319 |
|
|
105
|
+
| 1.25.1 | 2015-06-02 | 197 |
|
|
106
|
+
-------------------------------------
|
|
107
|
+
|
|
108
|
+
Notes about the resulting table:
|
|
109
|
+
|
|
110
|
+
- If a column contains numeric data (integer and/or floating point
|
|
111
|
+
numbers) in all rows (ignoring column names of course) then the content
|
|
112
|
+
of that column is right-aligned, as can be seen in the example above. The
|
|
113
|
+
idea here is to make it easier to compare the numbers in different
|
|
114
|
+
columns to each other.
|
|
115
|
+
|
|
116
|
+
- The column names are highlighted in color so they stand out a bit more
|
|
117
|
+
(see also :data:`.HIGHLIGHT_COLOR`). The following screen shot shows what
|
|
118
|
+
that looks like (my terminals are always set to white text on a black
|
|
119
|
+
background):
|
|
120
|
+
|
|
121
|
+
.. image:: images/pretty-table.png
|
|
122
|
+
"""
|
|
123
|
+
# Normalize the input because we'll have to iterate it more than once.
|
|
124
|
+
data = [normalize_columns(r, expandtabs=True) for r in data]
|
|
125
|
+
if column_names is not None:
|
|
126
|
+
column_names = normalize_columns(column_names)
|
|
127
|
+
if column_names:
|
|
128
|
+
if terminal_supports_colors():
|
|
129
|
+
column_names = [highlight_column_name(n) for n in column_names]
|
|
130
|
+
data.insert(0, column_names)
|
|
131
|
+
if column_types is not None:
|
|
132
|
+
column_types = normalize_columns(column_types)
|
|
133
|
+
column_types = [highlight_column_type(t) for t in column_types]
|
|
134
|
+
data.insert(1, column_types)
|
|
135
|
+
# Calculate the maximum width of each column.
|
|
136
|
+
widths = collections.defaultdict(int)
|
|
137
|
+
numeric_data = collections.defaultdict(list)
|
|
138
|
+
for row_index, row in enumerate(data):
|
|
139
|
+
for column_index, column in enumerate(row):
|
|
140
|
+
widths[column_index] = max(widths[column_index], ansi_width(column))
|
|
141
|
+
if not (column_names and row_index <= 1): # Skip both header rows for numeric check
|
|
142
|
+
numeric_data[column_index].append(bool(NUMERIC_DATA_PATTERN.match(ansi_strip(column))))
|
|
143
|
+
|
|
144
|
+
horizontal_bar = highlight_horizontal_bar(horizontal_bar)
|
|
145
|
+
# Create a horizontal bar of dashes as a delimiter.
|
|
146
|
+
line_delimiter = horizontal_bar * (sum(widths.values()) + len(widths) * 3 + 1)
|
|
147
|
+
# Start the table with a vertical bar.
|
|
148
|
+
lines = []
|
|
149
|
+
# Format the rows and columns.
|
|
150
|
+
for row_index, row in enumerate(data):
|
|
151
|
+
line = [vertical_bar]
|
|
152
|
+
for column_index, column in enumerate(row):
|
|
153
|
+
padding = " " * (widths[column_index] - ansi_width(column))
|
|
154
|
+
if all(numeric_data[column_index]):
|
|
155
|
+
line.append(" " + padding + column + " ")
|
|
156
|
+
else:
|
|
157
|
+
line.append(" " + column + padding + " ")
|
|
158
|
+
line.append(vertical_bar)
|
|
159
|
+
lines.append("".join(line))
|
|
160
|
+
if column_names and column_types and row_index > 0 and row_index < len(data) - 1:
|
|
161
|
+
lines.append(line_delimiter)
|
|
162
|
+
# Join the lines, returning a single string.
|
|
163
|
+
return "\n".join(lines)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def normalize_columns(row, expandtabs=False):
|
|
167
|
+
results = []
|
|
168
|
+
for value in row:
|
|
169
|
+
text = coerce_string(value)
|
|
170
|
+
if expandtabs:
|
|
171
|
+
text = text.expandtabs()
|
|
172
|
+
results.append(text)
|
|
173
|
+
return results
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def highlight_column_name(name):
|
|
177
|
+
return ansi_wrap(name)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def highlight_column_type(type):
|
|
181
|
+
return f"{bcolors.CGREY}\033[3m{type}\033[23m{bcolors.ENDC}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def highlight_horizontal_bar(horizontal_bar):
|
|
185
|
+
return f"{bcolors.CGREY}\033[3m{horizontal_bar}\033[23m{bcolors.ENDC}"
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
tinybird/__cli__.py,sha256=
|
|
2
|
-
tinybird/check_pypi.py,sha256=
|
|
1
|
+
tinybird/__cli__.py,sha256=P6C6n43jJIAsTyrXiOcbX-PiRKUCtqwYqXix7BHTRR8,250
|
|
2
|
+
tinybird/check_pypi.py,sha256=RIhZcTg9dk5SFuvV3yVtFgBael_5_VYl4GzOZJojCfU,971
|
|
3
3
|
tinybird/client.py,sha256=nd97gD2-8Ap8yDonBcVwk9eXDAL43hmIYdo-Pse43RE,50738
|
|
4
4
|
tinybird/config.py,sha256=Z-BX9FrjgsLw1YwcCdF0IztLB97Zpc70VVPplO_pDSY,6089
|
|
5
5
|
tinybird/connectors.py,sha256=lkpVSUmSuViEZBa4QjTK7YmPHUop0a5UFoTrSmlVq6k,15244
|
|
6
6
|
tinybird/context.py,sha256=kutUQ0kCwparowI74_YLXx6wtTzGLRouJ6oGHVBPzBo,1291
|
|
7
7
|
tinybird/datafile.py,sha256=dZIIp-V99yS9bnOpH4yAiohG76qKOcJF2wDj7WroT34,253516
|
|
8
8
|
tinybird/datatypes.py,sha256=IHyhZ86ib54Vnd1pbod9y2aS8DDvDKZm1HJGlThdbuQ,10460
|
|
9
|
-
tinybird/feedback_manager.py,sha256=
|
|
9
|
+
tinybird/feedback_manager.py,sha256=qX8yrjoHUL4kjb_IKzLZPPfg4m54XWUZkdVOrv1ktpo,67814
|
|
10
10
|
tinybird/git_settings.py,sha256=XUL9ZUj59-ZVQJDYmMEq4UpnuuOuQOHGlNcX3JgQHjQ,3954
|
|
11
11
|
tinybird/sql.py,sha256=gfRKjdqEygcE1WOTeQ1QV2Jal8Jzl4RSX8fftu1KSEs,45825
|
|
12
12
|
tinybird/sql_template.py,sha256=IqYRfUxDYBCoOYjqqvn--_8QXLv9FSRnJ0bInx7q1Xs,93051
|
|
13
13
|
tinybird/sql_template_fmt.py,sha256=1z-PuqSZXtzso8Z_mPqUc-NxIxUrNUcVIPezNieZk-M,10196
|
|
14
14
|
tinybird/sql_toolset.py,sha256=xS_yD5N_TZT5d4uPcXdeIYX4GQPz7-7wHywMGdfgqcM,13794
|
|
15
15
|
tinybird/syncasync.py,sha256=fAvq0qkRgqXqXMKwbY2iJNYqLT_r6mDsh1MRpGKrdRU,27763
|
|
16
|
-
tinybird/tb_cli.py,sha256=
|
|
16
|
+
tinybird/tb_cli.py,sha256=z8TUjtki-2YXj04kry_uKGTRto35qPSibNH1Up6ooRE,855
|
|
17
17
|
tinybird/tornado_template.py,sha256=o2HguxrL1Evnt8o3IvrsI8Zm6JtRQ3zhLJKf1XyR3SQ,41965
|
|
18
18
|
tinybird/ch_utils/constants.py,sha256=aYvg2C_WxYWsnqPdZB1ZFoIr8ZY-XjUXYyHKE9Ansj0,3890
|
|
19
19
|
tinybird/ch_utils/engine.py,sha256=OXkBhlzGjZotjD0vaT-rFIbSGV4tpiHxE8qO_ip0SyQ,40454
|
|
20
20
|
tinybird/tb_cli_modules/auth.py,sha256=cqxfGgFheuTmenQ3UwPBXTqwMm8JD7uzgLfoIRXdnyQ,9038
|
|
21
21
|
tinybird/tb_cli_modules/branch.py,sha256=Ik8rRVPXvhyChHBqdPVNtI_C-0gLYjUHaznNWE04Ecw,39120
|
|
22
|
-
tinybird/tb_cli_modules/build.py,sha256=
|
|
22
|
+
tinybird/tb_cli_modules/build.py,sha256=HLcjBiBB2bjWHZK2cbdI_4tCnld_D8dHVrWA0sQEnH0,7803
|
|
23
23
|
tinybird/tb_cli_modules/cicd.py,sha256=0lMkb6CVOFZl5HOwgY8mK4T4mgI7O8335UngLXtCc-c,13851
|
|
24
|
-
tinybird/tb_cli_modules/cli.py,sha256=
|
|
25
|
-
tinybird/tb_cli_modules/common.py,sha256=
|
|
24
|
+
tinybird/tb_cli_modules/cli.py,sha256=jBex1ocNkth1REPHxvvyUaPKj5CTkqg8TLeVFtAw5B8,61973
|
|
25
|
+
tinybird/tb_cli_modules/common.py,sha256=JfQp02DZFtlMoJyfaXkRAGBetadPqda2x-C1kvWC87U,79110
|
|
26
26
|
tinybird/tb_cli_modules/config.py,sha256=ppWvACHrSLkb5hOoQLYNby2w8jR76-8Kx2NBCst7ntQ,11760
|
|
27
27
|
tinybird/tb_cli_modules/connection.py,sha256=YggP34Qh3hCjD41lp1ZHVCwHPjI_-um2spvEV8F_tSU,28684
|
|
28
28
|
tinybird/tb_cli_modules/create.py,sha256=glQiRQEVGTOw9VmoICEF5_7YWuraJrk3BSUDSTSUfP0,6796
|
|
@@ -30,11 +30,13 @@ tinybird/tb_cli_modules/datasource.py,sha256=BVYwPkKvdnEv2YMkxof7n7FOffyJm-QK7Pk
|
|
|
30
30
|
tinybird/tb_cli_modules/exceptions.py,sha256=pmucP4kTF4irIt7dXiG-FcnI-o3mvDusPmch1L8RCWk,3367
|
|
31
31
|
tinybird/tb_cli_modules/fmt.py,sha256=_xL2AyGo3us7NpmfAXtSIhn1cGAC-A4PspdhUjm58jY,3382
|
|
32
32
|
tinybird/tb_cli_modules/job.py,sha256=AG69LPb9MbobA1awwJFZJvxqarDKfRlsBjw2V1zvYqc,2964
|
|
33
|
-
tinybird/tb_cli_modules/llm.py,sha256=
|
|
34
|
-
tinybird/tb_cli_modules/local.py,sha256=
|
|
33
|
+
tinybird/tb_cli_modules/llm.py,sha256=U6GaLVi4tOq39W60A0Rjfr9JOkGcJoTfeWLixNA06zY,2450
|
|
34
|
+
tinybird/tb_cli_modules/local.py,sha256=cHT8xtF4aYsR7kLKT6hqzhass3W1dl-VCYavJphmnUM,5761
|
|
35
|
+
tinybird/tb_cli_modules/mock.py,sha256=JGB-Hid1qWYuUk-F7NR3tltDSVf5mz6tOEPYXVhPaFs,2089
|
|
35
36
|
tinybird/tb_cli_modules/pipe.py,sha256=BCLAQ3ZuWKGAih2mupnw_Y2S5B5cNS-epF317whsSEE,30989
|
|
36
|
-
tinybird/tb_cli_modules/prompts.py,sha256=
|
|
37
|
+
tinybird/tb_cli_modules/prompts.py,sha256=12Abh1kZItxMa7r3WbTi_TgTYtlV30HcaG_JiodafIA,7255
|
|
37
38
|
tinybird/tb_cli_modules/regions.py,sha256=QjsL5H6Kg-qr0aYVLrvb1STeJ5Sx_sjvbOYO0LrEGMk,166
|
|
39
|
+
tinybird/tb_cli_modules/table.py,sha256=hG-PRDVuFp2uph41WpoLRV1yjp3RI2fi_iGGiI0rdxU,7695
|
|
38
40
|
tinybird/tb_cli_modules/tag.py,sha256=9YHnruPnUNp1IJUe4qcSEMg9EbquIRo--Nxcsbvkvq8,3488
|
|
39
41
|
tinybird/tb_cli_modules/telemetry.py,sha256=iEGnMuCuNhvF6ln__j6X9MSTwL_0Hm-GgFHHHvhfknk,10466
|
|
40
42
|
tinybird/tb_cli_modules/test.py,sha256=Vf8oK96V81HdKGsT79y6MUz6oz_VrYIwTbRnzzJs4rQ,4350
|
|
@@ -43,8 +45,8 @@ tinybird/tb_cli_modules/workspace.py,sha256=N8f1dl4BYc34ucmJ6oNZc4XZMGKd8m0Fhq7o
|
|
|
43
45
|
tinybird/tb_cli_modules/workspace_members.py,sha256=ksXsjd233y9-sNlz4Qb-meZbX4zn1B84e_bSm2i8rhg,8731
|
|
44
46
|
tinybird/tb_cli_modules/tinyunit/tinyunit.py,sha256=_ydOD4WxmKy7_h-d3fG62w_0_lD0uLl9w4EbEYtwd0U,11720
|
|
45
47
|
tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py,sha256=hGh1ZaXC1af7rKnX7222urkj0QJMhMWclqMy59dOqwE,1922
|
|
46
|
-
tinybird-0.0.1.
|
|
47
|
-
tinybird-0.0.1.
|
|
48
|
-
tinybird-0.0.1.
|
|
49
|
-
tinybird-0.0.1.
|
|
50
|
-
tinybird-0.0.1.
|
|
48
|
+
tinybird-0.0.1.dev5.dist-info/METADATA,sha256=0262NudObi6F7EKjaFVPfSl5_ZQWPd8r4OYG-4TMsMg,2404
|
|
49
|
+
tinybird-0.0.1.dev5.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92
|
|
50
|
+
tinybird-0.0.1.dev5.dist-info/entry_points.txt,sha256=PKPKuPmA4IfJYnCFHHUiw-aAWZuBomFvwCklv1OyCjE,43
|
|
51
|
+
tinybird-0.0.1.dev5.dist-info/top_level.txt,sha256=pgw6AzERHBcW3YTi2PW4arjxLkulk2msOz_SomfOEuc,45
|
|
52
|
+
tinybird-0.0.1.dev5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|