39

I want to create a very simple HTML/AJAX based GUI for a Python program. So the frontend is a HTML page which communicates with the program via AJAX. Can you give me a minimal implementation for the server-side using the python SimpleHTTPServer.SimpleHTTPRequestHandler?

A simple example would be a textfield and a button. When the button is pressed the content of the field is send to the server which then sends back a corresponding answer. I am aware that there are many powerful solutions for this in Python, but I would like to keep this very simple. I already found some nice examples for such a server (e.g. here), but so far I could not come up with a truly minimal one.

In case you wonder why I want to implement the GUI in such a way: My focus for this application is to display lots of data in a nice layout with only minimal interaction - so using HTML+CSS seems most convenient (and I have been already using it for non-interactive data display).

nikow
  • 21,190
  • 7
  • 49
  • 70

4 Answers4

53

O.K., I think I can now answer my own question. Here is an example implementation for calculating the square of a number on the server. Please let me know if there are any improvements or misconceptions.

the python server file:

import threading
import webbrowser
import BaseHTTPServer
import SimpleHTTPServer

FILE = 'frontend.html'
PORT = 8080


class TestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
    """The test example handler."""

    def do_POST(self):
        """Handle a post request by returning the square of the number."""
        length = int(self.headers.getheader('content-length'))        
        data_string = self.rfile.read(length)
        try:
            result = int(data_string) ** 2
        except:
            result = 'error'
        self.wfile.write(result)


def open_browser():
    """Start a browser after waiting for half a second."""
    def _open_browser():
        webbrowser.open('http://localhost:%s/%s' % (PORT, FILE))
    thread = threading.Timer(0.5, _open_browser)
    thread.start()

def start_server():
    """Start the server."""
    server_address = ("", PORT)
    server = BaseHTTPServer.HTTPServer(server_address, TestHandler)
    server.serve_forever()

if __name__ == "__main__":
    open_browser()
    start_server()

...and the HTML file (I call it 'frontend.html', unfortunately the name has to appear in the JavaScript code as well):

<html>
<head>
<title>AJAX test</title>
</head>
<body>
<script type="text/javascript">

function xml_http_post(url, data, callback) {
    var req = false;
    try {
        // Firefox, Opera 8.0+, Safari
        req = new XMLHttpRequest();
    }
    catch (e) {
        // Internet Explorer
        try {
            req = new ActiveXObject("Msxml2.XMLHTTP");
        }
        catch (e) {
            try {
                req = new ActiveXObject("Microsoft.XMLHTTP");
            }
            catch (e) {
                alert("Your browser does not support AJAX!");
                return false;
            }
        }
    }
    req.open("POST", url, true);
    req.onreadystatechange = function() {
        if (req.readyState == 4) {
            callback(req);
        }
    }
    req.send(data);
}

function test_button() {
    var data = document.test_form.test_text.value;           
    xml_http_post("frontend.html", data, test_handle)
}

function test_handle(req) {
    var elem = document.getElementById('test_result')
    elem.innerHTML =  req.responseText
}

</script>

<form name=test_form>
sqr(
<input type="text" name="test_text" value="0" size="4">
) =
<span id="test_result">0</span>
<input type=button onClick="test_button();" value="start" title="start">
</form>

</body>
</html>

Of course it would be much more convenient to use jQuery for the XML request, but in the interest of simplicity I'll leave it like that.

