|
| 1 | +from typing import Any |
| 2 | + |
| 3 | +from semeio.fmudesign.config_validation import SeedStrategy |
| 4 | +from semeio.fmudesign.utils import resolve_path |
| 5 | + |
| 6 | + |
| 7 | +class GeneralInputError(Exception): |
| 8 | + pass |
| 9 | + |
| 10 | + |
| 11 | +class MissingGeneralInputKeyError(Exception): |
| 12 | + def __init__(self, key: str) -> None: |
| 13 | + msg = f"key(s): '{key}' must be specified in general input sheet." |
| 14 | + super().__init__(msg) |
| 15 | + |
| 16 | + |
| 17 | +class ValidationError(Exception): |
| 18 | + def __init__(self, val: Any, key: str, type_str: str) -> None: # ruff: ignore[any-type] |
| 19 | + super().__init__( |
| 20 | + f"Could not validate '{val}' as {type_str} for key '{key}'. " |
| 21 | + f"Failed to validate '{val}'." |
| 22 | + ) |
| 23 | + |
| 24 | + |
| 25 | +class IntValidationError(ValidationError): |
| 26 | + pass |
| 27 | + |
| 28 | + |
| 29 | +class PositiveIntValidationError(ValidationError): |
| 30 | + pass |
| 31 | + |
| 32 | + |
| 33 | +class StrValidationError(ValidationError): |
| 34 | + pass |
| 35 | + |
| 36 | + |
| 37 | +class NoneValidationError(ValidationError): |
| 38 | + pass |
| 39 | + |
| 40 | + |
| 41 | +class PathValidationError(ValidationError): |
| 42 | + pass |
| 43 | + |
| 44 | + |
| 45 | +def validate_string(maybe_string: Any, key: str) -> str: # ruff: ignore[any-type] |
| 46 | + try: |
| 47 | + return str(maybe_string) |
| 48 | + except Exception as e: |
| 49 | + raise StrValidationError(maybe_string, key, "str") from e |
| 50 | + |
| 51 | + |
| 52 | +def validate_int(maybe_int: Any, key: str) -> int: # ruff: ignore[any-type] |
| 53 | + try: |
| 54 | + return int(maybe_int) |
| 55 | + except (TypeError, ValueError) as e: |
| 56 | + raise IntValidationError(maybe_int, key, "int") from e |
| 57 | + |
| 58 | + |
| 59 | +def validate_positive_int(maybe_int: Any, key: str) -> int: # ruff: ignore[any-type] |
| 60 | + try: |
| 61 | + int_ = int(maybe_int) |
| 62 | + except (TypeError, ValueError) as e: |
| 63 | + raise PositiveIntValidationError(maybe_int, key, "positive int") from e |
| 64 | + |
| 65 | + if int_ < 0: |
| 66 | + raise PositiveIntValidationError(maybe_int, key, "positive int") |
| 67 | + |
| 68 | + return int_ |
| 69 | + |
| 70 | + |
| 71 | +def validate_file(maybe_file: Any, input_file: str, key: str) -> None: # ruff: ignore[any-type] |
| 72 | + try: |
| 73 | + resolve_path(maybe_file, input_file) |
| 74 | + except (ValueError, TypeError) as e: |
| 75 | + raise GeneralInputError( |
| 76 | + f"Could not resolve file '{maybe_file}' to an existing " |
| 77 | + f"file for key '{key}'. " |
| 78 | + f"Failed to validate '{maybe_file}'." |
| 79 | + ) from e |
| 80 | + |
| 81 | + |
| 82 | +def validate_none(maybe_none: Any, key: str) -> None: # ruff: ignore[any-type] |
| 83 | + if maybe_none is None: |
| 84 | + return |
| 85 | + if isinstance(maybe_none, str) and maybe_none.lower() == "none": |
| 86 | + return |
| 87 | + raise NoneValidationError(maybe_none, key, "None") |
| 88 | + |
| 89 | + |
| 90 | +REQUIRED_KEYS = {"designtype", "repeats", "distribution_seed", "rms_seeds"} |
| 91 | +ALLOWED_KEYS = REQUIRED_KEYS | {"correlation_iterations", "seed_strategy", "background"} |
| 92 | + |
| 93 | + |
| 94 | +def _validate_required_keys(config: dict[str, Any]) -> None: |
| 95 | + missing_keys = REQUIRED_KEYS - set(config.keys()) |
| 96 | + if missing_keys: |
| 97 | + raise MissingGeneralInputKeyError(", ".join(missing_keys)) |
| 98 | + |
| 99 | + |
| 100 | +def _validate_no_extra_keys(config: dict[str, Any]) -> None: |
| 101 | + extra_keys = set(config.keys()) - set(ALLOWED_KEYS) |
| 102 | + if extra_keys: |
| 103 | + raise GeneralInputError( |
| 104 | + "In the general input sheet, the following parameter(s) are not " |
| 105 | + f"recognized and cannot be parsed:\n{extra_keys!r}\n" |
| 106 | + f"Allowed keys:{ALLOWED_KEYS!r}" |
| 107 | + ) |
| 108 | + |
| 109 | + |
| 110 | +def _validate_designtype(config: dict[str, Any]) -> None: |
| 111 | + if config["designtype"] != "onebyone": |
| 112 | + raise GeneralInputError( |
| 113 | + "Generation of DesignMatrix only implemented for designtype 'onebyone', " |
| 114 | + f"not '{config['designtype']}'" |
| 115 | + ) |
| 116 | + |
| 117 | + |
| 118 | +def _validate_repeats(config: dict[str, Any]) -> None: |
| 119 | + validate_positive_int(config["repeats"], "repeats") |
| 120 | + |
| 121 | + |
| 122 | +def _validate_correlation_iterations(config: dict[str, Any]) -> None: |
| 123 | + key = "correlation_iterations" |
| 124 | + if key in config: |
| 125 | + validate_positive_int(config["correlation_iterations"], key) |
| 126 | + |
| 127 | + |
| 128 | +def _validate_distribution_seed(config: dict[str, Any]) -> None: |
| 129 | + key = "distribution_seed" |
| 130 | + value = config["distribution_seed"] |
| 131 | + try: |
| 132 | + validate_positive_int(value, key) |
| 133 | + except PositiveIntValidationError: |
| 134 | + try: |
| 135 | + validate_none(value, key) |
| 136 | + except NoneValidationError as e: |
| 137 | + raise ValidationError(value, key, "positive int or None") from e |
| 138 | + |
| 139 | + |
| 140 | +def _validate_seed_strategy(config: dict[str, Any]) -> None: |
| 141 | + key = "seed_strategy" |
| 142 | + if key in config: |
| 143 | + value = validate_string(config[key], key).lower() |
| 144 | + if value.strip().lower() not in {*SeedStrategy, "none"}: |
| 145 | + raise ValidationError( |
| 146 | + value, key, f"SeedStrategy ({'/'.join(SeedStrategy)})" |
| 147 | + ) |
| 148 | + |
| 149 | + |
| 150 | +def _validate_rms_seeds(config: dict[str, Any], input_file: str) -> None: |
| 151 | + key = "rms_seeds" |
| 152 | + value = config[key] |
| 153 | + try: |
| 154 | + validate_file(value, input_file, key) |
| 155 | + except (PathValidationError, GeneralInputError): |
| 156 | + try: |
| 157 | + s = validate_string(value, key) |
| 158 | + if s != "default": |
| 159 | + raise StrValidationError(s, key, s) |
| 160 | + except StrValidationError: |
| 161 | + try: |
| 162 | + validate_none(value, key) |
| 163 | + except NoneValidationError as e: |
| 164 | + raise ValidationError(value, key, "str, path or None") from e |
| 165 | + |
| 166 | + |
| 167 | +def validate_general_input(config: dict[str, Any], input_filename: str) -> None: |
| 168 | + _validate_no_extra_keys(config) |
| 169 | + _validate_required_keys(config) |
| 170 | + _validate_designtype(config) |
| 171 | + _validate_repeats(config) |
| 172 | + _validate_correlation_iterations(config) |
| 173 | + _validate_distribution_seed(config) |
| 174 | + _validate_seed_strategy(config) |
| 175 | + _validate_rms_seeds(config, input_filename) |
0 commit comments