Skip to content

Commit

Permalink
[ENH] add CLI, logger and type annotation (#17)
Browse files Browse the repository at this point in the history
* add cli and logger

* add type annotation

* ignore version for type annotation
  • Loading branch information
Remi-Gau authored Sep 7, 2023
1 parent b5b79f0 commit f415b20
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 33 deletions.
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ repos:
- id: pretty-format-toml
args: [--autofix, --indent, '4']

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
hooks:
- id: mypy
additional_dependencies: [types-all]
args: [--config-file=pyproject.toml]

- repo: https://github.com/pyCQA/flake8
rev: 6.1.0
hooks:
Expand Down
29 changes: 21 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ git clone https://github.com/bids-standard/eye2bids.git
cd eye2bids
pip install .
```
foo

## Using eye2bids

- Supporeted Input data:
Expand All @@ -50,10 +50,28 @@ foo
### Run code

```bash
python edf2bids_json.py
eye2bids --input_file INPUT_FILE
```

[SR-Research support forum]: https://www.sr-research.com/support/forum-9.html
Usage: eye2bids [-h] [-v] --input_file INPUT_FILE [--metadata_file METADATA_FILE] [--output_dir OUTPUT_DIR] [-i] [--verbosity {0,1,2,3}]
[--input_type INPUT_TYPE]

Converts eyetracking data to a BIDS compatible format.

Options:
-h, --help show this help message and exit
-v, --version show program's version number and exit
--input_file INPUT_FILE
Path to the input file to convert.
--metadata_file METADATA_FILE
Path to the a yaml file containing extra metadata.
--output_dir OUTPUT_DIR
Path to output directory.
-i, --interactive To run in interactive mode.
--verbosity {0,1,2,3}
Verbosity level.
--input_type INPUT_TYPE
Input type if it cannot be determined from input_file.

## Docker

Expand All @@ -79,8 +97,3 @@ python tools/download_test_data.py
```

## Related projects


f

ff
59 changes: 59 additions & 0 deletions eye2bids/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

import sys
from pathlib import Path
from typing import Sequence

from eye2bids._parser import global_parser
from eye2bids.edf2bids import edf2bids
from eye2bids.logger import eye2bids_logger

e2b_log = eye2bids_logger()


def set_verbosity(verbosity: int | list[int]) -> None:
"""Set verbosity level."""
if isinstance(verbosity, list):
verbosity = verbosity[0]
if verbosity == 0:
e2b_log.setLevel("ERROR")
elif verbosity == 1:
e2b_log.setLevel("WARNING")
elif verbosity == 2:
e2b_log.setLevel("INFO")
elif verbosity == 3:
e2b_log.setLevel("DEBUG")


def cli(argv: Sequence[str] = sys.argv) -> None:
"""Entry point."""
parser = global_parser()
parser.add_argument(
"--input_type",
type=str,
help="""
Input type if it cannot be determined from input_file.
""",
required=False,
default=None,
)

args, _ = parser.parse_known_args(argv[1:])

input_file = args.input_file
input_file = Path(input_file).resolve()

metadata_file = args.metadata_file
if metadata_file is not None:
metadata_file = metadata_file[0]

output_dir = args.output_dir

set_verbosity(args.verbosity)

edf2bids(
input_file=input_file,
metadata_file=metadata_file,
output_dir=output_dir,
interactive=args.interactive,
)
69 changes: 63 additions & 6 deletions eye2bids/_parser.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,73 @@
import argparse
from argparse import ArgumentParser
from pathlib import Path

from rich_argparse import RichHelpFormatter

def global_parser():
from eye2bids._version import __version__


def global_parser() -> ArgumentParser:
"""Parse command line arguments.
Returns
-------
parser: An ArgumentParser object.
"""
parser = argparse.ArgumentParser()
parser.add_argument("--input_file", type=str)
parser.add_argument("--metadata_file", type=str)
parser.add_argument("--output_dir", type=str)
parser = ArgumentParser(
prog="eye2bids",
description="Converts eyetracking data to a BIDS compatible format.",
formatter_class=RichHelpFormatter,
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"{__version__}",
)
parser.add_argument(
"--input_file",
type=str,
help="""
Path to the input file to convert.
""",
required=True,
)
parser.add_argument(
"--metadata_file",
type=str,
help="""
Path to the a yaml file containing extra metadata.
""",
required=False,
default=None,
)
parser.add_argument(
"--output_dir",
type=str,
help="""
Path to output directory.
""",
required=False,
default=Path.cwd(),
)
parser.add_argument(
"-i",
"--interactive",
help="""
To run in interactive mode.
""",
action="store_true",
)
parser.add_argument(
"--verbosity",
help="""
Verbosity level.
""",
required=False,
choices=[0, 1, 2, 3],
default=2,
type=int,
nargs=1,
)
return parser
45 changes: 27 additions & 18 deletions eye2bids/edf2bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
import numpy as np
import pandas as pd
import yaml
from rich.prompt import Prompt
from yaml.loader import SafeLoader

from eye2bids._parser import global_parser
from eye2bids.logger import eye2bids_logger

e2b_log = eye2bids_logger()


def _check_inputs(
Expand All @@ -21,32 +25,30 @@ def _check_inputs(
) -> tuple[Path, Path | None, Path]:
if input_file is None:
if interactive:
input_file = input("Enter the edf file path: ")
input_file = Prompt.ask("Enter the edf file path")
else:
raise FileNotFoundError("No input file specified")
if isinstance(input_file, str):
input_file = Path(input_file)
if input_file.exists():
print("The file exists")
e2b_log.info(f"file found: {input_file}")
else:
raise FileNotFoundError(f"No such file: {input_file}")

if metadata_file is None and interactive:
print(
e2b_log.info(
"""Load the metadata.yml file with the additional metadata.\n
This file must contain at least the additional REQUIRED metadata
in the format specified in the BIDS specification.\n
Please enter the required metadata manually
before loading the file in a next step."""
in the format specified in the BIDS specification.\n"""
)
metadata_file = input("Enter the file path to the metadata.yml file: ")
metadata_file = Prompt.ask("Enter the file path to the metadata.yml file")
if isinstance(metadata_file, str):
metadata_file = Path(metadata_file)
if isinstance(metadata_file, str):
metadata_file = Path(metadata_file)
if isinstance(metadata_file, Path):
if metadata_file.exists():
print("The file exists")
e2b_log.info(f"file found: {metadata_file}")
else:
raise FileNotFoundError(f"No such file: {metadata_file}")

Expand All @@ -66,7 +68,10 @@ def _check_edf2asc_present() -> bool:
subprocess.run(["edf2asc"])
return True
except FileNotFoundError:
print("edf2asc not found in path")
e2b_log.error(
"""edf2asc not found in path.
Make sure to install it from https://www.sr-research.com/."""
)
return False


Expand All @@ -76,7 +81,7 @@ def _convert_edf_to_asc(input_file: str | Path) -> Path:
return Path(input_file).with_suffix(".asc")


def _calibrations(df):
def _calibrations(df: pd.DataFrame) -> pd.DataFrame:
return df[df[3] == "CALIBRATION"]


Expand All @@ -88,7 +93,7 @@ def _extract_CalibrationCount(df: pd.DataFrame) -> int:
return len(_calibrations(df))


def _get_calibration_positions(df: pd.DataFrame) -> np.array:
def _get_calibration_positions(df: pd.DataFrame) -> list[int]:
return (
np.array(df[df[2] == "VALIDATE"][8].str.split(",", expand=True))
.astype(int)
Expand All @@ -100,7 +105,7 @@ def _extract_CalibrationPosition(df: pd.DataFrame) -> list[list[int]]:
cal_pos = _get_calibration_positions(df)
cal_num = len(cal_pos) // _extract_CalibrationCount(df)

CalibrationPosition = []
CalibrationPosition: list[list[int]] = []

if len(cal_pos) == 0:
return CalibrationPosition
Expand All @@ -120,10 +125,11 @@ def _extract_CalibrationUnit(df: pd.DataFrame) -> str:
.iloc[0:1, 0:1]
.to_string(header=False, index=False)
)
if cal_unit in ["cm", "mm"]:
return "cm"
elif cal_unit == "pix.":
if cal_unit == "pix.":
return "pixel"
elif cal_unit in ["cm", "mm"]:
return cal_unit
return ""


def _extract_EyeTrackingMethod(events: list[str]) -> str:
Expand All @@ -138,7 +144,7 @@ def _extract_EyeTrackingMethod(events: list[str]) -> str:
)


def _validations(df: pd.DataFrame):
def _validations(df: pd.DataFrame) -> pd.DataFrame:
return df[df[3] == "VALIDATION"]


Expand Down Expand Up @@ -190,6 +196,7 @@ def _extract_RecordedEye(df: pd.DataFrame) -> str:
return "Right"
elif eye == "LR":
return "Both"
return ""


def _extract_ScreenResolution(df: pd.DataFrame) -> list[int]:
Expand All @@ -205,7 +212,7 @@ def _extract_ScreenResolution(df: pd.DataFrame) -> list[int]:
)


def _extract_TaskName(events: list[str]):
def _extract_TaskName(events: list[str]) -> str:
return (
" ".join([ts for ts in events if ts.startswith("** RECORDED BY")])
.replace("** RECORDED BY ", "")
Expand Down Expand Up @@ -235,7 +242,7 @@ def edf2bids(
metadata_file: str | Path | None = None,
output_dir: str | Path | None = None,
interactive: bool = False,
):
) -> None:
"""Convert edf to tsv + json."""
if not _check_edf2asc_present():
return
Expand Down Expand Up @@ -303,6 +310,7 @@ def edf2bids(

with open(output_dir / "eyetrack.json", "w") as outfile:
json.dump(eyetrack_json, outfile, indent=4)
e2b_log.info(f"file generated: {output_dir / 'eyetrack.json'}")

# Events.json Metadata
events_json = {
Expand All @@ -319,6 +327,7 @@ def edf2bids(

with open(output_dir / "events.json", "w") as outfile:
json.dump(events_json, outfile, indent=4)
e2b_log.info(f"file generated: {output_dir / 'events.json'}")


if __name__ == "__main__":
Expand Down
20 changes: 20 additions & 0 deletions eye2bids/logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""General logger for the eye2bids package."""
from __future__ import annotations

import logging

from rich.logging import RichHandler


def eye2bids_logger(log_level: str = "INFO") -> logging.Logger:
"""Create a logger for the eye2bids package."""
FORMAT = "%(message)s"

logging.basicConfig(
level=log_level,
format=FORMAT,
datefmt="[%X]",
handlers=[RichHandler()],
)

return logging.getLogger("cohort_creator")
Loading

0 comments on commit f415b20

Please sign in to comment.