"""Audit CSV row structure under an explicit Python CSV dialect; no type inference.""" import argparse import csv import io import json import sys MAX_BYTES = 1024 * 1024 MAX_FIELD_CHARACTERS = 65536 MAX_ERRORS = 100 def audit(raw, delimiter=",", header=True): result = {"ok": False, "dialect": {"delimiter": delimiter, "quotechar": '"', "doublequote": True, "strict": True}, "header_policy": "first_record" if header else "no_header", "header": None, "columns": None, "data_rows": 0, "physical_lines": 0, "errors": [], "error_count": 0, "errors_truncated": False} def add_error(error): result["error_count"] += 1 if len(result["errors"]) < MAX_ERRORS: result["errors"].append(error) else: result["errors_truncated"] = True if delimiter not in [",", "\t", ";", "|"]: add_error({"code": "unsupported_delimiter"}) return result if len(raw) > MAX_BYTES: add_error({"code": "input_too_large"}) return result try: text = raw.decode("utf-8-sig", errors="strict") except UnicodeDecodeError: add_error({"code": "invalid_utf8"}) return result previous_limit = csv.field_size_limit(MAX_FIELD_CHARACTERS) reader = csv.reader(io.StringIO(text, newline=""), delimiter=delimiter, quotechar='"', doublequote=True, strict=True) logical_row = 0 previous_line = 0 try: for logical_row, row in enumerate(reader, 1): line_start = previous_line + 1 previous_line = reader.line_num result["physical_lines"] = reader.line_num if logical_row == 1: result["columns"] = len(row) if not row: add_error({"code": "empty_first_record", "record": 1}) if header: result["header"] = row seen = set() for column, value in enumerate(row, 1): if not value.strip(): add_error({"code": "empty_header", "column": column}) if value in seen: add_error({"code": "duplicate_header", "column": column}) seen.add(value) continue result["data_rows"] += 1 if len(row) != result["columns"]: add_error({"code": "column_count", "record": logical_row, "physical_line_start": line_start, "physical_line_end": reader.line_num, "expected": result["columns"], "actual": len(row)}) if logical_row == 0: add_error({"code": "empty_input"}) except csv.Error as error: result["physical_lines"] = reader.line_num code = "field_too_large" if "field larger than field limit" in str(error) else "csv_parse_error" add_error({"code": code, "record": logical_row + 1, "physical_line_start": previous_line + 1, "physical_line_end": reader.line_num}) finally: csv.field_size_limit(previous_limit) result["ok"] = not result["errors"] return result def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--delimiter", choices=[",", "tab", ";", "|"], default=",") parser.add_argument("--no-header", action="store_true") args = parser.parse_args() delimiter = "\t" if args.delimiter == "tab" else args.delimiter result = audit(sys.stdin.buffer.read(MAX_BYTES + 1), delimiter, not args.no_header) print(json.dumps(result, ensure_ascii=True)) return 0 if result["ok"] else 1 if __name__ == "__main__": raise SystemExit(main())