├── .gitignore ├── argparseweb ├── requirements.txt ├── version.py ├── __init__.py ├── utils.py ├── webui.py ├── templates │ └── input.html └── page.py ├── .flake8 ├── examples ├── index.wsgi ├── index_cmdparser.wsgi └── python_server.py ├── setup.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /argparseweb/requirements.txt: -------------------------------------------------------------------------------- 1 | web.py 2 | -------------------------------------------------------------------------------- /argparseweb/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.3' 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E111, E113, E114, E121, E125, E127 3 | -------------------------------------------------------------------------------- /argparseweb/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ 2 | 3 | from .utils import * 4 | from .webui import * 5 | -------------------------------------------------------------------------------- /examples/index.wsgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # TODO: replace with your application path 4 | # i found now way to get it automatically in wsgi :/ 5 | APP_DIR = '/var/www/myapp' 6 | 7 | import sys, os 8 | sys.path.insert(0, APP_DIR) 9 | os.chdir(APP_DIR) 10 | 11 | from myapp import application 12 | -------------------------------------------------------------------------------- /examples/index_cmdparser.wsgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # TODO: replace with your application path 4 | # i found now way to get it automatically in wsgi :/ 5 | APP_DIR = '/var/www/myapp' 6 | 7 | import sys, os 8 | sys.path.insert(0, APP_DIR) 9 | os.chdir(APP_DIR) 10 | 11 | from myapp import get_parser 12 | import webui 13 | 14 | cmd_parser = get_parser() 15 | cmd_parser = webui.Webui(cmd_parser) 16 | 17 | application = cmd_parser.wsgi() 18 | -------------------------------------------------------------------------------- /argparseweb/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ReloadedIterable: 4 | """This is an automatically reloaded iterable object. 5 | Every time it starts an iteration it'll call the specified function 6 | and generate an updated version of the sequence, yielding one item at a time. 7 | 8 | Its generally useful but specifically created to wrap argparse.Action.choices-like parameters, 9 | so they'll be updated in new without the argparse object being re-generated.""" 10 | def __init__(self, f, *args, **kwargs): 11 | self._f = f 12 | self._args = args 13 | self._kwargs = kwargs 14 | 15 | def generate(self): 16 | return self._f(*self._args, **self._kwargs) 17 | 18 | def __iter__(self): 19 | for item in self.generate(): 20 | yield item 21 | 22 | def __getitem__(self, i): 23 | return self.generate()[i] 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from os.path import exists 4 | from setuptools import setup 5 | 6 | def get_requirements(): 7 | return open('./argparseweb/requirements.txt').readlines() 8 | 9 | def get_version(): 10 | context = {} 11 | execfile('./argparseweb/version.py', context) 12 | return context['__version__'] 13 | 14 | setup( 15 | name = 'argparseweb', 16 | packages = ['argparseweb'], 17 | package_data = {'argparseweb': ['templates/*.html', 18 | 'requirements.txt']}, 19 | version = get_version(), 20 | description = 'Automatic exposure of argparse-compatible scripts as a web interface', 21 | long_description=(open('README.md').read() if exists('README.md') else ''), 22 | author = 'Nir Izraeli', 23 | author_email = 'nirizr@gmail.com', 24 | url = 'https://github.com/nirizr/argparseweb', 25 | keywords = ['webui', 'web', 'user', 'interface', 'ui', 'argparse', 'webpy', 'web.py'], 26 | install_requires = get_requirements(), 27 | classifiers = [], 28 | ) 29 | -------------------------------------------------------------------------------- /argparseweb/webui.py: -------------------------------------------------------------------------------- 1 | import web 2 | 3 | import page 4 | 5 | import multiprocessing 6 | 7 | 8 | class Webui(object): 9 | def __init__(self, parser): 10 | self._parser = parser 11 | 12 | def get_urls(self): 13 | return ('/', 'index') 14 | 15 | def app(self, dispatch, parsed): 16 | # Make sure we get an argh-like object here that has a dispatch object 17 | if dispatch is None: 18 | if not hasattr(self._parser, 'dispatch'): 19 | raise ValueError("Can't dispatch a non dispatchable parser without a dispatch method") 20 | dispatch = self._parser.dispatch 21 | parsed = False 22 | 23 | class WebuiPageWrapper(page.WebuiPage): 24 | _parser = self._parser 25 | _dispatch = dispatch 26 | _parsed = parsed 27 | 28 | urls = ('/', 'index') 29 | classes = {'index': WebuiPageWrapper} 30 | 31 | return web.application(urls, classes) 32 | 33 | def dispatch(self, dispatch=None, parsed=False): 34 | self.app(dispatch=dispatch, parsed=parsed).run() 35 | 36 | def wsgi(self, dispatch=None, parsed=True): 37 | return self.app(dispatch, parsed).wsgifunc() 38 | 39 | def get(self, count=True): 40 | # prepare a process-safe queue to hold all results 41 | results = multiprocessing.Queue() 42 | 43 | # spawn web.py server in another process, have it's dispatch as queue.put method 44 | app = self.app(dispatch=results.put, parsed=True) 45 | t = multiprocessing.Process(target=app.run) 46 | t.start() 47 | 48 | # stop condition: if count is a number decrease and loop until 0, 49 | # if count is True, loop forever 50 | while count: 51 | yield results.get() 52 | 53 | if type(count) == int: 54 | count -= 1 55 | 56 | app.stop() 57 | t.terminate() 58 | 59 | def getone(self): 60 | return list(self.get(count=1))[0] 61 | -------------------------------------------------------------------------------- /examples/python_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import sys 3 | 4 | import webui 5 | 6 | import argparse 7 | 8 | # this example is actually taken from https://github.com/shimpe/argparseui/blob/master/argparseui/ui.py 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument("-m", "--make-argument-true", help="optional boolean argument", action="store_true") 11 | parser.add_argument("-o","--make-other-argument-true", help="optional boolean argument 2", action="store_true", default=True) 12 | parser.add_argument("-n","--number", help="an optional number", type=int) 13 | parser.add_argument("-r","--restricted-number", help="one of a few possible numbers", type=int, choices=[1,2,3], default=2) 14 | parser.add_argument("-c", "--counting-argument", help="counting #occurrences", action="count") 15 | parser.add_argument("--default-value-argument", "-d", help="default value argument with a very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long description", type=float, default="3.14") 16 | parser.add_argument("-a", "--append-args", help="append arguments to list", type=str, action="append", default=["dish", "dash"]) 17 | group = parser.add_mutually_exclusive_group() 18 | group.add_argument("-v", "--verbose", action="store_true") 19 | group.add_argument("-q", "--quiet", action="store_true") 20 | parser.add_argument('--foo', type=int, nargs='+') 21 | parser.add_argument('--bar', type=int, nargs=2, metavar=('bar', 'baz')) 22 | 23 | w = webui.Webui(parser) 24 | 25 | mode = sys.argv.pop() 26 | if mode == "dispatch": 27 | def p(webpage, arg): 28 | print(arg) 29 | 30 | w.dispatch(p) 31 | elif mode == "get": 32 | for r in w.get(): 33 | print(r) 34 | elif mode == "getone": 35 | print(w.getone()) 36 | -------------------------------------------------------------------------------- /argparseweb/templates/input.html: -------------------------------------------------------------------------------- 1 | $def with (form) 2 | 3 |
4 | 23 | 24 | 118 | 125 | 126 | 127 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Version Status][v-image]][pypi-url] 3 | 4 | # README # 5 | 6 | This web.py based simple module allows you to automatically set up a simple HTTP web server out of advanced `argparse.ArgumentParser` objects and similar (`argh.ArgumentParser`) ones. 7 | Using this on top of argh lets you automatically generate web user interface out of simple functions defined in your application. 8 | This package was made for getting your personal command line scripts to the next stage - internal shared utilities. 9 | 10 | ### How do I set up? ### 11 | 12 | For a production like setup you'll need: 13 | 14 | 1. make your main script expose an application global object by calling webui.Webui.wsgi() method 15 | 16 | 2. modify `index.wsgi` to fit your application (trivial configuration, import aforementioned application) 17 | 18 | 3. set up a wsgi supporting apache (or any other) server 19 | 20 | For debugging like setup you'll need (but since it's used for internal tools, this might also be fine): 21 | 22 | 1. replace methods like `argparse.ArgumentParser.parse_args()` or `argh.dispatch()` with `webui.Webui.getone()` or `webui.Webui.dispatch()` respectively. 23 | 24 | `dispatch()` will instantiate a web service and call dispatch methods (either provided by the user - you - or dispatch methods of supporting argument parsers like `argh`) 25 | 26 | `get()` and `getone()` wrap the `dispatch()` method and yield results as they are submitted in the web form, providing an interface that resembles the `parse_args()` method. 27 | 28 | ### Dependencies ### 29 | 30 | `argparseweb` requires `web.py` to be available. You can install it (check for the latest version) with: `pip install web.py` 31 | 32 | ### Basic examples ### 33 | This example will set up an http server, get one valid input, tear the http server down, print a welcoming message to stdout and exit: 34 | ```python 35 | import argparse 36 | from argparseweb import * 37 | 38 | def main(): 39 | parser = argparse.ArgumentParser() 40 | 41 | parser.add_argument("name", default="Anonymous") 42 | 43 | # previously opts = parser.parse_args() 44 | opts = webui.Webui(parser).getone() 45 | print("Hello {name},\nthis is a simple example.".format(name=opts.name)) 46 | 47 | if __name__ == "__main__": 48 | main() 49 | ``` 50 | 51 | This example will also run until stopped, printing a welcoming message for every valid input: 52 | ```python 53 | import argparse 54 | from argparseweb import * 55 | 56 | def main(): 57 | parser = argparse.ArgumentParser() 58 | 59 | parser.add_argument("name", default="Anonymous") 60 | 61 | # previously opts = parser.parse_args() 62 | for opts in webui.Webui(parser).get(): 63 | print("Hello {name},\nthis is a simple example.".format(name=opts.name)) 64 | 65 | if __name__ == "__main__": 66 | main() 67 | ``` 68 | 69 | This example will print the welcoming message in the http response, sending it back to the user: 70 | ```python 71 | import argparse 72 | from argparseweb import * 73 | 74 | def welcome(opts): 75 | print("Hello {name},\nthis is a simple example.".format(name=opts.name)) 76 | 77 | def main(): 78 | parser = argparse.ArgumentParser() 79 | 80 | parser.add_argument("name", default="Anonymous") 81 | 82 | # previously opts = parser.parse_args() 83 | webui.Webui(parser).dispatch(welcome, parsed=True) 84 | 85 | if __name__ == "__main__": 86 | main() 87 | ``` 88 | 89 | ### A more complicated example ### 90 | 91 | This snippet includes three modes of operation for the webui utility: 92 | 93 | 1. first and simplest: dispatch methods using argh's automatic function to command line parser facilities, this is completely unrelated to webui and that way you won't lose existing command line usage ability. 94 | 95 | 2. getting `--webui` as the first command line argument, sets up a development web server (defaults to *:8080) and is ready to use. 96 | 97 | 3. exposing an `application` global object that supports the wsgi interface. once you point a browser with correct wsgi configuration (was a bit of a pain for me first time) it'll work like magic :) 98 | 99 | myapp.py: 100 | ```python 101 | import argparse 102 | from argparseweb import * 103 | 104 | def get_parser(): 105 | """Generate generic argument parser""" 106 | cmd_parser = argh.ArghParser() 107 | cmd_parser.add_commands([...]) 108 | 109 | return cmd_parser 110 | 111 | def main_1(): 112 | # k. get the parser as usual 113 | cmd_parser = get_parser() 114 | 115 | # last chance to use webui, if --webui is passed as first command line argument 116 | # remove it as let webui handle the rest 117 | if sys.argv[1] == '--webui': 118 | sys.argv.remove('--webui') 119 | webui.Webui(cmd_parser).dispatch() # second mode of operation - development/fast setup 120 | else: 121 | # dispatch either webui or argh 122 | cmd_parser.dispatch() # first mode of operation - regular command line 123 | 124 | def main_2(): 125 | parser = argparse.ArgumentParser() 126 | 127 | # TODO: fill argparse 128 | 129 | # opts = parser.parse_args() 130 | opts = webui.Webui(parser).getone() 131 | 132 | # TODO: use opts as you would with any ArgumentParser generated namespace, 133 | # opts is really a namespace object directly created by parser, and webui only compiled an argument sequence 134 | # based on the filled form, passed into parser.parse_args() and back to you 135 | 136 | def wsgi(): 137 | global application 138 | 139 | # create a webui application using the command line argument parser object 140 | # and make it's wsgi function the global `application` parameter as required by wsgi 141 | cmd_parser = get_parser() 142 | application = webui.Webui(cmd_parser).wsgi() # third mode of operation - production wsgi application 143 | 144 | if __name__ == "__main__": 145 | # script initialized as main, lets do our trick 146 | main() 147 | else: 148 | # if script wasn't initialized as main script, we're probably running 149 | # in wsgi mode 150 | wsgi() 151 | ``` 152 | index.wsgi: 153 | ```python 154 | # TODO: replace with your application path 155 | # i found now way to get it automatically in wsgi :/ 156 | APP_DIR = '/var/www/myapp' 157 | 158 | import sys, os 159 | sys.path.insert(0, APP_DIR) 160 | os.chdir(APP_DIR) 161 | 162 | from myapp import application 163 | 164 | ``` 165 | 166 | More examples are at `test.py` 167 | 168 | ### known issues ### 169 | 170 | * right now vary-length arguments (nargs='?', nargs='*', nargs='+') are limited to one argument because i didn't write the HTML required for that. i'm considering multiple text inputs or textarea with line separation, input (and code) are most welcome. 171 | 172 | Done: 173 | 174 | * some code reordering is needed (split template to another file - it's grown quite big, handle action parameters better - shouldn't pass everything as html attributes although it's comfortable) 175 | * smoother integration into existing code. 176 | 177 | [v-image]: https://img.shields.io/pypi/v/argparseweb.svg 178 | [dm-image]: https://img.shields.io/pypi/dm/argparseweb.svg 179 | 180 | [pypi-url]: https://pypi.python.org/pypi/argparseweb/ 181 | -------------------------------------------------------------------------------- /argparseweb/page.py: -------------------------------------------------------------------------------- 1 | # builtin 2 | import os 3 | import sys 4 | import StringIO 5 | import argparse 6 | import collections 7 | 8 | # 3rd party 9 | import web 10 | 11 | 12 | class WebuiPage(object): 13 | _parser = None 14 | _dispatch = None 15 | _parsed = True 16 | _form_template = web.template.frender(os.path.join(os.path.dirname(__file__), 17 | "templates/input.html"), 18 | globals={'type': type, 19 | 'basestring': basestring}) 20 | 21 | def __init__(self): 22 | self._actions = collections.OrderedDict() 23 | 24 | form_inputs = self.get_form_inputs() 25 | self._form = web.form.Form(*form_inputs) 26 | 27 | def GET(self): 28 | form = self._form() 29 | yield self._form_template(form) 30 | 31 | def multiple_args(self, nargs): 32 | if nargs == '?': 33 | return False 34 | if nargs == '*' or nargs == '+': 35 | return True 36 | if type(nargs) == int and nargs > 1: 37 | return True 38 | return False 39 | 40 | def parsable_add_value(self, argv, action, value): 41 | if action.nargs == 0: 42 | pass 43 | elif self.multiple_args(action.nargs): 44 | argv.extend(value) 45 | else: 46 | argv.append(value) 47 | 48 | def get_input(self, form): 49 | for action_id, action in self._actions.items(): 50 | if action_id not in form.value: 51 | continue 52 | 53 | value = form.d[action_id] 54 | yield action_id, action, value 55 | 56 | def POST(self): 57 | form = self._form() 58 | 59 | if not form.validates(): 60 | return self._form_template(form) 61 | 62 | # make sure form is filled according to input 63 | defs = {} 64 | for action_id, action, _ in self.get_input(form): 65 | if self.multiple_args(action.nargs): 66 | defs[action_id] = [] 67 | i = web.input(**defs) 68 | form.fill(i) 69 | 70 | # get parameters without prefix 71 | pos_argv = [] 72 | opt_argv = [] 73 | 74 | for _, action, value in self.get_input(form): 75 | if self.get_disposition(action) == 'optional': 76 | action_name = self.get_name(action) 77 | arg_name = "--" + action_name 78 | opt_argv.append(arg_name) 79 | self.parsable_add_value(opt_argv, action, value) 80 | elif self.get_disposition(action) == 'positional': 81 | self.parsable_add_value(pos_argv, action, value) 82 | 83 | arg = pos_argv + opt_argv 84 | print(arg) 85 | 86 | web.header('Content-Type', 'text/html') 87 | content = 'attachment; filename="result_{}.txt"'.format(" ".join(arg)) 88 | web.header('Content-disposition', content) 89 | 90 | stdout = StringIO.StringIO() 91 | stderr = StringIO.StringIO() 92 | old_stderr = None 93 | old_stdout = None 94 | try: 95 | sys.stderr, old_stderr = stderr, sys.stderr 96 | sys.stdout, old_stdout = stdout, sys.stdout 97 | if self._parsed: 98 | arg = self._parser.parse_args(args=arg) 99 | result = self._dispatch(arg) 100 | except: 101 | import traceback 102 | result = traceback.format_exc() 103 | old_stderr.write(stderr.getvalue()) 104 | old_stdout.write(stdout.getvalue()) 105 | finally: 106 | if old_stderr: 107 | sys.stderr = old_stderr 108 | if old_stdout: 109 | sys.stdout = old_stdout 110 | 111 | return u"Running: {}\nErrors: {}\nResult: {}\nOutput:\n{}".format(arg, stderr.getvalue(), result, stdout.getvalue()) 112 | 113 | def get_base_id(self, action): 114 | base_id = action.dest 115 | for opt_name in action.option_strings: 116 | if len(base_id) < len(opt_name): 117 | base_id = opt_name 118 | if base_id == argparse.SUPPRESS: 119 | base_id = "command" 120 | return base_id.lstrip('-') 121 | 122 | def get_class(self, prefix): 123 | return "_".join(prefix) if prefix else "" 124 | 125 | def get_id(self, action, prefix): 126 | return self.get_class(prefix + [self.get_base_id(action)]) 127 | 128 | def get_name(self, action): 129 | return self.get_base_id(action) 130 | 131 | def get_description(self, action): 132 | base_id = self.get_base_id(action) 133 | base_id = base_id.replace('_', ' ').replace('-', ' ') 134 | return base_id[0].upper() + base_id[1:] 135 | 136 | def get_nargs(self, action): 137 | if action.nargs is None: 138 | return 1 139 | if type(action.nargs) == int: 140 | return action.nargs 141 | if action.nargs == '?' or action.nargs == '+' or action.nargs == '*': 142 | return action.nargs 143 | if hasattr(action.nargs, 'isdigit') and action.nargs.isdigit(): 144 | return int(action.nargs) 145 | return 1 146 | 147 | def get_help(self, action): 148 | if not action.help: 149 | return "" 150 | return action.help 151 | 152 | def get_disposition(self, action): 153 | if len(action.option_strings): 154 | return "optional" 155 | else: 156 | return "positional" 157 | 158 | def get_subparser(self, action): 159 | return isinstance(action, argparse._SubParsersAction) 160 | 161 | def get_multiple(self, action): 162 | return self.multiple_args(action.nargs) 163 | 164 | def get_choices(self, action): 165 | return bool(action.choices) 166 | 167 | def filter_input_object(self, action): 168 | if isinstance(action, argparse._VersionAction): 169 | return True 170 | if isinstance(action, argparse._HelpAction): 171 | return True 172 | return False 173 | 174 | # TODO: maybe this function should move to be near the opposite in webuipage.POST 175 | def get_input_object(self, action, prefix): 176 | input_parameters = {} 177 | input_parameters['class'] = self.get_class(prefix) 178 | input_parameters['name'] = self.get_id(action, prefix) 179 | input_parameters['id'] = self.get_id(action, prefix) 180 | 181 | input_type = web.form.Textbox 182 | 183 | if self.get_choices(action): 184 | input_type = web.form.Dropdown 185 | input_parameters['args'] = [choice for choice in action.choices] 186 | if self.get_multiple(action): 187 | input_parameters['multiple'] = 'multiple' 188 | input_parameters['size'] = 4 189 | elif isinstance(action, (argparse._StoreTrueAction, argparse._StoreFalseAction, argparse._StoreConstAction)): 190 | input_type = web.form.Checkbox 191 | input_parameters['checked'] = True if action.default else False 192 | input_parameters['value'] = action.const 193 | else: 194 | input_parameters['value'] = action.default if action.default else "" 195 | 196 | if isinstance(action, argparse._SubParsersAction): 197 | input_parameters['onChange'] = "javascript: update_show(this);" 198 | input_parameters['value'] = action.choices.keys()[0] 199 | 200 | if len(action.option_strings): 201 | input_parameters['default'] = action.default 202 | # if optional argument may be present with either 1 or no parameters, the default shifts 203 | # to being the no parameter's value. this is mearly to properly display actual values to the user 204 | if action.nargs == '?': 205 | input_parameters['value'] = action.const 206 | 207 | # TODO: support these actions: append, append_const, count 208 | self._actions[self.get_id(action, prefix)] = action 209 | input_object = input_type(**input_parameters) 210 | 211 | input_object.description = self.get_description(action) 212 | input_object.nargs = self.get_nargs(action) 213 | input_object.help = self.get_help(action) 214 | input_object.disposition = self.get_disposition(action) 215 | input_object.subparser = self.get_subparser(action) 216 | input_object.choices = self.get_choices(action) 217 | 218 | return input_object 219 | 220 | def get_form_inputs(self, parser=None, prefix=[]): 221 | inputs = [] 222 | 223 | if parser is None: 224 | parser = self._parser 225 | 226 | group_actions = [group_actions 227 | for group_actions in parser._mutually_exclusive_groups] 228 | actions = [action 229 | for action in parser._actions 230 | if action not in group_actions] 231 | 232 | for action in actions: 233 | if not self.filter_input_object(action): 234 | inputs.append(self.get_input_object(action, prefix)) 235 | 236 | if isinstance(action, argparse._SubParsersAction): 237 | for choice_name, choice_parser in action.choices.items(): 238 | inputs.extend(self.get_form_inputs(choice_parser, prefix + [choice_name])) 239 | 240 | return inputs 241 | --------------------------------------------------------------------------------