blog.humaneguitarist.org

easy command line and RESTful interfaces to Python functions with a Plac intensifier

[Mon, 03 Sep 2018 17:04:24 +0000]
For work, I've been using the excellent Plac [https://pypi.org/project/plac/] module for Python to make it easy to add command line interfaces to scripts. Because of our desire to have a web interface to our scripts, too, I started thinking about how to make it easy to do that with the same ease that Plac provides command line interfaces. But first, take this simple code example to see Plac in action: #!/usr/bin/python 3 # hello_you.py import plac def main(name: ("your first name"), exclaim: ("use an exclamation mark", "flag", "x")=False,): """Prints/Returns "Hello @name."\ \nEx: `python3 hello_you.py Nitin -x`""" punc = "." if not exclaim else "!" greet = "Hello {}{}".format(name, punc) print(greet) return greet if __name__ == "__main__": plac.call(main) Now, here are some usage examples: > py -3 .\hello_you.py -h usage: hello_you.py [-h] [-x] name Prints/Returns "Hello @name." Ex: `python3 hello_you.py Nitin -x` positional arguments: name your first name optional arguments: -h, --help show this help message and exit -x, --exclaim use an exclamation mark > py -3 .\hello_you.py Nitin Hello Nitin. > py -3 .\hello_you.py Nitin -x Hello Nitin! Now, here's the same script but using a wrapper around Plac that I'm calling Placissimo. #!/usr/bin/python 3 # hello_you.py import placissimo def main(name: ("your first name"), exclaim: ("use an exclamation mark", "flag", "x")=False,): """Prints/Returns "Hello @name."\ \nEx: `python3 hello_you.py Nitin -x`""" punc = "." if not exclaim else "!" greet = "Hello {}{}".format(name, punc) print(greet) return greet if __name__ == "__main__": placissimo.call(main) The only differences are the import statement and the .call function's namespace. With this wrapper, the usage examples above will work exactly the same. But there's one difference. It lets me do this: > py -3 .\hello_you.py servissimo -h usage: hello_you.py [-h] server [host] [port] Creates an HTTP server at @port. positional arguments: server trigger to launch a server host host to use port port number to use optional arguments: -h, --help show this help message and exit As you can see the servissimo command changes the behavior. Instead of using the command line version, I can launch a web server with a default host of localhost:8080 . Let me do that: py -3 .\hello_you.py servissimo <Flask 'hello_you'> 2018-09-03 12:32:58,767 - werkzeug - INFO - * Running on http://localhost:8080/ (Press CTRL+C to quit) Now, I can go to localhost:8080 in my browser and I'll see a JSON response: { "code": 1, "input": {}, "output": "TypeError(\"main() missing 1 required positional argument: 'name'\",)" } Let me pass in the required argument a la localhost:8080/?name=Nitin : { "code": 0, "input": { "name": "Nitin" }, "output": "Hello Nitin." } I'll also try localhost:8080/?name=Nitin&exclaim=True : { "code": 0, "input": { "name": "Nitin", "exclaim": "True" }, "output": "Hello Nitin!" } Note that I had to use the long hand argument exclaim instead of the short hand x as I did in the command line. I also had to explicitly use =True . Big deal. For now, I'm only supporting GET arguments, but supporting POST as well won't be hard. I also have it using the inspect [https://docs.python.org/3/library/inspect.html] module to support some online help at localhost:8080/info : { "module": { "name": "hello_you" }, "function": { "name": "main", "help": "Prints/Returns \"Hello @name.\" \nEx: `python3 hello_you.py Nitin -x`", "args": { "name": "your first name", "exclaim": [ "use an exclamation mark", "flag", "x" ] } } } I'm also going to have this add handlers to the root logger. That way, if this is being used to provide command line or web access to a function that uses logging, I can route all logging to a file, etc. I also want to add an option to launch a websocket server that will log as JSON and emit the tail of the JSON log. This will allow someone to connect to the socket server in their HTML/JavaScript page and show logging in near real-time. Anyway, I think this is something I'm going to really flesh out. I think it'll be a useful and easy way to simultaneously create command line and RESTful interfaces to a Python script.