From d1f077e2dd5f453de622ffc79d70bead86d1ff52 Mon Sep 17 00:00:00 2001 From: Jennings Zhang Date: Fri, 5 Jan 2024 12:25:30 -0500 Subject: [PATCH] Initial commit --- .dockerignore | 18 +++ .github/workflows/ci.yml | 183 +++++++++++++++++++++++++++ .gitignore | 11 ++ Dockerfile | 22 ++++ LICENSE | 21 +++ README.md | 177 ++++++++++++++++++++++++++ app.py | 70 ++++++++++ bootstrap.sh | 267 +++++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + setup.py | 48 +++++++ tests/test_example.py | 21 +++ 11 files changed, 839 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100755 app.py create mode 100755 bootstrap.sh create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/test_example.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..63df785 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.git/ +.github/ +.dockerignore +Dockerfile + +*~ +*.DS_Store +*.egg-info/ +__pycache__/ + +.docker + +.idea/ +.vscode/ + +examples/ + +venv/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ca4afb6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,183 @@ +# Continuous integration testing for ChRIS Plugin. +# https://github.com/FNNDSC/python-chrisapp-template/wiki/Continuous-Integration +# +# - on push and PR: run pytest +# - on push to main: build and push container images as ":latest" +# - on push to semver tag: build and push container image with tag and +# upload plugin description to https://chrisstore.co + +name: build + +on: + push: + branches: [ main ] + tags: + - "v?[0-9]+.[0-9]+.[0-9]+*" + pull_request: + branches: [ main ] + +jobs: + test: + name: Unit tests + if: false # delete this line to enable automatic testing + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - name: Build + uses: docker/build-push-action@v5 + with: + build-args: extras_require=dev + context: . + load: true + push: false + tags: "localhost/local/app:dev" + cache-from: type=gha + cache-to: type=gha,mode=max + - name: Run pytest + run: | + docker run -v "$GITHUB_WORKSPACE:/app:ro" -w /app localhost/local/app:dev \ + pytest -o cache_dir=/tmp/pytest + + 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' + # needs: [ test ] # uncomment to require passing tests + runs-on: ubuntu-22.04 + + steps: + - name: Decide image tags + id: info + shell: python + run: | + import os + import itertools + + def join_tag(t): + registry, repo, tag = t + return f'{registry}/{repo}:{tag}'.lower() + + registries = ['docker.io', 'ghcr.io'] + repos = ['${{ github.repository }}'] + if '${{ github.ref_type }}' == 'branch': + tags = ['latest'] + elif '${{ github.ref_type }}' == 'tag': + tag = '${{ github.ref_name }}' + version = tag[1:] if tag.startswith('v') else tag + tags = ['latest', version] + else: + tags = [] + + if '${{ github.ref_type }}' == 'tag': + local_tag = join_tag(('ghcr.io', '${{ github.repository }}', version)) + else: + local_tag = join_tag(('localhost', '${{ github.repository }}', 'latest')) + + product = itertools.product(registries, repos, tags) + tags_csv = ','.join(map(join_tag, product)) + outputs = { + 'tags_csv' : tags_csv, + 'push' : 'true' if tags_csv else 'false', + 'local_tag': local_tag + } + with open(os.environ['GITHUB_OUTPUT'], 'a') as out: + for k, v in outputs.items(): + out.write(f'{k}={v}\n') + + - uses: actions/checkout@v3 + # QEMU is used for non-x86_64 builds + - uses: docker/setup-qemu-action@v3 + # buildx adds additional features to docker build + - uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + # Here, we want to do the docker build twice: + # The first build pushes to our local registry for testing. + # The second build pushes to Docker Hub and ghcr.io + - name: Build (local only) + uses: docker/build-push-action@v3 + id: docker_build + with: + context: . + file: ./Dockerfile + tags: ${{ steps.info.outputs.local_tag }} + load: true + cache-from: type=gha + # If you have a directory called examples/incoming/ and examples/outgoing/, then + # run your ChRIS plugin with no parameters, and assert that it creates all the files + # which are expected. File contents are not compared. + - name: Run examples + id: run_examples + run: | + if ! [ -d 'examples/incoming/' ] || ! [ -d 'examples/outgoing/' ]; then + echo "No examples." + exit 0 + fi + + dock_image=${{ steps.info.outputs.local_tag }} + output_dir=$(mktemp -d) + cmd=$(docker image inspect -f '{{ (index .Config.Cmd 0) }}' $dock_image) + docker run --rm -u "$(id -u):$(id -g)" \ + -v "$PWD/examples/incoming:/incoming:ro" \ + -v "$output_dir:/outgoing:rw" \ + $dock_image $cmd /incoming /outgoing + + for expected_file in $(find examples/outgoing -type f); do + fname="${expected_file##*/}" + out_path="$output_dir/$fname" + printf "Checking output %s exists..." "$out_path" + if [ -f "$out_path" ]; then + echo "ok" + else + echo "not found" + exit 1 + fi + done + + - name: Login to DockerHub + if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'docker.io') + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Login to GitHub Container Registry + if: (github.event_name == 'push' || github.event_name == 'release') && contains(steps.info.outputs.tags_csv, 'ghcr.io') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v3 + if: (github.event_name == 'push' || github.event_name == 'release') + with: + context: . + file: ./Dockerfile + tags: ${{ steps.info.outputs.tags_csv }} + # if non-x86_84 architectures are supported, add them here + platforms: linux/amd64 #,linux/arm64,linux/ppc64le + push: ${{ steps.info.outputs.push }} + cache-to: type=gha,mode=max + + - name: Upload ChRIS Plugin + id: upload + if: github.ref_type == 'tag' + uses: FNNDSC/upload-chris-plugin@v1 + with: + dock_image: ${{ steps.info.outputs.local_tag }} + username: ${{ secrets.CHRISPROJECT_USERNAME }} + password: ${{ secrets.CHRISPROJECT_PASSWORD }} + chris_url: https://cube.chrisproject.org/api/v1/ + compute_names: host,galena + + - name: Update DockerHub description + if: steps.upload.outcome == 'success' + uses: peter-evans/dockerhub-description@v3 + continue-on-error: true # it is not crucial that this works + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + short-description: ${{ steps.upload.outputs.title }} + readme-filepath: ./README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffd9fdb --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*~ +*.DS_Store +*.egg-info/ +__pycache__/ + +.docker + +.idea/ +.vscode/ + +venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9611497 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# Python version can be changed, e.g. +# FROM python:3.8 +# FROM ghcr.io/mamba-org/micromamba:1.5.1-focal-cuda-11.3.1 +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..." + +ARG SRCDIR=/usr/local/src/app +WORKDIR ${SRCDIR} + +COPY requirements.txt . +RUN --mount=type=cache,sharing=private,target=/root/.cache/pip pip install -r requirements.txt + +COPY . . +ARG extras_require=none +RUN pip install ".[${extras_require}]" \ + && cd / && rm -rf ${SRCDIR} +WORKDIR / + +CMD ["commandname"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3ffa30 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 FNNDSC / BCH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..611ea6c --- /dev/null +++ b/README.md @@ -0,0 +1,177 @@ +# _ChRIS_ Plugin Template + +[![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) + +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 new file mode 100755 index 0000000..68e8e68 --- /dev/null +++ b/app.py @@ -0,0 +1,70 @@ +#!/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 new file mode 100755 index 0000000..6350aec --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,267 @@ +#!/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='My ChRIS Plugin' +SCRIPT_NAME='commandname' +DESCRIPTION='A ChRIS plugin to do something awesome' +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/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dfc8be5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +chris_plugin==0.3.1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..240b0dd --- /dev/null +++ b/setup.py @@ -0,0 +1,48 @@ +from setuptools import setup +import re + +_version_re = re.compile(r"(?<=^__version__ = (\"|'))(.+)(?=\"|')") + +def get_version(rel_path: str) -> str: + """ + Searches for the ``__version__ = `` line in a source code file. + + https://packaging.python.org/en/latest/guides/single-sourcing-package-version/ + """ + with open(rel_path, 'r') as f: + matches = map(_version_re.search, f) + filtered = filter(lambda m: m is not None, matches) + version = next(filtered, None) + if version is None: + raise RuntimeError(f'Could not find __version__ in {rel_path}') + return version.group(0) + + +setup( + name='chris-plugin-template', + version=get_version('app.py'), + description='A ChRIS DS plugin template', + author='FNNDSC', + author_email='dev@babyMRI.org', + url='https://github.com/FNNDSC/python-chrisapp-template', + py_modules=['app'], + install_requires=['chris_plugin'], + license='MIT', + entry_points={ + 'console_scripts': [ + 'commandname = app:main' + ] + }, + classifiers=[ + 'License :: OSI Approved :: MIT License', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Bio-Informatics', + 'Topic :: Scientific/Engineering :: Medical Science Apps.' + ], + extras_require={ + 'none': [], + 'dev': [ + 'pytest~=7.1' + ] + } +) diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..83d3846 --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,21 @@ +from pathlib import Path + +from app import parser, main + + +def test_main(tmp_path: Path): + # setup example data + inputdir = tmp_path / 'incoming' + outputdir = tmp_path / 'outgoing' + inputdir.mkdir() + outputdir.mkdir() + (inputdir / 'plaintext.txt').write_text('hello ChRIS, I am a ChRIS plugin') + + # simulate run of main function + options = parser.parse_args(['--word', 'ChRIS', '--pattern', '*.txt']) + main(options, inputdir, outputdir) + + # assert behavior is expected + expected_output_file = outputdir / 'plaintext.count.txt' + assert expected_output_file.exists() + assert expected_output_file.read_text() == '2'