Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
f9cc1d93f2 | |||
09417f7c23 | |||
cf4145aee5 | |||
98ef3fa49a | |||
4b69492d40 | |||
c8e1760470 | |||
861dc16a64 | |||
fc0479ff77 | |||
dae3b1dd45 | |||
88cd2f71a6 | |||
6c29958c13 | |||
682f760c10 | |||
742f24d0e5 | |||
abd4a07dec | |||
48f4d30903 | |||
a908ddf8c2 | |||
71f48e0f5e |
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.snakemake/
|
||||
vp/
|
||||
*.egg-info/
|
||||
build/
|
||||
dist/
|
13
.travis.yml
Normal file
13
.travis.yml
Normal 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
19
LICENSE
Normal 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
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
||||
include cli/Snakefile
|
77
README.md
77
README.md
@ -1,37 +1,82 @@
|
||||
# 2018-snakemake-cli
|
||||
# 2019-snakemake-cli
|
||||
|
||||
An example of parameterizing snakemake workflows with a simple CLI.
|
||||
[](https://travis-ci.org/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
2
cli/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
_program = "bananas"
|
||||
__version__ = "0.1.0"
|
103
cli/command.py
Executable file
103
cli/command.py
Executable 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()
|
||||
|
17
requirements-to-freeze.txt
Normal file
17
requirements-to-freeze.txt
Normal 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
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
snakemake>=5.4.0
|
91
run
91
run
@ -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
29
setup.py
Normal 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
76
test/test_bananas.py
Normal 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user