kryptorious-csvclean 1.0.0__py3-none-any.whl

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.
csvclean/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """CSVClean — CSV file cleaner and validator."""
2
+ __version__ = "1.0.0"
csvclean/cleaner.py ADDED
@@ -0,0 +1,260 @@
1
+ """CSVClean — CSV analysis and cleaning engine."""
2
+
3
+ import csv
4
+ import io
5
+ import os
6
+ from collections import Counter
7
+ from pathlib import Path
8
+ from typing import List, Dict, Any, Optional, Tuple
9
+
10
+
11
+ def analyze_csv(path: str) -> Dict[str, Any]:
12
+ """Analyze a CSV file and return diagnostic info.
13
+
14
+ Detects: encoding, delimiter, quote char, header issues,
15
+ empty rows, inconsistent column counts, type patterns, duplicates.
16
+ """
17
+ filepath = Path(path)
18
+ if not filepath.exists():
19
+ raise FileNotFoundError(f"File not found: {path}")
20
+
21
+ size = filepath.stat().st_size
22
+ raw = filepath.read_bytes()
23
+
24
+ # Detect encoding
25
+ encoding = _detect_encoding(raw)
26
+
27
+ # Read with detected encoding
28
+ text = raw.decode(encoding, errors="replace")
29
+ lines = text.splitlines()
30
+
31
+ if not lines:
32
+ return {"error": "File is empty", "rows": 0, "columns": 0}
33
+
34
+ # Detect delimiter
35
+ delimiter = _detect_delimiter(lines)
36
+
37
+ # Parse with csv module
38
+ reader = csv.reader(io.StringIO(text), delimiter=delimiter)
39
+ rows = list(reader)
40
+
41
+ if not rows:
42
+ return {"error": "No data rows found", "rows": 0, "columns": 0}
43
+
44
+ header = rows[0]
45
+ data_rows = rows[1:]
46
+
47
+ # Build result
48
+ result = {
49
+ "path": str(filepath),
50
+ "size_bytes": size,
51
+ "size_human": _format_size(size),
52
+ "encoding": encoding,
53
+ "delimiter": delimiter if delimiter != "," else "comma",
54
+ "total_rows": len(data_rows),
55
+ "total_columns": len(header),
56
+ "headers": header,
57
+ "issues": [],
58
+ }
59
+
60
+ # Check for empty headers
61
+ empty_headers = [i for i, h in enumerate(header) if not h.strip()]
62
+ if empty_headers:
63
+ result["issues"].append({
64
+ "severity": "error",
65
+ "message": f"Empty header at column(s): {', '.join(str(i+1) for i in empty_headers)}"
66
+ })
67
+
68
+ # Check for duplicate headers
69
+ seen = set()
70
+ dupes = []
71
+ for h in header:
72
+ clean = h.strip().lower()
73
+ if clean in seen:
74
+ dupes.append(h)
75
+ seen.add(clean)
76
+ if dupes:
77
+ result["issues"].append({
78
+ "severity": "warning",
79
+ "message": f"Duplicate headers: {', '.join(dupes)}"
80
+ })
81
+
82
+ # Check rows
83
+ empty_rows = 0
84
+ short_rows = 0
85
+ long_rows = 0
86
+ type_patterns = {i: Counter() for i in range(len(header))}
87
+
88
+ for i, row in enumerate(data_rows, start=2):
89
+ # Empty row
90
+ if all(not cell.strip() for cell in row):
91
+ empty_rows += 1
92
+ continue
93
+
94
+ # Column count mismatch
95
+ if len(row) < len(header):
96
+ short_rows += 1
97
+ elif len(row) > len(header):
98
+ long_rows += 1
99
+
100
+ # Type inference per column
101
+ for j, cell in enumerate(row):
102
+ if j >= len(header):
103
+ continue
104
+ val = cell.strip()
105
+ if not val:
106
+ type_patterns[j]["empty"] += 1
107
+ elif val.replace(".", "").replace("-", "").isdigit():
108
+ type_patterns[j]["int" if "." not in val and "-" not in val else "float"] += 1
109
+ elif val.lower() in ("true", "false", "yes", "no"):
110
+ type_patterns[j]["bool"] += 1
111
+ else:
112
+ type_patterns[j]["string"] += 1
113
+
114
+ result["empty_rows"] = empty_rows
115
+ result["column_count_issues"] = short_rows + long_rows
116
+
117
+ if empty_rows:
118
+ result["issues"].append({
119
+ "severity": "warning",
120
+ "message": f"{empty_rows} empty row(s)"
121
+ })
122
+ if short_rows:
123
+ result["issues"].append({
124
+ "severity": "error",
125
+ "message": f"{short_rows} row(s) with fewer columns than header"
126
+ })
127
+ if long_rows:
128
+ result["issues"].append({
129
+ "severity": "warning",
130
+ "message": f"{long_rows} row(s) with more columns than header"
131
+ })
132
+
133
+ # Infer types
134
+ result["column_types"] = {}
135
+ for j, counter in type_patterns.items():
136
+ if j < len(header):
137
+ col_name = header[j].strip() or f"column_{j}"
138
+ dominant = counter.most_common(1)[0][0] if counter else "empty"
139
+ result["column_types"][col_name] = {
140
+ "dominant_type": dominant,
141
+ "distribution": dict(counter.most_common()),
142
+ "empty_count": counter.get("empty", 0),
143
+ "empty_pct": round(counter.get("empty", 0) / max(len(data_rows), 1) * 100, 1),
144
+ }
145
+
146
+ # Duplicate detection
147
+ seen_rows = set()
148
+ duplicate_count = 0
149
+ for row in data_rows:
150
+ row_key = tuple(cell.strip() for cell in row)
151
+ if row_key in seen_rows:
152
+ duplicate_count += 1
153
+ seen_rows.add(row_key)
154
+
155
+ result["duplicate_rows"] = duplicate_count
156
+ if duplicate_count:
157
+ result["issues"].append({
158
+ "severity": "warning",
159
+ "message": f"{duplicate_count} duplicate row(s)"
160
+ })
161
+
162
+ # Health score
163
+ max_score = 100
164
+ penalties = 0
165
+ for issue in result["issues"]:
166
+ if issue["severity"] == "error":
167
+ penalties += 15
168
+ else:
169
+ penalties += 5
170
+ penalties += duplicate_count * 2
171
+ result["health_score"] = max(0, max_score - penalties)
172
+
173
+ return result
174
+
175
+
176
+ def clean_csv(path: str, output: str, fix_encoding: bool = True,
177
+ remove_empty: bool = True, normalize_delimiter: bool = True) -> Dict:
178
+ """Clean a CSV file (premium feature stub)."""
179
+ result = analyze_csv(path)
180
+
181
+ # This is a stub — full cleaning is premium
182
+ issues = []
183
+ if fix_encoding:
184
+ issues.append("Would fix encoding consistency")
185
+ if remove_empty:
186
+ issues.append(f"Would remove {result.get('empty_rows', 0)} empty rows")
187
+ if normalize_delimiter:
188
+ issues.append("Would normalize delimiter")
189
+
190
+ return {
191
+ "source": path,
192
+ "output": output,
193
+ "would_fix": issues,
194
+ "premium_feature": True,
195
+ }
196
+
197
+
198
+ def _detect_encoding(raw: bytes) -> str:
199
+ """Detect file encoding from BOM or content."""
200
+ if raw.startswith(b"\xff\xfe"):
201
+ return "utf-16-le"
202
+ if raw.startswith(b"\xfe\xff"):
203
+ return "utf-16-be"
204
+ if raw.startswith(b"\xef\xbb\xbf"):
205
+ return "utf-8-sig"
206
+ try:
207
+ raw.decode("utf-8")
208
+ return "utf-8"
209
+ except UnicodeDecodeError:
210
+ try:
211
+ raw.decode("latin-1")
212
+ return "latin-1"
213
+ except UnicodeDecodeError:
214
+ return "utf-8"
215
+
216
+
217
+ def _detect_delimiter(lines: List[str]) -> str:
218
+ """Detect CSV delimiter from content."""
219
+ if not lines:
220
+ return ","
221
+
222
+ # Check first 5 non-empty lines
223
+ sample = [l for l in lines[:20] if l.strip()][:5]
224
+ if not sample:
225
+ return ","
226
+
227
+ candidates = [",", "\t", ";", "|"]
228
+ header_line = sample[0]
229
+
230
+ # Count occurrences
231
+ counts = {}
232
+ for delim in candidates:
233
+ counts[delim] = header_line.count(delim)
234
+
235
+ # Find the delimiter that appears consistently across lines
236
+ best_delim = ","
237
+ best_score = 0
238
+
239
+ for delim in candidates:
240
+ line_counts = [line.count(delim) for line in sample]
241
+ if len(set(line_counts)) <= 1 and line_counts[0] > 0:
242
+ # Consistent count across lines
243
+ score = line_counts[0] * 10 + (5 if delim != "," else 0)
244
+ if score > best_score:
245
+ best_score = score
246
+ best_delim = delim
247
+ elif line_counts[0] > counts.get(best_delim, 0):
248
+ best_delim = delim
249
+
250
+ return best_delim
251
+
252
+
253
+ def _format_size(size: int) -> str:
254
+ if size < 1024:
255
+ return f"{size} B"
256
+ elif size < 1024 * 1024:
257
+ return f"{size / 1024:.1f} KB"
258
+ elif size < 1024 * 1024 * 1024:
259
+ return f"{size / (1024 * 1024):.1f} MB"
260
+ return f"{size / (1024 * 1024 * 1024):.2f} GB"
csvclean/cli.py ADDED
@@ -0,0 +1,174 @@
1
+ """CSVClean CLI — Clean and validate CSV files."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.table import Table
9
+
10
+ console = Console()
11
+
12
+
13
+ @click.group()
14
+ @click.version_option(version="1.0.0", prog_name="csvclean")
15
+ def main():
16
+ """CSVClean — Find and fix CSV file problems.
17
+
18
+ Detect encoding issues, inconsistent delimiters, missing headers,
19
+ duplicate rows, type mismatches — all in one command.
20
+ """
21
+ pass
22
+
23
+
24
+ @main.command()
25
+ @click.argument("path", type=click.Path(exists=True))
26
+ @click.option("--json", "output_json", is_flag=True, help="Output as JSON")
27
+ def check(path, output_json):
28
+ """Analyze a CSV file and report all issues.
29
+
30
+ \b
31
+ Examples:
32
+ csvclean check data.csv
33
+ csvclean check data.csv --json
34
+ """
35
+ from .cleaner import analyze_csv
36
+
37
+ console.print()
38
+ console.print(Panel(
39
+ f"[bold]CSVClean Check[/bold] — [cyan]{path}[/cyan]",
40
+ border_style="blue"
41
+ ))
42
+
43
+ try:
44
+ result = analyze_csv(path)
45
+ except Exception as e:
46
+ console.print(f"[red]Error:[/red] {e}")
47
+ return
48
+
49
+ if output_json:
50
+ import json as _json
51
+ console.print(_json.dumps(result, indent=2, default=str))
52
+ return
53
+
54
+ if "error" in result:
55
+ console.print(f"[red]{result['error']}[/red]")
56
+ return
57
+
58
+ # Overview
59
+ score = result["health_score"]
60
+ color = "green" if score >= 80 else "yellow" if score >= 50 else "red"
61
+ console.print(f"\n[bold {color}]Health: {score}/100[/bold {color}]")
62
+ console.print(f" Size: {result['size_human']}")
63
+ console.print(f" Encoding: [cyan]{result['encoding']}[/cyan]")
64
+ console.print(f" Delimiter: [cyan]{result['delimiter']}[/cyan]")
65
+ console.print(f" Rows: [bold]{result['total_rows']:,}[/bold] × {result['total_columns']} columns")
66
+ console.print(f" Empty rows: {result.get('empty_rows', 0)}")
67
+ console.print(f" Duplicates: {result.get('duplicate_rows', 0)}")
68
+
69
+ # Issues
70
+ if result["issues"]:
71
+ console.print()
72
+ console.print("[bold]Issues found:[/bold]")
73
+ for issue in result["issues"]:
74
+ icon = "[red]✗[/red]" if issue["severity"] == "error" else "[yellow]![/yellow]"
75
+ console.print(f" {icon} {issue['message']}")
76
+ else:
77
+ console.print()
78
+ console.print("[green]No issues found. File is clean![/green]")
79
+
80
+ # Column types
81
+ if result.get("column_types"):
82
+ console.print()
83
+ console.print("[bold]Column Analysis:[/bold]")
84
+ col_table = Table()
85
+ col_table.add_column("Column")
86
+ col_table.add_column("Type", style="cyan")
87
+ col_table.add_column("Empty %", justify="right")
88
+ col_table.add_column("Distribution")
89
+
90
+ for col_name, info in result["column_types"].items():
91
+ dist = ", ".join(f"{t}:{c}" for t, c in info["distribution"].items() if t != "empty")
92
+ col_table.add_row(
93
+ col_name[:25],
94
+ info["dominant_type"],
95
+ f"{info['empty_pct']}%",
96
+ dist[:40]
97
+ )
98
+
99
+ console.print(col_table)
100
+
101
+
102
+ @main.command()
103
+ @click.argument("path", type=click.Path(exists=True))
104
+ @click.argument("output")
105
+ @click.option("--remove-empty/--keep-empty", default=True, help="Remove empty rows")
106
+ @click.option("--normalize/--preserve", default=True, help="Normalize delimiter to comma")
107
+ def clean(path, output, remove_empty, normalize):
108
+ """Clean a CSV file and write fixed output (premium).
109
+
110
+ \b
111
+ Example:
112
+ csvclean clean messy.csv clean.csv
113
+ """
114
+ console.print()
115
+ console.print("[yellow]CSV cleaning is a premium feature.[/yellow]")
116
+ console.print("Upgrade at https://kryptorious.gumroad.com/l/jbvet")
117
+ console.print()
118
+
119
+ from .cleaner import clean_csv
120
+ result = clean_csv(path, output, remove_empty=remove_empty, normalize_delimiter=normalize)
121
+ for fix in result["would_fix"]:
122
+ console.print(f" [dim]• {fix}[/dim]")
123
+
124
+
125
+ @main.command()
126
+ @click.argument("path", type=click.Path(exists=True))
127
+ def dedupe(path):
128
+ """Remove duplicate rows (premium).
129
+
130
+ \b
131
+ Example:
132
+ csvclean dedupe data.csv
133
+ """
134
+ console.print()
135
+ console.print("[yellow]Deduplication is a premium feature.[/yellow]")
136
+ console.print("Upgrade at https://kryptorious.gumroad.com/l/jbvet")
137
+ console.print()
138
+
139
+ from .cleaner import analyze_csv
140
+ result = analyze_csv(path)
141
+ dups = result.get("duplicate_rows", 0)
142
+ console.print(f"Would remove [bold]{dups}[/bold] duplicate row(s)")
143
+
144
+
145
+ @main.command()
146
+ @click.argument("path", type=click.Path(exists=True))
147
+ def stats(path):
148
+ """Show column statistics.
149
+
150
+ \b
151
+ Example:
152
+ csvclean stats data.csv
153
+ """
154
+ from .cleaner import analyze_csv
155
+
156
+ result = analyze_csv(path)
157
+ console.print()
158
+ console.print(Panel(
159
+ f"[bold]CSVClean Stats[/bold] — [cyan]{path}[/cyan]",
160
+ border_style="blue"
161
+ ))
162
+
163
+ ct = result.get("column_types", {})
164
+ for col_name, info in ct.items():
165
+ console.print(f"\n[bold cyan]{col_name}[/bold cyan]")
166
+ console.print(f" Type: {info['dominant_type']}")
167
+ console.print(f" Empty: {info['empty_count']} ({info['empty_pct']}%)")
168
+ dist = info.get("distribution", {})
169
+ if len(dist) > 1:
170
+ console.print(f" Distribution: {dist}")
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: kryptorious-csvclean
3
+ Version: 1.0.0
4
+ Summary: CSV file cleaner and validator — detect encoding, inconsistent delimiters, missing headers, duplicates, type mismatches.
5
+ Author: Kryptorious Quantum Biosciences, Inc.
6
+ License: MIT
7
+ Project-URL: Homepage, https://devflow.sh
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: click>=8.0
11
+ Requires-Dist: rich>=13.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
@@ -0,0 +1,8 @@
1
+ csvclean/__init__.py,sha256=YecLNa4q0BWGMG6D_ozVn8_XBFwhcj75dNXptxXSPV4,73
2
+ csvclean/cleaner.py,sha256=0YxIjH7DO5Ve96liakep8WHqOX-6mLJzc1qVJ3QdkPk,7742
3
+ csvclean/cli.py,sha256=laCKJItuWXkA-lHfddT_iLsu2kkvtHjESFJqAFyWgro,5395
4
+ kryptorious_csvclean-1.0.0.dist-info/METADATA,sha256=7hnxsNzofu732Z1oRCiPPPEoTmxbe-h9vPaqde5Q950,488
5
+ kryptorious_csvclean-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
6
+ kryptorious_csvclean-1.0.0.dist-info/entry_points.txt,sha256=8rVSFSPXR9AIbo2mmnm9BtdEltwQy953PiMDI-5R1uQ,47
7
+ kryptorious_csvclean-1.0.0.dist-info/top_level.txt,sha256=YhFNS9J01BBcnWCmEJdv_grCykCt_GUBC1NK8MeYyro,9
8
+ kryptorious_csvclean-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ csvclean = csvclean.cli:main
@@ -0,0 +1 @@
1
+ csvclean