From 0d7bd0e3f7a413b24dcb0edcecfb8bd059d1b47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Cl=C3=A9net?= <117362283+bclenet@users.noreply.github.com> Date: Wed, 10 Jan 2024 16:55:38 +0100 Subject: [PATCH] New command line tools (#138) * [BUG] inside unit_tests workflow * [DOC] runner help * Creating entry-points for the project * [DOC] command line tools * [DOC] command line tools * Adding a tester command line tool --- INSTALL.md | 32 +++++++-- docs/data.md | 27 +++---- docs/description.md | 11 +-- docs/running.md | 18 +++-- docs/status.md | 9 ++- docs/testing.md | 24 ++++++- narps_open/data/description/__main__.py | 96 +++++++++++++------------ narps_open/data/results/__main__.py | 55 +++++++------- narps_open/runner.py | 7 +- narps_open/tester.py | 29 ++++++++ narps_open/utils/status.py | 11 ++- setup.py | 11 ++- 12 files changed, 224 insertions(+), 106 deletions(-) create mode 100644 narps_open/tester.py diff --git a/INSTALL.md b/INSTALL.md index be1b5939..9a429f00 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -74,12 +74,36 @@ cd /home/neuro/code/ pip install . ``` -Finally, you are able to run pipelines : +Finally, you are able to use the scripts of the project : + +* `narps_open_runner`: run pipelines +* `narps_open_tester`: run a pipeline and test its results against original ones from the team +* `narps_description`: get the textual description made by a team +* `narps_results`: download the original results from teams +* `narps_open_status`: get status information about the development process of the pipelines ```bash -python narps_open/runner.py - usage: runner.py [-h] -t TEAM (-r RSUBJECTS | -s SUBJECTS [SUBJECTS ...] | -n NSUBJECTS) [-g | -f] [-c] +# Run the pipeline for team 2T6S, with 40 subjects +narps_open_runner -t 2T6S -n 40 + +# Run the pipeline for team 08MQ, compare results with original ones, +# and produces a report with correlation values. +narps_open_tester -t 08MQ + +# Get the description of team C88N in markdown formatting +narps_description -t C88N --md + +# Download the results from all teams +narps_results -a + +# Get the pipeline work status information in json formatting +narps_open_status --json ``` > [!NOTE] -> For further information, read this documentation page [docs/running.md](docs/running.md). +> For further information about these command line tools, read the corresponding documentation pages. +> * `narps_open_runner` : [docs/running.md](docs/running.md) +> * `narps_open_tester` : [docs/testing.md](docs/testing.md#command-line-tool) +> * `narps_description` : [docs/description.md](docs/description.md) +> * `narps_results` : [docs/data.md](docs/data.md#results-from-narps-teams) +> * `narps_open_status` : [docs/status.md](docs/status.md) diff --git a/docs/data.md b/docs/data.md index c5b55fba..3a68b32e 100644 --- a/docs/data.md +++ b/docs/data.md @@ -67,28 +67,31 @@ for team in teams: collection.rectify() # Rectified versions are created ``` +> [!TIP] +> In the following examples, use `narps_results` or `python narps_open/data/results` indifferently to launch the command line tool. + ```bash # From the command line -$ python narps_open/data/results -h -usage: results [-h] (-t TEAMS [TEAMS ...] | -a) [-r] +narps_results -h + usage: results [-h] (-t TEAMS [TEAMS ...] | -a) [-r] -Get Neurovault collection of results from NARPS teams. + Get Neurovault collection of results from NARPS teams. -options: - -h, --help show this help message and exit - -t TEAMS [TEAMS ...], --teams TEAMS [TEAMS ...] - a list of team IDs - -a, --all download results from all teams - -r, --rectify rectify the results + options: + -h, --help show this help message and exit + -t TEAMS [TEAMS ...], --teams TEAMS [TEAMS ...] + a list of team IDs + -a, --all download results from all teams + -r, --rectify rectify the results # Either download all collections -python narps_open/utils/results -a +narps_results -a # Or select the ones you need -python narps_open/utils/results -t 2T6S C88N L1A8 +narps_results -t 2T6S C88N L1A8 # Download and rectify the collections -python narps_open/utils/results -r -t 2T6S C88N L1A8 +narps_results -r -t 2T6S C88N L1A8 ``` The collections are also available [here](https://zenodo.org/record/3528329/) as one release on Zenodo that you can download. diff --git a/docs/description.md b/docs/description.md index ac17f588..82f78097 100644 --- a/docs/description.md +++ b/docs/description.md @@ -12,8 +12,11 @@ The class `TeamDescription` of module `narps_open.data.description` acts as a pa You can use the command-line tool as so. Option `-t` is for the team id, option `-d` allows to print only one of the sub parts of the description among : `general`, `exclusions`, `preprocessing`, `analysis`, `categorized_for_analysis`, `derived`, and `comments`. Options `--json` and `--md` allow to choose the export format you prefer between JSON and Markdown. +> [!TIP] +> In the following examples, use `narps_description` or `python narps_open/data/description` indifferently to launch the command line tool. + ```bash -python narps_open/data/description -h +narps_description -h # usage: __init__.py [-h] -t TEAM [-d {general,exclusions,preprocessing,analysis,categorized_for_analysis,derived,comments}] # # Get description of a NARPS pipeline. @@ -26,7 +29,7 @@ python narps_open/data/description -h # --json output team description as JSON # --md output team description as Markdown -python narps_open/data/description -t 2T6S --json +narps_description -t 2T6S --json # { # "general.teamID": "2T6S", # "general.NV_collection_link": "https://neurovault.org/collections/4881/", @@ -41,7 +44,7 @@ python narps_open/data/description -t 2T6S --json # "preprocessing.preprocessing_order": "We used the provided preprocessed data by fMRIPprep 1.1.4 (Esteban, Markiewicz, et al. (2018); Esteban, Blair, et al. (2018); RRID:SCR_016216), which is based on Nipype 1.1.1 (Gorgolewski et al. (2011); Gorgolewski et al. (2018); RRID:SCR_002502) and we additionally conducted a spatial smoothing using the provided preprocessed data set and SPM12. Here, we attach the preprocessing steps described in the provided data set. \nAnatomical data preprocessing\nThe T1-weighted (T1w) image was corrected for intensity non-uniformity (INU) using N4BiasFieldCorrection (Tustison et al. 2010, ANTs 2.2.0), and used as T1w-reference throughout the workflow. The T1w-reference was then skull-stripped using antsBrainExtraction.sh (ANTs 2.2.0), using OASIS as target template. Brain surfaces we # ... -python narps_open/data/description -t 2T6S -d general --json +narps_description -t 2T6S -d general --json # { # "teamID": "2T6S", # "NV_collection_link": "https://neurovault.org/collections/4881/", @@ -53,7 +56,7 @@ python narps_open/data/description -t 2T6S -d general --json # "general_comments": "NA" # } -python narps_open/data/description -t 2T6S --md +narps_description -t 2T6S --md # # NARPS team description : 2T6S # ## General # * `teamID` : 2T6S diff --git a/docs/running.md b/docs/running.md index eb614eef..b2f7da77 100644 --- a/docs/running.md +++ b/docs/running.md @@ -2,10 +2,13 @@ ## Using the runner application -The `narps_open.runner` module allows to run pipelines from the command line : +The `narps_open.runner` module allows to run pipelines from the command line. + +> [!TIP] +> In the following examples, use `narps_open_runner` or `python narps_open/runner.py` indifferently to launch the command line tool. ```bash -python narps_open/runner.py -h +narps_open_runner -h usage: runner.py [-h] -t TEAM (-r RANDOM | -s SUBJECTS [SUBJECTS ...]) [-g | -f] Run the pipelines from NARPS. @@ -19,13 +22,14 @@ python narps_open/runner.py -h -f, --first run the first levels only (preprocessing + subjects + runs) -c, --check check pipeline outputs (runner is not launched) -python narps_open/runner.py -t 2T6S -s 001 006 020 100 -python narps_open/runner.py -t 2T6S -r 4 -python narps_open/runner.py -t 2T6S -r 4 -f -python narps_open/runner.py -t 2T6S -r 4 -f -c # Check the output files without launching the runner +narps_open_runner -t 2T6S -s 001 006 020 100 +narps_open_runner -t 2T6S -r 4 +narps_open_runner -t 2T6S -r 4 -f +narps_open_runner -t 2T6S -r 4 -f -c # Check the output files without launching the runner ``` -In this usecase, the paths where to store the outputs and to the dataset are picked by the runner from the [configuration](docs/configuration.md). +> [!NOTE] +> In this usecase, the paths where to store the outputs and to the dataset are picked by the runner from the [configuration](docs/configuration.md). ## Using the `PipelineRunner` object diff --git a/docs/status.md b/docs/status.md index 28492390..d461b1ea 100644 --- a/docs/status.md +++ b/docs/status.md @@ -46,8 +46,11 @@ report.markdown() # Returns a string containing the markdown You can also use the command-line tool as so. +> [!TIP] +> In the following examples, use `narps_open_status` or `python narps_open/utils/status.py` indifferently to launch the command line tool. + ```bash -python narps_open/utils/status -h +narps_open_status -h # usage: status.py [-h] [--json | --md] # # Get a work progress status report for pipelines. @@ -57,7 +60,7 @@ python narps_open/utils/status -h # --json output the report as JSON # --md output the report as Markdown -python narps_open/utils/status --json +narps_open_status --json # { # "08MQ": { # "softwares": "FSL", @@ -83,7 +86,7 @@ python narps_open/utils/status --json # }, # ... -python narps_open/utils/status --md +narps_open_status --md # ... # | team_id | status | main software | fmriprep used ? | related issues | related pull requests | excluded from NARPS analysis | reproducibility | # | --- |:---:| --- | --- | --- | --- | --- | --- | diff --git a/docs/testing.md b/docs/testing.md index 5294ea9b..1ea3b66c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -2,6 +2,13 @@ :mega: This file describes the test suite and features for the project. +## Test dependencies + +Before using the test suite, make sure you installed all the dependencies, after step 5 of the [installation process](docs/install.md), run this command: +```bash +pip install .[tests] +``` + ## Static analysis We use [*pylint*](http://pylint.pycqa.org/en/latest/) to run static code analysis. @@ -24,7 +31,7 @@ black ./narps_open/runner.py ## Automatic tests -Use [*pytest*](https://docs.pytest.org/en/6.2.x/contents.html) to run automatic testing and its [*pytest-cov*](https://pytest-cov.readthedocs.io/en/latest/) plugin to control code coverage. Furthermore, [*pytest-helpers-namespace*](https://pypi.org/project/pytest-helpers-namespace/) enables to register helper functions. +We use [*pytest*](https://docs.pytest.org/en/6.2.x/contents.html) to run automatic testing and its [*pytest-cov*](https://pytest-cov.readthedocs.io/en/latest/) plugin to control code coverage. Furthermore, [*pytest-helpers-namespace*](https://pypi.org/project/pytest-helpers-namespace/) enables to register helper functions. > The pytest framework makes it easy to write small tests, yet scales to support complex functional testing for applications and libraries. @@ -36,6 +43,21 @@ Tests can be launched manually or while using CI (Continuous Integration). * To run a tests with a given mark 'mark' : `pytest -m 'mark'` * To create code coverage data : `coverage run -m pytest ./tests` then `coverage report` to see the code coverage result or `coverage xml` to output a .xml report file +## Command line tool + +We created the simple command line tool `narps_open_tester` to help testing the outcome of one pipeline. + +> [!WARNING] +> This command must be launched from inside the repository's root directory, because it needs to access the `tests` directory relatively to the current/working directory. + +```bash +narps_open_tester -t 08MQ +``` + +This will run the pipeline for the requested team -here 08MQ- on subsets of subjects (20, 40, 60, 80 and 108). For each subset, the outputs of the pipeline (statistical maps for each of the 9 hypotheses) will be compared with original results from the team using a Pearson correlation computation. At each step, if one of the correlation score is below the threshold (see `correlation_thresholds` defined in `narps_open/utils/configuration/testing_config.toml`), the tests ends. Otherwise, it proceeds to the next step, i.e.: the next subset of subjects. + +Once finished, a text file report (`test_pipeline-*.txt`) is created, containing all the computed correlation values. + ## Configuration files for testing * `pytest.ini` is a global configuration files for using pytest (see reference [here](https://docs.pytest.org/en/7.1.x/reference/customize.html)). It allows to [register markers](https://docs.pytest.org/en/7.1.x/example/markers.html) that help to better identify tests. Note that `pytest.ini` could be replaced by data inside `pyproject.toml` in the next versions. diff --git a/narps_open/data/description/__main__.py b/narps_open/data/description/__main__.py index e538ff4d..b6c9ead3 100644 --- a/narps_open/data/description/__main__.py +++ b/narps_open/data/description/__main__.py @@ -8,49 +8,55 @@ from narps_open.data.description import TeamDescription -# Parse arguments -parser = ArgumentParser(description='Get description of a NARPS pipeline.') -parser.add_argument('-t', '--team', type=str, required=True, - help='the team ID') -parser.add_argument('-d', '--dictionary', type=str, required=False, - choices=[ - 'general', - 'exclusions', - 'preprocessing', - 'analysis', - 'categorized_for_analysis', - 'derived', - 'comments' - ], - help='the sub dictionary of team description') -formats = parser.add_mutually_exclusive_group(required = False) -formats.add_argument('--json', action='store_true', help='output team description as JSON') -formats.add_argument('--md', action='store_true', help='output team description as Markdown') -arguments = parser.parse_args() - -# Initialize a TeamDescription -information = TeamDescription(team_id = arguments.team) - -# Output description -if arguments.md and arguments.dictionary is not None: - print('Sub dictionaries cannot be exported as Markdown yet.') - print('Print the whole description instead.') -elif arguments.md: - print(information.markdown()) -else: - if arguments.dictionary == 'general': - print(dumps(information.general, indent = 4)) - elif arguments.dictionary == 'exclusions': - print(dumps(information.exclusions, indent = 4)) - elif arguments.dictionary == 'preprocessing': - print(dumps(information.preprocessing, indent = 4)) - elif arguments.dictionary == 'analysis': - print(dumps(information.analysis, indent = 4)) - elif arguments.dictionary == 'categorized_for_analysis': - print(dumps(information.categorized_for_analysis, indent = 4)) - elif arguments.dictionary == 'derived': - print(dumps(information.derived, indent = 4)) - elif arguments.dictionary == 'comments': - print(dumps(information.comments, indent = 4)) +def main(): + """ Entry-point for the command line tool narps_description """ + + # Parse arguments + parser = ArgumentParser(description='Get description of a NARPS pipeline.') + parser.add_argument('-t', '--team', type=str, required=True, + help='the team ID') + parser.add_argument('-d', '--dictionary', type=str, required=False, + choices=[ + 'general', + 'exclusions', + 'preprocessing', + 'analysis', + 'categorized_for_analysis', + 'derived', + 'comments' + ], + help='the sub dictionary of team description') + formats = parser.add_mutually_exclusive_group(required = False) + formats.add_argument('--json', action='store_true', help='output team description as JSON') + formats.add_argument('--md', action='store_true', help='output team description as Markdown') + arguments = parser.parse_args() + + # Initialize a TeamDescription + information = TeamDescription(team_id = arguments.team) + + # Output description + if arguments.md and arguments.dictionary is not None: + print('Sub dictionaries cannot be exported as Markdown yet.') + print('Print the whole description instead.') + elif arguments.md: + print(information.markdown()) else: - print(dumps(information, indent = 4)) + if arguments.dictionary == 'general': + print(dumps(information.general, indent = 4)) + elif arguments.dictionary == 'exclusions': + print(dumps(information.exclusions, indent = 4)) + elif arguments.dictionary == 'preprocessing': + print(dumps(information.preprocessing, indent = 4)) + elif arguments.dictionary == 'analysis': + print(dumps(information.analysis, indent = 4)) + elif arguments.dictionary == 'categorized_for_analysis': + print(dumps(information.categorized_for_analysis, indent = 4)) + elif arguments.dictionary == 'derived': + print(dumps(information.derived, indent = 4)) + elif arguments.dictionary == 'comments': + print(dumps(information.comments, indent = 4)) + else: + print(dumps(information, indent = 4)) + +if __name__ == '__main__': + main() diff --git a/narps_open/data/results/__main__.py b/narps_open/data/results/__main__.py index b9f1d728..88111b87 100644 --- a/narps_open/data/results/__main__.py +++ b/narps_open/data/results/__main__.py @@ -8,27 +8,34 @@ from narps_open.data.results import ResultsCollectionFactory from narps_open.pipelines import implemented_pipelines -# Parse arguments -parser = ArgumentParser(description='Get Neurovault collection of results from NARPS teams.') -group = parser.add_mutually_exclusive_group(required = True) -group.add_argument('-t', '--teams', nargs='+', type=str, action='extend', - help='a list of team IDs') -group.add_argument('-a', '--all', action='store_true', help='download results from all teams') -parser.add_argument('-r', '--rectify', action='store_true', default = False, required = False, - help='rectify the results') -arguments = parser.parse_args() - -factory = ResultsCollectionFactory() - -if arguments.all: - for team_id, _ in implemented_pipelines.items(): - collection = factory.get_collection(team_id) - collection.download() - if arguments.rectify: - collection.rectify() -else: - for team in arguments.teams: - collection = factory.get_collection(team) - collection.download() - if arguments.rectify: - collection.rectify() + +def main(): + """ Entry-point for the command line tool narps_results """ + + # Parse arguments + parser = ArgumentParser(description='Get Neurovault collection of results from NARPS teams.') + group = parser.add_mutually_exclusive_group(required = True) + group.add_argument('-t', '--teams', nargs='+', type=str, action='extend', + help='a list of team IDs') + group.add_argument('-a', '--all', action='store_true', help='download results from all teams') + parser.add_argument('-r', '--rectify', action='store_true', default = False, required = False, + help='rectify the results') + arguments = parser.parse_args() + + factory = ResultsCollectionFactory() + + if arguments.all: + for team_id, _ in implemented_pipelines.items(): + collection = factory.get_collection(team_id) + collection.download() + if arguments.rectify: + collection.rectify() + else: + for team in arguments.teams: + collection = factory.get_collection(team) + collection.download() + if arguments.rectify: + collection.rectify() + +if __name__ == '__main__': + main() diff --git a/narps_open/runner.py b/narps_open/runner.py index 0776c4aa..32c80180 100644 --- a/narps_open/runner.py +++ b/narps_open/runner.py @@ -152,7 +152,8 @@ def get_missing_group_level_outputs(self): return [f for f in files if not isfile(f)] -if __name__ == '__main__': +def main(): + """ Entry-point for the command line tool narps_open_runner """ # Parse arguments parser = ArgumentParser(description='Run the pipelines from NARPS.') @@ -191,7 +192,6 @@ def get_missing_group_level_outputs(self): # Check data if arguments.check: - missing_files = [] print('Missing files for team', arguments.team, 'after running', len(runner.pipeline.subject_list), 'subjects:') if not arguments.group: @@ -202,3 +202,6 @@ def get_missing_group_level_outputs(self): # Start the runner else: runner.start(arguments.first, arguments.group) + +if __name__ == '__main__': + main() diff --git a/narps_open/tester.py b/narps_open/tester.py new file mode 100644 index 00000000..1a2cf284 --- /dev/null +++ b/narps_open/tester.py @@ -0,0 +1,29 @@ +#!/usr/bin/python +# coding: utf-8 + +""" This module allows to compare pipeline output with original team results """ + +import sys +from argparse import ArgumentParser + +import pytest + +def main(): + """ Entry-point for the command line tool narps_open_tester """ + + # Parse arguments + parser = ArgumentParser(description='Test the pipelines from NARPS.') + parser.add_argument('-t', '--team', type=str, required=True, + help='the team ID') + arguments = parser.parse_args() + + sys.exit(pytest.main([ + '-s', + '-q', + '-x', + f'tests/pipelines/test_team_{arguments.team}.py', + '-m', + 'pipeline_test'])) + +if __name__ == '__main__': + main() diff --git a/narps_open/utils/status.py b/narps_open/utils/status.py index 0058b40b..4f80b11f 100644 --- a/narps_open/utils/status.py +++ b/narps_open/utils/status.py @@ -22,7 +22,6 @@ def get_opened_issues(): request_url = 'https://api.github.com/repos/Inria-Empenn/narps_open_pipelines' response = get(request_url, timeout = 2) response.raise_for_status() - nb_issues = response.json()['open_issues'] # Get all opened issues request_url = 'https://api.github.com/repos/Inria-Empenn/narps_open_pipelines/issues' @@ -185,11 +184,14 @@ def markdown(self): reproducibility_ranking += ':star:' for _ in range(4-team_values['reproducibility']): reproducibility_ranking += ':black_small_square:' - output_markdown += f'| {reproducibility_ranking}
{team_values["reproducibility_comment"]} |\n' + output_markdown += f'| {reproducibility_ranking}
' + output_markdown += f'{team_values["reproducibility_comment"]} |\n' return output_markdown -if __name__ == '__main__': +def main(): + """ Entry-point for the command line tool narps_open_status """ + # Parse arguments parser = ArgumentParser(description='Get a work progress status report for pipelines.') formats = parser.add_mutually_exclusive_group(required = False) @@ -204,3 +206,6 @@ def markdown(self): print(report.markdown()) else: print(report) + +if __name__ == '__main__': + main() diff --git a/setup.py b/setup.py index 2c6c9b06..91a2d63a 100644 --- a/setup.py +++ b/setup.py @@ -63,5 +63,14 @@ ('narps_open/data/description', ['narps_open/data/description/analysis_pipelines_comments.tsv']), ('narps_open/data/description', ['narps_open/data/description/analysis_pipelines_derived_descriptions.tsv']), ('narps_open/data/description', ['narps_open/data/description/analysis_pipelines_full_descriptions.tsv']) - ] + ], + entry_points = { + 'console_scripts': [ + 'narps_open_runner = narps_open.runner:main', + 'narps_open_tester = narps_open.tester:main', + 'narps_open_status = narps_open.utils.status:main', + 'narps_description = narps_open.data.description.__main__:main', + 'narps_results = narps_open.data.results.__main__:main' + ] + } )