Compare commits

...

17 Commits
master ... cli

Author SHA1 Message Date
f9cc1d93f2 travis.yml: python 3.7 -> 3.7-dev 2019-01-17 13:15:53 -08:00
09417f7c23 remove unused import 2019-01-17 11:43:21 -08:00
cf4145aee5 python version >= 3.5 2019-01-17 11:41:59 -08:00
98ef3fa49a add shields 2019-01-17 11:41:45 -08:00
4b69492d40 change newline to trigger travis 2019-01-17 11:36:40 -08:00
c8e1760470 add .travis.yml 2019-01-17 11:33:17 -08:00
861dc16a64 banana -> bananas. update gitignore. 2019-01-17 11:24:36 -08:00
fc0479ff77 clean up after test 2019-01-17 11:24:04 -08:00
dae3b1dd45 fix tests and fix the way we are looking for param and config files (use os.getcwd()) 2019-01-17 11:19:32 -08:00
88cd2f71a6 update gh-pages url in setup.py 2019-01-17 11:18:47 -08:00
6c29958c13 manifest should include files, not graft them (graft is for directories) 2019-01-17 10:44:37 -08:00
682f760c10 update readme instructions 2019-01-17 10:07:24 -08:00
742f24d0e5 include Snakefile with Manifest.in to make it run automagically 2019-01-16 23:26:01 -08:00
abd4a07dec move config and params to test/ 2019-01-16 23:16:12 -08:00
48f4d30903 add Snakefile to manifest 2019-01-16 23:16:04 -08:00
a908ddf8c2 update how/where we look for Snakefile 2019-01-16 23:15:18 -08:00
71f48e0f5e convert 2018-snakemake-cli to python package (setup.py)
converts the simple example in 2018-snakemake-cli,
which used a shell script sitting in a directory to
run the Snakemake workflow, into a full-fledged
python package that uses a setup.py file.

this is a way of bridging the complexity of taco
with the simplicity of 2018-snakemake-cli.

documentation/readme has not yet been updated.

tests have not yet been udpated.
2019-01-16 22:58:09 -08:00
17 changed files with 327 additions and 107 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.snakemake/
vp/
*.egg-info/
build/
dist/

13
.travis.yml Normal file
View File

@ -0,0 +1,13 @@
# https://docs.travis-ci.com/user/languages/python/
language: python
python:
- "3.5"
- "3.6"
- "3.7-dev"
# command to install dependencies
install:
- pip install -r requirements.txt
- python setup.py build install
# command to run tests
script:
- pytest

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2019 Charles Reid
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.

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include cli/Snakefile

View File

