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

For work, I've been using the excellent 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 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.

--------------

Related Content:

Leave a Comment

Your email address will not be published. Required fields are marked *

*