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.
Files changed (92) hide show
  1. tab_cli-0.1.3/CHANGELOG.md +8 -0
  2. {tab_cli-0.1.2 → tab_cli-0.1.3}/PKG-INFO +8 -1
  3. tab_cli-0.1.3/README.md +10 -0
  4. {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/cli-ref.md +2 -2
  5. {tab_cli-0.1.2/site → tab_cli-0.1.3/docs}/gen_assets.sh +1 -1
  6. {tab_cli-0.1.2 → tab_cli-0.1.3}/pyproject.toml +6 -2
  7. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/cli.py +37 -27
  8. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/avro.py +1 -1
  9. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/csv.py +1 -1
  10. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/jsonl.py +1 -1
  11. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/parquet.py +1 -1
  12. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/__init__.py +4 -3
  13. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/base.py +2 -2
  14. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/handlers/cli_table.py +8 -2
  15. tab_cli-0.1.3/tests/__init__.py +0 -0
  16. tab_cli-0.1.3/tests/test_cli.py +108 -0
  17. tab_cli-0.1.2/CHANGELOG.md +0 -5
  18. tab_cli-0.1.2/README.md +0 -3
  19. {tab_cli-0.1.2 → tab_cli-0.1.3}/.gitignore +0 -0
  20. {tab_cli-0.1.2 → tab_cli-0.1.3}/LICENSE +0 -0
  21. {tab_cli-0.1.2 → tab_cli-0.1.3}/Makefile +0 -0
  22. {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/cloud.md +0 -0
  23. {tab_cli-0.1.2 → tab_cli-0.1.3}/docs/index.md +0 -0
  24. {tab_cli-0.1.2 → tab_cli-0.1.3}/mkdocs.yml +0 -0
  25. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/404.html +0 -0
  26. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/images/favicon.png +0 -0
  27. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/bundle.d7c377c4.min.js +0 -0
  28. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/bundle.d7c377c4.min.js.map +0 -0
  29. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -0
  30. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -0
  31. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -0
  32. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -0
  33. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -0
  34. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -0
  35. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -0
  36. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -0
  37. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -0
  38. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -0
  39. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -0
  40. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -0
  41. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -0
  42. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -0
  43. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -0
  44. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -0
  45. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -0
  46. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -0
  47. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -0
  48. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -0
  49. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -0
  50. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -0
  51. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -0
  52. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -0
  53. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -0
  54. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -0
  55. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -0
  56. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -0
  57. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -0
  58. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -0
  59. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -0
  60. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -0
  61. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/tinyseg.js +0 -0
  62. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/lunr/wordcut.js +0 -0
  63. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/workers/search.f886a092.min.js +0 -0
  64. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/javascripts/workers/search.f886a092.min.js.map +0 -0
  65. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/main.50c56a3b.min.css +0 -0
  66. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/main.50c56a3b.min.css.map +0 -0
  67. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/palette.06af60db.min.css +0 -0
  68. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/stylesheets/palette.06af60db.min.css.map +0 -0
  69. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/test-where.svg +0 -0
  70. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/assets/test.svg +0 -0
  71. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/cli-ref/index.html +0 -0
  72. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/cloud/index.html +0 -0
  73. {tab_cli-0.1.2/docs → tab_cli-0.1.3/site}/gen_assets.sh +0 -0
  74. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/index.html +0 -0
  75. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/search/search_index.json +0 -0
  76. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/sitemap.xml +0 -0
  77. {tab_cli-0.1.2 → tab_cli-0.1.3}/site/sitemap.xml.gz +0 -0
  78. {tab_cli-0.1.2/docs → tab_cli-0.1.3/site}/test.csv +0 -0
  79. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/__init__.py +0 -0
  80. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/config.py +0 -0
  81. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/__init__.py +0 -0
  82. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/formats/base.py +0 -0
  83. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/__init__.py +0 -0
  84. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/aws.py +0 -0
  85. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/az.py +0 -0
  86. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/base.py +0 -0
  87. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/fsspec.py +0 -0
  88. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/gcloud.py +0 -0
  89. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/storage/local.py +0 -0
  90. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/style.py +0 -0
  91. {tab_cli-0.1.2 → tab_cli-0.1.3/src}/tab_cli/url_parser.py +0 -0
  92. {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.2
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
+ ![pypi](https://img.shields.io/pypi/v/tab-cli)
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)
@@ -0,0 +1,10 @@
1
+ # tab
2
+
3
+ ![pypi](https://img.shields.io/pypi/v/tab-cli)
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 docs/test.csv -o table-svg 2> docs/assets/test.svg
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.2"
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 _output(
50
+ def _apply_limit(
50
51
  lf: pl.LazyFrame,
51
52
  limit: int | None,
52
53
  skip: int,
53
- output: str | None,
54
- ) -> None:
55
- show_truncation = limit is None and output is None
56
- actual_limit = 20 if show_truncation else limit
57
-
58
- if show_truncation:
59
- assert actual_limit is not None
60
- lf = lf.slice(skip, length=actual_limit + 1)
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) > actual_limit
64
+ truncated = len(df) > default_limit
63
65
  if truncated:
64
- df = df.head(actual_limit)
65
- lf = df.lazy()
66
+ df = df.head(default_limit)
67
+ return df.lazy(), truncated
66
68
  else:
67
- if skip > 0 or actual_limit is not None:
68
- lf = lf.slice(skip, length=actual_limit)
69
- truncated = False
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
- output: Annotated[Optional[str], typer.Option("-o", "--output-format", help="Output format")] = None,
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 from a file."""
82
+ """View tabular data as a formatted table."""
86
83
  reader = infer_reader(path, format=input)
87
84
  lf = reader.read(path)
88
- _output(lf, limit=limit, skip=skip, output=output)
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
- _output(result_lf, limit=limit, skip=skip, output=output)
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
- _output(lf, limit=None, skip=0, output=output)
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:
@@ -14,7 +14,7 @@ class AvroFormat(FormatHandler):
14
14
  """Handler for Avro files."""
15
15
 
16
16
  def extension(self) -> str:
17
- return ".avro"
17
+ return "avro"
18
18
 
19
19
  def supports_glob(self) -> bool:
20
20
  # polars_fastavro doesn't support glob patterns
@@ -16,7 +16,7 @@ class CsvFormat(FormatHandler):
16
16
  self.separator = separator
17
17
 
18
18
  def extension(self) -> str:
19
- return ".csv" if self.separator == "," else ".tsv"
19
+ return "csv" if self.separator == "," else "tsv"
20
20
 
21
21
  def supports_glob(self) -> bool:
22
22
  return True
@@ -13,7 +13,7 @@ class JsonlFormat(FormatHandler):
13
13
  """Handler for JSONL files."""
14
14
 
15
15
  def extension(self) -> str:
16
- return ".jsonl"
16
+ return "jsonl"
17
17
 
18
18
  def supports_glob(self) -> bool:
19
19
  return True
@@ -13,7 +13,7 @@ class ParquetFormat(FormatHandler):
13
13
  """Handler for Parquet files."""
14
14
 
15
15
  def extension(self) -> str:
16
- return ".parquet"
16
+ return "parquet"
17
17
 
18
18
  def supports_glob(self) -> bool:
19
19
  return True
@@ -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
@@ -1,5 +0,0 @@
1
- - 0.1.2:
2
- - Bugfix on reading directories.
3
- - 0.1.1:
4
- - Better credential handling for Azure Blob Storage and Google Cloud Storage.
5
- - 0.1.0: Initial release
tab_cli-0.1.2/README.md DELETED
@@ -1,3 +0,0 @@
1
- # tab
2
-
3
- A CLI tool for viewing, querying, and converting tabular data files. Supports AWS / Azure / Google Cloud Storage URLs.
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