Compare commits
7 commits
Author | SHA1 | Date | |
---|---|---|---|
da271dcd52 | |||
4bc9d7bd9b | |||
0c69977e61 | |||
63e8059e94 | |||
b589dd70f3 | |||
c0d0e11591 | |||
de5549e43c |
11 changed files with 151 additions and 25 deletions
77
README.rst
Normal file
77
README.rst
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
WARPED - a Webbased frontend for ARgparser in Python
|
||||||
|
====================================================
|
||||||
|
|
||||||
|
``warped`` can be used to execute single Python file and Python modules.
|
||||||
|
It captures calls to the ``argparse`` module of Python and renders a web
|
||||||
|
GUI based on the options and arguments defined. It also displays the
|
||||||
|
output of the program inside the web GUI and allows you to stop, pause
|
||||||
|
and resume the program, as well as downloading the output.
|
||||||
|
|
||||||
|
Install
|
||||||
|
-------
|
||||||
|
|
||||||
|
Either get a stable version from PyPI, or install the current version
|
||||||
|
from git
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
# Installation from PyPI
|
||||||
|
pip install warped
|
||||||
|
|
||||||
|
# Installation from git
|
||||||
|
pip install git+https://git.k-fortytwo.de/christofsteel/warped/
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
warped [-h] [--port PORT] [--host HOST] [--module] file
|
||||||
|
|
||||||
|
a Webbased frontend for ARgparse in Python
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
file File to run
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--port PORT, -p PORT The port to listen on (default 5000)
|
||||||
|
--host HOST The host to bind to (default 0.0.0.0)
|
||||||
|
--module, -m If set, loads a module instead of a file
|
||||||
|
|
||||||
|
Sample
|
||||||
|
------
|
||||||
|
|
||||||
|
To test the capabilities of ``warped`` an example module was included.
|
||||||
|
You can run it like this:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
warped -m warped.samples.hooked
|
||||||
|
|
||||||
|
Since ``warped`` also makes use of the argparse module, ``warped``
|
||||||
|
itself can be //warped//.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
warped -m warped.hook
|
||||||
|
|
||||||
|
How does it work?
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
When ``warped`` is executed, it starts a flask webserver. The javascript
|
||||||
|
of the website reads the ``/arguments`` resource of the server, where
|
||||||
|
the configuration of the argparser returned. In a seperate process the
|
||||||
|
given program is executed using the ``runpy`` library, redirecting
|
||||||
|
``sys.stdin`` and ``sys.stdout`` to a ``multiprocessing.Queue``, which
|
||||||
|
can be read by the warp process to display it via the web GUI.
|
||||||
|
|
||||||
|
Additionally, ``warped`` adds an entry for ``argparse`` in the
|
||||||
|
``sys.modules`` list. Python looks first looks at this list, everytime a
|
||||||
|
module is imported, to avoid importing a module multiple times. This
|
||||||
|
custom ``argparse`` module behaves similar to the original ``argparse``
|
||||||
|
module. In fact with the exception of the
|
||||||
|
``ArgumentParser.parse_args()`` method, it works exactly like the
|
||||||
|
original. Once the program calls the ``parse_args()`` method, it blocks
|
||||||
|
and waits for user interaction via the web GUI. Once the user submits
|
||||||
|
the data, the process continues.
|
6
setup.py
6
setup.py
|
@ -1,11 +1,13 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
version = "0.1.2"
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='warped',
|
name='warped',
|
||||||
version='0.1.0',
|
version=version,
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
url='https://git.k-fortytwo.de/christofsteel/warp',
|
url='https://git.k-fortytwo.de/christofsteel/warp',
|
||||||
download_url = 'https://git.k-fortytwo.de/christofsteel/warped/archive/0.1.0.tar.gz',
|
download_url = 'https://git.k-fortytwo.de/christofsteel/warped/archive/%s.tar.gz' % version,
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Christoph Stahl',
|
author='Christoph Stahl',
|
||||||
author_email='christoph.stahl@uni-dortmund.de',
|
author_email='christoph.stahl@uni-dortmund.de',
|
||||||
|
|
|
@ -43,6 +43,7 @@ class Action():
|
||||||
internal_dict['dest'] = self.dest
|
internal_dict['dest'] = self.dest
|
||||||
internal_dict['desc'] = self.desc
|
internal_dict['desc'] = self.desc
|
||||||
internal_dict['checked'] = self.on_none if self.on_none is True or self.on_none is False else None
|
internal_dict['checked'] = self.on_none if self.on_none is True or self.on_none is False else None
|
||||||
|
internal_dict['default'] = self.on_none if type(self.on_none) == str else None
|
||||||
internal_dict['optional'] = self.optional
|
internal_dict['optional'] = self.optional
|
||||||
internal_dict['is_const'] = self.const is not None
|
internal_dict['is_const'] = self.const is not None
|
||||||
internal_dict['type'] = self.type_function.__name__
|
internal_dict['type'] = self.type_function.__name__
|
||||||
|
@ -89,7 +90,7 @@ class ActionContainer():
|
||||||
internal_dict = {}
|
internal_dict = {}
|
||||||
internal_dict['uuid'] = str(self.uuid)
|
internal_dict['uuid'] = str(self.uuid)
|
||||||
internal_dict['actions'] = [action.as_dict() for action in self.actions]
|
internal_dict['actions'] = [action.as_dict() for action in self.actions]
|
||||||
internal_dict['groups'] = [[action.as_dict() for action in group] for group in self.groups]
|
internal_dict['groups'] = [group.as_dict() for group in self.groups]
|
||||||
return internal_dict
|
return internal_dict
|
||||||
|
|
||||||
class StoreAction(Action):
|
class StoreAction(Action):
|
||||||
|
@ -97,6 +98,8 @@ class StoreAction(Action):
|
||||||
|
|
||||||
class StoreConstAction(Action):
|
class StoreConstAction(Action):
|
||||||
def store_type_function(self, x):
|
def store_type_function(self, x):
|
||||||
|
if x == 'false':
|
||||||
|
x = None
|
||||||
return self.const if x is not None else self.on_none
|
return self.const if x is not None else self.on_none
|
||||||
|
|
||||||
def __init__(self, action, **kwargs):
|
def __init__(self, action, **kwargs):
|
||||||
|
@ -109,4 +112,4 @@ class MutuallyExclusiveGroup(ActionContainer):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Group Object: ( Actions: {}, Groups: {} )".format(self.actions, self.mutex_groups)
|
return "Group Object: ( Actions: {}, Groups: {} )".format(self.actions, self.groups)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
import io
|
|
||||||
import runpy
|
import runpy
|
||||||
from traceback import print_exc
|
from traceback import print_exc
|
||||||
from contextlib import redirect_stdout, redirect_stderr
|
from contextlib import redirect_stdout, redirect_stderr
|
||||||
|
@ -24,9 +23,10 @@ class Context(Process):
|
||||||
sys.argv = [''] + self.arguments
|
sys.argv = [''] + self.arguments
|
||||||
else:
|
else:
|
||||||
sys.argv = ['']
|
sys.argv = ['']
|
||||||
#if not self.original_modules is None:
|
# if not self.original_modules is None:
|
||||||
# sys.modules = self.original_modules
|
# sys.modules.clear()
|
||||||
# print(sys.modules)
|
# sys.modules.update(self.original_modules)
|
||||||
|
|
||||||
sys.modules.update(self.overwritten_modules)
|
sys.modules.update(self.overwritten_modules)
|
||||||
with redirect_stdout(self.stdout):
|
with redirect_stdout(self.stdout):
|
||||||
with redirect_stderr(self.stderr):
|
with redirect_stderr(self.stderr):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from . import savemodules
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
@ -24,9 +25,15 @@ class QueuedOut(io.StringIO):
|
||||||
lines = b.split('\n')
|
lines = b.split('\n')
|
||||||
for line in lines[:-1]:
|
for line in lines[:-1]:
|
||||||
super().write(line)
|
super().write(line)
|
||||||
self.flush()
|
self.explflush()
|
||||||
super().write(lines[-1])
|
super().write(lines[-1])
|
||||||
|
|
||||||
|
def explflush(self):
|
||||||
|
value = self.getvalue()
|
||||||
|
self.queue.put((self.name, value))
|
||||||
|
self.seek(0)
|
||||||
|
self.truncate(0)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
value = self.getvalue()
|
value = self.getvalue()
|
||||||
if len(value) > 1:
|
if len(value) > 1:
|
||||||
|
@ -100,7 +107,7 @@ def start_module(name, is_module):
|
||||||
ioerr,
|
ioerr,
|
||||||
overwritten_modules={'argparse': argparser},
|
overwritten_modules={'argparse': argparser},
|
||||||
is_module = is_module,
|
is_module = is_module,
|
||||||
#original_modules = emptymodules
|
original_modules = savemodules.savedmodules
|
||||||
)
|
)
|
||||||
views.app.module_process.start()
|
views.app.module_process.start()
|
||||||
views.app.output.start()
|
views.app.output.start()
|
||||||
|
@ -112,6 +119,7 @@ def start_module(name, is_module):
|
||||||
views.app.output.stop()
|
views.app.output.stop()
|
||||||
|
|
||||||
ioerr.write("Process stopped ({})\n".format(views.app.module_process.exitcode))
|
ioerr.write("Process stopped ({})\n".format(views.app.module_process.exitcode))
|
||||||
|
views.app.output.queue.put(("sig", "stop"))
|
||||||
views.app.restart.wait()
|
views.app.restart.wait()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ def main():
|
||||||
group3 = group2.add_mutually_exclusive_group()
|
group3 = group2.add_mutually_exclusive_group()
|
||||||
group3.add_argument('--three')
|
group3.add_argument('--three')
|
||||||
group3.add_argument('--four')
|
group3.add_argument('--four')
|
||||||
bla.add_argument('--test', '-t', help="Blubb")
|
bla.add_argument('--test', '-t', help="Blubb", default="Blabla")
|
||||||
bla.add_argument('--bla', action="store_true", default=True)
|
bla.add_argument('--bla', action="store_true", default=True)
|
||||||
bla.add_argument('--blubb', action="store_true")
|
bla.add_argument('--blubb', action="store_true")
|
||||||
bla.add_argument('-f', action="append")
|
bla.add_argument('-f', action="append")
|
||||||
|
|
|
@ -11,3 +11,4 @@ if __name__ == "__main__":
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
print("Subparser %s was selected" % args.command)
|
print("Subparser %s was selected" % args.command)
|
||||||
|
print(args)
|
||||||
|
|
2
warped/savemodules.py
Normal file
2
warped/savemodules.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import sys
|
||||||
|
savedmodules = dict(sys.modules)
|
|
@ -7,6 +7,15 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
height:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=checkbox] {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
#output {
|
#output {
|
||||||
flex:1;
|
flex:1;
|
||||||
|
|
|
@ -21,6 +21,9 @@ function createSubparserAction(action) {
|
||||||
|
|
||||||
var content_div = $("<div/>", { id: choice['uuid'] })
|
var content_div = $("<div/>", { id: choice['uuid'] })
|
||||||
.addClass("tabs-panel").appendTo(tab_content);
|
.addClass("tabs-panel").appendTo(tab_content);
|
||||||
|
choice['groups'].forEach(function(group) {
|
||||||
|
content_div.append(createGroup(group));
|
||||||
|
});
|
||||||
choice['actions'].forEach(function(action) {
|
choice['actions'].forEach(function(action) {
|
||||||
content_div.append(createAction(action));
|
content_div.append(createAction(action));
|
||||||
});
|
});
|
||||||
|
@ -31,13 +34,14 @@ function createSubparserAction(action) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCheckboxAction(action) {
|
function createCheckboxAction(action) {
|
||||||
var switch_div = $("<div/>").addClass("switch");
|
//var switch_div = $("<div/>").addClass("switch");
|
||||||
|
var switch_div = $("<div/>").addClass("checkbox");
|
||||||
var input = $("<input/>", {
|
var input = $("<input/>", {
|
||||||
id: action["uuid"],
|
id: action["uuid"],
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
name: action['uuid']
|
name: action['uuid']
|
||||||
}).addClass('switch-input').data("name", action['dest']).appendTo(switch_div);
|
}).data("name", action['dest']).appendTo(switch_div);
|
||||||
var paddle = $("<label/>", {for: action["uuid"]}).addClass("switch-paddle")
|
var paddle = $("<label/>", {for: action["uuid"]}).addClass("css-label")
|
||||||
.appendTo(switch_div);
|
.appendTo(switch_div);
|
||||||
if(action['checked'] === true) {
|
if(action['checked'] === true) {
|
||||||
input.attr('checked', true);
|
input.attr('checked', true);
|
||||||
|
@ -120,7 +124,10 @@ function createInputAction(action) {
|
||||||
function disableAction(action) {
|
function disableAction(action) {
|
||||||
return function () {
|
return function () {
|
||||||
console.log("disable");
|
console.log("disable");
|
||||||
thisli = $('#action-' + action['uuid']);
|
var thisli = $('#action-' + action['uuid']);
|
||||||
|
var input = $('#action-' + action['uuid'] + ' input');
|
||||||
|
thisli.attr('old_val', input.val())
|
||||||
|
$('#action-' + action['uuid'] + ' input').val(action["default"]);
|
||||||
thisli.prop('disabled', true).addClass('disabled');
|
thisli.prop('disabled', true).addClass('disabled');
|
||||||
delbutton = $('#action-' + action['uuid' + "button"]);
|
delbutton = $('#action-' + action['uuid' + "button"]);
|
||||||
delbutton.prop('disabled', true).addClass('disabled');
|
delbutton.prop('disabled', true).addClass('disabled');
|
||||||
|
@ -134,7 +141,13 @@ function disableAction(action) {
|
||||||
function enableAction(action) {
|
function enableAction(action) {
|
||||||
return function() {
|
return function() {
|
||||||
console.log("enable");
|
console.log("enable");
|
||||||
thisli = $('#action-' + action['uuid']);
|
var thisli = $('#action-' + action['uuid']);
|
||||||
|
var oldval = thisli.attr('old_val');
|
||||||
|
if (oldval !== undefined) {
|
||||||
|
var input = $('#action-' + action['uuid'] + ' input');
|
||||||
|
thisli.oldval = input.val()
|
||||||
|
$('#action-' + action['uuid'] + ' input').val(oldval);
|
||||||
|
}
|
||||||
thisli.prop('disabled', false).removeClass('disabled');
|
thisli.prop('disabled', false).removeClass('disabled');
|
||||||
delbutton = $('#action-' + action['uuid' + "button"]);
|
delbutton = $('#action-' + action['uuid' + "button"]);
|
||||||
delbutton.prop('disabled', false).removeClass('disabled');
|
delbutton.prop('disabled', false).removeClass('disabled');
|
||||||
|
@ -181,6 +194,7 @@ function createAction(action) {
|
||||||
if(action.optional === true) {
|
if(action.optional === true) {
|
||||||
li.prop('disabled', true).addClass('disabled');
|
li.prop('disabled', true).addClass('disabled');
|
||||||
input.prop('disabled', true).addClass('disabled');
|
input.prop('disabled', true).addClass('disabled');
|
||||||
|
input.val(action["default"]);
|
||||||
input.find('input').prop('disabled', true).addClass('disabled');
|
input.find('input').prop('disabled', true).addClass('disabled');
|
||||||
input.find('.addbutton').prop('disabled', true).addClass('disabled');
|
input.find('.addbutton').prop('disabled', true).addClass('disabled');
|
||||||
input.find('.rembutton').prop('disabled', true).addClass('disabled');
|
input.find('.rembutton').prop('disabled', true).addClass('disabled');
|
||||||
|
|
|
@ -12,21 +12,33 @@ from flask import Flask, render_template, request, Response, \
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.mutex_groups=[]
|
app.mutex_groups=[]
|
||||||
|
|
||||||
def parse_argument(name, json, action):
|
def parse_argument(name, json, action, namespace):
|
||||||
try:
|
try:
|
||||||
argument = json[name]
|
argument = json[name]
|
||||||
if type(argument) == list:
|
if type(argument) == list:
|
||||||
if len(argument) == 1:
|
if len(argument) == 1:
|
||||||
return action.type_function(argument[0])
|
setattr(namespace, action.name, action.type_function(argument[0]))
|
||||||
else:
|
else:
|
||||||
return [action.type_function(elem) for elem in argument]
|
setattr(namespace, action.name, [action.type_function(elem) for elem in argument])
|
||||||
else:
|
else:
|
||||||
return action.type_function(argument)
|
setattr(namespace, action.name, action.type_function(argument))
|
||||||
except (KeyError, ValueError):
|
except (KeyError, ValueError):
|
||||||
if name.endswith("[]"):
|
if name.endswith("[]"):
|
||||||
return action.on_none
|
setattr(namespace, action.name, action.on_none)
|
||||||
else:
|
else:
|
||||||
return parse_argument(name + "[]", json, action)
|
parse_argument(name + "[]", json, action, namespace)
|
||||||
|
|
||||||
|
try:
|
||||||
|
for name, choice in action.choices.items():
|
||||||
|
actions = choice.actions
|
||||||
|
for group in choice.groups:
|
||||||
|
actions.extend(group.actions)
|
||||||
|
for act in actions:
|
||||||
|
parse_argument(act.name, json, act, namespace)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/arguments", methods=['POST'])
|
@app.route("/arguments", methods=['POST'])
|
||||||
|
@ -39,8 +51,7 @@ def fill_namespace():
|
||||||
all_actions.extend(group.actions)
|
all_actions.extend(group.actions)
|
||||||
|
|
||||||
for action in all_actions:
|
for action in all_actions:
|
||||||
value = parse_argument(action.name, json, action)
|
parse_argument(action.name, json, action, namespace)
|
||||||
setattr(namespace, action.name, value)
|
|
||||||
|
|
||||||
app.namespaceQueue.put(namespace)
|
app.namespaceQueue.put(namespace)
|
||||||
app.output.queue.put(("sig", "start"))
|
app.output.queue.put(("sig", "start"))
|
||||||
|
@ -57,7 +68,6 @@ def get_arguments():
|
||||||
def stop():
|
def stop():
|
||||||
os.kill(app.module_process.pid, signal.SIGCONT)
|
os.kill(app.module_process.pid, signal.SIGCONT)
|
||||||
app.module_process.terminate()
|
app.module_process.terminate()
|
||||||
app.output.queue.put(("sig", "stop"))
|
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
@app.route("/resume")
|
@app.route("/resume")
|
||||||
|
|
Loading…
Add table
Reference in a new issue