66import os
77import pathlib
88import subprocess
9+ import re
910
1011import pandas as pd
1112
@@ -24,6 +25,22 @@ def build_validator_call(path, ignore_headers=False):
2425 return command
2526
2627
28+ def get_bids_validator_version ():
29+ """Get the version of the BIDS validator.
30+
31+ Returns
32+ -------
33+ version : :obj:`str`
34+ Version of the BIDS validator.
35+ """
36+ command = ["deno" , "run" , "-A" , "jsr:@bids/validator" , "--version" ]
37+ result = subprocess .run (command , stdout = subprocess .PIPE , stderr = subprocess .PIPE )
38+ output = result .stdout .decode ("utf-8" ).strip ()
39+ version = output .split ()[- 1 ]
40+ clean_ver = re .sub (r'\x1b\[[0-9;]*m' , '' , version ) # Remove ANSI color codes
41+ return {"ValidatorVersion" : clean_ver }
42+
43+
2744def build_subject_paths (bids_dir ):
2845 """Build a list of BIDS dirs with 1 subject each."""
2946 bids_dir = str (bids_dir )
@@ -52,6 +69,26 @@ def build_subject_paths(bids_dir):
5269 return subjects_dict
5370
5471
72+ def build_first_subject_path (bids_dir , subject ):
73+ """Build a list of BIDS dirs with 1 subject each."""
74+ bids_dir = str (bids_dir )
75+ if not bids_dir .endswith ("/" ):
76+ bids_dir += "/"
77+
78+ root_files = [x for x in glob .glob (bids_dir + "*" ) if os .path .isfile (x )]
79+
80+ subject_dict = {}
81+
82+ purepath = pathlib .PurePath (subject )
83+ sub_label = purepath .name
84+
85+ files = [x for x in glob .glob (subject + "**" , recursive = True ) if os .path .isfile (x )]
86+ files .extend (root_files )
87+ subject_dict [sub_label ] = files
88+
89+ return subject_dict
90+
91+
5592def run_validator (call ):
5693 """Run the validator with subprocess.
5794
@@ -103,6 +140,7 @@ def parse_issue(issue_dict):
103140 return {
104141 "location" : issue_dict .get ("location" , "" ),
105142 "code" : issue_dict .get ("code" , "" ),
143+ "issueMessage" : issue_dict .get ("issueMessage" , "" ),
106144 "subCode" : issue_dict .get ("subCode" , "" ),
107145 "severity" : issue_dict .get ("severity" , "" ),
108146 "rule" : issue_dict .get ("rule" , "" ),
@@ -114,7 +152,9 @@ def parse_issue(issue_dict):
114152 # Extract issues
115153 issues = data .get ("issues" , {}).get ("issues" , [])
116154 if not issues :
117- return pd .DataFrame (columns = ["location" , "code" , "subCode" , "severity" , "rule" ])
155+ return pd .DataFrame (
156+ columns = ["location" , "code" , "issueMessage" , "subCode" , "severity" , "rule" ]
157+ )
118158
119159 # Parse all issues
120160 parsed_issues = [parse_issue (issue ) for issue in issues ]
@@ -135,7 +175,99 @@ def get_val_dictionary():
135175 return {
136176 "location" : {"Description" : "File with the validation issue." },
137177 "code" : {"Description" : "Code of the validation issue." },
178+ "issueMessage" : {"Description" : "Validation issue message." },
138179 "subCode" : {"Description" : "Subcode providing additional issue details." },
139180 "severity" : {"Description" : "Severity of the issue (e.g., warning, error)." },
140181 "rule" : {"Description" : "Validation rule that triggered the issue." },
141182 }
183+
184+
185+ def extract_summary_info (output ):
186+ """Extract summary information from the JSON output.
187+
188+ Parameters
189+ ----------
190+ output : str
191+ JSON string of BIDS validator output.
192+
193+ Returns
194+ -------
195+ dict
196+ Dictionary containing SchemaVersion and other summary info.
197+ """
198+ try :
199+ data = json .loads (output )
200+ except json .JSONDecodeError as e :
201+ raise ValueError ("Invalid JSON provided to get SchemaVersion." ) from e
202+
203+ summary = data .get ("summary" , {})
204+
205+ return {"SchemaVersion" : summary .get ("schemaVersion" , "" )}
206+
207+
208+ def update_dataset_description (path , new_info ):
209+ """Update or append information to dataset_description.json.
210+
211+ Parameters
212+ ----------
213+ path : :obj:`str`
214+ Path to the dataset.
215+ new_info : :obj:`dict`
216+ Information to add or update.
217+ """
218+ description_path = os .path .join (path , "dataset_description.json" )
219+
220+ # Load existing data if the file exists
221+ if os .path .exists (description_path ):
222+ with open (description_path , "r" ) as f :
223+ existing_data = json .load (f )
224+ else :
225+ existing_data = {}
226+
227+ # Update the existing data with the new info
228+ existing_data .update (new_info )
229+
230+ # Write the updated data back to the file
231+ with open (description_path , "w" ) as f :
232+ json .dump (existing_data , f , indent = 4 )
233+ print (f"Updated dataset_description.json at: { description_path } " )
234+
235+ # Check if .datalad directory exists before running the DataLad save command
236+ datalad_dir = os .path .join (path , ".datalad" )
237+ if os .path .exists (datalad_dir ) and os .path .isdir (datalad_dir ):
238+ try :
239+ subprocess .run (
240+ ["datalad" , "save" , "-m" ,
241+ "Save BIDS validator and schema version to dataset_description" ,
242+ description_path ],
243+ check = True
244+ )
245+ print ("Changes saved with DataLad." )
246+ except subprocess .CalledProcessError as e :
247+ print (f"Error running DataLad save: { e } " )
248+
249+
250+ def bids_validator_version (output , path , write = False ):
251+ """Save BIDS validator and schema version.
252+
253+ Parameters
254+ ----------
255+ output : :obj:`str`
256+ Path to JSON file of BIDS validator output.
257+ path : :obj:`str`
258+ Path to the dataset.
259+ write : :obj:`bool`
260+ If True, write to dataset_description.json. If False, print to terminal.
261+ """
262+ # Get the BIDS validator version
263+ validator_version = get_bids_validator_version ()
264+ # Extract schemaVersion
265+ summary_info = extract_summary_info (output )
266+
267+ combined_info = {** validator_version , ** summary_info }
268+
269+ if write :
270+ # Update the dataset_description.json file
271+ update_dataset_description (path , combined_info )
272+ elif not write :
273+ print (combined_info )
0 commit comments