csvsmith 0.11.1__tar.gz → 0.12.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.11.1/src/csvsmith.egg-info → csvsmith-0.12.0}/PKG-INFO +1 -1
- {csvsmith-0.11.1 → csvsmith-0.12.0}/pyproject.toml +1 -1
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/__init__.py +4 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/cli.py +33 -0
- csvsmith-0.12.0/src/csvsmith/tools/strip_bom.py +52 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0/src/csvsmith.egg-info}/PKG-INFO +1 -1
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith.egg-info/SOURCES.txt +3 -1
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_cli.py +20 -0
- csvsmith-0.12.0/tests/test_strip_bom.py +63 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/LICENSE +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/README.rst +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/setup.cfg +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/__init__.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/classify.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/csv_viewer.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/dense_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/excel2csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/filter_rows.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/find_matches_in_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/knapsack_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/move_files.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/row_dedup.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/sample_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/tools/strict_concat.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/utils/__init__.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/utils/clean_numeric.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/utils/distance.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/utils/io.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith/utils/normalize.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith.egg-info/dependency_links.txt +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith.egg-info/entry_points.txt +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith.egg-info/requires.txt +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/src/csvsmith.egg-info/top_level.txt +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_classify.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_clean_numeric.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_csv_viewer.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_dense_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_excel2csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_filter_rows.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_find_matches_in_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_knapsack_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_move_files.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_normalize.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_row_dedup.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_sample_csv.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_strict_concat.py +0 -0
- {csvsmith-0.11.1 → csvsmith-0.12.0}/tests/test_string_distance.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "csvsmith"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.12.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"
|
|
@@ -21,6 +21,7 @@ Public API:
|
|
|
21
21
|
- concentrate_csv
|
|
22
22
|
- rehydrate_csv
|
|
23
23
|
- create_sample_csv
|
|
24
|
+
- strip_utf8_bom
|
|
24
25
|
- DataFrame
|
|
25
26
|
- build_filter
|
|
26
27
|
- infer_type
|
|
@@ -67,6 +68,7 @@ from .tools.row_dedup import (
|
|
|
67
68
|
dedupe_with_report,
|
|
68
69
|
)
|
|
69
70
|
from .tools.sample_csv import SampleCSVResult, create_sample_csv
|
|
71
|
+
from .tools.strip_bom import StripBomResult, strip_utf8_bom
|
|
70
72
|
from .tools.strict_concat import save_csv, strict_concat_rows
|
|
71
73
|
from .utils.clean_numeric import clean_numeric
|
|
72
74
|
from .utils.distance import StringDistance, Relation, Result, analyze_pair
|
|
@@ -101,6 +103,8 @@ __all__ = [
|
|
|
101
103
|
"rehydrate_csv",
|
|
102
104
|
"SampleCSVResult",
|
|
103
105
|
"create_sample_csv",
|
|
106
|
+
"StripBomResult",
|
|
107
|
+
"strip_utf8_bom",
|
|
104
108
|
"DataFrame",
|
|
105
109
|
"build_filter",
|
|
106
110
|
"infer_type",
|
|
@@ -24,6 +24,7 @@ from .tools.sample_csv import (
|
|
|
24
24
|
DEFAULT_ROW_COUNT,
|
|
25
25
|
create_sample_csv,
|
|
26
26
|
)
|
|
27
|
+
from .tools.strip_bom import strip_utf8_bom
|
|
27
28
|
from .tools.strict_concat import save_csv, strict_concat_rows
|
|
28
29
|
from .tools.find_matches_in_csv import find_matches_in_csv
|
|
29
30
|
from .tools.knapsack_csv import mark_knapsack_csv
|
|
@@ -204,6 +205,18 @@ def cmd_find_matches(args: argparse.Namespace) -> int:
|
|
|
204
205
|
return 0
|
|
205
206
|
|
|
206
207
|
|
|
208
|
+
def cmd_strip_bom(args: argparse.Namespace) -> int:
|
|
209
|
+
try:
|
|
210
|
+
result = strip_utf8_bom(args.input, args.output, in_place=args.in_place)
|
|
211
|
+
except (OSError, ValueError) as e:
|
|
212
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
|
|
215
|
+
status = "removed BOM" if result.removed else "no BOM found"
|
|
216
|
+
print(f"Wrote CSV to: {result.output_path} ({status})")
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
207
220
|
def cmd_strict_concat(args: argparse.Namespace) -> int:
|
|
208
221
|
input_dir = Path(args.input_dir)
|
|
209
222
|
if not input_dir.is_dir():
|
|
@@ -388,6 +401,25 @@ def _add_find_matches_parser(subparsers) -> None:
|
|
|
388
401
|
parser.set_defaults(func=cmd_find_matches)
|
|
389
402
|
|
|
390
403
|
|
|
404
|
+
def _add_strip_bom_parser(subparsers) -> None:
|
|
405
|
+
parser = subparsers.add_parser(
|
|
406
|
+
"strip-bom",
|
|
407
|
+
help="Remove a leading UTF-8 BOM from a CSV file.",
|
|
408
|
+
)
|
|
409
|
+
parser.add_argument("input", help="Input CSV file.")
|
|
410
|
+
parser.add_argument(
|
|
411
|
+
"-o",
|
|
412
|
+
"--output",
|
|
413
|
+
help="Output CSV file (default: <input-stem>.no-bom.csv).",
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--in-place",
|
|
417
|
+
action="store_true",
|
|
418
|
+
help="Rewrite the input file instead of writing a separate output file.",
|
|
419
|
+
)
|
|
420
|
+
parser.set_defaults(func=cmd_strip_bom)
|
|
421
|
+
|
|
422
|
+
|
|
391
423
|
def _add_strict_concat_parser(subparsers) -> None:
|
|
392
424
|
parser = subparsers.add_parser(
|
|
393
425
|
"strict-concat",
|
|
@@ -664,6 +696,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
664
696
|
|
|
665
697
|
_add_row_duplicates_parser(subparsers)
|
|
666
698
|
_add_find_matches_parser(subparsers)
|
|
699
|
+
_add_strip_bom_parser(subparsers)
|
|
667
700
|
_add_strict_concat_parser(subparsers)
|
|
668
701
|
_add_concentrate_parser(subparsers)
|
|
669
702
|
_add_rehydrate_parser(subparsers)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
UTF8_BOM = b"\xef\xbb\xbf"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class StripBomResult:
|
|
12
|
+
input_path: Path
|
|
13
|
+
output_path: Path
|
|
14
|
+
removed: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def default_output_path(csv_path: str | Path) -> Path:
|
|
18
|
+
"""Return the default output path for a BOM-stripped CSV."""
|
|
19
|
+
path = Path(csv_path)
|
|
20
|
+
return path.with_name(f"{path.stem}.no-bom{path.suffix}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def strip_utf8_bom(
|
|
24
|
+
csv_path: str | Path,
|
|
25
|
+
output_path: str | Path | None = None,
|
|
26
|
+
*,
|
|
27
|
+
in_place: bool = False,
|
|
28
|
+
) -> StripBomResult:
|
|
29
|
+
"""Remove a leading UTF-8 BOM from a CSV file while preserving all other bytes."""
|
|
30
|
+
input_path = Path(csv_path)
|
|
31
|
+
if in_place and output_path is not None:
|
|
32
|
+
raise ValueError("--in-place cannot be used with --output")
|
|
33
|
+
|
|
34
|
+
if in_place:
|
|
35
|
+
resolved_output_path = input_path
|
|
36
|
+
elif output_path:
|
|
37
|
+
resolved_output_path = Path(output_path)
|
|
38
|
+
else:
|
|
39
|
+
resolved_output_path = default_output_path(input_path)
|
|
40
|
+
|
|
41
|
+
data = input_path.read_bytes()
|
|
42
|
+
removed = data.startswith(UTF8_BOM)
|
|
43
|
+
output_data = data[len(UTF8_BOM) :] if removed else data
|
|
44
|
+
|
|
45
|
+
resolved_output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
resolved_output_path.write_bytes(output_data)
|
|
47
|
+
|
|
48
|
+
return StripBomResult(
|
|
49
|
+
input_path=input_path,
|
|
50
|
+
output_path=resolved_output_path,
|
|
51
|
+
removed=removed,
|
|
52
|
+
)
|
|
@@ -21,6 +21,7 @@ src/csvsmith/tools/move_files.py
|
|
|
21
21
|
src/csvsmith/tools/row_dedup.py
|
|
22
22
|
src/csvsmith/tools/sample_csv.py
|
|
23
23
|
src/csvsmith/tools/strict_concat.py
|
|
24
|
+
src/csvsmith/tools/strip_bom.py
|
|
24
25
|
src/csvsmith/utils/__init__.py
|
|
25
26
|
src/csvsmith/utils/clean_numeric.py
|
|
26
27
|
src/csvsmith/utils/distance.py
|
|
@@ -40,4 +41,5 @@ tests/test_normalize.py
|
|
|
40
41
|
tests/test_row_dedup.py
|
|
41
42
|
tests/test_sample_csv.py
|
|
42
43
|
tests/test_strict_concat.py
|
|
43
|
-
tests/test_string_distance.py
|
|
44
|
+
tests/test_string_distance.py
|
|
45
|
+
tests/test_strip_bom.py
|
|
@@ -190,6 +190,26 @@ def test_cli_parses_find_matches_command():
|
|
|
190
190
|
assert args.no_nfkc is True
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def test_cli_parses_strip_bom_command():
|
|
194
|
+
parser = build_parser()
|
|
195
|
+
args = parser.parse_args(["strip-bom", "input.csv", "-o", "output.csv"])
|
|
196
|
+
|
|
197
|
+
assert args.command == "strip-bom"
|
|
198
|
+
assert args.input == "input.csv"
|
|
199
|
+
assert args.output == "output.csv"
|
|
200
|
+
assert args.in_place is False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_cli_parses_strip_bom_command_in_place():
|
|
204
|
+
parser = build_parser()
|
|
205
|
+
args = parser.parse_args(["strip-bom", "input.csv", "--in-place"])
|
|
206
|
+
|
|
207
|
+
assert args.command == "strip-bom"
|
|
208
|
+
assert args.input == "input.csv"
|
|
209
|
+
assert args.output is None
|
|
210
|
+
assert args.in_place is True
|
|
211
|
+
|
|
212
|
+
|
|
193
213
|
def test_cli_parses_strict_concat_command():
|
|
194
214
|
parser = build_parser()
|
|
195
215
|
args = parser.parse_args(["strict-concat", "input_dir", "-o", "output.csv"])
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from csvsmith.cli import main
|
|
4
|
+
from csvsmith.tools.strip_bom import UTF8_BOM, default_output_path, strip_utf8_bom
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_strip_utf8_bom_writes_default_output_without_bom(tmp_path):
|
|
8
|
+
source = tmp_path / "input.csv"
|
|
9
|
+
source.write_bytes(UTF8_BOM + b"id,name\r\n1,Alice\r\n")
|
|
10
|
+
|
|
11
|
+
result = strip_utf8_bom(source)
|
|
12
|
+
|
|
13
|
+
assert result.output_path == tmp_path / "input.no-bom.csv"
|
|
14
|
+
assert result.removed is True
|
|
15
|
+
assert result.output_path.read_bytes() == b"id,name\r\n1,Alice\r\n"
|
|
16
|
+
assert source.read_bytes().startswith(UTF8_BOM)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_strip_utf8_bom_preserves_file_without_bom(tmp_path):
|
|
20
|
+
source = tmp_path / "input.csv"
|
|
21
|
+
output = tmp_path / "output.csv"
|
|
22
|
+
source.write_bytes(b"id,name\n1,Alice\n")
|
|
23
|
+
|
|
24
|
+
result = strip_utf8_bom(source, output)
|
|
25
|
+
|
|
26
|
+
assert result.output_path == output
|
|
27
|
+
assert result.removed is False
|
|
28
|
+
assert output.read_bytes() == b"id,name\n1,Alice\n"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_strip_utf8_bom_can_rewrite_in_place(tmp_path):
|
|
32
|
+
source = tmp_path / "input.csv"
|
|
33
|
+
source.write_bytes(UTF8_BOM + b"id,name\n1,Alice\n")
|
|
34
|
+
|
|
35
|
+
result = strip_utf8_bom(source, in_place=True)
|
|
36
|
+
|
|
37
|
+
assert result.output_path == source
|
|
38
|
+
assert result.removed is True
|
|
39
|
+
assert source.read_bytes() == b"id,name\n1,Alice\n"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_strip_utf8_bom_rejects_in_place_with_output(tmp_path):
|
|
43
|
+
source = tmp_path / "input.csv"
|
|
44
|
+
source.write_bytes(UTF8_BOM + b"id,name\n")
|
|
45
|
+
|
|
46
|
+
with pytest.raises(ValueError, match="--in-place"):
|
|
47
|
+
strip_utf8_bom(source, tmp_path / "output.csv", in_place=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_default_output_path_handles_multi_dot_names():
|
|
51
|
+
assert default_output_path("report.final.csv").name == "report.final.no-bom.csv"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_strip_bom_cli_writes_requested_output(tmp_path, capsys):
|
|
55
|
+
source = tmp_path / "input.csv"
|
|
56
|
+
output = tmp_path / "clean.csv"
|
|
57
|
+
source.write_bytes(UTF8_BOM + b"id,name\n1,Alice\n")
|
|
58
|
+
|
|
59
|
+
exit_code = main(["strip-bom", str(source), "-o", str(output)])
|
|
60
|
+
|
|
61
|
+
assert exit_code == 0
|
|
62
|
+
assert output.read_bytes() == b"id,name\n1,Alice\n"
|
|
63
|
+
assert "removed BOM" in capsys.readouterr().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
|
|
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
|