Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions hed/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,175 @@ def validate_sidecar_cmd(
ctx.exit(result if result is not None else 0)


@validate.command(
name="tabular",
epilog="""
This command validates HED in a tabular file (TSV) against a specified HED schema
version. It can optionally include a sidecar file and check for warnings.

\b
Examples:
# Basic validation of a TSV file
hedpy validate tabular events.tsv -sv 8.3.0

# Validate with a sidecar
hedpy validate tabular events.tsv -s sidecar.json -sv 8.3.0

# Validate with multiple schemas (base + library)
hedpy validate tabular events.tsv -s sidecar.json -sv 8.3.0 -sv score_1.1.0

# Check for warnings as well as errors
hedpy validate tabular events.tsv -sv 8.4.0 --check-for-warnings

# Limit reported errors
hedpy validate tabular events.tsv -sv 8.4.0 -el 5

# Save validation results to a file
hedpy validate tabular events.tsv -sv 8.4.0 -o validation_results.txt
""",
)
@click.argument("tabular_file", type=click.Path(exists=True))
# Validation options
@optgroup.group("Validation options")
@optgroup.option(
"-sv",
"--schema-version",
required=True,
multiple=True,
metavar="VERSION",
help="HED schema version(s) to validate against (e.g., '8.4.0'). Can be specified multiple times for multiple schemas (e.g., -sv lang_1.1.0 -sv score_2.1.0)",
)
@optgroup.option(
"-s",
"--sidecar",
type=click.Path(exists=True),
metavar=METAVAR_FILE,
help="BIDS JSON sidecar file to use during validation",
)
@optgroup.option(
"-w",
"--check-for-warnings",
is_flag=True,
help="Check for warnings as well as errors",
)
@optgroup.option(
"-el",
"--error-limit",
type=int,
metavar=METAVAR_N,
help="Limit number of errors reported per code (default: No limit)",
)
@optgroup.option(
"-ef",
"--errors-by-file",
is_flag=True,
help="If using --error-limit, apply the limit per-file rather than globally",
)
# Output options
@optgroup.group("Output options")
@optgroup.option(
"-f",
"--format",
type=click.Choice(["text", "json"]),
default="text",
show_default="text",
help="Output format for validation results (text: human-readable; json: structured format for programmatic use)",
)
@optgroup.option(
"-o",
"--output-file",
type=click.Path(),
default="",
metavar=METAVAR_FILE,
help="Path for output file to hold validation results; if not specified, output to stdout",
)
# Logging options
@optgroup.group("Logging options")
@optgroup.option(
"-l",
"--log-level",
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]),
default="WARNING",
show_default="WARNING",
help="Log level for diagnostic messages",
)
@optgroup.option(
"-v",
"--verbose",
is_flag=True,
help="Output informational messages (equivalent to --log-level INFO)",
)
@optgroup.option(
"-lf",
"--log-file",
type=click.Path(),
metavar=METAVAR_FILE,
help="File path for saving log output; logs still go to stderr unless --log-quiet is also used",
)
@optgroup.option(
"-lq",
"--log-quiet",
is_flag=True,
help="Suppress log output to stderr; only applicable when --log-file is used (logs go only to file)",
)
@optgroup.option(
"--no-log",
is_flag=True,
help="Disable all logging output",
)
@click.pass_context
def validate_tabular_cmd(
ctx,
tabular_file,
schema_version,
sidecar,
check_for_warnings,
error_limit,
errors_by_file,
format,
output_file,
log_level,
log_file,
log_quiet,
no_log,
verbose,
):
"""Validate HED in a tabular file.

TABULAR_FILE: The path to the tabular file (e.g., TSV) to validate.
"""
from hed.scripts.validate_hed_tabular import main as validate_tabular_main

args = [tabular_file]
for version in schema_version:
args.extend(["-sv", version])
if sidecar:
args.extend(["-s", sidecar])
if check_for_warnings:
args.append("-w")
if error_limit is not None:
args.extend(["-el", str(error_limit)])
if errors_by_file:
args.append("-ef")
if format:
args.extend(["-f", format])
if output_file:
args.extend(["-o", output_file])
if log_level:
args.extend(["-l", log_level])
if log_file:
args.extend(["-lf", log_file])
if log_quiet:
args.append("-lq")
if no_log:
args.append("--no-log")
if verbose:
args.append("-v")

result = validate_tabular_main(args)
ctx.exit(result if result is not None else 0)


@schema.command(name="validate")
@click.argument("schema_path", type=click.Path(exists=True), nargs=-1, required=True)
@click.option("--add-all-extensions", is_flag=True, help="Always verify all versions of the same schema are equal")
Expand Down
203 changes: 203 additions & 0 deletions hed/scripts/validate_hed_tabular.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#!/usr/bin/env python
"""
Validates HED in a tabular file (TSV) against a specified schema version.

This script validates HED in a tabular file, optionally with a JSON sidecar,
against a specified HED schema version.
"""

import argparse
import sys
import os
from hed.models import TabularInput, Sidecar
from hed.errors import ErrorHandler
from hed.schema import load_schema_version
from hed.scripts.script_utils import setup_logging, format_validation_results


def get_parser():
"""Create the argument parser for validate_hed_tabular.

Returns:
argparse.ArgumentParser: Configured argument parser.
"""
parser = argparse.ArgumentParser(
description="Validate HED in a tabular file against a HED schema", formatter_class=argparse.RawDescriptionHelpFormatter
)

