66
77from physionet .validate .models import CheckResult , ValidationIssue , CheckCategory , Severity
88from physionet .validate .config import ValidationConfig
9+ from physionet .validate .checks .csv_formats import find_files_by_suffix , get_csv_layout
910
1011
1112def check_integrity (path : Path , config : ValidationConfig , progress_callback : Optional [Callable [[str ], None ]] = None ) -> CheckResult :
@@ -28,7 +29,7 @@ def check_integrity(path: Path, config: ValidationConfig, progress_callback: Opt
2829 result = CheckResult (category = CheckCategory .INTEGRITY )
2930
3031 # Find and validate CSV files
31- csv_files = list (path . rglob ( "* .csv") )
32+ csv_files = find_files_by_suffix (path , " .csv" )
3233 for i , csv_file in enumerate (csv_files ):
3334 if progress_callback :
3435 progress_callback (f"Checking { csv_file .name } ({ i + 1 } /{ len (csv_files )} CSV files)" )
@@ -43,24 +44,27 @@ def check_integrity(path: Path, config: ValidationConfig, progress_callback: Opt
4344
4445def _validate_csv_structure (csv_file : Path , base_path : Path , result : CheckResult ) -> None :
4546 """Validate CSV file structure."""
47+ layout = get_csv_layout (csv_file )
4648 try :
4749 with open (csv_file , "r" , encoding = "utf-8" ) as f :
4850 # Try to detect dialect
4951 sample = f .read (1024 )
5052 f .seek (0 )
5153
5254 try :
53- dialect = csv .Sniffer ().sniff (sample )
55+ dialect = csv .Sniffer ().sniff (sample , delimiters = ",; \t |" )
5456 except csv .Error :
5557 # Use default dialect if detection fails
5658 dialect = csv .excel
5759
5860 reader = csv .reader (f , dialect )
5961
60- # Read header
62+ # Read the first row, which is a header for ordinary CSV files.
6163 try :
62- header = next (reader )
64+ first_row = next (reader )
6365 except StopIteration :
66+ if layout .allow_empty :
67+ return
6468 result .issues .append (
6569 ValidationIssue (
6670 severity = Severity .ERROR ,
@@ -71,7 +75,7 @@ def _validate_csv_structure(csv_file: Path, base_path: Path, result: CheckResult
7175 )
7276 return
7377
74- if not header :
78+ if not first_row :
7579 result .issues .append (
7680 ValidationIssue (
7781 severity = Severity .ERROR ,
@@ -82,33 +86,41 @@ def _validate_csv_structure(csv_file: Path, base_path: Path, result: CheckResult
8286 )
8387 return
8488
85- # Check for duplicate column names
86- if len (header ) != len (set (header )):
87- duplicates = [col for col in header if header .count (col ) > 1 ]
88- result .issues .append (
89- ValidationIssue (
90- severity = Severity .ERROR ,
91- category = CheckCategory .INTEGRITY ,
92- file = str (csv_file .relative_to (base_path )),
93- message = f"Duplicate column names found: { ', ' .join (set (duplicates ))} " ,
89+ if layout .columns is None :
90+ header = first_row
91+ if len (header ) != len (set (header )):
92+ duplicates = [col for col in header if header .count (col ) > 1 ]
93+ result .issues .append (
94+ ValidationIssue (
95+ severity = Severity .ERROR ,
96+ category = CheckCategory .INTEGRITY ,
97+ file = str (csv_file .relative_to (base_path )),
98+ message = f"Duplicate column names found: { ', ' .join (set (duplicates ))} " ,
99+ )
94100 )
95- )
96-
97- # Check for empty column names
98- if any (not col .strip () for col in header ):
99- result .issues .append (
100- ValidationIssue (
101- severity = Severity .ERROR ,
102- category = CheckCategory .INTEGRITY ,
103- file = str (csv_file .relative_to (base_path )),
104- message = "CSV contains empty column names" ,
101+ if any (not col .strip () for col in header ):
102+ result .issues .append (
103+ ValidationIssue (
104+ severity = Severity .ERROR ,
105+ category = CheckCategory .INTEGRITY ,
106+ file = str (csv_file .relative_to (base_path )),
107+ message = "CSV contains empty column names" ,
108+ )
105109 )
106- )
110+ expected_cols = len (header )
111+ first_data_line = 2
112+ else :
113+ # Headerless files have explicit names; validate their first data row.
114+ f .seek (0 )
115+ reader = csv .reader (f , dialect )
116+ for _ in range (layout .metadata_rows ):
117+ next (reader , None )
118+ expected_cols = len (layout .columns )
119+ first_data_line = layout .metadata_rows + 1
107120
108121 # Validate row consistency
109- expected_cols = len (header )
110122 row_count = 0
111- for line_num , row in enumerate (reader , start = 2 ): # Start at 2 (after header)
123+ for line_num , row in enumerate (reader , start = first_data_line ):
112124 row_count += 1
113125 if len (row ) != expected_cols :
114126 result .issues .append (
@@ -132,13 +144,13 @@ def _validate_csv_structure(csv_file: Path, base_path: Path, result: CheckResult
132144 )
133145 break
134146
135- if row_count == 0 :
147+ if row_count == 0 and not layout . allow_empty :
136148 result .issues .append (
137149 ValidationIssue (
138- severity = Severity .WARNING ,
150+ severity = Severity .INFO if layout . no_data_message else Severity . WARNING ,
139151 category = CheckCategory .INTEGRITY ,
140152 file = str (csv_file .relative_to (base_path )),
141- message = "CSV file contains only header row (no data)" ,
153+ message = layout . no_data_message or "CSV file contains only header row (no data)" ,
142154 )
143155 )
144156
0 commit comments