csvsmith 0.7.3__tar.gz → 0.8.0__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.
- {csvsmith-0.7.3/src/csvsmith.egg-info → csvsmith-0.8.0}/PKG-INFO +9 -2
- {csvsmith-0.7.3 → csvsmith-0.8.0}/README.rst +8 -1
- {csvsmith-0.7.3 → csvsmith-0.8.0}/pyproject.toml +1 -1
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/__init__.py +8 -2
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/cli.py +29 -0
- csvsmith-0.8.0/src/csvsmith/tools/strict_concat.py +101 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/utils/clean_numeric.py +1 -1
- {csvsmith-0.7.3 → csvsmith-0.8.0/src/csvsmith.egg-info}/PKG-INFO +9 -2
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith.egg-info/SOURCES.txt +2 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_cli.py +10 -1
- csvsmith-0.8.0/tests/test_strict_concat.py +102 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/LICENSE +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/setup.cfg +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/__init__.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/classify.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/excel2csv.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/filter_rows.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/find_matches_in_csv.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/move_files.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/tools/row_dedup.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/utils/__init__.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/utils/distance.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/utils/io.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith/utils/normalize.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith.egg-info/dependency_links.txt +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith.egg-info/entry_points.txt +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith.egg-info/requires.txt +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/src/csvsmith.egg-info/top_level.txt +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_classify.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_clean_numeric.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_excel2csv.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_filter_rows.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_find_matches_in_csv.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_move_files.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_normalize.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_row_dedup.py +0 -0
- {csvsmith-0.7.3 → csvsmith-0.8.0}/tests/test_string_distance.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csvsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -87,6 +87,7 @@ Features
|
|
|
87
87
|
- Convert Excel workbooks to CSV
|
|
88
88
|
- Move files by suffix
|
|
89
89
|
- Find matching values inside CSV files
|
|
90
|
+
- Concatenate CSV files with identical headers
|
|
90
91
|
- Use the tools either from Python or from the command line
|
|
91
92
|
|
|
92
93
|
Installation
|
|
@@ -182,6 +183,12 @@ Find matches in a CSV:
|
|
|
182
183
|
|
|
183
184
|
csvsmith find-matches input.csv target --ignore-case --ignore-whitespace
|
|
184
185
|
|
|
186
|
+
Concatenate CSV files:
|
|
187
|
+
|
|
188
|
+
.. code-block:: console
|
|
189
|
+
|
|
190
|
+
csvsmith strict-concat file1.csv file2.csv -o combined.csv
|
|
191
|
+
|
|
185
192
|
Find matches in a CSV
|
|
186
193
|
---------------------
|
|
187
194
|
|
|
@@ -241,7 +248,7 @@ File and conversion helpers:
|
|
|
241
248
|
|
|
242
249
|
.. code-block:: python
|
|
243
250
|
|
|
244
|
-
from csvsmith import excel_to_csv, move_by_suffix
|
|
251
|
+
from csvsmith import excel_to_csv, move_by_suffix, strict_concat_rows, save_csv
|
|
245
252
|
|
|
246
253
|
String comparison utilities:
|
|
247
254
|
|
|
@@ -41,6 +41,7 @@ Features
|
|
|
41
41
|
- Convert Excel workbooks to CSV
|
|
42
42
|
- Move files by suffix
|
|
43
43
|
- Find matching values inside CSV files
|
|
44
|
+
- Concatenate CSV files with identical headers
|
|
44
45
|
- Use the tools either from Python or from the command line
|
|
45
46
|
|
|
46
47
|
Installation
|
|
@@ -136,6 +137,12 @@ Find matches in a CSV:
|
|
|
136
137
|
|
|
137
138
|
csvsmith find-matches input.csv target --ignore-case --ignore-whitespace
|
|
138
139
|
|
|
140
|
+
Concatenate CSV files:
|
|
141
|
+
|
|
142
|
+
.. code-block:: console
|
|
143
|
+
|
|
144
|
+
csvsmith strict-concat file1.csv file2.csv -o combined.csv
|
|
145
|
+
|
|
139
146
|
Find matches in a CSV
|
|
140
147
|
---------------------
|
|
141
148
|
|
|
@@ -195,7 +202,7 @@ File and conversion helpers:
|
|
|
195
202
|
|
|
196
203
|
.. code-block:: python
|
|
197
204
|
|
|
198
|
-
from csvsmith import excel_to_csv, move_by_suffix
|
|
205
|
+
from csvsmith import excel_to_csv, move_by_suffix, strict_concat_rows, save_csv
|
|
199
206
|
|
|
200
207
|
String comparison utilities:
|
|
201
208
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "csvsmith"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
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"
|
|
@@ -16,6 +16,8 @@ Public API:
|
|
|
16
16
|
- Relation
|
|
17
17
|
- Result
|
|
18
18
|
- analyze_pair
|
|
19
|
+
- strict_concat_rows
|
|
20
|
+
- save_csv
|
|
19
21
|
|
|
20
22
|
Compatibility aliases:
|
|
21
23
|
- CSVCleaner
|
|
@@ -28,10 +30,11 @@ Submodules:
|
|
|
28
30
|
- csvsmith.filter_rows
|
|
29
31
|
- csvsmith.excel2csv
|
|
30
32
|
- csvsmith.move_files
|
|
33
|
+
- csvsmith.strict_concat
|
|
31
34
|
- csvsmith.cli (CLI entrypoint)
|
|
32
35
|
"""
|
|
33
36
|
|
|
34
|
-
__version__ = "0.
|
|
37
|
+
__version__ = "0.8.0"
|
|
35
38
|
|
|
36
39
|
from .tools.classify import CSVClassifier
|
|
37
40
|
from .tools.excel2csv import excel_to_csv
|
|
@@ -43,6 +46,7 @@ from .tools.row_dedup import (
|
|
|
43
46
|
find_duplicate_rows,
|
|
44
47
|
dedupe_with_report,
|
|
45
48
|
)
|
|
49
|
+
from .tools.strict_concat import save_csv, strict_concat_rows
|
|
46
50
|
from .utils.clean_numeric import clean_numeric
|
|
47
51
|
from .utils.distance import StringDistance, Relation, Result, analyze_pair
|
|
48
52
|
from .utils.io import (
|
|
@@ -68,4 +72,6 @@ __all__ = [
|
|
|
68
72
|
"Result",
|
|
69
73
|
"analyze_pair",
|
|
70
74
|
"clean_numeric",
|
|
71
|
-
|
|
75
|
+
"strict_concat_rows",
|
|
76
|
+
"save_csv",
|
|
77
|
+
]
|
|
@@ -16,6 +16,7 @@ from .tools.row_dedup import (
|
|
|
16
16
|
dedupe_with_report,
|
|
17
17
|
find_duplicate_rows,
|
|
18
18
|
)
|
|
19
|
+
from .tools.strict_concat import save_csv, strict_concat_rows
|
|
19
20
|
from .tools.find_matches_in_csv import find_matches_in_csv
|
|
20
21
|
from .utils.distance import analyze_pair
|
|
21
22
|
from .utils.io import read_csv_rows, write_csv_rows
|
|
@@ -180,6 +181,23 @@ def cmd_find_matches(args: argparse.Namespace) -> int:
|
|
|
180
181
|
return 0
|
|
181
182
|
|
|
182
183
|
|
|
184
|
+
def cmd_strict_concat(args: argparse.Namespace) -> int:
|
|
185
|
+
input_dir = Path(args.input_dir)
|
|
186
|
+
if not input_dir.is_dir():
|
|
187
|
+
print(f"Error: input directory not found: {input_dir}", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
rows = strict_concat_rows(input_dir)
|
|
192
|
+
save_csv(rows, args.output)
|
|
193
|
+
except Exception as e:
|
|
194
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
195
|
+
return 1
|
|
196
|
+
|
|
197
|
+
print(f"Wrote concatenated CSV to: {args.output}")
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
183
201
|
def _add_find_matches_parser(subparsers) -> None:
|
|
184
202
|
parser = subparsers.add_parser("find-matches", help="Find matches in a CSV file.")
|
|
185
203
|
parser.add_argument("input", help="Input CSV file.")
|
|
@@ -190,6 +208,16 @@ def _add_find_matches_parser(subparsers) -> None:
|
|
|
190
208
|
parser.set_defaults(func=cmd_find_matches)
|
|
191
209
|
|
|
192
210
|
|
|
211
|
+
def _add_strict_concat_parser(subparsers) -> None:
|
|
212
|
+
parser = subparsers.add_parser(
|
|
213
|
+
"strict-concat",
|
|
214
|
+
help="Concatenate CSVs in a directory only when all headers match exactly.",
|
|
215
|
+
)
|
|
216
|
+
parser.add_argument("input_dir", help="Directory containing CSV files to concatenate.")
|
|
217
|
+
parser.add_argument("-o", "--output", required=True, help="Output CSV file path.")
|
|
218
|
+
parser.set_defaults(func=cmd_strict_concat)
|
|
219
|
+
|
|
220
|
+
|
|
193
221
|
def _add_row_duplicates_parser(subparsers) -> None:
|
|
194
222
|
parser = subparsers.add_parser("row-duplicates", help="Find duplicate rows in a CSV.")
|
|
195
223
|
parser.add_argument("input", help="Input CSV file.")
|
|
@@ -317,6 +345,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
317
345
|
|
|
318
346
|
_add_row_duplicates_parser(subparsers)
|
|
319
347
|
_add_find_matches_parser(subparsers)
|
|
348
|
+
_add_strict_concat_parser(subparsers)
|
|
320
349
|
_add_dedupe_parser(subparsers)
|
|
321
350
|
_add_classify_parser(subparsers)
|
|
322
351
|
_add_move_files_parser(subparsers)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_csvs(csv_dir: Path | str) -> list[Path]:
|
|
7
|
+
"""
|
|
8
|
+
Find all CSV files in the specified directory.
|
|
9
|
+
|
|
10
|
+
This function searches for all files with a ``.csv`` extension in the given
|
|
11
|
+
directory and returns a sorted list of their paths.
|
|
12
|
+
|
|
13
|
+
:param csv_dir: The directory to search for CSV files. This can be provided
|
|
14
|
+
as either a ``Path`` object or a string representing the path to the
|
|
15
|
+
directory.
|
|
16
|
+
:type csv_dir: Path | str
|
|
17
|
+
:return: Sorted list of paths to all ``.csv`` files found in the specified
|
|
18
|
+
directory.
|
|
19
|
+
:rtype: list[Path]
|
|
20
|
+
"""
|
|
21
|
+
return sorted(Path(csv_dir).glob("*.csv"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_header(csv_path: Path) -> list[str]:
|
|
25
|
+
"""
|
|
26
|
+
Reads the header row of a given CSV file.
|
|
27
|
+
|
|
28
|
+
This function opens a CSV file located at the specified path, reads its first
|
|
29
|
+
row, and returns it as a list of strings. The file is assumed to be encoded
|
|
30
|
+
in UTF-8 with optional BOM (Byte Order Mark). If the CSV file is empty, a
|
|
31
|
+
ValueError is raised indicating the problem. The CSV file is expected to be
|
|
32
|
+
opened in read mode with no newline translation.
|
|
33
|
+
|
|
34
|
+
:param csv_path: The path to the CSV file to read the header from.
|
|
35
|
+
:type csv_path: Path
|
|
36
|
+
:return: A list of strings representing the header row of the CSV file.
|
|
37
|
+
:rtype: list[str]
|
|
38
|
+
:raises ValueError: If the CSV file is empty and no header can be retrieved.
|
|
39
|
+
"""
|
|
40
|
+
with csv_path.open(encoding="utf-8-sig", newline="") as f:
|
|
41
|
+
reader = csv.reader(f)
|
|
42
|
+
try:
|
|
43
|
+
return next(reader)
|
|
44
|
+
except StopIteration as e:
|
|
45
|
+
raise ValueError(f"Empty CSV: {csv_path}") from e
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_headers_match(csv_paths: list[Path]) -> list[str]:
|
|
49
|
+
"""
|
|
50
|
+
Validates that the headers of all provided CSV files match each other. The function compares the
|
|
51
|
+
header of each CSV file in the input list against the header of the first file. If a mismatch
|
|
52
|
+
is encountered, a ValueError is raised indicating the problematic file. The function returns
|
|
53
|
+
the header of the first file if all headers are consistent.
|
|
54
|
+
|
|
55
|
+
:param csv_paths: A list of Path objects representing the file paths to the CSV files to validate.
|
|
56
|
+
:type csv_paths: list[Path]
|
|
57
|
+
:return: A list of strings representing the matched header of the first CSV file.
|
|
58
|
+
:rtype: list[str]
|
|
59
|
+
"""
|
|
60
|
+
expected_header = read_header(csv_paths[0])
|
|
61
|
+
for csv_path in csv_paths[1:]:
|
|
62
|
+
if read_header(csv_path) != expected_header:
|
|
63
|
+
raise ValueError(f"Header mismatch: {csv_path}")
|
|
64
|
+
return expected_header
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def strict_concat_rows(csv_dir: Path | str) -> list[list[str]]:
|
|
68
|
+
"""
|
|
69
|
+
Concatenates rows from multiple CSV files into a list of lists of strings, ensuring
|
|
70
|
+
the headers across all CSV files match. The output includes a new column indicating
|
|
71
|
+
the file stem.
|
|
72
|
+
|
|
73
|
+
:param csv_dir: Directory containing the CSV files or a specific path to a CSV file.
|
|
74
|
+
:type csv_dir: Path | str
|
|
75
|
+
:return: A list of lists, where each inner list represents a row from the concatenated
|
|
76
|
+
CSV files. The first row contains the headers, including a "file_stem" column.
|
|
77
|
+
:rtype: list[list[str]]
|
|
78
|
+
:raises FileNotFoundError: If no CSV files are found in the provided directory.
|
|
79
|
+
"""
|
|
80
|
+
csv_paths = find_csvs(csv_dir)
|
|
81
|
+
if not csv_paths:
|
|
82
|
+
raise FileNotFoundError(f"No CSV files found in: {csv_dir}")
|
|
83
|
+
|
|
84
|
+
expected_header = _validate_headers_match(csv_paths)
|
|
85
|
+
out_rows: list[list[str]] = [["file_stem", *expected_header]]
|
|
86
|
+
|
|
87
|
+
for csv_path in csv_paths:
|
|
88
|
+
with csv_path.open(encoding="utf-8-sig", newline="") as f:
|
|
89
|
+
reader = csv.reader(f)
|
|
90
|
+
next(reader) # skip header
|
|
91
|
+
for row in reader:
|
|
92
|
+
out_rows.append([csv_path.stem, *row])
|
|
93
|
+
return out_rows
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def save_csv(rows: Iterable[list[str]], out_path: Path | str) -> None:
|
|
97
|
+
"""Write rows to out_path."""
|
|
98
|
+
out_path = Path(out_path)
|
|
99
|
+
with out_path.open("w", encoding="utf-8", newline="") as f:
|
|
100
|
+
writer = csv.writer(f)
|
|
101
|
+
writer.writerows(rows)
|
|
@@ -5,7 +5,7 @@ NON_BREAKING_SPACE = "\xa0"
|
|
|
5
5
|
SEPARATOR_PATTERN = re.compile(r"[ _\xa0]")
|
|
6
6
|
NUMBER_PATTERN = re.compile(r"^-?(?:\d+|\d*\.\d+)$")
|
|
7
7
|
INVALID_NUMBER_MESSAGE = "Could not convert {value!r} to a valid number."
|
|
8
|
-
CURRENCY_PREFIX_PATTERN = re.compile(r"^[
|
|
8
|
+
CURRENCY_PREFIX_PATTERN = re.compile(r"^[\$€£¥₹]")
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def strip_currency_prefix(value: Any) -> Any:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: csvsmith
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
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
|
|
@@ -87,6 +87,7 @@ Features
|
|
|
87
87
|
- Convert Excel workbooks to CSV
|
|
88
88
|
- Move files by suffix
|
|
89
89
|
- Find matching values inside CSV files
|
|
90
|
+
- Concatenate CSV files with identical headers
|
|
90
91
|
- Use the tools either from Python or from the command line
|
|
91
92
|
|
|
92
93
|
Installation
|
|
@@ -182,6 +183,12 @@ Find matches in a CSV:
|
|
|
182
183
|
|
|
183
184
|
csvsmith find-matches input.csv target --ignore-case --ignore-whitespace
|
|
184
185
|
|
|
186
|
+
Concatenate CSV files:
|
|
187
|
+
|
|
188
|
+
.. code-block:: console
|
|
189
|
+
|
|
190
|
+
csvsmith strict-concat file1.csv file2.csv -o combined.csv
|
|
191
|
+
|
|
185
192
|
Find matches in a CSV
|
|
186
193
|
---------------------
|
|
187
194
|
|
|
@@ -241,7 +248,7 @@ File and conversion helpers:
|
|
|
241
248
|
|
|
242
249
|
.. code-block:: python
|
|
243
250
|
|
|
244
|
-
from csvsmith import excel_to_csv, move_by_suffix
|
|
251
|
+
from csvsmith import excel_to_csv, move_by_suffix, strict_concat_rows, save_csv
|
|
245
252
|
|
|
246
253
|
String comparison utilities:
|
|
247
254
|
|
|
@@ -16,6 +16,7 @@ src/csvsmith/tools/filter_rows.py
|
|
|
16
16
|
src/csvsmith/tools/find_matches_in_csv.py
|
|
17
17
|
src/csvsmith/tools/move_files.py
|
|
18
18
|
src/csvsmith/tools/row_dedup.py
|
|
19
|
+
src/csvsmith/tools/strict_concat.py
|
|
19
20
|
src/csvsmith/utils/__init__.py
|
|
20
21
|
src/csvsmith/utils/clean_numeric.py
|
|
21
22
|
src/csvsmith/utils/distance.py
|
|
@@ -30,4 +31,5 @@ tests/test_find_matches_in_csv.py
|
|
|
30
31
|
tests/test_move_files.py
|
|
31
32
|
tests/test_normalize.py
|
|
32
33
|
tests/test_row_dedup.py
|
|
34
|
+
tests/test_strict_concat.py
|
|
33
35
|
tests/test_string_distance.py
|
|
@@ -187,4 +187,13 @@ def test_cli_parses_find_matches_command():
|
|
|
187
187
|
assert args.target == "target"
|
|
188
188
|
assert args.ignore_case is True
|
|
189
189
|
assert args.ignore_whitespace is True
|
|
190
|
-
assert args.no_nfkc is True
|
|
190
|
+
assert args.no_nfkc is True
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_cli_parses_strict_concat_command():
|
|
194
|
+
parser = build_parser()
|
|
195
|
+
args = parser.parse_args(["strict-concat", "input_dir", "-o", "output.csv"])
|
|
196
|
+
|
|
197
|
+
assert args.command == "strict-concat"
|
|
198
|
+
assert args.input_dir == "input_dir"
|
|
199
|
+
assert args.output == "output.csv"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from csvsmith.tools.strict_concat import strict_concat_rows
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_csv(path: Path, rows: list[list[str]]):
|
|
10
|
+
with path.open("w", newline="", encoding="utf-8") as f:
|
|
11
|
+
writer = csv.writer(f)
|
|
12
|
+
writer.writerows(rows)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_strict_concat_happy(tmp_path):
|
|
16
|
+
f1 = tmp_path / "a.csv"
|
|
17
|
+
f2 = tmp_path / "b.csv"
|
|
18
|
+
|
|
19
|
+
write_csv(f1, [["id", "name"], ["1", "Alice"]])
|
|
20
|
+
write_csv(f2, [["id", "name"], ["2", "Bob"]])
|
|
21
|
+
|
|
22
|
+
rows = strict_concat_rows(tmp_path)
|
|
23
|
+
|
|
24
|
+
assert rows == [
|
|
25
|
+
["file_stem", "id", "name"],
|
|
26
|
+
["a", "1", "Alice"],
|
|
27
|
+
["b", "2", "Bob"],
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_strict_concat_header_mismatch(tmp_path):
|
|
32
|
+
write_csv(tmp_path / "a.csv", [["id", "name"], ["1", "Alice"]])
|
|
33
|
+
write_csv(tmp_path / "b.csv", [["id", "age"], ["2", "30"]])
|
|
34
|
+
|
|
35
|
+
with pytest.raises(ValueError, match="Header mismatch"):
|
|
36
|
+
strict_concat_rows(tmp_path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_strict_concat_empty_dir(tmp_path):
|
|
40
|
+
with pytest.raises(FileNotFoundError):
|
|
41
|
+
strict_concat_rows(tmp_path)
|
|
42
|
+
|
|
43
|
+
def test_strict_concat_with_non_csv_files(tmp_path):
|
|
44
|
+
# Create sample CSV files.
|
|
45
|
+
write_csv(tmp_path / "a.csv", [["id", "name"], ["1", "Alice"]])
|
|
46
|
+
write_csv(tmp_path / "b.csv", [["id", "name"], ["2", "Bob"]])
|
|
47
|
+
|
|
48
|
+
# Create some non-CSV files in the same directory.
|
|
49
|
+
(tmp_path / "file1.txt").write_text("This is a text file.", encoding="utf-8")
|
|
50
|
+
(tmp_path / "file2.doc").write_text("This is a Word document.", encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
# Call function and check results ignore non-CSV files.
|
|
53
|
+
rows = strict_concat_rows(tmp_path)
|
|
54
|
+
assert rows == [
|
|
55
|
+
["file_stem", "id", "name"],
|
|
56
|
+
["a", "1", "Alice"],
|
|
57
|
+
["b", "2", "Bob"],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_strict_concat_empty_csv(tmp_path):
|
|
62
|
+
(tmp_path / "a.csv").write_text("", encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
with pytest.raises(ValueError, match="Empty CSV"):
|
|
65
|
+
strict_concat_rows(tmp_path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_header_only_files(tmp_path):
|
|
69
|
+
write_csv(tmp_path / "a.csv", [["id", "name"]])
|
|
70
|
+
write_csv(tmp_path / "b.csv", [["id", "name"]])
|
|
71
|
+
|
|
72
|
+
rows = strict_concat_rows(tmp_path)
|
|
73
|
+
|
|
74
|
+
assert rows == [["file_stem", "id", "name"]]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_strict_concat_cli(tmp_path, capsys):
|
|
78
|
+
from csvsmith.cli import main
|
|
79
|
+
f1 = tmp_path / "a.csv"
|
|
80
|
+
f2 = tmp_path / "b.csv"
|
|
81
|
+
out = tmp_path / "out.csv"
|
|
82
|
+
|
|
83
|
+
write_csv(f1, [["id", "name"], ["1", "Alice"]])
|
|
84
|
+
write_csv(f2, [["id", "name"], ["2", "Bob"]])
|
|
85
|
+
|
|
86
|
+
exit_code = main(["strict-concat", str(tmp_path), "-o", str(out)])
|
|
87
|
+
|
|
88
|
+
assert exit_code == 0
|
|
89
|
+
assert out.exists()
|
|
90
|
+
|
|
91
|
+
with out.open(encoding="utf-8") as f:
|
|
92
|
+
reader = csv.reader(f)
|
|
93
|
+
rows = list(reader)
|
|
94
|
+
|
|
95
|
+
assert rows == [
|
|
96
|
+
["file_stem", "id", "name"],
|
|
97
|
+
["a", "1", "Alice"],
|
|
98
|
+
["b", "2", "Bob"],
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
captured = capsys.readouterr()
|
|
102
|
+
assert "Wrote concatenated CSV to:" in captured.out
|
|
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
|