# Required arguments
parser.add_argument("tabular_file", help="Tabular file (TSV) to validate")
parser.add_argument(
"-sv",
"--schema-version",
required=True,
nargs="+",
dest="schema_version",
help="HED schema version(s) to validate against (e.g., '8.4.0' or '8.3.0 score_1.1.0' for multiple schemas)",
)

# Optional arguments
parser.add_argument(
"-s",
"--sidecar",
dest="sidecar_file",
help="Optional BIDS JSON sidecar file to use during validation",
)
parser.add_argument(
"-w",
"--check-for-warnings",
action="store_true",
dest="check_for_warnings",
help="Check for warnings in addition to errors",
)

# Error limiting
error_group = parser.add_argument_group("Error limiting options")
error_group.add_argument(
"-el",
"--error-limit",
type=int,
dest="error_limit",
default=None,
help="Limit number of errors reported per code (default: No limit)",
)
error_group.add_argument(
"-ef",
"--errors-by-file",
action="store_true",
dest="errors_by_file",
help="If using --error-limit, apply the limit per-file rather than globally",
)

# Output options
output_group = parser.add_argument_group("Output options")
output_group.add_argument(
"-f",
"--format",
choices=["text", "json"],
default="text",
help="Output format for validation results (default: %(default)s)",
)
output_group.add_argument(
"-o",
"--output-file",
default="",
dest="output_file",
help="Output file for validation results; if not specified, output to stdout",
)

# Logging options
logging_group = parser.add_argument_group("Logging options")
logging_group.add_argument(
"-l",
"--log-level",
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
default="WARNING",
dest="log_level",
help="Logging level (default: %(default)s)",
)
logging_group.add_argument("-lf", "--log-file", default="", dest="log_file", help="File path for saving log output")
logging_group.add_argument(
"-lq", "--log-quiet", action="store_true", dest="log_quiet", help="Suppress log output to stderr when using --log-file"
)
logging_group.add_argument("--no-log", action="store_true", dest="no_log", help="Disable all logging output")
logging_group.add_argument("-v", "--verbose", action="store_true", help="Output informational messages")

return parser


def main(arg_list=None):
"""Main function for validating HED in a tabular file.

Parameters:
arg_list (list or None): Command line arguments.
"""
parser = get_parser()
args = parser.parse_args(arg_list)

# Set up logging
setup_logging(args.log_level, args.log_file, args.log_quiet, args.verbose, args.no_log)

import logging

logger = logging.getLogger("validate_hed_tabular")
effective_level_name = logging.getLevelName(logger.getEffectiveLevel())
logger.info(
"Starting HED validation of tabular file with effective log level: %s (requested: %s, verbose=%s)",
effective_level_name,
args.log_level,
"on" if args.verbose else "off",
)

try:
# Load schema (handle single version or list of versions)
schema_versions = args.schema_version[0] if len(args.schema_version) == 1 else args.schema_version
logging.info(f"Loading HED schema version(s) {schema_versions}")
schema = load_schema_version(schema_versions)

# Parse Sidecar if provided
sidecar = None
issues = []
error_handler = ErrorHandler(check_for_warnings=args.check_for_warnings)

if args.sidecar_file:
logging.info("Loading Sidecar file")
sidecar = Sidecar(args.sidecar_file, name=os.path.basename(args.sidecar_file))
sidecar_issues = sidecar.validate(schema, name=sidecar.name, error_handler=error_handler)
issues += sidecar_issues
if sidecar_issues:
logging.warning(f"Found {len(sidecar_issues)} issues in sidecar validation")

# Parse and Validate Tabular Input
logging.info("Loading Tabular file")
tabular_input = TabularInput(args.tabular_file, sidecar=sidecar, name=os.path.basename(args.tabular_file))

logging.info("Validating Tabular file")
# Validate tabular input
tabular_issues = tabular_input.validate(schema, name=tabular_input.name, error_handler=error_handler)
issues += tabular_issues

# Handle output
if issues:
# Format validation errors
output = format_validation_results(
issues,
output_format=args.format,
title_message="HED validation issues:",
error_limit=args.error_limit,
errors_by_file=args.errors_by_file,
)

# Write output
if args.output_file:
with open(args.output_file, "w") as f:
f.write(output)
logging.info(f"Validation errors written to {args.output_file}")
else:
print(output)

return 1 # Exit with error code if validation failed
else:
# Success message
success_msg = "Tabular file has valid HED!"
if args.output_file:
with open(args.output_file, "w") as f:
f.write(success_msg + "\n")
logging.info(f"Validation results written to {args.output_file}")
else:
print(success_msg)

return 0

except Exception as e:
logging.error(f"Validation failed: {str(e)}")
# If verbose, print stack trace
if args.verbose:
import traceback

traceback.print_exc()
return 1


if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ hedpy = "hed.cli.cli:main"
validate_bids = "hed.scripts.validate_bids:main"
validate_hed_string = "hed.scripts.validate_hed_string:main"
validate_hed_sidecar = "hed.scripts.validate_hed_sidecar:main"
validate_hed_tabular = "hed.scripts.validate_hed_tabular:main"
hed_extract_bids_sidecar = "hed.scripts.hed_extract_bids_sidecar:main"
hed_validate_schemas = "hed.scripts.validate_schemas:main"
hed_update_schemas = "hed.scripts.hed_convert_schema:main"
Expand Down
Loading
Loading