"""Support for creating WSGI handlers from functions.
This module makes it easy to create a WSGI handler whose implementation is a
function with a convenient signature. The function body does not need to
deal with the WSGI interface directly but can instead accept parameters
corresponding to whatever information it needs from the request and return
a result which is a data structure representing the HTML page to be rendered.
There are also routines making it easy to create handlers which return PDF,
ZIP, CSV, or text files.
"""
import json
from urllib.parse import quote
from . import parameter
[docs]class FinalHandler (object):
"""A handler class to create handlers with convenient signature.
This allows the body of a handler to be written as a function which takes
parameter values extracted from the environment by a number of standard
methods. The return value of the function can be converted into an actual
CGI response by any desired conversion routine.
"""
def __init__ (self, handler, convert,
use_set_status=False, use_add_header=False):
"""
handler (**params) -> response: the request handler
convert (environ, outer_headers, *result) -> wsgi response
use_set_status indicates whether or not to provide a set_status
parameter to allow the nested function to provide a different HTTP
status than the default 200 OK.
use_add_header indicates whether or not to provide an add_header
parameter to allow the nested function to add an extra header to
the HTTP response.
"""
self.__handler = handler
self.__convert = convert
self.__use_set_status = use_set_status
self.__use_add_header = use_add_header
@property
def handler (self):
"""The handler function.
Note that this is NOT a WSGI handler. Instead the parameters it
expects are determined by the parameters expected to be set by
previous handlers in the handler chain.
"""
return self.__handler
@property
def convert (self):
"""The result conversion routine.
This converts the result of the handler function into a proper
return value for a WSGI handler.
"""
return self.__convert
@property
def use_set_status (self):
"""Whether the handler function needs to set the HTTP status.
"""
return self.__use_set_status
@property
def use_add_header (self):
"""Whether the handler function needs to add HTTP headers.
"""
return self.__use_add_header
def __call__ (self, environ, start_response):
outer_status = ["200 OK"]
outer_headers = []
params = parameter.get_params (environ)
if self.use_set_status:
def set_status (status):
outer_status[0] = status
params['set_status'] = set_status
if self.use_add_header:
def add_header (header):
outer_headers.append (header)
params['add_header'] = add_header
result = self.handler (**params)
response = self.convert (environ, outer_headers, result)
start_response (outer_status[0], outer_headers)
return [response]
[docs]def return_html_template (handler, template):
def result (environ, start_response):
environ['template'] = template
return handler (environ, start_response)
return result
[docs]def return_html (handler, encoding='utf-8',
use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning HTML using a template.
The handler implementation is expected to return a 2-tuple consisting of
the page title and the page contents. The final response is obtained by
passing the results through a template found in the 'template' environment
entry and converting the result to bytes using the specified encoding.
This is meant to work with the ll.xist XML/HTML library.
"""
def convert (environ, outer_headers, result):
title, page = result
response = environ['template'] (title, page, environ).bytes (encoding=encoding)
outer_headers.append (('Content-Type', 'text/html; charset=%s' % encoding))
outer_headers.append (('Content-Length', str (len (response))))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_json (handler, encoding='utf-8', use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a JSON result.
The handler implementation is expected to return an object that can be
written as JSON using :func:`json.dumps()`.
"""
def convert (environ, outer_headers, json_object):
response = json.dumps (json_object).encode (encoding=encoding)
outer_headers.append (('Content-Type', 'application/json; charset=%s' % encoding))
outer_headers.append (('Content-Length', str (len (response))))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_pdf (handler, use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a PDF file.
The handler implementation is expected to return a 2-tuple consisting of
the PDF file content bytes and filename. The HTTP response sets the
Content-Length header to the length of the PDF file contents.
"""
def convert (environ, outer_headers, result):
response, filename = result
response = bytes (response)
outer_headers.append (('Content-Type', 'application/pdf'))
outer_headers.append (('Content-Length', str (len (response))))
outer_headers.append (header_content_disposition (filename))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_png (handler, use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a PNG file.
The handler implementation is expected to return a 2-tuple consisting of
the PNG file content bytes and filename. The HTTP response sets the
Content-Length header to the length of the PNG file contents.
"""
def convert (environ, outer_headers, result):
response, filename = result
response = bytes (response)
outer_headers.append (('Content-Type', 'image/png'))
outer_headers.append (('Content-Length', str (len (response))))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_zip (handler, use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a ZIP file.
The handler implementation is expected to return a 2-tuple consisting of
the .zip file content bytes and filename. The HTTP response sets the
Content-Length header to the length of the .zip file contents.
"""
def convert (environ, outer_headers, result):
response, filename = result
outer_headers.append (('Content-Type', 'application/zip'))
outer_headers.append (('Content-Length', str (len (response))))
outer_headers.append (header_content_disposition (filename))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_csv (handler, encoding='utf-8', use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a CSV file.
The handler implementation is expected to return a 2-tuple consisting of
the .csv file content text and filename. The HTTP response sets the
Content-Length header to the length of the encoded .csv file contents.
"""
def convert (environ, outer_headers, result):
response, filename = result
response = response.encode (encoding)
outer_headers.append (('Content-Type', 'text/csv; charset=%s' % encoding))
outer_headers.append (('Content-Length', str (len (response))))
outer_headers.append (header_content_disposition (filename))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_text (handler, encoding='utf-8',
use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a text file.
The handler implementation is expected to return just the file contents.
The HTTP response sets the Content-Length header to the length of the text
file contents once encoded as bytes.
"""
def convert (environ, outer_headers, page):
response = page.encode (encoding)
outer_headers.append (('Content-Type', 'text/plain; charset=%s' % encoding))
outer_headers.append (('Content-Length', str (len (response))))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)
[docs]def return_text_download (handler, encoding='utf-8',
use_set_status=False, use_add_header=False):
"""A FinalHandler subclass for returning a text file as a download.
The handler implementation is expected to return the file contents and
a suggested filename as which it should be saved.
The HTTP response sets the Content-Length header to the length of the text
file contents once encoded as bytes.
"""
def convert (environ, outer_headers, result):
response, filename = result
response = response.encode (encoding)
outer_headers.append (('Content-Type', 'text/plain; charset=%s' % encoding))
outer_headers.append (('Content-Length', str (len (response))))
outer_headers.append (header_content_disposition (filename))
return response
return FinalHandler (handler, convert, use_set_status, use_add_header)