Friday, March 4, 2011

Emitting headers from a tiny Python web-framework

I am writing a web-framework for Python, of which the goal is to be as "small" as possible (currently under 100 lines of code).. You can see the current code on github

Basically it's written to be as simple to use as possible. An example "Hello World" like site:

from pyerweb import GET, runner

@GET("/")
def index():
    return "<strong>This</strong> would be the output HTML for the URL / "

@GET("/view/([0-9]+?)$")
def view_something(id):
    return "Viewing id %s" % (id) # URL /view/123 would output "Viewing id 123"

runner(url = "/", # url would be from a web server, in actual use
       output_helper = "html_tidy" # run returned HTML though "HTML tidy"

Basically you have a function that returns HTML, and the GET decorator maps this to a URL.

When runner() is called, each decorated function is checked, if the URL regex matches the request URL, the function is run, and the output is sent to the browser.

Now, the problem - outputting headers. Currently for development I've just put a line before the runner() call which does print Content-type:text/html\n - this is obviously a bit limiting..

My first ideas was to have the functions return a dict, something like..

@GET("/")
def index():
    return {
        "html": "<html><body>...</body></html>",
        "headers": {"Location":"http://google.com"}
    }

I really don't like this - having to return a dict with a specifically named key isn't nearly as nice as just returning a string..

I could check if the returned data is a dict, if so use returned_data['html'] as the output, if it's a string, there is no custom headers to be sent... but this means to go from no headers (which would be the case a huge majority of the time) to headers, you'd have to change the return function from return my_html to return {'html':my_html} which isn't very elegant either..

After writing this, I discovered "Sinatra" - a similar-in-use Ruby library, and looked at how it dealt with headers:

get "/" do
    content_type 'text/css', :charset => 'utf-8'
end

This seems like it could be nice enough in Python:

@GET("/")
def index():
    header("location", "http://google.com")

To implement this, I was considering changing how the functions are executed - instead of simply using the return value, I would change sys.stdout to a StringIO, so you could do..

def index():
    print "<html>"
    print "<head><title>Something</title></head>"
    print "<body>...</body>"
    print "</html>

..without having to worry about concatenating a bunch of strings together. The upshot of this is I could have a separate stream for headers, so the above header() function would write to this.. Something like:

def header(name, value):
    pyerweb.header_stream.write("%s: %s" % (name, value))

Basically, the question is, how would you output headers from this web-framework (mostly in terms of use, but to a lesser extent implementation)?

From stackoverflow
  • You should reconsider the notion of returning HTML - headers are part of HTTP. If you build your framework around an HTTP stream, the headers are simply lines that precede the HTML payload.

    A headers example from the above link:

    HTTP/1.1 200 OK
    Date: Mon, 23 May 2005 22:38:34 GMT
    Server: Apache/1.3.3.7 (Unix)  (Red-Hat/Linux)
    Last-Modified: Wed, 08 Jan 2003 23:11:55 GMT
    Etag: "3f80f-1b6-3e1cb03b"
    Accept-Ranges: bytes
    Content-Length: 438
    Connection: close
    Content-Type: text/html; charset=UTF-8
    

    For a Python example, see the implementaion of BaseHTTPRequestHandler.send_header(keyword, value).

    dbr : This framework isn't "designed around an HTTP stream", it's designed to be a nice to use as possible. Making the user manually print headers followed by two carriage-returns would be not-nice to use, and difficult to make send default-headers.. I see your point, but I want to abstract HTTP-stuff
  • you could use that idea of returning a dict or a string, but add a new decorator, so the 'evolution' for a user would be:

    simple html:

    @GET("/")
    def index():
    return "<html><body>...</body></html>"
    

    with constant headers (one @HEADER for each one, or a dict with all of them):

    @GET("/")
    @HEADER("Location","http://google.com")
    def index():
    return "<html><body>...</body></html>"
    

    with complex, maybe calculated headers:

    @GET("/")
    def index():
    return {
        "html": "<html><body>...</body></html>",
        "headers": {"Location":"http://google.com"}
    }
    

    the @HEADER() decorator would simply change the returned value, so the 'framework' code would stay simple.

  • Look at PEP 333 for an excellent design pattern for a very lightweight web server. If your server has this exact API, you can reuse it in a lot of context with a lot of other products.

    PEP 333 (WSGI) suggests that you don't directly return the page, but you provide the HTML page to a "start_response" callable object, which wraps your HTML in the proper HTTP response, with the proper headers.

0 comments:

Post a Comment