Finally an alternative implementation using WSGI (unfortunately I didn't see a way to fall back on the standard file-serving handler if the request is not a POST):

import threading
import webbrowser
from wsgiref.simple_server import make_server

FILE = 'frontend.html'
PORT = 8080

def test_app(environ, start_response):
    if environ['REQUEST_METHOD'] == 'POST':
        try:
            request_body_size = int(environ['CONTENT_LENGTH'])
            request_body = environ['wsgi.input'].read(request_body_size)
        except (TypeError, ValueError):
            request_body = "0"
        try:
            response_body = str(int(request_body) ** 2)
        except:
            response_body = "error"
        status = '200 OK'
        headers = [('Content-type', 'text/plain')]
        start_response(status, headers)
        return [response_body]
    else:
        response_body = open(FILE).read()
        status = '200 OK'
        headers = [('Content-type', 'text/html'),
                   ('Content-Length', str(len(response_body)))]
        start_response(status, headers)
        return [response_body]

def open_browser():
    """Start a browser after waiting for half a second."""
    def _open_browser():
        webbrowser.open('http://localhost:%s/%s' % (PORT, FILE))
    thread = threading.Timer(0.5, _open_browser)
    thread.start()

def start_server():
    """Start the server."""
    httpd = make_server("", PORT, test_app)
    httpd.serve_forever()

if __name__ == "__main__":
    open_browser()
    start_server()
nikow
  • 21,190
  • 7
  • 49
  • 70
  • 2
    Just for comparison, here's a Ramaze example: http://news.ycombinator.com/item?id=383960 – jfs Dec 03 '08 at 20:19
  • "is there any way to fall back on the standard Python handler if the request is not a POST?" Doesn't mean much. What do you think the "standard" handler is? – S.Lott Dec 03 '08 at 21:08
  • S.Lott: In the first server implementation the behavior of SimpleHTTPRequestHandler is only changed for POST requests. Loading the the HTML file therefore does not require any additional code. In the WSGI implementation I have to explicitly send the HTML, the GET is not handled automatically. – nikow Dec 03 '08 at 22:22
  • 1
    This example was working up until the latest update of Google Chrome (55.0.2883) which now returns the error `net::ERR_INVALID_HTTP_RESPONSE`. Searching around the net, other applications seem to be having similar issues with this build of chrome, specifically with non-standard ports. Any ideas? – SkyNT Dec 17 '16 at 05:12
  • I did a python3 version : https://gitlab.com/Hugo-Trentesaux/simple-api – Hugo Trentesaux Dec 14 '18 at 23:08
  • `req.responseText` is empty, while `result` was not. Same issue is posted in https://stackoverflow.com/q/40314747/11769765. – Friedrich -- Слава Україні Oct 18 '20 at 13:33
11

Use the WSGI reference implementation. In the long run, you'll be happier.

from wsgiref.simple_server import make_server, demo_app

httpd = make_server('', 8000, demo_app)
print "Serving HTTP on port 8000..."

# Respond to requests until process is killed
httpd.serve_forever()

The demo_app is relatively easy to write; it handles your Ajax requests.

Jurn Ho
  • 69
  • 1
  • 5
S.Lott
  • 384,516
  • 81
  • 508
  • 779
3

Here is a simple example for Python 3 based on @nikow's example

I know this can have errors, comment what those are if you find them.

The code sends the string "I sent you this message" when you click run, python responds with "I got it"

Html Code

(you're gonna have to use the js console for this)

<body>
<button id="runButton">Run</button>
<script type="text/javascript">
function xml_http_post(url, data) {
var req = new XMLHttpRequest();
req.open("POST", url, true);
req.onreadystatechange = function() {
    if (req.readyState == 4) {
    console.log(req.responseText);
    }
}
req.send(data);
}

function runbuttonfunc() {
    xml_http_post("frontend.html", "I sent you this message")
}

document.getElementById("runButton").onclick = runbuttonfunc;
</script>
</body>

Python Code:

import http.server

FILE = 'frontend.html'
PORT = 8000


class TestHandler(http.server.SimpleHTTPRequestHandler):
    """The test example handler."""

    def do_POST(self):
        """Handle a post request by returning the square of the number."""
        print(self.headers)
        length = int(self.headers.get_all('content-length')[0])
        print(self.headers.get_all('content-length'))
        data_string = self.rfile.read(length)
        print(data_string)
        self.send_response(200)
        self.send_header("Content-type", "text/plain")
        self.end_headers()
        self.flush_headers()
        self.wfile.write("I got it!".encode())


def start_server():
    """Start the server."""
    server_address = ("", PORT)
    server = http.server.HTTPServer(server_address, TestHandler)
    server.serve_forever()

start_server()
jsotola
  • 2,238
  • 1
  • 10
  • 22
Aditya Shankar
  • 702
  • 6
  • 12
0

Thanks for a very intuitive example @nikow I was trying to follow your example, but did get an error:

(process:10281): GLib-CRITICAL **: g_slice_set_config: assertion 'sys_page_size == 0' failed

I modified your code to meet my needs.

webbrowser.open('file:///home/jon/workspace/webpages/frontend_example/%s' % FILE)
// skipped the port part
httpd = make_server("", 8080, test_app)
// hardcoded it here.

does my html file has to be put on the webserver ? I have not put it there yet !.

JonB
  • 804
  • 3
  • 12
  • 40
  • Yes, in my example the file is served by the webserver, so it has to be available there. – nikow Nov 27 '14 at 14:20