diff --git a/src/osekit/core/detection.py b/src/osekit/core/detection.py index e4b12065..085ea03a 100644 --- a/src/osekit/core/detection.py +++ b/src/osekit/core/detection.py @@ -12,6 +12,7 @@ from matplotlib.transforms import TransformedBbox from pandas import Timedelta, Timestamp +from osekit.config import global_logging_context as glc from osekit.core.event import Event from osekit.utils.core import is_empty_dataclass @@ -28,6 +29,7 @@ "min_frequency", "max_frequency", "annotation", + "label", "annotator", "annotator_expertise", "start_datetime", @@ -617,7 +619,7 @@ def from_dict(cls, row: dict) -> Self: return cls( metadata=metadata, - label=row.get("annotation"), + label=row.get("label") or row.get("annotation"), detector_info=detector_info, begin=Timestamp(row["start_datetime"]), end=Timestamp(row["end_datetime"]), @@ -764,7 +766,21 @@ def _from_csv(cls, csv: Path, **kwargs: Any) -> list[Self]: } for record in records ] - return [cls.from_dict(record) for record in records] + parsed_records = [] + failed_records = [] + for idx, record in enumerate(records): + try: + parsed_records.append(cls.from_dict(record)) + except TypeError: # Most probably a weak comment on the spectrogram + failed_records.append(idx) + + if failed_records: + glc.logger.warning( + f"The following line(s) couldn't be parsed as detections: " + f"{failed_records}", + ) + + return parsed_records @classmethod def from_csv(cls, csv: Path | list[Path], **kwargs: Any) -> list[Self]: @@ -792,4 +808,11 @@ def from_csv(cls, csv: Path | list[Path], **kwargs: Any) -> list[Self]: for csv_file in csv: output += cls._from_csv(csv_file, **kwargs) + if not output: + msg = ( + "No valid Detection was found in the given csv file(s).\n" + "Tip: you can specify the used separator with the sep parameter." + ) + raise ValueError(msg) + return output diff --git a/tests/_static/aplose_result.csv b/tests/_static/aplose_result.csv index 6fe60a84..06b65307 100644 --- a/tests/_static/aplose_result.csv +++ b/tests/_static/aplose_result.csv @@ -1,4 +1,4 @@ -dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,annotation,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan +dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,label,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan great_tit,990694,586654,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,vashti,NOVICE,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,WEAK,Sure,1/1,great tits |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,False great_tit,990694,586655,,1.412,3.651,2512.0,15661.0,2512.0,15661.0,bird,vashti,NOVICE,2021-01-01T00:00:01.412+00:00,2021-01-01T00:00:03.651+00:00,1,BOX,Not sure,0/1,fluffy-backed tit-babbler |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,False,False great_tit,990694,586673,,10.0,12.0,12000.0,13000.0,12000.0,13000.0,rain,heartleap,,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,BOX,,,,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,True @@ -9,3 +9,4 @@ great_tit,990694,586710,586655,1.412,3.651,2512.0,15660.0,2512.0,15660.0,bird,lo great_tit,994410,586671,,0.0,20.0,0.0,24000.0,0.0,24000.0,car,bunyan,EXPERT,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,0,WEAK,Sure,1/1,,MULTIPLE,,,,,,,,,,,,,,VERIFICATION,, great_tit,994410,586672,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,bunyan,EXPERT,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,0,WEAK,Sure,1/1,,MULTIPLE,,,,,,,,,,,,,,VERIFICATION,, ,,,,,,,,0.0,24000.0,,,,2021-01-01T00:01:18.218+00:00,2021-01-01T00:01:38.218+00:00,,,,,,,,,,,,,,,,,,,,,, +great_tit,990694,,,,,,,,,,heartleap,,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,,,,,weak comment |- heartleap,,,,,,,,,,,,,,,,, diff --git a/tests/_static/retro_compatibility_result.csv b/tests/_static/retro_compatibility_result.csv new file mode 100644 index 00000000..63e3f97b --- /dev/null +++ b/tests/_static/retro_compatibility_result.csv @@ -0,0 +1,2 @@ +dataset,filename,annotation_id,is_update_of_id,start_time,end_time,start_frequency,end_frequency,min_frequency,max_frequency,annotation,annotator,annotator_expertise,start_datetime,end_datetime,is_box,type,confidence_indicator_label,confidence_indicator_level,comments,signal_quantity,signal_is_intensity_too_low,signal_does_overlap_other_signals,signal_start_frequency,signal_end_frequency,signal_relative_min_frequency_count,signal_relative_max_frequency_count,signal_steps_count,signal_has_harmonics,signal_trend,signal_sidebands,signal_subharmonics,signal_frequency_jumps,signal_deterministic_chaos,created_at_phase,lookaftering,bunyan +great_tit,990694,586654,,0.0,20.0,0.0,24000.0,0.0,24000.0,bird,vashti,NOVICE,2021-01-01T00:00:00.000+00:00,2021-01-01T00:00:20.000+00:00,0,WEAK,Sure,1/1,great tits |- vashti,MULTIPLE,,,,,,,,,,,,,,ANNOTATION,True,False diff --git a/tests/test_detection.py b/tests/test_detection.py index a5f44636..3c5289ed 100644 --- a/tests/test_detection.py +++ b/tests/test_detection.py @@ -1,9 +1,11 @@ +import logging from contextlib import AbstractContextManager, nullcontext from pathlib import Path from typing import Any, Literal import numpy as np import pytest +from _pytest.logging import LogCaptureFixture from matplotlib.axes import Axes from matplotlib.patches import Rectangle from pandas import DataFrame, Timedelta, Timestamp @@ -235,10 +237,14 @@ def test_confidence_indicator_from_relative_level_string( assert ci.maximum_level == e.maximum_level -def test_detections_from_csv() -> None: - detections = Detection.from_csv( - csv=Path(__file__).parent / "_static" / "aplose_result.csv", - ) +def test_detections_from_csv(caplog: LogCaptureFixture) -> None: + with caplog.at_level(logging.WARNING): + detections = Detection.from_csv( + csv=Path(__file__).parent / "_static" / "aplose_result.csv", + ) + + # Failed parsed line should be logged (weak comment at idx 10) + assert "[10]" in caplog.text minimal_detection = next(d for d in detections if d.label is None) detections = [d for d in detections if d is not minimal_detection] @@ -336,6 +342,21 @@ def test_detections_from_csv() -> None: assert len(minimal_detection.verifications) == 0 +def test_detections_from_csv_raises_if_no_detection( + monkeypatch: pytest.MonkeyPatch, +) -> None: + def mock_from_dict(*args, **kwargs) -> None: # noqa: ANN002, ANN003 + msg = r"Mock a TypeError while trying to parse a Detection object." + raise TypeError(msg) + + monkeypatch.setattr(Detection, "from_dict", mock_from_dict) + + with pytest.raises(ValueError, match="No valid Detection was found"): + Detection.from_csv( + csv=Path(__file__).parent / "_static/minimal_detector_result.csv", + ) + + def test_detections_to_dict(tmp_path: Path) -> None: for result_file in ("aplose_result.csv", "minimal_detector_result.csv"): detections = Detection.from_csv( @@ -393,6 +414,14 @@ def test_minimal_detector_result_file() -> None: assert len(detections) == 2 +def test_retro_compatibility() -> None: + detections = Detection.from_csv( + csv=Path(__file__).parent / "_static/retro_compatibility_result.csv", + ) + + assert detections[0].label == "bird" + + def test_detections_from_csv_list() -> None: detections = Detection.from_csv( [