@ -1,37 +1,82 @@
# 2018-snakemake-cli
# 2019-snakemake-cli
An example of parameterizing snakemake workflows with a simple CLI.
[![travis](https://img.shields.io/travis/charlesreid1/2019-snakemake-cli.svg)](https://travis-ci.org/charlesreid1/2019-snakemake-cli.svg)
[![license](https://img.shields.io/github/license/charlesreid1/2019-snakemake-cli.svg)](https://github.com/charlesreid1/2019-snakemake-cli/blob/master/LICENSE)
An example of a Snakemake command line interface
bundled up as an installable Python package.
This example bundles the Snakefile with the
command line tool, but this tool can also look
in the user's working directory for Snakefiles.
Snakemake functionality is provided through
a command line tool called `bananas`.
# Quickstart
This runs through the installation and usage
of 2019-snakemake-cli.
## Installing banana
Start by setting up a virtual environment,
and install the required packages into the
virtual environment:
Usage:
```
./run <workflow_file> <parameters_file>
pip install -r requirements.txt
```
e.g.
Now install the `bananas` command line tool:
```
python setup.py build install
```
Now you can run
```
which banana
```
and you should see `bananas` in your Python
distribution's `bin/` directory.
## Running banana
Move to the `test/` directory and run the tests
with the provided config and params files.
Run the hello workflow with Amy params:
```
rm -f hello.txt
./run workflow-hello params-amy
bananas workflow-hello params-amy
```
creates `hello.txt` with "hello amy" in it, while
Run the hello workflow with Beth params:
```
rm -f hello.txt
./run workflow-hello params-beth
bananas workflow-hello params-beth
```
creates `hello.txt` with "hello beth" in it.
Here, the workflow file `workflow-hello.json` specifes the target
`hello.txt`, while the parameters file `params-amy` parameterizes
the workflow with the name "amy".
Likewise,
Run the goodbye workflow with Beth params:
```
rm -f goodbye.txt
./run workflow-goodbye params-beth
```
will put `goodbye beth` in `goodbye.txt`.
# Details
The entrypoint of the command line interface is
the `main()` function of `cli/command.py`.
The location of the Snakefile is `cli/Snakefile`.
An alternative arrangement would be for users
to provide a Snakefile via rules in the working
directory, or via a Github URL or a remote URL.
All workflows use the same set of Snakemake rules in `Snakefile`.

2
cli/__init__.py Normal file
View File

@ -0,0 +1,2 @@
_program = "bananas"
__version__ = "0.1.0"

103
cli/command.py Executable file
View File

@ -0,0 +1,103 @@
"""
Command line interface driver for snakemake workflows
"""
import argparse
import os.path
import snakemake
import sys
import pprint
import json
from . import _program
thisdir = os.path.abspath(os.path.dirname(__file__))
parentdir = os.path.join(thisdir,'..')
cwd = os.getcwd()
def main(sysargs = sys.argv[1:]):
parser = argparse.ArgumentParser(prog = _program, description='bananas: run snakemake workflows', usage='''bananas <workflow> <parameters> [<target>]
bananas: run snakemake workflows, using the given workflow name & parameters file.
''')
parser.add_argument('workflowfile')
parser.add_argument('paramsfile')
parser.add_argument('-n', '--dry-run', action='store_true')
parser.add_argument('-f', '--force', action='store_true')
args = parser.parse_args(sysargs)
# first, find the Snakefile
snakefile_this = os.path.join(thisdir,"Snakefile")
snakefile_parent = os.path.join(parentdir,"Snakefile")
if os.path.exists(snakefile_this):
snakefile = snakefile_this
elif os.path.exists(snakefile_parent):
snakefile = snakefile_parent
else:
msg = 'Error: cannot find Snakefile at any of the following locations:\n'
msg += '{}\n'.format(snakefile_this)
msg += '{}\n'.format(snakefile_parent)
sys.stderr.write(msg)
sys.exit(-1)
# next, find the workflow config file
workflowfile = None
w1 = os.path.join(cwd,args.workflowfile)
w2 = os.path.join(cwd,args.workflowfile+'.json')
if os.path.exists(w1) and not os.path.isdir(w1):
workflowfile = w1
elif os.path.exists(w2) and not os.path.isdir(w2):
workflowfile = w2
if not workflowfile:
msg = 'Error: cannot find workflowfile {} or {} '.format(w1,w2)
msg += 'in directory {}\n'.format(cwd)
sys.stderr.write(msg)
sys.exit(-1)
# next, find the workflow params file
paramsfile = None
p1 = os.path.join(cwd,args.paramsfile)
p2 = os.path.join(cwd,args.paramsfile+'.json')
if os.path.exists(p1) and not os.path.isdir(p1):
paramsfile = p1
elif os.path.exists(p2) and not os.path.isdir(p2):
paramsfile = p2
if not paramsfile:
msg = 'Error: cannot find paramsfile {} or {} '.format(p1,p2)
msg += 'in directory {}\n'.format(cwd)
sys.stderr.write(msg)
sys.exit(-1)
with open(workflowfile, 'rt') as fp:
workflow_info = json.load(fp)
target = workflow_info['workflow_target']
config = dict()
print('--------')
print('details!')
print('\tsnakefile: {}'.format(snakefile))
print('\tconfig: {}'.format(workflowfile))
print('\tparams: {}'.format(paramsfile))
print('\ttarget: {}'.format(target))
print('--------')
# run bananas!!
status = snakemake.snakemake(snakefile, configfile=paramsfile,
targets=[target], printshellcmds=True,
dryrun=args.dry_run, forceall=args.force,
config=config)
if status: # translate "success" into shell exit code of 0
return 0
return 1
if __name__ == '__main__':
main()

View File

@ -0,0 +1,17 @@
appdirs==1.4.3
certifi==2018.11.29
chardet==3.0.4
ConfigArgParse==0.14.0
datrie==0.7.1
docutils==0.14
gitdb2==2.0.5
GitPython==2.1.11
idna==2.8
jsonschema==2.6.0
PyYAML==3.13
ratelimiter==1.2.0.post0
requests==2.21.0
smmap2==2.0.5
snakemake==5.4.0
urllib3==1.24.1
wrapt==1.11.0

1
requirements.txt Normal file
View File

@ -0,0 +1 @@
snakemake>=5.4.0

91
run
View File

@ -1,91 +0,0 @@
#! /usr/bin/env python
"""
Execution script for snakemake workflows.
"""
import argparse
import os.path
import snakemake
import sys
import pprint
import json
thisdir = os.path.abspath(os.path.dirname(__file__))
def main(args):
# first, find the Snakefile
snakefile = os.path.join(thisdir, 'Snakefile')
if not os.path.exists(snakefile):
sys.stderr.write('Error: cannot find Snakefile at {}\n'.format(snakefile))
sys.exit(-1)
# next, find the workflow config file
workflowfile = None
if os.path.exists(args.workflowfile) and not os.path.isdir(args.workflowfile):
workflowfile = args.workflowfile
else:
for suffix in ('', '.json'):
tryfile = os.path.join(thisdir, args.workflowfile + suffix)
if os.path.exists(tryfile) and not os.path.isdir(tryfile):
sys.stderr.write('Found workflowfile at {}\n'.format(tryfile))
workflowfile = tryfile
break
if not workflowfile:
sys.stderr.write('Error: cannot find workflowfile {}\n'.format(args.workflowfile))
sys.exit(-1)
# next, find the workflow params file
paramsfile = None
if os.path.exists(args.paramsfile) and not os.path.isdir(args.paramsfile):
paramsfile = args.paramsfile
else:
for suffix in ('', '.json'):
tryfile = os.path.join(thisdir, args.paramsfile + suffix)
if os.path.exists(tryfile) and not os.path.isdir(tryfile):
sys.stderr.write('Found paramsfile at {}\n'.format(tryfile))
paramsfile = tryfile
break
if not paramsfile:
sys.stderr.write('Error: cannot find paramsfile {}\n'.format(args.paramsfile))
sys.exit(-1)
with open(workflowfile, 'rt') as fp:
workflow_info = json.load(fp)
target = workflow_info['workflow_target']
config = dict()
print('--------')
print('details!')
print('\tsnakefile: {}'.format(snakefile))
print('\tconfig: {}'.format(workflowfile))
print('\tparams: {}'.format(paramsfile))
print('\ttarget: {}'.format(target))
print('--------')
# run!!
status = snakemake.snakemake(snakefile, configfile=paramsfile,
targets=[target], printshellcmds=True,
dryrun=args.dry_run, config=config)
if status: # translate "success" into shell exit code of 0
return 0
return 1
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='run snakemake workflows', usage='''run <workflow> <parameters> [<target>]
Run snakemake workflows, using the given workflow name & parameters file.
''')
parser.add_argument('workflowfile')
parser.add_argument('paramsfile')
parser.add_argument('-n', '--dry-run', action='store_true')
args = parser.parse_args()
sys.exit(main(args))

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
from setuptools import setup, find_packages
import glob
import os
with open('requirements.txt') as f:
required = [x for x in f.read().splitlines() if not x.startswith("#")]
# Note: the _program variable is set in __init__.py.
# it determines the name of the package/final command line tool.
from cli import __version__, _program
setup(name='bananas',
version=__version__,
packages=['cli'],
test_suite='pytest.collector',
tests_require=['pytest'],
description='bananas command line interface',
url='https://charlesreid1.github.io/2019-snakemake-cli',
author='@charlesreid1',
author_email='cmreid@ucdavis.edu',
license='MIT',
entry_points="""
[console_scripts]
{program} = cli.command:main
""".format(program = _program),
install_requires=required,
include_package_data=True,
keywords=[],
zip_safe=False)

76
test/test_bananas.py Normal file
View File

@ -0,0 +1,76 @@
from unittest import TestCase
from subprocess import call, Popen, PIPE
import os
import shutil, tempfile
from os.path import isdir, join
"""
test banana
this test will run bananas with the test
config and params provided in the test dir.
this test will also show how to run tests where
failure is expected (i.e., checking that we handle
invalid parameters).
each test has a unittest TestCase defined.
pytest will automatically find these tests.
"""
class TestBananas(TestCase):
"""
simple bananas test class
This uses the subprocess PIPE var
to capture system input and output,
since we are running bananas from the
command line directly using subprocess.
"""
@classmethod
def setUpClass(self):
"""
set up a bananas workflow test.
we are using the existing test/ dir
as our working dir, so no setup to do.
if we were expecting the user to provide
a Snakefile, this is where we would set
up a test Snakefile.
"""
pass
def test_hello(self):
"""
test hello workflow
"""
command_prefix = ['bananas','workflow-hello']
params = ['params-amy','params-beth']
pwd = os.path.abspath(os.path.dirname(__file__))
for param in params:
command = command_prefix + [param]
p = Popen(command, cwd=pwd, stdout=PIPE, stderr=PIPE).communicate()
p_out = p[0].decode('utf-8').strip()
p_err = p[1].decode('utf-8').strip()
self.assertIn('details',p_out)
# clean up
call(['rm','-f','hello.txt'])
@classmethod
def tearDownClass(self):
"""
clean up after the tests
"""
pass