mirror of https://github.com/wooey/Wooey.git
Initial commit.
This commit is contained in:
parent
e6eb2f013a
commit
26f8a9668a
|
@ -1,54 +1,51 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
# Packages
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
*.egg-info
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
.tox
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# Complexity
|
||||
output/*.html
|
||||
output/*/index.html
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
# Sphinx
|
||||
docs/_build
|
||||
|
||||
.webassets-cache
|
||||
|
||||
# Virtualenvs
|
||||
env
|
||||
env*
|
||||
|
||||
|
||||
.idea
|
||||
dev.db
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Config file for automatic testing at travis-ci.org
|
||||
|
||||
language: python
|
||||
|
||||
python:
|
||||
- "3.3"
|
||||
- "2.7"
|
||||
|
||||
install: pip install -r requirements/dev.txt
|
||||
|
||||
script: py.test tests
|
24
LICENSE
24
LICENSE
|
@ -1,22 +1,12 @@
|
|||
The MIT License (MIT)
|
||||
Copyright (c) 2014, Martin Fitzpatrick
|
||||
All rights reserved.
|
||||
|
||||
Copyright (c) 2015 Martin Fitzpatrick
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
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:
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
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.
|
||||
* Neither the name of Wooey nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
web: gunicorn wooey.app:create_app\(\) -b 0.0.0.0:$PORT -w 3
|
|
@ -0,0 +1,106 @@
|
|||
Wooey
|
||||
=====
|
||||
|
||||
Automated web UIs for Python scripts
|
||||
|
||||
## About
|
||||
|
||||
Wooey is a simple web interface (built on Flask) to run command line Python scripts. Think of it as an easy solution
|
||||
to routine data analysis, file processing, or anything else. Get your scripts up on the web.
|
||||
|
||||
![Welcome](welcome_to_wooey
|
||||
|
||||
![mock_argparse example script](mock_argparse_example
|
||||
|
||||
![plot_some_numbers script with docs](plot_some_numbers_with_documentation
|
||||
|
||||
![User job listing](user_job_list
|
||||
|
||||
![Job with success 1](job_success_1.png)
|
||||
![Job with success 2](job_success_2
|
||||
|
||||
![Job with error console](job_with_error
|
||||
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
First, set your app's secret key as an environment variable. For example, example add the following to ``.bashrc`` or ``.bash_profile``.
|
||||
|
||||
|
||||
export WOOEY_SECRET='something-really-secret'
|
||||
|
||||
|
||||
Then run the following commands to bootstrap your environment.
|
||||
|
||||
|
||||
git clone https://github.com/mfitzp/wooey
|
||||
cd wooey
|
||||
pip install -r requirements/dev.txt
|
||||
python manage.py server
|
||||
|
||||
You will see a pretty welcome screen.
|
||||
|
||||
Once you have installed your DBMS, run the following to create your app's database tables and perform the initial migration:
|
||||
|
||||
python manage.py db init
|
||||
python manage.py db migrate
|
||||
python manage.py db upgrade
|
||||
python manage.py server
|
||||
|
||||
The server will start up. But you will see it is empty! To add the example scripts to the database and allow you to test
|
||||
also run:
|
||||
|
||||
python manage.py build_scripts
|
||||
python manage.py find_scripts
|
||||
|
||||
This will build (create JSON for Python scripts using argparse) and then add them to the database.
|
||||
|
||||
In another shell, you can start the temporary dev 'daemon' (which is nothing of the sort, yet) using:
|
||||
|
||||
python manage.py start_daemon
|
||||
|
||||
|
||||
Deployment
|
||||
----------
|
||||
|
||||
In your production environment, make sure the ``WOOEY_ENV`` environment variable is set to ``"prod"``.
|
||||
|
||||
|
||||
Shell
|
||||
-----
|
||||
|
||||
To open the interactive shell, run ::
|
||||
|
||||
python manage.py shell
|
||||
|
||||
By default, you will have access to ``app``, ``db``, and the ``User`` model. This can be used to quickly recreate database tables
|
||||
during development, i.e. delete `dev.db` (SQLite) and then from the shell enter:
|
||||
|
||||
db.create_all()
|
||||
|
||||
|
||||
Running Tests
|
||||
-------------
|
||||
|
||||
To run all tests, run ::
|
||||
|
||||
python manage.py test
|
||||
|
||||
|
||||
Migrations
|
||||
----------
|
||||
|
||||
Whenever a database migration needs to be made. Run the following commmands:
|
||||
::
|
||||
|
||||
python manage.py db migrate
|
||||
|
||||
This will generate a new migration script. Then run:
|
||||
::
|
||||
|
||||
python manage.py db upgrade
|
||||
|
||||
To apply the migration.
|
||||
|
||||
For a full migration command reference, run ``python manage.py db --help``.
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "wooey",
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import time
|
||||
|
||||
from flask.ext.script import Manager, Shell, Server
|
||||
from flask.ext.migrate import MigrateCommand
|
||||
|
||||
from wooey.app import create_app
|
||||
from wooey.user.models import User
|
||||
from wooey.settings import DevConfig, ProdConfig
|
||||
from wooey.database import db
|
||||
|
||||
from wooey.lib.utils import find_files
|
||||
from wooey.lib.python import collect_argparses
|
||||
|
||||
from wooey.public.models import Script, Job, STATUS_WAITING, STATUS_COMPLETE, STATUS_ERROR, STATUS_RUNNING
|
||||
|
||||
import select
|
||||
|
||||
if os.environ.get("WOOEY_ENV") == 'prod':
|
||||
app = create_app(ProdConfig)
|
||||
else:
|
||||
app = create_app(DevConfig)
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
TEST_PATH = os.path.join(HERE, 'tests')
|
||||
|
||||
manager = Manager(app)
|
||||
|
||||
|
||||
def _make_context():
|
||||
"""Return context dict for a shell session so you can access
|
||||
app, db, and the User model by default.
|
||||
"""
|
||||
return {'app': app, 'db': db, 'User': User}
|
||||
|
||||
|
||||
@manager.command
|
||||
def test():
|
||||
"""Run the tests."""
|
||||
import pytest
|
||||
exit_code = pytest.main([TEST_PATH, '--verbose'])
|
||||
return exit_code
|
||||
|
||||
manager.add_command('server', Server())
|
||||
manager.add_command('shell', Shell(make_context=_make_context))
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
|
||||
@manager.command
|
||||
def build_scripts():
|
||||
'''
|
||||
Build script JSON data for UIs (default), don't overwrite
|
||||
|
||||
:return:
|
||||
'''
|
||||
|
||||
scripts = find_files(os.path.join('.', 'scripts'), '.py')
|
||||
collect_argparses(scripts)
|
||||
|
||||
|
||||
@manager.command
|
||||
def find_scripts():
|
||||
'''
|
||||
Loader that iterates over script config folder, reading JSON files and updating the
|
||||
admin config to store. The config itself is not loaded (parsed and managed on output).
|
||||
|
||||
:return:
|
||||
'''
|
||||
|
||||
jsons = find_files(os.path.join('.', 'scripts'), '.json')
|
||||
|
||||
for json_filename in jsons:
|
||||
# Extract to dict structure, then interrogate the database to see if we already have this and update
|
||||
with open(json_filename, 'r') as f:
|
||||
jo = json.load(f)
|
||||
|
||||
full_path = os.path.realpath(json_filename)
|
||||
|
||||
# Try query the db for object with the same (?hash)
|
||||
script = Script.query.filter_by(config_path=full_path).first() # Will be only one
|
||||
if not script:
|
||||
script = Script(config_path=full_path) # Create it
|
||||
|
||||
# Amend the object
|
||||
script.exec_path = jo['program']['path']
|
||||
script.description = jo['program']['description']
|
||||
script.name = jo['program']['name']
|
||||
|
||||
if 'documentation' in jo['program']:
|
||||
script.doc_path = jo['program']['documentation']
|
||||
elif os.path.exists(os.path.splitext(script.exec_path)[0] + '.md'):
|
||||
script.doc_path = os.path.splitext(script.exec_path)[0] + '.md'
|
||||
elif os.path.exists(os.path.splitext(script.config_path)[0] + '.md'):
|
||||
script.doc_path = os.path.splitext(script.config_path)[0] + '.md'
|
||||
|
||||
db.session.add(script)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def read_all_so_far(proc, out=''):
|
||||
while (select.select([proc.stdout], [], [], 0)[0] != []):
|
||||
out += proc.stdout.read(1)
|
||||
return out
|
||||
|
||||
|
||||
@manager.command
|
||||
def start_daemon():
|
||||
'''
|
||||
Infinitely loop, checking the database for waiting Jobs and executing them.
|
||||
|
||||
This is nasty.
|
||||
|
||||
Keep handles to subprocesses so we can kill them if receiving signal (via database) from user/admin.
|
||||
|
||||
|
||||
:return:
|
||||
'''
|
||||
|
||||
# Track the Popen objects for each, dict to map via Job PK
|
||||
pids = {}
|
||||
|
||||
# Initialise by setting all running jobs to error (must have died on previous execution)
|
||||
Job.query.filter(Job.status == STATUS_RUNNING).update({Job.status: STATUS_ERROR})
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
||||
while True:
|
||||
# Query the database for running jobs
|
||||
|
||||
jobs_running = Job.query.filter(Job.status == STATUS_RUNNING)
|
||||
no_of_running_jobs = jobs_running.count()
|
||||
|
||||
for job in jobs_running:
|
||||
print(job)
|
||||
|
||||
|
||||
# Check each vs. running subprocesses to see if still active (if not, update database with new status)
|
||||
for k, v in pids.items():
|
||||
|
||||
# Check process status and flush to file
|
||||
v['pid'].poll()
|
||||
|
||||
if v['pid'].returncode is None: # Still running
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
v['out'].close()
|
||||
except IOError:
|
||||
pass
|
||||
|
||||
if v['pid'].returncode == 0: # Complete
|
||||
job = Job.query.get(k)
|
||||
job.status = STATUS_COMPLETE
|
||||
|
||||
else: # Error
|
||||
job = Job.query.get(k)
|
||||
job.status = STATUS_ERROR
|
||||
|
||||
# Delete the process object
|
||||
del pids[k]
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# If number of running jobs < MAX_RUNNING_JOBS start some more
|
||||
if no_of_running_jobs < 5:
|
||||
jobs_to_run = Job.query.filter(Job.status == STATUS_WAITING).order_by(Job.created_at.desc())
|
||||
for job in jobs_to_run:
|
||||
|
||||
# Get the config settings from the database (dict of values via JSON)
|
||||
if job.config:
|
||||
config = json.loads(job.config)
|
||||
else:
|
||||
config = {}
|
||||
|
||||
# Get args from the config dict (stored in job)
|
||||
args = config['args']
|
||||
|
||||
# Add the executable to the beginning of the sequence
|
||||
args.insert(0, job.script.exec_path)
|
||||
|
||||
print(' '.join(args))
|
||||
|
||||
out = open(os.path.join(job.path, 'STDOUT'), 'w')
|
||||
# Run the command and store the object for future use
|
||||
pids[job.id] = {
|
||||
'pid': subprocess.Popen(args, cwd=job.path, stdout=out, stderr=subprocess.STDOUT),
|
||||
'out': out,
|
||||
}
|
||||
|
||||
# Update the job status
|
||||
job.status = STATUS_RUNNING
|
||||
db.session.commit()
|
||||
|
||||
print(pids)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
manager.run()
|
|
@ -0,0 +1 @@
|
|||
Generic single-database configuration.
|
|
@ -0,0 +1,45 @@
|
|||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
|
@ -0,0 +1,22 @@
|
|||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = ${repr(up_revision)}
|
||||
down_revision = ${repr(down_revision)}
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
def upgrade():
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade():
|
||||
${downgrades if downgrades else "pass"}
|
|
@ -0,0 +1,80 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 280825fe3a4f
|
||||
Revises: None
|
||||
Create Date: 2015-03-08 20:53:26.958201
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '280825fe3a4f'
|
||||
down_revision = None
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('scripts',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=80), nullable=False),
|
||||
sa.Column('display_name', sa.String(length=80), nullable=True),
|
||||
sa.Column('exec_path', sa.String(length=255), nullable=False),
|
||||
sa.Column('config_path', sa.String(length=255), nullable=False),
|
||||
sa.Column('doc_path', sa.String(length=255), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('description', sa.String(length=255), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('config_path'),
|
||||
sa.UniqueConstraint('doc_path'),
|
||||
sa.UniqueConstraint('exec_path')
|
||||
)
|
||||
op.create_table('users',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=80), nullable=False),
|
||||
sa.Column('email', sa.String(length=80), nullable=False),
|
||||
sa.Column('password', sa.String(length=128), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('first_name', sa.String(length=30), nullable=True),
|
||||
sa.Column('last_name', sa.String(length=30), nullable=True),
|
||||
sa.Column('active', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_admin', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('email'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('roles',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=80), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('jobs',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('script_id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('path', sa.String(length=255), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
||||
sa.Column('status', sa.Enum('W', 'R', 'C', 'X'), nullable=False),
|
||||
sa.Column('config', sa.String(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['script_id'], ['scripts.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('path')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('jobs')
|
||||
op.drop_table('roles')
|
||||
op.drop_table('users')
|
||||
op.drop_table('scripts')
|
||||
### end Alembic commands ###
|
|
@ -0,0 +1,3 @@
|
|||
# Included because many Paas's require a requirements.txt file in the project root
|
||||
# Just installs the production requirements.
|
||||
-r requirements/prod.txt
|
|
@ -0,0 +1,10 @@
|
|||
# Everything the developer needs in addition to the production requirements
|
||||
-r prod.txt
|
||||
|
||||
# Testing
|
||||
pytest>=2.6.3
|
||||
webtest
|
||||
factory-boy==2.4.1
|
||||
|
||||
# Management script
|
||||
Flask-Script
|
|
@ -0,0 +1,40 @@
|
|||
# Everything needed in production
|
||||
|
||||
# Flask
|
||||
Flask==0.10.1
|
||||
MarkupSafe==0.23
|
||||
Werkzeug==0.9.6
|
||||
Jinja2==2.7.3
|
||||
itsdangerous==0.24
|
||||
|
||||
# Database
|
||||
Flask-SQLAlchemy==2.0
|
||||
SQLAlchemy==0.9.8
|
||||
|
||||
# Migrations
|
||||
Flask-Migrate==1.3.0
|
||||
|
||||
# Forms
|
||||
Flask-WTF==0.10.3
|
||||
WTForms==2.0.1
|
||||
|
||||
# Deployment
|
||||
gunicorn>=19.1.1
|
||||
|
||||
# Assets
|
||||
Flask-Assets==0.10
|
||||
cssmin>=0.2.0
|
||||
jsmin>=2.0.11
|
||||
|
||||
# Auth
|
||||
Flask-Login==0.2.11
|
||||
Flask-Bcrypt==0.6.0
|
||||
|
||||
# Caching
|
||||
Flask-Cache>=0.13.1
|
||||
|
||||
# Debug toolbar
|
||||
Flask-DebugToolbar==0.9.1
|
||||
|
||||
# Admin
|
||||
Flask-Admin=1.1.0
|
|
@ -0,0 +1,263 @@
|
|||
{
|
||||
"program": {
|
||||
"path": "/Users/mxf793/repos/wooey/scripts/bar.py",
|
||||
"epilog": null,
|
||||
"name": "bar",
|
||||
"description": null
|
||||
},
|
||||
"parser": {
|
||||
"prefix_chars": "-",
|
||||
"argument_default": null
|
||||
},
|
||||
"required": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-f",
|
||||
"--file"
|
||||
],
|
||||
"display_name": "file",
|
||||
"help": "csv file containing dumped data from plsmulti",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "FileChooser",
|
||||
"type": "FileType"
|
||||
}
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--statistic"
|
||||
],
|
||||
"display_name": "statistic",
|
||||
"help": "Statistical test: tind (t-independent), trel (t-related)",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": [
|
||||
"tind",
|
||||
"trel"
|
||||
]
|
||||
},
|
||||
"widget": "Dropdown",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-m",
|
||||
"--metabolite"
|
||||
],
|
||||
"display_name": "metabolite",
|
||||
"help": "name of metabolite to plot graph for",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-g",
|
||||
"--group"
|
||||
],
|
||||
"display_name": "axisgroup",
|
||||
"help": "Regular Expression pattern for axis cluster",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-s",
|
||||
"--search"
|
||||
],
|
||||
"display_name": "search",
|
||||
"help": "Show classes matching this regex",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-t"
|
||||
],
|
||||
"display_name": "tests",
|
||||
"help": "CLASS,CLASS combinations to statisically test (space-separated)",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--xlabel"
|
||||
],
|
||||
"display_name": "xlabel",
|
||||
"help": "X axis label",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--ylabel"
|
||||
],
|
||||
"display_name": "ylabel",
|
||||
"help": "Y axis label",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--ylim"
|
||||
],
|
||||
"display_name": "ylim",
|
||||
"help": "min,max for y axis",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "float"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--title"
|
||||
],
|
||||
"display_name": "title",
|
||||
"help": "Graph title",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--dpi"
|
||||
],
|
||||
"display_name": "dpi",
|
||||
"help": "DPI of output (TIF format only)",
|
||||
"default": 72,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--format"
|
||||
],
|
||||
"display_name": "format",
|
||||
"help": "File format for output",
|
||||
"default": "png",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-b",
|
||||
"--batch"
|
||||
],
|
||||
"display_name": "batch_mode",
|
||||
"help": "Batch mode (process all metabolite matches with same parameters)",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--multiplot"
|
||||
],
|
||||
"display_name": "multiplot",
|
||||
"help": "Plot more than one graph per figure",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--shareyaxis"
|
||||
],
|
||||
"display_name": "shareyaxis",
|
||||
"help": "Share y axis scale",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-d",
|
||||
"--display"
|
||||
],
|
||||
"display_name": "display",
|
||||
"help": "Display graphs to screen",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--annotate"
|
||||
],
|
||||
"display_name": "annotate",
|
||||
"help": "show command annotation for generation",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,271 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from numpy.random import uniform, seed
|
||||
from collections import OrderedDict
|
||||
from matplotlib.mlab import griddata
|
||||
from matplotlib import ticker
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
import numpy as np
|
||||
import re
|
||||
import os
|
||||
import random
|
||||
import csv
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
import utils
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("-m", "--metabolite", dest="metabolite", default='',
|
||||
help="name of metabolite to plot graph for")
|
||||
|
||||
parser.add_argument("-f", "--file", dest="file", type=argparse.FileType('rU'), required=True,
|
||||
help="csv file containing dumped data from plsmulti", metavar="FILE")
|
||||
|
||||
parser.add_argument("-b", "--batch", action="store_true", dest="batch_mode", default=False,
|
||||
help="Batch mode (process all metabolite matches with same parameters)")
|
||||
|
||||
parser.add_argument("-g", "--group", dest="axisgroup", default=None,
|
||||
help="Regular Expression pattern for axis cluster")
|
||||
|
||||
parser.add_argument("--multiplot", action="store_true", dest="multiplot", default=False,
|
||||
help="Plot more than one graph per figure")
|
||||
|
||||
parser.add_argument("--shareyaxis", action="store_true", dest="shareyaxis", default=False,
|
||||
help="Share y axis scale")
|
||||
|
||||
parser.add_argument("-s", "--search", dest="search", default=None,
|
||||
help="Show classes matching this regex")
|
||||
|
||||
parser.add_argument("-t", dest="tests", default=None,
|
||||
help="CLASS,CLASS combinations to statisically test (space-separated)")
|
||||
|
||||
parser.add_argument("-d", "--display", action="store_true", dest="display", default=False,
|
||||
help="Display graphs to screen")
|
||||
|
||||
parser.add_argument("--statistic", dest="statistic", default=None, choices=['tind', 'trel'],
|
||||
help="Statistical test: tind (t-independent), trel (t-related)")
|
||||
|
||||
parser.add_argument("--xlabel", dest="xlabel", default='',
|
||||
help="X axis label")
|
||||
|
||||
parser.add_argument("--ylabel", dest="ylabel", default='',
|
||||
help="Y axis label")
|
||||
|
||||
parser.add_argument("--ylim", dest="ylim", default=None, type=float,
|
||||
help="min,max for y axis")
|
||||
|
||||
parser.add_argument("--title", dest="title", default=None,
|
||||
help="Graph title")
|
||||
|
||||
parser.add_argument("--dpi", dest="dpi", default=72, type=int,
|
||||
help="DPI of output (TIF format only)")
|
||||
|
||||
parser.add_argument("--format", dest="format", default='png',
|
||||
help="File format for output")
|
||||
|
||||
parser.add_argument("--annotate", action="store_true", dest="annotate", default=False,
|
||||
help="show command annotation for generation")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
colors = ['#348ABD', '#7A68A6', '#A60628', '#467821', '#CF4457', '#188487', '#E24A33', 'r', 'b', 'g', 'c', 'm', 'y', 'k', 'w']
|
||||
|
||||
# Extract file root from the edge file name
|
||||
filebase = os.path.splitext(args.file.name)[0]
|
||||
[null, sep, string] = filebase.partition('-')
|
||||
filesuff = sep + string
|
||||
nodes = OrderedDict()
|
||||
|
||||
(metabolites, allquants, globalylim) = utils.read_metabolite_datafile(args.file, args)
|
||||
|
||||
# Turn off interactive plotting, speed up
|
||||
plt.ioff()
|
||||
figures = list()
|
||||
multiax = None
|
||||
ymaxf = 0
|
||||
|
||||
for metabolite in metabolites[:]:
|
||||
print "Processing %s" % metabolite
|
||||
quants = allquants[metabolite]
|
||||
|
||||
if args.search:
|
||||
okeys = quants.keys()
|
||||
for label in quants.keys():
|
||||
match = re.search(args.search, label)
|
||||
if not match:
|
||||
del quants[label]
|
||||
print "Filter matching classes '%s' with '%s' gives '%s'" % (', '.join(okeys), args.search, ', '.join(quants.keys()))
|
||||
if len(quants.keys()) == 0:
|
||||
print "Nothing left!; deleting metabolite"
|
||||
metabolites.remove(metabolite)
|
||||
continue
|
||||
|
||||
# Apply regex to split axis groups if specified
|
||||
if args.axisgroup:
|
||||
|
||||
axisgroups = defaultdict(list)
|
||||
for label in quants.keys():
|
||||
match = re.search(args.axisgroup, label)
|
||||
if match:
|
||||
axisgroups[match.group(1)].append(label)
|
||||
#else:
|
||||
# axisgroups['non-matched'].append(label)
|
||||
if len(axisgroups) == 0:
|
||||
print "No matching classes found for axisgroup regex, try again"
|
||||
exit()
|
||||
print "Axis groupings completed: " + ", ".join(axisgroups)
|
||||
|
||||
else:
|
||||
# No groups, create dummy for 'all'
|
||||
axisgroups = {'not-grouped': quants.keys()}
|
||||
|
||||
ind = list()
|
||||
graph = {
|
||||
'ticks': list(),
|
||||
'means': list(),
|
||||
'stddev': list(),
|
||||
'colors': list(),
|
||||
'ylim': (0, 0),
|
||||
}
|
||||
|
||||
# Sort the axis groups so follow some sort of logical order
|
||||
axisgroupsk = axisgroups.keys()
|
||||
axisgroupsk.sort()
|
||||
ymin = 0
|
||||
ymax = 0
|
||||
for counter, axisgroup in enumerate(axisgroupsk):
|
||||
l = axisgroups[axisgroup]
|
||||
l.sort()
|
||||
graph['ticks'] = graph['ticks'] + l
|
||||
for key in l:
|
||||
graph['means'].append(np.mean(quants[key]))
|
||||
graph['stddev'].append(np.std(quants[key]))
|
||||
ymax = max(ymax, max(quants[key]))
|
||||
ymin = min(ymin, min(quants[key]))
|
||||
ind = ind + list(np.arange(1 + counter + len(ind), 1 + counter + len(ind) + len(l)))
|
||||
graph['colors'] = graph['colors'] + colors[0: len(l)]
|
||||
|
||||
num = len(ind)
|
||||
width = 0.8 # the width of the bars: can also be len(x) sequence
|
||||
if args.shareyaxis:
|
||||
graph['ylim'] = globalylim
|
||||
else:
|
||||
graph['ylim'] = (ymin, ymax)
|
||||
|
||||
# Split error pos+neg to give single up/down errorbar in correct direction
|
||||
yperr = [(1, 0)[x > 0] for x in graph['means']]
|
||||
ynerr = [(1, 0)[x < 0] for x in graph['means']]
|
||||
yperr = np.array(list(yperr)) * np.array(list(graph['stddev']))
|
||||
ynerr = np.array(list(ynerr)) * np.array(list(graph['stddev']))
|
||||
|
||||
if args.multiplot:
|
||||
# Keep using same figure, append subplots
|
||||
if not multiax:
|
||||
#adjustprops = dict(left=0.1, bottom=0.1, right=0.97, top=0.93, wspace=0.2, hspace=0.2)
|
||||
fig = plt.figure()
|
||||
multiax = fig.add_subplot(1, len(metabolites), 1)
|
||||
figures.append(multiax)
|
||||
else:
|
||||
if not args.shareyaxis:
|
||||
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1, sharey=multiax)
|
||||
plt.setp(fp.get_yticklabels(), visible=False)
|
||||
plt.setp(fp.get_yaxis(), visible=False)
|
||||
else:
|
||||
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1)
|
||||
|
||||
figures.append(fp)
|
||||
else:
|
||||
# New figure
|
||||
figures.append(plt.figure())
|
||||
|
||||
plt.bar(ind, graph['means'], width, label=graph['ticks'], color=graph['colors'], ecolor='black', align='center', yerr=[yperr, ynerr])
|
||||
|
||||
plt.xticks(ind, graph['ticks']) #, rotation=45)
|
||||
|
||||
if args.title:
|
||||
plt.title(args.title)
|
||||
else:
|
||||
plt.title(metabolite)
|
||||
|
||||
plt.gca().xaxis.set_label_text(args.xlabel)
|
||||
plt.gca().yaxis.set_label_text(args.ylabel)
|
||||
|
||||
# Add some padding either side of graphs
|
||||
plt.xlim(ind[0] - 1, ind[-1] + 1)
|
||||
|
||||
if args.annotate:
|
||||
utils.annotate_plot(plt, options)
|
||||
|
||||
fig = plt.gcf()
|
||||
# Horizontal axis through zero
|
||||
plt.axhline(0, color='k')
|
||||
|
||||
if args.multiplot: # Scale the multiplots a bit more reasonably
|
||||
fig.set_size_inches(5 + len(metabolites) * 3, 6)
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
|
||||
# Get ylimits for significance bars
|
||||
ymin, ymax = graph['ylim']
|
||||
|
||||
sigs = list()
|
||||
if args.tests:
|
||||
from scipy import stats
|
||||
tests = args.tests.split()
|
||||
|
||||
for test in tests:
|
||||
classt = test.split(',')
|
||||
if args.statistic == 'trel':
|
||||
t, p = stats.ttest_rel(quants[classt[0]], quants[classt[1]])
|
||||
else:
|
||||
t, p = stats.ttest_ind(quants[classt[0]], quants[classt[1]])
|
||||
|
||||
# sigs.append( { a:classt[0], b:classt[1], p:p } )
|
||||
# We now lave a list of significant comparisons in significant
|
||||
bxstart = ind[graph['ticks'].index(classt[0])]
|
||||
bxend = ind[graph['ticks'].index(classt[1])]
|
||||
|
||||
# Nudge up to prevent overlapping
|
||||
ymax = ymax + ((ymax - ymin) * 0.1)
|
||||
|
||||
# Plot the bar on the graph
|
||||
c = patches.FancyArrowPatch(
|
||||
(bxstart, ymax),
|
||||
(bxend, ymax),
|
||||
arrowstyle="|-|", lw=2)
|
||||
ax = plt.gca()
|
||||
|
||||
plt.text(bxstart + (bxend - float(bxstart)) / 2, ymax + (ymax * 0.01), utils.sigstars(p), size=16, ha='center', va='bottom')
|
||||
ax.add_patch(c)
|
||||
print "Stats (%s): %s vs %s; p=%s" % (args.statistic, classt[0], classt[1], p)
|
||||
|
||||
# Final nudge up over last bar
|
||||
ymax = ymax + ((ymax - ymin) * 0.1)
|
||||
|
||||
# Store final limit
|
||||
ymaxf = max(ymaxf, ymax)
|
||||
|
||||
if args.ylim:
|
||||
ylim = args.ylim.split(',')
|
||||
plt.ylim(int(ylim[0]), int(ylim[1]))
|
||||
|
||||
if not args.multiplot:
|
||||
# Adjust plot on multiplot
|
||||
plt.ylim(ymin, ymaxf)
|
||||
ymaxf = 0
|
||||
print "Save as 'bar%s-%s.%s'" % (filesuff, metabolite, args.format)
|
||||
plt.savefig('bar%s-%s.%s' % (filesuff, metabolite, args.format), dpi=args.dpi, transparent=False)
|
||||
|
||||
if args.multiplot:
|
||||
# Adjust plot on multiplot
|
||||
plt.ylim(ymin, ymaxf)
|
||||
print "Save as 'bar%s-%s.%s'" % (filesuff, '-'.join(metabolites), args.format)
|
||||
plt.savefig('bar%s-%s.%s' % (filesuff, '-'.join(metabolites), args.format), dpi=args.dpi, transparent=False)
|
||||
|
||||
if args.display:
|
||||
plt.show()
|
||||
plt.close()
|
|
@ -0,0 +1,304 @@
|
|||
{
|
||||
"program": {
|
||||
"path": "/Users/mxf793/repos/wooey/scripts/longitudinal.py",
|
||||
"epilog": null,
|
||||
"name": "longitudinal",
|
||||
"description": null
|
||||
},
|
||||
"parser": {
|
||||
"prefix_chars": "-",
|
||||
"argument_default": null
|
||||
},
|
||||
"required": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-f",
|
||||
"--file"
|
||||
],
|
||||
"display_name": "file",
|
||||
"help": "csv file containing dumped data from plsmulti",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "FileChooser",
|
||||
"type": "FileType"
|
||||
}
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-m",
|
||||
"--metabolite"
|
||||
],
|
||||
"display_name": "metabolite",
|
||||
"help": "name of metabolite to plot graph for",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-l",
|
||||
"--longitude"
|
||||
],
|
||||
"display_name": "longitudinal",
|
||||
"help": "regex pattern for longitudinal data: grouped area is x axis",
|
||||
"default": "(\\d+)",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-c",
|
||||
"--control"
|
||||
],
|
||||
"display_name": "control",
|
||||
"help": "regex pattern for control data",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-s",
|
||||
"--search"
|
||||
],
|
||||
"display_name": "search",
|
||||
"help": "only show classes matching this regex",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--styles"
|
||||
],
|
||||
"display_name": "styles",
|
||||
"help": "style pattern to use to assign colors/markers/lines to lines; (3),(regex),(groups)",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--xlabel"
|
||||
],
|
||||
"display_name": "xlabel",
|
||||
"help": "x axis label for graph",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--ylabel"
|
||||
],
|
||||
"display_name": "ylabel",
|
||||
"help": "y axis label for graph",
|
||||
"default": "",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--xax"
|
||||
],
|
||||
"display_name": "xax",
|
||||
"help": "log/reg/(lin) x axis",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--yax"
|
||||
],
|
||||
"display_name": "yax",
|
||||
"help": "log/reg/(lin) y axis",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--ylim"
|
||||
],
|
||||
"display_name": "ylim",
|
||||
"help": "min,max for y axis",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-t",
|
||||
"--title"
|
||||
],
|
||||
"display_name": "title",
|
||||
"help": "title for graph",
|
||||
"default": null,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--dpi"
|
||||
],
|
||||
"display_name": "dpi",
|
||||
"help": "dpi for output",
|
||||
"default": 72,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--format"
|
||||
],
|
||||
"display_name": "format",
|
||||
"help": "fileformat for output",
|
||||
"default": "png",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-b",
|
||||
"--batch"
|
||||
],
|
||||
"display_name": "batch_mode",
|
||||
"help": "batch mode, process all metabolite matches with same parameters",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--multiplot"
|
||||
],
|
||||
"display_name": "multiplot",
|
||||
"help": "plot more than one graph on a figure",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--shareyaxis"
|
||||
],
|
||||
"display_name": "shareyaxis",
|
||||
"help": "whether to share y axis scale (and show on multiplots)",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"-d",
|
||||
"--display"
|
||||
],
|
||||
"display_name": "display",
|
||||
"help": "display resulting graphs to screen",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--smooth"
|
||||
],
|
||||
"display_name": "smooth",
|
||||
"help": "smooth graph through cubic interpolation",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--annotate"
|
||||
],
|
||||
"display_name": "annotate",
|
||||
"help": "show command annotation for generation",
|
||||
"default": false,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,385 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from numpy.random import uniform, seed
|
||||
from collections import OrderedDict
|
||||
from matplotlib.mlab import griddata
|
||||
from matplotlib import ticker
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
import numpy as np
|
||||
import re
|
||||
import os
|
||||
import random
|
||||
import csv
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
import utils
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("-m", "--metabolite", dest="metabolite", default='',
|
||||
help="name of metabolite to plot graph for")
|
||||
|
||||
parser.add_argument("-f", "--file", dest="file", type=argparse.FileType('rU'), required=True,
|
||||
help="csv file containing dumped data from plsmulti", metavar="FILE")
|
||||
|
||||
parser.add_argument("-b", "--batch", action="store_true", dest="batch_mode", default=False,
|
||||
help="batch mode, process all metabolite matches with same parameters")
|
||||
|
||||
parser.add_argument("-l", "--longitude", dest="longitudinal", default='(\d+)',
|
||||
help="regex pattern for longitudinal data: grouped area is x axis")
|
||||
|
||||
parser.add_argument("-c", "--control", dest="control", default=False,
|
||||
help="regex pattern for control data")
|
||||
|
||||
parser.add_argument("--multiplot", action="store_true", dest="multiplot", default=False,
|
||||
help="plot more than one graph on a figure")
|
||||
|
||||
parser.add_argument("--shareyaxis", action="store_true", dest="shareyaxis", default=False,
|
||||
help="whether to share y axis scale (and show on multiplots)")
|
||||
|
||||
parser.add_argument("-s", "--search", dest="search", default=None,
|
||||
help="only show classes matching this regex")
|
||||
|
||||
parser.add_argument("--styles", dest="styles", default=None,
|
||||
help="style pattern to use to assign colors/markers/lines to lines; (3),(regex),(groups)")
|
||||
|
||||
parser.add_argument("-d", "--display", action="store_true", dest="display", default=False,
|
||||
help="display resulting graphs to screen")
|
||||
|
||||
parser.add_argument("--xlabel", dest="xlabel", default='',
|
||||
help="x axis label for graph")
|
||||
|
||||
parser.add_argument("--ylabel", dest="ylabel", default='',
|
||||
help="y axis label for graph")
|
||||
|
||||
parser.add_argument("--xax", dest="xax", default=False,
|
||||
help="log/reg/(lin) x axis")
|
||||
|
||||
parser.add_argument("--yax", dest="yax", default=False,
|
||||
help="log/reg/(lin) y axis")
|
||||
|
||||
parser.add_argument("--smooth", dest="smooth", action="store_true", default=False,
|
||||
help="smooth graph through cubic interpolation")
|
||||
|
||||
parser.add_argument("--ylim", dest="ylim", default=None,
|
||||
help="min,max for y axis")
|
||||
|
||||
parser.add_argument("-t", "--title", dest="title", default=None,
|
||||
help="title for graph")
|
||||
|
||||
parser.add_argument("--dpi", dest="dpi", default=72,
|
||||
help="dpi for output")
|
||||
|
||||
parser.add_argument("--format", dest="format", default='png',
|
||||
help="fileformat for output")
|
||||
|
||||
parser.add_argument("--annotate", action="store_true", dest="annotate", default=False,
|
||||
help="show command annotation for generation")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
colors = ['r', 'b', 'g', 'c', 'm', 'y', 'k']
|
||||
markers = ['o', 's', 'v', '^', 'D', '+', 'x']
|
||||
linestyles = ['solid', 'dashed', 'dashdot'] # Do not include dotted, used for controls
|
||||
|
||||
if args.styles is None:
|
||||
# Build a full table we'll just iterate over
|
||||
styles = [(z, y, x) for x in linestyles for y in markers for z in colors]
|
||||
|
||||
# Extract file root from the edge file name
|
||||
filebase = os.path.splitext(args.file.name)[0]
|
||||
[null, sep, string] = filebase.partition('-')
|
||||
filesuff = sep + string
|
||||
nodes = OrderedDict()
|
||||
|
||||
(metabolites, allquants, globalylim) = utils.read_metabolite_datafile(args.file, args)
|
||||
|
||||
# Turn off interactive plotting, speed up
|
||||
plt.ioff()
|
||||
figures = list()
|
||||
multiax = None
|
||||
ymaxf = 0
|
||||
|
||||
for metabolite in metabolites[:]:
|
||||
print "Processing %s" % metabolite
|
||||
quants = allquants[metabolite]
|
||||
|
||||
if args.search:
|
||||
okeys = quants.keys()
|
||||
for label in quants.keys():
|
||||
match = re.search(args.search, label)
|
||||
if not match:
|
||||
del quants[label]
|
||||
print "Filter matching classes '%s' with '%s' gives '%s'" % (', '.join(okeys), args.search, ', '.join(quants.keys()))
|
||||
if len(quants.keys()) == 0:
|
||||
print "Nothing left!; deleting metabolite"
|
||||
metabolites.remove(metabolite)
|
||||
continue
|
||||
# Apply regex to class string for each variable;
|
||||
# the *.(\d+)*. longitudinal bit extracts that
|
||||
# use remainder for class assignment *at that timepoint*
|
||||
# Possible to use the same structure?
|
||||
|
||||
timepoints = set()
|
||||
classes = set()
|
||||
classtrans = defaultdict(list)
|
||||
non_decimal = re.compile(r'[^\d.]+')
|
||||
|
||||
for label in quants.keys():
|
||||
match = re.search(args.longitudinal, label)
|
||||
if match:
|
||||
# Rewrite the class label to not include the match content
|
||||
classlabel = label.replace(match.group(1), '', 1)
|
||||
timepoint = float(non_decimal.sub('', match.group(1)))
|
||||
timepoints.add(timepoint) # Build timepoint list
|
||||
classes.add(classlabel) # Build class list
|
||||
|
||||
classtrans[(classlabel, timepoint)] = label # Store translation for lookup
|
||||
|
||||
if len(timepoints) == 0:
|
||||
print "No matching classes found for longitude regex, try again"
|
||||
exit()
|
||||
|
||||
ind = list()
|
||||
graphs = dict()
|
||||
classes = list(classes)
|
||||
# Remove axis timepoint duplicates and sort
|
||||
timepoints = list(timepoints)
|
||||
timepoints.sort(key=float)
|
||||
print "Axis longitude completed: " + ", ".join(str(x) for x in timepoints)
|
||||
print "Re-classification: " + ", ".join(classes)
|
||||
|
||||
|
||||
# If set run styles setting against the class list and build a lookup table of variants
|
||||
# with assigned colours and styles
|
||||
if args.styles:
|
||||
|
||||
classstyles = dict()
|
||||
classmatch = defaultdict(list)
|
||||
stylesets = list()
|
||||
for n, styleret in enumerate(args.styles.split(',')):
|
||||
stylere = re.compile('(%s)' % styleret)
|
||||
stylesets.append(set())
|
||||
# Build table, then assign
|
||||
for classl in classes:
|
||||
match = stylere.search(classl)
|
||||
if match:
|
||||
stylesets[n].add(match.group(1))
|
||||
classmatch[classl].append(match.group(1))
|
||||
else:
|
||||
classmatch[classl].append(None)
|
||||
|
||||
# Sort the stylesets
|
||||
for n, ss in enumerate(stylesets):
|
||||
stylesets[n] = sorted(ss, reverse=True)
|
||||
|
||||
|
||||
# Now have 3 sets of styles, assign
|
||||
for (classl, classm) in classmatch.items():
|
||||
classstyles[classl] = (
|
||||
colors[list(stylesets[0]).index(classm[0])],
|
||||
markers[list(stylesets[1]).index(classm[1])],
|
||||
linestyles[list(stylesets[2]).index(classm[2])],
|
||||
)
|
||||
|
||||
# Get common substring from classes, for improved naming of saved graph file
|
||||
common_classname = ''
|
||||
# Exclude classes matching control regexp
|
||||
# ...
|
||||
classnc = [classl for classl in classes if not re.search(".*%s.*" % args.control, classl)]
|
||||
|
||||
if len(classnc) > 1 and len(classnc[0]) > 0:
|
||||
for i in range(len(classnc[0])):
|
||||
for j in range(len(classnc[0]) - i + 1):
|
||||
if j > len(common_classname) and all(classnc[0][i:i + j] in x for x in classnc):
|
||||
common_classname = classnc[0][i:i + j]
|
||||
common_classname = common_classname.strip("-")
|
||||
|
||||
# Output is a dict of lists, containing values matching each point on the X axis (or None if not existing)
|
||||
ymin = 0
|
||||
ymax = 0
|
||||
controls = 0
|
||||
|
||||
datasets = defaultdict(list)
|
||||
|
||||
for n, classl in enumerate(classes):
|
||||
graph = {
|
||||
'timepoints': list(),
|
||||
'means': list(),
|
||||
'stddev': list(),
|
||||
'samples': list(),
|
||||
'color': 'k',
|
||||
'control': False,
|
||||
}
|
||||
|
||||
if args.control:
|
||||
match = re.search(".*%s.*" % args.control, classl)
|
||||
if match: # This is a control sample, mark as such
|
||||
graph['control'] = True
|
||||
controls += 1
|
||||
|
||||
for t in timepoints:
|
||||
|
||||
if (classl, t) in classtrans:
|
||||
key = classtrans[(classl, t)] # Get original classname for this timepoint/class combo
|
||||
graph['timepoints'].append(float(t))
|
||||
graph['means'].append(np.mean(quants[key]))
|
||||
graph['stddev'].append(np.std(quants[key]))
|
||||
graph['samples'].append(len(quants[key]))
|
||||
ymax = max(ymax, max(quants[key]))
|
||||
ymin = min(ymin, min(quants[key]))
|
||||
|
||||
if args.styles:
|
||||
graph['style'] = classstyles[classl]
|
||||
else:
|
||||
graph['style'] = styles[n]
|
||||
|
||||
# Store completed graph
|
||||
graphs[classl] = graph
|
||||
|
||||
|
||||
if args.shareyaxis:
|
||||
ylim = globalylim
|
||||
else:
|
||||
ylim = (ymin, ymax)
|
||||
|
||||
|
||||
if args.multiplot:
|
||||
# Keep using same figure, append subplots
|
||||
if not multiax:
|
||||
#adjustprops = dict(left=0.1, bottom=0.1, right=0.97, top=0.93, wspace=0.2, hspace=0.2)
|
||||
fig = plt.figure()
|
||||
multiax = fig.add_subplot(1, len(metabolites), 1)
|
||||
figures.append(multiax)
|
||||
else:
|
||||
if args.shareyaxis:
|
||||
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1, sharey=multiax)
|
||||
plt.setp(fp.get_yticklabels(), visible=False)
|
||||
plt.setp(fp.get_yaxis(), visible=False)
|
||||
else:
|
||||
fp = fig.add_subplot(1, len(metabolites), len(figures) + 1)
|
||||
|
||||
figures.append(fp)
|
||||
else:
|
||||
# New figure
|
||||
figures.append(plt.figure())
|
||||
|
||||
if args.xax:
|
||||
xticks = range(0, len(graph['timepoints']))
|
||||
else:
|
||||
xticks = graph['timepoints']
|
||||
|
||||
for classl in classes:
|
||||
graph = graphs[classl]
|
||||
|
||||
if args.smooth and len(xticks) >= 3:
|
||||
|
||||
l = plt.errorbar(xticks, graph['means'], yerr=graph['stddev'], label=classl, fmt=graph['style'][0], marker=graph['style'][1], linestyle=graph['style'][2])
|
||||
# Calculate spline and plot that over
|
||||
# Duplicate data to additional timepoints
|
||||
# Convert data to linear timecourse first (doh!)
|
||||
ynew = list()
|
||||
for n in range(len(graph['means'][:-1])):
|
||||
t1 = xticks[n]
|
||||
t2 = xticks[n + 1]
|
||||
m1 = graph['means'][n]
|
||||
m2 = graph['means'][n + 1]
|
||||
dm = int(t2 - t1)
|
||||
for tp in range(dm):
|
||||
av = ((m1 * (dm - tp) / dm) + (m2 * tp) / dm)
|
||||
ynew.append(av)
|
||||
|
||||
window_len = 11
|
||||
|
||||
xnew = np.linspace(min(xticks), max(xticks), len(ynew))
|
||||
ynew = np.array(ynew)
|
||||
|
||||
# Iteratively smooth with a small window to minimise distortion
|
||||
# smooth the xaxis alongside to prevent x-distortion
|
||||
for x in range(20):
|
||||
ynew = utils.smooth(ynew, window_len=window_len, window='blackman')
|
||||
xnew = utils.smooth(xnew, window_len=window_len, window='blackman')
|
||||
|
||||
plt.plot(xnew, ynew, color=graph['color'])
|
||||
else:
|
||||
if graph['control']:
|
||||
plt.fill_between(xticks, [a + b for a, b in zip(graph['means'], graph['stddev'])], y2=[a - b for a, b in zip(graph['means'], graph['stddev'])], alpha=0.05, color=graph['style'][0])
|
||||
plt.plot(xticks, graph['means'], color=graph['style'][0], linestyle=graph['style'][2], label=classl, alpha=0.2)
|
||||
else:
|
||||
plt.errorbar(xticks, graph['means'], yerr=graph['stddev'], label=classl, fmt=graph['style'][0], marker=graph['style'][1], linestyle=graph['style'][2])
|
||||
|
||||
|
||||
if args.control:
|
||||
pass
|
||||
#for classl in baselines:
|
||||
# p = mpatches.Rectangle(xy, width, height, facecolor="orange", edgecolor="red")
|
||||
# plt.gca().add_patch(p)
|
||||
# plt.draw()
|
||||
|
||||
ax = plt.gca()
|
||||
handles, labels = ax.get_legend_handles_labels()
|
||||
hl = sorted(zip(labels, handles))
|
||||
labels = [hi[0] for hi in hl]
|
||||
handles = [hi[1] for hi in hl]
|
||||
ax.legend(handles, labels)
|
||||
|
||||
# Optional log x and y axes
|
||||
if args.xax == 'log':
|
||||
ax.set_xscale('log')
|
||||
|
||||
if args.yax == 'log':
|
||||
ax.set_yscale('log')
|
||||
|
||||
|
||||
#plt.xticks(timepoints, timepoints, rotation=45)
|
||||
|
||||
if args.title:
|
||||
plt.title("%s (%s)" % (args.title, metabolite))
|
||||
else:
|
||||
plt.title(metabolite)
|
||||
|
||||
plt.gca().set_xticklabels(graph['timepoints'], minor=False, rotation=45)
|
||||
|
||||
plt.gca().xaxis.set_label_text(args.xlabel)
|
||||
plt.gca().yaxis.set_label_text(args.ylabel)
|
||||
|
||||
# Add some padding either side of graphs
|
||||
#plt.xlim( ind[0]-1, ind[-1]+1)
|
||||
|
||||
if args.annotate:
|
||||
utils.annotate_plot(plt, args)
|
||||
|
||||
fig = plt.gcf()
|
||||
# Horizontal axis through zero
|
||||
#plt.axhline(0, color='k')
|
||||
|
||||
if args.multiplot: # Scale the multiplots a bit more reasonably
|
||||
fig.set_size_inches(5 + len(metabolites) * 3, 6)
|
||||
else:
|
||||
fig.set_size_inches(8, 6)
|
||||
|
||||
# Get ylimits for significance bars
|
||||
|
||||
if args.ylim:
|
||||
ylim = args.ylim.split(',')
|
||||
plt.ylim(int(ylim[0]), int(ylim[1]))
|
||||
|
||||
if not args.multiplot:
|
||||
# Adjust plot on multiplot
|
||||
#plt.ylim( ymin, ymaxf)
|
||||
ymaxf = 0
|
||||
print "Save as 'long%s-%s-%s.%s'" % (filesuff, metabolite, common_classname, args.format)
|
||||
plt.savefig('long%s-%s-%s.%s' % (filesuff, metabolite, common_classname, args.format), dpi=args.dpi, transparent=False)
|
||||
|
||||
if args.multiplot:
|
||||
# Adjust plot on multiplot
|
||||
plt.ylim(ymin, ymaxf)
|
||||
print "Save as 'long%s-%s-%s.%s'" % (filesuff, '-'.join(metabolites), common_classname, args.format)
|
||||
plt.savefig('long%s-%s-%s.%s' % (filesuff, '-'.join(metabolites), common_classname, args.format), dpi=args.dpi, transparent=False)
|
||||
|
||||
if args.display:
|
||||
plt.show()
|
||||
plt.close()
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"program": {
|
||||
"path": "/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py",
|
||||
"epilog": null,
|
||||
"name": "mock_argparse_example",
|
||||
"description": "Process some integers."
|
||||
},
|
||||
"parser": {
|
||||
"prefix_chars": "-",
|
||||
"argument_default": null
|
||||
},
|
||||
"required": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [],
|
||||
"display_name": "integers",
|
||||
"help": "an integer for the accumulator",
|
||||
"default": null,
|
||||
"nargs": "+",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--womp"
|
||||
],
|
||||
"display_name": "womp",
|
||||
"help": "subtract value",
|
||||
"default": 0,
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"commands": [
|
||||
"--sum"
|
||||
],
|
||||
"display_name": "accumulate",
|
||||
"help": "sum the integers (default: find the max)",
|
||||
"default": "max",
|
||||
"nargs": "",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "CheckBox",
|
||||
"type": "NoneType"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Process some integers.')
|
||||
|
||||
parser.add_argument('integers', metavar='N', type=int, nargs='+',
|
||||
help='an integer for the accumulator')
|
||||
|
||||
parser.add_argument('--sum', dest='accumulate', action='store_const',
|
||||
const=sum, default=max,
|
||||
help='sum the integers (default: find the max)')
|
||||
|
||||
parser.add_argument('--womp', dest='womp', action='store',
|
||||
type=int, default=0,
|
||||
help='subtract value')
|
||||
|
||||
args = parser.parse_args()
|
||||
print(args.accumulate(args.integers) - args.womp)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"program": {
|
||||
"path": "/Users/mxf793/repos/wooey/scripts/plot_some_numbers.py",
|
||||
"epilog": null,
|
||||
"name": "plot_some_numbers",
|
||||
"description": "Plot some numbers."
|
||||
},
|
||||
"parser": {
|
||||
"prefix_chars": "-",
|
||||
"argument_default": null
|
||||
},
|
||||
"required": [
|
||||
{
|
||||
"data": {
|
||||
"commands": [],
|
||||
"display_name": "integers",
|
||||
"help": "a space separated list of numbers to plot",
|
||||
"default": null,
|
||||
"nargs": "+",
|
||||
"choices": []
|
||||
},
|
||||
"widget": "TextField",
|
||||
"type": "int"
|
||||
}
|
||||
],
|
||||
"optional": []
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
# Plot some numbers
|
||||
|
||||
This script allows you to plot some numbers.
|
||||
|
||||
This is some markdown-based documentation for the script. Source files
|
||||
can be specified in the JSON markup or will be automatically loaded
|
||||
from the script source or config folders.
|
||||
|
||||
|
||||
A First Level Header
|
||||
====================
|
||||
|
||||
A Second Level Header
|
||||
---------------------
|
||||
|
||||
Now is the time for all good men to come to
|
||||
the aid of their country. This is just a
|
||||
regular paragraph.
|
||||
|
||||
The quick brown fox jumped over the lazy
|
||||
dog's back.
|
||||
|
||||
### Header 3
|
||||
|
||||
> This is a blockquote.
|
||||
>
|
||||
> This is the second paragraph in the blockquote.
|
||||
>
|
||||
> ## This is an H2 in a blockquote
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Plot some numbers.')
|
||||
parser.add_argument('integers', metavar='N', type=int, nargs='+',
|
||||
help='a space separated list of numbers to plot')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(1, 1, 1)
|
||||
|
||||
l1 = args.integers
|
||||
ax.plot(range(0, len(l1)), l1)
|
||||
print("Plotting Figure 1\n%s" % l1)
|
||||
fig.savefig('Figure 1.png')
|
||||
|
||||
fig = plt.figure()
|
||||
ax = fig.add_subplot(1, 1, 1)
|
||||
l2 = [x - args.integers[n + 1] for n, x in enumerate(args.integers[:-1])]
|
||||
ax.plot(range(0, len(l2)), l2)
|
||||
|
||||
print("Plotting Figure 2\n%s" % l2)
|
||||
fig.savefig('Figure 2.png')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,109 @@
|
|||
import re,os
|
||||
import csv
|
||||
from collections import defaultdict
|
||||
import math
|
||||
|
||||
def sigstars(p):
|
||||
# Return appropriate number of stars or ns for significance
|
||||
|
||||
if (p<=0.0001):
|
||||
s = '****'
|
||||
elif (p<=0.001):
|
||||
s = '***'
|
||||
elif (p<=0.01):
|
||||
s = '**'
|
||||
elif (p<=0.05):
|
||||
s = '*'
|
||||
else:
|
||||
s = 'ns'
|
||||
return s
|
||||
|
||||
|
||||
def read_metabolite_datafile( fp, options ):
|
||||
|
||||
# Read in data for the graphing metabolite, with associated value (generate mean)
|
||||
reader = csv.reader( fp, delimiter=',', dialect='excel')
|
||||
# Find matching metabolite column
|
||||
hrow = reader.next()
|
||||
try:
|
||||
metabolite_column = hrow.index( options.metabolite )
|
||||
print "'%s' found" % (options.metabolite)
|
||||
metabolites = [ options.metabolite ]
|
||||
except:
|
||||
all_metabolites = hrow[2:]
|
||||
metabolites = filter(lambda x:re.match('(.*)' + options.metabolite + '(.*)', x), all_metabolites)
|
||||
if len(metabolites) ==0:
|
||||
print "Metabolite not found, try again. Pick from one of:"
|
||||
print ', '.join( sorted(all_metabolites) )
|
||||
exit()
|
||||
elif len(metabolites) > 1:
|
||||
print "Searched '%s' and found multiple matches:" % (options.metabolite)
|
||||
print ', '.join( sorted(metabolites) )
|
||||
if not options.batch_mode:
|
||||
print "To process all the above together use batch mode -b"
|
||||
exit()
|
||||
elif len(metabolites) ==1:
|
||||
print "Searched '%s' and found match in '%s'" % (options.metabolite, metabolites[0])
|
||||
|
||||
|
||||
# Build quants table for metabolite classes
|
||||
allquants = dict()
|
||||
for metabolite in metabolites:
|
||||
allquants[ metabolite ] = defaultdict(list)
|
||||
|
||||
ymin = 0
|
||||
ymax = 0
|
||||
|
||||
for row in reader:
|
||||
if row[1] != '.': # Skip excluded classes # row[1] = Class
|
||||
for metabolite in metabolites:
|
||||
metabolite_column = hrow.index( metabolite )
|
||||
if row[ metabolite_column ]:
|
||||
allquants[metabolite][ row[1] ].append( float(row[ metabolite_column ]) )
|
||||
ymin = min( ymin, float(row[ metabolite_column ]) )
|
||||
ymax = max( ymax, float(row[ metabolite_column ]) )
|
||||
else:
|
||||
allquants[metabolite][ row[1] ].append( 0 )
|
||||
|
||||
return ( metabolites, allquants, (ymin,ymax) )
|
||||
|
||||
def annotate_plot(plt, options):
|
||||
annod = vars(options)
|
||||
annol = ', \n'.join(["%s=%s" % (x, annod[x]) for x in annod.keys()])
|
||||
bbox = dict( facecolor='#eeeeff', alpha=1, edgecolor='#000000',boxstyle="Square,pad=1")
|
||||
plt.text(1.1, 1.1, annol,
|
||||
backgroundcolor='#eeeeff',
|
||||
fontsize='x-small',
|
||||
ha='right',
|
||||
va='top',
|
||||
bbox=bbox,
|
||||
transform = plt.gca().transAxes)
|
||||
|
||||
import numpy
|
||||
|
||||
def smooth(x,window_len=11,window='hanning'):
|
||||
|
||||
if x.ndim != 1:
|
||||
raise ValueError, "smooth only accepts 1 dimension arrays."
|
||||
|
||||
if x.size < window_len:
|
||||
raise ValueError, "Input vector needs to be bigger than window size."
|
||||
|
||||
|
||||
if window_len<3:
|
||||
return x
|
||||
|
||||
|
||||
if not window in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']:
|
||||
raise ValueError, "Window is on of 'flat', 'hanning', 'hamming', 'bartlett', 'blackman'"
|
||||
|
||||
|
||||
s=numpy.r_[x[window_len-1:0:-1],x,x[-1:-window_len:-1]]
|
||||
#print(len(s))
|
||||
if window == 'flat': #moving average
|
||||
w=numpy.ones(window_len,'d')
|
||||
else:
|
||||
w=eval('numpy.'+window+'(window_len)')
|
||||
|
||||
y=numpy.convolve(w/w.sum(),s,mode='valid')
|
||||
return y
|
|
@ -0,0 +1,47 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Defines fixtures available to all tests."""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
from webtest import TestApp
|
||||
|
||||
from wooey.settings import TestConfig
|
||||
from wooey.app import create_app
|
||||
from wooey.database import db as _db
|
||||
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope='function')
|
||||
def app():
|
||||
_app = create_app(TestConfig)
|
||||
ctx = _app.test_request_context()
|
||||
ctx.push()
|
||||
|
||||
yield _app
|
||||
|
||||
ctx.pop()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def testapp(app):
|
||||
"""A Webtest app."""
|
||||
return TestApp(app)
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope='function')
|
||||
def db(app):
|
||||
_db.app = app
|
||||
with app.app_context():
|
||||
_db.create_all()
|
||||
|
||||
yield _db
|
||||
|
||||
_db.drop_all()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(db):
|
||||
user = UserFactory(password='myprecious')
|
||||
db.session.commit()
|
||||
return user
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from factory import Sequence, PostGenerationMethodCall
|
||||
from factory.alchemy import SQLAlchemyModelFactory
|
||||
|
||||
from wooey.user.models import User
|
||||
from wooey.database import db
|
||||
|
||||
|
||||
class BaseFactory(SQLAlchemyModelFactory):
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
sqlalchemy_session = db.session
|
||||
|
||||
|
||||
class UserFactory(BaseFactory):
|
||||
username = Sequence(lambda n: "user{0}".format(n))
|
||||
email = Sequence(lambda n: "user{0}@example.com".format(n))
|
||||
password = PostGenerationMethodCall('set_password', 'example')
|
||||
active = True
|
||||
|
||||
class Meta:
|
||||
model = User
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from wooey.app import create_app
|
||||
from wooey.settings import ProdConfig, DevConfig
|
||||
|
||||
|
||||
def test_production_config():
|
||||
app = create_app(ProdConfig)
|
||||
assert app.config['ENV'] == 'prod'
|
||||
assert app.config['DEBUG'] is False
|
||||
assert app.config['DEBUG_TB_ENABLED'] is False
|
||||
assert app.config['ASSETS_DEBUG'] is False
|
||||
|
||||
|
||||
def test_dev_config():
|
||||
app = create_app(DevConfig)
|
||||
assert app.config['ENV'] == 'dev'
|
||||
assert app.config['DEBUG'] is True
|
||||
assert app.config['ASSETS_DEBUG'] is True
|
|
@ -0,0 +1,62 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from wooey.public.forms import LoginForm
|
||||
from wooey.user.forms import RegisterForm
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
class TestRegisterForm:
|
||||
|
||||
def test_validate_user_already_registered(self, user):
|
||||
# Enters username that is already registered
|
||||
form = RegisterForm(username=user.username, email='foo@bar.com',
|
||||
password='example', confirm='example')
|
||||
|
||||
assert form.validate() is False
|
||||
assert 'Username already registered' in form.username.errors
|
||||
|
||||
def test_validate_email_already_registered(self, user):
|
||||
# enters email that is already registered
|
||||
form = RegisterForm(username='unique', email=user.email,
|
||||
password='example', confirm='example')
|
||||
|
||||
assert form.validate() is False
|
||||
assert 'Email already registered' in form.email.errors
|
||||
|
||||
def test_validate_success(self, db):
|
||||
form = RegisterForm(username='newusername', email='new@test.test',
|
||||
password='example', confirm='example')
|
||||
assert form.validate() is True
|
||||
|
||||
|
||||
class TestLoginForm:
|
||||
|
||||
def test_validate_success(self, user):
|
||||
user.set_password('example')
|
||||
user.save()
|
||||
form = LoginForm(username=user.username, password='example')
|
||||
assert form.validate() is True
|
||||
assert form.user == user
|
||||
|
||||
def test_validate_unknown_username(self, db):
|
||||
form = LoginForm(username='unknown', password='example')
|
||||
assert form.validate() is False
|
||||
assert 'Unknown username' in form.username.errors
|
||||
assert form.user is None
|
||||
|
||||
def test_validate_invalid_password(self, user):
|
||||
user.set_password('example')
|
||||
user.save()
|
||||
form = LoginForm(username=user.username, password='wrongpassword')
|
||||
assert form.validate() is False
|
||||
assert 'Invalid password' in form.password.errors
|
||||
|
||||
def test_validate_inactive_user(self, user):
|
||||
user.active = False
|
||||
user.set_password('example')
|
||||
user.save()
|
||||
# Correct username and password, but user is not activated
|
||||
form = LoginForm(username=user.username, password='example')
|
||||
assert form.validate() is False
|
||||
assert 'User not activated' in form.username.errors
|
|
@ -0,0 +1,111 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Functional tests using WebTest.
|
||||
|
||||
See: http://webtest.readthedocs.org/
|
||||
"""
|
||||
import pytest
|
||||
from flask import url_for
|
||||
|
||||
from wooey.user.models import User
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
class TestLoggingIn:
|
||||
|
||||
def test_can_log_in_returns_200(self, user, testapp):
|
||||
# Goes to homepage
|
||||
res = testapp.get("/")
|
||||
# Fills out login form in navbar
|
||||
form = res.forms['loginForm']
|
||||
form['username'] = user.username
|
||||
form['password'] = 'myprecious'
|
||||
# Submits
|
||||
res = form.submit().follow()
|
||||
assert res.status_code == 200
|
||||
|
||||
def test_sees_alert_on_log_out(self, user, testapp):
|
||||
res = testapp.get("/")
|
||||
# Fills out login form in navbar
|
||||
form = res.forms['loginForm']
|
||||
form['username'] = user.username
|
||||
form['password'] = 'myprecious'
|
||||
# Submits
|
||||
res = form.submit().follow()
|
||||
res = testapp.get(url_for('public.logout')).follow()
|
||||
# sees alert
|
||||
assert 'You are logged out.' in res
|
||||
|
||||
def test_sees_error_message_if_password_is_incorrect(self, user, testapp):
|
||||
# Goes to homepage
|
||||
res = testapp.get("/")
|
||||
# Fills out login form, password incorrect
|
||||
form = res.forms['loginForm']
|
||||
form['username'] = user.username
|
||||
form['password'] = 'wrong'
|
||||
# Submits
|
||||
res = form.submit()
|
||||
# sees error
|
||||
assert "Invalid password" in res
|
||||
|
||||
def test_sees_error_message_if_username_doesnt_exist(self, user, testapp):
|
||||
# Goes to homepage
|
||||
res = testapp.get("/")
|
||||
# Fills out login form, password incorrect
|
||||
form = res.forms['loginForm']
|
||||
form['username'] = 'unknown'
|
||||
form['password'] = 'myprecious'
|
||||
# Submits
|
||||
res = form.submit()
|
||||
# sees error
|
||||
assert "Unknown user" in res
|
||||
|
||||
|
||||
class TestRegistering:
|
||||
|
||||
def test_can_register(self, user, testapp):
|
||||
old_count = len(User.query.all())
|
||||
# Goes to homepage
|
||||
res = testapp.get("/")
|
||||
# Clicks Create Account button
|
||||
res = res.click("Create account")
|
||||
# Fills out the form
|
||||
form = res.forms["registerForm"]
|
||||
form['username'] = 'foobar'
|
||||
form['email'] = 'foo@bar.com'
|
||||
form['password'] = 'secret'
|
||||
form['confirm'] = 'secret'
|
||||
# Submits
|
||||
res = form.submit().follow()
|
||||
assert res.status_code == 200
|
||||
# A new user was created
|
||||
assert len(User.query.all()) == old_count + 1
|
||||
|
||||
def test_sees_error_message_if_passwords_dont_match(self, user, testapp):
|
||||
# Goes to registration page
|
||||
res = testapp.get(url_for("public.register"))
|
||||
# Fills out form, but passwords don't match
|
||||
form = res.forms["registerForm"]
|
||||
form['username'] = 'foobar'
|
||||
form['email'] = 'foo@bar.com'
|
||||
form['password'] = 'secret'
|
||||
form['confirm'] = 'secrets'
|
||||
# Submits
|
||||
res = form.submit()
|
||||
# sees error message
|
||||
assert "Passwords must match" in res
|
||||
|
||||
def test_sees_error_message_if_user_already_registered(self, user, testapp):
|
||||
user = UserFactory(active=True) # A registered user
|
||||
user.save()
|
||||
# Goes to registration page
|
||||
res = testapp.get(url_for("public.register"))
|
||||
# Fills out form, but username is already registered
|
||||
form = res.forms["registerForm"]
|
||||
form['username'] = user.username
|
||||
form['email'] = 'foo@bar.com'
|
||||
form['password'] = 'secret'
|
||||
form['confirm'] = 'secret'
|
||||
# Submits
|
||||
res = form.submit()
|
||||
# sees error
|
||||
assert "Username already registered" in res
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Model unit tests."""
|
||||
import datetime as dt
|
||||
|
||||
import pytest
|
||||
|
||||
from wooey.user.models import User, Role
|
||||
from .factories import UserFactory
|
||||
|
||||
|
||||
@pytest.mark.usefixtures('db')
|
||||
class TestUser:
|
||||
|
||||
def test_get_by_id(self):
|
||||
user = User('foo', 'foo@bar.com')
|
||||
user.save()
|
||||
|
||||
retrieved = User.get_by_id(user.id)
|
||||
assert retrieved == user
|
||||
|
||||
def test_created_at_defaults_to_datetime(self):
|
||||
user = User(username='foo', email='foo@bar.com')
|
||||
user.save()
|
||||
assert bool(user.created_at)
|
||||
assert isinstance(user.created_at, dt.datetime)
|
||||
|
||||
def test_password_is_nullable(self):
|
||||
user = User(username='foo', email='foo@bar.com')
|
||||
user.save()
|
||||
assert user.password is None
|
||||
|
||||
def test_factory(self, db):
|
||||
user = UserFactory(password="myprecious")
|
||||
db.session.commit()
|
||||
assert bool(user.username)
|
||||
assert bool(user.email)
|
||||
assert bool(user.created_at)
|
||||
assert user.is_admin is False
|
||||
assert user.active is True
|
||||
assert user.check_password('myprecious')
|
||||
|
||||
def test_check_password(self):
|
||||
user = User.create(username="foo", email="foo@bar.com",
|
||||
password="foobarbaz123")
|
||||
assert user.check_password('foobarbaz123') is True
|
||||
assert user.check_password("barfoobaz") is False
|
||||
|
||||
def test_full_name(self):
|
||||
user = UserFactory(first_name="Foo", last_name="Bar")
|
||||
assert user.full_name == "Foo Bar"
|
||||
|
||||
def test_roles(self):
|
||||
role = Role(name='admin')
|
||||
role.save()
|
||||
u = UserFactory()
|
||||
u.roles.append(role)
|
||||
u.save()
|
||||
assert role in u.roles
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''The public module, including the homepage and user auth.'''
|
||||
|
||||
from . import views
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from flask import Blueprint, render_template
|
||||
from flask.ext.login import login_required
|
||||
from flask.ext import login
|
||||
|
||||
from flask.ext.admin import Admin, BaseView, expose
|
||||
from flask.ext.admin.contrib.sqla import ModelView
|
||||
|
||||
|
||||
class AdminView(BaseView):
|
||||
|
||||
def is_accessible(self):
|
||||
return login.current_user.is_authenticated()
|
||||
|
||||
@expose('/')
|
||||
def index(self):
|
||||
return self.render('index.html')
|
||||
|
||||
from ..extensions import flask_admin, db
|
||||
|
||||
from ..user.models import User
|
||||
flask_admin.add_view(ModelView(User, db.session, endpoint='user-admin'))
|
||||
|
||||
from ..public.models import Script, Job
|
||||
flask_admin.add_view(ModelView(Script, db.session, endpoint='script-admin'))
|
||||
flask_admin.add_view(ModelView(Job, db.session, endpoint='job-admin'))
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''The app module, containing the app factory function.'''
|
||||
from flask import Flask, render_template
|
||||
|
||||
from wooey.settings import ProdConfig
|
||||
from wooey.assets import assets
|
||||
from wooey.extensions import (
|
||||
bcrypt,
|
||||
cache,
|
||||
db,
|
||||
login_manager,
|
||||
migrate,
|
||||
flask_admin,
|
||||
# debug_toolbar,
|
||||
)
|
||||
from wooey import public, user, admin
|
||||
|
||||
|
||||
def create_app(config_object=ProdConfig):
|
||||
'''An application factory, as explained here:
|
||||
http://flask.pocoo.org/docs/patterns/appfactories/
|
||||
|
||||
:param config_object: The configuration object to use.
|
||||
'''
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config_object)
|
||||
register_extensions(app)
|
||||
register_blueprints(app)
|
||||
register_errorhandlers(app)
|
||||
return app
|
||||
|
||||
|
||||
def register_extensions(app):
|
||||
assets.init_app(app)
|
||||
bcrypt.init_app(app)
|
||||
cache.init_app(app)
|
||||
db.init_app(app)
|
||||
login_manager.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
flask_admin.init_app(app)
|
||||
return None
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
app.register_blueprint(public.views.blueprint)
|
||||
app.register_blueprint(user.views.blueprint)
|
||||
return None
|
||||
|
||||
|
||||
def register_errorhandlers(app):
|
||||
def render_error(error):
|
||||
# If a HTTPException, pull the `code` attribute; default to 500
|
||||
error_code = getattr(error, 'code', 500)
|
||||
return render_template("{0}.html".format(error_code)), error_code
|
||||
for errcode in [401, 404, 500]:
|
||||
app.errorhandler(errcode)(render_error)
|
||||
return None
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from flask.ext.assets import Bundle, Environment
|
||||
|
||||
css = Bundle(
|
||||
"css/style.css",
|
||||
filters="cssmin",
|
||||
output="public/css/common.css"
|
||||
)
|
||||
|
||||
js = Bundle(
|
||||
"js/plugins.js",
|
||||
filters='jsmin',
|
||||
output="public/js/common.js"
|
||||
)
|
||||
|
||||
assets = Environment()
|
||||
|
||||
assets.register("js_all", js)
|
||||
assets.register("css_all", css)
|
|
@ -0,0 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Python 2/3 compatibility module."""
|
||||
|
||||
import sys
|
||||
|
||||
PY2 = int(sys.version[0]) == 2
|
||||
|
||||
if PY2:
|
||||
text_type = unicode
|
||||
binary_type = str
|
||||
string_types = (str, unicode)
|
||||
unicode = unicode
|
||||
basestring = basestring
|
||||
else:
|
||||
text_type = str
|
||||
binary_type = bytes
|
||||
string_types = (str, )
|
||||
unicode = str
|
||||
basestring = (str, bytes)
|
|
@ -0,0 +1,80 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Database module, including the SQLAlchemy database object and DB-related
|
||||
utilities.
|
||||
"""
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .extensions import db
|
||||
from .compat import basestring
|
||||
|
||||
# Alias common SQLAlchemy names
|
||||
Column = db.Column
|
||||
relationship = relationship
|
||||
|
||||
|
||||
class CRUDMixin(object):
|
||||
"""Mixin that adds convenience methods for CRUD (create, read, update, delete)
|
||||
operations.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, **kwargs):
|
||||
"""Create a new record and save it the database."""
|
||||
instance = cls(**kwargs)
|
||||
return instance.save()
|
||||
|
||||
def update(self, commit=True, **kwargs):
|
||||
"""Update specific fields of a record."""
|
||||
for attr, value in kwargs.iteritems():
|
||||
setattr(self, attr, value)
|
||||
return commit and self.save() or self
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Save the record."""
|
||||
db.session.add(self)
|
||||
if commit:
|
||||
db.session.commit()
|
||||
return self
|
||||
|
||||
def delete(self, commit=True):
|
||||
"""Remove the record from the database."""
|
||||
db.session.delete(self)
|
||||
return commit and db.session.commit()
|
||||
|
||||
|
||||
class Model(CRUDMixin, db.Model):
|
||||
"""Base model class that includes CRUD convenience methods."""
|
||||
__abstract__ = True
|
||||
|
||||
|
||||
# From Mike Bayer's "Building the app" talk
|
||||
# https://speakerdeck.com/zzzeek/building-the-app
|
||||
class SurrogatePK(object):
|
||||
"""A mixin that adds a surrogate integer 'primary key' column named
|
||||
``id`` to any declarative-mapped class.
|
||||
"""
|
||||
__table_args__ = {'extend_existing': True}
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
@classmethod
|
||||
def get_by_id(cls, id):
|
||||
if any(
|
||||
(isinstance(id, basestring) and id.isdigit(),
|
||||
isinstance(id, (int, float))),
|
||||
):
|
||||
return cls.query.get(int(id))
|
||||
return None
|
||||
|
||||
|
||||
def ReferenceCol(tablename, nullable=False, pk_name='id', **kwargs):
|
||||
"""Column that adds primary key foreign key reference.
|
||||
|
||||
Usage: ::
|
||||
|
||||
category_id = ReferenceCol('category')
|
||||
category = relationship('Category', backref='categories')
|
||||
"""
|
||||
return db.Column(
|
||||
db.ForeignKey("{0}.{1}".format(tablename, pk_name)),
|
||||
nullable=nullable, **kwargs)
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Extensions module. Each extension is initialized in the app factory located
|
||||
in app.py
|
||||
"""
|
||||
|
||||
from flask.ext.bcrypt import Bcrypt
|
||||
bcrypt = Bcrypt()
|
||||
|
||||
from flask.ext.login import LoginManager
|
||||
login_manager = LoginManager()
|
||||
|
||||
from flask.ext.sqlalchemy import SQLAlchemy
|
||||
db = SQLAlchemy()
|
||||
|
||||
from flask.ext.migrate import Migrate
|
||||
migrate = Migrate()
|
||||
|
||||
from flask.ext.cache import Cache
|
||||
cache = Cache()
|
||||
|
||||
from flask.ext.admin import Admin
|
||||
flask_admin = Admin()
|
||||
|
||||
# from flask.ext.debugtoolbar import DebugToolbarExtension
|
||||
# debug_toolbar = DebugToolbarExtension()
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''The public module, including the homepage and user auth.'''
|
||||
|
||||
from . import views
|
|
@ -0,0 +1,33 @@
|
|||
from flask_wtf import Form
|
||||
from wtforms import TextField, PasswordField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from wooey.user.models import User
|
||||
|
||||
|
||||
class LoginForm(Form):
|
||||
username = TextField('Username', validators=[DataRequired()])
|
||||
password = PasswordField('Password', validators=[DataRequired()])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
|
||||
def validate(self):
|
||||
initial_validation = super(LoginForm, self).validate()
|
||||
if not initial_validation:
|
||||
return False
|
||||
|
||||
self.user = User.query.filter_by(username=self.username.data).first()
|
||||
if not self.user:
|
||||
self.username.errors.append('Unknown username')
|
||||
return False
|
||||
|
||||
if not self.user.check_password(self.password.data):
|
||||
self.password.errors.append('Invalid password')
|
||||
return False
|
||||
|
||||
if not self.user.active:
|
||||
self.username.errors.append('User not activated')
|
||||
return False
|
||||
return True
|
|
@ -0,0 +1,91 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime as dt
|
||||
import json
|
||||
|
||||
from wooey.database import (
|
||||
Column,
|
||||
ReferenceCol,
|
||||
relationship,
|
||||
db,
|
||||
Model,
|
||||
SurrogatePK,
|
||||
)
|
||||
|
||||
STATUS_WAITING = "W"
|
||||
STATUS_RUNNING = "R"
|
||||
STATUS_COMPLETE = "C"
|
||||
STATUS_ERROR = "X"
|
||||
|
||||
|
||||
class Script(SurrogatePK, Model):
|
||||
|
||||
__tablename__ = 'scripts'
|
||||
name = Column(db.String(80), nullable=False) # The basic name of the script
|
||||
display_name = Column(db.String(80), nullable=True) # A nice, user friendly name
|
||||
|
||||
exec_path = Column(db.String(255), unique=True, nullable=False)
|
||||
config_path = Column(db.String(255), unique=True, nullable=False)
|
||||
doc_path = Column(db.String(255), unique=True, nullable=True)
|
||||
|
||||
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
|
||||
updated_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow, onupdate=db.func.now())
|
||||
|
||||
description = Column(db.String(255), nullable=True)
|
||||
|
||||
is_active = Column(db.Boolean(), default=True)
|
||||
|
||||
def load_config(self):
|
||||
'''
|
||||
Load JSON config from file
|
||||
:return: dict of config
|
||||
'''
|
||||
|
||||
with open(self.config_path, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
def load_docs(self):
|
||||
'''
|
||||
Load JSON config from file
|
||||
:return: dict of config
|
||||
'''
|
||||
if self.doc_path:
|
||||
with open(self.doc_path, 'r') as f:
|
||||
return f.read()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class Job(SurrogatePK, Model):
|
||||
|
||||
__tablename__ = 'jobs'
|
||||
|
||||
script_id = ReferenceCol('scripts')
|
||||
script = relationship('Script', backref='jobs')
|
||||
|
||||
user_id = ReferenceCol('users')
|
||||
user = relationship('User', backref='jobs')
|
||||
|
||||
path = Column(db.String(255), unique=True, nullable=False)
|
||||
|
||||
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
|
||||
updated_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow, onupdate=db.func.now())
|
||||
|
||||
status = Column(db.Enum(STATUS_WAITING, STATUS_RUNNING, STATUS_COMPLETE, STATUS_ERROR), nullable=False, default=STATUS_WAITING)
|
||||
|
||||
config = Column(db.String(), nullable=True)
|
||||
|
||||
@property
|
||||
def is_waiting(self):
|
||||
return self.status == STATUS_WAITING
|
||||
|
||||
@property
|
||||
def is_running(self):
|
||||
return self.status == STATUS_RUNNING
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
return self.status == STATUS_COMPLETE
|
||||
|
||||
@property
|
||||
def is_error(self):
|
||||
return self.status == STATUS_ERROR
|
|
@ -0,0 +1,201 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''Public section, including homepage and signup.'''
|
||||
from flask import (Blueprint, request, render_template, flash, url_for,
|
||||
redirect, session)
|
||||
from flask.ext.login import login_user, login_required, logout_user, current_user
|
||||
|
||||
from wooey.extensions import login_manager
|
||||
from wooey.user.models import User
|
||||
from wooey.public.forms import LoginForm
|
||||
from wooey.user.forms import RegisterForm
|
||||
from wooey.utils import flash_errors
|
||||
from wooey.database import db
|
||||
|
||||
from werkzeug import secure_filename
|
||||
|
||||
import tempfile
|
||||
|
||||
import json
|
||||
import os
|
||||
import base64
|
||||
import mistune
|
||||
|
||||
from .models import Script, Job
|
||||
|
||||
blueprint = Blueprint('public', __name__, static_folder="../static")
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(id):
|
||||
return User.get_by_id(int(id))
|
||||
|
||||
|
||||
@blueprint.route("/", methods=["GET", "POST"])
|
||||
def home():
|
||||
form = LoginForm(request.form)
|
||||
# Handle logging in
|
||||
if request.method == 'POST':
|
||||
if form.validate_on_submit():
|
||||
login_user(form.user)
|
||||
flash("You are logged in.", 'success')
|
||||
redirect_url = request.args.get("next") or url_for("user.members")
|
||||
return redirect(redirect_url)
|
||||
else:
|
||||
flash_errors(form)
|
||||
|
||||
scripts = Script.query.all()
|
||||
return render_template("public/home.html", form=form, scripts=scripts)
|
||||
|
||||
|
||||
@blueprint.route('/logout/')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('You are logged out.', 'info')
|
||||
return redirect(url_for('public.home'))
|
||||
|
||||
|
||||
@blueprint.route("/register/", methods=['GET', 'POST'])
|
||||
def register():
|
||||
form = RegisterForm(request.form, csrf_enabled=False)
|
||||
if form.validate_on_submit():
|
||||
new_user = User.create(username=form.username.data,
|
||||
email=form.email.data,
|
||||
password=form.password.data,
|
||||
active=True)
|
||||
flash("Thank you for registering. You can now log in.", 'success')
|
||||
return redirect(url_for('public.home'))
|
||||
else:
|
||||
flash_errors(form)
|
||||
return render_template('public/register.html', form=form)
|
||||
|
||||
|
||||
@blueprint.route("/about/")
|
||||
def about():
|
||||
form = LoginForm(request.form)
|
||||
return render_template("public/about.html", form=form)
|
||||
|
||||
|
||||
@blueprint.route("/scripts/")
|
||||
def scripts():
|
||||
scripts = Script.query.all()
|
||||
return render_template("public/scripts.html", scripts=scripts)
|
||||
|
||||
|
||||
@blueprint.route("/jobs/create/<int:script_id>/", methods=["GET", "POST"])
|
||||
def create_job(script_id):
|
||||
'''
|
||||
Create a new job from the given script.
|
||||
|
||||
GET request results in rendering the form
|
||||
POST accepts the form, creates the job and redirects to the job view
|
||||
|
||||
This function handles the conversion of POSTed data into a command line arg sequence for
|
||||
subsequent running as a job object.
|
||||
|
||||
:param script_id:
|
||||
:return:
|
||||
'''
|
||||
|
||||
# Get the script object from the database
|
||||
script = Script.query.get(script_id)
|
||||
|
||||
if request.method == 'GET':
|
||||
# Find the documentation and parse it using markdown
|
||||
documentation = script.load_docs()
|
||||
if documentation:
|
||||
documentation = mistune.markdown(documentation)
|
||||
|
||||
# Render the script view
|
||||
return render_template("public/create-job.html", script=script, metadata=script.load_config(), documentation=documentation)
|
||||
|
||||
elif request.method == 'POST':
|
||||
# Handle the form submission to generate the arguments for the script
|
||||
metadata = script.load_config()
|
||||
|
||||
args = []
|
||||
tempdir = tempfile.mkdtemp()
|
||||
|
||||
for l in ['required', 'optional']:
|
||||
for a in metadata[l]:
|
||||
# Positional arguments
|
||||
name = a['data']['display_name']
|
||||
|
||||
if (name in request.form and request.form[name]) or \
|
||||
(name in request.files and request.files[name]):
|
||||
|
||||
# Add the command switch if defined
|
||||
if a['data']['commands']:
|
||||
args.append(a['data']['commands'][0])
|
||||
|
||||
if name in request.form:
|
||||
|
||||
# Required arguments are positional; so plot it into place
|
||||
# FIXME: Probably a better check to do here, might require additional data from the parser
|
||||
# FIXME: Also need to handle FILE objects, etc.
|
||||
if a['widget'] not in ["CheckBox"]:
|
||||
if a['data']['nargs'] == '+' or a['data']['nargs'] == '*':
|
||||
args.extend(request.form[name].split(" "))
|
||||
else:
|
||||
args.append(request.form[name])
|
||||
|
||||
elif name in request.files:
|
||||
# Process file upload. We need to copy to a temporary file and
|
||||
# replace the value in the dictionary (or keep in the same folder, for easier cleanup?)
|
||||
# Will then need a way to identify uploaded vs. output files
|
||||
file = request.files[name]
|
||||
fname = os.path.join(tempdir, secure_filename(file.filename))
|
||||
file.save(fname)
|
||||
args.append(fname)
|
||||
|
||||
# Create the job
|
||||
job = Job(script=script, user=current_user, path=tempdir, config=json.dumps({'args': args}))
|
||||
db.session.commit()
|
||||
|
||||
return redirect(url_for('public.job', job_id=job.id))
|
||||
|
||||
|
||||
@blueprint.route("/jobs/<int:job_id>/")
|
||||
def job(job_id):
|
||||
'''
|
||||
View a single job (any status) with AJAX callback to update elements, e.g.
|
||||
- STDOUT/STDERR
|
||||
- File outputs (figures, etc.), intelligent render handling
|
||||
- Download link for all files
|
||||
|
||||
|
||||
:param job_id:
|
||||
:return:
|
||||
'''
|
||||
|
||||
# Get the job object from the database
|
||||
job = Job.query.get(job_id)
|
||||
script = job.script
|
||||
|
||||
try:
|
||||
with open(os.path.join(job.path, 'STDOUT'), 'r') as f:
|
||||
console = f.read()
|
||||
|
||||
except IOError:
|
||||
console = ""
|
||||
|
||||
excluded = ['STDOUT']
|
||||
|
||||
# Filter files for files and not excluded (STDOUT)
|
||||
files = [f for f in os.listdir(job.path) if os.path.isfile(os.path.join(job.path, f)) and f not in excluded]
|
||||
|
||||
display = {}
|
||||
for filename in files:
|
||||
|
||||
fullpath = os.path.join(job.path, filename)
|
||||
name, ext = os.path.splitext(filename)
|
||||
src = None
|
||||
|
||||
if ext in ['.png', '.jpg', '.jpeg', '.tif', '.tiff']:
|
||||
with open(fullpath, 'r') as f:
|
||||
src = '<img src="data:image/' + ext + ';base64,' + base64.b64encode(f.read()) + '">'
|
||||
|
||||
if src:
|
||||
display[name] = src
|
||||
|
||||
return render_template("public/job.html", script=script, job=job, metadata=script.load_config(), console=console, display=display)
|
|
@ -0,0 +1,44 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
os_env = os.environ
|
||||
|
||||
|
||||
class Config(object):
|
||||
SECRET_KEY = os_env.get('WOOEY_SECRET', 'secret-key') # TODO: Change me
|
||||
APP_DIR = os.path.abspath(os.path.dirname(__file__)) # This directory
|
||||
PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir))
|
||||
BCRYPT_LOG_ROUNDS = 13
|
||||
ASSETS_DEBUG = False
|
||||
DEBUG_TB_ENABLED = False # Disable Debug toolbar
|
||||
DEBUG_TB_INTERCEPT_REDIRECTS = False
|
||||
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
|
||||
|
||||
|
||||
class ProdConfig(Config):
|
||||
"""Production configuration."""
|
||||
ENV = 'prod'
|
||||
DEBUG = False
|
||||
SQLALCHEMY_DATABASE_URI = 'postgresql://localhost/example' # TODO: Change me
|
||||
DEBUG_TB_ENABLED = False # Disable Debug toolbar
|
||||
|
||||
|
||||
class DevConfig(Config):
|
||||
"""Development configuration."""
|
||||
ENV = 'dev'
|
||||
DEBUG = True
|
||||
DB_NAME = 'dev.db'
|
||||
# Put the db file in project root
|
||||
DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(DB_PATH)
|
||||
DEBUG_TB_ENABLED = True
|
||||
ASSETS_DEBUG = True # Don't bundle/minify static assets
|
||||
CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc.
|
||||
|
||||
|
||||
class TestConfig(Config):
|
||||
TESTING = True
|
||||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite://'
|
||||
BCRYPT_LOG_ROUNDS = 1 # For faster tests
|
||||
WTF_CSRF_ENABLED = False # Allows form testing
|
|
@ -0,0 +1,61 @@
|
|||
/* =============================================================================
|
||||
App specific CSS file.
|
||||
========================================================================== */
|
||||
|
||||
|
||||
|
||||
.form-with-annotation {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
|
||||
.form-annotation small {
|
||||
color: #bbb;
|
||||
|
||||
}
|
||||
|
||||
footer {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
body {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.tab-title > a {
|
||||
padding: 8px 16px !important;
|
||||
}
|
||||
|
||||
.tab-title.active > a {
|
||||
}
|
||||
|
||||
.settings-panel {
|
||||
background-color: #f5f5f5 !important;
|
||||
padding: 0.8em;
|
||||
}
|
||||
|
||||
.settings-panel-tabs .tab-title > a {
|
||||
background-color: #ddd !important;
|
||||
}
|
||||
|
||||
.settings-panel-tabs .tab-title.active > a {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.console {
|
||||
overflow:scroll;
|
||||
background-color: #333;
|
||||
color: #0f0;
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.console .console-body {
|
||||
margin:1em;
|
||||
white-space: pre;
|
||||
font-size: 0.9rem;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// place any jQuery/helper plugins in here, instead of separate, slower script files.
|
|
@ -0,0 +1,5 @@
|
|||
(function($, window) {
|
||||
|
||||
|
||||
|
||||
}).call(this, jQuery, window);
|
|
@ -0,0 +1,16 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block page_title %}Unauthorized{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="text-center">
|
||||
<h1>401</h1>
|
||||
<p>You are not authorized to see this page. Please <a href="{{ url_for('public.home')}}">log in</a> or
|
||||
<a href="{{ url_for('public.register') }}">create a new account</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block page_title %}Page Not Found{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="text-center">
|
||||
<h1>404</h1>
|
||||
<p>Sorry, that page doesn't exist.</p>
|
||||
<p>Want to <a href="{{ url_for('public.home') }}">go home</a> instead?</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block page_title %}Server error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="text-center">
|
||||
<h1>500</h1>
|
||||
<p>Sorry, something went wrong on our system. Don't panic, we are fixing it! Please try again later.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<footer>
|
||||
<!--
|
||||
<ul class="inline-list">
|
||||
<li>© Martin Fitzpatrick</li>
|
||||
<li><a href="{{ url_for('public.about') }}">About</a></li>
|
||||
<li><a href="mailto:martin.fitzpatrick@gmail.com">Contact</a></li>
|
||||
</ul>
|
||||
-->
|
||||
</footer>
|
|
@ -0,0 +1,82 @@
|
|||
|
||||
<!doctype html>
|
||||
<!-- paulirish.com/2008/conditional-stylesheets-vs-css-hacks-answer-neither/ -->
|
||||
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7" lang="en"> <![endif]-->
|
||||
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8" lang="en"> <![endif]-->
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" lang="en"> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" lang="en"> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{% block page_title %}
|
||||
|
||||
Wooey
|
||||
|
||||
{% endblock %}
|
||||
</title>
|
||||
<meta name="description" content="{% block meta_description %}{% endblock %}">
|
||||
<meta name="author" content="{% block meta_author %}{% endblock %}">
|
||||
|
||||
<!-- Mobile viewport optimized: h5bp.com/viewport -->
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/css/foundation.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/css/normalize.min.css">
|
||||
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.8.3/modernizr.min.js"></script>
|
||||
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
|
||||
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/foundation/5.5.1/js/foundation.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
|
||||
{% assets "css_all" %}
|
||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||
{% endassets %}
|
||||
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
|
||||
</head>
|
||||
<body class="{% block body_class %}{% endblock %}">
|
||||
{% block body %}
|
||||
{% with form=form %}
|
||||
{% include "nav.html" %}
|
||||
{% endwith %}
|
||||
|
||||
<header>{% block header %}{% endblock %}</header>
|
||||
<div class="{% block content_class %}row{% endblock content_class %}">
|
||||
|
||||
<div role="main">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="row">
|
||||
<div class="large-12 columns">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert-box {{ category }}">
|
||||
<a class="close" title="Close" href="#" data-dismiss="alert">×</a>
|
||||
{{message}}
|
||||
</div><!-- end .alert -->
|
||||
{% endfor %}
|
||||
</div><!-- end col-md -->
|
||||
</div><!-- end row -->
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
</div><!-- end container -->
|
||||
|
||||
{% include "footer.html" %}
|
||||
|
||||
<!-- JavaScript at the bottom for fast page loading -->
|
||||
{% assets "js_all" %}
|
||||
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||
{% endassets %}
|
||||
{% block js %}{% endblock %}
|
||||
<!-- end scripts -->
|
||||
{% endblock %}
|
||||
<script type="text/javascript">$(document).foundation();</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<nav class="top-bar" data-topbar role="navigation">
|
||||
<ul class="title-area"><li class="name"><h1><a href="{{ url_for('public.home') }}">Wooey</a></h1></li></ul>
|
||||
|
||||
|
||||
<section class="top-bar-section">
|
||||
<!-- Right Nav Section -->
|
||||
<ul class="right">
|
||||
|
||||
{% if current_user and current_user.is_authenticated() %}
|
||||
<li><a href="{{ url_for('user.members') }}">Logged in as {{ current_user.username }}</a></li>
|
||||
<li><a href="{{ url_for('public.logout') }}"><i class="fa fa-sign-out"></i></a></li>
|
||||
{% elif form %}
|
||||
<form id="loginForm" method="POST" action="/" role="login">
|
||||
<li><a href="{{ url_for('public.register') }}">Create account</a></li>
|
||||
|
||||
<li class="has-form">{{ form.hidden_tag() }}{{ form.username(placeholder="Username", class_="form-control") }}</li>
|
||||
<li class="has-form">{{ form.password(placeholder="Password", class_="form-control") }}</li>
|
||||
<li class="has-form"><button type="submit" class="btn btn-default">Log in</button></li>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<!-- Left Nav Section -->
|
||||
<ul class="left">
|
||||
<li><a href="{{ url_for('public.home') }}">Home</a></li>
|
||||
<li><a href="{{ url_for('public.about') }}">About</a></li>
|
||||
<li><a href="{{ url_for('public.scripts') }}">Scripts</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="large-12 columns">
|
||||
<h1>About</h1>
|
||||
<p>Woeey is a web interface for command-line scripts. Built to target Python scripts using argparse (based on <a href="https://github.com/chriskiehl/Gooey">Gooey</a>
|
||||
GUI construction from ArgumentParser instances) it will eventually run other style command lines, customisable through
|
||||
a per-script JSON configuration.</p>
|
||||
|
||||
<p>Auto-creation of JSON descriptions for Python scripts is based on the excellent <a href="https://github.com/chriskiehl/Gooey">Gooey</a> tool.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</h1>
|
||||
|
||||
<p>{{ script.description }}</p>
|
||||
|
||||
<!-- Build the UI
|
||||
{u'optional': [{u'type': u'CheckBox', u'data': {u'display_name': u'accumulate', u'commands': [u'--sum'], u'nargs': u'', u'help': u'sum the integers (default: find the max)', u'choices': []}}], u'program': {u'path': u'/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py', u'epilog': None, u'name': u'mock_argparse_example', u'description': u'Process some integers.'}, u'parser': {u'prefix_chars': u'-', u'argument_default': None}, u'required': [{u'type': u'FileChooser', u'data': {u'display_name': u'integers', u'commands': [], u'nargs': u'+', u'help': u'an integer for the accumulator', u'choices': []}}]}
|
||||
|
||||
'FileChooser',
|
||||
'MultiFileChooser',
|
||||
'FileSaver',
|
||||
'DirChooser',
|
||||
'DateChooser',
|
||||
'TextField',
|
||||
'Dropdown',
|
||||
'Counter',
|
||||
'RadioGroup',
|
||||
'CheckBox'
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
{% include "public/job-form.html" %}
|
||||
<div class="large-7 columns">
|
||||
{% if documentation %}
|
||||
{{ documentation|safe }}
|
||||
{% else %}
|
||||
<p>No documentation for this script.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<!-- Main jumbotron for a primary marketing message or call to action -->
|
||||
<div class="jumbotron">
|
||||
<h1>Welcome to Wooey</h1>
|
||||
<p>Perform data analysis, system administration, or anything you like, via a web-based interface to command-line scripts.</p>
|
||||
</div><!-- /.jumbotron -->
|
||||
|
||||
<div class="row">
|
||||
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="200">Script</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for script in scripts %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('public.create_job', script_id=script.id) }}" class="list-group-item{% if not script.is_active %} disabled{% endif %}">
|
||||
{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</a></td>
|
||||
<td>{{ script.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<div class="large-5 columns">
|
||||
|
||||
<ul class="tabs settings-panel-tabs" data-tab role="tablist">
|
||||
{% for section in ['required','optional'] %}
|
||||
<li class="tab-title {% if loop.first %}active{% endif %} radius" role="presentational" ><a href="#settings-{{section}}" role="tab" tabindex="0" aria-selected="{% if loop.first %}true{% else %}false{% endif %}" controls="settings-{{section}}">{{section}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" action="{{ url_for('public.create_job', script_id=script.id) }}">
|
||||
<div class="tabs-content settings-panel">
|
||||
|
||||
|
||||
{% for section in ['required','optional'] %}
|
||||
<section role="tabpanel" aria-hidden="false" class="content {% if loop.first %}active{% endif %}" id="settings-{{section}}">
|
||||
|
||||
{% for w in metadata[section] %}
|
||||
<div class="row">
|
||||
<div class="large-12 columns">
|
||||
|
||||
{% if w['widget'] == 'FileChooser' %}
|
||||
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
|
||||
<input type="file" name="{{ w['data']['display_name'] }}" id="{{ w['data']['display_name'] }}">
|
||||
</label>
|
||||
{% elif w['widget'] == 'CheckBox' %}
|
||||
<div class="row">
|
||||
<div class="large-1 columns">
|
||||
<input type="checkbox" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
|
||||
</div>
|
||||
<div class="large-11 columns">
|
||||
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}</label>
|
||||
</div>
|
||||
</div>
|
||||
{% elif w['widget'] == 'TextField' %}
|
||||
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
|
||||
<input type="text" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
|
||||
</label>
|
||||
{% elif w['widget'] == 'Dropdown' %}
|
||||
<label for="{{ w['data']['display_name'] }}">{{ w['data']['help'] }}
|
||||
<select type="text" placeholder="{{ w['data']['default'] }}" id="{{ w['data']['display_name'] }}" name="{{ w['data']['display_name'] }}">
|
||||
|
||||
{% for choice in w['data']['choices'] %}
|
||||
<option value="{{ choice }}">{{ choice }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</section>
|
||||
{% endfor%}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="large-9 columns">
|
||||
<input type="button" class="button alert right small" value="Cancel" onclick="window.history.back()">
|
||||
</div><div class="large-3 columns">
|
||||
<input type="submit" class="button success right small" value="Run">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,68 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}
|
||||
<small>(Job ID {{ job.id }})</small>
|
||||
|
||||
{% if job.status == 'W' %}<span class="label info">Waiting</span>
|
||||
{% elif job.status == 'R' %}<span class="label info">Running</span>
|
||||
{% elif job.status == 'C' %}<span class="label success">Complete</span>
|
||||
{% elif job.status == 'X' %}<span class="label alert">Error</span>{% endif %}
|
||||
|
||||
</h1>
|
||||
|
||||
<p>{{ script.description }}</p>
|
||||
|
||||
<!-- Build the UI
|
||||
{u'optional': [{u'type': u'CheckBox', u'data': {u'display_name': u'accumulate', u'commands': [u'--sum'], u'nargs': u'', u'help': u'sum the integers (default: find the max)', u'choices': []}}], u'program': {u'path': u'/Users/mxf793/repos/wooey/scripts/mock_argparse_example.py', u'epilog': None, u'name': u'mock_argparse_example', u'description': u'Process some integers.'}, u'parser': {u'prefix_chars': u'-', u'argument_default': None}, u'required': [{u'type': u'FileChooser', u'data': {u'display_name': u'integers', u'commands': [], u'nargs': u'+', u'help': u'an integer for the accumulator', u'choices': []}}]}
|
||||
|
||||
'FileChooser',
|
||||
'MultiFileChooser',
|
||||
'FileSaver',
|
||||
'DirChooser',
|
||||
'DateChooser',
|
||||
'TextField',
|
||||
'Dropdown',
|
||||
'Counter',
|
||||
'RadioGroup',
|
||||
'CheckBox'
|
||||
|
||||
-->
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="row">
|
||||
{% include "public/job-form.html" %}
|
||||
<div class="large-7 columns">
|
||||
|
||||
|
||||
<ul class="tabs" data-tab role="tablist">
|
||||
<li class="tab-title active" role="presentational" ><a href="#output-console" role="tab" tabindex="0" aria-selected="true" controls="output-console">Console</a></li>
|
||||
{% for section in display.keys() %}
|
||||
<li class="tab-title" role="presentational" ><a href="#output-{{ section.replace(' ','-') }}" role="tab" tabindex="0" aria-selected="" controls="output-{{ section.replace(' ','-') }}">{{section}}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tabs-content">
|
||||
<section role="tabpanel" aria-hidden="false" class="content active console" id="output-console">
|
||||
<div class="row">
|
||||
<div class="large-12 columns console-body">{{ console }}</div>
|
||||
</div>
|
||||
</section>
|
||||
{% for section, src in display.items() %}
|
||||
<section role="tabpanel" aria-hidden="false" class="content" id="output-{{ section.replace(' ','-') }}">
|
||||
<div class="row">
|
||||
<div class="large-12 columns console-body">{{ src|safe }}</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<div class="container-narrow">
|
||||
<h1>Register</h1>
|
||||
<br/>
|
||||
<form id="registerForm" class="form form-register" method="POST" action="" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="form-group">
|
||||
{{form.username.label}}
|
||||
{{form.username(placeholder="Username", class_="form-control")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{form.email.label}}
|
||||
{{form.email(placeholder="Email", class_="form-control")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{form.password.label}}
|
||||
{{form.password(placeholder="Password", class_="form-control")}}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
{{form.confirm.label}}
|
||||
{{form.confirm(placeholder="Password (again)", class_="form-control")}}
|
||||
</div>
|
||||
<p><input class="btn btn-default btn-submit" type="submit" value="Register"></p>
|
||||
</form>
|
||||
<p><em>Already registered?</em> Click <a href="/">here</a> to login.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>Available Scripts</h1>
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Script</th>
|
||||
<th></th>
|
||||
<th><i class="fa fa-play" title="Enabled?"></i></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for script in scripts %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('public.create_job', script_id=script.id) }}" class="list-group-item{% if not script.is_active %} disabled{% endif %}">
|
||||
{% if script.nicename %}{{ script.display_name }}{% else %}{{ script.name }}{% endif %}</a></td>
|
||||
<td>{{ script.description }}</td>
|
||||
<td>{% if script.is_active %}<i class="fa fa-check"></i>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<h1>{{ current_user.username }}'s Jobs</h1>
|
||||
|
||||
|
||||
<table width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th><th>Script name</th>
|
||||
<th></th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for job in user.jobs %}
|
||||
<tr>
|
||||
<td>{{ job.id }}
|
||||
</td><td>
|
||||
<a href="{{ url_for('public.job', job_id=job.id) }}">
|
||||
{% if job.script.nicename %}{{ job.script.display_name }}{% else %}{{ job.script.name }}{% endif %}</a></td>
|
||||
<td>{{ job.script.description }}</td>
|
||||
<td>{{ job.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>{{ job.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>
|
||||
|
||||
{% if job.status == 'W' %}<span class="label info">Waiting</span>
|
||||
{% elif job.status == 'R' %}<span class="label info">Running</span>
|
||||
{% elif job.status == 'C' %}<span class="label success">Complete</span>
|
||||
{% elif job.status == 'X' %}<span class="label alert">Error</span>{% endif %}
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody></table>
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
'''The user module.'''
|
||||
|
||||
from . import views
|
|
@ -0,0 +1,34 @@
|
|||
from flask_wtf import Form
|
||||
from wtforms import TextField, PasswordField
|
||||
from wtforms.validators import DataRequired, Email, EqualTo, Length
|
||||
|
||||
from .models import User
|
||||
|
||||
|
||||
class RegisterForm(Form):
|
||||
username = TextField('Username',
|
||||
validators=[DataRequired(), Length(min=3, max=25)])
|
||||
email = TextField('Email',
|
||||
validators=[DataRequired(), Email(), Length(min=6, max=40)])
|
||||
password = PasswordField('Password',
|
||||
validators=[DataRequired(), Length(min=6, max=40)])
|
||||
confirm = PasswordField('Verify password',
|
||||
[DataRequired(), EqualTo('password', message='Passwords must match')])
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||
self.user = None
|
||||
|
||||
def validate(self):
|
||||
initial_validation = super(RegisterForm, self).validate()
|
||||
if not initial_validation:
|
||||
return False
|
||||
user = User.query.filter_by(username=self.username.data).first()
|
||||
if user:
|
||||
self.username.errors.append("Username already registered")
|
||||
return False
|
||||
user = User.query.filter_by(email=self.email.data).first()
|
||||
if user:
|
||||
self.email.errors.append("Email already registered")
|
||||
return False
|
||||
return True
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime as dt
|
||||
|
||||
from flask.ext.login import UserMixin
|
||||
|
||||
from wooey.extensions import bcrypt
|
||||
from wooey.database import (
|
||||
Column,
|
||||
db,
|
||||
Model,
|
||||
ReferenceCol,
|
||||
relationship,
|
||||
SurrogatePK,
|
||||
)
|
||||
|
||||
|
||||
class Role(SurrogatePK, Model):
|
||||
__tablename__ = 'roles'
|
||||
name = Column(db.String(80), unique=True, nullable=False)
|
||||
user_id = ReferenceCol('users', nullable=True)
|
||||
user = relationship('User', backref='roles')
|
||||
|
||||
def __init__(self, name, **kwargs):
|
||||
db.Model.__init__(self, name=name, **kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Role({name})>'.format(name=self.name)
|
||||
|
||||
|
||||
class User(UserMixin, SurrogatePK, Model):
|
||||
|
||||
__tablename__ = 'users'
|
||||
username = Column(db.String(80), unique=True, nullable=False)
|
||||
email = Column(db.String(80), unique=True, nullable=False)
|
||||
#: The hashed password
|
||||
password = Column(db.String(128), nullable=True)
|
||||
created_at = Column(db.DateTime, nullable=False, default=dt.datetime.utcnow)
|
||||
first_name = Column(db.String(30), nullable=True)
|
||||
last_name = Column(db.String(30), nullable=True)
|
||||
active = Column(db.Boolean(), default=False)
|
||||
is_admin = Column(db.Boolean(), default=False)
|
||||
|
||||
def __init__(self, username, email, password=None, **kwargs):
|
||||
db.Model.__init__(self, username=username, email=email, **kwargs)
|
||||
if password:
|
||||
self.set_password(password)
|
||||
else:
|
||||
self.password = None
|
||||
|
||||
def set_password(self, password):
|
||||
self.password = bcrypt.generate_password_hash(password)
|
||||
|
||||
def check_password(self, value):
|
||||
return bcrypt.check_password_hash(self.password, value)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return "{0} {1}".format(self.first_name, self.last_name)
|
||||
|
||||
def __repr__(self):
|
||||
return '<User({username!r})>'.format(username=self.username)
|
|
@ -0,0 +1,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from flask import Blueprint, render_template
|
||||
from flask.ext.login import current_user, login_required
|
||||
|
||||
blueprint = Blueprint("user", __name__, url_prefix='/users',
|
||||
static_folder="../static")
|
||||
|
||||
|
||||
@blueprint.route("/")
|
||||
@login_required
|
||||
def members():
|
||||
|
||||
return render_template("users/members.html", user=current_user)
|
|
@ -0,0 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
'''Helper utilities and decorators.'''
|
||||
from flask import flash
|
||||
|
||||
|
||||
def flash_errors(form, category="warning"):
|
||||
'''Flash all errors for a form.'''
|
||||
for field, errors in form.errors.items():
|
||||
for error in errors:
|
||||
flash("{0} - {1}"
|
||||
.format(getattr(form, field).label.text, error), category)
|
Loading…
Reference in New Issue