from clan_lib.errors import ClanError from clan_lib.flake import Flake def _levenshtein_distance(s1: str, s2: str) -> int: """ Calculate the Levenshtein distance between two strings. """ if len(s1) < len(s2): return _levenshtein_distance(s2, s1) if len(s2) == 0: return len(s1) previous_row = list(range(len(s2) + 1)) for i, c1 in enumerate(s1): current_row = [i + 1] for j, c2 in enumerate(s2): insertions = previous_row[j + 1] + 1 deletions = current_row[j] + 1 substitutions = previous_row[j] + (c1 != c2) current_row.append(min(insertions, deletions, substitutions)) previous_row = current_row return previous_row[-1] def _suggest_similar_names( target: str, candidates: list[str], max_suggestions: int = 3 ) -> list[str]: if not candidates: return [] distances = [ (candidate, _levenshtein_distance(target.lower(), candidate.lower())) for candidate in candidates ] distances.sort(key=lambda x: (x[1], x[0])) return [candidate for candidate, _ in distances[:max_suggestions]] def get_available_machines(flake: Flake) -> list[str]: from clan_lib.machines.list import list_machines machines = list_machines(flake) return list(machines.keys()) def validate_machine_names(machine_names: list[str], flake: Flake) -> None: if not machine_names: return available_machines = get_available_machines(flake) invalid_machines = [ name for name in machine_names if name not in available_machines ] if invalid_machines: error_lines = [] for machine_name in invalid_machines: suggestions = _suggest_similar_names(machine_name, available_machines) if suggestions: suggestion_text = f"Did you mean: {', '.join(suggestions)}?" else: suggestion_text = ( f"Available machines: {', '.join(sorted(available_machines))}" ) error_lines.append(f"Machine '{machine_name}' not found. {suggestion_text}") raise ClanError("\n".join(error_lines))