csvsmith 0.7.3__tar.gz → 0.9.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.
Files changed (39) hide show
  1. {csvsmith-0.7.3/src/csvsmith.egg-info → csvsmith-0.9.0}/PKG-INFO +61 -4
  2. {csvsmith-0.7.3 → csvsmith-0.9.0}/README.rst +61 -4
  3. {csvsmith-0.7.3 → csvsmith-0.9.0}/pyproject.toml +10 -2
  4. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/__init__.py +19 -1
  5. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/cli.py +136 -1
  6. csvsmith-0.9.0/src/csvsmith/tools/dense_csv.py +348 -0
  7. csvsmith-0.9.0/src/csvsmith/tools/strict_concat.py +101 -0
  8. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/utils/clean_numeric.py +1 -1
  9. {csvsmith-0.7.3 → csvsmith-0.9.0/src/csvsmith.egg-info}/PKG-INFO +61 -4
  10. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith.egg-info/SOURCES.txt +4 -0
  11. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_cli.py +64 -1
  12. csvsmith-0.9.0/tests/test_dense_csv.py +333 -0
  13. csvsmith-0.9.0/tests/test_strict_concat.py +102 -0
  14. {csvsmith-0.7.3 → csvsmith-0.9.0}/LICENSE +0 -0
  15. {csvsmith-0.7.3 → csvsmith-0.9.0}/setup.cfg +0 -0
  16. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/__init__.py +0 -0
  17. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/classify.py +0 -0
  18. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/excel2csv.py +0 -0
  19. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/filter_rows.py +0 -0
  20. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/find_matches_in_csv.py +0 -0
  21. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/move_files.py +0 -0
  22. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/tools/row_dedup.py +0 -0
  23. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/utils/__init__.py +0 -0
  24. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/utils/distance.py +0 -0
  25. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/utils/io.py +0 -0
  26. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith/utils/normalize.py +0 -0
  27. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith.egg-info/dependency_links.txt +0 -0
  28. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith.egg-info/entry_points.txt +0 -0
  29. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith.egg-info/requires.txt +0 -0
  30. {csvsmith-0.7.3 → csvsmith-0.9.0}/src/csvsmith.egg-info/top_level.txt +0 -0
  31. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_classify.py +0 -0
  32. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_clean_numeric.py +0 -0
  33. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_excel2csv.py +0 -0
  34. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_filter_rows.py +0 -0
  35. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_find_matches_in_csv.py +0 -0
  36. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_move_files.py +0 -0
  37. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_normalize.py +0 -0
  38. {csvsmith-0.7.3 → csvsmith-0.9.0}/tests/test_row_dedup.py +0 -0
  39. {csvsmith-0.7.3 → csvsmith-0.9.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.7.3
3
+ Version: 0.9.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,8 @@ 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
91
+ - Tokenize repeated CSV values and restore them with a versioned map
90
92
  - Use the tools either from Python or from the command line
91
93
 
92
94
  Installation
@@ -168,7 +170,7 @@ Convert Excel to CSV:
168
170
 
169
171
  .. code-block:: console
170
172
 
171
- csvsmith excel2csv input.xlsx
173
+ csvsmith excel-to-csv input.xlsx
172
174
 
173
175
  Move files by suffix:
174
176
 
@@ -182,6 +184,54 @@ Find matches in a CSV:
182
184
 
183
185
  csvsmith find-matches input.csv target --ignore-case --ignore-whitespace
184
186
 
187
+ Concatenate CSV files:
188
+
189
+ .. code-block:: console
190
+
191
+ csvsmith strict-concat incoming/ -o combined.csv
192
+
193
+ Concentrate repeated values and restore them later:
194
+
195
+ .. code-block:: console
196
+
197
+ csvsmith concentrate input.csv
198
+ csvsmith rehydrate input.dense.csv -m input.dense-map.json -o restored.csv
199
+
200
+ Dense CSV scope and performance
201
+ -------------------------------
202
+
203
+ The dense CSV workflow is intended for spreadsheet-oriented and medium-sized
204
+ CSV files, roughly in the 100 MB class. Concentration makes two passes over the
205
+ input and keeps value counts in memory, so memory use depends on the number and
206
+ size of distinct values in the selected columns.
207
+
208
+ For substantially larger datasets, especially gigabyte-scale pipelines, a
209
+ flat CSV plus a separate JSON map is usually the wrong interchange format.
210
+ Consider a binary columnar format such as Apache Parquet instead.
211
+
212
+ Dense CSV replaces repeated values with tokens containing a 64-character
213
+ SHA-256 digest. The complete token includes the ``csvsmith:sha256:`` prefix and
214
+ is therefore 79 ASCII characters. The JSON map also stores each original value.
215
+
216
+ The workflow can still reduce expensive downstream work even when the files
217
+ become larger. A consumer can process each mapped value once, store the result
218
+ against its token, and apply that result to every repeated occurrence during
219
+ rehydration. The CLI reports this potential repeated-operation reduction among
220
+ mapped cells.
221
+
222
+ For example, 937 mapped cell occurrences backed by 82 unique map values imply
223
+ 855 avoidable repeated operations, or about 91.2%. This is a deduplication
224
+ ratio for mapped work, not a file-compression ratio or a guarantee of total
225
+ pipeline savings.
226
+
227
+ .. warning::
228
+
229
+ Concentration does not guarantee a smaller combined output. Replacing short
230
+ values such as ``True``, ``M``, or ``NY`` with a 79-character token will
231
+ increase the CSV size. Select columns containing sufficiently long,
232
+ repeated values and consider both the concentrated CSV and its JSON map
233
+ when evaluating storage savings.
234
+
185
235
  Find matches in a CSV
186
236
  ---------------------
187
237
 
@@ -235,13 +285,20 @@ CSV classification and filtering:
235
285
 
236
286
  .. code-block:: python
237
287
 
238
- from csvsmith import CSVClassifier, DropRowsBySubstring, CSVCleaner
288
+ from csvsmith import CSVClassifier, DropRowsBySubstring
239
289
 
240
290
  File and conversion helpers:
241
291
 
242
292
  .. code-block:: python
243
293
 
244
- from csvsmith import excel_to_csv, move_by_suffix
294
+ from csvsmith import (
295
+ concentrate_csv,
296
+ excel_to_csv,
297
+ move_by_suffix,
298
+ rehydrate_csv,
299
+ save_csv,
300
+ strict_concat_rows,
301
+ )
245
302
 
246
303
  String comparison utilities:
247
304
 
@@ -41,6 +41,8 @@ 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
45
+ - Tokenize repeated CSV values and restore them with a versioned map
44
46
  - Use the tools either from Python or from the command line
45
47
 
46
48
  Installation
@@ -122,7 +124,7 @@ Convert Excel to CSV:
122
124
 
123
125
  .. code-block:: console
124
126
 
125
- csvsmith excel2csv input.xlsx
127
+ csvsmith excel-to-csv input.xlsx
126
128
 
127
129
  Move files by suffix:
128
130
 
@@ -136,6 +138,54 @@ Find matches in a CSV:
136
138
 
137
139
  csvsmith find-matches input.csv target --ignore-case --ignore-whitespace
138
140
 
141
+ Concatenate CSV files:
142
+
143
+ .. code-block:: console
144
+
145
+ csvsmith strict-concat incoming/ -o combined.csv
146
+
147
+ Concentrate repeated values and restore them later:
148
+
149
+ .. code-block:: console
150
+
151
+ csvsmith concentrate input.csv
152
+ csvsmith rehydrate input.dense.csv -m input.dense-map.json -o restored.csv
153
+
154
+ Dense CSV scope and performance
155
+ -------------------------------
156
+
157
+ The dense CSV workflow is intended for spreadsheet-oriented and medium-sized
158
+ CSV files, roughly in the 100 MB class. Concentration makes two passes over the
159
+ input and keeps value counts in memory, so memory use depends on the number and
160
+ size of distinct values in the selected columns.
161
+
162
+ For substantially larger datasets, especially gigabyte-scale pipelines, a
163
+ flat CSV plus a separate JSON map is usually the wrong interchange format.
164
+ Consider a binary columnar format such as Apache Parquet instead.
165
+
166
+ Dense CSV replaces repeated values with tokens containing a 64-character
167
+ SHA-256 digest. The complete token includes the ``csvsmith:sha256:`` prefix and
168
+ is therefore 79 ASCII characters. The JSON map also stores each original value.
169
+
170
+ The workflow can still reduce expensive downstream work even when the files
171
+ become larger. A consumer can process each mapped value once, store the result
172
+ against its token, and apply that result to every repeated occurrence during
173
+ rehydration. The CLI reports this potential repeated-operation reduction among
174
+ mapped cells.
175
+
176
+ For example, 937 mapped cell occurrences backed by 82 unique map values imply
177
+ 855 avoidable repeated operations, or about 91.2%. This is a deduplication
178
+ ratio for mapped work, not a file-compression ratio or a guarantee of total
179
+ pipeline savings.
180
+
181
+ .. warning::
182
+
183
+ Concentration does not guarantee a smaller combined output. Replacing short
184
+ values such as ``True``, ``M``, or ``NY`` with a 79-character token will
185
+ increase the CSV size. Select columns containing sufficiently long,
186
+ repeated values and consider both the concentrated CSV and its JSON map
187
+ when evaluating storage savings.
188
+
139
189
  Find matches in a CSV
140
190
  ---------------------
141
191
 
@@ -189,13 +239,20 @@ CSV classification and filtering:
189
239
 
190
240
  .. code-block:: python
191
241
 
192
- from csvsmith import CSVClassifier, DropRowsBySubstring, CSVCleaner
242
+ from csvsmith import CSVClassifier, DropRowsBySubstring
193
243
 
194
244
  File and conversion helpers:
195
245
 
196
246
  .. code-block:: python
197
247
 
198
- from csvsmith import excel_to_csv, move_by_suffix
248
+ from csvsmith import (
249
+ concentrate_csv,
250
+ excel_to_csv,
251
+ move_by_suffix,
252
+ rehydrate_csv,
253
+ save_csv,
254
+ strict_concat_rows,
255
+ )
199
256
 
200
257
  String comparison utilities:
201
258
 
@@ -225,4 +282,4 @@ Example:
225
282
  License
226
283
  -------
227
284
 
228
- See the project license for details.
285
+ See the project license for details.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "csvsmith"
7
- version = "0.7.3"
7
+ version = "0.9.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"
@@ -50,4 +50,12 @@ Documentation = "https://csvsmith.readthedocs.io/en/latest/"
50
50
  csvsmith = "csvsmith.cli:main"
51
51
 
52
52
  [tool.setuptools.packages.find]
53
- where = ["src"]
53
+ where = ["src"]
54
+
55
+ [tool.semantic_release]
56
+ allow_zero_version = true
57
+ tag_format = "v{version}"
58
+ version_toml = ["pyproject.toml:project.version"]
59
+
60
+ [tool.semantic_release.changelog.default_templates]
61
+ changelog_file = "CHANGELOG.rst"
@@ -16,6 +16,10 @@ Public API:
16
16
  - Relation
17
17
  - Result
18
18
  - analyze_pair
19
+ - strict_concat_rows
20
+ - save_csv
21
+ - concentrate_csv
22
+ - rehydrate_csv
19
23
 
20
24
  Compatibility aliases:
21
25
  - CSVCleaner
@@ -28,12 +32,19 @@ Submodules:
28
32
  - csvsmith.filter_rows
29
33
  - csvsmith.excel2csv
30
34
  - csvsmith.move_files
35
+ - csvsmith.strict_concat
31
36
  - csvsmith.cli (CLI entrypoint)
32
37
  """
33
38
 
34
- __version__ = "0.7.3"
39
+ __version__ = "0.8.0"
35
40
 
36
41
  from .tools.classify import CSVClassifier
42
+ from .tools.dense_csv import (
43
+ ConcentrateResult,
44
+ RehydrateResult,
45
+ concentrate_csv,
46
+ rehydrate_csv,
47
+ )
37
48
  from .tools.excel2csv import excel_to_csv
38
49
  from .tools.filter_rows import DropRowsBySubstring
39
50
  from .tools.find_matches_in_csv import find_matches_in_csv
@@ -43,6 +54,7 @@ from .tools.row_dedup import (
43
54
  find_duplicate_rows,
44
55
  dedupe_with_report,
45
56
  )
57
+ from .tools.strict_concat import save_csv, strict_concat_rows
46
58
  from .utils.clean_numeric import clean_numeric
47
59
  from .utils.distance import StringDistance, Relation, Result, analyze_pair
48
60
  from .utils.io import (
@@ -68,4 +80,10 @@ __all__ = [
68
80
  "Result",
69
81
  "analyze_pair",
70
82
  "clean_numeric",
83
+ "strict_concat_rows",
84
+ "save_csv",
85
+ "ConcentrateResult",
86
+ "RehydrateResult",
87
+ "concentrate_csv",
88
+ "rehydrate_csv",
71
89
  ]
@@ -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.dense_csv import concentrate_csv, rehydrate_csv
11
12
  from .tools.excel2csv import excel_to_csv
12
13
  from .utils.clean_numeric import clean_currency_numeric, clean_numeric
13
14
  from .tools.filter_rows import DropRowsBySubstring
@@ -16,6 +17,7 @@ from .tools.row_dedup import (
16
17
  dedupe_with_report,
17
18
  find_duplicate_rows,
18
19
  )
20
+ from .tools.strict_concat import save_csv, strict_concat_rows
19
21
  from .tools.find_matches_in_csv import find_matches_in_csv
20
22
  from .utils.distance import analyze_pair
21
23
  from .utils.io import read_csv_rows, write_csv_rows
@@ -180,6 +182,87 @@ def cmd_find_matches(args: argparse.Namespace) -> int:
180
182
  return 0
181
183
 
182
184
 
185
+ def cmd_strict_concat(args: argparse.Namespace) -> int:
186
+ input_dir = Path(args.input_dir)
187
+ if not input_dir.is_dir():
188
+ print(f"Error: input directory not found: {input_dir}", file=sys.stderr)
189
+ return 1
190
+
191
+ try:
192
+ rows = strict_concat_rows(input_dir)
193
+ save_csv(rows, args.output)
194
+ except Exception as e:
195
+ print(f"Error: {e}", file=sys.stderr)
196
+ return 1
197
+
198
+ print(f"Wrote concatenated CSV to: {args.output}")
199
+ return 0
200
+
201
+
202
+ def cmd_concentrate(args: argparse.Namespace) -> int:
203
+ input_path = Path(args.input)
204
+ output_path = (
205
+ Path(args.output)
206
+ if args.output
207
+ else input_path.with_name(f"{input_path.stem}.dense.csv")
208
+ )
209
+ map_path = (
210
+ Path(args.map)
211
+ if args.map
212
+ else input_path.with_name(f"{input_path.stem}.dense-map.json")
213
+ )
214
+ columns = (
215
+ [column.strip() for column in args.columns.split(",") if column.strip()]
216
+ if args.columns
217
+ else None
218
+ )
219
+ try:
220
+ result = concentrate_csv(
221
+ input_path,
222
+ output_path,
223
+ map_path,
224
+ columns=columns,
225
+ min_occurrences=args.min_occurrences,
226
+ )
227
+ except (OSError, ValueError, csv.Error, json.JSONDecodeError) as e:
228
+ print(f"Error: {e}", file=sys.stderr)
229
+ return 1
230
+
231
+ print(
232
+ f"Wrote concentrated CSV to: {result.output_csv_path} "
233
+ f"({result.transformed_cell_count} cells, "
234
+ f"{result.mapped_value_count} mapped values)"
235
+ )
236
+ if result.transformed_cell_count:
237
+ avoided_operations = (
238
+ result.transformed_cell_count - result.mapped_value_count
239
+ )
240
+ reduction_percentage = (
241
+ avoided_operations / result.transformed_cell_count * 100
242
+ )
243
+ print(
244
+ "Potential repeated-operation reduction for mapped values: "
245
+ f"{avoided_operations}/{result.transformed_cell_count} "
246
+ f"({reduction_percentage:.1f}%)"
247
+ )
248
+ print(f"Wrote dense CSV map to: {result.output_map_path}")
249
+ return 0
250
+
251
+
252
+ def cmd_rehydrate(args: argparse.Namespace) -> int:
253
+ try:
254
+ result = rehydrate_csv(args.input, args.map, args.output)
255
+ except (OSError, ValueError, csv.Error, json.JSONDecodeError) as e:
256
+ print(f"Error: {e}", file=sys.stderr)
257
+ return 1
258
+
259
+ print(
260
+ f"Wrote rehydrated CSV to: {result.output_csv_path} "
261
+ f"({result.restored_cell_count} cells restored)"
262
+ )
263
+ return 0
264
+
265
+
183
266
  def _add_find_matches_parser(subparsers) -> None:
184
267
  parser = subparsers.add_parser("find-matches", help="Find matches in a CSV file.")
185
268
  parser.add_argument("input", help="Input CSV file.")
@@ -190,6 +273,56 @@ def _add_find_matches_parser(subparsers) -> None:
190
273
  parser.set_defaults(func=cmd_find_matches)
191
274
 
192
275
 
276
+ def _add_strict_concat_parser(subparsers) -> None:
277
+ parser = subparsers.add_parser(
278
+ "strict-concat",
279
+ help="Concatenate CSVs in a directory only when all headers match exactly.",
280
+ )
281
+ parser.add_argument("input_dir", help="Directory containing CSV files to concatenate.")
282
+ parser.add_argument("-o", "--output", required=True, help="Output CSV file path.")
283
+ parser.set_defaults(func=cmd_strict_concat)
284
+
285
+
286
+ def _add_concentrate_parser(subparsers) -> None:
287
+ parser = subparsers.add_parser(
288
+ "concentrate",
289
+ help="Replace repeated CSV values with deterministic tokens.",
290
+ )
291
+ parser.add_argument("input", help="Input CSV file.")
292
+ parser.add_argument(
293
+ "-o",
294
+ "--output",
295
+ help="Output CSV file (default: <input-stem>.dense.csv).",
296
+ )
297
+ parser.add_argument(
298
+ "-m",
299
+ "--map",
300
+ help="Output JSON map file (default: <input-stem>.dense-map.json).",
301
+ )
302
+ parser.add_argument(
303
+ "--columns",
304
+ help="Comma-separated column names to concentrate (default: all columns).",
305
+ )
306
+ parser.add_argument(
307
+ "--min-occurrences",
308
+ type=int,
309
+ default=2,
310
+ help="Minimum repetitions required before replacing a value (default: 2).",
311
+ )
312
+ parser.set_defaults(func=cmd_concentrate)
313
+
314
+
315
+ def _add_rehydrate_parser(subparsers) -> None:
316
+ parser = subparsers.add_parser(
317
+ "rehydrate",
318
+ help="Restore a concentrated CSV using its JSON map.",
319
+ )
320
+ parser.add_argument("input", help="Concentrated CSV file.")
321
+ parser.add_argument("-m", "--map", required=True, help="Dense CSV JSON map file.")
322
+ parser.add_argument("-o", "--output", required=True, help="Output CSV file.")
323
+ parser.set_defaults(func=cmd_rehydrate)
324
+
325
+
193
326
  def _add_row_duplicates_parser(subparsers) -> None:
194
327
  parser = subparsers.add_parser("row-duplicates", help="Find duplicate rows in a CSV.")
195
328
  parser.add_argument("input", help="Input CSV file.")
@@ -317,6 +450,9 @@ def build_parser() -> argparse.ArgumentParser:
317
450
 
318
451
  _add_row_duplicates_parser(subparsers)
319
452
  _add_find_matches_parser(subparsers)
453
+ _add_strict_concat_parser(subparsers)
454
+ _add_concentrate_parser(subparsers)
455
+ _add_rehydrate_parser(subparsers)
320
456
  _add_dedupe_parser(subparsers)
321
457
  _add_classify_parser(subparsers)
322
458
  _add_move_files_parser(subparsers)
@@ -342,4 +478,3 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
342
478
 
343
479
  if __name__ == "__main__":
344
480
  sys.exit(main())
345
-