csvsmith 0.10.0__tar.gz → 0.11.1__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 (45) hide show
  1. {csvsmith-0.10.0/src/csvsmith.egg-info → csvsmith-0.11.1}/PKG-INFO +9 -1
  2. {csvsmith-0.10.0 → csvsmith-0.11.1}/README.rst +8 -0
  3. {csvsmith-0.10.0 → csvsmith-0.11.1}/pyproject.toml +1 -1
  4. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/__init__.py +7 -0
  5. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/cli.py +110 -0
  6. csvsmith-0.11.1/src/csvsmith/tools/csv_viewer.py +168 -0
  7. {csvsmith-0.10.0 → csvsmith-0.11.1/src/csvsmith.egg-info}/PKG-INFO +9 -1
  8. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith.egg-info/SOURCES.txt +2 -0
  9. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_cli.py +29 -0
  10. csvsmith-0.11.1/tests/test_csv_viewer.py +124 -0
  11. {csvsmith-0.10.0 → csvsmith-0.11.1}/LICENSE +0 -0
  12. {csvsmith-0.10.0 → csvsmith-0.11.1}/setup.cfg +0 -0
  13. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/__init__.py +0 -0
  14. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/classify.py +0 -0
  15. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/dense_csv.py +0 -0
  16. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/excel2csv.py +0 -0
  17. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/filter_rows.py +0 -0
  18. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/find_matches_in_csv.py +0 -0
  19. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/knapsack_csv.py +0 -0
  20. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/move_files.py +0 -0
  21. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/row_dedup.py +0 -0
  22. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/sample_csv.py +0 -0
  23. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/tools/strict_concat.py +0 -0
  24. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/utils/__init__.py +0 -0
  25. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/utils/clean_numeric.py +0 -0
  26. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/utils/distance.py +0 -0
  27. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/utils/io.py +0 -0
  28. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith/utils/normalize.py +0 -0
  29. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith.egg-info/dependency_links.txt +0 -0
  30. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith.egg-info/entry_points.txt +0 -0
  31. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith.egg-info/requires.txt +0 -0
  32. {csvsmith-0.10.0 → csvsmith-0.11.1}/src/csvsmith.egg-info/top_level.txt +0 -0
  33. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_classify.py +0 -0
  34. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_clean_numeric.py +0 -0
  35. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_dense_csv.py +0 -0
  36. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_excel2csv.py +0 -0
  37. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_filter_rows.py +0 -0
  38. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_find_matches_in_csv.py +0 -0
  39. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_knapsack_csv.py +0 -0
  40. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_move_files.py +0 -0
  41. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_normalize.py +0 -0
  42. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_row_dedup.py +0 -0
  43. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_sample_csv.py +0 -0
  44. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_strict_concat.py +0 -0
  45. {csvsmith-0.10.0 → csvsmith-0.11.1}/tests/test_string_distance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csvsmith
3
- Version: 0.10.0
3
+ Version: 0.11.1
4
4
  Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
5
5
  Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
6
6
  License: MIT License
@@ -69,6 +69,7 @@ Small, focused CSV utilities for common data wrangling tasks.
69
69
  files, including cleaning numeric values, filtering rows, deduplicating
70
70
  records, classifying files, converting Excel spreadsheets to CSV, moving
71
71
  files by suffix, and finding matches inside CSV content.
72
+ It also includes a lightweight CSV viewer for quick terminal inspection.
72
73
 
73
74
  Documentation
74
75
  -------------
@@ -86,6 +87,7 @@ Features
86
87
  - Classify CSV files into folders based on headers/signatures
87
88
  - Convert Excel workbooks to CSV
88
89
  - Generate sample CSV files for testing and examples
90
+ - View selected CSV rows and columns as a text table
89
91
  - Mark rows selected by a knapsack-style capacity fit
90
92
  - Move files by suffix
91
93
  - Find matching values inside CSV files
