From 42ec8f6d3070db2abc54e35e9f985987b134d20f Mon Sep 17 00:00:00 2001 From: "sandipsamal117@gmail.com" Date: Tue, 3 Dec 2024 11:16:34 -0500 Subject: [PATCH] added working codebase --- .github/workflows/ci.yml | 3 +- Dockerfile | 8 +- README.md | 107 +++------------ bootstrap.sh | 267 ------------------------------------- app.py => pacs_retrieve.py | 73 +++++++--- pfdcm.py | 65 +++++++++ requirements.txt | 3 + setup.py | 12 +- tests/test_example.py | 2 +- 9 files changed, 151 insertions(+), 389 deletions(-) delete mode 100755 bootstrap.sh rename app.py => pacs_retrieve.py (53%) mode change 100755 => 100644 create mode 100644 pfdcm.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 966e0c5..e4768cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,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/Dockerfile b/Dockerfile index b3fd92b..17837b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,10 @@ FROM docker.io/python:3.12.1-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="A ChRIS plugin to retrieve from a remote PACS " \ + org.opencontainers.image.description="A plugin to retrieve DICOM images from a remote PACS using pfdcm" -ARG SRCDIR=/usr/local/src/app +ARG SRCDIR=/usr/local/src/pl-pacs_retrieve WORKDIR ${SRCDIR} COPY requirements.txt . @@ -19,4 +19,4 @@ RUN pip install ".[${extras_require}]" \ && cd / && rm -rf ${SRCDIR} WORKDIR / -CMD ["commandname"] +CMD ["pacs_retrieve"] diff --git a/README.md b/README.md index a8ff3ec..d851b99 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,10 @@ -# _ChRIS_ Plugin Template +# A ChRIS plugin to retrieve from a remote PACS -[![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-pacs_retrieve?sort=semver)](https://hub.docker.com/r/fnndsc/pl-pacs_retrieve) +[![MIT License](https://img.shields.io/github/license/fnndsc/pl-pacs_retrieve)](https://github.com/FNNDSC/pl-pacs_retrieve/blob/main/LICENSE) +[![ci](https://github.com/FNNDSC/pl-pacs_retrieve/actions/workflows/ci.yml/badge.svg)](https://github.com/FNNDSC/pl-pacs_retrieve/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/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index 7310cb9..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='A ChRIS plugin to retrieve from a remote PACS ' -SCRIPT_NAME='pacs_retrieve' -DESCRIPTION='A plugin to retrieve DICOM images from a remote PACS using pfdcm' -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/app.py b/pacs_retrieve.py old mode 100755 new mode 100644 similarity index 53% rename from app.py rename to pacs_retrieve.py index 68e8e68..ba769e7 --- a/app.py +++ b/pacs_retrieve.py @@ -2,13 +2,37 @@ from pathlib import Path from argparse import ArgumentParser, Namespace, ArgumentDefaultsHelpFormatter - +from pflog import pflog +from loguru import logger from chris_plugin import chris_plugin, PathMapper +import pfdcm +import json +import sys +import pprint + +LOG = logger.debug +logger_format = ( + "{time:YYYY-MM-DD HH:mm:ss} │ " + "{level: <5} │ " + "{name: >28}::" + "{function: <30} @" + "{line: <4} ║ " + "{message}" +) +logger.remove() +logger.add(sys.stderr, format=logger_format) __version__ = '1.0.0' DISPLAY_TITLE = r""" -ChRIS Plugin Template Title + _ _ _ + | | | | (_) + _ __ | |______ _ __ __ _ ___ ___ _ __ ___| |_ _ __ _ _____ _____ +| '_ \| |______| '_ \ / _` |/ __/ __| | '__/ _ \ __| '__| |/ _ \ \ / / _ \ +| |_) | | | |_) | (_| | (__\__ \ | | | __/ |_| | | | __/\ V / __/ +| .__/|_| | .__/ \__,_|\___|___/ |_| \___|\__|_| |_|\___| \_/ \___| +| | | | ______ +|_| |_| |______| """ @@ -16,10 +40,25 @@ '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( + '--PACSurl', + default='', + type=str, + help='endpoint URL of pfdcm' +) +parser.add_argument( + '--PACSname', + default='MINICHRISORTHANC', + type=str, + help='name of the PACS' +) +parser.add_argument( + '--PACSdirective', + default='', + type=str, + help='directive to query the PACS' +) parser.add_argument('-V', '--version', action='version', version=f'%(prog)s {__version__}') @@ -30,7 +69,7 @@ # documentation: https://fnndsc.github.io/chris_plugin/chris_plugin.html#chris_plugin @chris_plugin( parser=parser, - title='My ChRIS plugin', + title='A ChRIS plugin to retrieve from a remote PACS ', 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 @@ -49,21 +88,11 @@ def main(options: Namespace, inputdir: Path, outputdir: Path): 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)) + directive = json.loads(options.PACSdirective) + + search_response = pfdcm.retrieve_pacsfiles(directive, options.PACSurl, options.PACSname) + + LOG(f"response: {pprint.pformat(search_response)}") if __name__ == '__main__': diff --git a/pfdcm.py b/pfdcm.py new file mode 100644 index 0000000..6069c48 --- /dev/null +++ b/pfdcm.py @@ -0,0 +1,65 @@ +import requests +from loguru import logger +import sys +import copy +from collections import ChainMap +import json + +LOG = logger.debug + +logger_format = ( + "{time:YYYY-MM-DD HH:mm:ss} │ " + "{level: <5} │ " + "{name: >28}::" + "{function: <30} @" + "{line: <4} ║ " + "{message}" +) +logger.remove() +logger.add(sys.stderr, format=logger_format) + +def health_check(url: str): + pfdcm_about_api = f'{url}about/' + headers = {'Content-Type': 'application/json', 'accept': 'application/json'} + try: + response = requests.get(pfdcm_about_api, headers=headers) + return response + except Exception as er: + raise Exception("Connection to pfdcm could not be established.") + + +def retrieve_pacsfiles(directive: dict, url: str, pacs_name: str): + """ + This method uses the async API endpoint of `pfdcm` to send a single 'retrieve' request that in + turn uses `oxidicom` to push and register PACS files to a CUBE instance + """ + + pfdcm_dicom_api = f'{url}PACS/thread/pypx/' + headers = {'Content-Type': 'application/json', 'accept': 'application/json'} + body = { + "PACSservice": { + "value": pacs_name + }, + "listenerService": { + "value": "default" + }, + "PACSdirective": { + "withFeedBack": True, + "then": "retrieve", + "thenArgs": '', + "dblogbasepath": '/home/dicom/log', + "json_response": False + } + } + body["PACSdirective"].update(directive) + LOG(f"request : {body}") + + try: + response = requests.post(pfdcm_dicom_api, json=body, headers=headers) + d_response = json.loads(response.text) + if d_response['response']['job']['status']: + return d_response + else: + raise Exception(d_response['message']) + except Exception as er: + LOG(er) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 645d77e..3cc6661 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,4 @@ chris_plugin==0.4.0 +pflog==1.2.26 +loguru +requests \ No newline at end of file diff --git a/setup.py b/setup.py index 240b0dd..4878e75 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='pacs_retrieve', + version=get_version('pacs_retrieve.py'), + description='A plugin to retrieve DICOM images from a remote PACS using pfdcm', author='FNNDSC', author_email='dev@babyMRI.org', - url='https://github.com/FNNDSC/python-chrisapp-template', - py_modules=['app'], + url='https://github.com/FNNDSC/pl-pacs_retr', + py_modules=['pacs_retrieve','pfdcm'], install_requires=['chris_plugin'], license='MIT', entry_points={ 'console_scripts': [ - 'commandname = app:main' + 'pacs_retrieve = pacs_retrieve:main' ] }, classifiers=[ diff --git a/tests/test_example.py b/tests/test_example.py index 83d3846..11efbd8 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 pacs_retrieve import parser, main def test_main(tmp_path: Path):