From c925220090e36059dd46594ac2f34ea6dbf4415f Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Fri, 5 Jan 2024 12:27:48 -0500 Subject: [PATCH] New project --- .github/workflows/ci.yml | 4 +- .gitignore | 2 + Dockerfile | 8 +- README.md | 107 +++------------- app.py | 70 ---------- bootstrap.sh | 267 --------------------------------------- examples/download.sh | 13 ++ requirements.txt | 4 +- setup.py | 12 +- tests/test_example.py | 2 +- zb.py | 58 +++++++++ 11 files changed, 108 insertions(+), 439 deletions(-) delete mode 100755 app.py delete mode 100755 bootstrap.sh create mode 100755 examples/download.sh create mode 100644 zb.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca4afb6..e2eceaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,6 @@ on: jobs: test: name: Unit tests - if: false # delete this line to enable automatic testing runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -41,8 +40,7 @@ jobs: build: name: Build - if: false # delete this line and uncomment the line below to enable automatic builds - # if: github.event_name == 'push' || github.event_name == 'release' + if: github.event_name == 'push' || github.event_name == 'release' # needs: [ test ] # uncomment to require passing tests runs-on: ubuntu-22.04 diff --git a/.gitignore b/.gitignore index ffd9fdb..060e8c9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ __pycache__/ .vscode/ venv/ + +*.nii.gz diff --git a/Dockerfile b/Dockerfile index 9611497..87b5090 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,10 @@ FROM docker.io/python:3.12.0-slim-bookworm LABEL org.opencontainers.image.authors="FNNDSC " \ - org.opencontainers.image.title="ChRIS Plugin Title" \ - org.opencontainers.image.description="A ChRIS plugin that..." + org.opencontainers.image.title="Zero MRI Background" \ + org.opencontainers.image.description="Set the background intensity of a MRI to 0" -ARG SRCDIR=/usr/local/src/app +ARG SRCDIR=/usr/local/src/pl-zerobg WORKDIR ${SRCDIR} COPY requirements.txt . @@ -19,4 +19,4 @@ RUN pip install ".[${extras_require}]" \ && cd / && rm -rf ${SRCDIR} WORKDIR / -CMD ["commandname"] +CMD ["zb"] diff --git a/README.md b/README.md index 611ea6c..5b50b8a 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,10 @@ -# _ChRIS_ Plugin Template +# Zero MRI Background -[![test status](https://github.com/FNNDSC/python-chrisapp-template/actions/workflows/src.yml/badge.svg)](https://github.com/FNNDSC/python-chrisapp-template/actions/workflows/src.yml) -[![MIT License](https://img.shields.io/github/license/FNNDSC/python-chrisapp-template)](LICENSE) +[![Version](https://img.shields.io/docker/v/fnndsc/pl-zerobg?sort=semver)](https://hub.docker.com/r/fnndsc/pl-zerobg) +[![MIT License](https://img.shields.io/github/license/fnndsc/pl-zerobg)](https://github.com/FNNDSC/pl-zerobg/blob/main/LICENSE) +[![ci](https://github.com/FNNDSC/pl-zerobg/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/pl-zerobg/actions/workflows/ci.yml) -This is a minimal template repository for _ChRIS_ plugin applications in Python. - -## About _ChRIS_ Plugins - -A _ChRIS_ plugin is a scientific data-processing software which can run anywhere all-the-same: -in the cloud via a [web app](https://github.com/FNNDSC/ChRIS_ui/), or on your own laptop -from the terminal. They are easy to build and easy to understand: most simply, a -_ChRIS_ plugin is a command-line program which processes data from an input directory -and creates data to an output directory with the usage -`commandname [options...] inputdir/ outputdir/`. - -For more information, visit our website https://chrisproject.org - -## How to Use This Template - -Go to https://github.com/FNNDSC/python-chrisapp-template and click "Use this template". -The newly created repository is ready to use right away. - -A script `bootstrap.sh` is provided to help fill in and rename values for your new project. -It is optional to use. - -1. Edit the variables in `bootstrap.sh` -2. Run `./bootstrap.sh` -3. Follow the instructions it will print out - -## Example Plugins - -Here are some good, complete examples of _ChRIS_ plugins created from this template. - -- https://github.com/FNNDSC/pl-dcm2niix (basic command wrapper example) -- (parallelizes a command) -- https://github.com/FNNDSC/pl-mri-preview (uses [NiBabel](https://nipy.org/nibabel/)) -- https://github.com/FNNDSC/pl-pyvista-volume (example using Python package project structure and pytest) -- https://github.com/FNNDSC/pl-fetal-cp-surface-extract (has a good README.md) - -## What's Inside - -| Path | Purpose | -|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `app.py` | Main script: start editing here! | -| `tests/` | Unit tests | -| `setup.py` | [Python project metadata and installation script](https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#setup-py) | -| `requirements.txt` | List of Python dependencies | -| `Dockerfile` | [Container image build recipe](https://docs.docker.com/engine/reference/builder/) | -| `.github/workflows/ci.yml` | "continuous integration" using [Github Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions): automatic testing, building, and uploads to https://chrisstore.co | - -## Contributing - -The source code for the `main` branch of this repository is on the -[src](https://github.com/fnndsc/python-chrisapp-template/tree/src) -branch, which has an additional file -[`.github/workflows/src.yml`](https://github.com/FNNDSC/python-chrisapp-template/blob/src/.github/workflows/src.yml) -When tests pass, changes are automatically merged into `main`. -Developers should commit to or make pull requests targeting `src`. -Do not push directly to `main`. - -This is a workaround in order to do automatic testing of this template -without including the `.github/workflows/src.yml` file in the template itself. - - diff --git a/app.py b/app.py deleted file mode 100755 index 68e8e68..0000000 --- a/app.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python - -from pathlib import Path -from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter - -from chris_plugin import chris_plugin, PathMapper - -__version__ = '1.0.0' - -DISPLAY_TITLE = r""" -ChRIS Plugin Template Title -""" - - -parser = ArgumentParser(description='!!!CHANGE ME!!! An example ChRIS plugin which ' - 'counts the number of occurrences of a given ' - 'word in text files.', - formatter_class=ArgumentDefaultsHelpFormatter) -parser.add_argument('-w', '--word', required=True, type=str, - help='word to count') -parser.add_argument('-p', '--pattern', default='**/*.txt', type=str, - help='input file filter glob') -parser.add_argument('-V', '--version', action='version', - version=f'%(prog)s {__version__}') - - -# The main function of this *ChRIS* plugin is denoted by this ``@chris_plugin`` "decorator." -# Some metadata about the plugin is specified here. There is more metadata specified in setup.py. -# -# documentation: https://fnndsc.github.io/chris_plugin/chris_plugin.html#chris_plugin -@chris_plugin( - parser=parser, - title='My ChRIS plugin', - category='', # ref. https://chrisstore.co/plugins - min_memory_limit='100Mi', # supported units: Mi, Gi - min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core - min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU -) -def main(options: Namespace, inputdir: Path, outputdir: Path): - """ - *ChRIS* plugins usually have two positional arguments: an **input directory** containing - input files and an **output directory** where to write output files. Command-line arguments - are passed to this main method implicitly when ``main()`` is called below without parameters. - - :param options: non-positional arguments parsed by the parser given to @chris_plugin - :param inputdir: directory containing (read-only) input files - :param outputdir: directory where to write output files - """ - - print(DISPLAY_TITLE) - - # Typically it's easier to think of programs as operating on individual files - # rather than directories. The helper functions provided by a ``PathMapper`` - # object make it easy to discover input files and write to output files inside - # the given paths. - # - # Refer to the documentation for more options, examples, and advanced uses e.g. - # adding a progress bar and parallelism. - mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern, suffix='.count.txt') - for input_file, output_file in mapper: - # The code block below is a small and easy example of how to use a ``PathMapper``. - # It is recommended that you put your functionality in a helper function, so that - # it is more legible and can be unit tested. - data = input_file.read_text() - frequency = data.count(options.word) - output_file.write_text(str(frequency)) - - -if __name__ == '__main__': - main() diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index abf95a7..0000000 --- a/bootstrap.sh +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/env bash -# bootstrap.sh: customize python-chrisapp-template with project details -# -# WARNING: This script is for advanced users only! Do not proceed -# unless you understand what this does. New developers would find -# it easier to use python-chrisapp-template as is. Simply ignore -# and optionally delete this file. - -# ======================================== -# CONFIGURATION -# ======================================== - -# ---------------------------------------- -# STEP 1. Change these values to your liking. -# ---------------------------------------- - -PLUGIN_NAME="$(basename $(dirname $(realpath $0)))" # name of current directory -PLUGIN_TITLE='Zero MRI Background' -SCRIPT_NAME='zb' -DESCRIPTION='Set the background intensity of a MRI to 0' -ORGANIZATION='FNNDSC' -EMAIL='dev@babyMRI.org' - -# Github Actions: automatically test and build your code. -# https://github.com/FNNDSC/python-chrisapp-template/wiki/Continuous-Integration -# -# These options will fail unless your Github settings are preconfigured. -# Repositories under github.com/FNNDSC are preconfigured, so these defaults might work. -# Please review the file .github/workflows/ci.yml before you push it. - -# Automatically test on Github Actions each time you run `git push` -# If the value is "no" then tests are not performed. There are no side effects. -ENABLE_ACTIONS_TEST=yes -# Automatically build images on Github Actions each time you run `git push`, -# and also publish to https://chrisstore.co each time you run `git push --tags` -# If the value is "no" then builds will not be automated. -ENABLE_ACTIONS_BUILD=yes - -# WARNING: the default configuration in .github/workflows/ci.yml is to allow for -# the build to proceed regardless of whether tests pass. To modify this behavior -# and other advanced features (such as multi-architecture builds such as arm64, ppc64le) -# you must edit .github/workflows/ci.yml by hand. - - -# ---------------------------------------- -# STEP 2. Uncomment the line where it says READY=yes -# ---------------------------------------- - -READY=yes - -# ---------------------------------------- -# STEP 3. Run: ./bootstrap.sh -# ---------------------------------------- - - -if [ "$(uname -o 2> /dev/null)" != 'GNU/Linux' ]; then - >&2 echo "error: this script only works on GNU/Linux." -fi - -if ! [ "$READY" = 'yes' ]; then - >&2 echo "error: you are not READY." - exit 1 -fi - -cd $(dirname "$0") - - -# ======================================== -# VALIDATE INPUT -# ======================================== - -function contains_invalid_characters () { - [[ "$1" = *"/"* ]] -} - -# given a variable name, exit if the variable's value contains invalid characters. -function check_variable_value_valid () { - local varname="$1" - local varvalue="${!varname}" - if contains_invalid_characters "$varvalue"; then - >&2 echo "error: invalid characters in $varname=$varvalue" - exit 1 - fi -} - -# may not contain '/' -check_variable_value_valid PLUGIN_NAME -check_variable_value_valid SCRIPT_NAME -check_variable_value_valid ORGANIZATION -check_variable_value_valid EMAIL - - -# ======================================== -# COMMIT THE USER-SET CONFIG -# ======================================== - -# print command to run before running it -function verb () { - set -x - "$@" - { set +x; } 2> /dev/null -} - -# fail on error -set -e -set -o pipefail - -verb git commit -m 'Configure python-chrisapp-template/bootstrap.sh' -- "$0" - - -# ======================================== -# REPLACE VALUES -# ======================================== - -# execute sed on all files in project, excluding hidden paths and venv/ -function replace_in_all () { - if [ -z "$2" ]; then - return - fi - find . -type f \ - -not -path '*/\.*/*' -not -path '*/\venv/*' -not -name 'bootstrap.sh' \ - -exec sed -i -e "s/$1/$2/g" '{}' \; -} - -replace_in_all commandname "$SCRIPT_NAME" -replace_in_all pl-appname "$PLUGIN_NAME" -replace_in_all 'dev@babyMRI.org' "$EMAIL" -replace_in_all FNNDSC "$ORGANIZATION" - -# .github/ -if [ "${ENABLE_ACTIONS_TEST,,}" = 'yes' ]; then - sed -i -e '/delete this line to enable automatic testing/d' .github/workflows/ci.yml -fi - -if [ "${ENABLE_ACTIONS_BUILD,,}" = 'yes' ]; then - sed -i -e '/delete this line and uncomment the line below to enable automatic builds/d' .github/workflows/ci.yml - sed -i -e 's/# *if: github\.event_name/if: github\.event_name/' .github/workflows/ci.yml -fi - -# replace "/" with "\/" in string -function escape_slashes () { - sed 's/\//\\&/g' <<< "$@" -} - -escaped_description="$(escape_slashes "$DESCRIPTION")" -escaped_title="$(escape_slashes "$PLUGIN_TITLE")" - -# README.md -temp_file=$(mktemp) -sed -e'/^# ChRIS Plugin Title$/'\{ -e:1 -en\;b1 -e\} -ed README.md \ - | sed "s/^# ChRIS Plugin Title\$/# $escaped_title/" \ - | sed '/^END README TEMPLATE -->$/d' \ - | sed "s/fnndsc/${ORGANIZATION,,}/g" \ - | sed "s/app\\.py/$SCRIPT_NAME.py/g" \ - > $temp_file -mv $temp_file README.md - -# Dockerfile -sed "s#ARG SRCDIR=/usr/local/src/app#ARG SRCDIR=/usr/local/src/$PLUGIN_NAME#" Dockerfile \ - | sed "s/org\.opencontainers\.image\.title=\"ChRIS Plugin Title\"/org.opencontainers.image.title=\"$escaped_title\"/" \ - | sed "s/org\.opencontainers\.image\.description=\"A ChRIS plugin that\.\.\.\"/org.opencontainers.image.description=\"$escaped_description\"/" \ - > $temp_file -mv $temp_file Dockerfile - -# setup.py - -function guess_https_url () { - local origin="$(git remote get-url origin)" - local https_url="$origin" - if [[ "$https_url" = "git@"* ]]; then - # convert SSH url to HTTPS url by - # 1. change last ':' to '/' - # 2. replace leading 'git@' with 'https://' - https_url="$( - echo "$https_url" \ - | sed 's#\(.*\):#\1/#' \ - | sed 's#^git@#https://#' - )" - fi - echo "${https_url:0:-4}" # remove trailing ".git" -} - -appname_without_prefix="$(sed -E 's/(pl|dbg|ep)-//' <<< "$PLUGIN_NAME")" -sed "s/name='.*'/name='$appname_without_prefix'/" setup.py \ - | sed "s/description='.*'/description='$escaped_description'/" \ - | sed "s/py_modules=\['app'\]/py_modules=['$SCRIPT_NAME']/" \ - | sed "s/app:main/$SCRIPT_NAME:main/" \ - | sed "s#url='.*'#url='$(guess_https_url)'#" \ - | sed "s/app\.py/$SCRIPT_NAME.py/" \ - > $temp_file -mv $temp_file setup.py - -# app.py - -# FIGlet over HTTPS, since it's probably not installed locally -function figlet_wrapper () { - curl -fsSG 'https://figlet.chrisproject.org/' --data-urlencode "message=$*" \ - | grep -v '^[[:space:]]*$' -} - -function inject_figleted_title () { - python << EOF -for line in open('app.py'): - if line == 'ChRIS Plugin Template Title\n': - print(r"""$1""") - else: - print(line, end='') -EOF -} - -figleted_title="$(figlet_wrapper "$PLUGIN_NAME")" -echo "$figleted_title" -inject_figleted_title "$figleted_title" \ - | sed "s/title='My ChRIS plugin'/title='$escaped_title'/" \ - | sed "s/description='cli description'/description='$escaped_description'/" \ - > "$SCRIPT_NAME.py" -rm app.py - -# tests/ -for test_file in tests/*.py; do - sed "s/from app import/from $SCRIPT_NAME import/" $test_file > $temp_file - mv $temp_file $test_file -done - -# ======================================== -# SETUP -# ======================================== - -if ! [ -e venv ]; then - verb python -m venv venv -fi - ->&2 echo + source venv/bin/activate -source venv/bin/activate -verb pip install -r requirements.txt -verb pip install -e '.[dev]' - - -if [ -z "$TERM" ]; then - tput=tput -else - tput=true -fi - -$tput bold ->&2 printf '\n%s\n\n' '✨Done!✨' -$tput sgr0 - -$tput setaf 3 ->&2 echo 'To undo these actions and start over, run:' ->&2 printf '\n\t%s\n\t%s\n\t%s\n\t%s\n\n' \ - 'git reset --hard' \ - 'git clean -df' \ - 'rm -rf venv *.egg-info' \ - "git reset 'HEAD^'" -$tput setaf 6 ->&2 echo 'Activate the Python virtual environment by running:' ->&2 printf '\n\t%s\n\n' 'source venv/bin/activate' ->&2 echo 'Save these changes by running:' ->&2 printf '\n\t%s\n\n' 'git add -A && git commit -m "Run bootstrap.sh"' -$tput setaf 2 -echo 'For more information on how to get started, see README.md' -$tput sgr0 - -verb rm -v "$0" - -# Note to self: consider rewriting this in Python? diff --git a/examples/download.sh b/examples/download.sh new file mode 100755 index 0000000..23b4eac --- /dev/null +++ b/examples/download.sh @@ -0,0 +1,13 @@ +#!/bin/bash -ex +# Downloads a subset of "Fetal Brain Atlas (Serag et al.)" + +mkdir "$1" +cd "$1" + +wget -O template24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/307/template.nii.gz +wget -O ventricles24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/331/ventricles.nii.gz +wget -O mask24.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/330/mask.nii.gz + +wget -O template34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/351/template.nii.gz +wget -O ventricles34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/350/ventricles.nii.gz +wget -O mask34.nii.gz https://fetalmri-hosting-of-medical-image-analysis-platform-dcb83b.apps.shift.nerc.mghpcc.org/api/v1/files/310/mask.nii.gz diff --git a/requirements.txt b/requirements.txt index dfc8be5..2073410 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -chris_plugin==0.3.1 +chris_plugin==0.4.0 +nibabel==5.2.0 +tqdm==4.66.1 diff --git a/setup.py b/setup.py index 240b0dd..257ac94 100644 --- a/setup.py +++ b/setup.py @@ -19,18 +19,18 @@ def get_version(rel_path: str) -> str: setup( - name='chris-plugin-template', - version=get_version('app.py'), - description='A ChRIS DS plugin template', + name='zerobg', + version=get_version('zb.py'), + description='Set the background intensity of a MRI to 0', author='FNNDSC', author_email='dev@babyMRI.org', - url='https://github.com/FNNDSC/python-chrisapp-template', - py_modules=['app'], + url='https://github.com/FNNDSC/pl-zerobg', + py_modules=['zb'], install_requires=['chris_plugin'], license='MIT', entry_points={ 'console_scripts': [ - 'commandname = app:main' + 'zb = zb:main' ] }, classifiers=[ diff --git a/tests/test_example.py b/tests/test_example.py index 83d3846..324e4d0 100644 --- a/tests/test_example.py +++ b/tests/test_example.py @@ -1,6 +1,6 @@ from pathlib import Path -from app import parser, main +from zb import parser, main def test_main(tmp_path: Path): diff --git a/zb.py b/zb.py new file mode 100644 index 0000000..35e37da --- /dev/null +++ b/zb.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +from pathlib import Path +from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter +from tqdm.contrib.concurrent import process_map + +from chris_plugin import chris_plugin, PathMapper + +__version__ = '1.0.0' + +DISPLAY_TITLE = r""" + _ _ + | | | | + _ __ | |______ _______ _ __ ___ | |__ __ _ +| '_ \| |______|_ / _ \ '__/ _ \| '_ \ / _` | +| |_) | | / / __/ | | (_) | |_) | (_| | +| .__/|_| /___\___|_| \___/|_.__/ \__, | +| | __/ | +|_| |___/ +""" + + +parser = ArgumentParser(description='Set the background intensity of MRI volumes to zero.', + formatter_class=ArgumentDefaultsHelpFormatter) +parser.add_argument('-t', '--threshold', required=False, type=str, + default='.nii.gz:37', + help='A filename glob and background intensity threshold. Multiple pairs should be comma-separated,' + ' i.e. GLOB1:THRESHOLD1,GLOB2:THRESHOLD2,...') +parser.add_argument('-J', '--threads', type=int, default=0, + help='Number of threads to use for parallel jobs. ' + 'Pass 0 to use number of visible CPUs.') +parser.add_argument('-V', '--version', action='version', + version=f'%(prog)s {__version__}') + + +@chris_plugin( + parser=parser, + title='Zero MRI Background', + category='', # ref. https://chrisstore.co/plugins + min_memory_limit='1Gi', # supported units: Mi, Gi + min_cpu_limit='1000m', # millicores, e.g. "1000m" = 1 CPU core + min_gpu_limit=0 # set min_gpu_limit=1 to enable GPU +) +def main(options: Namespace, inputdir: Path, outputdir: Path): + print(DISPLAY_TITLE, flush=True) + + mapper = PathMapper.file_mapper(inputdir, outputdir, glob=options.pattern, suffix='.count.txt') + for input_file, output_file in mapper: + # The code block below is a small and easy example of how to use a ``PathMapper``. + # It is recommended that you put your functionality in a helper function, so that + # it is more legible and can be unit tested. + data = input_file.read_text() + frequency = data.count(options.word) + output_file.write_text(str(frequency)) + + +if __name__ == '__main__': + main()