@@ -180,6 +182,12 @@ Generate a sample CSV:
180
182
 
181
183
  csvsmith sample-csv -o sample.csv --rows 16 --seed 42
182
184
 
185
+ View a CSV file:
186
+
187
+ .. code-block:: console
188
+
189
+ csvsmith view sample.csv --columns date,value --filter value ">" 700 --head 5
190
+
183
191
  Mark rows whose values best fit a capacity:
184
192
 
185
193
  .. code-block:: console
@@ -23,6 +23,7 @@ Small, focused CSV utilities for common data wrangling tasks.
23
23
  files, including cleaning numeric values, filtering rows, deduplicating
24
24
  records, classifying files, converting Excel spreadsheets to CSV, moving
25
25
  files by suffix, and finding matches inside CSV content.
26
+ It also includes a lightweight CSV viewer for quick terminal inspection.
26
27
 
27
28
  Documentation
28
29
  -------------
@@ -40,6 +41,7 @@ Features
40
41
  - Classify CSV files into folders based on headers/signatures
41
42
  - Convert Excel workbooks to CSV
42
43
  - Generate sample CSV files for testing and examples
44
+ - View selected CSV rows and columns as a text table
43
45
  - Mark rows selected by a knapsack-style capacity fit
44
46
  - Move files by suffix
45
47
  - Find matching values inside CSV files
@@ -134,6 +136,12 @@ Generate a sample CSV:
134
136
 
135
137
  csvsmith sample-csv -o sample.csv --rows 16 --seed 42
136
138
 
139
+ View a CSV file:
140
+
141
+ .. code-block:: console
142
+
143
+ csvsmith view sample.csv --columns date,value --filter value ">" 700 --head 5
144
+
137
145
  Mark rows whose values best fit a capacity:
138
146
 
139
147
  .. code-block:: console
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csvsmith"
7
- version = "0.10.0"
7
+ version = "0.11.1"
8
8
  description = "Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers."
9
9
  readme = "README.rst"
10
10
  requires-python = ">=3.10"
@@ -21,6 +21,9 @@ Public API:
21
21
  - concentrate_csv
22
22
  - rehydrate_csv
23
23
  - create_sample_csv
24
+ - DataFrame
25
+ - build_filter
26
+ - infer_type
24
27
  - find_knapsack_indices
25
28
  - mark_knapsack_csv
26
29
 
@@ -42,6 +45,7 @@ Submodules:
42
45
  __version__ = "0.8.0"
43
46
 
44
47
  from .tools.classify import CSVClassifier
