diff --git a/bids2table/_bids2table.py b/bids2table/_bids2table.py index 03c7352..7c03909 100644 --- a/bids2table/_bids2table.py +++ b/bids2table/_bids2table.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Optional +from typing import Any, Dict, Optional import pandas as pd from elbow.builders import build_parquet, build_table @@ -8,6 +8,7 @@ from elbow.typing import StrOrPath from elbow.utils import setup_logging +from bids2table import exceptions from bids2table.extractors.bids import extract_bids_subdir from bids2table.helpers import flat_to_multi_columns @@ -25,6 +26,7 @@ def bids2table( worker_id: Optional[int] = None, max_failures: Optional[int] = 0, return_df: bool = True, + filters: Optional[Dict[str, Any]] = None, ) -> Optional[pd.DataFrame]: """ Index a BIDS dataset directory and load as a pandas DataFrame. @@ -44,6 +46,8 @@ def bids2table( overwrite. max_failures: number of extract failures to tolerate. return_df: whether to return the dataframe or just build the persistent index. + filters: optional dictionary of filters to apply to the index. Keys are + column names and values are values or lists of values to keep. Returns: A DataFrame containing the BIDS Index. @@ -75,7 +79,7 @@ def bids2table( else: logging.info("Found cached index %s; nothing to do", output) df = None - return df + return _filter(df, filters) if not persistent: logging.info("Building index in memory") @@ -85,7 +89,7 @@ def bids2table( max_failures=max_failures, ) df = flat_to_multi_columns(df) - return df + return _filter(df, filters) logging.info("Building persistent Parquet index") build_parquet( @@ -99,7 +103,7 @@ def bids2table( max_failures=max_failures, ) df = load_index(output) if return_df else None - return df + return _filter(df, filters) def load_index( @@ -112,3 +116,31 @@ def load_index( if split_columns: df = flat_to_multi_columns(df, sep=sep) return df + + +def _filter(df: pd.DataFrame, filters: Optional[Dict[str, Any]]) -> pd.DataFrame: + """ + Filter a pandas DataFrame based on a dictionary of filters. + + Args: + df: The bids2table DataFrame to filter. + filters: A dictionary of filters to apply to the DataFrame. Format must be + either a single value or a list of values. If None, does not filter. + + Returns: + pd.DataFrame: The filtered DataFrame. + """ + if filters is None: + return df + + for key, value in filters.items(): + if not isinstance(value, list): + value = [value] + try: + df = df[df["entities"][key].isin(value)] + except KeyError as exc_info: + raise exceptions.InvalidFilterError( + f"Invalid filter: {key} is not a valid column." + ) from exc_info + + return df diff --git a/bids2table/exceptions.py b/bids2table/exceptions.py new file mode 100644 index 0000000..9730c2a --- /dev/null +++ b/bids2table/exceptions.py @@ -0,0 +1,3 @@ +class InvalidFilterError(Exception): + """Raised when a filter is invalid.""" + pass \ No newline at end of file diff --git a/example/bids-examples b/example/bids-examples index bc36231..75968b9 160000 --- a/example/bids-examples +++ b/example/bids-examples @@ -1 +1 @@ -Subproject commit bc36231fc322c6fded1dd7146617cbce088ae30e +Subproject commit 75968b92844d0ecb636f7237763fec4e1516a61d diff --git a/example/example.ipynb b/example/example.ipynb index ecc45d6..79203f1 100644 --- a/example/example.ipynb +++ b/example/example.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -17,7 +17,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -39,20 +39,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 15, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "174it [00:01, 103.35it/s, tot=174, good=174, rec=2124, err=0]\n", - "193it [00:01, 100.70it/s, tot=193, good=193, rec=2553, err=0]\n", - "194it [00:01, 98.60it/s, tot=194, good=194, rec=2596, err=0]\n", - "219it [00:02, 101.11it/s, tot=219, good=219, rec=2914, err=0]\n" - ] - } - ], + "outputs": [], "source": [ "df = bids2table(root=\"bids-examples\", persistent=True, overwrite=True, workers=4)" ] @@ -71,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -79,10 +68,10 @@ "output_type": "stream", "text": [ "total 1928\n", - "-rw------- 1 clane staff 208K Jun 22 08:49 part-20230622084926-0000-of-0004.parquet\n", - "-rw------- 1 clane staff 236K Jun 22 08:49 part-20230622084926-0001-of-0004.parquet\n", - "-rw------- 1 clane staff 212K Jun 22 08:49 part-20230622084926-0002-of-0004.parquet\n", - "-rw------- 1 clane staff 142K Jun 22 08:49 part-20230622084926-0003-of-0004.parquet\n" + "-rw-------@ 1 reinder.vosdewael staff 208K Jul 11 09:45 part-20230711094528-0000-of-0004.parquet\n", + "-rw-------@ 1 reinder.vosdewael staff 212K Jul 11 09:45 part-20230711094528-0002-of-0004.parquet\n", + "-rw-------@ 1 reinder.vosdewael staff 235K Jul 11 09:45 part-20230711094528-0001-of-0004.parquet\n", + "-rw-------@ 1 reinder.vosdewael staff 141K Jul 11 09:45 part-20230711094528-0003-of-0004.parquet\n" ] } ], @@ -109,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -220,9 +209,9 @@ " .nii.gz\n", " {}\n", " None\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", " 1\n", @@ -262,9 +251,9 @@ " .nii.gz\n", " {}\n", " None\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", " 2\n", @@ -304,56 +293,56 @@ " .nii.gz\n", " {}\n", " {'RepetitionTime': 2.0, 'TaskName': 'mixed eve...\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", "\n", "" ], "text/plain": [ - " dataset \n", + " dataset \\\n", " dataset dataset_path \n", - "0 ds002 bids-examples/ds002 \\\n", + "0 ds002 bids-examples/ds002 \n", "1 ds002 bids-examples/ds002 \n", "2 ds002 bids-examples/ds002 \n", "\n", - " entities \n", + " entities \\\n", " dataset_description sub ses sample \n", - "0 {'BIDSVersion': '1.0.0', 'License': 'This data... 06 None None \\\n", + "0 {'BIDSVersion': '1.0.0', 'License': 'This data... 06 None None \n", "1 {'BIDSVersion': '1.0.0', 'License': 'This data... 06 None None \n", "2 {'BIDSVersion': '1.0.0', 'License': 'This data... 06 None None \n", "\n", - " \n", + " \\\n", " task acq ce trc stain rec dir run mod echo \n", - "0 None None None None None None None NaN None NaN \\\n", + "0 None None None None None None None NaN None NaN \n", "1 None None None None None None None NaN None NaN \n", "2 mixedeventrelatedprobe None None None None None None 2.0 None NaN \n", "\n", - " \n", + " \\\n", " flip inv mt part proc hemi space split recording chunk atlas res \n", - "0 NaN NaN None None None None None NaN None NaN None None \\\n", + "0 NaN NaN None None None None None NaN None NaN None None \n", "1 NaN NaN None None None None None NaN None NaN None None \n", "2 NaN NaN None None None None None NaN None NaN None None \n", "\n", - " \n", + " \\\n", " den label desc datatype suffix ext extra_entities \n", - "0 None None None anat T1w .nii.gz {} \\\n", + "0 None None None anat T1w .nii.gz {} \n", "1 None None None anat inplaneT2 .nii.gz {} \n", "2 None None None func bold .nii.gz {} \n", "\n", - " metadata \n", + " metadata \\\n", " sidecar \n", - "0 None \\\n", + "0 None \n", "1 None \n", "2 {'RepetitionTime': 2.0, 'TaskName': 'mixed eve... \n", "\n", " file \n", " file_path link_target mod_time \n", - "0 /Users/clane/Projects/ScalableQC/code/bids2tab... None 1.685583e+09 \n", - "1 /Users/clane/Projects/ScalableQC/code/bids2tab... None 1.685583e+09 \n", - "2 /Users/clane/Projects/ScalableQC/code/bids2tab... None 1.685583e+09 " + "0 /Users/reinder.vosdewael/repositories/bids2tab... None 1.689004e+09 \n", + "1 /Users/reinder.vosdewael/repositories/bids2tab... None 1.689004e+09 \n", + "2 /Users/reinder.vosdewael/repositories/bids2tab... None 1.689004e+09 " ] }, "execution_count": 5, @@ -381,7 +370,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -464,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -575,9 +564,9 @@ " .nii.gz\n", " {}\n", " {'CogAtlasID': 'https://www.cognitiveatlas.org...\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", " 1841\n", @@ -617,9 +606,9 @@ " .tsv.gz\n", " {}\n", " {'StartTime': 0, 'SamplingFrequency': 100, 'Co...\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", " 1837\n", @@ -659,65 +648,65 @@ " .nii.gz\n", " {}\n", " {'CogAtlasID': 'https://www.cognitiveatlas.org...\n", - " /Users/clane/Projects/ScalableQC/code/bids2tab...\n", + " /Users/reinder.vosdewael/repositories/bids2tab...\n", " None\n", - " 1.685583e+09\n", + " 1.689004e+09\n", " \n", " \n", "\n", "" ], "text/plain": [ - " dataset \n", + " dataset \\\n", " dataset dataset_path \n", - "1839 7t_trt bids-examples/7t_trt \\\n", + "1839 7t_trt bids-examples/7t_trt \n", "1841 7t_trt bids-examples/7t_trt \n", "1837 7t_trt bids-examples/7t_trt \n", "\n", - " entities \n", + " entities \\\n", " dataset_description sub ses sample task \n", - "1839 {'BIDSVersion': '1.8.0', 'Name': '7t_trt'} 01 1 None rest \\\n", + "1839 {'BIDSVersion': '1.8.0', 'Name': '7t_trt'} 01 1 None rest \n", "1841 {'BIDSVersion': '1.8.0', 'Name': '7t_trt'} 01 1 None rest \n", "1837 {'BIDSVersion': '1.8.0', 'Name': '7t_trt'} 01 1 None rest \n", "\n", - " \n", + " \\\n", " acq ce trc stain rec dir run mod echo flip inv mt \n", - "1839 fullbrain None None None None None 1.0 None NaN NaN NaN None \\\n", + "1839 fullbrain None None None None None 1.0 None NaN NaN NaN None \n", "1841 fullbrain None None None None None 1.0 None NaN NaN NaN None \n", "1837 fullbrain None None None None None 2.0 None NaN NaN NaN None \n", "\n", - " \n", + " \\\n", " part proc hemi space split recording chunk atlas res den label \n", - "1839 None None None None NaN None NaN None None None None \\\n", + "1839 None None None None NaN None NaN None None None None \n", "1841 None None None None NaN None NaN None None None None \n", "1837 None None None None NaN None NaN None None None None \n", "\n", - " \n", + " \\\n", " desc datatype suffix ext extra_entities \n", - "1839 None func bold .nii.gz {} \\\n", + "1839 None func bold .nii.gz {} \n", "1841 None func physio .tsv.gz {} \n", "1837 None func bold .nii.gz {} \n", "\n", - " metadata \n", + " metadata \\\n", " sidecar \n", - "1839 {'CogAtlasID': 'https://www.cognitiveatlas.org... \\\n", + "1839 {'CogAtlasID': 'https://www.cognitiveatlas.org... \n", "1841 {'StartTime': 0, 'SamplingFrequency': 100, 'Co... \n", "1837 {'CogAtlasID': 'https://www.cognitiveatlas.org... \n", "\n", - " file \n", + " file \\\n", " file_path link_target \n", - "1839 /Users/clane/Projects/ScalableQC/code/bids2tab... None \\\n", - "1841 /Users/clane/Projects/ScalableQC/code/bids2tab... None \n", - "1837 /Users/clane/Projects/ScalableQC/code/bids2tab... None \n", + "1839 /Users/reinder.vosdewael/repositories/bids2tab... None \n", + "1841 /Users/reinder.vosdewael/repositories/bids2tab... None \n", + "1837 /Users/reinder.vosdewael/repositories/bids2tab... None \n", "\n", " \n", " mod_time \n", - "1839 1.685583e+09 \n", - "1841 1.685583e+09 \n", - "1837 1.685583e+09 " + "1839 1.689004e+09 \n", + "1841 1.689004e+09 \n", + "1837 1.689004e+09 " ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -730,6 +719,274 @@ "df.head(3)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Filtering Rows\n", + "\n", + "A subset of files can be retrieved using the filters argument on `bids2table`. This argument takes a dictionary of entity names and values. The returned table will only contain rows where the entity values match the filter values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datasetentitiesmetadatafile
datasetdataset_pathdataset_descriptionsubsessampletaskacqcetrcstainrecdirrunmodechoflipinvmtpartprochemispacesplitrecordingchunkatlasresdenlabeldescdatatypesuffixextextra_entitiessidecarfile_pathlink_targetmod_time
0ds002bids-examples/ds002{'BIDSVersion': '1.0.0', 'License': 'This data...06NoneNoneNoneNoneNoneNoneNoneNoneNoneNaNNoneNaNNaNNaNNoneNoneNoneNoneNoneNaNNoneNaNNoneNoneNoneNoneNoneanatT1w.nii.gz{}None/Users/reinder.vosdewael/repositories/bids2tab...None1.689004e+09
14ds002bids-examples/ds002{'BIDSVersion': '1.0.0', 'License': 'This data...17NoneNoneNoneNoneNoneNoneNoneNoneNoneNaNNoneNaNNaNNaNNoneNoneNoneNoneNoneNaNNoneNaNNoneNoneNoneNoneNoneanatT1w.nii.gz{}None/Users/reinder.vosdewael/repositories/bids2tab...None1.689004e+09
29ds002bids-examples/ds002{'BIDSVersion': '1.0.0', 'License': 'This data...05NoneNoneNoneNoneNoneNoneNoneNoneNoneNaNNoneNaNNaNNaNNoneNoneNoneNoneNoneNaNNoneNaNNoneNoneNoneNoneNoneanatT1w.nii.gz{}None/Users/reinder.vosdewael/repositories/bids2tab...None1.689004e+09
\n", + "
" + ], + "text/plain": [ + " dataset \\\n", + " dataset dataset_path \n", + "0 ds002 bids-examples/ds002 \n", + "14 ds002 bids-examples/ds002 \n", + "29 ds002 bids-examples/ds002 \n", + "\n", + " entities \\\n", + " dataset_description sub ses sample \n", + "0 {'BIDSVersion': '1.0.0', 'License': 'This data... 06 None None \n", + "14 {'BIDSVersion': '1.0.0', 'License': 'This data... 17 None None \n", + "29 {'BIDSVersion': '1.0.0', 'License': 'This data... 05 None None \n", + "\n", + " \\\n", + " task acq ce trc stain rec dir run mod echo flip inv mt \n", + "0 None None None None None None None NaN None NaN NaN NaN None \n", + "14 None None None None None None None NaN None NaN NaN NaN None \n", + "29 None None None None None None None NaN None NaN NaN NaN None \n", + "\n", + " \\\n", + " part proc hemi space split recording chunk atlas res den label \n", + "0 None None None None NaN None NaN None None None None \n", + "14 None None None None NaN None NaN None None None None \n", + "29 None None None None NaN None NaN None None None None \n", + "\n", + " metadata \\\n", + " desc datatype suffix ext extra_entities sidecar \n", + "0 None anat T1w .nii.gz {} None \n", + "14 None anat T1w .nii.gz {} None \n", + "29 None anat T1w .nii.gz {} None \n", + "\n", + " file \\\n", + " file_path link_target \n", + "0 /Users/reinder.vosdewael/repositories/bids2tab... None \n", + "14 /Users/reinder.vosdewael/repositories/bids2tab... None \n", + "29 /Users/reinder.vosdewael/repositories/bids2tab... None \n", + "\n", + " \n", + " mod_time \n", + "0 1.689004e+09 \n", + "14 1.689004e+09 \n", + "29 1.689004e+09 " + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_filtered = bids2table(\"bids-examples\", filters={\"datatype\": \"anat\", \"suffix\": \"T1w\"})\n", + "df_filtered.head(3)" + ] + }, { "attachments": {}, "cell_type": "markdown", @@ -742,7 +999,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -783,7 +1040,7 @@ "dtype: int64" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -803,7 +1060,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -817,7 +1074,7 @@ "dtype: int64" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -838,7 +1095,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1421,7 +1678,7 @@ "synthetic/derivatives/fmriprep 150 60" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -1444,7 +1701,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -1458,7 +1715,7 @@ "positional arguments:\n", " ROOT Path to BIDS dataset\n", "\n", - "optional arguments:\n", + "options:\n", " -h, --help show this help message and exit\n", " --output OUTPUT, -o OUTPUT\n", " Path to output parquet dataset directory (default:\n", @@ -1491,17 +1748,17 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "174it [00:01, 100.91it/s, tot=174, good=174, rec=2124, err=0]\n", - "193it [00:01, 97.64it/s, tot=193, good=193, rec=2553, err=0] \n", - "194it [00:02, 95.48it/s, tot=194, good=194, rec=2596, err=0]\n", - "219it [00:02, 97.64it/s, tot=219, good=219, rec=2914, err=0] \n" + "174it [00:03, 45.18it/s, tot=174, good=174, rec=2124, err=0]\n", + "193it [00:04, 46.65it/s, tot=193, good=193, rec=2553, err=0]\n", + "194it [00:04, 45.92it/s, tot=194, good=194, rec=2596, err=0]\n", + "219it [00:04, 47.53it/s, tot=219, good=219, rec=2914, err=0]\n" ] } ], @@ -1545,7 +1802,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.3" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/tests/unit/test__bids2table.py b/tests/unit/test__bids2table.py new file mode 100644 index 0000000..25c9ae4 --- /dev/null +++ b/tests/unit/test__bids2table.py @@ -0,0 +1,75 @@ +""" Unit tests for the _bids2table module. """ +# pylint: disable=redefined-outer-name +# pylint: disable=protected-access +import pandas as pd +import pytest + +from bids2table import _bids2table, exceptions + + +@pytest.fixture +def test_df() -> pd.DataFrame: + """Returns a test DataFrame with entities and dataset columns.""" + entities = pd.DataFrame( + { + "subject": ["sub-01", "sub-02", "sub-03", "sub-03"], + "session": ["ses-01", "ses-01", "ses-01", "ses-02"], + } + ) + + dataset = pd.DataFrame( + { + "dataset": ["ds-01", "ds-01", "ds-01", "ds-01"], + } + ) + + return pd.concat( + [entities, dataset], + axis=1, + keys=["entities", "dataset"], + ) + + +def test_filter_one_value(test_df: pd.DataFrame) -> None: + """Test filtering by a single value.""" + filters = {"subject": "sub-01"} + expected = test_df.iloc[[0]] + + actual = _bids2table._filter(test_df, filters) + + assert actual.equals(expected) + + +def test_filter_list_values(test_df: pd.DataFrame) -> None: + """Test filtering by a list of values.""" + filters = {"subject": ["sub-01", "sub-02"]} + expected = test_df.iloc[[0, 1]] + + actual = _bids2table._filter(test_df, filters) + + assert actual.equals(expected) + + +def test_filter_multiple_values(test_df: pd.DataFrame) -> None: + """Test filtering by multiple values.""" + filters = {"subject": "sub-03", "session": "ses-01"} + expected = test_df.iloc[[2]] + + actual = _bids2table._filter(test_df, filters) + + assert actual.equals(expected) + + +def test_filter_no_values(test_df: pd.DataFrame) -> None: + """Test filtering with no filters.""" + actual = _bids2table._filter(test_df, None) + + assert actual.equals(test_df) + + +def test_filter_invalid_key(test_df: pd.DataFrame) -> None: + """Test filtering with an invalid key.""" + filters = {"invalid_key": "sub-01"} + + with pytest.raises(exceptions.InvalidFilterError): + _bids2table._filter(test_df, filters)