minimindslab-csv-normalize 0.1.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.
- minimindslab_csv_normalize-0.1.0/LICENSE +21 -0
- minimindslab_csv_normalize-0.1.0/PKG-INFO +66 -0
- minimindslab_csv_normalize-0.1.0/README.md +46 -0
- minimindslab_csv_normalize-0.1.0/pyproject.toml +33 -0
- minimindslab_csv_normalize-0.1.0/setup.cfg +4 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize/__init__.py +6 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize/__main__.py +34 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize/core.py +60 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize.egg-info/PKG-INFO +66 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize.egg-info/SOURCES.txt +12 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize.egg-info/dependency_links.txt +1 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize.egg-info/entry_points.txt +2 -0
- minimindslab_csv_normalize-0.1.0/src/minimindslab_csv_normalize.egg-info/top_level.txt +1 -0
- minimindslab_csv_normalize-0.1.0/tests/test_normalize.py +75 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MiniMindsLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minimindslab-csv-normalize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Normalize small CSV text into stable, comparison-friendly output with controllable header ordering, trimming, and line endings.
|
|
5
|
+
Author: MiniMindsLab
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: csv,normalize,deterministic,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Classifier: Topic :: Text Processing
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# MiniMindsLab CSV Normalize
|
|
22
|
+
|
|
23
|
+
Deterministically normalize small CSV text for stable diffs, tests, and reviews.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Trims surrounding whitespace in headers and cell values.
|
|
28
|
+
- Normalizes mixed line endings to `\n`.
|
|
29
|
+
- Optional stable header sorting.
|
|
30
|
+
- Deterministic output for identical logical input.
|
|
31
|
+
- Library API and CLI.
|
|
32
|
+
|
|
33
|
+
## Library Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from minimindslab_csv_normalize import normalize_csv
|
|
37
|
+
|
|
38
|
+
raw = " Name , Age \r\n Alice , 30 \r\n"
|
|
39
|
+
out = normalize_csv(raw)
|
|
40
|
+
print(out)
|
|
41
|
+
# Age,Name\n30,Alice\n
|
|
42
|
+
# Keep original header order
|
|
43
|
+
out2 = normalize_csv(raw, sort_headers=False)
|
|
44
|
+
print(out2)
|
|
45
|
+
# Name,Age\nAlice,30\n
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## CLI Usage
|
|
49
|
+
|
|
50
|
+
From stdin:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
echo " Name , Age \r\n Alice , 30 " | miniminds-csv-normalize
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From file:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
miniminds-csv-normalize --input sample.csv
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Disable header sorting:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
miniminds-csv-normalize --no-sort-headers --input sample.csv
|
|
66
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# MiniMindsLab CSV Normalize
|
|
2
|
+
|
|
3
|
+
Deterministically normalize small CSV text for stable diffs, tests, and reviews.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Trims surrounding whitespace in headers and cell values.
|
|
8
|
+
- Normalizes mixed line endings to `\n`.
|
|
9
|
+
- Optional stable header sorting.
|
|
10
|
+
- Deterministic output for identical logical input.
|
|
11
|
+
- Library API and CLI.
|
|
12
|
+
|
|
13
|
+
## Library Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from minimindslab_csv_normalize import normalize_csv
|
|
17
|
+
|
|
18
|
+
raw = " Name , Age \r\n Alice , 30 \r\n"
|
|
19
|
+
out = normalize_csv(raw)
|
|
20
|
+
print(out)
|
|
21
|
+
# Age,Name\n30,Alice\n
|
|
22
|
+
# Keep original header order
|
|
23
|
+
out2 = normalize_csv(raw, sort_headers=False)
|
|
24
|
+
print(out2)
|
|
25
|
+
# Name,Age\nAlice,30\n
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CLI Usage
|
|
29
|
+
|
|
30
|
+
From stdin:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
echo " Name , Age \r\n Alice , 30 " | miniminds-csv-normalize
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
From file:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
miniminds-csv-normalize --input sample.csv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Disable header sorting:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
miniminds-csv-normalize --no-sort-headers --input sample.csv
|
|
46
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0.3"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "minimindslab-csv-normalize"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Normalize small CSV text into stable, comparison-friendly output with controllable header ordering, trimming, and line endings."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "MiniMindsLab" }]
|
|
13
|
+
dependencies = []
|
|
14
|
+
keywords = ["csv", "normalize", "deterministic", "cli"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Testing",
|
|
23
|
+
"Topic :: Text Processing"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.scripts]
|
|
27
|
+
miniminds-csv-normalize = "minimindslab_csv_normalize.__main__:main"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools]
|
|
30
|
+
package-dir = {"" = "src"}
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
where = ["src"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from .core import normalize_csv
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
10
|
+
parser = argparse.ArgumentParser(description="Normalize small CSV text deterministically.")
|
|
11
|
+
parser.add_argument("--input", "-i", help="Path to input CSV file. If omitted, read from stdin.")
|
|
12
|
+
parser.add_argument("--no-sort-headers", action="store_true", help="Preserve original header order.")
|
|
13
|
+
parser.add_argument("--newline", choices=["lf", "crlf"], default="lf", help="Output newline style.")
|
|
14
|
+
return parser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
parser = build_parser()
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.input:
|
|
22
|
+
with open(args.input, "r", encoding="utf-8", newline="") as f:
|
|
23
|
+
text = f.read()
|
|
24
|
+
else:
|
|
25
|
+
text = sys.stdin.read()
|
|
26
|
+
|
|
27
|
+
nl = "\n" if args.newline == "lf" else "\r\n"
|
|
28
|
+
result = normalize_csv(text, sort_headers=not args.no_sort_headers, newline=nl)
|
|
29
|
+
sys.stdout.write(result)
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Core CSV normalization logic."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _normalize_newlines(text: str) -> str:
|
|
11
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def normalize_csv(text: str, *, sort_headers: bool = True, newline: str = "\n") -> str:
|
|
15
|
+
"""Normalize CSV text into stable output.
|
|
16
|
+
|
|
17
|
+
- Normalizes line endings in input.
|
|
18
|
+
- Trims leading/trailing whitespace for all cells.
|
|
19
|
+
- Optionally sorts headers alphabetically for deterministic column order.
|
|
20
|
+
- Emits output with a consistent newline and trailing newline when non-empty.
|
|
21
|
+
"""
|
|
22
|
+
if not isinstance(text, str):
|
|
23
|
+
raise TypeError("text must be a str")
|
|
24
|
+
if newline not in {"\n", "\r\n"}:
|
|
25
|
+
raise ValueError("newline must be '\\n' or '\\r\\n'")
|
|
26
|
+
|
|
27
|
+
src = _normalize_newlines(text)
|
|
28
|
+
if src.strip() == "":
|
|
29
|
+
return ""
|
|
30
|
+
|
|
31
|
+
reader = csv.reader(io.StringIO(src))
|
|
32
|
+
rows: List[List[str]] = [[cell.strip() for cell in row] for row in reader]
|
|
33
|
+
if not rows:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
header = rows[0]
|
|
37
|
+
data = rows[1:]
|
|
38
|
+
|
|
39
|
+
if sort_headers:
|
|
40
|
+
indices = sorted(range(len(header)), key=lambda i: (header[i], i))
|
|
41
|
+
header = [header[i] for i in indices]
|
|
42
|
+
|
|
43
|
+
def reorder(row: List[str]) -> List[str]:
|
|
44
|
+
padded = row + [""] * (len(indices) - len(row))
|
|
45
|
+
return [padded[i] if i < len(padded) else "" for i in indices]
|
|
46
|
+
|
|
47
|
+
data = [reorder(r) for r in data]
|
|
48
|
+
else:
|
|
49
|
+
data = [r + [""] * (len(header) - len(r)) if len(r) < len(header) else r[: len(header)] for r in data]
|
|
50
|
+
|
|
51
|
+
output = io.StringIO()
|
|
52
|
+
writer = csv.writer(output, lineterminator="\n")
|
|
53
|
+
writer.writerow(header)
|
|
54
|
+
for row in data:
|
|
55
|
+
writer.writerow(row)
|
|
56
|
+
|
|
57
|
+
rendered = output.getvalue()
|
|
58
|
+
if newline != "\n":
|
|
59
|
+
rendered = rendered.replace("\n", newline)
|
|
60
|
+
return rendered
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: minimindslab-csv-normalize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Normalize small CSV text into stable, comparison-friendly output with controllable header ordering, trimming, and line endings.
|
|
5
|
+
Author: MiniMindsLab
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: csv,normalize,deterministic,cli
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Classifier: Topic :: Text Processing
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# MiniMindsLab CSV Normalize
|
|
22
|
+
|
|
23
|
+
Deterministically normalize small CSV text for stable diffs, tests, and reviews.
|
|
24
|
+
|
|
25
|
+
## Features
|
|
26
|
+
|
|
27
|
+
- Trims surrounding whitespace in headers and cell values.
|
|
28
|
+
- Normalizes mixed line endings to `\n`.
|
|
29
|
+
- Optional stable header sorting.
|
|
30
|
+
- Deterministic output for identical logical input.
|
|
31
|
+
- Library API and CLI.
|
|
32
|
+
|
|
33
|
+
## Library Usage
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from minimindslab_csv_normalize import normalize_csv
|
|
37
|
+
|
|
38
|
+
raw = " Name , Age \r\n Alice , 30 \r\n"
|
|
39
|
+
out = normalize_csv(raw)
|
|
40
|
+
print(out)
|
|
41
|
+
# Age,Name\n30,Alice\n
|
|
42
|
+
# Keep original header order
|
|
43
|
+
out2 = normalize_csv(raw, sort_headers=False)
|
|
44
|
+
print(out2)
|
|
45
|
+
# Name,Age\nAlice,30\n
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## CLI Usage
|
|
49
|
+
|
|
50
|
+
From stdin:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
echo " Name , Age \r\n Alice , 30 " | miniminds-csv-normalize
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From file:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
miniminds-csv-normalize --input sample.csv
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Disable header sorting:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
miniminds-csv-normalize --no-sort-headers --input sample.csv
|
|
66
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/minimindslab_csv_normalize/__init__.py
|
|
5
|
+
src/minimindslab_csv_normalize/__main__.py
|
|
6
|
+
src/minimindslab_csv_normalize/core.py
|
|
7
|
+
src/minimindslab_csv_normalize.egg-info/PKG-INFO
|
|
8
|
+
src/minimindslab_csv_normalize.egg-info/SOURCES.txt
|
|
9
|
+
src/minimindslab_csv_normalize.egg-info/dependency_links.txt
|
|
10
|
+
src/minimindslab_csv_normalize.egg-info/entry_points.txt
|
|
11
|
+
src/minimindslab_csv_normalize.egg-info/top_level.txt
|
|
12
|
+
tests/test_normalize.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
minimindslab_csv_normalize
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from minimindslab_csv_normalize import normalize_csv
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestNormalizeCSV(unittest.TestCase):
|
|
10
|
+
def test_trim_and_stable_form(self):
|
|
11
|
+
text = " Name , Age \n Alice , 30 \n"
|
|
12
|
+
got = normalize_csv(text)
|
|
13
|
+
self.assertEqual(got, "Age,Name\n30,Alice\n")
|
|
14
|
+
|
|
15
|
+
def test_mixed_newlines_deterministic(self):
|
|
16
|
+
text = "b,a\r\n2,1\r3,4\n"
|
|
17
|
+
got = normalize_csv(text)
|
|
18
|
+
self.assertEqual(got, "a,b\n1,2\n4,3\n")
|
|
19
|
+
|
|
20
|
+
def test_idempotent(self):
|
|
21
|
+
text = " z , y \n 1 , 2 \n"
|
|
22
|
+
a = normalize_csv(text)
|
|
23
|
+
b = normalize_csv(text)
|
|
24
|
+
self.assertEqual(a, b)
|
|
25
|
+
|
|
26
|
+
def test_negative_newline_option(self):
|
|
27
|
+
with self.assertRaises(ValueError):
|
|
28
|
+
normalize_csv("a,b\n1,2\n", newline="x")
|
|
29
|
+
|
|
30
|
+
def test_negative_type(self):
|
|
31
|
+
with self.assertRaises(TypeError):
|
|
32
|
+
normalize_csv(None) # type: ignore[arg-type]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TestCLI(unittest.TestCase):
|
|
36
|
+
def _run_main(self, args, input_text=None):
|
|
37
|
+
old_argv = os.sys.argv
|
|
38
|
+
old_stdin = os.sys.stdin
|
|
39
|
+
old_stdout = os.sys.stdout
|
|
40
|
+
try:
|
|
41
|
+
mod = importlib.import_module("minimindslab_csv_normalize.__main__")
|
|
42
|
+
import io
|
|
43
|
+
|
|
44
|
+
os.sys.argv = ["miniminds-csv-normalize", *args]
|
|
45
|
+
os.sys.stdin = io.StringIO(input_text if input_text is not None else "")
|
|
46
|
+
captured = io.StringIO()
|
|
47
|
+
os.sys.stdout = captured
|
|
48
|
+
code = mod.main()
|
|
49
|
+
return code, captured.getvalue()
|
|
50
|
+
finally:
|
|
51
|
+
os.sys.argv = old_argv
|
|
52
|
+
os.sys.stdin = old_stdin
|
|
53
|
+
os.sys.stdout = old_stdout
|
|
54
|
+
|
|
55
|
+
def test_cli_stdin_matches_library(self):
|
|
56
|
+
text = " Name , Age \r\n Alice , 30 \n"
|
|
57
|
+
code, out = self._run_main([], input_text=text)
|
|
58
|
+
self.assertEqual(code, 0)
|
|
59
|
+
self.assertEqual(out, normalize_csv(text))
|
|
60
|
+
|
|
61
|
+
def test_cli_file_matches_library(self):
|
|
62
|
+
text = "b,a\n2,1\n"
|
|
63
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp:
|
|
64
|
+
tmp.write(text)
|
|
65
|
+
path = tmp.name
|
|
66
|
+
try:
|
|
67
|
+
code, out = self._run_main(["--input", path])
|
|
68
|
+
self.assertEqual(code, 0)
|
|
69
|
+
self.assertEqual(out, normalize_csv(text))
|
|
70
|
+
finally:
|
|
71
|
+
os.unlink(path)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
unittest.main()
|