48
+ from .tools.csv_viewer import DataFrame, build_filter, infer_type
45
49
  from .tools.dense_csv import (
46
50
  ConcentrateResult,
47
51
  RehydrateResult,
@@ -97,6 +101,9 @@ __all__ = [
97
101
  "rehydrate_csv",
98
102
  "SampleCSVResult",
99
103
  "create_sample_csv",
104
+ "DataFrame",
105
+ "build_filter",
106
+ "infer_type",
100
107
  "KnapsackCSVResult",
101
108
  "find_knapsack_indices",
102
109
  "mark_knapsack_csv",
@@ -8,6 +8,7 @@ from typing import Optional, Sequence
8
8
 
9
9
  from . import __version__
10
10
  from .tools.classify import CSVClassifier
11
+ from .tools.csv_viewer import DataFrame, build_filter
11
12
  from .tools.dense_csv import concentrate_csv, rehydrate_csv
12
13
  from .tools.excel2csv import excel_to_csv
13
14
  from .utils.clean_numeric import clean_currency_numeric, clean_numeric
@@ -30,6 +31,20 @@ from .utils.distance import analyze_pair
30
31
  from .utils.io import read_csv_rows, write_csv_rows
31
32
 
32
33
 
34
+ def _non_negative_int(value: str) -> int:
35
+ number = int(value)
36
+ if number < 0:
37
+ raise argparse.ArgumentTypeError("must be 0 or greater")
38
+ return number
39
+
40
+
41
+ def _positive_int(value: str) -> int:
42
+ number = int(value)
43
+ if number < 1:
44
+ raise argparse.ArgumentTypeError("must be 1 or greater")
45
+ return number
46
+
47
+
33
48
  def cmd_row_duplicates(args: argparse.Namespace) -> int:
34
49
  rows = read_csv_rows(args.input)
35
50
  subset = args.subset.split(",") if args.subset else None
@@ -287,6 +302,61 @@ def cmd_sample_csv(args: argparse.Namespace) -> int:
287
302
  return 0
288
303
 
289
304
 
305
+ def cmd_view(args: argparse.Namespace) -> int:
306
+ try:
307
+ df = DataFrame.from_csv(
308
+ args.input,
309
+ convert_types=not args.no_convert_types,
310
+ )
311
+
312
+ if args.filter:
313
+ column, op, value = args.filter
314
+ df = df.filter(build_filter(column, op, value))
315
+
316
+ if args.columns:
317
+ columns = [column.strip() for column in args.columns.split(",") if column.strip()]
318
+ df = df.select(columns)
319
+
320
+ if args.head is not None:
321
+ output = df.head(args.head)
322
+ if output:
323
+ print(output)
324
+ elif sys.stdin.isatty() and sys.stdout.isatty():
325
+ _paginate_dataframe(df, args.page_size)
326
+ else:
327
+ output = df.render()
328
+ if output:
329
+ print(output)
330
+ except (OSError, csv.Error) as e:
331
+ print(f"Error: {e}", file=sys.stderr)
332
+ return 1
333
+ except KeyError as e:
334
+ print(f"Error: unknown column: {e.args[0]}", file=sys.stderr)
335
+ return 1
336
+ except ValueError as e:
337
+ print(f"Error: {e}", file=sys.stderr)
338
+ return 1
339
+
340
+ return 0
341
+
342
+
343
+ def _paginate_dataframe(df: DataFrame, page_size: int) -> None:
344
+ for start in range(0, len(df), page_size):
345
+ end = min(start + page_size, len(df))
346
+ output = df.render(start=start, end=end)
347
+ if output:
348
+ print(output)
349
+
350
+ if end >= len(df):
351
+ break
352
+
353
+ print(f"\nRows {start + 1}-{end} of {len(df)}")
354
+ response = input("Press Enter for next page, or q to quit: ").strip().lower()
355
+ if response == "q":
356
+ break
357
+ print()
358
+
359
+
290
360
  def cmd_knapsack(args: argparse.Namespace) -> int:
291
361
  try:
292
362
  result = mark_knapsack_csv(
@@ -403,6 +473,45 @@ def _add_sample_csv_parser(subparsers) -> None:
403
473
  parser.set_defaults(func=cmd_sample_csv)
404
474
 
405
475
 
476
+ def _add_view_parser(subparsers) -> None:
477
+ parser = subparsers.add_parser(
478
+ "view",
479
+ help="View and lightly query CSV files as a text table.",
480
+ )
481
+ parser.add_argument("input", help="Input CSV file.")
482
+ parser.add_argument(
483
+ "-n",
484
+ "--head",
485
+ type=_non_negative_int,
486
+ help="Print only the first N rows.",
487
+ )
488
+ parser.add_argument(
489
+ "-p",
490
+ "--page-size",
491
+ type=_positive_int,
492
+ default=20,
493
+ help="Number of rows per interactive page (default: 20).",
494
+ )
495
+ parser.add_argument(
496
+ "-c",
497
+ "--columns",
498
+ help="Comma-separated columns to display, such as date,amount.",
499
+ )
500
+ parser.add_argument(
501
+ "-f",
502
+ "--filter",
503
+ nargs=3,
504
+ metavar=("COLUMN", "OP", "VALUE"),
505
+ help="Filter rows with a simple expression, such as: value '>' 700.",
506
+ )
507
+ parser.add_argument(
508
+ "--no-convert-types",
509
+ action="store_true",
510
+ help="Keep all CSV values as strings instead of inferring numbers.",
511
+ )
512
+ parser.set_defaults(func=cmd_view)
513
+
514
+
406
515
  def _add_knapsack_parser(subparsers) -> None:
407
516
  parser = subparsers.add_parser(
408
517
  "knapsack",
@@ -559,6 +668,7 @@ def build_parser() -> argparse.ArgumentParser:
559
668
  _add_concentrate_parser(subparsers)
560
669
  _add_rehydrate_parser(subparsers)
561
670
  _add_sample_csv_parser(subparsers)
671
+ _add_view_parser(subparsers)
562
672
  _add_knapsack_parser(subparsers)
563
673
  _add_dedupe_parser(subparsers)
564
674
  _add_classify_parser(subparsers)
@@ -0,0 +1,168 @@
1
+ from __future__ import annotations
2
+
3
+ import csv
4
+ import operator
5
+ from pathlib import Path
6
+ from typing import Any, Callable, TextIO
7
+
8
+
9
+ OPERATORS: dict[str, Callable[[Any, Any], bool]] = {
10
+ "==": operator.eq,
11
+ "!=": operator.ne,
12
+ ">": operator.gt,
13
+ ">=": operator.ge,
14
+ "<": operator.lt,
15
+ "<=": operator.le,
16
+ }
17
+
18
+
19
+ class DataFrame:
20
+ """A lightweight in-memory CSV table for inspection and simple filtering."""
21
+
22
+ def __init__(self, data: dict[str, list[Any]]) -> None:
23
+ self._data = data
24
+ self.columns = list(data)
25
+
26
+ @classmethod
27
+ def from_csv(
28
+ cls,
29
+ filepath: str | Path,
30
+ *,
31
+ convert_types: bool = True,
32
+ ) -> DataFrame:
33
+ """Read a CSV file into a column-oriented ``DataFrame``."""
34
+ with Path(filepath).open("r", newline="", encoding="utf-8-sig") as file:
35
+ reader = csv.DictReader(file)
36
+ fieldnames = reader.fieldnames or []
37
+ data: dict[str, list[Any]] = {column: [] for column in fieldnames}
38
+
39
+ for row in reader:
40
+ for column in fieldnames:
41
+ value = row.get(column)
42
+ if convert_types:
43
+ value = infer_type(value)
44
+ data[column].append(value)
45
+
46
+ return cls(data)
47
+
48
+ def render(self, *, start: int = 0, end: int | None = None) -> str:
49
+ """Return rows from ``start`` up to ``end`` formatted as a text table."""
50
+ start = max(start, 0)
51
+ end = len(self) if end is None else min(end, len(self))
52
+ if start >= end or not self.columns:
53
+ return ""
54
+
55
+ row_indexes = range(start, end)
56
+ widths = {column: len(column) for column in self.columns}
57
+ for column in self.columns:
58
+ for index in row_indexes:
59
+ widths[column] = max(widths[column], len(str(self._data[column][index])))
60
+
61
+ lines = [
62
+ " | ".join(f"{column:<{widths[column]}}" for column in self.columns),
63
+ "-+-".join("-" * widths[column] for column in self.columns),
64
+ ]
65
+ for index in row_indexes:
66
+ lines.append(
67
+ " | ".join(
68
+ f"{str(self._data[column][index]):<{widths[column]}}"
69
+ for column in self.columns
70
+ )
71
+ )
72
+
73
+ return "\n".join(lines)
74
+
75
+ def show(
76
+ self,
77
+ *,
78
+ start: int = 0,
79
+ end: int | None = None,
80
+ file: TextIO | None = None,
81
+ ) -> None:
82
+ """Print rows from ``start`` up to ``end`` as a text table."""
83
+ rendered = self.render(start=start, end=end)
84
+ if rendered:
85
+ print(rendered, file=file)
86
+
87
+ def head(self, n: int = 5) -> str:
88
+ """Return the first ``n`` rows formatted as a text table."""
89
+ return self.render(end=n)
90
+
91
+ def select(self, columns: list[str]) -> DataFrame:
92
+ """Return a new ``DataFrame`` with the selected columns."""
93
+ missing = [column for column in columns if column not in self._data]
94
+ if missing:
95
+ raise KeyError(", ".join(missing))
96
+ return DataFrame({column: self._data[column] for column in columns})
97
+
98
+ def filter(self, condition_func: Callable[[dict[str, Any]], bool]) -> DataFrame:
99
+ """Return rows for which ``condition_func`` evaluates to true."""
100
+ filtered_data: dict[str, list[Any]] = {column: [] for column in self.columns}
101
+
102
+ for index in range(len(self)):
103
+ row = {column: self._data[column][index] for column in self.columns}
104
+ if condition_func(row):
105
+ for column in self.columns:
106
+ filtered_data[column].append(self._data[column][index])
107
+
108
+ return DataFrame(filtered_data)
109
+
110
+ def __getitem__(self, column_name: str) -> list[Any]:
111
+ return self._data[column_name]
112
+
113
+ def __setitem__(self, column_name: str, values: list[Any]) -> None:
114
+ if len(values) != len(self):
115
+ raise ValueError(
116
+ f"Length of values ({len(values)}) does not match "
117
+ f"DataFrame length ({len(self)})"
118
+ )
119
+ self._data[column_name] = list(values)
120
+ if column_name not in self.columns:
121
+ self.columns.append(column_name)
122
+
123
+ def __len__(self) -> int:
124
+ if not self.columns:
125
+ return 0
126
+ return len(self._data[self.columns[0]])
127
+
128
+ def __repr__(self) -> str:
129
+ return f"<DataFrame with {len(self)} rows and {len(self.columns)} columns>"
130
+
131
+
132
+ def infer_type(value: str | None) -> int | float | str | None:
133
+ """Convert strings to ``int`` or ``float`` when possible."""
134
+ if value is None:
135
+ return None
136
+
137
+ stripped = value.strip()
138
+ try:
139
+ return int(stripped)
140
+ except ValueError:
141
+ pass
142
+
143
+ try:
144
+ return float(stripped)
145
+ except ValueError:
146
+ return stripped
147
+
148
+
149
+ def build_filter(column: str, op: str, raw_value: str) -> Callable[[dict[str, Any]], bool]:
150
+ """Build a row predicate from a column, operator, and comparison value."""
151
+ if op not in OPERATORS:
152
+ allowed = ", ".join(OPERATORS)
153
+ raise ValueError(f"Unsupported operator {op!r}. Use one of: {allowed}.")
154
+
155
+ expected = infer_type(raw_value)
156
+ compare = OPERATORS[op]
157
+
158
+ def row_matches(row: dict[str, Any]) -> bool:
159
+ if column not in row:
160
+ raise KeyError(column)
161
+
162
+ actual = row[column]
163
+ try:
164
+ return compare(actual, expected)
165
+ except TypeError:
166
+ return compare(str(actual), str(expected))
167
+
168
+ return row_matches
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csvsmith
3
- Version: 0.10.0
3
+ Version: 0.11.1
4
4
  Summary: Small CSV utilities: row deduplication, classification, row filtering, and CLI helpers.
5
5
  Author-email: Eiichi YAMAMOTO <info@yeiichi.com>
6
6
  License: MIT License
@@ -69,6 +69,7 @@ Small, focused CSV utilities for common data wrangling tasks.
69
69
  files, including cleaning numeric values, filtering rows, deduplicating
70
70
  records, classifying files, converting Excel spreadsheets to CSV, moving
71
71
  files by suffix, and finding matches inside CSV content.
72
+ It also includes a lightweight CSV viewer for quick terminal inspection.
72
73
 
73
74
  Documentation
74
75
  -------------
@@ -86,6 +87,7 @@ Features
86
87
  - Classify CSV files into folders based on headers/signatures
87
88
  - Convert Excel workbooks to CSV
88
89
  - Generate sample CSV files for testing and examples
90
+ - View selected CSV rows and columns as a text table
89
91
  - Mark rows selected by a knapsack-style capacity fit
90
92
  - Move files by suffix
91
93
  - Find matching values inside CSV files
@@ -180,6 +182,12 @@ Generate a sample CSV:
180
182
 
181
183
  csvsmith sample-csv -o sample.csv --rows 16 --seed 42
182
184
 
185
+ View a CSV file:
186
+
187
+ .. code-block:: console
188
+
189
+ csvsmith view sample.csv --columns date,value --filter value ">" 700 --head 5
190
+
183
191
  Mark rows whose values best fit a capacity:
184
192
 
185
193
  .. code-block:: console
@@ -11,6 +11,7 @@ src/csvsmith.egg-info/requires.txt
11
11
  src/csvsmith.egg-info/top_level.txt
12
12
  src/csvsmith/tools/__init__.py
13
13
  src/csvsmith/tools/classify.py
14
+ src/csvsmith/tools/csv_viewer.py
14
15
  src/csvsmith/tools/dense_csv.py
15
16
  src/csvsmith/tools/excel2csv.py
16
17
  src/csvsmith/tools/filter_rows.py
@@ -28,6 +29,7 @@ src/csvsmith/utils/normalize.py
28
29
  tests/test_classify.py
29
30
  tests/test_clean_numeric.py
30
31
  tests/test_cli.py
32
+ tests/test_csv_viewer.py
31
33
  tests/test_dense_csv.py
32
34
  tests/test_excel2csv.py
33
35
  tests/test_filter_rows.py
@@ -279,6 +279,35 @@ def test_cli_parses_sample_csv_command():
279
279
  assert args.seed == 42
280
280
 
281
281
 
282
+ def test_cli_parses_view_command():
283
+ parser = build_parser()
284
+ args = parser.parse_args(
285
+ [
286
+ "view",
287
+ "sample.csv",
288
+ "--head",
289
+ "3",
290
+ "--page-size",
291
+ "10",
292
+ "--columns",
293
+ "date,value",
294
+ "--filter",
295
+ "value",
296
+ ">",
297
+ "700",
298
+ "--no-convert-types",
299
+ ]
300
+ )
301
+
302
+ assert args.command == "view"
303
+ assert args.input == "sample.csv"
304
+ assert args.head == 3
305
+ assert args.page_size == 10
306
+ assert args.columns == "date,value"
307
+ assert args.filter == ["value", ">", "700"]
308
+ assert args.no_convert_types is True
309
+
310
+
282
311
  def test_cli_parses_knapsack_command():
283
312
  parser = build_parser()
284
313
  args = parser.parse_args(
@@ -0,0 +1,124 @@
1
+ import csv
2
+ from pathlib import Path
3
+
4
+ import pytest
5
+
6
+ from csvsmith.cli import main
7
+ from csvsmith.tools.csv_viewer import DataFrame, build_filter, infer_type
8
+
9
+
10
+ def write_csv(path: Path, rows: list[list[str]]) -> None:
11
+ with path.open("w", encoding="utf-8", newline="") as outfile:
12
+ csv.writer(outfile).writerows(rows)
13
+
14
+
15
+ def test_dataframe_reads_csv_and_infers_numeric_types(tmp_path):
16
+ source = tmp_path / "sample.csv"
17
+ write_csv(source, [["name", "value"], ["Alice", "12"], ["Bob", "13.5"]])
18
+
19
+ df = DataFrame.from_csv(source)
20
+
21
+ assert df.columns == ["name", "value"]
22
+ assert df["name"] == ["Alice", "Bob"]
23
+ assert df["value"] == [12, 13.5]
24
+ assert repr(df) == "<DataFrame with 2 rows and 2 columns>"
25
+
26
+
27
+ def test_dataframe_reads_utf8_bom_csv_headers(tmp_path):
28
+ source = tmp_path / "sample.csv"
29
+ with source.open("w", encoding="utf-8-sig", newline="") as outfile:
30
+ csv.writer(outfile).writerows([["name", "value"], ["Alice", "12"]])
31
+
32
+ df = DataFrame.from_csv(source)
33
+
34
+ assert df.columns == ["name", "value"]
35
+ assert df["name"] == ["Alice"]
36
+
37
+
38
+ def test_dataframe_can_keep_values_as_strings(tmp_path):
39
+ source = tmp_path / "sample.csv"
40
+ write_csv(source, [["id"], ["001"]])
41
+
42
+ df = DataFrame.from_csv(source, convert_types=False)
43
+
44
+ assert df["id"] == ["001"]
45
+
46
+
47
+ def test_dataframe_render_select_and_head(tmp_path):
48
+ source = tmp_path / "sample.csv"
49
+ write_csv(
50
+ source,
51
+ [
52
+ ["name", "score", "city"],
53
+ ["Alice", "10", "Tokyo"],
54
+ ["Bob", "8", "Osaka"],
55
+ ],
56
+ )
57
+
58
+ df = DataFrame.from_csv(source).select(["name", "score"])
59
+
60
+ assert df.head(1) == "name | score\n------+------\nAlice | 10 "
61
+ assert df.render(start=1) == "name | score\n-----+------\nBob | 8 "
62
+
63
+
64
+ def test_dataframe_filter_supports_numeric_comparisons(tmp_path):
65
+ source = tmp_path / "sample.csv"
66
+ write_csv(source, [["name", "score"], ["Alice", "10"], ["Bob", "8"]])
67
+
68
+ df = DataFrame.from_csv(source).filter(build_filter("score", ">", "8"))
69
+
70
+ assert df["name"] == ["Alice"]
71
+
72
+
73
+ def test_view_command_filters_and_selects_columns(tmp_path, capsys):
74
+ source = tmp_path / "sample.csv"
75
+ write_csv(
76
+ source,
77
+ [
78
+ ["name", "score", "city"],
79
+ ["Alice", "10", "Tokyo"],
80
+ ["Bob", "8", "Osaka"],
81
+ ],
82
+ )
83
+
84
+ exit_code = main(
85
+ [
86
+ "view",
87
+ str(source),
88
+ "--columns",
89
+ "name,score",
90
+ "--filter",
91
+ "score",
92
+ ">",
93
+ "8",
94
+ ]
95
+ )
96
+
97
+ assert exit_code == 0
98
+ assert capsys.readouterr().out == "name | score\n------+------\nAlice | 10 \n"
99
+
100
+
101
+ def test_dataframe_select_rejects_missing_columns(tmp_path):
102
+ source = tmp_path / "sample.csv"
103
+ write_csv(source, [["name"], ["Alice"]])
104
+
105
+ with pytest.raises(KeyError, match="score"):
106
+ DataFrame.from_csv(source).select(["score"])
107
+
108
+
109
+ def test_build_filter_rejects_unknown_operator():
110
+ with pytest.raises(ValueError, match="Unsupported operator"):
111
+ build_filter("score", "~", "10")
112
+
113
+
114
+ @pytest.mark.parametrize(
115
+ ("raw_value", "expected"),
116
+ [
117
+ (" 12 ", 12),
118
+ ("12.5", 12.5),
119
+ ("Alice", "Alice"),
120
+ (None, None),
121
+ ],
122
+ )
123
+ def test_infer_type(raw_value, expected):
124
+ assert infer_type(raw_value) == expected
File without changes
File without changes