tab-cli 0.1.2__tar.gz → 0.1.3__tar.gz
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.
- tab_cli-0.1.3/CHANGELOG.md +8 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/PKG-INFO +8 -1
- tab_cli-0.1.3/README.md +10 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/cli-ref.md +2 -2
- {tab_cli-0.1.2/site → tab_cli-0.1.3/docs}/gen_assets.sh +1 -1
- {tab_cli-0.1.2 → tab_cli-0.1.3}/pyproject.toml +6 -2
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/cli.py +37 -27
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/avro.py +1 -1
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/csv.py +1 -1
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/jsonl.py +1 -1
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/parquet.py +1 -1
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/__init__.py +4 -3
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/base.py +2 -2
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/cli_table.py +8 -2
- tab_cli-0.1.3/tests/__init__.py +0 -0
- tab_cli-0.1.3/tests/test_cli.py +108 -0
- tab_cli-0.1.2/CHANGELOG.md +0 -5
- tab_cli-0.1.2/README.md +0 -3
- {tab_cli-0.1.2 → tab_cli-0.1.3}/.gitignore +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/LICENSE +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/Makefile +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/cloud.md +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/index.md +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/mkdocs.yml +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/404.html +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/images/favicon.png +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/bundle.d7c377c4.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/bundle.d7c377c4.min.js.map +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/tinyseg.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/wordcut.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/workers/search.f886a092.min.js +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/workers/search.f886a092.min.js.map +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/main.50c56a3b.min.css +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/main.50c56a3b.min.css.map +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/palette.06af60db.min.css +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/palette.06af60db.min.css.map +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/test-where.svg +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/test.svg +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/cli-ref/index.html +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/cloud/index.html +0 -0
- {tab_cli-0.1.2/docs → tab_cli-0.1.3/site}/gen_assets.sh +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/index.html +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/search/search_index.json +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/sitemap.xml +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3}/site/sitemap.xml.gz +0 -0
- {tab_cli-0.1.2/docs → tab_cli-0.1.3/site}/test.csv +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/__init__.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/config.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/__init__.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/base.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/__init__.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/aws.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/az.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/base.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/fsspec.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/gcloud.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/local.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/style.py +0 -0
- {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/url_parser.py +0 -0
- {tab_cli-0.1.2/site → tab_cli-0.1.3/tests/assets}/test.csv +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- 0.1.3:
|
|
2
|
+
- Separate `tab view` from `tab cat`: `tab view` does not convert formats, `tab cat` does.
|
|
3
|
+
- Added `--max-cell-len` option to `tab view` to truncate long cell contents.
|
|
4
|
+
- 0.1.2:
|
|
5
|
+
- Bugfix on reading directories.
|
|
6
|
+
- 0.1.1:
|
|
7
|
+
- Better credential handling for Azure Blob Storage and Google Cloud Storage.
|
|
8
|
+
- 0.1.0: Initial release
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tab-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A CLI tool for tabular data
|
|
5
5
|
Author-email: Tongfei Chen <tongfei@pm.me>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -26,4 +26,11 @@ Description-Content-Type: text/markdown
|
|
|
26
26
|
|
|
27
27
|
# tab
|
|
28
28
|
|
|
29
|
+

|
|
30
|
+
```sh
|
|
31
|
+
pip install tab-cli
|
|
32
|
+
```
|
|
33
|
+
|
|
29
34
|
A CLI tool for viewing, querying, and converting tabular data files. Supports AWS / Azure / Google Cloud Storage URLs.
|
|
35
|
+
|
|
36
|
+
- Documentation: [docs](https://tongfei.me/tab)
|
tab_cli-0.1.3/README.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# tab
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
```sh
|
|
5
|
+
pip install tab-cli
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
A CLI tool for viewing, querying, and converting tabular data files. Supports AWS / Azure / Google Cloud Storage URLs.
|
|
9
|
+
|
|
10
|
+
- Documentation: [docs](https://tongfei.me/tab)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## `tab view`
|
|
4
4
|
|
|
5
|
-
View tabular data from a data file, or a directory of partitions of data files.
|
|
5
|
+
View tabular data from a data file in a rich CLI format, or a directory of partitions of data files.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
tab view $path [OPTIONS]
|
|
@@ -13,9 +13,9 @@ Options:
|
|
|
13
13
|
| Option | Description |
|
|
14
14
|
|-------------------------|-----------------------------------------------------------------------------------------------------------|
|
|
15
15
|
| `-i` / `--input-format` | Input format (`parquet`, `csv`, `tsv`, `jsonl`, `avro`). Auto-detected from extension if omitted. |
|
|
16
|
-
| `-o` / `--output-format` | Output format (`parquet`, `csv`, `tsv`, `jsonl`, `avro`). If not specified, print Rich table in terminal. |
|
|
17
16
|
| `--limit` | Maximum number of rows to display. |
|
|
18
17
|
| `--skip` | Number of rows to skip from the beginning. |
|
|
18
|
+
| `--max-cell-len` | Truncate cell contents longer than this. |
|
|
19
19
|
|
|
20
20
|
## `tab schema`
|
|
21
21
|
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
mkdir -p docs/assets
|
|
2
|
-
uv run tab view
|
|
2
|
+
uv run tab view tests/assets/test.csv -o table-svg 2> docs/assets/test.svg
|
|
3
3
|
uv run tab sql 'SELECT * FROM t WHERE Metric_A_Value > 80' docs/test.csv -o table-svg 2> docs/assets/test-where.svg
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tab-cli"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
4
4
|
description = "A CLI tool for tabular data"
|
|
5
5
|
authors = [{name = "Tongfei Chen", email = "tongfei@pm.me"}]
|
|
6
6
|
readme = "README.md"
|
|
@@ -26,12 +26,16 @@ azure = ["adlfs>=2025.1.0", "azure-identity>=1.10.0"]
|
|
|
26
26
|
dev = [
|
|
27
27
|
"ruff>=0.14.14",
|
|
28
28
|
"ty>=0.0.14",
|
|
29
|
-
"mkdocs-material>=9.0.0"
|
|
29
|
+
"mkdocs-material>=9.0.0",
|
|
30
|
+
"pytest>=8.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.scripts]
|
|
33
34
|
tab = "tab_cli.cli:main"
|
|
34
35
|
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/tab_cli"]
|
|
38
|
+
|
|
35
39
|
[build-system]
|
|
36
40
|
requires = ["hatchling"]
|
|
37
41
|
build-backend = "hatchling.build"
|
|
@@ -11,6 +11,7 @@ from rich.logging import RichHandler
|
|
|
11
11
|
|
|
12
12
|
from tab_cli import config
|
|
13
13
|
from tab_cli.handlers import TableWriter, infer_reader, infer_writer
|
|
14
|
+
from tab_cli.handlers.cli_table import CliTableFormatter
|
|
14
15
|
|
|
15
16
|
app = typer.Typer(
|
|
16
17
|
help="A CLI tool for viewing and manipulating tabular data.",
|
|
@@ -46,32 +47,28 @@ def main_callback(
|
|
|
46
47
|
)
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
def
|
|
50
|
+
def _apply_limit(
|
|
50
51
|
lf: pl.LazyFrame,
|
|
51
52
|
limit: int | None,
|
|
52
53
|
skip: int,
|
|
53
|
-
|
|
54
|
-
) ->
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
default_limit: int | None = None,
|
|
55
|
+
) -> tuple[pl.LazyFrame, bool]:
|
|
56
|
+
"""Apply skip/limit to a LazyFrame, optionally detecting truncation.
|
|
57
|
+
|
|
58
|
+
If limit is None and default_limit is set, caps at default_limit rows
|
|
59
|
+
and returns whether the data was truncated.
|
|
60
|
+
"""
|
|
61
|
+
if limit is None and default_limit is not None:
|
|
62
|
+
lf = lf.slice(skip, length=default_limit + 1)
|
|
61
63
|
df = lf.collect()
|
|
62
|
-
truncated = len(df) >
|
|
64
|
+
truncated = len(df) > default_limit
|
|
63
65
|
if truncated:
|
|
64
|
-
df = df.head(
|
|
65
|
-
|
|
66
|
+
df = df.head(default_limit)
|
|
67
|
+
return df.lazy(), truncated
|
|
66
68
|
else:
|
|
67
|
-
if skip > 0 or
|
|
68
|
-
lf = lf.slice(skip, length=
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
writer = infer_writer(output, truncated=show_truncation and truncated)
|
|
72
|
-
|
|
73
|
-
for chunk in writer.write(lf):
|
|
74
|
-
sys.stdout.buffer.write(chunk)
|
|
69
|
+
if skip > 0 or limit is not None:
|
|
70
|
+
lf = lf.slice(skip, length=limit)
|
|
71
|
+
return lf, False
|
|
75
72
|
|
|
76
73
|
|
|
77
74
|
@app.command()
|
|
@@ -79,13 +76,16 @@ def view(
|
|
|
79
76
|
path: Annotated[str, typer.Argument(help="Path to the data file or directory")],
|
|
80
77
|
limit: Annotated[Optional[int], typer.Option("--limit", help="Maximum number of rows to display")] = None,
|
|
81
78
|
skip: Annotated[int, typer.Option("--skip", help="Number of rows to skip")] = 0,
|
|
82
|
-
input: Annotated[Optional[str], typer.Option("-i", "--input-format", help="Input format")] = None,
|
|
83
|
-
|
|
79
|
+
input: Annotated[Optional[str], typer.Option("-i", "--input-format", help="Input format, auto-detected from extension if omitted")] = None,
|
|
80
|
+
max_cell_len: Annotated[Optional[int], typer.Option("--max-cell-len", help="Truncate cell contents longer than this")] = None,
|
|
84
81
|
) -> None:
|
|
85
|
-
"""View tabular data
|
|
82
|
+
"""View tabular data as a formatted table."""
|
|
86
83
|
reader = infer_reader(path, format=input)
|
|
87
84
|
lf = reader.read(path)
|
|
88
|
-
|
|
85
|
+
lf, truncated = _apply_limit(lf, limit=limit, skip=skip, default_limit=20 if limit is None else None)
|
|
86
|
+
writer = CliTableFormatter(truncated=truncated, max_cell_len=max_cell_len)
|
|
87
|
+
for chunk in writer.write(lf):
|
|
88
|
+
sys.stdout.buffer.write(chunk)
|
|
89
89
|
|
|
90
90
|
@app.command()
|
|
91
91
|
def schema(
|
|
@@ -113,7 +113,11 @@ def sql(
|
|
|
113
113
|
lf = reader.read(path)
|
|
114
114
|
ctx = pl.SQLContext(t=lf, eager=False)
|
|
115
115
|
result_lf = ctx.execute(query)
|
|
116
|
-
|
|
116
|
+
show_truncation = limit is None and output is None
|
|
117
|
+
result_lf, truncated = _apply_limit(result_lf, limit=limit, skip=skip, default_limit=20 if show_truncation else None)
|
|
118
|
+
writer = infer_writer(output, truncated=truncated)
|
|
119
|
+
for chunk in writer.write(result_lf):
|
|
120
|
+
sys.stdout.buffer.write(chunk)
|
|
117
121
|
|
|
118
122
|
|
|
119
123
|
@app.command()
|
|
@@ -156,11 +160,17 @@ def cat(
|
|
|
156
160
|
input: Annotated[Optional[str], typer.Option("-i", "--input-format", help="Input format")] = None,
|
|
157
161
|
output: Annotated[Optional[str], typer.Option("-o", "--output-format", help="Output format")] = None,
|
|
158
162
|
) -> None:
|
|
159
|
-
"""Concatenate tabular data from multiple files."""
|
|
163
|
+
"""Concatenate tabular data from multiple files, or just print a single file."""
|
|
160
164
|
reader = infer_reader(paths[0], format=input)
|
|
161
165
|
files = [reader.read(path) for path in paths]
|
|
162
166
|
lf = pl.concat(files, how="vertical")
|
|
163
|
-
|
|
167
|
+
if output is not None:
|
|
168
|
+
writer = infer_writer(format=output)
|
|
169
|
+
else:
|
|
170
|
+
writer = infer_writer(format=reader.format.extension())
|
|
171
|
+
assert isinstance(writer, TableWriter)
|
|
172
|
+
for chunk in writer.write(lf):
|
|
173
|
+
sys.stdout.buffer.write(chunk)
|
|
164
174
|
|
|
165
175
|
|
|
166
176
|
def main() -> None:
|
|
@@ -74,20 +74,21 @@ def infer_reader(path: str, format: str | None = None) -> TableReader:
|
|
|
74
74
|
return TableReader(backend, fmt)
|
|
75
75
|
|
|
76
76
|
|
|
77
|
-
def infer_writer(format: str | None = None, truncated: bool = False) -> TableWriter:
|
|
77
|
+
def infer_writer(format: str | None = None, truncated: bool = False, max_cell_len: int | None = None) -> TableWriter:
|
|
78
78
|
"""Infer the writer for a format.
|
|
79
79
|
|
|
80
80
|
Args:
|
|
81
81
|
format: Output format. If None, returns CLI table formatter.
|
|
82
82
|
truncated: Whether the output is truncated (for CLI display).
|
|
83
|
+
max_cell_len: Maximum cell content length for CLI table display.
|
|
83
84
|
|
|
84
85
|
Returns:
|
|
85
86
|
TableWriter for the format.
|
|
86
87
|
"""
|
|
87
88
|
if format is None:
|
|
88
|
-
return CliTableFormatter(truncated=truncated)
|
|
89
|
+
return CliTableFormatter(truncated=truncated, max_cell_len=max_cell_len)
|
|
89
90
|
if format == "table-svg":
|
|
90
|
-
return CliTableFormatter(truncated=truncated, svg_capture=True)
|
|
91
|
+
return CliTableFormatter(truncated=truncated, svg_capture=True, max_cell_len=max_cell_len)
|
|
91
92
|
|
|
92
93
|
fmt = _FORMAT_MAP.get(format.lower())
|
|
93
94
|
if fmt is None:
|
|
@@ -119,7 +119,7 @@ class TableReader:
|
|
|
119
119
|
def schema(self, url: str) -> TableSchema:
|
|
120
120
|
if self.backend.is_directory(url):
|
|
121
121
|
# Get schema from first file
|
|
122
|
-
files = list(self.backend.list_files(url, self.format.extension()))
|
|
122
|
+
files = list(self.backend.list_files(url, "." + self.format.extension()))
|
|
123
123
|
if not files:
|
|
124
124
|
raise ValueError(f"No {self.format.extension()} files found in {url}")
|
|
125
125
|
url = files[0].url
|
|
@@ -151,7 +151,7 @@ class TableReader:
|
|
|
151
151
|
|
|
152
152
|
def _summary_directory(self, url: str) -> TableSummary:
|
|
153
153
|
"""Aggregate summary from all files in directory."""
|
|
154
|
-
files = list(self.backend.list_files(url, self.format.extension()))
|
|
154
|
+
files = list(self.backend.list_files(url, "." + self.format.extension()))
|
|
155
155
|
if not files:
|
|
156
156
|
raise ValueError(f"No {self.format.extension()} files found in {url}")
|
|
157
157
|
|
|
@@ -11,9 +11,15 @@ from tab_cli.style import _ALT_ROW_STYLE_0, _ALT_ROW_STYLE_1, _KEY_STYLE
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CliTableFormatter(TableWriter):
|
|
14
|
-
def __init__(self, truncated: bool = False, svg_capture: bool = False):
|
|
14
|
+
def __init__(self, truncated: bool = False, svg_capture: bool = False, max_cell_len: int | None = None):
|
|
15
15
|
self.truncated = truncated
|
|
16
16
|
self.svg_capture = svg_capture
|
|
17
|
+
self.max_cell_len = max_cell_len
|
|
18
|
+
|
|
19
|
+
def _truncate(self, value: str) -> str:
|
|
20
|
+
if self.max_cell_len is not None and len(value) > self.max_cell_len:
|
|
21
|
+
return value[:self.max_cell_len] + "..."
|
|
22
|
+
return value
|
|
17
23
|
|
|
18
24
|
def extension(self) -> str:
|
|
19
25
|
return ".txt"
|
|
@@ -32,7 +38,7 @@ class CliTableFormatter(TableWriter):
|
|
|
32
38
|
|
|
33
39
|
for batch in lf.collect_batches():
|
|
34
40
|
for row in batch.iter_rows():
|
|
35
|
-
table.add_row(*[str(v) if v is not None else "" for v in row])
|
|
41
|
+
table.add_row(*[self._truncate(str(v)) if v is not None else "" for v in row])
|
|
36
42
|
|
|
37
43
|
if self.truncated:
|
|
38
44
|
table.add_row(*["..." for _ in lf.collect_schema().names()])
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Tests for the tab CLI commands."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from tab_cli.cli import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
TEST_CSV = os.path.join(os.path.dirname(__file__), "assets", "test.csv")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestView:
|
|
14
|
+
def test_basic(self):
|
|
15
|
+
result = runner.invoke(app, ["view", TEST_CSV])
|
|
16
|
+
assert result.exit_code == 0
|
|
17
|
+
assert "P001" in result.output
|
|
18
|
+
assert "Control" in result.output
|
|
19
|
+
|
|
20
|
+
def test_limit(self):
|
|
21
|
+
result = runner.invoke(app, ["view", TEST_CSV, "--limit", "2"])
|
|
22
|
+
assert result.exit_code == 0
|
|
23
|
+
assert "P001" in result.output
|
|
24
|
+
# Row 3 (P002 second row) should not appear
|
|
25
|
+
assert "P003" not in result.output
|
|
26
|
+
# No truncation indicator when explicit limit
|
|
27
|
+
assert "..." not in result.output
|
|
28
|
+
|
|
29
|
+
def test_skip(self):
|
|
30
|
+
result = runner.invoke(app, ["view", TEST_CSV, "--skip", "6", "--limit", "10"])
|
|
31
|
+
assert result.exit_code == 0
|
|
32
|
+
# First 6 rows skipped; only P004 rows remain
|
|
33
|
+
assert "P001" not in result.output
|
|
34
|
+
assert "P004" in result.output
|
|
35
|
+
|
|
36
|
+
def test_max_cell_len(self):
|
|
37
|
+
result = runner.invoke(app, ["view", TEST_CSV, "--max-cell-len", "5"])
|
|
38
|
+
assert result.exit_code == 0
|
|
39
|
+
# "Control" (7 chars) should be truncated to "Contr..."
|
|
40
|
+
assert "Contr..." in result.output
|
|
41
|
+
# "P001" (4 chars) fits within 5, should appear as-is
|
|
42
|
+
assert "P001" in result.output
|
|
43
|
+
|
|
44
|
+
def test_no_output_flag(self):
|
|
45
|
+
result = runner.invoke(app, ["view", TEST_CSV, "-o", "csv"])
|
|
46
|
+
assert result.exit_code != 0
|
|
47
|
+
|
|
48
|
+
def test_truncation_indicator(self):
|
|
49
|
+
"""With no --limit and more than 20 rows, truncation '...' should appear.
|
|
50
|
+
Our test.csv only has 8 rows, so no truncation."""
|
|
51
|
+
result = runner.invoke(app, ["view", TEST_CSV])
|
|
52
|
+
assert result.exit_code == 0
|
|
53
|
+
# 8 rows < 20 default limit, so no truncation
|
|
54
|
+
lines_with_ellipsis = [l for l in result.output.splitlines() if l.strip() == "... ... ... ... ... ..."]
|
|
55
|
+
assert len(lines_with_ellipsis) == 0
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class TestCat:
|
|
59
|
+
def test_basic_outputs_csv(self):
|
|
60
|
+
result = runner.invoke(app, ["cat", TEST_CSV])
|
|
61
|
+
assert result.exit_code == 0
|
|
62
|
+
# Should output in CSV format (the input format), not a Rich table
|
|
63
|
+
assert "Participant_ID," in result.output or "Participant_ID\t" in result.output or "P001" in result.output
|
|
64
|
+
|
|
65
|
+
def test_output_format_csv(self):
|
|
66
|
+
result = runner.invoke(app, ["cat", TEST_CSV, "-o", "csv"])
|
|
67
|
+
assert result.exit_code == 0
|
|
68
|
+
lines = result.output.strip().splitlines()
|
|
69
|
+
# CSV header
|
|
70
|
+
assert "Participant_ID" in lines[0]
|
|
71
|
+
# Should have header + 8 data rows
|
|
72
|
+
assert len(lines) == 9
|
|
73
|
+
|
|
74
|
+
def test_output_format_tsv(self):
|
|
75
|
+
result = runner.invoke(app, ["cat", TEST_CSV, "-o", "tsv"])
|
|
76
|
+
assert result.exit_code == 0
|
|
77
|
+
lines = result.output.strip().splitlines()
|
|
78
|
+
assert "\t" in lines[0]
|
|
79
|
+
|
|
80
|
+
def test_no_rich_table(self):
|
|
81
|
+
"""cat without -o should NOT produce a Rich formatted table."""
|
|
82
|
+
result = runner.invoke(app, ["cat", TEST_CSV])
|
|
83
|
+
assert result.exit_code == 0
|
|
84
|
+
# Rich tables use box-drawing chars; CSV output won't
|
|
85
|
+
assert "─" not in result.output
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestSql:
|
|
89
|
+
def test_basic_table_output(self):
|
|
90
|
+
result = runner.invoke(app, ["sql", "SELECT * FROM t WHERE Status = 'Baseline'", TEST_CSV])
|
|
91
|
+
assert result.exit_code == 0
|
|
92
|
+
assert "Baseline" in result.output
|
|
93
|
+
# Should show as a table by default (no -o)
|
|
94
|
+
assert "Active" not in result.output
|
|
95
|
+
|
|
96
|
+
def test_with_output_format(self):
|
|
97
|
+
result = runner.invoke(app, ["sql", "SELECT Participant_ID, Status FROM t", TEST_CSV, "-o", "csv"])
|
|
98
|
+
assert result.exit_code == 0
|
|
99
|
+
lines = result.output.strip().splitlines()
|
|
100
|
+
assert "Participant_ID" in lines[0]
|
|
101
|
+
assert "Status" in lines[0]
|
|
102
|
+
|
|
103
|
+
def test_limit(self):
|
|
104
|
+
result = runner.invoke(app, ["sql", "SELECT * FROM t", TEST_CSV, "--limit", "2"])
|
|
105
|
+
assert result.exit_code == 0
|
|
106
|
+
# Should have limited rows
|
|
107
|
+
count = sum(1 for line in result.output.splitlines() if "P00" in line)
|
|
108
|
+
assert count <= 2
|
tab_cli-0.1.2/CHANGELOG.md
DELETED
tab_cli-0.1.2/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|