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
|
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
|
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
|
Run the goodbye workflow with Beth params:
|
||||||
`hello.txt`, while the parameters file `params-amy` parameterizes
|
|
||||||
the workflow with the name "amy".
|
|
||||||
|
|
||||||
Likewise,
|
|
||||||
|
|
||||||
```
|
```
|
||||||
rm -f goodbye.txt
|
rm -f goodbye.txt
|
||||||
./run workflow-goodbye params-beth
|
./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