From 8ead0896decb4e050b7385bd2767a8d5dcf3b1b8 Mon Sep 17 00:00:00 2001 From: Bernd Bohmeier Date: Fri, 11 Sep 2026 16:12:30 +0200 Subject: [PATCH 1/2] Check minknow experiment folder on startup We checked that the minknow experiment folder is created only a few sec after the minknow experiment is started, probably after some checks are run. We decided this is good enough to check it so that people who misspelled something will be notified. Some folders inside are only created much later, so we should not wait for that. --- src/nomadic/realtime/commands.py | 58 +++++++++++++++++++++++++++++--- src/nomadic/util/minknow.py | 30 ++++++++++++++++- 2 files changed, 83 insertions(+), 5 deletions(-) diff --git a/src/nomadic/realtime/commands.py b/src/nomadic/realtime/commands.py index abe7565..4615433 100644 --- a/src/nomadic/realtime/commands.py +++ b/src/nomadic/realtime/commands.py @@ -1,3 +1,5 @@ +import difflib +import glob import os from pathlib import Path from shutil import rmtree @@ -246,6 +248,8 @@ def find_minknow_fastq_dirs( return minknow.resolve_minknow_fastq_dirs(minknow_dir, experiment_name) except minknow.MinknowPathError as e: raise BadParameterWithSource(message=str(e), param_hint="-k/--minknow_dir") + except UserInputError as e: + raise click.UsageError(f"Could not find minknow folders: {e}") from e else: # If fastq_dir is manually given, we assume there is no minknow dir return None, fastq_dir @@ -297,9 +301,55 @@ def find_metadata_file(experiment_name: str, workspace: Workspace) -> str: break if metadata_path is None or not os.path.isfile(metadata_path): - msg = f"Metadata file not found. Did you create your metadata file in `{workspace.get_metadata_dir()}`" - if shared_workspace: - msg += f", or in `{shared_workspace}`" - msg += f", and does the name match `{experiment_name}`?" + msg = "Metadata file not found. " + closest_match = find_closest_metadata_file( + experiment_name, + [workspace.get_metadata_dir()] + + ([shared_workspace.get_metadata_dir()] if shared_workspace else []), + ) + if closest_match is not None: + closest_experiment_name, closest_metadata_path = closest_match + msg += f"Did you mean `{closest_experiment_name}`? The closest metadata file is `{closest_metadata_path}`. Either rename your metadata file to match the experiment name or correct the experiment name in your command." + else: + msg += ( + f"Did you create your metadata file in `{workspace.get_metadata_dir()}`" + ) + if shared_workspace: + msg += f", or in `{shared_workspace}`" + msg += f", and does the name match `{experiment_name}`?" raise click.BadParameter(message=msg) return metadata_path + + +def find_closest_metadata_file( + experiment_name: str, metadata_folders: list[str] +) -> tuple[str, str] | None: + """ + Find the closest metadata file to the given experiment name within the specified metadata folders. + + Args: + experiment_name (str): The name of the experiment to find a metadata file for. + metadata_folders (list[str]): A list of folders to search for metadata files. + + Returns: + tuple[str, str] | None: A tuple containing the closest matching experiment name and the path to the metadata file, + or None if no close match is found. + """ + files = [] + for folder in metadata_folders: + files.extend(file for file in glob.glob(f"{folder}/*") if os.path.isfile(file)) + + close_matches = difflib.get_close_matches( + experiment_name, + [os.path.splitext(os.path.basename(f))[0] for f in files], + cutoff=0.8, + ) + if not close_matches: + return None + closest_match = close_matches[0] + + for file in files: + if os.path.splitext(os.path.basename(file))[0] == closest_match: + return closest_match, file + + return None diff --git a/src/nomadic/util/minknow.py b/src/nomadic/util/minknow.py index 3fc41d8..1993012 100644 --- a/src/nomadic/util/minknow.py +++ b/src/nomadic/util/minknow.py @@ -1,11 +1,14 @@ +import difflib import glob import platform import warnings from pathlib import Path from typing import Optional +from nomadic.util.exceptions import UserInputError -class MinknowPathError(Exception): + +class MinknowPathError(UserInputError): pass @@ -74,6 +77,18 @@ def resolve_minknow_fastq_dirs( if is_minknow_base_dir(minknow_path): minknow_dir = minknow_path / experiment_name + if not minknow_dir.exists(): + closest_match = get_most_similar_experiment_name( + experiment_name, minknow_path + ) + if closest_match: + raise UserInputError( + f"'{experiment_name}' does not match any existing minknow experiments in '{minknow_path}'. Did you mean '{closest_match}'?", + ) + else: + raise UserInputError( + f"'{experiment_name}' does not match any existing minknow experiments in '{minknow_path}', please ensure the experiment name is correct and matches the minknow experiment name.", + ) elif is_minknow_experiment_dir(minknow_path): minknow_dir = minknow_path else: @@ -114,3 +129,16 @@ def default_data_dir() -> Path: return integrated_devices_path return standard_path + + +def get_most_similar_experiment_name( + experiment_name: str, minknow_path: Path +) -> str | None: + close_matches = difflib.get_close_matches( + experiment_name.lower(), + [d.name for d in minknow_path.glob("*") if d.is_dir()], + cutoff=0.8, + ) + if close_matches: + return close_matches[0] + return None From 60a077d95d8e19aef4a7b3c1ba81b5047e98bd4d Mon Sep 17 00:00:00 2001 From: Bernd Bohmeier Date: Fri, 11 Sep 2026 16:56:57 +0200 Subject: [PATCH 2/2] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/nomadic/realtime/commands.py | 7 ++++++- src/nomadic/util/minknow.py | 9 +++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/nomadic/realtime/commands.py b/src/nomadic/realtime/commands.py index 4615433..7dcf002 100644 --- a/src/nomadic/realtime/commands.py +++ b/src/nomadic/realtime/commands.py @@ -337,7 +337,12 @@ def find_closest_metadata_file( """ files = [] for folder in metadata_folders: - files.extend(file for file in glob.glob(f"{folder}/*") if os.path.isfile(file)) + files.extend( + file + for file in glob.glob(f"{folder}/*") + if os.path.isfile(file) + and os.path.splitext(file)[1].lower() in {".csv", ".xlsx"} + ) close_matches = difflib.get_close_matches( experiment_name, diff --git a/src/nomadic/util/minknow.py b/src/nomadic/util/minknow.py index 1993012..bc51d97 100644 --- a/src/nomadic/util/minknow.py +++ b/src/nomadic/util/minknow.py @@ -77,7 +77,7 @@ def resolve_minknow_fastq_dirs( if is_minknow_base_dir(minknow_path): minknow_dir = minknow_path / experiment_name - if not minknow_dir.exists(): + if not minknow_dir.is_dir(): closest_match = get_most_similar_experiment_name( experiment_name, minknow_path ) @@ -136,7 +136,12 @@ def get_most_similar_experiment_name( ) -> str | None: close_matches = difflib.get_close_matches( experiment_name.lower(), - [d.name for d in minknow_path.glob("*") if d.is_dir()], + [ + d.name + for d in minknow_path.glob("*") + if d.is_dir() + and d.name not in {"persistence", "reads", "queued_reads", "intermediates"} + ], cutoff=0.8, ) if close_matches: